用JavaScript处理压缩位的最有效方法

时间:2019-02-13 03:31:51

标签: javascript encoding binary bit-manipulation huffman-code

我从未做过压缩,但是对Huffman encoding感兴趣。他们将其显示为前几个字母的简单演示编码:

A     0
E     10
P     110
space 1110
D     11110
T     111110
L     111111

否则,您看到的标准霍夫曼编码具有一组不同的代码,但是对于这个问题并不重要。我想知道的是如何最有效地操纵JavaScript中的这些位。据说您应该以8、16或32的块为单位进行处理,但实际上没有别的,因为这是在计算机体系结构中存储整数和值的方式。因此,据我了解,您可能应该一次读取8位输入块。我不确定如何执行此操作,但是我认为如果您执行此操作将会有效:

var bytes = new Uint8Array(array)
var byte1 = bytes[0]
var byte2 = bytes[1]
...

这似乎是访问的最有效方法。但是,我正在考虑要澄清的另一种选择。相反,您可以将输入转换为二进制文本字符串,因此字符串为1和0,如

var string = integerOrByteArray.toString(2)

但是我所学到的将任何东西都转换为字符串的方法对性能造成了很大的影响。因此,似乎应该尽可能避免转换为字符串。

如果是这种情况,那么我们将采用第一种方法使用Uint8Array(或Uint32Array等)。我想知道您随后如何将值有效/理想地拆分为各个组成部分。所以,如果我们有这个......

010110
AEP

....我们完成了整数操作,然后我们可能会像以下其中之一那样加载一些8位整数:

01011000
01011001
00101100
...

所以,就像,我们需要(潜在地)连接可能是最后一个8位块的一部分的任何前端数据,然后将其余部分拆分为字符。我的问题基本上是推荐这样做的方法。我可以提出解决方法,但到目前为止,它们似乎都非常复杂。

2 个答案:

答案 0 :(得分:2)

这实际上与霍夫曼减压的“其余部分”相互作用。您究竟需要什么取决于您打算进行高效的基于表的解码还是逐位树遍历。输入不能在没有解码的情况下进行拆分,因为只有在解码了它所代表的符号后才能找到代码的长度。解码后,拆分没有太多意义,因此最终得到的实际上只是霍夫曼解码器,而不是位字符串拆分器。

对于逐位的树遍历,您需要的是某种方式来访问字节数组中的任何特定位(根据其索引)。您还可以将以下技术用于1位块大小。

为了更有效地解码,您需要一个缓冲区,只要您预定义的最大代码长度(例如15位左右),就可以从中提取一个位块 [1] 。具体细节取决于您的代码按字节打包的顺序,即字节是从lsb到msb还是从msb到lsb填充,以及您要在缓冲区变量中的何处保留这些位。例如,这里我将缓冲区中的位保留在缓冲区的lsb附近,并假定如果代码被分成两个字节,则它位于第一个字节的lsb和第二个字节的msb [ 2] (未经测试):

var rawindex = 0;
var buffer = 0;
var nbits = 0;
var done = false;
var blockmask = (1 << MAX_CODELEN) - 1;
while (!done) {
    // refill buffer
    while (nbits < MAX_CODELEN && rawindex < data.length) {
        buffer = (buffer << 8) | data[rawindex++];
        nbits += 8;
    }
    if (nbits < MAX_CODELEN) {
        // this can happen at the end of the data
        buffer <<= MAX_CODELEN - nbits;
        nbits = MAX_CODELEN;
    }
    // get block from buffer
    var block = (buffer >> (nbits - MAX_CODELEN)) & blockmask;
    // decode by table lookup
    var sym = table[block];
    // drop only bits that really belong to the symbol
    nbits -= bitlengthOf(sym);
    ...
    // use the symbol somehow
}

这显示了最简单的基于表的解码策略,只是简单的查找。符号/长度对可以是一个对象,也可以存储在两个单独的Uint8Array中,也可以编码为单个Uint16Array。建立表很简单,例如使用伪代码:

# for each symbol/code do this:
bottomSize = maxCodeLen - codeLen
topBits = code << bottomSize
for bottom in inclusive_range(0, (1 << bottomSize) - 1):
    table[topBits | bottom] = (symbol, codeLen)

变量

从lsb向上将代码打包为字节会更改位流。要在缓冲区中重新组装代码,这些位需要从缓冲区的高位进入而从底部离开:

// refill step
buffer |= data[rawindex++] << nbits;
nbits += 8;
...
// get block to decode
var block = buffer & blockmask;
// decode by table lookup
var sym = table[block];
// drop bits
buffer >>= getlengthOf(sym);

表也不同,现在填充在表索引的高位,将属于单个符号的条目分散开,而不是将它们放在连续的范围内(未经测试,显示位填充的表条目带有5位代码长度):

// for each symbol/code:
var paddingCount = MAX_CODELEN - codeLen;
for (var padding = 0; padding < (1 << paddingCount); padding++)
    table[(padding << codelen) | code] = (symbol << 5) + codeLen;

[1]:最大代码长度长,会使解码表非常大,并且MAX_CODELEN> 25会使缓冲区溢出。有很多解决方法,但是超长符号反正不是很有用。

[2]:这不是DELFATE所做的。

答案 1 :(得分:1)

对于已经读取位,您有一个很好的答案。

出于完整性考虑,如果您还想研究压缩,这里有一个(未经测试的)输出函数,可以帮助您提出一些想法:

let remainder = 0; 
let remainderBits = 0;  // number of bits held in remainder

function putHuffman( 
    code,       // non-negative huffman code
    nBits) {    // bit length of code

    if( remainderBits) {
        code = code * (2 ** remainderBits) + remainder;
        nBits += remainderBits;
    }
    while( nBits >= 8) {
        putOctet( code % 256);
        code /= 256;
        nBits -=8;
    }
    remainder = code;
    remainderBits = nBits;
}

function putOctet( byte) {
    // add byte to the output stream
}

可以将其转换为使用移位运算符,但当前在代码中最多允许约46位-如果添加了7个剩余位,则位计数达到53,这是双浮点数尾数值的最大位精度。 / p>

当然,由于JavaScript缺少整数数据类型,因此它不太适合密集位操作-使用浮点乘法似乎比左移位要慢得多(如果确实比较慢)。