同一棵树,访问顺序可以完全不同——前序、中序、后序三种递归遍历,加上借助队列的层序遍历。
数组和链表是"一条线",从头走到尾顺序是唯一的。但树是"一对多"的结构——站在某个节点上,下一步是先往左子节点走,还是先往右子节点走,还是先处理完自己再说?不同的选择顺序,会得到完全不同的访问序列。二叉树的遍历,说的就是按什么顺序访问树上的每一个节点,能且仅能访问一次。
本节还是用 11.1 节那棵熟悉的示例树:
前序、中序、后序这三种遍历,都是先处理左子树,再处理右子树(这一点是共同的),唯一的区别是"访问根节点"这个动作插在哪一步:
1 2 4 5 3 6——规律就是"先访问自己,再左子树,再右子树"。4 2 5 1 6 3——规律就是"先左子树,再自己,再右子树"。4 5 2 6 3 1——规律就是"先左子树,再右子树,最后才轮到自己"。这三种遍历的代码结构完全一样,都是"处理左子树、处理右子树、访问根节点"这三个动作的排列组合——写成递归函数后,唯一的区别就是"访问根节点"这一行放在哪个位置:
| 1 | void PreOrder(TreeNode* root) // 前序:根→左→右 |
| 2 | { |
| 3 | if (root == nullptr) return; // 递归终止:空节点直接返回 |
| 4 | cout << root->val << " "; // ① 先访问根 |
| 5 | PreOrder(root->left); // ② 再递归左子树 |
| 6 | PreOrder(root->right); // ③ 再递归右子树 |
| 7 | } |
| 8 | |
| 9 | void InOrder(TreeNode* root) // 中序:左→根→右 |
| 10 | { |
| 11 | if (root == nullptr) return; |
| 12 | InOrder(root->left); // ① 先递归左子树 |
| 13 | cout << root->val << " "; // ② 再访问根 |
| 14 | InOrder(root->right); // ③ 再递归右子树 |
| 15 | } |
| 16 | |
| 17 | void PostOrder(TreeNode* root) // 后序:左→右→根 |
| 18 | { |
| 19 | if (root == nullptr) return; |
| 20 | PostOrder(root->left); // ① 先递归左子树 |
| 21 | PostOrder(root->right); // ② 再递归右子树 |
| 22 | cout << root->val << " "; // ③ 最后访问根 |
| 23 | } |
if (root == nullptr) return;——遇到空节点(比如访问某个只有左子节点的节点的右子节点)就直接返回,不做任何事。这一句保证了递归不会无限往下走,是三种遍历共同的"地基"。前中后序都是"一条路走到底"的思路——一头扎进左子树的最深处,处理完了才往回走。但有时候我们想要的是"一层一层"地访问:先访问根节点,再访问第二层的所有节点,再访问第三层……这种"一条路走到底"和"一层一层扩展"的区别,后面的章节会给它们专门起名字,分别叫 DFS(深度优先)和 BFS(广度优先)——这里不用急着记名字,只要先体会到这是两种完全不同的访问方式就够了。
| 1 | void LevelOrder(TreeNode* root) |
| 2 | { |
| 3 | if (root == nullptr) return; // 空树直接返回 |
| 4 | |
| 5 | queue<TreeNode*> q; |
| 6 | q.push(root); // 根节点先入队 |
| 7 | |
| 8 | while (!q.empty()) |
| 9 | { |
| 10 | TreeNode* cur = q.front(); // 取出队头 |
| 11 | q.pop(); |
| 12 | cout << cur->val << " "; // 访问当前节点 |
| 13 | |
| 14 | if (cur->left) q.push(cur->left); // 左子节点存在才入队 |
| 15 | if (cur->right) q.push(cur->right); // 右子节点存在才入队 |
| 16 | } |
| 17 | } |
把本节内容汇总成几条最容易踩坑的规则:
if (root == nullptr) return;,递归会在访问到空节点时继续往下访问 root->left,而空指针没有 left 成员可言,程序会直接崩溃(典型的"空指针解引用")。if (cur->left) q.push(cur->left) 这一步的判断不能省略——如果不判断直接 push(cur->left),遇到不存在的子节点(nullptr)也会入队,后面取出来访问 nullptr->val 同样会崩溃。