시간 복잡도 (Time Complexity)

시간복잡도를 알아야하는 이유

알고리즘 공부나 코딩테스트를 위해 백준 등과 같은 문제를 풀다보면 꼭 마주하는 것이 있다.

바로 시간이다.

실력이 늘다보면 어떠한 방법으로던 문제는 해결하지만,

입력받는 값이 커지면 시간이 기하급수적으로 증가해 제한시간 안에 정답을 도출하지 못한다.

이럴 때 쓰이는게 시간복잡도이다. 시간복잡도를 통해 걸리는 시간을 대략적으로 계산해보면, 문제에 맞는 어떤 알고리즘을 사용해야 할지 판단할 수 있다.

백준 단계별 문제에서도 시간복잡도에 대한 파트가 따로 있다.

백준 알고리즘 시간 복잡도 단계


시간 복잡도(Time Complexity)

  • Big-O(빅-오) ⇒ 최악의 경우 : 가장 오래 걸리는 시간
  • Big-Ω(빅-오메가) ⇒ 최선의 경우 : 최소한 걸리는 시간
  • Big-θ(빅-세타) ⇒ 평균의 경우 : 평균적으로 걸리는 시간

이해를 위한 예시 - 버블정렬

def bubbleSort(arr):
    n = len(arr)
    for i in range(n-1, -1, -1):
        for j in range(0, i):
            if arr[j] arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
    return arr

arr의 길이가 10만 이하인 테스트 케이스를 넣는다고 하자.

그럼 각각 몇번 for문의 반복이 이루어졌는지 계산해보자.

Big-O(빅-오)n=10만인 경우 : 10만 x 10만 =100억번 반복이 이루어진다.

Big-Ω(빅-오메가)n=1인 경우 : 1x1번 =1번 연산을 수행한다.

이렇듯 테스트 케이스의 값이 작다면 크게 오래걸리지 않겠지만,

테스트케이스의 값이 커질수록 엄청난 차이를 가져올 수가 있다.

하지만 가장 중요한건 바로 최악의 경우Big-O(빅-오) 이다.

항상 우리는 주어진 시간 내에 해결하기 위해서는 최악의 테스트 케이스를 생각하고 코드를 짜야한다.


Big-O 표기법에 따른 복잡도 그래프


상수는 무시한다 !

다음 예시는 시간복잡도가 O(n) 인 코드이다.

n = int(input())

print('1부터 n까지 2배씩 곱해주어 출력하기')
for i in range(1, n+1):
    i*=2
    print(i, end=' ')

그렇다면 다음 코드는 시간복잡도가 O(3n)일까?

n = int(input())

print('1부터 2n까지 3배씩 곱해주어 출력하기')
for i in range(1, 2*n+1):
    i*=3
    print(i, end=' ')
    
print('\n\n1부터 n까지 2배씩 곱해주어 출력하기')
for i in range(1, n+1):
    i*=2
    print(i, end=' ')

상수배를 한다는 것은 숫자가 작다면 큰 차이가 보여지겠지만,

나중에 어마어마하게 커진 입력값의 크기에 의한 영향에 비해 아주 작은 차이이다.

그러므로 시간 복잡도에서 우리는 상수는 무시한다.


O(1) - 상수 시간 (Constant time)

n = int(input())

print('입력받은 수를 3으로 나눈 나머지:', n%3)

n에 0이 들어가던 10억이 들어가던 똑같이 한번 연산한다.

n에 값에 관계 없이 같은 시간이 나오기 때문에 상수 시간인 O(1)이다.


O(logn) - 로그 시간 (Logarithmic time)

def binary_search(target, data):
    data.sort()
    start = 0
    end = len(data) - 1

    while start <= end:
        mid = (start + end) // 2

        if data[mid] == target:
            return mid # 함수를 끝내버린다.
        elif data[mid] < target:
            start = mid + 1
        else:
            end = mid -1
    return None

위의 코드는 이진 탐색 함수이다.

연산을 할때마다 탐색해야 할 자료의 갯수가 1/2로 줄어들기 때문에, O(logn)이다.

ex) 이진 탐색, 퀵 정렬, 병합 정렬, 힙 정렬


O(n) - 선형 시간 (Linear time)

n = int(input())

print('1부터 n까지 2배씩 곱해주어 출력하기')
for i in range(1, n+1):
    i*=2
    print(i, end=' ')

위의 상수를 무시하는 설명에서 사용했던 예시다.

for문을 통해 n번 연산을 수행하기 때문에 1차함수인 O(n)이다.

입력값 n에 비례해서 연산수가 증가하므로 O(n)이다.

ex) n에 대한 for문


O(n²) - 2차 시간 (Quadratic time)

n = int(input())
for i in range(1,n+1):
    for j in range(1,n+1):
        print(f'{i} * {j} = {i*j}')

n에 9를 넣으면 구구단을 출력하고,

100을 넣으면 백백단을 출력하는 알고리즘이다.

입력값 n의 제곱에 비례해서 연산수가 증가하므로 O(n²)이다.

ex) n에 대한 이중 for문, 삽입정렬, 버블정렬, 선택정렬 , 면적


O(nm) - 두가지 입력값인 경우

n = int(input())
m = int(input())
for i in range(1,n+1):
    for j in range(1,m+1):
        print(f'{i} * {j} = {i*j}')

n에 9, m에 9를 넣으면 구구단을 출력하고,

n에 100, m에 9를 넣으면 백구단을 출력하는 알고리즘이다.

입력값 n과 m의 곱에 비례해서 연산수가 증가하므로 O(nm)이다.

ex) n과 m에 대한 이중 for문


O(2ⁿ) - 지수 시간 (Exponential time)

def fibonacci(n):
  if n <= 1:
    return n
  return fibonacci(n-1) + fibonacci(n-2)

가장 좋은 예시인 피보나치수를 재귀함수로 사용할 때이다.

함수를 호출할 때마다 재귀로 함수를 2번씩 호출하기 때문에 2ⁿ에 비례해서 연산 수가 증가한다.

ex) 피보나치 수열


시간 복잡도를 통한 대략적 계산 방법

일반적으로 연산을 1억번 하는데 1초정도 걸린다.

문제를 푼다면 시간 복잡도를 통해 주어진 데이터의 범위에 따라

제한시간에 맞는 알고리즘을 사용해야한다.

데이터 수 시간복잡도 연산 횟수 소요 시간
10⁸ n, logn 1억 1초
10⁶ nlogn 1억 1초
10⁴ 1억 1초
500 1억 1초
20 n!, 2ⁿ 1억 1초

그 외의 Tip

  • Python3으로 시간초과가 났을 경우 PyPy3을 사용하면 통과할 수도 있다!

  • 파이썬은 느리지만 코드를 짜기가 쉽고, C계열의 언어는 코드 짜기가 번거롭지만 계산이 빠르다.

  • 컴퓨터 공학적 지식 없이 첫 알고리즘 공부라면 파이썬을 추천한다.