본문 바로가기
AI/데이터 엔지니어링 데브코스

[1주차-4] 코딩테스트 연습 - 해시, 그리디, 정렬 알고리즘

by aiant 2023. 4. 13.

해시

 파이썬에서는 딕셔너리만 가지고 해시함수를 간단히 구현할 수 있다. 해시를 이용하면 데이터가 key-value꼴로 묶이므로 데이터에 대한 탐색이 O(1)에 끝난다는 장점이 있다. 반면 리스트를 이용하면 리스트 전체를 탐색해야 하므로 O(n)의 시간이 소요된다. 예를 들어 파이썬의 in 매소드를 사용할 때, 딕셔너리는 k라는 값이 key에 존재하는지 dic[k]를 함으로써 바로 알아낼 수 있지만 리스트에서는 리스트의 처음부터 끝까지 탐색해보아야 한다. 대신 딕셔너리 자료구조는 데이터 간의 순서가 없다. 그래서 딕셔너리는 주로 순서가 필요 없는 알고리즘과 함께 쓰이곤 한다. 다음 예시 문항을 통해 해시의 쓰임을 익혀보자.

 

완주하지 못한 선수

문제 설명

 수많은 마라톤 선수들이 마라톤에 참여하였습니다. 단 한 명의 선수를 제외하고는 모든 선수가 마라톤을 완주하였습니다.
마라톤에 참여한 선수들의 이름이 담긴 배열 participant와 완주한 선수들의 이름이 담긴 배열 completion이 주어질 때, 완주하지 못한 선수의 이름을 return 하도록 solution 함수를 작성해주세요.

 

제한 사항

1. 마라톤 경기에 참여한 선수의 수는 1명 이상 100,000명 이하입니다.
2. completion의 길이는 participant의 길이보다 1 작습니다.
3. 참가자의 이름은 1개 이상 20개 이하의 알파벳 소문자로 이루어져 있습니다.
4. 참가자 중에는 동명이인이 있을 수 있습니다.

 

✅ 풀이

 이 문제를 리스트를 순회하면서 하나하나 찾아 풀면 시간이 매우 오래 걸리므로 해시 함수를 통해 "인물이름":count로 딕셔너리를 만들어 푼다. 이 문제의 핵심은 참가자 중 동명이인이 있다는 점이다. 따라서 리스트에 들어 있는 인물과 그 인물의 등장 횟수를 담은 딕셔너리를 만든다. 

def solution(participant, completion):
    dic = {}
    for i in participant:
        if i in dic: #딕셔너리에서는 in을 통해 key값에 있는지 확인할 수 있다.
            dic[i]+=1
        else:
            dic[i] = 1
    for i in completion:
        if i in dic:
            dic[i] -= 1
            if dic[i] == 0:
                del dic[i]
    return list(dic.keys())[0]

다른 풀이로는 collections 모듈을 이용한 풀이가 있다. 

from collections import Counter
def solution(participant, completion):
    answer= Counter(participant) - Counter(completion) #conter은 차집합을 지원한다.
    return list(answer)[0]

 

 

그리디

 그리디(탐욕법) 알고리즘은 알고리즘의 각 단계에서 그 순간 최적이라고 생각되는 것을 선택하는 알고리즘이다. 만약 현재의 선택이 마지막 해답의 최적성을 해치지 않을 때, 탐욕법으로 최적해를 찾을 수 있다. 대표적인 예시로 '가장 적은 수의 동전으로 교환하기' 같은 문제에서 가장 큰 단위인 500원의 개수부터 구하고 나머지 돈을 더 작은 단위 동전으로 순차적으로 교환하는 문제가 있다. 위의 예시에서 만일 작은 단위의 동전을 먼저 교환했다면 돈이 모두 가장 작은 단위의 동전으로 교환되어 마지막 해답의 최적성을 해치게 된다. 따라서 그리디 알고리즘을 구현할 때는 단순히 현재 최적인 방법만을 생각하는 것이 아니라 마지막 해답의 최적성을 반드시 고려해야 한다. 다음 예시 문항을 통해 그리디 알고리즘의 쓰임을 익혀보자.

 

 문제 설명

 점심시간에 도둑이 들어, 일부 학생이 체육복을 도난당했습니다. 다행히 여벌 체육복이 있는 학생이 이들에게 체육복을 빌려주려 합니다. 학생들의 번호는 체격 순으로 매겨져 있어, 바로 앞번호의 학생이나 바로 뒷번호의 학생에게만 체육복을 빌려줄 수 있습니다. 예를 들어, 4번 학생은 3번 학생이나 5번 학생에게만 체육복을 빌려줄 수 있습니다. 체육복이 없으면 수업을 들을 수 없기 때문에 체육복을 적절히 빌려 최대한 많은 학생이 체육수업을 들어야 합니다.
 전체 학생의 수 n, 체육복을 도난당한 학생들의 번호가 담긴 배열 lost, 여벌의 체육복을 가져온 학생들의 번호가 담긴 배열 reserve가 매개변수로 주어질 때, 체육수업을 들을 수 있는 학생의 최댓값을 return 하도록 solution 함수를 작성해주세요.

 

제한 사항

1. 전체 학생의 수는 2명 이상 30명 이하입니다.
2. 체육복을 도난당한 학생의 수는 1명 이상 n명 이하이고 중복되는 번호는 없습니다.
3. 여벌의 체육복을 가져온 학생의 수는 1명 이상 n명 이하이고 중복되는 번호는 없습니다.
4. 여벌 체육복이 있는 학생만 다른 학생에게 체육복을 빌려줄 수 있습니다.
5. 여벌 체육복을 가져온 학생이 체육복을 도난당했을 수 있습니다. 이때 이 학생은 체육복을 하나만 도난당했다고 가정하며, 남은 체육복이 하나이기에 다른 학생에게는 체육복을 빌려줄 수 없습니다.

 

 풀이

 우선 체육복의 개수가 담긴 리스트를 만든다. 예를 들어 2,4번 학생이 도난당했고 1,3,5번 학생이 여벌 체육복을 가지고 있다면 [2,0,2,0,2]가 만들어질 것이다. 이때, 체육복이 0인 학생은 왼쪽 학생 또는 오른쪽 학생에게 체육복을 받을 수 있는데, 둘 중 누구한테 먼저 받을지 고민해보아야 한다. 우리는 리스트의 왼쪽에서 오른쪽으로 0인 학생을 차례로 조사하고 있으므로 왼쪽 학생을 먼저 보아야 한다. 왜냐하면 왼쪽학생에게 빌릴 수 있음에도 오른쪽 학생에게 빌린다면 그보다 더 오른쪽에 있던 0인 학생이 체육복을 못 빌릴 수도 있기 때문이다. 따라서 0인 학생 기준으로 왼쪽에 2가 있으면 빌리고 없으면 오른쪽 학생이 2인지 확인하는 알고리즘을 구현하면 된다.

def solution(n, lost, reserve):
# 체육복 개수 리스트 만들기
	l = [1]*n
    for i in lost:
        l[i-1] = 0
    for i in reserve: # 도난당한 학생도 여벌 체육복이 있을 수 있으므로 2벌이다가 아니라 +1해줌..!
        l[i-1] += 1
# 체육복 빌려주기
    if l[0]==0 and l[1]==2: #index에러 방지를 위해 양 끝을 따로 처리
        l[0] = 1
        l[1] = 1
    if l[-1]==0 and l[-2]==2:
        l[-1] = 1
        l[-2] = 1
    for i in range(1,len(l)-1):
        if l[i]==0:
            if l[i-1]==2:
                l[i-1]=1
                l[i]=1
            elif l[i+1]==2:
                l[i+1]=1
                l[i]=1
    cnt = 0
    for i in l:
        if i !=0:
            cnt+=1
    return cnt

집합(set) 사용한 다른 풀이도 있다. set_lost만 신경쓰면 되니 코드가 한결 간결해졌다.

def solution(n, lost, reserve):
    set_reserve = set(reserve) - set(lost)
    set_lost = set(lost) - set(reserve)
    for i in set_reserve:
        if i-1 in set_lost:
            set_lost.remove(i-1)
        elif i+1 in set_lost:
            set_lost.remove(i+1)
    return n-len(set_lost)

 

정렬

정렬 알고리즘은 파이썬 내장함수인 sort로 간단히 구현 가능하다. 하지만 알고리즘 문제에서는 여러 변수 중 무엇을 기준으로 정렬해야 하는지 결정해야되는 경우가 많기 때문에 정렬 알고리즘의 구현보다는 정렬 알고리즘을 사용하는 방법에 초점을 맞춰 학습할 필요가 있다. 다음 예시 문항을 통해 정렬 알고리즘의 쓰임을 익혀보자.

 

 문제 설명

 0 또는 양의 정수가 주어졌을 때, 정수를 이어 붙여 만들 수 있는 가장 큰 수를 알아내 주세요. 예를 들어, 주어진 정수가 [6, 10, 2]라면 [6102, 6210, 1062, 1026, 2610, 2106]를 만들 수 있고, 이중 가장 큰 수는 6210입니다. 0 또는 양의 정수가 담긴 배열 numbers가 매개변수로 주어질 때, 순서를 재배치하여 만들 수 있는 가장 큰 수를 문자열로 바꾸어 return 하도록 solution 함수를 작성해주세요.

 

 제한 사항

1. numbers의 길이는 1 이상 100,000 이하입니다.
2. numbers의 원소는 0 이상 1,000 이하입니다.
3. 정답이 너무 클 수 있으니 문자열로 바꾸어 return 합니다.

 

 풀이

우선 리스트 내의 숫자들의 순서는 바뀌지 않는다는 사실을 고려해야 한다. 또한 자릿수가 다른 숫자들에 대하여 어떤 수를 왼쪽으로 보내야 큰지 생각해보아야 한다. 예를 들어 34와 343이 있을 때 34343과 34334 중 34343이 더 크므로 34가 더 왼쪽에 와야 한다. 이 문제는 한가지 아이디어가 필요한데.. 원소들이 1000이하의 수라는 점에서 착안해서 해당 원소를 4번반복하고 앞에서부터 네번째 숫자까지를 비교하는 것이다. 예를 들면 2와 25는 2222와 2525를 비교하여 25가 더 크다고 판단할 수 있고 72와 728은 7272와 7287를 비교하여 728이 더 크다고 판단할 수 있는 것이다. 같은 부분을 제외하고 그 다음 자릿수부터 다시 비교하므로 이런 구현이 가능한 것인데, 한번 생각해보면 알 수 있을 것이다.

 

def solution(numbers):
    num = sorted(numbers, key = lambda x : (str(x)*4)[:4], reverse = True)
    num = map(str, num)
    answer = "".join(num)
    if answer[0] =="0": #리스트 내의 원소가 2개 이상의 0으로 이루어져 있으면 조인한 결과가 아닌'0'을 출력하도록 한다
        return '0'
    else:
        return answer