Найшвидший спосіб пошуку в колекції рядків


80

Проблема:

У мене є текстовий файл із приблизно 120 000 користувачів (рядки), який я хотів би зберегти у колекції, а пізніше здійснити пошук у цій колекції.

Метод пошуку відбуватиметься щоразу, коли користувач змінить текст а, TextBoxа результатом повинні бути рядки, що містять текст у TextBox.

Мені не потрібно змінювати список, просто витягніть результати та помістіть їх у ListBox.

Те, що я намагався до цього часу:

Я спробував із двома різними колекціями / контейнерами, які я викидаю рядкові записи із зовнішнього текстового файлу (звичайно, один раз):

  1. List<string> allUsers;
  2. HashSet<string> allUsers;

З таким запитом LINQ :

allUsers.Where(item => item.Contains(textBox_search.Text)).ToList();

Моя пошукова подія (спрацьовує, коли користувач змінює текст пошуку):

private void textBox_search_TextChanged(object sender, EventArgs e)
{
    if (textBox_search.Text.Length > 2)
    {
        listBox_choices.DataSource = allUsers.Where(item => item.Contains(textBox_search.Text)).ToList();
    }
    else
    {
        listBox_choices.DataSource = null;
    }
}

Результати:

І те, і інше дало мені поганий час відгуку (приблизно 1-3 секунди між кожним натисканням клавіші).

Питання:

Як ви думаєте, де моє вузьке місце? Колекція, якою я користувався? Метод пошуку? І те, і інше?

Як я можу отримати кращу продуктивність та більш вільну функціональність?


10
HashSet<T>тут вам не допоможе, тому що ви шукаєте частину рядка.
Денніс

8
Перегляньте масиви суфіксів .
CodesInChaos

66
Не питайте, "до чого це найшвидший шлях", адже це займе буквально тижні до років досліджень. Швидше, скажіть "Мені потрібне рішення, яке працює менш ніж за 30 мс", або будь-яка ваша мета продуктивності. Вам не потрібен найшвидший пристрій, вам потрібен досить швидкий пристрій.
Eric Lippert

44
Також придбайте профайлер . Не здогадуйтесь про те, де знаходиться повільна частина; такі припущення часто помилкові. Вузьке місце може десь здивувати.
Ерік Ліпперт,

4
@Basilevs: Одного разу я написав чудову хеш-таблицю O (1), яка була надзвичайно повільною на практиці. Я сформулював його, щоб з’ясувати, чому, і виявив, що під час кожного пошуку він викликав метод, який - без жартів - в кінцевому підсумку запитував реєстр «ми зараз у Таїланді?». Не кешування, чи перебуває користувач у Таїланді, було вузьким місцем у коді O (1). Розташування вузького місця може бути глибоко протиречним . Використовуйте профайлер.
Ерік Ліпперт,

Відповіді:


48

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

Загальна ідея полягає в тому, щоб мати можливість використовувати його так:

public partial class YourForm : Form
{
    private readonly BackgroundWordFilter _filter;

    public YourForm()
    {
        InitializeComponent();

        // setup the background worker to return no more than 10 items,
        // and to set ListBox.DataSource when results are ready

        _filter = new BackgroundWordFilter
        (
            items: GetDictionaryItems(),
            maxItemsToMatch: 10,
            callback: results => 
              this.Invoke(new Action(() => listBox_choices.DataSource = results))
        );
    }

    private void textBox_search_TextChanged(object sender, EventArgs e)
    {
        // this will update the background worker's "current entry"
        _filter.SetCurrentEntry(textBox_search.Text);
    }
}

Грубий нарис буде приблизно таким:

public class BackgroundWordFilter : IDisposable
{
    private readonly List<string> _items;
    private readonly AutoResetEvent _signal = new AutoResetEvent(false);
    private readonly Thread _workerThread;
    private readonly int _maxItemsToMatch;
    private readonly Action<List<string>> _callback;

    private volatile bool _shouldRun = true;
    private volatile string _currentEntry = null;

    public BackgroundWordFilter(
        List<string> items,
        int maxItemsToMatch,
        Action<List<string>> callback)
    {
        _items = items;
        _callback = callback;
        _maxItemsToMatch = maxItemsToMatch;

        // start the long-lived backgroud thread
        _workerThread = new Thread(WorkerLoop)
        {
            IsBackground = true,
            Priority = ThreadPriority.BelowNormal
        };

        _workerThread.Start();
    }

    public void SetCurrentEntry(string currentEntry)
    {
        // set the current entry and signal the worker thread
        _currentEntry = currentEntry;
        _signal.Set();
    }

    void WorkerLoop()
    {
        while (_shouldRun)
        {
            // wait here until there is a new entry
            _signal.WaitOne();
            if (!_shouldRun)
                return;

            var entry = _currentEntry;
            var results = new List<string>();

            // if there is nothing to process,
            // return an empty list
            if (string.IsNullOrEmpty(entry))
            {
                _callback(results);
                continue;
            }

            // do the search in a for-loop to 
            // allow early termination when current entry
            // is changed on a different thread
            foreach (var i in _items)
            {
                // if matched, add to the list of results
                if (i.Contains(entry))
                    results.Add(i);

                // check if the current entry was updated in the meantime,
                // or we found enough items
                if (entry != _currentEntry || results.Count >= _maxItemsToMatch)
                    break;
            }

            if (entry == _currentEntry)
                _callback(results);
        }
    }

    public void Dispose()
    {
        // we are using AutoResetEvent and a background thread
        // and therefore must dispose it explicitly
        Dispose(true);
    }

    private void Dispose(bool disposing)
    {
        if (!disposing)
            return;

        // shutdown the thread
        if (_workerThread.IsAlive)
        {
            _shouldRun = false;
            _currentEntry = null;
            _signal.Set();
            _workerThread.Join();
        }

        // if targetting .NET 3.5 or older, we have to
        // use the explicit IDisposable implementation
        (_signal as IDisposable).Dispose();
    }
}

Крім того, ви повинні фактично розпоряджатися _filterекземпляром, коли батьківський Formрозміщений. Це означає , що ви повинні відкрити і редагувати Form«S Disposeметод (всередині YourForm.Designer.csфайлу) , щоб виглядати приблизно так:

// inside "xxxxxx.Designer.cs"
protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        if (_filter != null)
            _filter.Dispose();

        // this part is added by Visual Studio designer
        if (components != null)
            components.Dispose();
    }

    base.Dispose(disposing);
}

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

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


Я щойно перевірив ваше рішення, і воно чудово працює! Чудово зроблено. Єдина проблема у мене полягає в тому, що я не можу зробити _signal.Dispose();компіляцію (помилка щодо рівня захисту).
etaiso

@etaiso: це дивно, куди точно кличеш _signal.Dispose()Це десь поза BackgroundWordFilterкласом?
Groo

1
@Groo Це явна реалізація, тобто ви не можете викликати її безпосередньо. Ви повинні використовувати або usingблок, або дзвінокWaitHandle.Close()
Метью Уотсон,

1
Добре, тепер це має сенс, метод був опублікований у .NET 4. Сторінка MSDN для .NET 4 перелічує його як загальнодоступні методи , тоді як сторінка для .NET 3.5 відображає його під захищеними . Це також пояснює, чому у джерелі Mono для WaitHandle є умовне визначення .
Groo

1
@Groo Вибач, я повинен був згадати, що я говорив про стару версію .Net - вибач за плутанину! Однак зауважте, що йому не потрібно робити кастинг - він може .Close()замість цього подзвонити , який сам дзвонить .Dispose().
Метью Уотсон,

36

Я провів тестування, і пошук у списку із 120 000 елементів та заповнення нового списку записами займає незначну кількість часу (приблизно 1/50 секунди, навіть якщо всі рядки збігаються).

Отже, проблема, яку ви бачите, випливає із заповнення джерела даних тут:

listBox_choices.DataSource = ...

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

Можливо, вам слід спробувати обмежити його першими 20 записами, наприклад:

listBox_choices.DataSource = allUsers.Where(item => item.Contains(textBox_search.Text))
    .Take(20).ToList();

Також зауважте (як зазначали інші), що ви отримуєте доступ до TextBox.Textвласності для кожного елемента в allUsers. Це легко можна виправити наступним чином:

string target = textBox_search.Text;
listBox_choices.DataSource = allUsers.Where(item => item.Contains(target))
    .Take(20).ToList();

Однак я визначив, скільки часу потрібно для доступу TextBox.Text500 000 разів, і це зайняло лише 0,7 секунди, набагато менше, ніж 1 - 3 секунди, згадані в OP. Все-таки це варта оптимізація.


1
Дякую Метью. Я спробував ваше рішення, але я не думаю, що проблема полягає в популяції ListBox. Я думаю, що мені потрібен кращий підхід, оскільки такий тип фільтрації є дуже наївним (наприклад, пошук "abc" повертає 0 результатів, тоді я навіть не повинен шукати "abcX" тощо).
etaiso

@etaiso правильно (навіть якщо рішення Метью може чудово спрацювати, якщо вам насправді не потрібно попередньо налаштовувати всі збіги), ось чому я запропонував другим кроком уточнити пошук, а не виконувати кожен раз повний пошук.
Адріано Репетті

5
@etaiso Ну, час пошуку мізерно малий, як я вже сказав. Я спробував це з 120 000 рядків і шукав дуже довгий рядок, який не давав збігів, і дуже короткий рядок, який давав багато збігів, зайняв менше 1/50 секунди.
Метью Уотсон,

3
Чи textBox_search.Textсприяє вимірювана кількість часу? Отримання Textвластивості в текстовому полі один раз для кожної з 120 тис. Рядків, ймовірно, надсилає 120 тис. Повідомлень у вікно управління редагуванням.
Gabe

@Gabe Так, це так. Детальніше див. У моїй відповіді.
Андріс

28

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

Для введення:

Abraham
Barbara
Abram

Структура буде виглядати так:

a -> Barbara
ab -> Abram
abraham -> Abraham
abram -> Abram
am -> Abraham, Abram
aham -> Abraham
ara -> Barbara
arbara -> Barbara
bara -> Barbara
barbara -> Barbara
bram -> Abram
braham -> Abraham
ham -> Abraham
m -> Abraham, Abram
raham -> Abraham
ram -> Abram
rbara -> Barbara

Алгоритм пошуку

Припустимо, що користувач вводить "бюстгальтер".

  1. Розділіть словник на введення користувачем, щоб знайти введення користувача або місце, де він міг би йти. Таким чином ми знаходимо "барбару" - останній ключ нижче, ніж "бюстгальтер". Це називається нижньою межею для "бюстгальтера". Пошук займе логарифмічний час.
  2. Ітераціюйте від знайденого ключа далі, доки введення користувачем більше не збігається. Це дало б "брам" -> Абрам і "брахам" -> Авраам.
  3. Об’єднайте результат ітерації (Абрам, Авраам) і виведіть його.

Такі дерева призначені для швидкого пошуку підрядків. Його продуктивність близька до O (log n). Я вірю, що цей підхід буде працювати досить швидко, щоб його можна було безпосередньо використовувати графічним інтерфейсом користувача. Більше того, він буде працювати швидше, ніж різьбове рішення через відсутність накладних витрат на синхронізацію.


З того, що я знаю, суфіксальні масиви, як правило, кращий вибір, ніж суфіксальні дерева. Простіше впровадити та зменшити використання пам'яті.
CodesInChaos

Я пропоную SortedList, який дуже легко побудувати та підтримувати за рахунок накладних витрат на пам'ять, які можна звести до мінімуму, надавши обсяги списків.
Basilevs

