https://school.programmers.co.kr/learn/courses/30/lessons/133499
프로그래머스
SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프
programmers.co.kr
설계
가능한 옹알이가 { "aya", "ye", "woo", "ma" } 이므로 직관적으로 'a', 'y', 'w', 'm' 으로 시작하지 않는 단어(babbling)은 로직을 피하고 가능한 옹알이가 나오면 그부분을 잘라 계속 비교하고 완벽히 다 잘라져서 문자열이 공백이 되면 answer의 카운트를 올린다.
구현
#include <string>
#include <vector>
using namespace std;
int solution(vector<string> babbling) {
int answer = 0;
vector<string> word = { "aya", "ye", "woo", "ma" };
for (int i = 0; i < babbling.size(); i++)
{
if (babbling[i][0] == 'a'
|| babbling[i][0] == 'm'
|| babbling[i][0] == 'w'
|| babbling[i][0] == 'y')
{
string preWord = "";
bool exitCount = true;
while (exitCount)
{
exitCount = false;
if (babbling[i] == "")
{
answer++;
break;
}
if (preWord != word[0] && babbling[i].substr(0, word[0].size()) == word[0]) // aya
{
babbling[i] = babbling[i].substr(word[0].size(), babbling[i].size());
preWord = word[0];
exitCount = true;
}
if (preWord != word[1] && babbling[i].substr(0, word[1].size()) == word[1]) // ye
{
babbling[i] = babbling[i].substr(word[1].size(), babbling[i].size());
preWord = word[1];
exitCount = true;
}
if (preWord != word[2] && babbling[i].substr(0, word[2].size()) == word[2]) // woo
{
babbling[i] = babbling[i].substr(word[2].size(), babbling[i].size());
preWord = word[2];
exitCount = true;
}
if (preWord != word[3] && babbling[i].substr(0, word[3].size()) == word[3]) // ma
{
babbling[i] = babbling[i].substr(word[3].size(), babbling[i].size());
preWord = word[3];
exitCount = true;
}
}
}
}
return answer;
}
리펙토링
#include <string>
#include <vector>
using namespace std;
int solution(const vector<string>& babbling) {
int answer = 0;
vector<string> words = { "aya", "ye", "woo", "ma" };
for (const string& b : babbling) {
// [가지치기 (Early Exit)]
// 빈 문자열이거나, 첫 글자가 a, y, w, m 중 어느 것도 아니라면 발음 불가능한 단어로 판정
if (b.empty() || (b[0] != 'a' && b[0] != 'y' && b[0] != 'w' && b[0] != 'm')) {
continue; // 아래의 복잡한 비교 로직을 무시하고, 즉시 다음 단어로 점프합니다. (속도 향상)
}
int idx = 0;
int prev_match = -1;
bool is_valid = true;
// 2. 현재 단어(b)의 끝에 도달할 때까지 반복 검사합니다.
while (idx < b.length()) {
bool matched = false; // 이번 턴에 4개 단어 중 하나라도 일치했는지 확인하는 플래그
// 3. 4가지 발음 단어들을 차례대로 비교합니다.
for (int j = 0; j < 4; j++) {
// 조건 1: 연속된 발음이 아닐 것 (prev_match != j)
// 조건 2: 현재 위치(idx)부터 words[j]의 길이만큼 잘라서 비교했을 때 일치할 것
// b.compare(시작위치, 비교할길이, 비교할문자열) == 0 이면 완전히 일치한다는 뜻입니다.
if (prev_match != j && b.compare(idx, words[j].length(), words[j]) == 0) {
idx += words[j].length(); // 일치한 단어의 길이만큼 인덱스를 앞으로 점프! (문자열 자르기 대체)
prev_match = j; // 방금 일치한 단어의 번호를 기억 (연속 방지용)
matched = true; // 일치하는 단어를 찾았다고 표시
break; // 단어를 찾았으므로 나머지 발음은 검사할 필요 없이 for문 탈출
}
}
// 4. 4개 단어를 다 뒤져봤는데 일치하는 게 하나도 없다면?
// -> 조카가 발음할 수 없는 이상한 문자열이 섞여 있다는 뜻입니다.
if (!matched) {
is_valid = false; // 불가능한 단어로 판정
break; // 더 이상 검사할 필요 없으므로 while문 탈출
}
}
// 5. 문자열 끝까지 무사히 검사를 마쳤고(is_valid == true),
// 남은 찌꺼기 문자 없이 인덱스가 문자열 길이와 딱 맞아떨어지면 정답 개수 증가
if (is_valid && idx == b.length()) {
answer++;
}
}
return answer;
}
주요 리팩토링 포인트
1. substr() 사용 지양 (공간/시간 복잡도 최적화)
- 문제점: babbling[i] = babbling[i].substr(...) 코드는 문자열을 자를 때마다 새로운 문자열 객체를 메모리에 생성하고 복사합니다. 단어가 길어질수록, 자르는 횟수가 많아질수록 엄청난 메모리 낭비와 시간 지연(오버헤드)이 발생합니다.
- 해결책: 문자열을 직접 자르는 대신, **'현재 읽고 있는 위치(인덱스)'**만 숫자로 기억하면서 앞으로 이동하는 방식을 사용합니다. 이를 통해 문자열 복사 없이 원본 문자열을 한 번만 쭉 훑으면서 검사할 수 있습니다.
2. 반복되는 if문 병합 (가독성 향상)
- 문제점: "aya", "ye", "woo", "ma" 4개 단어에 대해 완전히 동일한 로직의 if문이 4번 반복되고 있습니다. 코드가 길어지고 오타가 발생하기 쉽습니다.
- 해결책: 4개의 발음을 배열(vector)에 넣어두고, 내부에서 for문을 돌리면서 검사하면 코드를 훨씬 짧고 우아하게 만들 수 있습니다.
3. 매개변수 상수 참조 const vector<string>& (공간 복잡도 최적화)
- 문제점: vector<string> babbling으로 받으면, 함수가 호출될 때 원본 배열 전체가 메모리에 통째로 복사됩니다.
- 해결책: const vector<string>& babbling처럼 참조자(&)를 사용하면 복사 없이 원본을 그대로 읽기만 하므로 메모리 사용량을 최소화할 수 있습니다.
회고
시간복잡도의 고려는 익숙해졌지만 공간복잡도의 고려를 못한 점이 아쉬웠고, 시간복잡도를 고려하여 가지치기를 하였는데 정작 검사 로직에선 for문을 사용하지 않아 전체적인 부분을 보지 못한점이 아쉬웠다.
'코드테스트' 카테고리의 다른 글
| [level 1] 체육복 - 42862 (0) | 2026.03.19 |
|---|---|
| [level 1] 숫자 짝꿍 - 131128 (0) | 2026.03.17 |
| [level 1] 기사단원의 무기 - 136798 (0) | 2026.03.13 |
| 덧칠하기 - 161989 (0) | 2026.03.09 |
| 소수 만들기 - 12977 (0) | 2026.03.08 |
