目录
深度优先遍历、递归、栈,三者的共同点:后进先出
回溯算法与深度优先遍历
以下是维基百科中「回溯算法」和「深度优先遍历」的定义。
回溯法 采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
1.找到一个可能存在的正确的答案;
2.在尝试了所有可能的分步方法后宣告该问题没有答案。
深度优先搜索 算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。这个算法会 尽可能深 的搜索树的分支。当结点 v 的所在边都己被探寻过,搜索将 回溯 到发现结点 v 的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止。
等做完一系列的题再回来总结自己的理解吧~~~~~~~
46.全排列
https://leetcode-cn.com/problems/permutations/
说明:
1.在全排列中,要自己学会画树,数的每一个结点表示一个阶段,每一个阶段的值不同,而这些变量不通过的值,表示为状态,在深度遍历每一次回头的过程,都需要和之前保持同样的状态值,因此在回到上一层的过程中,需要撤回的动作,叫做状态重置:例如:path.removeLast();path.remove(path.size() - 1);
2.在深度优先遍历中,需要做到的是通过一个系统栈来保存所有的状态变量,保持状态变量值正确的操作是:往下走一层的时候,path
变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,此 path
变量是一个栈;
3.深度优先遍历通过「回溯」操作,实现了全局使用一份状态变量的效果。
设计状态变量:
首先这棵树除了根结点和叶子结点以外,每一个结点做的事情其实是一样的,即:在已经选择了一些数的前提下,在剩下的还没有选择的数中,依次选择一个数,这显然是一个 递归 结构;
递归的终止条件是: 一个排列中的数字已经选够了 ,因此我们需要一个变量来表示当前程序递归到第几层,我们把这个变量叫做 depth,或者命名为 index ,表示当前要确定的是某个全排列中下标为 index 的那个数是多少;
布尔数组 used,初始化的时候都为 false 表示这些数还没有被选择,当我们选定一个数的时候,就将这个数组的相应位置设置为 true ,这样在考虑下一个位置的时候,就能够以 O(1)O(1) 的时间复杂度判断这个数是否被选择过,这是一种「以空间换时间」的思想。
这些变量称为「状态变量」,它们表示了在求解一个问题的时候所处的阶段。需要根据问题的场景设计合适的状态变量。
【这里参考:
作者:liweiwei1419
链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/
来源:力扣(LeetCode)】
因为回溯算法的时间复杂度很高,所以如果可以做一些预操作例如对数组进行排序等,提前知道这一条分支不能搜索到想要的结果,就可以节约一些时间,提前结束----剪枝。
代码运行如下,又学会了一种新的写主函数输出列表的方法,嘻嘻:
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
public class permute {
public static List<List<Integer>>permute(int[]nums){
int n=nums.length;
List<List<Integer>>res=new ArrayList<List<Integer>>();//用动态数组来存储所有可能的全排列,也就是结果
if(n==0){
return res;
}
boolean[] used=new boolean[n];
Deque<Integer>path=new ArrayDeque<>(n);//新建一个栈来进行状态转移记录
dfs(nums,0,n,path,used,res);
return res;
}
private static void dfs(int[] nums, int depth, int n, Deque<Integer> path, boolean[] used, List<List<Integer>> res) {
if(depth==n){
res.add(new ArrayList<>(path));//这里要注意的是,不能写成res.add(path),不然的话会输出空的列表
//变量 path 所指向的列表 在深度优先遍历的过程中只有一份 ,深度优先遍历完成以后,回到了根结点,成为空列表。
//在 Java 中,参数传递是 值传递,对象类型变量在传参的过程中,复制的是变量的地址。
// 这些地址被添加到 res 变量,但实际上指向的是同一块内存地址,因此我们会看到 66 个空的列表对象。
// 解决的方法很简单,在 res.add(path); 这里做一次拷贝即可。
//作者:liweiwei1419
//链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/
return;
}
for (int i = 0; i <n ; i++) {
if(!used[i]){//在还未选择的数中依次选择一个元素作为下一个位置的元素
path.addLast(nums[i]);
used[i]=true;
dfs(nums,depth+1,n,path,used,res);
used[i]=false;
path.removeLast();
}
}
}
public static void main(String[] args) {
int[] nums={1,2,3};
permute permute1=new permute();
List<List<Integer>>res=permute.permute(nums);
System.out.println(res);
}
}
运行结果如下:
47.全排||
https://leetcode-cn.com/problems/permutations-ii/
这里记录我做完46题之后,拿到47题的思考:
两题不同之处在于,46题给的是不重复的序列,要求返回所有排列结果;47题给的是可以包含重复的序列,返回的是不重复的全排列。就理解为给的和要求返回的在重复上的要求刚好相反吧~
画图分析:
这里看出,重复的结果被去掉了。想办法怎么在遍历的过程中直接去掉重复结果,而不是所有结果都拿到之后再去删减重复结果,造成复杂度增大。
思路:在会产生重复的地方进行剪枝,在遍历之前就对数组进行排序,一旦发现这一支搜索下去会出现重复,就停下来。
剪枝之后的图:
1和2两处,都是两次遇到1这个数字,但1处没有被剪掉,而二处被剪掉:
1、在图中 ② 处,搜索的数也和上一次一样,但是上一次的 1 还在使用中;
2、在图中 ① 处,搜索的数也和上一次一样,但是上一次的 1 刚刚被撤销,正是因为刚被撤销,下面的搜索中还会使用到,因此会产生重复,剪掉的就应该是这样的分支。
总结:撤销的剪掉,还在用的不剪掉;
代码:
import java.util.*;
public class permuteUnique {
public static List<List<Integer>>permuteUnique(int[]nums){
int n=nums.length;
List<List<Integer>>res=new ArrayList<>();//用动态数组来存储所有可能的全排列,也就是结果
if(n==0){
return res;
}
//排序是剪枝的前提:
Arrays.sort(nums);
boolean[] used=new boolean[n];
// 使用 Deque 是 Java 官方 Stack 类的建议
Deque<Integer>path=new ArrayDeque<>(n);//新建一个栈来进行状态转移记录
dfs(nums,0,n,path,used,res);
return res;
}
private static void dfs(int[] nums, int depth, int n, Deque<Integer> path, boolean[] used, List<List<Integer>> res) {
if(depth==n){
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i <n ; ++i) {//这里是++i
if(used[i]){//这里是used[i]
continue;
}
//剪枝条件:i > 0 是为了保证 nums[i - 1] 有意义
//!used[i - 1] 是因为 nums[i - 1] 在深度优先遍历的过程中刚刚被撤销选择
if(i>0 && nums[i]==nums[i-1] && !used[i-1]){
//这里是i-1,因为要判断的是上一个被撤销还是在用,这里的!used[i-1]表示被撤销,剪掉!used[i-1]表示在用
continue;
}
path.addLast(nums[i]);
used[i]=true;
dfs(nums,depth+1,n,path,used,res);
used[i]=false;
path.removeLast();
}
}
public static void main(String[] args) {
int[] nums={1,1,2};
permuteUnique permuteUnique1=new permuteUnique();
List<List<Integer>>res=permuteUnique.permuteUnique(nums);
System.out.println(res);
}
}
39.组合总和
这是我第一次接触回溯剪枝题,所以写的比较细致,三种方法在总结链接里。
题目链接:https://leetcode-cn.com/problems/combination-sum/
总结链接:https://blog.csdn.net/weixin_44021180/article/details/108484513
40.组合总和||
题目链接:https://leetcode-cn.com/problems/combination-sum-ii/
和39题的区别在于:
39题给的是一个无重复数组,40题给的是一个可以重复的数组;
39题数组种的数字可以无限制重复使用,40题只能使用一次;
相同点在于:相同数字列表的不同排列视为一个结果;都是通过一个大剪枝来优化代码,因为不重复,所以40题用了一个小剪枝来解决这个问题,同时不同之处还在于循环里的dfs中,从i+1开始。
代码中的体现在于:
// 小剪枝:同一层相同数值的结点,从第 2 个开始,候选数更少,结果一定发生重复,因此跳过,用 continue
if (i > begin && candidates[i] == candidates[i - 1]) {
continue;
}
//因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i,39题就是i开始,因为可以重复
dfs(candidates,i+1,target-candidates[i],n,path, res);
代码如下:
import java.util.*;
public class combinationSum222 {
public static List<List<Integer>>combinationSum222(int[] candidates,int target){
int n=candidates.length;
List<List<Integer>> res=new ArrayList<>();
if(n==0){
return res;
}
Arrays.sort(candidates);
Deque<Integer>path =new ArrayDeque<>();
dfs(candidates,0,target,n,path,res);
return res;
}
private static void dfs(int[] candidates, int begin, int target, int n, Deque<Integer> path, List<List<Integer>> res) {
if(target==0){
res.add(new ArrayList<>(path));
return;
}
for (int i = begin; i <n ; i++) {
if(target-candidates[i]<0){//大剪枝
break;
}
// 小剪枝:同一层相同数值的结点,从第 2 个开始,候选数更少,结果一定发生重复,因此跳过,用 continue
if (i > begin && candidates[i] == candidates[i - 1]) {
continue;
}
path.addLast(candidates[i]);
//因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i,39题就是i开始,因为可以重复
dfs(candidates,i+1,target-candidates[i],n,path, res);
path.removeLast();
}
}
public static void main(String[] args) {
int[] candidates={10,1,2,7,6,1,5};
int target=8;
List<List<Integer>>res=combinationSum222(candidates,target);
System.out.println(res.toString());
}
}
77.组合
链接:https://leetcode-cn.com/problems/combinations/
剪枝最重要的一步:
代码:
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
public class combine {
public static List<List<Integer>>combine(int n, int k){
List<List<Integer>>res = new ArrayList<>();
if(k<=0 ||n<k){
return res;
}
Deque<Integer>path=new ArrayDeque<>();
dfs(n,1,k,path,res);
return res;
}
private static void dfs(int n, int begin, int k, Deque<Integer> path, List<List<Integer>> res) {
if(path.size()==k){
res.add(new ArrayList<>(path));
return;
}
// 遍历可能的搜索起点,n-(k-path.size())+1是搜索数的上界,注意是i<'='n-(k-path.size())+1
for (int i = begin; i <=n-(k-path.size())+1 ; i++) {//这里是重点,接下来要选择的元素个数 = k - path.size()
// 向路径变量里添加一个数
path.addLast(i);
dfs(n,i+1,k,path,res);
//深度优先遍历有回头的过程,因此递归之前做了什么,递归之后需要做相同操作的逆向操作
path.removeLast();
}
}
public static void main(String[] args) {
int n=4,k=2;
List<List<Integer>>res=combine(n,k);
System.out.println(res.toString());
}
}
78.子集
链接:https://leetcode-cn.com/problems/subsets/
代码:
package leecode;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
public class subsets {
public static List<List<Integer>>subsets(int[]nums){
int n=nums.length;
List<List<Integer>>res=new ArrayList<>();
// if(n<=1){ 不加这个
// return res;
// }
Deque<Integer>path=new ArrayDeque<>();
dfs(nums,n,0,path,res);
return res;
}
private static void dfs(int[] nums, int n, int begin, Deque<Integer> path, List<List<Integer>> res) {
//if(path.size()>0 &&path.size()<=n){ 此处不需要加这个条件,
res.add(new ArrayList<>(path));
// return;
//}
for (int i = begin; i <n ; i++) {
path.addLast(nums[i]);
dfs(nums,n,i+1,path,res);
path.removeLast();
}
}
public static void main(String[] args) {
int[]nums={1,2,3};
List<List<Integer>>res=subsets(nums);
System.out.println(res.toString());
}
}
90.子集||
链接:https://leetcode-cn.com/problems/subsets-ii/
与 78题不同之处在于:78题的nums不含重复元素,90题的num可能包含重复元素
代码:就比不含重复的时候多了个提前排好顺序:
package leecode;
import java.util.*;
public class subsetsWithDup {
public static List<List<Integer>>subsetsWithDup(int[] nums){
int n=nums.length;
List<List<Integer>>res=new ArrayList<>();
if(n==0){//这里加不加运行结果怎么都一样?
return res;
}
Arrays.sort(nums);//没排序,就错了,有重复的时候,得排序?
Deque<Integer> path=new ArrayDeque<>();
dfs(nums,n,0,path,res);
return res;
}
private static void dfs(int[] nums, int n, int begin, Deque<Integer> path, List<List<Integer>> res) {
res.add(new ArrayList<>(path));
for (int i = begin; i <n ; i++) {
if(i>begin &&nums[i]==nums[i-1]){
continue;
}
path.addLast(nums[i]);
dfs(nums,n,i+1,path,res);
path.removeLast();
}
}
public static void main(String[] args) {
int[]nums={4,4,4,1,4};
List<List<Integer>>res=subsetsWithDup(nums);
System.out.println(res.toString());
}
}
60.第K个排列
链接:https://leetcode-cn.com/problems/permutation-sequence/
思路分析:
1.可以不必列出所有的排列;
2.得到的排列结果一定在叶子节点处;
3.可以根据叶节点点数的个数来结算每个分支上的排列个数;【2.3.4.5】为4!个结果;
4.根据K,可以分析想要得到的第k个结果在哪个分支上;
代码:
package leecode;
import java.util.Arrays;
public class getPermutation {
/**
* 记录数字是否使用过
*/
private boolean[] used;
/**
* 阶乘数组
*/
private int[] factorial;
private int n;
private int k;
public String getPermutation(int n, int k) {
this.n = n;
this.k = k;
calculateFactorial(n);
// 查找全排列需要的布尔数组
used = new boolean[n + 1];
Arrays.fill(used, false);
StringBuilder path = new StringBuilder();
dfs(0, path);
return path.toString();
}
/**
* @param index 在这一步之前已经选择了几个数字,其值恰好等于这一步需要确定的下标位置
* @param path
*/
private void dfs(int index, StringBuilder path) {
if (index == n) {
return;
}
// 计算还未确定的数字的全排列的个数,第 1 次进入的时候是 n - 1
int cnt = factorial[n - 1 - index];
for (int i = 1; i <= n; i++) {
if (used[i]) {
continue;
}
if (cnt < k) {
k -= cnt;
continue;
}
path.append(i);
used[i] = true;
dfs(index + 1, path);
// 注意 1:不可以回溯(重置变量),算法设计是「一下子来到叶子结点」,没有回头的过程
// 注意 2:这里要加 return,后面的数没有必要遍历去尝试了
return;
}
}
/**
* 计算阶乘数组
*
* @param n
*/
private void calculateFactorial(int n) {
factorial = new int[n + 1];
factorial[0] = 1;
for (int i = 1; i <= n; i++) {
factorial[i] = factorial[i - 1] * i;
}
}
}
93.复原IP地址
链接:https://leetcode-cn.com/problems/restore-ip-addresses/
package leecode;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
public class restoreIpAddresses {
public List<String>restoreIpAddresses(String s){
int len=s.length();
List<String>res=new ArrayList<>();
//剪枝1.长度分析
if(len<4 ||len>12){
return res;
}
Deque<String>path =new ArrayDeque<>();
int splitTimes=0;//:已经分割出多少个 ip 段;
dfs(s,len,splitTimes,0,path,res);
return res;
}
private int judgeIfIpSegment(String s,int left,int right){
int len=right-left+1;
//大于1位的时候,不可以以0开头
if(len>1&&s.charAt(left)=='0'){
return -1;
}
//转换成Int型
int res=0;
for (int i = left; i <=right ; i++) {
res=res*10+s.charAt(i)-'0';
}
if(res>255){
return -1;
}
return res;
}
private void dfs(String s, int len, int split, int begin, Deque<String> path, List<String> res) {
if(begin==len){
if(split==4){
res.add(String.join(".",path));
}
return;
}
//如果剩下的不够了,退出剪枝,len-begin表示剩余的还未分割的字符串的位数
if (len - begin < (4 - split) || len - begin > 3 * (4 - split)) {
return;
}
for (int i = 0; i <3 ; i++) {
if(begin+i>=len){
break;
}
int ipSegment = judgeIfIpSegment(s, begin, begin + i);
if (ipSegment != -1) {
// 在判断是 ip 段的情况下,才去做截取
path.addLast(ipSegment + "");
dfs(s, len, split + 1, begin + i + 1, path, res);
path.removeLast();
}
}
}
}