树的定义及性质
树的基本概念
根节点,边,分支结点,叶子结点,子树。
空树:结点数为0的树
非空树:有且仅有一个根节点。没有后继的结点称为叶子结点(或终端结点),有后继的结点称为分支结点(或非终端结点)。除了根节点外,任何一个结点都有且仅有一个前驱。每个结点可以有0个或多个后继。
结点之间的关系描述:祖先结点,子孙结点,双亲结点(父节点),孩子结点,兄弟结点,堂兄弟结点
两个结点之间的路径:只能从上往下。路径长度:经过几条边。
结点的深度:从上往下数。结点的高度:从上往下数。
树的高度/深度:总共多少层。
结点的度:有几个孩子/分支。
树的度:各结点的度的最大值。
有序树:逻辑上看,树中结点各子树从左至右是有次序的,不能互换。
无序树:逻辑上看,树中结点各子树从左至右是无次序的,可以互换。
森林:m棵互不相交的树的集合。
树的性质
-
结点数 = 总度数+1
-
树的度(各结点的度的最大值)与m叉树(每个结点最多只能有m个孩子)的区别
-
度为
m
的树第i
层至多有 m i − 1 m^{i-1} mi−1个结点,m
叉树第i
层至多有 m i − 1 m^{i-1} mi−1个结点 -
高度为
h
的m
叉树至多有 m h − 1 m − 1 \frac{m^h-1}{m-1} m−1mh−1个结点 -
高度为
h
的m
叉树至少有h
个结点;高度为h
,度为m
树至少有h+m-1
个结点 -
n
个结点的m
叉树的最小高度为 [ l o g m ( n ( m − 1 ) + 1 ) ] [log_m{(n(m-1)+1)}] [logm(n(m−1)+1)]
二叉树的定义和基本术语
二叉树是n
个结点的有限集合:
- 或者为空二叉树,即
n = 0
- 或者由一个根节点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。
特点:
- 每个结点至多只有两棵子树
- 左右子树不能颠倒(二叉树是有序树)
二叉树的性质:
- n 0 = n 2 + 1 n_0 = n_2+1 n0=n2+1(度为0,1,2的结点个数分别为 n 0 n_0 n0, n 1 n_1 n1, n 2 n_2 n2)
- 二叉树的第
i
层至多有 2 i − 1 2^{i-1} 2i−1个结点 - 高度为
h
的二叉树至多有 2 h − 1 2^h-1 2h−1个结点(满二叉树) n
个结点的完全二叉树的高度h
为 l o g 2 [ n + 1 ] log_2[n+1] log2[n+1]或 [ l o g 2 n ] + 1 [log_2n]+1 [log2n]+1- 对于完全二叉树,可以有结点数
n
推出度为0,1,2的结点个数分别为 n 0 n_0 n0, n 1 n_1 n1, n 2 n_2 n2
完全二叉树最多只有一个度为1的结点,且 n 0 = n 2 + 1 n_0 = n_2+1 n0=n2+1:
- n = 2 k − 1 n=2k-1 n=2k−1,则 n 1 = 0 n_1 = 0 n1=0, n 0 = k n_0=k n0=k, n 2 = k − 1 n_2=k-1 n2=k−1
- n = 2 k n=2k n=2k,则 n 1 = 1 n_1 = 1 n1=1, n 0 = k n_0=k n0=k, n 2 = k − 1 n_2=k-1 n2=k−1
几个特殊的二叉树
- 满二叉树:一棵高度为
h
,且含有 2 h − 1 2^h-1 2h−1个结点的二叉树。其特点为:
- 只有最后一层有叶子结点
- 不存在度为1的结点
- 按层序从1开始编号,结点
i
的左孩子为2i
,右孩子为2i+1
,结点i
的父节点为[i/2]
- 完全二叉树:当且仅当其每个结点都与高度为
h
的满二叉树中编号为 1 ∼ n 1\sim n 1∼n的结点一一对应时,称为完全二叉树。其特点为:
- 只有最后两层可能有叶子结点
- 最多只有一个度为1的结点
- 按层序从1开始编号,结点
i
的左孩子为2i
,右孩子为2i+1
,结点i
的父节点为[i/2]
- i ≤ [ n / 2 ] i \leq [n/2] i≤[n/2]为分支结点, i > [ n / 2 ] i > [n/2] i>[n/2]为叶子结点
- 二叉排序树:一棵二叉树或者是空二叉树,具有如下性质的二叉树:
- 左子树上所有结点的关键字均小于根节点的关键字
- 右子树上所有结点的关键字均大于根节点的关键字
- 左子树和右子树又各是一棵二叉排序树
- 平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1
二叉树的定义
链式存储
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {
}
};
二叉树的前中后序遍历
前序遍历
- 伪代码
typedef struct BiTNode {
ElemType data;
struct BiTNode* lchild, * rchild;
}BiTNode, *BiTree;
//先序遍历
void PreOrder(BiTree T) {
if (T != NULL) {
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
中序遍历
- 伪代码
typedef struct BiTNode {
ElemType data;
struct BiTNode* lchild, * rchild;
}BiTNode, *BiTree;
//中序遍历
void InOrder(BiTree T) {
if (T != NULL) {
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
后序遍历
- 伪代码
typedef struct BiTNode {
ElemType data;
struct BiTNode* lchild, * rchild;
}BiTNode, *BiTree;
//先序遍历
void PostOrder(BiTree T) {
if (T != NULL) {
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
二叉树的层次遍历
- 初始化一个辅助队列
- 根结点入队
- 若队列非空,队头结点出队,访问该结点,并将该结点的左右孩子依次入队
- 重复3直至队列为空
//二叉树结点,链式存储
typedef struct BiTNode {
char data;
struct BiTNode* lchild, * rchild;
}BiTNode, *BiTree;
//链式队列结点
typedef struct LinkNode {
BiTNode* data; //存指针
struct LinkNode* next;
}LinkNode;
typedef struct {
LinkNode* front, rear; // 队头队尾
}LinkQueue;
// 层序遍历
void LevelOrder(BiTree T) {
LinkQueue Q;
InitQueue(Q); // 初始化辅助队列
BiTree p;
EnQueue(Q, T); // 将根节点入队
while (!IsEmpty(Q)) {
// 队列不空则循环
DeQueue(Q, p); // 队头结点出队
visit(p); // 访问出队结点
if (p->lchild != NULL) {
EnQueue(Q, p->lchild); // 左孩子入队
}
if (p->rchild != NULL) {
EnQueue(Q, p->rchild); // 右孩子入队
}
}
}
由遍历序列构造二叉树
若只给出一棵二叉树的前/中/后/层序遍历序列中的一种,不能唯一确定一棵二叉树
由二叉树的遍历序列构造二叉树:
- 前序+中序遍历序列
前序遍历:根节点,左子树的前序遍历序列,右子树的前序遍历序列
中序遍历:左子树的中序遍历序列,根节点,右子树的前序遍历序列 - 后序+中序遍历序列
后序遍历:左子树的前序遍历序列,右子树的前序遍历序列,根节点
中序遍历:左子树的中序遍历序列,根节点,右子树的前序遍历序列 - 层序+中序遍历序列
层序遍历:根节点,左子树的根节点,右子树的根节点
中序遍历:左子树的中序遍历序列,根节点,右子树的前序遍历序列
技巧:先找整棵树的根节点,再根据中序序列划分左右子树,再找左右子树的根节点
树的存储结构
- 双亲表示法(顺序存储):每个节点中保存指向双亲的指针
#define MAX_TREE_SIZE 100
typedef struct {
//树的结点定义
int data; //数据元素
int parent; //双亲位置域
}PTNode;
typedef struct {
PTNode nodes[MAX_TREE_SIZE]; //双亲表示
int n; //结点数
}PTree;
新增数据元素:无需按逻辑上的次序存储
删除数据元素:直接删除指定结点/将尾部元素移动至待删除结点
优点:查指定结点的双亲很方便,但查找指定指定结点的孩子只能从头遍历
- 孩子表示法(顺序+链式存储):顺序存储各个结点,每个结点中保存孩子链表头指针
struct CTNode {
int child; //孩子结点在数组中的位置
struct CTNode* next; //下一个孩子
};
typedef struct {
int data;
struct CTNode* firstChild; //第一个孩子
}CTBox;
typedef struct {
CTBox nodes[MAX_TREE_SIZE];
int n, r; //结点数和根的位置
}CTree;
- 孩子兄弟表示法(链式存储)
typedef struct CSNode {
int data; //数据域
struct CSNode* firstchild, *nextsibling; //第一个孩子和右兄弟指针
}CSNode, *CSTree;
优点:可以用二叉树的操作来处理树。用孩子兄弟表示法存储的树在物理上呈现出二叉树的样子。(左孩子,右兄弟)
森林和二叉树的转化:每棵树先转换为二叉树,再将每棵二叉树根节点串联起来。(本质:用二叉链表存储森林)
树和森林的遍历
树的遍历:先根遍历、后根遍历、层序遍历
树是一种递归定义的数据结构
- 先根遍历:树的先根遍历序列与这棵树相应二叉树的先序序列相同
void PreOrder(TreeNode* R) {
if (R != NULL) {
visit(R); //访问根节点
while (R还有下一个子树T)
PreOrder(T); //先根遍历下一棵子树
}
}
- 后根遍历:树的后根遍历序列与这棵树相应二叉树的中序序列相同
void PostOrder(TreeNode* R) {
if (R != NULL) {
while (R还有下一个子树T)
PostOrder(T); //后根遍历下一棵子树
visit(R); //访问根结点
}
}
- 层次遍历:队列实现
森林的遍历:先序遍历、中序遍历
- 先序遍历:效果等同于依次对各个树进行先根遍历
若森林非空,则按如下规则遍历:访问森林中第一棵树的根节点。先序遍历第一棵树中根节点的子树森林。先序遍历除去第一棵树之后剩余的树构成的森林。
- 中序遍历:效果等同于依次对各个树进行先根遍历
若森林非空,则按如下规则遍历:中序遍历第一棵树中根节点的子树森林。访问第一棵树的根节点。中序遍历除去第一棵树之后剩余的树构成的森林。
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
二叉排序树
左子树结点值 < 根节点值 < 右子树结点值
typedef struct BSTNode{
int key;
struct BSTNode* lchild, *rchild;
}BSTNode, *BSTree;
二叉排序树的查找:若树非空,目标值与根节点的值比较,若相等,则查找成功;若小于根节点,则再左子树上查找,否则在右子树上查找。查找成功,返回结点指针;查找失败则返回NULL。
//在二叉排序树中查找值为key的结点
BSTNode* BST_Search(BSTree T, int key) {
while (T != NULL && key != T->key) {
// 若树空或等于根结点值,则结束循环
if (key < T->key) T = T->lchild; //小于,则在左子树上查找
else T = T->rchild; //大于,则在右子树上查找
}
return T;
}
//在二叉排序树中查找值为key的结点(递归实现)
BSTNode* BSTSearch(BSTree T, int key) {
if (T == NULL) return NULL; //查找失败
if (key == T->key) return T; //查找成功
else if (key < T->key) return BSTSearch(T->lchild, key); //在左子树中找
else return BSTSearch(T->rchild, key); //在右子树中找
}
二叉排序树的插入:若原二叉排序树为空,则直接插入结点;否则,若关键字k
小于根节点值,则插入到左子树,若关键字k
大于根节点值,则插入到右子树。
// 在二叉排序树中插入关键字为k的新节点(递归实现)
int BST_Insert(BSTree& T, int k) {
if (T == NULL) {
T = new BSTNode;
T->key = k;
T->lchild = T->rchild = NULL;
return 1;
}
else if (k == T->key) return 0;
else if (k < T->key) return BST_Insert(T->lchild, k);
else return BST_Insert(T->rchild, k);
}
构造二叉排序树
// 按照 str[]中的关键字序列建立二叉排序树
void Creat_BST(BSTree& T, int str[], int n) {
T = NULL;
int i = 0;
while (i < n) {
BST_Insert(T, str[i]);
i++;
}
}
不同的关键字序列可能得到相同的二叉排序树;也可能得到不同的二叉排序树。
二叉排序树的删除:1.若被删除得结点是叶子结点,则直接删除,不会破坏二叉排序树的性质。2.若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置。3.若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
查找长度——在查找运算中,需要对比关键字的次数称为查找长度,反映了查找操作时间复杂度。
查找成功的平均查找长度ASL(Average Search Length)
最好情况:n
个结点的二叉树最小高度为 [ l o g 2 n ] + 1 [log_2n]+1 [log2n]+1,平均查找长度= O ( l o g 2 n ) O(log_2n) O(log2n)
最坏情况:每个结点只有一个分支,树高h=结点数n。平均查找长度=O(n)
平衡二叉树
AVL树 —— 树上任一结点的左子树和右子树的高度之差不超过1
结点的平衡因子 = 左子树高 - 右子树高
//平衡二叉树结点
typedef struct AVLNode {
int key; //数据域
int balance; //平衡因子
struct AVLNode *lchild, *rchild;
}AVLNode, *AVLTree;
在平衡二叉树中插入新结点后,如何保持平衡? → \to → 调整最小不平衡子树
LL | 在A的左孩子的左子树中插入导致不平衡 |
---|---|
RR | 在A的右孩子的右子树中插入导致不平衡 |
LR | 在A的左孩子的右子树中插入导致不平衡 |
RL | 在A的右孩子的左子树中插入导致不平衡 |
为什么要假定所有子树的高度都是H
?
目标:1.恢复平衡 2.保持二叉排序树特性
LL
平衡旋转(右单旋转):由于在结点A的左孩子(L)的左子树(L)上插入了新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要一次向右的旋转操作。将A的左孩子B向右上旋转代替A成为根节点,将A结点向右下旋转成为B的右子树的根节点,而B的原右子树则作为A结点的左子树。
RR
平衡旋转(左单旋转):由于在结点A的右孩子®的右子树®上插入了新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要一次向左的旋转操作。将A的右孩子B向左上旋转代替A成为根节点,将A结点向左下旋转成为B的右子树的根节点,而B的原右子树则作为A结点的左子树。
代码思路:
至于LR
:先左旋C,再右旋C
LR
平衡旋转(先左后右双旋转):由于在结点A的左孩子(L)的右子树®上插入了新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。先将A结点的左孩子B的右子树的根节点C向左上旋转提升到B结点的位置,然后再C结点向右上旋转提升到A结点的位置。
RL
:先右旋C,后左旋C
若树高h,则最坏情况下,查找一个关键字最多需要对比h次,即查找操作的时间复杂度不可能超过O(h)
假设以 n h n_h nh表示深度为h
的平衡树中含有的最少结点数。
则有 n 0 = 0 n_0 = 0 n0=0, n 1 = 1 n_1 = 1 n1=1, n 2 = 2 n_2 = 2 n2=2,并且有 n h = n h − 1 + n h − 2 + 1 n_h = n_{h-1} + n_{h-2} + 1 nh=nh−1+nh−2+1
含n个结点的平衡二叉树的最大深度为 O ( l o g 2 n ) O(log_2n) O(log2n)