LeetCode部分习题解答记录-递归与回溯
- 46.全排列
- 方法1:回溯
- 47.全排列 II
- 方法1:回溯
- 39.组合总和
- 方法1:回溯
- 40.组合总和II
- 方法1:回溯+剪枝
- 77.组合总和 III
- 方法1:回溯
- 方法1:回溯
- 方法2:回溯+剪枝
- 60.排列序列
- 方法1:回溯
- 17.电话号码的字母组合
- 方法1:递归
- 93.复原 IP 地址
- 方法1:递归(回溯)
- 131.分割回文串
- 方法1:回溯
- 方法2:回溯优化,通过DP等方式预先判断是否回文。待做
- 401.二进制手边
- 方法1:回溯
- 79.单词搜索
- 方法1:回溯。
- 200.岛屿数量
- 方法1:回溯,DFS
- 130.被围绕的区域
- 方法1:DFS
- 733.图像渲染
- 方法1:DFS
- 784.字母大小写全排列
- 方法1:DFS
- 22.括号生成
- 方法1:回溯
46.全排列
方法1:回溯
- 代码1
/*
状态值:
used:值的状态,当为false时,则说明当前值没有被使用,为true则使用。
path:表示当前结果,遍历过程中不断压入新数值组成一个结果
res:结果集,当处理的次数等于数组的长度时,则压入当前结果。最终会得到一个结果集。
*/
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
int len = nums.length;
if(len == 0){
return res;
}
boolean[] used = new boolean[len];
dfs(nums,len,0,used);
return res;
}
public void dfs(int[] nums, int len, int depth, boolean[] used){
if(depth == len){
//必须使用new ArrayList<>(path);在 Java 中,参数传递是值传递,对象类型变量在传参的过程中,复制的是变量的地址。
//这些地址被添加到 res 变量,但实际上指向的是同一块内存地址,因此直接使用res.add(path)我们会看到结果为一个空的列表对象。
res.add(new ArrayList<>(path));
return;
}
for(int i = 0 ; i < len ; i++){
if(used[i]){
continue;
}
path.addLast(nums[i]);
used[i] = true;
dfs(nums,len,depth+1,used);
path.removeLast();
used[i] = false;
}
}
}
47.全排列 II
方法1:回溯
- 思路:本题在46题的基础上做的改进。初始数组包含重复元素,而结果集不需要包含重复元素。因此,本题重点在于如何取消重复元素,可使用剪枝。
- 在图中 ② 处,搜索的数也和上一次一样,但是上一次的 1 还在使用中;
- 在图中 ① 处,搜索的数也和上一次一样,但是上一次的 1 刚刚被撤销,正是因为刚被撤销,下面的搜索中还会使用到,因此会产生重复,剪掉的就应该是这样的分支。
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new LinkedList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
int len = nums.length;
if(len == 0){
return res;
}
// 排序(升序或者降序都可以),排序是剪枝的前提
Arrays.sort(nums);
boolean[] used = new boolean[len];
dfs(nums,len,0,used);
return res;
}
public void dfs(int[] nums, int len, int depth, boolean[] used){
if(depth == len){
res.add(new ArrayList<>(path));
}
for(int i = 0 ; i < len ;i++){
if(used[i]){
continue;
}
//剪枝
if(i > 0 && nums[i] == nums[i-1] && !used[i-1]){
continue;
}
path.addLast(nums[i]);
used[i] = true;
dfs(nums,len,depth+1,used);
path.removeLast();
used[i] = false;
}
}
}
39.组合总和
方法1:回溯
- 思路:在搜索时候去重。如图所示,第一次找到-2后所有满足要求的数字,若其中包含-3。则在第二次从-3开始时,找出的数字必然又会包含-2。因此,在求解过程中需要去重。
- 代码演示:
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if(candidates.length == 0){
return res;
}
dfs(candidates,target,0);
return res;
}
//去重操作,在到达当前数字所代表的递归层是,应从当前数字开始遍历,不包含之前的数字。例如,2、3、6,处理3时从3开始遍历,不包含2。结合图来看。
public void dfs(int[] candidates, int curTarget,int begin){
if(curTarget < 0){
return;
}
if(curTarget == 0){
res.add(new ArrayList<>(path));
return;
}
for(int i = begin ;i < candidates.length; i++){
path.addLast(candidates[i]);
dfs(candidates,curTarget-candidates[i],i);
path.removeLast();
}
}
}
40.组合总和II
方法1:回溯+剪枝
- 思路:与39类似,结合47题的剪枝的思想
- 代码1:
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
int len = candidates.length;
if(len == 0){
return res;
}
Arrays.sort(candidates);
dfs(candidates,target,0);
return res;
}
public void dfs(int[] candidates, int curTarget, int begin){
if(curTarget == 0){
res.add(new ArrayList<>(path));
return;
}
for(int i = begin; i < candidates.length; i++){
//剪枝
if(candidates[i] > curTarget){
break;
}
//剪枝
if(i > begin && candidates[i] == candidates[i -1]){
continue;
}
path.addLast(candidates[i]);
dfs(candidates,curTarget-candidates[i],i+1);
path.removeLast();
}
}
}
77.组合总和 III
方法1:回溯
- 代码:
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
dfs(k,n,1,0,0);
return res;
}
public void dfs(int k , int n, int begin,int curSum,int depth){
if(depth == k){
if(curSum == n){
res.add(new ArrayList<>(path));
return;
}
}
for(int i = begin; i <= 9 ;i++){
//若当前i已经大于n-curSum,则break。因为后面i都不会满足要求
if(i > n - curSum){
break;
}
curSum+=i;
path.addLast(i);
dfs(k,n,i+1,curSum,depth+1);
path.removeLast();
curSum-=i;
}
}
}
方法1:回溯
- 代码1:
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
if(n == 0){
return res;
}
dfs(n,k,0,1);
return res;
}
public void dfs(int n ,int k ,int depth,int begin){
if(depth == k){
res.add(new ArrayList<>(path));
}
for(int i = begin; i <= n;i++){
path.addLast(i);
dfs(n,k,depth+1,i+1);
path.removeLast();
}
}
}
- 存在问题:耗时较长,有很多无用的搜索操作
方法2:回溯+剪枝
- 思路:如下图所示,当n=5,k=3时,从4开始取值已经没有意义了。因此,搜索有上界,可以进行剪枝来优化时间效率。通过分析,上界为n-(k-path.size())+1。
- 代码2:
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
if(n == 0){
return res;
}
dfs(n,k,0,1);
return res;
}
public void dfs(int n ,int k ,int depth,int begin){
if(depth == k){
res.add(new ArrayList<>(path));
return;
}
//剪枝操作
for(int i = begin; i <= n - (k-path.size()) + 1;i++){
path.addLast(i);
dfs(n,k,depth+1,i+1);
path.removeLast();
}
}
}
60.排列序列
方法1:回溯
- 思路:如下图所示。
- 所求排列 一定在叶子结点处得到,进入每一个分支,可以根据已经选定的数的个数,进而计算还未选定的数的个数,然后计算阶乘,就知道这一个分支的叶子结点的个数(叶子节点为排列的组合):
- 如果 k 大于这一个分支将要产生的叶子结点数(说明满足输出的排列组合不在此分支),直接跳过这个分支,这个操作叫「剪枝」;
- 如果 k 小于等于这一个分支将要产生的叶子结点数,那说明所求的全排列一定在这一个分支将要产生的叶子结点里,需要递归求解。
- 代码1:
class Solution {
StringBuilder sb = new StringBuilder();
public String getPermutation(int n, int k) {
boolean[] used = new boolean[n+1];
int[] factorial = new int[n+1];
calculateFactorial(n,factorial);
dfs(n,k,0,used,factorial);
return sb.toString();
}
public void dfs(int n,int k,int index, boolean[] used,int[] factorial){
if(index == n){
return;
}
//固定选择一个数,确认其它数组成的全排列的个数。
int count = factorial[n- index -1];
for(int i = 1 ; i <= n ; i++){
//跳过当前已固定选择的数
if(used[i]){
continue;
}
//如果当前分支全排列的个数小于k,则跳过并计算下一个分支。直至找到叶子节点。
if(count < k){
k -= count;
continue;
}
sb.append(i);
used[i] = true;
dfs(n,k,index+1,used,factorial);
return;
}
}
//计算阶乘
public void calculateFactorial(int n,int[] factorial){
factorial[0] = 1;
for(int i = 1; i <= n ; i++){
factorial[i] = factorial[i-1] * i;
}
}
}
17.电话号码的字母组合
方法1:递归
- 思路:形似树形结构,某个数字代表的字符个数相当于当前层的分支数。处理下一层时,将当前节点所代表的字符串传入。直至寻找到层底,保存整体字符串。
- 代码1:
class Solution {
List<String> res = new ArrayList<>();
String[] letterMap = new String[]{"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
public List<String> letterCombinations(String digits) {
if(digits == "" || digits.length() == 0){
return res;
}
findCombinations(digits,0,"");
return res;
}
/*
subStr:存储了[0,index-1]所转换的一个字符串
index:当前遍历的索引
digits:给定的数字字符串
*/
public void findCombinations(String digits,int index,String substr){
if(index == digits.length()){
res.add(substr);
return;
}
char c = digits.charAt(index);
String str = letterMap[c-'0'];
for(int i = 0; i < str.length();i++){
findCombinations(digits,index + 1,substr + str.charAt(i));
}
return;
}
}
- 代码2:在代码1中,有相当多字符串的拼接操作,比较费时。因此,我们通过StringBuilder消除此操作,在当前层处理完返回上一次层时,删除StringBuilder的末尾字符。以免影响上一层其余节点的操作。(对StringBuilder的操作是全局的)。
class Solution {
List<String> res = new ArrayList<>();
String[] letterMap = new String[]{"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
public List<String> letterCombinations(String digits) {
if(digits == "" || digits.length() == 0){
return res;
}
findCombinations(digits,0,new StringBuilder());
return res;
}
public void findCombinations(String digits,int index,StringBuilder substr){
if(index == digits.length()){
res.add(substr.toString());
return;
}
char c = digits.charAt(index);
String str = letterMap[c-'0'];
for(int i = 0; i < str.length();i++){
findCombinations(digits,index + 1,substr.append(str.charAt(i)));
substr.deleteCharAt(substr.length()-1); //必须删除末尾字符
}
return;
}
}
93.复原 IP 地址
方法1:递归(回溯)
- 代码1;
class Solution {
List<String> res = new ArrayList<>();
Deque<String> deque = new LinkedList<>();
public List<String> restoreIpAddresses(String s) {
if(s.length() < 4 || s.length() > 12){
return res;
}
findIpAddress(s,s.length(),4,0);
return res;
}
// s:给定字符串,len:字符串长度,noSolitTimes:字符串未分割次数,begin:分割位(起始为0)
public void findIpAddress(String s,int len ,int noSplitTimes, int begin){
if(begin == len){
if(noSplitTimes == 0){
res.add(String.join(".", deque));
}
return;
}
for(int i = begin ;i < begin + 3 ;i++){
if(i >= len){
break;
}
//判断[begin,i]当前段是否可以成为一个ip;
if (judgeIpSegment(s, begin, i)) {
//若可以成为一个ip,则截取当前字符串添加至队列中。从后面的字符串进行递归判断
String currentIpSegment = s.substring(begin, i + 1);
deque.addLast(currentIpSegment);
findIpAddress(s, len, noSplitTimes - 1, i + 1);
deque.removeLast();
}
}
}
private boolean judgeIpSegment(String s, int left, int right) {
int len = right - left + 1;
//判断首位是否为0
if (len > 1 && s.charAt(left) == '0') {
return false;
}
//判断结果是否大于255
int res = 0;
while (left <= right) {
res = res * 10 + s.charAt(left) - '0';
left++;
}
return res >= 0 && res <= 255;
}
}
131.分割回文串
方法1:回溯
- 思路:类似于复原IP地址
class Solution {
List<List<String>> res = new ArrayList<>();
Deque<String> path = new LinkedList<>();
public List<List<String>> partition(String s) {
int len = s.length();
if(len == 0){
return res;
}
dfs(s,0,len);
return res;
}
public void dfs(String s, int begin, int len){
if(begin == len){
res.add(new ArrayList<>(path));
return;
}
for(int i = begin; i < len ; i++){
if(isHuiWen(s,begin,i)){
String substr = s.substring(begin,i + 1);
path.addLast(substr);
dfs(s,i+1,len);
path.removeLast();
}
}
}
//判断是否回文
public boolean isHuiWen(String s, int begin ,int end){
while(begin <= end){
if(s.charAt(begin) != s.charAt(end)){
return false;
}
begin++;
end--;
}
return true;
}
}
方法2:回溯优化,通过DP等方式预先判断是否回文。待做
401.二进制手边
方法1:回溯
- 思路:回溯的思想,但是在遍历时,需要注意小时与分钟的处理。
- 代码:
class Solution {
List<String> res = new ArrayList<>();
int[] times = new int[]{1,2,4,8,1,2,4,8,16,32};
public List<String> readBinaryWatch(int num) {
dfs(num,4,0,0,0,0);
return res;
}
public void dfs(int num,int hoursLen, int begin, int depth,int hour, int minute){
if(depth == num){
if(hour < 12 && minute < 60){
res.add(hour + ":" + (minute <= 9 ? "0"+minute : minute));
return;
}
return;
}
for(int i = begin; i < times.length;i++){
if(i < hoursLen){
hour += times[i];
dfs(num,hoursLen,i+1,depth+1,hour,minute);
hour -= times[i];
}else{
minute += times[i];
dfs(num,hoursLen,i+1,depth+1,hour,minute);
minute -= times[i];
}
}
}
}
79.单词搜索
方法1:回溯。
- 思路:与一维回溯有所区别。二维搜索有一个方向向量,也就是上下左右四个方向。
- 代码:
class Solution {
int[][] DIRECTS = new int[][]{{-1,0},{0,-1},{1,0},{0,1}};
int rows;
int cols;
int wordLen;
boolean[][] visited;
char[] wordCharArray;
char[][] board;
String word;
public boolean exist(char[][] board, String word) {
this.rows = board.length;
if(rows == 0){
return false;
}
this.cols = board[0].length;
this.visited = new boolean[rows][cols];
this.wordCharArray = word.toCharArray();
this.wordLen = word.length();
this.board = board;
this.word = word;
//从各个起点开始进行回溯,找到则返回true,都没有找到则返回false
for(int i = 0 ; i < rows ; i ++){
for(int j = 0 ; j < cols ; j++){
if(dfs(i, j, 0)){
return true;
}
}
}
return false;
}
public boolean dfs(int x, int y ,int begin){
//begin:当前寻找的长度。若已经搜索到最末尾,判断返回
if(begin == wordLen - 1){
return board[x][y] == wordCharArray[begin];
}
//当前搜索成功,则从下一个位置开始。否则直接false。
if(board[x][y] == wordCharArray[begin]){
visited[x][y] = true;
for(int[] direct : DIRECTS){
int newX = x + direct[0];
int newY = y + direct[1];
//若newx与newy符合要求且当前新位置未被搜索,则从该位置继续开始搜索。
if(judgeXandY(newX,newY) && !visited[newX][newY]){
if(dfs(newX, newY, begin+1)){
return true;
}
}
}
visited[x][y] = false;
}
return false;
}
public boolean judgeXandY(int x, int y){
return x >= 0 && x < rows && y >= 0 && y < cols;
}
}
200.岛屿数量
方法1:回溯,DFS
- 思路:从未访问且值为1的位置开始, 进行DFS遍历,将路过的节点设置访问标记。
- 代码
class Solution {
int[][] DIRECTIONS = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
char[][] grid;
int rows;
int cols;
boolean[][] visited;
int count;
public int numIslands(char[][] grid) {
this.rows = grid.length;
if(rows == 0){
return 0;
}
this.cols = grid[0].length;
this.grid = grid;
this.visited = new boolean[rows][cols];
this.count = 0;
for(int i = 0 ; i < rows ; i ++){
for(int j = 0 ; j < cols ; j++){
//每一次满足条件,则视为一个新的起点,count++。dfs主要是求与此位置可连接的所有位置,其可以组成一个岛屿
if(!visited[i][j] && grid[i][j] == '1'){
dfs(i,j);
count++;
}
}
}
return count;
}
public void dfs(int x, int y){
visited[x][y] = true;
for(int[] dierction : DIRECTIONS){
int newX = x + dierction[0];
int newY = y + dierction[1];
if(judgeXandY(newX,newY) && !visited[newX][newY] && grid[newX][newY] == '1'){
dfs(newX,newY);
}
}
}
public boolean judgeXandY(int x, int y){
return x >= 0 && x < rows && y >= 0 && y < cols;
}
}
130.被围绕的区域
方法1:DFS
- 思路:与200题,岛屿数量相类似。
- 不同点:
- 此题没有用visited数组来判断该位置是否已经访问过。原因:因为在第一次访问时,已经将边界的’O’和与之相连的’O’全部转换为’-’。这个操作类似与visited数组,在dfs中通过判断即可知道该位置是否已访问。
- DFS有所区别。本题DFS主要是寻找’O’,因此到达一个位置后首先判断其是否为’O’。若是,则继续进行递归寻找。
class Solution {
int[][] DIRECTIONS = {{-1,0},{0,-1},{1,0},{0,1}};
int rows;
int cols;
char[][] board;
boolean[][] visited;
public void solve(char[][] board) {
this.rows = board.length;
this.cols = board[0].length;
this.board = board;
//首先将四周有'O'和与之相连的'O'都换为'-'
//处理行
for(int i = 0 ; i < cols;i++){
if(board[0][i] == 'O'){
dfs(0,i);
}
if(board[rows-1][i] == 'O'){
dfs(rows-1,i);
}
}
//处理列
for(int i = 0 ; i < rows;i++){
if(board[i][0] == 'O'){
dfs(i,0);
}
if(board[i][cols - 1] == 'O'){
dfs(i,cols-1);
}
}
//随后遍历整个数组
for(int i = 0 ; i < rows; i ++){
for(int j = 0 ; j < cols; j ++){
if(board[i][j] == '-'){
board[i][j] = 'O';
}else if(board[i][j] == 'O'){
board[i][j] = 'X';
}
}
}
}
public void dfs(int x, int y){
if(judge(x,y) && board[x][y] == 'O'){
board[x][y] = '-';
for(int[] direct : DIRECTIONS){
int newx = x + direct[0];
int newy = y + direct[1];
dfs(newx,newy);
}
}
}
public boolean judge(int x,int y ){
return x >= 0 && x < rows && y >= 0 && y < cols;
}
}
733.图像渲染
方法1:DFS
- 思路:与上一题130基本类似。只是稍微简单
class Solution {
int[][] DIRECTIONS = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
int rows;
int cols;
int[][] image;
public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
this.rows = image.length;
this.cols = image[0].length;
this.image = image;
int oldColor = image[sr][sc];
if(newColor == oldColor){
return image;
}
dfs(sr,sc,newColor,oldColor);
return image;
}
public void dfs(int x, int y, int newColor,int oldColor){
if(judgeXandY(x,y) && image[x][y] == oldColor){
image[x][y] = newColor;
for(int[] dierction : DIRECTIONS){
int newX = x + dierction[0];
int newY = y + dierction[1];
dfs(newX,newY,newColor,oldColor);
}
}
}
public boolean judgeXandY(int x, int y){
return x >= 0 && x < rows && y >= 0 && y < cols;
}
}
784.字母大小写全排列
方法1:DFS
- 代码:
class Solution {
List<String> res = new ArrayList<>();
StringBuilder sb = new StringBuilder();
public List<String> letterCasePermutation(String S) {
int len = S.length();
if(len == 0){
return res;
}
dfs(S,len,0,sb);
return res;
}
public void dfs(String s,int len, int beign, StringBuilder sb){
if(beign == len){
res.add(sb.toString());
return;
}
char ch = s.charAt(beign);
sb.append(ch);
dfs(s,len,beign+1,new StringBuilder(sb));
sb.deleteCharAt(sb.length()-1);
if(!Character.isDigit(ch)){
sb.append(ch ^= 1<<5);
dfs(s,len,beign+1,new StringBuilder(sb));
sb.deleteCharAt(sb.length()-1);
}
}
}
- 注意问题:可以将S直接转换为字符数组String.toCharArray()。对字符数组进行处理,最终通过new String(char)再将其转换为字符串。 这样,在递归过程中,不需要不断的对字符串进行拼接处理。
22.括号生成
方法1:回溯
- 代码:
class Solution {
List<String> res = new ArrayList<>();
StringBuilder sb = new StringBuilder();
public List<String> generateParenthesis(int n) {
if(n == 0){
return res;
}
dfs(n,n,sb);
return res;
}
//遍历为二叉树,因此分别判断左右括号数值是否>0。若大于,则可继续添加括号。
public void dfs(int left,int right,StringBuilder sb){
if(right == 0 && left == 0){
res.add(sb.toString());
return;
}
//剪枝:当left>rigth,说明已写入的左括号少,不满足条件直接跳过。
if(left >right){
return;
}
if(left > 0){
dfs(left - 1,right,new StringBuilder(sb.append("(")));
sb.deleteCharAt(sb.length()-1);
}
//当到达右分支,进入递归后可以不恢复字符串。因为后面再没有其余分支。去掉这步时间缩短。
if(right > 0){
dfs(left,right-1,new StringBuilder(sb.append(")")));
// sb.deleteCharAt(sb.length()-1);
}
}
}