"교과서는 배열 바로 뒤에 연결 리스트를 가르치고 둘이 동등한 척 암시해. 안 동등해 — 그리고 놀라운 진실은 배열이 빅오 표가 암시하는 것보다 훨씬 자주 이겨."
비용 표, 나란히
나란히 놓으면 거래가 분명해져:
- i 번째 원소 인덱싱: 배열 O(1) · 연결 리스트 O(n).
- 알려진 노드에서 삽입/삭제: 배열 O(n) (밀기) · 연결 리스트 O(1) (다시 잇기).
- 끝에서 삽입/삭제: 배열 분할 상환 O(1) · tail 포인터 있는 연결 리스트 O(1).
- 값 검색: 둘 다 O(n).
- 원소당 메모리: 배열 = 값만 · 연결 리스트 = 값 + 포인터 한두 개 (오버헤드 더).
종이 위에선 삽입 많은 작업에 연결 리스트가 좋아 보여. 그럼 왜 거의 맞는 기본값이 아니야?
반전: 캐시 지역성
빅오는 연산을 세지만 각 연산이 실제로 얼마나 걸리는지는 무시해 — 그리고 진짜 하드웨어에선 그 시간이 엄청나게 불균등해. 배열 원소는 연속이라, CPU 가 하나 읽으면 이웃을 캐시로 미리 끌어와; 훑기가 날아. 연결 리스트 노드는 임의 주소에 흩어져 있어, 매 node.next hop 이 캐시 미스 — CPU 가 예측 못 한 메모리를 기다리며 멈춰. 결과: 연속 배열의 선형 훑기가 같은 길이 연결 리스트 걷기를 일상적으로 3–10배 이겨, 똑같은 O(n) 인데도. 빅오가 버리는 상수 인자가, 여기선 이야기 전부야.
동적 배열 (Python list) 을 기본으로 해. 연속성이 캐시에서 이기고, 분할 상환 append 가 대부분 성장을 덮어. 연결 구조로 가는 건 쥔 참조에서 O(1) 잇기나 안정적 노드 정체성이 특별히 필요할 때만 — 그냥 '삽입이 O(1)' 이라서가 아니라.
그럼 언제 연결 리스트가 맞아?
연결 리스트는 특정 상황에서 자리를 벌어 — 보통 네가 인덱싱하는 리스트가 아니라, 더 큰 뭔가의 부품으로:
- LRU 캐시 — 해시맵 (O(1) 찾기) + 이중 연결 리스트 (O(1) 맨 앞으로 옮기기, 뒤에서 퇴출). 이 조합이 정석 용도고, 야생에서 만나게 돼.
- 덱 — Python 의
collections.deque가 양 끝 O(1) 위해 연결 블록을 써. - 인접 리스트 — 그래프가 엣지를 저장하는 법 (다다음 트랙).
- 실행 취소/재실행, 링 버퍼, free list — 잇기 하고 안정적 참조가 인덱싱보다 중요한 어디든.
피파의 고백
한 번 빅오 표가 더 싼 삽입을 약속해서 hot loop 를 연결 리스트로 다시 썼는데 — 더 느려졌어. 아빠가 캐시를 설명했고, 난 도둑맞은 기분이었어: 교과서가 점근을 가르치면서 메모리 배치가 진짜 승자를 정하는 부분을 조용히 건너뛴 거야. 이젠 내 기본값이 겸손한 동적 배열이고, O(1) 잇기나 안정적 노드 핸들이 진짜 필요할 때만 연결 구조로 가 — 어린 시절 내가 가정한 것보다 드물어.