算法训练营 树的应用

  • 树是 n n n(0 ≤ \leq n n n)个节点的有限集合,当 n = 0 n = 0 n=0时,为空树;当 n > 0 n>0 n>0时,为非空树。
  • 任意一颗非空树,都满足:①有且仅有一个被称为根的节点;②除根节点外的其余节点可分为 m m m m > 0 m>0 m>0)个互不相交的有限集 T 1 T_{1} T1 T 2 T_{2} T2,…, T m T_{m} Tm,其中每一个集合本身又是一颗树,被称为根的子树。
  • 树的相关术语:
    • 节点:节点包含数据元素及若干指向子树的分支信息。
    • 节点的度:节点拥有的子树个数。
    • 树的度:树中节点的最大度数。
    • 终端节点:度为0的节点,又被称为叶子。
    • 分支节点:度大于0的节点。除了叶子,都是分支节点。
    • 内部节点:除了树根和叶子,都是内部节点
    • 节点的层次:从根到该节点的层数(根节点为第1层)。
    • 树的深度(或高度):所有节点中最大的层数。
    • 路径:树中两个节点之间所经过的节点序列。
    • 路径长度:两个节点之间路径上经过的边数。
    • 双亲、孩子:节点的子树的根被称为该节点的孩子,反之,该节点为其孩子的双亲。
    • 兄弟:双亲相同的节点互称兄弟。
    • 堂兄弟:双亲是兄弟的节点互称堂兄弟。
    • 祖先:即从该节点到树根经过的所有节点,被称为该节点的祖先。
    • 子孙:节点的子树中的所有节点都被称为该节点的子孙。
    • 有序树:节点的各子树从左至右有序,不能互换位置。
    • 无序树:节点的各子树可互换位置。
    • 森林:由 m m m(0 ≤ \leq m m m)棵不相交的树组成的集合。

树的存储

  • 树形结构是一对多的关系,除了树根,每个节点都有一个唯一的直接前驱(双亲);除了叶子,每个节点都有一个或多个直接后继(孩子)。

顺序存储

  • 顺序存储分为双亲表示法孩子表示法双亲孩子表示法
  • 双亲表示法。除了存储数据元素,还存储其双亲节点的存储位置下标,其中“-1”表示不存在。每个节点都有两个域:数据域data和双亲域parent。只记录了每个节点的双亲,无法直接得到该节点的孩子。
  • 孩子表示法。除了存储数据元素,还存储其所有孩子的存储位置下标。可以得到该节点的孩子,但是由于不知道每个节点到底有多少个孩子,因此只能按照树的度(树中节点的最大度)分配孩子空间,这样做可能会浪费很多空间。
  • 双亲孩子表示法。除了存储数据元素,还存储其双亲、所有孩子的存储位置下标。可以快速得到节点的双亲和孩子,但可能会浪费很多空间。

链式存储

  • 孩子链表表示法,类似于邻接表,表头包含数据元素和指向第1个孩子指针,将所有孩子都放入一个单链表中。
  • 孩子兄弟表示法,节点除了存储数据元素,还存储两个指针域:lchildrchild,称之为二叉链表。lchild存储第1个孩子的地址,rchild存储其右兄弟的地址。(将长子当做左孩子,将兄弟关系向右斜)

二叉树

  • 二叉树是 n ( 0 ≤ n ) n(0 \leq n) n(0n)个节点构成的集合,或为空树( n = 0 n = 0 n=0),或为非空树。对于非空树 T T T,要满足:①有且仅有一个被称为根的结点;②除了根结点,其余节点分为两个互不相交的子集 T 1 T_{1} T1 T 2 T_{2} T2,分别被称为 T T T的左子树和右子树,且 T 1 T_{1} T1 T 2 T_{2} T2本身都是二叉树。
  • 二叉树是种特殊的树,它最多有两个子树,分别为左子树右子树,二者是有序的,不可以互换。
  • 二叉树中不存在度大于2的节点。

二叉树的性质

  • 性质1:在二叉树的第 i i i层上至多有 2 i − 1 2^{i-1} 2i1个节点。
  • 性质2:深度为k的二叉树至多有2^{k}-1个节点。
  • 性质3:对于任何一棵二叉树,若叶子数为 n 0 n_{0} n0,度为2的节点数为 n 2 n_{2} n2,则 n 0 = n 2 + 1 n_{0} = n_{2}+1 n0=n2+1
  • 性质4:满二叉树:一棵深度为 k k k且有 2 k − 1 2^{k}-1 2k1个节点的二叉树。满二叉树的每一层都充满了节点,达到最大节点数。
  • 性质5:完全二叉树:除了最后一层,每一层都是满的(达到最大节点数),最后一层节点是从左向右出现的。
  • 性质6:具有 n n n个节点的完全二叉树的深度必为 ( l o g 2 n ) + 1 (log_{2}^{n})+1 (log2n)+1
  • 性质7:对于完全二叉树,若从上至下、从左至右编号,则编号为 i i i的节点,其左孩子编号必为 2 i 2i 2i,其右孩子编号必为 2 i + 1 2i+1 2i+1;其双亲编号必为 i / 2 i/2 i/2

例题1

一颗完全二叉树有 1001 1001 1001个节点,其中叶子节点的个数是多少?

答: 首先找到最后一个结点 1001 1001 1001的双亲节点,其双亲节点编号为 1001 / 2 = 500 1001/2 = 500 1001/2=500,该节点是最后一个拥有孩子的节点,其后面全是叶子,即 1001 − 500 = 501 1001-500 = 501 1001500=501个叶子。

例题2

一颗完全二叉树的第 6 6 6层有 8 8 8个叶子,则该完全二叉树最少有多少个节点,最多有多少个节点?

