--- comments: true --- # 6.3 Алгоритмы хеширования В двух предыдущих разделах мы рассмотрели принципы работы хеш-таблицы и способы обработки хеш-коллизий. Однако и открытая адресация, и метод цепочек **лишь позволяют хеш-таблице корректно работать при возникновении коллизий, но не уменьшают вероятность появления самих коллизий**. Если хеш-коллизии происходят слишком часто, производительность хеш-таблицы резко деградирует. Как показано на рисунке 6-8, для хеш-таблицы с методом цепочек в идеальном случае пары ключ-значение равномерно распределены по всем бакетам, и это дает наилучшую эффективность поиска. В худшем же случае все пары ключ-значение оказываются в одном бакете, и временная сложность вырождается до $O(n)$ . { class="animation-figure" }
Рисунок 6-8 Лучший и худший случаи хеш-коллизий
**Распределение пар ключ-значение определяется хеш-функцией**. Вспомним этапы вычисления хеш-функции: сначала вычисляется хеш-значение, затем оно берется по модулю длины массива: ```shell index = hash(key) % capacity ``` Из этой формулы видно: при фиксированной емкости хеш-таблицы `capacity` **выходное значение определяет именно хеш-алгоритм `hash()` **, а значит, именно он определяет распределение пар ключ-значение в хеш-таблице. Это означает, что для уменьшения вероятности хеш-коллизий нам следует сосредоточиться на проектировании хеш-алгоритма `hash()` . ## 6.3.1 Цели хеш-алгоритма Чтобы получить структуру данных хеш-таблицы, которая будет одновременно быстрой и надежной, хеш-алгоритм должен обладать следующими свойствами. - **Детерминированность**: для одинакового входа хеш-алгоритм всегда должен выдавать одинаковый результат. Только так хеш-таблица остается надежной. - **Высокая эффективность**: вычисление хеш-значения должно быть достаточно быстрым. Чем меньше вычислительные затраты, тем выше практическая ценность хеш-таблицы. - **Равномерное распределение**: хеш-алгоритм должен стараться распределять пары ключ-значение в хеш-таблице равномерно. Чем равномернее распределение, тем ниже вероятность хеш-коллизий. На практике хеш-алгоритмы используются не только для реализации хеш-таблиц, но и во многих других областях. - **Хранение паролей**: чтобы защищать пароли пользователей, система обычно хранит не сами пароли в открытом виде, а их хеш-значения. Когда пользователь вводит пароль, система вычисляет хеш-значение введенного пароля и сравнивает его с сохраненным значением. Если они совпадают, пароль считается правильным. - **Проверка целостности данных**: отправитель может вычислить хеш-значение данных и отправить его вместе с самими данными. Получатель затем вычисляет хеш-значение повторно и сравнивает его с полученным. Если они совпадают, данные считаются целостными. Для приложений, связанных с криптографией, чтобы не допустить восстановления исходного пароля по хеш-значению и иных форм обратного анализа, хеш-алгоритм должен обладать более строгими свойствами безопасности. - **Односторонность**: по хеш-значению нельзя восстановить какую-либо информацию о входных данных. - **Устойчивость к коллизиям**: должно быть крайне трудно найти два разных входа, имеющих одинаковое хеш-значение. - **Эффект лавины**: даже небольшое изменение во входных данных должно приводить к заметному и непредсказуемому изменению результата. Обрати внимание: **«равномерное распределение» и «устойчивость к коллизиям» - это два независимых понятия** , и выполнение первого не означает автоматического выполнения второго. Например, при случайном распределении входных `key` хеш-функция `key % 100` может выдавать достаточно равномерное распределение. Однако этот хеш-алгоритм слишком прост: все `key` с одинаковыми двумя последними цифрами будут иметь одинаковый результат, а значит, по хеш-значению можно легко подобрать подходящие `key` и, например, взломать пароль. ## 6.3.2 Проектирование хеш-алгоритма Разработка хеш-алгоритма - это сложная задача, в которой нужно учитывать множество факторов. Однако для некоторых нетребовательных сценариев мы можем спроектировать и несколько простых хеш-алгоритмов. - **Аддитивный хеш**: складываем ASCII-коды всех символов входной строки и используем полученную сумму как хеш-значение. - **Мультипликативный хеш**: используем «некоррелированность» умножения. На каждом шаге умножаем текущее значение на константу и добавляем ASCII-код очередного символа. - **XOR-хеш**: последовательно накапливаем элементы входных данных в одном хеш-значении через операцию XOR. - **Ротационный хеш**: последовательно накапливаем ASCII-коды символов, причем перед каждым накоплением выполняем циклический сдвиг хеш-значения. === "Python" ```python title="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 ``` === "C++" ```cpp title="simple_hash.cpp" /* Аддитивное хеширование */ int addHash(string key) { long long hash = 0; const int MODULUS = 1000000007; for (unsigned char c : key) { hash = (hash + (int)c) % MODULUS; } return (int)hash; } /* Мультипликативное хеширование */ int mulHash(string key) { long long hash = 0; const int MODULUS = 1000000007; for (unsigned char c : key) { hash = (31 * hash + (int)c) % MODULUS; } return (int)hash; } /* XOR-хеширование */ int xorHash(string key) { int hash = 0; const int MODULUS = 1000000007; for (unsigned char c : key) { hash ^= (int)c; } return hash & MODULUS; } /* Хеширование с циклическим сдвигом */ int rotHash(string key) { long long hash = 0; const int MODULUS = 1000000007; for (unsigned char c : key) { hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS; } return (int)hash; } ``` === "Java" ```java title="simple_hash.java" /* Аддитивное хеширование */ int addHash(String key) { long hash = 0; final int MODULUS = 1000000007; for (char c : key.toCharArray()) { hash = (hash + (int) c) % MODULUS; } return (int) hash; } /* Мультипликативное хеширование */ int mulHash(String key) { long hash = 0; final int MODULUS = 1000000007; for (char c : key.toCharArray()) { hash = (31 * hash + (int) c) % MODULUS; } return (int) hash; } /* XOR-хеширование */ int xorHash(String key) { int hash = 0; final int MODULUS = 1000000007; for (char c : key.toCharArray()) { hash ^= (int) c; } return hash & MODULUS; } /* Хеширование с циклическим сдвигом */ int rotHash(String key) { long hash = 0; final int MODULUS = 1000000007; for (char c : key.toCharArray()) { hash = ((hash << 4) ^ (hash >> 28) ^ (int) c) % MODULUS; } return (int) hash; } ``` === "C#" ```csharp title="simple_hash.cs" /* Аддитивное хеширование */ int AddHash(string key) { long hash = 0; const int MODULUS = 1000000007; foreach (char c in key) { hash = (hash + c) % MODULUS; } return (int)hash; } /* Мультипликативное хеширование */ int MulHash(string key) { long hash = 0; const int MODULUS = 1000000007; foreach (char c in key) { hash = (31 * hash + c) % MODULUS; } return (int)hash; } /* XOR-хеширование */ int XorHash(string key) { int hash = 0; const int MODULUS = 1000000007; foreach (char c in key) { hash ^= c; } return hash & MODULUS; } /* Хеширование с циклическим сдвигом */ int RotHash(string key) { long hash = 0; const int MODULUS = 1000000007; foreach (char c in key) { hash = ((hash << 4) ^ (hash >> 28) ^ c) % MODULUS; } return (int)hash; } ``` === "Go" ```go title="simple_hash.go" /* Аддитивное хеширование */ func addHash(key string) int { var hash int64 var modulus int64 modulus = 1000000007 for _, b := range []byte(key) { hash = (hash + int64(b)) % modulus } return int(hash) } /* Мультипликативное хеширование */ func mulHash(key string) int { var hash int64 var modulus int64 modulus = 1000000007 for _, b := range []byte(key) { hash = (31*hash + int64(b)) % modulus } return int(hash) } /* XOR-хеширование */ func xorHash(key string) int { hash := 0 modulus := 1000000007 for _, b := range []byte(key) { fmt.Println(int(b)) hash ^= int(b) hash = (31*hash + int(b)) % modulus } return hash & modulus } /* Хеширование с циклическим сдвигом */ func rotHash(key string) int { var hash int64 var modulus int64 modulus = 1000000007 for _, b := range []byte(key) { hash = ((hash << 4) ^ (hash >> 28) ^ int64(b)) % modulus } return int(hash) } ``` === "Swift" ```swift title="simple_hash.swift" /* Аддитивное хеширование */ func addHash(key: String) -> Int { var hash = 0 let MODULUS = 1_000_000_007 for c in key { for scalar in c.unicodeScalars { hash = (hash + Int(scalar.value)) % MODULUS } } return hash } /* Мультипликативное хеширование */ func mulHash(key: String) -> Int { var hash = 0 let MODULUS = 1_000_000_007 for c in key { for scalar in c.unicodeScalars { hash = (31 * hash + Int(scalar.value)) % MODULUS } } return hash } /* XOR-хеширование */ func xorHash(key: String) -> Int { var hash = 0 let MODULUS = 1_000_000_007 for c in key { for scalar in c.unicodeScalars { hash ^= Int(scalar.value) } } return hash & MODULUS } /* Хеширование с циклическим сдвигом */ func rotHash(key: String) -> Int { var hash = 0 let MODULUS = 1_000_000_007 for c in key { for scalar in c.unicodeScalars { hash = ((hash << 4) ^ (hash >> 28) ^ Int(scalar.value)) % MODULUS } } return hash } ``` === "JS" ```javascript title="simple_hash.js" /* Аддитивное хеширование */ function addHash(key) { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = (hash + c.charCodeAt(0)) % MODULUS; } return hash; } /* Мультипликативное хеширование */ function mulHash(key) { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = (31 * hash + c.charCodeAt(0)) % MODULUS; } return hash; } /* XOR-хеширование */ function xorHash(key) { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash ^= c.charCodeAt(0); } return hash % MODULUS; } /* Хеширование с циклическим сдвигом */ function rotHash(key) { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS; } return hash; } ``` === "TS" ```typescript title="simple_hash.ts" /* Аддитивное хеширование */ function addHash(key: string): number { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = (hash + c.charCodeAt(0)) % MODULUS; } return hash; } /* Мультипликативное хеширование */ function mulHash(key: string): number { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = (31 * hash + c.charCodeAt(0)) % MODULUS; } return hash; } /* XOR-хеширование */ function xorHash(key: string): number { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash ^= c.charCodeAt(0); } return hash % MODULUS; } /* Хеширование с циклическим сдвигом */ function rotHash(key: string): number { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS; } return hash; } ``` === "Dart" ```dart title="simple_hash.dart" /* Аддитивное хеширование */ int addHash(String key) { int hash = 0; final int MODULUS = 1000000007; for (int i = 0; i < key.length; i++) { hash = (hash + key.codeUnitAt(i)) % MODULUS; } return hash; } /* Мультипликативное хеширование */ int mulHash(String key) { int hash = 0; final int MODULUS = 1000000007; for (int i = 0; i < key.length; i++) { hash = (31 * hash + key.codeUnitAt(i)) % MODULUS; } return hash; } /* XOR-хеширование */ int xorHash(String key) { int hash = 0; final int MODULUS = 1000000007; for (int i = 0; i < key.length; i++) { hash ^= key.codeUnitAt(i); } return hash & MODULUS; } /* Хеширование с циклическим сдвигом */ int rotHash(String key) { int hash = 0; final int MODULUS = 1000000007; for (int i = 0; i < key.length; i++) { hash = ((hash << 4) ^ (hash >> 28) ^ key.codeUnitAt(i)) % MODULUS; } return hash; } ``` === "Rust" ```rust title="simple_hash.rs" /* Аддитивное хеширование */ fn add_hash(key: &str) -> i32 { let mut hash = 0_i64; const MODULUS: i64 = 1000000007; for c in key.chars() { hash = (hash + c as i64) % MODULUS; } hash as i32 } /* Мультипликативное хеширование */ fn mul_hash(key: &str) -> i32 { let mut hash = 0_i64; const MODULUS: i64 = 1000000007; for c in key.chars() { hash = (31 * hash + c as i64) % MODULUS; } hash as i32 } /* XOR-хеширование */ fn xor_hash(key: &str) -> i32 { let mut hash = 0_i64; const MODULUS: i64 = 1000000007; for c in key.chars() { hash ^= c as i64; } (hash & MODULUS) as i32 } /* Хеширование с циклическим сдвигом */ fn rot_hash(key: &str) -> i32 { let mut hash = 0_i64; const MODULUS: i64 = 1000000007; for c in key.chars() { hash = ((hash << 4) ^ (hash >> 28) ^ c as i64) % MODULUS; } hash as i32 } ``` === "C" ```c title="simple_hash.c" /* Аддитивное хеширование */ int addHash(char *key) { long long hash = 0; const int MODULUS = 1000000007; for (int i = 0; i < strlen(key); i++) { hash = (hash + (unsigned char)key[i]) % MODULUS; } return (int)hash; } /* Мультипликативное хеширование */ int mulHash(char *key) { long long hash = 0; const int MODULUS = 1000000007; for (int i = 0; i < strlen(key); i++) { hash = (31 * hash + (unsigned char)key[i]) % MODULUS; } return (int)hash; } /* XOR-хеширование */ int xorHash(char *key) { int hash = 0; const int MODULUS = 1000000007; for (int i = 0; i < strlen(key); i++) { hash ^= (unsigned char)key[i]; } return hash & MODULUS; } /* Хеширование с циклическим сдвигом */ int rotHash(char *key) { long long hash = 0; const int MODULUS = 1000000007; for (int i = 0; i < strlen(key); i++) { hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS; } return (int)hash; } ``` === "Kotlin" ```kotlin title="simple_hash.kt" /* Аддитивное хеширование */ fun addHash(key: String): Int { var hash = 0L val MODULUS = 1000000007 for (c in key.toCharArray()) { hash = (hash + c.code) % MODULUS } return hash.toInt() } /* Мультипликативное хеширование */ fun mulHash(key: String): Int { var hash = 0L val MODULUS = 1000000007 for (c in key.toCharArray()) { hash = (31 * hash + c.code) % MODULUS } return hash.toInt() } /* XOR-хеширование */ fun xorHash(key: String): Int { var hash = 0 val MODULUS = 1000000007 for (c in key.toCharArray()) { hash = hash xor c.code } return hash and MODULUS } /* Хеширование с циклическим сдвигом */ fun rotHash(key: String): Int { var hash = 0L val MODULUS = 1000000007 for (c in key.toCharArray()) { hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS } return hash.toInt() } ``` === "Ruby" ```ruby title="simple_hash.rb" ### Аддитивное хеширование ### def add_hash(key) hash = 0 modulus = 1_000_000_007 key.each_char { |c| hash += c.ord } hash % modulus end ### Мультипликативное хеширование ### def mul_hash(key) hash = 0 modulus = 1_000_000_007 key.each_char { |c| hash = 31 * hash + c.ord } hash % modulus end ### XOR-хеширование ### def xor_hash(key) hash = 0 modulus = 1_000_000_007 key.each_char { |c| hash ^= c.ord } hash % modulus end ### Хеширование с циклическим сдвигом ### def rot_hash(key) hash = 0 modulus = 1_000_000_007 key.each_char { |c| hash = (hash << 4) ^ (hash >> 28) ^ c.ord } hash % modulus end ``` ??? pythontutor "Визуализация кода" Нетрудно заметить, что последний шаг каждого из этих хеш-алгоритмов - взятие по модулю большого простого числа $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` присутствует периодичность, то взятие по модулю составного числа гораздо легче приводит к кластеризации. Итак, на практике мы обычно выбираем простое число в качестве модуля, причем это простое число желательно брать достаточно большим, чтобы по возможности убрать периодические закономерности и повысить устойчивость хеш-алгоритма. ## 6.3.3 Распространенные хеш-алгоритмы Нетрудно заметить, что описанные выше простые хеш-алгоритмы довольно хрупкие и далеки от поставленных целей. Например, сложение и XOR подчиняются коммутативному закону, поэтому аддитивный хеш и XOR-хеш не различают строки, состоящие из одних и тех же символов, но в разном порядке. Это может усиливать хеш-коллизии и даже создавать некоторые проблемы безопасности. На практике мы обычно используем стандартные хеш-алгоритмы, такие как MD5, SHA-1, SHA-2 и SHA-3. Они могут отображать входные данные произвольной длины в хеш-значения фиксированной длины. На протяжении почти ста лет хеш-алгоритмы непрерывно развивались и оптимизировались. Одни исследователи старались повысить их производительность, а другие исследователи и хакеры сосредоточивались на поиске уязвимостей в их безопасности. В таблице 6-2 приведены распространенные хеш-алгоритмы, которые часто встречаются в реальных приложениях. - MD5 и SHA-1 уже многократно были успешно атакованы, поэтому они выведены из большинства сценариев, где требуется безопасность. - SHA-256 из семейства SHA-2 является одним из самых надежных хеш-алгоритмов. На сегодняшний день не известно успешных практических атак, поэтому он широко используется в самых разных протоколах и системах безопасности. - SHA-3 по сравнению с SHA-2 требует меньших затрат на реализацию и обеспечивает более высокую вычислительную эффективность, но на данный момент распространен слабее, чем семейство SHA-2.Таблица 6-2 Распространенные хеш-алгоритмы