Самый простой способ получить верхние n элементов Scala Iterable
Существует ли простое и эффективное решение для определения верхних n элементов Scala Iterable? Я имею в виду что-то вроде
iter.toList.sortBy(_.myAttr).take(2)
но без сортировки всех элементов, когда интересны только верхние 2. В идеале я ищу что-то вроде
iter.top(2, _.myAttr)
см. также: Решение для верхнего элемента с помощью Ordering: В Scala, как использовать Ordering [T] с List.min или List.max и сохранить прочитанный код
Обновление:
Спасибо всем за ваши решения. Наконец, я взял оригинальное решение неизвестного пользователя и принял его для использования 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))
Ответы
Ответ 1
Мое решение (привязано к 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 и используйте упорядоченное.
- Отбросьте (_ > _) и используйте User-Ordering, чтобы разрешить BottomTen. (Harder: выберите средний 10:))
- Отбросьте список и вместо этого используйте Iterable
update (абстракция):
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, кажется, немного сложнее.
Как отметил Даниэль С. Собрал в комментариях, высокий n
в topN может привести к большой работе сортировки, так что было бы полезно сделать ручную сортировку вставки вместо повторной сортировки всего списка сверху -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 (_, _))
}
верхний/нижний метод и использование, как указано выше. Для небольших групп верхних/нижних элементов сортировка редко вызывается, несколько раз в начале, а затем все реже и реже с течением времени. Например, 70 раз с вершиной (10) 10 000 и 90 раз с вершиной (10) 100 000.
Ответ 2
Еще одна версия:
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
}
}
Ответ 3
Вам не нужно сортировать всю коллекцию, чтобы определить верхние N элементов. Тем не менее, я не считаю, что эта функциональность предоставляется исходной библиотекой, поэтому вам придется сворачивать вас самостоятельно, возможно, используя шаблон 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
Если цель состоит в том, чтобы не отсортировать весь список, тогда вы могли бы сделать что-то вроде этого (конечно, его можно было бы оптимизировать так, чтобы мы не меняли список, когда числа явно не должно быть):
List(1,6,3,7,3,2).foldLeft(List[Int]()){(l, n) => (n :: l).sorted.take(2)}
Ответ 5
Недавно я реализовал такой алгоритм ранжирования в классе Rank Apache Jackrabbit (на Java, хотя). См. Метод take
для его сущности. Основная идея заключается в быстрой сортировке, но заканчивается преждевременно, как только будут найдены верхние элементы n
.
Ответ 6
Здесь другое решение, которое является простым и имеет довольно хорошую производительность.
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())
}
Большой O - это O(n + k log n)
, где k <= n
. Таким образом, производительность является линейной для небольших k
и в худшем случае n log n
.
Решение также может быть оптимизировано для O(k)
для памяти, но O(n log k)
. Идея состоит в том, чтобы использовать MinHeap для отслеживания только верхних k
элементов во все времена. Здесь решение.
def pickTopN[T](n: Int, iterable: Iterable[T])(implicit ord: Ordering[T]): Seq[T] = {
val seq = iterable.toSeq
val q = collection.mutable.PriorityQueue[T](seq.take(n):_*)(ord.reverse)
seq.drop(n).foreach(v => {
q += v
q.dequeue()
})
q.dequeueAll.reverse
}
Ответ 7
При небольших значениях n
и больших списков получение верхних элементов n
можно реализовать, выбирая максимальный элемент n
times:
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)
.
Он не возвращает тип предоставляемой коллекции, но вы можете посмотреть Как применить шаблон обогащения-my-library к коллекциям Scala?, если это требование.
Ответ 8
Здесь асимптотически решение 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)
}