Làm thế nào để làm phẳng cây thông qua LINQ?


95

Vì vậy, tôi có cây đơn giản:

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

Tôi có một IEnumerable<MyNode>. Tôi muốn lấy một danh sách tất cả MyNode(bao gồm các đối tượng nút bên trong ( Elements)) dưới dạng một danh sách phẳng Where group == 1. Làm thế nào để làm điều đó thông qua LINQ?


1
Bạn muốn danh sách phẳng theo thứ tự nào?
Philip

1
Khi nào thì các nút ngừng có các nút con? Tôi đoán đó là khi Elementsnào trống hay rỗng?
Adam Houldsworth

có thể bị trùng lặp với stackoverflow.com/questions/11827569/…
Tamir

Cách dễ nhất / rõ ràng nhất để giải quyết vấn đề này là sử dụng truy vấn LINQ đệ quy. Câu hỏi này: stackoverflow.com/questions/732281/expressing-recursion-in-linq có rất nhiều cuộc thảo luận về vấn đề này và câu trả lời cụ thể này đi kèm một số chi tiết về cách bạn triển khai nó.
Alvaro Rodriguez

Câu trả lời:


137

Bạn có thể làm phẳng một cái cây như thế này:

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

Sau đó, bạn có thể lọc bằng groupcách sử dụng Where(...).

Để kiếm được một số "điểm cho phong cách", hãy chuyển đổi Flattensang một hàm mở rộng trong một lớp tĩnh.

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

Để kiếm được nhiều điểm hơn cho "phong cách thậm chí còn tốt hơn", hãy chuyển đổi Flattensang một phương pháp mở rộng chung sử dụng một cây và một hàm tạo ra con cháu từ một nút:

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

Gọi hàm này như thế này:

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

Nếu bạn muốn làm phẳng theo thứ tự trước hơn là theo thứ tự sau, hãy chuyển xung quanh các cạnh của Concat(...).


@AdamHouldsworth Cảm ơn bạn đã chỉnh sửa! Phần tử trong lệnh gọi Concatphải là new[] {e}, không phải new[] {c}(nó thậm chí sẽ không biên dịch với cở đó).
dasblinkenlight

Tôi không đồng ý: đã biên dịch, thử nghiệm và làm việc với c. Sử dụng ekhông biên dịch. Bạn cũng có thể thêm if (e == null) return Enumerable.Empty<T>();để đối phó với danh sách con rỗng.
Adam Houldsworth

1
giống như `public static IEnumerable <T> Flatten <T> (nguồn IEnumerable <T> này, Func <T, IEnumerable <T>> f) {if (source == null) return Enumerable.Empty <T> (); trả về source.SelectMany (c => f (c) .Flatten (f)). Concat (source); } `
myWallJSON

10
Lưu ý rằng giải pháp này là O (nh) với n là số lượng mục trong cây và h là độ sâu trung bình của cây. Vì h có thể nằm giữa O (1) và O (n), điều này nằm giữa thuật toán O (n) và O (n bình phương). Có những thuật toán tốt hơn.
Eric Lippert

1
Tôi nhận thấy rằng hàm sẽ không thêm các phần tử vào danh sách phẳng nếu danh sách là IEnumerable <baseType>. Bạn có thể giải quyết việc này bằng cách gọi hàm như thế này: var res = tree.Flatten (node => node.Elements.OfType <DerivedType>)
Frank Horemans

125

Vấn đề với câu trả lời được chấp nhận là nếu cây bị sâu thì không hiệu quả. Nếu cây rất sâu thì nó sẽ thổi đống. Bạn có thể giải quyết vấn đề bằng cách sử dụng một ngăn xếp rõ ràng:

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

Giả sử có n nút trong cây có chiều cao h và hệ số phân nhánh nhỏ hơn đáng kể n, phương pháp này là O (1) trong không gian ngăn xếp, O (h) trong không gian đống và O (n) trong thời gian. Thuật toán khác được đưa ra là O (h) trong ngăn xếp, O (1) trong đống và O (nh) trong thời gian. Nếu hệ số phân nhánh nhỏ so với n thì h nằm giữa O (lg n) và O (n), điều này minh họa rằng thuật toán ngây thơ có thể sử dụng một lượng ngăn xếp nguy hiểm và một lượng lớn thời gian nếu h gần với n.

