Tìm kiếm cây bằng LINQ


87

Tôi có một cái cây được tạo ra từ lớp này.

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

Tôi muốn tìm kiếm trong tất cả trẻ em và tất cả con cái của chúng để tìm những đứa trẻ phù hợp với điều kiện:

node.Key == SomeSpecialKey

Làm thế nào tôi có thể thực hiện nó?


Thật thú vị, tôi nghĩ bạn có thể thực hiện điều này bằng cách sử dụng chức năng SelectMany, Hãy nhớ rằng bạn phải làm điều gì đó tương tự cách đây một thời gian.
Jethro

Câu trả lời:


175

Đó là một quan niệm sai lầm rằng điều này yêu cầu đệ quy. Nó sẽ yêu cầu một ngăn xếp hoặc một hàng đợi và cách dễ nhất là thực hiện nó bằng cách sử dụng đệ quy. Để hoàn thiện, tôi sẽ cung cấp một câu trả lời không đệ quy.

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

Sử dụng biểu thức này chẳng hạn để sử dụng nó:

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

31
+1. Và phương thức này sẽ tiếp tục hoạt động khi cây sâu đến mức một đường truyền đệ quy sẽ thổi bay ngăn xếp cuộc gọi và gây ra a StackOverflowException.
LukeH

3
@LukeH Mặc dù rất hữu ích khi có những lựa chọn thay thế như thế này cho những trường hợp đó, nhưng điều đó có nghĩa là một cái cây rất lớn. Trừ khi bạn là cây rất sâu, các phương thức đệ quy thường đơn giản hơn / dễ đọc hơn.
ForbesLindesay

3
@Tuskan: Sử dụng trình lặp đệ quy cũng có ý nghĩa về hiệu suất, hãy xem phần "Chi phí của trình lặp" của blog.msdn.com/b/wesdyer/archive/2007/03/23/… (phải thừa nhận rằng các cây vẫn cần khá sâu để điều này đáng chú ý). Và, fwiw, tôi thấy câu trả lời của vidstige cũng dễ đọc như câu trả lời đệ quy ở đây.
LukeH

3
Vâng, không chọn giải pháp của tôi vì hiệu suất. Khả năng đọc luôn được ưu tiên hàng đầu, trừ khi được chứng minh là một nút thắt cổ chai. Mặc dù giải pháp của tôi khá dễ hiểu, vì vậy tôi đoán đó là vấn đề sở thích ... Tôi thực sự đã đăng câu trả lời của mình chỉ đơn thuần là bổ sung cho câu trả lời đệ quy, nhưng tôi rất vui vì mọi người thích nó.
vidstige

11
Tôi nghĩ rằng điều đáng nói là giải pháp được trình bày ở trên thực hiện tìm kiếm theo chiều sâu (cuối cùng là con đầu tiên). Nếu bạn muốn tìm kiếm theo chiều rộng (đầu tiên-con-đầu), bạn có thể thay đổi loại tập hợp các nút thành Queue<Node>(với các thay đổi tương ứng thành Enqueue/ Dequeuefrom Push/ Pop).
Andrew Coonce

16

Tìm kiếm cây đối tượng với 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 Giải quyết vấn đề nói chung. Bài báo được liên kết cung cấp một lời giải thích tuyệt vời.
John Jesus

Để hoàn tất, bạn cần kiểm tra rỗng các tham số headchildrenFuncchia các phương thức thành hai phần để việc kiểm tra tham số không bị trì hoãn theo thời gian truyền.
ErikE

15

Nếu bạn muốn duy trì cú pháp Linq like, bạn có thể sử dụng một phương thức để lấy tất cả các con (con + con của con, v.v.)

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

Sau đó, liệt kê này có thể được truy vấn giống như bất kỳ cách nào khác bằng cách sử dụng where hoặc first hoặc bất cứ điều gì.


Tôi thích điều này, sạch sẽ! :)
vidstige

3

Bạn có thể thử phương pháp mở rộng này để liệt kê các nút cây:

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

Sau đó, sử dụng điều đó với một Where()mệnh đề:

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

2
Lưu ý rằng kỹ thuật này không hiệu quả nếu cây bị sâu và có thể ném một ngoại lệ nếu cây rất sâu.
Eric Lippert

1
@Eric Điểm tốt. Và chào mừng trở lại sau kỳ nghỉ? (Thật khó để nói điều gì với thứ internet trải dài khắp thế giới này.)
dlev

2

Có lẽ bạn chỉ cần

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

Hoặc, nếu bạn cần tìm kiếm sâu hơn một cấp,

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

Nếu bạn cần tìm kiếm ở tất cả các cấp, hãy làm theo các bước sau:

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

Điều đó sẽ tìm kiếm những đứa trẻ trẻ em?
Jethro

Tôi nghĩ rằng công việc này wont, như tìm kiếm này chỉ ở một mức độ trong cây và không làm một duyệt cây hoàn chỉnh
lunactic

@Ufuk: tuyến 1 chỉ hoạt động sâu 1 cấp, tuyến 2 chỉ sâu 2 cấp. Nếu bạn cần tìm kiếm trên tất cả các cấp, bạn cần một hàm đệ quy.
Vlad

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

Và sau đó bạn có thể tìm kiếm như:

    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");

Vì đầu vào của Find là Func <Node, bool> myFunc nên bạn có thể sử dụng phương pháp này để lọc theo bất kỳ thuộc tính nào khác mà bạn có thể xác định trong Node. Ví dụ trong Node có một tài sản Tên và bạn muốn tìm một Node theo Tên, bạn chỉ có thể vượt qua trong p => p.Name == "Một cái gì đó"
Varun Chatterji

2

Tại sao không sử dụng một IEnumerable<T>phương pháp mở rộng

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

sau đó chỉ cần làm điều này

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

0

Một thời gian trước, tôi đã viết một bài viết về codeproject mô tả cách sử dụng Linq để truy vấn các cấu trúc dạng cây:

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

Điều này cung cấp một API kiểu linq-to-XML, nơi bạn có thể tìm kiếm con cháu, con cái, tổ tiên, v.v.

Có thể là quá mức cần thiết cho vấn đề hiện tại của bạn, nhưng có thể được người khác quan tâm.


0

Bạn có thể sử dụng phương thức mở rộng này để truy vấn cây.

    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

Tôi có một phương pháp mở rộng chung có thể làm phẳng bất kỳ IEnumerable<T>và từ bộ sưu tập được làm phẳng đó, bạn có thể có được nút bạn muốn.

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

Sử dụng như thế này:

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

0

Tôi sử dụng các triển khai sau để liệt kê các mục Cây

    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 trong việc triển khai ở trên sử dụng hàng đợi chuỗi nút thay vì hàng đợi nút. Đây không phải là cách thuật toán BFS cổ điển.


0

Và chỉ để cho vui (gần một thập kỷ sau) một câu trả lời cũng sử dụng Generics nhưng với vòng lặp Stack và While, dựa trên câu trả lời được chấp nhận bởi @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);
        }
    }
}

Đưa ra một bộ sưu tập người ta có thể sử dụng như thế này

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

hoặc với một đối tượng gốc

        var myNode = root.Descendants(x => x.Children).Where(x => x.Key == SomeKey);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.