Add ru version (#1865)
* Add Russian docs site baseline * Add Russian localized codebase * Polish Russian code wording * Update ru code translation. * Update code translation and chapter covers. * Fix pythontutor extraction. * Add README and landing page. * placeholder of profiles * Use figures of English version * Remove chapter paperbook
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,410 @@
|
||||
# Алгоритмы хеширования
|
||||
|
||||
В двух предыдущих разделах мы рассмотрели принципы работы хеш-таблицы и способы обработки хеш-коллизий. Однако и открытая адресация, и метод цепочек **лишь позволяют хеш-таблице корректно работать при возникновении коллизий, но не уменьшают вероятность появления самих коллизий**.
|
||||
|
||||
Если хеш-коллизии происходят слишком часто, производительность хеш-таблицы резко деградирует. Как показано на рисунке ниже, для хеш-таблицы с методом цепочек в идеальном случае пары ключ-значение равномерно распределены по всем бакетам, и это дает наилучшую эффективность поиска; в худшем же случае все пары ключ-значение оказываются в одном бакете, и временная сложность вырождается до $O(n)$ .
|
||||
|
||||

|
||||
|
||||
**Распределение пар ключ-значение определяется хеш-функцией**. Вспомним этапы вычисления хеш-функции: сначала вычисляется хеш-значение, затем оно берется по модулю длины массива:
|
||||
|
||||
```shell
|
||||
index = hash(key) % capacity
|
||||
```
|
||||
|
||||
Из этой формулы видно: при фиксированной емкости хеш-таблицы `capacity` **выходное значение определяет именно хеш-алгоритм `hash()` **, а значит, именно он определяет распределение пар ключ-значение в хеш-таблице.
|
||||
|
||||
Это означает, что для уменьшения вероятности хеш-коллизий нам следует сосредоточиться на проектировании хеш-алгоритма `hash()` .
|
||||
|
||||
## Цели хеш-алгоритма
|
||||
|
||||
Чтобы получить структуру данных хеш-таблицы, которая будет одновременно "быстрой и надежной", хеш-алгоритм должен обладать следующими свойствами.
|
||||
|
||||
- **Детерминированность**: для одинакового входа хеш-алгоритм всегда должен выдавать одинаковый результат. Только так хеш-таблица остается надежной.
|
||||
- **Высокая эффективность**: вычисление хеш-значения должно быть достаточно быстрым. Чем меньше вычислительные затраты, тем выше практическая ценность хеш-таблицы.
|
||||
- **Равномерное распределение**: хеш-алгоритм должен стараться распределять пары ключ-значение в хеш-таблице равномерно. Чем равномернее распределение, тем ниже вероятность хеш-коллизий.
|
||||
|
||||
На практике хеш-алгоритмы используются не только для реализации хеш-таблиц, но и во многих других областях.
|
||||
|
||||
- **Хранение паролей**: чтобы защищать пароли пользователей, система обычно хранит не сами пароли в открытом виде, а их хеш-значения. Когда пользователь вводит пароль, система вычисляет хеш-значение введенного пароля и сравнивает его с сохраненным значением. Если они совпадают, пароль считается правильным.
|
||||
- **Проверка целостности данных**: отправитель может вычислить хеш-значение данных и отправить его вместе с самими данными; получатель затем вычисляет хеш-значение повторно и сравнивает его с полученным. Если они совпадают, данные считаются целостными.
|
||||
|
||||
Для приложений, связанных с криптографией, чтобы не допустить восстановления исходного пароля по хеш-значению и иных форм обратного анализа, хеш-алгоритм должен обладать более строгими свойствами безопасности.
|
||||
|
||||
- **Односторонность**: по хеш-значению нельзя восстановить какую-либо информацию о входных данных.
|
||||
- **Устойчивость к коллизиям**: должно быть крайне трудно найти два разных входа, имеющих одинаковое хеш-значение.
|
||||
- **Эффект лавины**: даже небольшое изменение во входных данных должно приводить к заметному и непредсказуемому изменению результата.
|
||||
|
||||
Обрати внимание: **"равномерное распределение" и "устойчивость к коллизиям" - это два независимых понятия** , и выполнение первого не означает автоматического выполнения второго. Например, при случайном распределении входных `key` хеш-функция `key % 100` может выдавать достаточно равномерное распределение. Однако этот хеш-алгоритм слишком прост: все `key` с одинаковыми двумя последними цифрами будут иметь одинаковый результат, а значит, по хеш-значению можно легко подобрать подходящие `key` и, например, взломать пароль.
|
||||
|
||||
## Проектирование хеш-алгоритма
|
||||
|
||||
Разработка хеш-алгоритма - это сложная задача, в которой нужно учитывать множество факторов. Однако для некоторых нетребовательных сценариев мы можем спроектировать и несколько простых хеш-алгоритмов.
|
||||
|
||||
- **Аддитивный хеш**: складываем ASCII-коды всех символов входной строки и используем полученную сумму как хеш-значение.
|
||||
- **Мультипликативный хеш**: используем "некоррелированность" умножения; на каждом шаге умножаем текущее значение на константу и добавляем ASCII-код очередного символа.
|
||||
- **XOR-хеш**: последовательно накапливаем элементы входных данных в одном хеш-значении через операцию XOR.
|
||||
- **Ротационный хеш**: последовательно накапливаем ASCII-коды символов, причем перед каждым накоплением выполняем циклический сдвиг хеш-значения.
|
||||
|
||||
```src
|
||||
[file]{simple_hash}-[class]{}-[func]{rot_hash}
|
||||
```
|
||||
|
||||
Нетрудно заметить, что последний шаг каждого из этих хеш-алгоритмов - взятие по модулю большого простого числа $1000000007$ , чтобы гарантировать, что хеш-значение остается в разумных границах. Стоит задуматься: почему подчеркивается именно взятие по модулю простого числа, и какие недостатки возникают при использовании составного модуля? Это интересный вопрос.
|
||||
|
||||
Сначала дадим вывод: **использование большого простого числа в качестве модуля позволяет в максимальной степени обеспечивать равномерное распределение хеш-значений**. Поскольку простое число не имеет общих делителей с другими числами, это помогает уменьшить периодические закономерности, возникающие из-за операции взятия остатка, и тем самым снизить число хеш-коллизий.
|
||||
|
||||
Рассмотрим пример. Предположим, мы выбрали составное число $9$ в качестве модуля. Оно делится на $3$ , поэтому все `key` , которые делятся на $3$ , будут отображаться только в три хеш-значения: $0$ , $3$ , $6$ .
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\text{modulus} & = 9 \newline
|
||||
\text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline
|
||||
\text{hash} & = \{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\dots \}
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
Если входные `key` как раз удовлетворяют такому распределению в виде арифметической прогрессии, то хеш-значения начнут скучиваться, а это усугубит хеш-коллизии. Теперь предположим, что мы заменили `modulus` на простое число $13$ ; поскольку между `key` и `modulus` нет общих делителей, равномерность распределения хеш-значений заметно улучшится.
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\text{modulus} & = 13 \newline
|
||||
\text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline
|
||||
\text{hash} & = \{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \dots \}
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
Следует отметить: если можно гарантировать, что `key` распределены случайно и равномерно, то выбор простого или составного числа в качестве модуля не так важен - оба варианта способны дать равномерное распределение хеш-значений. Но если в распределении `key` присутствует периодичность, то взятие по модулю составного числа гораздо легче приводит к кластеризации.
|
||||
|
||||
Итак, на практике мы обычно выбираем простое число в качестве модуля, причем это простое число желательно брать достаточно большим, чтобы по возможности убрать периодические закономерности и повысить устойчивость хеш-алгоритма.
|
||||
|
||||
## Распространенные хеш-алгоритмы
|
||||
|
||||
Нетрудно заметить, что описанные выше простые хеш-алгоритмы довольно "хрупкие" и далеки от поставленных целей. Например, сложение и XOR подчиняются коммутативному закону, поэтому аддитивный хеш и XOR-хеш не различают строки, состоящие из одних и тех же символов, но в разном порядке. Это может усиливать хеш-коллизии и даже создавать некоторые проблемы безопасности.
|
||||
|
||||
На практике мы обычно используем стандартные хеш-алгоритмы, такие как MD5, SHA-1, SHA-2 и SHA-3. Они могут отображать входные данные произвольной длины в хеш-значения фиксированной длины.
|
||||
|
||||
На протяжении почти ста лет хеш-алгоритмы непрерывно развивались и оптимизировались. Одни исследователи старались повысить их производительность, а другие исследователи и хакеры сосредоточивались на поиске уязвимостей в их безопасности. В таблице ниже приведены распространенные хеш-алгоритмы, которые часто встречаются в реальных приложениях.
|
||||
|
||||
- MD5 и SHA-1 уже многократно были успешно атакованы, поэтому они выведены из большинства сценариев, где требуется безопасность.
|
||||
- SHA-256 из семейства SHA-2 является одним из самых надежных хеш-алгоритмов; на сегодняшний день не известно успешных практических атак, поэтому он широко используется в самых разных протоколах и системах безопасности.
|
||||
- SHA-3 по сравнению с SHA-2 требует меньших затрат на реализацию и обеспечивает более высокую вычислительную эффективность, но на данный момент распространен слабее, чем семейство SHA-2.
|
||||
|
||||
<p align="center"> Таблица <id> Распространенные хеш-алгоритмы </p>
|
||||
|
||||
| | MD5 | SHA-1 | SHA-2 | SHA-3 |
|
||||
| -------- | ------------------------------ | ---------------- | ---------------------------- | ------------------- |
|
||||
| Год появления | 1992 | 1995 | 2002 | 2008 |
|
||||
| Длина вывода | 128 bit | 160 bit | 256/512 bit | 224/256/384/512 bit |
|
||||
| Хеш-коллизии | Частые | Частые | Редкие | Редкие |
|
||||
| Уровень безопасности | Низкий, успешно атакован | Низкий, успешно атакован | Высокий | Высокий |
|
||||
| Применение | Устарел, но еще используется для проверки целостности данных | Устарел | Проверка криптовалютных транзакций, цифровые подписи и т. д. | Может использоваться как замена SHA-2 |
|
||||
|
||||
## Хеш-значения структур данных
|
||||
|
||||
Мы знаем, что `key` в хеш-таблице могут быть целыми числами, вещественными числами, строками и другими типами данных. Языки программирования обычно предоставляют встроенные хеш-алгоритмы для этих типов, чтобы вычислять индексы бакетов в хеш-таблице. Возьмем 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 Algo"
|
||||
hash_str = hash(str)
|
||||
# Хеш-значение строки "Hello Algo" равно 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 Algo";
|
||||
size_t hashStr = hash<string>()(str);
|
||||
// Хеш-значение строки "Hello Algo" равно 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 Algo";
|
||||
int hashStr = str.hashCode();
|
||||
// Хеш-значение строки "Hello Algo" равно -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 Algo";
|
||||
int hashStr = str.GetHashCode();
|
||||
// Хеш-значение строки "Hello Algo" равно -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 Algo"
|
||||
let hashStr = str.hashValue
|
||||
// Хеш-значение строки "Hello Algo" равно -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 Algo";
|
||||
int hashStr = str.hashCode;
|
||||
// Хеш-значение строки "Hello Algo" равно 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 Algo";
|
||||
let mut str_hasher = DefaultHasher::new();
|
||||
str.hash(&mut str_hasher);
|
||||
let hash_str = str_hasher.finish();
|
||||
// Хеш-значение строки "Hello Algo" равно 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 Algo"
|
||||
val hashStr = str.hashCode()
|
||||
// Хеш-значение строки "Hello Algo" равно -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 Algo"
|
||||
hash_str = str.hash
|
||||
# Хеш-значение строки "Hello Algo" равно -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%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D1%83%D0%B7%D0%B5%D0%BB%D0%BA%D0%BB%D0%B0%D1%81%D1%81%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D1%81%D0%BB%D0%B5%D0%B4%D1%83%D1%8E%D1%89%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%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%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%86%D0%B5%D0%BB%D0%BE%D0%B3%D0%BE%20%D1%87%D0%B8%D1%81%D0%BB%D0%B0%203%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%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%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%B1%D1%83%D0%BB%D0%B5%D0%B2%D0%B0%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20True%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%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%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%87%D0%B8%D1%81%D0%BB%D0%B0%203.14159%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20Algo%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%81%D1%82%D1%80%D0%BE%D0%BA%D0%B8%20%22Hello%20Algo%22%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836%2C%20%22%D0%A1%D1%8F%D0%BE%20%D0%A5%D0%B0%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BA%D0%BE%D1%80%D1%82%D0%B5%D0%B6%D0%B0%20%2812836%2C%20%27%D0%A1%D1%8F%D0%BE%20%D0%A5%D0%B0%27%29%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%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%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%B0%20%D1%83%D0%B7%D0%BB%D0%B0%20%3CListNode%20object%20at%200x1058fd810%3E%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
Во многих языках программирования **в качестве `key` хеш-таблицы можно использовать только неизменяемые объекты** . Если, например, использовать список (динамический массив) как `key` , то после изменения содержимого списка изменится и его хеш-значение, из-за чего мы уже не сможем найти прежнее `value` в хеш-таблице.
|
||||
|
||||
Хотя у пользовательских объектов (например, у узла связного списка) поля являются изменяемыми, сам объект все же может быть хешируемым. **Причина в том, что хеш-значение объекта обычно строится на основе адреса в памяти** : даже если содержимое объекта меняется, его адрес памяти остается прежним, а значит, и хеш-значение не меняется.
|
||||
|
||||
Внимательный читатель мог заметить, что при запуске программы в разных консолях выводимые хеш-значения отличаются. **Это связано с тем, что интерпретатор Python при каждом запуске добавляет в хеш-функцию строк случайную соль (salt)**. Такой подход эффективно защищает от атак типа HashDoS и повышает безопасность хеш-алгоритма.
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,108 @@
|
||||
# Хеш-коллизии
|
||||
|
||||
Как уже говорилось в предыдущем разделе, **в обычных условиях входное пространство хеш-функции намного больше выходного пространства** , поэтому теоретически хеш-коллизии неизбежны. Например, если входное пространство состоит из всех целых чисел, а выходное пространство ограничено размером массива, то неизбежно несколько целых чисел будут отображаться в один и тот же индекс бакета.
|
||||
|
||||
Хеш-коллизии приводят к ошибочным результатам поиска и серьезно влияют на пригодность хеш-таблицы к использованию. Чтобы решить эту проблему, можно при каждом конфликте выполнять расширение хеш-таблицы, пока конфликт не исчезнет. Этот метод прост и груб, но слишком неэффективен, потому что расширение хеш-таблицы требует большого объема переноса данных и вычислений хеш-значений. Чтобы повысить эффективность, можно использовать следующие стратегии.
|
||||
|
||||
1. Улучшить структуру данных хеш-таблицы, **чтобы она могла корректно работать даже при возникновении хеш-коллизий**.
|
||||
2. Выполнять расширение только тогда, когда это действительно необходимо, то есть когда хеш-коллизии становятся достаточно серьезными.
|
||||
|
||||
Основные способы улучшения структуры хеш-таблицы включают "метод цепочек" и "открытую адресацию".
|
||||
|
||||
## Метод цепочек
|
||||
|
||||
В исходной хеш-таблице каждый бакет может хранить только одну пару ключ-значение. <u>Метод цепочек (separate chaining)</u> превращает отдельный элемент в связный список: пары ключ-значение становятся узлами списка, и все конфликтующие пары ключ-значение хранятся в одном и том же списке. На рисунке ниже показан пример хеш-таблицы, реализованной методом цепочек.
|
||||
|
||||

|
||||
|
||||
Методы работы с хеш-таблицей, построенной на основе метода цепочек, меняются следующим образом.
|
||||
|
||||
- **Поиск элемента**: передаем `key` , по хеш-функции получаем индекс бакета, после чего обращаемся к голове списка и обходим список, сравнивая `key` , пока не найдем целевую пару ключ-значение.
|
||||
- **Добавление элемента**: сначала через хеш-функцию получаем голову списка, затем добавляем узел (пару ключ-значение) в этот список.
|
||||
- **Удаление элемента**: по результату хеш-функции обращаемся к голове списка, затем обходим список, находим целевой узел и удаляем его.
|
||||
|
||||
Метод цепочек имеет следующие ограничения.
|
||||
|
||||
- **Рост потребления памяти**: связный список содержит указатели на узлы, поэтому по сравнению с массивом он требует больше памяти.
|
||||
- **Снижение эффективности поиска**: для нахождения нужного элемента нужно линейно обходить связный список.
|
||||
|
||||
Ниже приведена простая реализация хеш-таблицы методом цепочек. Следует обратить внимание на два момента.
|
||||
|
||||
- Для упрощения кода вместо связного списка используется список (динамический массив). В этой реализации хеш-таблица (массив) содержит несколько бакетов, и каждый бакет представляет собой список.
|
||||
- Ниже включен метод расширения хеш-таблицы. Когда коэффициент загрузки превышает $\frac{2}{3}$ , мы расширяем хеш-таблицу до $2$ раз от прежней емкости.
|
||||
|
||||
```src
|
||||
[file]{hash_map_chaining}-[class]{hash_map_chaining}-[func]{}
|
||||
```
|
||||
|
||||
Следует отметить, что когда связный список становится очень длинным, эффективность поиска $O(n)$ оказывается низкой. **В этом случае список можно преобразовать в "AVL-дерево" или "красно-черное дерево"** , чтобы оптимизировать временную сложность поиска до $O(\log n)$ .
|
||||
|
||||
## Открытая адресация
|
||||
|
||||
<u>Открытая адресация (open addressing)</u> не вводит дополнительных структур данных, а обрабатывает хеш-коллизии с помощью "многократного пробирования"; основные варианты пробирования включают линейное пробирование, квадратичное пробирование и повторное хеширование.
|
||||
|
||||
Ниже на примере линейного пробирования рассмотрим механизм работы хеш-таблицы с открытой адресацией.
|
||||
|
||||
### Линейное пробирование
|
||||
|
||||
Линейное пробирование использует линейный поиск с фиксированным шагом. Его методы работы отличаются от обычной хеш-таблицы.
|
||||
|
||||
- **Вставка элемента**: по хеш-функции вычисляется индекс бакета; если бакет уже занят, то от места конфликта выполняется линейный обход вперед (шаг обычно равен $1$ ), пока не будет найден пустой бакет, после чего элемент вставляется туда.
|
||||
- **Поиск элемента**: если возник конфликт, то с тем же шагом продолжается линейный обход вперед, пока не будет найден целевой элемент и возвращено `value` ; если встречается пустой бакет, это означает, что искомого элемента в хеш-таблице нет, и возвращается `None` .
|
||||
|
||||
На рисунке ниже показано распределение пар ключ-значение в хеш-таблице с открытой адресацией (линейное пробирование). Для этой хеш-функции все `key` с одинаковыми двумя последними цифрами отображаются в один и тот же бакет. Благодаря линейному пробированию они по очереди сохраняются в этом бакете и в следующих за ним бакетах.
|
||||
|
||||

|
||||
|
||||
Однако **линейное пробирование легко приводит к "кластеризации"**. Иначе говоря, чем длиннее непрерывная занятая область в массиве, тем выше вероятность новых коллизий в этой области, что еще сильнее способствует росту этой группы и в итоге ухудшает эффективность операций добавления, удаления, поиска и обновления.
|
||||
|
||||
Стоит заметить, что **мы не можем напрямую удалять элементы из хеш-таблицы с открытой адресацией**. Причина в том, что удаление создаст внутри массива пустой бакет `None` , а при поиске элемента линейное пробирование остановится на этом пустом бакете и вернет результат, из-за чего элементы ниже этого бакета уже не смогут быть найдены, и программа может ошибочно посчитать, что их не существует, как показано на рисунке ниже.
|
||||
|
||||

|
||||
|
||||
Чтобы решить эту проблему, можно использовать механизм <u>ленивого удаления (lazy deletion)</u>: он не удаляет элемент из хеш-таблицы напрямую, **а помечает этот бакет специальной константой `TOMBSTONE` **. В этом механизме и `None` , и `TOMBSTONE` означают пустой бакет, и оба могут быть использованы для размещения пары ключ-значение. Но есть важное различие: при линейном пробировании, встретив `TOMBSTONE` , нужно продолжать обход, потому что ниже него все еще могут существовать пары ключ-значение.
|
||||
|
||||
Однако **ленивое удаление может ускорять деградацию производительности хеш-таблицы**. Это связано с тем, что каждая операция удаления создает новую метку удаления; по мере роста числа `TOMBSTONE` время поиска тоже увеличивается, потому что линейное пробирование может быть вынуждено перескакивать через множество `TOMBSTONE` , прежде чем найдет целевой элемент.
|
||||
|
||||
Поэтому имеет смысл при линейном пробировании запоминать индекс первого встреченного `TOMBSTONE` и затем менять найденный целевой элемент местами с этим `TOMBSTONE` . Преимущество такого подхода в том, что при каждом поиске или добавлении элемент будет перемещаться в бакет, расположенный ближе к его идеальной позиции (начальной точке пробирования), а значит, эффективность поиска улучшится.
|
||||
|
||||
Ниже приведена реализация хеш-таблицы с открытой адресацией (линейное пробирование), включающая ленивое удаление. Чтобы пространство хеш-таблицы использовалось более полно, мы рассматриваем ее как "кольцевой массив": когда обход выходит за конец массива, он возвращается к началу и продолжается.
|
||||
|
||||
```src
|
||||
[file]{hash_map_open_addressing}-[class]{hash_map_open_addressing}-[func]{}
|
||||
```
|
||||
|
||||
### Квадратичное пробирование
|
||||
|
||||
Квадратичное пробирование похоже на линейное пробирование и тоже является одной из распространенных стратегий открытой адресации. При возникновении конфликта оно не пропускает фиксированное число шагов, а переходит на расстояние, равное "квадрату числа попыток", то есть на $1, 4, 9, \dots$ шагов.
|
||||
|
||||
Квадратичное пробирование имеет следующие основные преимущества.
|
||||
|
||||
- Квадратичное пробирование пытается смягчить эффект кластеризации линейного пробирования, так как пропускает расстояния, равные квадрату номера попытки.
|
||||
- Квадратичное пробирование перепрыгивает на более дальние позиции в поисках свободного места, что помогает распределять данные более равномерно.
|
||||
|
||||
Однако квадратичное пробирование не является идеальным.
|
||||
|
||||
- Кластеризация все равно существует: некоторые позиции по-прежнему занимают чаще других.
|
||||
- Из-за быстрого роста квадрата квадратичное пробирование может не охватить всю хеш-таблицу, а это означает, что даже при наличии пустых бакетов оно может так до них и не добраться.
|
||||
|
||||
### Повторное хеширование
|
||||
|
||||
Как видно из названия, метод повторного хеширования использует для пробирования несколько хеш-функций $f_1(x)$, $f_2(x)$, $f_3(x)$, $\dots$ .
|
||||
|
||||
- **Вставка элемента**: если хеш-функция $f_1(x)$ вызывает конфликт, то пробуем $f_2(x)$ , и так далее, пока не будет найдено пустое место для вставки элемента.
|
||||
- **Поиск элемента**: поиск идет в том же порядке хеш-функций, пока не будет найден целевой элемент; если встречается пустая позиция или уже были опробованы все хеш-функции, это означает, что элемента в хеш-таблице нет, и возвращается `None` .
|
||||
|
||||
По сравнению с линейным пробированием метод повторного хеширования меньше подвержен кластеризации, но несколько хеш-функций приносят дополнительные вычислительные затраты.
|
||||
|
||||
!!! tip
|
||||
|
||||
Обрати внимание: у хеш-таблиц с открытой адресацией (линейное пробирование, квадратичное пробирование и повторное хеширование) есть общая проблема: в них нельзя напрямую удалять элементы.
|
||||
|
||||
## Выбор в языках программирования
|
||||
|
||||
Разные языки программирования используют разные стратегии реализации хеш-таблиц. Ниже приведено несколько примеров.
|
||||
|
||||
- Python использует открытую адресацию. В словаре `dict` для пробирования применяются псевдослучайные числа.
|
||||
- Java использует метод цепочек. Начиная с JDK 1.8, когда длина массива внутри `HashMap` достигает 64, а длина списка достигает 8, этот список преобразуется в красно-черное дерево для повышения производительности поиска.
|
||||
- Go использует метод цепочек. В Go установлено, что каждый бакет может хранить не более 8 пар ключ-значение; при переполнении подключается overflow-bucket, а когда таких bucket становится слишком много, выполняется специальное расширение того же масштаба, чтобы сохранить производительность.
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,591 @@
|
||||
# Хеш-таблица
|
||||
|
||||
<u>Хеш-таблица (hash table)</u>, также называемая <u>таблицей рассеяния</u>, обеспечивает эффективный поиск элементов за счет отображения между ключом `key` и значением `value` . Иначе говоря, если передать в хеш-таблицу ключ `key` , то можно за $O(1)$ времени получить соответствующее значение `value` .
|
||||
|
||||
Как показано на рисунке ниже, пусть есть $n$ студентов, и у каждого из них есть два поля данных: "имя" и "номер студенческого билета". Если мы хотим реализовать запрос вида "ввести номер студенческого билета и вернуть соответствующее имя", то для этого можно использовать показанную ниже хеш-таблицу.
|
||||
|
||||

|
||||
|
||||
Помимо хеш-таблицы, функции поиска можно реализовать и через массив, и через связный список. Сравнение их эффективности приведено в таблице ниже.
|
||||
|
||||
- **Добавление элемента**: нужно лишь добавить элемент в конец массива (или списка), что занимает $O(1)$ времени.
|
||||
- **Поиск элемента**: так как массив (или список) неупорядочен, приходится обходить все элементы, что занимает $O(n)$ времени.
|
||||
- **Удаление элемента**: сначала нужно найти элемент, затем удалить его из массива (или списка), что занимает $O(n)$ времени.
|
||||
|
||||
<p align="center"> Таблица <id> Сравнение эффективности поиска элементов </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)
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="hash_map.cpp"
|
||||
/* Инициализация хеш-таблицы */
|
||||
unordered_map<int, string> map;
|
||||
|
||||
/* Операция добавления */
|
||||
// Добавить пару ключ-значение (key, value) в хеш-таблицу
|
||||
map[12836] = "Сяо Ха";
|
||||
map[15937] = "Сяо Ло";
|
||||
map[16750] = "Сяо Суань";
|
||||
map[13276] = "Сяо Фа";
|
||||
map[10583] = "Сяо Я";
|
||||
|
||||
/* Операция поиска */
|
||||
// Передать в хеш-таблицу ключ key и получить значение value
|
||||
string name = map[15937];
|
||||
|
||||
/* Операция удаления */
|
||||
// Удалить пару ключ-значение (key, value) из хеш-таблицы
|
||||
map.erase(10583);
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="hash_map.java"
|
||||
/* Инициализация хеш-таблицы */
|
||||
Map<Integer, String> map = new HashMap<>();
|
||||
|
||||
/* Операция добавления */
|
||||
// Добавить пару ключ-значение (key, value) в хеш-таблицу
|
||||
map.put(12836, "Сяо Ха");
|
||||
map.put(15937, "Сяо Ло");
|
||||
map.put(16750, "Сяо Суань");
|
||||
map.put(13276, "Сяо Фа");
|
||||
map.put(10583, "Сяо Я");
|
||||
|
||||
/* Операция поиска */
|
||||
// Передать в хеш-таблицу ключ key и получить значение value
|
||||
String name = map.get(15937);
|
||||
|
||||
/* Операция удаления */
|
||||
// Удалить пару ключ-значение (key, value) из хеш-таблицы
|
||||
map.remove(10583);
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="hash_map.cs"
|
||||
/* Инициализация хеш-таблицы */
|
||||
Dictionary<int, string> map = new() {
|
||||
/* Операция добавления */
|
||||
// Добавить пару ключ-значение (key, value) в хеш-таблицу
|
||||
{ 12836, "Сяо Ха" },
|
||||
{ 15937, "Сяо Ло" },
|
||||
{ 16750, "Сяо Суань" },
|
||||
{ 13276, "Сяо Фа" },
|
||||
{ 10583, "Сяо Я" }
|
||||
};
|
||||
|
||||
/* Операция поиска */
|
||||
// Передать в хеш-таблицу ключ key и получить значение value
|
||||
string name = map[15937];
|
||||
|
||||
/* Операция удаления */
|
||||
// Удалить пару ключ-значение (key, value) из хеш-таблицы
|
||||
map.Remove(10583);
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="hash_map_test.go"
|
||||
/* Инициализация хеш-таблицы */
|
||||
hmap := make(map[int]string)
|
||||
|
||||
/* Операция добавления */
|
||||
// Добавить пару ключ-значение (key, value) в хеш-таблицу
|
||||
hmap[12836] = "Сяо Ха"
|
||||
hmap[15937] = "Сяо Ло"
|
||||
hmap[16750] = "Сяо Суань"
|
||||
hmap[13276] = "Сяо Фа"
|
||||
hmap[10583] = "Сяо Я"
|
||||
|
||||
/* Операция поиска */
|
||||
// Передать в хеш-таблицу ключ key и получить значение value
|
||||
name := hmap[15937]
|
||||
|
||||
/* Операция удаления */
|
||||
// Удалить пару ключ-значение (key, value) из хеш-таблицы
|
||||
delete(hmap, 10583)
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="hash_map.swift"
|
||||
/* Инициализация хеш-таблицы */
|
||||
var map: [Int: String] = [:]
|
||||
|
||||
/* Операция добавления */
|
||||
// Добавить пару ключ-значение (key, value) в хеш-таблицу
|
||||
map[12836] = "Сяо Ха"
|
||||
map[15937] = "Сяо Ло"
|
||||
map[16750] = "Сяо Суань"
|
||||
map[13276] = "Сяо Фа"
|
||||
map[10583] = "Сяо Я"
|
||||
|
||||
/* Операция поиска */
|
||||
// Передать в хеш-таблицу ключ key и получить значение value
|
||||
let name = map[15937]!
|
||||
|
||||
/* Операция удаления */
|
||||
// Удалить пару ключ-значение (key, value) из хеш-таблицы
|
||||
map.removeValue(forKey: 10583)
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="hash_map.js"
|
||||
/* Инициализация хеш-таблицы */
|
||||
const map = new Map();
|
||||
/* Операция добавления */
|
||||
// Добавить пару ключ-значение (key, value) в хеш-таблицу
|
||||
map.set(12836, 'Сяо Ха');
|
||||
map.set(15937, 'Сяо Ло');
|
||||
map.set(16750, 'Сяо Суань');
|
||||
map.set(13276, 'Сяо Фа');
|
||||
map.set(10583, 'Сяо Я');
|
||||
|
||||
/* Операция поиска */
|
||||
// Передать в хеш-таблицу ключ key и получить значение value
|
||||
let name = map.get(15937);
|
||||
|
||||
/* Операция удаления */
|
||||
// Удалить пару ключ-значение (key, value) из хеш-таблицы
|
||||
map.delete(10583);
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="hash_map.ts"
|
||||
/* Инициализация хеш-таблицы */
|
||||
const map = new Map<number, string>();
|
||||
/* Операция добавления */
|
||||
// Добавить пару ключ-значение (key, value) в хеш-таблицу
|
||||
map.set(12836, 'Сяо Ха');
|
||||
map.set(15937, 'Сяо Ло');
|
||||
map.set(16750, 'Сяо Суань');
|
||||
map.set(13276, 'Сяо Фа');
|
||||
map.set(10583, 'Сяо Я');
|
||||
console.info('\nПосле добавления хеш-таблица имеет вид\nKey -> Value');
|
||||
console.info(map);
|
||||
|
||||
/* Операция поиска */
|
||||
// Передать в хеш-таблицу ключ key и получить значение value
|
||||
let name = map.get(15937);
|
||||
console.info('\nПо номеру 15937 найдено имя ' + name);
|
||||
|
||||
/* Операция удаления */
|
||||
// Удалить пару ключ-значение (key, value) из хеш-таблицы
|
||||
map.delete(10583);
|
||||
console.info('\nПосле удаления 10583 хеш-таблица имеет вид\nKey -> Value');
|
||||
console.info(map);
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="hash_map.dart"
|
||||
/* Инициализация хеш-таблицы */
|
||||
Map<int, String> map = {};
|
||||
|
||||
/* Операция добавления */
|
||||
// Добавить пару ключ-значение (key, value) в хеш-таблицу
|
||||
map[12836] = "Сяо Ха";
|
||||
map[15937] = "Сяо Ло";
|
||||
map[16750] = "Сяо Суань";
|
||||
map[13276] = "Сяо Фа";
|
||||
map[10583] = "Сяо Я";
|
||||
|
||||
/* Операция поиска */
|
||||
// Передать в хеш-таблицу ключ key и получить значение value
|
||||
String name = map[15937];
|
||||
|
||||
/* Операция удаления */
|
||||
// Удалить пару ключ-значение (key, value) из хеш-таблицы
|
||||
map.remove(10583);
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="hash_map.rs"
|
||||
use std::collections::HashMap;
|
||||
|
||||
/* Инициализация хеш-таблицы */
|
||||
let mut map: HashMap<i32, String> = HashMap::new();
|
||||
|
||||
/* Операция добавления */
|
||||
// Добавить пару ключ-значение (key, value) в хеш-таблицу
|
||||
map.insert(12836, "Сяо Ха".to_string());
|
||||
map.insert(15937, "Сяо Ло".to_string());
|
||||
map.insert(16750, "Сяо Суань".to_string());
|
||||
map.insert(13279, "Сяо Фа".to_string());
|
||||
map.insert(10583, "Сяо Я".to_string());
|
||||
|
||||
/* Операция поиска */
|
||||
// Передать в хеш-таблицу ключ key и получить значение value
|
||||
let _name: Option<&String> = map.get(&15937);
|
||||
|
||||
/* Операция удаления */
|
||||
// Удалить пару ключ-значение (key, value) из хеш-таблицы
|
||||
let _removed_value: Option<String> = map.remove(&10583);
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="hash_map.c"
|
||||
// В C нет встроенной хеш-таблицы
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="hash_map.kt"
|
||||
/* Инициализация хеш-таблицы */
|
||||
val map = HashMap<Int,String>()
|
||||
|
||||
/* Операция добавления */
|
||||
// Добавить пару ключ-значение (key, value) в хеш-таблицу
|
||||
map[12836] = "Сяо Ха"
|
||||
map[15937] = "Сяо Ло"
|
||||
map[16750] = "Сяо Суань"
|
||||
map[13276] = "Сяо Фа"
|
||||
map[10583] = "Сяо Я"
|
||||
|
||||
/* Операция поиска */
|
||||
// Передать в хеш-таблицу ключ key и получить значение value
|
||||
val name = map[15937]
|
||||
|
||||
/* Операция удаления */
|
||||
// Удалить пару ключ-значение (key, value) из хеш-таблицы
|
||||
map.remove(10583)
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="hash_map.rb"
|
||||
# Инициализация хеш-таблицы
|
||||
hmap = {}
|
||||
|
||||
# Операция добавления
|
||||
# Добавить пару ключ-значение (key, value) в хеш-таблицу
|
||||
hmap[12836] = "Сяо Ха"
|
||||
hmap[15937] = "Сяо Ло"
|
||||
hmap[16750] = "Сяо Суань"
|
||||
hmap[13276] = "Сяо Фа"
|
||||
hmap[10583] = "Сяо Я"
|
||||
|
||||
# Операция поиска
|
||||
# Передать в хеш-таблицу ключ key и получить значение value
|
||||
name = hmap[15937]
|
||||
|
||||
# Операция удаления
|
||||
# Удалить пару ключ-значение (key, value) из хеш-таблицы
|
||||
hmap.delete(10583)
|
||||
```
|
||||
|
||||
??? 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%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F%20%D0%B4%D0%BE%D0%B1%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F%0A%20%20%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D0%B2%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%20%D0%BF%D0%B0%D1%80%D1%83%20%D0%BA%D0%BB%D1%8E%D1%87-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%28key%2C%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A5%D0%B0%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%9B%D0%BE%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A1%D1%83%D0%B0%D0%BD%D1%8C%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A4%D0%B0%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%D0%A3%D1%82%D0%B5%D0%BD%D0%BE%D0%BA%22%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F%20%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0%0A%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D0%B4%D0%B0%D1%82%D1%8C%20%D0%BA%D0%BB%D1%8E%D1%87%20key%20%D0%B2%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%20%D0%B8%20%D0%BF%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F%20%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F%0A%20%20%20%20%23%20%D0%A3%D0%B4%D0%B0%D0%BB%D0%B8%D1%82%D1%8C%20%D0%B8%D0%B7%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%8B%20%D0%BF%D0%B0%D1%80%D1%83%20%D0%BA%D0%BB%D1%8E%D1%87-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%28key%2C%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)
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="hash_map.cpp"
|
||||
/* Обход хеш-таблицы */
|
||||
// Обход пар ключ-значение key->value
|
||||
for (auto kv: map) {
|
||||
cout << kv.first << " -> " << kv.second << endl;
|
||||
}
|
||||
// Обход key->value с помощью итератора
|
||||
for (auto iter = map.begin(); iter != map.end(); iter++) {
|
||||
cout << iter->first << "->" << iter->second << endl;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="hash_map.java"
|
||||
/* Обход хеш-таблицы */
|
||||
// Обход пар ключ-значение key->value
|
||||
for (Map.Entry <Integer, String> kv: map.entrySet()) {
|
||||
System.out.println(kv.getKey() + " -> " + kv.getValue());
|
||||
}
|
||||
// Обход только ключей key
|
||||
for (int key: map.keySet()) {
|
||||
System.out.println(key);
|
||||
}
|
||||
// Обход только значений value
|
||||
for (String val: map.values()) {
|
||||
System.out.println(val);
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="hash_map.cs"
|
||||
/* Обход хеш-таблицы */
|
||||
// Обход пар ключ-значение Key->Value
|
||||
foreach (var kv in map) {
|
||||
Console.WriteLine(kv.Key + " -> " + kv.Value);
|
||||
}
|
||||
// Обход только ключей key
|
||||
foreach (int key in map.Keys) {
|
||||
Console.WriteLine(key);
|
||||
}
|
||||
// Обход только значений value
|
||||
foreach (string val in map.Values) {
|
||||
Console.WriteLine(val);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="hash_map_test.go"
|
||||
/* Обход хеш-таблицы */
|
||||
// Обход пар ключ-значение key->value
|
||||
for key, value := range hmap {
|
||||
fmt.Println(key, "->", value)
|
||||
}
|
||||
// Обход только ключей key
|
||||
for key := range hmap {
|
||||
fmt.Println(key)
|
||||
}
|
||||
// Обход только значений value
|
||||
for _, value := range hmap {
|
||||
fmt.Println(value)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="hash_map.swift"
|
||||
/* Обход хеш-таблицы */
|
||||
// Обход пар ключ-значение Key->Value
|
||||
for (key, value) in map {
|
||||
print("\(key) -> \(value)")
|
||||
}
|
||||
// Обход только ключей Key
|
||||
for key in map.keys {
|
||||
print(key)
|
||||
}
|
||||
// Обход только значений Value
|
||||
for value in map.values {
|
||||
print(value)
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="hash_map.js"
|
||||
/* Обход хеш-таблицы */
|
||||
console.info('\nОбход пар ключ-значение Key->Value');
|
||||
for (const [k, v] of map.entries()) {
|
||||
console.info(k + ' -> ' + v);
|
||||
}
|
||||
console.info('\nОбход только ключей Key');
|
||||
for (const k of map.keys()) {
|
||||
console.info(k);
|
||||
}
|
||||
console.info('\nОбход только значений Value');
|
||||
for (const v of map.values()) {
|
||||
console.info(v);
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="hash_map.ts"
|
||||
/* Обход хеш-таблицы */
|
||||
console.info('\nОбход пар ключ-значение Key->Value');
|
||||
for (const [k, v] of map.entries()) {
|
||||
console.info(k + ' -> ' + v);
|
||||
}
|
||||
console.info('\nОбход только ключей Key');
|
||||
for (const k of map.keys()) {
|
||||
console.info(k);
|
||||
}
|
||||
console.info('\nОбход только значений Value');
|
||||
for (const v of map.values()) {
|
||||
console.info(v);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="hash_map.dart"
|
||||
/* Обход хеш-таблицы */
|
||||
// Обход пар ключ-значение Key->Value
|
||||
map.forEach((key, value) {
|
||||
print('$key -> $value');
|
||||
});
|
||||
|
||||
// Обход только ключей Key
|
||||
map.keys.forEach((key) {
|
||||
print(key);
|
||||
});
|
||||
|
||||
// Обход только значений Value
|
||||
map.values.forEach((value) {
|
||||
print(value);
|
||||
});
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="hash_map.rs"
|
||||
/* Обход хеш-таблицы */
|
||||
// Обход пар ключ-значение Key->Value
|
||||
for (key, value) in &map {
|
||||
println!("{key} -> {value}");
|
||||
}
|
||||
|
||||
// Обход только ключей Key
|
||||
for key in map.keys() {
|
||||
println!("{key}");
|
||||
}
|
||||
|
||||
// Обход только значений Value
|
||||
for value in map.values() {
|
||||
println!("{value}");
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="hash_map.c"
|
||||
// В C нет встроенной хеш-таблицы
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="hash_map.kt"
|
||||
/* Обход хеш-таблицы */
|
||||
// Обход пар ключ-значение key->value
|
||||
for ((key, value) in map) {
|
||||
println("$key -> $value")
|
||||
}
|
||||
// Обход только ключей key
|
||||
for (key in map.keys) {
|
||||
println(key)
|
||||
}
|
||||
// Обход только значений value
|
||||
for (_val in map.values) {
|
||||
println(_val)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="hash_map.rb"
|
||||
# Обход хеш-таблицы
|
||||
# Обход пар ключ-значение key->value
|
||||
hmap.entries.each { |key, value| puts "#{key} -> #{value}" }
|
||||
|
||||
# Обход только ключей key
|
||||
hmap.keys.each { |key| puts key }
|
||||
|
||||
# Обход только значений value
|
||||
hmap.values.each { |val| puts val }
|
||||
```
|
||||
|
||||
??? 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%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F%20%D0%B4%D0%BE%D0%B1%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F%0A%20%20%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D0%B2%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%20%D0%BF%D0%B0%D1%80%D1%83%20%D0%BA%D0%BB%D1%8E%D1%87-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%28key%2C%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A5%D0%B0%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%9B%D0%BE%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A1%D1%83%D0%B0%D0%BD%D1%8C%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A4%D0%B0%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%D0%A3%D1%82%D0%B5%D0%BD%D0%BE%D0%BA%22%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D0%B1%D1%80%D0%B0%D1%82%D1%8C%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%0A%20%20%20%20%23%20%D0%9E%D0%B1%D0%BE%D0%B9%D1%82%D0%B8%D0%BF%D0%B0%D1%80%D0%B0%20%D0%BA%D0%BB%D1%8E%D1%87-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20key-%3Evalue%0A%20%20%20%20for%20key%2C%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key%2C%20%22-%3E%22%2C%20value%29%0A%20%20%20%20%23%20%D0%BE%D1%82%D0%B4%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D0%9E%D0%B1%D0%BE%D0%B9%D1%82%D0%B8%D0%BA%D0%BB%D1%8E%D1%87%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%D0%BE%D1%82%D0%B4%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D0%9E%D0%B1%D0%BE%D0%B9%D1%82%D0%B8%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%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>бакетом (bucket)</u>, и каждый бакет может хранить одну пару ключ-значение. Следовательно, операция поиска сводится к тому, чтобы найти бакет, соответствующий `key` , и получить из него `value` .
|
||||
|
||||
Но как определить бакет, соответствующий заданному `key` ? Это делается с помощью <u>хеш-функции (hash function)</u>. Назначение хеш-функции - отображать большое входное пространство в меньшее выходное пространство. В хеш-таблице входным пространством являются все `key` , а выходным - все бакеты (индексы массива). Иначе говоря, передав `key` на вход, **мы можем через хеш-функцию получить позицию хранения соответствующей пары ключ-значение в массиве**.
|
||||
|
||||
Процесс вычисления хеш-функции для одного `key` включает два шага.
|
||||
|
||||
1. Сначала с помощью некоторого хеш-алгоритма `hash()` вычисляется хеш-значение.
|
||||
2. Затем хеш-значение берется по модулю числа бакетов (длины массива) `capacity` , чтобы получить бакет (индекс массива) `index` , соответствующий этому `key` .
|
||||
|
||||
```shell
|
||||
index = hash(key) % capacity
|
||||
```
|
||||
|
||||
После этого можно использовать `index` для доступа к соответствующему бакету в хеш-таблице и получения `value` .
|
||||
|
||||
Пусть длина массива `capacity = 100` , а хеш-алгоритм `hash(key) = key` . Тогда легко получить хеш-функцию `key % 100` . На рисунке ниже на примере `key` "номер студенческого билета" и `value` "имя" показан принцип работы хеш-функции.
|
||||
|
||||

|
||||
|
||||
Ниже приведен код простой реализации хеш-таблицы. В нем мы инкапсулируем `key` и `value` в класс `Pair` , чтобы представить пару ключ-значение.
|
||||
|
||||
```src
|
||||
[file]{array_hash_map}-[class]{array_hash_map}-[func]{}
|
||||
```
|
||||
|
||||
## Хеш-коллизии и расширение
|
||||
|
||||
По сути, хеш-функция отображает входное пространство, состоящее из всех `key` , в выходное пространство, состоящее из всех индексов массива, а входное пространство обычно значительно больше выходного. Поэтому **теоретически неизбежно существование ситуации "несколько входов соответствуют одному выходу"**.
|
||||
|
||||
Для хеш-функции из приведенного выше примера, если последние две цифры `key` совпадают, то совпадает и результат хеш-функции. Например, если искать студентов с номерами 12836 и 20336, то получим:
|
||||
|
||||
```shell
|
||||
12836 % 100 = 36
|
||||
20336 % 100 = 36
|
||||
```
|
||||
|
||||
Как показано на рисунке ниже, два номера указывают на одно и то же имя, что, очевидно, неверно. Такую ситуацию, когда нескольким входам соответствует один и тот же выход, называют <u>хеш-коллизией (hash collision)</u>.
|
||||
|
||||

|
||||
|
||||
Легко понять, что чем больше емкость хеш-таблицы $n$ , тем ниже вероятность того, что несколько `key` попадут в один и тот же бакет, а значит, тем меньше коллизий. Поэтому **мы можем уменьшать число хеш-коллизий путем расширения хеш-таблицы**.
|
||||
|
||||
Как показано на рисунке ниже, до расширения пары ключ-значение `(136, A)` и `(236, D)` конфликтовали, а после расширения коллизия исчезла.
|
||||
|
||||

|
||||
|
||||
Подобно расширению массива, расширение хеш-таблицы требует перенести все пары ключ-значение из старой таблицы в новую, а это очень затратно по времени; кроме того, поскольку емкость хеш-таблицы `capacity` изменилась, нам приходится с помощью хеш-функции заново вычислять позиции хранения всех пар ключ-значение, что дополнительно увеличивает вычислительные расходы процесса расширения. Поэтому языки программирования обычно заранее резервируют достаточно большую емкость хеш-таблицы, чтобы избежать частых расширений.
|
||||
|
||||
<u>Коэффициент загрузки (load factor)</u> - важное понятие хеш-таблицы. Он определяется как отношение числа элементов в хеш-таблице к числу бакетов и используется для оценки степени серьезности хеш-коллизий, **а также часто служит условием срабатывания расширения хеш-таблицы**. Например, в Java, когда коэффициент загрузки превышает $0.75$ , система расширяет хеш-таблицу до $2$ раз от исходной емкости.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Хеш-таблицы
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
В мире компьютеров хеш-таблица похожа на сообразительного библиотекаря.
|
||||
|
||||
Он умеет вычислять шифр хранения и потому быстро находит нужную книгу.
|
||||
@@ -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 широко используется в протоколах и приложениях, связанных с безопасностью.
|
||||
- Языки программирования обычно предоставляют для типов данных встроенные хеш-алгоритмы, чтобы вычислять индексы бакетов в хеш-таблице. Как правило, хешируемыми могут быть только неизменяемые объекты.
|
||||
|
||||
### Q & A
|
||||
|
||||
**Q**: В каких случаях временная сложность хеш-таблицы становится $O(n)$ ?
|
||||
|
||||
Когда хеш-коллизии становятся достаточно серьезными, временная сложность хеш-таблицы деградирует до $O(n)$ . Если хеш-функция спроектирована хорошо, емкость выбрана разумно, а конфликты распределены достаточно равномерно, то временная сложность обычно считается $O(1)$ . При использовании встроенной хеш-таблицы языка программирования мы, как правило, и принимаем ее за $O(1)$ .
|
||||
|
||||
**Q**: Почему бы не использовать хеш-функцию $f(x) = x$ ? Тогда ведь коллизий не будет.
|
||||
|
||||
При хеш-функции $f(x) = x$ каждому элементу соответствует уникальный индекс бакета, и такая структура становится эквивалентна массиву. Однако входное пространство обычно намного больше выходного пространства (длины массива), поэтому последним шагом хеш-функции обычно выступает взятие по модулю длины массива. Иначе говоря, цель хеш-таблицы состоит в том, чтобы отобразить большее пространство состояний в меньшее пространство и при этом обеспечить $O(1)$ поиска.
|
||||
|
||||
**Q**: В основе хеш-таблицы лежат массив, связный список и двоичное дерево. Почему же она может быть быстрее них?
|
||||
|
||||
Во-первых, у хеш-таблицы повышается временная эффективность, но снижается пространственная эффективность. Значительная часть ее памяти остается неиспользованной.
|
||||
|
||||
Во-вторых, она быстрее только в определенных сценариях. Если одну и ту же задачу можно реализовать на массиве или связном списке с той же асимптотикой, то часто такая реализация окажется быстрее, чем хеш-таблица. Причина в том, что вычисление хеш-функции само по себе стоит времени, то есть константа в сложности получается выше.
|
||||
|
||||
Наконец, временная сложность хеш-таблицы тоже может деградировать. Например, при методе цепочек мы все равно выполняем поиск в связном списке или красно-черном дереве, поэтому риск деградации до $O(n)$ сохраняется.
|
||||
|
||||
**Q**: Есть ли у повторного хеширования недостаток "нельзя напрямую удалять элементы"? Можно ли повторно использовать место, помеченное как удаленное?
|
||||
|
||||
Повторное хеширование - это разновидность открытой адресации, а у всех методов открытой адресации есть недостаток: элементы нельзя удалять напрямую, поэтому приходится использовать метку удаления. Пространство, помеченное как удаленное, можно использовать повторно. Когда новый элемент вставляется в хеш-таблицу и в процессе пробирования попадает на такую отмеченную позицию, эта позиция может быть занята новым элементом. Такой подход сохраняет последовательность пробирования и одновременно поддерживает приемлемую эффективность использования памяти.
|
||||
|
||||
**Q**: Почему при линейном пробировании во время поиска элемента вообще возникает хеш-коллизия?
|
||||
|
||||
Во время поиска мы через хеш-функцию находим соответствующий бакет и соответствующую пару ключ-значение, но видим, что `key` не совпадает, а это и означает наличие хеш-коллизии. Поэтому метод линейного пробирования в соответствии с заранее заданным шагом последовательно движется дальше, пока не найдет правильную пару ключ-значение или не убедится, что поиск завершился неудачей.
|
||||
|
||||
**Q**: Почему расширение хеш-таблицы помогает смягчать хеш-коллизии?
|
||||
|
||||
Последний шаг хеш-функции обычно состоит во взятии по модулю длины массива $n$ , чтобы результат попадал в диапазон индексов массива; после расширения длина массива $n$ меняется, а значит, может измениться и индекс, соответствующий данному `key` . Несколько `key` , которые раньше попадали в один бакет, после расширения могут распределиться по нескольким бакетам, и тем самым хеш-коллизии будут ослаблены.
|
||||
|
||||
**Q**: Если нам нужен быстрый доступ, почему бы просто не использовать массив?
|
||||
|
||||
Когда `key` данных - это непрерывные целые числа из маленького диапазона, действительно можно напрямую использовать массив: это просто и эффективно. Но если `key` имеют другой тип данных (например, строки), тогда нужен хеш-алгоритм, который отобразит `key` в индекс массива, а хранение элементов будет выполняться через массив бакетов. Такая структура и называется хеш-таблицей.
|
||||