必须翻转右子链是因为Morris后序需在无栈无递归下逆序收集左子树最右路径节点,翻转构造可遍历单向链,正向走即得逻辑逆序;不翻转则无法确定收集时机且破坏线索结构。

为什么 Morris 后序遍历必须翻转右子链,而不是直接逆序打印
Morris 后序的核心难点不在“找前驱”,而在“如何在不递归、不栈、不标记的情况下收集子树访问序列”。right 指针被临时复用为线索指针,当某节点的左子树处理完、准备进入右子树时,它的 right 可能已被设为指向祖先(即形成向上的线索),此时无法自然回溯。翻转右子链的本质是把“从当前节点到前驱的路径”变成一条单向链表,从而能正向遍历并逆序输出——这比试图动态重建访问栈更轻量且确定。
常见错误现象:Segmentation fault 或输出顺序错乱,往往是因为翻转逻辑漏判了 nullptr 边界,或在翻转后未恢复原 right 指针。
- 只翻转「当前节点左子树的最右路径」上所有节点的
right指针,不是整棵树 - 翻转起点是
predecessor(左子树最右节点),终点是current->left - 翻转后必须用临时指针正向走一遍该链,并在每步将节点值加入结果——这是唯一合法收集时机
- 翻转完成后,需再走一遍链表,把所有
right指针恢复成nullptr(否则破坏原树结构)
如何安全实现右子链翻转与恢复:三步不可省略
翻转不是目的,是为构造一个可线性遍历的临时链。关键在于翻转、遍历、恢复三阶段必须原子化,中间不能穿插其他指针操作。
典型误用:current->right = nullptr 被提前写在翻转循环里,导致后续找不到下一个节点。
- 第一步(翻转):用
prev = nullptr,node = current->left,循环中先缓存node->right,再令node->right = prev,最后更新prev = node和node = temp - 第二步(收集):从
prev(此时是链头)开始,正向遍历该链,将每个节点的val推入结果容器;注意:此时链方向已反,所以正向走 = 逻辑上的逆序访问 - 第三步(恢复):再次从
prev开始,把每个节点的right设回nullptr;若跳过此步,后续 Morris 循环可能误用残留线索,引发崩溃或死循环
示例片段(核心逻辑):
TreeNode* reverse_and_collect(TreeNode* head, vector<int>& res) {
TreeNode* prev = nullptr;
TreeNode* node = head;
while (node != nullptr) {
TreeNode* next = node->right;
node->right = prev;
prev = node;
node = next;
}
// 此时 prev 是翻转后的头
node = prev;
while (node != nullptr) {
res.push_back(node->val);
node = node->right;
}
// 恢复
node = prev;
while (node != nullptr) {
TreeNode* next = node->right;
node->right = nullptr;
node = next;
}
return prev; // 返回原 head,便于调用方理解
}
</int>
Morris 主循环中何时触发翻转:判断条件必须精确
翻转只发生在「当前节点有左子树,且其左子树的最右节点的 right 尚未指向当前节点」时——即第一次到达该节点,准备建立线索。此时才需要把左子树最右路径翻转并输出(因为后序要求左右根,左子树已处理完,右子树尚未进入,但右子路径上的节点需在返回当前节点前被访问)。
容易踩的坑:if (current->left == nullptr) 分支里也去翻转,或在 else 分支里漏掉对 predecessor->right == nullptr 的判断。
- 正确触发点:
if (current->left != nullptr && predecessor->right == nullptr) - 此时先设
predecessor->right = current(建线索),再调用翻转函数处理current->left子树的最右链 - 若
predecessor->right == current,说明是第二次回来,应断开线索(predecessor->right = nullptr),然后进入右子树 - 绝对不要在
current->right != nullptr分支里做翻转——Morris 后序不依赖右线索,只用左线索驱动
零辅助空间的代价:时间复杂度仍是 O(n),但常数项明显升高
表面上没用栈和递归,空间 O(1),但每个边最多被遍历 3 次:一次找前驱,一次翻转,一次恢复。实测在深度较大的偏斜树上,比标准递归慢 2–3 倍。这不是 bug,是算法本质决定的。
兼容性提示:该算法要求 TreeNode 结构允许修改 right 指针;若节点被多线程共享或只读封装,Morris 方案直接不可用。
- 翻转链长度 = 左子树高度,最坏情况(完全左倾树)下,单次翻转耗时 O(h),总时间 O(n × h) → 实际仍是 O(n),因每条边总访问次数有界
- 若需调试,可在翻转前后加断言:
assert(node->right == nullptr || node->right == current),快速捕获指针污染 - 生产环境慎用:除非内存极端受限且树形态稳定,否则
std::stack+ 迭代后序更可靠
真正难的从来不是写出翻转代码,而是想清楚:为什么必须翻转右子链,而不是左?为什么翻转后要立刻收集再立刻恢复?这些约束一旦松动,整个零空间保证就崩了。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











