使用LINQ搜索树


87

我有一个从此类创建的树。

class Node
{
    public string Key { get; }
    public List<Node> Children { get; }
}

我想搜索所有孩子及其所有孩子,以找到符合条件的孩子:

node.Key == SomeSpecialKey

我该如何实施?


有趣的是,我认为您可以使用SelectMany函数完成此操作,请记住,之前必须做类似的事情。
杰斯罗(Jethro)

Answers:


175

这是一个误解,认为这需要递归。这需要一个堆栈或队列和最简单的方法是使用递归来实现它。为了完整起见,我将提供一个非递归答案。

static IEnumerable<Node> Descendants(this Node root)
{
    var nodes = new Stack<Node>(new[] {root});
    while (nodes.Any())
    {
        Node node = nodes.Pop();
        yield return node;
        foreach (var n in node.Children) nodes.Push(n);
    }
}

例如,使用以下表达式来使用它:

root.Descendants().Where(node => node.Key == SomeSpecialKey)

31
+1。当树太深时,此方法将继续起作用,以至于递归遍历将破坏调用堆栈并导致StackOverflowException
路加福音

3
@LukeH尽管在这种情况下使用这样的替代方法很有用,但这将意味着一棵非常大的树。除非您的树很深,否则递归方法通常更简单/更具可读性。
ForbesLindesay

3
@Tuskan:使用递归迭代器也有性能影响,请参阅blogs.msdn.com/b/wesdyer/archive/2007/03/23/…的“迭代器的成本”部分(诚​​然,对于这一点很明显)。而且,首先,我发现vidstige的答案与此处的递归答案一样可读。
2011年

3
是的,不要因为性能而选择我的解决方案。除非存在瓶颈,否则可读性始终是第一位的。尽管我的解决方案非常简单,所以我想这只是一个问题...实际上我发布的答案只是对递归答案的补充,但我很高兴人们喜欢它。
vidstige,2011年

11
我认为值得一提的是,上述解决方案执行了(最后一个孩子优先)深度优先搜索。如果要进行(第一个孩子优先)广度优先的搜索,可以将节点集合的类型更改为Queue<Node>(对Enqueue/进行相应的更改,DequeuePush/进行Pop)。
Andrew Coonce

16

用Linq搜索对象树

public static class TreeToEnumerableEx
{
    public static IEnumerable<T> AsDepthFirstEnumerable<T>(this T head, Func<T, IEnumerable<T>> childrenFunc)
    {
        yield return head;

        foreach (var node in childrenFunc(head))
        {
            foreach (var child in AsDepthFirstEnumerable(node, childrenFunc))
            {
                yield return child;
            }
        }

    }

    public static IEnumerable<T> AsBreadthFirstEnumerable<T>(this T head, Func<T, IEnumerable<T>> childrenFunc)
    {
        yield return head;

        var last = head;
        foreach (var node in AsBreadthFirstEnumerable(head, childrenFunc))
        {
            foreach (var child in childrenFunc(node))
            {
                yield return child;
                last = child;
            }
            if (last.Equals(node)) yield break;
        }

    }
}

1
+1一般解决问题。链接的文章提供了很好的解释。
约翰·耶稣

为了完整起见,您需要对参数进行空值检查,head并将childrenFunc方法分为两部分,以使参数检查不会延迟遍历时间。
ErikE 2015年

15

如果您想要维护类似于Linq的语法,则可以使用一种方法来获取所有后代(子代+孩子的子代等)。

static class NodeExtensions
{
    public static IEnumerable<Node> Descendants(this Node node)
    {
        return node.Children.Concat(node.Children.SelectMany(n => n.Descendants()));
    }
}

然后,可以像其他任何查询一样使用where或first或诸如此类查询此可枚举的对象。


我喜欢这个,干净!:)
vidstige

3

您可以尝试使用这种扩展方法来枚举树节点:

static IEnumerable<Node> GetTreeNodes(this Node rootNode)
{
    yield return rootNode;
    foreach (var childNode in rootNode.Children)
    {
        foreach (var child in childNode.GetTreeNodes())
            yield return child;
    }
}

然后将其与Where()子句一起使用:

var matchingNodes = rootNode.GetTreeNodes().Where(x => x.Key == SomeSpecialKey);

2
请注意,如果树很深,则此技术效率不高;如果树很深,则可能引发异常。
埃里克·利珀特

1
@Eric好点。放假回来吗?(很难说出这种遍及全球的互联网事物是什么。)
dlev

2

也许你只需要

node.Children.Where(child => child.Key == SomeSpecialKey)

或者,如果您需要更深一层的搜索,

node.Children.SelectMany(
        child => child.Children.Where(child => child.Key == SomeSpecialKey))

如果需要在所有级别上搜索,请执行以下操作:

IEnumerable<Node> FlattenAndFilter(Node source)
{
    List<Node> l = new List();
    if (source.Key == SomeSpecialKey)
        l.Add(source);
    return
        l.Concat(source.Children.SelectMany(child => FlattenAndFilter(child)));
}

那会搜寻孩子们的孩子吗?
杰斯罗(Jethro)

我认为这将行不通,因为它仅在树中的一个级别上搜索,并且无法进行完整的树遍历
lunactic 2011年

@Ufuk:第一行仅深1层,第二行仅深2层。如果需要在所有级别上进行搜索,则需要递归函数。
弗拉德

2
public class Node
    {
        string key;
        List<Node> children;

        public Node(string key)
        {
            this.key = key;
            children = new List<Node>();
        }

        public string Key { get { return key; } }
        public List<Node> Children { get { return children; } }

        public Node Find(Func<Node, bool> myFunc)
        {
            foreach (Node node in Children)
            {
                if (myFunc(node))
                {
                    return node;
                }
                else 
                {
                    Node test = node.Find(myFunc);
                    if (test != null)
                        return test;
                }
            }

            return null;
        }
    }

然后您可以像搜索:

    Node root = new Node("root");
    Node child1 = new Node("child1");
    Node child2 = new Node("child2");
    Node child3 = new Node("child3");
    Node child4 = new Node("child4");
    Node child5 = new Node("child5");
    Node child6 = new Node("child6");
    root.Children.Add(child1);
    root.Children.Add(child2);
    child1.Children.Add(child3);
    child2.Children.Add(child4);
    child4.Children.Add(child5);
    child5.Children.Add(child6);

    Node test = root.Find(p => p.Key == "child6");

因为Find的输入是Func <Node,bool> myFunc,所以您可以使用此方法来过滤您可能在Node中定义的任何其他属性。例如,在Node中具有Name属性,而您想按名称查找Node,则只需传递p => p.Name ==“ Something”
Varun Chatterji

2

为什么不使用IEnumerable<T>扩展方法

public static IEnumerable<TResult> SelectHierarchy<TResult>(this IEnumerable<TResult> source, Func<TResult, IEnumerable<TResult>> collectionSelector, Func<TResult, bool> predicate)
{
    if (source == null)
    {
        yield break;
    }
    foreach (var item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
        var childResults = SelectHierarchy(collectionSelector(item), collectionSelector, predicate);
        foreach (var childItem in childResults)
        {
            yield return childItem;
        }
    }
}

然后就这样做

var result = nodes.Children.SelectHierarchy(n => n.Children, n => n.Key.IndexOf(searchString) != -1);

0

前一段时间,我写了一篇代码项目文章,描述了如何使用Linq查询树状结构:

http://www.codeproject.com/KB/linq/LinqToTree.aspx

这提供了linq-to-XML样式的API,您可以在其中搜索后代,子代,祖先等。

对于您当前的问题,可能会大刀阔斧,但其他人可能会感兴趣。


0

您可以使用此扩展方法来查询树。

    public static IEnumerable<Node> InTree(this Node treeNode)
    {
        yield return treeNode;

        foreach (var childNode in treeNode.Children)
            foreach (var flattendChild in InTree(childNode))
                yield return flattendChild;
    }

0

我有一个通用的扩展方法,该方法可以展平任何对象,IEnumerable<T>并且可以从该展平的集合中获取所需的节点。

public static IEnumerable<T> FlattenHierarchy<T>(this T node, Func<T, IEnumerable<T>> getChildEnumerator)
{
    yield return node;
    if (getChildEnumerator(node) != null)
    {
        foreach (var child in getChildEnumerator(node))
        {
            foreach (var childOrDescendant in child.FlattenHierarchy(getChildEnumerator))
            {
                yield return childOrDescendant;
            }
        }
    }
}

像这样使用:

var q = from node in myTree.FlattenHierarchy(x => x.Children)
        where node.Key == "MyKey"
        select node;
var theNode = q.SingleOrDefault();

0

我使用以下实现枚举Tree项目

    public static IEnumerable<Node> DepthFirstUnfold(this Node root) =>
        ObjectAsEnumerable(root).Concat(root.Children.SelectMany(DepthFirstUnfold));

    public static IEnumerable<Node> BreadthFirstUnfold(this Node root) {
        var queue = new Queue<IEnumerable<Node>>();
        queue.Enqueue(ObjectAsEnumerable(root));

        while (queue.Count != 0)
            foreach (var node in queue.Dequeue()) {
                yield return node;
                queue.Enqueue(node.Children);
            }
    }

    private static IEnumerable<T> ObjectAsEnumerable<T>(T obj) {
        yield return obj;
    }

上面实现中的BreadthFirstUnfold使用节点序列队列而不是节点队列。这不是经典的BFS算法。


0

只是为了好玩(大约十年后),一个答案也使用了泛型,但是带有一个堆栈和While循环,基于@vidstige接受的答案。

public static class TypeExtentions
{

    public static IEnumerable<T> Descendants<T>(this T root, Func<T, IEnumerable<T>> selector)
    {
        var nodes = new Stack<T>(new[] { root });
        while (nodes.Any())
        {
            T node = nodes.Pop();
            yield return node;
            foreach (var n in selector(node)) nodes.Push(n);
        }
    }

    public static IEnumerable<T> Descendants<T>(this IEnumerable<T> encounter, Func<T, IEnumerable<T>> selector)
    {
        var nodes = new Stack<T>(encounter);
        while (nodes.Any())
        {
            T node = nodes.Pop();
            yield return node;
            if (selector(node) != null)
                foreach (var n in selector(node))
                    nodes.Push(n);
        }
    }
}

给定一个集合,就可以这样使用

        var myNode = ListNodes.Descendants(x => x.Children).Where(x => x.Key == SomeKey);

或带有根对象

        var myNode = root.Descendants(x => x.Children).Where(x => x.Key == SomeKey);
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.