本文由 简悦 SimpRead 转码, 原文地址 roadmap.isylar.com

节点是数据结构中的基础,是构成复杂数据结构的基本组成单位。

1 重点概念

1.1 节点概念

节点是数据结构中的基础,是构成复杂数据结构的基本组成单位。

2 树

2.1 定义

**树(Tree)**是 n(n>=0) 个结点的有限集。n=0 时称为空树。在任意一颗非空树中: 1)有且仅有一个特定的称为根(Root)的结点; 2)当 n>1 时,其余结点可分为 m(m>0) 个互不相交的有限集 T1、T2、......、Tn,其中每一个集合本身又是一棵树,并且称为根的子树。

此外,树的定义还需要强调以下两点: 1)n>0 时根结点是唯一的,不可能存在多个根结点,数据结构中的树只能有一个根结点。 2)m>0 时,子树的个数没有限制,但它们一定是互不相交的。

2.2 结点的度

结点拥有的子树数目称为结点的

**节点的度:**结点拥有的子树数目称为结点的度,叶子结点 就是度为 0 的结点

3 二叉树

3.1 定义

二叉树是 n(n>=0) 个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成。

一棵典型的二叉树如下图所示:

3.2 二叉树特点

由二叉树定义以及图示分析得出二叉树有以下特点: 1)每个结点最多有两颗子树,所以二叉树中不存在度大于 2 的结点。 2)左子树和右子树是有顺序的,次序不能任意颠倒。 3)即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。

3.3 二叉树性质

1)在二叉树的第 i 层上最多有 2(i-1) 个节点 。(i>=1) 2)二叉树中如果深度为 k, 那么最多有 2(k)-1 个节点。(k>=1) 3)n0=n2+1 n0 表示度数为 0 的节点数,n2 表示度数为 2 的节点数。 4)在完全二叉树中,具有 n 个节点的完全二叉树的深度为 [log2n]+1,其中[log2n] 是向下取整。 5)若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点有如下特性:

(1) 若 i=1,则该结点是二叉树的根,无双亲, 否则,编号为 [i/2] 的结点为其双亲结点; (2) 若 2i>n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点; (3) 若 2i+1>n,则该结点无右孩子结点, 否则,编号为 2i+1 的结点为其右孩子结点。

3.4 斜树

斜树:所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。

图 3.2 左斜树

图 3.3 右斜树

3.5 满二叉树

满二叉树:在一棵二叉树中。如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。 满二叉树的特点有: 1)叶子只能出现在最下一层。出现在其它层就不可能达成平衡。 2)非叶子结点的度一定是 2。 3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。

3.6 完全二叉树

完全二叉树:对一颗具有 n 个结点的二叉树按层编号,如果编号为 i(1<=i<=n) 的结点与同样深度的满二叉树中编号为 i 的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。 图 3.5 展示一棵完全二叉树

图 3.5 完全二叉树

特点: 1)叶子结点只能出现在最下层和次下层。 2)最下层的叶子结点集中在树的左部。 3)倒数第二层若存在叶子结点,一定在右部连续位置。 4)如果结点度为 1,则该结点只有左孩子,即没有右子树。 5)同样结点数目的二叉树,完全二叉树深度最小。 :满二叉树一定是完全二叉树,但反过来不一定成立。

3.7 二叉树的存储结构

3.7.1 顺序存储

二叉树的顺序存储结构就是使用一维数组存储二叉树中的结点,并且结点的存储位置,就是数组的下标索引。

图 3.6

图 3.6 所示的一棵完全二叉树采用顺序存储方式,如图 3.7 表示:

图 3.7 顺序存储

由图 3.7 可以看出,当二叉树为完全二叉树时,结点数刚好填满数组。 那么当二叉树不为完全二叉树时,采用顺序存储形式如何呢?

例如:对于图 3.8 描述的二叉树:

图 3.8.png

其中浅色结点表示结点不存在。那么图 3.8 所示的二叉树的顺序存储结构如图 3.9 所示:

图 3.9

其中,∧表示数组中此位置没有存储结点。此时可以发现,顺序存储结构中已经出现了空间浪费的情况。 那么对于图 3.3 所示的右斜树极端情况对应的顺序存储结构如图 3.10 所示:

图 3.10

由图 3.10 可以看出,对于这种右斜树极端情况,采用顺序存储的方式是十分浪费空间的。因此,顺序存储一般适用于完全二叉树。

3.7.2 二叉链表

既然顺序存储不能满足二叉树的存储需求,那么考虑采用链式存储。由二叉树定义可知,二叉树的每个结点最多有两个孩子。因此,可以将结点数据结构定义为一个数据和两个指针域。表示方式如图 3.11 所示:

图 3.11

定义结点代码:

typedef struct BiTNode{
    TElemType data;
    struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;


则图 3.6 所示的二叉树可以采用图 3.12 表示。

图 3.12

图 3.12 中采用一种链表结构存储二叉树,这种链表称为二叉链表。

3.8 二叉树遍历

二叉树的遍历一个重点考查的知识点。

3.8.1 定义

二叉树的遍历是指从二叉树的根结点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次,且仅被访问一次。 二叉树的访问次序可以分为四种:

前序遍历 中序遍历 后序遍历 层序遍历

前,中,后只是指父节点遍历的顺序,前序就是 父节点 -> 左子树 -> 右子树,中序是 左子树 -> 父节点 -> 右子树,后序是 左子树 -> 右子树 -> 父节点

3.8.2 前序遍历

前序遍历通俗的说就是从二叉树的根结点出发,当第一次到达结点时就输出结点数据,按照先向左在向右的方向访问。

3.13

图 3.13 所示二叉树访问如下:

从根结点出发,则第一次到达结点 A,故输出 A; 继续向左访问,第一次访问结点 B,故输出 B; 按照同样规则,输出 D,输出 H; 当到达叶子结点 H,返回到 D,此时已经是第二次到达 D,故不在输出 D,进而向 D 右子树访问,D 右子树不为空,则访问至 I,第一次到达 I,则输出 I; I 为叶子结点,则返回到 D,D 左右子树已经访问完毕,则返回到 B,进而到 B 右子树,第一次到达 E,故输出 E; 向 E 左子树,故输出 J; 按照同样的访问规则,继续输出 C、F、G;

则 3.13 所示二叉树的前序遍历输出为: ABDHIEJCFG

3.8.3 中序遍历

中序遍历就是从二叉树的根结点出发,当第二次到达结点时就输出结点数据,按照先向左在向右的方向访问。

图 3.13 所示二叉树中序访问如下:

从根结点出发,则第一次到达结点 A,不输出 A,继续向左访问,第一次访问结点 B,不输出 B;继续到达 D,H; 到达 H,H 左子树为空,则返回到 H,此时第二次访问 H,故输出 H; H 右子树为空,则返回至 D,此时第二次到达 D,故输出 D; 由 D 返回至 B,第二次到达 B,故输出 B; 按照同样规则继续访问,输出 J、E、A、F、C、G;

则 3.13 所示二叉树的中序遍历输出为: HDIBJEAFCG

3.8.4 后序遍历

后序遍历就是从二叉树的根结点出发,当第三次到达结点时就输出结点数据,按照先向左在向右的方向访问。

图 3.13 所示二叉树后序访问如下:

从根结点出发,则第一次到达结点 A,不输出 A,继续向左访问,第一次访问结点 B,不输出 B;继续到达 D,H; 到达 H,H 左子树为空,则返回到 H,此时第二次访问 H,不输出 H; H 右子树为空,则返回至 H,此时第三次到达 H,故输出 H; 由 H 返回至 D,第二次到达 D,不输出 D; 继续访问至 I,I 左右子树均为空,故第三次访问 I 时,输出 I; 返回至 D,此时第三次到达 D,故输出 D; 按照同样规则继续访问,输出 J、E、B、F、G、C,A;

则图 3.13 所示二叉树的后序遍历输出为: HIDJEBFGCA

虽然二叉树的遍历过程看似繁琐,但是由于二叉树是一种递归定义的结构,故采用递归方式遍历二叉树的代码十分简单。 递归实现代码如下:

void PreOrderTraverse(BiTree T)
{
    if(T==NULL)
    return;
    printf("%c", T->data);  
    PreOrderTraverse(T->lchild);    
    PreOrderTraverse(T->rchild);    
}



void InOrderTraverse(BiTree T)
{
    if(T==NULL)
    return;
    InOrderTraverse(T->lchild); 
    printf("%c", T->data);  
    InOrderTraverse(T->rchild); 
}



void PostOrderTraverse(BiTree T)
{
    if(T==NULL)
    return;
    PostOrderTraverse(T->lchild);   
    PostOrderTraverse(T->rchild);   
    printf("%c", T->data);  
}


3.8.5 层次遍历

层次遍历就是按照树的层次自上而下的遍历二叉树。针对图 3.13 所示二叉树的层次遍历结果为: ABCDEFGHIJ 层次遍历的详细方法可以参考二叉树的按层遍历法

3.8.6 遍历常考考点

对于二叉树的遍历有一类典型题型。 1)已知前序遍历序列和中序遍历序列,确定一棵二叉树。 例题:若一棵二叉树的前序遍历为 ABCDEF,中序遍历为 CBAEDF,请画出这棵二叉树。 分析:前序遍历第一个输出结点为根结点,故 A 为根结点。早中序遍历中根结点处于左右子树结点中间,故结点 A 的左子树中结点有 CB,右子树中结点有 EDF。 如图 3.14 所示:

图 3.14

按照同样的分析方法,对 A 的左右子树进行划分,最后得出二叉树的形态如图 3.15 所示:

图 3.15.png

2)已知后序遍历序列和中序遍历序列,确定一棵二叉树。 后序遍历中最后访问的为根结点,因此可以按照上述同样的方法,找到根结点后分成两棵子树,进而继续找到子树的根结点,一步步确定二叉树的形态。 :已知前序遍历序列和后序遍历序列,不可以唯一确定一棵二叉树。

平衡二叉树的提出就是为了保证树不至于太倾斜,尽量保证两边平衡。因此它的定义如下:

  1. 平衡二叉树要么是一棵空树
  2. 要么保证左右子树的高度之差不大于 1
  3. 子树也必须是一颗平衡二叉树

也就是说,树的两个左子树的高度差别不会太大。

那我们接着看前面的极端情况的二叉排序树,现在用它来构造一棵平衡二叉树。

以 12 为根节点,当添加 24 为它的右子树后,根节点的左右子树高度差为 1,这时还算平衡,这时再添加一个元素 28:

这时根节点 12 觉得不平衡了,我左孩子一个都没有,右边都有俩了,超过了之前说的最大为 1,不行,给我调整!

于是我们就需要调整当前的树结构,让它进行旋转。

因为最后一个节点加到了右子树的右子树,就要想办法给右子树的左子树加点料,因此需要逆时针旋转,将 24 变成根节点,12 右旋成 24 的左子树,就变成了这样(有点丑哈哈):

这时又恢复了平衡,再添加 37 到 28 的右子树,还算平衡:

这时如果再添加一个 30,它就需要在 37 的左子树:

这时我们可以看到这个树又不平衡了,以 24 为根节点的树,明显右边太重,左边太稀,想要保持平衡就 24 得让位给 28,然后变成这样:

丑了点,但的确保持了平衡。

依次类推,平衡二叉树在添加和删除时需要进行旋转保持整个树的平衡,内部做了这么复杂的工作后,我们在使用它时,插入、查找的时间复杂度都是 O(logn),性能已经相当好了。

Reference

深入学习二叉树 (一) 二叉树基础

3 分钟理解完全二叉树、平衡二叉树、二叉查找树