본문 바로가기
카테고리 없음

자료구조 - 배열과 연결리스트

by kik328288 2026. 5. 14.

자료구조 (배열, 연결리스트, 시간복잡도)

컴퓨터 과학에서 가장 먼저 배우는 두 자료구조가 배열과 연결리스트다. 두 구조는 데이터를 메모리에 어떻게 늘어놓을 것인가에 대한 가장 본질적인 두 답이며, 이후 등장하는 모든 고급 자료구조(스택·큐·트리·해시·그래프)가 이 둘의 변형이거나 조합이라고 해도 과언이 아니다. 본 글은 정보처리기사 시험 출제 범위와 학교 자료구조 수업 입문 영역을 모두 닿도록 배열과 연결리스트의 메모리 구조, 시간복잡도, 실전 선택 기준을 정리한다(출처: Python 공식 자료구조 문서). 제가 학교 자료구조 수업에서 처음 두 구조의 시간복잡도 표를 봤을 때 솔직히 "그래서 무엇을 언제 쓰라는 거지?" 한 줄로 정리되지 않아 한참 헤맸는데, 직접 두 구조로 같은 알고리즘을 짜고 실행 시간을 재 본 후로는 "임의 접근이 많은가, 중간 삽입이 많은가" 한 줄로 선택 기준이 정리되었다.

 

배열 자료구조의 메모리 구조와 시간복잡도

배열(Array)은 같은 타입의 데이터를 연속된 메모리 공간에 일렬로 늘어놓는 가장 기본적인 자료구조이다. 가장 큰 특징은 메모리가 연속적이라는 점이며, 이 한 가지 특성에서 배열의 모든 강점과 약점이 파생된다. 메모리가 연속되어 있기 때문에 i번째 원소의 주소는 시작 주소 + i × 원소 크기라는 단순한 산술 연산으로 계산되며, 그 결과 배열은 인덱스만 알면 O(1) 시간에 어떤 원소든 곧바로 꺼낼 수 있다. 여기서 임의 접근(Random Access)이란 인덱스로 직접 위치를 계산해 한 번에 도달하는 접근 방식을 의미하며, 배열의 가장 결정적인 강점이다.

배열은 크게 정적 배열과 동적 배열로 나뉜다. 정적 배열(Static Array)은 컴파일 시점에 크기가 정해져 변경할 수 없는 형태로, C/C++의 int arr[10] 같은 선언이 대표적이다. 동적 배열(Dynamic Array)은 실행 중에 크기를 늘릴 수 있는 형태로, 파이썬 list, 자바 ArrayList, C++ std::vector가 모두 여기에 속한다. 동적 배열은 내부적으로 더 큰 새 배열을 할당하고 기존 원소를 복사하는 방식으로 확장되며, 이 확장 비용은 평균(amortized) O(1)로 분석된다. 분할 상환 분석(amortized analysis)이란 가끔 발생하는 큰 비용을 자주 발생하는 작은 비용에 평균적으로 나누어 평가하는 분석 기법을 가리킨다.

// C — 정적 배열, 크기 고정
int arr[5] = {10, 20, 30, 40, 50};
printf("%d\n", arr[2]);   // O(1) 임의 접근 → 30

// 중간 삽입은 O(n) — 뒷쪽을 한 칸씩 밀어야 함
for (int i = 4; i > 2; i--) arr[i] = arr[i-1];
arr[2] = 99;              // 이제 arr = {10, 20, 99, 30, 40}

배열의 시간복잡도를 표로 정리하면 다음과 같다.

연산 시간복잡도 비고
인덱스 접근 O(1) 가장 큰 강점
맨 뒤 삽입(append) 평균 O(1), 최악 O(n) 동적 배열 확장 시
맨 앞·중간 삽입 O(n) 뒤 원소들을 한 칸씩 밀어야 함
맨 앞·중간 삭제 O(n) 뒤 원소들을 한 칸씩 당겨야 함
탐색(값으로 찾기) O(n) 정렬되어 있다면 이진 탐색으로 O(log n)

배열의 또 다른 강점은 캐시 친화성이다. 메모리가 연속되어 있어 CPU가 한 원소를 읽을 때 그 주변까지 캐시에 함께 들고 오기 때문에, 순회 작업의 실제 속도가 연결리스트보다 압도적으로 빠른 경우가 많다. 솔직히 이건 예상 밖이었는데, 이론상 같은 O(n)인 순회 작업도 배열이 연결리스트보다 3~10배 빨리 끝나는 모습을 직접 측정한 후로는 시간복잡도 표 한 장이 전부가 아니라는 점을 손끝으로 받아들였다.

연결리스트, 동적 메모리와 노드 연결

연결리스트(Linked List)는 데이터를 메모리의 흩어진 곳에 두고, 각 노드(Node)가 다음 노드를 가리키는 포인터로 연결해 순서를 표현하는 자료구조이다. 메모리 연속성을 포기하는 대신 크기 변경과 중간 삽입·삭제의 자유도를 얻는다. 한 노드는 데이터 값과 다음 노드를 가리키는 포인터(C에서는 next, 자바에서는 참조)를 함께 가지며, 이 단위가 사슬처럼 이어진다.

