key: 출석번호, value: 이름
(3, 문지은)
(5, 박서희)
(6, 최지수)
(7, 송아람)
직접 주소화 방법을 통해 key-value 쌍의 데이터를 저장하고자 하면 많은 문제가 발생한다.
문제점 1) 불필요한 공간 낭비
key: 학번, value: 이름
(2022390, 문지은)
(2022392, 박서희)
(2022393, 송아람)
(2022401, 최지수)
문제점 2) Key값으로 문자열이 올 수 없다.
key: ID, value: 이름
(jieun123, 문지은)
(jisoo123, 최지수)
(quokka123, 박서희)
(aram123, 송아람)
💡 Separate Chaining vs Open Addressing
- Separate Chaining
- 데이터 삽입 시, 버킷 혹은 슬롯에 연결리스트를 할당
- 만약 같은 해시값에 의해 해시 충돌이 발생하면, 연결리스트를 이어서 해시 충돌을 방지하고자 하는 방식
- 버킷이 모두 채워진 상태여도, 연결리스트로 데이터를 연결 짓기 때문에 한 번 정해진 데이터의 주솟값은 영구적으로 유지되며, 이를 Closed Addressing이라고도 부른다.
- 장점
- 단순한 연결리스트만을 활용한다.
- 해시테이블이 채워지면서, 탐색에 대한 성능 저하가 선형적으로 발생한다.
- Open Addressing
- 데이터를 삽입할 때, 버킷 혹은 슬롯에 연결리스트를 만들지 않는다. 다만, 해시 충돌이 발생한다면, 다른 비어있는 버킷에 데이터를 저장한다.
- 선형 탐색, 제곱 탐색, 이중 해시 등 비어있는 버킷 탐색 방식에 따라 내부 알고리즘이 다를 수 있다.
- 장점
- 해시테이블 자체만을 저장공간으로 활용하기 때문에, 추가적인 저장공간이 필요하지 않다.
- 삽입, 삭제 시, 발생하는 오버헤드가 적다.
- 상대적으로 데이터가 적을 때, 체이닝 방식보다 유리하다.
Hash table | Linked list | Array | |
---|---|---|---|
access | |||
insert | |||
append | |||
delete |
# {'a':1, 'b':2, 'c':3}
hash_table = {'a':1, 'b':2, 'c':3}
hash_table = dict(a=1, b=2, c=3)
dictionary[key] = value
같은 형식으로 각 key-value 쌍을 입력하면 된다.# 예시) (학번, 이름)을 (key, value)로 가지는 딕셔너리 만들기
# 원하는 결과 : {2022390: '문지은', 2022392: '박서희', 2022393: '송아람', 2022401: '최지수'}
student_info = {}
student_info[2022390] = "문지은"
student_info[2022392] = "박서희"
student_info[2022393] = "송아람"
student_info[2022401] = "최지수"
a = dict(one=1, two=2, three=3)
b = {'one': 1, 'two': 2, 'three': 3}
c = dict(zip(['one', 'two', 'three'], [1, 2, 3]))
d = dict([('two', 2), ('one', 1), ('three', 3)])
e = dict({'two':2, 'three':3}, two=2)
# a == b == c == d == e
# list처럼 컴프리헨션도 사용할 수 있다.
f = {i:i**2 for i in range(4) if (i % 2 == 0)}
print(f) # {0:0, 2:4}
# 사용 예시 (70점 이상의 점수를 받은 학생들에 대한 정보를 담은 dictionary 만들기)
name = ['bob', 'sam', 'maria', 'david', 'nancy']
score = [30, 50, 80, 92, 83]
result_dict = {name[i]:score[i] for i in range(len(name)) if score[i] >= 70}
print(result_dict) # {'maria': 80, 'david': 92, 'nancy': 83}
위에 만들었던 예시를 이용해서 주요 메서드의 사용 방법을 알아보자.
student_info = {}
student_info[2022390] = "문지은"
student_info[2022392] = "박서희"
student_info[2022393] = "송아람"
student_info[2022401] = "최지수"
print(student_info)
# {2022390: '문지은', 2022392: '박서희', 2022393: '송아람', 2022401: '최지수'}
print(student_info.items())
# dict_items([(2022390, '문지은'), (2022392, '박서희'), (2022393, '송아람'), (2022401, '최지수')])
for student_id, name in student_info.items():
print(student_id, name)
'''
2022390 문지은
2022392 박서희
2022393 송아람
2022401 최지수
'''
print(student_info.keys())
# dict_keys([2022390, 2022392, 2022393, 2022401])
for student_id in student_info.keys():
print(student_id)
'''
2022390
2022392
2022393
2022401
'''
print(student_info.values())
# dict_values(['문지은', '박서희', '송아람', '최지수'])
for name in student_info.values():
print(name)
'''
문지은
박서희
송아람
최지수
'''
print(student_info.get(2022390)) # 문지은
print(student_info.get(1111)) # None
print(student_info.get(1111, "서지현")) # 서지현
if 3 not in a:
a[3] = 1
else:
a[3] += 1
a[3] = 1 + a.get(3, 0)
for ... in ...
문에서 사용하는 경우를 제외하고, in
연산자는 포함 검사에 사용된다.in
+ (container)를 하면 해당 컨테이너(container)에서 특정 값(value)이 존재하는지에 대한 여부를 판단해서 True
혹은 False
로 알려주는 연산자이다.in
연산자가 사용된 예시를 보여주고 있습니다.n = [i for i in range(100)]
if 25 in n:
print("Yes")
else:
print("No")
s = "I like an apple"
if 'z' in s:
print("Yes")
else:
print("No")
q = [i for i in range(10)]
while 4 in q:
print("Popped element :", q.pop())
in
연산자는 key가 존재하는지 확인 해준다.True
를 반환하고 존재하지 않으면 False
를 반환한다.if 2022390 in student_info:
print("학생이 존재합니다")
else:
print("학생이 존재하지 않습니다")
in
연산자를 사용하면 의 시간복잡도를 가지기 때문에 매우 효율적이다.in
연산자를 사용하는 경우가 많다.li = [6, 9, 1000, 28, 4, 27, 45, 51, 16]
li_d = {i:True for i in li}
# li_d = {6:True, 9:True, 1000:True, 28:True, 4:True, 27:True, 45:True, 51:True, 16:True}
if 4 in li: # list와 in 연산자의 시간복잡도 : O(n)
if 4 in li_d: # dictionary와 in 연산자의 시간복잡도 : O(1)
💡 frozenset()
- set과 같이 집합을 나타내는 내장 클래스로, 인자로 iterable 객체를 넘기면 set 객체를 반환
- 하지만, set과 다르게 frozenset은 immutable 하기 때문에, 데이터 삽입 및 삭제 같은 변형이 불가능하다.
items = ["apple", "banana", "orange", "melon"] fruit_set = frozenset(items) fruit_set.add("New fruit")
결과
AttributeError: 'frozenset' object has no attribute 'add'
- frozenset에는 집합 연산 메서드가 없기 때문에 위와 같이 에러를 발생시킨다.
- frozenset을 사용하는 이유는 해시 가능한(hashable) 특성 때문인데, 고유한 값을 가지기 때문에 dictionary에 key로 사용할 수 있다.
snack_set = frozenset(["cookie", "chips", "icecream", "cereal"]) my_bag = {fruit_set:"Buy only these fruits", snack_set:"Snacks for Tom"} print(my_bag) # {frozenset({'melon', 'apple', 'orange', 'banana'}): 'Buy only these fruits', frozenset({'chips', 'cereal', 'icecream', 'cookie'}): 'Snacks for Tom'}
# 비어있는 집합 선언
a = set()
a.add(5) # 데이터 삽입
a.add(2)
a.add(4)
a.add(5) # 중복 데이터 삽입 시도
print(a)
{5, 2, 4} # 중복 데이터는 존재x
# set 선언과 동시에 초기화하기
a = {2, 4, 6, 5, 7}
b = {2, 4, 4, 6, 7, 5, 5}
# -> 결과 : a == b
🚧 올바른 set 선언하기
- 흔히 set이
{}
으로 묶이기 때문에 초기화할 때,a = {}
처럼 사용하는 경우가 많다.- 이것은 공식적으로 비어있는 dictionary 선언에 해당하는 문법이기 때문에, 위의 예시와 같이
set()
을 사용하는 것이 맞다.- 하지만
a = {}
형식으로 쓴다고 에러가 나는 것은 아니기 때문에 사용은 가능하다.- 분명하게 자료구조에 대한 표현을 해주기 위해
set()
을 사용하는 것을 권장
# 컴프리헨션
a = {i**2 for i in range(5)}
# iterable 객체 사용
a = set("abadcfesdf") # 문자열
b = set([1, 7, 5, 3, 2, 2, 8, 1]) # 리스트
c = set((2, 7, 4, 7, 5, 2)) # 튜플
a = set()
b = set()
a.add(4) # 삽입
a.pop() # 가장 앞에 배치된 데이터 반환 및 제거
a.remove() # 원하는 데이터 제거 (없는 데이터 제거 시도할 경우, 에러 발생)
a.discard() # 원하는 데이터 제거 (없는 데이터 제거 시도해도 에러 발생하지 않음)
# 집합 연산
c = a.intersection(b) == a & b # a와 b의 교집합
c = a.union(b) == a | b # a와 b의 합집합
c = a - b # a의 b에 대한 차집합
c = a.isdisjoint(b) # a와 b의 서로소 집합 관계 확인하기
collections
패키지로부터 불러와야 한다.from collections import Counter
# 선언
c = Counter()
# 문자열
c = Counter("abcbdbab")
print(c) # Counter({'b':4, 'a':2, 'c':1, 'd':1})
# 리스트
c = Counter(['banana', 'apple', 'apple', 'kiwi'])
print(c) # Counter({'apple': 2, 'banana': 1, 'kiwi': 1})
# 딕셔너리
c = Counter({'dogs':4, 'cats':2})
print(c) # Counter({'dogs':4, 'cats':2})
from collections import defaultdict
a = defaultdict(list)
# 결과 : a == defaultdict(<class 'list'>, {})
a[1].append(2)
# 결과 : defaultdict(<class 'list'>, {1:[2]})
a[2].append(3)
# 결과 : defaultdict(<class 'list'>, {1:[2], 2:[3]})
a['a'].append(4)
# 결과 : defaultdict(<class 'list'>, {1:[2], 2:[3], 'a':[4]})
a[1].append(5)
# 결과 : defaultdict(<class 'list'>, {1:[2, 5], 2:[3], 'a':[4]})
from collections import defaultdict
i = [('b', 4), ('a', 1), ('b', 4), ('c', 1), ('a', 2), ('a', 1), ('c', 3)]
result = defaultdict(set)
print(result) # defaultdict(<class 'set'>, {})
for name, point in i:
result[name].add(point)
print(result) # defaultdict(<class 'set'>, {'b': {4}, 'a': {1, 2}, 'c': {1, 3}})
defaultdict(int)
를 활용할 수 있다.from collections import defaultdict
s = "alkbjlkdnlsknldkmvlksndlk"
d = defaultdict(int)
for i in s:
d[i] += 1
print(d) # defaultdict(<class 'int'>, {'a': 1, 'l': 6, 'k': 6, 'b': 1, 'j': 1, 'd': 3, 'n': 3, 's': 2, 'm': 1, 'v': 1})
a[0] = "abc"
를 실행시키면, 더 이상 list형의 value가 아니기 때문에 append, pop 등의 list의 메서드를 사용할 수 없다.from collections import defaultdict
a = defaultdict(list) # value 자료형이 list인 defaultdict 선언
for i in range(5):
a[i].append(i+1)
# 결과 : defaultdict(<class 'list'>, {0: [1], 1: [2], 2: [3], 3: [4], 4: [5]})
a[0] = "abc"
# 결과 : defaultdict(<class 'list'>, {0: 'abc', 1: [2], 2: [3], 3: [4], 4: [5]})
a[0].pop()
# 결과 : AttributeError: 'str' object has no attribute 'pop'
from collections import OrderedDict
od1 = OrderedDict.fromkeys("abmdk")
# 결과 : OrderedDict([('a', None), ('b', None), ('m', None), ('d', None), ('k', None)])
od2 = OrderedDict.fromkeys([1, 2, 3, 4, 5])
# 결과 : OrderedDict([(1, None), (2, None), (3, None), (4, None), (5, None)])
from collections import OrderedDict
od = OrderedDict({2:4, 4:5})
# 결과 : OrderedDict([(2, 4), (4, 5)])
다음 예시를 통해, 주요 메서드 사용 방법을 알아보자.
from collections import OrderedDict
od = OrderedDict.fromkeys([1, 2, 3, 4, 5])
# 결과 : OrderedDict([(1, None), (2, None), (3, None), (4, None), (5, None)])
last=True
로 지정되어 있어, LIFO 형태로 마지막 요소가 반환 및 제거가 이루어지지만, last=False
로 지정하면, FIFO 방식으로 첫번째 요소를 반환 또는 제거한다.od.popitem() # last=True -> (5, None)
print(od) # OrderedDict([(1, None), (2, None), (3, None), (4, None)])
od.popitem(last=False) # -> (1, None)
print(od) # OrderedDict([(2, None), (3, None), (4, None), (5, None)])
last=True
로 지정해서 마지막 요소로 옮기거나, last=False
로 명시해서 첫 번째 요소로 옮길 수 있다.last=True
od.move_to_end(3)
print(od) # OrderedDict([(1, None), (2, None), (4, None), (5, None), (3, None)])
od.move_to_end(3, last=False)
print(od) # OrderedDict([(3, None), (1, None), (2, None), (4, None), (5, None)])
li = [4, 5, 2, 3, 1]
li.sort() # li = [1, 2, 3, 4, 5]
d = {1:2, 5:3, 2:4, 4:9}
d.sort() # dictionary 자체로는 sort 불가
d = {1:2, 5:3, 2:4, 4:9}
d.items() # dict_items([(1, 2), (5, 3), (2, 4), (4, 9)])
d.keys() # dict_keys([1, 5, 2, 4])
d.values() # dict_values([2, 3, 4, 9])
sorted(d.items()) # [(1, 2), (2, 4), (4, 9), (5, 3)]
sorted(d.keys()) # [1, 2, 4, 5]
sorted(d.values()) # [2, 3, 4, 9]
sorted()
에 의해 list로 반환된다.key
값은 해시 가능(hashable) 해야 한다.# Hashable
hashtable = {1:[value], 'a':[value], (1, 2):[value]} # 올바른 사용
# Unhashable
hashtable = {[1, 2]:[value], {1:2, 3:4}:3} # 올바르지 못한 사용
in
연산자를 사용하면, 로 줄일 수 있다.in
연산자를 사용하는 것이 매우 좋다.# 상황 : 리스트 a와 딕셔너리 b에서 n 찾기
# 리스트 시간복잡도 : O(n)
for i in a:
if i == n:
return 1
# 딕셔너리 시간복잡도 : O(1)
if n in b:
return 1
💡 역해시란?
- 해시테이블에서 키값을 해시함수에 넣어서 인덱스를 얻게 되고, 이것이 데이터를 저장하는 공간의 주소
- 역해시가 가능하다는 것은 인덱스에서 역방향으로 키값을 계산하는 것으로, 해시 함수에 따라 가능 여부가 달라진다.
- 따라서 데이터뿐만 아니라 키값도 같이 저장하기도 한다.
- 그렇게 되면, 저장 공간을 늘릴 수밖에 없기 때문에 메모리 사용량이 증가하게 된다.
References