Удалить дубликаты из массива структуры - PullRequest
1 голос
/ 14 августа 2011

Не могу разобраться в удалении дублирующихся записей из массива структуры

У меня есть такая структура:

public struct stAppInfo
{
    public string sTitle;
    public string sRelativePath;
    public string sCmdLine;
    public bool bFindInstalled;
    public string sFindTitle;
    public string sFindVersion;
    public bool bChecked;
}

Я изменил структуру stAppInfo на класс здесь благодаря Джону Скиту

Код такой: (короткая версия)

stAppInfo[] appInfo = new stAppInfo[listView1.Items.Count];

int i = 0;
foreach (ListViewItem item in listView1.Items)
{
    appInfo[i].sTitle = item.Text;
    appInfo[i].sRelativePath = item.SubItems[1].Text;
    appInfo[i].sCmdLine = item.SubItems[2].Text;
    appInfo[i].bFindInstalled = (item.SubItems[3].Text.Equals("Sí")) ? true : false;
    appInfo[i].sFindTitle = item.SubItems[4].Text;
    appInfo[i].sFindVersion = item.SubItems[5].Text;
    appInfo[i].bChecked = (item.SubItems[6].Text.Equals("Sí")) ? true : false;
    i++;
}

Мне нужно, чтобы массив appInfo был уникальным в элементах sTitle и sRelativePath , остальные члены которых могут быть дубликатами

EDIT:

Спасибо всем за ответы, но это приложение "переносимое". Я имею в виду, что мне просто нужен файл .exe, и я не хочу добавлять другие файлы, такие как ссылки * .dll, поэтому, пожалуйста, никаких внешних ссылок, для которых это приложение предназначено использовать в Pendrive

Все данные поступают из файла * .ini, что я делаю: (псевдокод)

ReadFile()
FillDataFromFileInAppInfoArray()
DeleteDuplicates()
FillListViewControl()

Когда я хочу сохранить эти данные в файл, у меня есть следующие опции:

  1. Использование данных ListView
  2. Использование массива appInfo (это быстрее?)
  3. Любое другое¿?

EDIT2:

Большое спасибо: Джон Скит, Майкл Хейс, спасибо за ваше время, ребята !!

Ответы [ 4 ]

9 голосов
/ 14 августа 2011

Во-первых, пожалуйста не используйте изменяемые структуры. Они плохая идея во всех отношениях.

Во-вторых, пожалуйста не используйте открытые поля. Поля должны быть деталями реализации - используйте свойства.

В-третьих, мне совсем не ясно, что это должна быть структура. Это выглядит довольно большим, и не особенно "одно значение".

В-четвертых, следуйте .NET соглашениям об именах , чтобы ваш код соответствовал всему остальному коду, написанному на .NET.

В-пятых, вы не можете удалять элементов из массива, так как массивы создаются с фиксированным размером ... но вы можете создать новый массив только с уникальными элементами.

LINQ to Objects позволит вам сделать это, используя GroupBy, как показано Альбином, но немного более аккуратный (на мой взгляд) подход заключается в использовании DistinctBy из MoreLINQ

var unique = appInfo.DistinctBy(x => new { x.sTitle, x.sRelativePath })
                    .ToArray();

Это, как правило, более эффективно, чем GroupBy, и, на мой взгляд, более элегантно.

Лично я обычно предпочитаю использовать List<T> над массивами, но приведенное выше создаст для вас массив.

Обратите внимание, что с этим кодом все еще могут быть два элемента с одинаковым названием, и все же могут быть два элемента с одинаковым относительным путем - просто не может быть двух элементов с одинаковым относительным путем и название. Если есть повторяющиеся элементы, DistinctBy всегда будет давать первый такой элемент из последовательности ввода.

РЕДАКТИРОВАТЬ: Просто чтобы удовлетворить Михаэля, вам на самом деле не нужно создавать массив для начала или создавать массив впоследствии , если он вам не нужен:

