#数据结构 #C
数据结构树,与自然界的树类似,从树根生长,逐级分支。 树分为空树和非空树,空树是指结点为0的树。
- 有且仅有一个根结点
- 没有后继的结点称为“叶子结点”(或终端结点)
- 有后继的结点称为“分支结点”(或非终端结点)
- 除了根结点外,任何一个结点都有且仅有一个前驱
- 每个结点可以有0个或多个后继 除了根结点外,任何一个结点都有且仅有一个前驱。
树是
因此在任意一棵非空树中应满足:
- 有且仅有一个特定的称为根的结点。
- 当
$n > 1$ 时,其余结点可分为$m(m > 0)$ 个互不相交的有限集合$T_1, T_2,…, T_m$ ,其中每个集合本身又是一棵树,并且称为根结点的子树。
由树的特点可以容易得出,树天然是一种递归定义的数据结构。
属性:
- 结点的层次(深度)—— 从上往下数
- 结点的高度—— 从下往上数
- 树的高度(深度)——总共多少层
- 结点的度——有几个孩子(分支)
- 树的度——各结点的度的最大值
森林。森林是m(m ≥ 0)棵互不相交的树的集合。
森林在机器学习算法中有随机森林算法。
树有空树的状态,森林也有空森林(m ≥ 0)的状态。
树的结点个数 = 总度数 + 1
由于根结点不是任何结点的分支,所以上式成立。
首先概念明确:
- 树的度是指 ----> 各个结点的度的最大值
m
叉树是指 ----> 每个结点最多能够有m
个孩子的树
度是m的树 | m叉树 |
---|---|
任意结点的度 ≤ m(最多m个孩子) | 任意结点的度 ≤ m(最多m个孩子) |
至少有一个结点度 = m(有m个孩子) | 允许所有结点的度都 < m |
一定是非空树,至少有m+1个结点 | 可以是空树 |
度为m的树第i层最多有
m叉的树第i层最多有
高度为h
的m
叉树最多有结点个数:
高度为h
的m
叉树至少有
高度为h
,度为m
的树,至少有
具有n个结点的m叉树的最小高度为:
二叉树是
- 可能情况1:空二叉树,即n = 0。
- 可能情况2:由一个根结点和两个互不相交的被称为根的左子树和右子树组成。 左子树和右子树又分别是一棵二叉树。
特点:
- 每个结点至多只有两棵子树
- 左右子树不能颠倒(二叉树是有序树)
二叉树由于其左右子树的状态,存在以下五种类型。
- 一棵高度为h,且含有
$2^{h - 1}$ 个结点的二叉树。 - 只有最后一层有叶子结点
- 不存在度为 1 的结点
- 按层序从 1 开始编号,结点
$i$ 的左孩子为$2i$ ,右孩子为$2i+1$ ;结点 i 的父结点为$⌊i/2⌋$ (如果有的话)
当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。 特点:
- 只有最后两层可能有叶子结点
- 最多只有一个度为1的结点(上图中的6结点)
- 按层序从 1 开始编号,结点
$i$ 的左孩子为$2i$ ,右孩子为$2i+1$ ;结点 i 的父结点为$⌊i/2⌋$ (如果有的话) -
$i≤ ⌊n/2⌋$ 为分支结点,$i≥ ⌊n/2⌋$ 为叶子结点。
一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
- 左子树上所有结点的关键字均小于根结点的关键字;
- 右子树上所有结点的关键字均大于根结点的关键字。
- 左子树和右子树又各是一棵二叉排序树。
二叉排序树可用于元素的排序、搜索。
平衡二叉树。树上任一结点的左子树和右子树的深度之差不超过1。他是一个“胖胖的、丰满的树”。 平衡二叉树能有更高的搜索效率。
结论:
设非空二叉树中度为0、1和2的结点个数分别为
理由:假设树中结点总数为 n,则
$n = n_0 + n_1 + n_2$ -
$n = n_1 + 2*n_2 +1$ (树的结点数 = 总度数 + 1,除去根结点的其他是度)
例如上图中,叶子结点(蓝色)5个,二分支结点(红色)4个,满足上图。
结论:
具有
n(n > 0)
个结点的完全二叉树的高度h
为:
$$⌈ \log_{2}{(n+1)}⌉$$ 或者$$⌊\log_{2}{n}⌋ + 1$$
- 高为
$h$ 的满二叉树共有$2^{h} − 1$ 个结点, - 高为
$h - 1$ 的满二叉树共有$2^{h-1}−1$ 个结点,
联立上式即可推导出。
对于完全二叉树,可以由的结点数 n 推出度为0、1和2的结点个数为
如果完全二叉树有
$2k$ 个(偶数)个结点,则必有$n_1=1, n_0 = k, n_2 = k-1$
如果完全二叉树有
$2k-1$ 个(奇数)个结点,则必有$n_1=0, n_0 = k, n_2 = k-1$
例如,上图完全二叉树结点数12个,叶子结点6个,二分支5个,一分支1个。
以二叉树为例子,以两种不同的存储方式,展示树这一种数据结构的特点。
使用数组存储元素,并添加标志位,代表该位置是否有数据。
#define MaxSize 100
typedef struct {
Element value; // 结点中的数据元素
bool isEmpty; // 结点是否为空
} TreeNode;
TreeNode tree[MaxSize];
定义一个长度为 MaxSize
的数组 tree
,按照从上至下、从左至右的顺序依次存储完全二叉树中的各个结点。
// 初始化时所有结点标记为空
void BiTreeSeqInit()
{
for (int i = 0; i < MaxSize; i++) {
tree[i].isEmpty = true;
tree[i].value = 0;
}
}
让第一个位置空缺,保证数组下标和结点编号一致。
在完全二叉树中:
- i 的左孩子是 2i
- i 的右孩子是 2i + 1;
- i 的父结点
$⌊i / 2⌋$ - i 所在的层次:$⌈\log_{2}{(n+1)}⌉$ 或者
$⌊\log_{2}{(n+1)}⌋$
判断场景 | 条件 |
---|---|
判断i是否有左孩子? | |
判断i是否有右孩子? | |
判断i是否是叶子/ 分支结点? |
二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来。
最坏情况:高度为 h 且只有 h 个结点的单支树(所有结点只有右孩子),也至少需要
最坏情况是指同样的存储空间下,存储的数据更少,密度更低。
同样,二叉树也可以使用链表的形式存储。
typedef struct BiTNode {
ElemType data;
struct BiTNode *lchild, *rchild; // 左右孩子指针
} BiTNode, *BiTree;
n
个结点的二叉链表共有 n + 1
个空链域。
实际举例:
// 假设结点存储数据时int类型
typedef struct BiTNode {
int data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
// 定义一个空树
BiTree root = NULL;
// 插入根结点
root = (BiTree)malloc(sizeof(BiTNode));
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; // 作为根结点的左孩子
/* ..code.. */
对于这样的二叉链表而言,找对应结点的子结点相对容易,而对于给定结点找到其父结点,相对较难。只能通过从头结点开始遍历查找。
因此,需要在原有的结构体上新增指向父结点的指针,这就是三叉链表表示树的数据结构:
typedef struct BiTNode {
ElemType data;
struct BiTNode *lchild, *rchild; // 左、右孩子指针
struct BiTNode *parent; // 父结点指针
} BiTNode, *BiTree; // 三叉链表,方便找父结点