十大排序算法

十大排序算法时间复杂度介绍

排序算法 平均时间复杂度 最好情况 最坏情况 空间复杂度 排序方式 稳定性
冒泡排序 In-place 稳定
选择排序 In-place 不稳定
插入排序 In-place 稳定
快速排序 In-place 不稳定
归并排序 Out-place 稳定
希尔排序 In-place 不稳定
堆排序 In-place 不稳定
计数排序 Out-place 稳定
桶排序 Out-place 稳定
基数排序 Out-place 稳定

冒泡排序(Bubble Sort)

简单介绍:

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

算法描述:

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

算法实现:

1
2
3
4
5
6
7
8
9
void Bubble_Sort(int a[]) {
for (int i = n - 1; i > 0; i--) { // 每次需要排序的长度
for (int j = 0; j < i; j++) {
if (a[j] > a[j + 1]) { // 交换的过程
std::swap(a[j], a[j + 1]);
}
}
}
}

稳定性与时间复杂度分析:

  • 在相邻元素相等时,它们并不会交换位置,所以,冒泡排序是稳定的排序。
  • 每次交换相邻两个元素,共计枚举 次,所以其时间复杂度为

选择排序(Selection Sort)

简单介绍:

  • 选择排序是一种简单直观的排序算法,它也是一种交换排序算法,和冒泡排序有一定的相似度,可以认为选择排序是冒泡排序的一种改进。

算法描述:

  1. 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。
选择排序-图解

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
void Selection_Sort(int a[]) {
for (int i = 0; i + 1 < n; i++) {
int min = i;
for (int j = i + 1; j < n; j++) {
if (a[min] > a[j]) {
min = j;
}
}
if (min != i) {
std::swap(a[min], a[i]);
}
}
}

稳定性与时间复杂度分析:

  • 用数组实现的选择排序是不稳定的,用链表实现的选择排序是稳定的。不过,一般提到排序算法时,大家往往会默认是数组实现,所以选择排序是不稳定的。
  • 选择排序的时间复杂度是

插入排序(Insertion Sort)

简单介绍:

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

算法描述:

  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
void Insertion_Sort(int a[]) {
for (int i = 1; i < n; i++) {
int val = a[i];
int p = i;
while (p > 0 && a[p - 1] > val) {
a[p] = a[p - 1];
p--;
}
a[p] = val;
}
}
/*
10 9 6 8 7 5 3 4 2 1 // 初始数列
9 10 6 8 7 5 3 4 2 1
6 9 10 8 7 5 3 4 2 1
6 8 9 10 7 5 3 4 2 1
6 7 8 9 10 5 3 4 2 1
5 6 7 8 9 10 3 4 2 1
3 5 6 7 8 9 10 4 2 1
3 4 5 6 7 8 9 10 2 1
2 3 4 5 6 7 8 9 10 1
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10 // 最终数列
*/

稳定性与时间复杂度分析:

  • 由于只需要找到不大于当前数的位置而并不需要交换,因此,直接插入排序是稳定的排序方法。
  • 插入排序的时间复杂度

快速排序(Quick Sort)

简单介绍:

  • 快速排序是一个知名度极高的排序算法,其对于大数据(Big Data)的优秀排序性能和相同复杂度算法中相对简单的实现使它注定得到比其他算法更多的宠爱。

算法描述:

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

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Quick_Sort(int a[], int l, int r) {
if (l >= r) return ;
int mid = a[l + r >> 1];
int i = l - 1, j = r + 1;
while (i < j) {
do i++; while (a[i] < mid);
do j--; while (a[j] > mid);
if (i < j) {
std::swap(a[i], a[j]);
}
}
Quick_Sort(a, l, j);
Quick_Sort(a, j + 1, r);
}

稳定性与时间复杂度分析:

  • 快速排序并不是稳定的。这是因为我们无法保证相等的数据按顺序被扫描到和按顺序存放。
  • 快速排序的时间复杂度介于 ,最坏的情况,他的时间复杂度是

归并排序(Merge Sort)

简单介绍:

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

算法描述:

方法1:递归法(Top-down)

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

方法2:迭代法(Bottom-up)

  1. 将序列每相邻两个数字进行归并操作,形成 个序列,排序后每个序列包含 个元素
  2. 若此时序列数不是 个则将上述序列再次归并,形成 个序列,每个序列包含 个元素
  3. 重复步骤 ,直到所有元素排序完毕,即序列数为
归并排序-图解

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void Merge_Sort(int a[], int l, int r, int tmp[]) {
if (l >= r) return ;

int mid = l + r >> 1;
Merge_Sort(a, l, mid, tmp), Merge_Sort(a, mid + 1, r, tmp);

int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) tmp[k++] = a[i++];
else {
tmp[k++] = a[j++];
}
}
while (i <= mid) tmp[k++] = a[i++];
while (j <= r) tmp[k++] = a[j++];

for (int i = l, j = 0; i <= r; i++, j++) {
a[i] = tmp[j];
}
}

稳定性与时间复杂度分析:

  • 因为我们在遇到相等的数据的时候必然是按顺序“抄写”到辅助数组上的,所以,归并排序同样是稳定算法。
  • 归并排序的时间复杂度为

希尔排序(Shell Sort)

简单介绍:

  • 在希尔排序出现之前,计算机界普遍存在“排序算法不可能突破 的观点。希尔排序是第一个突破 的排序算法,它是简单插入排序的改进版。希尔排序的提出,主要基于以下两点:
  1. 插入排序算法在数组基本有序的情况下,可以近似达到 复杂度,效率极高。
  2. 但插入排序每次只能将数据移动一位,在数组较大且基本无序的情况下性能会迅速恶化。

