深入淺出二分查找

二分查找是面試常考的知識(shí)點(diǎn),其方法是在有序序列中尋找滿足特定條件的值,存在許多不同的變種,最近在刷Leetcode深有感觸,整理整理。

說明:

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

普通的二分查找

  1. left=0,right=length-1,求mid = (left+right)/2;
  2. 對(duì)于arr[mid] < targetarr[left,...,mid]均小于target,那么target只可能存在于arr[mid+1,...,right];
  3. 對(duì)于arr[mid] > target,arr[mid,...,right]均大于target,那么target只可能存在于arr[left,...,mid-1]
  4. 對(duì)于arr[mid]==target,我們已經(jīng)找到了,直接返回下標(biāo);
  5. 對(duì)于2和3,只要滿足left<=right,表示總有數(shù)可以尋找。
普通二分查找
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出現(xiàn)的第一個(gè)位置(數(shù)組可能存在重復(fù)數(shù))

  1. left=0,right=length-1,求mid = (left+right)/2;
  2. 對(duì)于arr[mid] < target,arr[left,...,mid]均小于target,那么target只可能存在于arr[mid+1,...,right];
  3. 對(duì)于arr[mid] > target,arr[mid,...,right]均大于target,那么target只可能存在于arr[left,...,mid-1];
  4. 對(duì)于arr[mid]==target,我們已經(jīng)找到了相等的值,此時(shí)可能是第一個(gè)值,也可能是中間某一個(gè),但arr[mid+1,..,right]是不可能存在的第一個(gè)值的。因此應(yīng)該選擇right=mid;
  5. 對(duì)于2和3,只要滿足left<right,表示總有數(shù)可以尋找,而left==right時(shí),已經(jīng)是最后一個(gè)數(shù)了,此時(shí)判斷該數(shù)是否是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最后出現(xiàn)的位置(數(shù)組可能存在重復(fù)數(shù))

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

人工干預(yù)的兩種方式:

  1. left == midarr[mid]==target的時(shí)候,表示此時(shí)leftright相鄰,而left可能是重復(fù)值的中間部分,因此先判斷right是否等于target,相等返回right,不相等就返回left。當(dāng)left == right時(shí)由于只剩一個(gè)值,只需判斷arr[left]是否等于target即可。
  2. 終止條件設(shè)置為left < right - 1,這樣循環(huán)就會(huì)在l和h相鄰時(shí)終止,此時(shí)先判斷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的最后一個(gè)元素x的下標(biāo)(target可能不存在數(shù)組中)

  1. left=0,right=length-1,求mid = (left+right)/2;
  2. 對(duì)于arr[mid] < targetarr[left,...,mid]均小于target,那么x只可能存在于arr[mid,...,right],令left=mid;
  3. 對(duì)于arr[mid] > target,arr[mid,...,right]均大于x,那么x只可能存在于arr[left,...,mid-1],令right=mid-1;
  4. 對(duì)于arr[mid]==target,x只可能存在于arr[left,...,mid-1],令right=mid-1;
  5. 當(dāng)left+1==right的時(shí)候,mid總等于left,此時(shí)會(huì)陷入無限循環(huán),因此需要人工干預(yù)一下,將條件設(shè)置為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的第一個(gè)元素x的下標(biāo)(target可能不存在數(shù)組中)

  1. left=0,right=length-1,求mid = (left+right)/2;
  2. 對(duì)于arr[mid] < target,arr[left,...,mid]均小于target,那么x只可能存在于arr[mid+1,...,right],令left=mid+1;
  3. 對(duì)于arr[mid] > targetarr[mid,...,right]均大于x,那么x只可能存在于arr[left,...,mid],令right=mid;
  4. 對(duì)于arr[mid]==targetx只可能存在于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()

在一個(gè)旋轉(zhuǎn)數(shù)組中尋找指定target的位置(不存在重復(fù)元素)

旋轉(zhuǎn)數(shù)組有個(gè)特點(diǎn),就是至少有一邊是有序的。

  1. left=0,right=length-1,求mid = (left+right)/2
  2. 判斷arr[left]<=arr[mid]來確定是否左邊有序(注意這邊一定要等號(hào),因?yàn)閙id總是靠近left),否則表示另一邊有序;
  3. 判斷target是否在有序的一方,如果在,則在當(dāng)前范圍內(nèi)繼續(xù)查找,否則在另一邊查找。
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()

旋轉(zhuǎn)數(shù)組的最小值

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

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

import unittest

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

        if len(nums) == 0:
            return -1
        #數(shù)組本身有序
        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()

旋轉(zhuǎn)數(shù)組的最小值(數(shù)組包含重復(fù)值)

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

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

import unittest

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

        if len(nums) == 0:
            return -1
        #數(shù)組本身有序
        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()

做了以上這些題目,可以總結(jié)出以下要注意的點(diǎn)

  1. 解題思路:考慮初始、循環(huán)和終止過程,循環(huán)過程根據(jù)目標(biāo)值存在于哪個(gè)子數(shù)組來更改leftright;
  2. 如果存在使left=mid的情況,需要干預(yù)循環(huán)結(jié)束條件,因?yàn)樵?code>left和right相鄰時(shí),left==mid,那么如果left=mid,會(huì)導(dǎo)致每次循環(huán)無法減少候選數(shù)組,最終導(dǎo)致死循環(huán);
  3. 有時(shí)候需要在最外層判斷指針指向的位置是否符合條件,如循環(huán)結(jié)束條件為l < h - 1。
  4. 循環(huán)數(shù)組的解題思路就是尋找有序子數(shù)組;
  5. 對(duì)于旋轉(zhuǎn)數(shù)組如果存在重復(fù)數(shù)導(dǎo)致無法判斷前后哪個(gè)序列為有序,那么就要轉(zhuǎn)化為順序查找。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容