JavaScript实现五种排序算法

本文详细介绍了使用JavaScript实现的五种排序算法:快速排序、插入排序(包括直接插入和希尔排序)、选择排序(包括简单选择、树形选择和堆排序)、归并排序以及基数排序。文中对每种排序算法的原理、性能分析和稳定性进行了阐述,并提供了相应的代码示例。

最近复习一些数据结构的算法,想着既然弄熟了JavaScript,倒不如用JavaScript来实现一下。

在数据结构中的排序算法中,大致可以分为五类:快速排序、插入排序、选择排序、归并排序、基数排序。

目录

1 快速排序

1.1 冒泡排序

1.2 快速排序

2 插入排序

2.1 直接插入排序

2.2 希尔排序

3 选择排序

3.1 简单选择排序

3.2 树形选择排序

3.3 堆排序

4 归并排序

5 基数排序



1 快速排序

快速排序,法如其名,快——快速排序甚至被认为是目前最好的一种内部排序方法。不过在讲快速排序之前,还是有必要先理解一下冒泡排序。

1.1 冒泡排序

(一)算法过程: 

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 

  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。 

  3. 针对所有的元素重复以上的步骤,除了最后一个。 

  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

看,就像泡泡一样,每一趟最大的数总是会浮上来。

(二)算法分析:

a. 时间复杂度:

假定我们所需的结果序列是一个升序序列,冒泡排序存在最好和最坏的情况。

最好情况下,待排序列已经是升序序列,此时一趟扫描即可完成排序。所需的关键字比较次数C和移动次数M均达到最小值:

Cmin = n - 1,Mmin = 0

所以,冒泡排序最好的时间复杂度为O(n)。

最坏情况下,待排序列一开始是降序的,需要进行n-1趟排序。每趟排序要进行n-i次关键字的比较(1<=i<=n-1),且每次比较都必须移动记录3次来达到交换记录位置。此时,比较次数和移动次数都达到最大值:

Cmax = n(n-1)/2,Mmax = 3n(n-1)/2

所以,冒泡排序最坏的时间复杂度为O(n^2)。

综上,冒泡排序平均时间复杂度为O(n^2)。

b. 稳定性:

冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不会再交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是稳定排序算法。

(三)代码:

function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len - 1; i++) {
        for (var j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j+1]) {        // 相邻元素两两对比
                var temp = arr[j+1];        // 元素交换
                arr[j+1] = arr[j];
                arr[j] = temp;
            }
        }
    }
    return arr;
}

按照上面说的,冒泡排序存在最好和最坏的情况,我们说最好的情况下,只需要比较(n-1)次,可上面的代码可不是这么一回事——管你待排序列长什么样,我都比较个n(n-1)/2次。这让我们不得不考虑一下性能优化的问题。

这让我想起一次坑爹的经历——一次去应聘某公司的实习生,面试官问起来排序算法,首当其冲就是冒泡排序。

当时我这么和面试官说:“冒泡排序在最好的情况下,只需要比较n-1次”。

哪知面试官皱了皱眉,反驳我:“最好的情况下只能保证移动的记录最少为0,但是比较的次数没有增减,最好最坏的情况下都需要比较 ’n(n-1)/2‘ 次”。

当时我面试很是紧张,迷迷糊糊也觉得面试官说的没毛病——也对哦,就算原始数组是排好序的,但是也得比较了再说啊!

后来面试完在回去的路上想着,对个鬼!只要定义标准:“如果一趟比较下来,没有移动记录,则排序结束” 不就好了!这其实就是性能优化了。

      var arr = [1, 8, 5, 7, 10, 4, 3, 2, 6, 9]
      var arr2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

      //交换函数
      function swap(arr, i, j) {
        var temp = arr[j]
        arr[j] = arr[i]
        arr[i] = temp
      }

      // 1. 冒泡排序
      function bubbleSort(arr) {
        var len = arr.length,
          time = 0
        for (var i = 0; i < len; i++) {
          var flagSort = false //此趟排序是否交换记录
          for (var j = 0; j < len - i; j++) {
            if (arr[j] > arr[j + 1]) {
              flagSort = true
              time++
              swap(arr, j, j + 1)
            }
          }
          console.log(time)
          if (!flagSort) {
            return arr
          }
        }
        return arr
      }
      console.log(arr) //[1, 8, 5, 7, 10, 4, 3, 2, 6, 9]
      console.log(arr2) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
      console.log(bubbleSort(arr), '冒泡排序') //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
      console.log(bubbleSort(arr2)) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

上面代码我加入了一个 flagSort 变量,来标志这趟排序是否有交换的记录,如果在一趟排序下来,flagSort仍然为false,说明该趟排序并没有交换记录,直接return。除此之外,我还加入了一个time变量,可以在每趟排序中打印交换记录的次数。在数组arr2中,由于arr2已经是正序的数组,因此time为0。

