Scala: Usuń duplikaty na liście obiektów

Mam listę obiektów List[Object], które są instancjami z tej samej klasy. Ta klasa ma pole, które musi być unikalne Object.property. Jaki jest najczystszy sposób na iterację listy obiektów i usunięcie wszystkich obiektów (oprócz pierwszego) z tą samą właściwością?

Author: om-nom-nom, 2010-10-12

8 answers

list.groupBy(_.property).map(_._2.head)

Wyjaśnienie: metoda groupBy akceptuje funkcję, która przekształca element w klucz do grupowania. _.property jest skrótem od elem: Object => elem.property (kompilator generuje unikalną nazwę, coś w rodzaju x$1). Więc teraz mamy mapę Map[Property, List[Object]]. A Map[K,V] rozszerza Traversable[(K,V)]. Więc może być przemierzana jak lista, ale elementy są krotką. Jest to podobne do Javy Map#entrySet(). Metoda map tworzy nową kolekcję poprzez iterację każdego elementu i zastosowanie do niego funkcji. W tym przypadku funkcją jest _._2.head, która jest skrótem od elem: (Property, List[Object]) => elem._2.head. _2 jest tylko metodą krotki, która zwraca drugi element. Drugim elementem jest List [Object] i head zwraca pierwszy element

Aby wynik był typem, który chcesz:

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

Aby krótko wyjaśnić, map w rzeczywistości oczekuje dwóch argumentów, funkcji i obiektu, który jest używany do konstruowania wyniku. W pierwszym fragmencie kodu nie widzisz drugiej wartości, ponieważ jest ona oznaczona jako domyślna i tak dostarczana przez kompilator z listy predefiniowane wartości w zakresie. Wynik jest zwykle uzyskiwany z zmapowanego kontenera. To zwykle dobrze. mapa na liście zwróci listę, mapa na tablicy zwróci tablicę itd. W tym przypadku jednak chcemy wyrazić kontener, który chcemy jako wynik. W tym miejscu stosuje się metodę breakOut. Tworzy konstruktor (rzecz, która buduje wyniki), patrząc tylko na pożądany typ wyniku. Jest to metoda generyczna, a kompilator wywnioskował jej typy generyczne, ponieważ jawnie wpisaliśmy l2 do być List[Object] lub, aby zachować porządek (zakładając, że {[18] } jest typu 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 jest metodą, która akceptuje początkowy wynik oraz funkcją, która akceptuje element i zwraca zaktualizowany wynik. Metoda iteruje każdy element, aktualizując wynik zgodnie z zastosowaniem funkcji do każdego elementu i zwracając wynik końcowy. Przechodzimy od prawej do lewej (zamiast od lewej do prawej z foldLeft), ponieważ poprzedzamy objects - to jest O(1), ale dodawanie to O (N). Zobacz też dobra stylizacja tutaj, używamy dopasowania wzoru, aby wyodrębnić elementy.

W tym przypadku wynikiem początkowym jest para (krotka) pustej listy i zbioru. Lista jest wynikiem, który nas interesuje, a zestaw służy do śledzenia, jakie właściwości już napotkaliśmy. W każdej iteracji sprawdzamy, czy zbiór props zawiera już właściwość (w Scali obj(x) jest tłumaczony na obj.apply(x)). W Set, metoda apply jest def apply(a: A): Boolean. Oznacza to, że akceptuje element i zwraca true / false, jeśli istnieje lub nie). Jeśli właściwość istnieje (już napotkana), wynik jest zwracany jako-is. W przeciwnym razie wynik zostanie zaktualizowany tak, aby zawierał obiekt (o :: objects) i właściwość zostanie zapisana (props + o.property)

Update: @ andreypopp chciał metody generycznej:

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)

Do użycia:

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

Zauważ również, że jest to dość wydajne, ponieważ używamy konstruktora. Jeśli masz naprawdę duże listy, możesz użyć zmiennego HashSet zamiast zwykłego zestawu i porównać wydajność.

 114
Author: IttayD,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-12-29 08:32:45

Oto trochę podstępne, ale szybkie rozwiązanie, które zachowuje porządek:

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

Chociaż używa wewnętrznie var, myślę, że jest łatwiejszy do zrozumienia i odczytania niż rozwiązanie foldLeft.

 13
Author: Landei,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-11-21 19:11:20

Jeszcze jedno rozwiązanie

@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)
}
 6
Author: walla,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-12-29 08:36:08

Z zachowaniem kolejności:

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)
 4
Author: Timothy Klim,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-12-24 18:20:01

Znalazłem sposób, aby to działało z groupBy, z jednym pośrednim krokiem:

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

Użyj go tak:

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

Podobne do pierwszego rozwiązania IttayD, ale filtruje oryginalną kolekcję na podstawie zbioru unikalnych wartości. Jeśli moje oczekiwania są prawidłowe, to wykonuje trzy przejścia: jeden dla groupBy, jeden dla map i jeden dla filter. Utrzymuje kolejność oryginalnego zbioru, ale niekoniecznie przyjmuje pierwszą wartość dla każdej nieruchomości. Na przykład, może zamiast tego powróciły List(bluePrius, redLeon).

Oczywiście rozwiązanie IttayD jest jeszcze szybsze, ponieważ robi tylko jedno przejście.

Moje rozwiązanie ma również tę wadę, że jeśli kolekcja ma Car s, które są rzeczywiście takie same, oba będą na liście wyjściowej. Można to naprawić, usuwając filter i zwracając uniqueValues bezpośrednio z typem From[T]. Wydaje się jednak, że CanBuildFrom[Map[P, From[T]], T, From[T]] nie istnieje... sugestie są mile widziane!

 2
Author: Jodiug,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-12-29 08:41:23

Wiele dobrych odpowiedzi powyżej. Jednak distinctBy jest już w Scali, ale w niezbyt oczywistym miejscu. Być może możesz użyć go jak

def distinctBy[B](xs: List[A])(f: A => B): List[A] =
  scala.reflect.internal.util.Collections.distinctBy(xs)(f)
 0
Author: Abel Terefe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-20 13:10:02

Począwszy od Scala 2.13, większość kolekcji jest teraz zaopatrzona w distinctBy metoda, która zwraca wszystkie elementy sekwencji ignorując duplikaty po zastosowaniu danej funkcji przekształcającej:

list.distinctBy(_.property)

Na przykład:

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))
 0
Author: Xavier Guihot,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-10-02 00:18:09

Nie wiem jakiej wersji Scali używasz, ale 2.8.2 zdecydowanie ma

list.distinct

Edit (fixing the down votes)

list.distinctBy
 -3
Author: stackoverflower,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-13 17:25:15