力扣240——搜索二維矩陣


這道題主要是利用搜索二維矩陣本身的特性,找到其中的規律,就可以解決了。

原題

編寫一個高效的算法來搜索 m x n 矩陣 matrix 中的一個目標值 target。該矩陣具有以下特性:

  • 每行的元素從左到右升序排列。
  • 每列的元素從上到下升序排列。

示例:

現有矩陣 matrix 如下:

[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]

給定 target = 5,返回 true。

給定 target = 20,返回 false。

原題url:https://leetcode-cn.com/problems/search-a-2d-matrix-ii/

解題

這道題相比之前的二維矩陣,可能有序性沒有之前那么強,所以沒法直接拉成一個一維數組利用二分法查找,需要結合其特性,進行查找。

暴力解法

就是一行行、一列列慢慢找。假設是一個m * n的二維數組,那么時間復雜度就是O(mn),這個方法沒什么好說的,貼個代碼看看:

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[0].length; j++) {
                if (matrix[i][j] == target) {
                    return true;
                }
            }
        }

        return false;
    }
}

行列同時尋找

這是我自己想的方法,就是每次查找行列,只查每一行每一列最大值和最小值。根據這個二維數組的特性,找出可能存在 target 的行列的范圍,然后逐漸縮小,如果行列相同時,則開始遍歷尋找。

讓我們看看代碼:

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix.length == 0) {
            return false;
        }
        return search(0, matrix.length - 1, 0, matrix[0].length - 1, matrix, target);
    }

    private boolean search(
        int rowStart, int rowEnd,
        int colStart, int colEnd,
        int[][] matrix, int target) {
        
        if (rowStart == rowEnd || colStart == colEnd) {
            if (rowStart == rowEnd) {
                for (int i = colStart; i <= colEnd; i++) {
                    if (matrix[rowStart][i] == target) {
                        return true;
                    }
                }
            } else {
                for (int i = rowStart; i <= rowEnd; i++) {
                    if (matrix[i][colStart] == target) {
                        return true;
                    }
                }
            }
            return false;
        }
        
        // 是否有合適的行,默認沒有
        boolean has = false;
        // 新的rowStart、rowEnd
        int newRowStart = 0, newRowEnd = 0;
        // 篩選行
        for (int i = rowStart; i <= rowEnd; i++) {
            // 如果相等,說明找到了
            if (matrix[i][colStart] == target || matrix[i][colEnd] == target) {
                return true;
            }

            // target大於這一行的最大值,直接跳過
            if (target > matrix[i][colEnd]) {
                continue;
            }

            // target小於這一行的最小值,說明之后的就不用找了
            if (target < matrix[i][colStart]) {
                break;
            }

            // 如果是第一次找到
            if (!has) {
                has = true;
                newRowStart = i;
            }
            
            newRowEnd = i;
        }
        // 如果沒有找到
        if (!has) {
            return false;
        }

        has = false;
        int newColStart = 0, newColEnd = 0;
        // 篩選列
        for (int i = colStart; i <= colEnd; i++) {
            // 如果相等,說明找到了
            if (matrix[newRowStart][i] == target || matrix[newRowEnd][i] == target) {
                return true;
            }

            // target大於這一列的最大值,直接跳過
            if (target > matrix[newRowEnd][i]) {
                continue;
            }

            // target小於這一列的最小值,說明之后的就不用找了
            if (target < matrix[newRowStart][i]) {
                break;
            }

            // 如果是第一次找到
            if (!has) {
                has = true;
                newColStart = i;
            }
            
            newColEnd = i;
        }
        // 如果沒有找到
        if (!has) {
            return false;
        }

        return search(newRowStart, newRowEnd, newColStart, newColEnd, matrix, target);
    }
}

時間復雜度上,應該是比最基礎的暴力法好一些,但應該也是沒有本質區別。

行列同時二分查找

以行列總數中較小的那個數,選擇構成正方形的正對角線,每一次按照二分法,查找相應的行列,可以參考下面這張圖:

每次都會對行和列各用一次二分法,逐步排查。讓我們看看代碼:

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0) {
            return false;
        }

        // 利用二分法搜索
        // 尋找出行數、列數中較小的那個值,作為迭代次數
        int count = Math.min(matrix.length, matrix[0].length);

        int low, high, middle;
        // 迭代循環
        for (int i = 0; i < count; i++) {
            // 從(i,i)開始,尋找這一行是否有target
            low = i;
            high = matrix[0].length - 1;
            while (low <= high) {
                middle = (low + high) / 2;
                if (matrix[i][middle] == target) {
                    return true;
                }
                if (matrix[i][middle] > target) {
                    high = middle - 1;
                } else {
                    low = middle + 1;
                }
            }

            // 從(i,i)開始,尋找這一列是否有target
            low = i;
            high = matrix.length - 1;
            while (low <= high) {
                middle = (low + high) / 2;
                if (matrix[middle][i] == target) {
                    return true;
                }
                if (matrix[middle][i] > target) {
                    high = middle - 1;
                } else {
                    low = middle + 1;
                }
            }
        }

        return false;
    }
}

