算法动手刷题汇总——递归、回溯
【一】基础知识点
【1】递归
(1)什么是递归?
程序调用自身的编程技巧称为递归( recursion)。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
(2)构成递归需具备的条件
1-子问题须与原始问题为同样的事,且更为简单;
2-不能无限制地调用本身,须有个出口,化简为非递归状况处理。
(3)递归的套路
1-确定递归函数的参数和返回值
2-确定终止条件
3-确定单层函数的逻辑
(4)案例-二叉树遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int countNodes(TreeNode root) {
if(root==null) return 0;
int left = countNodes(root.left);
int right = countNodes(root.right);
return left+right+1;
}
}
程序自身调用自身是递归的首要特征,递归的本质是穷举法,会遍历所有可能发生的情况,然后选择符合的输出,所以递归的时间效率不理想,所以对于时间要求较严格的情况下,递归可能会超时。
【2】回溯
(1)什么是回溯?
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
(2)回溯和递归之间的关系?
回溯是递归的副产品,只要有递归就会有回溯。回溯算法和递归是类似的,只是在递归最后多了一步回退操作。
(3)案例-八皇后问题
n后问题要求在一个n*n格的棋盘上放置n个皇后,使得它们彼此不受攻击。按照国际象棋的规则,一个皇后可以攻击与之处在同一行或同一列或同一条斜线上的其他任何棋子。
分析:利用回溯算法,进行穷举每一种情况,不符合摆放规则的进行回溯。
public class Main{
static int max = 8;
static int count = 0;
static int[] res = new int[max];
public static void main(String[] args) {
Main queen = new Main();
queen.set(0);
System.out.println(count);
}
//判断当前行是否符合
public boolean judge(int n){
for(int i=0;i<n;i++)
//判断当前行的行(i<n说明行不可能会一样)、列(res[i]==res[n]判断列)、上斜/下斜(Math.abs(i-n)==Math.abs(res[i]-res[n])判断上下斜)是否符合
if(res[i]==res[n] || Math.abs(i-n)==Math.abs(res[i]-res[n]))
return false;
//循环遍历一遍行、列、上下斜都符合,则返回true
return true;
}
//设置当前的行
public void set(int n){
if(n==max)//如果符合n==8了,证明前面的0-7个位置判断是一定正确的,所以可以输出当前结果
{
out();
return;
}
for(int i=0;i<max;i++)
{
res[n] = i;//逐个试每个位置能否放置
if(judge(n)){//如果当前位置可以放置,则set(n+1)放置下一行,如果不可以继续试当前行的下一个位置,当相当于回溯到当前位置,没有进行下一步递归
set(n+1);
}
}
}
//输出
public void out(){
count++;//每输出一次就增加一次
System.out.println(Arrays.toString(res));
}
}
总结,回溯算法是递归的副产品,通常伴随着递归一起出现,需要注意的是,回溯算法在递归结束后会有一次当前行的回溯,也可以说是重置。
【二】刷题记录
【1】没有重复项数字的全排列
(1)题目描述
给出一组数字,返回该组数字的所有排列
例如:
[1,2,3]的所有排列如下
[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2], [3,2,1].
(以数字在数组中的位置靠前为优先级,按字典序排列输出。)
示例
输入 [1,2,3]
返回值:
[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
(2)解题思路
全排列就是对数组元素交换位置,使得每一种排列都可能出现。因为题目要求按照字典顺序输出,那么第一个排列就是数组的升序排列,它的字典序最小,后续每个元素和它后面的元素交换一次位置就是一种排列情况,但是如果要保持原来的位置不变,那就不应该从它后面的元素开始交换,而是从自己开始交换,才能保证原来的位置不变,不会漏情况。
如何保证每个元素能和自己开始后的每个元素都交换位置,这种时候我们可以考虑递归。为什么卡伊使用递归?我们可以看数组[1,2,3,4],如果遍历经过一个元素2以后,那就相当于我们确定了数组到该元素2为止的前半部分,前半部分1和2的位置都不用变了,只需要对3,4进行排列,这对于后半部分而言同样是一个全排列,同样要对从每个元素开始往后交换位置,因此后面部分就是一个子问题。
我们考虑递归的几个条件:
(1)终止条件:要交换位置的下标到了数组末尾,没有可交换的了,那这就构成了一个排列情况,可以加入输出数组。
(2)返回值:每一级的子问题应该把什么东西传递给父问题呢,这个题中我们是交换数组元素位置,前面已经确定好位置的元素就是我们返还给父问题的结果,后续递归下去会逐渐把整个数组位置都确定,形成一种排列情况。
(3)本级任务:每一级需要做的就是遍历从它开始的后续元素,每一级就与它交换一次位置
(3)具体做法
1-先把数组排序,获取字典序最小的排列情况
2-递归的时候根据当前下标,遍历后续的元素,交换二者位置后,进入下一层递归
3-处理完一分支的递归后,把交换的情况再交换回来进行回溯,进入其他分支
4-当前下标到达数组末尾就是一种排列情况
(4)实现代码
import java.util.*;
## 经典回溯
public class Solution {
List<Integer> list = new ArrayList<>();
ArrayList<ArrayList<Integer>> result = new ArrayList<>();
public ArrayList<ArrayList<Integer>> permute(int[] num) {
Arrays.sort(num);
dfs(num,list);
return result;
}
private void dfs(int[] num,List<Integer> list){
if(list.size() == num.length){
result.add(new ArrayList<>(list));
return;
}
for(int i = 0; i < num.length; i ++){
if(list.contains(num[i])){
continue;
}
list.add(num[i]);
dfs(num,list);
list.remove(list.size() - 1);
}
}
}
总结