【问题标题】:Is there any way to make a probabilistic constant time equality check on collection types?有没有办法对集合类型进行概率恒定时间相等检查?
【发布时间】:2012-03-26 18:03:15
【问题描述】:

问题

我想知道如何有效地比较两种集合类型(列表、集合、映射等)。应该注意的是,需要的是结构相等而不是基于引用的相等。

通常必须遍历集合的所有元素并在它们之间进行比较,每次比较的成本为 O(1),产生惊人的 O(n) 比较时间。

这可能会影响使用冲突检查相当昂贵的列表哈希表或使用合同设计(例如,将旧集合与新集合进行比较)。

电流解的方向

虽然我有一些方法可以确定快速解决方案,但它们似乎都是概率性/非确定性的。想法是,是否能够使用可以存储和比较的所有元素的某种唯一哈希。一个好的散列算法应该提供足够的熵,使得碰撞的可能性很小。

这种基于哈希的比较技术可以通过使用某种列表头的恒定时间比较来加强(比如比较前 10 个元素)。在开始时具有相同元素并使用良好哈希算法的两个列表在理论上应该提供一些独特的比较。

问题

是否可以创建一种恒定时间比较(泛化和专门化某个时间,如整数),是否可以通过唯一哈希技术来实现?

更新

为了澄清这个问题,我不需要一个完美的平等检查,而是一个快速的“预先平等”检查,作为一种在之后加快真正平等检查的方法。虽然许多哈希码实现对于集合比较很有用,但我也对列表(有序)比较感兴趣。

【问题讨论】:

  • 你关心集合中元素的顺序,还是只关心它是否包含任何顺序的相同对象?
  • 等式应该包含集合属性,即一个列表应该是有序的,一个集合应该包含唯一的元素等等。
  • 您的集合是否经常包含相同数量的元素,或者它们往往包含大量不同数量的元素?
  • @EricJ。对于不同数量的元素,通常很容易进行长度检查(如果设计正确,对于许多集合)。因此,它更多地是区分我正在寻找的相同数量元素的集合的一种方式。
  • 我会说计算集合和 有序 列表的哈希值在组合上没有区别。

标签: performance algorithm


【解决方案1】:

我花了几分钟用 C# 编写了这样一个集合类,源代码如下。我使用了通用的System.Collections.ObjectModel.Collection<T>,因为它很容易覆盖它的功能。

根本没有测试过,但恕我直言,它应该是一个可靠的开始。请注意,UpdateHash 考虑了索引(使哈希函数稍微好一点),而类似的 HashedSet<T> 将跳过这部分。

此外,由于 XOR 运算符的可逆性,在添加/删除时重新计算哈希需要 O(1) 复杂性。如果需要更好的散列,这些操作会增加到O(n),所以我建议进行分析,然后决定什么是最好的。

public class HashedList<T> : Collection<T>, IEquatable<HashedList<T>>
{
    private int _hash;
    private void UpdateHash(int index, T item)
    {
        _hash ^= index;
        if (item != null)
            _hash ^= item.GetHashCode();
    }

    #region Overriden collection methods

    protected override void InsertItem(int index, T item)
    {
        UpdateHash(index, item);
        base.InsertItem(index, item);
    }

    protected override void RemoveItem(int index)
    {
        UpdateHash(index, this[index]);
        base.RemoveItem(index);
    }

    protected override void ClearItems()
    {
        _hash = 0;
        base.ClearItems();
    }

    protected override void SetItem(int index, T item)
    {
        UpdateHash(index, this[index]);
        UpdateHash(index, item);
        base.SetItem(index, item);
    }

    #endregion 

    #region Value equality

    public bool Equals(HashedList<T> other)
    {
        if (other == null)
            return false;

        if (object.ReferenceEquals(this, other))
            return true;

        if (other.Count != this.Count)
            return false;

        if (other._hash != this._hash)
            return false;

        return CompareElements(other);
    }

    private bool CompareElements(HashedList<T> other)
    {
        for (int i = 0; i < this.Count; i++)
        {
            if (this[i] == null)
            {
                if (other[i] != null)
                    return false;
            }

            if (this[i].Equals(other[i]) == false)
                return false;
        }

        return true;
    }

    public override bool Equals(object obj)
    {
        var hashed = obj as HashedList<T>;
        if (hashed != null)
            return Equals(hashed);

        return base.Equals(obj);
    }

    public override int GetHashCode()
    {
        return _hash;
    }

    #endregion
}

您也可以争辩说,如果传递了具有相同元素的任何 IList&lt;T&gt; 实现,object.Equals 应该返回 true,但由于它们的哈希码会不同,因此会破坏一致性。这是object.Equals IIRC 的推荐实现。

