在 Javascript 中从字符串生成哈希

IT技术 javascript hash
2021-01-25 16:00:33

我需要将字符串转换为某种形式的哈希。这在 JavaScript 中可行吗?

我没有使用服务器端语言,所以我不能那样做。

6个回答
String.prototype.hashCode = function() {
  var hash = 0, i, chr;
  if (this.length === 0) return hash;
  for (i = 0; i < this.length; i++) {
    chr   = this.charCodeAt(i);
    hash  = ((hash << 5) - hash) + chr;
    hash |= 0; // Convert to 32bit integer
  }
  return hash;
};

来源:http : //werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/

任何人都可以评论输出的唯一性(或不是)吗?具体来说,如果我只对长度小于 的字符串使用这个哈希,那么我不可能发生冲突n的最大字符串n多少?
2021-03-12 16:00:33
我对 jsperf ( jsperf.com/hashing-strings )做了一些测试,按位函数实际上比基于数字的函数慢。
2021-03-23 16:00:33
这与 Java 中使用的相同。hash << 5 - hash是一样的hash * 31 + char,但速度快了很多。这很好,因为它太快了,而且 31 是一个小质数。赢赢那里。
2021-04-08 16:00:33
@PeterAronZentai 为什么它“无法使用”?基于数字的代码(hash * 31) + char产生的输出与基于移位的代码产生的输出相同((hash<<5)-hash)+char,即使对于很长的字符串(我已经用包含超过一百万个字符的字符串对其进行了测试),因此它不是“不可用”的的准确性。对于基于数字和基于移位的版本,复杂性都是 O(n),因此就复杂性而言,它并非“不可用”。
2021-04-08 16:00:33
是否有任何理由需要(或应该)在 String 原型上?仅拥有例如是否会降低效率/效率?var hashCode = function hashCode (str) {etc...}? 然后用作hashCode("mystring")
2021-04-09 16:00:33

编辑

根据我的 jsperf 测试,接受的答案实际上更快:http ://jsperf.com/hashcodelordvlad

原来的

如果有人感兴趣,这里有一个改进的(更快)版本,它会在缺少reduce数组功能的旧浏览器上失败

hashCode = function(s){
  return s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0);              
}

单线箭头函数版本:

hashCode = s => s.split('').reduce((a,b)=>{a=((a<<5)-a)+b.charCodeAt(0);return a&a},0)
奇怪的。我刚刚测试了它,结果证明它比接受的答案慢。jsperf.com/hashcodelordvlad
2021-03-12 16:00:33
[].reduce.call(str, (p, c, i, a) => (p << 5) - p + a.charCodeAt(i), 0);
2021-03-17 16:00:33
有没有办法获得只有正数的哈希?
2021-03-18 16:00:33
我刚刚意识到:接受的答案更快是完全有道理的,因为我的版本必须首先将字符串转换为数组,分配新内存并复制每个字符......
2021-03-18 16:00:33
好人@lordvlad,实际测试了他自己的答案,然后在速度较慢时报告。
2021-03-30 16:00:33

这里的许多答案都是String.hashCode取自 Java的相同哈希函数。它的历史可以追溯到 1981 年的 Gosling Emacs,非常弱,并且在现代 JavaScript 中的性能方面为零意义。事实上,使用 ES6 可以显着加快实现速度Math.imul,但没有人注意到。我们可以做得比这更好,性能基本相同。

这是我的一个—— cyrb53,一个简单但高质量的 53 位哈希。它非常快,提供了非常好的*散列分布,并且因为它输出 53 位,与任何32 位散列相比,冲突率显着降低

const cyrb53 = function(str, seed = 0) {
    let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
    for (let i = 0, ch; i < str.length; i++) {
        ch = str.charCodeAt(i);
        h1 = Math.imul(h1 ^ ch, 2654435761);
        h2 = Math.imul(h2 ^ ch, 1597334677);
    }
    h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);
    h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
    return 4294967296 * (2097151 & h2) + (h1>>>0);
};

*大致类似于众所周知的MurmurHash/xxHash算法。它使用乘法和Xorshift的组合来生成哈希,但没有那么彻底。因此,它比 JavaScript 中的任何一个都快,并且实现起来要简单得多,但可能无法通过 SMHasher 中的所有测试。这不是加密哈希函数,因此不要出于安全目的使用它。

