神马都会亿点点的毛毛张

一万次悲伤,依然会有Dream

分类不好,这道题就做不出来!

题目1:452. 用最少数量的箭引爆气球

1.题目描述

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstartxend之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 x``startx``end, 且满足 xstart ≤ x ≤ x``end,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。

给你一个数组 points返回引爆所有气球所必须射出的 最小 弓箭数

示例 1:

1
2
3
4
5
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8][1,6]
-在x = 11处发射箭,击破气球[10,16][7,12]

示例 2:

1
2
3
输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。

示例 3:

1
2
3
4
5
输入:points = [[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:气球可以用2支箭来爆破:
- 在x = 2处发射箭,击破气球[1,2][2,3]
- 在x = 4处射出箭,击破气球[3,4][4,5]

提示:

  • $1 <= points.length <= 10^5$
  • points[i].length == 2
  • $-2^{31} <= x_{start} < x_{end} <= 2^{31} - 1$

2.题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 解决方案类
class Solution {
public int findMinArrowShots(int[][] points) {
// 根据气球直径的开始坐标从小到大排序
// 使用Integer内置的比较方法,以避免整数溢出问题
Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0]));

// 初始化箭的数量为1,因为至少需要一支箭
int result = 1;

// 遍历气球数组,从第二个气球开始
for (int i = 1; i < points.length; i++) {
// 检查当前气球的起始坐标是否大于上一个气球的结束坐标
if (points[i][0] > points[i - 1][1]) {
// 当前气球不重叠,需要新的箭
result++;
} else {
// 当前气球与上一个气球重叠,更新当前气球的结束坐标为两者中较小的值
points[i][1] = Math.min(points[i][1], points[i - 1][1]);
}
}

// 返回最少的箭数量
return result;
}
}

题目2:435. 无重叠区间

1.题目描述

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠

注意 只在一点上接触的区间是 不重叠的。例如 [1, 2][2, 3] 是不重叠的。

示例 1:

1
2
3
输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。

示例 2:

1
2
3
输入: intervals = [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。

示例 3:

1
2
3
输入: intervals = [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

提示:

  • $1 <= intervals.length <= 10^5$
  • intervals[i].length == 2
  • $-5 * 10^4 <= start_i < end_i <= 5 * 10^4$

2.题解

2.1 贪心算法-写法1

  • count记录非交叉区间的个数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
// 将区间数组按每个区间的起始位置进行排序
Arrays.sort(intervals, (a, b) -> {
return a[0] - b[0]; // 比较两个区间的起始位置,按升序排列
});
int count = 1;// 计数器初始化为1,用于统计不重叠的区间数量
// 遍历区间数组,从第二个区间开始
for (int i = 1; i < intervals.length; i++) {
// 如果当前区间与前一个区间不重叠
if(intervals[i-1][1] <= intervals[i][0]){
// 增加计数器
count++;
} else {
// 如果当前区间与前一个区间重叠,更新当前区间的右边界为两者右边界的较小值
intervals[i][1] = Math.min(intervals[i - 1][1], intervals[i][1]);
}
}

return intervals.length - count;// 返回需要移除的重叠区间数量,即总区间数减去不重叠区间的数量
}
}

2.2 贪心算法-写法2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
// 将区间数组按每个区间的起始位置进行排序
Arrays.sort(intervals, (a, b) -> {
// 比较两个区间的起始位置,按升序排列
return Integer.compare(a[0], b[0]);
});

// 记录需要移除的重叠区间数量
int remove = 0;
// 初始化前一个区间的右边界为第一个区间的右边界
int preR = intervals[0][1];

// 遍历区间数组,从第二个区间开始
for (int i = 1; i < intervals.length; i++) {
// 如果当前区间的起始位置不小于前一个区间的右边界(无重叠)
if (preR <= intervals[i][0]) {
// 更新前一个区间的右边界为当前区间的右边界
preR = intervals[i][1];
} else {
// 如果当前区间与前一个区间重叠,增加需要移除的区间计数
remove++;
// 更新前一个区间的右边界为两者右边界的较小值,以减少后续重叠
preR = Math.min(preR, intervals[i][1]);
}
}

// 返回需要移除的重叠区间数量
return remove;
}
}

题目3:763. 划分字母区间

1.题目描述

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s

返回一个表示每个字符串片段的长度的列表。

示例 1:

1
2
3
4
5
6
输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca""defegde""hijhklij"
每个字母最多出现在一个片段中。
"ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。

示例 2:

1
2
输入:s = "eccbbbbdec"
输出:[10]

提示:

  • 1 <= s.length <= 500
  • s 仅由小写英文字母组成

2.题解

  • 思路:
    • 统计每一个字符最后出现的位置
    • 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
