S-99: Ninety-Nine Scala Problems所感 リスト操作編

S-99: Ninety-Nine Scala Problems

Prolog学習者のための99問の練習問題をScalaで解こうという企画。難易度は(*), (**), (***)の三段階で分けられている(Prolog向け難易度の引き写し)。

  • (*) 簡単。15分以内に解ける。
  • (**) 中級者向け。習熟したScalaプログラマなら30-90分程度で解けるだろう。
  • (***) 上級。綺麗な答えを出すには数時間程度かかると思われる。

解のエレガントさ、効率性にも留意せよ、ただし最初は効率より明瞭性を重視しましょうとのこと。いくつかの問題はビルトイン関数で解けちゃったりします。

以下では私が問題に取り組む際に考えていたことをだらだら述べます。こちらには私の書いた解を載せますが全てではありません。答えはリンク先で見れます。

リスト処理(問題 P1-P28)

P1 リストの最後の要素を返す

scala> last(List(1, 1, 2, 3, 5, 8))
res0: Int = 8

答えをいきなり見てScalaの基本的な書き方を再確認。去年ちょっとかじった程度だったので文法はほとんど忘れていたのだった。
型変数の付け方

def last[A](ls: List[A]): A

パターンマッチの書き方

ls match{
  case x :: Nil  => x
  case x :: tail => last(tail)
  case _         => new throw NoSuchElementException
}

を思い出す。
コーナーケースcase _(要素が1つもない場合)についても留意した。

P2 リストの最後から2番目の要素を返す

scala> penultimate(List(1, 1, 2, 3, 5, 8))
res0: Int = 5

1問目からの類推でどのようにしたらいいのかはだいたい見当がつくのだが、それに見合った記法が思い出せない。
よってこれもいきなり答えを見た。以下の書き方を覚えておく。

case x :: y :: Nil => x

P3 リストからK番目の要素を取り出す

リストの最初の要素を添字0で表すとする。

scala> nth(2, List(1, 1, 2, 3, 5, 8))
res0: Int = 2

タプルでのパターンマッチは覚えていたのですぐパスできた。

P4 リストの長さを求める

この辺りになるとだんだん頭が回ってくるようになる。今更ながら単純再帰の型がわりと決まりきっていることを感得する。

  def length[A](ls: List[A]): Int = ls match{
    case Nil        => 0
    case h :: tail  => 1 + length(tail)
  }

P5 リストを反転させる

単純な再帰でやると計算量オーダーがO(N^2)になる。これはリストの末尾への結合にかかる時間がリストの要素数分であるせい。継続渡しで要素をリストの先頭に継ぎ足していく方式にすればO(N)でいけるでしょう。もっとも解答見るまで忘れてたけど。

P6 リストが回文になっているか判定する

P5のような問題の次に必ず出される問題ですね。

P7 入れ子になったリストを平坦化する

解答はflatMapを使っているのですが、『mapを適用したあとリストの入れ子を解消する操作』ってなんかずるいですよね。
最初に書いたコードはこんな感じ。

object Flat{
  def flatten(ls: List[_]): List[_] = ls match{
    case List()                           => List()
    case (xs : List[_]) :: (ys : List[_]) => flatten(xs) ::: flatten(ys)
    case x :: (xs : List[_])              => List(x) ::: flatten(xs)
  }
}

パターンマッチが微妙に混乱している。Flatten a list - Rosetta Codeを見るともっとすっきり書いて良い事が分かる。

P8 連続した同じ要素を一つにまとめる

scala> compress(List('a, 'a, 'a, 'a, 'b, 'c, 'c, 'a, 'a, 'd, 'e, 'e, 'e, 'e))
res0: List[Symbol] = List('a, 'b, 'c, 'a, 'd, 'e)

これはすぐに分からなかった。dropWhileを覚えていれば、という感じ。

P9 連続する同じ要素をリストにまとめる

scala> pack(List('a, 'a, 'a, 'a, 'b, 'c, 'c, 'a, 'a, 'd, 'e, 'e, 'e, 'e))
res0: List[List[Symbol]] = List(List('a, 'a, 'a, 'a), List('b), List('c, 'c), List('a, 'a), List('d), List('e, 'e, 'e, 'e))

spanの使い方を覚える。

  def pack[A](ls: List[A]): List[List[A]] = ls match{
    case Nil  => Nil
    case _    => {
      val (packed, next) = ls span { _ == ls.head }
      packed :: pack(next)
    }
  }

P10 リストのランレングス符号をとる(P9の関数を利用して)

P9の返り値の型がList[(Int, Symbol)]になっているだけ。P9にもたついたので自習するつもりで一から書いたらP13と重複していた。言われたことを素直にやらないとこうなる(´・ω・`)

P11 リストのランレングス符号をとる(長さ1の文字は(1, 'a)でなく'aとする。

Aか(Int, A)型を返したいのだからEitherにするとスッキリする。

P12 ランレングス符号化されたリストをデコードする

簡単。解答のList.makeは新しめのScalaではdeprecateなのでList.fillにしましょう。

P13 リストのランレングス符号をとる

直接実装せよといってもP9と大した差はない。ちょっといじるだけ。

P14 それぞれの要素を複製する

scala> duplicate(List('a, 'b, 'c, 'c, 'd))
res0: List[Symbol] = List('a, 'a, 'b, 'b, 'c, 'c, 'c, 'c, 'd, 'd)

ところでリンク先の解答はここでもflatMapを使ってるけどやはり教育的じゃないと思う。

  def duplicate[A](ls: List[A]): List[A] = ls match{
    case Nil        => Nil
    case h :: tail  => List.fill(2)(h) ::: duplicate(tail)
  }

P15 与えられた数字の回数だけそれぞれの要素を複製する

scala> duplicateN(3, List('a, 'b, 'c, 'c, 'd))
res0: List[Symbol] = List('a, 'a, 'a, 'b, 'b, 'b, 'c, 'c, 'c, 'c, 'c, 'c, 'd, 'd, 'd)

P14をちょっと変えるだけ。

P16 N番目ごとに1つ要素を落としたリストをつくる

ちょっと日本語がわかりづらいかもしれないけど下を見れば単純。

scala> drop(3, List('a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, 'k))
res0: List[Symbol] = List('a, 'b, 'd, 'e, 'g, 'h, 'j, 'k)

この辺りから継続渡しを始める。

  def drop[A](n: Int, ls: List[A]): List[A] = {
    def dropR(n: Int, ls: List[A], pn: Int): List[A] = ls match{
      case Nil                      => Nil
      case h :: tail if pn % n == 0 => dropR(n, tail, pn+1)
      case h :: tail                => h :: dropR(n, tail, pn+1)
    }
    dropR(n, ls, 1)
  }

P17 リストを2つのリストに分割する

scala> split(3, List('a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, 'k))
res0: (List[Symbol], List[Symbol]) = (List('a, 'b, 'c),List('d, 'e, 'f, 'g, 'h, 'i, 'j, 'k))

takeとdropでやるのが普通だしそれが一番関数的なんだけど頭を使うために継続渡しする。

  def split[A](n: Int, lists: List[A]): (List[A], List[A]) = {
    // ls: left lists, rs: right lists
    def splitR(n: Int, ls: List[A], rs: List[A]):(List[A], List[A]) =
      (n, rs) match{
        case (_, Nil)       => (ls.reverse, rs)
        case (0, rs)        => (ls.reverse, rs)
        case (n, h :: tail) => splitR(n - 1, h::ls, tail)
      }
    splitR(n, Nil, lists)
  }

P18 リストのiからk番目までの要素を抜き出す

一応0を最初の要素としていることに注意。

scala> slice(3, 7, List('a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, 'k))
res0: List[Symbol] = List('d, 'e, 'f, 'g)

もうこれはtakeとdropで実装するのが自然だろうということで(妥協)車輪を再発明。

  def slice[A](i: Int, k: Int, ls: List[A]):List[A] = {
    def drop(i: Int)(ls: List[A]): List[A] = (i, ls) match{
      case (0, _)         => ls
      case (n, h :: tail) => drop(n-1)(tail)
    }
    def take(k: Int)(ls: List[A]): List[A] = (k, ls) match{
      case (0, _)         => Nil
      case (n, h :: tail) => h :: take(n-1)(tail)
    }
    take (k-i) (drop(i)(ls))
  }

P19 リストを回転させる。N番目以降の要素を一番左へ。

scala> rotate(3, List('a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, 'k))
res0: List[Symbol] = List('d, 'e, 'f, 'g, 'h, 'i, 'j, 'k, 'a, 'b, 'c)

scala> rotate(-2, List('a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, 'k))
res1: List[Symbol] = List('j, 'k, 'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i)

はじめ負の入力に気づかずP17を流用して事足れりとしていた(なんという注意力散漫!)。
リストの長さで剰余をとったあと負の値に関する条件分岐をする。大したことはないけど今までの問題に比べると泥臭い作業。

P20 K番目の要素を削除したリストと削除した要素のタプル

scala> removeAt(1, List('a, 'b, 'c, 'd))
res0: (List[Symbol], Symbol) = (List('a, 'c, 'd),'b)
  def removeAt[A](k: Int, ls: List[A]): (List[A], A) = {
    def removeAtR(k: Int, ls: List[A], rs: List[A]):(List[A], A) = (k, rs) match {
      case (_, Nil)       => throw new NoSuchElementException
      case (0, h :: tail) => (ls.reverse ::: tail, h)
      case (k, h :: tail) => removeAtR(k-1, h :: ls, tail)
    }
    removeAtR(k, Nil, ls)
  }

P21 リストに対して与えられた位置に要素を挿入するメソッド

scala> insertAt('new, 1, List('a, 'b, 'c, 'd))
res0: List[Symbol] = List('a, 'new, 'b, 'c, 'd)

splitAtも使わずに。

  def insertAt[A](elem: A, n: Int, ls: List[A]): List[A] =
    (n, ls) match {
      case (0, _)          => elem :: ls
      case (n, h :: tail)  => h :: insertAt(elem, n-1, tail)
    }

P22 rangeの再発明

  def range(start: Int, end: Int): List[Int] = {
    def rangeR(num: Int, count: Int): List[Int] = count match {
      case 0 => List(num)
      case _ => num :: rangeR(num+1, count-1)
    }
    rangeR(start, end-start)
  }

P23 リストからランダムにN個の要素を選んだリスト

util.Randomを使ったことがなかったので答えを見た。再帰するごとに(new util.Random)していては高くつくのでここでも継続渡しする。

P24 1..Mの数字からN個の異なる数のリストを得る

P22, P23から簡単に作れる。

P25 与えられたリストからランダムな並びのリストを作る(シャッフリング)

P23から即座に、

  def randomPermute1[A](ls: List[A]): List[A] =
    randomSelect(ls.length, ls)

すればよいことはわかる。純粋関数的にシャッフルするためには色々あるらしい。

// Efficient purely functional algorithms for shuffling are a lot harder. One
// is described in http://okmij.org/ftp/Haskell/perfect-shuffle.txt using
// Haskell. Implementing it in Scala is left as an exercise for the reader.

P26 与えられたリストからN個の要素を選ぶ組み合わせのリスト(combination)

その要素を選ぶか/選ばないかで再帰した。要素が尽きたらNil、選ぶ個数が尽きたらそれまでに選んだリストを返す。

  def combinations[A](n: Int, ls: List[A]): List[List[A]] = {
    def csR(n: Int, rs: List[A], ls: List[A]): List[List[A]] = {
      (n, ls) match{
        case (0, _)         => List(rs.reverse)
        case (_, Nil)       => Nil
        case (n, h :: tail) => csR(n, rs, tail) ::: csR(n-1, h :: rs, tail)
      }
    }
    csR(n, Nil, ls).reverse
  }

P27 リストの互いに素なグループ分けを行う

基本的にP26の結果を利用する。

a)問題

9人のグループを2, 3, 4人に分ける組み合わせをすべて列挙する。

scala> group3(List("Aldo", "Beat", "Carla", "David", "Evi", "Flip", "Gary", "Hugo", "Ida"))
res0: List[List[List[String]]] = List(List(List(Aldo, Beat), List(Carla, David, Evi), List(Flip, Gary, Hugo, Ida)), ...
b)問題

与えられた任意のグループ分けで分ける組み合わせを列挙する。

scala> group(List(2, 2, 5), List("Aldo", "Beat", "Carla", "David", "Evi", "Flip", "Gary", "Hugo", "Ida"))
res0: List[List[List[String]]] = List(List(List(Aldo, Beat), List(Carla, David), List(Evi, Flip, Gary, Hugo, Ida)), ...

自力では解けなかった。とくにb)問題はまだちょっと頭の中で消化できていない。

P28 リストをサブリストの長さに応じてソートする

a)問題

リストを要素に含むリストがあるとする。その要素リストの長さに応じて昇順にソートする。

  def lsort[T](ls: List[List[T]]): List[List[T]] = {
    ls.sortBy(_.length)
  }

ここに来て組み込み関数を使うずるさ。

b)問題

リストの要素リストの長さの出現頻度に応じて昇順にソートする。

  def lsortFreq[T](ls: List[List[T]]): List[List[T]] = {
    val countmap = mutable.Map[Int, Int]()
    val lens = ls map {_.length}
    lens.foreach ((i) => {
        val count = countmap.getOrElse(i, 0)
        countmap.update(i, count+1)
      }
    )
    ls.sortBy(x => countmap(x.length))
  }

講評

  • 単純な再帰で解く場合、何を考えずとも次のような型で書き始める。
def processList[A](ls: List[A]): List[A] = ls match{
  case Nil          => Nil
  case head :: tail => // ここに処理を書く & processList(tail)で再帰を呼び出す
}
  • 内部でクロージャを定義して継続渡しを行うと効率的になる場合がある。
  • 幾つかの問題はビルトインメソッドで解けるし、他の問題が解ければトリビアルになる問題も幾つかあるので自分で適度に距離感掴んで解かなければいけない感じはする。