【基础算法】剑指 Offer 热门「DFS & BFS」相关合集

937 阅读4分钟

二叉树

今天将带大家完成 44 道剑指 Offer 的二叉树相关题目。

对于此类题目,都可以运用「DFS」&「BFS」来进行求解,属于 高频 且 简单 类型的题目。


剑指 Offer 32 - III. 从上到下打印二叉树 III

请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。

例如:

给定二叉树: [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回其层次遍历结果:

[
  [3],
  [20,9],
  [15,7]
]

提示:

  • 节点总数 <= 1000
迭代 - BFS

这题相比于前两道二叉树打印题目,增加了打印方向的要求。

BFS 过程中,入队我们可以仍然采用「左子节点优先入队」进行,而在出队构造答案时,我们则要根据当前所在层数来做判别:对于所在层数为偶数(root 节点在第 00 层),我们按照「出队添加到尾部」的方式进行;对于所在层数为奇数,我们按照「出队添加到头部」的方式进行。

为支持「从尾部追加元素」和「从头部追加元素」操作,Java 可使用基于链表的 LinkedList,而 TS 可创建定长数组后通过下标赋值。

其中判断当前所在层数,无须引用额外变量,直接根据当前 ans 的元素大小即可。

Java 代码:

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> ans = new ArrayList<>();
        Deque<TreeNode> d = new ArrayDeque<>();
        if (root != null) d.addLast(root);
        while (!d.isEmpty()) {
            LinkedList<Integer> list = new LinkedList<>();
            int sz = d.size(), dirs = ans.size() % 2;
            while (sz-- > 0) {
                TreeNode t = d.pollFirst();
                if (dirs == 0) list.addLast(t.val);
                else list.addFirst(t.val); 
                if (t.left != null) d.addLast(t.left);
                if (t.right != null) d.addLast(t.right);
            }
            ans.add(list);
        }
        return ans;
    }
}

Typescript 代码:

function levelOrder(root: TreeNode | null): number[][] {
    const ans = new Array<Array<number>>()
    const stk = new Array<TreeNode>()
    let he = 0, ta = 0
    if (root != null) stk[ta++] = root
    while (he < ta) {
        const dirs = ans.length % 2 == 0
        let sz = ta - he, idx = dirs ? 0 : sz - 1
        const temp = new Array<number>(sz)
        while (sz-- > 0) {
            const t = stk[he++]
            temp[idx] = t.val
            idx += dirs ? 1 : -1
            if (t.left != null) stk[ta++] = t.left
            if (t.right != null) stk[ta++] = t.right
        }
        ans.push(temp)
    }
    return ans
};
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

递归 - DFS

递归的实现方式与前两题同理。

不过对于 TS 语言来说,由于 DFS 过程中无法知道当前层有多少节点,因此只能在使用「哈希表」记录每层「从左往右」的方向,然后在构造答案时,运用「双指针」来将奇数层的节点进行翻转。

Java 代码:

class Solution {
    Map<Integer, LinkedList<Integer>> map = new HashMap<>();
    int max = -1;
    public List<List<Integer>> levelOrder(TreeNode root) {
        dfs(root, 0);
        List<List<Integer>> ans = new ArrayList<>();
        for (int i = 0; i <= max; i++) ans.add(map.get(i));
        return ans;
    }
    void dfs(TreeNode root, int depth) {
        if (root == null) return ;
        max = Math.max(max, depth);
        dfs(root.left, depth + 1);
        LinkedList<Integer> list = map.getOrDefault(depth, new LinkedList<>());
        if (depth % 2 == 0) list.addLast(root.val);
        else list.addFirst(root.val);
        map.put(depth, list);
        dfs(root.right, depth + 1);
    }
}

TypeScript 代码:

const map: Map<number, Array<number>> = new Map<number, Array<number>> ()
let max = -1
function levelOrder(root: TreeNode | null): number[][] {
    map.clear()
    max = -1
    dfs(root, 0)
    const ans = new Array<Array<number>>()
    for (let i = 0; i <= max; i++) {
        const temp = map.get(i)
        if (i % 2 == 1) {
            for (let p = 0, q = temp.length - 1; p < q; p++, q--) {
                const c = temp[p]
                temp[p] = temp[q]
                temp[q] = c
            } 
        }
        ans.push(temp)
    }
    return ans
};
function dfs(root: TreeNode | null, depth: number): void {
    if (root == null) return 
    max = Math.max(max, depth)
    dfs(root.left, depth + 1)
    if (!map.has(depth)) map.set(depth, new Array<number>())
    map.get(depth).push(root.val)
    dfs(root.right, depth + 1)
}
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

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

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

叶子节点 是指没有子节点的节点。

示例 1:

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22

输出:[[5,4,11,2],[5,8,4,5]]

示例 2:

输入:root = [1,2,3], targetSum = 5

输出:[]

示例 3:

输入:root = [1,2], targetSum = 0

输出:[]

提示:

  • 树中节点总数在范围 [0,5000][0, 5000]
  • 1000<=Node.val<=1000-1000 <= Node.val <= 1000
  • 1000<=targetSum<=1000-1000 <= targetSum <= 1000
DFS

较为直观的做法是使用 DFS,在 DFS 过程中记录路径以及路径对应的元素和,当出现元素和为 target,且到达了叶子节点,说明找到了一条满足要求的路径,将其加入答案。

使用 DFS 的好处是在记录路径的过程中可以使用「回溯」的方式进行记录及回退,而无须时刻进行路径数组的拷贝。

Java 代码:

class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    int t;
    public List<List<Integer>> pathSum(TreeNode root, int target) {
        t = target;
        dfs(root, 0, new ArrayList<>());
        return ans;
    }
    void dfs(TreeNode root, int cur, List<Integer> list) {
        if (root == null) return ;
        list.add(root.val);
        if (cur + root.val == t && root.left == null && root.right == null) ans.add(new ArrayList<>(list));
        dfs(root.left, cur + root.val, list);
        dfs(root.right, cur + root.val, list);
        list.remove(list.size() - 1);
    }
}
  • 时间复杂度:最坏情况所有路径均为合法路径,复杂度为 O(n×h)O(n \times h)
  • 空间复杂度:最坏情况所有路径均为合法路径,复杂度为 O(n×h)O(n \times h)

BFS

使用 BFS 的话,我们需要封装一个类/结构体 TNode,该结构体存储所对应的原始节点 node,到达 node 所经过的路径 list,以及对应的路径和 tot

由于 BFS 过程并非按照路径进行(即相邻出队的节点并非在同一路径),因此我们每次创建新的 TNode 对象时,需要对路径进行拷贝操作。

Java 代码:

class Solution {
    class Node {
        TreeNode node;
        List<Integer> list;
        int tot;
        Node (TreeNode _node, List<Integer> _list, int _tot) {
            node = _node; list = new ArrayList<>(_list); tot = _tot;
            list.add(node.val); tot += node.val;
        }
    }
    public List<List<Integer>> pathSum(TreeNode root, int target) {
        List<List<Integer>> ans = new ArrayList<>();
        Deque<Node> d = new ArrayDeque<>();
        if (root != null) d.addLast(new Node(root, new ArrayList<>(), 0));
        while (!d.isEmpty()) {
            Node t = d.pollFirst();
            if (t.tot == target && t.node.left == null && t.node.right == null) ans.add(t.list);
            if (t.node.left != null) d.addLast(new Node(t.node.left, t.list, t.tot));
            if (t.node.right != null) d.addLast(new Node(t.node.right, t.list, t.tot));
        }
        return ans;
    }
}

Typescript 代码:

class TNode {
    node: TreeNode
    tot: number
    list: Array<number>
    constructor(node: TreeNode, tot: number, list: Array<number>) {
        this.node = node; this.tot = tot; this.list = list.slice();
        this.list.push(node.val)
        this.tot += node.val
    }
}
function pathSum(root: TreeNode | null, target: number): number[][] {
    const ans = new Array<Array<number>>()
    const stk = new Array<TNode>()
    let he = 0, ta = 0
    if (root != null) stk[ta++] = new TNode(root, 0, new Array<number>())
    while (he < ta) {
        const t = stk[he++]
        if (t.tot == target && t.node.left == null && t.node.right == null) ans.push(t.list)
        if (t.node.left != null) stk[ta++] = new TNode(t.node.left, t.tot, t.list)
        if (t.node.right != null) stk[ta++] = new TNode(t.node.right, t.tot, t.list)
    }
    return ans
};
  • 时间复杂度:最坏情况所有路径均为合法路径,复杂度为 O(n×h)O(n \times h)
  • 空间复杂度:最坏情况所有路径均为合法路径,复杂度为 O(n×h)O(n \times h)

剑指 Offer 32 - I. 从上到下打印二叉树

从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。

例如:

给定二叉树: [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回:

[3,9,20,15,7]

提示:

  • 节点总数 <= 1000
迭代 - BFS

使用「迭代」进行求解是容易的,只需使用常规的 BFS 方法进行层序遍历即可。

Java 代码:

class Solution {
    public int[] levelOrder(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        Deque<TreeNode> d = new ArrayDeque<>();
        if (root != null) d.addLast(root);
        while (!d.isEmpty()) {
            TreeNode t = d.pollFirst();
            list.add(t.val);
            if (t.left != null) d.addLast(t.left);
            if (t.right != null) d.addLast(t.right);
        }
        int n = list.size();
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) ans[i] = list.get(i);
        return ans;
    }
}

