LeetCode 373. 查找和最小的 K 对数字:题解+代码详解

LeetCode中等难度题目「373. 查找和最小的 K 对数字」,这道题核心考察优先队列(小顶堆)的应用,同时需要兼顾去重和边界处理,很多同学容易在堆调整和去重逻辑上踩坑,今天就结合代码一步步讲明白。

一、题目核心信息

先明确题目要求,避免理解偏差:

  • 给定两个非递减顺序排列的整数数组 nums1 和 nums2,以及一个整数 k;

  • 定义"数对"(u, v),u来自nums1,v来自nums2;

  • 需要找出和最小的 k 个数对,返回这k个数对组成的二维数组。

关键提示:数组是非递减的,这是解题的重要突破口;k可能小于所有可能数对的数量(此时返回所有数对中最小的k个),也可能大于(此时返回所有数对)。

二、解题思路分析

拿到题目,首先想到的是"暴力解法"------枚举所有可能的数对,计算它们的和,排序后取前k个。但这种方法的时间复杂度是 O(mn log(mn))(m、n分别是nums1、nums2的长度),当数组长度较大时(比如m、n都是1e4),会直接超时,所以必须寻找更高效的解法。

结合数组"非递减"的特性,我们可以用**小顶堆(优先队列)**来优化:

  1. 初始时,将最小的可能数对(nums1[0], nums2[0])加入堆中,同时用一个集合记录已加入堆的数对下标(i,j),避免重复加入;

  2. 每次从堆顶取出和最小的数对,加入结果集;

  3. 取出堆顶数对(i,j)后,将其"下一个可能的最小数对"(i, j+1)和(i+1, j)加入堆中(前提是下标未越界且未被访问过);

  4. 重复步骤2-3,直到取出k个数对,或堆为空(所有数对都已取出)。

为什么这样可行?因为数组是非递减的,(i,j)的下一个最小和,必然是从(i,j+1)或(i+1,j)中产生------比如nums1[i] ≤ nums1[i+1],nums2[j] ≤ nums2[j+1],所以(i,j)之后,最小的和一定是这两个数对之一,无需枚举其他无关数对,大大减少了计算量。

时间复杂度优化为 O(k log k):每次堆的插入和弹出操作都是 O(log k),总共执行k次,效率远高于暴力解法。

三、完整代码及逐行解析

先贴出完整可运行代码(TypeScript),再逐行拆解关键逻辑,重点讲解堆调整和去重细节:

typescript 复制代码
function kSmallestPairs(nums1: number[], nums2: number[], k: number): number[][] {
    const m = nums1.length;
    const n = nums2.length;
    const heap: [number, number, number][] = []; // 小顶堆,存储 [和, nums1下标, nums2下标]
    const visited = new Set<string>(); // 记录已加入堆的下标对,避免重复
    const res: number[][] = []; // 存储结果

    // 封装push函数:将下标(i,j)对应的数对加入堆(需判断边界和去重)
    function push(i: number, j: number) {
        if (i >= m || j >= n) return; // 下标越界,直接返回
        const key = `${i},${j}`; // 用字符串标记下标对,方便存入Set
        if (visited.has(key)) return; // 已访问过,避免重复加入
        visited.add(key); // 标记为已访问
        heap.push([nums1[i] + nums2[j], i, j]); // 加入堆
        
        // 堆的上浮调整(维护小顶堆特性)
        let cur = heap.length - 1; // 当前插入元素的下标
        while (cur > 0) {
            const parent = (cur - 1) >> 1; // 父节点下标(等价于Math.floor((cur-1)/2))
            if (heap[parent][0] <= heap[cur][0]) break; // 父节点更小,满足小顶堆,退出
            // 父节点大于当前节点,交换两者
            [heap[parent], heap[cur]] = [heap[cur], heap[parent]];
            cur = parent; // 继续向上调整
        }
    }

    push(0, 0); // 初始加入最小的数对下标(0,0)

    // 循环取出堆顶元素,直到拿到k个结果或堆为空
    while (heap.length && res.length < k) {
        const top = heap[0]; // 堆顶是当前和最小的元素
        const last = heap.pop()!; // 取出堆尾元素
        if (heap.length > 0) {
            heap[0] = last; // 将堆尾元素放到堆顶,准备下沉调整
            
            // 堆的下沉调整(维护小顶堆特性)
            let cur = 0;
            const len = heap.length;
            while (true) {
                let left = cur * 2 + 1; // 左子节点下标
                let right = cur * 2 + 2; // 右子节点下标
                let minIdx = cur; // 记录当前最小元素的下标
                // 比较左子节点和当前最小元素,更新minIdx
                if (left < len && heap[left][0] < heap[minIdx][0]) minIdx = left;
                // 比较右子节点和当前最小元素,更新minIdx
                if (right < len && heap[right][0] < heap[minIdx][0]) minIdx = right;
                if (minIdx === cur) break; // 没有比当前节点更小的子节点,退出
                // 交换当前节点和最小子节点
                [heap[cur], heap[minIdx]] = [heap[minIdx], heap[cur]];
                cur = minIdx; // 继续向下调整
            }
        }

        // 将堆顶元素对应的数对加入结果集
        const [sum, i, j] = top;
        res.push([nums1[i], nums2[j]]);
        
        // 加入下一个可能的最小数对(i,j+1)和(i+1,j)
        push(i, j + 1);
        push(i + 1, j);
    }

    return res;
}

关键细节拆解(避坑重点)

1. 堆的存储结构

堆中存储的是 [和, nums1下标, nums2下标],而不是直接存储数对(u,v)。这样做的目的是,方便后续取出下标后,快速找到"下一个可能的数对"(i,j+1)和(i+1,j),同时通过和的大小维护小顶堆。

2. 去重逻辑(visited集合)

为什么需要去重?比如(i+1,j)和(i,j+1)可能会指向同一个下标对(比如i=1,j=0和i=0,j=1,后续可能都会衍生出(1,1)),如果不去重,会导致同一个数对多次加入堆中,浪费空间和计算资源。

这里用 ${i},${j} 作为key存入Set,既简洁又能唯一标识一个下标对,避免重复。

3. 堆的调整(上浮+下沉)

这是小顶堆的核心,也是最容易出错的地方,代码中已经标注了关键步骤,再补充2个易错点:

  • 上浮调整:插入元素后,从当前位置向上和父节点比较,只要当前元素比父节点小,就交换,直到父节点更小或到达堆顶;

  • 下沉调整:取出堆顶后,将堆尾元素放到堆顶,然后向下和左右子节点比较,找到最小的子节点交换,直到没有更小的子节点或到达堆底;

  • 注意:下沉调整时,必须先判断左右子节点是否越界(left < len、right < len),否则会报错。

4. 边界处理

有两个边界需要注意:

  • 下标越界:push函数中,先判断i >= m或j >= n,避免访问数组不存在的元素;

  • k大于所有数对数量:当堆为空时,说明所有数对都已取出,此时即使res.length < k,也需要退出循环,返回已有的所有数对。

四、总结与优化方向

核心总结

这道题的核心是"利用非递减数组的特性,用小顶堆筛选最小和数对",避免暴力枚举。关键在于:

  • 堆的维护(上浮+下沉),确保堆顶始终是当前最小和;

  • 去重逻辑,避免重复加入同一个数对;

  • 边界处理,防止下标越界和k超出数对总数的情况。

相关推荐
wefly20172 小时前
零基础上手m3u8live.cn,免费无广告的M3U8在线播放器,电脑手机通用
前端·javascript·学习·电脑·m3u8·m3u8在线播放
思茂信息2 小时前
基于 CST 的方向图可重构天线仿真分析
网络·人工智能·单片机·算法·重构·cst·电磁仿真
IronMurphy2 小时前
【算法三十三】17. 电话号码的字母组合
算法
逆境不可逃2 小时前
LeetCode 热题 100 之 131. 分割回文串 51. N 皇后
算法·leetcode·职场和发展
进击的小头2 小时前
第21篇:BUCK变换器双环控制系统设计与参数整定调试实战
python·算法
晓13132 小时前
React篇——第四章 React Router基础
前端·javascript·react
liliangcsdn2 小时前
信息检索评估指标Recall@K的分析和计算示例
算法·全文检索
Moment2 小时前
如果想转 AI 全栈?推荐你学一下 Langchain!
前端·后端·面试
handsomethefirst2 小时前
【算法与数据结构】【面试经典150题】【题36-题40】
数据结构·算法·面试
cch89182 小时前
常见布局实现详解(Flex 实战版)
前端·javascript·css