[하코테] Day8. 오버헤드 줄이기 (프로그래머스 "기능개선" 풀이)

Posted by Space_Jin
2025. 9. 21. 16:28 Programming/Algorithm
728x90
반응형

하코테 8일차 문제 역시 스택/큐 문제입니다.

해당 문제에서는 큐를 이용한 문제 풀이를 하였으나 반드시 자료구조 큐를 이용할 필요없이 성능 향상을 위한 방식을 고려해볼 수 있어습니다.


문제 설명

프로그래머스 팀에서는 기능 개선 작업을 수행 중입니다. 각 기능은 진도가 100%일 때 서비스에 반영할 수 있습니다.

또, 각 기능의 개발속도는 모두 다르기 때문에 뒤에 있는 기능이 앞에 있는 기능보다 먼저 개발될 수 있고, 이때 뒤에 있는 기능은 앞에 있는 기능이 배포될 때 함께 배포됩니다.

먼저 배포되어야 하는 순서대로 작업의 진도가 적힌 정수 배열 progresses와 각 작업의 개발 속도가 적힌 정수 배열 speeds가 주어질 때 각 배포마다 몇 개의 기능이 배포되는지를 return 하도록 solution 함수를 완성하세요.

 

제한 사항

작업의 개수(progresses, speeds배열의 길이)는 100개 이하입니다.

작업 진도는 100 미만의 자연수입니다.

작업 속도는 100 이하의 자연수입니다.

배포는 하루에 한 번만 할 수 있으며, 하루의 끝에 이루어진다고 가정합니다. 예를 들어 진도율이 95%인 작업의 개발 속도가 하루에 4%라면 배포는 2일 뒤에 이루어집니다.

 

입출력 예

progresses speeds return

[93, 30, 55] [1, 30, 5] [2, 1]

[95, 90, 99, 99, 80, 99] [1, 1, 1, 1, 1, 1] [1, 3, 2]

 

입출력 예 설명

입출력 예 #1

첫 번째 기능은 93% 완료되어 있고 하루에 1%씩 작업이 가능하므로 7일간 작업 후 배포가 가능합니다.

두 번째 기능은 30%가 완료되어 있고 하루에 30%씩 작업이 가능하므로 3일간 작업 후 배포가 가능합니다. 하지만 이전 첫 번째 기능이 아직 완성된 상태가 아니기 때문에 첫 번째 기능이 배포되는 7일째 배포됩니다.

세 번째 기능은 55%가 완료되어 있고 하루에 5%씩 작업이 가능하므로 9일간 작업 후 배포가 가능합니다.

따라서 7일째에 2개의 기능, 9일째에 1개의 기능이 배포됩니다.

 

입출력 예 #2

모든 기능이 하루에 1%씩 작업이 가능하므로, 작업이 끝나기까지 남은 일수는 각각 5일, 10일, 1일, 1일, 20일, 1일입니다. 어떤 기능이 먼저 완성되었더라도 앞에 있는 모든 기능이 완성되지 않으면 배포가 불가능합니다.

따라서 5일째에 1개의 기능, 10일째에 3개의 기능, 20일째에 2개의 기능이 배포됩니다.


문제 풀이

해당 문제는 작업의 순서에 따라서 배포까지 남은 작업기간이 앞선작업과 일치하는지 계속 비교하는 문제로 FIFO(First In First Out)의 큐 자료구조를 혹은 방법론을 이용하는 문제입니다.

 

1. 배포까지 남은 작업 기간을 계산하여 큐에 넣기

2. 배포할 작업보다 작업 기간이 작거나 같은 작업은 함께 배포 -> 큐에서 함께 제거

방식으로 접근 합니다.

 

소스 코드

import java.util.Deque;
import java.util.ArrayDeque;
import java.util.List;
import java.util.ArrayList;

class Solution {
    public int[] solution(int[] progresses, int[] speeds) {
        /*
        문제 풀이 개요
        함께 배포될 세트를 작업의 순서대로 처리
        순차처리 -> 큐
        함께 배포 -> 현재 배포할 작업보다 완료 예정기간이 작거나 같은 것
        */
        Deque<Integer> q = new ArrayDeque<>();  //완료까지 남은 작업일을 담는 큐
        List<Integer> list = new ArrayList<>();    //정답을 담을 리스트
        
        //작업 담기
        for(int i = 0; i < progresses.length; i++) {
            //남은 작업 기간 -> 소숫점은 하루가 더 필요하므로 올림처리 한다.
            int leftD = (int)Math.ceil((100.0 - progresses[i]) / speeds[i]); 
            q.offer(leftD);
        }
        
        //배포일 계산
        while(!q.isEmpty()) {
            int cnt = 1;    //함께 배포될 작업 수
            int d = q.poll();   //현재 작업이 완료되기까지 걸리는 기간
            
            //함께 배포될 작업 카운트
            while(!q.isEmpty() && q.peek() <= d) {
                q.poll();
                cnt++;
            }
            
            list.add(cnt);
        }
               
        return list.stream()
            .mapToInt(Integer::intValue)
            .toArray();
    }
}

 

해당 코드는 앞선 문제풀이 방식을 순차적으로 코드로 구현한 코드입니다.

남은 작업을 계산하여 큐에 넣기 -> 배포일이 같은 작업을 큐에서 반복적으로 제거하면서 정답 리스트에 적재

시간복잡도 O(logN)

 

하지만 위 문제의 경우, 작업(porgresses)를 처음부터 끝까지 그대로 봐도 무방하므로 큐 자료구조의 직접 사용하지 않고 풀이가 가능합니다. 그 밖에도 ArrayList를 이용해서 정답을 삽입, Stream을 통한 정답 타입(int 배열)로 변환하면서 오버헤드 등을 줄이는 방식이 가능합니다.

 

