写在前面:
- 本系列笔记主要以《数据结构(C语言版)》为参考(本章部分图片来源于王道),结合下方视频教程对数据结构的相关知识点进行梳理。所有代码块使用的都是C语言,如有错误欢迎指出。
- 视频链接:第01周a--前言_哔哩哔哩_bilibili
- 基数排序部分的代码参考了一位小伙伴分享的代码,特此说明一下,其它C代码均由笔者根据书上的类C代码进行编写。
一、排序的基本概念和方法概述
1、排序的基本概念
(1)排序是按关键字的非递减或非递增顺序对一组记录重新进行排列的操作,如果参加排序的数据结点包含多个数据域,那么排序往往是针对其中某个域而言。
(2)当待排序的序列中存在两个或两个以上关键字相等的记录时,则排序所得的结果不唯一。假设(1≤i≤n,1≤j≤n,i≠j),且在排序前的序列中
领先于
(即i<j),若在排序后的序列中
仍领先于
,则称所用的排序方法是稳定的,反之,若可能使排序后的序列中
领先于
,则称所用的排序方法是不稳定的。
(3)排序的分类:
①按数据存储介质可分为:
1\]内部排序:待排序记录全部存放在计算机内存中进行排序的过程。
\[2\]外部排序:待排序记录的数量很大,以致内存一次不能容纳全部记录,在排序过程中尚需对外存进行访问的排序过程。
②按自然性可分为:
\[1\]自然排序:输入数据越有序,排序的速度越快的排序方法。
\[2\]非自然排序:不是自然排序的排序方法。
③按使用存储空间的大小可分为:
\[1\]原地排序:辅助空间用量为O(1)的排序方法。
\[2\]非原地排序:辅助空间用量超过O(1)的排序方法。
### 2、内部排序方法的分类
(1)插入类:将无序子序列中的一个或几个记录插入有序序列,从而增加记录的有序子序列的长度。主要包括直接插入排序、折半插入排序和希尔排序。
(2)交换类:通过交换无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入有序子序列中,以此方法增加记录的有序子序列的长度。主要包括冒泡排序和快速排序。
(3)选择类:从记录的无序子序列中选择关键字最小或最大的记录,并将它加入有序子序列中,以此方法增加记录的有序子序列的长度。主要包括简单选择排序和堆排序。
(4)归并类:通过归并两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度。2-路归并排序是最为常见的归并排序方法。
(5)分配类:是唯一一类不需要进行关键字比较的排序方法,排序时主要利用分配和收集两种基本操作来完成。基数排序是主要的分配排序方法。
### 3、待排序记录的存储方式
(1)顺序表:记录之间的次序关系由其存储位置决定,实现排序需要移动记录。(除基数排序外,下面介绍的算法均使用顺序表进行介绍)
```cpp
#define MAXSIZE 20
typedef int ElemType;
typedef int KeyType;
typedef struct
{
KeyType key; //关键字项
ElemType otherinfo; //其它数据项
}RedType;
typedef struct
{
RedType r[MAXSIZE + 1]; //0号元素闲置或用于做哨兵
int length;
}SqList;
```
(2)链表:记录之间的次序关系由指针指示,实现排序不需要移动记录,仅需修改指针,这种排序方式称为链表排序。
(3)待排序记录本身存储在一组地址连续的存储单元内,同时另设一个指示各个记录存储位置的地址向量,在排序过程中不移动记录本身,而移动地址向量中这些记录的"地址",在排序结束之后按照地址向量中的值调整记录的存储位置,这种排序方式称为地址排序。
### 4、排序算法效率的评价指标
(1)执行时间:对于排序操作,时间主要消耗在关键字之间的比较和记录的移动上(这里只考虑以顺序表方式存储待排序记录),排序算法的时间复杂度由这两个指标决定,因此可以认为高效的排序算法的比较次数和移动次数都应该尽可能的少。
(2)辅助空间:空间复杂度由排序算法所需的辅助空间决定,辅助空间是除了存放待排序记录占用的空间之外,执行算法所需要的其它存储空间。理想的空间复杂度为O(1),即算法执行期间所需要的辅助空间与待排序的数据量无关。
## 二、插入排序
### 1、概述
(1)插入排序的基本思想:每一趟将一个待排序的记录,按其关键字的大小插入已经排好序的一组记录的适当位置,直到所有待排序记录全部插入为止。
(2)可以选择不同的方法在已排好序的记录中寻找插入位置。根据查找方法的不同,有多种插入排序方法,这里仅介绍3种方法:直接插入排序、折半插入排序和希尔排序。
### 2、直接插入排序
(1)采用顺序查找法查找插入位置,然后插入即可。
(2)算法实现:
```cpp
void InsertSort(SqList* L) //直接插入排序
{
for (int i = 2; i <= L->length; i++)
{
if (L->r[i].key < L->r[i - 1].key) //新加入元素小于先前加入的最大元素,说明需要插入到前部
{
L->r[0] = L->r[i]; //将待插入的记录暂存到监视哨中
L->r[i] = L->r[i - 1]; //r[i-1]后移
int j;
for (j = i - 2; L->r[0].key < L->r[j].key; j--) //从后向前寻找插入位置
L->r[j + 1] = L->r[j]; //记录逐个后移,直到找到插入位置
L->r[j + 1] = L->r[0]; //将r[0](即原r[i])插入正确位置
}
}
}
```
(3)该算法的时间复杂度为O(),空间复杂度为O(1)。
(4)该算法能实现稳定排序,比较简便,易于实现,且适用于链式存储结构,当初始记录基本有序(正序)的时候该算法的效率较高。
### 3、折半插入排序
(1)采用折半查找法查找插入位置,然后插入即可。
(2)算法实现:
```cpp
void BInsertSort(SqList* L) //折半插入排序
{
for (int i = 2; i <= L->length; i++)
{
L->r[0] = L->r[i]; //将待插入的记录暂存到监视哨中
int low = 1, high = i - 1; //置查找区间初值
while (low <= high) //在查找区间中折半查找插入的位置
{
int m = (low + high) / 2; //折半
if (L->r[0].key < L->r[m].key)
high = m - 1;
else
low = m + 1;
}
for (int j = i - 1; j >= high + 1; j--) //记录后移
L->r[j + 1] = L->r[j];
L->r[high + 1] = L->r[0]; //将r[0](即原r[i])插入正确位置
}
}
```
(3)该算法的时间复杂度为O(),空间复杂度为O(1)。
(4)该算法能实现稳定排序,比较简便,易于实现,但不适用于链式存储结构,适合初始记录无序、记录量较大的情况。
### 4、希尔排序
(1)希尔排序实质上是采用分组插入的方法,先将整个待排序记录序列分割成几组(将相隔某个"增量"的记录分成一组),从而减少参与直接插入排序的数据量,对每组分别进行直接插入排序,然后增加每组的数据量,重新分组,这样当经过几次分组排序后,整个序列中的记录"基本有序"时,再对全体记录进行一次直接插入排序。(增量序列可以有各种取法,但应该使增量序列中的值没有除1之外的公因子,并且最后一个增量值必须等于1)