像任何适当的散列一样,它具有雪崩效应,这基本上意味着输入的微小变化会导致输出的巨大变化,从而使生成的散列看起来更“随机”:

"501c2ba782c97901" = cyrb53("a")
"459eda5bc254d2bf" = cyrb53("b")
"fbce64cc3b748385" = cyrb53("revenge")
"fb1d85148d13f93a" = cyrb53("revenue")

您可以选择为相同输入的备用流提供种子(无符号整数,最大 32 位):

"76fee5e6598ccd5c" = cyrb53("revenue", 1)
"1f672e2831253862" = cyrb53("revenue", 2)
"2b10de31708e6ab7" = cyrb53("revenue", 3)

从技术上讲,它是一个 64 位哈希,即两个不相关的 32 位哈希并行计算,但 JavaScript 仅限于 53 位整数。如果方便,可以通过使用十六进制字符串或数组更改return 语句来使用完整的 64 位输出

return [h2>>>0, h1>>>0];
// or
return (h2>>>0).toString(16).padStart(8,0)+(h1>>>0).toString(16).padStart(8,0);
// or 
return 4294967296n * BigInt(h2) + BigInt(h1);

请注意,构建十六进制字符串会大大减慢批处理速度。数组效率更高,但显然需要两次检查而不是一次检查。我还包括BigInt,它应该StringArrayor稍快,但仍然比or慢得多Number


只是为了好玩,这里是 TinySimpleHash,我能想出的最小的哈希值仍然不错。它是89 个字符的 32 位散列,具有比 FNV 或 DJB2 更好的质量随机性:

TSH=s=>{for(var i=0,h=9;i<s.length;)h=Math.imul(h^s.charCodeAt(i++),9**9);return h^h>>>9}
@aomarks 这两行代表'最终混合功能',是实现雪崩效果所必需的如果您将其删除,它将成为统计上较弱的哈希。在某些情况下,散列输出将不那么均匀(不那么随机),这会导致稍微更多的冲突。图像中红色突出显示的部分显示去除最终混合物时出现的缺陷。
2021-03-11 16:00:33
哇,这比通常的 *31 短(或类似)输入要好得多。:)
2021-03-24 16:00:33
@Andrew 更新了答案以澄清seed. 它旨在成为一个 32 位无符号整数。所以这些值可以是 0 到 2^32-1,并且没有小数点。所以没有浮动;无论如何,JS 只会在第一次 XOR 操作时去除小数部分。的长度没有限制str此外,它可以很容易地与 Array 而不是 String 一起使用,但这个问题是针对字符串的。
2021-03-25 16:00:33
@bryc 公共领域很棒!您愿意为 明确授予公共领域等效许可cyrb53吗?建议的示例: opensource.org/licenses/0BSD opensource.org/licenses/unlicense 这个请求的(令人沮丧的)原因是在某些司法管辖区,声明某些东西在公共领域并不合法。因此,最接近的可用工具是通过授予与公共领域相同的使用自由的许可证来覆盖它。请参阅en.wikipedia.org/wiki/Public-domain-equivalent_license。谢谢考虑!
2021-03-26 16:00:33
@BachT您可以使用填充工具或完整ES6垫片但是 IE11 在 2009 年悲惨地冻结了,没有更新。
2021-03-28 16:00:33

注意:即使使用最好的 32 位哈希,冲突迟早发生。

散列冲突概率可以计算为 1 - e ^ (-k(k-1) / 2N,近似为 k^2 / 2N见这里)。这可能比直觉所暗示的要高:
假设一个 32 位哈希和 k=10,000 个项目,发生碰撞的概率为 1.2%。对于 77,163 个样本,概率变为 50%!计算器)。
我建议底部的解决方法。

在对这个问题的回答中, 哪种散列算法最适合唯一性和速度?, Ian Boyd 发表了一篇很好的深入分析简而言之(按照我的解释),他得出的结论是 Murmur 最好,其次是 FNV-1a。
esmiralha 提出的 Java 的 String.hashCode() 算法似乎是 DJB2 的变体。

  • FNV-1a 具有比 DJB2 更好的分布,但速度较慢
  • DJB2 比 FNV-1a 快,但往往会产生更多的碰撞
  • MurmurHash3 比 DJB2 和 FNV-1a 更好更快(但优化的实现需要比 FNV 和 DJB2 多行代码)

