二分查找說“簡單”又“不簡單”

二分查找也叫折半查找,學過的人都會說二分查找思路很簡單,可以使用模板,是有規律可循的,事實確實是這樣的,也就是所謂的“簡單”,但是它真的“簡單”嗎?

爲什麼又說二分查找“不簡單”?二分查找“不簡單”在查找的邊界控制、循環控制,深入細節,比如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,熟悉使用場景,把握每個場景所對應細節,多思考再回來看,二分查找就可以說“簡單”了!

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