Morris后序遍历更难写,因其需在第二次访问节点时逆序输出左子树的“右链”,必须显式翻转并还原单向链表;而前序/中序仅需标记或简单访问,无需链表操作。

为什么 Morris 后序遍历比前序/中序更难写
Morris 遍历的核心是用叶子节点的空右指针“临时挂载”回溯路径,但后序要求左右子树访问完才处理根——这意味着你得在第二次经过某个节点时,**逆序输出从它的左孩子到当前节点的整条“右链”**。这不是简单打个标记就能搞定的事,而是必须显式翻转一段单向链表。
常见错误现象:nullptr 访问、重复访问某节点、漏掉根节点、或输出顺序完全错乱。根本原因往往是没真正理解“翻转右链”的触发时机和边界条件。
- 只有当
cur->left != nullptr且已建立过predecessor->right == cur的回边时,才进入翻转逻辑 - 翻转起点是
cur->left,终点是cur(不含cur自身) - 翻转后要再遍历一遍该链输出,然后还原指针——否则下一轮会断链
关键函数:reverseAndPrint 与指针还原
这个辅助函数不光要翻转,还得边翻转边输出,并在退出前把链表恢复原状。它不是纯工具函数,而是 Morris 后序的“心脏”。
实操建议:
- 输入参数必须是
from(起始节点,即cur->left),不能传cur - 翻转过程中用三指针:
prev、curr、next;每次翻转前先std::cout val - 翻转结束后,
prev指向原链尾(即最右节点),此时需将它的right设为nullptr,否则下次遍历时会误入死循环 - 别忘了在函数末尾把整条链再翻回去——用同样三指针再翻一次即可
示例片段(核心逻辑):
void reverseAndPrint(TreeNode* from) {
TreeNode *prev = nullptr, *curr = from, *next;
while (curr) {
next = curr->right;
curr->right = prev;
std::cout val right;
curr->right = prev;
prev = curr;
curr = next;
}
}
主循环里何时调用 reverseAndPrint
只在两种情况下需要翻转输出:
-
cur->left != nullptr且predecessor->right == cur(即第二次抵达该节点)→ 这是翻转的唯一信号 -
cur->left == nullptr时,直接输出cur->val,不翻转
容易踩的坑:
- 把
cur->left == nullptr的情况也塞进翻转逻辑,导致空指针解引用 - 在
predecessor->right = cur后立刻翻转(那是第一次路过,左子树还没遍历完) - 翻转后忘记把
predecessor->right重置为nullptr,后续 Morris 结构被污染
所以主循环中关键判断长这样:
if (cur->left == nullptr) {
std::cout val right;
} else {
TreeNode* pred = cur->left;
while (pred->right && pred->right != cur) pred = pred->right;
if (pred->right == nullptr) {
pred->right = cur;
cur = cur->left;
} else {
pred->right = nullptr; // 先断链
reverseAndPrint(cur->left); // 再翻转输出左子树的“右链”
std::cout val right;
}
}
边界与兼容性:空树、单节点、右斜树的处理
Morris 后序对结构退化特别敏感。右斜树(每个节点只有右孩子)不会触发任何 left 分支,整个流程退化为普通右向遍历,reverseAndPrint 完全不执行——这是对的,因为后序就是按原序输出。
但左斜树(每个节点只有左孩子)会高频触发翻转逻辑,容易暴露指针还原 bug。实测发现:
- 空树:主循环不进,安全
- 单节点:走
cur->left == nullptr分支,输出一次,安全 - 左斜树:每层都建回边、再拆回边、再翻转长度为 1 的链 → 必须确保
reverseAndPrint能正确处理from->right == nullptr的情况
真正的复杂点不在算法骨架,而在翻转链表那几行——它既要输出、又要还原、还不能破坏后续 Morris 走向。少一个 nullptr 赋值,整棵树就可能无限循环或崩溃。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











