
들어가며
C에서 배열을 쓸 때는 크기를 미리 정해야 했다. int arr[100]; 같은 식으로.
파이썬을 처음 보면 리스트, 튜플, 셋, 딕셔너리가 전부 비슷하게 생겨 보여서 "뭘 언제 써야 하지?" 라는 생각이 든다.
크래프톤 정글 2주차 W3Schools 12~15챕터를 직접 쳐보면서 각각 언제 쓰는 건지 감이 잡혔다.
이번 글은 그 4가지 자료구조를 C 출신 시선으로 비교 정리한 것이다.
시리즈 개요: W3Schools 파이썬 튜토리얼 1~25챕터를 공부한 내용을 묶어 정리합니다.
이번 편은 챕터 12~15: 자료구조 4총사 — List, Tuple, Set, Dictionary
한눈에 보는 4총사 비교
| 자료구조 | 기호 | 순서 | 변경 가능 | 중복 허용 | 주 용도 |
|---|---|---|---|---|---|
| List | [ ] | O | O | O | 일반 배열 대체, 순서 있는 컬렉션 |
| Tuple | ( ) | O | X | O | 변하면 안 되는 값, 함수 다중 반환 |
| Set | { } | X | O | X | 중복 제거, 집합 연산, O(1) 검색 |
| Dictionary | {k: v} | O (3.7+) | O | 키: X | key-value 매핑, 해시맵 |
1. List — C 배열보다 훨씬 편하다 (Ch.12)
파이썬 리스트는 C 배열과 비슷하지만 훨씬 강력하다. 크기를 미리 안 정해도 되고, 서로 다른 타입을 섞어도 되고, 중간에 삽입·삭제가 자유롭다.
알고리즘 풀 때 가장 많이 쓰게 되는 자료구조다.
기본 생성과 인덱싱
# 리스트 생성 — 대괄호, 타입 혼합 가능
fruits = ["apple", "banana", "cherry"]
mixed = [1, "hello", True, 3.14] # 타입 혼합도 OK
# 인덱싱 — 0부터 시작, 음수는 뒤에서부터
print(fruits[0]) # apple
print(fruits[-1]) # cherry — 마지막 요소
# 슬라이싱 — [시작:끝:간격], 끝은 미포함
print(fruits[1:3]) # ['banana', 'cherry']
print(fruits[::2]) # ['apple', 'cherry'] — 2칸씩 건너뜀
print(fruits[::-1]) # ['cherry', 'banana', 'apple'] — 리스트 뒤집기
추가·삽입·삭제
lst = ["a", "b", "c"]
# 끝에 추가
lst.append("d") # ['a', 'b', 'c', 'd']
# 중간에 삽입 — insert(위치, 값)
lst.insert(1, "x") # ['a', 'x', 'b', 'c', 'd']
# 다른 리스트 통째로 붙이기
lst.extend(["e", "f"]) # ['a', 'x', 'b', 'c', 'd', 'e', 'f']
# 삭제 — remove(값), pop(인덱스)
lst.remove("x") # 값으로 첫 번째 일치 항목 제거
lst.pop() # 마지막 요소 제거 + 반환
lst.pop(0) # 인덱스 0 제거 + 반환
del lst[1] # 인덱스 1 삭제 (반환값 없음)
lst.clear() # 리스트 비우기 (변수는 살아있음)
헷갈리는 포인트: remove()는 값으로 찾아 지우고, pop()은 인덱스로 찾아 지운다. del은 변수 자체도 지울 수 있다 (del lst).
리스트 컴프리헨션 — 파이썬의 꽃
C에서 배열을 초기화하거나 필터링할 때 for 루프를 길게 써야 했다. 파이썬은 이걸 한 줄로 압축한다.
처음엔 낯설어 보이지만 익숙해지면 코드가 훨씬 짧고 읽기 좋아진다.
# 기본 문법: [표현식 for 변수 in 이터러블 if 조건]
# 1) 1~9 제곱 리스트
squares = [x**2 for x in range(1, 10)]
# [1, 4, 9, 16, 25, 36, 49, 64, 81]
# 2) "a"가 포함된 과일만 필터링
fruits = ["apple", "banana", "cherry", "mango"]
result = [x for x in fruits if "a" in x]
# ['apple', 'banana', 'mango']
# 3) 조건부 표현식 (삼항연산자처럼)
result = ["짝수" if x % 2 == 0 else "홀수" for x in range(1, 6)]
# ['홀수', '짝수', '홀수', '짝수', '홀수']
# 4) 2차원 배열 초기화 (C에서 int arr[3][4] 하던 것)
matrix = [[0] * 4 for _ in range(3)]
# [[0,0,0,0], [0,0,0,0], [0,0,0,0]]
주의: [[0]*4]*3은 같은 리스트 객체를 3번 참조하므로 한 행을 바꾸면 전체가 바뀐다. 2D 배열은 컴프리헨션으로 초기화해야 안전하다.
정렬과 복사
nums = [3, 1, 4, 1, 5, 9]
# sort() — 원본 변경 (in-place)
nums.sort() # [1, 1, 3, 4, 5, 9]
nums.sort(reverse=True) # [9, 5, 4, 3, 1, 1]
# sorted() — 원본 유지, 새 리스트 반환
new = sorted(nums) # 원본 nums는 그대로
# 복사 — 얕은 복사 (Shallow Copy)
copy1 = nums.copy() # 메서드
copy2 = list(nums) # 생성자
copy3 = nums[:] # 슬라이싱
# 주의: copy2 = nums 는 참조 복사 — 같은 객체를 가리킴!

2. Tuple — 변경 불가 리스트, 언제 쓰나? (Ch.13)
튜플은 리스트랑 거의 같은데 딱 한 가지가 다르다: 한번 만들면 수정할 수 없다.
C의 const 배열과 비슷한 개념이다.
처음엔 "그럼 리스트 쓰면 되지 왜 튜플이 있어?"라고 생각했는데, 쓰다 보니 명확히 구분이 된다.
기본 사용법
# 튜플 생성 — 소괄호
t = ("apple", "banana", "cherry")
print(t[0]) # apple
print(t[-1]) # cherry
# 단일 요소 튜플 — 반드시 쉼표가 필요!
one = ("apple",) # 튜플
not_tuple = ("apple") # 이건 그냥 문자열!
# 수정 시도 → TypeError
# t[0] = "mango" → TypeError: 'tuple' object does not support item assignment
# 수정이 필요하면 리스트로 변환 후 다시 튜플로
lst = list(t)
lst[0] = "mango"
t = tuple(lst) # ('mango', 'banana', 'cherry')
언패킹 — 튜플의 진짜 활용
튜플의 가장 강력한 기능은 언패킹이다. 함수에서 여러 값을 동시에 반환할 때 특히 많이 쓴다.
# 기본 언패킹 — 변수 개수가 요소 개수와 같아야 함
fruits = ("apple", "banana", "cherry")
a, b, c = fruits
print(a) # apple
# *rest — 나머지를 리스트로 받기
first, *rest = fruits
print(first) # apple
print(rest) # ['banana', 'cherry']
# 함수에서 여러 값 반환 — 사실 튜플을 반환하는 것
def minmax(lst):
return min(lst), max(lst)
lo, hi = minmax([3, 1, 4, 1, 5])
print(lo, hi) # 1 5
# 변수 교환 — C에서 temp 변수 필요했던 것
x, y = 10, 20
x, y = y, x # 한 줄로 swap!
튜플을 쓰는 이유: 딕셔너리의 키로 쓸 수 있다 (리스트는 불가). 함수 반환값이 고정되어 있음을 명시적으로 나타낼 수 있다. 변경을 원치 않는 설정값에 쓴다.
3. Set — 중복 제거와 집합 연산 (Ch.14)
C에는 집합 자료구조가 기본으로 없다. 비슷한 걸 구현하려면 bool 배열을 쓰거나 직접 만들어야 했다.
파이썬 Set은 중복을 자동으로 제거하고 교집합·합집합 같은 수학적 집합 연산을 지원한다.
그리고 핵심은 검색이 O(1)이라는 것 — 해시 테이블 기반이기 때문이다.
기본 사용법과 특징
# 생성 — 중괄호. 순서 보장 없음!
s = {"apple", "banana", "cherry", "apple"}
print(s) # {'apple', 'banana', 'cherry'} — 중복 apple 제거됨
# 빈 셋은 반드시 set()으로! {} 는 빈 딕셔너리다
empty = set()
# 리스트 중복 제거에 활용
nums = [1, 2, 2, 3, 3, 3]
unique = list(set(nums)) # [1, 2, 3]
# in 연산자 — O(1) 검색 (리스트는 O(n))
if "apple" in s:
print("있다")
# True==1, False==0 이므로 중복으로 처리됨
s2 = {1, 2, 3, True} # True는 1과 같으므로 {1, 2, 3}
집합 연산 — 진짜 편리한 부분
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
# 합집합 (union) — a 또는 b에 있는 것
print(a | b) # {1, 2, 3, 4, 5, 6}
print(a.union(b)) # 동일
# 교집합 (intersection) — 둘 다 있는 것
print(a & b) # {3, 4}
print(a.intersection(b)) # 동일
# 차집합 (difference) — a에만 있는 것
print(a - b) # {1, 2}
print(a.difference(b)) # 동일
# 대칭 차집합 — 한쪽에만 있는 것 (교집합 제외)
print(a ^ b) # {1, 2, 5, 6}
print(a.symmetric_difference(b)) # 동일
추가/제거 — remove vs discard
s = {"apple", "banana", "cherry"}
s.add("mango") # 단일 추가
s.update(["grape", "kiwi"]) # 여러 개 추가
s.remove("apple") # 없으면 KeyError 발생
s.discard("apple") # 없어도 에러 없음 (안전한 제거)
실전 팁: 존재 여부가 불확실할 때는 remove() 대신 discard()를 쓰자. 에러 처리 코드를 줄일 수 있다.

