101道算法JavaScript描述【二叉树】6,面试真题解析

  • 空间复杂度:O(n)O(n)

    创建了长度为n的数组。

从前序与中序遍历序列构造二叉树


根据一棵树的前序遍历与中序遍历构造二叉树。

可假设树中没有重复的元素。

示例


给出

前序遍历 preorder = [3,9,20,15,7]

中序遍历 inorder = [9,3,15,20,7]



返回如下的二叉树

    3

   / \

  9  20

    /  \

   15   7



名词解释

前序遍历:首先访问根结点,然后遍历左子树,最后遍历右子树。在遍历左、右子树时,仍然先访问根结点,然后遍历左子树,最后遍历右子树。

示例解析:二叉树的前序遍历先访问根结点为3,然后遍历左子树9,最后遍历右子树;右子树为一棵树,先访问根结点为20,再遍历左子树为15,最后遍历右子树为7。则示例中二叉树的前序遍历为[3,9,20,15,7]。

中序遍历:首先遍历左子树,然后访问根结点,最后遍历右子树。在遍历左、右子树时,仍然先遍历左子树,然后访问根结点,最后遍历右子树。

示例解析:二叉树的中序遍历,先遍历左子树为9,然后访问根结点3,最后遍历右子树;右子树为一棵树,先遍历左子树为15,再访问根结点为20,最后遍历右子树为7。则示例中二叉树的中序遍历为[9,3,15,20,7]。

方法一 递归法

思路

前序遍历中首先出现的结点均为根结点,结合中序遍历中对应结点及距离上(下)一结点的中间的数字可确定此结点的左(右)子树中的结点值,在左(右)子树中的结点值确定后,再结合前序遍历的数组可得出左(右)子树的根结点,再根据中序遍历中的左(右)子树的根结点得出左(右)子树的左右子树……如此递归,则可得出完整的二叉树。

根据题目中的例子,前序遍历首先出现的数(即3)为根结点,3 在中序遍历中的位置之前的结点 9 必定为根结点的左子树;在前序遍历中 9 之后的结点为根结点的右子树;同样的方法确定前序遍历中的 20 为右子树的根结点,15 为右子树的左结点,7 为右子树的右结点。

详解

  1. 根据前序遍历数组找到该树的根结点;

const root = preorder[preStart];



preorder 为前序遍历的数组,preStart 为前序遍历数组的下标。初始化时置为 0,为二叉树的根结点;之后每次递归均为该子树的根结点的下标。

  1. 若中序遍历中根结点的位置之前有值,则证明该根结点有左子树,然后从第一步开始递归,拿到该根结点的左子树;

// 存在左子树

  if (inOrderIndex - inStart >= 1) {

    rootNode.left = constructNewNode(preorder, inorder, preStart + 1, preStart + (inOrderIndex - inStart), inStart, inOrderIndex - 1);

  }



  1. 若中序遍历中根结点的位置之后有值,则证明该根结点有右子树,然后从第一步开始递归,拿到该根结点的右子树

//存在右子树

  if (inLength - inOrderIndex >= 1) {

    rootNode.right = constructNewNode(preorder, inorder, preStart + (inOrderIndex - inStart) + 1, preLength, inOrderIndex + 1, inLength);

  }



代码


function TreeNode (val) {

  this.val = val;

  this.left = this.right = null;

}



function buildTree (preorder, inorder) {

  if (preorder.length === 0 || inorder.length === 0) {

    return null;

  }

  return constructNewNode(preorder, inorder, 0, preorder.length, 0, inorder.length);

}



function findInorderIndex (list, target) {

  if (list.length === 0) {

    return undefined;

  }



  let index;

  list.forEach((item, i) => {

    if (item === target) {

      index = i;

    }

  });

  return index;

}



function constructNewNode (preorder, inorder, preStart, preLength, inStart, inLength) {

  const root = preorder[preStart];

  const inOrderIndex = findInorderIndex(inorder, root);

  // 中序遍历根结点左边为左子树,右边为右子树

  const rootNode = new TreeNode(root);

  // 存在左子树

  if (inOrderIndex - inStart >= 1) {

    rootNode.left = constructNewNode(preorder, inorder, preStart + 1, preStart + (inOrderIndex - inStart), inStart, inOrderIndex - 1);

  }



  // 存在右子树

  if (inLength - inOrderIndex >= 1) {

    rootNode.right = constructNewNode(preorder, inorder, preStart + (inOrderIndex - inStart) + 1, preLength, inOrderIndex + 1, inLength);

  }

  return (root || root === 0) ? rootNode : null;

}



复杂度分析

  • 时间复杂度:O(n)O(n)

    由于每次递归 inorder 和 preorder 的总数都会减 1,因此需要递归 nn 次,故时间复杂度为 O(n)O(n),其中 nn 为结点个数

  • 空间复杂度:O(n)O(n)

    所用空间与树本身存储空间正相关

方法二 遍历法

思路

前序遍历 preorder = [3,9,20,15,7] 中序遍历 inorder = [9,3,15,20,7]

借用了栈的数据结构,先将根结点放入,然后前序遍历数组,若在中序遍历的节点与前序遍历的节点相等(即找到了已遍历节点的右子树的根结点)则从匹配到的节点到该根节点出栈。如此,遍历完前序数组后则可唯一确定一颗完整的二叉树。【重点在于判断何时找到了右子树的根节点,在中序遍历中的节点与前序遍历的当前节点相等时则可确定,原因在于前序遍历中遍历到右子树的根结点且与中序遍历中的节点相等时,必然可以确定匹配节点的左子节点已经遍历完毕;同时确定该子树是哪个根结点的右子树(示例解析中已说明),问题即可得到解决】

示例解析

若当前需要确定的二叉树为示例所示:


    3

   / \

  9  20

    /  \

   15   7



首先,假设只有前序遍历的数组preorder = [3,9,20,15,7],需要确定上面的唯一一颗二叉树,可以做什么?

我们首先可以确定这颗树的根结点为【3】,然后是节点【9】,按照前序遍历的原则,先遍历根结点,再遍历左子节点,然后再遍历右子节点,可以确定的是【9】是【3】的子树的根结点,但是无法确定是左子树的根结点,还是右子树的根结点;

此时需要结合中序遍历的数组inorder = [9,3,15,20,7]来进行判断。可以先假设【9】是【3】的右子树的根结点,根据中序遍历的特点,先遍历左子树,再访问根结点,再遍历右子树可得出中序遍历的顺序为[3,9],与既定的中序遍历数组的顺序不苻,所以可确定【9】是【3】的左子树的根结点;

同时,注意到了此时的前序遍历的【9】和中序遍历的【9】相等了,说明【9】是没有左子节点的,从中序遍历的特点以及【9】在中序遍历中所处的位置可以得出。则前序遍历的下一个节点必定是【20】必定为右子树的根结点,那么【20】是【3】的右子树的根结点,还是【9】的右子树的根结点呢?

假设【20】是【3】的右子树的根结点,那么中序遍历的数组顺序应当是[9,3,20];假设【20】是【9】的右子树的根结点,那么中序遍历的数组顺序应当是[9,20,3];由此可确定【20】是【3】的右子树的根结点。

此时我们的中序遍历的数组是[9,3,15,20,7],【9】匹配,【3】匹配,最后一次匹配是【3】,所以【20】是【3】的右子树。


    3

   / \

  9  20

    /  \

   15   7



综上所述,可用一个栈来记录已经遍历过的节点,遍历前序遍历的数组,作为当前节点的左子树的根结点,直到前序遍历的当前节点与中序遍历的节点相匹配,即找到了遍历过的某个节点的右子树的根结点,则从该匹配节点到最后一个匹配节点出栈,并将当前节点作为最后一个匹配节点的右子树的根结点。如此,将前序与中序数组遍历完毕,便可确定唯一的一颗二叉树。

详解

1、根据前序遍历确定该树的根结点,将其放入栈中;


treeNodeList.push(root);



2、进行前序遍历,若中序遍历中当前值为正好为栈中最后一个根结点时,该根结点出栈,当前前序遍历的结点为该根结点的右子根结点;否则当前前序遍历的结点为该根结点的左子根结点,并将当前值放入栈中,继续遍历,重复第二个步骤。

代码


function TreeNode (val) {

  this.val = val;

  this.left = this.right = null;

}



const buildTree = function (preorder, inorder) {

  if (preorder.length === 0) {

    return null;

  }

  const treeNodeList = [];

  const root = new TreeNode(preorder[0]);

  treeNodeList.push(root);

  // j从0开始,为中序遍历的序数

  let j = 0;

  for (let i = 1; i < preorder.length; i++) {

    const current = new TreeNode(preorder[i]);

    let curParent;



    // 中序遍历至当前根结点时,右子树根结点确定,当前根结点出栈

    while (treeNodeList.length !== 0 && treeNodeList[treeNodeList.length - 1].val === inorder[j]) {

      curParent = treeNodeList[treeNodeList.length - 1];

      treeNodeList.length--;

      j++;

    }

    if (curParent) {

      curParent.right = current;

    } else {

      treeNodeList[treeNodeList.length - 1].left = current;

    }

    treeNodeList.push(current);

  }

  return root;

};



复杂度分析

  • 时间复杂度:O(n)O(n)

    前序遍历与中序遍历的序数变动基本一致

  • 空间复杂度:O(n)O(n)

    所用空间与二叉树的左子树的深度正相关为 O(\log_2{n})O(log2n),树本身存储空间为 O(n)O(n),取大者

二叉搜索树中第 K 小的元素


给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。

说明 你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。

示例 1:


输入: root = [3,1,4,null,2], k = 1

   3

  / \

 1   4

  \

   2

输出: 1



示例 2:


输入: root = [5,3,6,2,4,null,null,1], k = 3

       5

      / \

     3   6

    / \

   2   4

  /

 1

输出: 3



进阶: 如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化 kthSmallest 函数?

方法一 递归查找

根据二叉搜索树的特性:

若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值

思路

我们只需要中序遍历输入的树,然后输出第 k 个元素即可以得到第 k 个最小的元素。最简单的办法就是写一个递归函数进行遍历然后将遍历结果保存到数组中。

详解

中序遍历的原则是:先遍历左子树,然后访问根节点,最后遍历右边子树,当发现已经找到第 k 个元素,提前中止遍历

代码


/**

 * Definition for a binary tree node.

 * function TreeNode(val) {

 *     this.val = val;

 *     this.left = this.right = null;

 * }

 */

/**

 * @param {TreeNode} root

 * @param {number} k

 * @return {number}

 */

const kthSmallest = function (root, k) {

  const result = [];



  function travel (node) {

    // 当已经找到 k 个元素时提前中止遍历

    if (result.length >= k) return;

    if (node.left) {

      // 遍历左子树

      travel(node.left);

    }

    // 保存根节点

    result.push(node.val);

    if (node.right) {

      // 遍历右子树

      travel(node.right);

    }

  }

  travel(root);

  return result[k - 1];

};



复杂度分析

  • 时间复杂度:O(n)O(n)

    时间复杂度与节点个数相关,nn 个节点最多需要递归查找 nn 次,所以时间复杂度为 O(n)O(n)。

  • 空间复杂度:O(n)O(n)

    空间复杂度与调用堆栈有关,调用栈需要记住每个节点的值,所以空间复杂度为 O(n)。

方法二 循环查找

思路

还是一样采用中序遍历,只不过我们不使用递归,改为循环的方式实现

详解

见代码注释

代码


/**

 * Definition for a binary tree node.

 * function TreeNode(val) {

 *     this.val = val;

 *     this.left = this.right = null;

 * }

 */

/**

 * @param {TreeNode} root

 * @param {number} k

 * @return {number}

 */

const kthSmallest = function (root, k) {

  const result = []; let current = root; const stack = [];

  while (result.length < k && (current || stack.length > 0)) {

    if (current) {

      // 有左孩子,表示有比当前元素更小的,继续查找

      if (current.left) {

        // 把当前节点暂存到堆栈

        stack.push(current);

        // 继续查找左子树

        current = current.left;

      } else {

        // 没有左孩子表示当前元素是目前最小,存入数组

        result.push(current.val);

        // 左子树查找完后开始查找右子树

        current = current.right;

      }

    } else {

      // 已经遍历到叶子节点,需要回溯,从节点堆栈中弹出一个节点

      current = stack.pop();

      // 由于左子树已经查找完成,那么当前节点是目前最小的节点

      result.push(current.val);

      // 然后继续查找右子树

      current = current.right;

    }


**自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

**深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

**因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**
![img](https://img-blog.csdnimg.cn/img_convert/91ab1957ee5fd649209fe5a1eabb25be.jpeg)
![img](https://img-blog.csdnimg.cn/img_convert/2bf2681ae2c52eeb87717f0c2c6b7652.png)
![img](https://img-blog.csdnimg.cn/img_convert/49cc898cacfa9289306d0b1c74f964f7.png)
![img](https://img-blog.csdnimg.cn/img_convert/6b90f2f73432ea392f19fec3b7ce9d39.png)
![img](https://img-blog.csdnimg.cn/img_convert/b410cdd6f0dd9549874ca083b5a415ae.png)
![img](https://img-blog.csdnimg.cn/img_convert/7ee9e6a5305a53bc0e470f221ecb9a76.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!**

**由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**

**如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)**
![img](https://img-blog.csdnimg.cn/img_convert/e23a3e74bd0ca2e9941ca5e677c28836.png)



### 总结

>技术学到手后,就要开始准备面试了,找工作的时候一定要好好准备简历,毕竟简历是找工作的敲门砖,还有就是要多做面试题,复习巩固。

**[CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】]( )**

**深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

**因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**
[外链图片转存中...(img-8Tgn7n2A-1711919864898)]
[外链图片转存中...(img-ieZuIY8E-1711919864898)]
[外链图片转存中...(img-Pgu19apD-1711919864899)]
[外链图片转存中...(img-2Yh9ntKA-1711919864899)]
[外链图片转存中...(img-LlefHkLK-1711919864900)]
[外链图片转存中...(img-xhgi1ZXx-1711919864900)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!**

**由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**

**如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)**
[外链图片转存中...(img-RM8jZVhI-1711919864901)]



### 总结

>技术学到手后,就要开始准备面试了,找工作的时候一定要好好准备简历,毕竟简历是找工作的敲门砖,还有就是要多做面试题,复习巩固。

**[CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】]( )**

![](https://img-blog.csdnimg.cn/img_convert/3e500c0d727af9494992eba126a352d1.webp?x-oss-process=image/format,png)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值