Morris遍历
概念
Morris解决的问题,二叉树的三种遍历,时间复杂度O(N),空间复杂度O(1),核心在与利用二叉树的空闲指针。
Morris遍历的基本思想是:
- 开始时当前节点(cur)指向头节点,当前节点为空整个过程停止
- 如果当前节点的左子树为空,则遍历当前节点的右子树。
- 如果当前节点的左子树不为空,找到当前节点左子树上最右的节点mostright
- 如果mostright的右指针指向空,让其指向cur,cur向左移动。
- 如果mostright的右指针指向cur,让其指向空,cur向右移动。
理解核心:没有左子树的节点只到达一次,有左子树的节点到达两次。因为有左子树就有左子树的最右节点,最右节点指向空时,就让其指向cur,进行“搭桥”,为第二次的访问做准备,但是如果没有左子树,遍历过程不会回到该节点,因此就不会再次访问到cur节点,所以到达一次。
对于有左子树的情况,利用左子树最右节点的右指针状态,来标记是第几次到达,如果指向空,表示第一次到达,如果指向的是cur,表示是第二次到达。
时间复杂度和空间复杂度
每个节点都需要判断它的左树最右节点指向的是空还是自己,这样做的时间复杂度还是O(N)吗?是的,首先对于没有左子树的节点,计算它们时不需要向下访问,只需要对有左子树的节点进行向下两次访问,都是找左子树的最右节点,这样子可以得出二叉树上的节点被访问2次,2是个常数,因此时间复杂度为O(N)。
Morris遍历不需要使用栈或递归调用栈来存储节点信息,因此空间复杂度为O(1)。
代码实现:
#include<iostream>
using namespace std;
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
void morris(TreeNode* head) {
TreeNode* cur = head;
TreeNode* mostRight = nullptr;
while (cur != nullptr) {
mostRight = cur->left;
if (mostRight != nullptr) {
// 有左树,找左树的最右节点
while (mostRight->right != nullptr && mostRight->right != cur) {
mostRight = mostRight->right;
}
// 第一次找到
if (mostRight->right == nullptr) {
mostRight->right = cur;
cur = cur->left;
continue; // 回到while
}
else {
// 第二次
mostRight->right = nullptr;
}
}
// 没有左树
cur = cur->right;
}
}
前序遍历:如果是没有左子树的节点,直接打印,具有左子树的节点,第一次遇到进行打印
中序遍历:如果是没有左子树的节点,直接打印,具有左子树的节点,第二次遇到进行打印
144. 二叉树的前序遍历 - 力扣(LeetCode)
void morrisPre(TreeNode* head,vector<int> &ans) {
TreeNode* cur = head;
TreeNode* mostRight = nullptr;
while (cur != nullptr) {
mostRight = cur->left;
if (mostRight != nullptr) {
// 有左树,找左树的最右节点
while (mostRight->right != nullptr && mostRight->right != cur) {
mostRight = mostRight->right;
}
// 第一次找到
if (mostRight->right == nullptr) {
ans.push_back(cur->val);
mostRight->right = cur;
cur = cur->left;
continue; // 回到while
}
else {
// 第二次
mostRight->right = nullptr;
}
} else {
// 没有左树
ans.push_back(cur->val);
}
cur = cur->right;
}
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
morrisPre(root, ans);
return ans;
}
中序遍历同理
后续遍历
只关注能到达两次的节点,当第二次到达后,逆序收集该节点的左树右边界,最后逆序收集整棵树的右边界。
那么逆序收集怎么来,用栈吗?虽然可行,但是不好,因为我们用Morris遍历就是为了省空间,开辟一个栈又要申请额外的空间。怎么办?进行单链表反转就可以了,在右指针上进行链表反转,可以在不增加额外空间的情况下,实现对节点的逆序收集 。
链表反转完成之后,进行答案的收集,最后将链表再次反转回去,有可能其他节点继续使用,不要破坏树的结构。实现要点:1.单链表的反转。2.节点的收集时间以及链表的恢复。
// 从from出发,类似单链表翻转,去翻转right指针的方向
TreeNode* reverse(TreeNode* from) {
TreeNode* pre = nullptr;
TreeNode* next = nullptr;
while (from != nullptr) {
next = from->right;
from->right = pre;
pre = from;
from = next;
}
return pre;
}
// head为头,树的右边界逆序收集
void collect(TreeNode* head, vector<int>& ans) {
TreeNode* tail = reverse(head);
TreeNode* cur = tail;
while (cur != nullptr) {
ans.push_back(cur->val);
cur = cur->right;
}
reverse(tail);
}
void morrisPostorder(TreeNode* head, vector<int>& ans) {
TreeNode* cur = head;
TreeNode* mostRight = nullptr;
while (cur != nullptr) {
mostRight = cur->left;
if (mostRight != nullptr) {
// 有左树,找左树的最右节点
while (mostRight->right != nullptr && mostRight->right != cur) {
mostRight = mostRight->right;
}
// 第一次找到
if (mostRight->right == nullptr) {
mostRight->right = cur;
cur = cur->left;
continue; // 回到while
}
else {
// 第二次
mostRight->right = nullptr;
collect(cur->left, ans);
}
}
cur = cur->right;
}
collect(head, ans);
}
vector<int> postorderTraversal(TreeNode* root) {
vector<int> ans;
morrisPostorder(root, ans);
return ans;
}
Morris的局限性
-
修改树结构:Morris遍历在遍历过程中会暂时修改树的结构,即通过设置某些节点的右指针指向其前驱节点(也称为"线索化")。虽然在遍历结束后会恢复树的原貌,但在遍历过程中树的结构是不稳定的。
-
实现复杂:相比于递归或迭代方法,Morris遍历的实现较为复杂,理解起来也更为困难。它需要处理多种情况,如节点的左子树是否存在、是否是第二次访问某个节点等。
-
不适用于非递归树:Morris遍历依赖于树是递归定义的,即每个节点最多有两个子节点。对于非递归定义的树结构(如多叉树),Morris遍历方法不适用。
-
不适合修改操作:由于Morris遍历会修改节点的右指针,如果在遍历过程中进行修改操作(如插入或删除节点),可能会导致算法失败或需要额外的逻辑来处理这些情况。
-
不适合需要回溯的情况:在某些遍历算法中,可能需要回溯到父节点或祖先节点,这在Morris遍历中比较困难,因为它不保存父节点的信息。
-
不适合并行处理:由于Morris遍历依赖于特定的线索化操作,这使得并行处理变得复杂,因为它需要同步访问和修改树的结构。
-
不适合遍历特定路径:如果需要遍历从根节点到某个特定节点的路径,Morris遍历并不是最佳选择,因为它不提供直接的路径跟踪机制。
-
不适用于有额外约束的树:如果树结构有额外的约束,比如某些节点不能被线索化,那么Morris遍历可能不适用。
二叉树的最小深度
首先要明确求当前节点在那一层,根据Morris遍历,树上的节点要么是从父节点到达,要么是从左子树的最右节点来的。有一个preLevel,如果是从父节点来的,那么高度是上一层的高度+1,如果是从左子树的最右节点来的,那么是preLevel减去该节点左树右边界的节点个数。
现在每个节点在那一层解决了,那么如何得到叶节点呢?左右子树为空?不行,因为在遍历的过程中,树已经被修改,就像上面,h指向了b,并不是空节点。
当节点恢复过来不就好了吗,第二次到达b是,h的右节点就指向了空,这就恢复过来了,此时就求得了h是叶节点以及h的正确高度,每到一个叶节点,高度进行比较。注意:有时向上指的并不一定是叶节点,例如:下图中的h,此时不进行更新答案。
以及整棵树的最右边的节点是叶节点,参与最小高度的比较,如果不是,不参与比较。
int minDepth(TreeNode* head) {
if (head == nullptr) {
return 0;
}
TreeNode* cur = head;
TreeNode* mostRight = nullptr;
// morris遍历中,上一个节点所在的层数
int preLevel = 0;
// 树的右边界长度
int rightLen;
int ans = INT_MAX;
while (cur != nullptr) {
mostRight = cur->left;
// 有左孩子
if (mostRight != nullptr) {
// 右边界的长度,为了向上更新第二次到达节点的高度
rightLen = 1;
// 左树右边界
while (mostRight->right != nullptr && mostRight->right != cur) {
rightLen++;
mostRight = mostRight->right;
}
// 第一次来的
if (mostRight->right == nullptr) {
preLevel++;
mostRight->right = cur;
cur = cur->left;
continue;
}
// 第二次来
else {
// 是叶节点
if (mostRight->left == nullptr) {
ans = min(ans, preLevel);
}
// 更新高度,左树最右节点高度 - 右边界的长度
preLevel -= rightLen;
mostRight->right = nullptr;
}
}
// 没有左孩子,当前节点深度是上一层+1
else {
preLevel++;
}
// 没有左孩子,向右走
cur = cur->right;
}
// 整棵树的最右节点
rightLen = 1;
cur = head;
while (cur->right != nullptr) {
rightLen++;
cur = cur->right;
}
// 整棵树的最右节点是叶节点才纳入统计
if (cur->left == nullptr) {
ans = min(ans, rightLen);
}
return ans;
}