数据结构(C语言版)-- 树和二叉树笔记

树和二叉树

  • 1、树和二叉树的定义
    • ①.树的定义
    • ②.树的基本术语
    • ③.二叉树的定义
  • 2、树和二叉树的抽象数据类型定义
  • 3、二叉树的性质和存储结构
    • ①.二叉树的性质
    • ②.二叉树的存储结构
  • 4、遍历二叉树和线索二叉树
    • ①.遍历二叉树
    • ②.线索二叉树
  • 5、树和森林
    • ①.树的存储结构
    • ②.森林与二叉树的转换
    • ③.树和森林的遍历
  • 6、哈夫曼树及其应用
    • ①.哈夫曼树的基本概念
    • ②.哈夫曼树的构造算法
    • ③.哈夫曼编码
  • 7、总结
  • 8、例题与应用


1、树和二叉树的定义

①.树的定义

树( Tree)是n(n≥0)个结点的有限集,它或为空树(n=0);或为非空树,对于非空树T:

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

在下图中,(a)是只有一个根结点的树;(b)是有13个结点的树,其中A是根,其余结点分成3个互不相交的子集:T1={B,E,F,K,L},T2 ={C,G},T3(D,H,I,J,M})。T1、T2和T3都是根A 的子树,且本身也是一棵树。例如T1,其根为B,其余结点分为两个互不相交的子集:T11={E,K,L},T12={F}。T11和T12都是B的子树。而T11中E是根,{K}和(L}是E的两棵互不相交的子树,其本身又是只有一个根结点的树。

数据结构(C语言版)-- 树和二叉树笔记_第1张图片

树的结构定义是一个递归的定义,即在树的定义中又用到树的定义,它道出了树的固有特性。

线性结构 树型结构
第一个数据元素(无前驱) 根结点(无前驱)
最后一个数据元素(无后继) 多个叶子结点(无后继)
其他数据元素(一个前驱,一个后继) 树中其它结点(一个前驱,一个后继)

②.树的基本术语

数据结构(C语言版)-- 树和二叉树笔记_第2张图片

术语 解释
结点 树的数据元素;如A、B、C、D等
根节点(没有前驱);如A
叶子 终端结点(没有后继);结点K、L、F、G、M、I、J都是树的叶子
森林 m棵不相交的树的集合(例如删除A后的子树个数)
有序树 结点各子树从左到右有序,不能互换(左为第一)
无序树 结点各子树可互换位置
双亲 上层的结点(直接前驱);B的双亲为A
孩子 下层结点子树的根(直接后继);B的孩子有E和F
兄弟 同一双亲下的同层结点(孩子之间互称兄弟);H、I和J互为兄弟
堂兄弟 双亲位于同一层的结点(并非同一双亲);结点G与E、F和H、I、J互为堂兄弟
祖先 根到该结点所经分支的所有结点;M的祖先为A、D和H
子孙 该结点下层子树中任一结点;B的子孙为E、K、L和F
结点的度 结点拥有的子树数;A的度为3,C的度为1,F的度为0
树的度 所有结点度中的最大值;该树的度为3
结点的层次 根到该结点的层数(根结点算第一层)
终端结点 度为0的结点,即叶子
分支结点 度不为0的结点
树的深度 所有结点中最大的层数;该树的深度为4

③.二叉树的定义

二叉树(Binary Tree)是n ( n≥0)个结点所构成的集合,它或为空树(n=0);或为非空树,对于非空树T:

1. 有且仅有一个称之为根的结点;
2. 除根结点以外的其余结点分为两个互不相交的子集T1,和T2,分别称为T的左子树和右子树,且T1和T2本身又都是二叉树。

二叉树与树—样具有递归性质,二叉树与树的区别主要有以下两点:

1. 二叉树每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点);
2. 二叉树的子树有左右之分,其次序不能任意颠倒。

二叉树的5种基本形态:

数据结构(C语言版)-- 树和二叉树笔记_第3张图片


2、树和二叉树的抽象数据类型定义

树的抽象数据类型定义:

数据结构(C语言版)-- 树和二叉树笔记_第4张图片
数据结构(C语言版)-- 树和二叉树笔记_第5张图片
二叉树的抽象数据类型定义:

数据结构(C语言版)-- 树和二叉树笔记_第6张图片
数据结构(C语言版)-- 树和二叉树笔记_第7张图片
数据结构(C语言版)-- 树和二叉树笔记_第8张图片
数据结构(C语言版)-- 树和二叉树笔记_第9张图片


3、二叉树的性质和存储结构

①.二叉树的性质

性质1:在二叉树的第i层上至多有2i-1个结点(i≥1)

证明:i=1时,只有一个根结点,2i-1=1成立﹔假设第i-1层至多有2i-2个结点,又二叉树每个结点的度至多为2;所以,第i层上最大结点数是第i-1层的2倍,即2i-1个结点。

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

证明:由性质1,可得深度为k的二叉树最大结点数是2k-1

性质3:对任何一棵二叉树T,如果其终端(叶子)结点个数为n0,度为2的结点数为n2,则n0=n2+1。

证明:n1为二叉树T中度为1的结点数,二叉树中所有结点的度均小于或等于2,其结点总数n=n0+n1+n2;二叉树中,除根结点外,其余结点都只有一个分支进入,设: B为分支总数,则n=B+1;分支由度为1和度为2的结点射出,B=n1+2n2于是n=B+1=n1+2n2+1=n0+n1+n2,所以no=n2+1

这里引入两个概念:满二叉树和完全二叉树

满二叉树:深度为k且含有2k-1个结点的二叉树。
满二叉树的特点是:每一层上的结点数都是最大结点数,即每一层i的结点数都具有最大值2i-1。

