假期连载之十五

(树、图补完篇)

 

    本篇的主要目的是对树和图部分内容中的代码进行分析,涉及构造哈夫曼树、图的几种主流应用算法等问题。本篇结束后假期系列的基本数据结构部分就暂告一段落了。

   

构建哈夫曼树

 

    现在讨论构建哈夫曼树的算法实现问题。在前文已经对构造哈夫曼树的算法思路有了一定的介绍,易知哈夫曼树中没有度为1的结点,因而一棵有n个叶子的哈夫曼树共有2n-1个结点,可以用一个大小为2n-1的一维数组存放哈夫曼树的各个结点。由于每个结点同时还包含其双亲信息和孩子结点的信息,所以构成一个静态三叉链表。其结点结构分别是权值域、双亲序号、左子女序号、右子女序号。各结点存储在一维数组中,0号单元不用,从1号位置开始使用。

    对于有n个叶子结点的哈夫曼树,结点总数为2n-1个,为实现方便,将叶子结点集中存储在前面部分1-n个位置,而后面的n-1个位置中存储其余非叶子结点。

    我们给出静态三叉链表实现的哈夫曼树类型C语言描述:

    #define N 20

    #define M 2*N-1

    typedef struct

{

    int weight;

    int parent;

    int LChild;

    int RChild;

}HTNode,HuffmanTree[M+1];

使用上述结构实现创建哈夫曼树算法:

void CrtHuffmanTree(HuffmanTree ht,int w[],int n)

{

    for(i=1;i<=n;i++)

    {

        ht[i]={w[i],0,0,0}  //初始化1n单元的叶结点

}

m=2*n-1;    //统计结点总数

for(i=n+1;i<=m;i++) //n+1m个结点为非叶结点

{

    select(ht,i-1,&s1,&s2); //ht中选择两个parent0weight最小的结点,将其序号分别赋值给s1s2返回

    ht[i].weight=ht[s1].weight+ht[s2].weight;   //计算每次结合后根结点的权值

    ht[s1].parent=i;    //s1s2的父结点指定为第i个结点

    ht[s2].parent=i;

    ht[i].LChild=s1;    //对第i个结点的左右子女结点赋值

    ht[i].RChild=s2;

}

}

 

计算哈夫曼编码

 

    我们知道,构建哈夫曼树的最终目的是求解哈夫曼编码,从而方便信息传输和数据压缩。因此基于哈夫曼树的哈夫曼编码计算算法就显得尤为重要了。

    void CrtHuffmanCode(HuffmanTree ht,HuffmanCode hc,int n)

    {

        char *cd;

        cd=(char*)malloc((n+1)*sizeof(char));   //初始化空间

        cd[n-1]=”\0”;                         //首先存放结束符

        for(int i=1;i<=n;i++)

        {

            start=n-1;          //编码起始位置

            c=i;               

            p=ht[i].parent;     //从叶结点开始倒推

            while(p!=0)

            {

                –start;        //倒推一层,判断左01,再推一层,直到根结点

                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);

}

 

深度优先的简单路径算法

 

    在图这一部分中,简单路径是指任意两结点路径中不含相同结点的一条。为求解图的简单路径,需要先对图中所有结点进行统一标识,每访问一个结点,则更改该结点标识符,从而达到求简单路径的目的。

    算法外围的核心是给所有结点统一赋值,并且建立对应于结点且独立的带值空间。下面给出用深度优先搜索寻找从uv的简单路径算法C语言描述。

    void DFS_path(Graph *G,int u,int v)

{

    int j;

    for(j=firstadj(G,u);j>=0;j=nextadj(G,u,j))

    {

        if(pre[j]==-1)      //默认结点标识为-1,一旦使用过即将标识符置为u

{

            pre[j]=u;

            if(j==v)

            {

                print_path(pre,v);

}

else

{

        DFS_path(G,j,v);

}

}

}

}

 

生成树

 

生成树涉及到很多实际应用,例如搭桥问题但利用MST性质求MSTMinimum Spanning Tree)还分别有Primkruskal两种角度不同的算法。

Prim算法通常被称为“加点法”,这是因为算法是在一个初始化为空的结点空间中,判断能令(u,v)最短的u对应的v结点,然后加入到空的结点空间中,反复执行直到该结点空间与图的结点空间相等。

Kruskal算法是根据权值最小的边,同时必须确保两个顶点不在同一个结点集合内,故有时Kruskal算法也被称为“加边法”。事实上以上两种算法除却初始化的部分,真正涉及到原理的代码段就已经非常简单了。

 

总结

 

对于拓扑排序、关键路径、最短路径等问题,读者应重点参照“图的应用”这一节连载,其原理已经比较详细,但由于经典算法时间复杂度高,已经有越来越多的专家学者对此提出了许多改进方案,并给出了许多算法解决方案,因此在此基础上参照相关科技文献,才能确保算法的实用性。