개선된 풀이

import java.util.Arrays;

class Solution {
    public int[] solution(int[] progresses, int[] speeds) {     
        /** 개선된 풀이 */
        int n = progresses.length;
        // 임시로 최대 n개까지 담을 버퍼
        int[] tmp = new int[n];
        int k = 0; // 실제 결과 길이

        // 첫 작업의 완료일(정수 올림)
        int curr = (100 - progresses[0] + speeds[0] - 1) / speeds[0];
        int cnt = 1;

        for (int i = 1; i < n; i++) {
            int day = (100 - progresses[i] + speeds[i] - 1) / speeds[i];
            if (day <= curr) {
                // 현재 배포 묶음에 포함
                cnt++;
            } else {
                // 지금까지 묶은 개수 확정
                tmp[k++] = cnt;
                // 새 기준일로 갱신
                curr = day;
                cnt = 1;
            }
        }
        // 마지막 묶음 추가
        tmp[k++] = cnt;

        // 결과 크기에 맞춰 잘라서 반환
        return Arrays.copyOf(tmp, k);
    }
}

 

1. 불필요한 큐에 담기 제거

2. 올림을 위한 형변환을 제거(float, Math 등 사용 제거)

3. 스트림 오버헤드 제거(List를 Array로 변환하기 위함)

 

을 통해서 메모리 사용 감소 및 동작 간소화로 성능을 개선할 수 있습니다.

실제 프로그래머스의 결과만 보았을 때, 시간적으로 약 100배 좋은 성능을 낼 수 있습니다.


단순히 문제가 요구하는 카테고리의 자료구조를 사용하여 문제를 풀 수 있지만, 기본형(int 배열 등)의 사용이나 정수형 올림 나눗셈을 이용한 캐스팅 제거, 스트림을 통한 오버헤드 제거 등을 적극적으로 활용해보는 것이 좋은 성능의 코드를 만들 수 있다는걸 체감하게된 좋은 문제였습니다.

728x90
반응형

[하코테] Day7. 약간고비 (프로그래머스 "주식가격" 풀이)

Posted by Space_Jin
2025. 9. 21. 13:30 Programming/Algorithm
728x90
반응형

하코테(하루 코딩 테스트) 2주차의 두번째 문제 입니다.

이번주는 회사 일정으로 문제풀이 및 블로그 글 작성이 버거움이 있었어서 블로그 글을 몰아쓰게 되었습니다.

그래도 매일 한 문제씩 풀자고 목표를두니 작성을 포기하지는 않게되어서 도움이되네요.


문제 설명

초 단위로 기록된 주식가격이 담긴 배열 prices가 매개변수로 주어질 때, 가격이 떨어지지 않은 기간은 몇 초인지를 return 하도록 solution 함수를 완성하세요.

 

제한사항

prices의 각 가격은 1 이상 10,000 이하인 자연수입니다.

prices의 길이는 2 이상 100,000 이하입니다.

 

입출력 예

prices return

[1, 2, 3, 2, 3] [4, 3, 1, 1, 0]

입출력 예 설명

1초 시점의 ₩1은 끝까지 가격이 떨어지지 않았습니다.

2초 시점의 ₩2은 끝까지 가격이 떨어지지 않았습니다.

3초 시점의 ₩3은 1초뒤에 가격이 떨어집니다. 따라서 1초간 가격이 떨어지지 않은 것으로 봅니다.

4초 시점의 ₩2은 1초간 가격이 떨어지지 않았습니다.

5초 시점의 ₩3은 0초간 가격이 떨어지지 않았습니다.


문제 풀이

해당 문제는 현재의 주식 가격과 미래의 주식 가격이 변경되는 시점의 차이를 확인해야합니다.

정확히는 가격이 떨어지는 시점을 비교하는 것이 핵심 입니다.

사실 이 문제를 처음 접했을 때, 주식 가격에 따른 가격/인덱스 정보를 담은 리스트를 담고 정렬하여 문제를 풀려고 시도 했습니다.

그와 같은 풀이는 가격이 처음 변동되는 시점을 확인할 수 없다는 문제로 잘못된 풀이라고 할 수 있습니다.

 

문제의 카테고리가 스택/큐 이기에 두 자료구조를 사용해야한다는 힌트로 문제를 풀 수 있었습니다.

주식 가격이 처음 변경되는 시점은 시간을 순서대로 가격을 비교할 때, 현재 시점의 가격이 가장 가까운 이전 시점의 가격의 순서와 비교를 합니다. 가장 먼저 들어온 가격이 아닌 가장 나중에 들어온 가격과 비교 -> 즉, 스택 자료구조를 선택 합니다.

 

소스코드

import java.util.Deque;
import java.util.ArrayDeque;

class Solution {
    public int[] solution(int[] prices) {
        /*
        문제 풀이 개요
        과거의 주식가격을 미래의 시점과 비교
        
        주가의 하락시점 확인
        -> 시간의 순서로 주식가격이 가장 먼저 나오는 주가하락 시점을 파악
        -> 스택 자료구조를 이용하여 순차비교
        */
        int len = prices.length;
        int[] answer  = new int[len]; //정답을 담을 배열
        Deque<Integer> st = new ArrayDeque<>(); //순번을 담을 스택
        
        //주식가격을 순회하며 미래시점 순번 비교
        for(int i = 0; i < len; i++) {
            //주식가격이 처음 하락하는 경우
            while(!st.isEmpty() && prices[st.peek()] > prices[i]) {
                int idx = st.pop();
                answer[idx] = i - idx;
            }
            
            st.push(i);
        }
        
        //스택에 남아있는 가격이 떨어지지 않은 시점 게산
        while(!st.isEmpty()) {
            int idx = st.pop();
            answer[idx] = len - idx - 1;
        }
        
        return answer;
    }
}

