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

分治算法-归并排序专题:从性能优化到索引数组的突破

分治算法-归并排序专题:从性能优化到索引数组的突破

在这里插入图片描述

目录

  • 刷题记录
  • 刷题过程
  • 核心概念
  • 我踩的坑
  • 题目详解
  • 核心收获

刷题记录

  • 刷题日期: 2024年10月17日(Day16)
  • 完成题量: 4题全部AC
  • 学习时长: 约4小时
  • 题目难度: 中等1题 + 困难3题
题目难度用时一次AC?
LeetCode 912 - 排序数组(归并排序)中等40分钟❌ 性能优化
剑指Offer 51 - 数组中的逆序对困难50分钟❌ cur2初始化错误
LeetCode 315 - 计算右侧小于当前元素的个数困难100分钟✅ 引导后一次过
LeetCode 493 - 翻转对困难40分钟❌ 混淆统计和归并

刷题过程

Day16(10.17):归并排序 + 统计问题

第1题:排序数组(LeetCode 912)

  • 写到合并两个有序数组时卡住了
  • 不确定用什么数据结构辅助
  • 经过引导:用临时数组 tmp,双指针比较
  • 第一版用局部 tmp:904ms,感觉很慢
  • 优化成全局 tmp:60ms,快了15倍!
  • 关键理解:resize 的作用,避免频繁内存分配

第2题:数组中的逆序对(剑指Offer 51)⭐⭐

  • 一开始不理解为什么暴力会超时
  • 计算了一下:50000² / 2 = 12.5亿次,确实会超时
  • 然后不理解为什么归并排序能统计逆序对
  • 困惑点:
    1. 排序了数组变了,统计还会正确吗?
    2. 为什么要递归?
    3. 什么时候统计?
  • 经过一步步引导理解:
    • 先统计再排序,count 已经记录了
    • 递归是为了让左右有序,才能批量统计
    • else 分支统计(左边大于右边时)
  • 写代码时出错:cur2 = right,应该是 cur2 = mid + 1
  • 改对后 AC,但理解过程花了很长时间

第3题:计算右侧小于当前元素的个数(LeetCode 315)⭐⭐⭐

  • 这题最难!一开始完全不知道怎么做
  • 困惑点:
    1. 返回值怎么处理?数组还是什么?
    2. 如何记录每个元素的统计结果?
    3. 统计公式和数组怎么关联?
  • 经过苏格拉底式引导,一步步理解:
    1. 逆序对统计整体,这题统计每个元素
    2. 归并排序会改变位置,需要追踪
    3. 索引数组追踪原始位置!
    4. 统计时机:左边被取走时
    5. 统计公式:cur2 - mid - 1
  • 花了100分钟,但最后一次AC!
  • 最大收获:索引数组这个技巧!

第4题:翻转对(LeetCode 493)⭐⭐

  • 有了前面的基础,这题思路清晰一些
  • 一开始想套用逆序对的模板,在归并时统计
  • 写成:if(nums[cur1] <= 2*nums[cur2])
  • 结果报错:heap-buffer-overflow(数组越界)
  • 还有个错:while(cur1 <= mid) tmp[k++] = nums[cur2++];(应该是cur1++)
  • 经过引导理解:
    • 翻转对的条件 ≠ 归并条件
    • 不能混在一起
    • 必须分两步:先统计,再归并
  • 还学到了 2LL 的作用:避免整数溢出
  • 改对后 AC

今日难点:

  • 归并排序的 resize 性能优化(15倍提升)
  • 逆序对的"排序后还能统计正确"的理解
  • 索引数组的引入和使用(最难但最有价值)
  • 翻转对的"分两步"理解

核心概念(我的理解)

什么是归并排序?

通过今天4道题,我的理解是:把数组分成两半,递归排序,然后合并两个有序数组。

三步骤:

  1. 分(Divide): 找中点,分成两半
  2. 治(Conquer): 递归排序左右
  3. 合(Combine): 合并两个有序数组

关键是第3步的"合并",用双指针 + 临时数组。


归并排序 vs 快速排序