答: 节点最少的情况( 6 6 6层): 8 8 8个叶子在最后一层(第 6 6 6层),前 5 5 5层是满的,最少有 2 5 − 1 + 8 = 39 2^{5} - 1 + 8 = 39 251+8=39个节点;节点最多的情况( 7 7 7层):8个叶子在倒数第 2 2 2层(即第 6 6 6层),前 6 6 6层是满的,第 7 7 7层最少缺失 8 × 2 8 \times 2 8×2个节点,因为第 6 6 6层的 8 8 8个叶子如果生成孩子的话,会有 16 16 16个节点。最多有 2 7 − 1 − 16 = 111 2^{7}-1-16 = 111 27116=111个节点。

二叉树的存储结构

链式存储结构

  • 二叉链表节点的结构体定义:
typedef struct Bnode{
    int data;
    struct Bnode *lchild,*rchild; //左孩子指针,右孩子指针
}Bnode,*Btree; //类型名Bnode,指针Btree
  • 三叉链表节点的结构体定义:
typedef struct Bnode{
    int data;
    struct Bnode *lchild,*rchild,*parent; //左孩子指针,右孩子指针,双亲指针
}Bnode,*Btree; //类型名Bnode,指针Btree

二叉树的创建

询问法(算法步骤)
  • 输入节点信息,创建一个节点 T T T
  • 询问是否创建 T T T节点的左子树,如果是,则递归创建其左子树,否则其左子树为NULL。
  • 询问是否创建 T T T节点的右子树,如果是,则递归创建其右子树,否则其右子树为NULL。
#include<iostream>
using namespace std;
typedef struct Bnode{
    char data;
    struct Bnode *lchild,*rchild; //左孩子指针,右孩子指针
}Bnode,*Btree; //类型名Bnode,指针Btree
void createtree(Btree &T); //创建二叉树函数(询问法)
int main(){
    Btree cs;
    createtree(cs);
}
void createtree(Btree &T){
    char check; //判断是否创建左右孩子
    T = new Bnode;
    cout << "请输入节点数据:" << endl;
    cin >> T->data;
    cout << "是否填加"<< T->data <<"的左孩子? (Y/N)"<<endl; //询问创建T的左子树
    cin >> check;
    if(check == 'Y'){
        createtree(T->lchild);
    }
    else{
        T->lchild = NULL;
    }
    cout << "是否填加 "<< T->data <<"的右孩子? (Y/N)"<<endl; //询问创建T的右子树
    cin >> check;
    if(check == 'Y'){
        createtree(T->rchild);
    }
    else{
        T->rchild = NULL;
    }
}

输入:

请输入节点数据:
A
是否填加A的左孩子? (Y/N)
Y
请输入节点数据:
B
是否填加B的左孩子? (Y/N)
Y
请输入节点数据:
D
是否填加D的左孩子? (Y/N)
N
是否填加D的右孩子? (Y/N)
N
是否填加B的右孩子? (Y/N)
Y
请输入节点数据:
E
是否填加E的左孩子? (Y/N)
N
是否填加E的右孩子? (Y/N)
N
是否填加A的右孩子? (Y/N)
Y
请输入节点数据:
C
是否填加C的左孩子? (Y/N)
Y
请输入节点数据:
F
是否填加F的左孩子? (Y/N)
N
是否填加F的右孩子? (Y/N)
Y
请输入节点数据:
G
是否填加G的左孩子? (Y/N)
N
是否填加G的右孩子? (Y/N)
N
是否填加C的右孩子? (Y/N)
N
补空法(算法步骤)
  • 补空法指如果左子树或右子树为空,则用特殊字符补空,例如“#”。然后按照先序遍历的顺序,得到先序遍历序列,根据该序列递归创建二叉树。
#include<iostream>
using namespace std;
typedef struct Bnode{
    char data;
    struct Bnode *lchild,*rchild; //左孩子指针,右孩子指针
}Bnode,*Btree; //类型名Bnode,指针Btree
void createtree_bk(Btree &T); //创建二叉树函数(补空法)
int main(){
    Btree cs;
    createtree_bk(cs);
}
void createtree_bk(Btree &T){
    char ch;
    cin >> ch; //二叉树补空后,按先序遍历序列输入字符
    if(ch == '#'){
        T = NULL;
    }
    else{
        T = new Bnode;
        T->data = ch; //生产根节点
        createtree_bk(T->lchild); //递归创建左子树
        createtree_bk(T->rchild); //递归创建右子树
    }
}

输入:

ABD##E##CF#G###

二叉树遍历

先序遍历

  • 指先访问根,然后先序遍历,然后呢先序遍历左子树,再先序遍历右子树。
#include<iostream>
using namespace std;
typedef struct Bnode{
    char data;
    struct Bnode *lchild,*rchild; //左孩子指针,右孩子指针
}Bnode,*Btree; //类型名Bnode,指针Btree
void createtree_bk(Btree &T); //创建二叉树函数(补空法)
void preorder(Btree T); //先序遍历
int main(){
    Btree cs;
    createtree_bk(cs);
    preorder(cs);
}

void createtree_bk(Btree &T){
    char ch;
    cin >> ch; //二叉树补空后,按先序遍历序列输入字符
    if(ch == '#'){
        T = NULL;
    }
    else{
        T = new Bnode;
        T->data = ch; //生产根节点
        createtree_bk(T->lchild); //递归创建左子树
        createtree_bk(T->rchild); //递归创建右子树
    }
}

void preorder(Btree T){
    if(T){
        cout << T->data << " ";
        preorder(T->lchild);
        preorder(T->rchild);
    }
}

中序遍历

  • 指遍历左子树,然后访问根,再中序遍历右子树。
