sisterAn / JavaScript-Algorithms

基础理论+JS框架应用+实践,从0到1构建整个前端算法体系
5.51k stars 634 forks source link

前端进阶算法7:小白都可以看懂的树与二叉树 #39

Open sisterAn opened 4 years ago

sisterAn commented 4 years ago

引言

不同与我们之前介绍的线性结构,今天我们介绍一种非线性结构:树,树的内容比较多,包括BST树、AVL树、Trie树等,这部分内容将放在下几个章节陆续放出,本章将介绍树与二叉树的基础必会内容,在开始这一章节前,请思考以下内容:

下面进入本节内容👇

一、树

不同于我们上面介绍的线性结构,树是一种非线性结构。

图:

img

它遵循:

这就是树!

树有几个概念:

B、C、D就互称为兄弟节点,其中,节点B的高度为2,节点B的深度为 1,树的高度为3

高度

树的高度计算公式:

下图每个节点值都代表来当前节点的高度:

二、二叉树

二叉树,故名思义,最多仅有两个子节点的树(最多能分两个叉的树🤦‍♀️):

图:

img

三、平衡二叉树

二叉树中,每一个节点的左右子树的高度相差不能大于 1,称为平衡二叉树。

另外还有满二叉树、完全二叉树等:

四、在代码中如何去表示一棵二叉树

1. 链式存储法

二叉树的存储很简单,在二叉树中,我们看到每个节点都包含三部分:

所以我们可以将每个节点定义为:

function Node(val) {
    // 保存当前节点 key 值
    this.val = val
    // 指向左子节点
    this.left = null
    // 指向右子节点
    this.right = null
}

一棵二叉树可以由根节点通过左右指针连接起来形成一个树。

function BinaryTree() {
  let Node = function (val) {
    this.val = val
    this.left = null
    this.right = null
  }
  let root = null
}

2. 数组存储法(适用于完全二叉树)

下图就是一棵完全二叉树,

如果我们把根节点存放在位置 i=1 的位置,则它的左子节点位置为 2i = 2 ,右子节点位置为 2i+1 = 3

如果我们选取 B 节点 i=2 ,则它父节点为 i/2 = 1 ,左子节点 2i=4 ,右子节点 2i+1=5

以此类推,我们发现所有的节点都满足这三种关系:

因此,如果我们把完全二叉树存储在数组里(从下标为 1 开始存储),我们完全可以通过下标找到任意节点的父子节点。从而完整的构建出这个完全二叉树。这就是数组存储法。

数组存储法相对于链式存储法不需要为每个节点创建它的左右指针,更为节省内存。

五、二叉树的遍历

二叉树的遍历可分为:

所谓前、中、后,不过是根的顺序,即也可以称为先根遍历、中根遍历、后根遍历

1. 前序遍历

对于二叉树中的任意一个节点,先打印该节点,然后是它的左子树,最后右子树

2. 中序遍历

对于二叉树中的任意一个节点,先打印它的左子树,然后是该节点,最后右子树

3. 后序遍历

对于二叉树中的任意一个节点,先打印它的左子树,然后是右子树,最后该节点

4. 代码实现(前序遍历为例)

所以,遍历二叉树的过程也就是一个递归的过程,例如前序遍历,先遍历根节点,然后是根的左子树,最后右子树;遍历根节点的左子树的时候,又是先遍历左子树的根节点,然后左子树的左子树,左子树的右子树…….

所以,它的核心代码就是:

// 前序遍历核心代码
var preOrderTraverseNode = (node) => {
    if(node) {
        // 先根节点
        result.push(node.val)
        // 然后遍历左子树
        preOrderTraverseNode(node.left)
        // 再遍历右子树
        preOrderTraverseNode(node.right)
    }
}

完整代码如下:

递归实现
// 前序遍历
var preorderTraversal = (root) => {
    let result = []
    var preOrderTraverseNode = (node) => {
        if(node) {
            // 先根节点
            result.push(node.val)
            // 然后遍历左子树
            preOrderTraverseNode(node.left)
            // 再遍历右子树
            preOrderTraverseNode(node.right)
        }
    }
    preOrderTraverseNode(root)
    return result
};

我们既然可以使用递归实现,那么是否也可以使用迭代实现喃?

迭代实现

利用栈来记录遍历的过程,实际上,递归就使用了调用栈,所以这里我们可以使用栈来模拟递归的过程

依次循环出栈遍历入栈,直到栈为空,遍历完成

// 前序遍历
const preorderTraversal = (root) => {
    const list = [];
    const stack = [];

    // 当根节点不为空的时候,将根节点入栈
    if(root) stack.push(root)
    while(stack.length > 0) {
        const curNode = stack.pop()
        // 第一步的时候,先访问的是根节点
        list.push(curNode.val)

        // 我们先打印左子树,然后右子树
        // 所以先加入栈的是右子树,然后左子树
        if(curNode.right !== null) {
            stack.push(curNode.right)
        }
        if(curNode.left !== null) {
            stack.push(curNode.left)
        }
    }
    return list
}
复杂度分析:

空间复杂度:O(n)

时间复杂度:O(n)

至此,我们已经实现了二叉树的前序遍历,尝试思考一下二叉树的中序遍历如何实现喃?

六、leetcode94:二叉树的中序遍历

给定一个二叉树,返回它的 中序 遍历。

示例:

输入: [1,null,2,3]
   1
    \
     2
    /
   3

输出: [1,3,2]

进阶:  递归算法很简单,你可以通过迭代算法完成吗?

欢迎将解答提交到 JavaScript-Algorithms ,这里我们提交了前端所用到的算法系列文章以及题目(已解答),欢迎star,如果感觉不错,点个赞支持一下呗😊

七、前端算法集训营第一期免费加入啦

欢迎关注「前端瓶子君」,回复「算法」自动加入,从0到1构建完整的数据结构与算法体系!

在这里,瓶子君不仅介绍算法,还将算法与前端各个领域进行结合,包括浏览器、HTTP、V8、React、Vue源码等。

在这里,你可以每天学习一道大厂算法题(阿里、腾讯、百度、字节等等)或 leetcode,瓶子君都会在第二天解答哟!

⬆️ 扫码关注公众号「前端瓶子君」,回复「算法」即可自动加入 👍👍👍

woaixiangbao commented 3 years ago

整篇文字清晰易懂,非常好!感谢!不过有个小问题: 1,对于”完全二叉树“的解释,您只有一句话:完全二叉树:深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h 层所有的结点都连续集中在最左边。 这是不够的,对于一个没有接触过算法、数据结构接触比较少的人来说,是非常不够的,起码用图表示一下。因为完全二叉树特别重要,至少在前端考察二叉树排序等算法上,是非常关键的知识点。我看了很多视频、文章才明白,”所有的结点都连续集中在最左边“到底是什么意思,其实这时候只要有一张图片就可以解释清楚了,文字不够直观,不明白的!

2,对于”中序遍历“,有些不清楚。 文中说”对于二叉树中的任意一个节点,先打印它的左子树,然后是该节点,最后右子树“。然后图中,没有标识出”任意节点“,我看您图的意思应该是”B“,那您应该说清楚,第一步先将B作为任意节点。应该再取一个不那么标准的节点比较一下才更好理解!因为您的文中标题是说”小白都能看懂“,对不起,我跟您说,缺乏这么多细节的,小白看不懂。(我现在懂了,是因为我又参考了好多篇文章,才懂的,光您这篇不够的。但您的意图应该是您这篇就够了才对)