算法学习13: Morris遍历

Morris遍历的引出

Morris遍历解决的问题

工程上的树结构时包含父节点指针的,由子节点容易找到父节点. 但是算法题目中的树结构通常不包括父节点指针,因此一般的遍历写法需要用栈来存储沿途经过的父节点.

Morris遍历可以做到不需要栈,只需要常数空间线性时间遍历完整棵树. 其主要思想是: 利用树的空闲空间(叶节点的子节点指针)来找到树的根节点.(同样思想的结构有线索二叉树)

线索二叉树

利用二叉链表中的空指针域,存放指向结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为线索). 将一个普通二叉树变为线索二叉树的操作叫做二叉树的线索化.

以下面这个二叉树为例,将其线索化:

在这里插入图片描述

该树中,H,I,J,F,G为叶子节点,其左右子树均指向空;E的右子树节点也为空,下面我们将其序列化,序列化的原则为:

  • 若某结点的左子树为空,则将该结点的左子树指针指向其中序遍历前驱结点
  • 若某结点的右子树为空,则将该结点的右子树指针指向其中序遍历后继结点

线索化后得到的线索二叉树如下(实线箭头为二叉树原有的边,虚线箭头为线索):

在这里插入图片描述

可以看到,二叉树中原有的空指针被利用起来了.

二叉树的线索化代码可以从Morris遍历代码修改而成,见后面.

Morris遍历实现

Morris遍历伪代码

Morris遍历的主要思路就是在遍历的过程中动态建立删除线索二叉树的线索.其具体步骤如下(记curNode为当前来到的节点):

  1. 若当前来到节点curNode无左子树(不需要遍历左子树,没有建立和删除线索的过程).则curNode直接向右子树移动(curNode = curNode.right)
  2. 若当前来到节点curNode有左子树(需要遍历左子树,要经过建立和删除线索的过程),则找到curNode左子树的最右节点(中序遍历最后一个节点),记为mostRight.
    1. mostRight的右子树指针right指向空(说明左子树还没被遍历过,则先建立线索,再遍历左子树).将mostRight的右子树指针right指向curNode,curNode向左子树移动(curNode = curNode.left).
    2. mostRight的右子树指针right指向curNode(说明左子树已经被遍历过,则先删除线索,再遍历右子树).将mostRight的右子树指针right指回空,curNode向右子树移动(curNode = curNode.right).

上面是Morris遍历程序主体的思路,要将其翻译为代码,还要解决一个问题: 即要实现前序遍历,中序遍历,后序遍历,分别应何时执行输出语句?
与一般的遍历相类似,前序遍历,中序遍历,后序遍历的过程中curNode指针访问树中节点的顺序是相同的,三种遍历的差别仅在于执行输出语句的时机.

Morris遍历的实现

下面通过将Morris遍历递归遍历相类比,分别得到Morris前序遍历,Morris中序遍历,Morris后序遍历的具体代码.

考虑下边一段递归遍历二叉树的代码,我们在代码中加了三个断点,不同断点处执行输出语句分别可以实现前序遍历,中序遍历,后序遍历.

// 递归遍历
public static void recursiveTraverse(Node head) {
	if(head == null) {
		return;
	}		
	// 断点1: 前序遍历
	//System.out.println(head.value);
	recursiveTraverse(head.left);
	// 断点2: 中序遍历
	//System.out.println(head.value);
	recursiveTraverse(head.right);
	// 断点3: 后序遍历
	//System.out.println(head.value);
}