문제 풀이에서 언급한 것처럼 해당 문제는 카테고리가 스택/큐 이기에 두 자료구조를 사용해야한다는 힌트로 문제를 풀 수 있었습니다.

이러한 힌트가 없었다면, 문제 풀이에 더 어려움을 겪었을 것 같습니다.

문제를 접할 때, 어떤 자료구조와 개념으로 접근해야할지 아직 더 많은 훈련이 필요할 것 같습니다.

728x90
반응형

[하코테] Day6. 2주차 시작 (프로그래머스 "같은 숫자는 싫어")

Posted by Space_Jin
2025. 9. 15. 23:10 Programming/Algorithm
728x90
반응형

하코테를 시작하고 2주차가 시작되었습니다.

월요일에 큰 일정이 없어서 무사히 문제를 풀 수 있었습니다.

 

지난주에는 계속 해시 문제를 풀었다면, 이번주에는 스택/큐 문제가 나올 것으로 보입니다.


문제 설명

배열 arr가 주어집니다. 배열 arr의 각 원소는 숫자 0부터 9까지로 이루어져 있습니다. 이때, 배열 arr에서 연속적으로 나타나는 숫자는 하나만 남기고 전부 제거하려고 합니다. 단, 제거된 후 남은 수들을 반환할 때는 배열 arr의 원소들의 순서를 유지해야 합니다. 예를 들면,

arr = [1, 1, 3, 3, 0, 1, 1] 이면 [1, 3, 0, 1] 을 return 합니다.

arr = [4, 4, 4, 3, 3] 이면 [4, 3] 을 return 합니다.

배열 arr에서 연속적으로 나타나는 숫자는 제거하고 남은 수들을 return 하는 solution 함수를 완성해 주세요.

제한사항

배열 arr의 크기 : 1,000,000 이하의 자연수

배열 arr의 원소의 크기 : 0보다 크거나 같고 9보다 작거나 같은 정수

 

입출력 예

arr answer

[1,1,3,3,0,1,1] [1,3,0,1]

[4,4,4,3,3] [4,3]

입출력 예 설명

입출력 예 #1,2

문제의 예시와 같습니다.


풀이

이번 문제는 arr를 순회하면서 현재의 값이 이전 값과 같은지를 비교하는 문제였습니다.

스택/큐 문제인만큼 스택 자료구조를 활용해서 최상단의 값이 현재 타겟 값과 동일한지 비교하는 문제로 판단했습니다.

 

실제로 해당 문제를 Stack이나 ArrayDeque를 활용해서 스택으로 문제를 풀 수 있지만, 성능적인 측면에서 조금 더 불리한 면이 있습니다.

특히 최종적으로 Stream을 통해서 return 값의 형태인 int 배열로의 변환은 박싱/언박싱의 오버헤드가 있습니다.

 

import java.util.ArrayDeque;

public class Solution {
    public int[] solution(int []arr) {
        /*
        문제 풀이 개요
        arr를 하나씩 담으면서 현재 값이 이전 값과 동일하지 않는다면, 담는다.
        */
        ArrayDeque<Integer> q = new ArrayDeque<>();

        //탐색
        for(int num : arr) {
            if(q.isEmpty() || q.peekLast() != num) {
                q.offer(num);
            }
        }

        //변환
        int[] answer = q.stream()
                .mapToInt(Integer::intValue)
                .toArray();

        return answer;
    }
}

ArrayDeque를 다시 int 배열로 만들기 위해 Stream의 사용은 성능적으로 불리함

 

개선된 풀이 1.

import java.util.stream.IntStream;

public class Solution {
    public int[] solution(int[] arr) {
        int n = arr.length;
        if (n == 0) return new int[0];

        return IntStream.range(0, n)
                .filter(i -> i == 0 || arr[i] != arr[i - 1])
                .map(i -> arr[i])
                .toArray(); // int[]
    }
}

스트림을 확용한 가독성 풀이

하지만, 역시 스트림을 사용하므로써 성능적으로 약간의 개선만 있을 뿐 효과적이라고 볼 수 없다.

효율성 테스트 평균 시간 30ms

 

개선된 풀이 2.

import java.util.Arrays;

public class Solution {
    public int[] solution(int []arr) {
        /*
        문제 풀이 개요
        arr를 하나씩 담으면서 현재 값이 이전 값과 동일하지 않는다면, 담는다.
        */
        int n = arr.length; // arr의 길이
        
        // 예외처리
        if(n == 0) return new int[0];
        
        int l = 0;  // 임시 배열의 크기
        int[] temp = new int[n];    // 임시 배열
        
        temp[l++] = arr[0]; //첫번째 값 넣기
        
        for(int i = 1; i < n; i++) {
            if(temp[l-1] != arr[i]) temp[l++] = arr[i];   // 이전 값과 다르면 담기
        }
        
        return Arrays.copyOf(temp, l);
    }
}

인덱스로 필요한 배열을 직접 생성한다.

성능적으로 가장 우수하다

효율성 테스트 평균 시간 10ms


오늘 풀이는 낮은 난이도의 문제이나 효율성적인 면에서 어떤 코드가 더 좋은지에 대한 고민이 필요한 부분인 것 같다.

 

위 세가지 풀이 모두 정답이지만, 해당 하는 팀이 선호하는 컨벤션이 무엇인지를 확인하고 모두 사용할 줄 아는것이 좋을 것 같다.

 

하지만 기본적으로 스트림 등을 활용한 멋진 코드 보다 속도와 메모리 관리 등에 효율적인 순수 배열/루트를 활용하는 것이 더 좋지 않을까란 생각이 들었다.

