【问题标题】:What would be a sensible way to implement a Trie in .NET?在 .NET 中实现 Trie 的明智方法是什么?
【发布时间】:2011-04-09 13:58:45
【问题描述】:

我了解trie 背后的概念。但是在实现方面我有点困惑。

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

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


更新:好的!根据 Jon 和 leppie 提供的非常有用的意见,这是我目前的想法:

(1) 我有Trie 类型,它有一个Trie.INodeCollection 类型的私有_nodes 成员。

(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,随后对Add 的调用会按照上述步骤从那里开始。

这有意义吗?这感觉像是一种改进,因为它在某种程度上减少了 Trie 的“体积”(节点不再是成熟的 Dictionary&lt;char, Trie&gt; 对象,直到它们有足够数量的子对象)。然而,它也变得更加复杂。是不是太纠结了?我是否采取了复杂的路线来实现本应直截了当的目标?

【问题讨论】:

    标签: .net data-structures dictionary implementation trie


    【解决方案1】:

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

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

    【讨论】:

      【解决方案2】:

      嗯,你需要每个节点都有一些有效实现IDictionary&lt;char, Trie&gt;的东西。您可以编写自己的自定义实现,根据它有多少子节点来改变其内部结构:

      • 对于单个子节点,仅使用 charTrie
      • 对于少量,请使用List&lt;Tuple&lt;char, Trie&gt;&gt;LinkedList&lt;Tuple&lt;char,Trie&gt;&gt;
      • 对于较大的数字,请使用Dictionary&lt;char, Trie&gt;

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

      【讨论】:

      • 你也可以压缩尾部,像单个子节点的情况。
      【解决方案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 没有的问题。

      【讨论】:

      • 对不起 - 我很欣赏这不一定回答如何实施的问题 - 只是提供一个替代方案:)
      【解决方案4】:

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

      Trie next = store[c-'A']
      

      其中 c 是当前查找字符。

      【讨论】:

      • 以数组作为存储的节点是我的首选方式——想不出更轻量级的方式
      • 我正在寻找更一般的案例。也就是说,我愿意接受也许 trie 真的不适合作为“一般情况”数据结构,在这种情况下,也许它只在这样的场景中才有意义(节点结构可以简化为普通数组)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-01-29
      • 1970-01-01
      • 2022-01-06
      • 2012-05-09
      • 1970-01-01
      相关资源
      最近更新 更多