题目描述: 给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
示例1:
1 | 输入:root = [1,null,2,3] |
示例2:
1 | 输入:root = [] |
示例3:
1 | 输入:root = [1] |
示例4:
1 | 输入:root = [1,2] |
示例5:
1 | 输入:root = [1,null,2] |
题解方法一:递归法
解题思路:二叉树的前序遍历:按照访问根节点——左子树——右子树的方式遍历这棵树,而在访问左子树或者右子树的时候,我们按照同样的方式遍历,直到遍历完整棵树。因此整个遍历过程天然具有递归的性质,我们可以直接用递归函数来模拟这一过程。
代码实现如下:1
2
3
4
5
6
7
8
9
10
11var preorderTraversal = (root) => {
let arr = []
preorder(root, arr);
return arr
}
var preorder = (root, arr) => {
if (root === null) return
arr.push(root.val);
preorder(root.left, arr)
preorder(root.right, arr)
}
题解方法二:迭代
解题思路:我们也可以用迭代的方式实现方法一的递归函数,两种方式是等价的,区别在于递归的时候隐式地维护了一个栈,而我们在迭代的时候需要显式地将这个栈模拟出来,其余的实现与细节都相同。
代码实现如下:1
2
3
4
5
6
7
8
9
10
11
12var preorderTraversal = function(root) {
if (!root) return []
const stack = [], res = []
stack.push(root)
while (stack.length) {
const curr = stack.pop()
res.push(curr.val)
if (curr.right) stack.push(curr.right)
if (curr.left) stack.push(curr.left)
}
return res
};
下面进行图解分析:
1 | stack.push(root) |
stack.length不为0 ,进入while循环
执行第一次循环之后:
执行第二次循环之后:
执行第3次循环之后:
执行第4次循环之后:
执行第5次循环之后:
执行第6次循环之后:
结束循环,执行完毕!!
题解方法三:Morris遍历 !!没看懂
解题思路:Morris遍历的详细解释+注释版
一些前置知识:
前驱节点,如果按照中序遍历访问树,访问的结果为ABC,则称A为B的前驱节点,B为C的前驱节点。
前驱节点pre是curr左子树的最右子树(按照中序遍历走一遍就知道了)。
由此可知,前驱节点的右子节点一定为空。
主要思想:
树的链接是单向的,从根节点出发,只有通往子节点的单向路程。
中序遍历迭代法的难点就在于,需要先访问当前节点的左子树,才能访问当前节点。
但是只有通往左子树的单向路程,而没有回程路,因此无法进行下去,除非用额外的数据结构记录下回程的路。
在这里可以利用当前节点的前驱节点,建立回程的路,也不需要消耗额外的空间。
根据前置知识的分析,当前节点的前驱节点的右子节点是为空的,因此可以用其保存回程的路。
但是要注意,这是建立在破坏了树的结构的基础上的,因此我们最后还有一步“消除链接”’的步骤,将树的结构还原。
重点过程: 当遍历到当前节点curr时,使用cuur的前驱节点pre
标记当前节点是否访问过
记录回溯到curr的路径(访问完pre以后,就应该访问curr了)
以下为我们访问curr节点需要做的事儿:
访问curr的节点时候,先找其前驱节点pre
找到前驱节点pre以后,我们根据其右指针的值,来判断curr的访问状态:
pre的右子节点为空,说明curr第一次访问,其左子树还没有访问,此时我们应该将其指向curr,并访问curr的左子树
pre的右子节点指向curr,那么说明这是第二次访问curr了,也就是说其左子树已经访问完了,此时将curr.val加入结果集中
更加细节的逻辑请参考代码:
1 | public List<Integer> method3(TreeNode root) { |
参考地址: