10

就计算复杂度而言,哪种数据结构最适合实现 (key,val) 项的字典,它必须支持以下命令:

  1. Insert(key)- 用 val=1 附加一个项目 (key,val)
  2. Increment(key)- 增加已存在的 val (key,val)
  3. Find(key)- 返回 (key,val) 的 val
  4. Select(part_of_key)strstr(key,part_of_key)!=NULL- 如果以相同类型的新字典的形式返回所有项目 (key,val) 的列表(如果可能,不分配新内存);例如,如果字典是 {(red,3), (blue,4), (green,1)},则 Select(re)={(red,3), (green,1)}
  5. Max(i)- 返回所有项目中具有第 i 个最大值的项目;例如,如果字典是 {(red,3), (blue,4), (green,1)},则 Max(1)=blue, Max(2)=red, Max(3)=green

键是字符串,值是正整数。字典中的项目数量预计会非常大。

我认为它一定是两种不同数据结构的综合。但它应该是哈希表 + 二叉树还是 trie + 排序数组或其他?

4

5 回答 5

6

平衡树(如红黑树)和后缀树(或后缀数组)的组合。

  • 平衡树:操作 1、2(实现为 remove + insert)、3 和 5。
  • 后缀树:操作 4。

注意:哈希表将无法有效地支持操作 5。

我认为您将很难实现后缀树。您可以使用Mark Nelson 的 Ukkonen 算法的 C++ 实现,但它存在内存泄漏并且本质上是一个单例,因此您需要在准备好用于生产之前对其进行清理。即使在你修复它之后,你也需要对其进行调整,以便它与你的“其他”数据结构(在我的建议中是平衡树)而不是一个大的普通字符串一起工作。

如果您比操作 4 更频繁地执行操作 1 和/或您可以使用线性操作 4,我建议您跳过后缀树的整个复杂性,而只是线性地遍历您的数据结构。

于 2011-09-23T12:22:35.537 回答
4

对于前三个操作,哈希表可能是个好主意。

对于第 4 次操作(选择键的一部分),您可能必须以不同的方式编写散列函数。是的,用于从给定键中查找/计算哈希值的哈希函数。由于您想支持部分匹配并且您的密钥是字符串,您可能需要使用 Suffix-tree 或 trie。

对于第 5 个操作(第 i 个最大元素),您可能希望维护与哈希表交互的堆或排序的链接列表(或跳过列表)。

您还必须查看各种用例并找到应该优化的操作。例如:如果你对 part_of_key 操作有很多查询,你应该使用 Suffix-tree/LC-trie 类型的结构,这样会得到很好的结果。但是,您的 Find 操作可能不会很快,因为它需要 O(logN) 而不是不断查找。

总而言之,你需要整合哈希表、堆和后缀树来实现所有的操作。

于 2011-09-23T11:23:21.287 回答
3

虽然确切的答案取决于您的操作频率,但您的选项列表应包含一个后缀数组

于 2011-09-23T11:21:13.860 回答
1

我认为一个有效的数据库将是一个修改过的trie,具有双向链接[可以从叶子到根并重建单词],并且每个终止节点都会有额外的“值”字段。
您还需要一个多重映射[即一个映射,其中每个值都是一组地址]。
键将像树一样排列,值集将基于散列。[在jave中,应该是这样的TreeMap<Integer,HashSet<Node>>]

伪代码:[嗯,非常伪......它只是显示了每个操作的一般想法]。

Insert(key):
1. Insert a word into the trie
2. add the value `1` to the terminating node.
3. add the terminating field to the multimap [i.e. map.add(1,terminating_node_address)]
Increment(key):
1. read the word in the trie, and store the value from the terminating node as temp.
2. delete the entree (temp,terminating_node_address) from the multimap.
3. insert the entree (temp+1,terminating_node_address) to the multimap.
4. increase the value of the terminating node's address by 1.
Find(key):
1. find the terminating node for the key in the trie, return its value.
Select(part_of_key):
1. go to the last node you can reach with part_of_key.
2. from this node: do BFS and retrieve all possible words [store them in an empty set you will later return].
Max(i):
1. find the i'th biggest element key in the multimap.
2. choose an arbitrary address, and return get the relevant terminating word node.
3. build the string by following the uplinks to the root, and return it.

复杂性:
设为n字符串的数量、k最大值和S字符串的长度。
插入:O(S) [trie 插入是 O(S)]。地图中的最小元素 (1) 可以被缓存,因此可以在 O(1) 时访问。
增量:O(S+logk):在trie中找到字符串是O(S),找到相关键是O(logk),从哈希集中删除元素是O(1),找到下一个元素树也是 O(logk) [实际上可以是 O(1),对于基于跳过列表的地图],插入一个值是 O(1)。请注意,通过将节点链接到映射中的相关键,复杂性甚至可以提高到 O(S),因为实际上不需要搜索。
查找:O(S):只需在 trie 中找到并返回值。
Select: O(S*n):在最坏的情况下,您需要检索所有元素,这会强制您迭代整个 trie 以找到所有单词。
Max: O(logk+S) : 在有序映射中找到一个键,重构这个词。

编辑:请注意,在这里,我假设 find(i) 你想要第 i 个不同的最大元素。如果不是这种情况,您将需要稍微修改集合,并使用允许重复键的多映射 [而不是将集合作为值]。这将使所有基于树的操作 O(logn) 而不是 O(logk),在我看来仍然有效......

于 2011-09-26T13:40:56.843 回答
1

我认为最好有几个捷径。

1-3 已经被尽可能快的 trie 支持(密钥的长度)。

对于 4,我将向可以从该字符输入的所有 trie 节点添加一个包含 256 个元素的附加表。通过这种方式,我可以快速查找部分字符串,而无需像在后缀树中那样明确地构造或存储后缀。

对于 5,我会在叶子上添加一个链表,在插入数据时会更新排序。您可能需要再次引用列表的头部和 trie 中的反向链接,以找到正确的值并获取密钥。

内存复杂度不会随着这些添加而改变,因为它受 trie 节点数量的限制。但是,由于链表的排序效率低下,插入所花费的时间可能会发生变化。

最终的结构可能应该称为 hash-leaf-trie。但我不想实现这样的野兽。

于 2011-09-23T14:05:05.117 回答