JavaScript各种排序算法的实现及其速度性能分析

今天我们来讨论的问题有两个:

  1. 如何用JavaScript实现选择排序、冒泡排序、插入排序、快速排序、归并排序、堆排序;

  2. 对生成的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]<array[j-1]) {
          self.swapItemInArray(j,j-1)
        }
      }
    }
  }

上面几种排序的实现是不是很小儿科呢?下面的就要稍微复杂点了。

快速排序

快速排序算法基本上是面试必考排序算法,也是传闻最好用的算法。
不过实现起来可一点都不容易,至少对我来说是这样。
算法思想
本质上快速排排序是一种分治算法的实际应用。
按照下图所示,对于左边的原始数集合,(随便地)取一个数(称其为主元),比如取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){
            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,中国工信出版集团,人民邮电出版社

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值