当前位置: 首页 > news >正文

算法笔记 09

1 排序

第 11 章   排序 - Hello 算法https://www.hello-algo.com/chapter_sorting/

2 选择排序

选择排序每次循环区间内的最小放在已排序的区间,怎么保证新选出的最小一定大于原来的最小呢,也就是原给出的数组不是递增的】

核心结论很明确:选择排序不用刻意 “保证新最小大于原最小”,而是通过 “已排序区间” 和 “未排序区间” 的严格划分,自然让新选出的最小元素≥已排序区间的最大元素,最终实现整体递增。

1. 先明确选择排序的核心逻辑(以升序为例)

选择排序的本质是 “逐步扩充已排序区间”,每一轮只做两件事:

  • 划分区间:数组分为「已排序区间(左半部分,初始为空)」和「未排序区间(右半部分,初始为整个数组)」。
  • 选最小放前面:在未排序区间中找最小元素,和未排序区间的第一个元素交换,此时这个最小元素就加入已排序区间的末尾。

2. 关键:为什么新选的最小一定≥已排序区间的所有元素?

核心在于「未排序区间的定义」—— 未排序区间是 “还没处理的元素”,而每一轮选的是 “当前未排序区间里的最小”,这就天然满足:

  • 已排序区间的所有元素,都是「之前每一轮从当时的未排序区间里选出来的最小」。
  • 本轮新选的最小,是「剩下的未排序元素里的最小」,必然≥之前所有轮次选出来的最小(因为之前的最小已经被移到已排序区间了,剩下的元素不可能比它们更小)。

举个具体例子(原数组非递增):假设原数组 [5, 3, 8, 1, 2],一步步看:

  1. 初始:已排序区间为空,未排序区间 [5,3,8,1,2]

    • 找未排序最小:1,和未排序第一个元素 5 交换 → 数组变为 [1, 3, 8, 5, 2]
    • 已排序区间变为 [1](此时已排序的最大 / 最小都是 1
  2. 第二轮:未排序区间 [3,8,5,2]

    • 找未排序最小:2,和未排序第一个元素 3 交换 → 数组变为 [1, 2, 8, 5, 3]
    • 已排序区间变为 [1,2](新最小 2 ≥ 已排序最大 1
  3. 第三轮:未排序区间 [8,5,3]

    • 找未排序最小:3,和未排序第一个元素 8 交换 → 数组变为 [1,2,3,5,8]
    • 已排序区间变为 [1,2,3](新最小 3 ≥ 已排序最大 2
  4. 后续轮次以此类推,每轮新选的最小都≥已排序区间的最大元素,最终数组递增。

3. 原数组非递增也不影响的原因

不管原数组是递减([5,4,3,2,1])、无序([3,1,4,2]),甚至有重复元素([2,1,2]),选择排序的 “区间划分 + 选当前未排序最小” 逻辑都成立:

  • 重复元素时:新选的最小可能等于已排序区间的最大元素(比如 [2,1,2] 第二轮选 2,已排序区间是 [1]),但依然满足 “非递减”,不影响最终有序。
  • 递减数组时:每轮选的最小都是未排序区间的最后一个元素,交换后逐步把最小元素移到前面,最终变成递增。

总结

选择排序的核心是 “每轮只关注当前未排序区间的最小”,通过 “交换到已排序区间末尾” 的操作,天然保证已排序区间始终是有序的(递增)。不用刻意比较 “新最小” 和 “原最小”,因为区间划分已经决定了 “剩下的元素里的最小,不可能比已经选走的最小更小”。

cpp

选择排序(升序)完整实现(C++ 代码)

#include <iostream>
using namespace std;// 选择排序函数:对数组nums从索引0到n-1排序
void selectionSort(int nums[], int n) {// 外层循环:控制已排序区间的扩充(i是已排序区间的末尾索引)for (int i = 0; i < n - 1; i++) {  // 最后一个元素无需处理,故循环到n-2int minIndex = i;  // 假设未排序区间的第一个元素是最小值,记录其索引// 内层循环:在未排序区间[i..n-1]中找最小元素的索引for (int j = i + 1; j < n; j++) {if (nums[j] < nums[minIndex]) {  // 找到更小的元素,更新minIndexminIndex = j;}}// 将找到的最小元素,与未排序区间的第一个元素(nums[i])交换swap(nums[i], nums[minIndex]);}
}// 测试函数
int main() {int nums[] = {5, 3, 8, 1, 2};  // 测试用例(非递增、无序)int n = sizeof(nums) / sizeof(nums[0]);  // 数组长度cout << "排序前:";for (int i = 0; i < n; i++) cout << nums[i] << " ";selectionSort(nums, n);cout << "\n排序后:";for (int i = 0; i < n; i++) cout << nums[i] << " ";return 0;
}

逐轮执行拆解(以测试用例 [5,3,8,1,2] 为例)

每一轮清晰标注「已排序区间」「未排序区间」「最小元素索引」「交换后数组」,直观看到逻辑走向:

初始状态
  • 数组:[5, 3, 8, 1, 2]
  • 已排序区间:[](空)
  • 未排序区间:[5, 3, 8, 1, 2](索引 0~4)
第 1 轮(i=0)
  • 目标:在未排序区间 [0~4] 找最小元素,放到 i=0 位置(已排序区间末尾)
  • 内层循环(j=1~4):
    • j=1:3 < 5 → minIndex=1
    • j=2:8 > 3 → 不变
    • j=3:1 < 3 → minIndex=3
    • j=4:2 > 1 → 不变
  • 最小元素索引:minIndex=3(元素 1)
  • 交换 nums [0] 和 nums [3] → 数组变为 [1, 3, 8, 5, 2]
  • 更新区间:
    • 已排序区间:[1](索引 0)
    • 未排序区间:[3, 8, 5, 2](索引 1~4)
第 2 轮(i=1)
  • 目标:在未排序区间 [1~4] 找最小元素,放到 i=1 位置
  • 内层循环(j=2~4):
    • j=2:8 > 3 → 不变
    • j=3:5 > 3 → 不变
    • j=4:2 < 3 → minIndex=4
  • 最小元素索引:minIndex=4(元素 2)
  • 交换 nums [1] 和 nums [4] → 数组变为 [1, 2, 8, 5, 3]
  • 更新区间:
    • 已排序区间:[1, 2](索引 0~1)
    • 未排序区间:[8, 5, 3](索引 2~4)
第 3 轮(i=2)
  • 目标:在未排序区间 [2~4] 找最小元素,放到 i=2 位置
  • 内层循环(j=3~4):
    • j=3:5 < 8 → minIndex=3
    • j=4:3 < 5 → minIndex=4
  • 最小元素索引:minIndex=4(元素 3)
  • 交换 nums [2] 和 nums [4] → 数组变为 [1, 2, 3, 5, 8]
  • 更新区间:
    • 已排序区间:[1, 2, 3](索引 0~2)
    • 未排序区间:[5, 8](索引 3~4)
第 4 轮(i=3)
  • 目标:在未排序区间 [3~4] 找最小元素,放到 i=3 位置
  • 内层循环(j=4):
    • j=4:8 > 5 → 不变
  • 最小元素索引:minIndex=3(元素 5)
  • 交换 nums [3] 和 nums [3](自身交换,无变化)→ 数组仍为 [1, 2, 3, 5, 8]
  • 更新区间:
    • 已排序区间:[1, 2, 3, 5](索引 0~3)
    • 未排序区间:[8](索引 4)
循环结束
  • 未排序区间只剩 1 个元素,无需处理,排序完成。

关键细节重申(解决你的核心疑问)

  1. 无需刻意比较 “新最小” 和 “原最小”:每轮的未排序区间是 “剩下的元素”,而本轮选的是 “剩下元素里的最小”,自然比已排序区间的所有元素都大(或相等)—— 因为已排序区间的元素都是 “之前每轮从当时的未排序区间里选出来的最小”,已经被移走了。

  2. 原数组是否递增不影响逻辑:不管原数组是无序、递减还是有重复元素,“划分区间 + 选当前未排序最小” 的逻辑始终成立,每轮都会让已排序区间扩充且保持有序,最终整体递增。

c

选择排序(升序)的 C 语言实现

#include <stdio.h>// 交换两个整数的值(C 中没有内置 swap,需手动实现)
void swap(int *a, int *b) {int temp = *a;  // 用临时变量存储 a 的值*a = *b;        // 将 b 的值赋给 a*b = temp;      // 将临时变量(原 a 的值)赋给 b
}// 选择排序函数:对数组 nums 从索引 0 到 n-1 排序
void selectionSort(int nums[], int n) {// 外层循环:控制已排序区间的末尾索引 i(从 0 到 n-2,最后一个元素无需处理)for (int i = 0; i < n - 1; i++) {int minIndex = i;  // 假设未排序区间的第一个元素(nums[i])是最小值,记录其索引// 内层循环:在未排序区间 [i..n-1] 中找最小元素的索引for (int j = i + 1; j < n; j++) {// 若当前元素比“假设的最小值”小,则更新最小索引if (nums[j] < nums[minIndex]) {minIndex = j;}}// 将找到的最小元素(nums[minIndex])与未排序区间的第一个元素(nums[i])交换swap(&nums[i], &nums[minIndex]);}
}// 测试函数
int main() {int nums[] = {5, 3, 8, 1, 2};  // 测试用例(非递增、无序)int n = sizeof(nums) / sizeof(nums[0]);  // 计算数组长度(元素个数)printf("排序前:");for (int i = 0; i < n; i++) {printf("%d ", nums[i]);}selectionSort(nums, n);  // 调用选择排序printf("\n排序后:");for (int i = 0; i < n; i++) {printf("%d ", nums[i]);}return 0;
}

C 版本与 C++ 版本的核心区别

  1. 交换函数:C 语言没有内置的 swap 函数,因此需要手动实现 void swap(int *a, int *b),通过指针操作交换两个变量的值(C++ 可以直接用 std::swap,或同样手动实现)。

  2. 输出方式:C 用 printf 输出,C++ 常用 cout;但逻辑上完全一致,都是遍历数组打印元素。

逐轮执行拆解(与 C++ 版本完全相同,以 [5,3,8,1,2] 为例)

为了让你更清晰地看到 C 代码的执行过程,这里简化重复说明,只标注核心步骤:

初始状态
  • 数组:[5, 3, 8, 1, 2]
  • 已排序区间:[],未排序区间:[0~4]
第 1 轮(i=0)
  • 内层循环找 [0~4] 的最小元素:索引 3(值 1)。
  • 交换 nums[0] 和 nums[3] → 数组变为 [1, 3, 8, 5, 2]
  • 已排序区间:[0](值 1),未排序区间:[1~4]
第 2 轮(i=1)
  • 内层循环找 [1~4] 的最小元素:索引 4(值 2)。
  • 交换 nums[1] 和 nums[4] → 数组变为 [1, 2, 8, 5, 3]
  • 已排序区间:[0~1](值 1,2),未排序区间:[2~4]
第 3 轮(i=2)
  • 内层循环找 [2~4] 的最小元素:索引 4(值 3)。
  • 交换 nums[2] 和 nums[4] → 数组变为 [1, 2, 3, 5, 8]
  • 已排序区间:[0~2](值 1,2,3),未排序区间:[3~4]
第 4 轮(i=3)
  • 内层循环找 [3~4] 的最小元素:索引 3(值 5)。
  • 交换 nums[3] 和 nums[3](无变化)→ 数组仍为 [1, 2, 3, 5, 8]
  • 已排序区间:[0~3],未排序区间:[4](只剩一个元素,循环结束)。

运行结果

排序前:5 3 8 1 2 
排序后:1 2 3 5 8 

核心逻辑再强调

C 版本的选择排序和 C++ 版本在算法逻辑上完全一致,都是通过 “划分已排序 / 未排序区间→找未排序区间最小元素→交换到已排序区间末尾” 的步骤,逐步让数组整体有序。关键依然是:每轮选出的 “未排序区间最小元素”,必然大于等于已排序区间的所有元素(因为已排序区间的元素是 “之前选走的最小元素”,剩下的元素不可能更小),因此无需额外判断,自然保证最终有序。

用数组[2,1,2]作为测试用例,验证选择排序对含重复元素数组的排序效果。下面用之前的 C 语言选择排序代码测试该数组,还会逐轮拆解执行过程,清晰展示重复元素的处理细节,同时说明选择排序处理重复元素时的特点:

测试代码(存在重复元素时)

基于之前的选择排序代码,将测试数组改为[2,1,2],具体代码如下:

#include <stdio.h>// 交换两个整数的值
void swap(int *a, int *b) {int temp = *a;*a = *b;*b = temp;
}// 选择排序函数
void selectionSort(int nums[], int n) {for (int i = 0; i < n - 1; i++) {int minIndex = i;// 在内层循环中找未排序区间的最小元素索引for (int j = i + 1; j < n; j++) {if (nums[j] < nums[minIndex]) {minIndex = j;}}// 交换最小元素与未排序区间第一个元素swap(&nums[i], &nums[minIndex]);}
}// 测试数组[2,1,2]
int main() {int nums[] = {2, 1, 2};int n = sizeof(nums) / sizeof(nums[0]);printf("排序前:");for (int i = 0; i < n; i++) {printf("%d ", nums[i]);}selectionSort(nums, n);printf("\n排序后:");for (int i = 0; i < n; i++) {printf("%d ", nums[i]);}return 0;
}

逐轮执行拆解

数组[2,1,2]长度为 3,外层循环只需执行 2 轮(i=0i=1),具体过程如下:

  1. 初始状态
    • 数组:[2, 1, 2]
    • 已排序区间:空,未排序区间:[0~2](整个数组)
  2. 第 1 轮(i=0)
    • 初始假设未排序区间第一个元素(nums[0]=2)是最小值,minIndex=0
    • 内层循环遍历j=1j=2j=1时,nums[1]=1 < nums[0]=2,更新minIndex=1j=2时,nums[2]=2 > nums[1]=1minIndex保持 1 不变。
    • 交换nums[0]nums[1],数组变为[1, 2, 2]
    • 此时已排序区间:[0](值 1),未排序区间:[1~2]
  3. 第 2 轮(i=1)
    • 假设未排序区间第一个元素(nums[1]=2)是最小值,minIndex=1
    • 内层循环遍历j=2nums[2]=2 == nums[1]=2,不更新minIndex
    • 无需交换,数组仍为[1, 2, 2]
    • 此时已排序区间:[0~1](值 1,2),未排序区间仅剩[2],循环结束。

运行结果

排序前:2 1 2
排序后:1 2 2

关于重复元素的补充说明

选择排序是不稳定排序。比如若测试数组为[2a,1,2b]2a2b为两个值相同但位置不同的元素),排序后2a2b的相对位置会交换为[1,2b,2a]。不过这并不影响排序的核心目标(数组整体有序),仅当场景要求保留相同元素原始相对位置时,才需要考虑用稳定排序(如冒泡排序)。

3 不稳定排序

不稳定排序是指排序后,数组中值相等的元素,原始相对位置发生了改变;反之,原始相对位置保持不变的就是稳定排序。

1. 用通俗例子理解:给 “带标记的重复元素” 排序

拿之前的测试数组 [2₁, 1, 2₂] 举例(下标₁、₂代表两个值相同但原始位置不同的元素):

  • 排序前2₁ 在 2₂ 前面(相对位置:2₁ → 2₂)。
  • 选择排序(不稳定) 排序后:
    1. 第一轮找最小元素 1,和 2₁ 交换 → 数组变为 [1, 2₁, 2₂]
    2. 第二轮无需交换,最终结果 [1, 2₁, 2₂](这里看似位置没变?再看另一个例子)。

再换一个数组 [3, 2₁, 4, 2₂]

  • 排序前2₁ 在 2₂ 前面(相对位置:2₁ → 2₂)。
  • 选择排序排序:
    1. 第一轮(i=0):找未排序区间 [0~3] 的最小元素 2₁(索引 1),和 3(索引 0)交换 → 数组变为 [2₁, 3, 4, 2₂]
    2. 第二轮(i=1):找未排序区间 [1~3] 的最小元素 2₂(索引 3),和 3(索引 1)交换 → 数组变为 [2₁, 2₂, 4, 3]
    3. 后续轮次排序后,最终结果 [2₁, 2₂, 3, 4](这里位置仍没变?再看极端情况)。

真正体现 “不稳定” 的例子:[2₁, 2₂, 1]

  • 排序前2₁ 在 2₂ 前面(2₁ → 2₂)。
  • 选择排序排序:
    1. 第一轮(i=0):找未排序区间 [0~2] 的最小元素 1(索引 2),和 2₁(索引 0)交换 → 数组变为 [1, 2₂, 2₁]
  • 排序后2₂ 在 2₁ 前面(相对位置变成 2₂ → 2₁),原始顺序被打乱 → 这就是 “不稳定”!

2. 稳定排序的对比(用冒泡排序)

同样对 [2₁, 2₂, 1] 用冒泡排序(稳定)

  • 排序过程:
    1. 第一轮:比较相邻元素,2₂ 和 1 交换 → [2₁, 1, 2₂];再 2₁ 和 1 交换 → [1, 2₁, 2₂]
  • 排序后2₁ 仍在 2₂ 前面(原始相对位置不变)→ 这就是 “稳定”!

3. 核心区别:是否保留相等元素的原始顺序

排序类型核心特点常见例子
不稳定排序相等元素原始相对位置可能改变选择排序、快速排序、堆排序
稳定排序相等元素原始相对位置保持不变冒泡排序、插入排序、归并排序

4. 为什么需要关注 “稳定性”?

大部分场景下(比如单纯对数字排序),稳定性不重要。但如果是带附加信息的排序(比如给 “学生成绩表” 排序,成绩相同的学生要保留原始排名),就必须用稳定排序:

  • 例:学生列表 [(小明, 90), (小红, 85), (小刚, 90)],按成绩升序排序。
    • 稳定排序后:[(小红, 85), (小明, 90), (小刚, 90)](小明仍在小刚前面,保留原始顺序)。
    • 不稳定排序可能得到:[(小红, 85), (小刚, 90), (小明, 90)](小明和小刚顺序颠倒,破坏原始排名)。

简单总结:不稳定排序只 “关心” 元素值的大小,不 “关心” 它们原本的位置;稳定排序则在保证值有序的同时,记住相等元素的 “先后关系”。

4 快速排序实现

一、代码结构与调用关系

先明确三个函数的分工和调用链:

  1. swap:辅助工具,负责交换数组中两个元素的值(被 partition 调用)。
  2. partition:核心分区函数,将数组划分为左右两部分并返回基准索引(被 quickSort 调用)。
  3. quickSort:主递归函数,通过调用 partition 获得基准索引,再递归排序左右子数组(用户直接调用的入口)。

调用关系用户 → quickSort(...) → partition(...) → swap(...)

二、如何使用?(完整示例代码)

下面是可直接运行的完整程序,包含测试用例,演示如何调用这些函数对数组排序:

#include <stdio.h>// 1. 元素交换函数(辅助工具)
void swap(int nums[], int i, int j) {int tmp = nums[i];nums[i] = nums[j];nums[j] = tmp;
}// 2. 哨兵划分函数(核心分区逻辑)
int partition(int nums[], int left, int right) {int i = left, j = right;while (i < j) {while (i < j && nums[j] >= nums[left]) j--;  // 右指针左移while (i < j && nums[i] <= nums[left]) i++;  // 左指针右移swap(nums, i, j);  // 交换左右指针指向的元素}swap(nums, i, left);  // 基准数归位return i;  // 返回基准索引
}// 3. 快速排序主函数(递归入口)
void quickSort(int nums[], int left, int right) {if (left >= right) return;  // 子数组长度为1时终止递归int pivot = partition(nums, left, right);  // 分区得到基准索引quickSort(nums, left, pivot - 1);  // 递归排序左子数组quickSort(nums, pivot + 1, right);  // 递归排序右子数组
}// 测试函数(演示如何使用)
int main() {// 待排序数组int nums[] = {3, 1, 4, 2, 5};// 计算数组长度(元素个数)int n = sizeof(nums) / sizeof(nums[0]);// 打印排序前的数组printf("排序前:");for (int i = 0; i < n; i++) {printf("%d ", nums[i]);}// 调用快速排序:排序整个数组(从索引0到n-1)quickSort(nums, 0, n - 1);  // 关键调用:left=0,right=n-1// 打印排序后的数组printf("\n排序后:");for (int i = 0; i < n; i++) {printf("%d ", nums[i]);}return 0;
}

三、执行流程拆解(以 [3,1,4,2,5] 为例)

通过跟踪 main 函数的调用,看三个函数如何配合完成排序:

步骤 1:用户调用 quickSort(nums, 0, 4)
  • 目标:排序整个数组(索引 0 到 4)。
  • 此时 left=0right=4left < right,不终止递归)。
步骤 2:quickSort 调用 partition(nums, 0, 4) 分区

partition 函数执行过程(详见上一次解析):

  • 基准数为 nums[0] = 3
  • 经过左右指针扫描和交换,最终基准数 3 归位到索引 2,数组变为 [2, 1, 3, 4, 5]
  • 返回 pivot = 2(基准索引)。
步骤 3:递归排序左、右子数组
  1. 递归左子数组:quickSort(nums, 0, 1)(排序 [0,1] 区间,元素 [2,1])。

    • left=0 < right=1,调用 partition(nums, 0, 1)
      • 基准数 nums[0] = 2,分区后数组变为 [1, 2, 3, 4, 5],返回 pivot=1
    • 递归左子数组 quickSort(nums, 0, 0)left >= right,直接返回)。
    • 递归右子数组 quickSort(nums, 2, 1)left > right,直接返回)。
    • 左子数组排序完成。
  2. 递归右子数组:quickSort(nums, 3, 4)(排序 [3,4] 区间,元素 [4,5])。

    • left=3 < right=4,调用 partition(nums, 3, 4)
      • 基准数 nums[3] = 4,分区后数组不变(4 <= 5),返回 pivot=3
    • 递归左子数组 quickSort(nums, 3, 2)left > right,返回)。
    • 递归右子数组 quickSort(nums, 4, 4)left >= right,返回)。
    • 右子数组排序完成。
