算法刷題筆記之二分法小結

近日刷了很多二分法相關的題目,想以一文簡單總結一下

1.二分查找經典框架

  • 1.相鄰就退出 start + 1<end
  • 2.求中點要防溢出
  • 3.三種情況分開寫
  • 4.雙重檢查更保險
 public static int binarySearch(int[] nums, int target) {
        // write your code here
        if (nums == null || nums.length == 0){
            return -1;
        }
        int start = 0;
        int end = nums.length - 1;
        //要素1.相鄰就退出 start + 1<end
        while (start + 1<end){

            // mid = (start +  end) /2 這種寫法在大多數情況下是對的
            // 但是如果 start = 2^31 -1 end = 2^32 - 1 明顯這會發生一個整數溢出
            // 要素2.求中點要防溢出
            int mid = (end - start)/2 + start;
            // 要素3.三種情況分開寫
            if (nums[mid] == target){
               return mid; //這裏的寫法要根據題目來 如果是找到並返回, 則此處可以直接返回
               // end = mid; //如果是找滿足target的第一個位置 則這裏繼續往前面找
               //start = mid; //如果是找滿足target的最後一個位置 則此處繼續往後找
            }else if(target < nums[mid]){
                //在左區間
                end = mid;
            }else {//在右區間
                start = mid;
            }
        }
        // 要素4.雙重檢查更保險
        if (nums[start] == target){
            return start;
        }
        if (nums[end] == target){
            return end;
        }
        return -1;
    }

2.First/Last Position

2.1First Position(lintcode14)

給定一排序數組,其中元素可能重複,給定一目標數,尋找該目標數在數組中首次出現的位置First Position

  • 非常清晰的二分法的應用,尋找最開始的,所以往最前面找
  • 關鍵點
     if (nums[mid] == target){
                //尋找最開始的位置 所以往前找
                end = mid;
                ...........
        //因爲找最開始的位置,所以先檢查前面的位置
       if (nums[start] == target){
            return start;
        }
        if (nums[end] == target){
            return end;
        }
    
  • 完整代碼
   */
    public static int binarySearch02(int[] nums, int target) {
        if (nums == null || nums.length == 0){
            return -1;
        }

        int start = 0;
        int end = nums.length - 1;
        //1.相鄰就退出
        while (start+1<end){
            //2.求中點 放溢出
            int mid = (end - start)/2 + start;
            //3.三種情況分別判斷
            if (nums[mid] == target){
                //尋找最開始的位置 所以往前找
                end = mid;
            }else if (target<nums[mid]){
                end = mid;
            }else {
                start = mid;
            }
        }
        //4.二次判斷更保險
        if (nums[start] == target){
            return start;
        }
        if (nums[end] == target){
            return end;
        }
        return -1;
    }

2.2First Bad Version(lintcode74)

尋找第一個壞的版本,思路和2.1幾乎完全一樣,,都是尋找第一個滿足條件的。需要注意的點也都一樣第一個壞的版本

  • 完整代碼
  public int findFirstBadVersion(int n) {
        // write your code here
        //可利用二分查找來解決
        //如果每次查找 中點是壞的 則往前找
        //如果每次查找 中點是好的 則往後找
        //這裏要注意這裏不是數組下標
        int start = 1;
        int end = n;
        while (start + 1 < end){
            int mid = (end - start)/2 + start;
            //如果中點對應版本是壞的 返回true 則繼續往前半部分找
            if (SVNRepo.isBadVersion(mid)){
                end = mid;
            }else {
                //如果中點對應版本是好的 返回false 則繼續往後半部分找
                start = mid;
            }
        }
        //二次判斷
        //從while中退出時 start 和 end一定是相鄰的
        //那麼顯然這兩個之中 一定有一個是壞的 但是題目求最近的那一個
        //所以先驗證start
        //如果不行 則一定是end
        if (SVNRepo.isBadVersion(start)){
            return start;
        }
        return end;
    }

2.3 Last Position(lintcode458)

和First Position剛好相反,尋找滿足添加的最後一個數。那關鍵的地方也正好相反。Last Position

  • 首先要注意,找到滿足添加的點後繼續往後半部分找
     int mid = (end - start)/2 + start;
    
            if (target == nums[mid]){
                //由於這裏是尋找 最後一個等於target的位置, 顯然應該儘可能的往後面找(這裏和First position的區別)
                //所以如果當前的mid = target的話 則繼續往後尋找
                start = mid;
            }
    
  • 其次就是最後判斷的時候先判斷end
     //因爲是找最後一個 所以先判斷後者
        if (nums[end] == target){
            return end;
        }
       if (nums[start] == target){
            //start 和 end一定相鄰,如果其中一個不等於目標 則最後的一個一定是start
           return start;
       }
       return -1;
    
  • 完整代碼
     public static int lastPosition(int[] nums, int target) {
        // write your code here
        if (nums == null || nums.length == 0){
            return -1;
        }
        int start = 0;
        int end = nums.length -1;
        while (start +1 < end){
            int mid = (end - start)/2 + start;
            if (target == nums[mid]){
                //由於這裏是尋找 最後一個等於target的位置, 顯然應該儘可能的往後面找(這裏和First position的區別)
                //所以如果當前的mid = target的話 則繼續往後尋找
                start = mid;
            }else if (target < nums[mid]){
                end = mid;
            }else {
                start = mid;
            }
        }
        //因爲是找最後一個 所以先判斷後者
        if (nums[end] == target){
            return end;
        }
       if (nums[start] == target){
            //start 和 end一定相鄰,如果其中一個不等於目標 則最後的一個一定是start
           return start;
       }
       return -1;
    }
    

3.尋找插入位置(leetcode35)

二分法的簡單應用,在給定數組中尋找給定的target,如果找到則返回位置,如果沒找到則返回該目標數應該插入的位置,插入後數組仍然有序。尋找插入位置

  • 1.首先兩種特殊情況的判斷,如果給定數小於數組的最小值和大於數組的最大值,則插入位置是固定的。

     //首先判斷兩種特殊的情況
        if(target < nums[0]){
            return 0;
        }
        if(target > nums[nums.length - 1]){
            return nums.length;
        }
    
  • 2.如果找到,則顯然可以直接返回位置

     int mid = (end - start)/2 + start;
            // 要素3.三種情況分開寫 循環內部不返回
            if (nums[mid] == target){
                return mid;
            }
    
  • 3.跳出while繼續判斷,此時start 和 end一定相鄰,先判斷start是否與之相等,如果不等,則end也不可能與之相等,則返回應該插入的位置,顯然應該是end的所在位置。

     if (target == nums[start] ){
            return start;
        }
        //這裏就是處理 在給定數組的中間尋找 找不到的情況 那麼target 一定是處於 start 和end之間
        //之間的(因爲相鄰即退出)那麼 要返回插入的位置 一定是end
        return end;
    
  • 4.完整代碼

          if (nums == null || nums.length == 0){
            return -1;
        }
        //首先判斷兩種特殊的情況
        if(target < nums[0]){
            return 0;
        }
        if(target > nums[nums.length - 1]){
            return nums.length;
        }
        int start = 0;
        int end = nums.length - 1;
        //要素1.相鄰就退出 start + 1<end
        while (start + 1<end){
            // mid = (start +  end) /2 這種寫法在大多數情況下是對的
            // 但是如果 start = 2^31 -1 end = 2^32 - 1 明顯這會發生一個整數溢出
            // 要素2.求中點要防溢出
            int mid = (end - start)/2 + start;
            // 要素3.三種情況分開寫 循環內部不返回
            if (nums[mid] == target){
                return mid;
            }else if(target < nums[mid]){
                //在左區間
                end = mid;
            }else {//在右區間
                start = mid;
            }
        }
        // 要素4.雙重檢查更保險
        //下面兩種if是能找到的情況 則正常返回
        if (target == nums[start] ){
            return start;
        }
        //這裏就是處理 在給定數組的中間尋找 找不到的情況 那麼target 一定是處於 start 和end之間
        //之間的(因爲相鄰即退出)那麼 要返回插入的位置 一定是end
        return end;
    }
    

4.區間查找(leetcode34)

給的數組和目標數,查找目標數在數組中的起始位置。
區間查找

  • 完全就是2.12.3的一個結合
  • 完整代碼
      public int[] searchRange02(int[] nums, int target) {
        int[] rs = {-1,-1};
        if (nums == null || nums.length == 0){
            return rs;
        }
        rs[0] = findFirst(nums, target);
        rs[1] = findLast(nums, target);
        return rs;
    }
    /**
     * 20190802
     * 找到滿足條件的第一個位置
     * @param nums
     * @param target
     * @return
     */
    public static int findFirst(int[] nums, int target){
        int start = 0;
        int end = nums.length - 1;
        while (start +1 <end){
            int mid = (end - start)/2 + start;
            if (target == nums[mid]){
                //往前找
                end = mid;
            }else if (target > nums[mid]){
                //往後找
                start = mid;
            }else {
                end = mid;
            }
        }
        if (target == nums[start]){
            return start;
        }
        if (target == nums[end]){
            return end;
        }
        return -1;
    }
    
    /**
     * 20190804
     * 找最後一個位置
     * @param nums
     * @param target
     * @return
     */
    public static int findLast(int[] nums, int target){
        int start = 0;
        int end = nums.length - 1;
        while (start +1 <end){
            int mid = (end - start)/2 + start;
            if (target == nums[mid]){
                //往後找
                start = mid;
            }else if (target > nums[mid]){
                //往後找
                start = mid;
            }else {
                end = mid;
            }
        }
        if (target == nums[end]){
            return end;
        }
        if (target == nums[start]){
            return start;
        }
        return -1;
    }
    

5.Search in a Big Sorted Array(lintcode458)

給定一個很大的升序正數數組,不能求長度,但是可以通過一個函數返回第k個數的值。要求在log(k)的時間內找到指定target的值

  • 不能求長度,二分法的end的初始值如何確定?
  • end的值可以不等於整個數組的長度,只要大於target即可
  • 那麼,只要我能找到一個index,它在數組中對應的值大於target,也就可以確定end的初始值
  • 所以,關鍵就在於如何在log(k)的時間內找到大於target的index
  • 倍增法:一次尋找 1 2 4 8 … 2n2^n
  • 只要確定了end的初始值,後面的則是一個普通的二分查找問題了
  • 完整代碼
    	 /**
     * 20190804
     * 顯然,這道題目需要二分法來解決
     * 但是 因爲不能求長度,而無法直接爲 end賦值
     * 其實 end的值 不一定是需要整個數組長度的
     * nums[end] 只要 > target即可
     * 所以關鍵點 如何在log(k)的時間內 能找到一個nums[end] 則成爲了關鍵
     * 這裏介紹一種“倍增”的思想 就是找1,找不到就找2 然後找4!!
     * @param reader: An instance of ArrayReader.
     * @param target: An integer
     * @return : An integer which is the index of the target number
     */
    public int searchBigSortedArray(ArrayReader reader, int target){
    
        //1利用倍增法 尋找end的最大值
        //即找到第一個 大於target 的 index值即可
        int index = 1;
        while (reader.get(index - 1) < target ){
            index = index * 2;
        }
    
        //只要找到了index 下面就是一個普通的二分查找了
        int start = 0;
        int end = index - 1;
    
        while (start + 1< end){
            int mid = (end - start)/2 + start;
    
            if (reader.get(mid) == target){
                //因爲這裏要找 First,所以繼續往前找 不能返回
                end = mid;
            }else if (target > reader.get(mid)){
                start = mid;
            }else {
                end = mid;
            }
        }
        if (reader.get(start) == target){
            return start;
        }
        if (reader.get(end) == target){
            return end;
        }
        return -1;
    }
    
    

6. Find Minimum in Rotated Sorted Array尋找旋轉排序數組中的最小值(leetcode153,lintcode159)

