本节为树与二叉树内容,回到总目录:点击此处
本章节目录
相关术语汇总
1.根(root)没有前驱节点,但有零个或多个直接后继
2.子树
树的相关术语
·结点
·结点的度:一个结点的子树的个数
·叶结点:度为0,没有后继结点,也称终端结点
·分支结点:度不为0的结点。也称 非终端结点
·结点的层次:从根结点开始定义,根结点的层次为1…
·结点的层序编号:树中的结点从上层到下层,同层结点从左到右依次排序成一个线性序列,依次给它们编以连续的自然数
·树的度:树中所有结点的度的最大值
·树的高度(深度):树中所有结点层次的最大值
·有序树:在树T中,如果各个子树 T i T_i Ti之间有先后次序,则称为有序树
·森林:m>=0棵互不相交的树的集合,将一个非空树的根结点删去,树就变成了一个森林。相反森林变成树。
·同构:对两棵树,同过对结点适当地重命名,就可以使两棵树完全相等(结点对应相等,对应结点的相关关系也相等)
·孩子结点、双亲结点、兄弟结点、堂兄弟、祖先结点、子孙结点、前辈(层好比结点小的结点)、后辈。
·树的抽象数据类型
ADT Tree
{
数据对象:
结构关系:
基本操作:
·InitTree(Tree) 将Tree初始化为一棵空树
·DestoryTree(Tree) 销毁树Tree
·CreateTree(Tree) 创建树Tree
·TreeEmpty(Tree) 判断树是否为空
·Root(Tree) 返回树的根
·Parent 若x为非根结点,则返回它的双亲,否则返回空
·FirstChild (Tree,x) 若x为非叶子结点,则返回它的第一个孩子结点,否则返回空
·NextSibling(Tree,x) 返回x的后面的下一个兄弟结点,前提:x不是其双亲的最后一个孩子结点
·InsertChild(Tree,p,child) 将child插入到Tree中,做p所指向结点的子树
·DeleteChild(Tree,p,i ) 删除Tree中p所指向结点的第i棵子树
·TraverseTree(Tree,Visit()) 按照某种次序对树Tree中每个结点调用Visit()函数访问依次且最多一次。若Visit()失败,则操作失败。
}
二叉树
二叉树 Binary Tree (BT):
1.每个结点的度都不大于2
2.每个结点的孩子结点次序不能任意颠倒
二叉树的性质
性质1: 在二叉树的第i层上至多有 2 i − 1 2^{i-1} 2i−1 个结点 (i>=1)
性质2:深度为k的二叉树至多有 2 k − 1 2^k-1 2k−1个结点(k>=1)
性质3:对任意一棵二叉树T,若终端结点数位 n 0 n_0 n0,而其度数为2的结点数为 n 2 n_2 n2,则 n 0 n_0 n0 = = = n 2 n_2 n2+1
性质4:具有n个结点的完全二叉树的深度为 [ l o g 2 n ] + 1 [log_2^n]+1 [log2n]+1
性质5:
二叉树的存储结构
· 顺序存储结构:
LChild[i] = 2i RChild[i] = 2i+1
这样会导致一定的内存浪费·链式存储结构:
typedef struct Node
{
DataType data;
struct Node * LChild;
struct Node * RChild;
}BiTNode,*BiTree;
若一个二叉树含有n个结点,则它的二叉链表中必含有2n个指针域,其中必有n+1个空的链域
★二叉树的创建
//先序创建
void CreateTree(BitTree *bt)
{
char ch;
ch = getchar();
if(ch=='#')
{
*bt = NULL;
}
else
{
*bt = (BitTree)malloc(sizeof(BitNode));
(*bt) -> data = ch;
CreateTree(&((*bt) -> Lchild));
CreateTree(&((*bt) -> Rchild));
}
}
这里的创建主要需要理解指针的传递
☆二叉树的遍历
【实质】
先序遍历:每遇到一个节点,先访问,然后再遍历其左右子树(对应图 4 中的 ①);
中序遍历:第一次经过时不访问,等遍历完左子树之后再访问,然后遍历右子树(对应图 4 中的 ②);
后序遍历:第一次和第二次经过时都不访问,等遍历完该节点的左右子树之后,最后访问该节点(对应图 4 中的 ③);
先序遍历 DLR 根左右
1.访问根结点
2.访问左子树
3.访问右子树
//递归方法
void PreOrder(BiTree root)
{
if(root != NULL)
{
Visit(root->data);
PreOrder(root->LChild);
PreOrder(root->RChild);
}
}
而递归的底层实现依靠的是栈存储结构,因此,二叉树的先序遍历既可以直接采用递归思想实现,也可以使用栈的存储结构模拟递归的 思想实现
//非递归先序遍历【显式栈】
#include <stdio.h>
#include <cstring>
#define TElemtype int
int top = -1;
typedef struct BiTNode
{
TElemType data;
struct BiTNode *Lchild,*Rchild;
}BiTNode,*BitTree;
//初始化树函数
//前序遍历使用的进栈函数
void push(BitNode** a,BiTNode *elem)
{
a[++top] = elem;
}
void pop()
{
if(top==-1)
{
return;
}
top --;
}
//输出结点本身的值
void display(BiTNode * elem)
{
cout << elem -> data;
}
//获取栈顶元素
BiTNode* getTop(BitNode **a)
{
return a[top];
}
--------------------------------------
先序非递归算法
--------------------------------------
void PreOrderTraverse(BiTree Tree)
{
BiTNode* a[20]; //顺序栈
BiTNode *p;
push(a,Tree);//根结点入栈
while(top!=-1) //如果栈不为空
{
p = getTop(a); //取栈顶元素
pop();//出栈
while(p)
{
display(p);
//如果该结点有右孩子,则右孩子进栈
if(p -> Rchild)
{
push(a,p->rchild);
}
p = p -> Lchild; //一直指向根结点最后一个左孩子
}
}
}
int main()
{
BiTree Tree;
CreateBiTree(&Tree);
cout<<"先序遍历";
PreOrderTraverse(Tree);
}
中序遍历 LDR
1.按中序遍历左子树
2.访问根结点
3.按中序遍历右子树
//递归方法
void InOrder(BitTree root)
{
if(root)
{
InOrder(root -> Lchild);
Visit(root -> data);
InOrder(root -> Rchild);
}
}
而递归的底层实现依靠的是栈存储结构,因此,二叉树的先序遍历既可以直接采用递归思想实现,也可以使用栈的存储结构模拟递归的 思想实现。
中序遍历的非递归方式实现思想是:从根结点开始,遍历左孩子同时压栈,当遍历结束,说明当前遍历的结点没有左孩子,从栈中取出 来调用操作函数,然后访问该结点的右孩子,继续以上重复性的操作。
除此之外,还有另一种实现思想:中序遍历过程中,只需将每个结点的左子树压栈即可,右子树不需要压栈。当结点的左子树遍历完成 后,只需要以栈顶结点的右孩子为根结点,继续循环遍历即可。
//非递归方法
void InOrder(BitTree Tree)
{
BitNode *a[100];
BiTNode *p;
push(a,Tree); //根结点入栈
while(top!=-1)//说明栈内不为空,程序继续运行
{
while((p=gettop(a)) && p) //取栈顶元素且不能为NULL
{
push(a,p->Lchild); //将结点的左孩子进栈,如果没有左孩子,NULL进栈
}
pop(); //跳出循环后,栈顶元素一定为NULL,将NULL弹出栈
if(top != -1)
{
p = gettop(a); //栈顶元素
pop();//栈顶元素弹出去
display(p);
push(a,p->Rchild);//将p指向的结点的右孩子进栈
}
}
}
//第二种中序遍历非递归实现方法
void InOrder(BitTree bt)
{
BitNode* a[100];
BitNode *p;
p = bt;
//当p为NULL或者栈为空的时候,遍历完毕
while(p||top!=-1)
{
if(p)
{
push(a,p);
p = p -> Lchild;
}
else
{
p = gettop(a);
pop();
display(p);
p = p -> Rchild;
}
}
}
后续遍历 LRD
1.后序遍历左子树
2.后序遍历右子树
3.访问根结点
//递归方法
void PostOrder(BitTree root)
{
if(root)
{
PostOrder(root -> Lchild);
PostOrder(root -> Rchild);
Visit(root -> data);
}
}
递归算法底层的实现使用的是栈存储结构,所以可以直接使用栈写出相应的非递归算法。
后序遍历是在遍历完当前结点的左右孩子之后,才调用操作函数,所以需要在操作结点进栈时,为每个结点配备一个标志位。当遍历该 结点的左孩子时,设置当前结点的标志位为 0,进栈;当要遍历该结点的右孩子时,设置当前结点的标志位为 1,进栈。 这样,当遍历完成,该结点弹栈时,查看该结点的标志位的值:如果是 0,表示该结点的右孩子还没有遍历;反之如果是 1,说明该结 点的左右孩子都遍历完成,可以调用操作函数。
//后续遍历非递归方法
typedef struct SNode
{
BiTree p;
int tag;
}SNode;
void postpush(SNode *a,SNode sdata)
{
a[++top]=sdata;
}
void PostOrder(BiTree bt)
{
SNode a[20];
BitNode *p;
int tag;
SNode sdata;
p = bt;
while(p||top!=-1)
{
while(p)
{
//为结点入栈做准备
sdata.p = p;
sdata.tag = 0;//由于遍历左孩子,设置标志位为0
postpush(a,sdata); //压栈
p = p -> Lchild;//以该结点为根结点,遍历左孩子
}
sdata = a[top]; //取栈顶元素
pop();//栈顶元素出栈
p = sdata.p;
tag = sdata.tag;
if(tag == 0) //如果tag ==0,说明该节点还没有遍历它的右孩子
{
sdata.p = p;
sdata.tag = 1;
postpush(a,sdata); //更改标志位为后重新压栈
p = p -> Rchild; //以该节点的右孩子为根结点,重新循环
}
else //如果取出来的栈顶元素tag == 1 则说明左右子树都遍历完成了,可以调用显示函数了
{
display(p);
p = NULL;
}
}
}
▲遍历算法应用
· 遍历二叉树中的结点
----三序遍历
----层次遍历
—层次遍历是指从二叉树的第一层(根结点)开始,从上至下逐层遍历,在同一层中,则按照从左到右的顺序对结点逐一访问。直到二叉树中的所有节点都被访问且只访问一次
【队列实现】设置一个队列,暂时存某层已访问过的结点,同时也保存了该层结点访问的先后次序。按照对该层结点访问的先后次序实现对其下层孩子结点的按次序访问。
1,初始化空队列Q
2,若二叉树bt为空树,则直接返回
3,将二叉树的根结点指针bt放入队列Q
4,若队列非空,则重复下面操作:
a,对头元素出队并访问该元素
b,若该结点的左孩子非空,则将该节点的左孩子结点指针入队
c,若该节点的右孩子非空,则将该节点的右孩子结点指针入队
int LayerOrder(BiTree bt)
{
SeqQueue Q;
BiTree p = NULL;
p = bt;
InitQueue(&Q);
if(bt == NULL) return ERROR; //若为空则结束遍历
EnterQueue(&Q,bt); //非空,则根节点bt入队,开始层次遍历
while(!IsEmpty(Q))
{
DeleteQueue(&Q,&p);//队头元素出队并访问
visit(p->data);
if(p -> Lchild) EnterQueue(&Q,p->Lchild);
if(p -> Rchild) EnterQueue(&Q,p->Rchild);
}
return 1;
}
· 输出二叉树中的叶子结点
void PreOrder(BiTree root)
{
if(root)
{
if(root->Lchild == NULL && root->Rchild == NULL)
{
printf(root -> data);
}
PreOrder(root->Lchild);
PreOrder(root->Rchild);
}
}
·统计叶子结点的数目
int LeafCount = 0;
//方法一:后序遍历统计叶子结点数目
void leaf(BiTree root)
{
if(root)
{
leaf(root->Lchild);
leaf(root->Rchild);
if(root->Lchild == NULL && root->Rchild == NULL)
{
LeafCount ++;
}
}
}
//方法二:分治算法,如果是空树则返回0;如果只有一个结点则返回1,否则为左右子树的叶子结点数之和
int leaf(BiTree root)
{
int count;
if(root == NULL) count = 0;
else if((root->Lchild == NULL)&&(root->Rchild == NULL)) count = 1;
else
{
count = leaf(root->Lchild)+leaf(root->Rchild);
}
return count;
}
//分治递归,把大问题转化成一个一个的小问题然后综合处理进行解决!
·建立二叉链表方式存储的二叉树
void CreateBiTree(BiTree *bt)
{
char ch;
ch = getchar();
if(ch == '.') *bt = NULL;
else
{
*bt = (BiTree)malloc(sizeof(BiTNode));
(*bt) -> data = ch;
CreateBiTree(&((*bt) -> Lchild));
CreateBiTree(&((*bt) -> Rchild));
}
}
·求二叉树的高度
二叉树bt高度的递归定义如下:
1.若bt为空,则高度为0;
2.若bt非空,其高度应为其左、右子树高度的最大值加1
//采用后续遍历二叉树的高度的递归算法
int PostTreeDepth(BiTree bt)
{
int hl,hr,max;
if(bt != NULL)
{
hl = PostTreeDepth(bt->Lchild);
hr = PostTreeDepth(bt->Rchild);
max = max(hl,hr);
return (max+1);
}
else return 0;
}
[也可以用先序遍历的方法]
void PreTreeDepth(BiTree br,int h)
{
//先序遍历求二叉树bt高度的递归算法,h为bt指向结点所在层次,初值为1
//depth为当前求得的最大层次,为全局变量,调用前初值为0
if(bt != NULL)
{
if(h>depth) depth = h;//如果该结点层次值大于depth,更新depth的值
PreTreeDepth(bt->Lchild,h+1);//遍历左子树
PreTreeDepth(bt->Rchild,h+1);//遍历右子树
}
}
·按树状打印二叉树
[算法思想]:
1、二叉树的横向显示应是竖向显示的90°旋转。易知:打印格式应该是先打印右子树再打印根,最后打印左子树—》恰为中序遍历
2、在这种输出格式中,结点的左右位置与结点的层深有关,所以算法中设置了一个表示当前深度的参数,以控制输出结点的左右位置,每当递归进层时层深参数加1.
void PrintTree(BiTree bt, int nLayer)
{
if(bt == NULL) return;
PrintTree(bt -> RChild,nLayer+1);
for(int i = 0;i < nLayer; i ++)
{
print(" ");
}
printf("%c\n",bt->data);
PrintTree(bt -> LChild, nLayer+1);
}
·已知一个二叉树的中序遍历序列和后序遍历序列,求这棵树的前序遍历序列
通过
中序遍历和后序遍历
以及通过前序遍历和中序遍历
可唯一确定一颗二叉树,注意一定要有中序遍历
,由于前序遍历第一个结点
为树的根结点,后序遍历最后一个结点
为树的根结点,所以通过中序遍历可由树的根结点将左子树和右子树分开
,以便于下面的计算【主要通过每次确定各个子树中根结点的位置以及左,右子树中结点的数目通过递归来求解(也可看作
分治求解
)】
方法一:
//根据中序遍历和后序遍历构建二叉树
BitNode* CreateBiTree(char *instr, char *afterstr, int length)
{
if (length == 0)//都构建完毕
{
return NULL;
}
char c = afterstr[length - 1];//树的根为后序遍历最后一个字符
int i = 0;
while ((instr[i] != c) && i < length)//找到中序遍历中该根的位置
{
i = i + 1;
}
int leftlength = i;//确定左子树中结点的数目
int rightlength = length - i - 1;//确定右子树中结点的数目
BitNode *T;
T = (BitNode*)malloc(sizeof(BitNode));
T->data = c;//建立新结点,每次都使其等于每棵小子树的根结点
T->lchild = NULL;
T->rchild = NULL;
T->lchild = CreateBiTree(&instr[0], &afterstr[0], leftlength);//创建左子树
T->rchild = CreateBiTree(&instr[i + 1], &afterstr[i], rightlength);//创建右子树
return T;
}
BiTree Create(int LPost, int RPost,int Lin, int Rin)
{
if (LPost > RPost) return NULL; //if (length == 0) return NULL; //构建完毕
BiNode *root = (BiNode*)malloc(sizeof(BiNode));
root -> data = post[RPost]; //存储根结点
int k;
for (k = Lin;k < int(in.length()); k ++) //在中序遍历中找到根结点
{
if (in[k]==post[RPost]) break;
}
int num = k - Lin; //num表示中序遍历中根结点左侧的数目,左子树
//算法思想【分治】!
root -> Lchild = Create(LPost,LPost+num-1,Lin,k-1);
root -> Rchild = Create(LPost+num,RPost-1,k+1,Rin);
return root;
}
·已知二叉树的前序和中序,创建二叉树
//依据前序和中序确定一棵二叉树
//递归方法
BiTree CreateTreePre(int Prebegin, int Preend, int Inbegin, int Inend, string prestr, string instr)
{
if (Prebegin > Preend) return NULL;
BiNode *Node = new BiNode;
Node -> data = prestr[Prebegin];
int k = 0;
for(int i = Inbegin; i <= Inend; i++)
{
if(prestr[Prebegin] == instr[i])
{
k = i;
break;
}
}
Node -> Lchild = CreateTreePre(Prebegin + 1,Prebegin+k-Inbegin,Inbegin,k-1,prestr,instr);
Node -> Rchild = CreateTreePre(Prebegin+k-Inbegin+1,Preend,k+1,Inend,prestr,instr);
return Node;
}
·递归交换左右子树
//用于交换左右子树
void SwapIntial(BiNode *&R,BiNode *&L)
{
BiNode *t = R;
R = L;
L = t;
}
//递归【分治】完成左右子树的交换
void Exchange(BiTree bt)
{
if (bt)
{
Exchange(bt -> Rchild);
Exchange(bt -> Lchild);
SwapIntial(bt -> Rchild,bt -> Lchild);
}
}
线索二叉树
算法中多次涉及到对二叉树的遍历,普通的二叉树需要使用栈结构做重复性的操作
线索二叉树不必如此,在遍历的同时,使用二叉树中空闲的内存空间记录某些结点的前驱和后继元素的位置(不是全部)。这样在算法后期需要遍历二叉树时,就可以利用保存的结点信息,提高遍历的效率,使用这种方法构建的二叉树,成为 ”线索二叉树“
利用规律:在有n个结点的二叉链表中必定存在n+1个空指针域
线索二叉树实际上就是利用这些空指针域来存储结点之间前驱和后继关系的一种特殊的二叉树
Ltag = 0,Lchild域指示结点的左孩子
= 1,Lchild域指示结点的遍历前驱
#define TElemType int
//枚举 Link 为 0 ; Thread 为1
typedef enum PointerTag
{
Link,
Thread
}PointerTag;
//结点结构构造
typedef struct BiThrNode
{
TElemType data; //数据域
struct BiThrNode * lchild, *rchild;
PointerTag Ltag,Rtag;
}BiThrNode,*BiThrTree;
二叉树经过某种遍历方式转化为线索二叉树的过程成为线索化 其过程其实就是在二叉树中修改空指针的过程
二叉树的线索化
·中序对二叉树进行线索化
中序线索化,采用中序遍历的框架
加线索的操作就是访问结点的操作
加线索操作需要利用刚访问过结点与当前结点的关系,因此设置一个指针pre,始终记录刚刚访问的结点
1 如果当前遍历结点root的左子域为空,则让左子域指向pre
2 如果前驱pre的右子域为空,则让右子域指向当前遍历结点root.
3 为下次做准备,当前访问结点root作为下一个访问结点的前驱pre
void InThread(BiTree root)
{
//对root所指的二叉树进行中序线索化,其中pre始终指向刚访问过的结点,其初值为NULL
if(root)
{
InThread(root->Lchild); //线索化左子树
if(root->Lchild == NULL)
{
root -> Ltag = 1;
root -> Lchild = pre;
}
if (pre != NULL && pre -> Rchild == NULL)
{
pre -> Rchild = root;
pre -> Rtag = 1;
}
pre = root;
InThread(root->Rchild);//
}
}
线索二叉树的遍历
在线索二叉树中寻找前驱、后继结点
//在中序线索二叉树中寻找结点前驱
BitNode * InPre(BitNode *p)
{
//在中序线索二叉树中寻找p的中序前驱,并用pre指针返回结果
if(p -> Ltag == 1) pre = p -> Lchild;
else
{
//在p的左子树中寻找最右下端的点
for(q = p -> Lchild; q -> Rtag == 0; q = q -> Rchild);
pre = q;
}
return pre;
}
//在中序线索二叉树中寻找结点后继
BitNode *InNext(BitNode *p)
{
//在中序线索二叉树中查找p的中序后继结点,并用Next指针返回结果
if(p -> Rtag == 1) Next = p -> Rchild;
else
{
//在p的右子树中查找”最左下端“的点
for(q = p -> Rchild;q -> Ltag == 0; q = q -> Lchild);
Next = q;
}
return Next;
}
—> 在先序遍历中 寻找结点的后继比较容易
—> 在后序遍历中 寻找结点的前驱比较容易
· 遍历中序线索二叉树
//在中序线索树上求中序遍历的第一个结点
BitNode *InFirst (BiTree bt)
{
BiTNode * p = bt;
if(!p) return NULL;
while(p -> LTag ==0) p = p -> Lchild;
return p;
}
//遍历中序线索树
void TInOrder(BiTree bt)
{
BitNode *p;
p = InFirst(bt);
while(p)
{
visit(p);
p = InNext(p);
}
}
void InOrderThraverse_Thr(BiThrTree p)
{
while (p)
{
//一直找左孩子,最后一个为中序序列中排第一的
while(p -> Ltag == Link)
{
p = p -> Lchild;
}
printf("%c",p->data);
//当右结点标志位为1时,直接找其后继结点
while (p -> Rtag == Thread && p -> Rchild != NULL)
{
p = p -> Rchild;
printf("%c",p->data);
}
//否则,按照中序遍历的规律,找其右子树中最左下的结点,也就是继续循环遍历
p = p -> Rchild;
}
}
树、森林和二叉树的关系
树的存储结构
1.双亲表示法
2.孩子表示法
3.孩子兄弟表示法
-----如何存储普通的树
·双亲表示法
-------->>适用于快速寻找双亲结点的方法
#define MAX 100
typedef struct TNode
{
DataType data;
int parent;
}TNode;
typedef struct
{
TNode tree[MAX];
int nodenum; //表示结点数
}ParentTree;
————双亲表示法完整代码
//人工初始化、通俗易懂
#include<stdio.h>
#include<stdlib.h>
#define MAX_SIZE 20
typedef char ElemType;//宏定义树结构中数据类型
typedef struct Snode //结点结构
{
ElemType data;
int parent;
}PNode;
typedef struct //树结构
{
PNode tnode[MAX_SIZE];
int n; //结点个数
}PTree;
PTree InitPNode(PTree tree) //初始化
{
int i, j;
char ch;
printf("请输出节点个数:\n");
scanf("%d", &(tree.n));
printf("请输入结点的值其双亲位于数组中的位置下标:\n");
for (i = 0; i < tree.n; i++)
{
getchar();
scanf("%c %d", &ch, &j);
tree.tnode[i].data = ch;
tree.tnode[i].parent = j;
}
return tree;
}
void FindParent(PTree tree)
{
char a;
int isfind = 0;
printf("请输入要查询的结点值:\n");
getchar();
scanf("%c", &a);
for (int i = 0; i < tree.n; i++) {
if (tree.tnode[i].data == a)
{
isfind = 1;
int ad = tree.tnode[i].parent;
printf("%c 的父节点为 %c,存储位置下标为 %d", a, tree.tnode[ad].data, ad);
break;
}
}
if (isfind == 0)
{
printf("树中无此节点");
}
}
int main()
{
PTree tree;
for (int i = 0; i < MAX_SIZE; i++)
{
tree.tnode[i].data = " ";
tree.tnode[i].parent = 0;
}
tree = InitPNode(tree);
FindParent(tree);
return 0;
}
·孩子表示法
把每个结点的孩子结点排列起来,构成一个单链表,成为孩子链表。采用顺序表+链表的存储结构
–>> n个结点共有n个孩子链表(叶子结点的孩子链表为空表),而n个结点的数据和n个孩子链表的头指针又组成一个顺序表
------>>适用于快速寻找孩子结点的方法
typedef struct ChildNode //孩子链表结点定义
{
int Child; //该孩子结点在线性表中的位置
struct ChildNode *next; //指向 下一个孩子结点的指针
}ChildNode;
typedef struct //顺序表结点的结构定义
{
DataType data; //结点的信息
ChildNode *FirstChild; //指向孩子链表的头指针
}DataNode;
typedef struct //树的定义
{
DataNode nodes[MAX]; //顺序表
int root; //该树根结点在线性表中的位置
int num; //该树的结点个数
}ChildTree;
————>完整演示代码
//树的孩子表示法
#include<stdio.h>
#include<stdlib.h>
#define MAX_SIZE 20
#define TElemType char
//孩子表示法
typedef struct CTNode
{
int child;//链表中每个结点存储的不是数据本身,而是数据在数组中存储的位置下标
struct CTNode * next;
}ChildPtr;
typedef struct
{
TElemType data;//结点的数据类型
ChildPtr* firstchild;//孩子链表的头指针
}CTBox;
typedef struct
{
CTBox nodes[MAX_SIZE];//存储结点的数组
int n, r;//结点数量和树根的位置
}CTree;
//孩子表示法存储普通树
CTree initTree(CTree tree)
{
printf("输入节点数量:\n");
scanf("%d", &(tree.n));
for (int i = 0; i < tree.n; i++)
{
printf("输入第 %d 个节点的值:\n", i + 1);
getchar();
scanf("%c", &(tree.nodes[i].data));
tree.nodes[i].firstchild = (ChildPtr*)malloc(sizeof(ChildPtr));
tree.nodes[i].firstchild->next = NULL;
printf("输入节点 %c 的孩子节点数量:\n", tree.nodes[i].data);
int Num;
scanf("%d", &Num);
if (Num != 0)
{
ChildPtr * p = tree.nodes[i].firstchild;
for (int j = 0; j < Num; j++)
{
ChildPtr * newEle = (ChildPtr*)malloc(sizeof(ChildPtr));
newEle->next = NULL;
printf("输入第 %d 个孩子节点在顺序表中的位置", j + 1);
scanf("%d", &(newEle->child));
p->next = newEle;
p = p->next;
}
}
}
return tree;
}
void findKids(CTree tree, char a)
{
int hasKids = 0;
for (int i = 0; i < tree.n; i++)
{
if (tree.nodes[i].data == a)
{
ChildPtr * p = tree.nodes[i].firstchild->next;
while (p)
{
hasKids = 1;
printf("%c ", tree.nodes[p->child].data);
p = p->next;
}
break;
}
}
if (hasKids == 0) {
printf("此节点为叶子节点");
}
}
int main()
{
CTree tree;
for (int i = 0; i < MAX_SIZE; i++)
{
tree.nodes[i].firstchild = NULL;
}
tree = initTree(tree);
//默认数根节点位于数组 notes[0]处
tree.r = 0;
printf("找出节点 F 的所有孩子节点:");
findKids(tree, 'F');
return 0;
}
将双亲表示法和孩子表示法可合二为一 既能快速找到双亲又能快速找到孩子结
·孩子兄弟表示法
树的二叉表示法,即以二叉链表作为树的存储结构。
typedef struct CSNode
{
DataType data; //结点信息
struct CSNode *FirstChild; //第一个孩子
struct CSNode *NextSibling; //下一个兄弟
}CSNode,*CSTree;
其与二叉树的关系:
树、森林与二叉树的相互转换
· 树转换为二叉树
转换方法:
1.树中所有相邻兄弟之间加一条连线
2.对树中的每个结点,只保留其与第一个孩子结点之间的连线,删去其与其他孩子结点之间的连线
3.以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。
结论:树中每个结点都可以对应到二叉树中的结点。
树中某结点的第一个孩子在二叉树中是相应结点的左孩子,树种某结点的右兄弟结点在二叉树中是相应结点的右孩子。》》》二叉树中,左分支上的各结点在原来的树中是父子关系,而右分支上的各结点在原来的树中是兄弟关系 ,由于根结点没有兄弟结点,所以变换后的二叉树的根的右子树一定为空NULL
· 森林转化为二叉树
方法:
- 将森林中的每棵树转换成相应的二叉树
- 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树根结点的右孩子,当所有二叉树连在一起后,所得到的二叉树就是由森林转换得到的二叉树
首先将森林中所有的普通树各自转化为二叉树;
将森林中第一棵树的树根作为整个森林的树根,其他树的根节点看作是第一棵树根节点的兄弟节点,采用孩子兄弟表示法将所有树进行 连接;
森林转化为二叉树**,更多的是为了对森林中的节点做遍历操作**。前面讲过,遍历二叉树有 4 种方法,分别是层次遍历、先序遍历、中序 遍历和后序遍历。转化前的森林与转化后的二叉树相比,其层次遍历和后序遍历的访问节点顺序不同,而前序遍历和中序遍历访问节点 的顺序是相同的。
以图 1 中的森林为例,其转化后的二叉树为图 2c),两者比较,其先序遍历访问节点的顺序都是 A B C D E F G H I J;同样,中序遍历 访问节点的顺序也相同,都是 B C D A F E H J I G。而后序遍历和层次遍历访问节点的顺序是不同的。
提示,由二叉树转化为森林的过程也就是森林转化二叉树的逆过程,也就是图 2 中由 c) 到 b) 再到 a) 的过程。
——>将森林还原成二叉树:
1 . 若某结点是其双亲的左孩子,则把该结点的右孩子、右孩子的右孩子……都与该结点的双亲结点用线连起来。
2 . 删掉原来二叉树中所有双亲结点与右孩子结点的连线
3 . 整理由1、2两步所得到的树和森林,使其结构分明
· 树与森林的遍历
树的遍历:
---- 先根遍历:
1、(若树非空)访问根结点;
2、 从左到右,依次先根遍历根结点的每一棵子树
----》转化为二叉树的前序遍历---- 后根遍历:
1、(若树非空)从左到右,依次后根遍历根节点的每一棵子树;
2、 访问根结点
----》 转化为二叉树的中序遍历
树的遍历算法实现
eg:使用孩子兄弟链表实现树的先根遍历
//先根遍历:法一
void RootFirst(CSTree root)
{
if(root)
{
Visit(root->data);
p = root -> FirstChild;
while(p!=NULL)
{
RootFirst(p); //访问以p为根的子树
p = p -> Nexsibling;
}
}
}
//法二:
void RootFirst(CSTree root)
{
if(root)
{
Visit(root->data);
RootFirst(root -> FirstChild); //遍历首子树
RootFirst(root -> Nextsibling); //遍历兄弟树
}
}
森林的遍历
由二叉树与森林之间的转换关系可知:
森林的遍历方式与二叉树的遍历方式一致
eg: 先序遍历:
1.访问森林中第一棵树的根结点;
2.先序遍历第一棵树的根节点的子树森林;
3.先序遍历除去第一棵树之后剩余的树构成的森林
★哈夫曼树
·哈夫曼树的基本概念
1.路径和路径长度
路径:是指从根结点到该结点到分支序列
路径长度:是指根结点到该结点所经过的分支数目
2.结点的权和带权路径长度
给树的每个结点赋予一个具有某种实际意义的实数,则称该实数为这个结点的权。
在树结构中,把从树根到某一结点到路径长度与该结点的权的乘积,称为该结点的带权路径长度
3.树的带权路径长度
Question1:什么样的二叉树的路径长度PL最小
路径长度为k的结点至多有2^k2*k*个
由此可知,完全二叉树具有最小路径长度的性质,但不具有唯一性。有些树不是完全二叉树,也可以具有最小路径长度
Question2: 什么样的二叉树带权路径长度最小?
WPL最小不一定是完全二叉树,依据二叉树构造出的哈夫曼树可以到达最小带权路径。
★构造哈夫曼树
——>最优二叉树
直观来看,先选择权小的,所以权小的结点被放置在树的较深层,而权较大的离根较近,故在哈夫曼树中权越大叶子离根越近,这样一来,在计算树的带权路径长度时,自然会具有最小带权路径长度,这种生成算法就是一种典型的贪心法。
·哈夫曼树的类型定义
–存储结构
#define N 20
#define M 2 * N - 1 //因为哈夫曼树没有度为1的点,因此一棵有n个叶子结点的哈夫曼树共有2n-1个结点,其中非叶子结点有 n - 1个
typedef struct
{
int weight; //权重
int parent; //双亲下标
int Lchild;
int Rchild;
}HTNode,HuffmanTree[M+1]; //哈夫曼树是一个结构数组类型,0号位置不用
★哈夫曼算法的实现
–创建哈夫曼树的算法
void CrtHuffmanTree(HuffmanTree ht, int w[], int n)
{//构造哈夫曼树ht[M+1],w[]存放n个权值
for(i = 1; i <= n; i ++)
{
ht[i] = {w[i],0,0,0}; //1~n号单元存放叶子结点,初始化
}
m = 2 * n - 1;
for(i = n + 1; i <= m; i ++)
{
ht[i] = {0,0,0,0}; //n+1~m号单元存放非叶子结点,初始化
}
/* 初始化完毕*/
for(i = n+1; i <= m; i ++)
{
select(ht,i-1,&s1,&s2);
//在ht[1]~ht[i-1]的范围内选择两个parent为0且weight最小的结点,其序号分别赋值给s1,s2
ht[i].weight = ht[s1].weight + ht[s2].weight;
ht[s1].parent = i;
ht[s2].parent = i;
ht[i].Lchild = s1;
ht[i].Rchild = s2;
}
}
/*
该算法分为两大部分,
其中第一部分是初始化,
>1 先初始化ht的前1~n号元素,存放叶子结点(相当初始森林),它们都没有双亲和孩子。
>2 如然后初始化后n-1个(序号从n+1 ~ 2n-1)非叶结点元素
第二部分为实施选择、删除合并n-1次
> 选择 :是指从当前森林中(在森林中树的根结点的双亲为0)选择两棵根的权值最小的树
> 删除合并 :是将选到的两棵树的根权和存入ht的当前最前面的空闲元素中(相当于合并树中新结点),并置入相应的双亲与孩子位置指示
*/
【例题】
哈夫曼编码
为了缩短数据文件长度,可采用不定长编码。基本思想:给使用频度较高的字符编以较短的编码。这是数据压缩技术的最基本思想。如何给数据文件中的字符编以不定长编码,使各种数据文件平均长度最短呢?这也是个哈夫曼树相关的最优问题
·前缀编码
如果在一个编码系统中,任一编码都不是其他任何编码的前缀(最左子串),则称该编码系统中的编码时前缀编码。 例如: 01,001,010,100,110就不是前缀编码,因为01是010的前缀,去掉其中一个便是前缀编码。
·哈夫曼编码
对一棵具有n个叶子的哈夫曼树,若对树中的每个左分支赋予0,右分支赋予1,则从根到每个叶子的通路上,各分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码。
哈夫曼编码的相关特性:
·哈夫曼编码是前缀编码
·哈夫曼编码是最优前缀编码
· 哈夫曼编码的作用: 哈夫曼树最典型的就是在编码技术上的应用。利用哈夫曼树可以得到平均长度最短的编码。
哈夫曼编码引例:
·哈夫曼编码的算法实现
typedef char * HuffmanCode[N+1]; //存储哈夫曼编码串的头指针数组
//由于每个哈夫曼编码是变长编码,因此可以使用指针数组存放每个编码串的头指针
void CrtHuffmanCode(HuffmanTree ht, HuffmanCode hc, int n)
{
//从叶子结点到根,逆向求每个叶子结点对应的哈夫曼编码
char *cd;
cd = (char*)malloc(n * sizeof(char));
cd[n-1] = '\0'; //从右往左逐位存放编码,首先存放编码结束符
for(i = 1; i <= n; i ++)
{
start = n - 1;//初始化编码的起始位置
c = i; //从叶结点向上倒推
p = ht[i].parent;
while(p != 0)
{
--start;
if(ht[p].Lchild == c) cd[start] = '0';
else cd[start] = '1';
c = p;
p = ht[p].parent;
}
hc[i] = (char*)malloc((n-start)*sizeof(char));
strcpy(hc[i],&cd[start]);
}
free(cd);
}
👆(若要求传送词间互相区分,则需加入一空白字符^)