TypeScript 代码:

function levelOrder(root: TreeNode | null): number[] {
    let he = 0, ta = 0
    const ans: number[] = new Array<number>()
    const d: TreeNode[] = new Array<TreeNode>()
    if (root != null) d[ta++] = root
    while (he < ta) {
        const t = d[he++]
        ans.push(t.val)
        if (t.left != null) d[ta++] = t.left
        if (t.right != null) d[ta++] = t.right
    }
    return ans
};
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)
递归 - DFS

使用「递归」来进行「层序遍历」虽然不太符合直观印象,但也是可以的。

此时我们需要借助「哈希表」来存储起来每一层的节点情况。

首先我们按照「中序遍历」的方式进行 DFS,同时在 DFS 过程中传递节点所在的深度(root 节点默认在深度最小的第 00 层),每次处理当前节点时,通过哈希表获取所在层的数组,并将当前节点值追加到数组尾部,同时维护一个最大深度 max,在 DFS 完成后,再使用深度范围 [0,max][0, max] 从哈希表中进行构造答案。

Java 代码:

class Solution {
    Map<Integer, List<Integer>> map = new HashMap<>();
    int max = -1, cnt = 0;
    public int[] levelOrder(TreeNode root) {
        dfs(root, 0);
        int[] ans = new int[cnt];
        for (int i = 0, idx = 0; i <= max; i++) {
            for (int x : map.get(i)) ans[idx++] = x;
        }
        return ans;
    }
    void dfs(TreeNode root, int depth) {
        if (root == null) return ;
        max = Math.max(max, depth);
        cnt++;
        dfs(root.left, depth + 1);
        List<Integer> list = map.getOrDefault(depth, new ArrayList<Integer>());
        list.add(root.val);
        map.put(depth, list);
        dfs(root.right, depth + 1);
    }
}
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

剑指 Offer 32 - II. 从上到下打印二叉树 II

从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。

例如: 给定二叉树: [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回其层次遍历结果:

[
  [3],
  [9,20],
  [15,7]
]

提示:

  • 节点总数 <= 1000
迭代 - BFS

这道题是 (题解)剑指 Offer 32 - I. 从上到下打印二叉树 的练习版本。

只需要在每次 BFS 拓展时,将完整的层进行取出,存如独立数组后再加入答案。

Java 代码:

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        Deque<TreeNode> d = new ArrayDeque<>();
        if (root != null) d.addLast(root);
        List<List<Integer>> ans = new ArrayList<>();
        while (!d.isEmpty()) {
            int sz = d.size();
            List<Integer> list = new ArrayList<>();
            while (sz-- > 0) {
                TreeNode t = d.pollFirst();
                list.add(t.val);
                if (t.left != null) d.addLast(t.left);
                if (t.right != null) d.addLast(t.right);
            }
            ans.add(list);
        }
        return ans;
    }
}
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)
递归 - DFS

同理,可以使用「递归」来进行「层序遍历」。

此时我们需要借助「哈希表」来存储起来每一层的节点情况。

首先我们按照「中序遍历」的方式进行 DFS,同时在 DFS 过程中传递节点所在的深度(root 节点默认在深度最小的第 00 层),每次处理当前节点时,通过哈希表获取所在层的数组,并将当前节点值追加到数组尾部,同时维护一个最大深度 max,在 DFS 完成后,再使用深度范围 [0,max][0, max] 从哈希表中进行构造答案。

Java 代码:

class Solution {
    Map<Integer, List<Integer>> map = new HashMap<>();
    int max = -1;
    public List<List<Integer>> levelOrder(TreeNode root) {
        dfs(root, 0);
        List<List<Integer>> ans = new ArrayList<>();
        for (int i = 0; i <= max; i++) ans.add(map.get(i));
        return ans;
    }
    void dfs(TreeNode root, int depth) {
        if (root == null) return ;
        max = Math.max(max, depth);
        dfs(root.left, depth + 1);
        List<Integer> list = map.getOrDefault(depth, new ArrayList<>());
        list.add(root.val);
        map.put(depth, list);
        dfs(root.right, depth + 1);
    }
}
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

总结

综上,无论是 DFS 还是 BFS 都有高度统一的模板,对于 DFS 而言,重点在于如何设计递归函数,对于 BFS 而言,重点在于设计哪些信息进行入队。

两者无论是效率上还是实现上都是较为类似。