Можно ли отобразить строку в int быстрее, чем при использовании hashmap?

Я понимаю, что не должен оптимизировать каждую часть моей программы, поэтому, пожалуйста, сочтите этот вопрос "академическим"

У меня есть максимум 100 строк и целое число для каждой из них, что-то вроде этого:

MSFT 1
DELL 2
HP   4
....
ABC  58

Этот набор предварительно инициализирован, что означает, что после его создания он никогда не изменяется. После того, как набор инициализирован, я использую его довольно интенсивно, поэтому приятно иметь быстрый поиск. Строки довольно короткие, максимум 30 символов. Подключенные int также ограничен и составляет от 1 до 100.

По крайней мере, зная, что строки предварительно инициализированы и никогда не меняются, должна быть возможность "найти" хеш-функцию, которая приводит к отображению "одна корзина-один элемент", но, вероятно, существуют и другие хаки.

Я могу себе представить одну оптимизацию - я могу читать только первый символ. Например, если "DELL" является единственной строкой, начинающейся с "D", и я получил что-то вроде "D***", то мне не нужно даже читать строку! это безусловно "DELL". Такой поиск должен быть значительно быстрее, чем "поиск по хэш-карте". (ну тут я предположил, что мы получаем только символы, которые в хэше, но это не всегда так)

Есть ли готовые к использованию или простые в реализации решения для моей проблемы? Я использую C++ и Boost.

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

7 ответов

Решение

Хеш-таблица [1] в принципе является самым быстрым способом.

Однако вы можете скомпилировать Perfect Hash Function, учитывая тот факт, что вы знаете весь домен заранее.

С идеальным хешем не должно быть столкновений, поэтому вы можете хранить хеш-таблицу в линейном массиве!

При правильной настройке вы можете тогда

  • разместить все элементы хеша в ограниченном пространстве, что делает прямой адрес потенциальным вариантом
  • иметь обратный поиск в O(1)

Инструментом старой школы для генерации функций Perfect Hash будет gperf (1). В википедии перечислено больше ресурсов на эту тему.

Из-за всех дебатов я запустил демонстрацию:

Загрузка символов тикера NASDAQ и получение 100 случайных выборок из этого набора с применением gperf следующим образом:

gperf -e ' \015' -L C++ -7 -C -E -k '*,1,$' -m 100 selection > perfhash.cpp

В результате получается хеш-значение MAX_HASH_VALUE, равное 157 и таблица прямого поиска строк, содержащая как можно больше элементов. Вот только хэш-функция для демонстрационных целей:

inline unsigned int Perfect_Hash::hash (register const char *str, register unsigned int len) {
  static const unsigned char asso_values[] = {
      156, 156, 156, 156, 156, 156, 156, 156, 156, 156,
      156, 156, 156, 156, 156, 156, 156, 156, 156, 156,
      156, 156, 156, 156, 156, 156, 156, 156, 156, 156,
      156, 156, 156, 156, 156, 156, 156, 156, 156, 156,
      156, 156, 156, 156, 156, 156, 156, 156, 156, 156,
      156, 156, 156, 156, 156, 156, 156, 156, 156, 156,
      156, 156, 156, 156, 156,  64,  40,   1,  62,   1,
       41,  18,  47,   0,   1,  11,  10,  57,  21,   7,
       14,  13,  24,   3,  33,  89,  11,   0,  19,   5,
       12,   0, 156, 156, 156, 156, 156, 156, 156, 156,
      156, 156, 156, 156, 156, 156, 156, 156, 156, 156,
      156, 156, 156, 156, 156, 156, 156, 156, 156, 156,
      156, 156, 156, 156, 156, 156, 156, 156, 156
    };
  register int hval = len;

  switch (hval) {
      default: hval += asso_values[(unsigned char)str[4]];   /*FALLTHROUGH*/
      case 4:  hval += asso_values[(unsigned char)str[3]];   /*FALLTHROUGH*/
      case 3:  hval += asso_values[(unsigned char)str[2]+1]; /*FALLTHROUGH*/
      case 2:  hval += asso_values[(unsigned char)str[1]];   /*FALLTHROUGH*/
      case 1:  hval += asso_values[(unsigned char)str[0]];   break;
  }
  return hval;
}

Это действительно не становится намного более эффективным. Посмотрите https://gist.github.com/sehe/5433535

Имейте в виду, это тоже идеальный хеш, поэтому столкновений не будет


Q. [...] это явно "DELL". Такой поиск должен быть значительно быстрее, чем "поиск по хэш-карте".

A: Если вы используете простой std::map чистый эффект - поиск по префиксу (потому что лексикографические сочетания клавиш при несовпадении первого символа). То же самое касается бинарного поиска в отсортированном контейнере.


[1] PS. Для 100 строк, отсортированный массив строк с std::search или же std::lower_bound потенциально будет так же быстро / быстрее из-за улучшенного местоположения ссылки. Посмотрите результаты своего профиля, чтобы увидеть, применимо ли это.

Небольшое дополнение к посту sehe:

Если вы используете простой std::map чистый эффект - поиск по префиксу (потому что лексикографические сочетания клавиш при несовпадении первого символа). То же самое касается бинарного поиска в отсортированном контейнере.

Вы можете использовать поиск по префиксу, чтобы быть намного более эффективным. Проблема с обоими std::map и наивный бинарный поиск состоит в том, что они будут читать один и тот же префикс для каждого отдельного сравнения, делая общий поиск O (m log n), где m - длина строки поиска.

Это причина, по которой хеш-карта превосходит эти два метода для больших наборов. Однако существует структура данных, которая не выполняет избыточные сравнения префиксов, и фактически должна сравнивать каждый префикс ровно один раз: дерево префикса (поиска), более известное как trie, и поиск единственной строки длины m осуществимы в O (m) то же самое асимптотическое время выполнения вы получаете для хеш-таблицы с идеальным хешированием.

Вопрос о том, является ли для ваших целей более эффективным использование хе-таблицы tree или (прямого поиска) с идеальным хешированием - вопрос профилирования.

(Пока) Еще одно небольшое дополнение к sehe's ответ:

Помимо Perfect Hash Functions, есть эта функция Minimal Perfect Hash Function, и соответственно C Minimal Perfect Hash Function, Это почти так же, как gperf, Кроме этого:

gperf немного отличается, так как он был задуман для создания очень быстрых идеальных хеш-функций для небольших наборов ключей, а библиотека CMPH была задумана для создания минимальных идеальных хеш-функций для очень больших наборов ключей

Библиотека CMPH объединяет новейшие и более эффективные алгоритмы в простой в использовании, быстрый и качественный API. Библиотека была разработана для работы с большими записями, которые не помещаются в основную память. Он успешно использовался для построения минимальных совершенных хеш-функций для наборов с более чем 100 миллионами ключей, и мы намерены расширить это число до порядка миллиарда ключей

источник: http://cmph.sourceforge.net/

Да!

Хеш должен пройти через вашу строку и построить хеш-значение. при использовании trie, как описано в ссылке [ Wiki: Trie], нужно только следовать по пути в связанной структуре без каких-либо перерасчетов. и если это сжатое три, как объяснено в конце страницы, он учитывает случай, когда инициал для одного слова (случай DELL, о котором вы говорили). предварительная обработка немного выше, но дает лучшую производительность во время выполнения.

еще несколько преимуществ:
1. если искомая строка не существует, вы знаете, что в первом символе, который отличается от существующих строк (не нужно продолжать вычисление)
2. после того, как реализовано, добавление большего количества строк в Trie является прямым.

Как упоминалось выше, стандартное хэш-отображение, а также совершенная хеш-функция страдают от относительно медленного выполнения самой хеш-функции. Набросок совершенной хэш-функции, например, имеет до 5 случайных обращений к массиву.

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

Вы найдете обсуждение связанной темы "переключение на строку" на моем сайте, а также множество решений, использующих общий тестовый стенд с использованием макросов в качестве бесплатных источников C / C++, которые решают проблему во время выполнения. Я также думаю о прекомпиляторе.

Ну, вы можете хранить строки в двоичном дереве и искать там. Хотя это имеет O(log n) теоретически, на практике это может быть намного быстрее, если у вас есть только несколько клавиш, которые действительно длинные и уже отличаются по первым нескольким символам.

Т.е. при сравнении ключей дешевле вычисления хеш-функции.

Кроме того, существуют эффекты кэширования процессора, и такие, которые могут (или не могут) быть полезными.

Тем не менее, с довольно дешевой хэш-функцией, хеш-таблицу будет трудно превзойти.

Если строки известны во время компиляции, вы можете просто использовать перечисление:

enum
{
  Str1,
  Str2
};

const char *Strings = {
  "Str1",
  "Str2"
};

Используя некоторые трюки макросов, вы можете удалить избыточность воссоздания таблицы в двух местах (с помощью включения файла и #undef).

Тогда поиск может быть достигнут так же быстро, как индексация массива:

const char *string = Strings[Str1]; // set to "Str1"

Это будет иметь оптимальное время поиска и местность ссылки.

Другие вопросы по тегам