如何在Scala中编写一个高效的groupBy-size过滤器,可以是近似的

时间:2015-08-12 19:02:57

标签: scala group-by bloom-filter foldleft

如果Scala中有x(p) = x_ll + (x_ur - x_ll) * (p - p_ll) / (p_ur - p_ll) y(q) = y_ll + (y_ur - y_ll) * (q - q_ll) / (q_ur - q_ll) ,我希望获得至少List[Int]次出现的所有Set[Int]的{​​{1}}。我可以使用Intthresh,然后使用groupBy来执行此操作。例如:

foldLeft

将提供filter

现在假设val thresh = 3 val myList = List(1,2,3,2,1,4,3,2,1) myList.foldLeft(Map[Int,Int]()){case(m, i) => m + (i -> (m.getOrElse(i, 0) + 1))}.filter(_._2 >= thresh).keys 非常大。有多大,很难说,但无论如何这似乎很浪费,因为我不关心每个Set(1,2)频率,我只关心它们是否至少List[Int]。一旦通过Int,就不再需要检查,只需将thresh添加到thresh

问题是:我是否可以针对非常大的Int

更有效地执行此操作

a)如果我需要真实,准确的结果(没有错误的余地)

b)如果结果可以是近似值,例如通过使用一些Hashing技巧或Bloom过滤器,其中Set[Int]可能包含一些误报,或List[Int]>的频率Set[Int]}不是Int,而是thresh中的Boolean

4 个答案:

答案 0 :(得分:3)

首先,你不能比O(N)做得更好,因为你需要至少检查一次初始数组的每个元素。您当前的方法是O(N),假设IntMap的操作实际上是恒定的。

现在您可以尝试以提高效率:

  • 仅在当前计数器值小于或等于阈值时更新映射。这将消除大量最昂贵的操作 - 地图更新
  • 尝试更快的地图而不是IntMap。如果您知道初始List的值在固定范围内,则可以使用Array而不是IntMap(索引作为键)。另一个可能的选项是具有足够初始容量的可变HashMap。正如我的基准测试显示它实际上有显着差异
  • 正如@ixx建议的那样,在增加地图中的值之后,检查它是否等于3,并在这种情况下立即将其添加到结果列表中。这将为您节省一次线性遍历(对于大输入来说似乎没有那么重要)

我不知道任何近似解决方案如何更快(只有你随机忽略一些元素)。否则它仍然是O(N)。

<强>更新

我创建了microbenchmark来测量不同实现的实际性能。对于足够大的输入和输出,Ixx关于立即向结果列表添加元素的建议不会产生显着的改进。但是,可以使用类似的方法来消除不必要的Map更新(这似乎是最昂贵的操作)。

基准测试结果(平均运行时间为1000000 elems,预热):

Authors solution:
447 ms
Ixx solution:
412 ms
Ixx solution2 (eliminated excessive map writes):
150 ms
My solution:
57 ms

我的解决方案涉及使用可变HashMap而不是不可变IntMap,并包括所有其他可能的优化。

Ixx的更新解决方案:

val tuple = (Map[Int, Int](), List[Int]())
val res = myList.foldLeft(tuple) {
  case ((m, s), i) =>
    val count = m.getOrElse(i, 0) + 1
    (if (count <= 3) m + (i -> count) else m, if (count == thresh) i :: s else s)
}

我的解决方案:

val map = new mutable.HashMap[Int, Int]()
val res = new ListBuffer[Int]
myList.foreach {
  i =>
    val c = map.getOrElse(i, 0) + 1
    if (c == thresh) {
      res += i
    }
    if (c <= thresh) {
      map(i) = c
    }
}

完整的微基准源可用here

答案 1 :(得分:1)

您可以使用foldleft收集匹配的项目,如下所示:

val tuple = (Map[Int,Int](), List[Int]())
myList.foldLeft(tuple) {
  case((m, s), i) => {
    val count = (m.getOrElse(i, 0) + 1) 
    (m + (i -> count), if (count == thresh) i :: s else s)
  }
}

我可以通过一个小清单来衡量性能提升约40%,所以它肯定是一种改进...

编辑使用List和前置,这需要不变的时间(见注释)。

答案 2 :(得分:1)

如果通过“更有效”来表示空间效率(在列表无限的极端情况下),则会有一个名为Count Min Sketch的概率数据结构来估计其中项目的频率。然后您可以丢弃频率低于阈值的那些。

来自Algebird库的Scala实现。

答案 3 :(得分:0)

您可以使用foldLeft逐步修改mutable.Set示例,同时使用Seq作为过滤器,使用withFilter迭代withFilter 。但是,因为我使用foldLeft我无法使用foreach并且必须使用import scala.collection.mutable def getItems[A](in: Seq[A], threshold: Int): Set[A] = { val counts: mutable.Map[A, Int] = mutable.Map.empty val result: mutable.Set[A] = mutable.Set.empty in.withFilter(!result(_)).foreach { x => counts.update(x, counts.getOrElse(x, 0) + 1) if (counts(x) >= threshold) { result += x } } result.toSet } 和可变地图:

Seq

因此,这会丢弃第一次在withFilter运行时已添加到结果集中的项目,因为Seq会过滤附加函数中的map, flatMap, foreach({ {1}})而不是返回已过滤的Seq

修改

我将我的解决方案更改为不使用Seq.count,这是愚蠢的,正如 Aivean 正确指出的那样。

使用 Aiveans microbench我可以看到它仍然比他的方法稍慢,但仍然优于作者的第一种方法。

Authors solution
377
Ixx solution: 
399
Ixx solution2 (eliminated excessive map writes): 
110
Sascha Kolbergs solution: 
72
Aivean solution: 
54