문제 : https://leetcode.com/problems/count-and-say/
재귀적으로 연속되는 숫자의 개수를 세는 문제이다.
연속된 경우에만 카운트하는 걸 주의해야 한다. 나도 처음에는 전체 문자열에서의 숫자로 착각했었다.
주어진 숫자를 앞에서부터 순서대로 "연속된 숫자의 개수 + 연속된 숫자" 이렇게 2자리의 문자열로 바꿔나간다.
예를들어 "222334552" 가 주어진다면
처음에 2가 연속 3개 있으므로 32
그 다음 3이 연속 2개 있으므로 23
4가 1개 있으므로 14
5가 2개 있으므로 25
2가 1개 있으므로 12
합쳐서 "3223142512"가 된다.
다만 이 문제는 특정 숫자가 주어졌을 때 그 숫자를 문자열로 변경한 값을 리턴하는 게 아니라,
1번째 문자열이 "1" 이라고 가정하고,
n 번째 문자열이 (n-1) 번째 문자열을 변경한 값일 때,
주어진 n 에 대한 문자열을 응답해야 한다.
여기서 재귀의 개념이 사용된다.
문제 해결은 간단하다.
한 단계씩 나아가보자.
나는 문제해결을 위해 "양방향 큐"를 생각했다.
양방향 큐에 "숫자와 갯수를 저장할 수 있는 객체"를 넣어
아래와 같은 절차를 통해 문제를 해결할 수 있다.
"now" 라고 하자.{"last", count} 라고 하자."last")와 갯수(count)를 저장하고 있다."now" == "last" 라면 count를 +1 한다."now" != "last" 라면 큐에 {"now", 1} 를 넣는다.{"now", 1} 를 넣는다.객체는 직접 정의해도 되고, 페어 자료구조를 사용해도 된다.
코드로 나타내면 아래와 같다.
private fun convert(say: String): String {
val queue = mutableListOf<Counter>() // 리스트를 양방향 큐처럼 사용
// 순회하며 양방향 큐를 규칙에 따라 채운다.
for (c in say) {
val now = Character.getNumericValue(c)
// 큐가 비어있거나 last != now 라면 큐에 now(1)를 넣는다.
if (queue.isEmpty() || queue.last().num != now) {
queue.add(Counter(now, 1))
} else { // last == now 라면 last 의 count 를 추가한다.
queue.last().count++
}
}
return queue.asSequence()
.map { counter -> counter.toString() }
.fold("") { ans, nxt -> ans + nxt } // 문자열을 이어붙인다.
}
class Counter(
val num: Int,
var count: Int
) {
override fun toString(): String {
return this.count.toString() + this.num.toString()
}
}
그러면 이제 해당 메서드를 재귀 호출 하면 된다.
fun countAndSay(n: Int): String {
if (n == 1) {
return "1"
}
return convert(countAndSay(n - 1))
}
전체 코드는 아래의 링크에서 확인할 수 있다.
https://github.com/yglee8048/leet-code/blob/master/src/main/kotlin/P38_Count_and_Say.kt
이제 시간 복잡도를 계산해보자.
일단 convert 함수의 시간 복잡도는 아래와 같다.
이제 재귀 함수의 시간 복잡도를 생각해보자.
아래와 같은 점화식을 생각해볼 수 있다.
이 때, T(1) = 1 이므로, T(2) = 1 + L(1) 이다.
결과적으로 아래와 같다.
따라서 시간 복잡도는 O(nL) 이다.