在Scala中生成字符串的频率映射

时间:2012-08-24 07:42:42

标签: string scala map

假设我有一个字符串,“你好”,我想生成一个字符频率图:

Map[Char,Int] = Map(h -> 1, e -> 1, o -> 1, l -> 2)

我可以迭代地执行此操作:

val str = "hello"
var counts = new scala.collection.mutable.HashMap[Char,Int]
for (i <- str) {
    if (counts.contains(i))
        counts.put(i, counts(i) + 1)
    else
        counts.put(i, 1)
}

通过在REPL中乱搞,我发现我可以做一些更简洁的事情,而不是使用可变集合:

> str.groupBy(_.toChar).map{ p => (p._1, p._2.length)}
scala.collection.immutable.Map[Char,Int] = Map(h -> 1, e -> 1, o -> 1, l -> 2)

但是我不知道groupBy()的性能特征,也不知道传递给map的块中发生了什么(就像p恰当的那样)。

如何使用Scala中的功能范例以惯用方式执行此操作?


对于背景,我是第一次从Ruby来到Scala。在Ruby中,我会使用inject,但我不确定在Scala中执行此操作的并行方法是:

counts = str.each_byte.inject(Hash.new(0)){ |h, c| h[c] += 1; h}

4 个答案:

答案 0 :(得分:35)

1)p是什么意思?

groupBy采用将元素映射到K类型的键的函数。在某个集合Coll上调用时,它会返回Map[K, Coll],其中包含从键K到映射到同一键的所有元素的映射。

因此,在您的情况下,str.groupBy(_.toChar)会生成从键k(这是一个字符)到包含所有元素(字符)c的字符串的映射映射,以便{ {1}}。 你明白了:

k == c.toChar

Map(e -> "e", h -> "h", l -> "ll", o -> "o") 是一组可重复的键和值。在这种情况下,每对都是一个字符和一串元素。在Map上调用map操作涉及映射这些对 - Map是一对p是一个字符,p._1是关联字符串(您可以像上面那样拨打p._2

2)如何惯用

以上是如何使用lengthgroupBy惯用法。或者,您可以在字符串长度上使用不可变映射和递归来计算频率,或使用不可变映射和map

3)性能特征

最好benchmark看看差异。 这里有几个microbenchmark用于高度重复的字符串(~3GHz iMac,JDK7,Scala 2.10.0每晚):

foldLeft

结果:

  • 势在必行:object Imperative extends testing.Benchmark { val str = "abc" * 750000 def run() { var counts = new scala.collection.mutable.HashMap[Char,Int] var i = 0 val until = str.length while (i < until) { var c = str(i) if (counts.contains(c)) counts.put(c, counts(c) + 1) else counts.put(c, 1) i += 1 } //println(f) } } object Combinators extends testing.Benchmark { val str = "abc" * 750000 def run() { val f = str.groupBy(_.toChar).map(p => (p._1, p._2.length)) } } object Fold extends testing.Benchmark { val str = "abc" * 750000 def run() { val f = str.foldLeft(Map[Char, Int]() withDefaultValue 0){(h, c) => h.updated(c, h(c)+1)} } }

  • 组合器:$ 103 57 53 58 53 53 53 53 53 53

  • 折叠:$ 72 51 63 56 53 52 52 54 53 53

请注意,更改命令式版本以使用$ 163 62 71 62 57 57 57 58 57 57

withDefaultValue
由于转发了每个var counts = new scala.collection.mutable.HashMap[Char,Int].withDefaultValue(0) var i = 0 val until = str.length while (i < until) { var c = str(i) counts.put(c, counts(c) + 1) i += 1 } 电话,

显然非常慢:

  • putwithDefaultValue

结论:在这种情况下,角色的装箱和拆箱足够高,因此难以观察到这些方法之间的性能差异。

编辑:

更新:您可能希望使用ScalaMeter inline benchmarking代替$ 133 87 109 106 101 100 101 100 101 101特征。

答案 1 :(得分:25)

扩展Axel的答案。

您的groupBy解决方案已经正常运行。它只是微小的修正,可以使它更清洁:

str.groupBy(_.toChar).mapValues(_.size)

Scala替代inject的替代方法是foldLeftfoldRightreducereduceOption取决于您使用它的方式。您在Ruby中使用inject的方式不起作用,因为您的解决方案基于变异h,而在功能世界中,可变性是“禁忌”。以下是您在Scala中以接近inject但功能样式的方式执行解决方案的方法:

str.foldLeft( Map[Char, Int]() ){ (m, c) => m + (c -> (m.getOrElse(c, 0) + 1)) }

显然groupBy看起来好多了。

答案 2 :(得分:11)

您在ruby上的示例几乎可以使用foldLeft和不可变Map直接转换为Scala。

以下是可能的解决方案之一:

str.foldLeft(Map[Char, Int]() withDefaultValue 0){(h, c) => h.updated(c, h(c)+1)}

实际上,如果你对局部可变性感到满意,你可以做这样的事情:

def charFrequencies(str: String): collection.Map[Char, Int] = {
  val hash = collection.mutable.HashMap.empty[Char, Int] withDefaultValue 0
  str foreach { hash(_) += 1 }
  hash
}

表达式hash(_) += 1将被移至c => hash(c) = hash(c) + 1,然后移至c => hash.update(c, hash.apply(c) + 1)

此解决方案应该比功能解决方案更有效,因为它不会创建中间集合。此外,因为方法返回不可变collection.Map[Char, Int],结果将被视为不可变(只要没有人会对它执行不安全的向下转换)。

答案 3 :(得分:2)

Scala 2.13开始,我们可以使用groupMapReduce方法,该方法(顾名思义)等效于groupBy,后跟mapValues和缩减步骤:< / p>

"hello".groupMapReduce(identity)(_ => 1)(_ + _)
// immutable.Map[Char,Int] = Map(e -> 1, h -> 1, l -> 2, o -> 1)

此:

  • group个字符( MapReduce的组部分)

  • map将每个分组值出现的次数设为1(组 Map Reduce的映射部分)

  • reduce在一组值(_ + _)中的值相加(减少groupMap Reduce 的一部分)。

这是等效于performed in one pass的版本,通过以下字符组成:

"hello".groupBy(identity).mapValues(_.map(_ => 1).reduce(_+_))