這個方法的時間復雜度為O(lg(n!))

這個算法產生的時間復雜度並不是特別明顯的是 O(lg(n!)) ,所以讓我們一步一步地分析它。
在主循環中執行的工作量逐漸最大,它運行 min(m,n)次迭代,其中 m 表示行數,n 表示列數。
在每次迭代中,我們對長度為 m-i 和 n-i 的數組執行兩次二分查找。因此,循環的每一次迭代都以 O(lg(m-i)+lg(n-i)) 時間運行,其中 i 表示當前迭代。
我們可以將其簡化為 O(2 lg(n-i))= O(lg(n-i)) ,在最壞的情況是 n≈m 。當 n≪m 時,n 將在漸近分析中占主導地位。通過匯總所有迭代的運行時間,我們得到以下表達式:
O(lg(n)+lg(n−1)+lg(n−2)+…+lg(1))

然后,我們可以利用對數乘法規則(lg(a)+lg(b)=lg(ab))將復雜度改寫為:

O(lg(n)+lg(n−1)+lg(n−2)+…+lg(1))
=O(lg(n⋅(n−1)⋅(n−2)⋅…⋅1))
=O(lg(1⋅…⋅(n−2)⋅(n−1)⋅n))
=O(lg(n!))

划分為四個二維數組

這是一種遞歸查找,同樣也是利用了這個二維搜索數組的特性。我們在當前矩陣中間的列上進行比較,如果小於 target 就繼續向下比較,直到找到比 target 大的數,此時遞歸查找左上和右上的矩陣。

以下面這張圖為例,我們假設target = 9,那么中間列找到 10 以后,發現比 9 大,因此尋找左上和右上兩個標紅的矩陣:

接下來我們看看代碼:

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0) {
            return false;
        }

        // 利用搜索二維矩陣的特性,划分為2個搜索矩陣進行遞歸搜索
        return searchRecursive(0, 0, matrix[0].length - 1, matrix.length - 1, matrix, target);
    }

    /**
     * 左上角和右下角的坐標,在這個二維矩陣中進行搜索
     */
    private boolean searchRecursive(
        int left, int up,
        int right, int down,
        int[][] matrix, int target) {
        // 此時矩陣的長度或者寬度為0,那么就沒有必要搜索了
        if (left > right || up > down) {
            return false;
        }
        // 如果target小於該矩陣的最小值(左上角)或者大於該矩陣的最大值(右下角),也沒有必要搜索了
        if (target < matrix[up][left] || target > matrix[down][right]) {
            return false;
        }

        // 利用列進行拆分
        int mid = (left + right) / 2;
        // 從上到下開始尋找
        int row = up;
        for (; row <= down; row++) {
            if (matrix[row][mid] == target) {
                return true;
            }

            // matrix[row][mid]作為一個標准點,如果小於target,則繼續往下一行尋找
            if (matrix[row][mid] < target) {
                continue;
            }

            // 如果matrix[row][mid]大於target,則停止增加
            break;
        }

        return searchRecursive(left, row, mid - 1, down, matrix, target) ||
               searchRecursive(mid + 1, up, right, row - 1, matrix, target);
    }
}

時間復雜度:O(nlgn)。分析如下:

單向尋找

結合該二維數組的特性,我們希望在進行比較的時候,只往一個方向尋找,這樣可以簡化查詢步驟。如果從左上(最小)開始尋找時,如果 target 比當前值大,我們不知道是該往右移還是往下移,右下(最大)同理。

此時我們可以換一種思路,從左下開始,因為比它小的數一定在它右邊,比它大的數一定在它右邊,這樣尋找的時候就簡單多了。

接下來我們看看代碼:

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0) {
            return false;
        }

        // 從左下角開始尋找,如果當前值大於target,則向上移動一行;如果當前值小於target,則向右移動一列。
        // 直到找到target,或者超出矩陣邊界

        // 列的最大值
        int colMax = matrix[0].length - 1;
        // 行列開始的下標
        int row = matrix.length - 1;
        int col = 0;

        while (row >= 0 && col <= colMax) {
            if (matrix[row][col] == target) {
                return true;
            }

            // 如果大於target,則向上移一行
            if (matrix[row][col] > target) {
                row--;
            }
            // 如果小於target,則向右一列
            else {
                col++;
            }
        }

        return false;
    }
}

時間復雜度:O(n+m)。如果和上面一樣,假設 n << m,那么就是 O(m),總的來說,還是比較高效的。

總結

以上就是這道題目我的解答過程了,不知道大家是否理解了。這道題目主要還是在於利用二維搜索數組的特性,完成解題。

有興趣的話可以訪問我的博客或者關注我的公眾號、頭條號,說不定會有意外的驚喜。

https://death00.github.io/

公眾號:健程之道


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM