강의실 오면서 현재 루틴 계산해 봄
7시 30분 기상
7시 50분 강의실 입실
8시 00분 공부 시작
점심, 저녁 식사 각각 1시간씩 소모
23시 00분 강의실 퇴실
강의실 재실 시간: 13시간
휴식 시간: 1시간
하루 학습 시간 12시간
6일 학습 시간 12 x 6 = 72시간
7일 학습 시간 12 x 7 = 84시간
결론: 주당 100시간에 한참 모자르다.
(루틴을 개선할 필요가 있음)
(스태과 마찬가지로) 데이터를 임시 저장하는 자료구조
선입입선출(FIFO, First In First Out)
용어 정리
인큐(enqueue): 데이터를 추가하는 작업 ('밀어넣기'라고도 함)
디큐(dequeue): 데이터를 꺼내는 작업
프론트(front): 데이터를 꺼내는 쪽
리어(rear): 데이터를 넣는 쪽
큐는 공항에서 체크인 할 때 줄 서는 거 생각!
입구(리어)로 들어가서 출구(프론트)로 나간다!
처리 복잡도
인큐: O(1) > 끝에 그냥 집어넣으면 됨
디큐: O(n) > 하나를 꺼내고, 나머지를 각각 한칸씩 앞으로 땡겨야 함
(참고)우선순위 큐
인큐할 때 데이터에 우선 순위 부여
디큐할 때 우선순위 높은 데이터를 꺼냄
(파이썬에서 heapq 모듈에서 제공)
우선순위큐 인큐: heapq.heappush(heap, data)
우선순위큐 디큐: heapq.heappop(heap)
우선 순위 큐도 hands on으로 구현해 보자!
(우선 순위 큐를 구현하려면 heap을 알아야 함)
그럼 heap을 알아보자!(하단)
디큐 이후에 배열 안의 원소를 옮기지 않는 큐
**링 버퍼는 디큐 후에 모든 원소를 옮길 필요 없이 front
와 rear
값을 업데이트만 하면 된다.
따라서 디큐를 할 때도 처리 복잡도는 O(1)이다.
링 버퍼자료구조란?
배열의 처음과 끝이 연결되어 있다고 보는 자료 구조
맨 앞과 맨 끝의 원소를 식별하기 위해front
,rear
변수를 사용함.
버퍼란?
데이터를 한 곳에서 다른 한 곳으로 전송하는 동안 일시적으로 데이터를 보관하는 메모리 영역**
'버퍼링'이라는 말이 여기서 나왔음.
버퍼링: 버퍼를 활용하는 방식이나 버퍼를 채우는 동작
(북한말로 버퍼를 '완충기억기'라고 함)
from typing import Any
class FixedQueue:
class Empty(Exception):
"""비어 있는 FixedQueue에서 디큐 또는 피크할 때 내보내는 예외 처리"""
pass
class Full(Exception):
"""가득 차 있는 FixedQueu에서 인큐할 때 내보내는 예외 처리"""
pass
def __init__(self, capacity: int) -> None:
"""큐 초기화"""
self.no = 0 # 현재 데이터 개수
self.front = 0 # 맨 앞 원소 커서
self.rear = 0 # 맨 끝 원소 커서
self.capacity = capacity # 큐의 크기
self.que = [None] * capacity # 큐의 본체
def __len__(self) -> int:
"""큐에 있는 모든 데이터 개수를 반환"""
return self.no
def is_empty(self) -> bool:
return self.no <= 0
def is_full(self) -> bool:
return self.no >= self.capacity
def enque(self, x: Any) -> None:
"""데이터 x를 인큐"""
if self.is_full():
raise FixedQueue.Full # 큐가 가득 차 있는 경우 예외 처리 발생
self.que[self.rear] = x
self.rear += 1
self.no += 1
if self.rear == self.capacity:
self.rear = 0
def deque(self) -> Any:
"""데이터를 디큐"""
if self.is_empty():
raise FixedQueue.Empty # 큐가 비어 있는 경우 예외 처리 발생
x = self.que[self.front]
self.front += 1
self.no -= 1
if self.front == self.capacity:
self.front = 0
return x
def peek(self) -> Any:
"""큐에서 데이터를 피크(맨 앞 데이터를 들어다봄)"""
if self.is_empty():
raise FixedQueue.Empty # 큐가 비어 있는 경우 예외 처리를 발생
return self.que[self.front]
def find(self, value: Any) -> Any:
"""큐에서 value를 찾아 인덱스를 반환(없으면 -1을 반환)"""
for i in range(self.no):
idx = (i + self.front) % self.capacity # 검색 성공
if self.que[idx] == value:
return idx
return -1 # 검색 실패
def count(self, value: Any) -> int:
"""큐에 있는 value의 개수를 반환"""
c = 0
for i in range(self.no): # 큐 데이터를 선형 검색
idx = (i + self.front) % self.capacity
if self.que[idx] == value: # 검색 성공
c += 1
return c
def __contains__(self, value: Any) -> bool:
"""큐에 value가 있는지 판단"""
return self.count(value)
def clear(self) -> None:
"""큐의 모든 데이터를 비움"""
self.no = self.front = self.rear = 0
def dump(self) -> None:
"""모든 데이터를 맨 앞부터 맨 끝 순서로 출력"""
if self.is_empty(): # 큐가 비어있음
print("큐가 비었습니다.")
else:
for i in range(self.no):
print(self.que[(i + self.front) % self.capacity], end=' ')
print()
que.enque(10)
que.enque(7)
que.enque(2)
que.enque(2)
que.enque(2)
print(que.is_empty())
print(que.is_full())
print(que.peek())
print(que.find(10))
print(que.deque())
print(que.count(2))
print(que.dump())
print(que.clear())
print(que.dump())
파이썬 배열로 큐 자료구조 구현해 봄!
(참고)양방향 대기열 덱
덱(deque, Double Ended Queue)
맨 앞과 맨 끝 양쪽에서 데이터를 넣고 꺼낼 수 있는 자료 구조
(프론트와 리어에서 인큐와 디큐가 각각 가능)
파이썬에서 collections.deque 컨테이너로 제공
큐와 스택을 합친 형태
from enum import Enum
from fixed_queue import FixedQueue
Menu = Enum('Mune', ['인큐', '디큐', '피크', '검색', '덤프', '종료'])
def select_menu() -> Menu:
"""메뉴 선택"""
s = [f'({m.value}){m.name}' for m in Menu]
while True:
print(*s, sep=' ', end='')
n = int(input(': '))
if 1 <= n <= len(Menu):
return Menu(n)
q = FixedQueue(64)
while True:
print(f'현태 데이터 개수: {len(q)} / {q.capacity}')
menu = select_menu() # 메뉴 선택
if menu == Menu.인큐: # 인큐
x = int(input('인큐할 데이터를 입력하세요.: '))
try:
q.enque(x)
except FixedQueue.Full:
print('큐가 가득 찼습니다.')
elif menu == Menu.디큐:
try:
x = q.deque()
print(f'디큐한 데이터는 {x}입니다.')
except FixedQueue.Empty:
print('큐가 비어 있습니다.')
elif menu == Menu.피크:
try:
x = q.peek()
print(f'피크한 데이터는 {x}입니다.')
except FixedQueue.Empty:
print('큐가 비었습니다.')
elif menu == Menu.검색:
x = int(input('검색할 값을 입력하세요: '))
if x in q:
print(f'{q.count(x)}개 포함되고, 맨 앞의 위치는 {q.find(x)}입니다.')
else:
print('검색값을 찾을 수 없습니다.')
elif menu == Menu.덤프:
q.dump()
else:
break
오래된 데이터를 버리는 용도로 사용
(버퍼가 가득 찬 상황에서 새로운 데이터가 인큐되면 마지막 원소에 덮어씌움)
n = int(input('정수를 몇 개 저장할까요?: '))
a = [None] * n
cnt = 0
while True:
a[cnt % n] = int(input((f'{cnt + 1}번째 정수를 입력하세요.: ')))
cnt += 1
retry = input(f'계속 할까요?(Y ... Yes / N ... No): ')
if retry in {'N', 'n'}:
break
i = cnt - n
if i < 0: i = 0
while i < cnt:
print(f'{i + 1}번째 = a{a[i % n]}')
i += 1
열거형을 정의
enumeration(특정 값들의 집합을 정의할 때 사용)
Enum(열거형의 이름, 열거형 멤버의 이름을 리스트 형태로 제공
Enum('Mune', ['인큐', '디큐', '피크', '검색', '덤프', '종료'])
# 클래스로 사용
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
print(Color.RED) # 출력: Color.RED
print(Color.RED.value) # 출력: 1
iter객체를 (0, iter[0]), (1, iter[1]) ... 형식으로 반환
리스트나 튜플 같은 순회 가능한 객체를 인데스와 값을 함께 사용할 때 활용
fruits = ["apple", "banana", "mango"]
for index, value in enumerate(fruits):
print(index, value)
내가 푼 방식(큐(덱)를 활용하지 않음)
N, K = map(int, input().split())
nums = [num for num in range(1, N+1)]
eliminated = []
idx = -1
while len(order) < N:
count = 0
while True:
idx = (idx + 1) % N
if nums[idx] != 0:
count += 1
if count == K:
eliminated.append(nums[idx])
nums[idx] = 0
break
print(str(eliminated).replace("[", "<").replace("]", ">"))
큐(덱)를 활용한 방식
from collections import deque
queue = deque()
answer = []
N, K = map(int, input().split())
for i in range(1, N+1):
queue.append(i)
while queue:
for i in range(K-1):
queue.append(queue.popleft())
answer.append(queue.popleft())
print(str(answer).replace("[", "<").replace("]", ">"))
파이썬 deque 연습을 많이 해 봐야겠다.
deque적 사고(?)에 익숙해지기!
# 정리 대기
우선 순위 큐는 일반적으로 힙(heap)으로 구현
힙 공부한 다음에 다시 정리
리스트 자료 구조 중 하나
리스트: 순서가 있는 데이터를 늘어놓은 자료구조
큐, 스택도 일종의 리스트 자료구조
(파이썬의 '리스트'와는 다른 개념임)
연결 리스트의 구성
연결 리스트는 노드로 구성 되어 있음
노드: 연결 리스트를 구성하는 원소
데이터: 해당 노드의 값(데이터 자체가 아니라 참조 값임!)
포인터: 다음 노드를 가리키는 값(다음 노드 자체가 아니라 참조 값임!)
머리 노드(head node): 맨 앞에 있는 노드
꼬리 노드(tail node): 맨 끝에 있는 노드
앞쪽 노드(predecessor node): 각 노드 바로 앞에 있는 노드
뒤쪽 노드(successor node): 각 노드 바로 뒤에 있는 노드
단순 배열로 포인터가 없는 리스트를 구현할 수 있지만 비효율적(이건 사실상 배열임)
자기 참조형
자신과 같은 형의 인스턴스를 참조하는 필드가 있는 구조
(참조하는 다음 노드가 자기랑 같은 자료형인 경우)
연결 리스트 왜 쓰나?
원소 삽입, 삭제할 때 빠름(포인터만 바꿔주면 되니까)
(배열은 삽입, 삭제 시 모든 원소를 이동시켜야 되는 문제 있음)
메모리, 속도 면에서는 배열보다 효율 낮음(선형 검색)
(기본적으로 포인터를 저장해야 됨, 순차 접근 해야 됨)
# 포인터로 연결 리스트 구현
from __future__ import annotations
from typing import Any, Type
class Node:
"""연결 리스트용 노드 클래스"""
def __init__(self, data: Any = None, next: Node = None):
"""초기화"""
self.data = data
self.next = next
class LinkedList:
"""연결 리스트 클래스"""
def __init__(self) -> None:
"""초기화"""
self.no = 0
self.head = None
self.current = None
def __len__(self) -> int:
"""연결 리스트의 노드 개수를 반환"""
return self.no
def search(self, data: Any) -> int:
"""data와 값이 같은 노드를 검색"""
cnt = 0
ptr = self.head
while ptr is not None:
if ptr.data == data:
self.current = ptr
return cnt
cnt += 1
ptr = ptr.next
return -1
def __contains__(self, data: Any) -> bool:
"""연결 리스트에 data가 포함되어 있는지 확인"""
return self.search(data) >= 0
def add_first(self, data: Any) -> None:
"""맨 앞에 노드를 삽입"""
ptr = self.head # 삽입하기 전의 머리 노드
self.head = self.current = Node(data, ptr) # head와 current에 같은 값을 할당
self.no += 1
def add_last(self, data: Any):
"""맨 끝에 노드를 삽입"""
if self.head is None:
self.add_first(data)
else:
ptr = self.head
while ptr.next is not None:
ptr = ptr.next
# current는 최근 활성화된 노드라고 생각하기
ptr.next = self.current = Node(data, None)
self.no += 1
def remove_first(self) -> None:
"""머리 노드를 삭제"""
if self.head is not None: # 리스트가 비어 있지 않다면
self.head = self.current = self.head.next
self.no -= 1
# 꼬리 노드를 삭제하려면 삭제 후에 꼬리가 될 노드(pre)를 찾아야 됨
def remove_last(self):
"""꼬리 노드를 삭제"""
if self.head is not None:
if self.head.next is None: # 노드가 1개 뿐이라면
self.remove_first # 머리 노드를 삭제
else:
ptr = self.head # 스캔 중인 노드
pre = self.head # 스캔 중인 노드의 앞쪽 노드
while ptr.next is not None:
pre = ptr
ptr = ptr.next
pre.next = None # pre는 삭제 뒤 꼬리 노드
self.current = pre
self.no -= 1
def remove(self, p: Node) -> None:
"""노드 p를 삭제"""
if self.head is not None:
if p is self.head:
self.remove_first() # p가 머리 노드이면
else:
ptr = self.head
while ptr.next is not p:
ptr = ptr.next
if ptr is None:
return # ptr은 리스트에 존재하지 않음
ptr.next = p.next
self.current = ptr
self.no -= 1
def remove_current_node(self) -> None:
"""주목 노드를 삭제"""
while self.head is not None: # 전체가 비어 있을 때까지
self.remove_first() # 머리 노드를 삭제
self.current = None
self.no = 0
def next(self) -> bool:
"""주목 노드를 한 칸 뒤로 이동"""
if self.current is None or self.current.next is None:
return False
self.current = self.current.next
return True
def print_current_node(self) -> None:
"""주목 노드를 출력"""
if self.current is None:
print("주목 노드가 존재하지 않습니다.")
else:
print(self.current.data)
def print(self) -> None:
"""모든 노드를 출력"""
ptr = self.head
while ptr is not None:
print(ptr.data)
ptr = ptr.next
def __iter__(self) -> LinkedListIterator:
"""이터레이터를 반환"""
return LinkedListIterator(self.head)
class LinkedListIterator:
"""클래스 LinkedList의 이터레이터용 클래스"""
def __init__(self, head: Node):
self.current = head
def __iter__(self) -> LinkedListIterator:
return self
def __next__(self) -> Any:
if self.current is None:
raise StopIteration
else:
data = self.current.data
self.current = self.current.next
return data
연결 리스트를 활용한 프로그램
# 포인터를 이용한 연결 리스트 클래스 LinkedList 사용하기
from enum import Enum
from linked_list import LinkedList
Menu = Enum('Menu', [
'머리에노드삽입',
'꼬리에노드삽입',
'머리노드삭제',
'꼬리노드삭제',
'주목노드출력',
'주목노드이동',
'주목노드삭제',
'모든노드삭제',
'검색',
'멤버십판단',
'모든노드출력',
'스캔',
'종료'
])
def select_Menu() -> Menu:
"""메뉴 선택"""
s = [f'({m.value}){m.name}' for m in Menu]
while True:
print(*s, sep=' ', end='')
n = int(input(': '))
if 1 <= n <= len(Menu):
return Menu(n)
lst = LinkedList()
while True:
menu = select_Menu()
if menu == Menu.머리에노드삽입:
lst.add_first(int(input('머리 노드에 넣을 값을 입력하세요.: ')))
elif menu == Menu.꼬리에노드삽입:
lst.add_last(int(input('꼬리 노드에 넣을 값을 입력하세요: ')))
elif menu == Menu.머리노드삭제:
lst.remove_first()
elif menu == Menu.꼬리노드삭제:
lst.remove_last()
elif menu == Menu.주목노드출력:
lst.print_current_node()
elif menu == Menu.주목노드이동:
lst.next()
elif menu == Menu.주목노드삭제:
lst.remove_current_node()
elif menu == Menu.모든노드삭제:
lst.clear()
elif menu == Menu.검색:
pos = lst.search(int(input('검색할 값을 입력하세요: ')))
if pos >= 0:
print(f'그 값의 데이터는 {pos + 1}번째에 있습니다.')
else:
print('해당하는 데이터가 없습니다.')
elif menu == Menu.멤버십판단:
print('그 값의 데이터는 포함되어' +
('있습니다.' if int(input('판단할 값을 입력하세요: ')) in lst else '있지 않습니다.'))
elif menu == Menu.모든노드출력:
lst.print()
elif menu == Menu.스캔:
for e in lst:
print(e)
else:
break
연결 리스트는 약간 git이랑 비슷하다?(특히 head 포인터)
꼬리 노드가 머리 노드를 가리키는 모양
꼬리 노드 뒤쪽 포인터가 None이 아니라 머리 노드의 포인터값
이중 연결 리스트가 왜 필요한가?
연결 리스트는 뒤쪽 노드 찾기 슆지만 앞쪽 노드 찾기 어렵다.(앞 포인터가 없다)
(머리쪽이 앞이다.)
이걸 개선한 게 이중 연결 리스트
이중 연결 리스트는 '양방향 리스트(bidirectional linked list)'라고도 함
이중 리스트의 필드
1. data: 데이터에 대한 참조
2. prev: 앞쪽 노드에 대한 참조
3. next: 뒤쪽 노드에 대한 참조
원형 이중 연결 리스트 구현
더미 노드를 만들고 더미 노드를 중심으로 연결한다.
(더미 노드는 실제 노드로 취급하지 않음.)
# 원형 이중 연결 리스트
from __future__ import annotations
from typing import Any, Type
class Node:
"""원형 이중 연결 리스트용 노드 클래스"""
def __init__(self, data: Any = None, prev: Node = None, next: Node = None) -> None:
"""초기화"""
self.data = data # 데이터
self.prev = prev or self # 앞쪽 포인터
self.next = next or self # 뒤쪽 포인터
class DoubleLinkedList:
"""원형 이중 연결 리스트 클래스"""
def __init__(self) -> None:
"""초기화"""
self.head = self.current = Node() # 더미 노드 생성
self.no = 0
def __len__(self) -> int:
"""연결 리스트의 노드 수를 반환"""
return self.no
def is_empty(self) -> bool:
"""리스트가 비었는지 확인"""
return self.head.next is self.head
def search(self, data: Any) -> Any:
"""data와 값이 같은 노드를 검색"""
cnt = 0
ptr = self.head.next # 현재 스캔 중인 노드
while ptr is not self.head:
if data == ptr.data:
self.current = ptr
return cnt # 검색 성공
cnt += 1
ptr = ptr.next # 뒤쪽 노드에 주목
return -1 # 검색 실패
def print_current_node(self) -> None:
"""주목 노드를 출력"""
if self.is_empty():
print('주목 노드는 없습니다.')
else:
print(self.current.data)
def print(self) -> None:
"""모든 노드를 출력"""
ptr = self.head.next # 더미 노드의 뒤쪽 노드
while ptr is not self.head:
print(ptr.data)
ptr = ptr.next
def print_reverse(self) -> None:
"""모든 노드를 역순으로 출력"""
ptr = self.head.prev # 더미 노드의 앞쪽 노드
while ptr is not self.head:
print(ptr.data)
ptr = ptr.prev
def next(self) -> bool:
"""주목 노드를 한 칸 뒤로 이동"""
if self.is_empty() or self.current.next is self.head:
return False # 이동할 수 없음
self.current = self.current.next
return True
def prev(self) -> bool:
"""주목 노드를 한 칸 앞으로 이동"""
if self.is_empty() or self.current.prev is self.head:
return False # 이동할 수 없음
self.current = self.current.prev
return True
def add(self, data: Any) -> None:
"""주목 노드 바로 뒤에 노드를 삽입"""
node = Node(data, self.current, self.current.next)
self.current.next.prev = node
self.current.next = node
self.current = node
self.no += 1
def add_first(self, data: Any) -> None:
"""맨 앞에 노드를 삽입"""
self.current = self.head # 더미 노드 head의 바로 뒤에 삽입
self.add(data)
def add_last(self, data: Any) -> None:
"""맨 뒤에 노드를 삽입"""
self.current = self.head.prev # 꼬리 노드 head.prev의 바로 뒤에 삽입
self.add(data)
def remove_current_node(self) -> None:
"""주목 노드 삭제"""
if not self.is_empty():
self.current.prev.next = self.current.next
self.current.next.prev = self.current.prev
self.current = self.current.prev
self.no -= 1
if self.current is self.head:
self.current = self.head.next
def remove(self, p: Node) -> None:
"""노드 p를 삭제"""
ptr = self.head.next
while ptr is not self.head:
if ptr is p:
self.current = p
self.remove_current_node()
break
ptr = ptr.next
def remove_first(self) -> None:
"""머리 노드 삭제"""
self.current = self.head.next # 머리 노드 head.next를 삭제
self.remove_current_node()
def remove_last(self) -> None:
"""꼬리 노드 삭제"""
self.current = self.head.prev # 꼬리 노드 head.prev를 삭제
self.remove_current_node()
def clear(self) -> None:
"""모든 노드를 삭제"""
while not self.is_empty(): # 리스트 전체가 빌 때까지
self.remove_first() # 머리 노드를 삭제
self.no = 0
def __iter__(self) -> DoubleLinkedList:
"""이터레이터를 반환"""
return DoubleLinkedListIterator(self.head)
def __reversed__(self) -> DoubleLinkedListReverseIterator:
"""내침차순 이터레이터를 반환"""
return DoubleLinkedListReverseIterator(self.head)
class DoubleLinkedListIterator:
"""DoubleLinkedList의 이터레이터용 클래스"""
def __init__(self, head: Node):
self.head = head
self.current = head.next
def __iter__(self) -> DoubleLinkedListIterator:
return self
def __next__(self) -> Any:
if self.current is self.head:
raise StopIteration
else:
data = self.current.data
self.current = self.current.next
return data
class DoubleLinkedListReverseIterator:
"""DoubleLinkedList의 내림차순 이터레이터 클래스"""
def __init__(self, head: Node):
self.head = head
self.current = head.prev
def __iter__(self) -> DoubleLinkedListReverseIterator:
return self
def __next__(self) -> Any:
if self.current is self.head:
raise StopIteration
else:
data = self.current.data
self.current = self.current.prev
return data
실행 프로그램은 기존 연결 리스트와 비슷해서 구현 안 함.
노드와 연결 리스크는 분리해서 생각하기
연결 리스트를 생성하고, 그 안에 노드를 생성해서 연결시킴
노드 자체가 연결 리스트가 아님
자료 구조를 hands on하면 좋은 점
각 자료구조의 구조를 이해하기 쉬움!
(자료 저장 방식이나 필요한 메서드 등)
포인터를 이용한 연결 리스트 구현 완료
커서를 이용한 연결 리스트도 구현해 봐야함(이건 나중에)
키를 값에 매핑할 수 있는 구조
연관 배열 추상 자료형(ADT)을 구현하는 자료 구조
배열과 해시 함수를 사용하여 map을 구현한 자료 구조
용어 정리
해싱(hashing): 데이터를 저장할 위치(인덱스)를 간단한 연산으로 구하는 것
해시값(hash): 해싱을 통해 계산된 값 (digest 값이라고도 함)
해시 테이블: 해시값을 인덱스로 해서 원소를 새로 저장한 배열
해시 함수: 키를 해시값으로 변환하는 과정
버킷(bucket): 해시 테이블에서 만들어진 원소
충돌(collision): 저장할 버킷이 충돌되는 현상(다른 입력값에 동일한 해시가 계산)
체인법(chaning, open hashing): 해시값이 같은 원소를 연결 리스트로 관리
오픈 주소법(open address): 빈 버킷을 찾을 때까지 해시를 반복
추상 자료형이란?
구현 방법은 명시하지 않고 데이터의 구조와 연산에 대한 명세
ADT는 마치 추상 클래스와 비슷하다!
자료구조를 만들기 위한 이데아 같은 느낌?
해시 함수
임의의 크기의 데이터를 고정된 크기의 값으로 매핑
해시 테이블의 핵심은 해시 함수
해시 함수를 사용하는 것을 해싱이라고 한다.
해시 테이블 안에서 해시 함수는 임의의 데이터를 정수로 변환
(해시 테이블 이외에서 사용되는 해시 테이블은 꼭 정수를 반환하는 건 아님)
좋은 해시 함수의 특징
해시 충돌(Hash Collision)
서로 다른 입력 데이터가 동일한 해시 코드로 매핑되는 상황
key는 다른데 hash가 같다
이것 뿐만 아니라 해시를 모듈러 연산했을 때 나오는 값이 같은 경우도 해시 충돌
해시 충돌을 해결하는 방법
무식한 방법은 해시 테이블을 충분히 크게 하는 것!(하지만 메모리 낭비)
충돌을 피하려면 해시 함수가 해시 테이블 크기보다 작거나 같은 정수를 고르게 생성해야 함. 그래서 테이블 크기는 소수를 권장
1. 개별 체이닝(separate chaining)
연결 리스트를 활용
충돌 발생 시 같은 해시 코드에 여러 값을 저장, 검색 시 해당 버킷 연결 리스트를 순회
2. 개방 주소법(Open Address, 더 효율적)
충돌이 발생하면 빈 버킷을 찾아 데이터를 저장
(여러 방법이 있음, linear probing(선형 탐사법) 기준으로 정리)
해시가 충돌하면 충돌되지 않을 때까지 재해시 함.
검색할 원래 버킷을 검색하고 다음 버킷이 비어있을 때까지 순차적으로 검색
(개방 주소법으로 값을 저장했다면 다음 버킷이 비어있을 수 없음)
개방 주소법에서는 값을 삭제하면 밀려났던 값을 데리고 오던가,
지워버린 값에 더미 데이터를 채워줘야 한다.
그렇지 않으면 제대로 검색 과정을 거치지 못하니까.
해시 테이블에 저장 시 데이터와 해시도 같이 저장함.
그러면 데이터 접근 시 저장된 데이터에 대해서는 해시 함수를 실행하지 않고, 저장된 해시를 활용할 수 있음.
해시의 시간 복잡도
일반적으로는 O(1)
그러나 해시 충돌로 체이닝이 길어지면 O(n)
해시는 어디에 쓰나?
데이터베이스 인덱싱, 캐싱, 집합 및 맵, 보안(암호화 및 데이터 무결성 검사)
# 체인법으로 해시 함수 구현하기
from __future__ import annotations
from typing import Any, Type
import hashlib
class Node:
"""해시를 구성하는 노드"""
def __init__(self, key: Any, value: Any, next: Node) -> None:
"""초기화"""
self.key = key
self.value = value
self.next = next
class ChaninedHash:
"""체인법으로 해시 클래스 구현"""
def __init__(self, capacity: int) -> None:
"""초기화"""
self.capacity = capacity # 해시 테이블의 크기를 지정
self.table = [None] * self.capacity # 해시 테이블(리스트)을 선언
def hash_value(self, key: Any) -> int:
"""해시값을 구함"""
if isinstance(key, int):
return key % self.capacity
return (int(hashlib.sha(str(key).encode()).hexidigest(), 16) % self.capacity)
def search(self, key: Any) -> Any:
"""키가 key인 원소를 검색하여 값을 반환"""
hash = self.hash_value(key) # 검색하는 키의 해시값
p = self.table[hash] # 노드를 주목
while p is not None:
if p.key == key:
return p.value # 검색 성공
p = p.next
return None # 검색 실패
def add(self, key: Any, value: Any) -> bool:
"""키가 key이고 값이 value인 원소를 추가"""
hash = self.hash_value(key) # 추가하는 key의 해시값
p = self.table[hash]
while p is not None:
if p.key == key:
return False
p = p.next # 뒤쪽 노드를 주목
temp = Node(key, value, self.table[hash])
self.table[hash] = temp # 노드를 추가
return True # 추가 성공
def remove(self, key: Any) -> bool:
"""키가 key인 원소를 삭제"""
hash = self.hash_value(key) # 삭제할 key의 해시값
p = self.table[hash] # 노드를 주목
pp = None # 바로 앞의 노드를 주목
while p is not None:
if p.key == key: # key를 발견하면 아래를 실행
if pp is None:
self.table[hash] = p.next
else:
pp.next = p.next
return True
pp = p # 뒤쪽 노드를 주목
p = p.next # 삭제 실패(key가 존재하지 않음)
return False
def dump(self) -> None:
"""해시 테이블을 덤프"""
for i in range(self.capacity):
p = self.table[i]
print(i, end='')
while p is not None:
print(f' -> {p.key} ({p.value})', end='')
p = p.next
print()
해시 테이블(체이닝) 테스트
# 체인법으로 해시 함수 구현하기
from __future__ import annotations
from typing import Any, Type
import hashlib
class Node:
"""해시를 구성하는 노드"""
def __init__(self, key: Any, value: Any, next: Node) -> None:
"""초기화"""
self.key = key
self.value = value
self.next = next
class ChaninedHash:
"""체인법으로 해시 클래스 구현"""
def __init__(self, capacity: int) -> None:
"""초기화"""
self.capacity = capacity # 해시 테이블의 크기를 지정
self.table = [None] * self.capacity # 해시 테이블(리스트)을 선언
def hash_value(self, key: Any) -> int:
"""해시값을 구함"""
if isinstance(key, int):
return key % self.capacity
return (int(hashlib.sha(str(key).encode()).hexidigest(), 16) % self.capacity)
def search(self, key: Any) -> Any:
"""키가 key인 원소를 검색하여 값을 반환"""
hash = self.hash_value(key) # 검색하는 키의 해시값
p = self.table[hash] # 노드를 주목
while p is not None:
if p.key == key:
return p.value # 검색 성공
p = p.next
return None # 검색 실패
def add(self, key: Any, value: Any) -> bool:
"""키가 key이고 값이 value인 원소를 추가"""
hash = self.hash_value(key) # 추가하는 key의 해시값
p = self.table[hash]
while p is not None:
if p.key == key:
return False
p = p.next # 뒤쪽 노드를 주목
temp = Node(key, value, self.table[hash])
self.table[hash] = temp # 노드를 추가
return True # 추가 성공
def remove(self, key: Any) -> bool:
"""키가 key인 원소를 삭제"""
hash = self.hash_value(key) # 삭제할 key의 해시값
p = self.table[hash] # 노드를 주목
pp = None # 바로 앞의 노드를 주목
while p is not None:
if p.key == key: # key를 발견하면 아래를 실행
if pp is None:
self.table[hash] = p.next
else:
pp.next = p.next
return True
pp = p # 뒤쪽 노드를 주목
p = p.next # 삭제 실패(key가 존재하지 않음)
return False
def dump(self) -> None:
"""해시 테이블을 덤프"""
for i in range(self.capacity):
p = self.table[i]
print(i, end='')
while p is not None:
print(f' -> {p.key} ({p.value})', end='')
p = p.next
print()
해시 테이블(체이닝) 시각화
0
1 -> 14 (민서) -> 1 (수연)
2
3
4
5 -> 5 (동혁)
6
7
8
9
10 -> 10 (예지)
11
12 -> 12 (원준)
해시 테이블은 배열인 것이었다!
그리고 배열에는 단순히 값이 들어가는 게 아니라 노드 또는 버킷이 들어간다.
(배열 하나 하나를 버킷이라고 칭하고, 체이닝의 경우 버킷 안에 노드로 이루어진 연결 리스트가 들어가고, 개방 주소법은 버킷 자체가 들어간다.)
# 오픈 주소법으로 해시 함수 구현하기
from __future__ import annotations
from typing import Any, Type
from enum import Enum
import hashlib
# 버킷의 속성
class Status(Enum):
OCCUPIED = 0 # 데이터를 저장
EMPTY = 1 # 비어있음
DELETED = 2 # 삭제 완료
class Bucket:
"""해시를 구성하는 버킷"""
def __init__(self, key: Any = None, value: Any = None, stat: Status = Status.EMPTY) -> None:
"""초기화"""
self.key = key # 키
self.value = value # 값
self.stat = stat
def set(self, key: Any, value: Any, stat: Status) -> None:
"""모든 필드에 값을 설정"""
self.key = key # 키
self.value = value # 값
self.stat = stat # 속성
def set_status(self, stat: Status) -> None:
"""속성을 설정"""
self.stat = stat
class OpenHash:
"""오픈 주소법으로 구현하는 해시 클래스"""
def __init__(self, capacity: int) -> None:
"""초기화"""
self.capacity = capacity # 해시 테이블의 크기를 지정
self.table = [Bucket()] * self.capacity # 해시 테이블
def hash_value(self, key: Any) -> int:
"""해시값을 구함"""
if isinstance(key, int): # key객체가 int형과 호환되는지
return key % self.capacity
return (int(hashlib.md5(str(key).encode()).hexidigest(), 16) % self.capacity)
def rehash_value(self, key: Any) -> int:
"""재해시값을 구함"""
return (self.hash_value(key) + 1) % self.capacity
def search_bucket(self, key: Any) -> Any:
"""키가 key인 버킷을 검색"""
hash = self.hash_value(key) # 검색하는 키의 해시값
p = self.table[hash] # 버킷을 주목
for i in range(self.capacity):
if p.stat == Status.EMPTY:
break
elif p.stat == Status.OCCUPIED and p.key == key:
return p
hash = self.rehash_value(hash) # 재해시
p = self.table[hash]
return None
def search(self, key: Any) -> Any:
"""키가 key인 원소를 검색하여 값을 반환"""
p = self.search_bucket(key)
if p is not None:
return p.value # 검색 성공
else:
return None # 검색 실패
def add(self, key: Any, value: Any) -> bool:
"""키가 key이고 값이 value인 원소를 추가"""
if self.search(key) is not None:
return False # 이미 등록된 키
hash = self.hash_value(key) # 추가하는 키의 해시값
p = self.table[hash] # 버킷을 주목
for i in range(self.capacity):
if p.stat == Status.EMPTY or p.stat == Status.DELETED:
self.table[hash] = Bucket(key, value, Status.OCCUPIED)
return True
hash = self.rehash_value(hash) # 재해시
p = self.table[hash]
return False # 해시 테이블이 가득참
def remove(self, key: Any) -> int:
"""키가 key인 원소를 삭제"""
p = self.search_bucket(key) # 버킷을 주목
if p is None:
return False # 이 키는 등록되어 있지 않음
p.set_status(Status.DELETED)
return True
def dump(self) -> None:
"""해시 테이블을 덤프"""
for i in range(self.capacity):
print(f'{i:2} ', end=' ')
if self.table[i].stat == Status.OCCUPIED:
print(f'{self.table[i].key} ({self.table[i].value})')
elif self.table[i].stat == Status.EMPTY:
print('-- 미등록 --')
elif self.table[i].stat == Status.DELETED:
print('-- 삭제 완료 --')
해시 테이블(개방 주소법) 테스트
# 오픈 주소법을 구현하는 해시 클래스 Openhash 사용
from enum import Enum
from open_hash import OpenHash
Menu = Enum('Menu', ['추가', '삭제', '검색', '덤프', '종료']) # 메뉴를 선억
def select_menu() -> Menu:
"""메뉴 선택"""
s = [f'({m.value}){m.name}' for m in Menu]
while True:
print(*s, sep=' ', end='')
n = int(input(': '))
if 1 <= n <= len(Menu):
return Menu(n)
hash = OpenHash(13) # 크기가 13인 해시 테이블 생성
while True:
menu = select_menu()
if menu == Menu.추가:
key = int(input('추가할 키를 입력하세요: '))
val = input('추가할 값을 입력하세요: ')
if not hash.add(key, val):
print('추가를 실패했습니다.')
elif menu == Menu.삭제:
key = int(input('삭제할 키를 입력하세요: '))
if not hash.remove(key):
print('삭제를 실패했습니다!')
elif menu == Menu.검색:
key = int(input('검색할 키를 입력하세요: '))
t = hash.search(key)
if t is not None:
print(f'검색한 키를 갖는 값은 {t}입니다.')
else:
print('검색할 데이터가 없습니다.')
elif menu == Menu.덤프:
hash.dump()
else:
break
해시 테이블(개방 주소법) 시각화
0 -- 미등록 --
1 1 (수연)
2 14 (민서)
3 -- 미등록 --
4 -- 미등록 --
5 5 (동혁)
6 -- 미등록 --
7 -- 미등록 --
8 -- 미등록 --
9 -- 미등록 --
10 10 (예지)
11 -- 미등록 --
12 12 (원준)
처음으로 손 코딩 해봄
(재귀 함수는 단일 원소만 출력하고 배열은 출력 안함(오답이겠지?)
# fin(n)만 제출함
def fib(n):
if n <= 2:
return 1
return fib(n-1) + fib(n-2)
# 이거 추가했으면 됐을 듯
def print_fib(n):
for i in range(1, n+1):
print(fib(i))
print_fib(10)
정렬은 과정 하나 하나를 분해하면서 공부해야 겠음
(시험 풀면서 퀵 소트 과정 다시 정리되었음)
해시는 진도를 못 나가서 못 풀었음(덕분에 해시는 더 빡세게 공부하게 됨)
재귀 어려웠는데 퀴즈로 나오니까 두뇌 풀가동해서 풀었음.
그러더니 갑자기 재귀의 아하 모먼트가 온 것 같음!!
피보나치 재귀 이해 제대로 못했는데 퀴즈 풀면서 두뇌 풀가동해서 깨달음!!
새롭게 알게된 용어
메모이제이션(memoization)