C#中的树数据结构


248

我在C#中寻找树或图的数据结构,但我猜没有提供。使用C#2.0进行的数据结构的广泛检查解释了一些原因。是否有一个方便使用的库通常用于提供此功能?也许可以通过一种策略模式来解决本文中提出的问题。

我实现自己的树有点愚蠢,就像实现自己的ArrayList一样。

我只想要可以不平衡的通用树。想一想目录树。C5看起来很漂亮,但是它们的树结构似乎被实现为平衡的红黑树,比表示节点的层次结构更适合于搜索。



是否有某些原因不能在项目中包含TreeView并使用它?没有理由将其实际显示给用户。当然,当这不是一种选择时,有几种形式的项目。如果需要特殊的复杂性,总是可以创建从示例TreeNode继承的新类吗?
Simply G.

9
我认为为一个非常简单的树导入整个UI库是一个坏主意。
stimms 2012年

1
你能激发动力吗?它不再像实际的硬盘空间要求那样成为问题了吗?笨拙?如前所述,我可以理解这不是针对专用软件的解决方案,也不是没有现有用户界面的解决方案。我是一个懒惰的程序员,如果我可以免费获得一个结构的话,那么一切都很好。现有的图书馆确实有很多免费的,人们可以从使用过很多东西的人们那里找到很多代码。
Simply G.12年

我不是在争论,我只是想知道你的理由。
Simply G.12年

Answers:


155

我最好的建议是,没有标准的树数据结构,因为您可以通过多种方法来实现它,以至于不可能用一种解决方案覆盖所有基础。解决方案越具体,就越不可能适用于任何给定的问题。我什至对LinkedList感到恼火-如果我想要一个循环链表怎么办?

您需要实现的基本结构将是节点的集合,这是一些入门的选项。假设Node类是整个解决方案的基类。

如果只需要在树上导航,则Node类需要一个子级列表。

如果需要在树上导航,则Node类需要一个指向其父节点的链接。

构建一个AddChild方法,该方法处理这两点的所有细节以及必须实现的任何其他业务逻辑(子级限制,对子级进行排序等)


5
我个人不介意将某种自平衡的二叉树添加到库中,因为这不仅仅是使用邻接表,还需要额外的工作。
jk。

8
@jk我相信SortedDictionary和SortedSet建立在红色/黑色树顶上,因此使用它们应该可以工作。
jonp 2010年

看一看复合模式;-)正是您想要的
Nicolas Voron 2012年

119
delegate void TreeVisitor<T>(T nodeData);

class NTree<T>
{
    private T data;
    private LinkedList<NTree<T>> children;

    public NTree(T data)
    {
         this.data = data;
        children = new LinkedList<NTree<T>>();
    }

    public void AddChild(T data)
    {
        children.AddFirst(new NTree<T>(data));
    }

    public NTree<T> GetChild(int i)
    {
        foreach (NTree<T> n in children)
            if (--i == 0)
                return n;
        return null;
    }

    public void Traverse(NTree<T> node, TreeVisitor<T> visitor)
    {
        visitor(node.data);
        foreach (NTree<T> kid in node.children)
            Traverse(kid, visitor);
    }
}

简单的递归实现... <40行代码...您只需要在类外部保留对树根的引用,或将其包装在另一个类中,也许重命名为TreeNode?


22
在这种情况下,无论如何在C#中,您可以避免编写自己的委托,而使用预制的Action<T>委托:public void traverse(NTree<T> node, Action<T> visitor)。Action <>的签名是:void Action<T>( T obj )。还有从0到4个不同参数的版本。还有一个类似的函数委托,称为Func<>
Benny Jobigan'2

2
我怎么称呼这个代表?
异想天开

3
将遍历方法更改为静态,或者可能将其包装起来以隐藏递归性质是一个好主意,但是遍历很简单:创建一个带有委托签名的方法,即一个整数树:void my_visitor_impl(int datum) -根据需要将其设置为静态,实例化一个委托:TreeVisitor <int> my_visitor = my_visitor_impl; 然后,如果您将其设为静态,则在根节点或NTree类上调用:NTree <int> .traverse(my_tree,my_visitor)
Aaron Gage

10
使addChild()返回它添加的NTree将使其更好地向树中添加数据。(除非我错过了一个狡猾的方法来以此来构建树,而不依赖于新添加的child == getChild(1)的实现细节?)
Rory

1
我认为该语句--i == 0仅在单个情况下起作用?这是真的。这让我感到困惑
Waseem Ahmad Naeem

57

