树,二叉树,二叉树遍历,哈夫曼树(详解+刷题)

👂 后街男孩经典之精选 - 歌单 - 网易云音乐

👂 年轮(电视剧《花千骨》男声版插曲) - 汪苏泷 - 单曲 - 网易云音乐

目录

🌼5.1 -- 树

🌼5.2 -- 二叉树

1,性质

2,存储

3,创建

🌼5.3 -- 二叉树遍历

(1)先序

(2)中序

(3)后续

(4)层序

(5)还原树

⚽刷题

🦎P1305 新二叉树

🦎Tree Recovery

🦎Tree

🌼5.4 -- 哈夫曼树

1,前置知识 

2,实现

3,分析

4,优化

⚽刷题

🦎围栏修复

🦎信息熵

🦎转换哈夫曼编码


🌼5.1 -- 树

1,树:除根节点,分为 m 个 互不相交的有限集

2,节点的度:节点拥有的子树个数

3,由于树中没有环,树中任意两个节点的路径是唯一的

4,有序树:节点的各子树,从左至右有序,不能互换位置

5,树 -- 顺序存储 -- 3种方式👇

#图解 数据结构:树的存储结构 - 知乎 (zhihu.com)

双亲表示法,无法直接得到该节点的孩子;孩子表示法,由于不知道每个节点到底有多少个孩子,只能按照树的度(节点的最大度)分配孩子空间,极大浪费空间双亲孩子表示法,浪费空间

6,树 -- 链式存储 -- 2种方式

二叉树的链式存储 - C语言学习教程_C语言程序_ c语言编程_ c语言入门 (itheima.net)

(1)孩子链表表示法:

类似邻接表,表头包含数据元素data和指向第一个孩子的指针,所有孩子都放入一个单链表中,单链表中的节点记录该节点的下标和下一个节点的地址。

(2)孩子兄弟表示法:

类似二叉链表,节点存储数据域data和2个指针域(lchild和rchild),lchild存储第1个孩子地址,rchild存储右兄弟地址。

7,树,森林和二叉树的转换

(1)森林和二叉树    (2)树和二叉树

森林、树与二叉树相互转换-阿里云开发者社区 (aliyun.com)

兄弟关系在右斜线上,长子是左孩子👆

8,总结

由于在普通的树中,每个节点子树个数不同,存储和运算都比较困难

(比如:不知道每个节点有多少个孩子,或者,知道用书中节点最大度分配空间,造成浪费)

因此实际应用中,将树 / 森林转换为二叉树(进行存储和运算)

二者存在唯一对应关系,不影响结果

🌼5.2 -- 二叉树

1,性质

(1)二叉树第 i 层至多有 2^{i+1} 个节点(某一层)

(2)深度为 k 的二叉树至多有 2^{k} - 1 个节点(总共)

(3)任何一棵二叉树,叶子数 n_{0}^{} ,度为2的节点数 n_{2}^{} ,那么 n_{2}^{}+ 1 =  n_{0}^{} 

-- 度表示节点拥有的子树个数 --

(4)具有 n 个节点的完全二叉树,深度为 \left \lfloor \log n \right \rfloor + 1

(5)完全二叉树,若从上至下,从左至右编号,编号为 i 的节点,其左孩子编号为 2*i,右孩子编号为 2*i + 1,双亲编号为 i / 2(下取整)

2,存储

完全二叉树  -- 顺序存储👇

二叉树的顺序存储结构(看了无师自通) (biancheng.net)

普通二叉树 -- 链式存储

(顺序存储需要补充为完全二叉树(补0),浪费空间)

链式存储分为二叉链表 和 三叉链表👇

二叉树的存储结构 二叉链表 三叉链表 - papering - 博客园 (cnblogs.com)

3,创建

树的应用 —— 二叉树的创建_创建二叉树_Ding Jiaxiong的博客-CSDN博客

👆就是《算法训练营入门篇》的原文,下面是代码👇

1,询问法

先序遍历顺序,每次输入节点信息后,都询问是否创建该节点的 左 / 右 子树(递归创建)

