深入浅出理解冒泡、插入排序和归并、快速排序递归调用过程

一:冒泡排序

1.1.概念

冒泡排序(Bubble Sort)是一种简单直观的原地比较类排序算法 ,核心思想是通过相邻元素的两两比较与交换,让较大的元素像 "气泡" 一样逐步 "上浮" 到数组的末端(或让较小元素 "下沉" 到前端),重复该过程直到整个数组有序。

1.2.核心原理

  1. 比较逻辑:从数组起始位置开始,依次比较相邻的两个元素(第 1 和第 2、第 2 和第 3、......、第 n-1 和第 n 个);
  2. 交换规则:若相邻元素顺序错误(比如 "前大后小",目标是升序排列),则交换这两个元素的位置;
  3. 轮次迭代 :每完成一轮遍历,数组中未排序部分的最大元素会被 "冒泡" 到未排序部分的末尾(成为已排序部分的起始);
  4. 终止条件 :当某一轮遍历中没有发生任何交换,说明数组已完全有序,排序结束(优化点)。

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. 分区逻辑:初始时,数组第 1 个元素(索引 0)作为 "已排序部分",其余元素为 "未排序部分";
  2. 取待插入元素 :从 "未排序部分" 依次取出第一个元素(记为temp),暂存起来(避免后续移位覆盖);
  3. 查找插入位置 :在 "已排序部分" 中,从后往前(或从前往后)比较,找到temp应该插入的位置(满足 "已排序部分" 仍有序);
  4. 元素移位 :将 "已排序部分" 中大于temp(升序场景)的元素,统一向后移动一位,腾出插入空间;
  5. 插入元素 :将temp放入腾出的位置,完成一次插入;
  6. 迭代终止:重复步骤 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>righti>mid),另一个子数组还剩元素(因本身有序,剩余元素都是大值),直接追加到临时数组。

3.7.2.核心价值

将合并阶段的时间复杂度从 O (n²) 优化到 O (n),是归并排序高效的关键。

3.8.递归调用过程

3.8.1.第 1 层递归(入口层):处理 L0-R3(整个数组)

  1. 调用 MSort(array, tempArray, 0, 3)
  2. 判断 0 < 3(成立),计算 mid = 0 + (3-0)/2 = 1
  3. 先递归拆分「左子数组」:调用 MSort(array, tempArray, 0, 1)(左半区 L0-R1[3,8]);
  4. 暂停当前层,进入「左子数组的递归」(第 2 层)。
3.8.2.第 2 层递归:处理 L0-R1(左子数组 [3,8]
  1. 调用 MSort(array, tempArray, 0, 1)
  2. 判断 0 < 1(成立),计算 mid = 0 + (1-0)/2 = 0
  3. 先递归拆分「左子数组的左半区」:调用 MSort(array, tempArray, 0, 0)L0-R0[3]);
  4. 暂停当前层,进入「左子数组的左半区递归」(第 3 层)。

3.8.3.第 3 层递归:处理 L0-R0(单个元素 [3]

  1. 调用 MSort(array, tempArray, 0, 0)
  2. 判断 0 < 0(不成立),触发递归终止条件,直接返回上一层(第 2 层)。

3.8.4.回到第 2 层递归:继续拆分「左子数组的右半区」

  1. 第 3 层返回后,第 2 层继续执行:递归拆分「右子数组」:调用 MSort(array, tempArray, 1, 1)L1-R1[8]);
  2. 进入「左子数组的右半区递归」(第 3 层)。

