算法学习:二叉树遍历那些事(递归,迭代,Morris和层序)

(一)二叉树的递归遍历

利用系统执行函数调用时的压栈弹栈来实现深度遍历二叉树,二叉树的递归版本较为简单。实现先序、中序、后序只需要在执行子函数的前、中、后打印树的信息即可。代码如下:

先序

function iterator (tree) {
    if (!tree) {
        return
    }
    console.log(tree.val)
    iterator(tree.left)
    iterator(tree.right)
}
复制代码

中序

function iterator (tree) {
    if (!tree) {
        return
    }
    iterator(tree.left)
    console.log(tree.val)
    iterator(tree.right)
}
复制代码

后序

function iterator (tree) {
    if (!tree) {
        return
    }
    iterator(tree.left)
    iterator(tree.right)
    console.log(tree.val)
}
复制代码

总结:时间复杂度:O(n),空间复杂度:O(h),h是树的高度

(二)二叉树的迭代遍历

先序

二叉树的先序遍历( 头 => 左 => 右),用迭代实现。思路:

  1. 使用一个栈,存入tree节点
  2. 不断的从栈中弹出一个节点,打印节点(遇到头节点即打印,再操作子节点,即为先序遍历,顺序为: 头 => 左 => 右)
  3. 依次向栈中添加弹出节点的右节点、左节点(因为是栈,后进先出,所以要先添加右子节点,后添加左子节点。下次循环会先打印左子节点, 后打印右节点)
// 先序遍历 (头 左 右)
function iterator (tree) {
    let stack = [tree]
    while (stack.length) {
        tree = stack.pop()
        console.log(tree.val)
        tree.right && (stack.push(tree.right))
        tree.left && (stack.push(tree.left))
    }
}
复制代码

中序

二叉树的后序遍历(左 => 头 => 右),用迭代实现。思路:

  1. 依次向栈中添加tree的所有左子节点,直到遍历到叶子节点
  2. 弹出节点并打印
  3. 指针右移,继续向栈中添加弹出节点的所有左子节点

47fff35dd3fd640ba60349c78b85242ae8f4b850f06a282cd7e92c91e6eff406-1.gif

// 中序遍历 (左 头 右)
function iterator (tree) {
    let stack = [tree]
    while (stack.length) {
        if (tree.left) {
            stack.push(tree.left)
            tree = tree.left
            continue
        }
        tree = stack.pop()
        console.log(tree.val)
        if (tree.right) {
            tree = tree.right
            stack.push(tree)
        }
    }
}
复制代码

后序

二叉树的后序遍历(左 => 右 => 头),用迭代实现,需要额外一个辅助栈。思路:

  1. 使用一个栈,存入tree节点
  2. 不断的从栈中弹出一个节点,添加到辅助栈中(辅助栈中先添加头节点)
  3. 依次向栈中添加弹出节点的左节点、右节点(栈后进先出,再下两次循环中,辅助栈中会依次添加右节点,左节点)
  4. 遍历辅助栈(辅助栈中,添加的顺序是 头=> 右 => 左,栈后进先出,即遍历的顺序是左 => 右 => 头)
function iterator (tree) {
    let stack = [tree]
    let helpStack = []
    while (stack.length) {
        tree = stack.pop()
        helpStack.push(tree)
        if (tree.left) {
            stack.push(tree.left)
        }
        if (tree.right) {
            stack.push(tree.right)
        }
    }
    while (helpStack.length) {
        tree = helpStack.pop()
        console.log(tree.val)
    }
}
复制代码

总结:先序,中序同递归都是深度遍历,时间复杂度:O(n),空间复杂度:O(h),h是树的高度,后序因为有辅助栈,空间复杂度是O(n)

(三)广度优先遍历二叉树

层序遍历

二叉树的层序遍历,用迭代实现,思路:

  1. 使用一个队列,存入tree节点
  2. 不断的从队列中弹出一个节点,并打印
  3. 依次向队列中添加弹出节点的左节点、右节点
// 层序遍历
function iterator(tree) {
    let queue = [tree]
    while (queue.length) {
        tree = queue.shift()
        console.log(tree.val)
        tree.left && queue.push(tree.left)
        tree.right && queue.push(tree.right)
    }
}
复制代码

(四)Morris遍历二叉树

morris 遍历

morris遍历的特点,用树的叶子节点(左右指针都是null)的空闲节点,遍历的过程中,改写叶子的空闲节点来控制遍历的走向,可以做到空间复杂度为O(1),时间复杂度为O(n)。

  1. 如果tree无左节点,tree向右移动(tree=tree.right)
  2. 如果tree有左节点,找到tree左子树上最右的节点,记为mostright
    • 如果mostright的right指针指向空,让其指向cur,cur向左移动(tree=tree.left)
    • 如果mostright的right指针指向cur,让其指向空,cur向右移动(tree=tree.right)

  解释一下为什么递归和非递归遍历需要用到栈。递归写法中虽然没有显式用到栈,但是本质上还是用栈实现的。
 因为二叉树中只有父节点指向子节点的指针,遍历的过程中,如果想从子节点回到父节点怎么办,这里使用栈这种数据结构,先把父节点入栈,访问子节点,访问完成后,父节点出栈,就相当于从子节点回到了父节点
  Morris遍历也需要遍历整棵二叉树,那么Morris遍历中是如何做到从子节点回到父节点的呢。从Morris遍历的规则可以知道,Morris遍历是通过mostRight的右孩子的指向来从子节点回到父节点的

4cc6b86e073841e2b89bd013cddc34f9.gif

function iterator(tree) {
    let mostRight = null
    while (tree) {
        if (tree.left) {
            mostRight = tree.left;
            while (mostRight.right && mostRight.right !== tree) {
                mostRight = mostRight.right;
            }
            if (!mostRight.right) {
                // 修改叶节点的right指针指向自己
                mostRight.right = tree
                tree = tree.left
                continue
            }
             // 被修改节点的right指针调整为原来的null
            mostRight.right = null
        }
        tree = tree.right
    }
}
复制代码

morris遍历过程中,对有左子节点的节点会遍历两次,对于没有左节点的节点只会遍历一次。根据这个特性可以改写先序,中序和后序。

morris先序

先序:对于没有左节点的节点,只会遍历到一次,则遍历到该节点即打印。对于对有左子节点的节点会遍历两次,取第一次遍历时机打印(该节点的左树最右节点为空)。

function iterator(tree) {
    let mostRight = null
    while (tree) {
        if (tree.left) {
            mostRight = tree.left;
            while (mostRight.right && mostRight.right !== tree) {
                mostRight = mostRight.right;
            }
            if (!mostRight.right) {
                console.log(tree.val)
                mostRight.right = tree
                tree = tree.left
                continue
            }
            mostRight.right = null
        } else {
            console.log(tree.val)
        }
        tree = tree.right
    }
}
复制代码

morris中序

中序:对于没有左节点的节点,只会遍历到一次,则遍历到该节点即打印。对于对有左子节点的节点会遍历两次,取第二次遍历时机打印(该节点的左树最右节点指向自己)。

// morris 中序遍历
function iterator(tree) {
    let mostRight = null
    while (tree) {
        if (tree.left) {
            mostRight = tree.left;
            while (mostRight.right && mostRight.right !== tree) {
                mostRight = mostRight.right;
            }
            if (!mostRight.right) {
                mostRight.right = tree
                tree = tree.left
                continue
            } else {
                console.log(tree.val)
                mostRight.right = null
            }
        } else {
            console.log(tree.val)
        }
        tree = tree.right
    }
}
复制代码

morris后序

后序:

  1. 对于没有左子树的节点,这些节点只会遍历一次,不用关心这些节点。
  2. 对于含有左子树的节点,这些节点会被遍历两次,在第二次遍历的时候逆序打印左子树的右边界。
  3. 最后在函数退出之前再单独逆序打印整棵树的右边界。

为了做到不适用额外空间的做法做到逆序打印树的右边界(额外空间的做法:用栈收集右边界,再打印栈), 可以联想到类似链表的翻转,对节点的right指针翻转之后,即可以做到逆序打印,再把指向调整回来即可。如图:

20201222134317196.jpg

// morris 后序遍历
function iterator(tree) {
    // 翻转树(以right指针翻转)
    function reverseTree(tree) {
        let pre = null
        let next = null
        let cur = tree
        while (cur) {
            next = cur.right
            cur.right = pre
            pre = cur
            cur = next
        }
        return pre
    }
    
    function operateTree (tree) {
        let cur
        // 先翻转右边界
        tree = reverseTree(tree)
        cur = tree
        // 打印翻转后的节点
        while (cur) {
            console.log(cur.val)
            cur = cur.right
        }
        // 翻转tree,调整为原来的顺序
        reverseTree(tree)
    }
    let cur = tree
    let mostRight = null
  
    while (tree) {
        if (tree.left) {
            mostRight = tree.left;
            while (mostRight.right && mostRight.right !== tree) {
                mostRight = mostRight.right;
            }
            if (!mostRight.right) {
                mostRight.right = tree
                tree = tree.left
                continue
            } else {
                mostRight.right = null
                operateTree(tree.left)
            }
        }
        tree = tree.right
    }
    operateTree(cur)
}
复制代码

Guess you like

Origin juejin.im/post/7034866672020389924