Видаліть дублікати зі списку <T> у C #


Відповіді:


227

Можливо, вам варто подумати про використання HashSet .

За посиланням 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
її неймовірний швидкий ... 100 000 струн з List займає 400s і 8MB ram, моє власне рішення займає 2,5s і 28MB, хешсет займає 0,1s !!! та 11 Мб оперативної пам’яті
sasjaq

3
HashSet не має індексу , тому використовувати його не завжди можливо. Я повинен створити один раз величезний список без дублікатів, а потім використовувати його ListViewу віртуальному режимі. Зробити HashSet<>перший було дуже швидко, а потім перетворити його в List<>(так ListViewможна отримати доступ до елементів за індексом). List<>.Contains()занадто повільно.
Синатр

58
Допоможе, якби був приклад того, як використовувати хешсет у цьому конкретному контексті.
Натан Маккаскле

23
Як це можна вважати відповіддю? Це посилання
mcont

2
HashSet чудово підходить для більшості обставин. Але якщо у вас є такий об'єкт, як DateTime, він порівнює за посиланням, а не за значенням, тому ви все одно будете дублювати дублікати.
Джейсон МакКіндлі

813

Якщо ви використовуєте .Net 3+, ви можете використовувати Linq.

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

14
Цей код вийде з ладу, оскільки .Distinct () повертає IEnumerable <T>. Ви повинні додати .ToList () до нього.
ljs

Цей підхід можна використовувати лише для списку з простими значеннями.
Polaris

20
Ні, він працює зі списками, що містять об'єкти будь-якого типу. Але вам доведеться перекрити порівняльний за замовчуванням для вашого типу. Як і так: public override bool Дорівнює (object obj) {...}
BaBu

1
Завжди корисно переосмислити ToString () та GetHashCode () своїми класами, щоб ця справа працювала.
B Сім

2
Ви також можете використовувати пакет MoreLinQ Nuget, який має метод розширення .DistinctBy (). Досить корисна.
yu_ominae

178

Як щодо:

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

В .net 3.5?


Чи дублює це список?
darkgaze

1
@darkgaze це просто створює ще один список із лише унікальними записами. Таким чином, будь-які дублікати будуть видалені, і вам залишиться список, де кожна позиція має інший об'єкт.
гексагод

Чи працює це за списком списку предметів списку, де коди товарів дублюються і потрібно отримати унікальний список
venkat

90

Просто ініціалізуйте HashSet зі списком одного типу:

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

Або якщо ви хочете, щоб список повернувся:

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

3
... а якщо вам потрібно використовувати List<T>як результатnew HashSet<T>(withDupes).ToList()
Тім Шмелтер

47

Сортуйте його, а потім перевірте два та два поруч, оскільки дублікати зіб’ються між собою.

Щось на зразок цього:

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

Примітки:

  • Порівняння проводиться ззаду спереду, щоб уникнути необхідності вдаватися до списку після кожного видалення
  • Цей приклад зараз використовує C # Value Tuples для заміни, заміни відповідним кодом, якщо ви не можете його використовувати
  • Кінцевий результат вже не сортується

1
Якщо я не помиляюся, більшість згаданих вище підходів - це лише абстракції цієї самої процедури, правда? Я б взяв ваш підхід тут, Лассе, тому що я подумки уявляю, як рухається по даних. Але зараз мене цікавлять відмінності у виконанні деяких пропозицій.
Ян Патрік Х'юз

7
Виконайте їх та вчасно їх, єдиний спосіб бути впевненим. Навіть нотація Big-O не допоможе вам у фактичних показниках ефективності, а лише у відносинах ефекту зростання.
Лассе В. Карлсен

1
Мені подобається такий підхід, він більш портативний для інших мов.
Джеррі Лян

10
Не робіть цього. Це дуже повільно. RemoveAtце дуже дорога операція наList
Clément

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

33

Мені подобається використовувати цю команду:

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

У мене в списку є такі поля: Id, StoreName, City, PostalCode. Я хотів показати список міст у спадному меню, що має повторювані значення. рішення: Групуйте по містах, тоді виберіть перший для списку.

Я сподіваюся, що це допомагає :)


31

Це працювало для мене. просто використовувати

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

Замініть "Тип" на потрібний тип, наприклад, int.


1
Відмінність знаходиться в Linq, а не System.Collections.Generic, як повідомляється на сторінці MSDN.
Алмо

5
Ця відповідь (2012 р.) Схожа на дві інші відповіді на цій сторінці, які є з 2008 року?
Джон Шнайдер

23

Як сказав kronoz в .Net 3.5, ви можете використовувати Distinct().

У .Net 2 ви можете це імітувати:

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

Це може бути використане для виведення будь-якої колекції і поверне значення у вихідному порядку.

Зазвичай фільтрувати колекцію (як Distinct()і цей зразок) зазвичай набагато швидше, ніж було б видалити з неї предмети.


Проблема цього підходу полягає в тому, що це O (N ^ 2) -ish, на відміну від хештету. Але принаймні видно, що це робить.
Тамас Цінеге

1
@DrJokepu - насправді я не розумів, що HashSetконструктор виводився, що робить його кращим для більшості обставин. Однак це збереже порядок сортування, якого HashSetнемає.
Кіт

1
HashSet <T> був представлений в 3.5
thorn̈

1
@thorn дійсно? Так важко відстежувати. У такому випадку ви можете просто використовувати Dictionary<T, object>замість цього, замінити .Containsна .ContainsKeyі .Add(item)з.Add(item, null)
Кіт

@Keith, згідно з моїм тестуванням, HashSetзберігає порядок, а Distinct()ні.
Денніс Т - Відновлення Моніки--

