说明:以下所有排序算法中nums数组都是从0开始,部分算法也给出了nums数组从1开始的情况。第i趟排序中的i也是从0开始。
直接插入排序:假设部分序列有序,然后将无序的部分循环插入到已有序的序列中。对于随即顺序的序列的时间复杂度:O(N^2),对于基本有序的序列时间复杂度:O(N),空间复杂度:O(1)。代码如下:
/*Straight Insertion Sort **时间复杂度:O(n^2) 最好:O(n) **空间复杂度:O(n) */ void straight_insert_sort(int *nums, int n) { int i, j, temp; for (i = 1; i < n; i++) { j = i-1; temp = nums[i]; while (j >= 0 && nums[j] > temp) { nums[j+1] = nums[j]; j--; } nums[j+1] = temp; } }
二分(折半)插入排序:假设部分序列有序,然后将无序的部分循环插入到已有序的序列中,与直接插入排序不同的地方在于每次比较的是有序序列的中间位置,这就有效的减少了比较的次数,但是每趟比较完需要移动的次数并不会减少,时间复杂度自然不变。对于随即顺序的序列的时间复杂度:O(N^2),有序序列时的时间复杂度大于O(n),空间复杂度:O(1)。代码如下:
/*Binary Insertion Sort **时间复杂度:O(n^2) 最好:O(n) **空间复杂度:O(1) */ void binary_insert_sort(int *nums, int n) { int i, j, low, high, m, temp; for (i = 1; i < n; i++) { low = 0; high = i-1; temp = nums[i]; while (low <= high) { m = (low + high)/2; if (nums[m] > temp) { high = m - 1; } else { low = m + 1; } } for (j = i-1; j > high; j--) { nums[j+1] = nums[j]; } nums[high+1] = temp; } }
希尔排序:将待排序列分割成若干子序列分别进行直接插入排序,第i趟的增量设置为dk[i],换而言之就是分成dk[i]个子序列。直接插入排序可以看做是增列恒定为1的希尔排序。时间复杂度:小于O(N^2),空间复杂度:O(1)。注意:增量序列中的值没有除1以为的公因子,并且最后一个增量必须为1,代码如下:
/*Shell Insert Sort **dk数组存放每趟插入的增量值,最后一个增量为1,k趟插入 **最后一趟插入前已基本有序 **时间复杂度:O(n^2) 较staight insert sort 减少了移动次数 **空间复杂度:O(1) */ void shell_insert_sort(int *nums, int n, int *dk, int k) { int i, j, z, temp; for (i = 0; i < k; i++) { //k个增量值,k趟插入 for (j = dk[i]; j < n; j++) { //第j个数 z = j - dk[i]; temp = nums[j]; while (z >= 0 && nums[z] > temp) { //找到第一个小于待插入数的下标z nums[z+dk[i]] = nums[z]; z -= dk[i]; } nums[z+dk[i]] = temp; //将待插入数插入到z右边第dk[i]位置 } } }
冒泡排序:每一趟从序列第一个元素开始比较nums[i] 和nums[i+1],直到序列最后一个元素,总共进行n-1趟。对于随即顺序的序列共进行比较(n-1)+(n-2)+...+1次,平均下来交换次数为比较次数的一半。时间复杂度:O(N^2),空间复杂度:O(1)。代码如下:
/*Bubble Sort **时间复杂度:O(n^2) **空间复杂度:O(1) */ void bubble_sort(int *nums, int n) { int i, j, temp; for (i = 0; i < n-1; i++) { for (j = 0; j < n-i-1; j++) { if ( nums[j] > nums[j+1]) { temp = nums[j]; nums[j] = nums[j+1]; nums[j+1] = temp; } } } }
简单选择排序:第i趟通过n-i-1次比较从序列nums[i...n-1]选择出第i大的数,总共进行n-1趟,时间复杂度:O(N^2),空间复杂度:O(1)。代码如下:
/*Simple select Sort **第i趟选择出第i小的数 **时间复杂度:O(n^2) **空间复杂度:O(1) */ void simple_select_sort(int *nums, int n) { int i, j, min, temp; for (i = 0; i < n-1; i++) { min = i; j = i+1; while (j < n) { if (nums[j] < nums[min]) { min = j; } j++; } if (min != i) { temp = nums[i]; nums[i] = nums[min]; nums[min] = temp; } } }
堆排序:用heap_adjust(n/2-1....0)将序列建成最大堆,然后依次将堆顶元素与最大堆的末尾元素交换(这时最后一个元素已不属于堆)并调整序列为最大堆。n-1次调整后,序列有序。最坏情况下时间复杂度:O(nlogn),空间复杂度:O(1)。代码如下:
/*Heap Sort **堆排序不提倡用于n比较小的序列 **时间复杂度:O(nlogn) **空间复杂度:O(1) */ void heap_adjust(int *nums, int s, int m) { //nums[s...m]中除nums[s]外均满足堆的定义,调整nums[s]使得nums[s...m]成为最大堆 //nums数组下标从0开始 int j; int temp = nums[s]; for (j = (s+1)*2-1; j <= m; j = (j+1)*2-1) { if (j < m && nums[j] < nums[j+1]) ++j; //沿较大的孩子结点向下筛选 if ( temp >= nums[j]) break; nums[s] = nums[j]; s = j; } nums[s] = temp; } void heap_sort(int *nums, int n) { //nums数组下标从0开始 int i, temp; for (i = n/2 - 1; i >=0; i--) { heap_adjust(nums, i, n-1); } for (i = n-1; i > 0; i--) { temp = nums[i]; nums[i] = nums[0]; nums[0] = temp; heap_adjust(nums, 0, i-1); } } void heap_adjust_(int *nums, int s, int m) { //nums[s...m]中除nums[s]外均满足堆的定义,调整nums[s]使得nums[s...m]成为最大堆 //nums数组下标从1开始 int j; int temp = nums[s]; for (j = s*2; j <= m; j = j*2) { if (j < m && nums[j] < nums[j+1]) ++j; //沿较大的孩子结点向下筛选 if ( temp >= nums[j]) break; nums[s] = nums[j]; s = j; } nums[s] = temp; } void heap_sort_(int *nums, int n) { //nums数组下标从1开始 int i, temp; for (i = n/2; i >0; i--) { heap_adjust(nums, i, n); } for (i = n; i > 1; i--) { temp = nums[i]; nums[i] = nums[1]; nums[1] = temp; heap_adjust(nums, 1, i); } }<
按平均时间将排序分为四类:
(1)平方阶(O(n2))排序
一般称为简单排序,例如直接插入、直接选择和冒泡排序;
(2)线性对数阶(O(nlgn))排序
如快速、堆和归并排序;
(3)O(n1+£)阶排序
£是介于0和1之间的常数,即0<£<1,如希尔排序;
(4)线性阶(O(n))排序
如桶、箱和基数排序。
各种排序方法比较
简单排序中直接插入最好,快速排序最快,当文件为正序时,直接插入和冒泡均最佳。
影响排序效果的因素
因为不同的排序方法适应不同的应用环境和要求,所以选择合适的排序方法应综合考虑下列因素:
①待排序的记录数目n;
②记录的大小(规模);
③关键字的结构及其初始状态;
④对稳定性的要求;
⑤语言工具的条件;
⑥存储结构;
⑦时间和辅助空间复杂度等。
不同条件下,排序方法的选择
(1)若n较小(如n≤50),可采用直接插入或直接选择排序。
当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
(2)若文件初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜;
(3)若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
若要求排序稳定,则可选用归并排序。但本章介绍的从单个记录起进行两两归并的 排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定 的,所以改进后的归并排序仍是稳定的。
排序算法的稳定性
1) 稳定的:如果存在多个具有相同排序码的记录,经过排序后,这些记录的相对次序仍然保持不变,则这种排序算法称为稳定的。
插入排序、冒泡排序、归并排序、分配排序(桶式、基数)都是稳定的排序算法。
2)不稳定的:否则称为不稳定的。
直接选择排序、堆排序、shell排序、快速排序都是不稳定的排序算法。