二叉树(上)

Posted 编程加油站

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二叉树(上)相关的知识,希望对你有一定的参考价值。

一、什么是树?

我们先看下图的例子,这些“树”都有什么特征?
二叉树(上)
“树”这种数据结构很像真实生活中的“树”,这里面每个元素我们叫做“节点”。
比如下面这幅图,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)。

以上是关于二叉树(上)的主要内容,如果未能解决你的问题,请参考以下文章

判断二叉树是否对称的代码

代码题— 二叉树的深度

python代码判断两棵二叉树是否相同

二叉树——高度和深度

判断一颗二叉树是否为二叉平衡树 python 代码

剑指OFFER 序列化二叉树