나뭇잎 교과서

빅오 표기법과 복잡도

자료가 1,000개일 때 0.01초 걸린 프로그램이 100만 개에서는 몇 초 걸릴까요? 답은 알고리즘에 따라 두 배 남짓에서 백만 배 이상까지 달라집니다. 연산 횟수가 늘어나는 빠르기를 한 글자로 요약하는 방법이 빅오 표기법입니다.

본문

빅오 표기법과 복잡도

본문

자료 1000개를 처리하는 데 0.01초 걸리는 프로그램이 있다. 자료가 100만 개로 1000배 늘면 몇 초가 걸릴까? 10초라고 답하기 쉽다. 그런데 어떤 프로그램은 0.02초로 거의 그대로이고, 어떤 프로그램은 3시간 가까이 걸리며, 어떤 프로그램은 끝나지 않는다. 같은 자료에 같은 컴퓨터인데 왜 이렇게 다를까? 연산 횟수가 늘어나는 빠르기를 한 글자로 요약하는 방법이 있다.

그림 1. 1초에 1억 번 연산하는 컴퓨터에서 복잡도별 실행 시간(막대는 로그 길이). n이 커질수록 복잡도의 차이가 시간의 차이를 압도한다.
먼저 알아야 할 것분할 정복(분할 정복), 동적 계획법(동적 계획법), 지수와 로그(수학).

3.6.1빅오 표기법

알고리즘의 실행 시간은 컴퓨터의 빠르기, 언어, 코드의 세부에 따라 달라진다. 그래서 알고리즘을 견줄 때는 시간 자체보다 입력의 크기 \(n\)이 커질 때 연산 횟수가 얼마나 빠르게 늘어나는지를 본다. 연산 횟수가 \(3n^2 + 5n + 7\)이라면 \(n\)이 클 때 \(n^2\) 항이 나머지를 압도하고, 앞의 상수 3은 컴퓨터를 바꾸면 달라지는 값이다. 그래서 가장 빨리 자라는 항만 남기고 상수를 버려 \(O(n^2)\)으로 쓴다. 이것을 빅오 표기법이라 하며, 알고리즘의 시간 복잡도를 나타낸다.

정리 3.6.1 주요 복잡도

빠른 것부터 \(O(1) < O(\log n) < O(n) < O(n\log n) < O(n^2) < O(2^n) < O(n!)\)이다. 입력이 \(k\)배가 될 때 \(O(n)\)은 \(k\)배, \(O(n^2)\)은 \(k^2\)배가 되고, \(O(2^n)\)은 \(n\)이 1 늘 때마다 두 배가 된다. 반복문이 겹칠 때마다 차수가 하나씩 오르고, 반씩 줄이는 반복은 \(\log n\)을 만든다.

그림 1에서 \(n\)을 바꾸어 보자. \(n = 100\)만이면 \(O(\log n)\)은 20번이라 눈 깜짝할 사이이고, \(O(n)\)은 0.01초, \(O(n\log n)\)은 0.2초이다. 그런데 \(O(n^2)\)은 \(10^{12}\)번으로 약 2.8시간이 걸리고, \(O(2^n)\)은 \(n = 100\)만 되어도 우주의 나이보다 길다. 처음 질문의 프로그램이 1000개에서 0.01초였다면, \(O(n)\)은 10초, \(O(n\log n)\)은 20초, \(O(n^2)\)은 약 2.8시간이 된다. 그래서 큰 자료를 다룰 때는 컴퓨터를 바꾸는 것보다 복잡도가 낮은 알고리즘을 고르는 것이 훨씬 효과가 크다.1

3.6.2코드에서 복잡도 읽기

코드를 보고 복잡도를 어림할 수 있다. 리스트를 한 번 훑는 반복문은 \(O(n)\)이고, 반복문 안에 같은 크기의 반복문이 또 있으면 \(O(n^2)\)이다. 범위를 반씩 줄이며 반복하면 \(O(\log n)\)이다. 재귀는 호출이 만드는 나무의 크기를 세어야 하는데, 피보나치처럼 호출마다 두 갈래로 갈라지면 \(O(2^n)\)에 가깝다. 그리고 반복문 안에서 쓰는 함수의 비용도 함께 세야 한다. 리스트의 pop(0)이나 in 검사는 원소 수만큼 걸리므로, 반복문 안에서 쓰면 \(O(n)\)이 \(O(n^2)\)이 된다. 메모리를 얼마나 쓰는지도 같은 방법으로 공간 복잡도로 나타낸다.

코드의 모양복잡도예 범위를 반씩 줄이는 반복O(log n)이진 탐색 한 번 훑는 반복O(n)최댓값 찾기 반복 안의 반복O(n²)선택 정렬
그림 2. 반복문이 겹치는 모양과 범위를 줄이는 방식으로 복잡도를 어림한다.

3.6.3예제 풀이

예제 3.6.2

리스트 안에 같은 수가 두 번 나오는지 검사하는 두 방법이 있다. (가) 모든 두 원소의 쌍을 견준다. (나) 리스트를 정렬한 뒤 이웃한 원소끼리만 견준다. 각 방법의 복잡도를 구하고, 원소가 1000개에서 0.005초 걸렸다면 원소 100만 개에서는 각각 얼마나 걸릴지 어림하여라.

(가)는 \(\dfrac{n(n-1)}{2}\)쌍을 견주므로 \(O(n^2)\)이다. 원소가 1000배이면 시간은 \(1000^2 = 100\)만 배인 약 5000초, 곧 1시간 20분 남짓이다.

(나)는 정렬에 \(O(n\log n)\), 이웃끼리 견주기에 \(O(n)\)이므로 전체가 \(O(n\log n)\)이다. 원소가 1000배이면 \(n\log n\)은 \(1000 \times \dfrac{\log 10^6}{\log 10^3} = 1000 \times 2 = 2000\)배이므로 약 10초이다. 같은 일을 하는 두 방법이 500배 차이 난다. 즉 빅오는 프로그램이 큰 자료에서 어떻게 될지 미리 알려 주는 도구이다. 그래서 코드를 짜기 전에 복잡도부터 어림해 보고, 감당할 수 없는 방법은 처음부터 고르지 않는다.

아래 탐구 카드에서 여러 알고리즘의 연산 횟수를 세어 빅오로 나타내고, 입력 크기를 바꾸며 실행 시간을 견주어 보자.

  1. 빅오 표기법은 1890년대 독일의 수학자 바흐만과 란다우가 수론에서 함수의 증가 속도를 나타내려고 쓰기 시작했고, 1970년대 크누스가 알고리즘 분석에 널리 퍼뜨렸다.
마무리

빅오 표기법은 입력의 크기가 커질 때 연산 횟수가 늘어나는 빠르기를 가장 빨리 자라는 항으로 요약하며, \(O(\log n)\), \(O(n)\), \(O(n\log n)\), \(O(n^2)\), \(O(2^n)\)으로 갈수록 크게 느려진다. 반복문이 겹치는 모양과 범위를 줄이는 방식으로 코드에서 복잡도를 어림할 수 있다. 그래서 처음 질문의 프로그램은 자료가 1000배 늘 때 \(O(\log n)\)이면 거의 그대로, \(O(n)\)이면 1000배, \(O(n^2)\)이면 100만 배 느려진다.

탐구 1

입력이 두 배가 되면 연산은 몇 배가 될까?

복잡도 · 연산 횟수 세기와 빅오 표기법

알고리즘

입력 자료

예상 f(n), 비율 연산 수/f(n)이 일정해지는 것을 고르세요


              

브라우저에서 같은 알고리즘을 실제로 실행해 강조한 줄(비교 또는 검사)이 실행된 횟수를 센다. 탐색은 찾는 값이 없는 경우(최악)이다. 시간은 이 컴퓨터에서 잰 값이라 매번, 기기마다 다르다.

  1. 질문정렬 프로그램이 자료 1,000개를 0.01초에 처리했다. 자료가 100만 개가 되면 몇 초 걸릴까? 1,000배라고 답하기 쉽지만, 알고리즘에 따라 2,000배쯤일 수도(O(n log n)), 100만 배일 수도(O(n²)) 있다.
  2. 복원알고리즘의 효율은 실행 시간 대신 기본 연산(비교, 덧셈, 배열 접근 등)의 횟수로 센다. 시간은 컴퓨터와 언어에 따라 달라지지만 연산 횟수는 알고리즘만으로 정해지기 때문이다.
  3. 설명입력 크기 n에 대한 연산 횟수 T(n)이 충분히 큰 n에서 언제나 c·f(n)보다 작거나 같도록 하는 상수 c가 있으면 T(n) = O(f(n))이라 쓴다(빅오 표기법). 그래서 3n² + 5n + 7은 O(n²)이다. 가장 빨리 자라는 항만 남기고 상수 계수는 버린다. 실제로는 T(n)/f(n)이 n이 커져도 일정한 값 근처에 머무는 f(n)을 찾으면 된다.
  4. 연결n이 두 배가 될 때 연산 수가 거의 그대로면 O(log n), 2배면 O(n), 2배보다 조금 크면 O(n log n), 4배면 O(n²)이다. O(2ⁿ)은 n이 1만 늘어도 2배가 된다. 이 ‘두 배 실험’은 코드를 몰라도 측정으로 복잡도를 추정하는 방법이며, 같은 문제를 푸는 두 알고리즘(버블 정렬과 병합 정렬, 선형 탐색과 이진 탐색)을 비교하는 기준이 된다.
  5. 적용버블 정렬에서 예상 f(n)을 n, n log₂ n, n²로 바꿔 가며 ‘연산/f(n)’ 열이 일정해지는 것을 찾자. 그다음 입력을 ‘이미 정렬됨’으로 바꾸면 어떻게 되는가? 병합 정렬과 이진 탐색의 f(n)도 찾고, 측정 시간 열이 연산 수와 같은 비율로 늘어나는지 확인해 보자.
  6. 판단빅오는 n이 클 때의 증가율만 말해 준다. 상수 계수와 작은 항을 버리므로, n이 작을 때는 O(n²) 알고리즘이 O(n log n)보다 빠를 수도 있다. 같은 알고리즘도 입력에 따라 연산 수가 달라지므로(최선·평균·최악), 보통은 최악의 경우로 표기한다. 측정 시간은 캐시, 다른 프로그램, 타이머 해상도 때문에 흔들리므로 작은 n의 시간으로 복잡도를 판단하면 안 된다.

확인조건이 달라지면

어떤 프로그램의 실행 시간을 재었더니 n = 1,000에서 0.2초, n = 2,000에서 0.8초, n = 4,000에서 3.2초였다. n = 16,000이면 약 몇 초가 걸릴까?

탐구 2

빠른 컴퓨터를 사면 느린 알고리즘을 따라잡을 수 있을까?

복잡도 · 증가율 비교와 상수 계수
A와 B가 같아지는 n
—
n = 100에서 빠른 쪽
—
n = 10⁶에서 B / A
—

연산 하나에 걸리는 시간을 똑같다고 본 단순 계산이다. 실제 프로그램의 속도는 언어, 메모리 접근, 입력 자료에 따라 달라진다.

  1. 질문O(n²) 정렬을 쓰는 프로그램이 느려서, 1,000배 빠른 컴퓨터를 샀다. 이제 O(n log n) 정렬을 쓰는 오래된 컴퓨터보다 빠를까? 2ⁿ번 연산하는 완전 탐색은 컴퓨터가 빨라지면 얼마나 더 큰 n을 풀 수 있을까?
  2. 복원빅오 표기법은 n이 커질 때 연산 횟수가 늘어나는 빠르기(증가율)를 나타내며, 상수 계수와 작은 항은 버린다. 빠른 순서는 O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ)이다.
  3. 설명그래프는 두 축이 모두 로그 눈금이라 거듭제곱 함수 nᵏ는 기울기가 k인 직선이 된다. n² 직선은 n 직선보다 두 배 가파르고, 2ⁿ은 곧바로 위로 꺾여 올라간다. 1초에 10억 번 연산하는 컴퓨터로도 n = 100에서 2ⁿ번 연산하려면 우주의 나이보다 훨씬 긴 시간이 걸린다.
  4. 연결컴퓨터가 1,000배 빨라지면 1초 안에 풀 수 있는 n은 O(n)이면 1,000배, O(n²)이면 약 32배(√1000), O(2ⁿ)이면 고작 10 정도(2¹⁰ ≈ 1000) 늘어난다. 그래서 큰 문제에서는 하드웨어보다 알고리즘을 바꾸는 효과가 훨씬 크다. 반면 n이 작으면 상수 계수가 작은 O(n²) 알고리즘이 더 빠를 수 있어, 실제 정렬 라이브러리 중에는 작은 구간을 삽입 정렬로 처리하는 것도 있다.
  5. 적용‘증가율 비교’에서 n을 10⁶까지 올리고 표의 시간을 읽자. 속도를 1,000배(10⁹ → 10¹²)로 올려도 1초를 넘는 것은 무엇인가? ‘상수 계수의 영향’에서 A의 계수를 크게 해도 아주 큰 n에서 B가 결국 느려지는지, 두 알고리즘이 같아지는 n이 어떻게 움직이는지 확인해 보자.
  6. 판단“O(n log n)이 O(n²)보다 언제나 빠르다”는 틀린 말이다. 빅오는 충분히 큰 n에서의 비교이고, 어디서부터 큰 n인지는 상수 계수가 정한다. 또 연산 횟수가 같아도 메모리 사용량, 구현의 단순함, 입력의 특징이 알고리즘 선택에 영향을 준다.

확인조건이 달라지면

2ⁿ번 연산하는 완전 탐색 프로그램이 지금 컴퓨터에서 1시간 동안 n = 40까지 풀 수 있다. 1,000,000배 빠른 컴퓨터가 생기면 1시간 동안 약 얼마까지 풀 수 있을까?

AI 출제 문항연습문제 풀어보기13문항
Esc

↑↓ 고르기Enter 열기