var query = listView1.Items
                     .Cast<ListViewItem>()
                     .Select(item => new stAppInfo
                             {
                                 sTitle = item.Text,
                                 sRelativePath = item.SubItems[1].Text,
                                 bFindInstalled = item.SubItems[3].Text == "Sí",
                                 sFindTitle = item.SubItems[4].Text,
                                 sFindVersion = item.SubItems[5].Text,
                                 bChecked = item.SubItems[6].Text == "Sí"
                             })
                     .DistinctBy(x => new { x.sTitle, x.sRelativePath });

Это даст вам IEnumerable<appInfo>, который лениво струится. Обратите внимание, что если вы выполните итерацию более одного раза, то будет выполнять итерацию за listView1.Items одинаковое количество раз, каждый раз выполняя одни и те же сравнения уникальности.

Я предпочитаю этот подход, а не Михаэля, поскольку он делает столбцы «различимыми» очень четкими в семантическом смысле и удаляет повторение кода, используемого для извлечения этих столбцов из ListViewItem. Да, это предполагает создание большего количества объектов, но я предпочитаю ясность эффективности, пока бенчмаркинг не доказал, что на самом деле требуется более эффективный код.

2 голосов
/ 14 августа 2011

Вам нужен набор. Это гарантирует, что элементы, введенные в него, являются уникальными (на основе некоторого квалификатора, который вы настроите). Вот как это делается:

  • Сначала измените вашу структуру на класс. Это действительно невозможно обойти.

  • Во-вторых, предоставьте реализацию IEqualityComparer<stAppInfo>. Это может быть хлопотно, но именно это заставляет ваш набор работать (что мы увидим через минуту):

    public class AppInfoComparer : IEqualityComparer<stAppInfo>
    {
        public bool Equals(stAppInfo x, stAppInfo y) {
            if (ReferenceEquals(x, y)) return true;
            if (x == null || y == null) return false;
            return Equals(x.sTitle, y.sTitle) && Equals(x.sRelativePath,
               y.sRelativePath);
        }
    
        // this part is a pain, but this one is already written 
        // specifically for your question.
        public int GetHashCode(stAppInfo obj) {
            unchecked {
                return ((obj.sTitle != null 
                    ? obj.sTitle.GetHashCode() : 0) * 397)
                    ^ (obj.sRelativePath != null 
                    ? obj.sRelativePath.GetHashCode() : 0);
            }
        }
    }
    
  • Затем, когда придет время сделать свой сет, сделайте следующее:

    var appInfoSet = new HashSet<stAppInfo>(new AppInfoComparer());
    foreach (ListViewItem item in listView1.Items)
    {
        var newItem = new stAppInfo { 
            sTitle = item.Text,
            sRelativePath = item.SubItems[1].Text,
            sCmdLine = item.SubItems[2].Text,
            bFindInstalled = (item.SubItems[3].Text.Equals("Sí")) ? true : false,
            sFindTitle = item.SubItems[4].Text,
            sFindVersion = item.SubItems[5].Text,
            bChecked = (item.SubItems[6].Text.Equals("Sí")) ? true : false};
        appInfoSet.Add(newItem);
    }
    
  • appInfoSet теперь содержит коллекцию stAppInfo объектов с уникальными комбинациями Заголовок / Путь, согласно вашему требованию. Если вам нужен массив, сделайте это:

    stAppInfo[] appInfo = appInfoSet.ToArray();
    

Примечание: я выбрал эту реализацию, потому что она выглядит так, как вы уже делаете вещи. Он легко читается для цикла (хотя мне не нужна переменная counter). Это не связано с LINQ (что может быть хлопотно, если вы не знакомы с ним). Он не требует никаких внешних библиотек за пределами того, что предоставляет .NET Framework. И, наконец, он предоставляет массив, как вы и просили. Что касается чтения файла из INI-файла, надеюсь, вы видите, что единственное, что изменится, это ваш foreach цикл.

Обновление

Хэш-коды могут быть болью. Возможно, вы задавались вопросом, зачем вам вообще их вычислять. В конце концов, не могли бы вы просто сравнить значения заголовка и относительного пути после каждой вставки? Конечно, конечно, вы могли бы, и именно так работает другой набор, называемый SortedSet. SortedSet заставляет вас реализовать IComparer так же, как я реализовал IEqualityComparer выше.

