最近几天在系统的复习排序算法,之前都没有系统性的学习过,也没有留下过什么笔记,所以很快就忘了,这次好好地学习一下。

首先说明为了减少限制,以下代码通通运行于Node V8引擎而非浏览器,源码在我的GitHub,感兴趣的话可以下载来然后运行试试。

为了方便对比各个排序算法的性能,这里先写了一个生成大规模数组的方法——generateArray

exports.generateArray = function(length) {let arr = Array(length);for(let i=0; i<length; i++) {arr[i] = Math.random();}return arr;
};
复制代码

只需要输入数组长度,即可生成一个符合长度要求的随机数组。

一、冒泡排序

冒泡排序也成为沉淀排序(sinking sort),冒泡排序得名于其排序方式,它遍历整个数组,将数组的每一项与其后一项进行对比,如果不符合要求就交换位置,一共遍历n轮,n为数组的长度。n轮之后,数组得以完全排序。整个过程符合要求的数组项就像气泡从水底冒到水面一样泡到数组末端,所以叫做冒泡排序。

冒泡排序是最简单的排序方法,容易理解、实现简单,但是冒泡排序是效率最低的排序算法,由于算法嵌套了两轮循环(将数组遍历了n遍),所以时间复杂度为O(n^2)。最好的情况下,给出一个已经排序的数组进行冒泡排序,时间复杂度也为O(n)。

特地感谢一下评论中@雪之祈舞的优化,每次冒泡都忽略尾部已经排序好的i项。

JavaScript实现(从小到大排序):

function bubbleSort(arr) {//console.time('BubbleSort');// 获取数组长度,以确定循环次数。let len = arr.length;// 遍历数组len次,以确保数组被完全排序。for(let i=0; i<len; i++) {// 遍历数组的前len-i项,忽略后面的i项(已排序部分)。for(let j=0; j<len - 1 - i; j++) {// 将每一项与后一项进行对比,不符合要求的就换位。if(arr[j] > arr[j+1]) {[arr[j+1], arr[j]] = [arr[j], arr[j+1]];}}}//console.timeEnd('BubbleSort');return arr;
}
复制代码

代码中的注释部分的代码都用于输出排序时间,供测试使用,下文亦如是。

二、选择排序

选择排序是一种原址比较排序法,大致思路:

找到数组中的最小(大)值,并将其放到第一位,然后找到第二小的值放到第二位……以此类推。

JavaScript实现(从小到大排序):

function selectionSort(arr) {//console.time('SelectionSort');// 获取数组长度,确保每一项都被排序。let len = arr.length;// 遍历数组的每一项。for(let i=0; i<len; i++) {// 从数组的当前项开始,因为左边部分的数组项已经被排序。let min = i;for(let j=i; j<len; j++) {if(arr[j]<arr[i]) {min = j;}}if(min !== i) {[arr[min], arr[i]] = [arr[i], arr[min]];}}//console.timeEnd('SelectionSort');return arr;
}
复制代码

由于嵌套了两层循环,其时间复杂度也是O(n^2),

三、插入排序

插入排序是最接近生活的排序,因为我们打牌时就差不多是采用的这种排序方法。该方法从数组的第二项开始遍历数组的n-1项(n为数组长度),遍历过程中对于当前项的左边数组项,依次从右到左进行对比,如果左边选项大于(或小于)当前项,则左边选项向右移动,然后继续对比前一项,直到找到不大于(不小于)自身的选项为止,对于所有大于当前项的选项,都在原来位置的基础上向右移动了一项。

示例:

// 对于如下数组
var arr = [2,1,3,5,4,3];
// 从第二项(即arr[1])开始遍历,
// 第一轮:
// a[0] >= 1为true,a[0]右移,
arr = [2,2,3,5,4,3];
// 然后1赋给a[0],
arr = [1,2,3,5,4,3];
// 然后第二轮:
// a[1] >= 3不成立,该轮遍历结束。
// 第三轮;
// a[2] >= 5不成立,该轮遍历结束。
// 第四轮:
// a[3] >= 4为true,a[3]右移,
arr = [1,2,3,5,5,3];
// a[2] >= 4不成立,将4赋给a[3],然后结束该轮遍历。
arr = [1,2,3,4,5,3];
// a[4] >= 3成立,a[4]右移一位,
arr = [1,2,3,4,5,5];
// arr[3] >= 3成立,arr[3]右移一位,
arr = [1,2,3,4,4,5];
// arr[2] >= 3成立,arr[2]右移一位,
arr = [1,2,3,3,4,5];
// arr[1] >= 3不成立,将3赋给a[2],结束该轮。
arr = [1,2,3,3,4,5];
// 遍历完成,排序结束。
复制代码

如果去掉比较时的等号的话,可以减少一些步骤,所以在JavaScript代码中减少了这部分, JavaScript实现(从小到大排序):

function insertionSort(arr) {//console.time('InsertionSort');let len = arr.length;for(let i=1; i<len; i++) {let j = i;let tmp = arr[i];while(j > 0 && arr[j-1] > tmp) {arr[j] = arr[j-1];j--;}arr[j] = tmp;}//console.timeEnd('InsertionSort');return arr;
}
复制代码

插入排序比一般的高级排序算法(快排、堆排)性能要差,但是还是具有以下优点的:

  • 实现起来简单,理解起来不是很复杂。
  • 对于较小的数据集而言比较高效。
  • 相对于其他复杂度为O(n^2)的排序算法(冒泡、选择)而言更加快速。这一点在文章最后的测试中可以看出来。
  • 稳定、及时……

四、归并排序

到目前为止,已经介绍了三种排序方法,包括冒泡排序、选择排序和插入排序。这三种排序方法的时间复杂度都为O(n^2),其中冒泡排序实现最简单,性能最差,选择排序比冒泡排序稍好,但是还不够,插入排序是这三者中表现最好的,对于小数据集而言效率较高。这些原因导致三者的实用性并不高,都是最基本的简单排序方法,多用于教学,很难用于实际中,从这节开始介绍更加高级的排序算法。

归并排序是第一个可以用于实际的排序算法,前面的三个性能都不够好,归并排序的时间复杂度为O(nlogn),这一点已经由于前面的三个算法了。

值得注意的是,JavaScript中的Array.prototype.sort方法没有规定使用哪种排序算法,允许浏览器自定义,FireFox使用的是归并排序法,而Chrome使用的是快速排序法。

归并排序的核心思想是分治,分治是通过递归地将问题分解成相同或者类型相关的两个或者多个子问题,直到问题简单到足以解决,然后将子问题的解决方案结合起来,解决原始方案的一种思想。

归并排序通过将复杂的数组分解成足够小的数组(只包含一个元素),然后通过合并两个有序数组(单元素数组可认为是有序数组)来达到综合子问题解决方案的目的。所以归并排序的核心在于如何整合两个有序数组,拆分数组只是一个辅助过程。

示例:

// 假设有以下数组,对其进行归并排序使其按从小到大的顺序排列:
var arr = [8,7,6,5];
// 对其进行分解,得到两个数组:
[8,7]和[6,5]
// 然后继续进行分解,分别再得到两个数组,直到数组只包含一个元素:
[8]、[7]、[6]、[5]
// 开始合并数组,得到以下两个数组:
[7,8]和[5,6]
// 继续合并,得到
[5,6,7,8]
// 排序完成
复制代码

JavaScript实现(从小到大排序):

