Java 8, Streams для пошуку дублікатів елементів


87

Я намагаюся перерахувати повторювані елементи у цілочисельному списку, наприклад, наприклад,

List<Integer> numbers = Arrays.asList(new Integer[]{1,2,1,3,4,4});    

за допомогою Streams jdk 8. Хто-небудь пробував. Для видалення дублікатів ми можемо використовувати API різного (). Але як щодо пошуку дубльованих елементів? Хто-небудь може мені допомогти?



Якщо ви не хочете збирати потік, це по суті зводиться до того, "як я можу переглядати більше ніж один елемент у потоці"?
Торбьорн Равн Андерсен,

Встановити <Integer> items = new HashSet (); numbers.stream (). filter (n -> i! tems.add (n)). collect (Collectors.toSet ());
Сарой Кумар Саху

Відповіді:


127

Ви можете використовувати Collections.frequency:

numbers.stream().filter(i -> Collections.frequency(numbers, i) >1)
                .collect(Collectors.toSet()).forEach(System.out::println);

11
Така ж ефективність O (n ^ 2), як і у відповіді @OussamaZoghlami , хоча, ймовірно, простіша. Тим не менше, ось голос за. Ласкаво просимо до StackOverflow!
Тагір Валєєв

6
Як вже згадувалося, це рішення ^ 2, де існує тривіальний лінійний розв'язок. Я б не прийняв це в CR.
jwilner

3
Це може бути повільніше, ніж варіант @Dave, але він симпатичніший, тому я візьму хіт продуктивності.
jDub9

@jwilner - це ваша думка щодо рішення n ^ 2, що стосується використання Collections.frequency у фільтрі?
mancocapac

5
@mancocapac так, це квадратично, оскільки частотний виклик повинен відвідувати кожен елемент у цифрах, і він викликається для кожного елемента. Таким чином, для кожного елемента ми відвідуємо кожен елемент - n ^ 2 і непотрібно неефективний.
jwilner

71

Основний приклад. Перша половина створює карту частот, друга половина зменшує її до відфільтрованого списку. Можливо, не такий ефективний, як відповідь Дейва, але більш універсальний (наприклад, якщо ви хочете виявити рівно два тощо)

     List<Integer> duplicates = IntStream.of( 1, 2, 3, 2, 1, 2, 3, 4, 2, 2, 2 )
       .boxed()
       .collect( Collectors.groupingBy( Function.identity(), Collectors.counting() ) )
       .entrySet()
       .stream()
       .filter( p -> p.getValue() > 1 )
       .map( Map.Entry::getKey )
       .collect( Collectors.toList() );

12
Ця відповідь є правильною, оскільки вона є лінійною та не порушує правило "предикат без громадянства".
jwilner

54

Вам потрібен набір ( allItemsнижче), щоб вмістити весь вміст масиву, але це O (n):

Integer[] numbers = new Integer[] { 1, 2, 1, 3, 4, 4 };
Set<Integer> allItems = new HashSet<>();
Set<Integer> duplicates = Arrays.stream(numbers)
        .filter(n -> !allItems.add(n)) //Set.add() returns false if the item was already in the set.
        .collect(Collectors.toSet());
System.out.println(duplicates); // [1, 4]

18
filter()вимагає присудка без громадянства. Ваше "рішення" вражаюче схоже на приклад предиката з переліченим станом,
Метт Макгенрі,

1
@MattMcHenry: чи означає це, що це рішення може спричинити несподівану поведінку, чи це просто погана практика?
IcedDante

7
@IcedDante У локалізованому випадку, наприклад там, де ви точно знаєте, що потік є sequential(), це, мабуть, безпечно. У більш загальному випадку, коли потік може бути parallel(), він майже гарантовано проривається дивними способами.
Matt McHenry

5
На додаток до несподіваної поведінки в деяких ситуаціях, це змішує парадигми, як Блох стверджує, що ви не повинні в третьому виданні Effective Java. Якщо ви виявите, що пишете це, просто використовуйте цикл for.
jwilner

6
Знайдено це в дикій природі, що використовується обмеженням Hibernate Validator UniqueElements .
Дейв

14

