알고리즘, 자료구조/프로그래머스

[Javascript] 프로그래머스 보석 쇼핑

jaee 2020. 7. 10. 23:27

문제

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

 

코딩테스트 연습 - 보석 쇼핑

["DIA", "RUBY", "RUBY", "DIA", "DIA", "EMERALD", "SAPPHIRE", "DIA"] [3, 7]

programmers.co.kr

 

 

풀이 과정

결론부터 말하면 이 문제는 단순 이중 for문으로 해결할 수 없다. 만약 이중 for문으로 해결하려고 한다면 정확성은 다 통과되지만 효율성에서 시간 초과될 것이다. 맨 처음 내가 작성한 코드는 아래와 같다. 

function solution(gems) {
    let result = [], minLen = 100000;
    let unique = [...new Set(gems)];
    let gemLen = gems.length, uniLen = unique.length;
    if(uniLen === 1){
        return [1, 1];
    }else if(gemLen === uniLen){
        return [1, gemLen];
    }
    for(let i = 0; i <= gemLen - uniLen; i++){
        if([...new Set(gems.slice(i))].length !== uniLen) break;
        let answer = [], values = [];
        values.push(gems[i]);
        answer.push(i);
        for(let j = i + 1; j < gemLen; j++){
            if(!values.includes(gems[j])){
                values.push(gems[j]);
                answer.push(j);
                let anLen = answer.length;
                if(anLen === uniLen){
                    if(answer[anLen - 1] - answer[0] < minLen){
                        minLen = answer[anLen - 1] - answer[0];
                        result[0] = answer[0] + 1;
                        result[1] = answer[anLen - 1] + 1;
                    }
                    break;
                }
            }
        }
    }
    return result;
}

정확성은 맞지만 효율적이지 않은 코드이다. 효율성을 증가시키기 위해 객체가 아닌 배열만 사용하고 리터럴 방식만 사용하려고 노력했으나 결과는 똑같았다. 그래서 찾아본 게 '슬라이딩 윈도우 알고리즘'이다.

 

슬라이딩 윈도우 알고리즘이란?

윈도우를 한 칸 옮기면, n - 1칸은 겹친다는 게 슬라이딩 윈도우의 기본 아이디어. 이를 적용해 수정한 코드는 아래와 같다.

function solution(gems) {
    let result = [], minLen = 100000;
    let checkMap = new Map();
    // 중복되는 보석 제거
    let kind = new Set(gems);
    let curr = new Map(), s = 0;
  
	// 보석 진열장 처음부터 끝까지 순환
    for(let i = 0; i < gems.length; i++){
        // 만약 checkMap에 특정 보석이 없다면
        if(!checkMap.has(gems[i])){
            // 특정 보석을 key로, 빈 배열을 value로 생성해주고
            checkMap.set(gems[i], []);
        }
        // 특정 보석을 key로 갖는 배열에 인덱스를 추가해준다
        checkMap.get(gems[i]).push(i);
        // caseArr의 key에 인덱스를, value에는 보석 이름을 추가해준다
        caseArr.set(i, gems[i]);
        // 만약 checkMap에 모든 보석의 종류가 추가되어있다면
        if(kind.size === checkMap.size){
            // checkMap에서 gems의 0번째부터 1씩 증가시키며, 
            // value 길이가 1이 될 때까지 배열의 요소를 삭제한다
            while(checkMap.get(gems[s]).length > 1){
                checkMap.get(gems[s]).shift();
                // caseArr에서도 0부터 1씩 증가시키며 key를 삭제한다
                caseArr.delete(s);
                s++;
            }
            // caseArr의 크기가 minLen보다 작다면
            if(caseArr.size < minLen){
                // minLen을 caseArr.size로 변경시켜 최소 길이를 바꿔주고
                minLen = caseArr.size;
                // result에 시작점 + 1, 시작점 + 최소 길이를 저장한다
                result[0] = s + 1;
                result[1] = s + minLen;
            }
            // 슬라이딩 윈도우 알고리즘에 맞춰 시작점을 한 칸 증가시켜주고,
            s++;
            // checkMap에서 이전 시작점에서 보석에 대한 value중, 먼저 저장된 인덱스를 제거한다
            checkMap.get(gems[s - 1]).shift();
            // 제거한 결과, 보석의 value값인 배열이 비었다면
            if(checkMap.get(gems[s - 1]).length === 0){
            	// checkMap에서 해당 보석을 삭제한다
                checkMap.delete(gems[s - 1]);
            }
            // caseArr에서도 이전 시작점을 제거한다
            caseArr.delete(s - 1);
        }
    }

    return result;
}

Map 객체를 사용한 이유는 배열을 사용하면 매번 인덱스를 확인해야되지만, Map 객체는 key를 알면 값을 찾을 때 시간이 단축되기 때문이다. 또한 Map 객체 타입이 가진 여러가지 특징 중 1) size 속성으로 객체 크기를 쉽게 알 수 있고, 2) 객체에서 키 - 값을 삭제하거나 추가할 경우 일반 Object 객체보다 성능이 좋기 때문에 이를 잘 활용하면 시간 단축을 할 수 있다.

 

Map

Map 객체는 키-값 쌍을 저장하며 각 쌍의 삽입 순서도 기억하는 콜렉션입니다.

developer.mozilla.org

 

 

테스트 케이스 추가