摘要:今天我们来讨论的问题有两个如何用实现选择排序冒泡排序插入排序快速排序归并排序堆排序对生成的万个随机数进行排序,各个排序算法的性能分析。快速排序快速排序算法基本上是面试必考排序算法,也是传闻最好用的算法。
今天我们来讨论的问题有两个:
如何用JavaScript实现选择排序、冒泡排序、插入排序、快速排序、归并排序、堆排序;
对生成的10万个随机数进行排序,各个排序算法的性能分析。
创建数据类型这里我们全部用数组来存储数据,首先创建一个类ArrayList。
其中属性的说明如下:
array空数组--->用以存放数据
insert()方法--->往array中插入数据
swapItemInArray(n,m)方法--->将array中第n个元素和第m个元素交换位置
toString()方法--->将array数组转换为字符串
originSort()方法--->JavaScript原生排序算法实现,在之后的性能比较中,我们会用到它
function ArrayList(){ var array = [] this.insert = function(item){ array.push(item) } this.swapItemInArray = function(n,m){ let temp = array[n] array[n] = array[m] array[m] = temp } this.toString = function(){ return array.join() } this.originSort = function(){ array.sort(function(a,b){ return a-b }) } }选择排序
先来实现最简单的选择排序。
其思路是:对于有N个数字的数组,进行N轮排序,在每一轮中,将最大的数找出,放到末尾。下一轮的时候再找出次大的数放到倒数第二位。
我们来为ArrayList类添加如下方法
this.selectSort = function(){ var self = this var len = array.length var maxIndex for (var i = 0; i < len; i++) { maxIndex = 0 //初始化最大数的位置 for (var j = 0; j < len - i; j++) { if (array[maxIndex] < array[j]) { //每一次都和之前的最大数比较 maxIndex = j //如果大于之前的最大数,则纪录当前数为最大数 } } //第i轮结束后,将最大数放到数组倒数第i个 self.swapItemInArray(maxIndex,len-i-1) } }冒泡排序
选择排序是不是太简单了?接下来我们就来实现冒泡排序。
思路:对于有N个数字的数组,进行N轮排序,在每一轮中,从前往后以此比较两两相邻的数字,每次比较后,都把大的往后放,一轮下来,最大的数会被推到数组最后。
this.bubbleSort = function(){ var self = this var len = array.length for (var i = 0; i < len; i++) { //数组长度要遍历的趟数 //第i趟之后,后面i个元素都不用比较 for (var j = 0; j < len - 1 - i; j++) { if (array[j]>array[j+1]) { //两两相邻进行比较 self.swapItemInArray(j,j+1) //将较大的数字放到后面 } } } }插入排序
插入排序的实现思路如下:
对于有N个数字的数组,进行N轮排序
第一轮 将第2个数字与第1个数字比较,如果第2个数字小,则与1交换
第二轮 将第3个数字与第2个数字比较
如果第3个数字小,则与第2个数字交换,再用第2个数字与第1个数字比较,将小的放前面。
第i轮 第1个到第i-1个数字已经全是从小到大排列了,第i个数字与前面的数字依次比较并交换位置,使得第1个数字,到第i个数字也是从小到达排列。
代码如下
this.insertSort = function(){ var self = this var len = array.length for (var i = 0; i < len; i++) { for (var j = i; j>0; j--) { if (array[j]上面几种排序的实现是不是很小儿科呢?下面的就要稍微复杂点了。
快速排序快速排序算法基本上是面试必考排序算法,也是传闻最好用的算法。
不过实现起来可一点都不容易,至少对我来说是这样。
算法思想
本质上快速排排序是一种分治算法的实际应用。
按照下图所示,对于左边的原始数集合,(随便地)取一个数(称其为主元),比如取65为主元,则65则将原来的集合划分为了A集合和B集合,A中所有的数字都小于65,B中所有的数字都大于65。
然后。
之后再对A集合和B集合采取相同方式的划分。
最后就分为了从小到大排列的众多小集合。实现思路
对于有N个数字的数组,进行大约logN轮的排序。
若每次都能划分为两等份,则效率最高。如果选择的那个数将数组划分为了1、N-1长度的两个数组则效率会非常低。
因此,主元的选择非常关键。不能用JavaScript中所提供的Math.random()获得主元,由该函数生成随机数代价昂贵。
根据相关资料,一个比较好的方法为:取首项、中间项、末尾项中的中值作为划分基准。主元的具体实现函数如下
它传入三个参数,数组本身、首项索引、尾项索引。
在查找中值的时候,顺便将三个值分别排列成,首项最小、中位数中等、尾项最大。
为了方便后续的划分,将主元和倒数第二个数进行交换(由于尾项已经大于中值,因此不必对其进行操作,故主元放到倒数第二个)function findPivot(arr,left,right){//主元取中位数 var center = Math.floor((left+right)/2) if (arr[left] > arr[center]) { self.swapItemInArray(left,center) } if (arr[left] > arr[right]) { self.swapItemInArray(left,right) } if (arr[center] > arr[right]) { self.swapItemInArray(center,right) } self.swapItemInArray(center,right-1)//主元被藏在倒数第二个 return arr[right-1] }每趟如何划分?
以下图为例,“9”为主元,我们把它放在最后一个。
这里设置i和j两个指针(JavaScript中则是数组下标),i指向首项“2”,j指向倒数第2个数字“7”。
让i指针往右边移动,遇到>=主元“9”的项时停下来
让j指针往左边移动,遇到<=主元“9”的项时停下来
交换i和j所指的值,并且i右移一位,j左移一位
i指针和j指针继续移动比较交换
当i与j发生交错时,本趟划分结束,把主元与i所指的“6”进行交换(即把主元放回原位)。此时数组被划分为了两个[0,...,j]、[i+1,...,last]。[0,...,j]中的所有元素都小于主元,[i+1,...,last]中所有元素都大于主元。
对划分出来的两个子数组继续进行下一步划分。具体函数实现如下,由于数组长度太小时采用快速排序效率较低,因而当数组长度太小时,我们改用插入排序。
function partition(arr,left,right){ //分割操作 var pivot = findPivot(arr,left,right) //找到主元 var length = right - left if (length>cutoff) { //当划分组没有小于阈值时,继续采用快速排序 var i = left var j = right - 2 while(i<=j){ //i和j没有交错 while(arr[i]pivot){ j-- } if (i<=j) { self.swapItemInArray(i,j) i++ j-- } } self.swapItemInArray(i,right-1) //结束后将主元放回原位 if (left 快速排序的完整代码
this.quickSort = function(){ var self = this var cutoff = 3 function partition(arr,left,right){ //分割操作 var pivot = findPivot(arr,left,right) var length = right - left if (length>cutoff) { var i = left var j = right - 2 while(i<=j){ while(arr[i]pivot){ j-- } if (i<=j) { self.swapItemInArray(i,j) i++ j-- } } self.swapItemInArray(i,right-1) if (left arr[center]) { self.swapItemInArray(left,center) } if (arr[left] > arr[right]) { self.swapItemInArray(left,right) } if (arr[center] > arr[right]) { self.swapItemInArray(center,right) } self.swapItemInArray(center,right-1)//主元被藏在倒数第二个 return arr[right-1] } function insertSort(left,right){ //当分块足够小的时候,用插入排序 var len = right - left for (var i = 0; i <= len; i++) { for (var j = i; j > 0; j--) { if (array[j] 归并排序 与快速排序的“在划分中排序”不同,归并排序的基本思想是先将长度为N的数组划分为N个长度为1的数组,然后两两合并,在合并的时候排序。
如何在两个子数组归并的时候排序
如下图,对于A数组和B数组,设置指针Aptr和指针Bptr,它们的初始位置都在俩数组的首部。
将Aptr和Bptr所指的数对比,将小的数放到C数组中。
比如Aptr所指“1”,Bptr所指“2”,Aptr所指的“1”小,则将“1”放入到C中,Aptr后移,Bptr不动。
再对比Aptr所指的“13”和Bptr所指的“2”,“2”较小,将其推入到C中,Bptr右移,Aptr不动。
反复重复上述操作,如果最后一个数组A空,另一个数组B还有剩余元素,则依次将数组B的剩余元素全部放到C中。
至此完成依次归并操作。代码实现为
function merge(arr1,arr2){ var i = 0 var j = 0 var tempArr = [] while(i归并排序的完整代码如下,我们这里采用递归来实现划分
this.mergeSort = function(){ function merge(arr1,arr2){ var i = 0 var j = 0 var tempArr = [] while(i堆排序 最大堆是什么
最大堆是一个完全二叉树。
但它还满足,任意一个节点,它的值大于左子树中的任意元素的值,也大于右子树中的任意元素值。
该节点的左子树元素的值和右子树元素的值的大小没有要求。堆的数组表示
当我们用数组(从0开始)来表示一个堆的时候,第i个元素的左子元素为第(2*i+1)个元素,右子元素为第(2i*2)个元素。堆排序的大致思想
将数组按最大堆的方式排列,比如排列为[a,b,c,d]
将一个最大堆的根(a)和最后一个元素(d)交换,
把数组中除了最后一个数(a)以外的元素[d,b,c]重新调整为最大堆。
对[d,b,c]重复上述操作如何创建最大堆
把所有元素插入到数组array(设长度为N)中后,从索引为(Math.floor(N/2) - 1)的元素开始,依次向前地将它和它的子树调整为最大堆。
如下图,先将子树①调整为最大堆 ----> 调整子树②为最大堆 ----> 调整整个树③为最大堆最大堆创建的代码如下
function createMaxHeap(){ //创建最大堆 var len = array.length var startIndex = Math.floor(len/2) - 1 //从这个节点开始,将其子树调整为最大堆 for (var i = startIndex; i >= 0; i--) { compareChildAndAdjust(i) } } function compareChildAndAdjust(i,lastIndex){ var bigChildIndex = findBigInChildren(i,lastIndex) if (bigChildIndex==false) { //当找到的子节点返回为false时,表示没有子节点应当结束 return } var parent = array[i] var bigChild = array[bigChildIndex] if (parent >= bigChild) { return }else{ self.swapItemInArray(i,bigChildIndex) compareChildAndAdjust(bigChildIndex,lastIndex)//调整后要对子树调整 } } function findBigInChildren(i,lastIndex){ var leftChild = array[2*i+1] //i节点的左子节点 var rightChild = array[2*i+2] //i节点的右子节点 if (lastIndex) { if (2*i+1 >= lastIndex) { return false } if (!(2*i+1 >= lastIndex) && (2*i+2 >= lastIndex)) { return 2*i+1 } } if (!leftChild) { return false } if ( leftChild && !rightChild) { return 2*i+1 } if (leftChild>rightChild) { return 2*i+1 }else{ return 2*i+2 } }堆排序的完整代码如下
this.heapSort = function(){ var self = this createMaxHeap() swapMaxWithLast() function swapMaxWithLast(){ var lastIndex = array.length - 1 for (var i = lastIndex; i > 0; i--) { self.swapItemInArray(0,i) //将根节点与最后一个节点交换 //从根节点开始,与其子节点比较并重新形成最大堆 //传入的第二个参数表示,向下比较的时候,比到第i个节点之前停下来 compareChildAndAdjust(0,i) } } function createMaxHeap(){ //创建最大堆 var len = array.length var startIndex = Math.floor(len/2) - 1 //从这个节点开始,将其子树调整为最大堆 for (var i = startIndex; i >= 0; i--) { compareChildAndAdjust(i) } } function compareChildAndAdjust(i,lastIndex){ var bigChildIndex = findBigInChildren(i,lastIndex) if (bigChildIndex==false) { //当找到的子节点返回为false时,表示没有子节点应当结束 return } var parent = array[i] var bigChild = array[bigChildIndex] if (parent >= bigChild) { return }else{ self.swapItemInArray(i,bigChildIndex) compareChildAndAdjust(bigChildIndex,lastIndex)//调整后要对子树调整 } } function findBigInChildren(i,lastIndex){ var leftChild = array[2*i+1] //i节点的左子节点 var rightChild = array[2*i+2] //i节点的右子节点 if (lastIndex) { if (2*i+1 >= lastIndex) { return false } if (!(2*i+1 >= lastIndex) && (2*i+2 >= lastIndex)) { return 2*i+1 } } if (!leftChild) { return false } if ( leftChild && !rightChild) { return 2*i+1 } if (leftChild>rightChild) { return 2*i+1 }else{ return 2*i+2 } } }各种排序的速度性能首先用一个函数来随机生成10万个数
function createNonSortedArray(size){ var array = new ArrayList() for (var i = size; i > 0; i--) { let num = Math.floor(1 + Math.random()*99) array.insert(num) } return array } var arr = createNonSortedArray(100000) //console.log(arr.toString()) //打印查看生成结果接下来采用如下函数来计算排序时间
var start = (new Date).getTime() //在这里调用arr的各种排序方法 //如 arr.quickSort() var end = (new Date).getTime() console.log(end-start) //打印查看生成结果 //console.log(arr.toString()) //打印查看排序结果数据结果如下
冒泡排序耗时26000ms左右
选择排序耗时5800ms左右
插入排序耗时10600ms左右
归并排序耗时80-100ms
快速排序
cutoff==5--->30-50ms
cutoff==10 --->30-60ms
cutoff==50 ---->40-50ms
cutoff==3效果不错--->30-50ms,30ms出现的机会很多
cutoff==0时(即不在分割长度短的时候转为插入排序),效果依然不错,30-50ms,30ms出现的很多堆排序耗时120-140ms
JavaScript提供的原生排序耗时55-70ms
结论
快速排序效率最高,cutoff取3效果最好(没有悬念)
原生排序竟然是第二快的排序算法!诸位同学参加笔试的时候,在没有指明必须要用哪种排序算法的情况下,如果需要排个序,还是用原生的yourArr.sort(function(a,b){return a-b})吧,毕竟不易错还特别快!
关于数据结构和排序算法的学习建议如果想了解数据结构和排序算法的基础理论知识,推荐中国大学mooc浙江大学陈越老师主讲的《数据结构》。该课程采用C语言讲解,但仍然可以系统地学习到数据结构的实现思路。
参考资料
你要是觉得本文文字描述难以理解,去听或看该课程的动态图片讲解应该会豁然开朗。《数据结构》(第2版),陈越,高等教育出版社
《学习JavaScript数据结构与算法》[巴西]Loiane Groner,中国工信出版集团,人民邮电出版社
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/85069.html
摘要:年,软件开发界发生了很多变化。六数据存储是一个关系型数据库管理系统,由瑞典公司开发,目前属于旗下公司。最流行的关系型数据库管理系统,在应用方面是最好的,关系数据库管理系统应用软件之一。七是最新的修订版本,年月由万维网联盟完成标准制定。 2015年,软件开发界发生了很多变化。有很多流行的新语言发布了,也有很多重要的框架和工具发布了新版本。下面有一个我们觉得最重要的简短清单,同时也有我们觉...
摘要:年,软件开发界发生了很多变化。六数据存储是一个关系型数据库管理系统,由瑞典公司开发,目前属于旗下公司。最流行的关系型数据库管理系统,在应用方面是最好的,关系数据库管理系统应用软件之一。七是最新的修订版本,年月由万维网联盟完成标准制定。 2015年,软件开发界发生了很多变化。有很多流行的新语言发布了,也有很多重要的框架和工具发布了新版本。下面有一个我们觉得最重要的简短清单,同时也有我们觉...
摘要:适用于数据比较少或基本有序的情况。插入排序时间复杂度为,空间复杂度为,属于稳定排序。算法适用于少量数据的排序。就像下图这样,可以理解桶的意思下图是整个排序过程示意图基数排序时间复杂度为,空间复杂度为,属于稳定排序。 写在前面 个人感觉:javascript对类似排序查找这样的功能已经有了很好的封装,以致于当我们想对数组排序的时候只需要调用arr.sort()方法,而查找数组元素也只需要...
摘要:代码实现六堆排序算法简介堆排序是指利用堆这种数据结构所设计的一种排序算法。九计数排序算法简介计数排序是一种稳定的排序算法。计数排序不是比较排序,排序的速度快于任何比较排序算法。 赞助我以写出更好的文章,give me a cup of coffee? 2017最新最全前端面试题 1、插入排序 1)算法简介 插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它...
阅读 2284·2021-11-08 13:13
阅读 1216·2021-10-09 09:41
阅读 1617·2021-09-02 15:40
阅读 3156·2021-08-17 10:13
阅读 2516·2019-08-29 16:33
阅读 3083·2019-08-29 13:17
阅读 3089·2019-08-29 11:00
阅读 3267·2019-08-26 13:40