簡體   English   中英

Java Streams - 從其他兩個列表中獲取“對稱差異列表”

[英]Java Streams - Get a "symmetric difference list" from two other lists

我正在嘗試使用 Java 8 流來組合列表。 如何從兩個現有列表中獲取“對稱差異列表”(僅存在於一個列表中的所有對象)。 我知道如何獲得相交列表以及如何獲得聯合列表。

在下面的代碼中,我想要兩個汽車列表(bigCarList、smallCarList)中不相交的汽車。 我希望結果是包含 2 輛車(“Toyota Corolla”和“Ford Focus”)的列表

示例代碼:

public void testDisjointLists() {
    List<Car> bigCarList = get5DefaultCars();
    List<Car> smallCarList = get3DefaultCars();

    //Get cars that exists in both lists
    List<Car> intersect = bigCarList.stream().filter(smallCarList::contains).collect(Collectors.toList());

    //Get all cars in both list as one list
    List<Car> union = Stream.concat(bigCarList.stream(), smallCarList.stream()).distinct().collect(Collectors.toList());

    //Get all cars that only exist in one list
    //List<Car> disjoint = ???

}

public List<Car> get5DefaultCars() {
    List<Car> cars = get3DefaultCars();
    cars.add(new Car("Toyota Corolla", 2008));
    cars.add(new Car("Ford Focus", 2010));
    return cars;
}

public List<Car> get3DefaultCars() {
    List<Car> cars = new ArrayList<>();
    cars.add(new Car("Volvo V70", 1990));
    cars.add(new Car("BMW I3", 1999));
    cars.add(new Car("Audi A3", 2005));
    return cars;
}

class Car {
    private int releaseYear;
    private String name;
    public Car(String name) {
        this.name = name;
    }
    public Car(String name, int releaseYear) {
        this.name = name;
        this.releaseYear = releaseYear;
    }

    //Overridden equals() and hashCode()
}

根據您自己的代碼,有一個直接的解決方案:

List<Car> disjoint = Stream.concat(
    bigCarList.stream().filter(c->!smallCarList.contains(c)),
    smallCarList.stream().filter(c->!bigCarList.contains(c))
).collect(Collectors.toList());

只需過濾一個列表中未包含在另一個列表中的所有項目,反之亦然,並連接兩個結果。 這對於小列表非常有效,在考慮優化的解決方案(例如散列或使結果distinct() ,如果您既不想要,也不想要重復或特定順序,您應該問自己為什么要使用列表。

看起來您實際上想要Set s,而不是List s。 如果使用SetTagir Valeev 的解決方案是合適的。 但它不適用於List的實際語義,即如果源列表包含重復項則不起作用。


但是如果你使用的是Set ,代碼可以更簡單:

Set<Car> disjoint = Stream.concat(bigCarSet.stream(), smallCarSet.stream())
  .collect(Collectors.toMap(Function.identity(), t->true, (a,b)->null))
  .keySet();

這使用toMap收集器創建一個Map (該值無關緊要,我們在這里簡單地映射為true )並使用合並函數來處理重復項。 因為對於兩個集合,只有當兩個集合中都包含一個項目時才會發生重復,所以這些是我們想要刪除的項目。

Collectors.toMap文檔說合並函數被視為“提供給Map.merge(Object, Object, BiFunction) ”,我們可以從那里了解到,簡單地將重復對映射到null將刪除條目。

所以之后,地圖的keySet()包含不相交的集合。

像這樣的事情可能會奏效:

Stream.concat(bigCarList.stream(), smallCarList.stream())
      .collect(groupingBy(Function.identity(), counting()))
      .entrySet().stream()
      .filter(e -> e.getValue().equals(1L))
      .map(Map.Entry::getKey)
      .collect(toList());

在這里,我們首先將所有汽車收集到Map<Car, Long> ,其中 value 是遇到的此類汽車的數量。 之后,我們filter這個Map只留下恰好遇到一次的汽車,刪除計數並收集到最終List

一點數學

disjoint = 如果 A 和 B 的相交為空,則它們不相交。

不相交不是一個集合,它是一個指示器,顯示兩個集合是否不相交。 根據您的描述,我認為您在哪里搜索對稱差異

對稱差

但無論如何,如果您只想收集到新列表,那么您只需要一個收集器。

我做了一個創建收集器的方法。 此收集器僅“收集”值,其中謂詞被評估為真。 因此,如果您正在搜索對稱差異,那么您只需要一個謂詞。

  public void testDisjointLists() {
    List<Car> bigCarList = get5DefaultCars();
    List<Car> smallCarList = get3DefaultCars();

    Collector<Car, ArrayList<Car>, ArrayList<Car>> inter
        = produceCollector(car -> {
          return bigCarList.contains(car) && smallCarList.contains(car);
        });

    Collector<Car, ArrayList<Car>, ArrayList<Car>> symDiff
        = produceCollector(car -> {
          return bigCarList.contains(car) ^ smallCarList.contains(car);
        });

    //Get all cars in both list as one list
    List<Car> union
        = Stream.concat(bigCarList.stream(), smallCarList.stream()).distinct().collect(Collectors.toList());

    List<Car> intersect = union.stream().collect(inter);

    //Get all cars that only exist not exists in both Lists
    List<Car> symmetricDifference = union.stream().collect(symDiff);

    System.out.println("Union Cars:");
    union.stream().forEach(car -> System.out.println("Car: " + car));
    System.out.println("");

    System.out.println("Intersect Cars: ");
    intersect.stream().forEach(car -> System.out.println("Car: " + car));
    System.out.println("");

    System.out.println("Symmetric Difference: ");
    symmetricDifference.stream().forEach(car -> System.out.println("Car: " + car));
    System.out.println("");
  }

  public Collector<Car, ArrayList<Car>, ArrayList<Car>> produceCollector(Predicate<Car> predicate) {
    Collector<Car, ArrayList<Car>, ArrayList<Car>> collector = Collector.of(
        ArrayList::new,
        (al, car) -> {
          if (predicate.test(car)) {
            al.add(car);
          }
        },
        (al1, al2) -> {
          al1.addAll(al2);
          return al1;
        }
    );
    return collector;
  }

對於性能怪胎

經過一些研究,似乎收集器比第一個過濾器解決方案快 14 倍。

long before2 = System.nanoTime();
List<Car> intersect2 = union.stream().filter(car -> {
  return bigCarList.contains(car) && smallCarList.contains(car);
}).collect(Collectors.toList());
long after2 = System.nanoTime();
System.out.println("Time for first filter solution: " + (after2 - before2));


long before = System.nanoTime();
List<Car> intersect = union.stream().collect(inter);
long after = System.nanoTime();
System.out.println("Time for collector solution: " + (after - before));

第一個過濾器解決方案的時間:540906

收集器解決時間:37543

我所尋求的是兩個列表的對稱差異(我已經改變了問題):為什么我使用Lists而不是Set只是因為我在我的方法中有2個列表,否則一個集合會更合適。

解決方案就是“holger”給了我的東西。 謝謝。

List<Car> disjoint = Stream.concat(
bigCarList.stream().filter(c->!smallCarList.contains(c)),
smallCarList.stream().filter(c->!bigCarList.contains(c))

).collect(Collectors.toList());

這個列表實際上得到了兩輛車豐田和福特只存在於任何一個列表中(我嘗試了兩個列表與獨特的汽車,結果是正確的)。

謝謝你的幫助。

另一種方法,雖然不如一行流優雅:

    HashMap<Integer, Boolean> y = new HashMap<>();
    bigCarSet ().forEach(i -> y.put(i, !y.containsKey(i)));
    bigCarList().forEach(i -> y.put(i, !y.containsKey(i)));
    y.entrySet().stream().filter(Map.Entry::getValue).map(Map.Entry::getKey)
     .collect(Collectors.toList());

至少可以簡化為:

    HashMap<Integer, Boolean> y = new HashMap<>();
    Stream.concat(list1.stream(), list2.stream()).forEach(i -> y.put(i, !y.containsKey(i)));
    y.entrySet().stream().filter(Map.Entry::getValue)
                 .map(Map.Entry::getKey).collect(Collectors.toList());

OP 要求對稱差異。 對稱差可表示為:

  1. 並集和交集的區別:

    A △ B = (A ∪ B) - (B ∩ A)

  2. 或差異的結合:

    A △ B = (A – B) ∪ (B – A)

此答案的第一部分通過 #2 實現,而第二部分通過 #1 實現。 在這里,我將展示方法 #1 的變體:

List<Car> result = new ArrayList<>(bigCarList);
result.addAll(smallCarList); // (A ∪ B)

result.removeIf(c -> bigCarList.contains(c) && smallCarList.contains(c)); // (B ∩ A)

如果列表被轉換為集合,這可以被優化,因此使用containsO(1)

List<Car> bigCarList = get5DefaultCars();
List<Car> smallCarList = get3DefaultCars();

Set<Car> bigCarSet = new HashSet<>(bigCarList);
Set<Car> smallCarSet = new HashSet<>(smallCarList);

Set<Car> result = new LinkedHashSet<>(bigCarList);
result.addAll(smallCarList); // (A ∪ B)

result.removeIf(c -> bigCarSet.contains(c) && smallCarSet.contains(c)); // (B ∩ A)

帶有groupingBy的 lambda 解決方案
帶有true -key 的映射值在兩個列表中
帶有false鍵的映射值不相交

Map<Boolean,List<Car>> map = Stream.concat(bigCarList.stream(),
    smallCarList.stream()).collect(
        groupingBy( b -> bigCarList.stream().anyMatch( s -> b.equals( s ) )
            && smallCarList.stream().anyMatch( s -> b.equals( s ) ) ) );
List<Car> disjoint = map.get( false );  // [Toyota Corolla, Ford Focus]


相同的原理但更短的無內聯流:

Map<Boolean,List<Car>> map = Stream.concat(bigCarList.stream(),
    smallCarList.stream()).collect(
        groupingBy( b -> bigCarList.contains( b )
            && smallCarList.contains( b ) ) );
List<Car> disjoint = map.get( false );  // [Toyota Corolla, Ford Focus]

兩者都在處理重復項
意思是:一個列表中的重復項不包含在另一個列表中
如果數據量不是太大以至於您遇到磁盤空間問題,一個簡單的groupingBy - 沒有過濾或額外的查詢來減少結果集 - 應該是最清晰和最快的解決方案。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM