중위 순회(Inorder Traversal)

중위 순회(Inorder Traversal)는 트리 자료구조, 특히 이진 트리를 탐색하는 세 가지 기본적인 방법(전위, 중위, 후위) 중 하나이다. 이 순회 방식은 특유의 방문 순서 때문에 특별한 의미와 활용 가치를 지니고 있다.

왼쪽 서브트리를 먼저 방문한 후 현재 노드를 방문하고 마지막으로 오른쪽 서브트리를 방문하는 이 방법은 정렬된 데이터가 필요한 다양한 문제에 활용된다.

이진 검색 트리에서 중위 순회를 수행하면 노드 값이 오름차순으로 방문되는 특성은 검색, 삽입, 삭제, 범위 쿼리 등 많은 작업에서 핵심적인 역할을 한다. 또한 표현식 트리에서 중위 표기법을 생성하거나 트리의 유효성을 검사하는 데에도 널리 사용된다.

재귀적 구현이 가장 간단하고 직관적이지만, 스택을 사용한 반복적 구현이나 모리스 순회와 같은 최적화된 알고리즘을 통해 성능과 공간 효율성을 개선할 수 있다.

중위 순회의 기본 개념

중위 순회는 이진 트리를 탐색할 때 다음과 같은 순서로 노드를 방문하는 방법이다:

  1. 왼쪽 서브트리를 중위 순회한다.
  2. 현재 노드(루트)를 방문한다.
  3. 오른쪽 서브트리를 중위 순회한다.

“중위(Inorder)“라는 이름은 현재 노드가 왼쪽과 오른쪽 서브트리 “사이(in order)“에 방문된다는 의미를 담고 있다. 이 순회 방식은 깊이 우선 탐색(DFS)의 한 형태로, 트리의 가장 왼쪽 노드부터 시작하여 점차 오른쪽으로 이동하는 특성을 가진다.

중위 순회의 알고리즘

아래 이진 트리를 예로 들어 중위 순회 과정을 단계별로 살펴보면:

1
2
3
4
5
       1
     /   \
    2     3
   / \   / \
  4   5 6   7

중위 순회 순서: 4 → 2 → 5 → 1 → 6 → 3 → 7

이 순서를 단계별로 설명하면:

  1. 가장 왼쪽 경로를 따라 내려가 노드 4에 도달한다.
  2. 노드 4를 방문한다 (왼쪽 자식이 없음).
  3. 노드 4의 부모인 노드 2로 돌아가 방문한다.
  4. 노드 2의 오른쪽 자식인 노드 5를 방문한다.
  5. 노드 5의 부모의 부모인 노드 1로 돌아가 방문한다.
  6. 노드 1의 오른쪽 서브트리로 이동하여 노드 6, 3, 7을 순서대로 방문한다.
1
2
3
4
5
        A
       / \
      B   C
     / \   \
    D   E   F

중위 순회(Inorder Traversal)의 과정은 다음과 같다.

  1. D 방문 (B의 왼쪽 자식) → 출력: D
  2. B 방문 (B의 루트) → 출력: D B
  3. E 방문 (B의 오른쪽 자식) → 출력: D B E
  4. A 방문 (A의 루트) → 출력: D B E A
  5. C 방문 (A의 오른쪽 자식) → 출력: D B E A C
  6. F 방문 (C의 오른쪽 자식) → 출력: D B E A C F

재귀적 구현

중위 순회의 재귀적 구현은 직관적이고 간결하다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Node:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

def inorder_traversal(node):
    if node is None:
        return
    
    # 왼쪽 서브트리 탐색
    inorder_traversal(node.left)
    
    # 현재 노드 방문
    print(node.value, end=" ")
    
    # 오른쪽 서브트리 탐색
    inorder_traversal(node.right)

# 트리 생성
root = Node('A')
root.left = Node('B')
root.right = Node('C')
root.left.left = Node('D')
root.left.right = Node('E')
root.right.right = Node('F')

# 중위 순회 실행
inorder_traversal(root)

실행결과

1
D B E A C F

반복적(비재귀적) 구현

스택을 사용한 반복적 구현도 가능하다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def inorder_traversal_iterative(root):
    stack = []
    current = root
    
    while True:
        if current is not None:
            stack.append(current)
            current = current.left  # 왼쪽 서브트리 탐색
        elif stack:
            current = stack.pop()
            print(current.value, end=" ")  # 현재 노드 방문
            current = current.right  # 오른쪽 서브트리 탐색
        else:
            break

# 실행
inorder_traversal_iterative(root)

실행 결과

1
D B E A C F

이 알고리즘은 먼저 현재 노드부터 시작하여 왼쪽 자식 노드를 따라 내려가며 모든 노드를 스택에 추가한다.
그런 다음 스택에서 노드를 꺼내서 방문하고, 오른쪽 자식으로 이동하여 프로세스를 반복한다.

다양한 트리 구조에서의 중위 순회

  1. 이진 검색 트리(Binary Search Tree)
    이진 검색 트리는 각 노드에 대해 왼쪽 서브트리의 모든 노드 값이 현재 노드보다 작고, 오른쪽 서브트리의 모든 노드 값이 현재 노드보다 큰 특성을 가진 이진 트리이다.

    1
    2
    3
    4
    5
    6
    7
    
           8
         /   \
        3    10
       / \     \
      1   6    14
         / \   /
        4   7 13
    

    중위 순회 순서: 1 → 3 → 4 → 6 → 7 → 8 → 10 → 13 → 14

    이진 검색 트리에서 중위 순회를 수행하면 노드 값이 정렬된 순서로 방문된다. 이 특성은 정렬된 배열이 필요한 많은 알고리즘에서 활용된다.

  2. 편향 트리(Skewed Tree)
    한쪽으로만 자식 노드가 있는 트리이다.

    왼쪽 편향 트리:

    1
    2
    3
    4
    5
    6
    7
    
      1
     /
    2
    /
    3
    /
    4
    

    중위 순회 순서: 4 → 3 → 2 → 1

    오른쪽 편향 트리:

    1
    2
    3
    4
    5
    6
    7
    
    1
     \
      2
       \
        3
         \
          4
    

    중위 순회 순서: 1 → 2 → 3 → 4

  3. 완전 이진 트리(Complete Binary Tree)
    완전 이진 트리는 마지막 레벨을 제외한 모든 레벨이 완전히 채워져 있고, 마지막 레벨의 노드들은 왼쪽부터 채워진 트리이다.

    1
    2
    3
    4
    5
    
           1
         /   \
        2     3
       / \   / 
      4   5 6   
    

    중위 순회 순서: 4 → 2 → 5 → 1 → 6 → 3

