Scala:将地图列表与每个键的最大值合并的惯用方法?

Posted

技术标签:

【中文标题】Scala:将地图列表与每个键的最大值合并的惯用方法?【英文标题】:Scala: idiomatic way to merge list of maps with the greatest value of each key? 【发布时间】:2013-02-13 17:53:09 【问题描述】:

我有一个 Map[Int, Int] 列表,它们都有相同的键(从 1 到 20),我想将它们的内容合并到一个 Map[Int, Int] 中。

我已阅读 another post on stack overflow 关于合并使用 scalaz 库中的 |+| 的地图。

我想出了以下解决方案,但对我来说似乎很笨拙。

val defaultMap = (2 to ceiling).map((_,0)).toMap
val factors: Map[Int, Int] = (2 to ceiling). map(primeFactors(_)).
        foldRight(defaultMap)(mergeMaps(_, _))

def mergeMaps(xm: Map[Int, Int], ym: Map[Int, Int]): Map[Int,Int] = 
    def iter(acc: Map[Int,Int], other: Map[Int,Int], i: Int): Map[Int,Int] = 
      if (other.isEmpty) acc
      else iter(acc - i + (i -> math.max(acc(i), other(i))), other - i, i + 1)
    
    iter(xm, ym, 2)
  

def primeFactors(number: Int): Map[Int, Int] = 
  def iter(factors: Map[Int,Int], rem: Int, i: Int): Map[Int,Int] = 
    if (i > number) factors
    else if (rem % i == 0) iter(factors - i + (i -> (factors(i)+1)), rem / i, i)
    else iter(factors, rem, i + 1)
  
  iter((2 to ceiling).map((_,0)).toMap, number, 2)

解释:val factors 创建了一个映射列表,每个映射代表 2-20 数字的质因数;然后将这 18 个映射折叠成一个映射,其中包含每个键的最大值。

更新

使用@folone 的建议,我最终得到以下代码(对我的原始版本有明显改进,我不必将 Maps 更改为 HashMaps):

import scalaz._
import Scalaz._
import Tags._

/**
 * Smallest Multiple
 *
 * 2520 is the smallest number that can be divided by each of the numbers 
 * from 1 to 10 without any remainder. What is the smallest positive number 
 * that is evenly divisible by all of the numbers from 1 to 20?
 *
 * User: Alexandros Bantis
 * Date: 1/29/13
 * Time: 8:07 PM
 */
object Problem005 

  def findSmallestMultiple(ceiling: Int): Int = 
    val factors = (2 to ceiling).map(primeFactors(_).mapValues(MaxVal)).reduce(_ |+| _)
    (1 /: factors.map(m => intPow(m._1, m._2)))(_ * _)
  

  private def primeFactors(number: Int): Map[Int, Int] = 
    def iter(factors: Map[Int,Int], rem: Int, i: Int): Map[Int,Int] = 
      if (i > number) factors.filter(_._2 > 0).mapValues(MaxVal)
      else if (rem % i == 0) iter(factors - i + (i -> (factors(i)+1)), rem / i, i)
      else iter(factors, rem, i + 1)
    
    iter((2 to number).map((_,0)).toMap, number, 2)
  

  private def intPow(x: Int, y: Int): Int = 
    def iter(acc: Int, rem: Int): Int = 
      if (rem == 0) acc
      else iter(acc * x, rem -1)
    
    if (y == 0) 1 else iter(1, y)
  

【问题讨论】:

【参考方案1】:

此解决方案不适用于一般的Maps,但如果您使用的是immutable.HashMaps,则可以考虑使用merged method:

def merged[B1 >: B](that: HashMap[A, B1])(mergef: ((A, B1), (A, B1)) ⇒ (A, B1)): HashMap[A, B1]

创建一个新映射,它是 this 和参数哈希的合并 地图。

如果有两个键,则使用指定的冲突解决函数 相同的。冲突解决函数将始终占据首位 来自这个哈希映射的参数和来自那个的第二个参数。

平均而言,合并的方法比进行遍历的性能更高 并从头开始重建一个新的不可变哈希映射,或者 ++。

用例:

val m1 = immutable.HashMap[Int, Int](1 -> 2, 2 -> 3)
val m2 = immutable.HashMap[Int, Int](1 -> 3, 4 -> 5)
m1.merged(m2) 
  case ((k1, v1), (k2, v2)) => ((k1, math.max(v1, v2)))

【讨论】:

【参考方案2】:

正如您的标签所暗示的,您可能对 scalaz 解决方案感兴趣。如下:

> console
[info] Starting scala interpreter...
[info] 
Welcome to Scala version 2.10.0 (OpenJDK 64-Bit Server VM, Java 1.7.0_15).
Type in expressions to have them evaluated.
Type :help for more information.

scala> import scalaz._, Scalaz._, Tags._
import scalaz._
import Scalaz._
import Tags._

在最大运算下存在一个 Ints 的 Semigroup 实例:

scala> Semigroup[Int @@ MaxVal]
res0: scalaz.Semigroup[scalaz.@@[Int,scalaz.Tags.MaxVal]] = scalaz.Semigroup$$anon$9@15a9a9c6

我们就用它吧:

scala> val m1 = Map(1 -> 2, 2 -> 3) mapValues MaxVal
m1: scala.collection.immutable.Map[Int,scalaz.@@[Int,scalaz.Tags.MaxVal]] = Map(1 -> 2, 2 -> 3)

scala> val m2 = Map(1 -> 3, 4 -> 5) mapValues MaxVal
m2: scala.collection.immutable.Map[Int,scalaz.@@[Int,scalaz.Tags.MaxVal]] = Map(1 -> 3, 4 -> 5)

scala> m1 |+| m2
res1: scala.collection.immutable.Map[Int,scalaz.@@[Int,scalaz.Tags.MaxVal]] = Map(1 -> 3, 4 -> 5, 2 -> 3)

如果您对这种“标记”(@@)的工作原理感兴趣,这里有一个很好的解释:http://etorreborre.blogspot.de/2011/11/practical-uses-for-unboxed-tagged-types.html

【讨论】:

【参考方案3】:

Scala 2.13 开始,另一个仅基于标准库的解决方案包括在应用groupMapReduce 之前将Maps 合并为序列,groupMapReduce(顾名思义)相当于groupBy,后跟一个映射和值的归约步骤:

// val map1 = Map(1 -> 2, 2 -> 3)
// val map2 = Map(1 -> 3, 4 -> 5)
(map1.toSeq ++ map2).groupMapReduce(_._1)(_._2)(_ max _)
// Map[Int,Int] = Map(2 -> 3, 4 -> 5, 1 -> 3)

这个:

将两个映射连接为一个元组序列 (List((1,2), (2,3), (1,3), (4,5)))。为简洁起见,map2隐式转换为Seq 以采用map1.toSeq 的类型 - 但您可以选择使用map2.toSeq 使其显式化。

groups 元素基于它们的第一个元组部分(groupMapReduce 的组部分)

maps 将值分组到它们的第二个元组部分(组的映射部分MapReduce)

reduces 映射值 (_ max _) 取最大值(减少 groupMap 的一部分Reduce

【讨论】:

以上是关于Scala:将地图列表与每个键的最大值合并的惯用方法?的主要内容,如果未能解决你的问题,请参考以下文章

在 F# 中“合并”多个相同长度的列表的惯用方式?

Scala:地图的未来清单

Clojure 基于键的地图合并列表

如何将 Spark/Scala RDD 合并/加入到 List 中,以便 RDD 中的每个值与每个 List 项一起获得一个新行

Scala将Map序列减少为每个键都有最大值的Map

如何使用惯用的Scala替换(填充)来自另一个列表的选项列表中的None条目?