728x90
반응형

[하코테] Day5. 일주일 풀이 완료 (프로그래머스 "완주하지 못한 선수")

Posted by Space_Jin
2025. 9. 13. 21:58 Programming/Algorithm
728x90
반응형

매일 코딩테스트 5일차 문제까지 완료했습니다. 조금씩 매일 문제를 푸는데에 익숙해지는 것 같습니다.


문제

수많은 마라톤 선수들이 마라톤에 참여하였습니다. 단 한 명의 선수를 제외하고는 모든 선수가 마라톤을 완주하였습니다.

마라톤에 참여한 선수들의 이름이 담긴 배열 participant와 완주한 선수들의 이름이 담긴 배열 completion이 주어질 때, 완주하지 못한 선수의 이름을 return 하도록 solution 함수를 작성해주세요.

 

제한사항

마라톤 경기에 참여한 선수의 수는 1명 이상 100,000명 이하입니다.

completion의 길이는 participant의 길이보다 1 작습니다.

참가자의 이름은 1개 이상 20개 이하의 알파벳 소문자로 이루어져 있습니다.

참가자 중에는 동명이인이 있을 수 있습니다.

 

입출력 예

[participant] [completion] return

["leo", "kiki", "eden"] ["eden", "kiki"] "leo"

["marina", "josipa", "nikola", "vinko", "filipa"] ["josipa", "filipa", "marina", "nikola"] "vinko"

["mislav", "stanko", "mislav", "ana"] ["stanko", "ana", "mislav"] "mislav"

 

입출력 예 설명

예제 #1

"leo"는 참여자 명단에는 있지만, 완주자 명단에는 없기 때문에 완주하지 못했습니다.

예제 #2

"vinko"는 참여자 명단에는 있지만, 완주자 명단에는 없기 때문에 완주하지 못했습니다.

예제 #3

"mislav"는 참여자 명단에는 두 명이 있지만, 완주자 명단에는 한 명밖에 없기 때문에 한명은 완주하지 못했습니다.


문제 풀이

해당문제는 참가자 - 완료자 목록을 계산해서 남은 1명을 구해냅니다.

각 문자열 배열로 존재하고 목록을 서로 비교해야하기에 문자열 배열 비교 혹은 해시 테이블을 사용할 수 있습니다.

 

배열풀이

각 배열을 정렬한다. -> i번째 값이 불일치 한다면, 해당하는 참가자 배열의 값이 완주못한 선수이다.

시간복잡도

- O(nlogn)

- 2번의 배열 정렬(O(nlogn))과 한번의 탐색(O(n))이 필요

 

해시 테이블 풀이

참가자를 해시 테이블에 담고 완주자를 테이블에서 제거서해서 남은 인원이 완주못한 선수이다.

시간복잡도

- O(n)

- 두번의 해시 테이블 삽입(O(1)) 및 제거(O(1))가 필요

 

참가자의 최댓값은 10만이고 메모리에 대한 제약이 없기에 해시 테이블의 풀이를 선택한다.

 

소스 코드

import java.util.Map;
import java.util.HashMap;

class Solution {
    public String solution(String[] participant, String[] completion) {
        /*
        문제 풀이 개요
        참가자 목록 - 완주자 목록을 계산한다.
        참가자 중에 동명이인(중복)이 있을 수 있다 -> Map 이용
        */
        Map<String, Integer> map = new HashMap<>();
        for(String p : participant) map.merge(p, 1, Integer::sum);  //참가자 추가
        for(String c : completion) map.merge(c, -1, Integer::sum);  //완료자 제거
        
        //결과 찾기
        String answer = map.entrySet().stream()
                .filter(e -> e.getValue() != 0)	//0이 아닌 것만 남기기
                .map(Map.Entry::getKey)	//엔트리의 key만 추출
                .findFirst()	//첫번째 값(이미 1개이지만 값을 가져오기 위함) -> Optional<String>
                .orElseThrow();	//예외처리를 통해 String으로 변환
        
        return answer;
    }
}

알아두면 좋은 점

해시 문제 중 비교적 간단한 문제이지만, Map의 추가/삭제 그리고 최종 값을 찾기 위한 탐색까지 사용해야하는 좋은 문제인것 같습니다.

특히 유용한 문법인 merge 메서드와 stream API, Map의 Entry 인터페이스 사용까지 적절히 연습할 수 있었습니다.

728x90
반응형

[하코테] Day4. 작심삼일 극복 (프로그래머스 "베스트 앨범")

Posted by Space_Jin
2025. 9. 13. 18:59 Programming/Algorithm
728x90
반응형

하코테 4일차 풀이까지 완료. 다행히 작심삼일로 끝내지 않게 되었으니 꾸준히 해보려고 합니다.


문제 설명

스트리밍 사이트에서 장르 별로 가장 많이 재생된 노래를 두 개씩 모아 베스트 앨범을 출시하려 합니다. 노래는 고유 번호로 구분하며, 노래를 수록하는 기준은 다음과 같습니다.

속한 노래가 많이 재생된 장르를 먼저 수록합니다.

장르 내에서 많이 재생된 노래를 먼저 수록합니다.

장르 내에서 재생 횟수가 같은 노래 중에서는 고유 번호가 낮은 노래를 먼저 수록합니다.

노래의 장르를 나타내는 문자열 배열 genres와 노래별 재생 횟수를 나타내는 정수 배열 plays가 주어질 때, 베스트 앨범에 들어갈 노래의 고유 번호를 순서대로 return 하도록 solution 함수를 완성하세요.

 

제한사항

genres[i]는 고유번호가 i인 노래의 장르입니다.