Крім того, здається, масиви (і оригінальні ST) призначені для обробки великих текстів, тоді як тут ми маємо велику кількість коротких фрагментів, що є іншим завданням.
Basilevs

+1 за хороший підхід, але я б скористався хеш-картою або фактичним деревом пошуку, а не ручним пошуком у списку.
OrangeDog

Чи є якась перевага використання дерева суфіксів замість дерева префіксів?
jnovacho

15

Вам потрібна або текстова пошукова система (наприклад, Lucene.Net ), або база даних (ви можете розглянути таку вбудовану, як SQL CE , SQLite тощо). Іншими словами, вам потрібен індексований пошук. Пошук на основі хешу тут не застосовується, оскільки ви шукаєте підрядок, тоді як пошук на основі хешу - це точний пошук.

В іншому випадку це буде ітеративний пошук із циклічним переглядом колекції.


Індексація - це пошук на основі хешу. Ви просто додаєте всі підрядки як ключі, а не лише значення.
OrangeDog

3
@OrangeDog: не погоджуюсь. індексований пошук може бути реалізований як пошук на основі хешу за допомогою ключів індексу, але це не потрібно, і це не пошук на основі хешу за самим значенням рядка.
Денніс

@ Денніс Погоджуюсь. +1, щоб скасувати привид -1.
користувач

+1, тому що такі реалізації, як текстовий пошуковий механізм, мають розумні оптимізації, ніж string.Contains. Тобто. пошук baв bcaaaabaaрезультаті призведе до (проіндексованого) списку пропуску. Перший bрозглядається, але не збігається, оскільки наступним є a c, тому він переходить до наступного b.
Caramiriel

12

Також може бути корисно мати подію типу "розбій". Це відрізняється від регулювання тим, що він очікує певного періоду часу (наприклад, 200 мс), щоб зміни закінчились, перш ніж запускати подію.

Див. " Розрядка і дросель": візуальне пояснення для отримання додаткової інформації про зняття з роботи. Я розумію, що ця стаття орієнтована на JavaScript, а не на C #, але принцип застосовується.

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


Дивіться реалізацію C # дроселя подій класу EventThrotler у бібліотеці Algorithmia: github.com/SolutionsDesign/Algorithmia/blob/master/…
Frans

11

Запустіть пошук в іншому потоці та покажіть деяку анімацію завантаження або індикатор виконання під час запуску цього потоку.

Ви також можете спробувати розпаралелювати запит LINQ .

var queryResults = strings.AsParallel().Where(item => item.Contains("1")).ToList();

Ось орієнтир, який демонструє переваги продуктивності AsParallel ():

{
    IEnumerable<string> queryResults;
    bool useParallel = true;

    var strings = new List<string>();

    for (int i = 0; i < 2500000; i++)
        strings.Add(i.ToString());

    var stp = new Stopwatch();

    stp.Start();

    if (useParallel)
        queryResults = strings.AsParallel().Where(item => item.Contains("1")).ToList();
    else
        queryResults = strings.Where(item => item.Contains("1")).ToList();

    stp.Stop();

    Console.WriteLine("useParallel: {0}\r\nTime Elapsed: {1}", useParallel, stp.ElapsedMilliseconds);
}

1
Я знаю, що це можливо. Але моє питання тут полягає в тому, якщо і як я можу скоротити цей процес?
etaiso

1
@etaiso це насправді не повинно бути проблемою, якщо ви не розробляєте якесь справді апаратне забезпечення низького класу, переконайтесь, що ви не використовуєте налагоджувач, CTRL + F5
animaonline

1
Це не найкращий кандидат для PLINQ, оскільки метод String.Containsне є дорогим. msdn.microsoft.com/en-us/library/dd997399.aspx
Тім

1
@TimSchmelter, коли ми говоримо про тонни струн, це так!
animaonline

