回溯算法总结
1. 概述
回溯大体分为:组合、排列、子集、切割、搜索几种类型
类型 | 题目链接 | 思路 | |
---|---|---|---|
组合问题 | 77.组合 39.组合总和 40. 组合总和 II 216. 组合总和 III | ||
排列问题 | 46. 全排列 47. 全排列 II | ||
子集问题 | 78. 子集 90. 子集 II 491. 递增子序列 | ||
切割问题 | 131. 分割回文串 93. 复原 IP 地址 | ||
搜索问题 | 51. N 皇后 37. 解数独 |
2. 组合问题
2.1 组合
LC链接:77.组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:[ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4],]
- 思路:
- 组合问题,使用
start
(indexStart
) 来逐层缩小搜索范围。比如这层选择了i
,那么递归到下层时,应该从i + 1
开始进行迭代。 - 递归何时结束,当
path
中的元素数量等于k
时,即可结束。
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.ArrayDeque;
class Solution {
List<List<Integer>> res;
Deque<Integer> path;
public List<List<Integer>> combine(int n, int k) {
res = new ArrayList();
// 固定path的大小,防止运行中反复扩容带来的性能损失
path = new ArrayDeque(k);
backtracking(n, k, 1);
return res;
}
private void backtracking(int n, int k, int start) {
// 当 path 中的元素数量等于k时,返回
if (path.size() == k) {
res.add(new ArrayList(path));
return;
}
// 下层遍历从 i+1 开始
for (int number = start; number <= n; number++) {
path.addLast(number);
backtracking(n, k, number + 1);
path.removeLast();
}
}
}
- 剪枝优化:当剩余的数全部添加到
path
中,也不足以使得path.size() == k
时,即可结束搜索(当前层及后续层级)。比如n = 5, k = 4,表示需要从 [1, 2, 3, 4, 5] 中选取4个元素。当程序运行到某个时刻,假设此时path.size()
为1,那么还需要3个元素添加到path
中才能满足 k = 4 这个条件,因此就不能从 4 及 4 以后开始进行遍历,因为即使把所有这些元素加入到path中,也无法满足 k = 4 这个条件。
path.size()
表示当前路径元素的个数k - path.size()
表示还需要的元素个数- 所以上述代码中 number 最大只能到
n - (k - path.size()) + 1
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.ArrayDeque;
class Solution {
List<List<Integer>> res;
Deque<Integer> path;
public List<List<Integer>> combine(int n, int k) {
res = new ArrayList();
// 固定path的大小,防止运行中反复扩容带来的性能损失
path = new ArrayDeque(k);
backtracking(n, k, 1);
return res;
}
private void backtracking(int n, int k, int start) {
// 当 path 中的元素数量等于k时,返回
if (path.size() == k) {
res.add(new ArrayList(path));
return;
}
// 下层遍历从 i+1 开始
// 剪枝优化
for (int number = start; number <= n - (k - path.size()) + 1; number++) {
path.addLast(number);
backtracking(n, k, number + 1);
path.removeLast();
}
}
}
优化前后运行时间对比,能看到剪枝后速度提升了19倍!
2.2 组合总和
LC链接:39.组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1: 输入:candidates = [2,3,6,7], target = 7, 所求解集为: [ [7], [2,2,3] ]
示例 2: 输入:candidates = [2,3,5], target = 8, 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ]
- 思路:
- 求组合需要利用
indexStart
缩小搜索范围,但是本题中每个元素可以使用次数的不限,所以下一次遍历的indexStart
等于本次遍历的indexStart
,而不需要加1 - 递归何时结束,由于目前还需要的
target
是由原target
一路减去path
中的数字而传递下来的,所以当target
小于等于 0 的时候,退出。特别地,当target
等于0 的时候说明找到了和为原target
的路径,需要将路径添加到最终的res
中。
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtracking(candidates, target, 0);
return res;
}
private void backtracking(int[] candidates, int remain, int indexStart) {
// target小于0 退出
if (remain < 0) return;
// target等于于0,添加路径、退出
if (remain == 0) {
res.add(new ArrayList(path));
return;
}
for (int i = indexStart; i < candidates.length; i++) {
path.addLast(candidates[i]);
// 每个数字可以使用无限次,所以下次还从i开始遍历即可
backtracking(candidates, remain - candidates[i], i);
path.removeLast();
}
}
}
- 剪枝:先对原数组进行排序,然后在遍历过程中如果发现如果
remain < candidates[i]
,则当candidates[i]或者candidates[i]
之后的数字再加入path
中,会导致path
中的数字和超过target
,所以一旦出现remain < candidates[i]
,即可终止。代码如下:
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Arrays;
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if (candidates == null || candidates.length == 0 || target <= 0) return res;
Arrays.sort(candidates);
backtracking(candidates, target, 0);
return res;
}
private void backtracking(int[] candidates, int remain, int indexStart) {
// target小于0 退出
if (remain < 0) return;
// target等于于0,添加路径、退出
if (remain == 0) {
res.add(new ArrayList(path));
return;
}
// 剪枝 remain >= candidates[i],如果 remain < candidates[i] 说明后续的数再加入path会导致path数字和超过原target
for (int i = indexStart; i < candidates.length && remain >= candidates[i]; i++) {
path.addLast(candidates[i]);
// 每个数字可以使用无限次,所以下次还从i开始遍历即可
backtracking(candidates, remain - candidates[i], i);
path.removeLast();
}
}
}
剪枝后,LC上运行时间从14ms降到3ms
2.3 组合总和 II
LC链接:40. 组合总和 II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。
说明: 所有数字(包括目标数)都是正整数。 解集不能包含重复的组合。
示例 1: 输入: candidates = [10,1,2,7,6,1,5], target = 8, 所求解集为: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ]
示例 2: 输入: candidates = [2,5,2,1,2], target = 5, 所求解集为: [ [1,2,2], [5] ]
- 思路:
- 使用
indexStart
来逐层缩小搜索范围:该题是求组合,每个索引对应的元素只能使用一次,虽然数组中的元素可能重复(如例子[10,1,2,7,6,1,5]
中,索引1和索引5对应的元素都是1),但是最终结果集中的每个组合都是对应索引只使用了一次,比如[1, 1, 6]
里的两个1分别对应[10,1,2,7,6,1,5]
数组索引1和索引5的位置。 - 树层去重:需要对原数组先排序!如果不排序的话,示例2中会出现
[2, 2, 1]
、[2, 1, 2]
的组合,但其实这两个算作一个组合。当然,也可以当把这两个组合都求出来后,在最后添加进结果集的时候再去重,但是这样做了很多无用功,势必会超时。而排序后,上述两个组合都变成[1, 2, 2]
,这也能够在搜索过程中进行去重。
树层去重1:使用used 数组:
- used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
- used[i - 1] == false,说明同一树层candidates[i - 1]使用过
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
boolean[] used = new boolean[candidates.length];
backtracking(candidates, target, 0, used);
return res;
}
private void backtracking(int[] candidates, int target, int indexStart, boolean[] used) {
if (target == 0) {
res.add(new ArrayList(path));
return;
}
for (int i = indexStart; i < candidates.length && target >= candidates[i]; i++) {
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) continue;
used[i] = true;
path.addLast(candidates[i]);
backtracking(candidates, target - candidates[i], i + 1, used);
path.removeLast();
used[i] = false;
}
}
}
树层去重2: 三数之和的思想,其实这种思想有类似点 15. 三数之和 ,不管是求和的目的,还是去重的逻辑。
- 先排序
if (i > indexStart && candidates[i] == candidates[i - 1]) continue;
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Arrays;
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backtracking(candidates, target, 0);
return res;
}
private void backtracking(int[] candidates, int target, int indexStart) {
// target 每添加一个元素到 path,target 都会减去悉新加入元素的值,当 target 为0的时候,说明 path 里元素和等于 target
if (target == 0) {
res.add(new ArrayList(path));
return;
}
// 每次从 indexStart 开始遍历
// 且由于数组排序过,当剩余的 target < candidates[i]时,说明再将 candidates[i] 加入path,path里的元素和将会大于 target,
// 所以对于candidates[i] candidates[i+1] ... 不用再遍历
for (int i = indexStart; i < candidates.length && target >= candidates[i]; i++) {
// 树层去重,注意 i > indexStart
if (i > indexStart && candidates[i] == candidates[i - 1]) continue;
path.addLast(candidates[i]);
backtracking(candidates, target - candidates[i], i + 1);
path.removeLast();
}
}
}
树层去重3:桶的思想,由于 1 <= candidates[i] <= 50
,我们为每一层创建一个桶(数组),如果每一层上某个数字已经使用过,就跳过
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backtracking(candidates, target, 0);
return res;
}
private void backtracking(int[] candidates, int target, int indexStart) {
if (target == 0) {
res.add(new ArrayList(path));
return;
}
// 1 <= candidates[i] <= 50 记录每层数字是否使用过
boolean[] used = new boolean[51];
for (int i = indexStart; i < candidates.length && target >= candidates[i]; i++) {
if (used[candidates[i]] == true) continue;
used[candidates[i]] = true;
path.addLast(candidates[i]);
backtracking(candidates, target - candidates[i], i + 1);
path.removeLast();
}
}
}
- 要点:
- 求组合需要利用
indexStart
缩小搜索范围,也是为了避免结果集中每个组合中的元素重复; - 树层去重,有3种方法,推荐使用第一种,可以和树枝去重共用同一个数组。
2.4 组合总和 III
LC链接:216. 组合总和 III
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]
示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]
- 思路:
- 组合问题考虑
start
(indexStart
)。由于题目中提到 “每种组合中不存在重复的数字”,所以需要利用indexStart
来缩小搜索范围确保元素的之前使用过的元素不会再选取到。至于是使用start
还是indexStart
取决于我们在for
循环中是使用索引进行遍历还是使用元素进行遍历。显然,本题直接使用元素进行遍历。 - 递归合适结束:当
path
中的元素个数等于k时,需要退出。并且当remain
刚好为0时,说明path
中所有数相加等于n
,所以此时需要将path
添加到res
中。
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.ArrayDeque;
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path;
public List<List<Integer>> combinationSum3(int k, int n) {
// 固定path的大小,防止运行中反复扩容带来的性能损失
path = new ArrayDeque(k);
backtrackin(k, n, 1);
return res;
}
private void backtrackin(int k, int remain, int start) {
// 元素达到 k 个,需要返回。特别地,如果此时 remain 刚好为0,需要将路径 path 添加到 res 中
if (path.size() == k) {
if (remain == 0) {
res.add(new ArrayList(path));
}
return;
}
// remain >= num 进行剪枝
for (int num = start; num <= 9 && remain >= num; num++) {
path.addLast(num);
backtrackin(k, remain - num, num + 1);
path.removeLast();
}
}
}
2.5 组合问题小结
- 组合问题一般都需要使用
indexStart
(start
) 来在递归中缩小搜索范围,避免组合中出现重复元素; - 当写出基本版本的代码后,可以考虑是否能通过剪枝的方式提高效率;
- 树层去重的时候需要对原数组进行排序。
3. 排列问题
3.1 全排列
LC链接:46. 全排列
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例: 输入: [1,2,3] 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
- 思路:
- 每层从0开始便利,不用
indexStart
:排列由于需要把数组中的数字(1、2、3)全部用到,所以不能像组合、子集问题一样利用indexStart
来逐步缩小问题范围,而是需要在每次递归时做完整的遍历(即从索引0开始遍历),把没有用到的元素添加到path
中。 - 递归何时结束:当
path
中元素数量等于原数组中元素数量时,说明原数组元素远不用完,返回。 - 树枝去重:我们规定使用
usedPath
数组来记录一条路径(树枝)上对应索引元素的使用情况。
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> permute(int[] nums) {
boolean[] usedPath = new boolean[nums.length];
backtracking(nums, usedPath);
return res;
}
// 排列,数组里的元素都要用到且只能用一次,不用像组合一样使用indexStart缩小取值范围
// 而是使用 usedPath 数组来标记每一条路径(树枝)上元素的使用情况,如果已经在该路径上使用过就跳过
private void backtracking(int[] nums, boolean[] usedPath) {
// 退出条件:当路径元素跟数组元素相等时
if (path.size() == nums.length) {
res.add(new ArrayList(path));
return;
}
// 排列每次都从0开始遍历,利用usedPath来确保每一条路径上元素的唯一性
for (int i = 0; i < nums.length; i++) {
if (usedPath[i] == true) continue;
usedPath[i] = true; // 记录该下标的元素在当前路径上已经使用过
path.addLast(nums[i]); // 将元素添加到当前路径上
backtracking(nums, usedPath); // 带着当前路径元素的使用记录,继续去(从下标0开始)遍历
path.removeLast(); // 回溯路径
usedPath[i] = false; // 回溯路径元素使用记录
}
}
}
- 要点:
- 树枝去重:排列问题需要每层都从0开始遍历不需要
indexStart
,而是使用usedPath
完成路径上的去重,usedPath
记录的是数组元素在每条路径上的使用情况——元素下标
3.2 全排列 II
LC链接:47. 全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1: 输入:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,1,1]]
示例 2: 输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10
- 思路:利用
used
数组完成数层和树枝去重
class Solution {
List<List<Integer>> res;
Deque<Integer> path;
public List<List<Integer>> permuteUnique(int[] nums) {
res = new ArrayList();
path = new ArrayDeque(nums.length);
boolean[] used = new boolean[nums.length];
Arrays.sort(nums);
backtracking(nums, used);
return res;
}
// 思路:在全排列的基础上增加树层去重
private void backtracking(int[] nums, boolean[] used) {
if (path.size() == nums.length) {
res.add(new ArrayList(path));
return;
}
for (int i = 0; i < nums.length; i++) {
// 树层去重
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue;
// 树枝去重
if (used[i] == true) continue;
used[i] = true;
path.addLast(nums[i]);
backtracking(nums, used);
path.removeLast();
used[i] = false;
}
}
}
- 思路2:利用桶的思想完成数层去重,笔者目前还没有想通为什么这种方法不用对原数组排序???
class Solution {
List<List<Integer>> res;
Deque<Integer> path;
public List<List<Integer>> permuteUnique(int[] nums) {
res = new ArrayList();
path = new ArrayDeque(nums.length);
boolean[] usedPath = new boolean[nums.length];
// Arrays.sort(nums);
backtracking(nums, usedPath);
return res;
}
// 思路:在全排列的基础上增加树层去重
private void backtracking(int[] nums, boolean[] usedPath) {
if (path.size() == nums.length) {
res.add(new ArrayList(path));
return;
}
boolean[] usedLayer = new boolean[21];
for (int i = 0; i < nums.length; i++) {
// 树层去重
if (usedLayer[nums[i] + 10] == true) continue;
// 树枝去重
if (usedPath[i] == true) continue;
usedLayer[nums[i] + 10] = true;
usedPath[i] = true;
path.addLast(nums[i]);
backtracking(nums, usedPath);
path.removeLast();
usedPath[i] = false;
}
}
}
4. 子集问题
4.1 子集
LC链接:78. 子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
- 思路:子集跟组合问题一样,也要求集合无序,所以需要
indexStart
来确保取过的元素不会重复添加。但是组合问题是到叶子结点才进行收集,而子集问题是收集所有节点。
class Solution {
List<List<Integer>> res;
Deque<Integer> path;
public List<List<Integer>> subsets(int[] nums) {
res = new ArrayList();
path = new ArrayDeque(nums.length);
backtracking(nums, 0);
return res;
}
private void backtracking(int[] nums, int indexStart) {
// 子集问题不需要主动退出,当遍历完数组自然结束即可
res.add(new ArrayList(path));
for (int i = indexStart; i < nums.length; i++) {
path.addLast(nums[i]);
backtracking(nums, i + 1);
path.removeLast();
}
}
}
4.2 子集 II
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例: 输入: [1,2,2] 输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]
- 思路(推荐):
used
数组去重。原数组有重复,而要求的集合中不允许有重复,画树形图分析后可知需要树层去重。使用used
数组结合if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue;
条件完成树层去重
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
backtracking(nums, 0, used);
return res;
}
private void backtracking(int[] nums, int indexStart, boolean[] used) {
res.add(new ArrayList(path));
for (int i = indexStart; i < nums.length; i++) {
// 树层去重
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue;
used[i] = true;
path.addLast(nums[i]);
backtracking(nums, i + 1, used);
path.removeLast();
used[i] = false;
}
}
}
- 思路(推荐):桶的思想去重。为每一层创建一个
usedLayer
数组用来在遍历某一层(for 循环)时,记录当前层的数字使用情况,由于题目限定 -10 <= nums[i] <= 10,所以usedLayer
大小为 21,相当于把 [-10,10] 平移到 [0, 20],然后以下标来进行统计,下标 0 对应 nums 中的-10,下标 1 对应 nums 中的-9 … 下标 20 对应 nums 中的10。
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
backtracking(nums, 0);
return res;
}
private void backtracking(int[] nums, int indexStart) {
res.add(new ArrayList(path));
boolean[] usedLayer = new boolean[21];
for (int i = indexStart; i < nums.length; i++) {
// 树层去重,桶的思想
if (usedLayer[nums[i] + 10] == true) continue;
usedLayer[nums[i] + 10] = true;
path.addLast(nums[i]);
backtracking(nums, i + 1);
path.removeLast();
}
}
}
4.3 递增子序列
LC链接:491. 递增子序列
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
示例:
输入: [4, 6, 7, 7] 输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
说明:
给定数组的长度不会超过15。
数组中的整数范围是 [-100,100]。
给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。
这道题坑的点还挺多的:
1、树层去重(不能排序)
2、大于才能添加到path中
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> findSubsequences(int[] nums) {
// 不能排序,求递增子序列,如果排序就改变了数组原有的顺序
backtracking(nums, 0);
return res;
}
private void backtracking(int[] nums, int indexStrat) {
if (path.size() >= 2) {
res.add(new ArrayList(path));
}
boolean[] usedLayer = new boolean[201];
for (int i = indexStrat; i < nums.length; i++) {
// 树层去重
if (usedLayer[nums[i] + 100] == true) continue;
// 确保递增
if (path.isEmpty() || nums[i] >= path.getLast()) {
usedLayer[nums[i] + 100] = true;
path.addLast(nums[i]);
backtracking(nums, i + 1);
path.removeLast();
}
}
}
}
4.4 划分为k个相等的子集
LC链接:698. 划分为k个相等的子集
给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。
示例 1:
输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4
输出: True
说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。
提示:
1 <= k <= len(nums) <= 16
0 < nums[i] < 10000
5 棋盘问题
5.1 N皇后
5.2 解数独
思考
- 三种树层去重的区别:
-
used 数组既可以用来树层去重,也可以用来树枝去重,为什么 46. 全排列 树枝去重时使用
if (i > 0 && nums[i] == nums[i - 1] && usedPath[i - 1] == true) continue;
不对? 因为46题中的元素没有重复的? -
说好的树层去重需要排序,但是为什么 47. 全排列 II 利用桶的思想去重时就不用排序?因为[2, 1, 2] 和 [2, 2, 1] 是不同的排列,但是属于同一个组合(子集)。
-
2.3、3.2、4.2三个题结合看,总结提炼树层去重的共性!
-
树层去重里的层指的是具有直接共同父节点的层,而不是层次遍历中指的一整层!
- 凡是原数组有重复,或者原数组的数字可以重复使用的情况,都需要树层去重,且树层去重需要先对原数组排序