Update ru translation

This commit is contained in:
krahets
2026-01-23 00:59:30 +08:00
parent 8071daddaa
commit 19dd60a6be
4 changed files with 579 additions and 108 deletions

View File

@@ -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}
```
请确认是否需要这样处理,或者提供正确的俄文对应内容。
При вводе в этот код массива $[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)

View File

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

View File

@@ -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) -->
![Представление идеального двоичного дерева с помощью массива](array_representation_of_tree.assets/array_representation_binary_tree.png)
**Роль формулы отображения эквивалентна ссылкам на узлы (указателям) в списке**. Для любого узла в массиве мы можем получить доступ к его левому (правому) дочернему узлу с помощью формулы отображения.
**Формула соответствия играет роль, аналогичную ссылкам (указателям) в списке**. Имея любой узел в массиве, можно с помощью формулы получить доступ к его левому и правому дочерним узлам.
## Представление произвольного двоичного дерева
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:完美二叉树是一个特例,在二叉树的中间层通常存在许多 `None` 。由于层序遍历序列并不包含这些 `None` ,因此我们无法仅凭该序列来推测 `None` 的数量和分布位置。**这意味着存在多种二叉树结构都符合该层序遍历序列**。 -->
Идеальное двоичное дерево является частным случаем. Обычно на средних уровнях двоичного дерева присутствует много пустых значений `None`. Но последовательность обхода по уровням не содержит этих `None`, поэтому невозможно по этой последовательности определить количество и расположение пустых значений. **Это означает, что существует множество структур двоичных деревьев, соответствующих данной последовательности обхода по уровням**.
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:如下图所示,给定一棵非完美二叉树,上述数组表示方法已经失效。 -->
Для такого неидеального двоичного дерева вышеописанный метод представления с помощью массива уже не работает, см. рис. 7.13.
<!-- 🔴 俄文版缺失图片 -->
<!-- 中文原文:![层序遍历序列对应多种二叉树可能性](array_representation_of_tree.assets/array_representation_without_empty.png) -->
![Для одной последовательности обхода по уровням существует несколько возможных вариантов двоичного дерева](array_representation_of_tree.assets/array_representation_without_empty.png)
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:为了解决此问题,**我们可以考虑在层序遍历序列中显式地写出所有 `None`** 。如下图所示,这样处理后,层序遍历序列就可以唯一表示二叉树了。示例代码如下: -->
Для решения этой проблемы **можно явно записать все значения `None` в последовательности обхода по уровням**. После такой обработки последовательность обхода по уровням уже может однозначно представлять двоичное дерево, как показано на рис. 7.14. Ниже приведен пример кода.
<!-- 🔴 俄文版缺失代码示例 -->
<!-- 中文原文:=== "Python" ... [多语言代码示例] -->
=== "Python"
<!-- 🔴 俄文版缺失图片 -->
<!-- 中文原文:![任意类型二叉树的数组表示](array_representation_of_tree.assets/array_representation_with_empty.png) -->
```python title=""
# Представление двоичного дерева с помощью массива
# Использование None для обозначения пустых мест
tree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]
```
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:值得说明的是,**完全二叉树非常适合使用数组来表示**。回顾完全二叉树的定义,`None` 只出现在最底层且靠右的位置,**因此所有 `None` 一定出现在层序遍历序列的末尾**。 -->
=== "C++"
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:这意味着使用数组表示完全二叉树时,可以省略存储所有 `None` ,非常方便。下图给出了一个例子。 -->
```cpp title=""
/* Представление двоичного дерева с помощью массива */
// Использование INT_MAX для обозначения пустых мест
vector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};
```
<!-- 🔴 俄文版缺失图片 -->
<!-- 中文原文:![完全二叉树的数组表示](array_representation_of_tree.assets/array_representation_complete_binary_tree.png) -->
=== "Java"
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:以下代码实现了一棵基于数组表示的二叉树,包括以下几种操作。 -->
```java title=""
/* Представление двоичного дерева с помощью массива */
// Использование обертки Integer для типа int, чтобы можно было использовать null для обозначения пустых мест
Integer[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };
```
<!-- 🔴 俄文版缺失列表 -->
<!-- 中文原文:- 给定某节点,获取它的值、左(右)子节点、父节点。- 获取前序遍历、中序遍历、后序遍历、层序遍历序列。 -->
=== "C#"
<!-- 🔴 俄文版缺失代码引用 -->
<!-- 中文原文:```src [file]{array_binary_tree}-[class]{array_binary_tree}-[func]{} ``` -->
```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<int?> 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]{}
```
## Преимущества и ограничения
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:二叉树的数组表示主要有以下优点。 -->
Представление двоичного дерева с помощью массива имеет следующие преимущества:
<!-- 🔴 俄文版缺失列表 -->
<!-- 中文原文:- 数组存储在连续的内存空间中,对缓存友好,访问与遍历速度较快。- 不需要存储指针,比较节省空间。- 允许随机访问节点。 -->
- Массив хранится в непрерывной области памяти, что хорошо для кеширования. Скорость доступа и обхода достаточно высока.
- Не требуется хранение указателей, что экономит пространство.
- Позволяет выполнять произвольный доступ к узлам.
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:然而,数组表示也存在一些局限性。 -->
Однако представление с помощью массива имеет и некоторые ограничения:
<!-- 🔴 俄文版缺失列表 -->
<!-- 中文原文:- 数组存储需要连续内存空间,因此不适合存储数据量过大的树。- 增删节点需要通过数组插入与删除操作实现,效率较低。- 当二叉树中存在大量 `None` 时,数组中包含的节点数据比重较低,空间利用率较低。 -->
- Хранение в массиве требует непрерывной области памяти, поэтому не подходит для хранения деревьев с очень большим объемом данных.
- Добавление и удаление узлов требует выполнения операций вставки и удаления в массиве, которые менее эффективны.
- Когда в двоичном дереве содержится много значений `None`, доля данных узлов в массиве низка, что приводит к низкой эффективности использования пространства.

View File

@@ -1,3 +1,5 @@
<!-- TODO: 此章节内容待翻译 -->
# Hello Алгоритмы
# Hello 算法
Учебник по структурам данных и алгоритмам с анимированными иллюстрациями и исполняемым кодом.
[Начать чтение](chapter_hello_algo/)