1. 概述
二叉树的常见遍历方法有递归和使用栈的非递归两种
这两种递归方法的时间复杂度都是O(n);
空间复杂度都是O(log n)
有一种遍历——Morris遍历,利用二叉树的数据结构特性,使用O(1)的空间即可完成遍历。
首先提出前序结点的概念:
在中序遍历时,当前结点的前一个结点即为它的前序结点。
查找前序结点的步骤:
若当前结点node的左子结点不为空,则令pre = node.left;若pre没有右子结点,则它就为node的前序结点;若pre有右子结点,则沿着pre的右子结点一直向右,到达最右,该结点即为node的前序结点;
若当前结点node的左子结点为空,则其没有前序结点
Morris遍历的算法步骤:
首先令 node = root;
如果node左子结点为空,则直接进入node的右子结点
如果node左子结点不为空,则查找它的前序结点pre;
如果pre.right == null, 则令pre.right = node,即用pre的右子结点指向当前结点node;然后node进入当前结点的左子结点;
如果pre.right != null (pre.right要么为null,要么等于当前结点node),则令pre.right = null,node进入右子结点;
Morris遍历利用前序结点的右子结点作为标记:
当前序结点的右子结点为null,意味着还没有对当前结点的左子树进行遍历,用pre的右子结点指向当前结点,以便从左子树返回时找到当前结点以及标记pre的右子结点为非null;
当前序结点的右子结点不为null(那么它必为当前结点node),这意味着已经遍历过了当前结点的左子树,不用再对它做什么操作了,接下来进入当前结点的右子树进行遍历;
2. 前序遍历
public List<Integer> morrisPreorder(TreeNode root) {
List<Integer> res = new ArrayList<>();
TreeNode node = root;
while (node != null) {
if (node.left == null) {
res.add(node.val);
node = node.right;
} else {
TreeNode pre = getPrenode(node);
if (pre.right == null) {
res.add(node.val);
pre.right = node;
node = node.left;
} else {
pre.right = null;
node = node.right;
}
}
}
return res;
}
private TreeNode getPrenode(TreeNode node) {
TreeNode pre = node.left;
while (pre.right != null && pre.right != node) pre = pre.right;
return pre;
}
前序遍历时:
若当前结点左子结点为空,则就对当前结点遍历;
否则,当第一次到达当前结点node,即对其遍历,因次在pre.right == null时就对当前结点遍历;
3. 中序遍历
public List<Integer> morrisInorder(TreeNode root) {
List<Integer> res = new ArrayList<>();
TreeNode node = root;
while (node != null) {
if (node.left == null) {
res.add(node.val);
node = node.right;
} else {
TreeNode pre = getPrenode(node);
if (pre.right == null) {
pre.right = node;
node = node.left;
} else {
res.add(node.val);
pre.right = null;
node = node.right;
}
}
}
return res;
}
private TreeNode getPrenode(TreeNode node) {
TreeNode pre = node.left;
while (pre.right != null && pre.right != node) pre = pre.right;
return pre;
}
中序遍历:
若当前结点左子结点为空,则就对当前结点遍历(先左再根后右);
否则,当第二次到达当前结点node,即对其遍历,因次在pre.right != null时就对当前结点遍历;第二次意味着已经完成了当前结点左子树的遍历,接下来就是遍历当前结点了;
4. 后序遍历
public List<Integer> morrisPostorder(TreeNode root) {
List<Integer> res = new ArrayList<>();
TreeNode dummy = new TreeNode(-1);
dummy.left = root;
TreeNode node = dummy;
while (node != null) {
if (node.left == null) {
node = node.right;
} else {
TreeNode pre = getPrenode(node);
if (pre.right == null) {
pre.right = node;
node = node.left;
} else {
printReverse(node.left, pre, res);
pre.right = null;
node = node.right;
}
}
}
return res;
}
private TreeNode getPrenode(TreeNode node) {
TreeNode pre = node.left;
while (pre.right != null && pre.right != node) pre = pre.right;
return pre;
}
private void printReverse(TreeNode leftNode, TreeNode pre, List<Integer> res) {
reverse(leftNode, pre);
TreeNode node = pre;
res.add(node.val);
while (node != leftNode) {
node = node.right;
res.add(node.val);
}
reverse(pre, leftNode);
}
private void reverse(TreeNode node1, TreeNode node2) {
if (node1 == node2) return;
TreeNode x = node1;
TreeNode y = node1.right;
while (x != node2) {
TreeNode temp = y.right;
y.right = x;
x = y;
y = temp;
}
}
Morris后序遍历要复杂一些:
后序遍历要求先左再右最后根结点;
因此,采用在回到根结点后,再对根结点从 该根结点的前序结点到该根结点的左子树遍历
因为在后序遍历中先访问右子树中的结点,再回到其上一层的根结点;
因此在这里,先将从左子结点到前序结点这一条一直向右的边的方向反转,方便遍历;遍历完成后再将其反转回去;
要到再次回到根结点后才能对其左子树的那条最右的边进行遍历,因此最后整棵树的最右那条边将不能被访问到;
因此,建立一个虚根结点,将整棵树作为它的左子树,这样,当再次回到虚根结点后,就可以对这棵树的最右那条边进行访问了。