排序算法
算法比较
稳定性
插入排序,冒泡排序,二路归并排序和基数排序是稳定的排序方法;
选择排序,希尔排序,快速排序和堆排序是不稳定的排序方法;
复杂度
排序方法 | 平均时间 | 最坏情况 | 辅助空间 |
插入排序 | O(n^2) | O(n^2) | O(1) |
希尔排序 | O(nlogn) | O(nlogn) | O(1) |
冒泡排序 | O(n^2) | O(n^2) | O(1) |
快速排序 | O(nlogn) | O(n^2) | O(logn) |
选择排序 | O(n^2) | O(n^2) | O(1) |
堆排序 | O(nlogn) | O(nlogn) | O(1) |
归并排序 | O(nlogn) | O(nlogn) | O(n) |
基数排序 | O(d(n+r)) | O(d(n+r)) | O(r+n) |
方法选择
(1) 排序数据的规模n较大,关键字元素分布比较随机,并且不要求排序稳定性时,宜选用快速排序;
(2) 排序数据规模n较大,内存空间又允许,并且有排序稳定性要求,宜采用归并排序;
(3) 排序数据规模n较大,元素分布可能出现升序或者逆序的情况,并且对排序的稳定性不要求,宜采用堆排序或者归并排序;
(4) 排序数据规模n较小,元素基本有序,或者分布也比较随机,并且有排序稳定性要求时,宜采用插入排序;
(5) 排序数据规模n较小,对排序稳定性又不做要求时,宜采用选择排序;
算法实现
插入排序
插入是个比较容易理解的排序算法,每次取下一个未排序的数,在前面已排序的部分从后往向前查找合适的位置,并将该数插入,而不满足排序条件的数字则需要不断的复制到下一位置,为待排序数字腾出位置;
1 void insert_sort(int a[], int n) 2 { 3 int i = 0; 4 int p = 0; 5 int temp = 0; 6 7 /* 认为第一个数字是有序的,从第二个数字开始 */ 8 for (i = 1; i < n; ++i) 9 { 10 temp = a[i]; /* 记录待插入数字 */ 11 p = i - 1; /* 从前面已经排好序的位置中选择 */ 12 13 /* 查找可插入位置,注意0位置也会进入循环,p可能为-1 */ 14 while (p >= 0 && temp < a[p]) 15 { 16 a[p+1] = a[p]; /* 把不符合插入位置的数据后移 */ 17 p--; /* 继续向前查找 */ 18 } 19 20 a[p+1] = temp; /* 待排序元素插入对应位置 */ 21 } 22 }
(1) 插入排序接住了一个temp作为辅助空间;
(2) 插入排序是一个稳定的排序方法;
(3) 最优情况:当序列是以有序状态输入时,达到最小比较次数,即n-1次比较,无需移动元素;
最坏情况:当序列是以逆序状态输入时,达到最大比较次数n(n-1)/2,需要移动n(n-1)/2次元素;
平均情况:最优与最坏情况的平均比较次数约为n^2/4次;
综上:该算法的平均复杂度为O(n^2);
选择排序
选择排序是按照位置进行的,即从第1个到第n个位置,依次选择未排序部分最小的值放入,如果最小值不是当前位置的初始值,则需要进行数值对调;
1 void select_sort(int a[], int n) 2 { 3 int i = 0; 4 int j = 0; 5 int min = 0; 6 int temp = 0; 7 8 /* 遍历序列,最后一个元素就不需要遍历了 */ 9 for (i = 0; i < n - 1; ++i) 10 { 11 /* 设置最小值为当前位置 */ 12 min = i; 13 14 /* 从后面查找比当前最小值还小的值的位置 */ 15 for (j = i + 1; j < n; ++j) 16 { 17 /* 不断与前一个最小值进行比较,如果更小则更新位置 */ 18 /* 注意这里要使用每次的最小值a[min]与后面元素进行比较 */ 19 if (a[j] < a[min]) 20 { 21 min = j; 22 } 23 } 24 25 /* 起始记录位置与最小值位置不一致,说明存在更小值,交换位置 */ 26 if (i != min) 27 { 28 temp = a[i]; 29 a[i] = a[min]; 30 a[min] = temp; 31 } 32 } 33 }
(1) 选择排序是不稳定排序;
(2) 选择排序的交换次数,当为有序序列时最优,交换次数为0,当为逆序序列时最坏,交换次数为3(n-1),其中3为交换操作;
(3) 选择排序的比较次数是不随序列是否有序的原始状态变化的,均为n(n-1)/2次比较;即算法的平均复杂度为O(n^2);
冒泡排序
冒泡排序的思想是未排序的部分从头到尾两两比较,若逆序则交换,每次排序后,最大的元素都会在序列尾端,然后再对前面未排序的部分进行起泡;
1 /* 进一步优化,可以加flag判断是否交换,若无交换则排序完成 */ 2 /* 如下面注释掉的部分代码 */ 3 void bubble_sort(int a[], int n) 4 { 5 int i = 0; 6 int j = 0; 7 int temp = 0; 8 //int flag = 0; 9 10 /* 不断将最大数起泡到序列结尾,n-1次完成 */ 11 for (i = 0; i < n - 1; ++i) 12 { 13 /* 如果没有进行交换,则说明排序完成,直接退出 */ 14 //if (flag == 0) 15 //{ 16 // break; 17 //} 18 19 /* 每次排序都需要重置标记 */ 20 //flag = 0; 21 22 /* 对尚未排好序的序列从头到尾进行起泡 */ 23 for (j = 0; j < n-1-i; ++j) 24 { 25 /* 交换数据 */ 26 if (a[j] > a[j+1]) 27 { 28 temp = a[j]; 29 a[j] = a[j+1]; 30 a[j+1] = temp; 31 32 /* 有交换,则打标记 */ 33 //flag = 1; 34 } 35 } 36 37 } 38 }
(1) 冒泡排序是稳定的排序;
(2) 分析使用flag的情况,如果序列有序时最优,只需要进行一次气泡过程即可,元素位置不变;如果序列逆序时最坏,需要进行n(n-1)/2比较;所以冒泡排序的评价时间复杂度为O(n^2);
希尔排序
对于插入排序和冒泡排序等,在序列基本有序的情况下,会得到更好的排序时间;希尔排序的思想是在进行上述排序之前,对元素进行移动使序列达到基本有序,从而减少比较和移动次数;希尔排序首先对所有元素按照一个gap为一组,组中元素小的往前移动,这样在达到最后一次排序之前,小的元素基本上都已经移动到前侧了,而最后一次gap=1的排序,可以认为是对基本有序的序列进行插入或冒泡排序,所需比较和移动次数大大减少;
1 void shell_sort(int a[], int n) 2 { 3 int i = 0; 4 int p = 0; 5 int gap = 0; 6 7 /* gap的规则,按照折半缩小,直到gap=1时,进行直接插入排序 */ 8 for (gap = n/2; gap > 0; gap /= 2) 9 { 10 /* 从gap开始,对属于每个gap范围的元素中小元素进行前移 */ 11 for (i = gap; i < n; ++i) 12 { 13 temp = a[i]; 14 p = i - gap; 15 16 /* 前移位置查找 */ 17 while (p >= 0 && a[p] > temp) 18 { 19 a[p+gap] = a[p]; 20 p -= gap; 21 } 22 23 /* 插入合适位置 */ 24 a[p+gap] = temp; 25 } 26 } 27 }
(1) 希尔排序是不稳定的排序;
(2) 希尔排序的平均时间复杂度为O(nlogn);
(3) 希尔排序的gap取法,以及内部的移动方式也不是固定的;
快速排序
快速排序的思想是找一个基准元素,然后将序列中比基准元素小的放到左侧,大的放到右侧,然后在分别对基准元素左右的序列再次重复上述步骤;
1 int partion(int a[], int low, int high) 2 { 3 /* 最左侧元素为基准值 */ 4 int t = a[low]; 5 6 /* 左右不相等则进行比对,相等则对调完毕 */ 7 while (low < high) 8 { 9 /* 从右侧向左侧查找小于基准值的元素 */ 10 while (low < high && a[high] >= t) 11 { 12 high--; 13 } 14 15 /* 较小值移动到左侧*/ 16 a[low] = a[high]; 17 18 /* 从左侧向右侧查找大于基准值的元素 */ 19 while (low < high && a[low] <= t) 20 { 21 low++; 22 } 23 24 /* 较大值移动到右侧 */ 25 a[high] = a[low]; 26 } 27 28 /* 基准元素放到中间位置 */ 29 a[low] = t; 30 31 return low; 32 } 33 34 35 void quick_sort(int a[], int low, int high) 36 { 37 int pivot = 0; 38 39 if (low < high) 40 { 41 /* 分区 */ 42 pivot = partion(a, low, high); 43 44 /* 分别对左右区域做排序 */ 45 quick_sort(a, low, pivot - 1); 46 quick_sort(a, pivot + 1, high); 47 } 48 }
(1) 快速排序是一种不稳定的排序;
(2) 在序列有序的情况下,快排退化为冒泡排序,此时的时间复杂度最高,约为O(n^2);在划分中如果均为中位数时,时间复杂度为O(nlogn);但对于平均情况来说,快排仍然是最好的内排序方法;
(3) 分界的基准元素取值方法有多种,通常是首元素,尾元素和中间元素;
归并排序
归并排序的思想是将待排序序列进行区域划分与合并,先将整个序列分成两个序列,然后其中的每个在分成两个,依次细分,然后对小区域进行归并,归并之后再对上层区域进行归并,最终得到有序序列;
1 void merge(int a[], int low, int mid, int high, int temp[]) 2 { 3 int i = low; 4 int j = mid + 1; 5 int k = 0; 6 7 /* 两个区域都从头开始比较大小,合并到temp */ 8 while (i <= mid && j <= high) 9 { 10 if (a[i] <= a[j]) 11 { 12 temp[k++] = a[i++]; 13 } 14 else 15 { 16 temp[k++] = a[j++]; 17 } 18 } 19 20 /* 若其中一个区域有剩余元素,则直接并入 */ 21 while (i <= mid) 22 { 23 temp[k++] = a[i++]; 24 } 25 26 while (j <= high) 27 { 28 temp[k++] = a[j++]; 29 } 30 31 32 /* 将temp填入待排序列中 */ 33 for (i = 0; i < k; ++i) 34 { 35 a[low+i] = temp[i]; 36 } 37 } 38 39 40 void merge_sort(int a[], int low, int high, int temp[]) 41 { 42 int mid = 0; 43 44 if (low < high) 45 { 46 /* 取中位数 */ 47 mid = (low + high)/2; 48 49 /* 分别对左右进行归并排序 */ 50 merge_sort(a, low, mid, temp); 51 merge_sort(a, mid + 1, high, temp); 52 53 /* 排序的合并过程 */ 54 merge(a, low, mid, high, temp); 55 } 56 }
(1) 归并排序是一种稳定的排序算法;
(2) 归并排序需要一个与待排序序列同样大小的空间;
(3) 归并排序的时间复杂度为O(nlogn);
堆排序
堆排序的思想是把一个待排序序列看成一个近似完全二叉树,第一步是从最后一个非叶子节点开始一直到第一个节点,对序列进行调整,调整之后进行排序;排序首先将0位置的最大节点与最后一个元素相交换,然后对前面的0元素按照堆的规则进行调整;如上,直至全部元素排序结束;
1 void heap_adjust(int a[], int i, int n) 2 { 3 int child = 0; 4 int temp = 0; 5 6 /* 对i节点进行调整 */ 7 while (i * 2 + 1 < n) 8 { 9 /* 找到做孩子节点 */ 10 child = 2 * i + 1; 11 12 /* 如果存在右孩子,并且右孩子比较大,那么记录改成右孩子 */ 13 if (child < n - 1 && a[child + 1] > a[child]) 14 { 15 child++; 16 } 17 18 /* 如果父节点大于等于最大的孩子节点,不需要调整 */ 19 if (a[i] >= a[child]) 20 { 21 break; 22 } 23 /* 父节点小于孩子节点,则需要与孩子节点对调 */ 24 else 25 { 26 temp = a[i]; 27 a[i] = a[child]; 28 a[child] = temp; 29 } 30 31 /* 继续调整孩子节点 */ 32 i = child; 33 } 34 } 35 36 void heap_sort(int a[], int n) 37 { 38 int i = 0; 39 int temp = 0; 40 41 42 /* 起始a[]认为是个数组形式表示的近似完全二叉树 */ 43 44 /* 从最后一个非叶子节点开始,向前逐步调整堆 */ 45 for (i = n / 2 - 1; i >= 0; --i) 46 { 47 heap_adjust(a, i, n); 48 } 49 50 /* 排序过程为第一个元素与最后一个未排序元素交换 */ 51 /* 然后调整前面未排序的部分,保证最大的首节点放到最后 */ 52 /* 调整之后,新的最大节点在根0位置 */ 53 for (i = n - 1; i > 0; --i) 54 { 55 /* 交换首元素和最后未排序元素 */ 56 temp = a[0]; 57 a[0] = a[i]; 58 a[i] = temp; 59 60 /* 调整堆 */ 61 heap_adjust(a, 0, i); 62 } 63 }
(1) 堆排序是一种不稳定的排序;
(2) 堆排序的时间复杂度是O(nlogn);无论是最坏或者平均情况皆如此;