diff --git a/ru/docs/chapter_backtracking/subset_sum_problem.md b/ru/docs/chapter_backtracking/subset_sum_problem.md index d699fdb18..d82a7fab5 100644 --- a/ru/docs/chapter_backtracking/subset_sum_problem.md +++ b/ru/docs/chapter_backtracking/subset_sum_problem.md @@ -1,35 +1,95 @@ -我仔细查看了提供的俄文内容,但发现这是关于"回溯算法"(Поиск с возвратом)的通用章节内容,而不是对应中文"子集和问题"(subset_sum_problem)小节的内容。 - -俄文内容主要讲述: -- 回溯算法的基本概念 -- 深度优先搜索 -- 尝试与回退 -- 剪枝操作 -- 代码框架 - -而中文内容是关于: -- 子集和问题的具体实现 -- 无重复元素的情况 -- 考虑重复元素的情况 -- 重复子集剪枝 - -**结论:提供的俄文章节内容与中文小节内容不匹配。** - -要完成对齐任务,我需要: -1. 俄文版本中对应"子集和问题"(subset_sum_problem)的具体内容 -2. 或者确认俄文版本中是否存在这个小节 - -**建议:** -- 请提供俄文版本中关于"子集和问题"或"Задача о сумме подмножеств"的章节内容 -- 或者提供完整的俄文 chapter_backtracking 目录结构,以便定位正确的对应内容 - -如果俄文版本确实缺少这个小节,我可以生成如下标记: - -```markdown # Задача о сумме подмножеств - - +## Случай без повторяющихся элементов + +!!! question + + Дан массив положительных целых чисел `nums` и целевое положительное целое число `target`. Найдите все возможные комбинации, сумма элементов которых равна `target`. В заданном массиве нет повторяющихся элементов, каждый элемент может быть выбран несколько раз. Верните эти комбинации в виде списка, в котором не должно быть повторяющихся комбинаций. + +Например, для входного множества $\{3, 4, 5\}$ и целевого числа $9$ решениями будут $\{3, 3, 3\}, \{4, 5\}$. Следует обратить внимание на следующие два момента: + +- элементы входного множества можно выбирать неограниченное количество раз; +- порядок элементов в подмножестве не имеет значения, например $\{4, 5\}$ и $\{5, 4\}$ — одно и то же подмножество. + +### Сравнение с решением задачи о полных перестановках + +Подобно задаче о полных перестановках, процесс генерации подмножеств можно представить как серию выборов, а в процессе выбора в реальном времени обновлять сумму элементов. Когда сумма элементов равна `target`, подмножество записывается в список результатов. + +Однако, в отличие от задачи о полных перестановках, **в данной задаче элементы множества можно выбирать неограниченное количество раз**, поэтому нет необходимости использовать булев список `selected` для записи выбранных элементов. Для получения начального решения можно просто немного изменить код для полных перестановок. + +```src +[file]{subset_sum_i_naive}-[class]{}-[func]{subset_sum_i_naive} ``` -请确认是否需要这样处理,或者提供正确的俄文对应内容。 \ No newline at end of file +При вводе в этот код массива $[3, 4, 5]$ и целевого элемента $9$ будет выведено $[3, 3, 3], [4, 5], [5, 4]$. **Хотя удалось найти все подмножества с суммой $9$, среди них есть повторяющиеся подмножества $[4, 5]$ и $[5, 4]$**. + +Это происходит потому, что процесс поиска различает порядок выбора, тогда как в подмножествах порядок элементов не важен. Как показано на рисунке ниже, сначала выбрать $4$, а затем $5$ и сначала выбрать $5$, а затем $4$ — это разные ветви, но они соответствуют одному и тому же подмножеству. + +![Поиск подмножеств и обрезка по превышению целевого значения](subset_sum_problem.assets/subset_sum_i_naive.png) + +**Одним из очевидных подходов к устранению повторяющихся подмножеств является удаление дубликатов из списка результатов**. Однако этот метод очень неэффективен по двум причинам: + +- Когда в массиве много элементов, особенно когда значение `target` велико, процесс поиска генерирует множество повторяющихся подмножеств. +- Сравнение подмножеств (массивов) на различия — очень затратная по времени операция. Она требует сначала сортировки массивов, затем сравнения различий каждого элемента в массиве. + +### Обрезка повторяющихся подмножеств + +**Рассмотрим устранение дубликатов в процессе поиска с помощью обрезки**. На рисунке ниже показано, что повторяющиеся подмножества возникают при выборе элементов массива в разном порядке, например в следующих случаях: + +1. Пусть на первом и втором этапах выбираются $3$ и $4$ соответственно, создаются все подмножества, содержащие эти два элемента, обозначенные как $[3, 4, \dots]$. +2. Затем если на первом этапе выбирается $4$, **то на втором этапе следует пропустить $3$**, так как подмножество $[4, 3, \dots]$ полностью повторяет подмножество, созданное на этапе `1.`. + +В процессе поиска выбор на каждом уровне осуществляется слева направо. Поэтому чем правее ветвь, тем больше она обрезается. + +1. На первых двух этапах выбираются $3$ и $5$ и создаются подмножества $[3, 5, \dots]$. +2. На первых двух этапах выбираются $4$ и $5$ и создаются подмножества $[4, 5, \dots]$. +3. Если на первом этапе выбирается $5$, **то на втором этапе следует пропустить $3$ и $4$**, так как подмножества $[5, 3, \dots]$ и $[5, 4, \dots]$ полностью повторяют подмножества, описанные на этапах `1.` и `2.`. + +![Повторяющиеся подмножества, полученные в результате различного порядка выбора](subset_sum_problem.assets/subset_sum_i_pruning.png) + +Обобщим эту мысль. Пусть задан входной массив $[x_1, x_2, \dots, x_n]$. Тогда в процессе поиска последовательность выбора $[x_{i_1}, x_{i_2}, \dots, x_{i_m}]$ должна удовлетворять условию $i_1 \leq i_2 \leq \dots \leq i_m$, **в противном случае она приведет к дубликатам, и ее нужно обрезать**. + +### Код реализации + +Для реализации этой обрезки мы инициализируем переменную `start`, которая указывает начальную точку обхода. **После выбора $x_{i}$ следующая итерация начинается с индекса $i$**. Это позволяет для последовательности выбора соблюдать условие $i_1 \leq i_2 \leq \dots \leq i_m$, обеспечивая уникальность подмножеств. + +Кроме того, в код были внесены следующие две оптимизации: + +- Перед началом поиска массив `nums` сортируется. При обходе всех вариантов, **если сумма подмножества превышает `target`, цикл завершается**, так как последующие элементы больше, и их сумма также превысит `target`. +- Исключение переменной `total`, **подсчет суммы элементов осуществляется с помощью вычитания из `target`**. Решение фиксируется, когда `target` равен $0$. + +```src +[file]{subset_sum_i}-[class]{}-[func]{subset_sum_i} +``` + +На рисунке ниже показан полный процесс поиска с возвратом для массива $[3, 4, 5]$ и целевого элемента $9$. + +![Процесс поиска с возвратом для реализации задачи о сумме подмножеств I](subset_sum_problem.assets/subset_sum_i.png) + +## Случай с повторяющимися элементами + +!!! question + + Дан массив положительных целых чисел `nums` и целевое положительное целое число `target`. Найдите все возможные комбинации, сумма элементов которых равна `target`. **Заданный массив может содержать повторяющиеся элементы, каждый элемент может быть выбран только один раз**. Верните эти комбинации в виде списка, в котором не должно быть повторяющихся комбинаций. + +В отличие от предыдущей задачи **входной массив может содержать повторяющиеся элементы**, что создает новую проблему. Например, для массива $[4, \hat{4}, 5]$ и целевого элемента $9$ текущий код выдает результат $[4, 5], [\hat{4}, 5]$, что приводит к повторяющимся подмножествам. + +**Причина этих повторов в том, что равные элементы выбираются несколько раз на одном этапе**. На рисунке ниже показано, что на первом этапе есть три варианта выбора, два из которых равны $4$. Это приводит к двум повторяющимся ветвям поиска и, следовательно, к повторяющимся подмножествам. Аналогично два элемента $4$ на втором этапе также создают повторяющиеся подмножества. + +![Повторяющиеся подмножества из-за равных элементов](subset_sum_problem.assets/subset_sum_ii_repeat.png) + +### Обрезка равных элементов + +Для решения этой проблемы **необходимо сделать выбор равных элементов на каждом этапе однократным**. Реализация этого подхода довольно изящна: поскольку массив отсортирован, равные элементы находятся рядом друг с другом. Это означает, что если текущий элемент равен предыдущему, то он уже был выбран, и его следует пропустить. + +В то же время **в этой задаче предусмотрено, что каждый элемент массива может быть выбран только один раз**. К счастью, можно использовать переменную `start` для выполнения этого ограничения: после выбора $x_{i}$ начинаем следующий цикл с индекса $i + 1$. Это позволяет исключить повторяющиеся подмножества и избежать повторного выбора элементов. + +### Код реализации + +```src +[file]{subset_sum_ii}-[class]{}-[func]{subset_sum_ii} +``` + +На рисунке ниже демонстрируется процесс обратного отслеживания для массива $[4, 4, 5]$ и целевого элемента $9$, включающий четыре вида обрезки. Проанализируйте рисунок и комментарии в коде, чтобы лучше понять весь процесс поиска и как работают различные операции обрезки. + +![Процесс поиска с возвратом для реализации задачи о сумме подмножеств II](subset_sum_problem.assets/subset_sum_ii.png) diff --git a/ru/docs/chapter_hashing/hash_algorithm.md b/ru/docs/chapter_hashing/hash_algorithm.md index 65c0de8e1..9491d41e7 100644 --- a/ru/docs/chapter_hashing/hash_algorithm.md +++ b/ru/docs/chapter_hashing/hash_algorithm.md @@ -1,60 +1,60 @@ -# Хеш-алгоритмы +# Алгоритмы хеширования -В предыдущих двух разделах были рассмотрены принципы работы хеш-таблиц и методы обработки хеш-коллизий. Однако ни открытая адресация, ни цепная адресация **не могут уменьшить возникновение хеш-коллизий, они лишь обеспечивают нормальную работу хеш-таблицы при возникновении коллизий**. +В предыдущих разделах были рассмотрены принципы работы хеш-таблиц и методы обработки хеш-конфликтов. Однако ни открытая, ни цепная адресация не могут уменьшить вероятность возникновения хеш-конфликтов, **они лишь обеспечивают корректную работу хеш-таблицы при их возникновении**. -Если хеш-коллизии возникают слишком часто, производительность хеш-таблицы резко ухудшается. Как показано на рисунке ниже, для хеш-таблицы с цепной адресацией в идеальном случае пары ключ-значение равномерно распределены по всем корзинам, достигая наилучшей эффективности поиска; в худшем случае все пары ключ-значение хранятся в одной корзине, и временная сложность деградирует до $O(n)$. +Если хеш-конфликты происходят слишком часто, производительность хеш-таблицы резко снижается. Как показано на рис. 6.8, для хеш-таблицы с цепной адресацией в идеальном случае пары ключ--значение равномерно распределены по всем корзинам, что обеспечивает наилучшую эффективность поиска. В худшем случае все пары ключ--значение хранятся в одной корзине, и временная сложность повышается до $O(n)$. -![Лучший и худший случаи хеш-коллизий](../assets/hash_collision_best_worst_condition.png) +![Лучший и худший случаи хеш-конфликтов](hash_algorithm.assets/hash_collision_best_worst_condition.png) -**Распределение пар ключ-значение определяется хеш-функцией**. Вспомним этапы вычисления хеш-функции: сначала вычисляется хеш-значение, затем берется остаток от деления на длину массива: +**Распределение пар ключ--значение определяется хеш-функцией**. Вспомним этапы вычисления хеш-функции: сначала вычисляется хеш-значение, затем берется остаток от деления на длину массива. ```shell index = hash(key) % capacity ``` -Рассматривая приведенную выше формулу, когда емкость хеш-таблицы `capacity` фиксирована, **хеш-алгоритм `hash()` определяет выходное значение**, а следовательно, и распределение пар ключ-значение в хеш-таблице. +Из этого выражения видно, что при фиксированной емкости хеш-таблицы `capacity` **алгоритм хеширования** `hash()` **определяет выходное значение**, которое, в свою очередь, определяет распределение пар ключ--значение в хеш-таблице. -Это означает, что для снижения вероятности возникновения хеш-коллизий следует сосредоточить внимание на разработке хеш-алгоритма `hash()`. +Это означает, что для снижения вероятности возникновения хеш-конфликтов следует сосредоточиться на разработке алгоритма хеширования `hash()`. -## Цели хеш-алгоритма +## Цели алгоритма хеширования -Для реализации структуры данных хеш-таблицы, которая является "быстрой и стабильной", хеш-алгоритм должен обладать следующими характеристиками. +Для создания быстрой и надежной структуры данных хеш-таблицы алгоритм хеширования должен обладать следующими характеристиками. -- **Детерминированность**: для одного и того же входа хеш-алгоритм всегда должен выдавать один и тот же выход. Только так можно обеспечить надежность хеш-таблицы. -- **Высокая эффективность**: процесс вычисления хеш-значения должен быть достаточно быстрым. Чем меньше вычислительные затраты, тем выше практичность хеш-таблицы. -- **Равномерное распределение**: хеш-алгоритм должен обеспечивать равномерное распределение пар ключ-значение в хеш-таблице. Чем равномернее распределение, тем ниже вероятность хеш-коллизий. +- **Детерминированность**: для одинакового ввода алгоритм хеширования должен всегда давать одинаковый вывод. Это необходимо для обеспечения надежности работы хеш-таблицы. +- **Высокая эффективность**: процесс вычисления хеш-значения должен быть достаточно быстрым. Чем меньше вычислительные затраты, тем выше практическая ценность хеш-таблицы. +- **Равномерное распределение**: алгоритм хеширования должен обеспечивать равномерное распределение пар ключ--значение в хеш-таблице. Чем равномернее распределение, тем ниже вероятность хеш-конфликтов. -На самом деле хеш-алгоритмы помимо реализации хеш-таблиц широко применяются и в других областях. +На практике алгоритмы хеширования применяются не только для реализации хеш-таблиц, но и в других областях. -- **Хранение паролей**: для защиты паролей пользователей система обычно не хранит пароли в открытом виде, а хранит их хеш-значения. Когда пользователь вводит пароль, система вычисляет хеш-значение введенного пароля и сравнивает его с сохраненным хеш-значением. Если они совпадают, пароль считается правильным. -- **Проверка целостности данных**: отправитель данных может вычислить хеш-значение данных и отправить его вместе с данными; получатель может заново вычислить хеш-значение полученных данных и сравнить его с полученным хеш-значением. Если они совпадают, данные считаются целостными. +- **Хранение паролей**: для защиты паролей пользователей система обычно не хранит пароли в открытом виде, а сохраняет их хеш-значения. Когда пользователь вводит пароль, система вычисляет его хеш-значение и сравнивает с сохраненным. Если они совпадают, пароль считается правильным. +- **Проверка целостности данных**: отправитель данных может вычислить хеш-значение данных и отправить его вместе с данными. Получатель может заново вычислить хеш-значение полученных данных и сравнить его с полученным. Если они совпадают, данные считаются неизмененными. -Для криптографических приложений, чтобы предотвратить обратное восстановление исходного пароля из хеш-значения и другие виды обратной инженерии, хеш-алгоритм должен обладать более высоким уровнем безопасности. +В криптографических приложениях для предотвращения обратного вычисления исходного пароля из хеш-значения и других видов обратной инженерии алгоритм хеширования должен обладать дополнительными характеристиками. -- **Односторонность**: невозможно восстановить какую-либо информацию о входных данных из хеш-значения. -- **Устойчивость к коллизиям**: должно быть крайне сложно найти два разных входа, дающих одинаковое хеш-значение. -- **Эффект лавины**: небольшое изменение входа должно приводить к значительному и непредсказуемому изменению выхода. +- **Необратимость**: невозможность извлечь какую-либо информацию о входных данных из хеш-значения. +- **Устойчивость к коллизиям**: должно быть крайне сложно найти два различных входа, дающих одинаковое хеш-значение. +- **Эффект лавины**: небольшие изменения на входе должны приводить к значительным и непредсказуемым изменениям на выходе. -Обратите внимание, что **"равномерное распределение" и "устойчивость к коллизиям" являются двумя независимыми понятиями**, удовлетворение равномерному распределению не обязательно означает устойчивость к коллизиям. Например, при случайном входе `key` хеш-функция `key % 100` может давать равномерно распределенный выход. Однако этот хеш-алгоритм слишком прост, все `key` с одинаковыми последними двумя цифрами дают одинаковый выход, поэтому мы можем легко восстановить подходящий `key` из хеш-значения и таким образом взломать пароль. +Следует отметить, что **«равномерное распределение» и «устойчивость к коллизиям»** -- это два независимых понятия, и выполнение одного из них не обязательно означает выполнение другого. Например, хеш-функция `key % 100` при случайном вводе значения `key` может давать равномерное распределение. Однако этот алгоритм хеширования слишком прост, и все ключи с одинаковыми последними двумя цифрами будут иметь одинаковый вывод, что позволяет легко извлечь пригодные ключи из хеш-значения и взломать пароль. -## Разработка хеш-алгоритма +## Разработка алгоритма хеширования -Разработка хеш-алгоритма — это сложная задача, требующая учета многих факторов. Однако для некоторых нетребовательных сценариев мы также можем разработать простые хеш-алгоритмы. +Создание хеш-алгоритмов представляет собой сложную задачу, требующую учета множества факторов. Однако для некоторых несложных сценариев можно разработать простые хеш-алгоритмы. -- **Аддитивное хеширование**: складываются ASCII-коды каждого символа входа, полученная сумма используется как хеш-значение. -- **Мультипликативное хеширование**: используя независимость умножения, на каждом шаге умножается на константу, ASCII-коды различных символов накапливаются в хеш-значении. -- **XOR-хеширование**: каждый элемент входных данных накапливается в хеш-значении через операцию XOR. -- **Ротационное хеширование**: ASCII-код каждого символа накапливается в хеш-значении, перед каждым накоплением выполняется операция вращения хеш-значения. +- **Аддитивный хеш**: складываются ASCII-коды каждого символа входных данных, полученная сумма используется в качестве хеш-значения. +- **Мультипликативный хеш**: используя свойство некоррелированности умножения, на каждом шаге значение хеша умножается на константу, и в результат добавляется ASCII-код очередного символа. +- **Хеш с использованием операции XOR**: каждый элемент входных данных накапливается в хеш-значении с помощью операции XOR. +- **Ротационный хеш**: ASCII-коды каждого символа накапливаются в хеш-значении, при этом перед каждым накоплением выполняется операция ротации хеш-значения. ```src [file]{simple_hash}-[class]{}-[func]{rot_hash} ``` -Можно заметить, что последним шагом каждого хеш-алгоритма является взятие остатка от деления на большое простое число $1000000007$, чтобы обеспечить нахождение хеш-значения в подходящем диапазоне. Стоит задуматься, почему важно брать остаток от деления именно на простое число, или каковы недостатки взятия остатка от деления на составное число? Это интересный вопрос. +Можно заметить, что последним шагом в каждом из хеш-алгоритмов является взятие остатка от деления на большое простое число $1000000007$, чтобы гарантировать, что хеш-значение находится в допустимом диапазоне. Интересно, почему акцент делается на взятии остатка от деления именно на простое число, и какие недостатки могут быть при делении на составное число? -Сначала дадим вывод: **использование большого простого числа в качестве модуля может максимально обеспечить равномерное распределение хеш-значений**. Поскольку простое число не имеет общих делителей с другими числами, это может уменьшить периодические паттерны, возникающие из-за операции взятия остатка, тем самым избегая хеш-коллизий. +Ответ: **использование большого простого числа в качестве модуля позволяет обеспечить максимально равномерное распределение хеш-значений**. Поскольку простое число не имеет общих делителей с другими числами, это позволяет уменьшить периодические закономерности, возникающие из-за операции взятия остатка, и избежать хеш-конфликтов. -Приведем пример: предположим, мы выбираем составное число $9$ в качестве модуля, оно делится на $3$, тогда все `key`, делящиеся на $3$, будут отображаться в три хеш-значения: $0$, $3$, $6$. +Например, если выбрать в качестве модуля составное число $9$, которое делится на $3$, то все ключи, делящиеся на $3$, будут отображаться в хеш-значения $0$, $3$ и $6$: $$ \begin{aligned} @@ -64,7 +64,7 @@ $$ \end{aligned} $$ -Если входные `key` как раз удовлетворяют такому арифметическому распределению данных, то хеш-значения будут группироваться, усиливая хеш-коллизии. Теперь предположим, что `modulus` заменяется на простое число $13$. Поскольку между `key` и `modulus` нет общих делителей, равномерность выходных хеш-значений значительно улучшится. +Если входные ключи имеют такую арифметическую прогрессию, то хеш-значения будут сгруппированы, что умножит хеш-конфликты. Теперь если заменить `modulus` на простое число $13$, то, поскольку между ключами и модулем нет общих делителей, равномерность распределения хеш-значений значительно улучшится: $$ \begin{aligned} @@ -74,20 +74,337 @@ $$ \end{aligned} $$ -Следует отметить, что если можно гарантировать, что `key` распределены случайно и равномерно, то выбор простого или составного числа в качестве модуля не имеет значения, оба могут выдавать равномерно распределенные хеш-значения. Однако когда распределение `key` имеет определенную периодичность, взятие остатка от составного числа с большей вероятностью приводит к группировке. +Следует отметить, что если ключи распределены случайно и равномерно, то выбор простого или составного числа в качестве модуля не имеет значения -- оба варианта обеспечат равномерное распределение хеш-значений. Однако при наличии периодичности в распределении ключей использование составного числа в качестве модуля может привести к кластеризации. -В целом мы обычно выбираем простое число в качестве модуля, причем это простое число должно быть достаточно большим, чтобы максимально устранить периодические паттерны и повысить надежность хеш-алгоритма. +В общем случае выбирается простое число в качестве модуля, и это простое число должно быть достаточно большим, чтобы максимально устранить периодические закономерности и повысить устойчивость хеш-алгоритма. ## Распространенные хеш-алгоритмы -Нетрудно заметить, что представленные выше простые хеш-алгоритмы довольно "хрупкие" и далеки от достижения целей разработки хеш-алгоритмов. Например, поскольку сложение и XOR удовлетворяют коммутативному закону, аддитивное хеширование и XOR-хеширование не могут различать строки с одинаковым содержимым, но разным порядком, что может усилить хеш-коллизии и вызвать некоторые проблемы безопасности. +Нетрудно заметить, что описанные выше простые хеш-алгоритмы довольно хрупкие и далеки от достижения целей создания хеш-алгоритмов. Например, сложение и операция XOR удовлетворяют коммутативному закону, поэтому соответствующие хеш-алгоритмы не различают строки с одинаковым содержанием, но разным порядком символов, что может усилить хеш-конфликты и вызвать некоторые проблемы с безопасностью. На практике обычно используются стандартные хеш-алгоритмы, такие как MD5, SHA-1, SHA-2 и SHA-3. Они могут отображать входные данные произвольной длины в хеш-значения фиксированной длины. -За последнее столетие хеш-алгоритмы находятся в процессе постоянного совершенствования и оптимизации. Часть исследователей стремится повысить производительность хеш-алгоритмов, другая часть исследователей и хакеров пытается найти проблемы безопасности хеш-алгоритмов. В таблице ниже представлены распространенные хеш-алгоритмы, используемые на практике. +На протяжении почти ста лет хеш-алгоритмы постоянно обновляются и оптимизируются. Одни исследователи стремятся повысить производительность, другие исследователи и хакеры сосредоточены на поиске проблем с безопасностью. В табл. 6.2 представлены распространенные хеш-алгоритмы, используемые в реальных приложениях. -- MD5 и SHA-1 неоднократно подвергались успешным атакам, поэтому они отвергнуты различными приложениями безопасности. -- SHA-256 из серии SHA-2 является одним из самых безопасных хеш-алгоритмов, для него до сих пор не было успешных атак, поэтому он часто используется в различных приложениях и протоколах безопасности. -- SHA-3 по сравнению с SHA-2 имеет меньшие затраты на реализацию и более высокую вычислительную эффективность, но в настоящее время его охват использования не так широк, как у серии SHA-2. +- В MD5 и SHA-1 были обнаружены многочисленные уязвимости, поэтому они не используются в сценариях, в которых требуется высокий уровень безопасности. +- SHA-256 из серии SHA-2 является одним из самых безопасных хеш-алгоритмов, до сих пор не было обнаружено ни одной уязвимости, поэтому он часто используется в различных приложениях и протоколах безопасности. +- SHA-3 имеет меньшие затраты на реализацию и более высокую вычислительную эффективность по сравнению с SHA-2, но в настоящее время его использование не так широко распространено, как серии SHA-2. -

Таблица Таблица   Распространенные хеш-алгоритмы

+ +| | MD5 | SHA-1 | SHA-2 | SHA-3 | +| -------- | ------------------------------ | ---------------- | ---------------------------- | ------------------- | +| Год появления | 1992 | 1995 | 2002 | 2008 | +| Длина вывода | 128 бит | 160 бит | 256/512 бит | 224/256/384/512 бит | +| Хеш-конфликты | Много | Много | Мало | Мало | +| Уровень безопасности | Низкий, есть известные уязвимости | Низкий, есть известные уязвимости | Высокий | Высокий | +| Применение | Устарел, но еще используется для проверки целостности данных | Устарел | Проверка транзакций в криптовалюте, цифровые подписи и т. д. | Может использоваться в качестве замены SHA-2 | + +## Хеш-значения для структур данных + +Ключи в хеш-таблице могут быть представлены в виде целых чисел, дробей или строк. Языки программирования обычно предоставляют встроенные хеш-алгоритмы для своих типов данных, чтобы вычислять индексы корзин в хеш-таблице. Например, в Python можно вызвать функцию `hash()` для вычисления хеш-значений для различных типов данных. + +- Хеш-значение целых чисел и булевых величин совпадает с их значением. +- Хеш-значение дробных чисел и строк вычисляется по более сложному алгоритму, заинтересованные читатели могут изучить его самостоятельно. +- Хеш-значение кортежа получается путем хеширования каждого элемента и объединения этих хеш-значений в одно. +- Хеш-значение объекта генерируется на основе его адреса в памяти. Путем переопределения метода хеширования объекта можно реализовать генерацию хеша на основе его содержимого. + +!!! tip + + Обратите внимание, что в разных языках программирования встроенные функции вычисления хеш-значений определяются и реализуются по-разному. + +=== "Python" + + ```python title="built_in_hash.py" + num = 3 + hash_num = hash(num) + # Хеш-значение целого числа 3 равно 3 + + bol = True + hash_bol = hash(bol) + # Хеш-значение булевой величины True равно 1 + + dec = 3.14159 + hash_dec = hash(dec) + # Хеш-значение дробного числа 3.14159 равно 326484311674566659 + + str = "Hello 算法" + hash_str = hash(str) + # Хеш-значение строки "Hello 算法" равно 4617003410720528961 + + tup = (12836, "小哈") + hash_tup = hash(tup) + # Хеш-значение кортежа (12836, '小哈') равно 1029005403108185979 + + obj = ListNode(0) + hash_obj = hash(obj) + # Хеш-значение объекта равно 274267521 + ``` + +=== "C++" + + ```cpp title="built_in_hash.cpp" + int num = 3; + size_t hashNum = hash()(num); + // Хеш-значение целого числа 3 равно 3 + + bool bol = true; + size_t hashBol = hash()(bol); + // Хеш-значение булевой величины 1 равно 1 + + double dec = 3.14159; + size_t hashDec = hash()(dec); + // Хеш-значение дробного числа 3.14159 равно 4614256650576692846 + + string str = "Hello 算法"; + size_t hashStr = hash()(str); + // Хеш-значение строки "Hello 算法" равно 15466937326284535026 + + // В C++ встроенная функция std:hash() предоставляет только вычисление хеш-значений базовых типов данных + // Для массивов и объектов нужно реализовывать вычисление хеш-значений самостоятельно + ``` + +=== "Java" + + ```java title="built_in_hash.java" + int num = 3; + int hashNum = Integer.hashCode(num); + // Хеш-значение целого числа 3 равно 3 + + boolean bol = true; + int hashBol = Boolean.hashCode(bol); + // Хеш-значение булевой величины true равно 1231 + + double dec = 3.14159; + int hashDec = Double.hashCode(dec); + // Хеш-значение дробного числа 3.14159 равно -1340954729 + + String str = "Hello 算法"; + int hashStr = str.hashCode(); + // Хеш-значение строки "Hello 算法" равно -727081396 + + Object[] arr = { 12836, "小哈" }; + int hashTup = Arrays.hashCode(arr); + // Хеш-значение массива [12836, 小哈] равно 1151158 + + ListNode obj = new ListNode(0); + int hashObj = obj.hashCode(); + // Хеш-значение объекта узла utils.ListNode@7dc5e7b4 равно 2110121908 + ``` + +=== "C#" + + ```csharp title="built_in_hash.cs" + int num = 3; + int hashNum = num.GetHashCode(); + // Хеш-значение целого числа 3 равно 3; + + bool bol = true; + int hashBol = bol.GetHashCode(); + // Хеш-значение булевой величины true равно 1; + + double dec = 3.14159; + int hashDec = dec.GetHashCode(); + // Хеш-значение дробного числа 3.14159 равно -1340954729; + + string str = "Hello 算法"; + int hashStr = str.GetHashCode(); + // Хеш-значение строки "Hello 算法" равно -586107568; + + object[] arr = [12836, "小哈"]; + int hashTup = arr.GetHashCode(); + // Хеш-значение массива [12836, 小哈] равно 42931033; + + ListNode obj = new(0); + int hashObj = obj.GetHashCode(); + // Хеш-значение объекта узла 0 равно 39053774; + ``` + +=== "Go" + + ```go title="built_in_hash.go" + // Go не предоставляет встроенную функцию hash code + ``` + +=== "Swift" + + ```swift title="built_in_hash.swift" + let num = 3 + let hashNum = num.hashValue + // Хеш-значение целого числа 3 равно 9047044699613009734 + + let bol = true + let hashBol = bol.hashValue + // Хеш-значение булевой величины true равно -4431640247352757451 + + let dec = 3.14159 + let hashDec = dec.hashValue + // Хеш-значение дробного числа 3.14159 равно -2465384235396674631 + + let str = "Hello 算法" + let hashStr = str.hashValue + // Хеш-значение строки "Hello 算法" равно -7850626797806988787 + + let arr = [AnyHashable(12836), AnyHashable("小哈")] + let hashTup = arr.hashValue + // Хеш-значение массива [AnyHashable(12836), AnyHashable("小哈")] равно -2308633508154532996 + + let obj = ListNode(x: 0) + let hashObj = obj.hashValue + // Хеш-значение объекта узла utils.ListNode равно -2434780518035996159 + ``` + +=== "JS" + + ```javascript title="built_in_hash.js" + // JavaScript не предоставляет встроенную функцию hash code + ``` + +=== "TS" + + ```typescript title="built_in_hash.ts" + // TypeScript не предоставляет встроенную функцию hash code + ``` + +=== "Dart" + + ```dart title="built_in_hash.dart" + int num = 3; + int hashNum = num.hashCode; + // Хеш-значение целого числа 3 равно 34803 + + bool bol = true; + int hashBol = bol.hashCode; + // Хеш-значение булевой величины true равно 1231 + + double dec = 3.14159; + int hashDec = dec.hashCode; + // Хеш-значение дробного числа 3.14159 равно 2570631074981783 + + String str = "Hello 算法"; + int hashStr = str.hashCode; + // Хеш-значение строки "Hello 算法" равно 468167534 + + List arr = [12836, "小哈"]; + int hashArr = arr.hashCode; + // Хеш-значение массива [12836, 小哈] равно 976512528 + + ListNode obj = new ListNode(0); + int hashObj = obj.hashCode; + // Хеш-значение объекта узла Instance of 'ListNode' равно 1033450432 + ``` + +=== "Rust" + + ```rust title="built_in_hash.rs" + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + let num = 3; + let mut num_hasher = DefaultHasher::new(); + num.hash(&mut num_hasher); + let hash_num = num_hasher.finish(); + // Хеш-значение целого числа 3 равно 568126464209439262 + + let bol = true; + let mut bol_hasher = DefaultHasher::new(); + bol.hash(&mut bol_hasher); + let hash_bol = bol_hasher.finish(); + // Хеш-значение булевой величины true равно 4952851536318644461 + + let dec: f32 = 3.14159; + let mut dec_hasher = DefaultHasher::new(); + dec.to_bits().hash(&mut dec_hasher); + let hash_dec = dec_hasher.finish(); + // Хеш-значение дробного числа 3.14159 равно 2566941990314602357 + + let str = "Hello 算法"; + let mut str_hasher = DefaultHasher::new(); + str.hash(&mut str_hasher); + let hash_str = str_hasher.finish(); + // Хеш-значение строки "Hello 算法" равно 16092673739211250988 + + let arr = (&12836, &"小哈"); + let mut tup_hasher = DefaultHasher::new(); + arr.hash(&mut tup_hasher); + let hash_tup = tup_hasher.finish(); + // Хеш-значение кортежа (12836, "小哈") равно 1885128010422702749 + + let node = ListNode::new(42); + let mut hasher = DefaultHasher::new(); + node.borrow().val.hash(&mut hasher); + let hash = hasher.finish(); + // Хеш-значение объекта узла RefCell { value: ListNode { val: 42, next: None } } равно 15387811073369036852 + ``` + +=== "C" + + ```c title="built_in_hash.c" + // C не предоставляет встроенную функцию hash code + ``` + +=== "Kotlin" + + ```kotlin title="built_in_hash.kt" + val num = 3 + val hashNum = num.hashCode() + // Хеш-значение целого числа 3 равно 3 + + val bol = true + val hashBol = bol.hashCode() + // Хеш-значение булевой величины true равно 1231 + + val dec = 3.14159 + val hashDec = dec.hashCode() + // Хеш-значение дробного числа 3.14159 равно -1340954729 + + val str = "Hello 算法" + val hashStr = str.hashCode() + // Хеш-значение строки "Hello 算法" равно -727081396 + + val arr = arrayOf(12836, "小哈") + val hashTup = arr.hashCode() + // Хеш-значение массива [12836, 小哈] равно 189568618 + + val obj = ListNode(0) + val hashObj = obj.hashCode() + // Хеш-значение объекта узла utils.ListNode@1d81eb93 равно 495053715 + ``` + +=== "Ruby" + + ```ruby title="built_in_hash.rb" + num = 3 + hash_num = num.hash + # Хеш-значение целого числа 3 равно -4385856518450339636 + + bol = true + hash_bol = bol.hash + # Хеш-значение булевой величины true равно -1617938112149317027 + + dec = 3.14159 + hash_dec = dec.hash + # Хеш-значение дробного числа 3.14159 равно -1479186995943067893 + + str = "Hello 算法" + hash_str = str.hash + # Хеш-значение строки "Hello 算法" равно -4075943250025831763 + + tup = [12836, '小哈'] + hash_tup = tup.hash + # Хеш-значение кортежа (12836, '小哈') равно 1999544809202288822 + + obj = ListNode.new(0) + hash_obj = obj.hash + # Хеш-значение объекта узла # равно 4302940560806366381 + ``` + +??? pythontutor "可视化运行" + + https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + +Во многих языках программирования **только неизменяемые объекты могут использоваться в качестве ключей в хеш-таблице**. Если список (динамический массив) используется в качестве ключа, то при изменении его содержимого хеш-значение также изменится, и мы не сможем найти исходное значение. + +Хотя переменные-члены пользовательских объектов (например, узлов связного списка) могут быть изменяемыми, сами объекты можно хешировать. **Это связано с тем**, **что хеш-значение объекта обычно генерируется на основе его адреса в памяти**, и даже если содержимое объекта изменяется, адрес остается неизменным, а значит, и хеш-значение также остается прежним. + +Возможно, вы заметили, что при запуске программы в разных окнах выводимые хеш-значения отличаются. **Это связано с тем, что интерпретатор Python при каждом запуске добавляет случайное значение «соли» к функции хеширования строк**. Такой подход эффективно предотвращает атаки типа HashDoS и повышает безопасность хеш-алгоритма. diff --git a/ru/docs/chapter_tree/array_representation_of_tree.md b/ru/docs/chapter_tree/array_representation_of_tree.md index 642da8828..aa4498f8d 100644 --- a/ru/docs/chapter_tree/array_representation_of_tree.md +++ b/ru/docs/chapter_tree/array_representation_of_tree.md @@ -1,4 +1,4 @@ -# Представление двоичного дерева с помощью массивом +# Представление двоичного дерева с помощью массива При представлении в виде списка единицей хранения двоичного дерева является узел `TreeNode`, а узлы соединяются между собой указателями. В предыдущем разделе были рассмотрены основные операции с двоичным деревом, представленным в виде списка. @@ -8,61 +8,153 @@ Сначала рассмотрим простой пример. Если дано идеальное двоичное дерево и все его узлы хранятся в массиве в порядке обхода по уровням, то каждому узлу соответствует уникальный индекс массива. -На основе свойств обхода по уровням можно вывести формулу отображения между индексом родительского узла и индексами дочерних узлов: **если индекс узла равен $i$, то индекс его левого дочернего узла равен $2i + 1$, а индекс правого дочернего узла равен $2i + 2$**. На рисунке ниже показаны отношения отображения между индексами различных узлов. +На основе свойств обхода по уровням можно вывести формулу соответствия между индексами родительского и дочерних узлов: **если индекс узла равен $i$, то индекс его левого дочернего узла равен $2i + 1$, а правого -- $2i + 2$**. На рис. 7.12 показаны отношения соответствия между индексами узлов. - - +![Представление идеального двоичного дерева с помощью массива](array_representation_of_tree.assets/array_representation_binary_tree.png) -**Роль формулы отображения эквивалентна ссылкам на узлы (указателям) в списке**. Для любого узла в массиве мы можем получить доступ к его левому (правому) дочернему узлу с помощью формулы отображения. +**Формула соответствия играет роль, аналогичную ссылкам (указателям) в списке**. Имея любой узел в массиве, можно с помощью формулы получить доступ к его левому и правому дочерним узлам. ## Представление произвольного двоичного дерева - - +Идеальное двоичное дерево является частным случаем. Обычно на средних уровнях двоичного дерева присутствует много пустых значений `None`. Но последовательность обхода по уровням не содержит этих `None`, поэтому невозможно по этой последовательности определить количество и расположение пустых значений. **Это означает, что существует множество структур двоичных деревьев, соответствующих данной последовательности обхода по уровням**. - - +Для такого неидеального двоичного дерева вышеописанный метод представления с помощью массива уже не работает, см. рис. 7.13. - - +![Для одной последовательности обхода по уровням существует несколько возможных вариантов двоичного дерева](array_representation_of_tree.assets/array_representation_without_empty.png) - - +Для решения этой проблемы **можно явно записать все значения `None` в последовательности обхода по уровням**. После такой обработки последовательность обхода по уровням уже может однозначно представлять двоичное дерево, как показано на рис. 7.14. Ниже приведен пример кода. - - +=== "Python" - - + ```python title="" + # Представление двоичного дерева с помощью массива + # Использование None для обозначения пустых мест + tree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15] + ``` - - +=== "C++" - - + ```cpp title="" + /* Представление двоичного дерева с помощью массива */ + // Использование INT_MAX для обозначения пустых мест + vector tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15}; + ``` - - +=== "Java" - - + ```java title="" + /* Представление двоичного дерева с помощью массива */ + // Использование обертки Integer для типа int, чтобы можно было использовать null для обозначения пустых мест + Integer[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 }; + ``` - - +=== "C#" - - + ```csharp title="" + /* Представление двоичного дерева с помощью массива */ + // Использование nullable типа int?, чтобы можно было использовать null для обозначения пустых мест + int?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; + ``` + +=== "Go" + + ```go title="" + /* Представление двоичного дерева с помощью массива */ + // Использование типа any для среза, чтобы можно было использовать nil для обозначения пустых мест + tree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15} + ``` + +=== "Swift" + + ```swift title="" + /* Представление двоичного дерева с помощью массива */ + // Использование nullable типа Int?, чтобы можно было использовать nil для обозначения пустых мест + let tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15] + ``` + +=== "JS" + + ```javascript title="" + /* Представление двоичного дерева с помощью массива */ + // Использование null для обозначения пустых мест + let tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; + ``` + +=== "TS" + + ```typescript title="" + /* Представление двоичного дерева с помощью массива */ + // Использование null для обозначения пустых мест + let tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; + ``` + +=== "Dart" + + ```dart title="" + /* Представление двоичного дерева с помощью массива */ + // Использование nullable типа int?, чтобы можно было использовать null для обозначения пустых мест + List tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; + ``` + +=== "Rust" + + ```rust title="" + /* Представление двоичного дерева с помощью массива */ + // Использование None для обозначения пустых мест + let tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)]; + ``` + +=== "C" + + ```c title="" + /* Представление двоичного дерева с помощью массива */ + // Использование максимального значения int для обозначения пустых мест, поэтому значения узлов не могут быть равны INT_MAX + int tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15}; + ``` + +=== "Kotlin" + + ```kotlin title="" + /* Представление двоичного дерева с помощью массива */ + // Использование null для обозначения пустых мест + val tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 ) + ``` + +=== "Ruby" + + ```ruby title="" + ### Представление двоичного дерева с помощью массива ### + # Использование nil для обозначения пустых мест + tree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15] + ``` + +![Представление произвольного двоичного дерева с помощью массива](array_representation_of_tree.assets/array_representation_with_empty.png) + +Стоит отметить, что **совершенное двоичное дерево очень удобно представлять с помощью массива**. Вспоминая определение совершенного двоичного дерева, `None` появляются только на самом нижнем уровне и в правой части, поэтому **все значения `None` обязательно находятся в конце последовательности обхода по уровням**. + +Это означает, что при использовании массива для представления совершенного двоичного дерева можно опустить хранение всех `None`, что очень удобно. На рис. 7.15 приведен пример такого представления. + +![Представление совершенного двоичного дерева с помощью массива](array_representation_of_tree.assets/array_representation_complete_binary_tree.png) + +В коде ниже реализуется двоичное дерево, основанное на представлении с помощью массива, включая следующие операции. + +- Для заданного узла получение его значения, левого и правого дочернего узла, родительского узла. +- Получение последовательностей обхода в прямом, симметричном, обратном порядке и в порядке обхода по уровням. + +```src +[file]{array_binary_tree}-[class]{array_binary_tree}-[func]{} +``` ## Преимущества и ограничения - - +Представление двоичного дерева с помощью массива имеет следующие преимущества: - - +- Массив хранится в непрерывной области памяти, что хорошо для кеширования. Скорость доступа и обхода достаточно высока. +- Не требуется хранение указателей, что экономит пространство. +- Позволяет выполнять произвольный доступ к узлам. - - +Однако представление с помощью массива имеет и некоторые ограничения: - - \ No newline at end of file +- Хранение в массиве требует непрерывной области памяти, поэтому не подходит для хранения деревьев с очень большим объемом данных. +- Добавление и удаление узлов требует выполнения операций вставки и удаления в массиве, которые менее эффективны. +- Когда в двоичном дереве содержится много значений `None`, доля данных узлов в массиве низка, что приводит к низкой эффективности использования пространства. diff --git a/ru/docs/index.md b/ru/docs/index.md index 203c2aede..3c7fe648d 100644 --- a/ru/docs/index.md +++ b/ru/docs/index.md @@ -1,3 +1,5 @@ - +# Hello Алгоритмы -# Hello 算法 \ No newline at end of file +Учебник по структурам данных и алгоритмам с анимированными иллюстрациями и исполняемым кодом. + +[Начать чтение](chapter_hello_algo/)