This commit is contained in:
krahets
2026-04-14 18:06:19 +08:00
parent 17b2a0b630
commit cf0747ba3e
131 changed files with 604 additions and 609 deletions
+7 -7
View File
@@ -4402,7 +4402,7 @@
<!-- Page content -->
<h1 id="63">6.3 &nbsp; Алгоритмы хеширования<a class="headerlink" href="#63" title="Permanent link">&para;</a></h1>
<p>В двух предыдущих разделах мы рассмотрели принципы работы хеш-таблицы и способы обработки хеш-коллизий. Однако и открытая адресация, и метод цепочек <strong>лишь позволяют хеш-таблице корректно работать при возникновении коллизий, но не уменьшают вероятность появления самих коллизий</strong>.</p>
<p>Если хеш-коллизии происходят слишком часто, производительность хеш-таблицы резко деградирует. Как показано на рисунке 6-8, для хеш-таблицы с методом цепочек в идеальном случае пары ключ-значение равномерно распределены по всем бакетам, и это дает наилучшую эффективность поиска; в худшем же случае все пары ключ-значение оказываются в одном бакете, и временная сложность вырождается до <span class="arithmatex">\(O(n)\)</span> .</p>
<p>Если хеш-коллизии происходят слишком часто, производительность хеш-таблицы резко деградирует. Как показано на рисунке 6-8, для хеш-таблицы с методом цепочек в идеальном случае пары ключ-значение равномерно распределены по всем бакетам, и это дает наилучшую эффективность поиска. В худшем же случае все пары ключ-значение оказываются в одном бакете, и временная сложность вырождается до <span class="arithmatex">\(O(n)\)</span> .</p>
<p><img alt="Лучший и худший случаи хеш-коллизий" class="animation-figure" src="../hash_algorithm.assets/hash_collision_best_worst_condition.png" /></p>
<p align="center"> Рисунок 6-8 &nbsp; Лучший и худший случаи хеш-коллизий </p>
@@ -4421,7 +4421,7 @@
<p>На практике хеш-алгоритмы используются не только для реализации хеш-таблиц, но и во многих других областях.</p>
<ul>
<li><strong>Хранение паролей</strong>: чтобы защищать пароли пользователей, система обычно хранит не сами пароли в открытом виде, а их хеш-значения. Когда пользователь вводит пароль, система вычисляет хеш-значение введенного пароля и сравнивает его с сохраненным значением. Если они совпадают, пароль считается правильным.</li>
<li><strong>Проверка целостности данных</strong>: отправитель может вычислить хеш-значение данных и отправить его вместе с самими данными; получатель затем вычисляет хеш-значение повторно и сравнивает его с полученным. Если они совпадают, данные считаются целостными.</li>
<li><strong>Проверка целостности данных</strong>: отправитель может вычислить хеш-значение данных и отправить его вместе с самими данными. Получатель затем вычисляет хеш-значение повторно и сравнивает его с полученным. Если они совпадают, данные считаются целостными.</li>
</ul>
<p>Для приложений, связанных с криптографией, чтобы не допустить восстановления исходного пароля по хеш-значению и иных форм обратного анализа, хеш-алгоритм должен обладать более строгими свойствами безопасности.</p>
<ul>
@@ -4429,12 +4429,12 @@
<li><strong>Устойчивость к коллизиям</strong>: должно быть крайне трудно найти два разных входа, имеющих одинаковое хеш-значение.</li>
<li><strong>Эффект лавины</strong>: даже небольшое изменение во входных данных должно приводить к заметному и непредсказуемому изменению результата.</li>
</ul>
<p>Обрати внимание: <strong>"равномерное распределение" и "устойчивость к коллизиям" - это два независимых понятия</strong> , и выполнение первого не означает автоматического выполнения второго. Например, при случайном распределении входных <code>key</code> хеш-функция <code>key % 100</code> может выдавать достаточно равномерное распределение. Однако этот хеш-алгоритм слишком прост: все <code>key</code> с одинаковыми двумя последними цифрами будут иметь одинаковый результат, а значит, по хеш-значению можно легко подобрать подходящие <code>key</code> и, например, взломать пароль.</p>
<p>Обрати внимание: <strong>«равномерное распределение» и «устойчивость к коллизиям» - это два независимых понятия</strong> , и выполнение первого не означает автоматического выполнения второго. Например, при случайном распределении входных <code>key</code> хеш-функция <code>key % 100</code> может выдавать достаточно равномерное распределение. Однако этот хеш-алгоритм слишком прост: все <code>key</code> с одинаковыми двумя последними цифрами будут иметь одинаковый результат, а значит, по хеш-значению можно легко подобрать подходящие <code>key</code> и, например, взломать пароль.</p>
<h2 id="632-">6.3.2 &nbsp; Проектирование хеш-алгоритма<a class="headerlink" href="#632-" title="Permanent link">&para;</a></h2>
<p>Разработка хеш-алгоритма - это сложная задача, в которой нужно учитывать множество факторов. Однако для некоторых нетребовательных сценариев мы можем спроектировать и несколько простых хеш-алгоритмов.</p>
<ul>
<li><strong>Аддитивный хеш</strong>: складываем ASCII-коды всех символов входной строки и используем полученную сумму как хеш-значение.</li>
<li><strong>Мультипликативный хеш</strong>: используем "некоррелированность" умножения; на каждом шаге умножаем текущее значение на константу и добавляем ASCII-код очередного символа.</li>
<li><strong>Мультипликативный хеш</strong>: используем «некоррелированность» умножения. На каждом шаге умножаем текущее значение на константу и добавляем ASCII-код очередного символа.</li>
<li><strong>XOR-хеш</strong>: последовательно накапливаем элементы входных данных в одном хеш-значении через операцию XOR.</li>
<li><strong>Ротационный хеш</strong>: последовательно накапливаем ASCII-коды символов, причем перед каждым накоплением выполняем циклический сдвиг хеш-значения.</li>
</ul>
@@ -5021,7 +5021,7 @@
\text{hash} &amp; = \{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\dots \}
\end{aligned}
\]</div>
<p>Если входные <code>key</code> как раз удовлетворяют такому распределению в виде арифметической прогрессии, то хеш-значения начнут скучиваться, а это усугубит хеш-коллизии. Теперь предположим, что мы заменили <code>modulus</code> на простое число <span class="arithmatex">\(13\)</span> ; поскольку между <code>key</code> и <code>modulus</code> нет общих делителей, равномерность распределения хеш-значений заметно улучшится.</p>
<p>Если входные <code>key</code> как раз удовлетворяют такому распределению в виде арифметической прогрессии, то хеш-значения начнут скучиваться, а это усугубит хеш-коллизии. Теперь предположим, что мы заменили <code>modulus</code> на простое число <span class="arithmatex">\(13\)</span>. Поскольку между <code>key</code> и <code>modulus</code> нет общих делителей, равномерность распределения хеш-значений заметно улучшится.</p>
<div class="arithmatex">\[
\begin{aligned}
\text{modulus} &amp; = 13 \newline
@@ -5037,7 +5037,7 @@
<p>На протяжении почти ста лет хеш-алгоритмы непрерывно развивались и оптимизировались. Одни исследователи старались повысить их производительность, а другие исследователи и хакеры сосредоточивались на поиске уязвимостей в их безопасности. В таблице 6-2 приведены распространенные хеш-алгоритмы, которые часто встречаются в реальных приложениях.</p>
<ul>
<li>MD5 и SHA-1 уже многократно были успешно атакованы, поэтому они выведены из большинства сценариев, где требуется безопасность.</li>
<li>SHA-256 из семейства SHA-2 является одним из самых надежных хеш-алгоритмов; на сегодняшний день не известно успешных практических атак, поэтому он широко используется в самых разных протоколах и системах безопасности.</li>
<li>SHA-256 из семейства SHA-2 является одним из самых надежных хеш-алгоритмов. На сегодняшний день не известно успешных практических атак, поэтому он широко используется в самых разных протоколах и системах безопасности.</li>
<li>SHA-3 по сравнению с SHA-2 требует меньших затрат на реализацию и обеспечивает более высокую вычислительную эффективность, но на данный момент распространен слабее, чем семейство SHA-2.</li>
</ul>
<p align="center"> Таблица 6-2 &nbsp; Распространенные хеш-алгоритмы </p>
@@ -5096,7 +5096,7 @@
<p>Мы знаем, что <code>key</code> в хеш-таблице могут быть целыми числами, вещественными числами, строками и другими типами данных. Языки программирования обычно предоставляют встроенные хеш-алгоритмы для этих типов, чтобы вычислять индексы бакетов в хеш-таблице. Возьмем Python: в нем можно вызвать функцию <code>hash()</code> , чтобы вычислить хеш-значения для различных типов данных.</p>
<ul>
<li>Хеш-значение целого числа и булева значения совпадает с самим значением.</li>
<li>Вычисление хеш-значений для вещественных чисел и строк устроено сложнее; интересующиеся читатели могут изучить это самостоятельно.</li>
<li>Вычисление хеш-значений для вещественных чисел и строк устроено сложнее. Интересующиеся читатели могут изучить это самостоятельно.</li>
<li>Хеш-значение кортежа получается путем хеширования каждого элемента, а затем объединения этих хеш-значений в одно итоговое значение.</li>
<li>Хеш-значение объекта обычно строится на основе его адреса в памяти. Если переопределить метод хеширования объекта, можно реализовать вычисление хеша по содержимому.</li>
</ul>
+7 -7
View File
@@ -5950,13 +5950,13 @@
</details>
<p>Следует отметить, что когда связный список становится очень длинным, эффективность поиска <span class="arithmatex">\(O(n)\)</span> оказывается низкой. <strong>В этом случае список можно преобразовать в AVL-дерево или красно-черное дерево</strong> , чтобы оптимизировать временную сложность поиска до <span class="arithmatex">\(O(\log n)\)</span> .</p>
<h2 id="622">6.2.2 &nbsp; Открытая адресация<a class="headerlink" href="#622" title="Permanent link">&para;</a></h2>
<p><u>Открытая адресация (open addressing)</u> не вводит дополнительных структур данных, а обрабатывает хеш-коллизии с помощью многократного пробирования; основные варианты пробирования включают линейное пробирование, квадратичное пробирование и повторное хеширование.</p>
<p><u>Открытая адресация (open addressing)</u> не вводит дополнительных структур данных, а обрабатывает хеш-коллизии с помощью многократного пробирования. Основные варианты пробирования включают линейное пробирование, квадратичное пробирование и повторное хеширование.</p>
<p>Ниже на примере линейного пробирования рассмотрим механизм работы хеш-таблицы с открытой адресацией.</p>
<h3 id="1">1. &nbsp; Линейное пробирование<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>Линейное пробирование использует линейный поиск с фиксированным шагом. Его методы работы отличаются от обычной хеш-таблицы.</p>
<ul>
<li><strong>Вставка элемента</strong>: по хеш-функции вычисляется индекс бакета; если бакет уже занят, то от места конфликта выполняется линейный обход вперед (шаг обычно равен <span class="arithmatex">\(1\)</span> ), пока не будет найден пустой бакет, после чего элемент вставляется туда.</li>
<li><strong>Поиск элемента</strong>: если возник конфликт, то с тем же шагом продолжается линейный обход вперед, пока не будет найден целевой элемент и возвращено <code>value</code> ; если встречается пустой бакет, это означает, что искомого элемента в хеш-таблице нет, и возвращается <code>None</code> .</li>
<li><strong>Вставка элемента</strong>: по хеш-функции вычисляется индекс бакета. Если бакет уже занят, то от места конфликта выполняется линейный обход вперед (шаг обычно равен <span class="arithmatex">\(1\)</span> ), пока не будет найден пустой бакет, после чего элемент вставляется туда.</li>
<li><strong>Поиск элемента</strong>: если возник конфликт, то с тем же шагом продолжается линейный обход вперед, пока не будет найден целевой элемент и возвращено <code>value</code>. Если встречается пустой бакет, это означает, что искомого элемента в хеш-таблице нет, и возвращается <code>None</code> .</li>
</ul>
<p>На рисунке 6-6 показано распределение пар ключ-значение в хеш-таблице с открытой адресацией (линейное пробирование). Для этой хеш-функции все <code>key</code> с одинаковыми двумя последними цифрами отображаются в один и тот же бакет. Благодаря линейному пробированию они по очереди сохраняются в этом бакете и в следующих за ним бакетах.</p>
<p><img alt="Распределение пар ключ-значение в хеш-таблице с открытой адресацией (линейное пробирование)" class="animation-figure" src="../hash_collision.assets/hash_table_linear_probing.png" /></p>
@@ -5968,7 +5968,7 @@
<p align="center"> Рисунок 6-7 &nbsp; Проблема поиска после удаления элемента в открытой адресации </p>
<p>Чтобы решить эту проблему, можно использовать механизм <u>ленивого удаления (lazy deletion)</u>: он не удаляет элемент из хеш-таблицы напрямую, **а помечает этот бакет специальной константой <code>TOMBSTONE</code> **. В этом механизме и <code>None</code> , и <code>TOMBSTONE</code> означают пустой бакет, и оба могут быть использованы для размещения пары ключ-значение. Но есть важное различие: при линейном пробировании, встретив <code>TOMBSTONE</code> , нужно продолжать обход, потому что ниже него все еще могут существовать пары ключ-значение.</p>
<p>Однако <strong>ленивое удаление может ускорять деградацию производительности хеш-таблицы</strong>. Это связано с тем, что каждая операция удаления создает новую метку удаления; по мере роста числа <code>TOMBSTONE</code> время поиска тоже увеличивается, потому что линейное пробирование может быть вынуждено перескакивать через множество <code>TOMBSTONE</code> , прежде чем найдет целевой элемент.</p>
<p>Однако <strong>ленивое удаление может ускорять деградацию производительности хеш-таблицы</strong>. Это связано с тем, что каждая операция удаления создает новую метку удаления. По мере роста числа <code>TOMBSTONE</code> время поиска тоже увеличивается, потому что линейное пробирование может быть вынуждено перескакивать через множество <code>TOMBSTONE</code> , прежде чем найдет целевой элемент.</p>
<p>Поэтому имеет смысл при линейном пробировании запоминать индекс первого встреченного <code>TOMBSTONE</code> и затем менять найденный целевой элемент местами с этим <code>TOMBSTONE</code> . Преимущество такого подхода в том, что при каждом поиске или добавлении элемент будет перемещаться в бакет, расположенный ближе к его идеальной позиции (начальной точке пробирования), а значит, эффективность поиска улучшится.</p>
<p>Ниже приведена реализация хеш-таблицы с открытой адресацией, то есть с линейным пробированием, включающая ленивое удаление. Чтобы пространство хеш-таблицы использовалось более полно, мы рассматриваем ее как кольцевой массив: когда обход выходит за конец массива, он возвращается к началу и продолжается.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:13"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><input id="__tabbed_2_11" name="__tabbed_2" type="radio" /><input id="__tabbed_2_12" name="__tabbed_2" type="radio" /><input id="__tabbed_2_13" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Python</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Java</label><label for="__tabbed_2_4">C#</label><label for="__tabbed_2_5">Go</label><label for="__tabbed_2_6">Swift</label><label for="__tabbed_2_7">JS</label><label for="__tabbed_2_8">TS</label><label for="__tabbed_2_9">Dart</label><label for="__tabbed_2_10">Rust</label><label for="__tabbed_2_11">C</label><label for="__tabbed_2_12">Kotlin</label><label for="__tabbed_2_13">Ruby</label></div>
@@ -7673,7 +7673,7 @@
</div>
</div>
<h3 id="2">2. &nbsp; Квадратичное пробирование<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>Квадратичное пробирование похоже на линейное пробирование и тоже является одной из распространенных стратегий открытой адресации. При возникновении конфликта оно не пропускает фиксированное число шагов, а переходит на расстояние, равное "квадрату числа попыток", то есть на <span class="arithmatex">\(1, 4, 9, \dots\)</span> шагов.</p>
<p>Квадратичное пробирование похоже на линейное пробирование и тоже является одной из распространенных стратегий открытой адресации. При возникновении конфликта оно не пропускает фиксированное число шагов, а переходит на расстояние, равное «квадрату числа попыток», то есть на <span class="arithmatex">\(1, 4, 9, \dots\)</span> шагов.</p>
<p>Квадратичное пробирование имеет следующие основные преимущества.</p>
<ul>
<li>Квадратичное пробирование пытается смягчить эффект кластеризации линейного пробирования, так как пропускает расстояния, равные квадрату номера попытки.</li>
@@ -7688,7 +7688,7 @@
<p>Как видно из названия, метод повторного хеширования использует для пробирования несколько хеш-функций <span class="arithmatex">\(f_1(x)\)</span>, <span class="arithmatex">\(f_2(x)\)</span>, <span class="arithmatex">\(f_3(x)\)</span>, <span class="arithmatex">\(\dots\)</span> .</p>
<ul>
<li><strong>Вставка элемента</strong>: если хеш-функция <span class="arithmatex">\(f_1(x)\)</span> вызывает конфликт, то пробуем <span class="arithmatex">\(f_2(x)\)</span> , и так далее, пока не будет найдено пустое место для вставки элемента.</li>
<li><strong>Поиск элемента</strong>: поиск идет в том же порядке хеш-функций, пока не будет найден целевой элемент; если встречается пустая позиция или уже были опробованы все хеш-функции, это означает, что элемента в хеш-таблице нет, и возвращается <code>None</code> .</li>
<li><strong>Поиск элемента</strong>: поиск идет в том же порядке хеш-функций, пока не будет найден целевой элемент. Если встречается пустая позиция или уже были опробованы все хеш-функции, это означает, что элемента в хеш-таблице нет, и возвращается <code>None</code> .</li>
</ul>
<p>По сравнению с линейным пробированием метод повторного хеширования меньше подвержен кластеризации, но несколько хеш-функций приносят дополнительные вычислительные затраты.</p>
<div class="admonition tip">
@@ -7700,7 +7700,7 @@
<ul>
<li>Python использует открытую адресацию. В словаре <code>dict</code> для пробирования применяются псевдослучайные числа.</li>
<li>Java использует метод цепочек. Начиная с JDK 1.8, когда длина массива внутри <code>HashMap</code> достигает 64, а длина списка достигает 8, этот список преобразуется в красно-черное дерево для повышения производительности поиска.</li>
<li>Go использует метод цепочек. В Go установлено, что каждый бакет может хранить не более 8 пар ключ-значение; при переполнении подключается overflow-бакет, а когда таких бакетов становится слишком много, выполняется специальное расширение того же масштаба, чтобы сохранить производительность.</li>
<li>Go использует метод цепочек. В Go установлено, что каждый бакет может хранить не более 8 пар ключ-значение. При переполнении подключается overflow-бакет, а когда таких бакетов становится слишком много, выполняется специальное расширение того же масштаба, чтобы сохранить производительность.</li>
</ul>
<!-- Source file information -->
+4 -4
View File
@@ -4380,7 +4380,7 @@
<!-- Page content -->
<h1 id="61-">6.1 &nbsp; Хеш-таблица<a class="headerlink" href="#61-" title="Permanent link">&para;</a></h1>
<p><u>Хеш-таблица (hash table)</u>, также называемая <u>таблицей рассеяния</u>, реализует эффективный поиск элементов за счет установления соответствия между ключом <code>key</code> и значением <code>value</code> . Иначе говоря, если передать в хеш-таблицу ключ <code>key</code> , то можно за <span class="arithmatex">\(O(1)\)</span> времени получить соответствующее значение <code>value</code> .</p>
<p>Как показано на рисунке 6-1, пусть есть <span class="arithmatex">\(n\)</span> студентов, и у каждого из них есть два поля данных: имя и номер студенческого билета. Если мы хотим реализовать запрос вида "ввести номер студенческого билета и вернуть соответствующее имя", то для этого можно использовать показанную ниже хеш-таблицу.</p>
<p>Как показано на рисунке 6-1, пусть есть <span class="arithmatex">\(n\)</span> студентов, и у каждого из них есть два поля данных: имя и номер студенческого билета. Если мы хотим реализовать запрос вида «ввести номер студенческого билета и вернуть соответствующее имя», то для этого можно воспользоваться хеш-таблицей, изображенной на рисунке 6-1.</p>
<p><img alt="Абстрактное представление хеш-таблицы" class="animation-figure" src="../hash_map.assets/hash_table_lookup.png" /></p>
<p align="center"> Рисунок 6-1 &nbsp; Абстрактное представление хеш-таблицы </p>
@@ -4905,7 +4905,7 @@
<div class="highlight"><pre><span></span><code><a id="__codelineno-26-1" name="__codelineno-26-1" href="#__codelineno-26-1"></a><span class="nv">index</span><span class="w"> </span><span class="o">=</span><span class="w"> </span>hash<span class="o">(</span>key<span class="o">)</span><span class="w"> </span>%<span class="w"> </span>capacity
</code></pre></div>
<p>После этого можно использовать <code>index</code> для доступа к соответствующему бакету в хеш-таблице и получения <code>value</code> .</p>
<p>Пусть длина массива <code>capacity = 100</code> , а хеш-алгоритм <code>hash(key) = key</code> . Тогда легко получить хеш-функцию <code>key % 100</code> . На рисунке 6-2 на примере <code>key</code> "номер студенческого билета" и <code>value</code> "имя" показан принцип работы хеш-функции.</p>
<p>Пусть длина массива <code>capacity = 100</code> , а хеш-алгоритм <code>hash(key) = key</code> . Тогда легко получить хеш-функцию <code>key % 100</code> . На рисунке 6-2 на примере <code>key</code> «номер студенческого билета» и <code>value</code> «имя» показан принцип работы хеш-функции.</p>
<p><img alt="Принцип работы хеш-функции" class="animation-figure" src="../hash_map.assets/hash_function.png" /></p>
<p align="center"> Рисунок 6-2 &nbsp; Принцип работы хеш-функции </p>
@@ -6072,7 +6072,7 @@
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20Pair%3A%0A%0A%20%20%20%20def%20__init__%28self%2C%20key%3A%20int%2C%20val%3A%20str%29%3A%0A%20%20%20%20%20%20%20%20self.key%20%3D%20key%0A%20%20%20%20%20%20%20%20self.val%20%3D%20val%0A%0Aclass%20ArrayHashMap%3A%0A%0A%20%20%20%20def%20__init__%28self%29%3A%0A%20%20%20%20%20%20%20%20self.buckets%3A%20list%5BPair%20%7C%20None%5D%20%3D%20%5BNone%5D%20%2A%2020%0A%0A%20%20%20%20def%20hash_func%28self%2C%20key%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20index%20%3D%20key%20%25%2020%0A%20%20%20%20%20%20%20%20return%20index%0A%0A%20%20%20%20def%20get%28self%2C%20key%3A%20int%29%20-%3E%20str%3A%0A%20%20%20%20%20%20%20%20index%3A%20int%20%3D%20self.hash_func%28key%29%0A%20%20%20%20%20%20%20%20pair%3A%20Pair%20%3D%20self.buckets%5Bindex%5D%0A%20%20%20%20%20%20%20%20if%20pair%20is%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20None%0A%20%20%20%20%20%20%20%20return%20pair.val%0A%0A%20%20%20%20def%20put%28self%2C%20key%3A%20int%2C%20val%3A%20str%29%3A%0A%20%20%20%20%20%20%20%20pair%20%3D%20Pair%28key%2C%20val%29%0A%20%20%20%20%20%20%20%20index%3A%20int%20%3D%20self.hash_func%28key%29%0A%20%20%20%20%20%20%20%20self.buckets%5Bindex%5D%20%3D%20pair%0A%0A%20%20%20%20def%20remove%28self%2C%20key%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20index%3A%20int%20%3D%20self.hash_func%28key%29%0A%20%20%20%20%20%20%20%20self.buckets%5Bindex%5D%20%3D%20None%0A%0A%20%20%20%20def%20entry_set%28self%29%20-%3E%20list%5BPair%5D%3A%0A%20%20%20%20%20%20%20%20result%3A%20list%5BPair%5D%20%3D%20%5B%5D%0A%20%20%20%20%20%20%20%20for%20pair%20in%20self.buckets%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20pair%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20result.append%28pair%29%0A%20%20%20%20%20%20%20%20return%20result%0A%0A%20%20%20%20def%20key_set%28self%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%20%20%20%20result%20%3D%20%5B%5D%0A%20%20%20%20%20%20%20%20for%20pair%20in%20self.buckets%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20pair%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20result.append%28pair.key%29%0A%20%20%20%20%20%20%20%20return%20result%0A%0A%20%20%20%20def%20value_set%28self%29%20-%3E%20list%5Bstr%5D%3A%0A%20%20%20%20%20%20%20%20result%20%3D%20%5B%5D%0A%20%20%20%20%20%20%20%20for%20pair%20in%20self.buckets%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20pair%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20result.append%28pair.val%29%0A%20%20%20%20%20%20%20%20return%20result%0A%0A%20%20%20%20def%20print%28self%29%3A%0A%20%20%20%20%20%20%20%20for%20pair%20in%20self.buckets%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20pair%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print%28pair.key%2C%20%27-%3E%27%2C%20pair.val%29%0A%27Driver%20Code%27%0Aif%20__name__%20%3D%3D%20%27__main__%27%3A%0A%20%20%20%20hmap%20%3D%20ArrayHashMap%28%29%0A%20%20%20%20hmap.put%2812836%2C%20%27%D0%A1%D1%8F%D0%BE%20%D0%A5%D0%B0%27%29%0A%20%20%20%20hmap.put%2815937%2C%20%27%D0%A1%D1%8F%D0%BE%20%D0%9B%D0%BE%27%29%0A%20%20%20%20hmap.put%2816750%2C%20%27%D0%A1%D1%8F%D0%BE%20%D0%A1%D1%83%D0%B0%D0%BD%D1%8C%27%29%0A%20%20%20%20hmap.put%2813276%2C%20%27%D0%A1%D1%8F%D0%BE%20%D0%A4%D0%B0%27%29%0A%20%20%20%20hmap.put%2810583%2C%20%27%D0%A1%D1%8F%D0%BE%20%D0%AF%27%29%0A%20%20%20%20name%20%3D%20hmap.get%2815937%29%0A%20%20%20%20hmap.remove%2810583%29%0A%20%20%20%20print%28%27%5Cn%D0%9E%D1%82%D0%B4%D0%B5%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9%20%D0%BE%D0%B1%D1%85%D0%BE%D0%B4%20%D0%BF%D0%B0%D1%80%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%27%29%0A%20%20%20%20for%20pair%20in%20hmap.entry_set%28%29%3A%0A%20%20%20%20%20%20%20%20print%28pair.key%2C%20%27-%3E%27%2C%20pair.val%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<h2 id="613-">6.1.3 &nbsp; Хеш-коллизии и расширение<a class="headerlink" href="#613-" title="Permanent link">&para;</a></h2>
<p>По сути, хеш-функция отображает входное пространство, состоящее из всех <code>key</code> , в выходное пространство, состоящее из всех индексов массива, а входное пространство обычно значительно больше выходного. Поэтому <strong>теоретически неизбежно существование ситуации "несколько входов соответствуют одному выходу"</strong>.</p>
<p>По сути, хеш-функция отображает входное пространство, состоящее из всех <code>key</code> , в выходное пространство, состоящее из всех индексов массива, а входное пространство обычно значительно больше выходного. Поэтому <strong>теоретически неизбежно существование ситуации «несколько входов соответствуют одному выходу»</strong>.</p>
<p>Для хеш-функции из приведенного выше примера, если последние две цифры <code>key</code> совпадают, то совпадает и результат хеш-функции. Например, если искать студентов с номерами 12836 и 20336, то получим:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-40-1" name="__codelineno-40-1" href="#__codelineno-40-1"></a><span class="m">12836</span><span class="w"> </span>%<span class="w"> </span><span class="nv">100</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">36</span>
<a id="__codelineno-40-2" name="__codelineno-40-2" href="#__codelineno-40-2"></a><span class="m">20336</span><span class="w"> </span>%<span class="w"> </span><span class="nv">100</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">36</span>
@@ -6086,7 +6086,7 @@
<p><img alt="Расширение хеш-таблицы" class="animation-figure" src="../hash_map.assets/hash_table_reshash.png" /></p>
<p align="center"> Рисунок 6-4 &nbsp; Расширение хеш-таблицы </p>
<p>Подобно расширению массива, расширение хеш-таблицы требует перенести все пары ключ-значение из старой таблицы в новую, а это очень затратно по времени; кроме того, поскольку емкость хеш-таблицы <code>capacity</code> изменилась, нам приходится с помощью хеш-функции заново вычислять позиции хранения всех пар ключ-значение, что дополнительно увеличивает вычислительные расходы процесса расширения. Поэтому языки программирования обычно заранее резервируют достаточно большую емкость хеш-таблицы, чтобы избежать частых расширений.</p>
<p>Подобно расширению массива, расширение хеш-таблицы требует перенести все пары ключ-значение из старой таблицы в новую, а это очень затратно по времени. Кроме того, поскольку емкость хеш-таблицы <code>capacity</code> изменилась, нам приходится с помощью хеш-функции заново вычислять позиции хранения всех пар ключ-значение, что дополнительно увеличивает вычислительные расходы процесса расширения. Поэтому языки программирования обычно заранее резервируют достаточно большую емкость хеш-таблицы, чтобы избежать частых расширений.</p>
<p><u>Коэффициент загрузки (load factor)</u> - важное понятие хеш-таблицы. Он определяется как отношение числа элементов в хеш-таблице к числу бакетов и используется для оценки степени серьезности хеш-коллизий, <strong>а также часто служит условием срабатывания расширения хеш-таблицы</strong>. Например, в Java, когда коэффициент загрузки превышает <span class="arithmatex">\(0.75\)</span> , система расширяет хеш-таблицу до <span class="arithmatex">\(2\)</span> раз от исходной емкости.</p>
<!-- Source file information -->
+3 -3
View File
@@ -4362,7 +4362,7 @@
<li>Передав <code>key</code> , мы можем получить <code>value</code> из хеш-таблицы за <span class="arithmatex">\(O(1)\)</span> времени, поэтому она очень эффективна.</li>
<li>К типичным операциям хеш-таблицы относятся поиск, добавление пары ключ-значение, удаление пары ключ-значение и обход хеш-таблицы.</li>
<li>Хеш-функция отображает <code>key</code> в индекс массива, после чего можно обратиться к соответствующему бакету и получить <code>value</code> .</li>
<li>Два разных <code>key</code> после хеш-функции могут дать один и тот же индекс массива, что приводит к ошибочному результату поиска; это явление называется хеш-коллизией.</li>
<li>Два разных <code>key</code> после хеш-функции могут дать один и тот же индекс массива, что приводит к ошибочному результату поиска. Это явление называется хеш-коллизией.</li>
<li>Чем больше емкость хеш-таблицы, тем ниже вероятность хеш-коллизий. Поэтому хеш-коллизии можно смягчать путем расширения хеш-таблицы. Как и у массива, операция расширения у хеш-таблицы очень затратна.</li>
<li>Коэффициент загрузки определяется как отношение числа элементов в хеш-таблице к числу бакетов, отражает степень серьезности хеш-коллизий и часто используется как условие запуска расширения хеш-таблицы.</li>
<li>Метод цепочек превращает одиночный элемент в связный список и хранит все конфликтующие элементы в одном списке. Однако слишком длинный список снижает эффективность поиска, поэтому его можно дополнительно преобразовать в красно-черное дерево.</li>
@@ -4382,12 +4382,12 @@
<p>Во-первых, у хеш-таблицы повышается временная эффективность, но снижается пространственная эффективность. Значительная часть ее памяти остается неиспользованной.</p>
<p>Во-вторых, она быстрее только в определенных сценариях. Если одну и ту же задачу можно реализовать на массиве или связном списке с той же асимптотикой, то часто такая реализация окажется быстрее, чем хеш-таблица. Причина в том, что вычисление хеш-функции само по себе стоит времени, то есть константа в сложности получается выше.</p>
<p>Наконец, временная сложность хеш-таблицы тоже может деградировать. Например, при методе цепочек мы все равно выполняем поиск в связном списке или красно-черном дереве, поэтому риск деградации до <span class="arithmatex">\(O(n)\)</span> сохраняется.</p>
<p><strong>Q</strong>: Есть ли у повторного хеширования недостаток "нельзя напрямую удалять элементы"? Можно ли повторно использовать место, помеченное как удаленное?</p>
<p><strong>Q</strong>: Есть ли у повторного хеширования недостаток «нельзя напрямую удалять элементы»? Можно ли повторно использовать место, помеченное как удаленное?</p>
<p>Повторное хеширование - это разновидность открытой адресации, а у всех методов открытой адресации есть недостаток: элементы нельзя удалять напрямую, поэтому приходится использовать метку удаления. Пространство, помеченное как удаленное, можно использовать повторно. Когда новый элемент вставляется в хеш-таблицу и в процессе пробирования попадает на такую отмеченную позицию, эта позиция может быть занята новым элементом. Такой подход сохраняет последовательность пробирования и одновременно поддерживает приемлемую эффективность использования памяти.</p>
<p><strong>Q</strong>: Почему при линейном пробировании во время поиска элемента вообще возникает хеш-коллизия?</p>
<p>Во время поиска мы через хеш-функцию находим соответствующий бакет и соответствующую пару ключ-значение, но видим, что <code>key</code> не совпадает, а это и означает наличие хеш-коллизии. Поэтому метод линейного пробирования в соответствии с заранее заданным шагом последовательно движется дальше, пока не найдет правильную пару ключ-значение или не убедится, что поиск завершился неудачей.</p>
<p><strong>Q</strong>: Почему расширение хеш-таблицы помогает смягчать хеш-коллизии?</p>
<p>Последний шаг хеш-функции обычно состоит во взятии по модулю длины массива <span class="arithmatex">\(n\)</span> , чтобы результат попадал в диапазон индексов массива; после расширения длина массива <span class="arithmatex">\(n\)</span> меняется, а значит, может измениться и индекс, соответствующий данному <code>key</code> . Несколько <code>key</code> , которые раньше попадали в один бакет, после расширения могут распределиться по нескольким бакетам, и тем самым хеш-коллизии будут ослаблены.</p>
<p>Последний шаг хеш-функции обычно состоит во взятии по модулю длины массива <span class="arithmatex">\(n\)</span> , чтобы результат попадал в диапазон индексов массива. После расширения длина массива <span class="arithmatex">\(n\)</span> меняется, а значит, может измениться и индекс, соответствующий данному <code>key</code> . Несколько <code>key</code> , которые раньше попадали в один бакет, после расширения могут распределиться по нескольким бакетам, и тем самым хеш-коллизии будут ослаблены.</p>
<p><strong>Q</strong>: Если нам нужен быстрый доступ, почему бы просто не использовать массив?</p>
<p>Когда <code>key</code> данных - это непрерывные целые числа из маленького диапазона, действительно можно напрямую использовать массив: это просто и эффективно. Но если <code>key</code> имеют другой тип данных (например, строки), тогда нужен хеш-алгоритм, который отобразит <code>key</code> в индекс массива, а хранение элементов будет выполняться через массив бакетов. Такая структура и называется хеш-таблицей.</p>