【问题标题】:A Java string with hashCode equal to Integer.MIN_VALUEhashCode 等于 Integer.MIN_VALUE 的 Java 字符串
【发布时间】:2022-11-16 03:58:43
【问题描述】:

是否存在 hashCode 完全等于 Integer.MIN_VALUE 的已知 Java 字符串?这有助于为哈希表编写测试,以帮助避免在执行余数操作之前在哈希码上运行 Math.Abs​​ 的常见错误。

理想情况下,该字符串只包含 ASCII 字符,但我不确定它是否可行。

【问题讨论】:

  • 如果允许使用下划线:"HZcxf_".hashCode() == Integer.MIN_VALUE
  • 哇@user16320675:太快了。如果您将其作为答案提交,我会接受。
  • 我很好奇@user16320675 是怎么发现的。我编写了一个小程序来检查可打印 ASCII 字符的随机字符串(所有字符串长度为 6)。在我杀死它之前,它在没有匹配的情况下运行了大约 30 亿个字符串。

标签: java testing hashcode


【解决方案1】:

基于哈希码的公式(来自StringLatin1):

    public static int hashCode(byte[] value) {
        int h = 0;
        for (byte v : value) {
            h = 31 * h + (v & 0xff);
        }
        return h;
    }

它与字符线性相关,字符串越长,字符越大,散列码就越大,直到溢出。另请注意,第一个字符具有更大的影响在生成的哈希码上(通常乘以 31)。

前两个算法的基本思想是递增字符直到哈希码变为负数,从最左边的字符开始,因为它具有更大的权重。搜索到的字符串必须有导致它在每个位置溢出的字符之前的字符,但最后一个除外。

代码开始测试字符串 "A", "AA", "AAA", ... 直到开始返回负值 - 前一个字符串用作起始值。
现在它开始将第一个字符递增到 Z 或直到找到具有负散列的字符串。对每个下一个字符都执行相同的操作。 由于此类字符串的哈希码尚未达到Integer.MIN_VALUE,因此进行了额外的传递,以测试小写字符。这应该已经集成在以前的循环中......
现在最后一个字符是调整准确地到达Integer.MIN_VALUE——很简单,因为刚刚添加了最后一个字符,没有乘法来计算哈希码。

这里的代码:

var string = "A";
while ((string+"A").hashCode() > 0) {
    string += "A";
}

var array = string.toCharArray();
var i = 0;
while (i < array.length) {
    array[i] += 1;
    if (array[i] > 'z' || new String(array).hashCode() < 0) {
        array[i] -= 1;
        i += 1;
        continue;
    }
}

i = 1;
while (i < array.length) {
    if (array[i] == 'Z') {
        array[i] = 'a';
    }else {
        array[i] += 1;
    }
    if (array[i] > 'Z' || new String(array).hashCode() < 0) {
        if (array[i] == 'a')
            array[i] = 'Z';
        else
            array[i] -= 1;
        i += 1;
        continue;
    }
}
int hash = new String(array).hashCode();
if (hash > 0) {
    array[array.length-1] += Integer.MAX_VALUE - hash + 1; 
}
System.out.printf("%s = %d%n", new String(array), new String(array).hashCode());

这导致:

HZcxf_ = -2147483648


合并前面代码的两个递增循环,我们有:

var string = "A";
while ((string+"A").hashCode() > 0) {
    string += "A";
}

var array = string.toCharArray();
var i = 0;
while (i < array.length) {
    var prev = array[i];
    if (prev == 'Z') {
        array[i] = 'a';
    } else {
        array[i] += 1;
    }
    if (array[i] > 'z' || new String(array).hashCode() < 0) {
        array[i] = prev;
        i += 1;
        continue;
    }
}
int hash = new String(array).hashCode();
if (hash > 0) {
    array[array.length-1] += Integer.MAX_VALUE - hash + 1; 
}
System.out.printf("%s = %d%n", new String(array), new String(array).hashCode());

结果(与之前略有不同):

HZdZG_ = -2147483648


另一种方法会更强烈地基于散列计算,基本上取消它。
由于我不想使用负数,它以Integer.MAX_VALUE 开头,比Integer.MIN_VALUE 少一个(考虑溢出/下溢)。
首先,它会找出它必须被31 除以的频率,直到结果小于 128 (ASCII),这在某种程度上决定了字符串的长度。 接下来它循环并找出每个字符并进行一些特殊处理以避免字符小于 ' '。
最后,将最后一个字符递增 1,通过溢出将哈希码从MAX_VALUE移动到MIN_VALUE

var string = "";
var remain = Integer.MAX_VALUE;
var i = 0;
var multiplier = 1;
while (remain > 127) {
    remain /= 31;
    multiplier *= 31;
    i += 1;
}
remain = Integer.MAX_VALUE;
while (i >= 0) {
    var ch = (char)(remain / multiplier);
    remain -= ch * multiplier;
    multiplier /= 31;
    if (i > 0) {
        // correct if next ch will be less than ' '
        var correct = (' ' - (remain / multiplier) + 30) / 31;  // old fashion rounding
        if (correct > 0) {
            ch -= correct;
            remain += correct * 31 * multiplier;
        }
    } else {
        ch += 1;
    }
    string += ch;
    i -= 1;
}
System.out.printf("%s = %d%n", string, string.hashCode());

及其结果:

I='&lt;*! = -2147483648


