【问题标题】:Generating a frequency map for a string in Scala在 Scala 中为字符串生成频率图
【发布时间】:2012-08-19 18:22:51
【问题描述】:

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

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}

【问题讨论】:

    标签: string scala map


    【解决方案1】:

    1) p 是什么意思?

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

    因此,在您的情况下,str.groupBy(_.toChar) 生成从键 k(这是一个字符)到包含所有元素(字符)c 的字符串的映射映射,例如 k == c.toChar。 你得到这个:

    Map(e -> "e", h -> "h", l -> "ll", o -> "o")
    

    Map 是键和值对的可迭代对象。在这种情况下,每一对都是一个字符和一串元素。在Map 上调用map 操作涉及到这些对的映射——p 是一对其中p._1 是一个字符,p._2 是关联的字符串(您可以在其上调用length,如你在上面做了)。

    2) 如何以惯用方式做到这一点

    以上是如何使用惯用的方法 - 使用 groupBymap。或者,您可以使用不可变映射和对字符串长度的递归来计算频率,或者使用不可变映射和 foldLeft

    3) 性能特点

    最好 benchmark 看看差异。 这里有几个针对高重复字符串的微基准测试(~3GHz iMac、JDK7、Scala 2.10.0 nightly):

    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
    }
    

    由于转发每个 put 呼叫,显然非常慢:

    • withDefaultValue: $ 133 87 109 106 101 100 101 100 101 101

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

    编辑:

    更新:您可能希望使用 ScalaMeter inline benchmarking 代替 Benchmark 特征。

    【讨论】:

      【解决方案2】:

      扩展 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 看起来好多了。

      【讨论】:

      • 我认为你可以这样做 groupBy(identity).mapValues(_.size) 因为字符串已经被视为一个字符序列;无需转换为toChar
      【解决方案3】:

      使用foldLeft 和不可变的Map 几乎可以将您关于ruby 的示例直接转换为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 =&gt; hash(c) = hash(c) + 1,然后是c =&gt; hash.update(c, hash.apply(c) + 1)

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

      【讨论】:

      • 你也可以用hash.toMap返回一个真正不可变的地图
      【解决方案4】:

      Scala 2.13 开始,我们可以使用groupMapReduce 方法(顾名思义)相当于groupBy 后跟mapValues 和reduce 步骤:

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

      这个:

      • groups 个字符(groupMapReduce 的分组部分)

      • maps 每个分组值出现为 1(映射组的一部分MapReduce)

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

      这是 performed in one pass 通过以下字符序列的等效版本:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-01-30
        • 1970-01-01
        • 1970-01-01
        • 2014-06-19
        • 2015-02-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多