前序遍历
栈
前序遍历按照 根节点 左子树 右子树 的顺序进行遍历。
由于根节点先遍历,因此每次将节点出栈时,按从右到左的顺序将子节点全部入栈,就可以保证按从左到右的顺序遍历所有子树。即前序遍历
这种方法可以推广到k叉树的前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List ans = new ArrayList();
if (root == null) return ans;
TreeNode[] stack = new TreeNode[100];
int top = 0;
stack[0] = root;
TreeNode now = null;
while (top != -1) {
now = stack[top--];
ans.add(now.val);
if (now.right != null) stack[++top] = now.right;
if (now.left != null) stack[++top] = now.left;
}
return ans;
}
}
Morris
- 此处
isLeftTraversed
方法和中序遍历完全相同,需要注意的是,root
不可能为空且root.left == null
的情况需要在函数体外单独考虑 - 由于前序遍历先遍历根节点,因此如果左子树为空,或者左子树不空但是没有遍历过,则认为是第一次到达该节点,此时应将该节点加入答案。如果左子树不空且遍历过了,则是第二次到达该节点,不需要将该节点加入答案。
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List ans = new ArrayList();
if (root == null) return ans;
while (root != null) {
if (root.left == null || !isLeftTraversed(root)) { //左子树为空或左子树没有遍历过,自身加入答案
ans.add(root.val);
if (root.left == null) root = root.right;
else root = root.left;
} else { //左边不空且左边遍历过了
root = root.right;
}
}
return ans;
}
public boolean isLeftTraversed(TreeNode root) {
TreeNode pre = root.left;
while (pre.right != null) {
if (pre.right == root) {
pre.right = null;
return true;
}
pre = pre.right;
}
pre.right = root;
return false;
}
}
中序遍历
栈
自己画个图模拟一下出栈入栈可能更好理解。
只需要保证每次从栈中取出栈顶时,栈顶的左子树都遍历完了即可。怎么保证呢?
从当前节点出发,一直向左走,将所有的左节点都入栈,直到没有左节点了为止(即当前节点为空),此时弹出栈顶元素(元素一定没有左节点),将当前元素指向该元素的右节点(若无右节点,则又会弹出栈顶,此时这个栈顶元素有左节点,但是左子树已经全部遍历完了)。重复上述操作,可以证明就是中序遍历。
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List ans = new ArrayList();
TreeNode[] stack = new TreeNode[101];
TreeNode point = root;
int top = -1;
while (top != -1 || point != null) {
if (point == null) { //当前为空,则弹出栈顶,并将当前节点指向栈顶的右节点(可能是左边走到头了,也可能是右子树为空)
TreeNode stackTop = stack[top--];
ans.add(stackTop.val);
point = stackTop.right;
} else { //当前不为空,则入栈,并更改当前节点为左节点(一路向左,将所有左节点全部入栈)
stack[++top] = point;
point = point.left;
}
}
return ans;
}
}
Morris
- 当没有遍历
root
的左子树时,让root
的前驱的右节点指向root
,是为了遍历完左子树能够继续回到root
。 - 当遍历完了
root
的左子树时断链,是为了保证不改变树结构。 - 由于中序遍历先遍历左节点,因此需要在左节点为空时、或者第二次到达该节点(即左节点不空但是遍历过了)时将自身加入答案
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List ans = new ArrayList();
while (root != null) {
if (root.left == null || isLeftTraversed(root)) { //左子树遍历过了(无左子树认为遍历过了左子树),则开始遍历右边(没遍历完右边不可能为空)
ans.add(root.val);
root = root.right;
} else { //有左子树且没有遍历过,开始遍历左边(此时root的前驱的右边已经指向root了)
root = root.left;
}
}
return ans;
}
//在判断是否遍历过左子树的同时,改变root前驱节点的指向
public boolean isLeftTraversed(TreeNode root) {
TreeNode pre = root.left;
while (pre.right != null) {
if (pre.right == root) {
pre.right = null;
return true;
}
pre = pre.right;
}
pre.right = root;
return false;
}
}
后序遍历
栈
后续遍历根节点最后出栈,因此只需要保存刚刚出栈的节点,如果是当前节点的左节点,则认为遍历完了左子树了。右子树同理。
对于任意一个节点,都有以下判断:
1)若左右子树都遍历完了,则出栈当前节点
2)若左子树遍历完了右子树没有,则右节点出栈
3)若左右子树都没遍历,则左节点入栈。
这种方法可以扩展到k叉树的后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List ans = new ArrayList();
if (root == null) return ans;
TreeNode[] stack = new TreeNode[100];
int top = 0;
stack[0] = root;
TreeNode poped = null; //保存刚刚出栈的节点。后续遍历根节点最后出栈,如果root.left == poped则认为左子树遍历完了,右子树同理
TreeNode now;
while (top != -1) {
now = stack[top];
//右子树为空且左子树为空、右子树为空且左子树遍历完了、右子树不空且右子树遍历完了。以上三种情况则认为左右子树都遍历完了
if ((now.right == null && (now.left == null || poped == now.left)) || (poped == now.right && now.right != null)) { //左右子树都遍历完了
ans.add(now.val);
poped = now;
top--;
} else if (now.left == null || poped == now.left) { //左子树为空或左子树遍历完了。
stack[++top] = now.right; //此时右子树一定不空,否则进入第一个条件里面
} else { //左子树不为空且左子树还没有遍历
stack[++top] = now.left;
}
}
return ans;
}
}
Morris
后序遍历其实就是斜着从左下向右上遍历,明白了这一点也就明白了为什么要倒序从root.left
输出到pre
,其实就是斜着输出,而且输出这一条支线的时候,可以证明该支线的所有左下支线都已经输出过了。
对于任何一个节点,都有如下判断:
1)如果当前节点的左子节点为空,则遍历当前节点的右子节点;
2)如果当前节点的左子节点不为空且还未遍历,则遍历左子树
3)如果当前节点的左子节点不为空且当前节点的前驱的右节点指向自己,则从下往上,输出该节点的中序前驱到当前节点的左子节点。并开始遍历右子节点
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
TreeNode orgRoot = root; //记录根节点
List<Integer> ans = new ArrayList();
if (root == null) return ans;
while (root != null) {
if (root.left == null) {
root = root.right;
} else if (!isLeftTraversed(root, ans)) { //左边没遍历过,进入左边
root = root.left;
} else { //左边遍历过
//root.left一路向右倒序遍历
reverseOutput(root.left, ans);
root = root.right;
}
}
reverseOutput(orgRoot, ans);
return ans;
}
public void reverseOutput(TreeNode start, List<Integer> ans) { //从start开始倒序输出
List<Integer> tmp = new ArrayList();
while (start != null) {
tmp.add(start.val);
start = start.right;
}
Collections.reverse(tmp);
ans.addAll(tmp);
}
public boolean isLeftTraversed(TreeNode root, List<Integer> ans) {
TreeNode pre = root.left;
while (pre.right != null) {
if (pre.right == root) {
pre.right = null;
return true;
}
pre = pre.right;
}
pre.right = root;
return false;
}
}
层序遍历
队列
先将根节点入队
每次依次将队头节点的左右子节点入队,直到队空为止。即可实现层序遍历。
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans = new ArrayList();
if (root == null) return ans;
Queue<TreeNode> queue = new LinkedList();
queue.add(root);
int nowLevelCount = 1;
int nextLevelCount = 0;
while (!queue.isEmpty()) {
List<Integer> nowLevel = new ArrayList<>();
while (nowLevelCount != 0) {
nowLevelCount--;
TreeNode nowNode = queue.remove();
if (nowNode.left != null) {
queue.add(nowNode.left);
nextLevelCount++;
}
if (nowNode.right != null) {
queue.add(nowNode.right);
nextLevelCount++;
}
nowLevel.add(nowNode.val);
}
ans.add(nowLevel);
nowLevelCount = nextLevelCount;
nextLevelCount = 0;
}
return ans;
}
}
总结
Morris的三种遍历方式以中序遍历为基础,看懂了中序遍历就比较简单了。三种遍历方式都基于isLeftTraversed
方法,该方法在三种遍历下完全相同,在判断左子树是否遍历过的同时,维护树的结构,这样主函数的判断逻辑就非常简单了。
关于Morris时间复杂度为O(n)
的理解:可以证明,Morris遍历中树的每条边最多遍历三次,每个节点最多遍历两次。因此最坏情况下时间复杂度依然是O(n)
。可以画一个完全二叉树理解一下。