二分查找
二分查找也称折半查找(Binary Search),是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
基本代码框架
# 基本代码框架
def binarySearch(nums, target):
left = 0
# right = len(nums) - 1 搭配 while left <= right使用;
# right = len(nums) 搭配 while left < right使用。
right = ...
while ...:
# 防溢出整型范围
mid = left + (right - left) // 2
if (nums[mid] == target):
...
elif (nums[mid] < target):
left = ...
elif (nums[mid] > target):
right = ...
return ...
注意:分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节。 上面代码只是最基础的框架,细节部分根据不同的题目会有变化。
基本应用
寻找一个数(最基本的二分查找)
def binary_search(nums, target):
left = 0
# right = len(nums) - 1 对应搜索区间 [left, right]
right = len(nums) - 1
# left <= right 代表 left==right+1时退出循环,这里使用left<=right,搭配right=len(nums)-1,
# 即[right+1, right]退出,可以保证遍历了所有元素;
# 如果使用left<right 代表 left=right时退出循环,即[right, right]时退出,这会导致right元素没有遍历到
while left <= right:
# 防溢出整型范围
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
return -1
寻找左边界
要求:列表中存在重复数字,找到左边界的下标。
def binary_search(nums, target):
left = 0
# right = len(nums) 对应搜索区间 [left, right)
right = len(nums)
# left < right 代表 left==right时退出循环,这里使用left<right,搭配right=len(nums),
# 即[right, right)退出,可以保证遍历了所有元素
while left < right:
mid = left + (right - left) // 2
if nums[mid] == target:
# 因为是找左边界,当前mid可能就是左边界,所以向左收缩空间并且不能漏掉当前mid
right = mid
elif nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid
# 判断找不到的情况
if right >= length or nums[right] != target:
return -1
return right
注意:
-
if nums[mid] == target —> right = mid
因为是找左边界,当前mid可能就是左边界,所以向左收缩空间并且不能漏掉当前mid,即 right=mid; -
因为向左收缩时right = mid;而mid=left+(right-left)//2 —> 2mid=left+right —> 2mid=left+mid —> mid=left —> mid=left=right,如果使用while left<=right,则会导致left=right时陷入死循环。因此,不能使用while left <= right,应该选择right=len(nums)搭配while left<right。
-
由于选择了right=len(nums)和while left<right,即搜索空间是[left, right),如果找到nums[mid] > target,下一步搜索空间应该是[left,mid),即right=mid。
-
由于选择了right=len(nums)和while left<right,即搜索空间是[left, right),如果找到nums[mid] < target,下一步搜索空间应该是[mid+1,righjt),即left=mid+1。
寻找右边界
要求:列表中存在重复数字,找到右边界的下标。
def binary_search(nums, target):
length = len(nums)
left = 0
# 对应搜索区间 [left, right)
right = length
# left < right 代表 left==right时退出循环,这里使用left<right,搭配right=len(nums),
# 即[right, right)退出,可以保证遍历了所有元素
while left < right:
mid = left + (right - left) // 2
if nums[mid] == target:
# 因为搜索区间是[left, right),如果当前nums[mid] == target,要想向右收缩搜索区间,因为mid已经选过了,所以left=mid+1,即[mid+1, right)
# 如果nums[mid]就是右边界,那么mid+1就会错过右边界,所以在最后left-1
left = mid + 1
elif nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
# 收缩搜索区间,因为是左闭右开区间,所以right=mid,不需要mid-1
right = mid
if left >= length or nums[left-1] != target:
return -1
return left - 1
LeetCode
69. x 的平方根
题解
def mySqrt(x):
if x <= 1:
return x
left = 0
# 这里缩小搜索范围,(x//2)^2平方小于x^2,所以最大值选择x//2
right = x // 2
# 因为搜索范围是[left, right],所以判断条件为left<right,
# 跳出条件为left==right,即[right,right],可以保证每个元素都被遍历过
while left < right:
mid = left + (right - left) // 2
if mid * mid == x:
return mid
elif mid * mid < x:
# 小于x时,判断mid+1平方是否大于x,如果满足则说明平方根在mid和mid+1之间,取整型后就是mid
if (mid+1) * (mid+1) > x:
return mid
# 走到else说明 mid+1也小于x,则向右缩减搜索空间,即left = mid + 1
else:
left = mid + 1
elif mid * mid > x:
right = mid - 1
# 退出循环时 left == right,这里返回left和right均可
return left
744. 寻找比目标字母大的最小字母
题解
length = len(letters)
left = 0
# 搜索区间[left, right]
right = length - 1
# 退出条件left = right + 1,即[right+1, right]
while left <= right:
mid = left + (right - left) // 2
if letters[mid] == target:
left = mid + 1
elif letters[mid] < target:
left = mid + 1
elif letters[mid] > target:
if letters[mid-1] < target:
return letters[mid]
else:
right = mid - 1
# 因为循环退出条件是left=right+1, 即执行elif letters[mid]<target: ---> left=mid+1
# 或 elif letters[mid]>target:--->right=mid-1后,导致left>right跳出循环,
# 这时right指向的是小于target的字母,left指向的是大于target的首字母,所以这里选择letters[left]
return letters[0] if left == length else letters[left]
540. 有序数组中的单一元素
题解
def singleNonDuplicate(nums):
length = len(nums)
if length == 1:
return nums[0]
left = 0
# 搜索区间[left, right]
right = length - 1
while left <= right:
mid = left + (right - left) // 2
# mid为列表边界元素时直接返回
if mid == length-1 or mid == 0:
return nums[mid]
# 已经列表中只有一个数时单独存在的,那么判断nums[mid]==nums[mid+1]或nums[mid]==nums[mid-1],
# 根据当前mid分割,存在单独数字的子列表长度肯定为奇数
if nums[mid] == nums[mid+1]:
if mid % 2 == 0:
left = mid + 2
else:
right = mid - 1
elif nums[mid] == nums[mid-1]:
if (mid-1) % 2 == 0:
left = mid + 1
else:
right = mid + 1
else:
return nums[mid]
return -1
278. 第一个错误的版本
题解
def firstBadVersion(n):
left = 1
# 搜索区间[left, right]
right = n
# 退出条件 left==right 即 [right, right]
while left < right:
mid = left + (right - left) // 2
# 返回true时,向左收缩搜索区间
if isBadVersion(mid):
right = mid
else:
left = mid + 1
# 1. 退出循环时left==right;
# 2. 假如走到了else---> left=mid+1导致了left==right,那么能走到else说明当前left指向的是好的版本,left+1后才是错误的版本
return left
153. 寻找旋转排序数组中的最小值
题解
length = len(nums)
left = 0
# 搜索区间[left, right]
right = length - 1
if nums[0] <= nums[-1]:
return nums[0]
# 退出条件 left==right+1 即 [right+1, right]
while left <= right:
mid = left + (right - left) // 2
# 如果mid > mid+1,说明mid+1就是反转点,即最小值
if nums[mid] > nums[mid+1]:
return nums[mid+1]
# nums[mid]>nums[0]时,说明最小值在右边,向右收缩搜索区间
if nums[mid] > nums[0]:
left = mid + 1
# nums[mid]<=num[0]时,说明最小值是当前mid或在左边
elif nums[mid] <= nums[0]:
# 判断当前节点值是否小于前一个节点,如果小于,说明当前节点就是反转节点
if nums[mid] < nums[mid-1]:
return nums[mid]
# 向左收缩搜索区间
else:
right = mid - 1
# 1. 走到这里说明left == right+1,
# 2. 假如是走到 nums[mid]>nums[0] ---> left=mid+1,则left原来的值是大于nums[0],即下一个节点有可能是反转点,所以是mid+1,即nums[left] 有可能是反转点
# 3. 假如是走到 nums[mid]<=nums[0] ---> right=mid-1,则right原来的值小于那等于nums[0],且前一个值肯定小于当前mid,所以这里不会触发退出循环
return nums[left]
6. 查找区间
题解
def searchRange(nums, target):
length = len(nums)
left = 0
# 搜索区间[left, right]
right = length - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
min_index = mid
max_index = mid
# 找到之后向前和向后遍历直到找到不是target的停止
# 如果整个数组都是target,则这种方法时间复杂度会是O(n)
# 所以最好的办法还是分别找左边界和右边界,时间复杂度是logN级别的
while min_index >= 0 and nums[min_index] == target:
min_index -= 1
while max_index < length and nums[max_index] == target:
max_index += 1
return [min_index+1, max_index-1]
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return [-1, -1]