昨天学了快速排序,今天学归并排序,对比一下:

对比项快速排序归并排序
分的方式按基准值分三块按中点分两半
是否需要合并不需要需要
时间复杂度(平均)O(n log n)O(n log n)
时间复杂度(最坏)O(n²)O(n log n)
空间复杂度O(log n)O(n)
稳定性不稳定稳定
适用场景一般排序需要稳定排序、外部排序

我的理解:

  • 快排:时间换空间(最坏O(n²),但空间小)
  • 归并:空间换时间(稳定O(n log n),但空间大)

归并排序的统计问题分类

今天4道题让我发现了一个规律:

题目统计条件归并条件能否合并?策略
逆序对nums[i] > nums[j]nums[cur1] <= nums[cur2]✅ 可以1步(归并时统计)
翻转对nums[i] > 2*nums[j]nums[cur1] <= nums[cur2]❌ 不行2步(先统计,再归并)
右侧更小每个元素各自统计nums[cur1] <= nums[cur2]❌ 不行需要索引数组

判断标准:

  • 统计条件 = 归并条件的互补 → 可以在归并时统计
  • 统计条件 ≠ 归并条件的互补 → 必须分开处理

索引数组技巧

这是今天最大的收获!

什么时候用?

  • 需要在排序的同时追踪元素原始位置
  • 需要对每个元素单独统计结果

怎么用?

// 1. 创建索引数组
vector<int> index(n);
for(int i = 0; i < n; i++) index[i] = i;// 2. 排序索引,比较元素
比较:nums[index[cur1]] vs nums[index[cur2]]
交换:交换 index 里的值
统计:count[index[cur1]] += ...// 3. index[i] 永远记录着原始位置

核心理解: 不排序元素本身,而是排序它们的"指针"(索引)


我踩的坑

坑1:局部tmp导致性能差(排序数组)

错误代码:

void mergeSort(vector<int>& nums, int left, int right) {// ...vector<int> tmp;  // ❌ 每次递归都创建新数组// 合并...
}

问题: 每次递归都分配内存,频繁分配释放
性能: 904ms

正确做法:

class Solution {
public:vector<int> tmp;  // ✅ 全局数组vector<int> sortArray(vector<int>& nums) {tmp.resize(nums.size());  // 只分配一次mergeSort(nums, 0, nums.size() - 1);return nums;}
};

性能: 60ms,快了15倍!

教训: 能复用的资源就复用,避免频繁分配


坑2:cur2初始化错误(逆序对)

错误代码:

int cur1 = left, cur2 = right;  // ❌ cur2应该是 mid+1

问题: 跳过了中间元素,导致统计错误

测试用例:

输入:[7, 5, 6, 4]
输出:3
预期:5

正确做法:

int cur1 = left, cur2 = mid + 1;  // ✅

教训: 归并排序的右半部分从 mid + 1 开始,不是 right


坑3:索引数组初始思路混乱(右侧更小元素)

初始困惑:

// 我一开始想这样
vector<int> mergeSort(vector<int>& nums, int left, int right) {vector<int> ret{};  // 先统计个数,最后压进去?// ...
}

问题:

  1. 返回值类型不清楚
  2. 如何记录每个元素的统计结果不清楚
  3. 统计公式和数组如何关联不清楚

理解过程(花了100分钟):

  1. 认识到需要追踪每个元素
  2. 归并排序会改变位置
  3. 引入索引数组
  4. 理解统计时机(左边被取走时)
  5. 理解统计公式(cur2 - mid - 1

教训: 复杂问题要一步步分解,不要一下想太多


坑4:混淆统计和归并(翻转对)

错误代码:

while(cur1 <= mid && cur2 <= right) {if(nums[cur1] <= 2*nums[cur2]) {  // ❌ 条件错了tmp[k++] = nums[cur1++];} else {tmp[k++] = nums[cur2++];count += mid - cur1 + 1;}
}

问题: 用翻转对的条件来决定取谁,导致归并排序错误

运行错误: heap-buffer-overflow

正确做法: 分两步

// Step 1: 统计翻转对
int cur1 = left, cur2 = mid + 1;
while(cur1 <= mid) {while(cur2 <= right && nums[cur1] > 2LL * nums[cur2]) {cur2++;}count += (cur2 - mid - 1);cur1++;
}// Step 2: 正常归并
// ...

教训: 统计条件 ≠ 归并条件时,必须分开处理


坑5:整数溢出(翻转对)

错误代码:

nums[cur1] > 2 * nums[cur2]  // ❌ 可能溢出

问题: nums[cur2] 可能是10亿,2 * 10亿 = 20亿,接近 int 上限

正确做法:

nums[cur1] > 2LL * nums[cur2]  // ✅ 用 long long

解释:

  • 2LLlong long 类型
  • nums[cur2] 会自动提升为 long long
  • 结果不会溢出

教训: 涉及乘法时要考虑溢出


题目详解

1. 排序数组(LeetCode 912)⭐

题目: 对数组进行升序排列。

思路: 归并排序三步骤

  • 分:找中点 mid = (left + right) >> 1
  • 治:递归排序左右
  • 合:合并两个有序数组

代码:

class Solution {
public:vector<int> tmp;  // 全局临时数组vector<int> sortArray(vector<int>& nums) {tmp.resize(nums.size());  // 预分配空间mergeSort(nums, 0, nums.size() - 1);return nums;}void mergeSort(vector<int>& nums, int left, int right) {if(left >= right) return;int mid = (left + right) >> 1;mergeSort(nums, left, mid);mergeSort(nums, mid + 1, right);// 合并两个有序数组int cur1 = left, cur2 = mid + 1;int k = left;while(cur1 <= mid && cur2 <= right) {if(nums[cur1] <= nums[cur2]) {tmp[k++] = nums[cur1++];} else {tmp[k++] = nums[cur2++];}}while(cur1 <= mid) tmp[k++] = nums[cur1++];while(cur2 <= right) tmp[k++] = nums[cur2++];// 拷贝回原数组for(int i = left; i <= right; i++) {nums[i] = tmp[i];}}
};

关键点:

  1. 用全局 tmp 避免频繁分配
  2. tmp 的索引从 left 开始
  3. 拷贝时 nums[i] = tmp[i](索引对应)

性能优化: 局部tmp 904ms → 全局tmp 60ms(快15倍

复杂度:

  • 时间:O(n log n)
  • 空间:O(n)

2. 数组中的逆序对(剑指Offer 51)⭐⭐

题目: 统计数组中的逆序对数量。

逆序对定义: i < jnums[i] > nums[j]

思路: 归并排序 + 批量统计

  • 在合并时,利用有序性批量统计
  • 当左边大于右边时,左边剩余的都大于右边
  • 公式:count += (mid - cur1 + 1)

代码:

class Solution {
public:vector<int> tmp;int reversePairs(vector<int>& nums) {tmp.resize(nums.size());return mergeSort(nums, 0, nums.size() - 1);}int mergeSort(vector<int>& nums, int left, int right) {if(left >= right) return 0;int mid = (left + right) >> 1;int count = mergeSort(nums, left, mid) + mergeSort(nums, mid + 1, right);int cur1 = left, cur2 = mid + 1;int k = left;while(cur1 <= mid && cur2 <= right) {if(nums[cur1] <= nums[cur2]) {tmp[k++] = nums[cur1++];} else {tmp[k++] = nums[cur2++];count += (mid - cur1 + 1);  // 批量统计}}while(cur1 <= mid) tmp[k++] = nums[cur1++];while(cur2 <= right) tmp[k++] = nums[cur2++];for(int i = left; i <= right; i++) {nums[i] = tmp[i];}return count;}
};

关键理解:

  • 为什么排序了还能统计?→ 先统计再排序,count 已记录
  • 为什么要递归?→ 让左右有序,才能批量统计
  • 为什么 mid - cur1 + 1?→ 左边剩余的都大于右边当前元素

复杂度: O(n log n)


3. 计算右侧小于当前元素的个数(LeetCode 315)⭐⭐⭐

题目: 返回数组 countscounts[i]nums[i] 右侧更小的元素个数。

思路: 归并排序 + 索引数组

  • 不直接排序 nums,而是排序索引数组 index
  • index[i] 记录原始位置
  • 统计时:count[index[cur1]] += (cur2 - mid - 1)

代码框架:

class Solution {
public:vector<int> tmp, count, nums;vector<int> countSmaller(vector<int>& nums) {int n = nums.size();this->nums = nums;// 创建索引数组vector<int> index(n);for(int i = 0; i < n; i++) index[i] = i;count.resize(n, 0);tmp.resize(n);mergeSort(index, 0, n - 1);return count;}void mergeSort(vector<int>& index, int left, int right) {if(left >= right) return;int mid = (left + right) >> 1;mergeSort(index, left, mid);mergeSort(index, mid + 1, right);// 合并int cur1 = left, cur2 = mid + 1, k = left;while(cur1 <= mid && cur2 <= right) {if(nums[index[cur1]] <= nums[index[cur2]]) {count[index[cur1]] += (cur2 - mid - 1);  // 统计tmp[k++] = index[cur1++];} else {tmp[k++] = index[cur2++];}}while(cur1 <= mid) {count[index[cur1]] += (right - mid);tmp[k++] = index[cur1++];}while(cur2 <= right) tmp[k++] = index[cur2++];for(int i = left; i <= right; i++) index[i] = tmp[i];}
};

关键点:

  1. 排序 index,比较 nums[index[i]]
  2. 统计 count[index[cur1]](用原始下标)
  3. 左边被取走时统计

理解过程: 花了100分钟,但完全理解了索引数组的作用

复杂度: O(n log n)


4. 翻转对(LeetCode 493)⭐⭐

题目: 统计满足 i < jnums[i] > 2 * nums[j] 的翻转对数量。

思路: 分两步

  • Step 1:统计翻转对
  • Step 2:正常归并排序

为什么要分两步?

  • 统计条件:nums[i] > 2 * nums[j]
  • 归并条件:nums[i] <= nums[j]
  • 两个条件不一样,不能混在一起

代码:

class Solution {
public:vector<int> tmp;int reversePairs(vector<int>& nums) {tmp.resize(nums.size());return mergeSort(nums, 0, nums.size() - 1);}int mergeSort(vector<int>& nums, int left, int right) {if(left >= right) return 0;int mid = (left + right) >> 1;int count = mergeSort(nums, left, mid) + mergeSort(nums, mid + 1, right);// Step 1: 统计翻转对int cur1 = left, cur2 = mid + 1;while(cur1 <= mid) {while(cur2 <= right && nums[cur1] > 2LL * nums[cur2]) {cur2++;}count += (cur2 - mid - 1);cur1++;}// Step 2: 正常归并cur1 = left;cur2 = mid + 1;int k = left;while(cur1 <= mid && cur2 <= right) {if(nums[cur1] <= nums[cur2]) {tmp[k++] = nums[cur1++];} else {tmp[k++] = nums[cur2++];}}while(cur1 <= mid) tmp[k++] = nums[cur1++];while(cur2 <= right) tmp[k++] = nums[cur2++];for(int i = left; i <= right; i++) {nums[i] = tmp[i];}return count;}
};

关键点:

  1. 分两步:先统计,再归并
  2. 2LL 避免整数溢出
  3. 不能用统计条件来决定取谁

性能对比:

  • 理论:比逆序对慢2倍(常数因子)
  • 实际:慢1.3-1.5倍(缓存友好性)

复杂度: O(n log n)


核心收获

1. 归并排序的性能优化

全局tmp vs 局部tmp:

方式时间原因
局部tmp904ms每次递归都分配内存
全局tmp60ms只分配一次

优化效果: 快了15倍!

教训: 能复用的资源就复用


2. 归并排序统计问题的规律

判断标准:

  • 统计条件 = 归并条件的互补 → 1步(归并时统计)
  • 统计条件 ≠ 归并条件的互补 → 2步(先统计,再归并)

3种情况:

  1. 逆序对: 条件互补,可以一起
  2. 翻转对: 条件不同,必须分开
  3. 右侧更小: 需要追踪每个元素,用索引数组

3. 索引数组的高级技巧

核心思想: 不排序元素本身,而是排序它们的"指针"

适用场景:

  • 需要在排序的同时追踪原始位置
  • 需要对每个元素单独统计结果

模板:

// 创建索引数组
vector<int> index(n);
for(int i = 0; i < n; i++) index[i] = i;// 排序索引,比较元素
排序:index
比较:nums[index[i]]
统计:count[index[i]]

4. 整数溢出的警惕

问题: 2 * nums[i] 可能溢出 int 范围

解决:2LL 强制转成 long long

2 * nums[cur2]      // ❌ 可能溢出
2LL * nums[cur2]    // ✅ 不会溢出

易错点总结

易错点错误写法正确写法影响
临时数组局部 tmp全局 tmp + resize性能差15倍
tmp索引k = 0k = left索引错位
cur2初始化cur2 = rightcur2 = mid + 1跳过元素
索引数组比较index[cur1]nums[index[cur1]]比较错误
混淆统计和归并在归并时统计翻转对分两步逻辑错误
整数溢出2 * nums[i]2LL * nums[i]溢出

后续计划

今天完成了归并排序的4道题,加上昨天的快速排序4道题,分治算法基本掌握了。

对比:

  • 快速排序:三路分区 + 快速选择
  • 归并排序:二路分治 + 合并统计

明天准备开始新的专题,期待新的挑战!


学习感悟:

  • 今天最大的突破是理解了索引数组这个高级技巧
  • 苏格拉底式学习虽然慢(第3题花了100分钟),但理解很深刻
  • 性能优化(15倍提升)让我意识到细节的重要性
  • 4道题形成了完整的归并排序知识体系

总用时: 约4小时


快排 vs 归并 对比:

对比项(快排)(归并)
题目数量4题4题
学习时长2.5小时4小时
最难的题第K个最大元素右侧更小元素
最大收获快速选择索引数组
http://www.dtcms.com/a/499217.html

相关文章:

  • iis怎么做IP网站有没有专门做数据分析的网站
  • 如何用 Docker Compose 管理多个容器
  • 《C++ STL 基础入门》教案
  • 基于对数灰关联度的IOWGA算子最优组合预测模型
  • VGW 技术解析:构建 Windows 平台的虚拟路由网关中枢
  • 内容安全优化:基于Redis实现分级反爬虫策略
  • 生成式设计案例:MG AEC利用Autodesk AEC Collection推进可持续建筑设计
  • 物流网站源代码修改wordpress后台文字
  • 【HTML】网络数据是如何渲染成HTML网页页面显示的
  • 做门图网站产品品牌推广公司
  • linux学习笔记(38)mysql索引详解
  • M1安装RocketMQ消息队列
  • 广西壮族自治区住房和城乡建设厅网站网站内页制作
  • PDFium导出pdf 图像
  • C++11标准 上 (万字解析)
  • Java基础语法—字面量、变量详解、存储数据原理
  • 手工视频制作网站移动网站建设初学视频教程
  • 【shell】每日shell练习(系统服务状态监控/系统性能瓶颈分析)
  • Swift 下标脚本
  • Spring Boot 3零基础教程,WEB 开发 默认页签图标 Favicon 笔记28
  • php 网站部署杭州企业自助建站系统
  • IntelliJ IDEA 2023中为 Spring Boot 项目添加注释模板
  • Java Web安全防护:SQL注入、XSS攻击的预防与处理
  • leetcode 912.排序数组
  • 个人网站可以做商城吗seo三人行网站
  • 第3讲:Go垃圾回收机制与性能优化
  • Mac 桌面动态壁纸软件|Live Wallpaper 4K Pro v19.7 安装包使用教程(附安装包)
  • 简易网站开发网站建设的各个环节
  • 用 Selenium 搞定动态网页:模拟点击、滚动、登录全流程
  • VBA数据结构抉择战:Dictionary与Collection谁才是效率王者?