나무는“처음으로 자식을 낳는”구조로 조직되어 있습니까? 그렇지 않다면 왜 안됩니까?


12

일반적으로 트리 데이터 구조는 각 노드가 모든 하위 노드에 대한 포인터를 포함하는 방식으로 구성됩니다.

       +-----------------------------------------+
       |        root                             | 
       | child1            child2         child3 |
       +--+------------------+----------------+--+
          |                  |                |
+---------------+    +---------------+    +---------------+
|    node1      |    |     node2     |    |     node3     |
| child1 child2 |    | child1 child2 |    | child1 child2 |
+--+---------+--+    +--+---------+--+    +--+---------+--+
   |         |          |         |          |         |

이것은 자연스러운 것처럼 보이지만 몇 가지 문제가 있습니다. 예를 들어 자식 노드의 수가 다양 할 경우 자식을 관리하려면 배열이나 목록과 같은 것이 필요합니다.

대신 (first) child 및 (next) 형제 포인터 만 사용하면 다음과 같은 것을 얻을 수 있습니다.

       +-------------------+
       |        root       |
       | child    sibling  +--->NULL
       +--+----------------+
          |             
+----------------+    +----------------+    +----------------+
|    node1       |    |     node2      |    |     node3      |
| child  sibling +--->| child  sibling +--->| child  sibling +--->NULL
+--+-------------+    +--+-------------+    +--+-------------+
   |                     |                     |

분명히 이런 종류의 구조는 나무를 나타낼 수도 있지만 몇 가지 장점을 제공합니다. 가장 중요한 것은 더 이상 자식 노드 수에 대해 걱정할 필요가 없다는 것입니다. 구문 분석 트리에 사용될 때 깊은 트리가되지 않고 "a + b + c + d + e"와 같은 용어를 자연스럽게 표현합니다.

컬렉션 라이브러리는 이와 같은 트리 구조를 제공합니까? 파서는 그러한 구조를 사용합니까? 그렇지 않다면 이유는 무엇입니까?


2
글쎄,이 구조는 분명히 더 높은 복잡성의 비용이 듭니다. 실제로 다양한 수의 자녀 가 필요한 경우에만 가치가 있습니다 . 많은 나무에는 디자인에 고유 한 고정 된 수의 자식 (또는 최소한 고정 된 최대 값)이 있습니다. 이러한 경우 추가 간접 지정은 아무런 가치를 추가하지 않습니다.
Joachim Sauer

4
링크 된 목록에 항목을 넣으면 O(n)알고리즘에 요인이됩니다.

그리고 루트에서 node3로 가려면 루트 의 cddar가져와야 합니다.
Tacroy