(2)算法实现:
```cpp
void ShellInsert(SqList* L, int dk) //一趟希尔排序
{
for (int i = dk + 1; i <= L->length; i++)
{
if (L->r[i].key < L->r[i - dk].key) //将L->r[i]插入有序增量子表
{
L->r[0] = L->r[i]; //暂存在r[0]中
int j;
for (j = i - dk; j > 0 && L->r[0].key < L->r[j].key; j -= dk)
L->r[j + dk] = L->r[j]; //记录后移,直到找到插入位置
L->r[j + dk] = L->r[0]; //将r[0](即原r[i])插入正确位置
}
}
}
void ShellSort(SqList* L, int dt[], int t) //希尔排序
{
for (int k = 0; k < t; k++)
ShellInsert(L, dt[k]); //一趟增量为dt[t]的希尔插入排序
}
```
(3)该算法的平均时间复杂度为O(),空间复杂度为O(1)。
(4)该算法不能实现稳定排序,只能用于顺序结构,不能用于链式结构。该算法记录总的比较次数和移动次数都比直接插入排序的要少,n越大时效果越明显,所以适合初始记录无序、n较大时的情况。
## 三、交换排序
### 1、概述
交换排序的基本思想是:两两比较待排序记录的关键字,一旦发现两个记录不满足次序要求时则进行交换,直到整个序列全部满足要求为止。
### 2、冒泡排序
(1)冒泡排序是一种最简单的交换排序方法,它通过两两比较相邻记录的关键字,如果为逆序,则进行交换,从而使关键字小的记录如气泡一般逐渐往上"漂浮"(左移),或者使关键字大的记录如石块一样逐渐向下"坠落"(右移),对待排序记录经过若干轮比较后,排序结束。

每趟结束时,不仅能挤出一个最大值到最后面,还能同时部分理顺其它元素,下一趟不必比较上一趟得出的最大值。
(2)算法实现:
```cpp
void BubbleSort(SqList* L) //冒泡排序
{
int m = L->length - 1;
int flag = 1; //flag用来标记某一趟排序是否发生交换
while ((m > 0) && (flag == 1)) //最坏情况下需要m-1趟排序
{
flag = 0; //flag置为0,如果本趟排序没有发生交换,则不必执行下一趟排序
for (int j = 1; j < m; j++)
if (L->r[j].key > L->r[j + 1].key)
{
flag = 1; //flag置为1,表示本趟排序发生了交换
RedType tmp = L->r[j]; //交换前后两个记录
L->r[j] = L->r[j + 1];
L->r[j + 1] = tmp;
}
m--;
}
}
```
(3)该算法的时间复杂度为O(),空间复杂度为O(1)。
(4)该算法能实现稳定排序,比较简便,易于实现,且适用于链式存储结构,但是移动记录的次数较多,算法的平均时间性能比直接插入排序差,当初始记录无序且记录量较大时,此算法不宜采用。
### 3、快速排序
(1)快速排序是由冒泡排序改进而得的,在冒泡排序过程中,只对相邻的两个记录进行比较,因此每次交换两个相邻记录时只能消除一个逆序排列,如果能通过两个(不相邻)记录的一次交换消除多个逆序排列,则会大大加快排序的速度。
(2)算法具体步骤:在待排序的n个记录中任取一个记录(通常取第一个记录)作为枢轴,设其关键字为pivotkey,经过一趟排序后,把所有关键字小于pivotkey的记录交换到前面,把所有关键字大于pivotkey的记录交换到后面,结果将待排序记录分成两个子表,最后将枢轴放置在分界处的位置,然后分别对左、右子表重复上述过程,直至每一子表只有一个记录时,排序完成。(若每一次选中的"枢轴"都将待排序序列划分为均匀的两个部分,则算法效率最高)

(3)算法实现:
```cpp
int Partition(SqList* L, int low, int high)
{
L->r[0] = L->r[low]; //用子表的第一个记录作为枢轴记录
KeyType pivotkey = L->r[low].key; //枢轴记录关键字保存在pivotkey中
while (low < high) //从表的两端交替地向中间查找
{
while (low