【问题标题】:Top n items in a List ( including duplicates )列表中的前 n 个项目(包括重复项)
【发布时间】:2011-11-25 22:16:59
【问题描述】:

试图找到一种有效的方法来获取一个非常大的列表中的前 N ​​个项目,可能包含重复项。

我首先尝试了排序和切片,这很有效。但这似乎是不必要的。如果您只需要前 20 名成员,则不需要对非常大的列表进行排序。 所以我写了一个递归例程来构建 top-n 列表。这也有效,但比非递归慢得多!

问题:我的第二个例程 (elite2) 比精英慢得多,我如何让它更快?我的代码附在下面。谢谢。

import scala.collection.SeqView
import scala.math.min
object X {

    def  elite(s: SeqView[Int, List[Int]], k:Int):List[Int] = {
        s.sorted.reverse.force.slice(0,min(k,s.size))
    }

    def elite2(s: SeqView[Int, List[Int]], k:Int, s2:List[Int]=Nil):List[Int] = {
        if( k == 0 || s.size == 0) s2.reverse
        else {
            val m = s.max
            val parts = s.force.partition(_==m)
            val whole = if( parts._1.size > 1) parts._1.tail:::parts._2 else parts._2
            elite2( whole.view, k-1, m::s2 )
        }
    }

    def main(args:Array[String]) = {
        val N = 1000000/3
        val x = List(N to 1 by -1).flatten.map(x=>List(x,x,x)).flatten.view
        println(elite2(x,20))
        println(elite(x,20))
    }
}

【问题讨论】:

  • 注意,如果你想取前n个元素,最好使用.take(n)
  • 我对前 n 个元素不感兴趣。对前n感兴趣。所以 { 4,3,2,5,7,1}.take(2) 给出 {4,3}。我想要前 2 个 { 7,5}。
  • 我在elite谈过.slice(0,min(k,s.size))
  • 哦,你希望我使用 take 而不是 slice。很好。很抱歉造成混乱。
  • 这里stackoverflow.com/q/5674741/312172是一个类似的线程

标签: scala sorting


【解决方案1】:

经典算法称为 QuickSelect。它就像快速排序,只是你只下降到树的一半,所以它最终平均为 O(n)。

【讨论】:

  • 因此要找到m 最大的元素,您将对每个元素执行一次快速选择,从而呈现O(mn) 算法。您可以通过同时选择多个元素来获得一些额外的性能。
  • 不,实际上您只会为 mth 运行 QuickSelect。那么第一个 m 元素就是你的结果。如果您希望它们完全排序,则可以对第一个 m-1 元素进行完全排序。这需要预期的O(n + m log m) 总数。
  • 啊,是的,完全正确。一旦你有了mth 项目,你可以用那个项目作为主元来分割原始列表,这只是 O(n) 额外的时间,假设它还没有被隐式地完成,它可能已经完成了。跨度>
  • 实际上 QuickSelect 已经进行了分区(至少在实现部分 QuickSort 的 Hoare 方式时)。它保证了A<=m<=B的不变性,所以当找到最后的mth元素时,我也知道前面的元素必须都更小,后面的元素都更大。 QuickSelect 类似于 Quicksort,除了它只确保 mth 最大元素位于 mth 位置并且此不变量成立。它不提供关于子列表 A 和 B 中的订单的保证。
【解决方案2】:

不要高估log(M) 的大小,因为M 的列表很长。对于一个包含十亿个项目的列表,log(M) 只有 30 个。所以排序和提取毕竟不是这种不合理的方法。实际上,对整数数组进行排序要快得多,这要归功于对列表进行排序(并且数组也占用更少的内存),所以我会说你最好(简短)的赌注(感谢takeRight,这对于短或空列表是安全的) )

val arr = s.toArray
java.util.Arrays.sort(arr)
arr.takeRight(N).toList

您可以采用多种其他方法,但实现起来不那么简单。您可以使用部分快速排序,但在最坏的情况下,您会遇到与快速排序相同的问题(例如,如果您的列表已经排序,那么简单的算法可能是O(n^2)!)。您可以将顶部的N 保存在环形缓冲区(数组)中,但这需要O(log N) 每一步进行二进制搜索以及O(N/4) 元素的滑动——只有在N 非常小的情况下才好。更复杂的方法(比如基于双轴快速排序的方法)也更复杂。

所以我建议你尝试数组排序,看看是否足够快。

(当然,如果您排序对象而不是数字,答案会有所不同,但如果您的比较总是可以简化为一个数字,您可以s.map(x => /* convert element to corresponding number*/).toArray 然后获取获胜分数并再次遍历列表,计数当你找到它们时,你需要从每个分数中取出数字;这有点记账,但除了地图之外并不会减慢速度。)

【讨论】:

    【解决方案3】:

    除非我遗漏了什么,否则为什么不直接遍历列表并选择前 20 名呢?只要您跟踪前 20 名中最小的元素,就应该没有开销,除非添加到前 20 名,这对于长列表来说应该是相对罕见的。这是一个实现:

      def topNs(xs: TraversableOnce[Int], n: Int) = {
        var ss = List[Int]()
        var min = Int.MaxValue
        var len = 0
        xs foreach { e =>
          if (len < n || e > min) {
            ss = (e :: ss).sorted
            min = ss.head
            len += 1
          }
          if (len > n) {
            ss = ss.tail
            min = ss.head
            len -= 1
          }                    
        }
        ss
      }  
    

    (已编辑,因为我最初使用 SortedSet 没有意识到您想保留重复项。)

    我针对 10 万个随机 Int 列表对此进行了基准测试,平均耗时 40 毫秒。您的 elite 方法大约需要 850 毫秒,而您的 elite2 方法大约需要 4100 毫秒。所以这比你最快的速度快 20 倍以上。

    【讨论】:

    • 我喜欢您的解决方案,并将使用它来代替elite2。但是我还是很好奇elite2为什么这么慢。算法本身就是一个非常简单的递归例程
    • @Krishnan 好吧,使用您的elite2 方法,它的运行时间将随着您要提取的项目数量线性增加,并且在每次迭代时,它都会遍历整个列表以评估其大小(所以尝试一个不同的数据结构来记住它的大小),然后再次遍历它以对其进行分区,创建两个新的大列表,然后创建另一个混合了重复项的列表。为什么不根据需要取多少,而不是只有一个?无论如何,基本上这不是一种非常有效的方式。
    • 关于基准测试时间:这些是在 32 位 JVM 下执行 10 次,从 100k 长的列表中取 10 次。同样为了比较,根据 Rex 的回答对整个 100k 列表进行排序需要 190 毫秒。
    • 不是ss = (e :: ss).sorted,难道没有像ss = ss insert e 这样的东西可以利用ss 已经是一个排序列表这一事实吗? (我在想 Haskell 的 Data.List.insert
    • @DanBurton 这当然是一个很好的优化,但没有想到支持这种优化的集合类型。也许是DoubleLinkedList?我以后可能会试一试。
    【解决方案4】:

    这是我要使用的算法的伪代码:

    selectLargest(n: Int, xs: List): List
      if size(xs) <= n
         return xs
      pivot <- selectPivot(xs)
      (lt, gt) <- partition(xs, pivot)
      if size(gt) == n
         return gt
      if size(gt) < n
         return append(gt, selectLargest(n - size(gt), lt))
      if size(gt) > n
         return selectLargest(n, gt)
    

    selectPivot 会使用一些技术来选择一个“枢轴”值来对列表进行分区。 partition 会将列表分成两部分:lt(小于枢轴的元素)和gt(大于枢轴的元素)。当然,您需要在其中一个组中抛出等于枢轴的元素,或者单独处理该组。只要您记得以某种方式处理这种情况,这并没有太大的区别。

    使用此算法的 Scala 实现,您可以随意编辑此答案,或发布您自己的答案。

    【讨论】:

      【解决方案5】:

      我想要一个多态的版本,并且还允许使用单个迭代器进行组合。例如,如果您在读取文件时想要最大和最小的元素怎么办?这是我想出的:

          import util.Sorting.quickSort
      
          class TopNSet[T](n:Int) (implicit ev: Ordering[T], ev2: ClassManifest[T]){
            val ss = new Array[T](n)
            var len = 0
      
            def tryElement(el:T) = {
              if(len < n-1){
                ss(len) = el
                len += 1
              }
               else if(len == n-1){
                ss(len) = el
                len = n
                quickSort(ss)
              }
              else if(ev.gt(el, ss(0))){
                ss(0) = el
                quickSort(ss)
              }
            }
            def getTop() = {
              ss.slice(0,len)
            }
          }
      

      与接受的答案进行比较:

      val myInts = Array.fill(100000000)(util.Random.nextInt)
      time(topNs(myInts,100)
      //Elapsed time 3006.05485 msecs
      val myTopSet = new TopNSet[In](100)
      time(myInts.foreach(myTopSet.tryElement(_)))
      //Elapsed time 4334.888546 msecs
      

      所以,速度不会慢很多,而且肯定更灵活

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-04-27
        • 2018-12-13
        • 1970-01-01
        • 2012-10-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多