【代码随想录 | day14】(JavaScript) 二叉树系列:理论基础、递归遍历、迭代遍历、统一迭代

130 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情

  • 理论基础、递归遍历、迭代遍历、统一迭代

二叉树理论基础

需要了解 二叉树的种类,存储方式,遍历方式 以及二叉树的定义

二叉树的种类

二叉树有两种主要的形式:满二叉树和完全二叉树。

满二叉树

  • 除了最后一层的节点没有任何子节点外,每层上的所有节点都有两个节点的二叉树。
  • 满二叉树高度为 h,则结点有 2h-1。

完全二叉树

  • 一颗二叉树的深度(从上往下数)为h,除了第h层外,其他各层的节点都有两个子节点,且第h层的所有节点都集中在最左边
  • 满二叉树一定是完全二叉树,但是完全二叉树不一定是满二叉树
  • 用数学公式讲,对于 K 层的完全二叉树,其节点数的范围是:2k11<N<2k12^{k-1}-1<N<2^k-1 。如果 k=3,则3<N<7。

二叉树的存储方式

链式存储:通过指针把分布在散落在各个地址的节点串联一起。

顺序存储:元素在内存是连续分布的。用数组存储二叉树时,如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。

二叉树的遍历方式

深度优先遍历和广度优先遍历是图论中最基本的两种遍历方式

  • 深度优先遍历

    • 前序遍历(前根遍历):——>左——>右
    • 中序遍历(中根遍历):左——>——>右
    • 后序遍历(后根遍历):左——>右——>
  • 广度优先遍历

    • 层次遍历(迭代法)

二叉树的定义

一定要注意二叉树节点定义的书写方式,在现场面试的时候 面试官可能要求手写代码,所以数据结构的定义以及简单逻辑的代码一定要锻炼白纸写出来。

JavaScript代码如下:

 function TreeNode(val, left, right) {
     this.val = (val===undefined ? 0 : val)
     this.left = (left===undefined ? null : left)
     this.right = (right===undefined ? null : right)
 }

二叉树的递归遍历

在写递归的时候,可以参考以下思路:

  1. 确定递归函数的参数和返回值
  2. 确定终止条件
  3. 确定单层递归的逻辑

dfs —— 深度优先搜索算法

首先在注释中给出了JavaScript版本的二叉树定义

 Definition for a binary tree node.
 function TreeNode(val, left, right) {
     this.val = (val===undefined ? 0 : val)
     this.left = (left===undefined ? null : left)
     this.right = (right===undefined ? null : right)
 }
 /**
  * @param {TreeNode} root
  * @return {number[]}
  */

形参root就是 TreeNode 类生成的实例对象,其具有三个属性(val, left, right)

前序遍历

144. 二叉树的前序遍历 - 力扣(LeetCode)

前序遍历是深度优先搜索的一个过程,它沿着一个方向,一直在搜索,当遇到空节点(null)的的时候就往回搜索。

前序遍历结果特点:第一个是根节点的值,接着是左子树,最后是右子树。

 var preorderTraversal = function(root) {
  let res=[];
  const dfs=function(root){
      if(root===null)return ; // 遇到空节点
      //先序遍历所以从父节点开始
      res.push(root.val); // 把当前的current它的value直接放到数组里
      //递归左子树
      dfs(root.left);
      //递归右子树
      dfs(root.right);
  }
  //只使用一个参数 使用闭包进行存储结果
  dfs(root);
  return res;
 };

preorderTraversal(root)执行,会跳到内部实现的

中序遍历

94. 二叉树的中序遍历 - 力扣(LeetCode)

中序遍历结果的特点root.val 在中间,左右子树在两侧:

 var inorderTraversal = function(root) {
     let res=[];
     const dfs=function(root){
         if(root===null){
             return ;
         }
         dfs(root.left);
         res.push(root.val);
         dfs(root.right);
     }
     dfs(root);
     return res;
 };

后序遍历

145. 二叉树的后序遍历 - 力扣(LeetCode)

后序遍历结果的特点:先是左子树,然后右子树,最后 root.val

 var postorderTraversal = function(root) {
     let res=[];
     const dfs=function(root){
         if(root===null){
             return ;
         }
         dfs(root.left);
         dfs(root.right);
         res.push(root.val);
     }
     dfs(root);
     return res;
 };

二叉树的迭代遍历

前中后序的遍历,不仅可以用递归的方法,还可以迭代遍历。那么递归和迭代的区别在哪里呢?可以看看文末的参考文章中的相关链接,大佬总结的 hin 不错!这里就简单记录一些笔记啦~

