算法训练营07-递归使用练习

递归代码模板


# Python def 

recursion(level, param1, param2, ...):     
    # recursion terminator     终止条件判断
    if level > MAX_LEVEL:        
        # process_result        
        return     

    # process logic in current level     当前递归逻辑处理
    process(level, data...)     

    # drill down       递归调用
    self.recursion(level + 1, p1, ...)    
     
    # reverse the current level status if needed     处理一些状态


实战题目
爬楼梯
括号问题22 重做
二叉树翻转226
二叉搜索树校验98 搜索树是不允许相等的元素存在的
二叉树最大深度104
二叉树最小深度111 深度优先并不好做
二叉树的序列化反序列化297
每日一课
如何优雅地计算斐波那契数列

课后作业
二叉树两个节点的公共祖先236
前序遍历与中序遍历构造二叉树105
n个数中所有可能结合77
没有重复 数字的序列,返回其所有可能的全排列46 这个需要在写一遍,同时需要搞懂回溯法
可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列47 思考重做
最后的三个题,题型不一样哦,
n个数中所有可能结合是c(n,k) 问题,元素的顺序是无关的,可以用嵌套for循环来理解
没有重复 数字的序列这两个是全排列问题,对应的是元素都是一样的,但是排列不一样,所以无法直接用嵌套for循环得到解,需要放大每个位置的可选元素,这样就需要标记数组了。

小结 01

一天做了10个左右的题,头晕脑胀,感觉相似的题目却又不同,所以有些题目做的很费劲,没有很好的处理,做完了效果也不是很好,并不像之前做完一个,都会有一些思路去处理,一些注意事项等等,现在感觉做完了就全凭感觉的样子,还需要冷静思考总结才行。
而且,虽然知道了这些题目都是针对递归的训练题目,但是并不是仅仅这样就够了,还需要注意的是有些题目是比较复杂的,是有逻辑需要抽象处理的,并不是知道用递归就行了,还要注意问题处理的过程,边界等。比如括号的问题,还是要了解有效括号的特性,知道如何迭代才行。
什么是有效的二叉搜索树,二叉搜索树的中序遍历特点是否可以用上等,有很多都是需要画图才能够真正解决的。但是这里条件有限,所以也别太心急,多做几遍就好了,加油。每个题都去思考对应的逻辑,处理逻辑,而不是总想一次带走,有些需要更多的逻辑处理来支撑,分析问题,找到问题的核心逻辑,找到重复点,才有助于解决问题。

同时,通过对下面的二叉树公共祖先的学习,进一步认识到了
状态空间和结果空间的区别
状态空间,是指问题本身可能有哪些情况,比如下面的分析
结果空间则是问题通过当前的解答方式可以得到的答案有哪些,比如括号组合的题,最后需要在解空间中进行过滤,得到全部合法的解,还有就是最大矩形面积,如果按照每个bar的高作为矩形的高进行枚举,则也是构建了一个解空间,在这个解空间中最终搜索只会得到一个最优解

有些问题需要对状态空间更好的划分,才更好得到解空间
而有些问题则不是构建解空间,而是直接求解得到唯一解,并不涉及最优概念,而是一个对错型的题,只有一个解,比如矩形面积,是多个解找最大的(数据线构建解空间再求解的情况),共同父节点则是直接得到最终解(没有解空间的概念)

小结02 二叉树两个节点的公共祖先

/**
     * 这个问题的分析,从网上看的思路,感觉有些很好,这里应该怎样分析
     * 首先是问题的状态空间
     * 1. 两个节点是父子关系
     *      1. 找到的时候肯定是先找到parent,直接返回parent即可
     *
     * 2. 两个节点不是父子关系
     * 在某个子树中没有找到,只需要返回null
     *
     *      1. 在某个节点的左子树和右子树中都找到了,说明当前节点为target,逐层返回之
     *
     *      2. 在某个节点的其中一个子树中找到某一个节点(或则两个),返回chiled返回的节点,即代表找到了至少一个节点,
     * 如果在其他层级的遍历中无法找到节点,则说明这个子树中有两个这个节点,这个返回的非null糅合了多种情况,隐含了树中必然有两个节点的条件
     *
     *
     *
     */

