查找和最小的 K 对数字

标签: 数组 堆(优先队列)

难度: Medium

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

定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2 

请找到和最小的 k 个数对 (u1,v1),  (u2,v2)  ...  (uk,vk) 。

示例 1:

输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
输出: [1,2],[1,4],[1,6]
解释: 返回序列中的前 3 对数:
     [1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]

示例 2:

输入: nums1 = [1,1,2], nums2 = [1,2,3], k = 2
输出: [1,1],[1,1]
解释: 返回序列中的前 2 对数:
     [1,1],[1,1],[1,2],[2,1],[1,2],[2,2],[1,3],[1,3],[2,3]

提示:

  • 1 <= nums1.length, nums2.length <= 105
  • -109 <= nums1[i], nums2[i] <= 109
  • nums1nums2 均为 升序排列
  • 1 <= k <= 104
  • k <= nums1.length * nums2.length

Submission

运行时间: 95 ms

内存: 32.1 MB

class Solution:
    def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]:
        m,n = len(nums1),len(nums2)
        # heap = [(nums1[i]+nums2[0],i,0) for i in range(min(m,k))]

        # ans = []
        # for _ in range(k):
        #     _,i,j = heappop(heap)
        #     ans.append([nums1[i],nums2[j]])
        #     if j+1<n: heappush(heap,(nums1[i]+nums2[j+1],i,j+1))
        # return ans

        heap = [(nums1[0]+nums2[0],0,0)]
        # 由于i,j之后要(i+1,j),(i,j+1),而(0,0)开始不会推(1,0),(1,0)不会推(2,0)
        # so,if j==0时,我们手动来推
        ans = []
        for _ in range(k):
            _,i,j = heappop(heap)
            ans.append([nums1[i],nums2[j]])
            if j==0 and i+1<m: heappush(heap,(nums1[i+1]+nums2[j],i+1,j))
            if j+1<n: heappush(heap,(nums1[i]+nums2[j+1],i,j+1))
        return ans
        

Explain

这个题解的思路是使用小根堆来维护当前最小的k个数对。首先将(nums1[0]+nums2[0], 0, 0)放入小根堆中,表示当前最小的数对。然后进行k次循环,每次从堆顶弹出当前最小的数对,并将其加入答案数组。接着,如果该数对的下标j等于0,且i+1<m,就把(nums1[i+1]+nums2[j], i+1, j)加入小根堆;如果j+1<n,就把(nums1[i]+nums2[j+1], i, j+1)加入小根堆。这样可以保证小根堆中始终维护着剩余最小的k个数对。

时间复杂度: O(klogk)

空间复杂度: O(k)

class Solution:
    def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]:
        m,n = len(nums1),len(nums2)
        
        # 初始化小根堆,放入(nums1[0]+nums2[0], 0, 0)
        heap = [(nums1[0]+nums2[0],0,0)]
        
        ans = []
        for _ in range(k):
            # 弹出堆顶最小的数对
            _,i,j = heappop(heap)
            ans.append([nums1[i],nums2[j]])
            
            # 如果j等于0且i+1<m,把(nums1[i+1]+nums2[j], i+1, j)加入小根堆 
            if j==0 and i+1<m: heappush(heap,(nums1[i+1]+nums2[j],i+1,j))
            
            # 如果j+1<n,把(nums1[i]+nums2[j+1], i, j+1)加入小根堆
            if j+1<n: heappush(heap,(nums1[i]+nums2[j+1],i,j+1))
        return ans
        

Explore

初始化小根堆时只加入(nums1[0]+nums2[0], 0, 0)是为了从最小可能的数对开始,并逐步扩展探索其他可能的数对。这种方法保证了算法的效率和简洁性,避免了一开始就加载过多的数对可能导致的冗余计算。从(nums1[0],nums2[0])开始,我们可以确保每次只扩展当前已知的最小数对,从而逐步覆盖所有可能的最小数对组合。

这样做是为了保证不会重复添加相同的数对组合。当我们从堆中取出一个元素(nums1[i], nums2[j])时,我们知道在(nums1[i], nums2[j+1])之前,所有的(nums1[i], nums2[x]) (其中x < j)已经被处理过。类似地,我们只在j为0时添加(nums1[i+1], nums2[j]),是因为这表示i行的开始,保证了每一对nums1中的元素与nums2中第一个元素的组合都会被考虑,而不会重复添加之前已经加入堆的组合。

这两种添加方式代表不同的探索方向。将(nums1[i+1]+nums2[j], i+1, j)添加到堆中是向下探索,即在固定列j的情况下,探索下一个行元素。而将(nums1[i]+nums2[j+1], i, j+1)添加到堆中是向右探索,即在固定行i的情况下,探索下一个列元素。这样做可以确保不遗漏任何可能的数对组合,同时避免重复添加相同的数对。

如果k值超过了所有可能的数对组合的数量,算法会继续执行,直到堆中没有更多元素可弹出为止。在这种情况下,算法将返回所有可能的数对组合,但总数会小于k。因此,算法的输出将是实际可能的数对的最大数量,而不是k个数对。