1. 프롤로그 - "이 코드 얼마나 빨라요?"
개발자라면 한 번쯤 들어봤을 질문입니다.
"이 기능 실행하는 데 몇 초 걸리나요?" / "사용자가 10만 명으로 늘어나도 버틸 수 있나요?"
만약 "3초 걸려요"라고 답했다면 틀린 대답입니다.
슈퍼컴퓨터에서는 0.001초, 구형 노트북에서는 10초가 걸릴 수 있으니까요. 하드웨어 스펙에 따라 달라지는 시간은 기준이 될 수 없습니다.
정답은 "데이터가 N배 늘어날 때, 계산 횟수는 얼마나 늘어나는가?" 로 답해야 합니다.
이 "변화의 비율(Rate of Growth)"을 수학적으로 약속한 기호가 바로 Big O 표기법입니다.
이것은 개발자들 사이의 공용어입니다. "이 알고리즘은 O(N)입니다"라고 하면, 전 세계 어느 개발자나 "아, 데이터 늘어나는 만큼 비례해서 느려지겠군" 하고 즉시 이해합니다.
2. 시간 복잡도와 공간 복잡도
Big O는 두 가지 측면에서 효율성을 측정합니다. 보통 시간 복잡도를 더 중요하게 여기지만, 메모리 제약이 심한 환경(임베디드, 모바일)에서는 공간 복잡도도 필수 체크 항목입니다.
2.1. 시간 복잡도 (Time Complexity)
-
"얼마나 오래 걸리는가?"
- CPU가 몇 번 연산(덧셈, 비교, 할당)을 수행하는지 셉니다.
- 절대적인 시간(초)이 아니라, 연산 횟수의 증가 추세(Trend)를 봅니다.
- 예: 데이터가 2배 늘었는데 시간은 4배 늘었다? -> 제곱 비례 ($O(N^2)$).
2.2. 공간 복잡도 (Space Complexity)
-
"메모리를 얼마나 많이 쓰는가?"
- 변수, 배열, 객체가 차지하는 메모리 양을 셉니다.
- 주의: 재귀 호출 스택(Call Stack)도 공간 복잡도에 포함됩니다. 많은 주니어 개발자가 이를 간과하여
StackOverflow를 만납니다. "변수 하나도 안 썼는데 왜 메모리 초과죠?" -> 재귀 함수가 스택을 10만 개 쌓았기 때문입니다.
3. Big O의 2가지 핵심 규칙 (Math Rules)
수학적으로 "점근 표기법(Asymptotic Notation)"이라고 부르지만, 복잡한 증명은 몰라도 됩니다. 규칙은 딱 2가지만 기억하면 됩니다.
규칙 1 - 상수는 과감히 버린다 (Drop Constants)
- 코드가
for 문을 두 번 따로 돌아서 실제 연산 횟수가 $2N + 5$번이 되더라도, 그냥 O(N)이라고 부릅니다.
- 왜냐하면 데이터($N$)가 무한대로 커지면(100억, 1조...), 2배 차이는 미미하기 때문입니다. 우리는 "기울기"가 궁금한 거지 y절편이 궁금한 게 아닙니다.
- $O(2N) \rightarrow O(N)$
- $O(500) \rightarrow O(1)$
- $O(N / 2) \rightarrow O(N)$
규칙 2 - 가장 큰 차수만 남긴다 (Drop Non-Dominant Terms)
- 함수가 $f(x) = 2N^2 + 100N + 5000$ 이라고 칩시다.
- N이 작을 때는 5000이 커 보이지만, N이 10만이 되면 $N^2$은 100억이 됩니다. 나머지는 티도 안 납니다.
- 그래서 최고차항인 $N^2$만 남깁니다.
- $O(N^2 + N) \rightarrow O(N^2)$
- $O(N \log N + N) \rightarrow O(N \log N)$
4. 대표적인 등급 (Rankings)
실행 속도가 빠른 순서대로 나열했습니다. (N=데이터 개수). 이 순서는 구구단처럼 외워야 합니다.
1위 - O(1) - 상수 시간 (Constant Time)
- 데이터가 1개든 100억 개든 실행 시간이 똑같습니다.
- 예: 배열 인덱스 접근(
arr[5]), 해시맵(map.get(key)).
- 이상적인 알고리즘입니다. 하지만 현실에서는 초기 세팅 비용이 클 수도 있습니다.
2위 - O(log N) - 로그 시간 (Logarithmic Time)
- 데이터가 2배로 늘어나면, 연산 횟수는 딱 1번 늘어납니다. (Up & Down 게임).
- 이진 탐색(Binary Search)이 대표적입니다.
- N=100만이어도 단 20번만 확인하면 됩니다 ($2^ \approx 1,000,000$).
- N=40억(전 세계 인구 절반)이어도 32번이면 찾습니다. 엄청나게 효율적입니다.
3위 - O(N) - 선형 시간 (Linear Time)
- 데이터가 10배 늘어나면, 시간도 정직하게 10배 늘어납니다.
for 반복문으로 데이터를 처음부터 끝까지 훑는 경우입니다.
- 가장 일반적인 경우입니다. "데이터를 한번은 봐야지" 하는 로직들입니다.
4위 - O(N log N) - 선형 로그 시간
- O(N)보다 조금 느리지만, O(N^2)보다는 훨씬 빠릅니다.
- 효율적인 정렬 알고리즘(병합 정렬, 퀵 정렬, 힙 정렬)의 한계선입니다.
- 데이터를 정렬해야 한다면 최소 이 정도 시간은 투자해야 합니다.
5위 - O(N^2) - 이차 시간 (Quadratic Time)
- 데이터가 10배 늘어나면, 시간은 100배 늘어납니다.
- 이중 for 문 (구구단)이 범인입니다.
- N=10,000만 넘어가도 1억 번 연산해야 하므로 서버가 멈출 위험이 있습니다.
- 알고리즘 문제에서 N의 범위가 10만 이상이라면 절대 O(N^2) 알고리즘을 쓰면 안 됩니다.
꼴등 - O(2^N) - 지수 시간 (Exponential Time)
- 데이터가 1개 늘어날 때마다 시간이 2배로 폭증합니다.
- 재귀 피보나치 수열이 여기 해당합니다. N=40만 돼도 계산 못 합니다.
- 암호 해독 등 특수한 경우에만 허용됩니다. 일반적인 서버 로직에 이게 들어가면 장애의 주범이 됩니다.
5. 상환 분석 (Amortized Analysis) 더 알아보기
"가끔 느려지는 건 괜찮다."
동적 배열(Dynamic Array, Python list, Java ArrayList)을 봅시다.
- 평소에는
append()가 O(1)입니다. (빈칸에 넣기만 하면 됨).
- 하지만 배열이 꽉 차면? 2배 크기의 새 배열을 만들고, 기존 데이터를 모조리 이사(Copy) 가야 합니다. 이때는 O(N)이 걸립니다.
그럼 append()의 시간 복잡도는 O(N)일까요? 아닙니다.
이사가 아주 가끔 일어나기 때문에, 전체 비용을 1/N로 나누면(상환하면) 결과적으로 O(1)이 됩니다.
이를 Amortized Time이라고 합니다.
마치 전세 보증금(목돈)을 내지만, 월세(평균 비용)는 0원인 것과 비슷합니다.
6. 재귀 함수의 공간 복잡도 함정
# O(N) 시간 복잡도
# O(N) 공간 복잡도!! (재귀 스택 때문)
def factorial(n):
if n <= 1: return 1
return n * factorial(n-1)
위 코드는 변수를 하나도 안 쓰는 것 같지만, 실제로는 factorial(5) -> factorial(4) -> ... 함수가 호출될 때마다 메모리에 스택 프레임(Stack Frame)이 쌓입니다.
그래서 공간 복잡도는 O(N)입니다.
이를 간과하면 StackOverflowError가 발생합니다.
반면 반복문(Iteration)을 쓰면 공간 복잡도는 O(1)입니다. 메모리가 쪼들리는 환경이라면 재귀보다는 반복문을 써야 하는 이유입니다.
7. 최선, 평균, 최악 (Best, Average, Worst)
우리는 주로 최악의 경우(Worst Case)를 기준으로 이야기합니다.
왜냐하면 엔지니어로서 "아무리 느려도 이 정도는 보장합니다"라고 말해야 하기 때문입니다. SLA(Service Level Agreement)를 지키기 위해서죠.
- Big Omega ($\Omega$): 최선의 경우 (하한선). "운 좋으면 1초". 의미 없음.
- Big Theta ($\Theta$): 평균적인 경우. "보통 5초".
- Big O ($O$): 최악의 경우 (상한선). "죽어도 10초 안에는 끝남". 중요.
퀵 정렬(Quick Sort)은 평균적으로 $O(N \log N)$이지만, 최악의 경우(이미 정렬된 데이터) $O(N^2)$이 됩니다.
하지만 현실 데이터에서는 평균 성능이 워낙 좋아서 퀵 정렬을 주로 씁니다. 반면, NASA 같이 0.1%의 오차도 허용하지 않는 곳에서는 최악의 경우에도 $O(N \log N)$을 보장하는 병합 정렬(Merge Sort)을 선호하기도 합니다.
8. 마스터 정리 (Master Theorem) 깊이 들여다보기
재귀 함수의 시간 복잡도를 계산하는 공식입니다.
$T(n) = a T(n/b) + f(n)$ 형태의 점화식은 마스터 정리를 통해 쉽게 계산할 수 있습니다.
- $a$: 하위 문제의 개수 (분기 수)
- $b$: 하위 문제의 크기가 줄어드는 비율
- $f(n)$: 결과를 합치는 데 드는 비용
예시:
-
이진 탐색 (Binary Search)
- $T(n) = T(n/2) + O(1)$
- 반으로 쪼개고($b=2$), 그중 하나만 선택($a=1$). 비교 비용은 상수($O(1)$).
- 결과: $O(\log n)$
-
병합 정렬 (Merge Sort)
- $T(n) = 2T(n/2) + O(n)$
- 반으로 쪼개고($b=2$), 둘 다 처리($a=2$). 합치는 비용은 선형($O(n)$).
- 결과: $O(n \log n)$
복잡한 점화식 풀이 없이 "입력이 어떻게 줄어들고, 몇 번 호출되는가"만 알면 복잡도를 짐작할 수 있습니다.
9. 교양 - P vs NP (밀레니엄 난제)
컴퓨터 과학의 가장 큰 미해결 문제인 P vs NP도 Big O와 관련이 있습니다.
- P (Polynomial): 다항 시간($O(N^k)$) 안에 풀 수 있는 문제. (예: 정렬, 최단 경로). "컴퓨터가 현실적으로 해결 가능한 영역"입니다.
- NP (Nondeterministic Polynomial): 답을 주면 그것이 정답인지 검산(Verify)하는 건 다항 시간 안에 되지만, 정답을 찾는 건 다항 시간 안에 되는지 모르는 문제. (예: 스도쿠, 암호 해독).
- NP-Complete: NP 중에서도 가장 어려운 문제들. (예: 외판원 순회 문제 TSP).
"P = NP 인가?"
이 질문은 "검산이 쉬운 모든 문제는 풀기도 쉬운가?"와 같습니다.
대부분의 학자들은 P != NP라고 믿습니다. 암호화 기술(RSA)이 "소인수분해는 어렵다(NP)"는 전제 위에 서 있기 때문입니다. 만약 P=NP임이 증명되면, 현재의 모든 암호 체계는 뚫릴 수 있습니다.
8. 예제
8.1. DB 인덱싱
- 인덱스 없음: 전체 스캔, $O(N)$.
- B-Tree 인덱스: 트리 탐색, $O(\log N)$.
100만 건 데이터에서 이 차이는 수백 배의 성능 차이를 만듭니다. 인덱스 하나가 쿼리 속도를 10초에서 0.01초로 만듭니다.
8.2. 문자열 연결
자바나 파이썬에서 문자열은 불변(Immutable)입니다.
s += "a"를 반복하면 매번 새로운 문자열을 복사해야 하므로 $O(N^2)$이 됩니다.
StringBuilder나 join()을 쓰면 $O(N)$으로 줄어듭니다. 사소해 보이지만, 대용량 로그 처리할 때 서버가 뻗는 주 원인 중 하나입니다.
8.3. 리스트에서 요소 찾기
- Linked List에서 n번째 요소 찾기: O(N). (처음부터 따라가야 함).
- Array에서 n번째 요소 찾기: O(1). (주소 계산).
그래서 데이터 조회가 잦으면 Array를, 삽입/삭제가 잦으면 Linked List를 쓰는 게 기본입니다.
9. 핵심 정리 - 실전 체크리스트
Big O는 코드 리뷰와 설계 논의에서 단골 주제입니다. 이 정도는 설명할 수 있어야 합니다.
Q1: 해시 테이블(HashMap)의 시간 복잡도는 항상 O(1)인가요?
A: 평균적으로는 O(1)입니다. 하지만 해시 충돌(Collision)이 많이 발생하면 최악의 경우 O(N)까지 느려질 수 있습니다. Java 8 이후부터는 충돌이 많아지면 내부적으로 LinkedList 대신 Red-Black Tree로 변환하여 O(log N)을 보장합니다.
Q2: 퀵 소트(Quick Sort)와 머지 소트(Merge Sort) 중 뭘 써야 하나요?
A: 일반적인 상황(메모리 상의 데이터)에서는 퀵 소트가 더 빠릅니다. 추가 메모리를 덜 쓰고, 캐시 지역성(Locality)이 좋기 때문입니다. 하지만 퀵 소트는 최악의 경우 O(N^2)이고 불안정 정렬(Unstable Sort)입니다. 반면 머지 소트는 항상 O(N log N)과 안정성을 보장하므로, 최악의 케이스 방어가 중요한 시스템이나 Linked List 정렬에 유리합니다.
Q3: 공간 복잡도 O(1)이라는 게 변수를 하나만 쓴다는 뜻인가요?
A: 아닙니다. 입력 데이터 크기 N과 상관없이 일정한 양의 메모리만 쓴다는 뜻입니다. 변수를 100개 써도, N이 늘어났을 때 변수 개수가 그대로라면 O(1)입니다. 반대로 재귀 함수를 쓰면 변수를 따로 안 선언해도 스택 메모리를 쓰므로 O(N)이 될 수 있습니다.
Q4: O(log N)은 얼마나 빠른 건가요?
A: 엄청나게 빠릅니다. 우주에 있는 모든 원자 수($10^$)만큼 데이터가 있어도, 이진 탐색으로는 약 270번만 비교하면 찾을 수 있습니다. 사실상 O(1) 다음으로 가장 빠른 시간 복잡도라고 보면 됩니다.
10. 마치며 - 당신의 코드는 확장 가능한가요?
Big O는 단순한 수학 기호가 아니라, 확장성(Scalability)에 대한 철학입니다.
지금 당장은 데이터가 10개뿐이라 $O(N^2)$ 코드를 짜도 아무 문제 없어 보입니다.
하지만 회사가 성장해서 데이터가 100만 개가 되는 순간, 그 코드는 시한폭탄이 되어 터집니다.
처음부터 완벽한 O(1)을 짤 필요는 없습니다. 하지만 적어도 내가 짠 코드가 O(N)인지 O(N^2)인지는 의식하고 있어야 합니다. 그것이 "코더"와 "엔지니어"의 차이입니다.
Big O Notation: The Language of Algorithm Efficiency (Definitive Guide)
1. Prologue: "Running Time" is Relative
Imagine you wrote a script to process user data.
"It takes 3 seconds," you tell your boss.
But does it take 3 seconds for 10 users? Or 10 million users?
Does it take 3 seconds on your Macbook Pro M3? Or on a cheap AWS t2.micro instance?
"Seconds" are not a reliable metric for code efficiency.
Hardware changes. Data size changes. The load on the server changes.
We need a universal language to describe how our code performs relative to the input size, stripping away the hardware differences.
That language is Big O Notation.
It answers the question: "If I double the input, how much slower does the code get?"
2. Time vs Space Complexity
Efficiency isn't just about speed. It's about resources.
2.1. Time Complexity
- Meaning: How many elementary operations (add, compare, assign) does the CPU perform?
- Focus: We care about the growth rate. If input $N$ becomes $2N$, does time become $2T$ or $4T$?
- Context: Time is money. Slow algorithms assume higher server costs and poor user experience.
2.2. Space Complexity
- Meaning: How much RAM (Memory) does the algorithm need?
- Components:
- Fixed Part: Constants, simple variables.
- Variable Part: Arrays, Objects dependent on N.
- Hidden Part: Recursion Stack. Every recursive call adds a frame to the memory stack. A depth of N means $O(N)$ space.
Trade-off: Often, we can make code faster by using more memory (Caching/Memoization). This is the Time-Space Trade-off. A Hash Map is a classic example: it uses O(N) space to achieve O(1) search time.
3. The Math Rules (Simplified)
You don't need a PhD in math. Just follow these 2 rules.
Rule 1: Drop Constants
- $O(2N)$ becomes O(N).
- $O(500)$ becomes O(1).
- Why? Because Big O describes the curve shape at infinity.
- Whether a line is steep ($2N$) or flat ($N$), it's still a linear line.
- Whether a curve is $2N^2$ or $N^2$, it's still a parabola.
- Caveat: In the real world, constants matter for small inputs. $100N$ is clearly slower than $N$. But theoretically, they are the same class.
Rule 2: Drop Non-Dominant Terms
- $O(N^2 + N + 1000)$ becomes O(N^2).
-
Why?
- If $N = 1000$:
- $N^2 = 1,000,000$ (Million)
- $N = 1,000$
- The $N^2$ term accounts for 99.9% of the cost. The rest is noise.
4. The Rankings (Visualized)
From the Usain Bolt of algorithms to the Sloth.
1. O(1) Constant (The Dream)
- Description: Speed is independent of input size.
- Example: Accessing an array by index
arr[5]. Pushing to a Stack. Hash Map lookup (average).
- Analogy: Opening a book to page 50. It takes the same time regardless of whether the book has 100 pages or 1000 pages.
2. O(log N) Logarithmic (Excellent)
- Description: Time grows very slowly. Doubling input adds only 1 step.
- Example: Binary Search, Balanced Search Trees (AVL, Red-Black).
- Analogy: Looking up a word in a dictionary. You split the pages in half, then half again.
- Power: Even if N is 4 Billion ($2^$), you only need 32 steps.
3. O(N) Linear (Good)
- Description: Time grows proportionally to input.
- Example: Simple
for loop. Linear Search. Copying an array.
- Analogy: Reading a book page by page. 2x pages = 2x time.
4. O(N log N) Linearithmic (Fair)
- Description: A bit slower than linear, but acceptable.
- Example: Efficient Sorting (Merge Sort, Quick Sort, Heap Sort).
- Significance: This is mathematically the fastest possible time complexity for comparison-based sorting.
5. O(N^2) Quadratic (Bad)
- Description: Performance degrades effectively.
- Example: Nested loops. Bubble Sort, Selection Sort.
- Analogy: Reviewing a class where everyone shakes hands with everyone else.
- Warning: If $N > 10,000$, do not use $O(N^2)$. It will freeze your server.
6. O(2^N) Exponential (Terrible)
- Description: Performance explodes. The "curve of death".
- Example: Recursive Fibonacci without memoization. Brute-force Password Cracking. Traveling Salesman Problem (exact).
- Analogy: Trying to guess a password by trying every combination.
5. Deep Dive: Amortized Analysis
Sometimes, an operation is slow once in a while but fast most of the time. How do we classify it?
Case Study: Dynamic Arrays (ArrayList / Python List)
- When you
append() an item, it usually just puts it in the next empty slot: O(1).
- But if the array is full? It allocates a new array (double size), checks capacity, and copies all N items: O(N).
- So is
append() O(1) or O(N)?
Mathematicians use Amortized Analysis.
- You "pay" a high cost once (resizing).
- This cost is "spread out" (amortized) over the many cheap operations that follow.
- Result: On average,
append() is still considered O(1).
6. Advanced: P vs NP
Big O leads to the biggest unsolved problem in Computer Science: P vs NP.
- P (Polynomial): Problems solvable in "reasonable" time ($O(N^k)$). Examples: Sorting, Shortest Path.
- NP (Nondeterministic Polynomial): Problems where, if someone gives you a solution, you can verify it quickly, but finding the solution might take forever ($O(2^N)$). Examples: Sudoku, Tetris optimization, Prime Factorization.
The Million Dollar Question: Does P = NP?
- Can every problem whose solution is easy to verify also be solved easily?
- Most scientists think No (P != NP), meaning there are fundamentally hard problems that computers can never solve efficiently. This assumption is the basis of modern Cryptography (RSA).
7. Best, Average, and Worst Case
In engineering, we usually prioritize the Worst Case ($O$).
Why? To prevent Denial of Service (DoS) and guarantee SLAs.
- Best Case ($\Omega$, Big Omega): The "Lucky" scenario.
- Searching an array and finding the target at index 0. O(1).
- Useless for capacity planning.
- Average Case ($\Theta$, Big Theta): The "Statistically Probable" scenario.
- Quick Sort is $O(N \log N)$ on average.
- Worst Case ($O$, Big O): The "Disaster" scenario.
- Quick Sort is $O(N^2)$ if the array is already sorted (and pivot is bad).
- We often choose Merge Sort over Quick Sort for critical systems because Merge Sort guarantees $O(N \log N)$ even in the worst case.
8. Master Theorem (Simplified)
How do we calculate the complexity of crazy recursive functions?
We use the Master Theorem.
For a recursion of form $T(n) = a T(n/b) + f(n)$:
- $a$: Number of sub-problems (branches).
- $b$: How much input shrinks (division factor).
- $f(n)$: Cost of combining results.
examples:
- Binary Search: $T(n) = T(n/2) + O(1)$. Here $a=1, b=2$. Complexity: $O(\log n)$.
- Merge Sort: $T(n) = 2T(n/2) + O(n)$. Here $a=2, b=2$. Complexity: $O(n \log n)$.
9. Real World Examples
9.1. Database Indexing (SQL)
- Problem: Find a user by email in a table of 100 million rows.
- Naive Approach: Full Table Scan. Read every row. O(N).
- Optimized: Create a B-Tree Index. Search the tree. O(log N).
- Trade-off: Index takes up disk space (Space Complexity) and slows down writes (Insert Complexity).
9.2. String Concatenation
Most languages (Java, Python) have Immutable Strings.
s = ""
for i in range(N):
s += "a"
- Step 1: Create "a". Copy 1 char.
- Step 2: Create "aa". Copy 2 chars.
- ...
- Step N: Copy N chars.
- Total: $1 + 2 + ... + N = N(N+1)/2 \approx O(N^2)$.
Solution: Use a Mutable Buffer (StringBuilder in Java, [].join in Python).
- Total: O(N).
This simple change can make a script run 1000x faster.
10. FAQ: Common Questions
Q1: Is O(1) always faster than O(N)?
A: Not for small N.
- Algorithm A: $O(1)$ but takes 5 seconds overhead (constant).
- Algorithm B: $O(N)$ but takes 1 millisecond per item.
If N < 5000, Algorithm B is faster.
Big O cares about the Limit as N approaches Infinity. It ignores the "overhead".
Q2: What is the complexity of Hash Maps (Dictionaries)?
A:
- Time: Average $O(1)$. Worst Case $O(N)$ (Hash Collisions, all keys in same bucket).
- Space: $O(N)$. You trade RAM for speed.
Q3: Can we sort faster than O(N log N)?
A: Yes and No.
- Comparison Sorts (Merge, Quick, Heap) cannot be faster than $O(N \log N)$. It's mathematically proven.
- Non-comparison Sorts (Counting Sort, Radix Sort) can be O(N).
- Constraint: You must treat data as numbers (binary), not just comparable items. And the range of numbers must be limited.
Q4: Why is nested loop O(N^2)?
A: Because for each of the N items in outer loop, you run the inner loop N times.
$N \times N = N^2$.
If inner loop runs $M$ times, it's $O(N \times M)$.