Amanda-Zhang
追梦女一枚

排序类算法

2021-05-21 Js篇
Word count: 4k | Reading time: 16min

七种常见排序算法可以分为两大类:

非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。

线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。

img

算法复杂度

img

相关概念

稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。

不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。

1、冒泡排序(Bubble Sort)

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

1.1 算法描述

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个;
  • 重复步骤1~3,直到排序完成。

1.2 动图演示

img

1.3 代码实现

1
2
3
4
5
6
7
8
9
10
11
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]) { // 相邻元素两两对比
[arr[j],arr[j+1]] = [arr[j+1],arr[j]];
}
}
}
return arr;
}

2、选择排序(Selection Sort)

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

2.1 算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • 初始状态:无序区为R[1..n],有序区为空;
  • 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  • n-1趟结束,数组有序化了。

2.2 动图演示

img  

2.3 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function selectionSort(arr) {
var len = arr.length;
var minIndex, temp;
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; // 将最小数的索引保存
}
}
[arr[i],arr[minIndex]] = [arr[minIndex],arr[i]];
}
return arr;
}

2.4 算法分析

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。

3、插入排序(Insertion Sort)

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

3.1 算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  • 从第一个元素开始,该元素可以认为已经被排序;
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤2~5。

3.2 动图演示

img

3.3 代码实现

1
2
3
4
5
6
7
8
9
10
function insertionSort(arr){
for(let i=1;i<arr.length;i++){
for(let j =i;j>=0;j--){
if(arr[j]<arr[j-1]){
[arr[j],arr[j-1]]=[arr[j-1],arr[j]];
}
}
}
return arr;
}

3.4 算法分析

插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

4、希尔排序(Shell Sort)

1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序

4.1 算法描述

  • 基本原理是,现将待排序的数组元素分成多个子序列,使得每个子序列的元素个数相对较少,然后对各个子序列分别进行直接插入排序,待整个待排序列“基本有序”后,最后在对所有元素进行一次直接插入排序。因此,我们要采用跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。希尔排序是对直接插入排序算法的优化和升级。
  • 希尔排序是基于插入排序的以下两点性质而提出改进方法的:
    1,插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
    2,插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

4.2 动图演示

img

4.3 代码实现**

1
2
3
4
5
6
7
8
9
10
11
12
13
function sheelSort(arr) {
for (let gap = arr.length / 2; gap >= 1;) { //这个gap是增量,也可以取别的值
for (let i = gap; i < arr.length; i++) {
let j = i;
while (j > 0 && arr[j] < arr[j - gap]) {
[arr[j],arr[j-gap]] = [arr[j-gap],arr[j]];
j -= gap
}
}
gap = Math.floor(gap / 2);
}
return arr;
}

另一个版本的(还没仔细研究)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function shellSort(arr) {
var len = arr.length,
temp,
gap = 1;
while (gap < len / 3) { // 动态定义间隔序列
gap = gap * 3 + 1;
}
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;
}

4.4 算法分析

希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。

5、归并排序(Merge Sort)(难)

归并排序使用了分治和递归的思想。

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

