回溯
递归、回溯思想类似bfs,通常需要考虑触底条件+回溯后处理(例如题目常有将临时结果记录的最后一项删除)
子集
在给定数组中做出所有子集。
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new LinkedList<>();
List<Integer> temp = new LinkedList<>();
backTrack(0, nums, res, temp);
return res;
}
private void backTrack(int i, int[] nums, List<List<Integer>> res, List<Integer> temp) {
res.add(new LinkedList<>(temp));
for (int j = i; j < nums.length; j++) {
temp.add(nums[j]);
backTrack(j + 1, nums, res, temp);
temp.remove(temp.size() - 1);
}
}
该题用递归回溯类似dfs,只不过是在一维数组内进行dfs,每次遇到新数加入结果,递归探到底,到底后回溯需要将结果数列最后一个数删掉。
子集 II
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> res = new LinkedList<>();
List<Integer> temp = new LinkedList<>();
boolean[] visited = new boolean[nums.length];
if (nums.length == 0) {
res.add(temp);
return res;
}
Arrays.sort(nums);
subSets(nums, 0, res, temp, visited);
return res;
}
private void subSets(int[] nums, int index, List<List<Integer>> res, List<Integer> temp, boolean[] visited) {
res.add(new LinkedList<>(temp));
if (index >= nums.length) {
return;
}
for (int i = index; i < nums.length; i++) {
if (i > 0 && nums[i] == nums[i - 1] && !visited[i - 1]) {
continue;
}
temp.add(nums[i]);
visited[i] = true;
subSets(nums, i + 1, res, temp, visited);
visited[i] = false;
temp.remove(temp.size() - 1);
}
}
相比于之前子集,添加了重复内容,所以需要进行重复性判断。
即使用visited进行是否遍历过的记录,当遇到新值nums[i] == nums[i - 1]
(前提需要数组排序)说明遇到两个相同值中的第二个,那么有两种情况:
①若i-1数值被用过,说明在重复的数据中,i下标值并不是重复数字中第一次出现,这次使用会出现类似12(i-1)2(i)这种情况,这样不会产生重复数据
②若i-1数值没被用,说明当前i下标值是重复数字中第一次出现,但在数组中并不是重复数字的头部,类似数组122,当第二个2想开始构造,就会与第一个2构造出的数据发生重复
所以由!visited[i - 1]
划分出两个情况进行处理。
全排列 II
目的得出给定数组的全部排列组合可能。
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new LinkedList<>();
if (nums.length == 0) {
return res;
}
Arrays.sort(nums);
boolean[] visited = new boolean[nums.length];
dfs(nums, visited, nums.length, 0, new LinkedList<>(), res);
return res;
}
private void dfs(int[] nums, boolean[] visited, int len, int size, List<Integer> path, List<List<Integer>> res) {
if (size == len) {
res.add(new LinkedList<>(path));
return;
}
for (int i = 0; i < len; i++) {
if (visited[i]) {
continue;
}
if (i > 0 && nums[i] == nums[i - 1] && !visited[i - 1]) {
continue;
}
path.add(nums[i]);
visited[i] = true;
dfs(nums, visited, len, size + 1, path, res);
visited[i] = false;
path.remove(path.size() - 1);
}
}
相比于子集的区别就是每次以新下标进行遍历时,仍要从头开始遍历,相关注意和子集II相同(相同字符判断等)
组合求和
给定数据数组和目标值,需要得出所有可以相加为目标值的组合。
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new LinkedList<>();
if (candidates.length == 0) {
return res;
}
Arrays.sort(candidates);
dfs(candidates, 0, target, new LinkedList<>(), res);
return res;
}
private void dfs(int[] candidates, int begin, int target, List<Integer> path, List<List<Integer>> res) {
if (target == 0) {
res.add(new LinkedList<>(path));
return;
}
for (int i = begin; i < candidates.length; i++) {
if (target - candidates[i] < 0) {
break;
}
path.add(candidates[i]);
dfs(candidates, i, target - candidates[i], path, res);
path.remove(path.size() - 1);
}
}
因为该题数字可以重复使用(即目标值为6,数组2 3,结果可以有{2,2,2}和{3,3})且没有重复元素,所以不用进行相同值判断的处理。
因为每次遍历由当前位置起始,所以遍历过的元素不会再次遍历,不用visited记录。
组合总和 II
相比上面题目,该题数字只能用一次
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
if (candidates.length == 0) {
return res;
}
Arrays.sort(candidates);
List<Integer> path = new LinkedList<>();
dfs(candidates, 0, target, path, res);
return res;
}
private void dfs(int[] candidates, int begin, int target, List<Integer> path, List<List<Integer>> res) {
if (target == 0) {
res.add(new ArrayList<>(path));
return;
}
for (int i = begin; i < candidates.length; i++) {
if (target - candidates[i] < 0) {
break;
}
if (i > begin && candidates[i] == candidates[i - 1]) {
continue;
}
path.add(candidates[i]);
dfs(candidates, i + 1, target - candidates[i], path, res);
path.remove(path.size() - 1);
}
}
因为数字只能用一次且有重复数据,就要防止重复组合情况出现i > begin && candidates[i] == candidates[i - 1]
。
电话号码的字母组合
给一串数字,得到其9键可组成的所有字串
public List<String> letterCombinations(String digits) {
List<String> res = new LinkedList<>();
if (digits.length() == 0) {
return null;
}
String[] map = new String[] {"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
backTrack(map, res, new StringBuilder(), digits, 0);
return res;
}
private void backTrack(String[] map, List<String> res, StringBuilder stringBuilder, String digits, int index) {
if (stringBuilder.length() == digits.length()) {
res.add(stringBuilder.toString());
return;
}
for (char word : map[digits.charAt(index) - '2'].toCharArray()) {
stringBuilder.append(word);
backTrack(map, res, stringBuilder, digits, index + 1);
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
}
}
该题目和之前求全排列类似,只是取字符换了一种方式,由String[] map = new String[] {"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
方便后续依据数字取可能字符。
括号生成
给定n组左右括号,生成所有组合并且有效
public List<String> generateParenthesis(int n) {
List<String> res = new LinkedList<>();
StringBuilder temp = new StringBuilder();
backTrack(res, temp, n, n);
return res;
}
private void backTrack(List<String> res, StringBuilder temp, int left, int right) {
if (left == 0 && right == 0) {
res.add(temp.toString());
return;
}
if (left > right) {
return;
}
if (left > 0) {
backTrack(res, temp.append("("), left - 1, right);
temp.deleteCharAt(temp.length() - 1);
}
if (right > 0) {
backTrack(res, temp.append(")"), left, right - 1);
temp.deleteCharAt(temp.length() - 1);
}
}
记录左右括号可用数,思路很简单,只需要注意剪枝条件:左括号可用大于右括号(其后无法完成匹配)
单词搜索
给定二维数组,查找其内部是否包含目标单词
public boolean exist(char[][] board, String word) {
boolean[][] visited = new boolean[board.length][board[0].length];
int[][] dir = new int[][] {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (backTrack(board, word, 0, visited, i, j, dir)) {
return true;
}
}
}
return false;
}
private boolean backTrack(char[][] board, String word, int pos, boolean[][] visited, int row, int col, int[][] dir) {
if (row < 0 || col < 0 || row >= board.length || col >= board[0].length || visited[row][col]) {
return false;
}
if (board[row][col] != word.charAt(pos)) {
return false;
}
if (pos == word.length() - 1) {
return true;
}
visited[row][col] = true;
for (int[] ints : dir) {
if (backTrack(board, word, pos + 1, visited, row + ints[0], col + ints[1], dir)) {
return true;
}
}
visited[row][col] = false;
return false;
}
这个题就可以看出经典bfs思路,到新节点,四方向遍历查找。