20. tree traversal과 parsing

Posted by aliontory on June 11, 2024 · 5 mins read

2024-06-11-20. Tree Traversal과 Parsing

Traversal (순회)

특정한 순서에 따라 모든 노드를 방문(visit)하는 것을 traversal이라 한다.
어떤 노드에 “방문”한다는 것은, 그 노드에 “있다”는 뜻이 아니다. 해당 노드에 어떠한 작업을 한다는 뜻이다.

이진 트리에서 순회를 하는 코드 Traverse는 다음과 같다.
Traverse(Node *D) {
if(D == NULL) return;
Visit(D); // Preorder Traversal
Traverse(D->Left);
Visit(D); // Inorder Traversal
Traverse(D->Right);
Visit(D); // Postorder Traversal
}
특정 노드 D에 대해 Traverse가 수행되면, D의 왼쪽 서브트리에 대해 Traverse가 수행된 뒤 오른쪽 서브트리에 대해 Traverse가 수행된다.
여기서 노드 D에 작업 Visit()를 언제 하느냐에 따라 Preorder, Inorder, Postorder Traversal로 나뉜다.
  • Preorder Traversal : 왼쪽 서브트리를 방문하기 전에 해당 노드를 방문
  • Inorder Traversal : 왼쪽 서브트리를 방문한 뒤, 오른쪽 서브트리를 방문하기 전 해당 노드를 방문
  • Postorder Traversal : 오른쪽 서브트리를 방문한 뒤 해당 노드를 방문

물론 위 세 시점 중 여러 시점에 중복해서 방문할 수도 있다.



Visit에서 트리의 키 값을 출력한다고 할 때, Preorder, Inorder, Postorder Traversal에 따른 추력 순서는 다음과 같다.
  • Preorder : 6, 4, 2, 1, 35, 8, 7, 9
  • Inorder : 1, 2, 3, 4, 567, 8, 9
  • Postorder : 1, 3, 2, 5, 47, 9, 86


Tree Traversal에 관련된 문제

노드 키값들의 배열이 두 개 주어진다고 하자.
첫 번째 배열은 어떤 이진 트리를 Preorder로 출력한 결과이고, 두 번째 배열은 어떤 이진 트리를 Inorder로 출력한 결과라고 가정할 때, 해당 배열이 똑같은 이진 트리(이진 탐색 트리 아님)로부터 만들어질 수 있는지를 어떻게 판별할 수 있을까?

하나의 이진 트리로부터 주어진 Preorder, Inorder 배열이 만들어진다면 true, 그렇지 않으면 false를 리턴하는 판별 알고리즘은 다음과 같다.
  1. Preorder 배열에서 루트 노드를 찾는다. 첫 번째 원소가 루트 노드이다.
  2. Inorder 배열에서 해당 루트 노드를 찾는다. 만약 찾지 못하면 false를 리턴한다.
  3. Inorder 배열의 k번째 원소가 루트 노드라면, 아래와 같이 왼쪽, 오른쪽 서브트리에 대해 재귀 수행한다.
    • Preorder의 1번째 원소 다음 k-1개 원소, Inorder의 왼쪽 k-1개 원소에 대해 재귀 수행
    • 루트와 왼쪽 서브트리 원소들을 제외한, Preorder와 Inorder의 나머지 원소들에 대해 재귀 수행
    • 다음과 같은 예제에서
      • Preorder : 6, 4, 2, 1, 35, 8, 7, 9
      • Inorder : 1, 2, 3, 4, 567, 8, 9
    • 초록색으로 표시된 왼쪽 서브트리와 파란색으로 표시된 오른쪽 서브트리에 대해 재귀 수행된다.
    • 재귀 수행 시 Preorder의 남은 원소 개수가 부족해지면 false 리턴
  4. 재귀 수행이 둘 다 true를 리턴하면 true 리턴


컴파일러에서 Tree의 응용

