1. 二分查找算法基础与力扣实战指南
二分查找(Binary Search)作为计算机科学中最经典且高效的搜索算法之一,其核心思想是通过不断缩小搜索范围来快速定位目标元素。这个看似简单的算法在实际应用中却蕴含着许多值得深入探讨的细节和技巧,尤其是在力扣(LeetCode)这类编程题库中,二分查找相关题目往往成为区分初级和中级开发者的重要分水岭。
1.1 二分查找的核心原理
二分查找算法建立在有序数据集的基础上,通过比较中间元素与目标值的大小关系,每次迭代都将搜索范围减半。其时间复杂度为O(log n),远优于线性搜索的O(n)。这种对数级的时间复杂度在处理大规模数据时优势尤为明显。
算法基本步骤如下:
- 确定初始搜索范围的左右边界(通常为数组的首尾索引)
- 计算中间位置 mid = left + (right - left) / 2(这种写法可防止整数溢出)
- 比较中间元素与目标值:
- 若相等,返回结果
- 若中间元素小于目标值,调整左边界
- 若中间元素大于目标值,调整右边界
- 重复上述过程直到找到目标或搜索范围为空
注意:边界条件的处理是二分查找最容易出错的地方,特别是在处理重复元素或目标不存在的情况时,需要特别注意循环终止条件和边界更新逻辑。
1.2 力扣平台上的二分查找题目特点
力扣上的二分查找题目大致可分为三类:
- 标准二分查找:如第704题,直接实现经典二分查找
- 变种二分查找:如寻找边界、旋转数组搜索等
- 二分查找的应用:如求解数学问题的最优解
这些题目往往考察以下几个关键点:
- 边界条件的正确处理能力
- 对循环不变量的理解
- 在复杂场景下应用二分思想的能力
- 代码的简洁性和鲁棒性
2. 力扣经典二分查找题目解析
2.1 基础题目:704. 二分查找
这是最基础的二分查找实现题,要求在一个有序数组中查找目标值,存在则返回索引,否则返回-1。虽然题目简单,但正确处理边界条件至关重要。
python复制def search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
关键点说明:
- 循环条件使用
left <= right确保能处理单元素数组 - 中间值计算使用防溢出写法
- 边界更新时
mid ± 1避免死循环
2.2 进阶题目:34. 在排序数组中查找元素的第一个和最后一个位置
这道题要求找出目标值在数组中的开始和结束位置,考察对二分查找变种的理解。解题思路是分别实现查找左边界和右边界的函数。
python复制def searchRange(nums, target):
def find_left():
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] >= target:
right = mid - 1
else:
left = mid + 1
return left if left < len(nums) and nums[left] == target else -1
def find_right():
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] <= target:
left = mid + 1
else:
right = mid - 1
return right if right >= 0 and nums[right] == target else -1
return [find_left(), find_right()]
提示:边界查找的关键在于当
nums[mid] == target时如何处理。查找左边界时继续向左搜索,查找右边界时继续向右搜索。
2.3 困难题目:4. 寻找两个正序数组的中位数
这道Hard题目要求在两个有序数组中找出中位数,时间复杂度要求O(log(m+n))。解题思路是将问题转化为在两个数组中寻找第k小的元素,使用二分查找每次排除k/2个元素。
python复制def findMedianSortedArrays(nums1, nums2):
def get_kth_element(k):
index1, index2 = 0, 0
while True:
if index1 == m:
return nums2[index2 + k - 1]
if index2 == n:
return nums1[index1 + k - 1]
if k == 1:
return min(nums1[index1], nums2[index2])
new_index1 = min(index1 + k // 2 - 1, m - 1)
new_index2 = min(index2 + k // 2 - 1, n - 1)
pivot1, pivot2 = nums1[new_index1], nums2[new_index2]
if pivot1 <= pivot2:
k -= new_index1 - index1 + 1
index1 = new_index1 + 1
else:
k -= new_index2 - index2 + 1
index2 = new_index2 + 1
m, n = len(nums1), len(nums2)
total = m + n
if total % 2 == 1:
return get_kth_element((total + 1) // 2)
else:
return (get_kth_element(total // 2) + get_kth_element(total // 2 + 1)) / 2
这道题的关键在于理解如何通过比较两个数组的k/2位置的元素来排除不可能包含第k小元素的那部分数组。
3. 二分查找的常见变种与解题技巧
3.1 旋转排序数组中的搜索
旋转排序数组是指一个原本有序的数组在某个点进行了旋转,例如[4,5,6,7,0,1,2]。在这类数组中搜索需要特殊的二分查找技巧。
以力扣33题为例,解题思路是通过比较中间元素与左右边界来确定哪一半是有序的,然后根据目标值是否在有序的那一半来决定搜索方向。
python复制def search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 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
3.2 寻找峰值元素
力扣162题要求在无序数组中找到任意一个峰值元素(大于相邻元素)。这道题的关键在于理解即使数组无序,仍然可以使用二分查找,因为只要比较中间元素与其右侧元素,就能确定峰值在哪一侧。
python复制def findPeakElement(nums):
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] > nums[mid + 1]:
right = mid
else:
left = mid + 1
return left
3.3 在无限序列中查找
有些题目假设输入是无限序列(如力扣702题),此时无法直接获取长度。解决方法是通过指数级扩大搜索范围(如1,2,4,8,...)先确定可能的上界,再进行常规二分查找。
python复制def searchInfiniteArray(reader, target):
left, right = 0, 1
while reader.get(right) < target:
left = right
right *= 2
while left <= right:
mid = left + (right - left) // 2
val = reader.get(mid)
if val == target:
return mid
elif val < target:
left = mid + 1
else:
right = mid - 1
return -1
4. 二分查找的常见陷阱与调试技巧
4.1 死循环问题
二分查找中最常见的错误是导致死循环,通常由边界更新不当引起。例如:
- 在
left = mid的情况下,如果left和right相邻且mid计算向下取整,可能导致无限循环 - 解决方法:确保每次迭代搜索范围都会缩小,必要时可以打印左右边界进行调试
4.2 边界条件处理
不同题目可能需要不同的边界条件:
while left <= rightvswhile left < rightright = mid - 1vsright = mid- 返回值是
left还是right
经验法则:
- 当搜索空间是闭区间
[left, right]时,使用left <= right - 当需要确保找到确切位置时,使用
left <= right - 当只需要缩小到一个可能范围时,可以使用
left < right
4.3 调试技巧
- 打印关键变量:在循环中打印left, mid, right的值,观察搜索范围的变化
- 小数据测试:用长度为0、1、2的数组测试边界情况
- 重复元素测试:特别测试包含重复元素的数组
- 目标不存在测试:确保算法能正确处理目标不存在的情况
4.4 常见错误模式
| 错误类型 | 表现 | 解决方法 |
|---|---|---|
| 整数溢出 | 大数组时mid计算溢出 | 使用left + (right - left) // 2 |
| 边界更新错误 | 死循环或提前终止 | 明确搜索区间开闭性质 |
| 返回值错误 | 返回错误索引 | 理解循环不变量,明确最终状态 |
| 初始条件错误 | 未考虑空数组 | 添加前置检查 |
5. 二分查找在力扣Hot100中的应用
力扣Hot100中有多道题目可以使用二分查找或其变种来解决。以下是几道典型题目:
5.1 力扣69. x的平方根
要求计算x的平方根的整数部分。可以使用二分查找在0到x之间搜索最大的整数n满足n² ≤ x。
python复制def mySqrt(x):
if x < 2:
return x
left, right = 2, x // 2
while left <= right:
mid = left + (right - left) // 2
num = mid * mid
if num == x:
return mid
elif num < x:
left = mid + 1
else:
right = mid - 1
return right
5.2 力扣287. 寻找重复数
给定包含n+1个整数的数组,数字在1到n之间,假设只有一个重复数,找出它。可以使用二分查找统计小于等于mid的数的个数。
python复制def findDuplicate(nums):
left, right = 1, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
count = 0
for num in nums:
if num <= mid:
count += 1
if count > mid:
right = mid
else:
left = mid + 1
return left
5.3 力扣300. 最长递增子序列
可以使用二分查找将时间复杂度优化到O(n log n)。维护一个tails数组,其中tails[i]表示长度为i+1的所有递增子序列的最小末尾。
python复制def lengthOfLIS(nums):
tails = []
for num in nums:
left, right = 0, len(tails)
while left < right:
mid = left + (right - left) // 2
if tails[mid] < num:
left = mid + 1
else:
right = mid
if left == len(tails):
tails.append(num)
else:
tails[left] = num
return len(tails)
6. 二分查找的扩展应用与优化技巧
6.1 在答案集上进行二分查找
有些问题可以转化为在可能的答案集上进行二分查找。例如力扣410. 分割数组的最大值,可以通过二分查找可能的子数组和来解决。
python复制def splitArray(nums, m):
def feasible(threshold):
count = 1
total = 0
for num in nums:
total += num
if total > threshold:
total = num
count += 1
if count > m:
return False
return True
left, right = max(nums), sum(nums)
while left < right:
mid = left + (right - left) // 2
if feasible(mid):
right = mid
else:
left = mid + 1
return left
6.2 浮点数二分查找
当问题涉及浮点数精度时,可以使用二分查找来逼近解。例如力扣50. Pow(x, n)可以通过二分查找快速计算幂。
python复制def myPow(x, n):
def fastPow(x, n):
if n == 0:
return 1.0
half = fastPow(x, n // 2)
if n % 2 == 0:
return half * half
else:
return half * half * x
if n < 0:
x = 1 / x
n = -n
return fastPow(x, n)
6.3 二维矩阵中的二分查找
力扣240. 搜索二维矩阵II这类题目可以在二维矩阵中应用二分思想。从左下角或右上角开始,根据比较结果决定移动方向。
python复制def searchMatrix(matrix, target):
if not matrix or not matrix[0]:
return False
row, col = len(matrix) - 1, 0
while row >= 0 and col < len(matrix[0]):
if matrix[row][col] == target:
return True
elif matrix[row][col] < target:
col += 1
else:
row -= 1
return False
在实际编程面试中,二分查找类题目考察的不仅是算法实现能力,更重要的是对边界条件的处理和对算法本质的理解。通过系统地练习力扣上的二分查找题目,开发者可以培养出解决这类问题的直觉和技巧。