13

Метод розширення може бути гідним шляхом ... щось подібне:

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

А потім дзвоніть так, наприклад:

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

11

У Java (я припускаю, що C # більш-менш однаковий):

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

Якщо ви дійсно хотіли вимкнути оригінальний список:

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

Щоб зберегти порядок, просто замініть HashSet на LinkedHashSet.


5
у C # це було б: Список <T> noDupes = новий Список <T> (новий HashSet <T> (список)); list.Clear (); list.AddRange (noDupes);
посміхнувся

В C # простіше так: var noDupes = new HashSet<T>(list); list.Clear(); list.AddRange(noDupes);:)
nawfal

10

Це відрізняє (елементи без дублювання елементів) і знову перетворює їх у список:

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

9

Використовуйте метод Linq's Union .

Примітка: для цього рішення не потрібно знати Linq, окрім того, що воно існує.

Код

Почніть з додавання наступного у верхній частині вашого класу:

using System.Linq;

Тепер ви можете скористатись наступним для видалення дублікатів із об’єкта, який називається obj1:

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

Примітка: Перейменуйте obj1на ім'я вашого об'єкта.

Як це працює

  1. Команда Union перераховує один з кожного запису двох вихідних об'єктів. Оскільки obj1 є обома вихідними об'єктами, це зводить obj1 до одного з кожного запису.

  2. ToList()Повертає новий список. Це необхідно, тому що команди команд Linq як Unionповертають результат як результат IEnumerable замість зміни оригінального списку або повернення нового списку.


7

Як допоміжний метод (без Linq):

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

Я думаю, що відмінність вже прийнята. Крім цього (якщо ви перейменовуєте метод), він повинен працювати.
Андреас Райф

6

Якщо ви не дбаєте про порядок ви можете просто засунути елементи в HashSet, якщо ви дійсно хочете зберегти замовлення ви можете зробити що - щось на зразок цього:

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

Або спосіб Linq:

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

Edit:HashSet метод O(N)часу і O(N)простору у час сортування , а потім зробити унікальний (як це було запропоновано @ lassevk і інші) це O(N*lgN)час і O(1)простір , так що це не так ясно для мене (як це було на перший погляд) , що сортування шлях поступається (мій вибачення за тимчасове голосування проти


6

Ось метод розширення для видалення сусідніх дублікатів in situ. Спершу зателефонуйте Сортувати () та передайте у цьому ж IComparer. Це повинно бути ефективнішим, ніж версія Лассе В. Карлсена, яка повторно викликає RemoveAt (внаслідок цього відбувається переміщення декількох блоків).

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

Встановивши пакет MoreLINQ через Nuget, ви можете легко розмежувати список об'єктів за властивістю

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

3

Можливо, буде простіше просто переконатися, що дублікати не додано до списку.

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

1
Я зараз роблю це так, але чим більше записів у вас, тим довше займатиме перевірку дублікатів.
Роберт Страуч

У мене така сама проблема. Я використовую List<T>.Containsметод щоразу, але з більш ніж 1 000 000 записів. Цей процес уповільнює мою програму. Я використовую List<T>.Distinct().ToList<T>()перший замість цього.
RPDeshaies

Цей метод дуже повільний
темний погляд

3

Ви можете використовувати Union

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

7
Пояснення, чому це буде працювати, безумовно, зробить цю відповідь кращою
Ігор Б

2

Ще один спосіб у .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

Існує багато способів вирішити - питання про дублікати у Списку, нижче - один із них:

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

Ура, Раві Ганесан


2

Ось просте рішення, яке не потребує важкого для читання LINQ або попереднього сортування списку.

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

Ви маєте більше контролю над дублюючими елементами цим методом. Навіть більше, якщо у вас є база даних для оновлення. Для InternalIndex, чому б не починати з externalIndex + 1, а починати з початку кожного разу?
Nolmë Informatique

2

Відповідь Девіда Дж. - це хороший метод, не потрібно зайвих об’єктів, сортування тощо. Однак його можна вдосконалити:

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

Таким чином, зовнішній цикл йде у верхній нижній частині для всього списку, а внутрішній цикл йде внизу, «поки не буде досягнуто положення зовнішньої петлі».

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

Або якщо ви не хочете робити знизу вгору для внутрішнього циклу, ви можете почати внутрішній цикл у externalIndex + 1.


2

Усі відповіді копіюють списки, або створюють новий список, або використовують повільні функції, або просто болісно повільні.

Наскільки я розумію, це найшвидший і найдешевший метод, який я знаю (також підтримується дуже досвідченим програмістом, який спеціалізується на оптимізації фізики в реальному часі).

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

Остаточна вартість:

nlogn + n + nlogn = n + 2nlogn = O (nlogn), що досить приємно.

Примітка про RemoveRange: Оскільки ми не можемо встановити кількість списку та уникати використання функцій Remove, я не знаю точно швидкості цієї операції, але, мабуть, це найшвидший спосіб.


2

Якщо у вас є класи буксирних Productі Customerми хочемо , щоб видалити повторювані елементи зі свого списку

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

}

Ви повинні визначити загальний клас у наведеній нижче формі

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

тоді Ви можете видалити повторювані елементи зі свого списку.

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

цей код видаляє повторювані елементи, Idякщо ви хочете видалити повторювані елементи іншою властивістю, ви можете змінити те nameof(YourClass.DuplicateProperty) саме, nameof(Customer.CustomerName)а потім видалити повторювані елементи за CustomerNameвластивостями.


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

Проста інтуїтивна реалізація:

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

Цей метод також повільний. Створює новий список.
darkgaze
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.