【问题标题】:How do I efficiently search this hierarchical structure?如何有效地搜索这种层次结构?
【发布时间】:2010-11-02 16:12:29
【问题描述】:

我有一个如下所示的数据结构:

public class Node
{
     public string Code { get; set; }
     public string Description { get; set; }
     ...
     public List<Node> Children { get; set; }
}

给定指定的Code,我想编写一个返回特定节点的方法。通常我只会在层次结构中进行递归遍历以找到节点,但我担心性能。层次结构中会有几千个节点,这个方法会被调用很多很多次。

如何构建它以使其更快?我是否可以使用现有的数据结构,该结构可能在 Code 上执行二进制搜索,同时保留层次结构,而无需自己重新实现某种形式的二进制搜索?

【问题讨论】:

  • 数据是否以任何方式组织/排序?如果孩子们的守则有一个模式或规则,您可以使用它来减少您的搜索空间。
  • @Abe:不,它在内存中。它已被序列化为一个 XML 文件,以便我可以在程序启动时检索它。
  • @jelbourn:代码的最大大小约为 16 个字符。除此之外,没有模式;代码是任意字符串。数据仅按层次组​​织。
  • 将节点保存在关键是代码的字典中?然后你得到一个 O(1) 的查找时间
  • 您将它们存储在单个字典中,并且字典的类型为 。然后字典包含实际的节点对象本身。当您完成从 xml 序列化节点对象时,只需将它们添加到此主字典集

标签: c# performance data-structures hierarchical-data


【解决方案1】:

以代码为键将所有节点添加到字典中。 (你可以做一次),字典中的查找基本上是O(1)。

void FillDictionary(Dictionary<string, Node> dictionary, Node node)
{
  if (dictionary.ContainsKey(node.Code))
    return;

  dictionary.Add(node.Code, node);

  foreach (Node child in node.Children)
    FillDictionary(dictionary, child)
}  

如果你知道根目录,用法将是:

var dictionary = new Dictionary<string, Node>();
FillDictionary(dictionary, rootNode);

如果不这样做,您可以在所有节点上使用相同的字典调用 FillDictionary() 方法。

【讨论】:

  • 如何将字典项与我的项关联?按索引[]?
  • 在从 XML 反序列化后,您可以将它们添加到字典中一次。
  • 你不需要EqualsGetHashCode
  • @Itay:是的,但是字典如何知道节点在原始集合中的位置?
  • @Robert:它不会 - 除非您将 ParentNode 添加到每个 Node
【解决方案2】:

这是我和其他人所谈论内容的完整实现。请注意,通过使用索引字典,您将使用更多内存(仅对节点的引用)以换取更快的搜索。我正在使用事件来动态更新索引。

