개발/알고리즘

[프로그래머스][월간 코드 챌린지 시즌1] 스타수열 문제 해결 (Java) Level 3

nova_dev 2021. 1. 10. 16:07
반응형

알고리즘
[프로그래머스][월간 코드 챌린지 시즌1] 스타수열 문제 해결 (Java) Level 3

https://programmers.co.kr/learn/courses/30/lessons/70130

 

코딩테스트 연습 - 스타 수열

 

programmers.co.kr

 문제 설명

스타 수열의 최대 길이 갯수를 구하는 문제입니다.

수열이 {x[0], x[1]}, {x[2], x[3]}, ..., {x[2n-2], x[2n-1]} 라고 가정하자.
이 때 아래 조건을 만족한 수열을 스타 수열이라고 부른다.

스타 수열의 조건1. x의 길이가 2이상의 짝수이다.
스타 수열의 조건2. {x[0], x[1]}, {x[2], x[3]}, ..., {x[2n-2], x[2n-1]}의 교집합의 원소 갯수가 1 이상이다.
스타 수열의 조건3. x[0] != x[1], x[2] != x[3], ..., x[2n-2] != x[2n-1]이다.

 문제 해결 방법1

  1. map에 교집합으로 사용할 수 있을만한 key값을 찾기 위해 각 숫자가 호출된 횟수를 저장합니다.
  2. keyset을 순회하면서 스타 수열의 최댓값을 찾습니다.
    • a[]를 돌면서 앞과 뒤에 교집합인 값이 있는지, 숫자가 같지는 않은지 확인하고 스타 수열 조건에 해당할 경우 스타수열의 길이인 star값을 2 올립니다.
    • start값과 answer를 비교해서 최대 값을 저장합니다.
  3. answer를 반환합니다.
import java.util.HashMap;
import java.util.Map;

class Solution {
    public int solution(int[] a) {
        int answer = 0;
        Map<Integer, Integer> map = new HashMap<>(a.length);

        for (int i = 0; i < a.length; i++) {
            if (map.get(a[i]) == null) {
                map.put(a[i], 1);
            } else {
                map.replace(a[i], map.get(a[i]) + 1);
            }
        }

        for (int key : map.keySet()) {
            if (map.get(key) * 2 <= answer) continue;
            int star = 0;
            for (int j = 0; j < a.length - 1; j++) {
                if ((a[j] == key || a[j + 1] == key) && (a[j] != a[j + 1])) {
                    star += 2;
                    j++;
                }
            }
            answer = Math.max(answer, star); // 스타 수열 최대값
        }
        return answer;
    }
}

정답도 맞고, 효율성 테스트도 통과했지만 성능이 그렇게 좋지는 않습니다.
처음에 문제를 풀었을 때에는 map의 캐패시티를 초기화 하지 않았었는데, 그 때는 성능이 1번과 같았고, 초기화를 한 경우는 2번과 같은 성능을 보였습니다. 이 이유는 map을 초기화 할 때, initialCapacity값을 주지 않는다면 초기 용량이 16으로 지정되고, 임계 값에 도달할 때마다 두 배가 되는 식으로 사이즈를 조정하기 때문에 성능에 영향을 미칩니다.

 Map<Integer, Integer> map = new HashMap<>(a.length);
더보기

세부 설명

해시맵의 최초 버킷 값(initialCapacity)은 16과 같고, 부하율(loadFactor)은 0.75입니다. 
부하율(Load Factor) = 해시 맵의 항목 수 (m) / 해시 맵의 버킷 수(n)이기 때문에 초기값을 지정하지 않았을 경우 해시 값의 항목이 늘어날 때마다 (m이 증가할 때마다) 부하율을 확인하고, 부하율보다 클 경우 버킷을 두배로 늘리는 작업(resize)을 하게 됩니다. (예) 0.75 < m / 16 일 경우, resize

HashMap 코드 상에서 putMapEntries메서드에서 위 작업을 하게 됩니다.

HashMap의 putMapEntries 메서드

즉, map에 값을 넣을 때마다 이런 값들을 체크해야 하고, 맵의 크기가 일정 사이즈 이상 되면 resize()하는 과정이 추가되기 때문에 상대적으로 성능이 나오지 않을 수밖에 없습니다.

 

