Generando un mapa de frecuencia para una cadena en Scala

Digamos que tengo una cadena, "hola", y quiero generar un mapa de frecuencia de caracteres:

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

Podría hacer esto iterativamente:

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

Al jugar en el REPL, descubrí que puedo hacer algo un poco más conciso y no usar una colección mutable:

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

Pero no conozco las características de rendimiento de groupBy() ni lo que sucede en el bloque pasado al mapa (como qué es exactamente p).

¿Cómo hago esto idiomáticamente usando los paradigmas funcionales en Scala?


Como antecedentes, vengo a Scala por primera vez desde Ruby. En Ruby, usaría inject pero no estoy seguro de cuál es la forma paralela de hacerlo en Scala:

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

preguntado el 24 de agosto de 12 a las 08:08

4 Respuestas

1) ¿Qué hace p significa?

groupBy toma una función que mapea un elemento a una clave de tipo K. Cuando se invoca en alguna colección Coll, devuelve un Map[K, Coll] que contiene asignaciones de claves K a todos los elementos que se asignaron a la misma clave.

Entonces, en tu caso, str.groupBy(_.toChar) produce una asignación de mapa de una clave k (que es un carácter) a una cadena con todos los elementos (caracteres) c tal que k == c.toChar. Obtienes esto:

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

A Map es un iterable de pares de claves y valores. En este caso, cada par es un carácter y una cadena de elementos. llamando al map operación en un Map implica el mapeo en estos pares - p es un par donde p._1 es un personaje, y p._2 es la cadena asociada (en la que puede llamar length, como hiciste anteriormente).

2) Cómo hacer esto idiomáticamente

Lo anterior es cómo hacerlo idiomáticamente - usando groupBy y map. Alternativamente, puede usar un mapa inmutable y una recursividad en la longitud de la cadena para calcular las frecuencias, o un mapa inmutable y un foldLeft.

3) característica de rendimiento

La mejor manera de punto de referencia para ver las diferencias. Aquí hay un par de microbenchmarks para una cadena altamente repetitiva (~3GHz iMac, JDK7, Scala 2.10.0 todas las noches):

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

Resultados:

  • Imperativo: $ 103 57 53 58 53 53 53 53 53 53

  • Combinadores: $ 72 51 63 56 53 52 52 54 53 53

  • Doblez: $ 163 62 71 62 57 57 57 58 57 57

Tenga en cuenta que cambiar la versión imperativa para usar 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
}

aparentemente es terriblemente lento debido al reenvío de cada put llamada:

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

Conclusión: el boxing y unboxing de personajes en este caso es lo suficientemente alto como para que las diferencias en el rendimiento entre estos enfoques sean difíciles de observar.

EDIT:

Actualización: es posible que desee utilizar Evaluación comparativa en línea de ScalaMeter en lugar de la Benchmark rasgo.

Respondido el 22 de enero de 15 a las 10:01

Ampliando la respuesta de Axel.

Su groupBy La solución ya es funcional. Solo hay una diminuta corrección que podría hacerlo más limpio:

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

La alternativa de la Scala a inject is foldLeft, foldRight, reduce, reduceOption dependiendo de cómo lo uses. La forma en que has usado inject en Ruby no es funcional, ya que su solución se basa en mutar h y en el mundo funcional la mutabilidad es un "no-no". Así es como haría la solución cerca de su inject pero en estilo funcional en Scala:

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

Obviamente groupBy se ve mucho mejor

Respondido 24 ago 12, 09:08

Creo que puedes hacer groupBy(identity).mapValues(_.size) porque una Cadena ya se trata como una secuencia de caracteres; no hay necesidad de convertir con toChar - ADN

Su ejemplo en Ruby se puede traducir casi directamente a Scala usando foldLeft e inmutable Map.

Aquí está una de las posibles soluciones:

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

En realidad, si está de acuerdo con la mutabilidad local, puede hacer algo como esto:

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

expresión hash(_) += 1 será desazucarado para c => hash(c) = hash(c) + 1 y luego a c => hash.update(c, hash.apply(c) + 1)

Esta solución debería ser más eficiente que las funcionales, porque no crea colecciones intermedias. También porque el método devuelve inmutable. collection.Map[Char, Int], el resultado se tratará como inmutable (siempre y cuando nadie realice una reducción insegura en él).

Respondido 24 ago 12, 10:08

También podría devolver un mapa que realmente es inmutable con hash.toMap - luigi plinge

a partir de Scala 2.13, podemos usar el groupMapReduce método que es (como sugiere su nombre) un equivalente de un groupBy seguido por mapValues y un paso de reducción:

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

Esta:

  • groups personajes (parte del grupo de GrupoMapa reducido)

  • maps cada ocurrencia de valor agrupado a 1 (asignar parte del grupoMapaReducir)

  • reduces valores dentro de un grupo de valores (_ + _) sumándolos (reducir parte de groupMapLa disminución ).

Esta es una versión equivalente realizado en una pasada a través de la secuencia de caracteres de:

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

Respondido el 07 de junio de 20 a las 10:06

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.