【问题标题】:compressing a string of 0's and 1's in js在js中压缩一串0和1
【发布时间】:2016-06-28 08:42:59
【问题描述】:

介绍

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

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

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

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

问题

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

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

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

我考虑过简单地为 1x1 到 100x100 的网格尺寸写下所有可能的网格选项,并为它们提供一个键/参考以用作可共享的代码。手动做这件事太疯狂了,但也许你们中的任何人都知道如何创建一个可以做到这一点的算法?

GitHub repository

【问题讨论】:

  • 查看FEN chess notation 是如何构建的。看来您可以使用类似的东西。您不需要保存 0-s。只保存 1-s 的序列
  • @NinaScholz 我认为这不是一个好方法,对于 0101010101010101... 结果字符串是原始字符串的两倍
  • 对于这种情况,您可以采用模式,如01 和运行长度
  • 编码模式的逻辑很复杂

标签: javascript decode encode compression


【解决方案1】:

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

计数系统

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

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

由于您使用的是二进制系统,因此每个位置只有 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

我们的意思是“解析此字符串并将其解释为以 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 个可能的值,并且你已经充分利用了你的字节。


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

【讨论】:

  • 在你回答的最后,你说的是一个 265 字符的查找表。这究竟是如何工作的?你的键盘上还有 256 个字符吗?还是我应该在某个时候开始使用双字符?谢谢你的回答顺便说一句。这非常有帮助,我现在将二进制文件转换为十六进制,反之亦然!
  • 这与 Arnould 在他的回答中用 String.fromCharCode 函数描述的非常相似。这个函数的缺点是你会得到一个无效的 UTF-8 字符串。 (这就是他进行 base-64 编码的原因,请参阅他的答案中的 cmets)。在数组中使用双字符将使您获得的字符串在最坏的情况下与正在编码的原始字符串一样长。在最好的情况下,字符串的长度会减半。
【解决方案2】:

如果我们假设网格包含的 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% 的固定压缩率)并且仍然可以由用户复制/粘贴。

包装:

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

【讨论】:

  • 我们不能假设网格中的 0 比 1 多。由于允许用户创建任何可能的网格,因此应该允许保存/分享他们的创作。
  • 请注意,String.fromCharCode 不一定为每个十六进制值返回一个值(请参阅jsfiddle.net/CorstianBoerman/0a7bhy2e
  • String.fromCharCode 不一定会返回 printable 字符,这正是结果字符串必须转换为 base64 的原因。
  • 看起来很酷! (无论如何,当结果被进一步编码时,谁在乎有效的 UTF-8 序列)
【解决方案3】:

LZ-字符串

使用LZ-string 我能够将“代码”压缩很多。
只需像这样将其压缩为 base64:

var compressed = LZString.compressToBase64(string)

解压也就是这么简单:

var decompressed = LZString.decompressFromBase64(compressed)

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

example

但压缩确实有效。

【讨论】:

    【解决方案4】:

    回答

    对于任何想知道我最终是如何做到这一点的人,这里是如何做到的:

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

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

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

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

    例如:
    十六进制字符串 11 将变为 hhh 将变为 H
    01101111 将变为 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 的数量,因此我们可以在解压缩结束时将其去掉。 这就是返回的字符串最终的样子。

    amount of leading 0s-compressed string 所以一个 64*64 empty 网格,将导致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 缩短器 (view here) 来使这个过程对用户来说更容易一些。

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

    原因如下:

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

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

    直播

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

    谢谢你

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

    Sooooo.... 谢谢大家!尝尝吧!

    【讨论】:

    • @Arnauld 谢谢你们俩
    【解决方案5】:

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

    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;
    }
    

    【讨论】:

    • 使用 压缩到原来的 14.2%。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-06
    • 2013-05-12
    • 1970-01-01
    • 2011-09-18
    • 1970-01-01
    • 2020-04-12
    相关资源
    最近更新 更多