有序矩阵中第k小的元素

题目描述

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

示例:

matrix = [ [ 1, 5, 9], [10, 11, 13], [12, 13, 15] ], k = 8,

返回 13。

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/kth-smallest-element-in-a-sorted-matrix 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

思路一

按大小遍历矩阵,遍历到第k个就是结果。bfs,维护最小堆保存下一步的最小值。

type Pos struct {
    x, y int
    val  int
}
func kthSmallest(matrix [][]int, k int) int {
    minHeap := make([]Pos, 0)
    vis := make([][]bool, len(matrix))
    for i := 0; i < len(matrix); i++ {
        vis[i] = make([]bool, len(matrix))
    }
    cnt := 0
    x, y := 0, 0
    for cnt < k {
        if len(minHeap) > 0 {
            minx := minHeap[0]
            cnt++
            if cnt >= k {
                return minx.val
            }
            if (minx.x+1 < len(matrix) && !vis[minx.x+1][minx.y]) && (minx.y+1 < len(matrix) && !vis[minx.x][minx.y+1]) {
                minHeap[0] = Pos{minx.x + 1, minx.y, matrix[minx.x+1][minx.y]}
                vis[minx.x+1][minx.y] = true
                adjustHeapSink(minHeap, 0)
                minHeap = append(minHeap, Pos{minx.x, minx.y + 1, matrix[minx.x][minx.y+1]})
                vis[minx.x][minx.y+1] = true
                adjustHeapFloat(minHeap, len(minHeap)-1)
            } else if minx.x+1 < len(matrix) && !vis[minx.x+1][minx.y] {
                minHeap[0] = Pos{minx.x + 1, minx.y, matrix[minx.x+1][minx.y]}
                vis[minx.x+1][minx.y] = true
                adjustHeapSink(minHeap, 0)
            } else if minx.y+1 < len(matrix) && !vis[minx.x][minx.y+1] {
                minHeap[0] = Pos{minx.x, minx.y + 1, matrix[minx.x][minx.y+1]}
                vis[minx.x][minx.y+1] = true
                adjustHeapSink(minHeap, 0)
            } else {
                minHeap[0], minHeap[len(minHeap)-1] = minHeap[len(minHeap)-1], minHeap[0]
                minHeap = minHeap[:len(minHeap)-1]
                adjustHeapSink(minHeap, 0)
            }
        } else {
            minHeap = append(minHeap, Pos{x, y, matrix[x][y]})
            vis[x][y] = true
        }
    }
    return minHeap[0].val
}
func adjustHeapSink(nums []Pos, k int) {
    tmp := nums[k]
    for i := k*2 + 1; i < len(nums); i = i*2 + 1 {
        if i+1 < len(nums) && nums[i+1].val < nums[i].val {
            i++
        }
        if nums[i].val < tmp.val {
            k, nums[k] = i, nums[i]
        }
    }
    nums[k] = tmp
}
func adjustHeapFloat(nums []Pos, k int) {
    tmp := nums[k]
    for i := (k - 1) / 2; i >= 0; i = (i - 1) / 2 {
        if nums[i].val > tmp.val {
            k, nums[k] = i, nums[i]
        }
        if i-1 < 0 {
            break
        }
    }
    nums[k] = tmp
}

思路二

二分枚举答案,下界为矩阵最小值(matrix(0,0)),上界为矩阵最大值(matrix(n-1,n-1))。对于每一个可能的答案v,在矩阵中统计小于等于v的数

关于二分枚举的答案是否在矩阵中的问题:

首先将问题转换一下,对于示例的数组,我们构建如下数组nums,nums[i]表示矩阵中小于等于i的个数

因此nums为[0,1,1,1,1,2,2,2,2,3,4,5,6,8,8,9]

对于查找第k小元素,问题变成在nums数组中查找第一个大于等于k的元素的位置。

  • 情况一:k在nums存在,这表示二分查找总能找到第一个等于k的元素的位置。并且我们统计的是在矩阵中统计小于等于v的数,因此一定存在一个数属于等于的情况。例如对于上面的nums,我们统计nums(12)=6,nums(13)=8,nums(14)=8,因此我们可以看出矩阵中等于13的数有2个,等于14的数有0个,因此在二分查找nums时,找第一个大于等于k=8的位置,找到的是13,在矩阵中是存在的。
  • 情况二:k在nums中不存在,这表示二分查找的结果位置是第一个大于k的位置。同理情况一,假设此时查找的k=7,因此二分查找找到的位置是第一个大于7的位置,即nums(13)=8。

我们统计的nums是在矩阵中统计小于等于v的数

简单来说,对于nums数组的统计结果,nums(i)-nums(i-1)等于数i在矩阵中出现的次数。由于二分查找找的是第一个大于等于k的数,因此只会找到恰好出现一次的数,比如6,或者多个相同数的第一个,比如8。因此连续相等的数后几位是不可能被查找到的,也就是出现次数为0的数不可能被查找到。

func kthSmallest(matrix [][]int, k int) int {
    l, r := matrix[0][0], matrix[len(matrix)-1][len(matrix)-1]
    var m int
    // 二分枚举答案,查找≥k的第一个元素。
    for l < r {
        m = (r-l)/2 + l
        if cnt(matrix, m) < k {
            l = m + 1
        } else {
            r = m
        }
    }
    return l
}
// 统计矩阵中≤val的个数
func cnt(matrix [][]int, val int) int {
    res, i, j := 0, 0, len(matrix)-1
    for j >= 0 {
        if matrix[i][j] <= val {
            i++
        }
        if i == len(matrix) || matrix[i][j] > val {
            res += i
            if i == len(matrix) {
                i--
            }
            j--
        }
    }
    return res
}