5.1 算法描述

  • 把长度为n的输入序列分成两个长度为n/2的子序列;

  • 对这两个子序列分别采用归并排序;

  • 将两个排序好的子序列合并成一个最终的排序序列。

    下面这个图很好的展示了归并排序的整体思想,具体怎么划分怎么归并(找了好多资料,只有这个让人醍醐灌顶!!博主tql!!

    放b站代码https://www.bilibili.com/video/BV1Pt4y197VZ?from=search&seid=16623903536813011391)

image-20210329092133068

5.2 动图演示

img

5.3 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function mergeSort(arr) { // 采用自上而下的递归方法
var len = arr.length;
if (len < 2) {
return arr;
}
var 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 result = [];
while (left.length>0 && right.length>0) { //看着上面地图,可以理解每次都是比较左右数组的第一个数,因为所有数组已经是一个有序的数组了,如果左小,则可以从当前数组中删掉这个数,并将其放到辅助数组中。继续比较两个数组中的第一个数,也就是下面的左右索引都为0的数。
if (left[0] <= right[0]) {
result.push(left.shift());
}else {
result.push(right.shift());
}
}
while (left.length) //左右数组进行比较的时候发现右边的已经都比完了,那么直接将左边的数组放入到辅助数组中
result.push(left.shift());
while (right.length) //同上,左边的比完了,剩下的右边的数组放到辅助数组中
result.push(right.shift());
return result;
}

5.4 算法分析

归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。

6、快速排序(难)

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

6.1 算法描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

6.2 动图演示

img

6.3代码实现

记这个记这个!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function quickSort(arr,left,right){
if(left>right) return; //这句必须加!否则会越界!!!
let i = left,j = right;
const key = arr[left];
while(i<j){
while(i<j&&arr[j]>=key){ //带一个例子特别好解释
j--;
}
arr[i] = arr[j];
while(i<j&&arr[i]<=key){
i++;
}
arr[j] = arr[i];
}
arr[i] = key
quickSort(arr,left,i-1);
quickSort(arr,j+1,right);
return arr;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function quickSort(arr, left, right) {
var len = arr.length,
partitionIndex,
left =typeof left !='number' ? 0 : left,
right =typeof right !='number' ? len - 1 : right; //这里的意思就是初始值左指针为0,右指针为length-1
if (left < right) {
partitionIndex = partition(arr, left, right);
quickSort(arr, left, partitionIndex-1);
quickSort(arr, partitionIndex+1, right);
}
return arr;
}
function partition(arr, left ,right) { // 分区操作
var pivot = left, // 设定基准值(pivot)
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;
}
function swap(arr, i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//感觉这个算法有漏洞,虽然也可以排序,但是感觉一趟下来并没有确定一个数的确切位置

另一种方法:(缺点就是方法里还需要定义左右指针!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function quickSort(arr,left,right){
//进行索引判断,如果左边索引比右边大。是不合法的,直接用return结束这个方法
if(left>right){
return;
}
//定义变量保存基准数
var base = arr[left];
//定义变量i,指向最左边
var i = left;
//定义变量j,指向最右边
var j = right;
//当i和j不相遇的时候,在循环中进行检索
while(i!=j){
//向由从右往左检索比基准数小的,如果检索到比基准数小的就停下。
//如果检索比基准数大的或者相等的,就继续检索
while(arr[j]>=base&&i<j){
j--;哦 //j从右往左移动
}
while(arr[i]<=base&&i<j){
i++; //i从左往右移动
}

//代码走到这里,i停下了,j也停下了。然后交换i和j位置的元素
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//如果上面while循环的条件不成立,会跳出这个循环,往下执行。
// 如果这个条件不成立说明i和j相遇了
//如果i和j相遇了,就交换基准数这个元素和相遇位置的元素。
// 把相遇位置的元素赋值给基准数这个位置的元素
arr[left] = arr[i];
//把基准数赋值给相遇位置的元素
arr[i] = base;
//基准数在这里就归位了,左边的数字都比他小,右边的都比他大。
// 排基准数的左边
quickSort(arr,left,i-1);
//排右边
quickSort(arr,j+1,right);
}
// 测试
let arr = [1, 6, 2, 7, 3, 5, 4]
quickSort(arr,0,6)
console.log(arr) // [1, 2, 3, 4, 5]
// 第 3 个最大值
console.log(arr[arr.length - 3]) // 5

7、堆排序(Heap Sort)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

7.1 算法描述

  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

7.2 动图演示

img

7.3 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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;
}

另外还有基数排序,桶排序,计数排序等,这里不做多介绍。

Author: Amanda-Zhang

Link: http://chunchunya.github.io/2021/03/24/%E6%9C%89%E5%85%B3%E6%8E%92%E5%BA%8F%E7%9A%84%E4%B8%80%E4%BA%9B%E7%AE%97%E6%B3%95/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
最长字符串前缀
NextPost >
Js中的Set和Map
CATALOG
  1. 1. 七种常见排序算法可以分为两大类:
  2. 2. 算法复杂度
  3. 3. 相关概念
  4. 4. 1、冒泡排序(Bubble Sort)
  5. 5. 2、选择排序(Selection Sort)
  6. 6. 3、插入排序(Insertion Sort)
  7. 7. 4、希尔排序(Shell Sort)
  8. 8. 5、归并排序(Merge Sort)(难)
  9. 9. 6、快速排序(难)
  10. 10. 7、堆排序(Heap Sort)