本文对几种常见的排序算法做一个简单总结
基本概念
-
排序稳定性:若排序的序列中,有多个相同的值,且假设A在B前面;若排序后可能使得B在A的前面,那么则称这个排序是不稳定的。
-
大O法则:用于分析算法的复杂度,有以下约定:用常数1取代算法复杂度表达式的加法常数;算法复杂度表达式中,只保留最高阶;算法复杂度表达式最高阶项的相乘系数修改为1。
冒泡排序
-
流程:
- 外循环控制排序轮数,排序的轮数为序列长度n。
- 内循环从序列第一/最后序列项开始,与前一个序列项比较;若小于/大于前一序列项,则交换位置;第i轮排序内循环比较次数为 (n – 1 – 第k轮排序)。
- 若某一次排序轮数为发生交换,则表示此时已完成排序。
-
时间复杂度:
- 最好情况:进行n-1次比较,0次交换,复杂度为O(n);
- 最坏情况:进行( (n-1) … + 2 + 1 ) = n(n-1)/2次比较,并进行n(n-1)/2次的交换,复杂度为O(n2)
-
空间复杂度:O(1)
-
代码实现:
int bubbling_Sort(int *arr, int len) { if (arr == NULL || len <= 1) return -1; int i, j, flag = 1; for (i = 0 ; i < (len-1) && flag --; i ++) { for (j = 0; j < (len-1-i); j ++) { if (arr[j] > arr[j+1]) { swap(&arr[j], &arr[j+1]); flag = 1; } } } return 0; }
简单选择排序
-
流程:
- 选择第i个序列项,与后续的 (n-i) 个序列项进行 (n-i) 次比较,找出这 (n-i+1) 个子序列中最小的序列项,并与第 i 个序列项交换。
- 总计需要循环的轮数为 (n-1) 轮。
-
时间复杂度:
- 最好情况:比较( (n-1) … + 2 + 1 ) = n(n-1)/2次,交换0次,复杂度为O(n2)。
- 最坏情况:比较n(n-1)/2次,交换 (n-1) 次,复杂度为O(n2)。
-
空间复杂度:O(1)
-
代码实现:
int select_sort(int *arr, int len) { if (arr == NULL || len <= 1) return -1; int i, j, min_index; for (i = 0; i < (len-1); i ++) { min_index = i; for (j = (i+1); j < len; j ++) { if (arr[j] < arr[min_index]) { min_index = j; } } swap(&arr[i], &arr[min_index]); } return 0; }
直接插入排序
-
流程:
-
从第2个序列项开始,第n个序列项结束。
-
判断当前第i (i≥2) 个序列项是否小于前一个序列项,是则启动插入程序。
-
前面的 (i-1) 个序列项,只要比第i个序列项小,就往后移一位。
-
直到遇到不比第i个序列大的序列,则停止,该位置即为第i个序列项的插入位置。
-
-
时间复杂度:
- 最好情况:比较 (n-1) 次,交换0次,复杂度为O(n)。
- 最坏情况:比较(2 + 3 + … + n) = (n-1)(n+2)/2次,交换 ( 1 + 2 + … + (n-1) ) = n(n-1)/2次。
-
空间复杂度:O(1)
-
代码实现:
int direct_insert_sort(int *arr, int len) { if (arr == NULL || len <= 1) return -1; int i, j, temp; for (i = 1; i < len; i ++) { if (arr[i] < arr[i-1]) { /*begin insert*/ temp = arr[i]; /*find insert index and move the node which bigger then index node*/ for (j = i-1; arr[j] > temp && j >= 0; j --) { arr[j+1] = arr[j]; } arr[j+1] = temp; } } return 0; }
希尔排序 — 增强版插入排序
-
流程:将大序列按照增量序列跳跃式地分割为多个小序列,每轮对这些小序列进行(直接)插入排序;这些小序列排序完成后,大序列就会逐渐趋向于基本有序(大的基本在前面,小的基本在后面);在最后一轮时,进行一次(直接)插入排序就可以了。
- 初始化incre为序列长度,选取增量序列incre为incre[k] = incre[k-1] / 3 + 1
- 令incre = incre / 2 + 1
- 从第incre+1个序列项开始,到第n个序列项结束。
- 判断当前第i(i≥incre+1) 个序列项是否小于第 (i-incre) 序列项,是则启动插入程序。
- 前面的 (i-1)/incre 个序列项,只要比第i个序列项小,就往后移一位。
- 直到遇到不比第i个序列大的序列,则停止,该位置即为第i个序列项的插入位置。
- 若incre大于1,从第2步开始重复(incre为1时,相当于做了一次直接插入排序)。
-
时间复杂度:当增量序列选取得好,复杂度为O(nlog n) — O(n2)。
-
空间复杂度:O(1)
-
不稳定性:由于希尔排序过程中存在跳跃式的子序列进行独立插入排序的情况,所以希尔排序是不稳定的排序算法。
-
代码实现:
int shell_sort(int *arr, int len) { if (arr == NULL || len <= 1) return -1; int i, j, temp, incre = len; do { incre = incre / 3 + 1; for (i = incre; i < len; i ++) { if (arr[i] < arr[i-incre]) { temp = arr[i]; for (j = i-incre; j >= incre-1 && arr[j] > temp; j -= incre) { arr[j+incre] = arr[j]; } arr[j+incre] = temp; } } } while (incre > 1); /*incre == 1, which mean direct insert sort*/ return 0; }
堆排序 — 增强版选择排序
-
堆是完全二叉树,若每个节点值都大于或等于其左右孩子的值,则为大顶堆;反之若每个节点值都小于或等于其左右孩子的值,则为小顶堆。
-
流程:
- 调整序列结构,使其为大/小顶堆。
- 交换大/小顶堆的头尾元素,同时将尾元素剔除出堆结构(此时序列中最大与最小元素完成交换)。
- 重新调整为大/小顶堆结构。
- 重复2,3步操作n-1次,此时完成排序。
-
时间复杂度:O(nlog n)
-
空间复杂度:O(1)
-
不稳定性:由于存在节点间的比较和交换是跳跃进行的现象,所以堆排序是不稳定的排序算法。
-
代码实现:
static void heap_adjust(int *arr, int s, int m) { int j, temp = arr[s]; for (j = 2*s+1; j <= m; j = 2*j+1) { if (j < m && arr[j] < arr[j+1]) /* right node bigger than left node */ j ++; /* use right node */ if (temp >= arr[j]) /* father node bigger than child node , exit*/ break; arr[s] = arr[j]; /* swap father node and child node */ s = j; /* index old father node */ } arr[s] = temp; /* set old father node value */ } int heap_sort(int *arr, int len) { if (arr == NULL || len <= 1) return -1; int i; for ( i = (len/2-1); i >= 0; i-- ) /* make a big head */ heap_adjust(arr, i, len-1); for (i = (len-1); i > 0; i --) { swap(&arr[0], &arr[i]); /* swap biggest and minimal */ heap_adjust(arr, 0, i-1); /* make a big head */ } return 0; }
归并排序
-
流程
-
将序列中间开始拆分子序列,当n/2小于等于1时停止拆分(拆分log2n次)。
-
对每个拆分的子序列排序(有些子序列存在两个元素)。
-
使用两个游标分别指向相邻子序列的头部,比较结点大小,较小的结点输出到缓冲序列中,同时游标移动;当一方游标到底后,另一方剩余数据全部输出,此时归并后的子序列是有序的。
-
重复第三步直至序列合并完成,此时完成排序。
-
-
时间复杂度:O(nlog n)
-
空间复杂度:O(n)
-
代码实现:
static void merge(int *sr, int *tr, int i, int m, int n) { int j, k, r; /*use two index to sort two orderly list*/ for (j = m+1, k = i; i <= m && j <= n; k ++) { if (sr[i] < sr[j]) tr[k] = sr[i++]; else tr[k] = sr[j++]; } /*left have node, push them*/ if (i <= m) { for (r = 0; r <= m-i; r++) { tr[k+r] = sr[i+r]; } } /*right have node, push them*/ if (j <= n) { for (r = 0; r <= n-j; r++) { tr[k+r] = sr[j+r]; } } } static void m_sort(int *sr, int *tr1, int s, int t) { int m; int tr2[26]; if (s == t) { tr1[s] = sr[s]; } else { m = (s + t) / 2; /* get mid index */ m_sort(sr, tr2, s, m); /* sort sr[s ...... m] to tr2 */ m_sort(sr, tr2, m+1, t); /* sort sr[m+1 ...... t] to tr2 */ merge(tr2, tr1, s, m, t); /* merge and sort tr2(which have two orderly list) to tr1, */ } } int merge_sort(int *arr, int len) { if (arr == NULL || len <= 1) return -1; m_sort(arr, arr, 0, len-1); return 0; }
快速排序 — 升级版冒泡排序
-
流程:通过一轮排序将序列项分为独立的两个子序列——较大序列项的子序列和较小序列项的子序列;再分别对两个子序列排序,最终达到有序目的。
- 使用两个游标指向序列的头部和尾部,选取序列中某个节点作为基准k
- 将比基准k小的数放在左边,比k大的数放在右边,返回最终基准k的位置
- 以k的位置为基准,分割成两个子序列
- 重复上诉步奏,直至所有子序列个数为1,此时排序完成
-
时间复杂度:情况介于O(nlog n) — O(n2),但平均情况是O(nlogn)
-
空间复杂度:情况介于O(nlog n) — O(n2),但平均情况是O(nlogn)
-
不稳定性:由于存在跳跃式的比较和交换,所以快速排序算法是不稳定
-
代码实现
static int partition(int *arr, int low, int high) { int pivot_val = arr[low]; /* init pivot value */ while (low < high) { /* loop until find the pivot */ /* find the node which lower than pivot value */ while (low < high && arr[high] >= pivot_val) high --; swap(&arr[low], &arr[high]); /* swap lower node to left */ /* find the node which bigger than pivot value */ while (low < high && arr[low] <= pivot_val) low ++; swap(&arr[low], &arr[high]); /* swap bigger node to right */ } return low; } static void q_sort(int *arr, int low, int high) { int pivot; if (low < high) { pivot = partition(arr, low, high); /* find the pivot, divide list */ q_sort(arr, low, pivot-1); /* sort low to pivot-1 */ q_sort(arr, pivot+1, high); /* sort pivot+1 to high */ } } int quick_sort(int *arr, int len) { if (arr == NULL || len <= 1) return -1; q_sort(arr, 0, len-1); return 0; }
桶排序
-
流程:按照数据的特点,规定几个范围称之为桶,将数据按照范围装进桶中;然后在桶里面在进行分别排序(可使用别的排序算法或递归使用桶排序),最后再将桶中数据分别输出,完成排序。
-
代码实现:假设数据为正整数类型,且数据范围小。
int bucket_sort(int *arr, int len) { if (arr == NULL || len <= 1) return -1; int *bucket = (int*)malloc(MAX_VAL*sizeof(int)); if (bucket == NULL) return -1; memset(bucket, 0, MAX_VAL*sizeof(int)); int i, j; for (i = 0; i < len; i ++) bucket[arr[i]] ++; for (i = 0, j = 0; i < MAX_VAL; i ++) { if (bucket[i] > 0) { while (bucket[i] --) arr[j++] = i; } } free(bucket); return 0; }
结语
-
分类:冒泡排序和快速排序属于交换排序类;简单选择排序和堆排序属于选择排序类;直接插入排序和希尔排序属于插入排序类;归并排序属于归并排序类。
-
各排序算法比较
-
由上表和前面分析可推测算法的选取准则:
- 若是序列长度短且序列元素简单,选择直接插入排序;序列长度短但序列元素复杂,需要减少交换次数选择简单选择排序;折中选取冒泡排序。
- 序列复杂但给足空间选择归并排序;序列复杂但空间不足选取堆排序或希尔排序;多次优化后快速排序整体性能最好。
参考资料
- 《大话数据结构》
- 《数据结构与算法分析 — C语言描述》