注意:如果更改String的哈希码算法,最后的代码肯定会失败!前两个可能会失败,这取决于哈希计算如何更改。

【讨论】:

    【解决方案2】:

    String#hashCode() 定义为:

    返回此字符串的哈希码。 String 对象的哈希码计算为

    s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
    

    使用 int 算法,其中 s[i] 是字符串的第 i 个字符,n 是字符串的长度,^ 表示求幂。 (空字符串的散列值为零。)

    现在你只需要解决-2147483648(可能只限制可打印的 ASCII 字符:32-127):)

    或者你蛮力(这需要一段时间):

    public class HashFinder {
        private static final int SIZE = 7;
        private static long hashesCalculated = 0L;
    
        public static void main(String[] args) {
            hashesCalculated = 0L;
            final long start = System.nanoTime();
            findHash(SIZE);
            final long duration = System.nanoTime() - start;
    
            System.err.println("Checked strings of size " + SIZE);
            System.err.println(hashesCalculated + " hashes in " + TimeUnit.NANOSECONDS.toSeconds(duration) + "s");
        }
    
        public static void findHash(final int size) {
            findHash("", size);
        }
    
        public static void findHash(final String prefix, final int size) {
            if (size <= 0) {
                return;
            }
    
            final StringBuilder sb = new StringBuilder(prefix).append(' ');
            for (char c = ' '; c < '~'; ++c) {
                sb.setCharAt(prefix.length(), c);
                final String s = sb.toString();
                ++hashesCalculated;
                if (s.hashCode() == Integer.MIN_VALUE) {
                    System.out.printf("Found string with min hashCode! '%s'%n", s);
                }
    
                findHash(s, size - 1);
            }
        }
    }
    

    但是分配所有这些字符串和字符串构建器是昂贵的。当我们从 char 数组手动计算哈希码时,暴力破解变得可行:

    public class HashFinderBytes {
        public static void main(String[] args) {
            final char start = ' ', end = '~';
            for (int size = 1; size <= 9; size++) {
                char[] chars = new char[size];
                Arrays.fill(chars, start);
    
                final long startNano = System.nanoTime();
                final long combinations = BigInteger.valueOf(end - start).pow(size).longValue();
                System.err.println("Checking " + combinations + " strings of size " + size);
                for (long i = 0; i < combinations; ++i) {
                    if (hashCode(chars) == Integer.MIN_VALUE) {
                        System.out.printf("Found string with min hashCode! "%s"%n", new String(chars));
                        System.out.println("Sanity check: " + (new String(chars).hashCode() == Integer.MIN_VALUE));
                    }
                    for (int j = 0; j < chars.length; ++j) {
                        ++chars[j];
                        if (chars[j] <= end) {
                            break;
                        }
                        chars[j] = (byte) start;
                    }
                }
                final long duration = System.nanoTime() - startNano;
    
                final long millis = TimeUnit.NANOSECONDS.toMillis(duration);
                System.err.println("in " + millis + "ms (" + (combinations / millis) + " ops/ms)");
            }
        }
    
        public static int hashCode(char[] value) {
            int h = 0;
            for (char v : value) {
                h = 31 * h + (v & 0xff);
            }
            return h;
        }
    }
    

    事实上,有很多字符串的哈希码与Integer.MIN_VALUE 相同。

    长度 6:

    I='<*!
    H'<*!
    G{'<*!
    I<F<*!
    H[F<*!
    GzF<*!
    I;e<*!
    HZe<*!
    Gye<*!
    I=&[*!
    H&[*!
    G{&[*!
    I<E[*!
    H[E[*!
    GzE[*!
    I;d[*!
    HZd[*!
    Gyd[*!
    I=%z*!
    H%z*!
    G{%z*!
    I<Dz*!
    H[Dz*!
    GzDz*!
    I;cz*!
    HZcz*!
    Gycz*!
    I=';I!
    H';I!
    G{';I!
    I<F;I!
    H[F;I!
    GzF;I!
    I;e;I!
    HZe;I!
    Gye;I!
    I=&ZI!
    H&ZI!
    G{&ZI!
    I<EZI!
    H[EZI!
    GzEZI!
    I;dZI!
    HZdZI!
    GydZI!
    I=%yI!
    H%yI!
    G{%yI!
    I<DyI!
    H[DyI!
    GzDyI!
    I;cyI!
    HZcyI!
    GycyI!
    I=':h!
    H':h!
    G{':h!
    I<F:h!
    H[F:h!
    GzF:h!
    I;e:h!
    HZe:h!
    Gye:h!
    I=&Yh!
    H&Yh!
    G{&Yh!
    I<EYh!
    H[EYh!
    GzEYh!
    I;dYh!
    HZdYh!
    GydYh!
    I=%xh!
    H%xh!
    G{%xh!
    I<Dxh!
    H[Dxh!
    GzDxh!
    I;cxh!
    HZcxh!
    Gycxh!
    

    长度 7(以下所有字符串均以空格字符结尾);未全部显示:

    p4*|{e 
    oS*|{e 
    nr*|{e 
    p3I|{e 
    oRI|{e 
    nqI|{e 
    p2h|{e 
    oQh|{e 
    nph|{e 
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-10-21
      • 2015-11-03
      • 1970-01-01
      • 2014-07-29
      • 1970-01-01
      • 2013-04-04
      • 1970-01-01
      • 2014-08-13
      相关资源
      最近更新 更多