77. 组合
思路:求的是组合,所以[1,2][2,1]这种算是同一个,不能重复计算。因此需要一个标识idx:来表明当前的横向遍历范围是从哪里开始的!
终止条件:
题目要的是能组成k 个数的所有组合。所以终止条件就是当前组合长度path等于k时,就可以把当前存储的组合path加入到最终集合结果result中。
横向遍历集合:从头开始遍历整个集合,一开始会传入idx的初始值,作为遍历的起头。
纵向遍历集合:因为不能含重复组合,所以要从idx+1作为纵向遍历的起头
class Solution {
List<List<Integer>> result=new ArrayList<>();
List<Integer> path=new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backTrack(n,k,1);
return result;
}
public void backTrack(int n,int k,int idx){
//终止条件
if(path.size()==k){
//存储符合的结果
result.add(new ArrayList<>(path));
return;
}
//横向
for(int i=idx;i<=n;i++){
path.add(i);
//纵向
backTrack(n,k,i+1);
//回溯
path.remove(path.size()-1);
}
}
}
216. 组合总和 III
思路:跟上题差不多,但要注意这里的k是使用的元素个数,n是元素累加要等于的和。
所以在终止条件判断时,是要用k来判断当前集合path中的个数是否符合要求,再接着判断累加值是否符合n,如果是则加入最终结果集合result中。
class Solution {
List<List<Integer>> result=new ArrayList<>();
//也可以跟上题一样用ArrayList,但删除元素时方法不同
LinkedList<Integer> path=new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backTrack(k,n,1,0);
return result;
}
public void backTrack(int k,int n,int idx,int sum){
if(path.size()==k){
if(sum==n){
result.add(new ArrayList<>(path));
}
return;
}
for(int i=idx;i<=9;i++){
sum+=i;
path.add(i);
backTrack(k,n,i+1,sum);
sum-=i;
//LinkedList删除元素
path.removeLast();
}
}
}
17. 电话号码的字母组合
思路:稍微复杂一点,但理清楚怎么进行遍历就好!首先每一个数字都会对应一组字母,例如2对应abc,3对于def,注意0和1没有对应的字母!
假设我按23,能呈现的组合就有:ad,ae,af,bd,be,bf,cd,ce,cf。看出一点感觉了,每次横向遍历要遍历的元素就是:每个数字对应的字母组!所以在遍历前需要先获得当前数字对应的字母组是谁!然后再开始横向遍历。
class Solution {
List<String> list=new ArrayList<>();
public List<String> letterCombinations(String digits) {
if(digits==null || digits.length()==0)
return list;
//strs:是数组,记录每个数字对应的字母
String[] strs={"","","abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
backTrack(digits,0,strs,new StringBuilder());
return list;
}
//idx:遍历digits的元素,获取按到的数字,数字会对应一组字母集合(strs记录了对应关系)
//sb:用来记录当前字母的组合
public void backTrack(String digits,int idx,String[] strs,StringBuilder sb){
//终止条件
if(idx>=digits.length()){
list.add(sb.toString());
return;
}
//获取当前要遍历的字母集合(横向遍历)
String str=strs[digits.charAt(idx)-'0'];
for(int i=0;i<str.length();i++){
sb.append(str.charAt(i));
//纵向遍历
//idx+1:因为下次横向遍历时,数字要往后一个才能获得对应字母组!
backTrack(digits,idx+1,strs,sb);
sb.deleteCharAt(sb.length()-1);
}
}
}
39. 组合总和
思路:这题解题思路差不多,但是要注意!元素可以重复获取! 所以纵向遍历时要注意一下方法中的参数不用加一哦!
class Solution {
List<List<Integer>> list=new ArrayList<>();
LinkedList<Integer> path=new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if(candidates.length==0 || candidates==null)
return list;
backTrack(candidates,target,0);
return list;
}
public void backTrack(int[] candidates,int target,int idx){
//终止条件
if(target<=0){
if(target==0){
list.add(new ArrayList<>(path));
}
return;
}
for(int i=idx;i<candidates.length;i++){
path.add(candidates[i]);
//可以重复取元素,所以i不用+1
backTrack(candidates,target-candidates[i],i);
path.removeLast();
}
}
}
40. 组合总和 II(注意)
思路:这题比前面更难一点,题目重点:
- 有重复的元素
- 每个元素只能使用一次
- 并且结果集合不能包含重复的组合!
-
前面两个要求,可以用一个布尔值数组来记录,每个元素是否被用到!如果正在使用元素需要标记为true,没有使用或者已经释放了元素要标记为false。
-
如何避免重复组合? 答:同一树层使用过的元素不能重复选取,这样才能避免结果组合重复!
例如同一树层中,有两个1,第一个1之前使用过了,所以当前树层不能再用1,于是第二个1需要跳过。(注意元素在同一个组合内(树枝)是可以重复的,但是不能有两个组合具备完全相同元素,所以是树层去重复)
-
如何判断树层中,当前元素是否需要跳过?
下面代码表示:前后两个元素相同,并且之前使用过这个元素但是已经把它释放了(所以标记为false),说明同一树层中该元素曾经被使用过!,避免结果组合重复,要跳过这个元素(放弃它这一树枝)
if(i>0 && candidates[i-1]==candidates[i] && used[i-1]==false){
continue;
}
注意!因为有重复的元素,所以集合要先进行排序!
class Solution {
List<List<Integer>> list=new ArrayList<>();
LinkedList<Integer> path=new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
if(candidates==null || candidates.length==0)
return list;
Arrays.sort(candidates);
//used:确保元素是否使用过,如果是则true,否则为false
boolean[] used=new boolean[candidates.length];
backTrack(candidates,target,0,used);
return list;
}
public void backTrack(int[] candidates,int target,int start,boolean[] used){
if(target<=0){
if(target==0){
list.add(new ArrayList<>(path));
}
return;
}
for(int i=start;i<candidates.length;i++){
//表示之前的树枝用过这个元素了并且把它释放了所以为false,说明同一树层中该元素曾经被使用过!,避免结果组合重复,要跳过这个元素(放弃它这一树枝)
if(i>0 && candidates[i-1]==candidates[i] && used[i-1]==false){
continue;
}
else{
//使用该元素,记录一下
used[i]=true;
path.add(candidates[i]);
backTrack(candidates,target-candidates[i],i+1,used);
//释放该元素,记录一下
used[i]=false;
path.removeLast();
}
}
}
}
131. 分割回文串
思路:把一树枝(纵向)都切割好,再放入到结果集合中。所以终止条件是:所有元素都遍历完。
要有一个方法来判断是否为回文子串。
class Solution {
List<List<String>> list=new ArrayList<>();
LinkedList<String> path=new LinkedList<>();
public List<List<String>> partition(String s) {
if(s==null || s.length()==0)
return list;
backTrack(s,0);
return list;
}
public void backTrack(String s,int idx){
if(idx==s.length()){
list.add(new ArrayList<>(path));
return;
}
for(int i=idx;i<s.length();i++){
//是回文子串才进行递归
if(compare(s,idx,i)){
path.add(s.substring(idx,i+1));
backTrack(s,i+1);
path.removeLast();
}
}
}
public boolean compare(String s,int start,int end){
if(start>end)
return false;
while(start<=end){
if(s.charAt(start)==s.charAt(end)){
start++;
end--;
}
else
return false;
}
return true;
}
}
93. 复原 IP 地址
思路:
- 首先需要一个方法判断选出的值是否符合要求(0-255之间)。
- 思考回溯方法的参数有哪些,通常都有一个起始下标变量
idx,再来一个变量point记录加入的点数量,方便作为终止条件的判断! - 符合终止条件后,里面还需要再判断一次最后的值是否符合要求!因为它在之前的遍历过程中是没有被判断到的(第三个点加上去的时候,判断的是第三个值符合要求)所以最后的值需要在终止条件中进行判断,符合才可以加入结果集合中
class Solution {
List<String> list=new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
if(s.length()>12)
return list;
backTrack(s,0,0);
return list;
}
public void backTrack(String s,int idx,int point){
//需要一个变量:记录.的数量
if(point==3){
//判断最后一区段是否符合要求(0-255)
if(isValid(s,idx,s.length()-1)){
list.add(s);
}
return;
}
for(int i=idx;i<s.length();i++){
if(isValid(s,idx,i)){
s=s.substring(0,i+1)+"."+s.substring(i+1);
//注意参数是i+2,要跳过上面刚加入的.(它下标是i+1)
backTrack(s,i+2,point+1);
//跳过上面刚加入的.
s=s.substring(0,i+1)+s.substring(i+2);
}
}
}
public boolean isValid(String s,int start,int end){
if(start>end)
return false;
//不能含有前导0
if(s.charAt(start)=='0' && start!=end)
return false;
int sum=0;
for(int i=start;i<=end;i++){
char ch=s.charAt(i);
if(ch>'9' || ch<'0')
return false;
sum=sum*10+ch-'0';
if(sum>255)
return false;
}
return true;
}
}
78. 子集
思路:返回该数组所有可能的子集,所以跟之前不同的是:每一个树节点都要加入到结果集合中,以前都是收集树的叶子节点(不是所有节点)。
如何收集每一个树节点?答:在循环外面,其他步骤没变
class Solution {
List<List<Integer>> list=new ArrayList<>();
LinkedList<Integer> path=new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
if(nums.length==0 || nums==null)
return list;
backTrack(nums,0);
return list;
}
public void backTrack(int[] nums,int start){
//加入结果集合中
list.add(new ArrayList<>(path));
for(int i=start;i<nums.length;i++){
path.add(nums[i]);
backTrack(nums,i+1);
path.removeLast();
}
}
}
90. 子集 II(注意)
思路:这题和上一题目的区别就在于有重复元素。题目要求结果必须是去重,看到去重,就要想到用布尔值数组来记录元素的使用!这里实现的是树层去重
class Solution {
List<List<Integer>> list=new ArrayList<>();
LinkedList<Integer> path=new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
//数组要先排序!
Arrays.sort(nums);
boolean[] used=new boolean[nums.length];
backTrack(nums,0,used);
return list;
}
public void backTrack(int[] nums,int start,boolean[] used){
list.add(new ArrayList<>(path));
for(int i=start;i<nums.length;i++){
if(i>0 && nums[i-1]==nums[i] && used[i-1]==false)
continue;
else{
used[i]=true;
path.add(nums[i]);
backTrack(nums,i+1,used);
path.removeLast();
used[i]=false;
}
}
}
}
491. 递增子序列(注意)
思路:这题有难度,去重方法跟之前不一样。首先题目要我们返回排序的子集,并且不能有重复结果,数组中还有可能有重复的元素。
-
根据要求,不能先给数组排序,这样以前用的去重方法就不能使用。那要如何知道哪些元素已经使用过了要跳过呢?
答:用set来存储:同一父节点下,本层使用过的元素! 注意这个
set要在哪里创建很重要,他要记录同一父节点下,本层使用过的元素,因此应该是在进入循环前就要创建。 -
数组中有重复的元素,要进行处理吗?
答:不需要处理,因为题目说:相等的数字应该被视为递增的一种情况。
class Solution {
List<List<Integer>> list=new ArrayList<>();
LinkedList<Integer> path=new LinkedList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
if(nums.length<2)
return list;
backTrack(nums,0);
return list;
}
public void backTrack(int[] nums,int start){
if(path.size()>=2){
list.add(new ArrayList<>(path));
return;
}
//同一父节点下的同层元素不能重复使用!
Set<Integer> set=new HashSet<>();
for(int i=start;i<nums.length;i++){
if(!path.isEmpty() && path.getLast()>nums[i] || set.contains(nums[i]))
continue;
else{
set.add(nums[i]);
path.add(nums[i]);
backTrack(nums,i+1);
path.removeLast();
}
}
}
}
46. 全排列
思路:进入排列问题,那么像:[1,2,3][2,1,3]这种就算不同的结果是符合要求的。所以不需要变量标识循环的起始下标了(舍弃start(idx)变量),每次循环都是从第一个元素开始!
那要如何跳过path中已有的元素呢?
答:用布尔值数组来记录!此时判断条件:如果为true表示该元素在树枝上使用过了(path中已有),需要跳过!
class Solution {
List<List<Integer>> list=new ArrayList<>();
LinkedList<Integer> path=new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
boolean[] used=new boolean[nums.length];
if(nums.length==0 || nums==null)
return list;
backTrack(nums,used);
return list;
}
public void backTrack(int[] nums,boolean[] used){
if(path.size()==nums.length){
list.add(new ArrayList<>(path));
return ;
}
for(int i=0;i<nums.length;i++){
if(used[i])
continue;
used[i]=true;
path.add(nums[i]);
backTrack(nums,used);
used[i]=false;
path.removeLast();
}
}
}
47. 全排列 II
思路:跟上一题目的区别在于,有重复的元素。所以除了要思考树枝,还要思考树层。
-
首先树枝要如何跳过已经使用的元素?
答:当
userd[i]=true时,表示path中已经使用该元素,要跳过 -
树层如何跳过重复的元素?
答:当前后元素相同,并且
userd[i-1]=false,表示前一个元素已经使用过并且释放掉,所以要跳过当前元素
class Solution {
List<List<Integer>> list=new ArrayList<>();
LinkedList<Integer> path=new LinkedList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
if(nums==null || nums.length==0)
return list;
Arrays.sort(nums);
boolean[] used=new boolean[nums.length];
backTrack(nums,used);
return list;
}
public void backTrack(int[] nums,boolean[] used){
if(path.size()==nums.length){
list.add(new ArrayList<>(path));
return;
}
for(int i=0;i<nums.length;i++){
//过滤树层
if(i>0 && nums[i-1]==nums[i] && used[i-1]==false)
continue;
//过滤树枝:used[i]=true表示树枝上已经使用该元素,要跳过
if(used[i]==false){
used[i]=true;
path.add(nums[i]);
backTrack(nums,used);
used[i]=false;
path.removeLast();
}
}
}
}
51. N 皇后
思路:
- 不能同行
- 不能同列
- 不能同斜线(45度和135度)
需要一个方法判断,当前坐标放上棋子,是否符合以上要求(只需要检查当前坐标之前的即可,因为后面都没放棋子所以肯定符合要求)
再来一个方法将当前结果数组转为list集合!
class Solution {
List<List<String>> list=new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] board=new char[n][n];
for(char[] ch : board){
Arrays.fill(ch,'.');
}
backTrack(n,0,board);
return list;
}
public void backTrack(int n,int row,char[][] board){
if(row==n){
list.add(ArraysToList(board));
return;
}
for(int col=0;col<n;col++){
if(isValid(n,board,row,col)){
board[row][col]='Q';
backTrack(n,row+1,board);
board[row][col]='.';
}
}
}
//转换集合
public List<String> ArraysToList(char[][] board){
List<String> list=new ArrayList<>();
for(char[] ch : board){
list.add(new String(ch));
}
return list;
}
//判断是否符合要求
public boolean isValid(int n,char[][] board,int row,int col){
//检查行
for(int i=0;i<row;i++){
if(board[i][col]=='Q')
return false;
}
//检查列
for(int i=0;i<col;i++){
if(board[row][i]=='Q')
return false;
}
// 检查45度对角线
for(int i=row-1,j=col-1;i>=0 && j>=0;i--,j--){
if(board[i][j]=='Q')
return false;
}
// 检查135度对角线
for(int i=row-1,j=col+1;i>=0 && j<n;i--,j++){
if(board[i][j]=='Q')
return false;
}
return true;
}
}
37. 解数独
思路:跟皇后一样,要判断当前位置放该数字是否符合要求,注意要判断两个九宫格!一个大九宫格和一个小九宫格。
可能存在:当前位置无法放入任何数字,那就是无解,返回false即可。
class Solution {
public void solveSudoku(char[][] board) {
backTrack(board);
}
public boolean backTrack(char[][] board){
for(int i=0;i<board.length;i++){
for(int j=0;j<board[0].length;j++){
if(board[i][j]=='.'){
for(char ch='1';ch<='9';ch++){
if(isValid(board,i,j,ch)){
board[i][j]=ch;
if(backTrack(board))
return true; //找到结果返回
//记得回溯
board[i][j]='.';
}
}
//9个数字都填不进去肯定无解
return false;
}
}
}
return true;
}
public boolean isValid(char[][] board,int row,int col,char k){
//检查大九宫格的范围
//检查行
for(int i=0;i<board.length;i++){
if(board[i][col]==k)
return false;
}
//检查列
for(int i=0;i<board[0].length;i++){
if(board[row][i]==k)
return false;
}
//检查小九宫格(要先获取小九宫格行列的起始位置)
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]==k)
return false;
}
}
return true;
}
}