First version.

This commit is contained in:
krahets
2026-01-20 15:08:42 +08:00
parent 2213a59ff6
commit 8071daddaa
106 changed files with 11790 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
# Хеш-алгоритмы
В предыдущих двух разделах были рассмотрены принципы работы хеш-таблиц и методы обработки хеш-коллизий. Однако ни открытая адресация, ни цепная адресация **не могут уменьшить возникновение хеш-коллизий, они лишь обеспечивают нормальную работу хеш-таблицы при возникновении коллизий**.
Если хеш-коллизии возникают слишком часто, производительность хеш-таблицы резко ухудшается. Как показано на рисунке ниже, для хеш-таблицы с цепной адресацией в идеальном случае пары ключ-значение равномерно распределены по всем корзинам, достигая наилучшей эффективности поиска; в худшем случае все пары ключ-значение хранятся в одной корзине, и временная сложность деградирует до $O(n)$.
![Лучший и худший случаи хеш-коллизий](../assets/hash_collision_best_worst_condition.png)
**Распределение пар ключ-значение определяется хеш-функцией**. Вспомним этапы вычисления хеш-функции: сначала вычисляется хеш-значение, затем берется остаток от деления на длину массива:
```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

View File

@@ -0,0 +1,62 @@
# Хеш-коллизии
Как упоминалось в предыдущем разделе, **в обычных условиях входное пространство хеш-функции значительно больше выходного пространства**, поэтому хеш-коллизии теоретически неизбежны. Например, если входное пространство состоит из всех целых чисел, а выходное пространство соответствует размеру массива, то обязательно несколько целых чисел будут отображаться в один и тот же индекс корзины.
Хеш-коллизии могут привести к ошибкам в результатах запросов, серьезно влияя на работоспособность хеш-таблицы. Чтобы решить эту проблему, при возникновении хеш-коллизий выполняется увеличение емкости хеш-таблицы до тех пор, пока коллизии не исчезнут. Этот метод понятен и прост в реализации, но крайне неэффективен, поскольку увеличение емкости хеш-таблицы требует значительных затрат на перенос данных и вычисление хеш-значений. Для повышения эффективности можно использовать следующие стратегии:
1. Улучшение структуры данных хеш-таблицы, **чтобы она могла нормально функционировать при возникновении хеш-коллизий**.
2. Выполнение увеличения емкости только при необходимости, т. е. когда хеш-коллизии становятся достаточно серьезными.
Основные методы улучшения структуры хеш-таблицы включают цепную адресацию и открытую адресацию.
## Цепная адресация
В исходной хеш-таблице каждая корзина может хранить только одну пару ключ--значение. Цепная адресация преобразует отдельный элемент в связный список, где пары ключ--значение выступают в качестве узлов списка, и все пары ключ--значение, вызвавшие коллизии, хранятся в одном и том же списке. На рис. 6.5 представлен пример хеш-таблицы с цепной адресацией.
![Хеш-таблица с цепной адресацией](../assets/media/image203.jpeg)
Методы работы с хеш-таблицей, реализованной на основе цепной адресации, изменяются следующим образом.
- **Поиск элемента**: вводится ключ, с помощью хеш-функции определяется индекс корзины, после чего осуществляется доступ к головному узлу списка. Затем выполняется обход списка и сравнение ключей для поиска целевой пары ключ--значение.
- **Добавление элемента**: сначала с помощью хеш-функции осуществляется доступ к головному узлу списка, затем узел (пара ключ--значение) добавляется в список.
- **Удаление элемента**: на основе результата хеш-функции осуществляется доступ к головному узлу списка, затем выполняется обход списка для поиска целевого узла и его удаления.
Цепная адресация имеет следующие ограничения.
- **Увеличение занимаемого пространства**: связный список содержит указатели на узлы, что требует больше памяти по сравнению с массивом.
- **Снижение эффективности поиска**: поскольку необходимо линейно обходить список для поиска соответствующего элемента.
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:以下代码给出了链式地址哈希表的简单实现,需要注意两点。使用列表(动态数组)代替链表,从而简化代码。在这种设定下,哈希表(数组)包含多个桶,每个桶都是一个列表。以下实现包含哈希表扩容方法。当负载因子超过 2/3 时,我们将哈希表扩容至原先的 2 倍... -->
```src
[file]{hash_map_chaining}-[class]{hash_map_chaining}-[func]{}
```
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:值得注意的是,当链表很长时,查询效率 O(n) 很差。此时可以将链表转换为"AVL 树"或"红黑树",从而将查询操作的时间复杂度优化至 O(log n)... -->
## Открытая адресация
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文开放寻址open addressing不引入额外的数据结构而是通过"多次探测"来处理哈希冲突,探测方式主要包括线性探测、平方探测和多次哈希等... -->
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:下面以线性探测为例,介绍开放寻址哈希表的工作机制... -->
### Линейная проба
<!-- 🔴 俄文版缺失完整的线性探测部分内容 -->
### Квадратичная проба
<!-- 🔴 俄文版缺失完整的平方探测部分内容 -->
### Множественное хеширование
<!-- 🔴 俄文版缺失完整的多次哈希部分内容 -->
## Выбор языка программирования
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文各种编程语言采取了不同的哈希表实现策略下面举几个例子。Python 采用开放寻址。字典 dict 使用伪随机数进行探测。Java 采用链式地址... -->

View File

@@ -0,0 +1,131 @@
# Хеш-таблицы
<u>Хеш-таблица</u> реализует эффективный поиск элементов через установление соответствия между ключом `key` и значением `value`. Более конкретно, передав ключ в хеш-таблицу, можно получить соответствующее значение за время $O(1)$.
Пусть имеется $n$ студентов, у каждого из которых есть имя и номер. Если нужно реализовать функцию «ввести номер студента и получить соответствующее имя», то можно использовать хеш-таблицу, как показано на рисунке ниже.
![Схематичное представление хеш-таблицы](../assets/media/image195.jpeg)
Помимо хеш-таблиц, функцией поиска также обладают массивы и связные списки. Сравнение их эффективности приведено в таблице ниже.
- **Добавление элемента**: достаточно добавить элемент в конец массива (списка) за время $O(1)$.
- **Поиск элемента**: так как массив (список) не упорядочен, необходимо просмотреть все элементы за время $O(n)$.
- **Удаление элемента**: сначала нужно найти элемент, а затем удалить его из массива (списка), понадобится время $O(n)$.
<p align="center"> Таблица <id> &nbsp; Сравнение эффективности поиска элементов </p>
| | Массив | Связный список | Хеш-таблица |
| --------------------- | -------- | -------------- | ----------- |
| Поиск элемента | $O(n)$ | $O(n)$ | $O(1)$ |
| Добавление элемента | $O(1)$ | $O(1)$ | $O(1)$ |
| Удаление элемента | $O(n)$ | $O(n)$ | $O(1)$ |
Мы видим, **что в хеш-таблице операции добавления, удаления и поиска имеют временную сложность $O(1)$**, что очень эффективно.
## Основные операции с хеш-таблицами
К основным операциям с хеш-таблицами относятся: инициализация, поиск, добавление и удаление пар ключ--значение. Ниже приведен пример кода.
=== "Python"
```python title="hash_map.py"
# Инициализация хеш-таблицы
hmap: dict = {}
# Операция добавления
# Добавление пары ключ-значение (key, value) в хеш-таблицу
hmap[12836] = "Иван"
hmap[15937] = "Петр"
hmap[16750] = "Владимир"
hmap[13276] = "Максим"
hmap[10583] = "Андрей"
# Операция поиска
# Ввод ключа key в хеш-таблицу для получения значения value
name: str = hmap[15937]
# Операция удаления
# Удаление пары ключ-значение (key, value) из хеш-таблицы
hmap.pop(10583)
```
<!-- 🔴 Русская версия содержит только Python код, отсутствуют примеры для C++, Java, C#, Go, Swift, JS, TS, Dart, Rust, C, Kotlin, Ruby -->
??? pythontutor "可视化运行"
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%90%91%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E8%BE%93%E5%85%A5%E9%94%AE%20key%20%EF%BC%8C%E5%BE%97%E5%88%B0%E5%80%BC%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E5%88%A0%E9%99%A4%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap.pop%2810583%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
Существует три распространенных способа обхода хеш-таблицы: обход пар ключ--значение, обход ключей и обход значений. Ниже приведен пример кода.
=== "Python"
```python title="hash_map.py"
# Обход хеш-таблицы
# Обход пар ключ-значение key->value
for key, value in hmap.items():
print(key, "->", value)
# Обход только ключей key
for key in hmap.keys():
print(key)
# Обход только значений value
for value in hmap.values():
print(value)
```
<!-- 🔴 Русская версия содержит только Python код, отсутствуют примеры для C++, Java, C#, Go, Swift, JS, TS, Dart, Rust, C, Kotlin, Ruby -->
??? pythontutor "可视化运行"
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E9%94%AE%E5%80%BC%E5%AF%B9%20key-%3Evalue%0A%20%20%20%20for%20key,%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key,%20%22-%3E%22,%20value%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E9%94%AE%20key%0A%20%20%20%20for%20key%20in%20hmap.keys%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E5%80%BC%20value%0A%20%20%20%20for%20value%20in%20hmap.values%28%29%3A%0A%20%20%20%20%20%20%20%20print%28value%29&cumulative=false&curInstr=8&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
## Простая реализация хеш-таблицы
Рассмотрим самый простой случай, **когда хеш-таблица реализуется с помощью одного массива**. В хеш-таблице каждый пустой слот массива называется <u>корзиной</u>, и каждая корзина может хранить одну пару ключ--значение. Таким образом, операция поиска заключается в нахождении корзины, соответствующей ключу `key`, и получении значения `value` из нее.
Как определить корзину, соответствующую ключу? Это осуществляется с помощью <u>хеш-функции</u>. Хеш-функция предназначена для отображения большого входного пространства в меньшее выходное пространство. В хеш-таблице входное пространство -- это все ключи, а выходное пространство -- это все корзины (индексы массива). Другими словами, **передав ключ в хеш-функцию, можно определить место хранения пары ключ--значение в массиве**.
Процесс вычисления хеш-функции для ключа включает следующие этапы:
1. вычисление хеш-значения с помощью некоторого хеш-алгоритма `hash()`;
2. взятие остатка от деления хеш-значения на количество корзин `capacity` (длину массива) для получения индекса массива `index`, соответствующего ключу `key`.
```shell
index = hash(key) % capacity
```
После этого можно использовать значение `index` для доступа к соответствующей корзине в хеш-таблице и получения значения `value`.
Предположим, что длина массива `capacity = 100`, хеш-алгоритм `hash(key) = key`. Тогда хеш-функция будет иметь вид `key % 100`. На рисунке ниже показан принцип работы хеш-функции на примере ключа «номер» и значения «имя» для студента.
![Принцип работы хеш-функции](../assets/media/image197.jpeg)
В коде ниже реализуется простая хеш-таблица. Здесь `key` и `value` заключены в класс `Pair` для представления пары ключ--значение.
```src
[file]{array_hash_map}-[class]{array_hash_map}-[func]{}
```
## Хеш-коллизии и расширение
По своей сути хеш-функция выполняет отображение входного пространства, состоящего из всех ключей, в выходное пространство, состоящее из всех индексов массива. Причем входное пространство зачастую значительно больше выходного. Следовательно, **теоретически неизбежна ситуация, когда несколько входов соответствуют одному выходу**.
Для хеш-функции в примере выше, если последние две цифры ключа совпадают, результат хеш-функции также совпадает. Например, при запросе студентов с номерами 12836 и 20336 мы получим:
```shell
12836 % 100 = 36
20336 % 100 = 36
```
Оба номера указывают на одно и то же имя, что очевидно неверно. Такую ситуацию, когда несколько входов соответствуют одному выходу, называют <u>хеш-коллизией</u>.
![Пример хеш-коллизии](../assets/media/image199.jpeg)
Логично предположить, что чем больше емкость хеш-таблицы $n$, тем ниже вероятность распределения нескольких ключей в одну корзину и тем меньше коллизий. Поэтому **можно уменьшить количество хеш-коллизий, увеличивая емкость хеш-таблицы**.
Как показано на рисунке ниже, до увеличения емкости пары ключ--значение `(136, A)` и `(236, D)` попадали в одну корзину, а после увеличения емкости коллизия исчезла.
![Увеличение емкости хеш-таблицы](../assets/media/image201.jpeg)
Подобно увеличению емкости массива, увеличение емкости хеш-таблицы требует переноса всех пар ключ--значение из старой хеш-таблицы в новую, что является очень затратной по времени операцией. Кроме того, поскольку емкость хеш-таблицы изменяется, необходимо заново вычислять местоположение хранения всех пар ключ--значение с помощью хеш-функции, что еще больше увеличивает вычислительные затраты процесса расширения. Поэтому в языках программирования обычно резервируется достаточно большая емкость хеш-таблицы, чтобы избежать частого увеличения.
<u>Коэффициент заполнения</u> является важным понятием для хеш-таблицы. Он определяется как количество элементов в хеш-таблице, деленное на количество корзин, и используется для оценки степени серьезности хеш-коллизий, **а также часто служит условием для увеличения емкости хеш-таблицы**. Например, в Java, когда коэффициент заполнения превышает $0.75$, система увеличивает емкость хеш-таблицы в $2$ раза.

View File

@@ -0,0 +1,12 @@
```markdown
# Хеш-таблицы
![Хеш-таблицы](../assets/media/image193.jpeg)
!!! abstract
*Хеш-таблица* реализует эффективный поиск элементов через установление соответствия между ключом key и значением value. Более конкретно, передав ключ в хеш-таблицу, можно получить соответствующее значение за время *O*(1).
Пусть имеется *n* студентов, у каждого из которых есть имя и номер. Если нужно реализовать функцию «ввести номер студента и получить соответствующее имя», то можно использовать хеш-таблицу, как показано на рис. 6.1.
```

View File

@@ -0,0 +1,51 @@
# Резюме
### Ключевые моменты
- При вводе `key` хеш-таблица может найти `value` за время $O(1)$, что очень эффективно.
- К основным операциям с хеш-таблицами относятся поиск, добавление пар ключ-значение, удаление пар ключ-значение и обход хеш-таблицы.
- Хеш-функция отображает `key` в индекс массива, что позволяет получить доступ к соответствующей корзине и получить `value`.
- Два различных `key` могут после применения хеш-функции получить одинаковый индекс массива, что приводит к ошибкам в результатах запросов. Это явление называется хеш-коллизией.
- Чем больше емкость хеш-таблицы, тем ниже вероятность хеш-коллизий. Поэтому можно уменьшить хеш-коллизии путем увеличения емкости хеш-таблицы. Подобно увеличению емкости массива, операция увеличения емкости хеш-таблицы требует больших затрат.
- Коэффициент заполнения определяется как количество элементов в хеш-таблице, деленное на количество корзин, и отражает степень серьезности хеш-коллизий. Часто используется как условие для увеличения емкости хеш-таблицы.
- Цепная адресация преобразует отдельный элемент в связный список, храня все конфликтующие элементы в одном списке. Однако слишком длинные списки снижают эффективность поиска, что можно улучшить путем дальнейшего преобразования списка в красно-черное дерево.
- Открытая адресация обрабатывает хеш-коллизии с помощью многократного зондирования. Линейное зондирование использует фиксированный шаг, но имеет недостаток в том, что элементы нельзя удалять, и легко возникает кластеризация. Множественное хеширование использует несколько хеш-функций для зондирования, что по сравнению с линейным зондированием менее склонно к кластеризации, но несколько хеш-функций увеличивают объем вычислений.
- Различные языки программирования используют разные реализации хеш-таблиц. Например, `HashMap` в Java использует цепную адресацию, а `Dict` в Python использует открытую адресацию.
- В хеш-таблице мы хотим, чтобы хеш-алгоритм обладал свойствами детерминированности, высокой эффективности и равномерного распределения. В криптографии хеш-алгоритм также должен обладать устойчивостью к коллизиям и эффектом лавины.
- Хеш-алгоритмы обычно используют большие простые числа в качестве модуля, чтобы максимально обеспечить равномерное распределение хеш-значений и уменьшить хеш-коллизии.
- К распространенным хеш-алгоритмам относятся MD5, SHA-1, SHA-2 и SHA-3. MD5 часто используется для проверки целостности файлов, SHA-2 часто используется в приложениях и протоколах безопасности.
- Языки программирования обычно предоставляют встроенные хеш-алгоритмы для типов данных, используемые для вычисления индекса корзины в хеш-таблице. Как правило, только неизменяемые объекты являются хешируемыми.
### Вопросы и ответы
**В**: В каких случаях временная сложность хеш-таблицы составляет $O(n)$?
Когда хеш-коллизии достаточно серьезны, временная сложность хеш-таблицы может деградировать до $O(n)$. Когда хеш-функция хорошо спроектирована, емкость установлена разумно и коллизии распределены равномерно, временная сложность составляет $O(1)$. При использовании встроенных хеш-таблиц языков программирования обычно считается, что временная сложность составляет $O(1)$.
**В**: Почему бы не использовать хеш-функцию $f(x) = x$? Тогда не будет коллизий.
При хеш-функции $f(x) = x$ каждый элемент соответствует уникальному индексу корзины, что эквивалентно массиву. Однако входное пространство обычно значительно больше выходного пространства (длины массива), поэтому последний шаг хеш-функции часто заключается в взятии остатка от деления на длину массива. Другими словами, цель хеш-таблицы — отобразить большое пространство состояний в меньшее пространство и обеспечить эффективность поиска $O(1)$.
**В**: Хеш-таблица в основе реализована на массивах, связных списках, бинарных деревьях, но почему её эффективность может быть выше?
Во-первых, временная эффективность хеш-таблицы повышается, но пространственная эффективность снижается. Хеш-таблица имеет значительную часть неиспользуемой памяти.
Во-вторых, временная эффективность повышается только в определенных сценариях использования. Если функциональность может быть реализована с той же временной сложностью с использованием массива или связного списка, то обычно это быстрее, чем хеш-таблица. Это связано с тем, что вычисление хеш-функции требует затрат, и константа временной сложности больше.
Наконец, временная сложность хеш-таблицы может деградировать. Например, при цепной адресации мы выполняем операцию поиска в связном списке или красно-черном дереве, что все еще имеет риск деградации до времени $O(n)$.
**В**: Имеет ли множественное хеширование недостаток невозможности прямого удаления элементов? Может ли пространство, помеченное как удаленное, быть использовано повторно?
Множественное хеширование является одним из видов открытой адресации, и все методы открытой адресации имеют недостаток невозможности прямого удаления элементов, требуя удаления с пометкой. Пространство, помеченное как удаленное, может быть использовано повторно. Когда новый элемент вставляется в хеш-таблицу и хеш-функция находит позицию, помеченную как удаленная, эта позиция может быть использована новым элементом. Это позволяет сохранить последовательность зондирования хеш-таблицы и обеспечить коэффициент использования пространства хеш-таблицы.
**В**: Почему при линейном зондировании возникают хеш-коллизии при поиске элемента?
При поиске с помощью хеш-функции находится соответствующая корзина и пара ключ-значение, и обнаруживается, что `key` не совпадает, что означает наличие хеш-коллизии. Поэтому метод линейного зондирования будет последовательно искать вниз в соответствии с заранее установленным шагом, пока не найдет правильную пару ключ-значение или не сможет найти и не выйдет из поиска.
**В**: Почему увеличение емкости хеш-таблицы может уменьшить хеш-коллизии?
Последний шаг хеш-функции часто заключается в взятии остатка от деления на длину массива $n$ (взятие остатка), чтобы выходное значение попало в диапазон индексов массива; после увеличения емкости длина массива $n$ изменяется, и индекс, соответствующий `key`, также может измениться. Несколько `key`, которые ранее попадали в одну корзину, после увеличения емкости могут быть распределены в несколько корзин, что уменьшает хеш-коллизии.
**В**: Если нужна эффективная запись и чтение, то почему бы не использовать просто массив?
Когда `key` данных представляет собой непрерывные целые числа в небольшом диапазоне, можно использовать массив напрямую — это просто и эффективно. Но когда `key` имеет другой тип (например, строка), необходимо использовать хеш-функцию для отображения `key` в индекс массива, а затем использовать массив корзин для хранения элементов. Такая структура и есть хеш-таблица.