// 定义一个方法 partitionLabels,接收一个字符串 s,返回一个整数列表
public List<Integer> partitionLabels(String s) {
// 创建一个长度为26的数组 edge,用于存储每个字符最后出现的位置
int[] edge = new int[26];
// 遍历字符串,记录每个字符的最右边界索引
for(int i = 0; i < s.length(); i++) edge[s.charAt(i) - 'a'] = i;
// 创建一个结果列表,用于存储每个分区的长度
List<Integer> result = new ArrayList<>();
// 初始化两个指针,left 表示分区的起始位置,right 表示当前分区的结束位置
int left = 0, right = 0;
// 遍历字符串
for(int i = 0; i < s.length(); i++) {
// 更新当前分区的最右边界
right = Math.max(right, edge[s.charAt(i) - 'a']);

// 如果当前位置 i 等于当前分区的最右边界
if(right == i) {
// 将当前分区的长度添加到结果列表中
result.add(right - left + 1);
// 更新左边界为下一个分区的起始位置
left = right + 1;
}
}
return result;
}
}

题目4:56. 合并区间

1.题目描述

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间

示例 1:

1
2
3
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例 2:

1
2
3
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。

提示:

  • $1 <= intervals.length <= 10^4$
  • intervals[i].length == 2
  • $0 <= start_i <= end_i <= 10^4$

2.题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {
public int[][] merge(int[][] intervals) {
// 如果区间数组为空,直接返回空数组
if(intervals.length == 0) return new int[0][2];

// 将区间数组按每个区间的起始位置进行排序
Arrays.sort(intervals, new Comparator<int[]>() {
public int compare(int[] intervals1, int[] intervals2) {
// 按照区间的起始值升序排列
return intervals1[0] - intervals2[0];
}
});

// 用来保存合并后的区间
List<int[]> result = new ArrayList<>();

// 遍历所有区间
for(int i = 0; i < intervals.length; i++) {
// 当前区间的左边界 L 和右边界 R
int L = intervals[i][0], R = intervals[i][1];

// 如果 result 为空,或者 result 中最后一个区间的右边界小于当前区间的左边界
// 说明当前区间与前一个区间没有重叠,直接添加当前区间
if(result.size() == 0 || result.get(result.size() - 1)[1] < L) {
result.add(new int[]{L, R});
} else {
// 否则,说明当前区间和前一个区间有重叠
// 将最后一个区间的右边界更新为两个区间中较大的右边界,合并区间
result.get(result.size() - 1)[1] = Math.max(result.get(result.size() - 1)[1], R);
}
}

// 将结果列表转换为二维数组形式并返回
return result.toArray(new int[result.size()][]);
}
}

栈与队列练习题

LeetCode链接:1047. 删除字符串中的所有相邻重复项

1.题目描述

给出由小写字母组成的字符串 s重复项删除操作会选择两个相邻且相同的字母,并删除它们。

s 上反复执行重复项删除操作,直到无法继续删除。

在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

示例:

1
2
3
4
输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"

提示:

  1. $1 <= s.length <= 10^5$
  2. s 仅由小写英文字母组成。

2.题解

2.1 栈与队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public String removeDuplicates(String s) {
// 使用 LinkedList 来存储不重复的字符,作为栈使用
LinkedList<Character> list = new LinkedList<>();

// 遍历字符串中的每一个字符
for (int i = 0; i < s.length(); i++) {
Character c = s.charAt(i);

// 如果列表不为空且当前字符与列表最后一个字符相同,则移除最后一个字符(即相邻的重复字符)
if (!list.isEmpty() && list.peekLast() == c) {
list.pollLast();
} else {
// 否则将当前字符加入列表的末尾
list.offer(c);
}
}

// 使用 StringBuilder 将最终结果拼接成字符串
StringBuilder sb = new StringBuilder();
while (!list.isEmpty()) {
// 依次从列表中取出字符,拼接成结果字符串
sb.append(list.poll());
}

// 返回拼接好的字符串
return sb.toString();
}
}

2.2 字符串当栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public String removeDuplicates(String s) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
Character c = s.charAt(i);
if(sb.length() > 0 && sb.charAt(sb.length()-1) == c){
sb.deleteCharAt(sb.length()-1);
}else{
sb.append(c);
}
}

return sb.toString();
}
}

2.3 双指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public String removeDuplicates(String s) {
// 双指针
char[] chs = s.toCharArray();
int left = 0; // 左指针用于表示结果部分的末尾
for (int right = 0; right < s.length(); right++) {
// 如果当前字符和结果末尾字符相同,左指针回退
if (left > 0 && chs[right] == chs[left - 1]) {
left--;
} else {
chs[left] = chs[right]; // 更新结果部分
left++;
}
}
return new String(chs, 0, left); // 只返回有效部分的字符串
}
}

栈与队列练习题

LeetCode链接:150. 逆波兰表达式求值

1.题目描述

给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。

请你计算该表达式。返回一个表示表达式值的整数。

