알고리즘 문제풀이

[2021 Dev-Matching: 웹 백엔드 개발자(상반기)] 코딩 테스트 JAVA SQL 풀이

오렌지색 귤 2021. 12. 28. 00:32
반응형

1번. 로또의 최고 순위와 최저 순위

문제 바로가기

 

코딩테스트 연습 - 로또의 최고 순위와 최저 순위

로또 6/45(이하 '로또'로 표기)는 1부터 45까지의 숫자 중 6개를 찍어서 맞히는 대표적인 복권입니다. 아래는 로또의 순위를 정하는 방식입니다. 1 순위 당첨 내용 1 6개 번호가 모두 일치 2 5개 번호

programmers.co.kr

간단한 문제 설명

  • 입력 값 : lottos (int 배열), win_nums (int 배열)
  • 출력 값 : 최고 순위와 최저 순위를 담은 int 배열

 

  • 문제 규칙
    • 민우가 구매한 로또 번호인 lottos 배열의 일부 숫자는 알아볼 수 없을 지도 모른다.
    • 알아볼 수 없는 경우 0으로 표기한다.
    • 순서는 상관 없다.

 

  • 제한 사항
    • 입력 값은 모두 길이가 6인 정수 배열이다.
    • lottos의 0을 제외하고는 두 배열에 1 이상 45 이하의 중복되지 않은 정수만 존재한다.

접근

첫 아이디어

따로 시간 제한이 존재하지 않는 문제였기 때문에 정렬 + 브루트포스로 쉽게 풀이 가능할 것이라 생각했다.
반복문에 list.contains() 메소드를 이용해서 일치하는 정수의 개수를 세면 된다.

두번째 아이디어

시간 제한이 없는 문제인 만큼 객체 지향적으로 문제를 풀면 좋을 것이라 생각했다.
자바8 스트림을 최대한 이용해서 코드의 가독성을 높이고 변수명도 최대한 의미를 가지도록 작성했다.

다만, 스트림에서 지역변수 값을 수정할 수 없어서 0개 또는 1개를 맞춘 경우 6등을 배열에 넣어주는 과정은 for 반복문 내부의 if 조건문 분기로 해결했다.
또한, else 예약어 사용을 하지 않기 위해 continue를 활용했다.

코드

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class Solution1 {

    public static final int MISSING_NUMBER = 0;
    public static final int BOTTOM_RANK = 6;
    public static final int WINNING_THRESHOLD = 1;
    public static final int DEFAULT_VALUE = 7;

    public int[] solution(int[] lottos, int[] win_nums) {

        // 알아볼 수 없는 숫자 세기
        int missingNumberCount = (int)Arrays.stream(lottos)
            .filter(num -> num == MISSING_NUMBER)
            .count();

        // 알아볼 수 있는 숫자는 따로 모아 리스트 생성
        List<Integer> myLottoNumber = Arrays.stream(lottos).sorted()
            .filter(num -> num != MISSING_NUMBER)
            .boxed().collect(Collectors.toList());

        // win_nums와 myLottoNumber의 원소를 비교하며 일치하는 개수 세기
        int minimumCorrectCount = (int)Arrays.stream(win_nums)
            .filter(num -> myLottoNumber.stream().anyMatch(Predicate.isEqual(num)))
            .count();

        return MinimumMaximumRank(missingNumberCount, minimumCorrectCount);
    }

    private int[] MinimumMaximumRank(int missingNumberCount, int minimumCorrectCount) {
        int maximumCorrectCount = missingNumberCount + minimumCorrectCount;  // 알아볼 수 없는 숫자가 모두 일치하면 최대 일치 개수

        List<Integer> ranks = new ArrayList<>();
        for (Integer count : Arrays.asList(minimumCorrectCount, maximumCorrectCount)) {
            if (count <= WINNING_THRESHOLD) {
                ranks.add(BOTTOM_RANK);
                continue;
            }
            ranks.add(DEFAULT_VALUE - count);
        }

        return ranks.stream().sorted().mapToInt(Integer::intValue).toArray();
    }
}

 

2번. 행렬 테두리 회전하기

문제 바로가기

 

코딩테스트 연습 - 행렬 테두리 회전하기

6 6 [[2,2,5,4],[3,3,6,6],[5,1,6,3]] [8, 10, 25] 3 3 [[1,1,2,2],[1,2,2,3],[2,1,3,2],[2,2,3,3]] [1, 1, 5, 3]

programmers.co.kr

간단한 문제 설명

  • 입력 값 : rows (int), columns (int), queries (int[][])
  • 출력 값 : 매번 회전한 테두리의 최소 값 배열 (int 배열)

 

  • 문제 규칙
    • 주어진 행과 열에 대한 matrix가 가로 방향으로 숫자가 1부터 하나씩 증가하면서 적혀있다.
    • queries의 원소는 (x1, y1, x2, y2)인 정수 4개로 표현된다.
    • x1 행 y1 열부터 x2 행 y2 열까지의 영역에 해당하는 직사각형에서 테두리에 있는 숫자들을 한 칸씩 시계방향으로 회전한다.
    • 매 query마다 회전하는 테두리 중 가장 작은 숫자를 순서대로 배열에 담아 반환한다.

 

  • 제한 사항
    • 행, 열을 이루는 수는 2 이상 100 이하의 자연수이다.
    • 회전의 개수는 1 이상 10,000 이하이다.
    • 모든 회전은 순서대로 이루어지며, 1 <= x1 < x2 <= rows , 1 <= y1 < y2 <= columns 이다.

