https://leetcode.com/problems/binary-tree-inorder-traversal/
解题思路:
这道题是中序遍历一棵二叉树。
首先回顾一下二叉树的遍历:
可以简单分为两种方法,深度优先遍历和广度优先遍历。
深度优先遍历:L、D、R分别表示遍历左子树、访问根结点和遍历右子树。
- 先序遍历(preorder traversal)二叉树的顺序是DLR,
- 中序遍历(inorder traversal)二叉树的顺序是LDR,
- 后序遍历(postorder traversal)二叉树的顺序是LRD。
广度优先遍历:又称为层序遍历(level traversal)二叉树,顾名思义,一层一层的遍历二叉树。
这些方法的时间复杂度都是O(n),n为结点个数。
根据中序遍历的定义,很容易推出中序遍历一棵二叉树的顺序:
- 遍历左子树
- 遍历根节点,并记录根节点的值。
- 遍历右子树
因此,用递归可以简单实现这道题。需要遍历整棵树一次,所以时间复杂度是 O(n)。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
inorder(root, result);
return result;
}
public void inorder(TreeNode root, List<Integer> result) {
if (root == null) return ;
inorder(root.left, result);
result.add(root.val);
inorder(root.right, result);
}
}
有递归就有迭代。下面同样用迭代的方式实现一下。
例如上面这棵二叉树,首先分析一下递归调用的顺序:
首先递归到左子树的左边叶子节点 D,它没有左子节点,接着记录节点 D 的值,接着递归 D 的右子节点,发现它也没有右子节点所以再返回到节点 B。记录节点 B 的值,并递归 B 的右子节点 E,从 E 开始又访问 E 的左右子树。即:相当于把 E 看成了新一轮的根节点。遍历完并记录完 E 的左右子树后,也就遍历完了 B 的右子树,就可以返回到节点 A 了,至此已经访问完毕 A 的左子树,以同样的方式再访问 A 的右子树。
可以发现,如果我们维护一个栈,将左边的节点(A,B,D)都入栈。接着一个一个弹出,记录节点的值,接着将它们的右子节点当做新一轮循环的根节点,再将左边的节点入栈。以此类推,就可以达到和递归一样的效果。
说起来很拗口,看一下代码吧~
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) return result;
Stack<TreeNode> stack = new Stack<>();
while (root != null || !stack.isEmpty()) {
if (root != null) {
stack.push(root);
root = root.left;
} else {
root = stack.pop();
result.add(root.val);
root = root.right;
}
}
return result;
}
}
同样,上面的代码可以在空间上优化一下。用到了 Morris Traversal 的方法。这个方法是基于二叉线索树的。
首先回顾一下二叉线索树:
“一个二叉树通过如下的方法“穿起来”:所有应该为空的右孩子指针指向该节点在中序序列中的后继,所有应该为空的左孩子指针指向该节点的中序序列的前驱。”
看下面这张图就明白了,图片来自WiKipedia。
以节点 E 为例,节点 E 的左右子节点本都为空。在二叉线索树中,它的左节点指向了 D,即中序遍历中的前驱结点;右节点指向了 F,即中序遍历中的后继结点。
有了二叉线索树的概念,可以更好地说明 Morris Traversal 这个方法了。在 Morris Traversal 方法中,将叶子节点的右指针与它中序遍历的后即结点建立链接,以便返回节点进行后续遍历。当遍历完毕后再解除绑定,使二叉树恢复原样。
Morris Traversal 的代码如下:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) return result;
TreeNode curr = root;
while (curr != null) {
if (curr.left == null) {
result.add(curr.val);
curr = curr.right;
} else {
TreeNode prev = curr.left;
while (prev.right != null && prev.right != curr)
prev = prev.right;
if (prev.right == null) {
prev.right = curr;
curr = curr.left;
} else {
prev.right = null;
result.add(curr.val);
curr = curr.right;
}
}
}
return result;
}
}
https://www.youtube.com/watch?v=wGXB9OWhPTg 这个视频有对 Morris Traversal 比较详细的解读~