注意:

  • 有效的算符为 '+''-''*''/'
  • 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
  • 两个整数之间的除法总是 向零截断
  • 表达式中不含除零运算。
  • 输入是一个根据逆波兰表示法表示的算术表达式。
  • 答案及所有中间计算结果可以用 32 位 整数表示。

示例 1:

1
2
3
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

示例 2:

1
2
3
输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6

示例 3:

1
2
3
4
5
6
7
8
9
10
输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
输出:22
解释:该算式转化为常见的中缀算术表达式为:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22

提示:

  • $1 <= tokens.length <= 10^4$
  • tokens[i] 是一个算符("+""-""*""/"),或是在范围 [-200, 200] 内的一个整数

逆波兰表达式:

逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。

  • 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 )
  • 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * )

逆波兰表达式主要有以下两个优点:

  • 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
  • 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中

2.题解

2.1 栈与队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Solution {
public int evalRPN(String[] tokens) {
// 使用栈来存储计算过程中的操作数
Stack<Integer> stack = new Stack<>();

// 遍历所有的输入 tokens
for (int i = 0; i < tokens.length; i++) {
// 如果当前 token 是加号
if (tokens[i].equals("+")) {
// 弹出栈顶的两个操作数,进行相加,然后将结果压入栈中
// 这里 pop() 两次是因为加法是二元操作,需要两个操作数
stack.push(stack.pop() + stack.pop());
}
// 如果当前 token 是减号
else if (tokens[i].equals("-")) {
// 弹出栈顶的两个操作数,先弹出的数为减数,后弹出的数为被减数
// 计算后将结果压入栈中
stack.push(-stack.pop() + stack.pop());
}
// 如果当前 token 是乘号
else if (tokens[i].equals("*")) {
// 弹出栈顶的两个操作数,进行相乘,然后将结果压入栈中
stack.push(stack.pop() * stack.pop());
}
// 如果当前 token 是除号
else if (tokens[i].equals("/")) {
// 弹出栈顶的两个操作数,注意除法的顺序:
// 第一个弹出的数为除数 (num2),第二个弹出的数为被除数 (num1)
int num2 = stack.pop();
int num1 = stack.pop();
// 进行除法计算后,将结果压入栈中
stack.push(num1 / num2);
}
// 如果当前 token 是数字
else {
// 将数字字符串转换为整数,并压入栈中
stack.push(Integer.parseInt(tokens[i]));
}
}

// 最终栈中只剩下一个元素,即计算结果,弹出栈顶并返回
return stack.pop();
}
}

栈与队列练习题

LeetCode链接:239. 滑动窗口最大值
---

1.题目描述

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值

示例 1:

1
2
3
4
5
6
7
8
9
10
11
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7

示例 2:

1
2
输入:nums = [1], k = 1
输出:[1]

提示:

  • $1 <= nums.length <= 10^5$
  • $-10^4 <= nums[i] <= 10^4$
  • 1 <= k <= nums.length

2.题解

  • 这道题目暴力解法超时,需要另寻良策

2.1 LinkedList双端队列实现单调队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
// 定义一个双端队列,用来存储滑动窗口中可能的最大值的下标
LinkedList<Integer> deque = new LinkedList<>();
int n = nums.length;
// 结果数组,用来存储每个滑动窗口的最大值
int[] res = new int[n - k + 1];
// 结果数组的下标,用于记录结果的位置
int idx = 0;

// 遍历数组
for (int i = 0; i < n; i++) {
// 1. 保证队列头节点在当前滑动窗口的范围内
// 如果队列头部元素的下标小于滑动窗口的起始下标(i - k + 1),
// 说明该下标已经滑出窗口范围,需要将其移除队列
while (!deque.isEmpty() && deque.peek() < i - k + 1) {
deque.poll();
}

// 2. 保证队列中元素按从大到小排列
// 如果当前元素 nums[i] 大于队列末尾元素对应的值,说明队列末尾元素不可能成为窗口的最大值,
// 需要将其从队列中移除,直到找到一个比当前元素大的元素或队列为空
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}

// 将当前元素下标加入队列,保持队列的单调性(从大到小)
deque.offer(i);

// 当窗口大小达到 k(即 i >= k - 1 时),可以确定此时的窗口内最大值
// 最大值就是队列头部的元素所对应的数组值,将其加入结果数组
if (i >= k - 1) {
res[idx++] = nums[deque.peek()];
}
}
return res;
}
}
  • 可以将 “未形成窗口” 和 “形成窗口后” 两个阶段拆分到两个循环里实现。代码虽变长,但减少了冗余的判断操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 0 || k == 0) return new int[0];
Deque<Integer> deque = new LinkedList<>();
int[] res = new int[nums.length - k + 1];
// 未形成窗口
for(int i = 0; i < k; i++) {
while(!deque.isEmpty() && deque.peekLast() < nums[i])
deque.removeLast();
deque.addLast(nums[i]);
}
res[0] = deque.peekFirst();
// 形成窗口后
for(int i = k; i < nums.length; i++) {
if(deque.peekFirst() == nums[i - k])
deque.removeFirst();
while(!deque.isEmpty() && deque.peekLast() < nums[i])
deque.removeLast();
deque.addLast(nums[i]);
res[i - k + 1] = deque.peekFirst();
}
return res;
}
}

栈与队列练习题

LeetCode链接:20. 有效的括号

1.题目描述

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

示例 1:

输入:s = “()”

输出:true

示例 2:

输入:s = “()[]{}”

输出:true

示例 3:

输入:s = “(]”

输出:false

示例 4:

输入:s = “([])”

输出:true

提示:

  • $1 <= s.length <= 10^4$
  • s 仅由括号 '()[]{}' 组成

2.题解

2.1 栈与队列-哈希集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution {
public boolean isValid(String s) {
// 初始化栈,用于存储需要匹配的右括号
Stack<Character> stack = new Stack<>();

// 创建哈希映射,将左括号与相应的右括号配对
Map<Character, Character> map = new HashMap<>();
map.put('(', ')');
map.put('{', '}');
map.put('[', ']');

// 遍历字符串的每一个字符
for (int i = 0; i < s.length(); i++) {
Character c = s.charAt(i);

// 如果字符是左括号(即 map 的键),则将对应的右括号压入栈中
if (map.containsKey(c)) {
stack.push(map.get(c));
}
// 如果是右括号,检查栈顶元素是否与当前右括号匹配
else {
// 如果栈非空,且栈顶的括号与当前字符匹配,则弹出栈顶元素
if (!stack.isEmpty() && stack.peek() == c) {
stack.pop();
} else {// 如果不匹配或栈为空,返回 false,表示括号不合法
return false;
}
}
}
return stack.isEmpty();// 最后检查栈是否为空,如果为空则表示所有括号匹配,否则返回 false
}
}

2.2 栈与队列-逻辑判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
// 如何碰到的是键,就入栈
Character c = s.charAt(i);
if (c == '(') {
stack.push(')');
} else if (c == '{') {
stack.push('}');
} else if (c == '[') {
stack.push(']');
} else {
if (!stack.isEmpty() && stack.peek() == c) {
stack.pop();
} else {
return false;
}
}
}
return stack.isEmpty();
}
}

回溯算法练习题


LeetCode链接:51. N 皇后

1.题目描述

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。

示例 1:

img

1
2
3
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:

1
2
输入:n = 1
输出:[["Q"]]

提示:

  • 1 <= n <= 9

2.题解

2.1 回溯算法-写法1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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
68
class Solution {
// 定义保存最终结果的列表,每个解法是一个List<String>,代表棋盘上的布局
List<List<String>> result = new ArrayList<>();
// 定义保存当前棋盘路径的列表,存放字符串形式的每一行布局
List<String> path = new ArrayList<>();

// 主函数,负责初始化棋盘并调用回溯算法
public List<List<String>> solveNQueens(int n) {
// 定义棋盘,char数组,用 '.' 表示空位
char[][] chessboard = new char[n][n];
// 初始化棋盘,每个位置都填充为 '.'
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
chessboard[i][j] = '.';
}
}
backtracking(chessboard, n, 0);// 开始回溯算法,从第0行开始放置皇后
return result;// 返回所有可能的解法
}

// 回溯函数,递归尝试每一行的不同列位置放置皇后
public void backtracking(char[][] chessboard, int n, int depth) {
// 如果当前路径长度等于n,说明已经找到一个解法,加入结果集中
if (path.size() == n) {
result.add(new ArrayList<>(path)); // 将当前路径加入到结果中
return; // 回溯终止条件
}

// 尝试在当前行(depth)每一列放置皇后
for (int col = 0; col < n; col++) {
// 如果当前位置放置皇后合法
if (isValid(chessboard, n, depth, col)) {
// 构建当前行的字符串表示,并在指定列放置 'Q'
StringBuilder sb = new StringBuilder();
for (int j = 0; j < n; j++) sb.append("."); // 初始化为全是空位 '.'
sb.replace(col, col + 1, "Q"); // 在col列位置放置皇后 'Q'
path.add(sb.toString());// 将当前行加入路径
chessboard[depth][col] = 'Q';// 在棋盘上放置皇后
backtracking(chessboard, n, depth + 1);// 递归放置下一行的皇后
chessboard[depth][col] = '.';// 回溯,移除当前行的皇后

// 回溯,移除当前行的字符串布局
path.remove(path.size() - 1); // 使用 path.removeLast() 会出错,因为 List 不支持这个方法
}
}
}

// 判断在棋盘的(row, col)位置放置皇后是否合法
public boolean isValid(char[][] chessboard, int n, int row, int col) {
// 判断当前列是否有其他皇后
for (int i = 0; i < row; i++) {
if (chessboard[i][col] == 'Q') return false; // 当前列已经有皇后,放置非法
}

// 判断135度对角线(左上到右下)是否有其他皇后
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') return false; // 135度对角线上已经有皇后
}

