做之前所提到的递归和迭代来实现二叉树的遍历所对应的时间复杂度和空间复杂度都是O(N)的,若使用morris来实现二叉树的遍历,虽然时间复杂度仍为O(N),但可将空间复杂度降至O(1)。这里根据自己的理解,解释一下morris遍历二叉树的原理,算法就是将叶子结点的空指针利用了起来,比如,一个二叉树有N个结点,那么一个结点有两个指针(Left、Right),而有效的指针只使用了N-1个,那么就会有N+1个指针是空的。如下图所示:
我们以前序遍历为例,从根节点3开始,我们遍历到9时,我们建立一个从9到3的指针,即9.right=3,这样我们在9的时候就可以通过9.right回溯到结点3,不需要额外的空间来进行存储,本着不改变树的结构原则,再次回到结点3时需要将之前建立的线索指针消除掉。这大概就是morris遍历二叉树的原理。
这本文中,我们使用morris算法来实现了对二叉树的前序遍历,中序遍历和后序遍历,题目描述和要求以及题目难度不赘述,参考之前的文章:
144. 前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
if(root==null)return list;
TreeNode pre = null;
while(root!=null){
pre = root.left;
if(pre!=null){
while(pre.right != null && pre.right != root){
pre = pre.right;
}
if(pre.right==null){
pre.right = root;
list.add(root.val);
root = root.left;
continue;
}
else{
pre.right = null;
}
}
else{
list.add(root.val);
}
root = root.right;
}
return list;
}
}
解释一下代码,定义了一个结点pre为root的左子结点,当pre不为Null时,我们进入if,通过之前的叙述可以明确知道,一个结点左子树中的最后一个结点是该结点的前继结点,也就是建立线索指针的起始点,所以我们用一个while循环,找到了结点root左子树的最右结点,判断其右指针是否为null也就是判断是否已经建立了线索指针, 若没有,则建立一个该结点指向root的线索指针,随后将root结点沿着其左指针指向其左子结点,跳过后序操作。若有,则表明已经建立了,xu将该线索指针消除掉。当root左子结点为Null时,root需要沿右指针移动,不需要担心root的右子结点不存在,因为在前面那个if(pre!=null)中,已经将线索指针建立好了。
举个例子:
按我们所给出的算法来进行操作,首先我们进while循环,另pre = 3.left,即9。因为9不为null,但结点9没有右子结点,所以我们建立一个由结点9到结点3的线索指针,然后将3赋到链表中,令root结点移动到结点9。然后进下一次while循环,此时root为结点9,则pre==null,将9赋值给list,沿着线索指针,root回到结点3,进入下一次循环,判断结点9的右指针存在,则消除线索指针,表明结点3的左子树已经遍历完了,沿着右指针将root赋为结点20,后续就不赘述了。
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:40.1 MB, 在所有 Java 提交中击败了5.24%的用户
94. 中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
if(root==null)return list;
TreeNode pre = null;
while(root!=null){
pre = root.left;
if(pre!=null){
while(pre.right != null && pre.right != root){
pre = pre.right;
}
if(pre.right==null){
pre.right = root;
root = root.left;
continue;
}
else{
pre.right = null;
}
}
list.add(root.val);
root = root.right;
}
return list;
}
}
morris实现中序遍历和实现前序遍历的原理是一样的,只是需要在输出的位置进行修改,因为中序遍历的优先级是:左结点>根结点>右结点。所以需要从最左方叶子结点开始输出,只需要在root = root.right之前把root的val输入到list即可。
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:39.9 MB, 在所有 Java 提交中击败了23.33%的用户
145. 后序遍历
class Solution {
List<Integer> list;
public List<Integer> postorderTraversal(TreeNode root) {
list = new ArrayList<>();
TreeNode cur = root;
TreeNode med = null;
while(cur != null){
med = cur.left;
if(med != null){
while(med.right != null && med.right != cur){
med = med.right;
}
if(med.right == null){
med.right = cur;
cur = cur.left;
continue;
}
else{
med.right = null;
printf_Node(cur.left);
}
}
cur = cur.right;
}
printf_Node(root);
return list;
}
void printf_Node(TreeNode head){
TreeNode tail = reverse_list(head);
while(tail!=null){
list.add(tail.val);
tail = tail.right;
}
reverse_list(tail);
}
TreeNode reverse_list(TreeNode head){
TreeNode med = null,next,curr;
curr = head;
while(curr!=null){
next = curr.right;
curr.right = med;
med = curr;
curr = next;
}
return med;
}
}
morris同样使用于后序遍历,只是需要在适当的实际进行输出,还是如下图所示:
根据后序遍历的原理,输出顺序为D、H、J、K、I、E、B、F、G、C、A。
我们只需要沿着线索指针回到根结点时,删除线索指针后,将根节点的左子结点沿着右指针逆向输出,比如,回到B时,将D到B的指针删除,输出D,回到A时,将K到A的指针删除,将B->E->I->K逆向输出。即KIEB。
根节点沿着右指针这条路径没有遍历到,需要在返回链表前,将root右指针这条路径进行添加赋值。
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:39.6 MB, 在所有 Java 提交中击败了59.72%的用户