Рассмотрим функцию поиска со следующей сигнатурой, которая должна возвращать целое число для заданного строкового ключа:
int GetValue(string key) { ... }
Рассмотрим также, что сопоставления значений ключа, нумерация N, известны заранее, когда записывается исходный код для функции, например:
// N=3
{ "foo", 1 },
{ "bar", 42 },
{ "bazz", 314159 }
Таким образом, действительная (но не идеальная!) реализация для функции для ввода выше:
int GetValue(string key)
{
switch (key)
{
case "foo": return 1;
case "bar": return 42;
case "bazz": return 314159;
}
// Doesn't matter what we do here, control will never come to this point
throw new Exception();
}
Также заранее известно, сколько раз (C >= 1) функция будет вызываться во время выполнения для каждого заданного ключа. Например:
C["foo"] = 1;
C["bar"] = 1;
C["bazz"] = 2;
Однако порядок таких вызовов неизвестен. Например. приведенное выше может описывать следующую последовательность вызовов во время выполнения:
GetValue("foo");
GetValue("bazz");
GetValue("bar");
GetValue("bazz");
или любой другой последовательности, если совпадают числа вызовов.
Существует также ограничение M, указанное в любых единицах, наиболее удобное, определяющее верхнюю границу памяти любых таблиц поиска и других вспомогательных структур, которые могут использоваться GetValue
(структуры инициализируются заранее, эта инициализация не учитывается сложность функции). Например, M = 100 символов или M = 256 sizeof (ссылка на объект).
Вопрос заключается в том, как написать тело GetValue
таким образом, чтобы оно было как можно быстрее - другими словами, суммарное время всех вызовов GetValue
(обратите внимание, что мы знаем общий счет за все вышеперечисленное ) минимальна для заданных N, C и M?
Алгоритм может требовать разумного минимального значения для M, например. M >= char.MaxValue
. Это может также потребовать, чтобы M был выровнен с некоторой разумной границей - например, чтобы он мог быть только двумя. Также может потребоваться, чтобы M была функцией N определенного вида (например, она может допускать допустимые M = N или M = 2N,... или действительные M = N или M = N ^ 2,... и т.д.).
Алгоритм может быть выражен на любом подходящем языке или в другой форме. Для ограничений производительности во время выполнения для сгенерированного кода предположим, что сгенерированный код для GetValue
будет находиться в С#, VB или Java (действительно, любой язык будет работать, если строки обрабатываются как неизменяемые массивы символов, то есть O (1) длину и O (1) индексацию, и никакие другие данные, рассчитанные для них заранее). Кроме того, чтобы упростить это, ответы, которые предполагают, что C = 1 для всех ключей считаются действительными, хотя предпочтительны те ответы, которые охватывают более общий случай.
Некоторые размышления о возможных подходах
Очевидным первым ответом на вышеупомянутое является использование идеального хеша, но общие подходы к нахождению кажутся несовершенными. Например, можно легко создать таблицу для минимального совершенного хэша с использованием хеширования Pearson для данных примера выше, но тогда входной ключ должен быть хэширован для каждого вызова GetValue
, а хэш Pearson обязательно сканирует всю входную строку, Но все образцы ключей на самом деле различаются в их третьем символе, поэтому только это может использоваться как вход для хэша вместо всей строки. Кроме того, если M требуется как минимум char.MaxValue
, то третий символ сам становится совершенным хешем.
Для другого набора ключей это может быть больше недействительным, но все же возможно уменьшить количество символов, рассмотренных до получения точного ответа. Кроме того, в некоторых случаях, когда минимальный совершенный хеш потребует проверки всей строки, может быть возможно уменьшить поиск до подмножества или иным образом сделать его быстрее (например, менее сложной хэширующей функцией?), Сделав хэш не минимальным (т.е. M > N) - эффективно жертвуя пространством ради скорости.
Возможно, также, что традиционное хеширование не является такой хорошей идеей для начала, и проще структурировать тело GetValue
как серию условных обозначений, устроенных так, что первая проверяет символ "самый переменный" (тот, который зависит от большинства ключей), с дополнительными вложенными проверками, если необходимо, чтобы определить правильный ответ. Обратите внимание, что на "дисперсию" здесь может влиять количество просмотров каждой клавиши (C). Кроме того, не всегда легко понять, какова должна быть лучшая структура ветвей - может быть, например, что "самый переменный" символ позволяет вам отличить 10 ключей из 100, но для остальных 90 эта одна дополнительная проверка нет необходимости различать их, и в среднем (учитывая C) количество проверок на ключ больше, чем в другом решении, которое не начинается с "самого переменного" символа. Цель состоит в том, чтобы определить совершенную последовательность проверок.