说明:以下所有排序算法中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排序、快速排序都是不稳定的排序算法。