目录
1 递归典型问题
LeetCode 17-电话号码的字母组合
LeetCode 93-复原IP地址
LeetCode 131-分割回文串
2 回溯法的应用
LeetCode 46-全排列
LeetCode 47-全排列 II
3 组合问题
LeetCode 77-组合
LeetCode 39-组合总和
LeetCode 40-组合总和 II
LeetCode 216-组合总和 III
LeetCode 78-子集
LeetCode 90-子集 II
LeetCode 401
4 在二维平面上使用回溯法
LeetCode 79-单词搜索
LeetCode 200-岛屿数量
LeetCode 130-被围绕的区域
LeetCode 417
1 递归典型问题
递归法不仅仅局限于二叉树这些已经明确的数据结构中的使用,在更广泛的问题中也会使用,这类问题通常有一个明显的特征即树形结构。
例1:LeetCode 17,根据题目可以分析出如下的结构图:从图上看这是一个明显的树形结构。
把图换位形式化的表述:
其代码如下:
class Solution {
//保存所对应的的字符串
private String[] letterMap = new String[]{"abc","def","ghi","jkl", "mno","pqrs","tuv","wxyz"};
//用来保存结果
List res = new LinkedList<>();
public List letterCombinations(String digits) {
//清空,当为空的时候不应该有任何东西
res.clear();
if (digits.equals("")){
return res;
}
findCombination(digits,0,"");
}
//s中保存了从digits[0...index-1]翻译得到的一个字符串
//这个方法是寻找和digits[index]匹配的字母,获得digits[0...index]翻译得到的解
private void findCombination(String digits,int index,String s){
//递归的终止条件
if (index == digits.length()){
res.add(s);
return;
}
char c = digits.charAt(index);
String letters = letterMap[c-'2'];
for (int i = 0; i < letters.length(); i++) {
findCombination(digits,index+1,s+letters.charAt(i));
}
return;
}
}
分析整个代码流程可以看出,递归调用每次都会返回一个结果,根据这个特点递归也常被称为回溯,回溯法也是常用的暴力解决方法。在本题中比较简单,只需要暴力求解即可了,后面类似题目有LeetCode 93、131
2 回溯法的应用
例1:LeetCode 46,根据题目可以分析出如下图结构:同样是一个树形结构
形式化的表述如下:
与前面的题目有所不同的是这里的数字会相互影响的,而在上面的题目中数字与数字之间是不会冲突的。
通过直接暴力递归便可进行回溯
class Solution {
public List> permute(int[] nums) {
List>res = new LinkedList>();
backTrack(nums,new LinkedList(),res);
return res;
}
private void backTrack(int[] nums, LinkedList path, List> res) {
if (path.size() == nums.length){
res.add(new LinkedList(path));
return;
}
for (int i = 0; i < nums.length; i++) {
//当已经存放过元素时,直接跳过
if (path.contains(nums[i])) continue;
//把元素添加进路径中
path.add(nums[i]);
backTrack(nums,path,res);
//回溯时返回之前状态
path.removeLast();
}
}
}
这个代码是优化后的,添加了记忆化搜索
/**
* 本题和17题的答案很相似,只不过在递归的时候要加入一些限制条件
*/
class Solution {
//返回的结果
List> res = new LinkedList<>();
//记录元素状态
boolean[] used;
public List> permute(int[] nums) {
//清空
res.clear();
if (nums.length == 0){
return res;
}
//构造一个数组用来记录元素是否被使用过了
used = new boolean[nums.length];
Arrays.fill(used,false);
//用来保存中间的生成结果
ArrayList p = new ArrayList<>();
generatePermution(nums,0,p);
return res;
}
// p中保存了一个有index个元素的排列
// 该方法向这个排列的末尾添加第index+1个元素,获得一个有index+1个元素的排列
private void generatePermution(int[] nums,int index, ArrayList p){
if (index == nums.length){
//添加的时候必须要加上new不知道为什么
//res.add(p)这样是添加进去没有数据的
res.add(new LinkedList(p));
return;
}
//递归部分
for (int i = 0; i < nums.length; i++) {
if (!used[i]){
//如果第i个元素没有被使用,则将第i个元素添加到p中
p.add(nums[i]);
//使用过后修改元素状态
used[i] = true;
//递归调用方法
generatePermution(nums,index+1,p);
/**
* 一开始我是想着generatePermution(nums,index+1,p.add(nums[i]));这样调用,但是出错,查询了api后才发现原因
* linkedlist的add方法返回的是boolean类型,没有办法转换为list类型的所以必须先添加然后再删除
*/
//当递归调用后需要回去,这时应该把所有的状态恢复的
p.remove(p.size()-1);
used[i] = false;
}
}
return;
}
}
这道题目稍微提高的为47需要处理一下。
//这道题目虽然跑的很慢,但是自己做出来的,仿照46,虽然元素会重复但是下标不会重复,因此保存的是下标,最后结果输出的时候把转成相应的数字
class Solution {
public List> permuteUnique(int[] nums) {
Set> set = new HashSet>();
backtrack(nums,new LinkedList(),set);
return new LinkedList>(set);
}
private void backtrack(int[] nums, LinkedList path, Set> set) {
if (path.size() == nums.length) {
LinkedList temp = new LinkedList();
for (int i = 0; i < path.size(); i++) {
temp.add(nums[path.get(i)]);
}
set.add(new LinkedList(temp));
return;
}
for (int i = 0; i < nums.length; i++) {
if (path.contains(i)) continue;
path.add(i);
backtrack(nums,path,set);
path.removeLast();
}
}
}
3 组合问题
例1::LeetCode 77,根据题目可以画出如下的图示:
这道题目也可以按照总结的回溯模板写出代码,虽然很占时间,但是编写的速度快,也能通过
public List> combine(int n, int k) {
List> res = new LinkedList>();
backTrack(n,k,1,new LinkedList(),res);
return res;
}
private void backTrack(int n,int k,int start, LinkedList path, List> res) {
if (path.size() == k){
res.add(new LinkedList(path));
return;
}
for (int i = start; i <= n; i++) {
//当已经存放过元素时,直接跳过
if (path.contains(i)) continue;
//把元素添加进路径中
path.add(i);
backTrack(n,k,i+1,path,res);
//回溯时返回之前状态
path.removeLast();
}
}
对于组合问题是不考虑数字顺序的,因此分支要少很多,之所以选用递归,是因为每次的操作流程都差不多,都是从一个数组中取出一个数字。在优化的时候还要考虑剪枝操作。其代码如下:
class Solution {
//保存结果
private List> res = new LinkedList<>();
public List> combine(int n, int k) {
res.clear();
if (n <= 0|| k<=0 ||k>n){
//当不满足条件时直接返回即可
return res;
}
LinkedList temp = new LinkedList<>();
//根据题目要求最开始是从1搜索的
generateCombinations(n,k,1,temp);
return res;
}
//求解C(n,k),当前已经找到的组合存储在c中,需要从start开始搜索新的元素
private void generateCombinations(int n,int k, int start, LinkedList temp){
//递归的终止条件
if (temp.size() == k){
res.add(new LinkedList<>(temp));
return;
}
/* 这里是没有进行剪枝优化的操作,可在博客的图中看出不管怎样还会遍历到n=4,但实际上对于这种我们并不想遍历
//在写递归时这里的递归循环可以放在最后写,先把第一次的调用流程写完
for (int i = start; i <= n ; i++) {
temp.add(i);
generateCombinations(n,k,i+1,temp);
//要返回原来的状态,即回溯
temp.remove(temp.size()-1);
}
*/
// 在没有循环前一共是有k-c.seize()个空位的,所有在[i...n]中要有k-c.seize()个元素
// 因此i最多为n-(k-c.seize())+1
for (int i = start; i <= n-(k-temp.size())+1; i++) {
temp.add(i);
generateCombinations(n,k,i+1,temp);
//要返回原来的状态,即回溯
temp.remove(temp.size()-1);
}
}
}
与此类似的题目有39、40、216、78、90、401
代码实现:
LeetCode-39
public List> combinationSum(int[] candidates, int target) {
List> res = new LinkedList>();
//Arrays.sort(candidates); //先对无序的数组排序
backtrack(candidates,target,res,0,new LinkedList<>());
return res;
}
private void backtrack(int[] candidates, int target, List> res, int start, LinkedList temp) {
if (target < 0) return;
if (target == 0){
res.add(new LinkedList(temp));
return;
}
for (int i = start; i < candidates.length; i++) {
if (target < 0) break;
temp.add(candidates[i]);
//这里与前面不同,不是i+1
backtrack(candidates,target-candidates[i],res,i,temp);
temp.removeLast();
}
}
LeetCode-40
public List> combinationSum2(int[] candidates, int target) {
Set> set = new HashSet<>();
//先排序再用set集合,就解决了[1,7][7,1]这样的问题
Arrays.sort(candidates);
backtrack(candidates,target,0,new LinkedList<>(),set);
return new LinkedList<>(set);
}
private void backtrack(int[] candidates, int target, int start, LinkedList temp, Set> set) {
if (target < 0) return;
if (target == 0){
set.add(new LinkedList<>(temp));
return;
}
for (int i = start; i < candidates.length; i++) {
if (target < 0) return;
temp.add(candidates[i]);
backtrack(candidates,target-candidates[i],i+1,temp,set);
temp.removeLast();
}
}
4 在二维平面上使用回溯法
例1:LeetCode 79。本题中使用的二维平面不容易直接思考,最好是画出图形来。左侧是给定的字符数组,右侧是待寻找的字符串。
在寻找的时候从(0,0)位置开始寻找,按照上、右、下、左的顺时针顺序进行递归寻找。
class Solution {
// 这是定义了四个方向的位移,上,右,下,左
private int[][] d = new int[][]{{-1,0},{0,1},{1,0},{0,-1}};
// 定义数组的范围的变量
private int m,n;
// 记录元素是否被访问过
private boolean[][] visited;
public boolean exist(char[][] board, String word) {
// 二维平面的长度
m = board.length;
n = board[0].length;
// 初始化全部为false
visited = new boolean[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
visited[i][j] = false;
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (searchWord(board,word,0,i,j)){
return true;
}
}
}
return false;
}
// 从board[startx][starty]开始,寻找word[index...word.size()-1]
private boolean searchWord(char[][] board,String word,int index,int startx,int starty){
// 递归的终止条件,搜寻到最后一个元素时直接判断
if (index == word.length() - 1){
return board[startx][starty] == word.charAt(index);
}
if (board[startx][starty] == word.charAt(index)){
visited[startx][starty] = true;
// 从startx、starty出发,向四个方向寻找
for (int i = 0; i < 4; i++) {
int newx = startx + d[i][0];
int newy = starty + d[i][1];
// 判断是否越界,并且元素以前并没有被访问过
if (inArea(newx,newy) && !visited[newx][newy]){
if (searchWord(board,word,index+1,newx,newy)){
return true;
}
}
}
visited[startx][starty] = false;
}
return false;
}
// 判断给定的坐标是否在二维平面中
private boolean inArea(int x,int y){
return x>= 0 && x< m && y>=0 && y < n;
}
}
例2:floodfill算法,LeetCode 200。floodfill就是在区域内不断的进行深度优先遍历,进行着色,其代码和例1很类似。
class Solution {
// 这是定义了四个方向的位移,上,右,下,左
private int[][] d = new int[][]{{-1,0},{0,1},{1,0},{0,-1}};
// 定义数组的范围的变量
private int m,n;
// 记录元素是否被访问过
private boolean[][] visited;
public int numIslands(char[][] grid) {
// 二维平面的长度
m = grid.length;
// 在测试用例中有数据为空,必须判断一下
if (m == 0){
return 0;
}
n = grid[0].length;
// 初始化全部为false
visited = new boolean[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
visited[i][j] = false;
}
}
int res = 0;
// 遍历二维平面
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1' && !visited[i][j]){
res ++;
// 进行深度优先遍历,进行标记
dfs(grid,i,j);
}
}
}
return res;
}
// grid[x][y]的位置开始,进行floodfill算法
// 在内部的if三个条件中保证(x,y)合法,其grid[x][y]是没有访问过的陆地
private void dfs(char[][] grid,int x,int y){
// 访问到当前坐标了标记为true
visited[x][y] = true;
for (int i = 0; i < 4; i++) {
int newx = x + d[i][0];
int newy = y + d[i][1];
// 在这个递归中没有定义递归终止条件,其实在这三个判断中已经定义好了递归的终止条件,不满足这三个条件就无法进入递归中
if (inArea(newx,newy) && !visited[newx][newy] && grid[newx][newy] == '1'){
dfs(grid,newx,newy);
}
}
}
// 判断给定的坐标是否在二维平面中
private boolean inArea(int x,int y){
return x>= 0 && x< m && y>=0 && y < n;
}
}
与此类似题目:LeetCode 130、417
5 困难问题视频中有讲但还没记录
递归常常包含着回溯的思想。