1.排序
1.1概念及其应用
• 排序就是将⼀个数据元素的任意序列,重新排列成⼀个按关键字有序的序列。假设含有n个记录的序列为 ,其对应的关键字为 ,需确定 的⼀种排列,使其对应的关键字满⾜如下的⾮递减或⾮递增关系,使得序列成为按关键字有序的序列
• 排序在实际⽣活中随处可⻅,如购物软件中通过综合评分、评价、价格等排序商品⽅便筛选。再或者⼤学综合实⼒排⾏榜,再或者华润富豪排⾏榜等等
1.2常见的排序算法

1.3排序算法的评价指标
• 复杂度:评价排序算法的第⼀⼤指标就是时间复杂度和空间复杂度,它衡量算法的时间效率和空间效率。
• 稳定性:相等的元素排序前后相对位置保持不变 如i1=5,i2=5,i3=3
在排序前数组为i1,i2,i3, 排序完成后为i3,i1,i2. i1还是在i2的前面
1.4内部排序和外部排序
• 由于待排序的记录数量不同,使得排序过程中涉及的存储器不同,可以将排序⽅法分为两⼤类:
内部排序:待排序的记录存放在计算机随机存储器(内存)中进⾏排序的过程。内部排序关注如何让
复杂度更低。
外部排序:待排序的记录数据量太⼤,以致内存⼀次不能容纳全部记录,在排序过程中需要对外存
(硬盘)进⾏访问的排序过程。外部排序关注如何使读写硬盘的次数更少。
• DDR5内存的读写速度可以达到 50-100GB/s,机械硬盘(HDD)的读写速度80-200MB/s,顶级固态硬盘(SSD)读写速度约为 7-14GB/s,内存的速度是顶级固态硬盘5-10倍,所以外排序影响性能的是读写硬盘的次数。
排序过程动图⽹站
•新加坡国⽴⼤学 (NUS) 计算机学院的教授 Steven Halim 博⼠牵头创建的:
https://visualgo.net/en/sorting
• 美国旧⾦⼭⼤学 (University of San Francisco) 计算机科学系 的官⽅⽹站:
https://www.cs.usfca.edu/\~galles/visualization/ComparisonSort.html
2.插入排序
2.1直接插入排序
2.1.1思想及过程
•
插⼊排序类似于玩扑克牌(⽃地主)等插牌过程,每次将⼀个待排序的元素按照其关键字⼤⼩插⼊到
前⾯已排好序的序列中,按照该种⽅式将所有元素全部插⼊完成即可。

• 单次插⼊思想:
0,end 为有序区间,将 end+1 位置的关键字插⼊到 0,end 有序区间,达到
0,end+1 区间有序。

• 整体插⼊思想:
- 直接插⼊排序最开始是将 0,0 做为第⼀个有序区间,将1位置的值插⼊到 0,0 ,这样
0,1 区间就有序了; - 再把2位置插⼊到 0,1 ,这样 0,2 区间就有序了;
- 以此类推,不断重复上述过程,直到 0,n-2 有序,最后把n-1位置插⼊到 0,n-2 区间,
这样 0,n-1 序列就整体有序了;

2.1.2代码实现

#pragma once
#include<stdio.h>
#include<assert.h>
void insertsort(int* a, int n);
#include"sort.h"
void insertsort(int*a,int n) {
for (int end = 0; end < n-1; end++) {
int tmp = aend+1;
while (aend>tmp && end >= 0) {
aend + 1 = aend;
end--;
}
aend + 1 = tmp;
}
}
void PrintArray(int* a, int n) {
for (int i = 0; i < n; i++) {
printf("%d ", ai);
}
printf("\n");
}
int main() {
int a\[\] = { 1,3,2,5,8,9,7,6,4 };
int len = sizeof(a) / sizeof(a0);
insertsort(a, len);
PrintArray(a, len);
return 0;
}
2.1.3复杂度和稳定性
• 直接插⼊排序最好情况为正序,如:要排升序,序列本⾝为升序,则每次不需要挪动数据,数据间⽐较n-1次,则时间复杂度为 O(n)
• 直接插⼊排序最坏情况为逆序,如:要排升序,序列本⾝为降序,则挪动数据的次数是
⼀个1⾄n-1等差数列,则时间复杂度为 O(n2 )
• 直接插⼊排序只需要常数个辅助变量,空间复杂度为 O(1)
• 从上述结论可⻅直接插⼊排序⽐较挑序列的原始顺序,当序列顺序有序或接近顺序有序时,效率较⾼。
• 直接插⼊排序只要end+1位置的值插⼊到0,end序列⽐较时,相等则插⼊到相等值的后⾯位置则可以保持稳定性。
2.2折半插入排序
• 下⼀个要插⼊的元素是key,key前⾯的 0,end 为有序区间。直接插⼊排序是从后往前逐个⽐
较,如果key⾮常⼩,则⽐较次数较多;如果使⽤⼆分查找在前⾯有序区间确认待插⼊元素key的位
置,则可以减少元素间的⽐较次数,这种⽅式叫做折半插⼊排序。
• 折半插⼊排序在逆序时减少了元素的⽐较次数,但是并没有减少挪动数据的次数;其次在正序时,⽐较次数反⽽会变多,挪动数据的次数不变;总之整体时间复杂度依旧是 ,所以折半插⼊排
序的优化效果有限,我们了解⼀下即可。
2.2.1代码实现
void InsertSortmid(int* a, int n) {
int mid,right,left;
for (int end = 0; end < n-1; end++) {
int tmp = aend + 1;
right = end; left = 0;
while (left <= right) {
mid = (right + left) / 2;
if (tmp < amid) {
right = mid - 1;
}
else {
left = mid + 1;
}
}
for (int j = end; j >= left; j--) {
aj + 1 = aj;
}
aright + 1 = tmp;
}
}
2.2.2折半插入排序过程

2.3希尔排序
• 希尔排序是直接插⼊排序的⼀种改进,⼜称缩⼩增量排序,是1959年由Donald Shell提出来的。
希尔排序的本质是先通过预排序(d>1)让待排序序列接近有序,最后再通过直接插⼊排序
(d==1)快速让序列有序。
• 假设d==3时,随机数组进⾏预排序过程展⽰

假设d==1时,所有数组看做一组,就是直接从插入排序

• 间距为d的单趟插⼊代码实现
// 0,end区间间距为d的数据有序,将end+d位置插⼊d间距组数据
void ShellSort(int* a, int n) {
d=3;
for (int end = 0; end < n - d; end++) {
int tmp = aend + d;
while (aend > tmp&&end > 0 ) {
aend + d = aend;
end -= d;
}
aend + d = tmp;
}
}
2.3.1代码实现
• Shell提出的缩⼩增量⽅案为d = d/2;
• Knuth提出的缩⼩增量⽅案为d = d/3+1;
void ShellSort(int* a, int n,int d) {
while (d > 1) {
d = d / 3 + 1;
for (int end = 0; end < n - d; end++) {
int tmp = aend + d;
while (aend > tmp&&end > 0 ) {
aend + d = aend;
end -= d;
}
aend + d = tmp;
}
}
}
2.3.2复杂度和稳定性
• 希尔排序的时间复杂度和稳定性,相⽐于直接插⼊排序,要复杂得多。因为它依赖于"增量序列"
的选择,并且涉及⼀些尚未完全解决的数学问题
对于希尔排序的时间复杂度:我们取决于增量序列的选择,在优良增量序列下可达到 O(n1.3 ) ⾄ O(n1.5 ) 之间。
• 希尔排序只需要常数个辅助变量,空间复杂度为 O(1)
• 希尔排序⽆法保持稳定性,相同的值预排时可能分到不同的组,不同组排序,则相对顺序就乱了
3.选择排序
3.1简单选择排序
3.1.1思想及其过程
每⼀趟在待排序的 n− i+ 1(i= 1, 2, ..., n− 1) 个元素中找关键码最⼩或最⼤的元素,作为有序序列
的第i个元素,等到第 n− 1 趟排完之后,待排序区间中就剩⼀个元素就⽆需再选,区间就有序了

3.1.2代码实现
void SelectSort(int* a, int n) {
for (int i = 0; i < n - 1; i++) {
//每次将第一个元素看出最小值
int min = i;
for (int j = i + 1; j < n; j++) {
//将符号换成小于号就是单调递减
if (amin >aj) {
min = j;
}
}
//如果i==min,说明第一个元素是最小值,不用交换,反之则交换
if (i != min) {
int tmp = amin;
amin = ai;
ai = tmp;
}
}
}
3.1.3复杂度和稳定性
• ⽆论待排序序列为正序、逆序、乱序,每趟都需要去⽐较选出最⼩数据,累积⽐较次数为1⾄n-1的等差数列,所以简单选择排序时间复杂度为 O(n2 )
• 简单选择排序只需要常数个辅助变量,空间复杂度为 O(1)
• 简单选择排序⽆法保持稳定性,如{5,6,5,0,2,0,3}我们可以遍历⼀遍选出第⼀个0交换到最左边位
置,保持0的相对顺序,但是在交换过程中5的相对顺序就被破坏了,所以简单选择排序⽆法保持稳
定性
3.2堆排序
• 堆排序也是⼀种选择排序,简单选择排序每次使⽤暴⼒遍历来选出最⼩或者最⼤的数,⽽堆排序则数组逻辑上看做完全⼆叉树,建堆/调堆后来选出最⼤或最⼤的数实现排序.
具体的可以看我数据结构堆这篇文章
3.2.1复杂度和稳定性
• 堆排序优势是通过建堆/调堆选数效率极⾼,时间复杂度为 O(nlogn) 。
• 堆排序只需要常数个辅助变量,空间复杂度为 O(1) 。
• 堆排序⽆法保持稳定性,如{2,1,2,2}建堆后排序,把堆顶的数据换到最后位置,则相对顺序就乱
了
3.2.2代码实现
void Swap(int* x, int* y) {
assert(x != NULL && y != NULL);
int tmp = *x;
*x = *y;
*y = tmp;
}
void Adjustdown(int* a, int parent, int n) {
int child = parent * 2 + 1;
while (child < n) {
//如果右孩子大于(或者小于)左孩子,则孩子更改为右孩子
if (achild + 1 > achild && child + 1 < n) {
child += 1;
}
if (achild>aparent) {
Swap(&aparent, &achild);
parent = child;
child = parent * 2 + 1;
}else{
break;
}
}
}
void HeapSort(int* a, int n) {
//从最后一个分支结点开始建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
Adjustdown(a, i, n);
}
for (int i = 0; i < n-1; i++) {
//n是数组的长度,将第一个和最后一个交换位置
Swap(&a0, &an - i-1);
//数组从第一个到倒数第n-i-1个位置开始按照大堆排序
Adjustdown(a,0,n-i-1);
}
}
4.交换排序
4.1冒泡排序(起泡排序)
4.1.1思想及其过程
• 单趟冒泡:假设排升序,冒泡的过程是从前往后(或从后往前)两两⽐较相邻元素的值,如果逆序(即A*i* > A*i*+ 1 )则交换,直到序列中相邻元素⽐较交换完成,称为⼀趟冒泡。

• 整体冒泡:假设排升序,⼀趟冒泡结束后,会将最⼤元素放在区间最后,即最⼤元素已经排列好。 下⼀趟冒泡可以对除最⼤元素外的⼦序列从前往后继续冒泡,结束后次⼤元素就可以排列好。以此类推,共 趟冒泡后序列就排好了。注意最后⼀趟区间中只有⼀个元素,本趟冒泡可以不需要,因此是n-1趟冒泡过程。