1.2 快速排序

(一)算法过程:

快速排序算法通过多次比较和交换来实现排序,其排序流程如下:

1. 首先设定一个分界值,通过该分界值将数组分成左右两部分。

2. 将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。

3. 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。

4. 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

(二)算法分析:

a. 时间复杂度:

b. 稳定性:

快速排序是不稳定的算法。举个例子就知道了。假定初始序列为:

[49,27,65,97,30,27*,49*]

运用快速排序算法,得到的有序序列为:

[27*,27,30,49,49*,65,97]

(三)代码:

      // 2. 快速排序-主函数
      function quickSort(arr, left, right) {
        var len = arr.length,
          left = typeof left == 'number' ? left : 0,
          right = typeof right == 'number' ? right : len - 1
        if (left < right) {
          var partIndex = partition(arr, left, right)
          quickSort(arr, left, partIndex - 1)
          quickSort(arr, partIndex + 1, right)
        }
        return arr
      }
      //快速排序-分区函数
      function partition(arr, left, right) {
        var pivot = left,
          index = pivot + 1
        for (var i = index; i <= right; i++) {
          if (arr[i] < arr[pivot]) {
            swap(arr, i, index)
            index++
          }
        }
        swap(arr, pivot, index - 1)
        return index - 1
      }
      console.log(arr) //[1, 8, 5, 7, 10, 4, 3, 2, 6, 9]
      console.log(quickSort(arr), '快速排序')//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

2 插入排序

2.1 直接插入排序

(一)算法过程:

将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

(二)算法分析:

a. 算法复杂度:

假定我们所需的结果序列是一个升序序列,插入排序存在最好和最坏的情况。最好情况下,如果待排序列n个元素已经是升序排序,那么需要比较(n-1)次。最坏情况下,如果待排序列n个元素是降序排序,那么需要比较n(n-1)/2次。平均来说插入排序是时间复杂度是O(n^2)。

因此,插入排序不适合数据量较大的排序应用。但是如果需要排序的数据量很小,例如量级小于千,那么插入排序还是一个不错的选择。

b. 稳定性:

插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

(三)代码:

      // 4. 插入排序
      function insertSort(arr) {
        var len = arr.length,
          preIndex,
          currentValue
        for (var i = 1; i < len; i++) {
          preIndex = i - 1
          currentValue = arr[i]
          while (preIndex >= 0 && arr[preIndex] > currentValue) {
            arr[preIndex + 1] = arr[preIndex]
            preIndex--
          }
          arr[preIndex + 1] = currentValue
        }
        return arr
      }
      console.log(arr) //[1, 8, 5, 7, 10, 4, 3, 2, 6, 9]
      console.log(insertSort(arr), '插入排序')//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 

2.2 希尔排序

(一)算法过程:

选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;

按增量序列个数 k,对序列进行 k 趟排序;

每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

(二)算法分析:

a. 时间性能:

由算法的过程可以知道,希尔排序的执行时间依赖于增量序列。好的增量序列有如下特征:

1. 最后一个增量必须为1;

2. 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。

b. 希尔排序性能优于直接插入排序:

1. 当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。

2. 当n值较小时,n和n^2 的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度O(n^2)差别不大。

3. 在希尔排序开始时z增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。

因此,希尔排序在效率上较直接插入排序有较大的改进。

c. 稳定性:

由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

(三)代码:

      // 5. 希尔排序
      function shellSort(arr) {
        var len = arr.length,
          temp,
          gap = Math.round(len / 3)

        for (gap; gap > 0; gap = Math.floor(gap / 3)) {
          for (var i = gap; i < len; i++) {
            temp = arr[i]
            for (var j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
              arr[j + gap] = arr[j]
            }
            arr[j + gap] = temp
          }
        }
        return arr
      }
      console.log(arr) //[1, 8, 5, 7, 10, 4, 3, 2, 6, 9]
      console.log(shellSort(arr), '希尔排序')//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

3 选择排序

3.1 简单选择排序

(一)算法过程:

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。

再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

重复第二步,直到所有元素均排序完毕。

(二)算法分析:

a. 时间复杂度:

在最好的情况下,待排序列已经有序,简单选择排序算法的交换次数为0;最坏情况下,交换次数为3(n-1)。然而,无论初始待排序列如何,简单选择排序的比较次数始终为n(n-1)/2。因此,总的时间复杂度也是O(n^2)。

b. 稳定性:

举个例子,有序列如下:

[5,8,5*,2,9]

我们知道第一遍选择中,5会和2交换,那么5和5*的位置顺序就被破坏了,因此选择排序不是一个稳定排序。

