算法总结二:recursion应用千变万化,我该怎么办?

本文通过讲解LeetCode中的多个题目,深入探讨了递归算法的应用,包括基本的递归分解策略,以及如何利用动态规划进行优化。文章通过实例解析了递归在解决最长大于等于K重复字符子串、最长回文子路径、二叉搜索树的范围和、全二叉树构建等问题中的应用,并强调了递归过程中子问题定义、当前层处理和返回结果的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

学习目标:

总结recursion的规律和自己的应对方法

学习内容:

1、 leetcode高频recursion25题

学习时间:

1、 周一至周五晚上 7 点—晚上9点

学习产出:

1、 技术笔记博客 1 篇

==================================
帅地博客讲解recursion

基础题:
lc13
Evaluate a to the power of b, assuming both a and b are integers and b is non-negative.
recursion
recursively将问题分解成小一号的问题。
power(a,b) --> power(a,b/2);

public class Solution {
  public long power(int a, int b) {
    if(a==0) return 0;
    if(b==0) return 1;

    long half=power(a,b/2);
    if(b%2==0){
      return half*half;
    }else{
      return half*half*a;
    }
  }
}

lc624
Get the Kth number in the Fibonacci Sequence. (K is 0-indexed, the 0th Fibonacci number is 0 and the 1st Fibonacci number is 1).
recursion
recursively将问题分解成小一号的问题。
fibonacci(3)=fibonacci(2)+fibonacci(1)
“递归”中,随着k不断减小,
“递去”遇到最小元素,k=0,k=1在base case中都已经写好了。
“归来”的就是答案。

public class Solution {
  public int fibonacci(int K) {
     //base case
     if(K<=0) return 0;
     if(K==1) return 1;
     //recursion rule
     return fibonacci(K-1)+fibonacci(K-2);
  }
}

优化:
dp的思想,用一个记事本,从最小元素开始,不断往大的元素推导。

  public int fibonacci(int K) {

   if(K<=0){
       return 0;
   }
   long[] array=new long[K+1];
   array[1]=1;
   for (int i=2;i<K+1;i++){
       array[i]=array[i-2]+array[i-1];
   }
   return (int)array[K]; 
  }
//why use long here then cast to int?
//it is possible to get an overflowed number, 
//and sometimes we will need to use something like BigInteger.

高频题:
395. Longest Substring with At Least K Repeating Characters
Given a string s and an integer k, return the length of the longest substring of s such that the frequency of each character in this substring is greater than or equal to k.
key insights:
这题中,将原问题longestSubstring(String s, int k)转成设定好边界的问题。longestSubstringUtil(s,0,s.length(),k);
然后用divide and conquer方法,将原问题分解成小一号的问题longestSubstringUtil(s,start,mid,k)和longestSubstringUtil(s,midnext,end,k)。

问自己三个问题
Q1.子问题是什么?–> 以第1个不满足条件的char为分界点,切分大问题
Q2.当前层做什么?–> 对比两个子问题,谁的解更长,取max?
Q3.返回给上一层什么?–> 返回一个长度。

class Solution {
    public int longestSubstring(String s, int k) {
        
        //longestSustring(start, end) 
        //= max(longestSubstring(start, mid), longestSubstring(mid+1, end))
        return longestSubstringUtil(s,0,s.length(),k);
    }
    public int longestSubstringUtil(String s, int start, int end,int k) {
        if(end<k) return 0;
        int[] countMap=new int[26];
        for(int i=start;i<end;i++){
            countMap[s.charAt(i)-'a']++;
        }
        for(int mid=start;mid<end;mid++){
            if(countMap[s.charAt(mid)-'a']>=k){
                continue;
            }
            int midnext=mid+1;
            while(midnext<end && countMap[s.charAt(midnext)-'a']<k) {
                //abba c ab
                midnext++;
                
            }
            return Math.max(longestSubstringUtil(s,start,mid,k),longestSubstringUtil(s,midnext,end,k));
        }
        //if no those returns, then return the whole s.length();
        return end-start; 
    }
}

687. Longest Univalue Path
在这里插入图片描述
key insights:
这题中,将原问题longestUnivaluePath(root)转成dfs(root);
然后用divide and conquer方法,将原问题分解成小一号的问题dfs(root.left)、dfs(root.right)。
问自己三个问题

典型的问题2和问题3不一致的例子
Q1.子问题是什么?–> dfs(root.left)、dfs(root.right)
Q2.当前层做什么?–>
发现root根的值和root.left的值相等,那么串连,长度+1。
发现root根的值和root.right的值相等,那么串连,长度+1。更新到结果中。
res=Math.max(res,arrowLeft+arrowRight);
Q3.子问题返回给上一层什么?–>
子问题返回更长的那个解给上一层。

class Solution {
    int res;
    public int longestUnivaluePath(TreeNode root) {
        res=0;
        dfs(root);
        return res;
    }
     public int dfs(TreeNode root){
        if(root==null) return 0;
         
        int leftRes=dfs(root.left);
        int rightRes=dfs(root.right);
         
        int arrowLeft=0,arrowRight=0;
         //?怎么数一数
        if(root.left!=null && root.val==root.left.val){
             arrowLeft+=leftRes+1;
        } 
        if(root.right!=null && root.val==root.right.val) {
             arrowRight+=rightRes+1;
        }
        
        res=Math.max(res,arrowLeft+arrowRight);
        return Math.max(arrowLeft,arrowRight);
    }
}

938. Range Sum of BST
Given the root node of a binary search tree, return the sum of values of all nodes with a value in the range [low, high].

在这里插入图片描述

key insights:
容易混淆的点:
1.如果 if(root.val>=high)
low3,high10, root10, 就不能走右侧。
根据逻辑推倒:
相反:
if(root.val<high){
dfs(root.right, low, high,res);
}

   由于不能走右侧,有“加值”和“走左侧”两种选择。以下推论就是错的:

low3,high10, root10, 就走左侧。
2.
if()…
else if()…
else()
三个情况是互斥的关系,满足了其中一个,就不可能满足另外一个。【人生路只能选一条】

if()…
if()…
if()…
三个情况有可能重叠。比如,low5,high15, root10, 既满足了第一个条件,又满足了第二个条件,又满足了第三个条件。
10>=5 10<=15, 可以被加入结果中,
10<15可以递归走到右边,根变成15继续判断…
10>5可以递归走到左边, 根变成5继续判断…

class Solution {   
    public int rangeSumBST(TreeNode root, int low, int high) {
        int[]res=new int[1];
        dfs(root, low, high,res);
        return res[0];
    }
    
    public void dfs(TreeNode root, int low, int high,int[]res){
        if(root==null) return;
// !!!重点!!!
        if(root.val<high){
            dfs(root.right, low, high,res);
        }
        if(root.val>low){
            dfs(root.left, low, high,res);
        }
        if(root.val>=low && root.val<=high){
            res[0]+=root.val;
        }  
    }
}

894. All Possible Full Binary Trees
在这里插入图片描述

key insights:
观察满树发现,总数为奇数。
如果为偶数则直接返回空。
如果奇数,分配数量时,
左子树如果为1,右子树为N-1-1;
左子树如果为3,右子树为N-3-1;
左子树如果为5,右子树为N-5-1;

分配好数量后,左子树和右子树各自执行recursion,内部细节不深究。
在最小单元,即1个node时,会new一个value为0的node
在最小关系,即3个node之间:
TreeNode root = new TreeNode(0);
root.left = l;
root.right = r;
疑惑: 为什么会想到用for for循环的方式构建?
这里是for each的想法。

