《数据结构》(C++)之第五章:树和二叉树

5.1 树的逻辑结构

5.1.1 树的定义和基本术语
1、树的定义
  • 结点:树中的数据元素

  • :是n个结点的有限集合(当n=0时,称为空树

    • 1⃣ 有且仅有一个特定的称为“”的结点
    • 2⃣ 当n > 1时,除根结点之外的其余结点被分成m(m > 0)个 互不相交 的有限集合T1、T2、…、Tm,其中每个集合又是一棵树,并称为这个根结点的子树树的定义是递归的
      • 每棵子树只能有一个根结点

    《数据结构》(C++)之第五章:树和二叉树_第1张图片

2、树的基本术语
  • 结点的度、树的度

    • 结点的度:某结点所拥有的 (直接)子树 的个数称为该结点的度
    • 树的度:树中个各结点度的最大值称为该树的度
  • 叶子结点、分支结点

    • 叶子结点(终端结点):度为0的结点(没有直接子节点)
    • 分支结点(非终端结点):度不为0的结点
  • 孩子结点、双亲结点、兄弟结点

    • 孩子结点:某结点的子树的根结点(即该结点的直接子结点)称为该结点的孩子结点
    • 双亲结点:反之,该结点称为其孩子结点的双亲结点
    • 兄弟结点:具有同一个双亲的孩子结点互称为兄弟结点
  • 路径、路径长度

    • 路径:如果树的结点序列n1、n2、…、nk满足如下关系:结点ni是结点ni+1的双亲(1 <= i < k),则把n1、n2、…、nk称为一条由n1至nk的路径

      • 路径的方向只能是父节点到子节点
      • 在树中,路径是唯一的
    • 路径长度:路径经过的边数称为路径长度

  • 祖先、子孙

    • 祖先/子孙:如果从结点x到结点有一条路径,那么x就称为y的祖先,y就称为x的子孙
  • 结点的层数、树的深度(高度)

    • 层:规定根结点的层数为1,对其余任何结点,若某结点在第k层,则其孩子结点在第k+1层

    • 树的深度(高度):树中所有结点的最大层数称为树的深度/高度

  • 层序编号

    • 层序编号:将树中结点按照 从上层到下层,同层从左到右 的次序一次给它们编以从1开始的连续自然数,树的这种编号方式称为层序编号
      • 通过层序编号可以将一棵树变为线性序列
  • 有序树、无序树

    • 有序树/无序树:如果有一棵树中的结点的各子树 从左到右 是有次序的,称这棵树为有序树;反之称为无序树

      • 讨论有序树时,若交换了结点各子树的相对位置,则构成不同的树;无序树则仍为同一棵树
      • 一般讨论的是有序树
  • 森林

    • 森林:m(m >= 0)棵互不相交的树的集合构成森林
      • 前序遍历森林即为从左到右依次前序遍历森林中的每一棵树
5.1.3 树的遍历操作
  • 树的遍历(树中最基本的操作):指从根结点出发,使得每个结点被访问一次且仅被访问一次
1、前序遍历(由上到下,深度优先
若树为空,则空操作返回
    否则:
        (1)先访问根结点
        (2)然后按照从左到右的顺序(由上到下)前序遍历根结点的每一棵子树
2、后序遍历(由下到上
若树为空,则空操作返回
    否则:
        (1)首先按照从左到右的顺序(由下到上)后序遍历根结点的每一棵子树
        (2)然后访问根结点
        
3、层序遍历(由上到下,广度优先
  • 又称“树的广度遍历”
    • 从树的第一层(即根结点)开始,自上而下 逐层遍历
    • 在同一层中,按 从左到右 的顺序对结点逐个访问

5.2 树的存储结构

5.2.1 双亲表示法
  • 存储方式:一维数组,数据元素为树中对应的一个结点(一般按层序存储)

    • 实质:静态链表
  • 表示:

    data parent
    • data:数据域,存储树中结点的数据信息
    • parent:指针域,存储该结点的双亲在数组中的下标
      • parent为-1表示该结点无双亲结点,即根结点
    private:
        DataType data;    //树中结点的数据信息
        int parent;       //该结点的双亲在数组中的下标
    
  • 原理:树中每个结点都 有且仅有 一个双亲结点

  • 优缺点

    优点 便于查找双亲结点和根结点
    缺点 不能反映各兄弟结点之间的关系
5.2.2 孩子表示法
  • 存储方式:基于链表
1、多重链表表示法
  • 定义:链表中每个结点包括 一个数据域和多个指针域,每个指针域指向该结点的一个孩子结点

  • 表示:

    • 法1: 各结点不同构(不易实现),指针域的个数等于 各个结点的度

      data degree child1 child2 childx
      • data:数据域,存放该结点的数据信息
      • drgree:度域,存放该结点的度
      • child:指针域,指向该结点的孩子结点
    • 法2: 各结点同构,指针域的个数等于 树的度

      data child1 child2 childx
      • data:数据域
      • child:指针域,指向该结点的孩子结点
  • 缺点:对于法2,链表中各结点同构,存在存储空间浪费的问题

    • 一棵有n个结点的度为k的树中有n x (k - 1) + 1个空链域
    • 适用于各结点度相差不大的情况

    《数据结构》(C++)之第五章:树和二叉树_第2张图片

2、孩子链表表示法
  • 定义:数组+单链表

    • 数组存储每个结点
    • 单链表存储每个结点的孩子结点
  • 表示:

    • (1)孩子结点

      child next
      • child:该孩子结点在存放表头结点的数组中的下标
      • next:该结点的下一个(从左到右)兄弟结点
    • (2)表头结点

      data firstChild
      • data:每个结点的数据域
      • firstChild:指向该结点的第一个(从左到右)孩子结点

    《数据结构》(C++)之第五章:树和二叉树_第3张图片

5.2.3 双亲孩子表示法
  • 定义:孩子链表表示法 + 双亲表示法

    • 孩子链表表示法加上parent域存储该结点的双亲结点在数组中的下标

    《数据结构》(C++)之第五章:树和二叉树_第4张图片

5.2.4 孩子兄弟表示法(二叉链表表示法)
  • 定义:链表中的每个结点除数据域外,还设置了两个指针分别指向该结点的第一个孩子和右兄弟

  • 表示:

    firstChild data rightSib
    • firstChild:指针域,存储该结点的第一个孩子结点的存储地址
    • data:数据域,存储该结点的数据信息
    • rightSib:指针域,存储该结点的右兄弟结点的存储地址

    《数据结构》(C++)之第五章:树和二叉树_第5张图片

5.3 二叉树的逻辑结构

5.3.1 二叉树的定义
  • 定义:二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为 根结点的左子树和右子树

  • 特点:

    • (1)每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点
    • (2)二叉树是有序的,其次序不能任意颠倒
      • 即使树中的某个结点只有一棵子树,也要区分它是左子树还是右子树
  • 二叉树的五种基本形态:

    • (1)空二叉树
    • (2)只有一个根结点
    • (3)根结点只有左子树
    • (4)根结点只有右子树
    • (5)根结点既有左子树又有右子树
1、斜树
  • 斜树:始终向单个方向延伸,每一层只有一个结点(所以斜树的结点个数与其深度相同)
    • 左斜树:所有结点都只有左子树的二叉树
    • 右斜树:所有结点都只有右子树的二叉树
2、满二叉树同级中叶子结点最多
  • 定义:在一棵二叉书中,满足

    • (1)所有分支结点都存在左子树和右子树
    • (2)**所有叶子结点都在同一层上*
  • 特点:

    • (1)叶子只能出现在最下一层
    • (2)只有度为0和度为2的结点

    《数据结构》(C++)之第五章:树和二叉树_第6张图片

3、完全二叉树
  • 定义:对一棵具有n个结点的二叉树 按层序 编号,如果编号i(1 <= i <= n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中的位置完全相同,则这棵二叉树称为完全二叉树

  • 特点:

    • (1)叶子结点只能出现在最下两层,且最下层的叶子结点都集中在二叉树 左侧连续 的位置
    • (2)如果有度为1的结点,只可能有1个,且该结点只有左孩子
    • 满二叉树一定是完全二叉树

    《数据结构》(C++)之第五章:树和二叉树_第7张图片

5.3.2 二叉树的基本性质
  • 性质1:二叉树 的底i层上最多有 2的i-1次方 个结点(i >= 1)

    • 归纳法
  • 性质2:在一棵深度为k的 二叉树 中,最多有 2的k次方 - 1 个结点(满二叉树),最少有 k 个结点(斜树)

    • 性质1各层求和
  • 性质3:在一棵 二叉树 中,如果叶子结点的个数为n0,度为2的结点个数为n2,则 n0 = n2 + 1

    • 设n为二叉树中的结点数,B为二叉树中的分支数
      n = n0 + n1 + n2          //式1
      
      n = B + 1                 //除根结点外,每个结点都只有一个分支进入
      B = n1 + 2*n2             //度为1的结点射出1个分支,度为2的结点射出2个分支
      
      n = n1 + 2*n2 + 1         //式2
      
      n0 = n2 + 1               //综合式1、式2得出结论
      
  • 性质4:具有n个结点的 完全二叉树 ,假设其深度为k,则 k =「log以2为底的n」+ 1 (「」表示向下取整)

    • 最后一层的结点序号从左到右第一个为 2的k-1次方 ,最后一个为 2的k次方-1

      2的k-1次方 <= n < 2的k次方     //取对数后,向下取整得出结论
      
  • 性质5:对一棵具有n个结点的 完全二叉树 中的结点从1开始按层序编号,则对于任意的编号为i(1 <= i <= n)的结点有

    • (1)如果 i > 1 ,则结点i的 双亲 的编号为 i/2

      • 否则结点i是根结点,无双亲
    • (2)如果 2i =< n,则结点i的 左孩子 的编号为 2i

      • 否则结点i无左孩子
    • (3)如果 2i + 1 =< n,则结点i的 右孩子 的编号为 2i+1

      • 否则结点i无右孩子
5.3.4 二叉树的遍历操作
  • 特点:已知 中序 + 前序/后序 可唯一确定二叉树
1、前序遍历
若二叉树为空,则空操作返回
    否则:
        (1)访问根结点                     //只有“访问”时才返回值
        (2)前序遍历根结点的左子树           //存在递归
        (3)前序遍历根结点的右子树
2、中序遍历
若二叉树为空,则空操作返回
    否则:
        (1)中序遍历根结点的左子树            //存在递归
        (2)访问根结点
        (3)中序遍历根结点的右子树            //存在递归
3、后序遍历
若二叉树为空,则空操作返回
    否则:
        (1)后序遍历根结点的左子树             //存在递归
        (2)后序遍历根结点的右子树             //存在递归
        (3)访问根结点
4、层序遍历
  • 从上到下,从左往右

  • 实质上是 广度优先 遍历

5.4 二叉树的存储结构及实现

5.4.1 顺序存储结构
  • 存储方式:一维数组

    • 原理:将二叉树按 完全二叉树 编号,使数组下标对应该编号(即在数组中的位置是固定的
      • 可能需要添加部分不存在的为空结点以模拟完全二叉树
      • 添加的为空的结点会造成空间浪费
  • 步骤:

    • (1)将二叉树按完全二叉树编号
    • (2)将二叉树中的结点以编号的顺序存储到一维数组中
  • 应用场景:一般仅适合存储完全二叉树

5.4.2 二叉链表(最常使用)
  • 结点结构:

    lchild data rchild
    • data:数据域
    • lchild:左指针域,存放指向左孩子的指针,当做孩子不存在时为空指针
    • rchild:右指针域,存放指向右孩子的指针,当右孩子不存在时为空指针
1、前序遍历递归算法
void BiTree::PreOrder(BiNode * bt)      //* bt为根指针
{
    if (bt == NULL) return;         //递归调用的结束条件
    else {
        cout << bt -> data;         //访问根结点bt的数据域
        PreOrder(bt -> lchild);     //前序递归遍历bt的左子树
        PreOrder(bt -> rchild);     //前序递归遍历bt的右子树
    }
}
2、中序遍历递归算法
void BiTree::InOrder(BiNode * bt)
{
    if (bt == NULL) return;         //递归调用的结束条件
    else {
        InOrder(bt -> lchild);      //中序递归遍历bt的左子树
        cout << bt -> data;         //访问根结点bt的数据域
        InOrder(bt -> rchild);      //中序递归遍历bt的右子树
    }
}
3、后序遍历递归算法
void BiTree::PostOrder(BiNode * bt)
{
    if(bt == NULL) return;          //递归调用的结束条件
    else {
        PostOrder(bt -> lchild);    //后序递归遍历bt的左子树
        PostOrder(bt -> rchild);    //后续递归遍历bt的右子树
        cout << bt -> data;         //访问根结点bt的数据域
    }
}
4、层序遍历
  • 结构:队列

  • 思想:设置一个队列存放已访问的结点。遍历从二叉树的根结点开始,首先将 根指针 入队,然后从队头取出一个元素并执行下列操作

    • (1)访问该指针所指结点
    • (2)若该指针所指结点的左、右孩子结点非空,则将其左孩子指针和右孩子指针入队
  • 算法实现:

void BiTree::LevelOrder() 
{
    front = rear = -1;                  //队列Q初始化(采用顺序队列,并假定不会发生上溢)
    if (root == NULL) return;           //二叉树为空,算法结束
    Q[++rear] = root;                   //根指针入队
    
    while(front != rear)
    {
        q = Q[++front];                 //队头元素出队
        
        cout << q -> data;              //访问结点q的数据域
        
        if (q -> lchild != NULL)        //若结点q存在左孩子,则将左孩子指针入队
        Q[++rear] = q -> lchild
        
        if (q -> rchild != NULL)        //若结点q存在右孩子,则将右孩子指针入队
        Q[++rear] = q -> rchild;
    }
}
5、构造函数
  • 构造 扩展二叉树 :将每一个结点的空指针引出一个虚结点

    • 扩展二叉树的一个遍历序列就能 唯一确定 一棵二叉树
  • 扩展二叉树建立二叉链表的算法实现:

    • 首先输入根结点,若输入的是“#”字符,则表明该二叉树为空树,即bt = NULL;
    • 否则输入的字符应该赋给bt -> data,之后依次递归建立它的左子树和右子树

??? 域前加*

BiNode * BiTree::Create(BiNode * bt)
{
    cin >> ch;                          //输入结点的数据信息,假设为字符
    
    if (ch == '#') bt = NULL;           //建立一棵空树(将parent的对应指针域置空)
    
    else{
    
        bt = new BiNode;                //生成一个结点,数据域为输入的ch(假设为前序遍历)
        bt -> data = ch;   
        
        bt -> lchild = Creat(bt -> lchild); //递归建立左子树
        bt -> rchild = Creat(bt -> rchild); //递归建立右子树
    }
}
6、析构函数
  • 方法:后序遍历

    • 原因:释放某结点时,要确保该结点的左右子树都已释放
  • 算法实现:

void BiTree::Release(BiNode * bt)
{
    if (bt != NULL) {
        Release(bt -> lchild);          //释放左子树
        Release(bt -> rchild);          //释放右子树
        delete bt;                      //释放根结点
    }
}
5.4.3 三叉链表
  • 定义:在二叉链表结点结构的基础上 增加parent域指向父节点

    lchild data rchild parent
5.4.4 线索链表
  • 定义:在二叉链表中,若lchild和rchild为空,则可将其指向二叉树 在某种遍历序列 中的前驱结点或后继结点

  • 结构:

    ltag lchild data rchild rtag
    • ltag:

      含义
      0 lchild指向该结点的左孩子
      1 lchild指向该结点的前驱结点
    • rtag:

      含义
      0 rchild指向该结点的右孩子
      1 rchild指向该结点的后继结点
  • 代码实现

    struct ThrNode
    {
        DataType data;
        ThrNode * lchild, * rchild;
        flag ltag, rtag;
    }
    

5.5 二叉树遍历的非递归算法(递推算法

  • 使用栈结构实现
5.5.1 前序遍历非递归算法
  • 关键点:前序遍历过某结点的整个左子树后,如何找到该结点的右子树的根指针

  • 二叉树的根指针bt值的两种情况:

    • (1)若bt != null,则表明当前的二叉树不空,此时,应输出根结点bt的值并将bt保存到栈中,准备继续遍历bt的左子树
    • (2)若bt = null,则表明以bt为根指针的二叉树遍历完毕,并且bt是栈顶指针所指结点的左子树
      • 若栈不空,则应根据栈顶指针所指结点找到待遍历右子树的根指针并赋予bt,以继续遍历下去
      • 若栈空,则表明整个二叉树遍历完毕,应结束
void BiTree::PreOrder(BiNode * bt)
{
    top = -1;                               //采用顺序栈,并假定不会发生上溢
    while (bt != NULL || top != -1)         //bt为空且栈也为空时才退出循环
    {
        while (bt != NULL)
        {
            cout << bt -> data;             //访问bt指向的结点
            s[++top] = bt;                  //将根指针bt入栈
            bt = bt -> lchild;              //继续遍历bt指向结点的左子树
        }
        
        if (top != -1)                      //栈非空
        {
            bt = s[top--];                  //将栈顶元素出栈赋值给bt
            bt = bt -> rchild;              //继续遍历bt指向结点的右子树
        }
        
    }
}
5.5.2 中序遍历非递归算法
  • 关键点:在遍历过程中遇到某结点时并不能立即访问它,而是将它压栈,等到它的左子树遍历完毕后,再从栈中弹出并访问之
    • 算法实现与前序遍历的区别只是将访问操作(即cout操作)放在了遍历右子树之前
void BiTree::PreOrder(BiNode * bt)
{
    top = -1;                               //采用顺序栈,并假定不会发生上溢
    while (bt != NULL || top != -1)         //bt不为空且栈也不为空时才退出循环
    {
        while (bt != NULL)
        {
            s[++top] = bt;                  //将根指针bt入栈
            bt = bt -> lchild;              //继续遍历bt指向结点的左子树
        }
        
        if (top != -1)                      //栈非空
        {
            bt = s[top--];                  //将栈顶元素出栈赋值给bt
            cout << bt -> data;             //访问bt指向的结点
            bt = bt -> rchild;              //继续遍历bt指向结点的右子树
        }
        
    }
}
5.5.3 后序遍历非递归算法
  • 关键点:在后序遍历中,结点要 入两次栈,出两次栈

    出栈情况 含义
    第一次出栈 只遍历完左子树,右子树尚未遍历,则该结点不出栈,利用栈顶结点找到它的右子树,准备遍历它的右子树
    第二次出栈 遍历完右子树,将该结点出栈,并访问它
    • 为了区别同一个结点的两次出栈,设置标志 flag
      flag值 含义
      flag = 1 第一次出栈,只遍历完左子树,该结点不能访问
      flag = 0 第二次出栈,遍历完右子树,该结点可以访问
  • 二叉树的根指针bt值的两种情况:

    • bt != null,则bt及标志flag(置为1)入栈,遍历其左子树
    • bt == null,此时
      • 若栈空,则整个遍历结束
      • 若栈不空,则表明栈顶即诶但的左子树或右子树已遍历完毕
      • 若栈顶结点的标志flag = 1,则表明栈顶结点的左子树已遍历完毕,将flag修改为2
      • 若栈顶结点的标志flag = 2,则表明栈顶结点的右子树也遍历完毕,输出栈顶结点
  • 代码实现:

void BiTree::PostOrder(BiNode * bt)
{
    top = -1;                                   //采用顺序栈s,并假定栈不会发生上溢
    
    while(bt != null || top != -1)              //bt为空且栈s也为空时才退出循环
    {
        while(bt != null)
        {
            top++;                          
            s[top].ptr = bt;                    //根结点bt入栈(ptr暂存bt用于遍历右子树或再次将左子树入栈)
            s[top].flag = 1;                    //更新flag值为1
            
            bt = bt -> lchild;                  //bt指向原根结点的左子树
        }
        
        while(top != -1 && s[top].flag == 2)    //当栈s非空且栈顶元素的标志等于2时,出栈并输出栈顶结点
        {
            bt = s[top--].ptr;                  //出栈(两次出栈都在这里,第二次时的top--避免死循环)
            cout << bt -> data;
        }
    }
}

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

1、树转换为二叉树
  • 引出:树的孩子兄弟表示法与二叉树的二叉链表的物理结构相同

《数据结构》(C++)之第五章:树和二叉树_第8张图片

  • 方法:

    步骤 方法 具体操作
    1 加线 树中所有相邻兄弟结点之间加一条连线
    2 去线 对树中的每个结点,只保留它与第一个孩子结点之间的连线,删除它与其他孩子结点之间的连线
    3 层次调整 以根结点为轴心,将树顺时针转动一定的角度,使之层次分明
    • 规则:右边的兄弟当作右儿子

    • 特点:

      • (1)在二叉树中,左分支上的各结点在原来的树中是父子关系右分支上的各结点在原来的树中是兄弟关系

      • (2)二叉树根结点的右子树必定为空(由于转换前树的根结点没有兄弟

  • 转换前后的遍历关系:

    转换前 转换后
    树的前序遍历 二叉树的前序遍历
    树的后序遍历 二叉树的中序遍历
2、森林转换为二叉树
  • 思想:将森林中的每棵树转换为二叉树,再将每棵树的根结点视为兄弟

  • 方法:

    • (1)将森林中的每棵树转换成二叉树
    • (2)从第二课二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树根结点的 右孩子
3、二叉树转换为树或森林
  • 转换成树/森林的判定依据:二叉树根结点有无右子树

  • 方法:

    步骤 方法 具体操作
    1 加线 若某结点x是其双亲y的左孩子,则把结点x的右孩子、右孩子的右孩子、…,都与结点y用线连接起来
    2 去线 删去原二叉树中所有的双亲结点与右孩子结点的连线
    3 层次调整 整理由(1)(2)两步所得到的树或森林,使之层次分明

5.7 哈夫曼树及哈夫曼编码

5.7.1 概念
  • 叶子结点的权值:叶子结点的权值是对叶子结点赋予的一个有意义的数值量

  • 二叉树的带权路径长度:设二叉树具有n个带权值的叶子结点,从根结点到各个叶子结点的路径长度与相应叶子结点权值的乘积之和叫做二叉树的带权路径长度(是对整个带权二叉树的范围求和

  • 哈夫曼树:给定一组具有确定权值的叶子结点,可以构造出不同的二叉树,将其中带权路径长度最小的二叉树称为哈夫曼树(又称 最优二叉树

5.7.2 基本思想及算法
  • 要点:一棵二叉树要使其带权路径长度最小,必须 使权值越大的叶子结点越靠近根结点,而权值越小的叶子结点越远离根结点

  • 基本思想:

    步骤 方法 具体操作
    1 初始化 由给定的n个权值{w1、w2、…、wn} 构造 只有一个根结点的二叉树,从而得到一个二叉树 集合 F={T1,T2,…,Tn}
    2 选取与合并 在F中选取根结点的 权值最小 的两颗二叉树分别作为左右子树,构造一棵新的二叉树,这棵新二叉树的根结点的权值为其左、右子树根结点的权值之和
    3 删除与加入 在F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到F中
    4 重复2、3 当集合F中只剩下一棵二叉树时,这棵二叉树便是哈夫曼树
    • 哈夫曼树的左右子树的顺序没有要求
    • 选取过程中可能有权值相同的根结点,因此 哈夫曼树可能不唯一 ,但它们具有相同的带权路径长度

《数据结构》(C++)之第五章:树和二叉树_第9张图片

  • 特点:

    • (1)在哈夫曼算法构造的哈夫曼树中,非叶子结点的度均为2
    • (2)具有n个叶子结点的哈夫曼树共有 2n-1 个结点
      • n:n个叶子结点
      • n-1:在n-1次合并过程中生成的结点
  • 存储结构:数组huffTree[2n-1]

    weight lchild rchild parent
    • weight:权值域,保存该结点的权值
    • lchild:指针域(游标),保存该结点的左孩子结点在数组中的下标
    • rchild:指针域(游标),保存该结点的右孩子结点在数组中的下标
    • parent:指针域(游标),保存该结点的双亲结点在数组中的下标
  • 过程:首先将n个权值的叶子结点存放到数组huffTree的前n个分量中,然后不断将两颗子树合并为一棵子树,并将新子树的根结点顺序存放到数组huffTree的前n个分量的后面

    • 判断一个结点是否已加入到哈夫曼树中:parent域的值为其双亲结点在数组中的下标
    • 最后parent = -1的为根结点
  • 算法:将n个叶子的权值保存在w[n]中

    • 伪代码:
      1、数组huffTree初始化,所有数组元素的双亲、左右孩子都置为-1
      2、数组huffTree的前n个元素的权值置给定权值
      3、进行n-1次合并
          3.1、在二叉树集合中选取两个权值最小的根结点,其下标分别为i1、i2;
          3.2、将i1、i2合并为一棵新的二叉树k
      

    《数据结构》(C++)之第五章:树和二叉树_第10张图片

    • 代码:三个循环
      void HuffmanTree(element huffTree[], int w[], int n) {
          
          //初始化所有结点均没有双亲和孩子(置-1)
          for (i = 0; i < 2 * n - 1; i++) {   
              huffTree[i].parent = -1;
              huffTree[i].lchild = -1;
              huffTree[i].rchild = -1;
          }
          
          //构造n棵只含有根结点的二叉树
          for (i = 0; i < n; i++) {
              hufTree[i].weight = w[i]
          }
          
          //n-1次合并
          for (k = n; k < 2 * n - 1; k++) {   //k为全局变量
              
              //查找权值最小的两个根结点,并将其下标的值赋给全局变量i1,i2
              Select(huffTree, i1, i2);    
              
              huffTree[i1].parent = k;
              huffTree[i2].parent = k;
              
              huffTree[k].weight = hufTree[i1].weight + huffTree[i2].weight;
              
              huffTree[k].lchild = i1;
              huffTree[k].rchild = i2;
          }
      }
      
5.7.3 哈夫曼编码
  • 编码:在进行程序设计时,通常给每一个字符标记一个单独的代码来表示一组字符,我们称之为编码

    • 解码:将代码向字符翻译
  • 不等长编码:根据字符出现的频率不同,让 出现频率高的字符尽可能使用较短的编码 ,以减少用于区分不同字符使用的代码位数

    • 解码的唯一性问题的解决:如果一组编码中任一编码都不是其他任何一个编码的前缀,则称这组编码为 前缀编码
  • 构造哈夫曼编码树:设需要编码的字符集为{d1, d2, … ,dn}作为叶子结点,它们在字符串中出现的频率{w1, w2, …, wn}作为为叶子结点的权值,构造一棵哈夫曼树

    • 规定哈夫曼编码树的 左分支代表0,右分支代表1

    • 作用:哈夫曼树棵用于构造 最短的不等长编码 方案

    • 哈夫曼编码:则从根结点到每个叶子结点所经过的路径组成的0和1的序列便为该叶子结点所对应字符的编码,称为哈夫曼编码

    • 优点:

      序号 优点 含义
      1 使字符串的编码总长度最短 哈夫曼编码树中,树的带权路径长度的含义是 各个字符的码长与出现次数的乘积之和 ,所以采用哈夫曼树构造的编码是一种能 使字符串的编码总长度最短 的不等长编码
      2 保证了解码的唯一性 哈夫曼树的每个字符结点都是叶子结点,不可能在根结点到其他字符结点的路径上,保证了前缀编码,保证了解码的唯一性

你可能感兴趣的:(数据结构)