วิธีการแบนต้นไม้ผ่าน LINQ


97

ดังนั้นฉันจึงมีต้นไม้ง่ายๆ:

class MyNode
{
 public MyNode Parent;
 public IEnumerable<MyNode> Elements;
 int group = 1;
}

ฉันมีIEnumerable<MyNode>. ฉันต้องการรับรายการทั้งหมดMyNode(รวมถึงวัตถุโหนดภายใน ( Elements)) เป็นรายการWhere group == 1เดียว ต้องทำอย่างไรผ่าน LINQ?


1
คุณต้องการให้รายการแบนอยู่ในลำดับใด
Philip

1
โหนดหยุดมีโหนดลูกเมื่อใด ฉันคิดว่าเมื่อElementsเป็นโมฆะหรือว่างเปล่า?
Adam Houldsworth

อาจจะซ้ำกับstackoverflow.com/questions/11827569/…
Tamir

วิธีที่ง่ายที่สุด / ชัดเจนที่สุดในการแก้ไขปัญหานี้คือการใช้แบบสอบถาม LINQ แบบเรียกซ้ำ คำถามนี้: stackoverflow.com/questions/732281/expressing-recursion-in-linqมีการพูดคุยกันมากมายเกี่ยวกับเรื่องนี้และคำตอบเฉพาะนี้มีรายละเอียดเกี่ยวกับวิธีการใช้งาน
Alvaro Rodriguez

คำตอบ:


141

คุณสามารถทำให้ต้นไม้แบนราบได้ดังนี้:

IEnumerable<MyNode> Flatten(IEnumerable<MyNode> e) =>
    e.SelectMany(c => Flatten(c.Elements)).Concat(new[] { e });

จากนั้นคุณสามารถกรองโดยใช้groupWhere(...)

หากต้องการรับ "คะแนนสำหรับรูปแบบ" บางส่วนให้แปลงFlattenเป็นฟังก์ชันส่วนขยายในคลาสแบบคงที่

public static IEnumerable<MyNode> Flatten(this IEnumerable<MyNode> e) =>
    e.SelectMany(c => c.Elements.Flatten()).Concat(e);

หากต้องการได้รับคะแนนเพิ่มขึ้นสำหรับ "สไตล์ที่ดียิ่งขึ้น" ให้แปลงFlattenเป็นวิธีการขยายทั่วไปที่รับต้นไม้และฟังก์ชันที่สร้างลูกหลานจากโหนด

public static IEnumerable<T> Flatten<T>(
    this IEnumerable<T> e
,   Func<T,IEnumerable<T>> f
) => e.SelectMany(c => f(c).Flatten(f)).Concat(e);

เรียกฟังก์ชันนี้ว่า

IEnumerable<MyNode> tree = ....
var res = tree.Flatten(node => node.Elements);

หากคุณต้องการแบนในการสั่งซื้อล่วงหน้ามากกว่าการสั่งซื้อภายหลังให้สลับด้านข้างของไฟล์Concat(...).


@AdamHouldsworth ขอบคุณสำหรับการแก้ไข! องค์ประกอบในการเรียกร้องConcatควรเป็นnew[] {e}ไม่ใช่new[] {c}(มันจะไม่รวบรวมด้วยcซ้ำ)
Sergey Kalinichenko

ผมไม่เห็นด้วย: cรวบรวมผ่านการทดสอบและการทำงานกับ การใช้eไม่ได้รวบรวม คุณยังสามารถเพิ่มif (e == null) return Enumerable.Empty<T>();เพื่อจัดการกับรายการย่อยที่ว่างเปล่า
Adam Houldsworth

1
มากกว่าเช่น `สาธารณะคงที่ IEnumerable <T> แบน <T> (แหล่งที่มาของ IEnumerable <T>, Func <T, IEnumerable <T>> f) {if (source == null) return Enumerable.Empty <T> (); return source.SelectMany (c => f (c) .Flatten (f)) Concat (ที่มา); } `
myWallJSON