最终结果

数组变为 [1, 2, 3, 4, 5],排序完成。

四、关键注意事项(使用时必看)

  1. 参数范围:调用 quickSort 时,left 必须是数组起始索引(通常为 0),right 必须是数组最后一个元素的索引(n-1),否则会访问越界。

  2. 数组类型:这里的代码只支持 int 类型数组,若要排序其他类型(如 floatchar),需修改函数参数中的类型(如 float nums[])。

  3. 稳定性:快速排序是不稳定排序(相等元素可能交换位置),若需稳定排序,需改用归并排序等算法。

  4. 性能:平均时间复杂度为 O(n log n),但在极端情况(如有序数组)下会退化为 O(n²)。实际使用时可优化 partition 函数的基准选择(如随机基准)避免最坏情况。

总结

使用方法非常简单:只需在你的代码中包含这三个函数,然后调用 quickSort(nums, 0, n-1) 即可完成对数组 nums 的排序(n 是数组长度)。内部通过 quickSort 递归调用 partition 划分区间,partition 再调用 swap 交换元素,三者配合实现高效排序。

5 归并排序

归并排序(Merge Sort)是典型的分治算法,核心思想是 “分而治之”

将大问题拆解为小问题,解决小问题后再合并结果。它的稳定性(相等元素相对位置不变)和稳定的 O(n log n) 时间复杂度使其在很多场景中非常实用。下面从核心逻辑→分步实现→代码细节→执行流程全方位拆解归并排序的实现。

