공부 기록/자료구조

Arrays & Linked Lists

와일 2026. 3. 14. 18:10
반응형

1. 배열(Array)이란?

배열은 같은 타입의 데이터를 연속적으로 나열한 자료구조다.
각 칸에는 인덱스(0, 1, 2…)가 붙어 있어서 A[i]처럼 바로 접근할 수 있다.

Python에서는 array 클래스와 list 클래스가 별개로 존재하는데, 편의상 같은 타입의 list를 배열처럼 사용하는 방식으로 진행한다.


2. 배열의 3가지 핵심 연산

배열에서 수행하는 연산은 크게 세 가지다.

  • Search (Find): 특정 값 찾기
  • Insert (Add): 새 원소 추가
  • Delete (Remove): 원소 제거

여기서 중요한 점은, 배열이 정렬되어 있느냐 아니냐에 따라 각 연산의 시간복잡도가 완전히 달라진다는 것이다.


3. Sorted vs Unsorted: Trade-off

Unsorted Array

순서를 신경 쓰지 않아도 되니까 삽입/삭제가 자유롭다.

  • Search: 처음부터 끝까지 다 봐야 함 → O(n)
  • Insert: 맨 뒤에 그냥 붙이면 됨 → O(1)
  • Delete: 지우고 싶은 원소와 마지막 원소를 swap한 뒤, 마지막 원소 제거 → O(1)

여기서 swap이 왜 O(1)이냐고 할 수 있는데, swap은 n이 얼마든
"temp에 저장 → A에 B 넣기 → B에 temp 넣기" 딱 3번으로 끝난다.
연산 횟수가 n에 비례해서 늘어나지 않으니까 O(1)이다.

Sorted Array

순서를 유지해야 하니까 삽입/삭제가 까다로운 대신, 탐색이 빠르다.

  • Search: 이진 탐색(Binary Search) 사용 가능 → O(log n)
  • Insert: 자리를 찾고 뒤 원소들을 전부 한 칸씩 밀어야 함 → O(n)
  • Delete: 구멍을 메우기 위해 뒤 원소들을 당겨야 함 → O(n)
연산 Sorted Array Unsorted Array
Search O(log n) O(n)
Insert O(n) O(1)
Delete O(n) O(1)
Sorting O(n log n) X

결론: 어떤 자료구조도 모든 연산에서 최고일 수는 없다. 상황에 맞게 골라 써야 한다.


4. Binary Search가 왜 O(log n)인가?

이진 탐색은 매 단계마다 탐색 범위를 절반씩 줄여나간다.

n = 8이라고 하면:

  • 1번째 루프: 8개 → 4개
  • 2번째 루프: 4개 → 2개
  • 3번째 루프: 2개 → 1개

딱 3번 만에 끝난다. 그런데 8 = 2³이므로 루프 횟수 = log₂8 = 3이다.
n개짜리 배열이면 log₂n번 만에 끝나니까 O(log n)이다.


5. 배열의 한계(Drawbacks)

배열에는 두 가지 구조적 약점이 있다.

① 크기가 고정되어 있음 (Dynamic Resizing)

배열이 꽉 차면 더 큰 배열을 새로 만들고, 기존 데이터를 전부 복사해야 한다.
이 복사 작업은 O(n)이다.

그런데 배열 크기를 2배씩 늘리면, resize가 아주 가끔만 일어난다.
(1칸씩 늘리면 매번 resize가 일어나서 총 비용이 O(n²)이 된다.)

2배씩 늘릴 때 총 복사 비용을 계산하면:
1 + 2 + 4 + 8 + … + n = 약 2n

n번 append하는 동안 총 비용이 2n이니까, append 한 번당 평균 비용은 2, 즉 O(1)이다.
이를 Amortized O(1) 이라고 한다 — 가끔 O(n)이 발생하더라도, 장기적으로 평균 내면 O(1)로 수렴한다.

② 메모리가 연속적이어야 함

배열은 메모리에 연속으로 붙어있어야 한다.
그래서 두 배열을 이어붙이려면 새 배열을 만들어야 하는 불편함이 있다.

이 두 가지 한계 때문에 등장한 것이 연결 리스트(Linked List) 다.


6. 연결 리스트(Linked List)

메모리가 연속적이지 않아도 되고, 각 노드(Node)가 포인터로 다음 노드를 직접 가리키는 방식으로 연결된다.

Singly Linked List ⭐

각 노드는 elementnext 포인터를 가진다.

head → [A|•] → [B|•] → [C|•] → [D|∅] ← tail

핵심 메서드 3가지:

addFirst(e)

  • 빈 리스트일 때: head = newest, tail = newest (같은 노드를 둘 다 가리킴)
  • 원소가 있을 때: newest.next = headhead = newest
  • O(1)

addLast(e)

  • 빈 리스트일 때: head = newest, tail = newest
  • 원소가 있을 때: tail.next = newesttail = newest
  • O(1)

removeFirst()

  • head = head.next로 head를 옮기면 됨
  • 삭제 후 빈 리스트가 되면 → tail = None (안 하면 tail이 삭제된 노드를 계속 가리킴)
  • O(1)

문제점: tail 삭제가 O(n)

tail을 삭제하려면 새로운 tail, 즉 기존 tail 바로 앞 노드를 알아야 한다.
그런데 Singly Linked List는 next로만 이동할 수 있어서 앞으로 거슬러 올라갈 방법이 없다. 결국 head부터 끝까지 다시 순회해야 하므로 O(n)이 된다.

Doubly Linked List

이 문제를 해결하기 위해 각 노드에 prev 포인터를 추가한다.

header ↔ [A] ↔ [B] ↔ [C] ↔ [D] ↔ trailer

headertrailer라는 더미 노드를 양 끝에 두면, 모든 삽입/삭제가
동일한 패턴으로 처리되어 엣지 케이스(head/tail 예외처리)가 사라진다.

tail 삭제도 tail._prev로 바로 접근 가능 → O(1)


7. 최종 시간복잡도 비교

연산 Sorted Array Unsorted Array Singly Linked Doubly Linked
Access(i) O(1) O(1) O(n) O(n)
Search O(log n) O(n) O(n) O(n)
Insert O(n) O(1) O(1) O(1)
Delete O(n) O(1) O(1) O(1)
Delete at Last O(1) O(1) O(n) O(1)

배열은 인덱스로 바로 접근(O(1))할 수 있지만,
연결 리스트는 head부터 순서대로 따라가야 해서 접근이 O(n)이다.
그 대신 삽입/삭제는 포인터만 바꾸면 되니까 O(1)이다.

결론: 정답인 자료구조는 없다. 상황에 맞게 trade-off를 이해하고 골라 쓰는 것이 핵심이다.

반응형

'공부 기록 > 자료구조' 카테고리의 다른 글

Priority Queues and Heaps  (5) 2026.04.17
Trees  (0) 2026.04.01
Queues and Stacks  (0) 2026.03.23
Recursion  (0) 2026.03.23
Algorithm Analysis (+자료구조를 배워야 하는 이유)  (0) 2026.03.09