LeetCode 第117题:填充每个节点的下一个右侧节点指针 II
题目描述
给定一个二叉树:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL
。
初始状态下,所有 next 指针都被设置为 NULL
。
难度
中等
题目链接
示例
示例 1:
输入:root = [1,2,3,4,5,null,7]
输出:[1,#,2,3,#,4,5,7,#]
解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化输出按层序遍历顺序(由 next 指针连接),'#' 表示每层的末尾。
提示
- 树中的节点数小于
6000
-100 <= node.val <= 100
解题思路
方法一:层序遍历(BFS)
与第116题类似,我们可以使用层序遍历(广度优先搜索)来解决这个问题。不同的是,这里的二叉树不一定是完美二叉树,可能有节点缺失。
关键点:
- 使用队列进行层序遍历
- 对于每一层,依次连接节点的next指针
- 每层结束时,最后一个节点的next指针设为NULL
具体步骤:
- 如果根节点为空,直接返回
- 初始化队列,将根节点入队
- 当队列不为空时,执行以下操作:
a. 获取当前层的节点数量
b. 遍历当前层的所有节点
c. 对于每个节点,如果它不是当前层的最后一个节点,将其next指针指向队列中的下一个节点
d. 将当前节点的左右子节点(如果存在)入队 - 返回根节点
时间复杂度:O(n),其中n是树中节点的数量,每个节点只会被访问一次
空间复杂度:O(n),队列中最多存储n/2个节点(最后一层的节点数量)
方法二:使用已建立的next指针(常数空间)
虽然这道题中的二叉树不是完美二叉树,但我们仍然可以利用已经建立好的next指针来实现常数空间的解法。
关键点:
- 使用dummy节点作为每一层的起始节点
- 使用prev指针在当前层建立next连接
- 使用curr指针遍历上一层节点
具体步骤:
- 如果根节点为空,直接返回
- 初始化curr指针指向根节点
- 当curr不为空时,执行以下操作:
a. 初始化dummy节点作为下一层的哑节点,prev指向dummy
b. 当curr不为空时:
i. 如果curr的左子节点不为空,将prev的next指针指向curr的左子节点,并将prev移动到curr的左子节点
ii. 如果curr的右子节点不为空,将prev的next指针指向curr的右子节点,并将prev移动到curr的右子节点
iii. 将curr指针移动到next节点
c. 将curr指针移动到下一层的起始节点(即dummy.next) - 返回根节点
时间复杂度:O(n),每个节点只会被访问一次
空间复杂度:O(1),只使用了常数额外空间
图解思路
方法一:层序遍历过程
层数 | 队列状态 | 操作 | 结果 |
---|---|---|---|
1 | [1] | 1.next = NULL | 1 -> NULL |
2 | [2, 3] | 2.next = 3, 3.next = NULL | 2 -> 3 -> NULL |
3 | [4, 5, 7] | 4.next = 5, 5.next = 7, 7.next = NULL | 4 -> 5 -> 7 -> NULL |
方法二:使用已建立的next指针过程
当前节点 | 操作 | 结果 |
---|---|---|
1 | dummy.next = 1.left(2), prev = 2, prev.next = 1.right(3), prev = 3 | 2 -> 3 -> NULL |
2 | dummy.next = 2.left(4), prev = 4, prev.next = 2.right(5), prev = 5 | 4 -> 5 |
3 | prev.next = 3.right(7), prev = 7 | 4 -> 5 -> 7 -> NULL |
代码实现
C# 实现
/*
// Definition for a Node.
public class Node {
public int val;
public Node left;
public Node right;
public Node next;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, Node _left, Node _right, Node _next) {
val = _val;
left = _left;
right = _right;
next = _next;
}
}
*/
public class Solution {
// 方法一:层序遍历(BFS)
public Node Connect(Node root) {
if (root == null) {
return null;
}
Queue<Node> queue = new Queue<Node>();
queue.Enqueue(root);
while (queue.Count > 0) {
int size = queue.Count;
for (int i = 0; i < size; i++) {
Node node = queue.Dequeue();
// 如果不是当前层的最后一个节点,设置next指针
if (i < size - 1) {
node.next = queue.Peek();
}
// 将子节点加入队列
if (node.left != null) {
queue.Enqueue(node.left);
}
if (node.right != null) {
queue.Enqueue(node.right);
}
}
}
return root;
}
// 方法二:使用已建立的next指针(常数空间)
public Node ConnectOptimized(Node root) {
if (root == null) {
return null;
}
Node curr = root;
while (curr != null) {
// 创建dummy节点作为下一层的起始节点
Node dummy = new Node(0);
Node prev = dummy;
// 遍历当前层的节点
while (curr != null) {
// 处理左子节点
if (curr.left != null) {
prev.next = curr.left;
prev = prev.next;
}
// 处理右子节点
if (curr.right != null) {
prev.next = curr.right;
prev = prev.next;
}
// 移动到当前层的下一个节点
curr = curr.next;
}
// 移动到下一层的起始节点
curr = dummy.next;
}
return root;
}
}
Python 实现
"""
# Definition for a Node.
class Node:
def __init__(self, val: int = 0, left: 'Node' = None, right: 'Node' = None, next: 'Node' = None):
self.val = val
self.left = left
self.right = right
self.next = next
"""
class Solution:
# 方法一:层序遍历(BFS)
def connect(self, root: 'Node') -> 'Node':
if not root:
return None
queue = collections.deque([root])
while queue:
size = len(queue)
for i in range(size):
node = queue.popleft()
# 如果不是当前层的最后一个节点,设置next指针
if i < size - 1:
node.next = queue[0]
# 将子节点加入队列
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return root
# 方法二:使用已建立的next指针(常数空间)
def connectOptimized(self, root: 'Node') -> 'Node':
if not root:
return None
curr = root
while curr:
# 创建dummy节点作为下一层的起始节点
dummy = Node(0)
prev = dummy
# 遍历当前层的节点
while curr:
# 处理左子节点
if curr.left:
prev.next = curr.left
prev = prev.next
# 处理右子节点
if curr.right:
prev.next = curr.right
prev = prev.next
# 移动到当前层的下一个节点
curr = curr.next
# 移动到下一层的起始节点
curr = dummy.next
return root
C++ 实现
/*
// Definition for a Node.
class Node {
public:
int val;
Node* left;
Node* right;
Node* next;
Node() : val(0), left(NULL), right(NULL), next(NULL) {}
Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}
Node(int _val, Node* _left, Node* _right, Node* _next)
: val(_val), left(_left), right(_right), next(_next) {}
};
*/
class Solution {
public:
// 方法一:层序遍历(BFS)
Node* connect(Node* root) {
if (root == nullptr) {
return nullptr;
}
queue<Node*> q;
q.push(root);
while (!q.empty()) {
int size = q.size();
for (int i = 0; i < size; i++) {
Node* node = q.front();
q.pop();
// 如果不是当前层的最后一个节点,设置next指针
if (i < size - 1) {
node->next = q.front();
}
// 将子节点加入队列
if (node->left != nullptr) {
q.push(node->left);
}
if (node->right != nullptr) {
q.push(node->right);
}
}
}
return root;
}
// 方法二:使用已建立的next指针(常数空间)
Node* connectOptimized(Node* root) {
if (root == nullptr) {
return nullptr;
}
Node* curr = root;
while (curr != nullptr) {
// 创建dummy节点作为下一层的起始节点
Node* dummy = new Node(0);
Node* prev = dummy;
// 遍历当前层的节点
while (curr != nullptr) {
// 处理左子节点
if (curr->left != nullptr) {
prev->next = curr->left;
prev = prev->next;
}
// 处理右子节点
if (curr->right != nullptr) {
prev->next = curr->right;
prev = prev->next;
}
// 移动到当前层的下一个节点
curr = curr->next;
}
// 移动到下一层的起始节点
curr = dummy->next;
// 释放dummy节点
delete dummy;
}
return root;
}
};
执行结果
C# 实现
- 执行用时:92 ms
- 内存消耗:39.8 MB
Python 实现
- 执行用时:48 ms
- 内存消耗:16.2 MB
C++ 实现
- 执行用时:12 ms
- 内存消耗:17.4 MB
性能对比
语言 | 执行用时 | 内存消耗 | 特点 |
---|---|---|---|
C# | 92 ms | 39.8 MB | 代码结构清晰,但性能较慢 |
Python | 48 ms | 16.2 MB | 代码简洁,性能适中 |
C++ | 12 ms | 17.4 MB | 执行速度最快,内存占用适中 |
代码亮点
- 🎯 方法二使用dummy节点巧妙地处理了下一层的起始节点问题
- 💡 使用prev指针在当前层建立next连接,避免了使用队列
- 🔍 正确处理了节点缺失的情况,适用于任意二叉树
- 🎨 两种方法各有优势,BFS代码简单直观,优化方法空间效率高
常见错误分析
- 🚫 没有正确处理节点缺失的情况,导致空指针异常
- 🚫 在方法二中,没有正确维护prev指针,导致next连接错误
- 🚫 忘记更新curr指针到下一层的起始节点
- 🚫 在C++实现中,没有释放dummy节点,导致内存泄漏
解法对比
解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
层序遍历(BFS) | O(n) | O(n) | 实现简单,直观易懂 | 需要额外空间存储队列 |
使用next指针 | O(n) | O(1) | 空间效率高 | 实现稍复杂 |