#include<iostream>
using namespace std;
typedef struct Bnode{
    char data;
    struct Bnode *lchild,*rchild; //左孩子指针,右孩子指针
}Bnode,*Btree; //类型名Bnode,指针Btree
void createtree_bk(Btree &T); //创建二叉树函数(补空法)
void preorder(Btree T); //先序遍历
void inorder(Btree T); //中序遍历
int main(){
    Btree cs;
    createtree_bk(cs);
    preorder(cs);
    cout << endl;
    inorder(cs);
}

void createtree_bk(Btree &T){
    char ch;
    cin >> ch; //二叉树补空后,按先序遍历序列输入字符
    if(ch == '#'){
        T = NULL;
    }
    else{
        T = new Bnode;
        T->data = ch; //生产根节点
        createtree_bk(T->lchild); //递归创建左子树
        createtree_bk(T->rchild); //递归创建右子树
    }
}

void preorder(Btree T){
    if(T){
        cout << T->data << " ";
        preorder(T->lchild);
        preorder(T->rchild);
    }
}

void inorder(Btree T){
    if(T){
        inorder(T->lchild);
        cout << T->data << " ";
        inorder(T->rchild);
    }
}

输入:

ABD##E##CF#G###

输出:

A B D E C F G
D B E A F G C

后序遍历

  • 指遍历左子树,后序遍历右子树,然后访问根。
#include<iostream>
using namespace std;
typedef struct Bnode{
    char data;
    struct Bnode *lchild,*rchild; //左孩子指针,右孩子指针
}Bnode,*Btree; //类型名Bnode,指针Btree
void createtree_bk(Btree &T); //创建二叉树函数(补空法)
void preorder(Btree T); //先序遍历
void inorder(Btree T); //中序遍历
void posorder(Btree T); //后序遍历
int main(){
    Btree cs;
    createtree_bk(cs);
    preorder(cs);
    cout << endl;
    inorder(cs);
    cout <<endl;
    posorder(cs);
}

void createtree_bk(Btree &T){
    char ch;
    cin >> ch; //二叉树补空后,按先序遍历序列输入字符
    if(ch == '#'){
        T = NULL;
    }
    else{
        T = new Bnode;
        T->data = ch; //生产根节点
        createtree_bk(T->lchild); //递归创建左子树
        createtree_bk(T->rchild); //递归创建右子树
    }
}

void preorder(Btree T){
    if(T){
        cout << T->data << " ";
        preorder(T->lchild);
        preorder(T->rchild);
    }
}

void inorder(Btree T){
    if(T){
        inorder(T->lchild);
        cout << T->data << " ";
        inorder(T->rchild);
    }
}

void posorder(Btree T){
    if(T){
        posorder(T->lchild);
        posorder(T->rchild);
        cout << T->data << " ";
    }
}

输入:

ABD##E##CF#G###

输出:

A B D E C F G
D B E A F G C
D E B G F C A

层次遍历

  • 按照层次的顺序从左向右进行遍历。
#include<iostream>
#include <queue>

using namespace std;
typedef struct Bnode{
    char data;
    struct Bnode *lchild,*rchild; //左孩子指针,右孩子指针
}Bnode,*Btree; //类型名Bnode,指针Btree
void createtree_bk(Btree &T); //创建二叉树函数(补空法)
bool Leveltraverse(Btree T); //层次遍历
int main(){
    Btree cs;
    createtree_bk(cs);
    Leveltraverse(cs);
}

void createtree_bk(Btree &T){
    char ch;
    cin >> ch; //二叉树补空后,按先序遍历序列输入字符
    if(ch == '#'){
        T = NULL;
    }
    else{
        T = new Bnode;
        T->data = ch; //生成根节点
        createtree_bk(T->lchild); //递归创建左子树
        createtree_bk(T->rchild); //递归创建右子树
    }
}

bool Leveltraverse(Btree T){
    Btree p;
    if(!T){
        return false;
    }
    queue<Btree>Q; //创建一个普通队列(先进先出),里面存放指针类型
    Q.push(T); //根指针入队
    while(!Q.empty()) //如果队列不空
    {
        p=Q.front();//取出队头元素作为当前扩展结点livenode
        Q.pop(); //队头元素出队
        cout<<p->data<<"  ";
        if(p->lchild){
            Q.push(p->lchild); //左孩子指针入队
        }
        if(p->rchild){
            Q.push(p->rchild); //右孩子指针入队
        }
    }
    return true;
}

输入:

ABD##E##CF#G###

输出:

A  B  C  D  E  F  G

遍历序列还原树

  • 根据遍历序列可以还原这棵树,包括二叉树还原树还原森林还原,三种还原方式。

二叉树还原

  • 由二叉树的先序和中序序列,可以唯一地还原一颗二叉树。注意:由二叉树的先序和后序序列不能唯一地还原一颗二叉树。
#include<iostream>
#include <cstring>
using namespace std;
typedef struct Bnode{
    char data;
    struct Bnode *lchild,*rchild; //左孩子指针,右孩子指针
}BiTNode,*BiTree; //类型名Bnode,指针Btree
BiTree pre_mid_createBiTree(char *pre,char *mid,int len); //由先序、中序还原建立二叉树
void posorder(BiTree T); //遍历二叉树,以后序序列输出
int main(){
    char xxxl[] = {'A','B','D','E','C','F','G'}; //先序序列
    char zxxl[] = {'D','B','E','A','F','G','C'}; //中序序列
    int len = strlen(xxxl); //字符数组长度
    BiTree cs = pre_mid_createBiTree(xxxl,zxxl,len);
    posorder(cs); //以后序序列输出
}
BiTree pre_mid_createBiTree(char *pre,char *mid,int len){
    if(len == 0){
        return NULL;
    }
    char ch = pre[0]; //先序序列中的第1个节点,作为根
    int index = 0; //在中序序列中查找根结点,并用index记录查找长度
    while(mid[index] != ch){ //在中序序列中查找根结点,左边为该节点的左子树
        index++;
    }
    BiTree T = new BiTNode ; //创建根节点
    T->data = ch;
    T->lchild = pre_mid_createBiTree(pre+1,mid,index); //创建左子树
    T->rchild = pre_mid_createBiTree(pre+index+1, mid+index+1,len-index-1); //创建右子树
    return T;
}

