1. LeetCode算法精要:二分查找实战指南
二分查找(Binary Search)是算法学习中最基础也最实用的搜索技术之一。我在刷LeetCode时发现,超过60%的搜索类题目都可以用二分查找或其变种解决。它的核心思想是通过不断缩小搜索范围来快速定位目标,时间复杂度仅为O(log n)。
1.1 标准二分查找实现
我们先看最基础的二分查找实现,以LeetCode第704题为例:
python复制def search(nums: List[int], target: int) -> int:
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 = left + (right - left) // 2的写法比(left+right)//2更安全,避免整数溢出- 每次比较后都要调整边界,
mid±1确保不会重复检查
注意:二分查找的数组必须是有序的,这是使用前提。如果题目给出无序数组,通常需要先排序。
1.2 二分查找的四种变体
实际题目中,我们经常需要处理以下变种情况:
- 查找第一个等于目标的值(LeetCode 34题)
python复制def first_equal(nums, target):
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 nums[left] == target else -1
- 查找最后一个等于目标的值
python复制def last_equal(nums, target):
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 nums[right] == target else -1
- 查找第一个大于等于目标的值(LeetCode 35题)
python复制def first_gte(nums, target):
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
- 查找最后一个小于等于目标的值
python复制def last_lte(nums, target):
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
1.3 二分查找的典型应用场景
-
旋转排序数组搜索(LeetCode 33题)
处理部分有序数组时,需要先判断哪一半是有序的:python复制def search_rotated(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 -
在无限序列中查找(LeetCode 702题)
先找到合适的搜索范围,再用标准二分:python复制def search_infinite(nums, target): left, right = 0, 1 while nums[right] < target: left = right right *= 2 return binary_search(nums, left, right, target) -
峰值查找(LeetCode 162题)
利用二分查找局部最大值:python复制def find_peak(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
2. 数组交集问题的多种解法
数组交集是面试中的高频考点,LeetCode上有多个变种题(如349、350题)。根据不同的需求,我们可以采用不同的解法。
2.1 两个数组的交集I(去重版)
LeetCode 349题要求返回两个数组的交集,且结果中的每个元素必须是唯一的。
哈希表解法(时间复杂度O(m+n)):
python复制def intersection(nums1, nums2):
set1 = set(nums1)
set2 = set(nums2)
return list(set1 & set2)
排序+双指针解法(当数组已排序时更高效):
python复制def intersection_sorted(nums1, nums2):
nums1.sort()
nums2.sort()
i = j = 0
res = []
while i < len(nums1) and j < len(nums2):
if nums1[i] == nums2[j]:
if not res or nums1[i] != res[-1]:
res.append(nums1[i])
i += 1
j += 1
elif nums1[i] < nums2[j]:
i += 1
else:
j += 1
return res
2.2 两个数组的交集II(包含重复元素)
LeetCode 350题要求返回所有共同元素,包括重复出现的。
哈希表计数解法:
python复制def intersect(nums1, nums2):
from collections import defaultdict
count = defaultdict(int)
for num in nums1:
count[num] += 1
res = []
for num in nums2:
if count[num] > 0:
res.append(num)
count[num] -= 1
return res
排序+双指针解法:
python复制def intersect_sorted(nums1, nums2):
nums1.sort()
nums2.sort()
i = j = 0
res = []
while i < len(nums1) and j < len(nums2):
if nums1[i] == nums2[j]:
res.append(nums1[i])
i += 1
j += 1
elif nums1[i] < nums2[j]:
i += 1
else:
j += 1
return res
2.3 多个数组的交集
当需要处理多个数组的交集时(如LeetCode 2248题),可以采用逐步缩小交集的方法:
python复制def intersection_multiple(nums):
if not nums:
return []
common = set(nums[0])
for arr in nums[1:]:
common &= set(arr)
if not common:
break
return sorted(common)
2.4 进阶:带区间限制的交集问题
有些题目会加入额外的限制条件,比如LeetCode 986题(区间列表的交集):
python复制def interval_intersection(A, B):
i = j = 0
res = []
while i < len(A) and j < len(B):
low = max(A[i][0], B[j][0])
high = min(A[i][1], B[j][1])
if low <= high:
res.append([low, high])
if A[i][1] < B[j][1]:
i += 1
else:
j += 1
return res
3. 环形链表检测与相关算法
环形链表是链表类题目中的经典问题,LeetCode 141和142题分别要求检测环和找到环的起点。
3.1 快慢指针法检测环
python复制def hasCycle(head):
if not head or not head.next:
return False
slow = head
fast = head.next
while slow != fast:
if not fast or not fast.next:
return False
slow = slow.next
fast = fast.next.next
return True
3.2 找到环的入口节点
python复制def detectCycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
break
else:
return None
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
原理说明:
- 设链表头到环入口距离为a,环入口到相遇点距离为b,相遇点到环入口距离为c
- 快指针路程:a + n(b+c) + b
- 慢指针路程:a + b
- 因为快指针速度是慢指针2倍:2(a+b) = a + n(b+c) + b
- 化简得:a = c + (n-1)(b+c)
- 这意味着从head和相遇点同时出发,必在环入口相遇
3.3 环形链表相关变种题
-
寻找链表中点(LeetCode 876题)
python复制def middleNode(head): slow = fast = head while fast and fast.next: slow = slow.next fast = fast.next.next return slow -
判断快乐数(LeetCode 202题)
将数字替换为各位平方和的过程看作链表,用快慢指针检测环:python复制def isHappy(n): def get_next(num): total = 0 while num > 0: num, digit = divmod(num, 10) total += digit ** 2 return total slow = n fast = get_next(n) while fast != 1 and slow != fast: slow = get_next(slow) fast = get_next(get_next(fast)) return fast == 1 -
环形数组问题(LeetCode 457题)
将数组视为链表,用快慢指针检测循环:python复制def circularArrayLoop(nums): n = len(nums) for i in range(n): if nums[i] == 0: continue slow = fast = i direction = nums[i] > 0 while True: slow = (slow + nums[slow]) % n if nums[fast] == 0: break fast = (fast + nums[fast]) % n if nums[fast] == 0: break fast = (fast + nums[fast]) % n if slow == fast: if slow == (slow + nums[slow]) % n: break return True if (nums[slow] > 0) != direction or (nums[fast] > 0) != direction: break # 标记已访问 ptr = i while nums[ptr] != 0 and (nums[ptr] > 0) == direction: next_ptr = (ptr + nums[ptr]) % n nums[ptr] = 0 ptr = next_ptr return False
4. 算法组合应用实战
在实际面试和竞赛中,经常需要组合使用这些算法技巧。我们来看几个典型例子。
4.1 二分查找+双指针解决复杂问题
LeetCode 167题(两数之和II - 输入有序数组)可以用双指针高效解决:
python复制def twoSum(numbers, target):
left, right = 0, len(numbers) - 1
while left < right:
s = numbers[left] + numbers[right]
if s == target:
return [left + 1, right + 1]
elif s < target:
left += 1
else:
right -= 1
return [-1, -1]
更复杂的LeetCode 18题(四数之和)则需要结合双指针和剪枝:
python复制def fourSum(nums, target):
nums.sort()
n = len(nums)
res = []
for i in range(n - 3):
if i > 0 and nums[i] == nums[i - 1]:
continue
for j in range(i + 1, n - 2):
if j > i + 1 and nums[j] == nums[j - 1]:
continue
left, right = j + 1, n - 1
while left < right:
total = nums[i] + nums[j] + nums[left] + nums[right]
if total == target:
res.append([nums[i], nums[j], nums[left], nums[right]])
while left < right and nums[left] == nums[left + 1]:
left += 1
while left < right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
elif total < target:
left += 1
else:
right -= 1
return res
4.2 链表与二分查找的结合
LeetCode 109题(有序链表转换二叉搜索树)需要找到链表中点作为根节点:
python复制def sortedListToBST(head):
if not head:
return None
if not head.next:
return TreeNode(head.val)
# 找到中点前一个节点
slow, fast = head, head.next.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 分割链表
mid = slow.next
slow.next = None
root = TreeNode(mid.val)
root.left = sortedListToBST(head)
root.right = sortedListToBST(mid.next)
return root
4.3 环形检测与数学结合
LeetCode 287题(寻找重复数)可以转化为环形链表检测问题:
python复制def findDuplicate(nums):
slow = fast = nums[0]
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break
slow = nums[0]
while slow != fast:
slow = nums[slow]
fast = nums[fast]
return slow
这个解法将数组视为链表,利用快慢指针找到环的入口,也就是重复的数字。
5. 算法优化技巧与常见错误
在解决这些算法问题时,有一些常见的优化技巧和易犯错误需要注意。
5.1 二分查找的边界处理
二分查找最容易出错的就是边界条件。以下是一些经验法则:
-
初始边界:
- 通常
left = 0,right = len(nums) - 1 - 如果可能超出数组范围,使用
right = len(nums)
- 通常
-
循环条件:
left <= right:确保检查所有元素left < right:适用于查找插入位置等情况
-
中间值计算:
- 优先使用
mid = left + (right - left) // 2防止溢出 - 在特定情况下可能需要调整,如
mid = left + (right - left + 1) // 2
- 优先使用
-
边界更新:
left = mid + 1和right = mid - 1是标准做法- 在某些变种中可能需要
left = mid或right = mid
5.2 双指针法的常见模式
双指针法有多种变体,适用于不同场景:
-
同向指针:两个指针从同一端出发,一快一慢
- 应用:移除元素、去重、滑动窗口
-
对向指针:两个指针从两端向中间移动
- 应用:两数之和、反转数组、盛水问题
-
分离指针:两个指针分别遍历不同数组/链表
- 应用:合并有序数组、链表相交
5.3 环形链表问题的注意事项
- 空指针检查:始终检查
head和head.next是否为空 - 快指针移动:确保
fast.next存在再访问fast.next.next - 环入口证明:理解数学原理而不仅是记忆代码
- 多指针应用:有时需要三个或更多指针解决复杂问题
5.4 性能优化技巧
- 提前终止:在搜索过程中发现不可能情况时立即返回
- 剪枝:在回溯或DFS中跳过不可能的分支
- 空间换时间:合理使用哈希表等数据结构存储中间结果
- 预处理:对数据进行排序或建立索引加速后续操作
- 位运算:在适当场景使用位运算提升效率
5.5 调试与验证方法
- 小数据测试:用最小可能输入验证边界条件
- 打印中间状态:在关键步骤输出变量值
- 可视化调试:对于链表问题,画图辅助理解
- 对拍测试:用暴力解法验证优化解法的正确性
- 压力测试:用最大规模输入检查性能瓶颈
在实际刷题过程中,我建议先从标准解法开始,理解透彻后再尝试优化。对于每道题目,至少要能解释清楚时间复杂度和空间复杂度,并思考是否有改进空间。
