在.NET中实现Trie的合理方法是什么?

时间:2010-09-08 06:59:18

标签: .net data-structures dictionary implementation trie

我得到trie背后的概念。但是在实施方面我有点困惑。

我认为构建Trie类型最明显的方法是让Trie维护内部Dictionary<char, Trie>。事实上,我已经用这种方式编写了一个,并且可以工作,但是......这看起来有点矫枉过正。我的印象是trie应该是轻量级的,并且对于每个节点单独Dictionary<char, Trie>对我来说似乎不是很轻量级。

有没有更合适的方法来实现我缺少的这个结构?


更新:好的!根据Jon和leppie的非常有用的意见,这是我到目前为止所提出的:

(1)我有Trie类型,其中包含_nodes类型的私有Trie.INodeCollection成员。

(2)Trie.INodeCollection接口具有以下成员:

interface INodeCollection
{
    bool TryGetNode(char key, out Trie node);
    INodeCollection Add(char key, Trie node);
    IEnumerable<Trie> GetNodes();
}

(3)此接口有三种实现方式:

class SingleNode : INodeCollection
{
    internal readonly char _key;
    internal readonly Trie _trie;

    public SingleNode(char key, Trie trie)
    { /*...*/ }

    // Add returns a SmallNodeCollection.
}

class SmallNodeCollection : INodeCollection
{
    const int MaximumSize = 8; // ?

    internal readonly List<KeyValuePair<char, Trie>> _nodes;

    public SmallNodeCollection(SingleNode node, char key, Trie trie)
    { /*...*/ }

    // Add adds to the list and returns the current instance until MaximumSize,
    // after which point it returns a LargeNodeCollection.
}

class LargeNodeCollection : INodeCollection
{
    private readonly Dictionary<char, Trie> _nodes;

    public LargeNodeCollection(SmallNodeCollection nodes, char key, Trie trie)
    { /*...*/ }

    // Add adds to the dictionary and returns the current instance.
}

(4)首次构建Trie时,其_nodes成员为null。第一次调用Add会创建一个SingleNode,然后根据上述步骤从{{}}}调用{<1}}。

这有意义吗?这感觉就像是一种改进,因为它有点减少了Add的“庞大”(节点不再是完整的Trie对象,直到它们有足够的数量孩子的)。然而,它也变得更加复杂。它太复杂了吗?我是否采取了一条复杂的路线来实现应该直截了当的事情?

4 个答案:

答案 0 :(得分:4)

嗯,您需要每个节点都有有效实现IDictionary<char, Trie>的内容。您可以编写自己的自定义实现,根据其具有的子节点数来改变其内部结构:

  • 对于单个子节点,只需使用charTrie
  • 对于较小的号码,请使用List<Tuple<char, Trie>>LinkedList<Tuple<char,Trie>>
  • 如果数量较多,请使用Dictionary<char, Trie>

(刚看到leppie的答案,我相信这是他所谈论的那种混合方法。)

答案 1 :(得分:3)

在我看来,将它作为一个字典实现,并没有实现一个Trie - 它正在实现一个字典词典。

当我实施了一个trie时,我已经按照Damien_The_Unbeliever(+1那里)建议的方式完成了它:

public class TrieNode
{
  TrieNode[] Children = new TrieNode[no_of_chars];
}

这理想情况下要求您的trie仅支持no_of_chars指示的有限字符子集,并且您可以将输入字符映射到输出索引。例如。如果支持A-Z,那么你自然会将A映射到0,Z映射到25。

当您需要添加/删除/检查节点的存在时,您可以执行以下操作:

public TrieNode GetNode(char c)
{
  //mapping function - could be a lookup table, or simple arithmetic
  int index = GetIndex(c);
  //TODO: deal with the situation where 'c' is not supported by the map
  return Children[index];
} 

在实际情况中,我已经看到了这个优化,例如,AddNode将采用ref TrieNode,以便可以根据需要新建节点并自动放入父级TrieNode的Children中正确的地方。

您也可以使用三元搜索树,因为trie的内存开销可能非常疯狂(特别是如果您打算支持所有32k的unicode字符!)并且TST性能相当令人印象深刻(并且还支持前缀和放大器) ;通配符搜索以及汉明搜索)。同样,TST可以原生支持所有unicode字符,而无需进行任何映射;因为它们使用大于/小于/等于操作而不是绝对索引值。

我接受了代码from here并略微调整了它(它是在泛型之前编写的)。

我想你会对TST感到惊喜;一旦我实施了一个,我完全离开了Tries。

唯一棘手的事情是保持TST的平衡;你没有Tries的问题。

答案 2 :(得分:3)

如果您的角色来自有限集(例如只有大写拉丁字母),那么您可以存储26个元素数组,每个查找只是

Trie next = store[c-'A']

其中c是当前查找字符。

答案 3 :(得分:2)

有几种方法,但使用单链接列表可能是最简单和最轻量级的。

我会做一些测试来查看每个节点都有的子节点数量。如果不多(比如20或更少),链接列表方法应该比哈希表更快。您也可以根据子节点的数量进行混合方法。

相关问题