Шлях O (n) буде таким, як показано нижче:

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 4, 4);
Set<Integer> duplicatedNumbersRemovedSet = new HashSet<>();
Set<Integer> duplicatedNumbersSet = numbers.stream().filter(n -> !duplicatedNumbersRemovedSet.add(n)).collect(Collectors.toSet());

Космічна складність у цьому підході зросте вдвічі, але цей простір - не марнотратство; насправді, ми тепер маємо дублікат окремо лише як набір, а також інший набір із усіма видаленими дублікатами.


13

Моя бібліотека StreamEx, яка покращує потоки Java 8, забезпечує спеціальну операцію, distinct(atLeast)яка може зберігати лише елементи, що з’являються принаймні вказану кількість разів. Тож вашу проблему можна вирішити так:

List<Integer> repeatingNumbers = StreamEx.of(numbers).distinct(2).toList();

Внутрішньо подібний до рішення @Dave, він підраховує об'єкти, щоб підтримувати інші потрібні величини, і є паралельним (він використовується ConcurrentHashMapдля паралелізованого потоку, але HashMapдля послідовного). Для великих обсягів даних ви можете прискорити використання .parallel().distinct(2).


26
Питання стосується Java Streams, а не сторонніх бібліотек.
ᄂ ᄀ

9

Ви можете отримати дублікат таким чином:

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 4, 4);
Set<Integer> duplicated = numbers
  .stream()
  .filter(n -> numbers
        .stream()
        .filter(x -> x == n)
        .count() > 1)
   .collect(Collectors.toSet());

11
Хіба це не операція O (n ^ 2)?
Трейказ

4
Спробуйте використатиnumbers = Arrays.asList(400, 400, 500, 500);
Тагір Валєєв

1
Це схоже на створення циклу 2 глибини? for (..) {for (..)} Просто цікаво, як внутрішньо це працює
redigaffi

Хоча це хороший підхід, але наявність streamусередині streamкоштує дорого.
Вішва Ратна,

4

Я думаю, що основні рішення цього питання мають бути такими:

Supplier supplier=HashSet::new; 
HashSet has=ls.stream().collect(Collectors.toCollection(supplier));

List lst = (List) ls.stream().filter(e->Collections.frequency(ls,e)>1).distinct().collect(Collectors.toList());

ну, не рекомендується виконувати операцію фільтрації, але для кращого розуміння я використовував її, крім того, у майбутніх версіях повинна бути якась спеціальна фільтрація.


3

Мультимножина - це структура, що підтримує кількість випадків для кожного елемента. Використання реалізації гуави:

Set<Integer> duplicated =
        ImmutableMultiset.copyOf(numbers).entrySet().stream()
                .filter(entry -> entry.getCount() > 1)
                .map(Multiset.Entry::getElement)
                .collect(Collectors.toSet());

2

створення додаткової карти чи потоку займає багато часу та місця ...

Set<Integer> duplicates = numbers.stream().collect( Collectors.collectingAndThen(
  Collectors.groupingBy( Function.identity(), Collectors.counting() ),
  map -> {
    map.values().removeIf( cnt -> cnt < 2 );
    return( map.keySet() );
  } ) );  // [1, 4]


... і для питання про яке стверджується, що це [дублікат]

public static int[] getDuplicatesStreamsToArray( int[] input ) {
  return( IntStream.of( input ).boxed().collect( Collectors.collectingAndThen(
      Collectors.groupingBy( Function.identity(), Collectors.counting() ),
      map -> {
        map.values().removeIf( cnt -> cnt < 2 );
        return( map.keySet() );
      } ) ).stream().mapToInt( i -> i ).toArray() );
}

1

Якщо вам потрібно лише виявити наявність дублікатів (замість того, щоб перераховувати їх, що саме і вимагав OP), просто перетворіть їх як у Список, так і в Набір, а потім порівняйте розміри:

    List<Integer> list = ...;
    Set<Integer> set = new HashSet<>(list);
    if (list.size() != set.size()) {
      // duplicates detected
    }

Мені подобається такий підхід, оскільки в ньому менше місць для помилок.


0

Думаю, у мене є гарне рішення, як вирішити подібну проблему - List => List with grouping by Something.a & Something.b. Існує розширене визначення:

public class Test {

    public static void test() {

        class A {
            private int a;
            private int b;
            private float c;
            private float d;

            public A(int a, int b, float c, float d) {
                this.a = a;
                this.b = b;
                this.c = c;
                this.d = d;
            }
        }


        List<A> list1 = new ArrayList<A>();

        list1.addAll(Arrays.asList(new A(1, 2, 3, 4),
                new A(2, 3, 4, 5),
                new A(1, 2, 3, 4),
                new A(2, 3, 4, 5),
                new A(1, 2, 3, 4)));

        Map<Integer, A> map = list1.stream()
                .collect(HashMap::new, (m, v) -> m.put(
                        Objects.hash(v.a, v.b, v.c, v.d), v),
                        HashMap::putAll);

        list1.clear();
        list1.addAll(map.values());

        System.out.println(list1);
    }

}

клас A, list1 це просто вхідні дані - магія знаходиться в Objects.hash (...) :)


1
Попередження: Якщо Objects.hashдля (v.a_1, v.b_1, v.c_1, v.d_1)і буде отримано одне і те ж значення (v.a_2, v.b_2, v.c_2, v.d_2), тоді вони вважатимуться рівними та видалятимуться як дублікати, фактично не перевіряючи, що значення a, b, c та d однакові. Це може бути прийнятним ризиком, або ви можете використовувати функцію, відмінну від Objects.hashякої гарантовано дасть унікальний результат у вашому домені.
Марті Ніл

0

Чи потрібно вам використовувати ідіоми Java (стими)? Перфапсом простим рішенням було б перенести складність на подібну структуру даних на карті, яка містить цифри як ключ (не повторюючи) і час, який він має як значення. Ви могли б їм повторити, що карта робить щось лише з тими числами, які є ocurrs> 1.

import java.lang.Math;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;

public class RemoveDuplicates
{
  public static void main(String[] args)
  {
   List<Integer> numbers = Arrays.asList(new Integer[]{1,2,1,3,4,4});
   Map<Integer,Integer> countByNumber = new HashMap<Integer,Integer>();
   for(Integer n:numbers)
   {
     Integer count = countByNumber.get(n);
     if (count != null) {
       countByNumber.put(n,count + 1);
     } else {
       countByNumber.put(n,1);
     }
   }
   System.out.println(countByNumber);
   Iterator it = countByNumber.entrySet().iterator();
    while (it.hasNext()) {
        Map.Entry pair = (Map.Entry)it.next();
        System.out.println(pair.getKey() + " = " + pair.getValue());
    }
  }
}

0

Спробуйте це рішення:

public class Anagramm {

public static boolean isAnagramLetters(String word, String anagramm) {
    if (anagramm.isEmpty()) {
        return false;
    }

    Map<Character, Integer> mapExistString = CharCountMap(word);
    Map<Character, Integer> mapCheckString = CharCountMap(anagramm);
    return enoughLetters(mapExistString, mapCheckString);
}

private static Map<Character, Integer> CharCountMap(String chars) {
    HashMap<Character, Integer> charCountMap = new HashMap<Character, Integer>();
    for (char c : chars.toCharArray()) {
        if (charCountMap.containsKey(c)) {
            charCountMap.put(c, charCountMap.get(c) + 1);
        } else {
            charCountMap.put(c, 1);
        }
    }
    return charCountMap;
}

static boolean enoughLetters(Map<Character, Integer> mapExistString, Map<Character,Integer> mapCheckString) {
    for( Entry<Character, Integer> e : mapCheckString.entrySet() ) {
        Character letter = e.getKey();
        Integer available = mapExistString.get(letter);
        if (available == null || e.getValue() > available) return false;
    }
    return true;
}

}

0

А як щодо перевірки індексів?

        numbers.stream()
            .filter(integer -> numbers.indexOf(integer) != numbers.lastIndexOf(integer))
            .collect(Collectors.toSet())
            .forEach(System.out::println);

1
Повинна працювати нормально, але також ефективність O (n ^ 2), як деякі інші рішення тут.
Флоріан Альбрехт,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.