【问题标题】:Scala: Remove duplicates in list of objectsScala:删除对象列表中的重复项
【发布时间】:2011-04-24 04:30:48
【问题描述】:

我有一个对象列表List[Object],它们都是从同一个类中实例化的。这个类有一个字段必须是唯一的Object.property。迭代对象列表并删除具有相同属性的所有对象(但第一个对象)的最简洁方法是什么?

【问题讨论】:

  • 使用 Set 而不是 List 怎么样?另外,你为什么要处理对象,即几乎是类层次结构的顶部?

标签: list scala duplicates


【解决方案1】:
list.groupBy(_.property).map(_._2.head)

说明:groupBy 方法接受一个函数,该函数将元素转换为分组键。 _.property 只是elem: Object => elem.property 的简写(编译器生成一个唯一的名称,类似于x$1)。所以现在我们有一张地图Map[Property, List[Object]]Map[K,V] 扩展 Traversable[(K,V)]。所以它可以像列表一样被遍历,但元素是一个元组。这类似于 Java 的Map#entrySet()。 map 方法通过迭代每个元素并向其应用函数来创建一个新集合。在这种情况下,函数是_._2.head,它是elem: (Property, List[Object]) => elem._2.head 的简写。 _2 只是返回第二个元素的 Tuple 方法。第二个元素是 List[Object],head 返回第一个元素

要让结果成为你想要的类型:

import collection.breakOut
val l2: List[Object] = list.groupBy(_.property).map(_._2.head)(breakOut)

简单解释一下,map 实际上需要两个参数,一个函数和一个用于构造结果的对象。在第一个代码 sn-p 中,您看不到第二个值,因为它被标记为隐式,因此由编译器从范围内的预定义值列表中提供。结果通常是从映射的容器中获得的。这通常是一件好事。 List 上的 map 将返回 List,Array 上的 map 将返回 Array 等。然而,在这种情况下,我们希望将我们想要的容器表示为结果。这是使用breakOut方法的地方。它仅通过查看所需的结果类型来构建构建器(构建结果的东西)。它是一个泛型方法,编译器会推断出它的泛型类型,因为我们将 l2 显式键入为 List[Object],或者为了保持顺序(假设 Object#property 的类型为 Property):

list.foldRight((List[Object](), Set[Property]())) {
  case (o, cum@(objects, props)) => 
    if (props(o.property)) cum else (o :: objects, props + o.property))
}._1

foldRight 是一个接受初始结果的方法和一个接受元素并返回更新结果的函数。该方法迭代每个元素,根据将函数应用于每个元素来更新结果并返回最终结果。我们从右到左(而不是使用foldLeft 从左到右),因为我们在objects 之前 - 这是 O(1),但附加是 O(N)。还要注意这里的良好样式,我们正在使用模式匹配来提取元素。

在这种情况下,初始结果是一个空列表和一个集合的对(元组)。该列表是我们感兴趣的结果,该集合用于跟踪我们已经遇到的属性。在每次迭代中,我们检查集合 props 是否已经包含属性(在 Scala 中,obj(x) 被转换为 obj.apply(x)。在 Set 中,方法 applydef apply(a: A): Boolean。也就是说,接受一个元素如果存在或不存在则返回真/假)。如果属性存在(已经遇到),则按原样返回结果。否则更新结果以包含对象 (o :: objects) 并记录属性 (props + o.property)

更新:@andreypopp 想要一个通用方法:

import scala.collection.IterableLike
import scala.collection.generic.CanBuildFrom

class RichCollection[A, Repr](xs: IterableLike[A, Repr]){
  def distinctBy[B, That](f: A => B)(implicit cbf: CanBuildFrom[Repr, A, That]) = {
    val builder = cbf(xs.repr)
    val i = xs.iterator
    var set = Set[B]()
    while (i.hasNext) {
      val o = i.next
      val b = f(o)
      if (!set(b)) {
        set += b
        builder += o
      }
    }
    builder.result
  }
}

implicit def toRich[A, Repr](xs: IterableLike[A, Repr]) = new RichCollection(xs)

使用:

scala> list.distinctBy(_.property)
res7: List[Obj] = List(Obj(1), Obj(2), Obj(3))

还请注意,这非常有效,因为我们使用的是构建器。如果您有非常大的列表,您可能希望使用可变 HashSet 而不是常规集并基准测试性能。

【讨论】:

  • 如果你能提供一个快速的解释会很棒。我认为 Scala 足够新,不是每个人都会立即理解这一点。
  • 具体来说,_2 在这种情况下做了什么?
  • @Sudhir:_1 和 _2 是返回元组的第一个和第二个元素的方法。
  • 也许 scala 集合需要 distinct(A => B),通过 key 来区分?
  • +1,这个方法 - distinctBy - 应该添加到标准库中,我想。
【解决方案2】:

Scala 2.13 开始,大多数集合现在都提供了 distinctBy 方法,该方法在应用给定的转换函数后返回序列的所有元素,忽略重复项:

list.distinctBy(_.property)

例如:

List(("a", 2), ("b", 2), ("a", 5)).distinctBy(_._1) // List((a,2), (b,2))
List(("a", 2.7), ("b", 2.1), ("a", 5.4)).distinctBy(_._2.floor) // List((a,2.7), (a,5.4))

【讨论】:

  • 每个人的答案
【解决方案3】:

这是一个有点狡猾但快速的解决方案,可以保持秩序:

list.filterNot{ var set = Set[Property]()
    obj => val b = set(obj.property); set += obj.property; b}

虽然它在内部使用了一个 var,但我认为它比 foldLeft 解决方案更容易理解和阅读。

【讨论】:

  • 我显然在这里遗漏了一些东西。究竟什么是财产?
  • @parsa28: 属性是obj.property的类型
【解决方案4】:

上面有很多很好的答案。然而,distinctBy 已经在 Scala 中,但在一个不那么明显的地方。也许你可以像这样使用它

def distinctBy[A, B](xs: List[A])(f: A => B): List[A] =
  scala.reflect.internal.util.Collections.distinctBy(xs)(f)

【讨论】:

  • 我来这里只是为了支持并说反射包中的那些函数是 0 到没有意义。
【解决方案5】:

保留顺序:

def distinctBy[L, E](list: List[L])(f: L => E): List[L] =
  list.foldLeft((Vector.empty[L], Set.empty[E])) {
    case ((acc, set), item) =>
      val key = f(item)
      if (set.contains(key)) (acc, set)
      else (acc :+ item, set + key)
  }._1.toList

distinctBy(list)(_.property)

【讨论】:

  • 您可以使用 Seq[L] 获得更通用的解决方案。
【解决方案6】:

另一种解决方案

@tailrec
def collectUnique(l: List[Object], s: Set[Property], u: List[Object]): List[Object] = l match {
  case Nil => u.reverse
  case (h :: t) => 
    if (s(h.property)) collectUnique(t, s, u) else collectUnique(t, s + h.prop, h :: u)
}

【讨论】:

    【解决方案7】:

    我找到了一种使用 groupBy 的方法,只需一个中间步骤:

    def distinctBy[T, P, From[X] <: TraversableLike[X, From[X]]](collection: From[T])(property: T => P): From[T] = {
      val uniqueValues: Set[T] = collection.groupBy(property).map(_._2.head)(breakOut)
      collection.filter(uniqueValues)
    }
    

    像这样使用它:

    scala> distinctBy(List(redVolvo, bluePrius, redLeon))(_.color)
    res0: List[Car] = List(redVolvo, bluePrius)
    

    类似于 IttayD 的第一个解决方案,但它根据唯一值集过滤原始集合。如果我的预期是正确的,这将执行三个遍历:一个用于groupBy,一个用于map,一个用于filter。它维护原始集合的顺序,但不一定为每个属性取第一个值。例如,它本可以返回 List(bluePrius, redLeon)

    当然,IttayD 的解决方案仍然更快,因为它只进行一次遍历。

    我的解决方案也有一个缺点,如果集合中的Cars 实际上是相同的,那么两者都将在输出列表中。这可以通过删除filter 并直接返回uniqueValues 来修复,类型为From[T]。但是,CanBuildFrom[Map[P, From[T]], T, From[T]] 似乎不存在...欢迎提出建议!

    【讨论】:

      【解决方案8】:

      使用集合和从记录到键的函数,这会产生一个键不同的记录列表。尚不清楚 groupBy 是否会保留原始集合中的顺序。它甚至可能取决于集合的类型。我猜headlast 将始终产生最早的元素。

      collection.groupBy(keyFunction).values.map(_.head)
      

      Scala 什么时候会收到nubBy?它已经在 Haskell 中使用了几十年。

      【讨论】:

        【解决方案9】:

        如果您想删除重复项并保留列表的顺序,您可以尝试以下两种方法:

        val tmpUniqueList = scala.collection.mutable.Set[String]()
        val myUniqueObjects = for(o <- myObjects if tmpUniqueList.add(o.property)) yield o
        

        【讨论】:

          猜你喜欢
          • 2018-07-19
          • 1970-01-01
          • 2017-06-23
          • 2021-12-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-01-08
          相关资源
          最近更新 更多