Home About Contact
Kotlin , Functional Programming

リストを n個ごとに分割する kotlin 編

以前にGroovy / Golang / Node.js, リストを n 個ごとに分割したリストのリスト(サブリスト)をつくりたい というエントリーを書いたのだが、kotlin でそれをつくり出す必要が生じたので備忘録として書き残します。

ここで言うサブリストとは、リストを n 個ごとに分けたい、という場合につくるリストのリストのことです。

list.take() や list.drop() があるので、これを使うことにします。

val list = listOf("1","2","3","4","5","6","7","8")
println(list.take(3)) // -> [1, 2, 3]
println(list.drop(3)) // -> [4, 5, 6, 7, 8]

という具合に作動する。

まずこのようなコードを考える。

var list = listOf("1","2","3","4","5","6","7","8")

val acc = mutableListOf<List<String>>()
while(list.size>0){
    val subList = list.take(3)
    acc.add(subList)
    list = list.drop(3)
}

println( acc )

sublist.main.kts にコードを保存して、実行。

$ kotlin sublist.main.kts
[[1, 2, 3], [4, 5, 6], [7, 8]]

完全に作動するのですが、処理対象となる list 変数の内容が順に書きかわっていくという作動が気持悪い。

そこで次のような再帰関数を考えてみた。

fun makeSubList(acc: MutableList<List<String>>, list:List<String>, n:Int): List<List<String>> {
    return if( list.isEmpty() ){
        acc
    } else {
        val subList = list.take(n)
        acc.add(subList)
        makeSubList(acc, list.drop(n), n)
    }
}

val list = listOf("1","2","3","4","5","6","7","8")
val subList = makeSubList(mutableListOf(), list, 3)
println( subList )

コードは増えてしまったが、var で宣言する(つまり、内容が書き換えられていく変数)は駆逐できた。 実行する。

$ kotlin sublist.main.kts
[[1, 2, 3], [4, 5, 6], [7, 8]]

問題はない。

最後にこの makeSubList に tailrec (末尾最適化)を入れて完成とする。

tailrec fun makeSubList(acc: MutableList<List<String>>, list:List<String>, n:Int): List<List<String>> {
    return if( list.isEmpty() ){
        acc
    } else {
        val subList = list.take(n)
        acc.add(subList)
        makeSubList(acc, list.drop(n), n)
    }
}

val list = listOf("1","2","3","4","5","6","7","8")
val subList = makeSubList(mutableListOf(), list, 3)
println( subList )

tailrec を入れる場合、再帰するコードが関数の最後にこないといけないとかなんとか(ふわったした理解しかないので、詳細はほかをあたってください)に注意。

以上です。

追伸

「n個 ごとにリストを分割する」をひとことで言い表す言葉はなに?サブリストの生成、などと調べていたところ、 kotlin には list.subList(fromIndex, toIndex) というメソッドがあることが判明。

val list = listOf("1","2","3","4","5","6","7","8")
println(list.subList(0, 3))
println(list.subList(3, 6))
//println(list.subList(6, 9))//これはエラーになる.

こうすることで、次のような実行結果を得ることができます。

[1, 2, 3]
[4, 5, 6]

最後の行はエラーになるのでコメントアウトしていますが、範囲外のindexは指定できません。 このコードをリストがどんな長さでも対応できるように書き直せば:

val list = listOf("1","2","3","4","5","6","7","8")
val n = 3
val acc = mutableListOf<List<String>>()

for( i in 0.until(list.size/n+1) ){
    val fromIndex = i*n
    val toIndex   = Math.min((i+1)*n, list.size) // 範囲外にならないようにする.
    val subList   = list.subList(fromIndex, toIndex)
    acc.add(subList)
}
println(acc)

実行:

[[1, 2, 3], [4, 5, 6], [7, 8]]

できました。

この方法と再帰関数を使うのとどちらが良いのでしょうか。 目的を達成できるという意味ではどちらも問題はない。 サブリストをつくる度に関数コールが発生するのは効率が悪いのか? つまり 関数コールより for 文の方が処理速度が速いのでしょうか。

一方で、効率云々よりコードを書く上での発想として見た場合、 再帰関数方式では、 「リストの先頭から n 個ずつ取り出す」というアイデアだけを使っているという意味でシンプルに感じます。 subList と for を使う方式では、人間が要素にアクセスするためにインデックス計算を考えなければならないので、 より原始的な感じがします。