단순 구현에서 시간 복잡도 최적화까지 – 삽질 기록(카운팅배열)

KUN·2025년 6월 15일


계기

위 문제를 풀기 위해서 다음과 같이 코드를 짰으나,
시간 복잡도에서 과감하게 털렸다.

class Solution {
    /*
        char 형식으로 1개씩 x 에서 뽑는다.
        
        x = 5, 5, 2, 5
        그리고 y 에서 5를 찾아서 맨 앞에를 삭제한다. 그리고 x 에서도 삭제한다.
        배열 arr 에다가 삭제된 5를 저장한다.
        
        배열 arr 이 완성되면 이들의 숫자중 가장 큰 수를 만든다.
        가장 큰 수를 맨앞에 두도록 정렬하면 끝
    */
    public String solution(String x, String y) {
        StringBuilder builder = new StringBuilder();
        for( char v : x.toCharArray() ){
            String c = String.valueOf(v);
            if ( y.contains(c) ){
                builder.append(c);
                y = y.replaceFirst(c,"");
            }
        }
        
        char[] arr = builder.toString().toCharArray();
        if ( arr.length == 0 ) return "-1";
        for ( int i = 0; i<arr.length-1;i++){
            for ( int j = i+1; j<arr.length; j++){
                if ( arr[i] < arr[j]){
                    char temp = arr[i];
                    arr[i] = arr[j];
                    arr[j] = temp;
                }
            }
        }
        
        builder = new StringBuilder();
        String v2 = builder.append(arr).toString();
        while (v2.startsWith("0")){
            v2 = v2.replaceFirst("0","");
        }
        return v2.isEmpty() ? "0" : v2;
    }
}


문제점 인식

무엇 때문에 시간 복잡도에서 오래걸렸나 고민을 해보면 다음과 같은 결론이 나왔다.

1번째 문제

for( char v : x.toCharArray() ) 를 하여 x 만큼 반복
y.contains(c) 을 진행하면서 내부적으로 반복이 또 들어간다.
y.replaceFirst() 는 y 문자열을 다시 탐색해서 해당 문자를 찾아 새 문자열로 복사한다.
그렇게 된다면 시간 복잡도는 O(n x m) 이 된다.

2번째 문제

흔한 선택정렬이다.
각 자리를 기준으로, 그 뒤에 있는 값들과 하나씩 비교하면서 더 큰 값을 찾아 swap하는 구조다.
시간 복잡도는 O(n²)로, 데이터가 많아질수록 성능에 큰 영향을 준다.

3번째 문제

정렬된 결과에서 앞쪽의 0을 제거하려고 startsWithreplaceFirst를 반복 사용했는데,
최악의 경우를 생각해보면 0000000000123 값이 들어올 경우 9 × O(n) = O(n²) 상황이 온다.


정리하면?

  • 공통 숫자 추출: O(n × m)
  • 선택 정렬: O(n²)
  • 앞자리 0 제거: O(n²)

문자열 길이가 약 3,000자 정도일 경우,
최악의 경우 총 2천만 번 이상의 연산이 발생하게 된다.


해결방안

1. 퀵 정렬(실패)

처음에 선택 정렬로 인해서 정렬이 매우 느리다 생각했다.
그래서 검색해서 가장 빠른 정렬을 할 수 있는 퀵 정렬 이라는 걸 알아냈다.

알고리즘을 직접 구현하는 방법도 있지만,
Arrays.sort() 라는 좋은 기능이 있다.

결국 이것도 퀵정렬, 일반적인 퀵정렬보다 더 최적화된 방식이다.

결과는 똑같이 시간초과가 된다.

2. 카운팅 정렬(성공)

퀵정렬을 사용해 시간 복잡도를 개선해보았지만,
여전히 공통 숫자를 찾는 과정에서 시간 초과가 발생했다.

이때 문득, 문제의 특성이 떠올랐다.

  • 입력은 모두 0~9까지의 숫자 문자열
  • 즉, 값의 범위가 매우 작고 고정되어 있다.

이런 경우, 굳이 비교 기반 정렬을 쓰지 않아도
숫자의 개수만 세는 방식으로 충분히 해결 가능하지 않을까? 하는 생각이 들었다.

가정

만약 552511552 가 나온다면, 5,5,2 가 나와야한다.
A 배열은 2: 1개, 5: 3개
B 배열은 1: 2개, 2: 1개, 5: 2개
복잡한 문자열 반복 없이,
각 숫자의 등장 횟수만 비교하면 정답을 쉽게 만들 수 있다.

이런방법이 뭐가 있나 했더니 카운팅 정렬 이라는 방식이 있다.
카운팅 정렬(Counting Sort, 계수 정렬) 알고리즘 해당 블로그를 참조해서 만들어보겠다.


해결과정

  1. 0 ~ 9 까지 총 10개의 숫자가 들어가기 때문에 10개의 자릿수가 들어가는 배열 2개를 만든다.

  2. c는 문자 '0'부터 '9' 사이의 문자이다.
    '0'은 아스키 코드로 48 -> c - '0' 하면 실제 정수 0~9로 변환되며,
    countX[5]++ > x 문자열에 '5'가 한 번 나왔다고 기록한다.

  1. 우리는 큰수부터 받기위해 9 ~ 0 까지 순서대로 가져올것이다.
  2. 만약, A: 5가 2개, B: 5가 3개 라면, commonCount 는 2가 된다.
  3. result에 5commonCount 만큼 반복해서 넣어준다. 현재 상태 : 5,5
  4. result에 2commonCount 만큼 반복해서 넣어준다. 현재 상태 : 5,5,2

만약 result 가 없다면 -1 있다면 하나씩 붙여주며,
앞에 숫자가 0 이라면, 모든 값이 0 이므로 0으로 반환한다.


성공했다.

해결완료

회고

  • 단순히 정답을 맞추는 데 그치지 않고,
    문제의 특성과 데이터의 구조를 살펴보는 것이 얼마나 중요한지를 체감했다.
  • '숫자의 범위가 작다'는 사실 하나만으로도
    비교 기반 정렬을 피하고, 더 빠른 알고리즘으로 전환할 수 있었다.

전후 데이터 비교

과거

초기 구현에서는 문자열을 반복 탐색하고, 정렬도 직접 구현한 방식이었기 때문에
전체 시간 복잡도가 O(n × m + n² + n²) 수준이였다.

현재

각 문자열을 한 번씩만 순회하며 등장 횟수만 기록하고 (O(n + m))
정수 범위(0~9)에 대해 고정된 반복을 수행하며 (O(1))
전체 시간 복잡도를 O(n + m) 수준으로 줄일 수 있었다.

profile
배우노라, 실험하노라, 기록하노라

0개의 댓글