class Solution {
  public List<TreeNode> allPossibleFBT(int N) {
    List<TreeNode> ans = new ArrayList<>();
    if (N % 2 == 0) return ans;
    if (N == 1) {
      ans.add(new TreeNode(0));
      return ans;
    }
    for (int i = 1; i < N; i += 2) {
      for (TreeNode l : allPossibleFBT(i))
        for (TreeNode r : allPossibleFBT(N - i - 1)) {
          TreeNode root = new TreeNode(0);
          root.left = l;
          root.right = r;
          ans.add(root);
        }          
    }
    return ans;
  }
}

776. Split BST
在这里插入图片描述
以例子中的树为例,如果V==5,
递归中,递去:
root4<5
调用子问题splitBST(root.right,V)的结果
即调用splitBST(6,5)的结果

root6>5
调用子问题splitBST(root.left,V)的结果
即调用splitBST(5,5)的结果

root5=5
调用子问题splitBST(root.right,V)的结果
即调用splitBST(null,5)的结果

结果找到base case,为{null, null};

递归中,归来:
root==null,返回「null,null」给上一层的结果
root5=5,返回「5,null」给上一层的结果,修改了res[0]
root6>5,返回「5,6」给上一层的结果,修改了res[1]
root4<5,返回「4,6」为最终结果,修改了res[0]
这个解法中,每次轮流修改结果中的一个数。

Solution1:
code更精巧

class Solution {
    public TreeNode[] splitBST(TreeNode root, int V) {     
        if(root==null) {
        	return new TreeNode[]{null, null};
        }else if(root.val<=V){   //r==5=5
            TreeNode[] rightRes=splitBST(root.right,V); // 6
            //TreeNode[] res=splitBST(root.right,V); 
            root.right=rightRes[0]; //5.right=null   
            rightRes[0]=root;//rR[0]=5
            return rightRes;//返回「5,null」给上一层的结果
        }else{//r==6  > v5    
            TreeNode[] leftRes=splitBST(root.left,V);//step t 2 step to null,
            //TreeNode[] res=splitBST(root.right,V); 
            root.left=leftRes[1]; //6.left=null   
            leftRes[1]=root; //leftRe[1]=6
            return leftRes;//返回「5,6」给上一层的结果
        }
    }
}

Solution2:
步骤分得更细致,更好理解!
Q1:recursion中子问题是什么?调用子问题的结果,
Q2:理清楚当前层左右关系。
Q3:返回给上一层什么?

public TreeNode[] splitBST(TreeNode root, int V) {
        if (root == null)
            return new TreeNode[]{null, null};
        if (root.val == V) {//r==5,right=null,
            TreeNode right = root.right;
            root.right = null;
            return new TreeNode[]{root, right};//return{5,null}
            //example:V==5
        } else if (root.val > V) {//root(6)>5 call res of splitBST(5,V)above
            TreeNode[] nodes = splitBST(root.left, V);//向左走,调用子问题的解
            TreeNode left = nodes[0];//left=5,
            TreeNode right = nodes[1];//right=null
            root.left = right;//6.left=null断开
            return new TreeNode[]{left,root}; //返回「5,6」给上一层的结果
        } else {//1.root=4<5, call res of splitBST(6,V)above
            TreeNode[] nodes = splitBST(root.right, V);
            TreeNode left = nodes[0];//left=5
            TreeNode right = nodes[1];//right=6
            root.right=left;//4.right=5连上
            return new TreeNode[]{root, right};//返回「4,6」bingo
        }
    }

726. Number of Atoms
Input: formula = “K4(ON(SO3)2)2”
Output: “K4N2O14S4”
key insights:
用指针从左到右读取,
看到"(“如何处理?
看到”)"如何处理?
看到字母/数字如何处理?

使用stack分层。例如K4(ON(SO3)2)2
先加K4这一层的TreeMap
再加ON这一层的TreeMap
再加SO3这一层的TreeMap

