目錄
二分查找也叫折半查找,學過的人都會說二分查找思路很簡單,可以使用模板,是有規律可循的,事實確實是這樣的,也就是所謂的“簡單”,但是它真的“簡單”嗎?
爲什麼又說二分查找“不簡單”?二分查找“不簡單”在查找的邊界控制、循環控制,深入細節,比如while循環中條件是否應該帶等號,mid 是否應該加一等等,這都需要去思考和測試才能找到答案,細節是魔鬼!
1.二分查找算法
爲什麼叫做二分查找?顧名思義需要“二分”,也就是將有序的數據分成兩部分來完成查找。
使用前提
- 必須爲順序存儲
- 存儲在數組中(鏈表不可以)
Tips:至於數組順序遞增排列還是遞減排列,數組中是否存在相同的元素都不要緊。不過一般情況,還是希望並假設數組是遞增排列,數組中的元素互不相同。
原理
- 將有序數組分爲三部分:左部分,中間值mid,右部分,即定義邊界left與right,計算mid
- 首先將要查找的目標值target與中間值進行對比,由於是有序數組,就會有三種比較結果:
1.mid==target,如果是查找target,可以直接返回索引mid;
2.mid<target,說明target存在於右區間,縮小區間至右區間
3.mid>target,說明target存在於左區間,縮小區間至左區間 - 依次遞歸,直到查找到target或者不存在target
2.二分查找模板
非遞歸模板
public int binarySearch(int[] nums,int target){
int left = 0, right = nums.length - 1; // 注意
while(left <= right) { // 注意
int mid = left + (right - left) / 2; // 注意
if(nums[mid] == target) { // 注意
// 相關邏輯
return mid;
} else if(nums[mid] < target) {// 右區間查找
left = mid + 1;
} else { //左區間查找
right = mid - 1; // 注意
}
}
// 相關返回值
return -1;
}
遞歸模板
public int binarySearch(int[] nums,int target,int left,int right) {
if(left > right) //遞歸終止條件
return -1;
int mid = left + (right-left)/2;
if(nums[mid] == target) //遞歸終止條件
return mid;
else if(nums[mid] > target) //查找左區間
return binarySearch(nums, target, left, mid-1);
else //查找右區間
return binarySearch(nums, target, mid+1, right);
}
Tips:計算 mid 時需要技巧防止溢出,建議寫成: mid = left + (right - left) / 2;
如果right-left也有可能涉及到溢出的時候,使用mid=(left+right)>>>1(無符號右移)
查找不到的數據均返回了-1,可以根據具體題意進行修改
3.二分查找細節
思路很簡單,細節是魔鬼。
使用場景
- 有序數組中查找一個數(存在或不存在) 35. 搜索插入位置
- 有序數組中查找邊界(左邊界或右邊界),也就是說在有序數組中找到“正好大於(小於)目標數”的那個數 278. 第一個錯誤的版本
- 有序數組中查找區域,假如存在重複數據,而數組依然有序,那麼還是可以用二分查找法判別目標數是否存在,希望知道目標值有多少個,也就是目標值所存在的區域 34. 在排序數組中查找元素的第一個和最後一個位置
- 將有序數組通過旋轉得到的數組,也就是“輪轉後的有序數組(Rotated Sorted Array)”,依舊可以使用二分查找,例如:{6,9,10,12,0,2,3,4}33. 搜索旋轉排序數組
Tips:超鏈接均是LeetCode中二分查找所對應的使用場景,可以根據具體的題目來深入學習和理解
while循環條件
拿非遞歸模板來說,循環條件是由left與right邊界的確定而確定的,循環條件指的是while語句中的判斷條件,有兩種可能:
int left = 0 ; right = nums.length;
對應的循環條件是while(left < right)
定義了right=nums.length,實際上nums[nums.length]這個值是不存在的,所以這個區間可以理解爲一個左閉右開的區間 [left,right),把這個區間叫做 搜索區間;
當使用二分查找的時候,當查找不到目標值的時候while條件應該停止進行返回, 那什麼時候查找不到目標值呢? 當搜索區間爲空的時候,也就是說沒有符合要求的數據來進行遍歷查找了, 搜索空間又何時爲空?
while(left < right)
的終止條件是 left == right,寫成區間的形式爲 [right,right),若right=6,搜索區間爲[6,6),即搜索區間是空的,即沒有數據可以查找進行返回,證明while條件是正確的。
如果循環條件寫成while(left <= right)
呢?終止條件是left=right+1,搜索區間爲[right+1,right),轉換爲(right,right+1],此時帶入right=6,搜索區間爲(6,7],此時搜索區間不爲空,漏掉了數字7,但是while條件卻不成立直接返回,那麼程序自然就出錯了!int left = 0 ; right = nums.length - 1;
對應的循環條件是while(left <= right)
定義了right = nums.length-1,這個索引對應的值nums[nums.length-1]是可以取到的,所以搜索區間是一個完全的閉區間[left,right];
while(left<=right)
對應的終止條件是left=right+1,搜索區間爲[right+1,right],帶入right=6,搜索區間爲[7,6],搜索區間爲空,這個區間不存在符合的數據,while循環可以正常終止,程序運行正確!
如果循環條件寫成while(left < right)
呢?終止條件爲right==left,搜索區間爲[right,right],帶入right=6,搜索區間爲[6,6],搜索區間不爲空,但是while循環卻終止了,說明漏掉了數字6,程序就可能出現錯誤了!
爲什麼 left = mid + 1,right = mid - 1?
如果使用場景是在有序數組中查找一個數,那麼當發現索引 mid 不是要找的 target 時,如何確定下一步的搜索區間呢?
當然是去搜索 [left, mid - 1] 或者 [mid + 1, right] 對不對?因爲 mid 已經搜索過,應該從搜索區間中去除。
爲什麼 left = mid + 1,right = mid ?
如果使用場景是查詢左側邊界,那麼當nums[mid]==target的時候,並不是第一時間返回mid索引,因爲需要尋找的是左側的邊界,也就是比target小的數據,有可能你所要查找的nums[]中就不存在target,當nums[mid]==target的時候,接下來的搜索區間是什麼呢?
一定是[left,mid]與[mid+1,right],因爲根據題意無法去排除掉mid!
Tips:同理,如果使用場景是查詢右側邊界,應該是left=mid,right=mid-1,對應搜索區間爲[left,mid-1]與[mid,right]
二分查找查詢右邊界
//二分查找查詢右邊界
public int binarySearchRight(int[] nums,int target){
int left = 0, right = nums.length - 1; // 閉區間
while(left <= right) { // 邊界決定循環條件
int mid = left + (right - left) / 2; // 防止整型溢出
if(nums[mid] > target) { // 左區間查找
// 相關邏輯
right = mid;
} else // 右區間查找
left = mid + 1;
}
// 相關返回值
return left;//返回left與right均可
}
Tips:查詢左邊界或右邊界實現都是找嚴格界限,也就是要大於或者小於。如果要找鬆散界限,也就是找到大於等於或者小於等於的值(即包含自身),只需要稍作修改即可
在與中間值的比較中加上等號:
nums[mid] > target 改爲 nums[mid] >= target
爲什麼最後返回 left 而不是 right?
返回left和right都是一樣的,因爲 while 終止的條件是 left == right
4.小小總結
二分查找法的缺陷
二分查找法的O(log n)讓它成爲十分高效的算法。不過它的缺陷卻也是那麼明顯的。就在它的限定之上:
必須有序,我們很難保證使用的數組都是有序的。當然可以在構建數組的時候進行排序,可是又落到了第二個瓶頸上:必須是數組數組讀取效率是O(1),可是它的插入和刪除某個元素的效率卻是O(n)。因而導致構建有序數組變成低效的事情。
解決這些缺陷問題更好的方法應該是使用二分搜索樹了,最好自然是自平衡二叉查找樹了,自能高效的(O(nlogn))構建有序元素集合,又能如同二分查找法一樣快速(O(logn))的搜尋目標數。
小小建議
二分查找模板需要記憶,其中提到了幾道典型的LeetCode,熟悉使用場景,把握每個場景所對應細節,多思考再回來看,二分查找就可以說“簡單”了!