前面算法系列文章有寫過分治算法基本原理和實踐,對於分治算法主要是理解遞歸的過程。二分法是分治算法的一種,相比分治算法會簡單不少,由於少了遞歸的存在。css
在計算機科學中,二分查找算法(英語:binary search algorithm),也稱折半搜索算法(英語:half-interval search algorithm)、對數搜索算法(英語:logarithmic search algorithm)[2],是一種在有序數組中查找某一特定元素的搜索算法。搜索過程從數組的中間元素開始,若是中間元素正好是要查找的元素,則搜索過程結束;若是某一特定元素大於或者小於中間元素,則在數組大於或小於中間元素的那一半中查找,並且跟開始同樣從中間元素開始比較。若是在某一步驟數組爲空,則表明找不到。這種搜索算法每一次比較都使搜索範圍縮小一半。html
二分查找算法在狀況下的複雜度是對數時間。二分查找算法使用常數空間,不管對任何大小的輸入數據,算法使用的空間都是同樣的。除非輸入數據數量不多,不然二分查找算法比線性搜索更快,但數組必須事先被排序。儘管特定的、爲了快速搜索而設計的數據結構更有效(好比哈希表),二分查找算法應用面更廣。python
二分查找算法有許多中變種。好比分散層疊能夠提高在多個數組中對同一個數值的搜索。分散層疊有效的解決了計算幾何學和其餘領域的許多搜索問題。指數搜索將二分查找算法拓寬到無邊界的列表。二叉搜索樹和B樹數據結構就是基於二分查找算法的。算法
對二分法的概念瞭解後,下面來看一道示例:數組
已知一個長度爲 n 的數組,預先按照升序排列,經由 1 到 n 次 旋轉 後,獲得輸入數組。例如,原數組 nums = [0,1,2,4,5,6,7] 在變化後可能獲得:
若旋轉 4 次,則能夠獲得 [4,5,6,7,0,1,2]
若旋轉 7 次,則能夠獲得 [0,1,2,4,5,6,7]
注意,數組 [a[0], a[1], a[2], ..., a[n-1]] 旋轉一次 的結果爲數組 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。數據結構
給你一個元素值 互不相同 的數組 nums ,它原來是一個升序排列的數組,並按上述情形進行了屢次旋轉。請你找出並返回數組中的最小元素 。post
示例 1:學習
輸入:nums = [3,4,5,1,2]
輸出:1
解釋:原數組爲 [1,2,3,4,5] ,旋轉 3 次獲得輸入數組。編碼
示例 2:url
輸入:nums = [4,5,6,7,0,1,2]
輸出:0
解釋:原數組爲 [0,1,2,4,5,6,7] ,旋轉 4 次獲得輸入數組。
示例 3:
輸入:nums = [11,13,15,17]
輸出:11
解釋:原數組爲 [11,13,15,17] ,旋轉 4 次獲得輸入數組。
提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums 中的全部整數 互不相同
nums 原來是一個升序排序的數組,並進行了 1 至 n 次旋轉
下面來看一下我寫的一個失敗版的答案,此時的我還沒入門二分法:
class Solution { public int findMin(int[] nums) { int left = 0; int right = nums.length-1; while (left<=right) { int middle = left + (right -left)/2; if (nums[middle] > nums[left]) { left = middle + 1; }else { right = right-1; } } return nums[left]; } }
輸入:[4,5,6,7,8,9,10,0,1,2,3]
輸出:10
結果:0
能夠看到結果是不對,那這裏的問題是什麼呢?都說失敗是成功之母,咱們只有分析清楚爲啥咱們的解法會存在問題,才能更好地明白二分法的精髓。
先從答案分析,這裏輸出 10,爲啥會是 10。
看上面這張圖,代碼邏輯寫的是 middle > left,那麼 left = middle +1; 這個邏輯這麼寫是沒有問題的。
接着看,當不知足 middle > left,說明 middle 處於最小值的右半部分,這時候咱們讓 right--。那若是 right 就是最小值呢,這時候就會錯過最小值。
還有若是 middle 是最大值呢?那麼 left= middle +1 就是最小值,此時你再去計算 middle ,就直接把最小值錯過了。好比輸入數組:[5,6,7,8,9,0,1,2,3,4];
還要考慮一種特殊狀況,若是此時只有兩個元素了,有兩種狀況 [1,2],[2,1] ,這時候若是按照 right--,就會直接取到第一個元素。因此在 middle 和 left 相等的時候也要在作額外的判斷。
完整版經過代碼以下:
class Solution { public int findMin(int[] nums) { int left = 0; int right = nums.length-1; while (left<right) { int middle = left + (right -left)/2; if (nums[middle] > nums[left] && nums[middle] > nums[right]) { left = middle +1;
// 說明最小值就在最右邊,此時處於只有兩個元素的時候 } else if(middle == left && nums[left] > nums[right]) { left = right; } else { right = right-1; } } return nums[left]; } }
當你看到這段代碼後,你懵逼了,這仍是二分法嘛,分析下來這麼複雜。
那咱們來看下官方給的代碼:
class Solution { public int findMin(int[] nums) { int low = 0; int high = nums.length - 1; while (low < high) { int pivot = low + (high - low) / 2;
// 最小值必定是在和 high 在一個區間內的,因此這裏要判斷 pivot 和 high 的大小關係,不能去判斷和 low 的關係 if (nums[pivot] < nums[high]) { high = pivot; } else { low = pivot + 1; } } return nums[low]; } }
是否是以爲官方代碼簡潔易懂。
那爲啥這兩個解法的代碼會差這麼多,答案在於 middle 究竟是應該和 left 比較,仍是應該和 right 比較。
這也說明了方向的選擇的重要性。但是咱們應該怎麼選擇呢。這個主要是在分析問題的時候要想清楚。我的以爲也能夠這麼理解:
本題是找最小值的。從最小值到最右端,這其實就是單調遞增的,所以咱們只要關注右半部分,拋棄左半部分就好。
那麼本題錯誤緣由就是跟左邊進行比較,你再怎麼找,最後得出值都不在這一部分上,就致使你得添加不少額外的邏輯來確保能夠找到值。
PS:對於二分法要時刻關注只有兩個元素的狀況。這時候 middle = left。這時候注意 left 和 right 之間的關係。
經過這道題目相信你們已經對二分法有必定的認識了。
二分查找的思想就一個:逐漸縮小搜索區間。 以下圖所示,它像極了「雙指針」算法,left 和 right 向中間走,直到它們重合在一塊兒:
根據看到的中間位置的元素的值 nums[mid] 能夠把待搜索區間分爲三個部分:
這樣就能夠得到二分法基本模板:
class Solution { public int search(int[] nums, int target) { int left = 0; int right = nums.length - 1; // 確保 left 和 right 都在數組可取範圍內 while (left <= right) { // < 仍是 <= 按照本身的習慣便可 int mid = left + (right -left)/2; if (nums[mid] == target) { // 找到結果就返回 return mid; }else if(nums[mid] > target) { right = mid-1; } else { left = mid +1; } }
// 退出循環就說明沒找到 return -1; } }
雖然咱們看到的寫法有不少,但思想就這一個;爲何老是有朋友以爲二分難?由於有不少二分的寫法,雖然都對,可是對於新手朋友們來講有必定干擾,由於不一樣的寫法其實對應着不一樣的前提和應用場景,比起套用模板,審題、練習和思考更重要。「二分查找」就幾行代碼,徹底不須要記憶,也不該該用記憶的方式解題.
下面解釋一些細節:
一、模板的結束條件是 left <= right ,也就是結果必定是在 while 裏面找到的。不然就是沒找到。
二、有些學習資料上說 while (left < right) 表示區間是 [left..rihgt) ,爲何你這裏是 [left..rihgt]?
區間的右端點究竟是開仍是閉,徹底由編寫代碼的人決定,不須要固定。主要仍是看你 left 和 right 的取值。 若是 right = nums.length ; 那麼其實 right 這個位置是取不到的,也就是開區間了。因此開閉就是看點位能不能取到。
三、有些學習資料給出了三種模板,例如「力扣」推出的 LeetBook 之 「二分查找專題」,應該如何看待它們?
回答:三種模板其實區別僅在於退出循環的時候,區間 [left..right] 裏有幾個數。
while (left <= right) :退出循環的時候,right 在左,left 在右,區間爲空區間,因此要討論返回 left 和 right;
while (left < right) :退出循環的時候,left 與 right 重合,區間裏只有一個元素,這一點是咱們很喜歡的;
while (left + 1 < right) :退出循環的時候,left 在左,right 在右,區間裏有 2 個元素,須要編寫專門的邏輯。這種寫法在設置 left 和 right 的時候不須要加 1 和減 1。
看似簡化了思考的難度,但實際上屏蔽了咱們應該且徹底能夠分析清楚的細節。退出循環之後必定要討論返回哪個,也增長了編碼的難度。
我我的的經驗是:
while (left <= right) 用在要找的數的性質簡單的時候,把區間分紅三個部分,在循環體內就能夠返回;
while (left < right) 用在要找的數的性質複雜的時候,把區間分紅兩個部分,在退出循環之後才能夠返回;
徹底不用 while (left + 1 < right) ,理由是不會使得問題變得更簡單,反而很累贅。
不少題目在二分法的基礎上有變化,咱們要學會靈活變化。還要理解題意。
示例:
給定一個排序數組和一個目標值,在數組中找到目標值,並返回其索引。若是目標值不存在於數組中,返回它將會被按順序插入的位置。
請必須使用時間複雜度爲 O(log n)
的算法。
示例 1:
輸入: nums = [1,3,5,6], target = 5 輸出: 2
示例 2:
輸入: nums = [1,3,5,6], target = 2 輸出: 1
示例 3:
輸入: nums = [1,3,5,6], target = 7 輸出: 4
示例 4:
輸入: nums = [1,3,5,6], target = 0 輸出: 0
示例 5:
輸入: nums = [1], target = 0 輸出: 0
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums
爲無重複元素的升序排列數組-104 <= target <= 104
class Solution { public int searchInsert(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; } if (nums[mid]>target) { right = mid-1; }else { left = mid+1; } } // 沒找到,那麼 left 就是它所處的位置 return left; } }
注意一點:二分法只是用於有序數組,若是是無序的,此時是沒法肯定邊界的,這時候咱們就須要本身創造條件,找到數組的有序部分。
好比下面兩道,你們能夠本身找二分法題目去練習。
關於二分法的理論就講到這裏了,剩下的就是靠你們多多練習了。
參考文章
https://leetcode-cn.com/problems/search-insert-position/solution/te-bie-hao-yong-de-er-fen-cha-fa-fa-mo-ban-python-/