Morris后序遍历的核心难点在于访问时机:必须等左右子树均处理完才访问根节点,而Morris遍历无栈无递归,无法自然回溯;因此需在第二次到达当前节点时,沿左子树最右路径逆序输出并恢复指针,最后整体逆序或用栈调整顺序。

Morris后序遍历的核心难点在哪
不是“怎么翻转链表”,而是“何时访问节点”。Morris前序/中序能边找前驱边输出,但后序必须等左子树和右子树都处理完才访问根——而Morris遍历本身不递归、不栈,没法自然回溯。所以标准做法是:**用两次翻转模拟“逆后序”(根→右→左),再整体逆序输出**。这导致实际访问顺序是反的,必须缓存或倒排。
为什么不能直接套用Morris中序的模板
MorrisTraverse 中序靠 if (pred->right == nullptr) 找到安全插入点;后序要多走一步:当 pred->right == curr 时,说明左子树已处理完,此时需沿 curr->left 向右一路到底,把路径上所有节点逆序输出(即模拟从叶子往上归的过程)。漏掉这步翻转,就会跳过大量节点。
- 中序只关心“当前节点是否该输出”,后序关心“当前节点的整个左子树是否已输出完毕”
- 必须显式判断
pred->right == curr这一回溯条件,否则永远卡在左子树里 - 翻转子路径时,
prev指针必须初始化为nullptr,否则首节点指针错乱
关键代码段:翻转并输出左子树链
这是整个算法最易出错的部分。以下片段用于在确认左子树处理完毕后,沿 curr->left → pred 这条临时链,逆向输出节点值:
TreeNode* tail = curr->left;
TreeNode* prev = nullptr;
while (tail != curr) {
TreeNode* next = tail->right;
tail->right = prev;
prev = tail;
tail = next;
}
// 此时 prev 是原链尾(最右下节点),沿 prev 往回走就是逆后序
tail = prev;
while (tail != nullptr) {
result.push_back(tail->val);
TreeNode* next = tail->right;
tail->right = prev; // 恢复原指针(可选,但推荐)
prev = tail;
tail = next;
}
注意:tail != curr 是终止条件,不是 tail != nullptr;恢复指针时用的是 tail->right = prev,不是 tail->right = next——这里错一个字母就全乱。
边界情况与性能陷阱
空树、单节点、只有左子树、只有右子树,这四种情况必须单独验证。尤其当 curr->left == nullptr 时,不能进翻转逻辑,应直接访问 curr(但注意:后序中,此时 curr 还不能立即输出,得先看右子树是否也空)。
- 右子树非空时,
curr必须推迟到右子树遍历完再访问——Morris本身不记录“右子树是否完成”,所以只能靠第二次遍历右子树路径来隐式判断 - 整棵树退化为链表(全左斜或全右斜)时,翻转链长度达
O(n),但仍是O(1)额外空间,只是常数因子略高 - LeetCode 测评中若用
vector存结果,最后要reverse(result.begin(), result.end());若边翻转边输出到栈再弹出,则省去这步但多一次内存分配
真正难的不是写对,是想明白“为什么必须翻转两次”——第一次翻转构造访问路径,第二次翻转还原树结构。这两个动作缺一不可,且顺序不能颠倒。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











