web-dev-qa-db-ja.com

a Scala Iterableの上位n個の要素を取得する最も簡単な方法

Scala Iterableの上位n個の要素を決定するためのシンプルで効率的なソリューションはありますか?

iter.toList.sortBy(_.myAttr).take(2)

ただし、上位2つだけに関心がある場合は、すべての要素を並べ替える必要はありません。理想的には私はのようなものを探しています

iter.top(2, _.myAttr)

参照:Orderingを使用した最上位要素のソリューション: Scalaでは、Listing [T]をList.minまたはList.maxで使用し、コードを読みやすくする方法

更新:

解決策をありがとうございます。最後に、user unknownの元のソリューションを採用し、Iterableを使用するように採用しましたpimp-my-libraryパターン:

implicit def iterExt[A](iter: Iterable[A]) = new {
  def top[B](n: Int, f: A => B)(implicit ord: Ordering[B]): List[A] = {
    def updateSofar (sofar: List [A], el: A): List [A] = {
      //println (el + " - " + sofar)

      if (ord.compare(f(el), f(sofar.head)) > 0)
        (el :: sofar.tail).sortBy (f)
      else sofar
    }

    val (sofar, rest) = iter.splitAt(n)
    (sofar.toList.sortBy (f) /: rest) (updateSofar (_, _)).reverse
  }
}

case class A(s: String, i: Int)
val li = List (4, 3, 6, 7, 1, 2, 9, 5).map(i => A(i.toString(), i))
println(li.top(3, _.i))
38

私のソリューション(Intにバインドされていますが、簡単にOrderedに変更する必要があります(数分お待ちください)):

def top (n: Int, li: List [Int]) : List[Int] = {

  def updateSofar (sofar: List [Int], el: Int) : List [Int] = {
    // println (el + " - " + sofar)
    if (el < sofar.head) 
      (el :: sofar.tail).sortWith (_ > _) 
    else sofar
  }

  /* better readable:
    val sofar = li.take (n).sortWith (_ > _)
    val rest = li.drop (n)
    (sofar /: rest) (updateSofar (_, _)) */    
  (li.take (n). sortWith (_ > _) /: li.drop (n)) (updateSofar (_, _)) 
}

使用法:

val li = List (4, 3, 6, 7, 1, 2, 9, 5)    
top (2, li)
  • 上記のリストでは、最初の2(4、3)をTopTen(TopTwo)の開始として使用します。
  • 最初の要素が(もしあれば)大きい要素になるように並べ替えます。
  • リストの残り(li.drop(n))を繰り返し反復し、現在の要素を最小値のリストの最大値と比較します。必要に応じて交換し、再度頼みます。
  • 改善:
    • Intを破棄し、orderedを使用します。
    • 破棄(_> _)し、ユーザーオーダーを使用してBottomTenを許可します。 (難しい:真ん中の10を選ぶ:))
    • リストを破棄し、代わりにIterableを使用する

更新(抽象化):

def extremeN [T](n: Int, li: List [T])
  (comp1: ((T, T) => Boolean), comp2: ((T, T) => Boolean)):
     List[T] = {

  def updateSofar (sofar: List [T], el: T) : List [T] =
    if (comp1 (el, sofar.head)) 
      (el :: sofar.tail).sortWith (comp2 (_, _)) 
    else sofar

  (li.take (n) .sortWith (comp2 (_, _)) /: li.drop (n)) (updateSofar (_, _)) 
}

/*  still bound to Int:  
def top (n: Int, li: List [Int]) : List[Int] = {
  extremeN (n, li) ((_ < _), (_ > _))
}
def bottom (n: Int, li: List [Int]) : List[Int] = {
  extremeN (n, li) ((_ > _), (_ < _))
}
*/

def top [T] (n: Int, li: List [T]) 
  (implicit ord: Ordering[T]): Iterable[T] = {
  extremeN (n, li) (ord.lt (_, _), ord.gt (_, _))
}
def bottom [T] (n: Int, li: List [T])
  (implicit ord: Ordering[T]): Iterable[T] = {
  extremeN (n, li) (ord.gt (_, _), ord.lt (_, _))
}

top (3, li)
bottom (3, li)
val sl = List ("Haus", "Garten", "Boot", "Sumpf", "X", "y", "xkcd", "x11")
bottom (2, sl)

ListをIterableに置き換えるのは少し難しいようです。

Daniel C. Sobralがコメントで指摘したように、topNのnが高いと、多くの並べ替え作業が発生する可能性があるため、topのリスト全体を繰り返し並べ替えるのではなく、手動で挿入並べ替えを行うと便利です。 -n要素:

def extremeN [T](n: Int, li: List [T])
  (comp1: ((T, T) => Boolean), comp2: ((T, T) => Boolean)):
     List[T] = {

  def sortedIns (el: T, list: List[T]): List[T] = 
    if (list.isEmpty) List (el) else 
    if (comp2 (el, list.head)) el :: list else 
      list.head :: sortedIns (el, list.tail)

  def updateSofar (sofar: List [T], el: T) : List [T] =
    if (comp1 (el, sofar.head)) 
      sortedIns (el, sofar.tail)
    else sofar

  (li.take (n) .sortWith (comp2 (_, _)) /: li.drop (n)) (updateSofar (_, _)) 
}

上/下の方法と上記の使用法。トップ/ボトムエレメントの小さなグループの場合、ソートが呼び出されることはめったにありません。たとえば、上部(10)が10 000の場合は70回、上部(10)が100 000の場合は90回です。

22
user unknown

次に、シンプルでパフォーマンスが非常に優れた別のソリューションを示します。

_def pickTopN[T](k: Int, iterable: Iterable[T])(implicit ord: Ordering[T]): Seq[T] {
  val q = collection.mutable.PriorityQueue[T](iterable.toSeq:_*)
  val end = Math.min(k, q.size)
  (1 to end).map(_ => q.dequeue())
}
_

Big OはO(n + k log n)で、_k <= n_です。そのため、パフォーマンスは、小さいkおよび最悪の場合_n log n_に対して線形です。

ソリューションは、メモリではO(k)、パフォーマンスではO(n log k)になるように最適化することもできます。 MinHeapを使用して、常に上位kアイテムのみを追跡するという考え方です。これが解決策です。

_def pickTopN[A, B](n: Int, iterable: Iterable[A], f: A => B)(implicit ord: Ordering[B]): Seq[A] = {
  val seq = iterable.toSeq
  val q = collection.mutable.PriorityQueue[A](seq.take(n):_*)(ord.on(f).reverse) // initialize with first n

  // invariant: keep the top k scanned so far
  seq.drop(n).foreach(v => {
    q += v
    q.dequeue()
  })

  q.dequeueAll.reverse
}
_
7
Tom Wang

さらに別のバージョン:

val big = (1 to 100000)

def maxes[A](n:Int)(l:Traversable[A])(implicit o:Ordering[A]) =
    l.foldLeft(collection.immutable.SortedSet.empty[A]) { (xs,y) =>
      if (xs.size < n) xs + y
      else {
        import o._
        val first = xs.firstKey
        if (first < y) xs - first + y
        else xs
      }
    }

println(maxes(4)(big))
println(maxes(2)(List("a","ab","c","z")))

Setを使用すると、リストに一意の値を設定できます。

def maxes2[A](n:Int)(l:Traversable[A])(implicit o:Ordering[A]) =
    l.foldLeft(List.empty[A]) { (xs,y) =>
      import o._
      if (xs.size < n) (y::xs).sort(lt _)
      else {
        val first = xs.head
        if (first < y) (y::(xs - first)).sort(lt _)
        else xs
      }
    }
7
shellholic

上位N個の要素を特定するためにコレクション全体を並べ替える必要はありません。ただし、この機能がrawライブラリによって提供されるとは思わないため、おそらくpimp-my-libraryパターンを使用して、自分でロールする必要があります。

たとえば、次のようにしてコレクションのn番目の要素を取得できます。

  class Pimp[A, Repr <% TraversableLike[A, Repr]](self : Repr) {

    def nth(n : Int)(implicit ord : Ordering[A]) : A = {
      val trav : TraversableLike[A, Repr] = self
      var ltp : List[A] = Nil
      var etp : List[A] = Nil
      var mtp : List[A] = Nil
      trav.headOption match {
        case None      => error("Cannot get " + n + " element of empty collection")
        case Some(piv) =>
          trav.foreach { a =>
            val cf = ord.compare(piv, a)
            if (cf == 0) etp ::= a
            else if (cf > 0) ltp ::= a
            else mtp ::= a
          }
          if (n < ltp.length)
            new Pimp[A, List[A]](ltp.reverse).nth(n)(ord)
          else if (n < (ltp.length + etp.length))
            piv
          else
            new Pimp[A, List[A]](mtp.reverse).nth(n - ltp.length - etp.length)(ord)
      }
    }
  }

(これはあまり機能的ではありません。申し訳ありません)

上位のn要素を取得するのは簡単です。

def topN(n : Int)(implicit ord : Ordering[A], bf : CanBuildFrom[Repr, A, Repr]) ={
  val b = bf()
  val elem = new Pimp[A, Repr](self).nth(n)(ord)
  import util.control.Breaks._
  breakable {
    var soFar = 0
    self.foreach { tt =>
      if (ord.compare(tt, elem) < 0) {
         b += tt
         soFar += 1
      }
    }
    assert (soFar <= n)
    if (soFar < n) {
      self.foreach { tt =>
        if (ord.compare(tt, elem) == 0) {
          b += tt
          soFar += 1
        }
        if (soFar == n) break
      }
    }

  }
  b.result()
}

残念ながら、私はこの暗黙のうちにこのヒモを発見するのに苦労しています:

implicit def t2n[A, Repr <% TraversableLike[A, Repr]](t : Repr) : Pimp[A, Repr] 
  = new Pimp[A, Repr](t)

私はこれを手に入れます:

scala> List(4, 3, 6, 7, 1, 2, 8, 5).topN(4)
<console>:9: error: could not find implicit value for evidence parameter of type (List[Int]) => scala.collection.TraversableLike[A,List[Int]]
   List(4, 3, 6, 7, 1, 2, 8, 5).topN(4)
       ^

ただし、コードは実際には問題なく動作します。

scala> new Pimp(List(4, 3, 6, 7, 1, 2, 8, 5)).topN(4)
res3: List[Int] = List(3, 1, 2, 4)

そして

scala> new Pimp("ioanusdhpisjdmpsdsvfgewqw").topN(6)
res2: Java.lang.String = adddfe
4
oxbow_lakes

リスト全体を並べ替えないことを目標とする場合は、次のようにすることができます(もちろん、数値がそこにないはずのときにリストを変更しないように、少し最適化することができます)。

List(1,6,3,7,3,2).foldLeft(List[Int]()){(l, n) => (n :: l).sorted.take(2)}
2
thoredge

私は最近、Apache Jackrabbitの Rank クラスにそのようなランキングアルゴリズムを実装しました(Javaでも)。その要点については、takeメソッドを参照してください。基本的な考え方は、クイックソートすることですが、上位のn要素が見つかるとすぐに終了します。

1
michid

ここに漸近的にO(n)解があります。

def top[T](data: List[T], n: Int)(implicit ord: Ordering[T]): List[T] = {
    require( n < data.size)

    def partition_inner(shuffledData: List[T], pivot: T): List[T] = 
      shuffledData.partition( e => ord.compare(e, pivot) > 0 ) match {
          case (left, right) if left.size == n => left
          case (left, x :: rest) if left.size < n => 
            partition_inner(util.Random.shuffle(data), x)
          case (left @ y :: rest, right) if left.size > n => 
            partition_inner(util.Random.shuffle(data), y)
      }

     val shuffled = util.Random.shuffle(data)
     partition_inner(shuffled, shuffled.head)
}

scala> top(List.range(1,10000000), 5)

再帰により、このソリューションは上記の非線形ソリューションよりも時間がかかり、Java.lang.OutOfMemoryError: GC overhead limit exceeded。しかし、少し読みやすいIMHOと機能的なスタイル。就職の面接のみ;)。

さらに重要なことは、このソリューションを簡単に並列化できることです。

def top[T](data: List[T], n: Int)(implicit ord: Ordering[T]): List[T] = {
    require( n < data.size)

    @tailrec
    def partition_inner(shuffledData: List[T], pivot: T): List[T] = 
      shuffledData.par.partition( e => ord.compare(e, pivot) > 0 ) match {
          case (left, right) if left.size == n => left.toList
          case (left, right) if left.size < n => 
            partition_inner(util.Random.shuffle(data), right.head)
          case (left, right) if left.size > n => 
            partition_inner(util.Random.shuffle(data), left.head)
      }

     val shuffled = util.Random.shuffle(data)
     partition_inner(shuffled, shuffled.head)
}
1
ponkin

複雑なO(nlogk)PriorityQueueを使用する最適化されたソリューション。更新で与えられたアプローチでは、毎回sofarリストをソートする必要がなく、その下ではPriorityQueueを使用して最適化されています。

import scala.language.implicitConversions
import scala.language.reflectiveCalls
import collection.mutable.PriorityQueue
implicit def iterExt[A](iter: Iterable[A]) = new {
    def top[B](n: Int, f: A => B)(implicit ord: Ordering[B]) : List[A] = {
        def updateSofar (sofar: PriorityQueue[A], el: A): PriorityQueue[A] = {
            if (ord.compare(f(el), f(sofar.head)) < 0){
                sofar.dequeue
                sofar.enqueue(el)
            }
            sofar
        }

        val (sofar, rest) = iter.splitAt(n)
        (PriorityQueue(sofar.toSeq:_*)( Ordering.by( (x :A) => f(x) ) ) /: rest) (updateSofar (_, _)).dequeueAll.toList.reverse
    }
}

case class A(s: String, i: Int)
val li = List (4, 3, 6, 7, 1, 2, 9, 5).map(i => A(i.toString(), i))
println(li.top(3, -_.i))
0
Ravi Teja

nの小さな値と大きなリストの場合、最上位のn要素を取得するには、max要素をn回選択することで実装できます。

_def top[T](n:Int, iter:Iterable[T])(implicit ord: Ordering[T]): Iterable[T] = {
  def partitionMax(acc: Iterable[T], it: Iterable[T]): Iterable[T]  = {
    val max = it.max(ord)
    val (nextElems, rest) = it.partition(ord.gteq(_, max))
    val maxElems = acc ++ nextElems
    if (maxElems.size >= n || rest.isEmpty) maxElems.take(n)
    else partitionMax(maxElems, rest)
  }
  if (iter.isEmpty) iter.take(0)
  else partitionMax(iter.take(0), iter)
}
_

これはリスト全体をソートするのではなく、Orderingを取ります。私がpartitionMaxで呼び出すすべてのメソッドはO(リストサイズ)であり、最大でn回しか呼び出さないと想定しているため、小さいnの全体的な効率はイテレータのサイズに比例します。

_scala> top(5, List.range(1,1000000))
res13: Iterable[Int] = List(999999, 999998, 999997, 999996, 999995)

scala> top(5, List.range(1,1000000))(Ordering[Int].on(- _))
res14: Iterable[Int] = List(1, 2, 3, 4, 5)
_

nがイテラブルのサイズに近づいたときにブランチを追加し、iter.toList.sortBy(_.myAttr).take(n)に切り替えることもできます。

これは提供されたコレクションのタイプを返しませんが、 enrich-my-libraryパターンをScala collections? に適用するにはどうすればよいですか?要件。

0
huynhjl