排序,我想大家一定经历过或者正在经历着。或许你不懂算法,对排序算法一无所知,但是你一定用过一些第三方库的api来一键排序,那么,在你享受便捷的同时,你是否想过它的底层是如何实现的?这样的算法实现方式是不是最好的?还有没有其它的可能性来实现更快速的排序?那么,希望这一篇文章过后。对于排序算法,你不会再觉得陌生和迷惑。
这篇文章会介绍一些简单常用的排序算法,比如我们耳熟能详的冒泡排序,以及选择排序、插入排序、归并排序等等等等。当然,你一旦学会了这些算法在js中的实现方式,其实你也就弄懂了这种算法。就算以后要用其它语言来实现这些算法,也不过就是一些语言特性上的差别罢了。
我们会专门写一个数组类,并在其中加入各种排序算法。那么,我们先开始搭一个简单的架子。
function ArrayList() { var array = []; this.insert = function (item) { array.push(item); }; this.toString = function() { return array.join(); }; }
我们构建一个数组类,并且只有一个insert和toString方法,以便我们输入数组元素和打印数组元素。
下面我们为这个数组类添加各种排序方法。我们先从最简单的开始。
1、冒泡排序
冒泡排序十分简单,就是比较数组中任何两个相邻的元素,如果第一个比第二个大,那么就交换两个元素的位置。这样,较大值得元素就会一点一点移动到正确的位置,就像气泡升至表面一样。我们先来看一下代码。
//交换数组中两个相邻元素的方法,传入的是相关的数组,以及相邻两个元素的下标。 var swap = function (array,index1,index2) { // 这里,是最“普通”的方式,通过一个中间量来存储index1元素,因为要把index1的值设置为index2,然后再把index2的值设置为刚才存储index1的aux变量。 // 希望我上面说的足够清楚。 var aux = array[index1]; array[index1] = array[index2]; array[index2] = aux; // 但是我们可以有更为简便的方式来做到替换两个数组元素位置的方式。 // 一个是下面的ES6新增的方法。数组的解构赋值。不多说,大家可以自行去查看。 //[array[index1],array[index2]] = [array[index2],array[index1]]; // 另外一个方法是利用数组的splice方法,删除从index1开始的两个元素,并且在删除的位置插入index2,和index1以达到替换元素的目的。
// 如果你对数组方法还不是很清楚,请看这里用js来实现那些数据结构02(数组篇02-数组方法)
//array.splice(index1,2,array[index2],array[index1]); }; // 这是最简单,当然也是最慢的排序方法,我们需要两层循环来判断值得大小,以此来一步一步确定每一个值得位置。 // 这里我要刨根问底一下,为什么外层循环(i)循环的是数组元素的长度,而内层(j)是比数组长度少1的循环次数? // 因为这是可以保证每两个元素都进行过比较的最小的循环次数,不信你把两次循环的次数增加(length +100000);来试一下,发现结果仍旧是我们想要的。(当然,这更耗费时间。) // 但是你把次数减得更少就不行了,排序的结果就不对了(其实这里可以合理的减少内层循环的次数,后面说)。你还可以这么理解,外层循环控制我们有多少个数需要比较,内层循环去具体的操作两个数的比较。 this.bubbleSort = function () { var length = array.length; for(var i = 0; i < length; i++) { //i=0,1,2,3,4...length-1; //console.log(i,"i"); for(var j = 0; j < length-1;j++) {//j = 0,1,2,3,4...length - 1 - 1; //console.log(j,"j"); if(array[j] > array[j+1]){ swap(array,j,j+1) } } } }; //我们来写一个简单的方法生成一个未排序的arraylist。//这个方法是在构造函数外的。你不用也可以。只是为了方便生成一个数组罢了。 function creatArrayList (size) { var array = new ArrayList(); for(var i = size; i > 0; i--) { array.insert(i); } return array; } var arraylist = creatArrayList(5); console.log(arraylist.toString());//5,4,3,2,1 arraylist.bubbleSort(); console.log(arraylist.toString());//1,2,3,4,5
上面的解释我相信已经够详细了,我们下面接着看看是否可以改进一下这垃圾的耗时间的效率低的冒泡排序,让我们在简单实现的基础上提高一点点性能?
//咱们看看modifiedBubbleSort和bubbleSort的区别,唯一不同的地方就在于内层循环的时候在for循环的第二个条件中多减了一个i。这么做的用意是什么呢? // 我们一点一点来捋一下这点点代码。假设我们的数组是【5,4,3,2,1】;当i = 0的时候(第一次外循环),我们拿5去依次和4,3,2,1来比较,最后数组渐变成了【4,3,2,1,5】; // 那么此时,5就是最大的,当i=1的时候(第二次外循环)。此时我们的内循环就不再需要去拿当前的j去与5(也就是数组中确定了的最大的)比较。 // 外层每循环一次,内层中的每个元素就相互交换了一下位置(如果符合条件的话),最终每一次内层循环完毕,都会确定一个当前轮数最大的值。 // 那么既然我们已经知道最大的值是什么,就无需在后面的循环轮数中再去和已经确定了位置的值去做比较了。这样就可以提高一点我们的执行效率。 // 这里再多句嘴,当i = 0时,j = 0,j < length - 1 - 0;当i = 1时,j = 0,j < length - 1 - 1;(要理解这句话) this.modifiedBubbleSort = function () { var length = array.length; for(var i = 0; i < length; i++) { for(var j = 0; j < length - 1 - i;j++) { if(array[j] > array[j+1]) { swap(array,j,j+1); } } } };
改进后的冒泡排序我们也学会了,但是也就只能这样了。没办法再进一步的进行优化和效率的提升。冒泡排序,是最基础的,最不推荐的排序方式。因为它的时间复杂度是O(n2),大O表示法,我们会在后面的内容中详细的讲解什么是大O表示法。这里可以暂时的理解为,两层循环形成了i*j的计算结果,也就是length*length的循环总次数。也就是n2.。(事实上就是这么回事)。如果是三层循环,那很有可能就是O(n3)的复杂度了。
2、选择排序
选择排序的思想是,找到数据结构中最小值并将其放在第一位,接着找到第二小的值,放到第二位,以此类推。(当然你也可以找最大的值放在最后一位)。我们还是来看代码。
//其实选择排序也并不复杂,我们来一起看一看。 this.selectionSort = function () { //首先,我们声明一个存储数组长度的变量,以及一个存储当前最小值的变量,哦不对,存储当前最小值的对应下标的变量。 var length = array.length,indexMin; // 在外层循环,我们循环次数是整个数组的长度。 for(var i = 0; i < length - 1; i++) { // 这里,最开始的循环我们也不知道谁是最小的,所以我们就把第一个值得下标作为最小值得下标。 indexMin = i; // 内层循环,我们会依次比较当前最小值(也就是indexMin对应的值)和数组中的其它值。 // 这里,为什么是j=i呢? // 我们每一次外层循环,都会确定一个最小值并把最小值放置在相应的位置(从下标0开始每次外循环都会往后加1)。 // 那么我们就不需要再去比较开始循环过比较过的下标了,所以我们每次外层循环过后,内层循环都从i的位置开始就可以了。 // 有点类似于modifiedBubbleSort的j<length - 1 - i; for(var j = i; j < length; j++) { // 这样,我们就可以判断出最小值是什么,如果indexMin所对应的值比j所对应的值还要大,说明最小值对应的下标应该为j。 if(array[indexMin] > array[j]) { indexMin = j; } } // 在外层循环一次结束后,如果i(最开始我们确定的最小值)不等于indexMin。这说明i并不是最小值。我们就交换两个值的位置。 // 如果相等,说明当前的indexMin就是最小值,无需交换位置。 console.log(i,indexMin) if(i !== indexMin) { swap(array,i,indexMin) } } };
不过选择排序的复杂度也是O(n2),效率并不是很好。那么我们继续往下看。
3、插入排序
插入排序,怎么说呢....就是假设数组中的第一个元素是已经排序过的了(不假设不行,或者说它就是排过序的了,因为就一个元素嘛),那么我们和第二个元素比较,第二个元素是应该在第一个元素之前,还是在原位置不动呢?也就是说,第一个元素和第二个元素比较大小来确定这两个元素的位置。那么这样,单纯就数组元素的前两项来说,他们是排好序的了。那么我们再以第三个元素跟前两个元素进行比较,来确定第三个元素应该插入在前两项元素的什么位置。
简单来说,我们可以认为在排序数组中有一个已排序的子数组,我们依次用后面的元素与子数组中的元素进行比较,以确定后面的元素应该插入到子数组的什么位置。最后,我们就会得到一个完全排序的数组了。
我们继续来看代码。
this.insertionSort = function () { //j和temp分别用来存储当前的下标和当前下标所对应的值。 var length = array.length,j,temp; // 为什么i要从1开始呢?因为index为0的元素我们视为已经排序过的了。 for(var i = 1; i < length; i++) { // 我们用j来存储当前要比较的值的下标也就是i,因为0上的元素已经排序过了(我们默认这样做的)。 j = i; // 同样,我们要比较当前索引的值得大小来确定是否需要换位,所以我们还要有一个临时存储当前下标所对应的值的变量。 temp = array[i]; // 如果j>0说明是数组中的元素, // 并且,如果当前j(i)的前一个元素(j-1)比当前的变量大,那么就把j(i)的值设置为j-1的值,也就是把j(i)的位置往后挪了一个。 // 直到array[j - 1] > temp为false为止。为什么不说j>0这个条件呢?因为这是保证数组正确对比的一个防护层,当然,它是很重要的。 // 这里有一个十分必要且需要注意的point,就是我们的变量j的值的问题。 // 我们在循环直到条件不成立跳出循环的时候,此时的j就是需要把临时存储array[i]的值(也就是temp)插入的地方。 // 因为,我们在while循环中,每次循环都会用j--来使下标的位置一点点往前移动,直到条件不成立后,我们得到了一个应该插入temp的位置的j。 while (j > 0 && array[j - 1] > temp) { array[j] = array[j - 1]; j--; } array[j] = temp; } };
上面的代码就是插入排序了,其中要注意的点就在while循环这一块,需要花费一点心思去理解一下。我在简单的啰嗦两句,其实在代码中,我们声明了j和temp变量用来存储当前的下标和其所对应的值。那么temp的作用是我们可以在找到该插入的位置的时候,可以知道应该插入的值是什么,而j的存在的意义是确定这个位置是哪里。所以,我们在while循环中会拿递减的j所对应的值的前一个去和temp比较,如果条件成立,那么我就往后挪,直到挪不动为止(while循环的条件不匹配),我们就找到了应该插入temp位置的j。这时候在该位置上插入temp就可以了。
那么,简单的排序方法就介绍到这里了,下一章我们来看看复杂一点的,但是效率更高的排序算法。
其实我在写这篇文章的时候,一直在纠结要不要去画画图,让大家可以更容易的去理解这些代码和这些排序算法的实现方式,但是,我在网上搜了一下。一大堆!!所以,我就觉得算了吧。不过文中我已经附上了相关的链接地址,其中有对该算法的概念的更为详细的解释。
最后,由于本人水平有限,能力与大神仍相差甚远,若有错误或不明之处,还望大家不吝赐教指正。非常感谢!
一只想要飞得更高的小菜鸟