树
一、树的基本概念
1.树的定义
树是n个结点的有限集,当 n = 0 n=0 n=0时,称为空树,树只有一个根结点,根结点没有前驱,除根结点之外的所以结点有且只有一个前驱,树中所有结点可以有一个或多个后继。
2.基本术语
-
祖先:从根结点到某一指定结点路径上的所有结点
-
父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点
-
子结点:一个结点含有的子树的根结点称为该结点的子结点
-
兄弟结点:拥有共同父结点的结点互称为兄弟结点
-
结点的度: 一个结点含有子结点的个数
-
树的度:树中结点的最大度数称为数的度
-
分支结点/叶子结点:度大于0的结点称为分支结点,度等于0的结点称为叶子结点
-
树的高度/深度:树从上至下层数逐层累加称为深度,从下至上逐层累加称为高度
-
有序树/无序树:树的任意结点的子结点是否存在顺序关系。
3.树的性质
-
树中的结点树等于所有结点的度数加1
-
度为 m m m的树中第 i i i层上之多有$m^{i-1} $ 个结点
-
高度为 h h h的 m m m叉树至多有 ( m h − 1 ) / ( m − 1 ) (m^h-1)/(m-1) (mh−1)/(m−1)个结点
-
具有 n n n个结点的 m m m叉树的最小高度为 log m ( n ( m − 1 ) + 1 ) \log_m(n(m-1)+1) logm(n(m−1)+1)
二、二叉树
1.定义
树任意结点至多只能有两颗子树的树称为二叉树,且二叉树的子树有左右之分,次序不能颠倒,二叉树也以递归的形式定义,其任意结点左右子树也是一颗二叉树。
2.性质
- 非空二叉树上的叶子结点数等于度为2的结点数加1,即 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
- 非空二叉树上第 k k k层上之多有 2 k − 1 2^{k-1} 2k−1个结点
- 高度为 h h h的二叉树之多有 2 h − 1 2^h-1 2h−1个结点
3.满二叉树
定义:一颗高度为 h h h,且含有 2 h − 1 2^h-1 2h−1个结点的二叉树称为满二叉树,即每层都含有最多的结点。
对满二叉树按层序编号:约定编号从根结点(根结点编号为1)起,自上而下,从左向右。对与编号为 i i i的结点,双亲为 [ i / 2 ] [i/2] [i/2](向下取整),左孩子为 2 i 2i 2i,右孩子为 2 i + 1 2i+1 2i+1。
4.完全二叉树
定义:高度为 h h h,有 n n n个结点的二叉树,其每个结点都与高度为 h h h的满二叉树中编号为 1 1 1~ n n n的结点一一对应时,称为完全二叉树。
特点:
- 若 i ≤ [ n / 2 ] i\le[n/2] i≤[n/2],则结点 i i i为分支结点,否则为叶子结点
- 只可能有一个度为1的结点,且该结点只有左孩子
- 按层序编号后,一旦某结点(编号为 i i i)为叶子结点或只有左孩子,则编号大于 i i i的结点均为叶子结点。
- 若 n n n为奇数,则每个分支结点都有左孩子和右孩子;若 n n n为偶数,则编号最大的分支结点(编号为 n / 2 n/2 n/2只有左孩子,没有右孩子)
5.二叉树的存储结构
顺序存储
只适合存储满二叉树,存储一般的二叉树空间利用率较低。
//顺序存储
#define MaxSize 100
struct TreeNode{
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
}
链式存储
二叉树主要采用的存储方式
//链式存储
typedef struct BiTNode{
int data;
struct BiTNode *lchild,*rchild;
//struct BiTNode *parent; 可以添加一个父结点指针,建立三叉链表
}BiTNode, //强调链表
*BiTree; //强调结点
6.二叉树的遍历
先序遍历
//1.先序遍历(根左右)
void PreOrder(BiTree T){
if(T != NULL){
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
中序遍历
//2.中序遍历(左根右)
void InOrder(BiTree T){
if(T != NULL){
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
后序遍历
void PostOrder(BiTree T){
if(T != NULL){
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
层序遍历
//建立一个辅助队列
typedef struct LinkNode{
BiTNode *data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front,*rear;
}LinkQueue;
//进行层序遍历
void LevelOrder(BiTree){
LinkQueue Q; //创建辅助队列Q
InitQueue(Q); //初始化辅助队列
BiTree p; //创建根结点
EnQueue(Q,T); //将根结点入队
//队头元素不空则循环
while(isEmpty(Q) != NULL){
//1.队头结点出队
DeQueue(Q,p);
//2.访问出队结点
visit(p);
//3.左孩子入队
if(p->lchild != NULL)
EnQueue(Q,p->lchild);
//4.右孩子入队
if(p->rchild != NULL)
EnQueue(Q,p->rchild);
}
}
7.线索二叉树
初始化
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag; //左右线索标志
}ThreadNode,*ThreadTree;
//创建全局变量pre,指向当前访问的结点的前驱
ThreadNode *pre = NULL;
中序线索化二叉树
//访问函数
void visit(ThreadNode *q){
if(q->lchild == NULL){
q->lchild = pre;
q->ltag = 1;
}
if(pre != NULL && pre->rchild = NULL){
pre->rchild = pre;
pre->rtag = 1;
}
pre = q;
}
//中序遍历函数
void InThread(ThreadTree T){
if(T != NULL){
InThread(T->lchild);
visit(T);
InThread(T->rchild);
}
}
//进行中序线索化
void CreateInThread(ThreadNode T){
//将pre初始为NULL
pre = NULL;
//判断二叉树是否为空,非空则进行线索化
if(T != NULL){
//中序线索化二叉树T
InThread(T);
//处理遍历的最后一个结点
if(pre->rchild == NULL)
pre->rtag = 1;
}
}
//中序线索二叉树中找中序后继
//找中序线索二叉树中中序序列下的第一个结点
ThreadNode* Firstnode(ThreadNode* p){
while(p->ltag==0)
p = p->lchild; //最左下结点(不一定是叶结点)
return p;
}
//找中序线索二叉树中节点p在中序序列下的后继
ThreadNode* Nextnode(ThreadNode* p){
if(p->rtag == 0)
return Firstnode(p->rchild);
return p->rchild; //rtag==1直接返回后继线索
}
//中序线索二叉树中找中序前驱
//找到以p为根的子树中,最后一个被中序遍历的结点
ThreadNode* Lastnode(ThreadNode* p){
//循环找到最右下结点(不一定是叶结点)
while(p->rtag == 0)
p = p->rchild;
return p;
}
//在中序线索二叉树中找到节点p的前驱结点
ThreadNode* Prenode(ThreadNode* p){
//左子树中最右下结点
if(p->ltag == 0)
return Lastnode(p->lchild);
return p->lchild; //ltag==1直接返回前驱
}
先序线索化二叉树
//访问函数
void visit(ThreadNode *q){
if(q->lchild == NULL){
q->lchild = pre;
q->ltag = 1;
}
if(pre != NULL && pre->rchild = NULL){
pre->lchild = pre;
pre->ltag = 1;
}
pre = q;
}
void PreThread(ThreadTree T){
if(T != NULL){
visit(T);
if(T->ltag == 0)
PreThread(T->lchild);
PreThread(T->rchild);
}
}
//进行先序线索化
void CreatePreThread(ThreadNode T){
//将pre初始为NULL
pre = NULL;
//判断二叉树是否为空,非空则进行线索化
if(T != NULL){
//先序线索化二叉树T
PreThread(T);
//处理遍历的最后一个结点
if(pre->rchild == NULL)
pre->rtag = 1;
}
}
//找先序线索二叉树中节点p在先序序列下的后继
ThreadNode* Nextnode(ThreadNode* p){
if(p->rtag == 0){
if(p->lchild!=NULL)
return p->lchild;
return p->rchild;
}
return p->rchild; //rtag==1直接返回后继线索
}
后序线索化二叉树
//访问函数
void visit(ThreadNode *q){
if(q->lchild == NULL){
q->lchild = pre;
q->ltag = 1;
}
if(pre != NULL && pre->rchild = NULL){
pre->rchild = pre;
pre->rtag = 1;
}
pre = q;
}
void PostThread(ThreadTree T){
if(T != NULL){
PostThread(T->lchild);
PostThread(T->rchild);
visit(T);
}
}
//进行后序线索化
void CreatePostThread(ThreadNode T){
//将pre初始为NULL
pre = NULL;
//判断二叉树是否为空,非空则进行线索化
if(T != NULL){
//后序线索化二叉树T
PostThread(T);
//处理遍历的最后一个结点
if(pre->rchild == NULL)
pre->rtag = 1;
}
}
//找后序线索二叉树中节点p在后序序列下的前驱
ThreadNode* Prenode(ThreadNode* p){
if(p->ltag == 0){
if(p->rchild!=NULL)
return p->rchild;
return p->lchild;
}
return p->lchild; //ltag==1直接返回前驱线索
}
三、树和森林
1.树的存储结构
双亲表示法
//双亲表示法 (顺序存储)
//1.树的结点定义
typedef struct{
int data;
int parent; //双亲位置域
}PTNode;
//2.树的类型定义
typedef struct{
int PTNode nodes[Maxsize]; //双亲表示
int n; //结点数
}PTree;
孩子表示法
//孩子表示法(顺序+链式存储)
struct CTNode{
int child; //孩子结点在数组中的位置
struct CTNode *next; //下一个孩子
};
typedef struct{
int data;
struct CTNode *firstChild; //第一个孩子
}CTBox;
typedef struct{
CTBox nodes[MaxSize];
int n,r; //结点数和根的位置
}CTree;
孩子兄弟表示法
//孩子兄弟表示法(链式结构)
typedef struct{
int data;
struct CSNode *firstChild,*nextsibling; //第一个孩子和右兄弟指针
}CSNode,*CSTree;
2.树,森林,二叉树之间的相互转换
-
森林和树相互转换:将森林中各个树的根结点之间视为兄弟关系
-
树和二叉树相互转换:将树中的兄弟结点视为右孩子
-
森林和二叉树相互转换:将森林中的每颗树按照2中的步骤转换为二叉树,再将不同树的根结点视为右孩子
3.树,森林的遍历
先根遍历:若树非空,先访问根结点,再依次对每棵子树进行先根遍历(对应二叉树的先序遍历)
后根遍历:若树非空,先依次对每棵子树进行后根遍历,最后再返回根结点(对应二叉树的中序遍历)
四、二叉排序树(BST)
1.定义
左子树结点值<跟结点值<右子树结点值
typedef struct BSTNode{
int key;
struct BSTNode *lchild, *rchild;
}BSTNode, *BSTree;
2.查找
//在二叉排序树中查找值为key的结点(非递归)
//最坏空间复杂度:O(1)
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的结点(递归)
//最坏空间复杂度:O(h)
BSTNode *BSTSearch(BSTree T, int key){
if(T == NULL)
return NULL;
if(Kry == T->key)
return T;
else if(key < T->key)
return BSTSearch(T->lchild, key);
else
return BSTSearch(T->rchild, key);
}
3.插入
//在二叉排序树中插入关键字为k的新结点(递归)
//最坏空间复杂度:O(h)
int BST_Insert(BSTree &T, int k){
if(T==NULL){ //原树为空,新插入的结点为根结点
T = (BSTree)malloc(sizeof(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);
}
4.建立
//按照str[]中的关键字序列建立二叉排序树
void Crear_BST(BSTree &T, int str[], int n){
T = NULL; //初始时T为空树
int i=0;
while(i<n){
BST_Insert(T,str[i]); //依次将每个关键字插入到二叉排序树中
i++;
}
}
五、平衡二叉树(AVL)
定义:任意结点的左右子树的高度差的绝对值不超过1
//平衡二叉树
typedef struct AVLNode{
int key; //数据域
int balance; //平衡因子
struct AVLNode *lchild; *rchild;
}AVLNode, *AVLTree;
平衡二叉树插入新结点后,如何调整不平衡的方法
- LL: 在A结点的左孩子的左子树中插入导致不平衡
调整: A的左孩子结点右上旋
- RR: 在A结点的右孩子的右子树中插入导致不平衡
调整: A的右孩子结点左上旋
- LR: 在A结点的左孩子的右子树中插入导致不平衡
调整: A的左孩子的右孩子,先左上旋再右上旋
- RL: 在A结点的右孩子的左子树中插入导致不平衡
调整: A的右孩子的左孩子,先右上旋再左上旋
查找效率分析(树高为 h h h)
最好时间复杂度 O ( l o g 2 h ) O(log_2h) O(log2h)
最坏时间复杂度 O ( h ) O(h) O(h)
六、哈夫曼树
- 带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积。
- 哈夫曼树的定义:带权路径最短的树。
- 哈夫曼树的构造。
- 哈夫曼编码:由哈夫曼树得到的二进制前缀编码称为哈夫曼编码。