8.31 回溯

本文探讨了如何使用回溯法解决编程问题,包括生成所有不同二叉搜索树的实例,以及找到二叉树中路径和等于目标的路径。通过递归与归并策略,展示了复杂数据结构返回的回溯技巧。

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

在这里更新一些回溯的题目吧

题1

给你一个整数 n ,请你生成并返回所有由 n 个节点组成且节点值从 1 到 n 互不相同的不同 二叉搜索树 。可以按 任意顺序 返回答案。

这道题返回的数据结构是List<TreeNode>,也就是多个二叉树的集合。之前做的回溯一般就是返回一个最好的结果或者是方法数,这次返回的数据结构比较复杂,这里埋了一个伏笔。

这道题解法靠递归。递归思路是:先选择x作为这个树的根节点,然后构造左子树:选择比X小的树作为左根节点;构造右子树:选择比X大的树作为右根节点。递归调用。

class Solution {
    public List<TreeNode> generateTrees(int n) {
        if (n == 0) {
            return new LinkedList<TreeNode>();
        }
        return generateTrees(1, n);
    }

    public List<TreeNode> generateTrees(int start, int end) {
        List<TreeNode> allTrees = new LinkedList<TreeNode>();
        if (start > end) {
            allTrees.add(null);
            return allTrees;
        }

        // 枚举可行根节点
        for (int i = start; i <= end; i++) {
            // 获得所有可行的左子树集合
            List<TreeNode> leftTrees = generateTrees(start, i - 1);
            // 获得所有可行的右子树集合
            List<TreeNode> rightTrees = generateTrees(i + 1, end);
            // 从左子树集合中选出一棵左子树,从右子树集合中选出一棵右子树,拼接到根节点上
            for (TreeNode left : leftTrees) {
                for (TreeNode right : rightTrees) {
                    TreeNode currTree = new TreeNode(i);
                    currTree.left = left;
                    currTree.right = right;
                    allTrees.add(currTree);
                }
            }
        }
        return allTrees;
    }
}

值得一提的是代码的最后一段用到了一个回溯我没见过的归并思路。原因可能在于,方法返回的是树的集合,而每次调用generateTrees返回的也是集合,也就是说,返回的树不止一颗,所以要for循环左选一颗右选一颗。这种归并方法记录一下。

其实正常来说,我认为的归并方法是像这样:

private void generateTrees(int start, int end, List<TreeNode> ans, TreeNode root, int count) {
    if (count == n) {
        //复制当前树并且加到结果中
        TreeNode newRoot = treeCopy(root);
        ans.add(newRoot);
        return;
    }
    .......后面不写了

没有采用这种方法的原因可能是,如果真的不能返回集合,返回void的话,意味着要在每一种情况下实时插入,而且会出现情况重复的情况。详情见:
看第一种回溯法
而且递归结束后还要移除插入的结点(保持原样,懂我意思吧)
注意到这个方法还用了一个值0的哨兵结点。这样不用考虑第一次遍历的时候是空树的情况。(其实也可以手动创建值为1-n的根节点送进递归调用)

题2

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

放到现在来看这题的思路不难,但要返回的是List<List<Integer>>,也就是所有成功的结果。这种的回溯,我当初写的代码如下:

class Solution {
    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        List<List<Integer>> res = new ArrayList<List<Integer>>();
        List<Integer> cur = new ArrayList<Integer>();
        judge(root , targetSum , cur ,res);
        return res;
    }
    void judge(TreeNode root, int target,List<Integer> cur,List<List<Integer>> res){
        if (root == null) return;
        Integer val = root.val;
        cur.add(val);
        if (val == target &&root.left ==null && root.right ==null) {
        	res.add(cur); 
            cur.remove(cur.size() - 1);
            return;
        }
        judge(root.left,target - val,cur,res);
        judge(root.right,target - val,cur,res);
        cur.remove(cur.size() - 1);
    }
}

注意:对于回溯的方法,在遍历之后要执行返回,在这里也就是remove操作。这里还有两个点:
1.对于一些特殊情况(在这题中为找到了路径),它会立刻返回,并不会执行下方的remove,所以我们要在特殊情况后再添加一个remove。
2.list的remove方法有两种形式:一种是删除索引,另外一种是删除list内元素。如果要使用后者方法,注意类型为Integer。

但我提交了代码之后仍是错的,看过正确答案后才知道原来是这句:

res.add(cur);

出了大问题。
正解为:

res.add(new ArrayList(cur)); 

其实读到这里已经感受到了问题。就是list是引用调用的,它现在的值可能是这样,但是之后就不会这样了。因此需要新建一个list类存现在的值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值