算法描述:

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • 选择一个增量序列 ,其中
  • 按增量序列个数 ,对序列进行 趟排序;
  • 每趟排序,根据对应的增量 ,将待排序列分割成若干长度为 的子序列,分别对各子表进行直接插入排序。仅增量因子为 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
希尔排序-图解

算法过程解释:

希尔排序-过程图解

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Shell_Sort(int a[], int l, int r) {
int n = r - l + 1;
for (int gap = n / 2; gap >= 1; gap /= 2) { // gap为步长,每次减为原来的一半。
for (int i = gap; i < n; i++) { // 共gap个组,对每一组都执行直接插入排序
int tmp = a[i];
int j = i - gap;
while (j >= l && tmp < a[j]) { // 如果a[j] < a[j-gap],则寻找a[j]位置,并将后面数据的位置都后移。
a[j + gap] = a[j];
j -= gap;
}
a[j + gap] = tmp;
}
}
}

稳定性与时间复杂度分析:

  • 我们都知道插入排序是稳定算法。但是,希尔(shell)排序是一个多次插入的过程。在一次插入中我们能确保不移动相同元素的顺序,但在多次的插入中,相同元素完全有可能在不同的插入轮次被移动,最后稳定性被破坏,因此,希尔(shell)排序不是一个稳定的算法。

  • 希尔排序的时间复杂度:

堆排序(Heap Sort)

简单介绍:

  • 堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆排序就是把最大堆堆顶的最大数取出,将剩余的堆继续调整为最大堆,再次将堆顶的最大数取出,这个过程持续到剩余数只有一个时结束。

算法描述:

  • 堆是一种特殊的完全二叉树(complete binary tree)。完全二叉树的一个“优秀”的性质是,除了最底层之外,每一层都是满的,这使得堆可以利用数组来表示(普通的一般的二叉树通常用链表作为基本容器表示),每一个结点对应数组中的一个元素。 如下图,是一个堆和数组的相互关系:
堆排序-原理

动图演示:

堆排序-图解

算法实现:

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
void Down(int a[], int l, int r) {
int p = l;
int u = p * 2 + 1;
while (u <= r) {
if (u + 1 <= r && a[u] < a[u + 1]) {
u++;
}
if (a[p] >= a[u]) {
return ;
} else {
std::swap(a[p], a[u]);
p = u;
u = p * 2 + 1;
}
}
}

void Heap_Sort(int a[], int len) {
for (int i = (len - 2) / 2; i >= 0; i--) {
Down(a, i, len - 1);
}
for (int i = len - 1; i > 0; i--) {
std::swap(a[0], a[i]);
Down(a, 0, i - 1);
}
}

稳定性与时间复杂度分析:

  • 堆排序存在大量的筛选和移动过程,属于不稳定的排序算法。
  • 堆排序的时间复杂度:

计数排序(Count Sort)

简单介绍:

  • 计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

算法描述:

  1. 找出待排序的数组中最大和最小的元素;
  2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
计数排序-图解

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
void Count_Sort() {
memset(cnt, 0, sizeof cnt); // 计数数组
for (int i = 1; i <= n; i++) {
cnt[a[i]]++;
}
for (int i = 1; i <= w; i++) {
cnt[i] += cnt[i - 1]; // 求前缀和
}
for (int i = n; i >= 1; --i) { // 排序
b[cnt[a[i]]--] = a[i];
}
memcpy(a, b, sizeof b);
}

稳定性与时间复杂度分析:

  • 计数排序的时间复杂度:

桶排序(Bucket Sort)

简单介绍:

  • 桶排序(Bucket sort)是排序算法的一种,适用于待排序数据值域较大但分布比较均匀的情况。桶排序又叫箱排序,是计数排序的升级版,它的工作原理是将数组分到有限数量的桶子里,然后对每个桶子再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后将各个桶中的数据有序的合并起来。

算法描述:

  1. 找出待排序数组中的最大值max、最小值min
  2. 我们使用 动态数组vector作为桶,桶里放的元素也用vector存储。桶的数量为(max - min) / arr.length + 1
  3. 遍历数组arr,计算每个元素arr[i]放的桶
  4. 每个桶各自排序
  5. 遍历桶数组,把排序好的元素放进输出数组

图片演示:

Bucket_Sort-图片演示-1
Bucket_Sort-图片演示-2

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Bucket_Sort() {
int bucket_size = w / n + 1;
for (int i = 0; i < n; i++) {
bucket[i].clear();
}
for (int i = 1; i <= n; i++) {
bucket[a[i] / bucket_size].push_back(a[i]);
}
int p = 0;
for (int i = 0; i < n; i++) {
/* bucket[i] with other sort */
for (int j = 0; j < bucket[i].size(); j++) {
a[++p] = buck[i][j];
}
}
}

稳定性与时间复杂度分析:

  • 如果使用稳定的内层排序,并且将元素插入桶中时不改变元素间的相对顺序,那么桶排序就是一种稳定的排序算法。由于每块元素不多,一般使用插入排序。此时桶排序是一种稳定的排序算法。
  • 桶排序的时间复杂度:

基数排序(Radix Sorter)

简单介绍:

  • 基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。 排序过程:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

算法描述:

  1. 取得数组中的最大数,并取得位数;
  2. arr为原始数组,从最低位开始取每个位组成radix数组;
  3. radix进行计数排序(利用计数排序适用于小范围数的特点);
基数排序-图解

算法实现:

稳定性与时间复杂度分析:

  • 基数排序的时间复杂度: