1.插入排序
1.1 核心思想:
把数组分成有序区间+无需区间两个部分,
假设位置0,end的值都是按升序排列的,把end+1位置的值保存到变量tmp里,end位置的值大于tmp就往后挪一位,end也走到end-1,一直比较直到成功插入该数,然后进行下一轮比较。

先写单趟排序的过程,再写整体。
1.2 代码:

1.3 复杂度
**·**时间复杂度:O(N^2)
逆序的情况下最坏:O(N^2)
顺序的情况下最好:O(N))
**·**空间复杂度:O(1),原地排序,只开临时变量,不额外开辟数组
2.冒泡排序
2.1 核心思想:
重复遍历数组相邻两个元素,逆序则交换;每一轮外层循环,会把当前无序区间里最大的数"冒泡"到区间末尾。
**优化点:**设置flag标记,如果一整轮遍历没有发生任何交换,代表数组已经完全有序,直接提前跳出循环,不用继续遍历。
2.2 代码:

2.3 复杂度
**·**时间复杂度:O(N^2)
**·**空间复杂度:O(1)
3.希尔排序
3.1 核心思想:
希尔排序是插入排序的优化版本,分为两个阶段:
**1.预排序:**设置间隔gap,将数组分为gap组,每组内部做插入排序,让数组接近有序。
**2.插入排序:**间隔缩小为1时,等价于标准插入排序
示例:

先只排红色这一组:

核心疑问:i 的范围为什么在0到n-gap之间?

如上图,数组下标最大为n-1,若 i >= n-gap,i + gap >= n,ai+gap就越界了。
接下来把绿色与紫色组的考虑进去,
写法一:分组依次遍历

写法二:多组依次遍历

gap缩小公式说明:gap = gap/3 + 1
-
除以3:快速缩小间隔
-
+1兜底,防止gap变为0
3.2 代码:

3.3 复杂度
**·**时间复杂度:O(N^1.3)
4.双向选择排序
4.1 核心思想:
双向选择排序每一趟同时找出区间begin, end的最小值、最大值,分别交换到区间左右两端:
-
遍历区间,记录最小值下标mini、最大值下标maxi
-
最小值交换到begin位置,最大值交换到end位置
-
begin++、end--,缩小区间,循环直到begin >= end

写出代码如下:

但是经过测试,我们发现:

并没有按照我们预想的成功排好序。
出错场景:最大值刚好在begin下标

第一轮begin=0,区间最小值下标mini=1,最大值下标maxi=0
1.执行Swap(&a0,&a1)后,下标为0的最大值9,被换到了mini=1的位置,但maxi仍然为0。
2.再执行Swap(&aend,&amaxi),把1交换到了末尾,而最大值9遗留在中间,排序出现错乱。
4.2 正确代码如下:

4.3 复杂度
**·**时间复杂度:O(N^2)
5.快排
5.1 核心思想:
分治二分
快速排序是经典分治类排序算法,核心逻辑:
-
在区间内选定一个key,一趟遍历将数组分割为两部分:左侧全部 ≤ key,右侧全部 ≥ key;
-
递归处理左子区间**left, keyi-1** 、右子区间**keyi+1, right**;
-
当区间长度≤1时天然有序,递归终止。
5.2 Hoare分区法(基础原生快排)
5.2.1 分区原理
我们默认把区间最左元素设为key,左指针L从左向右找大于key的数,右指针R从右向左找小于key的数:
• 行走规则:R先动,L后动,两指针相遇时停下
• 相遇位置性质保证:相遇处的值一定 ≤ key
-
场景1:R主动停下------R找到小于key的值,L全程未找到大数,直接和R相遇,R停留值天然小于key;
-
场景2:R走完整个区间无小数,直接与L碰头------上一轮交换已经把小于key的元素换到L侧。
• 收尾操作:两指针相遇后,交换key与相遇位置的值,完成区间分割。

补充:将key设为最右侧,需要改为L先走,R后走,相遇位置的值会恒大于key
代码如下:

易错点:内层循环必须重复添加"begin<end"判断:外层循环的初始检查,替代不了循环中持续变化的变量校验,如果漏写,begin与end可能会一直走,造成越界。
接下来只需要让key的左区间与右区间有序即可。我们可以用递归调用来实现,具体过程如下图:

5.2.2 代码:

时间复杂度:O(N)=N*logN
5.3 原生快排的缺陷与优化
5.3.1 两大风险
1.当数组本身有序时,递归深度极大有可能会出现栈溢出的情况。
2.当数组有序时,这个算法就会退化成O(N^2)的算法,如下图:

5.3.2 优化1:三数取中
所以为了避免有序情况下造成的效率退化,我们可以用到以下两种方法:
1.随机取key
2.三数取中(重点讲解)
三数取中即在一组数最左边,最右边以及中间的数中取一个不大不小的中间值作key。代码如下:

使用方法:分区函数开头获取中位数下标,交换至left位置,再执行Hoare逻辑
5.3.3 优化2:小区间插入排序
由于递归调用本身存在不小的性能损耗,我们可以对长度较短的小区间放弃递归,改用插入排序提升整体效率。
原理:假设快排每次分割都是二分,整个递归过程可以等效为满二叉树;二叉树最后一层的递归调用次数占总次数50%,倒数第二层占25%。直接去掉底层小规模区间的递归,能明显降低开销。
小区间阈值通常取长度10,代码示例:

5.4前后指针分区法
5.4.1 原理

设定最左侧为key,初始状态下prev与cur指向同一起点,cur向后遍历,找到小于key的值时prev就++,然后交换prev处与cur处的值,循环这个过程直到cur遍历到最后,交换prev处与key的值。
5.4.2 代码:

5.5 整合优化版递归快排代码
cs
void QuickSort(int* a,int left,int right)
{
if (left >= right)
return;
int keyi=PartSort2(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
int PartSort1(int* a, int left, int right)
{
//小区间优化
if (right - left + 1 < 10)
{
InsertSort(a + left, right - left + 1);
}
else
{
//三数取中
int mid = GetMidi(a, left, right);
Swap(&mid, &a[left]);
int begin = left, end = right;
int keyi = left;
while (begin < end)
{
while (begin < end && a[begin] <= a[keyi])
{
++begin;
}
while (begin < end && a[end] >= a[keyi])
{
--end;
}
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[keyi]);
return begin;
}
}
int PartSort2(int* a, int left, int right)
{
//三数取中
int mid = GetMidi(a, left, right);
Swap(&mid, &a[left]);
int prev = left;
int cur = left+1;
int keyi = left;
while (cur <= right)
{
if (a[cur] < a[keyi])
{
prev++;
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
5.6 非递归快排(栈模拟递归,解决栈溢出)
5.6.1 实现思路
递归快排依赖程序调用栈,有序大数据易栈溢出;使用自定义栈数据结构存储待排序区间下标,手动模拟递归二分流程。
优势:自定义栈在堆区申请内存,空间上限远大于程序栈,不会栈溢出。

如上图,右左子区间入栈,每次取栈顶区间,单趟排序。循环一次,相当于一次递归。
5.6.2 代码如下:

6.归并排序
6.1 核心思想:
分治思想,整体分为两个阶段:
1.分:递归拆分
不断将数组区间对半切割,直到每个子区间仅仅存在单个元素(单个元素天然有序)
2.治:有序归并
将两段已经排好序的子数组,借助临时数组tmp,按从小到大的顺序合并为一段完整有序区间,最后拷贝回原数组。

如上图,不断对比abegin1和abegin2,把更小的值存入临时数组tmp,对应下标向后移动,直至其中一段全部遍历完毕。
若某一段提前遍历完成,直接把另一段剩余元素全部追加至tmp尾部,最后将tmp内当前区间有序数据覆盖写回原数组。
6.2 代码:
cs
void _MergeSort(int* a, int* tmp, int begin, int end)
{
if (begin == end)
return;
int mid = (begin + end) / 2;
//[begin,mid][mid+1,end]
_MergeSort(a, tmp, begin, mid);
_MergeSort(a, tmp, mid+1, end);
//归并
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* a, int n)
{
int* tmp = malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc");
return;
}
_MergeSort(a, tmp, 0, n-1);
free(tmp);
tmp = NULL;
}
易错点:
1.memcpy容易漏掉+begin
2.为什么分割区间不这样分割:begin,mid-1mid,end?

根据上图分析可得:这样分割区间会造成区间重复递归,死循环
6.3 复杂度
**·**时间复杂度:O(N*logN)
**·**空间复杂度:O(N)
6.4 非递归实现归并排序
递归版归并排序需要借助函数调用栈完成区间拆分与回溯,栈操作逻辑抽象、不易理解;因此我们改用循环迭代的方式实现归并排序,思路直观清晰。
gap:代表每组归并区间的数据个数
• 初始 gap = 1:单个元素天然有序,执行11归并;
• 每轮循环结束执行 gap *= 2,子区间长度翻倍;
• 循环条件:gap < n,当子区间长度超过数组总长度时整体数组完成有序合并。

分组遍历逻辑:
for(int i=0;i<n;i+=2*gap)
i 代表每组归并区间的起始位置,每组包含两段长度为gap的有序空间,总长度2*gap。
区间划分公式:

基础代码框架如下:
cs
void MergeNonRSort(int* a,int n)
{
int* tmp = malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc");
return;
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int j = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
缺陷:上述代码在数组长度不能被2*gap整除时,会产生下标越界访问的问题。
我们添加打印语句,输出每一组区间的下标,直观观察越界场景:

测试数组:{6,1,2,7,9,3,4,5,10,8},数组长度n=10,下标范围0-9
输出结果如下:

红色标注的区间即为越界下标,一共分为两类异常场景,针对两种越界场景,我们在计算完区间下标后立刻添加两段边界校验:

7. 计数排序
7.1 核心思想:
开辟一块统计数组count,下标对应数组元素,count数值存储该数字出现次数,最后遍历count数组,按下标顺序还原有序序列。
示例演算:

**基础版缺陷:若数组数值跨度极大,最小值远大于0,会造成大量内存浪费。**例:

优化:偏移映射
1.遍历数组,找到最小值,最大值
2.实际开辟长度range=max-min+1的count数组
3.counta\[i-min]++,把原始数字减去最小值,映射从0开始的连续下标
4.读取还原:aj++=i+min,count下标i加上偏移量min,还原原始数值
7.2 代码:
cs
void CountSort(int* a, int n)
{
int min = a[0], max = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] < min)
min = a[i];
if (a[i] > max)
max = a[i];
}
int range = max - min + 1;
int* count = calloc(range, sizeof(int));
if (count == NULL)
{
perror("calloc");
return;
}
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
int j = 0;
for (int i = 0; i < range; i++)
{
while (count[i]--)
{
a[j++] = i + min;
}
}
free(count);
}
7.3 复杂度:
**·**时间复杂度:O(N+range)
**·**空间复杂度:O(range)
适用限制:
1.仅支持整数数组,浮点、字符串无法使用
2.仅适合数值分布集中,数值跨度小的数组