Итак, в этом случае AppInfoComparer будет выглядеть так:

private class AppInfoComparer : IComparer<stAppInfo>
{
   // return -1 if x < y, 1 if x > y, or 0 if they are equal
   public int Compare(stAppInfo x, stAppInfo y)
   {
      var comparison = x.sTitle.CompareTo(y.sTitle);
      if (comparison != 0) return comparison;
      return x.sRelativePath.CompareTo(y.sRelativePath);
   }
}

И тогда единственное другое изменение, которое вам нужно сделать, это использовать SortedSet вместо HashSet:

var appInfoSet = new SortedSet<stAppInfo>(new AppInfoComparer());

На самом деле так намного проще, что вам, наверное, интересно, что дает? Причина, по которой большинство людей выбирают HashSet вместо SortedSet, заключается в производительности. Но вы должны сбалансировать это с тем, насколько вы на самом деле заботитесь, так как вы будете поддерживать этот код. Я лично использую инструмент под названием Resharper, который доступен для Visual Studio, и он вычисляет эти хэш-функции для меня, потому что я думаю, что вычислять их тоже очень сложно.

(Я буду говорить о сложности двух подходов, но если вы уже знаете это или не заинтересованы, не стесняйтесь пропустить его.)

SortedSet имеет сложность O (log n), то есть каждый раз, когда вы вводите новый элемент, вы фактически пройдете половину своего набора и сравните. Если он не найдет вашу запись, он перейдет на полпути между последним предположением и группой слева или справа от этого предположения, быстро сократив места для вашего элемента, чтобы скрыть. Для миллиона записей требуется около 20 попыток. Совсем неплохо. Но если вы выбрали хорошую функцию хеширования, то HashSet может выполнять ту же работу, в среднем, в одном сравнении, а именно O (1). И прежде чем вы подумаете, что 20 - это не на самом деле такое большое дело по сравнению с 1 (после того, как все компьютеры работают довольно быстро), помните, что вам пришлось вставить эти миллионы элементов, поэтому в то время как HashSet потребовалось около миллиона попыток для создания этого набора SortedSet потребовалось несколько миллионов попыток. Но есть цена - HashSet ломается (очень плохо), если вы выбираете плохую функцию хеширования. Если числа для множества предметов уникальны, то они будут сталкиваться в HashSet, который затем придется пробовать снова и снова. Если множество предметов сталкиваются с одним и тем же номером, они будут повторять шаги друг друга, и вы будете ждать долго. Миллионная запись займет миллион раз миллион попыток - HashSet перешло в O (n ^ 2). Что важно для этих обозначений big-O (а именно O (1), O (log n) и O (n ^ 2) на самом деле), так это то, насколько быстро число в скобках увеличивается с увеличением n. Медленный рост или отсутствие роста лучше. Быстрый рост иногда неизбежен. Для дюжины или даже сотен предметов разница может быть незначительной - но если вы можете привыкнуть программировать эффективные функции так же легко, как альтернативы, тогда стоит подготовить себя к этому, так как проблемы являются самыми дешевыми, чтобы исправить их ближе всего к Точка, где вы создали эту проблему.

2 голосов
/ 14 августа 2011

Используйте LINQ2Objects, группируйте вещи, которые должны быть уникальными, а затем выберите первый элемент в каждой группе.

var noDupes = appInfo.GroupBy(
    x => new { x.sTitle, x.sRelativePath })
    .Select(g => g.First()).ToArray();
0 голосов
/ 15 августа 2011

!!! Массив структур (тип значения) + сортировка или любой вид поиска ==> много операций по распаковке.

  1. Я бы предложил придерживатьсярекомендации Джона и Хенка, так что сделайте его классом и используйте универсальный List<T>.
  2. Используйте LINQ GroupBy или DistinctBy, так как для меня гораздо проще использовать встроенный в GroupBy, но его также интересно взятьвзгляните на другую популярную библиотеку, возможно, она даст вам некоторое представление.

Кстати, также взгляните на LambdaComparer , она сделает вашу жизнь проще каждый раз, когда вам понадобится такаявид сортировки / поиска и т. д. ...

...