子集排列组合.md 43.8 KB
Newer Older
L
labuladong 已提交
1 2
---
title: '一文秒杀所有排列组合子集问题'
L
labuladong 已提交
3
tags: ['回溯算法']
L
labuladong 已提交
4
---
L
labuladong 已提交
5

6 7
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
L
labuladong 已提交
8
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
9 10 11 12
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>

L
labuladong 已提交
13
![](https://labuladong.github.io/pictures/souyisou1.png)
14

L
labuladong 已提交
15
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V2.1,[手把手刷二叉树系列课程](https://aep.xet.tech/s/3YGcq3) 上线。[第 18 期每日打卡](https://aep.xet.tech/s/2PLO1n) 开始报名。反馈或修正 chatGPT 翻译的多语言代码 [点击这里](https://github.com/labuladong/fucking-algorithm/issues/1113)。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
16 17 18



L
labuladong 已提交
19
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
20

L
labuladong 已提交
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [216. Combination Sum III](https://leetcode.com/problems/combination-sum-iii/) | [216. 组合总和 III](https://leetcode.cn/problems/combination-sum-iii/) | 🟠
| [39. Combination Sum](https://leetcode.com/problems/combination-sum/) | [39. 组合总和](https://leetcode.cn/problems/combination-sum/) | 🟠
| [40. Combination Sum II](https://leetcode.com/problems/combination-sum-ii/) | [40. 组合总和 II](https://leetcode.cn/problems/combination-sum-ii/) | 🟠
| [46. Permutations](https://leetcode.com/problems/permutations/) | [46. 全排列](https://leetcode.cn/problems/permutations/) | 🟠
| [47. Permutations II](https://leetcode.com/problems/permutations-ii/) | [47. 全排列 II](https://leetcode.cn/problems/permutations-ii/) | 🟠
| [77. Combinations](https://leetcode.com/problems/combinations/) | [77. 组合](https://leetcode.cn/problems/combinations/) | 🟠
| [78. Subsets](https://leetcode.com/problems/subsets/) | [78. 子集](https://leetcode.cn/problems/subsets/) | 🟠
| [90. Subsets II](https://leetcode.com/problems/subsets-ii/) | [90. 子集 II](https://leetcode.cn/problems/subsets-ii/) | 🟠
| - | [剑指 Offer II 079. 所有子集](https://leetcode.cn/problems/TVdhkn/) | 🟠
| - | [剑指 Offer II 080. 含有 k 个元素的组合](https://leetcode.cn/problems/uUsW3B/) | 🟠
| - | [剑指 Offer II 081. 允许重复选择元素的组合](https://leetcode.cn/problems/Ygoe9J/) | 🟠
| - | [剑指 Offer II 082. 含有重复元素集合的组合](https://leetcode.cn/problems/4sjJUc/) | 🟠
| - | [剑指 Offer II 083. 没有重复元素集合的全排列](https://leetcode.cn/problems/VvJkup/) | 🟠
| - | [剑指 Offer II 084. 含有重复元素集合的全排列](https://leetcode.cn/problems/7p8L0Z/) | 🟠
37 38 39

**-----------**

L
labuladong 已提交
40
> tip:本文有视频版:[回溯算法秒杀所有排列/组合/子集问题](https://www.bilibili.com/video/BV1Yt4y1t7dK/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。
L
labuladong 已提交
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67

虽然排列、组合、子集系列问题是高中就学过的,但如果想编写算法解决它们,还是非常考验计算机思维的,本文就讲讲编程解决这几个问题的核心思路,以后再有什么变体,你也能手到擒来,以不变应万变。

无论是排列、组合还是子集问题,简单说无非就是让你从序列 `nums` 中以给定规则取若干元素,主要有以下几种变体:

**形式一、元素无重不可复选,即 `nums` 中的元素都是唯一的,每个元素最多只能被使用一次,这也是最基本的形式**

以组合为例,如果输入 `nums = [2,3,6,7]`,和为 7 的组合应该只有 `[7]`

**形式二、元素可重不可复选,即 `nums` 中的元素可以存在重复,每个元素最多只能被使用一次**

以组合为例,如果输入 `nums = [2,5,2,1,2]`,和为 7 的组合应该有两种 `[2,2,2,1]``[5,2]`

**形式三、元素无重可复选,即 `nums` 中的元素都是唯一的,每个元素可以被使用若干次**

以组合为例,如果输入 `nums = [2,3,6,7]`,和为 7 的组合应该有两种 `[2,2,3]``[7]`

当然,也可以说有第四种形式,即元素可重可复选。但既然元素可复选,那又何必存在重复元素呢?元素去重之后就等同于形式三,所以这种情况不用考虑。

上面用组合问题举的例子,但排列、组合、子集问题都可以有这三种基本形式,所以共有 9 种变化。

除此之外,题目也可以再添加各种限制条件,比如让你求和为 `target` 且元素个数为 `k` 的组合,那这么一来又可以衍生出一堆变体,怪不得面试笔试中经常考到排列组合这种基本题型。

**但无论形式怎么变化,其本质就是穷举所有解,而这些解呈现树形结构,所以合理使用回溯算法框架,稍改代码框架即可把这些问题一网打尽**

具体来说,你需要先阅读并理解前文 [回溯算法核心套路](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版),然后记住如下子集问题和排列问题的回溯树,就可以解决所有排列组合子集相关的问题:

L
labuladong 已提交
68
![](https://labuladong.github.io/pictures/排列组合/1.jpeg)
L
labuladong 已提交
69

L
labuladong 已提交
70
![](https://labuladong.github.io/pictures/排列组合/2.jpeg)
L
labuladong 已提交
71 72 73 74 75 76

为什么只要记住这两种树形结构就能解决所有相关问题呢?

**首先,组合问题和子集问题其实是等价的,这个后面会讲;至于之前说的三种变化形式,无非是在这两棵树上剪掉或者增加一些树枝罢了**

那么,接下来我们就开始穷举,把排列/组合/子集问题的 9 种形式都过一遍,学学如何用回溯算法把它们一套带走。
L
labuladong 已提交
77

L
labuladong 已提交
78
### 子集(元素无重不可复选)
L
labuladong 已提交
79

L
labuladong 已提交
80
力扣第 78 题「子集」就是这个问题:
L
labuladong 已提交
81

L
labuladong 已提交
82
题目给你输入一个无重复元素的数组 `nums`,其中每个元素最多使用一次,请你返回 `nums` 的所有子集。
L
labuladong 已提交
83

L
labuladong 已提交
84 85
函数签名如下:

L
labuladong 已提交
86
<!-- muliti_language -->
L
labuladong 已提交
87 88
```java
List<List<Integer>> subsets(int[] nums)
L
labuladong 已提交
89 90
```

L
labuladong 已提交
91 92 93 94 95
比如输入 `nums = [1,2,3]`,算法应该返回如下子集:

```java
[ [],[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3] ]
```
L
labuladong 已提交
96

L
labuladong 已提交
97
好,我们暂时不考虑如何用代码实现,先回忆一下我们的高中知识,如何手推所有子集?
L
labuladong 已提交
98

L
labuladong 已提交
99
首先,生成元素个数为 0 的子集,即空集 `[]`,为了方便表示,我称之为 `S_0`
L
labuladong 已提交
100

L
labuladong 已提交
101
然后,在 `S_0` 的基础上生成元素个数为 1 的所有子集,我称为 `S_1`
L
labuladong 已提交
102

L
labuladong 已提交
103
![](https://labuladong.github.io/pictures/排列组合/3.jpeg)
L
labuladong 已提交
104

L
labuladong 已提交
105
接下来,我们可以在 `S_1` 的基础上推导出 `S_2`,即元素个数为 2 的所有子集:
L
labuladong 已提交
106

L
labuladong 已提交
107
![](https://labuladong.github.io/pictures/排列组合/4.jpeg)
L
labuladong 已提交
108

L
labuladong 已提交
109
为什么集合 `[2]` 只需要添加 `3`,而不添加前面的 `1` 呢?
L
labuladong 已提交
110

L
labuladong 已提交
111
因为集合中的元素不用考虑顺序, `[1,2,3]``2` 后面只有 `3`,如果你向前考虑 `1`,那么 `[2,1]` 会和之前已经生成的子集 `[1,2]` 重复。
L
labuladong 已提交
112

L
labuladong 已提交
113
**换句话说,我们通过保证元素之间的相对顺序不变来防止出现重复的子集**
L
labuladong 已提交
114

L
labuladong 已提交
115
接着,我们可以通过 `S_2` 推出 `S_3`,实际上 `S_3` 中只有一个集合 `[1,2,3]`,它是通过 `[1,2]` 推出的。
L
labuladong 已提交
116

L
labuladong 已提交
117
整个推导过程就是这样一棵树:
L
labuladong 已提交
118

L
labuladong 已提交
119
![](https://labuladong.github.io/pictures/排列组合/5.jpeg)
L
labuladong 已提交
120

L
labuladong 已提交
121
注意这棵树的特性:
L
labuladong 已提交
122

L
labuladong 已提交
123
**如果把根节点作为第 0 层,将每个节点和根节点之间树枝上的元素作为该节点的值,那么第 `n` 层的所有节点就是大小为 `n` 的所有子集**
L
labuladong 已提交
124

L
labuladong 已提交
125 126
你比如大小为 2 的子集就是这一层节点的值:

L
labuladong 已提交
127
![](https://labuladong.github.io/pictures/排列组合/6.jpeg)
L
labuladong 已提交
128

L
labuladong 已提交
129
> info:**注意,本文之后所说「节点的值」都是指节点和根节点之间树枝上的元素,且将根节点认为是第 0 层**。
L
labuladong 已提交
130 131 132 133 134

那么再进一步,如果想计算所有子集,那只要遍历这棵多叉树,把所有节点的值收集起来不就行了?

直接看代码:

L
labuladong 已提交
135
<!-- muliti_language -->
L
labuladong 已提交
136
```java
L
labuladong 已提交
137
class Solution {
L
labuladong 已提交
138

L
labuladong 已提交
139 140 141
    List<List<Integer>> res = new LinkedList<>();
    // 记录回溯算法的递归路径
    LinkedList<Integer> track = new LinkedList<>();
L
labuladong 已提交
142

L
labuladong 已提交
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
    // 主函数
    public List<List<Integer>> subsets(int[] nums) {
        backtrack(nums, 0);
        return res;
    }

    // 回溯算法核心函数,遍历子集问题的回溯树
    void backtrack(int[] nums, int start) {

        // 前序位置,每个节点的值都是一个子集
        res.add(new LinkedList<>(track));
        
        // 回溯算法标准框架
        for (int i = start; i < nums.length; i++) {
            // 做选择
            track.addLast(nums[i]);
            // 通过 start 参数控制树枝的遍历,避免产生重复的子集
            backtrack(nums, i + 1);
            // 撤销选择
            track.removeLast();
        }
L
labuladong 已提交
164 165
    }
}
L
labuladong 已提交
166 167
```

L
labuladong 已提交
168
看过前文 [回溯算法核心框架](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 的读者应该很容易理解这段代码吧,我们使用 `start` 参数控制树枝的生长避免产生重复的子集,用 `track` 记录根节点到每个节点的路径的值,同时在前序位置把每个节点的路径值收集起来,完成回溯树的遍历就收集了所有子集:
L
labuladong 已提交
169

L
labuladong 已提交
170
![](https://labuladong.github.io/pictures/排列组合/5.jpeg)
L
labuladong 已提交
171

L
labuladong 已提交
172
最后,`backtrack` 函数开头看似没有 base case,会不会进入无限递归?
L
labuladong 已提交
173

L
labuladong 已提交
174 175 176 177 178 179 180 181 182 183 184
其实不会的,当 `start == nums.length` 时,叶子节点的值会被装入 `res`,但 for 循环不会执行,也就结束了递归。

### 组合(元素无重不可复选)

如果你能够成功的生成所有无重子集,那么你稍微改改代码就能生成所有无重组合了。

你比如说,让你在 `nums = [1,2,3]` 中拿 2 个元素形成所有的组合,你怎么做?

稍微想想就会发现,大小为 2 的所有组合,不就是所有大小为 2 的子集嘛。

**所以我说组合和子集是一样的:大小为 `k` 的组合就是大小为 `k` 的子集**
L
labuladong 已提交
185

L
labuladong 已提交
186
比如力扣第 77 题「组合」:
L
labuladong 已提交
187

L
labuladong 已提交
188
给定两个整数 `n``k`,返回范围 `[1, n]` 中所有可能的 `k` 个数的组合。
L
labuladong 已提交
189

L
labuladong 已提交
190
函数签名如下:
L
labuladong 已提交
191

L
labuladong 已提交
192
<!-- muliti_language -->
L
labuladong 已提交
193 194 195 196 197
```java
List<List<Integer>> combine(int n, int k)
```

比如 `combine(3, 2)` 的返回值应该是:
L
labuladong 已提交
198

L
labuladong 已提交
199 200
```java
[ [1,2],[1,3],[2,3] ]
L
labuladong 已提交
201 202
```

L
labuladong 已提交
203 204 205 206 207
这是标准的组合问题,但我给你翻译一下就变成子集问题了:

**给你输入一个数组 `nums = [1,2..,n]` 和一个正整数 `k`,请你生成所有大小为 `k` 的子集**

还是以 `nums = [1,2,3]` 为例,刚才让你求所有子集,就是把所有节点的值都收集起来;**现在你只需要把第 2 层(根节点视为第 0 层)的节点收集起来,就是大小为 2 的所有组合**
L
labuladong 已提交
208

L
labuladong 已提交
209
![](https://labuladong.github.io/pictures/排列组合/6.jpeg)
L
labuladong 已提交
210

L
labuladong 已提交
211 212
反映到代码上,只需要稍改 base case,控制算法仅仅收集第 `k` 层节点的值即可:

L
labuladong 已提交
213
<!-- muliti_language -->
L
labuladong 已提交
214
```java
L
labuladong 已提交
215
class Solution {
L
labuladong 已提交
216

L
labuladong 已提交
217 218 219 220 221 222 223 224
    List<List<Integer>> res = new LinkedList<>();
    // 记录回溯算法的递归路径
    LinkedList<Integer> track = new LinkedList<>();

    // 主函数
    public List<List<Integer>> combine(int n, int k) {
        backtrack(1, n, k);
        return res;
L
labuladong 已提交
225
    }
L
labuladong 已提交
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243

    void backtrack(int start, int n, int k) {
        // base case
        if (k == track.size()) {
            // 遍历到了第 k 层,收集当前节点的值
            res.add(new LinkedList<>(track));
            return;
        }
        
        // 回溯算法标准框架
        for (int i = start; i <= n; i++) {
            // 选择
            track.addLast(i);
            // 通过 start 参数控制树枝的遍历,避免产生重复的子集
            backtrack(i + 1, n, k);
            // 撤销选择
            track.removeLast();
        }
L
labuladong 已提交
244 245 246 247
    }
}
```

L
labuladong 已提交
248
这样,标准的子集问题也解决了。
L
labuladong 已提交
249

L
labuladong 已提交
250
### 排列(元素无重不可复选)
L
labuladong 已提交
251

L
labuladong 已提交
252
排列问题在前文 [回溯算法核心框架](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 讲过,这里就简单过一下。
L
labuladong 已提交
253

L
labuladong 已提交
254
力扣第 46 题「全排列」就是标准的排列问题:
L
labuladong 已提交
255

L
labuladong 已提交
256
给定一个**不含重复数字**的数组 `nums`,返回其所有可能的**全排列**
L
labuladong 已提交
257

L
labuladong 已提交
258 259
函数签名如下:

L
labuladong 已提交
260
<!-- muliti_language -->
L
labuladong 已提交
261 262
```java
List<List<Integer>> permute(int[] nums)
L
labuladong 已提交
263 264
```

L
labuladong 已提交
265
比如输入 `nums = [1,2,3]`,函数的返回值应该是:
L
labuladong 已提交
266

L
labuladong 已提交
267
```java
L
labuladong 已提交
268
[
L
labuladong 已提交
269 270 271
    [1,2,3],[1,3,2],
    [2,1,3],[2,3,1],
    [3,1,2],[3,2,1]
L
labuladong 已提交
272
]
L
labuladong 已提交
273 274 275 276 277
```

刚才讲的组合/子集问题使用 `start` 变量保证元素 `nums[start]` 之后只会出现 `nums[start+1..]` 中的元素,通过固定元素的相对位置保证不出现重复的子集。

**但排列问题本身就是让你穷举元素的位置,`nums[i]` 之后也可以出现 `nums[i]` 左边的元素,所以之前的那一套玩不转了,需要额外使用 `used` 数组来标记哪些元素还可以被选择**
L
labuladong 已提交
278

L
labuladong 已提交
279
标准全排列可以抽象成如下这棵多叉树:
L
labuladong 已提交
280

L
labuladong 已提交
281
![](https://labuladong.github.io/pictures/排列组合/7.jpeg)
L
labuladong 已提交
282

L
labuladong 已提交
283
我们用 `used` 数组标记已经在路径上的元素避免重复选择,然后收集所有叶子节点上的值,就是所有全排列的结果:
L
labuladong 已提交
284

L
labuladong 已提交
285
<!-- muliti_language -->
L
labuladong 已提交
286
```java
L
labuladong 已提交
287
class Solution {
L
labuladong 已提交
288

L
labuladong 已提交
289 290 291 292 293 294 295 296 297 298 299
    List<List<Integer>> res = new LinkedList<>();
    // 记录回溯算法的递归路径
    LinkedList<Integer> track = new LinkedList<>();
    // track 中的元素会被标记为 true
    boolean[] used;

    /* 主函数,输入一组不重复的数字,返回它们的全排列 */
    public List<List<Integer>> permute(int[] nums) {
        used = new boolean[nums.length];
        backtrack(nums);
        return res;
L
labuladong 已提交
300
    }
L
labuladong 已提交
301

L
labuladong 已提交
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
    // 回溯算法核心函数
    void backtrack(int[] nums) {
        // base case,到达叶子节点
        if (track.size() == nums.length) {
            // 收集叶子节点上的值
            res.add(new LinkedList(track));
            return;
        }

        // 回溯算法标准框架
        for (int i = 0; i < nums.length; i++) {
            // 已经存在 track 中的元素,不能重复选择
            if (used[i]) {
                continue;
            }
            // 做选择
            used[i] = true;
            track.addLast(nums[i]);
            // 进入下一层回溯树
            backtrack(nums);
            // 取消选择
            track.removeLast();
            used[i] = false;
L
labuladong 已提交
325 326 327 328 329 330 331 332 333 334 335
        }
    }
}
```

这样,全排列问题就解决了。

但如果题目不让你算全排列,而是让你算元素个数为 `k` 的排列,怎么算?

也很简单,改下 `backtrack` 函数的 base case,仅收集第 `k` 层的节点值即可:

L
labuladong 已提交
336
<!-- muliti_language -->
L
labuladong 已提交
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
```java
// 回溯算法核心函数
void backtrack(int[] nums, int k) {
    // base case,到达第 k 层,收集节点的值
    if (track.size() == k) {
        // 第 k 层节点的值就是大小为 k 的排列
        res.add(new LinkedList(track));
        return;
    }

    // 回溯算法标准框架
    for (int i = 0; i < nums.length; i++) {
        // ...
        backtrack(nums, k);
        // ...
L
labuladong 已提交
352 353 354 355
    }
}
```

L
labuladong 已提交
356 357 358 359 360
### 子集/组合(元素可重不可复选)

刚才讲的标准子集问题输入的 `nums` 是没有重复元素的,但如果存在重复元素,怎么处理呢?

力扣第 90 题「子集 II」就是这样一个问题:
L
labuladong 已提交
361

L
labuladong 已提交
362
给你一个整数数组 `nums`,其中可能包含重复元素,请你返回该数组所有可能的子集。
L
labuladong 已提交
363

L
labuladong 已提交
364
函数签名如下:
L
labuladong 已提交
365

L
labuladong 已提交
366
<!-- muliti_language -->
L
labuladong 已提交
367 368
```java
List<List<Integer>> subsetsWithDup(int[] nums)
L
labuladong 已提交
369 370
```

L
labuladong 已提交
371
比如输入 `nums = [1,2,2]`,你应该输出:
L
labuladong 已提交
372

L
labuladong 已提交
373 374 375 376 377 378 379 380 381 382
```java
[ [],[1],[2],[1,2],[2,2],[1,2,2] ]
```

当然,按道理说「集合」不应该包含重复元素的,但既然题目这样问了,我们就忽略这个细节吧,仔细思考一下这道题怎么做才是正事。

就以 `nums = [1,2,2]` 为例,为了区别两个 `2` 是不同元素,后面我们写作 `nums = [1,2,2']`

按照之前的思路画出子集的树形结构,显然,两条值相同的相邻树枝会产生重复:

L
labuladong 已提交
383
![](https://labuladong.github.io/pictures/排列组合/8.jpeg)
L
labuladong 已提交
384

L
labuladong 已提交
385
```text
L
labuladong 已提交
386 387 388 389 390
[ 
    [],
    [1],[2],[2'],
    [1,2],[1,2'],[2,2'],
    [1,2,2']
L
labuladong 已提交
391
]
L
labuladong 已提交
392
```
L
labuladong 已提交
393

L
labuladong 已提交
394
你可以看到,`[2]``[1,2]` 这两个结果出现了重复,所以我们需要进行剪枝,如果一个节点有多条值相同的树枝相邻,则只遍历第一条,剩下的都剪掉,不要去遍历:
L
labuladong 已提交
395

L
labuladong 已提交
396
![](https://labuladong.github.io/pictures/排列组合/9.jpeg)
L
labuladong 已提交
397

L
labuladong 已提交
398
**体现在代码上,需要先进行排序,让相同的元素靠在一起,如果发现 `nums[i] == nums[i-1]`,则跳过**
L
labuladong 已提交
399

L
labuladong 已提交
400
<!-- muliti_language -->
L
labuladong 已提交
401
```java
L
labuladong 已提交
402
class Solution {
L
labuladong 已提交
403

L
labuladong 已提交
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
    List<List<Integer>> res = new LinkedList<>();
    LinkedList<Integer> track = new LinkedList<>();

    public List<List<Integer>> subsetsWithDup(int[] nums) {
        // 先排序,让相同的元素靠在一起
        Arrays.sort(nums);
        backtrack(nums, 0);
        return res;
    }

    void backtrack(int[] nums, int start) {
        // 前序位置,每个节点的值都是一个子集
        res.add(new LinkedList<>(track));
        
        for (int i = start; i < nums.length; i++) {
            // 剪枝逻辑,值相同的相邻树枝,只遍历第一条
            if (i > start && nums[i] == nums[i - 1]) {
                continue;
            }
            track.addLast(nums[i]);
            backtrack(nums, i + 1);
            track.removeLast();
L
labuladong 已提交
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
        }
    }
}
```

这段代码和之前标准的子集问题的代码几乎相同,就是添加了排序和剪枝的逻辑。

至于为什么要这样剪枝,结合前面的图应该也很容易理解,这样带重复元素的子集问题也解决了。

**我们说了组合问题和子集问题是等价的**,所以我们直接看一道组合的题目吧,这是力扣第 40 题「组合总和 II」:

给你输入 `candidates` 和一个目标和 `target`,从 `candidates` 中找出中所有和为 `target` 的组合。

`candidates` 可能存在重复元素,且其中的每个数字最多只能使用一次。

说这是一个组合问题,其实换个问法就变成子集问题了:请你计算 `candidates` 中所有和为 `target` 的子集。

所以这题怎么做呢?

对比子集问题的解法,只要额外用一个 `trackSum` 变量记录回溯路径上的元素和,然后将 base case 改一改即可解决这道题:
446

L
labuladong 已提交
447
<!-- muliti_language -->
L
labuladong 已提交
448
```java
L
labuladong 已提交
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
class Solution {

    List<List<Integer>> res = new LinkedList<>();
    // 记录回溯的路径
    LinkedList<Integer> track = new LinkedList<>();
    // 记录 track 中的元素之和
    int trackSum = 0;

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        if (candidates.length == 0) {
            return res;
        }
        // 先排序,让相同的元素靠在一起
        Arrays.sort(candidates);
        backtrack(candidates, 0, target);
L
labuladong 已提交
464 465
        return res;
    }
L
labuladong 已提交
466

L
labuladong 已提交
467 468 469 470 471 472 473 474 475 476 477
    // 回溯算法主函数
    void backtrack(int[] nums, int start, int target) {
        // base case,达到目标和,找到符合条件的组合
        if (trackSum == target) {
            res.add(new LinkedList<>(track));
            return;
        }
        // base case,超过目标和,直接结束
        if (trackSum > target) {
            return;
        }
L
labuladong 已提交
478

L
labuladong 已提交
479 480 481 482 483 484 485 486 487 488 489 490 491 492
        // 回溯算法标准框架
        for (int i = start; i < nums.length; i++) {
            // 剪枝逻辑,值相同的树枝,只遍历第一条
            if (i > start && nums[i] == nums[i - 1]) {
                continue;
            }
            // 做选择
            track.add(nums[i]);
            trackSum += nums[i];
            // 递归遍历下一层回溯树
            backtrack(nums, i + 1, target);
            // 撤销选择
            track.removeLast();
            trackSum -= nums[i];
L
labuladong 已提交
493 494 495 496 497 498 499 500 501 502 503
        }
    }
}
```

### 排列(元素可重不可复选)

排列问题的输入如果存在重复,比子集/组合问题稍微复杂一点,我们看看力扣第 47 题「全排列 II」:

给你输入一个可包含重复数字的序列 `nums`,请你写一个算法,返回所有可能的全排列,函数签名如下:

L
labuladong 已提交
504
<!-- muliti_language -->
L
labuladong 已提交
505 506 507 508 509 510 511 512 513 514 515 516
```java
List<List<Integer>> permuteUnique(int[] nums)
```

比如输入 `nums = [1,2,2]`,函数返回:

```java
[ [1,2,2],[2,1,2],[2,2,1] ]
```

先看解法代码:

L
labuladong 已提交
517
<!-- muliti_language -->
L
labuladong 已提交
518
```java
L
labuladong 已提交
519
class Solution {
L
labuladong 已提交
520

L
labuladong 已提交
521 522 523 524 525 526 527 528 529 530
    List<List<Integer>> res = new LinkedList<>();
    LinkedList<Integer> track = new LinkedList<>();
    boolean[] used;

    public List<List<Integer>> permuteUnique(int[] nums) {
        // 先排序,让相同的元素靠在一起
        Arrays.sort(nums);
        used = new boolean[nums.length];
        backtrack(nums);
        return res;
L
labuladong 已提交
531 532
    }

L
labuladong 已提交
533 534 535 536
    void backtrack(int[] nums) {
        if (track.size() == nums.length) {
            res.add(new LinkedList(track));
            return;
L
labuladong 已提交
537
        }
L
labuladong 已提交
538 539 540 541 542 543 544 545 546 547 548 549 550 551

        for (int i = 0; i < nums.length; i++) {
            if (used[i]) {
                continue;
            }
            // 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
                continue;
            }
            track.add(nums[i]);
            used[i] = true;
            backtrack(nums);
            track.removeLast();
            used[i] = false;
L
labuladong 已提交
552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621
        }
    }
}
```

你对比一下之前的标准全排列解法代码,这段解法代码只有两处不同:

1、对 `nums` 进行了排序。

2、添加了一句额外的剪枝逻辑。

类比输入包含重复元素的子集/组合问题,你大概应该理解这么做是为了防止出现重复结果。

但是注意排列问题的剪枝逻辑,和子集/组合问题的剪枝逻辑略有不同:新增了 `!used[i - 1]` 的逻辑判断。

这个地方理解起来就需要一些技巧性了,且听我慢慢到来。为了方便研究,依然把相同的元素用上标 `'` 以示区别。

假设输入为 `nums = [1,2,2']`,标准的全排列算法会得出如下答案:

```
[
    [1,2,2'],[1,2',2],
    [2,1,2'],[2,2',1],
    [2',1,2],[2',2,1]
]
```

显然,这个结果存在重复,比如 `[1,2,2']``[1,2',2]` 应该只被算作同一个排列,但被算作了两个不同的排列。

所以现在的关键在于,如何设计剪枝逻辑,把这种重复去除掉?

**答案是,保证相同元素在排列中的相对位置保持不变**

比如说 `nums = [1,2,2']` 这个例子,我保持排列中 `2` 一直在 `2'` 前面。

这样的话,你从上面 6 个排列中只能挑出 3 个排列符合这个条件:

```
[ [1,2,2'],[2,1,2'],[2,2',1] ]
```

这也就是正确答案。

进一步,如果 `nums = [1,2,2',2'']`,我只要保证重复元素 `2` 的相对位置固定,比如说 `2 -> 2' -> 2''`,也可以得到无重复的全排列结果。

仔细思考,应该很容易明白其中的原理:

**标准全排列算法之所以出现重复,是因为把相同元素形成的排列序列视为不同的序列,但实际上它们应该是相同的;而如果固定相同元素形成的序列顺序,当然就避免了重复**

那么反映到代码上,你注意看这个剪枝逻辑:

```java
// 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
    // 如果前面的相邻相等元素没有用过,则跳过
    continue;
}
// 选择 nums[i]
```

**当出现重复元素时,比如输入 `nums = [1,2,2',2'']`,`2'` 只有在 `2` 已经被使用的情况下才会被选择,同理,`2''` 只有在 `2'` 已经被使用的情况下才会被选择,这就保证了相同元素在排列中的相对位置保证固定**

这里拓展一下,如果你把上述剪枝逻辑中的 `!used[i - 1]` 改成 `used[i - 1]`,其实也可以通过所有测试用例,但效率会有所下降,这是为什么呢?

之所以这样修改不会产生错误,是因为这种写法相当于维护了 `2'' -> 2' -> 2` 的相对顺序,最终也可以实现去重的效果。

但为什么这样写效率会下降呢?因为这个写法剪掉的树枝不够多。

比如输入 `nums = [2,2',2'']`,产生的回溯树如下:

L
labuladong 已提交
622
![](https://labuladong.github.io/pictures/排列组合/12.jpeg)
L
labuladong 已提交
623 624 625

如果用绿色树枝代表 `backtrack` 函数遍历过的路径,红色树枝代表剪枝逻辑的触发,那么 `!used[i - 1]` 这种剪枝逻辑得到的回溯树长这样:

L
labuladong 已提交
626
![](https://labuladong.github.io/pictures/排列组合/13.jpeg)
L
labuladong 已提交
627 628 629

`used[i - 1]` 这种剪枝逻辑得到的回溯树如下:

L
labuladong 已提交
630
![](https://labuladong.github.io/pictures/排列组合/14.jpeg)
L
labuladong 已提交
631 632 633 634 635

可以看到,`!used[i - 1]` 这种剪枝逻辑剪得干净利落,而 `used[i - 1]` 这种剪枝逻辑虽然也能得到无重结果,但它剪掉的树枝较少,存在的无效计算较多,所以效率会差一些。

当然,关于排列去重,也有读者提出别的剪枝思路:

L
labuladong 已提交
636
<!-- muliti_language -->
L
labuladong 已提交
637
```java
L
labuladong 已提交
638 639 640 641 642
void backtrack(int[] nums, LinkedList<Integer> track) {
    if (track.size() == nums.length) {
        res.add(new LinkedList(track));
        return;
    }
L
labuladong 已提交
643 644 645 646

    // 记录之前树枝上元素的值
    // 题目说 -10 <= nums[i] <= 10,所以初始化为特殊值
    int prevNum = -666;
L
labuladong 已提交
647 648
    for (int i = 0; i < nums.length; i++) {
        // 排除不合法的选择
L
labuladong 已提交
649
        if (used[i]) {
L
labuladong 已提交
650
            continue;
L
labuladong 已提交
651 652 653 654 655
        }
        if (nums[i] == prevNum) {
            continue;
        }

L
labuladong 已提交
656
        track.add(nums[i]);
L
labuladong 已提交
657 658 659 660
        used[i] = true;
        // 记录这条树枝上的值
        prevNum = nums[i];

L
labuladong 已提交
661
        backtrack(nums, track);
L
labuladong 已提交
662 663 664 665 666 667 668 669 670

        track.removeLast();
        used[i] = false;
    }
}
```

这个思路也是对的,设想一个节点出现了相同的树枝:

L
labuladong 已提交
671
![](https://labuladong.github.io/pictures/排列组合/11.jpeg)
L
labuladong 已提交
672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704

如果不作处理,这些相同树枝下面的子树也会长得一模一样,所以会出现重复的排列。

因为排序之后所有相等的元素都挨在一起,所以只要用 `prevNum` 记录前一条树枝的值,就可以避免遍历值相同的树枝,从而避免产生相同的子树,最终避免出现重复的排列。

好了,这样包含重复输入的排列问题也解决了。

### 子集/组合(元素无重可复选)

终于到了最后一种类型了:输入数组无重复元素,但每个元素可以被无限次使用。

直接看力扣第 39 题「组合总和」:

给你一个无重复元素的整数数组 `candidates` 和一个目标和 `target`,找出 `candidates` 中可以使数字和为目标数 `target` 的所有组合。`candidates` 中的每个数字可以无限制重复被选取。

函数签名如下:

```java
List<List<Integer>> combinationSum(int[] candidates, int target)
```

比如输入 `candidates = [1,2,3], target = 3`,算法应该返回:

```
[ [1,1,1],[1,2],[3] ]
```

这道题说是组合问题,实际上也是子集问题:`candidates` 的哪些子集的和为 `target`

想解决这种类型的问题,也得回到回溯树上,**我们不妨先思考思考,标准的子集/组合问题是如何保证不重复使用元素的**

答案在于 `backtrack` 递归时输入的参数 `start`

L
labuladong 已提交
705
<!-- muliti_language -->
L
labuladong 已提交
706 707 708 709 710 711 712 713 714 715 716 717 718 719
```java
// 无重组合的回溯算法框架
void backtrack(int[] nums, int start) {
    for (int i = start; i < nums.length; i++) {
        // ...
        // 递归遍历下一层回溯树,注意参数
        backtrack(nums, i + 1);
        // ...
    }
}
```

这个 `i``start` 开始,那么下一层回溯树就是从 `start + 1` 开始,从而保证 `nums[start]` 这个元素不会被重复使用:

L
labuladong 已提交
720
![](https://labuladong.github.io/pictures/排列组合/1.jpeg)
L
labuladong 已提交
721 722 723

那么反过来,如果我想让每个元素被重复使用,我只要把 `i + 1` 改成 `i` 即可:

L
labuladong 已提交
724
<!-- muliti_language -->
L
labuladong 已提交
725 726 727 728 729 730 731 732 733 734 735 736 737 738
```java
// 可重组合的回溯算法框架
void backtrack(int[] nums, int start) {
    for (int i = start; i < nums.length; i++) {
        // ...
        // 递归遍历下一层回溯树,注意参数
        backtrack(nums, i);
        // ...
    }
}
```

这相当于给之前的回溯树添加了一条树枝,在遍历这棵树的过程中,一个元素可以被无限次使用:

L
labuladong 已提交
739
![](https://labuladong.github.io/pictures/排列组合/10.jpeg)
L
labuladong 已提交
740 741 742 743 744

当然,这样这棵回溯树会永远生长下去,所以我们的递归函数需要设置合适的 base case 以结束算法,即路径和大于 `target` 时就没必要再遍历下去了。

这道题的解法代码如下:

L
labuladong 已提交
745
<!-- muliti_language -->
L
labuladong 已提交
746
```java
L
labuladong 已提交
747 748 749 750 751 752 753 754 755 756 757 758 759
class Solution {

    List<List<Integer>> res = new LinkedList<>();
    // 记录回溯的路径
    LinkedList<Integer> track = new LinkedList<>();
    // 记录 track 中的路径和
    int trackSum = 0;

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates.length == 0) {
            return res;
        }
        backtrack(candidates, 0, target);
L
labuladong 已提交
760 761 762
        return res;
    }

L
labuladong 已提交
763 764 765 766 767 768 769 770 771 772 773
    // 回溯算法主函数
    void backtrack(int[] nums, int start, int target) {
        // base case,找到目标和,记录结果
        if (trackSum == target) {
            res.add(new LinkedList<>(track));
            return;
        }
        // base case,超过目标和,停止向下遍历
        if (trackSum > target) {
            return;
        }
L
labuladong 已提交
774

L
labuladong 已提交
775 776 777 778 779 780 781 782 783 784 785 786
        // 回溯算法标准框架
        for (int i = start; i < nums.length; i++) {
            // 选择 nums[i]
            trackSum += nums[i];
            track.add(nums[i]);
            // 递归遍历下一层回溯树
            // 同一元素可重复使用,注意参数
            backtrack(nums, i, target);
            // 撤销选择 nums[i]
            trackSum -= nums[i];
            track.removeLast();
        }
L
labuladong 已提交
787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808
    }
}
```

### 排列(元素无重可复选)

力扣上没有类似的题目,我们不妨先想一下,`nums` 数组中的元素无重复且可复选的情况下,会有哪些排列?

比如输入 `nums = [1,2,3]`,那么这种条件下的全排列共有 3^3 = 27 种:

```java
[
  [1,1,1],[1,1,2],[1,1,3],[1,2,1],[1,2,2],[1,2,3],[1,3,1],[1,3,2],[1,3,3],
  [2,1,1],[2,1,2],[2,1,3],[2,2,1],[2,2,2],[2,2,3],[2,3,1],[2,3,2],[2,3,3],
  [3,1,1],[3,1,2],[3,1,3],[3,2,1],[3,2,2],[3,2,3],[3,3,1],[3,3,2],[3,3,3]
]
```

**标准的全排列算法利用 `used` 数组进行剪枝,避免重复使用同一个元素。如果允许重复使用元素的话,直接放飞自我,去除所有 `used` 数组的剪枝逻辑就行了**

那这个问题就简单了,代码如下:

L
labuladong 已提交
809
<!-- muliti_language -->
L
labuladong 已提交
810
```java
L
labuladong 已提交
811
class Solution {
L
labuladong 已提交
812

L
labuladong 已提交
813 814
    List<List<Integer>> res = new LinkedList<>();
    LinkedList<Integer> track = new LinkedList<>();
L
labuladong 已提交
815

L
labuladong 已提交
816 817 818
    public List<List<Integer>> permuteRepeat(int[] nums) {
        backtrack(nums);
        return res;
L
labuladong 已提交
819 820
    }

L
labuladong 已提交
821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838
    // 回溯算法核心函数
    void backtrack(int[] nums) {
        // base case,到达叶子节点
        if (track.size() == nums.length) {
            // 收集叶子节点上的值
            res.add(new LinkedList(track));
            return;
        }

        // 回溯算法标准框架
        for (int i = 0; i < nums.length; i++) {
            // 做选择
            track.add(nums[i]);
            // 进入下一层回溯树
            backtrack(nums);
            // 取消选择
            track.removeLast();
        }
L
labuladong 已提交
839 840 841 842
    }
}
```

L
labuladong 已提交
843 844 845 846 847 848 849 850 851 852
至此,排列/组合/子集问题的九种变化就都讲完了。

### 最后总结

来回顾一下排列/组合/子集问题的三种形式在代码上的区别。

由于子集问题和组合问题本质上是一样的,无非就是 base case 有一些区别,所以把这两个问题放在一起看。

**形式一、元素无重不可复选,即 `nums` 中的元素都是唯一的,每个元素最多只能被使用一次**`backtrack` 核心代码如下:

L
labuladong 已提交
853
<!-- muliti_language -->
L
labuladong 已提交
854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888
```java
/* 组合/子集问题回溯算法框架 */
void backtrack(int[] nums, int start) {
    // 回溯算法标准框架
    for (int i = start; i < nums.length; i++) {
        // 做选择
        track.addLast(nums[i]);
        // 注意参数
        backtrack(nums, i + 1);
        // 撤销选择
        track.removeLast();
    }
}

/* 排列问题回溯算法框架 */
void backtrack(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        // 剪枝逻辑
        if (used[i]) {
            continue;
        }
        // 做选择
        used[i] = true;
        track.addLast(nums[i]);

        backtrack(nums);
        // 撤销选择
        track.removeLast();
        used[i] = false;
    }
}
```

**形式二、元素可重不可复选,即 `nums` 中的元素可以存在重复,每个元素最多只能被使用一次**,其关键在于排序和剪枝,`backtrack` 核心代码如下:

L
labuladong 已提交
889
<!-- muliti_language -->
L
labuladong 已提交
890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924
```java
Arrays.sort(nums);
/* 组合/子集问题回溯算法框架 */
void backtrack(int[] nums, int start) {
    // 回溯算法标准框架
    for (int i = start; i < nums.length; i++) {
        // 剪枝逻辑,跳过值相同的相邻树枝
        if (i > start && nums[i] == nums[i - 1]) {
            continue;
        }
        // 做选择
        track.addLast(nums[i]);
        // 注意参数
        backtrack(nums, i + 1);
        // 撤销选择
        track.removeLast();
    }
}


Arrays.sort(nums);
/* 排列问题回溯算法框架 */
void backtrack(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        // 剪枝逻辑
        if (used[i]) {
            continue;
        }
        // 剪枝逻辑,固定相同的元素在排列中的相对位置
        if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
            continue;
        }
        // 做选择
        used[i] = true;
        track.addLast(nums[i]);
L
labuladong 已提交
925

L
labuladong 已提交
926 927 928 929 930 931 932
        backtrack(nums);
        // 撤销选择
        track.removeLast();
        used[i] = false;
    }
}
```
L
labuladong 已提交
933

L
labuladong 已提交
934
**形式三、元素无重可复选,即 `nums` 中的元素都是唯一的,每个元素可以被使用若干次**,只要删掉去重逻辑即可,`backtrack` 核心代码如下:
L
labuladong 已提交
935

L
labuladong 已提交
936
<!-- muliti_language -->
L
labuladong 已提交
937 938 939 940 941 942 943 944 945 946 947 948 949
```java
/* 组合/子集问题回溯算法框架 */
void backtrack(int[] nums, int start) {
    // 回溯算法标准框架
    for (int i = start; i < nums.length; i++) {
        // 做选择
        track.addLast(nums[i]);
        // 注意参数
        backtrack(nums, i);
        // 撤销选择
        track.removeLast();
    }
}
L
labuladong 已提交
950 951


L
labuladong 已提交
952 953 954 955 956 957 958 959 960 961 962
/* 排列问题回溯算法框架 */
void backtrack(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        // 做选择
        track.addLast(nums[i]);
        backtrack(nums);
        // 撤销选择
        track.removeLast();
    }
}
```
L
labuladong 已提交
963

L
labuladong 已提交
964
只要从树的角度思考,这些问题看似复杂多变,实则改改 base case 就能解决,这也是为什么我在 [学习算法和数据结构的框架思维](https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法)[手把手刷二叉树(纲领篇)](https://labuladong.github.io/article/fname.html?fname=二叉树总结) 中强调树类型题目重要性的原因。
L
labuladong 已提交
965

L
labuladong 已提交
966
如果你能够看到这里,真得给你鼓掌,相信你以后遇到各种乱七八糟的算法题,也能一眼看透它们的本质,以不变应万变。另外,考虑到篇幅,本文并没有对这些算法进行复杂度的分析,你可以使用我在 [算法时空复杂度分析实用指南](https://labuladong.github.io/article/fname.html?fname=时间复杂度) 讲到的复杂度分析方法尝试自己分析它们的复杂度。
L
labuladong 已提交
967

L
labuladong 已提交
968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992


<hr>
<details>
<summary><strong>引用本文的文章</strong></summary>

 - [两种思路解决单词拼接问题](https://labuladong.github.io/article/fname.html?fname=单词拼接)
 - [分治算法详解:运算优先级](https://labuladong.github.io/article/fname.html?fname=分治算法)
 - [回溯算法解题套路框架](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版)
 - [我的刷题心得](https://labuladong.github.io/article/fname.html?fname=算法心得)
 - [算法时空复杂度分析实用指南](https://labuladong.github.io/article/fname.html?fname=时间复杂度)

</details><hr>




<hr>
<details>
<summary><strong>引用本文的题目</strong></summary>

<strong>安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:</strong>

| LeetCode | 力扣 |
| :----: | :----: |
L
labuladong 已提交
993
| [131. Palindrome Partitioning](https://leetcode.com/problems/palindrome-partitioning/?show=1) | [131. 分割回文串](https://leetcode.cn/problems/palindrome-partitioning/?show=1) |
L
labuladong 已提交
994
| [17. Letter Combinations of a Phone Number](https://leetcode.com/problems/letter-combinations-of-a-phone-number/?show=1) | [17. 电话号码的字母组合](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/?show=1) |
L
labuladong 已提交
995
| [368. Largest Divisible Subset](https://leetcode.com/problems/largest-divisible-subset/?show=1) | [368. 最大整除子集](https://leetcode.cn/problems/largest-divisible-subset/?show=1) |
L
labuladong 已提交
996
| [491. Non-decreasing Subsequences](https://leetcode.com/problems/non-decreasing-subsequences/?show=1) | [491. 递增子序列](https://leetcode.cn/problems/non-decreasing-subsequences/?show=1) |
L
labuladong 已提交
997 998 999 1000 1001 1002 1003 1004 1005 1006
| - | [剑指 Offer 38. 字符串的排列](https://leetcode.cn/problems/zi-fu-chuan-de-pai-lie-lcof/?show=1) |
| - | [剑指 Offer II 079. 所有子集](https://leetcode.cn/problems/TVdhkn/?show=1) |
| - | [剑指 Offer II 080. 含有 k 个元素的组合](https://leetcode.cn/problems/uUsW3B/?show=1) |
| - | [剑指 Offer II 081. 允许重复选择元素的组合](https://leetcode.cn/problems/Ygoe9J/?show=1) |
| - | [剑指 Offer II 083. 没有重复元素集合的全排列](https://leetcode.cn/problems/VvJkup/?show=1) |

</details>



1007
**_____________**
L
labuladong 已提交
1008

L
labuladong 已提交
1009
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「**进群**」可加入算法群;回复「**全家桶**」可下载配套 PDF 和刷题全家桶**
L
labuladong 已提交
1010

L
labuladong 已提交
1011
![](https://labuladong.github.io/pictures/souyisou2.png)
L
labuladong 已提交
1012

U
userLF 已提交
1013
======其他语言代码======
L
labuladong 已提交
1014

B
brucecat 已提交
1015 1016 1017 1018 1019 1020 1021 1022 1023 1024
[78.子集](https://leetcode-cn.com/problems/subsets)

[46.全排列](https://leetcode-cn.com/problems/permutations)

[77.组合](https://leetcode-cn.com/problems/combinations)



### java

U
userLF 已提交
1025 1026 1027 1028 1029 1030 1031
[userLF](https://github.com/userLF)提供全排列的java代码:

```java
import java.util.ArrayList;
import java.util.List;

class Solution {
B
brucecat 已提交
1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173
  List<List<Integer>> res = new ArrayList<>();
  public List<List<Integer>> permute(int[] nums) {
    res.clear();
    dfs(nums, 0);//
    return res;
  }

  public void dfs(int[] n, int start) {//start表示要被替换元素的位置
    if( start >= n.length) {
      List<Integer> list = new ArrayList<Integer>();
      for(int i : n) {
        list.add(i);
      }
      res.add(list);
      return;
    }

    for(int i = start; i< n.length; i++) {//i从start开始,如果从start+1开始的话,会把当前序列遗漏掉直接保存了下一个序列
      int temp= n[i];
      n[i] = n[start];
      n[start] = temp;
      dfs(n, start + 1);//递归下一个位置
      //回到上一个状态
      n[start] = n[i];
      n[i] = temp;
    }
  }
}

```



### javascript

[传送门:78. 子集](https://leetcode-cn.com/problems/subsets/)

数学归纳思想

```js
/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var subsets = function (nums) {
    // base case, 返回一个空集
    if (nums.length === 0) {
        return [[]]
    }

    // 把最后一个元素拿出来
    let n = nums.pop();

    // 递归算出前面元素的所有子集
    let res = subsets(nums);

    let size = res.length;

    for (let i = 0; i < size; i++) {
        // 然后在之前的结果之上追加
        res.push(res[i]);
        res[res.length - 1].push(n);
    }

    return res;
}
```

回溯思想

```js
/**
 * @param {number[]} nums
 * @return {number[][]}
 */
const subsets = (nums) => {
  // 1. 设置结果集
  const result = [[]];

  // 2. 数组排序
  nums.sort((a, b) => a - b);

  // 3. 递归
  const recursion = (index, path) => {
    // 3.1 设置终止条件
    if (path.length === nums.length) {
      return;
    }

    // 3.2 遍历数组
    for (let i = index; i < nums.length; i++) {
      // 3.2.1 添加内容
      path.push(nums[i]);

      // 3.2.2 添加结果集
      result.push(path.concat());

      // 3.2.3 进一步递归
      recursion(i + 1, path);

      // 3.2.4 回溯,还原之前状态,以备下一次使用
      path.pop();
    }
  };
  recursion(0, []);

  // 4. 返回结果
  return result;
};
console.log(subsets([1, 2, 3]));
```



[传送门:46.全排列](https://leetcode-cn.com/problems/permutations)

不得不说,用js实现递归总是能遇上很多坑,其中多半都是js内置函数和引用问题的坑。看完本文,你很容易就能写入如下的js解法,但结果放到leetcode一跑,输出却十分迷惑。

```js
let res = []
/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permute = function (nums) {
    let track = [];
    backtrack(nums, track);
    return res;
};


var backtrack = function (nums, track) {
    // 触发结束条件
    if (track.length === nums.length) {
        res.push(track)
        return;
    }

    for (let i = 0; i < nums.length; i++) {
        // 排除不合法的选择
        if (track.indexOf(nums[i]) > -1) {
            continue;
U
userLF 已提交
1174 1175
        }

B
brucecat 已提交
1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221
        // 做选择
        track.push(nums[i]);

        // 进入下一层决策树
        backtrack(nums, track);

        // 取消选择
        track.pop()
    }
}
```

```
输入:[1,2,3]
输出结果:[[],[],[],[],[],[]]
预期结果:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
```

经过借鉴和改进后,无bug写法如下。

```js
/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permute = function(nums){
    // 1. 设置结果集
    const result = [];

    // 2. 回溯
    const recursion = (path, set) => {
        // 2.1 设置回溯终止条件
        if (path.length === nums.length) {

            // 2.1.1 推入结果集
            result.push(path.concat());

            // 2.1.2 终止递归
            return;
        }

        // 2.2 遍历数组
        for (let i = 0; i < nums.length; i++) {

            // 2.2.1 必须是不存在 set 中的坐标
            if (!set.has(i)) {
U
userLF 已提交
1222

B
brucecat 已提交
1223 1224 1225 1226 1227 1228 1229 1230 1231 1232
                // 2.2.2 本地递归条件(用完记得删除)
                path.push(nums[i]);
                set.add(i);

                // 2.2.3 进一步递归
                recursion(path, set);

                // 2.2.4 回溯:撤回 2.2.2 的操作
                path.pop();
                set.delete(i);
U
userLF 已提交
1233 1234
            }
        }
B
brucecat 已提交
1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282
    };
  
    recursion([], new Set());

    // 3. 返回结果
    return result;
};

```

看到这,才恍然大悟,原来是在 2.1.1 推入结果集的过程中,需要使用concat来实现数组的浅复制,并加入到result结果集中,不然会因为引用问题而导致结果集中都是空list。除此之外,你还要注意格外let块级作用域,在作用域外是找不到的!

[传送门:77.组合](https://leetcode-cn.com/problems/combinations)

```js
var combine = function (n, k) {

    const res = []

    if (k <= 0 || n <= 0) return res;

    const track = [];


    const backtrack = (n, k, start, track) => {
        // 到达树的底部
        if (k === track.length) {
            res.push(track.concat());
            return;
        }

        // 注意i从start开始递增
        for (let i = start; i <= n; i++) {
            // 做选择
            track.push(i);
            backtrack(n, k, i + 1, track);

            // 撤销选择
            track.pop();
        }
    }


    backtrack(n, k, 1, track);

    return res;
};
```
U
userLF 已提交
1283