내 코드가 느린 이유? 시간 복잡도(Time Complexity)와 Big-O 정리_Part 1

2025. 12. 8. 17:30·TEST/Testing
📂
Problem Source: GitHub - Sw_Pilot_Java (Chapter 2 / Step 2 - 3)
이 포스팅은 해당 리포지토리의 문제에 대한 분석 및 풀이 과정을 담고 있습니다.

알고리즘 문제를 풀거나 실무에서 대용량 데이터를 다루다 보면, 기능은 완벽한데 "시간 초과"가 뜨거나 시스템이 버벅이는 경우를 마주합니다.

이때 우리가 가장 먼저 점검해야 할 것이 바로 시간 복잡도(Time Complexity)입니다. 오늘은 제가 공부한 내용을 바탕으로, O(n²)의 늪에서 빠져나와 O(n)으로 최적화하는 과정을 정리해보려 합니다.

 

 


 

1. 시간 복잡도란?

시간 복잡도는 알고리즘이 입력 크기(Input Size)에 따라 얼마나 많은 연산을 수행하는지를 나타내는 지표입니다.

단순히 "몇 초 걸린다"가 아니라, 수학적으로 실행 시간의 변화 추이를 표현하는 것이죠.

  • 최악의 시간(Worst Case): 빅오(Big-O) 표기법의 기준이 되며, 가장 오래 걸릴 때를 가정합니다.
  • 필요성: 효율적인 알고리즘 선택, 성능 개선 포인트 파악, 대규모 데이터 처리 가능 여부 판단

2. Big-O 표기법 총정리

Big-O 표기법은 데이터 입력 크기(n)가 무한대로 커질 때, 연산 횟수가 어떻게 증가하는지 상한선을 나타냅니다.

아래 표는 빠른 순서(위)에서 느린 순서(아래)로 정렬되어 있습니다.

표현 속도 / 이름 대표 예시
O(1) 상수 시간 (Fastest) Stack의 Push/Pop, 배열 인덱스 접근
O(log n) 로그 시간 이진 탐색 (Binary Search)
O(n) 선형 시간 for문 1번 순회
O(n log n) 선형 로그 시간 퀵 정렬(Quick), 병합 정렬(Merge)
O(n^2) 이차 시간 이중 for문, 삽입/거품 정렬
O(2^n) 지수 시간 (Exponential) 피보나치 수열(재귀), 부분집합 구하기
O(n!) 팩토리얼 시간 (Slowest) 외판원 순회 문제(TSP), 모든 순열 구하기
💡 성능 순서 요약:
O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2^n) < O(n!)

오른쪽으로 갈수록(아래로 갈수록) 실행 시간이 기하급수적으로 늘어나므로,
데이터가 많을 때는 O(n²) 이상의 알고리즘은 피하는 것이 좋습니다.

▲ Big-O Complexity Chart (입력 크기에 따른 시간 증가량)

 

3. Case Study 01: 중복 없는 숫자 찾기

첫 번째 케이스는 "리스트에서 딱 한 번만 등장하는 숫자를 찾는 상황"입니다. 여기서 초보자들이 가장 많이 하는 실수가 발생합니다.

 

내가 작성한 문제의 코드 (O(n²))

for (int val : list) {
    // 리스트 전체를 돌면서 val의 개수를 센다
    if (Collections.frequency(list, val) == 1) { 
        System.out.println(val);
    }
}

 

병목 분석

Collections.frequency(list, val) 메서드는 마법처럼 숫자를 세주는 게 아닙니다.

내부적으로 리스트 전체를 처음부터 끝까지 순회(O(n))합니다.

  • 바깥쪽 for문: n회 반복
  • 안쪽 frequency: n회 탐색
  • 총 시간 복잡도: n × n = O(n²)

데이터가 10만 개라면, 100억 번의 연산을 하게 되어 프로그램이 멈출 수 있습니다.


최적화: HashMap 사용 (O(n))

이 문제를 해결하기 위해 HashMap을 사용하여 데이터를 미리 세어둡니다(Preprocessing).

// 1. O(n): 리스트를 한 번만 돌면서 개수를 맵에 저장
Map<Integer, Integer> countMap = new HashMap<>();
for (int val : list) {
    countMap.put(val, countMap.getOrDefault(val, 0) + 1);
}

// 2. O(n): 맵을 확인하며 1번만 등장한 값 찾기
// (Map의 조회는 평균적으로 O(1)입니다)

 

이렇게 하면 루프가 중첩되지 않고 분리되어 O(n) + O(n) = O(2n), 즉 O(n)으로 획기적인 성능 향상이 일어납니다.


4. Case Study 02: 회문(Palindrome) 판별기

두 번째 케이스는 "문장을 단어별로 쪼개고, 회문이면 출력, 아니면 뒤집어서 출력"하는 로직입니다.

내가 작성한 코드 (O(n))

// 문장을 단어별로 쪼갬
for (int i = sens.length - 1; i >= 0; i--) {
    // 각 단어(word)의 앞뒤를 비교 (회문 검사)
    for (int e = 0; e < word.length() / 2; e++) {
        if (word.charAt(e) != word.charAt(word.length() - 1 - e)) {
            // ...
        }
    }
}

복잡도 분석 (이건 병목이 아님)

얼핏 보면 for문 안에 for문이 있어 O(n²)처럼 보일 수 있습니다. 하지만 자세히 들여다보면 다릅니다.

  • n: 단어의 개수
  • k: 각 단어의 평균 길이
  • N: 전체 문자열의 길이 (n × k)

우리는 모든 단어의 모든 글자를 한 번씩만 확인합니다. 즉, 전체 연산 횟수는 전체 글자 수(N)와 비례합니다.

💡 결론: O(nk) = O(N) (전체 문자 수 기준 선형 시간)
입력 길이가 최대 10⁶이라도, O(N)은 충분히 효율적이므로 굳이 최적화할 필요가 없습니다.

5. 병목 현상 및 최적화 방안 요약

오늘 분석한 내용을 표로 정리해 보았습니다.

구분 병목 원인 기존 시간 복잡도 개선 후
Case 01
(숫자 찾기)
Collections.frequency에 의한
중복 순회 발생
O(n²)
(매우 느림)
O(n)
(HashMap 사용)
Case 02
(회문 판별)
없음 (구조적으로 효율적) O(N)
(전체 문자 수)
변경 불필요

 


 

 

시간 복잡도 분석은 "내 코드가 왜 느린지"를 찾아내는 가장 중요한 핵심 이론입니다.

Case 01처럼 숨겨진 루프(frequency)를 찾아내 O(n)으로 만드는 것이 최적화의 핵심이고, Case 02처럼 구조적으로 이미 최적화된 코드를 구별해내는 안목 또한 중요한 것 같습니다.

 

 

  참고 자료
- Big-O Cheat Sheet
- VisuAlgo
- GeeksforGeeks (Collections.frequency)

'TEST > Testing' 카테고리의 다른 글

Big-O, 왜 상수는 무시할까? (feat. Ω, Θ 표기법 정리)_Part 2  (0) 2025.12.08
'TEST/Testing' 카테고리의 다른 글
  • Big-O, 왜 상수는 무시할까? (feat. Ω, Θ 표기법 정리)_Part 2
hlxecz
hlxecz
1인분 개발자가 되고 싶은 개발 기록
  • hlxecz
    H.Dev Log
    hlxecz
  • 전체
    오늘
    어제
    • 분류 전체보기 (12)
      • Language (7)
        • Java (7)
        • C (0)
        • PHP (0)
        • Python (0)
      • Framewrok (0)
        • Spring (0)
      • Data (0)
        • DBMS (0)
      • Cloud (0)
        • Amazon Cloud (0)
      • Knowledge (3)
        • 자료구조 (0)
        • 알고리즘 (1)
        • 디자인 패턴 (2)
      • DevOps (0)
      • Dev Kit (0)
        • InteliJ (0)
        • VSCode (0)
      • TEST (2)
        • Testing (2)
        • Error (0)
      • ETC (0)
        • 일상 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Stack
    Java
    인터페이스
    OOP
    BIG-O
    문제풀이
    Interface
    자바
    바밀로니아
    빅오표기법
    문제점
    Collection
    인터페이스 vs 추상클래스
    생각정리
    collections
    GOF
    string
    재귀함수
    시간복잡도
    디자인 패턴
    Abstract
    알고리즘
    setter
    캡슐화
    Solid
    피드백
    getter
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
hlxecz
내 코드가 느린 이유? 시간 복잡도(Time Complexity)와 Big-O 정리_Part 1
GitHub 상단으로

티스토리툴바