深入淺出二分查找

二分查找是面試??嫉闹R點,其方法是在有序序列中尋找滿足特定條件的值,存在許多不同的變種,最近在刷Leetcode深有感觸,整理整理。

說明:

  1. 本文的二分查找變種都來自于Leetcode
  2. 本文不考慮整數溢出問題

普通的二分查找

  1. left=0,right=length-1,求mid = (left+right)/2;
  2. 對于arr[mid] < target,arr[left,...,mid]均小于target,那么target只可能存在于arr[mid+1,...,right]
  3. 對于arr[mid] > target,arr[mid,...,right]均大于target,那么target只可能存在于arr[left,...,mid-1];
  4. 對于arr[mid]==target,我們已經找到了,直接返回下標;
  5. 對于2和3,只要滿足left<=right,表示總有數可以尋找。
普通二分查找
import unittest

class Solution:
    def search(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """

        l,h = 0, len(nums) - 1

        while l<=h:
            m = l + (h-l)//2
            if nums[m] == target:
                return m
            elif nums[m] > target:
                h = m - 1
            else:
                l = m + 1

        return -1

class TestSolution(unittest.TestCase):
    def setUp(self):
        self.s = Solution()

    def test_one(self):
        input = [-1, 0, 3, 5, 9, 12]
        self.assertEqual(self.s.search(input, 9), 4)
    
    def test_two(self):
        input = [-1, 0, 3, 5, 9, 12]
        self.assertEqual(self.s.search(input, 2), -1)

if __name__ == "__main__":
    unittest.main()

尋找target出現的第一個位置(數組可能存在重復數)

  1. left=0,right=length-1,求mid = (left+right)/2;
  2. 對于arr[mid] < target,arr[left,...,mid]均小于target,那么target只可能存在于arr[mid+1,...,right]
  3. 對于arr[mid] > targetarr[mid,...,right]均大于target,那么target只可能存在于arr[left,...,mid-1];
  4. 對于arr[mid]==target,我們已經找到了相等的值,此時可能是第一個值,也可能是中間某一個,但arr[mid+1,..,right]是不可能存在的第一個值的。因此應該選擇right=mid
  5. 對于2和3,只要滿足left<right,表示總有數可以尋找,而left==right時,已經是最后一個數了,此時判斷該數是否是target即可。
import unittest

class Solution:
    def search(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """

        if len(nums) == 0:
            return -1

        l = 0
        h = len(nums) - 1

        while l < h:
            m = l + (h-l)//2
            if nums[m] == target:
                h = m
            elif nums[m] < target:
                l = m + 1
            else:
                h = m - 1

        if nums[l] != target:
            return -1
        
        return l

class TestSolution(unittest.TestCase):
    def setUp(self):
        self.s = Solution()

    def test_one(self):
        input = [-1, 0, 3, 3, 5, 9, 12]
        self.assertEqual(self.s.search(input, 3), 2)
    
    def test_two(self):
        input = [-1, 0, 3, 3, 5, 9, 12]
        self.assertEqual(self.s.search(input, 2), -1)

if __name__ == "__main__":
    unittest.main()

尋找target最后出現的位置(數組可能存在重復數)

  1. left=0,right=length-1,求mid = (left+right)/2;
  2. 對于arr[mid] < targetarr[left,...,mid]均小于target,那么target只可能存在于arr[mid+1,...,right];
  3. 對于arr[mid] > target,arr[mid,...,right]均大于target,那么target只可能存在于arr[left,...,mid-1];
  4. 對于arr[mid]==target,我們已經找到了相等的值,此時可能是最后一個值,也可能是中間某一個,但arr[left,..,mid-1]是不可能存在的第一個值的。因此應該選擇left=mid,但這里有個問題,就是當left+1==right的時候,mid總等于left,此時會陷入無限循環,因此需要人工干預一下;
  5. 對于2和3,只要滿足left<right,表示總有數可以尋找,而left==right時,已經是最后一個數了,此時判斷該數是否是target即可。

人工干預的兩種方式:

  1. left == midarr[mid]==target的時候,表示此時leftright相鄰,而left可能是重復值的中間部分,因此先判斷right是否等于target,相等返回right,不相等就返回left。當left == right時由于只剩一個值,只需判斷arr[left]是否等于target即可。
  2. 終止條件設置為left < right - 1,這樣循環就會在l和h相鄰時終止,此時先判斷arr[right]是否等于target,不等再判斷arr[left]。
import unittest

class Solution:
    def search(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """

        if len(nums) == 0:
            return -1

        l = 0
        h = len(nums) - 1

        while l < h:
            m = l + (h-l)//2
            if nums[m] == target:
                if m == l:
                    if nums[h] == target:
                        return h
                    else:
                        return l
                l = m
            elif nums[m] < target:
                l = m + 1
            else:
                h = m - 1

        if nums[h] != target:
            return -1
        
        return h

    def search2(self, nums, target):
        if len(nums) == 0:
            return -1

        l = 0
        h = len(nums) - 1

        while l < h - 1:
            m = l + (h-l)//2
            if nums[m] == target:
                l = m
            elif nums[m] < target:
                l = m + 1
            else:
                h = m - 1
        
        high = -1 
        if nums[h] == target:
            high = h 
        elif nums[l] == target:
            high = l

        return high

class TestSolution(unittest.TestCase):
    def setUp(self):
        self.s = Solution()

    def test_one(self):
        input = [-1, 0, 3, 3, 5, 9, 12]
        self.assertEqual(self.s.search(input, 3), 3)
        self.assertEqual(self.s.search2(input, 3), 3)
    
    def test_two(self):
        input = [-1, 0, 3, 3, 5, 9, 12]
        self.assertEqual(self.s.search(input, 2), -1)
        self.assertEqual(self.s.search2(input, 2), -1)

if __name__ == "__main__":
    unittest.main()

返回小于target的最后一個元素x的下標(target可能不存在數組中)

  1. left=0,right=length-1,求mid = (left+right)/2;
  2. 對于arr[mid] < target,arr[left,...,mid]均小于target,那么x只可能存在于arr[mid,...,right],令left=mid
  3. 對于arr[mid] > target,arr[mid,...,right]均大于x,那么x只可能存在于arr[left,...,mid-1],令right=mid-1
  4. 對于arr[mid]==target,x只可能存在于arr[left,...,mid-1],令right=mid-1;
  5. left+1==right的時候,mid總等于left,此時會陷入無限循環,因此需要人工干預一下,將條件設置為left < right - 1,在外層,先檢查right再檢查left;
import unittest

class Solution:
    def search(self, nums, target):
        if len(nums) == 0:
            return -1

        l = 0
        h = len(nums) - 1

        while l < h - 1:
            m = l + (h-l)//2
            if nums[m] < target:
                l = m 
            elif nums[m] >= target:
                h = m - 1
        
        pos = -1 
        if nums[h] < target:
            pos = h
        elif nums[l] < target:
            pos = l

        return pos

class TestSolution(unittest.TestCase):
    def setUp(self):
        self.s = Solution()

    def test_one(self):
        input = [-1, 0, 3, 3, 5, 9, 12]
        self.assertEqual(self.s.search(input, 3), 1)
    
    def test_two(self):
        input = [-1, 0, 3, 3, 5, 9, 12]
        self.assertEqual(self.s.search(input, 2), 1)

    def test_three(self):
        input = [0]
        self.assertEqual(self.s.search(input, 0), -1)

if __name__ == "__main__":
    unittest.main()

返回大于target的第一個元素x的下標(target可能不存在數組中)

  1. left=0,right=length-1,求mid = (left+right)/2;
  2. 對于arr[mid] < targetarr[left,...,mid]均小于target,那么x只可能存在于arr[mid+1,...,right],令left=mid+1;
  3. 對于arr[mid] > targetarr[mid,...,right]均大于x,那么x只可能存在于arr[left,...,mid],令right=mid;
  4. 對于arr[mid]==target,x只可能存在于arr[mid+1,...,right],令left=mid+1;
  5. 判斷arr[left]是否大于target即可
import unittest

class Solution:
    def search(self, nums, target):
        if len(nums) == 0:
            return -1

        l = 0
        h = len(nums) - 1

        while l < h:
            m = l + (h-l)//2
            if nums[m] <= target:
                l = m + 1
            elif nums[m] >= target:
                h = m
        
        pos = -1 
        if nums[l] > target:
            pos = l

        return pos

class TestSolution(unittest.TestCase):
    def setUp(self):
        self.s = Solution()

    def test_one(self):
        input = [-1, 0, 3, 3, 5, 9, 12]
        self.assertEqual(self.s.search(input, 3), 4)
    
    def test_two(self):
        input = [-1, 0, 3, 3, 5, 9, 12]
        self.assertEqual(self.s.search(input, 2), 2)

    def test_three(self):
        input = [0]
        self.assertEqual(self.s.search(input, 0), -1)

if __name__ == "__main__":
    unittest.main()

在一個旋轉數組中尋找指定target的位置(不存在重復元素)

旋轉數組有個特點,就是至少有一邊是有序的

  1. left=0,right=length-1,求mid = (left+right)/2;
  2. 判斷arr[left]<=arr[mid]來確定是否左邊有序(注意這邊一定要等號,因為mid總是靠近left),否則表示另一邊有序;
  3. 判斷target是否在有序的一方,如果在,則在當前范圍內繼續查找,否則在另一邊查找。
import unittest

class Solution:
  def search(self, nums, target):
    """
    :type nums: List[int]
    :type target: int
    :rtype: int
    """

    if len(nums) == 0:
        return -1

    l,h = 0,len(nums) -1
    
    while l <= h:
        mid = l + (h-l)//2

        if nums[mid] == target:
            return mid

        if nums[mid] >= nums[l]:
            # 左邊有序
            if target >= nums[l] and target < nums[mid]:
                h = mid -1
            else:
                l = mid + 1
        else:
            # 右邊有序
            if target > nums[mid] and target <= nums[h]:
                l = mid + 1
            else:
                h = mid - 1
    return -1

class TestSolution(unittest.TestCase):
    def setUp(self):
        self.s = Solution()
    
    def test_one(self):
        input = [4, 5, 6, 7, 0, 1, 2]
        target = 0
        self.assertEqual(self.s.search(input, target), 4)

    def test_two(self):
        input = [4,5,6,7,0,1,2]
        target = 8
        self.assertEqual(self.s.search(input, target), -1)
    def test_three(self):
        input = [3,1]
        target = 1
        self.assertEqual(self.s.search(input, target), 1)

if __name__ == "__main__":
    unittest.main()

旋轉數組的最小值

  1. left=0,right=length-1,求mid = (left+right)/2;
  2. 判斷arr[left]<=arr[mid]來確定是否左邊有序(注意這邊一定要等號,因為mid總是靠近left),否則表示另一邊有序,如果左邊和右邊皆有序,那么最小值一定是arr[left];
  3. 如果左邊有序,則left=mid,如果右邊有序,則right=mid,終止條件為兩個leftright相鄰,此時left是前面子數組的最后一個,right是后面子數組的第一個,此時返回right。

注意:如果選擇旋轉數組旋轉為0,即本身有序,那么上面方法就會失效,因此在之前判斷數組本身是否有序。

import unittest

class Solution:
    def findMin(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """

        if len(nums) == 0:
            return -1
        #數組本身有序
        if nums[0] <= nums[-1]:
            return nums[0]

        l,h = 0,len(nums) - 1

        while l < h:
            m = l + (h-l)//2

            if nums[l] <= nums[m]:
                #左邊有序
                if nums[m] <= nums[h]:
                    # 右邊有序
                    return l
                else:
                    l = m+1
            else:
                #右邊有序
                h = m
        return l


class TestSolution(unittest.TestCase):
    def setUp(self):
        self.s = Solution()

    def test_one(self):
        input = [4,5,6,1,2,3]
        self.assertEqual(self.s.findMin(input), 3)
    
    def test_two(self):
        input = [1, 2, 3]
        self.assertEqual(self.s.findMin(input), 0)

if __name__ == "__main__":
    unittest.main()

旋轉數組的最小值(數組包含重復值)

  1. left=0,right=length-1,求mid = (left+right)/2
  2. 如果arr[mid]==arr[left] and arr[mid] == arr[right],那么因為無法判斷哪邊有序,只能轉換為順序查找;
  3. 判斷arr[left]<=arr[mid]來確定是否左邊有序(注意這邊一定要等號,因為mid總是靠近left),否則表示另一邊有序,如果左邊和右邊皆有序,那么最小值一定是arr[left]
  4. 如果左邊有序,則left=mid,如果右邊有序,則right=mid,終止條件為兩個leftright相鄰,此時left是前面子數組的最后一個,right是后面子數組的第一個,此時返回right

注意:如果選擇旋轉數組旋轉為0,即本身有序,那么上面方法就會失效,因此在之前判斷數組本身是否有序。

import unittest

class Solution:
    def findMin(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """

        if len(nums) == 0:
            return -1
        #數組本身有序
        if nums[0] <= nums[-1]:
            return nums[0]

        l,h = 0,len(nums) - 1

        while l < h:
            m = l + (h-l)//2

            if nums[l] <= nums[m]:
                #左邊有序
                if nums[m] <= nums[h]:
                    # 右邊有序
                    return l
                else:
                    l = m+1
            else:
                #右邊有序
                h = m
        return l


class TestSolution(unittest.TestCase):
    def setUp(self):
        self.s = Solution()

    def test_one(self):
        input = [4,5,6,1,2,3]
        self.assertEqual(self.s.findMin(input), 3)
    
    def test_two(self):
        input = [1, 2, 3]
        self.assertEqual(self.s.findMin(input), 0)

if __name__ == "__main__":
    unittest.main()

做了以上這些題目,可以總結出以下要注意的點

  1. 解題思路:考慮初始、循環和終止過程,循環過程根據目標值存在于哪個子數組來更改leftright;
  2. 如果存在使left=mid的情況,需要干預循環結束條件,因為在leftright相鄰時,left==mid,那么如果left=mid,會導致每次循環無法減少候選數組,最終導致死循環;
  3. 有時候需要在最外層判斷指針指向的位置是否符合條件,如循環結束條件為l < h - 1
  4. 循環數組的解題思路就是尋找有序子數組
  5. 對于旋轉數組如果存在重復數導致無法判斷前后哪個序列為有序,那么就要轉化為順序查找。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內容