문자열 신규 아이디 추천-프로그래머스-레벨1

C++로 제한된 환경에서 C 빌려오기

Category: 알고리즘 문제풀이 → 문자열
Difficulty: 초급
Date: 2026-01-20
Read Time: 24 mins read
Views: 조회

문제

카카오에 입사한 신입 개발자 네오는 “카카오계정개발팀”에 배치되어, 유저들의 아이디를 생성하는 업무를 담당하게 되었습니다. “네오”에게 주어진 첫 업무는 규칙에 맞지 않는 아이디를 입력했을 때, 규칙에 맞는 새로운 아이디를 추천해주는 프로그램을 개발하는 것입니다.

카카오 아이디 규칙

  • 아이디의 길이는 3자 이상 15자 이하여야 합니다.
  • 아이디는 알파벳 소문자, 숫자, 빼기(-), 밑줄(_), 마침표(.) 문자만 사용할 수 있습니다.
  • 단, 마침표(.)는 처음과 끝에 사용할 수 없으며 또한 연속으로 사용할 수 없습니다.

아이디 추천 7단계 처리 과정

  1. 1단계: new_id의 모든 대문자를 대응되는 소문자로 치환합니다.
  2. 2단계: new_id에서 알파벳 소문자, 숫자, 빼기(-), 밑줄(_), 마침표(.)를 제외한 모든 문자를 제거합니다.
  3. 3단계: new_id에서 마침표(.)가 2번 이상 연속된 부분을 하나의 마침표(.)로 치환합니다.
  4. 4단계: new_id에서 마침표(.)가 처음이나 끝에 위치한다면 제거합니다.
  5. 5단계: new_id가 빈 문자열이라면, new_id에 “a”를 대입합니다.
  6. 6단계: new_id의 길이가 16자 이상이면, 첫 15개의 문자를 제외한 나머지 문자들을 모두 제거합니다. 만약 제거 후 마침표(.)가 끝에 위치한다면 이를 제거합니다.
  7. 7단계: new_id의 길이가 2자 이하라면, 마지막 문자를 길이가 3이 될 때까지 반복해서 끝에 붙입니다.

제한사항

  • new_id는 길이 1 이상 1,000 이하인 문자열입니다.
  • new_id는 알파벳 대문자, 알파벳 소문자, 숫자, 특수문자로 구성되어 있습니다.
  • new_id에 나타날 수 있는 특수문자는 -_.~!@#$%^&*()=+[{]}:?,<>/ 로 한정됩니다.

입출력 예

no new_id result
예1 "...!@BaT#*..y.abcdefghijklm" "bat.y.abcdefghi"
예2 "z-+.^." "z--"
예3 "=.=" "aaa"
예4 "123_.def" "123_.def"
예5 "abcdefghijklmn.p" "abcdefghijklmn"

입출력 예에 대한 설명

입출력 예 #2

  • 2단계: "z-+.^.""z-.."
  • 3단계: "z-..""z-."
  • 4단계: "z-.""z-"
  • 7단계: "z-""z--"

입출력 예 #3

  • 2단계: "=.=""."
  • 4단계: ".""" (빈 문자열)
  • 5단계: """a"
  • 7단계: "a""aaa"

입출력 예 #5

  • 6단계: "abcdefghijklmn.p""abcdefghijklmn.""abcdefghijklmn"

문제 풀이

아…그냥 단계대로 빡구현이다. 이거는 뭔가 설명할 것은 없다. 그래서 이번엔 C++에서 C를 훔쳐오는 것이 잘만 사용하면 문익점이 목화 씨를 빼돌리듯 알짜 기능들을 빼 올 수 있는 것을 소개하고자 한다.

이번 문제 풀이에는 3가지 코드가 등장한다.

  • Gemini 3.0 Flash의 비교용 모범 C++ 코드
  • 내가 코딩 테스트 제출에 사용한 최적화가 부족한 코드
  • 성능을 깎고 깎아서 만들어낸 코드

우선 모범 답안부터 보자.

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

string solution(string new_id) {
    // 1단계: 소문자 치환
    for (char &c : new_id) c = tolower(c);

    // 2단계: 허용되지 않는 문자 제거
    string step2 = "";
    for (char c : new_id) {
        if (isalnum(c) || c == '-' || c == '_' || c == '.') {
            step2 += c;
        }
    }

    // 3단계: 마침표 연속 부분 압축
    string step3 = "";
    for (char c : step2) {
        if (c == '.' && !step3.empty() && step3.back() == '.') continue;
        step3 += c;
    }

    // 4단계: 처음이나 끝의 마침표 제거
    if (!step3.empty() && step3.front() == '.') step3.erase(0, 1);
    if (!step3.empty() && step3.back() == '.') step3.pop_back();

    // 5단계: 빈 문자열 처리
    if (step3.empty()) step3 = "a";

    // 6단계: 길이 제한 및 끝 마침표 제거
    if (step3.length() >= 16) {
        step3 = step3.substr(0, 15);
        if (step3.back() == '.') step3.pop_back();
    }

    // 7단계: 길이 보충
    while (step3.length() < 3) {
        step3 += step3.back();
    }

    return step3;
}

아주 깔끔하다. 문제는 전형적인 복잡한 테스트 케이스로 겁주기 유형에 해당한다.

나의 통과되었지만 아직은 느린 코드를 보도록 하겠다.

#include <string>
#include <vector>
#include <cstdlib>
#include <cstring>
#include <ctype.h>

using namespace std;

void push_char(char **str, int *top, char c) {
    char *s = *str;
    (*top)++; 
    s[*top] = c;
}

void pop(char **str, int *top) {
    if (*top < 0) return;
    (*str)[*top] = '\0';
    (*top)--;
}

string solution(string newid) {
    
    char *new_id   = (char *)newid.c_str()                           ;
    char *answer   = (char *)calloc(newid.length() + 1, sizeof(char));
    int top_answer = -1;
    
    for(int i = 0; i < strlen(new_id); i++) {
        char c = tolower(new_id[i]);
        if(
            isdigit(c)
            | (int)(c == '-')
            | (int)(c == '_')
            | (int)(c == '.') 
        ) {
        if (
            (c == '.')
          & ((i == 0) | (i == (int)strlen(new_id) - 1))
        ) continue;
            push_char(&answer, &top_answer, c);
        } else if(
            ('a' <= c)
         && ('z' >= c)
        ) {
             push_char(&answer, &top_answer, c);
         }
    }
    
    char * final_answer = (char *) calloc(newid.length() + 1, sizeof(char));
    int    final_top    = -1;
    
    char *ans    = (char *) answer;
    char *saveptr;
    char *token  = strtok_r(ans, ".", &saveptr);
    
    while(true) {
        if(token == NULL) break;
        for(char *c = token; *c != '\0'; c++) push_char(&final_answer, &final_top, *c);
        push_char(&final_answer, &final_top, '.');
        token = strtok_r(NULL, ".", &saveptr);
    }
    
    if (final_top != -1) pop(&final_answer, &final_top);
    
    if (strlen(final_answer) > 15) {
        int over = strlen(final_answer) - 15;
        for(int i = 0; i < over; i++) {
            pop(&final_answer, &final_top);
        }
    }    
    if(strlen(final_answer) == 0) return "aaa";
        
    if(strlen(final_answer) < 3) {
        char last = final_answer[final_top];
        int count = 3 - strlen(final_answer);
        for(int i = 0; i < count; i++) push_char(&final_answer, &final_top, last);
    }
    
    if (final_answer[final_top] == '.') pop(&final_answer, &final_top);
    string cpp_answer(final_answer);
    
    free(answer);
    free(final_answer);
    
    return cpp_answer;
}

여기서는 급하게 짠다고 비트 연산 등의 최적화가 있지만 strlen을 너무 과하게 호출한다. 벤치 결과는 좋지 못하다.

Metric | C-Style | C++ Style

Average Latency (ms) | 0.0208 | 0.0165 Max Latency (ms) | 0.0600 | 0.0400 Efficiency Gap | -20.37% slower

이제 본격적인 최적화를 해 보자. 이 경우 strlen을 지역 변수에 적당히 쌓아 두고 쓸 것이다.

#include <string>
#include <vector>
#include <cstdlib>
#include <cstring>
#include <ctype.h>

using namespace std;

void push_char(char **str, int *top, char c) {
    char *s = *str;
    (*top)++;
    s[*top] = c;
}

void pop(char **str, int *top) {
    if (*top < 0) return;
    (*str)[*top] = '\0';
    (*top)--;
}

string solution(string newid) {
    int original_len = (int)   newid.length();
    char *new_id     = (char *)newid.c_str() ;

    int top_answer = -1;
    char *answer     = (char *)calloc(original_len + 1, sizeof(char));

    for(int i = 0; i < original_len; i++) {
        char c = tolower(new_id[i]);
        if(isdigit(c)
           | (c == '-')
           | (c == '_')
           | (c == '.')) {

            if ((c == '.') & 
             & ((i == 0)
             | (i == original_len - 1))) continue;

            push_char(&answer, &top_answer, c);
        } else if(('a' <= c) && ('z' >= c)) {
            push_char(&answer, &top_answer, c);
        }
    }

    char * final_answer = (char *) calloc(original_len + 2, sizeof(char));
    int    final_top    = -1;
    char *saveptr;
    char *token  = strtok_r(answer, ".", &saveptr);

    while(token != NULL) {
        for(char *c = token; *c != '\0'; c++)
            push_char(&final_answer, &final_top, *c);
        push_char(&final_answer, &final_top, '.');
        token = strtok_r(NULL, ".", &saveptr);
    }

    if (final_top != -1)
        pop(&final_answer, &final_top);

    int current_len = final_top + 1;

    if (current_len > 15) {
        int over = current_len - 15;
        for(int i = 0; i < over; i++)
            pop(&final_answer, &final_top);
        current_len = 15;
    }

    if(current_len == 0) {
        free(answer);
        free(final_answer);
        return "aaa";
    }

    if(current_len < 3) {
        char last = final_answer[final_top];
        int count = 3 - current_len;
        for(int i = 0; i < count; i++)
            push_char(&final_answer, &final_top, last);
        current_len = 3;
    }

    if (final_answer[final_top] == '.')
        pop(&final_answer, &final_top);

    string cpp_answer(final_answer);
    free(answer);
    free(final_answer);

    return cpp_answer;
}

지역 변수에 길이 정보를 저장하니 가독성도 나아졌다. 또한, if 조건이 복잡할 때는 소괄호에서도 적재적소에 개행이나 공백을 넣어 프로그래밍적인 변경 없이 가독성을 챙길 수도 있다.

물론 순수한 C++ 코드에 비해 좋지 않으나 성능 향상을 위해서 용납 가능한 수준이다.

Performance Metric | C-Style (Opt) | C++ (STL)

Total Execution Time (ms) | 0.39 | 0.43 Average Latency (ms) | 0.0150 | 0.0165 Max Peak Latency (ms) | 0.03 | 0.04 Performance Lead | 10.26% faster than STL

그렇게 최적화가 잘 된 C++의 STL보다도 10.26% 빠르다.

이러한 C기반 코드의 실무적 장점은 코딩 테스트보다 프로젝트 작업 시 더욱 빛을 발한다.

압도적인 예측 가능성

C는 화려하고 똑똑한 문법을 도입하지 않는 대신 예측할 수 있는 성능을 보여준다. 이러한 언어를 사용한다면 프로그램은 극도로 정제되어 늘 비슷한 실행 시간, 비슷한 CPU 점유율, 심지어는 파이프라이닝 타이밍까지도 편차가 적을 것이다. 물론 C++ 역시 상당 부분 C를 대체할 수 있는 수준까지 왔으나, 도리어 그 로직의 복잡성이 증가할수록 잘 정리된 C++ 프로젝트와 잘 정리된 C 프로젝트 사이에서, 오히려 유지보수성의 일정 측면은 C가 우수한 경우도 있을 만큼 괜히 살아남은 언어가 아니다.

아까 strlen으로 인한 성능 악화 역시 C이기 때문에 반복적인 변수 참조로 길이를 잰 것이 문제라고 드러난 것이지, 만약 웬만한 알고리즘들이 다 붙박이로 들어가 있는 C++이나 Python 3, Java같은 언어였다면 내장된 스트림이나 문자열 처리 함수에서 어떤 것이 문제인지 굳이 문서를 뒤져 봐야 한다.

이러한 즉각적인 문제 인식과 수용이 가능하기에 아직도 C는 신생 프로젝트들이 태어나는 몇 안되는 원로급 언어로 자리잡은 것이다.

약간의 성능 향상이 중요할 때

금융 거래, 현물 투자 등의 상황에서는 밀리세컨드 단위의 성능 저하가 누적되어 사용자들이 불만을 느낄 수 있는 수준으로 늘어날 수도 있는데다, 1초의 지연으로 중요한 거래 도중 환율이 바뀌어 버린다던가 하는 사고가 날 수 있다. 이러한 환경에서는 C++ 사이에 C를, 심한 경우 어셈블리를 박아넣기도 한다. 이러할 때 C를 보더라도 겁먹지 않으려면 이런 쉬운 문제들로 연습해 볼 필요가 있다.

결론

실전 코딩 테스트에서 C++에 익숙하다면 나의 방식은 권장할 것이 아니며, 이것보다 복잡한 자료 구조를 다룰 때는 더더욱 권장하지 않는다. 그러나 C++로 짰을 때 확실히 코드가 너무 추상적인 상황이 있다면 그 때 이러한 연습은 빛을 발할 것이다.

만약 이 글을 읽는 당신이 대학교 1학년이라면 그 시점부터 이러한 연습을 충실하게 하여 향후 걸림이 없어야 한다. 기초를 다지는 것은 언제든 중요한 일이다.

Document Classification

Subcategory
문자열
Keywords
프로그래머스 알고리즘 문자열 파싱
Difficulty
초급
Permalink
https://gg582.github.io/codingtest/2026-01-20-%5B%EB%AC%B8%EC%9E%90%EC%97%B4%5D-%EC%8B%A0%EA%B7%9C-%EC%95%84%EC%9D%B4%EB%94%94-%EC%B6%94%EC%B2%9C-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%A0%88%EB%B2%A81/

Citation

이윤진(Lee Yunjin) (2026). [문자열] 신규 아이디 추천-프로그래머스-레벨1. 윤진의 IT 블로그. Retrieved from https://gg582.github.io/codingtest/2026-01-20-%5B%EB%AC%B8%EC%9E%90%EC%97%B4%5D-%EC%8B%A0%EA%B7%9C-%EC%95%84%EC%9D%B4%EB%94%94-%EC%B6%94%EC%B2%9C-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%A0%88%EB%B2%A81/
── 하략 ──