1. 算法刷题日课:为什么选择这8道题?
作为一名算法工程师,我坚持每日刷题已有三年多时间。今天要分享的这组题目特别有意思——它们看似分散,实则暗藏联系。从动态规划的打家劫舍到双指针的灵活运用,再到二分查找的边界控制,最后到前缀和的巧妙应用,这8道题基本覆盖了面试中80%的中等难度考点。
选择这些题目组合的原因有三:首先,它们都是LeetCode高频题,实际面试出现概率极高;其次,这些题目之间存在递进关系,比如打家劫舍的DP思想可以迁移到其他问题;最后,这些算法在实际工程中应用广泛,比如电商系统的优惠券分配就会用到类似的滑动窗口优化。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 打家劫舍问题:动态规划的经典教学案例
2.1 问题描述与暴力解法
LeetCode 198题的打家劫舍问题描述很简单:一排房屋,每个房屋存放着特定金额的财物。相邻房屋装有联动的报警系统,如果两间相邻的房屋在同一晚被闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算在不触动警报的情况下,能够偷窃到的最高金额。
暴力解法是考虑所有可能的组合,时间复杂度O(2^n)。这显然不可行,我们需要更聪明的办法。
python复制def rob(nums):
def helper(index):
if index >= len(nums):
return 0
return max(nums[index] + helper(index + 2), helper(index + 1))
return helper(0)
2.2 动态规划解法与优化
定义dp[i]表示从第i间房子开始抢劫能获得的最大金额。状态转移方程为:
dp[i] = max(nums[i] + dp[i+2], dp[i+1])
我们可以用备忘录法将时间复杂度降到O(n),空间复杂度O(n)。进一步观察发现,当前状态只依赖后面两个状态,因此可以优化到O(1)空间:
python复制def rob(nums):
prev2 = prev1 = 0
for num in nums:
curr = max(prev2 + num, prev1)
prev2, prev1 = prev1, curr
return prev1
注意:在实际面试中,面试官通常会要求解释为什么这个状态转移方程成立。关键点在于每个房子只有抢或不抢两种选择,且选择会影响下一个可选的房子。
2.3 变种问题:环形房屋排列
LeetCode 213题将房屋排列改为环形,即第一间和最后一间也算相邻。解决思路是分别计算去掉首元素和去掉尾元素的两种情况的最大值:
python复制def rob(nums):
if len(nums) == 1:
return nums[0]
return max(rob_helper(nums[:-1]), rob_helper(nums[1:]))
3. 双指针技术的精妙应用
3.1 快慢指针:链表中的环检测
双指针技术分为同向指针和相向指针。快慢指针的经典应用是检测链表中是否有环(LeetCode 141):
python复制def hasCycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
这个算法的精妙之处在于:如果有环,快指针最终一定会追上慢指针,就像两个人在环形跑道上跑步,速度快的人最终会套圈速度慢的人。
3.2 滑动窗口:最长无重复子串
LeetCode 3题的滑动窗口解法展示了同向双指针的威力:
python复制def lengthOfLongestSubstring(s):
char_index = {}
left = max_len = 0
for right, char in enumerate(s):
if char in char_index and char_index[char] >= left:
left = char_index[char] + 1
char_index[char] = right
max_len = max(max_len, right - left + 1)
return max_len
这个解法的时间复杂度是O(n),空间复杂度是O(min(m,n)),其中m是字符集大小。实际工程中,这种技术常用于日志分析、用户行为序列处理等场景。
4. 二分查找的边界控制艺术
4.1 基本模板与易错点
二分查找看似简单,但边界条件极易出错。标准模板应该这样写:
python复制def binary_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而不是left < right
- mid计算使用left + (right - left) // 2防止溢出
- 更新边界时要+1/-1,避免死循环
4.2 寻找旋转排序数组中的最小值
LeetCode 153题展示了二分查找在非标准场景的应用:
python复制def findMin(nums):
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] > nums[right]:
left = mid + 1
else:
right = mid
return nums[left]
这个解法的时间复杂度是O(logn)。在工程实践中,类似的技巧可用于处理部分有序的数据,比如某些特定条件下的日志查询优化。
5. 前缀和技术的实战应用
5.1 基本概念与实现
前缀和是一种重要的预处理技术,能够将区间求和的时间复杂度从O(n)降到O(1)。基本实现如下:
python复制class PrefixSum:
def __init__(self, nums):
self.prefix = [0] * (len(nums) + 1)
for i in range(len(nums)):
self.prefix[i+1] = self.prefix[i] + nums[i]
def query(self, i, j):
return self.prefix[j+1] - self.prefix[i]
5.2 应用案例:和为K的子数组
LeetCode 560题展示了前缀和与哈希表的组合应用:
python复制def subarraySum(nums, k):
from collections import defaultdict
prefix_sum = count = 0
sum_count = defaultdict(int)
sum_count[0] = 1
for num in nums:
prefix_sum += num
count += sum_count.get(prefix_sum - k, 0)
sum_count[prefix_sum] += 1
return count
这个解法的时间复杂度是O(n),空间复杂度是O(n)。在实际工程中,这种技术常用于分析时间序列数据中的特定模式,比如金融交易中的特定波动区间检测。
6. 方向控制问题的解决思路
6.1 螺旋矩阵问题
LeetCode 54题要求按照顺时针螺旋顺序返回矩阵中的所有元素。解决这类方向控制问题的关键是定义方向向量和边界条件:
python复制def spiralOrder(matrix):
if not matrix:
return []
rows, cols = len(matrix), len(matrix[0])
directions = [(0,1),(1,0),(0,-1),(-1,0)]
dir_idx = 0
visited = [[False]*cols for _ in range(rows)]
res = []
row, col = 0, 0
for _ in range(rows * cols):
res.append(matrix[row][col])
visited[row][col] = True
next_row, next_col = row + directions[dir_idx][0], col + directions[dir_idx][1]
if not (0 <= next_row < rows and 0 <= next_col < cols and not visited[next_row][next_col]):
dir_idx = (dir_idx + 1) % 4
next_row, next_col = row + directions[dir_idx][0], col + directions[dir_idx][1]
row, col = next_row, next_col
return res
6.2 优化技巧:层层剥离法
更高效的实现是层层剥离矩阵:
python复制def spiralOrder(matrix):
res = []
while matrix:
res += matrix.pop(0)
if matrix and matrix[0]:
for row in matrix:
res.append(row.pop())
if matrix:
res += matrix.pop()[::-1]
if matrix and matrix[0]:
for row in matrix[::-1]:
res.append(row.pop(0))
return res
这种方法在实际处理图像数据时特别有用,比如某些图像处理算法需要按照特定顺序访问像素。
7. 算法组合应用实战
7.1 滑动窗口+前缀和:和为s的最短子数组
LeetCode 209题要求找到数组中满足其和≥s的最短连续子数组。我们可以组合使用滑动窗口和前缀和技术:
python复制def minSubArrayLen(s, nums):
left = total = 0
min_len = float('inf')
for right in range(len(nums)):
total += nums[right]
while total >= s:
min_len = min(min_len, right - left + 1)
total -= nums[left]
left += 1
return min_len if min_len != float('inf') else 0
7.2 二分查找+贪心:分割数组的最大值
LeetCode 410题展示了如何组合多种算法思想:
python复制def splitArray(nums, m):
def can_split(max_sum):
count = 1
current_sum = 0
for num in nums:
current_sum += num
if current_sum > max_sum:
count += 1
current_sum = num
if count > m:
return False
return True
left, right = max(nums), sum(nums)
while left < right:
mid = left + (right - left) // 2
if can_split(mid):
right = mid
else:
left = mid + 1
return left
这种组合思路在实际资源分配问题中很常见,比如分布式系统中的任务分配、云计算资源调度等。
8. 算法刷题的系统性方法
8.1 分类刷题与模式识别
根据我的经验,有效的刷题方法应该是:
- 按算法类型分类刷题(如本周专注DP)
- 每种类型从易到难刷15-20题
- 总结每种算法的模板和变种
- 定期复习高频考题
8.2 调试与优化技巧
当你的解法不正确时:
- 先用小测试用例手动模拟
- 添加打印语句跟踪关键变量
- 对比与标准解法的中间结果差异
- 分析时间/空间复杂度的瓶颈
对于今天的打家劫舍问题,我最初忽略了环形排列的情况,后来通过添加特殊测试用例发现了这个问题。这提醒我们:边界条件测试至关重要。
