2012-05-15 3 views
3

У меня есть список, возможно, 100 000 строк в памяти в моем приложении. Мне нужно найти верхние 20 строк, которые содержат определенное ключевое слово (без учета регистра). Это легко сделать, я просто запускаю следующий LINQ.Самый быстрый способ поиска списка имен в C#

from s in stringList 
where s.ToLower().Contains(searchWord.ToLower()) 
select s 

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

+1

Если это было точное совпадение или операция запуска с начала работы, существует ряд способов значительно ускорить его, но с помощью содержит не так много, что вы можете сделать. Один маленький совет, хотя должен был бы сделать все строки в строчном строчке 'stringList', а не вызывать' ToLower' каждый раз, когда вы запускаете запрос (если вы это делаете). – Servy

+0

«Моя фантастическая машина» содержит ключевое слово «муравей»? Определите, что значит содержать ключевое слово. – hatchet

+0

Да, это чистый поиск подстроки, как указано в коде LINQ. –

ответ

1

Другой вариант, хотя он потребует достаточного количества памяти, состоял бы в том, чтобы прекомпилировать что-то вроде массива суффикса (список позиций внутри строк, отсортированных по строкам, на которые они указывают).

http://en.wikipedia.org/wiki/Suffix_array

Это было бы наиболее целесообразным, если список строк вы ищете против относительно статична. Весь список индексов строк можно сохранить в одном массиве кортежей (indexOfString, positionInString), на котором вы выполнили бы двоичный поиск, используя String.Compare(keyword, 0, target, targetPos, keyword.Length).

Итак, если у вас было 100 000 строк средней длины 20, вам понадобится 100 000 * 20 * 2 * sizeof (int) памяти для этой структуры. Вы можете сократить это пополам, упаковав как indexOfString, так и positionInString в один 32-битный int, например с positionInString в младших 12 битах, и indexOfString в остальных верхних битах. Вам просто нужно немного поиграть, чтобы вернуть два значения. Важно отметить, что структура не содержит ни строк, ни подстрок. Строки, которые вы ищете, существуют только один раз.

Это даст вам полный индекс и позволит быстро найти любую подстроку (бинарный поиск по индексу, представленному массивом суффикса) с минимальными фактическими сопоставлениями строк.

Если память дорога, простая оптимизация первоначального алгоритма грубой силы будет заключаться в прекомполяции словаря уникальных символов и присвоении порядковых номеров для представления каждого. Затем предкоммутите бит массива для каждой строки с битами, установленными для каждого уникального символа, содержащегося в строке. Поскольку ваши строки относительно короткие, должна существовать значительная изменчивость рестринга BitArrays (это не сработает, если ваши строки были очень длинными). Затем вы просто вычисляете BitArray или ключевое слово поиска и выполняете поиск только для ключевого слова в тех строках, где keywordBits & targetBits == keywordBits. Если ваши строки предварительно преобразуются в нижний регистр и являются только английским алфавитом, BitArray, вероятно, будет соответствовать одному int. Таким образом, это потребует минимум дополнительной памяти, будет простым в реализации и позволит вам быстро отфильтровать строки, в которых вы определенно не найдете ключевое слово. Это может быть полезной оптимизацией, поскольку поиск строк выполняется быстро, но у вас их так много, чтобы использовать поиск грубой силы.

EDIT Для заинтересованных, вот базовая реализация начального решения, которое я предложил. Я запускал тесты с использованием 100 000 случайно генерируемых строк длин, описанных OP. Хотя для создания и сортировки индекса потребовалось около 30 секунд, скорость поиска ключевых слов в 3000 раз составляла 49 805 миллисекунд для грубой силы и 18 миллисекунд, используя индексированный поиск, поэтому в несколько тысяч раз быстрее. Если вы редко создаете список, тогда мой простой, но относительно медленный метод изначально построения массива суффиксов должен быть достаточным. Есть более разумные способы построить его быстрее, но для этого потребуется больше кодирования, чем моя базовая реализация ниже.