// 判断45度对角线(右上到左下)是否有其他皇后
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (chessboard[i][j] == 'Q') return false; // 45度对角线上已经有皇后
}

// 没有冲突,放置合法
return true;
}
}

2.2 回溯算法-写法1-优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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
68
69
70
71
72
73
74
75
76
class Solution {
// 存储最终结果的列表,每个解法是一个 List<String>,表示棋盘的布局
List<List<String>> result = new ArrayList<>();

// 主函数,初始化棋盘并调用回溯算法
public List<List<String>> solveNQueens(int n) {
// 定义棋盘,初始化为 '.' 表示空位
char[][] chessboard = new char[n][n];
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) {
chessboard[i][j] = '.';
}
}

// 调用回溯函数,开始从第0行放置皇后
backtracking(chessboard, n, 0);

// 返回所有可能的解法
return result;
}

// 回溯函数,递归尝试每一行的不同列放置皇后
public void backtracking(char[][] chessboard, int n, int depth) {
// 如果已经放置了n个皇后,说明找到一个解,保存结果
if(depth == n) {
result.add(arrayToList(chessboard)); // 将棋盘的当前布局转为列表并加入结果
return; // 回溯终止条件
}

// 尝试在当前行(depth)的每一列放置皇后
for(int col = 0; col < n; col++) {
// 如果在 (depth, col) 放置皇后合法
if(isValid(chessboard, n, depth, col)) {
// 在棋盘上放置皇后
chessboard[depth][col] = 'Q';

// 递归放置下一行的皇后
backtracking(chessboard, n, depth + 1);

// 回溯,撤销当前行的皇后
chessboard[depth][col] = '.';
}
}
}

// 将棋盘转换为字符串列表,便于保存结果
public List<String> arrayToList(char[][] chessboard) {
List<String> list = new ArrayList<>();
for(char[] chs : chessboard) {
// 将每一行的字符数组转换为字符串并加入列表
list.add(String.copyValueOf(chs));
}
return list;
}

// 判断在棋盘 (row, col) 位置放置皇后是否合法
public boolean isValid(char[][] chessboard, int n, int row, int col) {
// 判断当前列是否有其他皇后
for (int i = 0; i < row; i++) {
if (chessboard[i][col] == 'Q') return false; // 同列有冲突
}

// 判断135度对角线(左上到右下)是否有皇后
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') return false; // 135度对角线上有冲突
}

// 判断45度对角线(右上到左下)是否有皇后
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (chessboard[i][j] == 'Q') return false; // 45度对角线上有冲突
}

// 没有冲突,放置合法
return true;
}
}

双指针进阶练习,在做这道题目之前大家可以先做一道这道题目,从这道题目中找思路:


LeetCode链接:88. 合并两个有序数组

1.题目描述

给你两个按 非递减顺序 排列的整数数组 nums1nums2,另有两个整数 mn ,分别表示 nums1nums2 中的元素数目。

请你 合并 nums2nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n

示例 1:

1
2
3
4
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3][2,5,6]
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。

示例 2:

1
2
3
4
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1][]
合并结果是 [1]

示例 3:

1
2
3
4
5
输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [][1]
合并结果是 [1]
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。

提示:

  • nums1.length == m + n
  • nums2.length == n
  • 0 <= m, n <= 200
  • 1 <= m + n <= 200
  • $-10^9 <= nums1[i], nums2[j] <= 10^9$

进阶:你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗?

2.题解

2.1 暴力解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
//暴力解法:直接合并,然后排序
for(int i=0;i<n;i++){
nums1[m+i] = nums2[i];
}

//方式1:使用内置函数
//Arrays.sort(nums1);
//方式2:冒泡排序
for(int i=0;i<nums1.length;i++){
for(int j=i+1;j<nums1.length;j++){
if(nums1[i] > nums1[j]){
int temp = nums1[j];
nums1[j] = nums1[i];
nums1[i] = temp;
}
}
}
}
}

2.2 双指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
// 双指针,i 指向 nums1 的第一个元素,j 指向 nums2 的第一个元素
int i = 0, j = 0;
// 创建一个临时数组 result,用于存储合并后的有序数组
int[] result = new int[m + n];
int cur; // 当前需要填入 result 的元素

// 当 i 或 j 不越界时,进行循环
while (i < m || j < n) {
// 如果 nums1 的所有有效元素已经处理完毕,将 nums2 的剩余元素直接放入 result
if (i == m) {
cur = nums2[j++];
}
// 如果 nums2 的所有元素已经处理完毕,将 nums1 的剩余元素放入 result
else if (j == n) {
cur = nums1[i++];
}
// 如果 nums1 当前元素小于 nums2 当前元素,将 nums1 的当前元素放入 result
else if (nums1[i] < nums2[j]) {
cur = nums1[i++];
}
// 否则将 nums2 当前元素放入 result
else {
cur = nums2[j++];
}
// 将当前值填入 result 的相应位置
result[i + j - 1] = cur;
}

// 将 result 数组中的元素复制回 nums1 中
for (int l = 0; l < nums1.length; l++) {
nums1[l] = result[l];
}
}
}

2.3 逆向双指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
// 双指针,i 指向 nums1 有效部分的最后一个元素,j 指向 nums2 的最后一个元素
int i = m - 1, j = n - 1;
int tail = m + n - 1;// tail 指向 nums1 的最后一个位置,作为填充的位置
int cur; // 当前需要填入 nums1 的元素

// 当 i 或 j 不越界时,进行循环
while (i >= 0 || j >= 0) {
if (i == -1) {// 如果 nums1 已经全部填完了,则将 nums2 中剩下的元素直接填入 nums1
cur = nums2[j--];
}else if (j == -1) {// 如果 nums2 已经全部填完了,则将 nums1 中剩下的元素留在原地
cur = nums1[i--];
}else if (nums1[i] > nums2[j]) {// 比较 nums1 和 nums2 的当前元素,将较大值填入 nums1 的尾部
cur = nums1[i--];
}else {
cur = nums2[j--];
}
// 将当前值填入 nums1 的尾部
nums1[tail--] = cur;
}
}
}

二分查找你学废了吗?快来看看这道题如何使用二分查找解决吧!

回溯算法练习题


LeetCode链接:491. 非递减子序列

1.题目描述

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

示例 1:

1
2
输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]

示例 2:

1
2
输入:nums = [4,4,3,2,1]
输出:[[4,4]]

提示:

  • 1 <= nums.length <= 15
  • -100 <= nums[i] <= 100

2.题解

2.1 回溯算法-哈希集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Solution {
// 存储最终结果的列表,用于保存所有满足条件的子序列
List<List<Integer>> result = new ArrayList<>();
// 存储当前子序列的路径,用于构建每一个子序列
List<Integer> path = new ArrayList<>();

// 主方法 findSubsequences,输入一个整数数组 nums,返回所有递增的子序列
public List<List<Integer>> findSubsequences(int[] nums) {
// 从索引 0 开始回溯生成子序列
backtracking(nums, 0);
// 返回生成的所有递增子序列
return result;
}

// 回溯算法,用于生成所有符合条件的子序列
public void backtracking(int[] nums, int start) {
// 当当前路径长度大于1时,将其加入结果列表中(子序列长度至少为2)
if (path.size() > 1) result.add(new ArrayList<>(path));

// 使用 Set 集合去重,避免在同一层次中使用相同的元素
Set<Integer> set = new HashSet<>();

// 遍历数组,从起始索引开始
for (int i = start; i < nums.length; i++) {
// 剪枝:跳过不符合递增条件或在同一层中已处理过的数字
// 1. 如果当前数字小于路径中的最后一个数字,则跳过(不满足递增条件)
// 2. 如果当前数字在本轮递归中已经使用过,则跳过
if ((!path.isEmpty() && nums[i] < path.get(path.size() - 1)) || set.contains(nums[i])) continue;

// 将当前数字加入 Set 集合,表示本层已经处理过该数字
set.add(nums[i]);
// 将当前数字加入子序列路径
path.add(nums[i]);
// 递归处理后续的数字,构建新的子序列
backtracking(nums, i + 1);
// 回溯:移除路径中的最后一个元素,进行下一个可能的选择
path.remove(path.size() - 1);
}
}
}

2.2 回溯算法-哈希数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Solution {
// 存储最终结果的列表,用于保存所有满足条件的子序列
List<List<Integer>> result = new ArrayList<>();
// 存储当前子序列的路径,用于构建每一个子序列
List<Integer> path = new ArrayList<>();

// 主方法 findSubsequences,输入一个整数数组 nums,返回所有递增的子序列
public List<List<Integer>> findSubsequences(int[] nums) {
// 从索引 0 开始回溯生成子序列
backtracking(nums, 0);
// 返回生成的所有递增子序列
return result;
}

// 回溯算法,用于生成所有符合条件的子序列
public void backtracking(int[] nums, int start) {
// 当当前路径长度大于1时,将其加入结果列表中(子序列长度至少为2)
if (path.size() > 1) result.add(new ArrayList<>(path));

// 创建哈希数组,范围为 [-100, 100],用于去重
boolean[] hash = new boolean[201];

// 遍历数组,从起始索引开始
for (int i = start; i < nums.length; i++) {
// 剪枝:跳过不符合递增条件或在同一层中已处理过的数字
// 1. 如果当前数字小于路径中的最后一个数字,则跳过(不满足递增条件)
// 2. 如果当前数字在本轮递归中已经使用过(即哈希数组中标记为 true),则跳过
if ((!path.isEmpty() && nums[i] < path.get(path.size() - 1)) || hash[nums[i] + 100]) continue;

// 将当前数字标记为已使用
hash[nums[i] + 100] = true;
// 将当前数字加入子序列路径
path.add(nums[i]);
// 递归处理后续的数字,构建新的子序列
backtracking(nums, i + 1);
// 回溯:移除路径中的最后一个元素,进行下一个可能的选择
path.remove(path.size() - 1);
}
}
}