plays[i]는 고유번호가 i인 노래가 재생된 횟수입니다.

genres와 plays의 길이는 같으며, 이는 1 이상 10,000 이하입니다.

장르 종류는 100개 미만입니다.

장르에 속한 곡이 하나라면, 하나의 곡만 선택합니다.

모든 장르는 재생된 횟수가 다릅니다.

 

입출력 예

genres plays return

["classic", "pop", "classic", "classic", "pop"] [500, 600, 150, 800, 2500] [4, 1, 3, 0]

 

입출력 예 설명

classic 장르는 1,450회 재생되었으며, classic 노래는 다음과 같습니다.

고유 번호 3: 800회 재생

고유 번호 0: 500회 재생

고유 번호 2: 150회 재생

pop 장르는 3,100회 재생되었으며, pop 노래는 다음과 같습니다.

고유 번호 4: 2,500회 재생

고유 번호 1: 600회 재생

따라서 pop 장르의 [4, 1]번 노래를 먼저, classic 장르의 [3, 0]번 노래를 그다음에 수록합니다.

장르 별로 가장 많이 재생된 노래를 최대 두 개까지 모아 베스트 앨범을 출시하므로 2번 노래는 수록되지 않습니다.


풀이

이번문제로 Map / Set 추상 자료형 활용한 해시 테이블 문제 입니다.

 

장르별로 mapping이 필요한 -> Map

장르별로 mapping된 value는 계속 추가됨 -> ArrayList

재생 수 만큼 내림차순 정렬이 필요함 ArrayList sort

소스 코드

import java.util.*;

class Solution {
    public int[] solution(String[] genres, int[] plays) {
        /*
        문제풀이 개요
        장르별로 mapping이 필요한 -> Map
        장르별로 mapping된 value는 계속 추가됨 -> ArrayList
        재생 수 만큼 내림차순 정렬이 필요함 ArrayList sort
        */
                
        Map<String, List<int[]>> songsByGenre = new HashMap<>(); // genre -> [index, plays]
        Map<String, Integer> totalByGenre = new HashMap<>();     // genre -> total plays

        for (int i = 0; i < genres.length; i++) {
            String g = genres[i];
            songsByGenre.computeIfAbsent(g, k -> new ArrayList<>()).add(new int[]{i, plays[i]});
            totalByGenre.merge(g, plays[i], Integer::sum);
        }

        // 1) 장르를 총 재생 수 기준으로 내림차순 정렬
        List<String> genreOrder = new ArrayList<>(totalByGenre.keySet());
        genreOrder.sort((a, b) -> Integer.compare(totalByGenre.get(b), totalByGenre.get(a)));

        // 2) 각 장르 내에서 (재생수 내림차순, 인덱스 오름차순) 정렬 후 상위 2개 선택
        List<Integer> ans = new ArrayList<>();
        for (String g : genreOrder) {
            List<int[]> list = songsByGenre.get(g);
            list.sort((x, y) -> {
                if (x[1] != y[1]) return Integer.compare(y[1], x[1]); // plays desc
                return Integer.compare(x[0], y[0]);                   // index asc
            });
            for (int k = 0; k < Math.min(2, list.size()); k++) {
                ans.add(list.get(k)[0]); // 노래 인덱스만 수록
            }
        }

        // 3) 리스트 -> 배열
        return ans.stream().mapToInt(Integer::intValue).toArray();
    }
}

 

 

알아두면 좋은 점.

1. computeIfAbsent

V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
  • 의미: 맵에서 key에 해당하는 값이 없으면 mappingFunction을 실행해서 새 값을 만들어 넣고, 그 값을 리턴.
  • 있으면 그대로 리턴, 없으면 새로 계산해서 put.

예시:

Map<String, List<Integer>> map = new HashMap<>(); // "rock" 키가 없으므로 새 ArrayList 생성 후 추가됨 

map.computeIfAbsent("rock", k -> new ArrayList<>()).add(100); // 다시 호출하면 이미 있으므로 기존 리스트에 추가 
map.computeIfAbsent("rock", k -> new ArrayList<>()).add(200); System.out.println(map); // {rock=[100, 200]}

→ 네 코드에서는 songsByGenre.computeIfAbsent(g, k -> new ArrayList<>()) 로 장르별 리스트를 자동으로 초기화해주는 역할.


2. merge

V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)
  • 의미: 맵에 key가 없으면 value를 넣고,
    이미 있으면 remappingFunction으로 기존 값과 새 값을 합쳐서 갱신.
  • 보통 합계 누적, 문자열 연결 등에 활용.
Map<String, Integer> total = new HashMap<>();

// rock 키가 없으니 300으로 추가됨
total.merge("rock", 300, Integer::sum);

// rock 키가 이미 있으니 (300 + 500)으로 갱신됨
total.merge("rock", 500, Integer::sum);

System.out.println(total);	// {rock=800}

이번에 풀이한 "베스트 앨범" 문제도 기존에 풀어봤던 문제였습니다.

다만, 기존 풀이가 남아있지 않아서 다시 풀어볼 수 있었습니다.

기존에도 Map을 활용해서 문제를 풀었는데 이번에는 put / get 문법으로의 조합이아닌 Map 유틸의 computedIfAbsent / merge를 활해서 풀이를 했습니다.

Java 8에서 추가된 해당 문법은 활용도가 높은만큼 익숙해지면 다른 문제를 푸는데에 도움이 될 것 같습니다.

728x90
반응형

[하코테] Day3. 작심삼일 달성 (프로그래머스 "폰켓몬" 풀이)

Posted by Space_Jin
2025. 9. 10. 23:12 Programming/Algorithm
728x90
반응형

하코테 3일차 풀이를 끝냈습니다.

