관리 메뉴

개발공부기록

Java - 직사각형 별찍기, 최대공약수와 최소 공배수, 3진법 뒤집기, 이상한 문자 만들기 / SQL(MySQL) - 오랜기간 보호한 동물(1), 카테고리 별 도서 판매량 집계하기 본문

기타 개발 공부/온라인 코딩 테스트 회고

Java - 직사각형 별찍기, 최대공약수와 최소 공배수, 3진법 뒤집기, 이상한 문자 만들기 / SQL(MySQL) - 오랜기간 보호한 동물(1), 카테고리 별 도서 판매량 집계하기

소소한나구리 2025. 3. 11. 14:20
728x90

Java

양수의 개수와 덧셈

문제

제한조건

  • n, m은 각각 1000 이하인 자연수임

입출력 예시

나의 풀이

import java.util.Scanner;

class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int a = sc.nextInt();
        int b = sc.nextInt();
        
        for (int i = 0; i < b; i++) {
            for (int j = 0; j < a; j++) {
                System.out.print("*");
            }
            System.out.println();   
        }    
    }
}
  • 2중 반복문을 활요하는 정석 풀이

다른 풀이

import java.util.Scanner;
import java.util.stream.IntStream;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int a = sc.nextInt();
        int b = sc.nextInt();

        StringBuilder sb = new StringBuilder();
        IntStream.range(0, a).forEach(s -> sb.append("*"));
        IntStream.range(0, b).forEach(s -> System.out.println(sb.toString()));
    }
}
  • 이런 단순한 풀이도 Stream API를 활용한 방법이 신박해서 가져왔다
  • IntStream.range(0, a)을 사용하면 0부터 a-1까지의 범위 숫자가 생성되어 0, 1, 2, 3, 4가 생성된다
  • 그 이후 forEach(s -> sb.append("*");각 요소의 개수만큼 반복해주면 생성된 0, 1, 2, 3, 4의 개수 즉, 5번 만큼 반복하게 되고, 그 만큼 생성해준 Stringbuilder에 "*"을 추가해주면 "*"을 "*****"이 StringBuilder에 담기게 된다
  • 그 다음 동일한 방법으로 StringBuilde를 toString()으로 문자열로 변환하여 range(0, b)에서 생성한 수의 개수만큼 반복해주면 된다

최대공약수(GCD), 최소공배수(LCM)

문제

  • 프로그래머스 - https://school.programmers.co.kr/learn/courses/30/lessons/12940
  • 입력받은 두 수의 최대공약수와 최소공배수를 반환하는 함수를 완성
  • 배열의 맨 앞에 최대공약수, 그다음 최소공배수를 넣어 반환
  • 3, 12의 최대공약수는 3, 최소공배수는 12이므로 [3, 12]를 반환하면 됨

제한조건

  • 두 수는 1 ~ 1,000,000의 자연수임

입출력 예시

다른 풀이

사실상 못 풀었음, 공식을 몰라서 무작정 반복문과 조건문을 통해 구해보고자 하니 코드가 너무 비효율 적으로 된 것 같아서 중간에 포기하고 이런 문제는 공식을 보고 구현하는 것이 맞다고 생각하여 다양한 풀이를 해석해보고자 함

class Solution {
    public int[] solution(int n, int m) {
        int[] answer = new int[2];
        
        // 최대 공약수 구하기
        for (int i = 1; i <= Math.min(n, m); i++) {
            if (n % i == 0 && m % i == 0) {
                answer[0] = i;
            }
        }
        
        // 최소 공배수 구하기
        answer[1] = (n * m) / answer[0];
        
        return answer;
    }
}

최대공약수 구하기

  • 입력 받은 두 수 중에 작은 값까지 반복하도록 Math.min(n, m)을 사용하여 반복 횟수를 제한한다
  • 공약수는 두 수를 모두 나누어 떨어지게 하는 수인데 어떤 수가 두 수 중에 작은 값보다 크다면 애초에 공약수가 될 수 없으므로 작은 값까지 반복하면 된다
  • 예를 들어 n이 3이고 m이 10인 경우 어떤 수인 a가 1, 2, 3 까지는 의미가 있지만 4로 나누는 것은 작은 수 3에 대해서는 공약수가 될 수 없기 때문에 의미가 없게 된다
  • 즉, 최대공약수를 구할 때는 작은 값보다 큰 수 초과되는 수로는 나눌 필요가 없기 때문에 불필요한 반복문을 줄일 수 있다
  • 반복문 내에서 1부터 작은 수까지 1씩 증가하는 수를 n과 m을 나누었을 때 나누어 떨어지면 answer[0]에 저장하도록 두 수의 공약수를 저장하게 되고 이때 가장 마지막에 저장되는 공약수가 최대공약수이다

최소공배수 구하기

  • 최소공배수는 두 수의 배수 중에서 공통으로 나타나는 가장 작은 양의 정수이다.
  • 예를 들어 4의 배수는 4, 8, 12, 16 24 ... 이고 6의 배수는 6, 12, 18 ,24 ...인데 두 수의 공통 배수는 12, 24 ... 중에서 가장 작은 12이가 최소공배수가 된다
  • 임의의 두 수의 곱은 항상 공통 배수가 되는데 최소공배수는 아니기 때문에 곱한 값에 두 수의 최대공약수로 나누어 주게 되면 최소공배수를 구할 수 있다.
  • 즉, 두 수의 곱을 최대공약수로 나누면 최소공배수가 된다는 공식을 활용하면되기 때문에 최대공약수만 알면 최소공배수는 구하기 쉽다.
class Solution {
    public int[] solution(int n, int m) {
        int gcd = getGCD(n, m);          // 재귀 함수를 통해 최대공약수를 구함
        int lcm = (n * m) / gcd;         // 두 수의 곱을 최대공약수로 나누면 최소공배수가 됨
        return new int[] {gcd, lcm};
    }
    
    // 유클리드 호제법을 이용한 재귀 함수
    private int getGCD(int a, int b) {
        if(b == 0) {
            return a;
        }
        return getGCD(b, a % b);
    }
}
  • 재귀를 활용하여 유클리드 호제법이라는 것을 사용하면 간결하게 최대공약수를 구할 수 있다고 하는데 나는 현단계에선 이부분을 활용하기에는 한참 어려울 것 같아서 풀이 설명으로 이해를 하고자 했는데
    • 유클리드 호제법을 활용하여 최대공약수를 구하는 getGCD(int a, int b) 메서드는 두 수를 나눈 나머지를 0이 될 때까지 계속 나누고 나머지 연산의 결과가 0이 되면 그 이전의 나눗셈의 대상이 되었던 수를 반환하여 최대공약수를 구한다.
  • 최소공배수는 공식에 따라 동일하게 구한다

3진법 뒤집기

문제

제한조건

  • n은 1 ~ 100,000,000 인 자연수임

입출력 예시

나의 풀이

class Solution {
    public int solution(int n) {
        StringBuilder revBase3 = new StringBuilder();
        while (n > 0) {
            revBase3.append(n % 3);
            n /= 3;
        }

        char[] charArr = revBase3.toString().toCharArray();
        int answer = 0;
        int digit = charArr.length-1;
        for (char c : charArr) {
            if (c == '0') {
                digit--;
                continue;
            }
            answer += (c - '0') * (int) Math.pow(3, digit);
            digit--;
        }
        return answer;
    }
}
  • 우선 입력받은 수를 3진법으로 분할하여 저장하려면 문자열에 저장하는 것이 좋다고 생각하여 가변 문자열인 StringBuilder를 사용하여 반복문으로 입력받은 수가 3으로 나누었을 때 0이 될 때까지 3으로 나눈 나머지를 StringBuilder에 저장했다.
  • 이렇게 저장하다보니 자연스럽게 3진수의 역순으로 문자열에 들어가게 되어 중간단계를 건너뛰게 되었다
  • 이후 문자열 3진수를 10진수로 변환하는 방법이 도저히 떠오르지 않아서 Stream API를 사용해보려다가 원초적으로 문자 배열로 변화하여 계산하기로 결정했다.
  • 3진법의 각 자리수를 digit이라고 선언하고 문자배열의 길이 -1로 선언하여 3진수의 자리수를 표현하였는데, -1을 한 이유는 3진수의 첫 번째 자리수가 3의 제곱이기 때문이다.
  • 이 후에 반복문은 문자 배열의 0번째 인덱스 부터 순회하기 때문에 문자 배열의 요소가 '0'이라면 건너뛴 다음 digit-- 연산을 해주었다
  • '0'이 아니라면 Math.pow(3, digit)을 사용하여 3진수의 제곱을 구하고, 문자 배열의 값을 '0'으로 빼서 3진수의 자리수를 구한다음 이 둘을 곱해서 answer 누적 저장하는 방식으로 10진수를 구했다.

다른 풀이

class Solution {
    public int solution(int n) {
        StringBuilder sb = new StringBuilder();
        while(n > 0){
            sb.append(n % 3);
            n /= 3;
        }
        return Integer.parseInt(sb.toString(), 3);
    }
}
  • 다른건 볼 필요없이 Integer.parserInt()를 사용하면 편하게 3진수로 변환할 수 있다는걸 잊고 있었다...
  • 첫 번째 인자에 문자열, 두 번째 인자에 변환하고자 하는 진수를 입력해주면 내부적으로 최적화된 로직을 통해 3진법으로 변환하기 때문에 오히려 더 안정적이다.
  • 지금은 StringBuilder.append() 메서드로 바로 3진법을 뒤집어서 반환했지만 제대로된 3진법을 반환하고자 한다면 sb.insert(0, n % 3)으로 입력하면 올바른 3진법을 나타낼 수 있다.

이상한 문자 만들기

문제

  • 프로그래머스 - https://school.programmers.co.kr/learn/courses/30/lessons/12930
  • 한 개 이상의 단어로 구성되어 있는 문자열은 하나 이상의 공백문자로 구분되어 있음
  • 각 단어의 짝수번째 알파벳은 대문자로, 홀수번째 알파벳은 소문자로 바꾼 문자열을 리턴하는 함수를 완성

제한조건

  • 문자열 전체의 짝/홀수 인덱스가 아닌 단어(공백을 기준) 별로 짝/홀수 인덱스를 판단해야 함
  • 첫 번째 글자는 0번째 인덱스로 보아 짝수번째 알파벳으로 처리해야 함

입출력 예시

나의 풀이

class Solution {
    public String solution(String s) {
        String[] strSplit = s.split(" ", -1);
        StringBuilder answer = new StringBuilder();

        for (int i = 0; i < strSplit.length; i++) {
            StringBuilder sb = new StringBuilder(strSplit[i]);
            StringBuilder sb2 = new StringBuilder();

            for (int j = 0; j < sb.length(); j++) {
                if (j % 2 == 0) {
                    sb2.append(sb.substring(j, j+1).toUpperCase());
                } else {
                    sb2.append(sb.substring(j, j+1).toLowerCase());
                }
            }
            if (i == strSplit.length -1) {
                answer.append(sb2);
            } else {
                answer.append(sb2).append(" ");
            }
        }
        return answer.toString();
    }

}
  • 문자열을 다루는 것이기 때문에 모두 StringBuilder를 사용했다
  • 처음 주어진 문자열 s를 split(" ", -1)로 공백을 기준으로 단어와 빈 문자열(공백 위치)을 모두 보존한 문자열 배열로 나누었다
    • 여기서 마지막에 계속 테스트가 통과 안되어 검색을 했었는데, 2번째 인자(limit)에 -1을 입력해 주어야 테스트가 제대로 통과된다는 것을 알았다
    • 만약 -1 없이 split(" ")만 사용한다면 split 대상이 되는 문자열의 마지막에 공백이 있다면 이 마지막 공백을 포함하지 않고 문자열을 분리하기 때문에 로직을 수행한 이후 문자열을 복원할 때 원래 제공된 문자열과 길이가 달라질 수 있다
    • 두 번째 인자(limit)에 -1을 입력해 주면 빈 문자열 토큰까지 모든 결과를 반환하기 때문에 다시 문자열을 합칠 때 제공된 문자열과 동일한 구조로 문자열을 합칠 수 있다
    • 예를 들어 제공된 문자열이 a이후 공백, b 이후 공백 2번 c 이후 공백 일때 ("a_b__c_", 공백을 구분하기 쉽게 _로 표현) split(" ")로만 하면 "a", "b", "", "c" 가 되어 마지막 공백이 사라지지만 split(" ", -1)로 하면 마지막 공백이 제거되지 않고 유지 되어 "a", "b", "", "c", ""가 문자열 배열에 저장된다.
    • 즉, split()을 사용할 때 트레일링 토큰(trailing token, 문자열 끝에 위치한 빈 문자열 토큰)을 제거하고 싶다면 split(" ")을 사용하면 되고 원본 상태를 유지하고자 트레일링 토큰을 유지하고자 할 때는 split(" ", -1)을 해주면 된다
  • 그 이후에 요구사항에 따른 로직을 구현하기 위해 나누어진 각 문자열 배열을 별도의 StringBuilder로 변환하고 반복문을 통해서 substring(j, j+1)로 나뉘어진 각 문자열 토큰의 단어들을 조건문에 따라 대문자와 소문자로 변환시키고 새로운 StringBuilder에 저장시켰다
  • 변환 작업이 진행되는 반복문이 종료되면 새로운 StringBuilder에 변환된 StringBuilder를 추가하는데, 이때  split(" ", -1)로 인해 날라갔던 공백을 붙이기 위해서 중간의 단어들의 변환이 끝나면 " " 을 붙이고 맨 마지막의 단어는 " "을 붙이지 않도록 로직을 구성했다

다른 풀이

class Solution {
  public String solution(String s) {

        String answer = "";
        int cnt = 0;
        String[] array = s.split("");

        for(String ss : array) {
            cnt = ss.contains(" ") ? 0 : cnt + 1;
            answer += cnt%2 == 0 ? ss.toLowerCase() : ss.toUpperCase(); 
        }
      return answer;
  }
}
  • 문자열을 그대로 사용했지만 split("")을 사용하여 입력 문자열을 한 글자씩 배열로 분리하고 카운터인 int cnt = 0; 변수를 활용해 분리한 문자 배열에 " "(공백)이 있으면 cnt 변수의 값을 0으로 초기화 시켜서 다음 단어를 첫 번째 단어로 간주하도록 로직을 구성했다
  • 3항 연산자를 통해 간단히 로직을 구성한 것이 인상 깊었고 나처럼 " "로 분리시키는게 아니라 문자열을 통째로 문자열 배열로 분리하여 count 변수와 " " 빈 문자열을 통해 각 단어를 구분한 것이 인상 깊었다.
  • 여기서 문자열이 아닌 StringBuilder를 사용하는 것으로 변경한다면 성능도 올라가는 완벽한 코드가 될 것 같다
class Solution {
    public String solution(String s) {
        char[] arr = s.toCharArray();
        int index = 0;
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == ' ') {
                index = 0;
            } else {
                arr[i] = (index % 2 == 0) ? Character.toUpperCase(arr[i])
                                          : Character.toLowerCase(arr[i]);
                index++;
            }
        }
        return new String(arr);
    }
}
  • 주어진 문자열을 애초에 문자 배열로 변환해서 문제를 해결하는 방식도 존재했는데 성능상 이게 제일 빠르고 코드도 매우 직관적이라고 가장 좋은 접근법이라고 생각되었다
  • 마찬가지로 문자열을 통채로 문자 배열에 담았기 때문에 각 단어를 구분하기 위해 index라는 변수를 두어 배열의 요소가 ' '이면 0으로 초기화 시키는 로직을 두었다
  • 여기서 Character 래퍼클래스에 toUpperCase, upLowerCase가 있는지 몰랐는데 이 문제를 통해 이 함수도 기억하게 될 것 같다

 


SQL(MySQL)

오랜 기간 보호한 동물(1)

문제 및 테이블 예시

  • 동물 보호소에 들어온 동물의 정보를 담은 ANIMAL_INS 테이블과 동물 보호소에서 입략 보낸 동물의 정보를 담은 ANIMAL_OUTS 테이블이 존재할 때 아직 입양을 못 간 동물 중, 가장 오래 보호소에 있었던 동물 3마리의 이름과 보호 시작일을 조회하는 SQL문을 작성
  • 결과는 보호 시작일 순으로 조회

 

입출력 예시

나의 풀이

SELECT 
    INS.NAME, INS.DATETIME
FROM ANIMAL_INS AS INS
LEFT JOIN ANIMAL_OUTS AS OUTS
ON INS.ANIMAL_ID = OUTS.ANIMAL_ID
WHERE
    OUTS.ANIMAL_ID IS NULL
ORDER BY INS.DATETIME LIMIT 3
  • 우선 입양을 간 데이터를 저장하고 있는 테이블(ANIMAL_OUTS)과, 모든 동물 보호소에 들어온 정보를 담고 있는 테이블(ANIMAL_INS)이 있으므로 우선 이 둘의 정보를 합쳐서 계산을 하는 것이 좋겠다고 생각하여 JOIN을 하려고 했다.
  • 일반 JOIN(INNER)을 하게되면 두 테이블의 공통 정보(교집합)만 테이블에 저장되기 때문에 모든 동물 보호소에 들어온 데이터를 기준으로 입양을 간 데이터를 합치는 LEFT JOIN을 사용하였고, 외래키인 ANIMAL_ID를 기준으로 LEFT JOIN 하였다.
  • LEFT JOIN으로  합치면 ANIMAL_INS 테이블 옆에 ANIMAL_OUTS의 데이터가 나란히 붙게 되는데, ANIMAL_ID를 기준으로 합쳤기 때문에 ANIMAL_OUTS에는 ANIMAL_INS의 ANIMAL_ID가 있는 데이터들의 정보가 없으므로 ANIMAL_OUTS의 컬럼들에 NULL인 데이터가 생겨난다
  • 합쳐진 테이블에서 ANIMAL_OUTS의 컬럼들 중에 NULL인 데이터들이 동물 보호소에 있지만 입양을 가지 않은 동물들의 데이터이므로 이 테이블의 날짜를 기준으로 오름차순 정렬하여 3개만 자르게 되면 가장 오래 남아있는 동물의 데이터를 얻을 수 있다.
  • 남아있는 동물의 이름과 보호 시작일을 조회해야 하므로 ANIMAL_INS의 NAME과 DATETIME을 조회하면 문제를 해결할 수 있다

다른 풀이

SELECT 
    NAME, DATETIME
FROM ANIMAL_INS
WHERE ANIMAL_ID 
NOT IN (SELECT ANIMAL_ID FROM ANIMAL_OUTS)
ORDER BY DATETIME LIMIT 3
  • 두 개의 테이블을 조회하기 때문에 LEFT JOIN이 아닌 서브 쿼리르 활용해도 된다.
  • JOIN문이 없기 때문에 조금더 쿼리문이 간결해지며 WHERE 절에 NOT IN 키워드로 서브쿼리의 결과인 ANIMAL_OUTS의 ANIMAL_ID가 ANIMAL_INS의 ANIMAL_ID에 포함되어있지 않을 때의 데이터를 조회하도록 작성할 수 있다

카테고리 별 도서 판매량 집계하기

문제 및 테이블 예시

  • 2022년 1월 카테고리 별 도서 판매량을 합산하고 카테고리, 총 판매량 리스트를 출력하는 SQL문 작성
  • 결과는 카테고리명을 기준으로 오름차순 정렬

입출력 예시

나의 풀이

SELECT
    CATEGORY,
    SUM(SALES) AS TOTAL_SALES
FROM 
    BOOK AS B
JOIN
    BOOK_SALES AS BS
ON 
    B.BOOK_ID = BS.BOOK_ID
WHERE
    BS.SALES_DATE BETWEEN '2022-01-01' AND '2022-01-31'
GROUP BY CATEGORY
ORDER BY CATEGORY
  • 우선 두 테이블의 공통 정보로 하나의 테이블로 합치기 위해 각 테이블의 BOOK_ID 컬럼으로 INNER JOIN을 사용했다
  • 카테고리 별로 출력을 해야하므로 CATEGORY로 GROUP BY를 하고 요구사항에 따라 ORDER BY도 CATEGORY의 오름차순 순으로 출력하게 했다
  • 이후 2022년 1월의 도서 판매량을 합산해야 하므로 BETWEEN 연산을 사용하여 2022년 1월의 데이터만 조회한 후 SUM(SALES)로 카테고리별 판매량을 합산하여 조회했다
  • 날짜 조회는 DATE_FORMAT, 직접 비교 연산자를 해도되고 다양하게 접근할 수 있다
728x90