數據結構(Python實現)------ 二分查找
數據結構(Python實現)------ 二分查找)
背景
基本概念
什麼是二分查找
二分查找是計算機科學中最基本、最有用的算法之一。 它描述了在有序集合中搜索特定值的過程。
二分查找中使用的術語:
目標 Target —— 你要查找的值
索引 Index —— 你要查找的當前位置
左、右指示符 Left,Right —— 我們用來維持查找空間的指標
中間指示符 Mid —— 我們用來應用條件來確定我們應該向左查找還是向右查找的索引
在最簡單的形式中,二分查找對具有指定左索引和右索引的連續序列進行操作。這就是所謂的查找空間。二分查找維護查找空間的左、右和中間指示符,並比較查找目標或將查找條件應用於集合的中間值;如果條件不滿足或值不相等,則清除目標不可能存在的那一半,並在剩下的一半上繼續查找,直到成功爲止。如果查以空的一半結束,則無法滿足條件,並且無法找到目標。
在接下來的章節中,我們將回顧如何識別二分查找問題,
“爲什麼我們使用二分查找” 這一問題的原因,以及你以前可能不知道的 3 個不同的二分查找模板。由於二分查找是一個常見的面試主題,我們還將練習問題按不同的模板進行分類,以便你可以在實踐使用到每一個。
注意:
二進制搜索可以採用許多替代形式,並且可能並不總是直接搜索特定值。有時您希望應用特定條件或規則來確定接下來要搜索的哪一側(左側或右側)。
識別和模板簡介
如何識別二分查找?
如前所述,二分查找是一種在每次比較之後將查找空間一分爲二的算法。每次需要查找集合中的索引或元素時,都應該考慮二分查找。如果集合是無序的,我們可以總是在應用二分查找之前先對其進行排序。
成功的二分查找的 3 個部分
二分查找一般由三個主要部分組成:
預處理 —— 如果集合未排序,則進行排序。
二分查找 —— 使用循環或遞歸在每次比較後將查找空間劃分爲兩半。
後處理 —— 在剩餘空間中確定可行的候選者。
3 個二分查找模板
當我們第一次學會二分查找時,我們可能會掙扎。我們可能會在網上研究數百個二分查找問題,每次我們查看開發人員的代碼時,它的實現似乎都略有不同。儘管每個實現在每個步驟中都會將問題空間劃分爲原來的 1/2,但其中有許多問題:
爲什麼執行方式略有不同?
開發人員在想什麼?
哪種方法更容易?
哪種方法更好?
經過許多次失敗的嘗試並拉扯掉大量的頭髮後,我們找到了三個主要的二分查找模板。爲了防止脫髮,並使新的開發人員更容易學習和理解,我們在接下來的章節中提供了它們。
模板 #1:
Python實現
def binarySearch(nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
if len(nums) == 0:
return -1
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
# End Condition: left > right
return -1
關鍵屬性
二分查找的最基礎和最基本的形式。
查找條件可以在不與元素的兩側進行比較的情況下確定(或使用它周圍的特定元素)。
不需要後處理,因爲每一步中,你都在檢查是否找到了元素。如果到達末尾,則知道未找到該元素。
區分語法
初始條件:left = 0, right = length-1
終止:left > right
向左查找:right = mid-1
向右查找:left = mid+1
模板 #2:
Python實現
def binarySearch(nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
if len(nums) == 0:
return -1
left, right = 0, len(nums)
while left < right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid
# Post-processing:
# End Condition: left == right
if left != len(nums) and nums[left] == target:
return left
return -1
模板 #2 是二分查找的高級模板。它用於查找需要訪問數組中當前索引及其直接右鄰居索引的元素或條件。
關鍵屬性
一種實現二分查找的高級方法。
查找條件需要訪問元素的直接右鄰居。
使用元素的右鄰居來確定是否滿足條件,並決定是向左還是向右。
保證查找空間在每一步中至少有 2 個元素。
需要進行後處理。 當你剩下 1 個元素時,循環 / 遞歸結束。 需要評估剩餘元素是否符合條件。
區分語法
初始條件:left = 0, right = length
終止:left == right
向左查找:right = mid
向右查找:left = mid+1
模板 #3:
Python實現
def binarySearch(nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
if len(nums) == 0:
return -1
left, right = 0, len(nums) - 1
while left + 1 < right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid
else:
right = mid
# Post-processing:
# End Condition: left + 1 == right
if nums[left] == target: return left
if nums[right] == target: return right
return -1
模板 #3 是二分查找的另一種獨特形式。 它用於搜索需要訪問當前索引及其在數組中的直接左右鄰居索引的元素或條件。
關鍵屬性
實現二分查找的另一種方法。
搜索條件需要訪問元素的直接左右鄰居。
使用元素的鄰居來確定它是向右還是向左。
保證查找空間在每個步驟中至少有 3 個元素。
需要進行後處理。 當剩下 2 個元素時,循環 / 遞歸結束。 需要評估其餘元素是否符合條件。
區分語法
初始條件:left = 0, right = length-1
終止:left + 1 == right
向左查找:right = mid
向右查找:left = mid
Python實現
二分查找
給定一個 n 個元素有序的(升序)整型數組 nums 和一個目標值 target ,寫一個函數搜索 nums 中的 target,如果目標值存在返回下標,否則返回 -1。
示例 1:
輸入: nums = [-1,0,3,5,9,12], target = 9
輸出: 4
解釋: 9 出現在 nums 中並且下標爲 4
示例 2:
輸入: nums = [-1,0,3,5,9,12], target = 2
輸出: -1
解釋: 2 不存在 nums 中因此返回 -1
提示:
你可以假設 nums 中的所有元素是不重複的。
n 將在 [1, 10000]之間。
nums 的每個元素都將在 [-9999, 9999]之間。
class Solution(object):
def search(self,nums,target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
if len(nums) == 0:
return -1
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
# End Condition: left > right
return -1
二分查找模板 I
基本概念
Python實現
x 的平方根
實現 int sqrt(int x) 函數。
計算並返回 x 的平方根,其中 x 是非負整數。
由於返回類型是整數,結果只保留整數的部分,小數部分將被捨去。
示例 1:
輸入: 4
輸出: 2
示例 2:
輸入: 8
輸出: 2
說明: 8 的平方根是 2.82842…,
由於返回類型是整數,小數部分將被捨去。
二分查找方法:
class Solution(object):
def mySqrt(self, x):
"""
:type x: int
:rtype: int
"""
if x <=1:
return x
else:
left=1
right=x
mid=(left+right)//2
while right-left >=2:
if mid*mid == x:
return mid
elif mid*mid < x:
left = mid
mid =(left+right)//2
else:
right = mid
mid = (right+left)//2
return mid
Python的特性解法
def mySqrt(x):
return int(x**0.5)
牛頓迭代法求解即可:
class Solution:
def mySqrt(self, x):
"""
:type x: int
:rtype: int
"""
if x <= 1:
return x
r = x
while r > x / r:
r = (r + x / r) // 2
return int(r)
猜數字大小
我們正在玩一個猜數字遊戲。 遊戲規則如下:
我從 1 到 n 選擇一個數字。 你需要猜我選擇了哪個數字。
每次你猜錯了,我會告訴你這個數字是大了還是小了。
你調用一個預先定義好的接口 guess(int num),它會返回 3 個可能的結果(-1,1 或 0):
-1 : 我的數字比較小
1 : 我的數字比較大
0 : 恭喜!你猜對了!
示例 :
輸入: n = 10, pick = 6
輸出: 6
# The guess API is already defined for you.
# @param num, your guess
# @return -1 if my number is lower, 1 if my number is higher, otherwise return 0
# def guess(num):
class Solution(object):
def guessNumber(self, n):
"""
:type n: int
:rtype: int
"""
left ,right = 1,n
mid =(left+right)//2
while guess(mid) != 0:
if guess(mid) == 1:#偏小
left = mid+1
elif guess(mid) == -1:#偏大
right = mid-1
mid =(left+right)//2
return mid
搜索旋轉排序數組
假設按照升序排序的數組在預先未知的某個點上進行了旋轉。
( 例如,數組 [0,1,2,4,5,6,7] 可能變爲 [4,5,6,7,0,1,2] )。
搜索一個給定的目標值,如果數組中存在這個目標值,則返回它的索引,否則返回 -1 。
你可以假設數組中不存在重複的元素。
你的算法時間複雜度必須是 O(log n) 級別。
示例 1:
輸入: nums = [4,5,6,7,0,1,2], target = 0
輸出: 4
示例 2:
輸入: nums = [4,5,6,7,0,1,2], target = 3
輸出: -1
思路:
先用二分查找找到旋轉的分界點,比如[4,5,6,7,0,1,2]的7, 特點是這一位比後一位大。
找到之後數組就分成了兩段單調遞增的區間,將target跟nums[0]比較之後可以判斷出target落在哪段區間上,
然後就是普通的二分查找。
class Solution(object):
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
if not nums:
return -1
if len(nums) == 1:
return 0 if nums[0] == target else -1
low,high = 0,len(nums)-1
while(low<=high):
mid = (low+high)//2
if mid + 1 <len(nums) and nums[mid]>nums[mid+1]:
break
if nums[mid] < nums[-1]:
high = mid -1
elif nums[mid] >= nums[0]:
low = mid + 1
if low > high:
low,high=0,len(nums)-1
else:
if target >= nums[0]:
low,high = 0,mid
else:
low,high = mid+1,len(nums)-1
while low <= high:
mid=(low+high)//2
if nums[mid] == target:
return mid
elif nums[mid] > target:
high = mid -1
else:
low = mid + 1
return -1
二分查找模板 I
Python實現
第一個錯誤的版本
你是產品經理,目前正在帶領一個團隊開發新的產品。不幸的是,你的產品的最新版本沒有通過質量檢測。由於每個版本都是基於之前的版本開發的,所以錯誤的版本之後的所有版本都是錯的。
假設你有 n 個版本 [1, 2, …, n],你想找出導致之後所有版本出錯的第一個錯誤的版本。
你可以通過調用 bool isBadVersion(version) 接口來判斷版本號 version 是否在單元測試中出錯。實現一個函數來查找第一個錯誤的版本。你應該儘量減少對調用 API 的次數。
示例:
給定 n = 5,並且 version = 4 是第一個錯誤的版本。
調用 isBadVersion(3) -> false
調用 isBadVersion(5) -> true
調用 isBadVersion(4) -> true
所以,4 是第一個錯誤的版本。
解題思路:
二分,如果是壞版本,說明邊界在左邊,令end等於mid,如果當前值是好版本,說明邊界在右邊,令begin等於mid+1
# The isBadVersion API is already defined for you.
# @param version, an integer
# @return a bool
# def isBadVersion(version):
class Solution(object):
def firstBadVersion(self, n):
"""
:type n: int
:rtype: int
"""
begin,end = 1,n
while begin < end:
mid = (begin + end) // 2
if isBadVersion(mid):
end = mid
else:
begin = mid + 1
return begin
尋找峯值
峯值元素是指其值大於左右相鄰值的元素。
給定一個輸入數組 nums,其中 nums[i] ≠ nums[i+1],找到峯值元素並返回其索引。
數組可能包含多個峯值,在這種情況下,返回任何一個峯值所在位置即可。
你可以假設 nums[-1] = nums[n] = -∞。
示例 1:
輸入: nums = [1,2,3,1]
輸出: 2
解釋: 3 是峯值元素,你的函數應該返回其索引 2。
示例 2:
輸入: nums = [1,2,1,3,5,6,4]
輸出: 1 或 5
解釋: 你的函數可以返回索引 1,其峯值元素爲 2;
或者返回索引 5, 其峯值元素爲 6。
說明:
你的解法應該是 O(logN) 時間複雜度的。
解題思路:二分查找。每次比較nums[mid]與nums[mid+1],然後在較大的一半區間內繼續查找。換句話說,每次都選擇局部上升的數字所在的一半區間。
class Solution(object):
def findPeakElement(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
begain,end = 0,len(nums)-1
while begain < end:
mid = (begain+end)//2
if nums[mid] > nums[mid + 1]:
end = mid
else:
begain = mid + 1
return begain
尋找旋轉排序數組中的最小值
假設按照升序排序的數組在預先未知的某個點上進行了旋轉。
( 例如,數組 [0,1,2,4,5,6,7] 可能變爲 [4,5,6,7,0,1,2] )。
請找出其中最小的元素。
你可以假設數組中不存在重複元素。
示例 1:
輸入: [3,4,5,1,2]
輸出: 1
示例 2:
輸入: [4,5,6,7,0,1,2]
輸出: 0
解題思路
二分搜索:
移動右指針到mid的條件:
nums[mid]<nums[left]或者nums[mid]<nums[right]
不符合上述條件時移動左指針到mid+1
class Solution(object):
def findMin(self,nums):
left,right = 0,len(nums)-1
while left<right:
mid = left +(right-left)//2
if nums[mid]<nums[left] or nums[mid] < nums[right]:
right = mid
else:
left = mid+1
return nums[left]
在排序數組中查找元素的第一個和最後一個位置
給定一個按照升序排列的整數數組 nums,和一個目標值 target。找出給定目標值在數組中的開始位置和結束位置。
你的算法時間複雜度必須是 O(log n) 級別。
如果數組中不存在目標值,返回 [-1, -1]。
示例 1:
輸入: nums = [5,7,7,8,8,10], target = 8
輸出: [3,4]
示例 2:
輸入: nums = [5,7,7,8,8,10], target = 6
輸出: [-1,-1]
class Solution(object):
def searchRange(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
if len(nums) == 0:
return [-1,-1]
elif target <nums[0] or target>nums[-1]:
return [-1,-1]
else:
l,r = 0,len(nums)-1
while l<=r:
mid = (l+r)//2
if target > nums[mid]:
l=mid+1
elif target<nums[mid]:
r = mid-1
elif target == nums[mid]:
l=r=mid
while l-1>=0 and nums[l-1] == target:
l-=1
while r+1 <=len(nums)-1 and nums[r+1] == target:
r+=1
return [l,r]
return [-1,-1]
找到 K 個最接近的元素
給定一個排序好的數組,兩個整數 k 和 x,從數組中找到最靠近 x(兩數之差最小)的 k 個數。返回的結果必須要是按升序排好的。如果有兩個數與 x 的差值一樣,優先選擇數值較小的那個數。
示例 1:
輸入: [1,2,3,4,5], k=4, x=3
輸出: [1,2,3,4]
示例 2:
輸入: [1,2,3,4,5], k=4, x=-1
輸出: [1,2,3,4]
說明:
k 的值爲正數,且總是小於給定排序數組的長度。
數組不爲空,且長度不超過 104
數組裏的每個元素與 x 的絕對值不超過 104
解題思路:
由於arr有序,所以可以使用二分搜索不斷縮小x可能存在的區間。最終得到left,right,並且left + 1 == right。
然後採取類似合併兩個有序數組的方法,每次取出與x之差的絕對值較小者
Pow(x, n)
實現 pow(x, n) ,即計算 x 的 n 次冪函數。
示例 1:
輸入: 2.00000, 10
輸出: 1024.00000
示例 2:
輸入: 2.10000, 3
輸出: 9.26100
示例 3:
輸入: 2.00000, -2
輸出: 0.25000
解釋: 2-2 = 1/22 = 1/4 = 0.25
思路:
分而治之實現O(logn)求解
利用位運算,讓它節省點常數時間
class Solution(object):
def myPow(self,x,n):
if not n:
return 1
if n < 0:
return 1/self.myPow(x,-n)
if not n & 1:
return self.myPow(x*x,n>>1)
else:
return x*self.myPow(x,n-1)
有效的完全平方數
給定一個正整數 num,編寫一個函數,如果 num 是一個完全平方數,則返回 True,否則返回 False。
說明:不要使用任何內置的庫函數,如 sqrt。
示例 1:
輸入:16
輸出:True
示例 2:
輸入:14
輸出:False
class Solution(object):
def isPerfectSquare(self, num):
"""
:type num: int
:rtype: bool
"""
left,right=0,num
while left<right:
mid = (left+right)//2
if num<mid**2:
right=mid
else:
left=mid+1
if left>1:
sqrt_num = left-1
else:
sqrt_num=left
return sqrt_num**2==num
尋找比目標字母大的最小字母
給定一個只包含小寫字母的有序數組letters 和一個目標字母 target,尋找有序數組裏面比目標字母大的最小字母。
數組裏字母的順序是循環的。舉個例子,如果目標字母target = ‘z’ 並且有序數組爲 letters = [‘a’, ‘b’],則答案返回 ‘a’。
示例:
輸入:
letters = [“c”, “f”, “j”]
target = “a”
輸出: “c”
輸入:
letters = [“c”, “f”, “j”]
target = “c”
輸出: “f”
輸入:
letters = [“c”, “f”, “j”]
target = “d”
輸出: “f”
輸入:
letters = [“c”, “f”, “j”]
target = “g”
輸出: “j”
輸入:
letters = [“c”, “f”, “j”]
target = “j”
輸出: “c”
輸入:
letters = [“c”, “f”, “j”]
target = “k”
輸出: “c”
注:
letters長度範圍在[2, 10000]區間內。
letters 僅由小寫字母組成,最少包含兩個不同的字母。
目標字母target 是一個小寫字母。
class Solution(object):
def nextGreatestLetter(self, letters, target):
"""
:type letters: List[str]
:type target: str
:rtype: str
"""
l,r=0,len(letters)-1
while l <=r:
mid = l+(r-l)/2
if letters[mid] <= target:
l = mid + 1
else:
r = m-1
if l < len(letters):
return letters[l]
else:
return letters[0]