[Python] 사전(Dict)과 집합(Set)

 

동빈나 채널의 파이썬 문법 부수기 유튜브 강의를 참고하여 정리한 내용이다.

 

사전(dict)과 집합(set)은 둘 다 해시 테이블 기반이다.

조회, 삽입, 삭제가 평균 O(1)이라는 게 가장 중요한 특징이다.

코딩테스트에서 특정 값의 존재 여부를 빠르게 확인하거나 빈도수를 셀 때 필수적으로 사용한다.

 


 

사전 (dict)

해시 테이블 기반 O(1) 조회

사전은 키-값 쌍으로 데이터를 저장한다. 내부적으로 해시 테이블을 사용하기 때문에 리스트에서 O(N)이 걸리던 탐색이 사전에서는 O(1)에 가능하다.

# 1만 개의 데이터에서 특정 값 탐색
import time

data_list = list(range(1000000))
data_dict = {i: True for i in range(1000000)}

# 리스트 탐색 - O(N)
start = time.time()
999999 in data_list
print(f"리스트: {time.time() - start:.6f}초")  # 약 0.012초

# 사전 탐색 - O(1)
start = time.time()
999999 in data_dict
print(f"사전: {time.time() - start:.6f}초")  # 약 0.000001초

 


 

사전 초기화

# 중괄호 {}
a = {"key1": "value1", "key2": "value2"}

# dict() 함수
b = dict(key1="value1", key2="value2")

# 빈 사전
c = {}
d = dict()

print(a)       # {'key1': 'value1', 'key2': 'value2'}
print(type(c)) # <class 'dict'>
print(type({})) # <class 'dict'> - 빈 {}는 사전
# 주의: {1, 2, 3}은 집합, {}은 사전
# 리스트로 사전 만들기
keys = ["a", "b", "c"]
values = [1, 2, 3]
d = dict(zip(keys, values))
print(d)  # {'a': 1, 'b': 2, 'c': 3}

# 컴프리헨션
squares = {i: i**2 for i in range(1, 6)}
print(squares)  # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

 


 

사전 접근 및 수정

값 읽기

d = {"name": "Alice", "age": 25, "score": 95}

# 대괄호 접근
print(d["name"])  # Alice
print(d["age"])   # 25

# 없는 키 접근 - KeyError 발생
print(d["gender"])
# KeyError: 'gender'

# get() 사용 - 없으면 None 반환
print(d.get("name"))    # Alice
print(d.get("gender"))  # None

# 기본값 지정
print(d.get("gender", "Unknown"))  # Unknown
print(d.get("age", 0))             # 25 (있으면 원래 값)

코딩테스트에서 키 존재 여부가 불확실할 때는 d.get(key, default) 패턴을 많이 쓴다.

 

값 추가 및 수정

d = {"a": 1}

# 새 키-값 추가
d["b"] = 2
print(d)  # {'a': 1, 'b': 2}

# 기존 값 수정
d["a"] = 100
print(d)  # {'a': 100, 'b': 2}

# update() 로 여러 개 한 번에
d.update({"c": 3, "d": 4})
print(d)  # {'a': 100, 'b': 2, 'c': 3, 'd': 4}

 

삭제

d = {"a": 1, "b": 2, "c": 3}

# del
del d["a"]
print(d)  # {'b': 2, 'c': 3}

# del - 없는 키 삭제 시 에러
del d["z"]  # KeyError: 'z'

# pop() - 삭제 후 값 반환
val = d.pop("b")
print(val)  # 2
print(d)    # {'c': 3}

# pop() - 없는 키에 기본값 설정
val = d.pop("z", "없음")
print(val)  # 없음 (에러 없음)

 


 

keys(), values(), items()

d = {"apple": 3, "banana": 5, "cherry": 2}

# 키만
print(d.keys())    # dict_keys(['apple', 'banana', 'cherry'])
print(list(d.keys()))  # ['apple', 'banana', 'cherry']

# 값만
print(d.values())  # dict_values([3, 5, 2])
print(list(d.values()))  # [3, 5, 2]

# 키-값 쌍
print(d.items())   # dict_items([('apple', 3), ('banana', 5), ('cherry', 2)])

# 반복
for key in d:
    print(key, d[key])

for key, value in d.items():
    print(f"{key}: {value}")

 

사전 정렬

d = {"banana": 2, "apple": 5, "cherry": 3}

# 키 기준 정렬
sorted_by_key = dict(sorted(d.items()))
print(sorted_by_key)  # {'apple': 5, 'banana': 2, 'cherry': 3}

# 값 기준 정렬
sorted_by_val = dict(sorted(d.items(), key=lambda x: x[1]))
print(sorted_by_val)  # {'banana': 2, 'cherry': 3, 'apple': 5}