4. Dictionary — 파이썬의 해시맵 (Ch.15)
C로 해시맵을 직접 구현하면 꽤 복잡하다. 파이썬에서는 딕셔너리가 그 역할을 기본으로 해준다.
key: value 쌍으로 데이터를 저장하고, key로 O(1) 검색이 가능하다. Python 3.7부터는 삽입 순서도 보장된다.
기본 사용법
# 생성
car = {"brand": "Ford", "model": "Mustang", "year": 1964}
# 접근 방법 2가지
print(car["brand"]) # Ford — 키 없으면 KeyError
print(car.get("color")) # None — 키 없어도 에러 없음
print(car.get("color", "N/A")) # N/A — 기본값 지정 가능
# 추가/수정 — 같은 문법
car["color"] = "red" # 새 키 추가
car["year"] = 2024 # 기존 키 수정
car.update({"model": "F-150"}) # 여러 키 한번에 수정
# 삭제
car.pop("color") # 해당 키 삭제 + 반환
car.popitem() # 마지막 삽입 항목 삭제
del car["model"] # 키 삭제
car.clear() # 전체 비우기
순회 — keys(), values(), items()
person = {"name": "철수", "age": 25, "city": "서울"}
# 키만 순회
for k in person.keys():
print(k) # name, age, city
# 값만 순회
for v in person.values():
print(v) # 철수, 25, 서울
# 키-값 쌍으로 순회 — items()가 가장 많이 쓰임
for k, v in person.items():
print(f"{k}: {v}") # name: 철수, age: 25, city: 서울
# 키 존재 확인
if "name" in person:
print("있다")
실전 패턴 — 빈도 카운트
알고리즘 문제에서 딕셔너리가 가장 많이 쓰이는 패턴 중 하나는 빈도 카운트다.
# 문자 빈도 카운트
text = "mississippi"
freq = {}
for ch in text:
freq[ch] = freq.get(ch, 0) + 1
print(freq)
# {'m': 1, 'i': 4, 's': 4, 'p': 2}
# collections.Counter 로 한 줄로 동일한 결과
from collections import Counter
freq2 = Counter(text) # Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
# 중첩 딕셔너리
family = {
"child1": {"name": "Emily", "age": 10},
"child2": {"name": "Tom", "age": 8},
}
print(family["child1"]["name"]) # Emily
⚡ 자료구조별 시간복잡도 성능 비교
알고리즘 문제를 풀 때 자료구조 선택이 통과/시간초과를 가른다.
같은 작업이라도 어떤 구조를 쓰느냐에 따라 O(1)과 O(n)이 갈리기 때문이다.
한눈에 외워두면 문제 풀 때 바로 써먹을 수 있다.
연산별 시간복잡도 Big-O 전체표
| 연산 | List [ ] | Tuple ( ) | Set { } | Dict {k:v} |
|---|---|---|---|---|
인덱스 접근 a[i] |
O(1) | O(1) | — | — |
키/값 검색 x in a |
O(n) | O(n) | O(1) * | O(1) * |
끝에 추가 append / add |
O(1) | — (불변) | O(1) | O(1) |
중간 삽입 insert(i, x) |
O(n) | — (불변) | — (순서없음) | — |
삭제 remove / pop / del |
O(n) | — (불변) | O(1) | O(1) |
정렬 sort() |
O(n log n) | — (불변) | — (순서없음) | — |
전체 순회 for x in a |
O(n) | O(n) | O(n) | O(n) |
| 메모리 효율 | 보통 | ✅ 최고 | 낮음 | 낮음 |
* 해시 충돌이 없는 평균 케이스. 최악의 경우(해시 충돌 연속) Set/Dict 검색도 O(n)이 될 수 있으나 실제로는 거의 발생하지 않는다.
핵심만 기억하는 카드 — "검색 속도 차이가 전부다"
🔵 List / Tuple
O(n)
x in list는 앞에서부터 하나씩 비교.
10만 개면 최대 10만 번 비교.
🟠 Set
O(1)
x in set는 해시값 계산 1번으로 끝.
10만 개든 1억 개든 속도 동일.
🟢 Dict
O(1)
d[key] 접근도 해시 기반.
key 검색, 삽입, 삭제 모두 O(1).
실제로 얼마나 차이날까? — timeit 비교
import timeit
n = 100_000
data_list = list(range(n))
data_set = set(range(n))
data_dict = {i: i for i in range(n)}
target = n - 1 # 리스트 최악의 경우: 맨 끝 원소 검색
t_list = timeit.timeit(lambda: target in data_list, number=1000)
t_set = timeit.timeit(lambda: target in data_set, number=1000)
t_dict = timeit.timeit(lambda: target in data_dict, number=1000)
print(f"List : {t_list:.4f}s") # 예) 2.1832s
print(f"Set : {t_set:.4f}s") # 예) 0.0001s ← 수천 배 빠름
print(f"Dict : {t_dict:.4f}s") # 예) 0.0001s ← 동일
실측 결과: 10만 개 원소에서 in 검색 1,000회 반복 시
List ≈ 2~3초 / Set·Dict ≈ 0.0001초 — 약 20,000배 차이가 난다.
Tuple이 List보다 가벼운 이유 — 메모리 비교
import sys
lst = [0, 1, 2, 3, 4]
tpl = (0, 1, 2, 3, 4)
print(sys.getsizeof(lst)) # 104 bytes
print(sys.getsizeof(tpl)) # 80 bytes ← 더 가볍다
리스트는 동적으로 크기를 늘릴 수 있어야 하기 때문에 오버알로케이션(over-allocation)을 한다.
미리 여유 공간을 잡아놓는 것이다. 반면 튜플은 고정 크기이므로 딱 필요한 만큼만 차지한다.
수백만 개의 좌표처럼 변하지 않는 대량 데이터라면 Tuple이 메모리 측면에서 훨씬 유리하다.
내부 구현 — 왜 그런 성능이 나오나?
| 자료구조 | 내부 구현 | 그래서 이런 성능 |
|---|---|---|
| List | 동적 배열 (Dynamic Array) 메모리 연속 할당 + 오버알로케이션 |
인덱스 O(1), 검색 O(n), append 평균 O(1), insert O(n) |
| Tuple | 고정 배열 (Fixed Array) 생성 후 메모리 변경 없음 |
인덱스 O(1), 검색 O(n), 메모리 효율 최고, 생성 속도 빠름 |
| Set | 해시 테이블 (Hash Table) hash(x) → 버킷 직접 접근 |
검색·삽입·삭제 O(1), 순서 없음, 인덱스 없음 |
| Dict | 해시 테이블 (Hash Table) key → hash → 버킷, value 저장 |
키 검색·삽입·삭제 O(1), 삽입 순서 보존 (Python 3.7+) |
정리: List·Tuple은 배열 기반이라 인덱스 접근이 빠르고, Set·Dict는 해시 기반이라 검색·삽입·삭제가 빠르다.
알고리즘 문제에서 "이 값이 존재하는지 반복적으로 확인해야 한다"면 리스트 대신 Set/Dict로 바꾸는 것만으로도 통과 여부가 갈릴 수 있다.
어떤 자료구조를 언제 써야 할까?
처음엔 리스트만 쓰게 된다. 그런데 문제를 풀다 보면 성능 차이가 생긴다. 상황에 맞게 골라서 써야 한다.
| 상황 | 추천 | 이유 |
|---|---|---|
| 순서 있는 데이터, 수정 필요 | List | append/remove 자유, 인덱스 접근 |
| 변하면 안 되는 값 (좌표, RGB, 설정값) | Tuple | 불변이라 dict 키로 사용 가능, 의도 명시 |
| 중복 제거, 빠른 존재 확인 필요 | Set | O(1) 검색, 자동 중복 제거 |
| key → value 매핑, 빈도 카운트 | Dict | O(1) 검색, 의미 있는 키 |
| 두 집합의 공통 원소 찾기 | Set | intersection() 한 줄로 해결 |
| 함수에서 여러 값 반환 | Tuple | 언패킹으로 변수에 바로 받기 |
마무리
처음엔 "리스트 하나면 되지 않나?"라는 생각이었다. 근데 직접 써보면서 각각의 쓰임새가 점점 명확해졌다.
특히 Set의 O(1) 검색과 집합 연산, Dictionary의 get() 기본값 패턴, 리스트 컴프리헨션은
알고리즘 문제를 풀 때 코드를 확 줄여주는 핵심 도구가 됐다.
다음 편은 Ch.16~18: 조건문, while, for 루프 다. 파이썬 루프에서도 C와 다른 것들이 꽤 있다.
네 가지를 다 외우려 하지 말고, 언제 어떤 걸 쓰는지 감을 잡는 게 먼저다.
크래프톤 정글 2주차 | W3Schools Python Ch.12~15 | 정리: choihyunjin1
#크래프톤정글 #파이썬자료구조 #리스트컴프리헨션 #딕셔너리 #Python
'개발 > 공부 기록' 카테고리의 다른 글
| CSAPP 11장 공부 기록 1편 : 네트워크 바깥세상: 장비, 주소, 캡슐화 (1) | 2026.04.23 |
|---|---|
| 컴퓨터 시스템 9장 핵심 정리 (1) | 2026.04.16 |
| [파이썬 기초 문법 총정리 #4] 실전 도구 완전 정복 — Arrays, Iterators, Modules, Dates, Math (Ch.21~25) (3) | 2026.03.08 |
| [파이썬 기초 문법 총정리 #3] 제어문과 함수 완전 정복 — if, while, for, def, lambda (Ch.16~20) (1) | 2026.03.08 |
| [파이썬 기초 문법 총정리#1] — C 출신이 처음 파이썬 보면 낯선 것들 (W3Schools Ch.01~11) (0) | 2026.03.08 |