Оптимизация алгоритма сопоставления дубликатов - PullRequest
1 голос
/ 05 мая 2011

Я написал небольшую служебную программу, которая идентифицирует дубликаты треков в iTunes.Фактическое сопоставление треков занимает много времени, и я бы хотел его оптимизировать.Я храню данные трека в NSMutableDictionary, который хранит отдельные данные трека в NSMutableDictionaries с ключом trackID.Эти отдельные словари треков имеют по крайней мере следующие ключи:

  • TrackID
  • Имя
  • Artist
  • Продолжительность (в милли ####.####)

Чтобы определить, соответствуют ли какие-либо треки друг другу, я должен проверить:

  • Если продолжительность двух треков не превышает 5 секунд друг от друга
  • совпадения имен
  • совпадения исполнителей

Медленный способ сделать это - использовать два цикла for:

-(void)findDuplicateTracks {

    NSArray *allTracks = [tracks allValues];

    BOOL isMatch = NO;

    int numMatches = 0;

    // outer loop

    NSMutableDictionary *track      = nil;
    NSMutableDictionary *otherTrack = nil;

    for (int i = 0; i < [allTracks count]; i++) { 

        track = [allTracks objectAtIndex:i];

        NSDictionary *summary = nil;

        if (![claimedTracks containsObject:track]) {

            NSAutoreleasePool *aPool = [[NSAutoreleasePool alloc] init];

            NSUInteger duration1  = (NSUInteger) [track objectForKey:kTotalTime];
            NSString *nName       = [track objectForKey:knName];
            NSString *nArtist     = [track objectForKey:knArtist];


            // inner loop - no need to check tracks that have
            // already appeared in i

            for (int j = i + 1; j < [allTracks count]; j++) { 

                otherTrack = [allTracks objectAtIndex:j];

                if (![claimedTracks containsObject:otherTrack]) {

                    NSUInteger duration2 = (NSUInteger)[otherTrack objectForKey:kTotalTime];

                    // duration check
                    isMatch = (abs(duration1 - duration2) < kDurationThreshold);

                    // match name
                    if (isMatch) {

                        NSString *onName = [otherTrack objectForKey:knName];

                        isMatch = [nName isEqualToString:onName];
                    }

                    // match artist
                    if (isMatch) {

                        NSString *onArtist = [otherTrack objectForKey:knArtist];

                        isMatch = [nArtist isEqualToString:onArtist];

                    }

                    // save match data
                    if (isMatch) {

                        ++numMatches;

                        // claim both tracks
                        [claimedTracks addObject:track];
                        [claimedTracks addObject:otherTrack];

                        if (![summary isMemberOfClass:[NSDictionary class]]) {

                            [track setObject:[NSNumber numberWithBool:NO] forKey:@"willDelete"];
                            summary = [self dictionarySummaryForTrack:track];

                        }


                        [otherTrack setObject:[NSNumber numberWithBool:NO] forKey:@"willDelete"];                        
                        [[summary objectForKey:kMatches] 
                                            addObject:otherTrack];

                    }
                }
            }

            [aPool drain];
        }
    }
}

Это становитсядовольно медленно для больших музыкальных библиотек, и использует только 1 процессор.Одной из рекомендуемых оптимизаций было использование блоков и обработка треков партиями (из 100 треков).Я попробовал это.Если мой код первоначально выполнялся за 9 часов, то теперь на четырехъядерном процессоре это занимает около 2 часов.Это все еще слишком медленно.Но (говоря выше о моей плате), возможно, есть способ сохранить все значения, которые мне нужны, в структуре C, которая «помещается в стек», и тогда мне не придется извлекать значения из более медленной памяти.Это кажется мне слишком низким уровнем, но я хочу узнать, есть ли у меня пример.

Кстати, я профилировал это в инструментах, и [NSCFSet member:] занимает 86,6% времени процессора.

Тогда я подумал, что мне следует извлечь все длительности в отсортированный массив, чтобы мне не пришлось искать значение длительности в словаре.Я думаю, что это хорошая идея, но когда я начал ее реализовывать, я подумал, как определить лучший размер партии.

Если у меня есть следующие длительности:

    2 2 3 4 5 6 6 16 17 38 59   Duration
    0 1 2 3 4 5 6  7  8  9 10   Index

Затем, просто перебирая массив, я знаю, что для поиска подходящих треков песни с индексом 0 мне нужно только сравнить еепротив песен до индекса 6. Это здорово, у меня есть моя первая партия.Но теперь мне нужно начать с индекса 1 только для того, чтобы выяснить, что его пакет также должен останавливаться на индексе 6 и исключать индекс 0. Я предполагаю, что здесь я трачу много циклов обработки, определяя, какой пакет должен быть / продолжительностьМатчи.Это казалось «установленной» проблемой, но мы не делали этого в моем классе «Введение в алгоритмы».

Мои вопросы:

1) Какой самый эффективный способ определить совпадающие треки?Это что-то похожее на то, что выше?Используются ли несвязанные и [унифицированные] операции над множествами, которые немного выше моего уровня знаний?Это фильтрация массивов с помощью NSArray?Есть ли онлайн-ресурс, описывающий эту проблему и решение?

