文章目录
- 题目
- 一、二叉树的节点定义
- 二、三种遍历方法
- 1.递归
- 算法思想
- 2.迭代
- 算法思想
- 3.Morris 中序遍历
- 算法思想
- 总结
题目
给定一个二叉树的根节点 root ,返回它的 中序 遍历
一、二叉树的节点定义
public class TreeNode {int val;TreeNode left;TreeNode right;TreeNode() {}TreeNode(int val) { this.val = val; }TreeNode(int val, TreeNode left, TreeNode right) {this.val = val;this.left = left;this.right = right;}}
二、三种遍历方法
1.递归
算法思想
我们都知道中序遍历就是先遍历节点的左子树节点,然后访问根节点,最后再遍历右子树。而在访问左子树或者右子树的时候我们按照同样的方式遍历,直到遍历完整棵树。因此整个遍历过程天然具有递归的性质。
代码如下:
class Solution {public List<Integer> inorderTraversal(TreeNode root) {List<Integer> list = new ArrayList<Integer>();inoder(root, list);return list;}// 递归遍历public void inoder(TreeNode root,List list){if(root == null){ //如果节点为null 这直接退出递归return;}inoder(root.left, list); // 访问左子树list.add(root.val); // 将节点的值存入到list中inoder(root.right,list); //访问右子树}}
复杂度分析:
-
时间复杂度:O(n)。其中 n 为二叉树节点的个数。二叉树的遍历中每个节点会被访问一次且只会被访问一次。
-
空间复杂度:O(n)。空间复杂度取决于递归的栈深度,而栈深度在二叉树为一条链的情况下会达到 O(n) 的级别。
2.迭代
算法思想
递归函数我们也可以用迭代的方式实现,两种方式是等价的,区别在于递归的时候隐式地维护了一个栈,而我们在迭代的时候需要显式地将这个栈模拟出来,其他都相同。
代码如下:
class Solution {// 迭代遍历 通过压栈和出栈来模拟递归过程 本质和上面的递归是一样的public List<Integer> inorderTraversal(TreeNode root) {List<Integer> res = new ArrayList<Integer>();Deque<TreeNode> stk = new LinkedList<TreeNode>(); //初始化栈while (root != null || !stk.isEmpty()) { // 如果节点为空或栈为空 则遍历结束while (root != null) { //如果根节点不为空stk.push(root); // 将根节点压栈root = root.left; //访问左子树}root = stk.pop(); // 存在节点为空 此时栈中从栈顶的元素一定为子树的最左边一个子节点res.add(root.val); // 将子节点存入列表中root = root.right; //访问右节点}return res;}
}
复杂度分析:
-
时间复杂度:O(n)。其中 n 为二叉树节点的个数。二叉树的遍历中每个节点会被访问一次且只会被访问一次。
-
空间复杂度:O(n)。空间复杂度取决于递归的栈深度,而栈深度在二叉树为一条链的情况下会达到 O(n) 的级别。
3.Morris 中序遍历
算法思想
Morris 遍历算法是另一种遍历二叉树的方法,它能将非递归的中序遍历空间复杂度降为 O(1)。
Morris 遍历算法整体步骤如下(假设当前遍历到的节点为 x):
- 如果 x 无左孩子,先将 x 的值加入答案数组,再访问 x 的右孩子,即 x=x.right。
- 如果 x 有左孩子,则找到 xx左子树上最右的节点(即左子树中序遍历的最后一个节点,x在中序遍历中的前驱节点),我们记为 predecessor。根据 predecessor 的右孩子是否为空,进行如下操作。
- 如果 predecessor 的右孩子为空,则将其右孩子指向 x,然后访问 x的左孩子,即 x=x.left。
- 如果 predecessor 的右孩子不为空,则此时其右孩子指向 x,说明我们已经遍历完 x 的左子树,我们将 predecessor 的右孩子置空,将 x 的值加入答案数组,然后访问 x 的右孩子,即 x = x.right。
重复上述操作,直至访问完整棵树
代码如下:
class Solution {public List<Integer> inorderTraversal(TreeNode root) {List<Integer> res = new ArrayList<Integer>();TreeNode predecessor = null;while (root != null) {if (root.left != null) {// predecessor 节点就是当前 root 节点向左走一步,然后一直向右走至无法走为止predecessor = root.left;while (predecessor.right != null && predecessor.right != root) {predecessor = predecessor.right;}// 让 predecessor 的右指针指向 root,继续遍历左子树if (predecessor.right == null) {predecessor.right = root;root = root.left;}// 说明左子树已经访问完了,我们需要断开链接else {res.add(root.val);predecessor.right = null;root = root.right;}}// 如果没有左孩子,则直接访问右孩子else {res.add(root.val);root = root.right;}}return res;}
}
大概执行过程:
复杂度分析
-
时间复杂度:O(n)。其中 n 为二叉搜索树的节点个数。Morris 遍历中每个节点会被访问两次,因此总时间复杂度为 O(2n)=O(n)。
-
空间复杂度:O(1)。
总结
本篇文章主要就是说明了三种实现二叉树中序遍历的方法,并且对其时间和空间复杂度有分析了一遍,最后morris算法总体来说是最高效的一个。