Today
-
Yesterday
-
Total
-
  • [프로그래머스] 베스트앨범 (Kotlin)
    문제풀이/프로그래머스 2021. 6. 29. 22:45

    문제 링크

     

    코딩테스트 연습 - 베스트앨범

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

    programmers.co.kr

     

    내 코드 (failed)

    파이썬으로 이미 풀어봤던 문제기 때문에 위풍당당하게 쭉쭉 코드를 적어나갔으나.... 여지없이 나를 붙잡는 UNSAFE_CALL 과 TYPE_MISMATCH.

    일단 기록해둔 뒤 나중에 문제점을 살펴보겠습니다.

     

    class Solution {
        fun solution(genres: Array<String>, plays: IntArray): IntArray {
            var answer = intArrayOf()
            val genre_play = HashMap<String, Int>()
            val genre_most = HashMap<String, Array<Int>>()
            for (i in 0 until genres.size) {
                genre_play[genres[i]]?.let { genre_play.put(genres[i], it + plays[i]) } ?: genre_play.put(genres[i], plays[i])
                genre_most[genres[i]]?.let {
                    if (plays[i] > plays[it[0]]) {
                        genre_most[genres[i]]
                        !!.set(1, it[0])
                        !!.set(0, i)
                    } else {
                        genre_most[genres[i]][1]?.let {
                            if (plays[genre_most[genres[i]][1]] < plays[i] ) {
                                genre_most[genres[i]]!!.set(1, i)
                            }
                        } ?: genre_most[genres[i]]!!.set(1, i)
                    }
                } ?: genre_most.put(genres[i], arrayOfNulls<Int>(2).let { it.set(0, i); it })
            }
            for (A in genre_play) {
                answer.add(genre_most[A[0]][0])
                genre_most[A[0]][1]?.let { answer.add(it) }
            }
            return answer
        }
    }

     

     

    다른 사람의 코드

    class Solution {
        fun solution(genres: Array<String>, plays: IntArray): IntArray {
            return genres.indices.groupBy { genres[it] }
                .toList()
                .sortedByDescending { it.second.sumBy { plays[it] } }
                .map { it.second.sortedByDescending { plays[it] }.take(2) }
                .flatten()
                .toIntArray()
        }
    }

    이것이 함수형 프로그래밍인가...? 😢

    하나씩 뜯어봅시다.

     

    genres: Array<String>과 plays: IntArray는 다음과 같은 형태로 입력됩니다.

    (아래에서부터 이 예시로 입력된다고 가정하고 설명하겠습니다.)

    genres => ["classic", "pop", "classic", "classic", "pop"]

    plays => [500, 600, 150, 800, 2500]

     

    그리고 그 뒤의 체인 프로퍼티&메서드를 하나씩 살펴보면 다음과 같습니다.

     

    indices

    val <T> Array<out T>.indices: IntRange

    Collection, Array 클래스에 선언되어있는 IntRange형 프로퍼티입니다.

    genres.indices는 genres의 크기만큼의 범위를 반환합니다.

    즉, 0..(genres.size - 1) (또는 0 until genres.size) 를 반환합니다.

     

     

    groupBy

    inline fun <T, K> Iterable<T>.groupBy(keySelector: (T) -> K): Map<K, List<T>>

    Iterable한 클래스들(IntRange, Array, Collection, List, ...)의 확장함수로 사용될 수 있는, 반환타입 Map<K, List<T>> 인 메서드입니다.

    지금과 같이 하나의 파라미터 genres[it]를 받을 시, 본인의 객체(it)의 원소들을 파라미터 genres[it]을 기준으로 분류하여 그룹화합니다.

    class Solution {
        fun solution(genres: Array<String>, plays: IntArray): IntArray { 
            return genres.indices.groupBy { genres[it] }  // {classic=[0,2,3], pop=[1,4]} Type : Map
                .toList()
                .sortedByDescending { it.second.sumBy { plays[it] } }
                .map { it.second.sortedByDescending { plays[it] }.take(2) }
                .flatten()
                .toIntArray()
        }
    }

    이는 즉 (0..4).groupBy { genres[it] } 이고, it에는 0부터 4까지의 정수가 순서대로 들어가게 됩니다.

     

    genres[0]은 "classic"이므로, map : { "classic" : [0] } 이 됩니다.

    genres[1]은 "pop"이므로, map : { "classic" : [0], "pop" : [1] } 이 됩니다.

    genres[2]는 "classic"이므로, map : { "classic" : [0, 2], "pop" : [1] } 이 됩니다.

    genres[3]는 "classic"이므로, map : { "classic" : [0, 2, 3], "pop" : [1] } 이 됩니다.

    genres[3]는 "pop"이므로, map : { "classic" : [0, 2, 3], "pop" : [1, 4] } 가 됩니다.

     

    최종적으로, { "classic" : [0, 2, 3], "pop" : [1, 4] } 의 map을 반환합니다.

     

     

    toList()

    fun <K, V> Map<out K, V>.toList(): List<Pair<K, V>>

    toList() 메서드는 Array의 확장함수로 쓸 때와, Map의 확장함수로 쓸 때가 다르게 작용합니다.

    Array.toList() 는 단순히 Array의 모든 원소를 복사하여 List타입으로 재구성합니다.

    Map.toList() 는 각 Key와 Value를 Pair쌍으로 묶어서 List타입으로 재구성합니다.

    class Solution {
        fun solution(genres: Array<String>, plays: IntArray): IntArray { 
            return genres.indices.groupBy { genres[it] } // {classic=[0,2,3], pop=[1,4]} Type : Map
                .toList() // [(classic, [0,2,3]),(pop,[1,4])] Type : List
                .sortedByDescending { it.second.sumBy { plays[it] } }
                .map { it.second.sortedByDescending { plays[it] }.take(2) }
                .flatten()
                .toIntArray()
        }
    }

    ※ Pair란?

    파이썬의 튜플과 같이 데이터를 쌍으로 묶은 데이터입니다. 실제로 Kotlin M3 이전 버전까지는 tuple 자료형이 있었으나, Pair와 Triple로 Migrate되어서 이제 tuple은 사용되고 있지 않습니다.

    즉, { "classic" : [0, 2, 3], "pop" : [1, 4] }.toList() == [ ("classic", [0, 2, 3]), ("pop", [1, 4]) ] (type: List<Pair<String, List<Int>>>) 가 됩니다.

     

     

    sortedByDescending()

    inline fun <T, R : Comparable<R>> Iterable<T>.sortedByDescending(crossinline selector: (T) -> R?): List<T>

    이건 메서드 이름만 봐도 알 수 있죠. 파라미터를 기준으로 데이터를 내림차순으로 정렬합니다.

    class Solution {
        fun solution(genres: Array<String>, plays: IntArray): IntArray { 
            return genres.indices.groupBy { genres[it] } // {classic=[0,2,3], pop=[1,4]} Type : Map
                .toList() // [(classic, [0,2,3]),(pop,[1,4])] Type : List
                .sortedByDescending { it.second.sumBy { plays[it] } } // [(pop, [1,4]), (classic, [0,2,3])] Type : List
                .map { it.second.sortedByDescending { plays[it] }.take(2) }
                .flatten()
                .toIntArray()
        }
    }

    파라미터로 쓰인 it.second.sumBy()는 앞 과정까지 구한 [ ("classic", [0, 2, 3]), ("pop", [1, 4]) ] (type: List)의 두번째 element인 [0, 2, 3]과 [1, 4]가 각각 파라미터로 들어가 plays를 참조하여 합을 리턴합니다.

    즉, [0, 2, 3]에 대해 500 + 150 + 800 = 1450, [1, 4]에 대해 600 + 2500 = 3100을 리턴합니다.

     

    pop키의 데이터인 [1, 4]의 sumBy가 더 크므로, sortedByDescending 메서드를 통해 다음과 같이 정렬됩니다.

     

    (정렬 전) [ ("classic", [0, 2, 3]), ("pop", [1, 4]) ] (type: List)

    (정렬 후) [ ("pop", [1, 4]), ("classic", [0, 2, 3]) ] (type: List)

     

    class Solution {
        fun solution(genres: Array<String>, plays: IntArray): IntArray { 
            return genres.indices.groupBy { genres[it] } // {classic=[0,2,3], pop=[1,4]} Type : Map
                .toList() // [(classic, [0,2,3]),(pop,[1,4])] Type : List
                .sortedByDescending { it.second.sumBy { plays[it] } } // [(pop, [1,4]), (classic, [0,2,3])] Type : List
                .map { it.second.sortedByDescending { plays[it] }.take(2) } // [[4,1],[3,0]] Type : List
                .flatten()
                .toIntArray()
        }
    }

    위에서 sortedByDescending과 sumBy를 이해하셨다면, 다음 line의 이해에도 무리가 없을 것입니다.

    다만 map함수는 이름과 다르게, 객체의 각 iterable의 값을 변경하여 새로운 List를 반환합니다.

    take함수인자의 개수만큼만 iterable 객체의 원소를 앞에서부터 List로 반환합니다.

     

    즉, it.second.sortedByDescending { plays[it] } == [4,1] , [3, 0, 2]

    [4, 1].take(2) == [4, 1]

    [3, 0, 2].take(2) == [3, 0]

    map { [4, 1] and [3, 0] } == [[4, 1], [3, 0]] List<List<Int>>

     

     

    flatten()

    fun <T> Iterable<Iterable<T>>.flatten(): List<T>

    List안에 List가 존재할 경우 이를 펼쳐놓아주는(flattening) 메서드입니다.

    즉, [[4, 1], [3, 0]].flatten() == [4, 1, 3, 0]

     

    답이 구해졌네요. 다만 지금 이 리스트의 타입이 List<Int>이므로, toIntArray를 통해 IntArray타입으로 변환해준 뒤 반환합니다.

    class Solution {
        fun solution(genres: Array<String>, plays: IntArray): IntArray { 
            return genres.indices.groupBy { genres[it] } // {classic=[0,2,3], pop=[1,4]} Type : Map
                .toList() // [(classic, [0,2,3]),(pop,[1,4])] Type : List
                .sortedByDescending { it.second.sumBy { plays[it] } } // [(pop, [1,4]), (classic, [0,2,3])] Type : List
                .map { it.second.sortedByDescending { plays[it] }.take(2) } // [[4,1],[3,0]] Type : List
                .flatten() // [4, 1, 3, 0] Type : List
                .toIntArray() // [4, 1, 3, 0] Type : IntArray
        }
    }
sangilyoon.dev@gmail.com