mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-08 21:46:06 +00:00
First version.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# Хеш-алгоритмы
|
||||
|
||||
В предыдущих двух разделах были рассмотрены принципы работы хеш-таблиц и методы обработки хеш-коллизий. Однако ни открытая адресация, ни цепная адресация **не могут уменьшить возникновение хеш-коллизий, они лишь обеспечивают нормальную работу хеш-таблицы при возникновении коллизий**.
|
||||
|
||||
Если хеш-коллизии возникают слишком часто, производительность хеш-таблицы резко ухудшается. Как показано на рисунке ниже, для хеш-таблицы с цепной адресацией в идеальном случае пары ключ-значение равномерно распределены по всем корзинам, достигая наилучшей эффективности поиска; в худшем случае все пары ключ-значение хранятся в одной корзине, и временная сложность деградирует до $O(n)$.
|
||||
|
||||

|
||||
|
||||
**Распределение пар ключ-значение определяется хеш-функцией**. Вспомним этапы вычисления хеш-функции: сначала вычисляется хеш-значение, затем берется остаток от деления на длину массива:
|
||||
|
||||
```shell
|
||||
index = hash(key) % capacity
|
||||
```
|
||||
|
||||
Рассматривая приведенную выше формулу, когда емкость хеш-таблицы `capacity` фиксирована, **хеш-алгоритм `hash()` определяет выходное значение**, а следовательно, и распределение пар ключ-значение в хеш-таблице.
|
||||
|
||||
Это означает, что для снижения вероятности возникновения хеш-коллизий следует сосредоточить внимание на разработке хеш-алгоритма `hash()`.
|
||||
|
||||
## Цели хеш-алгоритма
|
||||
|
||||
Для реализации структуры данных хеш-таблицы, которая является "быстрой и стабильной", хеш-алгоритм должен обладать следующими характеристиками.
|
||||
|
||||
- **Детерминированность**: для одного и того же входа хеш-алгоритм всегда должен выдавать один и тот же выход. Только так можно обеспечить надежность хеш-таблицы.
|
||||
- **Высокая эффективность**: процесс вычисления хеш-значения должен быть достаточно быстрым. Чем меньше вычислительные затраты, тем выше практичность хеш-таблицы.
|
||||
- **Равномерное распределение**: хеш-алгоритм должен обеспечивать равномерное распределение пар ключ-значение в хеш-таблице. Чем равномернее распределение, тем ниже вероятность хеш-коллизий.
|
||||
|
||||
На самом деле хеш-алгоритмы помимо реализации хеш-таблиц широко применяются и в других областях.
|
||||
|
||||
- **Хранение паролей**: для защиты паролей пользователей система обычно не хранит пароли в открытом виде, а хранит их хеш-значения. Когда пользователь вводит пароль, система вычисляет хеш-значение введенного пароля и сравнивает его с сохраненным хеш-значением. Если они совпадают, пароль считается правильным.
|
||||
- **Проверка целостности данных**: отправитель данных может вычислить хеш-значение данных и отправить его вместе с данными; получатель может заново вычислить хеш-значение полученных данных и сравнить его с полученным хеш-значением. Если они совпадают, данные считаются целостными.
|
||||
|
||||
Для криптографических приложений, чтобы предотвратить обратное восстановление исходного пароля из хеш-значения и другие виды обратной инженерии, хеш-алгоритм должен обладать более высоким уровнем безопасности.
|
||||
|
||||
- **Односторонность**: невозможно восстановить какую-либо информацию о входных данных из хеш-значения.
|
||||
- **Устойчивость к коллизиям**: должно быть крайне сложно найти два разных входа, дающих одинаковое хеш-значение.
|
||||
- **Эффект лавины**: небольшое изменение входа должно приводить к значительному и непредсказуемому изменению выхода.
|
||||
|
||||
Обратите внимание, что **"равномерное распределение" и "устойчивость к коллизиям" являются двумя независимыми понятиями**, удовлетворение равномерному распределению не обязательно означает устойчивость к коллизиям. Например, при случайном входе `key` хеш-функция `key % 100` может давать равномерно распределенный выход. Однако этот хеш-алгоритм слишком прост, все `key` с одинаковыми последними двумя цифрами дают одинаковый выход, поэтому мы можем легко восстановить подходящий `key` из хеш-значения и таким образом взломать пароль.
|
||||
|
||||
## Разработка хеш-алгоритма
|
||||
|
||||
Разработка хеш-алгоритма — это сложная задача, требующая учета многих факторов. Однако для некоторых нетребовательных сценариев мы также можем разработать простые хеш-алгоритмы.
|
||||
|
||||
- **Аддитивное хеширование**: складываются ASCII-коды каждого символа входа, полученная сумма используется как хеш-значение.
|
||||
- **Мультипликативное хеширование**: используя независимость умножения, на каждом шаге умножается на константу, ASCII-коды различных символов накапливаются в хеш-значении.
|
||||
- **XOR-хеширование**: каждый элемент входных данных накапливается в хеш-значении через операцию XOR.
|
||||
- **Ротационное хеширование**: ASCII-код каждого символа накапливается в хеш-значении, перед каждым накоплением выполняется операция вращения хеш-значения.
|
||||
|
||||
```src
|
||||
[file]{simple_hash}-[class]{}-[func]{rot_hash}
|
||||
```
|
||||
|
||||
Можно заметить, что последним шагом каждого хеш-алгоритма является взятие остатка от деления на большое простое число $1000000007$, чтобы обеспечить нахождение хеш-значения в подходящем диапазоне. Стоит задуматься, почему важно брать остаток от деления именно на простое число, или каковы недостатки взятия остатка от деления на составное число? Это интересный вопрос.
|
||||
|
||||
Сначала дадим вывод: **использование большого простого числа в качестве модуля может максимально обеспечить равномерное распределение хеш-значений**. Поскольку простое число не имеет общих делителей с другими числами, это может уменьшить периодические паттерны, возникающие из-за операции взятия остатка, тем самым избегая хеш-коллизий.
|
||||
|
||||
Приведем пример: предположим, мы выбираем составное число $9$ в качестве модуля, оно делится на $3$, тогда все `key`, делящиеся на $3$, будут отображаться в три хеш-значения: $0$, $3$, $6$.
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\text{modulus} & = 9 \newline
|
||||
\text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline
|
||||
\text{hash} & = \{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\dots \}
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
Если входные `key` как раз удовлетворяют такому арифметическому распределению данных, то хеш-значения будут группироваться, усиливая хеш-коллизии. Теперь предположим, что `modulus` заменяется на простое число $13$. Поскольку между `key` и `modulus` нет общих делителей, равномерность выходных хеш-значений значительно улучшится.
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\text{modulus} & = 13 \newline
|
||||
\text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline
|
||||
\text{hash} & = \{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \dots \}
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
Следует отметить, что если можно гарантировать, что `key` распределены случайно и равномерно, то выбор простого или составного числа в качестве модуля не имеет значения, оба могут выдавать равномерно распределенные хеш-значения. Однако когда распределение `key` имеет определенную периодичность, взятие остатка от составного числа с большей вероятностью приводит к группировке.
|
||||
|
||||
В целом мы обычно выбираем простое число в качестве модуля, причем это простое число должно быть достаточно большим, чтобы максимально устранить периодические паттерны и повысить надежность хеш-алгоритма.
|
||||
|
||||
## Распространенные хеш-алгоритмы
|
||||
|
||||
Нетрудно заметить, что представленные выше простые хеш-алгоритмы довольно "хрупкие" и далеки от достижения целей разработки хеш-алгоритмов. Например, поскольку сложение и XOR удовлетворяют коммутативному закону, аддитивное хеширование и XOR-хеширование не могут различать строки с одинаковым содержимым, но разным порядком, что может усилить хеш-коллизии и вызвать некоторые проблемы безопасности.
|
||||
|
||||
На практике обычно используются стандартные хеш-алгоритмы, такие как MD5, SHA-1, SHA-2 и SHA-3. Они могут отображать входные данные произвольной длины в хеш-значения фиксированной длины.
|
||||
|
||||
За последнее столетие хеш-алгоритмы находятся в процессе постоянного совершенствования и оптимизации. Часть исследователей стремится повысить производительность хеш-алгоритмов, другая часть исследователей и хакеров пытается найти проблемы безопасности хеш-алгоритмов. В таблице ниже представлены распространенные хеш-алгоритмы, используемые на практике.
|
||||
|
||||
- MD5 и SHA-1 неоднократно подвергались успешным атакам, поэтому они отвергнуты различными приложениями безопасности.
|
||||
- SHA-256 из серии SHA-2 является одним из самых безопасных хеш-алгоритмов, для него до сих пор не было успешных атак, поэтому он часто используется в различных приложениях и протоколах безопасности.
|
||||
- SHA-3 по сравнению с SHA-2 имеет меньшие затраты на реализацию и более высокую вычислительную эффективность, но в настоящее время его охват использования не так широк, как у серии SHA-2.
|
||||
|
||||
<p align="center"> Таблица <id
|
||||
Reference in New Issue
Block a user