在这里插入图片描述
在这里插入图片描述

摘要

在开发中,我们经常遇到需要处理大规模有序数据的场景,比如数据库分页、排行榜查询、或者处理排序过的矩阵。LeetCode 第 378 题“有序矩阵中第 K 小的元素”就是这样一个经典问题:它要求我们在一个行列都排好序的矩阵中,快速找到第 K 小的元素。本文将结合代码、算法思路和实际场景,带大家拆解这个题目的解法。

描述

题目要求我们在一个 n × n 的有序矩阵 中找到第 K 小的元素。矩阵的特点是:

  • 每一行是按升序排列的;
  • 每一列也是按升序排列的。

换句话说,矩阵不仅仅是二维数组,它在行和列两个维度上都保持有序。
我们需要返回排序后第 K 小的那个元素,而不是第 K 个不同的元素。

比如:

输入:matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8
输出:13
解释:矩阵展开后是 [1,5,9,10,11,12,13,13,15],第 8 小的数是 13

这个问题在实际开发中也很常见,比如:

  • 在数据库表中进行多字段排序后,查找某个偏移量的数据;
  • 在分布式日志系统中,根据时间和序号找到某条具体日志;
  • 在推荐系统里,找到用户兴趣排序中的第 K 个候选项。

题解答案

针对这个问题,有几个常见的思路:

  1. 最直接的做法:把矩阵“扁平化”,拉成一个数组,然后排序,再取第 K 个。

    • 时间复杂度:O(n² log n²),太大。
    • 空间复杂度:O(n²),内存开销也大。
  2. 利用小顶堆(最小堆):每一行都是有序的,所以我们可以把每行的第一个元素丢进最小堆,每次弹出最小值,再把该元素所在行的下一个元素压入堆。这样循环 K 次,就能得到第 K 小的元素。

    • 时间复杂度:O(k log n)。
  3. 二分查找 + 计数:利用矩阵整体有序的性质,在数值范围内进行二分搜索,统计小于等于 mid 的元素个数。根据计数结果收缩区间,直到找到第 K 小的数。

    • 时间复杂度:O(n log(max-min)),比堆解法更节省内存。

这里我们选择 二分查找的解法,因为它更高效,也符合题目“内存复杂度优于 O(n²)”的要求。

题解代码分析

下面是 Swift 的实现代码,使用二分查找:

import Foundation

class Solution {
    func kthSmallest(_ matrix: [[Int]], _ k: Int) -> Int {
        let n = matrix.count
        var left = matrix[0][0]
        var right = matrix[n - 1][n - 1]
        
        func countLessEqual(_ mid: Int) -> Int {
            var count = 0
            var row = n - 1
            var col = 0
            
            // 从左下角开始统计 <= mid 的元素个数
            while row >= 0 && col < n {
                if matrix[row][col] <= mid {
                    count += row + 1
                    col += 1
                } else {
                    row -= 1
                }
            }
            return count
        }
        
        // 二分查找
        while left < right {
            let mid = left + (right - left) / 2
            if countLessEqual(mid) < k {
                left = mid + 1
            } else {
                right = mid
            }
        }
        
        return left
    }
}

代码解析

  1. leftright 分别是矩阵中的最小值和最大值,作为二分查找的边界。

  2. countLessEqual(mid) 用来统计矩阵中小于等于 mid 的元素个数。这里从 左下角 开始走,效率高。

    • 如果当前元素 ≤ mid,那么这一列到当前行的所有数都 ≤ mid,所以 count += row + 1
    • 如果当前元素 > mid,就往上一行走。
  3. 在二分查找过程中,如果统计结果 < k,说明 mid 太小,需要右移;否则收缩右边界。

  4. 最终 left 就是第 K 小的数。

示例测试及结果

let solution = Solution()

let matrix1 = [[1,5,9],[10,11,13],[12,13,15]]
print(solution.kthSmallest(matrix1, 8))  
// 输出: 13

let matrix2 = [[-5]]
print(solution.kthSmallest(matrix2, 1))  
// 输出: -5

输出结果

13
-5

这和题目示例完全一致,说明算法是正确的。

时间复杂度

二分查找在区间 [min, max] 上进行,区间大小是数值范围,复杂度是 O(log(max-min))
每次统计时,需要遍历一行或一列,总共 O(n)。
因此总体复杂度是:
O(n log(max-min))

对于 n = 300 的规模,这个复杂度是可以接受的。

空间复杂度

整个算法只使用了几个变量,没有额外的存储结构,空间复杂度是:
O(1)

相比直接展开矩阵存储,节省了大量内存,非常适合大规模数据。

总结

这道题考察的关键点在于 如何利用矩阵的有序性

  • 如果直接暴力排序,肯定超时或内存爆炸。
  • 用堆可以解决,但还是会有 O(n) 的额外存储。
  • 最优解是二分查找 + 计数,既利用了矩阵的有序性,又避免了额外存储。

在实际业务中,这个思路同样适用:

  • 处理数据库分页查询时,可以用“数值二分 + 索引统计”来代替大规模排序。
  • 在大规模日志或指标矩阵中查找排名时,也可以用这个思路快速定位。

所以,这道题不仅是算法练习,更是一种 高效思维方式 的锻炼。

Logo

加入「COC·上海城市开发者社区」,成就更好的自己!

更多推荐