树的定义:树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝 上,而叶朝下的。
树的结构有以下定义:
有一个特殊的结点,称为根结点,根节点没有前驱结点
除根节点外,其余结点被分成M(M>0)个互不相交的集合 T 1 、 T 2 、 . . . . . . 、 T m T_1、T_2、......、T_m T1、T2、......、Tm,其中每一个集合 T i T_i Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。
树是递归定义的。
在树形结构中,子树之间不能有交集,否则就不是树形结构。
任何树都会被分为根和子树,树的每一个根节点之下的,都是它的子树
例如下例的右图,A作为根节点,它的子树是BCD;同样的,B作为根节点,它的子树是EF。以此类推…
- 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6,B为0,D为1,F为3;
- 叶子节点(终端节点):度为0的节点称为叶子节点; 如上图:B、C、H、I…等节点下面均没有子树且度数为0。
- 分支节点(非终端节点):度不为0的节点为分支节点; 如上图:D、E、F、G…等节点下均有子树且度数 !=0 ,即为分支节点;
- 双亲节点(父节点):若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图: A 包含 B,A是B的父节点;
- 孩子节点(子节点):一个节点含有的子树的根节点称为该节点的子节点;如上图:B 包含于 A,B是A的孩子节点;
- 兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C、D、F、G有共同父节点A,即兄弟节点,同理P、Q也是;
- 树的度:一棵树中,最大的节点的度称为树的度; 如上图:所有节点中度最大的为A节点为6,所以树的度为6;
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,A结点有4层;
- 树的高度或深度:树中节点的最大层次; 如上图:树的高度为4;
- 堂兄弟节点:双亲在同一层的节点互为堂兄弟结点;如上图:H、I、N、M互为堂兄弟节点;
- **节点的祖先:**从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先;
- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙;
- 森林:由m(m>0)棵互不相交的树的集合称为森林。
上述概念中,需要重点理解的就是:叶节点、双亲节点、孩子节点、树的高度与深度、节点的子孙
看了上面这些,你有没有想过,该如何通过代码去实现以上的结构呢?接下来我们来一起思考如何更高效的实现树的结构实现。
树的实现方式有很多种,数据表示很简单,但在树中并没有明确规定一个节点之下有几个孩子,所以树的重中之重应该是如何存储它每个结点的孩子。
**方式1:**假设说明了树的度为N;
struct TreeNode
{
int data; // 用来存储数据的成员
struct TreeNode* subs[N]; // 用来存储孩子结点(N个度)的指针数组
}
//这种方式的缺点显而易见,很有可能会出现不少的数组空间未使用导致内存空间的浪费
**方式2:**没有说明树的度数。
struct TreeNode
{
int data; // 用来存储数据的成员
SeqList s; // 在顺序表里存结点的指针
}
typedef struct TreeNode SLDataType; // 这里是一个顺序表SeqList
// 这种方式的缺点就是,结构过于复杂。因为SeqList这里已经是一个二级指针
**方式3:**结构体数组存储(双亲表示法)
// 这种方法的主要操作就是,通过下面的结构体TreeNode存储有效信息,最后将其存储到parentArr数组中。
struct TreeNode // 数组存储信息
{
int data; // 结点数据
int parent; // 父亲结点的下标位置
}
struct TreeNode parentArr[10]; // 存储结点的数组
// 在这个TreeNode结构体中,data表示当前结点的数据,而parent为这个结点的父节点的下标位置。根节点标记为-1;写入数据后存入结构体数组;
// 具体存储结果可以参考如下举例
data | A | B | C | D | E | H | I | J |
---|---|---|---|---|---|---|---|---|
parent | -1 | 0 | 0 | 0 | 0 | 3 | 4 | 4 |
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
data: 结点数据
parent: 父亲结点的下标位置
下标:数组下标
上面的3种方法各有优缺点,但最常见,也是最实用的方式是------左孩子右兄弟
方式4:左孩子右兄弟法。之后常用的树结构同样也是左孩子右兄弟结构。
它首先会从根节点开始,从左至右通过child记录子节点第一个孩子,再用brother找到兄弟节点C,再用child记录孩子节点以此继续递推下去。从而实现一个无论父节点有多少个子节点都能被记录到。例如,如果当你想找到A节点的子节点时,先通过child到第一个孩子处,接下来找到其他子节点就是像遍历链表一样一路过来即可。
同时再来看看这个结构的缺陷,这个结构你会发现它并没有空间浪费,在遍历时就是直来直去的链接关系。目前来看,这个结构是没有缺点的!
参考代码:
typedef int DataType;
struct Node
{
DataType data; // 节点数据
struct Node* firstChild; // 第一个孩子的节点(永远指向第一个孩子,叶子则为NULL)
struct Node* pNextBrother; // 指向其下一个兄弟节点(永远指向其的兄弟节点,否则为NULL)
};
data | A | B | C | D | E | F | G | H | I |
---|---|---|---|---|---|---|---|---|---|
firstChild | B | D | G | NULL | H | NULL | NULL | NULL | NULL |
pNextBrother | NULL | C | NULL | E | F | NULL | NULL | I | NULL |
一棵二叉树是结点的一个有限集合,该集合:
可能为空;
由一个根节点加上两棵别称为左子树和右子树的二叉树组成
从上图可以看出:
二叉树不存在度大于2的结点(度数最大为2)
二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。
任何二叉树可以出现以下情况
一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。
满二叉树的性质
所有叶子节点都在最后一层;
所有分支节点都有两个孩子;
如果一个满二叉树的层数为k,则树的第k层有$ 2^{k-1}$个节点;
如果一个满二叉树的层数为h,则树的节点总数是 2 h − 1 2^{h-1} 2h−1;
如果一个满二叉树有N个节点,则树的高度是 $log_2 (N+1) $。
完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
完全二叉树的性质:
前N-1层都是满的;
最后一层不满,但是最后一层是从左到右连续的。(例如上图最后一层,如果结点只有右孩子而没有左孩子就不是完全二叉树)
高度为h的完全二叉树,其节点个数范围是[ 2 h − 1 2^{h-1} 2h−1, 2 h − 1 2^h - 1 2h−1]。
二叉树的性质可以有以下几点:
若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2 i − 1 2^{i-1} 2i−1 个结点.
若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2 h − 1 2^h - 1 2h−1 .
(每一层的个数是一个等比数列)
对任何一棵二叉树, 如果度为0其叶结点个数为 n 0 n_0 n0 , 度为2的分支结点个数为 n 2 n_2 n2,则有 n 0 n_0 n0= n 2 n_2 n2+1(度为0的永远比度为2的多一个)
若规定根节点的层数为1,具有** n n n个结点的满二叉树的深度 h h h,** h = l o g 2 ( n + 1 ) h = log_2(n+1) h=log2(n+1)
对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
若i>0,i位置节点的双亲序号:(i - 1) / 2;i=0,i为根节点编号,无双亲节点;
若2i+1
若2i+2
满二叉树一定是一个完全二叉树,但完全二叉树不一定是满二叉树。
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
顺序结构存储就是使用数组来存储,**二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。**不过在现实中使用中只有堆才会使用数组来存储,关于堆在后面的章节会专门讲解。
二叉树顺序存储一般只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。
由上图可知,通过顺序存储会发现它们存储非常规律,可以很好表示每一个结点。用顺序存储的方式可以通过节点下标算出其父亲节点或左右孩子的下标位置。
假设parent是父亲节点为其在数组的下标,则有:
leftchild = parent * 2 + 1 rightchild = parent * 2 + 2
假设child是孩子结点,不论左右,则有:
parent = (child - 1) / 2
// 这个会在后面的堆的实现有非常重要的地位。
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。
链式结构又分为二叉链和三叉链
二叉链:有两个指针指着左右孩子
三叉链:不仅有两个指针存左右孩子,还有一个指针用来存储父节点的位置。
一般时都是在用二叉链,会在高阶数据结构如红黑树会用到三叉链 。
某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )
A、不存在这样的二叉树
B、200
C、198
D、199
在具有 2n 个结点的完全二叉树中,叶子结点个数为( )
A、n
B、n+1
C、n-1
D、n/2
一棵完全二叉树的节点数为531个,那么这棵树的高度为( )
A、11
B、10
C、8
D、12
一个具有767个节点的完全二叉树,其叶子节点个数为()
A、383
B、384
C、385
D、386
参考答案:
答案: B,
解析:叶子结点:度为0的节点称为叶子节点;而二叉树性质任意一棵二叉树中度为0的永远比度为2的多一个,题中度数为2的节点有199个,那么其叶子结点(度为0)就有199+1=200个;
答案:A,
解析:这题用性质推导法;
设该完全二叉树的度为0的节点个数为 x 0 x_0 x0,度为1的节点个数为 x 1 x_1 x1,度为0的节点个数为 x 2 x_2 x2,且根据题意其具有2n个节点
∴ 可得节点总数方程为: x 0 x_0 x0 + x 1 x_1 x1 + x 2 x_2 x2 = 2n;
∵ 任意一棵二叉树中度为0的永远比度为2的多一个;即: x 2 x_2 x2 = x 0 x_0 x0 - 1;
∴ 得方程 2 x 0 x_0 x0 + x 1 − 1 x_1 - 1 x1−1 = 2n;
题目中问叶子节点的个数,那就是问 x 0 x_0 x0的个数是多少,可是现在 x 1 x_1 x1(度数为1)还无法得出,现在我们来重新回顾一下完全二叉树的性质。
在完全二叉树中,前N-1层都是满的而最后一层不满,但是在最后一层是从左到右连续的。现在我们可以得出完全二叉树中度数为1的节点数量取值范围就是 x 1 x_1 x1 ∈ [0, 1],最多只有一个度为1的节点。
再回到这一题,方程 2 x 0 x_0 x0 + x 1 x_1 x1 -1 = 2n;,而x1的取值范围就是[0, 1],则 x 1 x_1 x1
可取0或1.那 x 1 x_1 x1取什么会好呢?
当 x 1 x_1 x1 = 0时,节点方程为 2 x 0 x_0 x0- 1 = 2n, x 0 x_0 x0 = (2n+1) ÷ 2;
当 x 1 x_1 x1 = 1时,节点方程为 2 x 0 x_0 x0 = 2n, x 0 x_0 x0 = n;
∵ x 1 x_1 x1 = 1时, x 0 x_0 x0非整数个,舍去;
∴ x 1 x_1 x1 = 1时符合题意,故答案选A n;
这道题不仅要结合完全二叉树的性质,还需要结合公式来进行解题,是一个非常好的题。再试试算一算同类型的第四题吧!
答案: B,
解析:在完全二叉树性质中我们提到,一个高度为h的完全二叉树,其节点个数范围是[ 2 h − 1 2^{h-1} 2h−1, 2 h − 1 2^h - 1 2h−1]。那么它的推理过程应该是什么样的呢?
在完全二叉树中,最大的情况就是完全二叉树的最后一层节点数量都是满的,即为满二叉树,所以最大值可以取到 2 h − 1 2^h - 1 2h−1,那么最小取值是什么呢?试想一下,最后一层最少的情况应该是最后一层只有一个节点,所有就是上一层的节点数量再 + 1,所以就有当最大层数为h,h-1层的节点个数就是 2 h − 1 − 1 2^{h-1}-1 2h−1−1,再加上最后一层的节点就是 2 h − 1 − 1 + 1 2^{h-1}-1+1 2h−1−1+1, 所以就有一个高度为h的完全二叉树的节点个数范围是[ 2 h − 1 2^{h-1} 2h−1, 2 h − 1 2^h - 1 2h−1]。
综上,当h = 11时,该完全二叉树的节点个数范围为[1024, 2047]当h = 10时,该完全二叉树的节点个数范围为[512, 1023]当h = 8时, 该完全二叉树的节点个数范围为[128, 255] 当h = 12时,该完全二叉树的节点个数范围为[2048, 4095]
故答案选 B 10
答案: A 383,
解析:同第2题。
二叉树的基础说完啦!在学习二叉树时,通常不会直接学习二叉树的增删查改,因为二叉树通常不会直接用来存储数据,而是由二叉树进行各种各样的变形来完成各式各样的需求,所以在学习二叉树时我们更注重学习二叉树的结构与概念,在后面例如AVL、红黑树时再真正的能高效存储数据结构时才学习增删查改在开始进入二叉树的完整结构操作时,我们需要学习一个内容------堆,这是开始学二叉树前需要了解的一个数据结构。
创作不易,如果你觉得这篇文章对你有用的话,别忘了点赞在看+关注噢!
后续,我们会将最新文章第一时间发送在微信公众号:“01编程小屋” 当中,关注小屋,学习编程不迷路.别忘了关注我们的公众号以免错过了噢!