function mergeSort(arr) {//console.time('MergeSort');//let count = 0;console.log(main(arr));//console.timeEnd('MergeSort');//return count;// 主函数。function main(arr) {// 记得添加判断,防止无穷递归导致callstack溢出,此外也是将数组进行分解的终止条件。if(arr.length === 1) return arr;// 从中间开始分解,并构造左边数组和右边数组。let mid = Math.floor(arr.length/2);let left = arr.slice(0, mid);let right = arr.slice(mid);// 开始递归调用。return merge(arguments.callee(left), arguments.callee(right));}// 数组的合并函数,left是左边的有序数组,right是右边的有序数组。function merge(left, right) {// il是左边数组的一个指针,rl是右边数组的一个指针。let il = 0,rl = 0,result = [];// 同时遍历左右两个数组,直到有一个指针超出范围。while(il < left.length && rl < right.length) {//count++;// 左边数组的当前项如果小于右边数组的当前项,那么将左边数组的当前项推入result,反之亦然,同时将推入过的指针右移。if(left[il] < right[rl]) {result.push(left[il++]);}else {result.push(right[rl++]);}}// 记得要将未读完的数组的多余部分读到result。return result.concat(left.slice(il)).concat(right.slice(rl));}
}
复制代码

注意是因为数组被分解成为了只有一个元素的许多子数组,所以merge函数从单个元素的数组开始合并,当合并的数组的元素个数超过1时,即为有序数组,仍然还可以继续使用merge函数进行合并。

归并排序的性能确实达到了应用级别,但是还是有些不足,因为这里的merge函数新建了一个result数组来盛放合并后的数组,导致空间复杂度增加,这里还可以进行优化,使得数组进行原地排序。

五、快速排序

快速排序由Tony Hoare在1959年发明,是当前最为常用的排序方案,如果使用得当,其速度比一般算法可以快两到三倍,比之冒泡排序、选择排序等可以说快成千上万倍。快速排序的复杂度为O(nlogn),其核心思想也是分而治之,它递归地将大数组分解为小数组,直到数组长度为1,不过与归并排序的区别在于其重点在于数组的分解,而归并排序的重点在于数组的合并。

基本思想:

在数组中选取一个参考点(pivot),然后对于数组中的每一项,大于pivot的项都放到数组右边,小于pivot的项都放到左边,左右两边的数组项可以构成两个新的数组(left和right),然后继续分别对left和right进行分解,直到数组长度为1,最后合并(其实没有合并,因为是在原数组的基础上操作的,只是理论上的进行了数组分解)。

基本步骤:

  • (1)首先,选取数组的中间项作为参考点pivot。
  • (2)创建左右两个指针left和right,left指向数组的第一项,right指向最后一项,然后移动左指针,直到其值不小于pivot,然后移动右指针,直到其值不大于pivot。
  • (3)如果left仍然不大于right,交换左右指针的值(指针不交换),然后左指针右移,右指针左移,继续循环直到left大于right才结束,返回left指针的值。
  • (4)根据上一轮分解的结果(left的值),切割数组得到left和right两个数组,然后分别再分解。
  • (5)重复以上过程,直到数组长度为1才结束分解。

JavaScript实现(从小到大排序):

function quickSort(arr) {let left = 0,right = arr.length - 1;//console.time('QuickSort');main(arr, left, right);//console.timeEnd('QuickSort');return arr;function main(arr, left, right) {// 递归结束的条件,直到数组只包含一个元素。if(arr.length === 1) {// 由于是直接修改arr,所以不用返回值。return;}// 获取left指针,准备下一轮分解。let index = partition(arr, left, right);if(left < index - 1) {// 继续分解左边数组。main(arr, left, index - 1);}if(index < right) {// 分解右边数组。main(arr, index, right);}}// 数组分解函数。function partition(arr, left, right) {// 选取中间项为参考点。let pivot = arr[Math.floor((left + right) / 2)];// 循环直到left > right。while(left <= right) {// 持续右移左指针直到其值不小于pivot。while(arr[left] < pivot) {left++;}// 持续左移右指针直到其值不大于pivot。while(arr[right] > pivot) {right--;}// 此时左指针的值不小于pivot,右指针的值不大于pivot。// 如果left仍然不大于right。if(left <= right) {// 交换两者的值,使得不大于pivot的值在其左侧,不小于pivot的值在其右侧。[arr[left], arr[right]] = [arr[right], arr[left]];// 左指针右移,右指针左移准备开始下一轮,防止arr[left]和arr[right]都等于pivot然后导致死循环。left++;right--;}}// 返回左指针作为下一轮分解的依据。return left;}
}
复制代码

快速排序相对于归并排序而言加强了分解部分的逻辑,消除了数组的合并工作,并且不用分配新的内存来存放数组合并结果,所以性能更加优秀,是目前最常用的排序方案。

之前还在知乎上看到过一个回答,代码大致如下(从小到大排序):

function quickSort(arr) {// 当数组长度不大于1时,返回结果,防止callstack溢出。if(arr.length <= 1) return arr;return [// 递归调用quickSort,通过Array.prototype.filter方法过滤小于arr[0]的值,注意去掉了arr[0]以防止出现死循环。...quickSort(arr.slice(1).filter(item => item < arr[0])),arr[0],...quickSort(arr.slice(1).filter(item => item >= arr[0]))];
}
复制代码

以上代码有利于对快排思想的理解,但是实际运用效果不太好,不如之前的代码速度快。

六、堆排序

如果说快速排序是应用性最强的排序算法,那么我觉得堆排序是趣味性最强的排序方法,非常有意思。

堆排序也是一种很高效的排序方法,因为它把数组作为二叉树排序而得名,可以认为是归并排序的改良方案,它是一种原地排序方法,但是不够稳定,其时间复杂度为O(nlogn)。

实现步骤:

  • (1)由数组构造一个堆结构,该结构满足父节点总是大于(或小于)其子节点。
  • (2)从堆结构的最右边的叶子节点开始,从右至左、从下至上依次与根节点进行交换,每次交换后,都要再次构建堆结构。值得注意的是每次构建堆结构时,都要忽略已经交换过的非根节点。

数组构建的堆结构:

// 数组
var arr = [1,2,3,4,5,6,7];
// 堆结构1/   \2       3/   \   /   \
4      5 6     7
复制代码

可以发现对于数组下标为i的数组项,其左子节点的值为下标2*i + 1对应的数组项,右子节点的值为下标2*i + 2对应的数组项。

实际上并没有在内存中开辟一块空间构建堆结构来存储数组数据,只是在逻辑上把数组当做二叉树来对待,构建堆结构指的是使其任意父节点的子节点都不大于(不小于)父节点。

JavaScript实现(从小到大排序):

function heapSort(arr) {//console.time('HeapSort');buildHeap(arr);for(let i=arr.length-1; i>0; i--) {// 从最右侧的叶子节点开始,依次与根节点的值交换。[arr[i], arr[0]] = [arr[0], arr[i]];// 每次交换之后都要重新构建堆结构,记得传入i限制范围,防止已经交换的值仍然被重新构建。heapify(arr, i, 0);}//console.timeEnd('HeapSort');return arr;function buildHeap(arr) {// 可以观察到中间下标对应最右边叶子节点的父节点。let mid = Math.floor(arr.length / 2);for(let i=mid; i>=0; i--) {// 将整个数组构建成堆结构以便初始化。heapify(arr, arr.length, i);}return arr;}// 从i节点开始下标在heapSize内进行堆结构构建的函数。function heapify(arr, heapSize, i) {// 左子节点下标。let left = 2 * i + 1,// 右子节点下标。right = 2 * i + 2,// 假设当前父节点满足要求(比子节点都大)。largest = i;// 如果左子节点在heapSize内,并且值大于其父节点,那么left赋给largest。if(left < heapSize && arr[left] > arr[largest]) {largest = left;}// 如果右子节点在heapSize内,并且值大于其父节点,那么right赋给largest。if(right < heapSize && arr[right] > arr[largest]) {largest = right;}if(largest !== i) {// 如果largest被修改了,那么交换两者的值使得构造成一个合格的堆结构。[arr[largest], arr[i]] = [arr[i], arr[largest]];// 递归调用自身,将节点i所有的子节点都构建成堆结构。arguments.callee(arr, heapSize, largest);}return arr;}
}
复制代码

堆排序的性能稍逊于快速排序,但是真的很有意思。

七、性能对比

通过console.time()console.timeEnd()查看排序所用时间,通过generateArray()产生大规模的数据,最终得到如下结论:

通过对冒泡排序的测试,得到以下数据:

BubbleSort: 406.567ms
复制代码

给10000(一万)条数据进行排序,耗时406毫秒。

BubbleSort: 1665.196ms
复制代码

给20000(两万)条数据进行排序,耗时1.6s。

BubbleSort: 18946.897ms
复制代码

给50000(五万)条数据进行排序,耗时19s。 由于机器不太好,当数据量达到100000时基本就非常漫长了,具体多久也没等过,这已经可以看出来性能非常不好了。

通过对选择排序的测试,得到以下数据:

SelectionSort: 1917.083ms
复制代码

对20000(两万)条数据进行排序,耗时1.9s。

SelectionSort: 12233.060ms
复制代码

给50000(五万)条数据进行排序时,耗时12.2s,可以看出相对于冒泡排序而言已经有了进步,但是远远不够。还可以看出随着数据量的增长,排序的时间消耗越来越大。

通过对插入排序的测试,得到以下数据:

InsertionSort: 273.891ms
复制代码

对20000(两万)条数据进行排序,耗时0.27s。

InsertionSort: 1500.631ms
复制代码

对50000(五万)条数据进行排序,耗时1.5s。

InsertionSort: 7467.029ms
复制代码

对100000(十万)条数据进行排序,耗时7.5秒,对比选择排序,又有了很大的改善,但是仍然不够。

通过对归并排序的测试,得到以下数据:

MergeSort: 287.361ms
复制代码

对100000(十万)条数据进行排序,耗时0.3秒,真的很优秀了hhh,

MergeSort: 2354.007ms
复制代码

对1000000(一百万)条数据进行排序,耗时2.4s,绝对的优秀,难怪FireFox会使用这个来定义Array.prototype.sort方法,

MergeSort: 26220.459ms
复制代码

对10000000(一千万)条数据进行排序,耗时26s,还不错。 接下来看快排。

通过对快速排序的测试,得到以下数据:

QuickSort: 51.446ms
复制代码

100000(十万)条数据排序耗时0.05s,达到了可以忽略的境界,

QuickSort: 463.528ms
复制代码

1000000(一百万)条数据排序耗时0.46s,也基本可以忽略,太优秀了,

QuickSort: 5181.508ms
复制代码

10000000(一千万)条数据排序耗时5.2s,完全可以接受。

通过对堆排序的测试,得到以下数据:

HeapSort: 3124.188ms
复制代码

对1000000(一百万)条数据进行排序,耗时3.1s,逊色于快速排序和归并排序,但是对比其他的排序方法还是不错的啦。

HeapSort: 41746.788ms
复制代码

对10000000(一千万)条数据进行排序,耗时41.7s,不太能接受。

八、结论

以前都认为排序方法随便用用无可厚非,现在想想确实挺naive的hhh,想到了以前实习的时候,SQL Server几百万数据几秒钟就排序完成了,这要是用冒泡排序还不得等到两眼发黑?通过这次学习总结排序算法,尤其是对于每种方法性能的测试,我深刻地认识到了算法设计的重要性,只有重视算法的设计、复杂度的对比,才能写出优秀的算法,基于优秀的算法才能写出性能出色的应用!

此外,由于对于算法复杂度的研究不够深入,理解只停留在表面,所以文中如果存在有错误,恳请大牛不吝赐教!

最后,我想说一声,支持阮老师!

原文发布时间为:2018年05月24日
原文作者:DM.Zhong
本文来源掘金如需转载请联系原作者

