二分法基本步骤
- 初始化边界:
- 定义左边界
left
和右边界right
,通常初始化为数组的起始索引和结束索引。
- 定义左边界
- 计算中间位置:
- 计算中间位置
mid
,通常使用公式left + (right - left) // 2
。
- 计算中间位置
- 比较中间元素:
- 将中间位置的元素
arr[mid]
与目标元素target
进行比较。 - 如果
arr[mid] == target
,则找到目标元素,返回mid
。 - 如果
arr[mid] < target
,则目标元素在右半部分,更新左边界left = mid + 1
。 - 如果
arr[mid] > target
,则目标元素在左半部分,更新右边界right = mid - 1
。
- 将中间位置的元素
- 重复步骤 2 和 3:
- 不断缩小查找范围,直到左边界超过右边界,表示目标元素不存在于数组中。
35、搜索插入位置
链接:35、搜索插入位置
题目描述:
方法 1
# 方法 1:二分法(闭区间)
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
# 初始化左边界和右边界
left, right = 0, len(nums) - 1
# 开始二分查找
while left <= right:
# 计算中间位置,避免直接使用 (left + right) // 2 可能导致的整数溢出
mid = left + (right - left) // 2
# 如果中间元素等于目标值,返回中间位置
if nums[mid] == target:
return mid
# 如果中间元素小于目标值,目标值在右半部分,更新左边界
elif nums[mid] < target:
left = mid + 1
# 如果中间元素大于目标值,目标值在左半部分,更新右边界
else:
right = mid - 1
# 如果目标值不存在于数组中,返回插入位置
return left
代码解释
- 初始化边界:
- left 初始化为 0,表示数组的起始索引。
- right 初始化为 len(nums) - 1,表示数组的结束索引。
- 循环查找:
- 使用 while left <= right 循环,确保查找范围有效。
- 计算中间位置 mid,使用公式 mid = left + (right - left) // 2,避免直接使用 (left + right) // 2 可能导致的整数溢出。
- 比较中间元素:
- 如果 nums[mid] == target,则找到目标元素,返回 mid。
- 如果 nums[mid] < target,则目标元素在右半部分,更新左边界 left = mid + 1。
- 如果 nums[mid] > target,则目标元素在左半部分,更新右边界 right = mid - 1。
- 返回插入位置:
- 如果循环结束后仍未找到目标元素,返回 left,表示目标值的插入位置。
复杂度分析
- 时间复杂度:O(log n),其中 n 是数组的长度。每次查找范围减半,因此时间复杂度为对数级别。
- 空间复杂度:O(1),只使用了常数级别的额外空间。
方法 2
lower_bound
函数:返回最小的满足 nums[i] >= target
的 i
# 方法 2:lower_bound
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
return lower_bound(nums, target)
复杂度分析
- 时间复杂度:O(log n),其中 n 是数组的长度。每次查找范围减半,因此时间复杂度为对数级别。
- 空间复杂度:O(1),只使用了常数级别的额外空间。
方法 3
bisect_left
函数:用于在有序列表中查找插入位置。它返回一个位置,使得插入元素后列表仍然保持有序。如果元素已经存在于列表中,返回它的左侧位置。
# 方法 3:bisect_left
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
return lower_bound(nums, target)
复杂度分析
- 时间复杂度:O(log n)
- 空间复杂度:O(1)
74、搜索二维矩阵
链接:74、搜索二维矩阵
题目描述:
方法 1
思路:
由于矩阵的每一行是递增的,且每行的第一个数大于前一行的最后一个数,如果把矩阵每一行拼在一起,我们可以得到一个递增数组。
例如示例 1
,三行拼在一起得 a=[1,3,5,7,10,11,16,20,23,30,34,60]
。由于这是一个有序数组,我们可以用二分查找判断 target
是否在 matrix
中。
转化方式: a[i]=matrix[⌊i/n⌋][i%n]
# 方法 1:二分法
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
# 检查矩阵是否为空或矩阵的第一行是否为空
if not matrix or not matrix[0]:
return False
# 获取矩阵的行数和列数
m, n = len(matrix), len(matrix[0])
# 初始化左边界和右边界
left, right = 0, m * n - 1
# 开始二分查找
while left <= right:
# 计算中间位置,避免直接使用 (left + right) // 2 可能导致的整数溢出
mid = left + (right - left) // 2
# 将中间位置映射回二维矩阵中的行和列
mid_value = matrix[mid // n][mid % n]
# 如果中间元素等于目标值,返回 True
if mid_value == target:
return True
# 如果中间元素小于目标值,目标值在右半部分,更新左边界
elif mid_value < target:
left = mid + 1
# 如果中间元素大于目标值,目标值在左半部分,更新右边界
else:
right = mid - 1
# 如果循环结束后仍未找到目标值,返回 False
return False
代码解释
- 检查矩阵是否为空:
- 如果矩阵为空或矩阵的第一行为空,返回
False
。
- 如果矩阵为空或矩阵的第一行为空,返回
- 初始化边界:
m
是矩阵的行数,n
是矩阵的列数。left
初始化为 0,表示一维数组的起始索引。right
初始化为m * n - 1
,表示一维数组的结束索引。
- 循环查找:
- 使用
while left <= right
循环,确保查找范围有效。 - 计算中间位置
mid
,并将其映射回二维矩阵中的行和列。 - 比较中间元素
mid_value
与目标值target
。 - 如果
mid_value == target
,返回True
。 - 如果
mid_value < target
,目标值在右半部分,更新左边界left = mid + 1
。 - 如果
mid_value > target
,目标值在左半部分,更新右边界right = mid - 1
。
- 使用
- 返回结果:
- 如果循环结束后仍未找到目标值,返回
False
。
- 如果循环结束后仍未找到目标值,返回
复杂度分析
- 时间复杂度:O(log(m * n)),其中 m 是矩阵的行数,n 是矩阵的列数。每次查找范围减半,因此时间复杂度为对数级别。
- 空间复杂度:O(1),只使用了常数级别的额外空间。
方法 2
思路:这种方法利用了矩阵的有序性,从右上角开始搜索。如果当前元素大于目标值,则向左移动;如果当前元素小于目标值,则向下移动。这样可以确保每一步都缩小搜索范围。
# 方法 2:从右上角开始的搜索
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
# 检查矩阵是否为空或矩阵的第一行是否为空
if not matrix or not matrix[0]:
return False
# 获取矩阵的行数和列数
m, n = len(matrix), len(matrix[0])
# 从右上角开始搜索
row, col = 0, n - 1
# 当行索引在矩阵范围内且列索引在矩阵范围内时继续搜索
while row < m and col >= 0:
# 如果当前元素等于目标值,返回 True
if matrix[row][col] == target:
return True
# 如果当前元素大于目标值,向左移动一列
elif matrix[row][col] > target:
col -= 1
# 如果当前元素小于目标值,向下移动一行
else:
row += 1
# 如果循环结束后仍未找到目标值,返回 False
return False
代码解释
- 检查矩阵是否为空:
- 如果矩阵为空或矩阵的第一行为空,返回
False
。
- 如果矩阵为空或矩阵的第一行为空,返回
- 获取矩阵的行数和列数:
m
是矩阵的行数,n
是矩阵的列数。
- 初始化搜索起点:
- 从右上角开始搜索,初始化
row
为 0,col
为n - 1
。
- 从右上角开始搜索,初始化
- 循环查找:
- 使用
while row < m and col >= 0
循环,确保行索引和列索引在矩阵范围内。 - 如果当前元素
matrix[row][col]
等于目标值,返回True
。 - 如果当前元素
matrix[row][col]
大于目标值,向左移动一列,即col -= 1
。 - 如果当前元素
matrix[row][col]
小于目标值,向下移动一行,即row += 1
。
- 使用
- 返回结果:
- 如果循环结束后仍未找到目标值,返回
False
。
- 如果循环结束后仍未找到目标值,返回
复杂度分析
- 时间复杂度:O(m + n),其中 m 是矩阵的行数,n 是矩阵的列数。每一步要么向左移动一列,要么向下移动一行。
- 空间复杂度:O(1),只使用了常数级别的额外空间。
方法 3
思路:对每一行进行二分查找,查找目标值是否在当前行中。虽然时间复杂度较高,但实现简单。
代码实现
# 方法 3: 逐行二分查找
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
for row in matrix:
# 使用二分查找在当前行中查找目标值
index = bisect.bisect_left(row, target)
if index < len(row) and row[index] == target:
return True
return False
复杂度分析
- 时间复杂度:O(m * log n),其中 m 是矩阵的行数,n 是矩阵的列数。对每一行进行二分查找。
- 空间复杂度:O(1),只使用了常数级别的额外空间。
162、寻找峰值
链接:162、寻找峰值
题目描述:
方法 1
思路:由于左右元素不相同,因此nums
的最大值不会在一起,因此找到最大值的index
就一定是峰值。
# 方法 1:最大值
class Solution:
def findPeakElement(self, nums: List[int]) -> int:
return nums.index(max(nums))
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
方法 2
作者:灵茶山艾府
# 方法 2:二分法
class Solution:
def findPeakElement(self, nums: List[int]) -> int:
# 初始化左右边界,使用开区间 (-1, len(nums) - 1)
left, right = -1, len(nums) - 1
# 当开区间不为空时继续搜索
while left + 1 < right:
# 计算中间位置
mid = (left + right) // 2
# 如果中间元素大于其右侧元素,则峰值在左侧(包括mid)
if nums[mid] > nums[mid + 1]:
right = mid
# 否则,峰值在右侧
else:
left = mid
# 返回右边界,即峰值元素的索引
return right
# 作者:灵茶山艾府
# 链接:https://leetcode.cn/problems/find-peak-element/solutions/1987497/by-endlesscheng-9ass/
# 来源:力扣(LeetCode)
复杂度分析
- 时间复杂度:O(log n),其中 n 是数组的长度。
- 空间复杂度:O(1)。
33、搜索旋转排序数组
链接:33、搜索旋转排序数组
题目描述:
方法 1
思考过程:
- 初始化左右边界:
- 初始化
left
为数组的起始索引0
,right 为数组的结束索引len(nums) - 1
。
- 初始化
- 二分查找:
- 计算中间位置
mid
。 - 判断
nums[mid]
是否等于目标值target
,如果是则返回mid
。 - 如果
nums[mid]
不等于目标值,则需要判断目标值在左半部分还是右半部分。- 如果
nums[left] <= nums[mid]
,说明左半部分是有序的。- 如果
target
在nums[left]
和nums[mid]
之间,则在左半部分继续查找,否则在右半部分继续查找。
- 如果
- 如果
nums[left] > nums[mid]
,说明右半部分是有序的。- 如果
target
在nums[mid]
和nums[right]
之间,则在右半部分继续查找,否则在左半部分继续查找。
- 如果
- 如果
- 计算中间位置
- 返回结果:
- 如果在整个查找过程中没有找到目标值,则返回
-1
。
- 如果在整个查找过程中没有找到目标值,则返回
# 方法 1:二分类
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
# 判断左半部分是否有序
if nums[left] <= nums[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
代码解释
- 初始化边界:
left
初始化为0
,表示数组的起始索引。right
初始化为len(nums) - 1
,表示数组的结束索引。
- 循环查找:
- 使用
while left <= right
循环,确保查找范围有效。 - 计算中间位置
mid
,使用公式mid = (left + right) // 2
。 - 如果
nums[mid]
等于目标值target
,则返回mid
。 - 判断左半部分是否有序:
- 如果
nums[left] <= nums[mid]
,说明左半部分是有序的。- 如果目标值
target
在nums[left]
和nums[mid]
之间,则在左半部分继续查找,更新右边界right = mid - 1
。 - 否则,在右半部分继续查找,更新左边界
left = mid + 1
。
- 如果目标值
- 如果
nums[left] > nums[mid]
,说明右半部分是有序的。- 如果目标值
target
在nums[mid]
和nums[right]
之间,则在右半部分继续查找,更新左边界left = mid + 1
。 - 否则,在左半部分继续查找,更新右边界
right = mid - 1
。
- 如果目标值
- 如果
- 使用
- 返回结果:
- 如果在整个查找过程中没有找到目标值,则返回
-1
。
- 如果在整个查找过程中没有找到目标值,则返回
复杂度分析
- 时间复杂度:O(log n),其中 n 是数组的长度。二分查找每次将搜索范围减半。
- 空间复杂度:O(1)
方法 2
这种方法是最直接的,通过遍历整个数组来查找目标值。虽然简单,但时间复杂度为 O(n),不符合题目要求。
# 方法 2:线性扫描法
class Solution:
def search(self, nums: List[int], target: int) -> int:
for i in range(len(nums)):
if nums[i] == target:
return i
return -1
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)