부트캠프 15일차 후기
오늘은 안타깝게도 CodeKata에서 2문제 밖에 풀지 못했다.
첫 번째 문제에서 시간이 조금 끌리긴 했지만 가장 근본적인 원인은 두 번째 문제인 "63. 숫자 짝꿍" 때문이다.
https://school.programmers.co.kr/learn/courses/30/lessons/131128
프로그래머스
코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.
programmers.co.kr
나는 각각 X, Y 에서 나타나는 숫자의 갯수를 저장하고 9에서 부터 역순으로 0까지 X와 Y에 해당 숫자가 있다면 각각 나타난 수의 최솟값 만큼 answer에 문자열로써 더하는 방식으로 문제를 해결하려했다. 그리고 채점을 버튼을 누르고 "맞았습니다"가 많은 것을 확인하고 바로 다음 문제를 풀 준비를 하고 결과를 확인하니, 중간에 "시간 초과"가 떠서 틀리고 말았다.
왜 시간 초과가 되는지 이해가 안 되어서 튜터님께도 물어보고, C++로도 코드를 다시 짜보기도 했다. 그렇게 많은 시도 끝에 문제를 해결하였다. 사실 나는 문제해결 방안을 이미 알고 있었지만 그 해결방안을 제대로 하지 못해서 끙끙 앓는 상태로 이어졌던 것이여서 허무한 기분이 들기도하다.
해결책을 떠올린 계기는 튜터님이 제안한 방식이 실패했던 것과 C++로 다시 짜본 코드가 통과 된다는 것이였다. 튜터님이 제시한 방식은 더 비효율적이였고, C++로 제출해보니 동일 코드가 무사히 통과가 된 것을 본 나는 언어의 성능 문제여서 해결을 할 수 없나는 비관적인 생각해 사로잡혔다. 그 때 내가 시도했던 방식 중에 잘못된 방식이 있는건지 의심이 들기 시작했다. 가장 의심스러웠던 것은 StringBuilder를 사용하는 방식이였다. 비교적 초기에 사용을 시도했다가 실패했었던 방법이였고, 튜터님에게 이 방식을 사용했봤었다고 보고를 했었는데, 튜터님도 명확한 답이 나오지 않은 것을 보니, 내가 이것을 잘못 사용했었던게 아닌가? 란 의심이 싹텄다. 그래서 다시 한 번 StringBuilder를 구글링 한 결과, 아니나 다를까 잘못 사용하고 그냥 넘어갔었다. 그래서 제대로 사용을 하고 코드를 제출해본 결과, 통과가 되었다. 정말로 어이가 없는 결말이 아닐 수 없다..
그래서 String에서 내가 아는 정보를 다시 정리해고자 한다. String은 불변의 문자열이라서 문자열을 추가하는 등의 작업은 메모리를 다시 할당해야해기에 성능이 많이 좋지 않다. 그래서 문자열을 자주 바꾼다면 StringBuilder나 StringBuffer를 사용하는 것이 훨씬 효율적이다. StringBuilder는 쓰레드 세이프하지 않고, StringBuffer는 쓰레드 세이프 하다는 차이점이 있다. 그렇기에 StringBuilder가 성능이 더 좋다.
밑의 코드가 내가 구성한 코드이다.
var xn = IntArray(10)
var yn = IntArray(10)
fun solution(X: String, Y: String): String {
var answer = StringBuilder("")
val z = '0'.toInt()
for(c in X)
{
++xn[c.toInt() - z]
}
for(c in Y)
{
++yn[c.toInt() - z]
}
for(n in 9 downTo 0)
{
if(xn[n] == 0 || yn[n] == 0) continue
if(n == 0 && answer.toString() == "")
return "0"
var v = n.toString()
var L = xn[n].coerceAtMost(yn[n])
for(i in 1 .. L)
{
answer.append(v)
}
}
if(answer.toString() == "") return "-1"
return answer.toString()
}
}
여기서 튜터님이 제안했던 키워드들을 되돌아보도록하자. 비록 실패한 방법이지만, 나중에 사용해볼만한 매력적인 키워드들이라서 정리해보려고 한다. 튜터님이 제안하신 것은 GroupingBy와 eachCount이다. GroupingBy는 Collection을 Map의 형태로 그룹화를 하는 메소드이고 eachCount는 그룹화한 것의 갯수를 세는 함수이다. 밑의 코드는 내가 튜터님의 제안을 바탕으로 내가 짰었던 코드이다.
var xn = IntArray(10)
var yn = IntArray(10)
fun solution(X: String, Y: String): String {
var answer = StringBuilder("")
val list1 = X.groupingBy {ch -> ch}.eachCount();
val list2 = Y.groupingBy {ch -> ch}.eachCount();
val f = list2.filter { list1.containsKey(it.key)}.toList().sortedByDescending { it.first }
for(n in f)
{
for(r in 1 .. list1[n.first]!!.coerceAtMost(list2[n.first]!!))
answer.append(n.first)
}
if(answer.toString() == "") return "-1"
else if(answer.toString()[0] == '0') return "0"
return answer.toString()
}
}
해당 코드도 통과되지만 위의 코드보단 비효율적이다. 그래도 좋은 메소드를 알게 되었고, 그렇게까지 헛된 고생은 아니었다. GroupingBy와 eachCount에 대한 자세한 내용이 기억이 안 날땐
Kotlin 콜렉션 정리 프로젝트1 - groupingBy
Kotlin Collection에는 라는 확장함수가 있습니다. 배열이나 콜렉션에 대해 을 이용해서 그룹화와 폴드 연산을 하고, 그룹화된 소스를 반환합니다(먼소리야🤔).. 먼말인지 감이 안오니 원형을 보져!!
velog.io
여기서 다시 공부해보도록 해보자.
그렇게 문제를 해결하고 나서 "키오스크 프로젝트"의 해설 강의 영상을 훑어보았고, kotlin에서 사용하기 괜찮아보이는 메소드를 몇 개 발견하여서 여기에 적으려고 한다.
첫째는 fold이다. 이 메소드를 활용하면 컬렉션의 요소를 누적해서 반환하는 메소드이다. 덤으로 사용처가 비슷한 reduce가 있다. 둘의 차이점은 초기값이다. fold는 초기값을 정할 수 있으나 reduce는 초기값이 컬렉션의 첫번째 요소이다.
그래서 reduce는 빈 컬렉션에서 런타임 에러를 일으켜서 fold에 비해 불안정한 면이 있다. 왠만하면 fold를 사용하도록 하자.
val numbers = arrayOf(1, 2, 3, 4, 5)
val sum = numbers.fold(0) {
total, num -> total + num
}
// 15
println(sum)
다음은 repeat이다. 기존에 특수한 횟수를 반복을 하기 위해선 for(i in 0 .. N - 1) {}이런 방식을 택하였는데,
repeat(반복 횟수, 반복할 행동) 이렇게 사용함으로써 가독성이고 편리하게 코드를 짤 수 있다.
그리고 또 repeat인데, 이번엔 String의 메소드 repeat이다. 이 메소드는 해당 String을 반복하는 String을 반환한다.
// abcabcabcabcabc
println("abc".repeat(5));
사실 위의 "63. 숫자 짝꿍" 도 반복문을 사용하지 않고 이 repeat을 사용했다면 더 가독성이 좋았을지도 모르겠다.
이제 다음주부터 조도 바뀌고, Spring에 돌입하는 것 같다. 앞으로 어떻게 될지 잘 모르겠지만, 만일을 대비해 주말엔 푹 쉬도록 하자.