11
โปรดสังเกตว่าโซลูชันนี้คือ O (nh) โดยที่ n คือจำนวนรายการในต้นไม้และ h คือความลึกเฉลี่ยของต้นไม้ เนื่องจาก h สามารถอยู่ระหว่าง O (1) และ O (n) จึงอยู่ระหว่างอัลกอริทึม O (n) และ O (n กำลังสอง) มีอัลกอริทึมที่ดีกว่า
Eric Lippert

1
ฉันสังเกตเห็นว่าฟังก์ชันจะไม่เพิ่มองค์ประกอบลงในรายการแบบแบนหากรายการเป็น <baseType> ที่สามารถใช้งานได้ของ IE คุณสามารถแก้ปัญหานี้ได้โดยเรียกใช้ฟังก์ชันดังนี้: var res = tree.Flatten (node ​​=> node.Elements.OfType <DerivedType>)
Frank Horemans

127

ปัญหาเกี่ยวกับคำตอบที่ได้รับการยอมรับคือมันไม่มีประสิทธิภาพถ้าต้นไม้อยู่ลึก ถ้าต้นไม้อยู่ลึกมากก็พัดกอง คุณสามารถแก้ปัญหาได้โดยใช้ Explicit stack:

public static IEnumerable<MyNode> Traverse(this MyNode root)
{
    var stack = new Stack<MyNode>();
    stack.Push(root);
    while(stack.Count > 0)
    {
        var current = stack.Pop();
        yield return current;
        foreach(var child in current.Elements)
            stack.Push(child);
    }
}

สมมติว่าโหนดในต้นไม้ที่มีความสูง h และปัจจัยการแตกแขนงน้อยกว่า n อย่างมากวิธีนี้คือ O (1) ในพื้นที่สแต็ก O (h) ในพื้นที่ฮีปและ O (n) ในเวลา อัลกอริทึมอื่น ๆ ที่กำหนดคือ O (h) ในสแต็ก O (1) ในฮีปและ O (nh) ในเวลา หากปัจจัยการแตกแขนงมีขนาดเล็กเมื่อเทียบกับ n ดังนั้น h จะอยู่ระหว่าง O (lg n) และ O (n) ซึ่งแสดงให้เห็นว่าอัลกอริทึมไร้เดียงสาสามารถใช้กองซ้อนที่เป็นอันตรายและใช้เวลาจำนวนมากหาก h ใกล้กับ n

ตอนนี้เรามีการส่งผ่านแล้วคำถามของคุณตรงไปตรงมา:

root.Traverse().Where(item=>item.group == 1);

3
@johnnycardy: หากคุณกำลังจะโต้แย้งประเด็นบางทีรหัสอาจไม่ถูกต้องอย่างชัดเจน อะไรจะทำให้ถูกต้องชัดเจนขึ้น
Eric Lippert

4
@ebramtharwat: ถูกต้อง. คุณสามารถเรียกTraverseใช้องค์ประกอบทั้งหมด หรือคุณอาจจะปรับเปลี่ยนจะใช้ลำดับและมีมันผลักดันให้ทุกองค์ประกอบของลำดับบนTraverse stackจำไว้ว่าstack"องค์ประกอบที่ฉันยังไม่ได้ผ่าน" คือ หรือคุณอาจสร้างรูท "จำลอง" โดยที่ลำดับของคุณคือลูกของมันจากนั้นสำรวจรูทจำลอง
Eric Lippert

2
หากคุณทำforeach (var child in current.Elements.Reverse())คุณจะได้รับความคาดหวังมากขึ้น โดยเฉพาะอย่างยิ่งเด็กจะปรากฏตามลำดับที่ปรากฏมากกว่าเด็กคนสุดท้ายก่อน สิ่งนี้ไม่สำคัญในกรณีส่วนใหญ่ แต่ในกรณีของฉันฉันต้องการให้แบนราบเพื่อให้อยู่ในลำดับที่คาดเดาได้และคาดหวัง
Micah Zoltu

2
@MicahZoltu คุณสามารถหลีกเลี่ยงได้.Reverseโดยการแลกเปลี่ยนStack<T>เป็นQueue<T>
Rubens Farias

2
@MicahZoltu คุณถูกต้องเกี่ยวกับคำสั่งซื้อ แต่ปัญหาReverseคือมันสร้างตัวทำซ้ำเพิ่มเติมซึ่งเป็นสิ่งที่แนวทางนี้มีไว้เพื่อหลีกเลี่ยง @RubensFarias การแทนที่Queueเพื่อStackผลลัพธ์ในการข้ามผ่านครั้งแรกแบบกว้าง
Jack A.

27

เพื่อความสมบูรณ์นี่คือการรวมกันของคำตอบจาก dasblinkenlight และ Eric Lippert หน่วยทดสอบและทุกอย่าง :-)

 public static IEnumerable<T> Flatten<T>(
        this IEnumerable<T> items,
        Func<T, IEnumerable<T>> getChildren)
 {
     var stack = new Stack<T>();
     foreach(var item in items)
         stack.Push(item);

     while(stack.Count > 0)
     {
         var current = stack.Pop();
         yield return current;

         var children = getChildren(current);
         if (children == null) continue;

         foreach (var child in children) 
            stack.Push(child);
     }
 }

3
เพื่อหลีกเลี่ยง NullReferenceException var children = getChildren (current); if (children! = null) {foreach (var child in children) stack.Push (child); }
serg

3
ฉันต้องการทราบว่าแม้ว่าสิ่งนี้จะทำให้รายการแบน แต่ก็ส่งกลับในลำดับย้อนกลับ องค์ประกอบสุดท้ายกลายเป็นสิ่งแรกเป็นต้น
Corcus

23

อัปเดต:

สำหรับคนที่สนใจเรื่องระดับความลึก สิ่งที่ดีอย่างหนึ่งเกี่ยวกับการใช้งานสแต็กตัวแจงนับอย่างชัดเจนก็คือในช่วงเวลาใด ๆ (และโดยเฉพาะอย่างยิ่งเมื่อให้องค์ประกอบ) stack.Countความลึกของการประมวลผล ดังนั้นเมื่อคำนึงถึงสิ่งนี้และการใช้สิ่งที่เป็นค่า C # 7.0 เราสามารถเปลี่ยนวิธีการประกาศได้ดังนี้:

public static IEnumerable<(T Item, int Level)> ExpandWithLevel<T>(
    this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)

และyieldคำสั่ง:

yield return (item, stack.Count);

จากนั้นเราสามารถใช้วิธีการเดิมได้โดยใช้วิธีง่ายๆSelectในด้านบน:

public static IEnumerable<T> Expand<T>(
    this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector) =>
    source.ExpandWithLevel(elementSelector).Select(e => e.Item);

ต้นฉบับ:

น่าแปลกใจที่ไม่มีใคร (แม้แต่เอริค) แสดงพอร์ตการทำซ้ำแบบ "ธรรมชาติ" ของ DFT แบบสั่งซื้อล่วงหน้าแบบวนซ้ำดังนั้นนี่คือ:

    public static IEnumerable<T> Expand<T>(
        this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)
    {
        var stack = new Stack<IEnumerator<T>>();
        var e = source.GetEnumerator();
        try
        {
            while (true)
            {
                while (e.MoveNext())
                {
                    var item = e.Current;
                    yield return item;
                    var elements = elementSelector(item);
                    if (elements == null) continue;
                    stack.Push(e);
                    e = elements.GetEnumerator();
                }
                if (stack.Count == 0) break;
                e.Dispose();
                e = stack.Pop();
            }
        }
        finally
        {
            e.Dispose();
            while (stack.Count != 0) stack.Pop().Dispose();
        }
    }

ฉันถือว่าคุณเปลี่ยนeทุกครั้งที่โทรelementSelectorเพื่อรักษาการสั่งซื้อล่วงหน้า - หากคำสั่งซื้อไม่สำคัญคุณสามารถเปลี่ยนฟังก์ชันเพื่อประมวลผลทั้งหมดeเมื่อเริ่มต้นได้หรือไม่
NetMage

@NetMage ฉันต้องการสั่งซื้อล่วงหน้าโดยเฉพาะ ด้วยการเปลี่ยนแปลงเล็กน้อยสามารถจัดการกับคำสั่งซื้อภายหลังได้ แต่จุดหลักนี้เป็นความลึกครั้งแรกที่ข้ามผ่าน สำหรับลมหายใจแรกข้ามQueue<T>ผมจะใช้ อย่างไรก็ตามแนวคิดในที่นี้คือการเก็บสแต็กเล็ก ๆ ไว้กับตัวนับซึ่งคล้ายกับสิ่งที่เกิดขึ้นในการนำไปใช้ซ้ำ
Ivan Stoev

@IvanStoev ฉันคิดว่ารหัสจะง่ายขึ้น ฉันเดาว่าการใช้สิ่งStackนี้จะส่งผลให้เกิดการข้ามผ่านครั้งแรกแบบซิกแซก
NetMage

อะไรคือจุดในการรักษา a Stack<IEnumerator<T>>แทน a Stack<T>? โดยทั่วไปแล้วตัวแจงนับเป็นประเภทค่าที่ไม่แน่นอนและโดยปกติจะใช้เป็นเครื่องของรัฐ ดังนั้นฉันคาดว่าStack<IEnumerator<T>>วิธีแก้ปัญหาโดยทั่วไปแล้วหน่วยความจำจะไม่มีประสิทธิภาพและเพื่อเพิ่มแรงกดดันให้กับตัวรวบรวมขยะ (เนื่องจากประเภทค่าบรรจุกล่อง)
Theodor Zoulias

1
อีวานหลังจากตรวจสอบอย่างใกล้ชิดคุณอยู่ที่ทั้งสองจุด การชกมวยเป็นสิ่งที่หลีกเลี่ยงไม่ได้และการจัดเก็บเครื่องนับของเด็กนั้นดีกว่าการเก็บเด็กทั้งหมด โหวตแล้ว :-)
Theodor Zoulias

8

ฉันพบปัญหาเล็กน้อยกับคำตอบที่ให้ไว้ที่นี่:

  • จะเกิดอะไรขึ้นถ้ารายการเริ่มต้นของรายการเป็นโมฆะ?
  • จะเกิดอะไรขึ้นถ้ามีค่าว่างในรายการลูก?

สร้างขึ้นจากคำตอบก่อนหน้านี้และได้รับสิ่งต่อไปนี้:

public static class IEnumerableExtensions
{
    public static IEnumerable<T> Flatten<T>(
        this IEnumerable<T> items, 
        Func<T, IEnumerable<T>> getChildren)
    {
        if (items == null)
            yield break;

        var stack = new Stack<T>(items);
        while (stack.Count > 0)
        {
            var current = stack.Pop();
            yield return current;

            if (current == null) continue;

            var children = getChildren(current);
            if (children == null) continue;

            foreach (var child in children)
                stack.Push(child);
        }
    }
}

และการทดสอบหน่วย:

[TestClass]
public class IEnumerableExtensionsTests
{
    [TestMethod]
    public void NullList()
    {
        IEnumerable<Test> items = null;
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(0, flattened.Count());
    }
    [TestMethod]
    public void EmptyList()
    {
        var items = new Test[0];
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(0, flattened.Count());
    }
    [TestMethod]
    public void OneItem()
    {
        var items = new[] { new Test() };
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(1, flattened.Count());
    }
    [TestMethod]
    public void OneItemWithChild()
    {
        var items = new[] { new Test { Id = 1, Children = new[] { new Test { Id = 2 } } } };
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(2, flattened.Count());
        Assert.IsTrue(flattened.Any(i => i.Id == 1));
        Assert.IsTrue(flattened.Any(i => i.Id == 2));
    }
    [TestMethod]
    public void OneItemWithNullChild()
    {
        var items = new[] { new Test { Id = 1, Children = new Test[] { null } } };
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(2, flattened.Count());
        Assert.IsTrue(flattened.Any(i => i.Id == 1));
        Assert.IsTrue(flattened.Any(i => i == null));
    }
    class Test
    {
        public int Id { get; set; }
        public IEnumerable<Test> Children { get; set; }
    }
}

4

ในกรณีที่ใครก็ตามพบสิ่งนี้ แต่ก็จำเป็นต้องทราบระดับหลังจากที่พวกเขาแบนต้นไม้แล้วสิ่งนี้จะขยายไปสู่การผสมผสานระหว่าง dasblinkenlight และโซลูชันของ Eric Lippert ของ Konamiman:

    public static IEnumerable<Tuple<T, int>> FlattenWithLevel<T>(
            this IEnumerable<T> items,
            Func<T, IEnumerable<T>> getChilds)
    {
        var stack = new Stack<Tuple<T, int>>();
        foreach (var item in items)
            stack.Push(new Tuple<T, int>(item, 1));

        while (stack.Count > 0)
        {
            var current = stack.Pop();
            yield return current;
            foreach (var child in getChilds(current.Item1))
                stack.Push(new Tuple<T, int>(child, current.Item2 + 1));
        }
    }

2

อีกทางเลือกหนึ่งคือการออกแบบ OO ที่เหมาะสม

เช่นขอMyNodeให้กลับแบนทั้งหมด

แบบนี้:

class MyNode
{
    public MyNode Parent;
    public IEnumerable<MyNode> Elements;
    int group = 1;

    public IEnumerable<MyNode> GetAllNodes()
    {
        if (Elements == null)
        {
            return Enumerable.Empty<MyNode>(); 
        }

        return Elements.SelectMany(e => e.GetAllNodes());
    }
}

ตอนนี้คุณสามารถขอให้ MyNode ระดับบนสุดรับโหนดทั้งหมดได้

var flatten = topNode.GetAllNodes();

หากคุณไม่สามารถแก้ไขชั้นเรียนได้แสดงว่านี่ไม่ใช่ตัวเลือก แต่อย่างอื่นฉันคิดว่านี่อาจเป็นที่ต้องการของวิธี LINQ (เรียกซ้ำ) แยกต่างหาก

นี่ใช้ LINQ ดังนั้นฉันคิดว่าคำตอบนี้ใช้ได้ที่นี่;)


อาจจะ Enumerabl ว่างเปล่าดีกว่ารายการใหม่?
Frank

1
แน่นอน! อัพเดท!
Julian

1

การรวมคำตอบของ Dave และ Ivan Stoev ในกรณีที่คุณต้องการระดับการซ้อนและรายการแบน "ตามลำดับ" และไม่ย้อนกลับเหมือนในคำตอบของ Konamiman

 public static class HierarchicalEnumerableUtils
    {
        private static IEnumerable<Tuple<T, int>> ToLeveled<T>(this IEnumerable<T> source, int level)
        {
            if (source == null)
            {
                return null;
            }
            else
            {
                return source.Select(item => new Tuple<T, int>(item, level));
            }
        }

        public static IEnumerable<Tuple<T, int>> FlattenWithLevel<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)
        {
            var stack = new Stack<IEnumerator<Tuple<T, int>>>();
            var leveledSource = source.ToLeveled(0);
            var e = leveledSource.GetEnumerator();
            try
            {
                while (true)
                {
                    while (e.MoveNext())
                    {
                        var item = e.Current;
                        yield return item;
                        var elements = elementSelector(item.Item1).ToLeveled(item.Item2 + 1);
                        if (elements == null) continue;
                        stack.Push(e);
                        e = elements.GetEnumerator();
                    }
                    if (stack.Count == 0) break;
                    e.Dispose();
                    e = stack.Pop();
                }
            }
            finally
            {
                e.Dispose();
                while (stack.Count != 0) stack.Pop().Dispose();
            }
        }
    }

นอกจากนี้ยังเป็นการดีที่สามารถระบุความลึกก่อนหรือด้านกว้างก่อน ...
ฮิวจ์

1

ที่นี่มีบางส่วนที่พร้อมใช้งานโดยใช้ Queue และส่งคืน Flatten tree ให้ฉันก่อนจากนั้นให้ลูก ๆ ของฉัน

public static IEnumerable<T> Flatten<T>(this IEnumerable<T> items, 
    Func<T,IEnumerable<T>> getChildren)
    {
        if (items == null)
            yield break;

        var queue = new Queue<T>();

        foreach (var item in items) {
            if (item == null)
                continue;

            queue.Enqueue(item);

            while (queue.Count > 0) {
                var current = queue.Dequeue();
                yield return current;

                if (current == null)
                    continue;

                var children = getChildren(current);
                if (children == null)
                    continue;

                foreach (var child in children)
                    queue.Enqueue(child);
            }
        }

    }

1

คำตอบส่วนใหญ่ที่นำเสนอในที่นี้คือการสร้างลำดับความลึกก่อนหรือซิกแซก ตัวอย่างเช่นเริ่มต้นด้วยต้นไม้ด้านล่าง:

        1                   2 
       / \                 / \
      /   \               /   \
     /     \             /     \
    /       \           /       \
   11       12         21       22
  / \       / \       / \       / \
 /   \     /   \     /   \     /   \
111 112   121 122   211 212   221 222

คำตอบของ dasblinkenlight สร้างลำดับที่แบนราบ:

111, 112, 121, 122, 11, 12, 211, 212, 221, 222, 21, 22, 1, 2

คำตอบของ Konamiman (ซึ่งสรุปคำตอบของ Eric Lippert ) สร้างลำดับที่ราบเรียบนี้:

2, 22, 222, 221, 21, 212, 211, 1, 12, 122, 121, 11, 112, 111

คำตอบของ Ivan Stoev สร้างลำดับที่ราบเรียบนี้:

1, 11, 111, 112, 12, 121, 122, 2, 21, 211, 212, 22, 221, 222

หากคุณสนใจลำดับความกว้างก่อนเช่นนี้:

1, 2, 11, 12, 21, 22, 111, 112, 121, 122, 211, 212, 221, 222

... นี่คือทางออกสำหรับคุณ:

public static IEnumerable<T> Flatten<T>(this IEnumerable<T> source,
    Func<T, IEnumerable<T>> childrenSelector)
{
    var queue = new Queue<T>(source);
    while (queue.Count > 0)
    {
        var current = queue.Dequeue();
        yield return current;
        var children = childrenSelector(current);
        if (children == null) continue;
        foreach (var child in children) queue.Enqueue(child);
    }
}

ความแตกต่างในการใช้งานนั้นโดยพื้นฐานแล้วจะใช้ a QueueแทนStack. ไม่มีการเรียงลำดับที่แท้จริงเกิดขึ้น


ข้อควรระวัง:การใช้งานนี้ยังห่างไกลจากประสิทธิภาพที่ดีที่สุดเกี่ยวกับประสิทธิภาพของหน่วยความจำเนื่องจากส่วนใหญ่ของจำนวนองค์ประกอบทั้งหมดจะถูกเก็บไว้ในคิวภายในระหว่างการแจงนับ Stack- การสำรวจตามต้นไม้ที่อิงตามนั้นมีประสิทธิภาพในการใช้หน่วยความจำมากกว่าการใช้งานQueueตามฐาน


0
void Main()
{
    var allNodes = GetTreeNodes().Flatten(x => x.Elements);

    allNodes.Dump();
}

public static class ExtensionMethods
{
    public static IEnumerable<T> Flatten<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> childrenSelector = null)
    {
        if (source == null)
        {
            return new List<T>();
        }

        var list = source;

        if (childrenSelector != null)
        {
            foreach (var item in source)
            {
                list = list.Concat(childrenSelector(item).Flatten(childrenSelector));
            }
        }

        return list;
    }
}

IEnumerable<MyNode> GetTreeNodes() {
    return new[] { 
        new MyNode { Elements = new[] { new MyNode() }},
        new MyNode { Elements = new[] { new MyNode(), new MyNode(), new MyNode() }}
    };
}

class MyNode
{
    public MyNode Parent;
    public IEnumerable<MyNode> Elements;
    int group = 1;
}

1
การใช้ foreach ในส่วนขยายของคุณหมายความว่าจะไม่ 'ดำเนินการล่าช้า' อีกต่อไป (เว้นแต่คุณจะใช้ผลตอบแทนที่ได้รับ)
Tri Q Tran

0

จากคำตอบของ Konamiman และความคิดเห็นที่การสั่งซื้อนั้นไม่คาดคิดนี่คือเวอร์ชันที่มีพารามิเตอร์การเรียงลำดับที่ชัดเจน:

public static IEnumerable<T> TraverseAndFlatten<T, V>(this IEnumerable<T> items, Func<T, IEnumerable<T>> nested, Func<T, V> orderBy)
{
    var stack = new Stack<T>();
    foreach (var item in items.OrderBy(orderBy))
        stack.Push(item);

    while (stack.Count > 0)
    {
        var current = stack.Pop();
        yield return current;

        var children = nested(current).OrderBy(orderBy);
        if (children == null) continue;

        foreach (var child in children)
            stack.Push(child);
    }
}

และตัวอย่างการใช้งาน:

var flattened = doc.TraverseAndFlatten(x => x.DependentDocuments, y => y.Document.DocDated).ToList();

0

ด้านล่างนี้คือรหัสของ Ivan Stoev ที่มีคุณสมบัติเพิ่มเติมในการบอกดัชนีของทุกวัตถุในเส้นทาง เช่นค้นหา "Item_120":

Item_0--Item_00
        Item_01

Item_1--Item_10
        Item_11
        Item_12--Item_120

จะส่งคืนรายการและอาร์เรย์ int [1,2,0] แน่นอนว่ายังมีระดับการซ้อนกันตามความยาวของอาร์เรย์

public static IEnumerable<(T, int[])> Expand<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> getChildren) {
    var stack = new Stack<IEnumerator<T>>();
    var e = source.GetEnumerator();
    List<int> indexes = new List<int>() { -1 };
    try {
        while (true) {
            while (e.MoveNext()) {
                var item = e.Current;
                indexes[stack.Count]++;
                yield return (item, indexes.Take(stack.Count + 1).ToArray());
                var elements = getChildren(item);
                if (elements == null) continue;
                stack.Push(e);
                e = elements.GetEnumerator();
                if (indexes.Count == stack.Count)
                    indexes.Add(-1);
                }
            if (stack.Count == 0) break;
            e.Dispose();
            indexes[stack.Count] = -1;
            e = stack.Pop();
        }
    } finally {
        e.Dispose();
        while (stack.Count != 0) stack.Pop().Dispose();
    }
}

สวัสดี @lisz คุณวางรหัสนี้ไว้ที่ไหน ฉันได้รับข้อผิดพลาดเช่น "ตัวปรับแต่ง" สาธารณะ "ไม่ถูกต้องสำหรับรายการนี้" "ตัวปรับแต่ง" คงที่ "ไม่สามารถใช้ได้กับรายการนี้"
Kynao

0

ทุก ๆ ครั้งในขณะที่ฉันพยายามที่จะขีดข่วนที่ปัญหานี้และคิดหาวิธีแก้ปัญหาของตัวเองที่รองรับโครงสร้างที่ลึกตามอำเภอใจ (ไม่มีการเรียกซ้ำ) ทำการสำรวจครั้งแรกในวงกว้างและไม่ละเมิดการสืบค้นของ LINQ มากเกินไปหรือดำเนินการเรียกซ้ำล่วงหน้ากับเด็ก ๆ หลังจากขุดค้นในแหล่งที่มา. NETและลองใช้วิธีแก้ปัญหามากมายในที่สุดฉันก็พบวิธีแก้ปัญหานี้ มันใกล้เคียงกับคำตอบของ Ian Stoev มาก (ซึ่งคำตอบที่ฉันเพิ่งเห็นในตอนนี้) แต่ของฉันไม่ได้ใช้การวนซ้ำที่ไม่มีที่สิ้นสุดหรือมีการไหลของรหัสที่ผิดปกติ

public static IEnumerable<T> Traverse<T>(
    this IEnumerable<T> source,
    Func<T, IEnumerable<T>> fnRecurse)
{
    if (source != null)
    {
        Stack<IEnumerator<T>> enumerators = new Stack<IEnumerator<T>>();
        try
        {
            enumerators.Push(source.GetEnumerator());
            while (enumerators.Count > 0)
            {
                var top = enumerators.Peek();
                while (top.MoveNext())
                {
                    yield return top.Current;

                    var children = fnRecurse(top.Current);
                    if (children != null)
                    {
                        top = children.GetEnumerator();
                        enumerators.Push(top);
                    }
                }

                enumerators.Pop().Dispose();
            }
        }
        finally
        {
            while (enumerators.Count > 0)
                enumerators.Pop().Dispose();
        }
    }
}

ตัวอย่างการทำงานที่สามารถพบได้ที่นี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.