等效的 JavaScript 哈希图

IT技术 javascript data-structures language-features hashmap
2021-02-08 17:48:06

正如在这个答案的更新 3 中明确指出的,这个符号:

var hash = {};
hash[X]

实际上并不散列对象X它实际上只是转换X为一个字符串(通过.toString()如果它是一个对象,或者其他一些用于各种原始类型的内置转换),然后在“ hash”中查找该字符串,而不对其进行散列处理也不检查对象相等性 - 如果两个不同的对象具有相同的字符串转换,它们将相互覆盖。

鉴于此 - JavaScript 中是否有任何有效的哈希图实现?

(例如,javascript hashmap对于任何操作,第二个 Google 结果产生的实现都是 O(n)。各种其他结果忽略了具有等效字符串表示的不同对象相互覆盖的事实。

6个回答

自己手动散列对象,并将结果字符串用作常规 JavaScript 字典的键。毕竟,您最了解是什么让您的对象与众不同。我就是做这个的。

例子:

var key = function(obj){
  // Some unique object-dependent key
  return obj.totallyUniqueEmployeeIdKey; // Just an example
};

var dict = {};

dict[key(obj1)] = obj1;
dict[key(obj2)] = obj2;

通过这种方式,您可以控制由 JavaScript 完成的索引,而无需大量内存分配和溢出处理。

当然,如果你真的想要“工业级解决方案”,你可以构建一个由关键函数参数化的类,并带有容器所需的所有API,但是……我们使用JavaScript,并力求简单和轻量级,所以这个功能性的解决方案既简单又快速。

密钥功能可以像选择对象的正确属性一样简单,例如,一个密钥或一组密钥,它们已经是唯一的,密钥的组合,它们一起是唯一的,或者像使用一些加密哈希一样复杂,例如在DojoX 编码DojoX UUID 中虽然后一种解决方案可能会产生唯一的键,但我个人会不惜一切代价避免它们,特别是如果我知道是什么让我的对象独一无二。

2014 年更新:在 2008 年回答这个简单的解决方案仍然需要更多的解释。让我以问答形式澄清这个想法。

您的解决方案没有真正的哈希值。它在哪里???

JavaScript 是一种高级语言。它的基本原语 ( Object ) 包括一个哈希表来保存属性。为了提高效率,这个哈希表通常用低级语言编写。使用带有字符串键的简单对象,我们使用了一个高效实现的哈希表,而我们无需付出任何努力。

你怎么知道他们使用哈希?

可以通过三种主要方法来保持一组对象可通过键寻址:

  • 无序。在这种情况下,要通过键检索对象,我们必须遍历所有在找到它时停止的键。平均而言,它将进行 n/2 次比较。
  • 订购了。
    • 示例#1:排序数组——进行二分搜索,我们将在平均 ~log2(n) 次比较后找到我们的键。好多了。
    • 示例#2:一棵树。再次尝试 ~log(n) 次。
  • 哈希表。平均而言,它需要一个恒定的时间。比较:O(n) vs. O(log n) vs. O(1)。繁荣。

显然,JavaScript 对象以某种形式使用哈希表来处理一般情况。

浏览器厂商真的使用哈希表吗???

真的。

他们处理碰撞吗?

是的。往上看。如果您发现不等字符串发生冲突,请不要犹豫,向供应商提交错误。

那么你的想法是什么?

如果你想散列一个对象,找到它的独特之处并将其用作键。不要试图计算一个真正的哈希或模拟哈希表——它已经被底层的 JavaScript 对象有效地处理了。

将此键与 JavaScript 结合使用Object以利用其内置哈希表,同时避免与默认属性可能发生冲突。

帮助您入门的示例:

  • 如果您的对象包含唯一的用户名 - 将其用作键。
  • 如果它包含唯一的客户编号 - 将其用作密钥。
    • 如果它包含唯一的政府颁发的号码,例如美国SSN或护照号码,并且您的系统不允许重复 - 将其用作密钥。
  • 如果字段组合是唯一的 - 将其用作键。
    • 美国州缩写 + 驾照号码是很好的钥匙。
    • 国家缩写+护照号码也是一个很好的关键。
  • 字段上的某些函数或整个对象可以返回唯一值 — 将其用作键。

我使用了您的建议并使用用户名缓存了所有对象。但是有一个聪明的家伙被命名为“toString”,这是一个内置属性!我现在应该怎么办?

显然,如果结果键完全由拉丁字符组成的可能性微乎其微,那么您应该对此采取一些措施。例如,在开头或结尾添加您喜欢的任何非拉丁语 Unicode 字符以取消与默认属性的冲突:“#toString”、“#MarySmith”。如果使用复合键,请使用某种非拉丁分隔符分隔键组件:“name,city,state”。

通常,这是我们必须发挥创造力并选择具有给定限制(唯一性、与默认属性的潜在冲突)的最简单键的地方。

注意:根据定义,唯一键不会发生冲突,而潜在的哈希冲突将由底层Object.

你为什么不喜欢工业解决方案?

恕我直言,最好的代码是根本没有代码:它没有错误,不需要维护,易于理解,并立即执行。我看到的所有“JavaScript 中的哈希表”都超过 100 行代码,并且涉及多个对象。比较一下:dict[key] = value

另一点:甚至有可能击败用低级语言编写的原始对象的性能,使用 JavaScript 和完全相同的原始对象来实现已经实现的内容吗?

我仍然想在没有任何键的情况下散列我的对象!

我们很幸运:ECMAScript 6(2015 年 6 月发布)定义了mapset

从定义来看,它们可以使用对象的地址作为键,这使得对象在没有人工键的情况下立即区分。OTOH,两个不同但相同的对象,将被映射为不同的对象。

来自MDN 的比较细分

对象与 Maps 的相似之处在于,它们都允许您将键设置为值、检索这些值、删除键以及检测某个键是否存储了某些内容。正因为如此(并且因为没有内置的替代品),对象在历史上一直被用作地图;然而,在某些情况下,使用 Map 有一些重要的区别:

  • 对象的键是字符串和符号,而它们可以是 Map 的任何值,包括函数、对象和任何原语。
  • Map 中的键是有序的,而添加到对象的键则不是。因此,当迭代它时,一个 Map 对象按插入的顺序返回键。
  • 您可以使用 size 属性轻松获取 Map 的大小,而 Object 中的属性数量必须手动确定。
  • Map 是可迭代的,因此可以直接迭代,而迭代 Object 需要以某种方式获取其键并对其进行迭代。
  • 一个对象有一个原型,所以如果你不小心,地图中有一些默认的键可能会与你的键发生冲突。从 ES5 开始,这可以通过使用 map = Object.create(null) 绕过,但很少这样做。
  • Map在涉及频繁添加和删除密钥对的场景中可能表现得更好。
这看起来不像一个合适的地图,因为你不处理碰撞。如果这恰好是真的:hash(obj1) == hash(obj2),你将会丢失你的数据。
2021-03-17 17:48:06
@MattR 实际上,即使使用模拟哈希函数,您的示例也可以在没有天堂帮助的情况下正常工作。我希望其他读者会意识到过度简化的非现实哈希函数被用作占位符来演示不同的技术。代码注释和答案本身都强调它不是真实的。答案的最后一段讨论了正确键的选择。
2021-03-17 17:48:06
@EugeneLazutkin 大多数人没有读到你在 ES6 出现之前就已经回答了这个问题......让我祝贺你对 JS 的深入了解。
2021-03-22 17:48:06
当“PAUL AINLEY”和“PAULA INLEY”都在您的系统中注册时,天堂会帮助您...
2021-03-25 17:48:06
@EugeneLazutkin——恐怕你还是错了。您的示例仍然容易发生哈希冲突。不要认为只将姓氏放在首位会以某种方式帮助您!
2021-04-10 17:48:06

问题描述

JavaScript 没有内置的通用映射类型(有时称为关联数组字典),它允许通过任意键访问任意值。JavaScript 的基本数据结构是object,一种特殊类型的映射,它只接受字符串作为键,并具有特殊的语义,如原型继承、getter 和 setter 以及一些进一步的伏都教。

将对象用作映射时,您必须记住,键将通过 转换为字符串值toString(),这会导致映射5'5'到相同的值以及所有不会将toString()方法覆盖到由 索引的值的对象'[object Object]'如果您不检查hasOwnProperty().

JavaScript 的内置数组类型没有一点帮助:JavaScript 数组不是关联数组,而只是具有一些特殊属性的对象。如果您想知道为什么它们不能用作地图,请查看此处

尤金的解决方案

Eugene Lazutkin 已经描述了使用自定义散列函数生成唯一字符串的基本思想,这些字符串可用于查找关联值作为字典对象的属性。这很可能是最快的解决方案,因为对象在内部实现为哈希表

  • 注意:哈希表(有时称为哈希映射)是映射概念的特定实现,它使用支持数组并通过数字哈希值进行查找。运行时环境可能会使用其他结构(例如搜索树跳过列表)来实现 JavaScript 对象,但由于对象是基本数据结构,因此应该对其进行充分优化。

为了获得任意对象的唯一哈希值,一种可能性是使用全局计数器并将哈希值缓存在对象本身中(例如,在名为 的属性中__hash)。

执行此操作并适用于原始值和对象的哈希函数是:

function hash(value) {
    return (typeof value) + ' ' + (value instanceof Object ?
        (value.__hash || (value.__hash = ++arguments.callee.current)) :
        value.toString());
}

hash.current = 0;

可以按照 Eugene 的描述使用此功能。为方便起见,我们将其进一步包装在一个Map类中。

我的Map实现

下面的实现将额外地将键值对存储在一个双向链表中,以允许对键和值进行快速迭代。要提供您自己的哈希函数,您可以hash()在创建后覆盖实例的方法。

// Linking the key-value-pairs is optional.
// If no argument is provided, linkItems === undefined, i.e. !== false
// --> linking will be enabled
function Map(linkItems) {
    this.current = undefined;
    this.size = 0;

    if(linkItems === false)
        this.disableLinking();
}

Map.noop = function() {
    return this;
};

Map.illegal = function() {
    throw new Error("illegal operation for maps without linking");
};

// Map initialisation from an existing object
// doesn't add inherited properties if not explicitly instructed to:
// omitting foreignKeys means foreignKeys === undefined, i.e. == false
// --> inherited properties won't be added
Map.from = function(obj, foreignKeys) {
    var map = new Map;

    for(var prop in obj) {
        if(foreignKeys || obj.hasOwnProperty(prop))
            map.put(prop, obj[prop]);
    }

    return map;
};

Map.prototype.disableLinking = function() {
    this.link = Map.noop;
    this.unlink = Map.noop;
    this.disableLinking = Map.noop;
    this.next = Map.illegal;
    this.key = Map.illegal;
    this.value = Map.illegal;
    this.removeAll = Map.illegal;

    return this;
};

// Overwrite in Map instance if necessary
Map.prototype.hash = function(value) {
    return (typeof value) + ' ' + (value instanceof Object ?
        (value.__hash || (value.__hash = ++arguments.callee.current)) :
        value.toString());
};

Map.prototype.hash.current = 0;

// --- Mapping functions

Map.prototype.get = function(key) {
    var item = this[this.hash(key)];
    return item === undefined ? undefined : item.value;
};

Map.prototype.put = function(key, value) {
    var hash = this.hash(key);

    if(this[hash] === undefined) {
        var item = { key : key, value : value };
        this[hash] = item;

        this.link(item);
        ++this.size;
    }
    else this[hash].value = value;

    return this;
};

Map.prototype.remove = function(key) {
    var hash = this.hash(key);
    var item = this[hash];

    if(item !== undefined) {
        --this.size;
        this.unlink(item);

        delete this[hash];
    }

    return this;
};

// Only works if linked
Map.prototype.removeAll = function() {
    while(this.size)
        this.remove(this.key());

    return this;
};

// --- Linked list helper functions

Map.prototype.link = function(item) {
    if(this.size == 0) {
        item.prev = item;
        item.next = item;
        this.current = item;
    }
    else {
        item.prev = this.current.prev;
        item.prev.next = item;
        item.next = this.current;
        this.current.prev = item;
    }
};

Map.prototype.unlink = function(item) {
    if(this.size == 0)
        this.current = undefined;
    else {
        item.prev.next = item.next;
        item.next.prev = item.prev;
        if(item === this.current)
            this.current = item.next;
    }
};

// --- Iterator functions - only work if map is linked

Map.prototype.next = function() {
    this.current = this.current.next;
};

Map.prototype.key = function() {
    return this.current.key;
};

Map.prototype.value = function() {
    return this.current.value;
};

例子

下面的脚本,

var map = new Map;

map.put('spam', 'eggs').
    put('foo', 'bar').
    put('foo', 'baz').
    put({}, 'an object').
    put({}, 'another object').
    put(5, 'five').
    put(5, 'five again').
    put('5', 'another five');

for(var i = 0; i++ < map.size; map.next())
    document.writeln(map.hash(map.key()) + ' : ' + map.value());

生成此输出:

string spam : eggs
string foo : baz
object 1 : an object
object 2 : another object
number 5 : five again
string 5 : another five

进一步的考虑

PEZ 建议toString()用我们的哈希函数覆盖该方法。这是不可行的,因为它不适用于原始值(更改原始值toString()是一个非常糟糕的主意)。如果我们想toString()为任意对象返回有意义的值,我们将不得不修改Object.prototype,有些人(不包括我自己)认为是verboten


可以从这里获得我的Map实现的当前版本以及其他 JavaScript 好东西

ES5 不赞成使用 callee ( goo.gl/EeStE )。相反,我建议Map._counter = 0,并在 Map 构造函数中执行this._hash = 'object ' + Map._counter++然后 hash() 变成return (value && value._hash) || (typeof(value) + ' ' + String(value));
2021-03-14 17:48:06
@jsc123:我会研究一下 - 现在你可以在pikacode.com/mercurial.intuxication.org/js-hacks.tar.gz 上获得存储库的转储
2021-03-18 17:48:06
2021-04-03 17:48:06
嗨@Christoph,你能把你的链接更新到我可以找到你的地图实现的地方吗?
2021-04-08 17:48:06

现在有一些非常好的外部库解决方案:

JavaScript 也有它的语言提供Map

这是迈向21世纪的道路。太糟糕了,我在用一些难看的自制地图完成我的代码后发现了你的帖子。WEEE 需要更多投票支持您的答案
2021-03-14 17:48:06
@CodeBling 不知道。我想我把它与地图功能混淆了。我要把它从答案中删除。
2021-03-14 17:48:06
Collections.js 有一些实现,但我在 underscore.js 或 lodash 中找不到任何实现......你在下划线中指的是什么有用的?
2021-03-15 17:48:06
这还算公平。任何考虑 Collections.js 的人都应该知道它以有问题的方式修改全局 Array、Function、Object 和 Regexp 原型(请参阅我在这里遇到的问题)。尽管我最初对 collections.js(以及这个答案)非常满意,但使用它的风险太高,所以我放弃了它。只有 kriskowal 的collections.jsv2 分支(特别是 v2.0.2+)消除了全局原型修改并且可以安全使用。
2021-04-03 17:48:06

根据 ECMAScript 2015 (ES6),标准 JavaScript 具有 Map 实现。可以在此处找到更多信息

基本用法:

var myMap = new Map();
var keyString = "a string",
    keyObj = {},
    keyFunc = function () {};

// Setting the values
myMap.set(keyString, "value associated with 'a string'");
myMap.set(keyObj, "value associated with keyObj");
myMap.set(keyFunc, "value associated with keyFunc");

myMap.size; // 3

// Getting the values
myMap.get(keyString);    // "value associated with 'a string'"
myMap.get(keyObj);       // "value associated with keyObj"
myMap.get(keyFunc);      // "value associated with keyFunc"
但它是否使用基于哈希的实现?显然,出于性能原因,这很重要,以防您在其他语言中使用哈希图
2021-04-08 17:48:06

这是使用类似于 Java 地图的简单方便的方法:

var map = {
              'map_name_1': map_value_1,
              'map_name_2': map_value_2,
              'map_name_3': map_value_3,
              'map_name_4': map_value_4
          }

并获得value:

alert(map['map_name_1']);    // Gives the value of map_value_1

... etc. ...
这仅适用于字符串键。我相信 OP 有兴趣使用任何类型的密钥。
2021-03-29 17:48:06