目录
- 基础背景
- 模板一
- 模板二
- 模板三
- [278. 第一个错误的版本](https://leetcode-cn.com/problems/first-bad-version/)
- [162. 寻找峰值](https://leetcode-cn.com/problems/find-peak-element/)
- [153. 寻找旋转排序数组中的最小值](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/)
- [154. 寻找旋转排序数组中的最小值 II](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/)
- [34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/)
- [658. 找到 K 个最接近的元素](https://leetcode-cn.com/problems/find-k-closest-elements/)
- [50. Pow(x, n)](https://leetcode-cn.com/problems/powx-n/)
- [367. 有效的完全平方数](https://leetcode-cn.com/problems/valid-perfect-square/)
- [744. 寻找比目标字母大的最小字母](https://leetcode-cn.com/problems/find-smallest-letter-greater-than-target/)
- [349. 两个数组的交集](https://leetcode-cn.com/problems/intersection-of-two-arrays/)
- [350. 两个数组的交集 II](https://leetcode-cn.com/problems/intersection-of-two-arrays-ii/)
- [167. 两数之和 II - 输入有序数组](https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/)
- [287. 寻找重复数](https://leetcode-cn.com/problems/find-the-duplicate-number/)
基础背景
二分查找主要有几个要素,三个指针分别指向left, right, mid,停止条件等。下面就是一个二分查找的标准模板
之后部分会分别介绍二分查找的三种模板。
704. 二分查找
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums)-1
while left + 1 < right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
if nums[mid] < target:
left = mid
if nums[mid] > target:
right = mid
# 还剩下两种情况,手动判断
if nums[left] == target:
return left
if nums[right] == target:
return right
return -1
什么时候可以采用二分查找?
每当需要查询集合中的元素或者其索引时,都可以采用二分查找。二分查找的前提是集合元素有序,所以对于无序集合需要先排序。
二分查找的三个部分
- 排序(有序集合忽略)
- 二分查找
- 在剩余空间在选取结果(二分查找可能会结束语right-left=1)
模板一
这是一个标准的二分查找模板
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>right,所以不需要后续处理。适用于查找通过单个索引访问的元素。
几个关键的点
初始化:left = 0, right = len(nums) - 1
循环条件: left <= right
向左查询: right = mid - 1
向右查询: left = mid + 1
69. x 的平方根
确定左边界,右边界。X的平方根不可能大于X/2,所以以此设计好左右边界
跟模板不同的地方取决于题目性质。需要注意的地方是中点的取值,左右取决于题目的分析
class Solution:
def mySqrt(self, x: int) -> int:
if x == 0:
return 0
left = 1
right = x // 2
while left < right:
mid = (left + right + 1) >> 1 # 取右中点, 否则当left+1=right时left=mid<right会陷入死循环
temp_square = mid * mid
if temp_square > x:
right = mid - 1
if temp_square <= x:
left = mid
return left
374. 猜数字大小
常规的二分查找思路,需要注意的是跟模板之间的差别
# 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: int) -> int:
class Solution:
def guessNumber(self, n: int) -> int:
left, right = 1, n
mid = (left + right + 1) >> 1
ans = guess(mid)
while ans != 0:
# print(f"now left {left}, right {right}, mid {mid}")
if ans == 1:
# print(f"too small")
left = mid
mid = (left + right + 1) >> 1 # 取右边的点
ans = guess(mid)
if ans == -1:
# print("too large")
right = mid
mid = (left + right) >> 1
ans = guess(mid)
# print(f"correct {mid}")
return mid
33. 搜索旋转排序数组
记住,所有二分法都由模板改进,所有的终点都在于左右指针究竟哪个变换到mid
class Solution:
def search(self, nums: List[int], target: int) -> int:
def isOrder(left, right): # 看是不是排序的数组
return nums[left] < nums[right]
if not nums:
return -1
left, right = 0, len(nums)-1
while left <= right:
mid = (left + right) // 2
if target == nums[mid]:
return mid
if isOrder(left, mid):
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else:
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1
模板二
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
这个不好用,我们忽略它
模板三
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个以上元素,结束时剩下left和right两个元素
初始条件: left = 0, right = length - 1
循环条件: left + 1 < right
向左右查找都是直接赋值mid
278. 第一个错误的版本
模板三的标准应用
# The isBadVersion API is already defined for you.
# @param version, an integer
# @return an integer
# def isBadVersion(version):
class Solution:
def firstBadVersion(self, n):
"""
:type n: int
:rtype: int
"""
if n == 1:
return 1
left, right = 1, n
while left + 1 < right:
mid = (left + right) // 2
if isBadVersion(mid):
right = mid
else:
left = mid
if isBadVersion(left):
return left
else:
return right
162. 寻找峰值
这道题仍然使用模板三,注意判断峰值的两种特殊情况
class Solution:
def findPeakElement(self, nums: List[int]) -> int:
def isPeak(i): # 判断是不是顶峰元素,注意有首尾两种特殊情况
if i == 0:
return nums[1] < nums[0]
if i == len(nums):
return nums[len(nums)-1] < nums[len(nums)]
return nums[i-1] < nums[i] and nums[i+1] < nums[i]
if len(nums) == 1:
return 0
left, right = 0, len(nums)-1
while left + 1 < right:
mid = (left + right) // 2
if isPeak(mid):
return mid
if nums[mid+1] > nums[mid]:
left = mid
else:
right = mid
if isPeak(left):
return left
return right
153. 寻找旋转排序数组中的最小值
class Solution:
def findMin(self, nums: List[int]) -> int:
def isOrder(left, right):
return nums[left] <= nums[right]
left, right = 0, len(nums)-1
if len(nums) == 1:
return nums[0]
if isOrder(left, right):
return nums[0]
while left + 1 < right:
mid = (left + right) // 2
# print(f"left {left}, right {right}, mid {mid}")
if isOrder(left, mid):
left = mid
else:
right = mid
# print(left, right)
if isOrder(left, right):
return nums[left]
else:
return nums[right]
154. 寻找旋转排序数组中的最小值 II
相比之前多了限制条件,就是数字可能会有重复。遇到这种题不应该深入细节去思考数字重复的几种情况,而是应该思考这样的限制条件会对原始的模板造成什么样的影响。
原始的模板答案,针对mid与right的比较来确定mid在左边序列还是右边序列,从而移动指针。
加入重复的限制,我们会发现,当mid=right的时候无法判断左右序列,这个时候我们要移动指针,就可以考虑移动单步,左边或者右边,经过验证我们发现移动右边并不会对结果造成影响,所以右指针左移一步即可使得mid继续改变。
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums)-1
while left <= right:
mid = (left + right) // 2
if nums[mid] < nums[right]: # mid在右边序列
# 搜索范围[left, mid]
right = mid
elif nums[mid] > nums[right]: # mid在左边序列
# 搜索范围[mid+1, right]
left = mid + 1
else: # 无法判断在哪个序列
# 无论是在右序列还是左序列,right -= 1不会改变最小值的存在
# 搜索范围[left, right-1]
right = right - 1
return nums[left]
34. 在排序数组中查找元素的第一个和最后一个位置
这道题是二分查找的一个变种,如果按照传统的二分法,会发现当nums[mid]==target的时候仍然不知怎么做,因为mid可能是在左右边界中,所以需要把这道题转换成一个求左边界,再求右边界的问题,运用两个二分查找。
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
if not nums:
return [-1,-1]
left, right = 0 ,len(nums)-1
# 第一次二分查找,找左边界, 使用模板一
while left < right:
mid = (left + right) // 2
if nums[mid] < target:
# 搜寻范围为[mid+1, right]
left = mid + 1
elif nums[mid] == target:
# 搜寻范围为[left, mid],因为左边还可能有target
right = mid
else:
# 搜寻范围为[left, mid-1]
right = mid - 1
if nums[left] == target:
# 这里要确认一下左边界是否存在
left_bd = left
else:
# 不存在说明数组中没有target值,直接返回[-1,-1]
return [-1, -1]
# 第二次二分查找,找右边界, 使用模板一
right = len(nums)-1 # 搜寻范围[left, n-1]
while left < right:
mid = (left + right + 1) // 2
# nums[mid]只有可能>=target
if nums[mid] > target:
# 搜寻范围为[left, mid-1]
right = mid - 1
else:
# 搜寻范围为[mid, right]
left = mid
if nums[right] == target:
right_bd = right
return [left_bd, right_bd]
658. 找到 K 个最接近的元素
这道题有几个要素。
- 将找到K个元素确定为找到答案的最左边界,利用二分法
- 确定最后的答案是什么样?最后的答案一定包含了x(或者说离x最近的元素),其次一定是左边开始第一个达到(右边比左边离x更加)的条件。由于我们的二分法可以确定是left是在从0开始慢慢右移,所以第一个满足要求的左边界就是我们的答案。
class Solution:
def findClosestElements(self, arr: List[int], k: int, x: int) -> List[int]:
# 以划窗的思维确定左边界的范围
left, right = 0, len(arr)-k
while left < right:
mid = (left + right) // 2
if x - arr[mid] > arr[mid + k] - x:
left = mid + 1
else:
right = mid
return arr[left:left + k]
50. Pow(x, n)
这道题的一个要点就是如果将n分解为可以重复利用的模式:
Pow(n) = Pow(n//2) * P(n//2) (* x )
以及需要注意的是负数的情况
class Solution:
def myPow(self, x: float, n: int) -> float:
def computePow(n):
if n == 1:
return x
P = computePow(n//2)
if n % 2 == 1:
return P * P * x
return P * P
if n == 0:
return 1
if n < 0:
return 1 / computePow(-n)
return computePow(n)
367. 有效的完全平方数
class Solution:
def isPerfectSquare(self, num: int) -> bool:
if num == 0 or num == 1:
return True
left = 2
right = num // 2
while left + 1 < right:
mid = (left + right) // 2
print(left, right, mid)
sqrt = mid * mid
if sqrt == num:
return True
if sqrt < num:
left = mid + 1
else:
right = mid - 1
if left * left == num or right * right == num:
return True
return False
744. 寻找比目标字母大的最小字母
class Solution:
def nextGreatestLetter(self, letters: List[str], target: str) -> str:
left, right = 0, len(letters)-1
while left + 1 < right:
mid = (left + right) // 2
if letters[mid] <= target:
left = mid
else:
right = mid
if letters[left] > target:
return letters[left]
if letters[right] > target:
return letters[right]
return letters[0]
349. 两个数组的交集
# 二分法
# 时间复杂度: 排序O(mlogm) + 查找O(n*logm)
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
def binary_search(x):
left, right = 0, len(nums1)-1
while left <= right:
mid = (left + right) // 2
if nums1[mid] == x:
return nums1[mid]
if nums1[mid] < x:
left = mid + 1
else:
right = mid - 1
return -1
if not nums1:
return []
results = set()
nums1 = sorted(nums1)
for num in nums2:
res = binary_search(num)
if res != -1:
results.add(res)
return list(results)
350. 两个数组的交集 II
如果使用二分法,跟上一题一模一样,除了储存结果不需要去重
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
def binary_search(x):
left, right = 0, len(nums1)-1
while left <= right:
mid = (left + right) // 2
if nums1[mid] == x:
return nums1[mid]
if nums1[mid] < x:
left = mid + 1
else:
right = mid - 1
return -1
if not nums1:
return []
results = []
nums1 = sorted(nums1)
for num in nums2:
res = binary_search(num)
if res != -1:
results.append(res)
return list(results)
167. 两数之和 II - 输入有序数组
# 双指针法更好,时间复杂度O(n)
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
left, right = 0, len(numbers)-1
while left < right:
res = numbers[left] + numbers[right]
# print(f"{numbers[left]} + {numbers[right]} = {res}")
if res == target:
# print("bingo")
return [left+1, right+1]
if res < target:
# print("too small")
left += 1
else:
# print("too big")
right -= 1
# 但我们还是用二分查找来做
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
def binary_search(i, num):
left = i+1
right = len(numbers) - 1
while left <= right:
mid = (left + right) // 2
res = numbers[mid] + num
if res == target:
return [i+1, mid+1]
if res < target:
left = mid + 1
else:
right = mid - 1
return -1
for i, num in enumerate(numbers):
res = binary_search(i,num)
if res != -1:
return res
287. 寻找重复数
抽屉原理,如果有10个抽屉,11个苹果,那么一定有至少一个抽屉放了2只或2只以上的苹果
假设现在我们有5个抽屉,每个位置只能存放一种数字,且这个数字小于等于5。但是现在我们发现这5个位置放了6个或者7个数字,说明有一个位置放了不止一个数字,换句话来说,这个数字是重复的数字。
由此二分法雏形出现,我们需要找到这个具体的抽屉的数量。
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
left = 1
right = len(nums) - 1
while left < right:
mid = (left + right) // 2
count = 0
for n in nums:
if n <= mid:
count += 1
if count > mid: # 重复数在[left,mid]
right = mid
else: # 重复数在[mid+1, right]
left = mid + 1
return left