看到")“时,每次pop stack顶端的TreeMap,乘以”)"外的数。
K4(ON(SO3)2)2 --> K4(N1S2O7)2 --> K4N2O14S4
一层一层,从内部往外剥离/数个数。

class Solution {
    public String countOfAtoms(String formula) {
    
        int N=formula.length();
        
        Stack<Map<String,Integer>> stack=new Stack();
        stack.push(new TreeMap());//know the alphabatic order of chars:abcdef...
       
        for(int i=0;i<N;){
            if(formula.charAt(i)=='('){//入栈,new TreeMap()
                //Mg3(H2O)2O2
                //         i
                //        is
                stack.push(new TreeMap());
                i++;
            }else if(formula.charAt(i)==')'){ //乘法
                Map<String,Integer> top=stack.pop();
                i++; //跳过)
                int iStart=i; //重新开始
                int multiplicity = 1;
                while(i<N && Character.isDigit(formula.charAt(i))){ //碰到数字,跳过
                    i++;
                }
                if(i>iStart){
                    multiplicity=Integer.parseInt(formula.substring(iStart,i));
                }
                for(String c:top.keySet()){
                    int v=top.get(c);//H:2,O:1,  出了(,就做乘法
                    stack.peek().put(c,stack.peek().getOrDefault(c,0)+v* multiplicity);
                }
            }else{//没有括号,简单 加法
                int iStart=i;
                i++;
                 while(i<N && Character.isLowerCase(formula.charAt(i))){ //碰到数字,跳过
                    i++;
                }
                String name=formula.substring(iStart,i);//得到元素名
                iStart=i;
                while(i<N && Character.isDigit(formula.charAt(i))){ //碰到数字,跳过
                    i++;
                }
                //可能有多位数 H12O6
                int multiplicity =i>iStart ?Integer.parseInt(formula.substring(iStart,i)):1;
                stack.peek().put(name,stack.peek().getOrDefault(name,0)+ multiplicity);//
            }
        }
        
        StringBuilder sb=new StringBuilder();
        for(String name:stack.peek().keySet()){
            sb.append(name);
            int multiplicity=stack.peek().get(name);
            if(multiplicity>1){
                sb.append(""+multiplicity);
            }
        }
        return sb.toString();
    }
}

794. Valid Tic-Tac-Toe State
key insights:

Ad-Hoc专门方法 :
这个解法,和recursion没什么关系呀。

class Solution {
    public boolean validTicTacToe(String[] board) {
          /*
            x先下,数量不相等时,必须是x>o,o>x就是无效的。
            数量相等时,如果已经又三个连成一条,就是无效的。false
           */  
        int xcount=0;
        int ocount=0;
        for(String s:board){
            for(int i=0;i<3;i++){
                if(s.charAt(i)=='X') xcount++;
                if(s.charAt(i)=='O') ocount++;
            }
        }
        //if xcount!=ocount, it must be xcount==ocount+1
        if(xcount!=ocount && xcount!=ocount+1) return false;
        //when x win, the xcount==ocount+1
        if(win(board,'X') && xcount!=ocount+1) return false;
         //when o win, the xcount==ocount
        if(win(board,'O') && xcount!=ocount) return false;
        return true;
    }
    //player's turn, player win
    boolean win(String[] B,char P){
        //8 ways to win
        for(int i=0;i<3;i++){
            //in same row
            if(P==B[i].charAt(0) && P==B[i].charAt(1) && P==B[i].charAt(2)){
                 return true;
             }
            //in same col
            if(P==B[0].charAt(i) && P==B[1].charAt(i) && P==B[2].charAt(i)){
                 return true;
             }
            //in diagnals
            if(P==B[0].charAt(0) && P==B[1].charAt(1) && P==B[2].charAt(2) ){
                  return true;
             }
            if(P==B[0].charAt(2) && P==B[1].charAt(1) && P==B[2].charAt(0) ){
                  return true;
             }
        }
        return false;
    }
}

添加链接描述
key insights:

在这里插入代码片
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值