算法比较
稳定性
插入排序,冒泡排序,二路归并排序和基数排序是稳定的排序方法;
选择排序,希尔排序,快速排序和堆排序是不稳定的排序方法;
复杂度
| 排序方法 | 平均时间 | 最坏情况 | 辅助空间 |
| 插入排序 | 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);无论是最坏或者平均情况皆如此;