This commit is contained in:
krahets
2026-07-19 03:42:10 +08:00
parent cb5235bc3f
commit 2b8612bc41
238 changed files with 7477 additions and 3 deletions
@@ -0,0 +1,112 @@
---
comments: true
---
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
# 4.6 &nbsp; Упражнения
## 4.6.1 &nbsp; Вопросы для самопроверки
### 1. &nbsp; Как массив и связный список находят элемент
В массиве и односвязном списке по порядку хранятся `[A, B, C, D, E]`. Требуется прочитать 4-й элемент `D`.
<!-- numbered-subquestions -->
1. Какой индекс можно напрямую использовать в массиве?
2. Через какие узлы нужно последовательно пройти по `next` в односвязном списке, начиная с головного узла `A`?
3. Как меняется число шагов в каждой структуре по мере удаления нужного элемента от начала? Какая структура лучше подходит для многократного доступа по позиции и почему?
??? success "Ответ"
1. Если индексация начинается с 0, индекс 4-го элемента равен 3, поэтому в массиве можно напрямую обратиться к `arr[3]`.
2. В односвязном списке нужно начинать с головы. Путь имеет вид `A → B → C → D`, то есть по `next` нужно перейти 3 раза.
3. Массив позволяет напрямую определить положение элемента по адресу начала и индексу, поэтому временная сложность доступа по позиции равна $O(1)$.
Чтобы обратиться к $k$-му узлу односвязного списка, нужно начать с головного узла и перейти по `next` $k-1$ раз,
что в худшем случае требует $O(n)$ времени.
Здесь сравнивается только доступ по позиции; это не означает, что связный список медленнее при всех операциях.
### 2. &nbsp; Как вставить элемент в массив и связный список
В массиве и односвязном списке хранятся `A, B, C, D`. Требуется вставить `X` после `B`.
- емкость массива равна 5, его текущее состояние — `[A, B, C, D, _]`;
- связный список имеет вид `A → B → C → D`, и ссылка на узел `B` уже получена.
<!-- numbered-subquestions -->
1. Какие элементы массива нужно переместить? Запишите массив после вставки.
2. В каком порядке следует изменить `X.next` и `B.next`? Запишите связный список после вставки.
3. Почему при сравнении эффективности вставки важно уточнить, что «ссылка на узел B уже получена»?
??? success "Ответ"
1. В массиве сначала нужно сдвинуть `D` на одну позицию вправо, затем так же сдвинуть `C` и, наконец, поместить `X` по индексу 2.
Получится `[A, B, X, C, D]`.
2. Изначально `B.next` указывает на `C`. Сначала следует выполнить `X.next = B.next`, чтобы `X` указывал на `C`,
а затем `B.next = X`. Получится `A → B → X → C → D`.
Если сначала перезаписать `B.next`, не сохранив прежнюю связь, узел `C` можно потерять.
3. Если положение `B` известно, для вставки в связный список достаточно изменить две связи, что занимает $O(1)$ времени.
Если же `B` еще нужно искать от головы списка, сам поиск может потребовать $O(n)$ времени.
### 3. &nbsp; Как растет емкость списка
Список на основе массива сейчас содержит `[A, B, C]`, его длина `size = 3`, а емкость `capacity = 4`.
При нехватке места емкость нового массива увеличивается в 2 раза.
<!-- numbered-subquestions -->
1. Чему будут равны длина и емкость списка после добавления `D`? Потребуется ли увеличить емкость?
2. Затем добавляют `E`. Какой станет емкость и сколько прежних элементов потребуется скопировать?
3. Длина базового массива неизменна. Почему тогда кажется, что емкость списка может расти?
??? success "Ответ"
1. `D` можно поместить в последнюю свободную ячейку. Теперь содержимое равно `[A, B, C, D]`,
`size = 4`, `capacity = 4`, и увеличивать емкость не требуется.
2. Для `E` свободного места уже нет, поэтому нужно создать новый массив емкостью 8,
скопировать в него 4 прежних элемента `A, B, C, D`, а затем добавить `E`.
После этого `size = 5`, `capacity = 8`.
3. Исходный массив сам по себе не удлиняется. Список создает новый массив большего размера, копирует прежние элементы
и затем использует новый массив как базовое хранилище. Поэтому для пользователя емкость выглядит увеличившейся.
## 4.6.2 &nbsp; Задачи по программированию
### 1. &nbsp; Прибавление единицы к большому числу в массиве
Массив `digits` хранит слева направо цифры неотрицательного целого числа. Например, `[3, 0, 8]` обозначает 308.
Число 0 представлено как `[0]`; у всех остальных входных данных первая цифра не равна 0.
Смоделируйте сложение столбиком в десятичной системе: увеличьте это число на 1 и верните результат в том же формате массива.
Массив `digits` можно изменять непосредственно. Если перед первой цифрой появляется новый перенос, можно вернуть более длинный массив.
??? tip "Подсказки"
1. Как при сложении столбиком, начните с последней цифры массива
2. Если текущая цифра меньше 9, увеличьте ее на один и сразу верните результат
3. Если текущая цифра равна 9, замените ее на 0; если все цифры равны 9, добавьте 1 в начало
[LeetCode](https://leetcode.com/problems/plus-one/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
### 2. &nbsp; Разворот односвязного списка
Дан головной узел `head` односвязного списка. Каждый узел содержит значение и поле `next`, указывающее на следующий узел.
Итеративно разверните все связи между узлами и верните новый головной узел.
Не создавайте новые узлы связного списка.
??? tip "Подсказки"
1. Нарисуйте на бумаге три связанных узла и два указателя prev и cur
2. Прежде чем изменить cur.next, сохраните исходный следующий узел в nxt
3. Развернув cur.next, выполните prev = cur, затем cur = nxt и продолжайте с очередным узлом исходного списка
[LeetCode](https://leetcode.com/problems/reverse-linked-list/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
@@ -20,3 +20,4 @@ icon: material/view-list-outline
- [4.3 &nbsp; Список](list.md)
- [4.4 &nbsp; Оперативная память и кэш *](ram_and_cache.md)
- [4.5 &nbsp; Резюме](summary.md)
- [4.6 &nbsp; Упражнения](exercises.md)
+93
View File
@@ -0,0 +1,93 @@
---
comments: true
---
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
# 13.6 &nbsp; Упражнения
## 13.6.1 &nbsp; Вопросы для самопроверки
### 1. &nbsp; Пропустит ли этот алгоритм некоторые перестановки
Алгоритм поиска с возвратом пытается построить все перестановки, перебирая числа в порядке `1, 2, 3`. Выбрав число `x`, он:
1. добавляет `x` в конец текущего пути;
2. помечает `x` как «использованное»;
3. рекурсивно заполняет следующую позицию.
После возврата из рекурсии ученик удалил `x` только из конца пути и перешел к следующему числу.
<!-- numbered-subquestions -->
1. Какую перестановку алгоритм получит первой? Сможет ли он получить все 6 перестановок?
2. Достаточно ли перед возвратом на предыдущий уровень удалить число из конца пути? Если нет, что еще нужно сделать и почему?
??? success "Ответ"
1. Первой получится перестановка `[1, 2, 3]`, но алгоритм не сможет получить все перестановки. Хотя после возврата путь становится короче,
числа 1, 2 и 3 по-прежнему помечены как «использованные», поэтому в последующих ветвях не останется доступных чисел.
2. Нет. После удаления `x` из конца пути нужно также снова пометить `x` как «неиспользованное».
Текущий путь и отметки «использовано» вместе описывают состояние поиска. При выборе изменяются обе части состояния, поэтому при возврате нужно восстановить обе,
чтобы другие ветви могли снова выбрать `x`.
### 2. &nbsp; Важен ли порядок выбора чисел
Дан упорядоченный массив `[2, 3, 5]` и целевое значение 5. Каждое число разрешено выбирать повторно.
Алгоритм требует, чтобы числа в каждом пути поиска располагались только в неубывающем порядке.
<!-- numbered-subquestions -->
1. Какие различные комбинации можно получить?
2. Почему один и тот же набор чисел не нужно повторно перебирать в разном порядке? Какую роль играет ограничение «по неубыванию»?
3. Текущий путь равен `[3]`, до цели не хватает 2, а следующий возможный элемент равен 3. Почему теперь можно прекратить проверку всех следующих элементов на этом уровне?
??? success "Ответ"
1. Различные комбинации: `[2, 3]` и `[5]`.
2. В этой задаче `[2, 3]` и `[3, 2]` считаются одной комбинацией: порядок выбора чисел не влияет на ответ.
Требование располагать числа в пути по неубыванию позволяет сразу исключить из поиска повторные комбинации наподобие `[3, 2]`.
3. До цели осталось 2, а возможный элемент 3 уже больше 2. Поскольку массив упорядочен,
все последующие элементы будут еще больше и также не смогут войти в текущую комбинацию. Поэтому проверку этого уровня можно сразу завершить.
### 3. &nbsp; Куда можно поставить следующего ферзя
Ферзи расставляются по строкам на доске `4 × 4`; индексы строк и столбцов начинаются с 0.
Ферзи уже стоят на позициях `(0, 1)` и `(1, 3)`. Следующего ферзя нужно поставить в строке 2.
<!-- numbered-subquestions -->
1. Какие столбцы исключаются из-за того, что в них уже есть ферзь?
2. Какие из оставшихся позиций исключаются из-за расположения на одной диагонали с другим ферзем?
3. Какие позиции в строке 2 еще можно попробовать?
??? success "Ответ"
1. В столбцах 1 и 3 уже стоят ферзи, поэтому позиции `(2, 1)` и `(2, 3)` исключаются.
2. Среди оставшихся позиций `(2, 2)` находится на одной диагонали с `(1, 3)`, поэтому ее также нужно исключить.
Позиция `(2, 0)` не совпадает по столбцу ни с одним из уже поставленных ферзей и не лежит с ними на одной диагонали.
3. В строке 2 можно попробовать только позицию `(2, 0)`.
Этот шаг показывает лишь, что текущее расположение допустимо. Если завершить доску затем не удастся, потребуется вернуться и попробовать другие, более ранние варианты.
## 13.6.2 &nbsp; Задачи по программированию
### 1. &nbsp; Все перестановки различных элементов
Массив целых чисел `nums` содержит хотя бы один элемент, и все его элементы попарно различны.
Перечислите все возможные порядки, в которых каждый элемент используется ровно один раз, и верните каждый порядок как отдельный массив.
Порядок самих перестановок в результате не имеет значения.
Используйте поиск с возвратом и булев массив, отмечающий, выбран ли элемент из каждой позиции входного массива в текущую перестановку.
??? tip "Подсказки"
1. Глубина рекурсии показывает, какую по счету позицию перестановки нужно заполнить
2. На каждом уровне пробуйте только еще не использованные элементы
3. Когда длина пути станет равна длине `nums`, добавьте в ответ копию пути
[LeetCode](https://leetcode.com/problems/permutations/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
+1
View File
@@ -20,3 +20,4 @@ icon: material/map-marker-path
- [13.3 &nbsp; Задача о сумме подмножеств](subset_sum_problem.md)
- [13.4 &nbsp; Задача о n ферзях](n_queens_problem.md)
- [13.5 &nbsp; Резюме](summary.md)
- [13.6 &nbsp; Упражнения](exercises.md)
@@ -0,0 +1,116 @@
---
comments: true
---
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
# 2.6 &nbsp; Упражнения
## 2.6.1 &nbsp; Вопросы для самопроверки
### 1. &nbsp; Временная и пространственная сложность итерации и рекурсии
Обе функции ниже вычисляют $1 + 2 + \dots + n$ (считайте, что $n \ge 1$). Присвойте `n` значение 4,
ответьте на вопросы в порядке фактического выполнения программы, а затем сравните эффективность двух вариантов.
```python
def sum_iter(n):
s = 0
for i in range(1, n + 1):
s += i
return s
def sum_recur(n):
if n == 1:
return 1
return n + sum_recur(n - 1)
```
<!-- numbered-subquestions -->
1. Какие значения принимает переменная `s` после каждой итерации при выполнении `sum_iter(4)`?
2. Какие функции поочередно вызываются при выполнении `sum_recur(4)`? Как формируется результат при возврате, начиная с самого глубокого вызова?
3. Каковы временная и пространственная сложности каждого варианта? Обоснуйте ответ, опираясь на процессы выполнения из вопросов 1 и 2.
??? success "Ответ"
1. Переменная цикла `i` поочередно принимает значения `1, 2, 3, 4`, а `s` после каждой итерации становится равной
`1, 3, 6, 10`. Поэтому `sum_iter(4)` возвращает 10.
2. Функции вызываются в следующем порядке:
`sum_recur(4) → sum_recur(3) → sum_recur(2) → sum_recur(1)`.
`sum_recur(1)` возвращает 1, после чего следующие уровни поочередно получают `2 + 1 = 3`, `3 + 3 = 6` и `4 + 6 = 10`.
В самой глубокой точке все 4 вызова функции еще не завершены.
3. В обеих функциях число итераций или вызовов пропорционально $n$, поэтому их временная сложность равна $O(n)$.
Пространственная сложность различается: итеративный вариант использует постоянное число переменных, поэтому она равна $O(1)$;
в рекурсивном варианте предыдущие вызовы ожидают возврата результата до достижения условия завершения, поэтому в стеке вызовов одновременно хранится до $n$ вызовов.
Его пространственная сложность равна $O(n)$.
При анализе пространственной сложности нужно учитывать не только переменные в коде, но и память, занимаемую рекурсивными вызовами.
### 2. &nbsp; Временная сложность трех фрагментов кода
Во всех трех фрагментах кода входные данные — положительное целое число $n$. Расположите фрагменты в порядке возрастания временной сложности и укажите сложность каждого из них.
```python
# Фрагмент 1
s = 0
for i in range(n):
s += i
# Фрагмент 2
s = 0
for i in range(n):
for j in range(i, n):
s += j
# Фрагмент 3
while n > 1:
n = n // 2
```
??? success "Ответ"
Порядок от меньшей сложности к большей: фрагмент 3 — $O(\log n)$, фрагмент 1 — $O(n)$, фрагмент 2 — $O(n^2)$.
В фрагменте 3 на каждой итерации $n$ уменьшается вдвое, поэтому цикл выполняется примерно $\log_2 n$ раз.
Цикл во фрагменте 1 выполняется ровно $n$ раз. Число итераций внутреннего цикла во фрагменте 2 последовательно равно
$n,n-1,\dots,1$, а их сумма — $n(n+1)/2$, поэтому сложность квадратичная.
### 3. &nbsp; Какой способ разворота экономнее по памяти
Все элементы массива `nums` можно расположить в обратном порядке двумя способами:
<!-- numbered-subquestions -->
1. создать новый массив `res` той же длины, скопировать в него элементы в обратном порядке и вернуть его;
2. задать два индекса `i` и `j`, перемещать их от начала и конца массива к середине и попарно менять местами `nums[i]` и `nums[j]`.
Какова пространственная сложность каждого способа? Какой из них выполняет операцию «на месте»?
??? success "Ответ"
1. Требуется вспомогательный массив той же длины, что и входной, поэтому пространственная сложность равна $O(n)$.
2. Используются только две индексные переменные,
поэтому пространственная сложность равна $O(1)$, а операция выполняется на месте.
Обратите внимание: разворот на месте изменяет входной массив,
поэтому его следует предпочитать только тогда, когда менять входные данные разрешено. Если исходный массив нужно сохранить, затрат на копирование в способе 1 не избежать.
## 2.6.2 &nbsp; Задачи по программированию
### 1. &nbsp; Число Фибоначчи
Последовательность Фибоначчи задается так: $F(0)=0$, $F(1)=1$, а при $n\ge2$
выполняется $F(n)=F(n-1)+F(n-2)$.
Дано неотрицательное целое число `n`. Вычислите и верните $F(n)$ с помощью цикла, не используя рекурсию.
??? tip "Подсказки"
1. Сначала отдельно обработайте случаи, когда n равно 0 или 1
2. Для вычисления следующего члена нужны только два предыдущих, поэтому хранить всю последовательность не требуется
3. Обновляя две переменные, не перезапишите слишком рано старое значение, которое еще понадобится
[LeetCode](https://leetcode.com/problems/fibonacci-number/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
@@ -20,3 +20,4 @@ icon: material/timer-sand
- [2.3 &nbsp; Временная сложность](time_complexity.md)
- [2.4 &nbsp; Пространственная сложность](space_complexity.md)
- [2.5 &nbsp; Резюме](summary.md)
- [2.6 &nbsp; Упражнения](exercises.md)
@@ -0,0 +1,92 @@
---
comments: true
---
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
# 3.6 &nbsp; Упражнения
## 3.6.1 &nbsp; Вопросы для самопроверки
### 1. &nbsp; Связи между данными в повседневных ситуациях
Для каждого из трех примеров выберите по характеру связей между данными один вариант: «линейная структура», «древовидная структура» или «сетевая структура». Объясните свой выбор.
<!-- numbered-subquestions -->
1. Ученики стоят в ряд, и каждого интересуют только стоящие непосредственно перед ним и за ним;
2. школа организована по уровням «школа → параллель → класс»;
3. городские дороги соединяют множество перекрестков: из одного перекрестка можно попасть в несколько других, а дороги могут образовывать циклы.
??? success "Ответ"
1. Это линейная структура. Кроме первого и последнего в ряду, каждый ученик соседствует ровно с одним человеком впереди и одним сзади, а связи вытянуты в одну линию.
2. Это древовидная структура. Каждый класс относится к одной параллели, а каждая параллель — к школе; связи образуют уровни сверху вниз.
3. Это сетевая структура. Один перекресток может быть связан с несколькими другими, а дороги могут образовывать циклы, поэтому связи нельзя выстроить в единую последовательность или строгую иерархию.
Определяя структуру, сначала рассматривайте связи между элементами, а не объем памяти, который они занимают.
### 2. &nbsp; Как сохранить логический порядок в памяти
Логический порядок `A → B → C` можно сохранить в памяти двумя упрощенными способами:
- вариант 1: `A, B, C` размещены в ячейках памяти с номерами `20, 21, 22` соответственно;
- вариант 2: `A, B, C` размещены в ячейках с номерами `20, 7, 31` соответственно, причем `A` хранит расположение `B`, а `B` — расположение `C`.
<!-- numbered-subquestions -->
1. Какой вариант использует хранение в непрерывном, а какой — в разрозненном пространстве памяти?
2. Какой вариант ближе к массиву, а какой — к связному списку?
3. Почему вариант 2 по-прежнему задает логический порядок `A → B → C`, хотя номера ячеек памяти не упорядочены по величине?
??? success "Ответ"
1. Вариант 1 использует соседние ячейки памяти, поэтому хранение происходит в непрерывном пространстве. Узлы в варианте 2 находятся в разных местах, поэтому они хранятся в разрозненном пространстве.
2. Вариант 1 ближе к массиву, а вариант 2 — к связному списку.
3. Логический порядок задается связями, записанными между узлами, а не величиной номеров ячеек памяти.
По адресу, который хранит `A`, можно найти `B`, а затем по адресу из `B` найти `C`, поэтому элементы по-прежнему доступны в порядке `A, B, C`.
Это также показывает, что логическая и физическая структуры — два разных взгляда на одни и те же данные.
### 3. &nbsp; Типы данных и структура записей о домашних заданиях
Учебная группа записала в порядке рассадки, сдали ли домашнее задание 4 ученика:
`[true, false, true, true]`
<!-- numbered-subquestions -->
1. Какой базовый тип данных подходит для каждого элемента?
2. Какую логическую структуру образуют эти 4 элемента, расположенные в ряд по порядку рассадки?
3. Если затем записывать оценки за задание каждого ученика как `[90, 0, 85, 100]`, изменится «тип содержимого» или «способ организации» данных? Объясните ответ.
??? success "Ответ"
1. Каждый элемент выражает только «да» или «нет», поэтому подходит логический тип `bool`.
2. Элементы расположены по порядку рассадки и образуют линейную структуру, которую можно хранить в массиве.
3. Изменится тип содержимого: логические значения будут заменены целыми числами. Способ организации не изменится:
данные по-прежнему расположены в ряд по порядку рассадки и могут храниться в массиве как линейная структура.
Базовый тип данных описывает, «что хранится», а структура данных — «как организованы данные».
## 3.6.2 &nbsp; Задачи по программированию
### 1. &nbsp; Число единиц в двоичной записи
Дано неотрицательное целое число `n`. Подсчитайте количество единиц в его двоичной записи.
Используйте побитовые операции. Не преобразовывайте двоичную запись в строку и не применяйте встроенную функцию, которая напрямую подсчитывает единицы.
??? tip "Подсказки"
1. Выражение n & 1 извлекает крайний правый бит n и позволяет проверить, равен ли он 1
2. Сдвиг вправо на один разряд отбрасывает текущий крайний правый бит; в большинстве языков для этого используется оператор >>
3. Реализовав проверку каждого бита со сдвигом вправо, обратите внимание: n & (n - 1) превращает в 0 крайнюю правую единицу в n
[LeetCode](https://leetcode.com/problems/number-of-1-bits/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
+1
View File
@@ -20,3 +20,4 @@ icon: material/shape-outline
- [3.3 &nbsp; Кодирование чисел *](number_encoding.md)
- [3.4 &nbsp; Кодирование символов *](character_encoding.md)
- [3.5 &nbsp; Резюме](summary.md)
- [3.6 &nbsp; Упражнения](exercises.md)
@@ -0,0 +1,102 @@
---
comments: true
---
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
# 12.6 &nbsp; Упражнения
## 12.6.1 &nbsp; Вопросы для самопроверки
### 1. &nbsp; Какие задачи подходят для метода «разделяй и властвуй»
Ученик хочет решить следующие задачи способом «разделить на две половины, решить каждую отдельно, затем объединить результаты».
Для каждой задачи выберите одно из утверждений: «подходит для метода “разделяй и властвуй”», «разделение возможно, но не уменьшает общий объем работы» или «половины нельзя решить независимо». Объясните свой выбор.
<!-- numbered-subquestions -->
1. Отсортировать неупорядоченный массив;
2. найти максимальный элемент массива;
3. последовательно выполнить набор операций стека `push(x)` и `pop()`, выводя элемент, полученный при каждой операции `pop()`.
??? success "Ответ"
1. Подходит: массив делится пополам, обе половины независимо сортируются, а затем сливаются за $O(n)$ — именно так работает сортировка слиянием.
2. Разделение возможно, но оно не уменьшит общий объем работы: в обеих половинах в сумме все равно нужно проверить все $n$ элементов,
поэтому, как и при прямом обходе, потребуется $O(n)$ времени.
3. Половины нельзя решить независимо: содержимое стека в начале второй половины операций зависит от результата первой,
поэтому решить их независимо, не зная результатов друг друга, невозможно.
### 2. &nbsp; Как быстрое возведение в степень уменьшает число вычислений
Рекурсивная функция ниже вычисляет $x^n$ методом «разделяй и властвуй»:
```python
def fast_pow(x, n):
if n == 0:
return 1
half = fast_pow(x, n // 2)
if n % 2 == 0:
return half * half
return half * half * x
```
Функция вызывается как `fast_pow(3, 5)`.
<!-- numbered-subquestions -->
1. Какие значения последовательно принимает параметр `n` при рекурсивных вызовах?
2. Какие значения поочередно возвращаются на каждом уровне, начиная с самого глубокого?
3. Почему результат нужно сначала сохранить в `half`, а не дважды записывать `fast_pow(x, n // 2)`?
??? success "Ответ"
1. Параметр последовательно принимает значения `5 → 2 → 1 → 0`: на каждом шаге показатель степени уменьшается вдвое до достижения условия завершения.
2. При `n = 0` возвращается 1; при `n = 1` возвращается $1×1×3=3$;
при `n = 2` — $3×3=9$; при `n = 5` — $9×9×3=243$.
3. Если записать `fast_pow(x, n // 2)` по обе стороны умножения, два рекурсивных вызова будут решать одну и ту же подзадачу.
Когда результат сохраняется в `half`, на каждом уровне выполняется только один рекурсивный вызов, а глубина рекурсии составляет примерно $\log n$;
два вызова привели бы к множеству повторных вычислений.
### 3. &nbsp; Разделение левого и правого поддеревьев по последовательностям обхода
Двоичное дерево без повторяющихся узлов имеет следующие последовательности прямого и симметричного обходов:
- прямой обход: `[A, B, D, E, C]`
- симметричный обход: `[D, B, E, A, C]`
Выполните только разделение на уровне корневого узла: продолжать рекурсию и рисовать все дерево не требуется.
<!-- numbered-subquestions -->
1. Какой узел является корневым?
2. Какие участки симметричного обхода соответствуют левому и правому поддеревьям?
3. Какие участки прямого обхода соответствуют левому и правому поддеревьям? Какие непосредственные дочерние узлы имеет корень?
??? success "Ответ"
1. Первый узел прямого обхода является корневым, поэтому корень — `A`.
2. Узел `A` делит симметричный обход на две части: левому поддереву соответствует `[D, B, E]`, правому — `[C]`.
3. Левое поддерево содержит 3 узла, поэтому следующие после корня `A` 3 элемента прямого обхода относятся к левому поддереву:
`[B, D, E]`. Оставшийся участок `[C]` относится к правому поддереву.
Следовательно, левый дочерний узел корня — `B`, а правый — `C`.
## 12.6.2 &nbsp; Задачи по программированию
### 1. &nbsp; Быстрое возведение в степень
Даны вещественное число `x` и целое число `n`. Вычислите $x^n$, не используя встроенную функцию возведения в степень.
Примените рекурсивный метод «разделяй и властвуй»: каждый раз уменьшайте показатель степени вдвое и повторно используйте уже вычисленный результат подзадачи.
В этой задаче $x^0=1$, в том числе при `x = 0`; если `n < 0`, гарантируется, что `x != 0`, а ответ можно преобразовать к $(1/x)^{-n}$.
??? tip "Подсказки"
1. При n, равном 0, ответ равен 1
2. Вычислив x в степени n // 2, сохраните результат в half и не выполняйте рекурсивный вызов второй раз
3. Если n < 0, сначала замените x на 1 / x, а n — на -n; в C++ или Java предварительно преобразуйте n в 64-битное целое число, чтобы избежать переполнения при смене знака у минимального 32-битного целого
[LeetCode](https://leetcode.com/problems/powx-n/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
@@ -20,3 +20,4 @@ icon: material/set-split
- [12.3 &nbsp; Задача построения двоичного дерева](build_binary_tree_problem.md)
- [12.4 &nbsp; Задача о Ханойской башне](hanota_problem.md)
- [12.5 &nbsp; Резюме](summary.md)
- [12.6 &nbsp; Упражнения](exercises.md)
@@ -0,0 +1,117 @@
---
comments: true
---
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
# 14.8 &nbsp; Упражнения
## 14.8.1 &nbsp; Вопросы для самопроверки
### 1. &nbsp; Когда подходит динамическое программирование
Ученик утверждает: «Если можно записать рекуррентное соотношение, нужно использовать динамическое программирование».
Для каждой из трех задач выберите наиболее подходящий из трех вариантов: динамическое программирование; поиск с возвратом; цикл или математическая формула без таблицы `dp`. Укажите одну основную причину.
<!-- numbered-subquestions -->
1. Набрать сумму 6 с помощью монет номиналов `[1, 3, 4]`, используя каждый номинал любое число раз, и найти минимальное число монет;
2. вывести все перестановки `[1, 2, 3]`;
3. вычислить $1 + 2 + \dots + n$.
Для задачи, которой подходит динамическое программирование, также объясните значение `dp[i]`.
??? success "Ответ"
1. Подходит динамическое программирование. Пусть `dp[i]` — минимальное число монет, необходимое для получения суммы `i`.
Для каждой монеты `c`, не превосходящей `i`, значение `dp[i-c] + 1` можно рассматривать как возможный ответ,
а затем выбрать минимум среди всех вариантов. Разные последовательности выбора многократно приводят к одной и той же сумме, а оптимальное решение для большей суммы можно составить из оптимальных решений для меньших сумм.
Для суммы 6 ответ равен 2: `3 + 3`.
2. Следует использовать поиск с возвратом. Задача требует поочередно получить все 6 перестановок; поиск с возвратом позволяет систематически сделать выбор, продолжить поиск,
затем отменить выбор и перейти к другой ветви. Каким бы ни был метод, при фактическом выводе всех перестановок пропустить их перебор нельзя.
3. Достаточно цикла или формулы суммы арифметической прогрессии. Хотя можно записать `S(i) = S(i-1) + i`, при вычислении `S(i)` требуется только одно меньшее значение `S(i-1)`,
а каждая частичная сумма вычисляется один раз. Повторяющихся подзадач нет, поэтому таблица `dp` не нужна. Возможность записать рекуррентное соотношение не означает необходимости динамического программирования.
### 2. &nbsp; Как вычислить одну ячейку таблицы рюкзака
Задача о рюкзаке 0-1: веса предметов `wgt = [1, 2, 3]`, их стоимости `val = [5, 11, 15]`, вместимость рюкзака равна 4.
`dp[i][c]` — максимальная стоимость, которую можно получить, рассматривая только первые $i$ предметов при предельной вместимости рюкзака $c$;
заполнять рюкзак точно до предела не требуется.
Вычислите только состояние `dp[3][4]`. Известно, что `dp[2][4] = 16` и `dp[2][1] = 5`.
<!-- numbered-subquestions -->
1. Какова возможная стоимость, если не брать 3-й предмет?
2. Какая вместимость останется, если взять 3-й предмет, и чему равна возможная стоимость?
3. Какое значение нужно присвоить `dp[3][4]`? Какие предметы ему соответствуют?
??? success "Ответ"
1. Если не брать 3-й предмет, сохраняется результат для первых двух предметов, поэтому возможная стоимость равна `dp[2][4] = 16`.
2. Вес 3-го предмета равен 3, после его добавления остается вместимость $4-3=1$. Возможная стоимость равна
`dp[2][1] + 15 = 5 + 15 = 20`.
3. Сравнив 16 и 20, получаем `dp[3][4] = 20`. Это соответствует выбору 1-го и 3-го предметов:
их общий вес равен $1+3=4$, а общая стоимость — $5+15=20$.
Вычисление этого состояния показывает один выбор в задаче о рюкзаке 0-1: «взять или не взять».
### 3. &nbsp; В каком порядке обновлять вместимость рюкзака
В задаче о рюкзаке 0-1 есть только один предмет весом 2 и стоимостью 5; вместимость рюкзака равна 4.
Каждый предмет разрешено выбрать не более одного раза, а исходный одномерный массив имеет вид `dp = [0, 0, 0, 0, 0]`.
Обрабатывая предмет, ученик обновляет вместимость от 2 до 4:
- после обновления `dp[2]` получается 5;
- после обновления `dp[3]` также получается 5;
- при обновлении `dp[4]` снова используется только что полученное `dp[2]`, поэтому выходит `dp[4] = 10`.
<!-- numbered-subquestions -->
1. Правилен ли результат `dp[4] = 10`? Почему?
2. Чему должно быть равно `dp[4]`, если каждый предмет можно выбрать не более одного раза?
3. При обработке каждого предмета вместимость нужно перебирать от большей к меньшей или от меньшей к большей? Какую проблему предотвращает такой порядок?
??? success "Ответ"
1. Результат неверен. Стоимость 10 означает, что предмет стоимостью 5 положили в рюкзак дважды,
нарушив условие «каждый предмет можно выбрать не более одного раза».
2. В рюкзак можно положить не более одного такого предмета, поэтому правильное значение `dp[4]` равно 5.
3. Вместимость нужно обновлять от большей к меньшей, то есть в порядке 4, 3, 2.
Тогда при вычислении `dp[c]` читаемое значение `dp[c-2]` все еще относится к состоянию до обработки текущего предмета,
и этот предмет нельзя повторно использовать на той же итерации.
## 14.8.2 &nbsp; Задачи по программированию
### 1. &nbsp; Число способов подняться по лестнице
Лестница состоит из `n` ступеней. За один шаг можно подняться только на 1 или 2 ступени, причем нужно попасть точно на ступень `n`.
Вычислите число различных способов подъема. Считайте, что `n >= 1`; способы различаются только последовательностью шагов на 1 и 2 ступени.
Используйте одномерный массив динамического программирования; пока не применяйте оптимизацию памяти, сохраняющую лишь два состояния.
??? tip "Подсказки"
1. Последний шаг на ступень i мог быть только на 1 или на 2 ступени
2. Поэтому dp[i] = dp[i-1] + dp[i-2]
3. Сначала обработайте случаи n, равного 1 и 2, а затем заполняйте таблицу начиная с 3-й ступени
[LeetCode](https://leetcode.com/problems/climbing-stairs/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
### 2. &nbsp; Рюкзак 0-1
Даны массивы одинаковой длины `wgt` и `val`: вес предмета `i` — положительное целое число `wgt[i]`, а его стоимость — неотрицательное целое число `val[i]`.
Вместимость рюкзака `cap` — неотрицательное целое число. Каждый предмет можно выбрать не более одного раза. Найдите максимальную суммарную стоимость предметов,
общий вес которых не превышает `cap`. Используйте одномерное динамическое программирование.
??? tip "Подсказки"
1. Создайте массив dp длины cap + 1, где dp[c] — максимальная стоимость при предельной вместимости c
2. Обрабатывая предмет i, сравните dp[c] для варианта «не брать предмет» и dp[c-wgt[i]] + val[i] для варианта «взять предмет»
3. Вместимость нужно обновлять от большей к меньшей, чтобы не выбрать текущий предмет повторно на одной итерации
@@ -22,3 +22,4 @@ icon: material/table-pivot
- [14.5 &nbsp; Задача о полном рюкзаке](unbounded_knapsack_problem.md)
- [14.6 &nbsp; Задача о расстоянии редактирования](edit_distance_problem.md)
- [14.7 &nbsp; Резюме](summary.md)
- [14.8 &nbsp; Упражнения](exercises.md)
+109
View File
@@ -0,0 +1,109 @@
---
comments: true
---
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
# 9.5 &nbsp; Упражнения
## 9.5.1 &nbsp; Вопросы для самопроверки
### 1. &nbsp; Два представления одного графа
Неориентированный граф содержит 4 вершины `A, B, C, D` и ребра
`A-B, A-C, B-C, C-D`.
<!-- numbered-subquestions -->
1. Запишите его список смежности.
2. Заполните матрицу смежности, содержащую только 0 и 1.
3. Какое представление графа позволяет определить, соединены ли напрямую `A` и `D`, просмотрев лишь один элемент хранилища?
4. Какое представление обычно экономнее по памяти, если в графе много вершин, но мало ребер?
??? success "Ответ"
1. Список смежности:
```text
A: B, C
B: A, C
C: A, B, D
D: C
```
2. Матрица смежности:
| | A | B | C | D |
| --- | --- | --- | --- | --- |
| A | 0 | 1 | 1 | 0 |
| B | 1 | 0 | 1 | 0 |
| C | 1 | 1 | 0 | 1 |
| D | 0 | 0 | 1 | 0 |
3. В матрице смежности достаточно посмотреть ячейку на пересечении строки `A` и столбца `D`, поэтому она удобна для проверки прямой связи любой пары вершин.
4. Если вершин много, а ребер мало, список смежности хранит только существующие ребра и обычно занимает меньше памяти, чем матрица, в которой место отводится каждой паре вершин.
### 2. &nbsp; Порядок обхода в ширину и в глубину
Неориентированный граф содержит вершины `A, B, C, D, E` и ребра
`A-B, A-C, B-D, C-D, D-E`.
Начните с A и при наличии нескольких непосещенных смежных вершин выбирайте их в алфавитном порядке.
<!-- numbered-subquestions -->
1. Запишите порядок обхода в ширину (BFS).
2. Запишите порядок рекурсивного обхода в глубину (DFS).
3. Почему при обоих обходах необходимо отмечать уже посещенные вершины?
??? success "Ответ"
1. Порядок BFS: `A, B, C, D, E`. Сначала посещаются B и C, отстоящие от A на одно ребро,
а затем более далекие D и E.
2. Порядок DFS: `A, B, D, C, E`. Обход последовательно переходит в непосещенную смежную вершину текущей вершины,
поэтому сначала проходит путь `A → B → D → C`. Когда у C не остается новых смежных вершин, обход возвращается к D и посещает E.
3. В графе есть цикл, например `A-B-D-C-A`. Если не отмечать посещенные вершины,
обход может снова и снова проходить по циклу через одни и те же вершины и не завершиться.
### 3. &nbsp; Можно ли посетить весь граф за один BFS
Неориентированный граф содержит вершины `A, B, C, D, E, F` и только ребра
`A-B, B-C, D-E`.
<!-- numbered-subquestions -->
1. Какие вершины можно посетить за один BFS, начав с A?
2. Посещены ли после этого все вершины графа? Почему?
3. Если перебирать все вершины в алфавитном порядке и запускать новый BFS при каждой непосещенной вершине,
какие вершины станут начальными для отдельных запусков? На сколько несвязанных частей (компонент связности) разделен граф?
??? success "Ответ"
1. Из A можно посетить только `A, B, C`.
2. Не все вершины посещены. `D, E` образуют другую связную часть, а F — отдельная вершина;
путей от A до них нет, поэтому из A они недостижимы.
3. Начальными вершинами трех запусков BFS будут последовательно `A, D, F`; они посетят
`{A, B, C}`, `{D, E}` и `{F}` соответственно. Следовательно, граф содержит 3 компоненты связности.
## 9.5.2 &nbsp; Задачи по программированию
### 1. &nbsp; Проверка существования пути в неориентированном графе
Дан неориентированный граф из $n$ вершин, пронумерованных от $0$ до $n-1$. Каждый элемент `[u, v]` массива `edges` обозначает неориентированное ребро между вершинами `u` и `v`.
Также даны начальная вершина `source` и конечная вершина `destination`. Сначала постройте по `edges` список смежности, а затем с помощью BFS или DFS
определите, существует ли путь от `source` до `destination`: если существует, верните `true`, иначе — `false`.
Граф может содержать циклы и быть несвязным.
??? tip "Подсказки"
1. Каждое неориентированное ребро нужно добавить в обоих направлениях
2. Граф может содержать циклы, поэтому обязательно отмечайте уже посещенные вершины
3. Начните с source: встретив destination, верните true; если обход завершился без нее, верните false
[LeetCode](https://leetcode.com/problems/find-if-path-exists-in-graph/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
+1
View File
@@ -19,3 +19,4 @@ icon: material/graphql
- [9.2 &nbsp; Базовые операции графа](graph_operations.md)
- [9.3 &nbsp; Обход графа](graph_traversal.md)
- [9.4 &nbsp; Краткие итоги](summary.md)
- [9.5 &nbsp; Упражнения](exercises.md)
+90
View File
@@ -0,0 +1,90 @@
---
comments: true
---
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
# 15.6 &nbsp; Упражнения
## 15.6.1 &nbsp; Вопросы для самопроверки
### 1. &nbsp; Всегда ли лучше брать самую крупную монету
Даны монеты номиналов `[1, 7, 10]` и целевая сумма 14.
<!-- numbered-subquestions -->
1. Запишите монеты, выбранные по правилу «каждый раз брать наибольший номинал, не превышающий оставшуюся сумму».
2. Существует ли способ использовать меньше монет? Если да, приведите один пример; если нет, объясните почему.
3. Доказывает ли результат, что эта жадная стратегия верна для любых номиналов монет?
??? success "Ответ"
1. Жадный алгоритм последовательно выбирает `10 + 1 + 1 + 1 + 1`, всего 5 монет.
2. Способ с меньшим числом монет существует: `7 + 7`, всего 2 монеты.
3. Нет. Этот контрпример показывает, что для произвольных номиналов выбор наибольшей допустимой в данный момент монеты не всегда дает минимальное число монет;
лучший на текущем шаге выбор может помешать получить более выгодную комбинацию в дальнейшем.
### 2. &nbsp; Какой предмет первым положить в рюкзак
В рюкзак вместимостью 4 килограмма можно положить следующие предметы. Разрешено брать только часть предмета,
а полученная ценность пропорциональна взятому весу.
- предмет A: вес 4 килограмма, ценность 20;
- предмет B: вес 3 килограмма, ценность 18.
<!-- numbered-subquestions -->
1. Какова ценность одного килограмма каждого предмета? Какой предмет нужно брать первым?
2. Заполните рюкзак по жадной стратегии для дробного рюкзака. Какова итоговая ценность?
3. Если предметы можно делить, а рюкзак ограничен по общему весу, что следует сравнивать при выборе: общую ценность или ценность килограмма? Почему?
??? success "Ответ"
1. Ценность килограмма A равна `20 ÷ 4 = 5`, а килограмма B — `18 ÷ 3 = 6`,
поэтому первым нужно взять B с большей удельной ценностью.
2. Сначала берется весь предмет B: он занимает 3 килограмма и дает ценность 18. В рюкзаке остается место для 1 килограмма,
поэтому затем берется 1 килограмм предмета A ценностью 5. Итоговая ценность равна `18 + 5 = 23`.
3. Рюкзак ограничен по общему весу, а предметы можно делить, поэтому нужно сравнивать ценность единицы веса.
Хотя общая ценность A выше, ценность его килограмма ниже, чем у B. Если сначала полностью положить A, получится только ценность 20.
### 3. &nbsp; Как переместить два указателя на следующем шаге
Высоты вертикальных перегородок заданы массивом `[1, 8, 6, 2, 5]`. Для поиска наибольшей вместимости используются два указателя на концах массива.
Вместимость равна произведению высоты более короткой из двух перегородок на расстояние между их индексами.
<!-- numbered-subquestions -->
1. Сначала левый указатель находится по индексу 0, а правый — по индексу 4. Чему равна текущая вместимость? Какой указатель нужно переместить следующим?
2. После одного перемещения, выбранного в вопросе 1, по каким индексам находятся указатели? Чему теперь равна вместимость? Какой указатель нужно переместить следующим?
3. Для текущей пары перегородок можно переместить указатель как более короткой, так и более высокой перегородки. Какое перемещение еще может привести к большей вместимости и почему?
??? success "Ответ"
1. Текущая вместимость равна `min(1, 5) × (4 - 0) = 4`. Левая перегородка короче, поэтому нужно переместить левый указатель.
2. После перемещения левый и правый указатели находятся по индексам 1 и 4. Текущая вместимость равна
`min(8, 5) × (4 - 1) = 15`. Теперь короче правая перегородка, поэтому следующим нужно переместить правый указатель.
3. Большую вместимость еще может дать перемещение указателя более короткой перегородки. При перемещении более высокой перегородки расстояние между ними обязательно уменьшается,
а высота контейнера по-прежнему ограничена оставшейся короткой перегородкой, поэтому вместимость может только остаться прежней или уменьшиться.
Только перемещение короткой перегородки дает возможность встретить более высокую.
## 15.6.2 &nbsp; Задачи по программированию
### 1. &nbsp; Дробный рюкзак
Даны массивы одинаковой длины `wgt` и `val`, где `wgt[i] > 0`, `val[i] >= 0`, а вместимость рюкзака `cap >= 0`.
Каждый предмет имеется в одном экземпляре, но разрешено положить в рюкзак только его часть;
полученная ценность пропорциональна доле веса предмета, которую положили в рюкзак. Используйте жадный алгоритм
и верните максимальную суммарную ценность как вещественное число.
??? tip "Подсказки"
1. Сначала вычислите ценность единицы веса каждого предмета как val[i] / wgt[i]; результат деления должен сохранять дробную часть
2. Чем выше ценность единицы веса предмета, тем раньше его нужно класть в рюкзак
3. Если оставшаяся вместимость меньше веса текущего предмета, возьмите такую его часть, которая точно заполнит рюкзак, и завершите работу
+1
View File
@@ -20,3 +20,4 @@ icon: material/head-heart-outline
- [15.3 &nbsp; Задача о максимальной вместимости](max_capacity_problem.md)
- [15.4 &nbsp; Задача о максимальном произведении разбиения](max_product_cutting_problem.md)
- [15.5 &nbsp; Резюме](summary.md)
- [15.6 &nbsp; Упражнения](exercises.md)
+107
View File
@@ -0,0 +1,107 @@
---
comments: true
---
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
# 6.5 &nbsp; Упражнения
## 6.5.1 &nbsp; Вопросы для самопроверки
### 1. &nbsp; Как искать после хеш-коллизии
Хеш-таблица содержит 5 бакетов и использует хеш-функцию $h(x)=x \bmod 5$. При коллизии элементы последовательно добавляются в список соответствующего бакета.
В таблицу по порядку вставляют `[1, 6, 11, 7]`.
<!-- numbered-subquestions -->
1. Запишите содержимое бакетов с номерами 0–4.
2. В какой бакет сначала попадает поиск числа 6 и какие элементы проверяются по порядку?
3. Исходя из содержимого бакетов в вопросе 1, перезаписали ли позднее вставленные элементы более ранние? Объясните ответ с учетом этого способа разрешения коллизий.
??? success "Ответ"
1. Так как $1\bmod5=6\bmod5=11\bmod5=1$, а $7\bmod5=2$, бакеты имеют вид:
```text
0: []
1: [1, 6, 11]
2: [7]
3: []
4: []
```
2. При поиске числа 6 сначала выбирается бакет 1, затем сравниваются элементы 1 и 6. Искомый элемент находится при втором сравнении.
3. Одинаковые хеш-значения означают лишь попадание элементов в один бакет, но не равенство самих элементов. При цепной адресации все столкнувшиеся элементы сохраняются в бакете,
а при поиске сравниваются по очереди. Поэтому 1, 6 и 11 не перезаписывают друг друга.
### 2. &nbsp; Куда перемещаются элементы при расширении хеш-таблицы
Хеш-таблица с цепной адресацией первоначально содержит 5 бакетов и использует хеш-функцию $h(x)=x\bmod5$.
Ключи `[1, 6, 11]` находятся в бакете 1.
Теперь таблицу расширяют до 7 бакетов, и хеш-функция принимает вид $h(x)=x\bmod7$.
<!-- numbered-subquestions -->
1. Вычислите новые номера бакетов для 1, 6 и 11.
2. Какие бакеты будут непустыми после расширения?
3. Можно ли при расширении скопировать список из прежнего бакета 1 без изменений в новый бакет 1? Обоснуйте ответ результатами вопросов 1 и 2.
??? success "Ответ"
1. Новые номера бакетов:
- $1\bmod7=1$;
- $6\bmod7=6$;
- $11\bmod7=4$.
2. В бакете 1 хранится 1, в бакете 4 — 11, а в бакете 6 — 6. Три ключа больше не находятся в одном бакете.
3. Копировать список без изменений нельзя. Номер бакета вычисляется как остаток от деления ключа на число бакетов. После замены 5 на 7 номер бакета для того же ключа может измениться,
поэтому положение каждого ключа необходимо вычислить заново. Если просто скопировать прежний бакет 1, то последующий поиск по новой формуле направится за 6 в бакет 6, а за 11 — в бакет 4
и не найдет эти ключи.
### 3. &nbsp; Можно ли найти 11 после удаления 6
Хеш-таблица содержит 5 позиций с индексами `04` и использует хеш-функцию $h(x)=x\bmod5$.
При коллизии поиск первого свободного места начинается с вычисленного хеш-функцией индекса и продолжается вправо.
В таблицу по порядку вставляют `[1, 6, 11]`.
<!-- numbered-subquestions -->
1. По какому индексу в итоге находится каждое из трех чисел?
2. Какие индексы проверяются по порядку при поиске 11?
3. Допустим, при удалении 6 его позицию просто освобождают, как если бы она никогда не использовалась, а поиск прекращается при встрече с пустой позицией.
Что произойдет при следующем поиске 11? Будет ли результат правильным? Если нет, как избежать ошибки?
??? success "Ответ"
1. Число 1 находится по индексу 1. Число 6 также отображается в индекс 1, но после коллизии помещается по индексу 2.
Число 11 тоже начинает поиск с индекса 1, пропускает занятые индексы 1 и 2 и в итоге помещается по индексу 3.
2. При поиске 11 последовательно проверяются индексы `1, 2, 3`, и число находится по индексу 3.
3. Если пометить индекс 2 как «никогда не использовавшийся», то поиск 11 остановится на индексе 2 после проверки индекса 1
и ошибочно решит, что числа 11 нет. При удалении следует оставлять отметку «удалено»:
встретив ее, поиск продолжает проверять следующий индекс (после индекса 4 возвращаясь к индексу 0), а при последующей вставке эту позицию можно использовать снова.
## 6.5.2 &nbsp; Задачи по программированию
### 1. &nbsp; Сравнение состава символов в двух строках
Даны две строки `s` и `t`, состоящие только из строчных английских букв.
Символы в `s` можно переставлять произвольным образом, но нельзя добавлять, удалять или заменять.
Определите, можно ли после перестановки получить `t`. Если можно, верните `true`, иначе — `false`.
Используйте хеш-таблицу для подсчета числа вхождений каждой буквы и не сортируйте символы в строках.
??? tip "Подсказки"
1. Если строки имеют разную длину, их состав символов не может совпадать
2. Сохраняйте в хеш-таблице количество каждой буквы; при обходе s увеличивайте соответствующий счетчик на 1
3. При обходе t уменьшайте соответствующий счетчик на 1; состав символов совпадает, только если все счетчики в конце равны 0
[LeetCode](https://leetcode.com/problems/valid-anagram/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
+1
View File
@@ -19,3 +19,4 @@ icon: material/table-search
- [6.2 &nbsp; Хеш-коллизии](hash_collision.md)
- [6.3 &nbsp; Алгоритмы хеширования](hash_algorithm.md)
- [6.4 &nbsp; Резюме](summary.md)
- [6.5 &nbsp; Упражнения](exercises.md)
+92
View File
@@ -0,0 +1,92 @@
---
comments: true
---
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
# 8.5 &nbsp; Упражнения
## 8.5.1 &nbsp; Вопросы для самопроверки
### 1. &nbsp; Как перестраивается куча после добавления числа 10
Массив `[9, 7, 8, 3, 5]` представляет максимальную кучу. Теперь в нее добавляют число 10.
<!-- numbered-subquestions -->
1. Сначала число 10 добавляется в конец массива. Чему равно значение его родительского узла?
2. Выполните упорядочивание кучи снизу вверх, начиная с нового узла, и запишите массив после каждого обмена.
3. Какой элемент в итоге окажется на вершине кучи? Сколько всего обменов выполнено?
??? success "Ответ"
1. После добавления 10 его индекс равен 5, а индекс родительского узла равен
$\lfloor(5-1)/2\rfloor=2$. Значение родительского узла — 8.
2. Число 10 больше 8, поэтому после первого обмена получается `[9, 7, 10, 3, 5, 8]`;
затем 10 больше родительского узла 9, и после второго обмена получается `[10, 7, 9, 3, 5, 8]`.
Число 10 достигло корневого узла, поэтому упорядочивание завершено.
3. В итоге на вершине кучи находится 10. Всего выполнено 2 обмена.
### 2. &nbsp; Проверка отношений между родительскими и дочерними узлами минимальной кучи
Массив `[1, 4, 3, 7, 6, 2]` представляет полное двоичное дерево. В минимальной куче значение каждого родительского узла не превышает значения его дочерних узлов.
Для индекса $i$ индексы левого и правого дочерних узлов равны $2i+1$ и $2i+2$ соответственно.
<!-- numbered-subquestions -->
1. Укажите индексы и значения дочерних узлов для индекса 2.
2. В узле с индексом 2 находится значение 3. Нарушается ли правило минимальной кучи при сравнении этого узла с его дочерним узлом? Если да, какие элементы нужно поменять местами?
3. С учетом ответа на вопрос 2: если правило нарушено, запишите массив после обмена; если не нарушено, объясните, почему обмен не требуется. Затем проверьте остальные отношения между родительскими и дочерними узлами.
??? success "Ответ"
1. Левый дочерний узел индекса 2 имеет индекс 5 и значение 2. Индекс правого дочернего узла равен 6, но длина массива равна 6, поэтому такого узла нет.
2. Значение родительского узла 3 больше значения дочернего узла 2, что нарушает правило минимальной кучи. Нужно поменять местами элементы с индексами 2 и 5.
3. После обмена получается `[1, 4, 2, 7, 6, 3]`. Проверим все отношения:
`1 ≤ 4`, `1 ≤ 2`; `4 ≤ 7`, `4 ≤ 6`; `2 ≤ 3`.
Каждый родительский узел теперь не больше своих дочерних узлов, поэтому правило минимальной кучи выполняется.
### 3. &nbsp; Сохранение трех наибольших чисел в минимальной куче
Чтобы сохранять 3 наибольших числа из потока данных `[4, 1, 7, 3, 8]`, можно поддерживать минимальную кучу размером не более 3.
Сначала последовательно добавьте в минимальную кучу первые 3 числа. Когда куча заполнена, для каждого нового числа:
если оно больше вершины кучи, удалите вершину и добавьте новое число; иначе оставьте кучу без изменений.
Запишите после чтения каждого числа набор чисел, хранящихся в куче, и ее вершину.
Достаточно записывать сохраненные числа как множество; порядок их расположения в массиве кучи указывать не нужно.
??? success "Ответ"
Результаты после чтения каждого числа:
| Прочитанное число | Сохраненные числа | Вершина кучи |
| --- | --- | --- |
| 4 | `{4}` | 4 |
| 1 | `{1, 4}` | 1 |
| 7 | `{1, 4, 7}` | 1 |
| 3 | `{3, 4, 7}` | 3 |
| 8 | `{4, 7, 8}` | 4 |
После заполнения кучи на ее вершине находится наименьшее из сохраненных чисел. Новое число заменяет вершину,
только если оно больше нее. Итоговое множество `{4, 7, 8}` действительно содержит 3 наибольших числа.
## 8.5.2 &nbsp; Задачи по программированию
### 1. &nbsp; k-й наибольший элемент массива
Даны массив целых чисел `nums` и целое число $k$, где $1 \le k \le n$, а $n$ — длина массива. Если расположить массив по убыванию, верните элемент на позиции $k$.
Повторяющиеся элементы учитываются по отдельности. Например, 2-й наибольший элемент массива `[5, 5, 2]` по-прежнему равен 5. Используйте минимальную кучу размером не более $k$.
??? tip "Подсказки"
1. k-й наибольший элемент — наименьший среди k наибольших чисел
2. Добавляйте каждое число в минимальную кучу, а когда ее размер превышает k, извлекайте наименьшее значение
3. После обхода в куче останутся k наибольших чисел, а на ее вершине будет находиться ответ
[LeetCode](https://leetcode.com/problems/kth-largest-element-in-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
+1
View File
@@ -19,3 +19,4 @@ icon: material/family-tree
- [8.2 &nbsp; Построение кучи](build_heap.md)
- [8.3 &nbsp; Задача Top-k](top_k.md)
- [8.4 &nbsp; Резюме](summary.md)
- [8.5 &nbsp; Упражнения](exercises.md)
+101
View File
@@ -0,0 +1,101 @@
---
comments: true
---
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
# 10.7 &nbsp; Упражнения
## 10.7.1 &nbsp; Вопросы для самопроверки
### 1. &nbsp; Как двоичный поиск сужает область поиска
Найдите число 16 в упорядоченном массиве `[2, 5, 8, 12, 16, 23, 38]`.
Используйте двойной замкнутый интервал `[i, j]` и середину
$m=i+(j-i)/2$ с округлением вниз.
Для каждой итерации запишите `(i, j, m)`, средний элемент и способ дальнейшего сужения интервала,
пока цель не будет найдена.
??? success "Ответ"
Процесс поиска по итерациям:
| Итерация | `(i, j, m)` | Средний элемент | Следующий шаг |
| --- | --- | --- | --- |
| 1 | `(0, 6, 3)` | 12 | `12 < 16`, присвоить `i = 4` |
| 2 | `(4, 6, 5)` | 23 | `23 > 16`, присвоить `j = 4` |
| 3 | `(4, 4, 4)` | 16 | Цель найдена, вернуть индекс 4 |
Массив упорядочен, поэтому если значение в середине меньше цели, можно исключить средний элемент и все элементы слева от него;
если оно больше цели — исключить средний элемент и все элементы справа.
### 2. &nbsp; Левая и правая границы повторяющегося элемента
В массиве `[1, 2, 2, 2, 4, 6]` ищут число 2.
Ученик выполнил двоичный поиск, нашел цель по индексу 2, сразу вернул ответ и сказал:
«Индекс 2 — левая граница числа 2».
<!-- numbered-subquestions -->
1. Верно ли утверждение ученика? Чему равны левая и правая границы числа 2? Объясните ответ.
2. В какой части массива нужно продолжить поиск левой границы, если средний элемент равен цели?
3. В какой части нужно продолжить поиск правой границы? Достаточно указать направление, полный процесс поиска записывать не нужно.
??? success "Ответ"
1. Утверждение неверно. Немедленный возврат после обнаружения числа 2 гарантирует лишь, что найден один из таких элементов, но не самый левый или самый правый.
Левая граница в этом массиве имеет индекс 1, а правая — индекс 3.
2. При поиске левой границы, даже если средний элемент равен 2, нужно продолжить поиск слева,
например, при двойном замкнутом интервале присвоить `j = m - 1`.
3. При поиске правой границы после совпадения среднего элемента с 2 нужно продолжить поиск справа,
например, присвоить `i = m + 1`.
### 3. &nbsp; Как выбрать способ поиска для разных данных
Для каждой ситуации выберите подходящий вариант из «линейный поиск, двоичный поиск, хеш-таблица» и объясните свой выбор.
<!-- numbered-subquestions -->
1. Многократно выполнять поиск среди $10^7$ уже упорядоченных и больше не изменяющихся целых чисел, не создавая дополнительных структур данных;
2. многократно проверять наличие ключа в наборе с частыми вставками и удалениями, не поддерживая порядок и не выполняя поиск по диапазону;
3. один раз найти некоторое значение в неупорядоченном массиве.
??? success "Ответ"
1. Двоичный поиск: данные упорядочены и не меняются, сложность равна $O(\log n)$, дополнительная память не нужна.
2. Хеш-таблица: если хеш-функция достаточно равномерно распределяет ключи по бакетам, средняя временная сложность вставки, удаления и поиска по ключу равна $O(1)$.
3. Прямой обход от начала до конца: ради одного поиска сортировка или построение хеш-таблицы все равно потребует предварительно обработать весь массив
и не уменьшит общий объем работы для этой единственной задачи.
Выбор зависит от того, упорядочены ли данные, разрешено ли создавать дополнительную структуру, сколько будет запросов и какие операции нужно выполнять.
## 10.7.2 &nbsp; Задачи по программированию
### 1. &nbsp; Двоичный поиск в упорядоченном массиве
Дан массив целых чисел `nums`, упорядоченный строго по возрастанию, и целевое значение `target`. Найдите `target` с помощью двоичного поиска: если значение присутствует, верните его индекс в массиве; иначе верните -1.
??? tip "Подсказки"
1. Начальный интервал задается как left = 0, right = n - 1; он непуст, пока left <= right
2. Вычисляйте середину как mid = left + (right - left) // 2
3. Если `nums[mid]` меньше `target`, перенесите левую границу в `mid + 1`; если `nums[mid]` больше `target`, перенесите правую границу в `mid - 1`; при равенстве сразу верните ответ
[LeetCode](https://leetcode.com/problems/binary-search/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
### 2. &nbsp; Позиция вставки в упорядоченный массив
Дан массив целых чисел `nums`, упорядоченный строго по возрастанию, и целевое значение `target`.
Если `target` уже находится в массиве, верните его индекс. Иначе верните позицию, в которую нужно вставить `target`, чтобы массив остался строго возрастающим. Ответ может быть равен 0 или длине массива. Используйте двоичный поиск.
??? tip "Подсказки"
1. Ответ может быть равен 0 или длине массива n
2. При двойном замкнутом интервале, если `nums[mid]` больше или равно `target`, присвойте `right = mid - 1` и продолжите проверять позиции левее; иначе присвойте `left = mid + 1`
3. После завершения цикла left будет позицией вставки
[LeetCode](https://leetcode.com/problems/search-insert-position/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
+1
View File
@@ -21,3 +21,4 @@ icon: material/text-search
- [10.4 &nbsp; Стратегии оптимизации хеширования](replace_linear_by_hashing.md)
- [10.5 &nbsp; Переосмысление алгоритмов поиска](searching_algorithm_revisited.md)
- [10.6 &nbsp; Резюме](summary.md)
- [10.7 &nbsp; Упражнения](exercises.md)
+100
View File
@@ -0,0 +1,100 @@
---
comments: true
---
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
# 11.12 &nbsp; Упражнения
## 11.12.1 &nbsp; Вопросы для самопроверки
### 1. &nbsp; Первые итерации сортировки выбором и пузырьком
Дан массив `[4, 2, 5, 1, 3]`. В обоих случаях элементы сортируются по возрастанию.
<!-- numbered-subquestions -->
1. Выполните первые две итерации сортировки выбором, запишите массив после каждой из них и укажите, какие позиции уже определены.
2. Выполните первую итерацию сортировки пузырьком, запишите состояние массива и число обменов, а также укажите, какая позиция уже определена.
??? success "Ответ"
1. Первые две итерации:
| Итерация | Состояние массива | Пояснение |
| --- | --- | --- |
| 1 | `[1, 2, 5, 4, 3]` | Наименьший элемент 1 меняется местами с первым элементом |
| 2 | `[1, 2, 5, 4, 3]` | Значение 2 уже находится по индексу 1, обмен не нужен |
Теперь первые две позиции определены, а дальнейший выбор наименьшего элемента выполняется только в `[5, 4, 3]`.
2. Последовательно сравниваются соседние элементы: 4 и 2 меняются местами; 4 и 5 остаются на месте; 5 и 1 меняются местами; 5 и 3 меняются местами.
Получается `[2, 4, 1, 3, 5]`, всего 3 обмена. Наибольший элемент 5 переместился в конец массива, поэтому последняя позиция определена.
### 2. &nbsp; Меняется ли взаимный порядок равных элементов
В массиве $[2_a, 2_b, 1]$ значения $2_a$ и $2_b$ равны, но нижние индексы отмечают их исходный порядок.
<!-- numbered-subquestions -->
1. Запишите массив после первой итерации сортировки выбором. Изменился ли взаимный порядок $2_a$ и $2_b$?
2. Запишите массив после первой итерации сортировки пузырьком. Изменился ли взаимный порядок $2_a$ и $2_b$?
3. На основании первых двух ответов объясните, чем различаются эти сортировки в сохранении исходного порядка равных элементов.
??? success "Ответ"
1. На первой итерации сортировка выбором находит наименьший элемент 1 и меняет его местами с первым элементом $2_a$, поэтому получается
$[1, 2_b, 2_a]$. Элемент $2_a$ переместился за $2_b$, и их взаимный порядок изменился.
2. Сортировка пузырьком сначала сравнивает $2_a$ и $2_b$. Они равны, поэтому обмена нет. Затем $2_b$ сравнивается с 1 и меняется с ним местами.
После первой итерации получается $[2_a, 1, 2_b]$. Элемент $2_a$ по-прежнему находится перед $2_b$, их взаимный порядок не изменился.
3. В этом примере сортировка выбором меняет исходный порядок равных элементов. Сортировка пузырьком обменивает соседние элементы, только когда левый больше правого.
Равные элементы не меняются местами, поэтому их исходный взаимный порядок сохраняется.
### 3. &nbsp; Сравнение сортировки подсчетом и поразрядной сортировки
Школе нужно упорядочить большое количество номеров учеников, каждый из которых состоит ровно из 8 цифр. Ответьте на вопросы.
<!-- numbered-subquestions -->
1. Сколько итераций потребуется поразрядной сортировке, начинающей с младшего разряда?
2. Почему сортировка подсчетом, использующая весь номер как целое число, подготовит множество неиспользуемых позиций для счетчиков?
3. Какой из двух способов вы выберете для большого количества 8-значных номеров — сортировку подсчетом или поразрядную сортировку? Обоснуйте выбор с учетом первых двух ответов.
??? success "Ответ"
1. Номер состоит из 8 разрядов, поэтому от младшего до старшего потребуется 8 итераций; на каждой из них элементы распределяются только по цифрам 0–9.
2. Для прямого подсчета пришлось бы выделить позиции для всех возможных 8-значных значений, хотя в действительности используется лишь небольшая их часть.
Большинство счетчиков всегда оставалось бы равным 0.
3. Следует выбрать поразрядную сортировку. Она использует свойства «фиксированное число разрядов и только 10 возможных значений каждого разряда» и требует лишь 8 повторений стабильной группировки.
Если же использовать весь 8-значный номер как индекс при сортировке подсчетом, пришлось бы выделить счетчики для множества значений, которые никогда не встретятся.
## 11.12.2 &nbsp; Задачи по программированию
### 1. &nbsp; Сортировка массива слиянием
Дан массив целых чисел `nums`. Самостоятельно реализуйте сортировку слиянием, расположите элементы в неубывающем порядке и верните массив. Не используйте встроенную функцию сортировки языка.
??? tip "Подсказки"
1. Интервал длины не более 1 уже упорядочен
2. Разделите интервал в середине на две половины и рекурсивно отсортируйте каждую
3. Слейте два упорядоченных полуинтервала с помощью двух указателей, а затем запишите результат обратно в исходный массив
[LeetCode](https://leetcode.com/problems/sort-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
### 2. &nbsp; Сортировка массива целых чисел подсчетом
Даны массив целых чисел `nums` и неотрицательное целое число $K$. Каждый элемент массива находится в диапазоне от $0$ до $K$.
Реализуйте сортировку подсчетом, запишите результат в `nums` в неубывающем порядке и верните `nums`.
Не определяйте порядок элементов с помощью сравнений между ними и не используйте встроенную функцию сортировки языка.
??? tip "Подсказки"
1. Поскольку каждый элемент находится в диапазоне от 0 до K, значение элемента можно напрямую использовать как индекс массива счетчиков
2. При первом обходе nums увеличивайте счетчик в соответствующей позиции на 1
3. Затем переберите массив счетчиков от 0 до K: если значение x встретилось несколько раз, запишите x в nums подряд столько же раз
+1
View File
@@ -26,3 +26,4 @@ icon: material/sort-ascending
- [11.9 &nbsp; Сортировка подсчетом](counting_sort.md)
- [11.10 &nbsp; Поразрядная сортировка](radix_sort.md)
- [11.11 &nbsp; Резюме](summary.md)
- [11.12 &nbsp; Упражнения](exercises.md)
@@ -0,0 +1,105 @@
---
comments: true
---
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
# 5.5 &nbsp; Упражнения
## 5.5.1 &nbsp; Вопросы для самопроверки
### 1. &nbsp; Что первым извлекается из стека и очереди
Подготовьте пустой стек `S` и пустую очередь `Q`, а затем выполните с каждым из них один и тот же набор операций:
шаг 1: добавить `A`;
шаг 2: добавить `B`;
шаг 3: удалить один элемент и записать его;
шаг 4: добавить `C`;
шаг 5: удалять и записывать элементы, пока контейнер не опустеет.
Запишите порядок удаления элементов из `S` и `Q`. Объясните различие принципами «последним пришел — первым вышел» и «первым пришел — первым вышел».
??? success "Ответ"
Порядок удаления из стека `S`: `B, C, A`. После добавления `A, B` первым извлекается последний добавленный элемент `B`; затем добавляется `C`,
после чего извлекаются `C, A`. Это принцип «последним пришел — первым вышел».
Порядок удаления из очереди `Q`: `A, B, C`. После добавления `A, B` первым удаляется самый ранний элемент `A`;
затем добавляется `C`, после чего удаляются `B, C`. Это принцип «первым пришел — первым вышел».
### 2. &nbsp; Что происходит, когда хвост очереди проходит конец массива
Очередь реализована с помощью кольцевого массива длины 5 с индексами `04`.
Сейчас `front = 3`, `size = 2`, а элементы очереди `A, B` находятся по индексам 3 и 4 соответственно.
<!-- numbered-subquestions -->
1. По какому индексу нужно поместить `C` при операции «добавить `C` в очередь»? Чему после этого равно `size`?
2. Затем из очереди удаляется один элемент. Какой именно? Чему равны новые значения `front` и `size`?
3. Каков теперь логический порядок элементов от головы к хвосту очереди? Нужно ли при удалении сдвигать остальные элементы массива и почему?
??? success "Ответ"
1. Позиция нового элемента равна
`(front + size) % 5 = (3 + 2) % 5 = 0`,
поэтому `C` помещается по индексу 0. После добавления `size = 3`.
2. Из очереди удаляется текущий первый элемент `A`. Новый индекс головы равен
`(3 + 1) % 5 = 4`, поэтому `front = 4`, `size = 2`.
3. Логический порядок находящихся в очереди элементов — `B, C`: `B` находится по индексу 4, а `C` — по индексу 0.
При удалении достаточно изменить `front` и `size`. Благодаря остатку от деления индекс в кольцевом массиве возвращается к началу,
поэтому сдвигать вперед все остальные элементы не нужно.
### 3. &nbsp; Операции с обоих концов двусторонней очереди
Здесь `push_first` означает добавление в голову, `push_last` — добавление в хвост,
`pop_first` — извлечение из головы, а `pop_last` — извлечение из хвоста.
С пустой двусторонней очередью `deq` последовательно выполняются операции:
1. `push_last(A)`
2. `push_last(B)`
3. `push_first(C)`
4. `pop_last()`
5. `push_last(D)`
6. `pop_first()`
<!-- numbered-subquestions -->
1. Какие элементы возвращают две операции извлечения?
2. Какие элементы остаются после всех операций в порядке от головы к хвосту?
3. Рассмотрите все 6 шагов. Можно ли выполнить их с обычной очередью, в которую разрешено добавлять только с хвоста, а удалять только с головы? Если нет, укажите недопустимые операции. Объясните также, может ли выполнить все шаги двусторонняя очередь и почему.
??? success "Ответ"
После первых трех шагов двусторонняя очередь от головы к хвосту имеет вид `[C, A, B]`.
<!-- numbered-subquestions -->
1. `pop_last()` возвращает `B`. После добавления `D` очередь имеет вид `[C, A, D]`,
а `pop_first()` возвращает `C`.
2. В конце остаются `[A, D]`.
3. Обычная очередь, допускающая добавление с хвоста и удаление с головы, не может выполнить все операции:
шаг 3 `push_first(C)` требует добавить элемент с головы, а шаг 4 `pop_last()` — удалить элемент с хвоста. Обе операции выходят за рамки обычной очереди.
Двусторонняя очередь позволяет добавлять и удалять элементы с обоих концов, поэтому может выполнить все 6 шагов.
## 5.5.2 &nbsp; Задачи по программированию
### 1. &nbsp; Проверка последовательности скобок
Дана строка `s`, содержащая только скобки трех видов: `()`, `[]`, `{}`. С помощью стека определите, является ли последовательность правильной.
Правильная последовательность должна удовлетворять двум условиям: каждая закрывающая скобка соответствует по виду ближайшей еще не сопоставленной открывающей скобке,
а после обхода не остается несопоставленных открывающих скобок. Верните логическое значение результата проверки.
??? tip "Подсказки"
1. Можно создать отображение из закрывающей скобки в соответствующую открывающую
2. Встретив закрывающую скобку, сначала проверьте, не пуст ли стек, а затем — совпадает ли его вершина
3. После обхода стек также должен быть пуст
[LeetCode](https://leetcode.com/problems/valid-parentheses/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
+1
View File
@@ -19,3 +19,4 @@ icon: material/stack-overflow
- [5.2 &nbsp; Очередь](queue.md)
- [5.3 &nbsp; Двусторонняя очередь](deque.md)
- [5.4 &nbsp; Резюме](summary.md)
- [5.5 &nbsp; Упражнения](exercises.md)
+132
View File
@@ -0,0 +1,132 @@
---
comments: true
---
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
# 7.7 &nbsp; Упражнения
## 7.7.1 &nbsp; Вопросы для самопроверки
### 1. &nbsp; Полные, строгие и идеальные двоичные деревья
Два массива ниже представляют двоичные деревья в порядке обхода по уровням; `None` обозначает пустую позицию.
- дерево A: `[1, 2, 3, 4, 5, 6]`
- дерево B: `[1, 2, 3, None, None, 6, 7]`
<!-- numbered-subquestions -->
1. Какое из деревьев является полным?
2. Какое из деревьев является строгим, то есть каждый его нелистовой узел имеет два дочерних узла?
3. Есть ли среди них идеальное двоичное дерево? Объясните ответ для каждого дерева.
??? success "Ответ"
1. Дерево A полное. В нем не заполнен только последний уровень, причем узлы на нем расположены непрерывно слева направо.
Дерево B не является полным: на последнем уровне слева уже есть пустые позиции, но справа от них еще находятся узлы.
2. Дерево B строгое: узлы 1 и 3 имеют по два дочерних узла, а все остальные узлы являются листьями.
Дерево A не является строгим, потому что у узла 3 есть только левый дочерний узел 6.
3. Ни одно дерево не является идеальным, поскольку последний уровень каждого из них заполнен не полностью.
### 2. &nbsp; Три порядка обхода одного дерева
Поместите массив `[1, 2, 3, 4, 5, 6, 7]` по уровням в полное двоичное дерево.
<!-- numbered-subquestions -->
1. Нарисуйте это дерево.
2. Запишите последовательности прямого, симметричного и обратного обходов.
3. Каким частям дерева соответствуют участки последовательности симметричного обхода слева и справа от корневого узла 1?
??? success "Ответ"
1. Получится дерево:
```text
1
/ \
2 3
/ \ / \
4 5 6 7
```
2. Прямой обход: `1, 2, 4, 5, 3, 6, 7`;
симметричный обход: `4, 2, 5, 1, 6, 3, 7`;
обратный обход: `4, 5, 2, 6, 7, 3, 1`.
3. Последовательность `4, 2, 5` слева от корневого узла 1 — симметричный обход левого поддерева,
а `6, 3, 7` справа — симметричный обход правого поддерева.
### 3. &nbsp; Сравнение двух двоичных деревьев поиска
Последовательно, слева направо, вставьте каждую из двух последовательностей в пустое двоичное дерево поиска:
- последовательность A: `[4, 2, 6, 1, 3, 5, 7]`
- последовательность B: `[1, 2, 3, 4, 5, 6, 7]`
<!-- numbered-subquestions -->
1. Запишите узлы, через которые проходит поиск числа 7 в каждом дереве.
2. Если высота равна числу ребер на пути от корневого узла до самого удаленного листового узла, чему равна высота каждого дерева?
3. Одинакова ли эффективность поиска числа 7 в двух деревьях? Объясните ответ, учитывая форму деревьев и пути поиска.
??? success "Ответ"
1. В дереве, построенном из последовательности A, путь поиска равен `4 → 6 → 7`;
в дереве из последовательности B — `1 → 2 → 3 → 4 → 5 → 6 → 7`.
2. Каждый уровень первого дерева заполнен, его высота равна 2. Второе дерево состоит только из правых дочерних узлов, его высота равна 6.
3. Эффективность поиска 7 различается. Порядок вставки изменил форму и высоту двоичного дерева поиска. В первом дереве поиск 7 проходит только через 3 узла,
а во втором — через все 7. Чем выше дерево, тем больше узлов в худшем случае приходится сравнивать вдоль пути.
## 7.7.2 &nbsp; Задачи по программированию
### 1. &nbsp; Максимальная глубина двоичного дерева
Дан корневой узел `root` двоичного дерева. Каждый узел содержит целое значение и ссылки на левый и правый дочерние узлы.
Максимальной глубиной двоичного дерева назовем **число узлов** на пути от корневого узла до самого удаленного листового узла. Верните максимальную глубину дерева; глубина пустого дерева равна 0.
Используйте рекурсию.
??? tip "Подсказки"
1. В этой задаче глубина измеряется числом узлов: если в дереве есть только корневой узел, максимальная глубина равна 1
2. Пусть рекурсивная функция возвращает максимальную глубину поддерева с корнем в текущем узле
3. Для пустого узла верните 0, для непустого — max(depth(left), depth(right)) + 1
[LeetCode](https://leetcode.com/problems/maximum-depth-of-binary-tree/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
### 2. &nbsp; Обход двоичного дерева по уровням
Дан корневой узел `root` двоичного дерева. С помощью очереди посетите все узлы уровень за уровнем сверху вниз, а внутри каждого уровня — слева направо.
Верните двумерный массив: первый вложенный массив хранит значения узлов на уровне корня, второй — значения следующего уровня и так далее.
Для пустого дерева верните пустой массив.
??? tip "Подсказки"
1. При обходе по уровням узлы нужно посещать в порядке их добавления, поэтому используйте очередь
2. В начале каждой итерации в очереди находятся ровно узлы одного уровня
3. Сначала запомните длину очереди, затем извлеките столько узлов и добавьте их дочерние узлы
[LeetCode](https://leetcode.com/problems/binary-tree-level-order-traversal/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
### 3. &nbsp; k-й наименьший элемент двоичного дерева поиска
Двоичное дерево поиска содержит `n` узлов с попарно различными значениями.
Если расположить все значения по возрастанию, их позиции нумеруются начиная с 1.
Даны корневой узел `root` и целое число `k`, удовлетворяющее `1 <= k <= n`. Верните значение узла на позиции `k`.
Найдите ответ непосредственно во время симметричного обхода, не собирая предварительно значения всех узлов.
??? tip "Подсказки"
1. Симметричный обход двоичного дерева поиска посещает значения узлов в порядке возрастания
2. При симметричном обходе последовательно обрабатываются левое поддерево, текущий узел и правое поддерево; при посещении текущего узла увеличьте счетчик на 1
3. Когда счетчик впервые станет равен k, значение текущего узла будет ответом и продолжать обход не потребуется
[LeetCode](https://leetcode.com/problems/kth-smallest-element-in-a-bst/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
+1
View File
@@ -21,3 +21,4 @@ icon: material/graph-outline
- [7.4 &nbsp; Двоичное дерево поиска](binary_search_tree.md)
- [7.5 &nbsp; AVL-дерево *](avl_tree.md)
- [7.6 &nbsp; Краткие итоги](summary.md)
- [7.7 &nbsp; Упражнения](exercises.md)