题目

标题和出处

标题:填充每个结点的下一个右侧结点指针 II

出处:117. 填充每个结点的下一个右侧结点指针 II

难度

4 级

题目描述

要求

给定一个二叉树,定义如下:

struct Node {
    int val;
    Node *left;
    Node *right;
    Node *next;
}

填充它的每个 next \texttt{next} next 指针,让这个指针指向其下一个右侧结点。如果找不到下一个右侧结点,则将 next \texttt{next} next 指针设置为 NULL \texttt{NULL} NULL

初始状态下,所有 next \texttt{next} next 指针都被设置为 NULL \texttt{NULL} NULL

示例

示例 1:

二叉树题目:填充每个结点的下一个右侧结点指针 II-LMLPHP

输入: root   =   [1,2,3,4,5,null,7] \texttt{root = [1,2,3,4,5,null,7]} root = [1,2,3,4,5,null,7]
输出: [1,#,2,3,#,4,5,7,#] \texttt{[1,\#,2,3,\#,4,5,7,\#]} [1,#,2,3,#,4,5,7,#]
解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next \texttt{next} next 指针,以指向其下一个右侧结点,如图 B 所示。序列化的输出按层序遍历排列,同一层结点由 next \texttt{next} next 指针连接, ‘#’ \texttt{`\#'} ‘#’ 标志着每一层的结束。

示例 2:

输入: root   =   [] \texttt{root = []} root = []
输出: [] \texttt{[]} []

数据范围

  • 树中结点数目在范围 [0,   6000] \texttt{[0, 6000]} [0, 6000]
  • -100 ≤ Node.val ≤ 100 \texttt{-100} \le \texttt{Node.val} \le \texttt{100} -100Node.val100

进阶

  • 你只能使用常量级额外空间。
  • 使用递归解题也符合要求,本题中递归程序占用的栈空间不计入额外的空间。

解法一

思路和算法

由于每个结点的下一个右侧结点一定和该结点位于同一层,因此只要在同一层的结点之间填充下一个右侧结点指针即可。可以使用层序遍历实现。

层序遍历的方法为从根结点开始依次遍历每一层的结点,同一层的结点的遍历顺序为从左到右。在层序遍历的过程中需要区分不同结点所在的层,确保每一轮访问的结点为同一层的全部结点。

使用队列存储待访问的结点,初始时将根结点入队列。每一轮访问结点之前首先得到队列内的元素个数,然后访问这些结点,并将这些结点的非空子结点入队列。该做法可以确保每一轮访问的结点为同一层的全部结点。

每次访问结点时,将待访问的结点出队列。如果当前访问的结点不是当前层的最后一个结点,则此时的队首元素即为当前结点的下一个右侧结点(注意当前结点已经出队列),将当前结点的下一个右侧结点指针指向队首元素。

遍历结束之后返回根结点,此时二叉树中每个结点的下一个右侧结点指针都被填充。

代码

class Solution {
    public Node connect(Node root) {
        if (root == null) {
            return root;
        }
        Queue<Node> queue = new ArrayDeque<Node>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                Node node = queue.poll();
                if (i < size - 1) {
                    node.next = queue.peek();
                }
                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }
        }
        return root;
    }
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。每个结点都被访问一次。

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。空间复杂度主要是队列空间,队列内元素个数不超过 n n n

解法二

思路和算法

也可以使用深度优先搜索填充每个结点的下一个右侧结点指针。具体做法是使用前序遍历,依次访问二叉树的根结点、左子树和右子树,由于前序遍历满足同一层结点被访问的顺序为从左到右,因此可以在前序遍历过程中填充每个结点的下一个右侧结点指针。由于每个结点的下一个右侧结点一定和该结点位于同一层,因此需要使用哈希表存储每一层已经访问过的的最右侧结点。

前序遍历过程中,对于每个结点,执行如下操作。

  1. 如果当前结点所在层已经有被访问过的结点,则从哈希表中得到该层已经访问过的最右侧结点,并将已经访问过的最右侧结点的下一个右侧结点指针指向当前结点。如果当前结点所在层没有被访问过的结点,则跳过这一步。

  2. 将哈希表中当前结点所在层已经访问过的最右侧结点设为当前结点。

