• [LintCode] Kth Smallest Number in Sorted Matrix


    一个行和列都有序的矩阵,找出其中第k小的元素。
    问题链接: http://www.lintcode.com/en/problem/kth-smallest-number-in-sorted-matrix/

    Brute Force

    最暴力的方式是把矩阵里所有元素存到一个数组里,然后对数组排序后输出第k个元素。复杂度O(N + Nlog(N)) (其中N是矩阵所有元素个数)。C++代码:

    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) {
            int n = matrix.size(), m = matrix[0].size();
            vector<int> myvec(n*m);
            
            int num = 0;
            for (int i = 0; i < n; ++i) {
                for (int j = 0; j < m; ++j) {
                    myvec[num++] = matrix[i][j];
                }
            }
            sort(myvec.begin(), myvec.end());
            return myvec[k-1];
        }
    };
    

    在LintCode/LeetCode上,这个非常暴力的算法能够得到Accepted

    Merge Sorted Arrays

    显然,上面的算法不是一个优秀的算法,因为它不但复杂度高,而且完全没有用到矩阵有序这一条件。如果继续按上面的思路,利用矩阵每一行和每一列都是有序的条件,可以采用合并排序数组的方式,这样就可以省去最后的排序操作。

    我们知道合并两个排序数组可以用归并排序里的归并操作,时间复杂度是O(两个数组的长度之和)。如果是n个排序数组呢?当然可以依次合并:先将第1个数组和第2个数组合并,再将合并后的数组与第3个数组合并,以此类推,直到合并完所有数组。分析一下这么做的复杂度(设一共有n个待合并的排序数组,每个数组的长度均为m,其实长度不等也没关系,这里假设它们长度相等是为了便于分析):

    • 第1轮,合并数组1和数组2,得到长度为2*m的新数组x,时间复杂度O(m+m) = O(2*m)
    • 第2轮,合并数组x和数组3,得到长度为3*m的新数组x,时间复杂度O(2m+m) = O(3*m)
    • ...
    • 第n-1轮,合并数组x和数组n,得到长度为n*m的新数组x,时间复杂度O(n*m)

    所以总复杂度是O(2*m) + O(3*m) + ... + O(n*m) = O(n*n*m)。不能说它就一定比上一个方法好。可见单纯地依次合并不是好的做法,所以选择合并的方式是很重要的。其实可以在每一轮两两合并,仔细思考就会发现这样的合并方式是二叉树形的——第1轮合并n/2次,第2轮合并n/4次,...,每一轮合并的次数是上一轮的一半,不难分析其复杂度是O(log(n)*n*m)。C++代码:

    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) {
            int n = matrix.size(), m = matrix[0].size();
            
            int num = n / 2;
            if (n > 1 && n % 2 != 0) matrix[n-2] = mergeSortedArray(matrix[n-2], matrix[n-1]);
            while (num > 0) {
                for (int i = 0; i < num; ++i) {
                    matrix[i] = mergeSortedArray(matrix[2*i], matrix[2*i+1]);
                }
                if (num > 1 && num % 2 != 0) matrix[num-2] = mergeSortedArray(matrix[num-2], matrix[num-1]);
                num /= 2;
            }
            return matrix[0][k-1];
        }
        
        /**
         * @param A and B: sorted integer array A and B.
         * @return: A new sorted integer array
         */
        vector<int> mergeSortedArray(vector<int> &A, vector<int> &B) {
            if (A.empty()) return B;
            if (B.empty()) return A;
            
            int n = A.size(), m = B.size();
            vector<int> mergeArr(n + m);
            
            int p = 0, q = 0, k = 0;
            while (p != n && q != m) {
                if (A[p] < B[q]) mergeArr[k++] = A[p++];
                else mergeArr[k++] = B[q++];
            }
            while (p != n) mergeArr[k++] = A[p++];
            while (q != m) mergeArr[k++] = B[q++];
            return mergeArr;
        }
    };
    

    上面的代码在LintCode和LeetCode上都通过了测试。其中函数mergeSortedArray是合并两个排序数组的接口。由于在每一轮中,需要两两合并的数组并不总是偶数对的,所以应该将最后「落单」的那个数组考虑进去,在上面的代码中,处理方法就是将最后落单的数组与它前一个数组合并(两个if语句)。

    Min Heap

    考虑这样一种情形:矩阵很大,而k很小(比如,k就等于1)。这时将所有的数组合并就显得代价太大了。我们能不能实现这样的操作:第1次找到最小的元素,第2次找到第二小的元素,...,第k次找到第k小的元素?以下面的矩阵为例,我们看看要实现这个想法需要哪些条件。

      1, 2, 4, 7
      3, 5, 6, 9
      5, 8, 7, 12
    

    这个矩阵满足题目条件,即每一行、每一列都是有序的。显然,最小的元素位于矩阵的左上角,即(0,0)处,而第2小的元素则一定是最小元素的下边和右边即(0,1)和(1,0)的其中一个,在本例中,是位于(0,1)处的2。对于第3小的元素,就需要看2下边和右边的元素,以及之前未被选中的位于(1,0)处的3。我们发现2下边的元素没有必要考虑,因为它在3的右边,所以只需要考虑2右边的4,如此进行下去,直到找到第k小的。

    通过上面的简单例子可见,我们的想法可以实现,但是需要维护一些信息。每挑出一个第i小的元素后,就会有一些新的待考察元素出现(第i小元素的下边和右边),在选择第(i+1)小的元素时,还要同时考察之前出现的、且未被选中的元素,然后在这些待考察元素中选最小的。有一种数据结构可以帮助我们更好地组织这种操作——最小堆(min heap)。

    注意第i小元素的下边或右边的元素可能无需考察(它比那些前几轮滞留下的元素大),为了避免重复和不必要的比较,可以选定一个方向,比如统一按横向或者纵向扩展。这里我们选择横向。在初始化时,将第一列的所有元素加入最小堆(如果按纵向,就将第一行的所有元素入堆)。然后我们执行下面的操作k次:

    • 取堆顶元素,将其位置右侧的元素插入最小堆
    • 将堆顶元素出堆

    第i次出堆的元素就是第i小的元素。在具体实现时,由于还要插入和堆顶节点相邻的元素,所以堆节点还要记录元素的位置。

    我们可以用优先队列(priority queue)来维护最小堆。这里为了更好地展示代码思路,采用C++ STL中的priority_queue

    // 堆节点定义
    struct HeapNode {
        int val;    // 元素值
        int row;    // 行标号
        int col;    // 列标号
    };
    
    // 重载元素比较规则
    struct cmp {
        bool operator()(HeapNode a, HeapNode b) {
            return a.val > b.val;
        }
    };
    
    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) {
            int nRow = matrix.size(), nCol = matrix[0].size();
            
            // 创建一个min heap, 初始化为矩阵的第一列
            priority_queue<HeapNode, vector<HeapNode>, cmp> q;
            for (int i = 0; i < nRow; ++i) {
                HeapNode node = {matrix[i][0], i, 0};
                q.push(node);
            }
            
            // 取堆顶元素,将其右侧元素插入min heap,之后出堆,做k次
            HeapNode popNode;
            for (int i = 0; i < k; ++i) {
                popNode = q.top(); q.pop();
                if (popNode.col < nCol - 1) {
                    HeapNode newNode = {matrix[popNode.row][popNode.col + 1], popNode.row, popNode.col + 1};
                    q.push(newNode);
                }
            }
            return popNode.val;
        }
    };
    

    上面算法的时间复杂度:设矩阵有n行m列。

    • 堆的初始化:O(n)
    • 取堆顶是O(1),在k次操作中,每一次至少出堆一个节点,所以每次插入操作的最坏时间复杂度不会超过O(log(n)),于是k次操作的复杂度不会超过O(k log(n))
    • 所以总的时间复杂度是O(n + k log(n))

    当然,对于堆结构和其各种操作,还是建议学习并能够自己实现。

    References

    [1] Merge K Sorted Arrays, http://algorithms.tutorialhorizon.com/merge-k-sorted-arrays/

  • 相关阅读:
    使用Xcode和Instruments调试解决iOS内存泄露(转载) sansan
    GCD介绍(二): 多核心的性能(转载) sansan
    iphone 常用的<app>info.plist设置(转载) sansan
    GCD介绍(一): 基本概念和Dispatch Queue (转载) sansan
    iOS 证书与签名 解惑详解[转] sansan
    Xcode快捷键 sansan
    GCD介绍(三): Dispatch Sources(转载) sansan
    Apple开发者授权 sansan
    MFC中Spin control的使用
    c++ windows mobile中设置菜单活动与否
  • 原文地址:https://www.cnblogs.com/ilovezyg/p/6940686.html
Copyright © 2020-2023  润新知