一、复原IP地址
递归参数:startIndex一定是需要的,因为不能重复分割,记录下一层递归分割的起始位置。我们还需要一个变量dotCount,记录添加逗点的数量。
终止条件:dotCount表示逗点数量,dotCount为3说明字符串分成了4段了。然后验证一下第四段是否合法,如果合法就加入到结果集里。
单层逻辑:[startIndex, i] 这个区间就是截取的子串,需要判断这个子串是否合法。如果合法就在字符串后面加上符号.
表示已经分割。如果不合法,那此树枝和本层之后的也不需要再遍历了,直接break退出循环。
class Solution {
List<String> result = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
StringBuilder sb = new StringBuilder(s);
backTracking(sb, 0, 0);
return result;
}
private void backTracking(StringBuilder s, int startIndex, int dotCount){
if(dotCount == 3){
if(isValid(s, startIndex, s.length() - 1)){
result.add(s.toString());
}
return;
}
for(int i = startIndex; i < s.length(); i++){
if(isValid(s, startIndex, i)){
s.insert(i + 1, '.');
backTracking(s, i + 2, dotCount + 1);
s.deleteCharAt(i + 1);
}else{
break;
}
}
}
//[start, end]
private boolean isValid(StringBuilder s, int start, int end){
if(start > end)
return false;
if(s.charAt(start) == '0' && start != end)
return false;
int num = 0;
for(int i = start; i <= end; i++){
int digit = s.charAt(i) - '0';
num = num * 10 + digit;
if(num > 255)
return false;
}
return true;
}
}
二、子集
递归参数:全局变量数组path为子集收集元素,二维数组result存放子集组合。需要startIndex,因为取过的元素不能重复取。
终止条件:就是startIndex已经大于数组的长度了,就终止了,因为没有元素可取了。
单层逻辑:因为收集的是子集,不仅仅是叶子节点上的,也包括树枝上的,所以每层递归都需收集起来。
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
subsets1(nums, 0);
return result;
}
public void subsets1(int[] nums, int startIdx) {
result.add(new ArrayList<>(path));
if (startIdx >= nums.length) return;
for (int i = startIdx; i < nums.length; i++) {
path.add(nums[i]);
subsets1(nums, i + 1);
path.removeLast();
}
}
}
三、子集II
递归参数:全局变量数组path为子集收集元素,二维数组result存放子集组合。需要startIndex,因为取过的元素不能重复取。
终止条件:就是startIndex已经大于数组的长度了,就终止了,因为没有元素可取了。
单层逻辑:因为收集的是子集,不仅仅是叶子节点上的,也包括树枝上的,所以每层递归都需收集起来。同时需要进行树层去重,每一层中对于大小重复的元素是不能再使用的,所以每一层都用一个hashset来去重。
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
boolean[] used;
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
used = new boolean[nums.length];
subsetsWithDup1(nums, 0);
return result;
}
public void subsetsWithDup1(int[] nums, int startIdx) {
result.add(new ArrayList<>(path));
if (startIdx >= nums.length) return;
HashSet<Integer> hs = new HashSet<>();
for (int i = startIdx; i < nums.length; i++) {
if (hs.contains(nums[i])) continue;
path.add(nums[i]);
hs.add(nums[i]);
subsetsWithDup1(nums, i + 1);
path.removeLast();
}
}
}
四、递增子序列
递归参数:一个元素不能重复使用,所以需要startIndex。
终止条件:递增子序列大小至少为2。
单层逻辑:因为收集的是子集,不仅仅是叶子节点上的,也包括树枝上的,所以每层递归都需收集起来。同时需要进行树层去重,每一层中对于大小重复的元素是不能再使用的,所以每一层都用一个hashset来去重。同时与path中的上一个元素比较是否递增。
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracking(nums, 0);
return result;
}
private void backTracking(int[] nums, int startIndex) {
if (path.size() >= 2) {
result.add(new ArrayList<>(path));
}
HashSet<Integer> hs = new HashSet<>();
for(int i = startIndex; i < nums.length; i++) {
if (!path.isEmpty() && path.get(path.size() -1) > nums[i] || hs.contains(nums[i])) continue;
hs.add(nums[i]);
path.add(nums[i]);
backTracking(nums, i + 1);
path.removeLast();
}
}
}
五、全排列
递归参数:一个元素不以重复使用,所以不需要startIndex。
终止条件:叶子节点,就是收割结果的地方。当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。
单层逻辑:需要used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次。
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
boolean[] used;
public List<List<Integer>> permute(int[] nums) {
if (nums.length == 0){
return result;
}
used = new boolean[nums.length];
permuteHelper(nums);
return result;
}
private void permuteHelper(int[] nums){
if (path.size() == nums.length){
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++){
if (used[i]){
continue;
}
used[i] = true;
path.add(nums[i]);
permuteHelper(nums);
path.removeLast();
used[i] = false;
}
}
}
六、全排列II
递归参数:一个元素不以重复使用,所以不需要startIndex。
终止条件:叶子节点,就是收割结果的地方。当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。
单层逻辑:需要used数组,对树层和树枝都进行去重。
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
boolean[] used = new boolean[nums.length];
Arrays.fill(used, false);
Arrays.sort(nums);
backTrack(nums, used);
return result;
}
private void backTrack(int[] nums, boolean[] used) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
// used[i - 1] == true,说明同⼀树⽀nums[i - 1]使⽤过
// used[i - 1] == false,说明现在进行的是树层上的去重
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { //树层去重
continue;
}
//如果同⼀树⽀nums[i]没使⽤过开始处理
if (used[i] == false) {
used[i] = true;//标记同⼀树⽀nums[i]使⽤过,防止同一树枝重复使用
path.add(nums[i]);
backTrack(nums, used);
path.removeLast();//回溯,说明同⼀树层nums[i]使⽤过,防止下一树层重复
used[i] = false;//回溯
}
}
}
}
七、N皇后
递归参数:n是棋盘的大小,然后用row来记录当前遍历到棋盘的第几层了。
终止条件:当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了。
单层逻辑:递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。每次都是要从新的一行的起始位置开始搜,所以都是从0开始。
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] chessboard = new char[n][n];
for (char[] c : chessboard) {
Arrays.fill(c, '.');
}
backTrack(n, 0, chessboard);
return res;
}
public void backTrack(int n, int row, char[][] chessboard) {
if (row == n) {
res.add(Array2List(chessboard));
return;
}
for (int col = 0; col < n; col++) {
if (isValid(row, col, n, chessboard)) {
chessboard[row][col] = 'Q';
backTrack(n, row + 1, chessboard);
chessboard[row][col] = '.';
}
}
}
public List Array2List(char[][] chessboard) {
List<String> list = new ArrayList<>();
for (char[] c : chessboard) {
list.add(String.valueOf(c));
}
return list;
}
public boolean isValid(int row, int col, int n, char[][] chessboard) {
// 检查列
for (int i = 0; i < row; i++) { // 相当于剪枝
if (chessboard[i][col] == 'Q') {
return false;
}
}
// 检查45度对角线
for (int i = row-1, j = col-1; i >= 0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查135度对角线
for (int i = row-1, j = col+1; i >= 0 && j <= n-1; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
}
八、数独解
递归参数:解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。
终止条件:不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。
单层逻辑:一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性。
class Solution {
public void solveSudoku(char[][] board) {
solveSudokuHelper(board);
}
private boolean solveSudokuHelper(char[][] board){
//「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,
// 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」
for (int i = 0; i < 9; i++){ // 遍历行
for (int j = 0; j < 9; j++){ // 遍历列
if (board[i][j] != '.'){ // 跳过原始数字
continue;
}
for (char k = '1'; k <= '9'; k++){ // (i, j) 这个位置放k是否合适
if (isValidSudoku(i, j, k, board)){
board[i][j] = k;
if (solveSudokuHelper(board)){ // 如果找到合适一组立刻返回
return true;
}
board[i][j] = '.';
}
}
// 9个数都试完了,都不行,那么就返回false
return false;
// 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!
// 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」
}
}
// 遍历完没有返回false,说明找到了合适棋盘位置了
return true;
}
/**
* 判断棋盘是否合法有如下三个维度:
* 同行是否重复
* 同列是否重复
* 9宫格里是否重复
*/
private boolean isValidSudoku(int row, int col, char val, char[][] board){
// 同行是否重复
for (int i = 0; i < 9; i++){
if (board[row][i] == val){
return false;
}
}
// 同列是否重复
for (int j = 0; j < 9; j++){
if (board[j][col] == val){
return false;
}
}
// 9宫格里是否重复
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++){
for (int j = startCol; j < startCol + 3; j++){
if (board[i][j] == val){
return false;
}
}
}
return true;
}
}