上圖表示常用的二分查詢模板:
第一種是最基礎的,查詢區間左右都為閉區間,比較後若不等,剩餘區間都不會再包含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 + 1
而 right = mid
? 這是因為我們的查詢區間始終是保持為左閉右開。
重要的來了,如果前面兩種你覺得麻煩不好記憶那麼你只需要記住第三種即可
!第三種最為強大,查詢區間左右都為閉區間,比較後若不等,剩餘區間都會再包含mid
,最後退出時left
和right
相鄰,故都有可能為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
的陣列,預先按照升序排列,經由1
到n
次 旋轉 後,得到輸入陣列。例如,原陣列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
的陣列,預先按照升序排列,經由1
到n
次 旋轉 後,得到輸入陣列。例如,原陣列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
在預先未知的某個下標k
(0 <= 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
在預先未知的某個下標k
(0 <= 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;
}
}