一、归并排序的核心逻辑(分治思想)

归并排序分为三个步骤,形成递归闭环:

  1. 分(Divide):将原数组从中点拆分为两个等长的子数组(若长度为奇数,左子数组比右子数组多一个元素)。
  2. 治(Conquer):递归对两个子数组进行归并排序,直到子数组长度为 1(单个元素天然有序)。
  3. 合(Merge):将两个已排序的子数组合并为一个有序数组。

关键:合并两个有序子数组是核心操作,决定了归并排序的正确性和效率。

二、完整实现代码(C 语言)

下面是可直接运行的归并排序代码,包含辅助函数和详细注释:

#include <stdio.h>
#include <stdlib.h>  // 用于动态分配内存(malloc/free)// 辅助函数:将两个有序子数组合并为一个有序数组
// nums[left..mid] 和 nums[mid+1..right] 是两个已排序的子数组
void merge(int nums[], int left, int mid, int right) {// 1. 开辟临时数组,存储合并后的结果int len = right - left + 1;  // 合并后数组的长度int* temp = (int*)malloc(len * sizeof(int));  // 动态分配临时数组int i = left;       // 左子数组的起始索引int j = mid + 1;    // 右子数组的起始索引int k = 0;          // 临时数组的起始索引// 2. 比较两个子数组的元素,按从小到大顺序放入临时数组while (i <= mid && j <= right) {// 若左子数组元素 <= 右子数组元素,取左元素(保证稳定性)if (nums[i] <= nums[j]) {temp[k++] = nums[i++];} else {temp[k++] = nums[j++];}}// 3. 将剩余元素(左或右子数组)复制到临时数组while (i <= mid) {temp[k++] = nums[i++];  // 左子数组有剩余}while (j <= right) {temp[k++] = nums[j++];  // 右子数组有剩余}// 4. 将临时数组的结果复制回原数组(覆盖原区间 [left..right])for (k = 0; k < len; k++) {nums[left + k] = temp[k];}// 5. 释放临时数组的内存(避免内存泄漏)free(temp);
}// 归并排序主函数:递归排序 nums[left..right]
void mergeSort(int nums[], int left, int right) {// 递归终止条件:子数组长度为 1 时(left == right),无需排序if (left >= right) {return;}// 1. 分:计算中点,拆分左右子数组int mid = left + (right - left) / 2;  // 等价于 (left + right)/2,避免溢出// 2. 治:递归排序左右子数组mergeSort(nums, left, mid);       // 排序左子数组 [left..mid]mergeSort(nums, mid + 1, right);  // 排序右子数组 [mid+1..right]// 3. 合:合并两个已排序的子数组merge(nums, left, mid, right);
}// 测试函数
int main() {int nums[] = {3, 1, 4, 2, 5};  // 待排序数组int n = sizeof(nums) / sizeof(nums[0]);  // 数组长度printf("排序前:");for (int i = 0; i < n; i++) {printf("%d ", nums[i]);}// 调用归并排序:排序整个数组(left=0,right=n-1)mergeSort(nums, 0, n - 1);printf("\n排序后:");for (int i = 0; i < n; i++) {printf("%d ", nums[i]);}return 0;
}