遍历结束之后返回根结点,此时二叉树中每个结点的下一个右侧结点指针都被填充。

代码

class Solution {
    public Node connect(Node root) {
        Map<Integer, Node> rightmost = new HashMap<Integer, Node>();
        Deque<Node> nodeStack = new ArrayDeque<Node>();
        Deque<Integer> depthStack = new ArrayDeque<Integer>();
        Node node = root;
        int depth = 0;
        while (!nodeStack.isEmpty() || node != null) {
            while (node != null) {
                if (rightmost.containsKey(depth)) {
                    rightmost.get(depth).next = node;
                }
                rightmost.put(depth, node);
                nodeStack.push(node);
                depthStack.push(depth);
                node = node.left;
                depth++;
            }
            node = nodeStack.pop().right;
            depth = depthStack.pop() + 1;
        }
        return root;
    }
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。每个结点都被访问一次。

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。空间复杂度主要是哈希表和栈空间,取决于二叉树的高度,最坏情况下二叉树的高度是 O ( n ) O(n) O(n)

解法三

思路和算法

为了将空间复杂度降到 O ( 1 ) O(1) O(1),需要使用其他方法遍历二叉树并填充每个结点的下一个右侧结点指针。

如果根结点为空或者根结点为叶结点,则不需要填充任何结点的下一个右侧结点指针,直接返回原始二叉树。以下只考虑根结点不为空且不为叶结点的情况。

规定根结点所在层是第 0 0 0 层,从根结点开始填充每一层的下一个右侧结点指针。由于根结点所在层没有其他结点,因此不需要填充根结点的下一个右侧结点指针。

对于 k ≥ 0 k \ge 0 k0,当第 k k k 层的全部结点的下一个右侧结点指针填充完毕时,第 k k k 层的全部结点组成一个单向链表,每个结点的下一个右侧结点指针指向单向链表中的后一个结点。

当第 k k k 层有结点时,第 k k k 层的全部结点的子结点即为第 k + 1 k + 1 k+1 层的全部结点。从左到右依次遍历第 k k k 层的每个结点,对于每个结点依次得到其左子结点和右子结点,则遍历子结点的顺序为从左到右遍历第 k + 1 k + 1 k+1 层的所有结点的顺序,遍历子结点的过程中即可填充第 k + 1 k + 1 k+1 层的所有结点的下一个右侧结点指针。

填充下一个右侧结点指针需要维护两项信息,一是同一层结点的最左侧结点,二是上一个被访问的结点。这两项信息都可以在遍历过程中完成,具体做法如下。

  1. 将第 k + 1 k + 1 k+1 层的第一个被访问的结点设为第 k + 1 k + 1 k+1 层结点的最左侧结点。

  2. 如果上一个被访问的子结点不为空,则将上一个被访问的子结点的下一个右侧结点指针指向当前子结点。

  3. 将当前子结点赋给上一个被访问的子结点。

当第 k k k 层遍历完毕且第 k + 1 k + 1 k+1 层的下一个右侧结点指针填充完毕时,移动到第 k + 1 k + 1 k+1 层的最左侧结点,继续填充其余层的下一个右侧结点指针。当遍历到的层没有结点时,结束操作,此时所有结点的下一个右侧结点指针填充完毕。

代码

class Solution {
    Node nextStart;
    Node prev;

    public Node connect(Node root) {
        if (root == null) {
            return root;
        }
        Node start = root;
        while (start != null) {
            Node node = start;
            nextStart = null;
            prev = null;
            while (node != null) {
                update(node.left);
                update(node.right);
                node = node.next;
            }
            start = nextStart;
        }
        return root;
    }

    public void update(Node curr) {
        if (curr == null) {
            return;
        }
        if (nextStart == null) {
            nextStart = curr;
        }
        if (prev != null) {
            prev.next = curr;
        }
        prev = curr;
    }
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。每个结点都被访问一次。

  • 空间复杂度: O ( 1 ) O(1) O(1)

09-07 20:09