二叉树的前、中、后序遍历
方法一:递归
-
前序遍历
var preorderTraversal = function(root) { const res = [] const preorder = node => { if(node){ res.push(node.val) preorder(node.left) preorder(node.right) } } preorder(root) return res }; -
中序遍历
var inorderTraversal = function(root) { const res = [] const inorder = node => { if(node){ inorder(node.left) res.push(node.val) inorder(node.right) } } inorder(root) return res }; -
后序遍历
var postorderTraversal = function(root) { const res = [] const postorder = node => { if(node){ postorder(node.left) postorder(node.right) res.push(node.val) } } postorder(root) return res };
方法二:迭代
-
前序遍历
var preorderTraversal = function(root) { const res = [] const stack = [] let p = root while(p || stack.length){ if(p){ // 在第一次遍历到该节点时,对该节点进行访问 res.push(p.val) stack.push(p) p = p.left }else{ p = stack.pop() p = p.right } } return res }; -
中序遍历
var inorderTraversal = function(root) { const res = [] const stack = [] let p = root while(p || stack.length){ if(p){ stack.push(p) p = p.left }else{ p = stack.pop() // 在第二次遍历到该节点时,对该节点进行访问 res.push(p.val) p = p.right } } return res }; -
后序遍历
var postorderTraversal = function(root) { const res = [] const stack = [] let p = root let r = null // 定义一个指针,指向最近访问过的节点 while(p || stack.length){ if(p){ stack.push(p) p = p.left }else{ // 读取栈顶元素 p = stack[stack.length - 1] if(!p.right || p.right === r){ // 若右子树不存在,或者已经被访问过,则从栈中弹出节点并访问 p = stack.pop() res.push(p.val) // 记录最近访问过的节点 r = p // 节点访问完后,重置p指针 p = null }else{ // 否则,转向右 p = p.right } } } return res };我们知道,后序遍历中节点的访问顺序是『左右根』,而前序遍历是『根左右』。所以我们也可以这样做:将前序遍历修改为『根右左』,然后将结果反转即可。下面是代码实现:
var postorderTraversal = function(root) { const res = [] const stack = [] let p = root while(p || stack.length){ if(p){ res.push(p.val) stack.push(p) p = p.right }else{ p = stack.pop() p = p.left } } return res.reverse() };
方法三:标记法(参考力扣第 94 题的题解——颜色标记法)
这种方法的优点在于对于前、中、后序遍历,能够写成完全一致的代码。其核心步骤如下:
- 根节点入栈
- 每次从栈中弹出一个节点,记为 top,判断该节点有没有被标记过:
- 如果 top 被标记过,则直接访问该节点
- 如果 top 没有被标记过,则标记该节点,并以遍历顺序的逆序入栈(因为对于栈这种数据结构,是后进先出的)。比如:对于前序遍历,由于其遍历顺序是『根左右』,那么入栈顺序就是『右左根』。中序遍历和后序遍历以此类推
注意:为了节省空间,我这里不创建额外的空间来保存一个节点是否被标记过,而是使用这种方式:对于不标记的结点,是将该节点(包括节点的值和左右指针)压栈;而对于需要标记的节点,只将节点的值压栈。下面是代码实现:
-
前序遍历
var preorderTraversal = function(root) { const res = [] const stack = [] // 根节点入栈 root && stack.push(root) // 栈不为空时循环 while(stack.length){ // 弹出栈顶元素,判断其是否被标记过(一个节点或是一个原始值) const top = stack.pop() if(top instanceof TreeNode){ // 未被标记过,将右子节点、左子节点和自身的值入栈 top.right && stack.push(top.right) top.left && stack.push(top.left) stack.push(top.val) }else{ // 被标记过,加入结果集 res.push(top) } } return res }; -
中序遍历
var inorderTraversal = function(root) { const res = [] const stack = [] // 根节点入栈 root && stack.push(root) // 栈不为空时循环 while(stack.length){ // 弹出栈顶元素,判断其是否被标记过(一个节点或是一个原始值) const top = stack.pop() if(top instanceof TreeNode){ // 未被标记过,将右子节点、自身的值和左子节点入栈 top.right && stack.push(top.right) stack.push(top.val) top.left && stack.push(top.left) }else{ // 被标记过,加入结果集 res.push(top) } } return res }; -
后序遍历
var postorderTraversal = function(root) { const res = [] const stack = [] // 根节点入栈 root && stack.push(root) // 栈不为空时循环 while(stack.length){ // 弹出栈顶元素,判断其是否被标记过(一个节点或是一个原始值) const top = stack.pop() if(top instanceof TreeNode){ // 未被标记过,将自身的值、右子节点和左子节点入栈 stack.push(top.val) top.right && stack.push(top.right) top.left && stack.push(top.left) }else{ // 被标记过,加入结果集 res.push(top) } } return res };
方法四:Morris 遍历
Morris 遍历算法是一种优秀的遍历算法,能将空间复杂度降到 O(1)。其整体步骤如下(假设当前遍历到的节点为 x):
- 如果 x 无左子树,则遍历 x 的右子树,即
x = x.right - 如果 x 有左子树,则找到 x 在中序遍历下的前驱节点,即左子树的最右节点,记为 pre。根据 pre 的右孩子是否为空,进行如下操作:
- 如果 pre 的右孩子为空,则将其右孩子指向 x,然后遍历 x 的左子树,即
x = x.left - 如果 pre 的右孩子不为空,则此时其右孩子肯定指向 x,说明我们已经遍历完 x 的左子树。我们需要将 pre 的右孩子置空,然后继续遍历 x 的右子树,即
x = x.right
- 如果 pre 的右孩子为空,则将其右孩子指向 x,然后遍历 x 的左子树,即
下面是 Morris 遍历的代码实现:
var traversal = function(root) {
let p = root // 指向当前节点
let pre = null // 指向当前节点在中序遍历下的前驱节点,即左子树的最右节点
while(p){
if(!p.left){
// 当前节点没有左子树,继续遍历右子树
p = p.right
}else{
// 找到当前节点在中序遍历下的前驱节点
pre = p.left
while(pre.right && pre.right !== p){
pre = pre.right
}
if(!pre.right){
// 第一次到该节点,当前节点的左子树还未被遍历,让前驱节点的右指针指向当前节点,继续遍历左子树
pre.right = p
p = p.left
}else{
// 第二次到该节点,当前节点的左子树已经遍历完了,断开连接,继续遍历右子树
pre.right = null
p = p.right
}
}
}
};
可以发现:对于没有左子树的节点,只会到该节点一次。而对于有左子树的节点,会到该节点两次:第一次到该节点时,需要找到左子树的最右节点,然后将其右指针指向当前节点,继续遍历左子树;第二次到该节点时,说明当前节点的左子树已经遍历完了,应该将当前节点的前驱节点的右指针指向 null,转而遍历其右子树。对于这样一颗二叉树:[1, 2, 3, 4, 5, 6, null],其 Morris 序列为1 2 4 2 5 1 3 6 3。其中节点4 5 6只会到该节点一次,而节点1 2 3会到该节点两次。下面我们讨论如何使用 Morris 算法对二叉树进行前、中、后序遍历:
- 前序遍历:对于二叉树
[1, 2, 3, 4, 5, 6, null],其前序遍历序列为1 2 4 5 3 6,可以发现,这在 Morris 序列中,对每个节点的访问时间点为:对于只会到一次的节点,遍历到该节点时就访问;对于会到两次的节点,在第一次到该节点时访问 - 中序遍历:对于二叉树
[1, 2, 3, 4, 5, 6, null],其中序遍历序列为4 2 5 1 6 3,可以发现,这在 Morris 序列中,对每个节点的访问时间点为:对于只会到一次的节点,遍历到该节点时就访问;对于会到两次的节点,在第二次到该节点时访问 - 后序遍历:对于二叉树
[1, 2, 3, 4, 5, 6, null],其后序遍历序列为4 5 2 6 3 1,可以发现,这在 Morris 序列中,无法直接找到这样的序列。我们可以这样做:对于会到两次的节点,在第二次到该节点时,逆序访问其左子树的右边缘。遍历结束后,再逆序访问整棵树的右边缘。在这个例子中,我们定义一个结果集res = [],第二次到节点 2 时,逆序访问其左子树的右边缘,即4,此时res = [4];第二次到节点 1 时,逆序访问其左子树的右边缘,即5 2,此时res = [4 5 2];第二次到节点 3 时,逆序访问其左子树的右边缘,即6,此时res = [4 5 2 6]。最后逆序访问整棵树的右边缘,即3 1,此时结果集就是最终的后序遍历序列4 5 2 6 3 1
具体代码实现如下:
-
前序遍历
var preorderTraversal = function(root) { const res = [] let p = root let pre = null while(p){ if(!p.left){ // 只会到一次的节点,遍历到该节点时就访问 res.push(p.val) p = p.right }else{ pre = p.left while(pre.right && pre.right !== p){ pre = pre.right } if(!pre.right){ // 会到两次的节点,在第一次到该节点时访问 res.push(p.val) pre.right = p p = p.left }else{ pre.right = null p = p.right } } } return res }; -
中序遍历
var inorderTraversal = function(root) { const res = [] let p = root let pre = null while(p){ if(!p.left){ // 只会到一次的节点,遍历到该节点时就访问 res.push(p.val) p = p.right }else{ pre = p.left while(pre.right && pre.right !== p){ pre = pre.right } if(!pre.right){ pre.right = p p = p.left }else{ // 会到两次的节点,在第二次到该节点时访问 res.push(p.val) pre.right = null p = p.right } } } return res }; -
后序遍历
let res; // 该函数传入一棵二叉树的根节点,将该树的右边缘(可以看成是一个单链表)进行反转 var reverse = function(root){ let cur = root, pre = null while(cur){ const right = cur.right cur.right = pre pre = cur cur = right } return pre } // 该函数传入一颗二叉树的根节点,逆序访问其右边缘 var visitRight = function(root){ const tail = reverse(root) let p = tail while(p){ res.push(p.val) p = p.right } // 为了不破坏原本树的结构,反转右边缘后还需要进行恢复 reverse(tail) } var postorderTraversal = function(root) { res = [] let p = root let pre = null while(p){ if(!p.left){ p = p.right }else{ pre = p.left while(pre.right && pre.right !== p){ pre = pre.right } if(!pre.right){ pre.right = p p = p.left }else{ pre.right = null // 对于会到两次的节点,第二次到该节点时,逆序访问其左子树的右边缘 visitRight(p.left) p = p.right } } } // 遍历完成,逆序访问整棵树的右边缘 visitRight(root) return res };