Python/자료구조 & 알고리즘

[자료구조와 알고리즘] 시간 복잡도, 빅오 표기법

sdesign416 2026. 6. 4. 11:51

프로그램을 개발할때에는 같은 결과를 만들어 내는 코드라도 실행 속도는 천차만별이다.

 

예를 들어 특정 데이터를 찾을 때

처음부터 끝까지 순서대로 하나씩 확인하는 방법이 있고,

정렬된 데이터를 활용해서 훨씬 빠르게 찾아내는 방법도 있다.

 

데이터의 개수가 적을때는 큰 차이를 못느끼지만,

수백개, 수만개의 데이터를 처리하게 되면 알고리즘의 성능 차이는 매우 극명하게 나타난다.

 

이때 알고리즘의 성능 분석을 하기 위해 사용되는 개념이 시간 복잡도이다.


시간 복잡도란?

시간 복잡도는 프로그램이 실행될 때 걸리는 시간을

입력 크기에 따라 나타낸 것이다.

 

여기서 말하는 시간은 실제 시간(몇시, 몇분, 몇초)를 뜻하는게 아니라

프로그램이 실행되면서 수행되는 연산의 횟수를 말하는 것이다.

 

쉽게 말해 시간 복잡도는

'데이터가 많아졌을 때 이 코드가 얼마나 느려질까?'에 대한 

답을 내리는 기준이 된다고 보면 쉽다.

※ 입력크기 n이 증가할수록 + 위쪽에 있는 알고리즘일수록 빠르게 연산횟수가 증가한다.

즉, 효율적인 알고리즘일수록 아래쪽에 위치한다.


💡 시간 복잡도 왜 사용할까?

예를 들어 영어 사전에서 'Koala'라는 단어를 찾으려고 할 때

K단락을 펼쳐서 찾으면 효율적으로 빠르게 단어를 찾을 수 있지만

사전의 첫 페이지(A)부터 마지막 페이지(Z)까지 한글자씩 모두 읽어가며 단어를 찾으면 매우 비효율적이다.

 

즉, 시간 복잡도는 내 코드가 무식하게 일하는 코드인지,

스스로 판단하고 성능을 지키며 효율적이게 일하는 코드인지 확인하기 위해 사용된다.


빅오 표기법

빅오 표기법은 시간 복잡도를 표현할 수 있는 가장 대표적인 방법이다.

입력 크기가 커질 때 연산량이 어떻게 증가하는지 간단하게 나타내며,

보통 O(1), O(n), O(n²)과 비슷한 형태로 작성된다.

※ n = 데이터 개수

※ log n = 데이터를 절반씩 나누었을 때 1이 될 때까지의 횟수 (데이터 8이면 log n은 3, 데이터 16이면 log n은 4) 실제로는 log₂(n) 형태로 작동하는것을 log n으로 표기한것이다.


자주보는 빅오 표기법 종류

O(1) : 상수 시간

데이터가 많아져도 연산 횟수가 항상 일정한 경우이다.

가장 빠른 시간 복잡도이며, 데이터가 아무리 많아져도 성능에는 큰 변화가 없다.

데이터 1개 → 1번 실행
데이터 100개 → 1번 실행
데이터 1000개 → 1번 실행

 

대표 예시

  • 배열(Array)의 인덱스 접근
  • 스택(Stack)의 Push, Pop

예제 코드

데이터가 1개든 100만개든 상관없이 연산 횟수가 고정되어있어

입력값 n이 실행시간에 전혀 영향을 주지 않는다.

# 입력값 n이 아무리 커져도 반복 횟수는 3 고정
n = 1000000 

for i in range(3):    # n과 관계없이 항상 3번만 실행 (상수 연산)
    print("실행")

 

O(log n) : 로그 시간

데이터를 절반씩 줄여가며 탐색하거나 처리하는 방식으로

데이터가 매우 많아져도 연산 횟수가 크게 증가하지 않아 효율적이다.

데이터 8개 → 약 3번
데이터 16개 → 약 4번
데이터 32개 → 약 5번
데이터 1024개 → 약 10번


대표 예시

  • 이진 탐색(Binary Search)
  • 이진 탐색 트리(BST)

예제 코드

실행할 때마다 처리해야 할 데이터의 양이 절반씩 줄어든다.

n = 1024

while n > 1:
    print(f"현재 남은 데이터 크기: {n}")
    n //= 2  # 루프를 한 번 돌 때마다 n이 절반으로 감소 (O(log n))
    
# 데이터가 1024개일 때, 10번의 연산으로 종료

 

O(n) : 선형 시간

데이터 전체를 한 번씩 확인해야 하는 방식으로

데이터 개수에 비례하여 연산 횟수가 증가한다.

데이터 10개 → 10번 실행
데이터 100개 → 100번 실행
데이터 1000개 → 1000번 실행


대표 예시

  • 선형 탐색(Linear Search)
  • 리스트 전체 순회

예제 코드

입력값 n의 크기에 비례하여 실행 시간이 직선적으로 증가한다.

n = 10

for i in range(n):  # n의 크기만큼 n번 반복 수행
    print(f"{i+1}번째 데이터 처리 중")

# n이 10배 커지면 실행 시간도 10배 늘어남

 

O(n log n) : 선형 로그 시간

데이터를 나누면서 동시에 전체 데이터를 처리하는 방식으로

효율적인 정렬 알고리즘에서 자주 등장한다.

O(n)보다는 조금 더 느리지만, O(n²)보다는 훨씬 효율적이다

데이터 10개 → 약 33번
데이터 100개 → 약 664번
데이터 1000개 → 약 9966번


대표 예시

  • 병합 정렬(Merge Sort)
  • 퀵 정렬(Quick Sort, 평균)
  • 힙 정렬(Heap Sort)

예제 코드

가장 대표적인 예시는 이진 탐색으로

1부터 100까지 숫자 중 하나를 맞출 때,

매번 업/다운을 통해 범위를 절반씩 줄여나간다.

데이터가 2개 늘어나도 연산 횟수는 단 1번만 늘어난다.

n = 16

for i in range(n):  # 바깥 루프 : n번 반복 (O(n))
    x = n
    while x > 1:    # 안쪽 루프 : 데이터를 절반씩 줄임 (O(log n))
        x //= 2

# 결과적으로 n * log n 만큼 실행됨

 

O(n²) : 제곱 시간

데이터를 서로 비교해야하는 경우 자주 나타나며,

이중 반복문 구조에서 많이 발생하게 된다.

데이터가 10배 증가하면 연산횟수는 100배 증가하는 방식이다.

데이터 10개 → 100번 실행
데이터 100개 → 10,000번 실행
데이터 1000개 → 1,000,000번 실행


대표 예시

  • 버블 정렬(Bubble Sort)
  • 선택 정렬(Selection Sort)
  • 삽입 정렬(Insertion Sort, 평균)

예제 코드

중첩 반복문 사용 시 나타나며,

데이터가 조금만 늘어나도 실행 시간이 많이 증가한다.

데이터가 10배 늘어나면 시간은 100배가 된다.

n = 10

for i in range(n):      # 첫 번째 루프 (n번)
    for j in range(n):  # 두 번째 루프 (n번)
        # n x n 번의 연산이 발생
        print(f"좌표 ({i}, {j}) 처리")

# 모든 원소를 서로 비교해야 할 때 발생(버블 정렬)

반복문 개수에 따른 시간 복잡도

시간 복잡도는 반복문의 개수와 형태에 따라 달라질 수 있다.

 

1) 반복문이 1개일 경우

데이터를 n번 반복하기 때문에 시간 복잡도는 O(n)이다.

for i in range(n):
    print(i)

 

2) 중첩 반복문 사용할 경우

n x n번 실행되기 때문에 시간 복잡도는 O(n²)이다.

for i in range(n):
    for j in range(n):
        print(i, j)

 

3) 중첩 반복문이 3개 사용될 경우

n x n x n번 실행되기 때문에 시간 복잡도는 O(n³)이다.

for i in range(n):
    for j in range(n):
        for k in range(n):
            print(i, j, k)

 

4) 연속된 반복문이 사용될 경우

n + n = 2n이지만 빅오 표기법에선 상수를 제거하기 때문에 O(n)으로 표현된다.

for i in range(n):
    print(i)

for j in range(n):
    print(j)

시간 복잡도 비교

일반적으로 O(1)에 가까울수록 효율이 좋으며,
O(n²) 이상부터는 데이터가 많아질 경우 성능 저하가 크게 발생할 수 있다.

따라서 자료구조와 알고리즘을 선택할 때는 시간 복잡도를 함께 고려하는 것이 중요하다.

시간 복잡도 증가 속도 대표 예시
O(1) 일정 배열 인덱스 접근
O(log n) 매우 느리게 증가 이진 탐색
O(n) 데이터와 비례하여 증가 선형 탐색
O(n log n) 비교적 효율적 병합 정렬, 퀵 정렬
O(n²) 빠르게 증가 버블 정렬

자료구조별 시간복잡도

1) 배열(Array)

배열은 연속된 메모리 공간에 데이터를 저장하는 자료구조

인덱스를 통해 원하는 위치에 바로 접근할 수 있어 데이터 조회가 매우 빠른 특징이 있다.

 

연속된 메모리 공간이기 때문에 

중간에 데이터를 삽입하거나 삭제할 경우 많은 데이터 이동이 발생할 수 있다.

  • 접근 → O(1)
  • 탐색 → O(n)
  • 삽입 → O(n)
  • 삭제 → O(n)

2) 연결리스트(Linked List)

연결리스트는 노드가 데이터와 다음 노드의 주소를 저장하는 방식으로

배열과 다르게 메모리가 연속적이지 않기 때문에

특정 위치의 데이터를 찾기 위해선 처음부터 순서대로 탐색하여 찾아야한다.

 

노드 간 연결을 통하여 데이터를 관리하기 때문에

노드 연결 정보만 변경하면 삽입과 삭제가 빠르게 가능하다.

  • 접근 → O(n)
  • 탐색 → O(n)
  • 삽입 → O(1)
  • 삭제 → O(1)

3) 스택(Stack)

스택은 LIFO구조로 가장 마지막에 들어온 데이터가 가장 먼저 제거되는 방식이다.

그렇기 때문에 테이터 추가와 삭제가 매우 빠른 반면

중간 데이터에 대한 접근은 비효율적인 구조이다.

  • 접근 → O(1)
  • 탐색 → O(n)
  • 삽입(Push) → O(1)
  • 삭제(Pop) → O(1)

4) 큐(Queue)

큐는 FIFO구조로 가장 먼저 들어온 데이터가 가장 먼저 제거되는 방식이다.

스택과 구조는 다르지만 데이터 추가와 삭제가 빠르고

중간 데이터를 찾는 경우 순차 탐색이 필요해 비효율적이다.

  • 접근 → O(1)
  • 탐색 → O(n)
  • 삽입(Enqueue) → O(1)
  • 삭제(Dequeue) → O(1)

5) 트리(Tree)

트리는 부모-자식 관계를 가지고 있는 자료구조로

이진탐색트리는 데이터를 효율적으로 탐색할 수 있다.

 

트리구조는 계층형 자료구조이기 때문에 균형이 유지 될 경우 빠른 탐색이 가능하지만

트리가 한쪽으로 치우치게 된다면 탐색과정이 O(n)이 될수도 있다.

  • 접근 → O(log n)
  • 탐색 → O(log n)
  • 삽입 → O(log n)
  • 삭제 → O(log n)

리스트 함수 시간복잡도

Python의 리스트는 내부적으로 동적 배열 구조를 사용하고 있다.

그렇기 때문에 리스트와 함께 사용되는 함수마다 시간 복잡도가 다르게 나타난다.


1) append()

append() 함수는 리스트의 맨 뒤에 데이터를 추가하는 방식으로

기존 데이터 이동하지 않고 마지막 위치에 추가 데이터를

바로 저장하기 때문에 매우 빠르게 동작한다.

  • 평균 시간복잡도 → O(1)

2) insert()

insert() 함수는 원하는 위치에 데이터를 추가하는 방식으로

추가 위치 뒤에 데이터가 있을경우 한 칸씩 뒤로 이동시켜야한다.

그렇기 때문에 데이터가 많을수록 시간이 증가한다.

  • 평균 시간복잡도 → O(n)

3) pop()

pop() 함수는 리스트의 맨 뒤에 데이터를 삭제하는 방식으로

마지막 데이터만 톡 삭제해주면 되기 때문에

별다른 데이터 이동이 발생하지 않는다.

※ pop(0) 으로 앞쪽 요소를 제거 할 경우 : 뒤의 데이터들을 모두 앞으로 이동해야하기 때문에 O(n)이 된다.

  • 평균 시간복잡도 → O(1)

4) remove()

remove() 함수는 지정한 값을 찾아 삭제하는 방식으로

삭제 전, 먼저 데이터를 순차적으로 탐색한다.

그렇기 때문에 데이터가 많을수록 탐색 할 데이터가 많아지기 때문에 시간이 증가한다.

  • 평균 시간복잡도 → O(n)

5) sort()

sort() 함수는 리스트 전체를 정렬하는 방식으로

파이썬의 Timsort라는 알고리즘을 사용하여

많은 데이터를 효율적으로 정렬할 수 있다.

  • 평균 시간복잡도 → O(n log n)

💡 Timsort란 무엇인가?

Python의 list.sort()와 sorted() 함수에서 사용하는 정렬 알고리즘으로

병합 정렬과 삽입 정렬의 장점을 합쳐서 만든 정렬 알고리즘이다.

 

❓ 왜 만든걸까?

실제로 데이터가 어느정도 정렬이 되어있는 경우(대부분 정렬이 되어있고 일부만 섞임)

Timsort는 이미 정렬된 구간을 찾고, 해당 구간을 분석하여

정렬되지 않은 부분들을 정렬해주는 기능을 한다.

 

그렇기 때문에 Timsort는 사용할 수 있는 상황이 주어지면

안정적이고 성능이 좋은 정렬 알고리즘으로 사용될 수 있다.

 

  • 최선(Best) → O(n)
  • 평균(Average) → O(n log n)
  • 최악(Worst) → O(n log n)

 


정렬알고리즘 시간복잡도

1) 버블 정렬(Bubble Sort)

버블 정렬은 인전한 두 데이터를 비교하면서

큰 값은 뒤로 이동시키는 정렬방식으로

데이터가 많아질수록 비교횟수가 많아져 비효율적이다.

  • 평균 시간복잡도 → O(n²)

2) 선택 정렬(Selection Sort)

선택 정렬은 가장 작은값을 찾아 앞으로 이동시키는 정렬방식으로

버블정렬과 동일하게 데이터가 많아질수록 비교횟수가 많아진다.

  • 평균 시간복잡도 → O(n²)

3) 삽입 정렬(Insertion Sort)

삽입 정렬은 이미 정렬된 영역의 적절한 위치에 데이터를 추가하는 방식이다.

이는 데이터가 거의 정렬되어있을 때 효율적으로 사용할 수 있다.

  • 평균 시간복잡도 → O(n²)

4) 병합 정렬(Merge Sort)

병합 정렬은 데이터를 계속 반으로 나누고 합치는것을 반복하는 정렬방식으로

안정적이고 성능이 매우 효율적인 방식이다..

  • 평균 시간복잡도 → O(n log n)

5) 퀵 정렬(Quick Sort)

기준값을 선택해서 작은값과 큰값을 분할해가며 정렬하는 방식이다.

  • 평균 시간복잡도 → O(n log n)
  • 최악 시간복잡도 → O(n²)

정리

※ 표의 색상 의미

🟢 초록색 : 성능, 특성 우수
🟡 노란색 : 보통 수준
🔴 빨간색 : 상대적으로 불리함

 

1) 자료구조 시간복잡도 비교

자료구조 접근 탐색 삽입 삭제
배열 O(1) O(n) O(n) O(n)
연결리스트 O(n) O(n) O(1) O(1)
스택 O(1) O(n) O(1) O(1)
O(1) O(n) O(1) O(1)
트리 O(log n) O(log n) O(log n) O(log n)

2) 리스트 함수 시간복잡도 비교

함수 시간복잡도 설명
append() O(1) 맨 뒤에 데이터 추가
insert() O(n) 중간 위치 삽입 시 데이터 이동 발생
pop() O(1) 맨 뒤 데이터 삭제
remove() O(n) 삭제할 값을 탐색 후 제거
sort() O(n log n) 전체 데이터 정렬

3) 정렬 알고리즘 시간복잡도 비교

정렬 알고리즘 최선 평균 최악
버블 정렬 O(n) O(n²) O(n²)
선택 정렬 O(n²) O(n²) O(n²)
삽입 정렬 O(n) O(n²) O(n²)
병합 정렬 O(n log n) O(n log n) O(n log n)
퀵 정렬 O(n log n) O(n log n) O(n²)