三 二分查找
from: https://realpython.com/
目录
- 三 二分查找
- 1 定义、基本思想及步骤
- 2 简单二分查找python实现
- 3 二分查找细节总结
- 4 二分法两种思路
- 5 相关题目
- 5.1 [374 . 猜数字大小](https://leetcode-cn.com/problems/guess-number-higher-or-lower/)
- 5.2 [35 . 搜索插入位置](https://leetcode-cn.com/problems/search-insert-position/)
- 5.3 [69 . Sqrt(x)](https://leetcode-cn.com/problems/sqrtx/)
- 5.4 [167 . 两数之和 II - 输入有序数组](https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/)
- 5.5 [1011 . 在 D 天内送达包裹的能力](https://leetcode-cn.com/problems/capacity-to-ship-packages-within-d-days/)
- 5.6 [278 . 第一个错误的版本](https://leetcode-cn.com/problems/first-bad-version/)
- 5.7 [33 . 搜索旋转排序数组](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/)
- 5.8 [153 . 寻找旋转排序数组中的最小值](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/)
- 参考资料
来源
Datewhale31期__LeetCode 刷题 :
-
航路开辟者:杨世超
-
领航员:刘军
-
航海士:杨世超、李彦鹏、叶志雄、赵子一
-
开源电子书
https://algo.itcharge.cn
1 定义、基本思想及步骤
二分查找又称折半查找,它是一种效率较高的查找方法。
二分查找要求:线性表是有序表,即表中结点按关键字有序,并且要用向量作为表的存储结构。不妨设有序表是递增有序的。
-
步骤
-
初始状态下,将整个序列作为搜索区域(假设为
[low, high]
); -
找到搜索区域内的中间元素
half
,和目标元素进行比对。如果相等,则搜索成功;如果中间元素大于目标元素,表明目标元素位于中间元素的左侧,将[low, half-1]
作为新的搜素区域;反之,若中间元素小于目标元素,表明目标元素位于中间元素的右侧,将[half+1, high]
作为新的搜素区域; -
重复执行第二步,直至找到目标元素。如果搜索区域无法再缩小,且区域内不包含任何元素,表明整个序列中没有目标元素,查找失败。
2 简单二分查找python实现
- 以704. 二分查找为例;
class Solution:
def search(self, nums: List[int], target: int) -> int:
low, high = 0, len(nums) - 1
while low <= high:
half = (high + low) // 2
if target == nums[half]:
return half
elif target > nums[half]: # 小于目标值,则在 [half + 1 , high ] 中继续搜索
low = half + 1
elif target < nums[half]: # 大于目标值,则在 [low, half - 1 ] 中继续搜索
high = half - 1
return -1
3 二分查找细节总结
-
初始区间问题.
初始化赋值时, 区间取左闭右闭、左闭右开都可写出相应解法及代码, 但左闭右开这种写法在解决问题的过程中,需要考虑的情况更加复杂,所以建议 全部使用「左闭右闭」区间, 即起始区间为[0, len(nums)]
, 而非[0, len(nums)-1]
。 -
half的取值问题
常见的 half 取值就是half = (low + high) // 2
或者half = low + (high - low) // 2
。前者是最常见写法,后者是为了防止整型溢出。式子中// 2
就代表的含义是中间数「向下取整」------- 取左。
half = (low + high + 1) // 2
,或者half = low + (high - low + 1) // 2
可以做到取右, 但一般来说,取中间位置元素在平均意义下所达到的效果最好。同时这样写最简单。而对于 half 值是向下取整还是向上取整,大多数时候是选择不加 1. -
边界条件问题----------
low <= high
和low < high
low <= high
,且查找的元素不存在,则while
判断语句出界条件是low == high + 1
,写成区间形式就是[high + 1, high]
,此时待查找区间为空,待查找区间中没有元素存在,所以此时终止循环可以直接return -1
是正确的。- 如果判断语句为
low < high
,且查找的元素不存在,则while
判断语句出界条件是low == high
,写成区间形式就是[high, high]
。此时区间不为空,待查找区间还有一个元素存在,并不能确定查找的元素不在这个区间中,此时终止循环return -1
是错误的。 >>>>>> 改为return low if nums[low] == target else -1
, 且不用判断返回low
还是high
.
-
搜索区间范围的选择
区间范围3种情况:
low = half + 1、high = half - 1
-----对应直接找法
low = half + 1 、high = half
---- 对应排除法中half
向下取整(half = low + (high - low) // 2
)的情况
low = half、high = half - 1
---- 对应排除法中half
向上(half = low + (high - low + 1) // 2
)取整的情况
4 二分法两种思路
- 即上面提到的直接找法和排除法
- 直接找法: 在循环体中找到元素就直接返回结果----若
nums[half]
值大于或小于目标值, 分别取区间[low, half - 1 ]
或[half + 1 , high ]
>>>>>>>适用用于简单的,非重复的元素查找值问题 - 排除法: >>>> 适用于可能不存在的元素,找边界问题等复杂问题
(1) 取两个节点中心位置 half,根据判断条件先将目标元素一定不存在的区间排除。
(2) 然后在剩余区间继续查找元素,继续根据条件排除不存在的区间。
(3) 直到区间中只剩下最后一个元素,然后再判断这个元素是否是目标元素。
- 直接找法: 在循环体中找到元素就直接返回结果----若
排除法思路有两种代码:
- 一、区间分为
[half + 1, high]
和[low, half]
; 而half = low + (high - low) // 2
:
class Solution:
def search(self, nums: List[int], target: int) -> int:
low = 0
high = len(nums) - 1
# 在区间 [low, high] 内查找 target
while low < high:
# 取区间中间节点
half = low + (high - low) // 2
# nums[half] 小于目标值,排除掉不可能区间 [low, half],在 [half + 1, high] 中继续搜索
if nums[half] < target:
low = half + 1
# nums[half] 大于等于目标值,目标元素可能在 [low, half] 中,在 [low, half] 中继续搜索
else:
high = half
# 判断区间剩余元素是否为目标元素,不是则返回 -1
return low if nums[low] == target else -1
- 二、区间分为
[low, half - 1]
和[half, high]
; 而half = low + (high - low + 1) // 2
:
class Solution:
def search(self, nums: List[int], target: int) -> int:
low = 0
high = len(nums) - 1
# 在区间 [low, high] 内查找 target
while low < high:
# 取区间中间节点
half = low + (high - low + 1) // 2
# nums[half] 大于目标值,排除掉不可能区间 [half, high],在 [low, half - 1] 中继续搜索
if nums[half] > target:
high = half - 1
# nums[half] 小于等于目标值,目标元素可能在 [half, high] 中,在 [half, high] 中继续搜索
else:
low = half
# 判断区间剩余元素是否为目标元素,不是则返回 -1
return low if nums[low] == target else -1
- 区分被分为两部分:
[low, half - 1] 与 [half, high]
时,half
取值要向上取整。即half = low + (high - low + 1) // 2
。因为如果当区间中只剩下两个元素时(此时high = low + 1
),一旦进入low = half
分支,区间就不会再缩小了,下一次循环的查找区间还是[low, high]
,就陷入了死循环。
5 相关题目
5.1 374 . 猜数字大小
- 1到n随机取数>>>>>序列递增有序>>>>>二分查找
class Solution:
def guessNumber(self, n: int) -> int:
low, high = 1, n
while low <= high:
half = low + (high - low)//2
if guess(half) == 0:
return half
elif guess(half) == -1:
high = half - 1
elif guess(half) == 1:
low = half + 1
5.2 35 . 搜索插入位置
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
low, high = 0, len(nums)-1
while low <= high:
half = low + (high - low) // 2
if nums[half] == target:
return half
elif nums[half] > target:
high = half - 1
else:
low = half + 1
return low
5.3 69 . Sqrt(x)
- 法一----------二分
- 思路: 对于给定的 x , 有
0
≤
x
<
x
2
0\le x<x^2
0≤x<x2 >>>> 对于在区间 [0,x] 判断 x 位于哪个[
n
u
m
2
num^2
num2
, ( n u m + 1 ) 2 (num+1)^2 (num+1)2]区间即可,return num
即可
- 思路: 对于给定的 x , 有
0
≤
x
<
x
2
0\le x<x^2
0≤x<x2 >>>> 对于在区间 [0,x] 判断 x 位于哪个[
n
u
m
2
num^2
num2
class Solution:
def mySqrt(self, x: int) -> int:
low = 0
high = x
while low < high:
mid = low + (high - low + 1) // 2
if mid*mid == x:
return mid
elif mid*mid > x:
high = mid - 1
elif mid*mid < x:
low = mid
return low
5.4 167 . 两数之和 II - 输入有序数组
- 法一------------优先使用二分查找-------O(NlogN)
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
for i in range(len(numbers)):
goal = target - numbers[i]
low = i + 1
high = len(numbers) - 1
while low <= high:
mid = low + (high - low) // 2
if numbers[mid] == goal:
return [ i + 1, mid + 1]
elif numbers[mid] > goal:
high = mid - 1
else:
low = mid + 1
- 法二>>>>>>>>以left和right指针, 和大right左移, 和小left右移
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
left, right = 0, len(numbers) - 1
while left <= right:
sum_num = numbers[left] + numbers[right]
if sum_num == target:
return [left + 1, right + 1]
elif sum_num < target:
left += 1
else:
right -= 1
5.5 1011 . 在 D 天内送达包裹的能力
- 法一: -----------参考答案------二分
- 思路: 船最小的运载能力,最少也要等于或大于最重的那件包裹,即 max(weights)。最多的话,可以一次性将所有包裹运完,即 sum(weights)。船的运载能力介于
[max(weights), sum(weights)]
之间。 >>>> 同时先计算运载能力mid = (high + low) // 2 的运输天数, 再与之D比较进而缩小区间:
- 思路: 船最小的运载能力,最少也要等于或大于最重的那件包裹,即 max(weights)。最多的话,可以一次性将所有包裹运完,即 sum(weights)。船的运载能力介于
class Solution:
def shipWithinDays(self, weights: List[int], days: int) -> int:
low, high = max(weights), sum(weights)
while low < high:
mid = low + (high - low) // 2
# 计算载重为mid 需要多少天运完
cur = 0 # 目前天数的重量
day_mid = 1 # 目前天数
for weight in weights:
if weight + cur > mid:
day_mid += 1
cur = 0
cur += weight
if day_mid > days: #如果天数超了, 则增加载重
low = mid + 1
else:
high = mid
return low
5.6 278 . 第一个错误的版本
- 二分法>>>>>>>>> 获得第一个错误版本
class Solution:
def firstBadVersion(self, n):
"""
:type n: int
:rtype: int
"""
low, high = 1, n
while low < high:
mid = low + (high - low) // 2
if isBadVersion(mid):
high = mid
else:
low = mid + 1
return low
- 若想获得最后一个正确版本可用mid向上取整, 区间在
[mid, high]
或[low, mid-1]
的方法
5.7 33 . 搜索旋转排序数组
class Solution:
def search(self, nums: List[int], target: int) -> int:
k = self.compute_K(nums)
n = len(nums)
if target >= nums[0]: # [0, n- k -1] [n - k, n - 1]两个递增区间分别二分
left = 0
right = n - k -1
else:
left = n - k
right = n - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] > target:
right = mid - 1
else:
left = mid + 1
return -1
# 先二分求k的值
def compute_K(self, nums):
low, high = 0, len(nums) - 1
reference = nums[0]
while low < high:
mid = low + (high - low + 1) // 2
if nums[mid] > reference:
low = mid
else:
high = mid - 1
return len(nums) - 1 - low
5.8 153 . 寻找旋转排序数组中的最小值
- 与上题类似用二分法求k
class Solution:
def findMin(self, nums: List[int]) -> int:
k = self.compute_K(nums)
if k == 0:
return nums[0]
else:
return nums[len(nums) - k]
def compute_K(self, nums):
low, high = 0, len(nums) - 1
reference = nums[0]
while low < high:
mid = low + (high - low + 1) // 2
if nums[mid] > reference:
low = mid
else:
high = mid - 1
return len(nums) - 1 - low