列表中的前n项(包括重复项)

时间:2011-11-25 22:17:00

标签: scala sorting

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

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

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

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))
    }
}

5 个答案:

答案 0 :(得分:4)

经典算法称为QuickSelect。它就像QuickSort,除了你只下降到树的一半,所以最终平均为O(n)。

答案 1 :(得分:4)

对于长度为log(M)的大型列表,不要高估M的大小。对于包含十亿个项目的列表,log(M)只有30个。因此,排序和获取并不是一个不合理的方法。事实上,排序一个整数数组要快得多,因为排序列表(并且数组也占用更少的内存),所以我会说你最好(简短)的赌注(由于{{1}这对于短或空列表是安全的}})

takeRight

可以采取其他各种方法,但实施方式不那么简单。您可以使用部分快速排序,但是对于快速排序的最坏情况(例如,如果您的列表已经排序,一个天真的算法可能是val arr = s.toArray java.util.Arrays.sort(arr) arr.takeRight(N).toList !),您会遇到相同的问题。您可以将顶部O(n^2)保存在环形缓冲区(数组)中,但这需要N每一步的二进制搜索以及元素的O(log N)滑动 - 仅在{{1很小。更复杂的方法(比如基于双枢轴快速排序的东西)更复杂。

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

(当然,如果您要排序对象而不是数字,答案会有所不同,但如果您的比较总是可以减少到一个数字,您可以O(N/4)然后获取获胜分数并再次浏览列表在你发现每个分数时,计算你需要拿走的数字;这是一些记账,但除了地图之外不会减慢很多东西。)

答案 2 :(得分: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没有意识到你想保留重复。)

我将此基准测试为100k随机Ints的列表,平均花费40毫秒。您的elite方法大约需要850毫秒,而elite2方法大约需要4100毫秒。所以这比你最快的快20多倍。

答案 3 :(得分:0)

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

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会使用某种技术来选择“pivot”值来分区列表。 partition会将列表拆分为两个:lt(小于数据透视的元素)和gt(大于数据透视的元素)。当然,您需要在其中一个组中抛出等于pivot的元素,否则分别处理该组。它没有太大的区别,只要你记得处理那个案例某种程度上

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

答案 4 :(得分:0)

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

    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

所以,不要太慢,当然也要灵活得多