如何有效地搜索此层次结构?

时间:2010-11-02 16:12:29

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

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

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

我想编写一个方法,在给定指定的Code的情况下返回特定节点。通常我会在层次结构中进行递归遍历以找到节点,但我关注性能。层次结构中将有数千个节点,这种方法将被多次调用。

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

8 个答案:

答案 0 :(得分:13)

将所有节点添加到字典中,并将代码作为键。 (你可以做一次),字典中的查找基本上是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)
}  

如果您知道root,则用法为:

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

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

答案 1 :(得分:3)

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

编辑:添加评论并修正了一些内容。

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;
        }
    }
}

答案 2 :(得分:2)

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

答案 3 :(得分:2)

将它们存储在字典中,这为您提供了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类添加某种指针层次结构,以便它了解前一个节点

答案 4 :(得分:1)

如果您可以更改节点的顺序,则可以创建Binary Search Tree

答案 5 :(得分:1)

This SO answer引用了一个你应该可以使用的库吗?

答案 6 :(得分:1)

我会诚实的;我很难理解Itay's suggestion如何没有完全理解。

以下是您已声明的要求:

  

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

所以Code是唯一的,我接受了吗?然后,没有什么可以阻止您将所有Node个对象放入Dictionary<string, Node>

在你对Itay回答的评论中,你这样说:

  

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

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

提供一个简化的例子(如果我在这里侮辱你的情报,我道歉,但是请耐心等待,因为这可能至少澄清了其他人的观点),假设你有一个包含所有人的简单LinkedList<int>独特的整数。然后,您枚举此列表并使用它来构造Dictionary<int, LinkedListNode<int>>,这个想法是您希望能够根据其值快速查找节点。

字典是否需要知道每个节点中在链接列表中的位置?当然不仅仅是它在记忆中的位置。一旦使用字典在O(1)中找到了基于其值的节点,您当然可以使用节点本身轻松地向前或向后遍历链表,这恰好意识到(按设计)链表包含它。

它与您的层次结构相同,只比链表更复杂。但同样的原则适用。

答案 7 :(得分:0)

为什么不使用SortedSet<Node>构建包含所有Node实例的BST?比较器将基于Code - 容器必须具有范围,以使其在所有成员中都是唯一的。