力扣(组合)
解析 LeetCode 77. 组合:回溯算法的高效实现
一、题目分析
(一)问题定义
给定整数 n
和 k
,返回 [1, n]
中所有长度为 k
的组合,组合内元素无序且不重复。
(二)核心挑战
高效枚举所有符合条件的组合,避免重复和遗漏,同时通过剪枝优化减少不必要的递归。
二、算法思路:回溯 + 剪枝
(一)回溯思想
通过递归遍历所有可能的元素选择:
- 选择:从当前起始位置开始,选一个数加入组合。
- 递归:基于当前选择,递归处理下一个位置,继续选数。
- 回溯:递归返回后,撤销当前选择,尝试其他数。
(二)剪枝优化
在循环中,通过条件 (n - i + 1 + answerMin.size()) >= k
剪枝:
n - i + 1
表示当前位置i
到n
还剩的可选数数量。answerMin.size()
是已选数数量。- 若剩余可选数 + 已选数 <
k
,说明无法凑够k
个数,直接跳过后续循环。
三、代码解析
class Solution {// 存储当前组合List<Integer> answerMin = new ArrayList<>(); // 存储所有符合条件的组合List<List<Integer>> result = new ArrayList<>(); public List<List<Integer>> combine(int n, int k) {// 从起始位置 1 开始回溯backtracking(n, k, 1); return result;}private void backtracking(int n, int k, int startIndex) {// 终止条件:组合长度达到 kif (answerMin.size() == k) { // 将当前组合加入结果(新建列表避免引用问题)result.add(new ArrayList<>(answerMin)); return;}// 遍历当前层可选的数,i 从 startIndex 开始for (int i = startIndex; // 剪枝:剩余数 + 已选数 >= k 才继续,否则无法凑够 k 个i <= n && (n - i + 1 + answerMin.size()) >= k; i++) { // 选择:将 i 加入当前组合answerMin.add(i); // 递归:处理下一层,起始位置为 i+1backtracking(n, k, i + 1); // 回溯:撤销选择,尝试下一个数answerMin.remove(answerMin.size() - 1); }}
}
(一)流程拆解
- 初始化:
answerMin
存当前组合,result
存所有结果。 - 回溯启动:调用
backtracking
,从startIndex = 1
开始。 - 终止条件:组合长度达
k
时,将当前组合加入result
。 - 遍历与剪枝:循环中,通过剪枝条件跳过无法凑够
k
个数的情况;选择数i
后递归,返回后回溯(移除i
)。 - 结果返回:
result
存储所有组合,作为最终结果。
(二)关键逻辑
- 剪枝优化:减少不必要的递归,提升效率。例如,
n=5, k=3
,若已选 1 个数,当前i=4
,剩余数5-4+1=2
,已选 1,2+1=3
刚好满足k=3
,继续;若i=5
,剩余数1
,1+1=2 < 3
,直接跳过。 - 引用问题:
result.add(new ArrayList<>(answerMin))
确保存储的是当前组合的副本,避免后续修改影响结果。
四、复杂度分析
(一)时间复杂度
- 最坏情况(无剪枝):需枚举所有组合,数量为 C(n,k)C(n, k)C(n,k),每个组合生成时间为 O(k)O(k)O(k),总复杂度 O(k×C(n,k))O(k \times C(n, k))O(k×C(n,k))。
- 剪枝后:减少无效递归,实际复杂度低于最坏情况。
(二)空间复杂度
- 递归栈深度最多为
k
,answerMin
最多存k
个元素,result
存 C(n,k)C(n, k)C(n,k) 个组合。 - 空间复杂度为 O(k+C(n,k))O(k + C(n, k))O(k+C(n,k)),主要受结果存储影响。
通过回溯 + 剪枝高效解决组合枚举问题。回溯实现了所有可能的选择与撤销,剪枝优化减少了无效递归,确保算法在合理时间内运行。