# 값 기준 내림차순
sorted_desc = dict(sorted(d.items(), key=lambda x: x[1], reverse=True))
print(sorted_desc)  # {'apple': 5, 'cherry': 3, 'banana': 2}

 


 

빈도수 세기 패턴

코딩테스트에서 사전을 가장 많이 쓰는 상황이다.

# 기본 패턴
text = "abracadabra"
freq = {}
for char in text:
    if char in freq:
        freq[char] += 1
    else:
        freq[char] = 1
print(freq)  # {'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}

# get() 활용 - 더 간결
freq = {}
for char in text:
    freq[char] = freq.get(char, 0) + 1
print(freq)  # {'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}

# collections.Counter - 가장 편함
from collections import Counter
freq = Counter(text)
print(freq)  # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
print(freq["a"])  # 5
print(freq["z"])  # 0 (없는 키도 0 반환)

# 가장 많이 등장한 것
print(freq.most_common(2))  # [('a', 5), ('b', 2)]

 

defaultdict 패턴

from collections import defaultdict

# 기본값이 있는 사전
d = defaultdict(int)   # 기본값 0
d["a"] += 1
d["b"] += 5
d["a"] += 2
print(dict(d))  # {'a': 3, 'b': 5}
print(d["c"])   # 0 - 없는 키도 에러 없이 기본값 반환

# 리스트 기본값
graph = defaultdict(list)
graph[1].append(2)
graph[1].append(3)
graph[2].append(4)
print(dict(graph))  # {1: [2, 3], 2: [4]}

그래프를 인접 리스트로 표현할 때 defaultdict(list)를 자주 쓴다.

 


 

집합 (set)

두 가지 특징

  • 중복 불허: 같은 값이 두 번 이상 저장되지 않음
  • 순서 없음: 인덱싱이 안 되고, 출력 순서가 보장되지 않음
a = {1, 2, 3, 4, 5}
print(a)       # {1, 2, 3, 4, 5}
print(type(a)) # <class 'set'>

# 중복 자동 제거
b = {1, 2, 2, 3, 3, 3}
print(b)  # {1, 2, 3}

# 리스트에서 중복 제거
nums = [1, 2, 2, 3, 3, 3, 4]
unique = list(set(nums))
print(unique)  # [1, 2, 3, 4] (순서는 보장 안 됨)

 


 

집합 초기화

# 중괄호 - 원소가 있을 때
a = {1, 2, 3}

# set() 함수 - 빈 집합은 반드시 set() 사용
b = set()  # 빈 집합
c = {}     # 이건 빈 사전!

print(type(b))  # <class 'set'>
print(type(c))  # <class 'dict'>

# 이터러블로 초기화
d = set([1, 2, 3, 2, 1])  # 리스트에서
print(d)  # {1, 2, 3}

e = set("hello")  # 문자열에서
print(e)  # {'h', 'e', 'l', 'o'} - 중복 l 하나 제거

빈 집합은 반드시 set()으로 만든다. {}은 빈 사전이다.

 


 

집합 연산

a = {1, 2, 3, 4, 5}
b = {3, 4, 5, 6, 7}

# 합집합 (|)
print(a | b)          # {1, 2, 3, 4, 5, 6, 7}
print(a.union(b))     # 동일

# 교집합 (&)
print(a & b)            # {3, 4, 5}
print(a.intersection(b)) # 동일

# 차집합 (-)
print(a - b)           # {1, 2} - a에만 있는 것
print(a.difference(b)) # 동일

# 대칭차집합 (^) - 한쪽에만 있는 것
print(a ^ b)                    # {1, 2, 6, 7}
print(a.symmetric_difference(b)) # 동일

 

부분집합, 초집합 확인

a = {1, 2, 3}
b = {1, 2, 3, 4, 5}
c = {1, 2, 3}

print(a.issubset(b))    # True - a ⊆ b
print(a <= b)           # True - 동일

print(b.issuperset(a))  # True - b ⊇ a
print(b >= a)           # True - 동일

print(a == c)           # True - 같은 집합
print(a.isdisjoint({6, 7, 8}))  # True - 교집합 없음

 


 

집합 메서드

s = {1, 2, 3}

# 추가
s.add(4)
print(s)  # {1, 2, 3, 4}

# 이미 있는 값 추가 - 무시됨 (에러 아님)
s.add(3)
print(s)  # {1, 2, 3, 4}

# 여러 개 추가
s.update({5, 6, 7})
print(s)  # {1, 2, 3, 4, 5, 6, 7}

# 제거
s.remove(7)
print(s)  # {1, 2, 3, 4, 5, 6}

# 없는 값 remove - 에러
s.remove(99)  # KeyError: 99

# discard - 없어도 에러 없음
s.discard(99)  # 조용히 무시
s.discard(6)
print(s)  # {1, 2, 3, 4, 5}

