力扣 373:有序数组最小K数对的暴力与优化双解

力扣 373:有序数组最小K数对的暴力与优化双解

力扣 373:有序数组最小K数对的暴力与优化双解

  • 📜 前言:算法千般道,有序为捷径
  • Bilibili 同步视频
  • 🧩 一、题意剖释:明解题之本,晓边界之规
    • 1.1 原题题意
    • 1.2 解题前置心法:为何选用大顶堆?
  • 📊 二、暴力枚举解法:直白可行,却逢时限之困
    • 2.1 算法思路(Plain Text原理示意图)
    • 2.2 完整C++暴力源码
    • 2.3 致命缺陷与超时根源
  • ⚡ 三、有序特性优化:顺势而为,斩断无效遍历
    • 3.1 优化核心原理(Plain Text分步图解)
    • 3.2 优化前后性能直观对比
    • 3.3 完整版优化C++代码(可直接AC通过)
    • 3.4 关键代码逐行注解
  • 📖 四、算法求学悟道:万般阻碍,皆为成长土壤
  • 📝 全文骈文速记口诀(一键吃透本题)
  • 🎯 文末总结

📜 前言:算法千般道,有序为捷径

数组分列,有序成行;数对相依,和值分章。

刷题千万,误区暗藏:枚举全域虽稳,难免超时之殇;善用序列天性,方可破壁图强。

本篇以双有序数组寻找和最小K个数对为核心,骈文行文、对仗释理,逐层拆解暴力大顶堆解法、超时根源、有序特性极致优化方案,附完整可运行C++源码、分步原理图、性能对照分析,由表及里,由愚至巧,吃透堆排序+有序数组双重算法核心✨。


Bilibili 同步视频

力扣 373:有序数组最小K数对的暴力与优化双解

🧩 一、题意剖释:明解题之本,晓边界之规

1.1 原题题意

给定两个升序排列的整数数组 nums1、nums2,从两数组中分别取出一个元素组成数对,要求返回所有数对中和值最小的前K个数对。

约束核心:

  • 元素一一配对,一数取自nums1,一数取自nums2

  • 数组全程升序,元素从左至右单调递增

  • 输出结果无需再次排序,保留堆筛选后的有效数对即可

1.2 解题前置心法:为何选用大顶堆?

求前K小,堆分两类:

小顶堆逐次弹出最小值,冗余遍历,开销居高不下;

大顶堆留存备选集合,堆顶为当前备选最大值,超限则剔除大数,留小去大,适配本题最优场景✅。

核心逻辑:维护容量为K的大顶堆,始终保留当前最小的K组数对,新数对小于堆顶则入堆,大于堆顶直接舍弃。


📊 二、暴力枚举解法:直白可行,却逢时限之困

2.1 算法思路(Plain Text原理示意图)

【暴力枚举流程】 nums1: [1,2,4] nums2: [1,3,5] 全量两两枚举 → 生成全部9组数对 全部入大顶堆 → 堆容量超过K → 弹出堆顶最大值 最终剩余K组最小数对 缺陷:无视数组有序性,无脑全遍历,数据量大直接TLE

2.2 完整C++暴力源码

#include<iostream>#include<vector>#include<queue>usingnamespacestd;// 自定义比较器:构建大顶堆,按照数对之和降序排列structcmp{booloperator()(vector<int>&a,vector<int>&b){returna[0]+a[1]<b[0]+b[1];}};vector<vector<int>>kSmallestPairs(vector<int>&nums1,vector<int>&nums2,intk){// 定义大顶堆priority_queue<vector<int>,vector<vector<int>>,cmp>maxHeap;// 双层循环:无脑枚举所有数对for(intx:nums1){for(inty:nums2){maxHeap.push({x,y});// 堆容量超出K,弹出当前最大数对if(maxHeap.size()>k){maxHeap.pop();}}}// 导出结果vector<vector<int>>res;while(!maxHeap.empty()){res.push_back(maxHeap.top());maxHeap.pop();}returnres;}intmain(){vector<int>n1={1,2,4};vector<int>n2={1,3,5};autoans=kSmallestPairs(n1,n2,3);for(auto&item:ans){cout<<item[0]<<" "<<item[1]<<endl;}return0;}

2.3 致命缺陷与超时根源

双循环嵌套,全域遍历所有组合,时间复杂度高达O(N*M)

