在js中压缩一串0和1的字符串

时间:2016-06-28 08:42:59

标签: javascript decode encode compression

Itroduction

我目前正在研究John Conway的生命游戏。我让游戏工作(view here),我正在研究额外的功能,例如分享你的网格/游戏"给你的朋友为此,我将网格的值(如果单元格还活着或死亡)提取为0和1的长字符串。

此字符串具有可变长度,因为网格的大小并不总是相同。例如:

  

网格1的长度和宽度为30 =>所以字符串的长度是900

     

网格2的长度和宽度为50 =>所以字符串的长度是2500

问题

正如您所看到的,这些0和1的字符串太长,无法复制和共享。

无论如何我努力尝试我似乎无法想出能够将字符串压缩到易于处理的代码。

关于如何压缩(和解压缩)的任何想法?

我考虑过简单地写下网格大小为1x1到100x100的每个可能的网格选项,并为它们提供一个键/引用以用作可共享代码。手动执行此操作会很疯狂,但也许你们中的任何人都知道如何创建一个可以做到这一点的算法?

GitHub repository

5 个答案:

答案 0 :(得分:1)

如果我们做出假设而不是网格包含比1更多的0,你可能想尝试这个简单的压缩方案:

  1. 将二进制字符串转换为十六进制字符串
  2. 转换' 00'子串到' z'符号
  3. 转换' zz'子串到' Z'符号
  4. 我们可以走得更远,但让我们来这里停止演示
  5. 以下是16x16网格的示例:

    var bin =
        '0000000000000000' +
        '0000001000000000' +
        '0000011100000000' +
        '0000001000000000' +
        '0000000000000000' +
        '0000000000111000' +
        '0000100000111000' +
        '0000000000111000' +
        '0000000000000000' +
        '0000000000000000' +
        '0000000010000000' +
        '0000000101000000' +
        '0000000010000000' +
        '0000000000000000' +
        '0000100000000000' +
        '0000000000000000';
    
    var packed = bin
      .match(/(.{4})/g)
      .map(function(x) {
        return parseInt(x, 2).toString(16);
      })
      .join('')
      .replace(/00/g, 'z')
      .replace(/zz/g, 'Z');
    

    这将产生字符串" Z02z07z02ZZ380838z38ZZz8z14z08Zz8Zz"。

    解包过程正好相反:

    var bin = packed
      .replace(/Z/g, 'zz')
      .replace(/z/g, '00')
      .split('')
      .map(function(x) {
        return ('000' + parseInt(x, 16).toString(2)).substr(-4, 4);
      })
      .join('');
    

    请注意,只有输入字符串的长度为4的倍数时,此代码才能正常工作。如果不是这种情况,则您必须填充输入并裁剪输出。 / p>

    编辑:第二种方法

    如果输入是完全随机的 - 大约和1一样多,并且没有特定的重复模式 - 你可以做的最好的事情就是将二进制字符串转换为BASE64字符串。它将显着缩短(这次固定压缩比约为17%),并且仍可由用户复制/粘贴。

    包装:

    var bin =
      '1110101110100011' +
      '0000101111100001' +
      '1010010101011010' +
      '0000110111011111' +
      '1111111001010101' +
      '0111000011100001' +
      '1011010100110001' +
      '0111111110010100' +
      '0111110110100101' +
      '0000111101100111' +
      '1100001111011100' +
      '0101011100001111' +
      '0110011011001101' +
      '1000110010001001' +
      '1010100010000011' +
      '0011110000000000';
    
    var packed =
      btoa(
        bin
        .match(/(.{8})/g)
        .map(function(x) {
          return String.fromCharCode(parseInt(x, 2));
        })
        .join('')
      );
    

    将产生字符串" 66ML4aVaDd / + VXDhtTF / lH2lD2fD3FcPZs2MiaiDPAA ="。

    启封:

    var bin =
      atob(packed)
      .split('')
      .map(function(x) {
        return ('0000000' + x.charCodeAt(0).toString(2)).substr(-8, 8);
      })
      .join('');
    

    或者,如果您想更进一步,可以考虑使用类似base91的内容,以减少编码开销。

答案 1 :(得分:1)

如果还不是很明显,那么您尝试存储的字符串看起来就像二进制字符串。

计数系统

二进制是base-2中的数字。这实际上意味着有两个字符用于保持计数。通常我们习惯用base-10(十进制字符)计数。在计算机科学中,十六进制系统(base-16)也被广泛使用。

由于您不是将位存储为位而是存储为字节(使用var a = 0b1100001;如果您希望将它们存储为位),您希望存储的“二进制”只需要与任何其他具有相同长度的随机字符串一样多的空间。

由于您使用的是二元系统,因此每个位置只有两个可能的值。使用十六进制值时,单个位置最多可容纳16个可能的值。在紧凑地存储数据时,这已经是一个很大的改进。例如0b111111110xff都表示十进制数255。

在你的情况下,你必须存储每8个字节中的6个字节。最后,你会被一根字符串所困,只是原始字符串长度的1/4。

Javascript实施

基本上我们想要做的是将您存储的字符串解释为二进制并检索十六进制值。幸运的是,JavaScript内置了这样的功能:

var bin =
  '1110101110100011' +
  '0000101111100001' +
  '1010010101011010' +
  '0000110111011111' +
  '1111111001010101' +
  '0111000011100001' +
  '1011010100110001' +
  '0111111110010100' +
  '0111110110100101' +
  '0000111101100111' +
  '1100001111011100' +
  '0101011100001111' +
  '0110011011001101' +
  '1000110010001001' +
  '1010100010000011' +
  '0011110000000000';

var returnValue = '';

for (var i = 0; i < parseInt(bin.length / 8); i++) {
    returnValue += parseInt(bin.substr(i*8, 8), 2).toString(16);
}

console.log(bin.length); // Will return 265
console.log(returnValue.length); // Will return 64

我们说“解析此字符串并将其解释为基数为2的数字并将其存储为十六进制字符串”。

解码几乎是一样的。将上面示例中所有出现的数字8替换为2,反之亦然。

请注意

此代码正常工作的先决条件是二进制长度可以被8分割。请参阅以下示例:

parseInt('00011110', 2).toString(16); // returns '1e'
parseInt('1e', 16).toString(2); // returns '11110'
// Technically both representations still have the same decimal value

解码时,你应该添加前导零,直到你有一个完整的字节(8位)。

如果你必须存储的位置不能被8分割,你可以,例如,添加填充并在输出字符串的前面添加一个数字,以确定要剥离的位置。

等等,还有更多

要获得更短的字符串,您可以构建一个包含265个字符的查找表,您可以在其中搜索与特定位置关联的字符。 (这是有效的,因为你仍然将十六进制值存储为字符串。)遗憾的是,ASCIIUTF-8编码都不适用于此,因为有值的块没有定义字符。

看起来像:

// Go fill this array until you have 265 values within it.
var lookup = ['A', 'B', 'C', 'D'];
var smallerValue = lookup[0x00];

这样,您可以在一个位置拥有265个可能的值,并且您已充分利用了您的字节。

请注意,这里没有真正的压缩。我们宁愿利用数据类型更有效地用于您当前的用例。

答案 2 :(得分:0)

LZ-串

使用LZ-string我能够压缩&#34;代码&#34;相当多 只需将其压缩到base64,就像这样:

var compressed = LZString.compressToBase64(string)

解压缩也就是这样简单:

var decompressed = LZString.decompressFromBase64(compressed)

然而,这个压缩字符串的长度仍然很长,因为你有大约0的1(在示例中没有给出)

example

但压缩确实有效。

答案 3 :(得分:0)

ANSWER

对于任何想知道我到底是怎么做的人,请按照以下方式进行操作:

首先,我确保传入的每个字符串都会填充前导0,直到它可以被8除外。(保存用于填充的0的数量,因为它们在解压缩时需要它们)

我使用Corstian的答案和函数将我的字符串(解释为二进制)压缩为十六进制字符串。虽然我不得不做一点改动。

并非每个长度为8的二进制子字符串都会返回正好2个十六进制字符。所以对于那些情况我最后只是在子串前面添加一个0。十六进制子字符串将具有相同的值它的长度现在将为2.

接下来,我使用了Arnaulds回答的功能。取每个双字符并将其替换为单个字符(未在十六进制字母表中使用以避免冲突)。我为每个十六进制字符做了两次。

  

例如:
  十六进制字符串11将变为hhh将变为H
  01101111将成为0h0H

由于大多数网格都会有更多的死细胞然后活着,我确保0s能够再次使用Arnaulds方法进一步压缩,但更进一步。

  

00 - &gt; g | gg - &gt; G | GG - &gt; w | ww - &gt; W | WW - &gt; x | xx - &gt; X | XX - &GT; y | yy - &gt; Y | YY - &gt; z | zz - &gt; Z

这导致Z代表4096(二进制)0s

压缩的最后一步是在压缩字符串前面添加前导0的数量,因此我们可以在解压缩结束时将其删除。 这是返回的字符串最后的样子。

  

amount of leading 0s - compressed string因此64 * 64 网格将导致0-Z

解压缩实际上是反过来做的所有事情。

首先拆分表示我们从压缩字符串中用作填充的前导0数的数字。

然后使用Arnaulds功能,进一步转动&#34;压缩&#34;字符回到十六进制代码。

取这个十六进制字符串并将其转回二进制代码。正如Corstian指出的那样,确保每个二进制子字符串的长度都为8.(如果不是我们用前导0填充子字符串,直到do,确切地说,长度为8)

然后最后一步是削减我们用作填充的前导0,使开始字符串可以偏离8。

功能

我用来压缩的功能:

/**
 * Compresses the a binary string into a compressed string.
 * Returns the compressed string.
 */
Codes.compress = function(bin) {
  bin = bin.toString(); // To make sure the binary is a string;
  var returnValue = ''; // Empty string to add our data to later on.

  // If the lenght of the binary string is not devidable by 8 the compression
  // won't work correctly. So we add leading 0s to the string and store the amount
  // of leading 0s in a variable.


  // Determining the amount of 'padding' needed.
  var padding = ((Math.ceil(bin.length/8))*8)-bin.length;
  // Adding the leading 0s to the binary string.
  for (var i = 0; i < padding; i++) {
    bin = '0'+bin;
  }

  for (var i = 0; i < parseInt(bin.length / 8); i++) {
    // Determining the substring.
    var substring = bin.substr(i*8, 8)
    // Determining the hexValue of this binary substring.
    var hexValue = parseInt(substring, 2).toString(16);
    // Not all binary values produce two hex numbers. For example:
    // '00000011' gives just a '3' while what we wand would be '03'. So we add a 0 in front.
    if(hexValue.length == 1) hexValue = '0'+hexValue;
    // Adding this hexValue to the end string which we will return.
    returnValue += hexValue;
  }

  // Compressing the hex string even further.
  // If there's any double hex chars in the string it will take those and compress those into 1 char.
  // Then if we have multiple of those chars these are compressed into 1 char again.
  // For example: the hex string "ff will result in a "v" and "ffff" will result in a "V".
  // Also: "11" will result in a "h" and "1111" will result in a "H"
  // For the 0s this process is repeated a few times.
  // (string with 4096 0s) (this would represent a 64*64 EMPTY grid)
  // will result in a "Z".
  var returnValue = returnValue.replace(/00/g, 'g')
                               .replace(/gg/g, 'G')
                              // Since 0s are probably more likely to exist in our binary and hex, we go a step further compressing them like this:
                               .replace(/GG/g, 'w')
                               .replace(/ww/g, 'W')
                               .replace(/WW/g, 'x')
                               .replace(/xx/g, 'X')
                               .replace(/XX/g, 'y')
                               .replace(/yy/g, 'Y')
                               .replace(/YY/g, 'z')
                               .replace(/zz/g, 'Z')
                               //Rest of the chars...
                               .replace(/11/g, 'h')
                               .replace(/hh/g, 'H')
                               .replace(/22/g, 'i')
                               .replace(/ii/g, 'I')
                               .replace(/33/g, 'j')
                               .replace(/jj/g, 'J')
                               .replace(/44/g, 'k')
                               .replace(/kk/g, 'K')
                               .replace(/55/g, 'l')
                               .replace(/ll/g, 'L')
                               .replace(/66/g, 'm')
                               .replace(/mm/g, 'M')
                               .replace(/77/g, 'n')
                               .replace(/nn/g, 'N')
                               .replace(/88/g, 'o')
                               .replace(/oo/g, 'O')
                               .replace(/99/g, 'p')
                               .replace(/pp/g, 'P')
                               .replace(/aa/g, 'q')
                               .replace(/qq/g, 'Q')
                               .replace(/bb/g, 'r')
                               .replace(/rr/g, 'R')
                               .replace(/cc/g, 's')
                               .replace(/ss/g, 'S')
                               .replace(/dd/g, 't')
                               .replace(/tt/g, 'T')
                               .replace(/ee/g, 'u')
                               .replace(/uu/g, 'U')
                               .replace(/ff/g, 'v')
                               .replace(/vv/g, 'V');

  // Adding the number of leading 0s that need to be ignored when decompressing to the string.
  returnValue = padding+'-'+returnValue;

  // Returning the compressed string.
  return returnValue;
}

我用来解压缩的功能:

/**
 * Decompresses the compressed string back into a binary string.
 * Returns the decompressed string.
 */
Codes.decompress = function(compressed) {
  var returnValue = ''; // Empty string to add our data to later on.

  // Splitting the input on '-' to seperate the number of paddin 0s and the actual hex code.
  var compressedArr = compressed.split('-');
  var paddingAmount = compressedArr[0]; // Setting a variable equal to the amount of leading 0s used while compressing.
  compressed = compressedArr[1]; // Setting the compressed variable to the actual hex code.

  // Decompressing further compressed characters.
  compressed = compressed// Decompressing the further compressed 0s. (even further then the rest of the chars.)
                         .replace(/Z/g, 'zz')
                         .replace(/z/g, 'YY')
                         .replace(/Y/g, 'yy')
                         .replace(/y/g, 'XX')
                         .replace(/X/g, 'xx')
                         .replace(/x/g, 'WW')
                         .replace(/W/g, 'ww')
                         .replace(/w/g, 'GG')
                         .replace(/G/g, 'gg')
                         .replace(/g/g, '00')
                         // Rest of chars...
                         .replace(/H/g, 'hh')
                         .replace(/h/g, '11')
                         .replace(/I/g, 'ii')
                         .replace(/i/g, '22')
                         .replace(/J/g, 'jj')
                         .replace(/j/g, '33')
                         .replace(/K/g, 'kk')
                         .replace(/k/g, '44')
                         .replace(/L/g, 'll')
                         .replace(/l/g, '55')
                         .replace(/M/g, 'mm')
                         .replace(/m/g, '66')
                         .replace(/N/g, 'nn')
                         .replace(/n/g, '77')
                         .replace(/O/g, 'oo')
                         .replace(/o/g, '88')
                         .replace(/P/g, 'pp')
                         .replace(/p/g, '99')
                         .replace(/Q/g, 'qq')
                         .replace(/q/g, 'aa')
                         .replace(/R/g, 'rr')
                         .replace(/r/g, 'bb')
                         .replace(/S/g, 'ss')
                         .replace(/s/g, 'cc')
                         .replace(/T/g, 'tt')
                         .replace(/t/g, 'dd')
                         .replace(/U/g, 'uu')
                         .replace(/u/g, 'ee')
                         .replace(/V/g, 'vv')
                         .replace(/v/g, 'ff');

  for (var i = 0; i < parseInt(compressed.length / 2); i++) {
    // Determining the substring.
    var substring = compressed.substr(i*2, 2);
    // Determining the binValue of this hex substring.
    var binValue = parseInt(substring, 16).toString(2);

    // If the length of the binary value is not equal to 8 we add leading 0s (js deletes the leading 0s)
    // For instance the binary number 00011110 is equal to the hex number 1e,
    // but simply running the code above will return 11110. So we have to add the leading 0s back.
    if (binValue.length != 8) {
      // Determining how many 0s to add:
      var diffrence = 8 - binValue.length;

      // Adding the 0s:
      for (var j = 0; j < diffrence; j++) {
        binValue = '0'+binValue;
      }
    }

    // Adding the binValue to the end string which we will return.
    returnValue += binValue
  }

  var decompressedArr = returnValue.split('');
  returnValue = ''; // Emptying the return variable.

  // Deleting the not needed leading 0s used as padding.
  for (var i = paddingAmount; i < decompressedArr.length; i++) {
    returnValue += decompressedArr[i];
  }

  // Returning the decompressed string.
  return returnValue;
}

