《大话数据结构》树以及赫夫曼编码的例子

简介: 第六章 树 6.2 树的定义 树(Tree)的n个结点的有限集。当n=0时,称为空树。 任意一个非空树中: 1)有且仅有一个特定的称为根(root)的结点 2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2 …… 、Tm。

第六章 树

6.2 树的定义

树(Tree)的n个结点的有限集。当n=0时,称为空树。

任意一个非空树中:

1)有且仅有一个特定的称为根(root)的结点

2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2 …… 、Tm。其中每个集合本身又是一棵树,并且称为根的子树(SubTree)。

 

注意:

1)n>0时根节点的唯一的,不可能存在多个根节点。

2)m>0时,字数的个数没有限制,但是它们一定是互不相交的。

 

6.2.1 结点分类

结点拥有的子树个数称为结点的度(Degree)。

度为0的结点称为叶子结点或终端结点。度不为0的结点称为非终端结点或分支结点。

除根结点之外,分支结点也称为内部结点。

树的度是树内各结点的度的最大值。

 

上面的结点中D的度最大为3,所以树的度也是3

 

6.2.2 结点间关系

结点的子树的根称为该结点的孩子。该结点称为孩子的双亲(parent)。

同一个双亲之间的孩子互称为兄弟(sibling)。

结点的祖先的从根到该结点所经分支上的所有的结点。

以某结点为根的子树中的任一结点都称为该结点的子孙。

 

6.2.3 树的其他相关概念

结点的层次从根开始定义起,根为第一层,根的孩子为第二层。

树中结点的最大层次称为树的深度(depth)或高度。

如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树。否则称为无序树。

森林(Forest)是m(m>0)棵互不相交的树的集合。对树中的每个结点而言,其子树的集合即为森林。

 

6.4 树的存储结构

有三种不同的表示方法:双亲表示法、孩子表示法、孩子兄弟表示法

6.4.1 双亲表示法

在每一个结点中附设一个指示器指示其双亲结点到链表中的位置。

还可以增加一个长子域。表示最左边的儿子。

 

6.4.2 孩子表示法

每个结点有多个指针域,其中每个指针指向一颗子树的根节点,我们把这种方法叫做多重链表表示法

有两种方法:

1)每个结点都有自己的数据域,和指向子树的指针域。假如这个结点的度为3,那么就会有三个指针域和一个数据域。

当结点数目不确定的时候,这种方法就不好办了。会浪费空间,因为需要每个结点都有最大的度的结点。这样会有很多空的指针域

2)一个数据域,一个度,外加几个指针域(数目由度决定)。这样不会浪费空间,但是运算会复杂一点,会带来时间上的损耗。

孩子表示法:把每个结点的孩子结点排列起来,以单链表作为存储结构,则n个结点就有n个孩子链表。如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中。

每个结点有一个数据域,还有一个链表头。指向子节点。

6.4.3 孩子兄弟表示法

任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此我们设置两个指针,分别指向该结点的第一个孩子和次结点的右兄弟。

 

6.5 二叉树的定义

二叉树(Binary Tree)是n(n>=0)个结点的有限集合。该集合或者为空集(称为空二叉树),或者由一个根节点和两个互不相交的,分别称为根节点的左子树和右子树组成。

 

6.5.1 二叉树的特点

1)每个结点最多有两颗子树,所以不存在度大于2的结点。

2)左子树和右子树是有顺序的,次序不能颠倒。

3)即使某结点只有一颗子树,也要区分他是左子树还是右子树。这是不同的。

 

6.5.2 特殊二叉树

1.斜树:所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。

2.满二叉树:如果所有分支结点都存在左子树和右子树,并且所有的叶子都在同一层上。就称为满二叉树。

3.完全二叉树:对一棵具有n个结点的二叉树按层序编号,如果编号为i的结点与同样深度的满二叉树中编号为i的结点在二叉树中的位置完全相同。则这棵二叉树称为完全二叉树。

完全二叉树不一定是满二叉树。但是满二叉树一定是完全二叉树。

特点:

1)叶子节点只能出现在最下两层

2)最下层的叶子一定几种在左部连续位置

3)倒数第二层,若存在叶子结点,一定在右部连续位置。

4)结点度为1,则该结点只有左孩子

5)同样结点树的二叉树,完全二叉树深度最小。

注意,一定要按层序编号,开始数,出现了空挡就不是完全二叉树了。

 

6.6 二叉树的性质

1:在二叉树的第i层上最多有2i-1个结点(i>=1)

2:深度为k的二叉树至多有2k-1个结点(k>=1)

3:任何一颗二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。

(终端结点数就是叶子结点)

4:具有n个结点的完全二叉树的深度为log2n + 1。(满二叉树深度为k,则结点数n=2k - 1)

 

6.7 二叉树的存储结构

6.7.1 二叉树顺序存储结构

放到数组中。但是这样会浪费资源

 

6.7.2 二叉链表

每个结点最多有两个孩子。所以设计一个数据域和两个指针域

data leftchild rightchild  后两个是指针域

typedef struct BiTNode

{

         int data;

         struct BiTNode *lchild, *rchild;

}BiTNode, *BiTree;

 

6.8 遍历二叉树

6.8.1 二叉树遍历原理

二叉树的遍历是指从根节点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。

6.8.2 二叉树遍历方法

1.前序遍历

规则是若二叉树为空,则立即返回。否则先遍历根结点,然后前序遍历左子树。再前序遍历右子树。

2.中序遍历:

先遍历左子树,再根节点,最后右子树。

3.后序遍历:

先叶子结点后结点的方式遍历左子树,再是右子树,最后是根结点

 

6.8.3 前序遍历算法

采用递归

void PreOrder(BiTree *T)

{

         if(T == NULL)

                   return;

         printf(“%d”, T->data);

         PreOrder(T->lchild);

         PreOrder(T->rchild);

}

 

6.8.4 中序遍历法

void MidOrder(BiTree *T)

{

         if(T == NULL)

                   return;    

         MidOrder (T->lchild);

printf(“%d”, T->data);

         MidOrder (T->rchild);

}

 

6.8.5 后序遍历法

void PostOrder(BiTree *T)

{

         if(T == NULL)

                   return;    

         PostOrder (T->lchild);

         PostOrder (T->rchild);

printf(“%d”, T->data);

}

一个搜索二叉树的例子:http://www.cnblogs.com/xcywt/p/8289765.html

 

6.11 树、森林与二叉树的转换

6.11.1 树转换成二叉树

分三个步骤:

1)将所有兄弟连起来,(注意不要连堂兄弟)

2)每个结点都保留与第一个孩子的线,与其他孩子的全部断掉。(注意与兄弟结点的关系保留)

3)以根节点为轴顺时针旋转。注意每个结点的第一个孩子将变成左子树。兄弟变成右子树。

如图:

 

 

 

6.11.2 森林转换成二叉树

步骤如下:

1)将每个树转换成二叉树

2)第一棵树不动,从第二个树开始,依次把后一棵二叉树的根节点作为前一个二叉树的根节点的右孩子(树转成二叉树,根结点是没有右子树的)。当所有二叉树连接起来的时候就得到了一个森林。

3)将得到的森林转换成二叉树。

 

6.11.3 二叉树转换成树(这棵树一定根结点没有右子树)

也就是树转换成二叉树的逆过程:

1)加线:如果存在左子树。就将这个左孩子的右结点,右孩子的右孩子结点。。。。。。。。反正就是左孩子的n个右孩子结点都作为次结点的孩子。将该结点与这些右孩子结点连接起来

2)去线:删除之前二叉树中所有结点与右孩子结点的连线

3)层次调整,使之结构分明。

(如下图:假如D还有右孩子H,那么A还要与H连接起来,再去掉D到H的线)

 

 

6.11.4 二叉树转成森林

如果这棵二叉树根结点有右子树,那么就只能转换成森林。

步骤:

1)将右子树断开。再查看分离的二叉树,根结点还有右子树就接着断开。直到所有的右孩子连线都删除为止。得到分离的二叉树。

2)再将分离的二叉树转成树。

 

6.11.5 树与森林的遍历

1.树遍历分为两种方式:

1)先根遍历先访问树的根结点,然后依次遍历根的每颗子树。(比如上图6-11-4的树,采用这种方式遍历就是ABEFCDG)

2)后根遍历先依次后根遍历每棵子树,再访问根结点。(6-11-4用这种方法就是:EFBCGDA)

 

2.森林的遍历也分两种:

1)前序遍历:先访问森林中第一个树的根结点,再依次先根遍历根的每棵子树(也就是先根遍历)。最后用同样的方式去遍历剩余的树。

