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?

Siva
fonte
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);
Bao Dinh
fonte
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() );
RobAu
fonte
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]
Dave
fonte
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.

Thomas Mathew
fonte
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).

Tagir Valeev
fonte
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());
Oussama Zoghlami
fonte
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.

Prashant
fonte
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());
numéro6
fonte
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() );
}
Kaplan
fonte
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.

Patrick
fonte
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 (...) :)

Zhurov Konstantin
fonte
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());
    }
  }
}
Vencedor
fonte
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;
}

}
Ilia Galperin
fonte
0

E quanto à verificação de índices?

        numbers.stream()
            .filter(integer -> numbers.indexOf(integer) != numbers.lastIndexOf(integer))
            .collect(Collectors.toSet())
            .forEach(System.out::println);
bagom
fonte
1
Deve funcionar bem, mas também o desempenho O (n ^ 2) como algumas outras soluções aqui.
Florian Albrecht,