Я хочу реструктурировать словарь треков любым способом (структура данных), который наиболее эффективен.Сначала я подумал, что мне нужно выполнить много поисков по TrackID, но это уже не так.

2) Есть ли более эффективный способ решения этой проблемы?Как вы, рок-звезды, переходите от пункта 1 к оптимизированному решению?

Я искал ответ дольше, чем хотел бы признаться, и нашел следующие интересные, но бесполезные ответы:

найти дубликаты

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

Спасибо за любую помощь, которую вы можете предоставить, Лэнс

Ответы [ 2 ]

1 голос
/ 05 мая 2011

Моя первая мысль - сохранить некоторые отсортированные коллекции в виде индексов в вашем словаре, чтобы вы могли прекратить выполнять поиск O (n ^ 2), сравнивая каждую дорожку с любой другой дорожкой.

Если у вас есть массивы TrackID, отсортированные по длительности, то для любого трека вы можете выполнить более эффективный O (log n) двоичный поиск, чтобы найти треки с длительностями в пределах вашего 5-секундного допуска.

Еще лучше для исполнителя и имени можно сохранить словарь с указанием имени исполнителя или названия трека, значения которого являются массивами TrackID. Тогда вам нужен только поиск O (1), чтобы получить набор треков для конкретного исполнителя, который должен позволить вам очень быстро определить, есть ли возможные дубликаты.

Наконец, если вы создали подобный словарь названий для TrackID, то вы можете просмотреть все его ключи и искать дубликаты, только если есть несколько дорожек с одинаковым заголовком. Выполнение дальнейших сравнений только при наличии нескольких дорожек с одинаковым заголовком должно исключить значительный процент библиотеки и значительно сократить время поиска (до O (n) для построения словаря и еще одно O (n) для поиска в худшем случае для дубликаты все еще оставляют вас на O (n), а не на O (n ^ 2), который у вас есть сейчас).


Если больше ничего не делать для этой последней оптимизации, результирующее увеличение производительности должно быть огромным для библиотеки без значительного количества дубликатов:

NSMutableArray *possibleDuplicates = [NSMutableArray array];
NSMutableDictionary *knownTitles = [NSMutableDictionary dictionary];
for (NSMutableDictionary *track in [tracks allKeys]) {
    if ([knownTitles objectForKey:[track objectForKey:@"title"]] != nil) {
        [possibleDuplicates addObject:track];
    }
    else {
        [knownTitles addObject:[track objectForKey:@"TrackID"] forKey:[track objectForKey:@"title"]];
    }
}
//check for duplicates of the tracks in possibleDuplicates only.
1 голос
/ 05 мая 2011

Есть несколько способов сделать это, но вот мое первое наивное предположение:

Иметь изменяемый словарь.Ключи в этом словаре являются названиями песен.Значением каждого ключа является другой изменяемый словарь.Ключи этого вторичного изменяемого словаря - художники.Значение каждого ключа представляет собой изменяемый массив песен.

В итоге вы получите что-то вроде этого:

NSArray *songs = ...; //your array of songs
NSMutableDictionary *nameCache = [NSMutableDictionary dictionary];

for (Song *song in songs) {
  NSString *name = [song name];
  NSMutableDictionary *artistCache = [nameCache objectForKey:name];
  if (artistCache == nil) {
    artistCache = [NSMutableDictionary dictionary];
    [nameCache setObject:artistCache forKey:name];
  }

  NSString *artist = [song artist];
  NSMutableArray *songCache = [artistCache objectForKey:artist];
  if (songCache == nil) {
    songCache = [NSMutableArray array];
    [artistCache setObject:songCache forKey:artist];
  }

  for (Song *otherSong in songCache) {
    //these are songs that have the same name and artist
    NSTimeInterval myDuration = [song duration];
    NSTimeInterval otherDuration = [otherSong duration];
    if (fabs(myDuration - otherDuration) < 5.0f) {
      //name matches, artist matches, and their difference in duration is less than 5 seconds
    }
  }
  [songCache addObject:song];
}

Это наихудший случай O (n 2) алгоритм (если каждая песня имеет одно и то же имя, исполнителя и продолжительность).Это алгоритм O (n) в лучшем случае (если каждая песня имеет свое имя / исполнителя / продолжительность), и в действительности он окажется ближе к O (n), чем к O (n 2 ) (скорее всего).

...