Bây giờ chúng tôi có một đường truyền, truy vấn của bạn rất đơn giản:

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

3
@johnnycardy: Nếu bạn định tranh luận một điểm thì có lẽ mã rõ ràng không chính xác. Điều gì có thể làm cho nó chính xác rõ ràng hơn?
Eric Lippert

3
@ebramtharwat: Đúng. Bạn có thể gọi Traversetất cả các yếu tố. Hoặc bạn có thể sửa đổi Traverseđể lấy một chuỗi và để nó đẩy tất cả các phần tử của chuỗi lên đó stack. Hãy nhớ rằng, stacklà "các yếu tố tôi chưa đi qua". Hoặc bạn có thể tạo một gốc "dummy" trong đó chuỗi của bạn là con của nó, sau đó đi ngang qua gốc giả.
Eric Lippert

2
Nếu bạn làm như vậy, foreach (var child in current.Elements.Reverse())bạn sẽ nhận được một sự phẳng hơn mong đợi. Đặc biệt, các con sẽ xuất hiện theo thứ tự xuất hiện chứ không phải con cuối cùng trước. Điều này không thành vấn đề trong hầu hết các trường hợp, nhưng trong trường hợp của tôi, tôi cần sự san phẳng theo một thứ tự có thể dự đoán và mong đợi.
Micah Zoltu

2
@MicahZoltu, bạn có thể tránh được .Reversebằng cách trao đổi các Stack<T>cho mộtQueue<T>
Rubens Farias

2
@MicahZoltu Bạn nói đúng về thứ tự, nhưng vấn đề Reverselà nó tạo thêm các trình lặp, đó là điều mà cách tiếp cận này có nghĩa là phải tránh. @RubensFarias Thay thế Queueđể có Stackkết quả theo chiều rộng-thứ nhất.
Jack A.

25

Đây là sự kết hợp các câu trả lời từ dasblinkenlight và Eric Lippert. Đơn vị đã thử nghiệm và mọi thứ. :-)

 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
Để tránh NullReferenceException var children = getChildren (current); if (children! = null) {foreach (var child in children) stack.Push (child); }
serg

2
Tôi muốn lưu ý rằng mặc dù điều này làm phẳng danh sách, nhưng nó trả về thứ tự ngược lại. Phần tử cuối cùng trở thành phần tử đầu tiên, v.v.
Corcus

21

Cập nhật:

Đối với những người quan tâm đến mức độ làm tổ (độ sâu). Một trong những điều tốt về việc triển khai ngăn xếp liệt kê rõ ràng là tại bất kỳ thời điểm nào (và đặc biệt là khi cung cấp phần tử), nó stack.Countthể hiện độ sâu xử lý hiện tại. Vì vậy, có tính đến điều này và sử dụng các bộ giá trị C # 7.0, chúng ta có thể chỉ cần thay đổi khai báo phương thức như sau:

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

yieldtuyên bố:

yield return (item, stack.Count);

Sau đó, chúng tôi có thể thực hiện phương pháp ban đầu bằng cách áp dụng đơn giản Selectở trên:

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

Nguyên:

Đáng ngạc nhiên là không ai (ngay cả Eric) cho thấy cổng lặp lại "tự nhiên" của một DFT đặt trước đệ quy, vì vậy đây là:

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

Tôi giả sử bạn chuyển đổi emỗi khi bạn gọi elementSelectorđể duy trì đơn đặt hàng trước - nếu đơn đặt hàng không quan trọng, bạn có thể thay đổi chức năng để xử lý tất cả mỗi ekhi bắt đầu không?
NetMage

@NetMage Tôi muốn đặt hàng trước cụ thể. Với thay đổi nhỏ, nó có thể xử lý đơn đặt hàng bài. Nhưng vấn đề chính là, đây là Giao dịch đầu tiên theo chiều sâu . Đối với Breath First Traversal, tôi sẽ sử dụng Queue<T>. Dù sao, ý tưởng ở đây là giữ một ngăn xếp nhỏ với các bộ điều tra, rất giống với những gì đang xảy ra trong quá trình triển khai đệ quy.
Ivan Stoev

@IvanStoev Tôi đã nghĩ rằng mã sẽ được đơn giản hóa. Tôi đoán việc sử dụng giá trị Stacknày sẽ dẫn đến Đường truyền đầu tiên theo chiều rộng zig-zag.
NetMage

7

Tôi đã tìm thấy một số vấn đề nhỏ với câu trả lời được đưa ra ở đây:

  • Điều gì sẽ xảy ra nếu danh sách các mục ban đầu là rỗng?
  • Điều gì sẽ xảy ra nếu có một giá trị null trong danh sách con?

Được xây dựng dựa trên các câu trả lời trước đó và đưa ra những điều sau:

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

Và các bài kiểm tra đơn vị:

[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

Trong trường hợp bất kỳ ai khác tìm thấy điều này, nhưng cũng cần biết mức độ sau khi họ đã san phẳng cây, điều này mở rộng dựa trên sự kết hợp của Konamiman giữa các giải pháp dasblinkenlight và Eric Lippert:

    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

Một lựa chọn thực sự khác là có một thiết kế OO thích hợp.

Ví dụ: yêu cầu MyNodetrả lại tất cả các phẳng.

Như thế này:

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

Bây giờ bạn có thể yêu cầu MyNode cấp cao nhất để lấy tất cả các nút.

var flatten = topNode.GetAllNodes();

Nếu bạn không thể chỉnh sửa lớp, thì đây không phải là một tùy chọn. Nhưng nếu không, tôi nghĩ rằng điều này có thể được ưu tiên hơn một phương pháp LINQ riêng biệt (đệ quy).

Đây là sử dụng LINQ, Vì vậy, tôi nghĩ câu trả lời này có thể áp dụng ở đây;)


Có lẽ Enumerabl.Empty tốt hơn List mới?
Frank

1
Thật! Đã cập nhật!
Julian

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
sử dụng foreach trong tiện ích mở rộng của bạn có nghĩa là nó không còn là 'thực thi bị trì hoãn' nữa (tất nhiên trừ khi bạn sử dụng lợi nhuận trả về).
Tri Q Tran

0

Kết hợp câu trả lời của Dave và Ivan Stoev trong trường hợp bạn cần mức độ lồng vào nhau và danh sách được làm phẳng "theo thứ tự" chứ không phải đảo ngược như trong đáp án mà Konamiman đưa ra.

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

Nó cũng sẽ được tốt đẹp để có thể xác định độ sâu đầu tiên hoặc theo chiều rộng đầu tiên ...
Hugh

0

Dựa trên câu trả lời của Konamiman và nhận xét rằng thứ tự không mong muốn, đây là một phiên bản có tham số sắp xếp rõ ràng:

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

Và cách sử dụng mẫu:

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

0

Dưới đây là đoạn mã của Ivan Stoev với tính năng bổ sung cho biết chỉ số của mọi đối tượng trong đường dẫn. Ví dụ: tìm kiếm "Item_120":

Item_0--Item_00
        Item_01

Item_1--Item_10
        Item_11
        Item_12--Item_120

sẽ trả về mục và một mảng int [1,2,0]. Rõ ràng, mức lồng nhau cũng có sẵn, như độ dài của mảng.

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

Xin chào, @lisz, bạn dán mã này ở đâu? Tôi nhận được lỗi như "The modifier 'công cộng' không hợp lệ cho mặt hàng này", "Các modifier 'tĩnh' không hợp lệ cho mặt hàng này"
Kynao

0

Ở đây một số đã sẵn sàng để sử dụng việc triển khai bằng cách sử dụng Hàng đợi và trả về cây Flatten cho tôi trước rồi đến các con của tôi.

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

    }

0

Thỉnh thoảng, tôi cố gắng giải quyết vấn đề này và đưa ra giải pháp của riêng mình hỗ trợ các cấu trúc sâu tùy ý (không đệ quy), thực hiện truyền tải đầu tiên theo chiều rộng và không lạm dụng quá nhiều truy vấn LINQ hoặc thực thi đệ quy trước trên các phần tử con. Sau khi tìm hiểu về nguồn .NET và thử nhiều giải pháp, cuối cùng tôi đã đưa ra giải pháp này. Nó đã kết thúc rất gần với câu trả lời của Ian Stoev (người mà tôi mới chỉ thấy câu trả lời vừa rồi), tuy nhiên câu trả lời của tôi không sử dụng vòng lặp vô hạn hoặc có dòng mã bất thường.

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

Một ví dụ làm việc có thể được tìm thấy ở đây .

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.