중위 순회의 활용 사례

  1. 이진 검색 트리 검증
    이진 검색 트리의 유효성을 검사할 때 중위 순회가 유용하다.
    중위 순회 결과가 오름차순으로 정렬되어 있다면 유효한 이진 검색 트리이다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    def is_valid_bst(root):
        prev = [float('-inf')]  # 이전 값을 추적하기 위한 리스트
    
        def inorder(node):
            if not node:
                return True
    
            # 왼쪽 서브트리 검사
            if not inorder(node.left):
                return False
    
            # 현재 노드 값 검사
            if node.value <= prev[0]:
                return False
            prev[0] = node.value
    
            # 오른쪽 서브트리 검사
            return inorder(node.right)
    
        return inorder(root)
    
  2. 중위 표기법 생성
    표현식 트리에서 중위 순회를 수행하면 중위 표기법(infix notation)의 수식을 얻을 수 있다.
    중위 표기법은 우리가 일상적으로 사용하는 수식 표기법이다(예: a + b * c).

    1
    2
    3
    4
    5
    
        +
       / \
      a   *
         / \
        b   c
    

    중위 순회 결과: a + b * c

  3. 이진 검색 트리에서의 검색, 삽입, 삭제
    이진 검색 트리에서 중위 순회는 정렬된 데이터를 얻는 데 사용된다.
    이는 범위 쿼리(range query)나 정렬된 데이터가 필요한 작업에 유용하다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    def kth_smallest(root, k):
        """이진 검색 트리에서 k번째로 작은 요소를 찾습니다."""
        count = [0]
        result = [None]
    
        def inorder(node):
            if not node or count[0] >= k:
                return
    
            inorder(node.left)
    
            count[0] += 1
            if count[0] == k:
                result[0] = node.value
                return
    
            inorder(node.right)
    
        inorder(root)
        return result[0]
    
  4. 모리스 중위 순회(Morris Inorder Traversal)
    모리스 순회는 추가 공간을 사용하지 않고(O(1) 공간 복잡도) 트리를 순회하는 고급 기법이다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    def morris_inorder_traversal(root):
        result = []
        current = root
    
        while current:
            if not current.left:
                # 왼쪽 자식이 없으면 현재 노드를 방문하고 오른쪽으로 이동
                result.append(current.value)
                current = current.right
            else:
                # 중위 순회에서 현재 노드의 선행자(왼쪽 서브트리에서 가장 오른쪽 노드) 찾기
                predecessor = current.left
                while predecessor.right and predecessor.right != current:
                    predecessor = predecessor.right
    
                if not predecessor.right:
                    # 선행자의 오른쪽이 없으면 현재 노드로 연결하고 왼쪽으로 이동
                    predecessor.right = current
                    current = current.left
                else:
                    # 선행자의 오른쪽이 현재 노드를 가리키면 그 연결을 끊고 현재 노드를 방문한 후 오른쪽으로 이동
                    predecessor.right = None
                    result.append(current.value)
                    current = current.right
    
        return result
    

    이 알고리즘은 트리의 구조를 일시적으로 수정하여 스택이나 재귀 없이 순회를 가능하게 한다.

중위 순회의 시간 및 공간 복잡도

  1. 시간 복잡도
    중위 순회는 트리의 모든 노드를 한 번씩 방문하므로 시간 복잡도는 O(n)이다. 여기서 n은 트리의 노드 수이다.

  2. 공간 복잡도

    • 재귀적 구현: 재귀 호출 스택의 크기는 트리의 높이 h에 비례하므로 공간 복잡도는 O(h)이다. 최악의 경우(편향 트리)에는 O(n)이 될 수 있다.
    • 반복적 구현: 명시적 스택을 사용하는 경우에도 공간 복잡도는 O(h)이다. 마찬가지로 최악의 경우에는 O(n)이 될 수 있다.
    • 모리스 순회: 추가 공간을 사용하지 않으므로 공간 복잡도는 O(1)이다.

중위 순회와 다른 순회 방법 비교

  1. 중위 순회 vs 전위 순회(Preorder Traversal)

    • 중위 순회: 왼쪽 → 루트 → 오른쪽
    • 전위 순회: 루트 → 왼쪽 → 오른쪽
      전위 순회는 루트 노드를 먼저 방문하므로 트리의 복제나 전위 표기법 생성에 유용하다.
  2. 중위 순회 vs 후위 순회(Postorder Traversal)

    • 중위 순회: 왼쪽 → 루트 → 오른쪽
    • 후위 순회: 왼쪽 → 오른쪽 → 루트
      후위 순회는 자식 노드를 먼저 방문한 후 부모 노드를 방문하므로 트리 삭제나 후위 표기법 생성에 유용하다.
  3. 중위 순회 vs 레벨 순회(Level Order Traversal)

    • 중위 순회: 깊이 우선(DFS) 방식으로 왼쪽 가지를 먼저 완전히 탐색한다.
    • 레벨 순회: 너비 우선(BFS) 방식으로 각 레벨의 노드를 순서대로 방문한다.
      레벨 순회는 트리의 레벨별 특성을 분석하거나 최단 경로 문제에 유용합니다.

실제 코드 예제: 이진 트리의 중위 순회 구현

