재귀를 사용하지 않고 나무를 횡단하는 방법은 무엇입니까?


19

메모리 노드 트리가 매우 커서 트리를 통과해야합니다. 각 자식 노드의 반환 값을 부모 노드로 전달합니다. 모든 노드가 루트 노드까지 데이터 버블을 가질 때까지이 작업을 수행해야합니다.

순회는 이와 같이 작동합니다.

private Data Execute(Node pNode)
{
    Data[] values = new Data[pNode.Children.Count];
    for(int i=0; i < pNode.Children.Count; i++)
    {
        values[i] = Execute(pNode.Children[i]);  // recursive
    }
    return pNode.Process(values);
}

public void Start(Node pRoot)
{
    Data result = Execute(pRoot);
}

이것은 잘 작동하지만 호출 스택이 노드 트리의 크기를 제한한다고 걱정됩니다.

재귀 호출을하지 않도록 코드를 어떻게 다시 작성할 수 Execute있습니까?


8
노드를 추적하려면 자체 스택을 유지 관리하거나 트리 모양을 변경해야합니다. 참조 stackoverflow.com/q/5496464stackoverflow.com/q/4581576
로버트 하비

1
또한 이 Google 검색 , 특히 Morris Traversal 에서 많은 도움을 받았습니다 .
Robert Harvey

@RobertHarvey 고맙습니다 Rob, 나는 이것이 어떤 용어인지 알지 못했습니다.
Reactgular

2
계산을하면 메모리 요구 사항에 놀랄 수 있습니다. 예를 들어, 완벽하게 균형 잡힌 테라 노드 이진 트리는 스택 40 항목 만 필요합니다.
Karl Bielefeldt

@KarlBielefeldt 그것은 나무가 완벽하게 균형을 잡았다 고 가정합니다. 때로는 균형 이 맞지 않는 나무를 모델링해야하는데 ,이 경우 스택을 날리는 것이 매우 쉽습니다.
Servy

답변:


27

다음은 재귀를 사용하지 않는 범용 트리 순회 구현입니다.

public static IEnumerable<T> Traverse<T>(T item, Func<T, IEnumerable<T>> childSelector)
{
    var stack = new Stack<T>();
    stack.Push(item);
    while (stack.Any())
    {
        var next = stack.Pop();
        yield return next;
        foreach (var child in childSelector(next))
            stack.Push(child);
    }
}

귀하의 경우 다음과 같이 호출 할 수 있습니다.

IEnumerable<Node> allNodes = Traverse(pRoot, node => node.Children);

깊이 대신 검색을 위해 먼저 호흡 Queue대신 a Stack를 사용하십시오 . PriorityQueue최상의 첫 검색을 위해를 사용하십시오 .


이것이 나무를 컬렉션으로 평평하게 할 것이라고 생각하는 것이 맞습니까?
Reactgular

1
@MathewFoscarini 네, 그 목적입니다. 물론 반드시 실제 컬렉션으로 구체화 될 필요는 없습니다. 시퀀스 일뿐입니다. 전체 데이터 세트를 메모리로 가져 오지 않고도 데이터를 스트리밍하기 위해 반복 할 수 있습니다.
Servy

나는 그것이 문제를 해결한다고 생각하지 않는다.
Reactgular

4
그는 검색과 같은 독립적 인 작업을 수행하는 그래프를 순회 할뿐 아니라 자식 노드에서 데이터를 집계합니다. 트리를 평탄화하면 집계를 수행하는 데 필요한 구조 정보가 손상됩니다.
Karl Bielefeldt

1
참고로, 이것이이 질문을 검색하는 대부분의 사람들이 정답이라고 생각합니다. +1
Anders Arpi

4

사전에 나무 깊이에 대한 추정치가 있다면 사례가 스택 크기를 조정하는 것으로 충분합니까? 버전 2.0 이후의 C #에서는 새 스레드를 시작할 때마다 가능합니다. 여기를 참조하십시오.

