今天咱们来讨论的问题有两个:面试
如何用JavaScript实现选择排序、冒泡排序、插入排序、快速排序、归并排序、堆排序;算法
对生成的10万个随机数进行排序,各个排序算法的性能分析。数组
这里咱们所有用数组来存储数据,首先建立一个类ArrayList。
其中属性的说明以下:数据结构
array空数组--->用以存放数据dom
insert()方法--->往array中插入数据函数
swapItemInArray(n,m)方法--->将array中第n个元素和第m个元素交换位置性能
toString()方法--->将array数组转换为字符串学习
originSort()方法--->JavaScript原生排序算法实现,在以后的性能比较中,咱们会用到它ui
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
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]<array[j-1]) { self.swapItemInArray(j,j-1) } } } }
上面几种排序的实现是否是很小儿科呢?下面的就要稍微复杂点了。
快速排序算法基本上是面试必考排序算法,也是传闻最好用的算法。
不过实现起来可一点都不容易,至少对我来讲是这样。
算法思想
本质上快速排排序是一种分治算法的实际应用。
按照下图所示,对于左边的原始数集合,(随便地)取一个数(称其为主元),好比取65为主元,则65则将原来的集合划分为了A集合和B集合,A中全部的数字都小于65,B中全部的数字都大于65。
而后。
以后再对A集合和B集合采起相同方式的划分。
最后就分为了从小到大排列的众多小集合。
实现思路
对于有N个数字的数组,进行大约logN轮的排序。
若每次都能划分为两等份,则效率最高。若是选择的那个数将数组划分为了一、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){ i++ } while(arr[j]>pivot){ j-- } if (i<=j) { self.swapItemInArray(i,j) i++ j-- } } self.swapItemInArray(i,right-1) //结束后将主元放回原位 if (left<i-1) { //对主元左侧的子数组展开快排 partition(arr,left,i-1) } if (i+1<right) { //对主元右侧的子数组展开快排 partition(arr,i+1,right) } }else{ //若是数组长度小于阈值,采用插入排序 insertSort(left,right) } }
快速排序的完整代码
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){ i++ } while(arr[j]>pivot){ j-- } if (i<=j) { self.swapItemInArray(i,j) i++ j-- } } self.swapItemInArray(i,right-1) if (left<i-1) { partition(arr,left,i-1) } if (i+1<right) { partition(arr,i+1,right) } }else{ insertSort(left,right) } } 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] } 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]<array[j-1]) { self.swapItemInArray(j,j-1) } } } } partition(array,0,array.length-1) }
与快速排序的“在划分中排序”不一样,归并排序的基本思想是先将长度为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<arr1.length && j<arr2.length){ if(arr1[i]<=arr2[j]){ tempArr.push(arr1[i]) i++ }else{ tempArr.push(arr2[j]) j++ } } while(i<arr1.length){ //若是arr1还有剩余元素,则所有放到tempArr中 tempArr.push(arr1[i]) i++ } while(j<arr2.length){ //若是arr2还有剩余元素,则所有放到tempArr中 tempArr.push(arr2[j]) j++ } return tempArr }
归并排序的完整代码以下,咱们这里采用递归来实现划分
this.mergeSort = function(){ function merge(arr1,arr2){ var i = 0 var j = 0 var tempArr = [] while(i<arr1.length && j<arr2.length){ if(arr1[i]<=arr2[j]){ tempArr.push(arr1[i]) i++ }else{ tempArr.push(arr2[j]) j++ } } while(i<arr1.length){ tempArr.push(arr1[i]) i++ } while(j<arr2.length){ tempArr.push(arr2[j]) j++ } return tempArr } function sliceArr(array){ var len = array.length if (len === 1) { return array } var middle = Math.floor(len/2) var left = array.slice(0,middle) var right = array.slice(middle,len) return merge(sliceArr(left),sliceArr(right)) } array = sliceArr(array) }
最大堆是什么
最大堆是一个彻底二叉树。
但它还知足,任意一个节点,它的值大于左子树中的任意元素的值,也大于右子树中的任意元素值。
该节点的左子树元素的值和右子树元素的值的大小没有要求。
堆的数组表示
当咱们用数组(从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,中国工信出版集团,人民邮电出版社