У меня есть SortedDictionary<string, string>, упорядоченный по убыванию длины ключа, в форме:
Мне нужно найти значение самого длинного ключа в словаре, который соответствует подстроке в данной фразе. Учитывая, что в словаре около тысячи статей, есть ли лучший подход, чем перебор?
Примеры выше должны вернуться:
"рыжая лиса" - адрес1
"лисы" - адрес3
У меня есть решение методом перебора ключей, но, учитывая количество ключей и необходимость выполнять поиск по десяткам фраз, я ищу более умный подход.
-1
Я провел кое-какие измерения производительности.
При словаре из 100 ключей поиск совпадений по 100 фразам занимал в среднем 65 мс.
Если время выполнения линейно зависит от длины словаря и количества фраз, то весь процесс займет около 20 секунд (только время поиска).
Учитывая время, необходимое для чтения фраз из файлов и использования совпадений, на весь прогон уйдет примерно 3-4 минуты.
Пользователю за это время придется посмотреть какую-нибудь анимацию, а можно выпить стаканчик чего-нибудь.
Будет ли это приемлемо?
Если да, то подход грубой силы подойдет.
Если нет, то я все еще открыт для предложений.
@JonasH На самом деле мне приходится перебирать около 300 файлов, каждый из которых в среднем содержит 10 фраз. В сумме получается 500 x 300 x 10 = 1,5 миллиона строк, операций Содержит(), довольно много.
Я бы, вероятно, порекомендовал какую-нибудь стороннюю библиотеку для полнотекстового поиска. Может быть, Lucene.Net. Но требования к производительности и то, как часто вам нужно это делать, по-прежнему имеют значение. Создание поискового индекса также займет некоторое время.
Вы ищете какую-либо подстроку, скажем, d fox is ho
или ограниченный/предварительно обработанный набор из них, например. только целые слова, без знаков препинания - red fox is home
, fox is home
, red fox is
, но не d fox is ho
Похоже, вам нужен индекс ngram. Какова минимальная и максимальная длина строк поиска?
@Dmitry Фразы из одного или двух слов, только целые слова, без знаков препинания, без учета регистра.
QMathias Одно слово мин. два слова максимум. Я не знаком с нграмом.
Судя по вашему требованию, подход грубой силы действительно не лучший способ сделать это. Гораздо более эффективным подходом было бы использование типа «Trie» (дерево префиксов). Использование «Trie» может помочь вам эффективно найти самый длинный совпадающий ключ в заданной фразе. Просто создайте структуру «Tire», а затем вставьте каждый ключ из вашего «SortedDictionary» в «Trie». Каждый узел в структуре «Trie» будет представлять символ ключа. В конце каждого ключа сохраните соответствующее значение (адрес). Для поиска самого длинного совпадения вы можете реализовать следующее: Для каждой фразы переберите каждую подстроку и найдите ее в структуре «Trie». Затем просто отслеживайте самое длинное совпадение, найденное для каждой фразы. Я считаю, что это то, что вам нужно. Посмотрите здесь: Реализация Trie на C#, Использование класса Trie для эффективного поиска по тексту в C#
Я просто поместил код ниже. Я думаю, что в целом он делает то, что вам нужно:
public sealed class Trie
{
private readonly TrieNode _root = new();
public void Insert(string key, string address)
{
var node = _root;
foreach (var ch in key)
{
if (!node.Children.ContainsKey(ch))
{
node.Children[ch] = new TrieNode();
}
node = node.Children[ch];
}
node.Address = address;
}
public string SearchLongestMatch(string phrase, out string address)
{
address = null;
var maxLength = 0;
TrieNode longestMatchNode = null;
for (var i = 0; i < phrase.Length; i++)
{
var node = _root;
var length = 0;
for (var j = i; j < phrase.Length; j++)
{
var ch = phrase[j];
if (node.Children.TryGetValue(ch, out var child))
{
node = child;
length++;
if (length > maxLength)
{
maxLength = length;
longestMatchNode = node;
}
}
else
{
break;
}
}
}
if (longestMatchNode != null)
{
address = longestMatchNode.Address;
var index = phrase.IndexOf(longestMatchNode.Address, StringComparison.Ordinal);
if (index != -1)
{
return phrase.Substring(index, maxLength);
}
}
return null;
}
}
public class TrieNode
{
public Dictionary<char, TrieNode> Children { get; set; } = new();
public string Address { get; set; }
}
public sealed class DescendingComparer<T> : IComparer<T> where T : IComparable<T>
{
public int Compare(T? x, T? y)
{
return y.CompareTo(x);
}
}
public class Program
{
public static void Main()
{
var dict = new SortedDictionary<string, string>(new DescendingComparer<string>())
{ { "red fox", "address1" }, { "weasel", "address2" }, { "foxes", "address3" }, { "fox", "address4" } };
var trie = new Trie();
foreach (var kvp in dict)
{
trie.Insert(kvp.Key, kvp.Value);
}
var phrases = new List<string> { "today the red fox is home", "no foxes today" };
foreach (var phrase in phrases)
{
var match = trie.SearchLongestMatch(phrase, out var address);
Console.WriteLine($"Phrase: \"{phrase}\" - Match: \"{match}\" - Address: \"{address}\"");
}
}
}
Мне неясно, как Trie поможет при поиске подстрок, т. е. .Contains(...)
. Однако суффиксное дерево может сделать это.
Я имею в виду, что это может быть иерархический шаблон Root-Node, поэтому он должен состоять из классов Tire - TireNode. Где класс TrieNode представляет каждый узел в Trie со словарем для дочерних элементов и строкой для адреса. А класс Trie занимается вставкой ключей и поиском самого длинного совпадения. Таким образом, Tire должен реализовать что-то вроде «GetLongest(строковая фраза, адрес выходной строки), который находит самый длинный совпадающий ключ в заданной фразе и возвращает самый длинный адрес узла совпадения.
Я решил это другим подходом:
Данный:
Для каждой фразы в файлах найдите самую длинную подфразу из 1–3 слов, соответствующую одному из ключей словаря.
Решение:
Для каждой фразы p в F создайте список, содержащий все подфразы из 1, 2 и 3 слов, упорядоченные по убыванию длины; будут подфразы 3W-3.
Для каждой подфразы в списке проверьте, соответствует ли она ключу в словаре.
(Напротив, метод грубой силы для каждой фразы p в каждом файле перебирает ключи словаря, используя p.Contains(key) для поиска совпадения.)
Производительность:
Затраченное время линейно пропорционально F, P и W и постоянно для поиска по словарю.
Это улучшение по сравнению с методом грубой силы, который также пропорционален размеру словаря. Таким образом, прирост производительности увеличивается с увеличением размера словаря. Кроме того, метод грубой силы использует string.Contains(), что также отнимает много времени.
Подводя итог, решение грубой силы — это O(N), а решение подфразы — O(1).
Я думаю, нет необходимости публиковать какой-либо код, это всего лишь простой двойной цикл, создающий список подфраз.
Вы измеряли производительность? Каковы ваши требования к производительности? Строки длиной 1 тыс. — это не так уж и много, поэтому линейный поиск должен быть достаточно быстрым для многих целей.