✅ 백준 풀기 | 1110 더하기 사이클, 1182 부분수열의 합
✅ Do it! ~ 읽고 구현 연습 #자료구조 | 이진 검색 트리
☑️ 정답 봤던거 직접 코딩해보기 | 사냥꾼, 하노이탑, nQueen
☑️ Do it! ~ 읽고 구현 연습 #자료구조 | 큐, 스택
☑️ Do it! ~ 읽고 구현 연습 #정렬 | 퀵 정렬(피봇 3선발), 병합 정렬, 힙 정렬
☑️ 슈도 코드 변환 | 버블 정렬, 이분 탐색, 백트래킹
딕셔너리에 tree[root] = left, right 이렇게 하면 어떻게 입력되는거임
딕셔너리에 tree[root] = left, right처럼 입력하면, tree 딕셔너리에서 root 키는 튜플 (left, right)를 값으로 가지게 됩니다. 이는 각 노드가 left와 right 자식을 가지는 트리 구조를 나타내기 위한 방법입니다.
left: Node = None 매개변수는 Node 타입의 값을 받으며, 기본값은 None입니다. 즉, left 매개변수는 노드의 왼쪽 자식을 나타내며, 초기화 시에 명시적으로 제공되지 않으면 None으로 설정됩니다.
이진 검색 트리에서 key랑 value랑 뭐가 다름
이진 검색 트리(BST)에서 key와 value는 다음과 같이 구분됩니다:
Key (키):
Value (값):
전위 순회
노드 방문 → 왼쪽 자식 → 오른쪽 자식
중위 순회
왼쪽 자식 → 노드 방문 → 오른쪽 자식
후위 순회
왼쪽 자식 → 오른쪽 자식 → 노드 방문
https://m.blog.naver.com/rlakk11/60159303809
왜 이렇게 나눠놓은거임 그냥 하나로만 통일하면 되잖아 배우는 사람 헷갈리게 왜
이진 트리에서 다양한 순회(traversal) 방법이 존재하는 이유는 각각의 순회 방법이 트리의 구조나 요소를 다르게 접근하고 처리하는 방식에 따라 다른 용도와 목적을 가지고 있기 때문입니다. 이들 순회 방법은 다양한 알고리즘과 문제 해결에 유용하게 사용됩니다. 각각의 순회 방법을 통해 트리 구조에서 필요한 데이터를 효율적으로 추출하고, 특정 작업을 수행할 수 있습니다.
방문 순서: 뿌리(root) -> 왼쪽 하위 트리 -> 오른쪽 하위 트리
방문 순서: 왼쪽 하위 트리 -> 뿌리(root) -> 오른쪽 하위 트리
방문 순서: 왼쪽 하위 트리 -> 오른쪽 하위 트리 -> 뿌리(root)
방문 순서: 위에서 아래로, 왼쪽에서 오른쪽으로 노드를 방문
https://www.youtube.com/watch?v=WLvU5EQVZqY
어려워서 이해 못했는데 인도인이 알려줬다.
일단 탐색 순서는 그려진 대로의 경로가 고정.
chatgpt가 만들어준 예제 코드
class Node:
def __init__(self, key):
self.left = None
self.right = None
self.val = key
# 전위 순회
def preorder(root):
if root:
print(root.val, end=' ')
preorder(root.left)
preorder(root.right)
# 중위 순회
def inorder(root):
if root:
inorder(root.left)
print(root.val, end=' ')
inorder(root.right)
# 후위 순회
def postorder(root):
if root:
postorder(root.left)
postorder(root.right)
print(root.val, end=' ')
# 층별 순회
from collections import deque
def level_order(root):
if root is None:
return
queue = deque([root])
while queue:
node = queue.popleft()
print(node.val, end=' ')
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
# 트리 생성 및 순회 예제
root = Node(1)
root.left = Node(2)
root.right = Node(3)
root.left.left = Node(4)
root.left.right = Node(5)
print("전위 순회:")
preorder(root)
print("\n중위 순회:")
inorder(root)
print("\n후위 순회:")
postorder(root)
print("\n층별 순회:")
level_order(root)
앞으로 이거 기반으로 구현하면 될듯
print()의 위치를 봤더니
대놓고 첫번째, 두번째, 세번째에 있어서
전위, 중위, 후위 순회의 이름 납득됨
코드에서 이해할 땐 전위, 중위, 후위로
순서를 이해할 땐 1번째, 2번째, 3번째 방문으로 이해하면 될듯
가지 2개까지만 가질 수 있는 트리.
어렵게 말하면, 노드가 왼쪽 자식과 오른쪽 자식만을 갖는 트리.
대충 딱 보면 빈틈 없이 꽉 차있는 트리
어렵게 말하면, 루트부터 아래쪽 레벨로 노드가 가득 차 있고, 같은 레벨 안에서 왼쪽부터 오른쪽으로 노드가 채워져 있는 이진 트리
이진 검색 트리는 모든 노드가 다음 조건을 만족해야 함.
- 왼쪽 서브트리의 노드의 키값은 자신의 노드 키값보다 작아야 한다.
- 오른쪽 서브트리 노드의 키값은 자신의 노드 키값보다 커야 한다.
따라서 키값이 같은 노드는 복수로 존재하면 안됨.
모든 키값은 고유해야 함. 안 그럼 규칙 위배함.
- 구조가 단순하다.
- 중위 순회의 깊이 우선 검색을 통하여 노드값을 오름차순으로 얻을 수 있다.
- 이진 검색과 비슷한 방식으로 아주 빠르게 검색할 수 있다.
- 노드를 삽입하기 쉽다.
# 이진 검색 트리 구현하기
from __future__ import annotations
from typing import Any, Type
class Node:
"""이진 검색 트리의 노드"""
def __init__(self, key: Any, value: Any,
left: Node = None, right: Node = None):
"""생성자(constructor)"""
self.key = key # 키
self.value = value # 값
self.left = left # 왼쪽 포인터
self.right = right # 오른쪽 포인터
키, 값, 왼쪽 노드, 오른쪽 노드를 갖는 노드를 생성
class BinarySearchTree:
"""이진 검색 트리"""
def __init__(self):
"""초기화"""
self.root = None
def search(self, key: Any) -> Any:
"""키가 key인 노드를 검색"""
p = self.root # 루트에 주목
while True:
if p is None: # 더 이상 진행할 수 없으면
return None # 검색 실패
if key == p.key: # key와 노드 p의 키가 같으면
return p.value # 검색 성공
elif key < p.key: # key 쪽이 작으면
p = p.left # 왼쪽 서브트리에서 검색
else: # key 쪽이 크면
p = p.right # 오른쪽 서브트리에서 검색
노드 검색할 때 4가지 경우
현재 주목 중인 노드가
(1) None이면 : 검색 실패
(2) 찾을 값과 같으면 : 검색 성공
(3) key 쪽이 작으면 : 왼쪽 자식 노드로 넘어가기
(4) key 쪽이 크면 : 오른쪽 자식 노드로 넘어가기
def add(self, key: Any, value: Any) -> bool:
"""키가 key이고 값이 value인 노드를 삽입"""
def add_node(node: Node, key: Any, value: Any) -> None:
"""node를 루트로 하는 서브트리에 키가 key이고 값이 value인 노드를 삽입"""
if key == node.key:
return False # key가 이진 검색 트리에 이미 존재
elif key < node.key: # 키가 루트 노트보다 작으면
if node.left is None: # 왼쪽 자식이 있는지 확인하고 없으면
node.left = Node(key, value, None, None) # 왼쪽 자식에 노드 삽입
else: # 왼쪽 자식이 있으면
add_node(node.left, key, value) # 왼쪽 자식으로 가서 노드 삽입 시도
else: # 키가 루트 노드보다 크면
if node.right is None: # 오른쪽 자식이 있는지 확인하고 없으면
node.right = Node(key, value, None, None) # 오른쪽 자식에 노드 삽입
else: # 오른쪽 자식이 있으면
add_node(node.right, key, value) # 오른쪽 자식으로 가서 노드 삽입 시도
return True
if self.root is None:
self.root = Node(key, value, None, None)
return True
else:
return add_node(self.root, key, value)
필요한 질문
1. 루트 노드가 있는지
2. 부모 노드와 자식 노드의 키를 비교
3. 왼쪽/오른쪽 자식이 있는가?
class Node:
def __init__(self, key, value, left=None, right=None):
self.key = key
self.value = value
self.left = left
self.right = right
class BinarySearchTree:
def __init__(self):
self.root = None
def remove(self, key: Any) -> bool:
"""키가 key인 노드를 삭제"""
p = self.root # 스캔 중인 노드를 담음 (루트에서 스캔 시작)
parent = None # 스캔 중인 노드의 부모 노드
is_left_child = True # p는 parent의 왼쪽 자식 노드인지 확인
while True:
if p is None: # 더 이상 진행할 수 없으면
return False # 그 키는 존재하지 않음
if key == p.key: # key와 노드 p의 키가 같으면
break
else:
parent = p # 가지를 내려가기 전에 부모를 설정
if key < p.key: # key 쪽이 작으면
is_left_child = True # 여기서 내려가는 것은 왼쪽 자식
p = p.left # 왼쪽 서브트리에서 검색
else: # key 쪽이 크면
is_left_child = False # 여기서 내려가는 것은 오른쪽 자식
p = p.right # 오른쪽 서브트리에서 검색
if p.left is None: # p에 왼쪽 자식이 없으면
if p is self.root: # p가 루트라면
self.root = p.right # 루트를 원래 루트의 오른쪽 자식으로 바꿈
elif is_left_child: # p가 부모의 왼쪽 노드라면
parent.left = p.right # 부모의 왼쪽 노드는 주목 노드의 오른쪽 노드
else: # p가 부모의 오른쪽 노드라면
parent.right = p.right # 부모의 오른쪽 노드는 주목 노드의 오른쪽 노드
elif p.right is None: # p에 오른쪽 자식이 없으면
if p is self.root: # p가 루트라면
self.root = p.left # 루트를 원래 루트의 왼쪽 자식으로 바꿈
elif is_left_child: # p가 부모의 왼쪽 노드라면
parent.left = p.left # 부모의 왼쪽 노드는 주목 노드의 왼쪽 노드
else: # p가 부모의 오른쪽 노드라면
parent.right = p.left # 부모의 오른쪽 노드는 주목 노드의 왼쪽 노드
else: # p에 왼쪽 자식도 있고 오른쪽 자식도 있으면
parent = p # 부모 노드를 주목 노드로 변경
left = p.left # 왼쪽 서브트리의 가장 큰 노드를 찾기 위해 초기화
is_left_child = True # 왼쪽 자식을 탐색할 것임을 표시
while left.right is not None:
parent = left # 부모 노드를 현재 노드로 업데이트
left = left.right # 가장 큰 노드를 찾기 위해 오른쪽 자식으로 이동
is_left_child = False # 이동한 노드는 오른쪽 자식임을 표시
p.key = left.key # 주목 노드의 키를 가장 큰 노드의 키로 변경
p.value = left.value # 주목 노드의 값을 가장 큰 노드의 값으로 변경
if is_left_child: # 만약 가장 큰 노드가 왼쪽 자식이라면
parent.left = left.left # 부모 노드의 왼쪽 자식을 가장 큰 노드의 왼쪽 자식으로 변경
else: # 만약 가장 큰 노드가 오른쪽 자식이라면
parent.right = left.left # 부모 노드의 오른쪽 자식을 가장 큰 노드의 왼쪽 자식으로 변경
return True # 노드 삭제 완료
def dump(self) -> None:
"""덤프(모든 노드를 키의 오름차순으로 출력)"""
# 이진 검색 트리는 깊이 우선 검색의 중위 순회를 하면 오름차순으로 출력된다
def print_subtree(node: Node):
"""node를 루트로 하는 서브트리의 노드를 키의 오름차순으로 출력"""
if node is not None: # 노드가 있다면 (리프의 자식을 만난게 아니라면)
print_subtree(node.left) # 왼쪽 서브트리를 오름차순으로 출력
print(f'{node.key} {node.value}') # node를 출력
print_subtree(node.right) # 오른쪽 서브트리를 오름차순으로 출력
print_subtree(self.root) # 루트에서부터 시작
이진 검색 트리는 깊이 우선 검색의 중위 순회를 하면 오름차순으로 출력된다
def min_key(self) -> Any:
"""가장 작은 키"""
if self.root is None:
return None
p = self.root
while p.left is not None:
p = p.left
return p.key
def max_key(self) -> Any:
"""가장 큰 키"""
if self.root is None:
return None
p = self.root
while p.right is not None:
p = p.right
return p.key
이진 검색 트리는
왼쪽 자식은 자신보다 작고, 오른쪽 자식은 자신보다 크니까
왼쪽으로 쭉 가면 최솟값이 나오고, 오른쪽으로 쭉 가면 최댓값이 나온다.
def print_subtree_rev(node: Node):
"""node를 루트로 하는 서브트리의 노드를 키의 내림차순으로 출력"""
if node is not None:
print_subtree_rev(node.right)
print(f'{node.key} {node.value}')
print_subtree_rev(node.left)
dump 함수에서 key 오름차순이 아니라 내림차순으로 출력하고 싶으면
right랑 left 순서만 바꾸면 된다.
일단 여기까지 접근 해봤다.
2번 접근 방식도 살짝 생각해보면 좋을듯.