컴파일러는 코드의 표현식을 기계어로 변환해야 한다.
프로그래머가 작성한 표현식은 기계어로 변환되기 전 Tree 등의 중간 형태로 변환된다.
예를 들어, 표현식 (6/3 + 5) * (6 - 4) 는 아래와 같은 표현식 트리로 변환될 수 있다.

해당 트리에 postorder traversal을 수행하면 연산 결과가 계산된다.


Parsing

컴파일러가 고급 프로그래밍 언어를 기계어에 더 가까운 언어로 변환하는 것을 Parsing이라고 한다. 중위 표현식을 트리로 변환하는 것도 parsing의 과정에 포함된다.

트리로 변환하는 알고리즘을 설명하기 이전에 다음과 같은 가정을 하겠다.

  • 가정
    • 연산자마다 우선순위가 있다.
    • 우선순위가 같은 연산자는 왼쪽에 있는 것이 먼저 계산된다고 하자.
    • 중위 표현식의 처음과 끝을 $로 표시한다.
      • ex) $ a + b * c $
      • $는 우선순위가 가장 낮은 연산자이다.

parsing 알고리즘에서는 두 개의 스택이 사용된다. 1번 스택은 연산자를 저장하는 스택이며, 2번 스택은 피연산자나 표현식 트리를 저장하는 스택이다.
1번 스택에서 연산자 노드가 pop될 때마다 2번 스택의 노드 2개가 pop되어 자식으로 들어간다. 여기서 표현식 트리가 형성되며, 해당 트리에 후위 순회를 함으로써 연산 결과값이 산출된다. 따라서 해당 트리를 하나의 연산 결과값으로 해석할 수 있다.

(위 그림과 같은 트리를 6 / 3 + 5 의 결과값인 7로 해석할 수 있음)
그러므로 트리를 만드는 과정은 1번 스택의 연산자와 2번 스택의 값에 대해 연산을 수행하는 것으로 볼 수 있다.

여기서 중요한 것은 1번 스택에서 우선순위가 더 높은 연산자, 수식에서 더 왼쪽에 있는 연산자가 먼저 pop되도록 하는 것, 즉 먼저 연산되도록 하는 것이다.

구체적인 알고리즘은 다음과 같다.

  • 두 개의 스택을 준비한다.
  • while (수식을 처음부터 끝까지 읽는다)
    • 피연산자를 만나면 노드로 만들어 2번 스택에 push 한다.
    • 연산자를 만나면 1번 스택의 맨 위에 있는 노드와 비교한다.
      • 현재 연산자가 우선순위가 더 높으면 노드로 만들어 1번 스택에 push 한다.
      • 현재 연산자가 우선순위가 같거나 더 낮으면
        • 1번 스택에서 연산자 노드 하나를 pop한다.
        • 2번 스택에서 두 개의 노드를 꺼내, pop된 연산자 노드의 자식으로 연결한다.
        • 트리가 형성되었다. 해당 트리는 표현식을 나타낸다. 루트 노드는 1번 스택에서 꺼낸 연산자이다.
        • 루트 노드를 2번 스택에 push한다.
      • 단, 현재 연산자가 $이고 1번 스택의 top이 $인 경우, while 루프를 종료한다.


괄호가 있는 경우 Parsing

괄호는 괄호 바깥쪽에 있는 모든 연산자보다 우선순위가 높고, 괄호 안쪽의 모든 연산자보다 우선순위가 낮은 연산자이다. 이러한 괄호의 특성은 parsing에서 다음 규칙으로 반영된다.

  • 여는 괄호 ‘(’ 가 중위 표현식에서 읽혀지는 경우, 우선순위가 가장 높다.
  • ‘(’ 가 1번 stack에서 읽혀지는 경우, 우선순위가 가장 낮다.
  • 닫는 괄호 ‘)’는 우선순위가 가장 낮다.
  • ‘)’는 1번 stack에 들어가는 일이 없다. 단지 ‘)’가 읽혀질 때 1번 스택의 top이 ‘(’인 경우, 두 연산자가 만나 그냥 사라진다.

괄호는 중위 표현식 전체의 시작과 끝을 표현하는 데 $대신 사용될 수도 있다.