在js中压缩一串0和1

compressing a string of 0's and 1's in js

简介

我目前正在用 js 编写 John Conway 的 Game of Life。我的游戏正在运行 (view here),并且我正在开发额外的功能,例如与您的朋友分享您的“网格/游戏”。为此,我将网格的值(如果单元格是活的还是死的)提取到一长串 0 和 1 中。

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

grid 1 has a length and width of 30 => so the string's length is 900

grid 2 has a length and width of 50 => so the string's length is 2500

问题

如您所见,这些由 0 和 1 组成的字符串太长,无法复制和共享。

无论我多么努力,我似乎​​都无法想出一个代码来将这么长的字符串压缩成易于处理的字符串。

关于如何压缩(和解压)这个有什么想法吗?

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

GitHub repository

如果我们假设网格包含的 0 比 1 多得多,您可能想试试这个简单的压缩方案:

  1. 将二进制字符串转换为十六进制字符串
  2. 将 '00' 子字符串转换为 'z' 符号
  3. 将 'zz' 个子字符串转换为 'Z' 个符号
  4. 我们可以走得更远,但让我们在这里停下来进行演示

下面是一个 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 的倍数时,此代码才能正常工作。如果不是这种情况,您将不得不填充输入并裁剪输出。

编辑:第二种方法

如果输入是完全随机的——0 和 1 的数量大致相同,并且没有特定的重复模式——你能做的最好的事情可能是将二进制字符串转换为 BASE64 字符串。它将明显更短(这次固定压缩率约为 17%),并且用户仍然可以copied/pasted。

包装:

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 之类的东西,以减少编码开销。

LZ-字符串

使用 LZ-string 我能够将 "code" 压缩很多。
通过像这样简单地将其压缩为 base64:

var compressed = LZString.compressToBase64(string)

解压也是这么简单:

var decompressed = LZString.decompressFromBase64(compressed)

但是这个压缩字符串的长度仍然很长,因为你有大约 0 和 1 一样多(示例中没有给出)

example

但压缩确实有效。

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

计数系统

二进制是一种数字base-2. This essentially means that there are two characters being used to keep count. Normally we are used to count with base-10 (decimal characters). In computer science the hexadecimal system (base-16)也被广泛使用

由于您不是将位存储为位而是字节(如果您希望将它们存储为位,请使用var a = 0b1100001;)'binary' 您希望存储的内容与具有相同长度的任何其他随机字符串一样多 space。

由于您使用的是二进制系统,因此每个位置只有 2 个可能的值。使用十六进制值时,单个位置最多可以容纳 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

我们说 "parse this string and interpret it like a base-2 number and store it as a hexadecimal string"。

解码实际上是一样的。将上面示例中出现的所有数字 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 个字符的查找 table,您可以在其中搜索与特定位置关联的字符。 (这是可行的,因为您仍然将十六进制值存储为字符串。)遗憾的是,ASCII nor the UTF-8 编码都不适合这种情况,因为有些块的值没有字符定义。

它可能看起来像:

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

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


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

回答

对于任何想知道我到底是怎么做到的,这里是方法:

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

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

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

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

For example:
the hex string 11 will become h and hh will become H
01101111 will become 0h0H

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

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

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

压缩的最后一步是在压缩字符串前面添加前导 0 的数量,因此我们可以在解压结束时将它们剃掉。 这就是 returned 字符串最后的样子。

amount of leading 0s-compressed string so a 64*64 empty grid, will result in 0-Z

解压缩实际上就是以相反的方式做所有事情。

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

然后使用 Arnaulds 功能,将进一步“压缩”的字符转回十六进制代码。

获取这个十六进制字符串并将其转回二进制代码。确保,正如 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;
}

URL 缩短器

我仍然发现“压缩”字符串对于共享/粘贴来说有点长。所以我使用了一个简单的 URL 缩短器 (view here) 来使这个过程对用户来说更容易一些。

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

原因如下:

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

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

直播

您可以查看我项目的实时版本here. and/or find the github repository here

谢谢

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

太棒了.... 谢谢你们!给它点赞!

在生命游戏中有一个由 1 和 0 组成的棋盘。我想备份到上一代 - 大小 4800 - 将每 16 个单元格保存为十六进制 = 大小的 1/4。 http://innerbeing.epizy.com/cwebgl/gameoflife.html [g = 开始] [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;
}