排序算法总结
大多数排序算法都需要查找操作(一般是找到第一个比它小的元素或者第一个比它大的元素),而查找操作一般都会设置哨兵,哨兵可以免去查找过程中每次比较后都要判断查找位置是否越界的小技巧,看似与原先差别不大,但是总数据较多时,效率提高很明显,是非常好的编程技巧,当然哨兵的第二个作用就是作为临时存储。
本篇博文所用系列定义:
1 #define SIZE 100 2 typedef struct SqList{ 3 int r[SIZE + 1]; //一个整数系列,第0个元素为哨兵 4 int length; //整数系列长度 5 }SqList;
-
插入排序
-
直接插入排序
原理:将数组分为无序区和有序区两个区,然后不断将无序区的第一个元素按大小顺序插入到有序区中去,最终将所有无序区元素都移动到有序区完成排序。
实现:
1 void InsertSort(SqList &L) 2 { 3 for(i = 2; i <= L.length; i++) 4 { 5 if(L.r[i] < L.r[i - 1]) 6 { 7 L.r[0] = L.r[i]; 8 L.r[i] = L.r[i - 1]; 9 for(j = i - 2; L.r[0] < L.r[j]; j--) 10 L.r[j + 1] = L.r[j]; 11 L.r[j + 1] = L.r[0]; 12 } 13 } 14 }
算法分析:当待排序序列为正序时,比较次数为N-1,不需移动;当待排序系列为逆序时,比较次数为(N+2)(N-1)/2,移动次数为(N+4)(N-1)/2。我们认为待排序系列可能出现的各种排列的概率相同,得到比较次数和移动次数都为N2/4。由此可以得出直接插入排序的时间复杂度为O(N2),其空间复杂度为O(1)。
对于直接排序算法的各种改进算法:折半插入排序、2-路插入排序、表插入排序。这些算法只是减少了排序算法过程中的比较次数或者移动次数,并没有减少算法的数量级,其时间复杂度仍为O(N2)。
-
希尔排序
原理:初始化一个增量,将序列按增量划分为元素个数相同的若干个子系列,使用直接插入排序法进行排序,然后不断缩小增量直至为1,最后使用直接插入排序完成排序。
实现:
1 // d为初始增量系列,dLength为d的长度 2 void ShellSort(SqList &L, int d[], int dLength) 3 { 4 for(i = 0; i < dLength; i++) 5 ShellInsert(L, d[i]); 6 } 7 8 void ShellInsert(SqList &L, int dk) 9 { 10 for(i = dk + 1; i <= L.length; i++) 11 if(L.r[i] < L.r[i - dk]) 12 { 13 L.r[0] = L.r[i]; 14 for(j = i - dk; j > 0 && L.r[0] < L.r[j]; j -= dk) 15 L.r[j + dk] = L.r[j]; 16 L.r[j + dk] = L.r[0]; 17 } 18 }
算法分析:希尔排序的时间复杂度无法计算,但是效率优于一般的插入排序。需要注意的是:增量系列中的值应该没有除1之外的公因子,并且最后一个增量值必须是1。
-
快速排序
-
冒泡排序
原理:将序列划分为无序和有序区,其中有序区的所有元素都大于无序区的所有元素,每一次排序过程,都将选出无序区的最大元素,将其作为有序区的第一个元素。
实现:
1 void BubbleSort(SqList &L) 2 { 3 for(i = 1; i < L.length; i++) 4 for(j = 1; j <= L.length - i; j++) 5 if(L.r[i] > L.r[i + 1]) 6 Swap(L.r[i], L.r[i + 1]); 7 }
算法分析:当待排序序列为正序时,比较次数为N-1,不需移动;当待排序系列为逆序时,比较次数为N(N-1)/2,移动次数为N(N-1)/2。由此可以得出其时间复杂度为O(N2),其空间复杂度为O(1)。
-
快速排序
原理:分治思想,在系列中选择一个记录K,对系列中其他元素进行如下操作,比K小的都放在K之前,比K大的都放在K之后,将其分成两个子系列,重复此过程,直至子系列长度为1,排序完成。
实现:
1 void QuickSort(SqList &L) 2 { 3 QSort(L, 1, length); 4 } 5 6 void QSort(SqList &L, int low, int high) 7 { 8 if(low < high) 9 { 10 pivotLoc = Partition(L, low, high); 11 QSort(L, low, pivotLoc - 1); 12 QSOrt(L, pivotloc + 1, high); 13 } 14 } 15 16 int Partition(SqList &L, int low, int high) 17 { 18 //选取系列的第一个元素作为枢轴 19 L.r[0] = L.r[low]; 20 while(low < high) 21 { 22 while(low < high && L.r[high] >= L.r[0]) 23 high--; 24 L.r[low] = L.r[high]; 25 while(low < high && L.r[low] <= L.r[0]) 26 low++; 27 L.r[high] = L.r[low]; 28 } 29 L.r[low] = L.r[0]; 30 return low; 31 }
算法分析:当待排序序列为正序时,快速排序将会蜕化为冒泡排序。根据分析可以得出快速排序时间复杂度为O(nlogn),在所有同数量级的算法中,快速排序的性能最好。空间复杂度:排序过程使用递归,需要一个栈空间。
-
选择排序
-
简单选择排序
原理:将序列划分为无序和有序区,每次循环,寻找无序区中的最小值和无序区的首元素交换,有序区扩大一个,循环最终完成全部排序。
实现:
1 void SelectSort(SqList &L) 2 { 3 for(i = 1; i < L.length; i++) 4 { 5 minIndex = i; 6 for(j = i + 1; j < L.length; j++) 7 { 8 if(L.r[j] < L.r[minIndex]) 9 minIndex = j; 10 } 11 Swap(L.r[i], L.r[minIndex ]); 12 } 13 }
算法分析:时间复杂度O(N2),空间复杂度O(1)。
-
堆排序
堆:堆实际上是一棵完全二叉树,其任何一非叶结点满足性质:任何一非叶结点的关键字不大于或者不小于其左右孩子结点的关键字。
堆分为大顶堆和小顶堆,满足任何一非叶结点的关键字不小于其左右孩子结点的关键字性质的称为大顶堆,满足任何一非叶结点的关键字不大于于其左右孩子结点的关键字性质的称为小顶堆。由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。
原理:对待排序系列建立一棵完全二叉树,对其进行堆调整,每次将其根结点取出,然后继续调整堆,直至结束。
实现:
1 void HeapSort(SqList &L) 2 { 3 BuildHeap(L); 4 for(i = L.length; i >= 1; i--) 5 { 6 swap(L.r[1], L.r[i]); //取出堆顶元素,放入适当位置 7 HeapAdjust(L, 1, i - 1); 8 } 9 } 10 11 void BuildHeap(SqList &L) 12 { 13 //所有非叶子结点 14 for(i = L.length / 2; i >= 1; i--) 15 { 16 HeapAdjust(a, i); 17 } 18 } 19 20 void HeapAdjust(SqList &L, int i) //调整堆 21 { 22 int lchild = 2 * i; 23 int rchild = 2 * i + 1; 24 int max = i; 25 //只针对非叶子结点 26 if(i <= L.length / 2) 27 { 28 if(lchild <= L.length && L.r[lchild] > L.r[max]) 29 { 30 max = lchild; 31 } 32 if(rchild <= L.length && L.r[rchild] > L.r[max]) 33 { 34 max = rchild; 35 } 36 if(max != i) 37 { 38 Swap(L.r[i], L.r[max]); 39 HeapAdjust(a, max); 40 } 41 } 42 }
算法分析:堆排序对记录数较小的系列并不适用,其对记录数较大的系列很有效。时间复杂度O(NlogN),空间复杂度O(1)。
-
归并排序
原理:分治思想,将序列划分为两个子系列,假设这两个子系列都是有序的,对这两个系列进行归并操作,递归进行直至有序。
实现:
1 bool MergeSort(SqList &L) 2 { 3 MSort(L, 1, n); 4 } 5 6 void MSort(SqList &L, int low, int high) 7 { 8 if (low< high) 9 { 10 int mid = (low+ high) / 2; 11 MSort(a, low, mid); 12 MSort(a, mid + 1, high); 13 Merge(a, low, mid, high); 14 } 15 } 16 17 // temp为一个辅助数组 18 void Merge(SqList &L, int low, int mid, int high) 19 { 20 int i = low, j = mid + 1; 21 int m = mid, n = high; 22 int k = 1; 23 while (i <= m && j <= n) 24 { 25 if (a[i] <= a[j]) 26 temp[k++] = a[i++]; 27 else 28 temp[k++] = a[j++]; 29 } 30 while (i <= m) 31 temp[k++] = a[i++]; 32 while (j <= n) 33 temp[k++] = a[j++]; 34 for (i = 1; i <= k; i++) 35 a[low+ i] = temp[i]; 36 }
算法分析:时间复杂度O(NlogN),空间复杂度O(N)。
效率对比
排序方法 | 时间复杂度 | 最坏情况 | 空间复杂度 | 稳定性 |
插入排序 | O(N2) | O(N2) | O(1) | 稳定 |
希尔排序 | — | — | — | 不稳定 |
冒泡排序 | O(N2) | O(N2) | O(1) | 稳定 |
快速排序 | O(NlogN) | O(N2) | O(logN)[递归栈] | 不稳定 |
简单选择排序 | O(N2) | O(N2) | O(1) | 稳定 |
堆排序 | O(NlogN) | O(NlogN) | O(1)[上述代码可以不用递归] | 不稳定 |
归并排序 | O(NlogN) | O(NlogN) | O(logN)[递归栈] | 稳定 |