4
@TimSchmelter Я не уявляю, що ти намагаєшся довести, використовуючи наданий мною код, швидше за все, збільшить продуктивність для ОП, і ось орієнтир, який демонструє, як це працює: pastebin.com/ATYa2BGt --- Період - -
animaonline

11

Оновлення:

Я зробив кілька профілів.

(Оновлення 3)

  • Вміст списку: Числа, створені від 0 до 2.499.999
  • Текст фільтру: 123 (20.477 результатів)
  • Core i5-2500, Win7 64 біт, 8 ГБ оперативної пам'яті
  • VS2012 + JetBrains dotTrace

Початковий тестовий запуск для 2 500 000 записів зайняв у мене 20 000 мс.

Винуватцем номер один є заклик textBox_search.Textвсередині Contains. Це викликає для кожного елемента дорогий get_WindowTextметод текстового поля. Просто змінивши код на:

    var text = textBox_search.Text;
    listBox_choices.DataSource = allUsers.Where(item => item.Contains(text)).ToList();

скоротив час виконання до 1,858 мс .

Оновлення 2:

Дві інші значні проблеми - це заклик до string.Contains(близько 45% часу виконання) та оновлення елементів списку set_Datasource(30%).

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

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

Використовуючи BeginUpdateта EndUpdateне змінюючи час виконання set_Datasource.

Як зазначали тут інші, сам запит LINQ працює досить швидко. Я вважаю, що ваша пляшка - це оновлення самого списку. Ви можете спробувати щось на зразок:

if (textBox_search.Text.Length > 2)
{
    listBox_choices.BeginUpdate(); 
    listBox_choices.DataSource = allUsers.Where(item => item.Contains(textBox_search.Text)).ToList();
    listBox_choices.EndUpdate(); 
}

Сподіваюся, це допоможе.


Я не думаю , що це дозволить поліпшити що - або в якості BeginUpdateі EndUpdateпризначені для використання при додаванні елементів в окремо або при використанні AddRange().
etaiso

Це залежить від того, як DataSourceреалізовано властивість. Можливо, варто спробувати.
Андріс

Ваші результати профілювання сильно відрізняються від моїх. Я зміг здійснити пошук 120 тис. Рядків за 30 мс, але додавання їх до списку зайняло 4500 мс. Схоже, ви додаєте до списку 2,5 мільйона рядків менше 600 мс. Як це можливо?
Гейб

@Gabe Під час профілювання я використав введення, де текст фільтру усунув велику частину оригінального списку. Якщо я використовую введення, де текст фільтра нічого не вилучає зі списку, я отримую результати, подібні до вашого. Я оновлю свою відповідь, щоб пояснити, що я виміряв.
Андріс,

9

Якщо припустити, що ви співпадаєте лише за префіксами, структура даних, яку ви шукаєте, називається тріе , також відоме як "дерево префіксів". IEnumerable.WhereМетод , який ви використовуєте тепер доведеться перебрати всі пункти в вашому словнику на кожному доступі.

Цей потік показує, як створити трійку на C #.


1
Припускаючи, що він фільтрує свої записи з префіксом.
Тарек

1
Зверніть увагу, що він використовує метод String.Contains () замість String.StartsWith (), тому він може бути не зовсім тим, що ми шукаємо. Все-таки - ваша ідея, безсумнівно, краща, ніж звичайна фільтрація із розширенням StartsWith () у префіксному сценарії.
Тарек

Якщо він означає, що починається з, тоді Trie можна поєднати з фоновим підходом працівника для поліпшення роботи
Lyndon White

8

Елемент управління WinForms ListBox справді є вашим ворогом тут. Завантажувати записи буде повільно, і ScrollBar буде боротися з вами, щоб показати всі 120 000 записів.

Спробуйте скористатися старомодним джерелом даних DataGridView до DataTable з одним стовпцем [UserName] для зберігання ваших даних:

private DataTable dt;

