Rimuovere i duplicati da un elenco <T> in C #


487

Qualcuno ha un metodo rapido per de-duplicare un elenco generico in C #?


4
Ti interessa l'ordine degli elementi nel risultato? Ciò escluderà alcune soluzioni.
Colonnello Panic,

Una soluzione su una riga:ICollection<MyClass> withoutDuplicates = new HashSet<MyClass>(inputList);
Harald Coppoolse,

Risposte:


227

Forse dovresti prendere in considerazione l'utilizzo di un HashSet .

Dal collegamento MSDN:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        HashSet<int> evenNumbers = new HashSet<int>();
        HashSet<int> oddNumbers = new HashSet<int>();

        for (int i = 0; i < 5; i++)
        {
            // Populate numbers with just even numbers.
            evenNumbers.Add(i * 2);

            // Populate oddNumbers with just odd numbers.
            oddNumbers.Add((i * 2) + 1);
        }

        Console.Write("evenNumbers contains {0} elements: ", evenNumbers.Count);
        DisplaySet(evenNumbers);

        Console.Write("oddNumbers contains {0} elements: ", oddNumbers.Count);
        DisplaySet(oddNumbers);

        // Create a new HashSet populated with even numbers.
        HashSet<int> numbers = new HashSet<int>(evenNumbers);
        Console.WriteLine("numbers UnionWith oddNumbers...");
        numbers.UnionWith(oddNumbers);

        Console.Write("numbers contains {0} elements: ", numbers.Count);
        DisplaySet(numbers);
    }

    private static void DisplaySet(HashSet<int> set)
    {
        Console.Write("{");
        foreach (int i in set)
        {
            Console.Write(" {0}", i);
        }
        Console.WriteLine(" }");
    }
}

/* This example produces output similar to the following:
 * evenNumbers contains 5 elements: { 0 2 4 6 8 }
 * oddNumbers contains 5 elements: { 1 3 5 7 9 }
 * numbers UnionWith oddNumbers...
 * numbers contains 10 elements: { 0 2 4 6 8 1 3 5 7 9 }
 */

11
è incredibilmente veloce ... 100.000 stringhe con List richiedono 400 secondi e 8 MB di RAM, la mia soluzione richiede 2,5 secondi e 28 MB, l'hashset richiede 0,1 secondi !!! e 11MB di ram
sasjaq il

3
HashSet non ha un indice , quindi non è sempre possibile usarlo. Devo creare una volta un enorme elenco senza duplicati e quindi utilizzarlo per ListViewla modalità virtuale. È stato velocissimo fare un HashSet<>primo e poi convertirlo in un List<>(quindi ListViewpuò accedere agli oggetti per indice). List<>.Contains()è troppo lento.
Sinatr

58
Aiuterebbe se ci fosse un esempio di come utilizzare un hashset in questo particolare contesto.
Nathan McKaskle,

23
Come può essere considerata una risposta? È un link
mcont

2
HashSet è eccezionale nella maggior parte dei casi. Ma se hai un oggetto come DateTime, lo confronta per riferimento e non per valore, quindi finirai comunque con i duplicati.
Jason McKindly,

813

Se stai usando .Net 3+, puoi usare Linq.

List<T> withDupes = LoadSomeData();
List<T> noDupes = withDupes.Distinct().ToList();

14
Quel codice fallirà perché .Distinct () restituisce un IEnumerable <T>. Devi aggiungere .ToList () ad esso.
ljs,

Questo approccio può essere utilizzato solo per l'elenco con valori semplici.
Polaris,

20
No, funziona con elenchi contenenti oggetti di qualsiasi tipo. Ma dovrai sostituire il comparatore predefinito per il tuo tipo. In questo modo: public override bool Equals (object obj) {...}
BaBu

1
È sempre una buona idea sostituire ToString () e GetHashCode () con le tue classi in modo che questo tipo di cose funzionerà.
B Seven,

2
Puoi anche usare il pacchetto MoreLinQ Nuget che ha un metodo di estensione .DistinctBy (). Abbastanza utile.
yu_ominae,

178

Che ne dite di:

var noDupes = list.Distinct().ToList();

In .net 3.5?


Duplica l'elenco?
darkgaze

1
@darkgaze crea solo un altro elenco con solo voci uniche. Quindi tutti i duplicati verranno rimossi e ti verrà lasciato un elenco in cui ogni posizione ha un oggetto diverso.
esagod

