【数据结构与算法】初识树和二叉树

86 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

🔥 本文由 程序喵正在路上 原创,在稀土掘金首发!
💖 系列专栏:数据结构与算法
🌠 首发时间:2022年11月23日
🦋 欢迎关注🖱点赞👍收藏🌟留言🐾
🌟 一以贯之的努力 不得懈怠的人生

树的基本概念

树是 n(n0)n (n \geq 0) 个结点的有限集合,n=0n = 0 时,称之为空树,这是一种特殊的情况

在任意一颗非空树中应满足:

  1. 有且仅有一个特定的成为根的结点
  2. n>1n > 1 时,其余结点可分为 m(m>0)m (m > 0) 个互不相交的有限集合 T1,T2,,TmT_1, T_2, \dots, T_m,其中每个集合本身又是一棵树,并且称为根节点的子树

树是一种递归定义的数据结构

image.png

非空树的特性:

  • 有且仅有一个根节点
  • 没有后继的结点称之为 “叶子结点” 或者 “终端结点”
  • 有后继的结点称之为 “分支结点” 或者 “非终端结点”
  • 除了根节点以外,任何一个结点都有且仅有一个前驱
  • 每个结点可以有 0 个或者多个后继

树的基本术语

术语描述
结点的度结点拥有的子树数
树的度树内各结点度的最大值
祖先结点从根到该结点所经分支上的所有结点
子孙结点以某结点为根的子树中的任一结点都称为该结点的子孙
双亲结点(父节点)结点的上一结点
孩子结点结点的子树的根称为该结点的孩子
兄弟结点同一个双亲的孩子之间称为兄弟
堂兄弟结点双亲在同一层的结点互为堂兄弟
层次结点的层次从根开始定义起,根为第一层,根的孩子为第二层。树中任一结点的层次等于其双亲结点的层次加 1
树的深度树中结点的最大层次称为树的深度或高度
有序树和无序树有序树,从逻辑上看,树中结点的各子树从左到右是有次序的,不能互换;无序树反之
森林m(m0)m(m \geq 0) 颗互不相交的树的集合

树的常考性质

常见考点1:结点数 = 总度数 + 1

结点的度 —— 结点有几个孩子(分支)

解释:所有的度相加再加上根节点即为总结点数

常见考点2:度为 mm 的树、mm叉树的区别

树的度 —— 各结点的度的最大值;mm叉树 —— 每个结点最多只能有 mm 个孩子的树

对比

度为 mm 的树mm叉树
任意结点的度 m\leq m (最多 mm 个孩子)任意结点的度 m\leq m (最多 mm 个孩子)
至少有一个结点度 =m= m (有 mm 个孩子)允许所有结点的度都 <m< m
一定是非空树,至少有 m+1m+1 个结点可以是空树

常见考点3:度为 mm 的树第 ii 层至多有 mi1m^{i-1} 个结点 (i1i \geq 1),mm叉树第 ii 层至多有 mi1m^{i-1} 个结点 (i1i \geq 1

常见考点4:高度为 hhmm叉树至多有 mh1m1\frac{m^h - 1}{m - 1} 个结点

常见考点5:高度为 hhmm叉树至少有 hh 个结点;高度为 hh、度为 mm 的树至少有 h+m1h+m-1 个结点

常见考点6:具有 nn 个结点的 mm叉树的最小高度为 logm(n(m1)+1)\lceil log_m{(n(m - 1) + 1)} \rceil (其中的符号为向上取整)

解释:要得到高度最小的情况,那么意味着所有结点都有 mm 个孩子

我们假设最小高度为 hh,根据考点4、5,有

mh11m1<nmh1m1\frac{m^{h-1} - 1}{m - 1} < n \leq \frac{m^h - 1}{m - 1}
mh1<n(m1)+1mhm^{h-1} < n(m - 1) + 1 \leq m^h
h1<logm(n(m1)+1)hh - 1 < log_m{(n(m - 1) + 1)} \leq h

最后解得

hmin=logm(n(m1)+1)h_{min} = \lceil log_m{(n(m - 1) + 1)} \rceil

二叉树的基本概念

二叉树是 n(n0)n(n \geq 0) 个结点的有限集合

  1. 或为空二叉树,即 n=0n=0
  2. 或为由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树

image.png

几种特殊的二叉树

满二叉树 —— 一棵高度为 hh,且含有 2h12^h - 1 个结点的二叉树

image.png

满二叉树的特点:

  • 只有最后一层有叶子结点
  • 不存在度为 11 的结点
  • 按层序从 11 开始编号,结点 ii 的左孩子为 2i2i,右孩子为 2i+12i + 1;结点 ii 的父节点为 i/2\lfloor i / 2 \rfloor (如果有的话)

完全二叉树 —— 当且仅当其每个结点都与高度为 hh 的满二叉树中编号为 1n1 \sim n 的结点一一对应时,称为完全二叉树

image.png

完全二叉树的特点:

  • 只有最后两层可能有叶子结点
  • 最多只有一个度为 11 的结点
  • 按层序从 11 开始编号,结点 ii 的左孩子为 2i2i,右孩子为 2i+12i + 1;结点 ii 的父节点为 i/2\lfloor i / 2 \rfloor (如果有的话)
  • in/2i \leq \lfloor n / 2 \rfloor 为分支结点,i>n/2i > \lfloor n / 2 \rfloor 为叶子结点
  • 如果某结点只有一个孩子,那么一定是左孩子

二叉排序树 —— 一棵二叉树或者空二叉树,或者是具有如下性质的二叉树:

  • 左子树上所有结点的关键字均小于根结点的关键字
  • 右子树上所有结点的关键字均大于根结点的关键字
  • 左子树和右子树又各是一棵二叉排序树

image.png

二叉排序树可用于元素的排序和搜索

平衡二叉树 —— 树上任一结点的左子树和右子树的深度之差不超过 11

image.png

平衡二叉树能有更高的搜索效率

二叉树的常考性质

常见考点1:设非空二叉树中度为 010、122 的结点个数分别为 n0n1n_0、n_1n2n_2,则 n0=n2+1n_0 = n_2 + 1 (叶子结点比二分支结点多一个)

解析:

我们假设树中结点总数为 nn,则

  1. n=n0+n1+n2n = n_0 + n_1 + n_2
  2. n=n1+2n2+1n = n_1 + 2n_2 + 1(树的结点数 == 总度数 +1+ 1

化简一下两个等式,就可以得到 n0=n2+1n_0 = n_2 + 1

常见考点2:二叉树第 ii 层至多有 2i12^{i-1} 个结点 (i1)(i \geq 1)mm 叉树第 ii 层至多有 mi1m^{i-1} 个结点 (i1)(i \geq 1)

常见考点3:高度为 hh 的二叉树至多有 2h12^h - 1 个结点(满二叉树),高度为 hhmm 叉树至多有 mh1m1\frac{m^h - 1}{m - 1} 个结点

完全二叉树的常考性质

常见考点1:具有 nn(n>0)(n > 0) 结点的完全二叉树的高度 hhlog2(n+1)\lceil log_2{(n + 1)} \rceil 或者 log2n+1\lfloor log_2{n} \rfloor + 1

解析:

第一个解:

  • 高为 hh 的满二叉树共有 2h12^h - 1 个结点
  • 高为 h1h - 1 的满二叉树共有 2h112^{h-1} - 1 个结点

所有具有 nn(n>0)(n > 0) 结点的完全二叉树一定满足 2h11<n2h12^{h-1} - 1 < n \leq 2^h - 1

2h1<n+12h2^{h-1} < n + 1 \leq 2^h
h1<log2(n+1)hh - 1 < log_2{(n + 1)} \leq h

解得

h=log2(n+1)h = \lceil log_2{(n + 1)} \rceil

第二个解:

  • 高为 hh 的完全二叉树至少有 2h12^{h - 1} 个结点
  • 高为 hh 的完全二叉树至多有 2h12^{h} - 1 个结点

所有具有 nn(n>0)(n > 0) 结点的完全二叉树一定满足 2h1n<2h2^{h-1} \leq n < 2^h

h1log2n<hh - 1 \leq log_2{n} < h

解得

h=log2n+1h = \lfloor log_2{n} \rfloor + 1

推论:第 ii 个结点所在层次为 log2(n+1)\lceil log_2{(n + 1)} \rceil 或者 log2n+1\lfloor log_2{n} \rfloor + 1

常见考点2:对于完全二叉树,可以由结点数 nn 推出度为 010、122 的结点个数为 n0n1n_0、n_1n2n_2

解析:

① 我们知道,完全二叉树最多只有一个度为 11 的结点,即 n0=0n_0 = 011

② 由二叉树的常考考点 11 我们知道 n0=n2+1n_0 = n_2 + 1,很容易推出 n0n_0n2n_2 肯定一奇一偶,因此 n0+n2n_0 + n_2 一定是奇数

③ 然后我们分情况讨论:

  • 若完全二叉树有 2k2k (偶数)个结点,则必有 n1=1,n0=k,n2=k1n_1 = 1, n_0 = k, n_2 = k - 1
  • 若完全二叉树有 2k12k - 1 (奇数)个结点,则必有 n1=0,n0=k,n2=k1n_1 = 0, n_0 = k, n_2 = k - 1

二叉树的存储结构

顺序存储

#define MaxSize 100
struct TreeNode {
	ElemType value;		//结点中的数据元素
	bool isEmpty;		//结点是否为空
};

TreeNode t[MaxSize];

我们定义一个长度为 MaxSizeMaxSize 的数组 tt,按照从上到下、从左到右的顺序依次存储完全二叉树中的各个结点

在初始化时我们需要将所有结点标记为空

for (int i = 0; i < MaxSize; ++i) {
	t[i].isEmpty = true;
}

由于树的结点是从 11 开始计数的,我们可以让数组的第一个位置空缺,这样可以保证数组下标和结点编号一致;当然,你也可以不这么做,只不过把数组下标换成结点编号的时候要记得加 11

几个重要常考的基本操作:

  • ii 的左孩子:2i2i
  • ii 的右孩子:2i+12i + 1
  • ii 的父节点:i/2\lfloor i / 2 \rfloor
  • ii 所在的层次:log2(n+1)\lceil log_2{(n + 1)} \rceil 或者 log2n+1\lfloor log_2{n} \rfloor + 1

若完全二叉树中共有 nn 个结点,则怎么

  • 判断 ii 是否有左孩子?—— 2in?2i \leq n ?
  • 判断 ii 是否有右孩子?—— 2i+1n?2i + 1 \leq n ?
  • 判断 ii 是否为叶子 / 分支结点? —— i>n/2?i > \lfloor n / 2 \rfloor ?

那如果不是完全二叉树,我们依然按层序将各节点顺序存储的话,上面的方案就行不通了

为了解决这个问题,在二叉树的顺序存储中,我们一定要把二叉树的结点编号和完全二叉树一一对应起来

image.png

这样的话基本操作依旧符合

  • ii 的左孩子:2i2i
  • ii 的右孩子:2i+12i + 1
  • ii 的父节点:i/2\lfloor i / 2 \rfloor
  • ii 所在的层次:log2(n+1)\lceil log_2{(n + 1)} \rceil 或者 log2n+1\lfloor log_2{n} \rfloor + 1

但是对于各个判断就不行了,这时候 isEmptyisEmpty 的作用就展现出来了

看到这里,相信你也发现了顺序存储的不足之处,在最坏情况下,高度为 hh 且只有 hh 个结点的单支树(所有结点只有右孩子),也至少需要 2h12^h - 1 个存储单元

结论:二叉树的顺序存储结构只适合存储完全二叉树

链式存储

//二叉树的结点
typedef struct BiTNode {
	ElemType data;						//数据域
	struct BiTNode *lchild, *rchild;	//左、右孩子指针
}BiTNode, *BiTree;

image.png

如果一个二叉链表有 nn 个结点,那么就有 2n2n 个指针域,同时会有 n+1n + 1 个空链域,这些空链域我们之后可以用来构造线索二叉树

下面我们构造一棵简单的二叉树

struct ElemType {
	int value;
};

typedef struct BiTNode {
	ElemType data;						//数据域
	struct BiTNode *lchild, *rchild;	//左、右孩子指针
}BiTNode, *BiTree;

//定义一棵空树
BiTree root = NULL;

//插入根节点
root = (BiTree)malloc(sizeof(BiTree));
root->data = {1};
root->lchild = NULL;
root->rchild = NULL;

//插入新节点
BiTNode *p = (BiTNode*)malloc(sizeof(BiTNode));
p->data = {2};
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p;	//作为根节点的左孩子

通过观察,我们发现想要找到二叉链表中指定结点 pp 的左右孩子非常简单,只需要找到 pp 的左右孩子指针指向哪里就可以了;但是想要找到结点 pp 的父结点的话,我们只能从根开始遍历寻找,当结点非常多的时候,这就很不方便

所以出现了三叉链表 —— 方便找父结点

typedef struct BiTNode {
	ElemType data;						//数据域
	struct BiTNode *lchild, *rchild;	//左、右孩子指针
	struct BiTNode *parent;				//父结点指针
}BiTNode, *BiTree;