LeetCode入門指南 之 二分搜尋

WINLSR 發表於 2021-09-01
LeetCode

img

上圖表示常用的二分查詢模板:

第一種是最基礎的,查詢區間左右都為閉區間,比較後若不等,剩餘區間都不會再包含mid;一般在不需要確定目標值的邊界時,用此法即可。

第二種查詢區間為左閉右開,要確定target左邊界時,若nums[mid] == target,取right = mid

int left = 0; 
int right = arr.length;	//注意

while (left < right) {	//注意
    //相比 mid = (left + right) / 2的寫法可以防止越界
    int mid = left + (right - left) / 2; 
    if (arr[mid] == target) {
        //向左查詢邊界
        right = mid;
    } else if (arr[mid] < target) {
        left = mid + 1;
    } else if (arr[mid] > target) {
        right = mid;
    }   
}

/**
  * 跳出迴圈時,left=right;left對應元素可能為target。
  * 當target若大於所有元素,退出迴圈時有left=right=nums.length。故最後需要判斷left是否越界以及left對應元素是否為target
  */
if (left >= arr.length) return -1;		//target比所有元素都大(沒找到),此時表示小於target的元素有left個
if (arr[left] != target) return -1;	//(沒找到)此時表示小於target的元素有left個

return left;	//left為target的左邊界,表示小於target的元素有left個。可知,若沒找到target,left為target該順序插入的位置。

要確定target右邊界時,若nums[mid] == target,取left = mid + 1:

int left = 0; 
int right = arr.length; //注意

while (left < right) {	//注意
    int mid = left + (right - left) / 2;
    if (arr[mid] == target) {
        //向右邊界查詢
        left = mid + 1;
    } else if (arr[mid] < target) {
        left = mid + 1;
    } else if (arr[mid] > target) {
        right = mid;
    }
}
/**
  * 跳出迴圈時,left=right;left - 1 對應元素可能為target(比如:只有一個target時,mid指向target,下一步會將left置為mid + 1);
  * 當target小於所有元素時有left=right=0;故最後需要判斷left-1是否越界和left-1對應元素是否為target
  */
if (left - 1 < 0) return -1;
if (arr[left - 1] != target) return -1;

return left - 1;

為什麼left = mid + 1right = mid? 這是因為我們的查詢區間始終是保持為左閉右開

重要的來了,如果前面兩種你覺得麻煩不好記憶那麼你只需要記住第三種即可第三種最為強大,查詢區間左右都為閉區間,比較後若不等,剩餘區間都再包含mid,最後退出時leftright相鄰,故都有可能為target。理論上絕大部分場景第三種模板都能解決。

int left = 0; 
int right = arr.length - 1;

while (left + 1 < right) {
    int mid = left + (right - left) / 2;
    if (arr[mid] == target) {
        //向右邊界查詢;向左邊界查詢改為 right = mid;
        left = mid;
    } else if (arr[mid] < target) {
        left = mid;
    } else if (arr[mid] > target) {
        right = mid;
    }
}

//判斷結果
if (arr[left] == target) {
    return left;
}
if (arr[right] == target) {
    return right;
}

return -1;

61 · 搜尋區間

給定一個包含 n 個整數的排序陣列,找出給定目標值 target 的起始和結束位置。

如果目標值不在陣列中,則返回[-1, -1]

使用模板二:

public class Solution { 
    public int[] searchRange(int[] arr, int target) {
        int left = 0;
        int right = arr.length; //注意左閉右開

        while (left < right) {  //注意
            int mid = left + (right - left) / 2;

            if (arr[mid] == target) {
                //向左查詢邊界
                right = mid;
            } else if (arr[mid] < target) {
                left = mid + 1;
            } else if (arr[mid] > target) {
                right = mid;
            }
        }

        int start = 0;
        if (left >= arr.length) {
            return new int[] {-1, -1};      //沒找到
        } else if (arr[left] != target) {
            return new int[] {-1, -1};      //沒找到
        } else if (arr[left] == target) {
            start = left;
        }

        left = 0;
        right = arr.length;

        while (left < right) {
            int mid = left + (right - left) / 2;

            if (arr[mid] == target) {
                //向右邊界查詢
                left = mid + 1;
            } else if (arr[mid] < target) {
                left = mid + 1;
            } else if (arr[mid] > target) {
                right = mid;
            }
        }

        //這裡無需判斷是因為查詢左邊界時已經確定了target存在
        int end = left - 1;	//注意

        return new int[] {start, end};
    }
}

你能嘗試用模板三解決嗎?

35. 搜尋插入位置

給定一個排序陣列和一個目標值,在陣列中找到目標值,並返回其索引。如果目標值不存在於陣列中,返回它將會被按順序插入的位置。

你可以假設陣列中無重複元素。

使用模板三:

class Solution {
    public int searchInsert(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;

        while (left + 1 < right) {
            int mid = left + (right - left) / 2;

            if (nums[mid] < target) {
                left = mid;
            } else if (nums[mid] > target) {
                right = mid;
            } else if (nums[mid] == target) {
                left = mid;  // 查詢左邊界
            }
        }
        
        if (nums[left] >= target) return left;
        if (nums[right] >= target) return right;
        // 如果 target 大於所有元素
        if (target > nums[right]) return nums.length;

        return 0;
    }
}

使用模板二:

class Solution {
    public int searchInsert(int[] nums, int target) {
        int left = 0;
        int right = nums.length; //注意,左閉右開

        while (left < right) {
            int mid = left + (right - left) / 2;

            //查詢target的左邊界
            if (nums[mid] == target) {
                right = mid;
            } else if (nums[mid] > target) {
                right = mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            }
        }

        //target的下標(存在時)或小於target的個數
        return left;
    }
}

74. 搜尋二維矩陣

編寫一個高效的演算法來判斷 m x n 矩陣中,是否存在一個目標值。該矩陣具有如下特性:

  • 每行中的整數從左到右按升序排列。
  • 每行的第一個整數大於前一行的最後一個整數。

使用模板三:將二維轉換為一維

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length;
        int n = matrix[0].length;

        int left = 0;
        int right = m*n - 1;

        while (left + 1 < right) {
            int mid = left + (right - left) / 2;

            int row = mid / n;
            int col = mid % n;

            if (matrix[row][col] == target) {
                return true;
            } else if (matrix[row][col] > target) {
                right = mid;
            } else if (matrix[row][col] < target) {
                left = mid;
            }
        }


        System.out.println(right);
        if (matrix[left / n][left % n] == target) return true;
        if (matrix[right / n][right % n] == target) return true;

        return false;
    }
}

使用模板一:

class Solution {
    //將二維矩陣轉化為一維陣列
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length;
        int n = matrix[0].length;
        int left = 0;
        int right = m * n - 1;  //普通二分查詢,兩邊都閉合

        while (left <= right) {
            int mid = left + (right - left) / 2;
            int row = mid / n;
            int col = mid % n;

            if (matrix[row][col] == target) {
                return true;
            } else if (matrix[row][col] > target) {
                right = mid - 1;
            } else if (matrix[row][col] < target) {
                left = mid + 1;
            }
        }

        return false;
    }
}

278. 第一個錯誤的版本

你是產品經理,目前正在帶領一個團隊開發新的產品。不幸的是,你的產品的最新版本沒有通過質量檢測。由於每個版本都是基於之前的版本開發的,所以錯誤的版本之後的所有版本都是錯的。

假設你有 n 個版本 [1, 2, ..., n],你想找出導致之後所有版本出錯的第一個錯誤的版本。

你可以通過呼叫 bool isBadVersion(version) 介面來判斷版本號 version 是否在單元測試中出錯。實現一個函式來查詢第一個錯誤的版本。你應該儘量減少對呼叫 API 的次數。

使用模板三:

public class Solution extends VersionControl {
    //思路:T代表爭取,F代表錯誤。給定一個序列 T T T T F F F,查詢第一個F下標。二分查詢,
    public int firstBadVersion(int n) {
        int left = 1;
        int right = n;  //左右都閉合

        while (left + 1 < right) { //注意
            int mid = left + (right - left) / 2;

            if (isBadVersion(mid)) {
                right = mid;
            } else {
                left = mid;
            }
        }

        if (isBadVersion(left)) {
            return left;
        }
        return right;
    }
}

使用模板二:

public class Solution extends VersionControl {
    //思路:T代表爭取,F代表錯誤。給定一個序列 T T T T F F F,查詢第一個F下標。二分查詢,
    public int firstBadVersion(int n) {
        long left = 1;
        long right = (long)n + 1;  //左閉右開,改為long,否則2147483647會越界

        while (left < right) {
            int mid = (int) (left + (right - left) / 2);

            //查詢左邊界
            if (isBadVersion(mid)) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }

        return (int)left;
    }
}

153. 尋找旋轉排序陣列中的最小值

已知一個長度為 n 的陣列,預先按照升序排列,經由 1n旋轉 後,得到輸入陣列。例如,原陣列 nums = [0,1,2,4,5,6,7] 在變化後可能得到:

  • 若旋轉 4 次,則可以得到 [4,5,6,7,0,1,2]
  • 若旋轉 7 次,則可以得到 [0,1,2,4,5,6,7]

注意,陣列 [a[0], a[1], a[2], ..., a[n-1]] 旋轉一次 的結果為陣列 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

給你一個元素值 互不相同 的陣列 nums ,它原來是一個升序排列的陣列,並按上述情形進行了多次旋轉。請你找出並返回陣列中的 最小元素

使用模板三:

class Solution {
    public int findMin(int[] nums) {
        int left = 0;
        int right = nums.length - 1; //左閉右開

        while (left + 1 < right) {   //注意
            int mid = left + (right - left) / 2;


            //始終拿 mid 和 right比較
            //在最小值的左邊
            if (nums[mid] > nums[right]) {
                left = mid;
            //在最小值的右邊
            } else if (nums[mid] < nums[right]) {
                right = mid;
            }
            //沒有重複數字的情況下不會出現等於的情況
        }

        if (nums[left] < nums[right]) {
            return nums[left];
        }
        return nums[right];
    }
}

