빅오 표기법
시간 복잡도의 존재 이유
연결 리스트 : prev 포인터와 next 포인터로 앞 뒤의 노드를 연결시킨 것
1. 싱글 연결 리스트
2. 이중 연결 리스트
3. 원형 이중 연결 리스트
장점:
배열과 달리 새로운 자료, 즉 노드를 뒤에 연결하거나 중간에 노드 몇개를 껴넣는 것이 쉽다.
단점:
자료 번호가 없이 연결 관계만 있기 때문에 특정한 노드를 불러내기가 어렵다
from collections import deque
a = deque()
for i in range(10):
a.append(i)
for i in range(10):
a.appendleft(i)
it = iter(a)
next(it)
a.insert(1, 1000)
print(*a)
a.popleft()
a.pop()
print(*a)
iter, next
iter는 객체의 iter 메서드를 호출해주고, next는 객체의 next 메서드를 호출해줍니다
iter는 반복을 끝낼 값을 지정하면 특정 값이 나올 때 반복을 끝냅니다. 이 경우에는 반복 가능한 객체 대신 호출 가능한 객체(callable)를 넣어줍니다. 참고로 반복을 끝낼 값은 sentinel이라고 부르는데 감시병이라는 뜻입니다. 즉, 반복을 감시하다가 특정 값이 나오면 반복을 끝낸다고 해서 sentinel입니다.
예를 들어 random.randint(0, 5)와 같이 0부터 5까지 무작위로 숫자를 생성할 때 2가 나오면 반복을 끝내도록 만들 수 있습니다. 이때 호출 가능한 객체를 넣어야 하므로 매개변수가 없는 함수 또는 람다 표현식으로 만들어줍니다.
>>> import random
>>> it = iter(lambda : random.randint(0, 5), 2)
>>> next(it)
0
>>> next(it)
3
>>> next(it)
1
>>> next(it)
Traceback (most recent call last):
File "<pyshell#37>", line 1, in <module>
next(it)
StopIteration
next(it)로 숫자를 계속 만들다가 2가 나오면 StopIteration이 발생합니다. 물론 숫자가 무작위로 생성되므로 next(it)를 호출하는 횟수도 매번 달라집니다. 물론 다음과 같이 for 반복문에 넣어서 사용할 수도 있습니다.
>>> import random
>>> for i in iter(lambda : random.randint(0, 5), 2):
... print(i, end=' ')
...
3 1 4 0 5 3 3 5 0 4 1
이렇게 iter 함수를 활용하면 if 조건문으로 매번 숫자가 2인지 검사하지 않아도 되므로 코드가 좀 더 간단해집니다. 즉, 다음 코드와 동작이 같습니다.
import random
while True:
i = random.randint(0, 5)
if i == 2:
break
print(i, end=' ')
next는 기본값을 지정할 수 있습니다. 기본값을 지정하면 반복이 끝나더라도 StopIteration이 발생하지 않고 기본값을 출력합니다. 즉, 반복할 수 있을 때는 해당 값을 출력하고, 반복이 끝났을 때는 기본값을 출력합니다. 다음은 range(3)으로 0, 1, 2 세 번 반복하는데 next에 기본값으로 10을 지정했습니다.
>>> it = iter(range(3))
>>> next(it, 10)
0
>>> next(it, 10)
1
>>> next(it, 10)
2
>>> next(it, 10)
10
>>> next(it, 10)
10
0, 1, 2까지 나온 뒤에도 next(it, 10)을 호출하면 예외가 발생하지 않고 계속 10이 나옵니다.
지금까지 반복 가능한 객체의 개념과 이터레이터를 만드는 방법을 배웠습니다. 여기서는 이터레이터를 만들 때 iter, next 메서드 또는 getitem 메서드를 구현해야 한다는 점만 기억하면 됩니다.
정적배열 기반
랜덤 접근과 순차적 접근
배열과 연결 리스트 비교
배열: 상자를 순서대로 나열한 데이터 구조, 인덱스만 알면 해당 값을 알 수 있다
연결 리스트: 상자를 선으로 연결한 형태의 데이터 구조, 내부 요소 알기 위해선 내부 확인을 해봐야함
v = []
for i in range(1, 11):
v.append(i)
print(*v)
v.pop()
print(*v)
v = v[1:]
print(*v)
if 100 not in v:
print("not found")
v = [10] * len(v)
print(*v)
v.clear()
print(*v)
# 1 2 3 4 5 6 7 8 9 10
# 1 2 3 4 5 6 7 8 9
# 2 3 4 5 6 7 8 9
# not found
# 10 10 10 10 10 10 10 10
stack = []
for i in range(10):
stack.append(i)
while stack:
print(stack.pop(), end=" ")
# 9 8 7 6 5 4 3 2 1 0
from collections import deque
q = deque()
for i in range(10):
q.append(i)
while q:
print(q.popleft(), end=" ")
# 0 1 2 3 4 5 6 7 8 9
정점과 간선
단방향 간선과 양방향 간선
가중치
트리의 특징
트리의 높이와 레벨
이진 트리
자식의 노드 수가 두 개 이하인 트리
정이진 트리: 자식 노드가 0 또는 두 개인 이진트리
완전 이진 트리: 왼쪽에서부터 채워져 있는 이진 트리. 마지막 레벨을 제외하고는 모든 레벨이 완전히 채워져 있고 마지막 레벨의 경우 왼쪽부터 채워져 있다
변질 이진 트리: 자식 노드가 하나밖에 없는 이진 트리
포화 이진 트리: 모든 노드가 꽉 차 있는 이진 트리
균형 이진 트리: 왼쪽과 오른쪽 노드의 높이 차이가 1 이하인 이진 트리. map, set을 구성하는 레드 블랙 트리는 균형 이진 트리 중 하나
이진탐색트리(BST)는 노드의 오른쪽 하위 트리에는 '노드 값보다 큰 값'이 있는 노드만 포함되고, 왼쪽하위 트리에는 '노드 값보다 작은 값'이 들어 있는 트리
검색에 용이
탐색 시 평균 시간 복잡도: O(logn), 최악 시: O(n)
왜냐하면 삽인 순서에 따라 선형적일 수 있기 때문
균형을 잡는데 쓰이는 규칙
모든 리프 노드와 루트 노드는 블랙이고 어떤 노드가 레드이면 그 노드의 자식은 반드시 블랙이다(블랙의 자식은 블랙일 수 있다)
특정 노드에서 후손 노드로 가는 경로는 동일한 수의 검정색 노드가 존재한다
완전 이진 트리 기반의 자료 구조
최소힙과 최대힙 두 가지가 있고 해당 힙에 따라 특정한 특징을 지닌 트리
우선순위 큐는 우선순위 대기열이라고도 한다
대기열에서 우선순위가 놓은 요소가 우선순위가 낮은 요소보다 먼저 제공되는 자료구조
# 최소힙
import heapq
heap = []
heapq.heappush(heap, 5)
heapq.heappush(heap, 4)
heapq.heappush(heap, 3)
heapq.heappush(heap, 2)
heapq.heappush(heap, 1)
print(heap[0])
# 1 출력
# 최대힙
import heapq
heap = []
heapq.heappush(heap, -5)
heapq.heappush(heap, -4)
heapq.heappush(heap, -3)
heapq.heappush(heap, -2)
heapq.heappush(heap, -1)
print(-heap[0])
# 5 출력
특정 순서에 따라 키와 매핑된 값의 조합
레드블랙 트리 자료구조를 기반으로 형성되고 삽입 시 자동 정렬됨
map은 해시 테이블을 구현할 때 쓰며 정렬을 보장하지 않는 unordered_map과 정렬을 보장하는 map 두가지가 있다(c++)
v = [0] * 10
umap = {
"test1": 4,
"test2": 10
}
for key, value in umap.items():
print(key, "::", value)
print(umap["test1"])
셋은 특정 순서에 따라 고유한 요소를 저장하는 컨테이너이며 중복되는 요소는 없고 오로지 희소한 값만 저장하는 자료구조
_set = set()
_set.add(("test", 1))
_set.add(("test", 1))
_set.add(("test", 1))
_set.add(("test", 1))
print(len(_set))
# 1
해시 테이블은 무한에 가까운 데이터들을 유한한 개수의 해시 값으로 매핑한 테이블. 삽입, 삭제, 탐색 시 평균적으로 O(1) 시간 복잡도 가진다
해싱인란 임의의 길이의 값을 해시함수
를 사용하여 고정된 크기의 값으로 변환하는 작업을 말한다
해시 테이블이란 해시함수를 사용하여 변환한 값을 index로 삼아 키와 데이터를 저장하는 자료구조를 말한다.
해시 테이블이 빠른 검색속도를 제공하는 이유는 내부적으로 배열(버킷)을 사용하여 데이터를 저장하기 때문이다.
이러한 해싱 구조로 데이터를 저장하면 key값으로 데이터를 찾을 시 함수를 1번만 수행하면 되므로 빠르게 삽입/삭제/조회 할 수 있다
해시테이블의 평균 시간복잡도는 O(1)이다
해시테이블은 키값을 해시함수를 통해 해시값을 얻는 방식이기 때문에 다른 키값이 같은 해시값을 가지는 충돌의 문제가 생긴다
충돌을 이해하기 위해선 먼저 적재율에 대해 알아야한다
개방주소법은 해시값 충돌 시 비어있는 해시 테이블의 공간을 탐사(probe)하여 할당하는 방식이다
개방 주소법은 다시 3가지 기법으로 나뉜다
클러스터링
문제가 발생하고 이로 인해 탐색과 삭제가 느려진다클러스터링
문제가 발생하게 된다대표적 해시함수 4가지
h(k)=k mod N
h(k) = [N(kA mod 1)]
kA mod 1의 의미는 kA의 소수점 이하 부분을 말하며 이를 N에 곱하므로 0부터 N사이의 값이 된다. 이방법의 장점은 N이 어떤 값이더라도 잘 작동하는다는 것이며 A를 잘 잡는 것이 중요하다
해시맵은 맵과 유사하지만 내부 구현 방식이 다르다
해시맵은 덕분에 빠른 검색 속도를 제공한다. 또한 null값을 키로 사용가능하다
많은 데이터를 빠르게 검색할 시 사용된다
캐시(cache) 구현: 해시맵은 검색 속도가 빠르기 때문에, 캐시 구현에 많이 사용됩니다. 캐시는 데이터를 미리 저장해 놓고, 이후에 동일한 요청이 들어올 경우 저장된 데이터를 반환하여 속도를 향상시키는 역할을 합니다.
검색과 연관된 작업: 해시맵은 검색과 관련된 작업에서 매우 유용합니다. 예를 들어, 특정 키를 가지는 데이터를 빠르게 검색하거나, 키에 해당하는 값을 수정하거나 삭제하는 작업 등을 수행할 수 있습니다.
대용량 데이터 처리: 해시맵은 대용량 데이터를 처리할 때 매우 효율적입니다. 이는 내부 구현 방식 때문인데, 해시맵은 해시 함수를 사용하여 데이터를 저장하므로 검색 속도가 매우 빠릅니다.
자료구조 구현: 해시맵은 자료구조를 구현할 때 매우 유용합니다. 예를 들어, 집합(set)이나 다중 집합(multiset)을 구현할 때 해시맵을 사용할 수 있습니다. 또한, 그래프에서 노드를 관리하는 경우에도 해시맵을 사용할 수 있습니다.
성능이 중요한 작업: 해시맵은 내부적으로 배열을 사용하므로, 빠른 속도를 보장합니다. 따라서, 대용량 데이터를 빠르게 처리해야 하는 경우나, 성능이 중요한 작업을 수행해야 하는 경우에도 해시맵을 사용할 수 있습니다.
예를 들어, 온라인 쇼핑몰에서 상품을 검색하는 기능을 구현해야 한다고 가정해보겠습니다. 이때, 사용자가 입력한 검색어와 일치하는 상품을 빠르게 찾아야 하므로 해시맵을 사용할 수 있습니다.
해시맵을 사용하면 검색 속도가 빠르기 때문에, 수백만 개의 상품 정보를 빠르게 검색할 수 있습니다. 또한, 사용자가 입력한 검색어를 키(key)로 사용하여 해당하는 상품 정보를 값(value)으로 저장하면, 사용자가 다시 같은 검색어를 입력했을 때 더 빠르게 결과를 반환할 수 있습니다.
또 다른 예로는, 소셜 미디어에서 해시태그 검색 기능을 구현하는 경우가 있습니다. 해시태그는 대용량 데이터이기 때문에, 검색 속도가 빠른 해시맵을 사용하면 빠르게 결과를 반환할 수 있습니다. 해시태그를 키(key)로 사용하여 해당하는 게시물 정보를 값(value)으로 저장하면, 사용자가 특정 해시태그를 검색할 때 해당하는 게시물을 빠르게 찾아볼 수 있습니다.
해시테이블이란?
AVL 트리
레드블랙 트리
우선순위 큐와 힙
해시맵
해시맵과 해시테이블
자료구조 면접질문