접근

아이디어

클린 코드로 짤 수 없을 것 같은 문제라고 생각했다.

이 문제에서 필요한 이중 for문을 스트림으로 표현하면 오히려 가독성을 해칠 것 같아 이중 for문을 사용했다.

첫번째 칸이 0행 0열이 아닌 1행 1열으로 시작하므로 matrix 크기는 1씩 크게 생성했다.

아래 그림의 왼쪽은 query의 원소에 따라 border 리스트에 저장되는 테두리 값이 저장되는 로직을 표현한 것이다.

오른쪽은 border 리스트에서 값을 꺼내 matrix에 저장함으로써 회전한 효과를 주는 로직을 표현한 것이다.

고찰

다른 사람의 풀이를 봐도 더 객체 지향적인 풀이는 존재하지 않았지만, 토요일에 있을 스터디까지 좀 더 생각해 봐야겠다.

코드

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Solution {

    public int[] solution(int rows, int columns, int[][] queries) {
        int[][] matrix = new int[rows + 1][columns + 1];
        List<Integer> minimumNumbers = new ArrayList<>();

    // 행렬 초기화
        for (int i = 1; i < rows + 1; i++) {
            for (int j = 1; j < columns + 1; j++) {
                matrix[i][j] = (i - 1) * columns + j;
            }
        }

    // queries의 원소를 순회하며 테두리를 회전하고, 그 중 최소값은 minimumNumbers에 더한다
        Arrays.stream(queries)
                .forEachOrdered(query -> minimumNumbers.add(getMinimumBorder(query, matrix)));

        return minimumNumbers.stream().mapToInt(Integer::intValue).toArray();
    }

    // 테두리를 리스트에 담고, 회전한 다음 최소값을 반환하는 메소드
    private Integer getMinimumBorder(int[] query, int[][] matrix) {
        List<Integer> border = getBorder(query[0], query[1], query[2], query[3], matrix);
        return rotateBorder(query[0], query[1], query[2], query[3], border, matrix);
    }

    // 테두리를 리스트에 담아 반환하는 메소드
    private List<Integer> getBorder(int row1, int col1, int row2, int col2, int[][] matrix) {
        List<Integer> border = new ArrayList<>();

        // 상단
        for (int col_index = col1; col_index < col2; col_index++) {
            border.add(matrix[row1][col_index]);
        }

        // 우측
        for (int row_index = row1; row_index < row2; row_index++) {
            border.add(matrix[row_index][col2]);
        }

        // 하단
        for (int col_index = col2; col_index > col1; col_index--) {
            border.add(matrix[row2][col_index]);
        }

        // 좌측
        for (int row_index = row2; row_index > row1; row_index--) {
            border.add(matrix[row_index][col1]);
        }

        return border;
    }

    // 테두리를 회전하고 그 중 최소값을 반환하는 메소드
    private Integer rotateBorder(int row1, int col1, int row2, int col2, List<Integer> border, int[][] matrix) {
        int list_index = 0;

        // 상단
        for (int col_index = col1 + 1; col_index <= col2; col_index++) {
            matrix[row1][col_index] = border.get(list_index++);
        }

        // 우측
        for (int row_index = row1 + 1; row_index <= row2; row_index++) {
            matrix[row_index][col2] = border.get(list_index++);
        }

        // 하단
        for (int col_index = col2 - 1; col_index >= col1; col_index--) {
            matrix[row2][col_index] = border.get(list_index++);
        }

        // 좌측
        for (int row_index = row2 - 1; row_index >= row1; row_index--) {
            matrix[row_index][col1] = border.get(list_index++);
        }

        return Collections.min(border);
    }
}

 

3번. 다단계 칫솔 판매

문제 바로가기

 

코딩테스트 연습 - 다단계 칫솔 판매

민호는 다단계 조직을 이용하여 칫솔을 판매하고 있습니다. 판매원이 칫솔을 판매하면 그 이익이 피라미드 조직을 타고 조금씩 분배되는 형태의 판매망입니다. 어느정도 판매가 이루어진 후,

programmers.co.kr

간단한 문제 설명

  • 입력 값 : enroll (int 배열), referral (int 배열), seller (int 배열), amount (int 배열)
  • 출력 값 : result (int 배열)

 

  • 문제 규칙
    • 판매원은 자신이 번 돈의 90%를 가지며, 나머지 10%는 자신의 추천인에게 준다.
    • 추천으로 획득한 돈 또한 90%를 가지고, 나머지 10%는 자신의 추천인에게 준다.
    • 추천인이 존재하지 않으면 자신이 모든 돈을 가진다.
    • 돈이 1원 미만이면 본인이 다 가진다.
    • 10%에 해당하는 금액에 소수점이 존재하면 내림을 하고 추천인에게 주며, 나머지는 본인이 갖는다.
    • 칫솔 한개를 판매하여 얻어지는 이익은 100원이다.

 

  • 제한 사항
    • enrollreferral의 길이는 1 이상 10,000 이하이다.
      • 누구의 추천도 받지 않은 경우 referral에는 - 값이 들어있다.
      • enroll은 회사에 가입한 순서를 지키므로 특정 사람은 enroll에 나오고 나서 referral에 나올 수 있다.
    • selleramount의 길이는 1 이상 100,000 이하이다.
      • seller에는 중복된 이름이 존재할 수 있다.
      • amount 원소들의 범위는 1 이상 100 이하인 자연수이다.
    • 모든 구성원들의 이름은 10글자 이내의 영문 알파벳 소문자로만 이루어져 있다.

접근

첫 아이디어

  • 등록인과 추천인 관계로 이루어진 Map
  • 판매자와 판매 수량 관계로 이루어진 Map

위의 두 Map을 활용해 추천인을 거슬러 올라가는 재귀 함수를 만들었다.

이 아이디어로 문제를 풀었지만, 맵을 초기화하거나 값을 저장 또는 불러오는 코드가 복잡하여 가독성이 떨어졌다.

두번째 아이디어

Member 내부 클래스를 생성하고, 자체 메소드인 distributeMoney()를 통해 재귀 함수를 구성했다.

객체 지향적인 코드를 작성할 수 있었고, 가독성 또한 훨씬 높아졌다.

고민

두번째 풀이에서 배열의 크기만큼 인덱스를 순회하는 for문 대신 stream을 사용했는데, 오히려 가독성이 있는 코드는 for문을 활용하는 것이 아닌가 하는 생각이 들었다.

코드

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.IntStream;

public class Solution {

    static class Member {
        String name;
        Member recommender;
        int reward;

        public Member(String name, Member recommender, int reward) {
            this.name = name;
            this.recommender = recommender;
            this.reward = reward;
        }

        // 10%는 recommender에게 주고 나머지는 본인이 갖는 재귀 함수
        public void distributeMoney(int givenMoney) {
            int moneyForRecommender = (int)(givenMoney * 0.1);
            this.reward += givenMoney - moneyForRecommender;
            if (this.recommender != null) {
                this.recommender.distributeMoney(moneyForRecommender);
            }
        }
    }

    public int[] solution(String[] enroll, String[] referral, String[] seller, int[] amount) {
        Map<String, Member> memberMap = new HashMap<>();
        // <회원 이름, 멤버 객체> 맵 생성
        Arrays.stream(enroll)
            .forEach(name -> memberMap.put(name, new Member(name, null, 0)));

        // 추천인이 "-" 인 경우를 제외하고 각각의 멤버들의 추천인을 설정
        IntStream.range(0, enroll.length)
            .filter(index -> !referral[index].equals("-"))
            .forEach(index -> memberMap.get(enroll[index]).recommender = memberMap.get(referral[index]));

        // 모든 판매 내역에 대해서 돈을 분배
        IntStream.range(0, seller.length)
            .forEach(index -> memberMap.get(seller[index]).distributeMoney(100 * amount[index]));

        return Arrays.stream(enroll)
            .map(memberMap::get)
            .map(member -> member.reward)
            .mapToInt(Integer::intValue)
            .toArray();
    }
}

 

4번. 헤비 유저가 소유한 장소

문제 바로가기

 

코딩테스트 연습 - 헤비 유저가 소유한 장소

PLACES 테이블은 공간 임대 서비스에 등록된 공간의 정보를 담은 테이블입니다. PLACES 테이블의 구조는 다음과 같으며 ID, NAME, HOST_ID는 각각 공간의 아이디, 이름, 공간을 소유한 유저의 아이디를

programmers.co.kr

간단한 문제 설명

  • 입력 값 : PLACES 테이블
    • COLUMN
      • ID : 공간의 아이디 INT 타입
      • NAME : 공간의 이름 VARCHAR 타입
      • HOST_ID : 유저의 아이디 INT 타입
  • 출력 값 : 헤비 유저가 등록한 공간의 정보를 ID 순으로 조회

 

  • 문제 규칙 : 공간을 둘 이상 사용하는 유저를 헤비 유저라 부른다.

접근

WHERE 절에 2개 이상의 공간을 사용하는 헤비 유저를 포함하는 다중행 서브쿼리를 만들고, 해당 쿼리안에 존재하는 HOST_ID 값에 해당하는 필드를 가져온다.
마지막에는 ID 값으로 정렬했다.

코드

SELECT *
FROM PLACES
WHERE HOST_ID IN (
    SELECT HOST_ID
    FROM PLACES
    GROUP BY HOST_ID
    HAVING COUNT(*) >= 2
    )
ORDER BY ID;

 

반응형