C#中的树数据结构
Posted
技术标签:
【中文标题】C#中的树数据结构【英文标题】:Tree data structure in C# 【发布时间】:2010-09-09 04:12:42 【问题描述】:我一直在寻找 C# 中的树或图形数据结构,但我想没有提供。 An Extensive Examination of Data Structures Using C# 2.0 有点关于为什么。是否有一个方便的库通常用于提供此功能?或许可以通过strategy pattern来解决文章中提出的问题。
我觉得实现自己的树有点傻,就像我实现自己的 ArrayList 一样。
我只想要一个可以不平衡的通用树。想想目录树。 C5 看起来很漂亮,但它们的树结构似乎被实现为平衡的红黑树,比表示节点层次结构更适合搜索。
【问题讨论】:
有点极端的树:***.com/questions/196294/… ;-) 我认为为一个非常简单的树导入整个 UI 库是个坏主意。 你能激励一下吗?它不再像实际的硬盘空间需求是一个问题了吗?笨拙?正如我之前提到的,我可以理解这不是针对专业软件或没有现有用户界面的东西的解决方案。我是一个懒惰的程序员,如果我能免费获得一个结构,那就太好了。现有的库确实有很多免费的,可以从使用它的人那里找到很多代码。 这是一个简单的树类型:public class Tree<T> : List<Tree<T>> public T Value;
.
另外,它可能会产生很多兼容性和维护问题。您的程序仅适用于 Windows...仅仅因为您使用了一些 UI 树用于 winforms 或 WPF?如果你想更新你的软件,但你还依赖于 UI 机制的(可能很多)依赖项兼容性,会发生什么?
【参考方案1】:
我最好的建议是没有标准的树数据结构,因为有很多方法可以实现它,不可能用一种解决方案覆盖所有基础。解决方案越具体,它就越不可能适用于任何给定的问题。我什至对 LinkedList 感到恼火——如果我想要一个循环链表怎么办?
您需要实现的基本结构是节点集合,这里有一些选项可以帮助您入门。假设 Node 类是整个解决方案的基类。
如果您只需要在树下导航,那么 Node 类需要一个子列表。
如果您需要向上导航树,那么 Node 类需要一个指向其父节点的链接。
构建一个 AddChild 方法来处理这两点的所有细节以及任何其他必须实现的业务逻辑(子限制、对子排序等)
【讨论】:
就我个人而言,我不介意将某种自平衡二叉树添加到库中,因为这比仅使用邻接列表需要额外的工作。 @jk 我相信 SortedDictionary 和 SortedSet 是建立在红/黑树之上的,所以使用它们应该可以工作。 看看复合模式 ;-) 正是你要找的【参考方案2】: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);
简单的递归实现...
【讨论】:
在这种情况下,无论如何,在 C# 中,您可以避免编写自己的委托并使用预制的Action<T>
委托:public void traverse(NTree<T> node, Action<T> visitor)
。 Action 的签名是:void Action<T>( T obj )
。还有从 0 到 4 个不同参数的版本。还有一个类似的函数委托,称为Func<>
。
LinkedList 的优势在于,它对于我们将其置于此处的目的更有效,并且它只消耗所需的内存,无论存储多少子节点。唯一对基于数组的 List 实现更有效的操作是 getChild(int),但我希望谨慎调用它,通常会使用 add 和 traverse,LinkedList 非常适合。完成实现并添加 Remove 可能会使事情复杂化。如果 C#s 泛型允许用户指定 List 实现以最适合使用,那就太好了,但它没有。
我该如何称呼这个代表?
将遍历方法更改为静态或可能将其包装以隐藏递归性质将是一个好主意,但遍历很简单:创建一个带有委托签名的方法,即为一棵树ints: void my_visitor_impl(int datum) - 如果需要,将其设为静态,实例化一个 delgate: TreeVisitor在我看来,这是我的,与Aaron Gage's 非常相似,只是更传统一点。就我而言,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 属性时会暴露它?在您已经通过构造函数设置它之后,它会打开以进行操作吗?应该是私人设置吗? 当然,为什么不让它不可变呢?已编辑。 谢谢!我很喜欢不用自己写。 (仍然无法相信它不是原生存在的东西。我一直认为 .net,或者至少 .net 4.0,拥有一切。) 我喜欢这个解决方案。我还发现我需要插入,我添加了以下方法来做到这一点。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);
这是一段特殊的代码,在我看来是最好的答案。通读它本身就是一场讲座。【参考方案4】:
又一个树形结构:
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
来自哪里?这是否意味着我必须遍历树才能使用搜索代码?
@GrzegorzDev 可能是-1,因为它没有实现所有IEnumerable<>
成员,所以它不会编译。
@UweKeim 干得好,下次尝试使用实际使用的代码。
我看到的唯一问题是它不会被基本的 JsonConvert 正确序列化,因为它实现了 IEnumerable
@Grzegorz Dev - 嗨,有没有办法将所有二级节点作为字符串列表?【参考方案5】:
普遍优秀的C5 Generic Collection Library 有几种不同的基于树的数据结构,包括集合、包和字典。如果您想研究它们的实现细节,可以使用源代码。 (我在生产代码中使用了 C5 集合,效果很好,虽然我没有专门使用任何树结构。)
【讨论】:
不知道事情是否发生了变化,但现在这本书可以从 C5 网站以 PDF 格式免费下载。 缺少文档不再是一个问题,因为有一个 272 页长的 pdf 补充了库......无法评论代码质量,但从文档质量来看,我真的很期待今晚深入研究! 据我了解,这个C5库根本没有树,只有一些树派生的数据结构。【参考方案6】:见https://github.com/YaccConstructor/QuickGraph(以前的http://quickgraph.codeplex.com/)
QuickGraph 为 .NET 2.0 及更高版本提供通用的有向/无向图数据结构和算法。 QuickGraph自带depth-first search、breadth-first search、A*搜索、最短路径、k-最短路径、最大流量、最小生成树、最小共同祖先等算法... QuickGraph支持MSAGL、GLEE , 和Graphviz 渲染图形,序列化为GraphML 等。
【讨论】:
QuickGraph 链接已损坏:“嗯。我们无法找到该站点。我们无法连接到 quickgraph.codeplex.com 上的服务器。” 【参考方案7】:这是我自己的:
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("01", 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
【讨论】:
【参考方案8】:我对解决方案有一点扩展。
使用递归泛型声明和派生子类,您可以更好地专注于实际目标。
注意,它与非泛型实现不同,您不需要将 'node' 强制转换为 'NodeWorker'。
这是我的例子:
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("01: 2", String.Join(" ", new string[depth + 1]), depth, node.Name);
【讨论】:
什么是深度,你从哪里以及如何得到它? @WeDoTDD.com 查看他的类,您会看到 Traverse 将其声明为 0 以从根节点开始,然后使用 traverse 方法在每次迭代中添加到该 int。 如何在整个树中搜索特定节点?【参考方案9】:试试这个简单的示例。
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
【讨论】:
【参考方案10】:我创建了一个可能对其他人有帮助的Node<T> class。该类具有以下属性:
儿童 祖先 后裔 兄弟姐妹 节点级别 父母 根 等还可以将具有 Id 和 ParentId 的项目的平面列表转换为树。节点包含对子节点和父节点的引用,因此迭代节点非常快。
【讨论】:
工作链接:siepman.nl/blog/a-generic-tree-of-nodes-the-easy-way【参考方案11】:现在发布了 .NET 代码库:特别是实现 red-black tree 的 SortedSet
的代码:sortedset.cs
然而,这是一个平衡的树形结构。所以我的回答更多的是参考我认为是 .NET 核心库中唯一的原生树结构。
【讨论】:
【参考方案12】:我已经完成了Berezh has shared的代码。
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;
【讨论】:
好设计。但是我不确定节点是否“是”其子节点的序列。我会考虑以下几点:一个节点“有”零个或多个子节点,因此节点不是从子节点序列派生的,而是其子节点的聚合(组合?)【参考方案13】:大多数树是由您正在处理的数据形成的。
假设您有一个
person
类,其中包含某人的详细信息parents
,您愿意将树结构作为您的一部分吗? “域类”,或使用包含指向的链接的单独树类 你的人反对?想想一个简单的操作,比如get allperson
的grandchildren
,如果此代码在person
中 类,或者person
类的用户必须知道 单独的树类?
另一个例子是编译器中的parse tree...
这两个示例都表明,树的概念是数据域的一部分,使用单独的通用树至少可以使创建的对象数量增加一倍,并且使 API 更难再次编程。
我们想要一种方法来重用标准树操作,而不必为所有树重新实现它们,同时不必使用标准树类。 Boost 已尝试为 C++ 解决此类问题,但我尚未看到 .NET 对其进行调整的任何效果。
【讨论】:
@Puchacz,对不起,我已经 15 年没有 C++ 的数据了,看看 Boost 和 Templates,经过一些薄弱的研究,你可能会理解它们。电源学习成本高!!【参考方案14】:我使用上面的 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);
【讨论】:
应该遍历可能是静态方法吗?作为一个将自己传递给自身的实例方法,这似乎很尴尬【参考方案15】:还可以将 XML 与 LINQ 一起使用:
Create XML tree in C# (LINQ to XML)
在使用树时,XML 是最成熟、最灵活的解决方案,而 LINQ 为您提供了所需的所有工具。 树的配置也变得更加简洁和用户友好,因为您可以简单地使用 XML 文件进行初始化。
如果需要处理对象,可以使用 XML 序列化:
XML serialization
【讨论】:
这是一个练习法语的好机会,但也许也提供相应的英语?【参考方案16】:如果要在 GUI 上显示此树,可以使用 TreeView 和 TreeNode。 (我想从技术上讲,您可以创建一个 TreeNode 而无需将它放在 GUI 上,但它确实比简单的本土 TreeNode 实现有更多的开销。)
【讨论】:
【参考方案17】:这是我对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();
【讨论】:
【参考方案18】:具有通用数据的树
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
public class Tree<T>
public T Data get; set;
public LinkedList<Tree<T>> Children get; set; = new LinkedList<Tree<T>>();
public Task Traverse(Func<T, Task> actionOnNode, int maxDegreeOfParallelism = 1) => Traverse(actionOnNode, new SemaphoreSlim(maxDegreeOfParallelism, maxDegreeOfParallelism));
private async Task Traverse(Func<T, Task> actionOnNode, SemaphoreSlim semaphore)
await actionOnNode(Data);
SafeRelease(semaphore);
IEnumerable<Task> tasks = Children.Select(async input =>
await semaphore.WaitAsync().ConfigureAwait(false);
try
await input.Traverse(actionOnNode, semaphore).ConfigureAwait(false);
finally
SafeRelease(semaphore);
);
await Task.WhenAll(tasks);
private void SafeRelease(SemaphoreSlim semaphore)
try
semaphore.Release();
catch (Exception ex)
if (ex.Message.ToLower() != "Adding the specified count to the semaphore would cause it to exceed its maximum count.".ToLower())
throw;
public async Task<IEnumerable<T>> ToList()
ConcurrentBag<T> lst = new ConcurrentBag<T>();
await Traverse(async (data) => lst.Add(data));
return lst;
public async Task<int> Count() => (await ToList()).Count();
单元测试
using System.Threading.Tasks;
using Xunit;
public class Tree_Tests
[Fact]
public async Task Tree_ToList_Count()
Tree<int> head = new Tree<int>();
Assert.NotEmpty(await head.ToList());
Assert.True(await head.Count() == 1);
// child
var child = new Tree<int>();
head.Children.AddFirst(child);
Assert.True(await head.Count() == 2);
Assert.NotEmpty(await head.ToList());
// grandson
child.Children.AddFirst(new Tree<int>());
child.Children.AddFirst(new Tree<int>());
Assert.True(await head.Count() == 4);
Assert.NotEmpty(await head.ToList());
[Fact]
public async Task Tree_Traverse()
Tree<int> head = new Tree<int>() Data = 1 ;
// child
var child = new Tree<int>() Data = 2 ;
head.Children.AddFirst(child);
// grandson
child.Children.AddFirst(new Tree<int>() Data = 3 );
child.Children.AddLast(new Tree<int>() Data = 4 );
int counter = 0;
await head.Traverse(async (data) => counter += data);
Assert.True(counter == 10);
counter = 0;
await child.Traverse(async (data) => counter += data);
Assert.True(counter == 9);
counter = 0;
await child.Children.First!.Value.Traverse(async (data) => counter += data);
Assert.True(counter == 3);
counter = 0;
await child.Children.Last!.Value.Traverse(async (data) => counter += data);
Assert.True(counter == 4);
【讨论】:
什么单元测试框架? NUnit? 解释一下。例如,想法/要点是什么? SafeRelease() 的目的是什么?例如,为什么需要 SafeRelease()?线程安全?决定使用async
和await
背后的想法是什么?需要的最低 C# 版本是多少?请通过editing (changing) your answer 回复,而不是在 cmets 中(without "Edit:"、"Update:" 或类似的 - 答案应该看起来像是今天写的)。【参考方案19】:
如果您需要使用较少内存的有根树数据结构实现,您可以按如下方式编写 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
【讨论】:
在专门针对 C# 的问题上发布 C++ 代码并不是最好的主意,Jake。尤其是包含指针的。你知道指针在 C# 中被无情地追捕,对吧? :p @ThunderGr 这不公平。用 C# 回答会更好,但是那些 C++ 指针可以被 C# 使用者理解为引用(它们不太安全,好吧)。在 David Boike、Aaron Gage、Ronnie Overby、Grzegorz Dev、Berezh 和 Erik Nagel 都提出了基本相同的数据结构,只有表达方式上的细微差别之后,Jake 提出了分解链表以产生一个只有一种节点类型的更简单的结构和同级导航。不要通过否决建设性的答案来表达您对 C++ 的厌恶。 @migle 我没有对答案投反对票(也没有投反对票)。而且我并不讨厌 C++。我看到答案被否决了,没有人向杰克提出任何关于他为什么以及如何改进他的答案的建议。这不是关于“变得更好”。该问题仅针对 C# 标记。不建议使用标签以外的其他语言发布答案,有些人会投反对票。以上是关于C#中的树数据结构的主要内容,如果未能解决你的问题,请参考以下文章