Morris 算法

Morris 算法相较于其他二叉树遍历算法不同的是,可以使用非递归原地无影响地进行前中后序遍历。

这里推荐左神的视频,讲解详细

线索化

无论是哪种遍历顺序,线索化都是一样的。可以按照下图对照遍历规则结合看:

image

用 cur 节点遍历整棵树:

  1. cur 没有左孩子,则 cur = cur.right;

  2. cur 有左孩子,找到左孩子的最右子节点 mostRight,因为这是当前节点的左子树要遍历的最后一个元素

    • 如果 mostRight 节点的右子树为空,则让该节点的右子树指向当前节点,用于回溯到 cur 节点,遍历右子树,而我们要知道 cur 是用来遍历所有节点的,所以不使用递归必然要涉及回溯操作,并且在回溯节点设置好后,要进行 cur 的遍历,也即 mostRight.right = cur; cur = cur.left;

    • 如果 mostRight 的右节点不为空,也就是说 mostRight 的右节点就是回溯点,同时需要撤销线索化。也即 cur = cur.right;mostRight.right = null;

大家可以对照一下,这是cur的遍历流程:

1、2、4、2、5、1、3、6

前提代码

public class Node {
    public Node left;
    public Node right;
    public int val;

    public Node(int val) {
        this.val = val;
    }

    public Node(){}

    public Node buildTree(int[] nums, int i){
        if(nums.length==0)
            return null;
        if(i>=nums.length || nums[i] == 0)
            return null;
        Node root = new Node(nums[i]);
        root.left = buildTree(nums,2*i+1);
        root.right = buildTree(nums,2*i+2);
        return root;
    }
}

Morris 的前序遍历

聪明的读者肯定发现了,上面的遍历又会重复遍历的节点。更聪明的读者肯定又发现了,如果把所有的数字除掉第二遍,再看就是 1、2、4、5、3、6

这就是前序遍历的顺序,那么聪明的同学就发现了,其实这就和递归操作类似了,此时我们只要将第一次出现的结果进行打印就能得到前序遍历。

注意下面代码中的循环退出条件:

  1. 因为左孩子为空的时候不会去遍历他的左孩子

  2. 遍历右孩子时,如果也没有有孩子,那么会有线索,右孩子仍不为空

所以只有到了最后一个节点的下一个节点,才可能为空。这个可以自己拟图按照下面的代码走一遍,我说再多都不如你自己走一遍。

public List<Integer> preOrderTraverse(Node root) {

	List<Integer> res = new ArrayList<>();
	Node cur = root, mostRight;
	
	while(cur != null) {
		mostRight = cur.left;
		if(mostRight != null) {
			while(mostRight.right != null && mostRight.right != cur) {
				mostRight = mostRight.right;
			}
			
			if(mostRight.right == null) {
				res.add(cur.val);
				mostRight.right = cur;
				cur = cur.left;
			} else {
				cur = cur.right;
				mostRight.right = null;
			}
		} else {
			res.add(cur.val);
			cur = cur.right;
		}
	}
	
	return res;
}

Morris 的中序遍历

刚刚肯定有读者在想了,如果把重复遍历的节点的第一次省略会得到什么结果呢?很明显就是中序遍历。

前序和中序遍历其实都是一样的操作流程,只是输出位置不一样,这和递归的逻辑很像。

public List<Integer> preOrderTraverse(Node root) {

	List<Integer> res = new ArrayList<>();
	Node cur = root, mostRight;
	
	while(cur != null) {
		mostRight = cur.left;
		if(mostRight != null) {
			while(mostRight.right != null && mostRight.right != cur) {
				mostRight = mostRight.right;
			}
			
			if(mostRight.right == null) {
				mostRight.right = cur;
				cur = cur.left;
			} else {
				res.add(cur.val);
				cur = cur.right;
				mostRight.right = null;
			}
		} else {
			res.add(cur.val);
			cur = cur.right;
		}
	}
	
	return res;
}

Morris 后序遍历

显然,应该有读者自己去试试了凑出后序遍历的规律,但发现好像并不是那么好凑。

从原理出发,后序遍历是:左右中

那么会发现一个现象,对于 b、c、d、e 这些节点,无论他们的左孩子有多少个节点,如何排列,当我开始打印节点 e 的时候,必然是要打印 e、d、c、b,也就是逆序输出,也就是链表倒转操作。而这个操作的时机,肯定是第二次遍历到 a 点的时候
image

再继续看这张图,了解更具体的原因:

假设 g 还有左孩子,那么就会继续往左孩子方向遍历;假设 g 没有左孩子只有右孩子 p,我们知道 p 会线索化到 g 的父节点,也就是说 p 的右节点会指向 g 的父节点, 此时根据左右中的顺序,就需要逆序遍历g、p。

如果到这还没有感觉,继续。

此时已经逆序遍历完了 g、p,我们知道 i 会线索化 h,所以第二次遍历 h 时,会逆序遍历 i;然后 h 时线索化到 b 的,所以……

image

大家应该明白大致的运行流程了。

private List<Integer> afterOrderTraverse(Node root) {
	List<Integer> res = new ArrayList<>();

	Node cur = root, mostRight;
	while (cur != null) {

		mostRight = cur.left;
		if (mostRight != null) {

			while (mostRight.right != null && mostRight.right != cur) {
				mostRight = mostRight.right;
			}
			if (mostRight.right == cur) {
				mostRight.right = null;
				printEdge(cur.left, res);
				cur = cur.right;
			} else {
				mostRight.right = cur;
				cur = cur.left;
			}
		} else {
			cur = cur.right;
		}
	}
	// 没有任何节点的左孩子为root,所以直接调用
	printEdge(root, res);

	return res;
}

private void printEdge(Node root, List<Integer> res) {
	Node cur = reverseEdge(root);
	Node temp = cur;

	while (cur != null) {
		res.add(cur.val);
		cur = cur.right;
	}

	reverseEdge(temp);
}

private Node reverseEdge(Node root) {
	Node cur = root;
	Node pre = null;
	Node after = root.right;

	while(after != null) {
		cur.right = pre;
		pre = cur;
		cur = after;
		after = after.right;
	}
	cur.right = pre;
	return cur;
}

有啥问题相互讨论哈

posted on 2022-08-04 23:54  wtgiiwtg  阅读(86)  评论(0)    收藏  举报