一:冒泡排序
1.1.概念
冒泡排序(Bubble Sort)是一种简单直观的原地比较类排序算法 ,核心思想是通过相邻元素的两两比较与交换,让较大的元素像 "气泡" 一样逐步 "上浮" 到数组的末端(或让较小元素 "下沉" 到前端),重复该过程直到整个数组有序。
1.2.核心原理
- 比较逻辑:从数组起始位置开始,依次比较相邻的两个元素(第 1 和第 2、第 2 和第 3、......、第 n-1 和第 n 个);
- 交换规则:若相邻元素顺序错误(比如 "前大后小",目标是升序排列),则交换这两个元素的位置;
- 轮次迭代 :每完成一轮遍历,数组中未排序部分的最大元素会被 "冒泡" 到未排序部分的末尾(成为已排序部分的起始);
- 终止条件 :当某一轮遍历中没有发生任何交换,说明数组已完全有序,排序结束(优化点)。
1.3. 时间复杂度
- 最坏情况(数组完全逆序):需进行 n-1 轮遍历,每轮比较 n-i 次(i 为轮次),总操作次数为 O (n²);
- 最好情况(数组已有序,且优化 "无交换则终止"):只需 1 轮遍历(无交换),时间复杂度为 O (n);
- 平均时间复杂度:O (n²)(适用于小规模数据,大规模数据效率低)。
1.4. 空间复杂度
- 原地排序(仅需额外 1 个临时变量存储交换元素),空间复杂度为 O (1)。
1.5. 适用场景
- 数据量小(如 n<100)、对排序效率要求不高的场景;
- 需保持元素稳定性、且内存受限(要求原地排序)的场景。
1.6.伪代码示例
cpp
//冒泡排序
void bubbleSort(int* array, int len)
{
int i,j,tmp;
for (i = 0; i < len - 1; i++)
{
for (j = 0; j < len - 1 - i; j++)
{
if (array[j] > array[j+1])
{
tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
}
}
}
}
二:插入排序
2.1.概念
插入排序是一种简单直观的原地比较类排序算法,核心思想类比 "整理扑克牌"------ 将数组分为 "已排序部分" 和 "未排序部分",每次从 "未排序部分" 取第一个元素,插入到 "已排序部分" 的合适位置,直到整个数组有序。
核心原理
- 分区逻辑:初始时,数组第 1 个元素(索引 0)作为 "已排序部分",其余元素为 "未排序部分";
- 取待插入元素 :从 "未排序部分" 依次取出第一个元素(记为
temp),暂存起来(避免后续移位覆盖); - 查找插入位置 :在 "已排序部分" 中,从后往前(或从前往后)比较,找到
temp应该插入的位置(满足 "已排序部分" 仍有序); - 元素移位 :将 "已排序部分" 中大于
temp(升序场景)的元素,统一向后移动一位,腾出插入空间; - 插入元素 :将
temp放入腾出的位置,完成一次插入; - 迭代终止:重复步骤 2-5,直到 "未排序部分" 为空,数组完全有序。
2.2. 时间复杂度
- 最坏情况(数组完全逆序):每次插入需遍历整个 "已排序部分" 并移位,总操作次数为 O (n²);
- 最好情况(数组已有序):每次插入只需比较 1 次(无需移位),时间复杂度为 O (n)(适用于 "几乎有序" 的数据);
- 平均时间复杂度:O (n²)(比冒泡排序略高效,因为移位操作比交换操作耗时更少)。
2.3. 空间复杂度
- 原地排序(仅需额外 1 个临时变量存储
temp),空间复杂度为 O (1)。
2.4. 适用场景
- 数据量小(n<1000)或 "几乎有序" 的数据(此时效率接近 O (n));
- 需保持元素稳定性、内存受限(要求原地排序)的场景;
- 在线排序场景(数据流式输入,需边接收边排序,插入排序可实时将新元素插入已排序序列)。
2.5.伪代码示例
cpp
//插入排序
void insertSort(int* array, int len)
{
int i,j,tmp;
for (i = 1; i < len; i++)
{
tmp = array[i];
for (j = i; j > 0 && array[j - 1] > tmp; j--)
{
array[j] = array[j - 1];//元素后移
}
array[j] = tmp;
}
}
三:归并排序
3.1.概念优点
归并排序是一种经典的 分治思想 排序算法,核心逻辑是 "先拆分、后合并"------ 把无序数组不断拆分成子数组,直到每个子数组只有 1 个元素(天然有序),再逐步将相邻的有序子数组合并,最终得到完整的有序数组。
优势是 稳定性强 (相等元素相对位置不变)、时间复杂度稳定为 O(n log n)(无论最好 / 最坏情况),缺点是需要额外的辅助空间(空间复杂度 O (n))。
3.2.核心步骤(以升序为例)
3.2.1. 拆分(分治阶段)
- 把数组从中间分成左右两个子数组;
- 递归拆分左右子数组,直到每个子数组长度为 1(单个元素无需排序)。
3.2.2. 合并(合并阶段)
- 准备一个辅助数组,用于临时存储合并后的有序元素;
- 两个指针分别指向两个待合并的有序子数组的起始位置;
- 比较两个指针指向的元素,把较小的元素放入辅助数组,同时对应指针后移;
- 重复上一步,直到其中一个子数组的元素全部放入辅助数组;
- 把另一个子数组中剩余的元素依次放入辅助数组;
- 最后把辅助数组的元素复制回原数组的对应位置,完成一次合并。
3.3.时间复杂度
拆分阶段每次把数组分成 2 份,共拆分 log₂n 层;合并阶段每一层的元素总处理量是 n,因此总时间 O (n log n)。
3.4.空间复杂度:
合并时需要辅助数组存储所有元素,因此空间 O (n)(递归调用栈的空间是 O (log n),可忽略)。
3.5.适用场景:
适合大数据量排序(O (n log n) 效率优于冒泡、插入排序),或需要稳定排序的场景(如对象排序);缺点是不适用于内存受限的场景(需额外空间)。
3.6.代码示例
cpp
#include <stdio.h>
#include <stdlib.h>
//打印
void printArray(int *array, int len)
{
for (int n = 0; n < len; n++)
{
printf("%d ", array[n]);
}
putchar('\n');
}
// 归并排序
void _merge(int *array, int *tempArray, int left, int mid, int right)
{
int i = left;//标记左半区第一个未排序数组
int j = mid + 1;//标记右半区第一个未排序数组
int k = left;//临时数组下标
// 将小的放到目的地中
while (i <= mid && j <= right)
{
if (array[i] < array[j])
tempArray[k++] = array[i++]; // 左半区第一个剩余元素更小
else
tempArray[k++] = array[j++]; // 右半区第一个剩余元素更小
}
// 合并左伴区剩余元素(有子数组合并速率快如 L[3 4 5] R[1 2 3] 这时把左半区追加到临时数组)
while (i <= mid)
{
tempArray[k++] = array[i++];
}
// 合并右半区剩余元素
while (j <= right)
{
tempArray[k++] = array[j++];
}
// 将临时素组元素中合并后的元素复制到原来的数组
while (left <= right)
{
array[left] = tempArray[left];
left++;
}
}
//归并排序
void MSort(int *array, int *tempArray, int left, int right)
{
//只有一个元素时不需要划分,一个元素本身有序
if (left < right)
{
int mid = left + (right-left) / 2;
//划分左半区
MSort(array, tempArray, left, mid);
//划分右半区
MSort(array, tempArray, mid + 1, right);
//合并
_merge(array, tempArray, left, mid, right);
}
}
// 归并排序入口
void mergeSort(int *array, int len)
{
// 分配辅助数组
int *tempArray = (int *)malloc(sizeof(int) * len);
if (tempArray)
{
MSort(array, tempArray, 0, len - 1);
free(tempArray);
}
else
{
printf(" error: malloc failed !!\n");
}
}
int main()
{
int array[] = {3,8,7,5};
int len = (int)sizeof(array) / sizeof(*array);
printArray(array, len);
mergeSort(array, len);
printf("\n");
printArray(array, len);
printf("\n");
system("pause");
return 0;
}
3.7.解析双指针的应用
3.7.1.逻辑
双指针「并行遍历」两个有序子数组,每次只比较当前指针指向的元素,选更小的写入临时数组,然后对应指针和写入指针一起右移 ------ 这样无需重复比较,仅需「线性遍历一次」就能完成合并,时间复杂度是 O (n)(n 为两个子数组总长度)。
当其中一个子数组遍历完(比如 j>right 或 i>mid),另一个子数组还剩元素(因本身有序,剩余元素都是大值),直接追加到临时数组。
3.7.2.核心价值
将合并阶段的时间复杂度从 O (n²) 优化到 O (n),是归并排序高效的关键。
3.8.递归调用过程
3.8.1.第 1 层递归(入口层):处理 L0-R3(整个数组)
- 调用
MSort(array, tempArray, 0, 3); - 判断
0 < 3(成立),计算mid = 0 + (3-0)/2 = 1; - 先递归拆分「左子数组」:调用
MSort(array, tempArray, 0, 1)(左半区L0-R1:[3,8]); - 暂停当前层,进入「左子数组的递归」(第 2 层)。
3.8.2.第 2 层递归:处理 L0-R1(左子数组 [3,8])
- 调用
MSort(array, tempArray, 0, 1); - 判断
0 < 1(成立),计算mid = 0 + (1-0)/2 = 0; - 先递归拆分「左子数组的左半区」:调用
MSort(array, tempArray, 0, 0)(L0-R0:[3]); - 暂停当前层,进入「左子数组的左半区递归」(第 3 层)。
3.8.3.第 3 层递归:处理 L0-R0(单个元素 [3])
- 调用
MSort(array, tempArray, 0, 0); - 判断
0 < 0(不成立),触发递归终止条件,直接返回上一层(第 2 层)。
3.8.4.回到第 2 层递归:继续拆分「左子数组的右半区」
- 第 3 层返回后,第 2 层继续执行:递归拆分「右子数组」:调用
MSort(array, tempArray, 1, 1)(L1-R1:[8]); - 进入「左子数组的右半区递归」(第 3 层)。
3.8.5.第 3 层递归:处理 L1-R1(单个元素 [8])
- 调用
MSort(array, tempArray, 1, 1); - 判断
1 < 1(不成立),递归终止,返回上一层(第 2 层)。
3.8.6.回到第 2 层递归:合并 L0-R0 和 L1-R1
- 第 3 层两次递归(左、右半区)都返回后,第 2 层执行「合并」:调用
_merge(array, tempArray, 0, 0, 1); - 合并逻辑(双指针操作):将两个单个元素
[3]和[8]合并为[3,8](临时数组过渡后,写回原数组); - 第 2 层递归执行完毕,返回上一层(第 1 层)。
3.8.7.回到第 1 层递归:拆分「右子数组」L2-R3([7,5])
- 第 2 层返回后,第 1 层继续执行:递归拆分「右子数组」:调用
MSort(array, tempArray, 2, 3)(右半区L2-R3:[7,5]); - 暂停当前层,进入「右子数组的递归」(第 2 层)。
3.8.8.第 2 层递归:处理 L2-R3(右子数组 [7,5])
- 调用
MSort(array, tempArray, 2, 3); - 判断
2 < 3(成立),计算mid = 2 + (3-2)/2 = 2; - 先递归拆分「右子数组的左半区」:调用
MSort(array, tempArray, 2, 2)(L2-R2:[7]); - 暂停当前层,进入「右子数组的左半区递归」(第 3 层)。
3.8.9.第 3 层递归:处理 L2-R2(单个元素 [7])
- 调用
MSort(array, tempArray, 2, 2); - 判断
2 < 2(不成立),递归终止,返回上一层(第 2 层)。
3.8.10.回到第 2 层递归:继续拆分「右子数组的右半区」
- 第 3 层返回后,第 2 层继续执行:递归拆分「右子数组的右半区」:调用
MSort(array, tempArray, 3, 3)(L3-R3:[5]); - 进入「右子数组的右半区递归」(第 3 层)。
3.8.11.第 3 层递归:处理 L3-R3(单个元素 [5])
- 调用
MSort(array, tempArray, 3, 3); - 判断
3 < 3(不成立),递归终止,返回上一层(第 2 层)。
3.8.12.回到第 2 层递归:合并 L2-R2 和 L3-R3
- 第 3 层两次递归都返回后,第 2 层执行「合并」:调用
_merge(array, tempArray, 2, 2, 3); - 合并逻辑:将两个单个元素
[7]和[5]按大小排序,合并为[5,7](写回原数组,此时原数组变为[3,8,5,7]); - 第 2 层递归执行完毕,返回上一层(第 1 层)。
3.8.13.回到第 1 层递归:合并最终两个有序子数组
- 第 2 层的两个子数组(
L0-R1=[3,8]和L2-R3=[5,7])都已拆分 + 合并为有序数组; - 第 1 层执行最终「合并」:调用
_merge(array, tempArray, 0, 1, 3); - 合并逻辑(双指针核心操作):将
[3,8]和[5,7]合并为[3,5,7,8](写回原数组); - 第 1 层递归执行完毕,整个递归过程结束。
四:快速排序
4.1.概念
快速排序是基于「分治思想」的经典排序算法,核心逻辑是 「选基准→分区→递归排序」,以「原地排序」为主要特点,平均时间复杂度 O (n log n),是实际开发中应用最广泛的排序算法之一。
4.2.核心思想(分治思想)
快速排序的核心是「将大问题拆分为小问题,逐个解决后合并」,具体分为 3 步:
- 选基准(Pivot):从数组中选一个元素作为「基准」(比如数组第一个元素、最后一个元素、中间元素或随机元素);
- 分区(Partition) :重新排列数组,将所有 小于基准 的元素放到基准左边,所有 大于基准 的元素放到基准右边(等于基准的元素可左可右),最终基准元素会落在「它的最终排序位置」;
- 递归排序:对基准左边的子数组和右边的子数组,重复「选基准→分区」步骤,直到子数组长度为 1(天然有序)或 0(无需排序)。
4.3.时间复杂度
- 平均情况:O (n log n)。每次分区将数组拆分为两个大致相等的子数组,递归层数为 log n,每层分区的时间复杂度为 O (n)(遍历数组);
- 最坏情况:O (n²)。当数组已有序(或逆序),且选择「第一个 / 最后一个元素」为基准时,每次分区只能将数组拆分为「长度为 n-1 的子数组」和「长度为 0 的子数组」,递归层数为 n,每层分区 O (n);
- 优化方案:选择「随机元素」或「三数取中」(左、中、右三个元素的中位数)作为基准,避免最坏情况,实际中几乎能稳定达到 O (n log n)。
4.4. 空间复杂度
- 平均情况:O (log n)。递归调用栈的深度为 log n(对应递归层数);
- 最坏情况:O (n)。递归层数为 n(如有序数组);
- 优化方案:使用「尾递归优化」或「非递归实现」,将空间复杂度优化到 O (log n) 甚至 O (1)(原地排序)。
4.5.代码示例
cpp
#include <stdio.h>
#include <stdlib.h>
void printArray(int* array, int len)
{
for (int n = 0; n < len; n++)
{
printf("%d ", array[n]);
}
}
//快速排序
/*获取基准坐标,并相对有序(左边比基准坐标小,右边比基准坐标大)*/
int getStandard(int* array, int low, int high)
{
int key = array[low];
while (low < high)
{
while (low < high && array[high] >= key)
{
high--;
}
if (low < high)
{
array[low] = array[high];
}
while (low < high && array[low] <= key)
{
low++;
}
if (low < high)
{
array[high] = array[low];
}
}
array[low] = key;
return low;
}
void quickSort(int* array, int low, int high)
{
if (low < high)
{ //递归出口
int standard = getStandard(array, low, high);
quickSort(array, low, standard - 1);
quickSort(array, standard + 1, high);
}
}
int main()
{
int array[] = {3,8,7,5};
int len = (int)sizeof(array) / sizeof(*array);
printArray(array, len);
quickSort(array, 0, len - 1);
printf("\n");
printArray(array, len);
printf("\n");
system("pause");
return 0;
}
4.6.递归调用过程
4.6.1.初始状态
- 数组:
[3, 8, 7, 5] - 数组长度
len=4,初始调用quickSort(array, 0, 3)(low=0,high=3)
4.6.2.第一层递归:quickSort(array, 0, 3)
1. 分区操作(核心:找到基准 3 的最终位置)
- 基准值
key = array[0] = 3,初始化low=0,high=3; - 高位扫描 (从右往左找 < key 的元素):
array[3] = 5 ≥ 3→high=2;array[2] = 7 ≥ 3→high=1;array[1] = 8 ≥ 3→high=0;- 此时
low=high=0,高位扫描停止;
- 低位扫描 (从左往右找 > key 的元素):
array[0] = 3 ≤ 3→low=1;- 此时
low=1 > high=0,低位扫描停止;
- 基准归位 :基准
3的最终位置为standard=0(无需交换,本身已在正确位置); - 分区后数组不变:
[3, 8, 7, 5]。
2. 递归调用(按基准位置拆分左右子数组)
- 左子数组:
quickSort(array, 0, standard-1) = quickSort(0, -1)→low=0 ≥ high=-1,不满足递归条件,直接退出; - 右子数组:
quickSort(array, standard+1, 3) = quickSort(1, 3)→low=1 < high=3,满足条件,进入第二层递归。
4.6.3.第二层递归:quickSort(array, 1, 3)
1. 分区操作(找到基准 8 的最终位置)
- 基准值
key = array[1] = 8,初始化low=1,high=3; - 高位扫描 (从右往左找 < key 的元素):
array[3] = 5 < 8,高位扫描停止;- 将
array[3] = 5赋值给array[1],数组变为[3, 5, 7, 5]; - 此时
low=1(待低位扫描);
- 低位扫描 (从左往右找 > key 的元素):
array[1] = 5 ≤ 8→low=2;array[2] = 7 ≤ 8→low=3;- 此时
low=high=3,低位扫描停止;
- 基准归位 :将基准
8赋值给array[3],数组恢复为[3, 5, 7, 8]; - 基准最终位置
standard=3。
2. 递归调用(按基准位置拆分左右子数组)
- 左子数组:
quickSort(array, 1, standard-1) = quickSort(1, 2)→low=1 < high=2,满足条件,进入第三层递归; - 右子数组:
quickSort(array, standard+1, 3) = quickSort(4, 3)→low=4 ≥ high=3,不满足递归条件,直接退出。
4.6.3.第三层递归:quickSort(array, 1, 2)
1. 分区操作(找到基准 5 的最终位置)
- 基准值
key = array[1] = 5,初始化low=1,high=2; - 高位扫描 (从右往左找 < key 的元素):
array[2] = 7 ≥ 5→high=1;- 此时
low=high=1,高位扫描停止;
- 低位扫描 (从左往右找 > key 的元素):
array[1] = 5 ≤ 5→low=2;- 此时
low=2 > high=1,低位扫描停止;
- 基准归位 :基准
5的最终位置为standard=1(无需交换,本身已在正确位置); - 分区后数组不变:
[3, 5, 7, 8]。
2. 递归调用(按基准位置拆分左右子数组)
- 左子数组:
quickSort(array, 1, standard-1) = quickSort(1, 0)→low=1 ≥ high=0,不满足递归条件,直接退出; - 右子数组:
quickSort(array, standard+1, 2) = quickSort(2, 2)→low=2 ≥ high=2,不满足递归条件,直接退出。
4.6.4.递归终止与最终结果
- 所有递归层执行完毕,无更多满足条件的递归调用;
- 最终排序后数组:
[3, 5, 7, 8]。