각 원소들을 줄줄이 엮어서 관리하는 방식
여러 Node로 구성되어 있으며, Node는 Data와 Link(next)로 구성되어있음
class Node:
def __init__(self, item):
self.data = item
self.next = None
class LinkedList:
def __init__(self):
self.node_count = 0
self.head = None
self.tail = None
특정 원소 참조(k 번째)
def get_at(self, pos): # 1부터 시작
if pos <= 0 or pos > self.node_count:
return None
curr = self.head
for _ in range(pos - 1):
curr = curr.next
return curr
연결리스트 순회
(아래 실습 에서 작성)
길이 얻어내기
def get_length(self):
return self.node_count
🏃♂️실습: 연결 리스트 순회 구현하기
문제 설명
제 7 강에서 소개된 추상적 자료구조로 LinkedList
라는 이름의 클래스가 정의되어 있다고 가정하고, 이 리스트를 처음부터 끝까지 순회하는 메서드 traverse()
를 완성하세요.
제출
def traverse(self):
result = []
curr = self.head
while curr:
result.append(curr.data)
curr = curr.next
return result
원소의 삽입
💡주의사항
def insert_at(self, pos, new_node):
if pos < 1 or pos > self.node_count + 1:
return False
if pos == 1:
new_node.next = self.head
self.head = new_node
else:
if pos == self.node_count + 1:
prev = self.tail
else:
prev = self.get_at(pos - 1)
new_node.next = prev.next
prev.next = new_node
if pos == self.node_count + 1:
self.tail = new_node
self.node_count += 1
return True
원소의 삭제
(아래 실습에서 작성)
두 리스트 합치기(concatenation)
def get_at(self, pos):
if pos <= 0 or pos > self.node_count:
return None
curr = self.head
for _ in range(pos):
curr = curr.next
return curr
🏃♂️실습: 연결 리스트 노드 삭제하기
문제 설명
제 8 강에서 소개된 추상적 자료구조 LinkedList
클래스의 메서드로서 popAt()
메서드를 강의 내용에 소개된 요구조건을 만족시키도록 구현하세요.
제출
def pop_at(self, pos):
if pos < 1 or pos > self.node_count:
raise IndexError
prev_node = None
if pos == 1:
popped_node = self.head
self.head = popped_node.next
elif pos <= self.node_count:
prev_node = self.get_at(pos - 1)
popped_node = prev_node.next
prev_node.next = popped_node.next
if pos == self.node_count:
self.tail = prev_node
self.node_count -= 1
return popped_node.data
연결 리스트는 삽입과 삭제가 유연하다는 것이 가장 큰 장점
특정 원소의 바로 다음을 지정하여 원소를 삽입/삭제하는 연산을 정의하기 위해 맨 앞에 dummy node를 추가한다.
더미노드(dummy node):데이터 원소를 담고 있지 않은 노드
head
와 tail
을 더미 노드로 구현하면 좀더 깔끔하게 작성할 수 있다.
class Node:
def __init__(self, item):
self.data = item
self.next = None
class LinkedList2:
def __init__(self):
self.nodeCount = 0
self.head = Node(None)
self.tail = None
self.head.next = self.tail
def get_at(self, pos):
if pos <= 0 or pos > self.node_count:
return None
curr = self.head
for _ in range(pos - 1):
curr = curr.next
return curr
def insert_after(self, prev, new_node):
new_node.next = prev.next
if prev.next is None:
self.tail = new_node
prev.next = new_node
self.node_count += 1
return True
def insert_at(self, pos, new_node):
if pos < 1 or pos > self.node_count + 1:
return False
if pos != 1 and pos == self.node_count + 1:
prev = self.tail
else:
prev = self.get_at(pos - 1)
return self.insert_after(prev, new_node)
insert_at()
메서드에서 prev_node
를 구하여 insert_after()
메서드를 호출한다
🏃♂️실습: dummy head 를 가지는 연결 리스트 노드 삭제
문제 설명
제 9 강에서 소개된 추상적 자료구조 LinkedList 는 dummy head node 를 가지는 연결 리스트입니다. 이 클래스의 아래와 같은 메서드들을, 강의 내용에 소개된 요구조건을 만족시키도록 구현하세요.
제출
def pop_after(self, prev):
popped_node = prev.next
if popped_node.next is None:
self.tail = prev
prev.next = popped_node.next
self.node_count -= 1
return popped_node.data
def pop_at(self, pos):
if pos < 1 or pos > self.node_count:
raise IndexError
prev = self.get_at(pos - 1)
return self.pop_after(prev)
단방향 연결 리스트는 링크가 한 방향으로, 즉 앞에서 뒤로 향하는 방향으로만 연결되어 있다.
양방향 연결 리스트는 앞 노드와 뒤 노드가 서로 연결되어 양방향으로 접근이 가능한 구조이다.
class Node:
def __init__(self, item):
self.data = item
self.prev = None
self.next = None
class DoublyLinkedList:
def __init__(self):
self.nodeCount = 0
self.head = Node(None)
self.tail = Node(None)
self.head.prev = None
self.head.next = self.tail
self.tail.prev = self.head
self.tail.next = None
양방향 연결 리스트는 단방향 연결 리스트에 비해 링크를 나타내기 위한 메모리 사용량이 늘어나지만,
뒤에서 앞으로 이동할 수 있어서 앞뒤로 왔다갔다 하면서 작업을 행하는 일을 효율적으로 할 수 있으며
아래와 같이 코드를 깔끔하게 구현할 수 있다.
def insert_after(self, prev, new_node):
next = prev.next
new_node.prev = prev
new_node.next = next
prev.next = new_node
next.prev = new_node
self.node_count += 1
return True
def insert_at(self, pos, new_node):
if pos < 1 or pos > self.node_count + 1:
return False
prev = self.get_at(pos - 1)
return self.insert_after(prev, new_node)
양방향으로 연결되어 있어 코드를 조금 개선할 수 있다 → 리스트를 반으로 나누어서 얻고자 하는 pos
가 뒤 쪽에 있으면 tail
부터 시작하여 시간을 줄일 수 있다.
def getAt(self, pos):
if pos < 0 or pos > self.nodeCount:
return None
if pos > self.nodeCount // 2:
i = 0
curr = self.tail
while i < self.nodeCount - pos + 1:
curr = curr.prev
i += 1
else:
i = 0
curr = self.head
while i < pos:
curr = curr.next
i += 1
return curr
🏃♂️실습1: 양방향 연결 리스트 역방향 순회
def reverse(self):
result = []
curr = self.tail
while curr.prev.prev:
curr = curr.prev
result.append(curr.data)
return result
🏃♂️실습2: 양방향 연결 리스트 노드 삽입
def insert_before(self, next, new_node):
prev = next.prev
new_node.prev = prev
new_node.next = next
prev.next = new_node
next.prev = new_node
self.node_count += 1
return True
🏃♂️실습3: 양방향 연결 리스트 노드 삭제
def pop_after(self, prev):
popped_node = prev.next
if popped_node.next is None:
self.tail.prev = prev
next = popped_node.next
prev.next = next
next.prev = prev
self.node_count -= 1
return popped_node.data
def pop_before(self, next):
popped_node = next.prev
if popped_node.prev is None:
self.head.next = next
prev = popped_node.prev
prev.next = next
next.prev = prev
self.node_count -= 1
return popped_node.data
def pop_at(self, pos):
if pos < 1 or pos > self.node_count:
raise IndexError
next = self.get_at(pos).next
return self.pop_before(next)
🏃♂️실습4: 양방향 연결 리스트 노드 병합
def concat(self, L):
left_last = self.tail.prev
right_first = L.head.next
left_last.next = right_first
right_first.prev = left_last
self.tail = L.tail
self.node_count += L.node_count
후입선출(LIFO) 구조
스택은 선형 배열과 연결 리스트 두 가지를 이용해서 구현할 수 있다.
class ArrayStack:
def __init__(self):
self.data = []
def size(self):
return len(self.data)
def is_empty(self):
return self.size() == 0
def push(self, item):
self.data.append(item)
def pop(self):
return self.data.pop()
def peek(self):
return self.data[-1]
class LinkedListStack:
def __init__(self):
self.data = DoublyLinkedList()
def size(self):
return self.data.get_length()
def is_empty(self):
return self.size() == 0
def push(self, item):
node = Node(item)
self.data.insert_at(self.size() + 1, node)
def pop(self):
return self.data.pop_at(self.size())
def peek(self):
return self.data.get_at(self.size()).data
size()
: 현재 스택에 들어 있는 데이터 원소의 수를 구함is_empty()
: 현재 스택이 비어 있는지를 판단push(x)
: 원소 x
를 스택에 추가pop()
: 스택에 가장 나중에 저장된 데이터 원소를 제거하고 반환peek()
: 스택에 가장 나중에 저장된 데이터 원소를 반환🏃♂️실습: 수식의 괄호 검사 (스택) (빈칸)
소괄호(), 중괄호{}, 대괄호[]를 포함할 수 있는 수식을 표현한 문자열 expr
이 인자로 주어질 때, 이 수식의 괄호가 올바르게 여닫혀 있는지를 판단하는 함수 solution()
을 완성하세요.
def solution(expr):
match = {
')': '(',
'}': '{',
']': '['
}
S = ArrayStack()
for c in expr:
if c in '({[':
S.push(c) # 빈칸
elif c in match:
if S.is_empty(): # 빈칸
return False
else:
t = match[c] # 빈칸
if t != S.pop(): # 빈칸
return False
return S.is_empty() # 빈칸
중위 표기법(Infix notation): 연산자가 피연산자 들의 사이에 위치
(A + B) * (C + D)
후위 표기법(postfix notation): 연산자가 피연산자들의 뒤에 위치
A B + C D + *
스택을 이용하면 중위 표현식을 후위 표현식으로 표현할 수 있다!
중위 표현식을 왼쪽부터 한 글자씩 읽어서
(
이면 스택에 push
)
이면 (
이 나올 때까지 스택에서 pop
후 출력pop
후 출력, 그리고 이 연산자는 스택에 push
모두 읽은 후에 스택에 남아 있는 연산자를 모두 pop
후 출력
예시1) A * B + C
→ A B * C +
예시2) A + B * C
→ A B C * +
예시3) A + B + C
→ A B + C +
⚡괄호의 처리
여는 괄호 (
는 스택에 push
닫는 괄호를 만나면 여는 괄호가 나올 때까지 pop
여는 괄호의 우선순위는 가장 낮게 설정!
예시4) (A + B) * C
→ A B + C *
예시5) A * (B + C)
→ A B C + *
🔥연산자의 우선순위 설정
prec = {
'*': 3, '/': 3,
'+': 2, '-': 2,
'(': 1,
}
🏃♂️실습: 중위표현 수식 → 후위표현 수식
class ArrayStack:
def __init__(self):
self.data = []
def size(self):
return len(self.data)
def is_empty(self):
return self.size() == 0
def push(self, item):
self.data.append(item)
def pop(self):
return self.data.pop()
def peek(self):
return self.data[-1]
prec = {
'*': 3, '/': 3,
'+': 2, '-': 2,
'(': 1
}
def solution(S):
op_stack = ArrayStack()
result = []
for c in S:
if c == '(':
op_stack.push(c)
elif c in prec:
if not op_stack.is_empty():
while not op_stack.is_empty() and prec[op_stack.peek()] >= prec[c]:
result.append(op_stack.pop())
op_stack.push(c)
else:
op_stack.push(c)
elif c == ')':
while op_stack.peek() != '(':
result.append(op_stack.pop())
op_stack.pop()
else:
result.append(c)
while op_stack.is_empty() is False:
result.append(op_stack.pop())
return ''.join(result)
후위 표기 수식을 왼쪽부터 한 글자씩 읽어서
push
push
결과가 스택에 남아 있음
🏃♂️실습: 후위표현 수식 계산
class ArrayStack:
def __init__(self):
self.data = []
def size(self):
return len(self.data)
def is_empty(self):
return self.size() == 0
def push(self, item):
self.data.append(item)
def pop(self):
return self.data.pop()
def peek(self):
return self.data[-1]
def split_tokens(expr_str):
tokens = []
val = 0
val_processing = False
for c in expr_str:
if c == ' ':
continue
if c in '0123456789':
val = val * 10 + int(c)
val_processing = True
else:
if val_processing:
tokens.append(val)
val = 0
val_processing = False
tokens.append(c)
if val_processing:
tokens.append(val)
return tokens
def infix_to_postfix(token_list):
prec = {
'*': 3,
'/': 3,
'+': 2,
'-': 2,
'(': 1,
}
op_stack = ArrayStack()
postfix_list = []
for c in token_list:
if c == '(':
op_stack.push(c)
elif c in prec:
if not op_stack.is_empty():
while not op_stack.is_empty() and prec[op_stack.peek()] >= prec[c]:
postfix_list.append(op_stack.pop())
op_stack.push(c)
else:
op_stack.push(c)
elif c == ')':
while op_stack.peek() != '(':
postfix_list.append(op_stack.pop())
op_stack.pop()
else:
postfix_list.append(c)
while op_stack.is_empty() is False:
postfix_list.append(op_stack.pop())
return postfix_list
def postfix_eval(token_list):
num_stack = ArrayStack()
for t in token_list:
if isinstance(t, int):
num_stack.push(t)
else:
if t == '+':
num_stack.push(num_stack.pop() + num_stack.pop())
elif t == '-':
num_stack.push(-num_stack.pop() + num_stack.pop())
elif t == '*':
num_stack.push(num_stack.pop() * num_stack.pop())
elif t == '/':
num_stack.push(1 / num_stack.pop() * num_stack.pop())
return num_stack.pop()
def solution(expr):
tokens = split_tokens(expr)
postfix = infix_to_postfix(tokens)
val = postfix_eval(postfix)
return val
선입선출(FIFO) 구조
큐는 스택과 동일하게 선형 배열과 연결 리스트 두 가지를 이용해서 구현할 수 있다.
class ArrayStack:
def __init__(self):
self.data = []
def size(self):
return len(self.data)
def is_empty(self):
return self.size() == 0
def enqueue(self, item):
self.data.append(item)
def dequeue(self):
return self.data.pop(0)
def peek(self):
return self.data[0]
class LinkedListStack:
def __init__(self):
self.data = DoublyLinkedList()
def size(self):
return self.data.get_length()
def is_empty(self):
return self.size() == 0
def enqueue(self, item):
node = Node(item)
self.data.insert_at(self.size() + 1, node)
def dequeue(self):
return self.data.pop_at(1)
def peek(self):
return self.data.get_at(1).data
size()
: 현재 큐에 들어 있는 데이터 원소의 수를 구함is_empty()
: 현재 큐가 비어 있는지를 판단enqueue()
: 데이터 원소를 큐에 넣는 동작dequeue()
: 데이터 원소를 꺼내는 동작peek()
: 큐의 맨 앞에 저장된 데이터 원소를 반환🏃♂️실습: 양방향 연결 리스트로 규현하는 큐 (빈칸)
from .DoublyLinkedList import DoublyLinkedList
class LinkedListQueue:
def __init__(self):
self.data = DoublyLinkedList()
def size(self):
return self.data.getLength() # 빈칸
def isEmpty(self):
return self.data.getLength() == 0 # 빈칸
def enqueue(self, item):
node = Node(item)
return self.data.insertAt(self.size() + 1, node) # 빈칸
def dequeue(self):
return self.data.popAt(1) # 빈칸
def peek(self):
return self.data.getAt(1).data # 빈칸
정해진 개수의 저장 공간을 빙 돌려가며 이용
큐가 가득차면 더이상 원소를 넣을 수 없음
→ 큐 길이를 기억하고 있어야 함!
큐 연산에서 is_full()
(큐에 데이터 원소가 꽉 차 있는 지를 판단) 메서드 추가
🏃♂️실습: 환형 큐 구현 (빈칸)
class CircularQueue:
def __init__(self, n):
self.maxCount = n
self.data = [None] * n
self.count = 0
self.front = -1
self.rear = -1
def size(self):
return self.count
def is_empty(self):
return self.count == 0
def is_full(self):
return self.count == self.maxCount
def enqueue(self, x):
if self.is_full():
raise IndexError('Queue full')
self.rear = (self.rear + 1) % self.maxCount # 빈칸
self.data[self.rear] = x
self.count += 1
def dequeue(self):
if self.is_empty(): # 빈칸
raise IndexError('Queue empty')
self.front = (self.front + 1) % self.maxCount # 빈칸
x = self.data[self.front] # 빈칸
self.count -= 1
return x
def peek(self):
if self.is_empty():
raise IndexError('Queue empty')
return self.data[(self.front + 1) % self.maxCount] # 빈칸
큐가 FIFO 방식을 따르지 않고 원소들의 우선순위에 따라 큐에서 빠져나오는 방식
우선순위 큐의 구현은
🏃♂️실습: 우선순위 큐의 enqueue 연산 구현
from DoublyLinkedList import DoublyLinkedList, Node
class PriorityQueue:
def __init__(self):
self.queue = DoublyLinkedList()
def size(self):
return self.queue.get_length()
def is_empty(self):
return self.size() == 0
def enqueue(self, x):
newNode = Node(x)
curr = self.queue.head # 빈칸
while curr.next.data is not None and curr.next.data[0] > x[0]: # 빈칸
curr = curr.next
self.queue.insert_after(curr, newNode) # 빈칸
def dequeue(self):
return self.queue.pop_at(self.queue.get_length())
def peek(self):
return self.queue.get_at(self.queue.get_length()).data
정점(Node)과 간선(Edge)을 이용하여 데이터의 배치 형태를 추상화한 자료구조
노드 (nodes)
간선 (edges)
루트 노드 (root node), 말단 노드 (leaf nodes), 내부 노드 (internal nodes)
부모 (parent) 노드와 자식 (child) 노드
노드의 수준 (level)
root 노드로부터 거쳐야 하는 간선의 개수
root 노드부터 0으로 시작
트리의 높이 (height) = 수준(level)의 최대값 + 1 (또는, 깊이 (depth)라고도 함)
부분 트리 (서브트리; subtrees)
트리 내에서 root 노드를 제외한 다른 노드로부터 시작하는 트리
노드의 차수 (degree)
자식 노드의 수
이진 트리 (binary trees)
모든 노드의 차수가 2 이하인 트리
재귀적으로 정의 가능 → 빈 트리이거나, 루트 노드 + 왼쪽 서브트리 + 오른쪽 서브트리(단, 모든 서브트리 또한 이진 트리)
포화 이진 트리 (full binary trees)
모든 레벨에서 노드들이 모두 채워져 있는 이진 트리
(높이가 이고 노드의 개수가 인 이진 트리)
완전 이진 트리 (complete binary trees)
높이가 인 완전 이진 트리는 다음 조건을 만족하여야 함
모든 노드의 차수가 2 이하인 트리
class Node:
def __init__(self, item):
self.data = item
self.left = None
self.right = None
def size(self):
l = self.left.size() if self.left else 0
r = self.right.size() if self.right else 0
return l + r + 1
class BinaryTree:
def __init__(self, r):
self.root = r
def size(self):
if self.root:
return self.root.size()
else:
return 0
size()
: 현재 트리에 포함되어 있는 노드의 수를 구함depth()
: 현재 트리의 깊이 (또는 높이) 를 구함깊이 우선 순회는 재귀적 방법이 적합하지만, 너비 우선 순회는 적합하지 않음!
🏃♂️실습1: 이진트리의 depth() 연산 구현
class Node:
# ...
def depth(self):
l = self.left.depth() if self.left else 0
r = self.right.depth() if self.right else 0
return max(l, r) + 1
class BinaryTree:
# ...
def depth(self):
return self.root.depth() if self.root else 0
🏃♂️실습2: 이진트리의 전위순회 연산 구현
class Node:
# ...
def preorder(self):
traversal = []
traversal.append(self.data)
traversal += self.left.preorder() if self.left else []
traversal += self.right.preorder() if self.right else []
return traversal
class BinaryTree:
# ...
def preorder(self):
return self.root.preorder() if self.root else []
🏃♂️실습3: 이진트리의 후위순회 연산 구현
class Node:
# ...
def postorder(self):
traversal = []
traversal += self.left.postorder() if self.left else []
traversal += self.right.postorder() if self.right else []
traversal.append(self.data)
return traversal
class BinaryTree:
# ...
def postorder(self):
return self.root.postorder() if self.root else []
너비 우선 순회는 재귀적 방법이 적합하지 않음!
→ 큐(queue)를 이용
traversal
← 빈 리스트 / q
← 빈 큐q
에 추가 (enqueue)node
← q에서 원소를 추출 (dequeue)node
를 방문node
의 왼쪽, 오른쪽 자식이 존재하면 차례로 q
에 추가 (enqueue)q
가 빈 큐가 되면 모든 노드 방문 완료🏃♂️실습: 이진 트리의 너비 우선 순회
class ArrayQueue:
def __init__(self):
self.data = []
def size(self):
return len(self.data)
def isEmpty(self):
return self.size() == 0
def enqueue(self, item):
self.data.append(item)
def dequeue(self):
return self.data.pop(0)
def peek(self):
return self.data[0]
class Node:
def __init__(self, item):
self.data = item
self.left = None
self.right = None
class BinaryTree:
def __init__(self, r):
self.root = r
def bft(self):
traversal, q = [], ArrayQueue()
if self.root:
q.enqueue(self.root)
while q.isEmpty() is False:
node = q.dequeue()
traversal.append(node.data)
if node.left:
q.enqueue(node.left)
if node.right:
q.enqueue(node.right)
return traversal
모든 노드에 대해서,
성질을 만족하는 이진 트리
class Node:
def __init__(self, key, data):
self.key = key
self.data = data
self.left = None
self.right = None
class BinSearchTree:
def __init__(self):
self.root = None
정렬된 배열을 이용한 이진 탐색과 비교했을때
이진 탐색 트리를 이용하면 데이터 원소의 추가와 삭제가 용이 (하지만 공간 소요가 큼)
이진 탐색 트리는 중복값을 삽입할 수 없음!
항상 시간 복잡도가 O(logn) 나오는 것이 아님. 그렇지 않은 경우가 존재!
계속 증가하는 값을 삽입하거나 감소하는 값을 삽입하여 한 쪽으로 치우쳐진 트리가 만들어지는 경우가 아닐까? 그래서 보완하기 위해 나온 게 AVL 트리, 레드-블랙 트리였던 것 같은데...🤔
insert(key, data)
: 트리에 주어진 데이터 원소를 추가remove(key)
: 특정 원소를 트리로부터 삭제lookup(key)
: 특정 원소를 검색inorder()
: 키의 순서대로 데이터 원소를 나열min(), max()
: 최소 키, 최대 키를 가지는 원소를 각각 탐색insert()
메서드 호출🏃♂️실습: 이진 탐색 트리의 원소 삽입 연산 구현
class Node:
# ...
def insert(self, key, data):
if self.key > key:
if self.left is None:
self.left = Node(key, data)
else:
return self.left.insert(key, data)
elif self.key < key:
if self.right is None:
self.right = Node(key, data)
else:
return self.right.insert(key, data)
else:
raise KeyError
return True
class BinSearchTree:
# ...
def insert(self, key, data):
if self.root:
self.root.insert(key, data)
else:
self.root = Node(key, data)
이진 탐색 트리 구조의 유지
삭제되는 노드가
left
혹은 right
값을 None
으로 설정)left
혹은 right
값을 배치된 자식 노드로 설정)left
혹은 right
값을 배치된 자식 노드로 설정)*삭제되는 노드가 root node 인 경우 → 대신 들어오는 자식이 새로 root가 됨 (처리 필요)
트리가 한쪽으로 치우쳐진 경우 → O(n)
소요
높이의 균형을 유지함으로써 O(logn)의 시간 복잡도를 보장해주는 이진 탐색 트리는 다음과 같다.
하지만, 삽입/삭제가 보다 복잡함
🏃♂️실습: 이진 탐색 트리에서 노드의 삭제 연산 구현
def remove(self, key):
node, parent = self.lookup(key)
if node:
nChildren = node.countChildren()
# The simplest case of no children
if nChildren == 0:
# 만약 parent 가 있으면
# node 가 왼쪽 자식인지 오른쪽 자식인지 판단하여
# parent.left 또는 parent.right 를 None 으로 하여
# leaf node 였던 자식을 트리에서 끊어내어 없앱니다.
if parent:
if node == parent.left:
parent.left = None
else:
parent.right = None
# 만약 parent 가 없으면 (node 는 root 인 경우)
# self.root 를 None 으로 하여 빈 트리로 만듭니다.
else:
self.root = None
# When the node has only one child
elif nChildren == 1:
# 하나 있는 자식이 왼쪽인지 오른쪽인지를 판단하여
# 그 자식을 어떤 변수가 가리키도록 합니다.
chileNode = node.left if node.left else node.right
# 만약 parent 가 있으면
# node 가 왼쪽 자식인지 오른쪽 자식인지 판단하여
# 위에서 가리킨 자식을 대신 node 의 자리에 넣습니다.
if parent:
if parent.left == node:
parent.left = chileNode
else:
parent.right = chileNode
# 만약 parent 가 없으면 (node 는 root 인 경우)
# self.root 에 위에서 가리킨 자식을 대신 넣습니다.
else:
self.root = chileNode
# When the node has both left and right children
else:
parent = node
successor = node.right
# parent 는 node 를 가리키고 있고,
# successor 는 node 의 오른쪽 자식을 가리키고 있으므로
# successor 로부터 왼쪽 자식의 링크를 반복하여 따라감으로써
# 순환문이 종료할 때 successor 는 바로 다음 키를 가진 노드를,
# 그리고 parent 는 그 노드의 부모 노드를 가리키도록 찾아냅니다.
while successor.left:
parent, successor = successor, successor.left
# 삭제하려는 노드인 node 에 successor 의 key 와 data 를 대입합니다.
node.key = successor.key
node.data = successor.data
# 이제, successor 가 parent 의 왼쪽 자식인지 오른쪽 자식인지를 판단하여
# 그에 따라 parent.left 또는 parent.right 를
# successor 가 가지고 있던 (없을 수도 있지만) 자식을 가리키도록 합니다.
if parent.left == successor:
parent.left = successor.left
else:
parent.right = successor.right
return True
else:
return False
이진 트리의 한 종류
모든 부모 노드는 자식 노드보다 작은 값 (또는 큰 값) 을 가짐
→ 재귀적으로도 정의됨 (힙 내의 모든 서브트리 또한 최대 힙)
힙은 중복값 삽입 가능!
🔥이진 탐색 트리 vs 힙
이진 탐색 트리는 특정 값을 검색하는 데에 사용
힙은 최대/최소값을 찾는 데에 사용
(root 노드 번호를 1로 설정)
노드 번호 k를 기준으로
힙은 완전 이진 트리 성질을 가지기 때문에 배열로 구현해도 나쁘지 않다.
class MaxHeap:
def __init__(self):
self.data = [None]
🏃♂️실습: 최대 힙에 새로운 원소 삽입
def insert(self, item):
k = len(self.data)
self.data.append(item)
while k // 2 > 0:
if self.data[k // 2] < self.data[k]:
self.data[k // 2], self.data[k] = self.data[k], self.data[k // 2]
k //= 2
def remove(self):
if len(self.data) > 1:
self.data[1], self.data[-1] = self.data[-1], self.data[1]
data = self.data.pop(-1)
self.maxHeapify(1)
else:
data = None
return data
🏃♂️실습: 최대 힙에서의 원소 삭제
def remove(self):
if len(self.data) > 1:
self.data[1], self.data[-1] = self.data[-1], self.data[1]
data = self.data.pop(-1)
self.maxHeapify(1)
else:
data = None
return data
def maxHeapify(self, i):
smallest = i
left = 2 * i
right = 2 * i + 1
if left < len(self.data) and self.data[left] > self.data[smallest]:
smallest = left
if right < len(self.data) and self.data[right] > self.data[smallest]:
smallest = right
if smallest != i:
self.data[i], self.data[smallest] = self.data[smallest], self.data[i]
self.maxHeapify(smallest)