树形结构是一种非线性结构。

树形结构是结点之间有分支,并具有层次关系的结构。它非常类似于自然界中的树。

树结构在客观世界中是大量存在的,例如家谱、行政组织机构都可用树形象地表示。

树在计算机领域中也有着广泛的应用,例如在编译程序中,用树来表示源程序的语法结构;在数据库系统中,可用树来组织信息;在分析算法的行为时,可用树来描述其执行过程。

1. 树的实例

家族树:在现实生活中,有入如下血统关系的家族可用树形图表示:

tree.jpg

2. 树的定义

树是由n (n≥0)个结点构成的有限集合;n=0的树称为空树;当n≠0时,树中的结点应该满足以下两个条件:

3. 树的图示

  1. 倒置的树形图表示
    tree2.jpg

  2. 凹入表示
    indenttree.jpg

  3. 嵌套集合表示(文氏图表示)

treeset.jpg

  1. 嵌套括号表示(广义表表示) (A(B(E,F),C,D(G,H(J,K),I)))

4. 树的相关术语

5. 树形结构的特点

6. 树的基本操作

二叉树

1. 二叉树的定义

二叉树(Binary Tree)是n(n≥0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两棵互不相交的、分别称作这个根的左子树和右子树的二叉树组成。

二叉树与度为2的有序树不同:在有序树中,虽然一个结点的孩子之间是有左右次序的,但是若该结点只有一个孩子,就无须区分其左右次序。而在二叉树中,即使是一个孩子也有左右之分。

2. 二叉树的相关术语

3. 二叉树五种基本形态

binarytree.jpg

二叉树可以是空集;根可以有空的左子树或右子树;或者左、右子树皆为空。

4. 二叉树的特殊形态

4.1. 满二叉树

一棵深度为k且有2k-1个结点的二又树称为满二叉树(Full Binary Tree) 。

满二叉树的特点:

4.2. 完全二叉树

若一棵二叉树至多只有最下面的两层上结点的度数可以小于2,并且最下一层上的结点都集中在该层最左边的若干位置上,则此二叉树称为完全二叉树(Complete Binary Tree)。

completetree.jpg

完全二叉树的特点:

结点的编号:在一棵n个结点的完全二叉树中,从树根起,自上层到下层,每层从左至右,给所有结点编号,能得到一个反映整个二叉树结构的线性序列。

从一个结点的编号就可推得其双亲,左、右孩子,兄弟等结点的编号。假设编号为i的结点是ki(1≤i≤n),则有:

5. 二叉树的重要性质

5.1. 性质1

性质1 二叉树第i层上的结点数目最多为2i-1(i≥1)。

证明:使用数学归纳法证明。

那么,因为二叉树中每个节点的度最大为2,则第k+1层的结点总数最多为第k层上结点最大数的两倍,即2 * 2i-1=2k,故结论成立。证毕。

5.2. 性质2

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

证明:最大总结点数等于每层最大结点数之和 = 1+2+4+...+2k-1 = 2k-1

5.3. 性质3

性质3 在任意-棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1。

证明:设度为1的结点数为n1。则二叉树的总边数等于n0+n1+n2-1(除了根结点外,其余结点都有一条边连接其父结点),也等于n0*0+n1*1+n2*2(每个n1结点有一条边连接子结点,每个n2结点有2条边连接2个子结点)。因此n0+n1+n2-1 = n1+2n2,即n0=n2+1。

5.4. 性质4

性质4 具有n个结点的完全二叉树的深度为 floor(log2n)+1。其中,floor为舍去小数取整。

证明:假设层数为k,则k层的完全二叉树至多有2k-1个结点,至少有2k-1-1 +1(比k-1层的满二叉树多一个结点)个结点。因此2k-1-1+1≤n≤2k-1,即2k-1≤n<2k,两边取对数k-1≤log2n<k(log2n为介于[k-1,k)之间的一个整数),因此k=floor(log2n)+1。

6. 二叉树的存储结构

6.1. 顺序存储

完全二叉树的顺序存储

将完全二叉树中所有结点按编号顺序依次存储在一个数组bt[0..n]中。其中:

completetree.jpg

treearray.jpg

非完全二叉树的顺序存储

将一般二叉树添上一些 "虚结点",成为"完全二叉树"

noncompletetree.jpg

treearray2.jpg

顺序存储的特点:

思考:最坏情况下顺序存储的存储密度是多少?答案:最坏的情况下,一个深度为k且只有k个结点的右单支树需要2k-1个结点的存储空间,存储密度为k/2k-1

顺序存储结构的定义

   1 #define MAXSIZE 100
   2 typedef char ElemType;
   3 typedef char BOOL;
   4 struct sequence_bin_tree {
   5    ElemType elem[MAXSIZE+1]; /* 元素的值 */
   6    BOOL used[MAXSIZE+1]; /* 对应完全二叉树的编号上是否有元素 */
   7    int size; /*最大结点编号*/
   8 }
   9 typedef struct sequence_bin_tree sequence_bin_tree; 

6.2. 链式存储

二叉链表:二叉树的每个结点最多有两个孩子,可以用链接方式存储二叉树。每个结点除了存储结点本身的数据外,还应设置两个指针域lchild和rchild,分别指向该结点的左孩子和右孩子。结点bintree_node的结构为:

bintreenode.jpg

linkedbintree.jpg

一棵二叉树中,所有类型为bintree_node的结点,再加上一个指向开始结点(即根结点)的bintree_node型指针root,就构成了二叉树的链式存储结构,并将其称为二叉链表。

   1 typedef char ElemType;
   2 struct bintree_node{
   3    ElemType data; /*元素的值*/
   4    struct bintree_node *lchild; /*左孩子结点的地址*/
   5    struct bintree_node *rchild; /*右孩子结点的地址*/
   6 };
   7 typedef struct bintree_node bintree_node;
   8 typedef struct bintree_node *bintree; 

二叉链表的特点:

带父节点指针的链式表示:经常要在二叉树中寻找某结点的父节点时,可在每个结点上再加一个指向其父节点的指针parent,形成一个带双亲指针的二叉链表,称作三叉链表。

link3tree.jpg

   1 typedef char ElemType;
   2 struct tritree_node{
   3    ElemType data; /*元素的值*/
   4    struct tritree_node *lchild; /*左孩子结点的地址*/
   5    struct tritree_node *rchild; /*右孩子结点的地址*/
   6    struct tritree_node *parent; /*父结点的地址*/
   7 };
   8 typedef struct tritree_node tritree_node;
   9 typedef struct tritree_node *tritree; 

7. 二叉树的操作

此处所列操作均以二叉链表为例

7.1. 遍历

遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。

访问结点所做的操作依赖于具体的应用问题,比如打印节点的数据,或者修改节点的数据等。

遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算之基础。

三种遍历的顺序:从二叉树的递归定义可知,一棵非空的二叉树由根结点及左、右子树这三个基本部分组成。因此,在任一给定结点上,可以按某种次序执行三个操作:

以上三种操作有六种执行次序:

VLR、LVR、LRV、VRL、RVL、RLV

前三种次序与后三种次序对称,故只讨论先左后右的前三种次序:

例:如图 linkedbintree.jpg

遍历次序的另一种理解

traverse.jpg

给定一棵二叉树的前序遍历序列和中序遍历序列,能够唯一确定一棵二叉树。

例:前序遍历序列为:ABDFGCEH,中序遍历序列为:FDGBACHE,则可以唯一确定这棵二叉树为: bintree3.jpg

同样给定一棵二叉树的中序遍历序列和后序遍历序列,也能够唯一确定一棵二叉树。

递归实现

   1 /*前序遍历*/
   2 void preorder(bintree t) {
   3    if (t)  {
   4       printf("%c", t->data); /*假定元素类型为char,访问操作为打印元素的值*/ 
   5       preorder(t->lchild);   /*递归的遍历左子树*/
   6       preorder(t->rchild);   /*递归的遍历右子树*/
   7    }
   8 } 
   9 
  10 /*中序遍历*/
  11 void inorder(bintree t) {
  12    if (t)  {
  13       inorder(t->lchild);    /*递归的遍历左子树*/
  14       printf("%c", t->data); /*假定元素类型为char,访问操作为打印元素的值*/ 
  15       inorder(t->rchild);    /*递归的遍历右子树*/
  16    }
  17 } 
  18 
  19 /*后序遍历*/
  20 void postorder(bintree t) {
  21    if (t)  {
  22       postorder(t->lchild);  /*递归的遍历左子树*/
  23       postorder(t->rchild);  /*递归的遍历右子树*/
  24       printf("%c", t->data); /*假定元素类型为char,访问操作为打印元素的值*/ 
  25    }
  26 }

采用函数指针实现可以将访问与遍历分开

   1 /*前序遍历,visit参数指出了访问所作的操作是什么*/
   2 void preorder(bintree t, void (*visit)(ElemType)) {
   3    if (t)  {
   4       visit(t->data);
   5       preorder(t->lchild, visit); 
   6       preorder(t->rchild, visit);
   7    }
   8 } 
   9 
  10 /*中序遍历*/
  11 void inorder(bintree t, void (*visit)(ElemType)) {
  12    if (t)  {
  13       inorder(t->lchild, visit); 
  14       visit(t->data);
  15       inorder(t->rchild, visit);
  16    }
  17 } 
  18 
  19 /*后序遍历*/
  20 void postorder(bintree t, void (*visit)(ElemType)) {
  21    if (t)  {
  22       postorder(t->lchild, visit); 
  23       postorder(t->rchild, visit);
  24       visit(t->data); 
  25    }
  26 }

遍历的非递归实现

非递归实现需要借助栈的帮助

   1 /*先序非递归遍历*/
   2 void preorder(bintree t) {
   3    seq_stack st;
   4    init_stack(&st);
   5    while(t != NULL) {
   6       while(t != NULL) {
   7          printf("%c", t->data);
   8          if(t->rchild != NULL)
   9              push(&st, t->rchild);
  10          t = t->lchild;
  11       }
  12       if(!empty(&st))
  13          t = pop(&st);
  14    }
  15 }
  16 
  17 /*中序非递归遍历*/
  18 void inorder(bintree t) {
  19    seq_stack st;
  20    init_stack(&st);
  21    while(t != NULL || !empty(&st)) {
  22       while(t != NULL) {
  23          push(&st, t);
  24          t = t->lchild;
  25       }
  26       if(!empty(&st)) {
  27          t = pop(&st);
  28          printf("%c", t->data);
  29          t = t->rchild;
  30       }
  31    }
  32 }
  33 
  34 /*后序非递归遍历*/
  35 void postorder(bintree t) {
  36    seq_stack st;
  37    init_stack(&st);
  38    bintree_node *last = NULL;
  39    while(t != NULL || !empty(&st)) {
  40       while(t != NULL) {
  41          push(&st, t);
  42          t = t->lchild;
  43       }
  44       if(!empty(&st)) {
  45          t = get_top(&st);
  46          if(t->rchild == NULL || t->rchild == last) {
  47             printf("%c", t->data);
  48             last = t;
  49             pop(&st);
  50             t = NULL;
  51          } else {
  52             t = t->rchild;
  53          }
  54       }
  55    }
  56 }

7.2. 创建

基于先序遍历的构造,即以二叉树的先序序列为输入构造。

先序序列中必须加入虚结点以示空指针的位置。

建立下图所示二叉树,其输入的先序序列是:ABD ### CE## F##。

traverse.jpg

假设结点的元素类型为char,空二叉树以#字符表示,相应的构造算法为:

   1 bintree create_bintree() {
   2    bintree T;
   3    char ch = getchar();
   4    if (ch == '#') /*创建空二叉树*/ 
   5       T = NULL;  /*读入空格,将相应指针置空 */
   6    else { /* 非空二叉树 */
   7       T=(bintree_node *)malloc(sizeof(bintree_node)); /*生成结点*/
   8       T->data = ch;
   9       T->lchild = create_bintree(); /*构造左子树*/
  10       T->rchild = create_bintree(); /*构造右子树*/
  11    }
  12    return T;
  13 } 

   1 bintree create(ElemType data, bintree left, bintree right) {
   2    bintree T = (bintree_node *)malloc(sizeof(bintree_node));
   3    T->data = data;
   4    T->lchild = left;
   5    T->rchild = right;
   6    return T;
   7 }

7.3. 输出叶子结点

输出二叉树中所有叶子结点的值

   1 void traverse_leaf(bintree t) {
   2    if (t)  {
   3       if(t->lchild == NULL && t->rchild == NULL)
   4          printf("%c", t->data); /*假定元素类型为char*/ 
   5       preorder(t->lchild); 
   6       preorder(t->rchild);
   7    }
   8 } 

7.4. 查找结点

在二叉树t中查找值为x的结点,找到了返回指向元素值为x的结点的指针,找不到返回NULL

   1 bintree locate(bintree t, ElemType x) {
   2    bintree p;
   3    if (t == NULL)
   4       return NULL;
   5    else if( t->data == x)
   6       return t;
   7    else {
   8       p = locate(t->lchild, x);
   9       if (p)
  10          return p;
  11       else
  12          return locate(t->rchild, x);
  13    }
  14 }

7.5. 计算结点个数

计算二叉树t中的节点个数

   1 int node_count(bintree t) {
   2    if( t == NULL)
   3       return 0;
   4    else 
   5       return node_count(t->lchild) + node_count(t->rchild) + 1;
   6 }

计算二叉树t中的叶子结点个数

   1 int leaf_count(bintree t) {
   2    if( t == NULL )
   3       return 0;
   4    else if(t->lchild == NULL && t->rchild == NULL)
   5       return 1;
   6    else 
   7       return leaf_count(t->lchild) + leaf_count(t->rchild);
   8 }

7.6. 判断相等

判断两个二叉树t1和t2是否完全相同。

   1 int isequal( bintree t1, bintree t2) {
   2    if( t1 == NULL && t2 == NULL)
   3       return 1;
   4    else if( t1 != NULL && t2 != NULL)
   5       return t1->data == t2 -> data && 
   6          isequal( t1->lchild, t2 -> lchild) && 
   7          isequal(t1->rchild, t2->rchild);
   8    else
   9       return 0;
  10 }

7.7. 计算高度

计算二叉树t的高度

   1 int max(int a, int b) {
   2    return a > b ? a : b;
   3 }
   4 int depth(bintree t) {
   5    if( t == NULL)
   6       return 0;
   7    else
   8       return max( depth(t->lchild), depth(t->rchild) ) +1;
   9 }

7.8. 交换左右子树

交换二叉树上所有左右子树的顺序

   1 void swap(bintree t) {
   2    if( t != NULL) {
   3       bintree temp = t->lchild;
   4       t->lchild = t->rchild;
   5       t->rchild = temp;
   6       swap(t->lchild);
   7       swap(t->rchild);
   8    }
   9 }

8. 练习

线索二叉树

1. 定义

n个结点的二叉链表中含有n+1个空指针域。利用二叉链表中的空指针域,存放指向结点在某种遍历次序下的前趋和后继结点的指针(这种附加的指针称为"线索")。

这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。

线索链表解决了二叉链表找左、右孩子困难的问题,出现了无法直接找到该结点在某种遍历序列中的前趋和后继结点的问题。

2. 线索链表的结点结构

线索链表中的结点结构为:

threadbintreenode.jpg

ltag和rtag是增加的两个标志域,用来区分结点的左、右指针域是指向其左、右孩子的指针(tag=0),还是指向其前趋或后继的线索(tag=1)。

   1 enum pointer_tag{LINK, THREAD};
   2 struct threaded_binnode{
   3     ElemType data;
   4     pointer_tag ltagrtag; 
   5     struct threaded_binnode *lchild,*rchild;
   6 } threaded_binnode;
   7 typedef struct threaded_binnode *threaded_bintree;

3. 线索二叉树的表示

【例】下面(a)图所示的中序线索二叉树,其线索链表如下面(b)图所示。

threadbintree.jpg

图中的实线表示指针,虚线表示线索。结点C的左线索为空,表示C是中序序列的开始结点,无前趋;结点E的右线索为空,表示E是中序序列的终端结点,无后继。线索二叉树中,一个结点是叶结点的充要条件为:左、右标志均是1。

4. 二叉树线索化

将二叉树变为线索二叉树的过程称为线索化。按某种次序将二叉树线索化的实质是:按该次序遍历二叉树,在遍历过程中用线索取代空指针。

例:二叉树的中序线索化

算法与中序遍历算法类似。只需要将遍历算法中访问结点的操作具体化为建立正在访问的结点与其非空中序前趋结点间线索。

该算法应附设一个指针pre始终指向刚刚访问过的结点(pre的初值应为NULL),而指针p指示当前正在访问的结点。结点*pre是结点*p的前趋,而*p是*pre的后继。

   1 threaded_binnode *pre=NULL; 
   2 
   3 void InorderThreading(threaded_bintree p){
   4     if(p) {
   5         InorderThreading(p->lchild); 
   6         p->ltag=(p->lchild)?LINK:THREAD;
   7         p->rtag=(p->rchild)?LINK:THREAD;
   8         if(pre){
   9             if(pre->rtag==THREAD) 
  10                 pre->rchild=p; 
  11             if(p->ltag==THREAD)
  12                 p->lchild=pre; 
  13         } 
  14         pre=p; 
  15         InorderThreeding(p->rehild); 
  16     }
  17 }

5. 线索二叉树的运算

在中序线索二叉树中,查找结点*p的中序后继结点

在中序线索二叉树中,查找结点p的中序后继结点分两种情形:

   1 threaded_binnode *InorderSuccessor(threaded_binnode *p) {
   2     threaded_binnode *q;
   3     if (p->rtag==THREAD) 
   4         return p->rchild; 
   5     else{ 
   6         q=p->rchild;
   7         while (q->ltag==LINK)
   8             q=q->lchild;
   9         return q;
  10     }
  11 }

在中序线索二叉树中查找结点*p的中序前趋结点

中序是一种对称序,故在中序线索二叉树中查找结点*p的中序前趋结点与找中序后继结点的方法完全对称。具体情形如下:

   1 threaded_binnode *Inorderpre(threaded_binnode *p) {
   2     threaded_binnode *q
   3     if (p->ltag==THREAD)
   4         return p->lchild
   5     else{ 
   6         q=p->lchild
   7         while (q->rtag==LINK)
   8             q=q->rchild
   9         return q
  10     }
  11 }

在后序线索二叉树中,查找指定结点*p的后序前趋结点

在后序线索二叉树中,查找指定结点*p的后序前趋结点的具体规律是:

在后序线索二叉树中,查找指定结点*p的后序后继结点

6. 遍历线索二叉树

遍历某种次序的线索二叉树,只要从该次序下的开始结点开发,反复找到结点在该次序下的后继,直至终端结点。

遍历中序线索二叉树算法:

   1 void TraverseInorderThrTree(threaded_bintree p){
   2     if(p){
   3         while(p->ltag==LINK)
   4             p = p->lchild;
   5         do{
   6             printf("%c", p->data);
   7             p=InorderSuccessor(p);
   8         }while(p)
   9     }
  10 }

树和森林

1. 树、森林和二叉树的关系

树或森林与二叉树之间有一个自然的一一对应关系。任何一个森林或一棵树可惟一地对应到一棵二叉树;反之,任何一棵二叉树也能惟一地对应到一个森林或一棵树。

将树转换为二叉树

树中每个结点最多只有一个最左边的孩子(长子)和一个右邻的兄弟。按照这种关系很自然地就能将树转换成相应的二叉树:

tree2bintree.jpg

将一个森林转换为二叉树

forest2bintree.jpg

二叉树到树、森林的转换

若结点x是双亲y的左孩子,则把x的右孩子,右孩子的右孩子,…,都与y用连线连起来,最后去掉所有双亲到右孩子的连线。

bintree2tree.jpg

2. 树的存储结构

2.1. 双亲表示法

双亲表示法利用树中每个结点的双亲唯一性,在存储结点信息的同时,为每个结点附设一个指向其双亲的指针parent,惟一地表示任何一棵树。

链式:

   1 struct treenode{
   2     ElemType data; //结点数据
   3     struct treenode  parent; //双亲指针,指示结点的双亲的位置
   4 };

向量式:

   1 #define MaxTreeSize 100 //向量空间的大小,由用户定义
   2 struct treenode{
   3     ElemType data; //结点数据
   4     int parent; //双亲指针,指示结点的双亲在向量中的位置
   5 };
   6 struct tree{ 
   7     struct treenode nodes[MaxTreeSize];
   8     int size; //结点总数 
   9 };

parenttree.jpg

parenttree2.jpg

2.2. 孩子链表表示法

孩子链表表示法是为树中每个结点设置一个孩子链表,并将这些结点及相应的孩子链表的头指针存放在一个向量中。

   1 struct childnode{ //子链表结点
   2     int child; //孩子结点在向量中对应的序号
   3     struct childnode *next;
   4 };
   5 
   6 struct node{ 
   7     ElemType data; //存放树中结点数据
   8     struct childnode *firstchild; //孩子链表的头指针
   9 };
  10 
  11 struct tree{
  12     node nodes[MaxTreeSize];
  13     int n, root; //n为结点总数,root指出根在向量中的位置
  14 };

parenttree.jpg

childtree1.jpg

将双亲链表表示法和孩子链表表示法结合起来,可形成双亲孩子链表表示法。

childtree2.jpg

2.3. 孩子兄弟表示法

在存储结点信息的同时,附加两个分别指向该结点最左孩子和右邻兄弟的指针域leftmostchild和rightsibling,即可得树的孩子兄弟链表表示。

这种存储结构的最大优点是:它和二叉树的二叉链表表示完全一样。可利用二叉树的算法来实现对树的操作。

childsiblingtree.jpg

3. 树和森林的遍历

树的前序遍历

树的后序遍历定义:

注意:

森林的前序遍历:

森林的后序遍历:

注意:

例:对下面(a)图中所示的森林进行前序遍历和后序遍历,则得到该森林的前序序列和后序序列分别为ABCDEFICJH和BDCAIFJGHE。而(b)图所示二叉树的前序序列和中序序列也分别为ABCDEFICJH和BDCAIFJGHE。

treetraverse.jpg

哈夫曼树和哈夫曼编码

1. 基本概念

例:给定4个叶子结点a,b,c和d,分别带权7,5,2和4。构造如下图所示的三棵二叉树(还有许多棵),它们的带权路径长度分别为

  1. WPL=7*2+5*2+2*2+4*2=36
  2. WPL=7*3+5*3+2*1+4*2=46
  3. WPL=7*1+5*2+2*3+4*3=35

其中(c)树的WPL较小。所有可能的形态中,WPL最小的树就是哈夫曼树了。

wplexample.jpg

最优二叉树的特点:

  1. 最优二叉树中,权越大的叶子离根越近。即:如果wi<wj则li>=lj。

  2. 最优二叉树的形态不唯一。
  3. 最优二叉树中,没有度为1的结点。

2. 哈夫曼算法

哈夫曼算法是构造最优二叉树的算法,其基本思想是:

  1. 根据给定的n个权值wl,w2,…,wn构成n棵二叉树的森林F={T1,T2,…,Tn},其中每棵二叉树Ti中都只有一个权值为wi的根结点,其左右子树均空。
  2. 在森林F中选出两棵根结点权值最小的树(当这样的树不止两棵树时,可以从中任选两棵),将这两棵树合并成一棵新树,为了保证新树仍是二叉树,需要增加一个新结点作为新树的根,并将所选的两棵树的根分别作为新根的左右孩子(谁左,谁右无关紧要),将这两个孩子的权值之和作为新树根的权值。
  3. 对新的森林F重复(2),直到森林F中只剩下一棵树为止。这棵树便是哈夫曼树。

注意:

  1. 初始森林中的n棵二叉树,每棵树有一个孤立的结点,它们既是根,又是叶子
  2. n个叶子的哈夫曼树要经过n-1次合并,产生n-1个新结点。最终求得的哈夫曼树中共有2n-1个结点。

哈夫曼树的存储结构

用一个大小为2n-1的向量来存储哈夫曼树中的结点,其存储结构为:

   1 struct huffman_node{ //结点类型
   2     float weight; //权值,不妨设权值均大于零
   3     int lchild, rchild, parent; //左右孩子及双亲指针
   4 };
   5 typedef huffman_node huffman_tree[MAXNODE]; //HuffmanTree是向量类型
   6 

注意:

   1 //构造哈夫曼树,T[m-1]为其根结点,n个叶子
   2 void create_huffman_tree(int weights[], int n, huffman_tree T){
   3     int i, p1, p2;
   4     for(i = 0; i < n; i++) {
   5         T[i].weight = weights[i];
   6         T[i].lchild=T[i].rchild=t[i].parent=-1;
   7     }
   8     for(i=n; i< 2*n-1; i++){
   9         selectmin(T, i-1, &p1, &p2); //在T[0..i-1]中选择两个权最小的根结点,其序号分别为p1和p2
  10         T[p1].parent = T[p2].parent = i;
  11         T[i].1child = p1;
  12         T[j].rchild = p2;
  13         T[i].weight = T[p1].weight + T[p2].weight;
  14         T[i].parent = -1;
  15     }
  16 }

例:以7个权值:7,5,1,4,8,10,20为例,写出用上述算法求最优二叉树的过程

3. 哈夫曼编码

给定的字符集C,可能存在多种编码方案。

例:设待压缩的数据文件共有100000个字符,这些字符均取自字符集C={a,b,c,d,e,f},等长编码需要三位二进制数字来表示六个字符,因此,整个文件的编码长度为300000位。每个字符在文件中出现的次数(简称频度)见表

字符

a

b

c

d

e

f

频度(单位:千次)

45

13

12

16

9

5

定长编码

000

001

010

011

100

101

变长编码

0

101

100

111

1101

1100

使用变长编码:(45*1+13*3+12*3+16*3+9*4+584)*1000=224000 整个文件被编码为224000位,比定长编码方式节约了约25%的存储空间。

变长编码可能使解码产生二义性。产生该问题的原因是某些字符的编码可能与其他字符的编码开始部分(称为前缀)相同。

例:若将表中所示的文件作为统计的样本,则a至f六个字符的概率分别为0.45,0.13,0.12,0.16,0.09,0.05,对变长编码求得的平均码长为2.24,优于定长编码(平均码长为3)。

4. 根据最优二叉树构造哈夫曼编码

  1. 用字符ci作为叶子,pi或fi做为叶子ci的权,构造一棵哈夫曼树,并将树中左分支和右分支分别标记为0和1;
  2. 将从根到叶子的路径上的标号依次相连,作为该叶子所表示字符的编码。该编码即为最优前缀码(也称哈夫曼编码)。

   1 /*根据哈夫曼树T求哈夫曼编码表H,n为叶子结点个数*/
   2 void huffman_encoding(huffman_tree T, int n, char* H[])
   3 {
   4     int i;
   5     char *code =(char*)malloc(n);
   6     code[n-1]='\0';
   7 
   8     for(i=0; i<n; i++){ 
   9         int start = n-1; 
  10         int current = i;
  11         int parent;
  12         while((parent = T[current].parent)!=-1){ //直至上溯到T[c]是树根为止
  13             code[--start] = (T[parent].1child==current) ? '0' : '1';
  14             current = parent;
  15         }
  16         H[i] = (char*)malloc(sizeof(n-start));
  17         strcpy(H[i], &code[start]);
  18     }
  19 }

编码与解码

有了字符集的哈夫曼编码表之后,对数据文件的编码过程是:依次读人文件中的字符c,在哈夫曼编码表H中找到此字符,若H[i].ch=c,则将字符c转换为H[i].bits中存放的编码串。

对压缩后的数据文件进行解码则必须借助于哈夫曼树T,其过程是:依次读人文件的二进制码,从哈夫曼树的根结点(即T[m-1])出发,若当前读人0,则走向左孩子,否则走向右孩子。一旦到达某一叶子T[i]时便译出相应的字符H[i].ch。然后重新从根出发继续译码,直至文件结束。

树和二叉树 (2008-02-23 15:34:59由localhost编辑)

ch3n2k.com | Copyright (c) 2004-2020 czk.