两数组皆为升序序列,代码完全舍弃有序天性,后续递增数对无需校验依旧强行入堆,无效计算堆砌,大数据场景直接触发TLE超时错误

痛点总结:算法切忌蛮力遍历,无视题干特性,直白代码终究难抗大数据评测用例。


⚡ 三、有序特性优化:顺势而为,斩断无效遍历

3.1 优化核心原理(Plain Text分步图解)

【有序数组优化逻辑】 已知:nums1、nums2 全局升序 固定 nums1 中元素 x,向后遍历 nums2 nums2 元素y持续变大 → x+y 和值持续单调递增 判定规则: 1. 堆未满k个:直接入堆,无需判断 2. 堆已满k个:当前和 < 堆顶和 → 替换堆顶 3. 当前和 ≥ 堆顶和 → 后续所有和只会更大 → 直接break终止内层循环 核心:依托单调性,提前截断循环,消灭全部无效遍历

3.2 优化前后性能直观对比

解法类型时间复杂度运行耗时是否超时
暴力全枚举O(N*M)130ms+
有序截断优化O(N*K)12ms左右

3.3 完整版优化C++代码(可直接AC通过)

#include<iostream>#include<vector>#include<queue>usingnamespacestd;structcmp{booloperator()(vector<int>&a,vector<int>&b){returna[0]+a[1]<b[0]+b[1];}};vector<vector<int>>kSmallestPairs(vector<int>&nums1,vector<int>&nums2,intk){priority_queue<vector<int>,vector<vector<int>>,cmp>maxHeap;for(intx:nums1){for(inty:nums2){intcurSum=x+y;// 分支1:堆内元素不足k,直接存入if(maxHeap.size()<k){maxHeap.push({x,y});}else{// 分支2:堆已满,当前数对更小则替换堆顶if(curSum<maxHeap.top()[0]+maxHeap.top()[1]){maxHeap.pop();maxHeap.push({x,y});}else{// 依托升序单调性,后续和值只会更大,直接截断内层循环break;}}}}vector<vector<int>>res;while(!maxHeap.empty()){res.push_back(maxHeap.top());maxHeap.pop();}returnres;}intmain(){vector<int>n1={1,2,4};vector<int>n2={1,3,5};autoans=kSmallestPairs(n1,n2,3);for(auto&item:ans){cout<<item[0]<<"+"<<item[1]<<"="<<item[0]+item[1]<<endl;}return0;}

3.4 关键代码逐行注解

  • 堆容量判断前置:优先判断堆空间,未满直接存入,省去多余比较开销

  • break截断核心:内层循环一旦命中大于等于堆顶,立刻终止本轮nums2遍历,杜绝无效循环

  • 比较器无需深究:数组比较依据为元素之和,底层语法实现属于语言细节,无需纠结底层重载逻辑,聚焦算法思维即可


📖 四、算法求学悟道:万般阻碍,皆为成长土壤

刷题之路,荆棘相伴;代码之途,苦练为岸。

常有学子观他人代码行云流水,自敲代码寸步难行,妄图跳过实操,一步登天,此乃虚妄之念。

天下代码,无捷径可走;一身功力,唯苦练可成。天赋分高下,努力无偏颇,付出几分耕耘,便得几分收获。

遇难题而退缩,困当下之桎梏;迎难题而攻坚,铺来日之坦途。

譬如种子埋于泥土,泥土一时为阻隔,压制破土锋芒;待到嫩芽而出,泥土便为根基,托举枝干生长。

当下算法之难、代码之苦,皆是脚下土壤;今日熬过万般阻碍,来日便可傲视群雄,自成锋芒✨。


📝 全文骈文速记口诀(一键吃透本题)

双序数组寻小数,大顶堆存备选组;

暴力双层全遍历,无视序列超时苦;

升序单调和递增,遇大截断少往复;

算法巧用题干性,少算一步快一步;

刷题不惧当下苦,困境终成脚下土。


🎯 文末总结

本题看似是堆结构基础应用题,实则考察算法优化思维:优秀的代码从不是无脑模拟流程,而是读懂题干隐藏条件,顺势简化计算。

暴力解法保正确率,优化解法保运行效率,二者结合,方能兼顾逻辑与性能。

💬 评论区交流:你刷题时是否也经常无脑遍历忽略数组有序特性?欢迎留言讨论!

#算法 #C++ #大顶堆 #数组算法 #LeetCode刷题 #代码优化