1번 (map initialCapacity초기화 안할 때) 2번 (map initialCapacity 초기화 할 때)
테스트 1 〉 | 통과 (0.09ms, 52MB)
테스트 2 〉 | 통과 (0.07ms, 51.9MB)
테스트 3 〉 | 통과 (0.07ms, 51.9MB)
테스트 4 〉 | 통과 (0.10ms, 52MB)
테스트 5 〉 | 통과 (0.08ms, 52.7MB)
테스트 6 〉 | 통과 (0.09ms, 52.1MB)
테스트 7 〉 | 통과 (0.10ms, 51.9MB)
테스트 8 〉 | 통과 (0.20ms, 52.8MB)
테스트 9 〉 | 통과 (0.10ms, 51.9MB)
테스트 10 〉 | 통과 (0.47ms, 53.4MB)
테스트 11 〉 | 통과 (0.16ms, 52.4MB)
테스트 12 〉 | 통과 (0.14ms, 51.7MB)
테스트 13 〉 | 통과 (93.85ms, 80.2MB)
테스트 14 〉 | 통과 (106.24ms, 91MB)
테스트 15 〉 | 통과 (111.76ms, 91MB)
테스트 16 〉 | 통과 (133.34ms, 93.3MB)
테스트 17 〉 | 통과 (192.89ms, 88.7MB)
테스트 18 〉 | 통과 (67.35ms, 69.8MB)
테스트 19 〉 | 통과 (141.78ms, 85MB)
테스트 20 〉 | 통과 (347.02ms, 103MB)
테스트 21 〉 | 통과 (303.73ms, 103MB)
테스트 22 〉 | 통과 (317.78ms, 103MB)
테스트 23 〉 | 통과 (225.37ms, 98.5MB)
테스트 24 〉 | 통과 (292.07ms, 103MB)
테스트 25 〉 | 통과 (271.63ms, 103MB)
테스트 26 〉 | 통과 (257.83ms, 102MB)
테스트 27 〉 | 통과 (202.27ms, 88.9MB)
테스트 28 〉 | 통과 (0.10ms, 52.8MB)
테스트 1 〉 | 통과 (0.07ms, 52.4MB)
테스트 2 〉 | 통과 (0.08ms, 52.6MB)
테스트 3 〉 | 통과 (0.07ms, 52MB)
테스트 4 〉 | 통과 (0.13ms, 52.4MB)
테스트 5 〉 | 통과 (0.08ms, 52.9MB)
테스트 6 〉 | 통과 (0.10ms, 52.4MB)
테스트 7 〉 | 통과 (0.09ms, 52.1MB)
테스트 8 〉 | 통과 (0.09ms, 51.9MB)
테스트 9 〉 | 통과 (0.12ms, 52.5MB)
테스트 10 〉 | 통과 (0.11ms, 52.2MB)
테스트 11 〉 | 통과 (0.16ms, 52.8MB)
테스트 12 〉 | 통과 (0.16ms, 52.5MB)
테스트 13 〉 | 통과 (108.81ms, 84.7MB)
테스트 14 〉 | 통과 (99.97ms, 92MB)
테스트 15 〉 | 통과 (112.27ms, 93.2MB)
테스트 16 〉 | 통과 (142.51ms, 93.3MB)
테스트 17 〉 | 통과 (161.52ms, 87.9MB)
테스트 18 〉 | 통과 (55.06ms, 66.3MB)
테스트 19 〉 | 통과 (122.73ms, 84.9MB)
테스트 20 〉 | 통과 (251.55ms, 102MB)
테스트 21 〉 | 통과 (262.48ms, 102MB)
테스트 22 〉 | 통과 (246.94ms, 101MB)
테스트 23 〉 | 통과 (195.73ms, 97.4MB)
테스트 24 〉 | 통과 (222.22ms, 101MB)
테스트 25 〉 | 통과 (245.02ms, 101MB)
테스트 26 〉 | 통과 (202.33ms, 100MB)
테스트 27 〉 | 통과 (159.93ms, 86.8MB)
테스트 28 〉 | 통과 (0.12ms, 51.7MB)

 

 문제 해결 방법2

  1. array에 교집합으로 사용할 수 있을만한 key값을 찾기 위해 각 숫자가 호출된 횟수를 저장합니다.
  2. keyset을 순회하면서 스타 수열의 최댓값을 찾습니다.
    • a[]를 돌면서 앞과 뒤에 교집합인 값이 있는지, 숫자가 같지는 않은지 확인하고 스타 수열 조건에 해당할 경우 스타수열의 길이인 star값을 2 올립니다.
    • start값과 answer를 비교해서 최대 값을 저장합니다.
  3. answer를 반환합니다.

class Solution {
    public int solution(int[] a) {
        int answer = 0;

        int[] count = new int[a.length + 1]; // a길이 미만의 수
        for (int i = 0; i < a.length; i++) {
            count[a[i]]++;
        }

        for (int i = 0; i<count.length; i++) {
            if (count[i] * 2 <= answer) continue;
            int star = 0;
            for (int j = 0; j < a.length - 1; j++) {
                if ((a[j] == i || a[j + 1] == i) && (a[j] != a[j + 1])) {
                    star += 2;
                    j++;
                }
            }
            answer = Math.max(answer, star); // 스타 수열 최대값
        }
        return answer;
    }
}​

방법은 위와 같지만, count 갯수를 저장할 때, array에 저장하고 있습니다.
사실 위 1번 문제 해결 방식과 같이 map을 이용하지 않아도, 1~n까지의 값이 몇번 호출되는지를 확인하면 되기 때문에, 배열에 저장하고 순회를 하는 것이 훨씬 더 빠르게 값에 접근할 수 있습니다.

성능 (count array로 만들 때)
테스트 1 〉 | 통과 (0.03ms, 52.3MB)
테스트 2 〉 | 통과 (0.04ms, 53.6MB)
테스트 3 〉 | 통과 (0.03ms, 52.6MB)
테스트 4 〉 | 통과 (0.04ms, 52MB)
테스트 5 〉 | 통과 (0.03ms, 52.3MB)
테스트 6 〉 | 통과 (0.05ms, 52.6MB)
테스트 7 〉 | 통과 (0.04ms, 53MB)
테스트 8 〉 | 통과 (0.04ms, 52.9MB)
테스트 9 〉 | 통과 (0.07ms, 53.5MB)
테스트 10 〉 | 통과 (0.03ms, 52.2MB)
테스트 11 〉 | 통과 (0.05ms, 52.3MB)
테스트 12 〉 | 통과 (0.09ms, 51.5MB)
테스트 13 〉 | 통과 (17.76ms, 67.5MB)
테스트 14 〉 | 통과 (20.86ms, 74.7MB)
테스트 15 〉 | 통과 (24.84ms, 73.8MB)
테스트 16 〉 | 통과 (25.61ms, 74.6MB)
테스트 17 〉 | 통과 (19.64ms, 69.1MB)
테스트 18 〉 | 통과 (10.29ms, 59.5MB)
테스트 19 〉 | 통과 (19.84ms, 64.1MB)
테스트 20 〉 | 통과 (23.46ms, 87.2MB)
테스트 21 〉 | 통과 (25.92ms, 87.2MB)
테스트 22 〉 | 통과 (23.98ms, 88.6MB)
테스트 23 〉 | 통과 (21.59ms, 71MB)
테스트 24 〉 | 통과 (23.15ms, 86.7MB)
테스트 25 〉 | 통과 (24.18ms, 75.9MB)
테스트 26 〉 | 통과 (23.69ms, 72.8MB)
테스트 27 〉 | 통과 (20.33ms, 68.8MB)
테스트 28 〉 | 통과 (0.04ms, 51.6MB)

마무리

map의 keyset을 순회하는 속도와, 배열의 값에 접근하는건 역시 성능적으로 크게 차이가 나네요. 순회해야 하는 값이 일정하게 정해져 있고 숫자라면 map을 쓰는 것보단 배열을 쓸 수 있도록 어떤 자료구조가 더 좋은 방법일지 항상 생각하는 습관을 가져야겠습니다.

데이터의 크기가 클 경우 map을 초기화 할 때 테이블의 용량이 커지면 배열 크기를 현재 크기의 2배로 재산정하여 동적 확장하는데 시간이 걸리기 때문에 initialCapacity를 주는 것도 중요하네요.

 

관련글

http://wonwoo.ml/index.php/post/1728 
(HashMap의 해시충돌과 해시 동적확장 관련 포스트)

 

 
 

 

 

 

 

반응형