(三)代码:

      // 3. 选择排序
      function selectionSort(arr) {
        var len = arr.length,
          temp,
          minIndex
        for (var i = 0; i < len - 1; i++) {
          minIndex = i
          for (var j = i + 1; j < len; j++) {
            if (arr[j] < arr[minIndex]) {
              minIndex = j
            }
          }
          swap(arr, i, minIndex)
        }
        return arr
      }
      console.log(arr) //[1, 8, 5, 7, 10, 4, 3, 2, 6, 9]
      console.log(selectionSort(arr), '选择排序')//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

3.2 树形选择排序

(一)算法过程:

树形选择排序,又称锦标赛排序,是一种按照锦标赛的思想进行选择排序的方法。

首先对n个记录进行两两比较,然后在其中角逐出来的较小者继续进行比较,如此重复,直至选出最小记录为止:

(二)算法分析:

由于n个叶子结点的完全二叉树深度为$\lceil \log_{2}n \rceil$+1,因此在树形选择排序中,除了最小关键字外,每选择一个次小关键字仅需进行$\lceil \log_{2}n \rceil$次比较,因此它的时间复杂度为O(n\log_{2}n)

3.3 堆排序

树形选择排序尚有辅助存储空间较多、和“最大值”进行多余的比较等缺点。为了弥补它的缺点,推排序出现了。

(一)算法过程:

将一个数组看成一颗完全二叉树的话,则堆的含义表明,完全二叉树中所有非终端结点的值均不大于(或不小于)其左右孩子结点的值。堆排序其实就是一个建堆和输出堆顶元素的过程。

建立初始堆的过程:

输出堆顶元素并调整建成新堆的过程:

(二)算法分析:

堆排序在最坏的情况下,其时间复杂度也为O(n\log n)

(三)代码:

var len;    // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量

function buildMaxHeap(arr) {   // 建立大顶堆
    len = arr.length;
    for (var i = Math.floor(len/2); i >= 0; i--) {
        heapify(arr, i);
    }
}

function heapify(arr, i) {     // 堆调整
    var left = 2 * i + 1,
        right = 2 * i + 2,
        largest = i;

    if (left < len && arr[left] > arr[largest]) {
        largest = left;
    }

    if (right < len && arr[right] > arr[largest]) {
        largest = right;
    }

    if (largest != i) {
        swap(arr, i, largest);
        heapify(arr, largest);
    }
}

function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

function heapSort(arr) {
    buildMaxHeap(arr);

    for (var i = arr.length-1; i > 0; i--) {
        swap(arr, 0, i);
        len--;
        heapify(arr, 0);
    }
    return arr;
}

4 归并排序

(一)算法过程:

归并操作的工作原理如下:

第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列

第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置

第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置

重复步骤3直到某一指针超出序列尾

将另一序列剩下的所有元素直接复制到合并序列尾

(二)算法分析:

归并算法是分治法的一种重要应用,归并排序是仅次于快速排序的排序算法。

归并排序是稳定的排序.即相等的元素的顺序不会改变.如输入记录 1(1) 3(2) 2(3) 2(4) 5(5) (括号中是记录的关键字)时输出的 1(1) 2(3) 2(4) 3(2) 5(5) 中的2 和 2 是按输入的顺序.这对要排序数据包含多个信息而要按其中的某一个信息排序,要求其它信息尽量按输入的顺序排列时很重要。归并排序的比较次数小于快速排序的比较次数,移动次数一般多于快速排序的移动次数。

(三)代码:

      // 6. 归并排序
      function mergeSort(arr) {
        //采用自上而下的递归
        var len = arr.length,
          middle,
          left,
          right
        if (len < 2) {
          return arr
        }
        middle = Math.floor(len / 2)
        left = arr.slice(0, middle)
        right = arr.slice(middle)
        return merge(mergeSort(left), mergeSort(right))
      }
      function merge(left, right) {
        var res = []
        while (left.length && right.length) {
          if (left[0] <= right[0]) {
            res.push(left.shift())
          } else {
            res.push(right.shift())
          }
        }
        while (left.length) {
          res.push(left.shift())
        }
        while (right.length) {
          res.push(right.shift())
        }
        return res
      }
      console.log(arr)
      console.log(shellSort(arr), '归并排序')

5 基数排序

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

来看一个例子:

对待排序列,首先进行个位上的分配,依次取出项数,得到个位上正序的序列:
 

针对个位正序的序列,进行十位上的分配,依次取出项数,得到十位上正序的序列: 

最后针对上面得到的结果序列,进行百位上的分配,依次取出项数,得到百位上正序的序列: 

此时整个序列已是正序序列。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值