연결리스트는 연결 방향에 따라 세 종류로 나뉜다. 첫째, 단일 연결리스트(Singly Linked List)는 각 노드가 다음 노드만 가리키는 가장 단순한 형태이다. 둘째, 이중 연결리스트(Doubly Linked List)는 각 노드가 다음과 이전 두 방향을 모두 가리켜, 양방향 순회와 임의 노드 삭제가 모두 O(1)에 가능하다. 셋째, 원형 연결리스트(Circular Linked List)는 마지막 노드가 다시 첫 노드를 가리키는 형태로, 순환 구조가 필요한 라운드 로빈 스케줄링 같은 영역에서 활용된다. 여기서 라운드 로빈이란 정해진 순서대로 자원을 순환하며 분배하는 운영체제의 CPU 스케줄링 기법을 의미한다.

# Python — 단일 연결리스트
class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None

    def push_front(self, value):       # O(1) — 맨 앞 삽입
        node = Node(value)
        node.next = self.head
        self.head = node

    def insert_after(self, target, value):  # O(1) — 노드 뒤에 삽입
        node = Node(value)
        node.next = target.next
        target.next = node

    def get(self, idx):                # O(n) — 인덱스 접근
        cur = self.head
        for _ in range(idx):
            if cur is None: return None
            cur = cur.next
        return cur.value

연결리스트의 시간복잡도를 표로 정리하면 다음과 같다.

연산 시간복잡도 비고
인덱스 접근 O(n) 처음부터 따라가야 함
맨 앞 삽입·삭제 O(1) 가장 큰 강점
알려진 노드 뒤 삽입·삭제 O(1) 포인터만 갈아 끼움
맨 뒤 삽입 O(n) 또는 O(1)* * tail 포인터 유지 시 O(1)
탐색(값으로 찾기) O(n) 정렬해도 이진 탐색 불가

연결리스트의 결정적 강점은 포인터 갈아 끼우기만으로 끝나는 중간 삽입·삭제이다. 배열은 한 원소를 중간에 넣으려면 뒤의 모든 원소를 한 칸씩 밀어야 하지만, 연결리스트는 인접 노드 두 개의 포인터만 갱신하면 끝난다. 다만 인덱스 i번째 노드를 찾으려면 처음부터 i번 따라가야 하므로, 임의 접근은 O(n)이다. 또 노드마다 포인터 공간이 추가로 필요해 메모리 오버헤드가 배열보다 크고, 메모리가 흩어져 있어 캐시 효율도 낮다.

제가 학교 프로젝트에서 처음 단일 연결리스트를 직접 짜 본 게 학생 수강신청 대기열을 구현하는 과제였는데, 등록 취소가 빈번한 상황에서 배열로 짜다가 매번 뒤 원소를 다 미는 비용이 누적되어 결국 연결리스트로 갈아 끼운 경험이 있다. 그 한 번의 비교가 두 구조의 차이를 가장 직관적으로 가르쳐 준 사례였다.

배열과 연결리스트, 언제 무엇을 쓸 것인가

두 구조의 선택은 결국 "어떤 연산이 가장 자주 일어나는가"라는 한 질문으로 좁힌다. 임의 접근(인덱스 조회)이 많은 환경이라면 배열, 중간 삽입·삭제가 많은 환경이라면 연결리스트가 자연스러운 선택이다. 다음 표는 시험과 실무 모두에서 자주 참조되는 비교표이다(출처: 위키백과 — Introduction to Algorithms).

기준 배열 연결리스트
메모리 배치 연속 불연속
인덱스 접근 O(1) O(n)
맨 앞 삽입·삭제 O(n) O(1)
중간 삽입·삭제(노드 알면) O(n) O(1)
크기 변경 가능(동적 배열) 항상 가능
캐시 효율 매우 좋음 나쁨
메모리 오버헤드 작음 노드당 포인터 추가

실전에서 자주 헷갈리는 지점이 "파이썬 list가 배열인가 연결리스트인가"이다. 결론부터 말하면 파이썬 list동적 배열이며, 자바의 ArrayList와 같은 부류이다. 자바의 LinkedList만이 진짜 연결리스트이고, 따라서 파이썬에서 큐를 만들고 싶을 때 list.pop(0)을 쓰면 O(n)이 되는 사고가 자주 발생한다. 이 경우에는 collections.deque가 양방향 연결리스트에 가까운 구현체를 제공하므로 그쪽을 선택해야 한다(출처: Python collections — deque). 솔직히 제 경험상 학교 알고리즘 과제에서 가장 자주 시간 초과가 났던 사고가 정확히 list.pop(0) 한 줄이었고, 그 한 줄을 deque.popleft()로 갈아 끼우는 것만으로도 시간이 5배 이상 줄어드는 경험을 직접 했다.

마지막으로 정보처리기사 실기 답안에서 자주 쓰이는 정형 표현 하나를 정리해 두면, "배열은 임의 접근에 강하고 삽입·삭제에 약하며, 연결리스트는 그 반대이다. 따라서 인덱스 기반 조회가 많으면 배열을, 중간 삽입·삭제가 많으면 연결리스트를 선택한다"라는 두 문장이다. 시험에서 한 줄짜리 비교 답안을 요구할 때 이 두 문장이 그대로 쓰일 만큼 표준화된 표현이며, 그 뒤에 시간복잡도 표를 곁들이면 가장 단단한 답안이 된다. 다음 글에서는 이 두 구조 위에 책임을 부여해 만든 첫 추상 자료형(ADT)인 스택과 큐를 이어 다룬다.


메타 디스크립션: 배열과 연결리스트의 메모리 구조, 시간복잡도, 실전 선택 기준을 비교표·코드 예시·실무 사례와 함께 자료구조 입문자 관점에서 세심하게 정리합니다.


소개 및 문의 · 개인정보처리방침 · 면책조항

© 2026 블로그 이름