递归

  • 基本概念: 程序调用自身的编程技巧称为递归,是函数自己调用自己。
  • 递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中。
  • 递归是用机制实现的,每深入一层,都要占去一块栈数据区域,对嵌套层数深的一些算法,递归会力不从心,空间上会以内存崩溃而告终,而且递归也带来了大量的函数调用,这也有许多额外的时间开销。所以在深度大时,它的时空性就不好了。

迭代:

  • 利用变量的原值推算出变量的一个新值.如果递归是自己调用自己的话,迭代就是A不停的调用B。
  • 迭代虽然效率高,运行时间只因循环次数增加而增加,没什么额外开销,空间上也没有什么增加,但缺点就是不容易理解,编写复杂问题时困难。

前序遍历

考虑到入栈和出栈的顺序,迭代法在进行前序遍历的时候,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。

 // 入栈 右 -> 左
 // 出栈 中 -> 左 -> 右
 var preorderTraversal = function(root, res = []) {
     if(!root) return res;
     const stack = [root];
     let cur = null;
     while(stack.length) {
         cur = stack.pop();
         res.push(cur.val);
         // 如果将右孩子存在,就将其压入栈中
         cur.right && stack.push(cur.right);
         // 如果将左孩子存在,就将其压入栈中
         cur.left && stack.push(cur.left);
     }
     return res;
 };

中序遍历

用迭代法写中序遍历的时候,会发现套路又不一样了,目前的前序遍历的逻辑无法直接应用到中序遍历上。

中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点。处理顺序和访问顺序和前序是不一样的。

那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。

 // 入栈 左 -> 右
 // 出栈 左 -> 中 -> 右
 ​
 var inorderTraversal = function(root, res = []) {
     const stack = [];
     let cur = root;
     while(stack.length || cur) {
         if(cur) {
             stack.push(cur);
             // 左
             cur = cur.left;
         } else {
             // --> 弹出 中
             cur = stack.pop();
             res.push(cur.val); 
             // 右
             cur = cur.right;
         }
     };
     return res;
 };

后序遍历

只需要调整一下先序遍历(根左右)的代码顺序,就变成根右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右根了。

 // 入栈 左 -> 右
 // 出栈 中 -> 右 -> 左 结果翻转
 ​
 var postorderTraversal = function(root, res = []) {
     if (!root) return res;
     const stack = [root];
     let cur = null;
     do {
         cur = stack.pop();
         res.push(cur.val);
         cur.left && stack.push(cur.left);
         cur.right && stack.push(cur.right);
     } while(stack.length);
     return res.reverse();
 };

二叉树的统一迭代

介绍一种前中后序遍历的统一风格的写法。

要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。

前序遍历

压栈顺序:右左中

 var preorderTraversal = function(root, res = []) {
     const stack = [];
     if (root) stack.push(root);
     while(stack.length) {
         const node = stack.pop();
         if(!node) {
             res.push(stack.pop().val);
             continue;
         }
         if (node.right) stack.push(node.right); // 右
         if (node.left) stack.push(node.left); // 左
         stack.push(node); // 中
         stack.push(null);
     };
     return res;
 };

中序遍历

压栈顺序:右中左

 var inorderTraversal = function(root, res = []) {
     const stack = [];
     if (root) stack.push(root);
     while(stack.length) {
         const node = stack.pop();
         if(!node) {
             res.push(stack.pop().val);
             continue;
         }
         if (node.right) stack.push(node.right); // 右
         stack.push(node); // 中
         stack.push(null);
         if (node.left) stack.push(node.left); // 左
     };
     return res;
 };

后序遍历

压栈顺序:中右左

 var postorderTraversal = function(root, res = []) {
     const stack = [];
     if (root) stack.push(root);
     while(stack.length) {
         const node = stack.pop();
         if(!node) {
             res.push(stack.pop().val);
             continue;
         }
         stack.push(node); // 中
         stack.push(null);
         if (node.right) stack.push(node.right); // 右
         if (node.left) stack.push(node.left); // 左
     };
     return res;
 };

总结

c++中map、set、multimap、multiset的底层实现都是平衡二叉树

而在JavaScript中,Set去重机制,准确来说是底层数据结构决定的,Set的底层数据结构是哈希表。我们通常将哈希表称为数组升级版,数组在“值”查询上表现拙劣,性能不佳,而哈希在”值“查询上表现良好,性能不错。

⭐️Set比较Array的最大不同就是:Set的值查找效率要比Array快的多,原因就是Set底层是哈希表,它查找元素,不是真的找,而是根据哈希算法,算出元素在哈希表中位置。


参考文章