文章目录
- 一、排序
-
- [ 排序类型定义:](# 排序类型定义:)
- [二、直接插入排序(Insertion Sort)](#二、直接插入排序(Insertion Sort))
-
- [ 1、工作原理](# 1、工作原理)
- [ 2、算法步骤](# 2、算法步骤)
- [ 3、代码实现](# 3、代码实现)
- [ 4、测试代码](# 4、测试代码)
- [三、折半插入排序(Binary Insert Sort)](#三、折半插入排序(Binary Insert Sort))
-
- [ 1、实现代码](# 1、实现代码)
- [ 2、直接插入和折半插入的比较](# 2、直接插入和折半插入的比较)
- [四、希尔排序(Shell's Sort)](#四、希尔排序(Shell's Sort))
-
- [ 1、希尔排序特点:](# 1、希尔排序特点:)
- [ 2、排序示例](# 2、排序示例)
- [ 3、代码实现](# 3、代码实现)
- [ 4、测试代码](# 4、测试代码)
- [五、冒泡排序(Bubble Sort)](#五、冒泡排序(Bubble Sort))
-
- [ 1、工作原理:](# 1、工作原理:)
- [ 2、实现代码](# 2、实现代码)
- 六、快速排序(Quicksort)
-
- [ 1、算法原理](# 1、算法原理)
- [ 2、实现步骤](# 2、实现步骤)
- [ 3、实现代码](# 3、实现代码)
- [七、选择排序(Selection Sort)](#七、选择排序(Selection Sort))
-
- [ 1、工作原理](# 1、工作原理)
- [ 2、实现代码](# 2、实现代码)
- [ 3、测试代码](# 3、测试代码)
- 八、堆排序(Heapsort)
-
- [ 1、堆排序](# 1、堆排序)
- [ 2、堆调整与排序代码实现](# 2、堆调整与排序代码实现)
- [九、并归排序(Merge Sort)](#九、并归排序(Merge Sort))
-
- [ 1、算法原理](# 1、算法原理)
- [ 2、实现步骤](# 2、实现步骤)
- [ 3、代码实现](# 3、代码实现)
- [十、基数排序(Radix Sort)](#十、基数排序(Radix Sort))
-
- [ 1、算法思想](# 1、算法思想)
- [ 2、算法步骤](# 2、算法步骤)
-
- [ 1)、先按个位排序](# 1)、先按个位排序)
- [ 2)、再按十位排序](# 2)、再按十位排序)
- [ 3)、再按百位排序](# 3)、再按百位排序)
- [ 3、代码实例](# 3、代码实例)
-
- [ 1)、数据类型定义](# 1)、数据类型定义)
- [ 2)、分配函数](# 2)、分配函数)
- [ 3)、// 收集函数](# 3)、// 收集函数)
- [ 4)、排序函数](# 4)、排序函数)
- [ 5)、初始化和输出函数](# 5)、初始化和输出函数)
- 十一、总结
一、排序
排序是计算机科学与技术领域中的一项基本操作,旨在将一组数据按某种顺序排列。
排序类型定义:
c
#define SORTSIZE 20
typedef int SortType;
typedef char SortOther;
typedef struct __RecordType {
SortType key;
SortOther other;
} RecordType;
typedef struct __RecordList {
RecordType r[SORTSIZE + 1]; //数组中的0号位置作guard
int length;
} RecordList;
二、直接插入排序(Insertion Sort)
1、工作原理
初始状态:将数组的第一个元素视为已排序部分,其余元素视为未排序部分。
遍历过程:从数组的第二个元素开始,逐个将未排序部分的元素插入到已排序部分的适当位置。
插入操作:对于每个待插入的元素,从已排序部分的末尾开始向前扫描,找到其应该插入的位置,并将该位置及其后的元素向后移动一位,然后将待插入元素放入正确位置。
2、算法步骤
将数组分为已排序部分和未排序部分。
从未排序部分的第一个元素开始,逐个取出元素进行插入操作。
在已排序部分中找到待插入元素的正确位置,并进行插入。
重复步骤2和3,直到所有元素都被插入到已排序部分中。

3、代码实现
c
void InsertSort(RecordList *List)
{
for (int i = 2; i <= List->length; i++)
{
// 因为有guard所以,i = 2,而不是 i = 1;
if (List->r[i].key < List->r[i - 1].key)
{
// 判断下标为i的元素是否大于i-1,如果大于则不需要进行排序
// 如果小于则需要进行如下排序
List->r[0].key = List->r[i].key; // 设置guard
List->r[i] = List->r[i - 1]; // 向后移动
int j;
for (j = i - 2; List->r[0].key < List->r[j].key; j--)
{
List->r[j + 1] = List->r[j];
}
// 当上面循环结束后,下标为j的元素此时小于guard,则向j的直接后继(j+1)插入
List->r[j + 1] = List->r[0];
}
}
}
4、测试代码
c
#include "InsertSort.h"
void input(RecordList *RL);
int main(void)
{
RecordList RL;
RL.length = 9;
input(&RL);
InsertSort(&RL);
for (int i = 1; i <= RL.length; i++)
{
printf("%d ", RL.r[i].key);
}
system("pause");
return 0;
}
// 输入数据:47 7 29 11 16 92 22 8 3
void input(RecordList *RL)
{
RecordType *p = RL->r + 1;
for (int i = 1; i <= RL->length; i++)
{
SortType key;
scanf(" %d", &key);
p->key = key;
p++;
}
p = NULL;
}

三、折半插入排序(Binary Insert Sort)
在直接插入排序的基础上,对已经排序好的有序表进行折半操作,随着折半的进行,right+1就是插入的位置。
1、实现代码
c
void InserSotr_Binary(RecordList *List)
{
for (int i = 2; i <= List->length; i++)
{
List->r[0] = List->r[i];
int left = 1;
int right = i - 1;
while (left <= right)
{
int mid = (left + right) / 2;
if (List->r[0].key > List->r[mid].key)
{
left = mid + 1;
}
else
{
right = mid - 1;
}
}
for (int j = i - 1; j >= right + 1; j--)
{
List->r[j + 1] = List->r[j];
}
List->r[right + 1] = List->r[0];
}
}
2、直接插入和折半插入的比较
折半插入的比较次数和待排序序列的初始排列无关,仅依赖序列元素个数
折半插入减少了比较次数,但是没有减少移动次数
折半插入平均性能优于直接插入排序
直接插入在基本有序时,效率更高
四、希尔排序(Shell's Sort)
希尔排序的基本思想是将待排序的一组元素按一定间隔分成若干个子序列,分别进行插入排序。
开始时设置的"间隔"较大,在每轮排序中将间隔逐步减小,直到"间隔"为1,此时进行最后一次插入排序,整个排序过程结束。
1、希尔排序特点:
移动位置较大,跳跃式地接近排序后的最终位置。
最后一次只需要少量移动。
增量序列必须是递减的,最后一个必须是1。
增量序列必须是互质的。
2、排序示例
SqList的的length=9,R[10]={0,9,1,5,8,3,7,4,6,2}。
第一轮increment=4:

第二轮increment=2:

第三轮increment=1:

最终结果:

3、代码实现
c
void ShellInsert(RecordList *L, int dk)
{
for (int i = 1 + dk; i <= L->length; i++)
{
if (L->r[i].key < L->r[i - dk].key)
{
L->r[0] = L->r[i];
L->r[i] = L->r[i - dk];
int j;
for (j = i - (2 * 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];
}
}
}
void ShellSort(RecordList *L, int *dt, int t)
{
for (int i = 0; i < t; i++)
{
ShellInsert(L, dt[i]);
}
}
4、测试代码
c
#include "InsertSort.h"
void input(RecordList *RL);
int main(void)
{
RecordList RL;
RL.length = 10;
int dt[3] = {5, 3, 1};
input(&RL);
ShellSort(&RL, dt, 3);
for (int i = 1; i <= RL.length; i++)
{
printf("%d ", RL.r[i].key);
}
system("pause");
return 0;
}
// 输入数据:49 38 65 97 76 13 27 49 55 4
void input(RecordList *RL)
{
RecordType *p = RL->r + 1;
for (int i = 1; i <= RL->length; i++)
{
SortType key;
scanf(" %d", &key);
p->key = key;
p++;
}
p = NULL;
}

五、冒泡排序(Bubble Sort)
1、工作原理:
比较相邻的元素。如果第一个比第二个大(对于升序排序,如果是降序则相反),就交换它们两个。
对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大数(或最小数)。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
2、实现代码
c
void BubbleSort(RecordList *L)
{
bool flag = true;
for (int i = 1; i <= L->length - 1 && flag; i++)
{
// 注意此行flag的位置,不要写在j循环里面
flag = false;
for (int j = 1; j <= L->length - i; j++)
{
if (L->r[j].key > L->r[j + 1].key)
{
// 只要发生一次交换,flag就为true
flag = true;
RecordType temp = L->r[j];
L->r[j] = L->r[j + 1];
L->r[j + 1] = temp;
}
}
}
}
六、快速排序(Quicksort)
1、算法原理
快速排序的核心原理是选择一个基准元素(pivot),将待排序数组划分为两个子数组,一个子数组包含所有小于基准的元素,另一个子数组包含所有大于基准的元素(注意,基准元素在其最后的排序数组中的位置就已经被确定),然后递归地对两个子数组进行快速排序,以此达到整个数组有序的目的。
2、实现步骤
选择基准: 从数组中选择一个元素作为基准元素。基准元素的选择有多种策略,如选择第一个元素、最后一个元素、中间元素或随机选择一个元素等。
划分操作: 通过一系列的比较和交换操作,使得小于基准的元素都在基准的左边,大于基准的元素都在基准的右边。这个过程称为划分(partition)。
递归排序: 递归地对划分出来的左右两个子数组进行快速排序。
合并: 由于快速排序是就地排序(in-place sort),在划分过程中已经实现了子数组的排序,因此不需要额外的合并步骤。

3、实现代码
c
// 找pivot
int Partition(RecordList *L, int left, int right)
{
L->r[0] = L->r[left];
while (left < right)
{
while (left < right && L->r[0].key <= L->r[right].key)
// 必须<=,如果不加等于号,则死循环
right--;
L->r[left] = L->r[right];
while (left < right && L->r[0].key >= L->r[left].key)
left++;
L->r[right] = L->r[left];
}
// 此时 left和right重叠
L->r[left] = L->r[0];
return left;
}
// 排序模型
void QSort(RecordList *L, int left, int right)
{
if (left < right)
{
// 获取pivot位置
int pivot = Partition(L, left, right);
// pivot把序列分成两块,并分别递归
QSort(L, left, pivot - 1);
QSort(L, pivot + 1, right);
}
}
// 为了使用方便,封装函数
void QuickSort(RecordList *L)
{
QSort(L, 1, L->length);
}
七、选择排序(Selection Sort)
1、工作原理
从未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置。
再从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。

2、实现代码
c
void SelectionSort(RecordList *L)
{
int i, j;
for (i = 1; i < L->length; i++)
{
int k = i; // k记录最大值或最小值
for (j = i + 1; j <= L->length; j++)
{
if (L->r[k].key < L->r[j].key) // 从大到小排序
k = j;
// k记录最大值
}
if (k != i)
{ // 如果k!=i 说明有值比i大
SortType temp = L->r[i].key;
L->r[i].key = L->r[k].key;
L->r[k].key = temp;
}
}
}
3、测试代码
c
#include "ExchangeSort.h"
void input(RecordList *RL);
int main(void)
{
RecordList RL;
RL.length = 10;
input(&RL);
SelectionSort(&RL);
for (int i = 1; i <= RL.length; i++)
{
printf("%d ", RL.r[i].key);
}
system("pause");
return 0;
}
八、堆排序(Heapsort)
1、堆排序
堆: 堆是一种特殊的完全二叉树结构,分为大根堆和小根堆。大根堆要求每个节点的值都不小于其父节点的值,即堆顶元素为最大值;小根堆则要求每个节点的值都不大于其父节点的值,即堆顶元素为最小值。
堆排序: 堆排序是利用堆这种数据结构所设计的一种排序算法。它首先将要排序的数列构造成一个堆,然后反复将堆顶元素(最大值或最小值)与末尾元素交换,并重新调整堆结构,直到整个数列有序。
建堆: 将无序数组整理为堆。具体步骤为找到无序数组最后一个非叶子节点(也是最后一个父节点),随后对每一个非叶子节点都进行一次堆整理(下移操作),直到所有节点以下的子堆都已符合最小堆/最大堆的要求。
交换与调整: 将堆的尾节点与堆的根节点交换,并将堆的大小往前挪一个单位(等同于移除原来的堆根节点)。然后从根节点开始,对新的堆进行堆调整,使其重新满足堆的性质。
重复步骤: 重复上述交换与调整步骤,直到堆的大小为1,此时数组已完全排序。

2、堆调整与排序代码实现
c
void HeapAdjust(RecordList *L, int s, int n)
{
RecordType temp = L->r[s]; // temp暂存,用来作哨兵
for (int j = 2 * s; j <= n; j *= 2)
{
// 此处 j<n 确保了如果j是序列最后的元素不进行比较
if (j < n && L->r[j].key < L->r[j + 1].key)
j++; // j记录最大值
// 如果j的key<=temp 说明temp为根的大根堆
if (L->r[j].key <= temp.key)
break;
// 因为j.key > s.key 所以把j赋给s
L->r[s] = L->r[j];
//! 注意并不需要更改j
// 因为当发生堆调整时j会赋值给s,并且s=j,即再次循环发生堆调整时,s变动,也就是说上一个j发生变动
// 所以不用特地给j赋值。
s = j;
}
// 当循环结束再给s赋值,也就是给上一个j赋值
L->r[s] = temp;
}
void HeapSort(RecordList *L)
{
InitHeap(L);
for (int i = L->length; i > 1; i--)
{
RecordType temp = L->r[1];
L->r[1] = L->r[i];
L->r[i] = temp;
HeapAdjust(L, 1, i - 1);
}
}
九、并归排序(Merge Sort)
1、算法原理
归并排序的主要思路是:将待排序序列分成若干个子序列,每个子序列是有序的(初始时,每个子序列只包含一个元素,因此可以认为它们是有序的);然后再将这些有序子序列逐步合并,得到更大的有序序列,直到最后合并成一个完全有序的序列。
2、实现步骤
分解: 将待排序的n个元素的序列分成两个子序列,每个子序列包含n/2个元素(当n为偶数时)或接近n/2个元素(当n为奇数时)。
解决: 使用归并排序递归地排序两个子序列。
合并: 将两个已排序的子序列合并成一个最终的排序序列。

3、代码实现
c
// 合并两个有序序列
// 令R的1到mid为有序序列,mid+1到n为有序序列
void Merge(RecordType *R, RecordType *Temp, int left, int mid, int right)
{
int i = left, j = mid + 1, k = left;
while (i <= mid && j <= right)
{
// 利用三目运算符简化语句
// <= 保证了稳定性
Temp[k++] = (R[i].key <= R[j].key ? R[i++] : R[j++]);
}
while (i <= mid)
Temp[k++] = R[i++];
while (j <= right)
Temp[k++] = R[j++];
}
// 分割序列并进行合并
void MSort(RecordType *R, RecordType *Temp, int left, int right)
{
// 递归函数的出口为left = right = 1 和 left = right = mid+1
if (left >= right)
return;
int mid = (left + right) >> 1;
MSort(R, Temp, left, mid);
MSort(R, Temp, mid + 1, right);
Merge(R, Temp, left, mid, right);
//! 注意:此函数的核心语句为下行的循环
//! 每一次递归都需要更新R数组,因为进行栈底递归时,R必须为以mid为中心,两侧都是有序序列
for (int i = left; i < right + 1; ++i)
{
R[i] = Temp[i];
}
}
//函数封装
void MergeSort(RecordList *L)
{
RecordType Temp[L->length + 1];
MSort(L->r, Temp, 1, L->length);
}
十、基数排序(Radix Sort)
1、算法思想
第一步、获取待排序元素的最大值,并确定其位数。
第二步、从最低位开始,依次对所有元素进行"分配"和"收集"操作。
第三步、在每一位上,根据该位上数字的值将元素分配到相应的桶中。
第四步、对每个桶中的元素进行顺序收集,得到排序后的部分结果,
重复上述步骤,直到对所有位都进行了排序。
2、算法步骤
基数排序则不需要比较,通过关键字中的信息进行分类,进行分配和采集来实现排序。

1)、先按个位排序

2)、再按十位排序

3)、再按百位排序

3、代码实例
1)、数据类型定义
c
// 采用静态链表来对3位数字排序
#define MAXBIT 3 // 排序的关键字为3位
#define RADIX 10 // 对10进制进行排序
#define MAX_SPACE 100 // 最多可以有99个待排序元素,因为有一个头节点
struct SLCell
{
// 元素类型
SortType keys[MAXBIT]; // 存储个位,十位,百位
SortOther other;
// 其他信息
int next;
};
int length;
// 存放下一个元素在数组中的位置
struct SLList
{
SLCell r[MAX_SPACE]; // r[0]不存放数据,类似于链表的头指针
int bitnumber; // 表示此静态链表对n位数排序
int length; // 表中有效元素个数
};
typedef int RadixArr[RADIX]; // 用于创建first, end 数组
2)、分配函数
c
void Distrubute(SLCell *r, int i, RadixArr first, RadixArr end)
{
// r表示 SLCell数组的首地址,i=0,i=1,i=2 分别表示对百位,十位,个位进行分配
// first数组存放首个被分配的下标,end存放first指向的最后元素
memset(first, 0, sizeof(int) * RADIX); // 初始化为0
memset(end, 0, sizeof(int) * RADIX);
for (int p = r[0].next; p; p = r[p].next)
{
// 因为r[0]为头指针,所以p指向表中第一个元素
int j = r[p].keys[i]; // j为下标,等式右边则表示映射关系
// 如果first[j]==0说明first指向任何元素,直接把p赋给first[j],
if (!first[j])
first[j] = p;
else
{
// first[j]已经有指向,那么需要找到end[j],并把它们连起来
r[end[j]].next = p;
}
// 因为p指向新加入的元素,所以最后一个元素变成p
end[j] = p;
}
}
3)、// 收集函数
c
void Collect(SLCell *r, int i, RadixArr first, RadixArr end)
{
// 此时分配已经完成,需要做的是按顺序把分配的元素连起来,即收集
int j = 0;
// 寻找第一个非空的first子表
while (!first[j])
j++;
// 此时j指向第一个非空子表
r[0].next = first[j]; // 让头指针指向此子表
int tail = end[j];
// tail代表此子表最后元素的下标
// 寻找第2个非空子表,依此类推,直到j>=Radix
for (j = j + 1; j < RADIX; j++)
{
if (!first[j])
continue; // 如果子表为空则跳过
else
{ // 当不为空时
r[tail].next = first[j]; // 让上一个子表的最后一个元素指向first[j]
tail = end[j]; // 此时更新尾部下标
}
}
// 当下面循环结束后,说明收集完毕
r[tail].next = 0;
}
4)、排序函数
c
void RadixSort(SLList *L)
{
// 创建first,end数组,不需要初始化,因为Distrubute(函数会进行初始化)
RadixArr first, end;
// 因为是静态链表,需要更新next
for (int i = 0; i < L->length; ++i)
{
L->r[i].next = i + 1;
}
L->r[L->length].next = 0; // 设置结束表示0
// 因为是对三位数进行分配,依此对个位,十位,百位进行分配并收集
for (int i = L->bitnumber - 1; i >= 0; --i)
{
Distrubute(L->r, i, first, end);
Collect(L->r, i, first, end);
}
}
5)、初始化和输出函数
c
void Init_Radix_3(SLList *L, int n)
{
int hundreds, tens, ones;
printf("Please input %d intergers(tree digits):", n);
for (int i = 1; i < n + 1; ++i)
{
int value;
scanf(" %d", &value);
ones = value % 10;
tens = (value / 10) % 10;
hundreds = value / 100;
L->r[i].keys[0] = hundreds;
L->r[i].keys[1] = tens;
L->r[i].keys[2] = ones;
}
L->length = n;
L->bitnumber = 3;
}
void Display_Radix(SLList *L)
{
for (int p = L->r[0].next; p != NULL; p = L->r[p].next)
{
for (int i = 0; i < L->bitnumber; i++)
{
printf("%d", L->r[p].keys[i]);
}
printf(" ");
}
}
十一、总结
