⚡ 한 줄 요약: 데이터를 효율적으로 관리하는 자료구조의 분류(선형/비선형)를 이해하고, Big-O 표기법을 통해 알고리즘의 시간 및 공간 효율성을 판단하는 기준을 배웁니다.
프로그램의 성능은 어떤 자료구조를 선택하느냐에 따라 결정됩니다.
단순히 코드가 동작하는 것을 넘어, 데이터의 검색/삽입/삭제 속도를 최적화하기 위해서는 데이터의 특성에 맞는 구조를 선택할 수 있어야 합니다.
이번 글에서는 자료구조의 큰 흐름과 성능 분석의 필수 지표인 복잡도에 대해 알아봅니다.
🧐 Why:
🎯 Goal:
자료구조를 한마디로 정의하면 데이터를 효율적으로 관리하는 방법입니다.
우리가 효율적인 자료구조를 선택해야 하는 이유는 데이터의 검색, 삽입, 삭제 속도를 높여 결국 프로그램의 성능을 향상시킬 수 있기 때문입니다.
자료구조는 크게 데이터가 배치되는 방식에 따라 선형과 비선형으로 나뉩니다.
선형 자료구조(Linear Data Structure)
데이터가 일정한 순서에 따라 연속적으로 배치되는 구조입니다.
특정 데이터의 앞과 뒤가 명확한 '선후 관계'가 있다는 것이 특징입니다.
배열: 같은 자료형의 데이터를 메모리의 연속된 공간에 저장합니다.
리스트: 데이터를 순차적으로 저장하며, 동적으로 크기를 조절할 수 있습니다. (ArrayList, LinkedList 등)
스택: 나중에 들어온 것이 먼저 나가는 후입선출(LIFO) 방식입니다.
큐: 먼저 들어온 것이 먼저 나가는 선입선출(FIFO) 방식입니다.
데크: 양쪽 끝에서 삽입과 삭제가 모두 가능합니다.
비선형 자료구조(Non-linear Data Structure):
데이터가 계층적이거나 복잡한 연결 관계를 가지며 비순차적으로 저장되는 구조입니다.
해시 테이블: 키-값 쌍으로 데이터를 저장하며, 해시 함수를 통해 매우 빠르게 검색합니다.
그래프: 정점(Vertex)과 간선(Edge)을 이용해 복잡한 객체 간의 관계를 표현합니다.
트리: 부모-자식 관계를 가지는 계층적 구조입니다.
힙: 최솟값이나 최댓값을 빠르게 찾기 위해 고안된 트리 기반 구조입니다.
💡 비유로 이해하기
선형 자료구조는 '한 줄로 서서 기다리는 편의점 계산대 줄'과 같고,
비선형 자료 구조는 '가족 관계도'나 '복잡하게 얽힌 지하철 노선도'와 같습니다.
💻 참고
프론트엔드 실무에서 마주하는 DOM(Documnet Object Model)은 대표적인 비선형 자료구조인 트리 형태입니다.
우리가
querySelector로 요소를 찾거나 컴포넌트 계층을 설계할 때 이미 비선형 자료구조의 원리를 활용하고 있는 셈입니다.
시간 복잡도는 단순히 코드가 돌아가는 시간을 측정하는 것이 아니라,
입력 데이터의 크기()가 커짐에 따라 연산 횟수가 어떻게 변하는지를 분석하는 것입니다.
성능 예측
효율적 선택
실무 필수 지표
신뢰성
실무에서 개발자는 "언제 어떤 입력이 오더라도 성능이 급격히 나빠지지 않도록" 보장해야 합니다.
가끔씩 느려지는 프로그램은 사용자가 신뢰할 수 없기 때문입니다.
그래서 우리는 최악의 시나리오에서도 안전한 성능을 보장하기 위해 빅-오(Big-O) 표기법을 사용합니다.
| 입력 크기() | |||||
|---|---|---|---|---|---|
| 10 | 1 | 3.3 | 10 | 33.2 | 100 |
| 100 | 1 | 6.6 | 100 | 664.4 | 10,000 |
| 1,000 | 1 | 9.9 | 1,000 | 9,966 | 1,000,000 |
(상수 시간)
(로그 시간)
(로그 선형 시간)
(이차 시간)
(지수 시간)
💡 비유로 이해하기
은 책장에서 내가 원하는 책의 위치를 이미 알고 있어 바로 꺼내는 것이고,
은 책의 첫 장부터 마지막 장까지 한 장씩 넘기며 원하는 내용을 찾는 것과 같습니다.데이터가 100만 권으로 늘어난다면 그 차이는 어마어마하겠죠?
"내 컴퓨터에서는 빠른데?"
은 무조건 빠르다?
우리가 Big-O 표기법에서 상수나 낮은 차수의 항을 무시하는 이유는
입력 이 압도적으로 커지는 상황을 가정하기 때문입니다.
동작 원리
수치로 보는 효율성
결과
실제 코드에서는 어떤 형태로 나타내는지 파이썬 예시로 확인해 보세요.
- 상수 시간: 배열의 인덱스로 즉시 접근하는 경우입니다.
def get_first_element(arr):
return arr[0] # 입력 크기와 상관없이 즉시 반환
- 선형 시간: 배열의 모든 요소를 하나씩 확인해야 하는 경우입니다.
def find_target(arr, target):
for item in arr: # n에 비례해 연산 증가
if item == target: return True
return False
- 이차 시간: 중첩 반복문을 사용하여 모든 쌍을 비교하는 경우입니다.
def print_all_pairs(arr):
for a in arr:
for b in arr: # n * n 연산
print(a, b)
이진 탐색은 언제나 쓸 수 있다?
은 정렬의 기준
공간 복잡도는 알고리즘이 실행될 때 필요한 메모리 사용량을 분석한 것입니다.
입력 데이터의 크기에 따라 얼마나 많은 추가 공간이 필요한지를 계산하며, 주로 빅-오(Big-O) 표기법을 사용합니다.
제한된 자원
치명적인 에러
(상수 공간): 추가적인 메모리 할당 없이 기존 데이터를 출력만 하는 경우입니다.
def print_items(arr):
for item in arr:
print(item)
(선형 공간): 입력 크기만큼의 새로운 배열을 생성하거나, 재귀 호출이 번 발생하여 스택 프레임이 쌓이는 경우입니다.
def double_items(arr):
result = []
for item in arr:
result.append(item * 2)
return result
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
우리가 알고리즘을 설계할 때 가장 고민하는 지점이 바로 이 트레이드오프입니다.
개념
관계
실무의 관점
💻 참고
프론트엔드 실무에서 자주 쓰는 메모이제이션이나 캐싱이 대표적인 '공간을 써서 시간을 사는' 행위입니다.
리액트의
useMemo도 이전 계산값을 메모리에 저장해두고 재사용함으로써 렌더링 속도를 높이는 트레이드오프의 한 예시라고 볼 수 있습니다.
자료구조의 분류
시간 복잡도와 Big-O
이진 탐색의 효율성
공간 복잡도와 트레이드오프