这段代码如何打印出“hello world”?

170

我发现了这个奇怪的事情:

for (long l = 4946144450195624l; l > 0; l >>= 5)
    System.out.print((char) (((l & 31 | 64) % 95) + 32));

输出:

hello world

这是如何工作的?


3
这是一个JVM的漏洞。请向Oracle报告此问题。 - Martijn Courteaux
6
这个问题似乎不适合讨论,因为它涉及代码混淆。 - Blazemonger
9个回答

264

数字4946144450195624适合64位,其二进制表示为:

 10001100100100111110111111110111101100011000010101000

该程序从右向左解码每个5位组的字符。
 00100|01100|10010|01111|10111|11111|01111|01100|01100|00101|01000
   d  |  l  |  r  |  o  |  w  |     |  o  |  l  |  l  |  e  |  h

5位编码

对于5位编码,可以表示2⁵ = 32个字符。英文字母表包含26个字母,因此除了字母外还有6个符号的空间。使用这种编码方案,您可以拥有所有26个(一个大小写)英文字母和6个符号(其中包括空格)。

算法描述

for循环中的>>= 5从一组跳到另一组,然后通过在句子l & 31中将数字与掩码31₁₀ = 11111₂相与来隔离出5位组。

现在,代码将5位值映射到其相应的7位ASCII字符。这是棘手的部分。请查看以下表格中小写字母的二进制表示:

  ASCII   |     ASCII     |    ASCII     |    Algorithm
character | decimal value | binary value | 5-bit codification
--------------------------------------------------------------
  space   |       32      |   0100000    |      11111
    a     |       97      |   1100001    |      00001
    b     |       98      |   1100010    |      00010
    c     |       99      |   1100011    |      00011
    d     |      100      |   1100100    |      00100
    e     |      101      |   1100101    |      00101
    f     |      102      |   1100110    |      00110
    g     |      103      |   1100111    |      00111
    h     |      104      |   1101000    |      01000
    i     |      105      |   1101001    |      01001
    j     |      106      |   1101010    |      01010
    k     |      107      |   1101011    |      01011
    l     |      108      |   1101100    |      01100
    m     |      109      |   1101101    |      01101
    n     |      110      |   1101110    |      01110
    o     |      111      |   1101111    |      01111
    p     |      112      |   1110000    |      10000
    q     |      113      |   1110001    |      10001
    r     |      114      |   1110010    |      10010
    s     |      115      |   1110011    |      10011
    t     |      116      |   1110100    |      10100
    u     |      117      |   1110101    |      10101
    v     |      118      |   1110110    |      10110
    w     |      119      |   1110111    |      10111
    x     |      120      |   1111000    |      11000
    y     |      121      |   1111001    |      11001
    z     |      122      |   1111010    |      11010

在这里,我们要映射的ASCII字符从第7位和第6位开始(11xxxxx₂)(空格除外,只有第6位打开)。您可以使用OR将5位编码与96 (96₁₀ = 1100000₂) 相加以进行映射,但对于空格无效(该死的空格!)。

现在我们知道处理空格和其他字符时必须特别小心。为了实现这一点,代码会在提取的5位组中打开第7位(但不是第6位)并使用 OR 64 64₁₀ = 1000000₂l & 31 | 64)。

到目前为止,5位组的形式为:10xxxxx₂(空格将是1011111₂ = 95₁₀)。

如果我们可以将空格映射为0,同时不影响其他值,则可以打开第6位,就可以完成了。

这就是为什么要使用mod 95的原因。 空格是1011111₂ = 95₁₀,使用取模运算(l & 31 | 64) % 95),只有空格返回到0,然后将第6位打开,通过在先前的结果((l & 31 | 64) % 95) + 32)上加上32₁₀ = 100000₂,将5位值转换为有效的ASCII字符。

isolates 5 bits --+          +---- takes 'space' (and only 'space') back to 0
                  |          |
                  v          v
               (l & 31 | 64) % 95) + 32
                       ^           ^
       turns the       |           |
      7th bit on ------+           +--- turns the 6th bit on

以下代码实现了反向过程,给定一个小写字符串(最多12个字符),返回可与OP代码一起使用的64位长整型值:
public class D {
    public static void main(String... args) {
        String v = "hello test";
        int len = Math.min(12, v.length());
        long res = 0L;
        for (int i = 0; i < len; i++) {
            long c = (long) v.charAt(i) & 31;
            res |= ((((31 - c) / 31) * 31) | c) << 5 * i;
        }
        System.out.println(res);
    }
}

12
这个回答没有任何神秘感,相反它会代替你进行思考。 - user978923
7
答案比问题还难 :D - Yazan
1
解释更加清晰 :) - Prashant

40

以下Groovy脚本会打印出中间值。

String getBits(long l) {
    return Long.toBinaryString(l).padLeft(8, '0');
}

for (long l = 4946144450195624l; l > 0; l >>= 5) {
    println ''
    print String.valueOf(l).toString().padLeft(16, '0')
    print '|' + getBits((l & 31))
    print '|' + getBits(((l & 31 | 64)))
    print '|' + getBits(((l & 31 | 64) % 95))
    print '|' + getBits(((l & 31 | 64) % 95 + 32))

    print '|';
    System.out.print((char) (((l & 31 | 64) % 95) + 32));
}

就是这个:

4946144450195624|00001000|01001000|01001000|01101000|h
0154567014068613|00000101|01000101|01000101|01100101|e
0004830219189644|00001100|01001100|01001100|01101100|l
0000150944349676|00001100|01001100|01001100|01101100|l
0000004717010927|00001111|01001111|01001111|01101111|o
0000000147406591|00011111|01011111|00000000|00100000|
0000000004606455|00010111|01010111|01010111|01110111|w
0000000000143951|00001111|01001111|01001111|01101111|o
0000000000004498|00010010|01010010|01010010|01110010|r
0000000000000140|00001100|01001100|01001100|01101100|l
0000000000000004|00000100|01000100|01000100|01100100|d

26

有趣!

标准 ASCII 字符的可见字符范围为 32 到 127。

这就是为什么你在那里看到了 32 和 95(127-32)。

实际上,每个字符都映射到这里的 5 位,(你可以找到每个字符的 5 位组合是什么),然后所有位被连接起来形成一个大数。

正长整数是 63 位数字,足以容纳加密形式的 12 个字符。所以它足够大来容纳 Hello word,但对于更大的文本,应使用更大的数字,甚至是一个 BigInteger。


在一个应用程序中,我们希望通过短信传输可见的英文字符、波斯字符和符号。

如你所见,有 32 (波斯字符的数量) + 95 (英文字符和标准可见符号的数量) = 127 种可能的值,可以用 7 位表示。

我们将每个 UTF-8(16 位)字符转换为 7 位,并获得超过 56% 的压缩比率。因此,我们可以在相同数量的短信中发送两倍长度的文本。(不知何故,在这里也发生了同样的事情。)


OP的代码中还有很多其他的内容。例如,这并没有真正解释| 64是在做什么。 - Ted Hopp
1
@Amir:实际上95在这里是因为你需要获取一个空格字符。 - Bee

17

你得到的结果是以下值的字符表示形式:char

104 -> h
101 -> e
108 -> l
108 -> l
111 -> o
32  -> (space)
119 -> w
111 -> o
114 -> r
108 -> l
100 -> d

16

你已经将字符编码为5位值并将其中11个打包成64位的长整型

(packedValues >> 5*i) & 31是第i个编码值,范围为0-31。

如你所说,困难的部分在于对空格进行编码。小写英文字母在Unicode(和ASCII、大多数其他编码)中占据连续的范围97-122,但空格是32。

为了解决这个问题,你使用了一些算术运算。((x+64)%95)+32几乎与x + 96相同(注意在这种情况下位运算符OR等同于加法),但当x=31时,我们得到32


6

它打印“hello world”的原因与此类似:

for (int k=1587463874; k>0; k>>=3)
    System.out.print((char) (100 + Math.pow(2,2*(((k&7^1)-1)>>3 + 1) + (k&7&3)) + 10*((k&7)>>2) + (((k&7)-7)>>3) + 1 - ((-(k&7^5)>>3) + 1)*80));

但是原因与此略有不同:

for (int k=2011378; k>0; k>>=2)
    System.out.print((char) (110 + Math.pow(2,2*(((k^1)-1)>>21 + 1) + (k&3)) - ((k&8192)/8192 + 7.9*(-(k^1964)>>21) - .1*(-((k&35)^35)>>21) + .3*(-((k&120)^120)>>21) + (-((k|7)^7)>>21) + 9.1)*10));

22
你应该解释你正在做什么,而不是发布另一个谜语。 - Aleksandr Dubinsky
1
我建议你花些力气去找一个欢迎贡献有趣谜语的网站(也许是一些Beta StackExchange?)。Stack Overflow是一个严格执行重点的问答网站。 - Marko Topolnik
1
@MarkoTopolnik,我不想生活在一个所有规则或重点都严格执行以至于不允许任何例外的世界中。更不用说在 SO 上有无数这样的例外。 - גלעד ברקן
1
我也想这样做,但是 Stack Overflow(SO)在很大程度上是这样的一个世界。当然,在这里也有例外,但它们并不受欢迎。 - Marko Topolnik
1
另外还有15个人分享了亚历山大的观点。你指出这个问题本身不适合在SO上讨论是正确的,正如下面的评论所说。 - Marko Topolnik
显示剩余9条评论

3

我主要使用Oracle数据库,因此我将使用一些Oracle知识进行解释和说明 :-)

让我们把数字4946144450195624转换成二进制。为此,我使用一个名为dec2bin的小函数,即十进制转二进制。

