mirror of
https://github.com/krahets/hello-algo.git
synced 2026-05-05 10:13:06 +08:00
808 lines
74 KiB
Markdown
808 lines
74 KiB
Markdown
# Хеш-таблицы
|
||
|
||
{width="3.8073009623797027in" height="4.927083333333333in"}
|
||
|
||
#### хеш-таблицы
|
||
|
||
> *Хеш-таблица* реализует эффективный поиск элементов через установле- ние соответствия между ключом key и значением value. Более конкретно, передав ключ в хеш-таблицу, можно получить соответствующее значение за время *O*(1).
|
||
>
|
||
> Пусть имеется *n* студентов, у каждого из которых есть имя и номер. Если нужно реализовать функцию «ввести номер студента и получить соответству- ющее имя», то можно использовать хеш-таблицу, как показано на рис. 6.1.
|
||
|
||

|
||
|
||
> **Рис. 6.1.** Схематичное представление хеш-таблицы
|
||
>
|
||
> Помимо хеш-таблиц, функцией поиска также обладают массивы и связные списки. Сравнение их эффективности приведено в табл. 6.1.
|
||
|
||
- **Добавление элемента**: достаточно добавить элемент в конец массива (списка) за время *O*(1).
|
||
|
||
- **Поиск элемента**: так как массив (список) не упорядочен, необходимо просмотреть все элементы за время *O*(*n*).
|
||
|
||
- **Удаление элемента**: сначала нужно найти элемент, а затем удалить его из массива (списка), понадобится время *O*(*n*).
|
||
|
||
> **Таблица 6.1.** Сравнение эффективности поиска элементов
|
||
|
||
+-----------------------+--------------+----------------------+-------------------+
|
||
| | > **Массив** | > **Связный список** | > **Хеш-таблица** |
|
||
+=======================+:============:+:====================:+:=================:+
|
||
| > Поиск элемента | > *O*(*n*) | > *O*(*n*) | > *O*(1) |
|
||
+-----------------------+--------------+----------------------+-------------------+
|
||
| > Добавление элемента | > *O*(1) | > *O*(1) | > *O*(1) |
|
||
+-----------------------+--------------+----------------------+-------------------+
|
||
| > Удаление элемента | > *O*(*n*) | > *O*(*n*) | > *O*(1) |
|
||
+-----------------------+--------------+----------------------+-------------------+
|
||
|
||
> Мы видим, **что в хеш-таблице операции добавления**, **удаления и поис- ка имеют временную сложность** *O*(1), что очень эффективно.
|
||
|
||
1. **Основные операции с хеш-таблицами**
|
||
|
||
> К основным операциям с хеш-таблицами относятся: инициализация, поиск, добавление и удаление пар ключ--значение. Ниже приведен пример кода.
|
||
>
|
||
> \# === File: 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)
|
||
>
|
||
> Существует три распространенных способа обхода хеш-таблицы: обход пар ключ--значение, обход ключей и обход значений. Ниже приведен пример кода.
|
||
>
|
||
> \# === File: 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)
|
||
|
||
### Простая реализация хеш-таблицы
|
||
|
||
> Рассмотрим самый простой случай, **когда хеш-таблица реализуется с помо- щью одного массива**. В хеш-таблице каждый пустой слот массива называется корзиной, и каждая корзина может хранить одну пару ключ--значение. Таким образом, операция поиска заключается в нахождении корзины, соответствую- щей ключу key, и получении значения value из нее.
|
||
>
|
||
> Как определить корзину, соответствующий ключу? Это осуществляется с по- мощью хеш-функции. Хеш-функция предназначена для отображения большо- го входного пространства в меньшее выходное пространство. В хеш-таблице входное пространство -- это все ключи, а выходное пространство -- это все кор- зины (индексы массива). Другими словами, **передав ключ в хеш-функцию, можно определить место хранения пары ключ--значение в массиве**.
|
||
>
|
||
> Процесс вычисления хеш-функции для ключа включает следующие этапы:
|
||
|
||
1) вычисление хеш-значения с помощью некоторого хеш-алгоритма hash();
|
||
|
||
2) взятие остатка от деления хеш-значения на количество корзин capacity (длину массива) для получения индекса массива index, соответствующе- го ключу key.
|
||
|
||
> index = hash(key) % capacity
|
||
>
|
||
> После этого можно использовать значение index для доступа к соответству- ющей корзине в хеш-таблице и получения значения value.
|
||
>
|
||
> Предположим, что длина массива capacity = 100, хеш-алгоритм hash(key) = key. Тогда хеш-функция будет иметь вид key % 100. На рис. 6.2 показан принцип ра- боты хеш-функции на примере ключа «номер» и значения «имя» для студента.
|
||
>
|
||
>  **Ввод key**
|
||
>
|
||
> **Индекс Массив**
|
||
>
|
||
> **Вывод value**
|
||
>
|
||
> \"Иван\" \"Иван\"
|
||
>
|
||
> \"Петр\"
|
||
|
||
Хеш-функция
|
||
|
||
> \"Яков\" \"Яков\"
|
||
>
|
||
> \"Ира\" \"Ира\"
|
||
>
|
||
> \"Аня\"
|
||
>
|
||
> (Каждая ячейка хранит одну пару ключ--значение)
|
||
>
|
||
> **Рис. 6.2.** Принцип работы хеш-функции
|
||
>
|
||
> В коде ниже реализуется простая хеш-таблица. Здесь key и value заключены в класс Pair для представления пары ключ--значение.
|
||
>
|
||
> \# === File: array_hash_map.py === class Pair:
|
||
>
|
||
> \"\"\" Пара ключ-значение.\"\"\"
|
||
>
|
||
> def init (self, key: int, val: str): self.key = key
|
||
>
|
||
> self.val = val
|
||
>
|
||
> class ArrayHashMap:
|
||
>
|
||
> \"\"\" Хеш-таблица на основе массива.\"\"\"
|
||
>
|
||
> def init (self):
|
||
>
|
||
> \"\"\" Конструктор.\"\"\"
|
||
>
|
||
> \# Инициализация массива, содержащего 100 корзин. self.buckets: list\[Pair \| None\] = \[None\] \* 100
|
||
>
|
||
> def hash_func(self, key: int) -\> int: \"\"\" Хеш-функция.\"\"\"
|
||
>
|
||
> index = key % 100 return index
|
||
>
|
||
> def get(self, key: int) -\> str: \"\"\" Операция поиска.\"\"\"
|
||
>
|
||
> index: int = self.hash_func(key) pair: Pair = self.buckets\[index\] if pair is None:
|
||
>
|
||
> return None return pair.val
|
||
>
|
||
> def put(self, key: int, val: str): \"\"\" Операция добавления.\"\"\" pair = Pair(key, val)
|
||
>
|
||
> index: int = self.hash_func(key) self.buckets\[index\] = pair
|
||
>
|
||
> def remove(self, key: int): \"\"\" Операция удаления.\"\"\"
|
||
>
|
||
> index: int = self.hash_func(key)
|
||
>
|
||
> \# Установка в None означает удаление. self.buckets\[index\] = None
|
||
>
|
||
> def entry_set(self) -\> list\[Pair\]:
|
||
>
|
||
> \"\"\" Получение всех пар ключ-значение.\"\"\" result: list\[Pair\] = \[\]
|
||
>
|
||
> for pair in self.buckets: if pair is not None:
|
||
>
|
||
> result.append(pair) return result
|
||
>
|
||
> def key_set(self) -\> list\[int\]: \"\"\" Получение всех ключей.\"\"\" result = \[\]
|
||
>
|
||
> for pair in self.buckets: if pair is not None:
|
||
>
|
||
> result.append(pair.key) return result
|
||
>
|
||
> def value_set(self) -\> list\[str\]: \"\"\" Получение всех значений.\"\"\" result = \[\]
|
||
>
|
||
> for pair in self.buckets: if pair is not None:
|
||
>
|
||
> result.append(pair.val) return result
|
||
>
|
||
> def print(self):
|
||
>
|
||
> \"\"\" Печать хеш-таблицы.\"\"\" for pair in self.buckets:
|
||
>
|
||
> if pair is not None: print(pair.key, \"-\>\", pair.val)
|
||
|
||
### Хеш-коллизии и расширение
|
||
|
||
> По своей сути хеш-функция выполняет отображение входного пространства, состоящего из всех ключей, в выходное пространство, состоящее из всех ин- дексов массива. Причем входное пространство зачастую значительно больше выходного. Следовательно, **теоретически неизбежна ситуация**, **когда не- сколько входов соответствуют одному выходу**.
|
||
>
|
||
> Для хеш-функции в примере выше, если последние две цифры ключа совпа- дают, результат хеш-функции также совпадает. Например, при запросе студен- тов с номерами 12836 и 20336 мы получим:
|
||
>
|
||
> 12836 % 100 = 36
|
||
>
|
||
> 20336 % 100 = 36.
|
||
>
|
||
> Оба номера указывают на одно и то же имя, что очевидно неверно, см. рис. 6.3. Такую ситуацию, когда несколько входов соответствуют одному вы- ходу, называют хеш-коллизией.
|
||
>
|
||
>  **Ввод key**
|
||
>
|
||
> **Индекс Массив Хеш-коллизия**
|
||
>
|
||
> **Вывод value**
|
||
>
|
||
> \"Иван\"
|
||
|
||
Хеш-функция
|
||
|
||
\"Петр\"
|
||
|
||
\"Яков\"
|
||
|
||
> \"Иван\"
|
||
>
|
||
> \"Иван\"
|
||
>
|
||
> \"Ира\"
|
||
>
|
||
> \"Аня\"
|
||
>
|
||
> (Каждая ячейка хранит одну пару ключ--значение)
|
||
>
|
||
> **Рис. 6.3.** Пример хеш-коллизии
|
||
>
|
||
> Логично предположить, что чем больше емкость хеш-таблицы *n*, тем ниже вероятность распределения нескольких ключей в одну корзину и тем меньше коллизий. Поэтому **можно уменьшить количество хеш-коллизий**, **увели- чивая емкость хеш-таблицы**.
|
||
>
|
||
> Как показано на рис. 6.4, до увеличения емкости пары ключ--значение (136, A)
|
||
|
||
и (236, D) попадали в одну корзину, а после увеличения емкости коллизия исчезла.
|
||
|
||
> **Увеличение хеш-таблицы**
|
||
>
|
||
> **Рис. 6.4.** Увеличение емкости хеш-таблицы
|
||
>
|
||
> Подобно увеличению емкости массива, увеличение емкости хеш-таблицы требует переноса всех пар ключ--значение из старой хеш-таблицы в новую, что является очень затратной по времени операцией. Кроме того, поскольку емкость хеш-таблицы изменяется, необходимо заново вычислять местополо- жение хранения всех пар ключ--значение с помощью хеш-функции, что еще больше увеличивает вычислительные затраты процесса расширения. Поэтому в языках программирования обычно резервируется достаточно большая ем- кость хеш-таблицы, чтобы избежать частого увеличения.
|
||
>
|
||
> Коэффициент заполнения является важным понятием для хеш-таблицы. Он определяется как количество элементов в хеш-таблице, деленное на коли- чество корзин, и используется для оценки степени серьезности хеш-коллизий, **а также часто служит условием для увеличения емкости хеш-таблицы**. Например, в Java, когда коэффициент заполнения превышает 0.75, система увеличивает емкость хеш-таблицы в 2 раза.
|
||
|
||
#### хеш-коллизии
|
||
|
||
> Как упоминалось в предыдущем разделе, **в обычных условиях входное про- странство хеш-функции значительно больше выходного пространства**,
|
||
>
|
||
> поэтому хеш-коллизии теоретически неизбежны. Например, если входное пространство состоит из всех целых чисел, а выходное пространство соответ- ствует размеру массива, то обязательно несколько целых чисел будут отобра- жаться в один и тот же индекс корзины.
|
||
>
|
||
> Хеш-коллизии могут привести к ошибкам в результатах запросов, серьезно влияя на работоспособность хеш-таблицы. Чтобы решить эту проблему, при возникновении хеш-коллизий выполняется увеличение емкости хеш-таблицы до тех пор, пока коллизии не исчезнут. Этот метод понятен и прост в реализа- ции, но крайне неэффективен, поскольку увеличение емкости хеш-таблицы требует значительных затрат на перенос данных и вычисление хеш-значений. Для повышения эффективности можно использовать следующие стратегии:
|
||
|
||
1) улучшение структуры данных хеш-таблицы, **чтобы она могла нор- мально функционировать при возникновении хеш-коллизий**;
|
||
|
||
2) выполнение увеличения емкости только при необходимости, т. е. когда хеш-коллизии становятся достаточно серьезными.
|
||
|
||
> Основные методы улучшения структуры хеш-таблицы включают цепную адресацию и открытую адресацию.
|
||
|
||
### Цепная адресация
|
||
|
||
> В исходной хеш-таблице каждая корзина может хранить только одну пару ключ--значение. Цепная адресация преобразует отдельный элемент в связный список, где пары ключ--значение выступают в качестве узлов списка, и все пары ключ--значение, вызвавшие коллизии, хранятся в одном и том же списке. На рис. 6.5 представлен пример хеш-таблицы с цепной адресацией.
|
||
|
||

