LintCode 401. Kth Smallest Number in Sorted Matrix (堆经典好题!!!)

本文探讨了在排序矩阵中寻找第K小元素的有效算法,包括使用最大堆、最小堆和二分法等多种方法,详细解析了不同算法的时间复杂度及适用场景。
  1. Kth Smallest Number in Sorted Matrix
    Find the kth smallest number in a row and column sorted matrix.

Example
Given k = 4 and a matrix:

[
[1 ,5 ,7],
[3 ,7 ,8],
[4 ,8 ,9],
]
return 5

Challenge
Solve it in O(k log n) time where n is the bigger one between row size and column size.

解法1:用最大堆。即不管三七二十一,将矩阵元素塞进最大堆中。然后只要堆的size > k,就pop(),此时pop的是当前堆的最大元素。显然,前面mn-k大的元素都被pop掉了(一共pop了mn-k次),剩下的元素一共k个,即k个最小的元素,其中top即为第k小的元素。
这个方法简单,但是没有用上matrix行列都有序这个条件。时间复杂度O(MNlogMN)。
代码如下:

class Solution {
public:
    /**
     * @param matrix: a matrix of integers
     * @param k: An integer
     * @return: the kth smallest number in the matrix
     */
    int kthSmallest(vector<vector<int>> &matrix, int k) {
        
        priority_queue<int> pq;
        int nRow = matrix.size();
        int nCol = matrix[0].size();
        
        for (int i = 0; i < nRow; ++i) {
            for (int j = 0; j < nCol; ++j) {
                pq.push(matrix[i][j]);
                if (pq.size() > k) pq.pop();
            }
        }
        
        return pq.top();
    }
};

解法2:用最大堆。
这个方法可以用于行列皆没有排序的matrix。该方法不是最优,因为没有用上每列都有序这个条件。

注意:如果没有行列皆排序这个条件,那么,求第K大的数我们应该用最小堆,求第K小的数我们应该用最大堆。
该方法的时间复杂度为O(MNlogM)。

class Solution {
public:
    int kthSmallest(vector<vector<int>>& matrix, int k) {
        int n = matrix.size();
        if (n == 0) return 0;
        int m = matrix[0].size();
        priority_queue<int> maxHeap;
        vector<int> pos(n, 0);
        int count = 0, total_count = m * n;
        while (count < total_count) {
            for (int i = 0; i < n; ++i) {
                int candidate = matrix[i][pos[i]];
                if (count < k) {
                    maxHeap.push(candidate);
                    pos[i]++;
                }
                else if (candidate < maxHeap.top()) {
                    maxHeap.push(candidate);
                    maxHeap.pop();
                    pos[i]++;
                }
                count++;
            }
        }
        return maxHeap.top();
    }
};

解法3:用最小堆。
思路跟解法1有点像,但是反过来:
解法1是踢掉前MN-K个最大的值,用maxheap。
解法3是踢掉前K-1个最小的值,用minHeap。

我们建最小堆,先放matrix[0][0],即最小的那个元素,然后将其pop(),再把它的正下方和右边的那个元素放进堆,因为这两个肯定是仅比top大的那两个最小的元素。如此反复,一共pop掉k-1次,并且每次pop掉top后都把它正下方和右方的元素加进来。
显然,k-1个最小的元素都被pop掉了,现在堆的top是第k小的元素。
时间复杂度应该是klogk。

struct Elem {
    int r;
    int c;
    int v;
    Elem(int row = 0, int col = 0, int val = 0) : r(row), c(col), v(val) {}

};

struct cmp {
    bool operator() (const Elem & a, const Elem &b) {
        return (a.v > b.v);
    }
};

