排序
排序算法分为交换类排序、插入类排序、选择类排序、归并类排序。
交换类排序
冒泡排序
冒泡排序的基本思想是:从后往前(或从前往后)两两比较相邻元素的值。若A[ j - 1 ] > A[ j ],则交换它们,直到序列比较完。我们称它为第一趟冒泡,结果是将最小的元素交换到待排序列的第一个位置。关键字最小的元素如气泡一般逐渐往上“漂浮”直至“水面”。下一趟冒泡时,前一趟确定的最小元素不再参与比较,每趟冒泡的结果是把序列中的最小元素放到了序列的最终位置…这样最多做 n − 1 n - 1 n−1趟冒泡就能把所有元素排好序。
可以通过点击此处进入旧金山大学提供的网站来演示冒泡排序动画效果。
代码实战:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
typedef int ElemType;
typedef struct {
// 存储元素的起始地址
ElemType *elem;
// 元素个数
int table_len;
} SSTable;
/*
* 顺序表初始化
*/
void st_init(SSTable &ST, int len) {
ST.table_len = len;
ST.elem = (ElemType *) malloc(sizeof(ElemType) * ST.table_len);
// 用于生成随机数的骰子
srand(time(NULL));
for (int i = 0; i < ST.table_len; i++) {
// 生产的数是0-99之间
ST.elem[i] = rand() % 100;
}
}
/*
* 顺序表打印
*/
void st_print(SSTable ST) {
for (int i = 0; i < ST.table_len; i++) {
printf("%3d", ST.elem[i]);
}
printf("\n");
}
/*
* 交换两个元素
*/
void swap(ElemType &a, ElemType &b) {
ElemType temp;
temp = a;
a = b;
b = temp;
}
/*
* 冒泡排序
*
* 排序往往都是用两层循环:1.内层循环控制比较/交换 2.外层循环控制
*/
void bubble_sort(ElemType *A, int len) {
// 哨兵: 是否发生交换
bool flag;
// 外层循环控制
for (int i = 0; i < len - 1; i++) {
flag = false;
// 内层控制比较/交换
// j = len - 1 是从最后一个元素从后往前比较/交换
for (int j = len - 1; j > i; j--) {
if (A[j - 1] > A[j]) {
swap(A[j - 1], A[j]);
flag = true;
}
}
// 本趟排序未发生交换说明整个顺序表已经有序
if (false == flag) {
return;
}
}
}
/*
* 冒泡排序
*/
int main() {
SSTable ST;
// 一、顺序表初始化
st_init(ST, 10);
// 固定数组用于调试
ElemType A[10] = {64, 94, 95, 79, 69, 84, 18, 22, 12, 78};
// memcpy可用于拷贝整型数组、浮点型数组 strcpy只能拷贝字符型数组
memcpy(ST.elem, A, sizeof(A));
// 打印顺序表
st_print(ST);
// 二、冒泡排序
bubble_sort(ST.elem, ST.table_len);
st_print(ST);
return 0;
}
时间复杂度:时间复杂度其实就是程序实际的运行次数,可以看到内层是 j > i j > i j>i,外层 i i i的值是从 0 0 0到 n − 1 n - 1 n−1,所以程序的总运行次数是 1 + 2 + 3 + . . . + ( n − 1 ) 1 + 2 + 3 + ... + (n - 1) 1+2+3+...+(n−1),这是等差数列求和,得到是结果是 n ( n − 1 ) / 2 n(n - 1)/2 n(n−1)/2,忽略低阶项和高阶项的首项系数,因此时间复杂度是 O ( n 2 ) O(n^2) O(n2)。
空间复杂度:因为未使用额外的空间(额外空间必须与输入元素的个数N相关),所以空间复杂度是 O ( 1 ) O(1) O(1)
快速排序
快速排序的核心是分治思想:假设我们的目标依然是按从小到大的顺序排列,我们找到数组中的一个分割值,把比分割值小的数都放在数组的左边,把比分割值大的数都放在数组的右边,这样分割值的位置就被确定。
数组一分为二,我们只需排前一半数组和后一半数组,复杂度直接减半。采用这种思想,不断地进行递归,最终分割得只剩一个元素时,整个序列自然就是有序的。
可以通过点击此处进入旧金山大学提供的网站来演示快速排序动画效果。
代码实战:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
typedef int ElemType;
typedef struct {
// 存储元素的起始地址
ElemType *elem;
// 元素个数
int table_len;
} SSTable;
/*
* 顺序表初始化
*/
void st_init(SSTable &ST, int len) {
ST.table_len = len;
ST.elem = (ElemType *) malloc(sizeof(ElemType) * ST.table_len);
// 用于生成随机数的骰子
srand(time(NULL));
for (int i = 0; i < ST.table_len; i++) {
// 生产的数是0-99之间
ST.elem[i] = rand() % 100;
}
}
/*
* 顺序表打印
*/
void st_print(SSTable ST) {
for (int i = 0; i < ST.table_len; i++) {
printf("%3d", ST.elem[i]);
}
printf("\n");
}
/*
* 分割函数 挖坑法
*/
int partition(ElemType *A, int low, int high) {
// 拿最左边的元素作为分割值 并存储下来
ElemType pivot = A[low];
while (low < high) {
// 找到一个比分割值小的元素
while (low < high && A[high] >= pivot) {
high--;
}
A[low] = A[high];
// 找到一个比分割值大的元素
while (low < high && A[low] <= pivot) {
low++;
}
A[high] = A[low];
}
// 分割值放到中间位置
// 左边的都比分割值小 右边都比分割值大
A[low] = pivot;
return low;
}
/*
* 快速排序
*/
void quick_sort(ElemType *A, int low, int high) {
if (low < high) {
// 存储分割值的位置
int pivoit_pos = partition(A, low, high);
quick_sort(A, low, pivoit_pos - 1);
quick_sort(A, pivoit_pos + 1, high);
}
}
int main() {
SSTable ST;
// 一、顺序表初始化
st_init(ST, 10);
// 固定数组用于调试
ElemType A[10] = {64, 94, 95, 79, 69, 84, 18, 22, 12, 78};
// memcpy可用于拷贝整型数组、浮点型数组 strcpy只能拷贝字符型数组
memcpy(ST.elem, A, sizeof(A));
// 打印顺序表
st_print(ST);
// 二、快速排序
quick_sort(ST.elem, 0, ST.table_len - 1);
st_print(ST);
return 0;
}
时间复杂度:假如每次快速排序数组都被平均地一分为二,那么可以得出quick_sort递归的次数是 l o g 2 n log_2 n log2n,第一次partition遍历次数为 n n n,分成两个数组后,每个数组遍历 n / 2 n/2 n/2次,加起来还是 n n n,因此时间复杂度是 O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n),因计算机是二进制的,所以在复试面试回答复杂度或与人交流时,提到复杂度时一般直接讲 O ( n l o g n ) O(nlog n) O(nlogn),而不带下标 2 2 2。
快速排序最差的时间复杂度为什么是呢?
因为数组本身从小到大有序时,如果每次我们仍然用最左边的数作为分割值,那么每次数组都不会二分,导致递归 n n n次,所以快速排序最坏时间复杂度为 n n n的平方。当然,为了避免这种情况,有时会首先随机选择一个下标,先将对应下标的值与最左边的元素交换,再进行partition操作,从而极大地降低出现最坏时间复杂度的概率,但是仍然不能完全避免。
因此快排最好和平均时间复杂度是 O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n),最差是 O ( n 2 ) O(n^2) O(n2).
空间复杂度: O ( l o g 2 n ) O(log_2 n) O(log2n),因递归的次数是 l o g 2 n log_2 n log2n,而每次递归的形参都是需要占用空间的。
插入类排序
插入类排序有三种:直接插入排序、折半插入排序、希尔排序。
直接插入排序
如果一个序列只有一个数,那么该序列自然是有序的。插人排序首先将第一个数视为有序序列,然后把后面9个数视为要依次插入的序列。首先,我们通过外层循环控制要插人的数,用 insertVal保存要插入的值87,我们比较 arr[0]是否大于 arr[1],即3是否大于87,由于不大于,因此不发生移动,这时有序序列是3,87.接着,将数值2插入有序序列,首先将2赋给 insertVal,这时判断87 是否大于2,因为87大于2,所以将87向后移动,将2覆盖然后判断3是否大于2,因为3大于2,所以3移动到87所在的位置,内层循环结束,这时将2赋给arr[0]的位置,得到下表中第2次插人后的效果。
有序序列 | 要插入的数的序列 | |
---|---|---|
最初 | 3 | 87 2 93 78 56 61 38 12 40 |
第1次插入 | 3 87 | 2 93 78 56 61 38 12 40 |
第2次插入 | 2 3 87 | 93 78 56 61 38 12 40 |
… | … | … |
继续循环会将数依次插人有序序列,最终使得整个数组有序。插入排序主要用在部分数有序的场景,例如手机通讯录时时刻刻都是有序的,新增一个电话号码时,以插入掛序的方法将其插入原有的有序序列,这样就降低了复杂度。
可以通过点击此处进入旧金山大学提供的网站来演示插入排序动画效果。
代码实战:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
typedef int ElemType;
typedef struct {
// 存储元素的起始地址
ElemType *elem;
// 元素个数
int table_len;
} SSTable;
/*
* 顺序表初始化
*/
void st_init(SSTable &ST, int len) {
ST.table_len = len;
ST.elem = (ElemType *) malloc(sizeof(ElemType) * ST.table_len);
// 用于生成随机数的骰子
srand(time(NULL));
for (int i = 0; i < ST.table_len; i++) {
// 生产的数是0-99之间
ST.elem[i] = rand() % 100;
}
}
/*
* 顺序表打印
*/
void st_print(SSTable ST) {
for (int i = 0; i < ST.table_len; i++) {
printf("%3d", ST.elem[i]);
}
printf("\n");
}
/*
* 插入排序
*/
void insert_sort(ElemType *A, int len) {
int i, j, insert_val;
// 外层控制要插入的数 从第A[1]个开始
for (i = 1; i < len; i++) {
// 待插入的值
insert_val = A[i];
// 内层控制 j要大于等于0 同时A[j]大于insert_val时 A[j]位置元素往后覆盖
for (j = i - 1; j >= 0 && A[j] > insert_val; j--) {
A[j + 1] = A[j];
}
A[j + 1] = insert_val;
}
}
int main() {
SSTable ST;
// 一、顺序表初始化
st_init(ST, 10);
// 固定数组用于调试
ElemType A[10] = {64, 94, 95, 79, 69, 84, 18, 22, 12, 78};
// memcpy可用于拷贝整型数组、浮点型数组 strcpy只能拷贝字符型数组
memcpy(ST.elem, A, sizeof(A));
// 打印顺序表
st_print(ST);
// 二、插入排序
insert_sort(ST.elem, ST.table_len);
st_print(ST);
return 0;
}
时间复杂度:随着有序序列的不断增加,插入排序比较的次数也会增加,插入排序的执行次数也是从 1 1 1加到 n − 1 n-1 n−1,总运行次数为 n ( n − 1 ) / 2 n(n - 1)/2 n(n−1)/2,时间复杂度依然为 O ( n 2 ) O(n^2) O(n2).
如果数组本身有序,那么就是最好的时间复杂度 O ( n ) O(n) O(n)。当数组有序,我们的内层循环每次都是无法进入的,因此,最好的时间复杂度就是 O ( n ) O(n) O(n)。
空间复杂度:因为未使用额外的空间(额外空间必须与输入元素的个数 N相关),所以空间复杂为 O ( 1 ) O(1) O(1)。
选择类排序
选择类排序有两种:简单选择排序、堆排序
简单选择排序
简单选择排序原理:假设排序表为L[1⋯n],第i趟排序即从L[i⋯n]中选择关键字最小的元素与L(i)交换,每趟排序可以确定一个元素的最终位置,这样经过n-1 趟排序就可使得整个排序表有序。
首先假定第零个元素是最小的,把下标0赋值给 min(min 记录最小的元素的下标),内层比较时,从1号元素一直比较到9号元素,谁更小,就把它的下标赋给 min,一轮比较结束后,将min 对应位置的元素与元素 i 交换,如下表所示。第一轮确认 2最小,将2与数组开头的元素3交换。第二轮我们最初认为87 最小,经过轮比较,发现3最小,这时将87与3交换。持续进行,最终使数组有序。
最初 | 3 87 2 93 78 56 61 38 12 40 |
第1轮比较后标记 | 3 87 2 93 78 56 61 38 12 40 |
第1次交换 | 2 87 3 93 78 56 61 38 12 40 |
第2轮比较后标记 | 2 87 3 93 78 56 61 38 12 40 |
第2次交换 | 2 3 87 93 78 56 61 38 12 40 |
第3轮比较后标记 | 2 3 87 93 78 56 61 38 12 40 |
第3次交换 | 2 3 12 93 78 56 61 38 87 40 |
⋯⋯ |
可以通过点击此处进入旧金山大学提供的网站来演示选择排序动画效果。
代码实战:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
typedef int ElemType;
typedef struct {
// 存储元素的起始地址
ElemType *elem;
// 元素个数
int table_len;
} SSTable;
/*
* 顺序表初始化
*/
void st_init(SSTable &ST, int len) {
ST.table_len = len;
ST.elem = (ElemType *) malloc(sizeof(ElemType) * ST.table_len);
// 用于生成随机数的骰子
srand(time(NULL));
for (int i = 0; i < ST.table_len; i++) {
// 生产的数是0-99之间
ST.elem[i] = rand() % 100;
}
}
/*
* 顺序表打印
*/
void st_print(SSTable ST) {
for (int i = 0; i < ST.table_len; i++) {
printf("%3d", ST.elem[i]);
}
printf("\n");
}
/*
* 交换两个元素
*/
void swap(ElemType &a, ElemType &b) {
ElemType temp;
temp = a;
a = b;
b = temp;
}
/*
* 选择排序
*/
void select_sort(ElemType *A, int len) {
// min用于记录最小的元素的下标
int i, j, min;
// 如序列长度为10 下标最多遍历到8即可
// 因为i=8时A[8]已经和A[9]比较过一次
for (i = 0; i < len - 1; i++) {
min = i;
// 找到从i开始到最后的序列的最小值的下标
for (j = i + 1; j < len; j++) {
if (A[j] < A[min]) {
// min记录最小值的下标
min = j;
}
}
// 最小元素为i时不用交换
if (min != i) {
swap(A[i], A[min]);
}
}
}
int main() {
SSTable ST;
// 一、顺序表初始化
st_init(ST, 10);
// 固定数组用于调试
ElemType A[10] = {64, 94, 95, 79, 69, 84, 18, 22, 12, 78};
// memcpy可用于拷贝整型数组、浮点型数组 strcpy只能拷贝字符型数组
memcpy(ST.elem, A, sizeof(A));
// 打印顺序表
st_print(ST);
// 二、选择排序
select_sort(ST.elem, ST.table_len);
st_print(ST);
return 0;
}
时间复杂度:选择排序虽然减少了交换次数,但是循环比较的次数依然和冒泡排序的数量是一样的,都是从 1 1 1加到 n − 1 n-1 n−1,总运行次数为 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2。我们忽略循环内语句的数量,因为我们在计算时间复杂度时,主要考虑与 n n n有关的循环,如果循环内交换得多,例如有5条语句,那么最终得到的无非是 5 n 2 5n^2 5n2;循环内交换得少,例如有2条语句,那么得到的就是 2 n 2 2n^2 2n2,但是时间复杂度计算是忽略首项系数的,因此最终还是 O ( n 2 ) O(n^2) O(n2)。因此,选择排序的时间复杂度依然为 O ( n 2 ) O(n^2) O(n2)
空间复杂度:因为未使用额外的空间(额外空间必须与输入元素的个数 N相关),所以空间复杂为 O ( 1 ) O(1) O(1)。
堆排序
堆(Heap)是计算机科学中的一种特殊的树状数据结构。
若满足以下特性,则可称为堆:“给定堆中任意结点P和C,若P是C的父结点,则P的值小于等于(或大于等于)C的值。”若父结点的值恒小于等于子结点的值,则该堆称为最小堆(min heap);反之,若父结点的值恒大于等于子结点的值,则该堆称为最大堆(max heap)。堆中最顶端的那个结点称根结点 (root node),根结点本身没有父结点(parent node)•平时在工作中,我们将最小堆称小根堆或小顶堆,把最大堆称大根堆或大顶堆。
假设我们有3,87,2,93,78, 56,61, 38, 12,40共10个元素,我们将这10个元素建成一棵完全二叉树,这里采用层次建树法,虽然只用一个数组存储元素,但是我们能将二叉树中任意一个位置的元素对应到数组下标上,我们将二叉树中每个元素对应到数组下标的这种数据结构称为堆,比如最后一个父元素的下标是N/2-1,也就是a[4],对应的值为78。
为什么是N/2-1?因这是层次建立一棵完全二叉树的特性。可以这样记忆:如果父结点的下标是dad,那么父结点对应的左子结点的下标值是2*dad+1。接着,依次将每棵子树都调整为父结点最大,最终将整棵树变为一个大根堆。
可以通过点击此处进入旧金山大学提供的网站来演示堆排序动画效果。
1 | 数组元素情况 | 3 87 2 93 78 56 61 38 12 40 |
---|---|---|
数组元素对应的二叉树 | ||
2 | 数组元素情况 | 3 87 2 93 78 56 61 38 12 40 |
数组元素对应的二叉树,arr[4] 与 arr[9] 进行比较 | ||
3 | 数组元素情况 | 3 87 2 93 78 56 61 38 12 40 |
数组元素对应的二叉树,arr[3] 与 arr[7] 进行比较 | ||
4 | 数组元素情况 | 3 87 61 93 78 56 2 38 12 40 |
数组元素对应的二叉树,arr[2] 与 arr[6] 进行比较,发生交换 | ||
5 | 数组元素情况 | 3 93 61 87 78 56 2 38 12 40 |
数组元素对应的二叉树,arr[1] 与 arr[3] 进行比较,发生交换,发生交换后arr[3]重新作为父结点,与孩子结点进行比较 | ||
6 | 数组元素情况 | 93 87 61 38 78 56 2 3 12 40 |
数组元素对应的二叉树,arr[0] 与 arr[1] 进行比较,发生交换,发生交换后arr[1]重新作为父结点,与孩子结点进行比较 |
代码实战:
首先我们通过随机数生成10个元素,通过随机数生成,我们可以多次测试排序算法是否正确,然后打印随机生成后的元素顺序,然后通过堆排序对元素进行排序,然后再次打印排序后的元素顺序。
堆排序的步骤是首先把堆调整为大根堆,然后我们交换根部元素也就是A[0],和最后一个元素,这样最大的元素就放到了数组最后,接着我们将剩余9个元素继续调整大根堆,然后交换A[0]和9个元素的最后一个,循环往复,直到有序。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
typedef int ElemType;
typedef struct {
// 存储元素的起始地址
ElemType *elem;
// 元素个数
int table_len;
} SSTable;
/*
* 顺序表初始化
*/
void st_init(SSTable &ST, int len) {
ST.table_len = len;
ST.elem = (ElemType *) malloc(sizeof(ElemType) * ST.table_len);
// 用于生成随机数的骰子
srand(time(NULL));
for (int i = 0; i < ST.table_len; i++) {
// 生产的数是0-99之间
ST.elem[i] = rand() % 100;
}
}
/*
* 顺序表打印
*/
void st_print(SSTable ST) {
for (int i = 0; i < ST.table_len; i++) {
printf("%3d", ST.elem[i]);
}
printf("\n");
}
/*
* 交换两个元素
*/
void swap(ElemType &a, ElemType &b) {
ElemType temp;
temp = a;
a = b;
b = temp;
}
/*
* 调整为大根堆
*/
void adjust_down(ElemType *A, int k, int len) {
// 父亲结点的下标
int dad = k;
// 左孩子的下标
int son = 2 * dad + 1;
while (son < len) {
// 左孩子和右孩子比较
if (son + 1 < len && A[son] < A[son + 1]) {
// 如果右孩子大则拿右孩子跟父亲比较
son++;
}
// 如果孩子比父亲大
if (A[son] > A[dad]) {
// 孩子和父亲进行值交换
swap(A[son], A[dad]);
// 孩子成为新的父亲 需要去判断下面的子树符合大根堆
dad = son;
son = 2 * dad + 1;
} else {
break;
}
}
}
/*
* 堆排序
*/
void heap_sort(ElemType *A, int len) {
int i;
// 从最后一个父亲元素开始调整为大根堆
for (i = len / 2 - 1; i >= 0; i--) {
adjust_down(A, i, len);
}
swap(A[0], A[len - 1]);
// i代表的时剩余无序数的数组长度
for (i = len - 1; i > 1; i--) {
// 调整剩余元素为大根堆
adjust_down(A, 0, i);
// A[0]和无序数数组的最后一个元素
swap(A[0], A[i - 1]);
}
}
int main() {
SSTable ST;
// 一、顺序表初始化
st_init(ST, 10);
// 固定数组用于调试
ElemType A[10] = {3, 87, 2, 93, 78, 56, 61, 38, 12, 40};
// memcpy可用于拷贝整型数组、浮点型数组 strcpy只能拷贝字符型数组
memcpy(ST.elem, A, sizeof(A));
// 打印顺序表
st_print(ST);
// 二、堆排序
heap_sort(ST.elem, ST.table_len);
st_print(ST);
return 0;
}
时间复杂度:adjust_down函数的循环次数时 l o g 2 n log_2 n log2n,heap_sort函数的第一个for循环了 n / 2 n / 2 n/2次,第二个for循环了 n n n次,总计次数是 3 n l o g 2 n / 2 3nlog_2 n/2 3nlog2n/2,因此时间复杂度时 O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n)。堆排序最好、最坏、平均时间复杂度都是 O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n)。
空间复杂度:因为没有使用额外的跟 n n n相关的空间,所以空间复杂度时 O ( 1 ) O(1) O(1)。
归并类排序
如上图所示,我们把每两个元素归为一组,进行小组内排序,然后再次把两个有序小组合并为一个有序小组,不断进行,最终合并一个有序数组。
可以通过点击此处进入旧金山大学提供的网站来演示归并排序动画效果。
代码实战:归并排序的代码是采用递归思想实现的,掌握递归实现即可。首先,最小下标值和最大下标值相加并除以 2,得到中间下标值 mid,用 MergeSor t对 low 到 mid 排序,然后用 MergeSort 对 mid+1 到high 排序。当数组的前半部分和后半部分都排好序后,使用 Merge 函数。Merge 函数的作用是合并两个有序数组。为了提高合并有序数组的效率,在Merge 函数内定义了 B[N]. 首先,我们通过循环把数组 A 中从 low 到 high 的元素全部复制到 B 中,这时游标 i (遍历的变量称为游标)从low 开始,游标 j 从 mid+1开始,谁小就将谁先放入数组A,对其游标加1,并在每轮循环时对数组 A的计数游标 k 加1。
#include <stdio.h>
#include <stdlib.h>
#define N 7
typedef int ELemType;
/*
* 合并两个有序数组
*/
void merge(ELemType A[], int low, int mid, int high) {
// 家static的目的是无论递归调用多少次 都只有一个B[N]
static ELemType B[N];
int i, j, k;
// 把A[i]所有的元素都给B[i]
for (i = low; i <= high; i++) {
B[i] = A[i];
}
// 合并两个有序数组
for (i = low, j = mid + 1, k = i; i <= mid && j <= high; k++) {
if (B[i] < B[j]) {
A[k] = B[i];
i++;
} else {
A[k] = B[j];
j++;
}
}
// 把某一个数组中剩余的元素放到数组A
while (i <= mid) {
// 前一半有剩余元素放入
A[k] = B[i];
i++;
k++;
}
while (j <= high) {
// 后一半有剩余元素放入
A[k] = B[j];
j++;
k++;
}
}
/*
* 归并排序
*/
void merge_sort(ELemType A[], int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
merge_sort(A, low, mid);
merge_sort(A, mid + 1, high);
merge(A, low, mid, high);
}
}
/*
* 打印
*/
void print(ELemType A[]) {
for (int i = 0; i < N; i++) {
printf("%3d", A[i]);
}
}
int main() {
int A[N] = {49, 38, 65, 97, 76, 13, 27};
merge_sort(A, 0, 6);
print(A);
return 0;
}
时间复杂度:MergeSort 函数的递归次数是 l o g 2 n log_2 n log2n,Merge 函数的循环了 n n n次,因此时间复杂度是 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。
归并排序最好、最坏、平均时间复杂度都是 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。
空间复杂度: O ( n ) O(n) O(n),因为使用了数组B,它的大小与A一样占用n个元素的空间。
所有排序算法时间与空间复杂度汇总
各种排序算法的时间复杂度、空间复杂度、稳定性和复杂性
排序方式 | 时间复杂度 | 空间复杂度 | 稳定性 | 复杂性 | ||
---|---|---|---|---|---|---|
平均情况 | 最坏情况 | 最好情况 | ||||
插人排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 稳定 | 简单 |
希尔排序 | O ( n ( 1.3 ) ) O(n^(1.3)) O(n(1.3)) | O ( 1 ) O(1) O(1) | 不稳定 | 较复杂 | ||
冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 稳定 | 简单 |
快速排序 | O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n) | O ( n 2 ) O(n^2) O(n2) | O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n) | O ( l o g 2 n ) O(log_2 n) O(log2n) | 不稳定 | 较复杂 |
选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 | 简单 |
堆排序 | O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n) | O ( 1 ) O(1) O(1) | 不稳定 | 较复杂 |
归并排序 | O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n) | O ( n ) O(n) O(n) | 稳定 | 较复杂 |
基数排序 | O ( d ( n + r ) ) O(d(n + r)) O(d(n+r)) | O ( d ( n + r ) ) O(d(n + r)) O(d(n+r)) | O ( d ( n + r ) ) O(d(n + r)) O(d(n+r)) | O ( r ) O(r) O(r) | 稳定 | 较复杂 |
稳定性是指排序前后,相等的元素位置是否会被交换。
复杂性是指代码编写的难度。