编辑:添加了 cmets 并修复了一些问题。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reflection;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            // Create the root node for the tree
            MyNode rootNode = new MyNode { Code = "abc", Description = "abc node" };

            // Instantiate a new tree with the given root node.  string is the index key type, "Code" is the index property name
            var tree = new IndexedTree<MyNode, string>("Code", rootNode);

            // Add a child to the root node
            tree.Root.AddChild(new MyNode { Code = "def", Description = "def node" });

            MyNode newNode = new MyNode { Code = "foo", Description = "foo node" };

            // Add a child to the first child of root
            tree.Root.Children[0].AddChild(newNode);

            // Add a child to the previously added node
            newNode.AddChild(new MyNode { Code = "bar", Description = "bar node" });


            // Show the full tree
            Console.WriteLine("Root node tree:");
            PrintNodeTree(tree.Root, 0);

            Console.WriteLine();

            // Find the second level node
            MyNode foundNode = tree.FindNode("def");

            if (foundNode != null)
            {
                // Show the tree starting from the found node
                Console.WriteLine("Found node tree:");
                PrintNodeTree(foundNode, 0);
            }

            // Remove the last child
            foundNode = tree.FindNode("foo");
            TreeNodeBase nodeToRemove = foundNode.Children[0];
            foundNode.RemoveChild(nodeToRemove);

            // Add a child to this removed node.  The tree index is not updated.
            nodeToRemove.AddChild(new MyNode { Code = "blah", Description = "blah node" });

            Console.ReadLine();
        }

        static void PrintNodeTree(MyNode node, int level)
        {
            Console.WriteLine(new String(' ', level * 2) + "[" + node.Code + "] " + node.Description);

            foreach (MyNode child in node.Children)
            {
                // Recurse through each child
                PrintNodeTree(child, ++level);
            }
        }
    }

    public class NodeEventArgs : EventArgs
    {
        public TreeNodeBase Node { get; private set; }

        public bool Cancel { get; set; }

        public NodeEventArgs(TreeNodeBase node)
        {
            this.Node = node;
        }
    }

    /// <summary>
    /// The base node class that handles the hierarchy
    /// </summary>
    public abstract class TreeNodeBase
    {
        /// <summary>
        /// A read-only list of children so that you are forced to use the proper methods
        /// </summary>
        public ReadOnlyCollection<TreeNodeBase> Children
        {
            get { return new ReadOnlyCollection<TreeNodeBase>(this.children); }
        }

        private IList<TreeNodeBase> children;

        /// <summary>
        /// Default constructor
        /// </summary>
        public TreeNodeBase()
            : this(new List<TreeNodeBase>())
        {
        }

        /// <summary>
        /// Constructor that populates children
        /// </summary>
        /// <param name="children">A list of children</param>
        public TreeNodeBase(IList<TreeNodeBase> children)
        {
            if (children == null)
            {
                throw new ArgumentNullException("children");
            }

            this.children = children;
        }

        public event EventHandler<NodeEventArgs> ChildAdding;

        public event EventHandler<NodeEventArgs> ChildRemoving;

        protected virtual void OnChildAdding(NodeEventArgs e)
        {
            if (this.ChildAdding != null)
            {
                this.ChildAdding(this, e);
            }
        }

        protected virtual void OnChildRemoving(NodeEventArgs e)
        {
            if (this.ChildRemoving != null)
            {
                this.ChildRemoving(this, e);
            }
        }

        /// <summary>
        /// Adds a child node to the current node
        /// </summary>
        /// <param name="child">The child to add.</param>
        /// <returns>The child node, if it was added.  Useful for chaining methods.</returns>
        public TreeNodeBase AddChild(TreeNodeBase child)
        {
            NodeEventArgs eventArgs = new NodeEventArgs(child);
            this.OnChildAdding(eventArgs);

            if (eventArgs.Cancel)
            {
                return null;
            }

            this.children.Add(child);
            return child;
        }

        /// <summary>
        /// Removes the specified child in the current node
        /// </summary>
        /// <param name="child">The child to remove.</param>
        public void RemoveChild(TreeNodeBase child)
        {
            NodeEventArgs eventArgs = new NodeEventArgs(child);
            this.OnChildRemoving(eventArgs);

            if (eventArgs.Cancel)
            {
                return;
            }

            this.children.Remove(child);
        }
    }

    /// <summary>
    /// Your custom node with custom properties.  The base node class handles the tree structure.
    /// </summary>
    public class MyNode : TreeNodeBase
    {
        public string Code { get; set; }
        public string Description { get; set; }
    }

    /// <summary>
    /// The main tree class
    /// </summary>
    /// <typeparam name="TNode">The node type.</typeparam>
    /// <typeparam name="TIndexKey">The type of the index key.</typeparam>
    public class IndexedTree<TNode, TIndexKey> where TNode : TreeNodeBase, new()
    {
        public TNode Root { get; private set; }

        public Dictionary<TIndexKey, TreeNodeBase> Index { get; private set; }

        public string IndexProperty { get; private set; }

        public IndexedTree(string indexProperty, TNode rootNode)
        {
            // Make sure we have a valid indexProperty
            if (String.IsNullOrEmpty(indexProperty))
            {
                throw new ArgumentNullException("indexProperty");
            }

            Type nodeType = rootNode.GetType();
            PropertyInfo property = nodeType.GetProperty(indexProperty);

            if (property == null)
            {
                throw new ArgumentException("The [" + indexProperty + "] property does not exist in [" + nodeType.FullName + "].", "indexProperty");
            }

            // Make sure we have a valid root node
            if (rootNode == null)
            {
                throw new ArgumentNullException("rootNode");
            }

            // Set the index properties
            this.IndexProperty = indexProperty;
            this.Index = new Dictionary<TIndexKey, TreeNodeBase>();

            // Add the root node
            this.Root = rootNode;

            // Notify that we have added the root node
            this.ChildAdding(this, new NodeEventArgs(Root));
        }

        /// <summary>
        /// Find a node with the specified key value
        /// </summary>
        /// <param name="key">The node key value</param>
        /// <returns>A TNode if found, otherwise null</returns>
        public TNode FindNode(TIndexKey key)
        {
            if (Index.ContainsKey(key))
            {
                return (TNode)Index[key];
            }

            return null;
        }

        private void ChildAdding(object sender, NodeEventArgs e)
        {
            if (e.Node.Children.Count > 0)
            {
                throw new InvalidOperationException("You can only add nodes that don't have children");
                // Alternatively, you could recursively get the children, set up the added/removed events and add to the index
            }

            // Add to the index.  Add events as soon as possible to be notified when children change.
            e.Node.ChildAdding += new EventHandler<NodeEventArgs>(ChildAdding);
            e.Node.ChildRemoving += new EventHandler<NodeEventArgs>(ChildRemoving);
            Index.Add(this.GetNodeIndex(e.Node), e.Node);
        }

        private void ChildRemoving(object sender, NodeEventArgs e)
        {
            if (e.Node.Children.Count > 0)
            {
                throw new InvalidOperationException("You can only remove leaf nodes that don't have children");
                // Alternatively, you could recursively get the children, remove the events and remove from the index
            }

            // Remove from the index.  Remove events in case user code keeps reference to this node and later adds/removes children
            e.Node.ChildAdding -= new EventHandler<NodeEventArgs>(ChildAdding);
            e.Node.ChildRemoving -= new EventHandler<NodeEventArgs>(ChildRemoving);
            Index.Remove(this.GetNodeIndex(e.Node));
        }

        /// <summary>
        /// Gets the index key value for the given node
        /// </summary>
        /// <param name="node">The node</param>
        /// <returns>The index key value</returns>
        private TIndexKey GetNodeIndex(TreeNodeBase node)
        {
            TIndexKey key = (TIndexKey)node.GetType().GetProperty(this.IndexProperty).GetValue(node, null);
            if (key == null)
            {
                throw new ArgumentNullException("The node index property [" + this.IndexProperty + "] cannot be null", this.IndexProperty);
            }

            return key;
        }
    }
}

【讨论】:

  • 感谢您的代码,我会仔细研究它。 +1 努力。我现在感觉很糟糕;事实证明问题比我想象的要简单。
  • @Robert:没问题,我们都有这种情况。我认为关键是树和字典都指向同一个对象。无论您如何到达该节点,您始终拥有与子节点和所有节点相同的节点。另外,关于我的代码的注释:我认为您可以使用 ObservableCollection 之类的东西来代替 ReadOnlyCollection 和自定义事件,但我对此知之甚少,无法确定。
【解决方案3】:

如果没有任何基于比较的代码组织,您将无法阻止 O(n) 遍历树。但是,如果您在读取 ​​XML 文件以构建节点树的同时构建另一个数据结构(用于 O(1) 访问的字典或用于 O(log n) 访问的列表),则可以构建无需更多开销即可快速访问的附加结构。

【讨论】:

  • 字典如何知道节点在原始集合中的位置?
  • @Robert - 你不能在Node 中保留一个指向父级的反向链接,以便在找到Node 实例后提供位置信息吗?
  • 或者将反向链接填充到辅助数据结构中,尽管我宁愿将其粘贴在Node中。
【解决方案4】:

将它们存储在字典中,这将为您提供 O(1) 查找时间。

var dict = new Dictionary<string, Node>();
dict.Add(n.Code, n);

