保存递归的结果时使用数组或str具有天然的回溯性。反向思维:如果一个题正向解题较难,考虑逆向解题。比如说求满足某个条件的结果,可以找到不满足这个条件的结果,用原来给的源减去不满足条件的结果,剩下的就是满足条件的结果。
目录
leetcode17-电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
说明:
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
思路:递归
private final String[] letterMap={
" ",
" ",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz"
};
public List<String> letterCombinations(String digits) {
List<String> res = new ArrayList<>();
if(digits.length()==0){
return res;
}
findCombination(digits,0,"",res);
return res;
}
//digits表示字符串,index表示搜寻到第几个字符的索引,s表示现在索引之前已经组合好的字符串,res用来存放最终结果
private void findCombination(String digits,int index,String s,List<String> res){
if(index == digits.length()){
res.add(s);
return;
}
char[] digit = digits.toCharArray();
//获取到index对应的数字
char c = digit[index];
if(c>='0'&&c<='9'&&c!='1'){
int sum = c-'0';
char[] letter = letterMap[sum].toCharArray();
for(int i=0;i<letter.length;i++){
findCombination(digits,index+1,s+letter[i],res);
}
}
}
leetcode46-全排列
思路:递归加回溯
注意1:使用Integer[]数组来暂存一个排列,然后倒退的时候,将对应i位置的值改变就行(天然回溯)
注意2:将数组转换成List,使用Arrays.asList(Arrays.copyOf(ans,ans.length))
注意3:因为使用过的元素不能再次使用,用boolean[] visited数组标识该元素是否被访问过,注意当回溯的时候,将对应位置的visited复原。
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
if(nums.length==0){
return res;
}
Integer[] ans = new Integer[nums.length];
boolean[] visited = new boolean[nums.length];
findPermute(nums,0,res,ans,visited);
return res;
}
//nums代表原始数组,index代表排列到第几层,res存放最终结果,ans代表到index层之前存放的结果,visited用来标识该数是否被访问过
private void findPermute(int[] nums,int index,List<List<Integer>> res,Integer[] ans,boolean[] visited){
//递归终止条件
if(index==nums.length){
res.add(Arrays.asList(Arrays.copyOf(ans,ans.length)));
return;
}
for(int i=0;i<nums.length;i++){
if(visited[i]==false){
visited[i] = true;
ans[index] = nums[i];
findPermute(nums,index+1,res,ans,visited);
visited[i] = false;
}
}
}
leetcode77-组合
给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
思路:递归的时候每次都从选中的那个数后面的一个数开始找到一个没有使用过的数。
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res = new ArrayList<>();
if(k>n){
return res;
}
Integer[] ans = new Integer[k];
findCombine(n,k,1,0,ans,res);
return res;
}
//start表示从哪个元素开始查找,index表示找到第几个元素,ans用来存放结果,List<List<Integer>> res用来记录最终结果
private void findCombine(int n,int k,int start,int index,Integer[] ans,List<List<Integer>> res){
//递归终止条件
if(index == k){
res.add(Arrays.asList(Arrays.copyOf(ans,k)));
return;
}
for(int i = start;i<=n;i++){
ans[index] = i;
findCombine(n,k,i+1,index+1,ans,res);
}
}
leetcode79-单词搜索-二维平面的回溯
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
board =
[
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
]
给定 word = "ABCCED", 返回 true.
给定 word = "SEE", 返回 true.
给定 word = "ABCB", 返回 false.
思路:找到匹配的节点之后进行上下左右搜索匹配下一个节点,使用visited标记未访问的节点。使用辅助数组来实现上下左右的搜寻
public boolean exist(char[][] board, String word) {
if(board==null||word==null){
return false;
}
int[][] d = {{0,-1},{1,0},{0,1},{-1,0}};
//二维网格的长度
int m = board.length;
//二维网格的宽度
int n = board[0].length;
boolean[][] visited = new boolean[m][n];
//先遍历二维网格,若找到一个和word的第一个字母相同的元素,则进行左上右下搜索看是否能查找到后面的字母
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(isExist(board,word,0,visited,d,i,j,m,n)){
return true;
}
}
}
return false;
}
//board,word分别为二维网格和单词,index表示查找到了第几个单词,visited表示网格中某个字母是否被访问过,startx和starty表示从哪个点开始查找 //index开始的单词,m和n为二维网格的长度和宽度
private boolean isExist(char[][] board, String word,int index,boolean[][] visited,int[][] d,int startx,int starty,int m,int n){
if(index == word.length()-1){
return board[startx][starty]==word.charAt(index);
}
if(board[startx][starty]==word.charAt(index)){
//将这个字母标记为已经访问过
visited[startx][starty] = true;
//从四个方向进行查找
for(int i=0;i<4;i++){
int newx = startx + d[i][0];
int newy = starty + d[i][1];
//判断下表是否越界
if(newx>=0&&newx<m&&newy>=0&&newy<n&&visited[newx][newy]==false){
if(isExist(board,word,index+1,visited,d,newx,newy,m,n)){
return true;
}
}
}
visited[startx][starty] = false;
}
return false;
}
leetcode200-岛屿数量-floodfill算法
给定一个由 '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
示例 1:
输入:
11110
11010
11000
00000
输出: 1
示例 2:
输入:
11000
11000
00100
00011
输出: 3
思路:找到一个‘1’,且该位置没有被置为true没有被访问过,用递归将与1相连的所有‘1’置为true,知道遍历完数组。也是左上右下进行搜索,在判断新位置的时候,注意数组下表越界问题。
public int numIslands(char[][] grid) {
if(grid.length==0){
return 0;
}
//二维网格的长和宽
int m = grid.length;
int n = grid[0].length;
//用来标记该陆地是否被使用过
boolean[][] visited = new boolean[m][n];
//辅助数组,辅助实现左、上、右、下搜寻
int[][] d = {{0,-1},{1,0},{0,1},{-1,0}};
//用来记录最终的结果
int res = 0;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
//找到一个陆地就将res+1;并且将所有与之相连的陆地的visited置为true
if(grid[i][j]=='1'&&!visited[i][j]) {
res++;
dfs(grid,i,j,visited,m,n,d);
}
}
}
return res;
}
private void dfs(char[][] grid,int startx,int starty,boolean[][] visited,int m,int n,int[][] d){
visited[startx][starty]=true;
for(int i=0;i<4;i++){
int newx = startx+d[i][0];
int newy = starty+d[i][1];
if(newx>=0&&newx<m&&newy>=0&&newy<n&&!visited[newx][newy]&&grid[newx][newy]=='1'){
dfs(grid,newx,newy,visited,m,n,d);
}
}
return;
}
leetcode51-N皇后-回溯算法在人工智能中的应用
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
上图为 8 皇后问题的一种解法。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
示例:
输入: 4
输出: [
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解释: 4 皇后问题存在两个不同的解法。
思路:一层一层的放入皇后,使用int[] row数组表示第i行的皇后放在哪一行,使用col,dia1和dia2来判断某一列,两条对角线是否已经放入皇后,要回溯。将row转化为想要的形式存入最终结果。
public List<List<String>> solveNQueens(int n) {
List<List<String>> res = new ArrayList<>();
if(n==0){
return res;
}
//用来表示每一行的元素存在几列。例如row[2]=2;第3行(索引从0开始)的元素存在第二列
int[] row = new int[n];
//表示对应的列是否有皇后占用。例如col[2]=true;说明第三列已经有皇后了,不能再放皇后
boolean[] col = new boolean[n];
//对角线1,对角线上的元素i+j相等。n为8时范围为0~14
boolean[] dia1 = new boolean[2*n-1];
//对角线2,对角线上的元素i-j相等。范围为-7~7,转换为0~14
boolean[] dia2 = new boolean[2*n-1];
putQueen(n,0,row,col,dia1,dia2,res);
return res;
}
//n代表共几个皇后,index表示当前放置第几个皇后,row以数组的形式存放一个可能结果,col保证一列上不存在两个皇后,
//dia1和dia2保证对角线不存在两个皇后,res存放最终结果
private void putQueen(int n,int index,int[] row,boolean[] col,boolean[] dia1,boolean[] dia2,List<List<String>> res){
if(index == n){
//todo 将row转换成List
res.add(rowToList(row));
}
for(int i=0;i<n;i++){
if(!col[i]&&!dia1[index+i]&&!dia2[index-i+n-1]){
System.out.println(i+" "+index);
//表明可以将皇后放置在这个位置
row[index] = i;
col[i] = true;
dia1[index+i] = true;
dia2[index-i+n-1] = true;
//放置下一个皇后
putQueen(n,index+1,row,col,dia1,dia2,res);
//回溯
col[i] = false;
dia1[index+i] = false;
dia2[index-i+n-1] = false;
}
}
}
private List<String> rowToList(int[] row){
for(int i=0;i<row.length;i++){
System.out.println(5+" "+row[i]);
}
List<String> list = new ArrayList<>();
for(int i=0;i<row.length;i++){
StringBuffer str = new StringBuffer();
for(int j=0;j<row.length;j++){
if(j!=row[i]){
str.append(".");
}else{
str.append("Q");
}
}
list.add(str.toString());
}
return list;
}
习题
leetcode93-复原IP地址
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
示例:
输入: "25525511135"
输出: ["255.255.11.135", "255.255.111.35"]
思路:start代表从这个位置开始寻找后面的ip,index代表已经寻找到的ip的位数(一共4位),递归的条件是,剩下的字符串的长度要比还没有组合的ip长,剩下的字符串的长度不能比剩下的ip位数乘以3还要大。当start到达字符串末尾,且已经找到了4
位ip就找到了一个结果,使用stringbuffer的delete方法删掉后面指定的位数进行回溯。两位ip的值要>9,99<三位ip<256
public List<String> restoreIpAddresses(String s) {
List<String> res = new ArrayList<>();
if(s.length()==0){
return res;
}
StringBuilder ans = new StringBuilder();
findIpAddress(s,0,0,ans,res);
return res;
}
//从start开始查找第index个ip,str暂存之前的结果
private void findIpAddress(String s,int start,int index,StringBuilder ans,List<String> res){
//保证剩下的s的长度要在合理的范围内
if(s.length()-start<4-index){
return;
}
if(s.length()-start>(4-index)*3){
return;
}
if(index==4){
if(start>=s.length()){
//将最后多余的.去掉
res.add(ans.toString().substring(0,ans.length()-1));
}
return;
}
if(start<s.length()){
ans.append(s.charAt(start)).append(".");
findIpAddress(s,start+1,index+1,ans,res);
ans.delete(ans.length()-2,ans.length());
}
if(start+1<s.length()&&Integer.valueOf(s.substring(start, start + 2)) > 9){
ans.append(s.substring(start,start+2)).append(".");
findIpAddress(s,start+2,index+1,ans,res);
ans.delete(ans.length()-3,ans.length());
}
if(start+2<s.length()&&Integer.valueOf(s.substring(start, start + 3)) < 256
&& Integer.valueOf(s.substring(start, start + 3)) >99){
ans.append(s.substring(start,start+3)).append(".");
findIpAddress(s,start+3,index+1,ans,res);
ans.delete(ans.length()-4,ans.length());
}
}
leetcode131-分割回文串
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例:
输入: "aab"
输出:
[
["aa","b"],
["a","a","b"]
]
思路:递归加回溯,若前i个字符串为回文字符串,则判断后面的字符串是否为回文字符串,如果是则将其加入结果中。注意:加入结果的时候,使用一个新的list来装原来ans中的值,因为在进行回溯的时候,ans的值改变了,之前的结果也就不存在了。
public List<List<String>> partition(String s) {
List<List<String>> res = new ArrayList<>();
if(s.length()==0){
return res;
}
List<String> ans = new ArrayList<>();
findStr(s,ans,res);
return res;
}
//在字符串s中寻找回文字符串,ans存前面已经寻找到的字符串
private void findStr(String s,List<String> ans,List<List<String>> res){
if(s.length()==0){
List<String> temp = new ArrayList<>();
for(String s1:ans){
temp.add(s1);
}
res.add(temp);
return;
}
//一个一个的进行分割
for(int i=1;i<=s.length();i++){
if(isTrue(s.substring(0,i))){
ans.add(s.substring(0,i));
findStr(s.substring(i,s.length()),ans,res);
ans.remove(ans.size()-1);
}
}
}
//判断一个字符串是否为回文字符串
private boolean isTrue(String str){
int n = str.length();
if(n==0){
return true;
}
if(n==1){
return true;
}
int mid = n/2;
int left = 0;
int right = n-1;
while(left<right){
if(str.charAt(left)==str.charAt(right)){
left++;
right--;
continue;
}else{
return false;
}
}
return true;
}
leetcode47-全排列2
给定一个可包含重复数字的序列,返回所有不重复的全排列。
示例:
输入: [1,1,2]
输出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
思路:基本思想还是全排列。不同之处在于,在进行排列前将nums进行排序,当遍历到某个数字时,若它前面的数与它 相同,且前面的数没有被访问,说明以这个数字放在这个位置的情况已经存在了,跳过这个数字。
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
if(nums.length==0){
return res;
}
Integer[] ans = new Integer[nums.length];
boolean[] visited = new boolean[nums.length];
//将数组排序
Arrays.sort(nums);
find(nums,ans,visited,0,res);
return res;
}
private void find(int[] nums,Integer[] ans,boolean[] visited,int index,List<List<Integer>> res){
if(index == nums.length){
res.add(Arrays.asList(Arrays.copyOf(ans,ans.length)));
return;
}
for(int i=0;i<nums.length;i++){
if(i-1>=0&&nums[i]==nums[i-1]&&!visited[i-1]){
continue;
}
if(!visited[i]){
visited[i] = true;
ans[index] = nums[i];
find(nums,ans,visited,index+1,res);
visited[i] = false;
}
}
}
leetcode130-被围绕的区域
给定一个二维的矩阵,包含 'X' 和 'O'(字母 O)。
找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。
示例:
X X X X
X O O X
X X O X
X O X X
运行你的函数后,矩阵变为:
X X X X
X X X X
X X X X
X O X X
解释:
被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
思路:反向寻找,找到与边界的'O'相连的所有'O',将其置为’#‘。最后将剩余的'O'变为‘X‘,将’#‘变为'O'。对于边界上的O使用DFS进行深度优先遍历,将与之相连的'O'标记为’#‘。可使用visited[][]数组来标记已经转换后的'O'减少重复操作。
public void solve(char[][] board) {
if(board.length==0){
return;
}
int m = board.length;
int n = board[0].length;
//辅助数组
int[][] d = {{0,-1},{1,0},{0,1},{-1,0}};
//从边界上的'O'开始搜索,用visited标记其是否被使用过
boolean[][] visited = new boolean[m][n];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(board[0][j]=='O'&&!visited[0][j]){
visited[0][j]=true;
board[0][j] = '#';
dfs(board,visited,d,0,j,m,n);
}
if(board[m-1][j]=='O'&&!visited[m-1][j]){
visited[m-1][j]=true;
board[m-1][j] = '#';
dfs(board,visited,d,m-1,j,m,n);
}
if(board[i][0]=='O'&&!visited[i][0]){
visited[i][0]=true;
board[i][0] = '#';
dfs(board,visited,d,i,0,m,n);
}
if(board[i][n-1]=='O'&&!visited[i][n-1]){
visited[i][n-1]=true;
board[i][n-1] = '#';
dfs(board,visited,d,i,n-1,m,n);
}
}
}
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(board[i][j]=='O'){
board[i][j]='X';
}
if(board[i][j]=='#'){
board[i][j]='O';
}
}
}
}
private void dfs(char[][] board,boolean[][] visited,int[][] d,int i,int j,int m,int n){
for(int k=0;k<4;k++){
int newi = i+d[k][0];
int newj = j+d[k][1];
if(newi>=0&&newi<m&&newj>=0&&newj<n&&board[newi][newj]=='O'&&!visited[newi][newj]){
board[newi][newj] = '#';
visited[newi][newj] = true;
dfs(board,visited,d,newi,newj,m,n);
}
}
return;
}
leetcode417-太平洋大西洋水流问题
给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。
规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。
请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。
提示:
输出坐标的顺序不重要
m 和 n 都小于150
示例:
给定下面的 5x5 矩阵:
太平洋 ~ ~ ~ ~ ~
~ 1 2 2 3 (5) *
~ 3 2 3 (4) (4) *
~ 2 4 (5) 3 1 *
~ (6) (7) 1 4 5 *
~ (5) 1 1 2 4 *
* * * * * 大西洋
返回:
[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (上图中带括号的单元).
思路:从左边和上边开始去dfs能够流到太平洋的点,将其标记为true;从右边和下边开始去dfs能够流到大西洋的点,将其标记为true。两个boolean[][]分别标记能够流到大西洋和太平洋的点,遍历数组,如果这个点既能流到太平洋也能流到大西洋,将其加入结果中。
public List<List<Integer>> pacificAtlantic(int[][] matrix){
List<List<Integer>> res = new ArrayList<>();
if(matrix.length==0){
return res;
}
int m = matrix.length;
int n = matrix[0].length;
//从海洋边界向陆地进行搜索
//可以流向太平洋的所有点标记为true
boolean[][] pacific = new boolean[m][n];
//可以流向大西洋的所有点标记为true
boolean[][] atlantic = new boolean[m][n];
for(int i=0;i<m;i++){
dfs(matrix,pacific,i,0,m,n);
dfs(matrix,atlantic,i,n-1,m,n);
}
for(int j=0;j<n;j++){
dfs(matrix,pacific,0,j,m,n);
dfs(matrix,atlantic,m-1,j,m,n);
}
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(pacific[i][j]&&atlantic[i][j]){
res.add(Arrays.asList(i,j));
}
}
}
return res;
}
private void dfs(int[][] matrix,boolean[][] visited,int i,int j,int m,int n){
if(i<0||i>m-1||j<0||j>n-1){
return;
}
visited[i][j] = true;
if(i-1>=0&&matrix[i][j]<=matrix[i-1][j]&&!visited[i-1][j]){
dfs(matrix,visited,i-1,j,m,n);
}
if(i+1<m&&matrix[i][j]<=matrix[i+1][j]&&!visited[i+1][j]){
dfs(matrix,visited,i+1,j,m,n);
}
if(j-1>=0&&matrix[i][j]<=matrix[i][j-1]&&!visited[i][j-1]){
dfs(matrix,visited,i,j-1,m,n);
}
if(j+1<n&&matrix[i][j]<=matrix[i][j+1]&&!visited[i][j+1]){
dfs(matrix,visited,i,j+1,m,n);
}
}