可以得到这个精简的代码

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
            if(root == p || root == q || root == null){
                return root;
            }
            TreeNode left = lowestCommonAncestor(root.left,p,q);
            TreeNode right = lowestCommonAncestor(root.right,p,q);
            if((left != null)&&(right != null)){
                return root;
            }
            if (left != null) {
                return left;
            } else {
                return right;
            }
        }

小结03 可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列47

非常开心,这个题通过自己的探索得到了一个新的解法,而且效率也算中规中矩,60%以上
这个是根据不重复的全排推出来的,在每个迭代中增加了一个map,如果有重复的就不交换位置了

 class Solution {

        List<List<Integer>> res = new LinkedList<>();

        public List<List<Integer>> permuteUnique(int[] nums) {
            List<Integer> numList = new LinkedList<>();
            for (int i : nums) {
                numList.add(i);
            }
            unique(numList, 0, nums.length);
            return res;

        }

        private void unique(List<Integer> numList, int cur, int len) {
            if (cur == len - 1) {
                res.add(new ArrayList<>(numList));
                return;
            }
            // 这里增加了一个map用来判断,就是没法重复用,有点可惜
            Map<Integer, Boolean> dic = new HashMap<>();
            for (int i = cur; i < len; i++) {
                if (dic.get(numList.get(i)) != null) {
                    continue;
                }
                Collections.swap(numList, i, cur);
                unique(numList, cur + 1, len);
                Collections.swap(numList, i, cur);
                dic.put(numList.get(i), Boolean.TRUE);
            }
        }
    }

更好的答案应该是体统了更好的时间复杂度,节约了不少时间,但是思路不太一样

class Solution {
    boolean[] vis;

    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> ans = new ArrayList<List<Integer>>();
        List<Integer> perm = new ArrayList<Integer>();
        vis = new boolean[nums.length];
        Arrays.sort(nums);
        backtrack(nums, ans, 0, perm);
        return ans;
    }

    public void backtrack(int[] nums, List<List<Integer>> ans, int idx, List<Integer> perm) {
        if (idx == nums.length) {
            ans.add(new ArrayList<Integer>(perm));
            return;
        }
        for (int i = 0; i < nums.length; ++i) {
            if (vis[i] || (i > 0 && nums[i] == nums[i - 1] && !vis[i - 1])) {
                continue;
            }
            perm.add(nums[i]);
            vis[i] = true;
            backtrack(nums, ans, idx + 1, perm);
            vis[i] = false;
            perm.remove(idx);
        }
    }
}

04. 括号问题,

最开始看错题目了,当做了一个动态规划的题目,实际上这个就是一个模拟排列组合的题目,不用太害怕,直接模拟这个过程就 ok了,
需要get的逻辑是左括号可以随便添加,右边的括号则不能大于左边括号的个数。
通过迭代模拟这个过程就行了。后面每道题尽量不超过5分钟的思考时间,没有办法写出来就赶紧换,第二天再做一遍就是了。

05.搜索二叉树的check

这个当时没有意思到整体二叉树的特点要求,只注意到了父子节点之间的关系,犯错应该不止一次了。

06.二叉树的最小深度

这个使用广度的话直接搞终止条件为两个子节点都是空就行了,但是如果使用深度的话,使用迭代,需要注意有一个子节点为空的情况,这个时候是没有办法直接使用min进行计算的,这种简单的逻辑要自己尝试推一下,还是可以用5分钟作为边界。

class Solution:
    def minDepth(self, root: TreeNode) -> int:
        if root == None :
            return 0
        if root.left == root.right == None :
            return 1
        left_min = 100000000000
        if root.left != None :
            left_min = min(self.minDepth(root.left),left_min)
        right_min = 100000000000
        if root.right != None :
            right_min = self.minDepth(root.right)
        return min(left_min,right_min)+1

07.n个数中所有可能结合77

