morris前序遍历的核心逻辑是:在首次抵达每个节点时立即访问(“见根即访”),利用左子树最右节点的空right指针建立临时线索,有左子树时于建线前输出、无左子树时直接输出并右移,确保“根→左→右”顺序且线索建拆各一次、树结构最终恢复。

Morris前序遍历的核心逻辑是什么?
Morris遍历的本质是用二叉树中原本为空的 right 指针(或 left,取决于方向)临时“链接”回祖先节点,从而避免使用栈或递归。前序遍历要求在第一次访问节点时就输出值,这和中序不同——中序是在“回溯”到某节点时输出,而前序必须抓住“首次抵达”的时机。
关键判断点:当从左子树绕回当前节点时,那个临时链接(predecessor->right == curr)意味着这次是第二次来,要拆除链接;而第一次来时,predecessor->right 是 nullptr,此时应先输出 curr->val,再建立链接并左移。
如何避免重复访问和漏输出?
常见错误是把前序输出位置放错:有人在建立线索后立刻输出,结果导致右子树根被提前输出;也有人只在无左子树时输出,漏掉有左子树但已完成线索遍历的节点。
正确做法是统一在两个地方输出:
- 当节点没有左子树时,直接输出
curr->val,然后向右走 - 当有左子树,且找到前驱(
predecessor)后,若predecessor->right为nullptr,说明是首次到达,此时输出curr->val,再设predecessor->right = curr,然后向左走 - 若
predecessor->right == curr,说明已遍历完左子树,此时**不输出**,只恢复空链接(predecessor->right = nullptr),然后向右走
注意:输出动作永远发生在“准备向左深入”之前,而不是“从左回来之后”。
C++实现中指针操作的易错点
C++里最容易出问题的是指针判空和链接复位:
- 找前驱时,必须用
while (pred->right && pred->right != curr),漏掉pred->right != curr会导致死循环(比如右链已连回自己) - 建立线索前必须确认
pred->right == nullptr,否则可能覆盖已有链接(尤其在多轮遍历中) - 复位时写成
pred->right = nullptr,不是curr->right = nullptr—— 错对象会破坏树结构 - 移动
curr的逻辑必须严格对应:无左子树 → 向右;有左且首次 → 输出 + 左移;有左且二次 → 复位 + 右移
示例片段(核心循环节):
while (curr) {
if (!curr->left) {
result.push_back(curr->val);
curr = curr->right;
} else {
TreeNode* pred = curr->left;
while (pred->right && pred->right != curr) {
pred = pred->right;
}
if (!pred->right) {
result.push_back(curr->val); // ← 前序输出在此
pred->right = curr;
curr = curr->left;
} else {
pred->right = nullptr;
curr = curr->right;
}
}
}
为什么Morris前序不能简单套用中序模板?
中序遍历输出在“从左子树返回时”,即 pred->right == curr 分支里;前序则必须把输出提前到进入左子树前。如果照搬中序结构,只是把输出语句挪到外面,大概率会漏掉最右路径上的节点(比如只有右子树的链),或者对某些节点输出两次。
更隐蔽的问题是:当节点只有右子树、没有左子树时,它不会触发任何线索逻辑,全靠第一分支处理——这个分支必须存在且不可省略。有人试图合并条件,结果让 curr = curr->right 出现在多个分支末尾,看似简洁,实则掩盖了控制流差异,调试时极难定位漏输出点。
真正需要警惕的,不是代码长度,而是每个 curr = ... 赋值背后的访问意图是否与前序定义一致。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