http://www.atalasoft.com/cs/blogs/rickm/archive/2008/04/22/increasing-the-size-of-your-stack-net-memory-management-part-3.aspx

그렇게하면 더 복잡한 것을 구현하지 않고도 재귀 코드를 유지할 수 있습니다. 물론 자신의 스택으로 비 재귀 솔루션을 만드는 것이 시간과 메모리 효율성이 더 높을 수 있지만 코드가 지금처럼 간단하지 않을 것이라고 확신합니다.


방금 빠른 테스트를했습니다. 내 컴퓨터에서 스택 오버 플로우에 도달하기 전에 14000 재귀 호출을 할 수 있습니다. 트리가 균형을 이루면 40 억 개의 노드를 저장하기 위해 32 개의 호출 만 필요합니다. 각 노드가 1 바이트 인 경우 (이 습관 함)는 높이 (32)의 균형 잡힌 트리 저장하는 4GB의 RAM이 걸릴 것이다
Esben SKOV 페데르센

스택에서 14000 개의 모든 호출을 사용해야했습니다. 각 노드가 하나의 바이트 (이 습관 된) 인 경우 나무는 2.6x10 ^ 4214 바이트를 차지할
Esben SKOV 페데르센이

-3

재귀를 사용하지 않고 트리 형태로 데이터 구조를 순회 할 수 없습니다. 언어에서 제공하는 스택 프레임 및 함수 호출을 사용하지 않으면 기본적으로 자체 스택 및 함수 호출을 프로그래밍해야합니다. 가능성 당신이 그것을 어떻게 관리하는 컴파일러 작가가 프로그램을 실행할 것이라는 시스템에서보다 더 효율적인 방법으로 방식으로 언어.

따라서 자원 제한에 대한 두려움으로 인해 재귀를 피하는 것은 일반적으로 잘못된 것입니다. 확실히, 조기 리소스 최적화는 항상 잘못 안내되지만,이 경우 메모리 사용량이 병목 상태인지 측정하고 확인하더라도 시스템 수준을 떨어 뜨리지 않으면 서 향상시킬 수 없을 것입니다. 컴파일러 라이터.


2
이것은 단순히 거짓입니다. 재귀를 사용하지 않고 나무를 횡단하는 것이 가장 확실합니다. 어렵지 않습니다 . 명시 적 스택에 특정 순회에 필요한만큼의 정보 만 포함 할 수 있으므로 재귀를 사용하면 실제로는 많은 정보에서 필요한 것보다 많은 정보를 저장하게되므로 훨씬 효율적으로 아주 간단하게 수행 할 수 있습니다. 사례.
Servy

2
이 논쟁은 가끔씩 일어납니다. 일부 포스터는 자신의 스택을 재귀가 아닌 것으로 간주하는 반면 다른 포스터는 런타임이 암시 적으로 수행하는 것과 똑같은 일을 명시 적으로 수행한다고 지적합니다. 이와 같은 정의에 대해서는 논쟁 할 필요가 없습니다.
Kilian Foth

그러면 재귀를 어떻게 정의합니까? 자체 정의 내에서 자체를 호출하는 함수로 정의합니다. 내 대답에서 증명했듯이 나무를 횡단하지 않아도 가장 확실하게 횡단 할 수 있습니다.
Servy

2
그런 높은 점수를받은 사람에게 공감대를 클릭하는 행위를 즐기는 것이 나쁜가요? 이 웹 사이트에서 매우 희귀 한 즐거움입니다.
Reactgular

2
@Mat에 가자, 그건 꼬마 야. 너무 깊은 나무에서 폭탄을 터뜨리는 것을 두려워하는 경우 합리적 인 우려가있는 것처럼 동의하지 않을 수 있습니다. 당신은 그렇게 말할 수 있습니다.
Mike Dunlavey가
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.