java算法day22
- 78 子集
- 46 全排列
- 131 分割回文串
78 子集
拿到这个题,我感觉和组合的思路没有太大的区别,特点就在于,组合是要取目标节点,而子集是取所有的节点。
而且根据集合的定义,{1,2,3}和{3,2,1}是一个集合。这个时候我就感受出来,这就是组合问题。那么就按组合的思路来做。元素总是取index之后的。
所以还是先构建抽象树。
从这个过程可以进一步明确,每把一个节点加入path,就要把该状态加入结果集。而且不用剪枝。因为每个状态都要,按这个组合的思路设计好了,不会存在冗余状态。
由于我们总是需要递归,就必须要设置一个递归终止添加,不然死循环了。那么递归终止条件是什么。从遍历就可以想出来,一旦startIndex>=nums.size的时候,就代表没元素扫描了,可以return了。
但是实际写代码的时候,我发现这个递归条件可以不加,因为当startIndex>=nums.size的时候,for循环进不去,所以不用担心不断的进入下一层,从而爆栈。所以不写也可以。但是一般我都喜欢写。
class Solution {
List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
//主函数
public List<List<Integer>> subsets(int[] nums) {
//回溯算法入口,由于是数组,所以下标为0是起点。
subsetsHelper(nums, 0);
return result;
}
private void subsetsHelper(int[] nums, int startIndex){
//这个题要先把结果加进来,因为开局就要加一个空集,所以把收集都放到了最开始。
result.add(new ArrayList<>(path));
//「遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合」。
if (startIndex >= nums.length){ //终止条件可不加,因为一旦条件不符合,下面那个for循环你不可能进去的。
return;
}
for (int i = startIndex; i < nums.length; i++){
//将节点加入路径中,收集操作在下一层。
path.add(nums[i]);
//递归下一层
subsetsHelper(nums, i + 1);
//上来做回溯。
path.removeLast();
}
}
}
46 全排列
最简单的方法就是for循环。判断条件整个i!=j&&j!=k就完事了。
但是这个题目可以看到,nums.length最大是六,那你要写六层嵌套循环?
所以解法还是回溯法。
而且由于是排列,{1,2,3}和{3,2,1}是不同的答案。
该怎么做?
这个时候不妨联想一下,组合的做法。组合的思路是,前面的选了,在后面的状态中就只能往后面去选。但是组合就不一样,组合要后面的选了,还能够去取前面。
直接来看具体的做法:
把抽象树构造出来就懂了。
可以看到显然这个过程就像循环遍历一样,每到一个状态,还是从头去取,比如{1,2,3}
从取2开始看,到下一层的时候取了1,那说明每到新的一层,都是从头开始取。但是怎么防止取到重复的,这里运用一个used数组进行标记。
在取之前先检查数组,输入标记数组中存在了,那就不取,否则可以取。
所以说就是多了一个标记数组来帮助我们在回溯算法中收集需要的元素。
收割结果:
那就是叶子节点的时候,这个时候满足path.size==nums.size;
来看代码:
class Solution {
List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
//标记数组
boolean[] used;
//主函数
public List<List<Integer>> permute(int[] nums) {
if (nums.length == 0){
return result;
}
//对标记数组也做初始化。
used = new boolean[nums.length];
//开始回溯算法,可以看到这里就不用什么传参了,因为每次都从0开始
permuteHelper(nums);
return result;
}
private void permuteHelper(int[] nums){
//递归出口,收割结果
if (path.size() == nums.length){
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++){
//每到一个元素,先判断他用过没,用过了就跳下一轮,尝试下一个元素。
if (used[i]){
continue;
}
//能走到这说明这个元素没用过。那么现在就要用这个元素
//首先进行标记为用过。
used[i] = true;
//然后加入path中
path.add(nums[i]);
//递归下一层
permuteHelper(nums);
//这里已经是递归回来的逻辑了,下层已经扫描完了。
//那么就是恢复现场
//把之前加进去的元素弹出。
path.removeLast();
//把状态也改回来
used[i] = false;
}
}
}
做完就可以感受出与组合的不同了。
每层都是从0开始搜索而不是startIndex
需要used数组记录path里都放了哪些元素了
131 分割回文串
这个题我刚做时没读懂,过了两天来看懂了。
之前我以为这个题又是组合。实则是:
一个字符串,进行切割,要求切完后里面的每一个字串都是回文字串,然后把每个回文字串收集到list里面,就算一个解了。
所以这里也可以推断出,收割结果要在每一个字串都是回文字串的时候才收割。
所以可以总结出本题的两大核心:
1.怎么切
2.判断里面每个字串是否是回文字串。
这个题既然要用回溯,那么这个树怎么构建出来,这个是结论:
例如对于字符串abcdef:
组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…。
切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段…。
感受出来了不?
所以现在再来看切割问题,横向应该能感觉出来了。横向遍历应该是,第一刀切在a,那么第二刀切在ab,第三刀切在abc。
纵向:第一刀切了a,那么往bcdef之后进行第二段的切割,这个应该是在第二层了,现在我切b,那么下一层应该是从cdef开始切。
所以说就是这样来枚举的。这样穷尽所有的可能性。
做回溯的题,我们想清楚了思路之后,就开始套模板了。找出模板三要素:1、递归终止条件。2、递归函数参数。3、单层搜索逻辑。
我感觉这个题光想清楚三个问题就能做出来,在切割的过程不断的判断切割出来的是否符合要求,一旦不符合在for循环中直接提前continue,不让进递归。直接下一轮循环。
我个人理解纵向都是负责在单刀的切,横向是大刀的切。
纵向一刀一刀往外推,横向刀不断的变大。
class Solution {
List<List<String>> lists = new ArrayList<>();
Deque<String> deque = new LinkedList<>();
public List<List<String>> partition(String s) {
backTracking(s, 0);
return lists;
}
private void backTracking(String s, int startIndex) {
//如果起始位置大于s的大小,说明找到了一组分割方案
//也许刚开始看代码你会觉得还要符合都是回文字串才收割结果,起始我是每切割一次我就判断是不是回文字串,因为单反切的过程中,有一个组合不是,后面的都没必要继续切了。
//所以能走到这里,也就是startIndex能走到底,说明符合结果。
if (startIndex >= s.length()) {
lists.add(new ArrayList(deque));
return;
}
//横向遍历
for (int i = startIndex; i < s.length(); i++) {
//这里[startIndex,i]是在模拟横向切割的过程,这可以理解为横向的切割
//比如从abc来举例,那么这里刚下来就是a ,下一个循环就是ab,所以这个i不断往后走,这样截取前面的,就达到了截取横向的目的
if (isPalindrome(s, startIndex, i)) {//本层切割出来的元素符合才有资格收集元素然后进入下一层递归。
//截取子序列。
String str = s.substring(startIndex, i + 1);
//收集子序列
deque.addLast(str);
} else {
//不是就代表本层已经结束,递归下一层没有任何意义,所以continue
continue;
}
//起始位置后移,保证不重复
//能走到这说明本次切割出来的元素,符合回文,继续进入下一轮
backTracking(s, i + 1);
//这里已经从递归出来了,要进行回溯,把之前收集进去的元素弹出。
deque.removeLast();
}
}
//判断是否是回文串
//这纯粹就是个双指针。判回文串
private boolean isPalindrome(String s, int startIndex, int end) {
for (int i = startIndex, j = end; i < j; i++, j--) {
if (s.charAt(i) != s.charAt(j)) {
return false;
}
}
return true;
}
}