回溯算法

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优​​搜索​​法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

题目

给定一个字符串​​S​​​,通过将字符串​​S​​中的每个字母转变大小写,我们可以获得一个新的字符串。返回所有可能得到的字符串集合。

示例:
输入: S = "a1b2"
输出: ["a1b2", "a1B2", "A1b2", "A1B2"]

输入: S = "3z4"
输出: ["3z4", "3Z4"]

输入: S = "12345"
输出: ["12345"]

注意:

  • ​S​​​ 的长度不超过​​12​​。
  • ​S​​ 仅由数字和字母组成。

实现方法

public void dfs(String pre, String S, List<String> res, int index) {
if (index == S.length())
res.add(pre);
else {
char ch = S.charAt(index);
if (!Character.isLetter(ch))
dfs(pre + ch, S, res, index + 1);
else {
// 小写字符分支
ch = Character.toLowerCase(ch);
dfs(pre + ch, S, res, index + 1);
// 大写字符分支
ch = Character.toUpperCase(ch);
dfs(pre + ch, S, res, index + 1);
}
}
}

public List<String> letterCasePermutation(String S) {
List<String> res = new LinkedList<>();
dfs("", S, res, 0);
return res;
}

// 测试
public static void main(String[] args) {
String S = "a1b";
To784To to784To = new To784To();
System.out.println(to784To.letterCasePermutation(S));
}

个人见解

回溯算法和递归紧密的联系,而递归和栈有紧密关联。我们可以将递归看做一次次入栈出栈的过程,对于回溯算法则是一种参考二叉树的遍历过程中的产生的算法处理方式。在二叉树使用遍历的处理也同样用到了递归和回溯的深度优先dfs的处理方案。

递归基本实现

递归实现1+2+3+4+5+…+100

public int test(int i) {
if(i > 0) {
int sum = i + test(i - 1);
return sum;
} else {
return 0;
}
// 最简单一个三表达式就解决了
// return i > 0 ? i + test(i - 1) : 0;

}

public static void main(String[] args) {
Test1 test1 = new Test1();
System.out.println(test1.test(5));
}

彻底理解回溯和递归算法_回溯

上面是的debug入栈数量,会将i-1依次放入系统栈中,当i = 0 时候开始出栈并计算 。

彻底理解回溯和递归算法_回溯_02


如果数量不是很多使用系统栈是可以的,但是如果数量太大就会出现栈溢出,那时候可以使用我们自己的定义栈实现或者java的集合作为栈进行处理。

接下来可以看一看二叉树的递归遍历

对于这样一颗二叉树来说:

彻底理解回溯和递归算法_递归_03


我们拿中序遍历为例

//中序遍历采用递归的方式
public void inOrder(BinaryTreeNode root){
if(null!=root){
inOrder(root.getLeft());
System.out.print(root.getData()+"\t");
inOrder(root.getRight());
}
}

执行步骤为:

  1. 根节点1,根节点不为空,入栈,执行到 inOrder(根节点1,左节点2);
  2. 节点2,不为空,入栈,执行inOder(节点2, 左节点4);
  3. 节点4,不为空, 入栈, 执行inOder(节点4, 左节点null);
  4. 节点null, 为空,结束;
  5. 接着执行第三步的 System.out.print(), 将节点4的data值 4 输出,此时打印出来的就是 【4】;
  6. 执行 inOder(结点4, 右节点 null);
  7. 节点null, 为空,结束, 这样就把第三步的代码就执行完了,节点4出栈,此时出栈的4节点后,下一步就会从节点2开始,此时就发生了回溯;
  8. 这样系统栈就剩 (从上到下)节点2、节点1,接着执行 System.out.print(),并执行下一步 inOder(节点2, 右节点5),此时打印出来的就是 【4, 2】;
  9. 接着会重复3、4、5、6、7 ,此时打印出来的就是 【4,2, 5】;
  10. 这时候将节点 2 出栈,然后执行根节点的剩余的代码,先打印根节点数据,然后执行inOrder(根节点, 右节点3), 此时打印出来的就是 【4,2, 5, 1】;
  11. 省下的步骤都是和左子树相同的步骤。

对于步骤中出栈的过程,就可以看作是回溯的过程。

接下回归这道题上

给定一个字符串​​S​​​,通过将字符串​​S​​中的每个字母转变大小写,我们可以获得一个新的字符串。返回所有可能得到的字符串集合。这样我们脑袋中是不是就形成了一颗这样二叉树:

彻底理解回溯和递归算法_递归_04

这样我们是不是可以通过遍历二叉树的思维实现上述的回溯算法呢!

注意

在使用回溯算法的时候,需要多使用剪枝函数,剔除一些无用的条件,实现回溯的优化!