排序算法 |
最好时间复杂度 |
平均时间复杂度 |
最坏时间复杂度 |
空间复杂度 |
稳定性 |
直接插入排序 |
有序:O(n) |
O(n2) |
逆序:O(n2) |
O(1) |
稳定 |
希尔排序 |
复杂度未得到证明 |
O(n1.3−2) |
d=1:O(n2) |
O(1) |
不稳定 |
冒泡排序 |
有序:O(n) |
O(n2) |
逆序:O(n2) |
O(1) |
稳定 |
快速排序 |
划分均匀:O(nlog2n) |
O(nlog2n) |
有序:O(n2) |
O(log2n∼n) |
不稳定 |
简单选择排序 |
O(n2) |
O(n2) |
O(n2) |
O(1) |
不稳定 |
堆排序 |
O(nlog2n) |
O(nlog2n) |
O(nlog2n) |
O(1) |
不稳定 |
归并排序 |
|
|
O(nlog2n) |
O(n) |
稳定 |
基数排序 |
|
|
O(d(r+n)) |
O(r) |
稳定 |
1 直接插入排序(Insertion Sort)
插入排序的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
优化:先用折半查找找到应该插入的位置,再移动元素。
2 希尔排序(Shell Sort)
先将待排序表分割成若干形如L[i,i+d,i+2d,...,i+kd]的特殊子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直到d=1为止。(希尔本人建议:d初始值为n,然后每次将增量缩小一半)
适用性:仅适用于顺序表,不适用于链表
3 冒泡排序(Bubble Sort)
冒泡排序是一种交换排序,它的思路就是在待排序的数据中,两两比较相邻元素的大小,看是否满足大小顺序的要求,如果满足则不动,如果不满足则让它们互换。然后继续与下一个相邻元素的比较,一直到一次遍历完成。一次遍历的过程就被成为一次冒泡,一次冒泡的结束至少会让一个元素移动到了正确的位置。所以要想让所有元素都排序好,一次冒泡还不行,我们得重复N次去冒泡,这样最终就完成了N个数据的排序过程。
对一个长度为 n 的排列 p[i] 进行一轮冒泡排序的伪代码如下:
1 2 3
| for i = 1 to n-1: if p[i] > p[i + 1]: swap(p[i], p[i + 1])
|
4 快速排序(Quck Sort)
快速排序是一种交换排序,它的思路:在待排序表L[1...n]中任取一个元素 pivot 作为枢轴(或基准,通常取首元素),通过一趟排序表划分为独立的两部分L[1...k−1]和L[k+1...n],使得L[1...k−1]中的所有元素小于 pivot,L[k+1...n]中的所有元素大于等于 pivot,则 pivot放在了其最终位置L[k]上,这个过程称为一次“划分”。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。
若每一次选中的“枢轴”将待排序序列划分为均匀的两个部分,则递归深度最小,算法效率最高。
优化:
- 选头、中、尾、三个位置的元素,取中间值作为枢轴元素;
- 随机选一个元素作为枢轴元素。
5 简单选择排序(Selection Sort)
选择排序:每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列。
适用性:既可以用于顺序表,也可用于链表
6 堆排序(Heap Sort)
堆排序是选择排序的一种,思路:每一趟将堆顶元素加入到有序子序列(与待排序序列中的最后一个元素交换),并将待排序元素序列再次调整为大根堆(小元素不断下坠)
什么是堆?
若n个关键字序列L[1..n]满足下面某一条性质,则称为堆(Heap):
- 若满足:L(i)≥L(2i)且L(i)≥L(2i+1),(1≤i≤n/2)——大根堆(大顶堆)
- 若满足:L(i)≤L(2i)且L(i)≤L(2i+1),(1≤i≤n/2)——小根堆(小顶堆)
建立大根堆:
- 把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足则进行调整
- 检查当前结点是否满足
根节点>=左、右
,若不满足,将当前结点与更大的一个孩子互换
- 若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断“下坠”)
建堆O(n),排序O(nlogn)
在堆中插入新元素:
- 对于大根堆,新元素放到表尾,与父结点相比,若新元素比父节点更大,则将二者互换。新元素就这样一路上升,直到无法继续上升。
在堆中删除元素:
- 被删除元素用堆底元素代替,然后让该元素不断下坠,直到无法下坠为止。
7 归并排序(Merge Sort)
把两个或多个已经有序的序列合并成一个。
核心操作:把数组内的两个有序序列归并为一个。
8 基数排序(Radix Sort)
假设长度为n的线性表中每个结点a的关键字由d元组(kjd−1,kjd−2,kjd−3,…,kj1,kj0)组成,其中,0≤kji≤r−1(0≤j<n,0≤i≤d−1),r称为基数。
基数排序得到递减序列的过程如下:
- 初始化:设置r个空队列,Q_{r-1},Q_{r-2}...Q_0
- 按照各个关键字位权重递增的次序(个、十、百),对d个关键字位分别做“分配”和“收集”
- 分配:顺序扫描各个元素,若当前处理的关键字位=x,则将元素插入Qx队尾
- 收集:把Q_{r-1},Q_{r-2}...Q_0各个队列中的结点依次出队并链接
注意:
- 基数排序不是基于比较的排序算法;基数排序通常基于链式存储实现。
- 需要r个辅助队列,空间复杂度为O(r),其中r为基数;
- 一趟分配O(n),一趟收集O(r),总共d趟分配、收集,总的时间复杂度=O(d(n+r))
基数排序擅长解决的问题:
- 数据元素的关键字可以方便地拆分成d组,且d较小;
- 每组的关键字的取值范围不大,即r较小
- 数据元素个数n较大
9 外部排序
外部排序原理:
外部排序:数据元素太多,无法一次全部读入内存进行排序。
使用归并排序的方法,最少只需在内存中分配3块大小的缓冲区即可对任意一个大文件进行排序。
步骤:
- 构造初始归并段:归并排序要求各个子序列有序,每次读入两个块的内容,进行内部排序后写回磁盘;
- 第一趟归并:将两个归并段归并为一个,缓冲区1/2空了就要立即用归并段1/2的下一块补上
- 第二趟归并:与第一趟类似…
时间开销分析:
- 外部排序时间开销=读写外存时间+内部排序所需时间+内部归并所需时间
优化:
- 多路归并:采用多路归并可以减少归并趟数,从而减少磁盘I/O(读写)次数
- 减少初始归并段数量:生成初始归并段的“内存工作区”越大,初始归并段越长,则可减少初始归并段数量r
代码实现
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 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
| #include<bits/stdc++.h> using namespace std;
const int maxn = 1e5 + 5; int arr[maxn] = {0, 53, 17, 78, 9, 45, 65, 87, 32}; int n;
void insert_sort() { int i, j, tmp; for (i = 2; i <= n; i++) { tmp = arr[i]; for (j = i - 1; j >= 1; j--) { if (arr[j] > tmp) { arr[j + 1] = arr[j]; } else { break; } } arr[j + 1] = tmp; } }
void shell_sort() { int i, j, tmp, d; for (d = n / 2; d >= 1; d /= 2) { for (i = 1 + d; i <= n; i++) { tmp = arr[i]; for (j = i - d; j >= d; j -= d) { if (arr[j] > tmp) { arr[j + d] = arr[j]; } else { break; } } arr[j + d] = tmp; } } }
void bubble_sort() { for (int i = 1; i <= n - 1; i++) { bool flag = false; for (int j = 1; j <= n - i; j++) { if (arr[j] > arr[j + 1]) { swap(arr[j], arr[j + 1]); flag = true; } } if (flag == false) { break; } } }
int partition(int low, int high) { int pivot = arr[low]; while (low < high) { while (low < high && arr[high] >= pivot) { high--; } arr[low] = arr[high]; while (low < high && arr[low] <= pivot) { low++; } arr[high] = arr[low]; } arr[low] = pivot; return low; }
void quck_sort(int low, int high) { if (low < high) { int pivot_pos = partition(low, high); quck_sort(low, pivot_pos - 1); quck_sort(pivot_pos + 1, high); } }
void select_sort() { for (int i = 1; i <= n - 1; i++) { int mini = i; for (int j = i + 1; j <= n; j++) { if (arr[j] < arr[mini]) { mini = j; } } if (mini != i) { swap(arr[i], arr[mini]); } } }
void heap_adjust(int k, int len) { arr[0] = arr[k]; for (int i = 2 * k; i <= len; i *= 2) { if (i < len && arr[i] < arr[i + 1]) { i++; } if (arr[0] >= arr[i]) { break; } else { arr[k] = arr[i]; k = i; } } arr[k] = arr[0]; }
void build_max_heap(int len) { for (int i = len / 2; i > 0; i--) { heap_adjust(i, len); } }
void heap_sort() { build_max_heap(n); for (int i = n; i > 1; i--) { swap(arr[i], arr[1]); heap_adjust(1, i - 1); } }
int *b = (int *)malloc(n * sizeof(int));
void merge(int low, int mid, int high) { int i, j, k; for (k = low; k <= high; k++) { b[k] = arr[k]; } for (i = low, j = mid + 1, k = low; i <= mid && j <= high; k++) { if (b[i] < b[j]) { arr[k] = b[i++]; } else { arr[k] = b[j++]; } } while (i <= mid) arr[k++] = b[i++]; while (j <= high) arr[k++] = b[j++]; }
void merge_sort(int low, int high) { if (low < high) { int mid = (low + high) / 2; merge_sort(low, mid); merge_sort(mid + 1, high); merge(low, mid, high); } }
int main() {
n = 8; merge_sort(1, n); for (int i = 1; i <= n; i++) { cout<< arr[i]<< " "; } return 0; }
|
练习