前言
先说递归,
递归的应用场景非常多,无论是DFS、回溯、树、图甚至排序(快排等)都会涉及递归。
再说搜索,
经典的有BFS、DFS、回溯等方法,很多情况下需要使用递归或是队列。
一、基本概念
二、经典题
0、快排(顺带说一说堆排)
//一次快排划分
public static int once(int[] a, int low, int high){
int pivot=a[low];
while (low < high){
while(low<high && a[high] >= pivot){
//从右往左开始不断把右边元素放到最左边,直到此元素小于pivot,比枢轴小的元素放到左边
high--;
a[low] = a[high];
}
while(low<high && a[low] <= pivot){
low++;
a[high] = a[low];
}
}
a[low] = pivot; //枢轴放入中间位置
return low; //返回枢轴位置
}
//快排
public static void quick(int[] a, int low, int high){
if(low < high){
int pivotPos = once(a, low, high);//获取pivot位置
quick(a, low, pivotPos); //对左边递归划分
quick(a, pivotPos+1, high); //对右边递归划分
}
}
下面是堆排序,同样是考察非常多的知识点,必须熟练。
//调整,将a[i]与a[2*i]和a[2*i+1]比较并调整。若调整后这个数导致了下面的子树不满足大根堆特性,则还要继续调整
public static void HeadAdjust(int[] a, int k, int len){
a[0] = a[k]; // 暂存
for(int i = 2 * k; i <= len; i *= 2){
//i默认找左边
if(i < len && a[i] < a[i+1]){
//左 < 右
//找左右较大的那个节点
i++;
}
if(a[0] >= a[i]){
//a[k]这个数已经大于左右节点了,完成调整
break;
}
else{
a[k] = a[i]; //否则,把较大的子节点放到父节点位置
k = i; //k表示此父节点,继续向下
}
}
a[k] = a[0]; //放回
}
//从后往前,从下往上调整所有非叶子节点,使之符合大根堆特征
public static void Build(int[] a, int len){
for(int i = len/2; i > 0; i--){
HeadAdjust(a, i, len);
}
}
//以上方法可以使数组变动后调整为大根堆,下面是从大根堆中不断取出堆顶元素从而实现递增排序的过程。
public static void HeapSort(int[] a, int len){
Build(a, len);
//每一趟将堆顶元素(最大元素)加入有序序列头部
//i指向待排序列中的最后一个元素
//实现大根堆的递增排序
for(int i = len; i > 1; i--){
int t = a[i];
a[i] = a[1];
a[1] = t;
HeadAdjust(a, 1, i-1);
}
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
}
1、表达式求值
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
String s=sc.nextLine();
//将其他括号,替换成小括号
s=s.replace("{","(");
s=s.replace("[","(");
s=s.replace("}",")");
s=s.replace("]",")");
System.out.println(slove(s));
}
public static int slove(String s){
Stack<Integer> stack=new Stack<>();
int n=s.length();
char[] chs=s.toCharArray();
int index=0;
//初始化符号为'+'
char sign='+';
//记录数字
int number=0;
for(int i=0;i<n;i++){
char ch=chs[i];
//当前字符是空格,跳过
if(ch==' ')continue;
//当前字符是数字,拼数字
if(Character.isDigit(ch)){
number=number*10+ch-'0';
}
//如果当前字符是小括号
if(ch=='('){
//移到小括号后一位字符
int j=i+1;
//统计括号的数量
int count=1;
while(count>0){
//遇到右括号,括号数-1
if(chs[j]==')')count--;
//遇到左括号,括号数+1
if(chs[j]=='(')count++;
j++;
}
//递归,解小括号中的表达式
number=slove(s.substring(i+1,j-1));
i=j-1;
}
//遇到符号,将数字处理后放进栈
if(!Character.isDigit(ch) || i==n-1){
//如果是'+',直接入栈
if(sign=='+'){
stack.push(number);
}
//如果是'-',数字取相反数在入栈
else if(sign=='-'){
stack.push(-1*number);
}
//如果是'*',弹出一个数字乘后放入栈
else if(sign=='*'){
stack.push(stack.pop()*number);
}
//如果是'/',弹出一个数字/后放入栈
else if(sign=='/'){
stack.push(stack.pop()/number);
}
//更新符号
sign=ch;
//刷新数字
number=0;
}
}
//栈中数字求和得到结果
int ans=0;
while(!stack.isEmpty()){
ans+=stack.pop();
}
return ans;
}
}
2、全排列(LC46)
DFS+回溯
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
List<Integer> output = new ArrayList<Integer>();
for (int num : nums) {
output.add(num);
}
int n = nums.length;
backtrack(n, output, res, 0);
return res;
}
public void backtrack(int n, List<Integer> output, List<List<Integer>> res, int first) {
// 所有数都填完了
if (first == n) {
res.add(new ArrayList<Integer>(output));
}
for (int i = first; i < n; i++) {
// 动态维护数组,避免再开辟一个标记数组记录已经填入的数
Collections.swap(output, first, i);
// 继续递归填下一个数
backtrack(n, output, res, first + 1);
// 撤销操作
Collections.swap(output, first, i);
}
}
3、HJ77 火车进站
栈+递归遍历
还有火车未入过站时,可以选择将此车入栈,也可以选择将之前的一个车出栈;当栈空时,也就是入栈次数等于出栈次数时,只能选择将车入栈;当出栈次数等于总车数时,即所有车都入过栈又出过栈时,便得到一个出站序列。
回溯时,则考虑将之前出栈的老车再压回栈,将之前压入栈的新车弹出栈。
这种有些类似全排列的问题是明显的回溯问题,为什么要回溯呢?是因为之前的分支对数据的操作污染了数据,在新的操作开始前必须恢复原有数据。
public class Main {
static List<String> l = new ArrayList<>(); //储存结果
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()) {
//初始化
l.clear();
int nums = in.nextInt();
int[] id = new int[nums];
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < nums; i++) {
id[i] = in.nextInt();
}
//回溯+栈
trainOut(id, 0, stack, "", 0);
//对结果集排序,输出以字典序从小到大排序的火车出站序列号
Collections.sort(l);
for (String str : l) {
System.out.println(str);
}
}
}
//i为入栈次数,n为出栈次数,str存储一趟结果
public static void trainOut(int[] id, int i, Stack<Integer> s, String str, int n) {
//当最后一个元素出栈时
if (n == id.length) {
l.add(str); //如果所有火车均出栈则将当前结果保存
}
//栈非空时,老车出栈
if (!s.empty()) {
int temp = s.pop();
trainOut(id, i, s, str + temp + " ", n + 1);
s.push(temp); //恢复现场
}
//当还有火车未进站时,新车入栈
if (i < id.length) {
s.push(id[i]);
trainOut(id, i + 1, s, str, n);
s.pop(); //恢复现场
}
}
}
4、省份数量(LC547)(图的邻接矩阵遍历)
class Solution {
public int findCircleNum(int[][] isConnected) {
int cities = isConnected.length;
boolean[] visited = new boolean[cities];
int provinces = 0;
for (int i = 0; i < cities; i++) {
if (!visited[i]) {
dfs(isConnected, visited, cities, i);
provinces++;
}
}
return provinces;
}
public void dfs(int[][] isConnected, boolean[] visited, int cities, int i) {
for (int j = 0; j < cities; j++) {
if (isConnected[i][j] == 1 && !visited[j]) {
visited[j] = true;
dfs(isConnected, visited, cities, j);
}
}
}
}
5、树的子结构
public boolean isSubStructure(TreeNode A, TreeNode B) {
if(A != null && B != null){
if(recur(A, B)){ //若AB相同
return true;
}
//若A的子树中有与B相同的
if(isSubStructure(A.left, B) || isSubStructure(A.right, B)){
return true;
}
}
return false;
}
//比较A与B是否相同
boolean recur(TreeNode A, TreeNode B) {
if(B == null) return true;
if(A == null || A.val != B.val) return false;
return recur(A.left, B.left) && recur(A.right, B.right);
}
三、回溯专题
JAVA的回溯中经常会用到使用一个List存储分支情况和进行回溯,再用一个List存储前面的那些满足情况的List。在往后面的List中添加记录时,注意要使用下面这种方式,而不是res.add(arr);
具体原因参考list.add(list)为空的原因
res.add(new ArrayList<>(arr));
大意就是加到res里的arr是arr对象的地址值的拷贝,也就是arr一旦改变则res里的每个加入的arr都将随之改变,而由于回溯的最终结果是arr为空,所以会出现res里都是空的情况。解决办法就是存入时new一个实例存入。
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
List<List<Integer>> res = new ArrayList<>();
List<Integer> arr = new ArrayList<>();
find(candidates, target, 0, 0, res ,arr);
return res;
}
public void find(int[] candidates, int target, int tempSum, int index, List<List<Integer>> res, List<Integer> arr){
if(tempSum == target){
res.add(new ArrayList<>(arr));
return;
}
for(int i = index; i < candidates.length; i++){
//由于排序,可以剪枝
if(tempSum + candidates[i] > target){
break;
}
arr.add(candidates[i]);
find(candidates, target, tempSum + candidates[i], i, res, arr);
arr.remove(arr.size()-1);
}
}