class Solution {
public:
    /**
     * @param matrix: a matrix of integers
     * @param k: An integer
     * @return: the kth smallest number in the matrix
     */
    int kthSmallest(vector<vector<int>> &matrix, int k) {
        
        priority_queue<Elem, vector<Elem>, cmp> pq;  //min-heap
        int nRow = matrix.size();
        int nCol = matrix[0].size();
        
        pq.push(Elem(0, 0, matrix[0][0]));
        vector<vector<int>> visited(nRow, vector<int>(nCol, 0));
        visited[0][0] = 1;
        
        for (int i = 0; i < k - 1; ++i) {
            Elem elem = pq.top();  //totally popped k - 1 elements
            pq.pop();
            
            int newR = elem.r + 1;
            if (newR < nRow && !visited[newR][elem.c]) {
                pq.push(Elem(newR, elem.c, matrix[newR][elem.c]));
                visited[newR][elem.c] = 1;
            }
            
            int newC = elem.c + 1;
            if (newC < nCol && !visited[elem.r][newC]) {
                pq.push(Elem(elem.r, newC, matrix[elem.r][newC]));
                visited[elem.r][newC] = 1;
            }
        }
        
        return pq.top().v;
    }
};

二刷:
注意:重要!
visited[][]一定要设计成nRow和nCol的二维矩阵,不要设计成set visited。
否则

  1. 如果operator<()里面是return val > a.val; 那么同值的不同Node进set两次,会被判断成相同的两个节点
    以输入
    [[1,5,9],[10,11,13],[12,13,15]]
    8
    为例,里面的两个13会判断成为相同的两个节点。
  2. 如果operator<()里面是return val >= a.val; 那么同一个Node进set两次,会被判断成不一样的两个节点。

代码如下:

struct Node {
    int row;
    int col;
    int val;
    Node(int x, int y, int v) : row(x), col(y), val(v) {}
    bool operator < (const Node & a) const {
        return val > a.val;
    }
};

class Solution {
public:
    int kthSmallest(vector<vector<int>>& matrix, int k) {
        int nRow = matrix.size();
        int nCol = matrix[0].size();
        priority_queue<Node> minHeap;
        vector<vector<int>> visited(nRow, vector<int>(nCol, 0));
        Node node = Node(0, 0, matrix[0][0]);
        minHeap.push(node);
        visited[0][0] = 1;
        int index = 1;
        while (!minHeap.empty()) {
            Node topNode = minHeap.top();
            minHeap.pop();
            if (index == k) return topNode.val;
            if (topNode.row + 1 < nRow) {
                Node newNode = Node(topNode.row + 1, topNode.col, matrix[topNode.row + 1][topNode.col]);
                if (visited[topNode.row + 1][topNode.col] == 0) {
                    minHeap.push(newNode);
                    visited[topNode.row + 1][topNode.col] = 1;
                }
            }
            if (topNode.col + 1 < nCol) {
                Node newNode = Node(topNode.row, topNode.col + 1, matrix[topNode.row][topNode.col + 1]);
                if (visited[topNode.row][topNode.col + 1] == 0) {
                    minHeap.push(newNode);
                    visited[topNode.row][topNode.col + 1] = 1;
                }
            }
            index++;
        }
        return -1;
    }
};

解法4:还是最小堆,可以直接
用LintCode 1874: Kth Smallest Element in a Specific Array中的做法,
https://blog.youkuaiyun.com/roufoo/article/details/105308293
一开始把每个有效行的头元素放进堆,然后每次pop后取pop节点的右节点。该算法时间复杂度是O(klogM) M为有效行数。有效是指该行不为空。这个算法没用到列也有序这个条件。
那么我们可以看出,如果M<k时,似乎该算法更好?

代码如下:

struct Node {
    int x;
    int y;
    int v;
    Node(int _x = 0, int _y = 0, int _v = 0) : x(_x), y(_y), v(_v) {}
    bool operator < (const Node & a) const {
        return v > a.v;
    }
};
 
class Solution {
public:
    /**
     * @param arr: an array of integers
     * @param k: an integer
     * @return: the Kth smallest element in a specific array
     */
    int kthSmallest(vector<vector<int>> &arr, int k) {
        int rowNum = arr.size();
        priority_queue<Node> pq;
        
        for (int i = 0; i < rowNum; ++i) {
            if (arr[i].size() > 0) {
                pq.push(Node(i, 0, arr[i][0]));
            }
        }
        int count = 0;
        while(!pq.empty()) {
            Node top = pq.top();
            pq.pop();
            count++;
            if (count == k) return top.v;
            if (top.y < arr[top.x].size() - 1) {
                pq.push(Node(top.x, top.y + 1, arr[top.x][top.y + 1]));
            }
        }
        return -1;
    }
};

解法5:二分法。下次做。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值