二叉树(上)

扫码关注公众号,获取更多内容

目录

一、什么是树?

二、二叉树

三、二叉树的遍历


一、什么是树?

我们先看下图的例子,这些“树”都有什么特征?

“树”这种数据结构很像真实生活中的“树”,这里面每个元素我们叫做“节点”。

比如下面这幅图,A节点叫做B节点的父节点,B节点是A节点的子节点。B,、C、D这三个节点的父节点是同一个节点,它们之间互称为兄弟节点。没有父节点的节点叫做根节点,下图中E就是根节点。没有子节点的节点叫做叶子节点或者叶节点,比如下图中的G、H、I、J、K、L,他们都是叶子节点。

“树”还有三个比较相似的概念:高度(Height)、深度(Depth)、层(Level),他们的定义如下:

对于上图的理解可能不是那么的直观,我们来看下面的例子:

 

二、二叉树

树的结构多种多样,我们最常用的是二叉树。

二叉树每个节点最多有两个“叉”(两个子节点),分别是左子节点右子节点。二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。我们可以看一下下图所示的二叉树,加深理解:

上图中有两个比较特殊的二叉树,分别是编号2和编号3的这两个。

编号为2的二叉树,叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这样的二叉树叫做满二叉树

编号为3的二叉树,叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,除了最后一层,其他层的节点个数都达到最大,这种二叉树叫做完全二叉树

满二叉树是很好理解与识别的,但完全二叉树的分辨就没那么简单了,我们查看下图:

如何表示(存储)一棵二叉树?

我们有两种办法来存储一棵二叉树,一种是基于指针或者引用的二叉链式存储法,另一种是基于数组的顺序存储法。

链式存储法如下图所示:

每个节点都有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针。这种存储方式比较常用,大部分二叉树代码都是通过这种结构来实现的。

顺序存储法如下图所示:

我们把根节点存储在下标 i = 1 的位置,那左子节点存储在下标 2 * i = 2 的位置,右子节点存储在 2 * i + 1 = 3 的位置。以此类推,B 节点的左子节点存储在 2 * i = 2 * 2 = 4 的位置,右子节点存储在 2 * i + 1 = 2 * 2 + 1 = 5 的位置。

上面的例子是一个完全二叉树,所以仅仅是浪费了一个下标为0的存储位置,如果是非完全二叉树,会浪费更多的数组存储空间,看下面的例子:

所以,当是完全二叉树的时候,用数组存储是最节省内存的方式,用数组存储并不需要像链式存储法那样额外的存储左右子节点指针。

 

三、二叉树的遍历

二叉树有三种经典的遍历方法:前序遍历、中序遍历、后序遍历

前序遍历:对于树中的任意节点来说,先打印这个节点,然后打印它的左子树,最后打印他的右子树(节点本身->左子树->右子树)(根、左、右)。

中序遍历:对于树中的任意节点来说,先打印他的左子树,然后再打印它本身,最后打印右子树(左子树->节点本身->右子树)(左、根、右)。

后序遍历:对于树中的任意节点来说,先打印左子树,然后打印它的右子树,最后打印这个节点本身(左子树->右子树->节点本身)(左、右、根)。

本质上,二叉树的前中后序遍历就是一个递归的过程,比如前序遍历,其实就是打印根节点,然后再递归地打印左子树,最后递归打印右子树。

我们可以写出如下的递归公式:

前序遍历的递推公式:
preOrder(r) = print r->preOrder(r->left)->preOrder(r->right)

中序遍历的递推公式:
inOrder(r) = inOrder(r->left)->print r->inOrder(r->right)

后序遍历的递推公式:
postOrder(r) = postOrder(r->left)->postOrder(r->right)->print r

我们用代码实现一下,还是很简单的:

const tree = {
    value: 1,
    left: {
        value: 2,
        left: {
            value: 4
        },
        right: {
            value: 5
        }
    },
    right: {
        value: 3,
        left: {
            value: 6
        },
        right: {
            value: 7
        }
    }
}


let arrDLR = []
// 前序遍历(根左右)
function DLR(obj) {
    arrDLR.push(obj.value)
    obj.left && DLR(obj.left)
    obj.right && DLR(obj.right)
}

let arrLDR = []
// 中序遍历(左根右)
function LDR(obj) {
    obj.left && LDR(obj.left)
    arrLDR.push(obj.value)
    obj.right && LDR(obj.right)
}

let arrLRD = []
// 后序遍历(左右根)
function LRD(obj) {
    obj.left && LRD(obj.left)
    obj.right && LRD(obj.right)
    arrLRD.push(obj.value)
}

DLR(tree)
console.log(arrDLR) //[ 1, 2, 4, 5, 3, 6, 7 ]

LDR(tree)
console.log(arrLDR) // [ 4, 2, 5, 1, 6, 3, 7 ]

LRD(tree)
console.log(arrLRD) // [ 4, 5, 2, 6, 7, 3, 1 ]

我们来分析下前中后序遍历的时间复杂度:

从顺序图中我们可以发现,每个节点做多被访问两次,所以遍历操作的时间复杂度与节点个数n成正比,所以二叉树遍历的时间复杂度是O(n)

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值