一文帶你攻克二分查找

二分查找是一種高效的查找算法,通常普通的循環遍歷查找須要O(n)的時間複雜度,而二分查找時間複雜度則爲O(logN),由於每一次都將查找範圍減半。java

看看百度百科以及LeetCode官方給出的二分查找算法的解釋:面試

(百度百科)算法

二分查找也稱折半查找(Binary Search),它是一種效率較高的查找方法。可是,折半查找要求線性表必須採用順序存儲結構,並且表中元素按關鍵字有序排數組

列。數據結構

(LeetCode)優化

二分查找也稱折半查找(Binary Search),它是一種效率較高的查找方法,前提是數據結構必須先排好序,能夠在數據規模的對數時間複雜度內完成查找。但spa

是,二分查找要求線性表具備有隨機訪問的特色(例如數組),也要求線性表可以根據中間元素的特色推測它兩側元素的性質,以達到縮減問題規模的效果。指針

二分查找問題也是面試中常常考到的問題,雖然它的思想很簡單,但寫好二分查找算法並非一件容易的事情。code

可以使用二分查找有兩個前提:一是要求數據結構必須先排好序或者說是有序的(這樣能夠推斷出一個元素左邊以及右邊的性質) 二是還能夠隨機訪問(順序存blog

儲結構)。

爲何須要者兩個前提呢?咱們經過一個案例來看看

 


 

二分查找最經典的案例 就是給出一個升序的無重複元素的數組,再給出一個目標值,返回數組中與目標值相等的

元素的索引(下標)找不到則返回-1;看下方核心代碼:

int left = 0,right = array.length;
while
(left<right){ //left 爲左邊界 //right 爲右邊界 mid = (left+right)/2; if(array[mid]==target) return mid; //找到目標索引 直接返回 else if(array[mid]>target) { right = mid; //大於目標值 則搜索區間改成左半區間 }else if(array[mid]<target) { left = mid+1; 0 //小於目標值 則搜索區間改成右半區間 } return -1; }

可能不少小夥伴看到left<right會疑惑由於還見過left<=right的,這個咱們留待後面討論。

這題其實就是在查找索引。假定數組爲【1,2,3,4,5,6,7】 target值爲6 咱們看看是如何查找的。

其實二分查找的核心就是一步一步的把查找(搜索)範圍減半。

 

 

left = 0 ,right = array.length爲初始化邊界,即將查找範圍鎖定爲[0,array.length)注意是左閉右開;

接着咱們將數組分爲兩部分,準確的說是3部分,一是 該查找範圍的中間值mid  二是mid左邊部分(姑且叫左半區間) 三是mid右邊部分(姑且叫右半區間)

而後咱們來看看 mid 這個索引 對於的數組元素與目標值的大小比較。若是相等則表明mid 就是須要查找的索引直接返回,若是mid對應元素比目標值大,值大了

就應該縮小,因此咱們將查找範圍改成mid 的左邊部分

即right = mid。若是mid對應元素比目標值小,值小了就應該放大,因此咱們將查找範圍改成mid右邊部分即left = mid +1.可能有人好奇爲何left = mid+1,而right 卻直接變爲mid。這個咱們也留在後面討論。

一開始咱們left = 0 ,right = 7.那麼第一步一開始查找的值即是(0+7)/2=3。便是4 比6要小,說明目標值更大因此將查找範圍減半至更大的一部分(右邊部分),咱們經過將左邊界右移至mid+1的位置來改變

邊界,此時left = 4,right是7,接着咱們查找(4+7)/2 = 5 索引5 對應的是6 正好是目標值因此直接返回了5。

爲何咱們經過mid與目標值關係就能夠知道查找範圍應該落在哪裏呢?

就是由於數組是已經排好序的(知足了一個條件),那爲何咱們能夠知道左右區間又或者說知道了範圍能夠直接訪問中間的元素呢?由於數組是順序存儲結構可

以隨機訪問(經過下標),因此必須是

順序存儲結構。若是是鏈表沒有下標訪問是沒法直接判斷中間元素與目標值關係的。這就是爲何想要二分查找必須知足這兩個條件了。

接着咱們來討論下 何時循環條件是left<=right,而何時又是left<right。

這一個其實取決與咱們定義查找範圍區間時是如何定義的,因爲咱們初始化left = 0,right = array.length。因此咱們能夠肯定初始狀態的左邊界是0而右邊界是數組長度。因爲是查找索引,因此一開始的查找區間是[left,right)爲左閉右開的,此時咱們的循環執行條件是left<right,循環的終止條件是left = right  ,此時

對應的區間是[left,left),該區間內已經沒法再查詢了因此該循環條件是可行的。

但當咱們將right 初始化爲array.length-1時,此時的查找範圍區間即是[left,right],左閉右閉,此時若是咱們的循環條件仍然是left<right,那麼表明終止時left=right,對應的搜索區間是【left,left】此時咱們帶入一個具體數字如是2,則區間則爲[2,2],此時區間內還有一個數2沒有查詢而循環卻終止了(漏查,所

以是不可取的。故循環條件的中的這個=號取決於查找區間的定義是左閉右開仍是左閉右閉。

還有一個問題是當減半查找範圍時,left = mid +1,爲何right = mid又或者right = mid-1。這其實也跟查找區間有關,上面講到其實二分查找時將數組分爲三

部分,mid,mid左半區間,mid右半區間。

若是咱們定義的right 爲array.length,那麼搜索區間是左閉右開的。咱們每查找一次mid 的值後,就要對範圍作改變,由於mid已經查找過了因此咱們的選擇要麼左邊要麼右邊。若是咱們去右邊,則left 指針右移, 即left = mid +1。而要去左邊時,須要的是right指針左移,因爲區間是左閉右開的,因此右邊界直接等於mid。咱們也能夠將三部分兩個區間直接用區間表示出來[left,mid) , [mid+1,right)這就是爲何right 直接等於mid了。

固然咱們也能夠將right 初始化爲array.length-1,那麼查找區間就是左閉右閉的 ,那三部分中裏的兩個區間分別爲[left,mid-1],[mid+1,right]。這時咱們改變查

找範圍時left = mid+1,而right = mid-1。

因此循環條件的等號和left,right的相應變化都取決於左右邊界時的定義(取決因而左閉右閉仍是左閉右開)。

另外,對於 mid = (left+right)/2。 這一式子咱們能夠變形優化, mid = left + (right-left)/2  防止溢出,也能夠將整除2換成位運算右移一位即 mid = left = ((right-left)>>1)。

這是二分查找的一種經典題型——精確查找,找到某值就直接返回。關於精確查找的例題能夠直接在leetcode裏找到——704.二分查找

二分查找的應用除了精確查找還有查找左右邊界

關於查找左右邊界的定義我的看法爲:

(查找左邊界)從後往前有一部分知足條件,則前面必有一個臨界點屬於邊界,比邊界大則知足,比邊界小則不符合。

(查找右邊界)從前日後有一部分知足條件,則後面必有一個臨界點屬於邊界,比邊界小則知足,比邊界大則不符合。

對於查找兩個邊界我也找了兩個題目來幫助理解

先看看查找左邊界(原題 leetcode278.第一個錯誤的版本):

 

 

 剛看題目,首先暴力確定能夠,直接從頭至尾掃一次,發現是錯誤的版本直接return出來就好。

雖然不是考精確查值,可是其實這個是考二分查找的。並且極其明顯的查值左邊界的特徵:從後往前有一部分知足條件,則前面必有一個臨界點屬於邊界,比邊界大則知足,比邊界小

則不符合。從最後往前到第一個錯誤的版本都是知足條件的都是錯誤的版本,可是第一個錯誤的版本就是臨界點就是邊界。邊界前的版本都是正確的版本。

首先咱們想一想

咱們想一想若是第k個版本是錯誤的版本 那麼很天然  它後面的全部版本 (k+1~n) 也都是錯誤的版本,可是第一個錯誤的版本倒是落在k的左邊的或者就是k。

那若是第k個版本並不是錯誤的版本,那錯誤的版本會落在哪裏呢?很明顯確定是落在k的右手邊,而且第一個錯誤的版本也是在右邊。

直接看代碼更便於理解:

 

 1 /* The isBadVersion API is defined in the parent class VersionControl.
 2       boolean isBadVersion(int version); */
 3 public class Solution extends VersionControl {
 4     public int firstBadVersion(int n) {
 5     int left = 1;
 6     int right = n;
 7     //【left,right】 左閉右閉搜索區間
 8     while (left <= right) {   
 9         int mid = left + ((right-left)>>1);
10         if (isBadVersion(mid)) {    //當前版本是錯誤的版本   要找第一個錯誤的版本   搜索區間改成左半部分
11             right = mid-1;
12         } else {
13             left = mid + 1;    //不是錯誤版本直接去右方找
14         }
15     }
16     return right+1;    //why?
17 }
18 }

 

對於區間的減半方向咱們已經瞭解了。那這題的返回值該是什麼呢?咱們這樣循環結束後,第一個錯誤的版本號是什麼呢?

咱們能夠根據循環內部的代碼得出咱們想要的答案,由於是要第一個錯誤的版本,那麼版本首先確定是錯誤的那咱們直接就將目光鎖定在

 

 咱們看到 ,mid 版本是確定錯誤的,那咱們循環最後終止時的mid 應該就是咱們要找的第一個錯誤版本號。但是咱們定義的變量只有left,right兩個指針。

那咱們該如何返回mid呢?咱們看到知足條件執行的語句是 right = mid -1;那mid 等於什麼呢?很明顯 mid = right +1; 因此咱們能夠直接返回right+1;

不少人可能會疑惑,不是查詢左邊界嘛,怎麼返回是指針是right 再+1?想返回左指針便於理解怎麼辦?其實也能夠,咱們知道正確的返回值是right+1,那循環

條件是什麼?是left<=right,那循環終止時left 的值就是right+1.因此能夠返回right+1,也可直接返回left。

 

看完左邊界咱們繼續看看查找右邊界(特殊緣由不放連接直接上圖):

 

首先單看題目,其實就是問你,給你n塊巧克力讓你切成k份而且要保證切出來的是正方形。讓咱們求知足條件下最大這個切出來的正方形的邊長是多少。

可能會有些小夥伴看不出這是個二分查找的題目。讓咱們分析一下。   咱們求的是知足條件下切出來的正方形最大的邊長爲多少。咱們知足的條件是什麼?要切出

k塊。那其實咱們想象,若是切出來的正方形的邊長越小,那是否是獲得的塊數就越多呢?假設一下,   若是最大邊長是  x 那,邊長爲1的正方形去切也確定知足吧?

那就是說咱們須要查的最大邊長,其實就是知足條件下邊長是右邊界。肯定了是二分查找的題目,那咱們怎麼肯定初始的搜索區間呢?對於左邊,題目提示了最小

能夠爲1,那咱們就

爲1,對於右邊沒有規定,那咱們就直接初始化爲所給出的巧克力的最大邊長就好。那如今問題就轉變到了,對於如何肯定邊長x是否知足條件咱們能夠經過計算最

終切出來的塊數而判

斷出來。那對於一塊巧克力,若是按照x的邊長是正方形去切,咱們能獲得幾塊巧克力呢?假設爲6*5的巧克力去切邊長爲2的正方形。正方形是特殊的長方形,他

們的面積其實都是長*寬,那咱們其實能夠直接經過巧克力的長寬分別除於邊長再乘起來就能夠了,注意是整出。如這個例子,能切出來的巧克力塊數就是(6/2)*(5/2)=6塊。

全部的問題都解決了咱們就直接上代碼吧

 1 import java.util.Scanner;
 2 
 3 public class Main {
 4 
 5     public static void main(String[] args) {
 6         Scanner in = new Scanner(System.in);
 7         int left = 1;
 8         int right = -1;
 9         int n = in.nextInt();
10         int k = in.nextInt();
11         int[] h = new int[n];
12         int[] w = new int[n];
13         for (int i = 0; i < n; i++) {
14             h[i] = in.nextInt();
15             w[i] = in.nextInt();
16             right = Math.max(Math.max(h[i], w[i]), right);
17         }
18         while (left <= right) {
19             int mid = left + ((right - left) >> 1);
20             if (check(mid, h, w, k)) {
21                 left = mid + 1;
22             } else
23                 right = mid - 1;
24         }
25         System.out.println(right);
26     }
27 
28     public static boolean check(int mid, int[] h, int[] w, int k) {
29         int cnt = 0;
30         for (int i = 0; i < h.length; i++) {
31             cnt += ((h[i] / mid) * (w[i] / mid));
32             if (cnt >= k)
33                 return true;
34         }
35         return false;
36     }
37 }

好了本篇文章到此結束了,可能並不能真的讓你攻克二分查找,但確定會讓你更好的理解二分查找。感謝瀏覽!

相關文章
相關標籤/搜索