树是n( n ≥ 0 n \geq 0 n≥0)个节点的有限集。当n=0时,称为空树。每一棵非空树应该满足:
关系描述:
结点数=总度数+1(结点的度:结点有几个孩子)
树的度——各结点的度的最大值 | m叉树——每个结点最多只能有m个孩子的树 |
---|---|
度为m的树 | m叉树 |
任意结点的度 ≤ m \leq m ≤m(最多有m个孩子) | 任意结点的度 ≤ m \leq m ≤m(最多有m个孩子) |
至少有一个结点度=m(有m个孩子) | 允许所有结点的度都 |
一定是非空树 | 可以是空树 |
度为m的树第i层至多有 m i − 1 m^{i-1} mi−1个结点( i ≥ 1 i \geq 1 i≥1),也可以说m叉树的第i层至多有 m i − 1 m^{i-1} mi−1个结点( i ≥ 1 i \geq 1 i≥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 ⌉ \lceil log_m^{n(m-1) + 1} \rceil ⌈logmn(m−1)+1⌉
二叉树(一种有序树)是n( n ≥ 0 n \geq 0 n≥0)个结点的有限集合:
顺序存储比较适合——>满二叉树或者完全二叉树
// 二叉树的顺序存储
#define MaxSize 100
struct TreeNode{
ElemType value;
bool isEmpty;
};
// 二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来,可以用0表示不存在的空结点
TreeNode t[MaxSize];
若是存储二叉树的数组下标是从1开始,则有以下性质:i的左孩子: 2 i 2i 2i;i的右孩子: 2 i + 1 2i+1 2i+1;i的父节点: ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋;i所在的层次: ⌊ log 2 i ⌋ + 1 \lfloor\log_2i\rfloor+1 ⌊log2i⌋+1或者 ⌈ l o g 2 ( i + 1 ) ⌉ \lceil log_2(i+1)\rceil ⌈log2(i+1)⌉;判断i是否使叶子结点/分支结点: s > ⌊ n / 2 ⌋ ? s> \lfloor n/2\rfloor? s>⌊n/2⌋?
n个结点一共有2n个指针域,共有n-1个指针指向其他结点,故有n+1个空链域(用于构造线索二叉树)
// 二叉树的链式存储
// 二叉树的结点
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
遍历:按照某种次序,把所有结点都访问一边
二叉树的递归特性:①要么是个空二叉树;②要么是由“根结点+左子树+右子树”组成的二叉树
先序遍历:根左右;中序遍历:左根右;后序遍历:左右根
前、中、后序遍历算法如下:——》递归版
// 二叉树的先序遍历
void PerOrder(BiTree T){
if(T!=NULL){
visit(T); // 访问根节点
PerOrder(T->lchild); // 递归遍历左子树
PerOrder(T->rchild); // 递归遍历右子树
}
}
// 二叉树的中序遍历
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); // 访问根节点
}
}
遍历的应用——求树的深度
// 应用,求树的深度
int treeDepth(BiTree T){
if(T==NULL)
return 0;
else{
int l=treeDepth(T->lchild);
int r=treeDepth(T->rchild);
return l>r ? l+1 :r+1; // 树的深度=Max{左子树,右子树}+1
}
}
前、中、后序遍历算法如下:——》非递归版
// 先序遍历
void PreOrder(BiTree T){
InitStack(S);// 初始化栈S用来存放结点
BiTree p=T; // 遍历指针p
while(p || !Empty(S)){ // 栈不空或者p不空时候循环
if(p){
visit(p); // 访问栈顶元素
Push(S,p); // 当前结点进栈
p=p->lchild; // 左孩子不空,一直向前走
}
else{ // 出栈转向右子树
Pop(S,p); // 栈顶元素出栈
p=p->rchild; // 转向右子树
}
}
}
// 中序遍历
void InOrder(BiTree T){
InitStack(S);// 初始化栈S用来存放结点
BiTree p=T; // 遍历指针p
while(p || !Empty(S)){ // 栈不空或者p不空时候循环
if(p){
Push(S,p); // 当前结点进栈
p=p->lchild; // 左孩子不空,一直向前走
}
else{ // 出栈转向右子树
Pop(S,p); // 栈顶元素出栈
visit(p); // 访问栈顶元素
p=p->rchild; // 转向右子树
}
}
}
后序遍历较为特殊:
// 后序遍历
void PostOrder(BiTree T){
InitStack(S);// 初始化栈S用来存放结点
BiTree p=T; // 遍历指针p
r=NULL; // 辅助指针r,指向最近访问过的结点
while(p || !Empty(S)){ // 栈不空或者p不空时候循环
if(p){
Push(S,p); // 当前结点进栈
p=p->lchild; // 左孩子不空,一直向前走
}
else{ //向右
GetTop(S,p); // 读取栈顶元素,栈顶元素不出栈
if(p->rchild && p->rchild!=r){ // 右子树存在并且未被访问
p=p->rchild;
}
else{ // 出栈转向右子树
Pop(S,p); // 栈顶元素出栈
visist(p); //访问p结点
r=p; // 记录最近被访问过的结点
// 每次出栈访问完毕一个结点就相当于遍历完以该结点为根的子树,需要将p置为null
p=NULL; // 结点访问完毕后,重置p指针
}
}
}
}
算法基本思想:
①初始化一个辅助队列
②根结点入队
③若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果存在)
④重复③直至队列为空
// 二叉树的链式存储
// 二叉树的结点
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
// 链式队列结点!!!!!!!!!此处存储的为指针
typedef struct LinkNode{
BiTNode *data;
struct LinkNode *next;
}LinkNode;
typedef struct {
LinkNode *front,*rear;
}LinkQueue;
void LevelQrder(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); // 右孩子入队
}
}
}
只给出一个二叉树的前/中/后/层序遍历序列中的一种:不能唯一确定一棵二叉树
如何寻找指定结点p在中序遍历序列中的前驱?如何寻找p的中序后继?思路:从根节点出发,重新进行依次中序遍历,指针q记录当前访问的结点,指针pre记录上一个被访问的结点:①当q=p时,pre为前驱;②当pre=p时,q为后继
缺点:找前驱、后继很不方便;遍历操作必须从根开始
因此提出线索二叉树:利用n个结点的二叉树有n+1个空链域:可以用来记录前驱、后继的信息——<含有这n+1个空链域的结点是:度为1(只有一个孩子)和度为0(叶子结点)>
前驱线索:左孩子指针充当;后继线索:右孩子指针充当
tag=0,表示指针指向孩子;tag=1,表示指针是“线索”
线索二叉树的存储结构:
// 线索二叉树的存储
typedef struct ThreadTNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;// 左、右线索标志
}ThreadNode,*ThreadTree;
中序线索二叉树——线索指向中序前驱、中序后继
先序线索二叉树——线索指向先序前驱、先序后继
后序线索二叉树——线索指向后序前驱、后序后继
// 中序线索化二叉树T
void CreateInThread(ThreadTree T){
ThreadTree pre=NULL; // pre初始为NULL
if(T!=NULL){
InThread(T,pre); // 中序线索化二叉树
pre->rchild=NULL;
pre->ltag=1; // 处理遍历最后一个结点,右子树指向空
}
}
void InThread(ThreadTree &p,ThreadTree &pre){
if(p!=NULL){
InThread(p->lchild,pre); // 递归遍历左子树
// visit; // 访问根节点
if(p->lchild==NULL){ //左子树为空,建立前驱索引
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL && pre->rchild==NULL){
pre->rchild=p; // 建立前驱结点的后继线索
pre->rtag=1;
}
pre=p;
InThread(p->rchild,pre); // 递归遍历右子树
}
}
// 中序线索二叉树的遍历
// 对中序线索二叉树进行中序遍历(利用线索实现非递归算法)
void Inorder(ThreadNode *T){
for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p)){
visit(p);
}
}
// 在中序线索二叉树中给找到结点p的后继结点
ThreadNode *Nextnode(ThreadNode* p){
// 右子树最左下角
if(p->rtag==0)
return Firstnode(p->rchild);
else
return p->rchild;
}
// 找到以p为根的子树中,第一个被中序遍历的结点
ThreadNode *Firstnode(ThreadNode *p){
// 循环找到最左下结点(不一定为叶结点)
while(p->ltag==0){
p=p->lchild;
}
return p;
}
双亲表示李永乐每个结点(根节点除外)只有唯一双亲的性质,可以很快地得到每个结点的双亲结点,但是求结点的孩子时候需要遍历整个结构。
// 双亲表示法
// 存储结构
#define MAX_TREE_SIZE 100 // 树中最多结点数
typedef struct{ // 树中结点定义
ElemType data; // 数据元素
int parent; // 双亲位置域
}PTNode;
typedef struct{ // 树的类型表示
PTNode nodes[MAX_TREE_SIZE]; // 双亲表示
int n; // 结点数
}PTree;
孩子表示法:顺序存储各个结点,每个结点中保存孩子链表头指针
将各个结点的孩子结点都用单链表链接起来形成一个新的线性结构
#define MAX_TREE_SIZE 100 // 树中最多结点数
// 孩子表示法
struct CTNode{
int child; // 孩子结点在数组中的位置
struct CTNode *next; // 下一个孩子
};
typedef struct{
ElemType data;
struct CTNode *firstChild; //第一个孩子
}CTBox;
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n,r; // 结点数和根的位置
}CTree;
孩子兄弟表示法又称为二叉树表示法,也就是说二叉链表作为书的存储结构,孩子兄弟表示法使每个结点包括三部分内容:节点值、指向结点第一个孩子结点的指针以及指向结点下一个兄弟结点的指针。
// 孩子兄弟表示法——树与二叉树的转换
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling; // 第一个孩子和右兄弟指针
}CSNode,*CSTree;
森林:森林是m( m ≥ 0 m\geq0 m≥0)棵互不相交的树的集合
森林中各个树的根节点之间是为兄弟关系
树:
森林:
总结:
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
定义见上文:按住Ctrl键,鼠标点击此处——左子树结点值<根结点值<右子树结点值
查找:
// 二叉排序树
//二叉排序树结点
typedef struct BSTNode{
int key;
struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree;
// 在二叉排序树中查找值为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;
}
插入:
// 二叉排序树的插入
// 二叉排序树不允许由两个相同的数值
// 二叉排序树插入结点一定为叶子结点
// 算法思想:如果原二叉树为空,则直接插入结点;
// 否则,若关键字k小于根节点数值,则插入到左子树,
// 若关键字k大于根结点值,则插入到右子树
int BST_Insert(BSTree &T,int k){
if(T==NULL){
T=(BSTree)malloc(sizeof(BSTNode));
T->key=k;
T->lchild=T->rchild=0;
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 Create_BST(BSTree &T,int str[],int n){
T==NULL;
int i=0;
while(i<n){
BST_Insert(T,str[i]);
i++;
}
}
删除:
平衡二叉树(AVL),简称平衡树——树上任一结点的左子树和右子树的高度之差不超过1
结点的平衡因子=左子树高-右子树高=(0/1/-1)
二叉排序树中插入新节点后如何保持平衡?不平衡时:每次调整对象都是“”最小不平衡子树“
二叉平衡树保证平衡的基本思想为:每当在二叉排序树中插入(删除)一个节点是,首先检查其插入路径上的结点是否因为此次操作导致了不平衡。若导致了不平衡,则先找到插入路径上离插入结点最近的平衡因子的绝对值大于1的结点A,在对以A为根的子树,在保证二叉排序树特性的前提下,调整个结点的位置关系,使之重新达到平衡。
结点的权:有某种显示含义的数值(如:表示结点的重要性等)
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上的权值的乘积
树的带权路径长度(WPL):树中所有叶结点的带权路径长度之和
哈夫曼树(最优二叉树):在含有n个带权叶结点的二叉树中,其中 带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树
构造哈夫曼树:
给定n个权值分别为w1,w2,w3,…,wn的结点,构造哈夫曼树的算法描述如下:
1)将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
2)构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
3)从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
4)重复步骤2)和3),直至F中只剩下一棵树为止。
注意:
1)每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
2)哈夫曼树的结点总数为 2 n − 1 2n-1 2n−1
3)哈夫曼树中不存在度为1的结点。
4)哈夫曼树并不唯一,但WPL必然相同且为最优
哈夫曼树应用:
固定长度编码——每个字符用相等长度的二进制为表示
可变长度编码——允许对不同字符用不等长的二进制位表示