Tacroy : 루트에 올바른 찾는 다시 정확히 쉬운 일이 아닙니다,하지만 난 정말 그림 ;-) 망칠 것입니다 불구하고 (즉, 백 포인터가 될 것이다 생성하라 필요한 경우
user281377

답변:


7

트리는 목록과 같이 다른 방식으로 구현할 수있는 "추상 데이터 유형"입니다. 각 방법마다 장점과 단점이 있습니다.

첫 번째 예에서이 구조의 주요 장점은 O (1)의 모든 자식에 액세스 할 수 있다는 것입니다. 단점은 배열을 확장해야 할 때 자식을 추가하는 것이 때때로 더 비쌀 수 있다는 것입니다. 이 비용은 비교적 적습니다. 또한 가장 간단한 구현 중 하나입니다.

두 번째 예에서 주된 장점은 항상 O (1)에 자식을 추가한다는 것입니다. 가장 큰 단점은 아이에 대한 임의의 액세스 비용이 O (n)라는 것입니다. 또한 두 가지 이유로 거대한 트리의 경우 덜 흥미로울 있습니다 . 하나의 객체 헤더에 대한 메모리 오버 헤드와 노드 당 두 개의 포인터가 있으며 노드가 메모리에 무작위로 분산 되어 CPU 캐시와 CPU 캐시 사이에 많은 스왑 발생할 있습니다. 트리가 순회 할 때 메모리를 사용하여이 구현에 덜 매력적입니다. 그러나 일반 트리 및 응용 프로그램에는 문제가되지 않습니다.

언급되지 않은 마지막 흥미로운 가능성은 전체 트리를 단일 배열로 저장하는 것입니다. 이로 인해 코드가 더 복잡해 지지만 개체 헤더의 비용을 절약하고 연속 메모리를 할당 할 수 있기 때문에 특히 고정 된 고정 트리의 경우에는 특히 유리한 구현이됩니다.


1
예를 들어 : B + tree는이 "firstchild, nextsibling"구조를 사용하지 않습니다. 디스크 기반 트리의 경우 부조리하기에는 비효율적이며 메모리 기반 트리의 경우 여전히 비효율적입니다. 메모리 내 R- 트리는이 구조를 허용 할 수 있지만 여전히 더 많은 캐시 누락을 의미합니다. 나는 "처음, 다음 형제"가 더 나은 상황을 생각하기가 힘들다. 예, ammoQ가 언급했듯이 구문 트리에서 작동 할 수 있습니다. 다른 거있어?
Qwertie

3
"항상 O (1)에 자식을 추가합니다"-O (1)의 인덱스 0에 항상 자식을 삽입 할 수 있지만 자식을 추가하는 것은 분명히 O (n) 인 것 같습니다.
Scott Whitlock

전체 트리를 단일 배열로 저장하는 것은 힙에 일반적입니다.
Brian

1
@Scott : 음, I는 O (1)에 대한 중 첫 번째 또는 마지막 POS ...는 OPS 예에서 누락 하였지만가되는데, 연결리스트도 역시 마지막 항목에 대한 포인터 / 레퍼런스를 포함 상정
dagnelies을

나는 (아마도 제외하고 그 내기 것 극단적으로 구현이 없다 "로 nextSibling, firstchild"는 퇴화 경우) 결코 어레이 기반 자식 테이블 구현보다 더 효율적입니다. 캐시 로컬 리티가 승리합니다. B 트리는 현대적인 아키텍처에서 지금까지 가장 효율적인 구현으로 입증되었으며, 향상된 캐시 위치로 인해 전통적으로 사용 된 빨강-검정 트리에 대항하여 승리했습니다.
Konrad Rudolph

2

편집 가능한 모델 또는 문서가있는 거의 모든 프로젝트에는 계층 구조가 있습니다. '계층 노드'를 다른 엔티티의 기본 클래스로 구현하는 것이 편리 할 수 ​​있습니다. 종종 링크 된리스트 (자식 형제, 2 차 모델)는 많은 클래스 라이브러리가 성장하는 자연스러운 방법이지만, 아이들은 다양한 유형일 수 있으며 아마도 " 객체 모델 "은 일반적으로 나무에 대해 이야기 할 때 고려하지 않는 것입니다.

내가 가장 좋아하는 첫 번째 모델의 트리 (노드) 구현은 하나의 라이너 (C #)입니다.

public class node : List<node> { /* props go here */ }

자신의 형식의 일반 목록에서 상속하거나 다른 일반 형식의 컬렉션에서 상속합니다. 한 방향으로 걷기가 가능합니다. 뿌리를 아래쪽으로 내립니다 (항목은 부모를 모름).

부모 만의 나무

언급하지 않은 또 다른 모델은 모든 어린이가 부모에 대한 참조를 갖는 모델입니다.

               null
                 |
       +---------+---------------------------------+
       |       parent                              |
       | root                                      |
       +-------------------------------------------+
          |                   |                |
+---------+------+    +-------+--------+    +--+-------------+
|     parent     |    |     parent     |    |     parent     |
|     node 1     |    |     node 2     |    |     node 3     |
+----------------+    +----------------+    +----------------+

이 트리를 걷는 것은 다른 방법으로 만 가능합니다. 일반적으로 이러한 모든 노드는 컬렉션 (배열, 해시 테이블, 사전 등)에 저장되며 노드는 일반적으로 중요하지 않은 트리.

이러한 부모 전용 트리는 일반적으로 데이터베이스 응용 프로그램에서 볼 수 있습니다. "SELECT * WHERE ParentId = x"문으로 노드의 자식을 쉽게 찾을 수 있습니다. 그러나 우리는 이것들이 트리 노드 클래스 객체로 변형되는 것을 거의 찾지 않습니다. Statefull (데스크톱) 응용 프로그램에서는 기존 트리 노드 컨트롤에 래핑 될 수 있습니다. 상태 비 저장 (웹) 응용 프로그램에서도 가능하지 않을 수 있습니다. ORM 매핑 클래스 생성기 도구는 자신과 관계가있는 테이블에 대한 클래스를 생성 할 때 스택 오버플로 오류를 발생시키는 것을 보았습니다.

양방향 탐색 가능한 트리

그러나 대부분의 실제 상황에서는 두 세계를 모두 활용하는 것이 편리합니다. 자식 목록이 있고 또한 부모 인 부모 탐색 가능한 트리를 가진 노드가 있습니다.

                          null
                            |
       +--------------------+--------------------+
       |                  parent                 |
       |        root                             | 
       | child1            child2         child3 |
       +--+------------------+----------------+--+
          |                  |                |
+---------+-----+    +-------+-------+    +---+-----------+
|      parent   |    |     parent    |    |  parent       |
|    node1      |    |     node2     |    |     node3     |
| child1 child2 |    | child1 child2 |    | child1 child2 |
+--+---------+--+    +--+---------+--+    +--+---------+--+
   |         |          |         |          |         |

이것은 고려해야 할 더 많은 측면을 제공합니다.

  • 부모의 연결 및 연결 해제를 어디에서 구현해야합니까?
    • bussiness 논리를 처리하고 노드에서 측면을 제거하십시오 (그들은 잊어 버릴 것입니다!)
    • 노드에는 자식을 만드는 방법이 있습니다 (재주문을 허용하지 않음) (Systems.Xml.XmlDocument DOM 구현에서 Microsoft의 선택, 처음 만났을 때 거의 나를 미치게했습니다)
    • 노드는 생성자에서 부모를 취합니다 (재주문을 허용하지 않음)
    • 모든 add (), insert () 및 remove () 메소드 및 노드의 과부하 (일반적으로 내 선택)
  • 지속성
    • 지속될 때 나무를 걷는 방법 (예를 들어 부모 링크 제외)
    • 직렬화 해제 후 양방향 링크를 다시 작성하는 방법 (모든 부모를 직렬화 해제 후 조치로 다시 설정)
  • 알림
    • 정적 메커니즘 (IsDirty 플래그), 속성에서 재귀 적으로 처리합니까?
    • 이벤트, 부모를 통해 거품, 어린이를 통해 거품, 또는 두 가지 방법 (예 : Windows 메시지 펌프 고려)

이제 질문에 대답하기 위해 양방향 내비게이션 트리는 (내 경력과 분야에서) 가장 널리 사용되는 경향이 있습니다. 예를 들어 .Net 프레임 워크에서 System.Windows.Forms.Control 또는 System.Web.UI.Control을 Microsoft에서 구현 한 것이지만 모든 DOM (Document Object Model) 구현에는 부모뿐 아니라 열거도 알고있는 노드가 있습니다. 그들의 아이들의. 이유 : 구현 용이성에 비해 사용 편의성. 또한 이들은 일반적으로 더 구체적인 클래스 (XmlNode는 Tag, Attribute 및 Text 클래스의 기본 일 수 있음)의 기본 클래스이며 이러한 기본 클래스는 일반 직렬화 및 이벤트 처리 아키텍처를 배치 할 수있는 자연스러운 장소입니다.

Tree는 많은 아키텍처의 중심에 위치하고 자유롭게 탐색 할 수 있다는 것은 솔루션을 더 빠르게 구현할 수 있다는 것을 의미합니다.


1

두 번째 사례를 직접 지원하는 컨테이너 라이브러리를 모르지만 대부분의 컨테이너 라이브러리는 해당 시나리오를 쉽게 지원할 수 있습니다. 예를 들어 C ++에서는 다음을 가질 수 있습니다.

class Node;  // forward reference to satisfy the compiler
typedef std::list<Node*> NodeList;
class Node : public NodeList { /* . . . */ };  // a node is also a list

Node* n = new Node;
n->push_back(new Node);
Node* tree = new Node;
tree->push_back(new Node);
tree->push_back(n);

파서는 변수 항목 수가 많고 자식이있는 노드를 효율적으로 지원하기 때문에 이와 유사한 구조를 사용합니다. 나는 보통 그들의 소스 코드를 읽지 않기 때문에 확실하지 않습니다.


1

하위 배열을 갖는 것이 바람직한 경우 중 하나는 하위에 무작위로 액세스해야하는 경우입니다. 그리고 이것은 보통 아이들이 분류 될 때입니다. 예를 들어, 파일과 같은 계층 구조 트리는 더 빠른 경로 검색을 위해 이것을 사용할 수 있습니다. 또는 인덱스 액세스가 매우 자연스러운 DOM 태그 트리

또 다른 예는 모든 어린이에게 "포인터"를 사용하면 더 편리하게 사용할 수있는 경우입니다. 예를 들어, 관계형 데이터베이스로 트리 관계를 구현할 때 설명한 두 유형을 모두 사용할 수 있습니다. 그러나 전자 (이 경우 부모에서 자식으로의 마스터 세부 정보)는 유용한 SQL에 대한 일반 SQL 쿼리를 허용하지만 후자는 크게 제한합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.