某一天想起之前和群友讨论过的压缩 base64 编码的事,我把一个图像转 base64 编码后用 zip 压缩文本文件,得到的文件比原图差不多大小,我又想起了大二学过的哈夫曼编码,那玩意也是用来压缩文本的。于是我有了一个想法,把图片转 base64 后用哈夫曼编码压缩一遍再和原图比较大小。

说干就干。要实现哈夫曼编码的位操作,首先得实现一个位数组。我也懒得找什么工具类了,直接自己搓一个,实现 push 和 pop 方法就行。这一篇就讲这个吧。

一开始先定义结构体:

type bit8 struct {
	len byte
	val byte
}

type BitList struct {
	Size int
	arr  []bit8
}

后来写着写着突然开窍了,反正是存的二进制流,用 64 位的 int64 感觉更好,能减少数组重分配的问题,代价是 int64 的大小是 byte 的 8 倍,对于少量元素来说空间的冗余有点多。

总之是改了。

type bit64 struct {
	len byte
	val uint64
}

type BitList struct {
	Size int
	arr  []bit64
}

push 方法写的差不多了,测试也通过了。突然想到还得实现一个方法把这些数据转成 byte 数组,方法名就叫 DumpBytes 吧,突然有点后悔改成 int64 存储数据了......

实现了 index 方法,先通过除法运算来定位要找的是哪个 int64,再通过位运算来截取对应的位置。这里不得不吐槽一下 go 语言的位运算了,java 的位运算有一个 >>> 运算符,表示无视符号位右移,但是 go 语言没有这玩意,进行右移是不得不考虑符号位的问题。所以我想了一个绕了一圈的方法,就是右移后再根据剩下的长度(记为 n)和 pow(2, n) - 1 进行与运算,得到的结果就是无视符号位右移的结果了。

用 byte 来举个例子 假如有一个 11001100 的 byte,右移 2 位后变成了 11110011,可以看到符号位也移了过来。此时移位后剩下的部分是后 6 位,我只需要将 pow(2, 6) - 1,也就是 0011111111110011 进行与运算,就能得到 00110011,正是无符号右移 2 位的结果。

正准备实现 stringer 接口的时候,突然想到给 int64 套一个结构体真的有必要嘛,最后一个 int64 的有效长度可以算出来的。最烦这种写到一大半才发现前期的设计问题。算了,以后再优化咯。

然后开始捣鼓 DumpBytes 方法,突然想到一个妙招,用 go 语言里 const 变量的 iota 关键字生成一些“int64 的片段”(就是把 int64 按 8 位分开,每个区域都是 1,比如说 00 ... 56 个0 ... 011111111 是第一个片段,00 ... 48 个 0 ... 01111111100000000 是第二个片段,以此类推),然后用这些片段分别和原来的 int64 做与运算得到 byte,至于最后一位没有占满完整的 int64,不好用切割法,所以就直接调用 index 的那个方法来做了。其实一开始只在调用 index 方法,写到一半突然想了下,数据量大的时侯就算 index 是 O(1) 时间复杂度还是会很慢。于是就想出了这个方法,几乎比循环调用 index 方法快了 8 倍。

2022-07-24