[2025-02-12] SQL, Java 알고리즘 풀이

이규정·2025년 2월 12일

1] SQL - 입양 시각 구하기(2)

문제 설명
ANIMAL_OUTS 테이블은 동물 보호소에서 입양 보낸 동물의 정보를 담은 테이블입니다. ANIMAL_OUTS 테이블 구조는 다음과 같으며, ANIMAL_ID, ANIMAL_TYPE, DATETIME, NAME, SEX_UPON_OUTCOME는 각각 동물의 아이디, 생물 종, 입양일, 이름, 성별 및 중성화 여부를 나타냅니다.

보호소에서는 몇 시에 입양이 가장 활발하게 일어나는지 알아보려 합니다. 0시부터 23시까지, 각 시간대별로 입양이 몇 건이나 발생했는지 조회하는 SQL문을 작성해주세요. 이때 결과는 시간대 순으로 정렬해야 합니다.

예시
SQL문을 실행하면 다음과 같이 나와야 합니다.

1. 코드작성

  1. sql도 데이터를 생성할 수 있다(Union all, with recursive 이용)
  2. 생성한 테이블을 기존 데이터테이블과 결합.
WITH RECURSIVE hours AS (
    SELECT 0 AS hour
    UNION ALL
    SELECT hour + 1 FROM hours WHERE hour < 23
)
SELECT 
    h.hour AS HOUR, 
    COUNT(a.ANIMAL_ID) AS COUNT
FROM hours h
LEFT JOIN ANIMAL_OUTS a
ON h.hour = HOUR(a.DATETIME)
GROUP BY h.hour
ORDER BY h.hour;

2] Java 알고리즘 문제 - 공원 산책

문제 설명
지나다니는 길을 'O', 장애물을 'X'로 나타낸 직사각형 격자 모양의 공원에서 로봇 강아지가 산책을 하려합니다. 산책은 로봇 강아지에 미리 입력된 명령에 따라 진행하며, 명령은 다음과 같은 형식으로 주어집니다.

["방향 거리", "방향 거리" … ]
예를 들어 "E 5"는 로봇 강아지가 현재 위치에서 동쪽으로 5칸 이동했다는 의미입니다. 로봇 강아지는 명령을 수행하기 전에 다음 두 가지를 먼저 확인합니다.

주어진 방향으로 이동할 때 공원을 벗어나는지 확인합니다.
주어진 방향으로 이동 중 장애물을 만나는지 확인합니다.
위 두 가지중 어느 하나라도 해당된다면, 로봇 강아지는 해당 명령을 무시하고 다음 명령을 수행합니다.
공원의 가로 길이가 W, 세로 길이가 H라고 할 때, 공원의 좌측 상단의 좌표는 (0, 0), 우측 하단의 좌표는 (H - 1, W - 1) 입니다.

공원을 나타내는 문자열 배열 park, 로봇 강아지가 수행할 명령이 담긴 문자열 배열 routes가 매개변수로 주어질 때, 로봇 강아지가 모든 명령을 수행 후 놓인 위치를 [세로 방향 좌표, 가로 방향 좌표] 순으로 배열에 담아 return 하도록 solution 함수를 완성해주세요.

제한사항

입출력 예

입출력 예 #1

입력된 명령대로 동쪽으로 2칸, 남쪽으로 2칸, 서쪽으로 1칸 이동하면 [0,0] -> [0,2] -> [2,2] -> [2,1]이 됩니다.

입출력 예 #2

입력된 명령대로라면 동쪽으로 2칸, 남쪽으로 2칸, 서쪽으로 1칸 이동해야하지만 남쪽으로 2칸 이동할 때 장애물이 있는 칸을 지나기 때문에 해당 명령을 제외한 명령들만 따릅니다. 결과적으로는 [0,0] -> [0,2] -> [0,1]이 됩니다.

입출력 예 #3

처음 입력된 명령은 공원을 나가게 되고 두 번째로 입력된 명령 또한 장애물을 지나가게 되므로 두 입력은 제외한 세 번째 명령만 따르므로 결과는 다음과 같습니다. [0,1] -> [0,0]

1. 요구사항

- 입력 데이터

  1. 공원(park):

    • 문자열 배열로 주어지며, 각 문자열은 공원의 한 행을 나타냅니다.
    • 각 문자는 다음 중 하나입니다.
      • 'S': 시작 위치 (단 하나 존재)
      • 'O': 이동 가능한 통로
      • 'X': 장애물
    • 공원은 직사각형 모양이며, 가로 길이와 세로 길이를 알 수 있음.
  2. 경로(routes):

    • 문자열 배열로 주어지며, 각 원소는 "방향 거리" 형식입니다.
    • 방향: 'N', 'S', 'W', 'E' 중 하나.
    • 거리: 1 이상 9 이하의 정수 (이동할 칸 수).

- 동작 방식

  1. 시작 위치 탐색:
    • park 배열에서 시작 위치 'S'를 찾아 초기 위치로 설정한다.
  2. 명령어 처리:
    • 주어진 routes의 각 명령어에 대해 순서대로 처리한다.
    • 각 명령어를 파싱하여 이동 방향과 이동할 칸 수를 구한다.
    • 이동 전 검증:
      • 범위 검사: 이동하는 도중 공원의 경계를 벗어나는지 확인.
      • 장애물 검사: 이동 경로 상에 장애물 'X'가 있는지 확인.
    • 위 두 가지 조건 중 하나라도 만족하면 해당 명령어는 무시한다.
    • 모든 검증을 통과한 경우에만 로봇 강아지의 현재 위치를 업데이트한다.

- 출력

  • 모든 명령어를 처리한 후 최종 위치를 [행(세로 좌표), 열(가로 좌표)] 형태의 배열로 반환한다.

2. 플로우차트

          ┌────────────────────────────┐
          │          시작(Start)       │
          └────────────────────────────┘
                        │
                        ▼
          ┌────────────────────────────┐
          │   park, routes 입력 받기    │
          └────────────────────────────┘
                        │
                        ▼
          ┌────────────────────────────┐
          │  공원의 행 수와 열 수 계산   │
          └────────────────────────────┘
                        │
                        ▼
          ┌────────────────────────────┐
          │  park 배열에서 'S' 위치 탐색 │
          └────────────────────────────┘
                        │
                        ▼
          ┌────────────────────────────┐
          │  현재 위치를 시작 위치로 설정 │
          └────────────────────────────┘
                        │
                        ▼
          ┌────────────────────────────┐
          │    routes 배열을 순회하며    │
          │   각 명령어 처리(아래 참조)   │
          └────────────────────────────┘
                        │
                        ▼
          ┌────────────────────────────┐
          │ 모든 명령어 처리 후 최종 위치 │
          │      반환 (리턴)           │
          └────────────────────────────┘
                        │
                        ▼
          ┌────────────────────────────┐
          │            종료            │
          └────────────────────────────┘

[각 명령어 처리 세부 플로우]

         ┌────────────────────────────┐
         │  명령어 "방향 거리" 파싱하기  │
         └────────────────────────────┘
                        │
                        ▼
         ┌────────────────────────────┐
         │    해당 방향에 대한        │
         │  이동 벡터(Δ행, Δ열) 구하기  │
         └────────────────────────────┘
                        │
                        ▼
         ┌────────────────────────────┐
         │ 임시 위치를 현재 위치로 설정 │
         └────────────────────────────┘
                        │
                        ▼
         ┌────────────────────────────┐
         │ 1부터 거리만큼 반복하면서  │
         │   임시 위치 갱신하기         │
         └────────────────────────────┘
                        │
                        ▼
         ┌────────────────────────────┐
         │  임시 위치가 범위 내인지,    │
         │  장애물 없는지 확인         │
         └────────────────────────────┘
                        │
                ┌───────┴───────┐
                │               │
             [문제 있음]     [문제 없음]
                │               │
                ▼               ▼
         ┌─────────────────┐  ┌─────────────────┐
         │   명령어 무시   │  │  명령어 완료 후  │
         │ (현재 위치 유지)│  │  현재 위치 갱신  │
         └─────────────────┘  └─────────────────┘

3. 의사코드

function solution(park, routes):
    // 1. 공원의 크기 계산
    rows ← park.length
    cols ← park[0].length

    // 2. 시작 위치 'S' 탐색
    for r from 0 to rows - 1:
        for c from 0 to cols - 1:
            if park[r].charAt(c) == 'S':
                startRow ← r
                startCol ← c

    // 3. 현재 위치 초기화
    currentRow ← startRow
    currentCol ← startCol

    // 4. 방향 벡터 매핑 설정
    directionMap ← {
        'N': (-1, 0),
        'S': (1, 0),
        'W': (0, -1),
        'E': (0, 1)
    }

    // 5. routes 배열의 각 명령어 처리
    for each route in routes:
        // 5-1. 명령어 파싱: 방향과 거리 구하기
        split route by space into [dir, distStr]
        distance ← integer value of distStr

        // 5-2. 해당 방향의 이동 벡터 가져오기
        deltaRow, deltaCol ← directionMap[dir]

        // 5-3. 임시 위치 변수 초기화
        newRow ← currentRow
        newCol ← currentCol
        validCommand ← true

        // 5-4. 주어진 거리만큼 한 칸씩 이동하면서 검증
        for i from 1 to distance:
            newRow ← newRow + deltaRow
            newCol ← newCol + deltaCol

            // 범위 검사 및 장애물 확인
            if newRow < 0 or newRow ≥ rows or newCol < 0 or newCol ≥ cols or park[newRow].charAt(newCol) == 'X':
                validCommand ← false
                break out of the loop

        // 5-5. 명령어가 유효한 경우에만 현재 위치 업데이트
        if validCommand is true:
            currentRow ← newRow
            currentCol ← newCol

    // 6. 최종 위치 반환
    return [currentRow, currentCol]

4. 코드작성

public int[] solution(String[] park, String[] routes) {
        int rows = park.length;
        int cols = park[0].length();
        int startRow = 0, startCol = 0;
        
        // 시작 위치 'S' 찾기
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (park[r].charAt(c) == 'S') {
                    startRow = r;
                    startCol = c;
                    break;
                }
            }
        }
        
        // 방향 벡터 설정
        Map<Character, int[]> directions = new HashMap<>();
        directions.put('N', new int[]{-1, 0});
        directions.put('S', new int[]{1, 0});
        directions.put('W', new int[]{0, -1});
        directions.put('E', new int[]{0, 1});
        
        // 현재 위치 초기화
        int curRow = startRow;
        int curCol = startCol;
        
        // 명령어 처리
        for (String route : routes) {
            String[] parts = route.split(" ");
            char dir = parts[0].charAt(0);
            int dist = Integer.parseInt(parts[1]);
            
            int[] delta = directions.get(dir);
            boolean canMove = true;
            int newRow = curRow;
            int newCol = curCol;
            
            // 주어진 거리만큼 이동할 수 있는지 확인
            for (int i = 1; i <= dist; i++) {
                newRow += delta[0];
                newCol += delta[1];
                if (newRow < 0 || newRow >= rows || newCol < 0 || newCol >= cols 
                        || park[newRow].charAt(newCol) == 'X') {
                    canMove = false;
                    break;
                }
            }
            
            if (canMove) {
                curRow = newRow;
                curCol = newCol;
            }
        }
        
        return new int[]{curRow, curCol};
} 

5. 원래코드(...)

import java.util.*;

class Solution {
    public int[] solution(String[] park, String[] routes) {
        int[] answer = {0, 0};

        int garo = park[0].length(); 
        int sero = park.length;      
        int[] map = new int[garo * sero];

        for (int i = 0; i < map.length; i++) {
            if (park[i / garo].charAt(i % garo) == 'S') { 
                answer[0] = i / garo;
                answer[1] = i % garo;
            }
            char tempChar = park[i / garo].charAt(i % garo);
            map[i] = tempChar == 'S' ? 1 : (tempChar == 'X' ? -1 : 0);
        }

        char[] direct = new char[routes.length];
        int[] distance = new int[routes.length];
        for (int i = 0; i < routes.length; i++) {
            String[] temp = routes[i].split(" ");
            direct[i] = temp[0].charAt(0);
            distance[i] = Integer.parseInt(temp[1]);
        }

        for (int i = 0; i < direct.length; i++) {
            int tempInt = distance[i];
            int now = answer[0] * garo + answer[1];
            int move = 0;

            switch (direct[i]) {
                case 'N':
                    if (answer[0] - tempInt < 0) break; 
                    for (int j = 1; j <= tempInt; j++) {
                        if (map[now - garo * j] < 0) { 
                            move = 0;
                            break;
                        }
                        move--;
                    }
                    answer[0] += move;
                    break;
                case 'S':
                    if (answer[0] + tempInt >= sero) break; 
                    for (int j = 1; j <= tempInt; j++) {
                        if (map[now + garo * j] < 0) { 
                            move = 0;
                            break;
                        }
                        move++;
                    }
                    answer[0] += move;
                    break;
                case 'W':
                    if (answer[1] - tempInt < 0) break;
                    for (int j = 1; j <= tempInt; j++) {
                        if (map[now - j] < 0) {
                            move = 0;
                            break;
                        }
                        move--;
                    }
                    answer[1] += move;
                    break;
                case 'E':
                    if (answer[1] + tempInt >= garo) break;
                    for (int j = 1; j <= tempInt; j++) {
                        if (map[now + j] < 0) {
                            move = 0;
                            break;
                        }
                        move++;
                    }
                    answer[1] += move;
                    break;
            }
        }
        return answer;
    }
}

[코드 비교]

1. 자료구조 및 좌표 표현 방식

원본 코드

  • 1차원 배열 활용:
    • 공원을 1차원 배열 map으로 표현하여 i / garoi % garo를 통해 2차원 좌표로 변환.
    • 'S', 'X', 'O'에 대해 각각 1, -1, 0으로 매핑하여 장애물 여부를 숫자로 판단.
  • 장점:
    • 1차원 배열로 변환하면 연속적인 메모리 접근이 가능.
  • 단점:
    • 인덱스 계산이 필요하여 코드 가독성이 떨어지고, 실수할 가능성이 높음.
    • 각 방향에 대해 반복되는 switch-case 블록이 존재하여 코드 중복이 많음.

개선 코드

  • 2차원 좌표 및 문자열 접근:
    • 공원 정보를 2차원(행, 열) 좌표로 처리하여 park[r].charAt(c)로 접근.
  • 장점:
    • 좌표가 직관적이고 코드 가독성이 좋음.
    • 'S'나 'X' 등 문자를 직접 비교하여 장애물 여부를 판단하므로 자료의 의미가 명확함.
  • 단점:
    • 2차원 배열 접근이 약간의 오버헤드를 줄 수 있으나, 문제 크기(최대 50×50)에서는 성능상 큰 문제가 되지 않음.

2. 명령어 처리 및 이동 로직

원본 코드

  • switch-case를 통한 방향별 처리:
    • 각 방향('N', 'S', 'W', 'E')마다 별도의 switch-case 분기를 사용하여 처리.
    • 각 케이스 내에서 이동 가능 여부를 검사하고, 문제가 발생하면 바로 break로 명령어 전체를 무시.
  • 장점:
    • 각 방향에 대해 명시적으로 처리 로직을 작성하여, 방향별 세부 로직을 한 눈에 볼 수 있음.
  • 단점:
    • 각 방향별로 거의 동일한 로직(한 칸씩 이동하며 검사)을 반복하여 코드 중복이 많음.
    • 인덱스 계산(예, now - garo * j, now + j 등) 때문에 디버깅이나 유지보수가 어려워질 수 있음.

개선 코드

  • 방향 벡터와 공통 루프 사용:
    • Map<Character, int[]>를 활용하여 각 방향에 대한 (행, 열) 변화량을 저장.
    • 하나의 공통 루프에서 주어진 거리만큼 한 칸씩 이동하면서 범위와 장애물을 동시에 체크.
  • 장점:
    • 중복 코드를 제거하고, 모든 방향에 대해 동일한 로직을 재사용하므로 유지보수가 쉬움.
    • 방향 벡터를 사용하면 새로운 방향이 추가되거나 로직 수정 시 간단히 수정 가능.
  • 단점:
    • 별도의 자료구조(Map)를 사용함으로써 약간의 초기 설정 코드가 추가되지만, 전체 코드 길이와 가독성 측면에서는 유리함.

3. 경로 검증 로직

원본 코드

  • 각 방향마다 이동 전 경계 검사와 장애물 검사를 수행.
  • 한 방향에 대해 이동할 때마다 1차원 배열 인덱스로 계산하여 체크하므로, 실수로 인덱스 계산을 잘못할 위험이 있음.

개선 코드

  • 명령어별 시뮬레이션:
    • 각 명령어에 대해 한 칸씩 이동하면서 즉시 범위 및 장애물 여부를 확인.
    • 문제가 발생하면 해당 명령어 전체를 무효화하고, 문제가 없을 경우에만 최종 위치를 업데이트.
  • 장점:
    • 전체 이동 경로를 한 번에 시뮬레이션하므로, 중간에 문제가 생기면 바로 종료할 수 있음.
    • 직관적이고 디버깅하기 쉬운 구조.

종합 비교 및 결론

비교 항목원본 코드개선 코드
가독성1차원 배열과 switch-case 사용으로 복잡2차원 좌표 및 방향 벡터로 직관적
유지보수성중복 코드가 많아 수정 어려움공통 로직을 사용하여 유지보수 용이
성능문제 크기 내에서는 성능 차이 없음문제 크기 내에서 성능 문제 없음
코드 중복각 방향별로 동일한 로직 반복방향 벡터 활용으로 중복 제거

결론

  • 원본 코드는 요구사항을 충족하지만, 1차원 배열과 switch-case 구조 때문에 코드가 다소 복잡하고 유지보수가 어려움.
  • 개선 코드는 2차원 좌표 및 방향 벡터를 활용하여 더 직관적이고 간결한 구조를 가지며, 향후 코드 수정이나 확장이 필요할 때 유리함.
  • 유지보수성과 가독성을 고려할 때 개선된 접근 방식이 더 좋은 선택으로 평가 가능.
  1. 레코드클래스 추천! (절대불변, 필드를 get 안써도 .만 붙여도 가져올 수 있음.)
  2. 웹 페이지 모바일 슬라이스 (offset 단점)
  3. 유틸 클래스를 만들자!
  4. page와 size는 Pageable 객체로 한방에 받아올 수 있음.
  5. 비즈니스로직이 아니면 컨트롤러도 갠차늠
  6. 근거만 있으면 일단 ok
profile
반갑습니다. 백엔드 개발자가 되기 위해 노력중입니다.

0개의 댓글