2)后序遍历:先访问第一个树,用后根遍历的方式遍历。接着把其他的树遍历一下。

 

总结:

1)森林的前序遍历和二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结果相同。

2)当以二叉链表(每个结点设计成一个数据域和两个指针域(left,right,我觉得还可以加上parent,把这样的链表叫做二叉链表))作为树的存储结构时,

树的先根遍历和后根遍历完全可以借用二叉树的前序遍历和中序遍历的算法来实现。我们就找到了树和森林这种复杂问题的简单解决办法。

 

6.12 赫夫曼树及其应用

6.12. 2赫夫曼树定义与原理

百度百科上说:给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度达到最小,这样的二叉树称为最优二叉树(也叫赫夫曼树huffman tree)。

哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

 

接下来是书上的内容:

赫夫曼大叔说:从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目叫路径长度。

树的路径长度就是从根到每一个结点的路径长度之和。

考虑到带权的结点,结点的带权的路径长度为从该结点的路径长度与结点上权的乘积。

 

定义:假设有n个权值(w1,w2,w3,….wn),构造一棵有n个叶子结点的二叉树,每个叶子结点带权wk,每个叶子的路径长度为lk,我们通常记作,则其中带权路径长度WPL最小的二叉树称作赫夫曼树。

如何构造赫夫曼树:

1)根据给定的n个权值{w1, w2, w3 … wn}构成n棵二叉树的集合F={T1, T2, … Tn}。其中每棵二叉树Ti中只有一个带权为wi根节点,其左右子树均为空。

2)在F中选取两棵根节点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根节点的权值为其左右子树上跟结点的权值之和。

3)在F中删除这两棵树,同时将新得到的二叉树加入F中。

4)重复2和3,直到F只包含一棵树为止,这棵树就是赫夫曼树。

 

6.12.3 赫夫曼编码

假设我们要给别人发送BADCADFEED,全部展开成二进制,是3*10共30位。

但其实出现的是有概率的,比如E出现的多,F出现的少,我们就可以假定6个字母的频率为A27 B8 C15 D15 E30 F5.

再把它们转换成赫夫曼树,然后将权值左分支改为0,右分支改为1,得到下图:

  

得到一个新的编码对应关系:

 

再重新编码就可以看到短了很多。

 

那么解码过程呢:

要设计长短不等的编码,则必须是任意字符都不是另一个字符的前缀。这种编码称为前缀编码。

比如不能存在这样的编码,10,100,1001。

解码时还要用到赫夫曼树,接收方和发送方必须要约定好同样的赫夫曼编码规则。

下面是和赫夫曼编码的例子: 

 

/*
作者:xcywt
时间:20180130
参考:http://blog.csdn.net/wtfmonking/article/details/17150499#
说明:实现的一个赫夫曼树以及编码解码的过程。
*/

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#ifdef WIN32
#define __func__ __FUNCTION__
#endif

#define CODELEN 10  // 这里假定解码最长只有10
typedef int ElemType;
typedef struct _BTreeNode
{
    ElemType data; // data表示权值,
    struct _BTreeNode *left;
    struct _BTreeNode * right;
}BTreeNode;

// 打印树,先根遍历
void Print_BTree(BTreeNode *BT)
{
    if(BT)
    {
        printf("%d", BT->data);
        if(BT->left || BT->right)
        {
            printf("(");
            Print_BTree(BT->left);
            if(BT->right)
                printf(",");
            Print_BTree(BT->right);
            printf(")");
        }    
    }
}

