/*下面来介绍树
树的度-树中所有结点的度的最大值;树的高度:树中所有节点的层次的最大值
树的抽象数据类型:
ADT Tree{
InitTree(Tree);
DestroyTree(Tree);
CreateTree(Tree);
TreeEmpty(Tree);
Root(Tree);
Parent(Tree);
FirstChild(Tree,x);
NexSibling(Tree, x);
InsertChild(Tree,p,Child);
DeleteChild(Tree,p,i);
TraverseTree(Tree,Visit());
}ADT Tree;
*/
#include<stdio.h>
#include<stdlib.h>
/*
下面来实现二叉树Binary Tree(每个结点度不大于2,每个结点的孩子结点次序不能任意颠倒的树成为二叉树)
二叉树的基本操作:
Init(bt)
Create(bt)
Destroy(bt)
Empty(bt)
Root(bt)
Parent(bt, x) //求双亲函数
LeftChild(bt, x)//求左孩子
RightChild(bt, x)
Traverse(bt)
Clear(bt) //把二叉树bt置为空树
*/
/*
二叉树的性质:
在二叉树的第i层上至多有2的i-1次方个结点;
深度为k的二叉树至多有2的k次方-1个结点;
二叉树终端结点的个数是度为2的结点数+1;
(引入概念:满二叉树,完全二叉树)
性质4:具有n个结点的完全二叉树的深度为log2n(下取整)+1;
性质5:对于具有n个结点的完全二叉树,按照从上到下和从左到右的顺序对二叉树中的所有结点从1开始顺序编号,则对于任意的序号为i的节点有:
i=1,则序号为i的结点是根结点;i>1,则序号为i的结点的双亲结点是:[i/2]
若2i>n,则序号为i的结点无左孩子;如果2i<=n,则序号为i的结点的左孩子结点的序号为2i;
如2i+1>n,则序号为i的结点无右孩子;如2i+1<=n,则序号为i的结点的有孩子结点的序号为2i+1;
*/
/*
二叉树的存储结构:
1-顺序存储结构(完全二叉树由序号可以推断出来左孩子和右孩子的序号);
2-链式存储结构:数据域+左孩子域+右孩子域(LChild+Data+RChild)
*/
typedef struct Node
{
DataType data;
struct Node *LChild;
struct Node *RChild;
}BiTNode, *BiTree;
//若一个二叉树有n个结点,则它的二叉链表中必含有2n个指针域,其中必有n+1个空的链域
//二叉树的遍历(默认先左后右)
//先序遍历DLR;中序遍历LDR;后序遍历LRD
//遍历问题的提出:计算机中的表达式求值-后缀表达式即为逆波兰表达式
//先序遍历二叉树
void PreOrder(BiTree root)
{
if(root!=NULL)
{
Visit(root->data);
PreOrder(root->LChild);
PreOrder(root->RChild);s
}
}
//中序遍历
void InOrder(BiTree root)
{
if(root!=NULL)
{
InOrder(root->LChild);
Visit(root->data);
InOrder(root->RChild);
}
}
//后序遍历二叉树
void PostOrder(BiTree root){
if(root!=NULL)
{
PostOrder(root->LChild);
PostOrder(root->RChild);
Visit(root->data);
}
}
//遍历算法应用
/*
1-输出二叉树中的结点(把visit换成printf)
2-输出二叉树的叶子结点(在1的基础上加一个判断条件:左孩子域和右孩子域都是NULL)
3-统计叶子结点数目(第一种方法:在2的基础上改成计数器+1;第二种方法:分而治之-如果是空树,返回0;如果只有一个结点返回1;否则为左右子树的叶子结点之和
*/
//分治算法统计叶子结点的数目
int leaf(BiTree root)
{
int LeafCount;
if(root==NULL)
LeafCount=0;
else if((root->LChild==NULL)&&(root->RChild==NULL))
LeafCount=1;
else//叶子数为左右子数的叶子数目之和
LeafCount=leaf(root->LChild)+leaf(root->RChild);
return LeafCount;
}
//继续遍历算法的应用
/*
4-给定一颗二叉树的遍历序列,可以创建对应的二叉链表;
*/
//比如说先序序列AB.DF..G..C.E.H..;其中用小圆点表示空子树
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));
}
}
//5-求二叉树的高度(若bt为空,则高度为0;若bt非空,其高度应为其左右子树高度的最大值加1)
//后续遍历求高度
int PostTreeDepth(BiTree bt)
{
int hl, hr, max;
if(bt!=NULL){
hl=PostTreeDepth(bt->LChild); //求左子树的深度
hr=PostTreeDepth(bt->RChild);//求右子树深度
max=hl>hr?hl:hr;s
return (max+1);
}
else return 0;//代表是空树
}
//先序遍历求二叉树高度
void PreTreeDepth(BiTree bt, int h)
{
//h为bt指向结点所在层次,初值为1
if(bt!=NULL)
{
if(h>depth) depth=h;//时刻更新depth为最大
PreTreeDepth(bt->LChild, h+1);
PreTreeDepth(bt->RChild, h+1);
}
}
//应用6-按树状打印二叉树(书P181)
//基于栈的递归消除:这里还不太懂,需要进一步看书
//线索二叉树
/*
想法:在n个结点的二叉链表中有2n个链域,但有n+1个是空的。
可以用空链存放结点的前驱和后继信息;
为结点结构增设两个标志域Ltag和Rtag
LChild Ltag Data Rtag RChild
tag=0代表存放的是孩子;Ltag=1代表存放的是遍历前驱,Rtag=1代表存放的是遍历后继
*/
//二叉树的线索化实质是将二叉链表的空指针域填上相应结点的遍历前去或后继结点的地址;
//线索化的过程即为在遍历过程中修改空指针域的过程
//建立中序线索树(不同的遍历方法得到不同的线索二叉树)
BiTNode* pre=NULL;
void Inthread(BiTree root)
//pre始终指向刚访问过的结点,初值为NULL
{
if(root!=NULL)
{
Inthread(root->LChild);//线索化左子树
//root和pre互相置
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)
{
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)
{
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(BiTree Bt);
while(p)
{
visit(p);
p=InNext(p);
}
}
//由给定的序列确定二叉树:先序或后序+中序可以唯一的确定二叉树
int main()
{
return 0;
}
#include<stdio.h>
/*
树的表示方法:
1:Data-Parent双亲表示法
2: 孩子表示法(孩子链表)
3:孩子兄弟表示法(树的二叉表示法)
*/
/*
树、森林和二叉树的相互转换
*/
//树的遍历:先根遍历( 转换后的前序遍历);后根遍历(转换后的中序遍历)
//双亲表示法
#define MAX 100
typedef struct TNode{
DataType data;
int parent;
}TNode;
typedef struct
{
TNode tree[MAX];
int nodenum; //结点数
}ParentTree;
//孩子表示法:把每个结点的孩子用单链表组织起来,而将各个单链表的头结点组成一个顺序表
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;
/*孩子兄弟表示法(树的二叉表示法)
链表中每个结点有两个链域,指向该结点的第一个孩子结点和下一个兄弟结点
*/
typedef struct CSNode
{
DataType data;
Struct CSNode *FirstChild;
Struct CSNode *NextSibling;
}CSNode, *CSTree; //便于实现数的各种操作
//以后都用孩子兄弟表示法
void RootFirst(CSTree root)
{
if(root!=NULL)
{
Visit(root->data);
p=root->FirstChild;
while(p!=NULL)
{
RootFirst(p); //访问以p为根的子树
p = p->NextSibling;
}
}
}
//遍历方法二
void RootFirst(CSTree root)
{
if(root!=NULL)
{
Visit(root->data);
RootFirst(root->FirstChild);
RootFirst(root->Nextsibling);//这里应该是删除了原来的root,现在的root变为了以前root的第一个孩子
}
}
int main()
{
return 0;
}
#include<stdio.h>
/*
基本概念:
路径,路径长度;结点的权,带权路径长度(路径长度与该结点的权的乘积成为带权路径长度)
树的带权路径长度:树中从根到所有叶子结点的各个带权路径长度之和
*/
/*
有关结论:完全二叉树最小路径长度;
哈夫曼树(二叉树)带权路径长度最小
*/
/*构造哈夫曼树的算法:
初始化(构造有n棵仅有一个点的二叉树,构成森林F)
找最小树(在森林F中选择两颗根结点权值最小的二叉树,作为一颗新二叉树的左、右子树,重新标记新二叉树根结点的权值)
删除与加入
判断并重复(主要思想:让权值小的在深层)
*/
//哈夫曼树存储结构
//哈夫曼树的特征:没有度为1的结点(拥有n个叶子的哈夫曼树共有2n-1个结点)
//静态三叉链表:权值+双亲序号+左孩子序号+右孩子序号(0号单元代表不使用)
//哈夫曼树类型定义
#define N 20
#define M 2*N-1
typedef struct
{
int weight;
int parent;
int LChild;
int RChild;
}HTNode, HuffmanTree[M+1]; //HuffmanTree是一个结构数组类型,0号单元不用
//创建哈夫曼树ht[M+1], w[]存放n个权值(叶子结点)
void CrtHuffmanTree(HuffmanTree ht, int w[], int n)
{
//初始化
for(i=1; i<=n; i++) ht[i]={w[i], 0, 0, 0}
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++) //创建非叶结点,建二叉树s
{
select(ht, i-1, &s1,) //从h[1]到h[i-1]选取两个parent为0且weight最小的结点,并用s1和s2保存对应序号值
ht[i].weight=ht[s1].weight+ht[s2].weught;
ht[s1].parent = i; ht[s2].weight = i;
ht[i].LChild=s1; ht[i].RChild=s2;
}
} //哈夫曼树建立完毕
/*哈夫曼编码*/
//终于来到真正有意思的部分了
/*
前缀编码:在一个编码系统任一编码都不是其他任何编码的前缀
哈夫曼编码:对哈夫曼树,树中左分支赋予0,右分支赋予1,从根到每个叶子的通路上,各分支的赋值构成了一个二进制串
这个二进制串就是哈夫曼编码
*/
/*
哈夫曼编码的相关性质:
1-哈夫曼编码是前缀编码
2-哈夫曼编码是最优前缀编码
*/
//求哈夫曼树的哈夫曼编码的算法
void CrtHuffmanCode(HuffmanTree ht, HuffmanCode hc, int n)
{ //从叶子结点到根,逆向求每个叶子结点对应的哈夫曼编码
char *cd;
cd=(char *)malloc(sizeof(char));
cd[n-1]='\0'; //从右到左诸位存放编码,首先存放对应结束符
//求n个叶子结点对应的哈夫曼编码(上面是n-1的原因是哈夫曼树n-1个非叶子结点)
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'; //左分支标0
else cd[start]='1';
c=p; p=ht[p].parent;
}
hc[i]=(char*)malloc(sizeof(char));
strcpy(hc[i], &cd[start]);}
free(cd);
}
int main()
{
return 0;
}
终于来到了非线性数据结构的领域,知识点都在以上代码段中用注释的形式给出了。有的具体内容没有写完整,详细参考耿国华老师和严蔚敏老师分别写的《数据结构》。
总结一下:了解数据结构树家族,首先要了解一些有关树的基本概念,一些名词概念和性质,了解树的ADT是什么样子的。然后了解二叉树的代码实现,重点关注其遍历方式(前序、中序、后序),知道什么。之后,由二叉树,推广了解一般的树,对应的代码实现。由一般的树,进一步了解森林,学习树、森林、二叉树之间的关系和转换。霍夫曼树的代码我还没写,到时候写完了粘上来。(更于2022.4.27已经补充好了最后一段就是)
其他想到了再补充。最近在学写CPU,想看的可以留言我有时间出个帖子。看关注我的好多都是因为JAVA那篇关注我的,下一篇文章预计写一下JAVA有关知识,把UML图一起梳理了。先立个flag有时间回来填坑。