在第 4.3.3 节中,我们学习了二叉树的广度优先搜索(BFS)层序遍历。
题目
给定一棵二叉树,分别输出其标准层序遍历和锯齿形层序遍历(之字形)。
- 标准层序:自顶向下,每层从左向右;
- 锯齿形层序:第 0 层从左向右,第 1 层从右向左,第 2 层从左向右,依次类推交替翻转。
输入格式
- 一行以空格分隔的若干个 token,表示二叉树的层序遍历序列。
输出格式
- 第一段输出
LEVEL_ORDER:后跟标准层序遍历,每层占一行; - 第二段输出
ZIGZAG_ORDER:后跟锯齿形层序遍历,每层占一行。
💡 输入提示
使用流分词极简读取;仅在 token != "null" 时调用 std::stoi,防止异常崩溃:
cpp
std::vector<std::string> tokens;
std::string token;
while (std::cin >> token) tokens.push_back(token);样例
样例输入 1
input
3 9 20 null null 15 7样例输出 1
output
LEVEL_ORDER:
3
9 20
15 7
ZIGZAG_ORDER:
3
20 9
15 7样例输入 2
input
1 2 3 4 5 6 7样例输出 2
output
LEVEL_ORDER:
1
2 3
4 5 6 7
ZIGZAG_ORDER:
1
3 2
4 5 6 7样例输入 3
input
1样例输出 3
output
LEVEL_ORDER:
1
ZIGZAG_ORDER:
1如何验证
先安装 Node.js、pnpm 和支持 C++17 的编译器。GNU Make 是首选入口,但不是强制依赖。
powershell
# 已进入本 Lab 目录
make doctor
make run
make run CASE=001-sample
make interactive
make scoreWindows 没有安装 Make 时,在仓库根目录使用完全相同的评分内核:
powershell
pnpm lab:doctor -- labs/chapter-04/exercise/E-04-09-binary-tree-level-and-zigzag-order
pnpm lab:run -- labs/chapter-04/exercise/E-04-09-binary-tree-level-and-zigzag-order
pnpm lab:run -- labs/chapter-04/exercise/E-04-09-binary-tree-level-and-zigzag-order --case 001-sample
pnpm lab:score -- labs/chapter-04/exercise/E-04-09-binary-tree-level-and-zigzag-ordermake run 在答案尚未全对时仍正常返回,避免 Make 把学习结果显示成工具故障;make score 是严格入口,只有 100 分才返回成功。标准输出参与判题,调试信息请写入标准错误。
题解
点击查看题解
核心思路
二叉树锯齿形层序遍历的本质是:在广度优先搜索(BFS)按层推进的同时,控制每层元素的存储次序。本题有两种优雅的实现方案:
方案一:双端队列法(std::deque)
- 核心机制:利用
std::deque允许在两端均以 时间插入的物理特性; - 遍历流程:
- 偶数层(从左到右):直接向队尾推进
deque.push_back(node->val); - 奇数层(从右到左):直接向队头推入
deque.push_front(node->val);
- 偶数层(从左到右):直接向队尾推进
- 优势:利用了deque本身两端pop和push的特性,省去了翻转或下标处理的操作。
方案二:定长 Vector 镜像坐标填充法(进阶)
- 核心机制:在进入每层循环时,当前层的节点总数已知为
sz = q.size(); - 遍历流程:
- 一次性精准开辟大小为
sz的连续内存:std::vector<int> row(sz); - 若当前层从左向右,填入下标
row[i] = node->val; - 若当前层从右向左,直接从后向前填入镜像下标
row[sz - 1 - i] = node->val;
- 一次性精准开辟大小为
- 优势:单块物理内存连续分配,CPU L1/L2 缓存行预取效率极高,无多次指针解引用或额外开辟分段 buffer 的常数开销。
复杂度分析与性能对比
| 解法对比 | 传统解法 (vector + reverse) | 方案一:双端队列法 (std::deque) | 方案二:镜像下标法 (vector 镜像索引) |
|---|---|---|---|
| 时间复杂度 | |||
| 空间复杂度 | |||
| 缓存局部性 | 高(但在按值拷贝时开销大) | 中等(跨 chunk 访问时有两次解引用) | 极高(100% 缓存行顺序命中) |
| 设计契约 | 需后置倒序处理 | 算法语义最纯粹,吻合题意 | 理论执行速度更快 |
点击查看参考代码
方案一:双端队列法(std::deque 推荐面试解)
利用 std::deque 的双端插入能力,偶数层 push_back,奇数层 push_front,无需事后倒序翻转。
cpp
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <deque>
struct TreeNode {
int val = 0;
TreeNode* left = nullptr;
TreeNode* right = nullptr;
TreeNode() = default;
TreeNode(int x) : val(x) {}
TreeNode(int x, TreeNode* left, TreeNode* right) : val(x), left(left), right(right) {}
};
TreeNode* buildTree(const std::vector<std::string>& tokens) {
if (tokens.empty() || tokens[0] == "null") return nullptr;
TreeNode* root = new TreeNode(std::stoi(tokens[0]));
std::queue<TreeNode*> q;
q.push(root);
size_t i = 1;
while (!q.empty() && i < tokens.size()) {
TreeNode* curr = q.front();
q.pop();
if (i < tokens.size()) {
if (tokens[i] != "null") {
curr->left = new TreeNode(std::stoi(tokens[i]));
q.push(curr->left);
}
i++;
}
if (i < tokens.size()) {
if (tokens[i] != "null") {
curr->right = new TreeNode(std::stoi(tokens[i]));
q.push(curr->right);
}
i++;
}
}
return root;
}
void freeTree(TreeNode* root) {
if (!root) return;
freeTree(root->left);
freeTree(root->right);
delete root;
}
void printLevelAndZigzagDeque(TreeNode* root) {
if (!root) {
std::cout << "LEVEL_ORDER:\nZIGZAG_ORDER:\n";
return;
}
std::vector<std::vector<int>> levelOrder;
std::vector<std::deque<int>> zigzagOrder;
std::queue<TreeNode*> q;
q.push(root);
bool leftToRight = true;
while (!q.empty()) {
size_t sz = q.size();
std::vector<int> curLevel;
std::deque<int> curZigzag;
for (size_t i = 0; i < sz; i++) {
TreeNode* node = q.front();
q.pop();
curLevel.push_back(node->val);
// 偶数层尾插,奇数层头插
if (leftToRight) {
curZigzag.push_back(node->val);
} else {
curZigzag.push_front(node->val);
}
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
levelOrder.push_back(curLevel);
zigzagOrder.push_back(curZigzag);
leftToRight = !leftToRight;
}
// 1. 输出标准层序
std::cout << "LEVEL_ORDER:\n";
for (const auto& lv : levelOrder) {
for (size_t i = 0; i < lv.size(); i++) {
std::cout << (i == 0 ? "" : " ") << lv[i];
}
std::cout << "\n";
}
// 2. 输出锯齿层序
std::cout << "ZIGZAG_ORDER:\n";
for (const auto& row : zigzagOrder) {
for (size_t i = 0; i < row.size(); i++) {
std::cout << (i == 0 ? "" : " ") << row[i];
}
std::cout << "\n";
}
}
int main() {
std::vector<std::string> tokens;
std::string token;
while (std::cin >> token) {
tokens.push_back(token);
}
TreeNode* root = buildTree(tokens);
printLevelAndZigzagDeque(root);
freeTree(root);
return 0;
}solution-deque.cpp1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
方案二:定长 Vector 镜像坐标法(追求极致性能与 CPU 缓存)
预分配确定长度的 vector<int> curZigzag(sz),按方向选择下标 i 或 sz - 1 - i 写入,享受单块连续内存最高缓存命中率。
cpp
#include <iostream>
#include <vector>
#include <string>
#include <queue>
void printLevelAndZigzagMirror(TreeNode* root) {
if (!root) {
std::cout << "LEVEL_ORDER:\nZIGZAG_ORDER:\n";
return;
}
std::vector<std::vector<int>> levelOrder;
std::vector<std::vector<int>> zigzagOrder;
std::queue<TreeNode*> q;
q.push(root);
bool leftToRight = true;
while (!q.empty()) {
size_t sz = q.size();
std::vector<int> curLevel(sz);
std::vector<int> curZigzag(sz); // 精准预分配,零冗余开销
for (size_t i = 0; i < sz; i++) {
TreeNode* node = q.front();
q.pop();
curLevel[i] = node->val;
// 核心:偶数层填在 i,奇数层直接填在镜像位置 sz - 1 - i
size_t zigzagIdx = leftToRight ? i : (sz - 1 - i);
curZigzag[zigzagIdx] = node->val;
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
levelOrder.push_back(curLevel);
zigzagOrder.push_back(curZigzag);
leftToRight = !leftToRight;
}
std::cout << "LEVEL_ORDER:\n";
for (const auto& lv : levelOrder) {
for (size_t i = 0; i < lv.size(); i++) {
std::cout << (i == 0 ? "" : " ") << lv[i];
}
std::cout << "\n";
}
std::cout << "ZIGZAG_ORDER:\n";
for (const auto& row : zigzagOrder) {
for (size_t i = 0; i < row.size(); i++) {
std::cout << (i == 0 ? "" : " ") << row[i];
}
std::cout << "\n";
}
}solution-vector-mirror.cpp1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58