// 根据输入权值创建赫夫曼树。返回值就是赫夫曼树指针
BTreeNode * CreateHuffman(ElemType *a, int n)
{
    {
#if 1
        printf("CreateHuffman() input: ");
        int i = 0;
        for( i = 0; i < n; i++)
        {
            printf("%d ", a[i]);
        }
        printf("\n");
#endif
    }

    int i, j, initcode;
    BTreeNode **b, *btRoot;
    b = (BTreeNode**)malloc(n*sizeof(BTreeNode));
    if(!b)
    {
        printf("%s() --- malloc error step1\n", __func__);
        return NULL;
    }

    // 初始化b指针数组,使每个指针元素指向a数组中对应的元素结点
    for(i = 0; i< n; i++)
    {
        b[i] = (BTreeNode*)malloc(sizeof(BTreeNode));
        if(b[i])
        {
            b[i]->data = a[i];
            b[i]->left = b[i]->right = NULL;
        }
    }

    // 进行n-1次循环建立哈夫曼树。循环的过程中b数组里面的数据会变,可能会为null。
    for(i = 1; i < n; i++)
    {
        // k1:森林中具有最小权值的树根结点的下标,  k2:次最小的下标
        int k1 = -1, k2 = 0;

        // 让k1初始指向森林中的第一棵树,k2指向第2棵
        for(j = 0; j < n; j++) 
        {
            if(b[j] != NULL && k1 == -1)
            {
                k1 = j;
                continue;
            }
            if(b[j])
            {
                k2 = j;
                break;
            }
        
        }

        // 从当前森林中求出最小权值树和次最小
        for( j = k2; j < n; j++)
        {
            if(b[j])
            {
                if(b[j]->data < b[k1]->data)
                {
                    k2 = k1;
                    k1 = j;
                }
                else if(b[j]->data < b[k2]->data)
                    k2 = j;
            }
        }

        // 由最小权值树和次最小权值树建立一棵新树,btRoot指向树根结点
        btRoot = (BTreeNode*)malloc(sizeof(BTreeNode));
        if(!btRoot)
        {
            printf("%s() --- malloc error step2\n", __func__);
            return NULL;
        }

        btRoot->data = b[k1]->data + b[k2]->data;
        btRoot->left = b[k1];
        btRoot->right = b[k2];    

        // 将指向新树的指针赋给b指针数组中的k1位置
        b[k1] = btRoot;
        b[k2] = NULL; // k2的位置为空,这步非常重要。
    }

    free(b);
    b = NULL;
    return btRoot; // 返回整个哈夫曼树根指针
}

// 求赫夫曼树带权路径
ElemType GetRootPathLength(BTreeNode *FBT, int len) // len 初始为0
{
    if(!FBT)
        return 0;
    else
    {
        if(!FBT->left && !FBT->right) // 这里已经到了叶子结点,带权路径 = 权 * len
            return FBT->data * len;
        else // 返回左子树带权路径 + 右子树带权路径
            return GetRootPathLength(FBT->left, len+1) + GetRootPathLength(FBT->right, len+1);
    }
}


// 赫夫曼编码
void HuffmanCode(BTreeNode *FBT, int len) // len表示到结点的路径
{
    // 用来存放编码的数组,必须要大于树的深度-1.
    static int arr[10] = {0};
    if(FBT)
    {
#if 1
        if(!FBT->left && !FBT->right) // 到了叶子结点
        {
            int i = 0;
            printf("赫夫曼编码,权值为[%d]的编码为:[", FBT->data);
            for(i = 0; i < len; i++)
                printf("%d", arr[i]);
            printf("]\n");
        }
        else
        {
            // 左边结点为0, len要加1
            arr[len] = 0;
            HuffmanCode(FBT->left, len+1);

            // 右边结点为1, len要加1
            arr[len] = 1;
            HuffmanCode(FBT->right, len+1);
        }
#endif
    }
}

// 赫夫曼解码
int HuffmanDeCode(BTreeNode *FBT, char *strcode)
{
    if(!strcode && !FBT)
        return -1;
    int arr[CODELEN] = {0};
    int i = 0, codelen = strlen(strcode);
    if(codelen > CODELEN)
    {
        printf("输入太长了,解码失败\n");
        return -1;
    }
    
    for(i = 0; i < codelen; i++)
    {
        //arr[i] = (strcode[i] == '0')?0:1;
        if(strcode[i] == '0')
            arr[i] = 0;
        else if(strcode[i] == '1')
            arr[i] = 1;
        else
        {
            printf("输入有非法字符,解码失败\n");
            return -1;
        }
    }
    
    printf("您要解码的编码是:[");
    for(i = 0; i < codelen; i++)
    {
        printf("%d",arr[i]);
    }
    printf("]\n");
    
    BTreeNode *pNode = FBT;
    bool bdecodesuccess = false;
    for(i = 0; i < codelen; i++)
    {
        if(arr[i] == 0)
        {
            pNode = pNode->left;
        }
        else if(arr[i] == 1)
        {
            pNode = pNode->right;
        }

        if(!pNode->left && !pNode->right)
        {
        //    printf("Get code:%d\n", pNode->data);
            if(i == codelen - 1) // 不等于表示输入过长,就失败了。
            {
                bdecodesuccess = true;
            }
            break;
        }
    }

    if(bdecodesuccess)
    {
        printf("解码为:%d\n", pNode->data);
    }
    else
    {
        printf("解码失败\n");
    }

    return 0;
}

int fun()
{
    printf("%s() +++\n", __func__);
    int n = 6, i;
    ElemType *a;
    BTreeNode *fbt;
#if 0
    printf("请输入待构造的赫夫曼树带权叶子结点数:");
    while(1)
    {
        scanf("%d", &n);
        if(n > 1)
            break;
        else
            printf("输入有误,请重试:");
    }

    a = (ElemType*)malloc(n*sizeof(ElemType));
    if(!a)
    {
        printf("malloc error\n");
        return -1;
    }

    printf("请输入%d个整数作为权值:", n);
    for(i = 0; i < n; i++)
        scanf(" %d", &a[i]);
#else
    a = (ElemType*)malloc(n*sizeof(ElemType));
    if(!a)
    {
        printf("malloc error\n");
        return -1;
    }

    a[0] = 3;
    a[1] = 9;
    a[2] = 5;
    a[3] = 12;
    a[4] = 6;
    a[5] = 15;
#endif


    fbt = CreateHuffman(a, n);
    if(!fbt)
    {
        printf("创建赫夫曼树失败\n");
        return -1;
    }
    printf("创建赫夫曼树成功:");
    Print_BTree(fbt);
    printf("\n");

    printf("带权路径为:%d\n", GetRootPathLength(fbt, 0));
    HuffmanCode(fbt, 0);

    HuffmanDeCode(fbt, "00");
    HuffmanDeCode(fbt, "01");
    HuffmanDeCode(fbt, "100");
    HuffmanDeCode(fbt, "1010");
    HuffmanDeCode(fbt, "1011");
    HuffmanDeCode(fbt, "11");
    
    // 下面为出错测试
    HuffmanDeCode(fbt, "");
    HuffmanDeCode(fbt, "1");
    HuffmanDeCode(fbt, "101");
    HuffmanDeCode(fbt, "101101");
    HuffmanDeCode(fbt, "101sdd01");
    HuffmanDeCode(fbt, "101301");
    HuffmanDeCode(fbt, "654");
    HuffmanDeCode(fbt, "1011011011");
    HuffmanDeCode(fbt, "10110110111");

    printf("%s() ---\n", __func__);
    return 0;
}


int main()
{
    fun();
    return 0;
}
View Code

 

目录
相关文章
|
1月前
|
存储 算法 搜索推荐
探索常见数据结构:数组、链表、栈、队列、树和图
探索常见数据结构:数组、链表、栈、队列、树和图
86 64
|
4天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
43 16
|
27天前
|
存储 算法 关系型数据库
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
这篇文章主要介绍了多路查找树的基本概念,包括二叉树的局限性、多叉树的优化、B树及其变体(如2-3树、B+树、B*树)的特点和应用,旨在帮助读者理解这些数据结构在文件系统和数据库系统中的重要性和效率。
16 0
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
|
1月前
|
存储 编译器 C++
【初阶数据结构】掌握二叉树遍历技巧与信息求解:深入解析四种遍历方法及树的结构与统计分析
【初阶数据结构】掌握二叉树遍历技巧与信息求解:深入解析四种遍历方法及树的结构与统计分析
|
1月前
【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解(三)
【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解
|
1月前
【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解(二)
【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解
|
1月前
|
存储
【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解(一)
【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解
|
24天前
|
Java C++
【数据结构】探索红黑树的奥秘:自平衡原理图解及与二叉查找树的比较
本文深入解析红黑树的自平衡原理,介绍其五大原则,并通过图解和代码示例展示其内部机制。同时,对比红黑树与二叉查找树的性能差异,帮助读者更好地理解这两种数据结构的特点和应用场景。
23 0
|
2月前
|
JSON 前端开发 JavaScript
一文了解树在前端中的应用,掌握数据结构中树的生命线
该文章详细介绍了树这一数据结构在前端开发中的应用,包括树的基本概念、遍历方法(如深度优先遍历、广度优先遍历)以及二叉树的先序、中序、后序遍历,并通过实例代码展示了如何在JavaScript中实现这些遍历算法。此外,文章还探讨了树结构在处理JSON数据时的应用场景。
一文了解树在前端中的应用,掌握数据结构中树的生命线