graph TD
RAM["컴퓨터 주기억장치 (RAM): 번호가 붙은 거대한 사물함"] --> Continuous["연속 메모리 할당: 배열 Array"]
RAM --> Discontinuous["불연속 포인터 연결: 연결 리스트 Linked List"]
Continuous --> CacheWin["CPU 캐시 지역성 대폭 향상!"]
Discontinuous --> MemoryFlex["동적 크기 조절의 유연성 확보!"]
style RAM fill:#F1F5F9,stroke:#475569,stroke-width:2px;
style Continuous fill:#DCFCE7,stroke:#16A34A,stroke-width:2px;
style Discontinuous fill:#FEF3C7,stroke:#D97706,stroke-width:2px;
style CacheWin fill:#BAE6FD,stroke:#0284C7,stroke-width:1px;
style MemoryFlex fill:#F3E8FF,stroke:#9333EA,stroke-width:1px;
0. 자료구조는 ’데이터를 담는 그릇’이다
주방을 요리하는 공간이라고 할 때: * 알고리즘이 맛있는 음식을 만드는 ‘조리 레시피(절차)’라면, * 자료구조는 신선한 식재료를 보관하는 ‘냉장고와 밀폐용기(그릇)’입니다.
식재료가 뒤죽박죽 엉망으로 널브러져 있다면 아무리 세계 최고의 셰프라도 재료를 찾는 데 시간을 다 허비합니다. 데이터를 컴퓨터 메모리(RAM)에 어떻게 정돈해 두느냐에 따라 프로그램의 속도가 수천 배 달라집니다.
1. 선형 자료구조의 영원한 라이벌: 배열 vs 연결 리스트
| 비교 항목 | 배열 (Array) | 연결 리스트 (Linked List) |
|---|---|---|
| 메모리 배치 | 빈틈없이 연속된 물리 메모리 공간에 나란히 배치 | 메모리 여기저기 흩어져 있고 주소(Pointer)로 서로를 가리킴 |
| 인덱스 임의 접근 (Lookup) | \(O(1)\) 초고속 (시작 주소 + 인덱스 * 크기) | \(O(N)\) 느림 (헤드부터 노드를 하나씩 타고 넘어가야 함) |
| 중간 삽입 / 삭제 | \(O(N)\) 느림 (뒤의 모든 데이터를 한 칸씩 밀거나 당겨야 함) | \(O(1)\) 즉각 완료 (앞뒤 포인터 연결선만 뚝 떼어 붙이면 끝) |
| CPU 캐시 적중률 | 압도적으로 높음 (공간 지역성 Spatial Locality) | 낮음 (메모리가 흩어져 있어 매번 캐시 미스 발생) |
힌트🚀 왜 현대 개발에서는 연결 리스트보다 배열을 훨씬 많이 쓸까?
이론상 연결 리스트가 삽입/삭제 \(O(1)\)로 좋아 보이지만, 현대 CPU는 메모리를 읽을 때 요청된 바이트만 가져오는 것이 아니라 주변 64바이트 덩어리(Cache Line)를 통째로 L1/L2 캐시로 퍼옵니다. 배열은 연속되어 있어 다음 데이터가 이미 CPU 캐시에 들어있지만, 연결 리스트는 메모리 곳곳을 포인터로 점프해야 하므로 CPU가 멍때리는 캐시 미스(Cache Miss)가 발생합니다.
2. 출입구가 통제된 데이터 통로: 스택(Stack)과 큐(Queue)
graph LR
subgraph StackBox["스택 (Stack: LIFO)"]
S_In["PUSH"] --> S1["[ 3 ] 최상단 (Top)"]
S1 --> S2["[ 2 ]"]
S2 --> S3["[ 1 ] 바닥"]
S1 --> S_Out["POP"]
end
subgraph QueueBox["큐 (Queue: FIFO)"]
Q_In["ENQUEUE (Rear)"] --> Q3["[ 3 ]"]
Q3 --> Q2["[ 2 ]"]
Q2 --> Q1["[ 1 ]"]
Q1 --> Q_Out["DEQUEUE (Front)"]
end
style StackBox fill:#FEF3C7,stroke:#D97706,stroke-width:2px;
style QueueBox fill:#E0F2FE,stroke:#0284C7,stroke-width:2px;
- 스택 (Stack - LIFO 후입선출): 프링글스 감자칩 통. 가장 마지막에 넣은 것이 가장 먼저 나옵니다.
- 실무 활용: 브라우저 ‘뒤로 가기’, 텍스트 에디터의 ‘실행 취소(Ctrl+Z)’, 프로그래밍 언어의 함수 호출 스택(Call Stack).
- 큐 (Queue - FIFO 선입선출): 은행 창구 줄서기. 먼저 온 사람이 먼저 서비스를 받습니다.
- 실무 활용: 프린터 인쇄 대기열, 백엔드 비동기 작업 큐(RabbitMQ, Celery), 웹 서버의 접속 대기열.
- 덱 (Deque - Double-Ended Queue): 양쪽 끝(앞/뒤) 모두에서 넣고 뺄 수 있는 만능 큐.
3. \(O(1)\)의 마법: 해시 테이블 (Hash Table)
전화번호부에서 100만 명 중 ’홍길동’의 번호를 0.0001초 만에 찾는 비결은 무엇일까요?
sequenceDiagram
autonumber
participant Key as 입력 키: "apple"
participant HashFn as 해시 함수 (Hash Function)
participant Bucket as 해시 버킷 배열 (RAM)
Key->>HashFn: "apple" 문자열 전달
HashFn-->>Bucket: 수학 연산을 통해 정수 인덱스 4번 슬롯 계산!
Bucket-->>Key: 4번 버킷의 값 "$3.50" 즉시 반환 (O(1))
해시 충돌(Hash Collision)을 해결하는 2대 비책:
- 체이닝 (Chaining): 충돌이 난 버킷 뒤에 연결 리스트나 Red-Black 트리를 매달아 데이터를 줄줄이 연결합니다. (Java HashMap 표준)
- 개방 주소법 (Open Addressing): 충돌이 나면 옆의 빈 슬롯을 찾아 들어갑니다. (선형 탐사 Linear Probing, 이중 해싱)
4. 계층형 데이터의 정점: 트리(Tree)와 B+Tree
단순한 선형 구조로는 수억 건의 계층 데이터를 다룰 수 없습니다.
[ 루트 노드 ]
/ [ 서브트리 ] [ 서브트리 ]
/ \ / [단말] [단말] [단말] [단말] (리프 노드)
- 이진 탐색 트리 (BST, Binary Search Tree): “왼쪽 자식은 나보다 작고, 오른쪽 자식은 나보다 크다.” 평균 \(O(\log N)\)으로 탐색하지만, 정렬된 데이터가 순서대로 들어오면 한쪽으로 길게 늘어서는 편향 트리(\(O(N)\) 타락)가 됩니다.
- 균형 이진 트리 (AVL Tree & Red-Black Tree): 노드가 한쪽으로 기울어지면 스스로 회전(Rotation)하여 높이 균형을 강제로 맞춥니다. 자바의
TreeMap과 C++의std::map이 Red-Black Tree로 구현되어 있습니다. - B+Tree (데이터베이스 인덱스의 제왕):
- 디스크 I/O를 최소화하기 위해 자식 노드를 수백 개씩 갖는 다진 트리(Multi-way Tree)입니다.
- 모든 실제 데이터는 가장 바닥의 리프 노드(Leaf Node)에만 저장되며, 리프 노드들끼리 양방향 연결 리스트(Linked List)로 촘촘히 연결되어 있습니다.
- 덕분에
WHERE age BETWEEN 20 AND 30과 같은 범위 스캔(Range Scan)을 할 때 트리를 다시 올라갈 필요 없이 리프 노드만 옆으로 횡단하며 초고속으로 데이터를 긁어옵니다.
5. 최댓값·최솟값 즉시 뽑기: 힙(Heap)과 우선순위 큐
새로운 환자가 응급실에 실려 왔을 때, 도착 순서가 아니라 환자의 위급한 상태(우선순위)에 따라 가장 위급한 환자를 \(O(1)\)에 호출해야 합니다.
- 최대 힙 (Max Heap): 부모 노드의 값이 항상 자식 노드보다 큽니다. 루트 노드에는 항상 전체 데이터의 최댓값이 위치합니다.
- 최소 힙 (Min Heap): 루트 노드에 항상 최솟값이 위치합니다.
- 새 데이터 삽입: \(O(\log N)\)
- 최우선 순위 데이터 추출: \(O(\log N)\)
6. 핵심 자료구조 8대 요약표
| 자료구조 | 탐색(Lookup) | 삽입(Insert) | 삭제(Delete) | 핵심 강점 및 추천 실무 상황 |
|---|---|---|---|---|
| 배열 (Array) | \(O(1)\) | \(O(N)\) | \(O(N)\) | 데이터 개수가 정해져 있고 조회가 압도적으로 많은 경우 |
| 연결 리스트 | \(O(N)\) | \(O(1)\) | \(O(1)\) | 중간 삽입/삭제가 빈번하고 전체 크기를 가늠하기 힘든 경우 |
| 스택 (Stack) | \(O(N)\) | \(O(1)\) | \(O(1)\) | 최근 작업 되돌리기, 괄호 짝 맞추기, 깊이 우선 탐색(DFS) |
| 큐 (Queue) | \(O(N)\) | \(O(1)\) | \(O(1)\) | 요청 버퍼링, 이벤트 큐, 너비 우선 탐색(BFS) |
| 해시 테이블 | \(O(1)\) | \(O(1)\) | \(O(1)\) | 고유 키(Key)로 데이터를 즉각 조회해야 하는 모든 캐시 및 맵 |
| Red-Black Tree | \(O(\log N)\) | \(O(\log N)\) | \(O(\log N)\) | 정렬 상태를 유지하면서 삽입/삭제가 지속적으로 발생하는 경우 |
| B+Tree | \(O(\log N)\) | \(O(\log N)\) | \(O(\log N)\) | 대용량 디스크 기반 인덱스 및 범위 검색(Range Scan) |
| 힙 (Heap) | 최댓값: \(O(1)\) | \(O(\log N)\) | \(O(\log N)\) | 우선순위 큐, 실시간 스케줄러, 다익스트라 최단 경로 알고리즘 |
📚 참고 문헌 및 공식 출처
| 구분 | 자료명 | 주요 내용 | 링크 |
|---|---|---|---|
| 자료구조 바이블 | Fundamentals of Data Structures in C (Horowitz 저) | 배열, 트리, 그래프의 수학적 표현과 메모리 구조 이론 | Silicon Press |
| DB 인덱스 심화 | Database Internals | Alex Petrov 저, B-Tree, B+Tree, LSM-Tree의 디스크 스토리지 아키텍처 | O’Reilly |
| Java 표준 구현체 | OpenJDK HashMap & ConcurrentHashMap Implementation | 버킷 충돌 해결, 트리화 임계치(Treeify Threshold 8) 소스 분석 | OpenJDK Source |