SQL> CREATE OR REPLACE FUNCTION dec2bin (N in number) RETURN varchar2 IS
  2    binval varchar2(64);
  3    N2     number := N;
  4  BEGIN
  5    while ( N2 > 0 ) loop
  6       binval := mod(N2, 2) || binval;
  7       N2 := trunc( N2 / 2 );
  8    end loop;
  9    return binval;
 10  END dec2bin;
 11  /

Function created.

SQL> show errors
No errors.
SQL>

让我们使用该函数来获取二进制数值 -

SQL> SELECT dec2bin(4946144450195624) FROM dual;

DEC2BIN(4946144450195624)
--------------------------------------------------------------------------------
10001100100100111110111111110111101100011000010101000

SQL>

现在问题在于5位二进制的转换。从右到左每组5个数字进行分组。我们得到:

100|01100|10010|01111|10111|11111|01111|01100|01100|00101|01000

在二进制转换中,我们最终将只剩下右侧的3个数字。因为总共有53个数字。

SQL> SELECT LENGTH(dec2bin(4946144450195624)) FROM dual;

LENGTH(DEC2BIN(4946144450195624))
---------------------------------
                               53

SQL>

Hello World 总共有 11 个字符(包括空格),因此我们需要在最后一组中添加 两个 比特,以便在分组后剩下仅三个比特。

现在,我们有:

00100|01100|10010|01111|10111|11111|01111|01100|01100|00101|01000

现在,我们需要将它转换为7位ASCII值。对于字符来说很容易; 我们只需要设置第6和第7位。将左侧的每个5位组加上11即可。

这样就得到了:

1100100|1101100|1110010|1101111|1110111|1111111|1101111|1101100|1101100|1100101|1101000

让我们解释二进制值。 我将使用二进制到十进制转换函数

SQL> CREATE OR REPLACE FUNCTION bin2dec (binval in char) RETURN number IS
  2    i                 number;
  3    digits            number;
  4    result            number := 0;
  5    current_digit     char(1);
  6    current_digit_dec number;
  7  BEGIN
  8    digits := length(binval);
  9    for i in 1..digits loop
 10       current_digit := SUBSTR(binval, i, 1);
 11       current_digit_dec := to_number(current_digit);
 12       result := (result * 2) + current_digit_dec;
 13    end loop;
 14    return result;
 15  END bin2dec;
 16  /

Function created.

SQL> show errors;
No errors.
SQL>

让我们来看看每个二进制值 -

SQL> set linesize 1000
SQL>
SQL> SELECT bin2dec('1100100') val,
  2    bin2dec('1101100') val,
  3    bin2dec('1110010') val,
  4    bin2dec('1101111') val,
  5    bin2dec('1110111') val,
  6    bin2dec('1111111') val,
  7    bin2dec('1101111') val,
  8    bin2dec('1101100') val,
  9    bin2dec('1101100') val,
 10    bin2dec('1100101') val,
 11    bin2dec('1101000') val
 12  FROM dual;

       VAL        VAL        VAL        VAL        VAL        VAL        VAL        VAL        VAL     VAL           VAL
---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ----------
       100        108        114        111        119        127        111        108        108     101           104

SQL>

让我们来看看这些字符是什么:
SQL> SELECT chr(bin2dec('1100100')) character,
  2    chr(bin2dec('1101100')) character,
  3    chr(bin2dec('1110010')) character,
  4    chr(bin2dec('1101111')) character,
  5    chr(bin2dec('1110111')) character,
  6    chr(bin2dec('1111111')) character,
  7    chr(bin2dec('1101111')) character,
  8    chr(bin2dec('1101100')) character,
  9    chr(bin2dec('1101100')) character,
 10    chr(bin2dec('1100101')) character,
 11    chr(bin2dec('1101000')) character
 12  FROM dual;

CHARACTER CHARACTER CHARACTER CHARACTER CHARACTER CHARACTER CHARACTER CHARACTER CHARACTER CHARACTER CHARACTER
--------- --------- --------- --------- --------- --------- --------- --------- --------- --------- ---------
d         l         r         o         w         ⌂         o         l         l         e         h

SQL>

那么,我们得到了什么输出结果?

d l r o w ⌂ o l l e h

这就是将hello⌂world反转后的结果。唯一的问题是空格。@higuaro在他的回答中对此进行了很好的解释。起初我自己也无法理解空格问题,直到看到他回答中的解释。


1

我发现将代码转换为PHP语言后,更容易理解,具体如下:

<?php

$result=0;
$bignum = 4946144450195624;
for (; $bignum > 0; $bignum >>= 5){
    $result = (( $bignum & 31 | 64) % 95) + 32;
    echo chr($result);
}

请查看实时代码


为什么更容易理解?你能详细说明一下吗? - Peter Mortensen
它是如何回答这个问题的?问题是:“这是如何工作的?” - Peter Mortensen

0

使用

out.println((char) (((l & 31 | 64) % 95) + 32 / 1002439 * 1002439));

将它变成大写字母。


1
请考虑添加一些关于你正在做什么以及为什么这样做的解释。 - fedorqui

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接