【问题标题】:Data structure with O(1) lookup time which will allow duplicates [closed]具有O(1)查找时间的数据结构将允许重复[关闭]
【发布时间】:2012-11-17 21:32:45
【问题描述】:

我的目标是创建一个实现IList<T> 接口的数据结构,它将通过牺牲内存来实现O(1) 元素查找时间。

背景 如您所知,所有基于数组的IList<T> 实现(如List<T>)都有O(n) 元素查找时间。这意味着像 int IndexOf(T element)bool Contains(T element) 这样的操作会遍历底层数组,直到找到匹配项。

众所周知的想法是使用列表和哈希表的组合作为底层数据结构。值保存在列表中。哈希表将索引作为值和列表的值作为键。因此可以使用哈希表进行查找。

这正是KeyedCollection<TKey, TItem> see MSDN 的实现方式。

到目前为止我已经尝试过什么

internal class MyList<T> : KeyedCollection<T, T>
{
    protected override T GetKeyForItem(T item)
    {
        return item;
    }
}

到目前为止,除了一个问题外,这一直有效。这个数据结构并没有完全模仿List&lt;T&gt; 背后的预期行为。关键是List&lt;T&gt; 允许重复,MyList 不允许。

问题

有没有现成的数据结构,或者你能推荐一种优雅的方式来实现IList&lt;T&gt;,以便:

  1. 查找操作的时间为O(1)
  2. 所有其他操作具有与O() 相同的性能List&lt;T&gt;
  3. 内存可能会受到哈希表开销(constantA + constantB * n 字节)的影响。
  4. 必须允许重复
  5. 允许空值是可选的(可以将它们装箱到空对象中)

【问题讨论】:

  • “必须允许重复”是什么意思。是否需要保留重复的每个实例,忽略重复,覆盖重复?
  • 我认为其中最棘手的部分将匹配IndexOf 的定义,该定义声明:“如果一个对象在列表中多次出现,IndexOf 方法总是返回找到的第一个实例。”
  • 您不断将您的数据结构与ListSet 同时进行比较。两者之间不仅性能不同,而且 API 也有很大差异。集合是无序的,列表是有序的,这产生了很大的不同。你的收藏会被订购吗?拥有一个允许重复的集合/映射称为“包”(仍然是无序的)。不存在具有 O(1) 搜索速度的列表。
  • @achitaka-san 那么这是不可能的。完成。
  • 最好的情况是你嫁给 Dictionary&lt;T,LinkedList&lt;T&gt;&gt;List&lt;LinkedListNode&lt;T&gt;&gt;。处理dictMapListHashStructKerfloovy[17] = newValue; 之类的事情太可怕了。

标签: c# .net performance data-structures collections


【解决方案1】:

我能看到这一点的唯一方法是使用列表字典。点击该键将为您提供创建该特定键的所有重复项的列表。总是拿第一个。

【讨论】:

  • 如果他想要一张带有重复键的地图,那是真的。如果他想要一套,那是行不通的。 (关于他在 OP 中暗示的内容,他在两者之间切换,所以我不知道他真正需要哪个。)
  • 谢谢。当然会奏效。我也考虑过类似的解决方案。问题是被覆盖的列表很少包含一个以上的元素。仍在等待更“优雅”的解决方案,否则会接受你的。
  • @achitaka-san 所以你不需要一个有序的集合,尽管你重复的 cmets 另有说明?
  • Ryan,显然他需要一个有序的集合,所以这不是一个选择。
  • @Servy - 我想我开始理解你了。您能否更好地解释一下为什么此解决方案中没有保留订单?
【解决方案2】:

如果你可以开发一个搜索时间为 O(1) 的结构,你会发现自己变得非常非常富有:p

这种结构基本上不存在,最接近this的是Hash table

C# 有一个内置的哈希表类型 - C~ Hash Table

【讨论】:

  • OP 的整个前提是他正在寻找允许重复的哈希表;这表明他已经熟悉哈希表的概念,但它并不能解决他的问题。
  • 是的……他要找的东西并不存在。甚至哈希表本身也没有 O(1) 的搜索时间。
  • 是的,这里可以实现的最好的事情是平均情况 O(1) 查找。在最坏的情况下,哈希表仍然是 O(n)。也就是说,平均情况 O(1) 查找确实很有价值,只是说。
  • @EamonnMcEvoy 哈希表搜索时间 O(1)...在平均情况下。是的,最坏的情况有所不同,但经过数十年的 CS 研究和实验,这种情况非常罕见,几乎不值得一提(在讨论成熟的实际实现时)。
  • @delnan 实际上,基于哈希的结构的 O-ness 主要基于密钥的哈希函数的质量。如果您将自定义类型与自定义散列函数一起使用,并且实现不佳,则很容易以 O(n) 访问时间结束(或者更糟糕的是,如果您的散列函数无法在 O(1) 时间内计算)。现在,当使用诸如字符串之类的东西时,或者其他带有库编写的散列函数的常用散列键时,您通常可以相信它们写得足够好,从而导致(足够接近)O(1) 访问。
【解决方案3】:

哈希表应该包含每个键的索引列表。我想这就是你所需要的,不是吗?

【讨论】:

  • 但是您的所有方法都没有适当的性能。搜索可以,但不能删除,因为您需要维护这两种数据结构。
【解决方案4】:

基于Ryan Bennett 提出的建议,我认为您将提出的最好办法(因为您声明顺序很重要)是创建一个实现 IList 的类,然后在内部有这样的东西:

class MyList<T> : IList<T>
{
    Dictionary<T, List<int>> _indexMap;
    List<T> _items;


    public int IndexOf(T item)
    {
        List<int> indices;
        if(_indexMap.TryGetValue(item, out indices))
        {
            return indices[0];
        }
        return -1;
    }

    public void Add(T item)
    {
        List<int> indices;
        if(!_indexMap.TryGetValue(item, out indices))
        {
            indices = new List<int>();
            _indexMap[item] = indices;
        }

        indices.Add(_items.Count);
        _items.Add(item);
    }

    // Attempt at a Remove implementation, this could probably be improved
    // but here is my first crack at it
    public bool Remove(T item)
    {
        List<int> indices;
        if(!_indexMap.TryGetValue(item, out indices))
        {
            // Not found so can just return false
            return false;
        }

        int index = indices[0];
        indices.RemoveAt(0);
        if (indices.Count == 0)
        {
            _indexMap.Remove(item);
        }

        for(int i=index+1; i < _items.Count; ++i)
        {
            List<int> otherIndexList = _indexMap[_items[i]];
            for(int j=0; j < otherIndexList.Count; ++j)
            {
                int temp = otherIndexList[j];
                if (temp > index)
                {
                    otherIndexList[j] = --temp;
                }
            }
        }

        return _items.RemoveAt(index);
    }

    // ... Other similar type functions here
}

编辑:

刚刚意识到,当您执行Remove 时,事情变得非常棘手。您将不得不遍历索引集合并使用值 > 您删除的项目的索引来更新任何索引。您现在已经增加了“删除”时间。你也让正确变得很棘手。如果你要尝试实现这样的东西,我会围绕这个集合进行大量的单元测试。

我知道你说顺序很重要,所以我假设这就是为什么你不使用允许重复并给你 O(log n) 操作时间的排序列表方法。

编辑 2:另一种簿记类型方法
我只是在脑海中弹跳这个,所以我只会给出一些粗略的伪代码,但你可能会采取一种方法,你只有一个映射到索引列表的项目字典和一个将索引映射到项目的第二个字典.如果您添加 T 是一个类的限制,那么您只需支付两次存储参考的开销。然后,您需要维护当前的“最后一个”,以便您可以轻松地将新项目添加到集合中。这应该使删除操作更干净一些。它仍然是 O(n),因为您必须使用索引 > 已删除项目来更新任何内容。在最初的想象中,这似乎是一个潜在的解决方案,可以让您接近您想要实现的目标(如果我正确理解目标的话)。

【讨论】:

  • 我会使用LinkedList&lt;int&gt; 而不是List&lt;int&gt;。这避免了数组移位 List&lt;int&gt;MyList&lt;T&gt; 上调用 Remove(T item) 时会发生,方法是始终通过调用 RemoveFirst() 删除 First 元素。 :)
  • @hIpPy 实际上,LinkedList 在实践中几乎肯定会更慢。如果您有对节点的引用,则添加/删除操作是 O(1),但搜索速度不再是 O(1),而是 O(n),而且是特别慢的 O(n)。与基于数组的结构相比,链表会失去内存逻辑性,因此使用它们会导致对项目的访问速度更慢、缓存未命中率更高等。这极大地损害了具有相同大 O 值的操作的性能基于数组的结构。最重要的是,由于内存碎片,最终会使用更多的内存。
  • @Servy,或者可以从List&lt;int&gt;中删除最后一个元素。
  • @hIpPy 不知道你想说什么。从List 中删除最后一个元素是 O(1)。我假设您的意思是说第一个,因为那将是 O(n)?从List 中删除第一项实际上比从LinkedList 中删除最后一项要快得多,尽管它们都是 O(n)。
  • @pstrjds 恐怕我不明白为什么 O(n^2)?插入的 O(n) + 更新的 O(n) 仍然是 2 * O(n) = O(n) 而不是 O(n^2)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-25
  • 2020-02-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-13
相关资源
最近更新 更多