# 임의의 원소 제거 후 반환
val = s.pop()
print(val)  # 임의의 값 (순서 없으므로 예측 불가)
메서드 기능 없을 때
add(x) x 추가 무시
remove(x) x 제거 KeyError
discard(x) x 제거 무시
update(s) s의 모든 원소 추가 -
pop() 임의 원소 제거 후 반환 KeyError (빈 집합)
clear() 전체 삭제 -

 


 

집합의 in 연산 - O(1)

# 리스트에서 in - O(N)
a_list = list(range(1000000))
999999 in a_list  # 느림

# 집합에서 in - O(1)
a_set = set(range(1000000))
999999 in a_set  # 빠름

값의 존재 여부만 확인하면 되고 순서나 중복이 필요 없다면 리스트보다 집합을 쓴다.

 


 

사전과 집합의 공통점

특징 사전 (dict) 집합 (set)
내부 구조 해시 테이블 해시 테이블
탐색 시간 O(1) 평균 O(1) 평균
순서 Python 3.7+에서 삽입 순서 유지 순서 없음
중복 키 중복 불가 원소 중복 불가
인덱싱 키로 접근 불가

Python 3.7부터 사전은 삽입 순서를 보장한다. 하지만 집합은 여전히 순서가 없다.

 


 

실전 패턴 모음

두 리스트의 공통 원소 찾기

a = [1, 2, 3, 4, 5]
b = [3, 4, 5, 6, 7]

# set 교집합 활용 - O(min(len(a), len(b)))
common = set(a) & set(b)
print(common)         # {3, 4, 5}
print(list(common))   # [3, 4, 5]

 

중복 없이 방문 체크

visited = set()

def dfs(node):
    if node in visited:
        return
    visited.add(node)
    # 탐색 로직

 

아나그램 확인

def is_anagram(s1, s2):
    from collections import Counter
    return Counter(s1) == Counter(s2)

print(is_anagram("listen", "silent"))  # True
print(is_anagram("hello", "world"))    # False

# 집합만으로는 부족 (빈도수 무시)
print(set("aab") == set("ab"))  # True - 틀린 결과

 

키 존재 여부 체크 후 처리

graph = {"A": ["B", "C"], "B": ["D"]}

# 잘못된 방법 - KeyError 가능
neighbors = graph["E"]  # KeyError

# 올바른 방법들
neighbors = graph.get("E", [])  # 빈 리스트 기본값
print(neighbors)  # []

if "E" in graph:
    neighbors = graph["E"]
else:
    neighbors = []

 


 

자주 하는 실수 모음

# 1. 빈 집합을 {}로 만들려다 사전이 됨
s = {}
print(type(s))  # <class 'dict'> - 집합 아님!
s = set()       # 이게 올바른 빈 집합

# 2. 집합은 인덱싱 불가
s = {1, 2, 3}
print(s[0])  # TypeError: 'set' object is not subscriptable

# 3. 없는 키에 [] 접근
d = {"a": 1}
print(d["b"])  # KeyError: 'b'
print(d.get("b", 0))  # 0 - 안전한 방법

# 4. remove vs discard 혼동
s = {1, 2, 3}
s.remove(5)   # KeyError: 5
s.discard(5)  # 에러 없음

# 5. 집합 원소로 리스트 사용 시도 (unhashable)
s = set()
s.add([1, 2])  # TypeError: unhashable type: 'list'
s.add((1, 2))  # 튜플은 가능

# 6. 사전 반복 중 크기 변경
d = {"a": 1, "b": 2, "c": 3}
for key in d:
    if key == "b":
        del d[key]
# RuntimeError: dictionary changed size during iteration

# 올바른 방법
keys_to_delete = [key for key in d if key == "b"]
for key in keys_to_delete:
    del d[key]

 


 

정리

사전과 집합은 해시 테이블 덕분에 O(1) 탐색이 가능하다. 코딩테스트에서:

  • 빈도수 세기: Counter 또는 d.get(key, 0) + 1 패턴
  • 방문 체크: 집합으로 O(1) in 연산
  • 빠른 조회 필요: 리스트 대신 사전/집합
  • 공통 원소 찾기: set(a) & set(b)
  • 그래프 표현: defaultdict(list)

빈 집합은 set(), 빈 사전은 {} 또는 dict()로 만든다는 것만 헷갈리지 않으면 된다.

'Study > Python' 카테고리의 다른 글

[Python] 반복문  (0) 2026.03.07
[Python] 조건문  (0) 2026.03.07
[Python] 리스트(List)  (0) 2026.03.07
[Python] Boolean 자료형  (0) 2026.03.07
[Python] 문자열(String) 자료형과 튜플  (1) 2026.03.07