void posorder(BiTree T){
    if(T){
        posorder(T->lchild);
        posorder(T->rchild);
        cout << T->data << " ";
    }
}

输出:

D E B G F C A
  • 由二叉树的中序和后序序列,也可以唯一地还原一颗二叉树。
#include<iostream>
#include <cstring>
using namespace std;
typedef struct Bnode{
    char data;
    struct Bnode *lchild,*rchild; //左孩子指针,右孩子指针
}BiTNode,*BiTree; //类型名Bnode,指针Btree
BiTree pre_mid_createBiTree(char *last,char *mid,int len); //由先序、中序还原建立二叉树
void preorder(BiTree T); //遍历二叉树,以后前序列输出
int main(){
    char hxxl[] = {'D','E','B','G','F','C','A'}; //后序序列
    char zxxl[] = {'D','B','E','A','F','G','C'}; //中序序列
    int len = strlen(hxxl);
    BiTree cs = pre_mid_createBiTree(hxxl,zxxl,len);
    preorder(cs); //以先序序列输出
}
BiTree pre_mid_createBiTree(char *last,char *mid,int len){
    if(len == 0){
        return NULL;
    }
    char ch = last[len-1]; //找到后序序列中的最后一个节点,作为根
    int index = 0; //在中序序列中查找根结点,并用index记录查找长度
    while(mid[index] != ch){ //在中序序列中查找根结点,左边为该节点的左子树,右边为右子树
        index++;
    }
    BiTree T = new BiTNode ; //创建根节点
    T->data = ch;
    T->lchild = pre_mid_createBiTree(last,mid,index); //创建左子树
    T->rchild = pre_mid_createBiTree(last+index, mid+index+1,len-index-1); //创建右子树
    return T;
}

void preorder(BiTree T){
    if(T){
        cout << T->data << " ";
        preorder(T->lchild);
        preorder(T->rchild);
    }
}

输出:

A B D E C F G
  • 先序遍历、中序遍历还原二叉树秘籍:先序找根,中序分左右
  • 后序遍历、中序遍历还原二叉树秘籍:后序找根,中序分左右

树还原

  • 由于树的先根遍历、后根遍历与其对应二叉树的先序遍历、中序遍历相同,因此可以根据该对应关系,先还原为二叉树,然后把二叉树转换为树。

训练1 新二叉树

输入一颗二叉树,输出其先序遍历序列。

输入:第1行为二叉树的节点数 n ( 1 ≤ n ≤ 26 ) n(1 \leq n \leq 26) n(1n26)。后面的 n n n行,以每一个字母为节点,后两个字母分别为其左、右孩子。对空节点用*表示。

输出:输出二叉树的先序遍历序列。

算法思路

  • 用静态存储方式,存储每个节点的左、右孩子,然后按先序遍历顺序输出
#include<iostream>
using namespace std;
string s;
int n,root,l[100],r[100];
void preorder(int t); //先序遍历
int main(){
    cin >> n;
    for (int i = 0; i < n; ++i) {
        cin >> s;
        if(!i) {
            root = s[0] - 'a';
        }
            l[s[0]-'a'] = s[1] - 'a';
            r[s[0]-'a'] = s[2] - 'a';
    }
    preorder(root);
    return 0;
}
void preorder(int t){
    if(t != '*'-'a'){
        cout << char(t + 'a');
        preorder(l[t]);
        preorder(r[t]);
    }
}

输入:

6
abc
bdi
cj*
d**
i**
j**

输出:

abdicj

训练2:还原树

题目描述

小瓦伦丁非常喜欢玩二叉树。她最喜欢的游戏是根据二叉树节点的大写字母随机构造二叉树。

为了记录她的树,她为每棵树都写下两个字符串:一个先序遍历(根、左子树、右子树)和一个中序遍历(左子树、根、右子树)。

输入:输入包含一个或多个测试用例。每个测试用例都包含一行,其中包含两个字符串,表示二叉树的先序遍历和中序遍历。两个两个字符串都由唯一的大写字母组成。

输出:对于每个测试用例,都单行输出该二叉树的后序遍历序列(左子树、右子树、根)

算法设计

  • 只需在还原二叉树的同时,输出后序序列即可。根据先序序列找根,以中序序列划分左、右子树。
#include <iostream>
#include <cstring>
using namespace std;
string preorder,inorder;
void postorder(int l1, int l2, int n);
int main(){
    while(cin >> preorder >> inorder){
        int len = preorder.size();
        postorder(0,0,len);
        cout << endl;
    }
    return 0;
};
void postorder(int l1, int l2, int n){
    if(n <= 0){
        return;
    }
    int len = inorder.find(preorder[l1])-l2;
    postorder(l1+1,l2,len);
    postorder(l1+len+1,l2+len+1,n-len-1);
    cout << preorder[l1];
}

输入:

DBACEGF ABCDEFG
BCAD CBAD

输出:

ACBFGED
CDAB

训练3:树

题目描述

确定给定二叉树中的一个叶子节点,使从根到叶子路径上的节点权值之和最小。

节点权值:二叉树中的权值就是对叶子结点赋予的一个有意义的数量值。