給定一旋轉的排序數組,要求找到其中的最小值。例如:Input: [3,4,5,1,2] Output: 1

  • 首先一個很直白的思路,就是一個for循環嘛,遍歷整個數組,只要找到第一個nums[i]>nums[i+1],則就是最小值,時間複雜度爲O(n)\mathcal{O}(n)
  • 如果想把一個O(n)\mathcal{O}(n)的繼續做優化的話,只能到O(logn)\mathcal{O}(logn),而且幾乎只能用二分法
  • 這道題還非常明顯的告知了有序數組,所以幾乎應該用二分來做了
    在這裏插入圖片描述
  • 這道題的結構,可以用這個圖很清晰的表達出來,顯然A點和B點的值都是可以獲取的A = nums[0],B=nums[nums.length-1]
  • C點的值明顯就是最小值,那如何求得C點的值呢?
  • 二分法中一定會涉及到一個目標數,這裏也一樣,只是這裏要求的是小於等於目標數的第一個位置上的數,取等號是因爲可能原數組只旋轉一位,則原來最小的第一位跑到了最後一位
  • 所以這裏選B作爲目標數,尋找<=B的First position即可
  • 後面用二分法解即可
  • 完整代碼
  public static int findMin(int[] nums) {
        if(nums == null || nums.length == 0){
            return -1;
        }

        //定義 target
        int target = nums[nums.length-1];

        //下面的工作就變成尋找 小於target的第一個數
        //當然也是用二分法尋找

        int start = 0;
        int end = nums.length - 1;

        while (start + 1 < end){
            int mid = (end - start)/2 + start;
            //如果當前終點位置 的值 小於等於 target
            // 則當前的mid 一定在旋轉後數組 的後一段了
            // 因爲數組是旋轉過的 target一定是 後一段的最大值
            // 當前的mid位置 小於 target則一定是在後半段了(前半段的值一定都大於target)
            // 爲了找到小於target的第一個位置 則繼續往前找
            if (nums[mid] <= target ){
                end = mid;
            }else {
                //如果 當前中點的值 大於target 則顯然小於target的值肯定還在後面
                // 這是因爲數組是旋轉過的 target一定是 後一段的最大值
                // 當前還有比target大的 則說明當前的位置一定在前一段 所以繼續往後找
                start = mid;
            }
        }
        if (nums[start] <= target){
            return nums[start];
        }
        //給定排序數組中的最小值是一定存在的
        return nums[end];

    }

7.

8.搜索二維矩陣(leetcode74)

給定一二維數組,其中每一行從左到右是升序,而且下一行的第一個數大於上一行的最後一個數,也就是按每一行排列,整個二維數組的值都是升序

  • 本質還是二分法的應用,關鍵在於把二維的數組轉換爲一維數組,然後進行二分查找

8.1解法一:先確定目標數所在行,然後在該行進行二分查找

   /**
     * 20190805(自解AC)
     * 關鍵在於如何把2D轉換爲1D 然後進行二分查找
     * @param matrix 給定矩陣
     * @param target 給定搜索的數
     * @return
     */
    public static boolean searchMatrix(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0){
            return false;
        }
        int row = matrix.length;
        int col = matrix[0].length;

        int target_row = 0;
        for (int i=0;i<row; i++){
            //按行 遍歷 找到target所在的行
            if (target>=matrix[i][0] && target<=matrix[i][col-1]){
                target_row = i;
            }
        }
        //這裏已經把2D轉換爲了1D直接 利用二分查找即可
        //對target_row 行 進行二分查找
        int start = 0;
        int end = col - 1;

        while (start+1<end){
            int mid = (end - start)/2 + start;

            if (matrix[target_row][mid] == target ){
                return true;
            }else if (matrix[target_row][mid] < target){
                start = mid;
            }else {
                end = mid;
            }
        }

        if (target == matrix[target_row][start]){
            return true;
        }
        if (target == matrix[target_row][end]){
            return true;
        }

        return false;

    }

8.2解法2

  • 和解法1沒有本質區別
  • 關鍵在於一個知識點:在一個m*n的數組中,第k個數的位置是[k/n][k%n]
  • 又因爲整個數組的數按行排列起來都升序的,因此可以直接在整個數組中進行二分查找
 /**
     * 20190805
     * 參考:https://yisuang1186.gitbooks.io/-shuatibiji/search_a_2d_matrix.html
     * 其實和我的解法 沒有本質上的差別,仍然是把2D轉換爲1D
     * 但是關鍵的地方在於如和轉換 這裏的轉換方法感覺比我的優雅
     * 給定 m*n的數組 第k個數 在其中的位置爲 [k/n][k%n]
     * 知道了這個 就可以查找了
     * @param matrix
     * @param target
     * @return
     */
    public static boolean searchMatrix02(int[][] matrix, int target){
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0){
            return false;
        }
        int row = matrix.length;
        int col = matrix[0].length;

        int start = 0;
        int end = row * col - 1;

        while (start + 1 < end){
            int mid = (start) + (end - start)/2;

            if (matrix[mid/col][mid%col] == target){
                return true;
            }else if (matrix[mid/col][mid%col] < target){
                start = mid;
            }else {
                end = mid;
            }
        }

        if (matrix[start/col][start%col] == target){
            return true;
        }
        if (matrix[end/col][end%col] == target){
            return true;
        }

        return false;
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章