Java 8, Streams para encontrar os elementos duplicados


87

Estou tentando listar elementos duplicados na lista de inteiros, por exemplo,

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

usando Streams de jdk 8. Alguém já experimentou. Para remover as duplicatas, podemos usar a API distinta (). Mas e quanto a encontrar os elementos duplicados? Alguém pode me ajudar?


2
possível duplicação do fluxo
Tagir Valeev

Se você não deseja coletar o fluxo, isso basicamente se resume a "como posso olhar para mais de um item de uma vez em um fluxo"?
Thorbjørn Ravn Andersen

Definir itens <Integer> = new HashSet (); number.stream (). filter (n -> i! tems.add (n)). collect (Collectors.toSet ());
Saroj Kumar Sahoo

Respostas:


127

Você pode usar Collections.frequency:

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

11
O mesmo desempenho O (n ^ 2) da resposta @OussamaZoghlami , embora provavelmente mais simples. No entanto, aqui está um voto positivo. Bem-vindo ao StackOverflow!
Tagir Valeev

6
Como mencionado, esta é uma solução ^ 2 onde existe uma solução linear trivial. Eu não aceitaria isso no CR.
jwilner

3
Pode ser mais lento do que a opção @Dave, mas é mais bonito, então vou suportar o impacto no desempenho.
jDub9 de

@jwilner é seu ponto em relação a n ^ 2 solução referindo-se ao uso de Coleções.frequência em um filtro?
mancocapac

5
@mancocapac sim, é quadrático porque a chamada de frequência tem que visitar todos os elementos em números e está sendo chamada em todos os elementos. Assim, para cada elemento, visitamos cada elemento - n ^ 2 e desnecessariamente ineficiente.
jwilner

71

Exemplo básico. A primeira parte constrói o mapa de frequência, a segunda metade reduz a uma lista filtrada. Provavelmente não tão eficiente quanto a resposta de Dave, mas mais versátil (como se você quiser detectar exatamente dois etc.)

     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
Esta resposta é a correta porque é linear e não viola a regra do "predicado sem estado".
jwilner de

53

Você precisa de um conjunto ( allItemsabaixo) para conter todo o conteúdo da matriz, mas este é 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()requer um predicado sem estado. Sua "solução" é notavelmente semelhante ao exemplo de um predicado com estado fornecido no javadoc: docs.oracle.com/javase/8/docs/api/java/util/stream/…
Matt McHenry

1
@MattMcHenry: isso significa que esta solução tem o potencial de produzir um comportamento inesperado ou é apenas uma prática ruim?
IcedDante

7
@IcedDante Em um caso localizado como aquele em que você sabe com certeza que o stream está sequential(), provavelmente é seguro. No caso mais geral em que o fluxo pode estar parallel(), é praticamente garantido que ele quebrará de maneiras estranhas.
Matt McHenry

5
Além de produzir um comportamento inesperado em algumas situações, isso mistura paradigmas como Bloch argumenta que você não deve fazer na terceira edição do Effective Java. Se você estiver escrevendo isso, apenas use um loop for.
jwilner

6
Encontrado isso em estado selvagem sendo usado pela restrição UniqueElements do Validador do Hibernate .
Dave

14

Uma forma O (n) seria a seguinte:

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());

A complexidade do espaço dobraria nesta abordagem, mas esse espaço não é um desperdício; na verdade, agora temos a duplicata sozinha apenas como um Conjunto, bem como outro Conjunto com todas as duplicatas removidas também.


13

A biblioteca My StreamEx , que aprimora os fluxos Java 8, oferece uma operação especial distinct(atLeast)que pode reter apenas os elementos que aparecem pelo menos o número especificado de vezes. Portanto, seu problema pode ser resolvido assim:

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

Internamente é semelhante à solução @Dave, conta objetos, para suportar outras quantidades desejadas e é compatível com paralelismo (usa ConcurrentHashMappara fluxo paralelizado, mas HashMappara sequencial). Para grandes quantidades de dados, você pode obter uma aceleração usando .parallel().distinct(2).


26
A questão é sobre Java Streams, não bibliotecas de terceiros.
ᄂ ᄀ

9

Você pode obter o duplicado assim:

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
Não é uma operação O (n ^ 2)?
Trejkaz

4
Tente usarnumbers = Arrays.asList(400, 400, 500, 500);
Tagir Valeev

1
Isso é semelhante a criar um loop de 2 profundidades? for (..) {for (..)} Só curiosidade de como funciona internamente
redigaffi

Embora seja uma boa abordagem, ter streamdentro streamé caro.
Vishwa Ratna

4

Acho que as soluções básicas para a questão devem ser as seguintes:

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());

bem, não é recomendável realizar uma operação de filtro, mas para melhor compreensão, tenho usado, além disso, deve haver alguma filtragem customizada em versões futuras.


3

Um multiset é uma estrutura que mantém o número de ocorrências para cada elemento. Usando a implementação de Guava:

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

2

a criação de um mapa ou fluxo adicional consome tempo e espaço ...

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]


… E para a pergunta de qual é reivindicado ser um [duplicado]

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

Se você só precisa detectar a presença de duplicatas (em vez de listá-las, que é o que o OP queria), basta convertê-las em Lista e Conjunto e, em seguida, compare os tamanhos:

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

Gosto dessa abordagem porque tem menos lugares para erros.


0

Acho que tenho uma boa solução para resolver um problema como este - List => List com agrupamento por Something.a & Something.b. Existe uma definição estendida:

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

}

classe A, lista1 são apenas dados de entrada - a magia está no Objects.hash (...) :)


1
Aviso: Se Objects.hashproduzir o mesmo valor para (v.a_1, v.b_1, v.c_1, v.d_1)e (v.a_2, v.b_2, v.c_2, v.d_2), então eles serão considerados iguais e removidos como duplicatas, sem realmente verificar se os a's, b's, c's e d's são iguais. Este pode ser um risco aceitável, ou você pode querer usar uma função diferente daquela Objects.hashque é garantida para produzir um resultado único em seu domínio.
Marty Neal

0

Você tem que usar o idioma java 8 (steams)? Talvez uma solução simples seja mover a complexidade para uma estrutura de dados semelhante a um mapa que mantém os números como chave (sem repetir) e as vezes em que ocorre como um valor. Você poderia iterar esse mapa e fazer algo apenas com os números que ocorrem> 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

Experimente esta solução:

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

E quanto à verificação de índices?

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

1
Deve funcionar bem, mas também o desempenho O (n ^ 2) como algumas outras soluções aqui.
Florian Albrecht,
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.