回溯算法练习题 | 子集问题
LeetCode链接:78. 子集

题目1:46. 全排列

1.题目描述

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 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]]

示例 2:

1
2
输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

1
2
输入:nums = [1]
输出:[[1]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同

2.题解

  • 图解:

image-20240909193332821

2.1 回溯解法1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Solution {
// 定义存储最终结果的列表,result 用于存储所有可能的全排列
List<List<Integer>> result = new ArrayList<>();
// 定义存储当前排列路径的列表,path 用于构建当前的排列
List<Integer> path = new ArrayList<>();

// 主方法 permute,输入一个整数数组 nums,返回其所有全排列
public List<List<Integer>> permute(int[] nums) {
// 调用回溯算法,初始化一个长度为 nums.length 的布尔数组 used,用于记录每个元素是否被使用
backtracking(nums, new boolean[nums.length]);
// 返回最终的排列结果
return result;
}

// 回溯算法方法 backtracking,用于生成所有可能的全排列
public void backtracking(int[] nums, boolean[] used) {
// 如果当前路径的长度等于数组的长度,则表示已经生成了一个完整的排列
if (path.size() == nums.length) {
// 将当前路径 path 的副本加入结果列表中
result.add(new ArrayList<>(path));
return; // 结束当前递归
}

// 遍历数组中的每一个元素
for (int i = 0; i < nums.length; i++) {
// 如果当前元素已经被使用,则跳过该元素
if (used[i]) continue;
// 标记当前元素为已使用
used[i] = true;
// 将当前元素添加到排列路径 path 中
path.add(nums[i]);
// 递归调用回溯函数,处理下一个元素
backtracking(nums, used);
// 回溯:移除路径中的最后一个元素
path.remove(path.size() - 1);
// 重置当前元素的使用状态,以便下一次使用
used[i] = false;
}
}
}

2.2 回溯解法2

  • 这种写法得益于数组中没有重复元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {
// 定义存储最终结果的列表,result 用于存储所有可能的全排列
List<List<Integer>> result = new ArrayList<>();
// 定义存储当前排列路径的列表,path 用于构建当前的排列
List<Integer> path = new ArrayList<>();

// 主方法 permute,输入一个整数数组 nums,返回其所有全排列
public List<List<Integer>> permute(int[] nums) {
// 调用回溯算法,开始生成全排列
backtracking(nums, new boolean[nums.length]);
// 返回最终的排列结果
return result;
}

// 回溯算法方法 backtracking,用于生成所有可能的全排列
public void backtracking(int[] nums, boolean[] used) {
// 如果当前路径的长度等于数组的长度,则表示已经生成了一个完整的排列
if (path.size() == nums.length) {
// 将当前路径 path 的副本加入结果列表中
result.add(new ArrayList<>(path));
return; // 结束当前递归
}

// 遍历数组中的每一个元素
for (int i = 0; i < nums.length; i++) {
// 如果 path 已经包含当前元素 nums[i],则跳过该元素
if (path.contains(nums[i])) continue;
// 将当前元素添加到排列路径 path 中
path.add(nums[i]);
// 递归调用回溯函数,处理下一个元素
backtracking(nums, used);
// 回溯:移除路径中的最后一个元素
path.remove(path.size() - 1);
}
}
}

题目2:47. 全排列 II

1.题目描述

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

示例 1:

1
2
3
4
5
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]

示例 2:

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

2.题解

2.1 回溯算法-哈希集合去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Solution {
// 存储最终结果的列表,result 用于存储所有可能的全排列
List<List<Integer>> result = new ArrayList<>();
// 存储当前排列路径的列表,path 用于构建当前的排列
List<Integer> path = new ArrayList<>();

// 主方法 permuteUnique,输入一个包含重复元素的整数数组 nums,返回其所有不重复的全排列
public List<List<Integer>> permuteUnique(int[] nums) {
// 调用回溯算法,初始化标记数组,并开始回溯
backtracking(nums, new boolean[nums.length]);
// 返回最终结果
return result;
}

// 回溯算法方法 backtracking,用于生成所有可能的不重复全排列
public void backtracking(int[] nums, boolean[] used) {
// 当路径的长度等于数组的长度时,表示已经生成一个完整的排列
if (path.size() == nums.length) {
// 将当前路径 path 的副本加入结果列表中
result.add(new ArrayList<>(path));
return; // 结束当前递归
}

// 使用一个 Set 集合去重,避免在同一层中使用相同的元素
Set<Integer> set = new HashSet<>();
// 遍历数组中的每一个元素
for (int i = 0; i < nums.length; i++) {
// 如果当前元素已经被使用,或在同一层中已添加到 set 集合,则跳过该元素
if (used[i] || set.contains(nums[i])) continue;
// 添加当前元素到 set 集合,表示在当前层已经处理过该元素
set.add(nums[i]);
// 标记当前元素为已使用
used[i] = true;
// 将当前元素添加到排列路径中
path.add(nums[i]);
// 递归处理下一个元素
backtracking(nums, used);
// 回溯:移除路径中的最后一个元素
path.remove(path.size() - 1);
// 重置当前元素的使用状态,以便回溯到上一步
used[i] = false;
}
}
}

2.2 回溯算法-哈希数组去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
List<List<Integer>> result = new ArrayList<>(); // 存储最终结果的列表,保存所有不重复的全排列
List<Integer> path = new ArrayList<>(); // 存储当前排列路径的列表,用于构建每一个排列

// 主函数,输入一个包含重复数字的数组 nums,返回所有不重复的全排列
public List<List<Integer>> permuteUnique(int[] nums) {
boolean[] used = new boolean[nums.length]; // 标记数组,记录每个数字是否已经在当前排列中使用过
backtracking(nums, used); // 调用回溯算法开始生成全排列
return result; // 返回生成的所有不重复全排列结果
}

// 回溯算法方法,用于生成所有不重复的全排列
public void backtracking(int[] nums, boolean[] used) {
// 当当前排列路径的长度等于数组的长度时,表示已生成一个完整的排列
if (path.size() == nums.length) {
result.add(new ArrayList<>(path)); // 将当前路径保存到结果列表中
return; // 结束当前递归
}

int[] hash = new int[21]; // 哈希数组,避免同一层中使用相同的数字,范围 [-10, 10] 映射为 [0, 20]

// 遍历数组中的每一个元素,尝试将其加入当前排列路径
for (int i = 0; i < nums.length; i++) {
// 如果当前元素已经使用过,或者在本轮递归中已经处理过相同的元素,则跳过
if (used[i] || hash[nums[i] + 10] == 1) continue;

used[i] = true; // 标记该元素为已使用
hash[nums[i] + 10] = 1; // 在哈希数组中记录该元素本轮递归已使用
path.add(nums[i]); // 将当前元素加入排列路径

backtracking(nums, used); // 递归处理下一个元素

// 回溯过程:移除路径中的最后一个元素,并重置其使用状态
path.remove(path.size() - 1);
used[i] = false;
}
}
}

2.3 回溯算法-排序去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution {
// 存储最终结果的列表,用于保存所有不重复的全排列
List<List<Integer>> result = new ArrayList<>();
// 存储当前排列路径的列表,用于构建每一个排列
List<Integer> path = new ArrayList<>();

// 主方法 permuteUnique,输入一个包含重复元素的整数数组 nums,返回其所有不重复的全排列
public List<List<Integer>> permuteUnique(int[] nums) {
// 对数组进行排序,便于后续剪枝去重
Arrays.sort(nums);
// 调用回溯算法,初始化标记数组并开始递归生成全排列
backtracking(nums, new boolean[nums.length]);
// 返回生成的所有不重复的全排列
return result;
}

// 回溯算法,用于生成所有不重复的全排列
public void backtracking(int[] nums, boolean[] used) {
// 当路径的长度等于数组的长度时,表示已生成一个完整的排列
if (path.size() == nums.length) {
// 将当前路径的副本加入结果列表中
result.add(new ArrayList<>(path));
return; // 结束当前递归
}

// 遍历数组中的每一个元素,尝试将其加入当前排列路径
for (int i = 0; i < nums.length; i++) {
// 剪枝:跳过重复的数字
// nums[i] == nums[i-1] 保证当前数字和前一个相同
// used[i-1] == false 保证是在同一层级使用过该元素
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue;

// 如果当前元素未使用过
if (used[i] == false) {
used[i] = true; // 标记当前元素为已使用
path.add(nums[i]); // 将元素加入当前排列路径
backtracking(nums, used); // 递归处理下一个元素
path.remove(path.size() - 1); // 回溯,移除最后加入的元素
used[i] = false; // 重置使用状态,以便后续递归使用
}
}
}
}
  • 补充:去重最为关键的代码为:、
1
2
3
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
  • **如果改成 used[i - 1] == true, 也是正确的!**,去重代码如下:
1
2
3
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
continue;
}

这是为什么呢,就是上面我刚说的,如果要对树层中前一位去重,就用used[i - 1] == false,如果要对树枝前一位去重用used[i - 1] == true

对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!

这么说是不是有点抽象?

来来来,我就用输入: [1,1,1] 来举一个例子。

树层上去重(used[i - 1] == false),的树形结构如下:

47.全排列II2

树枝上去重(used[i - 1] == true)的树型结构如下:

47.全排列II3

大家应该很清晰的看到,树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索