不创建则置空(NULL)

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);elseT->lchild = NULL;cout<<"是否添加 "<<T->data<<"的右孩子? (Y/N)"<<endl; //询问创建T的右子树cin>>check;if(check == "Y")createtree(T->rchild);elseT->rchild = NULL;
}

2,补空法

为空则用特殊字符补空(比如"#"),依然是先序遍历和递归创建

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

🌼5.3 -- 二叉树遍历

(1)先序

访问根,先序遍历左子树,左子树为空(或已遍历)才可以先序遍历右子树

void preorder(Btree T) //先序遍历
{if(T) {cout<<T->data<<" ";preorder(T->lchild);preorder(t->rchild);}
}

(2)中序

中序遍历左子树,左子树为空才可以访问根,最后中序遍历右子树

void inorder(Btree T) //中序遍历
{if(T) { //该节点不为空inorder(T->lchild); //中序遍历左子树cout<<T->data<<" "; //访问根节点inorder(t->rchild); //中序遍历右子树}
}

(3)后续

后续遍历左子树,后续遍历右子树,左右子树为空才可以先访问根

void posorder(Btree T) //后序遍历
{if(T) { //该节点不为空posorder(T->lchild); //后序遍历左子树posorder(t->rchild); //后序遍历右子树cout<<T->data<<" "; //访问根节点}
}

(4)层序

第1层开始,从左至右顺序访问(队列实现:先被访问节点的孩子也先被访问)

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

(5)还原树

根据遍历顺序,可以还原这棵树,包括二叉树还原,树还原,森林还原(3种方式)

1,二叉树还原

(已知中序和先序)

根据中序和先序 或者 中序和后续,可以唯一地还原二叉树

步骤:a.先序序列第1个字符为根  b.中序序列,以根为中心划分左右子树  c.重复上述步骤

eg:

1,已知一棵二叉树,先序ABDECFG,中序DBEAFGC,那么根为A,左子树DBE,右子树FGC

分析1:

左子树DBE在先序中是BDE,所以左子树的根为B,且中序以B为中心划分左右孩子D,E

分析2:

右子树FGC在先序中是CFG,所以右子树的根为C,左子树包含FG,右子树空,先序中为FG,所以F为根,中序中以F为根划分左右子树,左子树为空,右子树为G

代码

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;
}

👆代码解释👇

1,pre,mid为指针类型,分别指向先序,中序序列的首地址

len为序列长度

2,先序序列第1个字符pre[0]为根,index记录中序中查找pre[0]的长度

3,左子树:先序的首地址为pre + 1,中序首地址mid,长度index

4,右子树:先序首地址pre + index + 1,中序首地址mid + index + 1,长度len - index - 1

(右子树长度 = 总长度 - 左子树长度 - 根)

(已知中序和后序)

后序 DEBGFCA,中序 DBEAFGC

建议画图

BiTree pro_mid_createBiTree(char *last, char *mid, int len) //后中序建立二叉树
{if(len == 0)return NULL;char ch = last[len - 1]; //后续最后1个节点为根int index = 0; //中序查找根节点,index记录查找长度while(mid[index] != ch)index++;BiTree T = new BiTNode; //创建根节点T->data = ch;T->lchild = pro_mid_createBiTree(last, mid, index); //创建左子树T->rchild = pro_mid_createBiTree(last+index, mid+index+1, len-index-1); //创建右子树return T;
}

2,树还原

树的先序,中序遍历 == 二叉树的先序,中序遍历,所以

(根据给定的先序,中序序列)👇先还原为二叉树,再从二叉树转树

先序ABEFCDGIH,中序EFBCIGHDA   (A是根)

先序BEFCDGIH,中序EFBCIGHD          (B是根)

先序EFCDGIH,中序  (左)EF      (右)CIGHD      (E,C是根)

先序DGIH,中序IGHD            (D是根)

先序GIH,中序IGH     (G是根)           

-->(至此,还原完毕)

👆二叉树转树,长子作为左孩子,兄弟关系右斜

3,森林还原

类似上面的树还原,区别在于,森林加1个根节点,就变成了普通的树

⚽刷题

🦎P1305 新二叉树

P1305 新二叉树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

静态存储的方式,l[]存储左儿子,r[]存储右儿子,比如:

l[0] == 1表示a的左儿子是b

r[0] == 2表示a的右儿子是c

#include<iostream>
using namespace std;int l[30], r[30], root, n;
string s;void preorder(int t) //先序遍历
{if(t != '*' - 'a') { //非空节点cout<<char(t + 'a'); //整型转字符preorder(l[t]); preorder(r[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;
}

🦎Tree Recovery

Tree Recovery - UVA 536 - Virtual Judge (vjudge.net)

为了更好理解postorder()函数的过程,想想递归树,也可以对照样例来模拟

比如👇

DBACEGF ABCDEFG
ACBFGED

解释

int len = inorder.find(preorder[l1]) - l2; //左子树长度

👆inorder.find(preorder[l1]) 表示中序中根节点下标,l2表示中序中子树的起始位置

根节点 - 起始 = 左子树长度

AC  代码

#include<iostream>
using namespace std;string preorder, inorder;//l1先序起始位置, l2中序起始, n节点数
void postorder(int l1, int l2, int n)
{if(n <= 0) return; //递归出口int len = inorder.find(preorder[l1]) - l2; //左子树长度//先序DBACEGF 中序ABCDEFG  对照样例写递归postorder(l1 + 1, l2, len); //递归左子树postorder(l1 + len + 1, l2 + len + 1, n - len - 1); //递归右子树cout<<preorder[l1]; //输出根节点位置
}int main()
{while(cin>>preorder>>inorder) {int len = preorder.size();postorder(0, 0, len);cout<<endl;}return 0;
}

🦎Tree

Tree - UVA 548 - Virtual Judge (vjudge.net)

思路

输入二叉树中序,后续序列,求从根到叶子编号之和最小的叶子节点,sum一样就取编号最小的叶子节点。

先根据后序确定根,然后在中序中划分左右子树,还原二叉树,最后先序遍历,并查找根到叶子编号之和最小的叶子节点的编号(权值)。

解释

(1) 

stringstream s(line);

将字符串传递给 stringstream 构造函数,后续的int x; while(s>>x);     因为x是int,所以会读取字符串中的int,而不读取空格

(2)

if(!getline(cin, line)) {}

getline 函数会从输入流cin中读取一行数据,并将其存储在字符串变量line中,包含空格和其他特殊字符

(3)

lch[root]=createtree(l1,l2,len);
rch[root]=createtree(l1+len+1,l2+len,m-len-1);与findmin(rch[v],sum);

这里解释下lch[] 和 rch[],两个数组保存的是左,右子树的信息,下标索引为节点编号,也即节点的值,比如 lch[13] = 5; 表示节点13的左儿子为5

(4)

sum_min = 0x7fffffff;

0x 十六进制,0 八进制,0b 二进制

0x7fffffff 就表示 0111 1111 1111 1111 1111 1111 1111 1111,即10进制中 2^31 - 1

十六进制的 7 表示成二进制 0111,十六进制的 f,即 15,表示成二进制 1111

AC  代码                

#include<iostream>
#include<sstream> // stringstream
using namespace std;const int maxn = 10000+5;
int lch[maxn], rch[maxn], in[maxn], post[maxn], n, ans; // in中序, post后序
int sum_min; // 最大int// 读入带空格字符串
bool read(int *a)
{string line;if(!getline(cin, line))return false; // 读入失败stringstream s(line);n = 0;int num;while (s >> num) // num为int,所以只会读取整数a[n++] = num; // 得到初始节点总数 nreturn n > 0;
}// 递归建树
int create_tree(int L1, int L2, int len) // L1中序起始, L2后序起始
{ // len当前子树长度if (len <= 0)return 0; //空树int root = post[L2 + len - 1]; // 当前子树后序最后一个为根, 所以要加上L2// 中序中找根节点int p = 0;while (in[L1 + p] != root)p++; // 根节点在中序位置lch[root] = create_tree(L1, L2, p); // 递归左子树rch[root] = create_tree(L1 + p + 1, L2 + p, len - p - 1); // 递归右子树return root;
}void findmin(int v, int sum) // v当前节点编号值value
{sum += v;if (!lch[v] && !rch[v]) // 没有左右子树, 即叶子节点if (sum < sum_min || (sum == sum_min && v < ans)) {ans = v;sum_min = sum;}if (lch[v]) // v 有左子树findmin(lch[v], sum); // 递归左子树if (rch[v]) // v 有右子树findmin(rch[v], sum); // 递归右子树
}int main()
{while(read(in)) {read(post);create_tree(0, 0, n);sum_min = 0x7fffffff; // 记得在此处更新findmin(post[n - 1], 0);cout<<ans<<endl;}return 0;
}

🌼5.4 -- 哈夫曼树

1,前置知识 

(1)根据维基百科

霍夫曼编码使用变长编码表对源符号(如文件中的一个字母)进行编码,其中变长编码表是通过一种评估来源符号出现机率的方法得到的,出现机率高的字母使用较短的编码,反之出现机率低的则使用较长的编码,这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。

(2)

哈夫曼编码(Huffman Coding)多图详细解析_von Libniz的博客-CSDN博客

👍 ---- 知识点介绍,引用别人的就好,省的敲

(3) 

为了追求最优编码方案,如果每个字符使用频率一样,我们可以采用固定长度编码

但使用频率不一样的话,需要采用不等长编码 ,哈夫曼编码就是其中的一个代表

哈夫曼编码广泛用于 数据压缩,尤其是远距离通信和大容量数据存储,常用的JPEG图片,就是采用哈夫曼编码压缩的。

基本思想 

以字符的使用频率作为权值构建一颗哈夫曼树。

要编码的字符作为叶子节点,出现的频率作为权值。

以自底向上的方式,进行 n - 1 次“合并”。

权值越大的叶子节点,离根越近。

(4)

贪心策略

每次从森林中,找到2棵权值最小孤儿树,作为左右子树,构造一棵新树,新树权值为2颗孤儿树的权值和,将新树放回森林。

2,实现

(1)数据结构

节点结构体 HnodeType  --  二叉树

typedef struct {double weight; // 权值int parent, lchild, rchild; // 双亲,左孩,右孩char value; // 字符
} HNodeType;

编码结构体 HcodeType   --  与字符一一对应的二进制

bit[] 存放节点的编码,start 记录开始的下标

逆向编码存储时(从叶子到根),start 从 n - 1 开始依次递减,从后向前存储 

读取时,从 start + 1开始到 n - 1,前向后输出,即该字符的编码

为什么存储 -- 后向前;而读取 -- 前向后呢?

这符合哈夫曼树的 构造和编码 流程,每个字符对应的二进制,都是从根节点开始读的(前向后),但是记录 bit[] 时,是从每一个叶节点出发的(后向前)(详情看前置只是的博客)

typedef struct {int bit[MAXBIT]; // 存储编码的数组int start; // 编码开始的下标
} HCodeType;

(2)初始化

节点权值为 0

parent, lchild, rchild 为 -1

value 对应字符

然后读入叶子节点权值

(3) 循环构造哈夫曼树

集合T中取出双亲为-1且权值最小的两棵树ti, tj,合并为一颗新树zk

int x1, x2; //  最小权值节点的序号
double m1, m2; // 最小权值节点的权值
for (int i = 0; i < n - 1; i++) {m1 = m2 = MAXVALUE; // 初始化为最大值, m编号, x序号x1 = x2 = -1; // 初始化为 -1// 找出所有节点中权值最小, 无双亲节点的两个节点for (int j = 0; j < n + i; j++) {if (HuffNode[j].weight < m1 && HuffNode[j].parent == -1) {// 交换当前最小权值的两棵树m2 = m1, x2 = x1; // m1最小, m2次小m1 = HuffNode[j].weight; // 最小节点权值x1 = j; // 记录序号}else if (HuffNode[j].weight < m2 && HuffNode[j].parent == -1) {m2 = HuffNode[j].weight; // 次小节点权值x2 = j; // 记录序号}}/* 更新新树信息 */// 设置 parentHuffNode[x1].parent = n + i;HuffNode[x2].parent = n + i;// 设置 child 和 weightHuffNode[n + i].weight = m1 + m2;HuffNode[n + i].lchild = x1; HuffNode[n + i].rchild = x2;
}

解释

(1)外层 for 循环 n - 1 次

构建哈夫曼树时,假设 n 个叶节点,那么总节点数 2n - 1 个,此时内部节点数 = 总数 - 叶子数 = n - 1

所以循环次数为 n - 1(内部节点数)

(2)初始化权值MAX 和序号 -1

为了保证后续选择序号和权值时,有合适的初始值

(3)内层 for 循环

在当前节点集合中找出权值最小的两个节点x1和x2,并且这两个节点没有双亲节点

(parent == -1)

通过遍历节点集合,比较节点的权值找出最小和次小的节点并记录它们的序号

(4)更新信息

为什么双亲的序号是 n + i 呢

n 表示原始叶子节点的个数,i 表示当前正在处理的第 i 个非叶子节点

保证新节点的编号是唯一的,不会与其他已存在的节点重复

(4)输出 哈夫曼编码 

回顾

节点结构体 HNodeType

typedef struct {double weight; // 权值int parent, lchild, rchild; // 双亲,左孩,右孩char value; // 字符
} HNodeType;

编码结构体 HCodeType

typedef struct {int bit[MAXBIT]; // 存储编码的数组int start; // 编码开始的下标
} HCodeType;

代码

解释看注释(详细过程的图解 + 图表 + 代码,请自行看书)

// (用于生成叶子节点编码) 参数1编码结果的HCodeType数组, 参数2叶子个数
void HuffmanCode(HCodeType HuffCode[MAXLEAF], int n) 
{HCodeType cd; // 编码信息int c, p; // 叶节点和父节点for (int i = 0; i < n; i++) { // 遍历所有叶节点// 索引0开始, 所以最后一个位置是 n - 1cd.start = n - 1; // 临时编码起始位置 n - 1, 后往前c = i; // i 为叶子节点编号p = HuffNode[c].parent; // 叶节点父亲的编号// p != -1保证向上遍历, 直到根节点, 因为根节点没有父亲, p == -1while (p != -1) { // 父节点不为 -1if (HuffNode[p].lchild == c) cd.bit[cd.start] = 0; // 左 0else cd.bit[cd.start] = 1; // 右 1cd.start--; // 向前记录编码的 0 / 1c = p; // 子变父p = HuffNode[c].parent; // 此时的c就是原来的p, 父变爷爷} // while循环结束后, 当前叶节点 i 的二进制记录完毕// 每个while循环, 都是一条完整的从叶节点到根节点的路径/* 叶节点的编码从临时编码cd中复制出来, 放入编码结构体数组 */// bit是结构体成员, cd是临时编码结构体, 第 i 个叶节点, 第 j 个编码位置// 编码起始位置开始, 依次复制// cd.start + 1, 开始, 因为最后多前移了一格, 而这一格没有父节点, bit[]没有记录for (int j = cd.start + 1; j < n; j++) // 前往后HuffCode[i].bit[j] = cd.bit[j]; HuffCode[i].start = cd.start; // 更新HuffCode中起始位置, 打印时和cd位置一样}
}

3,分析

在 构造循环哈夫曼树 JuffmanTree() 函数 中,

if (HuffNode[j].weight < m1 && HuffNode[j].parent == -1) 为基本语句

外层 i 与 内层 j 双层循环

i = 0, 该语句执行 n 次

i = 1,n + 1次

...

i = n - 2,执行 n + (n - 2) 次

加起来 (3n - 2) * (n - 1) / 2

而在函数 HuffmanCode() 中,编码和输出的时间复杂度都接近 n^2

所以时间复杂度 O(n^2)

空间复杂度:所需存储空间为 节点结构体数组 和 编码结构体数组

哈夫曼树数组 HuffNode[] 中节点为 n 个,每个节点包含 bit[MAXBIT] 和 start 两个域,

则算法空间复杂度为 O(n * MAXBIT)

4,优化

(1)时间

HuffmanTree() 构造哈夫曼树中,找两个权值最小节点时,使用优先队列,时间复杂度 O(logn),执行 n - 1 次,总时间复杂度 O(nlogn)

(2)空间

HuffmanCode() 输出哈夫曼编码中,哈夫曼编码数组 HuffCode[] 中,定义一个动态分配空间的线性表来存储编码(每个线性表的长度都为实际编码长度,大大节省空间

⚽刷题

🦎围栏修复

Fence Repair - POJ 3253 - Virtual Judge (vjudge.net)

3253 -- Fence Repair (poj.org)

样例解释

思路

21 + 13 = 34

容易发现,要取最小值,正向思考,每次去掉当前最大值,逆向则符合哈夫曼树的构造

(每次取两个最小的合并,直到合并为一棵树,每次合并的结果就是切割的费用)


(优先队列默认最大堆,队头(堆顶)元素始终是最大值,这里我们需要设置最小堆)

使用优先队列(最小值优先),每次都弹出两个最小值 t1, t2, t = t1 + t2, sum += t,将 t 入队,继续,直到队空。sum为所需花费


假设20000个木板,每块50000米,哈夫曼构造,结果 sum = 10^4 * (5*2 + 5*3 + ... + 5*20000) = 5 * 10^4 * (2 + 3 + ... + 20000) ≈ 5 * 10^4 * 2 * 10^8 = 10^13(用long long)

priority_queue三个参数👇

std::priority_queue - cppreference.com

关于第 2 参数

用于存储元素的底层容器类型。容器必须满足 SequenceContainer 的要求,其迭代器必须满足 LegacyRandomAccessIterator 的要求

priority_queue常用函数

AC  代码

注意测试样例

#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;int main()
{long long sum = 0;int n, t, t1, t2;priority_queue<int, vector<int>, greater<int> > q;scanf("%d", &n);for (int i = 0; i < n; ++i) {scanf("%d", &t);q.push(t);}if (q.size() == 1) {sum += q.top();q.pop();}while (q.size() > 1) {t1 = q.top(), q.pop();t2 = q.top(), q.pop();t = t1 + t2, q.push(t);sum += t;// cout << sum << endl;}printf("%lld", sum);return 0;
}

🦎信息熵

Entropy - POJ 1521 - Virtual Judge (vjudge.net)

Huffman模板题 

The text strings will consist only of uppercase alphanumeric characters and underscores (which are used in place of spaces)

文本字符串只包含大写字母数字字符和下划线(用来代替空格)

样例解释

对于输入中的每个文本字符串,输出 8 位 ASCII 编码的比特长度、最佳无前缀变长编码的比特长度,以及精确到小数点后一位的压缩比

思路

最佳无前缀可变长度编码(optimal prefix-free variable-length encoding)

就是哈夫曼编码

首先根据字符串统计每个字符出现频率,然后按频率构造哈夫曼树,计算总的编码长度

(多次读入)

理解

这里可以按2种方式理解

AC  代码

源码是漏了考虑数字,但是,有点还是不错,它有对一个字符的特判,初始化 ans = s.size(),这是我不足的地方

有个坑,double 的 printf 输出用 .f ,而 scanf 输入用 .lf

第二个坑,需要特判一个字符的情况,比如 aaaa,输出应该是 32 4 8.0

所以 if (q.size() == 1) ans = s.size()

#include<iostream>
#include<cstring> //memset()
#include<cstdio> // printf()
#include<queue>
using namespace std;int a[200]; // 字符出现次数int main()
{string s;int ans, t1, t2, t, n;while (1) {cin >> s;if (s == "END") break;memset(a, 0, sizeof(a)); // 初始化ans = 0, n = s.size();for (int i = 0; i < n; ++i) {if (s[i] == '_')a[99]++; // _代替空格elsea[s[i] - '0']++; // 字符出现次数}// 最小值在堆顶的优先队列priority_queue<int , vector<int>, greater<int> > q;for (int i = 0; i <= 99; ++i)if (a[i]) q.push(a[i]);// 特判: 只出现一种字符if (q.size() == 1) ans = n; // 注意这里是n, 每个字符用一个0/1表示, n个0/1while (q.size() > 1) {t1 = q.top(), q.pop();t2 = q.top(), q.pop();t = t1 + t2;ans += t, q.push(t);}printf("%d %d %.1f\n", 8*n, ans, (double)8*n / ans);}return 0;
}

也可以不用 a[] 来统计 数字 + 大写字母 + 下划线,出现次数

直接对字符串 s 排序,然后顺序判断每个字符出现次数

🦎转换哈夫曼编码

Inverting Huffman - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

Inverting Huffman - UVA 12676 - Virtual Judge (vjudge.net)

思路

本题不是常规(简单)的哈夫曼编码问题,不是给出字符出现次数,来求最短编码长度;

而是给定编码长度,即离根节点的距离

编码长度 = 离根节点距离(与出现次数成反比

给定编码长度,已知长度越大,离根节点越远,权值越小(出现次数越小)

那么我们假定编码长度最大的为1,然后秉持着 “每一层当前节点权值,至少是下一节点权值的最大值”,即可找到最短编码长度

如果当前节点权值比下一层最大值小,那么应该出现在下一层,而不会出现在本层 

举个例子

4 // 4个不同字符
3 1 2 3 // 每个字符编码长度

3 1 2 3 即 3 3 2 1,那么可以假定离根节点距离最大,为 3 的两个字符,权值都为 1👇

那么最短编码长度为 5

算法设计

根节点权值最大可能:1 2 3 5 8 13 21 34 55 89 144 ...   到第50个数,第49层, 用 long long

  1. 读取输入的字符个数 n

  2. 清空之前的数据,对深度数组 deep[] 进行清空操作,确保每次循环都是从空数组开始

  3. 初始化变量 maxd 为 0(最大深度)

  4. 读取每个字符对应的深度 x

    a. 更新 maxd,如果当前深度 x 大于 maxd,则更新 maxd 的值为 x

    b.   deep[x] ,第 x 层中,初始化每个节点权值为 0

  5. 向上遍历处理树的每一层,从最大深度开始,直到第 0 层

    a. 当前层

    i. 节点的值为 0,找到一个字符,根据贪心,赋值为上一层最大元素

    ii. 对当前层的节点进行排序

    b. 合并节点,并放入上一层

    i. t = t1 + t2,t 加入上一层

    c. 更新 temp 当前层最大值

  6. 输出第 0 层第 1 个元素的值,即 *(deep[0].begin())

AC  代码

#include<iostream>
#include<algorithm> // sort()
#include<cmath> // max()
#include<vector>
using namespace std;const int maxn = 55; // 最大深度
vector<long long> deep[maxn]; // 每个元素都是vector数组int main()
{int n, x; // 第 x 层, n 个字符while (cin >> n) {// 清空for (int i = 0; i < n; ++i)deep[i].clear(); // 清空vector数组// 初始化int maxd = 0; // 最大深度for (int i = 0; i < n; ++i) {cin >> x; // 第 x 层, 即到根节点距离 xmaxd = max(maxd, x);deep[x].push_back(0); // 第 x 层每个节点初始化为0}// 向上遍历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 层没有权值的节点, 被上一层最大值赋值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 为第 i 层最大值temp = *(deep[i].end() - 1); // 最大值即最后一个元素}cout << *(deep[0].begin()) << endl; // 输出第 0 层第 1 个元素}return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/196040.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Figma 插件学习(一)

一.插件介绍 插件在文件中运行&#xff0c;执行一个或多个用户操作&#xff0c;并允许用户自定义其体验或创建更高效的工作流程。 插件通过专用插件API与Figma的编辑器交互。还可以利用外部Web API。 1.插件API 插件API支持读写功能&#xff0c;允许查看、创建和修改文件的…

开源WIFI继电器之硬件电路

一、原理图 源文件 二、原理图说明 1、器件说明 U4&#xff1a;ESP8285模块 U6&#xff1a;触发器 U3&#xff1a;继电器 2、继电器状态检测说明 检测继电器线圈是否通电来判断继电器是否导通&#xff0c;当Q1不导通时&#xff0c;Q1集电极的电压为3.3V&#xff0c;经…

【项目管理】中途接手的项目应对实用指南

导读&#xff1a;作为项目经理中途接手项目往往不可避免&#xff0c;为了保证项目成功需要项目经理额外考虑更多的因素和处理相关问题&#xff0c;也往往带来很大的挑战性。本文提供可应对借鉴的思路&#xff0c;在一定程度上可以作为最佳实践。 目录 1、首先、了解项目项目背…

[AutoSar]导出task mapping 表到excel

目录 关键词平台说明背景实现方法 关键词 嵌入式、C语言、autosar 平台说明 项目ValueOSautosar OSautosar厂商vector芯片厂商TI编程语言C&#xff0c;C编译器HighTec (GCC) 背景 为了做文档输出&#xff0c;要导出task mapping 到excel。 实现方法 1.按住shift&#xf…

Windows核心编程 静态库与动态库

资源文件 .rc 文件 会被 rc.exe 变成 .res 文件(二进制文件) 在链接时链接进入 .exe 文件 一、如何保护源码 程序编译链接过程 不想让别人拿到源代码&#xff0c;但是想让其使用功能&#xff0c;根据上图观察&#xff0c;把自己生成的obj给对方&#xff0c;对方拿到obj后&…

ps找不到msvcp140.dll怎么办?亲测5个有效的修复方法分享

运行Photoshop时提示找不到MSVCP140.dll&#xff0c;这是因为计算机MSVCP140.dll文件丢失或者损坏。msvcp140.dll是微软Visual C 2015运行库的一部分&#xff0c;它包含了许多用于支持C运行的函数和类。当我们在使用某些程序时&#xff0c;如果这个程序依赖于msvcp140.dll&…

Day35力扣打卡

打卡记录 相邻字符不同的最长路径&#xff08;树状DP&#xff09; 链接 若节点也存在父节点的情况下&#xff0c;传入父节点参数&#xff0c;若是遍历到父节点&#xff0c;直接循环里 continue。 class Solution:def longestPath(self, parent: List[int], s: str) -> in…

一个C语言程序的分析:运行速度和文件大小以及变量初始值

环境 Ubuntu 22.04gcc 11.4.0Window 11Microsoft Visual Studio Community 2022 (64-bit) - Current Version 17.6.2 运行速度 一个C程序 test1.c 如下&#xff1a; int array[30000][30000];int main() {for (int i 0; i < 30000; i)for (int j 0; j < 30000; j) …

VMware Workstation 与 Device/Credential Guard 不兼容 解决办法

问题描述 问题描述&#xff1a; VMware 启动虚拟机会报错。无法运行。 错误信息&#xff1a;VMware Workstation 与 Device/Credential Guard 不兼容。在禁用 Device/Credential Guard 原因分析&#xff1a; 通常原因是 Window 系统开启了 内置的Hyper-V 虚拟机。 解决方案&…

sftp 从windows10向linux(centos7)传输文件

前言背景&#xff1a;该示例是需要从windows10向本地linux系统传输一个qt安装文件&#xff0c;不想或者无法安装xftp这些传输工具&#xff0c;直接通过命令传输&#xff1b; 首先保证windows10 ping通linux系统ip&#xff0c;linux ping 通windows10系统&#xff1b; 注意&am…

计算机网络的标准化工作及相关组织

一、国际化组织 计算机网络的标准化工作由一些主要的组织来进行管理和推动。以下是几个主要的计算机网络标准化的国际组织及其相关的标准&#xff1a; 1. 国际标准化组织&#xff08;ISO&#xff09;&#xff1a;国际标准化组织负责制定各种行业的标准&#xff0c;包括计算机…

【Java程序员面试专栏 专业技能篇】Java SE核心面试指引(一):基础知识考察

关于Java SE部分的核心知识进行一网打尽,包括四部分:基础知识考察、面向对象思想、核心机制策略、Java新特性,通过一篇文章串联面试重点,并且帮助加强日常基础知识的理解,全局思维导图如下所示 本篇Blog为第一部分:基础知识考察,子节点表示追问或同级提问 基本概念 …