일단은 작심삼일은 무사히 달성했고 4일차까지는 꼭 진행해서 작심삼일을 극복해야할 것 같습니다.

5일차는 금요일이니만큼 달성할 수 있을지...

 


문제 설명

당신은 폰켓몬을 잡기 위한 오랜 여행 끝에, 홍 박사님의 연구실에 도착했습니다. 홍 박사님은 당신에게 자신의 연구실에 있는 총 N 마리의 폰켓몬 중에서 N/2마리를 가져가도 좋다고 했습니다.

홍 박사님 연구실의 폰켓몬은 종류에 따라 번호를 붙여 구분합니다. 따라서 같은 종류의 폰켓몬은 같은 번호를 가지고 있습니다. 예를 들어 연구실에 총 4마리의 폰켓몬이 있고, 각 폰켓몬의 종류 번호가 [3번, 1번, 2번, 3번]이라면 이는 3번 폰켓몬 두 마리, 1번 폰켓몬 한 마리, 2번 폰켓몬 한 마리가 있음을 나타냅니다. 이때, 4마리의 폰켓몬 중 2마리를 고르는 방법은 다음과 같이 6가지가 있습니다.

첫 번째(3번), 두 번째(1번) 폰켓몬을 선택

첫 번째(3번), 세 번째(2번) 폰켓몬을 선택

첫 번째(3번), 네 번째(3번) 폰켓몬을 선택

두 번째(1번), 세 번째(2번) 폰켓몬을 선택

두 번째(1번), 네 번째(3번) 폰켓몬을 선택

세 번째(2번), 네 번째(3번) 폰켓몬을 선택

이때, 첫 번째(3번) 폰켓몬과 네 번째(3번) 폰켓몬을 선택하는 방법은 한 종류(3번 폰켓몬 두 마리)의 폰켓몬만 가질 수 있지만, 다른 방법들은 모두 두 종류의 폰켓몬을 가질 수 있습니다. 따라서 위 예시에서 가질 수 있는 폰켓몬 종류 수의 최댓값은 2가 됩니다.

당신은 최대한 다양한 종류의 폰켓몬을 가지길 원하기 때문에, 최대한 많은 종류의 폰켓몬을 포함해서 N/2마리를 선택하려 합니다. N마리 폰켓몬의 종류 번호가 담긴 배열 nums가 매개변수로 주어질 때, N/2마리의 폰켓몬을 선택하는 방법 중, 가장 많은 종류의 폰켓몬을 선택하는 방법을 찾아, 그때의 폰켓몬 종류 번호의 개수를 return 하도록 solution 함수를 완성해주세요.

 

제한사항

nums는 폰켓몬의 종류 번호가 담긴 1차원 배열입니다.

nums의 길이(N)는 1 이상 10,000 이하의 자연수이며, 항상 짝수로 주어집니다.

폰켓몬의 종류 번호는 1 이상 200,000 이하의 자연수로 나타냅니다.

가장 많은 종류의 폰켓몬을 선택하는 방법이 여러 가지인 경우에도, 선택할 수 있는 폰켓몬 종류 개수의 최댓값 하나만 return 하면 됩니다.

 

입출력 예

nums result

[3,1,2,3] 2

[3,3,3,2,2,4] 3

[3,3,3,2,2,2] 2

 

입출력 예 설명

입출력 예 #1

문제의 예시와 같습니다.

 

입출력 예 #2

6마리의 폰켓몬이 있으므로, 3마리의 폰켓몬을 골라야 합니다.

가장 많은 종류의 폰켓몬을 고르기 위해서는 3번 폰켓몬 한 마리, 2번 폰켓몬 한 마리, 4번 폰켓몬 한 마리를 고르면 되며, 따라서 3을 return 합니다.

 

입출력 예 #3

6마리의 폰켓몬이 있으므로, 3마리의 폰켓몬을 골라야 합니다.

가장 많은 종류의 폰켓몬을 고르기 위해서는 3번 폰켓몬 한 마리와 2번 폰켓몬 두 마리를 고르거나, 혹은 3번 폰켓몬 두 마리와 2번 폰켓몬 한 마리를 고르면 됩니다. 따라서 최대 고를 수 있는 폰켓몬 종류의 수는 2입니다.


문제 풀이

N마리의 폰켓몬 목록이 주어졌을 때, N/2 마리의 폰켓몬을 가져가는데 가져갈 수 있는 최대 종류를 구하는 문제

해당 문제도 Day2의 의상문제와 같이 종류/타입 등이 주어지고 주어지는 값 중 중복 값을 효율적으로 제어하는 문제이다.

https://reinvestment.tistory.com/128

 

[하코테] Day2. 어쩌다 복습 (프로그래머스 "의상" 풀이)

하코테 이틀차 문제는 프로그래머스의 "의상" 을 받았습니다. 이전에 풀어본 문제가 기존 풀이가 남아있었고 다행히? 아직 정답으로 인정이 되고 있었습니다.새로 문제를 풀기보다는 이런 경우

reinvestment.tistory.com

의상의 조합을 구하는 문제(경우의 수)
조합의 경우, Map이나 Set 자료구조를 활용하는 경우가 많으므로 우선적으로 고려해 봅니다.
Map과 Set 특징을 고려해본다면,
Map의 경우, key / value를 가지고 있다는 점.
Set의 경우, 중복을  피하는데 특화되어있다는 점.


Day2 의상 풀이를 하면서 작성한 Map과 Set 자료구조의 구분을 고려해보면 폰켓몬 문제는 Set 자료구조를 사용하는 것이 효율적으로 판단하여 풀이하였습니다.

 

예시해설

만약 6자리의 폰켓몬이 nums로 주어진다면, 가져갈 수 있는 폰켓몬의 수 N/2는 3이다. 만약, 폰켓몬의 종류가 3보다 크다면 3종류의 폰켓몬을 가져갈 수 있지만 3보다 작은 종류만 있다면 해당 종류만큼만 가져갈 수 있다.

 

nums: [3,3,3,2,2,2]

return: 2

 

nums의 크기는 6이고 N/2는 3이다. 하지만, nums 안의 종류는 (2, 3) 두가지 뿐이다.

즉, 3마리의 폰켓몬을 가져갈 수 있지만, 최대 가져갈 수 있는 종류는 2와 3두개 뿐이다(2, 2, 3 조합 or 3, 3, 2 조합).

 

결과적으로 정답은 N/2와 nums 안의 종류의 크기 중 더 작은 값

 

소스 코드

import java.util.*;

class Solution {
    public int solution(int[] nums) {
        /*
        * 풀이
        * 골라야하는 종류의 수(N/2)의 최댓 값은 폰켓몬의 종류의 수와 같다.
        * 즉, N/2와 포켓몬 종류의 수 중 더 작은 것이 고를 수 있는 포켓몬 종류의 수이다.
        */
        
        int answer = 0;
        int M = nums.length / 2;    // 골라야하는 폰켓몬 수
        
        // 폰켓몬 종류를 담는 Set
        Set<Integer> set = new HashSet<>();
        
        // Set에 담기
        for(int num : nums) set.add(num);
        
        // 결과
        answer = Math.min(M, set.size());
        
        return answer;
    }
}

Set 자료구조를 활용하면 쉽게 풀 수 있는 문제였습니다.

다른 사람의 풀이를 보면 함수형 등의 풀이로 숏 코드, 화려한 혹은 간결한 코드로 작성된 풀이법이 많이 보입니다.

 

제 풀이처럼 투박하지만 이해하기 쉬운 코드와 조금 더 세련되고 깔끔한? 코드 중 기업에서 선호하는 스타일은 어떤 것일지?

그리고 어떤게 더 좋은 코드일지 늘 고민하게 됩니다.

728x90
반응형

[하코테] Day2. 어쩌다 복습 (프로그래머스 "의상" 풀이)

Posted by Space_Jin
2025. 9. 9. 23:27 Programming/Algorithm
728x90
반응형

하코테 이틀차 문제는 프로그래머스의 "의상" 을 받았습니다.

 

이전에 풀어본 문제가 기존 풀이가 남아있었고 다행히? 아직 정답으로 인정이 되고 있었습니다.

새로 문제를 풀기보다는 이런 경우는 복습의 기회로 삼는것도 좋을 것 같습니다.


문제 설명

코니는 매일 다른 옷을 조합하여 입는것을 좋아합니다.

예를 들어 코니가 가진 옷이 아래와 같고, 오늘 코니가 동그란 안경, 긴 코트, 파란색 티셔츠를 입었다면 다음날은 청바지를 추가로 입거나 동그란 안경 대신 검정 선글라스를 착용하거나 해야합니다.

종류 이름
얼굴 동그란 안경, 검정 선글라스
상의 파란색 티셔츠
하의 청바지
겉옷 긴 코트

 

코니는 각 종류별로 최대 1가지 의상만 착용할 수 있습니다. 예를 들어 위 예시의 경우 동그란 안경과 검정 선글라스를 동시에 착용할 수는 없습니다.

착용한 의상의 일부가 겹치더라도, 다른 의상이 겹치지 않거나, 혹은 의상을 추가로 더 착용한 경우에는 서로 다른 방법으로 옷을 착용한 것으로 계산합니다.

코니는 하루에 최소 한 개의 의상은 입습니다.

코니가 가진 의상들이 담긴 2차원 배열 clothes가 주어질 때 서로 다른 옷의 조합의 수를 return 하도록 solution 함수를 작성해주세요.

 

제한사항

clothes의 각 행은 [의상의 이름, 의상의 종류]로 이루어져 있습니다.

코니가 가진 의상의 수는 1개 이상 30개 이하입니다.

같은 이름을 가진 의상은 존재하지 않습니다.

clothes의 모든 원소는 문자열로 이루어져 있습니다.

모든 문자열의 길이는 1 이상 20 이하인 자연수이고 알파벳 소문자 또는 '_' 로만 이루어져 있습니다.

 

입출력 예

clothes return
[["yellow_hat", "headgear"], ["blue_sunglasses", "eyewear"], ["green_turban", "headgear"]]  5
[["crow_mask", "face"], ["blue_sunglasses", "face"], ["smoky_makeup", "face"]]  3

 

입출력 예 설명

예제 #1

headgear에 해당하는 의상이 yellow_hat, green_turban이고 eyewear에 해당하는 의상이 blue_sunglasses이므로 아래와 같이 5개의 조합이 가능합니다.

1. yellow_hat

2. blue_sunglasses

3. green_turban

4. yellow_hat + blue_sunglasses

5. green_turban + blue_sunglasses

 

예제 #2

face에 해당하는 의상이 crow_mask, blue_sunglasses, smoky_makeup이므로 아래와 같이 3개의 조합이 가능합니다.

1. crow_mask

2. blue_sunglasses

3. smoky_makeup

 


풀이

의상의 조합을 구하는 문제(경우의 수)

조합의 경우, Map이나 Set 자료구조를 활용하는 경우가 많으므로 우선적으로 고려해 봅니다.

 

Map과 Set 특징을 고려해본다면,

Map의 경우, key / value를 가지고 있다는 점.

Set의 경우, 중복을  피하는데 특화되어있다는 점.

 