三、核心细节拆解

1. 递归终止条件:if (left >= right) return;
  • 当子数组长度为 1(left == right)时,数组本身有序,无需排序。
  • 若 left > right(理论上不会出现,因拆分逻辑保证子数组有效),同样直接返回。
2. 拆分逻辑:mid = left + (right - left) / 2
  • 计算中点 mid 时,用 left + (right - left)/2 而非 (left + right)/2,是为了避免 left + right 溢出(例如 left 和 right 都是接近 INT_MAX 的值时,相加会超出 int 范围)。
  • 拆分后,左子数组为 [left..mid],右子数组为 [mid+1..right],确保两个子数组无重叠且覆盖原区间。
3. 合并函数 merge 的关键步骤

合并是归并排序的 “灵魂”,需重点理解:

  • 临时数组 temp:用于暂存合并结果,避免在原数组上直接操作导致元素被覆盖。
  • 双指针比较i 指向左子数组当前元素,j 指向右子数组当前元素,每次取较小的元素放入 temp,确保合并后有序。
  • 稳定性保证:当 nums[i] == nums[j] 时,取左子数组的元素(temp[k++] = nums[i++]),这样相等元素的原始相对位置(左在前,右在后)得以保留,使归并排序成为稳定排序。
  • 剩余元素处理:当一个子数组遍历完后,将另一个子数组的剩余元素直接复制到 temp(因子数组已排序,剩余元素必然大于等于 temp 中已有元素)。
  • 复制回原数组:最后将 temp 中的结果覆盖原数组的 [left..right] 区间,完成合并。