此处使用大输入字符串的一些基准测试:http : //jsperf.com/32-bit-hash
当对输入字符串进行哈希处理时,相对于 DJ2B 和 FNV-1a,murmur 的性能下降:http ://jsperf.com/32-位哈希/3

所以总的来说,我会推荐 murmur3。
在这里查看 JavaScript 实现:https : //github.com/garycourt/murmurhash-js

如果输入字符串很短并且性能比分发质量更重要,请使用 DJB2(如 esmiralha 接受的答案所建议的那样)。

如果质量和小代码量比速度更重要,我使用 FNV-1a 的这个实现(基于这个代码)。

/**
 * Calculate a 32 bit FNV-1a hash
 * Found here: https://gist.github.com/vaiorabbit/5657561
 * Ref.: http://isthe.com/chongo/tech/comp/fnv/
 *
 * @param {string} str the input value
 * @param {boolean} [asString=false] set to true to return the hash value as 
 *     8-digit hex string instead of an integer
 * @param {integer} [seed] optionally pass the hash of the previous chunk
 * @returns {integer | string}
 */
function hashFnv32a(str, asString, seed) {
    /*jshint bitwise:false */
    var i, l,
        hval = (seed === undefined) ? 0x811c9dc5 : seed;

    for (i = 0, l = str.length; i < l; i++) {
        hval ^= str.charCodeAt(i);
        hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
    }
    if( asString ){
        // Convert to 8 digit hex string
        return ("0000000" + (hval >>> 0).toString(16)).substr(-8);
    }
    return hval >>> 0;
}

提高碰撞概率

如此处所述,我们可以使用以下技巧扩展哈希位大小:

function hash64(str) {
    var h1 = hash32(str);  // returns 32 bit (as 8 byte hex string)
    return h1 + hash32(h1 + str);  // 64 bit (as 16 byte hex string)
}

小心使用它,但不要期望太多。

你为什么这样做("0000000" + (hval >>> 0).toString(16)).substr(-8);那不是一样(hval >>> 0).toString(16)吗?
2021-03-11 16:00:33
这会添加前导'0',以便生成的哈希值始终为 8 个字符长。在输出中更容易阅读和识别,但这是我个人的意见
2021-03-18 16:00:33
啊,好的,我明白了。对于 small hval(hval >>> 0).toString(16)可能少于 8 个字符,因此您可以用零填充它。我只是感到困惑,因为(hval >>> 0).toString(16)总是为我产生一个正好 8 个字符的字符串。
2021-03-20 16:00:33
我喜欢这个答案,因为它产生了一个更好的分布式散列:这里提出的其他函数将产生随后的散列值。例如`hash("example1") - hash("example2") == 1",而这个更不可预测。
2021-03-27 16:00:33
针对“FNV-1a 的分布比 DJB2 更好,但速度较慢”——我认为应该说 FNV1a 在使用 ES6Math.imul功能实现时可以非常快仅此一项就使其成为最高基准,并且从长远来看最终是比 DJB2 更好的选择。
2021-04-09 16:00:33

基于ES6 中接受的答案更小、可维护且适用于现代浏览器。

function hashCode(str) {
  return str.split('').reduce((prevHash, currVal) =>
    (((prevHash << 5) - prevHash) + currVal.charCodeAt(0))|0, 0);
}

// Test
console.log("hashCode(\"Hello!\"): ", hashCode('Hello!'));

编辑(2019-11-04)

单线箭头函数版本:

const hashCode = s => s.split('').reduce((a,b) => (((a << 5) - a) + b.charCodeAt(0))|0, 0)

// test
console.log(hashCode('Hello!'))

但比以下任何一个都慢得多:https : //jsperf.com/hashing-strings
2021-03-27 16:00:33
@BeetleJuice 更合适的问题是,如果您有一个旨在接受字符串的函数,那么为什么您的程序首先向它发送非字符串?也许这个错误表明调用者正在做奇怪的事情。深思熟虑。
2021-03-30 16:00:33
@deekshith 接受的答案用于hash |= 0转换为 32 位整数。这个实现没有。这是一个错误吗?
2021-03-30 16:00:33
有什么方法可以使这仅产生积极但仍然独特的结果?
2021-04-06 16:00:33
感谢分享我str += ""在散列之前添加以避免str.split is not a function在非字符串作为参数传递时引发异常
2021-04-09 16:00:33