Questo funziona per un elenco di voci di elenco in cui i codici delle voci sono duplicati e deve ottenere un elenco univoco
venkat,

90

Basta inizializzare un HashSet con un elenco dello stesso tipo:

var noDupes = new HashSet<T>(withDupes);

Oppure, se si desidera che venga restituito un elenco:

var noDupsList = new HashSet<T>(withDupes).ToList();

3
... e se hai bisogno di un List<T>risultato usanew HashSet<T>(withDupes).ToList()
Tim Schmelter il

47

Ordinalo, quindi controlla due e due uno accanto all'altro, poiché i duplicati si raggrupperanno insieme.

Qualcosa come questo:

list.Sort();
Int32 index = list.Count - 1;
while (index > 0)
{
    if (list[index] == list[index - 1])
    {
        if (index < list.Count - 1)
            (list[index], list[list.Count - 1]) = (list[list.Count - 1], list[index]);
        list.RemoveAt(list.Count - 1);
        index--;
    }
    else
        index--;
}

Appunti:

  • Il confronto viene effettuato dalla parte anteriore a quella anteriore, per evitare di dover ricorrere all'elenco dopo ogni rimozione
  • Questo esempio ora utilizza Tuple valore C # per eseguire lo scambio, sostituire con il codice appropriato se non è possibile utilizzarlo
  • Il risultato finale non è più ordinato

1
Se non sbaglio, la maggior parte degli approcci sopra menzionati sono solo astrazioni di questa routine, giusto? Avrei adottato il tuo approccio qui, Lasse, perché è il modo in cui immagino mentalmente di muovermi tra i dati. Ma ora sono interessato alle differenze di rendimento tra alcuni dei suggerimenti.
Ian Patrick Hughes,

7
Implementali e cronometrali, unico modo per essere sicuri. Anche la notazione Big-O non ti aiuterà con le effettive metriche delle prestazioni, ma solo una relazione con effetti di crescita.
Lasse V. Karlsen,

1
Mi piace questo approccio, è più portabile in altre lingue.
Jerry Liang,

10
Non farlo. È super lento. RemoveAtè un'operazione molto costosa su unList
Clément

1
Clément ha ragione. Un modo per salvarlo sarebbe quello di avvolgerlo in un metodo che cede a un enumeratore e restituisce solo valori distinti. In alternativa, è possibile copiare i valori in un nuovo array o elenco.
JHubbard80,

33

Mi piace usare questo comando:

List<Store> myStoreList = Service.GetStoreListbyProvince(provinceId)
                                                 .GroupBy(s => s.City)
                                                 .Select(grp => grp.FirstOrDefault())
                                                 .OrderBy(s => s.City)
                                                 .ToList();

Ho questi campi nella mia lista: Id, StoreName, City, PostalCode Volevo mostrare la lista delle città in un menu a discesa che ha valori duplicati. soluzione: raggruppa per città, quindi scegli il primo per l'elenco.

Spero possa essere d'aiuto :)


31

Ha funzionato per me. semplicemente usa

List<Type> liIDs = liIDs.Distinct().ToList<Type>();

Sostituisci "Tipo" con il tipo desiderato, ad es. Int.


1
Distinct è in Linq, non System.Collections.Generic come riportato dalla pagina MSDN.
Almo,

5
Questa risposta (2012) sembra essere la stessa di altre due risposte su questa pagina del 2008?
Jon Schneider,

23

Come diceva kronoz in .Net 3.5 puoi usare Distinct().

In .Net 2 potresti imitarlo:

public IEnumerable<T> DedupCollection<T> (IEnumerable<T> input) 
{
    var passedValues = new HashSet<T>();

    // Relatively simple dupe check alg used as example
    foreach(T item in input)
        if(passedValues.Add(item)) // True if item is new
            yield return item;
}

Questo potrebbe essere usato per deduperare qualsiasi raccolta e restituirà i valori nell'ordine originale.

Normalmente è molto più veloce filtrare una raccolta (come entrambi Distinct()e in questo esempio) di quanto sarebbe rimuovere elementi da essa.


Il problema con questo approccio è che è O (N ^ 2) -ish, al contrario di un hashset. Ma almeno è evidente cosa sta facendo.
Tamas Czinege,

1
@DrJokepu - in realtà non mi rendevo conto che il HashSetcostruttore ha dedotto, il che lo rende migliore per la maggior parte delle circostanze. Tuttavia, ciò preserverebbe il criterio di ordinamento, cosa HashSetche non accade.
Keith,

