经典的二叉树遍历
二叉树的遍历过程中,无论是递归的还是非递归的,都绕不过额外空间复杂度O(h), h为树的高度。因为我们在遍历完一棵子树之后希望能够回到父节点上,而经典的二叉树是没有指向父亲的指针的(实际工程上的红黑树中每个节点会有一个指向父的指针)。这就意味着从上级到下级容易,但是从下级到上级很难,于是我们就用到了栈,递归版本是递归函数帮助我们压栈,而非递归的版本是由我们自己做栈自己来压入,都省不了O(h)的额外空间复杂度。
morris遍历用来干什么呢的
对于一个节点个数为N,高度为h的二叉树,遍历该二叉树,morris遍历的时间复杂度为O(N),额外空间复杂度为O(1)。
morris遍历怎么做到的
实际上是利用里二叉树中大量空闲的空间。比如指向空的指针,正是利用了这些空间完成了回到父节点的操作,所以不用使用额外空间,实际上就是修改二叉树的结构,让我们不使用额外空间也能够回到父亲节点
步骤
1) 来到的当前节点记为cur引用,如果cur无左孩子,cur向右移动(cur=cur.right)
2) 如果cur有左孩子,找到cur左子树上最右的节点,记为mostright
- 如果mostright的right指针指向空,让其指向cur,cur向左移动(cur = cur.left)
- 如果mostright的right指针指向cur,让其指向空,cur向右移动,cur = cur.right。
morris遍历的特点
- 如果一个节点没有左子树,那么只到达这个节点一次。如果一个节点有左子树Morris遍历能到达它两次,而且第二次到达的时候,该节点的左子树我已经全部遍历完了
- 在递归版本中,如果当前节点不空,那我们可以三次来到当前节点,根据打印时机不同,我们可以得到先序、中序和后序,Morris遍历模拟了递归版本,只是它无论如何不能像递归版本一样三次(第三次无法)回到自己
- 那么它怎么知道自己是第一次来到该节点还是第二次来到该节点呢? 递归函数是记录每次在函数中执行的行号来区分整个过程的,利用的是函数体中的信息来区分是第一次来到该节点还是第二次来到该节点。但Morris遍历不能利用这种信息,所以他就使用看它左子树最右节点的右指针指向谁这将事情来标记是第一次来到该节点还是第二次来到该节点。
- 不是完全二叉树也可以通过Morris进行遍历
morris遍历实现前序、中序和后序遍历
前序遍历
void morrisPre(Node *head)
{
if (head == nullptr)
{
return;
}
Node *mostRight = nullptr;
Node *cur = head;
while (cur != nullptr)
{
mostRight = cur->m_left;
if (mostRight != nullptr)
{
while (mostRight->m_right != cur && mostRight->m_right != nullptr)
{
mostRight = mostRight->m_right;
}
if (mostRight->m_right == nullptr)
{
mostRight->m_right = cur;
cout << cur->m_value << ",";
cur = cur->m_left;
continue;
}
else
{
mostRight->m_right = nullptr;
}
}
else // 没有左孩子,所以应该立即打印
{
cout << cur->m_value << ",";
}
cur = cur->m_right;
}
cout << endl;
}
中序遍历
void morrisIn(Node *head)
{
if (head == nullptr)
{
return;
}
Node *mostRight = nullptr;
Node *cur = head;
while (cur != nullptr)
{
mostRight = cur->m_left;
if (mostRight != nullptr)
{
while (mostRight->m_right != cur && mostRight->m_right != nullptr)
{
mostRight = mostRight->m_right;
}
if (mostRight->m_right == nullptr)
{
mostRight->m_right = cur;
cur = cur->m_left;
continue;
}
else
{
mostRight->m_right = nullptr;
}
}
//如果一个节点想要往右边走了,那么它再也不会来到这个节点,正是我们打印的时机。
cout << cur->m_value << ",";
cur = cur->m_right;
}
cout << endl;
}
后序遍历
虽然Morris遍历只会到达某一个节点两次或者一次,但是却可以做到后序遍历(这个后序遍历其实就是在递归版本中第三次到达某一个节点时打印的),Morris遍历实现后序遍历的时候只关注可以到达两次的节点,对于没有左子树的节点,我们直接忽略,当我们第二次来到某一个节点的时候,我们直接逆序打印它的左子树的右边界,直到遍历结束,然后逆序打印整棵树的右边界。这个其实就是告诉我们,当我们使用Morris后序打印二叉树的时候,可以假象该树是挂载一个节点下,作为该节点的左子树的。这样可以获得逻辑上的统一。当我们逆序打印边界的时候,不能使用栈,如果使用栈额外空间复杂度就大了,我们通过将右边界逆序,然后打印,最后再逆序回来的做法完成。
Node *reverseList(Node *cur)
{
Node *pre = nullptr;
while (cur != nullptr)
{
Node *next = cur->m_right;
cur->m_right = pre;
pre = cur;
cur = next;
}
return pre;
}
void printEdge(Node *cur)
{
if (cur == nullptr)
{
return;
}
Node * tail = reverseList(cur);
Node *temp = tail;
while (temp != nullptr)
{
cout << temp->m_value << ",";
temp = temp->m_right;
}
reverseList(tail);
}
void morrisPos(Node *head)
{
if (head == nullptr)
{
return;
}
Node *mostRight = nullptr;
Node *cur = head;
while (cur != nullptr)
{
mostRight = cur->m_left;
if (mostRight != nullptr)
{
while (mostRight->m_right != cur && mostRight->m_right != nullptr)
{
mostRight = mostRight->m_right;
}
if (mostRight->m_right == nullptr)
{
mostRight->m_right = cur;
cur = cur->m_left;
continue;
}
else
{
mostRight->m_right = nullptr;
printEdge(cur->m_left);
}
}
cur = cur->m_right;
}
printEdge(head);
cout << endl;
}
Morris遍历的时间复杂度
时间复杂度就是O(N),尽管存在找左子树最右节点这个过程。在我们找左子树最右节点的过程中,我们可以将整棵树分成多个右边界(右边界中包含了所有的节点,N),而每一个右边界只会从指定的节点开始向下遍历,所以只要是边界被遍历有限次数,所以所有的找左子树最右节点这个过程的时间复杂度就是O(N)的,除了找左子树最右节点这个过程其余部分的时间复杂度就是O(N)的