二分查找法基本原理和實踐

概述

前面算法系列文章有寫過分治算法基本原理和實踐,對於分治算法主要是理解遞歸的過程。二分法是分治算法的一種,相比分治算法會簡單不少,由於少了遞歸的存在。css

在計算機科學中,二分查找算法(英語:binary search algorithm),也稱折半搜索算法(英語:half-interval search algorithm)、對數搜索算法(英語:logarithmic search algorithm)[2],是一種在有序數組中查找某一特定元素的搜索算法。搜索過程從數組的中間元素開始,若是中間元素正好是要查找的元素,則搜索過程結束;若是某一特定元素大於或者小於中間元素,則在數組大於或小於中間元素的那一半中查找,並且跟開始同樣從中間元素開始比較。若是在某一步驟數組爲空,則表明找不到。這種搜索算法每一次比較都使搜索範圍縮小一半。html

二分查找算法在狀況下的複雜度是對數時間。二分查找算法使用常數空間,不管對任何大小的輸入數據,算法使用的空間都是同樣的。除非輸入數據數量不多,不然二分查找算法比線性搜索更快,但數組必須事先被排序。儘管特定的、爲了快速搜索而設計的數據結構更有效(好比哈希表),二分查找算法應用面更廣。python

二分查找算法有許多中變種。好比分散層疊能夠提高在多個數組中對同一個數值的搜索。分散層疊有效的解決了計算幾何學和其餘領域的許多搜索問題。指數搜索將二分查找算法拓寬到無邊界的列表。二叉搜索樹和B樹數據結構就是基於二分查找算法的。算法

入門 demo 

對二分法的概念瞭解後,下面來看一道示例:數組

153. 尋找旋轉排序數組中的最小值

已知一個長度爲 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] 能夠把待搜索區間分爲三個部分:

  • 狀況 1:若是 nums[mid] = target,這時候咱們直接返回便可。
  • 狀況 2: target 在 mid 左半部分 [left..mid - 1],此時分別設置 right = mid - 1 ;
  • 狀況 3: target 在 mid 右半部分 [mid+1..right],此時分別設置  left = mid + 1。

這樣就能夠得到二分法基本模板:

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;
    }
}

注意一點:二分法只是用於有序數組,若是是無序的,此時是沒法肯定邊界的,這時候咱們就須要本身創造條件,找到數組的有序部分。

好比下面兩道,你們能夠本身找二分法題目去練習。

33. 搜索旋轉排序數組

81. 搜索旋轉排序數組 II

關於二分法的理論就講到這裏了,剩下的就是靠你們多多練習了。

 

算法系列文章:

滑動窗口算法基本原理與實踐

廣度優先搜索原理與實踐

深度優先搜索原理與實踐

雙指針算法基本原理和實踐

分治算法基本原理和實踐

動態規劃算法原理與實踐

算法筆記

 

參考文章

https://leetcode-cn.com/problems/search-insert-position/solution/te-bie-hao-yong-de-er-fen-cha-fa-fa-mo-ban-python-/

相關文章
相關標籤/搜索