LINQ를 통해 트리를 평면화하는 방법은 무엇입니까?


95

그래서 간단한 트리가 있습니다.

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

나는 IEnumerable<MyNode>. 나는 모든 MyNode(내부 노드 객체 ( Elements) 포함) 목록을 하나의 플랫 목록 으로 얻고 싶습니다 Where group == 1. LINQ를 통해 이러한 작업을 수행하는 방법은 무엇입니까?


1
병합 된 목록을 어떤 순서로 원하십니까?
Philip

1
노드는 언제 자식 노드를 가지지 않습니까? 나는 그것이 Elementsnull이거나 비어 있을 때라고 생각 합니까?
Adam Houldsworth


이 문제를 해결하는 가장 쉽고 명확한 방법은 재귀 LINQ 쿼리를 사용하는 것입니다. 이 질문 : stackoverflow.com/questions/732281/expressing-recursion-in-linq는 이 이상의 논의를 많이 가지고 있으며, 특별한 대답은 당신이 그것을 구현하는 것입니다 방법에 관한 몇 가지 세부 사항에 간다.
Alvaro Rodriguez

답변:


137

다음과 같이 나무를 평평하게 만들 수 있습니다.

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

그런 다음을 group사용하여 필터링 할 수 있습니다 Where(...).

"스타일 점수"를 얻으려면 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있다).
dasblinkenlight 2012 년

동의하지 않습니다. 컴파일, 테스트 및 c. 사용하면 e컴파일되지 않습니다. if (e == null) return Enumerable.Empty<T>();null 자식 목록에 대처하기 위해 추가 할 수도 있습니다 .
Adam Houldsworth

1
`public static IEnumerable <T> Flatten <T> (this IEnumerable <T> source, Func <T, IEnumerable <T >> f) {if (source == null) return Enumerable.Empty <T> (); return source.SelectMany (c => f (c) .Flatten (f)). Concat (source); }`
myWallJSON

10
이 솔루션은 O (nh)입니다. 여기서 n은 트리의 항목 수이고 h는 트리의 평균 깊이입니다. h는 O (1)과 O (n) 사이에있을 수 있으므로 O (n)과 O (n 제곱) 알고리즘 사이입니다. 더 나은 알고리즘이 있습니다.
Eric Lippert

1
목록이 IEnumerable <baseType> 인 경우 함수가 병합 된 목록에 요소를 추가하지 않는 것으로 나타났습니다. 이 같은 함수를 호출하여이 문제를 해결 할 수 있습니다 : var에 입술을 = tree.Flatten (노드 => node.Elements.OfType <DerivedType>)
프랭크 Horemans

125

받아 들여지는 대답의 문제는 트리가 깊으면 비효율적이라는 것입니다. 나무가 매우 깊으면 스택이 날아갑니다. 명시 적 스택을 사용하여 문제를 해결할 수 있습니다.

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 개 노드와 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 2014 년

3
@ebramtharwat : 맞습니다. Traverse모든 요소를 호출 할 수 있습니다. 또는 Traverse시퀀스를 취하도록 수정 하여 시퀀스의 모든 요소를 stack. stack"내가 아직 횡단하지 않은 요소"라는 것을 기억하십시오 . 또는 시퀀스가 ​​자식 인 "더미"루트를 만든 다음 더미 루트를 횡단 할 수 있습니다.
Eric Lippert

2
그렇게 foreach (var child in current.Elements.Reverse())하면 더 기대되는 평탄화를 얻을 수 있습니다. 특히 아이들은 마지막 아이가 아닌 순서대로 나타납니다. 이것은 대부분의 경우 중요하지 않지만 제 경우에는 예측 가능하고 예상되는 순서로 평탄화가 필요했습니다.
Micah Zoltu 2015

2
@MicahZoltu, 당신은을 피할 수 .Reverse를 교환하여 Stack<T>A의Queue<T>
루벤스 파리 아스

2
@MicahZoltu 순서에 대해서는 정확하지만 문제 Reverse는 추가 반복자를 생성한다는 것입니다.이 접근 방식은 피해야합니다. @RubensFarias 너비 우선 순회에서 결과를 대체 Queue합니다 Stack.
Jack A.

25

완전성을 위해 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) stack.Push (child); }
serg apr.

2
이것이 목록을 평평하게하더라도 역순으로 반환한다는 점에 주목하고 싶습니다. 마지막 요소는 제 등해진다
Corcus

21

최신 정보:

중첩 수준 (깊이)에 관심이있는 사람들을위한 것입니다. 명시 적 열거 자 스택 구현에 대한 좋은 점 중 하나는 언제든지 (특히 요소를 생성 할 때) 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);

실물:

놀랍게도 아무도 (심지어 Eric) 재귀 적 선주문 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 나는 특별히 사전 주문을 원했습니다. 작은 변화로 주문 후 처리 할 수 ​​있습니다. 하지만 요점은 이것이 Depth First Traversal 입니다. 들어 호흡 우선 탐색 내가 사용합니다 Queue<T>. 어쨌든, 여기서 아이디어는 재귀 구현에서 발생하는 것과 매우 유사한 열거자를 사용하여 작은 스택을 유지하는 것입니다.
Ivan Stoev

@IvanStoev 나는 코드가 단순화 될 것이라고 생각했습니다. 를 사용하면 Stack지그재그 Breadth First Traversal이 발생합니다.
NetMage

7

여기에 제공된 답변에서 몇 가지 작은 문제를 발견했습니다.

  • 초기 항목 목록이 null이면 어떻게됩니까?
  • 자식 목록에 null 값이 있으면 어떻게됩니까?

이전 답변을 바탕으로 다음을 생각해 냈습니다.

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

다른 사람이 이것을 찾았지만 나무를 평평하게 한 후에 레벨을 알아야 할 경우, Konamiman의 dasblinkenlight와 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

정말 다른 옵션은 적절한 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.Empty가 새 목록보다 더 좋을까요?
Frank

1
과연! 업데이트되었습니다!
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
확장에 foreach를 사용하면 더 이상 '지연된 실행'이 아닙니다 (물론 수익률 반환을 사용하지 않는 한).
Tri Q Tran 2013 년

0

중첩 수준이 필요한 경우 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();
            }
        }
    }

또한 ... 최초의 깊이를 지정할 수 또는 첫 번째 폭 것이 좋을 것이다

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

여기에서는 큐를 사용하여 구현을 사용할 준비가되어 있고 먼저 Flatten 트리를 반환 한 다음 자녀를 반환합니다.

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

가끔씩 나는이 문제를 긁어 내고 임의의 깊은 구조 (재귀 없음)를 지원하고 폭 우선 순회를 수행하며 너무 많은 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.