public Form1() {
  InitializeComponent();

  dt = new DataTable();
  dt.Columns.Add("UserName");
  for (int i = 0; i < 120000; ++i){
    DataRow dr = dt.NewRow();
    dr[0] = "user" + i.ToString();
    dt.Rows.Add(dr);
  }
  dgv.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
  dgv.AllowUserToAddRows = false;
  dgv.AllowUserToDeleteRows = false;
  dgv.RowHeadersVisible = false;
  dgv.DataSource = dt;
}

Потім використовуйте DataView у події TextChanged вашого TextBox для фільтрування даних:

private void textBox1_TextChanged(object sender, EventArgs e) {
  DataView dv = new DataView(dt);
  dv.RowFilter = string.Format("[UserName] LIKE '%{0}%'", textBox1.Text);
  dgv.DataSource = dv;
}

2
+1, поки всі інші намагалися оптимізувати пошук, який займає лише 30 мс, ви єдина людина, яка визнала, що проблема насправді полягає у заповненні вікна списку.
Gabe

7

По- перше я хотів би змінити , як ListControlбачить джерело даних, ви перетворюєте результат IEnumerable<string>в List<string>. Особливо, коли ви просто набрали кілька символів, це може бути неефективним (і непотрібним). Не робіть обширних копій ваших даних .

  • Я б переніс .Where()результат у колекцію, яка реалізує лише те, що вимагається від IList(пошук). Це допоможе вам створити новий великий список для кожного введеного символу.
  • В якості альтернативи я б уникав LINQ і писав би щось більш конкретне (та оптимізоване). Зберігайте свій список у пам'яті та створюйте масив відповідних індексів, повторно використовуйте масив, щоб вам не довелося перерозподіляти його для кожного пошуку.

Другий крок - не шукати у великому списку, коли достатньо малого. Коли користувач почав набирати "ab", і він додає "c", тоді вам не потрібно досліджувати у великому списку, достатньо (і швидше) пошуку у відфільтрованому списку. Кожен раз уточнюйте пошук , не виконуйте кожен раз повний пошук.

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

ABC
 Додайте кращу стелю
 Над кістковим контуром

Це може бути просто реалізовано за допомогою масиву (якщо ви працюєте з іменами ANSI, інакше словник був би кращим). Побудуйте список таким чином (для ілюстрації, він відповідає початку рядка):

var dictionary = new Dictionary<char, List<string>>();
foreach (var user in users)
{
    char letter = user[0];
    if (dictionary.Contains(letter))
        dictionary[letter].Add(user);
    else
    {
        var newList = new List<string>();
        newList.Add(user);
        dictionary.Add(letter, newList);
    }
}

Потім пошук буде здійснено за допомогою першого символу:

char letter = textBox_search.Text[0];
if (dictionary.Contains(letter))
{
    listBox_choices.DataSource =
        new MyListWrapper(dictionary[letter].Where(x => x.Contains(textBox_search.Text)));
}

Будь ласка, зверніть увагу, що я використовував, MyListWrapper()як було запропоновано на першому кроці (але я пропустив другу пропозицію щодо стислості, якщо ви вибрали правильний розмір для словникового ключа, ви можете зробити кожен список коротким і швидким, щоб - можливо - уникати чогось іншого). Крім того, зауважте, що ви можете спробувати використовувати перші два символи для свого словника (більше списків і коротші). Якщо ви продовжите це, у вас буде дерево (але я не думаю, що у вас така велика кількість предметів).

