【树】基本概念及简单代码学习

【树】(上)

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. 斜二叉树:线性结构,是一种链表。

  1. 完美/满二叉树:完美二叉树,一个不少。

  1. 完全二叉树:在完美二叉树的基础上少几片叶,同时序号要连贯

二叉树的几个重要性质

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二叉树的存储结构

  1. 顺序存储结构

完全二叉树用数组实现非常方便。

想要遍历,我们可以发现一些规律,使得数组实现非常容易;

非根结点(序号 i > 1)的父结点的序号是 [i / 2];

结点 (序号为 i ) 的左孩子结点的序号是 2i, 其中2 i <= n,否则没有左孩子;

结点 (序号为 i ) 的右孩子结点的序号是 2i+1,其中2 i +1<= n,否则没有右孩子;

同时,一般的二叉树也可以采用这种方式,但会带来空间的浪费。

一般的二叉树补成一个完全二叉树,同时相应缺的节点要在数组里留下空位

  1. 链表存储

typedef struct TreeNode *BinTree;
typedef BinTree Position;
struct TreeNode{
ElementType Data;
BinTree Left;
BinTree Right;
}

struct

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];
程序框架搭建
  1. 如何建二叉树

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;
}
  1. 如何判别两二叉树同构

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; 
  1. 查找某个元素所在集合(用根节点表示)

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;
}
  1. 并运算

分别找到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函数

  1. 按高度

S[root]=-树的高度,根相同的时候高度才变。

  1. 按规模

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的父结点;再返回根
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值