二分查找是面試??嫉闹R點,其方法是在有序序列中尋找滿足特定條件的值,存在許多不同的變種,最近在刷Leetcode深有感觸,整理整理。
說明:
- 本文的二分查找變種都來自于Leetcode
- 本文不考慮整數溢出問題
普通的二分查找
- 令
left=0,right=length-1
,求mid = (left+right)/2
; - 對于
arr[mid] < target
,arr[left,...,mid]
均小于target
,那么target只可能存在于arr[mid+1,...,right]
; - 對于
arr[mid] > target
,arr[mid,...,right]
均大于target
,那么target
只可能存在于arr[left,...,mid-1]
; - 對于
arr[mid]==target
,我們已經找到了,直接返回下標; - 對于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出現的第一個位置(數組可能存在重復數)
- 令
left=0,right=length-1
,求mid = (left+right)/2
; - 對于
arr[mid] < target
,arr[left,...,mid]
均小于target
,那么target只可能存在于arr[mid+1,...,right]
; - 對于
arr[mid] > target
,arr[mid,...,right]
均大于target
,那么target
只可能存在于arr[left,...,mid-1]
; - 對于
arr[mid]==target
,我們已經找到了相等的值,此時可能是第一個值,也可能是中間某一個,但arr[mid+1,..,right]
是不可能存在的第一個值的。因此應該選擇right=mid
; - 對于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最后出現的位置(數組可能存在重復數)
- 令
left=0,right=length-1
,求mid = (left+right)/2
; - 對于
arr[mid] < target
,arr[left,...,mid]
均小于target
,那么target只可能存在于arr[mid+1,...,right]
; - 對于
arr[mid] > target
,arr[mid,...,right]
均大于target
,那么target
只可能存在于arr[left,...,mid-1]
; - 對于
arr[mid]==target
,我們已經找到了相等的值,此時可能是最后一個值,也可能是中間某一個,但arr[left,..,mid-1]
是不可能存在的第一個值的。因此應該選擇left=mid
,但這里有個問題,就是當left+1==right
的時候,mid
總等于left,此時會陷入無限循環,因此需要人工干預一下; - 對于2和3,只要滿足
left<right
,表示總有數可以尋找,而left==right
時,已經是最后一個數了,此時判斷該數是否是target即可。
人工干預的兩種方式:
- 在
left == mid
且arr[mid]==target
的時候,表示此時left
和right
相鄰,而left
可能是重復值的中間部分,因此先判斷right
是否等于target
,相等返回right
,不相等就返回left
。當left == right
時由于只剩一個值,只需判斷arr[left]
是否等于target
即可。 - 終止條件設置為
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可能不存在數組中)
- 令
left=0,right=length-1
,求mid = (left+right)/2
; - 對于
arr[mid] < target
,arr[left,...,mid]
均小于target
,那么x只可能存在于arr[mid,...,right]
,令left=mid
; - 對于
arr[mid] > target
,arr[mid,...,right]
均大于x
,那么x
只可能存在于arr[left,...,mid-1]
,令right=mid-1
; - 對于
arr[mid]==target
,x
只可能存在于arr[left,...,mid-1]
,令right=mid-1
; -
當
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可能不存在數組中)
- 令
left=0,right=length-1
,求mid = (left+right)/2
; - 對于
arr[mid] < target
,arr[left,...,mid]
均小于target
,那么x只可能存在于arr[mid+1,...,right]
,令left=mid+1
; - 對于
arr[mid] > target
,arr[mid,...,right]
均大于x
,那么x
只可能存在于arr[left,...,mid]
,令right=mid
; - 對于
arr[mid]==target
,x
只可能存在于arr[mid+1,...,right]
,令left=mid+1
; - 判斷
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的位置(不存在重復元素)
旋轉數組有個特點,就是至少有一邊是有序的。
- 令
left=0,right=length-1
,求mid = (left+right)/2
; - 判斷
arr[left]<=arr[mid]
來確定是否左邊有序(注意這邊一定要等號,因為mid總是靠近left),否則表示另一邊有序; - 判斷
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()
旋轉數組的最小值
- 令
left=0,right=length-1
,求mid = (left+right)/2
; - 判斷
arr[left]<=arr[mid]
來確定是否左邊有序(注意這邊一定要等號,因為mid總是靠近left),否則表示另一邊有序,如果左邊和右邊皆有序,那么最小值一定是arr[left]
; - 如果左邊有序,則
left=mid
,如果右邊有序,則right=mid
,終止條件為兩個left
和right
相鄰,此時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()
旋轉數組的最小值(數組包含重復值)
- 令
left=0,right=length-1
,求mid = (left+right)/2
; - 如果
arr[mid]==arr[left] and arr[mid] == arr[right]
,那么因為無法判斷哪邊有序,只能轉換為順序查找; - 判斷
arr[left]<=arr[mid]
來確定是否左邊有序(注意這邊一定要等號,因為mid總是靠近left),否則表示另一邊有序,如果左邊和右邊皆有序,那么最小值一定是arr[left]
; - 如果左邊有序,則
left=mid
,如果右邊有序,則right=mid
,終止條件為兩個left
和right
相鄰,此時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()
做了以上這些題目,可以總結出以下要注意的點
- 解題思路:考慮初始、循環和終止過程,循環過程根據目標值存在于哪個子數組來更改
left
和right
; - 如果存在使
left=mid
的情況,需要干預循環結束條件,因為在left
和right
相鄰時,left==mid
,那么如果left=mid
,會導致每次循環無法減少候選數組,最終導致死循環; - 有時候需要在最外層判斷指針指向的位置是否符合條件,如循環結束條件為
l < h - 1
。 - 循環數組的解題思路就是尋找有序子數組;
- 對于旋轉數組如果存在重復數導致無法判斷前后哪個序列為有序,那么就要轉化為順序查找。