우아한 테크코스 5기 백엔드 프리코스를 진행하게 되었다. 1주차 미션의 문제들을 풀어보고 풀이과정을 정리하였다.
이 글은 다양한 풀이중 하나로, 더 나은 방법이 많이 있을겁니다. 참고만 부탁드립니다!
java-onboarding Repository
GitHub - JeongHunHui/java-onboarding at JeongHunHui
포비와 크롱이 페이지 번호가 1부터 시작되는 400 페이지의 책을 주웠다. 책을 살펴보니 왼쪽 페이지는 홀수, 오른쪽 페이지는 짝수 번호이고 모든 페이지에는 번호가 적혀있었다. 책이 마음에 든 포비와 크롱은 페이지 번호 게임을 통해 게임에서 이긴 사람이 책을 갖기로 한다. 페이지 번호 게임의 규칙은 아래와 같다.
포비와 크롱이 펼친 페이지가 들어있는 리스트/배열 pobi와 crong이 주어질 때, 포비가 이긴다면 1, 크롱이 이긴다면 2, 무승부는 0, 예외사항은 -1로 return 하도록 solution 메서드를 완성하라.
pobi | crong | result |
---|---|---|
[97, 98] | [197, 198] | 0 |
[131, 132] | [211, 212] | 1 |
[99, 102] | [211, 212] | -1 |
package onboarding;
import java.util.ArrayList;
import java.util.List;
class Problem1 {
static final int MIN_PAGE = 1;
static final int MAX_PAGE = 400;
public static int solution(List<Integer> pobi, List<Integer> crong) {
// ---------- 예외 상황 확인 ----------
// 각 List에 요소가 2개인지 확인
if(pobi.size() != 2 || crong.size() != 2) return getException("리스트의 요소가 2가 아닙니다.");
int pobiLeft = pobi.get(0);
int pobiRight = pobi.get(1);
int crongLeft = crong.get(0);
int crongRight = crong.get(1);
// 페이지가 1~400 인지 확인
if(!isInRange(pobiLeft) || !isInRange(pobiRight) || !isInRange(crongLeft)
|| !isInRange(crongRight)) return getException("페이지의 범위가 1~400이 아닙니다.");;
// 왼쪽 페이지 번호가 홀수인지 확인
if(pobiLeft % 2 != 1 || crongLeft % 2 != 1) return getException("왼쪽 페이지가 홀수가 아닙니다.");;
// 오른쪽 페이지가 왼쪽페이지 다음 페이지인지 확인
if(pobiRight != pobiLeft + 1 || crongRight != crongLeft + 1) return getException("오른쪽 페이지가 왼쪽의 다음 페이지가 아닙니다.");;
// --------------------------------
// 포비와 크롱의 점수 구하기
int pobiScore = Integer.max(getMaxScore(pobiLeft), getMaxScore(pobiRight));
int crongScore = Integer.max(getMaxScore(crongLeft), getMaxScore(crongRight));
System.out.println("pobi: " + pobiScore + " crong: " + crongScore);
// 점수가 같으면 무승부
if(pobiScore == crongScore) return 0;
if(pobiScore > crongScore) return 1;
else return 2;
}
// 예외 상황 발생시 메세지 출력 후 -1 반환
static int getException(String message){
System.out.println(message);
return -1;
}
// 페이지가 범위 내에 있는지(시작이나 마지막면 제외)
static boolean isInRange(int value) {
return value > MIN_PAGE && value < MAX_PAGE;
}
// 페이지를 넣으면 각 자리의 합과 곱중에 큰 수를 반환한다.
static int getMaxScore(Integer page) {
List<Integer> newList = new ArrayList<>();
int num = page;
while (num > 0) {
newList.add(num % 10);
num /= 10;
}
int sum = 0;
int duplecate = 1;
for(int i : newList) {
sum += i;
duplecate *= i;
}
return sum >= duplecate ? sum : duplecate;
}
}
일단 예외 상황들을 체크해서 -1을 반환하는 코드를 먼저 작성하였다.
예외 체크가 완료된 상태에서 각 페이지의 점수를 구하는 코드를 작성하였다.
점수를 구하는 방식은 페이지 변수(정수)를 받아서 각 자리수를 리스트에 저장한 뒤에 각 자리수의 합과 곱을 구한 뒤 둘 중 큰 수를 반환하고, 이렇게 구한 왼쪽과 오른쪽 페이지의 점수를 비교하여 더 높은 점수를 pobi와 crong의 점수로 하였다.
그렇게 작성하니 코드의 중복이 너무 많아져서 범위 내에 있는지 체크하는 메소드를 따로 만들고, 점수를 구하는 메소드도 원래 코드에서 분리하였다.
마지막으로 구한 점수를 바탕으로 결과값을 반환하였다.
암호문을 좋아하는 괴짜 개발자 브라운이 이번에는 중복 문자를 이용한 새로운 암호를 만들었다. 예를 들어 "browoanoommnaon"이라는 암호문은 다음과 같은 순서로 해독할 수 있다.
임의의 문자열 cryptogram이 매개변수로 주어질 때, 연속하는 중복 문자들을 삭제한 결과를 return 하도록 solution 메서드를 완성하라.
cryptogram | result |
---|---|
"browoanoommnaon" | "brown" |
"zyelleyz" | "" |
package onboarding;
public class Problem2 {
public static String solution(String cryptogram) {
boolean isChange = true;
String newCytogram = cryptogram;
// 더이상 중복되는 문자가 없을 때 까지 반복
while (isChange) {
isChange = false;
char[] charArray = newCytogram.toCharArray();
char overlapChar = ' ';
for(int i = 0; i < charArray.length; i++) {
// 이전 문자와 중복되면 '_'로 변환
if(charArray[i] == overlapChar) {
charArray[i] = '_';
charArray[i - 1] = '_';
isChange = true;
}
// 중복되지 않으면 overlapChar에 새로운 문자를 넣음
else overlapChar = charArray[i];
}
// '_' 로 변환했던 문자들 제거
newCytogram = String.valueOf(charArray).replace("_", "");
System.out.println(newCytogram);
}
return newCytogram;
}
}
더 효율적인 방법이 있을 것 같긴 하지만 일단 당장 떠오르는 방법으로 했다.
문자열의 각 문자를 순회하며 이전 문자와 겹치면 ‘_’로 변환시키고, 한번 순회가 끝나면 ‘_’를 없애고 변경 사항이 없을 때 까지 반복한다.
배달이가 좋아하는 369게임을 하고자 한다. 놀이법은 1부터 숫자를 하나씩 대면서, 3, 6, 9가 들어가는 숫자는 숫자를 말하는 대신 3, 6, 9의 개수만큼 손뼉을 쳐야 한다.
숫자 number가 매개변수로 주어질 때, 1부터 number까지 손뼉을 몇 번 쳐야 하는지 횟수를 return 하도록 solution 메서드를 완성하라.
number | result |
---|---|
13 | 4 |
33 | 14 |
package onboarding;
import java.util.ArrayList;
import java.util.List;
public class Problem3 {
public static int solution(int number) {
// 1~10000 의 범위를 벗어나는지 체크
if(number < 1 || number > 10000) return -1;
int count = 0;
// 각 숫자를 돌면서 369의 개수를 체크 후 count에 합한다.
for (int i = 1; i <= number; i++) {
count += get369Count(i);
}
return count;
}
// 정수를 넣으면 해당 정수의 3,6,9의 개수를 반환하는 함수
static int get369Count(int number) {
List<Integer> newList = new ArrayList<Integer>();
int count = 0;
// 정수의 각 자리수를 newList에 저장
while (number > 0) {
newList.add(number % 10);
number /= 10;
}
// 각 자리수를 돌며 3,6,9에 해당하는 수가 있으면 count 증가
for (int i : newList) {
if(i % 3 == 0 && i != 0) count++;
}
return count;
}
}
우선 제약사항을 체크하고, 1부터 입력받은 number 까지 반복문을 돌린다.
가독성을 위해서 369의 개수를 구하는 메소드를 따로 만들었다.
369의 개수를 구하는 방법은, 정수의 각 자리수를 저장하는 리스트를 만들어서 거기에 각 자리수를 넣고, 각 자리수가 3의 배수인지 판별해서 3의 배수이면 count를 올렸다.
어느 연못에 엄마 말씀을 좀처럼 듣지 않는 청개구리가 살고 있었다. 청개구리는 엄마가 하는 말은 무엇이든 반대로 말하였다.
엄마 말씀 word가 매개변수로 주어질 때, 아래 청개구리 사전을 참고해 반대로 변환하여 return 하도록 solution 메서드를 완성하라.
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 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Z | Y | X | W | V | U | T | S | R | Q | P | O | N | M | L | K | J | I | H | G | F | E | D | C | B | A |
word | result |
---|---|
"I love you" | "R olev blf" |
package onboarding;
public class Problem4 {
public static String solution(String word) {
String str = "";
for (char c : word.toCharArray()) {
str += getOppositeWord(c);
}
return str;
}
// 아스키 코드를 이용하여 반대되는 알파뱃을 반환하는 함수
static char getOppositeWord(char c) {
// A~Z 65~90
// a~z 97~122
// 문자를 아스키 코드로 변환
int asciiCode = (int)c;
// 알파뱃 대문자인 경우 반대되는 알파뱃으로 변환
if(asciiCode >= 65 && asciiCode <= 90) {
return (char)(155 - asciiCode);
}
// 알파뱃 소문자인 경우 반대되는 알파뱃으로 변환
else if (asciiCode >= 97 && asciiCode <= 122) {
return (char)(219 - asciiCode);
}
// 알파뱃이 아닌 경우 그대로 반환
return c;
}
}
처음에는 어떻게 해야되나 고민 했었는데, 아스키코드의 존재가 떠올랐다.
아스키 코드를 활용하여 반대되는 알파뱃으로 바꾸는 메소드를 만들었다. 만약 알파뱃이 아니면 입력값을 그대로 반환한다.
그래서 문자열의 각 문자를 메소드를 사용하여 변경시켜주었다.
계좌에 들어있는 돈 일부를 은행에서 출금하고자 한다. 돈 담을 지갑이 최대한 가볍도록 큰 금액의 화폐 위주로 받는다.
돈의 액수 money가 매개변수로 주어질 때, 오만 원권, 만 원권, 오천 원권, 천 원권, 오백원 동전, 백원 동전, 오십원 동전, 십원 동전, 일원 동전 각 몇 개로 변환되는지 금액이 큰 순서대로 리스트/배열에 담아 return 하도록 solution 메서드를 완성하라.
money | result |
---|---|
50237 | [1, 0, 0, 0, 0, 2, 0, 3, 7] |
15000 | [0, 1, 1, 0, 0, 0, 0, 0, 0] |
package onboarding;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Problem5 {
public static List<Integer> solution(int money) {
// 결과를 저장할 리스트
List<Integer> answer = new ArrayList<>();
// 화폐 금액을 담은 배열
int[] moneyArray = {50000, 10000, 5000, 1000, 500, 100, 50, 10, 1};
// 큰 금액의 화폐 순서대로 바꾼뒤 남는 금액을 저장할 변수
int leftMoney = money;
for (int i = 0; i < moneyArray.length; i++) {
// 남은 금액이 바꾸려는 화폐의 금액이상이라면
if(leftMoney >= moneyArray[i]) {
// 남은 금액을 화폐의 금액으로 나눈 몫을 결과 리스트에 저장
answer.add(leftMoney/moneyArray[i]);
// 화폐의 금액으로 나눈뒤 나머지를 남은 금액에 저장
leftMoney = leftMoney % moneyArray[i];
}
// 남은 금액이 바꾸려는 화폐의 금액보다 낮다면 0을 결과 리스트에 저장
else answer.add(0);
}
return answer;
}
}
우선 화폐의 금액을 담은 배열을 하나 만들었다.
그리고 각 화폐의 금액으로 money를 나눠서 그 몫을 결과리스트에 저장한다.
그리고 나머지를 남은 금액에 저장하고 이를 반복한다.
우아한테크코스에서는 교육생(이하 크루) 간 소통 시 닉네임을 사용한다. 간혹 비슷한 닉네임을 정하는 경우가 있는데, 이러할 경우 소통할 때 혼란을 불러일으킬 수 있다.
혼란을 막기 위해 크루들의 닉네임 중 같은 글자가 연속적으로 포함 될 경우 해당 닉네임 사용을 제한하려 한다. 이를 위해 같은 글자가 연속적으로 포함되는 닉네임을 신청한 크루들에게 알려주는 시스템을 만들려고 한다.
신청받은 닉네임 중 같은 글자가 연속적으로 포함 되는 닉네임을 작성한 지원자의 이메일 목록을 return 하도록 solution 메서드를 완성하라.
email.com
도메인으로만 제한한다.forms | result |
---|---|
[ ["jm@email.com", "제이엠"], ["jason@email.com", "제이슨"], ["woniee@email.com", "워니"], ["mj@email.com", "엠제이"], ["nowm@email.com", "이제엠"] ] | ["jason@email.com", "jm@email.com", "mj@email.com"] |
package onboarding;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class Problem6 {
public static List<String> solution(List<List<String>> forms) {
if(forms.size() < 1 || forms.size() > 10000) {
System.out.println("크루는 1명 이상 10000명 이하입니다.");
return null;
}
Pattern emailPattern = Pattern.compile("^[A-Za-z0-9]+(.[_A-Za-z0-9-]+)*@email[.]com");
Pattern nicknamePattern = Pattern.compile("^[ㄱ-ㅎ가-힣]+(.[ㄱ-ㅎ가-힣]+)*");
for (List<String> data : forms) {
if(!emailPattern.matcher(data.get(0)).matches()) {
System.out.println("올바르지 않은 이메일 형식입니다.");
return null;
}
if(!nicknamePattern.matcher(data.get(1)).matches()) {
System.out.println("닉네임은 한글만 가능합니다.");
return null;
}
if(data.get(0).length() > 20) {
System.out.println("이메일의 길이가 20자 이상입니다.");
return null;
}
if(data.get(1).length() > 20) {
System.out.println("닉네임의 길이가 20자 이상입니다.");
return null;
}
}
List<String> emailList = new ArrayList<>();
for (int i = 0; i < forms.size(); i++) {
// 기준 문자열
String str = forms.get(i).get(1);
for (int j = 0; j < forms.size(); j++) {
// 기준 문자열과 동일한 경우 스킵
if(i == j) continue;
// 검사 대상 문자열
List<String> jStringList = forms.get(j);
for (int k = 0; k < str.length()-1; k++) {
// 기준 문자열내에서 2글자씩 빼서 검사
String checkStr = str.substring(k, k+2);
// checkStr가 검사하는 문자열에 들어있으면 해당 닉네임의 이메일을 emailList에 추가
if(jStringList.get(1).contains(checkStr)) {
emailList.add(jStringList.get(0));
}
}
}
}
// 중복 제거
List<String> resultList = emailList.stream().distinct().collect(Collectors.toList());
// 오름차순 정렬
resultList.sort(Comparator.naturalOrder());
return resultList;
}
}
사실 제한사항을 코드안에서 다 체크를 해줘야 하는지는 잘 모르겠지만, 정규식도 공부해볼겸 그냥 추가하였다.
일단 이메일 같은 경우는 ID부분은 알파뱃 대소문자와 숫자가 들어가고, 최소 1글자는 있어야 하므로 [A-Za-z0-9]+(.[_A-Za-z0-9-]+)*
이렇게 했고, 이메일 부분은 @와 email.com 고정이므로 @email[.]com
이렇게 했다.
닉네임 같은 경우는 1글자이상, 한글만 들어가기 때문에 [ㄱ-ㅎ가-힣]+(.[ㄱ-ㅎ가-힣]+)*
이렇게 해주었다.
작성한 정규식 Pattern으로 이메일과 닉네임, 그리고 글자수도 한번 체크를 해주었다.
그리고 이제 닉네임 중복을 검사해야하는데, 더 효율적인 방법이 없나 찾아보았지만, 일단 내 생각대로 만들었다.
기준 문자열을 하나 정하고, 검사 대상 문자열에 기준 문자열의 0~1, 1~2 … n~n+1 이 포함 되어있는지를 판단해서 중복 검사를 진행하였다.
예를 들면, 기준 문자열이 “정훈희”이고, 검사 대상 문자열이 “훈희정”이라면 “정훈”, “훈희” 가 “훈희정”에 포함되어있는지 검사하는 식이다.
그렇게 검사를 끝내고, 결과 리스트의 중복을 제거하고 오름차순 정렬을 해주었다.
레벨 2의 팀 프로젝트 미션으로 SNS(Social Networking Service)를 만들고자 하는 팀이 있다. 팀에 속한 크루 중 평소 알고리즘에 관심이 많은 미스터코는 친구 추천 알고리즘을 구현하고자 아래와 같은 규칙을 세웠다.
사용자 아이디 user와 친구 관계 정보 friends, 사용자 타임 라인 방문 기록 visitors가 매개변수로 주어질 때, 미스터코의 친구 추천 규칙에 따라 점수가 가장 높은 순으로 정렬하여 최대 5명을 return 하도록 solution 메서드를 완성하라. 이때 추천 점수가 0점인 경우 추천하지 않으며, 추천 점수가 같은 경우는 이름순으로 정렬한다.
user | friends | visitors | result |
---|---|---|---|
"mrko" | [ ["donut", "andole"], ["donut", "jun"], ["donut", "mrko"], ["shakevan", "andole"], ["shakevan", "jun"], ["shakevan", "mrko"] ] | ["bedi", "bedi", "donut", "bedi", "shakevan"] | ["andole", "jun", "bedi"] |
package onboarding;
import java.util.*;
public class Problem7 {
// 친구 추천 점수를 저장할 딕셔너리
static Map<String, Integer> friendScoreDict = new HashMap<>();
public static List<String> solution(String user, List<List<String>> friends, List<String> visitors) {
// 사용자의 친구를 저장할 리스트
List<String> userFriends = new ArrayList<>();
// friends 에서 사용자와 친구인 유저를 리스트에 저장
for (List<String> friendRelations : friends) {
if(friendRelations.get(0) == user) userFriends.add(friendRelations.get(1));
else if(friendRelations.get(1) == user) userFriends.add(friendRelations.get(0));
}
// 이미 친구가 아닌 사용자 중에 친구가 겹치면 10점
for (List<String> friendRelations : friends) {
String userA = friendRelations.get(0);
String userB = friendRelations.get(1);
if(userA == user || userB == user) continue;
if(userFriends.contains(userA) && !userFriends.contains(userB)) {
putScoreDict(userB, 10);
}
else if(userFriends.contains(userB) && !userFriends.contains(userA)) {
putScoreDict(userA, 10);
}
}
// 방문한 사람의 점수를 딕셔너리에 저장 및 업데이트
for (String visitor : visitors) {
// 이미 친구라면 스킵
if(userFriends.contains(visitor)) continue;
putScoreDict(visitor, 1);
}
// 정렬을 위해서 List 형태로 Map을 가져옴
List<Map.Entry<String, Integer>> entryList = new LinkedList<>(friendScoreDict.entrySet());
// 이름순으로 정렬, 점수순으로 내림차순 정렬
entryList.sort(Map.Entry.comparingByKey());
entryList.sort(Map.Entry.comparingByValue(Comparator.reverseOrder()));
// 최대 반복 횟수
int count = 5;
List<String> resultList = new ArrayList<>();
// 점수 순대로 정렬된 Map에서 이름을 하나씩 리스트에 저장. (최대 5번 반복)
for(Map.Entry<String, Integer> entry : entryList) {
resultList.add(entry.getKey());
count--;
if(count <= 0) break;
}
return resultList;
}
// 점수 딕셔너리에 이미 있는 유저면 score업데이트, 없으면 새로 추가
static void putScoreDict(String name, int score) {
if(friendScoreDict.containsKey(name))
score += friendScoreDict.get(name);
friendScoreDict.put(name, score);
}
}
일단 추천 점수를 저장하기 위해 이름을 key로 가지고 점수를 value로 가지는 Map을 하나 만들었다.
또한, 이미 친구인 사람은 점수를 계산할 필요가 없기에, 친구의 이름을 저장하는 리스트를 하나 만들고, friends 리스트를 돌면서 친구인 사람의 이름을 저장하였다.
일단 사용자와 함께아는 친구를 알아내기 위해서 friends를 돌면서 user가 포함이 안되면서 둘중 한명만 user의 친구인 관계를 찾아서 user의 친구가 아닌 쪽에게 점수를 주었다.
예를 들면 “정훈희”가 user이고, “정훈희” 와 “홍길동”이 친구이고 [“홍길동”, “전우치”] 라는 값이 들어오면, 두 요소중 “정훈희”가 있는지 확인하고, 두 요소중 한명만 이미 만들어둔 userFriends리스트에 포함되어있는지 확인한다. 여기선 “홍길동”이 친구 리스트에 있으므로 friendScoreDict에 key로 “전우치”를 넣고, value에 10을 추가한다.
방문객은 1점을 주므로 visitors 배열을 돌면서 userFriends에 없는 사람만 friendScoreDict에 추가하였다.
이렇게 하니 Map에 값을 넣을때 키가 있는지 확인하고 있으면 기존 값에 더해서 주고 없으면 새로운 키벨류를 넣고 하니까 중복되는 코드가 생겨서 putScoreDict라는 메소드를 따로 만들어서 분리하였다.
이렇게 점수 계산을 하고, friendScoreDict을 value 값을 기준으로 내림차순으로 정렬하고, 점수가 같으면 이름순으로 오름차순 정렬을 해야한다.
정렬을 하기위해서 Map을 List<Map.Entry<String, Integer>> entryList = new LinkedList<>(friendScoreDict.entrySet());
이렇게 List형태로 바꿔주었다.
그리고 키와 값을 기준으로 각각 정렬을 해준 뒤에 최대 5개 까지만 결과 리스트에 저장해주었다.
자바로 이러한 코딩문제를 풀어보는 것은 처음인데, 문제을 풀어보면서 몰랐던 부분을 자연스럽게 공부하게 되어서 정말 좋았다. 다른 사람의 코드를 참고하지 않고 혼자서 풀어서 다른사람의 코드를 보면서 내 코드와 비교해봐야 할 것 같다.