前言: 二分查詢作為很常見的一種演算法,基本思想是定義頭和尾雙指標,計算中間的index指標,每次去和陣列的中間值和目標值進行比較,如果相同就直接返回,如果目標值小於中間值就將尾指標重新賦值為中間值-1,頭指標不變,相當於從左邊區域去找;如果目標值大於中間值就將頭指標賦值為中間值+1,尾巴指標不變,相當於從右邊區間去找元素.依次迴圈這個過程,將區間一層層的壓縮,最終就可以得到最終的目標值的index.
目錄
一:二分使用條件和時間複雜度
二:二分的基本寫法
三:二分的相關問題
四:總結
正文
一:二分的使用條件和時間複雜度
1.1:①必須是單調遞增或者遞減陣列
注意這裡是單調遞增或者遞減,而不是全部遞增或者遞減,這點很重要。如果是完全亂序的陣列,那麼二分演算法就會完全失效。二分的本質就是藉助於單調性然後比較中間節點的值來達到縮小範圍去查詢元素的目的。如果是亂序的,那麼就無法比對中間值來達到縮減區間的目的
②必須是線性結構
對於像圖二叉樹等結構,二分是不合適的,因為沒辦法去用二分,這是由結構決定的
1.2:二分的時間複雜度
二分查詢的時間複雜度是o(logn),關於二分的時間複雜度是怎麼計算出來的呢?假如陣列的長度是n,每次進行二分查詢的時候是n/2,下次是n/4,再一次是n/8。在最壞的情況下,每次都找不到目標值,那麼就有可以設查詢的次數為T,(n/2)^T=1;則T=logn,底數是2,時間複雜度為O(logn)
二:二分的基本寫法
2.1:圖解二分
上述的例子是在{1,3,5,6,12,14,19}這個陣列中尋找target=3這個元素,因為陣列的長度是7,中點的index=3,值為6,發現3小於6,所以最終值肯定在中點的左半區域裡,因此右指標左移動=2,接下來是0、1、 2這三個元素中查詢,找到中點3,發現3=target,因此直接返回最終index=1。分析其過程,發現一共用了三次,就完成了元素的查詢,總體來說效率還是很高的。
2.2:二分查詢的標準寫法
1 /** 2 * 二分查詢 3 * 4 * @param nums 5 * @param target 6 * @return 7 */ 8 public int binarySearch(int[] nums, int target) { 9 10 int lo = 0; 11 int hi = nums.length - 1; 12 while (lo < hi) { 13 int middle = (low + hi) / 2; 14 if (nums[middle] < target) { 15 lo = middle + 1; 16 } else if (nums[middle] > target) { 17 hi = middle - 1; 18 } else { 19 return middle; 20 } 21 } 22 return -1; 23 }
三:二分變種問題
3.1:找出有重複資料的陣列元素的第一個值
在1.1裡談了二分的適用條件,但是需要注意的一點是,並沒有排除到陣列沒有重複的元素,在有重複元素的情況下二分可能會存在一個問題:查詢出來的元素並不是第一次出現的,假如我們要求必須查詢出來第一次出現的元素,那二分又怎麼寫呢?
首先思考一點:找出重複陣列的第一個值,那麼這個值肯定是第一個找出數字的index左側,當中間值等於目標值的時候我們不能立刻返回,因為此時它並不一定是第一次出現的(當然它也有可能就是第一次出現),之後我們需要將它繼續劃分,直到left>right的值後,返回當前的left左邊的值就是它第一次出現的位置(如果元素確實存在的話)。每次去查詢元素的時候,我們尋找目標值的時候就需要一直向左側逼近,當發現在這個值等於目標值的時候不能立刻返回,必須再次移動區間,直到逼近目標值的index再返回
1 /** 2 * 找到重複元素出現的第一個值 3 * 4 * @param array 5 * @param target 6 * @return 7 */ 8 public static int findFirstBinarySearch(int[] array, int target) { 9 int lo = 0; 10 int hi = array.length - 1; 11 12 while (lo <= hi) { 13 int mid = (lo + hi) / 2; 14 //注意這裡不再有array[mid]=target return mid; 15 if (array[mid] >= target) { 16 hi = mid - 1; 17 } else { 18 lo = mid + 1; 19 } 20 } 21 //防止lo越界 並且判斷lo的值 22 if (lo < array.length && array[lo] == target) { 23 return lo; 24 } 25 26 return -1; 27 }
3.1:找出有重複資料的陣列元素的最後一個值
找到最後一個元素,那麼它出現的值肯定在找出的第一個值的右側,同樣在找到middle值等於目標值的時候不能立刻返回,指標還需要繼續移動.因此就需要將值進行向右逼近,直到找到middle等於目標值的最後一個,此時返回的index一定是目標值的最後一個(假如存在目標值的話),最終取right索引,因為在條件不成立的時候,目標的索引值肯定在最終肯定是在middle的右邊
/** * 找出陣列中最後一個重複的數字 * @param array * @param key * @return */ static int findLastBinarySearch(int[] array, int key) { int lo = 0; int hi = array.length - 1; while (lo <= hi) { int mid = (lo + hi) / 2; if (array[mid] <= key) { //儘可能在右半區域裡找 lo = mid + 1; } else { hi = mid - 1; } }
// if (hi >= 0 && array[hi] == key) { return hi; } return -1; }
3.3:求一個數的平方根
初步看這道題,很難想到用二分去做。但是仔細一想,二分可以解決這個問題的,假如數字是8,那麼我們可以分為一個陣列為[1,2,3,4,5,6,7,8];這個陣列屬於單調遞增的,完全滿足二分的使用條件,題目要求返回的是整數部分,因此8的平方根肯定在這個陣列中的某一個數字,每個數的平方也是單調自增的,因此完全可以用二分查詢來解決這個問題:
public int sqrt(int n) { //0和1返回它本身 if (n==0||n==1){ return n; } //左指標 int lo = 0; //右指標 一個數的平方根肯定小於這個數的一半 int hi = n / 2; //儲存結果值 預設-1 int res = -1; while (lo <= hi) { //取中間值 int mid = lo + (hi - lo) / 2; //計算平方 int square = mid * mid; if (square == n) { return mid; } else if (square < n) { //對於非完全平方根 結果肯定是在左區間 res = mid; lo = mid + 1; } else { hi = mid - 1; } } return res; }
3.4:旋轉陣列的最小數字
在1.1中討論過二分的使用條件,是單調遞增,注意是單調遞增,並不是全部遞增。在旋轉陣列的最小數字這個問題中,它的整體陣列並不是全量遞增或者遞減的,但是它依然適用於二分,不過此時需要將原二分進行改造一下:
public int minarrayinRotationArray(int[] numbers) { //左邊指標 int lo = 0; //右邊指標 int hi = numbers.length - 1; while (lo < hi) { //中間指標 int middle = (lo + hi) / 2; //中間值大於右邊指標值 說明尋找的旋轉點一定在右區間 if (numbers[middle] > numbers[hi]) { lo = middle + 1; //中間值小於右邊指標值 說明旋轉點一定在左區間 } else if (numbers[middle] < numbers[hi]) { hi = middle; //如果中間值等於右邊指標值 無法確定旋轉點在哪 右指標減一 縮小區間 } else { hi--; } } return numbers[lo]; }
四:總結
本篇部落格介紹了常見二分方法的思想以及時間複雜度的由來,還有二分的基本變形題,在找第一個出現的目標值和最後一個目標值的時候如何用二分去找。以及二分不太容易想到的尋找一個數的平方根和旋轉陣列的最小值,解決這兩個問題能夠幫助更加深刻的理解二分法。二分作為一種常見的演算法,能夠知道普通的二分還是不夠的,靈活掌握並運用二分並解決實際中出現的問題,是值得我們思考的。