稳定性:设在排序前的序列中记录 Ri 领先于 Rj(即 i<j ),且 Ri、Rj 对应的关键字为 Ki、Kj,如果 Ki=Kj 并且在排序后的序列中 Ri 仍领先于 Rj,称所用方法是稳定的。
一、插入排序
1.直接插入排序
从数组的第二个数据开始比较,由于num[1:i-1]已经有序,若num[i]<num[i-1],则说明num[i]需要插入到前面,反之不用。插入过程为从后往前依次将比num[i]大的元素往后挪,直到num[k]<num[i],
则num[k+1]=num[i]。用哨兵num[0]存储num[i],减少一次比较。
void InsertSort(int num[],int n) {
int i, j;
for (i = 2; i <= n; i++) {
if (num[i] < num[i - 1]) {
num[0] = num[i];
for (j = i - 1;num[j]>num[0]; --j) {
num[j + 1] = num[j];
}
num[j + 1] = num[0];
}
}
}
可以适用于链表
2.折半插入排序
由于num[1:i-1]属于有序序列,所以将直接插入排序查找num[k]的过程改为折半查找,提高一点点效率。
void HalfInsertSort(int num[],int n) {
int i, j;
int low, high, mid;
for (i = 2; i <= n; i++) {
if (num[i] < num[i - 1]) {
num[0] = num[i];
low = 1; high = i - 1;
while (low <= high) {
mid = (low + high) / 2;
if (num[mid] > num[0]) high = mid - 1;
else low = mid + 1;
}
for (j = i - 1; j >= high + 1; --j) {
num[j + 1] = num[j];
}
num[high + 1] = num[0];
}
}
}
虽然查找k的过程时间复杂度为O(log2n),但是移动元素的次数并未改变,时间复杂度依然是O(n2)
3.希尔排序
先将待排序表分割成若干形如L(i,i+d,i+2d,...,i+kd)的子表,对各子表进行直接插入排序,再更改步长继续划分子表,直到步长为1,为整体的排序。常用方法是d1=n/2,,直到d=1。
void ShellSort(int num[],int n) {
int i, j, d;
for (d = n / 2; d >= 1; d = d / 2) {
for (i = d + 1; i <= n; i++) {
if (num[i] < num[i - d]) {
num[0] = num[i];
for (j = i - d; j > 0 && num[j] > num[0]; j -= d) {
num[j + d] = num[j];
}
num[j + d] = num[0];
}
}
}
}
二、交换排序
1.冒泡排序
基本思想:从后往前(或者从前往后)两两比较相邻元素的值,若为A[i-1]>A[i],则交换它们,直到序列比较完。我们这这样一次过程为一趟冒泡,结果是将最小的元素交换到待排序序列的第一个位置。下一趟冒泡时,前一趟确定的元素就不再参与比较,每趟冒泡的结果是把待排序列中的最小元素放到待排序列最前的位置,这样最多n-1趟冒泡就能把所有元素排好序。
void BubbleSort(int num[],int n) {
int i, j;
for (i = 1; i < n; i++) {
int flag = 1;
for (j = n; j > i; j--) {
if (num[j] < num[j - 1]) {
int temp = num[j];
num[j] = num[j - 1];
num[j - 1] = temp;
flag = 0;
}
}
if (flag) return;
}
}
2.快速排序
基本思想:在待排序表L[1:n]中任取一个元素pivot,通过一趟排序将待排序表划分为独立的两部分L1[1:k-1]和L2[k+1:n],其中L1中的元素均小于pivot,L2中的元素均大于等于pivot,则pivot放在了其最终的位置上,这个过程称为一次划分。然后递归地对L1,L2重复上述过程,直至每部分内只有一个元素或空位置。
int Partition(int num[], int low, int high) {
int pivot = num[low];
while (low < high) {
while (low < high&&num[high] >= pivot) --high;
num[low] = num[high];
while (low < high&&num[low] < pivot) ++low;
num[high] = num[low];
}
num[low] = pivot;
return low;
}
void QuickSort(int num[], int low, int high) {
if (low < high) {
int pos = Partition(num, low, high);
QuickSort(num, low, pos - 1);
QuickSort(num, pos + 1, high);
}
}
快速排序的趟数取决于划分是否堆成有关,快速排序的最坏情况发生在两个区域分别包含n-1个和0个元素时,并且每次递归都如此,即对应初始排序表基本有序或基本逆序时,就得到最坏情况下的时间复杂度O(n2)。
为避免这种情况,一种方法是尽量选取一个可以将数据中分的枢纽元素,如选择num[low],num[(low+high)/2],num[high]中的中间值。或者随机的从num[low:high]中选择取枢纽元素。
不过快速排序在平均情况下的运行时间与其最佳情况下的运行时间很接近,而不是接近其最坏的情况。快速排序是所有内部排序算法中平均性能最优的排序算法。
三、选择排序
1.简单选择排序
基本思想:每次从待排序列中选择最小(或最大)的元素,重复n-1次。
void SelectSort(int num[],int n) {
int i, j;
for (i = 1; i < n ; i++) {
int min = i;
for (j = i + 1; j <= n; j++) {
if (num[j] < num[min]) min = j;
}
if (min != i) {
int temp = num[min];
num[min] = num[i];
num[i] = temp;
}
}
}
2.堆排序
堆是具有下列性质的完全二叉树:每个结点的值都小于或等于其左右孩子结点的值(L[i]<=L[2i]且L[i]<=L[2i+1])(称为小根堆),或每个结点的值都大于或等于其左右孩子结点的值(L[i]>=L[2i]且L[i]>=L[2i+1])(称为大根堆)。
基本思想:首先将待排序的记录序列构造成一个堆,此时,选出了堆中所有记录的最小者,然后将它从堆中移走,并将剩余的记录再调整成堆,这样又找出了次小的记录,以此类推,直到堆中只有一个记录。
void HeapAdjust(int num[], int k, int len) {
num[0] = num[k];
for (int i = 2 * k; i <= len; i *= 2) {
if (i < len&&num[i] < num[i + 1]) i++; //使i指向左右子树中最大的
if (num[0] >= num[i]) break;
else {
num[k] = num[i];
k = i;
}
}
num[k] = num[0];
}
void BuildMaxHeap(int num[], int len) {
for (int i = len / 2; i > 0; i--) {
HeapAdjust(num, i, len);
}
}
void HeapSort(int num[],int len) {
BuildMaxHeap(num, len);
for (int i = len; i > 1; i--) {
int temp = num[i];
num[i] = num[1];
num[1] = temp;
HeapAdjust(num, 1, i - 1);
}
}
同时堆也支持插入和删除,操作完调整堆即可。
四、归并排序
思想:将两个或两个以上的有序表组合成一个新的有序表。假定待排序表中含有n个记录,则可将其视为n给有序的子表,每个子表的长度为1,然后两两归并,得到个长度为2或1的有序表;继续两两归并,如此重复,直到合并成一个长度为n的有序表为止,这种排序方法称为2路归并排序。
void Merge(int num[], int copy[], int low, int mid, int high) {
for (int k = low; k <= high; k++) {
copy[k] = num[k];
}
int i = low, j = mid + 1, k = low;
while (i <= mid && j <= high) {
if (copy[i] <= copy[j]) num[k++] = copy[i++];
else num[k++] = copy[j++];
}
while (i <= mid) num[k++] = copy[i++];
while (j <= high) num[k++] = copy[j++];
}
void MergeSort(int num[],int copy[], int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
MergeSort(num, copy, low, mid);
MergeSort(num, copy, mid + 1, high);
Merge(num, copy, low, mid, high);
}
}
思考:如何实现辅助空间O(1)的归并排序?
五、基数排序
思想:对单关键字构成进行分解为 k = k1, k2, …, kd,借助多关键字排序的方法对单关键字排序。
如对扑克牌,先按照花色排序成4个队列,再按照队列先后收集,再按照牌面大小排序,排为13个队列,这时相同牌面大小的,花色大的再队列前,之后再收集即可。
或按照个位大小,十位大小....排序。
六、排序算法总结
排序方法 | 最好时间 | 最坏时间 | 平均时间 | 辅助空间 | 稳定性 | 一趟结束是否有元素在最终位置上 |
直接插入 | O(n) | O(n2) | O(n2) | O(1) | 稳定 | 否 |
折半插入 | O(n) | O(n2) | O(n2) | O(1) | 稳定 | 否 |
希尔排序 | O( | O(1) | 不稳定 | 否 | ||
冒泡排序 | O(n) | O(n2) | O(n2) | O(1) | 稳定 | 是 |
快速排序 | O(nlog2n) | O(n2) | O(nlog2n) | O(log2n) | 不稳定 | 是 |
简单选择 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 | 是 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 | 是 |
2路归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 稳定 | 否 |
基数排序 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O(n+r) | 稳定 | 否 |
- 简单的排序算法(直接排序、折半排序、冒泡法)的最好时间复杂度都为O(n), 该类算法的输入在接近有序时的效率比较高。
- 三种平均时间复杂度为 O(nlog2n) 的算法中,快速排序平均效率高,但最坏时间复杂度为O(n2),且空间复杂度为O(log2n)。堆排序最坏时间复杂度为O(nlog2n),且空间复杂度仅为 O(1)。归并排序最坏时间复杂度为O(nlog2n),且是稳定算法,但空间复杂度为 O(n)。但是快速排序依然是最优的内部排序算法,堆排序涉及到过多的数据移动,不利于内存读取。
排序算法的选择
1.若 n 较小,可采用直接插入或简单选择排序 当规模较小时,直接插入排序较好,它比选择排序有更少的比较次数,且稳定; 当规模较大时,因为简单选择移动的记录数少于直接插入,所以宜选用简单选择排
2.若初始状态基本有序,则应选用直接插入、冒泡排序;
3.若 n 较大,应采用时间复杂度为 O(nlog2n) 的方法:快速排序、堆排序或归并排序。(数据量特别大的外部排序采用归并排序)
4.特殊的基数排序