Files
hello-algo/ru/docs/chapter_hashing/hash_algorithm.md
T
2026-01-23 00:59:30 +08:00

411 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Алгоритмы хеширования
В предыдущих разделах были рассмотрены принципы работы хеш-таблиц и методы обработки хеш-конфликтов. Однако ни открытая, ни цепная адресация не могут уменьшить вероятность возникновения хеш-конфликтов, **они лишь обеспечивают корректную работу хеш-таблицы при их возникновении**.
Если хеш-конфликты происходят слишком часто, производительность хеш-таблицы резко снижается. Как показано на рис. 6.8, для хеш-таблицы с цепной адресацией в идеальном случае пары ключ--значение равномерно распределены по всем корзинам, что обеспечивает наилучшую эффективность поиска. В худшем случае все пары ключ--значение хранятся в одной корзине, и временная сложность повышается до $O(n)$.
![Лучший и худший случаи хеш-конфликтов](hash_algorithm.assets/hash_collision_best_worst_condition.png)
**Распределение пар ключ--значение определяется хеш-функцией**. Вспомним этапы вычисления хеш-функции: сначала вычисляется хеш-значение, затем берется остаток от деления на длину массива.
```shell
index = hash(key) % capacity
```
Из этого выражения видно, что при фиксированной емкости хеш-таблицы `capacity` **алгоритм хеширования** `hash()` **определяет выходное значение**, которое, в свою очередь, определяет распределение пар ключ--значение в хеш-таблице.
Это означает, что для снижения вероятности возникновения хеш-конфликтов следует сосредоточиться на разработке алгоритма хеширования `hash()`.
## Цели алгоритма хеширования
Для создания быстрой и надежной структуры данных хеш-таблицы алгоритм хеширования должен обладать следующими характеристиками.
- **Детерминированность**: для одинакового ввода алгоритм хеширования должен всегда давать одинаковый вывод. Это необходимо для обеспечения надежности работы хеш-таблицы.
- **Высокая эффективность**: процесс вычисления хеш-значения должен быть достаточно быстрым. Чем меньше вычислительные затраты, тем выше практическая ценность хеш-таблицы.
- **Равномерное распределение**: алгоритм хеширования должен обеспечивать равномерное распределение пар ключ--значение в хеш-таблице. Чем равномернее распределение, тем ниже вероятность хеш-конфликтов.
На практике алгоритмы хеширования применяются не только для реализации хеш-таблиц, но и в других областях.
- **Хранение паролей**: для защиты паролей пользователей система обычно не хранит пароли в открытом виде, а сохраняет их хеш-значения. Когда пользователь вводит пароль, система вычисляет его хеш-значение и сравнивает с сохраненным. Если они совпадают, пароль считается правильным.
- **Проверка целостности данных**: отправитель данных может вычислить хеш-значение данных и отправить его вместе с данными. Получатель может заново вычислить хеш-значение полученных данных и сравнить его с полученным. Если они совпадают, данные считаются неизмененными.
В криптографических приложениях для предотвращения обратного вычисления исходного пароля из хеш-значения и других видов обратной инженерии алгоритм хеширования должен обладать дополнительными характеристиками.
- **Необратимость**: невозможность извлечь какую-либо информацию о входных данных из хеш-значения.
- **Устойчивость к коллизиям**: должно быть крайне сложно найти два различных входа, дающих одинаковое хеш-значение.
- **Эффект лавины**: небольшие изменения на входе должны приводить к значительным и непредсказуемым изменениям на выходе.
Следует отметить, что **«равномерное распределение» и «устойчивость к коллизиям»** -- это два независимых понятия, и выполнение одного из них не обязательно означает выполнение другого. Например, хеш-функция `key % 100` при случайном вводе значения `key` может давать равномерное распределение. Однако этот алгоритм хеширования слишком прост, и все ключи с одинаковыми последними двумя цифрами будут иметь одинаковый вывод, что позволяет легко извлечь пригодные ключи из хеш-значения и взломать пароль.
## Разработка алгоритма хеширования
Создание хеш-алгоритмов представляет собой сложную задачу, требующую учета множества факторов. Однако для некоторых несложных сценариев можно разработать простые хеш-алгоритмы.
- **Аддитивный хеш**: складываются ASCII-коды каждого символа входных данных, полученная сумма используется в качестве хеш-значения.
- **Мультипликативный хеш**: используя свойство некоррелированности умножения, на каждом шаге значение хеша умножается на константу, и в результат добавляется ASCII-код очередного символа.
- **Хеш с использованием операции XOR**: каждый элемент входных данных накапливается в хеш-значении с помощью операции XOR.
- **Ротационный хеш**: ASCII-коды каждого символа накапливаются в хеш-значении, при этом перед каждым накоплением выполняется операция ротации хеш-значения.
```src
[file]{simple_hash}-[class]{}-[func]{rot_hash}
```
Можно заметить, что последним шагом в каждом из хеш-алгоритмов является взятие остатка от деления на большое простое число $1000000007$, чтобы гарантировать, что хеш-значение находится в допустимом диапазоне. Интересно, почему акцент делается на взятии остатка от деления именно на простое число, и какие недостатки могут быть при делении на составное число?
Ответ: **использование большого простого числа в качестве модуля позволяет обеспечить максимально равномерное распределение хеш-значений**. Поскольку простое число не имеет общих делителей с другими числами, это позволяет уменьшить периодические закономерности, возникающие из-за операции взятия остатка, и избежать хеш-конфликтов.
Например, если выбрать в качестве модуля составное число $9$, которое делится на $3$, то все ключи, делящиеся на $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}
$$
Если входные ключи имеют такую арифметическую прогрессию, то хеш-значения будут сгруппированы, что умножит хеш-конфликты. Теперь если заменить `modulus` на простое число $13$, то, поскольку между ключами и модулем нет общих делителей, равномерность распределения хеш-значений значительно улучшится:
$$
\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}
$$
Следует отметить, что если ключи распределены случайно и равномерно, то выбор простого или составного числа в качестве модуля не имеет значения -- оба варианта обеспечат равномерное распределение хеш-значений. Однако при наличии периодичности в распределении ключей использование составного числа в качестве модуля может привести к кластеризации.
В общем случае выбирается простое число в качестве модуля, и это простое число должно быть достаточно большим, чтобы максимально устранить периодические закономерности и повысить устойчивость хеш-алгоритма.
## Распространенные хеш-алгоритмы
Нетрудно заметить, что описанные выше простые хеш-алгоритмы довольно хрупкие и далеки от достижения целей создания хеш-алгоритмов. Например, сложение и операция XOR удовлетворяют коммутативному закону, поэтому соответствующие хеш-алгоритмы не различают строки с одинаковым содержанием, но разным порядком символов, что может усилить хеш-конфликты и вызвать некоторые проблемы с безопасностью.
На практике обычно используются стандартные хеш-алгоритмы, такие как MD5, SHA-1, SHA-2 и SHA-3. Они могут отображать входные данные произвольной длины в хеш-значения фиксированной длины.
На протяжении почти ста лет хеш-алгоритмы постоянно обновляются и оптимизируются. Одни исследователи стремятся повысить производительность, другие исследователи и хакеры сосредоточены на поиске проблем с безопасностью. В табл. 6.2 представлены распространенные хеш-алгоритмы, используемые в реальных приложениях.
- В MD5 и SHA-1 были обнаружены многочисленные уязвимости, поэтому они не используются в сценариях, в которых требуется высокий уровень безопасности.
- SHA-256 из серии SHA-2 является одним из самых безопасных хеш-алгоритмов, до сих пор не было обнаружено ни одной уязвимости, поэтому он часто используется в различных приложениях и протоколах безопасности.
- SHA-3 имеет меньшие затраты на реализацию и более высокую вычислительную эффективность по сравнению с SHA-2, но в настоящее время его использование не так широко распространено, как серии SHA-2.
<p align="center"> Таблица <id> &nbsp; Распространенные хеш-алгоритмы </p>
| | MD5 | SHA-1 | SHA-2 | SHA-3 |
| -------- | ------------------------------ | ---------------- | ---------------------------- | ------------------- |
| Год появления | 1992 | 1995 | 2002 | 2008 |
| Длина вывода | 128 бит | 160 бит | 256/512 бит | 224/256/384/512 бит |
| Хеш-конфликты | Много | Много | Мало | Мало |
| Уровень безопасности | Низкий, есть известные уязвимости | Низкий, есть известные уязвимости | Высокий | Высокий |
| Применение | Устарел, но еще используется для проверки целостности данных | Устарел | Проверка транзакций в криптовалюте, цифровые подписи и т. д. | Может использоваться в качестве замены SHA-2 |
## Хеш-значения для структур данных
Ключи в хеш-таблице могут быть представлены в виде целых чисел, дробей или строк. Языки программирования обычно предоставляют встроенные хеш-алгоритмы для своих типов данных, чтобы вычислять индексы корзин в хеш-таблице. Например, в Python можно вызвать функцию `hash()` для вычисления хеш-значений для различных типов данных.
- Хеш-значение целых чисел и булевых величин совпадает с их значением.
- Хеш-значение дробных чисел и строк вычисляется по более сложному алгоритму, заинтересованные читатели могут изучить его самостоятельно.
- Хеш-значение кортежа получается путем хеширования каждого элемента и объединения этих хеш-значений в одно.
- Хеш-значение объекта генерируется на основе его адреса в памяти. Путем переопределения метода хеширования объекта можно реализовать генерацию хеша на основе его содержимого.
!!! tip
Обратите внимание, что в разных языках программирования встроенные функции вычисления хеш-значений определяются и реализуются по-разному.
=== "Python"
```python title="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
```
=== "C++"
```cpp title="built_in_hash.cpp"
int num = 3;
size_t hashNum = hash<int>()(num);
// Хеш-значение целого числа 3 равно 3
bool bol = true;
size_t hashBol = hash<bool>()(bol);
// Хеш-значение булевой величины 1 равно 1
double dec = 3.14159;
size_t hashDec = hash<double>()(dec);
// Хеш-значение дробного числа 3.14159 равно 4614256650576692846
string str = "Hello 算法";
size_t hashStr = hash<string>()(str);
// Хеш-значение строки "Hello 算法" равно 15466937326284535026
// В C++ встроенная функция std:hash() предоставляет только вычисление хеш-значений базовых типов данных
// Для массивов и объектов нужно реализовывать вычисление хеш-значений самостоятельно
```
=== "Java"
```java title="built_in_hash.java"
int num = 3;
int hashNum = Integer.hashCode(num);
// Хеш-значение целого числа 3 равно 3
boolean bol = true;
int hashBol = Boolean.hashCode(bol);
// Хеш-значение булевой величины true равно 1231
double dec = 3.14159;
int hashDec = Double.hashCode(dec);
// Хеш-значение дробного числа 3.14159 равно -1340954729
String str = "Hello 算法";
int hashStr = str.hashCode();
// Хеш-значение строки "Hello 算法" равно -727081396
Object[] arr = { 12836, "小哈" };
int hashTup = Arrays.hashCode(arr);
// Хеш-значение массива [12836, 小哈] равно 1151158
ListNode obj = new ListNode(0);
int hashObj = obj.hashCode();
// Хеш-значение объекта узла utils.ListNode@7dc5e7b4 равно 2110121908
```
=== "C#"
```csharp title="built_in_hash.cs"
int num = 3;
int hashNum = num.GetHashCode();
// Хеш-значение целого числа 3 равно 3;
bool bol = true;
int hashBol = bol.GetHashCode();
// Хеш-значение булевой величины true равно 1;
double dec = 3.14159;
int hashDec = dec.GetHashCode();
// Хеш-значение дробного числа 3.14159 равно -1340954729;
string str = "Hello 算法";
int hashStr = str.GetHashCode();
// Хеш-значение строки "Hello 算法" равно -586107568;
object[] arr = [12836, "小哈"];
int hashTup = arr.GetHashCode();
// Хеш-значение массива [12836, 小哈] равно 42931033;
ListNode obj = new(0);
int hashObj = obj.GetHashCode();
// Хеш-значение объекта узла 0 равно 39053774;
```
=== "Go"
```go title="built_in_hash.go"
// Go не предоставляет встроенную функцию hash code
```
=== "Swift"
```swift title="built_in_hash.swift"
let num = 3
let hashNum = num.hashValue
// Хеш-значение целого числа 3 равно 9047044699613009734
let bol = true
let hashBol = bol.hashValue
// Хеш-значение булевой величины true равно -4431640247352757451
let dec = 3.14159
let hashDec = dec.hashValue
// Хеш-значение дробного числа 3.14159 равно -2465384235396674631
let str = "Hello 算法"
let hashStr = str.hashValue
// Хеш-значение строки "Hello 算法" равно -7850626797806988787
let arr = [AnyHashable(12836), AnyHashable("小哈")]
let hashTup = arr.hashValue
// Хеш-значение массива [AnyHashable(12836), AnyHashable("小哈")] равно -2308633508154532996
let obj = ListNode(x: 0)
let hashObj = obj.hashValue
// Хеш-значение объекта узла utils.ListNode равно -2434780518035996159
```
=== "JS"
```javascript title="built_in_hash.js"
// JavaScript не предоставляет встроенную функцию hash code
```
=== "TS"
```typescript title="built_in_hash.ts"
// TypeScript не предоставляет встроенную функцию hash code
```
=== "Dart"
```dart title="built_in_hash.dart"
int num = 3;
int hashNum = num.hashCode;
// Хеш-значение целого числа 3 равно 34803
bool bol = true;
int hashBol = bol.hashCode;
// Хеш-значение булевой величины true равно 1231
double dec = 3.14159;
int hashDec = dec.hashCode;
// Хеш-значение дробного числа 3.14159 равно 2570631074981783
String str = "Hello 算法";
int hashStr = str.hashCode;
// Хеш-значение строки "Hello 算法" равно 468167534
List arr = [12836, "小哈"];
int hashArr = arr.hashCode;
// Хеш-значение массива [12836, 小哈] равно 976512528
ListNode obj = new ListNode(0);
int hashObj = obj.hashCode;
// Хеш-значение объекта узла Instance of 'ListNode' равно 1033450432
```
=== "Rust"
```rust title="built_in_hash.rs"
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let num = 3;
let mut num_hasher = DefaultHasher::new();
num.hash(&mut num_hasher);
let hash_num = num_hasher.finish();
// Хеш-значение целого числа 3 равно 568126464209439262
let bol = true;
let mut bol_hasher = DefaultHasher::new();
bol.hash(&mut bol_hasher);
let hash_bol = bol_hasher.finish();
// Хеш-значение булевой величины true равно 4952851536318644461
let dec: f32 = 3.14159;
let mut dec_hasher = DefaultHasher::new();
dec.to_bits().hash(&mut dec_hasher);
let hash_dec = dec_hasher.finish();
// Хеш-значение дробного числа 3.14159 равно 2566941990314602357
let str = "Hello 算法";
let mut str_hasher = DefaultHasher::new();
str.hash(&mut str_hasher);
let hash_str = str_hasher.finish();
// Хеш-значение строки "Hello 算法" равно 16092673739211250988
let arr = (&12836, &"小哈");
let mut tup_hasher = DefaultHasher::new();
arr.hash(&mut tup_hasher);
let hash_tup = tup_hasher.finish();
// Хеш-значение кортежа (12836, "小哈") равно 1885128010422702749
let node = ListNode::new(42);
let mut hasher = DefaultHasher::new();
node.borrow().val.hash(&mut hasher);
let hash = hasher.finish();
// Хеш-значение объекта узла RefCell { value: ListNode { val: 42, next: None } } равно 15387811073369036852
```
=== "C"
```c title="built_in_hash.c"
// C не предоставляет встроенную функцию hash code
```
=== "Kotlin"
```kotlin title="built_in_hash.kt"
val num = 3
val hashNum = num.hashCode()
// Хеш-значение целого числа 3 равно 3
val bol = true
val hashBol = bol.hashCode()
// Хеш-значение булевой величины true равно 1231
val dec = 3.14159
val hashDec = dec.hashCode()
// Хеш-значение дробного числа 3.14159 равно -1340954729
val str = "Hello 算法"
val hashStr = str.hashCode()
// Хеш-значение строки "Hello 算法" равно -727081396
val arr = arrayOf<Any>(12836, "小哈")
val hashTup = arr.hashCode()
// Хеш-значение массива [12836, 小哈] равно 189568618
val obj = ListNode(0)
val hashObj = obj.hashCode()
// Хеш-значение объекта узла utils.ListNode@1d81eb93 равно 495053715
```
=== "Ruby"
```ruby title="built_in_hash.rb"
num = 3
hash_num = num.hash
# Хеш-значение целого числа 3 равно -4385856518450339636
bol = true
hash_bol = bol.hash
# Хеш-значение булевой величины true равно -1617938112149317027
dec = 3.14159
hash_dec = dec.hash
# Хеш-значение дробного числа 3.14159 равно -1479186995943067893
str = "Hello 算法"
hash_str = str.hash
# Хеш-значение строки "Hello 算法" равно -4075943250025831763
tup = [12836, '小哈']
hash_tup = tup.hash
# Хеш-значение кортежа (12836, '小哈') равно 1999544809202288822
obj = ListNode.new(0)
hash_obj = obj.hash
# Хеш-значение объекта узла #<ListNode:0x000078133140ab70> равно 4302940560806366381
```
??? pythontutor "可视化运行"
https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
Во многих языках программирования **только неизменяемые объекты могут использоваться в качестве ключей в хеш-таблице**. Если список (динамический массив) используется в качестве ключа, то при изменении его содержимого хеш-значение также изменится, и мы не сможем найти исходное значение.
Хотя переменные-члены пользовательских объектов (например, узлов связного списка) могут быть изменяемыми, сами объекты можно хешировать. **Это связано с тем**, **что хеш-значение объекта обычно генерируется на основе его адреса в памяти**, и даже если содержимое объекта изменяется, адрес остается неизменным, а значит, и хеш-значение также остается прежним.
Возможно, вы заметили, что при запуске программы в разных окнах выводимые хеш-значения отличаются. **Это связано с тем, что интерпретатор Python при каждом запуске добавляет случайное значение «соли» к функции хеширования строк**. Такой подход эффективно предотвращает атаки типа HashDoS и повышает безопасность хеш-алгоритма.