二叉树的遍历方式,无论是递归还是非递归的,都绕不过一个东西,叫额外空间复杂度O(H) (其中 H 为树高)。
因为二叉树遍历过程中只能向下查找孩子节点而无法回溯父结点,因此这些算法借助栈来保存要回溯的父节点(递归的实质是系统帮我们压栈,非递归的方式是 我们自己压栈),并且栈要保证至少能容纳下 H 个元素(比如遍历到叶子结点时回溯父节点,要保证其所有父节点在栈中)。
而morris遍历则能做到时间复杂度仍为 O(N) 的情况下额外空间复杂度只需 O(1) 。
morris遍历利用了二叉树 大量的空闲的空间。
比如像下面这种,
有四个空节点完全没用!
例子:
先忘掉 先序,后序,中序
只有Morris 遍历。
cur 的左孩子不为空,说明 cur 的左子树存在,找出该左子树的最右结点,记为 mostRight
mostRight 的右孩子为空,那就让其指向 cur ( mostRight.right=cur ),并左移cur ( cur=cur.left )
同理,此时,cur左子树不为空,找出该左子树的最右结点,记为 mostRight
如果, mostRight 的右孩子为空,那就让其指向 cur ( mostRight.right=cur ),并左移
cur ( cur=cur.left )
此时,cur划过的 节点为:
此时,cur 的左孩子为空,说明 cur 的左子树不存在,那么 cur 右移来到 cur.right,即回到2。
此时,cur 的左孩子不为空
cur的左子树,是4,4右指针是指向cur的!即4右指针 不空。即cur左子树的最右结点mostRight是指向cur的。
所以,mostRight 的右孩子不空
那么让 cur 右移( cur=cur.right ),并将 mostRight 的右孩子置空。
此时,cur没有左孩子,所以说明 cur 的左子树不存在,那么 cur 右移来到 cur.right
此时,
cur 的左孩子不为空,说明 cur 的左子树存在,找出该左子树的最右结点,记为 mostRight。
mostRight 的右孩子为cur,不为空,那么让 cur 右移( cur=cur.right ),并将 mostRight 的右
孩子置空。
此时 cur 的左孩子不为空,即cur的左子树存在,将该左子树的最右结点6记为 mostRight
mostRight 6的右孩子为空,那就让其指向 cur ( mostRight.right=cur ),并左移cur( cur=cur.left )
得到:
此时cur 的左孩子为空,cur 的左子树不存在,cur 右移来到3
cur 的左孩子不为空,该左子树的最右结点6,记为 mostRight
mostRight 的右孩子=cur,不为空,那么让 cur 右移( cur=cur.right ),并将 mostRight 的右
孩子置空
cur没有左孩子,直接向右移动,划向 空
整个遍历过程停止,
cur 划过的点有:
代码:
蓝色部分指 我来到一个当前节点,当其没有左孩子的时候,这部分是不会发生的。
之后cur向右移动。
这段代码不断地 寻找左子树上 最右节点。
如果mostright不等于空,且不等于当前节点cur,执行while
这两个条件,只有两个都成立,才能往右。
有一个不成立,不能往右。
如果左子树最右节点为空
则满足此条逻辑,
相应代码:
接回讲morris
只要有一个节点,存在左子树,那么cur返回这个节点两次;且第二次到达该节点时,左子树都已经遍历完了。
不存在左子树,那么cur返回这个节点一次
利用左子树 最右节点的 右指针 指向谁 这件事 来标记 是第一次来到这个节点 还是第二次。
如果左子树 最右节点的 右指针 指向null,那么就是第一次来。
如果左子树 最右节点的 右指针 指向 自身,那么就是第二次来。
不是完全二叉树的情况,也可以用morris遍历
进阶3-2
morrispre 改先序的code:
打印行为在这里:
一个当前节点没有左子树的时候 会 执行 else
这部分在 进阶3-2,不是很懂。
后序序列
使用morris遍历得到二叉树的后序序列就没那么容易了,因为对于树种的非叶结点,morris遍历最多会经过它两次,而我们后序遍历实在第三次来到该结点时打印该结点的。因此要想得到后序序列,仅仅改变在morris遍历时打印结点的时机是无法做到的。
但其实,在morris遍历过程中,如果在每次遇到第二次经过的结点时,将该结点的左子树的右边界上的结点从下到
上打印,最后再将整个树的右边界从下到上打印,最终就是这个数的后序序列:
后序遍历中,我们关注的是,能够回到自身两次的节点。
例子:
一个节点必须两次回到自己,
morris遍历:
删除morris遍历 只回到一次的 节点
只关注morris遍历 中 能够 来到两次的节点。
当第二次来到2的时候,逆序打印 其 左子树的右边界。即4
当第二次来到1的时候,逆序打印 其 左子树的右边界。即5,2
当第二次来到3的时候,逆序打印 其 左子树的右边界。即6
然后函数退出之前,单独打印整棵树的右边界:7,3,1
最后结果为:
相应代码:
如果一个节点可以回到两次,执行if
发现是第二次来的时候,执行else
逆序打印左子树右边界
等while跑完后,
单独 逆序打印整棵树的右边界
那么这么样 在额外空间复杂度为O(1)的前提下,实现 逆序打印树的右边界?
肯定不能用栈,不然复杂度超过了。
第二次来到a的时候,拓扑结构是这样的。
左子树最右节点 的指针 是 指向 我自己的
用链表reverse的方法 把指针给改了。
讲edcb打印完以后,
把指针都调回来。
完成逆序打印。
进阶3-3 01.51 讲时间复杂度,不是很懂,之后再听一遍吧。
整棵树是可以被右节点分解掉的;
所有右边界的节点个数是N,而每个右节点只会遍历有限几次,所以时间复杂度O(N)
例子:
这一棵树,
mirrors遍历:
整颗左子树都可以被右边界分解:
且每个右边界都可以趟两遍:
总结:
整棵树 整体 可以被右边界分解:
所以右边界 整体 个数为N,
而每个右边界最多只能趟 有限几次。
所以复杂度为O(N)
总结:
以后用遍历解决的问题,都可以用 morris遍历解决。
morris遍历是极为强大的装逼利器,面试官一旦问到遍历,就往这个方向上靠!