Існує безліч різних алгоритмів пошуку рядків (із пов’язаними структурами даних), лише згадавши кілька:

  • Пошук на основі автоматів кінцевих станів : у цьому підході ми уникаємо зворотного відстеження, створюючи детермінований кінцевий автомат (DFA), який розпізнає збережений рядок пошуку. Вони дорогі для побудови - зазвичай вони створюються за допомогою конструкції блоку живлення, - але дуже швидкі у використанні.
  • Заглушки : Кнут – Морріс – Пратт обчислює DFA, який розпізнає вхідні дані за допомогою рядка для пошуку як суфікс, Бойер – Мур починає пошук з кінця голки, тому він може перестрибувати на цілу довжину голки на кожному кроці. Baeza – Yates відстежує, чи були попередні символи j префіксом пошукового рядка, і тому пристосований до нечіткого пошуку рядків. Алгоритм бітап є застосуванням підходу Беези – Йейтса.
  • Методи індексування : алгоритми швидшого пошуку засновані на попередній обробці тексту. Після побудови індексу підрядків, наприклад дерева суфіксів або суфіксального масиву, входження шаблону можна швидко знайти.
  • Інші варіанти : деякі методи пошуку, наприклад, пошук у триграмі, призначені для пошуку оцінки "близькості" між пошуковим рядком та текстом, а не "збіг / невідповідність". Це іноді називають "нечіткими" пошуками.

Кілька слів про паралельний пошук. Це можливо, але це рідко буває тривіальним, оскільки накладні витрати, щоб зробити це паралельним, можуть бути набагато вищими, ніж сам пошук. Я б не виконував сам пошук паралельно (розділення та синхронізація стануть незабаром занадто обширними і, можливо, складними), але я б перемістив пошук в окремий потік . Якщо основний потік не зайнятий, ваші користувачі не відчуватимуть затримки під час введення тексту (вони не помітять, чи з'явиться список через 200 мс, але їм буде незручно, якщо їм доведеться чекати 50 мс після введення) . Звичайно, сам пошук повинен бути досить швидким, в цьому випадку ви не використовуєте потоки для пришвидшення пошуку, а щоб ваш інтерфейс відповідав . Зверніть увагу, що окремий потік не буде робити ваш запитшвидше , він не зависне інтерфейс, але якщо ваш запит був повільним, він все одно буде повільним в окремому потоці (крім того, вам також доведеться обробляти кілька послідовних запитів).


1
Як деякі вже зазначали, OP не хоче обмежувати результати лише префіксами (тобто він використовує Contains, а не StartsWith). Як примітка, зазвичай краще використовувати загальний ContainsKeyметод під час пошуку ключа, щоб уникнути боксу, а ще краще використовувати, TryGetValueщоб уникнути двох пошуків.
Гру

2
@Groo ти маєш рацію, як я вже казав, це лише для ілюстрації. Сенс цього коду - не робоче рішення, а підказка: якщо ви спробували все інше - уникайте копій, уточнюйте пошук, переміщуйте його в інший потік - і цього недостатньо, тоді вам доведеться змінити структуру даних, яку ви використовуєте . Приклад - початок рядка, щоб просто залишатися простим.
Адріано Репетті

@Adriano дякуємо за чітку та детальну відповідь! Я погоджуюсь з більшістю речей, які ви згадали, але, як сказав Гру, остання частина організації даних не є застосовною в моєму випадку. Але я думаю, можливо, мати подібний словник із ключами, як міститься лист (хоча дублікати все одно будуть)
etaiso

після швидкої перевірки та обчислення ідея "вміщений лист" не годиться лише для одного символу (і якщо ми будемо використовувати комбінації двох або більше, у нас вийде дуже велика хеш-таблиця)
etaiso

@etaiso так, ви можете зберегти список із двох літер (для швидкого зменшення підсписків), але справжнє дерево може працювати краще (кожна буква пов'язана зі своїми наступниками, неважливо, де вона знаходиться всередині рядка, тому для "HOME" у вас є "H-> O", "O-> M" і "M-> E". Якщо ви шукаєте "om", ви швидко знайдете його. Проблема в тому, що він стає значно складнішим і може бути занадто великим для вас (IMO).
Адріано Репетті,

4

Ви можете спробувати використовувати PLINQ (паралельний LINQ). Хоча це не гарантує підвищення швидкості, це вам потрібно з’ясувати методом спроб і помилок.


4

Я сумніваюся, що ви зможете зробити це швидше, але точно:

а) Використовуйте метод розширення AsParallel LINQ

а) Використовуйте якийсь таймер для затримки фільтрації

б) Покладіть метод фільтрації на інший потік

Зберігайте якусь string previousTextBoxValueдесь. Створіть таймер із затримкою в 1000 мс, який спрацьовує на пошук, якщо previousTextBoxValueзначення збігається з вашим textbox.Text. Якщо ні - перепризначте previousTextBoxValueпоточне значення та скиньте таймер. Встановіть запуск таймера на подію, змінену в текстовому полі, і це зробить вашу програму більш гладкою. Фільтрування 120 000 записів за 1-3 секунди - це нормально, але ваш інтерфейс повинен залишатися чуйним.


1
Я не погоджуюсь робити це паралельно, але я абсолютно згоден з іншими двома пунктами. Це може бути навіть достатньо для задоволення вимог щодо інтерфейсу користувача.
Адріано Репетті

Забув згадати про це, але я використовую .NET 3.5, тому AsParallel не є варіантом.
etaiso

3

Ви також можете спробувати скористатися функцією BindingSource.Filter . Я використовував його, і він працює як шарм для фільтрації з купи записів, кожен раз, коли оновлюю цю властивість, шукаючи текст. Іншим варіантом було б використання AutoCompleteSource для елемента керування TextBox.

Сподіваюся, це допоможе!


2

Я хотів би спробувати відсортувати колекцію, шукати, щоб відповідати лише початковій частині, і обмежити пошук на деяке число.

так далі про ініалізацію

allUsers.Sort();

та пошук

allUsers.Where(item => item.StartWith(textBox_search.Text))

Можливо, ви можете додати трохи кешу.


1
Він не працює з початком рядка (саме тому він використовує String.Contains ()). За допомогою Contains () відсортований список не змінює продуктивності.
Адріано Репетті

Так, із "Містить" марно. Мені подобається пропозиція з деревом суфіксів stackoverflow.com/a/21383731/994849 У темі є багато цікавих відповідей, але це залежить від того, скільки часу він може витратити на це завдання.
hardsky

1

Використовуйте паралельно LINQ. PLINQє паралельною реалізацією LINQ для об’єктів. PLINQ реалізує повний набір стандартних операторів запитів LINQ як методи розширення для простору імен T: System.Linq і має додаткові оператори для паралельних операцій. PLINQ поєднує в собі простоту та читабельність синтаксису LINQ з потужністю паралельного програмування. Подібно до коду, який націлений на Паралельну бібліотеку завдань, PLINQ запитує масштаб у ступені одночасності на основі можливостей головного комп'ютера.

Вступ до PLINQ

Розуміння прискорення в PLINQ

Також ви можете використовувати Lucene.Net

Lucene.Net - це порт бібліотеки пошукової системи Lucene, написаний на C # і орієнтований на користувачів середовища виконання .NET. Пошукова бібліотека Lucene базується на перевернутому індексі. Lucene.Net має три основні цілі:


1

Відповідно до того, що я бачив, я погоджуюсь з тим, щоб відсортувати список.

Однак сортувати, коли список будується, буде дуже повільно, сортуючи під час побудови, ви матимете кращий час виконання.

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

Хеш-карта буде хеш ваш рядок і пошук з точним зміщенням. Я думаю, це має бути швидше.


Геш-карта з яким ключем? Я хочу мати можливість знайти ключові слова, що містяться в рядках.
etaiso

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

в іншому, або я не все прочитав, або було погане пояснення (можливо, і те, і інше;)) [цитата] має текстовий файл приблизно 120 000 користувачів (рядки), який я хотів би зберігати в колекції, а пізніше виконати пошук у цій колекції. [/ quote] Я думав, що це просто пошук рядків.
дада

1

Спробуйте скористатися методом BinarySearch, він повинен працювати швидше, ніж метод містять.

Містить буде O (n) BinarySearch - O (lg (n))

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

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