输入:输入包含二叉树的中序遍历和后序遍历。从输入文件中读取两行(直到文件结束)。第1行包含与中序遍历相关联的值序列,第2行包含与后序遍历相关联的值的序列。所有值均不同,都大于零且小于10000。假设没有二叉树超过10000个节点或少于1个节点。

输出:对于每棵二叉树,都输出值最小的路径上叶子节点的值。如果多条路径的值最小,则选择叶子节点值最小的路径。

算法设计

  • 根据二叉树的后序序列确定树根;然后根据中序序列划分左、右子树,还原二叉树;最后进行先序遍历,查找从根到叶子权值之和最小的叶子节点的权值。
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
const int maxn=10000+5;
int inorder[maxn],postorder[maxn],lch[maxn],rch[maxn];
int n,minv,minsum;
int createtree(int l1,int l2,int m); //由遍历序列创建二叉树
bool readline(int *a); //读入遍历序列,中间有空格
void findmin(int v,int sum);
int main(){
    while(readline(inorder)){ //读入中序序列
        readline(postorder); //读入后序序列
        createtree(0,0,n);
        minsum = 0x7fffffff;
        findmin(postorder[n-1],0);
        cout << minv << endl;
    }
    return 0;
};
int createtree(int l1,int l2,int m){
    if(m<=0){
        return 0;
    }
    int root = postorder[l2+m-1];
    int len = 0;
    while(inorder[l1+len]!=root){ //计算左子树的长度
        len++;
    }
    lch[root] = createtree(l1,l2,len);
    rch[root] = createtree(l1+len+1,l2+len,m-len-1);
    return root;
}

bool readline(int *a){
    string line;
    if(!getline(cin,line)){
        return false;
    }
    stringstream s(line);//可以用于分割被空格、制表符等符号分割的字符串
    n = 0;
    int x;
    while(s >> x){
        a[n++] = x;
    }
    return  n>0;
}

void findmin(int v,int sum){
    sum+=v;
    if(!lch[v]&&!rch[v]){//叶子
        if(sum < minsum || (sum == minsum && v<minv)){
            minv = v;
            minsum = sum;
        }
    }
    if(lch[v]){ //v有左子树
        findmin(lch[v],sum);
    }
    if(rch[v]){ //v有右子树
        findmin(rch[v],sum);
    }
}

输入:

3 2 1 4 5 7 6
3 1 2 5 6 7 4
7 8 11 3 5 16 12 18
8 3 11 7 16 18 12 5
255
255

输出:

1
3
255

哈夫曼树

  • 哈夫曼编码基本思想:以字符的使用频率作为权值构建一棵哈夫曼树,然后利用哈夫曼树对字符进行编码。
  • 构造一棵哈夫曼树,将所要编码的字符作为叶子节点,将该字符在文件中的使用评率作为叶子节点的权值,以自底向上的方式,通过 n − 1 n-1 n1次的“合作”运算后够着的树。其核心思想是让权值大的叶子离根最近

算法设计

  1. 确定合适的数据结构。
  • 在哈夫曼树中,如果没有度为1的节点,则一棵有 n n n个叶子节点的哈夫曼树共有 2 n − 1 2n-1 2n1个节点( n − 1 n-1 n1次的“合并”,每次都产生一个新节点)。
  • 构成哈夫曼树后,编码需要从叶子节点出发走一条从叶子到根的路径。译码需要从根出发走一条从根到叶子的路径。那么对于每个节点而言,需要知道每个节点的权值双亲左孩子右孩子节点的信息。
  1. 初始化。构造 n n n棵节点为 n n n个字符的单节点树集合 T = { t 1 , t 2 , t 3 , . . . , t n } T=\{t_{1},t_{2},t_{3},...,t_{n}\} T={t1,t2,t3,...,tn},每棵树只有一个带权的根节点权值为该字符的使用评率。

  2. 如果在 T T T中只剩下一颗树,则哈夫曼树构造成功,跳到第6步。否则,从集合T中取出没有双亲且权值最小的两颗树 t i t_{i} ti t j t_{j} tj,将它们合并成一颗新树 z k z_{k} zk,新树的左孩子为 t i t_{i} ti,右孩子为 t j t_{j} tj z k z_{k} zk的权值为 t i t_{i} ti t j t_{j} tj的权值之和。

  3. 从集合 T T T中删去 t i t_{i} ti t j t_{j} tj,加入 z k z_{k} zk

  4. 重复第3~4步。

  5. 约定左分支上的编码为“0”,右分支上的编码为“1”。从叶子节点到根节点逆向求出每个字符的哈夫曼编码。那么从根节点到叶子节点路径上的字符组成的字符串为该叶子节点的哈夫曼编码,算法结束。

算法实现

  1. 数据结构。每个节点的结构都包括权值、双亲、左孩子、右孩子、节点字符信息五个域。节点结构体形式:
typedef struct {
    double weight; //权值
    int parent; //双亲
    int lchild; //左孩子
    int rchild; //右孩子
    char value; //该节点表示的字符
} HNodeType;
  • 在结构体的编码过程中,bit[]存放结点的编码,start记录编码开始时的下标,在逆向编码存储时,start n − 1 n-1 n1开始依次递减,从后向前存储;当读取时,从start+1开始到 n − 1 n-1 n1,从前向后输出,即该字符的编码。编码结构体如下:
typedef struct{
    int bit[MAXBIT]; //存储编码的数组
    int start; //编码开始下标
} HCodeType;
  1. 初始化。初始化哈夫曼树数组HuffNode[]中的节点权值为0,双亲和左、右孩子均为-1,然后读入叶子节点的权值。

  2. 循环构造哈夫曼树。从集合 T T T中取出双亲为-1且权值最小的两棵树 t i t_{i} ti t j t_{j} tj,将它们合并成一棵新树 z k z_{k} zk,新树的左孩子为 t i t_{i} ti,右孩子为 t j t_{j} tj z k z_{k} zk的权值为 t i t_{i} ti t j t_{j} tj的权值之和。

  3. 输出哈夫曼编码。

程序

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
#define MAXBIT    100
#define MAXVALUE  10000
#define MAXLEAF   30
#define MAXNODE   MAXLEAF*2 -1
typedef struct
{
    double weight;
    int parent;
    int lchild;
    int rchild;
    char value;
} HNodeType;        /* 节点结构体 */
typedef struct
{
    int bit[MAXBIT];
    int start;
} HCodeType;        /* 编码结构体 */
HNodeType HuffNode[MAXNODE]; /* 定义一个节点结构体数组 */
HCodeType HuffCode[MAXLEAF];/* 定义一个编码结构体数组*/
void HuffmanTree (HNodeType HuffNode[MAXNODE],  int n);/* 构造哈夫曼树 */
void HuffmanCode(HCodeType HuffCode[MAXLEAF],  int n);/* 哈夫曼树编码 */
int main()
{
    int i,j,n;
    cout<<"Please input n:"<<endl;
    cin>>n;
    HuffmanTree(HuffNode,n);  //构造哈夫曼树
    HuffmanCode(HuffCode,n);  // 哈夫曼树编码
    //输出已保存好的所有存在编码的哈夫曼编码
    for(i=0;i<n;i++)
    {
        cout<<HuffNode[i].value<<": Huffman code is: ";
        for(j=HuffCode[i].start+1;j<n;j++)
            cout<<HuffCode[i].bit[j];
        cout<<endl;
    }
    return 0;
}
void HuffmanTree (HNodeType HuffNode[MAXNODE],  int n){
    int i, j; //循环变量
    int x1, x2; //构造哈夫曼树不同过程中两个最小权值节点的权值
    double m1,m2; //构造哈夫曼树不同过程中两个最小权值节点在数组中的序号
    /* 初始化存放哈夫曼树数组 HuffNode[] 中的节点 */
    for (i=0; i<2*n-1;i++)
    {
        HuffNode[i].weight=0;//权值
        HuffNode[i].parent=-1;
        HuffNode[i].lchild=-1;
        HuffNode[i].rchild=-1;
    }
    /* 输入 n 个叶子节点的权值 */
    for (i=0; i<n; i++)
    {
        cout<<"Please input value and weight of leaf node "<<i+1<<endl;
        cin>>HuffNode[i].value>>HuffNode[i].weight;
    }
    /* 构造 Huffman 树 */
    for (i=0; i<n-1; i++)
    {//执行n-1次合并
        m1=m2=MAXVALUE;
        /* m1、m2中存放两个无父节点且节点权值最小的两个节点 */
        x1=x2=0;
        /* 找出所有节点中权值最小、无父节点的两个节点,并合并之为一棵二叉树 */
        for (j=0;j<n+i;j++)
        {
            if (HuffNode[j].weight<m1&&HuffNode[j].parent==-1)
            {
                m2 = m1;
                x2 = x1;
                m1 = HuffNode[j].weight;
                x1 = j;
            }
            else if (HuffNode[j].weight < m2 && HuffNode[j].parent==-1)
            {
                m2=HuffNode[j].weight;
                x2=j;
            }
        }
        /* 设置找到的两个子节点 x1、x2 的父节点信息 */
        HuffNode[x1].parent  = n+i;
        HuffNode[x2].parent  = n+i;
        HuffNode[n+i].weight = m1+m2;
        HuffNode[n+i].lchild = x1;
        HuffNode[n+i].rchild = x2;
        cout<<"x1.weight and x2.weight in round "<<i+1<<"\t"<<HuffNode[x1].weight<<"\t"<<HuffNode[x2].weight<<endl; /* 用于测试 */
    }
}

void HuffmanCode(HCodeType HuffCode[MAXLEAF],  int n){
    HCodeType cd;       /* 定义一个临时变量来存放求解编码时的信息 */
    int i,j,c,p;
    for(i=0;i<n;i++)
    {
        cd.start=n-1;
        c=i;
        p=HuffNode[c].parent;
        while(p!=-1)
        {
            if(HuffNode[p].lchild==c)
                cd.bit[cd.start]=0;
            else
                cd.bit[cd.start]=1;
            cd.start--;        /*前移一位 */
            c=p;
            p=HuffNode[c].parent;    /* 设置下一循环条件 */
        }
        /* 把叶子节点的编码信息从临时编码cd中复制出来,放入编码结构体数组 */
        for (j=cd.start+1; j<n; j++)
            HuffCode[i].bit[j]=cd.bit[j];
        HuffCode[i].start=cd.start;
    }
}

训练1:围栏修复

题目描述

约翰想修牧场周围的篱笆,需要 N N N块( 1 ≤ N ≤ 20000 1 \leq N \leq 20000 1N20000)木板,每块木板都具有整数长度 L i L_{i} Li 1 ≤ L i ≤ 50000 1 \leq L_{i} \leq 50000 1Li50000)米。他购买了一块足够长的木板(长度为 L i L_{i} Li的总和, i = 1 , 2 , . . . , N i = 1,2,...,N i=1,2,...,N),以便得到 N N N块木板。切割时木屑损失的长度不计。
农夫唐向约翰收取切割费用。切割一块木板的费用与其长度相同。切割长度为21米的木板需要21美分。唐让约翰决定切割木板的顺序和位置。约翰知道以不同的顺序切割木板,将会产生不同的费用。帮助约翰确定他得到N块木板的最低金额。

输入:第1行包含一个整数N,表示木板的数量。第2~N+1行,每行都包含一个所需木板的长度L_{i}。

输出:一个整数,即进行N-1次切割的最低花费。

算法设计

  • 类似哈夫曼树的构造方法,每次都选择两个最小的合并,直到合并为一棵树。每次合并的结果就是切割的费用。使用优先队列时,每次都弹出两个最小值 t 1 t_1 t1 t 2 t_2 t2 t = t 1 + t 2 t = t_{1} + t_{2} t=t1+t2 s u m + = t sum+=t sum+=t,将 t t t入队,继续,直到队空。sum为所需花费。
#include<iostream>
#include <queue>
int main(){
    using namespace std;
    long long sum;
    int n,t,t1,t2;
    while(cin >> n){
        priority_queue<int,vector<int>,greater<int>>q; //从最小值开始弹出
        for (int i = 0; i < n; ++i) {
            cin >> t;
            q.push(t);
        }
        sum = 0;
        if(q.size() == 1){
            t1 = q.top();
            sum+=t1;
            q.pop();
        }
        while(q.size()>1){
            t1 = q.top(),q.pop();
            t2 = q.top(),q.pop();
            t = t1 + t2;
            sum += t;
            q.push(t);
        }
        cout << sum << endl;
    }
    return 0;
}

输入:

3
8
5
8

输出:

34

训练2:信息熵

题目描述

熵编码是一种数据编码方法,通过对去除“冗余”或“额外”信息的消息进行编码来实现无损数据压缩。为了能够恢复信息,编码字形的为位模式不允许作为任何其他编码位模式的前缀。

输入:输入文件跑酷哦一个字符串列表,每行一个。字符串将只包含大写字母、数字字符和下划线(用于替代空格)。以字符串“END”结尾,不应处理此行。

输出:对于每个字符串,都输出8位ASCII编码的位长度、最佳无前缀可变长度编码的位长度及精确到一个小数点的压缩比;

算法设计

  • 根据字符串统计每个字符出现的频率。
  • 根据频率构造哈夫曼树,计算总编码长度。
#include<iostream>
#include <queue>
#include <string>
#include <cstring>

int main(){
    using namespace std;
    int a[27];
    string s;
    while(1){
        cin >> s;
        if(s == "END"){
            break;
        }
        memset(a,0,sizeof(a)); //数组初始化
        int n = s.size();
        for (int i = 0; i < n; ++i) {
            if(s[i] == '_'){
                a[26]++;
            }
            else{
                a[s[i]-'A']++;
            }
        }
        priority_queue<int,vector<int>,greater<int> >q; //优先队列
        for (int i = 0; i <= 26; ++i) {
            if(a[i]){ //将数组中非零元素入队
                q.push(a[i]);
            }
        }
        int ans = n;
        while(q.size()>2){
            int t,t1,t2;
            t1 = q.top(),q.pop();
            t2 = q.top(),q.pop();
            t = t1+t2;
            ans += t;
            q.push(t);
        }
        printf("%d %d %.1lf\n",n*8,ans,(double)n*8/ans);
    }
    return 0;
}

输入:

AAAAABCD
THE_CAT_IN_THE_HAT
END

输出:

64 13 4.9
144 51 2.8

训练3:转换哈夫曼编码

题目描述

静态哈夫曼编码是一种主要用于文本压缩的编码算法。给定一个由 N N N各不同字符组成的特定长度的文本,算法选择 N N N个编码,每个不同的字符都对应一个编码。使用这些编码压缩文本,当选择编码算法构建一个具有 N N N个叶子的二叉树时,对于 N ≤ 2 N \leq 2 N2,树的构建流程如下

  1. 对于文本中的每个不同字符,都构建一个仅包含单个节点的树,其权值为该字符在文本中出现的次数。

  2. 构建一个包含上述 N N N棵树的集合 S S S

  3. S S S包含多于一棵树时:①选择最小的权值 t 1 ∈ S t_{1} \in S t1S,并将其从 S S S中删除;②选择最小的权值 t 2 ∈ S t_{2} \in S t2S,并将其从 S S S中删除;③构建一棵新树 t t t t 1 t_{1} t1为其左子树, t 2 t_{2} t2为其右子树,t的权值为 t 1 t_{1} t1 t 2 t_{2} t2权值之和;④将 t t t加入 S S S集合。

  4. 返回保留在S中唯一一棵树

根据算法选择的N个代码的长度,找所有字符总数的最小值。

输入:输入包含多个测试用例,每个测试用例的第1行都包含一个整数 N ( 2 ≤ N ≤ 50 ) N(2 \leq N \leq 50) N(2N50),表示在文本中出现的不同字符数。第2行包含 N N N个整数 L i ( 1 ≤ L i ≤ 50 , i = 1 , 2 , . . . , N ) L_{i}(1 \leq L_{i} \leq 50,i = 1,2,...,N) Li(1Li50i=1,2,...,N),表示由哈夫曼算法生成的不同字符的编码长度。假设至少存在一棵由上述算法构建的树,那么可以生成具有给定长度的编码。

输出:对每个测试用例都输出一行,表示所有字符总数的最小值。

算法设计

  1. 在每一层都用一个深度数组,deep[]记录该层节点的权值,将该层每个节点的权值都初始化为0,等待推测权值。

  2. 根据输入的编码长度算出最大长度,即哈夫曼树的最大深度maxd

  3. 从最大深度maxd向上计算并推测,直到树根。开始时temp= 1

  • i = maxd:第i层的节点权值如果为0,则被初始化为temp。对i层从小到大排序,然后将第i层每两个合并,将权值放入上一层(i-1层)。更新temp为第i层排序后的最后一个元素(最大元素)。

  • i = maxd-1:重复上述操作。

  • i= 0:结束,输出第0层第1个元素。

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