四、执行流程示例(以 [3,1,4,2,5] 为例)

为直观展示分治过程,按 “拆分→递归排序→合并” 三步拆解:

初始调用mergeSort(nums, 0, 4)(排序整个数组)
  • 拆分mid = 0 + (4-0)/2 = 2,左子数组 [0..2] = [3,1,4],右子数组 [3..4] = [2,5]
递归处理左子数组 [0..2]mergeSort(nums, 0, 2)
  • 拆分mid = 0 + (2-0)/2 = 1,左子数组 [0..1] = [3,1],右子数组 [2..2] = [4]
    • 递归排序 [0..1]mergeSort(nums, 0, 1)
      • 拆分mid = 0 + (1-0)/2 = 0,左子数组 [0..0] = [3],右子数组 [1..1] = [1]
      • 递归排序 [0..0] 和 [1..1]:因长度为 1,直接返回。
      • 合并:调用 merge(nums, 0, 0, 1),合并 [3] 和 [1] → 结果 [1,3],原数组变为 [1,3,4,2,5]
    • 递归排序 [2..2]:直接返回。
    • 合并:调用 merge(nums, 0, 1, 2),合并 [1,3] 和 [4] → 结果 [1,3,4],原数组变为 [1,3,4,2,5]
递归处理右子数组 [3..4]mergeSort(nums, 3, 4)
  • 拆分mid = 3 + (4-3)/2 = 3,左子数组 [3..3] = [2],右子数组 [4..4] = [5]
  • 递归排序两个子数组:直接返回。
  • 合并:调用 merge(nums, 3, 3, 4),合并 [2] 和 [5] → 结果 [2,5],原数组仍为 [1,3,4,2,5](右子数组本身有序)。