1
HashSet <T> è stato introdotto il 3.5
thorn̈ il

1
@ spina davvero? Così difficile da tenere traccia. In tal caso, puoi semplicemente utilizzare un Dictionary<T, object>invece, sostituiscilo .Containscon .ContainsKeye .Add(item)con.Add(item, null)
Keith il

@Keith, secondo i miei test HashSetconserva l'ordine mentre Distinct()no.
Dennis T - Ripristina Monica -

13

Un metodo di estensione potrebbe essere un modo decente per andare ... qualcosa del genere:

public static List<T> Deduplicate<T>(this List<T> listToDeduplicate)
{
    return listToDeduplicate.Distinct().ToList();
}

E quindi chiama in questo modo, ad esempio:

List<int> myFilteredList = unfilteredList.Deduplicate();

11

In Java (suppongo che C # sia più o meno identico):

list = new ArrayList<T>(new HashSet<T>(list))

Se davvero volevi mutare la lista originale:

List<T> noDupes = new ArrayList<T>(new HashSet<T>(list));
list.clear();
list.addAll(noDupes);

Per preservare l'ordine, è sufficiente sostituire HashSet con LinkedHashSet.


5
in C # sarebbe: Elenco <T> noDupes = nuovo Elenco <T> (nuovo HashSet <T> (elenco)); list.Clear (); list.AddRange (noDupes);
smohamed

In C #, è più facile in questo modo: var noDupes = new HashSet<T>(list); list.Clear(); list.AddRange(noDupes);:)
nawfal,

10

Questo prende distinti (gli elementi senza duplicare gli elementi) e lo converte nuovamente in un elenco:

List<type> myNoneDuplicateValue = listValueWithDuplicate.Distinct().ToList();

9

Usa il metodo Union di Linq .

Nota: questa soluzione non richiede alcuna conoscenza di Linq, a parte il fatto che esiste.

Codice

Inizia aggiungendo quanto segue all'inizio del file di classe:

using System.Linq;

Ora è possibile utilizzare quanto segue per rimuovere i duplicati da un oggetto chiamato obj1:

obj1 = obj1.Union(obj1).ToList();

Nota: rinominare obj1il nome dell'oggetto.

Come funziona

  1. Il comando Unione elenca una di ciascuna voce di due oggetti sorgente. Poiché obj1 è entrambi oggetti sorgente, ciò riduce obj1 a una di ciascuna voce.

  2. Il ToList()restituisce una nuova lista. Ciò è necessario, poiché Linq comanda come Unionrestituisce il risultato come risultato IEnumerable invece di modificare l'elenco originale o restituire un nuovo elenco.


7

Come metodo di supporto (senza Linq):

public static List<T> Distinct<T>(this List<T> list)
{
    return (new HashSet<T>(list)).ToList();
}

Penso che Distinct sia già stato preso. A parte questo (se si rinomina il metodo) dovrebbe funzionare.
Andreas Reiff,

6

Se non vi interessa circa l'ordine si può solo spingere gli elementi in una HashSet, se non desidera mantenere l'ordine si può fare qualcosa di simile a questo:

var unique = new List<T>();
var hs = new HashSet<T>();
foreach (T t in list)
    if (hs.Add(t))
        unique.Add(t);

O il modo Linq:

var hs = new HashSet<T>();
list.All( x =>  hs.Add(x) );

Modifica: il HashSetmetodo è O(N)tempo e O(N)spazio durante l'ordinamento e quindi rendere unico (come suggerito da @ lassevk e altri) è il O(N*lgN)tempo e lo O(1)spazio, quindi non è così chiaro per me (come era a prima vista) che il modo di ordinamento è inferiore (il mio scuse per il voto temporaneo in giù ...)


6

Ecco un metodo di estensione per rimuovere duplicati adiacenti in situ. Chiama prima Sort () e passa nello stesso IComparer. Questo dovrebbe essere più efficiente della versione di Lasse V. Karlsen che chiama ripetutamente RemoveAt (con conseguente spostamento di più blocchi di memoria).

public static void RemoveAdjacentDuplicates<T>(this List<T> List, IComparer<T> Comparer)
{
    int NumUnique = 0;
    for (int i = 0; i < List.Count; i++)
        if ((i == 0) || (Comparer.Compare(List[NumUnique - 1], List[i]) != 0))
            List[NumUnique++] = List[i];
    List.RemoveRange(NumUnique, List.Count - NumUnique);
}

5

Installando il pacchetto MoreLINQ tramite Nuget, puoi facilmente distinguere l'elenco degli oggetti da una proprietà

IEnumerable<Catalogue> distinctCatalogues = catalogues.DistinctBy(c => c.CatalogueCode); 

3

Potrebbe essere più semplice assicurarsi che i duplicati non vengano aggiunti all'elenco.

if(items.IndexOf(new_item) < 0) 
    items.add(new_item)

1
Attualmente lo sto facendo in questo modo, ma più voci hai più tempo richiede il controllo dei duplicati.
Robert Strauch,

Ho lo stesso problema qui. Sto usando il List<T>.Containsmetodo ogni volta ma con più di 1.000.000 di voci. Questo processo rallenta la mia domanda. List<T>.Distinct().ToList<T>()Invece sto usando un primo.
RPDeshaies

Questo metodo è molto lento
darkgaze

3

Puoi usare Union

obj2 = obj1.Union(obj1).ToList();

7
Spiegare perché funzionerebbe renderebbe sicuramente migliore questa risposta
Igor B

2

Un altro modo in .Net 2.0

    static void Main(string[] args)
    {
        List<string> alpha = new List<string>();

        for(char a = 'a'; a <= 'd'; a++)
        {
            alpha.Add(a.ToString());
            alpha.Add(a.ToString());
        }

        Console.WriteLine("Data :");
        alpha.ForEach(delegate(string t) { Console.WriteLine(t); });

        alpha.ForEach(delegate (string v)
                          {
                              if (alpha.FindAll(delegate(string t) { return t == v; }).Count > 1)
                                  alpha.Remove(v);
                          });

        Console.WriteLine("Unique Result :");
        alpha.ForEach(delegate(string t) { Console.WriteLine(t);});
        Console.ReadKey();
    }

2

Esistono molti modi per risolvere: il problema dei duplicati nell'elenco, di seguito è uno di questi:

List<Container> containerList = LoadContainer();//Assume it has duplicates
List<Container> filteredList = new  List<Container>();
foreach (var container in containerList)
{ 
  Container duplicateContainer = containerList.Find(delegate(Container checkContainer)
  { return (checkContainer.UniqueId == container.UniqueId); });
   //Assume 'UniqueId' is the property of the Container class on which u r making a search

    if(!containerList.Contains(duplicateContainer) //Add object when not found in the new class object
      {
        filteredList.Add(container);
       }
  }

Saluti Ravi Ganesan


2

Ecco una soluzione semplice che non richiede alcun LINQ difficile da leggere o alcun ordinamento precedente dell'elenco.

   private static void CheckForDuplicateItems(List<string> items)
    {
        if (items == null ||
            items.Count == 0)
            return;

        for (int outerIndex = 0; outerIndex < items.Count; outerIndex++)
        {
            for (int innerIndex = 0; innerIndex < items.Count; innerIndex++)
            {
                if (innerIndex == outerIndex) continue;
                if (items[outerIndex].Equals(items[innerIndex]))
                {
                    // Duplicate Found
                }
            }
        }
    }

Con questo metodo hai più controllo sugli oggetti duplicati. Ancora di più se hai un database da aggiornare. Per innerIndex, perché non partire da outerIndex + 1 anziché iniziare ogni volta?
Nolmë Informatique,

2

La risposta di David J. è un buon metodo, non c'è bisogno di oggetti extra, ordinamento, ecc. Tuttavia può essere migliorato su:

for (int innerIndex = items.Count - 1; innerIndex > outerIndex ; innerIndex--)

Quindi il loop esterno va in fondo in alto per l'intero elenco, ma il loop interno va in fondo "fino a quando non viene raggiunta la posizione del loop esterno".

Il ciclo esterno si assicura che l'intero elenco sia elaborato, il ciclo interno trova i duplicati effettivi, questi possono accadere solo nella parte che il ciclo esterno non ha ancora elaborato.

Oppure, se non vuoi fare il bottom up per il loop interno, potresti far iniziare il loop interno da outerIndex + 1.


2

Tutte le risposte copiano gli elenchi o creano un nuovo elenco o utilizzano le funzioni lente o sono dolorosamente lente.

Per quanto ne so , questo è il metodo più veloce ed economico che conosco (supportato anche da un programmatore di grande esperienza specializzato nell'ottimizzazione della fisica in tempo reale).

// Duplicates will be noticed after a sort O(nLogn)
list.Sort();

// Store the current and last items. Current item declaration is not really needed, and probably optimized by the compiler, but in case it's not...
int lastItem = -1;
int currItem = -1;

int size = list.Count;

// Store the index pointing to the last item we want to keep in the list
int last = size - 1;

// Travel the items from last to first O(n)
for (int i = last; i >= 0; --i)
{
    currItem = list[i];

    // If this item was the same as the previous one, we don't want it
    if (currItem == lastItem)
    {
        // Overwrite last in current place. It is a swap but we don't need the last
       list[i] = list[last];

        // Reduce the last index, we don't want that one anymore
        last--;
    }

    // A new item, we store it and continue
    else
        lastItem = currItem;
}

// We now have an unsorted list with the duplicates at the end.

// Remove the last items just once
list.RemoveRange(last + 1, size - last - 1);

// Sort again O(n logn)
list.Sort();

Il costo finale è:

nlogn + n + nlogn = n + 2nlogn = O (nlogn) che è piuttosto carino.

Nota su RemoveRange: poiché non è possibile impostare il conteggio dell'elenco ed evitare di utilizzare le funzioni Rimuovi, non conosco esattamente la velocità di questa operazione, ma immagino che sia il modo più veloce.


2

Se hai classi di rimorchio Producte Customervogliamo rimuovere elementi duplicati dal loro elenco

public class Product
{
    public int Id { get; set; }
    public string ProductName { get; set; }
}

public class Customer
{
    public int Id { get; set; }
    public string CustomerName { get; set; }

}

È necessario definire una classe generica nel modulo seguente

public class ItemEqualityComparer<T> : IEqualityComparer<T> where T : class
{
    private readonly PropertyInfo _propertyInfo;

    public ItemEqualityComparer(string keyItem)
    {
        _propertyInfo = typeof(T).GetProperty(keyItem, BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public);
    }

    public bool Equals(T x, T y)
    {
        var xValue = _propertyInfo?.GetValue(x, null);
        var yValue = _propertyInfo?.GetValue(y, null);
        return xValue != null && yValue != null && xValue.Equals(yValue);
    }

    public int GetHashCode(T obj)
    {
        var propertyValue = _propertyInfo.GetValue(obj, null);
        return propertyValue == null ? 0 : propertyValue.GetHashCode();
    }
}

quindi, puoi rimuovere elementi duplicati dal tuo elenco.

var products = new List<Product>
            {
                new Product{ProductName = "product 1" ,Id = 1,},
                new Product{ProductName = "product 2" ,Id = 2,},
                new Product{ProductName = "product 2" ,Id = 4,},
                new Product{ProductName = "product 2" ,Id = 4,},
            };
var productList = products.Distinct(new ItemEqualityComparer<Product>(nameof(Product.Id))).ToList();

var customers = new List<Customer>
            {
                new Customer{CustomerName = "Customer 1" ,Id = 5,},
                new Customer{CustomerName = "Customer 2" ,Id = 5,},
                new Customer{CustomerName = "Customer 2" ,Id = 5,},
                new Customer{CustomerName = "Customer 2" ,Id = 5,},
            };
var customerList = customers.Distinct(new ItemEqualityComparer<Customer>(nameof(Customer.Id))).ToList();

questo codice rimuove gli elementi duplicati Idse si desidera rimuovere gli elementi duplicati da un'altra proprietà, è possibile modificare lo nameof(YourClass.DuplicateProperty) stesso nameof(Customer.CustomerName)quindi rimuovere gli elementi duplicati dalla CustomerNameproprietà.


1
  public static void RemoveDuplicates<T>(IList<T> list )
  {
     if (list == null)
     {
        return;
     }
     int i = 1;
     while(i<list.Count)
     {
        int j = 0;
        bool remove = false;
        while (j < i && !remove)
        {
           if (list[i].Equals(list[j]))
           {
              remove = true;
           }
           j++;
        }
        if (remove)
        {
           list.RemoveAt(i);
        }
        else
        {
           i++;
        }
     }  
  }

1

Una semplice implementazione intuitiva:

public static List<PointF> RemoveDuplicates(List<PointF> listPoints)
{
    List<PointF> result = new List<PointF>();

    for (int i = 0; i < listPoints.Count; i++)
    {
        if (!result.Contains(listPoints[i]))
            result.Add(listPoints[i]);
        }

        return result;
    }

Anche questo metodo è lento. Crea un nuovo elenco.
darkgaze
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.