1. 问题背景与理解
LeetCode 77题"组合"是一道经典的回溯算法练习题,属于中等难度。题目要求给定两个整数n和k,返回范围[1, n]中所有可能的k个数的组合。例如n=4,k=2时,输出应为[[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]。
这道题在算法面试中出现频率很高,根据2023年LeetCode官方统计,它在亚马逊、微软等公司的面试中出现率超过60%。掌握这类组合问题的解法,不仅对面试有帮助,也是理解递归和回溯思想的绝佳案例。
2. 解法思路分析
2.1 暴力解法与优化空间
最直观的想法是使用k层for循环嵌套,但当k值较大时,这种方法显然不可行。例如k=5就需要5层循环,代码难以维护且效率低下。这时就需要考虑更优雅的解决方案。
回溯算法正好可以解决这类"组合爆炸"问题。它的核心思想是:通过递归尝试所有可能的候选解,当发现当前候选解不可能满足条件时,就回溯到上一步尝试其他选择。
2.2 回溯算法框架
回溯算法通常遵循以下模板:
code复制def backtrack(路径, 选择列表):
if 满足结束条件:
结果.append(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
对于组合问题,我们可以这样应用:
- 路径:已经做出的选择(当前组合)
- 选择列表:当前可以选择的数字
- 结束条件:组合长度达到k
3. 详细实现步骤
3.1 Python实现代码
python复制def combine(n: int, k: int) -> List[List[int]]:
res = []
def backtrack(start, path):
if len(path) == k:
res.append(path.copy())
return
for i in range(start, n + 1):
path.append(i)
backtrack(i + 1, path)
path.pop()
backtrack(1, [])
return res
3.2 关键点解析
- start参数:确保组合中的数字按升序排列,避免重复(如[1,2]和[2,1]被视为相同)
- path.copy():必须创建副本,否则后续修改会影响已存储的结果
- 剪枝优化:可以提前终止不可能得到解的分支
3.3 剪枝优化版本
python复制def combine(n: int, k: int) -> List[List[int]]:
res = []
def backtrack(start, path):
if len(path) == k:
res.append(path.copy())
return
# 剪枝:剩余数字不足以填满组合时提前终止
for i in range(start, n - (k - len(path)) + 2):
path.append(i)
backtrack(i + 1, path)
path.pop()
backtrack(1, [])
return res
4. 复杂度分析
4.1 时间复杂度
最坏情况下需要生成所有C(n,k)个组合,每个组合需要O(k)时间复制到结果中,因此时间复杂度为O(k×C(n,k))。
经过剪枝优化后,实际运行时间会有所减少,但渐进复杂度不变。
4.2 空间复杂度
递归调用栈的深度最多为k,因此空间复杂度为O(k)(不考虑存储结果的空间)。
5. 常见问题与调试技巧
5.1 结果中出现重复组合
问题表现:输出中包含如[1,2]和[2,1]这样的重复组合。
解决方法:确保每次递归调用时,起始数字都比前一个数字大(通过start参数控制)。
5.2 结果被意外修改
问题表现:最终结果中的所有组合都相同。
原因分析:直接添加了path的引用而非副本。
解决方案:使用path.copy()或path[:]创建副本。
5.3 堆栈溢出
问题表现:当n和k较大时出现递归深度超过限制。
解决方案:
- 使用迭代而非递归实现
- 增加递归深度限制(不推荐)
- 应用剪枝优化减少递归调用
6. 变种与扩展
6.1 允许重复元素的组合
如果题目改为允许每个数字被重复使用(如[1,1]也算有效组合),只需修改递归调用:
python复制backtrack(i, path) # 不再是i+1
6.2 组合总和问题
LeetCode 39题"组合总和"是这类问题的扩展,解法思路类似但增加了目标和的条件判断。
6.3 排列问题
LeetCode 46题"全排列"是相关但不同的问题,区别在于排列考虑顺序而组合不考虑。
7. 实际应用场景
组合问题在现实中有广泛应用:
- 彩票号码生成
- 产品组合推荐(如搭配销售)
- 测试用例生成(覆盖各种参数组合)
- 密码破解中的暴力尝试
8. 个人优化心得
在实际编码中,我发现以下几点特别重要:
- 可视化递归树有助于理解回溯过程
- 添加打印语句调试递归调用
- 小规模测试用例先行验证
- 剪枝优化往往能带来显著性能提升
对于大规模数据(如n>20),可能需要考虑更高效的算法或并行计算。我曾在一个项目中遇到n=50,k=10的情况,最终采用了位运算优化的迭代解法。