使用模板二:

class Solution {
    public int findMin(int[] nums) {
        int target = nums[nums.length - 1];

        int left = 0;
        int right = nums.length; //注意,左閉右開

        while (left < right) {
            int mid = left + (right - left) / 2;

            //查詢左邊界
            if (nums[mid] == target) { // 2 3 4 1
                right = mid;
            //在最小值的左邊
            } else if (nums[mid] > target) {
                left = mid + 1;
            //在最小值的右邊
            } else if (nums[mid] < target) {
                right = mid;
            }
        }

        return nums[left];
    }
}

154. 尋找旋轉排序陣列中的最小值 II

已知一個長度為 n 的陣列,預先按照升序排列,經由 1n旋轉 後,得到輸入陣列。例如,原陣列 nums = [0,1,4,4,5,6,7] 在變化後可能得到:

  • 若旋轉 4 次,則可以得到 [4,5,6,7,0,1,4]
  • 若旋轉 7 次,則可以得到 [0,1,4,4,5,6,7]

注意,陣列 [a[0], a[1], a[2], ..., a[n-1]] 旋轉一次 的結果為陣列 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

給你一個可能存在 重複 元素值的陣列 nums ,它原來是一個升序排列的陣列,並按上述情形進行了多次旋轉。請你找出並返回陣列中的 最小元素

使用模板三:

class Solution {
    public int findMin(int[] nums) {
        int left = 0;
        int right = nums.length - 1; 

        while (left + 1 < right) {
            int mid = left + (right - left) / 2;

            //相等時,你無法確定此時 mid 在最小值的左邊還是右邊,如:2 2 2 2 2 3 4 2 
            //不用擔心 2 為最小值時 right-- 會漏掉最小值,此時 2 總是有一個副本,mid 或 最左側
            if (nums[mid] == nums[right]) {
                right--;
            //在最小值的左邊
            } else if (nums[mid] > nums[right]) {
                left = mid;
            //在最小值的右邊
            } else if (nums[mid] < nums[right]) {
                right = mid;
            }
        }

        if (nums[left] < nums[right]) {
            return nums[left];
        }
        return nums[right];
    }
}

33. 搜尋旋轉排序陣列

整數陣列 nums 按升序排列,陣列中的值 互不相同

在傳遞給函式之前,nums 在預先未知的某個下標 k0 <= k < nums.length)上進行了 旋轉,使陣列變為 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下標 從 0 開始 計數)。例如, [0,1,2,4,5,6,7] 在下標 3 處經旋轉後可能變為 [4,5,6,7,0,1,2]

給你 旋轉後 的陣列 nums 和一個整數 target ,如果 nums 中存在這個目標值 target ,則返回它的下標,否則返回 -1

使用模板三:

class Solution {
    public int search(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1; //左右都為閉區間

        while (left + 1 < right) {
            int mid = left + (right - left) / 2;

            //可利用mid跟left 或 right比較從而確定 left 和 mid 或 mid 與 right那個區間是有序的
            //left 和 mid間有序
            if (nums[left] < nums[mid]) {
                if (target >= nums[left] && target <= nums[mid]) {
                    right = mid;
                } else {
                    left = mid;
                }
             //mid 和 right間有序
            } else if (nums[mid] < nums[right]) {
                if (target >= nums[mid] && target <= nums[right]) {
                    left = mid;
                } else {
                    right = mid;
                }
            }
        }

        if (nums[left] == target) {
            return left;
        }

        if (nums[right] == target) {
            return right;
        }

        return -1;
    }
}

81. 搜尋旋轉排序陣列 II

已知存在一個按非降序排列的整數陣列 nums ,陣列中的值不必互不相同。

在傳遞給函式之前,nums 在預先未知的某個下標 k0 <= k < nums.length)上進行了 旋轉 ,使陣列變為 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下標 從 0 開始 計數)。例如, [0,1,2,4,4,4,5,6,6,7] 在下標 5 處經旋轉後可能變為 [4,5,6,6,7,0,1,2,4,4]

給你 旋轉後 的陣列 nums 和一個整數 target ,請你編寫一個函式來判斷給定的目標值是否存在於陣列中。如果 nums 中存在這個目標值 target ,則返回 true ,否則返回 false

使用模板三:

class Solution {
    public boolean search(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1; //左右都為閉區間

        while (left + 1 < right) {
            int mid = left + (right - left) / 2;

            //可利用mid跟left 或 right比較從而確定 left 和 mid 或 mid 與 right那個區間是有序的
            if (nums[left] < nums[mid]) {
                if (target >= nums[left] && target <= nums[mid]) {
                    right = mid;
                } else {
                    left = mid;
                }
            } else if (nums[mid] < nums[right]) {
                if (target >= nums[mid] && target <= nums[right]) {
                    left = mid;
                } else {
                    right = mid;
                }
            } else if (nums[left] == nums[mid]) {	//與上例不同點
                left++;
            } else if (nums[right] == nums[mid]) {  //與上例不同點
                right--;
            }
        }

        if (nums[left] == target || nums[right] == target) {
            return true;
        }

        return false;
    }
}