【发布时间】:2011-04-24 04:30:48
【问题描述】:
我有一个对象列表List[Object],它们都是从同一个类中实例化的。这个类有一个字段必须是唯一的Object.property。迭代对象列表并删除具有相同属性的所有对象(但第一个对象)的最简洁方法是什么?
【问题讨论】:
-
使用 Set 而不是 List 怎么样?另外,你为什么要处理对象,即几乎是类层次结构的顶部?
标签: list scala duplicates
我有一个对象列表List[Object],它们都是从同一个类中实例化的。这个类有一个字段必须是唯一的Object.property。迭代对象列表并删除具有相同属性的所有对象(但第一个对象)的最简洁方法是什么?
【问题讨论】:
标签: list scala duplicates
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 中,方法 apply 是 def 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 而不是常规集并基准测试性能。
【讨论】:
_2 在这种情况下做了什么?
distinctBy - 应该添加到标准库中,我想。
从 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))
【讨论】:
这是一个有点狡猾但快速的解决方案,可以保持秩序:
list.filterNot{ var set = Set[Property]()
obj => val b = set(obj.property); set += obj.property; b}
虽然它在内部使用了一个 var,但我认为它比 foldLeft 解决方案更容易理解和阅读。
【讨论】:
上面有很多很好的答案。然而,distinctBy 已经在 Scala 中,但在一个不那么明显的地方。也许你可以像这样使用它
def distinctBy[A, B](xs: List[A])(f: A => B): List[A] =
scala.reflect.internal.util.Collections.distinctBy(xs)(f)
【讨论】:
保留顺序:
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)
【讨论】:
另一种解决方案
@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)
}
【讨论】:
我找到了一种使用 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]] 似乎不存在...欢迎提出建议!
【讨论】:
使用集合和从记录到键的函数,这会产生一个键不同的记录列表。尚不清楚 groupBy 是否会保留原始集合中的顺序。它甚至可能取决于集合的类型。我猜head 或last 将始终产生最早的元素。
collection.groupBy(keyFunction).values.map(_.head)
Scala 什么时候会收到nubBy?它已经在 Haskell 中使用了几十年。
【讨论】:
如果您想删除重复项并保留列表的顺序,您可以尝试以下两种方法:
val tmpUniqueList = scala.collection.mutable.Set[String]()
val myUniqueObjects = for(o <- myObjects if tmpUniqueList.add(o.property)) yield o
【讨论】: