우아한테크코스

우아한테크코스 - 프리코스 1주차 회고

aoaa 2022. 11. 2. 01:16

 저번주 수요일에 시작한 프리코스 1주차가 마무리되었습니다. 엠바고 느낌으로 글을 안쓰고있다가 1차 미션 제출시간이 지나서 리뷰를 작성해봤습니다.

 

 1주차 미션은 요구사항에 맞게 값을 출력하는 코딩테스트 형식의 문제였습니다. 느낀 바로는 특정 알고리즘 구현보다 데이터를 어떤 자료구조를 이용하여 취급하는지를 확인하는 미션처럼 느꼈습니다. 문제는 총 7문제이며, 특출나게 어려웠던 문제는 없었습니다. 작성했던 코드를 리뷰해보겠습니다.

 작성한 코드는 여기서 확인할 수 있습니다.

https://github.com/Voyager003/java-onboarding/tree/Voyager003

 

GitHub - Voyager003/java-onboarding: 온보딩 미션을 진행하는 저장소

온보딩 미션을 진행하는 저장소. Contribute to Voyager003/java-onboarding development by creating an account on GitHub.

github.com


1. Problem1

 첫 문제답게 가벼운 문제였습니다. 두 플레이어가 책을 펼쳐서 양 쪽 페이지의 합, 곱 중에 큰 값을 점수로 하고 이를 비교하여 점수를 비교하여 숫자를 출력하는 문제입니다.

import java.util.List;


/**
 * 기능 구현 목록
 * 1) 페이지 번호를 자릿수 별로 곱해서 수를 만드는 기능 구현
 * 2) 예외 상황처리 구현
 */
class Problem1 {
    public static int solution(List<Integer>pobi, List<Integer>crong) {
        int answer = Integer.MAX_VALUE;

        if (!FindException(pobi.get(0),pobi.get(1)) || !FindException(crong.get(0),crong.get(1))) {
            answer = -1;
        }else {
            int pobiscore = Math.max(Score(pobi.get(0)),Score(pobi.get(1)));
            int crongscore = Math.max(Score(crong.get(0)),Score(crong.get(1)));
            if (pobiscore > crongscore) {
                answer = 1;
            } else if (pobiscore == crongscore){
                answer = 0;
            } else{
                answer = 2;
            }
        }
        return answer;
    }

    private static boolean FindException(intpage1, intpage2){
        boolean b = (page1<=1 ||page1>=399 ||page1!=page2-1) ? false : true;
        return b;
    }
    private static int Score(intidx){
        int sum = 0;
        int mul = 1;

        while(idx!=0){
            sum +=idx%10;
            mul *=idx%10;
	    idx/= 10;
        }
        return Math.max(sum, mul);
    }
}

 문제를 보고 구현할 목록을 정리했는데

  1. pobi와 crong의 페이지 수를 더하거나, 곱해서 가장 큰 수를 반환하는 기능 (public static int Score())
  2. 예외상황 : - 시작면이나 마지막면(1, 2 or 399, 400)이 나오면 -1, 0 index가 1 index 보다 1작지 않다면 -1 (FindException())

크게 두 기능 구현으로 분류했습니다. Score는 각 플레이어가 펼친 페이지의 자릿수를 합하거나 곱하여, 그 중 큰 값을 return하는 메서드입니다. 처음에  mul=0으로 설정하여서 무슨 값을 넣어도 0이 되었는데 이를 1로 수정하여 오류를 수정했습니다. 그리고 FIndException()에서 예외 처리를 하는 부분에서는 if else문으로 길어져서 3항 연산자를 이용해 코드의 수를 줄였습니다.

가볍게 테스트 케이스 통과


2. Problem2

 Problem2는 임의의 문자열이 매개변수로 주어지면 연속하는 중복 문자를 삭제하는 문제였습니다. 

예를 들어, "abbcddcff"라면 "a"가 나올 수 있도록 중복을 제거하는 문제입니다.

/**
 * 기능 구현 목록
 * 1) 중복을 제거하는 메서드 구현(Stack 이용)
 * 2) 구현된 값을 answer에 추가하는 기능 구현
 */
public class Problem2 {
    public static String solution(String cryptogram) {
        StringBuilder answer = new StringBuilder();
        for(char result : DeleteDuplication(cryptogram)){
            answer.append(result);
        }
        return answer.toString();
    }
    public static Stack<Character> DeleteDuplication(String str) {
        Stack <Character> stack = new Stack<>();

        for(char c : str.toCharArray()){
            if(!stack.isEmpty()){
                if(stack.peek()== c){
                    stack.pop();
                }else{
                    stack.push(c);
                }
            }
            else{
                stack.push(c);
            }
        }
        return stack;
    }
}

 문제를 보고 바로 떠오른 것은 Stack 자료구조였습니다. 인자로 받은 문자열을 Stack에 하나씩 넣은 뒤(push), stack의 맨 위에 쌓인 peek값과 현재 들어온 문자열을 비교하고 같으면 stack에서 pop해준다면 중복된 문자열을 제거할 수 있을거라고 판단했습니다.

 그 후에 Stack에 남은 값을 answer에 담아주면 중복이 제거된 문자열을 담을 수 있게 되겠죠. 테스트 케이스 통과.


3. Problem3

 Problem3는 369게임 문제였습니다. 1부터 시작하여 입력받은 숫자까지 3, 6, 9의 개수만큼 손뼉을 몇 번 쳐야하는지 횟수를 return하는 문제였습니다. 처음 생각한 것은 1부터 숫자를 증가시키면서 증가하는 숫자를 인자로 넘겨 박수 횟수를 카운트하는 메서드를 하나 구현해야 겠다고 생각했습니다. 

/**
 * 구현 기능 목록
 * 1. number를 인자로 받아 박수 cnt를 하는 기능 구현
 */
public class Problem3 {
    public static int solution(int number) {
        int answer = 0;
        for(int i=1; i<=number; i++){
            answer+= CountClap(i);
        }
        return answer;
    }
    static final List<Integer> threesixnine = List.of(3, 6, 9);
    public static int CountClap(int num){
        int cnt = 0;
        while(true){
            if(num<=0){
                return cnt;
            }
            if(threesixnine.contains(num%10)){
                cnt++;
            }
            num = num/10;
        }
    }
}

 먼저 계속 사용해야할 숫자인 3, 6, 9를 static final로 List에 담아주고, 1부터 number까지의 int형 데이터를 CountClap()에 인자로 넘겼습니다. 인자로 받은 num을 10으로 나눈 나머지에 3, 6, 9가 포함되어 있다면, cnt를 증가시키고 num은 다시 10으로 나눠서 0이 될때까지 반복하도록 만들었습니다.

 예를 들어 33이 num으로 입력된다면 33 % 10 = 3이기 때문에 cnt를 1 증가시키게 되고, 33 = 33/ 10 =3이 되기 때문에 반복문을 한번 더 실행하게 됩니다. 3 % 10 = 3이기때문에 cnt를 1증가시키고, num = 0이되어 반복문을 종료하고 그 cnt 횟수를 return하게 됩니다. 

 테스트 통과.


4. Problem4

 Problem4는 알파벳이 입력되면 이를 역순으로 출력하게 만드는 문제였습니다. A가 입력되면 Z를, B가 입력되면 Y를 출력하도록 말이죠

처음에 생각한 것은 정규표현식을 이용해 해당 문자를 replace하도록 설계하는 것이었습니다. 하지만 알파벳 26개(대소문자 합은 52개)에 대입하기에는 크다고 생각했고, 이전에 풀었던 문제를 떠올려 Character class를 이용하도록 했습니다. 

/**
 * 구현 기능 목록
 * 1. 문자열을 받아 역순으로 바꿔주는 기능
 * 2. result에 치환한 문자열을 담고 반환
 */
public class Problem4 {
    static final int lowera = 'a';
    static final int lowerz = 'z';
    static final int upperA = 'A';
    static final int upperZ = 'Z';

    public static String solution(String word) {
        return ReverseWord(word).toString();
    }
    public static StringBuilder ReverseWord(String words) {
        StringBuilder result = new StringBuilder();
        for (int i=0; i<words.length(); i++) {
            int charcasting = words.charAt(i);
            if (charcasting>lowera && charcasting<lowerz) {
                charcasting = lowerz-(charcasting-lowera);
            } else if (charcasting>upperA&&charcasting<upperZ) {
                charcasting = upperZ-(charcasting-upperA);
            }
            result.append((char) charcasting);
        }
        return result;
    }
}

먼저 알파벳을 대, 소문자를 구분해서 변환하도록 UpperA,Z와 Lowera,z를 static final 변수로 지정했습니다. 그 후에

문자열을 인자로 받고 한 문자씩 꺼내어 대, 소문자를 구분한 뒤 반환하도록 만들었습니다.

 예를 들어 word가 "Test"라면, 첫 글자인 T를 받아 대, 소문자를 구분(if문)한 뒤에 charcasting = 'Z'(90) - ('T' (84)- 'A'(65)) = 'G'(71)로 변환되게 됩니다. 이렇게 되면 다른 char형을 변환하지 않고 알파벳만을 변환할 수 있게됩니다. 테스트 통과.


5. Problem5 

 Problem5는 int형 값인 money가 주어지면 5만원부터 10000원 ... 1원까지 얼마로 변환될 수 있는지, 금액이 큰 순서대로 list에 담는 문제였습니다. 문제를 보자마자 최적의 해를 찾아내는 Greedy 알고리즘을 떠올렸고 바로 구현했습니다.

import java.util.*;

/**
 * 구현 기능 목록
 * 1. greedy를 이용해 최적의 해를 찾도록 함
 * 2. 돈의 액수를 나열한 배열을 생성한다. {50000, 10000 … 10, 1}
 * 3. 돈의 액수를 담은 배열을 순회하면서, 변환된 개수를 list에 담는다.
 * 4. list를 반환한다.
 */
public class Problem5 {
    public static List<Integer> solution(int money) {
        List <Integer> result = new ArrayList<>();
        List <Integer> amount = List.of(50000, 10000, 5000, 1000, 500, 100 ,50, 10, 1);

        for(int i=0; i<amount.size(); i++){
            result.add(money / amount.get(i));
            money = money%amount.get(i);
        }
        return result;
    }
}

 먼저 돈의 액수를 나열한 list를 생성하고, 돈의 액수를 담은 배열을 순회하면서 변환된 개수를 list에 담도록 했습니다. 특별히 고민한 문제가 아니였기 때문에 바로 제출하고 테스트 케이스에 통과했습니다.


6. Problem6 

 Problem6부터 문제가 살짝 난이도가 올라간 것을 느꼈습니다. List에 담긴 닉네임에 중복이 있다면(두 글자 이상 연속적된다면 중복), 그 닉네임을 작성한 지원자의 이메일을 모은 목록을 return하는 문제였습니다. 문제를 보고 Hashmap을 이용하여 데이터를 넣고 접근하는 방식을 생각했습니다. 그래서 정리한 요구사항은

1) 닉네임을 확인하면서 Hashmap의 key에 두 글자씩(중복) 잘라낸 닉네임과, 그 잘라낸 닉네임이 사용된 횟수를 저장

2) 데이터를 저장한 map에서 2번 이상 사용된 경우, 그 이메일을 추가한 뒤

3) 오름차순으로 정렬하여 return 하도록 만든다.

였습니다.

import java.util.*;

/**
 * 1. map을 생성하고, key에 두 글짜씩 substring한 닉네임, value에 사용된 횟수를 저장한다.
 * 2. 이 때, map에서 key값(두 글짜씩 substring한 닉네임)이 2번 이상 사용되었고, email에 포함되지 않았다면 그 이메일을 추가한다.
 * 3. 이메일에 해당하는 부분의 문자열을 오름차순으로 정렬한다.
 */
public class Problem6 {
    public static List<String> solution(List<List<String>> forms) {
        List<String> result = new ArrayList<>();
        Map<String, Integer> duplication = new HashMap<>();

        for(List<String> list : forms){
            String nickname = list.get(1);
            for(int i = 0; i < nickname.length()-1; i++){
                String strcase = nickname.substring(i, i+2);
                duplication.put(strcase, duplication.getOrDefault(strcase, 0) +1);
            }
        }

        for(List<String> list: forms) {
            String email = list.get(0);
            String nickname = list.get(1);
            for(int i = 0; i < nickname.length()-1; i++){
                String strcase = nickname.substring(i, i+2);
                if(duplication.get(strcase) >= 2 && !result.contains(email)){
                    result.add(email);
                }
            }
        }
        Collections.sort(result);
        return result;
    }
}

 이메일과 닉네임이 담긴 forms에서 닉네임의 두 글자씩 substring하여 strcase에 담았습니다. 이 strcase를 map의 key에 담고, getOrDefault를 사용해 이미 값이 있다면 value값을 +1해주도록 하여 두 번 이상 사용된 닉네임을 찾도록 했습니다. 그렇다면 결과는

 [ ["jm@email.com", "제이엠"], ["jason@email.com", "제이슨"], ["woniee@email.com", "워니"], ["mj@email.com", "엠제이"], ["nowm@email.com", "이제엠"] ] 으로 주어진 데이터를 duplication이라는 map에 [["제이", 3], ["이엠", 1], ["이슨", 1], ["워니", 1], ["엠제", 1], ["이제", 1], ["제엠", 1]]이라는 데이터를 담을 수 있습니다. 

 map에 담은 데이터에서 value가 2이상이라면, 자른 문자열이 포함된 이메일을 오름차순으로 return하도록 했습니다.

데이터를 어떻게 처리할지에 대한 고민이 좀 길었고, 막상 구현하는 것은 시간이 별로 소요되지 않았던 문제입니다. 테스트 통과.


7. Problem7

 대망의 7번문제입니다. 친구 추천 알고리즘을 구현하는 문제였는데, 처음에는 주어진 user의 친구의 친구라면 10점을 부여한다는 것이 문제가 잘 이해되지않아 다른 분들의 의견을 슬랙에서 참고했습니다. 문제를 이해를 하고나서 처음 정리한 요구사항은 

1) 먼저 user와 친구인 목록을 만들어준다.

2) frineds를 순회하면서 map의 key에 친구의 닉네임을 담고, value에 10점을 부여

3) visitors도 마찬가지로 map의 key에 친구의 닉네임을 담고, value에 10점을 부여

4) map에서 이미 친구인 목록을 remove해주고

5) key값을 빼서 오름차순으로 정렬한다.

위와 같이 정리했습니다.

import java.util.*;

/**
 * 구현 기능 목록
 * 1. 이미 friends인 사람을 set에 담는다.
 * 2. friends를 탐색하면서, map의 key에 친구, value에 추천점수 +10을 한다.
 * 3. visitors를 탐색하면서, map의 key에 친구, value에 추천점수 +1을 한다.
 * 4. 친구들과 점수가 담긴 map에서 이미 친구인 목록을 제거한다.
 * 5. 결과인 result에 추천친구를 담는다.
 */
public class Problem7 {
    public static List<String> solution(String user, List<List<String>> friends, List<String> visitors) {
        List<String> result = new LinkedList<String>();
        Map<String, Integer> map = new HashMap<>();

        // 이미 친구인 목록을 만든다. [shakevan, donut]
        HashSet<String> alreadyfriends = new HashSet<>();
        for(int i=0; i<friends.size(); i++){
            if(friends.get(i).get(0)==user){
                alreadyfriends.add(friends.get(i).get(1));

            }else if(friends.get(i).get(1)==user){
                alreadyfriends.add(friends.get(i).get(0));
            }
        }

        for(List<String> list :friends){
            String s1 = list.get(0);
            String s2 = list.get(1);
            if(alreadyfriends.contains(s1) && !s2.contains(user)){
                map.put(s2, map.getOrDefault(s2, 0)+10);
            }else if(!s1.contains(user) && alreadyfriends.contains(s1)){
                map.put(s1, map.getOrDefault(s1, 0)+10);
            }
        }

        for(int i=0; i<visitors.size(); i++){
            map.put(visitors.get(i), map.getOrDefault(visitors.get(i), 0)+1);
        } // {andole=20, jun=20, shakevan=11, bedi=3, donut=11}

        for(int i=0; i< map.size(); i++){
            for(String key : alreadyfriends){
                if(map.containsKey(key)){
                    map.remove(key);
                }
            }
        }

        for(String key : map.keySet()){
            result.add(key);
        }
        return result;
    }
}

 처음 완성하고 테스트를 통과한 코드입니다. 하지만 문제가 있었는데 추천 점수가 같으면 정렬하는 조건을 구현하지 않았다는 점과 map에 닉네임을 넣을 때, 이미 친구인 목록을 넣지않도록하면 코드를 더 짧게 짤 수 있다고 생각했습니다. 그래서 리팩토링한 결과는

/**
 * 구현 기능 목록
 * 1. 이미 friends인 사람을 set에 담는다.
 * 2. friends를 탐색하면서, map의 key에 친구, value에 추천점수 +10을 한다.
 * 3. visitors를 탐색하면서, map의 key에 친구, value에 추천점수 +1을 한다.
 * 4. 결과를 요구사항에 맞게 정렬하고 result에 추가한 뒤 return한다.
 */
public class Problem7 {
    public static List<String> solution(String user, List<List<String>> friends, List<String> visitors) {
        List<String> result = new LinkedList<String>();
        Map<String, Integer> map = new HashMap<>();

        HashSet<String> alreadyfriends = new HashSet<>();
        for(int i=0; i<friends.size(); i++){
            if(friends.get(i).get(0)==user){
                alreadyfriends.add(friends.get(i).get(1));

            }else if(friends.get(i).get(1)==user){
                alreadyfriends.add(friends.get(i).get(0));
            }
        }

        for(List<String> list :friends){
            String s1 = list.get(0);
            String s2 = list.get(1);
            if(alreadyfriends.contains(s1) && !s2.contains(user)){
                map.put(s2, map.getOrDefault(s2, 0)+10);
            }else if(alreadyfriends.contains(s2) && !s1.contains(user)){
                map.put(s1, map.getOrDefault(s1, 0)+10);
            }
        }

        for(int i=0; i<visitors.size(); i++){
            if(!alreadyfriends.contains(visitors.get(i))){
                map.put(visitors.get(i), map.getOrDefault(visitors.get(i), 0)+1);
            }
        }
        List<Map.Entry<String, Integer>> list = SortList(map);
        for(Map.Entry<String, Integer> entry : list){
            result.add(entry.getKey());
        }
        return result;
    }
    private static List<Map.Entry<String, Integer>> SortList (Map<String, Integer> m){
        List<Map.Entry<String, Integer>> list = new LinkedList<>(m.entrySet());
        list.sort((o1, o2) ->{
            int comparison = (o1.getValue() - o2.getValue()) * -1;
            return comparison == 0 ? o1.getKey().compareTo(o2.getKey()) : comparison;
        });

        if(list.size()>5){
            list.subList(0, 5);
        }
        return list;
    }
}

 이미 친구인 목록을 담은 alreadyfriends라는 Hashset을 먼저 만들어줬습니다. 이후 frineds와 visitors를 순회하면서 이미 친구인 목록은 map에 더하지 않도록 점수를 부여하게 하여 첫 코드와 달리 불필요한 코드를 제거했습니다. 그 후 정렬을 하는 메서드인 Sortlist()를 만들었는데, 이 과정은 이전 글인 compare에서 자세하게 담았습니다. 조건에 맞도록 정렬한 뒤, 최대 5명까지 추천을 받도록 자른 뒤에 key값을 return하도록 했습니다. 1주차 미션을 통틀어 시간을 가장 많이 투자한 문제였습니다.

 

 미션 전체적으로 크게 어려운 문제는 없었지만, 제한사항을 확인해가며 리팩토링 해가는 과정이 재밌어서 시간가는 줄 몰랐고, 얻어가는 것도 많았습니다. 1주차부터 얻어가는 것이 많았는데 이어지는 프리코스에서는 얼마나 성장할지 기대되고, 이 기대감이 좋은 결과로 이어졌으면 좋겠네요. 화이팅!