原理和代码来自耿国华数据结构–用C语言描述(第二版)
1 排序的基本概念
- 排序:有 n n n个记录的序列 { R 1 , R 2 , … , R n } \{R_1,R_2,{\dots},R_n\} {R1,R2,…,Rn},其相应关键字的序列是 { K 1 , K 2 , … , K n } \{K_1,K_2,{\dots},K_n\} {K1,K2,…,Kn},相应的下标序列为 { 1 , 2 , … , n } \{1,2,{\dots},n\} {1,2,…,n}。通过排序,要求找出当前下标序列 { 1 , 2 , … , n } \{1,2,{\dots},n\} {1,2,…,n}的一种排序 { p 1 , p 2 , … , p n } \{p_1,p_2,{\dots},p_n\} {p1,p2,…,pn},使得相应关键字满足非递减或非递增关系。
- 内部排序:整个排序过程完全在内存中进行的。
- 外部排序:由于待排序记录数据量太大,内存无法容纳全部数据,排序需要借助外部存储设备才能完成。
- 排序的稳定性:假设在待排序的序列中存在多个具有相同关键字的记录。设 K i = K j K_i=K_j Ki=Kj。若在排序前的序列中 R i R_i Ri领先于 R j R_j Rj,经过排序后得到的序列中 R i R_i Ri仍然领先于 R j R_j Rj,则称所用的排序方法是稳定的,反之是不稳定的。
2 插入类排序
基本思想:在一个已排好序的记录子集上,每一步将下一个待排序的记录有序插入到已排好序的记录子集中,直到将所有待排序记录全部插入为止。
2.1 直接插入排序
算法思想:将第个记录插入到前面个已排好序的记录中。
#include "stdio.h"
#include "stdlib.h"
const int MaxLength = 100;
int arr[MaxLength];
// 直接插入排序的时间复杂度为O(n^2),空间复杂度为O(1)
void InsSort(int arr[], int length) {
for (int i = 2; i <= length; i++) {
arr[0] = arr[i]; // 设置监视哨
int j = i - 1;
while (arr[0] < arr[j]) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = arr[0]; // 此处需要注意是j + 1
}
}
void Print(int arr[], int length) {
for (int i = 1; i <= length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int n;
printf("请输入待排序的元素个数n,其中n必须大于0,并且小于100:");
scanf_s("%d\n", &n);
for (int i = 1; i <= n; i++) {
printf("请输入第%d个元素:", i);
scanf_s("%d", &arr[i]);
}
Print(arr, n);
InsSort(arr, n);
Print(arr, n);
system("pause");
return 0;
}
注:
直接插入排序的时间复杂度为O(n^2),空间复杂度为O(1)。
2.2 折半插入排序
算法改进:折半插入排序改进于直接插入排序,直接插入排序在寻找带插入元素的位置时,采用的是顺序查找。
算法思想:在寻找待排序记录在子表中的位置时,采用折半查找。
#include "stdio.h"
#include "stdlib.h"
const int MaxLength = 100;
int arr[MaxLength];
// 折半插入排序改善了直接插入排序中比较次数的数量级,但是时间复杂度仍为O(n^2),空间复杂度为O(1)
void BinSort(int arr[], int length) {
for (int i = 2; i <= length; i++) {
arr[0] = arr[i];
int low = 1, high = i - 1;
while (low <= high) {
int mid = (low + high) / 2;
if (arr[0] < arr[mid])
high = mid - 1;
else
low = mid + 1;
}
for (int j = i - 1; j >= low; j--)
arr[j + 1] = arr[j];
arr[low] = arr[0];
}
}
void Print(int arr[], int length) {
for (int i = 1; i <= length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int n;
printf("请输入待排序的元素个数n,其中n必须大于0,并且小于100:");
scanf_s("%d\n", &n);
for (int i = 1; i <= n; i++) {
printf("请输入第%d个元素:", i);
scanf_s("%d", &arr[i]);
}
Print(arr, n);
BinSort(arr, n);
Print(arr, n);
system("pause");
return 0;
}
注:
1、平均关键字比较次数为
n
l
o
g
2
n
nlog_2n
nlog2n,其比直接插入排序的最差情况要好,但是比其最好情况要差。
2、时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。
2.3 希尔排序
算法改进:希尔排序改进于直接插入排序,直接插入排序在待排序的关键字较少且基本有序的情况下,性能最佳,希尔排序利用了这一点。
算法思想:将待排序的关键字序列分成若干较小的子序列,对子序列进行直接插入排序,使整个待排序序列排好序。
#include "stdio.h"
#include "stdlib.h"
const int MaxLength = 100;
int arr[MaxLength];
void ShellInsert(int arr[], int length, int delta) {
for (int i = delta + 1; i <= length; i++) {
if (arr[i] < arr[i - delta]) {
arr[0] = arr[i];
int j;
for (j = i - delta; j > 0 && arr[0] < arr[j]; j -= delta)
arr[j + delta] = arr[j];
arr[j + delta] = arr[0];
}
}
}
void SheSort(int arr[], int length, int delta[], int n) {
for (int i = 0; i <= n - 1; i++) {
ShellInsert(arr, length, delta[i]);
}
}
void Print(int arr[], int length) {
for (int i = 1; i <= length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int n, delta[] = {5, 2, 1};
printf("请输入待排序的元素个数n,其中n必须大于0,并且小于100:");
scanf_s("%d\n", &n);
for (int i = 1; i <= n; i++) {
printf("请输入第%d个元素:", i);
scanf_s("%d", &arr[i]);
}
Print(arr, n);
SheSort(arr, n, delta, 3);
Print(arr, n);
system("pause");
return 0;
}
注:
1、算法在具体实现的时候,并不是先对一个子序列完成插入排序,再对另一个子序列进行插入排序,而是从第一个子序列的第二个记录开始,顺序扫描整个待排序记录序列,当前记录属于哪个子序列,就在哪一个子序列中进行插入排序。
2、希尔排序的增量序列并没有唯一的选择,此处选取
d
=
n
/
2
d=n/2
d=n/2,再取
d
=
d
/
2
d=d/2
d=d/2。
3、时间复杂度为
O
(
n
1.5
)
O(n^{1.5})
O(n1.5)。
2.4 插入类排序的总结
排序算法 | 改进思路 | 时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|---|
直接插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 | |
折半插入排序 | 改进了确定插入位置方法:利用折半思想确定在有序表中的插入位置 | O ( n 2 ) O(n^2) O(n2)比较时间复杂度为 n l o g n nlogn nlogn | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
希尔排序 | 利用直接插入排序的最好情况: n n n比较小时,基本有序 | O ( n 1.5 ) O(n^{1.5}) O(n1.5) | O ( 1 ) O(1) O(1) | 不稳定 |
3 交换类排序
基本思想:通过一系列交换逆序元素进行排序的方法。
3.1 冒泡排序
算法思想:反复扫描待排序记录序列,在扫描的过程中顺次比较相邻的两个元素的大小,若逆序就交换位置。
#include "stdio.h"
#include "stdlib.h"
const int MaxLength = 100;
int arr[MaxLength];
/*
冒泡排序的时间复杂度:O(n^2),并且是一种稳定的排序方法。
*/
void BubSort(int arr[], int length) {
bool flag = true; // 快速得知是否已经排好序
for (int i = 1; i < length && flag; i++) {
flag = false;
for (int j = 0; j < (length - i); j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = true;
}
}
}
}
void Print(int arr[], int length) {
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int n;
printf("请输入待排序的元素个数n,其中n必须大于0,并且小于等于100:");
scanf_s("%d\n", &n);
for (int i = 0; i < n; i++) {
printf("请输入第%d个元素:", (i + 1));
scanf_s("%d", &arr[i]);
}
Print(arr, n);
BubSort(arr, n);
Print(arr, n);
system("pause");
return 0;
}
注:
1、对于
n
n
n个记录的序列,冒泡排序最多进行
n
−
1
n-1
n−1趟。且若数组下标从
0
0
0开始记录,那么每趟冒泡排序选择的记录为
n
−
i
−
1
n-i-1
n−i−1个。
2、冒泡排序的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。最好的情况是待排序序列已经有序,那么此时的时间复杂度为
O
(
n
)
O(n)
O(n)。
3、空间复杂度为
O
(
1
)
O(1)
O(1)。
3.2 快速排序
算法改进:快速排序改进于冒泡排序,冒泡排序一次只能消除一个逆序。而快速排序的一次交换可能消除多个逆序。
算法思想:从待排序记录序列中选取一个记录为枢轴,将小于枢轴的记录移到其前面,大于或等于枢轴的记录移到其后面,最后将枢轴插入到两个子表分界线的位置。这一过程称为一趟快速排序。对分割后的子表再次按以上原则分割,知道所有子表的表长不超过1为止。
#include "stdio.h"
#include "stdlib.h"
const int MaxLength = 100;
int arr[MaxLength];
/*
一趟快速排序:基准选取arr[low],对arr[low]至arr[high]进行一趟排序划分,使得基准记录之前所有的
记录均小于基准,基准之后的记录均大于基准。
*/
int QKPass(int arr[], int low, int high) {
int temp = arr[low];
while (low < high) {
while (low < high && arr[high] > temp)
high--;
if (low < high) {
arr[low] = arr[high];
low++;
}
while (low < high && arr[low] < temp)
low++;
if (low < high) {
arr[high] = arr[low];
high--;
}
}
arr[low] = temp;
return low;
}
/*
完整的快速排序:注意递归控制条件low < high
*/
void QKSort(int arr[], int low, int high) {
if (low < high) {
int pos = QKPass(arr, low, high);
QKSort(arr, low, pos - 1);
QKSort(arr, pos + 1, high);
}
}
void Print(int arr[], int length) {
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int n;
printf("请输入待排序的元素个数n,其中n必须大于0,并且小于等于100:");
scanf_s("%d\n", &n);
for (int i = 0; i < n; i++) {
printf("请输入第%d个元素:", (i + 1));
scanf_s("%d", &arr[i]);
}
Print(arr, n);
QKSort(arr, 0, n - 1);
Print(arr, n);
system("pause");
return 0;
}
注:
1、快速排序的平均时间复杂度为
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
2
)
O(n^2)
O(n2)。
2、空间复杂度为
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)。
3、快速排序是一种不稳定的排序方法。
3.3 交换类排序的总结
排序算法 | 改进思路 | 时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 | |
快速排序 | 交换不相邻的两个元素,消除多个逆序 | 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 2 ) O(n^2) O(n2) | O ( l o g 2 n ) O(log_2n) O(log2n) | 不稳定 |
4 选择类排序
基本思想:每一趟在 n − i + 1 n-i+1 n−i+1个记录中选择关键字最小的记录作为有序序列中的第 i i i个记录。
4.1 简单选择排序
算法思想:每次从序列中选择最小的记录。
#include "stdio.h"
#include "stdlib.h"
const int MaxLength = 100;
int arr[MaxLength];
void SelSort(int arr[], int length) {
for (int i = 0; i < length - 1; i++) {
int index = i;
for (int j = i + 1; j < length; j++) {
if (arr[j] < arr[index])
index = j;
}
if (index != i) {
int temp = arr[i];
arr[i] = arr[index];
arr[index] = temp;
}
}
}
void Print(int arr[], int length) {
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int n;
printf("请输入待排序的元素个数n,其中n必须大于0,并且小于等于100:");
scanf_s("%d\n", &n);
for (int i = 0; i < n; i++) {
printf("请输入第%d个元素:", (i + 1));
scanf_s("%d", &arr[i]);
}
Print(arr, n);
SelSort(arr, n);
Print(arr, n);
system("pause");
return 0;
}
注:
1、简单选择排序过程中需要进行的比较次数与初始状态下的待排序的记录序列的排列情况无关。
2、平均时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。
3、最坏情况为:第一个记录最大,其余记录从小到大有序排列。
4、简单选择排序是一种不稳定的排序方法。
4.2 树形选择排序
算法改进:树形选择排序改进于简单选择排序,在简单选择排序中,
n
n
n个记录需要
n
−
1
n-1
n−1次比较,而
n
−
1
n-1
n−1个记录则要
n
−
2
n-2
n−2次比较,…,比较的操作时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。每次比较没有利用上一次比较的结果,所以若要降低比较操作的时间复杂度,则需要把比较过程中的大小关系保存下来。
算法思想:先把待排序的
n
n
n个记录的关键字两两比较,取出较小者,然后再在
n
/
2
n/2
n/2个较小中,采取同样的方法进行比较,选出每两个的较小者,如此反复,直至选出最小关键字记录为止。
树形选择排序的代码省略
注:
1、树形选择排序的时间复杂度为
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n),最好情况和最坏情况都是
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n)。
2、树形选择排序的空间复杂度为
O
(
n
)
O(n)
O(n)。
3、树形选择排序是一种稳定的排序算法
4.3 堆排序
算法改进:堆排序改进于树形选择排序,树形选择排序的空间复杂度为
O
(
n
)
O(n)
O(n),为弥补这一缺陷,从而提出的堆排序。
算法思想:把待排序的记录的关键字存放在数组arr[1…n]中,将arr看成是一颗完全二叉树的顺序排序表示,每个结点表示一个记录,任意结点arr[i]的左孩子是arr[2i],右孩子是arr[2i+1]。堆排序的过程主要解决两个问题:一是按堆定义建初堆,二是去掉最大元后重建堆,得到次大元。
①:将一个任意序列看成是对应的完全二叉树,由于叶结点可以看成单元素的堆,反复利用重建堆算法,自底向上把所有子树调整为堆,直至将整个完全二叉树调整为堆。
②:重建堆过程:首先将与堆相对应的完全二叉树根结点中的记录移出,该记录称为待调整记录。此时根结点相当于空结点,从空结点的左右子树中挑选一个关键字较大的记录,如果该记录的关键字大于待调整记录的关键字,则将该记录上移至空结点中。此时,原来那个关键字较大的子结点相当于空结点,从空结点的左右子树中选出一个关键字较大的记录,如果该记录的关键字仍大于待调整记录的关键字,则将该记录上移至空结点中。重复上述移动过程,直到空结点左右子树的关键字均小于待调整记录的关键字。此时将待调整记录放入空结点即可。
#include "stdio.h"
#include "stdlib.h"
const int MaxLength = 100;
int arr[MaxLength];
void Print(int arr[], int length) {
for (int i = 1; i <= length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
/*
1、重建堆过程:假设arr[k...m]是以arr[k]为根的完全二叉树,且分别以arr[2k]和arr[2k+1]
为根的左右子树的大根堆,调整arr[k],使得整个序列arr[k...m]满足堆得性质。
*/
void sift(int arr[], int k, int m) {
int temp = arr[k];
int i = k;
int j = 2 * i; // 左孩子下标
bool finished = false; // 设立标记,一旦左右孩子没有大于待调整记录的结点,那么重建堆完成
while (j <= m && !finished) {
if ((j + 1) <= m && arr[j] < arr[j + 1]) // 如果右孩子的关键字大,那么沿右子树进行筛选
j = j + 1;
if (temp > arr[j]) // 如果堆尾移上来的元素直接大于左右子树的最大者,筛选结束
finished = true;
else { // 否则,进一步往下筛选
arr[i] = arr[j];
i = j;
j = 2 * i;
}
}
arr[i] = temp; // 将待调整元素放置恰当的位置
}
/*
2、建初堆:对记录数组arr[]建堆,n为数组的长度。
*/
void crt_heap(int arr[], int n) {
for (int i = n / 2; i >= 1; i--) { // 从最后一个非叶子结点,自底向上逐层调整所有子树形成堆
sift(arr, i, n);
}
}
/*
3、堆排序算法:对arr[1...n]进行堆排序,执行本算法后,arr中记录按关键字由小到大有序排序
*/
void HeaSort(int arr[], int n) {
crt_heap(arr, n); // 建初堆
printf("建立大根堆如下:");
Print(arr, n);
for (int i = n; i > 1; i--) { // n-1次筛选得到堆排序结果
int temp = arr[1]; // 交换堆顶和堆底元素
arr[1] = arr[i];
arr[i] = temp;
sift(arr, 1, i - 1); // 重建堆
}
}
int main() {
int n;
printf("请输入待排序的元素个数n,其中n必须大于0,并且小于100:");
scanf_s("%d\n", &n);
for (int i = 1; i <= n; i++) {
printf("请输入第%d个元素:", i);
scanf_s("%d", &arr[i]);
}
printf("待排序序列:");
Print(arr, n);
HeaSort(arr, n);
printf("堆排序结果:");
Print(arr, n);
system("pause");
return 0;
}
注:
1、堆排序的时间复杂度为
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n),最好情况和最坏情况都是
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n)。
2、堆排序的空间复杂度为
O
(
1
)
O(1)
O(1)。
3、堆排序是一种不稳定的排序算法
4.4 选择类排序的总结
排序算法 | 改进思路 | 时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|---|
简单选择排序 | 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_2n) O(nlog2n) | 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) | 稳定 |
堆排序 | 将存储在向量中的数据元素看成一颗完全二叉树,减少了辅助空间 | 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 l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( 1 ) O(1) O(1) | 不稳定 |
5 归并排序
基本思想:将两个或两个以上有序表合并成一个新的有序表。
5.1 2路归并排序
算法思想:假设初始序列含有 n n n个记录,首先将这 n n n个记录看成 n n n个有序的子序列,每个子序列的长度为1,然后两两归并,得到 n / 2 n/2 n/2个长度为2的有序子序列,在此基础上,再对长度为2的子序列进行两两归并,直到得到长度为 n n n的有序序列。
#include "stdio.h"
#include "stdlib.h"
const int MaxLength = 100;
int arr01[MaxLength];
void Merge(int array01[], int low, int mid, int high) {
int i = low;
int j = mid + 1;
int k = 0;
int array02[MaxLength];
while (i <= mid && j <= high) {
if (array01[i] <= array01[j]) {
array02[k] = array01[i];
i++;
}
else {
array02[k] = array01[j];
j++;
}
k++;
}
while (i <= mid) {
array02[k] = array01[i];
k++;
i++;
}
while (j <= high) {
array02[k] = array01[j];
k++;
j++;
}
for (int m = low; m < low + k; m++) {
array01[m] = array02[m - low];
}
}
void MSort(int array01[], int low, int high) {
if(low < high){
int mid = (low + high) / 2;
MSort(array01, low, mid);
MSort(array01, mid + 1, high);
Merge(array01, low, mid, high);
}
}
void Print(int arr[], int length) {
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int n;
printf("请输入待排序的元素个数n,其中n必须大于0,并且小于等于100:");
scanf_s("%d\n", &n);
for (int i = 0; i < n; i++) {
printf("请输入第%d个元素:", i + 1);
scanf_s("%d", &arr01[i]);
}
Print(arr01, n);
MSort(arr01, 0, n - 1);
Print(arr01, n);
system("pause");
return 0;
}
注:
1、归并排序的时间复杂度为
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n),空间复杂度为
O
(
n
)
O(n)
O(n)。
2、归并排序是一种稳定的排序方法。
3、由于需要附加辅助空间,归并排序一般不用于内部排序,而是用于外部排序。
5.2 归并排序的总结
排序算法 | 改进思路 | 时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|---|
归并排序 | 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 l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( n ) O(n) O(n) | 稳定 |
6 分配类排序
基本思想:利用分配和收集两种基本操作实现排序。而之前所述的各种排序方法(插入类排序、交换类排序、选择类排序和归并排序)都是基于比较和交换。
6.1 多关键字排序
一般用例子来说明,了解思想即可。
6.2 链式基数排序
算法思想:基数排序属于上述“低位优先”排序法,通过反复进行分配和收集操作完成排序。