JS算法 | 二叉树基础

667 阅读4分钟

前言

二叉树遍历终结篇,用 js 和图描述二叉树的前序遍历、中序遍历、后序遍历,层次遍历以及广度优先遍历(BFS)和深度优先遍历(DFS)

准备

  • 为练习准备下图中的二叉树

二叉树.png

  • 使用 JS 对象,将上图中的树录入到代码
const treeObj = {
  val: 1,
  left: {
    val: 2,
    left: {
      val: 4,
    },
    right: {
      val: 5,
      left: {
        val: 7,
      },
      right: {
        val: 8,
      },
    },
  },
  right: {
    val: 3,
    right: {
      val: 6,
    },
  },
}

前序遍历(深度优先遍历)

深度优先遍历(DFS)与前序遍历相同,写在一起

图解

前序遍历.png

递归思路

  • 打印当前节点的值
  • 递归调用自身,传入左子树
  • 递归调用自身,传入右子树

递归代码

const arr = []
function fun(tree) {
    if(!tree){
        return
    }
    arr.push(tree.val)
    fun(tree.left)
    fun(tree.right)
}
fun(treeObj)
console.log(arr)
// 输出:[1, 2, 4, 5, 7, 8, 3, 6]

非递归思路

  • 将根节点塞入栈

  • 执行以下循环,当栈为空结束循环

    • 推出栈中的第一个节点
    • 检测是否有右子树,如果有,塞入栈
    • 检测是否有左子树,如果有,塞入栈
    • 打印当前推出节点的值

非递归代码

function fun(tree) {
    const result = []
        // 根节点塞入栈
    const stack = [tree]
    let currentNode
    while(stack.length > 0){
                // 推出栈中的第一个节点
        currentNode = stack.pop()
        // 如果有右节点,右节点入栈
        if(currentNode.right){
            stack.push(currentNode.right)
        }
                // 如果有左节点,左节点入栈
        if(currentNode.left){
            stack.push(currentNode.left)
        }
                // 打印当前节点
        result.push(currentNode.val)
    }
    return result 
}

中序遍历

图解

中序遍历.png

递归思路

  • 递归调用自身,传入左子树
  • 打印当前节点
  • 递归调用自身,传入右子树

递归代码

const arr = []
function fun(tree) {
    if(!tree){
        return
    }
    fun(tree.left)
    arr.push(tree.val)
    fun(tree.right)
}
fun(treeObj)
console.log(arr)
// 输出:[4, 2, 7, 5, 8, 1, 3, 6]

非递归思路

  • 执行循环,当树为空并且栈为空时停止循环
    1. 从根节点开始将所有的左节点入栈,直到最后一个左节点
    2. 推出栈中的最后一个节点
    3. 打印节点值
    4. 如果推出的节点有右子树,基于右子树继续执行步骤 a
    5. 如果没有右子树,执行步骤 b,重新推出栈中的最后一个节点

非递归代码

function fun(root) {
   if(!root){
       return [];
    }
    var result = []
    var stack = []
    while(stack.length!==0||root){
        // 从树的根节点开始将所有的左节点塞入栈
        while(root){
            stack.push(root);
            root = root.left;
        }
                // 推出栈中的节点
        root = stack.pop();
        result.push(root.val)
        // 基于右子树继续进行下一次循环
                // 右子树是否存在,下次循环中判断
        root = root.right;
      }
      return result;
}

后序遍历

图解

后序遍历.png

递归思路

  • 递归调用自身,传入左子树
  • 递归调用自身,传入右子树
  • 打印当前节点

递归代码

const arr = []
function fun(tree) {
    if(!tree){
        return
    }
    fun(tree.left)
    fun(tree.right)
    arr.push(tree.val)
}
fun(treeObj)
console.log(arr)
// 输出:[4, 7, 8, 5, 2, 6, 3, 1]

非递归思路

  • 刚好与前序遍历相反

  • 将根节点塞入栈

  • 执行以下循环,当栈为空结束循环

    • 推出栈中的第一个节点
    • 检测是否有左子树,如果有,塞入栈
    • 检测是否有右子树,如果有,塞入栈
    • 将当前节点从数组首位插入到结果数组
  • 打印结果数组

非递归代码

function fun(tree) {
    const result = []
        // 根节点塞入栈
    const stack = [tree]
    let currentNode
    while(stack.length > 0){
                // 推出栈中的第一个节点
        currentNode = stack.pop()
                // 如果有左节点,左节点入栈
        if(currentNode.left){
            stack.push(currentNode.left)
        }
        // 如果有右节点,右节点入栈
        if(currentNode.right){
            stack.push(currentNode.right)
        }
                // 打印当前节点
        result.unshift(currentNode.val)
    }
    return result 
}

层序遍历(广度优先遍历)

广度优先遍历(BFS)与层序遍历相同,写在一起

实现思路

  • 使用队列存储树

  • 将树存入队列

  • 进行循环迭代,直到队列为空,停止

    • 从队列首部推出节点
    • 打印当前节点
    • 如果有左节点,从队列尾部推入左节点
    • 如果有右节点,从队列尾部推入右节点
  • 打印最终结果

function fun(root){
   if(!root){
       return [];
    }
   var queue = [root];
   var result = [];
   
   while (queue.length!==0){
      // 队列首部推出
      var node = queue.shift();
      // 记录结果
      result.push(node.val);
      // 尾部推入左子树
      if(node.left){
          queue.push(node.left);
      }
      // 尾部推入右子树
      if(node.right){
          queue.push(node.right);
      }
   }
   return result; 
}

总结

个人感觉二叉树遍历的非递归调用没什么作用,不便于理解,入门可以优先研究递归的调用方式,在层次遍历的实现中也找到了队列的使用场景。

参考

《二叉树遍历(前序、中序、后序、层次遍历、深度优先、广度优先)》

《JS实现二叉树遍历(递归和非递归)》