위 문제의 경우, 의상의 종류를 특정해야한다는 점에서 Map을 선택하는 것이 유리해보였다. 그런 경우, 동일한 의상이 들어올 수 있는 경우를 생각해봐야하지만, "같은 이름을 가진 의상은 존재하지 않습니다."라는 조건으로 인해서 중복을 고려하지 않아도 된다.

즉, Map 자료구조를 사용하는 것이 좋을 것을 판단.

 

Key는 의상의 종류(type)이고 value는 의상 종류별 갯수(item의 갯수)로 정한다.

의상의 종류별 조합읜 모든 경우의 수로 판단할 수 있으므로 "의상 종류별 갯수의 곱"으로 생각할 수 있다.

착용을 안하는 경우도 고려해야하기 때문에 정확히는 "의상 종류별 갯수 + 1 의 곱"이 되겠다.

 

소스 코드

import java.util.*;

class Solution {
    public int solution(String[][] clothes) {
        // 의상별 종류 수 곱 - 1
        // ( (n1 + 1) * ... (n4 +1 ) ) - 1
        int answer = 1;
        
        // 의상 종류별 개수(중복없음)
        Map<String, Integer> map = new HashMap<>();
        
        // 의상 세팅
        for(String[] cloth : clothes) {
            String item = cloth[0];
            String type = cloth[1];
            
            map.put(type, map.getOrDefault(type, 0) + 1);
        }
        
        // 조합 구현
        for(int count : map.values()) {
            // 의상 타입별 갯수 + 1 의 곱
            answer *= (count + 1);
        }
        // 아무것도 착용하지 않음 예외처리
        answer -= 1;
        
        return answer;
    }
}

 

유의사항

해당 조합에는 의상의 종류별로 "미착용" 상태를 포함하고 있다. 즉, 모든 종류의 의상을 입지 않는 경우도 포함되어 있으므로 해당 경우를 예외처리한다.

(문제의 제한사항 "코니는 하루에 최소 한 개의 의상은 입습니다.")

 

+ Map 자료구조에서 사용할 수 있는 getOrDefault 메서드는 사용에 유리하므로 알아두면 좋다.

getOrDefault(key, defaultValue) => key가 존재하면, 해당 key에 맞는 value를 return하고 그렇지 않다면, defaultValue를 return 한다.

728x90
반응형

[하코테] Day1. 시작이 반 (프로그래머스 "전화번호 목록" 풀이)

Posted by Space_Jin
2025. 9. 8. 22:07 Programming/Algorithm
728x90
반응형

링크드 인에서 유연히 본 하코테 프로젝트에 무지성으로 신청을 해봤다.

 

하코테 프로젝트란 하루 매일 코딩테스트 루틴을 만들어주기 위해서 이메일로 문제를 보내주는 서비스입니다.

매일 참여로 신청을 했는데 얼마나 꾸준히할 수 있을지 모르겠지만 해보는데까지는 해보는걸로...

 

첫 번째 문제로 프로그래머스 "전화번호 목록"을 받았다.

 


문제 설명

전화번호부에 적힌 전화번호 중, 한 번호가 다른 번호의 접두어인 경우가 있는지 확인하려 합니다.
전화번호가 다음과 같을 경우, 구조대 전화번호는 영석이의 전화번호의 접두사입니다.

  • 구조대 : 119
  • 박준영 : 97 674 223
  • 지영석 : 11 9552 4421

전화번호부에 적힌 전화번호를 담은 배열 phone_book 이 solution 함수의 매개변수로 주어질 때, 어떤 번호가 다른 번호의 접두어인 경우가 있으면 false를 그렇지 않으면 true를 return 하도록 solution 함수를 작성해주세요.

제한 사항
  • phone_book의 길이는 1 이상 1,000,000 이하입니다.
    • 각 전화번호의 길이는 1 이상 20 이하입니다.
    • 같은 전화번호가 중복해서 들어있지 않습니다.
입출력 예제phone_bookreturn
["119", "97674223", "1195524421"] false
["123","456","789"] true
["12","123","1235","567","88"] false
입출력 예 설명

입출력 예 #1
앞에서 설명한 예와 같습니다.

입출력 예 #2
한 번호가 다른 번호의 접두사인 경우가 없으므로, 답은 true입니다.

입출력 예 #3
첫 번째 전화번호, “12”가 두 번째 전화번호 “123”의 접두사입니다. 따라서 답은 false입니다.


 

문제 풀이

import java.util.*;

class Solution {
    public boolean solution(String[] phone_book) {
        boolean answer = true;
        
        // 모두 set에 삽입
        Set<String> set = new HashSet<>(Arrays.asList(phone_book));
        
        // 접두어 확인
        for(String num : phone_book) {
            answer = isCorrect(set, num);
            if(answer == false) break;
        }
        
        return answer;
    }
    
    public boolean isCorrect(Set<String> set, String num) {
        int len = num.length();
        
        // 자기 자신은 포함해서는 안된다!
        for(int i = 1; i < len; i++) {
            String n = num.substring(0, i);
            
            if(set.contains(n)) return false;
        }
        
        return true;
    }
}

 

주의사항

1. 처음 Set에 모두 삽입한다.

처음 풀이를 할 때, Set에 모두 삽입을 하지 않고 접두어를 비교하였는데 이렇게 풀게되면 뒤에 들어오는 값이 앞의 값읠 접두어인지만 확인하다. 즉, 앞에 먼저 들어있는 값이 현재 값의 접두어인지는 비교할 수 없다.

 

2. isCorrect 함수에서 접두어 확인은 문자열 전체를 확인해서는 안된다. 문자열 전체는 자기자신이되며 처음 Set에 모든 값을 넣었기 때문에 반드시 Set에 들어있으므로 결과가 false가 된다.

728x90
반응형