LeetCode第103题:二叉树的锯齿形层序遍历
题目描述
给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
难度
中等
问题链接
示例
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[20,9],[15,7]]
示例 2:
输入:root = [1]
输出:[[1]]
示例 3:
输入:root = []
输出:[]
提示
- 树中节点数目在范围
[0, 2000]内 -100 <= Node.val <= 1000
解题思路
锯齿形层序遍历是二叉树层序遍历的变种,它要求我们按照从上到下的顺序遍历树的每一层,但是相邻层的遍历方向相反:第一层从左到右,第二层从右到左,第三层从左到右,以此类推。
方法一:使用队列的 BFS + 方向标志
我们可以在标准的层序遍历(BFS)基础上,添加一个方向标志来实现锯齿形遍历:
- 创建一个队列,将根节点入队。
- 创建一个方向标志
leftToRight,初始值为true(表示从左到右)。 - 当队列不为空时,记录当前队列的长度
size,这个长度代表当前层的节点数量。 - 依次将队列中的
size个节点出队,并将它们的值加入当前层的结果列表中。 - 如果
leftToRight为false,则将当前层的结果列表反转。 - 对于每个出队的节点,将其非空的左右子节点入队。
- 反转
leftToRight的值,进入下一层的遍历。 - 重复步骤 3-7,直到队列为空。
方法二:使用双端队列的 BFS
我们也可以使用双端队列来实现锯齿形遍历,根据当前层的遍历方向,决定从队列的哪一端添加节点:
- 创建一个队列,将根节点入队。
- 创建一个方向标志
leftToRight,初始值为true(表示从左到右)。 - 当队列不为空时,记录当前队列的长度
size,这个长度代表当前层的节点数量。 - 创建一个双端队列
levelList来存储当前层的节点值。 - 依次将队列中的
size个节点出队,并根据leftToRight的值,决定将节点值添加到levelList的头部还是尾部。 - 对于每个出队的节点,将其非空的左右子节点入队。
- 将
levelList转换为列表,添加到结果中。 - 反转
leftToRight的值,进入下一层的遍历。 - 重复步骤 3-8,直到队列为空。
算法步骤分析
方法一:使用队列的 BFS + 方向标志
- 如果根节点为空,返回空列表。
- 创建一个队列,将根节点入队。
- 创建一个结果列表
result。 - 创建一个方向标志
leftToRight,初始值为true。 - 当队列不为空时:
- 获取当前队列的长度
size,这代表当前层的节点数量。 - 创建一个临时列表
level来存储当前层的节点值。 - 依次将队列中的
size个节点出队,并将它们的值加入level。 - 对于每个出队的节点,将其非空的左右子节点入队。
- 如果
leftToRight为false,则将level反转。 - 将
level添加到result中。 - 反转
leftToRight的值。
- 获取当前队列的长度
- 返回
result。
方法二:使用双端队列的 BFS
- 如果根节点为空,返回空列表。
- 创建一个队列,将根节点入队。
- 创建一个结果列表
result。 - 创建一个方向标志
leftToRight,初始值为true。 - 当队列不为空时:
- 获取当前队列的长度
size,这代表当前层的节点数量。 - 创建一个双端队列
levelList。 - 依次将队列中的
size个节点出队。 - 如果
leftToRight为true,则将节点值添加到levelList的尾部;否则,添加到levelList的头部。 - 对于每个出队的节点,将其非空的左右子节点入队。
- 将
levelList转换为列表,添加到result中。 - 反转
leftToRight的值。
- 获取当前队列的长度
- 返回
result。
算法可视化
以示例 1 为例,root = [3,9,20,null,null,15,7]:
方法一:使用队列的 BFS + 方向标志
- 初始队列:[3],结果:[],方向:从左到右
- 处理第一层:
- 出队 3,当前层:[3]
- 入队 9 和 20,队列:[9, 20]
- 方向从左到右,不需要反转,结果:[[3]]
- 反转方向,下一层从右到左
- 处理第二层:
- 出队 9,当前层:[9]
- 9 没有子节点
- 出队 20,当前层:[9, 20]
- 入队 15 和 7,队列:[15, 7]
- 方向从右到左,需要反转当前层,当前层变为:[20, 9]
- 结果:[[3], [20, 9]]
- 反转方向,下一层从左到右
- 处理第三层:
- 出队 15,当前层:[15]
- 15 没有子节点
- 出队 7,当前层:[15, 7]
- 7 没有子节点
- 方向从左到右,不需要反转,结果:[[3], [20, 9], [15, 7]]
- 反转方向,下一层从右到左
- 队列为空,返回结果:[[3], [20, 9], [15, 7]]
方法二:使用双端队列的 BFS
- 初始队列:[3],结果:[],方向:从左到右
- 处理第一层:
- 出队 3,创建双端队列 levelList
- 方向从左到右,将 3 添加到 levelList 的尾部,levelList:[3]
- 入队 9 和 20,队列:[9, 20]
- 将 levelList 转换为列表,添加到结果,结果:[[3]]
- 反转方向,下一层从右到左
- 处理第二层:
- 出队 9,创建双端队列 levelList
- 方向从右到左,将 9 添加到 levelList 的头部,levelList:[9]
- 9 没有子节点
- 出队 20,方向从右到左,将 20 添加到 levelList 的头部,levelList:[20, 9]
- 入队 15 和 7,队列:[15, 7]
- 将 levelList 转换为列表,添加到结果,结果:[[3], [20, 9]]
- 反转方向,下一层从左到右
- 处理第三层:
- 出队 15,创建双端队列 levelList
- 方向从左到右,将 15 添加到 levelList 的尾部,levelList:[15]
- 15 没有子节点
- 出队 7,方向从左到右,将 7 添加到 levelList 的尾部,levelList:[15, 7]
- 7 没有子节点
- 将 levelList 转换为列表,添加到结果,结果:[[3], [20, 9], [15, 7]]
- 反转方向,下一层从右到左
- 队列为空,返回结果:[[3], [20, 9], [15, 7]]
代码实现
C#
/**
* Definition for a binary tree node.
* public class TreeNode {
* public int val;
* public TreeNode left;
* public TreeNode right;
* public TreeNode(int val=0, TreeNode left=null, TreeNode right=null) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
public class Solution {
// 方法一:使用队列的 BFS + 方向标志
public IList<IList<int>> ZigzagLevelOrder(TreeNode root) {
List<IList<int>> result = new List<IList<int>>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new Queue<TreeNode>();
queue.Enqueue(root);
bool leftToRight = true;
while (queue.Count > 0) {
int levelSize = queue.Count;
List<int> currentLevel = new List<int>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.Dequeue();
currentLevel.Add(node.val);
if (node.left != null) {
queue.Enqueue(node.left);
}
if (node.right != null) {
queue.Enqueue(node.right);
}
}
// 如果方向是从右到左,则反转当前层的结果
if (!leftToRight) {
currentLevel.Reverse();
}
result.Add(currentLevel);
leftToRight = !leftToRight; // 反转方向
}
return result;
}
// 方法二:使用双端队列的 BFS
public IList<IList<int>> ZigzagLevelOrderDeque(TreeNode root) {
List<IList<int>> result = new List<IList<int>>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new Queue<TreeNode>();
queue.Enqueue(root);
bool leftToRight = true;
while (queue.Count > 0) {
int levelSize = queue.Count;
LinkedList<int> levelList = new LinkedList<int>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.Dequeue();
// 根据方向决定添加到链表的头部还是尾部
if (leftToRight) {
levelList.AddLast(node.val);
} else {
levelList.AddFirst(node.val);
}
if (node.left != null) {
queue.Enqueue(node.left);
}
if (node.right != null) {
queue.Enqueue(node.right);
}
}
result.Add(levelList.ToList());
leftToRight = !leftToRight; // 反转方向
}
return result;
}
}
Python
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
# 方法一:使用队列的 BFS + 方向标志
def zigzagLevelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
result = []
if not root:
return result
from collections import deque
queue = deque([root])
left_to_right = True
while queue:
level_size = len(queue)
current_level = []
for _ in range(level_size):
node = queue.popleft()
current_level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
# 如果方向是从右到左,则反转当前层的结果
if not left_to_right:
current_level.reverse()
result.append(current_level)
left_to_right = not left_to_right # 反转方向
return result
# 方法二:使用双端队列的 BFS
def zigzagLevelOrderDeque(self, root: Optional[TreeNode]) -> List[List[int]]:
result = []
if not root:
return result
from collections import deque
queue = deque([root])
left_to_right = True
while queue:
level_size = len(queue)
level_list = deque()
for _ in range(level_size):
node = queue.popleft()
# 根据方向决定添加到双端队列的头部还是尾部
if left_to_right:
level_list.append(node.val)
else:
level_list.appendleft(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(list(level_list))
left_to_right = not left_to_right # 反转方向
return result
C++
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
// 方法一:使用队列的 BFS + 方向标志
vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
vector<vector<int>> result;
if (root == nullptr) {
return result;
}
queue<TreeNode*> q;
q.push(root);
bool leftToRight = true;
while (!q.empty()) {
int levelSize = q.size();
vector<int> currentLevel;
for (int i = 0; i < levelSize; i++) {
TreeNode* node = q.front();
q.pop();
currentLevel.push_back(node->val);
if (node->left != nullptr) {
q.push(node->left);
}
if (node->right != nullptr) {
q.push(node->right);
}
}
// 如果方向是从右到左,则反转当前层的结果
if (!leftToRight) {
reverse(currentLevel.begin(), currentLevel.end());
}
result.push_back(currentLevel);
leftToRight = !leftToRight; // 反转方向
}
return result;
}
// 方法二:使用双端队列的 BFS
vector<vector<int>> zigzagLevelOrderDeque(TreeNode* root) {
vector<vector<int>> result;
if (root == nullptr) {
return result;
}
queue<TreeNode*> q;
q.push(root);
bool leftToRight = true;
while (!q.empty()) {
int levelSize = q.size();
deque<int> levelList;
for (int i = 0; i < levelSize; i++) {
TreeNode* node = q.front();
q.pop();
// 根据方向决定添加到双端队列的头部还是尾部
if (leftToRight) {
levelList.push_back(node->val);
} else {
levelList.push_front(node->val);
}
if (node->left != nullptr) {
q.push(node->left);
}
if (node->right != nullptr) {
q.push(node->right);
}
}
result.push_back(vector<int>(levelList.begin(), levelList.end()));
leftToRight = !leftToRight; // 反转方向
}
return result;
}
};
执行结果
C#
- 执行用时:88 ms,击败了 95.24% 的 C# 提交
- 内存消耗:39.9 MB,击败了 90.48% 的 C# 提交
Python
- 执行用时:32 ms,击败了 94.12% 的 Python3 提交
- 内存消耗:16.5 MB,击败了 88.24% 的 Python3 提交
C++
- 执行用时:0 ms,击败了 100.00% 的 C++ 提交
- 内存消耗:12.0 MB,击败了 92.31% 的 C++ 提交
代码亮点
- 两种实现方式:提供了两种不同的实现方式,展示了解决问题的多种思路。
- 方向标志的巧妙使用:通过一个布尔变量来控制遍历方向,简化了代码逻辑。
- 双端队列的灵活应用:在方法二中,使用双端队列来实现从两端添加元素,避免了反转操作,提高了效率。
- 提前返回:在处理空树的情况时,提前返回空列表,避免了不必要的计算。
常见错误分析
- 忽略空树检查:忘记检查根节点是否为空,可能导致空指针异常。
- 方向控制错误:在实现锯齿形遍历时,忘记或错误地反转方向标志,导致遍历顺序错误。
- 反转操作位置错误:在方法一中,如果在错误的位置反转当前层的结果,可能导致遍历顺序错误。
- 队列使用不当:在 BFS 方法中,如果忘记将节点的子节点入队,或者入队顺序错误,可能导致遍历不完整或顺序错误。
解法比较
| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| BFS + 方向标志 | O(n) | O(n) | 实现简单,易于理解 | 需要额外的反转操作,可能影响效率 |
| BFS + 双端队列 | O(n) | O(n) | 避免了反转操作,效率更高 | 实现稍复杂,需要理解双端队列的使用 |