完全二叉树:深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点—一对应时,称之为完全二叉树。
完全二叉树的特点是:叶子结点只可能在层次最大的两层上出现;对任一结点,若其右分支下的子孙的最大层次为l,则其左分支下的子孙的最大层次必为l或l+1。

数据结构(C语言版)-- 树和二叉树笔记_第10张图片

性质4:具有n个结点的完全二叉树的深度为⌊log2n⌋+1

证明:2k-1-1 k-1 或2k-1≤ n <2k,k-1≤ log2n 2n+ 1

性质5:如果对一棵有n个结点的完全二叉树从上至下、从左至右编号,则编号为i (1≤ i≤ n)的结点:
若i=1,则该结点是二叉树的根,无双亲,否则,其双亲结点编号为⌊i/2⌋;
若2i > n,则该结点无左孩子,否则,其左孩子结点编号为2i ;
若2i+1 >n,则该结点无右孩子结点,否则,其右孩子结点编号为2i+1。

证明:

数据结构(C语言版)-- 树和二叉树笔记_第11张图片

②.二叉树的存储结构

顺序存储结构

//二叉树的顺序存储表示
#define MAXTSIZE 100
typedef TElemType SqBiTree[MAXTSIZE];
SqBiTree bt;

顺序存储结构使用一组地址连续的存储单元来存储数据元素,为了能够在存储结构中反映出结点之间的逻辑关系,必须将二叉树中的结点依照―定的规律安排在这组单元中。

对于完全二叉树,只要从根起按层序存储即可,依次自上而下、自左至右存储结点元素,即将完全二叉树上编号为i的结点元素存储在如上定义的一维数组中下标为i-1的分量中。

对于一般二叉树,则应将其每个结点与完全二叉树上的结点相对照,存储在一维数组的相应分量中,图中以“0”表示不存在此结点。

在这里插入图片描述

这种顺序结构只适合完全二叉树,所以对于一般二叉树,更适合采用链式存储结构。

链式存储结构

由二叉树的定义可知,二叉树的结点有一个数据元素和分别指向其左、右子树的两个分支构成,则表示二叉树的链表中的结点至少包含3个域:数据域和左、右指针域。 有时为了方便找到结点的双亲,还可以在结点结构中增加一个指向其双亲结点的指针域。利用这两种结点结构所得二叉树的存储结构分别称之为二叉链表和三叉链表。

数据结构(C语言版)-- 树和二叉树笔记_第12张图片

数据结构(C语言版)-- 树和二叉树笔记_第13张图片

//二叉树的二叉链表存储表示
typedef struct BiTNode
{
    TElemType data;                 //结点数据域
    struct BiTNode *lchild, *rchild; //左右孩子指针
} BiTNode, *BiTree;                  //二叉树结点


4、遍历二叉树和线索二叉树

①.遍历二叉树

遍历二叉树算法描述

遍历二叉树( traversing binary tree)是指按某条搜索路径巡访树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。

二叉树是由3个基本单元组成:根结点、左子树和右子树。因此,若能依次遍历这三部分,便是遍历了整个二叉树。假如从L、D、R分别表示遍历左子树、访问根结点和遍历右子树,则可有DLR、LDR、LRD、DRL、RDL、RLD这6种遍历二叉树的方案。若限定先左后右,则只有前3种情况,分别称之为先(根)序遍历、中(根)序遍历和后(根)序遍历。

数据结构(C语言版)-- 树和二叉树笔记_第14张图片

数据结构(C语言版)-- 树和二叉树笔记_第15张图片

void PreOrderTraverse(BiTree T)
{
    if (T) //非空二叉树
    {
        cout << T->data;             //访问根结点
        PreOrderTraverse(T->lchild); //递归遍历左子树
        PreOrderTraverse(T->rchild); //递归遍历右子树
    }
}

数据结构(C语言版)-- 树和二叉树笔记_第16张图片

void InOrderTraverse(BiTree T)
{
    if (T) //非空二叉树
    {
        InOrderTraverse(T->lchild); //递归遍历左子树
        cout << T->data;            //访问根结点
        InOrderTraverse(T->rchild); //递归遍历右子树
    }
}


数据结构(C语言版)-- 树和二叉树笔记_第17张图片

void PostOrderTraverse(BiTree T)
{
    if (T) //非空二叉树
    {
        PostOrderTraverse(T->lchild); //递归遍历左子树
        PostOrderTraverse(T->rchild); //递归遍历右子树
        cout << T->data;              //访问根结点
    }
}

用二叉树表示算术表达式

数据结构(C语言版)-- 树和二叉树笔记_第18张图片

根据遍历序列确定二叉树

由二叉树的先序序列和中序序列,或由其后序序列和中序序列均能唯一地确定一棵二叉树。但是由一颗二叉树的先序序列和后序序列不能唯一确定一颗二叉树。

例:已知一棵二叉树的中序序列和后序序列分别是 BDCEAFHG和DECBHGFA,请画出这棵二叉树。

  1. 由后序遍历特征,根结点必在后序序列尾部,即根结点是A;
  2. 由中序遍历特征,根结点必在其中间,而且其左部必全部是左子树子孙(BDCE),其右部必全部是右子树子孙(FHG );
  3. 继而,根据后序中的DECB子树可确定B为A的左孩子,根据HGF子串可确定F为A的右孩子;依此类推,可以唯一地确定一棵二叉树,如下图所示。

数据结构(C语言版)-- 树和二叉树笔记_第19张图片

例:

数据结构(C语言版)-- 树和二叉树笔记_第20张图片