最终合并:调用 merge(nums, 0, 2, 4),合并 [1,3,4] 和 [2,5]
  • 双指针比较过程:
    • i=0(1) vs j=3(2)→ 取 1 → temp=[1]i=1
    • i=1(3) vs j=3(2)→ 取 2 → temp=[1,2]j=4
    • i=1(3) vs j=4(5)→ 取 3 → temp=[1,2,3]i=2
    • i=2(4) vs j=4(5)→ 取 4 → temp=[1,2,3,4]i=3(左子数组遍历完)。
    • 复制右子数组剩余元素 5 → temp=[1,2,3,4,5]
  • 复制回原数组 → 最终数组 [1,2,3,4,5]

五、归并排序的特性

  1. 时间复杂度O(n log n)

    • 拆分过程:将数组拆分为 log n 层(每一层子数组总数翻倍,长度减半)。
    • 合并过程:每一层的合并操作总耗时为 O(n)(所有子数组的总长度为 n)。
    • 总耗时:O(log n) * O(n) = O(n log n),且最好、最坏、平均情况均为此值,非常稳定。
  2. 空间复杂度O(n)

    • 主要来自合并时的临时数组 temp,其最大长度为 n(合并最上层时)。递归调用栈的深度为 O(log n),可忽略。
  3. 稳定性:稳定排序

    • 合并时当 nums[i] == nums[j] 取左子数组元素,保证相等元素的原始相对位置不变。
  4. 适用场景

    • 适合大规模数据排序(时间复杂度稳定)。
    • 适合链表排序(无需额外空间存储临时数组,通过指针操作合并)。
    • 适合需要稳定排序的场景(如带附加信息的排序)。

