Morris后序遍历更难,因其需额外翻转右链表以逆序收集节点,再恢复结构;而前序/中序仅需单向线索即可完成访问。

为什么 Morris 后序遍历比前序/中序更难写
Morris 遍历的核心是用叶子节点的空右指针“临时挂载”回溯路径,但后序要求左右子树访问完再访根——而 Morris 的线索只能单向(右指针),无法天然支持“从右子树返回根”的跳转。所以必须靠翻转右子路径(即把从当前节点经 right 指针一路走到最右的链表反转),才能按后序逆序收集节点;访问完再翻回来恢复树结构。
这不是加个打印顺序就能改的,本质是多了一层链表翻转操作,且翻转时机、范围、恢复点极易出错。
关键三步:翻转 → 收集 → 恢复
对每个有左子树的节点 cur,在它第一次被线索指向时(即找到其前驱 pre 后让 pre->right = cur),不直接访问,而是先沿 cur->right 向右走到底,把这条“右链”翻转成从底向上指向 cur 的链表,再从链表头(原最右节点)开始遍历收集——此时顺序恰好是该子树后序的逆序;最后再翻回去,断开线索。
- 翻转范围:从
cur->right开始,一直往右走,直到nullptr或已存在线索(即node->right == cur)为止,对该段链表做就地翻转 - 收集方式:翻转后,从新链表头开始,把每个节点
val压入结果 vector(注意:这是逆序,所以最终要整体 reverse 一次,或用 stack 临时存) - 恢复动作:翻转回来,并将
pre->right = nullptr断线索,避免重复访问
容易漏掉的边界和坑
常见 crash 或漏节点,基本都卡在这几个地方:
- 翻转链表时没判空:
cur->right可能为nullptr,直接进翻转循环会解引用空指针 - 误把线索当作真实右子:当
node->right == cur时,说明已建立过 Morris 线索,此时应停止向右走,否则会绕回父节点造成死循环 - 忘记恢复右链:翻转后若不翻回来,后续树结构被破坏,下一轮遍历会乱;尤其
pre->right必须清为nullptr,否则第二次进入同一子树会误判 - 后序结果需整体 reverse:因为每次翻转链表后收集的是“右→根→左”的逆后序,不是简单拼接就能对的
C++ 实现要点与最小可运行片段
核心逻辑集中在处理有左子树的节点;无左子树的节点直接加入结果(它们就是后序中“最左下的叶子”,无需翻转)。
vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
TreeNode* cur = root;
while (cur) {
if (!cur->left) {
cur = cur->right; // 无左,直接向右
} else {
TreeNode* pre = cur->left;
while (pre->right && pre->right != cur) pre = pre->right;
if (!pre->right) {
pre->right = cur;
cur = cur->left;
} else {
// 找到前驱,且已连线索:开始翻转右链
TreeNode* node = cur->left;
vector<int> tmp;
// ① 翻转从 cur->left 开始的右链(注意:是 left 子树的最右路径!)
TreeNode* head = nullptr, *tail = node;
while (tail && tail != cur) { // 防止绕回 cur
TreeNode* next = tail->right;
tail->right = head;
head = tail;
tail = next;
}
// ② 收集翻转后的链表(即从原最右节点往 cur 方向)
node = head;
while (node) {
tmp.push_back(node->val);
node = node->right;
}
// ③ 恢复:再翻一次,并清理线索
tail = head; head = nullptr;
while (tail) {
TreeNode* next = tail->right;
tail->right = head;
head = tail;
tail = next;
}
pre->right = nullptr;
// ④ 追加 tmp(注意:tmp 是逆序,所以 insert 到 res 尾部前要 reverse)
reverse(tmp.begin(), tmp.end());
for (int x : tmp) res.push_back(x);
cur = cur->right;
}
}
}
// 最后别忘了把 root 自身补上?不 —— 它已在某次翻转中被包含。
// 但注意:整棵树最右路径末端的节点,可能始终没被翻转覆盖,所以需额外处理无右子的“最右支”
// 更稳妥做法:在 while 结束后,手动沿 root 右链收集(见标准实现补丁)
return res;
}
</int></int></int>
实际项目中建议用栈暂存翻转路径而非 vector,避免反复 reverse;另外,标准 Morris 后序通常把 root 加到最后,是因为所有左子树翻转都覆盖不到 root 本身——它只在 cur = cur->right 跳转时自然落到末尾,这点非常容易忽略。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











