ค้นหาต้นไม้โดยใช้ LINQ


87

ฉันมีต้นไม้ที่สร้างขึ้นจากคลาสนี้

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

ฉันต้องการค้นหาเด็กทุกคนและลูก ๆ ทุกคนเพื่อให้ได้คนที่ตรงกับเงื่อนไข:

node.Key == SomeSpecialKey

ฉันจะใช้งานได้อย่างไร?


น่าสนใจฉันคิดว่าคุณสามารถทำได้โดยใช้ฟังก์ชัน SelectMany จำไว้ว่าต้องทำอะไรคล้าย ๆ กันเมื่อไม่นานมานี้
Jethro

คำตอบ:


176

เป็นความเข้าใจผิดที่ต้องมีการเรียกซ้ำ มันจะต้องมีสแต็คหรือคิวและวิธีที่ง่ายที่สุดคือการดำเนินการโดยใช้การเรียกซ้ำ เพื่อความสมบูรณ์ฉันจะให้คำตอบแบบไม่เรียกซ้ำ

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. และวิธีนี้จะยังคงทำงานต่อไปเมื่อต้นไม้อยู่ลึกมากจนการเคลื่อนที่แบบวนซ้ำจะระเบิด call stack และทำให้เกิด a StackOverflowException.
LukeH

3
@LukeH แม้ว่าการมีทางเลือกเช่นนี้จะมีประโยชน์สำหรับสถานการณ์เหล่านั้น แต่นั่นก็หมายถึงต้นไม้ที่มีขนาดใหญ่มาก หากต้นไม้ของคุณไม่ลึกมากวิธีการวนซ้ำมักจะง่ายกว่า / อ่านง่ายกว่า
ForbesLindesay

3
@Tuskan: การใช้ตัววนซ้ำแบบวนซ้ำยังมีผลต่อประสิทธิภาพดูส่วน "ต้นทุนของตัวทำซ้ำ " ของblogs.msdn.com/b/wesdyer/archive/2007/03/23/… (ยอมรับว่าต้นไม้ยังคงต้องมีความลึกพอสมควรสำหรับ สิ่งนี้จะเห็นได้ชัดเจน) และ fwiw ฉันพบว่าคำตอบของ vidstige นั้นสามารถอ่านได้เหมือนกับคำตอบแบบวนซ้ำที่นี่
LukeH

3
ใช่อย่าเลือกวิธีแก้ปัญหาของฉันเพราะประสิทธิภาพ ความสามารถในการอ่านเป็นอันดับแรกเสมอเว้นแต่จะพิสูจน์ได้ว่ามีปัญหาคอขวด แม้ว่าวิธีแก้ปัญหาของฉันจะค่อนข้างตรงไปตรงมา แต่ฉันคิดว่ามันเป็นเรื่องของรสนิยม ... จริงๆแล้วฉันโพสต์คำตอบของฉันเป็นเพียงส่วนเสริมของคำตอบแบบวนซ้ำ แต่ฉันดีใจที่คนชอบ
vidstige

11
ฉันคิดว่ามันคุ้มค่าที่จะกล่าวถึงโซลูชันที่นำเสนอข้างต้นทำการค้นหาในเชิงลึกก่อน (ลูกคนสุดท้ายก่อน) หากคุณต้องการการค้นหาแบบกว้างเป็นอันดับแรก (ลูกตัวแรก) คุณสามารถเปลี่ยนประเภทของคอลเลกชันโหนดเป็นQueue<Node>(ด้วยการเปลี่ยนแปลงที่เกี่ยวข้องกับEnqueue/ DequeueจากPush/ Pop)
Andrew Coonce

16

ค้นหา Tree of Objects ด้วย 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 แก้ปัญหาโดยทั่วไป บทความที่เชื่อมโยงให้คำอธิบายที่ดี
John Jesus

เพื่อให้สมบูรณ์คุณต้องมีการตรวจสอบค่าว่างบนพารามิเตอร์headและchildrenFuncแบ่งวิธีการออกเป็นสองส่วนดังนั้นการตรวจสอบพารามิเตอร์จึงไม่ถูกเลื่อนไปเป็นเวลาข้าม
ErikE

15

หากคุณต้องการรักษา Linq เหมือนไวยากรณ์คุณสามารถใช้วิธีการรับลูกหลานทั้งหมด (เด็ก + ลูกของเด็ก ฯลฯ )

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

จากนั้นสามารถแจงนับได้เช่นเดียวกับที่อื่นโดยใช้ที่ไหนหรือก่อนหรืออะไรก็ได้


ชอบแบบนี้สะอาด! :)
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
โปรดทราบว่าเทคนิคนี้ไม่มีประสิทธิภาพหากต้นไม้อยู่ลึกและอาจเกิดข้อยกเว้นได้หากต้นไม้อยู่ลึกมาก
Eric Lippert

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

@Ufuk: บรรทัดที่ 1 ทำงานได้เพียง 1 ระดับลึก 2 ระดับเท่านั้น หากคุณต้องการค้นหาในทุกระดับคุณต้องมีฟังก์ชันเรียกซ้ำ
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;
        }
    }

จากนั้นคุณสามารถค้นหาเช่น:

    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 by Name คุณสามารถส่งผ่าน 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

ในขณะที่ฉันเขียนบทความ codeproject ซึ่งอธิบายถึงวิธีการใช้ Linq เพื่อค้นหาโครงสร้างที่เหมือนต้นไม้:

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

สิ่งนี้ให้ API สไตล์ linq-to-XML ที่คุณสามารถค้นหาลูกหลานลูกหลานบรรพบุรุษ ฯลฯ ...

อาจมากเกินไปสำหรับปัญหาปัจจุบันของคุณ แต่อาจเป็นที่สนใจของผู้อื่น


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

และเพื่อความสนุกสนาน (เกือบทศวรรษต่อมา) คำตอบที่ใช้ Generics แต่ใช้ Stack และ While loop โดยอิงจากคำตอบที่ยอมรับโดย @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);
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.