经典排序算法详解

冒泡排序

算法步骤

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

算法实现

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
public static void BetterSort(int[] array)
{
//外循环从1开始,进行N-1轮比较
for (int i = 1; i < array.Length; i++)
{
//标记是否有发生交换
bool exchange = false;

//内循环从0开始,到array.Length-i结束,因为后面的i个数已经是有序的
for (int j = 0; j < array.Length - i; j++)
{
if (array[j] > array[j + 1])
{
exchange = true;
int tmp = array[j];
array[j] = array[j + 1];
array[j + 1] = tmp;
}
}

//若一次遍历下没有发生交换,则数组为有序,终止排序
if (!exchange)
break;
}
}

总结归纳

Q:为什么冒泡算法在最好情况下时间复杂度为O(n)?
A:因为对于有序数组可以在遍历一次后提前结束。

Q:冒泡排序与选择排序哪个效率高?
A:两者时间复杂度都时候O(n),但冒泡排序在内存循环交换,选择排序在外循环交换,一般而言选择排序效率更高。

选择排序

算法步骤

  1. 首先在未排序序列中找到最小元素,存放到排序序列的起始位置。
  2. 再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。
  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
public static void SelectionSort(int[] array)
{
// 外循环从0开始,总共要经过 N-1 轮比较
for (int i = 0; i < array.Length - 1; i++)
{
//先假定当前值为最小值
int minIndex = i;
//内循环从1开始,总共要经过 N-1 轮比较
for (int j = i + 1; j < array.Length; j++)
{
//把当前值和后面每一位数作对比
if (array[minIndex] > array[j])
{
//若存在比当前值小的数,则记录为最小值
minIndex = j;
}
}

//若当前值不是最小值(最小值索引发生了变化),则交换当前值和最小值的位置,把最小值放到前面
if (i != minIndex)
{
int tmp = array[i];
array[i] = array[minIndex];
array[minIndex] = tmp;
}
}
}

总结归纳

略略略

插入排序

算法步骤

  1. 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
  2. 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

算法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void InsertSort(int[] array)
{
//从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
for (int i = 1; i < array.Length; i++)
{
//记录要插入的数据
int tmp = array[i];

//从已经排序的序列最右边的开始比较,找到比其小的数
int j = i;
while (j > 0 && tmp < array[j - 1])
{
//把较大的数往前移一位
array[j] = array[j - 1];
j--;
}

//若数据发生了移动,则把要插入的数据放到
if (j != i)
array[j] = tmp;
}
}

总结归纳

插入排序的原理很好理解,排序过程类似于我们打扑克牌时,在未排序的牌中抽取第一张牌,并在已排序的序列中从后往前对比,找到合适的位置插入。

详解:https://zhuanlan.zhihu.com/p/35328552

希尔排序

算法步骤

  1. 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1。
  2. 按增量序列个数 k,对序列进行 k 趟排序。
  3. 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

算法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void ShellSort(int[] array)
{
//增量序列
for (int gap = array.Length / 2; gap > 0; gap /= 2)
{
for (int i = gap; i < array.Length; i += 1)
{
//插入排序
int tmp = array[i];
int j = i;
while (j > 0 && tmp < array[j - 1])
{
array[j] = array[j - 1];
j--;
}
if (j != i)
array[j] = tmp;
}
}
}

总结归纳

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

希尔排序效率还受增量序列的影响

参考:
https://zhuanlan.zhihu.com/p/87781731
https://zhuanlan.zhihu.com/p/73726253
https://en.wikipedia.org/wiki/Shellsort

归并排序

算法步骤

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列。
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置。
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置。
  4. 重复步骤 3 直到某一指针达到序列尾。
  5. 将另一序列剩下的所有元素直接复制到合并序列尾。

算法实现

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
47
48
49
50
51
public static void MergeSort(int[] array)
{
int[] temp = new int[array.Length];
Sort(array, 0, array.Length - 1, temp);
}

// 归并
private static void Sort(int[] array, int first, int last, int[] tmp)
{
if (first < last)
{
int mid = (first + last) / 2;
Sort(array, first, mid, tmp); // 递归归并左边元素
Sort(array, mid + 1, last, tmp); // 递归归并右边元素
MergeArray(array, first, mid, last, tmp); // 再将二个有序数列合并
}
}

private static void MergeArray(int[] array, int first, int mid, int last, int[] tmp)
{
int i = first, j = mid + 1; // i为第一组的起点, j为第二组的起点
int m = mid, n = last; // m为第一组的终点, n为第二组的终点
int k = 0; // k用于指向temp数组当前放到哪个位置
// 将两个有序序列循环比较, 填入数组temp

while (i <= m && j <= n)
{
if (array[i] <= array[j])
tmp[k++] = array[i++];
else
tmp[k++] = array[j++];
}

// 如果比较完毕, 第一组还有数剩下, 则全部填入temp
while (i <= m)
{
tmp[k++] = array[i++];
}

// 如果比较完毕, 第二组还有数剩下, 则全部填入temp
while (j <= n)
{
tmp[k++] = array[j++];
}

// 将排好序的数填回到array数组的对应位置
for (i = 0; i < k; i++)
{
array[first + i] = tmp[i];
}
}

总结归纳

详解:https://zhuanlan.zhihu.com/p/36075856

快速排序

算法步骤

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

递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

算法实现

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
47
48
49
50
51
52
public static void QuickSort(int[] arr)
{
Sort(arr, 0, arr.Length - 1);
}

private static void Sort(int[] arr, int startIndex, int endIndex)
{
if (startIndex < endIndex)
{
//分区
int partition = Partition(arr, startIndex, endIndex);
//分区后再递归排序
Sort(arr, startIndex, partition - 1);
Sort(arr, partition + 1, endIndex);
}
}

private static int Partition(int[] arr, int startIndex, int endIndex)
{
int pivot = arr[startIndex];
int left = startIndex;
int right = endIndex;
while (left != right)
{
//从右边往左边找,找到比基准值小的数值
while (left < right && arr[right] > pivot)
{
right--;
}
//从左边往右边找,找到比基准值大的数值
while (left < right && arr[left] <= pivot)
{
left++;
}
//找到left比基准大,right比基准小,进行交换
if (left < right)
{
Swap(arr, left, right);
}
}
//第一轮完成,让left和right重合的位置和基准交换,返回基准的位置
Swap(arr, startIndex, left);
return left;
}

//两数交换
public static void Swap(int[] arr, int i, int j)
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

总结归纳

对于上面的快速排序实现,还可以改进一下几点实现简单优化:

  1. 基准的确定
  2. 对于较小的分区,换用插入排序
  3. 递归改成非递归

参考:https://zhuanlan.zhihu.com/p/57436476

堆排序

算法步骤

  1. 将待排序序列构建成一个堆 H[0……n-1],根据(升序降序需求)选择大顶堆或小顶堆。
  2. 把堆首(最大值)和堆尾互换。
  3. 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置。
  4. 重复步骤 2,直到堆的尺寸为 1。

算法实现

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
47
48
49
50
51
52
53
54
55
56
57
public static void HeapSort(int[] arr)
{
int len = arr.Length;

BuildMaxHeap(arr, len);

for (int i = len - 1; i > 0; i--)
{
Swap(arr, 0, i);//把堆首(最大值)和堆尾互换
len--;//把堆的尺寸缩小 1
Heapify(arr, 0, len);//堆化,把新的最大值移动到堆首
}
}

//构建最大堆
private static void BuildMaxHeap(int[] arr, int len)
{
for (int i = (len / 2); i >= 0; i--)//只需要扫描数组的一半元素,因为可以跳过大小为1的之堆
{
Heapify(arr, i, len);
}
}

//堆化
private static void Heapify(int[] arr, int i, int len)
{
int left = 2 * i + 1;//左节点
int right = 2 * i + 2;//右节点
int 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, len);
}
}

private static void Swap(int[] arr, int i, int j)
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

总结归纳

堆排序需要了解前置知识“二叉堆”,具体讲解:https://zhuanlan.zhihu.com/p/31440467

堆排序cache不友好会有造成一定的性能下降。

桶排序&计数排序&基数排序

区别于上文的7种排序算法,这3种排序算法是非比较算法,本文不给出具体实现,具体实现可见文末参考链接。

桶排序

桶排序本身不是具体的排序,桶排序的原理是将数组分到有限数量的桶中,再对每个桶子再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后将各个桶中的数据有序的合并起来。

  1. 假设待排序的一组数统一的分布在一个范围中,并将这一范围划分成几个子范围,也就是桶
  2. 将待排序的一组数,分档规入这些子桶,并将桶中的数据进行排序
  3. 将各个桶中的数据有序的合并起来

计数排序

计数排序是桶排序的一种特殊情况,可以把计数排序当成每个桶里只有一个元素的情况。

  1. 找出序列中最大值和最小值,开辟Max-Min+1的辅助空间。
  2. 最小的数对应下标为0的位置,遇到一个数就给对应下标处的值+1。
  3. 遍历一遍辅助空间,就可以得到有序的一组序列。

基数排序

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

参考:https://zhuanlan.zhihu.com/p/84126129

基数排序 vs 计数排序 vs 桶排序

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异

  • 基数排序:根据键值的每位数字来分配桶。
  • 计数排序:每个桶只存储单一键值。
  • 桶排序:每个桶存储一定范围的数值。

大O表示法

什么是大O表示法

在计算机科学中,大的O表示法用于根据算法的运行时间或空间要求随输入大小的增长而对其进行分类。大O表示法就是将算法的所有步骤转换为代数项,然后排除不会对问题的整体复杂度产生较大影响的较低阶常数和系数。

如何理解各个具体的大O表示法

https://www.zhihu.com/question/21387264/answer/830343420

一图流

O

稳定性

排序后 2 个相等键值的顺序和排序之前它们的顺序相同,则为稳定。

概括

sort

参考

https://github.com/hustcc/JS-Sorting-Algorithm