六种排序算法的JavaScript实现以及总结相关推荐

  1. 十大经典排序算法(JavaScript实现)

    十大经典排序算法 排序算法是<数据结构与算法>中最基本的算法之一. 排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的 ...

  2. 几种常用的排序算法之JavaScript实现

    文章目录 插入排序 二分插入排序 选择排序 选择排序 冒泡排序 快速排序 堆排序 归并排序 桶排序 计数排序 插入排序 <html> <script> /* 1)算法简介插入排 ...

  3. javascript写各种排序算法

    在知乎上看到这个题目,就自己写了一下,在这里附上链接,里面有各种排序的动态图,非常形象直观,有助于新手对排序算法理解,链接:常见排序算法之JavaScript实现 首先各种排序算法都会用到的交换函数: ...

  4. javascript常用排序算法总结

    算法是程序的灵魂.虽然在前端的开发环境中排序算法不是很经常用到,但常见的排序算法还是应该要掌握的.我在这里从网上整理了一下常见排序算法的javascript实现,方便以后查阅. 归并排序: 1 fun ...

  5. JavaScript的排序算法——快速排序

    排序算法(Sorting algorithm)是计算机科学最古老.最基本的课题之一.要想成为合格的程序员,就必须理解和掌握各种排序算法. 快速排序(Quicksort)是对冒泡排序的一种改进. 快速排 ...

  6. 用c语言编写插入排序算法,C语言实现常用排序算法——插入排序

    插入排序是最基础的排序算法,原理: 首先1个元素肯定是有序的,所以插入排序从第二个元素开始遍历: 内循环首先请求一个空间保存待插入元素,从当前元素向数组起始位置反向遍历: 当发现有大于待插入元素的元素 ...

  7. JavaScript实现十种经典排序算法(js排序算法)

    冒泡排序算法 冒泡排序(Bubble Sort)是一种简单直观的排序算法.冒泡排序算法的步骤描述如下: 比较相邻的元素.如果第一个比第二个大,就交换他们两个. 对每一对相邻元素作同样的工作,从开始第一 ...

  8. JavaScript实现ShellSort希尔排序算法(附完整源码)

    JavaScript实现ShellSort希尔排序算法(附完整源码) Comparator.js完整源代码 Sort.js完整源代码 ShellSort.js完整源代码 Comparator.js完整 ...

  9. JavaScript实现SelectionSort选择排序算法(附完整源码)

    JavaScript实现SelectionSort选择排序算法(附完整源码) Comparator.js完整源代码 Sort.js完整源代码 SelectionSort.js完整源代码 Compara ...

最新文章

  1. [Spring MVC] - JSP + Freemarker视图解释器整合(转)
  2. 深入解析Python中的变量和赋值运算符
  3. FineReport搭建物流报表平台的解决方案
  4. 函数式编程语言python-Python函数式编程
  5. 如何轮播 DataV 大屏
  6. android下拉会谈效果,Android实现下拉展示条目效果
  7. 软件工程----8面向对象设计
  8. 第 18 章 访问者模式
  9. or导致索引失效的解决方法_电容引脚断裂失效的机理和解决方法
  10. Tyche 2147 旅行
  11. select、poll、epoll的区别
  12. Excel数据导入到oracle
  13. Win10桌面美化(桌面数字时钟,悬浮侧边栏、透明任务栏、底部居中软件图标)
  14. linux常用指令pro,第二章:Linux常用基本命令及常用技巧
  15. android 6g 有必要吗,Android手机6GB内存有必要吗?实测出真知
  16. 如何用一个IPad屏幕适配各尺寸的IPhone
  17. mybatisPlus中getOne方法如何只取其中一条数据(Wrapper有多条数据时)
  18. Cordova在左,Capacitor在右
  19. Vue爬坑之旅(二十一):vue使用富文本编辑器vue-quill-editor实现配合后台将图片上传至七牛
  20. 【UCIe】UCIe Standard 256B Flit for PCIe 6.0 vs. PCIe 6.0 Flit

热门文章

  1. jenkins无法安装插件问题
  2. MaxCompute实践分析
  3. mysql中binlog_format模式与配置详解
  4. jsr133-第一二章
  5. Ticker View
  6. _CRT_SECURE_NO_WARNINGS宏-转
  7. 组合模式java怎么获取钥匙_java中组合模式详解和使用方法
  8. Nginx的rewrite之set指令
  9. Zookeeper基于Java访问-权限
  10. Java并发编程的基础-线程的终止原理