【讨论】:

  • 您的 UpdateHash 有缺陷,当您存储 2 个相等的元素时,它们的哈希码会自行抵消。 Mush 更好:_hash ^= index; _hash ^= index * item.GetHashCode()(你可能想在这里使用unchecked
  • @maniek:嗯,这只是一个简单的概念,我说我根本没有测试过它,也没有检查过它。而且你不能真正说哈希是有缺陷的,除非它为相同的输入提供不同的值。在大多数情况下,它有助于丢弃不同的集合,这一点很重要。但是,是的,您的建议似乎是一种改进。
  • @maniek:一个小问题,如果你从列表中删除,那么你需要支付 O(n) 来重新计算哈希码。尽管我普遍同意您的评估,即这是一个非常幼稚的哈希码提议。
  • @Mikola 不,您不需要重新计算,它可以正常工作(好吧,除了 index=0 的情况,但它很容易修复)
  • 我在使用 C# 代码的基础上接受了这个答案,这可以为未来寻找类似答案的参考提供有用的参考。
【解决方案2】:

使用基于哈希的比较。

哈希(SetA)与哈希(SetB)。

PS:在计算哈希之前,您需要对集合中的元素进行排序(或任何其他确定性排序)。哈希可能匹配,但集合不匹配(由于哈希冲突),但发生这种情况的可能性非常低。

PS:PS: 我假设这些集合是静态的(或者,几乎是静态的)。在这种情况下,您可以在创建集合本身的过程中预先计算哈希值。所以每次比较都是 O(1)。否则,正如 Groo 所提到的,使用基于 XOR 的哈希非常有效。

跟进:使用信息论可以证明,如果 X 和 Y 各自可以取 2^n 个唯一值,则至少需要进行 O(n) 次比较。没有办法解决这个问题。哈希给你的是有效比较的能力。

【讨论】:

  • 如果xor 用于散列,那么它应该可以在没有排序的情况下工作(并且删除元素也应该按预期运行)。
  • 如果你走那条路,你的散列算法很重要。 OP 特别不想检查引用相等性,而是“结构”(我猜这意味着值)相等性。如果哈希算法是基于对象地址的,这个方法会失败。哈希必须基于对象中包含的值。
  • 我看不出哈希解决方案怎么可能比 O(n) 更快。任何对容器有用的散列函数都必须依赖于容器上的每个元素。我也不确定为什么 o.p.认为 O(n) 对于比较容器来说“非常慢”。
  • @japreiss:检查相等性的平均时间应该是 O(1)。创建散列总是比简单地比较元素更昂贵(除非您选择 const 散列),但您只需为集合执行一次,一旦完成,您可以快速丢弃不相等的集合。对于许多比较,它会有所作为。
  • 如果您可以预先计算/缓存集合的哈希值并在 O(1) 中为每次插入和删除更新它,那么比较也将是 O(1)...
【解决方案3】:

可以使用bloom filters 来完成这项任务[for sets]。每个集合还将附加一个布隆过滤器。

如果两个过滤器相同 - 结构可能相同

如果两个过滤器不相同 - 结构明显不同

好的一面:
没有假阴性。如果过滤器不同 - 结构不同。

缺点:
您可能有误报。您将需要进行额外的检查 [full tr​​aversal] 以确保 2 个结构确实相同。

请注意,误报率是布隆过滤器大小的函数 - 它越大 - 你得到的误报越少。

另请注意:由于布隆过滤器实际上是位集 - 比较两个布隆过滤器可以非常有效地实现。

【讨论】:

  • 如果没有从集合中删除任何元素,我认为布隆过滤器是一个很好的建议 (+1)。如果使用许多散列函数过于昂贵,您可以放弃散列函数的独立性并获得更快的计算(参见例如spinroot.com/spin/Workshops/ws04/040-spin2004.pdf)。
  • @DaveBall:我实际上不认为布隆过滤器与 OP 的问题有任何关系。 OP 不想检查一个元素是否是集合的一部分,而是想比较两个集合(在他/她的情况下,每个集合都是一个“单个元素”,尽管它是值得的)。为一个集合创建一个布隆过滤器,只是为了将其与不同的布隆过滤器进行比较,这与创建单个哈希值(任意大小)进行比较没有什么不同。
  • @Groo:你的最后一句话不是表明布隆过滤器确实与 OP 的问题有关吗?对于集合来说,完整的布隆过滤器不是一个很好的哈希值吗?我看到的缺点是它的计算成本很高,我之前的评论是一个解决方案。
  • @DaveBall:是的,布隆过滤器可以工作,但不,它们不是集合的好哈希值。如果您需要检查一个元素是否属于一个集合,它们是完美的,但它们根本不是为此目的而创建的。每次添加/删除元素时,使用 k 哈希函数计算过滤器都需要 O(k*n),当只有一个通用函数(如 murmurhash)产生相同的结果时,没有理由这样做碰撞条款。当用作普通哈希值时,增加或减少 k 对位数组的质量也没有直接影响。
  • 令 a \in S,b \not\in S,使得 murmur(a) == murmur(b)。然后杂音(S 用 b 代替 a)== 杂音(S),对吗?如果您使用多个散列函数,则可以避免这种可能的冲突,但会产生不同的冲突,但这可能更不可能(例如,使用不同大小的集合)。正如我之前提到的,您不需要纯布隆过滤器,使用多个不独立的哈希函数几乎可以完成同样的工作,并且几乎与单个哈希函数一样快。
【解决方案4】:

这是关于该主题的非常有用(且详细)的讨论,包括几种集合类型的参考实现。

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2986.pdf

一般来说,计算排列是一个二次运算。然而, 给定两个使用相同哈希的无序容器和 键等价函数,元素将被划分为 使比较更有效的键等效组。

【讨论】:

    【解决方案5】:

    不,这理论上是不可能的:如果你有 32 位的哈希值,你只能区分 2^32 个变体,但列表可以任意增长。使用相同的论点,在运行时

    如果您不想 100% 保证,当然可以使用哈希函数。不过,我不会重新发明轮子,这通常会导致比使用标准库更糟糕的结果。例如,您可能会忘记:

    【讨论】:

    • 当然,对于所有类型的长度的所有类型的集合,理论上都不可能这样做。我所需要的只是一种强大的概率方法,这样就可以更快地检查集合不等式。
    • 我相信“恒定时间”OP 的意思是“平均恒定时间”。与任何散列解决方案一样,很明显会有冲突,但重要的是不同的散列值总是意味着集合是不同的。如果要进行许多比较,尝试减少搜索空间是合理的。而且,根据散列函数的不同,32 位在今天通常的 RAM 大小方面实际上是一个相当大的数字。
    • 如果比较数量相对较少的集合和足够大的散列(我偏爱 64 位城市散列),两个长度相等的集合(!)也有相同的机会哈希值非常小。计算机被流星击中的可能性比您成为 64+ 位哈希和相当少量集合的生日问题的受害者更有可能。
    • 这个答案有点误导“但列表可以任意增长”,因为您可以通过让哈希大小任意增长来做出回应。 @EricJ.:你可能会遇到生日悖论:只需要十亿个元素就可以遇到 64 位的生日悖论;十亿个 64 位元素可以合理地放入现代 RAM。但是,如果您只是使用 128 位哈希,问题就会消失。
    【解决方案6】:

    如果您使用安全的散列函数,那么发生冲突的可能性就会非常小(如果您使用最新的散列函数,如果发现冲突,您可以写一篇论文 :-))。

    如果您的集合被实现为一棵树,那么您可以维护一个从叶子到根计算的哈希,其成本是您必须执行的树更新成本的常数倍。不幸的是,计算安全散列的常数因素可能相当大。不幸的是,您需要两个具有相同对象的集合才能具有相同的树结构。这适用于http://en.wikipedia.org/wiki/Radix_tree,但不适用于典型的平衡树,其中历史或更新会影响树结构。

    完美的哈希函数通常被调整为完美适合特定的集合,这可能不适用于您的情况。如果一个哈希函数映射到数字 1..N,那么给定 N+1 个对象,总是至少会发生一次冲突。

    【讨论】:

      【解决方案7】:

      阅读您的问题后,我的第一个想法是您所说的“概率”是什么意思。您是否将概率方法视为获得某种正确(无错误)的精确(可数)答案的方法?还是您愿意在结果中产生一些错误?

      在后一种情况下,您可以在应用对数函数后比较数据时利用渐近“等价性”。考虑Linear Counting

      • 创建一个大小为 m 的零初始化位图 b
      • 选择一个哈希函数f
      • 对每个输入应用f,得到值v
      • 将位置 v 的位图设置为 1

      计算计数,公式为:

      n = -m * ln(Un / m)

      地点:

      • n -> 近似计数
      • Un -> m 中的零位数

      要适当调整 m 的大小,请参阅上面的原始论文链接。另外,请参阅最近的博客文章,其中还包括 HyperLogLog:

      http://highscalability.com/blog/2012/4/5/big-data-counting-how-to-count-a-billion-distinct-objects-us.html

      【讨论】:

      • 引人入胜的阅读,谢谢。然而,链接的博客文章没有提到他们真正想要做的事情:计算唯一元素的总数,而不是元素的总数;这让前五分钟的阅读有点混乱。希望这可以为其他读者澄清一些事情。
      【解决方案8】:

      我会选择

      hash(structure) := hast(item1) ^ hash(item2) ^ ... ^ hash(item_n)
      

      取决于散列函数(首先是它的输出大小),这会给你一个很好的误报概率。它不会产生假阴性,并且很容易在很短的恒定时间内通过插入和移除来实现。他们击败了布隆过滤器,因为误报概率不取决于元素的数量。

      对于数组或列表 - 具有相同内容但顺序不同的数组的可能性有多大?如果是,您可以轻松地使散列依赖于项目位置:

      hash(structure) := hast(item1, 1) ^ hash(item2, 2) ^ ... ^ hash(item_n, n)
      

      在这种情况下,数组末尾的删除和插入可以是 O(1)。中间的随机插入比较困难,但话说回来,无论如何,它们对于数组来说都是 O(n)。

      【讨论】:

        猜你喜欢
        • 2020-04-08
        • 2017-08-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-02-08
        • 1970-01-01
        • 2019-07-12
        • 1970-01-01
        相关资源
        最近更新 更多