先序遍历的顺序建立二叉链表

  1. 扫描字符序列,读人字符ch。
  2. 如果ch是一个“#”字符,则表明该二叉树为空树,即T为NULL;否则执行以下操作:
    - 申请一个结点空间T;
    - 将ch赋给T-> data;
    - 递归创建T的左子树;
    - 递归创建T的右子树;
void CreateBiTree(BiTree &T)
{
    cin >> ch;
    if (ch ==’#’)
        T = NULL; //递归结束,建空树
    else
    {
        T = new BiTNode;
        T - >data = ch;            //生成根结点
        CreateBiTree(T - >lchild); //递归创建左子树
        CreateBiTree(T - >rchild); //递归创建右子树
    }
}

复制二叉树

如果是空树,递归结束,否则执行以下操作:

  1. 申请一个新结点空间,复制根结点;
  2. 递归复制左子树;
  3. 递归复制右子树。
void Copy(BiTree T, BiTree &NewT)
{
    if (T = NULL)
    {
        NewT = NULL;
        return;
    }
    else
    {
        NewT = new BiTNode;
        NewT->data = T->data;
        Copy(T->lchild, NewT->lchild);
        Copy(T->rchild, NewT->rchild);
    }
}

计算二叉树的深度

如果是空树,递归结束,深度为0,否则执行以下操作:

  1. 递归计算左子树的深度记为m;
  2. 递归计算右子树的深度记为n;
  3. 如果m大于n,二叉树的深度为m+1,否则为n+1。
int Depth(BiTree T)
{ // 返回二叉树的深度
    if (!T)
        return 0;
    else
    {
        m = Depth(T->lchild);
        n = Depth(T->rchild);
        if (m > n)
            return (m + 1);
        else return (n+1;
    }
} 

统计二叉树中结点的个数

int NodeCount(BiTree T)
{
    if (T == NULL)
        return 0;
    else
        return NodeCount(T->lchild) + NodeCount(T->rchild) + 1;
}

统计二叉树中叶子结点个数

int CountLeaf(BiTree T)
{ // 返回指针T所指二叉树中的叶子结点的个数
    if (!T)
        return 0;
    if (!T->lchild && !T->rchild)
        return 1;
    else
    {
        m = CountLeaf(T->lchild);
        n = CountLeaf(T->rchild);
        return (m + n);
    } // else
} // CountLeaf

②.线索二叉树

当以二叉链表作为存储结构时,只能找到结点的左、右孩子信息,而不能直接得到结点在任一序列中的前驱和后继信息,这种信息只有在遍历的动态过程中才能得到,为此引人线索二叉树来保存这些在动态过程中得到的有关前驱和后继的信息。

线索:指向前驱或后继结点的指针称为线索

线索二叉树:加上线索的二叉链表表示的二叉树叫线索二叉树

线索化:对二叉树按某种遍历次序使其变为线索二叉树的过程叫线索化

试做如下规定:若结点有左子树,则其lchild域指示其左孩子,否则令lchild域指示其前驱;若结点有右子树,则其rchild域指示其右孩子,否则令rchild域指示其后继。为了避免混淆,尚需改变结点结构,增加两个标志域。

数据结构(C语言版)-- 树和二叉树笔记_第21张图片

数据结构(C语言版)-- 树和二叉树笔记_第22张图片

二叉树的二叉线索存储表示

typedef struct BiThrNode
{
    TElemType data;
    struct BiThrNode *lchild, *rchild;//左右孩子标志
    int LTag, RTag;//左右标志
} BiThrNode, *BiThrTree;

以结点p为根的子树中序线索化

算法中有一全局变量:pre,在主调程序中初值为空,在整个线索化算法中pre始终指向当前结点p的前驱。

  1. 如果p非空,左子树递归线索化。
  2. 如果p的左孩子为空,则给p加上左线索,将其LTag置为1,让p的左孩子指针指向pre(前驱)﹔否则将p的LTag置为0。
  3. 如果pre的右孩子为空,则给pre加上右线索,将其RTag置为1,让pre的右孩子指针指向p(后继)﹔否则将pre的RTag置为0。
  4. 将pre指向刚访问过的结点p,即pre=p。
  5. 右子树递归线索化。
void InThreading(BiThrTree p)
{ // pre是全局变量,初始化时其右孩子指针为空,便于在树的最左点开始建线索
    if (p)
    {
        InThreading(p->lchild); //左子树递归线索化
        if (!p->lchild)         // p的左孩子为空
        {
            p->LTag = 1;     //给p加上左线索
            p->lchild = pre; // p的左孩子指针指向pre(前驱)
        }
        else
            p->LTag = 0;
        if (!pre->rchild) // pre的右孩子为空
        {
            pre->RTag = 1;   //给pre加上右线索
            pre->rchild = p; // pre的右孩子指针指向p(后继)
        }
        else
            pre->RTag = 0;
        pre = p;                //保持pre指向p的前驱
        InThreading(p->rchild); //右子树递归线索化
    }

} 

带头结点的二叉树中序线索化

void InOrderThreading(BiThrTree &Thrt, BiThrTree T)
{                         //中序遍历二叉树T,并将其中序线索化,Thrt指向头结点
    Thrt = new BiThrNode; //建头结点
    Thrt->LTag = 0;       //头结点有左孩子,若树非空,则其左孩子为树根
    Thrt->RTag = 1;       //头结点的右孩子指针为右线索
    Thrt->rchild = Thrt;  //初始化时右指针指向自己
    if (!T)
        Thrt->lchild = Thrt; //若树为空,则左指针也指向自己
    else
    {
        Thrt->lchild = T;
        pre = Thrt;         //头结点的左孩子指向根,pre初值指向头结点
        InThreading(T);     //对以T为根的二叉树进行中序线索化
        pre->rchild = Thrt; //算法5.7结束后,pre为最右结点,pre的右线索指向头结点
        pre->RTag = 1;
        Thrt->rchild = pre; //头结点的右线索指向pre
    }
}

遍历中序线索二叉树

  1. 指针p指向根结点。
  2. p为非空树或遍历未结束时,循环执行以下操作:
    - 沿左孩子向下,到达最左下结点p,它是中序的第一个结点;
    - 访问
    p;
    - 沿右线索反复查找当前结点*p的后继结点并访问后继结点,直至右线索为0或者遍历结束;
    - 转向p的右子树。
void InOrderTraverse_Thr(BiThrTree T)
{ // T指向头结点,头结点的左链lchild指向根结点,可参见线索化算法5.8。
  //中序遍历二叉线索树T的非递归算法,对每个数据元素直接输出
    p = T->lchild; // p指向根结点
    while (p != T) //空树或遍历结束时,p==T
    {
        while (p->LTag == 0) //沿左孩子向下
            p = p->lchild;   //访问其左子树为空的结点
        cout << p->data;
        while (p->RTag == 1 && p->rchild != T)
        {
            p = p->rchild; //沿右线索访问后继结点
            cout << p->data;
        }
        p = p->rchild;
    }
}

数据结构(C语言版)-- 树和二叉树笔记_第23张图片

数据结构(C语言版)-- 树和二叉树笔记_第24张图片


5、树和森林

①.树的存储结构

双亲表示法

这种表示方法中,以一组连续的存储单元存储树的结点,每个结点除了数据域data外,还附设一个parent域用以指示其双亲结点的位置。

数据结构(C语言版)-- 树和二叉树笔记_第25张图片

#defin MAX_TREE_SIZE 100
typedef struct PTNode
{ //结点结构
    ElemType data;
    int parent; //保存双亲位置
} PTNode;

typedef struct
{ //树结构
    PTNode nodes[MAX_TREE_SIZE];
    int r, n; //根的位置和结点数
} PTree;

孩子表示法

由于树中每个结点可能有多棵子树,则可用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点。

数据结构(C语言版)-- 树和二叉树笔记_第26张图片
数据结构(C语言版)-- 树和二叉树笔记_第27张图片

//树结构
typedef struct
{
    CTBox nodes[MAX_TREE_SIZE];
    int n, r; // 结点数和根结点的位置
} CTree;
//孩子链表表示法的类型描述
typedef struct CTNode
{
    int child;
    struct CTNode *nextchild;
} * ChildPtr;
//双亲结点结构
typedef struct
{
    ElemType data;
    int parent; //保存双亲位置
    ChildPtr firstchild;
    // 孩子链的头指针
} CTBox;

孩子兄弟法

又称二叉树表示法,或二叉链表表示法,即以二叉链表做树的存储结构。链表中结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点,分别命名为firstchild域和nextsibling域。

数据结构(C语言版)-- 树和二叉树笔记_第28张图片
数据结构(C语言版)-- 树和二叉树笔记_第29张图片

typedef struct CSNode
{
    ElemType data;
    struct CSNode *firstchild, *nextsibling;
} CSNode, *CSTree;

②.森林与二叉树的转换

从树的二叉链表表示的定义可知,任何一棵和树对应的二叉树,其根结点的右子树必空。若把森林中第二棵树的根结点看成是第一棵树的根结点的兄弟,则同样可导出森林和二叉树的对应关系。

树转换成二叉树的方法

数据结构(C语言版)-- 树和二叉树笔记_第30张图片

二叉树转换为树的方法

数据结构(C语言版)-- 树和二叉树笔记_第31张图片

森林转换成二叉树的方法

数据结构(C语言版)-- 树和二叉树笔记_第32张图片

二叉树转换成森林的方法

数据结构(C语言版)-- 树和二叉树笔记_第33张图片

例:

数据结构(C语言版)-- 树和二叉树笔记_第34张图片
数据结构(C语言版)-- 树和二叉树笔记_第35张图片

③.树和森林的遍历

树的遍历:

先根(序)遍历:先访问树的根结点,然后依次先根遍历根的每棵子树后根(序)遍历:先依次后根遍历每棵子树,然后访问根结点。

按层次遍历:先访问第一层上的结点,然后依次遍历第二层,…,第n层的结点。

数据结构(C语言版)-- 树和二叉树笔记_第36张图片
森林的遍历:

  1. 先序遍历森林
    - 访问森林中第一棵树的根结点
    - 先序遍历第一棵树中根结点的子树森林
    - 先序遍历除去第一棵树之后剩余的树构成森林

  2. 中序遍历森林
    - 中序遍历森林中第一棵树的根结点的子树森林
    - 访问第一棵树的根结点
    - 中序遍历除去第一棵树之后剩余的树构成的森林

数据结构(C语言版)-- 树和二叉树笔记_第37张图片


6、哈夫曼树及其应用

①.哈夫曼树的基本概念

哈夫曼(Huffman )树又称最优树,是一类带权路径长度最短的树,在实际中有广泛的用途。

路径:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径。

路径长度:路径上的分支数目称作路径长度。

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

权:赋予某个实体的一个量,是对实体的某个或某些属性的数值化描述。

结点的带权路径长度:从该结点到树根之间的路径长度与结点上权的乘积。

树的带权路径长度:树中所有叶子结点的带权路径长度之和。

哈夫曼树:假设有m个权值{w1,w2…,wm},可以构造一棵含n个叶子结点的二叉树,每个叶子结点的权为wi,则其中带权路径长度WPL最小的二叉树称做最优二叉树或哈夫曼树。

数据结构(C语言版)-- 树和二叉树笔记_第38张图片

②.哈夫曼树的构造算法

哈夫曼树的构造过程

核心思想:使权值大的结点靠近根。

  1. 根据给定的n个权值{w1,w2…wn},构造n棵只有根结点的二叉树,这n棵二叉树构成一个森林F。
  2. 在森林中选取两棵根结点权值最小的树作左右子树,构造一棵新的二叉树,置新二叉树根结点权值为其左右子树根结点权值之和。
  3. 在森林中删除这两棵树,同时将新得到的二叉树加入森林中。
  4. 重复上述两步,直到只含一棵树为止,这棵树即哈夫曼树。
    数据结构(C语言版)-- 树和二叉树笔记_第39张图片

哈夫曼算法的实现

哈夫曼树是―种二叉树,当然可以采用前面介绍过的通用存储方法,而由于哈夫曼树中没有度为1的结点,则一棵有n个叶子结点的哈夫曼树共有2n-1个结点,可以存储在一个大小为2n-1的一维数组中。

typedef struct
{
    int weight;                 //结点的权值
    int parent, lchild, rchild; //双亲、左孩子、右孩子的下标
} HTnode, *HuffmanTree;

构造哈夫曼树

  1. 初始化:首先动态申请2n个单元;然后循环2n-1次,从1号单元开始,依次将1至2n-1所有单元中的双亲、左孩子、右孩子的下标都初始化为0;最后再循环n次,输人前n个单元中叶子结点的权值。
  2. 创建树:循环n-1次,通过n-1次的选择、删除与合并来创建哈夫曼树。选择是从当前森林中选择双亲为0且权值最小的两个树根结点s1和s2;删除是指将结点sl和s2的双亲改为非0;合并就是将s1和s2的权值和作为一个新结点的权值依次存入到数组的第n+1之后的单元中,同时记录这个新结点左孩子的下标为s1,右孩子的下标为s2。
void CreatHuffmanTree(HuffmanTree HT, int n)
{ //构造哈夫曼树HT
    if (n <= 1)
        return;
    m = 2 * n - 1;
    HT = new HTNode[m + 1];  // 0号单元未用,HT[m]表示根结点
    for (i = 1; i <= m; ++i) //初始化:双亲、左孩子,右孩子的下标都为0
    {
        HT[i].parent = 0;
        HT[i].lchild = 0;
        HT[i].rchild = 0;
    }
    for (i = 1; i <= n; ++i) //输入前n个单元中叶子结点的权值
        cin >> HT[i].weight;
    /*―――初始化工作结束,下面开始创建哈夫曼树――――*/
    for (i = n + 1; i <= m; ++i)
    {                              //通过n-1次的选择、删除、合并来创建哈夫曼树
        Select(HT, i - 1, s1, s2); //选择两个其双亲域为0且权值最小的结点,
        HT[s1].parent = i;
        HT[s2].parent = i; //得到新结点i, 将s1和s2的双亲域由0改为i
        HT[i].lchild = s1;
        HT[i].rchild = s2;                            // s1,s2分别作为i的左右孩子
        HT[i].weight = HT[s1].weight + HT[s2].weight; // i 的权值为左右孩子权值之和
    }                                                 // for
}

例:

数据结构(C语言版)-- 树和二叉树笔记_第40张图片
数据结构(C语言版)-- 树和二叉树笔记_第41张图片

③.哈夫曼编码

下面给出有关编码的两个概念。

前缀编码: 如果在一个编码方案中,任一个编码都不是其他任何编码的前缀(最左子串),则称编码是前缀编码。

哈夫曼编码: 对一棵具有n个叶子的哈夫曼树,若对树中的每个左分支赋予0,右分支赋予1,则从根到每个叶子的路径上,各分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码。

哈夫曼编码满足下面的两个性质:

哈夫曼编码是前缀码
哈夫曼编码是最优前缀编码

哈夫曼编码的算法实现

在构造哈夫曼树之后,求哈夫曼编码的主要思想是:依次以叶子为出发点,向上回溯至根结点为止。回溯时走左分支则生成代码0,走右分支则生成代码1。由于每个哈夫曼编码是变长编码,因此使用一个指针数组来存放每个字符编码串的首地址。

  1. 分配存储n个字符编码的编码表空间HC,长度为n +1;分配临时存储每个字符编码的动态数组空间cd, cd[n-1]置为‘\0’
  2. 逐个求解n个字符的编码,循环n次,执行以下操作:
    设置变量start 用于记录编码在cd中存放的位置,start初始时指向最后,即编码结束符位置n -1;
    设置变量c用于记录从叶子结点向上回溯至根结点所经过的结点下标,c初始时为当前待编码字符的下标i,f 用于记录i的双亲结点的下标;
    从叶子结点向上回溯至根结点,求得字符i的编码,当f没有到达根结点时,循环执行以下操作:
    回溯一次start向前指一个位置,即–start;
    若结点c是f的左孩子,则生成代码0,否则生成代码1,生成的代码0或1保存在cd[start]中;
    继续向上回溯,改变c和f的值。
    根据数组cd的字符串长度为第i个字符编码分配空间HC[i],然后将数组cd中的编码复制到HC[i]中。
  3. 释放临时空间cd。
void CreatHuffmanCode(HuffmanTree HT, HuffmanCode &HC, int n)
{                           //从叶子到根逆向求每个字符的赫夫曼编码,存储在编码表HC中
    HC = new char *[n + 1]; //分配n个字符编码的头指针矢量
    cd = new char[n];       //分配临时存放编码的动态数组空间
    cd[n - 1] = '\0';       //编码结束符
    for (i = 1; i <= n; ++i)
    {                  // 逐个字符求赫夫曼编码
        start = n - 1; // start开始时指向最后,即编码结束符位置
        c = i;
        f = HT[i].parent; // f指向结点c的双亲结点
        while (f != 0)
        {            //从叶子结点开始向上回溯,直到根结点
            --start; //回溯一次start向前指一个位置
            if (HT[f].lchild == c)
                cd[start] =0; //结点c是f的左孩子,则生成代码0
            else
                cd[start] =1; //结点c是f的右孩子,则生成代码1
            c = f;
            f = HT[f].parent;        //继续向上回溯
        }                            //求出第i个字符的编码
        HC[i] = new char[n - start]; // 为第i 个字符编码分配空间
        strcpy(HC[i], &cd[start]);   //将求得的编码从临时空间cd复制到HC的当前行中
    }
    delete cd; //释放临时空间
}

例:

数据结构(C语言版)-- 树和二叉树笔记_第42张图片
数据结构(C语言版)-- 树和二叉树笔记_第43张图片


7、总结

  1. 二叉树是―种最常用的树形结构,二叉树具有一些特殊的性质,而满二叉树和完全二叉树又是两种特殊形态的二叉树
  2. 二叉树有两种存储表示:顺序存储和链式存储。顺序存储就是把二叉树的所有结点按照层次顺序存储到连续的存储单元中,这种存储更适用于完全二叉树。链式存储又称二叉链表,每个结点包括两个指针,分别指向其左孩子和右孩子。链式存储是二叉树常用的存储结构。
  3. 树的存储结构有三种:双亲表示法、孩子表示法和孩子兄弟表示法,孩子兄弟表示法是常用的表示法,任意一棵树都能通过孩子兄弟表示法转换为二叉树进行存储。森林与二叉树之间也存在相应的转换方法,通过这些转换,可以利用二叉树的操作解决一般树的有关问题。
  4. 二叉树的遍历算法是其他运算的基础,通过遍历得到了二叉树中结点访问的线性序列,实现了非线性结构的线性化。根据访问结点的次序不同可得三种遍历:先序遍历、中序遍历、后序遍历,时间复杂度均为O(n)。
  5. 在线索二叉树中,利用二叉链表中的n+1个空指针域来存放指向某种遍历次序下的前驱结点和后继结点的指针,这些附加的指针就称为“线索”。引人二叉线索树的目的是加快查找结点前驱或后继的速度。
  6. 哈夫曼树在通信编码技术上有广泛的应用,只要构造了哈夫曼树,按分支情况在左路径上写代码0,右路径上写代码1,然后从上到下叶结点相应路径上的代码序列就是该叶结点的最优前缀码,即哈夫曼编码

8、例题与应用

数据结构(C语言版)-- 树和二叉树笔记_第44张图片
数据结构(C语言版)-- 树和二叉树笔记_第45张图片
数据结构(C语言版)-- 树和二叉树笔记_第46张图片

  1. 编程实现如下功能:
    (1)假设二叉树的结点值是字符型,根据输入的一棵二叉树的完整先序遍历序列(子树空用’#’表示),建立一棵以二叉链表存储表示的二叉树。
    (2)对二叉树进行先序、中序和后序遍历操作,并输出遍历序列,观察输出的序列是否与逻辑上的序列一致。
    (3)主程序中要求设计一个菜单,允许用户通过菜单来多次选择执行哪一种遍历操作。

测试样例:ABC##DE#G##F###

数据结构(C语言版)-- 树和二叉树笔记_第47张图片

数据结构(C语言版)-- 树和二叉树笔记_第48张图片

数据结构(C语言版)-- 树和二叉树笔记_第49张图片

#include 
using namespace std;

//二叉树的二叉线索存储表示
typedef struct BiNode
{
	char data;
	struct BiNode *lchild, *rchild;
} BiTNode, *BiTree;

//先序遍历的的顺序建立二叉链表
void CreateBiTree(BiTree &T)
{
	char ch;
	cin >> ch;
	if (ch == '#')
	{
		T = NULL;
	}
	else
	{
		T = new BiTNode;
		T->data = ch;
		CreateBiTree(T->lchild);
		CreateBiTree(T->rchild);
	}
}

//先序遍历的递归算法
void PreOrderTraverse(BiTree T)
{
	if (T)
	{
		cout << T->data;
		PreOrderTraverse(T->lchild);
		PreOrderTraverse(T->rchild);
	}
}

//中序遍历的递归算法
void InOrderTraverse(BiTree T)
{
	if (T)
	{
		InOrderTraverse(T->lchild);
		cout << T->data;
		InOrderTraverse(T->rchild);
	}
}

//后序遍历的递归算法
void PostOrderTraverse(BiTree T)
{
	if (T)
	{
		PostOrderTraverse(T->lchild);
		PostOrderTraverse(T->rchild);
		cout << T->data;
	}
}
int main(void)
{
	BiTree tree;
	int select;

	while (1)
	{
		cout << "1、建立二叉树\n";
		cout << "2、先序遍历\n";
		cout << "3、中序遍历\n";
		cout << "4、后序遍历\n";
		cout << "0、退出程序\n";

		cout << "\n请选择:";
		cin >> select;

		switch (select)
		{
		case 1:
			cout << "请输入建立二叉链表的序列:\n";
			CreateBiTree(tree);
			cout << endl;
			break;
		case 2:
			cout << "所建立的二叉链表先序序列:\n";
			PreOrderTraverse(tree);
			cout << endl
				 << endl;
			break;
		case 3:
			cout << "所建立的二叉链表中序序列:\n";
			InOrderTraverse(tree);
			cout << endl
				 << endl;
			break;
		case 4:
			cout << "所建立的二叉链表后序序列:\n";
			PostOrderTraverse(tree);
			cout << endl;
			break;
		case 0:
			return 0;
			break;
		}
	}
	return 0;
}
  1. 编程实现如下功能:
    (1)建立由英文字符组成的文件f1(字符种类≥10,长度≥100),并统计不同字符出现的次数;
    (2)按字符出现的次数对其建立哈夫曼树,并求出各个字符的哈夫曼编码;
    (3)读入要编码的文件f1,编码后存入另一个文件f2;
    (4)接着再调出编码后的文件f2,对其进行译码输出,最后存入文件f3。
f1.txt
TherearemomentsinlifewhenyoumisssomeonesomuchthatyoujustwanttopickthemfromyourdreamsandhugthemforrealDreamwhatyouwanttodreamgowhereyouwanttogobewhatyouwanttobebecauseyouhaveonlyonelifeandonechancetodoallthethingsyouwanttodo
f2.txt
110000011010011111010111111111010110011100001101110110001110001001101101000010011101000111101110100111011110011000101001101001111001110011100100001101110010110111110010000110101110101101000010101111000110011000101110000000101111000001101111111011000000100110000110100111010111000010000101001100111101001110110000111100110001011110100101111010111111001111100111110110010110100101110001000101001100111101001001110111101011111101000001000111101011111100111101110101111000110011000101110111111101100000010000101111010111111001111000110011011101001111101011110011000101110111111101100000010011000110000100101111011101011110001100110001011101111111011000000100001001011001001011110101111101011110001111001100010110101111001000001110010110100011001100101101101000010011101000111111101100101100101101111010110101111101111010101100010000101100111101000010000001010011000101001001101111000111100110011000101110111111101100000010000101100
f3.txt
TherearemomentsinlifewhenyoumisssomeonesomuchthatyoujustwanttopickthemfromyourdreamsandhugthemforrealDreamwhatyouwanttodreamgowhereyouwanttogobewhatyouwanttobebecauseyouhaveonlyonelifeandonechancetodoallthethingsyouwanttodo

数据结构(C语言版)-- 树和二叉树笔记_第50张图片

数据结构(C语言版)-- 树和二叉树笔记_第51张图片
数据结构(C语言版)-- 树和二叉树笔记_第52张图片

#include 
using namespace std;

typedef struct //定义哈夫曼树的结构
{
    int weight;
    int parent, lchild, rchild;
} HTNode, *HuffmanTree;

typedef char **HuffmanCode;

typedef struct //存储数据扫描统计结果
{
    char *data;    //字符
    int *quantity; //次数
    int length;    //总长度
} TNode;

void InitList(TNode &N) //初始化TNode定义的结点
{
    N.data = new char[256];
    N.quantity = new int[256];
    if (!N.data || !N.quantity)
        exit(1);
    N.length = 0;
}

int Find(TNode N, char ch)
{
    for (int i = 0; i < N.length; i++)
        if (ch == N.data[i])
            return true;
    return false;
}

void ReadFile(vector<char> &f) //读取文件
{
    char ch;
    ifstream infile("f1.txt", ios::in);
    if (!infile) //文件不存在
    {
        cout << "打开文件失败!" << endl;
        exit(1);
    }
    while (infile.peek() != EOF)
    {
        infile.get(ch);
        f.push_back(ch); //把字符ch推入vector
    }
    infile.close(); //关闭文件
    cout << "读取完成" << endl;
    system("pause");
}

void WriteTNode(vector<char> v, TNode &N) //将vector中的数据存入TNode结构体中
{
    char ch;
    int len = v.size(), j = 0;
    for (int i = 0; i < len; i++)
    {
        ch = v[i];
        if (!Find(N, ch))
        {
            N.data[j] = ch;
            N.quantity[j] = count(v.begin(), v.end(), ch);
            j++;
            N.length++;
        }
    }
    cout << "写入完成" << endl;
    system("pause");
}

void Select(HuffmanTree &HT, int n, int &min1, int &min2) //查找HT中未被使用的权值最小的两个点的下标
{
    min1 = min2 = 0; //初始化
    for (int i = 1; i < n; i++)
    {
        if (HT[i].parent != 0)
            continue; //略过已经加入的结点
        if (min1 == 0)
            min1 = min2 = i; //赋初值
        else if (HT[i].weight <= HT[min1].weight)
        { //min1是最小值
            min2 = min1;
            min1 = i;
        }
        else if (HT[i].weight < HT[min2].weight)
        { //min2是次小值
            min2 = i;
        }
        else if (HT[i].weight > HT[min2].weight)
        { //防止两个值相等
            if (min1 == min2)
                min2 = i;
        }
    }
}

void HuffmanCoding(HuffmanTree &HT, HuffmanCode &HC, TNode N) //建树函数
{
    int m, start, n = N.length;
    char *cd;
    unsigned int c, f;
    if (n <= 1)
        return;
    m = 2 * n - 1;
    HT = (HuffmanTree)malloc((m + 1) * sizeof(HTNode)); //0号单元未用 从1开始
    for (int i = 1; i <= n; i++)
    { //利用TNode将叶节点初始化
        HT[i].parent = 0;
        HT[i].lchild = HT[i].rchild = 0;
        HT[i].weight = N.quantity[i - 1];
    }
    for (int i = n + 1; i <= m; i++)
    { //初始化非叶节点
        HT[i].parent = HT[i].weight = 0;
        HT[i].lchild = HT[i].rchild = 0;
    }
    for (int i = n + 1; i <= m; i++)
    {                   //构建赫夫曼树
        int min1, min2; //选出最小的两个结点合并
        Select(HT, i, min1, min2);
        HT[i].weight = HT[min1].weight + HT[min2].weight;
        HT[i].lchild = min1;
        HT[i].rchild = min2;
        HT[min1].parent = HT[min2].parent = i;
    }
    //从叶子到根逆向求每个字符的赫夫曼编码
    HC = (HuffmanCode)malloc((n + 1) * sizeof(char *)); //分配n个字符编码的头指针向量
    cd = (char *)malloc(n * sizeof(char));              //分配求编码的工作空间
    cd[n - 1] = '\0';                                   //编码结束符
    for (int i = 1; i <= n; i++)
    {                  //逐个字符求赫夫曼编码
        start = n - 1; //编码结束符位置
        for (c = i, f = HT[i].parent; f != 0; c = f, f = HT[f].parent)
        { //从叶子到根逆向求编码
            if (HT[f].lchild == c)
                cd[--start] = '0';
            else
                cd[--start] = '1';
        }
        HC[i] = (char *)malloc((n - start) * sizeof(char)); //为第i个字符编码分配空间
        strcpy(HC[i], &cd[start]);                          //从cd复制编码(串)到HC
    }
    free(cd); //释放工作空间
    cout << "完成哈夫曼建树!" << endl;
    system("pause");
}

void ZipFile(HuffmanCode HC, vector<char> v, TNode N) //压缩文件
{
    int i = 0, j = 0, k = 0;
    ofstream outfile("f2.txt", ios::out);
    if (!outfile)
    { //文件为空
        cerr << "wrong open!!" << endl;
        exit(1);
    }
    for (i = 0; i < v.size(); i++)
    { //遍历vector容器
        for (j = 0; j < N.length; j++)
            if (N.data[j] == v[i])
                break;
        for (k = 0; HC[j + 1][k] != '\0'; k++)
            outfile << HC[j + 1][k];
    }
    outfile.close();
    cout << "压缩完成!" << endl;
    system("pause");
}

void RZipFile(HuffmanCode HC, TNode N)
{ //解压文件
    int flag, flag2 = 0, m = 0, i, j;
    char ch;
    char ch2[55];
    ofstream outfile("f3.txt", ios::out);
    ifstream infile("f2.txt", ios::in);
    if (!outfile)
    { //文件打开失败
        cerr << "打开错误!" << endl;
        exit(1); //运行错误,返回值1
    }
    if (!infile)
    {
        cerr << "打开错误!" << endl;
        exit(1); //运行错误,返回值1
    }
    while (infile.peek() != EOF)
    {
        flag = 0;
        char *cd = new char[N.length];
        for (i = 0;; i++)
        {
            infile >> ch;
            cd[i] = ch;
            cd[i + 1] = '\0';
            for (int j = 1; j <= N.length; j++)
            {
                if (strcmp(HC[j], cd) == 0)
                {
                    if (flag2 == 1)
                    {
                        ch2[m] = N.data[j - 1];
                        flag = 1;
                        m++;
                        delete cd;
                        break;
                    }
                    if (flag2 == 0)
                    {
                        outfile << N.data[j - 1];
                        flag = 1;
                        delete cd;
                        break;
                    }
                }
            }
            if (flag == 1)
                break;
        }
    }
    cout << "解压缩完成!" << endl; //提示完成解压缩
    system("pause");
}

void OutPutFile(vector<char> &f)
{
    char path[200];
    char ch;
    cout << "请输入您所要查询的文件路径" << endl;
    cin >> path;

    ifstream infile(path, ios::in);
    if (!infile)
    { //文件不存在
        cout << "打开错误!" << endl;
        exit(1);
    }
    while (infile.peek() != EOF)
    {
        infile.get(ch);  //读取字符赋值给ch
        f.push_back(ch); //把字符ch推入vector
    }
    infile.close();
    cout << "路径:" << path << "的文件如下" << endl;
    for (int i = 0; i < f.size(); i++)
        cout << f.at(i);
    cout << endl;
    f.clear();
    system("pause");
}

int main(void)
{
    int choose;
    vector<char> V, Vr;
    HuffmanTree HT;
    HuffmanCode HC;
    TNode N;
    InitList(N);
    while (1)
    {
        system("cls");
        cout << "1、压缩文件" << endl;
        cout << "2、解压文件" << endl;
        cout << "3、输出文件" << endl;
        cout << "0、  退出  " << endl;
        cout << "请输入您的操作:" << endl;
        cin >> choose;
        switch (choose)
        {
        case 1:
            cout << "*****进行压缩文件操作*****" << endl;
            ReadFile(V);              //从文件读取数据存入vector容器
            WriteTNode(V, N);         //将vector中的数据存储到NTode结构中
            HuffmanCoding(HT, HC, N); //将TNode中的数据存到哈夫曼树中并生成哈夫曼编码
            ZipFile(HC, V, N);        //压缩文件
            break;
        case 2:
            cout << "*****进行解压缩文件操作*****" << endl;
            RZipFile(HC, N); //对文件内容进行解码
            break;
        case 3:
            cout << "*****进行输出文件操作*****" << endl;
            OutPutFile(Vr); //将文件内容借助vector输出到终端
            break;
        case 0:
            return 0;
        default:
            cout << "请正确输入!";
            system("pause");
            break;
        }
    }
    return 0;
}

你可能感兴趣的:(数据结构,数据结构,c语言,树结构,二叉树,哈夫曼编码)