Kakao, 2021 카카오 블라인드 코딩 테스트, 신규 아이디 추천

Series: 알고리즘 문제풀이

알고리즘 문제풀이contains 12

1. 문제 설명

카카오에 입사한 신입 개발자 네오는 "카카오계정개발팀"에 배치되었다. 네오가 맡게 된 업무는 카카오 서비스에 가입하는 유저들의 아이디를 생성하고 관리하는 일이었다.

네오에게 주어진 첫 번째 업무는 신규 유저가 카카오 아이디 규칙에 맞지 않는 아이디를 입력했을 때, 입력된 아이디와 유사하면서도 규칙에 맞는 아이디를 추천해주는 프로그램을 개발하는 것이었다.

카카오 아이디 규칙은 다음과 같다.

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

네오는 신규 유저가 입력한 아이디가 위 규칙에 맞는지 검사하고, 규칙에 맞지 않는 경우 새로운 아이디를 추천해주려고 한다. 이때 추천 아이디는 총 7단계의 순차적인 처리 과정을 거쳐 만들어진다.

신규 유저가 입력한 아이디를 new_id라고 할 때, 처리 과정은 다음과 같다.

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

예를 들어 new_id 값이 "...!@BaT#*..y.abcdefghijklm"이라면, 위 7단계를 거치면서 값은 다음과 같이 변경된다.

1단계에서는 대문자 BT가 각각 소문자 bt로 바뀐다.

"...!@BaT#*..y.abcdefghijklm"
→ "...!@bat#*..y.abcdefghijklm"

2단계에서는 사용할 수 없는 문자 !, @, #, *가 제거된다.

"...!@bat#*..y.abcdefghijklm"
→ "...bat..y.abcdefghijklm"

3단계에서는 연속된 마침표가 하나의 마침표로 치환된다.

"...bat..y.abcdefghijklm"
→ ".bat.y.abcdefghijklm"

4단계에서는 아이디의 처음에 위치한 마침표가 제거된다.

".bat.y.abcdefghijklm"
→ "bat.y.abcdefghijklm"

5단계에서는 아이디가 빈 문자열이 아니므로 변화가 없다.

"bat.y.abcdefghijklm"
→ "bat.y.abcdefghijklm"

6단계에서는 아이디의 길이가 16자 이상이므로, 처음 15자를 제외한 나머지 문자가 제거된다.

"bat.y.abcdefghijklm"
→ "bat.y.abcdefghi"

7단계에서는 아이디의 길이가 2자 이하가 아니므로 변화가 없다.

"bat.y.abcdefghi"
→ "bat.y.abcdefghi"

따라서 신규 유저가 입력한 new_id"...!@BaT#*..y.abcdefghijklm"일 때, 네오의 프로그램이 추천하는 새로운 아이디는 "bat.y.abcdefghi"가 된다.

2. 문제

신규 유저가 입력한 아이디를 나타내는 new_id가 매개변수로 주어진다.

이때 네오가 설계한 7단계 처리 과정을 거친 후의 추천 아이디를 반환하도록 solution 함수를 완성해야 한다.

3. 제한사항

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

4. 입출력 예시

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

5. 문제 풀이

이 문제는 주어진 7단계 규칙을 순서대로 구현하면 되는 문제이다. 저는 처음 문제를 풀 때 최대한 기본적인 문자열 처리 방식에 충실해서 코드를 작성했다.

먼저 소문자 알파벳, 사용 가능한 기호, 숫자를 flags 리스트에 담아두었다. 이후 new_id를 순차적으로 가공하면서 문제에서 요구하는 조건을 하나씩 처리했다.

1단계에서는 lower() 함수를 사용해 입력받은 new_id를 모두 소문자로 변환했다.

2단계에서는 new_id 문자열을 순회하면서 사용할 수 없는 문자를 제거했다. 이때 문자열을 리스트로 변환한 뒤 다시 집합으로 변환하여 중복을 제거했고, 해당 문자가 flags에 포함되어 있지 않다면 replace() 함수를 사용해 빈 문자열로 바꿔주었다.

3단계에서는 마침표의 연속 여부를 확인하기 위해 point_cnt 변수를 사용했다. new_id를 순회하면서 마침표가 등장하면 point_cnt를 증가시키고, 마침표가 2번 이상 연속되는 경우 해당 부분을 하나의 마침표로 치환했다.

4단계에서는 new_id의 처음이나 끝에 마침표가 있는지 확인했다. 문자열의 길이가 0이 아니면서 첫 번째 문자가 마침표라면 첫 문자를 제거했고, 마지막 문자가 마침표라면 마지막 문자를 제거했다.

5단계부터 7단계까지는 문자열 길이에 따라 처리했다. 가공된 new_id의 길이가 0이라면 최종적으로 "aaa"가 되도록 처리했고, 길이가 16 이상이라면 처음부터 15개 문자만 남겼다. 이때 남은 문자열의 마지막 문자가 마침표라면 다시 제거했다. 마지막으로 길이가 2 이하라면 전체 길이가 3이 될 때까지 마지막 문자를 반복해서 붙였다.

작성한 코드는 다음과 같다.

def solution(new_id):

    flags = [
        "a", "b", "c", "d", "e", "f", "g",
        "h", "i", "j", "k", "l", "m", "n",
        "o", "p", "q", "r", "s", "t", "u",
        "v", "w", "x", "y", "z",
        "-", "_", "."
    ] + [str(x) for x in range(0, 10)]

    # 1단계
    new_id = new_id.lower()

    # 2단계
    for flag in set(list(new_id)):

        if flag not in flags:
            new_id = new_id.replace(flag, "")

    # 3단계
    point_cnt = 0
    for flag in new_id:

        if flag == ".":
            point_cnt += 1

            if point_cnt > 1:
                new_id = new_id.replace("."*point_cnt, ".")
                point_cnt = 0

    # 4단계
    if (len(new_id)) and (new_id[0] == "."):
        new_id = new_id[1:]
    if (len(new_id)) and (new_id[-1] == "."):
        new_id = new_id[:-1]

    # 5단계
    if len(new_id) == 0:
        new_id = "a"*3
    elif len(new_id) >= 16:
        new_id = new_id[:15]
        if new_id[-1] == ".":
            new_id = new_id[:-1]
    elif len(new_id) <= 2:
        new_id = new_id + new_id[-1] * (3-len(new_id))

    return new_id

이 코드는 문제에서 제시한 조건을 그대로 따라가면서 구현한 방식이다. 각 단계를 직접 나누어 처리했기 때문에 동작 흐름을 이해하기 쉽다는 장점이 있다. 다만 문자열 치환을 여러 번 수행하고, 직접 조건을 하나씩 처리하기 때문에 코드가 다소 길어지는 편이다.

다른 참여자의 풀이 중에서는 정규표현식을 사용한 답안이 가장 인상 깊었다. 정규표현식을 사용하면 각 단계에서 처리해야 하는 문자열 조건을 훨씬 간결하게 표현할 수 있다.

import re

def solution(new_id):
    st = new_id
    st = st.lower()
    st = re.sub('[^a-z0-9\-_.]', '', st)
    st = re.sub('[.]+', '.', st)
    st = re.sub('^[.]|[.]$', '', st)
    st = 'a' if len(st) == 0 else st[:15]
    st = re.sub('^[.]|[.]$', '', st)
    st = st if len(st) > 2 else st + "".join([st[-1] for i in range(3-len(st))])
    return st

정규표현식을 사용한 코드는 확실히 제가 작성한 코드보다 짧고 간결하다. 특히 사용할 수 없는 문자를 제거하는 부분이나, 연속된 마침표를 하나로 줄이는 부분은 정규표현식과 잘 어울린다.

st = re.sub('[^a-z0-9\-_.]', '', st)
st = re.sub('[.]+', '.', st)

첫 번째 정규표현식은 소문자 알파벳, 숫자, 빼기, 밑줄, 마침표를 제외한 문자를 제거한다. 두 번째 정규표현식은 하나 이상 연속된 마침표를 하나의 마침표로 치환한다. 처음 풀이를 작성할 때는 문제의 7단계를 그대로 코드로 옮기는 데 집중했다. 그래서 각 단계가 명확하게 드러나는 코드를 작성할 수 있었다. 반면 정규표현식을 사용한 풀이는 같은 처리를 더 짧고 효율적으로 표현한다. 이번 문제는 단순히 정답을 맞히는 것뿐만 아니라, 문자열 처리 문제에서 정규표현식을 적절히 사용하면 코드가 얼마나 간결해질 수 있는지 확인할 수 있는 문제였다.