《剑指offer》 -day15- 搜索与回溯算法(中等)【DFS】

剑指 Offer 34. 二叉树中和为某一值的路径

题目描述

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

提示:

  • 树中节点总数在范围 [0, 5000] 内
  • − 1000 < = N o d e . v a l < = 1000 -1000 <= Node.val <= 1000 1000<=Node.val<=1000
  • − 1000 < = t a r g e t S u m < = 1000 -1000 <= targetSum <= 1000 1000<=targetSum<=1000

DFS 预防污染-写法1

思路

  1. 确定递归参数、返回类型
    需要遍历整颗树,所以不需要返回类型。
    dfs(TreeNode root, int target, List<Integer> cntList)
    
  2. 终止条件
    当前节点为 叶子节点(root.left == null && root.right == null),终止遍历:
    • 若总和为 t a r g e t target target,则收集当前路径;
    • 否则,不作收集,继续迭代。
  3. 单层逻辑
    分别递归遍历 左、右子树。

Note:

  • 这里采用的是“避免污染”的递归写法,即 不允许 r o o t = = n u l l root == null root==null 的节点 进入 dfs。
  • 这种方法,收集单条路径结果 (cntList.add(root.val))放在 dfs 的最开头,而不是和 左、右子树 dfs 的上面。
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    List<List<Integer>> res = new ArrayList<>();

    public List<List<Integer>> pathSum(TreeNode root, int target) {
        if (root == null) return res;
        dfs(root, target - root.val, new ArrayList<>());
        return res;
    }

    public void dfs(TreeNode root, int target, List<Integer> cntList) {
        cntList.add(root.val); // 保证root为null不会进入dfs
        // System.out.println("cntList = " + cntList);
        // 终止条件(遇到叶子节点,则终止)
        if (root.left == null && root.right == null) {
            if (target == 0) {
                res.add(new ArrayList<>(cntList));
            }
            return;
        }
        // 左
        if (root.left != null) {
            dfs(root.left, target - root.left.val, cntList);
            cntList.remove(cntList.size() - 1); // 回溯
        }
        // 右
        if (root.right != null) {
            dfs(root.right, target - root.right.val, cntList);
            cntList.remove(cntList.size() - 1); // 回溯
        }
    }
}
  • 时间复杂度 O ( n ) O(n) O(n) (先序遍历二叉树中所有节点 O ( n ) O(n) O(n),每次dfs时间复杂度为 O ( 1 ) O(1) O(1),所以最终时间复杂度为 O ( 1 ∗ n ) O(1 * n) O(1n)
  • 空间复杂度 O ( n ) O(n) O(n) (树退化为链表时,即递归深度为 O ( n ) O(n) O(n)

dfs

DFS 预防污染-写法2

如果想将 每次收集节点/回溯节点 都和 DFS 放在一起的话,则可以采用如下写法。

  • root == null,则不进入 dfs,直接返回即可;
  • 否则,将 root.val 在进入 dfs 就加入 cnt 中;
  • dfs 终止条件:若当前节点为叶子节点,且 target == root.val,则说明当前路径符合题意,需要收集之;
  • 否则,左子树非空,则 dfs 遍历左子树(此时,收集节点和回溯 都是和 dfs 紧密在一起的);
  • 否则,右子树非空时,同理。
class Solution {
    List<List<Integer>> res = new ArrayList<>();

    public List<List<Integer>> pathSum(TreeNode root, int target) {
        if (root == null) return res;
        List<Integer> cnt = new ArrayList<>();
        cnt.add(root.val); // 避免污染的写法
        dfs(root, target, cnt);
        return res;
    }

    void dfs(TreeNode root, int target, List<Integer> cnt) {
        // 叶子节点(保证null不会进入递归函数dfs中)
        if (root.left == null && root.right == null) {
            // 该路径和为target,则收集
            if (target == root.val) { // 避免污染的写法,所以是 == root.val,而不是 == 0
                res.add(new ArrayList<>(cnt));
            }
            return;
        }
        // 保证null不会进入下层递归dfs
        if (root.left != null) {
            cnt.add(root.left.val);
            dfs(root.left, target - root.val, cnt); // target的回溯暗含在递归中
            cnt.remove(cnt.size() - 1); // 回溯
        }
        if (root.right != null) {
            cnt.add(root.right.val);
            dfs(root.right, target - root.val, cnt);
            cnt.remove(cnt.size() - 1); // 回溯
        }
    }
}
  • 时间复杂度 O ( n ) O(n) O(n) (先序遍历二叉树中所有节点 O ( n ) O(n) O(n),每次dfs时间复杂度为 O ( 1 ) O(1) O(1),所以最终时间复杂度为 O ( 1 ∗ n ) O(1 * n) O(1n)
  • 空间复杂度 O ( n ) O(n) O(n) (树退化为链表时,即递归深度为 O ( n ) O(n) O(n)

在这里插入图片描述

剑指 Offer 36. 二叉搜索树与双向链表

题目描述

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。


DFS + pre 指针 ⭐️

参考

DFS 思路 🤔

  • 使用前驱指针pre,记录中序序列上一个遍历的节点
  • 由于 BST 中序遍历结果是升序的,且题目要求转换后的链表“升序”,所以这里 dfs 采用 “中序遍历
    • 中序遍历左子树
    • 根逻辑处理
      • pre == null,则说明找到二叉树的最左下节点,即转换后链表的头节点,则 head = root;
      • pre != null,则说明当前节点是其前驱节点 pre 的后继节点,则 pre.right = root;
      • 更新当前节点的前驱指针,即 root.left = pre;
        • 注意⚠️:这里和【线索二叉树】不太一样,这里不用判断 root.left 不为空
      • 更新前驱节点 pre
    • 中序遍历右子树

dfs 结束后,此时链表中 leftright 已经更新完事了,但是题目要求的是 “循环链表”,所以还需要将链表的 “首尾相连”,构成 “循环链表”:

  • head.left = pre;
  • pre.right = head;
/*
// Definition for a Node.
class Node {
    public int val;
    public Node left;
    public Node right;

    public Node() {}

    public Node(int _val) {
        val = _val;
    }

    public Node(int _val,Node _left,Node _right) {
        val = _val;
        left = _left;
        right = _right;
    }
};
*/
class Solution {
    // 前驱指针(记录中序序列上一个遍历的节点)
    Node pre = null;
    Node head = null;
    
    public Node treeToDoublyList(Node root) {
        if (root == null) {
            return null;
        }
        dfs(root);
        // 首尾相连,构成循环链表
        head.left = pre;
        pre.right = head;
        return head;
    }

    void dfs(Node root) {
        if (root == null) return;
        // 中序
        dfs(root.left);
        // 根
        root.left = pre; // 和【线索二叉树】不太一样,这里不用判断 root.left 不为空
        if (pre != null) {
        	/** 中序-线索化的逻辑 */
            // if (root.left == null) { // 当前节点左指针为空时,
            //     root.left = pre;
            // }
            // if (pre.right == null) {
            //     pre.right = root;
            // }
            pre.right = root;
        } else { // 最左下,即头节点
            head = root;
        }
        pre = root; // 更新前驱
        dfs(root.right);
    }
}

在这里插入图片描述

  • 时间复杂度 O ( N ) O(N) O(N) (中序遍历需要访问所有节点)
  • 空间复杂度: O ( N ) O(N) O(N) (最差情况下,即树退化为链表时,递归深度达到 N,使用 O(N) 栈空间)

复杂度分析:K佬

剑指 Offer 54. 二叉搜索树的第 k 大节点

题目描述

给定一棵二叉搜索树,请找出其中第k大的节点。

限制:

  • 1 ≤ k ≤ 二叉搜索树元素个数

DFS + 排序

思路

  • 一种通用的做法是,即如果是普通的二叉树时,则可以通过一遍遍历(可以采用任意的遍历方式)将二叉树中所有节点都保存到 res 中;
  • 等二叉树中所有节点收集完毕时,对 res 降序排序,然后获取其第 k 个元素,即为所求。
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    List<Integer> res = new ArrayList<>();
    
    public int kthLargest(TreeNode root, int k) {
        if (root == null) return 0;
        dfs(root);
        // 降序排序
        Collections.sort(res, (o1, o2) -> (o2 - o1));
        System.out.println(res);
        return res.get(k - 1);
    }

    void dfs(TreeNode root) {
        if (root == null) return;
        // 此时遍历方式可以任意,这里以“前序”为例
        res.add(root.val);
        dfs(root.left);
        dfs(root.right);
    }
}

在这里插入图片描述

  • 时间复杂度 O ( n ) O(n) O(n) (**先序**遍历二叉树中所有节点为 O ( n ) O(n) O(n),每次dfs时间复杂度为 O ( 1 ) O(1) O(1),所以最终时间复杂度为 O ( 1 ∗ n ) O(1 * n) O(1n)
  • 空间复杂度 O ( n ) O(n) O(n) (树退化为链表时,即所有节点都挂在left上,此时递归深度为 O ( n ) O(n) O(n)

DFS 反-中序 ⭐️

思路:

  • BST 中序遍历结果是 升序的,本题要找 降序排序的第 k k k 个元素,所以可以 “反-中序”遍历
  • 即,采用 “右根左” 的顺序遍历 BST,取第 k k k 个元素,即可。
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    int res = 0;
    int index = 0;
    
    public int kthLargest(TreeNode root, int k) {
        if (root == null) return 0;
        dfs(root, k);
        return res;
    }

    boolean dfs(TreeNode root, int k) {
        if (root == null) return false;
        // 右左根遍历 --> 保证BST降序
        if (dfs(root.right, k)) { // 找到第k大元素,立即返回
        	return true;
        }
        if (++index == k) {
            res = root.val;
            return true;
        }
        if (dfs(root.left, k)) { // 找到第k大元素,立即返回
        	return true;
        }
        return false;
    }
}
  • 时间复杂度 O ( n ) O(n) O(n) (最坏情况,即所有节点都挂在right上,且 k = = 1 k == 1 k==1时,则需要 “中序” 遍历所有节点 O ( n ) O(n) O(n),每次dfs时间复杂度为 O ( 1 ) O(1) O(1),所以最终时间复杂度为 O ( 1 ∗ n ) O(1 * n) O(1n)
  • 空间复杂度 O ( n ) O(n) O(n) (树退化为链表时,即所有节点都挂在right上,此时递归深度为 O ( n ) O(n) O(n)

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值