배열 vs 연결 리스트
배열(Array)과 연결 리스트(Linked List)는 데이터를 순서 있게 저장하는 가장 기본적인 자료구조입니다. 배열은 메모리에 연속으로 저장해 인덱스로 즉시 접근하고, 연결 리스트는 노드가 다음 노드의 주소를 가리키며 분산 저장합니다.
핵심 개념
배열의 메모리 구조
배열은 같은 타입의 데이터를 메모리에 연속으로 저장합니다. 시작 주소와 인덱스만 알면 임의 원소에 O(1)으로 접근할 수 있습니다. 크기는 선언 시 고정되며, 중간 삽입·삭제 시 원소를 한 칸씩 이동해야 해 O(n)이 걸립니다.
핵심 개념
연결 리스트의 메모리 구조
연결 리스트의 각 노드는 데이터(data)와 다음 노드를 가리키는 포인터(next)로 구성됩니다. 노드들이 메모리에 분산 저장되어 위치를 알고 있는 노드의 삽입·삭제가 O(1)이지만, 특정 위치까지 순차 탐색이 필요해 임의 접근은 O(n)입니다.
핵심 개념
시간 복잡도 비교
임의 접근: 배열 O(1) vs 연결 리스트 O(n). 맨 앞 삽입: 배열 O(n) vs 연결 리스트 O(1). 맨 뒤 삽입: 배열 O(1) vs 연결 리스트 O(n) 또는 tail 포인터 유지 시 O(1). 탐색(값으로): 둘 다 O(n).
핵심 개념
캐시 지역성(Cache Locality)
배열은 연속 메모리이므로 CPU 캐시 히트율이 높습니다. 연결 리스트는 노드가 힙 메모리 곳곳에 흩어져 있어 캐시 미스가 자주 발생합니다. 그래서 이론적 복잡도가 같아도 실제 성능은 캐시 효율 때문에 크게 달라질 수 있습니다.
핵심 개념
단순/이중/원형 연결 리스트
단순 연결 리스트(Singly)는 next 포인터만 가집니다. 이중 연결 리스트(Doubly)는 prev·next 포인터를 모두 가져 양방향 순회와 O(1) 삭제가 가능합니다. 원형(Circular)은 마지막 노드가 첫 노드를 가리켜 순환 구조를 이룹니다.
핵심 개념
동적 배열(Dynamic Array)
Java의 ArrayList, Python의 list가 동적 배열입니다. 내부적으로 고정 배열을 사용하되, 크기 초과 시 더 큰 배열을 새로 할당해 복사합니다. 성장 배율은 구현마다 다른데, OpenJDK의 ArrayList는 약 1.5배(oldCapacity + oldCapacity/2)로 늘립니다. 분할 상환 분석(Amortized)으로 append는 O(1)이며, 배열의 인덱스 접근 장점과 동적 크기 조정을 모두 갖습니다.
왜 둘을 비교하나
배열과 연결 리스트는 '순서 있는 데이터를 저장한다'는 같은 목적을 서로 반대되는 방식으로 풉니다. 배열은 메모리를 연속으로 잡아 위치 계산으로 바로 접근하고, 연결 리스트는 흩어진 노드를 포인터로 이어 붙입니다. 어느 쪽이 낫다기보다 어떤 연산이 많은지에 따라 갈립니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
배열과 연결 리스트의 차이를 설명해 주세요.
배열은 연속 메모리라 인덱스로 O(1) 접근이 되지만 중간 삽입·삭제는 원소를 밀어야 해서 O(n)입니다. 연결 리스트는 포인터로 이어져 노드 위치만 알면 삽입·삭제가 O(1)이지만 임의 접근은 O(n)입니다.
면접 질문
동적 배열의 append가 왜 분할 상환 O(1)인가요?
용량이 찰 때만 O(n) 복사가 일어나지만 용량을 일정 배율로 늘리면 그 복사가 드물게 일어나서, n번 추가하는 전체 비용을 n으로 나누면 상수가 됩니다.
면접 질문
ArrayList와 LinkedList 중 무엇을 쓰나요?
대부분의 경우 ArrayList가 낫습니다. LinkedList는 양쪽 끝에서 자주 넣고 빼는 Deque 용도에서 쓸 수 있지만, 같은 용도라면 ArrayDeque가 더 빠르다고 Java 문서가 설명합니다.
면접 질문
이중 연결 리스트의 장점은 무엇인가요?
prev 포인터가 있어 양방향 순회가 되고, 삭제할 노드를 알면 앞 노드를 따로 찾지 않고 O(1)에 지울 수 있습니다. 대신 노드마다 포인터가 하나 더 듭니다.
실제 시스템에서는
ArrayList 문서는 크기 조정 가능한 배열 구현이고 add가 분할 상환 상수 시간이라고 설명합니다. LinkedList는 이중 연결 리스트이고 인덱스로 접근하면 처음이나 끝 중 가까운 쪽에서 순회합니다.
list는 객체 참조를 담은 가변 길이 배열입니다. 양쪽 끝에서 자주 넣고 빼려면 collections.deque를 쓰라고 문서가 안내합니다.
함께 보면 좋은 토픽
참고 자료
Java — 배열 직접 구현 vs ArrayList
/* ── 고정 배열 — 인덱스 직접 접근 ─────────────────────── */ int[] arr = new int[5]; // 크기 고정, 연속 메모리 할당 arr[0] = 10; arr[1] = 30; // O(1) 임의 접근 /* ── 중간 삽입: O(n) — 원소 이동 필요 ──────────────────── */ public static void insertAt(int[] arr, int idx, int val, int size) { // 뒤에서부터 한 칸씩 오른쪽으로 이동 for (int i = size - 1; i >= idx; i--) { arr[i + 1] = arr[i]; } arr[idx] = val; } /* ── 동적 배열 ArrayList — 내부는 배열, 크기 자동 조정 ──── */ List<Integer> list = new ArrayList<>(); list.add(10); // Amortized O(1) — 공간 초과 시 2배 재할당 list.add(1, 20); // O(n) — 이후 원소 이동 list.get(0); // O(1) — 인덱스 직접 접근 list.remove(0); // O(n) — 이후 원소 앞으로 이동 // ArrayList 내부 재할당 원리 // DEFAULT_CAPACITY = 10, 초과 시 newCapacity = oldCapacity * 1.5 // Arrays.copyOf()로 새 배열에 복사 후 참조 교체 /* ── 배열 이진 탐색 — 정렬된 배열에서 O(log n) ─────────── */ int[] sorted = {10, 20, 30, 40, 50}; int idx = Arrays.binarySearch(sorted, 30); // idx = 2
배열은 메모리에 연속으로 저장되므로 arr[i] 접근은 시작 주소 + i × 타입크기로 계산해 O(1)입니다.
중간 삽입·삭제는 이후 원소를 이동해야 하므로 O(n)입니다.
ArrayList는 내부 배열이 꽉 차면 1.5배 크기의 새 배열을 할당하고 복사합니다. add()의 분할 상환 복잡도는 O(1)입니다.
정렬된 배열에서는 이진 탐색으로 O(log n) 탐색이 가능합니다.
연결 리스트는 이진 탐색 불가능(임의 접근 없음)합니다.
Java — 연결 리스트 직접 구현 vs LinkedList
/* ── 단순 연결 리스트 노드 ──────────────────────────────── */ class Node<T> { T data; Node<T> next; Node(T data) { this.data = data; } } /* ── 중간 삽입: O(1) — 포인터 교환만 필요 ──────────────── */ public void insertAfter(Node<Integer> prev, int val) { Node<Integer> newNode = new Node<>(val); newNode.next = prev.next; // 새 노드가 다음 노드 가리킴 prev.next = newNode; // 이전 노드가 새 노드 가리킴 // 포인터 2번 변경으로 완료 — 원소 이동 없음 } /* ── 탐색: O(n) — 순차 순회 필요 ───────────────────────── */ public Node<Integer> findNode(Node<Integer> head, int val) { Node<Integer> cur = head; while (cur != null) { if (cur.data == val) return cur; cur = cur.next; } return null; } /* ── Java LinkedList — 이중 연결 리스트 ─────────────────── */ LinkedList<Integer> linked = new LinkedList<>(); linked.addFirst(10); // O(1) — head 포인터 변경 linked.addLast(50); // O(1) — tail 포인터 변경 linked.add(1, 20); // O(n) 탐색 + O(1) 삽입 linked.get(2); // O(n) — 순차 탐색 linked.removeFirst(); // O(1) — head 포인터 변경 /* ── Deque로 사용 시 — 양끝 O(1) ───────────────────────── */ Deque<Integer> deque = new LinkedList<>(); deque.offerFirst(1); // 앞에 추가 deque.offerLast(2); // 뒤에 추가 deque.pollFirst(); // 앞에서 제거
연결 리스트 삽입·삭제는 포인터 2개만 바꾸면 되므로 위치를 알고 있을 때 O(1)입니다.
단, 삽입 위치까지 탐색하는 과정이 O(n)이므로 전체 복잡도는 탐색+삽입입니다.
Java의 LinkedList는 이중 연결 리스트로 prev·next 포인터를 모두 갖습니다. head/tail 포인터도 유지해 맨 앞/뒤 삽입·삭제는 O(1)입니다.
임의 접근(get(i))은 O(n)이므로 인덱스 기반 조회가 많다면 ArrayList가 훨씬 유리합니다.
실무에서 LinkedList가 가장 빛나는 용도는 양끝 삽입/삭제가 빈번한 Deque 구현입니다.
Q1
배열과 연결 리스트 중 임의 인덱스 접근(random access)이 O(1)인 것은?
Q2
연결 리스트가 배열보다 유리한 경우는?
Q3
배열의 삽입/삭제가 연결 리스트보다 비효율적인 이유는?