我认为这是我的,与Aaron Gage的非常相似。就我的目的而言,我还没有遇到任何性能问题List<T>。如果需要的话,切换到LinkedList很容易。


namespace Overby.Collections
{
    public class TreeNode<T>
    {
        private readonly T _value;
        private readonly List<TreeNode<T>> _children = new List<TreeNode<T>>();

        public TreeNode(T value)
        {
            _value = value;
        }

        public TreeNode<T> this[int i]
        {
            get { return _children[i]; }
        }

        public TreeNode<T> Parent { get; private set; }

        public T Value { get { return _value; } }

        public ReadOnlyCollection<TreeNode<T>> Children
        {
            get { return _children.AsReadOnly(); }
        }

        public TreeNode<T> AddChild(T value)
        {
            var node = new TreeNode<T>(value) {Parent = this};
            _children.Add(node);
            return node;
        }

        public TreeNode<T>[] AddChildren(params T[] values)
        {
            return values.Select(AddChild).ToArray();
        }

        public bool RemoveChild(TreeNode<T> node)
        {
            return _children.Remove(node);
        }

        public void Traverse(Action<T> action)
        {
            action(Value);
            foreach (var child in _children)
                child.Traverse(action);
        }

        public IEnumerable<T> Flatten()
        {
            return new[] {Value}.Concat(_children.SelectMany(x => x.Flatten()));
        }
    }
}

在构造函数中进行设置时,为什么会暴露Value属性?在您已经通过构造函数对其进行设置之后,就可以对其进行操作了吗?应该私人设置吗?
PositiveGuy

当然,为什么不使它不变呢?编辑。
罗尼·欧弗比

谢谢!我很喜欢不必自己写。(仍然无法相信这不是本机存在的东西。我一直认为.net或至少.net 4.0拥有一切。)
neminem

3
我喜欢这个解决方案。我还发现我需要插入,因此添加了以下方法。 public TreeNode<T> InsertChild(TreeNode<T> parent, T value) { var node = new TreeNode<T>(value) { Parent = parent }; parent._children.Add(node); return node; } var five = myTree.AddChild(5); myTree.InsertChild(five, 55);
JabberwockyDecompiler 2015年

48

另一个树结构:

public class TreeNode<T> : IEnumerable<TreeNode<T>>
{

    public T Data { get; set; }
    public TreeNode<T> Parent { get; set; }
    public ICollection<TreeNode<T>> Children { get; set; }

    public TreeNode(T data)
    {
        this.Data = data;
        this.Children = new LinkedList<TreeNode<T>>();
    }

    public TreeNode<T> AddChild(T child)
    {
        TreeNode<T> childNode = new TreeNode<T>(child) { Parent = this };
        this.Children.Add(childNode);
        return childNode;
    }

    ... // for iterator details see below link
}

用法示例:

TreeNode<string> root = new TreeNode<string>("root");
{
    TreeNode<string> node0 = root.AddChild("node0");
    TreeNode<string> node1 = root.AddChild("node1");
    TreeNode<string> node2 = root.AddChild("node2");
    {
        TreeNode<string> node20 = node2.AddChild(null);
        TreeNode<string> node21 = node2.AddChild("node21");
        {
            TreeNode<string> node210 = node21.AddChild("node210");
            TreeNode<string> node211 = node21.AddChild("node211");
        }
    }
    TreeNode<string> node3 = root.AddChild("node3");
    {
        TreeNode<string> node30 = node3.AddChild("node30");
    }
}

奖金
请参阅具有以下特征的成熟树:

  • 迭代器
  • 搜寻
  • Java / C#

https://github.com/gt4dev/yet-another-tree-structure


如何在您的代码示例中使用搜索?哪里node来的?这是否意味着我必须遍历树才能使用搜索代码?
BadmintonCat

@GrzegorzDev可能为-1,因为它未实现所有IEnumerable<>成员,因此无法编译。
Uwe Keim

1
@UweKeim好工作,下一次尝试将代码与实际用法结合使用。
szab.kel

我看到的唯一问题是,在实现IEnumerable <>时,它将无法使用基本的JsonConvert正确序列化
Rakiah

22

通常来说非常出色的C5通用收藏库具有几种不同的基于树的数据结构,包括集合,包装袋和字典。如果您想研究其实现细节,可以使用源代码。(尽管我没有专门使用任何树结构,但我在生产代码中使用了C5集合,效果很好。)


7
不知道情况是否已经改变,但现在该书可从C5网站免费下载为PDF。
奥斯卡(Oskar)

4
缺少文档不再是问题,因为有272页长的pdf补充了库...无法评论代码质量,但是从文档质量来看,我真的很想今晚深入研究!
Florian Doyon 2010年

2
据我了解,这个C5库根本没有树,只有一些树派生的数据结构。
2014年

10

http://quickgraph.codeplex.com/

QuickGraph为.Net 2.0及更高版本提供通用的有向/无向图数据结构和算法。QuickGraph带有诸如深度优先搜索,呼吸优先搜索,A *搜索,最短路径,k最短路径,最大流量,最小生成树,最不常见祖先等算法,... QuickGraph支持MSAGL,GLEE和Graphviz渲染图,序列化为GraphML等...


8

如果您想编写自己的文档,则可以从这份由六部分组成的文档开始,详细介绍C#2.0数据结构的有效用法以及如何分析C#中数据结构的实现。每篇文章都有示例和安装程序,并提供了示例。

Scott Mitchell的“使用C#2.0的数据结构的广泛检查”


7

我对解决方案有一点扩展。

使用递归泛型声明和派生子类,您可以更好地专注于实际目标。

注意,它与非通用实现不同,您无需在“ NodeWorker”中强制转换“ node”。

这是我的示例:

public class GenericTree<T> where T : GenericTree<T> // recursive constraint  
{
  // no specific data declaration  

  protected List<T> children;

  public GenericTree()
  {
    this.children = new List<T>();
  }

  public virtual void AddChild(T newChild)
  {
    this.children.Add(newChild);
  }

  public void Traverse(Action<int, T> visitor)
  {
    this.traverse(0, visitor);
  }

  protected virtual void traverse(int depth, Action<int, T> visitor)
  {
    visitor(depth, (T)this);
    foreach (T child in this.children)
      child.traverse(depth + 1, visitor);
  }
}

public class GenericTreeNext : GenericTree<GenericTreeNext> // concrete derivation
{
  public string Name {get; set;} // user-data example

  public GenericTreeNext(string name)
  {
    this.Name = name;
  }
}

static void Main(string[] args)  
{  
  GenericTreeNext tree = new GenericTreeNext("Main-Harry");  
  tree.AddChild(new GenericTreeNext("Main-Sub-Willy"));  
  GenericTreeNext inter = new GenericTreeNext("Main-Inter-Willy");  
  inter.AddChild(new GenericTreeNext("Inter-Sub-Tom"));  
  inter.AddChild(new GenericTreeNext("Inter-Sub-Magda"));  
  tree.AddChild(inter);  
  tree.AddChild(new GenericTreeNext("Main-Sub-Chantal"));  
  tree.Traverse(NodeWorker);  
}  

static void NodeWorker(int depth, GenericTreeNext node)  
{                                // a little one-line string-concatenation (n-times)
  Console.WriteLine("{0}{1}: {2}", String.Join("   ", new string[depth + 1]), depth, node.Name);  
}  

什么是深度,在哪里以及如何获得?
PositiveGuy

@ WeDoTDD.com看着他的类,您会看到Traverse将其声明为0以从根节点开始,然后使用traverse方法在每次迭代中添加到该int。
爱德华

您将如何在整个树中搜索特定节点?
mattpm

6

这是我自己的:

class Program
{
    static void Main(string[] args)
    {
        var tree = new Tree<string>()
            .Begin("Fastfood")
                .Begin("Pizza")
                    .Add("Margherita")
                    .Add("Marinara")
                .End()
                .Begin("Burger")
                    .Add("Cheese burger")
                    .Add("Chili burger")
                    .Add("Rice burger")
                .End()
            .End();

        tree.Nodes.ForEach(p => PrintNode(p, 0));
        Console.ReadKey();
    }

    static void PrintNode<T>(TreeNode<T> node, int level)
    {
        Console.WriteLine("{0}{1}", new string(' ', level * 3), node.Value);
        level++;
        node.Children.ForEach(p => PrintNode(p, level));
    }
}

public class Tree<T>
{
    private Stack<TreeNode<T>> m_Stack = new Stack<TreeNode<T>>();

    public List<TreeNode<T>> Nodes { get; } = new List<TreeNode<T>>();

    public Tree<T> Begin(T val)
    {
        if (m_Stack.Count == 0)
        {
            var node = new TreeNode<T>(val, null);
            Nodes.Add(node);
            m_Stack.Push(node);
        }
        else
        {
            var node = m_Stack.Peek().Add(val);
            m_Stack.Push(node);
        }

        return this;
    }

    public Tree<T> Add(T val)
    {
        m_Stack.Peek().Add(val);
        return this;
    }

    public Tree<T> End()
    {
        m_Stack.Pop();
        return this;
    }
}

public class TreeNode<T>
{
    public T Value { get; }
    public TreeNode<T> Parent { get; }
    public List<TreeNode<T>> Children { get; }

    public TreeNode(T val, TreeNode<T> parent)
    {
        Value = val;
        Parent = parent;
        Children = new List<TreeNode<T>>();
    }

    public TreeNode<T> Add(T val)
    {
        var node = new TreeNode<T>(val, this);
        Children.Add(node);
        return node;
    }
}

输出:

Fastfood
   Pizza
      Margherita
      Marinara
   Burger
      Cheese burger
      Chili burger
      Rice burger

4

试试这个简单的示例。

public class TreeNode<TValue>
{
    #region Properties
    public TValue Value { get; set; }
    public List<TreeNode<TValue>> Children { get; private set; }
    public bool HasChild { get { return Children.Any(); } }
    #endregion
    #region Constructor
    public TreeNode()
    {
        this.Children = new List<TreeNode<TValue>>();
    }
    public TreeNode(TValue value)
        : this()
    {
        this.Value = value;
    }
    #endregion
    #region Methods
    public void AddChild(TreeNode<TValue> treeNode)
    {
        Children.Add(treeNode);
    }
    public void AddChild(TValue value)
    {
        var treeNode = new TreeNode<TValue>(value);
        AddChild(treeNode);
    }
    #endregion
}

2

我创建了一个Node类,可能对其他人有帮助。该类具有以下属性:

  • 小孩儿
  • 祖先
  • 后代
  • 兄弟姐妹
  • 节点级别
  • 父母
  • 等等。

也可以将带有Id和ParentId的项目的平面列表转换为树。节点同时拥有对子节点和父节点的引用,因此使节点迭代非常快。



2

我已经完成@Berezh共享的代码。

  public class TreeNode<T> : IEnumerable<TreeNode<T>>
    {

        public T Data { get; set; }
        public TreeNode<T> Parent { get; set; }
        public ICollection<TreeNode<T>> Children { get; set; }

        public TreeNode(T data)
        {
            this.Data = data;
            this.Children = new LinkedList<TreeNode<T>>();
        }

        public TreeNode<T> AddChild(T child)
        {
            TreeNode<T> childNode = new TreeNode<T>(child) { Parent = this };
            this.Children.Add(childNode);
            return childNode;
        }

        public IEnumerator<TreeNode<T>> GetEnumerator()
        {
            throw new NotImplementedException();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return (IEnumerator)GetEnumerator();
        }
    }
    public class TreeNodeEnum<T> : IEnumerator<TreeNode<T>>
    {

        int position = -1;
        public List<TreeNode<T>> Nodes { get; set; }

        public TreeNode<T> Current
        {
            get
            {
                try
                {
                    return Nodes[position];
                }
                catch (IndexOutOfRangeException)
                {
                    throw new InvalidOperationException();
                }
            }
        }


        object IEnumerator.Current
        {
            get
            {
                return Current;
            }
        }


        public TreeNodeEnum(List<TreeNode<T>> nodes)
        {
            Nodes = nodes;
        }

        public void Dispose()
        {
        }

        public bool MoveNext()
        {
            position++;
            return (position < Nodes.Count);
        }

        public void Reset()
        {
            position = -1;
        }
    }

好的设计。但是我不确定节点是否是其子节点的序列。我会考虑以下内容:一个节点“具有”零个或多个子节点,因此一个节点不是从一系列子节点派生而来的,而是它的子节点的集合(组成?)
Harald Coppoolse

2

这是一棵树

public class Tree<T> : List<Tree<T>>
{
    public  T Data { get; private set; }

    public Tree(T data)
    {
        this.Data = data;
    }

    public Tree<T> Add(T data)
    {
        var node = new Tree<T>(data);
        this.Add(node);
        return node;
    }
}

您甚至可以使用初始化程序:

    var tree = new Tree<string>("root")
    {
        new Tree<string>("sample")
        {
            "console1"
        }
    };

1

大多数树由您正在处理的数据组成。

假设您有一个person包含某人的详细信息的类, parents是将树结构作为“域类”的一部分,还是使用一个单独的树类来包含个人对象的链接?考虑一个简单的操作,例如获取a的所有grandchildren内容person,此代码应该在person 类中,还是类的用户person必须知道一个单独的树类?

另一个例子是编译器中的解析树…

这两个示例都表明,树的概念是数据的一部分,使用单独的通用树至少会使创建的对象数量增加一倍,并使API难以再次编程。

我们想要的是一种重用标准树操作的方法,而不必为所有树重新实现它们,同时又不必使用标准树类。Boost试图解决C ++的这类问题,但我尚未看到适应.NET的任何效果。


@Puchacz,对不起,我用C ++的数据还剩15年,请看一下Boost和Templates,经过一些研究之后,您可能会理解它们。力量有很高的学习成本!!
伊恩·林罗斯

1

我已经使用上面的NTree类添加了完整的解决方案和示例,还添加了“ AddChild”方法...

    public class NTree<T>
    {
        public T data;
        public LinkedList<NTree<T>> children;

        public NTree(T data)
        {
            this.data = data;
            children = new LinkedList<NTree<T>>();
        }

        public void AddChild(T data)
        {
            var node = new NTree<T>(data) { Parent = this };
            children.AddFirst(node);
        }

        public NTree<T> Parent { get; private set; }

        public NTree<T> GetChild(int i)
        {
            foreach (NTree<T> n in children)
                if (--i == 0)
                    return n;
            return null;
        }

        public void Traverse(NTree<T> node, TreeVisitor<T> visitor, string t, ref NTree<T> r)
        {
            visitor(node.data, node, t, ref r);
            foreach (NTree<T> kid in node.children)
                Traverse(kid, visitor, t, ref r);
        }
    }
    public static void DelegateMethod(KeyValuePair<string, string> data, NTree<KeyValuePair<string, string>> node, string t, ref NTree<KeyValuePair<string, string>> r)
    {
        string a = string.Empty;
        if (node.data.Key == t)
        {
            r = node;
            return;
        }
    }

使用

 NTree<KeyValuePair<string, string>> ret = null;
 tree.Traverse(tree, DelegateMethod, node["categoryId"].InnerText, ref ret);

遍历应该是静态方法吗?作为实例方法传递给自己的方法,似乎很尴尬
Sinaesthetic,

0

这是我的BST实现

class BST
{
    public class Node
    {
        public Node Left { get; set; }
        public object Data { get; set; }
        public Node Right { get; set; }

        public Node()
        {
            Data = null;
        }

        public Node(int Data)
        {
            this.Data = (object)Data;
        }

        public void Insert(int Data)
        {
            if (this.Data == null)
            {
                this.Data = (object)Data;
                return;
            }
            if (Data > (int)this.Data)
            {
                if (this.Right == null)
                {
                    this.Right = new Node(Data);
                }
                else
                {
                    this.Right.Insert(Data);
                }
            }
            if (Data <= (int)this.Data)
            {
                if (this.Left == null)
                {
                    this.Left = new Node(Data);
                }
                else
                {
                    this.Left.Insert(Data);
                }
            }
        }

        public void TraverseInOrder()
        {
            if(this.Left != null)
                this.Left.TraverseInOrder();
            Console.Write("{0} ", this.Data);
            if (this.Right != null)
                this.Right.TraverseInOrder();
        }
    }

    public Node Root { get; set; }
    public BST()
    {
        Root = new Node();
    }
}

0

如果要在GUI上显示此树,则可以使用TreeViewTreeNode。(我想从技术上讲,您可以创建TreeNode而不将其放置在GUI上,但是与简单的本地TreeNode实现相比,它确实有更多开销。)


-4

如果需要使用较少内存的根目录树数据结构实现,则可以按以下方式编写Node类(C ++实现):

class Node {
       Node* parent;
       int item; // depending on your needs

       Node* firstChild; //pointer to left most child of node
       Node* nextSibling; //pointer to the sibling to the right
}

12
在专门针对C#的问题上发布C ++代码并不是最好的主意,杰克。特别是包含指针的指针。您知道在C#中无情地追捕指针吧?:p
ThunderGr

2
@ThunderGr不公平。用C#回答会更好,但是那些C ++指针可以被C#讲者理解为引用(它们不太安全,可以)。在David Boike,Aaron Gage,Ronnie Overby,Grzegorz Dev,Berezh和Erik Nagel都接受了基本相同的数据结构,只是在表达上存在细微差别之后,Jake提出对链表进行分解,从而产生了一种仅具有一种类型的节点的简单结构,并且同级的可导航性。不要通过否决建设性的答案来表达对C ++的厌恶。
migle

3
@migle我没有对答案进行投票(也没有投票)。而且我不喜欢C ++。我看到答案被否决了,没有人向杰克提出任何关于他为什么以及如何改善答案的建议。这与“变得更好”无关。该问题仅被标记为C#。不建议使用除标签之外的其他语言发布答案,有些人会投票反对。
ThunderGr 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.