前言
二分查詢法是什麼,難道是我們剛入行時候寫的搜尋演算法嗎?
還記得我們剛入行,接觸演算法的時候,一般都會從氣泡排序、二分查詢開始入手演算法,那小夥伴們會不會覺得這個演算法太容易了,沒有必要用一篇文章來講解呢。
如果你有這樣的疑問,那麼王子問大家幾個問題,看大家能否很容易的就回答的上。
你清楚二分查詢法一般用於哪些查詢場景嗎?
你清楚迴圈終止條件嗎?
什麼時候使用<=,什麼時候使用<這些你都清楚嗎?
本文就與小夥伴們一起探討幾個最常用的二分查詢場景:尋找一個數、尋找左側邊界、尋找右側邊界。
並對裡面的實現細節做一個仔細的分析。
尋找一個數(最基本的二分查詢法)
這個場景是最簡單的場景,也是大家最熟悉的入門演算法,即在陣列中搜尋一個指定的數字,如果存在返回索引,如果不存在返回-1.
直接看程式碼:
int binarySearch(int[] nums, int target) { int left = 0; int 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 if (nums[mid] > target) right = mid - 1; // 注意 } return -1; }
針對於上邊的程式碼,我們探討幾個問題。
1.為什麼while迴圈中用的是<=而不是<?
因為我們初始化的right=nums.length-1,而不是nums.length。這兩者的區別就是前者表示的是閉區間查詢[left,right],後者表示的是開區間查詢[left,right)。
因為我們選擇的是[left,right]閉區間查詢,所以while中的條件就是left<=right.
那麼什麼時候應該退出迴圈呢?當然是找到目標值了。
if(nums[mid] == target) return mid;
如果沒有找到,就會通過while迴圈條件終止,並返回-1,而這個while迴圈中的條件,用大白話來講就是代表搜尋的區間是空的,
while(left <= right)的終止條件是 left == right + 1,寫成區間的形式就是 [right + 1, right],這種情況已經把陣列中所有該尋找的值尋找過了,所以直接返回 -1 是沒有問題的。
while(left < right)的終止條件是 left == right,寫成區間的形式就是 [right, right],這時候搜尋區間非空,還有一個數 right,但此時 while 迴圈終止了。也就是說索引 right被漏掉了,沒有進行判斷,如果這時候直接返回 -1 就可能出現錯誤。
這種錯誤我們只要在下邊做些改動就可以解決。
//... while(left < right) { // ... } return nums[left] == target ? left : -1;
2.為什麼 left = mid + 1,right = mid - 1?
其實只要我們明白了剛才討論的搜尋區間原則,理解這個並不難。
我們的搜尋區間是[left,right],那麼當我們發現mid不是目標值的時候,應該怎麼繼續查詢呢?當然是去查詢[left,mid-1]或者是[mid+1,right]了,這個應該好理解吧。
所以如果我們nums[mid]<target的時候,代表要找的值在[mid+1,right]這一區間中,所以就有了left=mid+1。right=mid-1同理。
3.這樣的寫法是否可以解決所有二分查詢的問題?
答案是不能的,如果給我們的nums=[1,3,3,3,4],target=3,由於是在中間二分查詢,那返回的結果就是2。
但是如果我們要得到左側邊界和右側邊界,這種寫法就不能實現了。
接下來我們分別說明左側邊界和右側邊界的寫法。
左側邊界問題
首先我們要明白什麼是左側邊界。
其實左側邊界可以理解成,這個陣列中所有小於目標值的數字有幾個,還是拿nums=[1,3,3,3,4],target=3來舉例,它的左側邊界就是1,白話解釋就是所有小於3的數字有1個。如果target=5,它的左側邊界就是5,也就是陣列的長度,白話解釋就是所有小於5的數字有5個。
好了,我們直接看程式碼:
int left_bound(int[] nums, int target) { if (nums.length == 0) return -1; int left = 0; int right = nums.length; // 注意 while (left < right) { // 注意 int mid = left+( right - left ) / 2; if (nums[mid] == target) { right = mid; } else if (nums[mid] < target) { left = mid + 1; } else if (nums[mid] > target) { right = mid; // 注意 } } return left; }
1.為什麼沒有返回 -1 的操作?如果 nums 中不存在 target 這個值,怎麼辦?
上邊我們給大家白話解釋了什麼是左側邊界,那麼什麼時候會返回-1呢?就是沒有找到目標數字的時候才會返回-1.
那麼什麼時候算是沒有找到這個數字呢,一共有兩種情況,一種情況是目標數字大於陣列中的每個數字,這種情況下,left=right=nums.length。另一種情況是目標數字小於陣列中的每個數字,也就是left=right=0,而且nums[0]!=target。
明白了上邊的道理,我們就可以更改程式碼如下:
while (left < right) { //... } // target 比所有數都大 if (left == nums.length) return -1; // 類似之前演算法的處理方式 return nums[left] == target ? left : -1;
2為什麼 left = mid + 1,right = mid ?
這個問題其實很好解釋,我們之前的區間是[left,right]閉區間,而我們現在的區間是[left,right),所以當我們發現nums[mid]<target的時候,代表目標值在[left,mid)中,所以right=mid,而nums[mid]>target的時候,代表目標值在[mid+1,right)中,所以left=mid+1.
3.如何解釋nums[mid]==target的時候,right=mid
其實這個就是解決左側邊界的主要判斷條件。我們找到target的時候不是直接返回索引的,而是繼續縮小範圍並向左收縮的,也就是縮小[left,mid)的範圍,所以就有了right=mid的寫法。其實明白了這裡,右側邊界也是同理,向右收縮搜尋範圍即可,寫成left=mid+1就可以了。
右側邊界
有了左側邊界的經驗,我們再來看右側邊界的問題就很容易了,直接看程式碼:
int right_bound(int[] nums, int target) { if (nums.length == 0) return -1; int left = 0, right = nums.length; while (left < right) { int mid = left+(right-left) / 2; if (nums[mid] == target) { left = mid + 1; // 注意 } else if (nums[mid] < target) { left = mid + 1; } else if (nums[mid] > target) { right = mid; } } if (left == nums.length) return -1; return nums[left-1] == target ? (left-1) : -1;// 注意 }
程式碼中已經標註了與左側邊界不同的地方。
1 left=mid+1剛才我們已經解釋過了,不再說明。
2 為什麼返回的值是left-1,而不是left
其實這個問題也好解釋,因為本來我們要返回的值是mid,而在左側邊界問題中,最終結果的時候left=mid=right,所以返回left或者right都可以
而在右側邊界問題中,我們的left=mid+1=right,所以mid=left-1。
所以我們的返回值就是left-1了。
擴充套件
為了讓大家對二分查詢法印象更深刻,王子給大家分享一道擴充套件的題目演算法,求x的平方根,題目如下:
這道題目其實也是可以用二分查詢法做出來的。
求x的平方根,我們可以把搜素區間粗略的設定為[0,x],在這個區間中進行二分查詢鎖定目標就可以了。
那麼什麼時候算是找到了目標呢,肯定是mid*mid==target,或者是mid*mid<target,而且(mid+1)*(mid+1)>target,這兩種情況下mid就是我們想要的結果。
有了思路,我們來看程式碼:
public int mySqrt(int x) { // result用於儲存結果 int left = 0, right = x, result = -1; while (left <= right) { int mid = left + (right - left) / 2; // 這裡一定要轉換成long型別,否者如果數字過大會變成負數,影響結果 if ((long) mid * mid <= x) { result = mid; left = mid + 1; } else { right = mid - 1; } } return result; }
這裡我們引入中間變數result來儲存想要的結果。
值得注意的就是mid*mid一定要強轉成long,因為如果數字過大超過int的範圍會變成負數,影響最終答案。
總結
至此,幾個最常用的二分查詢場景:尋找一個數、尋找左側邊界、尋找右側邊界,王子與大家就討論完畢了。
通過本文,小夥伴們應該對於二分查詢法的細節有了更深一步的瞭解。就算遇到二分查詢法的變形,也可以運用這種解題思維獨立思考並解決問題了。
看完本文,大家可以去LeeCode的二分查詢法專題中看一看,會很容易看懂其中的原理。
小夥伴們去試一試吧,入果覺得本篇文章對你有所幫助,記得關注哦。
往期文章推薦:
中介軟體專輯:
演算法專輯: