【树】(上)
3.1树与树的表示
3.3.1/2引子(查找)
什么是树?
客观世界中许多事物存在层次关系,这种层次关系就是我们所说的树。
分层次组织在管理上有更高的效率!
数据管理的基本操作之一:查找
如何实现有效率的查找?
查找
查找:根据某个给定关键词K,从集合R中找出关键字与K相同的记录
静态查找
方法1:顺序查找
无哨兵
int SequentialSearch(List Tbl,ElementType K)
{
int i;
for(i=Tbl->Length;i>0&&Tbl->ElementType[i]!=K;i--);
//循环退出时,要么没找到i=0,或者找到了i
return i;//直接返回i即可
}
有哨兵
int SequentialSearch(List Tbl,ElementType K)
{
int i;
Tbl->Element[0]=K;//建立哨兵
for(i=Tbl->Length;Tbl->ElementType[i]!=K;i--);
return i;//直接返回i即可
}
方法2:二分查找 (效率更高)
二分查找的前提:有序,数组中存放
int BinarySearch(List Tbl,ElementType K)
{
int left,right,mid,NOFOUND=-1;
left=1;
right=Tbl->Length;
while(left<=right)
{
mid=(left+right)/2;
if(K<Tbl->Element[mid])
{
right=mid-1;
}else if(K>Tbl->Element[mid]){
left=mid+1;
}else return mid;
}
return NOFOUND;
}
二分查找判定树(加强理解
想想能不能不用数组的形式实现二分查找?
我们以树的一种形式来存储我们需要的数据,使得我们查找的过程更加方便。查找树的效率跟二分查找基本一样,但当在树里插入结点、删除结点时会更加方便。这样可以很好的的解决我们遇到的第二个问题,动态查找问题。
3.1.4树的定义与术语
树(Tree):n个结点构成的有限集合。
当n=0时,称为空树。
任一棵非空树,它具备以下性质:
树的基本术语:
3.1.5树的表示
树是否能用数组实现?用数组实现就是把这些结点按顺序存储在数组里面,难度是比较大的。因为只看到了一个结点的顺序我们很难判别它的父亲和儿子。
如果用链表实现呢?每个结点用一个结构来表示,
逻辑如图。
但仔细看会发现,其中每个结构的样子都不一样,会给程序实现带来困难。
我们采用儿子--兄弟表示法:
3.2二叉树及存储结构
3.2.1二叉树的定义及性质
二叉树就是度为2的一种树,每个节点的指针最多是2个。
研究二叉树就能解决一般树的问题。
特殊二叉树
斜二叉树:线性结构,是一种链表。
完美/满二叉树:完美二叉树,一个不少。
完全二叉树:在完美二叉树的基础上少几片叶,同时序号要连贯
二叉树的几个重要性质
1.一个二叉树的第i层最大节点数为:,;
2.深度为k的二叉树有最大节点数为,;
从第一层到第k层的最大总结点数为:,求和得到。
3.对于任何非空二叉树T,若用n0表示叶节点,n1表示度为1的数,n2为度为2的非叶节点个数
(度表示节点的子树个数)
则二叉树的总结点数sum=n0+n1+n2;
同时满足关系n0=n2+1
证明:
除了根节点没有边外,其他所有节点都有且只有一条边。因此,边的总数=节点总数-1=n0+n1+n2;
再从上往下看,每个节点对边的贡献数是不一样的。
因此对应,边的总数=n2*2+n1*1+n0*0
给出等式:n0+n1+n2=n2*2+n1*1+n0*0,即n0=n2+1
二叉树的抽象数据类型定义
二叉树的最主要操作---遍历;
常用遍历方法:
void PreOrderTravelsal(BinTree BT) //先序--根,左,右
void InOrderTravelsal(BinTree BT) //中序--左,根,右
void PostOrderTravel(BinTree BT) //后序--左,右,根
void LevelOrderTraversal(BinTree BT) //层次遍历--从上到下,从左到右
3.3.2二叉树的存储结构
顺序存储结构
完全二叉树用数组实现非常方便。
想要遍历,我们可以发现一些规律,使得数组实现非常容易;
非根结点(序号 i > 1)的父结点的序号是 [i / 2];
结点 (序号为 i ) 的左孩子结点的序号是 2i, 其中2 i <= n,否则没有左孩子;
结点 (序号为 i ) 的右孩子结点的序号是 2i+1,其中2 i +1<= n,否则没有右孩子;
同时,一般的二叉树也可以采用这种方式,但会带来空间的浪费。
把一般的二叉树补成一个完全二叉树,同时相应缺的节点要在数组里留下空位。
链表存储
typedef struct TreeNode *BinTree;
typedef BinTree Position;
struct TreeNode{
ElementType Data;
BinTree Left;
BinTree Right;
}
3.3.3二叉树的遍历
先序中序后序遍历
(主要以链式存储结构实现)
1.先序遍历:根-左-右
void PreOrderTraversal( BinTree BT )
{
if( BT ) {
printf(“%d”, BT->Data);
PreOrderTraversal( BT->Left );
PreOrderTraversal( BT->Right );
}
}
2.中序遍历:左-根-右
void InOrderTraversal( BinTree BT )
{
if( BT ) {
InOrderTraversal( BT->Left );
printf(“%d”, BT->Data);
InOrderTraversal( BT->Right );
}
}
3.后序遍历:左-右-根
void PostOrderTraversal( BinTree BT )
{
if( BT ) {
PostOrderTraversal( BT->Left );
PostOrderTraversal( BT->Right);
printf(“%d”, BT->Data);
}
}
先序、中序和后序遍历过程:遍历过程中经过结点的路线一样,只是访问各结点的时机不同。
中序非递归遍历
遇到一个结点,就把它压栈,并去遍历它的左子树
当左子树遍历结束后,从栈顶弹出这个结点并访问它
然后按其右指针再去中序遍历该结点的右子树
void InOrderTravelsal(BinTree BT)
{
BinTree T=BT;
stack S=CreatStack(MAXSIZE);//申请栈空间
while(T||!IsEmpty(S))//树不空或者堆栈不空就进行
{
while(T)//树不空就进行,直到树为空
{
push(S,T);//压入栈
T=T->left;//指向左边
}
if(!IsEmpty(S))//栈不空
{
T=Pop(S); //节点弹出栈,同时用T记录弹出的,以便后面操作
printf("%d",T->Data);
T=T-right;//转向右子树
}
}
}
理解代码:
申请栈空间;
一直往左移,直到空为止;//此时栈不空
T为空了,则弹出并用T接受,
转向右子树;
理解了上面的代码,就可以同样的写出先序和后序的算法,只需要改变printf的位置即可。
层序遍历
二叉树遍历的核心问题:二维结构的线性化
从结点访问其左、右儿子结点,访问左儿子后,右儿子结点怎么办?
需要一个存储结构保存暂时不访问的结点:堆栈、队列
队列实现:遍历从根结点开始,首先将根结点入队,然后开始执行循环:结点出队、访问该结点、其左右儿子入队。
层序基本过程:先根结点入队,然后:
从队列中取出一个元素;
访问该元素所指结点;
若该元素所指结点的左、右孩子结点非空, 则将其左、右孩子的指针顺序入队。
void LevelOrderTravelsal(BinTree BT)
{
Queue Q;
BinTree T;
if(!BT) return;//若树为空则直接返回
Q=CreatQueue(MAXSIZE); //创建并初始化队列
AddQ(Q,BT);//入队
while(!IsEmptyQ(Q))//当不空
{
T=DeleteQ(Q);
printf("%d",&Q->data);
if(T->left) AddQ(Q,T->left);
if(T->right) AddQ(Q,T->right);
}
}
3.3.4遍历应用例子
1.求二叉树高度
int PostOrderGetHeight( BinTree BT )
{
int HL, HR, MaxH;
if( BT ) {
HL = PostOrderGetHeight(BT->Left); /*求左子树的深度*/
HR = PostOrderGetHeight(BT->Right); /*求右子树的深度*/
MaxH = (HL > HR)? HL : HR; /*取左右子树较大的深度*/
return ( MaxH + 1 ); /*返回树的深度*/
}
else return 0; /* 空树深度为0 */
}
2.二元表达式树
中缀表达式会受到运算符优先级的影响。
3.确定二叉树
树的同构问题
题目:给定两棵树T1和T2。如果T1可以通过若干次左右孩子互换就变成T2,则我们称两棵树是“同构”的。现给定两棵树,请你判断它们是否是同构的
输入格式:
输入给出2棵二叉树的信息:
• 先在一行中给出该树的结点数,随后N行
• 第i行对应编号第i个结点,给出该结点中存储的字母、其左孩子结点的编号、右孩子结点的编号。
• 如果孩子结点为空,则在相应位置上给出“-”。
求解思路: 1. 二叉树表示 2. 建二叉树 3. 同构判别
二叉树表示
1.用链表,两个指针
2.用数组,补成完全二叉树
3.结构数组表示二叉树
物理上的存储时数组,但思想是链表的思想。//静态链表
#define MaxTree 10
#define ElementType char
#define Tree int
#define Null -1
struct TreeNode
{
ElementType Element;
Tree Left;//指向左儿子数组下标
Tree Right;
} T1[MaxTree], T2[MaxTree];
程序框架搭建
如何建二叉树
Tree BuildTree( struct TreeNode T[] )
{ …..
scanf("%d\n", &N);//n个节点
if (N)
{
for (i=0; i<N; i++) check[i] = 0;
for (i=0; i<N; i++) {
scanf("%c %c %c\n", &T[i].Element, &cl, &cr);
if (cl != '-') {//左儿子位置
T[i].Left = cl-'0';//减去字符0的到字符类型
check[T[i].Left] = 1;//把被指向的节点check就赋为1,根节点不会被指向,即check=0代表根节点
}else T[i].Left = Null;
…….. /*对cr的对应处理 */
}
for (i=0; i<N; i++)
if (!check[i]) break;
Root = i;
}
return Root;
}
如何判别两二叉树同构
int Isomorphic ( Tree R1, Tree R2 )
{
if ( (R1==Null )&& (R2==Null) ) /* both empty */
return 1;
if ( ((R1==Null)&&(R2!=Null)) || ((R1!=Null)&&(R2==Null)) )
return 0; /* one of them is empty */
if ( T1[R1].Element != T2[R2].Element )
return 0; /* roots are different */
if ( ( T1[R1].Left == Null )&&( T2[R2].Left == Null ) )
/* both have no left subtree */
return Isomorphic( T1[R1].Right, T2[R2].Right );
……
}
【树】(中)
4.1二叉搜索树
查找问题:
静态查找与动态查找
针对动态查找,数据如何组织?
二叉搜索树:一棵二叉树,可以为空;如果不为空,满足以下性质: 1. 非空左子树的所有键值小于其根结点的键值。 2. 非空右子树的所有键值大于其根结点的键值。 3. 左、右子树都是二叉搜索树。
二叉搜索树操作的特别函数:
Position Find( ElementType X, BinTree BST ):从二叉搜索树BST中查找元素X,返回其所在结点的地址;
Position FindMin( BinTree BST ):从二叉搜索树BST中查找并返回最小元素所在结点的地址;Position FindMax( BinTree BST ) :从二叉搜索树BST中查找并返回最大元素所在结点的地址。
BinTree Insert( ElementType X, BinTree BST ) BinTree Delete( ElementType X, BinTree BST )
查找操作
查找从根结点开始,如果树为空,返回NULL
若搜索树非空,则根结点关键字和X进行比较,并进行不同处理:
若X小于根结点键值,只需在左子树中继续搜索;
如果X大于根结点的键值,在右子树中进行继续搜索;
若两者比较结果是相等,搜索完成,返回指向此结点的指针。
1.递归实现
Position Find(ElemetntType X,BinTree BT)
{
if(!BT) return NULL;//树为空,查找失败
if(X>BT->Data)
{
return Find(X,BT->right);
}else if(X<BT->left){
return Find(X,BT->left);
}
else return BT;//X==BT->Data,查找成功
}
由于非递归函数的执行效率高,可将尾递归函数改为迭代函数。
2.迭代实现(循环)
Position IterFind(ElementType X,BInTree Bt)
{
while(BT)//只要不空就一直缩小范围
{
if(X>BT->Data)
{
BT=BT->right;
}else if(X<BT->Data){
BT=BT->left;
}
else return BT;
}
return NULL;//循环退出,树为空,则未找到
}
由此可以看出,查找的效率取决于树的高度。
因此希望树看起来比较平衡,不往一边倒。所以,我们后面会提到哟个重要的议题:平衡二叉树。
查找最大和最小元素
最大元素一定是在树的最右分枝的端结点上
最小元素一定是在树的最左分枝的端结点上
1.递归实现
Position FindMin(BinTree BT)
{
if(!BT) return NULL;//空则返回NULL
else if(!BT->left) return BT;//左子子树为空直接返回BT
//以上为递归终止条件
else return FindMin(BT->left);
}
2.迭代实现
Position FindMax(BinTree BT)
{
if(BT)//只要BT不空
while(BT->Left) BT=BT->left; //一直往左子树移动
return BT;
}
二叉搜索树的插入操作
〖分析〗关键是要找到元素应该插入的位置, 可以采用与Find类似的方法
BinTree Insert(ElementType X,BinTree BT)
{
if(!BT)//若原树为空,就插入
{
BT=malloc(sizeof(struct TreeNode));
BT->Data=X;
BT->left=Bt->right=NULL;
}else{
if(X>BT->Data)
{
return Insert(X,BT->right);
}else if(X<BT->Data){
return Insert(X,BT->left);
}
}
return BT;
}
二叉搜索树的删除操作
考虑三种情况:
1.要删除的是 叶结点: 直接删除,并再修改其父结点指针---置为NULL
2.要删除的结点 只有一个孩子结点: 将其父结点的指针指向要删除结点的孩子结点
3.要删除的结点有左、右两棵子树: 用另一结点替代被删除结点:右子树的最小元素 或者 左子树的最大元素
BinTree Delete( ElementType X, BinTree BST )
{ Position Tmp;
if( !BST ) printf("要删除的元素未找到");
else if( X < BST->Data )
BST->Left = Delete( X, BST->Left); /* 左子树递归删除 */
else if( X > BST->Data )
BST->Right = Delete( X, BST->Right); /* 右子树递归删除 */
//以上实质上是查找操作
else /*找到要删除的结点 */ if( BST->Left && BST->Right ) { /*被删除结点有左右两个子结点 */
Tmp = FindMin( BST->Right );
/*在右子树中找最小的元素填充删除结点*/
BST->Data = Tmp->Data;
BST->Right = Delete( BST->Data, BST->Right);
/*在删除结点的右子树中删除最小元素*/
} else { /*被删除结点有一个或无子结点*/
Tmp = BST;
if( !BST->Left ) /* 有右孩子或无子结点*/
BST = BST->Right;
else if( !BST->Right ) /*有左孩子或无子结点*/
BST = BST->Left;
free( Tmp );
}
return BST;
}
4.2平衡二叉树
什么是平衡二叉树
搜索树结点不同插入次序,将导致不同的深度和平均查找长度ASL
平衡因子(Balance Factor,简称BF): BF(T) = hL-hR(其中hL和hR分别为T的左、右子树的高度)
平衡二叉树(Balanced Binary Tree)(AVL树)
空树,
或者 任一结点左、右子树高度差的绝对值不超过1,即|BF(T) |≤ 1
给定结点数为 n的AVL树的最大高度为O(log2n)!
平衡二叉树的调整
1.RR旋转
不平衡的“发现者”是Mar,“麻烦结点”Nov 在发现者右子树的右边,因而叫 RR 插入,需要RR 旋转(右单旋)
注意:这里BL是在B的左子树上,因此BL<B,且BL>A,因此旋转后直接把BL挂在A的右子树上
同时,破坏节点在BR的左边或右边都无所谓,拎起来B的时候给BR留了高度,即破坏节点只要在发现者的右子树的右子树上就不影响。
例子:
2.LL旋转
理解了RR,理解LL旋转就是顺其自然。
“发现者”是Mar,“麻烦结点”Apr 在发现者左子树的左边,因而叫 LL 插入,需要LL 旋转(左单旋)
这里,apr<aug<mar.
3.LR旋转
“发现者”是May,“麻烦结点”Jan在左子树的右边,因而叫 LR 插入,需要LR 旋转
4.RL旋转
5.代码
typedef struct AVLNode *Position;
typedef Position AVLTree; /* AVL树类型 */
struct AVLNode{
ElementType Data; /* 结点数据 */
AVLTree Left; /* 指向左子树 */
AVLTree Right; /* 指向右子树 */
int Height; /* 树高 */
};
int Max ( int a, int b )
{
return a > b ? a : b;
}
AVLTree SingleLeftRotation ( AVLTree A )
{ /* 注意:A必须有一个左子结点B */
/* 将A与B做左单旋,更新A与B的高度,返回新的根结点B */
AVLTree B = A->Left;
A->Left = B->Right;
B->Right = A;
A->Height = Max( GetHeight(A->Left), GetHeight(A->Right) ) + 1;
B->Height = Max( GetHeight(B->Left), A->Height ) + 1;
return B;
}
AVLTree DoubleLeftRightRotation ( AVLTree A )
{ /* 注意:A必须有一个左子结点B,且B必须有一个右子结点C */
/* 将A、B与C做两次单旋,返回新的根结点C */
/* 将B与C做右单旋,C被返回 */
A->Left = SingleRightRotation(A->Left);
/* 将A与C做左单旋,C被返回 */
return SingleLeftRotation(A);
}
/*************************************/
/* 对称的右单旋与右-左双旋请自己实现 */
/*************************************/
AVLTree Insert( AVLTree T, ElementType X )
{ /* 将X插入AVL树T中,并且返回调整后的AVL树 */
if ( !T ) { /* 若插入空树,则新建包含一个结点的树 */
T = (AVLTree)malloc(sizeof(struct AVLNode));
T->Data = X;
T->Height = 0;
T->Left = T->Right = NULL;
} /* if (插入空树) 结束 */
else if ( X < T->Data ) {
/* 插入T的左子树 */
T->Left = Insert( T->Left, X);
/* 如果需要左旋 */
if ( GetHeight(T->Left)-GetHeight(T->Right) == 2 )
if ( X < T->Left->Data )
T = SingleLeftRotation(T); /* 左单旋 */
else
T = DoubleLeftRightRotation(T); /* 左-右双旋 */
} /* else if (插入左子树) 结束 */
else if ( X > T->Data ) {
/* 插入T的右子树 */
T->Right = Insert( T->Right, X );
/* 如果需要右旋 */
if ( GetHeight(T->Left)-GetHeight(T->Right) == -2 )
if ( X > T->Right->Data )
T = SingleRightRotation(T); /* 右单旋 */
else
T = DoubleRightLeftRotation(T); /* 右-左双旋 */
} /* else if (插入右子树) 结束 */
/* else X == T->Data,无须插入 */
/* 别忘了更新树高 */
T->Height = Max( GetHeight(T->Left), GetHeight(T->Right) ) + 1;
return T;
}
是否同一颗二叉搜索树
给定一个插入序列就可以唯一确定一棵二叉搜索树。然而,一棵给定的二叉搜索树却可以由多种不同的插入序列得到。
例如,按照序列{2, 1, 3}和{2, 3, 1}插入初始为空的二叉搜索树,都得到一样的结果。
问题:对于输入的不同插入序列,你需要判断它们是否能生成一样的二叉搜索树
求解思路:
两个序列是否对应相同搜索树的判别
方法1.分别建两棵搜索树的判别方法
根据两个序列分别建树,再判别树是否一样
即判断两颗树的根节点,左子树,右子树是否一样。
是很容易实现的。
方法2.不建树的判别方法
方法3. 建一棵树,再判别其他序列是否与该树一致
1. 搜索树表示 2. 建搜索树T 3. 判别一序列是否与搜索树T一致
1.搜索树的表示
typedef struct TreeNode* Tree;
struct TreeNode
{
int v;
Tree Left,Right;
int flag;
}
这里多了一个域--flag,用来判别一个序列是否跟树一致
实际含义:如果某个节点没被访问过flag==0,被访问过就flag==1,作为有没有被访问过的标记。
2.程序框架的搭建
需要设计的主要函数: 读数据建搜索树T 判别一序列是否与T构成一样的搜索树
数据结构_中国大学MOOC(慕课) (icourse163.org)
5.5.1堆
优先队列(Priority Queue):特殊的“队列”,取出元素的顺序是依照元素的优先权(关键字)大小,而不是元素进入队列的先后顺序。
若采用数组或链表实现优先队列
数组 : 插入 — 元素总是插入尾部 ~ ( 1 ) 删除 — 查找最大(或最小)关键字 ~ ( n ) 从数组中删去需要移动元素 ~ O( n ) 链表: 插入 — 元素总是插入链表的头部 ~ ( 1 ) 删除 — 查找最大(或最小)关键字 ~ ( n ) 删去结点 ~ ( 1 ) 有序数组: 插入 — 找到合适的位置 ~ O( n ) 或 O(log2 n ) 移动元素并插入 ~ O( n ) 删除 — 删去最后一个元素 ~ ( 1 ) 有序链表: 插入 — 找到合适的位置 ~ O( n ) 插入元素 ~ ( 1 ) 删除 — 删除首元素或最后元素 ~ ( 1 )
是否可以采用二叉树存储结构? 二叉搜索树? 如果采用二叉树结构,应更关注插入还是删除? 树结点顺序怎么安排? 树结构怎样?
用树操作首先会想到用一个查找树,插入、删除是跟树的高度有关的,
我们每次都要删除一个最大或最小元素,也就是在树的最左边或者最右边,重复几次操作之后树会变得一边倒。效率也就不是log2N
因此,我们不能简单的使用查找树,同时要重点考虑删除操作。
一种想法:把数据放在二叉树里面,最大的在树根,如果要删除的话,拿掉树根就好了。
用完全二叉树进行存储
堆的两个特性
结构性:用数组表示的完全二叉树; 有序性:任一结点的关键字是其子树所有结点的最大值(或最小值)
“最大堆(MaxHeap)”,也称“大顶堆”:最大值 “最小堆(MinHeap)”,也称“小顶堆” :最小值
堆的抽象数据类型描述
最大堆的创建
typedef struct HeapStruct* MaxHeap;
struct HeapStruct
{
ElementType* Elements;//存储堆元素的数组
int Size;//堆的当前元素个数
int Capacity;//堆的最大容量
}
MaxHeap Create(int MAXSIZE) //创建容量为MAXSIZE的空的最大堆
{
MaxHeap H=malloc(sizeof (struct HeapStruct));
H->Elements=malloc((MAXSIZE+1)*sizeof(ElementType));
//Elements本身是要指向一个数组,所以我们再申请一块数组空间。
//堆是从数组下标为1的地方开始存放,所以是MAXSIZIE+1
H->Size=0;
H->Capacity=MAXSIZE;
H->Elements[0]=Maxdata;//定义哨兵为大于堆中所有可能元素的值,便于以后更快操作
return H;
}
插入
void Insert(MaxHeap H,ElementType item)
{
int i;
if(IsFull(H))
{
printf("最大堆已满"); return 0;
}
i=++H->Size;//i指向堆中最后一个元素位置
while(H->Elements[i/2]<item)//只要根节点比item小
{
H->Elements[i]=H->Elements[i/2];//原先根节点往下走
i/=2;//往上走
}
H->Elements[i]=item;//到上面的根节点
}
H->Element[ 0 ] 是哨兵元素,它不小于堆中的最大元素,控制顺环结束。
删除
取出根节点元素,同时删除堆的一个节点。
ElementType DeleteMax(MaxHeap H)
{
int Parent,Child;
ElementType MaxItem,temp;
if(IsEmpty(H)) printf("最大堆已空"); return 0;
MaxItem=H->Elements[1];//保存最大元素,以便函数最后返回删除元素
temp=H->Elements[H->Size];//保存最后一个元素
Size--;//删除元素,size要减少
for(Parent=1;Parent*2<=H->Size/*根节点有儿子*/;Parent=Child)
{
Child=Parent*2;//左儿子
if((Child!=H->Size)/*根节点有右儿子*/&&(H->Elements[Child]<H->Elements[Child+1]))
{
Child++;//Child记录较大儿子
}
if(temp>=H->Elements[Child]) break;//最后一个元素比儿子都要大,直接结束
else H->Elements[Parent]= H->Elements[Child];
}
H->Elements[Parent]=temp;
return MaxItem;
}
5.5.2哈夫曼树与哈夫曼编码
什么是哈夫曼树?
已知频率不一样,如何进行编码能达到比较好的效果?
例子:
哈夫曼树的定义
带权路径长度(WPL):设二叉树有n个叶子结点,每个叶子结点带有权值 wk,从根结点到每个叶子结点的长度为 lk,则每个叶子结点的带权路径长度之和就是: k=1
最优二叉树或哈夫曼树: WPL最小的二叉树
不同的构造导致最后的WPL值是不一样的,接下来讨论哈夫曼树的构造问题。
哈夫曼树的构造
核心:每次把权值最小的两棵二叉树合并。
typedef struct TreeNode* HuffmanTree;
struct TreeNode
{
int weight;
HuffmanTree Left,Right;
}
HuffmanTree Huffman(MinHeap H)
{//假设H->Size个权值已存在H->Elements[]->weight里
int i; HuffmanTree T;
BuildMinHeap(H);//将H->Elements[]按权值调为最小堆
for(i=1;i<H->Size;i++)//要做Size-1次合并
{
T=malloc(sizeof(struct TreeNode));//建立新节点
T->Left=DeleteMin(H);//从最小堆删除,作为新T的左子节点
T->Right=DeleteMin(H);
T->weight=T->Left->weight+T->Right->weight;//新权值为左右权值的和
Insert(H,T);//将新T插入最小堆
}
T=DeleteMin(H);
return T;//返回新树的树根
}
整体复杂度为O(NlogN)
哈夫曼树的特点
1.没有度为1的结点;
哈夫曼树的本质是每两个节点结合在一起,当然不可能有度为1的节点
2.哈夫曼树的任意(非叶)节点的左右子树交换后仍是哈夫曼树;
3.n个叶子结点的哈夫曼树共有2n-1个结点;
之前的结论:n0=n2+1,化为n2=n0-1
节点总数:n0+n2=n0-1+n0=2n-1
4.对同一组权值{w1,w2, …… , wn},存在不同构的两棵哈夫曼树。但WPL值相同。
哈夫曼编码
其中会出现一些问题,需要我们注意:
如果a、e、s、t分别编码为1、0、10、11,那么aet、saa、st的编码都会是1011
如何避免二义性呢?
前缀码prefix code:任何字符的编码都不是另一字符编码的前缀
可以无二义地解码
用二叉树进行编码:
(1)左右分支:0、1
(2)字符只在叶结点上
怎么构造一颗编码代价最小的二叉树?
也就是构造一棵哈夫曼树。
集合及运算
集合运算:交、并、补、差,判定一个元素是否属于某一集合。
并查集问题中集合存储如何实现?
可以用树结构表示集合,树的每个结点代表一个集合元素
查:要查某个元素属于哪个集合,是从这个元素的节点开始找树根。
并:把一个根连到另外一个根
因此,在集合的运算中我们需要查找集合的parent,用到双亲表示法--孩子指向双亲
这里采用数组的存储方式:
typedef struct
{
ElementType Data;
int Parent;//父亲的数组下标
}setType;
查找某个元素所在集合(用根节点表示)
int Find(SetTpe S[],ElementType X)
{
int i;
for(i=0;i<MAXSIZE&&S[i].Data!=X;i++);
if(i>=MAXSIZE) return -1;//找不到
for(;S[i].Parent>0;i=S[i].Parent); //往上走,寻找树的根节点
return i;
}
并运算
分别找到X1和X2两个元素所在集合树的根结点
如果它们不同根,则将其中一个根结点的父结点指针设置成另一个根结点的数组下标。
void Union(SetType S[],ElementType X1,ElementType X2)
{
int Root1,Root2;
Root1=Find(S,X1);
Root2=Find(S,X2);
if(Root1!=Root2) S.[Root2].Parent=Root1;
}
不断Union的时候树越来越高,查找效率会变低;
为了改善合并以后的查找性能,可以采用小的集合并到相对大的集合中(修改Union函数)
这样的话我们就需要比较两个集合的大小,也就是要记录集合中的元素个数,
但每个节点都记录会浪费存储空间,我们只需要让根节点记录即可
同时可以观察到,根节点都是-1,既然都是根节点数组下标只要是负数就行(用来区分根节点与非根节点),我们可以用它来代表元素个数
如:
然后就只需要改一下Union函数,判断一下哪个集合大,然后把小的挂在上面就好了。
#define MAXN 1000 /* 集合最大元素个数 */
typedef int ElementType; /* 默认元素可以用非负整数表示 */
typedef int SetName; /* 默认用根结点的下标作为集合名称 */
typedef ElementType SetType[MAXN]; /* 假设集合元素下标从0开始 */
void Union( SetType S, SetName Root1, SetName Root2 )
{ /* 这里默认Root1和Root2是不同集合的根结点 */
/* 保证小集合并入大集合 */
if ( S[Root2] < S[Root1] ) { /* 如果集合2比较大 */
S[Root2] += S[Root1]; /* 集合1并入集合2 */
S[Root1] = Root2;
}
else { /* 如果集合1比较大 */
S[Root1] += S[Root2]; /* 集合2并入集合1 */
S[Root2] = Root1;
}
}
SetName Find( SetType S, ElementType X )
{ /* 默认集合元素全部初始化为-1 */
if ( S[X] < 0 ) /* 找到集合的根 */
return X;
else
return S[X] = Find( S, S[X] ); /* 路径压缩 */
}
并查集的应用---File transfer
1.集合的简化表示
2.题意理解
5,代表计算机个数
C代表check查询3和2之间是否联通
I 代表input,在3和2之间加一条网线
S表示整个输入结束
输入结束后要对整个网络进行判断,它分成了几个联通集
3.程序框架搭建
int main()
{
SetType S;//创建集合
int n;
char in;
scanf("%d\n", &n);
Initialization( S, n );//初始化集合
do
{
scanf("%c", &in);//读入指令
switch (in)
{//处理指令
case 'I': Input_connection( S ); break;//检查是否连好find,然后union。
case 'C': Check_connection( S ); break;//检查是不是在一个集合里面,find
case 'S': Check_network( S, n ); break;//数数集合的根,也就是有几个联通集
}
} while ( in != 'S')//没结束
return 0;
}
void Input_connection( SetType S )//检查是否连好find,然后union。
{
ElementType u, v;
SetName Root1, Root2;
scanf("%d %d\n", &u, &v);//读进两个计算机编号
Root1 = Find(S, u-1);//查所属集合的根节点
Root2 = Find(S, v-1);
if ( Root1 != Root2 ) Union( S, Root1, Root2 );
}
void Check_connection( SetType S )//检查是否联通
{
ElementType u, v;
SetName Root1, Root2;
scanf("%d %d\n", &u, &v);
Root1 = Find(S, u-1);
Root2 = Find(S, v-1);
if ( Root1 == Root2 ) printf("yes\n");
else printf("no\n");
}
void Check_network( SetType S, int n )//数有几个联通集
{
int i, counter = 0;
for (i=0; i<n; i++)
{
if ( S[i] < 0 ) counter++;//扫描根节点
}
if ( counter == 1 ) printf("The network is connected.\n");
else printf("There are %d components.\n", counter);
}
整个程序的运行效率取决于Find和Union函数是怎么实现的。
too simple sometimes neives,以上比较简单的代码无法通过一些测试集且易超时。
4.按秩归并
改进Union函数
按高度
S[root]=-树的高度,根相同的时候高度才变。
按规模
S[root]=-元素个数,规模一定会改变。
按照等秩归并,树高最大为O(logN)
复杂度为N O(logN)
5.路径压缩
改进Find函数
并查集路径压缩 | 菜鸟教程 (runoob.com)这里解释得很详细,就不在此赘述。
SetName Find(SetType S, ElementType X)
{
if (S[X] < 0) /* 找到集合的根 */
return X;
else
return S[X] = Find(S, S[X]);
// 先找到根;把根变成X的父结点;再返回根
}