Поиск строк в большом массиве занимает много времени

Я реализую поле поиска, которое фильтрует UITableView в соответствии с текстом, который вводит пользователь.
TableView построен из массива, содержащего NSStrings (данные для отображения и поиска), и может содержать 6000+ элементов. .
Когда пользователь начинает поиск, я реализую метод -(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText.

Однако мой код работает, когда массив данных большой, он очень медленный и создает очень плохой пользовательский интерфейс (мой iPhone 4 зависает на несколько секунд).

Я реализую поиск (методом, упомянутым выше):

NSMutableArray *discardedItems = [[NSMutableArray alloc] init]; // Items to be removed
searchResultsArray = [[NSMutableArray alloc] initWithArray:containerArray]; // The array that holds all the data

// Search for matching results
for (int i=0; i<[searchResultsArray count]; i++) {   
    NSString *data = [[containerArray objectAtIndex:i] lowercaseString];
    NSRange r = [data rangeOfString:searchText];
    if (r.location == NSNotFound) {
        // Mark the items to be removed
        [discardedItems addObject:[searchResultsArray objectAtIndex:i]];
    }
}
// update the display array
[searchResultsArray removeObjectsInArray:discardedItems];
[myTableView reloadData];

Я не думал, что перебор массива с несколькими тысячами элементов вызовет какие-либо проблемы...
Любое предложение будет оценено!

ОБНОВЛЕНИЕ Я только что понял, что большую часть времени занимает следующее:

[searchResultsArray removeObjectsInArray:discardedItems];

person Code Monkey    schedule 01.09.2012    source источник
comment
Вы пробовали сортировать, а затем использовать двоичный поиск?   -  person Samir    schedule 02.09.2012
comment
Сортировка невозможна, потому что: 1. Мне нужно, чтобы результаты были упорядочены в соответствии с их первоначальным порядком в массиве. 2. Я ищу подстроку в каждом элементе массива, а не строку, с которой он начинается. Так что сортировка не поможет...   -  person Code Monkey    schedule 02.09.2012
comment
По сути, вы выполняете полнотекстовый поиск, который в любом случае не будет невероятно эффективным, если только у вас нет заранее созданного индекса. И, как предлагает Миентус, вам нужно избегать создания объектов (строки в нижнем регистре) в цикле. Это особенно плохо, поскольку куча заполняется этими отброшенными строками (чего можно было бы избежать с диапазоном autorelease внутри цикла, хотя это добавило бы собственные затраты). Лучше использовать сравнение без учета регистра или иметь предварительно уменьшенную версию вашего текста.   -  person Hot Licks    schedule 02.09.2012
comment
Кстати, какой длины типичная строковая запись в вашем списке и какова длина типичного аргумента поиска?   -  person Hot Licks    schedule 02.09.2012
comment
(Конечно, вы также можете крутить стрелку во время поиска, чтобы пользователь знал, что что-то происходит, а приложение не просто зависло.)   -  person Hot Licks    schedule 02.09.2012
comment
Типичная строковая запись имеет длину около 15 символов. Типичный аргумент поиска - около 4 символов.   -  person Code Monkey    schedule 02.09.2012


Ответы (2)


Попробуйте быстрый способ перечисления, мой фрагмент:

- (void)searchBar:(UISearchBar*)searchBar textDidChange:(NSString*)text
{
    if(text.length == 0)
    {
        self.isFiltered = NO;
    }
    else
    {
        self.isFiltered = YES;
        self.searchArray = [NSMutableArray arrayWithCapacity:self.places.count];

        for (PTGPlace* place in self.places)
        {
            NSRange nameRange = [place.name rangeOfString:text options:NSCaseInsensitiveSearch];

            if(nameRange.location != NSNotFound)
            {
                [self.searchArray addObject:place];
            }
        }
    }

    [self.tableView reloadData];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if(self.isFiltered)
        return self.searchArray.count;
    else
        return self.places.count;
}

В ячейкеForRowAtIndexPath:

    PTGPlace *place = nil;

    if(self.isFiltered)
        place = [self.searchArray objectAtIndex:indexPath.row];
    else
        place = [self.places objectAtIndex:indexPath.row];

    // Configure the cell...
    cell.textLabel.text = place.name;
    cell.detailTextLabel.text = [place subtitle];
person Michal Zaborowski    schedule 01.09.2012
comment
Спасибо за ответ, но вы сделали именно то, что я сделал. Единственная разница в том, что вы проверили длину текста в начале — то, что я уже делаю в своем коде (но не добавил в свой пример). - person Code Monkey; 02.09.2012
comment
@CodeMonkey это совсем не то, что у тебя. проблема в том, как вы сравниваете. Реализация mientus выполняет сравнение строк без учета регистра вместо создания тысяч временных строк в нижнем регистре и последующего поиска. Увеличение автовыпуска и создание временных строк являются самыми большими затратами времени и памяти в вашей реализации. Реализация mientus не создает временных строк и имеет низкую активность автоматического освобождения. я написал версию, которая слишком похожа на публикацию - подход, который мы с Миентусом использовали, был в 3 раза быстрее, чем ваш (для входных данных, которые я тестировал). ++улучшено - person justin; 02.09.2012
comment
@CodeMonkey iow - быстрое перечисление не является большим преимуществом при использовании этого подхода - это сокращение активности автоматического выпуска и избежание создания множества временных строк - просто ищите существующие строки, не создавая промежуточных (и ненужных) представлений. - person justin; 02.09.2012
comment
Спасибо за разъяснение, Джастин, и извините за то, что упустил суть @mientus. Я проверю это и обновлю. - person Code Monkey; 02.09.2012
comment
@Justin, я пробовал то, что вы и Миентус упомянули. Я не вижу существенного улучшения. Пожалуйста, поправьте меня, если я ошибаюсь, но вы имеете в виду эту строку 'NSRange nameRange = [place.name rangeOfString: text options: NSCaseInsensitiveSearch];' Верно? Я удалил все экземпляры временных строк, я использую параметры: NSCaseInsensitiveSearch, но все равно - та же проблема остается... - person Code Monkey; 02.09.2012
comment
@CodeMonkey правильно — вы используете rangeOfString:options:, но другой важный шаг заключается в том, что вам больше не нужно создавать временное представление lowercaseString. Вы также удалили эту часть? - person justin; 02.09.2012
comment
@CodeMonkey - я не уверен, что верю, что вы внесли критические изменения, если вы говорите, что нет существенной разницы с предыдущим. - person Hot Licks; 02.09.2012
comment
Я сделал именно эти изменения. Я не тестировал производительность раньше, я знаю, что сейчас она довольно плохая (с критическими изменениями) - person Code Monkey; 02.09.2012
comment
Я только что понял, что проблема заключается в удалении отфильтрованных результатов из массива, а не в самом поиске. - person Code Monkey; 02.09.2012
comment
Я не видел этого раньше, но изменяемые массивы в objc работают медленно, поэтому, если вы хотите удалить пару объектов из массива 6000 объектов, это займет несколько секунд.. ;) - person Michal Zaborowski; 02.09.2012
comment
Чтобы решить проблему, я заменил массив discardedItems (из моего примера) на 'NSMutableIndexSet *discardedItems = [NSMutableIndexSet indexSet]; // Элементы для удаления' Я добавил индекс каждого элемента для удаления - '[discardedItems addIndex:i];' и в конце удалил элементы с помощью - '[searchResultsArray removeObjectsAtIndexes: discardedItems];' Теперь это работает мгновенно! - person Code Monkey; 04.09.2012

Попробуй это:

для первых трех позиций создайте 26 наборов индексов, каждый из которых представляет индекс массива элементов с этой буквой (только в нижнем регистре). То есть, скажем, запись с idx=100 начинается с «формулы». Набор индексов, представляющий «f» в первой позиции, будет содержать индекс «100». Индекс, установленный для второго символа 'o', будет содержать индекс 100, а индекс, установленный для третьего символа 'r', будет содержать 100.

Когда пользователь вводит символ «f», вы сразу же получаете набор индексов всех элементов массива, начинающихся с «f» (и можете быстро создать подмножество вашего основного массива). Когда затем набирается 'o', вы можете найти пересечение индексов в первом совпадении со вторым совпадением. То же самое для третьего. Теперь создайте подмассив основного массива, в котором совпадают первые три индекса — вы можете просто использовать для этого наборы индексов.

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

person David H    schedule 01.09.2012
comment
Спасибо за ответ. Я не уверен, что полностью вас понимаю... Похоже, было бы лучше иметь какой-то словарь для хранения данных или даже трехмерного массива и т. д. Тем не менее, решение недостаточно хорошее, потому что я ищу подстрока из строк в массиве. Это не обязательно начинается с подстроки, которую я использую для поиска... - person Code Monkey; 02.09.2012
comment
Я только что понял, что проблема именно в удалении из массива, а не в самом поиске... - person Code Monkey; 02.09.2012