아래는 Python으로 이진 트리 노드 클래스와 중위 순회 메서드를 구현한 예제:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class TreeNode:
    def __init__(self, value=0, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

def inorder_traversal_recursive(root):
    result = []
    
    def dfs(node):
        if not node:
            return
        
        # 왼쪽 서브트리 순회
        dfs(node.left)
        
        # 현재 노드 방문
        result.append(node.value)
        
        # 오른쪽 서브트리 순회
        dfs(node.right)
    
    dfs(root)
    return result

def inorder_traversal_iterative(root):
    result = []
    stack = []
    current = root
    
    while current or stack:
        # 왼쪽 경로의 모든 노드를 스택에 추가
        while current:
            stack.append(current)
            current = current.left
        
        # 스택에서 노드를 꺼내서 방문
        current = stack.pop()
        result.append(current.value)
        
        # 오른쪽 자식으로 이동
        current = current.right
    
    return result

# 다음과 같은 이진 트리 생성:
#       1
#     /   \
#    2     3
#   / \   / \
#  4   5 6   7
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
root.right.left = TreeNode(6)
root.right.right = TreeNode(7)

# 재귀적 방법으로 중위 순회
print("재귀적 중위 순회:", inorder_traversal_recursive(root))  # [4, 2, 5, 1, 6, 3, 7]

# 반복적 방법으로 중위 순회
print("반복적 중위 순회:", inorder_traversal_iterative(root))  # [4, 2, 5, 1, 6, 3, 7]

중위 순회의 응용 문제

  1. 이진 검색 트리의 두 노드 값 교환 복구
    이진 검색 트리에서 두 노드의 값이 실수로 교환되었을 때, 트리를 복구하는 문제이다.
    중위 순회를 사용하면 값이 정렬되지 않은 노드를 찾을 수 있다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    def recover_bst(root):
        first = second = prev = None
    
        def inorder(node):
            nonlocal first, second, prev
    
            if not node:
                return
    
            # 왼쪽 서브트리 순회
            inorder(node.left)
    
            # 현재 노드 검사
            if prev and prev.value > node.value:
                if not first:
                    first = prev
                second = node
    
            prev = node
    
            # 오른쪽 서브트리 순회
            inorder(node.right)
    
        inorder(root)
    
        # 발견한 두 노드의 값 교환
        first.value, second.value = second.value, first.value
    
  2. 중위 순회 결과로부터 트리 재구성
    중위 순회의 결과와 전위 순회의 결과가 주어졌을 때, 원래 이진 트리를 재구성하는 문제이다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    def build_tree(inorder, preorder):
        if not inorder or not preorder:
            return None
    
        # 전위 순회의 첫 번째 요소는 루트
        root_value = preorder[0]
        root = TreeNode(root_value)
    
        # 중위 순회에서 루트의 위치 찾기
        root_index = inorder.index(root_value)
    
        # 왼쪽 서브트리와 오른쪽 서브트리로 분할
        root.left = build_tree(inorder[:root_index], preorder[1:1+root_index])
        root.right = build_tree(inorder[root_index+1:], preorder[1+root_index:])
    
        return root
    

중위 순회의 고급 응용

  1. 중위 순회를 사용한 이진 트리 균형 검사
    이진 검색 트리가 균형 잡혀 있는지 검사하는 방법 중 하나는 중위 순회를 사용하는 것이다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    def is_balanced(root):
        balanced = [True]
    
        def get_height(node):
            if not node or not balanced[0]:
                return 0
    
            left_height = get_height(node.left)
            right_height = get_height(node.right)
    
            # 왼쪽과 오른쪽 서브트리의 높이 차이가 1보다 크면 균형이 맞지 않음
            if abs(left_height - right_height) > 1:
                balanced[0] = False
    
            return max(left_height, right_height) + 1
    
        get_height(root)
        return balanced[0]
    
  2. 중위 순회를 사용한 스레드 이진 트리(Threaded Binary Tree) 구현
    스레드 이진 트리는 null 포인터를 사용하는 대신 중위 순회에서 선행자나 후속자를 가리키는 “스레드"를 사용하여 공간을 절약하는 특수한 이진 트리이다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    
    class ThreadedTreeNode:
        def __init__(self, value=0):
            self.value = value
            self.left = None
            self.right = None
            self.is_threaded_right = False  # 오른쪽 포인터가 스레드인지 여부
    
    def create_inorder_threaded_tree(root):
        if not root:
            return
    
        prev = [None]
    
        def inorder(node):
            if not node:
                return
    
            # 왼쪽 서브트리 순회
            inorder(node.left)
    
            # 이전 노드의 오른쪽이 없으면 현재 노드를 가리키는 스레드 생성
            if prev[0] and not prev[0].right:
                prev[0].right = node
                prev[0].is_threaded_right = True
    
            prev[0] = node
    
            # 오른쪽 서브트리 순회
            if not node.is_threaded_right:
                inorder(node.right)
    
        inorder(root)
    

트리 순회의 실제 응용 사례

  1. 컴파일러에서의 표현식 파싱
    컴파일러에서 중위 표기법으로 작성된 표현식을 파싱할 때 중위 순회가 사용된다.
    표현식은 구문 분석 트리(parse tree)로 변환된 후, 중위 순회를 통해 원래 표현식을 재구성하거나 다른 표기법으로 변환할 수 있다.

  2. 데이터베이스 인덱싱
    B-트리나 B+ 트리와 같은 자기 균형 이진 검색 트리는 데이터베이스 시스템의 인덱싱에 널리 사용된다.
    이러한 트리의 중위 순회는 정렬된 순서로 레코드를 검색하는 데 사용된다.

  3. 기계 학습에서의 결정 트리
    결정 트리 알고리즘에서 트리 구조를 시각화하거나 규칙을 추출할 때 중위 순회가 사용될 수 있다.

중위 순회의 변형

  1. 역중위 순회(Reverse Inorder Traversal)
    역중위 순회는 오른쪽 서브트리를 먼저 방문한 후 현재 노드를 방문하고 마지막으로 왼쪽 서브트리를 방문한다. 이진 검색 트리에서 역중위 순회를 수행하면 노드 값이 내림차순으로 방문된다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    def reverse_inorder_traversal(root):
        result = []
    
        def dfs(node):
            if not node:
                return
    
            # 오른쪽 서브트리 순회
            dfs(node.right)
    
            # 현재 노드 방문
            result.append(node.value)
    
            # 왼쪽 서브트리 순회
            dfs(node.left)
    
        dfs(root)
        return result
    
  2. 대칭 중위 순회(Symmetric Inorder Traversal)
    대칭 중위 순회는 트리의 대칭성을 검사하는 데 사용된다. 왼쪽 서브트리는 중위 순회로, 오른쪽 서브트리는 역중위 순회로 방문하여 결과를 비교한다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    def is_symmetric(root):
        if not root:
            return True
    
        def is_mirror(left, right):
            if not left and not right:
                return True
            if not left or not right:
                return False
    
            return (left.value == right.value and
                    is_mirror(left.left, right.right) and
                    is_mirror(left.right, right.left))
    
        return is_mirror(root.left, root.right)
    

중위 순회를 사용한 실제 문제 해결 예시

이진 검색 트리의 범위 합(Range Sum)

주어진 범위 [low, high] 내에 있는 이진 검색 트리의 모든 노드 값의 합을 구하는 문제.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def range_sum_bst(root, low, high):
    result = [0]
    
    def inorder(node):
        if not node:
            return
        
        # 현재 노드 값이 low보다 크거나 같으면 왼쪽 서브트리 탐색
        if node.value >= low:
            inorder(node.left)
        
        # 현재 노드 값이 범위 내에 있으면 결과에 추가
        if low <= node.value <= high:
            result[0] += node.value
        
        # 현재 노드 값이 high보다 작거나 같으면 오른쪽 서브트리 탐색
        if node.value <= high:
            inorder(node.right)
    
    inorder(root)
    return result[0]

이 문제는 중위 순회의 특성을 활용하여 효율적으로 해결할 수 있다.
불필요한 서브트리 탐색을 피하기 위해 현재 노드 값을 범위와 비교하여 조건부로 탐색한다.


참고 및 출처