由浅入深:二叉树遍历全面解析(递归与非递归)

1,157 阅读9分钟

树的概念

通过以下例子介绍树的概念。

      A           
    / | \  
   B  C  D
 / \ / \ / \
E  FG  HI   J	

树作为一种基本的数据结构,其本质就是n个结点的集合,其中n的值是不小于0的,当n为0时被称为空树。在非空树的结构中有且只有一个结点被称作根结点,并且非空树中可以划分出若干个没有直接关联的子集合,这些子集合被称为树的子树。

树的特性定义:

  1. 结点:结点是树的组成成分,在树中存在着多种结点,其中有根结点、叶子结点、父结点和子结点。
    • 根结点:在非空树中,根结点是树的最顶端的位置,也就是树的起点,它没有前驱,但是除根结点的其他结点都有且只有一个前驱。在例子中的A结点就是根结点。
    • 父结点:在某个结点之上并且与其直接相连的结点被称作父结点,每个结点有且只有一个父结点。
    • 子结点:在某个结点之下并且与其直接相连的结点被称为子结点,每个结点可以有零个或者多个结点,
    • 叶子结点:没有子结点的结点被称作叶子结点,叶子结点也可以被称作为终端结点。在例子中的E、F、G、H、I、J结点都是叶子节点。
  2. 边:结点之间的连接被称之为边,表示着父子关系。
  3. 度:结点的子结点个数被称作该结点的度,在树中最大的结点度数被称为树的度。在例子中A结点的度是最大的,所以树的度为3。在了解了度的概念后,叶子结点也可以被称作度为0的结点。
  4. 层次:结点的层次是从根结点开始定义的,根结点位于第一层,其子节点位于第二层,依次类推。结点的层次表示该结点到根结点的距离。在例子中A结点位于第一层,B、C、D结点位于第二层,E、F、G、H、I、J结点位于第三层。
  5. 高度和深度:结点的深度是从根结点开始向下逐层增加的,结点的高度是从叶子结点开始向上逐层增加的,树的高度或深度是树中结点的最大层数。例子中树的深度就是3。

二叉树的概念

二叉树是一种特殊的树结构,其中的结点最多只有两个子结点,分别被称作左结点和右结点。

二叉树中也有许多类型,其中常见的有:

  1. 满二叉树:在高度为h的树中,树中的结点个数是固定的并且为2h12^h - 1​。满二叉树的叶子结点都在最下层,并且除叶子结点外的每个结点度数都为2。简单来说,满二叉树就是把所有可以放结点的地方都填满了。

          1
        /   \
       2     3
      / \   / \
     4   5 6   7
    
  2. 完全二叉树:完全二叉树除了最后一层外,每一层上的节点数都是满的,并且最后一层上的节点都集中在左侧。

          1
        /   \
       2     3
      / \   / 
     4   5 6  
    
  3. 二叉搜索树:左子树上所有的关键字均小于根结点的关键字,右子树上的所有结点的关键字均大于根结点的关键字。并且二叉搜索树中任意结点的左子树和右子树也分别都是二叉排序树。

         8
       /   \
      3     10
     / \    /
    1   6  12
    
  4. 平衡二叉树:在平衡二叉树上的任意一个结点的左子树和右子树的深度之差不超过1.

          8
         / \
        4   10
       / \   \
      2   6   20
     / \
    5   7
    

二叉树数据结构在JavaScript中是由对象的嵌套实现的。

     1
   /   \
  2     3
 / \      \
4   5      6

实现以上二叉树的JavaScript代码为:

const root = {
    val: 1,
    left: {
        val: 2,
        left: {
            val: 4,
            left: null,
            right: null
        },
        right: {
            val: 5,
            left: null,
            right: null
        }
    },
    right: {
        val: 3,
        left: null,
        right: {
            val: 6,
            left: null,
            right: null
        }
    }
}

二叉树的遍历

二叉树的遍历是按照某种规律依次访问二叉树中的所有结点,使每一个结点都有且仅被访问一次。常见的二叉树遍历方式有先序遍历、中序遍历、后序遍历和层次遍历。

先序遍历

先序遍历:先访问根结点,然后先序遍历左子树,再先序遍历右子树。(根左右)

     1
   /   \
  2     3
 / \      \
4   5      6

该二叉树的先序遍历的结果是:124536。

递归方法实现

若二叉树非空,则依次执行访问根结点、遍历左子树、遍历右子树的操作。

function preorder(root) {
	if(!root){
        return
    }
    console.log(root.val);
    preorder(root.left)
    preorder(root.right)
}
preorder(root);

非递归方法实现(迭代方式)

通过使用栈模拟递归逻辑遍历二叉树。

例:LeetCode144. 二叉树的前序遍历,通过二叉树的根节点 root ,返回它节点值的前序遍历,用数组存储。

var preorderTraversal = function(root) {
    let stack = [];
    let res =[];
    if(root==null)
        return res
    stack.push(root);
    while(stack.length){
        const cur = stack.pop()
        res.push(cur.val)
        if(cur.right){
            stack.push(cur.right)
        }
        if(cur.left){
            stack.push(cur.left)
        }
    }
    return res
};

用以下二叉树为例过一遍代码逻辑。

     1
   /   \
  2     3
 / \      \
4   5      6

首先将根结点入栈,然后从栈中弹出根结点,并且将值存入数组。弹出根结点的原因是为了可以对根结点进行操作。([]是数组,[]下面的是栈)

根结点入栈     []       根结点出栈             [1] 
           ---------                      ---------
---->     |  1           ---->           |  
           ---------                      ---------

获取到根结点,然后判断出栈结点的右结点是否存在,如果存在就压入栈中,然后再判断左结点是否存在,如果存在就压入栈中。

右结点入栈     [1]       左结点入栈          [1] 
           ---------                      ---------
---->     |  3             ---->         | 3 2      
           ---------                      ---------

先让右结点进栈的原因是栈是先进后出的,所以进去的时候是右左,出来的时候才是左右。

后面同样道理,出栈一个结点,然后判断左右结点是否存在然后入栈,知道栈内为空才结束。最终数组的结果和先序遍历结果一样。

后序遍历

后序遍历:后序遍历左子树,然后后序遍历右子树,最后访问根结点。(左右根)

     1
   /   \
  2     3
 / \      \
4   5      6

该二叉树的后序遍历的结果是:452631。

递归方法实现

若二叉树非空,则依次执行遍历左子树、遍历右子树的操作,访问根结点。

function postorder(root) {
    if(!root){
        return
    }
    preorder(root.left)
    preorder(root.right)
    console.log(root.val);
}
postorder(root);

非递归方法实现(迭代方式)

通过使用栈模拟递归逻辑遍历二叉树。

例:LeetCode145. 二叉树的后序遍历,通过二叉树的根节点 root ,返回它节点值的前序遍历,用数组存储。

var postorderTraversal = function(root) {
    let stack = [];
    let res =[];
    if(root==null)
        return res
    stack.push(root);
    while(stack.length){
        const cur = stack.pop()
        res.unshift(cur.val)
        if(cur.left){
            stack.push(cur.left)
        }
        if(cur.right){
            stack.push(cur.right)
        }
    }
    return res
};

其实和先序操作非常相似,只需要稍微修改一下就好了。

先序的顺序是根左右,在先序遍历代码的基础上通过在入栈判断时让左结点先入栈,右结点后入栈,这样得出来的顺序是根右左,然后通过unshiftt()方法在数组的头部插入实现了数组的倒转,最终顺序为左右根

中序遍历

中序遍历:先遍历左子树,然后访问根节点,最后遍历右子树。(左根右)

     1
   /   \
  2     3
 / \      \
4   5      6

该二叉树的后序遍历的结果是:425136。

递归方法实现

若二叉树非空,则依次执行遍历左子树、访问根结点、遍历右子树的操作。

function inorder(node) {
    if(!root){
        return
    }
    preorder(root.left)
    console.log(root.val);
    preorder(root.right)  
}
inorder(root);

非递归方法实现(迭代方式)

通过使用栈模拟递归逻辑遍历二叉树。

例:LeetCode94. 二叉树的中序遍历,通过二叉树的根节点 root ,返回它节点值的前序遍历,用数组存储。

var inorderTraversal = function(root) {
    let res = []
    let stack = []
    if(root==null)
        return res
    let cur = root
    while(stack.length || cur){
        while(cur){
            stack.push(cur)
            cur = cur.left
        }
        cur = stack.pop()
        res.push(cur.val)
        cur = cur.right
    }
    return res
};

将根节点放入栈;如果根节点有左子树,则将左子树的根节点放入栈;重复前两步,继续遍历左子树;从栈中弹出节点并进行访问,然后遍历该节点的右子树(重复前两步);如果栈为空,则遍历完成。

设计思路:依次将根结点的左子树存入栈中,然后出栈最左的叶子结点,再出栈栈顶结点(栈顶结点就是最左叶子结点的后继)并且将值存入数组,然后再找栈顶结点的右子树的最左结点,重复这样执行就能按照左根右的顺序放入数组中。

tips:后继指的是在某种特定顺序下,一个节点的后面紧邻的那个节点。在二叉树的中序遍历中,一个节点的后继就是按照中序遍历顺序,在它之后被访问的那个节点。

  1. 判断树是否为空;
  2. 如果根节点有左子树,则将左子树的根节点放入栈,重复执行,直到左子树的根结点为叶子结点结束;
  3. 将栈顶结点出栈,将值存入数组中;
  4. 只要栈不为空或者当前结点不为空,则用出栈结点的右子树根结点从步骤2开始向下执行;

层次遍历

层次遍历:从二叉树的根结点开始,自上而下,从左到右逐层访问二叉树的节点。

      1
    /   \
   2     3
  / \   / \
 4   5 6   7

层次遍历的顺序为:1 2 3 4 5 6 7 。

实现思想:用队列进行辅助。

  1. 将根结点放入队列中;
  2. 开始循环:
    1. 取出队头结点,然后访问该结点;

    2. 如果该结点有左子结点,将左子结点加入队列。

    3. 如果该结点有右子结点,将右子结点加入队列。

    实现代码:
function levelTraversal(root) {
    if (!root) {
        return [];
    }
    const queue = [root];
    const result = [];
    while (queue.length > 0) {
        const levelSize = queue.length;
        const currentLevel = [];
        for (let i = 0; i < levelSize; i++) {
            const node = queue.shift();
            currentLevel.push(node.val);
            if (node.left) {
                queue.push(node.left);
            }
            if (node.right) {
                queue.push(node.right);
            }
        }
        result.push(currentLevel);
    }
    return result;
}