网址缩短

我仍然发现&#34;压缩&#34;字符串有点长,可以分享/粘贴。所以我使用了一个简单的URL缩短器(view here)来使这个过程对用户来说更容易。

现在您可能会问,那么为什么还需要压缩此字符串呢?

这就是原因:

首先,我的项目托管在github页面(gh-pages)上。 gh-pages的信息页面告诉我们,网址不能超过2000个字符 。这意味着最大网格大小将是2000的平方根 - 基本网址的长度,这不是那么大。通过使用&#34;压缩&#34;我们能够共享更大的网格。

现在第二个原因是,这是一个挑战。我发现处理这些问题很有趣,也很有帮助,因为你学到了很多东西。

您可以查看我的项目here的实时版本。和/或找到github存储库here

三江源

我要感谢所有帮助我解决这个问题的人。特别是Corstian和Arnauld,因为我最终使用他们的答案来完成我的最终职能。

Sooooo .... 谢谢你们! apriciate it!

答案 4 :(得分:0)

在生命游戏中,有一个由 1 和 0 组成的棋盘。我想备份到上一代 - 大小为 4800 - 将每 16 个单元格保存为十六进制 = 1/4 大小。 http://innerbeing.epizy.com/cwebgl/gameoflife.html [g = Go] [b = 备用]

function drawGen(n) {
  stop(); var i = clamp(n,0,brw*brh-1), hex = gensave[i].toString();
  echo(":",i, n,nGEN); nGEN = i; var str = '';

  for (var i = 0; i < parseInt(hex.length / 4); i++)
    str = str + pad(parseInt(hex.substr(i*4,4), 16).toString(2),16,'0');
  for (var j=0;j<Board.length;j++) Board[j] = intr(str.substr(j,1));
  drawBoard();
}

function Bin2Hex(n) {
  var i = n.indexOf("1"); /// leading Zeros = NAN
  if (i == -1) return "0000";
  i = right(n,i*-1);
  return pad(parseInt(i,2).toString(16),4,'0');
}
function saveGen(n) {
  var b = Board.join(''), str = ''; /// concat array to string 10101
  for (var i = 0; i < parseInt(b.length / 16); i++)
    str = str + Bin2Hex(b.substr(i*16,16));
  gensave[n] = str;
}
function right(st,n) {
  var s = st.toString();
  if (!n) return s;
  if (n < 0) return s.substr(n * -1,s.length + n);
  return s.substr(s.length - n,n);
}
function pad(str, l, padwith) {
  var s = str;
  while (s.length < l) s = padwith + s;
  return s;
}