Автор оригинала: FreeCodeCamp Community Member.
Алексом Надалин
Насколько красивая {}
?
Он позволяет хранить значения по ключу и получить их очень экономически эффективным образом ( o (1)
, больше на это позже).
В этом посте я хочу реализовать очень базовый хэш-стол и взглянуть на его внутреннюю работу, чтобы объяснить одну из самых ориентированных идей в информатике.
Проблема
Представьте, что вы строите новый язык программирования: вы начинаете с помощью довольно простых типов (строк, целые числа, плавать, …), а затем перейти к реализации очень основных структур данных. Сначала вы придумываете массив ( []
), затем приходит хэш-таблица (в противном случае известный как словарь, ассоциативный массив, hashmap, карта и … список поступает).
Вы когда-нибудь задавались вопросом, как они работают? Как они так чертовски быстро?
Ну, давайте скажем, что JavaScript не имел? {} или
Новая карта () и давайте реализовать наш самой собственным
Ганмап !
Примечание о сложности
Прежде чем мы доставим шарика, нам нужно понять, как работает сложность функций: Wikipedia имеет хорошее повышение к Вычислительная сложность , но я добавлю краткое объяснение ленивых.
Сложность измеряет, сколько шагов требуется нашей функции – чем меньше шагов, тем быстрее выполнение (также известное как «время работы»).
Давайте посмотрим на следующий фрагмент:
function fn(n, m) { return n * m}
Вычислительная сложность (от сейчас просто «сложность») FN
это O (1)
, что означает, что это постоянно (вы можете прочитать o (1)
как « Стоимость – это один »): Независимо от того, какие аргументы вы проходите, платформа, которая запускает этот код, только должна выполнять одну операцию ( Умножьте n
в m
). Опять же, поскольку это одна операция, стоимость называется O (1)
Отказ
Сложность измеряется путем при условии, что аргументы вашей функции могут иметь очень большие значения. Давайте посмотрим на этот пример:
function fn(n, m) { let s = 0
for (i = 0; i < 3; i++) { s += n * m }
return s}
Вы думаете, что его сложность – это O (3)
, верно?
Опять же, поскольку сложность измеряется в контексте очень больших аргументов, мы склонны «падать» константы и рассмотреть O (3)
так же, как O (1)
Отказ Итак, даже в этом случае мы бы сказали, что сложность FN
это O (1)
Отказ Независимо от того, какова ценность N
и м
Вы всегда заканчиваете три операции – которые, опять же, является постоянной стоимостью (поэтому o (1)
).
Теперь этот пример немного отличается:
function fn(n, m) { let s = []
for (i = 0; i < n; i++) { s.push(m) }
return s}
Как видите, мы пишите столько раз, сколько ценность N
, что может быть в миллионах. В этом случае мы определяем сложность этой функции как O (n)
, как вам нужно будет делать столько операций как значение одного из ваших аргументов.
Другие примеры?
function fn(n, m) { let s = []
for (i = 0; i < 2 * n; i++) { s.push(m) }
return s}
Это примеры петли 2 * n
раз, то есть сложность должна быть O (2n)
Отказ Поскольку мы упомянули, что константы «игнорируются» при расчете сложности функции, этот пример также классифицируется как O (n)
Отказ
Еще один?
function fn(n, m) { let s = []
for (i = 0; i < n; i++) { for (i = 0; i < n; i++) { s.push(m) } }
return s}
Здесь мы зацикливаемся на N
и снова зацикливаться внутри основной петли, что означает сложность «в квадрате» ( N * N
): если N
2, мы будем работать S.PUSH (M)
4 раза, если 3 мы будем работать в 9 раз и так далее.
В этом случае сложность функции называется O (n²)
Отказ
Один последний пример?
function fn(n, m) { let s = []
for (i = 0; i < n; i++) { s.push(n) }
for (i = 0; i < m; i++) { s.push(m) }
return s}
В этом случае у нас нет вложенных петель, но мы в два раза больше двух разных аргументов: сложность определяется как O (n + m)
Отказ Кристально чистый.
Теперь, когда вы только что получили краткое введение (или освещение) на сложность, очень легко понять, что функция со сложностью O (1)
собирается выступать намного лучше, чем один с O (n)
Отказ
Hash Tables есть O (1)
Сложность: в Условиях Лэймана, они сверхбырский Отказ Давайте двигаться дальше.
(Я вроде лежа на хэш-таблицах всегда O (1) сложность, но просто прочитано;))
Давайте построим (тупой) хэш-стол
Наш хеш-столик имеет 2 простых метода – Set (x, y)
и Получить (х)
Отказ Давайте начнем писать какой-нибудь код:
И давайте реализовать очень простой неэффективный способ сохранения этих пар клавиш и получить их позже. Сначала мы начинаем, сохраняя их во внутреннем массиве (помните, мы не можем использовать {}
Поскольку мы реализуем {}
– разум!):
Тогда это просто вопрос получения правильного элемента из списка:
Наш полный пример:
Наше Ганмап
удивительно! Он работает прямо из коробки, но как он будет выполнять, когда мы добавим большое количество пар ключевых пар?
Давайте попробуем простой ориентир. Сначала мы постараемся найти несуществующий элемент в хэш-таблице с очень немногими элементами, а затем попробуйте одинаковую в одном с большим количеством элементов:
Результаты? Не так поощряет:
with very few records in the map: 0.118mswith lots of records in the map: 14.412ms
В нашей реализации нам нужно петить все элементы внутри Это .List
Для того, чтобы найти один с соответствующим ключом. Стоимость – это O (n)
И это совсем ужасно.
Сделать это быстро (ER)
Нам нужно найти способ избежать зацикливания через наш список: время поставить хеш вернуться в хеш-таблица Отказ
Вы когда-нибудь задавались вопросом, почему эта структура данных называется хеш Таблица? Это потому, что на клавишах вы устанавливаете функцию хеширования и получаете. Мы будем использовать эту функцию, чтобы превратить наш ключ в целое число Я
и сохранить нашу ценность в индексе Я
нашего внутреннего списка. Поскольку доступ к элементу, по его индексу из списка имеет постоянную стоимость ( o (1)
), то таблица Hash также будет иметь стоимость O (1)
Отказ
Давайте попробуем это:
Здесь мы используем string-hash Модуль, который просто преобразует строку в числовой хеш. Мы используем его для хранения и привлечения элементов при индексе хэш (ключ)
из нашего списка. Результаты?
with lots of records in the map: 0.013ms
W – O – W. Это то, о чем я говорю!
Нам не нужно вешать все элементы в списке и извлекать элементы из Ганмап
очень быстро!
Позвольте мне поставить это как можно лучше: Hashing – это то, что делает хеш-таблицы чрезвычайно эффективными Отказ Нет магии. Ничего больше. Нада. Просто простая, умная, гениальная идея.
Стоимость выбора правильной хеширования функции
Конечно, Выбор быстрого хеширования очень важен. Если наше хэш (ключ)
Проходит через несколько секунд, наша функция будет довольно медленной независимо от его сложности.
В то же время Очень важно убедиться, что наша функция хеширования не производит много столкновений , так как они были бы вредными для сложности нашего хэш-стола.
Смущенный? Давайте приблизимся к столкновениям.
Коллизии
Вы можете подумать « ах, хорошая функция хеширования никогда не генерирует столкновения! »: Ну, вернитесь к реальному миру и снова подумайте. Google смог производить столкновения для алгоритма хеширования SHA-1 , и это просто вопрос времени, или вычислительная мощность, прежде чем хеширование функции трещин и возвращает то же хеш для 2 различных входов. Всегда предполагайте, что ваша функция хеширования генерирует столкновения и реализует право защиту от таких случаев.
Случай в точке, давайте попробуем использовать хеш ()
Функция, которая генерирует много столкновений:
Эта функция использует массив из 10 элементов для хранения значений, что означает, что элементы могут быть заменены – противная ошибка в нашем Ганмап
:
Чтобы решить проблему, мы можем просто хранить несколько пар клавишных пар при одном индексе. Итак, давайте изменим наш хеш-таблица:
Как вы можете заметить, здесь мы возвращаемся к нашему оригинальному осуществлению: храните список пар клавишных пар и петлей через каждый из них. Это будет довольно медленно, когда есть много столкновений для определенного индекса списка.
Давайте ориентирован на это, используя наш собственный хеш ()
Функция, которая генерирует индексы от 1 до 10:
with lots of records in the map: 11.919ms
и используя хеш-функцию из string-hash
, который генерирует случайные индексы:
with lots of records in the map: 0.014ms
WHOA! Существует стоимость выбора правильной функции хеширования – достаточно быстро, чтобы она не замедляла наше исполнение самостоятельно, и достаточно хорош, чтобы он не производит много столкновений.
Как правило, o (1)
Помните мои слова?
Ну, я лгал: сложность хеш-таблица зависит от функции хеширования, которую вы выбираете. Чем больше столкновения вы генерируете, тем больше сложность имеет тенденцию к O (n)
Отказ
Функция хеширования, такая как:
function hash(key) { return 0}
будет означать, что наш хэш-столик имеет сложность O (n)
Отказ
Вот почему в целом вычислительная сложность имеет три меры: лучшие, средние и худшие сценарии. У Hashtables есть O (1)
Сложность в лучших и средних случаях сценариев, но упасть на O (n)
в их худшем случае.
Помните: Хорошая функция хеширования – ключ к эффективному хэш-таблицу – ни больше ни меньше.
Подробнее о столкновениях …
Техника, которую мы использовали для исправления Ганмап
В случае столкновения называется Отдельные цепочки : Мы храним все ключевые пары, которые генерируют столкновения в списке и петле через них.
Еще одна популярная техника открытая адресация :
- На каждом индексе нашего списка мы храним один и один только пара
- При попытке хранить пару на индекс
х
, если уже есть пара ключа, попробуйте сохранить нашу новую пару нах + 1.
- Если
х + 1
взят, попробуйх + 2
и так далее… - При извлечении элемента, хеш-ключ и посмотрите, если элемент в этой позиции (
x
) соответствует нашему ключу - Если нет, попробуйте получить доступ к элементу в положении
х + 1.
- Промыть и повторите, пока не дойдете до конца списка, или когда вы найдете пустой индекс – это означает, что наш элемент не находится в таблице Hash
Умный, простой, элегантный и Обычно очень эффективно !
Часто задаваемые вопросы (или TL; DR)
Хеш-таблицы хэш значений, которые мы храним?
Нет, ключи хешируются, чтобы их можно было превратить в целое число Я
и оба клавиши и значения хранятся в положении Я
в списке.
Действуйте функции хеширования, используемые хеш-таблицами, генерируют столкновения?
Абсолютно – так хэш-таблицы реализованы с Стратегии защиты Чтобы избежать неприятных ошибок.
Hash Tabls используют список или связанный список внутри?
Это зависит, Оба могут работать Отказ В наших примерах мы используем массив JavaScript ( []
), который может быть Динамически изменены :
> a = []
> a[3] = 1
> a[ <3 empty items>, 1 ]
Почему вы выбрали JavaScript для примеров? Массивы JS – это хэш-таблицы!
Например:
> a = [][]
> a["some"] = "thing"'thing'
> a[ some: 'thing' ]
> typeof a'object'
Я знаю, чертовски JavaScript.
JavaScript – это «универсальный» и, вероятно, самый простой язык, чтобы понять, когда смотрите на какой-то код образца. JS не может быть лучшим языком, но я надеюсь, что эти примеры достаточно ясны.
Ваш пример действительно хорошая реализация хэш-стола? Это действительно так просто?
Нет, совсем нет.
Посмотрите на « », реализация хэш-стола в JavaScript “by Мэтт Зеунерт , как это даст вам немного больше контекста. Есть гораздо больше, чтобы учиться, поэтому я бы также предположил, что вы проверили:
- Курс Пола Кубе на хэш-таблицах
- Реализация нашего собственного хэш-стола с отдельными цепочками в Java
- Алгоритмы, 4-е издание – хэш-таблицы
- Проектирование быстрого хэш-стола
В конце концов…
Хэш-таблицы – очень умная идея, которую мы используем на регулярной основе: независимо от того, создаете ли вы Словарь в Python , Ассоциативный массив в PHP или Карта в JavaScript Отказ Все они разделяют одни и те же концепции и красиво работа, чтобы сообщить об использовании и извлечении элемента идентификатором, в (скорее всего) постоянной стоимости.
Надеюсь, вам понравилась эта статья и не стесняйтесь делиться своими отзывами со мной.
Особая спасибо Джо Кто помог мне, рассмотрев эту статью.
АДИОС!