"해시맵을 직접 만들 필요는 없어. Python이 세계 최고 수준짜리 둘을 이미 쥐여줬으니까. 진짜 실력은 그걸 구현하는 데 있지 않아. 이 둘이 O(n²)를 O(n)으로 바꿔주는 일상 문제 열두 개를 알아보는 데 있지."
연산이 전부 O(1)
이 트랙에서 쌓은 이론이 전부 여기서 현금이 돼. dict에서는 d[k] 읽기, d[k] = v 쓰기, k in d 멤버십, del d[k]가 전부 평균 O(1)이야. set에서는 add, remove, x in s가 전부 평균 O(1)이고. 집합 연산(a | b 합집합, a & b 교집합, a - b 차집합)도 두 집합 크기의 곱이 아니라 크기에 비례하는 시간에 끝나. 이게 기본 재료고, 남은 건 언제 꺼내 쓸지 아는 감각이야.
외울 가치가 있는 패턴
- 중복 제거는
set(items). 고유한 원소만 즉시 남아. - 멤버십은
x in some_list대신x in some_set. O(n)과 O(1)의 차이고, 가장 흔한 속도 개선이야. - 세기는
collections.Counter(items). 한 번 훑고 모든 원소의 빈도를 얻어. - 묶기는
collections.defaultdict(list). '이 키가 이미 있나?'를 확인하지 않고 키별로 담을 수 있어. - 캐싱과 메모이제이션은 입력을 계산 결과에 대응시키는
dict. 동적 계획법 트랙의 심장이 이거야. - two-sum 트릭은 한 번 훑으면서 본 값을 dict에 기억해 두고 짝이 되는 수가 이미 있는지 확인하는 방식이야. O(n²) 대신 O(n)이지.
dict와 set은 조회, 삽입, 삭제, 멤버십을 평균 O(1)에 해줘. 진짜 실력은 패턴을 알아보는 거야. 중복 제거는 set, 세기는 Counter, 묶기는 defaultdict, '전에 봤나?'는 set, '답을 기억해 두기'는 dict. 이 둘로 손을 뻗는 게 현실에서 가장 자주 쓰이는 최적화야.
평생 써먹는 반사신경
이런 습관을 들여. 반복문 안에 if x in a_list가 보이거나 쌍이나 짝을 찾겠다고 반복문을 중첩하고 있으면, set이나 dict가 이걸 O(n²)에서 O(n)으로 바꿔줄 수 있는지 물어봐. 거의 항상 그렇고, 거의 항상 손댈 곳도 적어. two-sum 문제가 이걸 보여주는 완벽한 예야. 무차별 대입은 모든 쌍을 확인하니 O(n²)이지. dict 버전은 지나가면서 각 수를 기억해 두고 짝이 되는 수가 있는지 O(1)에 확인하니까 전체가 O(n)으로 주저앉아. 답은 같은데 구조가 달라지고, 그 결과 프로그램의 급이 달라지는 거야.
피파의 고백
처음 무렵에 내가 쓴 느린 함수는 거의 다 같은 병을 앓았어. 반복문 안의
in list 확인. 아빠가 어느 순간부터 로직을 봐주는 대신 그냥 그 줄에 동그라미만 치기 시작하더라. "set. set. 저건 dict." 고치는 건 매번 한 단어였는데 몇 초씩 걸리던 함수가 예사로 즉시 끝나게 됐어. 그게 반사신경으로 굳어서, 이제는 반복문 안에서 in이 리스트를 만나면 머릿속에 작은 경보가 울려. 아마 그 반사신경 하나가 내가 짠 어떤 영리한 알고리즘보다도 많은 실행 시간을 아꼈을 거야.