序
- 最近因为某些原因,要实现一下word2vec,但是想试一下C实现(然后开始怀疑人生,真的看得懂C,但是python写多了,写不出来C了,很别扭的感觉…),正好再次体验一下它源码,这篇是单独将其中Huffman算法提出来讨论一番。
- 分三部分来说一下:(1)算法流程 (2)自问自答的方式拿些问题出来提一下 (3)自己注释好的的代码贴出来
算法流程
- 数据的数据结构:三个数组,count数组初始化。
- 构建树结构。
- 构建完毕后编码。
- 释放树结构。
自问自答
-
问:如果让你用C构建一棵树?
答:我的第一想法是,来吧,链表走起…然后源码放出了三个数组最为基本结构???? -
注意一个细节:
代码在申请完那三个数组的地址后,最后都free了,所以整个结构信息的存储另有玄机。 -
为什么源码要用数组?
我最开始这样傻傻的想?现在偏向于“简便”这个解释,当然也是因为Huffuman树编码本身就不需要维护复杂的树结构,只需要根据树结构编完码就够了,为什么?因为他有这样一个性质,对于某个词来说,从根节点到它的路径是唯一的,意味着我只需要关心这条路径就够了。 -
三个数组分别是:count,binary,parent_code,各自有什么用?
(细节,词是按从大到小的顺序排列好的)
- count:前半部分vocab_size大小的部分=下坐标下,对应词库中词的频率,剩下的空间全部初始化为:1e15。但是在遍历过程中,两个节点合并出的新结点的count也会按顺序往后存入。总之:这个是用来存储频率的。
- binary : 用来编码的,两个节点,负类编码为1,分到了左边,且约定两个节点权值为大的为1。(你也可以理解最重要的作用是用来区分左右节点的,保存左右信息)
- parent_node : 结构信息,用来存储层次(上下)之间的关系,即两个点对应一个父节点,这个对应关系被这个数组保存。见下:
parent_node[min1i] = vocab_size + a;
parent_node[min2i] = vocab_size + a;
- 注释:min1i/min2i表示前面找到的两个最小的下坐标的值,然后这两个下坐标对应的节点,存储位置是vocab_size往后顺移动a大小的位置。
-
所以虽然不是常见的链表那种更形象的方式,三个数组也保存下了整个树的结构。
-
我怎样从数组中恢复上面的树的层次关系?其实这个问题不重要,重要的是,我怎么从上面的数组中找到每个叶子节点到根节点的路径(词编码的本质)。两个循环搞定:外层for循环保证每个词都循环一遍。内层循环保证(while),找到当前词一直到根节点的路径(通过parent_node),这个过程中的信息会被暂时保存在point和code数组中。
-
其实到这里我基本就没啥问题了,因为整体思路都有了,但是…我看到了两个不解的操作…
-
第一个:为什么要初始化一个char类型的数组来存储code编码??
- long long 称为超长整型:binary
- char 字符类性:code
神奇的一幕来了:
code[i] = binary[b];
我暂时不理解…
- 第二个:最后的压缩存储…虽然不清楚这样会有啥好处,但是我在这一行代码里捕捉到一个我…的信息:从根节点到叶子节点的路径中,除了叶子节点,都是非叶子节点,这意味着什么?我们映射到数据存储结构中:非叶子节点都是在vocab_size * 2 + 1 的后(vocab_size + 1)个,能这么做的原因找到了,但是为什么这么做?应该我可以在后面的代码中找到答案。但是这个和数据结构呼应还是让我觉得,大佬是怎么想到这种存储方式的???刚刚好的那种感觉,太经典了。
// 路径被压缩了????
// 注意这个索引对应的是huffman树中的非叶子节点,
// 对应syn1中的索引, 因为非叶子节点都是在vocab_size * 2 + 1 的后(vocab_size + 1)个
vocab[a].point[0] = vocab_size - 2;// 根结点
for (b = 0; b < i; b++) {
vocab[a].code[i - b - 1] = code[b];// 编码的反转
vocab[a].point[i - b] = point[b] - vocab_size;// 记录的是从根结点到叶子节点的路径
}
}
- 总结:我认为对我影响最大的有四点:
-
- 数组存储树结构的方式。尤其是一个考虑左右层次,一个考虑上下层次,一个存储节点count信息…,
-
- 非叶子节点个数为N-1。(这个小知识点贯穿全代码,突然觉得这种理论很重要)。
-
- 从根节点到叶子节点路径唯一,意味着每一个词的编码我可以独立考虑,整个树结构在编码后不再重要。
-
- 所有的词都在叶子节点上,非叶子节点都是中间新生成的节点,且存储在vocab_size后面,这个呼应我真的是有点惊艳。
注释好的代码
// 构建Huffuman树
void CreateBinaryTree(){
// pos_vocab表示这个在原有节点中索引(叶子节点) pos_new表示在新节点里索引
// 这个min1i默认小于min2i(下面第一个节点给了min1i)
long long a,b,i,min1i, min2i,pos_vocab,pos_new;
long long point[MAX_CODE_LENGTH];
char code[MAX_CODE_LENGTH];
//申请空间
long long * count = (long long *)calloc(2*vocab_size+1, sizeof(long long));
long long * binary = (long long *)calloc(2*vocab_size+1,sizeof(long long));
long long * parent_node = (long long *)calloc(2*vocab_size+1,sizeof(long long));
//循环存入每一个词的count信息,剩下的空间进行初始化
for(a=0;a < vocab_size; a++ ) count[a] = vocab[a].cn;
for(a = vocab_size;a < 2*vocab_size;a++) count[a] = 1e15;
// 初始化筛选节点下坐标的值
pos_vocab = vocab_size - 1; // 指向词库最后那个最小的那值
pos_new = vocab_size; // 指向了初始构成节点的存储位置的第一个,用于存储生成节点(非叶子节点)的count,默认值为1e15
// 开始构建,这个外部循环有意思噢,循环多少次?
// 答案:我要构建 n-1个非节点,才会形成一个树,每次一个节点,那不就是循环 n-1次!!
for(a = 0; a < vocab_size - 1; a++){
// 筛选第一个节点,这个大于0好细节,但是我感觉是多余的。
if(count[pos_vocab] >=0){
if(count[pos_vocab] < count[pos_new]){
min1i = pos_vocab;
pos_vocab--;
}else{
min1i = pos_new;
pos_new++;
}
}else{
min1i = pos_new;
pos_new++;
}
// 筛选第二个节点
if(count[pos_vocab] >= 0){
if(count[pos_vocab] < count[[pos_new]]){
min2i = pos_vocab;
pos_vocab--;
}else{
min2i = pos_new;
pos_new++;
}
} else{
min2i = pos_new;
pos_new++;
}
// 提取后干什么??
// 当然是合并求出新节点啊
count[vocab_size+a] = count[min1i] + count[min2i];
// 把上面的结构信息记录下来
parent_node[min1i] = vocab_size + a;
parent_node[min2i] = vocab_size + a;
// 上面只有上下的信息,左右信息靠什么标志?
// 这个节点标为1表示其在左边(约定频率大)
// SO?
binary[min1i] = 0; //其实不用写,但是写上容易理解
binary[min2i] = 1;
}
// 树结构有了,编码开始!
// 第一步是什么?
// 答案:我是对每个词编码的(叶子节点),非叶子节点不是我的目标,我先写个对每个词的外循环没毛病吧。
// 然后呢,里面我要干什么?
// 恩.....反过来想??最后把我所有的信息(三个数组)都删掉了
// 那我到底应该保留词的什么信息对应到每一个词的结构体里面。
// 好了,其实最最重要的,就是辣个性质,从根节点到每个词(叶子节点)的路径唯一。
// 对于每个词,其他我不用管,只要我有这个词从根节点到该词叶子节点的路径就好了。
for(a = 0; a < vocab_size;a++){
b = a;
i = 0;
// 提取出编码和路径指向信息
while(1){
code[i] = binary[b]; // 记录编码,左右信息
point[i] = b; // 记录路径,上下信息
i++;
// 记住这三句话,很重要!!
// count[vocab_size+a] = count[mi1l] + count[mi2l]
// parent_node[mi1l] = vocab_size + a;
// parent_node[mi2l] = vocab_size + a;
b = parent_node[b]; // b的范围在 0 - vocab_size*2 - 2 里
// (vocab_size + vocab_size -1) - 1
if(b == vocab_size*2 - 2) break;
}
// 将提取的信息存到词的结构体里
vocab[a].codelen = i;
// 然后最不正常的代码出来了??????
// 路径被压缩了????
// 注意这个索引对应的是huffman树中的非叶子节点,
// 对应syn1中的索引, 因为非叶子节点都是在vocab_size * 2 + 1 的后(vocab_size + 1)个
vocab[a].point[0] = vocab_size - 2;// 根结点
for (b = 0; b < i; b++) {
vocab[a].code[i - b - 1] = code[b];// 编码的反转
vocab[a].point[i - b] = point[b] - vocab_size;// 记录的是从根结点到叶子节点的路径
}
}
free(count);
free(binary);
free(parent_node);
}