假设n 是一个含水的Node 对象。然后要获取特定的节点项,您可以这样做

var node = dict["CODEKEY"];

它将根据提供的密钥返回您的特定节点。您甚至可以使用以下方法检查节点是否存在:

if (d.ContainsKey("CODEKEY"))
{
   //Do something
}

为了知道节点在原始集合中的位置,您需要向 Node 类添加某种指针层次结构,以便它了解前一个节点

【讨论】:

    【解决方案5】:

    如果你可以改变节点的顺序,你可以发一个Binary Search Tree

    【讨论】:

    • 来自 OP:without re-implementing some form of binary search myself?
    • C# 的库中没有某种二叉树吗?如果不是的话有点不起眼
    • @Paul:不。@Abe:他没有说树。
    • 那么他可以在不进行二分搜索的情况下搜索二叉树中的项目吗? (我不是刻薄,只是好奇)
    • @Abe - 当然,您可以在不进行二分搜索的情况下搜索二叉树。你可以浏览每一个项目——没有什么能阻止你。事实上,如果二叉树是按Code排序的,而你想按Description进行搜索,则不能进行二分查找。当然,不在二叉树上进行二分搜索确实违背了目的:)
    【解决方案6】:

    This SO answer 引用了您应该可以使用的库?

    【讨论】:

      【解决方案7】:

      我会说实话;我很难理解Itay's suggestion 是如何不完全合理的。

      这是您所说的要求:

      给定指定的Code,我想编写一个返回特定节点的方法。

      所以Code 是独一无二的,我接受吗?那么没有什么能阻止你将所有的 Node 对象放入 Dictionary&lt;string, Node&gt; 中。

      在您对 Itay 的回答中,您这样说:

      我知道字典是一个平面索引,我只是还不明白该索引将如何进入我的分层数据结构并检索正确的节点。

      如果您的意思是您不了解字典将如何知道您的Node 在数据结构中的位置,那是因为它不是。这有关系吗?您没有在您的要求中说明您想知道节点在数据结构中的位置;您只指定要获取节点。为了做到这一点,字典只需要知道节点在内存中的位置,而不是在一些完全独立的数据结构中。

      提供一个简化的例子(如果我在这里侮辱你的智力,我很抱歉,但请耐心等待,因为这至少可以为其他人澄清这一点),假设你有一个简单的 LinkedList&lt;int&gt; 包含所有唯一整数.然后枚举这个列表并用它来构造一个Dictionary&lt;int, LinkedListNode&lt;int&gt;&gt;,这个想法是你希望能够根据它的值快速找到一个节点。

      字典是否需要知道在链表中每个节点的位置?当然不是——只有它在内存中的位置。一旦你使用字典根据它在 O(1) 中的值找到了你的节点,你当然可以使用节点本身轻松地向前或向后遍历链表,这恰好知道(通过设计)链表包含它。

      你的层次结构也一样,只是比链表复杂一点。但同样的原则也适用。

      【讨论】:

      • 我相信我现在看到 Itay 发布的代码就明白了。该字典仅保存对原始节点对象的对象引用的索引集合。对于我实施ParentNode 的建议,我有点偏离了方向……对此感到抱歉。我现在正在实现 Itay 的代码……我会让你知道它是怎么回事。
      【解决方案8】:

      为什么不使用SortedSet&lt;Node&gt; 构建一个包含所有节点实例的 BST?比较器将基于 Code - 容器的范围必须使其在所有成员中都是唯一的。

      【讨论】:

      • 如何在保留层次结构的同时在整个树中获得良好的搜索特性?
      • @Robert - 这是对您的父子结构的补充。您不能从孩子反向链接到父母以方便从位于 Node 的树步行吗?
      猜你喜欢
      • 1970-01-01
      • 2011-05-26
      • 1970-01-01
      • 1970-01-01
      • 2014-09-28
      • 1970-01-01
      • 1970-01-01
      • 2017-03-22
      • 1970-01-01
      相关资源
      最近更新 更多