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超出数对总数的情况。

相关推荐
KaMeidebaby10 分钟前
卡梅德生物技术快报|冻干工艺开发:注射用心肌肽全流程参数优化与工程化方案
前端·其他·百度·新浪微博
Moment1 小时前
面试官:如果产品经理给你多个需求,怎么让AI去完成❓❓❓
前端·后端·面试
每天吃饭的羊1 小时前
JSONP
前端
Hello.Reader1 小时前
算法基础(十)——分治思想把大问题拆成小问题
java·开发语言·算法
gogoing1 小时前
ESLint 配置字段说明
前端·javascript
gogoing1 小时前
CSS 属性值计算过程(Computed Value)
前端·css
gogoing1 小时前
webpack 的性能优化
前端·javascript
桃花键神1 小时前
Bright Data Web Scraping指南 2026: 使用 MCP + Dify 自动采集海外社交媒体数据
大数据·前端·人工智能
gogoing1 小时前
await fetch() 的两阶段设计
前端·javascript
gogoing1 小时前
前端首屏加载优化
前端·javascript