本文整理了常用的七种排序算法的原理、代码及复杂度,欢迎批评指正~
概览
本文将介绍选择排序、插入排序、希尔排序、冒泡排序、快速排序、归并排序、基数排序这七种常用排序算法,几种算法的比较如下图所示:
在正式开始前,为了便捷地对每种算法进行测试,我们首先准备一个.h文件,并命名为“0.sort_test.h”,内容如下:
#ifndef _SORT_TEST_H
#define _SORT_TEST_H
#include <string.h>
#include <stdlib.h>
#include <time.h>
#define SMALL_DATA_N 5000
#define BIG_DATA_N 1000000
__attribute__((constructor))//这段语句让该函数可以在主函数main之前执行,而不需要调用,用来初始化时间种子
void __init_Rand__() {
printf("init rand\n");
srand(time(0));
}
//该函数用来检查数组是否被正确排序
bool check(int *arr, int l, int r) {
for (int i = l + 1; i < r; i++) {
if (arr[i] < arr[i - 1]) return false;
}
return true;
}
//该函数用来生成随机数组
int *getRandData(int n) {
int *arr = (int *)malloc(sizeof(int) * n);
for (int i = 0; i < n; i++) arr[i] = rand() % 10000;
return arr;
}
//该宏用来交换两个数字
#define swap(a, b) { \
__typeof(a) __c = a; \
a = b, b = __c; \
}
//该宏用来测试排序函数
#define TEST(func, arr, n) { \
printf("Test %s : ", #func); \
int *temp = (int *)malloc(sizeof(int) * n); \
memcpy(temp, arr, sizeof(int) * n); \
long long b = clock(); \
func(temp, 0, n); \
long long e = clock(); \
if (check(temp, 0, n)) { \
printf("\tOK "); \
} else { \
printf("Failed "); \
} \
printf(" %ditems %lldms\n", n, (e - b) * 1000 / CLOCKS_PER_SEC); \
free(temp);\
}
#endif
1. 选择排序
排序原理
遍历数组,每次找到待排序区最大的元素,放到已排序区。
代码实现
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "0.sort_test.h"
void selection_sort(int *arr, int l, int r) {
for (int i = l, I = r - 1; i < I; i++) {
int ind = i;
for (int j = i + 1; j < r; j++) {
if (arr[j] < arr[ind]) ind = j;
}
swap(arr[i], arr[ind]);
}
return ;
}
int main() {
int *arr = getRandData(SMALL_DATA_N);
TEST(selection_sort, arr, SMALL_DATA_N);
free(arr);
return 0;
}
算法分析
- 时间复杂度:数组有n个元素,扫描n次,共需要比较n*(n-1)/2次,因此时间复杂度为O(n^2);
- 空间复杂度:排序中没有开辟额外的存储空间,因此空间复杂度为O(1);
- 稳定性:排序中每次遍历将最大值调整到待排序区,因此会打乱相同元素的顺序,算法不稳定。
2. 插入排序
排序原理
依次调整每个元素,如果前面的元素大于当前元素,则交换顺序。
代码实现
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "0.sort_test.h"
void insert_sort(int *arr, int l, int r) {
for (int i = l + 1; i < r; i++) {
int j = i;
while (j > l && arr[j] < arr[j - 1]) {
swap(arr[j], arr[j - 1]);
j -= 1;
}
}
return ;
}
void unguarded_insert_sort(int *arr, int l, int r) {
int ind = l;
for (int i = l + 1; i < r; i++) {
if (arr[i] < arr[ind]) ind = i;
}
while (ind > l) {
swap(arr[ind], arr[ind - 1]);//将最小的元素移动到数组起始处,避免了j>l条件的判断
ind -= 1;
}
for (int i = l + 1; i < r; i++) {
int j = i;
while (arr[j] < arr[j - 1]) {
swap(arr[j], arr[j - 1]);
j -= 1;
}
}
return ;
}
int main() {
int *arr = getRandData(SMALL_DATA_N);
TEST(insert_sort, arr, SMALL_DATA_N);
TEST(unguarded_insert_sort, arr, SMALL_DATA_N);
free(arr);
return 0;
}
此处展示了两种插入排序算法,第二种为无监督的插入排序。在第一种排序中,需要考虑边界条件"j > l", 而无监督的插入排序则首先将最小的元素移动到数组前端,这样就避免了每次对于"j > l"条件的判断,提高了算法的效率。
算法分析
- 时间复杂度:最坏的情况是原数组是逆序,则需要调整n * (n - 1) / 2次,复杂度为O(n^2)。最好的情况是原先数组就是顺序,只需要遍历一遍数组,此时复杂度为O(n)。因此,平均的时间复杂度是O(n^2);
- 空间复杂度:排序中没有开辟额外的存储空间,因此空间复杂度为O(1);
- 稳定性:排序中不会交换相同元素的顺序,因此算法是稳定的。
3. 希尔排序
排序原理
希尔排序在插入排序的基础上发展而来,首先选择一定的步长,通过步长对数组进行分组,对每组进行插入排序。之后减小步长,重复上述过程。
对于希尔排序的个人理解:对于普通的插入排序,如果原数组越有序,则插入排序的效率越高。希尔排序就是借鉴了这一思想,通过分组排序,使数组整体更加有序,因此提高了效率。
代码实现
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "0.sort_test.h"
void unguarded_insert_sort(int *arr, int l, int r, int step) {
int ind = l;
for (int i = l + step; i < r; i += step) {
if (arr[i] < arr[ind]) ind = i;
}
while (ind > l) {//借鉴了无监督的插入排序的思想
swap(arr[ind], arr[ind - step]);
ind -= step;
}
for (int i = l + 2 * step; i < r; i += step) {
int j = i;
while (arr[j] < arr[j - step]) {
swap(arr[j], arr[j - step]);
j -= step;
}
}
return ;
}
void shell_sort(int *arr, int l, int r) {
int k = 2, n = (r - l), step;
do {
step = n / k == 0 ? 1 : n / k;
for (int i = l, I = l + step; i < I; i++) {
unguarded_insert_sort(arr, i, r, step);
}
k *= 2;
} while (step != 1);
return ;
}
int main() {
int *arr = getRandData(BIG_DATA_N);
TEST(shell_sort, arr, BIG_DATA_N);
TEST(shell_sort_hibbard, arr, BIG_DATA_N);
free(arr);
return 0;
}
算法分析
- 时间复杂度:最坏情况的复杂度为O(n^2),最好情况的复杂度为O(nlogn);
- 空间复杂度:排序中没有开辟额外的存储空间,因此空间复杂度为O(1);
- 稳定性:因为涉及分组排序,所以有可能交换相同元素的顺序,算法是不稳定的。
4. 冒泡排序
排序原理
遍历数组中的每个元素,如果后面的元素小于前面的元素,则交换两个元素。每次遍历都将最大的元素调整到已排序区。
代码实现
#include <stdio.h>
#include <stdlib.h>
#include "0.sort_test.h"
void bubble_sort(int *arr, int l, int r) {
for (int i = r - 1, I = l + 1, cnt; i >= I; i--) {
cnt = 0;
for (int j = l; j < i; j++) {
if (arr[j] <= arr[j + 1]) continue;
swap(arr[j], arr[j + 1]);
cnt += 1;
}
if (cnt == 0) break;//扫描一遍后如果是顺序,则直接跳出
}
return ;
}
int main() {
int *arr = getRandData(SMALL_DATA_N);
TEST(bubble_sort, arr, SMALL_DATA_N);
free(arr);
return 0;
}
算法分析
- 时间复杂度:最坏的情况是原数组是逆序,则需要调整n * (n - 1) / 2次,复杂度为O(n^2)。最好的情况是原先数组就是顺序,只需要遍历一遍数组,此时复杂度为O(n)。因此,平均的时间复杂度是O(n^2);
- 空间复杂度:排序中没有开辟额外的存储空间,因此空间复杂度为O(1);
- 稳定性:排序中不会交换相同元素的顺序,因此算法是稳定的。
5. 快速排序
排序原理
快速排序首先选择一个基准值,后将剩余元素分成两个区域,一个区域的元素都小于基准值,一个区域的元素都大于基准值,将基准值插入到两个区域的中间,后分别对两个区域进行快速排序。
代码实现
#include <stdio.h>
#include <stdlib.h>
#include "0.sort_test.h"
void quick_sort(int *arr, int l, int r) {
if (r - l <= 2) {//当元素较少时,直接排序
if (r - l <= 1) return ;
if (arr[l] > arr[l + 1]) swap(arr[l], arr[l + 1]);
return ;
}
int x = l, y = r - 1, z = arr[l];
while (x < y) {
while (x < y && z <= arr[y]) --y;
if (x < y) arr[x++] = arr[y];
while (x < y && arr[x] <= z) ++x;
if (x < y) arr[y--] = arr[x];
}
arr[x] = z;
quick_sort(arr, l, x);
quick_sort(arr, x + 1, r);
return ;
}
int main() {
int *arr_s = getRandData(SMALL_DATA_N);
int *arr_b = getRandData(BIG_DATA_N);
TEST(quick_sort, arr_s, SMALL_DATA_N);
TEST(quick_sort, arr_b, BIG_DATA_N);
free(arr_s);
free(arr_b);
return 0;
}
算法分析
- 时间复杂度:最好的情况是每次进行分区操作时可以均分数组,此时的时间复杂度为O(nlogn);最坏的情况分区极度不平衡,此时的时间复杂度为O(n^2);
- 空间复杂度:跟实现的方法有关;
- 稳定性:排序过程中会将基准值插入到两个区间中间,因此有可能破坏相同元素的顺序,算法是不稳定的。
6. 归并排序
排序原理
将数组分成个子数字,并继续将子数组分成两份,直到没法再分。之后对数组进行合并,合并每两个数组时,保证合并的顺序是从小到大。
代码实现
#include <stdio.h>
#include <stdlib.h>
#include "0.sort_test.h"
int *buff;
void merge_sort(int *arr, int l, int r) {
if (r - l <= 1) return ;//递归终止条件
int mid = (l + r) / 2;
merge_sort(arr, l, mid);//左段递归
merge_sort(arr, mid, r);//右段递归
// merge
int p1 = l, p2 = mid, k = 0;
while (p1 < mid || p2 < r) {//利用左右两个指针进行合并
if (p2 == r || (p1 < mid && arr[p1] <= arr[p2])) {
buff[k++] = arr[p1++];
} else {
buff[k++] = arr[p2++];
}
}
for (int i = l; i < r; i++) arr[i] = buff[i - l];
return ;
}
int main() {
int *arr_s = getRandData(SMALL_DATA_N);
int *arr_b = getRandData(BIG_DATA_N);
buff = (int *)malloc(sizeof(int) * BIG_DATA_N);
TEST(merge_sort, arr_s, SMALL_DATA_N);
TEST(merge_sort, arr_b, BIG_DATA_N);
free(arr_s);
free(arr_b);
free(buff);
return 0;
}
算法分析
- 时间复杂度:每次将数组从中间分成两份,因此时间复杂度为O(nlogn);
- 空间复杂度:O(n);
- 稳定性:在排序中可以保证大小相同元素的前后顺序不变,因此算法稳定。
7. 基数排序
排序原理
基数排序给人一种用空间换时间的感觉,如下图所示,对于10以上的数组,我们可以首先只关注其个位,通过基数排序保证其个位数字从大到小排列。之后再对十位的数字进行基数排序,另外因为基数排序是稳定的,因此在对十位进行排序时,不会打断个位的排序结果。以此类推,直到把所有位数都排序完。
再进一步,在基数排序时也不一定非要按照数字的位数进行排序,可以选择一个数字,比如下面代码中选择的是65536(因为int类型最大是2的32次方,而65536是2的16次方,一个int类型的数字除以65536的结果一定小于65536)。之后开辟一个65536大小的存储空间,首先对每个元素除以65536的余数排序,后对每个元素除以65536的结果排序,只需要两次遍历,就可以将数组排序。
代码实现
#include <stdio.h>
#include <stdlib.h>
#include "0.sort_test.h"
void radix_sort(int *arr, int l, int r) {
#define K 65536
int *cnt = (int *)malloc(sizeof(int) * K);
int *temp = (int *)malloc(sizeof(int) * (r - l));
// round 1
memset(cnt, 0, sizeof(int) * K);
for (int i = l; i < r; i++) cnt[arr[i] % K] += 1;
for (int i = 1; i < K; i++) cnt[i] += cnt[i - 1];
for (int i = r - 1; i >= l; i--) temp[--cnt[arr[i] % K]] = arr[i];
memcpy(arr + l, temp, sizeof(int) * (r - l));
// round 2
memset(cnt, 0, sizeof(int) * K);
for (int i = l; i < r; i++) cnt[arr[i] / K] += 1;
for (int i = 1; i < K; i++) cnt[i] += cnt[i - 1];
for (int i = r - 1; i >= l; i--) temp[--cnt[arr[i] / K]] = arr[i];
memcpy(arr + l, temp, sizeof(int) * (r - l));
return ;
}
int main() {
int *arr_s = getRandData(SMALL_DATA_N);
int *arr_b = getRandData(BIG_DATA_N);
TEST(radix_sort, arr_s, SMALL_DATA_N);
TEST(radix_sort, arr_b, BIG_DATA_N);
free(arr_s);
free(arr_b);
return 0;
}
算法分析
- 时间复杂度:根据基数排序原理可知复杂度为O(n*K);
- 空间复杂度:O(n + K);
- 稳定性:在排序中可以保证大小相同元素的前后顺序不变,因此算法稳定。