4.1.2代码实现
void BubbleSort(int* a, int n) {
for (int i = 0; i < n - 1; i++) {
int flag = 0;
for (int j = 0; j < n - 1 - i; j++) {
if (aj > aj + 1) {
Swap(&aj, &aj + 1);
flag = 1;
}
}
//排序过程中,没有交换说明数据有序,
if (flag == 0)
break;
}
}
4.1.3复杂度和稳定性
• 最坏情况下,序列逆序,累积⽐较次数为1⾄n-1的等差数列,所以冒泡排序时间复杂度为O(n2 )
• 最好情况下,序列正序,⽐较n-1次,没有发⽣交换,时间复杂度为 O(n) 。
• 冒泡排序只需要常数个辅助变量,空间复杂度为 O(1)
• 冒泡排序⽐较时,前⼀个元素等于后⼀个元素时不交换,则可以保持稳定性。
4.2快速排序
4.2.1思想及其过程
快速排序由C.A.R.Hoare在1960年提出的,是基于分治思想。它是在待排序区间中任取⼀个元素作
为枢轴(pivot,或称基准值,通常选取区间⾸元素),然后按照基准值⼤⼩将区间中元素分割成左右
两部分,左侧区间中元素⼩于等于基准值,右侧区间中元素⼤于等于基准值。

4.2.2代码实现
int Partition(int* a, int low, int high) {
int pivot = alow;
while (low < high) {
while (ahigh >= pivot && low < high) {
high--;
}
alow = ahigh;
while (alow <= pivot && low < high) {
low++;
}
ahigh = alow;
}
alow = pivot;
return low;
}
void QuickSort(int* a, int low,int high) {
if (low >= high)
return;
int partition = Partition(a, low,high);
QuickSort(a, low,partition-1);
QuickSort(a, partition+1,high);
}
4.2.3复杂度和稳定性
• 快速排序效率取决于基准值的选取,如果每次选择的基准值恰巧都能将区间中元素分割成左右⼏乎相等的两部分,快排的效率会达到最优,整个递归过程展开近似⼀颗平衡⼆叉树。整体⽽⾔对每⼀层序列进⾏划分为O(n ) ,递归的深度为log2n ,因此最好情况时间复杂度为O(nlogn ) ,空间复杂度O(logn)
5.归并排序
5.1思想及过程
• 归并的含义是将两个或两个以上的有序表合并成⼀个新的有序表。链表归并可以直接取结点下来归并,数组归并则必须结束⼀个额外的数组空间归并,归并后再拷⻉回去。

• 如果⼀个数组分成两个⼦数组,⼦数组有序了,两个⼦数组归并⼀下,整体就有序了。那么如何让⼦数组有序呢,我们类似快排使⽤分治的思想即可,不断递归划分数组,直到⼦数组为1个值的区间时返回,递归回退时进⾏归并。

5.2代码实现
void _MergeSort(int* a, int begin, int end, int* tmp) {
if (begin >= end)
return;
int mid = (begin + end) / 2;
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
int begin1 = begin, begin2 = mid + 1;
int end1 = mid, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2) {
if (abegin1 < abegin2) {
tmpi++ = abegin1++;
}
else {
tmpi++ = abegin2++;
}
}
while (begin1 <= end1) {
tmpi++ = abegin1++;
}
while (begin2 <= end2) {
tmpi++ = abegin2++;
}
for (int i = begin; i <= end; i++) {
ai = tmpi;
}
}
void MergeSort(int* a, int n) {
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL) {
printf("malloc fail\n");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
}
5.3复杂度和稳定性
• 归并的过程两个有序⼦数组的时间复杂度为 O(n)
因为他划分是每次都是均分,且数组数据分布不会影响他归并的复杂度,所以归并排序整体累计
log2n层,每层归并合计 O(n) ,则归并排序整体时间复杂度为 O(nlogn) 。
6.计数排序
6.1思想及其过程
计数排序是⼀种⾮⽐较型的整数排序算法。它的核⼼思想不是通过"⽐较"元素的⼤⼩来决定顺序,
⽽是通过统计每个元素出现的次数,直接计算出该元素在最终有序数组中的位置。

6.2代码实现
void CountSort(int* a, int n) {
int max = a0, min = a0;
for (int i = 1; i < n; i++) {
if (max < ai)
max = ai;
if (min > ai)
min = ai;
}
int range = max - min + 1;
int* countarr = (int*)calloc(sizeof(int),range);
if (countarr == NULL) {
printf("malloc fail\n");
exit(-1);
}
for (int i = 0; i < n; i++) {
countarra\[i - min]++;
}
int j = 0;
for (int i = 0; i <range; i++) {
while (countarri--) {
aj++ =min+i;
}
}
free(countarr);
}
但是这个方法只能够排序整数