这个题可以使用排列组合的逻辑,这个逻辑可能更加具备通用性和适用性,我使用的是模拟for循环的过程来处理,感觉殊途同归,都还算容易理解,只是我的参数判断好像更多一些。

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        self.res = []
        one_res = []
        self.combine_iterator(1,one_res,n,k)
        return self.res
    
    def combine_iterator(self,cur,one_res,n,k):
        if len(one_res) == k :
            one = one_res.copy()
            self.res.append(one)
            return
        if (n-cur+1) < (k-len(one_res)) :
            return 
        self.combine_iterator(cur+1,one_res,n,k)
        one_res.append(cur)
        self.combine_iterator(cur+1,one_res,n,k)
        one_res.pop(len(one_res)-1)





#这里的使用的是for循环的思路,来进行处理,需要注意的是迭代的条件判断,需要非常仔细的check才行
    def combine(self, n: int, k: int) -> List[List[int]]:
        res = []
        one_res = []
        self.combine_iterator(0,1,res,one_res,n,k)
        return res
    
    def combine_iterator(self,cur_k,left,res,one_res,n,k):
        if cur_k == k :
            one = one_res.copy()
            res.append(one)
            return
        for cur in range(left,n+1-(k-cur_k)+1) :
            one_res.append(cur)
            self.combine_iterator(cur_k+1,cur+1,res,one_res,n,k)
            one_res.pop(len(one_res)-1)


08全排列

这个题目说是用了回溯法,可以适当了解一下,但是这个思路有点难以想到,需要注意一下,还有就是这里的参数处理保持了逻辑的简介一致性,避免了特殊判断处理
这个题目的解法可以好好思考,类似于排列组合的过程,每次在剩余的当中选一个,但是还要保持不重不漏,所以用数组来处理,仔细思考这个过程,题解中给的slad看着也很容易理解
这里结合带重复的全排列给出的另一种的思考方式,更加通用一些,就是回溯然后标记


class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        res = []
        visited = [False]*len(nums)
        temp = []
        self.premute_iter(nums,temp,res,visited)
        return res
    def premute_iter(self,nums,temp,res,visited):
        if len(temp) == len(nums) :
            res.append(temp.copy())
            return 
        for i in range(len(nums)):
            if visited[i] :
                continue 
            visited[i] = True
            temp.append(nums[i])
            self.premute_iter(nums,temp,res,visited)
            temp.pop()
            visited[i] = False
            
class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        res = []
        self.premute_iter(0,nums,res)
        return res
    def premute_iter(self,index,nums,res):
        if index == len(nums) :
            res.append(nums.copy())
            return 
        # 避免了特殊判断处理,当index==i的时候多了一次无用的交换
        for i in range(index,len(nums)):
            temp = nums[index]
            nums[index] = nums[i]
            nums[i] = temp 
            self.premute_iter(index+1,nums,res)
            temp = nums[index]
            nums[index] = nums[i]
            nums[i] = temp 

09 全排列带重复数字

两种思路,一种是按照原来的原地全排列,每次选一个数字,通过交换来维持两个集合,每次从剩余的当中选取,加一个dic来判断
还有一种是加一个全局的dic,可能更加节约一些,这种是每次都从0开始进行遍历,感觉思路有些不一样

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        res = []
        one_res = []
        nums.sort()
        self.visit = [False] * len(nums)
        self.per_iter(0,nums,res,one_res)
        return res

    def per_iter(self,index,nums,res,one_res) :
        if index == len(nums) :
            res.append(one_res.copy())
            return 
        for i in range(0,len(nums)):
            # 同一个位置选数字的行为中不能重复
            if self.visit[i] or (i>0 and nums[i]== nums[i-1] and (not self.visit[i-1])):
                continue
            self.visit[i] = True
            one_res.append(nums[i])
            self.per_iter(index+1,nums,res,one_res)
            one_res.pop(len(one_res)-1)
            self.visit[i] = False



    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        res = []
        self.per_iter(0,nums,res)
        return res

    def per_iter(self,index,nums,res) :
        if index == len(nums) :
            res.append(nums.copy())
            return 
        dic = set()
        for i in range(index,len(nums)) :
            if not (nums[i] in dic) :
                dic.add(nums[i])
                nums[i],nums[index] = nums[index],nums[i]
                self.per_iter(index+1,nums,res)
                nums[i],nums[index] = nums[index],nums[i]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值