[Algorithm] 복잡도

작성:    

업데이트:

카테고리:

태그: ,

복잡도

  • 알고리즘의 성능을 나타내는 척도
  • 시간복잡도와 공간 복잡도로 구분한다.


1. 시간 복잡도

  • 특정한 크기의 입력에 대하여 알고리즘이 얼마나 오래 걸리는지를 의미
  • 알고리즘을 위해 필요한 연산의 횟수

  • 알고리즘 문제를 풀 때 단순히 ‘복잡도’라고 하면 시간 복잡도 의미
  • 프로그램을 효율적으로 작성해야 프로그램이 시간 제한 내에 동작할 수 있다.


1.1. 표기법 ‘Big-O’

  • 가장 빠르게 증가하는 항만을 고려하는 표기법
  • 즉, 함수의 상한만을 나타낸다.

  • 시간 복잡도 분석은 문제 풀이의 핵심
  • 문제의 조건을 먼저 확인하여 문제를 풀기 위해 얼마나 효율적인 알고리즘을 작성해야 하는지 먼저 파악할 것


1.2. Big-O 활용예제 1

array = [3,5, 1, 2, 4]  # 5개의 데이터(N=5)
summary = 0             # 합계를 저장할 변수

# 모든 데이터를 하나씩 확인하면서 합계를 계산
for x in array:
    summary += x

# 결과를 출력
print(summary)

실행결과는 다음과 같다.

15

위의 예제는 5개의 데이터를 받아 차례로 5회 더해준다.(N=5) 즉, 연산횟수는 N에 비례한다.

본 소스코드에서 가장 영향력이 큰 부분은 N에 비례하는 연산을 수행하는 반복문 부분이므로 시간 복잡도를 O(N)이라고 표기한다.


1.3. Big-O 활용예제 2

다음 예제는 어떤 시간 복잡도를 가질까.

a = 5
b = 7
print(a+b)

대입 연산과 출력 함수를 무시하면, 이 소스코드의 연산 횟수는 1이다. (더하기 연산 한 번) 이는 상수 연산으로, 시간 복잡도는 ‘O(1)’로 표현할 수 있다.


1.4. Big-O 활용예제 3

  • 다음 예제는 어떤 시간 복잡도를 가질까.
array = [3, 5, 1, 2, 4]  # 5개의 데이터(N=5)

for i in array:
    for j in array:
        temp = i * j
        print(temp)
  • 이 소스코드는 데이터의 개수(array 리스트 변수의 길이)가 N개일 때, O(N²)의 시간 복잡도를 가진다. 2중 반복문이기 때문이다.
  • 모든 2중 반복문의 시간 복잡도가 O(N²)인 것은 아니다. 내부 함수가 있는 경우 내부 함수의 시간 복잡도도 고려해주어야 한다.
  • 퀵 정렬(6장에서 학습)의 평균 시간 복잡도는 O(NlogN)이지만 최악의 경우 시간 복잡도는 O(N²)이다.
  • 최악의 경우의 시간 복잡도를 우선적으로 고려해야 한다.


1.5. 시간 복잡도 표

  • 위쪽에 있을수록 더 빠르다.
빅오 표기법 명칭
O(1) 상수 시간(Constant time)
O(logN) 로그 시간(Log time)
O(N) 상수 시간(Constant time)
O(N²) 이차 시간
O(N³) 삼차 시간
O(2ⁿ) 지수시간


1.6. 빅오 표기법의 한계

  • 빅오 표기법이 항상 절대적인 것은 아니다.
  • 실제 코딩테스트에서는 차수가 작은 항들을 완전히 무시하는 것도 곤란
3N³ + 5N² + 1,000,000인 알고리즘의 경우

빅오 표기법에서는 차수가 가장 큰 항만 남기 때문에 O(N³)으로 표기

하지만, 실제로 N이 작을 때는 상수 값인 1,000,000의 영향력이 크다.


1.7. 시간복잡도에 따른 알고리즘 선택

  • N의 범위가 500인 경우 : 시간복잡도가 O(N³)인 알고리즘을 설계하면 문제를 풀 수 있다.
  • N의 범위가 2,000인 경우 : 시간복잡도가 O(N²)인 알고리즘을 설계하면 문제를 풀 수 있다.
  • N의 범위가 100,000인 경우 : 시간복잡도가 O(NlogN)인 알고리즘을 설계하면 문제를 풀 수 있다.
  • N의 범위가 10,000,000인 경우 : 시간복잡도가 O(N)인 알고리즘을 설계하면 문제를 풀 수 있다.


2. 공간복잡도

  • 특정한 크기의 입력에 대하여 알고리즘이 얼마나 많은 메모리를 차지하는지를 의미
  • 알고리즘을 위해 필요한 메모리의 양

  • 시간 복잡도처럼 빅오 표기법을 이용
  • 코딩 테스트 문제는 대부분 리스트(배열)를 사용해서 풀어야 한다. 대부분의 문제는 다수의 데이터에 대한 효율적인 처리를 요구하기 때문
  • 정수형 자료인 int를 기준으로 리스트 크기에 따른 메모리 사용량을 확인해보자.

    • int a[1000]: 4KB
    • int a[1000000]: 4MB
    • int a[2000][2000]: 16MB
  • 코딩테스트에서는 보통 메모리 사용량을 128~512MB 정도로 제한한다. 즉, 일반적인 경우 데이터의 개수가 1,000만 단위가 넘어가지 않도록 알고리즘 설계를 해야 한다는 의미


3. 시간과 메모리 측정

  • 다음의 코드를 통해 프로그램 수행 시간을 측정할 수 있다.
* 수행 시간 측정 소스코드

import time
start_time = time.time()  # 측정 시작

# 프로그램 소스코드
end_time = time().time()  # 측정 종료

# 수행 시간 출력
print("time :", end_time - start_time)

3.1. 예시

  • ‘선택 정렬’과 ‘파이썬 기본 정렬 라이브러리’의 속도 비교
  • ‘선택 정렬’ : 최악의 경우 O(N²)
  • ‘기본 정렬 라이브러리’ : 최악의 경우 O(NlogN)
from random import *
import time

# 배열에 10,000개의 정수를 삽입
array = []
for _ in range(10000):
    array.append(randint(1, 100))  # 1부터 100 사이의 랜덤한 정수

# 선택 정렬 프로그램 성능 측정
start_time = time.time()

# 선택 정렬 프로그램 소스코드
for i in range(len(array)):
    min_index = i  # 가장 작은 원소의 인덱스
    for j in range(i + 1, len(array)):
        if array[min_index] > array[j]:
            min_index = j
    # 스와프
    array[i], array[min_index] = array[min_index], array[i]

end_time = time.time()  # 측정 종료

# 수행시간 출력
print("선택 정렬 성능 측정: ", end_time - start_time)


# 기본 정렬 라이브러리 성능 측정
start_time = time.time()

# 기본 정렬 라이브러리 사용
array.sort()

end_time = time.time()  # 측정 종료

# 수행시간 출력
print("기본 정렬 라이브러리 성능 측정: ", end_time - start_time)

출력값은 다음과 같다.

선택 정렬 성능 측정: 3.139763116836548
기본 정렬 라이브러리 성능 측정: 0.0

즉, 이렇게 알고리즘의 성능을 측정하기 위해서 시간 측정 라이브러리를 사용하는 습관을 기르는 것이 좋다.


4. 번외

  • 메모리를 더 소모하는 대신 얻을 수 있는 시간적 이점이 매우 큰 경우가 있다.
  • 메모이제이션(Memoization)
    • 메모리를 더 많이 사용해서 시간을 비약적으로 줄이는 방법
    • 8장에서 더 다룰 예정


REFERENCES

  • <이것이 취업을="" 위한="" 코딩테스트다="" with="" 파이썬="">, 나동빈, 한빛미디어

댓글남기기