|
||
|
||
> **Рис. 6.5.** Хеш-таблица с цепной адресацией
|
||
>
|
||
> Методы работы с хеш-таблицей, реализованной на основе цепной адреса- ции, изменяются следующим образом.
|
||
|
||
- **Поиск элемента**: вводится ключ, с помощью хеш-функции определяет- ся индекс корзины, после чего осуществляется доступ к головному узлу списка. Затем выполняется обход списка и сравнение ключей для поиска целевой пары ключ--значение.
|
||
|
||
- **Добавление элемента**: сначала с помощью хеш-функции осуществля- ется доступ к головному узлу списка, затем узел (пара ключ--значение) добавляется в список.
|
||
|
||
- **Удаление элемента**: на основе результата хеш-функции осуществляется доступ к головному узлу списка, затем выполняется обход списка для по- иска целевого узла и его удаления.
|
||
|
||
> Цепная адресация имеет следующие ограничения.
|
||
|
||
- **Увеличение занимаемого пространства**: связный список содержит указатели на узлы, что требует больше памяти по сравнению с массивом.
|
||
|
||
- **Снижение эффективности поиска**: необходимо линейно обходить связный список для нахождения соответствующего элемента.
|
||
|
||
> Ниже приведен простой пример реализации хеш-таблицы с цепной адреса- цией. Следует обратить внимание на следующие моменты.
|
||
|
||
- Использование списка (динамического массива) вместо связного списка для упрощения кода. В данной конфигурации хеш-таблица (массив) со- держит несколько корзин, каждая из которых является списком.
|
||
|
||
- В данной реализации предусмотрен метод расширения хеш-таблицы. Когда коэффициент заполнения превышает 2/3, хеш-таблица расширя- ется в 2 раза.
|
||
|
||
> \# === File: hash_map_chaining.py === class HashMapChaining:
|
||
>
|
||
> \"\"\" Хеш-таблица с цепной адресацией.\"\"\"
|
||
>
|
||
> def init (self):
|
||
>
|
||
> \"\"\" Конструктор.\"\"\"
|
||
>
|
||
> self.size = 0 \# Количество пар ключ-значение. self.capacity = 4 \# Вместимость хеш-таблицы.
|
||
>
|
||
> self.load_thres = 2.0 / 3.0 \# Порог коэффициента заполнения для расши-
|
||
>
|
||
> рения.
|
||
>
|
||
> self.extend_ratio = 2 \# Коэффициент расширения.
|
||
>
|
||
> self.buckets = \[\[\] for \_ in range(self.capacity)\] \# Массив корзин.
|
||
>
|
||
> def hash_func(self, key: int) -\> int: \"\"\" Хеш-функция.\"\"\"
|
||
>
|
||
> return key % self.capacity
|
||
>
|
||
> def load_factor(self) -\> float:
|
||
>
|
||
> \"\"\" Коэффициент заполнения.\"\"\" return self.size / self.capacity
|
||
>
|
||
> def get(self, key: int) -\> str \| None: \"\"\" Операция поиска.\"\"\"
|
||
>
|
||
> index = self.hash_func(key) bucket = self.buckets\[index\]
|
||
>
|
||
> \# Обход корзины, если ключ найден, возвращается соответствующее значение. for pair in bucket:
|
||
>
|
||
> if pair.key == key: return pair.val
|
||
>
|
||
> \# Если ключ не найден, возвращается None. return None
|
||
>
|
||
> def put(self, key: int, val: str): \"\"\" Операция добавления.\"\"\"
|
||
>
|
||
> \# При превышении коэффициента заполнения выполняется расширение. if self.load_factor() \> self.load_thres:
|
||
>
|
||
> self.extend()
|
||
>
|
||
> index = self.hash_func(key) bucket = self.buckets\[index\]
|
||
>
|
||
> \# Обход корзины; если ключ найден, значение обновляется \# и выполняется выход из функции.
|
||
>
|
||
> for pair in bucket:
|
||
>
|
||
> if pair.key == key: pair.val = val return
|
||
>
|
||
> \# Если ключ отсутствует, пара ключ-значение добавляется в конец. pair = Pair(key, val)
|
||
>
|
||
> bucket.append(pair) self.size += 1
|
||
>
|
||
> def remove(self, key: int): \"\"\" Операция удаления.\"\"\"
|
||
>
|
||
> index = self.hash_func(key) bucket = self.buckets\[index\]
|
||
>
|
||
> \# Обход корзины, удаление пары ключ-значение. for pair in bucket:
|
||
>
|
||
> if pair.key == key: bucket.remove(pair) self.size -= 1 break
|
||
>
|
||
> def extend(self):
|
||
>
|
||
> \"\"\" Расширение хеш-таблицы.\"\"\"
|
||
>
|
||
> \# Сохранение исходной хеш-таблицы. buckets = self.buckets
|
||
>
|
||
> \# Инициализация новой расширенной хеш-таблицы. self.capacity \*= self.extend_ratio
|
||
>
|
||
> self.buckets = \[\[\] for \_ in range(self.capacity)\] self.size = 0
|
||
>
|
||
> \# Перенос пар ключ-значение из исходной хеш-таблицы в новую. for bucket in buckets:
|
||
>
|
||
> for pair in bucket: self.put(pair.key, pair.val)
|
||
>
|
||
> def print(self):
|
||
>
|
||
> \"\"\" Печать хеш-таблицы.\"\"\" for bucket in self.buckets:
|
||
>
|
||
> res = \[\]
|
||
>
|
||
> for pair in bucket:
|
||
>
|
||
> res.append(str(pair.key) + \" -\> \" + pair.val) print(res)
|
||
>
|
||
> Стоит отметить, что в длинных списках эффективность поиска *O*(*n*) весьма низка. В этом случае **можно преобразовать список в АВЛ-дерево или крас- но-черное дерево**, чтобы оптимизировать временную сложность операции поиска до *O*(log *n*).
|
||
|
||
### Открытая адресация
|
||
|
||
> Открытая адресация не вводит дополнительные структуры данных, а исполь- зует многократное пробирование для обработки хеш-конфликтов. Основные методы пробирования включают линейное пробирование, квадратичное про- бирование и двойное хеширование.
|
||
>
|
||
> Далее на примере линейного зондирования рассмотрим механизм работы хеш-таблицы с открытой адресацией.
|
||
|
||
##### Линейное зондирование
|
||
|
||
> При линейном зондировании используется линейный поиск с фиксирован- ным шагом. Метод зондирования отличается от обычной хеш-таблицы.
|
||
|
||
- **Вставка элемента**: с помощью хеш-функции вычисляется индекс кор- зины. Если корзина уже занята, начинается линейный обход от места конфликта (обычно с шагом 1) до нахождения пустой корзины, куда и вставляется элемент.
|
||
|
||
- **Поиск элемента**: при обнаружении хеш-конфликта используется тот же шаг для линейного обхода до нахождения соответствующего элемента, после чего возвращается его значение. Если встречается пустая корзина, это озна- чает, что целевой элемент отсутствует в хеш-таблице, возвращается None.
|
||
|
||
> На рис. 6.6 демонстрируется распределение пар ключ--значение в хеш- таблице с открытой адресацией (линейное зондирование). Согласно этой хеш- функции ключи с одинаковыми последними двумя цифрами отображаются в одну корзину. Однако благодаря линейному зондированию они последова- тельно размещаются в этой и следующих корзинах.
|
||
>
|
||
> Однако линейное зондирование **подвержено так называемой класте- ризации**. Чем длиннее последовательность занятых позиций в массиве, тем выше вероятность хеш-конфликта в этих позициях. Это способствует дальней- шему росту кластера, создавая порочный круг и в конечном итоге снижая эф- фективность операций добавления, удаления и поиска.
|
||
>
|
||
> Стоит отметить, что **в хеш-таблице с открытой адресацией нельзя на- прямую удалять элементы**. Это связано с тем, что удаление элемента соз- дает в массиве пустую корзину None. И затем при поиске элемента линейное зондирование, достигнув этой пустой корзины, закончит поиск, что приведет к невозможности доступа к элементам ниже этой корзины. Программа может ошибочно посчитать, что эти элементы отсутствуют, как показано на рис. 6.7.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 6.6.** Распределение пар ключ--значение в хеш-таблице с открытой адресацией (линейное зондирование)
|
||
|
||

|
||
|
||
> **Рис. 6.7.** Проблемы поиска, вызванные удалением элементов при открытой адресации
|
||
>
|
||
> Для решения этой проблемы можно использовать механизм ленивого удаления: элементы не удаляются из хеш-таблицы напрямую, **вместо это- го для пометки этой корзины используется константа** TOMBSTONE. При та- ком подходе None и TOMBSTONE обозначают пустую корзину, в которую можно поместить пару ключ--значение. Однако при линейном зондировании зна- чение TOMBSTONE не завершает обход, так как ниже все еще могут находиться пары ключ--значение.
|
||
>
|
||
> Однако **ленивое удаление может ускорить падение производительно- сти хеш-таблицы**. Это происходит потому, что каждая операция удаления оставляет метку удаления, и с увеличением количества значений TOMBSTONE время поиска также увеличивается, поскольку при линейном зондировании может потребоваться пропуск нескольких TOMBSTONE.
|
||
>
|
||
> В качестве решения предлагается при линейном зондировании фиксиро- вать индекс первого встретившегося TOMBSTONE и менять местами найденный целевой элемент с этим TOMBSTONE. Преимущество такого подхода заключает- ся в том, что при каждом запросе или добавлении элемента он перемещается в корзину, находящуюся ближе к идеальному положению (начальной точке по- иска), что оптимизирует эффективность запросов.
|
||
>
|
||
> В следующем коде реализована хеш-таблица с открытой адресацией (ли- нейным зондированием), включающей ленивое удаление. Для более полного использования пространства хеш-таблицы она рассматривается как кольце- вой массив, и при переходе за конец массива происходит возврат к началу.
|
||
>
|
||
> \# === File: hash_map_open_addressing.py === class HashMapOpenAddressing:
|
||
>
|
||
> \"\"\" Открытая адресация хеш-таблицы.\"\"\"
|
||
>
|
||
> def init (self):
|
||
>
|
||
> \"\"\" Конструктор.\"\"\"
|
||
>
|
||
> self.size = 0 \# Количество пар ключ-значение. self.capacity = 4 \# Вместимость хеш-таблицы.
|
||
>
|
||
> self.load_thres = 2.0 / 3.0 \# Порог коэффициента заполнения для расширения. self.extend_ratio = 2 \# Коэффициент расширения.
|
||
>
|
||
> self.buckets: list\[Pair \| None\] = \[None\] \* self.capacity \# Массив корзин. self.TOMBSTONE = Pair(-1, \"-1\") \# Метка удаления.
|
||
>
|
||
> def hash_func(self, key: int) -\> int: \"\"\" Хеш-функция.\"\"\"
|
||
>
|
||
> return key % self.capacity
|
||
>
|
||
> def load_factor(self) -\> float:
|
||
>
|
||
> \"\"\" Коэффициент заполнения.\"\"\" return self.size / self.capacity
|
||
>
|
||
> def find_bucket(self, key: int) -\> int:
|
||
>
|
||
> \"\"\" Поиск индекса корзины по ключу.\"\"\" index = self.hash_func(key) first_tombstone = -1
|
||
>
|
||
> \# Линейное зондирование, выход при встрече пустой корзины. while self.buckets\[index\] is not None:
|
||
>
|
||
> \# Если найден ключ, возвращается соответствующий индекс корзины. if self.buckets\[index\].key == key:
|
||
>
|
||
> \# Если ранее была метка удаления, перемещение пары
|
||
>
|
||
> \# ключ-значение в этот индекс. if first_tombstone != -1:
|
||
>
|
||
> self.buckets\[first_tombstone\] = self.buckets\[index\] self.buckets\[index\] = self.TOMBSTONE
|
||
>
|
||
> return first_tombstone \# Возврат индекса перемещенной корзины. return index \# Возврат индекса корзины.
|
||
>
|
||
> \# Фиксация первого встретившегося удаления.
|
||
>
|
||
> if first_tombstone == -1 and self.buckets\[index\] is self.TOMBSTONE: first_tombstone = index
|
||
>
|
||
> \# Вычисление индекса корзины, при переходе за конец
|
||
>
|
||
> \# возвращение к началу.
|
||
>
|
||
> index = (index + 1) % self.capacity
|
||
>
|
||
> \# Если ключ не существует, возвращается индекс для добавления. return index if first_tombstone == -1 else first_tombstone
|
||
>
|
||
> def get(self, key: int) -\> str: \"\"\" Операция запроса.\"\"\"
|
||
>
|
||
> \# Поиск индекса корзины по ключу. index = self.find_bucket(key)
|
||
>
|
||
> \# Если найдена пара ключ-значение, возвращается соответствующее значение. if self.buckets\[index\] not in \[None, self.TOMBSTONE\]:
|
||
>
|
||
> return self.buckets\[index\].val
|
||
>
|
||
> \# Если пара ключ-значение не существует, возвращается None. return None
|
||
>
|
||
> def put(self, key: int, val: str): \"\"\" Операция добавления.\"\"\"
|
||
>
|
||
> \# При превышении порога коэффициента заполнения выполняется расширение. if self.load_factor() \> self.load_thres:
|
||
>
|
||
> self.extend()
|
||
>
|
||
> \# Поиск индекса корзины по ключу. index = self.find_bucket(key)
|
||
>
|
||
> \# Если найдена пара ключ-значение, значение перезаписывается и возвращается. if self.buckets\[index\] not in \[None, self.TOMBSTONE\]:
|
||
>
|
||
> self.buckets\[index\].val = val return
|
||
>
|
||
> \# Если пара ключ-значение не существует, добавляется новая пара. self.buckets\[index\] = Pair(key, val)
|
||
>
|
||
> self.size += 1
|
||
>
|
||
> def remove(self, key: int): \"\"\" Операция удаления.\"\"\"
|
||
>
|
||
> \# Поиск индекса корзины по ключу. index = self.find_bucket(key)
|
||
>
|
||
> \# Если найдена пара ключ-значение, она заменяется меткой удаления. if self.buckets\[index\] not in \[None, self.TOMBSTONE\]:
|
||
>
|
||
> self.buckets\[index\] = self.TOMBSTONE self.size -= 1
|
||
>
|
||
> def extend(self):
|
||
>
|
||
> \"\"\" Расширение хеш-таблицы.\"\"\"
|
||
>
|
||
> \# Временное сохранение оригинальной хеш-таблицы. buckets_tmp = self.buckets
|
||
>
|
||
> \# Инициализация новой хеш-таблицы после расширения. self.capacity \*= self.extend_ratio
|
||
>
|
||
> self.buckets = \[None\] \* self.capacity self.size = 0
|
||
>
|
||
> \# Перенос пар ключ-значение из оригинальной хеш-таблицы в новую. for pair in buckets_tmp:
|
||
>
|
||
> if pair not in \[None, self.TOMBSTONE\]: self.put(pair.key, pair.val)
|
||
>
|
||
> def print(self):
|
||
>
|
||
> \"\"\" Печать хеш-таблицы.\"\"\" for pair in self.buckets:
|
||
>
|
||
> if pair is None: print(\"None\")
|
||
>
|
||
> elif pair is self.TOMBSTONE: print(\"TOMBSTONE\")
|
||
>
|
||
> else:
|
||
>
|
||
> print(pair.key, \"-\>\", pair.val)
|
||
|
||
##### Квадратичное зондирование
|
||
|
||
> Квадратичное зондирование, как и линейное, является одной из распростра- ненных стратегий открытой адресации. При возникновении конфликта ква- дратичное зондирование пропускает не просто фиксированное количество шагов, а количество шагов, равное квадрату числа попыток, т. е. 1, 4, 9, \...
|
||
>
|
||
> Квадратичное зондирование обладает следующими преимуществами:
|
||
|
||
- квадратичное зондирование, пропуская расстояние, равное квадрату числа попыток, стремится сгладить эффект кластеризации линейного зондирования;
|
||
|
||
- квадратичное зондирование пропускает большее расстояние в поисках пустого места, что способствует более равномерному распределению данных.
|
||
|
||
> Тем не менее квадратичный поиск не является идеальным и обладает сле- дующими недостатками:
|
||
|
||
- все еще существует эффект кластеризации, т. е. некоторые позиции за- нимаются более вероятно, чем другие;
|
||
|
||
- из-за быстрого роста квадрата квадратичное зондирование может не ох- ватить всю хеш-таблицу. То есть даже при наличии в хеш-таблице пустых корзин квадратичное зондирование может никогда не добраться до них.
|
||
|
||
##### Множественное хеширование
|
||
|
||
> Как следует из названия, метод множественного хеширования использует для поиска несколько хеш-функций *f*1(*x*), *f*2(*x*), *f*3(*x*), \...
|
||
|
||
- **Вставка элемента**: если хеш-функция *f*1(*x*) вызывает конфликт, вычис- ляется *f*2(*x*) и т. д., пока не будет найдено пустое место для вставки эле- мента.
|
||
|
||
- **Поиск элемента**: поиск выполняется в том же порядке хеш-функций, пока не будет найден целевой элемент. Если встречается пустое место или были испробованы все хеш-функции, это означает, что элемента в хеш-таблице нет, и возвращается None.
|
||
|
||
> По сравнению с линейным зондированием метод множественного хеши- рования менее склонен к кластеризации, но использование нескольких хеш- функций увеличивает вычислительную нагрузку.
|
||
|
||
### Выбор языка программирования
|
||
|
||
> Различные языки программирования используют разные стратегии реализа- ции хеш-таблиц, ниже приведено несколько примеров.
|
||
|
||
- В Python используется метод открытой адресации. В словарях dict для поиска применяется псевдослучайное число.
|
||
|
||
- В Java используется цепная адресация. Начиная с JDK 1.8, когда длина мас- сива в HashMap достигает 64, а длина цепочки достигает 8, цепочка преоб- разуется в красно-черное дерево для повышения эффективности поиска.
|
||
|
||
- В Go используется цепная адресация. Здесь предусмотрено, что в каждой корзине может храниться не более 8 пар ключ--значение. При превыше- нии емкости подключается дополнительная корзина. При избыточном количестве дополнительных корзин выполняется специальная опера- ция расширения для поддержания производительности.
|
||
|
||
#### Алгоритмы хеширования
|
||
|
||
> В предыдущих разделах были рассмотрены принципы работы хеш-таблиц и методы обработки хеш-конфликтов. Однако ни открытая, ни цепная адре- сация не могут уменьшить вероятность возникновения хеш-конфликтов, **они лишь обеспечивают корректную работу хеш-таблицы при их воз- никновении**.
|
||
>
|
||
> Если хеш-конфликты происходят слишком часто, производительность хеш- таблицы резко снижается. Как показано на рис. 6.8, для хеш-таблицы с цепной адресацией в идеальном случае пары ключ--значение равномерно распреде- лены по всем корзинам, что обеспечивает наилучшую эффективность поиска. В худшем случае все пары ключ--значение хранятся в одной корзине, и вре- менная сложность повышается до *O*(*n*).
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 6.8.** Лучший и худший случаи хеш-конфликтов
|
||
>
|
||
> **Распределение пар ключ--значение определяется хеш-функцией**. Вспомним этапы вычисления хеш-функции: сначала вычисляется хеш- значение, затем берется остаток от деления на длину массива.
|
||
>
|
||
> index = hash(key) % capacity
|
||
>
|
||
> Из этого выражения видно, что при фиксированной емкости хеш-таблицы capacity **алгоритм хеширования** hash() **определяет выходное значение**, которое, в свою очередь, определяет распределение пар ключ--значение в хеш-таблице.
|
||
>
|
||
> Это означает, что для снижения вероятности возникновения хеш-конфликтов следует сосредоточиться на разработке алгоритма хеширования hash().
|
||
|
||
### Цели алгоритма хеширования
|
||
|
||
> Для создания быстрой и надежной структуры данных хеш-таблицы алгоритм хеширования должен обладать следующими характеристиками.
|
||
|
||
- **Детерминированность**: для одинакового ввода алгоритм хеширования должен всегда давать одинаковый вывод. Это необходимо для обеспече- ния надежности работы хеш-таблицы.
|
||
|
||
- **Высокая эффективность**: процесс вычисления хеш-значения должен быть достаточно быстрым. Чем меньше вычислительные затраты, тем выше практическая ценность хеш-таблицы.
|
||
|
||
- **Равномерное распределение**: алгоритм хеширования должен обе- спечивать равномерное распределение пар ключ--значение в хеш- таблице. Чем равномернее распределение, тем ниже вероятность хеш- конфликтов.
|
||
|
||
> На практике алгоритмы хеширования применяются не только для реализа- ции хеш-таблиц, но и в других областях.
|
||
|
||
- **Хранение паролей**: для защиты паролей пользователей система обыч- но не хранит пароли в открытом виде, а сохраняет их хеш-значения. Когда пользователь вводит пароль, система вычисляет его хеш-значение и сравнивает с сохраненным. Если они совпадают, пароль считается правильным.
|
||
|
||
- **Проверка целостности данных**: отправитель данных может вычис- лить хеш-значение данных и отправить его вместе с данными. По- лучатель может заново вычислить хеш-значение полученных данных и сравнить его с полученным. Если они совпадают, данные считаются неизмененными.
|
||
|
||
> В криптографических приложениях для предотвращения обратного вычис- ления исходного пароля из хеш-значения и других видов обратной инженерии алгоритм хеширования должен обладать дополнительными характеристиками.
|
||
|
||
- **Необратимость**: невозможность извлечь какую-либо информацию о входных данных из хеш-значения.
|
||
|
||
- **Устойчивость к коллизиям**: должно быть крайне сложно найти два раз- личных входа, дающих одинаковое хеш-значение.
|
||
|
||
- **Эффект лавины**: небольшие изменения на входе должны приводить к значительным и непредсказуемым изменениям на выходе.
|
||
|
||
> Следует отметить, что **«равномерное распределение» и «устойчивость к коллизиям»** -- это два независимых понятия, и выполнение одного из них не обязательно означает выполнение другого. Например, хеш-функция key % 100 при случайном вводе значения key может давать равномерное распределение. Однако этот алгоритм хеширования слишком прост, и все ключи с одинаковыми последними двумя цифрами будут иметь одинако- вый вывод, что позволяет легко извлечь пригодные ключи из хеш-значения и взломать пароль.
|
||
|
||
### Разработка алгоритма хеширования
|
||
|
||
> Создание хеш-алгоритмов представляет собой сложную задачу, требующую учета множества факторов. Однако для некоторых несложных сценариев мож- но разработать простые хеш-алгоритмы.
|
||
|
||
- **Аддитивный хеш**: складываются ASCII-коды каждого символа входных данных, полученная сумма используется в качестве хеш-значения.
|
||
|
||
- **Мультипликативный хеш**: используя свойство некоррелированности умножения, на каждом шаге значение хеша умножается на константу, и в результат добавляется ASCII-код очередного символа.
|
||
|
||
- **Хеш с использованием операции XOR**: каждый элемент входных дан- ных накапливается в хеш-значении с помощью операции XOR.
|
||
|
||
- **Ротационный хеш**: ASCII-коды каждого символа накапливаются в хеш- значении, при этом перед каждым накоплением выполняется операция ротации хеш-значения.
|
||
|
||
> \# === File: simple_hash.py ===
|
||
>
|
||
> def add_hash(key: str) -\> int: \"\"\" Аддитивный хеш.\"\"\" hash = 0
|
||
>
|
||
> modulus = 1000000007 for c in key:
|
||
>
|
||
> hash += ord(c) return hash % modulus
|
||
>
|
||
> def mul_hash(key: str) -\> int: \"\"\" Мультипликативный хеш.\"\"\" hash = 0
|
||
>
|
||
> modulus = 1000000007 for c in key:
|
||
>
|
||
> hash = 31 \* hash + ord(c) return hash % modulus
|
||
>
|
||
> def xor_hash(key: str) -\> int:
|
||
>
|
||
> \"\"\" Хеш с использованием операции XOR.\"\"\" hash = 0
|
||
>
|
||
> modulus = 1000000007 for c in key:
|
||
>
|
||
> hash \^= ord(c) return hash % modulus
|
||
>
|
||
> def rot_hash(key: str) -\> int: \"\"\" Ротационный хеш.\"\"\" hash = 0
|
||
>
|
||
> modulus = 1000000007 for c in key:
|
||
>
|
||
> hash = (hash \<\< 4) \^ (hash \>\> 28) \^ ord(c) return hash % modulus
|
||
>
|
||
> Можно заметить, что последним шагом в каждом из хеш-алгоритмов явля- ется взятие остатка от деления на большое простое число 1000000007, чтобы гарантировать, что хеш-значение находится в допустимом диапазоне. Инте- ресно, почему акцент делается на взятии остатка от деления именно на про- стое число, и какие недостатки могут быть при делении на составное число?
|
||
>
|
||
> Ответ: **использование большого простого числа в качестве модуля позволяет обеспечить максимально равномерное распределение хеш- значений**. Поскольку простое число не имеет общих делителей с другими числами, это позволяет уменьшить периодические закономерности, возника- ющие из-за операции взятия остатка, и избежать хеш-конфликтов.
|
||
>
|
||
> Например, если выбрать в качестве модуля составное число 9, кото- рое делится на 3, то все ключи, делящиеся на 3, будут отображаться в хеш- значения 0, 3 и 6:
|
||
>
|
||
> modulus = 9
|
||
>
|
||
> key = {0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \... }
|
||
>
|
||
> hash = {0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6, \... }.
|
||
>
|
||
> Если входные ключи имеют такую арифметическую прогрессию, то хеш- значения будут сгруппированы, что умножит хеш-конфликты. Теперь если за- менить modulus на простое число 13, то, поскольку между ключами и модулем нет общих делителей, равномерность распределения хеш-значений значи- тельно улучшится:
|
||
>
|
||
> modulus = 13
|
||
>
|
||
> key = {0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \... }
|
||
>
|
||
> hash = {0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \... }.
|
||
>
|
||
> Следует отметить, что если ключи распределены случайно и равномерно, то выбор простого или составного числа в качестве модуля не имеет значения -- оба варианта обеспечат равномерное распределение хеш-значений. Однако при наличии периодичности в распределении ключей использование состав- ного числа в качестве модуля может привести к кластеризации.
|
||
>
|
||
> В общем случае выбирается простое число в качестве модуля, и это простое число должно быть достаточно большим, чтобы максимально устранить пери- одические закономерности и повысить устойчивость хеш-алгоритма.
|
||
|
||
### Распространенные хеш-алгоритмы
|
||
|
||
> Нетрудно заметить, что описанные выше простые хеш-алгоритмы довольно хрупкие и далеки от достижения целей создания хеш-алгоритмов. Например, сложение и операция XOR удовлетворяют коммутативному закону, поэтому соответствующие хеш-алгоритмы не различают строки с одинаковым содер- жанием, но разным порядком символов, что может усилить хеш-конфликты и вызвать некоторые проблемы с безопасностью.
|
||
>
|
||
> На практике обычно используются стандартные хеш-алгоритмы, такие как MD5, SHA-1, SHA-2 и SHA-3. Они могут отображать входные данные произ- вольной длины в хеш-значения фиксированной длины.
|
||
>
|
||
> На протяжении почти ста лет хеш-алгоритмы постоянно обновляются и оп- тимизируются. Одни исследователи стремятся повысить производительность, другие исследователи и хакеры сосредоточены на поиске проблем с безопас- ностью. В табл. 6.2 представлены распространенные хеш-алгоритмы, исполь- зуемые в реальных приложениях.
|
||
|
||
- В MD5 и SHA-1 были обнаружены многочисленные уязвимости, поэтому они не используются в сценариях, в которых требуется высокий уровень безопасности.
|
||
|
||
- SHA-256 из серии SHA-2 является одним из самых безопасных хеш- алгоритмов, до сих пор не было обнаружено ни одной уязвимости, по- этому он часто используется в различных приложениях и протоколах безопасности.
|
||
|
||
<!-- -->
|
||
|
||
- SHA-3 имеет меньшие затраты на реализацию и более высокую вычис- лительную эффективность по сравнению с SHA-2, но в настоящее время его использование не так широко распространено, как серии SHA-2.
|
||
|
||
+--------------------------+------------------------------------------------------------------+----------------+-----------------------+--------------------------------------------------+
|
||
| | > **MD5** | > **SHA-1** | > **SHA-2** | > **SHA-3** |
|
||
+==========================+==================================================================+================+=======================+==================================================+
|
||
| > Год появления | > 1992 | > 1995 | > 2002 | > 2008 |
|
||
+--------------------------+------------------------------------------------------------------+----------------+-----------------------+--------------------------------------------------+
|
||
| > Длина вывода | > 128 бит | > 160 бит | > 256/512 бит | > 224/256/384/512 бит |
|
||
+--------------------------+------------------------------------------------------------------+----------------+-----------------------+--------------------------------------------------+
|
||
| > Хеш-конфликты | > Много | > Много | > Мало | > Мало |
|
||
+--------------------------+------------------------------------------------------------------+----------------+-----------------------+--------------------------------------------------+
|
||
| > Уровень без- опасности | > Низкий, есть из- вестные уязви- мости | > Низкий, есть | > Высокий | > Высокий |
|
||
| | | > | | |
|
||
| | | > уязвимости | | |
|
||
+--------------------------+------------------------------------------------------------------+----------------+-----------------------+--------------------------------------------------+
|
||
| > Применение | > Устарел, но еще используется для проверки целост- ности данных | > Устарел | > Проверка транзакций | > Может использовать- ся в качестве замены SHA-2 |
|
||
| | | | > | |
|
||
| | | | > в криптовалюте, | |
|
||
| | | | > | |
|
||
| | | | > подписи и т. д. | |
|
||
+--------------------------+------------------------------------------------------------------+----------------+-----------------------+--------------------------------------------------+
|
||
|
||
> известные
|
||
>
|
||
> цифровые
|
||
|
||
### Хеш-значения для структур данных
|
||
|
||
> Ключи в хеш-таблице могут быть представлены в виде целых чисел, дробей или строк. Языки программирования обычно предоставляют встроенные хеш- алгоритмы для своих типов данных, чтобы вычислять индексы корзин в хеш- таблице. Например, в Python можно вызвать функцию hash() для вычисления хеш-значений для различных типов данных.
|
||
|
||
- Хеш-значение целых чисел и булевых величин совпадает с их значением.
|
||
|
||
- Хеш-значение дробных чисел и строк вычисляется по более сложному ал- горитму, заинтересованные читатели могут изучить его самостоятельно.
|
||
|
||
- Хеш-значение кортежа получается путем хеширования каждого элемен- та и объединения этих хеш-значений в одно.
|
||
|
||
- Хеш-значение объекта генерируется на основе его адреса в памяти. Пу- тем переопределения метода хеширования объекта можно реализовать генерацию хеша на основе его содержимого.
|
||
|
||
> \# === File: built_in_hash.py ===
|
||
>
|
||
> num = 3
|
||
>
|
||
> hash_num = hash(num)
|
||
>
|
||
> \# Хеш-значение целого числа 3 равно 3.
|
||
>
|
||
> bol = True
|
||
>
|
||
> hash_bol = hash(bol)
|
||
>
|
||
> \# Хеш-значение булевой величины True равно 1.
|
||
>
|
||
> dec = 3.14159
|
||
>
|
||
> hash_dec = hash(dec)
|
||
>
|
||
> \# Хеш-значение дробного числа 3.14159 равно 326484311674566659.
|
||
>
|
||
> str = \"Hello 算法\"
|
||
>
|
||
> hash_str = hash(str)
|
||
>
|
||
> \# Хеш-значение строки \"Hello 算法\" равно 4617003410720528961.
|
||
>
|
||
> tup = (12836, \" 小哈\") hash_tup = hash(tup)
|
||
>
|
||
> \# Хеш-значение кортежа (12836, \'小哈\') равно 1029005403108185979.
|
||
>
|
||
> obj = ListNode(0) hash_obj = hash(obj)
|
||
>
|
||
> \# Хеш-значение объекта \<ListNode object at 0x1058fd810\> равно 274267521.
|
||
>
|
||
> Во многих языках программирования **только неизменяемые объекты могут использоваться в качестве ключей в хеш-таблице**. Если список (ди- намический массив) используется в качестве ключа, то при изменении его со- держимого хеш-значение также изменится, и мы не сможем найти исходное значение.
|
||
>
|
||
> Хотя переменные-члены пользовательских объектов (например, узлов связного списка) могут быть изменяемыми, сами объекты можно хеширо- вать. **Это связано с тем**, **что хеш-значение объекта обычно генериру- ется на основе его адреса в памяти**, и даже если содержимое объекта из- меняется, адрес остается неизменным, а значит, и хеш-значение также оста- ется прежним.
|
||
>
|
||
> Возможно, вы заметили, что при запуске программы в разных окнах выво- димые хеш-значения отличаются. **Это связано с тем, что интерпретатор Py- thon при каждом запуске добавляет случайное значение «соли» к функ- ции хеширования строк**. Такой подход эффективно предотвращает атаки типа HashDoS и повышает безопасность хеш-алгоритма.
|
||
|
||
#### резюме
|
||
|
||
##### Ключевые моменты
|
||
|
||
- При вводе ключа key хеш-таблица может найти значение value за время
|
||
|
||
> *O*(1), что очень эффективно.
|
||
|
||
- Основные операции с хеш-таблицами включают поиск, добавление пар ключ--значение, удаление пар и обход хеш-таблицы.
|
||
|
||
- Хеш-функция отображает ключ в индекс массива, что позволяет полу- чить доступ к соответствующей корзине и извлечь значение.
|
||
|
||
- Два разных ключа могут после хеширования получить одинаковый ин- декс массива, что приводит к ошибке в результате поиска. Это явление называется хеш-конфликтом.
|
||
|
||
- Чем больше емкость хеш-таблицы, тем ниже вероятность хеш- конфликтов. Поэтому расширение хеш-таблицы может уменьшить ко- личество конфликтов, но, как и в случае с массивами, расширение хеш- таблицы требует значительных затрат.
|
||
|
||
- Коэффициент заполнения определяется как отношение числа элемен- тов в хеш-таблице к числу корзин и отражает степень серьезности хеш- конфликтов. Он часто используется как условие для расширения хеш- таблицы.
|
||
|
||
- Метод цепной адресации преобразует отдельные элементы в связный спи- сок, храня все конфликтующие элементы в одном списке. Однако слишком длинные списки снижают эффективность поиска, поэтому для повышения эффективности можно преобразовать списки в красно-черные деревья.
|
||
|
||
- Метод открытой адресации решает хеш-конфликты с помощью многократ- ного поиска (зондирования). Линейное зондирование использует фикси- рованный шаг, но его элементы нельзя удалять, и он склонен к кластери- зации. Множественное хеширование использует несколько хеш-функций для поиска, что снижает вероятность кластеризации по сравнению с ли- нейным зондированием, но увеличивает вычислительную нагрузку.
|
||
|
||
- Разные языки программирования реализуют хеш-таблицы по-разному. Например, класс 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*). **Вопрос**. Есть ли у метода множественного хеширования недостаток невоз- можности прямого удаления элементов? Можно ли повторно использовать
|
||
>
|
||
> пространство, помеченное как удаленное?
|
||
>
|
||
> **Ответ**. Множественное хеширование является одной из форм открытой адресации, и все методы открытой адресации имеют недостаток невозможно- сти прямого удаления элементов, что требует пометки удаленных элементов. Пространство, помеченное как удаленное, можно использовать повторно. Ког- да новый элемент вставляется в хеш-таблицу и хеш-функция находит пози- цию, помеченную как удаленная, эту позиция можно использовать для нового элемента. Это позволяет сохранить последовательность поиска в хеш-таблице неизменной и обеспечить эффективность использования пространства.
|
||
>
|
||
> **Вопрос**. Почему в линейном зондировании при поиске элемента возникают хеш-коллизии?
|
||
>
|
||
> **Ответ**. При поиске с помощью хеш-функции находится соответствующая корзина и пара ключ--значение. Если ключ не совпадает, это означает наличие хеш-коллизии. Поэтому метод линейного зондирования будет последователь- но искать с заранее заданным шагом, пока не найдет правильное значение ключа или не достигнет условия конца поиска.
|
||
>
|
||
> **Вопрос**. Почему расширение хеш-таблицы может уменьшить количество хеш-коллизий?
|
||
>
|
||
> **Ответ**. Последним шагом хеш-функции часто является взятие остатка от деления на длину массива *n*, чтобы выходное значение попадало в диапазон индексов массива. После расширения длина массива *n* изменяется, и соответ- ствующий индекс ключа также может измениться. Несколько ключей, которые ранее попадали в одну корзину, после расширения могут распределиться по нескольким корзинам, что снижает количество хеш-коллизий.
|
||
>
|
||
> Глава 7
|