Morris遍历中,每个节点会被访问两次(若该节点没有左子树,则这两次访问会被合并为一次).

  1. Morris遍历中,第一次访问该节点是在其左子树还没有被遍历前,对应上述递归函数recursiveTraverse断点1处,在该处执行输出即为前序遍历.
    在这里插入图片描述

    // Morris前序遍历
    public static void morrisPre(TreeNode root) {
    	if (root == null) {
    		return;
    	}
    	TreeNode curNode = root;	// 指向当前节点
    	TreeNode mostRight = null;	// 指向左子树最右节点
    	while (curNode != null) {
    		// 不断判断当前节点左子树是否存在
    		if (curNode.left != null) {
    			// 若当前节点存在左子树,则不断寻找左子树最右节点mostRight(先查看线索情况,即查看左子树是否已被遍历)
    			mostRight = curNode.left;
    			while (mostRight.right != null && mostRight.right != curNode) {
    				mostRight = mostRight.right;
    			}
    			// 找到rightMost,则判断rightMost的right指针是指向空还是指向当前节点,即线索是否建立
    			if (mostRight.right == null) {
    				// 若mostRight的右子树指针right指向空,则先建立线索,再遍历左子树
    				mostRight.right = curNode;
    				System.out.print(curNode.val + " ");
    				curNode = curNode.left;
    			} else {
    				// 若mostRight的右子树指针right指向curNode,则先删除线索,再遍历右子树
    				mostRight.right = null;
    				curNode = curNode.right;
    			}
    		} else {
    			// 若当前节点不存在左子树,则直接遍历右子树,即curNode直接向右子树移动
    			System.out.print(curNode.val + " ");
    			curNode = curNode.right;
    		}
    	}
    	System.out.println();
    }
    
  • 同理,Morris遍历中,第二次访问该节点是在其左子树已被遍历完成后,对应上述递归函数recursiveTraverse断点2处,在该处执行输出即为中序遍历.
    在这里插入图片描述

    // Morris中序遍历
    public static void morrisIn(TreeNode root) {
    	if (root == null) {
    		return;
    	}
    	TreeNode curNode = root;	// 指向当前节点
    	TreeNode mostRight = null;	// 指向左子树最右节点
    	while (curNode != null) {
    		// 不断判断当前节点左子树是否存在
    		if (curNode.left != null) {
    			// 若当前节点存在左子树,则不断寻找左子树最右节点mostRight(先查看线索情况,即查看左子树是否已被遍历)
    			mostRight = curNode.left;
    			while (mostRight.right != null && mostRight.right != curNode) {
    				mostRight = mostRight.right;
    			}
    			// 找到rightMost,则判断rightMost的right指针是指向空还是指向当前节点,即线索是否建立
    			if (mostRight.right == null) {
    				// 若mostRight的右子树指针right指向空,则先建立线索,再遍历左子树
    				mostRight.right = curNode;
    				curNode = curNode.left;
    			} else {
    				// 若mostRight的右子树指针right指向curNode,则先删除线索,再遍历右子树
    				System.out.print(curNode.val + " ");
    				mostRight.right = null;
    				curNode = curNode.right;
    			}
    		} else {
    			// 若当前节点不存在左子树,则直接遍历右子树,即curNode直接向右子树移动
    			System.out.print(curNode.val + " ");
    			curNode = curNode.right;
    		}
    	}
    	System.out.println();
    }
    
  • 要利用Morris遍历实现后序遍历有点麻烦,困难在于Morris遍历中每个节点只会被访问两次,对应递归遍历recursiveTraverse断点1断点2处,因此Morris遍历中不会存在对应递归遍历recursiveTraverse断点3的时机.

    在这里插入图片描述

    在这里我们使用一个特殊的策略: 在第二次访问到curNode节点时,倒序输出左子树右边界(从当前节点的左孩子curNode.left左子树最右节点rightMost的路径).
    如上图: 第二次访问到B时,输出A;第二次访问到D时,输出C;第二次访问到F时,输出ED;第二次访问到H时,输出GFB;第二次访问到K时,输出J;第二次访问到0时,输出KIH.

    那么如何实现倒序输出呢? 自然而然地想到用栈,但是Morris遍历要求占用常数空间,因此不能用栈.因此我们使用反转链表的方式输出.(当然,这种方式太变态了).

    // Morris后序遍历
    public static void morrisPos(TreeNode root) {
    	if (root == null) {
    		return;
    	}
    	TreeNode curNode = root;	// 指向当前节点
    	TreeNode mostRight = null;	// 指向左子树最右节点
    	while (curNode != null) {
    		// 不断判断当前节点左子树是否存在
    		if (curNode.left != null) {
    			// 若当前节点存在左子树,则不断寻找左子树最右节点mostRight(先查看线索情况,即查看左子树是否已被遍历)
    			mostRight = curNode.left;
    			while (mostRight.right != null && mostRight.right != curNode) {
    				mostRight = mostRight.right;
    			}
    			// 找到rightMost,则判断rightMost的right指针是指向空还是指向当前节点,即线索是否建立
    			if (mostRight.right == null) {
    				// 若mostRight的右子树指针right指向空,则先建立线索,再遍历左子树
    				mostRight.right = curNode;
    				curNode = curNode.left;
    			} else {
    				// 若mostRight的右子树指针right指向curNode,则先删除线索,再遍历右子树
    				// 在这里要注意下边两句的顺序: 要先断开线索,再倒序输出从 `当前节点的左孩子` 到 `左子树最右节点rightMost` 的路径
    				mostRight.right = null;
    				printEdge(curNode.left);
    				curNode = curNode.right;
    			}
    		} else {
    			// 若当前节点不存在左子树,则直接遍历右子树,即curNode直接向右子树移动
    			curNode = curNode.right;
    		}
    	}
    	printEdge(root);
    	System.out.println();
    }
    
    // 倒序输出树右边界(从 根节点root 到 树最右节点)的路径
    public static void printEdge(TreeNode root) {
    	// 将待输出路径先反转,再输出,再反转回来
    	TreeNode tail = reverseEdge(root);
    	TreeNode cur = tail;
    	while (cur != null) {
    		System.out.print(cur.val + " ");
    		cur = cur.right;
    	}
    	reverseEdge(tail);
    }
    
    // 反转链表
    public static TreeNode reverseEdge(TreeNode from) {
    	TreeNode pre = null;
    	TreeNode next = null;
    	while (from != null) {
    		next = from.right;
    		from.right = pre;
    		pre = from;
    		from = next;
    	}
    	return pre;
    }
    

    逆序输出时路径时,要注意: 先断开线索,再逆序输出路径.

Morris遍历的时间复杂度

Morris遍历的时间复杂度是O(N). 这个结论比较意外,因为考虑到第一次访问到某个节点时要找到其左子树最右节点mostRight,因此其时间复杂度应为O(N*lgN),但这是不对的,因为并不是访问到每一个节点都要寻找其左子树最右节点mostRight(叶子节点制备访问一次).

下面证明Morris遍历的时间复杂度为O(N):

在这里插入图片描述

我们将树按右边界划分为有限个部分,每次寻找左子树最右节点mostRight,要遍历一次右边界.但这个搜索mostRight的过程只会在访问到树根的上层节点这棵树作为上层节点的左子树的情况下触发,因为每个节点最多被访问两次,所以每个右边界最多被遍历两次,右边界的总结点数为N,因此Morris遍历的时间复杂度为O(N).

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值