二分查找
1. 二分查找相关知识
1.1 算法介绍:
二分查找算法(Binary Search Algorithm):也叫做折半查找算法、对数查找算法。是一种在有序数组中查找某一特定元素的搜索算法。
注意, 有序数组中的一种搜索算法,有序数组中的一种搜索算法,有序数组中的一种搜索算法。当你看到有序数组时,就可以往二分查找这里想一想,看看能否运用。
基本思想:先确定带查找元素所在的区间范围,再逐步缩小范围,直到找到元素或找不到元素为止。
1.2 基本步骤:
1. 每次查找从数组的中间元素开始,如果中间的元素刚好是要查找的元素,则搜索过程结束;
2. 比较中间元素与目标元素的大小,如果中间元素大,那么搜寻左边;如果中间元素小,那么搜寻右边;**从小区间的中值查找**
3. 如果某一步骤数组为空,则代表找不到
1.3 算法思想:减而治之
减:减少问题的规模
治:解决问题
结合就是**排除法解决问题**
每一次查找,排除掉不可能存在目标元素的区间,在剩下可能存在目标元素
的区间中继续查找。
1.4 二分查找的细节问题
1. 区间的开闭问题:区间应该是左闭右开还是左闭右闭?
左闭右开:即right = len(nums), 区间内的最后一个元素取不到
左闭右闭:即right = len(nums) - 1,区间内的最后一个元素可以取到
两种方式都有各自对应的代码,但相对来说,左闭右开的方式考虑的情况更加复杂,建议全部使用左闭右闭的形式
2. mid的取值问题:mid = (left + right) // 2,还是mid = (left + right + 1) // 2
mid = (left + right) // 2:常见写法
mid = left + (right - left) // 2:防止整形溢出写法
上述的两个写法如果为偶数个数据,取得都是靠左的数据(向下取整),其实,靠右的数据也是可以取的
mid = (left + right + 1) // 2
mid = left + (right - left + 1) // 2
原因:二分查找是通过中间选择的数值来确定下次查找的区间,并不强制要求在中间
位置,靠左靠右都可以,只是在中间的效率更高
3. 出界条件的判断:left <= right还是left < right
left <= right:出循环时,left = right + 1, 此时查找区间为[right+1, right],此时区间为空,返回-1
left < right:出循环时, left = right,此时查找区间为[right, right],
此时区间不为空,直接返回-1是错误的,还需要加一个判断条件
4. 搜索区间范围的选择:left = mid + 1、right = mid - 1、left = mid、 right = mid怎么写
这是我们需要知道,二分查找算法的两个思路:直接找与排除法
- 直接找:在循环体中找到元素直接返回结果
思路:查看中心位置的元素nums[mid]
1. 与目标元素相等,直接返回下表
2. 小于目标元素,将左节点设置为mid + 1, 再在[mid+1, right]区间内寻找
3. 大于目标元素,将右节点设置为mid - 1, 再在[left, mid - 1]区间内寻找
退出循环时,区间内不一定不存在目标元素
适用范围:要查找的元素性质简单,数组中都是非重复元素 - 排除法:在循环体中排除目标元素一定不存在的区间
思路:
1. 取中间位置mid,根据判断条件先将目标元素一定不存在的区间排除
2. 再在剩余区间继续查找元素,继续根据条件排除不存在的区间
3. 直到区间中只剩下最后一个元素,然后再判断这个元素是否是目标元素
适用范围:解决复杂题目,比如查找一个数组里可能不存在的元素,找边界问题等
1.5 两种思路的代码实现:
1. 直接找的代码实现
class Solution:
def search(self, nums:List[int], target:int):
left = 0
right = len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] > target:
right = mid - 1
else:
left = mid + 1
return -1
2. 排除法的代码实现
class Solution:
def search(self, nums, target):
left = 0
right = len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid
return left
2. 二分查找的相关题目
35.搜索查找位置
- 思路:一般二分查找的思路,只不过在判断出来mid与target的大小关系后,可以顺便查看一下与相邻元素的大小关系从而决定是否返回结果
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums) - 1
if target > nums[-1]:
return len(nums)
if target < nums[0]:
return 0
while left <= right:
mid = (right + left) // 2
if nums[mid] == target:
return mid
elif nums[mid] > target:
if nums[mid - 1] < target:
return mid
right = mid - 1
else:
if nums[mid + 1] > target:
return mid + 1
left = mid + 1
374.猜数字大小
- 思路:一般二分查找的思路,只不过在判断mid与target的大小关系时使用了他给定的guess函数来表明大小关系。
- 代码实现
class Solution:
def guessNumber(self, n: int) -> int:
left = 0
right = n
while left <= right:
mid = (left + right) // 2
if guess(mid) == 0:
return mid
elif guess(mid) == 1:
left = mid + 1
else:
right = mid - 1
69.Sqrt(x)
- 思路:使用折半查找,跟上文中的搜索查找位置有异曲同工之妙
- 代码实现:
class Solution:
def mySqrt(self, x: int) -> int:
left, right = 0, x
while left <= right:
mid = (left + right) // 2
if mid * mid == x:
return mid
elif mid * mid < x:
if (mid + 1) ** 2 > x:
return mid
left = mid + 1
else:
if (mid - 1) ** 2 < x:
return mid - 1
right = mid - 1
167.两数之和||-输入有序数组
- 思路:
- 先确定一个数字,再使用二分查找来寻找另外一个数字
注意的点:
1. 二分查找时记得同一个数字不能使用两次 - 双指针法
- 先确定一个数字,再使用二分查找来寻找另外一个数字
- 代码实现
- 二分查找
class Solution: def twoSum(self, numbers: List[int], target: int) -> List[int]: n = len(numbers) for i in range(n): res = target - numbers[i] left = i right = n - 1 while left <= right: mid = (left + right) // 2 if numbers[mid] == res: if i == mid and numbers[mid + 1] == numbers[mid]: return [i + 1, mid + 2] return [i + 1, mid + 1] elif numbers[mid] > res: right = mid - 1 else: left = mid + 1
- 双指针
class Solution: def twoSum(self, numbers: List[int], target: int) -> List[int]: left = 0 right = len(numbers) - 1 while left < right: if numbers[left] + numbers[right] == target: return [left + 1, right + 1] elif numbers[left] + numbers[right] > target: right -= 1 else: left += 1
1011.在D天内送达包裹的能力
- 思路:使用二分查找时,我们需要先设定我们查找元素的左边界以及右边界。在这道题中,左边界即为weights中的最大值(不然最大值没办法运输),右边界即为weights中元素之和(一天之内能运送完所有元素)。
同时,我们还需要对其进行判定(按照对应元素下所需天数的变焦),如果所需天数小于目标天数,则排除掉mid右边的元素;否则,排除掉目标左边的元素。 - 代码实现:
class Solution:
def shipWithinDays(self, weights: List[int], days: int) -> int:
left = max(weights)
right = sum(weights)
while left <= right:
mid = (left + right) // 2
need = 1 # 表示需要的天数
cur = 0 # 表示当天运送物资数
for weight in weights:
if cur + weight > mid:
need += 1
cur = 0
cur += weight
if need <= days:
right = mid - 1
else:
left = mid + 1
return left
278.第一个错误的版本
- 思路:当一个版本为正确版本,则该版本之前的所有版本均为正确版本;当一个版本为错误版本,则该版本之后的所有版本均为错误版本。当我们将范围缩小到只剩一个元素时,即为结果
- 代码实现:
class Solution:
def firstBadVersion(self, n):
left = 1
right = n
while left <= right:
mid = (left + right) // 2
if isBadVersion(mid) == True:
right = mid - 1
else:
left = mid + 1
return left
33.搜索旋转排序数组
- 思路:原本想先使用二分查找找到最大值的位置,将左右两边分为两个有序数组,再使用二分查找在对应区间内区寻找目标元素,可是实现的时候有些许bug哈哈,so还是按照官方题解的思路先写一下,以后会补充第一种思路
- 代码实现:
class Solution:
def search(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
if nums[0] <= nums[mid]:
if nums[0] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else:
if nums[mid] < target <= nums[-1]:
left = mid + 1
else:
right = mid - 1
return -1
153.寻找旋转排序数组中的最小值
-
思路:寻找最小值的下标
第一种情况:
第二种情况:
注意:等号的使用情况以及左右指针的变化情况 -
代码实现:
class Solution:
def findMin(self, nums: List[int]) -> int:
left = 0
right = len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] < nums[right]:
right = mid
else:
left = mid + 1
return nums[left]