有序矩阵中第 K 小的元素

标签: 数组 二分查找 矩阵 排序 堆(优先队列)

难度: Medium

给你一个 n x n 矩阵 matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是 排序后 的第 k 小元素,而不是第 k不同 的元素。

你必须找到一个内存复杂度优于 O(n2) 的解决方案。

示例 1:

输入:matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8
输出:13
解释:矩阵中的元素为 [1,5,9,10,11,12,13,13,15],第 8 小元素是 13

示例 2:

输入:matrix = [[-5]], k = 1
输出:-5

提示:

  • n == matrix.length
  • n == matrix[i].length
  • 1 <= n <= 300
  • -109 <= matrix[i][j] <= 109
  • 题目数据 保证 matrix 中的所有行和列都按 非递减顺序 排列
  • 1 <= k <= n2

进阶:

  • 你能否用一个恒定的内存(即 O(1) 内存复杂度)来解决这个问题?
  • 你能在 O(n) 的时间复杂度下解决这个问题吗?这个方法对于面试来说可能太超前了,但是你会发现阅读这篇文章( this paper )很有趣。

Submission

运行时间: 24 ms

内存: 19.5 MB

class Solution:
    def kthSmallest(self, matrix: List[List[int]], k: int) -> int:
        n = len(matrix)

        def check(mid):
            i, j = n-1, 0
            cnt = 0
            while i >= 0 and j < n:
                if matrix[i][j] <= mid:
                    j += 1
                    cnt += i+1
                else:
                    i -= 1
            return cnt >= k

        left, right = matrix[0][0], matrix[n-1][n-1]
        while left < right:
            mid = (left + right) // 2
            if check(mid):
                right = mid
            else:
                left = mid + 1
        return left
            

Explain

这个题解采用了二分查找的方法。首先确定查找的范围是矩阵的最小值matrix[0][0]到最大值matrix[n-1][n-1]。然后在这个范围内进行二分查找,每次查找都通过check函数来判断矩阵中小于等于mid的元素个数是否大于等于k。如果是,说明第k小的元素在左半部分,否则在右半部分。最终当left和right相遇时,left即为所求的第k小的元素。

时间复杂度: O(nlog(matrix[n-1][n-1] - matrix[0][0]))

空间复杂度: O(1)

class Solution:
    def kthSmallest(self, matrix: List[List[int]], k: int) -> int:
        n = len(matrix)

        def check(mid):
            i, j = n-1, 0
            cnt = 0
            while i >= 0 and j < n:
                if matrix[i][j] <= mid:
                    j += 1
                    cnt += i+1
                else:
                    i -= 1
            return cnt >= k

        left, right = matrix[0][0], matrix[n-1][n-1]
        while left < right:
            mid = (left + right) // 2
            if check(mid):
                right = mid
            else:
                left = mid + 1
        return left

Explore

在二分查找中,选择左上角最小值和右下角最大值作为初始查找范围是因为在一个有序矩阵中,左上角的元素是整个矩阵中最小的,而右下角的元素是最大的。这样的选择确保了覆盖所有可能的值,从而能够通过二分查找有效地缩小查找范围来找到第K小的元素。

check函数中的遍历逻辑是基于矩阵的行和列都是递增排序的特性。从左下角开始遍历(即最底行的最左列),可以利用这一特性有效地计数。向右移动(增加列)时,元素递增,向上移动(减少行)时,元素递减。这样可以根据当前元素与mid的比较,快速地计算出矩阵中小于等于mid的元素数量,从而判断是否满足条件。

check函数通过从左下角到右上角的遍历方式,确保了所有小于等于mid的元素都被计入计数。即使矩阵中存在多个相同的元素,当这些元素小于或等于mid时,它们都会在遍历过程中被统计。每次向右移动都会增加当前行的全部元素(直到该行当前列的元素大于mid),因此所有小于等于mid的重复元素都会被正确统计。

在二分查找中,当check(mid)返回true,表示矩阵中小于等于mid的元素个数至少是k,因此mid有可能是第k小的元素。将right设置为mid而不是mid-1是为了确保不错过这个可能的解。如果设置为mid-1,可能会将实际的第k小元素排除在外。在最终的循环结束时,left和right会相遇在第k小的元素上。