시간을 줄이는 효율적인 코딩 기술

1. 문제

 

https://programmers.co.kr/learn/courses/30/lessons/42888

 

코딩테스트 연습 - 오픈채팅방

오픈채팅방 카카오톡 오픈채팅방에서는 친구가 아닌 사람들과 대화를 할 수 있는데, 본래 닉네임이 아닌 가상의 닉네임을 사용하여 채팅방에 들어갈 수 있다. 신입사원인 김크루는 카카오톡 오

programmers.co.kr

 

카카오톡 오픈채팅방에는 친구가 아닌 사람들과 대화를 할 수 있는데, 본래 닉네임이 아닌 가상의 닉네임을 사용하여 채팅방에 들어갈 수 있다.

 

신입사원인 김크루는 카카오톡 오픈 채팅방을 개설한 사람을 위해, 다양한 사람들이 들어오고 나가는 것을 지켜볼 수 있는 관리자창을 만들기로 했다.

 

채팅방에 누군가 들어오면 다음 메시지가 출력된다.

 

“[닉네임]님이 들어왔습니다.”

 

채팅방에 누군가 나가면 다음 메시지가 출력된다.

 

“[닉네임]님이 나갔습니다.”

 

채팅방에서 닉네임을 변경하는 방법은 다음과 같이 두가지이다.

 

채팅방을 나간 후, 새로운 닉네임으로 다시 들어간다.

 

채팅방에서 닉네임을 변경한다.

 

닉네임을 변경할 때는 기존에 채팅방에 출력되어 있던 메시지의 닉네임도 전부 변경된다.

 

예를 들어, 채팅방에 “Muzi”와 “Prodo”라는 닉네임을 사용하는 사람이 순서대로 들어오면 채팅방에는 다음과 같이 메시지가 출력된다.

 

“Muzi님이 들어왔습니다.”

 

“Prodo님이 들어왔습니다.”

 

채팅방에 있던 사람이 나가면 채팅방에는 다음과 같이 메시지가 남는다.

 

“Muzi님이 들어왔습니다.”

 

“Prodo님이 들어왔습니다.”

 

“Muzi님이 나갔습니다.”

 

Muzi가 나간후 다시 들어올 때, Prodo라는 닉네임으로 들어올 경우 기존에 채팅방에 남아있던 Muzi도 Prodo로 다음과 같이 변경된다.

 

‘Prodo님이 들어왔습니다.”

 

“Prodo님이 들어왔습니다.”

 

“Prodo님이 나갔습니다.”

 

“Prodo님이 들어왔습니다.”

 

채팅방은 중복 닉네임을 허용하기 때문에, 현재 채팅방에는 Prodo라는 닉네임을 사용하는 사람이 두 명이 있다.

 

이제, 채팅방에서 두 번째로 들어왔던 Prodo가 Ryan으로 닉네임을 변경하면 채팅방 메시지는 다음과 같이 변경된다.

 

‘Prodo님이 들어왔습니다.”

 

“Ryan님이 들어왔습니다.”

 

“Prodo님이 나갔습니다.”

 

‘Prodo님이 들어왔습니다.”

 

채팅방에 들어오고 나가거나, 닉네임을 변경한 기록이 담긴 문자열 배열 record가 매개변수로 주어질 때,

 

모든 기록이 처리된 후, 최종적으로 방을 개설한 사람이 보게 되는 메시지를 문자열 배열 형태로 return하도록 solution 함수를 완성해라.

 

 

2. 제한사항과 예시

 

 

3. 나의 풀이

 

record에서 하나씩 빼서 공백(‘ ‘)을 기준으로 split하면 (명령어), (아이디), (닉네임)을 가지는 리스트로 분리될거임

 

명령어가 enter냐 leave냐 change냐에 따라서 행동이 달라질거

 

def solution(record):
    
    answer_list = []
    
    in_dic = {}
    
    for rec in record:
        
        rec = rec.split(' ')

 

여기서 rec[0]가 enter냐 leave냐 change냐 셋중하나인데…

 

if문을 이용해서 rec[0] == ‘Enter’, rec[0] == ‘Leave’, rec[0] == ‘Change’로 나눌 수 있는데

 

사실 이것보다는 

 

맨 앞 한글자만 비교하면 바로 할 수 있다는 점에서 시간을 조금이라도 더 줄일 수 있음

 

애초에 글자수가 적어서 큰 의미 없긴한데.,.. 이런 디테일이 시간을 줄이는 코딩을 할 수 있다는거임

 

아무튼 그래서 if rec[0][0] = ‘E’, elif rec[0][0] == ‘L’, else:를 이용해서 3개의 분기로 나눔

 

if rec[0][0] == 'E':
    
    in_dic[rec[1]] = rec[2]

 

먼저 누군가 들어왔다고 했을 때, leave 메시지에는 닉네임이 나오지 않기 때문에 아이디에 매칭되는 닉네임을 따로 저장해둘 필요가 있음

 

그래서 입장하는 순간 아이디와 매칭되는 닉네임을 사전에 저장을 시킴

 

rec = rec.split(' ')

if rec[0][0] == 'E':
    
    in_dic[rec[1]] = rec[2]
    
    answer.append(f'{rec[2]}님이 들어왔습니다.')
    
elif rec[0][0] == 'L':
    
    answer.append(f'{in_dic[rec[1]]}님이 나갔습니다.')

 

그러면 단순하게는 이렇게 할 수 있을 것 같은데… 그러면 문제는 change할 때가 문제임

 

change를 한다고 하면 answer의 메시지의 닉네임을 바꿔야하는데…

 

닉네임과 id가 매칭이 안되니까 어떤 메시지를 바꿔야할지 모른단 말이지

 

그래서.. 들어가는 메시지와 나가는 메시지를 따로 사전으로 저장하는거지

 

rec = rec.split(' ')

if rec[0][0] == 'E':
    
    in_dic[rec[1]] = rec[2]
    
    answer_in_dic[rec[1]] = f'{rec[2]}님이 들어왔습니다.'
    
elif rec[0][0] == 'L':
    
    answer_out_dic[rec[1]] = f'{in_dic[rec[1]]}님이 나갔습니다.'

 

그러면 id를 찾아서 매칭시켜서 메시지를 바꾸면 되니까 change도 할 수 있는거임

 

 

if rec[0][0] == 'E':
    
    in_dic[rec[1]] = rec[2]
    
    answer_in_dic[rec[1]] = f'{rec[2]}님이 들어왔습니다.'
    
elif rec[0][0] == 'L':
    
    answer_out_dic[rec[1]] = f'{in_dic[rec[1]]}님이 나갔습니다.'
    
else:
    
    answer_in_dic[rec[1]] = f'{rec[2]}님이 들어왔습니다.'
    
    in_dic[rec[1]] = rec[2]

 

change 메시지는 채팅방 안에 있는 사람만 가능하니까… 나갔습니다를 바꿀필요는 없어

 

그리고 닉네임 바꿨으니까 닉네임 바꾼 정보를 in_dic에 넣어

 

근데 이제 answer에 메시지를 넣어야하는데 이제 문제는 순서가 어떻게 되는지를 몰라

 

그래서 enumerate로 index 정보까지 넣겠다는 거

 

for index,rec in enmuerate(record):
    
    rec = rec.split(' ')
    
    if rec[0][0] == 'E':
        
        in_dic[rec[1]] = rec[2]
        
        answer_in_dic[rec[1]] = [index,f'{rec[2]}님이 들어왔습니다.']
        
    elif rec[0][0] == 'L':
    
        answer_out_dic[rec[1]] = [index,f'{in_dic[rec[1]]}님이 나갔습니다.']
        
    else:
        
        answer_in_dic[rec[1]][1] = f'{rec[2]}님이 들어왔습니다.'
        
        in_dic[rec[1]] = rec[2]

 

리스트로 담은 이유는 튜플은 원소를 변경할 수 없어서 그럼..

 

아무튼 이제 이런식으로 하면

 

사전 answer_in_dic, answer_out_dic의 value들만 가지고 와서

 

리스트에 전부 담은 다음에 sort시키면 index의 오름차순으로 정렬시킬 수 있어서

 

하나씩 뺀다음에 첫번째 원소만 다시 담으면 된단 말이지

 

total_list = list(answer_in_dic.values())

total_list.extend(list(answer_out_dic.values()))

answer = [rec[1] for rec in total_list]

 

이렇게하면 통과를 못하는데 고려하지 못한 부분들이 조금 있음

 

동일한 아이디가 enter하고 leave한다음에 닉네임을 바꾸고 다시 enter할 수 있단 말이지

 

이런 사항을 고려하고자 아이디와 닉네임이 저장된 사전에서 들어오는 사람의 아이디가 존재하면 다른 행동을 취해야함

 

if rec[0][0] == 'E':
    
    if rec[1] in in_dic.keys():
        
        answer_in_dic[rec[1]][1] = f'{rec[2]}님이 들어왔습니다.'
        
        answer_out_dic[rec[1]][1] = f'{rec[2]}님이 나갔습니다.'
        
    else:
        
        in_dic[rec[1]] = rec[2]
        
        answer_in_dic[rec[1]] = [index,f'{rec[2]}님이 들어왔습니다.]

 

근데 이렇게 하면 문제는 새로운 닉네임으로 들어온 메시지를 넣질 못했음

 

이전 메시지를 바꾸기만 했을 뿐 새로운 닉네임으로 들어온 메시지를 넣질 않아서…

 

새로운 사전을 만들어서...

 

if rec[1] in in_dic.keys():

    answer_in_dic[rec[1]][1] = f'{rec[2]}님이 들어왔습니다.'
    
    answer_out_dic[rec[1]][1] = f'{rec[2]}님이 나갔습니다.'
    
    answer_re_in_dic[rec[1]] = [index,f'{rec[2]}님이 들어왔습니다.']
    
    in_dic[rec[1]] = rec[2]
    
else:

    in_dic[rec[1]] = rec[2]
    
    answer_in_dic[rec[1]] = [index,f'{rec[2]}님이 들어왔습니다.']

 

answer_re_in_dic을 고려해서 거기에 들어왔다는 메시지를 담았음

 

또 하나.. change도 고려해야하는데… enter하고 leave하고 다시 enter한다음에 change할 수 있단 말이지…

 

그러면 leave한 메시지의 닉네임도 바꿔줘야함

 

else:

    answer_in_dic[rec[1]][1] = f'{rec[2]}님이 들어왔습니다.'
    
    if rec[1] in answer_out_dic.keys():
        
        answer_out_dic[rec[1]][1] = f'{rec[2]}님이 나갔습니다.'
        
    in_dic[rec[1]] = rec[2]

 

그러면 re_in_dic의 value도 고려해서 리스트에 담아가지고 했는데.. 통과를 못함

 

왜 안될까..? 여기서 고민을 엄청했는데

 

똑같은 사람이 enter하고 leave하고 enter하는 상황을 잘 고려해서 answer_re_in_dic을 고려했는데

 

문제는 enter하고 leave하고 enter한다음에 다시 leave하고 다시 enter하면 어떻게 할거냐 이거임…

 

그러면 문제가 answer_re_out_dic도 고려해야하고 answer_re_re_in_dic을 또 고려해야하고…

 

그걸 고려했다고 치더라도 또 문제는 enter leave enter leave enter leave enter… 이렇게 되면???

 

사전을 더 늘릴거임..?? 얼마나 늘려야할지 모르기때문에 근본부터 갈아엎어야함…

 

여기서 한가지 잘못된 생각이란건… index를 고려해서 사전에 있는 내용을 다 합쳐서 정렬해서 한다는것이다

 

그렇다면 index를 고려한다는 생각이 잘못된 것이기 때문에 record의 기록을 하나 처리하면 리스트에 그대로 집어넣어야함...

 

for rec in record:
    
    rec = rec.split(' ')
    
    if rec[0][0] == 'E':
        
        in_dic[rec[1]] = rec[2]
        
        answer_list.append([rec[1], f'{rec[2]}님이 들어왔습니다.'])
        
    elif rec[0][0] == 'L':
        
        answer_list.append([rec[1], f'{in_dic[rec[1]]}님이 나갔습니다.'])

 

change를 또 고려해야하기 때문에 일단 메시지만 넣는게 아니라 매칭되는 아이디까지 함께 넣어줌

 

다음에 change를 해야되는 상황이 오면 결국에 answer_list를 돌아서 하나씩 뺀다음에… 아이디가 같으면 메시지를 change한다

 

for ans in answer_list:
    
    if rec[1] == ans[0]:
        
        ans[1] =
        
    else:
        
        continue

 

 

근데 이제 이렇게하면 문제가… 들어온 메시지인지 나간 메시지인지를 알 수가 없으니까 바꾸기가 까다롭다는거임

 

그래서 그런 지표를 answer_list에 넣어줘

 

for ans in answer_list:
    
    if rec[1] == ans[0]:
    
        if ans[1] == 'in':
            
            ans[2] = f'{rec[2]}님이 들어왔습니다.'
            
        else:
        
        
            ans[2] = f'{rec[2]}님이 나갔습니다.'
            
in_dic[rec[1]] = rec[2]

 

이렇게 하면 change 명령어에서는 지금까지 출력된 메시지들에서 해당 아이디의 닉네임은 전부 바꾸는거거든… 그리고 마지막에 in_dic에 바뀐 닉네임을 저장해둠

 

이런 방식을 이용해서 enter하고 leave한다음에 닉네임을 바꾸고 enter한 경우에도 그동안 메시지를 바꿔야하니까…

 

if rec[0][0] == 'E':
    
    if rec[1] in in_dic.keys():
        
        for ans in answer_list:
            
            if rec[1] == ans[0]:
                
                if ans[1] == 'in':
                    
                    ans[2] = f'{rec[2]}님이 들어왔습니다.'
                    
                else:
                    
                    ans[2] = f'{rec[2]}님이 나갔습니다.'
                    
        answer_list.append([rec[1], 'in', f'{rec[2]}님이 들어왔습니다.'])
        
        in_dic[rec[1]]= rec[2]

 

이미 들어온 기록이 있는 경우에.. 그동안 answer_list의 for문을 돌아서 해당 아이디의 메시지 닉네임을 전부 바꾼 다음에… 다시 들어온 기록을 answer_list에 넣어주고

 

바뀐 닉네임을 저장..

 

이러면 사전이 무한정 필요로하는 아까의 문제를 해결함

 

그러면 마지막으로 answer_list에서 하나씩 빼서 index=2인 원소를 리스트에 집어넣으면.. 끝

 

 

 

이렇게 하니까 시간 초과로 통과를 못해…

 

왜 시간 초과가 걸릴까…

 

change를 위해서 for ans in answer_list:을 도는거랑 key가 존재하는지 if rec[1] in in_dic.keys():로 검색하는거

 

이게 리스트가 길어지면 길어질수록 시간이 걸릴수밖에 없거든

 

여기서 또 고민을 엄청 했는데

 

이 문제의 가장 큰 핵심이 뭐냐면 닉네임을 바꾸는 작업

 

근데 record가 끝나고 나서 결국에 최종적으로 바뀌는 닉네임을 answer에 집어넣으면 되거든

 

내가 한 코드는 매회마다 닉네임을 바꿔서 집어넣고 하니까 상당히 비효율적인거임

 

 

for rec in record:

    rec = rec.split(' ')
    
    if rec[0][0] == 'E':
        
        answer_list.append([rec[1],'in'])
        
        in_dic[rec[1]] = rec[2]
        
    elif rec[0][0] == 'L':
        
        answer_list.append([rec[1],'out'])
        
    else:
    
        in_dic[rec[1]] = rec[2]

 

record의 모든 원소를 하나씩 돌면서… 들어오면 in을 하고 해당 아이디에 매칭되는 닉네임을 사전에 넣어

 

나가면 out했다는 것만 일단 넣어놓고

 

change하는 경우 닉네임만 바꿔 넣어

 

그러면 for문을 전부 다 돌면 결국에 in_dic에는 record가 끝나고 나서 최종적으로 해당 아이디의 닉네임만 기록될거거든..

 

record가 끝나고 나서 최종적으로 변경된 닉네임이 모든 채팅방의 메시지에 표시된다는 핵심을 잘 이해한다면 이것이 가능

 

answer = [f'{in_dic[rec[0]]}님이 들어왔습니다.' if rec[1] == 'in' else
          f'{in_dic[rec[0]]}님이 나갔습니다.' for rec in answer_list]
  
return answer

 

그러면 answer_list에서 하나씩 빼서 ‘in’인 경우 들어온 메시지를 넣어야하는데 해당하는 닉네임은 in_dic에 저장되어 있고

 

in이 아니라면 나갔다는 메시지를 넣어준다면…?

 

 

완벽하게 시간을 줄여서 통과했음을 확인할 수 있다.

 

 

4. 다른 풀이

 

다른 사람 풀이도 보면 비슷함

 

결국 이 문제의 가장 큰 핵심은… record가 다 끝나고 나서 아이디의 최종적인 닉네임으로 메시지가 전부 바뀐다는거임

 

user_id = {EC.split()[1]:EC.split()[-1] for EC in record
           if EC.startswith('Enter') or EC.startswith('Change')}
           
return [f'{user_id[E_L.split()[1]])님이 들어왔습니다.' if E_L.startswith('Enter')
        else f'{user_d[E_L.split()[1]]}님이 나갔습니다.'
        for E_L in record if not E_L.startswith('Change')]

 

record를 돌면서 user_id에 매칭되는 닉네임을 user_id에 사전으로 저장함

 

for문이 끝나면 id에 매칭되는 닉네임은 최종적으로 변경된 닉네임

 

그러면 다시 record에서 돌아서 enter인 경우 해당 아이디의 최종 닉네임으로 들어왔습니다 출력하고 enter가 아니면 나갔습니다를 출력함

 

change면 채팅방에 메시지를 출력 안하니까 if not으로 change는 제외했음

 

def solution(record):
    
    answer = []
    
    namespace = {}
    
    printer = {'Enter':'님이 들어왔습니다.', 'Leave':'님이 나갔습니다.'}
    
    for r in record:
        
        rr = r.split(' ')
        
        if rr[0] in ['Enter', 'Change']:
            
            namespace[rr[1]] = rr[2]
            
    for r in record:
        
        if r.split(' ')[0] != 'Change':
            
            answer.append(namespace[r.split(' ')[1]] + printer[r.split(' ')[0]]])
            
     return answer

 

이것도 비슷한게 첫번째 for문에서 namespace에는 for문이 끝나면 해당 아이디의 최종 닉네임이 나오고

 

 

두번째 for문에서는 change는 메시지를 출력 안하니까 change가 아니라면

 

namespace에서 해당하는 닉네임 가지고오고

 

거기에 printer에서 enter이냐 leave이냐로 메시지 붙여서 answer에 넣어줌

 

 

5. 되돌아보기

 

이리저리 헤맸지만 결국 해결한게 대단하다

 

시간을 어떻게 하면 효율적으로 줄일 수 있을까 항상 생각하면서…

 

for문을 돌아도 되는건지…

 

rec[1] in in_dic.keys(): 같은걸로 검색을 해도 되는건지

 

‘Enter’와 비교할건지… ‘E’와 비교할건지…

 

이게 가능할려면 결국에 매 순간마다 닉네임을 바꿔주면 비효율적이기 때문에

 

문제를 관통하는 핵심이었던 record가 끝나고 나서 최종적으로 바뀐 닉네임으로 메시지가 전부 바뀐다는걸 이해할 수 있느냐 아니냐였다..

 

동적프로그래밍이랑 비슷하네

TAGS.

Comments