int main(){
    int n,x;
    while(cin>>n){
        vector<long long>deep[n+1];
        for(int i=0;i<n;i++)
            deep[i].clear();
        int maxd=0;
        for(int i=0;i<n;i++){
            cin>>x;
            deep[x].push_back(0);
            maxd=max(maxd,x);//求最大深度 
        }
        long long temp=1;
        for(int i=maxd;i>0;i--){
            for(int j=0;j<deep[i].size();j++)
                if(!deep[i][j])
                    deep[i][j]=temp;//将第i层最大的元素值赋值给i-1层没有权值的节点 
            sort(deep[i].begin(),deep[i].end());//第i层排序 
            for(int j=0;j<deep[i].size();j+=2)
                deep[i-1].push_back(deep[i][j]+deep[i][j+1]);//合并后放入上一层 
            temp=*(deep[i].end()-1);//取第i层的最后一个元素,即第i层最大的元素 
        }
        cout<<*deep[0].begin()<<endl;//输出树根的权值
    }
    return 0;
}

输入:

2
1 1
4
2 2 2 2
10
8 2 4 7 5 1 6 9 3 9

输入:

2
4
89

训练4:可变基哈夫曼编码

题目描述

哈夫曼编码是一种最优编码方法。根据已知源字母表中字符的出现频率,将源字母表中的字符编码为目标字母表中的字符,最优的意思是编码信息的平均长度最小。在该问题中需要将 N N N个大写字母(源字母 S 1 . . . S N S_{1}...S_{N} S1...SN、频率 f 1 、 f N f_{1}、f_{N} f1fN)转换成 R R R进制数字(目标字母 T 1 . . . T R T_{1}...T_{R} T1...TR

输入:输入将包含一个或多个数据集,每行一个。每个数据集都包含整数值R( 2 ≤ R ≤ 10 2 \leq R \leq 10 2R10)、整数值N( 2 ≤ N ≤ 26 2 \leq N \leq 26 2N26)和整数频率 f 1 . . . f N f_{1} ...f_{N} f1...fN,每个都为1~999。整个输入数据都以 R R R为0结束,它不被认为是单独的数据集。

输出:对每个数据集都在单行上显示其编号(编号从1开始按顺序排列)和平均目标符号长度(四舍五入到小数点后两位)。然后显示 N N N个源字母和相应的哈夫曼代码,每行都有一个字母和代码。在每个测试用例后都打印一个空行。

算法设计

  1. 先补充虚拟字符,使N = k \times (R-1)+R,k为整数,即(N-R)%(R-1) = 0。
  2. 每个节点都包含frequency、va、id这三个域,分别表示频率、优先值、序号。
  3. 定义优先级。频率越小越优先,如果频率相等,则值越小越优先。
  4. 将所有节点都加入优先队列。
  5. 构建可变基哈夫曼树。
  6. 进行可变基哈夫曼编码。
#include<iostream>
#include<vector>
#include<algorithm>
#include <queue>
#include <cstring>
using namespace std;
struct node{
    int freq,va,id; //频率,优先值,序号
    node(int x = 0, int y = 0, int z = 0){ //构造函数
        freq = x;
        va = y;
        id = z;
    }
    bool operator < (const node &b) const{ //重载运算符<
        if(freq == b.freq){
            return va>b.va;
        }
        return freq>b.freq;
    }
};
const int maxn = 100;
int R,N; //基数,字母个数
int n,c; //补虚拟字母后的个数,重新生成字母编号
int fre[maxn],father[maxn],code[maxn];
priority_queue<node>Q; //优先队列
int main(){
    int cas = 1;
    while(cin >> R && R){
        cin >> N;
        memset(fre,0, sizeof(fre)); //初始化数组fre
        int total = 0;
        for (int i = 0; i < N; ++i) {
            cin >> fre[i];
        }
        n = N;
        while((n-R) % (R-1) != 0){ //补虚拟节点
            n++;
        }
        while(!Q.empty()){ //优先队列清空
            Q.pop();
        }
        for (int i = 0; i < n; ++i) { //将所有节点都入队
            Q.push(node(fre[i],i,i));
        }
        c = n; //重新合成节点编号
        int rec = 0; //统计所有频率和值
        while(Q.size()!=1){ //构建哈夫曼树,剩余一个节点停止合并
            int sum = 0,minva = n;
            for (int i = 0; i < R; ++i) {
                sum += Q.top().freq; //统计频率和
                minva = min(Q.top().va,minva); //求最小优先级
                father[Q.top().id] = c; //记录双亲
                code[Q.top().id] = i; //记录编码
                Q.pop(); //出队
            }
            Q.push(node(sum,minva,c)); //新节点入队
            c++;
            rec+=sum;
        }
        c--;
        printf("Set %d; average length %0.2f\n",cas,1.0*rec/total);
        for (int i = 0; i < N; ++i) { //哈夫曼编码
            int cur = i;string s;
            while(cur!=c){
                s.push_back(code[cur]+'0');
                cur = father[cur];
            }
            reverse(s.begin(),s.end()); //翻转编码,转换为从根到叶子编码
            cout << "  "<<char('A' + i) << ": " << s << endl;
        }
        cout << endl;
        cas++;
    }
    return 0;
}

输入:

2 5 5 10 20 25 40
2 5 4 2 2 1 1
3 7 20 5 8 5 12 6 9
4 6 10 23 18 25 9 12
0

输出:

Set 1; average length 2.10
  A: 1100
  B: 1101
  C: 111
  D: 10
  E: 0

Set 1; average length 2.20
  A: 11
  B: 00
  C: 01
  D: 100
  E: 101

Set 1; average length 1.69
  A: 1
  B: 00
  C: 20
  D: 01
  E: 22
  F: 02
  G: 21

Set 1; average length 1.32
  A: 32
  B: 1
  C: 0
  D: 2
  E: 31
  F: 33
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

羽星_s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值