본문으로 건너뛰기
김신건의 로그

배열 (Array)

· 수정 · 📖 약 4분 · 1,599자/단어 #algorithm #data-structure #array #random-access
array, 배열, Array, 정적 배열, 동적 배열, static array, dynamic array

정의

배열 (Array) 는 동일한 타입의 원소를 메모리에 연속으로 저장하는 가장 기본적인 선형 자료구조다.

  • 정적 배열 (Static Array): 크기가 컴파일 타임에 고정. C++ int arr[N], Java int[].
  • 동적 배열 (Dynamic Array): 런타임에 크기 조정 가능. C++ std::vector, Python list, Java ArrayList.

인덱스 i 번째 원소의 주소 = base_address + i × element_size. 이 계산이 O(1) 이므로 랜덤 접근 O(1) 이 보장된다.

PS 에서 가장 많이 사용하는 자료구조. 캐시 친화성과 단순한 인터페이스 덕에 다른 자료구조로 교체 가능한 상황에서도 배열을 우선 고려한다.

문제 상황과 동기

N개 원소를 저장하고, 특정 위치 접근이 잦은 경우를 생각하자.

  • naive (별도 변수): a, b, c, d… 인덱스로 접근 불가. 루프 작성 불가능.
  • 연결 리스트: 임의 접근 O(N), 캐시 miss 빈발.
  • 배열: 임의 접근 O(1), 캐시 지역성 우수.

핵심 통찰: 메모리 연속 배치 가 가져오는 두 가지 이점:

  1. 랜덤 접근 O(1): arr[i] = *(base + i) 계산 한 번.
  2. 캐시 지역성 (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 (항상 동적)
Javaint[] 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;
}
stdin
5
3 1 4 1 5
결과
a[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

중간 삽입/삭제 O(N) 를 N번 하면 O(N^2). 삽입/삭제가 빈번하면 연결 리스트 또는 세그먼트 트리 검토.

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.dequeappendleft (O(1)) 또는 다른 자료구조 검토.

IMPORTANT

정렬된 배열에 삽입: 삽입 위치를 이분 탐색 O(log N) 으로 찾을 수 있지만, shift 는 여전히 O(N). 총 삽입 N번이면 O(N^2).

BOJ 연습 문제

번호제목설명
BOJ 10818최소, 최대배열 선형 탐색
BOJ 2562최댓값인덱스 함께 추적
BOJ 11399ATM누적 합 기초
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 의 누적 합 (…

이 개념을 다룬 위키 페이지 (1)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기