// little test console app 
static void Main(string[] args) { 
    var list = new SearchStringList(true); 
    list.Add("Now is the time"); 
    list.Add("for all good men"); 
    list.Add("Time now for something"); 
    list.Add("something completely different"); 
    while (true) { 
     string keyword = Console.ReadLine(); 
     if (keyword.Length == 0) break; 
     foreach (var pos in list.FindAll(keyword)) { 
      Console.WriteLine(pos.ToString() + " =>" + list[pos.ListIndex]); 
     } 
    } 
} 
~~~~~~~~~~~~~~~~~~ 
// file for the class that implements a simple suffix array 
using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Collections; 

namespace ConsoleApplication1 { 
    public class SearchStringList { 
     private List<string> strings = new List<string>(); 
     private List<StringPosition> positions = new List<StringPosition>(); 
     private bool dirty = false; 
     private readonly bool ignoreCase = true; 

     public SearchStringList(bool ignoreCase) { 
      this.ignoreCase = ignoreCase; 
     } 

     public void Add(string s) { 
      if (s.Length > 255) throw new ArgumentOutOfRangeException("string too big."); 
      this.strings.Add(s); 
      this.dirty = true; 
      for (byte i = 0; i < s.Length; i++) this.positions.Add(new StringPosition(strings.Count-1, i)); 
     } 

     public string this[int index] { get { return this.strings[index]; } } 

     public void EnsureSorted() { 
      if (dirty) { 
       this.positions.Sort(Compare); 
       this.dirty = false; 
      } 
     } 

     public IEnumerable<StringPosition> FindAll(string keyword) { 
      var idx = IndexOf(keyword); 
      while ((idx >= 0) && (idx < this.positions.Count) 
       && (Compare(keyword, this.positions[idx]) == 0)) { 
       yield return this.positions[idx]; 
       idx++; 
      } 
     } 

     private int IndexOf(string keyword) { 
      EnsureSorted(); 

      // binary search 
      // When the keyword appears multiple times, this should 
      // point to the first match in positions. The following 
      // positions could be examined for additional matches 
      int minP = 0; 
      int maxP = this.positions.Count - 1; 
      while (maxP > minP) { 
       int midP = minP + ((maxP - minP)/2); 
       if (Compare(keyword, this.positions[midP]) > 0) { 
        minP = midP + 1; 
       } else { 
        maxP = midP; 
       } 
      } 
      if ((maxP == minP) && (Compare(keyword, this.positions[minP]) == 0)) { 
       return minP; 
      } else { 
       return -1; 
      } 
     } 

     private int Compare(StringPosition pos1, StringPosition pos2) { 
      int len = Math.Max(this.strings[pos1.ListIndex].Length - pos1.StringIndex, this.strings[pos2.ListIndex].Length - pos2.StringIndex); 
      return String.Compare(strings[pos1.ListIndex], pos1.StringIndex, this.strings[pos2.ListIndex], pos2.StringIndex, len, ignoreCase); 
     } 

     private int Compare(string keyword, StringPosition pos2) { 
      return String.Compare(keyword, 0, this.strings[pos2.ListIndex], pos2.StringIndex, keyword.Length, this.ignoreCase); 
     } 

     // Packs index of string, and position within string into a single int. This is 
     // set up for strings no greater than 255 bytes. If longer strings are desired, 
     // the code for the constructor, and extracting ListIndex and StringIndex would 
     // need to be modified accordingly, taking bits from ListIndex and using them 
     // for StringIndex. 
     public struct StringPosition { 
      public static StringPosition NotFound = new StringPosition(-1, 0); 
      private readonly int position; 
      public StringPosition(int listIndex, byte stringIndex) { 
       this.position = (listIndex < 0) ? -1 : this.position = (listIndex << 8) | stringIndex; 
      } 
      public int ListIndex { get { return (this.position >= 0) ? (this.position >> 8) : -1; } } 
      public byte StringIndex { get { return (byte) (this.position & 0xFF); } } 
      public override string ToString() { 
       return ListIndex.ToString() + ":" + StringIndex; 
      } 
     } 
    } 
} 
+0

Большое спасибо, кажется хорошим решением, в значительной степени по тем же линиям, что и предложение Trie. –

+0

