출시·고도화 중
트리와 힙 안내서 · 2/6
이 장에서는 작은 예제를 손으로 따라가며 트리 순회, 이진 탐색 트리의 삽입 · 탐색 · 삭제, 힙의 sift up · sift down과 heapify가 어떻게 움직이는지 살펴봅니다. 예제 트리는 키 50, 30, 70, 20, 40, 60, 80을 이 순서대로 BST에 넣어 만든 것입니다.
50
/ \
30 70
/ \ / \
20 40 60 80순회는 모든 노드를 한 번씩 방문하는 방법입니다. 깊이 우선 순회 세 가지는 "자신을 언제 방문하느냐"만 다릅니다.
| 순회 | 방문 순서 | 예제 결과 |
|---|---|---|
| 전위(pre-order) | 자신, 왼쪽, 오른쪽 | 50 30 20 40 70 60 80 |
| 중위(in-order) | 왼쪽, 자신, 오른쪽 | 20 30 40 50 60 70 80 |
| 후위(post-order) | 왼쪽, 오른쪽, 자신 | 20 40 30 60 80 70 50 |
| 레벨(level-order) | 깊이 순서, 같은 깊이는 왼쪽부터 | 50 30 70 20 40 60 80 |
전위 순회는 트리를 복사하거나 직렬화할 때, 중위 순회는 BST를 정렬 순서로 읽을 때, 후위 순회는 자식 결과를 모아 부모를 계산할 때(폴더 크기 합, 트리 삭제) 씁니다. 레벨 순회는 큐를 쓰는 너비 우선 탐색(BFS)입니다.
from collections import deque
class Node:
def __init__(self, key):
self.key, self.left, self.right = key, None, None
def insert(root, key):
if root is None:
return Node(key)
if key < root.key:
root.left = insert(root.left, key)
elif key > root.key:
root.right = insert(root.right, key)
return root
def preorder(n):
return [n.key] + preorder(n.left) + preorder(n.right) if n else []
def inorder(n):
return inorder(n.left) + [n.key] + inorder(n.right) if n else []
def postorder(n):
return postorder(n.left) + postorder(n.right) + [n.key] if n else []
def level_order(root):
out, queue = [], deque([root] if root else [])
while queue:
n = queue.popleft()
out.append(n.key)
if n.left:
queue.append(n.left)
if n.right:
queue.append(n.right)
return out
root = None
for k in [50, 30, 70, 20, 40, 60, 80]:
root = insert(root, k)
print(preorder(root), inorder(root), postorder(root), level_order(root), sep="\n")60을 찾는 과정은 다음과 같습니다. 50과 비교해 크므로 오른쪽(70)으로, 70보다 작으므로 왼쪽(60)으로 가서 찾습니다. 비교 세 번이면 됩니다. 65를 찾으면 60의 오른쪽 자식이 비어 있는 곳에서 멈추고 "없음"을 돌려줍니다. 삽입은 바로 그 빈자리에 새 노드를 붙이는 것입니다. 즉 65를 넣으면 60의 오른쪽 자식이 됩니다.
| 단계 | 현재 노드 | 비교 | 다음 |
|---|---|---|---|
| 1 | 50 | 65 큼 | 오른쪽 70 |
| 2 | 70 | 65 작음 | 왼쪽 60 |
| 3 | 60 | 65 큼 | 오른쪽 없음, 여기에 삽입 |
삭제는 지울 노드의 자식 수에 따라 나뉩니다.
def delete(root, key):
if root is None:
return None
if key < root.key:
root.left = delete(root.left, key)
elif key > root.key:
root.right = delete(root.right, key)
else:
if root.left is None:
return root.right
if root.right is None:
return root.left
succ = root.right
while succ.left:
succ = succ.left
root.key = succ.key
root.right = delete(root.right, succ.key)
return root
root = delete(root, 30)
print(inorder(root), preorder(root))
# [20, 40, 50, 60, 70, 80] [50, 40, 20, 70, 60, 80]최소 힙 [1, 3, 2, 7, 4, 5]에 0을 넣어 봅니다. 완전 이진 트리 모양을 지키려고 먼저 배열 끝(인덱스 6)에 붙인 뒤, 부모보다 작은 동안 부모와 자리를 바꿉니다.
| 단계 | 배열 | 설명 |
|---|---|---|
| 0 | 1 3 2 7 4 5 0 | 끝에 0 추가(인덱스 6, 부모 인덱스 2) |
| 1 | 1 3 0 7 4 5 2 | 0 과 2 교환(인덱스 2, 부모 인덱스 0) |
| 2 | 0 3 1 7 4 5 2 | 0 과 1 교환, 루트 도착 |
루트(0)를 꺼낸 다음, 마지막 원소(2)를 루트로 옮기고 더 작은 자식과 바꾸며 내려갑니다.
| 단계 | 배열 | 설명 |
|---|---|---|
| 0 | 2 3 1 7 4 5 | 마지막 원소 2를 루트로 |
| 1 | 1 3 2 7 4 5 | 자식 3, 1 중 작은 1과 교환 |
| 2 | 1 3 2 7 4 5 | 인덱스 2의 자식은 5뿐이고 2보다 크므로 멈춤 |
원소 n개를 하나씩 넣으면 O(n log n)이 들지만, 마지막 내부 노드(인덱스 n // 2 - 1)부터 루트까지 거꾸로 sift down을 하면 O(n)에 힙이 됩니다. 대부분의 노드는 잎 근처에 있어 조금만 내려가면 되기 때문입니다.
def sift_down(a, i, n):
while True:
small, l, r = i, 2 * i + 1, 2 * i + 2
if l < n and a[l] < a[small]:
small = l
if r < n and a[r] < a[small]:
small = r
if small == i:
return
a[i], a[small] = a[small], a[i]
i = small
def heapify(a):
for i in range(len(a) // 2 - 1, -1, -1):
sift_down(a, i, len(a))
print(f"i={i}: {a}")
heapify([9, 5, 7, 1, 3, 2])
# i=2: [9, 5, 2, 1, 3, 7]
# i=1: [9, 1, 2, 5, 3, 7]
# i=0: [1, 3, 2, 5, 9, 7]깊이 우선 순회 세 가지는 자신을 방문하는 시점만 다르고, 레벨 순회는 큐로 구현합니다. BST는 비교 한 번마다 한쪽으로 내려가 탐색 · 삽입하며, 자식이 둘인 노드는 중위 후속자로 바꿔 지웁니다. 힙은 끝에 붙이고 위로 올리는 sift up, 루트를 바꾸고 아래로 내리는 sift down 두 동작만으로 우선순위 큐를 유지하며, heapify는 아래쪽부터 sift down을 해 O(n)에 힙을 만듭니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.