diff --git a/labs/lab_01/example/Untitled.md b/labs/lab_01/example/Untitled.md new file mode 100644 index 0000000..6882abb --- /dev/null +++ b/labs/lab_01/example/Untitled.md @@ -0,0 +1,310 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.17.3 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +## Цель работы + +Целью данной лабораторной работы является приобретение практических навыков эмпирического анализа временной сложности алгоритмов. Это включает проведение серии экспериментальных замеров времени работы различных алгоритмов при изменении размера входных данных, построение графиков зависимости времени от объема данных, а также проведение теоретического анализа и сопоставление полученных эмпирических результатов с теоретическими оценками временной сложности. + + +1.2 сумма + +```python +import random +import usage_time +import matplotlib.pyplot as plt + +def sum_nums(v: list): + total = 0 + for num in v: + total += num + return total + +items = range(1, 10**5 * (20 - 6), 50000) +func = usage_time.get_usage_time()(sum_nums) +times = [ + func([ + random.randint(1, 3) + for _ in range(n) + ]) + for n in items +] + +plt.plot(items, times, 'bo-') +ax = plt.gca() + +plt.title('The execution time of the sum of list numbers algorithm') +ax.set_xlabel('Number of elements') +ax.set_ylabel('Time, sec') +plt.savefig('sum.png') +plt.show() + +``` + +![](images/sum.png) + +1.3 произведение эллементов + +```python + +``` + +```python +import random, usage_time +import matplotlib.pyplot as plt + + +def multiplication_nums(v: list): + mult = 1.0 + for num in v: + mult *= num + return mult + + +items = range(1, 10**5 * (20 - 6), 50000) +func = usage_time.get_usage_time()(multiplication_nums) +times = [ + func([ + random.randint(1, 3) + for _ in range(n) + ]) + for n in items +] + +fig = plt.plot(items, times, 'bo-') +ax = plt.gca() + +plt.title('The execution time of the get multiplication of list numbers algorithm') +ax.set_xlabel('Number of elements') +ax.set_ylabel('Time, sec') +plt.savefig('mult.png') +plt.show() +``` + +![](images/mult.png) + +1.6. поиск минимума простым перебором + +```python +import random +import usage_time +import matplotlib.pyplot as plt + + +def get_min(v: list) -> int: + min_num = v[0] + for num in v: + if num < min_num: + min_num = num + return min_num + + +items = range(1, 10**5 * (20 - 6), 50000) +func = usage_time.get_usage_time()(get_min) +times = [ + sum([ + func([ + random.randint(1, 10) + for _ in range(n) + ]) + for _ in range(20) + ]) / 20 + for n in items +] + +plt.plot(items, times, 'bo-') +ax = plt.gca() + +plt.title('The execution time of the getting min of list numbers algorithm') +ax.set_xlabel('Number of elements') +ax.set_ylabel('Time, sec') +plt.grid(True) +plt.savefig('min.png') +plt.show() +``` + +![](images/min.png) + +1.4. вычисление полинома методом Горнера + +```python +import random +import usage_time +import matplotlib.pyplot as plt + + +def horner(coeffs: list, x: float) -> float: + result = 0 + for coef in reversed(coeffs): + result = result * x + coef + return result + + +items = range(1, 10**5 * (20 - 6), 50000) +func = usage_time.get_usage_time()(horner) + +times = [ + sum([ + func( + [random.random() for _ in range(n)], + random.random() + ) + for _ in range(20) + ]) / 20 + for n in items +] + +plt.plot(items, times, 'bo-') +ax = plt.gca() + +plt.title('Execution time of polynomial evaluation by Horner\'s method') +ax.set_xlabel('Number of polynomial coefficients') +ax.set_ylabel('Time, sec') +plt.grid(True) +plt.savefig('horn.png') +plt.show() +``` +![](images/horn.png) + +Задание 2 + +```python +import random, usage_time +import matplotlib.pyplot as plt + + +def matrix_mult(matrix_a: list, matrix_b: list): + matrix_c = [ + [0 for _ in range(len(matrix_a))] + for _ in range(len(matrix_a)) + ] + + for y in range(len(matrix_a)): + for x in range(len(matrix_a)): + num = 0 + for i in range(len(matrix_a)): + num += matrix_a[y][i] * matrix_b[i][x] + matrix_c[y][x] = num + print(len(matrix_a)) + return matrix_c + + +items = range(1, 10**2 * (20 - 6), 100) +func = usage_time.get_usage_time()(matrix_mult) +times = [ + func( + [ + [ + random.randint(1, 3) + for _ in range(n) + ] + for _ in range(n) + ], + [ + [ + random.randint(4, 6) + for _ in range(n) + ] + for _ in range(n) + ] + ) + for n in items +] + +fig = plt.plot(items, times, 'bo-') +ax = plt.gca() + +plt.title('The execution time of the get matrix multiplication of list numbers algorithm') +ax.set_xlabel('Number of elements') +ax.set_ylabel('Time, sec') +plt.savefig('matr.png') +plt.show() +``` + +![](images/matr.png) + + +## Контрольные вопросы по вычислительной сложности алгоритмов + +1. Вычислительная сложность алгоритма — это характеристика количества ресурсов (времени и памяти), которые требуются алгоритму для обработки данных в зависимости от размера входа. Анализ сложности важен для оценки эффективности алгоритма и прогноза его поведения при большом объеме данных. +2. Время выполнения — сколько времени требуется алгоритму для обработки входных данных. Пространство (память) — сколько памяти алгоритм использует. Иногда приходится жертвовать временем ради уменьшения памяти (например, жадные алгоритмы) или наоборот — использовать больше памяти для ускорения работы (например, хеш-таблицы). +3. Асимптотический анализ изучает поведение алгоритма при больших объемах данных, избавляясь от постоянных и малозначимых факторов. Это удобнее, чем точные замеры в наносекундах, которые сильно зависят от конкретного оборудования и условий выполнения. +4. Нотация «О-большое» (Big O) описывает верхнюю границу времени или памяти, которую может потребовать алгоритм в худшем случае при росте объема данных. +5. Классы сложности (в порядке возрастания): +| Класс сложности | Описание | Пример алгоритма | +| :-- | :-- | :-- | +| O(1) | Константная | Доступ к элементу по индексу | +| O(log n) | Логарифмическая | Бинарный поиск | +| O(n) | Линейная | Простое сканирование массива | +| O(n log n) | Линейно-логарифмическая | Быстрая сортировка (QuickSort) | +| O(n²) | Квадратичная | Сортировка пузырьком | +| O(2ⁿ) | Экспоненциальная | Наивное вычисление Фибоначчи | + +6. Сложность фрагментов кода: + +- Простой цикл от 0 до n — O(n). +- Два вложенных цикла от 0 до n — O(n²). +- Цикл с удвоением счетчика (i = i * 2) — O(log n). +- Цикл с делением счетчика на 2 (i = i / 2) — O(log n). +- Два независимых цикла подряд — O(n + m), при m=n — O(n). +- Рекурсивная функция, вызывающая себя дважды — O(2ⁿ). + +7. Сложность в худшем, среднем и лучшем случае отличается по объему потребляемых ресурсов для разных входных данных. Например, QuickSort имеет O(n²) в худшем (плохой выбор опорного элемента) и O(n log n) в среднем и лучшем. +8. Пространственная сложность — количество дополнительной памяти, используемой алгоритмом. Для рекурсивных функций учитывается память стека вызовов, равная глубине рекурсии. +9. При очень малых n алгоритм с O(2ⁿ) может быть быстрее, чем O(n³), так как для маленьких n экспонента не успеет «выстрелить». Однако для средних и больших n O(n³) рациональнее из-за экспоненциального роста. +10. Временная сложность операций: + +| Операция | Сложность | +| :--: | :---: | +| Поиск в неотсортированном массиве | O(n) | +| Поиск в отсортированном массиве | O(log n) | +| Вставка в начало связного списка | O(1) | +| Вставка в хеш-таблицу (ср.случай) | O(1), (худший) O(n) | +| Поиск минимума в мин-куче | O(1) | + +11. Сравнение сортировок: + +- QuickSort: средний O(n log n), худший O(n²), зависит от выбора опорного элемента. +- MergeSort: всегда O(n log n), но из-за дополнительной памяти и копирования в практике медленнее. +- Insertion Sort: эффективнее MergeSort на почти отсортированных или очень маленьких массивах. + +12. Пространственно-временная дилемма — компромисс между временем и памятью, например, использование хеш-таблиц (больше памяти, меньше времени) vs. обход без дополнительной памяти (дольше). +13. NP-полнота — класс задач, решение которых можно проверить за полиномиальное время, но неизвестно, можно ли их решить за полиномиальное время. Класс P — задачи, решаемые за полиномиальное время. +14. Полиномиальное решение одной NP-полной задачи означает, что все NP-полные задачи можно быстро решить — важнейшая проблема теории алгоритмов (P=NP?). +15. NP-полноту доказывают сведением задачи к уже известной NP-полной задаче — так называемое "сведение по Карпу". +16. Омега (Ω) — нижняя оценка, тета (Θ) — точная оценка. В отличии от O, они дают соответственно минимум или точный порядок роста. +17. Сложность определяется по наибольшему слагаемому, так как при больших n остальные становятся несущественными. Константы отбрасываются для удобства анализа. +18. Не всегда. При малых n O(n) может быть быстрее O(log n) из-за констант и накладных расходов. +19. Для анализа можно предложить конкретный код. +20. Поиск двух чисел с суммой X в отсортированном массиве: + +- Используем два указателя — один слева, другой справа. +- Если сумма больше X — сдвигаем правый указатель, иначе левый. +- Сложность O(n), память O(1). +- Эффективнее чем полный перебор O(n²). + +21. Улучшение алгоритма: + +- Пример: из O(n²) сделать O(n) с помощью хеш-таблицы для поиска пар чисел. + + +## Выводы по лабораторной работе + +В ходе лабораторной работы были успешно реализованы замеры времени работы алгоритмов для различных функций и операций на случайных данных разного объема. Полученные эмпирические графики временных затрат в целом соответствуют ожидаемым теоретическим оценкам асимптотической сложности алгоритмов. + +Эксперимент подтвердил, что для алгоритмов с разной сложностью рост времени их исполнения при возрастании размера входных данных соответствует классификации: от константной и линейной до квадратичной и кубической. По мере увеличения объема данных различия во временных сложностях становятся все более очевидными. + +Эмпирический анализ показал важность учета практических особенностей исполнения, таких как накладные расходы системы и случайные отклонения времени, что требует усреднения результатов по нескольким запускам. + +Таким образом, лабораторная работа продемонстрировала применение эмпирического подхода для оценки временной сложности алгоритмов, что является важным дополнением к теоретическому анализу и помогает лучше понять их поведение на практике. + diff --git a/labs/lab_01/example/Untitled1.md b/labs/lab_01/example/Untitled1.md new file mode 100644 index 0000000..01f385f --- /dev/null +++ b/labs/lab_01/example/Untitled1.md @@ -0,0 +1,9 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.17.3 +--- diff --git a/labs/lab_01/example/images/horn.png b/labs/lab_01/example/images/horn.png new file mode 100644 index 0000000..3dec0db Binary files /dev/null and b/labs/lab_01/example/images/horn.png differ diff --git a/labs/lab_01/example/images/matr.png b/labs/lab_01/example/images/matr.png new file mode 100644 index 0000000..d337bdd Binary files /dev/null and b/labs/lab_01/example/images/matr.png differ diff --git a/labs/lab_01/example/images/min.png b/labs/lab_01/example/images/min.png new file mode 100644 index 0000000..ac264b3 Binary files /dev/null and b/labs/lab_01/example/images/min.png differ diff --git a/labs/lab_01/example/images/mult.png b/labs/lab_01/example/images/mult.png new file mode 100644 index 0000000..2b1ef3f Binary files /dev/null and b/labs/lab_01/example/images/mult.png differ diff --git a/labs/lab_01/example/images/sum.png b/labs/lab_01/example/images/sum.png new file mode 100644 index 0000000..0fa7b53 Binary files /dev/null and b/labs/lab_01/example/images/sum.png differ diff --git a/labs/lab_01/example/mult.png b/labs/lab_01/example/mult.png new file mode 100644 index 0000000..116927c Binary files /dev/null and b/labs/lab_01/example/mult.png differ diff --git a/labs/lab_02/var6/LaTeX/comb_sort.pdf b/labs/lab_02/var6/LaTeX/comb_sort.pdf new file mode 100644 index 0000000..53f9fe1 Binary files /dev/null and b/labs/lab_02/var6/LaTeX/comb_sort.pdf differ diff --git a/labs/lab_02/var6/LaTeX/comb_sort.png b/labs/lab_02/var6/LaTeX/comb_sort.png new file mode 100644 index 0000000..5a35472 Binary files /dev/null and b/labs/lab_02/var6/LaTeX/comb_sort.png differ diff --git a/labs/lab_02/var6/LaTeX/comb_sort.tex b/labs/lab_02/var6/LaTeX/comb_sort.tex new file mode 100644 index 0000000..d2cf5ab --- /dev/null +++ b/labs/lab_02/var6/LaTeX/comb_sort.tex @@ -0,0 +1,54 @@ +\documentclass[tikz,border=3.14mm]{standalone} +\usepackage{tikz} +\usetikzlibrary{shapes.geometric, arrows.meta, positioning} + +\tikzset{ + startstop/.style = {rectangle, rounded corners, minimum width=3cm, minimum height=1cm, text centered, draw=black, fill=red!30}, + process/.style = {rectangle, minimum width=3cm, minimum height=1cm, text centered, draw=black, fill=orange!30}, + decision/.style = {diamond, minimum width=3cm, minimum height=1cm, text centered, draw=black, fill=green!30}, + arrow/.style = {thick, -Stealth} +} + +\begin{document} +\begin{tikzpicture}[node distance=1.5cm] + +% Main algorithm flow +\node (start) [startstop] {Start Comb Sort}; +\node (init) [process, below=of start] {gap = n\\shrink = 1.3\\sorted = false}; +\node (dec1) [decision, below=of init] {gap > 1 OR\\not sorted?}; +\node (update) [process, below=of dec1] {gap = max(1, floor(gap/shrink))\\sorted = true}; +\node (init_i) [process, below=of update] {i = 0}; +\node (dec2) [decision, below=of init_i] {i + gap < n?}; + +% Inner loop - comparison and swap +\node (dec3) [decision, right=2cm of dec2] {array[i] > array[i+gap]?}; +\node (swap) [process, below=of dec3] {Swap array[i] and array[i+gap]\\sorted = false}; +\node (increment) [process, below=2cm of dec2] {i = i + 1}; +\node (stop) [startstop, below=2.5cm of dec1] {End}; + +% Connections - main flow +\draw [arrow] (start) -- (init); +\draw [arrow] (init) -- (dec1); +\draw [arrow] (dec1) -- node[right] {Yes} (update); +\draw [arrow] (update) -- (init_i); +\draw [arrow] (init_i) -- (dec2); + +% Connections - inner loop +\draw [arrow] (dec2) -- node[above] {Yes} (dec3); +\draw [arrow] (dec3) -- node[right] {Yes} (swap); +\draw [arrow] (swap) |- (increment); + +% Connections - no swap path +\draw [arrow] (dec3.east) -- ++(0.5,0) node[above] {No} |- (increment.east); + +% Connections - loop back +\draw [arrow] (increment.west) -| node[below left] {Continue inner loop} ([xshift=-1cm]dec2.west) -- (dec2.west); + +% Connections - exit conditions +\draw [arrow] (dec2.south) -- node[left] {No} (increment.north); +\draw [arrow] (increment.south) -- ++(0,-0.5) -| node[below right] {Next iteration} ([xshift=1cm]dec1.east) -- (dec1.east); + +\draw [arrow] (dec1.west) -- node[above] {No} ++(-2,0) |- (stop.west); + +\end{tikzpicture} +\end{document} diff --git a/labs/lab_02/var6/LaTeX/radix_sort.pdf b/labs/lab_02/var6/LaTeX/radix_sort.pdf new file mode 100644 index 0000000..c8d4917 Binary files /dev/null and b/labs/lab_02/var6/LaTeX/radix_sort.pdf differ diff --git a/labs/lab_02/var6/LaTeX/radix_sort.png b/labs/lab_02/var6/LaTeX/radix_sort.png new file mode 100644 index 0000000..8eda452 Binary files /dev/null and b/labs/lab_02/var6/LaTeX/radix_sort.png differ diff --git a/labs/lab_02/var6/LaTeX/radix_sort.tex b/labs/lab_02/var6/LaTeX/radix_sort.tex new file mode 100644 index 0000000..11aa754 --- /dev/null +++ b/labs/lab_02/var6/LaTeX/radix_sort.tex @@ -0,0 +1,43 @@ +\documentclass[tikz,border=3.14mm]{standalone} +\usepackage{tikz} +\usetikzlibrary{shapes.geometric, arrows.meta, positioning} + +\tikzset{ + startstop/.style = {rectangle, rounded corners, minimum width=3cm, minimum height=1cm, text centered, draw=black, fill=red!30}, + process/.style = {rectangle, minimum width=3cm, minimum height=1cm, text centered, draw=black, fill=orange!30}, + decision/.style = {diamond, minimum width=2.5cm, minimum height=1.5cm, text centered, draw=black, fill=green!30, align=center}, + arrow/.style = {thick, -Stealth} +} + +\begin{document} +\begin{tikzpicture}[node distance=1.2cm] + +\node (start) [startstop] {Start}; +\node (findMax) [process, below=of start] {Find maximum number}; +\node (initExp) [process, below=of findMax] {Set digit position = 1}; +\node (dec1) [decision, below=of initExp] {More digits?}; +\node (sortDigit) [process, below=of dec1] {Stable sort by current digit}; +\node (nextDigit) [process, below=of sortDigit] {Move to next digit position}; +\node (stop) [startstop, below=2cm of dec1] {End}; + +% Main flow +\draw [arrow] (start) -- (findMax); +\draw [arrow] (findMax) -- (initExp); +\draw [arrow] (initExp) -- (dec1); +\draw [arrow] (dec1) -- node[right] {Yes} (sortDigit); +\draw [arrow] (sortDigit) -- (nextDigit); +\draw [arrow] (nextDigit.west) -- ++(-1,0) |- (dec1.west); +\draw [arrow] (dec1.east) -- node[above] {No} ++(1,0) |- (stop.east); + +% Algorithm details +\node [right=1cm of sortDigit, align=left, font=\small] { + \textbf{For each digit (LSD first):}\\ + • Use Counting Sort\\ + • Sort by current digit\\ + • Maintain relative order\\ + • Time: O(d*(n+k))\\ + • Space: O(n+k) +}; + +\end{tikzpicture} +\end{document} diff --git a/labs/lab_02/var6/Untitled.md b/labs/lab_02/var6/Untitled.md new file mode 100644 index 0000000..db7516e --- /dev/null +++ b/labs/lab_02/var6/Untitled.md @@ -0,0 +1,1008 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.17.3 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +### Вариант 6 + +## Сортировка расческой (Comb Sort) + +# 1. Классификация сортировки расчесткой + +Сортировка расчесткой является внутренней сортировкой, сортирующей элементы в оперативной памяти. Она относится к адаптивным алгоритмам, так как учитывает уже относительно отсортированные участки массива. Это сортировка на месте (on place), не требующая дополнительной памяти, кроме переменных для индексов и шага. По устойчивости сортировка неустойчивая, поскольку при обмене элементов порядок одинаковых ключей может изменяться. По сложности в худшем и среднем случаях время работы близко к $O(n^2)$, но за счет использования "расчески" значительно быстрее пузырьковой сортировки, особенно на больших массивах. + +# 2. Теоретическое описание сортировки расчесткой + +Сортировка расчесткой улучшает сортировку пузырьком, уменьшая вероятность "залипания" мелких элементов в конце массива. Основная идея — использовать меняющийся интервал (gap) сравнения пар элементов, с шагом уменьшающимся примерно в 1.3 раза после каждой итерации, пока не дойдет до 1. При каждом проходе элементы, находящиеся на расстоянии gap, сравниваются и при необходимости меняются местами. Как только gap достигает 1, сортировка превращается в обычную пузырьковую, но к этому моменту массив уже сильно упорядочен, что ускоряет сортировку. + +# 3. Блок-схема сортировки расчесткой + +![](comb.jpeg) + + +![](LaTeX/comb_sort.png) + + +# 4. Псевдокод сортировки расчесткой + +```python +def comb_sort(arr): + gap = len(arr) + shrink = 1.247 # оптимальный коэффициент + sorted = False + + while not sorted: + gap = int(gap / shrink) + if gap <= 1: + gap = 1 + sorted = True + i = 0 + while i + gap < len(arr): + if arr[i] > arr[i + gap]: + arr[i], arr[i + gap] = arr[i + gap], arr[i] + sorted = False + i += 1 + return arr + +``` + +# 5. Достоинства и недостатки + +**Достоинства:** + +- Быстрее пузырьковой сортировки благодаря уменьшению инвертированных пар с большим шагом. +- Простая реализация. +- Работает на месте, не требует дополнительной памяти. + +**Недостатки:** + +- Неустойчивая сортировка. +- Сложность в худшем случае близка к $O(n^2)$, что медленнее более сложных алгоритмов (быстрой или поразрядной сортировки). +- Эффективность зависит от коэффициента уменьшения gap (обычно 1.3). + +*** + +# 6. Реализовать алгоритмы сортировки согласно номеру индивидуального варианта. + +```python +import random +import usage_time +import matplotlib.pyplot as plt +import numpy as np + +def comb_sort(arr): + gap = len(arr) + shrink = 1.247 + sorted = False + while not sorted: + gap = int(gap / shrink) + if gap <= 1: + gap = 1 + sorted = True + i = 0 + while i + gap < len(arr): + if arr[i] > arr[i + gap]: + arr[i], arr[i + gap] = arr[i + gap], arr[i] + sorted = False + i += 1 + return arr + +%store comb_sort +``` + +# 7 Протестировать корректность реализации алгоритма и 8 Провести ручную трассировку алгоритма + +```python +def quick_comb_sort_debug(arr): + gap = len(arr) + shrink = 1.247 + step = 0 + + print(f"Начало: {arr}") + + while True: + gap = max(1, int(gap / shrink)) + step += 1 + print(f"\nШаг {step}, gap={gap}:") + + swapped = False + for i in range(len(arr) - gap): + if arr[i] > arr[i + gap]: + arr[i], arr[i + gap] = arr[i + gap], arr[i] + swapped = True + print(f" Меняем {arr[i+gap]}↔{arr[i]}: {arr}") + + if gap == 1 and not swapped: + break + + print(f"\nФинальный результат: {arr}") + return arr + +# Быстрая проверка +test = [6, 2, 8, 1, 5, 3] +print("Быстрая проверка сортировки:") +quick_comb_sort_debug(test.copy()) +``` + +Провести сравнение указанных алгоритмов сортировки массивов, содержащих n1, n2, n3 и n4 элементов. +Каждую функцию сортировки вызывать трижды: для сортировки упорядоченного массива, массива, упорядоченного в обратном порядке и неупорядоченного массива. Сортируемая последовательность для всех методов должна быть одинаковой (сортировать копии одного массива). +Проиллюстрировать эффективность алгоритмов сортировок по заданному критерию. Построить диаграммы указанных зависимостей. + +```python +import random +import usage_time +import matplotlib.pyplot as plt +import numpy as np + +def comb_sort(arr): + gap = len(arr) + shrink = 1.247 + sorted_flag = False + + while not sorted_flag: + gap = int(gap / shrink) + if gap <= 1: + gap = 1 + sorted_flag = True + + i = 0 + while i + gap < len(arr): + if arr[i] > arr[i + gap]: + arr[i], arr[i + gap] = arr[i + gap], arr[i] + sorted_flag = False + i += 1 + return arr + +sizes = [1000, 5000, 10000, 100000] + +def generate_arrays(size): + sorted_arr = list(range(size)) + reversed_arr = list(range(size, 0, -1)) + random_arr = [random.randint(1, size * 10) for _ in range(size)] + + return { + 'sorted': sorted_arr, + 'reversed': reversed_arr, + 'random': random_arr + } + +func = usage_time.get_usage_time(ndigits=6)(comb_sort) + +results = { + 'sorted': [], + 'reversed': [], + 'random': [] +} + +print("ИЗМЕРЕНИЕ ВРЕМЕНИ СОРТИРОВКИ РАСЧЕСКОЙ") +print("=" * 70) + +for size in sizes: + print(f"Размер массива: {size} элементов") + print("-" * 50) + + arrays = generate_arrays(size) + + for array_type, arr in arrays.items(): + time_taken = func(arr.copy()) + results[array_type].append(time_taken) + + print(f" {array_type:>8}: {time_taken:.6f} сек") + +plt.figure(figsize=(15, 5)) + +colors = {'sorted': 'green', 'reversed': 'red', 'random': 'blue'} + +for i, array_type in enumerate(['sorted', 'reversed', 'random'], 1): + plt.subplot(1, 3, i) + plt.plot(sizes, results[array_type], 'o-', color=colors[array_type], + linewidth=2, markersize=8) + + for size, time in zip(sizes, results[array_type]): + plt.annotate(f'{time:.4f}с', (size, time), + textcoords="offset points", xytext=(0,10), + ha='center', fontsize=8) + + plt.xlabel('Размер массива') + plt.ylabel('Время, секунды') + plt.title(f'Тип: {array_type}') + plt.grid(True, alpha=0.3) + +plt.tight_layout() +plt.show() + +print("СТАТИСТИЧЕСКИЙ АНАЛИЗ") +print("=" * 70) + +for array_type in ['sorted', 'reversed', 'random']: + times = results[array_type] + print(f"{array_type.upper():>15}:") + for size, time in zip(sizes, times): + print(f" {size:6d} эл.: {time:8.4f} сек") + + if len(times) > 1: + growth = times[-1] / times[0] + print(f" Рост времени ({sizes[-1]}/{sizes[0]}): {growth:.2f}x") + +print("СРАВНЕНИЕ ЭФФЕКТИВНОСТИ:") +print(f"Упорядоченный массив сортируется в {results['reversed'][-1]/results['sorted'][-1]:.2f} раза медленнее") +print(f"Случайный массив сортируется в {results['random'][-1]/results['sorted'][-1]:.2f} раза медленнее") +``` + +## Сравнение + +```python +import random +import usage_time +import matplotlib.pyplot as plt +import numpy as np + +def comb_sort(arr): + gap = len(arr) + shrink = 1.247 + sorted_flag = False + + while not sorted_flag: + gap = int(gap / shrink) + if gap <= 1: + gap = 1 + sorted_flag = True + + i = 0 + while i + gap < len(arr): + if arr[i] > arr[i + gap]: + arr[i], arr[i + gap] = arr[i + gap], arr[i] + sorted_flag = False + i += 1 + return arr + +def radix_sort(arr): + if not arr: + return arr + + max_num = max(arr) + exp = 1 + + while max_num // exp > 0: + counting_sort_for_radix(arr, exp) + exp *= 10 + + return arr + +def counting_sort_for_radix(arr, exp): + n = len(arr) + output = [0] * n + count = [0] * 10 + + for i in range(n): + index = (arr[i] // exp) % 10 + count[index] += 1 + + for i in range(1, 10): + count[i] += count[i - 1] + + for i in range(n - 1, -1, -1): + index = (arr[i] // exp) % 10 + output[count[index] - 1] = arr[i] + count[index] -= 1 + + for i in range(n): + arr[i] = output[i] + +sizes = [1000, 5000, 10000, 100000] + +def generate_arrays(size): + sorted_arr = list(range(size)) + reversed_arr = list(range(size, 0, -1)) + random_arr = [random.randint(1, size * 10) for _ in range(size)] + + return { + 'sorted': sorted_arr, + 'reversed': reversed_arr, + 'random': random_arr + } + +comb_func = usage_time.get_usage_time(ndigits=6)(comb_sort) +radix_func = usage_time.get_usage_time(ndigits=6)(radix_sort) + +comb_results = { + 'sorted': [], + 'reversed': [], + 'random': [] +} + +radix_results = { + 'sorted': [], + 'reversed': [], + 'random': [] +} + +print("ИЗМЕРЕНИЕ ВРЕМЕНИ СОРТИРОВКИ РАСЧЕСКОЙ") +print("=" * 70) + +for size in sizes: + print(f"Размер массива: {size} элементов") + print("-" * 50) + + arrays = generate_arrays(size) + + for array_type, arr in arrays.items(): + time_taken = comb_func(arr.copy()) + comb_results[array_type].append(time_taken) + + print(f" {array_type:>8}: {time_taken:.6f} сек") + +print("\nИЗМЕРЕНИЕ ВРЕМЕНИ ПОРАЗРЯДНОЙ СОРТИРОВКИ") +print("=" * 70) + +for size in sizes: + print(f"Размер массива: {size} элементов") + print("-" * 50) + + arrays = generate_arrays(size) + + for array_type, arr in arrays.items(): + time_taken = radix_func(arr.copy()) + radix_results[array_type].append(time_taken) + + print(f" {array_type:>8}: {time_taken:.6f} сек") + +plt.figure(figsize=(15, 5)) + +for i, array_type in enumerate(['sorted', 'reversed', 'random'], 1): + plt.subplot(1, 3, i) + + plt.plot(sizes, comb_results[array_type], 'o-', color='red', + linewidth=2, markersize=8, label='Comb Sort') + + plt.plot(sizes, radix_results[array_type], 's-', color='blue', + linewidth=2, markersize=8, label='Radix Sort') + + for j, size in enumerate(sizes): + plt.annotate(f'{comb_results[array_type][j]:.4f}с', (size, comb_results[array_type][j]), + textcoords="offset points", xytext=(0,10), + ha='center', fontsize=7, color='red') + plt.annotate(f'{radix_results[array_type][j]:.4f}с', (size, radix_results[array_type][j]), + textcoords="offset points", xytext=(0,-15), + ha='center', fontsize=7, color='blue') + + plt.xlabel('Размер массива') + plt.ylabel('Время, секунды') + plt.title(f'Тип: {array_type}') + plt.legend() + plt.grid(True, alpha=0.3) + +plt.tight_layout() +plt.show() + +print("СТАТИСТИЧЕСКИЙ АНАЛИЗ - СОРТИРОВКА РАСЧЕСКОЙ") +print("=" * 70) + +for array_type in ['sorted', 'reversed', 'random']: + times = comb_results[array_type] + print(f"{array_type.upper():>15}:") + for size, time in zip(sizes, times): + print(f" {size:6d} эл.: {time:8.4f} сек") + + if len(times) > 1: + growth = times[-1] / times[0] + print(f" Рост времени ({sizes[-1]}/{sizes[0]}): {growth:.2f}x") + +print("\nСТАТИСТИЧЕСКИЙ АНАЛИЗ - ПОРАЗРЯДНАЯ СОРТИРОВКА") +print("=" * 70) + +for array_type in ['sorted', 'reversed', 'random']: + times = radix_results[array_type] + print(f"{array_type.upper():>15}:") + for size, time in zip(sizes, times): + print(f" {size:6d} эл.: {time:8.4f} сек") + + if len(times) > 1: + growth = times[-1] / times[0] + print(f" Рост времени ({sizes[-1]}/{sizes[0]}): {growth:.2f}x") + +print("\nСРАВНЕНИЕ ЭФФЕКТИВНОСТИ:") +print(f"Comb Sort - обратный/упорядоченный: {comb_results['reversed'][-1]/comb_results['sorted'][-1]:.2f}x") +print(f"Comb Sort - случайный/упорядоченный: {comb_results['random'][-1]/comb_results['sorted'][-1]:.2f}x") +print(f"Radix Sort - обратный/упорядоченный: {radix_results['reversed'][-1]/radix_results['sorted'][-1]:.2f}x") +print(f"Radix Sort - случайный/упорядоченный: {radix_results['random'][-1]/radix_results['sorted'][-1]:.2f}x") +print(f"Comb/Radix на случайных данных: {comb_results['random'][-1]/radix_results['random'][-1]:.2f}x") +``` + + +## Поразрядная сортировка +# 1. Классификация алгоритмов сортировки + +- Внутренняя сортировка (работает в оперативной памяти) +- Устойчивая сортировка (сохраняет порядок элементов с одинаковыми ключами) +- Использует дополнительную память для промежуточных структур +- Линейная временная сложность при фиксированной длине ключей: $O(n \cdot k)$, где $n$ — количество элементов, $k$ — количество разрядов +- Сортирует методом обхода и группировки по разрядам из системы счисления (основание radix) + + +# 2. Теоретическое описание алгоритма + +Поразрядная сортировка "Radix Sort" обрабатывает элементы по разрядам ключа, сначала группирует их по младшему разряду (LSD) или по старшему (MSD). + +Основные шаги: + +1. Определяется максимальное количество разрядов у элементов. +2. На каждом шаге выполняется устойчивая сортировка по текущему разряду (например, сортировка подсчётом). +3. После сортировки по всем разрядам массив оказывается полностью отсортированным. + +Преимущество — отсутствие прямых сравнений элементов, что ускоряет работу при ограниченной длине ключей. + +# 3. Блок-схема алгоритма + + +![](LaTeX/radix_sort.png) + +- Начало +- Найти максимальный элемент и определить количество разрядов $d$ +- Цикл по $i = 1 \text{ до } d$: + - Устойчивая сортировка массива по $i$-му разряду +- Конец — массив отсортирован + + +# 4. Псевдокод алгоритма (LSD-версия) + +``` +function radixSort(array A): + maxVal = max(A) + d = число_разрядов(maxVal) + for digit in 1 to d: + A = stableSortByDigit(A, digit) + return A +``` + +Где `stableSortByDigit` — устойчивая сортировка массива по $digit$-му разряду, чаще всего применяется сортировка подсчётом. + +# 5. Достоинства и недостатки + +| Достоинства | Недостатки | +| :-- | :-- | +| Линейная сложность при фиксированном $k$ | Требует дополнительной памяти для подсчёта | +| Стабильность сортировки | Неэффективна при очень больших или длинных ключах | +| Отсутствие прямых сравнений элементов | Требует знания основания системы счисления | + +# 6. Реализовать алгоритмы сортировки согласно номеру индивидуального варианта. + + +```python +def radix_sort(arr): + if not arr: + return arr + + max_num = max(arr) + + exp = 1 + while max_num // exp > 0: + counting_sort_for_radix(arr, exp) + exp *= 10 + + return arr + +def counting_sort_for_radix(arr, exp): + """ + Вспомогательная функция для сортировки подсчетом по текущему разряду + """ + n = len(arr) + output = [0] * n + count = [0] * 10 + + for i in range(n): + index = (arr[i] // exp) % 10 + count[index] += 1 + + for i in range(1, 10): + count[i] += count[i - 1] + + for i in range(n - 1, -1, -1): + index = (arr[i] // exp) % 10 + output[count[index] - 1] = arr[i] + count[index] -= 1 + + for i in range(n): + arr[i] = output[i] +``` + +# 7. Ручная тестировка + +```python +def radix_sort_visual(arr): + print("Начало сортировки") + print(f"Исходный массив: {arr}") + + if not arr: + return arr + + max_num = max(arr) + print(f"Максимальное число: {max_num}") + + exp = 1 + iteration = 1 + + while max_num // exp > 0: + print(f"\n--- Итерация {iteration}: разряд {exp} ---") + arr = counting_sort_visual(arr, exp) + exp *= 10 + iteration += 1 + + print(f"\nСортировка завершена!") + print(f"Результат: {arr}") + return arr + +def counting_sort_visual(arr, exp): + n = len(arr) + output = [0] * n + count = [0] * 10 + + print("Шаг 1: Подсчет цифр") + for i in range(n): + digit = (arr[i] // exp) % 10 + count[digit] += 1 + print(f"{arr[i]} -> цифра {digit}") + + print(f"Count: {count}") + + print("Шаг 2: Преобразование count") + for i in range(1, 10): + count[i] += count[i - 1] + + print(f"Positions: {count}") + + print("Шаг 3: Построение output") + print(f"Исходный: {arr}") + + for i in range(n - 1, -1, -1): + digit = (arr[i] // exp) % 10 + position = count[digit] - 1 + output[position] = arr[i] + count[digit] -= 1 + print(f"{arr[i]} (цифра {digit}) -> позиция {position}") + print(f"Output: {output}") + + print(f"Результат: {output}") + return output + +def simple_radix_sort_visual(arr): + print(f"Сортируем: {arr}") + + if len(arr) <= 1: + return arr + + max_num = max(arr) + exp = 1 + + while max_num // exp > 0: + print(f"\nРазряд: {exp}") + + n = len(arr) + count = [0] * 10 + output = [0] * n + + print("Цифры чисел:") + for num in arr: + digit = (num // exp) % 10 + count[digit] += 1 + print(f"{num} -> {digit}") + + print(f"Count: {count}") + + for i in range(1, 10): + count[i] += count[i - 1] + + print(f"Позиции: {count}") + + for i in range(n - 1, -1, -1): + num = arr[i] + digit = (num // exp) % 10 + pos = count[digit] - 1 + output[pos] = num + count[digit] -= 1 + print(f"{num} -> позиция {pos}: {output}") + + arr = output + exp *= 10 + + print(f"\nИтог: {arr}") + return arr + +# Тестирование +if __name__ == "__main__": + print("ТЕСТ 1:") + test1 = [170, 45, 75, 90] + radix_sort_visual(test1.copy()) + + print("\n" + "="*40 + "\n") + + print("ТЕСТ 2:") + test2 = [42, 17, 89, 5] + simple_radix_sort_visual(test2.copy()) +``` + +# Пункты 9, 10, 11 + +```python +import random +import usage_time +import matplotlib.pyplot as plt +import numpy as np + +def radix_sort(arr): + if not arr: + return arr + + max_num = max(arr) + exp = 1 + + while max_num // exp > 0: + counting_sort_for_radix(arr, exp) + exp *= 10 + + return arr + +def counting_sort_for_radix(arr, exp): + n = len(arr) + output = [0] * n + count = [0] * 10 + + for i in range(n): + index = (arr[i] // exp) % 10 + count[index] += 1 + + for i in range(1, 10): + count[i] += count[i - 1] + + for i in range(n - 1, -1, -1): + index = (arr[i] // exp) % 10 + output[count[index] - 1] = arr[i] + count[index] -= 1 + + for i in range(n): + arr[i] = output[i] + +sizes = [1000, 2000, 4000, 8000] + +def generate_arrays(size): + sorted_arr = list(range(size)) + reversed_arr = list(range(size, 0, -1)) + random_arr = [random.randint(1, size * 10) for _ in range(size)] + + return { + 'sorted': sorted_arr, + 'reversed': reversed_arr, + 'random': random_arr + } + +func = usage_time.get_usage_time(ndigits=6)(radix_sort) + +results = { + 'sorted': [], + 'reversed': [], + 'random': [] +} + +print("ИЗМЕРЕНИЕ ВРЕМЕНИ ПОРАЗРЯДНОЙ СОРТИРОВКИ") +print("=" * 70) + +for size in sizes: + print(f"Размер массива: {size} элементов") + print("-" * 50) + + arrays = generate_arrays(size) + + for array_type, arr in arrays.items(): + time_taken = func(arr.copy()) + results[array_type].append(time_taken) + + print(f" {array_type:>8}: {time_taken:.6f} сек") + +plt.figure(figsize=(15, 5)) + +colors = {'sorted': 'green', 'reversed': 'red', 'random': 'blue'} + +for i, array_type in enumerate(['sorted', 'reversed', 'random'], 1): + plt.subplot(1, 3, i) + plt.plot(sizes, results[array_type], 'o-', color=colors[array_type], + linewidth=2, markersize=8, markerfacecolor='white', markeredgewidth=2) + + for size, time in zip(sizes, results[array_type]): + plt.annotate(f'{time:.4f}с', (size, time), + textcoords="offset points", xytext=(0,10), + ha='center', fontsize=8, fontweight='bold') + + plt.xlabel('Размер массива') + plt.ylabel('Время, секунды') + plt.title(f'Поразрядная сортировка: {array_type}') + plt.grid(True, alpha=0.3) + +plt.tight_layout() +plt.show() + +print("СТАТИСТИЧЕСКИЙ АНАЛИЗ") +print("=" * 70) + +for array_type in ['sorted', 'reversed', 'random']: + times = results[array_type] + print(f"{array_type.upper():>15}:") + for size, time in zip(sizes, times): + print(f" {size:6d} эл.: {time:8.4f} сек") + + if len(times) > 1: + growth = times[-1] / times[0] + print(f" Рост времени ({sizes[-1]}/{sizes[0]}): {growth:.2f}x") + +print("СРАВНЕНИЕ ЭФФЕКТИВНОСТИ:") +print(f"Обратный порядок сортируется в {results['reversed'][-1]/results['sorted'][-1]:.2f} раза медленнее упорядоченного") +print(f"Случайный массив сортируется в {results['random'][-1]/results['sorted'][-1]:.2f} раза медленнее упорядоченного") +``` + + +1. В чем состоит суть метода сортировки вставками? +Суть метода заключается в том, что массив делится на отсортированную и неотсортированную части. На каждом шаге берется очередной элемент из неотсортированной части и вставляется на правильную позицию в отсортированной части. + +2. Какие шаги выполняет алгоритм сортировки вставками? + +Начинаем со второго элемента (i = 1) + +Сохраняем текущий элемент в временную переменную + +Сдвигаем элементы отсортированной части, которые больше текущего элемента, вправо + +Вставляем текущий элемент на освободившееся место + +Повторяем для всех элементов массива + +3. Как программно реализуется сортировка вставками? + +```python +def insertion_sort(arr): + for i in range(1, len(arr)): + key = arr[i] + j = i - 1 + while j >= 0 and arr[j] > key: + arr[j + 1] = arr[j] + j -= 1 + arr[j + 1] = key + return arr +``` +4. В чем достоинства и недостатки метода сортировки вставками? +Достоинства: простая реализация, эффективен на небольших массивах, устойчив, адаптивен +Недостатки: O(n²) в худшем случае, неэффективен на больших массивах + +5. Приведите практический пример сортировки массива методом вставок +Массив: [5, 2, 4, 6, 1, 3] +Шаги: [2, 5, 4, 6, 1, 3] → [2, 4, 5, 6, 1, 3] → [2, 4, 5, 6, 1, 3] → [1, 2, 4, 5, 6, 3] → [1, 2, 3, 4, 5, 6] + +6. В чем состоит суть сортировки методом Шелла? +Суть в сравнении элементов, стоящих на определенном расстоянии друг от друга (с убывающим шагом), что позволяет быстрее перемещать элементы на большие расстояния. + +7. За счет чего метод Шелла дает лучшие показатели по сравнению с простейшими методами? +За счет предварительной сортировки элементов на больших расстояниях, что уменьшает количество инверсий и позволяет быстрее упорядочивать массив. + +8. Приведите практический пример сортировки массива методом Шелла +Массив: [8, 3, 7, 4, 1, 9, 2, 6, 5] +Шаг 4: [1, 3, 7, 4, 8, 9, 2, 6, 5] +Шаг 2: [1, 3, 2, 4, 5, 6, 7, 9, 8] +Шаг 1: [1, 2, 3, 4, 5, 6, 7, 8, 9] + +9. Какой фактор оказывает наибольшее влияние на эффективность сортировки методом Шелла? +Выбор последовательности шагов (интервалов) для сравнения элементов. + +10. Какие последовательности шагов группировки рекомендуются для практического использования в методе Шелла? +Последовательность Кнута: 1, 4, 13, 40, 121... или последовательность Седжвика: 1, 8, 23, 77, 281... + +11. Как программно реализуется сортировка методом Шелла? + +```python +def shell_sort(arr): + n = len(arr) + gap = n // 2 + while gap > 0: + for i in range(gap, n): + temp = arr[i] + j = i + while j >= gap and arr[j - gap] > temp: + arr[j] = arr[j - gap] + j -= gap + arr[j] = temp + gap //= 2 + return arr +``` +12. В чем состоит суть метода сортировки выбором? +На каждом шаге находится минимальный элемент из неотсортированной части и помещается в конец отсортированной части. + +13. Какие шаги выполняет алгоритм сортировки выбором? + +Находим минимальный элемент в неотсортированной части + +Меняем его местами с первым элементом неотсортированной части + +Увеличиваем границу отсортированной части на 1 + +Повторяем пока весь массив не будет отсортирован + +14. Как программно реализуется сортировка выбором? + +```python +def selection_sort(arr): + for i in range(len(arr)): + min_idx = i + for j in range(i + 1, len(arr)): + if arr[j] < arr[min_idx]: + min_idx = j + arr[i], arr[min_idx] = arr[min_idx], arr[i] + return arr +``` + +15. В чем достоинства и недостатки метода сортировки выбором? +Достоинства: простая реализация, минимальное количество перестановок +Недостатки: O(n²) в любом случае, неустойчив + + +16. Приведите практический пример сортировки массива методом выбора +Массив: [64, 25, 12, 22, 11] +Шаги: [11, 25, 12, 22, 64] → [11, 12, 25, 22, 64] → [11, 12, 22, 25, 64] → [11, 12, 22, 25, 64] + +17. В чем состоит суть метода сортировки обменом? +Сравниваются соседние элементы и меняются местами, если они находятся в неправильном порядке. + +18. Какие шаги выполняет алгоритм сортировки обменом? + +Проходим по массиву несколько раз + +На каждой итерации сравниваем соседние элементы + +Если порядок неправильный - меняем их местами + +Повторяем до тех пор, пока массив не будет отсортирован + +19. Как программно реализуется сортировка обменом? + +```python +def bubble_sort(arr): + n = len(arr) + for i in range(n): + for j in range(0, n - i - 1): + if arr[j] > arr[j + 1]: + arr[j], arr[j + 1] = arr[j + 1], arr[j] + return arr +``` +20. В чем достоинства и недостатки метода сортировки обменом? +Достоинства: простая реализация, устойчив +Недостатки: O(n²) в худшем случае, медленный на больших массивах + +21. Приведите практический пример сортировки массива методом обмена +Массив: [5, 1, 4, 2, 8] +1 проход: [1, 5, 4, 2, 8] → [1, 4, 5, 2, 8] → [1, 4, 2, 5, 8] → [1, 4, 2, 5, 8] +2 проход: [1, 4, 2, 5, 8] → [1, 2, 4, 5, 8] → [1, 2, 4, 5, 8] + +22. В чем состоит суть метода быстрой сортировки? +Выбирается опорный элемент, массив разбивается на две части: элементы меньше опорного и элементы больше опорного, затем рекурсивно сортируются обе части. + +23. За счет чего метод быстрой сортировки дает лучшие показатели по сравнению с простейшими методами? +За счет принципа "разделяй и властвуй" и в среднем случае времени O(n log n). + +24. Что такое опорный элемент в методе быстрой сортировки и как он используется? +Опорный элемент - это элемент, относительно которого происходит разделение массива. Элементы меньше опорного перемещаются влево, больше - вправо. + +25. Приведите практический пример быстрой сортировки массива +Массив: [10, 80, 30, 90, 40, 50, 70] +Опорный: 70 +Разделение: [10, 30, 40, 50] 70 [80, 90] +Рекурсивная сортировка обеих частей + +26. Что можно сказать о применимости метода быстрой сортировки с точки зрения его эффективности? +Эффективен для больших массивов, но может деградировать до O(n²) при неудачном выборе опорного элемента. + +27. Какой фактор оказывает решающее влияние на эффективность метода быстрой сортировки? +Выбор опорного элемента. + +28. Почему выбор серединного элемента в качестве опорного в методе быстрой сортировки может резко ухудшать эффективность метода? +Если массив уже отсортирован или почти отсортирован, выбор серединного элемента может привести к несбалансированному разделению. + +29. Какое правило выбора опорного элемента в методе быстрой сортировки является наилучшим и почему его сложно использовать? +Медиана трех элементов (первого, среднего и последнего). Сложно использовать из-за дополнительных сравнений. + +30. Какое простое правило выбора опорного элемента в методе быстрой сортировки рекомендуется использовать на практике? +Случайный выбор или медиана трех. + +31. Какие усовершенствования имеет базовый алгоритм метода быстрой сортировки? + +Выбор опорного элемента через медиану трех + +Использование insertion sort для маленьких подмассивов + +Итеративная реализация для избежания переполнения стека + +32. Почему быстрая сортировка проще всего программно реализуется с помощью рекурсии? +Рекурсия естественным образом отражает принцип "разделяй и властвуй". + +33. Как программно реализуется рекурсивный вариант метода быстрой сортировки? + +```python +def quick_sort(arr): + if len(arr) <= 1: + return arr + pivot = arr[len(arr) // 2] + left = [x for x in arr if x < pivot] + middle = [x for x in arr if x == pivot] + right = [x for x in arr if x > pivot] + return quick_sort(left) + middle + quick_sort(right) +``` + +34. Какие особенности имеет не рекурсивная программная реализация метода быстрой сортировки? +Использует стек для хранения границ подмассивов, требует ручного управления памятью. + +35. В чем состоит суть метода пирамидальной сортировки? +Строится двоичная куча из элементов массива, затем многократно извлекается максимальный элемент и перестраивается куча. + +36. Какой набор данных имеет пирамидальную организацию? +Двоичная куча - полное двоичное дерево, где каждый узел больше (max-heap) или меньше (min-heap) своих потомков. + +37. Чем отличаются друг от друга дерево поиска и пирамидальное дерево? +Дерево поиска: левый потомок < родитель < правый потомок +Пирамидальное дерево: родитель > потомков (max-heap) или родитель < потомков (min-heap) + +38. Приведите пример пирамидального дерева с целочисленными ключами + + +```text + 100 + / \ + 19 36 + / \ / + 17 3 25 +``` +39. Какие полезные свойства имеет пирамидальное дерево? + +Максимальный/минимальный элемент всегда в корне + +Эффективные операции вставки и извлечения за O(log n) + +40. Какие шаги выполняются при построении пирамидального дерева? + +Начинаем с последнего нелистового узла + +Просеиваем узел вниз до правильной позиции + +Переходим к предыдущему узлу + +Повторяем до корня + +41. Что такое просеивание элемента через пирамиду? +Процесс перемещения элемента вниз по дереву до тех пор, пока он не займет правильную позицию относительно своих потомков. + +42. Приведите практический пример построения пирамидального дерева +Массив: [4, 10, 3, 5, 1] +Построение: [10, 5, 3, 4, 1] + +43. Какие шаги выполняются на втором этапе пирамидальной сортировки? + +Меняем корень (максимальный элемент) с последним элементом + +Уменьшаем размер кучи на 1 + +Просеиваем новый корень + +Повторяем пока куча не пуста + +44. Приведите практический пример реализации второго этапа пирамидальной сортировки +Куча: [10, 5, 3, 4, 1] +Шаг 1: [1, 5, 3, 4, 10] → Просеиваем: [5, 4, 3, 1, 10] +Шаг 2: [1, 4, 3, 5, 10] → Просеиваем: [4, 1, 3, 5, 10] +и т.д. + +45. Что можно сказать о трудоемкости метода пирамидальной сортировки? +Время: O(n log n) в худшем, среднем и лучшем случае. Память: O(1). + diff --git a/labs/lab_02/var6/comb.jpeg b/labs/lab_02/var6/comb.jpeg new file mode 100644 index 0000000..5fc0147 Binary files /dev/null and b/labs/lab_02/var6/comb.jpeg differ diff --git a/labs/lab_02/var6/comb.png b/labs/lab_02/var6/comb.png new file mode 100644 index 0000000..f45c72a --- /dev/null +++ b/labs/lab_02/var6/comb.png @@ -0,0 +1,55 @@ +┌─────────────────────────────────────────────────────────────────┐ +│ НАЧАЛО СОРТИРОВКИ РАСЧЕСКОЙ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Инициализация: │ +│ - gap = длина(массив) │ +│ - swapped = true │ +│ - factor = 1.3 (коэффициент уменьшения) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌───────────────┐ + │ gap > 1 ИЛИ │ + │ swapped = true? │ + └───────────────┘ + │ + ┌─────────────┴─────────────┐ + ▼ ▼ + НЕТ ┌─────────────────┐ ДА ┌─────────────────┐ + │ КОНЕЦ │ │ swapped = false │ + └─────────────────┘ └─────────────────┘ + │ + ▼ + ┌─────────────────────────┐ + │ gap = max(1, gap/factor)│ + │ i = 0 │ + └─────────────────────────┘ + │ + ▼ + ┌─────────────────────────────────┐ + │ i + gap < длина(массива)? │ + └─────────────────────────────────┘ + │ + ┌─────────────┴─────────────┐ + ▼ ▼ + НЕТ ┌─────────────────┐ ДА ┌─────────────────┐ + │ Возврат к │ │ array[i] > │ + │ проверке gap │ │ array[i+gap]? │ + └─────────────────┘ └─────────────────┘ + │ + ┌─────────────┴─────────────┐ + ▼ ▼ + НЕТ ┌─────────────────┐ ДА ┌─────────────────┐ + │ i = i + 1 │ │ Поменять местами│ + └─────────────────┘ │ array[i] и │ + │ array[i+gap] │ + │ swapped = true │ + └─────────────────┘ + │ + ▼ + ┌─────────────────────┐ + │ i = i + 1 │ + └─────────────────────┘ \ No newline at end of file diff --git a/labs/lab_02/var6/comb_n1.png b/labs/lab_02/var6/comb_n1.png new file mode 100644 index 0000000..c7e5528 Binary files /dev/null and b/labs/lab_02/var6/comb_n1.png differ diff --git a/labs/lab_02/var6/usage_time.py b/labs/lab_02/var6/usage_time.py new file mode 100644 index 0000000..077358b --- /dev/null +++ b/labs/lab_02/var6/usage_time.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Usage Time + +Project: TryPython +A collection of educational materials for learning the Python + +Author: Alexander Krasnikov aka askras + +License: BSD 3 clause +""" + +import functools +import timeit +import typing + + +def get_usage_time( + *, number: int = 1, setup: str = 'pass', ndigits: int = 3 +) -> typing.Callable: + """Decorator for measuring the speed of the function (in seconds) + + Parameters + ---------- + number : int, optional + Number of code repetitions. + setup : str, optional + Code executed once before timing. + ndigits : int, optionalgit status + Number of decimal places in the returned value. + + Returns + ------- + decorator: typing.Callable + Decorator for measuring the time of the function in seconds. + + See Also + -------- + timeit + Measure execution time of small code snippets. + + References + ---------- + [1] timeit documentation : https://docs.python.org/3/library/timeit.html + + Examples + -------- + Decorating an existing function: + + >>> import time + >>> def sleep_func(n): + ... time.sleep(n) + ... return n + ... + >>> get_usage_time_sleep_func = get_usage_time()(sleep_func) + >>> time_sleep_func = get_usage_time_sleep_func(2) + >>> print(f'The function was executed for {time_sleep_func} seconds') + The function was executed for 2.0 seconds + >>> get_usage_time(number=5)(sleep_func)(4) + 4.0 + + Measuring the running time of a function for different parameter values: + + >>> import time + >>> def sleep_func(n): + ... time.sleep(n) + ... return n + ... + >>> for n in range(1,4): + ... get_usage_time_sleep_func = get_usage_time(number=2)(sleep_func) + ... print(get_usage_time_sleep_func(n)) + 1.0 + 2.0 + 3.0 + + Using the `setup` option: + + >>> import time + >>> def sleep_func(n): + ... time.sleep(n) + ... return n + ... + >>> setup = 'print("Start setup"); time.sleep(10); print("End setup")' + >>> get_usage_time_sleep_func = get_usage_time(setup=setup)(sleep_func) + >>> print(get_usage_time_sleep_func(3)) + Start setup + End setup + 3.0 + + Decoding the generated function: + + >>> import time + >>> @get_usage_time(number=2, setup='print("Start");', ndigits=0) + ... def sleep_func(n): + ... time.sleep(n) + ... return n + ... + >>> time_sleep_func = sleep_func(3) + Start + >>> print(time_sleep_func) + 3.0 + """ + + def decorator(func: typing.Callable) -> typing.Callable: + @functools.wraps(func) + def wrapper(*args, **kwargs) -> float: + usage_time = timeit.timeit( + lambda: func(*args, **kwargs), + setup=setup, + number=number, + ) + return round(usage_time / number, ndigits) + + return wrapper + + return decorator + + +if __name__ == '__main__': + import time + + def sleep_func(n): + time.sleep(n) + return n + + for i in range(1, 4): + time_sleep_func = get_usage_time(number=3)(sleep_func) + print(time_sleep_func(i)) diff --git a/labs/lab_03/Untitled.md b/labs/lab_03/Untitled.md new file mode 100644 index 0000000..056b7c5 --- /dev/null +++ b/labs/lab_03/Untitled.md @@ -0,0 +1,646 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.17.3 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +## Лабораторная работа №3 +## Вариант 6 +Цели лабораторной работы: +- Изучить, что такое линейный список — структура данных, где элементы связаны ссылками, а не расположены подряд в памяти. +- Научиться создавать и использовать односвязные линейные списки в программе. +- Освоить основные операции со списками: добавление, удаление, поиск элементов. +- Развить навыки работы с динамической памятью при построении и изменении списков. +- Научиться писать функции для обработки списков (например, удаление повторов, разворот, сортировка). +- Приобрести умения структурного и модульного программирования через реализацию этих задач. + +## Задание 1 +Версия 1: + +```python +from typing import Any, Self +import doctest + +class Node: + def __init__(self, data: Any = None, next: 'Node' = None): + self.data = data + self.next = next + + def __repr__(self): + return f'Node(data={self.data}, next={self.next})' + + +class SingleLinkedListV1: + def __init__(self) -> Self: + self._head = None + + def insert_first_node(self, value: Any) -> None: + '''Добавить элемент в начало списка''' + self._head = Node(value, self._head) + + def remove_first_node(self) -> Any: + '''Удалить первый элемент списка''' + if self._head is None: + raise ValueError("Список пуст") + temp = self._head.data + self._head = self._head.next + return temp + + def insert_last_node(self, value: Any) -> None: + '''Добавить элемент в конец списка''' + if self._head is None: + self.insert_first_node(value) + else: + current_node = self._head + while current_node.next is not None: + current_node = current_node.next + current_node.next = Node(value) + + def remove_last_node(self) -> Any: + '''Удалить последний элемент списка''' + if self._head is None: + raise ValueError("Список пуст") + + if self._head.next is None: + return self.remove_first_node() + + current_node = self._head + while current_node.next.next is not None: + current_node = current_node.next + temp = current_node.next.data + current_node.next = None + return temp + + def __repr__(self) -> str: + return f'SingleLinkedListV1({self._head})' + + def __str__(self): + node = self._head + elements = [] + while node: + elements.append(str(node.data)) + node = node.next + return 'LinkedList.head -> ' + ' -> '.join(elements) + ' -> None' + +``` + +Версия 2: Добавлены методы поиска, замены и удаления по значению + получение размера + +```python +class SingleLinkedListV2(SingleLinkedListV1): + def get_size(self) -> int: + '''Вернуть размер списка (сложность O(n))''' + count = 0 + current = self._head + while current: + count += 1 + current = current.next + return count + + def find_node(self, value: Any) -> Node: + '''Найти первый узел с заданным значением''' + current = self._head + while current: + if current.data == value: + return current + current = current.next + return None + + def replace_node(self, old_value: Any, new_value: Any) -> bool: + '''Заменить значение первого найденного узла''' + node = self.find_node(old_value) + if node: + node.data = new_value + return True + return False + + def remove_node(self, value: Any) -> bool: + '''Удалить первый узел с заданным значением''' + if self._head is None: + return False + + if self._head.data == value: + self.remove_first_node() + return True + + current = self._head + while current.next: + if current.next.data == value: + current.next = current.next.next + return True + current = current.next + + return False +``` + +Версия 3: Добавлено хранение размера списка для оптимизации + +```python +class SingleLinkedListV3(SingleLinkedListV2): + def __init__(self) -> Self: + '''Инициализация с счетчиком размера''' + super().__init__() + self.size = 0 + + def insert_first_node(self, value: Any) -> None: + '''Добавление в начало с обновлением размера''' + super().insert_first_node(value) + self.size += 1 + + def insert_last_node(self, value: Any) -> None: + '''Добавление в конец с обновлением размера''' + super().insert_last_node(value) + self.size += 1 + + def remove_first_node(self) -> Any: + '''Удаление из начала с обновлением размера''' + result = super().remove_first_node() + self.size -= 1 + return result + + def remove_last_node(self) -> Any: + '''Удаление из конца с обновлением размера''' + result = super().remove_last_node() + self.size -= 1 + return result + + def remove_node(self, value: Any) -> bool: + '''Удаление узла по значению с обновлением размера''' + if super().remove_node(value): + self.size -= 1 + return True + return False + + def get_size(self) -> int: + '''Получение размера за O(1)''' + return self.size +``` + +Версия 4: Добавлены методы для работы с соседними узлами + +```python +class SingleLinkedListV4(SingleLinkedListV3): + def find_previous_node(self, value: Any) -> Any: + '''Найти значение предыдущего узла относительно узла с заданным значением''' + if self._head is None or self._head.data == value: + return None + + current = self._head + while current.next: + if current.next.data == value: + return current.data + current = current.next + return None + + def find_next_node(self, value: Any) -> Any: + '''Найти значение следующего узла относительно узла с заданным значением''' + node = self.find_node(value) + if node and node.next: + return node.next.data + return None + + def insert_before_node(self, target_value: Any, new_value: Any) -> bool: + '''Вставить новый узел перед узлом с заданным значением''' + if self._head is None: + return False + + if self._head.data == target_value: + self.insert_first_node(new_value) + return True + + current = self._head + while current.next: + if current.next.data == target_value: + new_node = Node(new_value, current.next) + current.next = new_node + self.size += 1 + return True + current = current.next + + return False + + def insert_after_node(self, target_value: Any, new_value: Any) -> bool: + '''Вставить новый узел после узла с заданным значением''' + node = self.find_node(target_value) + if node: + new_node = Node(new_value, node.next) + node.next = new_node + self.size += 1 + return True + return False + + def replace_previous_node(self, target_value: Any, new_value: Any) -> bool: + '''Заменить значение в предыдущем узле относительно узла с заданным значением''' + if self._head is None or self._head.data == target_value: + return False + + current = self._head + while current.next: + if current.next.data == target_value: + current.data = new_value + return True + current = current.next + return False + + def replace_next_node(self, target_value: Any, new_value: Any) -> bool: + '''Заменить значение в следующем узле относительно узла с заданным значением''' + node = self.find_node(target_value) + if node and node.next: + node.next.data = new_value + return True + return False + + def remove_previous_node(self, target_value: Any) -> Any: + '''Удалить предыдущий узел относительно узла с заданным значением''' + if self._head is None or self._head.data == target_value: + return None + + if self._head.next and self._head.next.data == target_value: + return self.remove_first_node() + + current = self._head + while current.next and current.next.next: + if current.next.next.data == target_value: + removed_data = current.next.data + current.next = current.next.next + self.size -= 1 + return removed_data + current = current.next + return None + + def remove_next_node(self, target_value: Any) -> Any: + '''Удалить следующий узел относительно узла с заданным значением''' + node = self.find_node(target_value) + if node and node.next: + removed_data = node.next.data + node.next = node.next.next + self.size -= 1 + return removed_data + return None +``` + +Версия 5: Добавлен указатель на хвост для оптимизации операций + +```python +class SingleLinkedListV5(SingleLinkedListV4): + def __init__(self) -> Self: + '''Инициализация с указателем на хвост''' + super().__init__() + self._tail = None + + def insert_first_node(self, value: Any) -> None: + '''Добавление в начало с обновлением хвоста''' + super().insert_first_node(value) + if self.size == 1: + self._tail = self._head + + def insert_last_node(self, value: Any) -> None: + '''Добавление в конец за O(1) с использованием хвоста''' + if self._head is None: + self.insert_first_node(value) + else: + new_node = Node(value) + self._tail.next = new_node + self._tail = new_node + self.size += 1 + + def remove_first_node(self) -> Any: + '''Удаление из начала с обновлением хвоста''' + result = super().remove_first_node() + if self.size == 0: + self._tail = None + return result + + def remove_last_node(self) -> Any: + '''Удаление из конца с обновлением хвоста''' + if self._head is None: + raise ValueError("Список пуст") + + if self._head.next is None: + return self.remove_first_node() + + current = self._head + while current.next != self._tail: + current = current.next + + result = self._tail.data + current.next = None + self._tail = current + self.size -= 1 + + return result + + def find_node(self, value: Any) -> Node: + '''Оптимизированный поиск: O(1) для головы и хвоста''' + if self._head is None: + return None + + if self._head.data == value: + return self._head + + if self._tail and self._tail.data == value: + return self._tail + + return super().find_node(value) + + def insert_after_node(self, target_value: Any, new_value: Any) -> bool: + '''Оптимизированная вставка после узла: O(1) для хвоста''' + if self._tail and self._tail.data == target_value: + new_node = Node(new_value) + self._tail.next = new_node + self._tail = new_node + self.size += 1 + return True + + return super().insert_after_node(target_value, new_value) + + def replace_node(self, old_value: Any, new_value: Any) -> bool: + '''Оптимизированная замена: O(1) для головы и хвоста''' + if self._head is None: + return False + + if self._head.data == old_value: + self._head.data = new_value + return True + + if self._tail and self._tail.data == old_value: + self._tail.data = new_value + return True + + return super().replace_node(old_value, new_value) + + def remove_node(self, value: Any) -> bool: + '''Оптимизированное удаление: O(1) для головы и хвоста''' + if self._head is None: + return False + + if self._head.data == value: + self.remove_first_node() + return True + + if self._tail and self._tail.data == value: + self.remove_last_node() + return True + + return super().remove_node(value) + + def replace_next_node(self, target_value: Any, new_value: Any) -> bool: + '''Оптимизированная замена следующего узла: O(1) проверка для хвоста''' + if self._tail and self._tail.data == target_value: + return False + + return super().replace_next_node(target_value, new_value) + + def remove_next_node(self, target_value: Any) -> Any: + '''Оптимизированное удаление следующего узла: O(1) проверка для хвоста''' + if self._tail and self._tail.data == target_value: + return None + + return super().remove_next_node(target_value) + + def find_next_node(self, value: Any) -> Any: + '''Оптимизированный поиск следующего узла: O(1) проверка для хвоста''' + if self._tail and self._tail.data == value: + return None + + return super().find_next_node(value) + + def get_tail(self) -> Any: + '''Получить значение хвоста за O(1)''' + return self._tail.data if self._tail else None + + def __str__(self): + '''Строковое представление с информацией о хвосте''' + base_str = super().__str__() + tail_info = f" (tail: {self.get_tail()})" if self._tail else " (tail: None)" + return base_str + tail_info +``` + +Версия 6: Оптимизация операций через замену значений + +```python +class SingleLinkedListV6(SingleLinkedListV5): + + def insert_before_node_optimized(self, target_node: Node, new_value: Any) -> bool: + '''Оптимизированная вставка перед узлом за O(1) через замену значений''' + if target_node is None: + return False + + new_node = Node(target_node.data, target_node.next) + target_node.data = new_value + target_node.next = new_node + + if self._tail == target_node: + self._tail = new_node + + self.size += 1 + return True + + def remove_node_optimized(self, target_node: Node) -> Any: + '''Оптимизированное удаление узла за O(1) через замену значений''' + if target_node is None or target_node.next is None: + return None + + removed_data = target_node.data + target_node.data = target_node.next.data + target_node.next = target_node.next.next + + if target_node.next is None: + self._tail = target_node + + self.size -= 1 + return removed_data + + def remove_previous_node_optimized(self, target_node: Node) -> Any: + '''Оптимизированное удаление предыдущего узла за O(1)''' + if (target_node is None or self._head is None or + self._head == target_node or self._head.next == target_node): + return None + + if self._head.next == target_node: + return self.remove_first_node() + + current = self._head + while current.next and current.next.next: + if current.next.next == target_node: + removed_data = current.next.data + current.next = target_node + self.size -= 1 + return removed_data + current = current.next + return None + + def insert_before_node(self, target_value: Any, new_value: Any) -> bool: + '''Переопределение для использования оптимизации при наличии узла''' + target_node = self.find_node(target_value) + if target_node: + return self.insert_before_node_optimized(target_node, new_value) + return False + + def remove_node(self, value: Any) -> bool: + '''Переопределение для использования оптимизации при наличии узла''' + if self._head and self._head.data == value: + self.remove_first_node() + return True + + if self._tail and self._tail.data == value: + self.remove_last_node() + return True + + target_node = self.find_node(value) + if target_node: + self.remove_node_optimized(target_node) + return True + + return False + + def remove_previous_node(self, target_value: Any) -> Any: + '''Переопределение для использования оптимизации при наличии узла''' + if self._head is None or self._head.data == target_value: + return None + + target_node = self.find_node(target_value) + if target_node: + return self.remove_previous_node_optimized(target_node) + return None + + def sort(self) -> None: + '''Сортировка списка (пузырьковая сортировка)''' + if self.size < 2: + return + + for i in range(self.size): + current = self._head + for j in range(self.size - i - 1): + if current.data > current.next.data: + current.data, current.next.data = current.next.data, current.data + current = current.next + + def get_middle_node(self) -> Node: + '''Найти средний узел за один проход (для демонстрации)''' + if self._head is None: + return None + + slow = self._head + fast = self._head + + while fast and fast.next: + slow = slow.next + fast = fast.next.next + + return slow + + #Задание 2 + + def reverse(self) -> None: + if self._head is None or self._head.next is None: + return + + prev = None + current = self._head + self._tail = self._head + + while current: + next_node = current.next + current.next = prev + prev = current + current = next_node + + self._head = prev + + #Задание 3 (сортировка вставками) + + def sort_insertion(self) -> None: + if self.size < 2: + return + + sorted_head = None + + current = self._head + while current: + next_node = current.next + + if sorted_head is None or current.data < sorted_head.data: + current.next = sorted_head + sorted_head = current + else: + temp = sorted_head + while temp.next and temp.next.data < current.data: + temp = temp.next + current.next = temp.next + temp.next = current + + current = next_node + + self._head = sorted_head + + if self._head: + current = self._head + while current.next: + current = current.next + self._tail = current +``` + +## Задание 2 +Сложность: O(n), Память: O(1) + +## Задание 3 +1. Лучший случай (уже отсортированный список): +- Вставка всегда происходит в начало: O(1) на элемент +- Итого: O(n) +2. Худший случай (обратно отсортированный список): +- Каждый новый элемент вставляется в конец отсортированной части +- Для i-го элемента нужно пройти i узлов +- Сумма: 1 + 2 + 3 + ... + (n-1) = n(n-1)/2 +- Итого: O(n²) +3. Средний случай: +- В среднем для каждого элемента нужно пройти половину отсортированной части +- Итого: O(n²) + +## Задание 4 + +```python +def remove_consecutive_duplicates(linked_list: 'SingleLinkedListV6') -> None: + if linked_list._head is None or linked_list._head.next is None: + return + + current = linked_list._head + + while current and current.next: + if current.data == current.next.data: + current.next = current.next.next + linked_list.size -= 1 + + if current.next is None: + linked_list._tail = current + else: + # Переходим к следующему узлу + current = current.next +``` + +## Таблица из контрольных вопросов +| Операция | Односвязный список (без tail) | Односвязный список (с tail) | Двусвязный список (с tail) | +| :--- | :---: | :---: | :---: | +| Вставка в начало | O(1) | O(1) | O(1) | +| Вставка в конец | O(n) | O(1) | O(1) | +| Удаление из начала | O(1) | O(1) | O(1) | +| Удаление из конца | O(n) | O(n) | O(1) | +| Поиск элемента по значению | O(n) | O(n) | O(n) | +| Вставка после известного узла | O(1) | O(1) | O(1) | +| Удаление известного узла | O(n) | O(n) | O(1) | +| Доступ к элементу по индексу | O(n) | O(n) | O(n) | + +## Выводы +- Линейные списки — это важная структура данных, позволяющая динамически хранить и обрабатывать упорядоченные наборы элементов без необходимости contiguous памяти. +- В ходе лабораторной работы были изучены основные виды списков (односвязный, двусвязный, кольцевой), а также реализованы базовые операции над ними: добавление, удаление, обход. +- Выполнение заданий позволило закрепить навыки работы с динамической памятью и освоить приемы структурного программирования на языке C++. +- Реализация функций обработки списков (удаление повторов, реверс, сортировка) помогла понять особенности алгоритмов и их временную сложность при работе со связными структурами. +- Полученные знания и практические навыки пригодятся при работе с более сложными структурами данных и алгоритмами в программировании. diff --git a/labs/lab_04/Untitled.md b/labs/lab_04/Untitled.md new file mode 100644 index 0000000..fd12007 --- /dev/null +++ b/labs/lab_04/Untitled.md @@ -0,0 +1,577 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.17.3 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +## Лаборатрная работа №4 +Цели: +- Познакомиться со структурами данных стек, очередь и дек, понять, как они устроены и работают. +- Научиться реализовывать эти структуры на основе массивов и связных списков. +- Изучить основные операции для каждой структуры (добавление, удаление, просмотр элементов). +- На практике применить стек для решения задач: проверка правильности скобок и вычисление выражений в обратной польской записи. +- Освоить перевод математических выражений из обычной записи в постфиксную форму. +### Базовые классы: + +```python +class Stack: + """Базовый класс стека""" + def __init__(self): + self.items = [] + + def push(self, item): + raise NotImplementedError("Метод должен быть реализован в подклассе") + + def pop(self): + raise NotImplementedError("Метод должен быть реализован в подклассе") + + def peek(self): + raise NotImplementedError("Метод должен быть реализован в подклассе") + + def is_empty(self): + raise NotImplementedError("Метод должен быть реализован в подклассе") + + def size(self): + raise NotImplementedError("Метод должен быть реализован в подклассе") + +class Queue: + """Базовый класс очереди""" + def __init__(self): + self.items = [] + + def enqueue(self, item): + raise NotImplementedError("Метод должен быть реализован в подклассе") + + def dequeue(self): + raise NotImplementedError("Метод должен быть реализован в подклассе") + + def front(self): + raise NotImplementedError("Метод должен быть реализован в подклассе") + + def is_empty(self): + raise NotImplementedError("Метод должен быть реализован в подклассе") + + def size(self): + raise NotImplementedError("Метод должен быть реализован в подклассе") + +class Deque: + """Базовый класс дека с методами по ТЗ""" + def __init__(self): + self.items = [] + + def insert_left(self, value): + raise NotImplementedError("Метод должен быть реализован в подклассе") + + def insert_right(self, value): + raise NotImplementedError("Метод должен быть реализован в подклассе") + + def remove_left(self): + raise NotImplementedError("Метод должен быть реализован в подклассе") + + def remove_right(self): + raise NotImplementedError("Метод должен быть реализован в подклассе") + + def get_left(self): + raise NotImplementedError("Метод должен быть реализован в подклассе") + + def get_right(self): + raise NotImplementedError("Метод должен быть реализован в подклассе") + + def is_empty(self): + raise NotImplementedError("Метод должен быть реализован в подклассе") + + def size(self): + raise NotImplementedError("Метод должен быть реализован в подклассе") +``` + +### Задание 1 +1. Реализовать стек на основе массива + +```python +class ArrayStack(Stack): + def __init__(self): + super().__init__() + self.items = [] + + def push(self, item): + self.items.append(item) + + def pop(self): + if self.is_empty(): + raise IndexError("Стек пуст") + return self.items.pop() + + def peek(self): + if self.is_empty(): + raise IndexError("Стек пуст") + return self.items[-1] + + def is_empty(self): + return len(self.items) == 0 + + def size(self): + return len(self.items) + + def __str__(self): + return f"ArrayStack({self.items})" +``` + +2. Реализовать стек на основе связного списка. + +```python +class Node: + """Узел связного списка""" + def __init__(self, data): + self.data = data + self.next = None + +class LinkedListStack(Stack): + def __init__(self): + super().__init__() + self.head = None + self._size = 0 + + def push(self, item): + new_node = Node(item) + new_node.next = self.head + self.head = new_node + self._size += 1 + + def pop(self): + if self.is_empty(): + raise IndexError("Стек пуст") + data = self.head.data + self.head = self.head.next + self._size -= 1 + return data + + def peek(self): + if self.is_empty(): + raise IndexError("Стек пуст") + return self.head.data + + def is_empty(self): + return self.head is None + + def size(self): + return self._size + + def __str__(self): + items = [] + current = self.head + while current: + items.append(current.data) + current = current.next + return f"LinkedListStack({items})" +``` + +3. Реализовать очередь на основе массива. + +```python +class ArrayQueue(Queue): + def __init__(self): + super().__init__() + self.items = [] + + def enqueue(self, item): + self.items.append(item) + + def dequeue(self): + if self.is_empty(): + raise IndexError("Очередь пуста") + return self.items.pop(0) + + def front(self): + if self.is_empty(): + raise IndexError("Очередь пуста") + return self.items[0] + + def is_empty(self): + return len(self.items) == 0 + + def size(self): + return len(self.items) + + def __str__(self): + return f"ArrayQueue({self.items})" +``` + +4. Реализовать очередь на основе связного списка. + +```python +class LinkedListQueue(Queue): + def __init__(self): + super().__init__() + self.head = None + self.tail = None + self._size = 0 + + def enqueue(self, item): + new_node = Node(item) + if self.is_empty(): + self.head = self.tail = new_node + else: + self.tail.next = new_node + self.tail = new_node + self._size += 1 + + def dequeue(self): + if self.is_empty(): + raise IndexError("Очередь пуста") + data = self.head.data + self.head = self.head.next + if self.head is None: + self.tail = None + self._size -= 1 + return data + + def front(self): + if self.is_empty(): + raise IndexError("Очередь пуста") + return self.head.data + + def is_empty(self): + return self.head is None + + def size(self): + return self._size + + def __str__(self): + items = [] + current = self.head + while current: + items.append(current.data) + current = current.next + return f"LinkedListQueue({items})" +``` + +5. Реализовать дек на основе массива. + +```python +class ArrayDeque(Deque): + """Дек на основе массива""" + def __init__(self): + super().__init__() + self.items = [] + + def insert_left(self, value): + """Помещает объект на левый конец дека""" + self.items.insert(0, value) + + def insert_right(self, value): + """Помещает объект на правый конец дека""" + self.items.append(value) + + def remove_left(self): + """Удаляет первый объект с левого конца дека и возвращает его""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self.items.pop(0) + + def remove_right(self): + """Удаляет первый объект с правого конца дека и возвращает его""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self.items.pop() + + def get_left(self): + """Возвращает первый объект с левого конца дека не удаляя его""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self.items[0] + + def get_right(self): + """Возвращает первый объект с правого конца дека не удаляя его""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self.items[-1] + + def is_empty(self): + return len(self.items) == 0 + + def size(self): + return len(self.items) + + def __str__(self): + return f"ArrayDeque({self.items})" +``` + +6. Реализовать дек на основе связного списка. + +```python +class LinkedListDeque(Deque): + """Дек на основе связного списка с методами по ТЗ""" + def __init__(self): + super().__init__() + self.head = None + self.tail = None + self._size = 0 + + def insert_left(self, value): + """Помещает объект на левый конец дека""" + new_node = DoublyNode(value) + if self.is_empty(): + self.head = self.tail = new_node + else: + new_node.next = self.head + self.head.prev = new_node + self.head = new_node + self._size += 1 + + def insert_right(self, value): + """Помещает объект на правый конец дека""" + new_node = DoublyNode(value) + if self.is_empty(): + self.head = self.tail = new_node + else: + new_node.prev = self.tail + self.tail.next = new_node + self.tail = new_node + self._size += 1 + + def remove_left(self): + """Удаляет первый объект с левого конца дека и возвращает его""" + if self.is_empty(): + raise IndexError("Дек пуст") + data = self.head.data + if self.head == self.tail: + self.head = self.tail = None + else: + self.head = self.head.next + self.head.prev = None + self._size -= 1 + return data + + def remove_right(self): + """Удаляет первый объект с правого конца дека и возвращает его""" + if self.is_empty(): + raise IndexError("Дек пуст") + data = self.tail.data + if self.head == self.tail: + self.head = self.tail = None + else: + self.tail = self.tail.prev + self.tail.next = None + self._size -= 1 + return data + + def get_left(self): + """Возвращает первый объект с левого конца дека не удаляя его""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self.head.data + + def get_right(self): + """Возвращает первый объект с правого конца дека не удаляя его""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self.tail.data + + def is_empty(self): + return self.head is None + + def size(self): + return self._size + + def __str__(self): + items = [] + current = self.head + while current: + items.append(current.data) + current = current.next + return f"LinkedListDeque({items})" +``` + +### Задание 2 +Используя операции со стеком, написать программу, проверяющую своевременность закрытия скобок «(, ), [, ] ,{, }» в строке символов (строка состоит из одних скобок этих типов). + +В процессе решения анализируются символы строки. Если встречена одна из открывающихся скобок, то она записывается в стек. При обнаружении закрывающейся скобки, соответствующей скобке, находящейся в вершине стека, последняя удаляется. При несоответствии скобки выдается сообщение об ошибке, которое фиксируется в логической переменной. + +```python +def check_brackets(expression): + """ + Проверяет корректность расстановки скобок в выражении + Возвращает True если скобки расставлены правильно, False в противном случае + """ + stack = ArrayStack() + brackets = {'(': ')', '[': ']', '{': '}'} + + for char in expression: + if char in brackets.keys(): + stack.push(char) + elif char in brackets.values(): + if stack.is_empty(): + return False + opening_bracket = stack.pop() + if brackets[opening_bracket] != char: + return False + + return stack.is_empty() + +def test_bracket_checker(): + test_cases = [ + ("()", True), + ("()[]{}", True), + ("(]", False), + ("([)]", False), + ("{[]}", True), + ("((()))", True), + ("((())", False), + ("", True) + ] + + print("Тестирование проверки скобочной последовательности:") + print("=" * 55) + print(f"{'Выражение':<15} {'Результат':<10} {'Ожидалось':<10} {'Статус':<6}") + print("-" * 55) + + for expr, expected in test_cases: + result = check_brackets(expr) + status = "✓" if result == expected else "✗" + print(f"{expr:<15} {str(result):<10} {str(expected):<10} {status:<6}") + + print("=" * 55) + +test_bracket_checker() +``` + +### Задание 3 + +Написать программу вычисления значения выражения, представленного в обратной польской записи (в постфиксной записи). Выражение состоит из цифр от 1 до 9 и знаков операции. + +| Обычная (инфиксная) запись | Обратная польская (постфиксная) запись | +|:---|:---| +| (a+b) * c | a b + c * | +| a + (b+c)*d | a b c + d * + | + +Просматривая строку, анализируем очередной символ, если это: + - цифра, то записываем ее в стек; + - знак, то читаем два элемента из стека, выполняем математическую операцию, определяемую этим знаком, и заносим результат в стек. + +После просмотра всей строки в стеке должен оставаться один элемент, он и является решением задачи. + +```python +def evaluate_rpn(expression): + """ + Вычисляет значение выражения в обратной польской записи + """ + stack = ArrayStack() + operators = { + '+': lambda x, y: x + y, + '-': lambda x, y: x - y, + '*': lambda x, y: x * y, + '/': lambda x, y: x / y + } + + for token in expression.split(): + if token.isdigit() or (token[0] == '-' and token[1:].isdigit()): + stack.push(float(token)) + elif token in operators: + if stack.size() < 2: + raise ValueError("Недостаточно операндов для операции") + y = stack.pop() + x = stack.pop() + result = operators[token](x, y) + stack.push(result) + else: + raise ValueError(f"Неизвестный токен: {token}") + + if stack.size() != 1: + raise ValueError("Некорректное выражение") + + return stack.pop() + +def test_rpn_evaluator(): + test_cases = [ + ("3 4 +", 7), + ("5 2 * 3 +", 13), + ("10 5 / 2 *", 4), + ("4 2 + 3 * 6 -", 12), + ("1 2 + 4 * 3 +", 15) + ] + + print("Тестирование вычисления RPN выражений:") + print("-" * 50) + for expr, expected in test_cases: + result = evaluate_rpn(expr) + status = "Правильно" if abs(result - expected) < 1e-9 else "Не правиильно" + print(f"{status} '{expr}' = {result} (ожидалось: {expected})") + print() + +test_rpn_evaluator() +``` + +### Задание 4 +Реализовать перевод математических выражений из инфиксной в постфиксную форму записи. + +```python +def infix_to_postfix(expression): + """ + Переводит инфиксное выражение в постфиксную запись (обратную польскую) + """ + precedence = {'+': 1, '-': 1, '*': 2, '/': 2, '^': 3} + + output = [] + stack = ArrayStack() + + for token in expression.split(): + if token.isdigit() or (token[0] == '-' and token[1:].isdigit()): + output.append(token) + elif token == '(': + stack.push(token) + elif token == ')': + while not stack.is_empty() and stack.peek() != '(': + output.append(stack.pop()) + stack.pop() + elif token in precedence: + while (not stack.is_empty() and + stack.peek() != '(' and + precedence.get(stack.peek(), 0) >= precedence[token]): + output.append(stack.pop()) + stack.push(token) + + while not stack.is_empty(): + output.append(stack.pop()) + + return ' '.join(output) + +def test_infix_to_postfix(): + test_cases = [ + ("3 + 4", "3 4 +"), + ("3 + 4 * 2", "3 4 2 * +"), + ("( 3 + 4 ) * 2", "3 4 + 2 *"), + ("3 + 4 * 2 / ( 1 - 5 )", "3 4 2 * 1 5 - / +"), + ("a + b * c", "a b c * +") + ] + + print("Тестирование перевода из инфиксной в постфиксную запись:") + print("-" * 50) + for infix, expected_postfix in test_cases: + result = infix_to_postfix(infix) + status = "Правиьно" if result == expected_postfix else "Не правильно" + print(f"{status} '{infix}' -> '{result}' (ожидалось: '{expected_postfix}')") + print() + +test_infix_to_postfix() +``` + +### Контрольные вопросы + +| Операция | Стек (список) | Очередь (список) | Дек (двусвязный список) | Дек (массив) | +| :--- | :---: | :---: | :---: | :---: | +| `push` / `enqueue` / `pushBack` | **O(1)** | **O(1)** | **O(1)** | **O(1)*** | +| `pop` / `dequeue` / `popBack` | **O(1)** | **O(1)** | **O(1)** | **O(1)*** | +| `pushFront` | — | — | **O(1)** | **O(n)** | +| `popFront` | — | — | **O(1)** | **O(n)** | +| `peek` / `front` / `back` | **O(1)** | **O(1)** | **O(1)** | **O(1)** | +| `isEmpty` | **O(1)** | **O(1)** | **O(1)** | **O(1)** | + + diff --git a/labs/lab_05/Untitled.md b/labs/lab_05/Untitled.md new file mode 100644 index 0000000..c563e73 --- /dev/null +++ b/labs/lab_05/Untitled.md @@ -0,0 +1,437 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.17.3 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +## Хеш-функции и хеш-таблицы + +## Задание 1: + +```python +class HashTableChaining: + def __init__(self, size=10): + self.size = size + self.table = [[] for _ in range(size)] + self.count = 0 + + def _hash(self, key): + return hash(key) % self.size + + def insert(self, key, value): + index = self._hash(key) + bucket = self.table[index] + + for i, (k, v) in enumerate(bucket): + if k == key: + bucket[i] = (key, value) + return + + bucket.append((key, value)) + self.count += 1 + + if self.count / self.size > 0.7: + self._rehash() + + def search(self, key): + index = self._hash(key) + bucket = self.table[index] + + for k, v in bucket: + if k == key: + return v + return None + + def delete(self, key): + index = self._hash(key) + bucket = self.table[index] + + for i, (k, v) in enumerate(bucket): + if k == key: + del bucket[i] + self.count -= 1 + return True + return False + + def _rehash(self): + old_table = self.table + self.size *= 2 + self.table = [[] for _ in range(self.size)] + self.count = 0 + + for bucket in old_table: + for key, value in bucket: + self.insert(key, value) + + def __str__(self): + result = [] + for i, bucket in enumerate(self.table): + if bucket: + result.append(f"{i}: {bucket}") + return "\n".join(result) +``` + +Тест: + +```python +print("=== Тестирование HashTableChaining ===") + +ht = HashTableChaining(5) + +ht.insert("apple", 1) +ht.insert("banana", 2) +ht.insert("cherry", 3) + +assert ht.search("apple") == 1, "Ошибка поиска apple" +assert ht.search("banana") == 2, "Ошибка поиска banana" +assert ht.search("cherry") == 3, "Ошибка поиска cherry" +assert ht.search("unknown") is None, "Ошибка поиска неизвестного ключа" +print("✓ Тест вставки и поиска пройден") + +ht.insert("apple", 10) +assert ht.search("apple") == 10, "Ошибка обновления значения" +print("✓ Тест обновления значения пройден") + +assert ht.delete("banana") == True, "Ошибка удаления существующего ключа" +assert ht.search("banana") is None, "Ошибка: ключ не удален" +assert ht.delete("unknown") == False, "Ошибка удаления несуществующего ключа" +print("✓ Тест удаления пройден") + +ht2 = HashTableChaining(1) +ht2.insert("key1", "value1") +ht2.insert("key2", "value2") +ht2.insert("key3", "value3") + +assert ht2.search("key1") == "value1", "Ошибка при коллизии" +assert ht2.search("key2") == "value2", "Ошибка при коллизии" +assert ht2.search("key3") == "value3", "Ошибка при коллизии" +print("✓ Тест коллизий пройден") + +print("Все тесты HashTableChaining пройдены успешно!\n") +``` + +## Задание 2: + +```python +class HashTableOpenAddressing: + def __init__(self, size=10): + self.size = size + self.table = [None] * size + self.count = 0 + self.DELETED = object() + + def _hash(self, key, i=0): + return (hash(key) + i) % self.size + + def insert(self, key, value): + if self.count / self.size > 0.7: + self._rehash() + + i = 0 + while i < self.size: + index = self._hash(key, i) + + if self.table[index] is None or self.table[index] is self.DELETED: + self.table[index] = (key, value) + self.count += 1 + return + elif self.table[index][0] == key: + self.table[index] = (key, value) + return + + i += 1 + + self._rehash() + self.insert(key, value) + + def search(self, key): + i = 0 + while i < self.size: + index = self._hash(key, i) + + if self.table[index] is None: + return None + elif self.table[index] is not self.DELETED and self.table[index][0] == key: + return self.table[index][1] + + i += 1 + + return None + + def delete(self, key): + i = 0 + while i < self.size: + index = self._hash(key, i) + + if self.table[index] is None: + return False + elif self.table[index] is not self.DELETED and self.table[index][0] == key: + self.table[index] = self.DELETED + self.count -= 1 + return True + + i += 1 + + return False + + def _rehash(self): + old_table = self.table + self.size *= 2 + self.table = [None] * self.size + self.count = 0 + + for item in old_table: + if item is not None and item is not self.DELETED: + self.insert(item[0], item[1]) + + def __str__(self): + result = [] + for i, item in enumerate(self.table): + if item is not None and item is not self.DELETED: + result.append(f"{i}: {item}") + elif item is self.DELETED: + result.append(f"{i}: DELETED") + return "\n".join(result) +``` + +Тесты + +```python +print("=== Тестирование HashTableOpenAddressing ===") + +ht = HashTableOpenAddressing(5) + +ht.insert("apple", 1) +ht.insert("banana", 2) +ht.insert("cherry", 3) + +assert ht.search("apple") == 1, "Ошибка поиска apple" +assert ht.search("banana") == 2, "Ошибка поиска banana" +assert ht.search("cherry") == 3, "Ошибка поиска cherry" +assert ht.search("unknown") is None, "Ошибка поиска неизвестного ключа" +print("✓ Тест вставки и поиска пройден") + +ht.insert("apple", 10) +assert ht.search("apple") == 10, "Ошибка обновления значения" +print("✓ Тест обновления значения пройден") + +assert ht.delete("banana") == True, "Ошибка удаления существующего ключа" +assert ht.search("banana") is None, "Ошибка: ключ не удален" +assert ht.delete("unknown") == False, "Ошибка удаления несуществующего ключа" +print("✓ Тест удаления пройден") + +ht2 = HashTableOpenAddressing(3) +ht2.insert("key1", "value1") +ht2.insert("key2", "value2") +ht2.insert("key3", "value3") + +assert ht2.search("key1") == "value1", "Ошибка при коллизии" +assert ht2.search("key2") == "value2", "Ошибка при коллизии" +assert ht2.search("key3") == "value3", "Ошибка при коллизии" +print("✓ Тест коллизий пройден") + +print("Все тесты HashTableOpenAddressing пройдены успешно!\n") +``` + +## Задание 4 + +```python +def array_intersection_custom(arr1, arr2): + """Проверка пересечения массивов через нашу хеш-таблицу""" + ht = HashTableChaining(len(arr1)) + + for item in arr1: + ht.insert(item, True) + + for item in arr2: + if ht.search(item) is not None: + return True + + return False + +print("=== Пересечение через HashTableChaining ===") +arr1 = [1, 2, 3, 4, 5] +arr2 = [4, 5, 6, 7, 8] +result = array_intersection_custom(arr1, arr2) +print(f"Массивы {arr1} и {arr2} пересекаются: {result}") +``` + +## Здание 5 + +```python +def has_unique_elements(arr): + """Проверка уникальности""" + ht = HashTableOpenAddressing(len(arr)) + + for item in arr: + if ht.search(item) is not None: + return False + ht.insert(item, True) + + return True + + +print("\n=== Уникальность через HashTableOpenAddressing ===") +arr = [1, 2, 3, 2, 4, 1] +arr2 = [1, 3, 4, 6] +print(f"Массив {arr} уникален: {has_unique_elements(arr)}") +print(f"Массив {arr2} уникален: {has_unique_elements(arr2)}") +``` + +## Задание 6 + +```python +def find_pairs_with_sum(arr, target_sum): + """Поиск пар с заданной суммой""" + ht = HashTableChaining(len(arr)) + pairs = [] + + for num in arr: + complement = target_sum - num + + if ht.search(complement) is not None: + pairs.append((complement, num)) + + ht.insert(num, True) + + return pairs + +print("\n=== Пары с суммой через HashTableChaining ===") +arr = [1, 4, 2, 3, 5, 6, 7] +target = 7 +pairs = find_pairs_with_sum(arr, target) +print(f"Пары с суммой {target} в {arr}: {pairs}") +``` + +## Задание 7 + +```python +def are_anagrams_custom(str1, str2): + """Проверка анаграмм через нашу хеш-таблицу""" + if len(str1) != len(str2): + return False + + ht = HashTableOpenAddressing(26) + + for char in str1: + current_count = ht.search(char) + if current_count is None: + ht.insert(char, 1) + else: + ht.insert(char, current_count + 1) + + for char in str2: + current_count = ht.search(char) + if current_count is None or current_count == 0: + return False + ht.insert(char, current_count - 1) + + return True + +print("\n=== Анаграммы через HashTableOpenAddressing ===") +str1 = "listen" +str2 = "silent" +print(f"'{str1}' и '{str2}' - анаграммы: {are_anagrams_custom(str1, str2)}") +``` + +## Задание 2 + +```python +import hashlib +import time +import json + +class Block: + def __init__(self, data, previous_hash=""): + self.timestamp = time.time() + self.data = data + self.previous_hash = previous_hash + self.hash = self.calculate_hash() + + def calculate_hash(self): + """Вычисляет хеш блока на основе его данных""" + import hashlib + data_string = f"{self.timestamp}{self.data}{self.previous_hash}" + return hashlib.sha256(data_string.encode()).hexdigest() + + def __str__(self): + return f"Данные: {self.data}\nХеш: {self.hash[:20]}...\nПредыдущий: {self.previous_hash[:20]}...\n" + +class BlockchainWithCustomHashTable: + def __init__(self): + self.chain = HashTableChaining(10) + self.current_hash = None + self.create_genesis_block() + + def create_genesis_block(self): + """Создает генезис-блок используя нашу хеш-таблицу""" + genesis_data = "Genesis Block" + genesis_hash = self._calculate_hash(genesis_data, "0") + self.chain.insert(genesis_hash, Block(genesis_data, "0")) + self.current_hash = genesis_hash + print("✓ Создан генезис-блок (в нашей хеш-таблице)") + + def _calculate_hash(self, data, previous_hash): + """Вычисляет хеш для блока""" + import hashlib + data_string = f"{data}{previous_hash}" + return hashlib.sha256(data_string.encode()).hexdigest() + + def add_block(self, data): + """Добавляет блок используя нашу хеш-таблицу""" + previous_hash = self.current_hash + new_hash = self._calculate_hash(data, previous_hash) + + new_block = Block(data, previous_hash) + self.chain.insert(new_hash, new_block) + self.current_hash = new_hash + + print(f"✓ Добавлен блок: {data}") + return new_hash + + def is_chain_valid(self): + """Проверяет целостность через нашу хеш-таблицу""" + current = self.current_hash + + while current: + block = self.chain.search(current) + if not block: + return False + + previous_hash = block.previous_hash + + if previous_hash == "0": + return True + + if block.calculate_hash() != current: + return False + + current = previous_hash + + return False + +def test_custom_blockchain(): + print("\n" + "="*60) + print("БЛОКЧЕЙН С НАШЕЙ HASHTableChaining") + print("="*60) + + blockchain = BlockchainWithCustomHashTable() + blockchain.add_block("Транзакция 1: Alice -> Bob") + blockchain.add_block("Транзакция 2: Bob -> Carol") + + print(f"✓ Цепочка валидна: {blockchain.is_chain_valid()}") + +test_custom_blockchain() +``` + +```python + +```