@NielsBrinch - Я добавил код для простой реализации начального решения, которое я предложил, в случае, если вам интересно. – hatchet

+0

Итак, он просто возвращает одно совпадение, а затем «грубая сила» может быть использована для получения следующих матчей сразу после этого, пока это не будет соответствовать больше? –

4

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

Вы можете вытащить searchWord.ToLower() в локальную переменную, чтобы сохранить тонны струнных операций, кстати. Вы также можете предварительно вычислить строчную версию stringList. Если вы не можете прекомпомировать, используйте, по крайней мере, s.IndexOf(searchWord, StringComparison.InvariantCultureIgnoreCase) != -1. Это экономит дорогостоящие вызовы ToLower.

Вы также можете щелкнуть .AsParallel по запросу.

+0

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

+0

Я просто посмотрел на Trie, и это похоже на то, что я ищу. Даже нашел, что кто-то осуществил это для меня! :) http://geekyisawesome.blogspot.com/2010/07/c-trie.html –

+0

Trie не спасет вас от 100 000 всех, содержащих одно и то же письмо «a». У вашего Trie будет 100 000 детей для определенных коротких путей, которые существуют на большинстве строк? Это все тот же метод обратного индекса. –

0

В этом случае вам нужен обратный индекс.

Если вы хотите много заплатить, вы можете использовать индекс полнотекстового поиска для конкретной базы данных и настраивать индексирование на индекс для каждого подмножества слов.

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

Вам необходимо предварительно проиндексировать строку, используя токенизатор, и создать обратный индексный файл. Мы имеем аналогичный вариант использования на Java, где у нас должен быть очень быстрый автозаполнение в большом наборе данных.

Вы можете посмотреть Lucene.NET, который является портом Apache Lucene (на Java).

Если вы хотите повернуть LINQ, вы можете использовать NHibernate Search. (Подмигивание).

Другой вариант - реализовать предварительную индексацию в памяти, с предварительной обработкой и обходом сканирования ненужных, взгляните на Knuth-Morris-Pratt algorithm.

+1

Это работает только в том случае, если OP ищет целые слова. – usr

+0

Здравствуйте, usr, для конкретной базы данных это зависит от конкретной базы данных. Для Lucene вы можете настроить токенизатор Lucene для всех подмножеств последовательности символов, которые производят больший индексный файл. –

+1

Вы уверены? Для одной (!) 100-символьной строки это произвело бы около 5000 токенов (квадратичных)! Это непрактично. Также вам не нужно будет использовать Lucene для этого. Просто используйте словарь. Но, как я уже сказал, слишком много возможных подстрок. – usr

0

Существует один подход, который будет намного быстрее. Но это означало бы поиск точных совпадений слов, а не использование функциональности Contains.

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

Таким образом, словарь может иметь тип <string, List<int>>. Разумеется, здесь преимущество заключается в том, что вы объединяете много слов в меньшую коллекцию. И словарь очень быстрый с поиском, поскольку он построен на хеш-таблице.

Теперь, если это не то, что вы ищете, вы можете искать библиотеки полнотекстового поиска в памяти. SQL Server поддерживает полнотекстовый поиск, используя индексирование, чтобы ускорить процесс за пределами традиционных подстановочных поисков. Но чистое решение в памяти, несомненно, будет быстрее. Однако это все равно может не дать вам точной функциональности поиска подстановочных знаков.

+0

Спасибо, да, это была бы полная подстрока, а не просто поиск слов. Спасибо хоть. –

+0

@NielsBrinch - Мне любопытно. Является ли поиск подстановочных знаков (как вы написали свой код) действительно желаемым результатом? Я имею в виду, что я использовал код для поиска подстановочных знаков все время, пока не обнаружил преимущества полнотекстового поиска. Есть 3 больших преимущества, которые предоставляют полнотекстовые поисковые библиотеки: индексирование (для скорости), ранжирование (для релевантности) и возможность поиска альтернативных версий корневого слова. –

+0

Оптимально мне было бы хорошо с поиском по фактическим английским словам, но это нужно, даже если оно добавлено к другому слову. «Велосипед» должен быть найден «циклом». –

Смежные вопросы