배열 (Array)
정의
배열 (Array) 는 동일한 타입의 원소를 메모리에 연속으로 저장하는 가장 기본적인 선형 자료구조다.
- 정적 배열 (Static Array): 크기가 컴파일 타임에 고정. C++
int arr[N], Javaint[]. - 동적 배열 (Dynamic Array): 런타임에 크기 조정 가능. C++
std::vector, Pythonlist, JavaArrayList.
인덱스 i 번째 원소의 주소 = base_address + i × element_size. 이 계산이 O(1) 이므로 랜덤 접근 O(1) 이 보장된다.
PS 에서 가장 많이 사용하는 자료구조. 캐시 친화성과 단순한 인터페이스 덕에 다른 자료구조로 교체 가능한 상황에서도 배열을 우선 고려한다.
문제 상황과 동기
N개 원소를 저장하고, 특정 위치 접근이 잦은 경우를 생각하자.
- naive (별도 변수):
a,b,c,d… 인덱스로 접근 불가. 루프 작성 불가능. - 연결 리스트: 임의 접근 O(N), 캐시 miss 빈발.
- 배열: 임의 접근 O(1), 캐시 지역성 우수.
핵심 통찰: 메모리 연속 배치 가 가져오는 두 가지 이점:
- 랜덤 접근 O(1):
arr[i]=*(base + i)계산 한 번. - 캐시 지역성 (Cache Locality): 순차 접근 시 CPU 캐시에 인접 원소가 함께 로드됨.
중간 삽입/삭제가 잦으면 배열이 불리 (O(N) shift). 그럼에도 PS 에서는 대부분의 경우 배열이 먼저.
시각화
메모리 구조
flowchart LR
P["base 포인터"]
A0["arr[0]<br/>addr: base"]
A1["arr[1]<br/>addr: base+4"]
A2["arr[2]<br/>addr: base+8"]
A3["arr[3]<br/>addr: base+12"]
P --> A0 --> A1 --> A2 --> A3
arr[i] 에 접근하면 base + i × sizeof(int) 주소를 읽는다. 상수 시간 연산.
동적 배열 확장 (push_back)
flowchart TD
S["push_back(x) 호출"]
Q["n == capacity?"]
E["새 배열 2배 할당<br/>기존 원소 복사 O(N)"]
W["arr[n] = x, n++"]
D["완료 - amortized O(1)"]
S --> Q
Q -->|"Yes"| E
E --> W
Q -->|"No"| W
W --> D
재할당이 필요한 경우는 드물다. N번 push_back 의 총 복사 비용은 O(N) → 1회 평균 O(1).
핵심 아이디어
정적 배열 vs 동적 배열
| 항목 | 정적 배열 | 동적 배열 |
|---|---|---|
| 크기 결정 시점 | 컴파일 타임 | 런타임 |
| 메모리 재할당 | 없음 | 용량 초과 시 2배 확장 |
| tail 삽입 | 불가 (고정) | O(1) amortized |
| 메모리 오버헤드 | 없음 | 용량 - 실제 크기 |
언어별 구현
| 언어 | 정적 배열 | 동적 배열 |
|---|---|---|
| C++ | int arr[N], std::array<int,N> | std::vector<int> |
| Python | - | list (항상 동적) |
| Java | int[] arr = new int[N] | ArrayList<Integer> |
IMPORTANT
Python list 와 Java ArrayList<Integer> 는 내부적으로 참조/박싱 오버헤드가 있어 int 기본형 배열보다 메모리를 더 쓴다. 성능이 중요한 Java PS 코드에서는 int[] 를 우선한다.
캐시 지역성
배열 순차 탐색: arr[0], arr[1], arr[2], ...
-> 캐시 라인(64 bytes)에 연속 원소 16개가 함께 로드
-> 캐시 히트율 높음, 매우 빠름
연결 리스트 탐색: node0 -> node1 -> node2 -> ...
-> 각 노드가 힙 메모리 전역에 산재
-> 캐시 miss 빈발, 실제 느림
이론 복잡도가 같아도 배열 순회가 연결 리스트보다 수 배 빠른 이유.
알고리즘
1. 중간 삽입 (인덱스 k)
insert(arr, n, k, x):
for i = n-1 downto k:
arr[i+1] = arr[i] // 오른쪽으로 shift
arr[k] = x
n += 1
시간: O(N) (k 이후 원소 shift). 배열 크기 여유가 필요하다.
2. 중간 삭제 (인덱스 k)
delete(arr, n, k):
for i = k+1 to n-1:
arr[i-1] = arr[i] // 왼쪽으로 shift
n -= 1
시간: O(N). 마지막 원소 삭제는 O(1) (shift 불필요).
3. 동적 배열 확장 (push_back)
push_back(arr, n, capacity, x):
if n == capacity:
new_arr = allocate(2 * capacity) // O(capacity)
copy arr[0..n-1] to new_arr // O(N)
capacity = 2 * capacity
arr = new_arr
arr[n] = x
n += 1
재할당 발생 횟수: O(log N). 각 재할당 시 복사 비용 총합: 1 + 2 + 4 + … + N = O(N) → 1회 평균 O(1) amortized.
구현
// 정적 배열 vs 동적 배열 (vector) 기본 연산
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
// 정적 배열 (최대 크기 선언)
int a[100005];
for (int i = 0; i < n; i++) cin >> a[i];
// 동적 배열 (vector)
vector<int> v(a, a + n);
// 랜덤 접근 O(1)
cout << "a[0]=" << a[0] << " v[n-1]=" << v[n-1] << "\n";
// 정렬 O(N log N)
sort(v.begin(), v.end());
cout << "sorted[0]=" << v[0] << " sorted[n-1]=" << v[n-1] << "\n";
// tail 삽입 O(1) amortized
v.push_back(-1);
cout << "after push_back: size=" << v.size() << "\n";
// 중간 삽입 O(N)
v.insert(v.begin() + 1, 999);
cout << "after insert(1,999): v[1]=" << v[1] << "\n";
return 0;
}5
3 1 4 1 5a[0]=3 v[n-1]=5
sorted[0]=1 sorted[n-1]=5
after push_back: size=6
after insert(1,999): v[1]=999복잡도
| 연산 | 정적 배열 | 동적 배열 (amortized) | 비고 |
|---|---|---|---|
| 랜덤 접근 arr[i] | O(1) | O(1) | base + i 계산 |
| tail 삽입 | 불가 | O(1) amortized | 가끔 O(N) 재할당 |
| head/중간 삽입 | O(N) | O(N) | 전체/부분 shift |
| tail 삭제 | O(1) | O(1) | 마지막 원소 제거 |
| 중간 삭제 | O(N) | O(N) | shift 비용 |
| 선형 탐색 | O(N) | O(N) | unsorted |
| 이분 탐색 (정렬 후) | O(log N) | O(log N) | 정렬 상태 필요 |
| 공간 | O(N) | O(N) | 동적은 최대 2N |
변형 / 활용
| 패턴 | 설명 |
|---|---|
| **[[prefix-sum | 누적 합]]** |
| **[[difference-array | 차분 배열]]** |
| **정렬 + [[binary-search | 이분 탐색]]** |
| **[[stack | 스택]] / [[deque |
| 2차원 배열 (격자) | arr[r][c] = arr[r*C + c]. 시뮬레이션/BFS 에서 핵심 |
| 비트마스크 배열 | bool 대신 bitset 으로 8배 메모리 절약 |
함정
WARNING
C++ 범위 초과 (OOB): int arr[N] 에서 arr[N] 접근 시 undefined behavior (UB). 런타임 에러 없이 이상한 값 반환하거나 segfault. 반드시 인덱스 범위 확인.
WARNING
C++ vector iterator 무효화: push_back, insert, erase 후 기존 iterator 가 무효화될 수 있다. 루프 안에서 insert/erase 후 iterator 재사용 금지.
CAUTION
Python list.insert(i, x): 시간 복잡도 O(N). Python 에서 중간 삽입을 N번 하면 O(N^2). collections.deque 의 appendleft (O(1)) 또는 다른 자료구조 검토.
IMPORTANT
정렬된 배열에 삽입: 삽입 위치를 이분 탐색 O(log N) 으로 찾을 수 있지만, shift 는 여전히 O(N). 총 삽입 N번이면 O(N^2).
BOJ 연습 문제
| 번호 | 제목 | 설명 |
|---|---|---|
| BOJ 10818 | 최소, 최대 | 배열 선형 탐색 |
| BOJ 2562 | 최댓값 | 인덱스 함께 추적 |
| BOJ 11399 | ATM | 누적 합 기초 |
| BOJ 10989 | 수 정렬하기 3 | 카운팅 정렬 (배열 활용) |
| BOJ 1546 | 평균 | 배열 변환 |
관련 위키
이 글의 용어 (8개)
- 누적 합 (Prefix Sum)algorithm
- 정의 누적 합 (Prefix Sum) 은 배열 에 대해 (또는 1-indexed ) 을 미리 계산해 두고, 임의 구간 합 을 O(1) 에 구하는 정형. 문제 풀이에서 "구간 N …
- 덱 (Deque)algorithm
- 정의 Deque (덱, double-ended queue) 는 양쪽 끝에서 O(1) push/pop 이 가능한 선형 자료구조. 큐 + 스택의 일반화. 내부 구현은 대개 chunk…
- 세그먼트 트리 (Segment Tree)algorithm
- 정의 세그먼트 트리 (Segment Tree) 는 배열의 구간 쿼리 (range query) 와 점 갱신 (point update) 를 모두 O(log N) 에 처리하는 이진 트…
- 스택 (Stack)algorithm
- 정의 스택 (Stack) 은 LIFO (Last In, First Out) 순서로 원소를 관리하는 추상 자료구조입니다. , , 세 연산만 제공하며, 가장 최근 삽입된 원소만 접근…
- 연결 리스트 (Linked List)algorithm
- 정의 연결 리스트 (Linked List) 는 노드 + 포인터로 구성된 선형 자료구조. 각 노드는 데이터 + 다음 노드 포인터. 종류: - Singly Linked List: 노…
- 이분 탐색 (Binary Search)algorithm
- 정의 이분 탐색 (Binary Search) 은 정렬된 시퀀스에서 목표값의 위치를 O(log N) 에 찾는 알고리즘. 매 단계에서 후보 구간을 절반으로 줄인다. 탐색이 본질이 아…
- 자료구조 (Data Structures)algorithm
- 정의 자료구조 (Data Structure) 는 데이터를 효율적으로 저장하고 접근하기 위한 조직화 방법. 문제 풀이에서는 시간 복잡도와 공간 복잡도의 trade-off 를 정확히…
- 차분 배열 (Difference Array)algorithm
- 정의 차분 배열 (Difference Array) 은 배열 의 인접 차 (adjacent difference) 를 저장하는 배열 (단, ). 원 배열은 , 즉 d 의 누적 합 (…
💬 댓글