总结

归并排序的核心是 “分治 + 合并”,其中合并两个有序子数组的操作是关键。它通过牺牲 O(n) 的空间换取了稳定的 O(n log n) 时间复杂度和稳定性,是非常经典的高效排序算法。实际实现时需注意临时数组的内存管理(避免泄漏)和中点计算(避免溢出)。

http://www.dtcms.com/a/569618.html

相关文章:

  • 【VLAs篇】08:以实时速度运行VLA
  • 广西桂林建设局网站建立网站需要多少钱 索 圈湖南岚鸿
  • 买完服务器怎么做网站网页编辑软件绿色
  • 从奠基到前沿:CIFAR-10 数据集如何驱动计算机视觉研发进化
  • 计算机网络第六章学习
  • 华为A800I A2 arm64架构鲲鹏920cpu的ubuntu22.04 tls配置直通的grub配置
  • 耐热型发光颜料:高温环境下的功能材料新星
  • 简单易做的的网站做网站一定要注册域名吗
  • 正态分布概率:1σ、2σ、3σ、4σ深度解读
  • 红帽Linux-调优系统性能
  • python找到文件夹A中但是不在文件夹B中的文件
  • 做企业网站要怎么设计方案机关单位网站安全建设
  • 网站建设乙方义务wordpress 模板 淘宝客
  • 归并排序解读(基于java实现)
  • 从0开始学算法——第一天(如何高效学习算法)
  • 相似度计算算法系统性总结
  • 大型网站建设用什么系统好佛山网站设计哪里好
  • Perplexity AI 的 RAG 架构全解析:幕后技术详解
  • 免费查找资料的网站不同网站建设特点
  • 信诚网络公司网站莱芜吧莱芜贴吧
  • Web Js逆向——加密参数定位方法(Hook)
  • Python3 模块
  • APP网站建设什么用处昆明装饰企业网络推广
  • Vue开发系列——自定义组件开发
  • 网站网页和网址的关系乐陵森林覆盖率
  • 贵阳响应式网站开发汕头网站推广找哪里
  • 测试——bug
  • 日语学习-日语知识点小记-构建基础-JLPT-N3阶段-二阶段(13):文法和单词-第三课
  • 网站开发环境设计wordpress微信小程序one
  • 建行业网站的必要性沈阳网站维护