3.8.5.第 3 层递归:处理 L1-R1(单个元素 [8]

  1. 调用 MSort(array, tempArray, 1, 1)
  2. 判断 1 < 1(不成立),递归终止,返回上一层(第 2 层)。

3.8.6.回到第 2 层递归:合并 L0-R0L1-R1

  1. 第 3 层两次递归(左、右半区)都返回后,第 2 层执行「合并」:调用 _merge(array, tempArray, 0, 0, 1)
  2. 合并逻辑(双指针操作):将两个单个元素 [3][8] 合并为 [3,8](临时数组过渡后,写回原数组);
  3. 第 2 层递归执行完毕,返回上一层(第 1 层)。

3.8.7.回到第 1 层递归:拆分「右子数组」L2-R3[7,5]

  1. 第 2 层返回后,第 1 层继续执行:递归拆分「右子数组」:调用 MSort(array, tempArray, 2, 3)(右半区 L2-R3[7,5]);
  2. 暂停当前层,进入「右子数组的递归」(第 2 层)。

3.8.8.第 2 层递归:处理 L2-R3(右子数组 [7,5]

  1. 调用 MSort(array, tempArray, 2, 3)
  2. 判断 2 < 3(成立),计算 mid = 2 + (3-2)/2 = 2
  3. 先递归拆分「右子数组的左半区」:调用 MSort(array, tempArray, 2, 2)L2-R2[7]);
  4. 暂停当前层,进入「右子数组的左半区递归」(第 3 层)。

3.8.9.第 3 层递归:处理 L2-R2(单个元素 [7]

  1. 调用 MSort(array, tempArray, 2, 2)
  2. 判断 2 < 2(不成立),递归终止,返回上一层(第 2 层)。

3.8.10.回到第 2 层递归:继续拆分「右子数组的右半区」

  1. 第 3 层返回后,第 2 层继续执行:递归拆分「右子数组的右半区」:调用 MSort(array, tempArray, 3, 3)L3-R3[5]);
  2. 进入「右子数组的右半区递归」(第 3 层)。

3.8.11.第 3 层递归:处理 L3-R3(单个元素 [5]

  1. 调用 MSort(array, tempArray, 3, 3)
  2. 判断 3 < 3(不成立),递归终止,返回上一层(第 2 层)。

3.8.12.回到第 2 层递归:合并 L2-R2L3-R3

  1. 第 3 层两次递归都返回后,第 2 层执行「合并」:调用 _merge(array, tempArray, 2, 2, 3)
  2. 合并逻辑:将两个单个元素 [7][5] 按大小排序,合并为 [5,7](写回原数组,此时原数组变为 [3,8,5,7]);
  3. 第 2 层递归执行完毕,返回上一层(第 1 层)。

3.8.13.回到第 1 层递归:合并最终两个有序子数组

  1. 第 2 层的两个子数组(L0-R1=[3,8]L2-R3=[5,7])都已拆分 + 合并为有序数组;
  2. 第 1 层执行最终「合并」:调用 _merge(array, tempArray, 0, 1, 3)
  3. 合并逻辑(双指针核心操作):将 [3,8][5,7] 合并为 [3,5,7,8](写回原数组);
  4. 第 1 层递归执行完毕,整个递归过程结束。

四:快速排序

4.1.概念

快速排序是基于「分治思想」的经典排序算法,核心逻辑是 「选基准→分区→递归排序」,以「原地排序」为主要特点,平均时间复杂度 O (n log n),是实际开发中应用最广泛的排序算法之一。

4.2.核心思想(分治思想)

快速排序的核心是「将大问题拆分为小问题,逐个解决后合并」,具体分为 3 步:

  1. 选基准(Pivot):从数组中选一个元素作为「基准」(比如数组第一个元素、最后一个元素、中间元素或随机元素);
  2. 分区(Partition) :重新排列数组,将所有 小于基准 的元素放到基准左边,所有 大于基准 的元素放到基准右边(等于基准的元素可左可右),最终基准元素会落在「它的最终排序位置」;
  3. 递归排序:对基准左边的子数组和右边的子数组,重复「选基准→分区」步骤,直到子数组长度为 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=0high=3

4.6.2.第一层递归:quickSort(array, 0, 3)

1. 分区操作(核心:找到基准 3 的最终位置)

  • 基准值 key = array[0] = 3,初始化 low=0high=3
  • 高位扫描 (从右往左找 < key 的元素):
    • array[3] = 5 ≥ 3high=2
    • array[2] = 7 ≥ 3high=1
    • array[1] = 8 ≥ 3high=0
    • 此时 low=high=0,高位扫描停止;
  • 低位扫描 (从左往右找 > key 的元素):
    • array[0] = 3 ≤ 3low=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=1high=3
  • 高位扫描 (从右往左找 < key 的元素):
    • array[3] = 5 < 8,高位扫描停止;
    • array[3] = 5 赋值给 array[1],数组变为 [3, 5, 7, 5]
    • 此时 low=1(待低位扫描);
  • 低位扫描 (从左往右找 > key 的元素):
    • array[1] = 5 ≤ 8low=2
    • array[2] = 7 ≤ 8low=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=1high=2
  • 高位扫描 (从右往左找 < key 的元素):
    • array[2] = 7 ≥ 5high=1
    • 此时 low=high=1,高位扫描停止;
  • 低位扫描 (从左往右找 > key 的元素):
    • array[1] = 5 ≤ 5low=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]
相关推荐
czlczl200209252 小时前
算法:二叉搜索树的最近公共祖先
算法
司铭鸿2 小时前
祖先关系的数学重构:从家谱到算法的思维跃迁
开发语言·数据结构·人工智能·算法·重构·c#·哈希算法
yk0820..2 小时前
测试用例的八大核心要素
数据结构
SoleMotive.3 小时前
redis实现漏桶算法--https://blog.csdn.net/m0_74908430/article/details/155076710
redis·算法·junit
-森屿安年-3 小时前
LeetCode 283. 移动零
开发语言·c++·算法·leetcode
北京地铁1号线3 小时前
数据结构:堆
java·数据结构·算法
得物技术3 小时前
从数字到版面:得物数据产品里数字格式化的那些事
前端·数据结构·数据分析
散峰而望3 小时前
C++数组(一)(算法竞赛)
c语言·开发语言·c++·算法·github
自然常数e4 小时前
深入理解指针(1)
c语言·算法·visual studio