diff --git a/labs/lab_01/Lab_01_praktika/lab_01.md b/labs/lab_01/Lab_01_praktika/lab_01.md new file mode 100644 index 0000000..b21294f --- /dev/null +++ b/labs/lab_01/Lab_01_praktika/lab_01.md @@ -0,0 +1,962 @@ +--- +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 +--- + +```python +"""Эмпирический анализ временной сложности алгоритмов""" +``` + +```python +"""Асонов С.В ИУ10-36""" +``` + +```python +"""Задания""" +``` + +```python +"""Задание 1.1""" +``` + +```python +import random, usage_time +import matplotlib.pyplot as plt + + +def get_by_index(v: list): + return v[random.randint(0, len(v) -1)] + +items = range(1, 10**5 * (20 - 2), 50000) +func = usage_time.get_usage_time()(get_by_index) +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('Время выполнения алгоритма') +ax.set_xlabel('Номера элементов') +ax.set_ylabel('Время, сек') +``` + +![png](../lab_01_files/lab_01_1_1.png) + + +```python +"""Задание 1.3""" +``` + +```python +import random, usage_time +import matplotlib.pyplot as plt + + +def multiplication_nums(v: list): + multi = 1 + for num in v: + multi *= num + return multi + + +items = range(1, 10**4 * (20 - 2), 10000) +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('Время выполнения алгоритма') +ax.set_xlabel('Номера элементов') +ax.set_ylabel('Время, сек') +``` + +![png](../lab_01_files/lab_01_1_3.png) + +```python +"""Задание 1.4""" +``` + +```python +import random, usage_time +import matplotlib.pyplot as plt + + +def horner_method(v, x): + result = v[0] + for i in range(1, len(v)): + result = result * x + v[i] + return result + +items = range(1, 10**4 * (20 - 2), 10000) +x_val = 1.5 + +func = usage_time.get_usage_time()(horner_method) +times = [ + sum([ + func([ + random.randint(1, 10) + for _ in range(n) + ], x_val) + for _ in range(20) + ]) / 20 + for n in items +] + + +fig = plt.plot(items, times, 'bo-') +ax = plt.gca() + +plt.title('Время выполнения алгоритма') +ax.set_xlabel('Номера элементов') +ax.set_ylabel('Время, сек') +``` + +![png](../lab_01_files/lab_01_1_4.png) + + +```python +"""Задание 1.7""" +``` + +```python +import random, usage_time +import matplotlib.pyplot as plt + + +def mean(v): + total = 0.0 + for element in v: + total += element + return total / len(v) + + +items = range(1, 10**4 * (20 - 2), 10000) +func = usage_time.get_usage_time()(mean) +times = [ + sum([ + func([ + random.randint(1, 10) + for _ in range(n) + ]) + for _ in range(20) + ]) / 20 + for n in items +] + +fig = plt.plot(items, times, 'bo-') +ax = plt.gca() + +plt.title('Время выполнения алгоритма') +ax.set_xlabel('Номера элементов') +ax.set_ylabel('Время, сек') +``` + +![png](../lab_01_files/lab_01_1_7.png) + +```python +"""Задание 2""" +``` + +```python +import numpy as np +import time +import matplotlib.pyplot as plt +from statistics import mean + + +N = 18 +max_n = 10**2 * N +step = 100 +num_runs = 5 + + +n_values = [] +times = [] + + +for n in range(1, max_n + 1, step): + + + A = np.random.rand(n, n) + B = np.random.rand(n, n) + print(f"Обрабатывается размер матрицы = {n}x{n}") + run_times = [] + + + for run in range(num_runs): + start_time = time.time() + + C = np.dot(A, B) + + end_time = time.time() + run_times.append(end_time - start_time) + + n_values.append(n) + times.append(mean(run_times)) + + +plt.figure(figsize=(12, 6)) +plt.plot(n_values, times, 'ro-', linewidth=2, markersize=6) +plt.title('Время выполненения алгоритма') +plt.set_xlabel('Номера элементов') +plt.set_ylabel('Время, сек') +plt.grid(True, alpha=0.3) +plt.show() +``` + +![png](../lab_01_files/lab_01_2.png) + +```python +import numpy as np +import matplotlib.pyplot as plt + +# Параметры для варианта 1/11 +R1 = 220 +Lk = 0.1 +Rk = 190 +C = 1e-6 + +# Диапазон частот +omega = np.logspace(2, 5, 1000) # от 100 до 100000 рад/с + +# 1. R-L +KU_RL = np.sqrt(36100 + 0.01 * omega**2) / np.sqrt(168100 + 0.01 * omega**2) +phi_RL = np.arctan(omega/1900) - np.arctan(omega/4100) + +# 2. L-R +KU_LR = 220 / np.sqrt(168100 + 0.01 * omega**2) +phi_LR = -np.arctan(omega/4100) + +# 3. R-C +KU_RC = 1 / np.sqrt((0.00022 * omega)**2 + 1) +phi_RC = -np.pi/2 + np.arctan(4545.45/omega) + +# 4. C-R +KU_CR = (0.00022 * omega) / np.sqrt((0.00022 * omega)**2 + 1) +phi_CR = np.arctan(4545.45/omega) + +# Создание графиков +plt.figure(figsize=(12, 10)) + +# АЧХ +plt.subplot(2, 1, 1) +plt.semilogx(omega, KU_RL, 'b', linewidth=2, label='R-L') +plt.semilogx(omega, KU_LR, 'r', linewidth=2, label='L-R') +plt.semilogx(omega, KU_RC, 'g', linewidth=2, label='R-C') +plt.semilogx(omega, KU_CR, 'm', linewidth=2, label='C-R') +plt.xlabel('ω, рад/с', fontsize=12) +plt.ylabel('K_U(ω)', fontsize=12) +plt.legend(loc='best', fontsize=10) +plt.grid(True, which='both', alpha=0.3) +plt.title('АЧХ для варианта 1/11', fontsize=14) +plt.ylim(0, 1.1) + +# ФЧХ (в градусах) +plt.subplot(2, 1, 2) +plt.semilogx(omega, np.degrees(phi_RL), 'b', linewidth=2, label='R-L') +plt.semilogx(omega, np.degrees(phi_LR), 'r', linewidth=2, label='L-R') +plt.semilogx(omega, np.degrees(phi_RC), 'g', linewidth=2, label='R-C') +plt.semilogx(omega, np.degrees(phi_CR), 'm', linewidth=2, label='C-R') +plt.xlabel('ω, рад/с', fontsize=12) +plt.ylabel('φ(ω), градусы', fontsize=12) +plt.legend(loc='best', fontsize=10) +plt.grid(True, which='both', alpha=0.3) +plt.title('ФЧХ для варианта 1/11', fontsize=14) +plt.ylim(-100, 100) + +plt.tight_layout() +plt.show() + +# Дополнительно: вывод численных значений на характерных частотах +print("Численные значения на характерных частотах:") +print("\nПри ω = 100 рад/с:") +print(f"R-L: K_U = {KU_RL[0]:.3f}, φ = {np.degrees(phi_RL[0]):.1f}°") +print(f"L-R: K_U = {KU_LR[0]:.3f}, φ = {np.degrees(phi_LR[0]):.1f}°") +print(f"R-C: K_U = {KU_RC[0]:.3f}, φ = {np.degrees(phi_RC[0]):.1f}°") +print(f"C-R: K_U = {KU_CR[0]:.3f}, φ = {np.degrees(phi_CR[0]):.1f}°") + +print("\nПри ω = 10 000 рад/с:") +idx_10k = np.argmin(np.abs(omega - 10000)) +print(f"R-L: K_U = {KU_RL[idx_10k]:.3f}, φ = {np.degrees(phi_RL[idx_10k]):.1f}°") +print(f"L-R: K_U = {KU_LR[idx_10k]:.3f}, φ = {np.degrees(phi_LR[idx_10k]):.1f}°") +print(f"R-C: K_U = {KU_RC[idx_10k]:.3f}, φ = {np.degrees(phi_RC[idx_10k]):.1f}°") +print(f"C-R: K_U = {KU_CR[idx_10k]:.3f}, φ = {np.degrees(phi_CR[idx_10k]):.1f}°") + +print("\nПри ω = 100 000 рад/с:") +print(f"R-L: K_U = {KU_RL[-1]:.3f}, φ = {np.degrees(phi_RL[-1]):.1f}°") +print(f"L-R: K_U = {KU_LR[-1]:.3f}, φ = {np.degrees(phi_LR[-1]):.1f}°") +print(f"R-C: K_U = {KU_RC[-1]:.3f}, φ = {np.degrees(phi_RC[-1]):.1f}°") +print(f"C-R: K_U = {KU_CR[-1]:.3f}, φ = {np.degrees(phi_CR[-1]):.1f}°") +``` + +```python +import numpy as np +import matplotlib.pyplot as plt + +# Параметры для варианта 1 (или 11) +R1 = 220 # Ом +Lk = 0.1 # Гн (100 мГн = 0.1 Гн) +Rk = 190 # Ом +C = 1e-6 # Ф (1.0 мкФ = 1e-6 Ф) + +# Диапазон частот +omega = np.logspace(2, 5, 1000) # от 100 до 100000 рад/с + +# 1. Случай R-L: Z1 = R1, Z2 = Rk + jωLk +def case_RL(omega): + A = Rk + B = omega * Lk + C_val = R1 + Rk + D = omega * Lk + + KU = np.sqrt(A**2 + B**2) / np.sqrt(C_val**2 + D**2) + phi = np.arctan2(B, A) - np.arctan2(D, C_val) + return KU, np.degrees(phi) # phi в градусах + +# 2. Случай L-R: Z1 = Rk + jωLk, Z2 = R1 +def case_LR(omega): + A = R1 + B = 0 + C_val = R1 + Rk + D = omega * Lk + + KU = np.sqrt(A**2 + B**2) / np.sqrt(C_val**2 + D**2) + phi = np.arctan2(B, A) - np.arctan2(D, C_val) + return KU, np.degrees(phi) + +# 3. Случай R-C: Z1 = R1, Z2 = -j/(ωC) +def case_RC(omega): + A = 0 + B = -1/(omega * C) + C_val = R1 + D = -1/(omega * C) + + KU = np.sqrt(A**2 + B**2) / np.sqrt(C_val**2 + D**2) + phi = np.arctan2(B, A) - np.arctan2(D, C_val) + return KU, np.degrees(phi) + +# 4. Случай C-R: Z1 = -j/(ωC), Z2 = R1 +def case_CR(omega): + A = R1 + B = 0 + C_val = R1 + D = -1/(omega * C) + + KU = np.sqrt(A**2 + B**2) / np.sqrt(C_val**2 + D**2) + phi = np.arctan2(B, A) - np.arctan2(D, C_val) + return KU, np.degrees(phi) + +# Вычисление характеристик для всех случаев +KU_RL, phi_RL = case_RL(omega) +KU_LR, phi_LR = case_LR(omega) +KU_RC, phi_RC = case_RC(omega) +KU_CR, phi_CR = case_CR(omega) + +# Построение графиков +fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 10)) + +# АЧХ для всех случаев +ax1.semilogx(omega, KU_RL, 'b-', linewidth=2, label='R-L') +ax1.semilogx(omega, KU_LR, 'r-', linewidth=2, label='L-R') +ax1.semilogx(omega, KU_RC, 'g-', linewidth=2, label='R-C') +ax1.semilogx(omega, KU_CR, 'm-', linewidth=2, label='C-R') +ax1.set_xlabel('ω, рад/с') +ax1.set_ylabel('K_U(ω)') +ax1.set_title('АЧХ всех случаев') +ax1.grid(True, which="both", ls="-", alpha=0.2) +ax1.legend() + +# ФЧХ для всех случаев +ax2.semilogx(omega, phi_RL, 'b-', linewidth=2, label='R-L') +ax2.semilogx(omega, phi_LR, 'r-', linewidth=2, label='L-R') +ax2.semilogx(omega, phi_RC, 'g-', linewidth=2, label='R-C') +ax2.semilogx(omega, phi_CR, 'm-', linewidth=2, label='C-R') +ax2.set_xlabel('ω, рад/с') +ax2.set_ylabel('φ(ω), градусы') +ax2.set_title('ФЧХ всех случаев') +ax2.grid(True, which="both", ls="-", alpha=0.2) +ax2.legend() + +# Отдельные АЧХ для каждого случая +ax3.semilogx(omega, KU_RL, 'b-', linewidth=2, label='R-L') +ax3.semilogx(omega, KU_LR, 'r-', linewidth=2, label='L-R') +ax3.semilogx(omega, KU_RC, 'g-', linewidth=2, label='R-C') +ax3.semilogx(omega, KU_CR, 'm-', linewidth=2, label='C-R') +ax3.set_xlabel('ω, рад/с') +ax3.set_ylabel('K_U(ω)') +ax3.set_title('АЧХ (отдельные графики)') +ax3.grid(True, which="both", ls="-", alpha=0.2) +ax3.legend() + +# Отдельные ФЧХ для каждого случая +ax4.semilogx(omega, phi_RL, 'b-', linewidth=2, label='R-L') +ax4.semilogx(omega, phi_LR, 'r-', linewidth=2, label='L-R') +ax4.semilogx(omega, phi_RC, 'g-', linewidth=2, label='R-C') +ax4.semilogx(omega, phi_CR, 'm-', linewidth=2, label='C-R') +ax4.set_xlabel('ω, рад/с') +ax4.set_ylabel('φ(ω), градусы') +ax4.set_title('ФЧХ (отдельные графики)') +ax4.grid(True, which="both", ls="-", alpha=0.2) +ax4.legend() + +plt.tight_layout() +plt.show() + +# Дополнительно: построение графиков в функции lg(ω) +lg_omega = np.log10(omega) + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6)) + +# АЧХ в функции lg(ω) +ax1.plot(lg_omega, KU_RL, 'b-', linewidth=2, label='R-L') +ax1.plot(lg_omega, KU_LR, 'r-', linewidth=2, label='L-R') +ax1.plot(lg_omega, KU_RC, 'g-', linewidth=2, label='R-C') +ax1.plot(lg_omega, KU_CR, 'm-', linewidth=2, label='C-R') +ax1.set_xlabel('lg(ω)') +ax1.set_ylabel('K_U(ω)') +ax1.set_title('АЧХ = f(lg(ω))') +ax1.grid(True, alpha=0.3) +ax1.legend() + +# ФЧХ в функции lg(ω) +ax2.plot(lg_omega, phi_RL, 'b-', linewidth=2, label='R-L') +ax2.plot(lg_omega, phi_LR, 'r-', linewidth=2, label='L-R') +ax2.plot(lg_omega, phi_RC, 'g-', linewidth=2, label='R-C') +ax2.plot(lg_omega, phi_CR, 'm-', linewidth=2, label='C-R') +ax2.set_xlabel('lg(ω)') +ax2.set_ylabel('φ(ω), градусы') +ax2.set_title('ФЧХ = f(lg(ω))') +ax2.grid(True, alpha=0.3) +ax2.legend() + +plt.tight_layout() +plt.show() + +# Вывод аналитических выражений +print("Аналитические выражения для варианта 1 (или 11):") +print("\n1. Случай R-L:") +print(" K_U(ω) = sqrt(Rk² + (ωLk)²) / sqrt((R1 + Rk)² + (ωLk)²)") +print(" φ(ω) = arctg(ωLk/Rk) - arctg(ωLk/(R1 + Rk))") + +print("\n2. Случай L-R:") +print(" K_U(ω) = R1 / sqrt((R1 + Rk)² + (ωLk)²)") +print(" φ(ω) = -arctg(ωLk/(R1 + Rk))") + +print("\n3. Случай R-C:") +print(" K_U(ω) = 1 / sqrt(1 + (ωR1C)²)") +print(" φ(ω) = -π/2 + arctg(ωR1C)") + +print("\n4. Случай C-R:") +print(" K_U(ω) = ωR1C / sqrt(1 + (ωR1C)²)") +print(" φ(ω) = arctg(1/(ωR1C))") +``` + +```python +import numpy as np +import matplotlib.pyplot as plt + +# Параметры для варианта 1 (или 11) +R1 = 220 # Ом +Lk = 0.1 # Гн (100 мГн = 0.1 Гн) +Rk = 190 # Ом +C = 1e-6 # Ф (1.0 мкФ = 1e-6 Ф) + +# Диапазон частот +omega = np.logspace(2, 5, 1000) # от 100 до 100000 рад/с +lg_omega = np.log10(omega) # десятичный логарифм частоты + +# 1. Случай R-L: Z1 = R1, Z2 = Rk + jωLk +def case_RL(omega): + A = Rk + B = omega * Lk + C_val = R1 + Rk + D = omega * Lk + + KU = np.sqrt(A**2 + B**2) / np.sqrt(C_val**2 + D**2) + phi = np.arctan2(B, A) - np.arctan2(D, C_val) + return KU, np.degrees(phi) # phi в градусах + +# 2. Случай L-R: Z1 = Rk + jωLk, Z2 = R1 +def case_LR(omega): + A = R1 + B = 0 + C_val = R1 + Rk + D = omega * Lk + + KU = np.sqrt(A**2 + B**2) / np.sqrt(C_val**2 + D**2) + phi = np.arctan2(B, A) - np.arctan2(D, C_val) + return KU, np.degrees(phi) + +# 3. Случай R-C: Z1 = R1, Z2 = -j/(ωC) +def case_RC(omega): + A = 0 + B = -1/(omega * C) + C_val = R1 + D = -1/(omega * C) + + KU = np.sqrt(A**2 + B**2) / np.sqrt(C_val**2 + D**2) + phi = np.arctan2(B, A) - np.arctan2(D, C_val) + return KU, np.degrees(phi) + +# 4. Случай C-R: Z1 = -j/(ωC), Z2 = R1 +def case_CR(omega): + A = R1 + B = 0 + C_val = R1 + D = -1/(omega * C) + + KU = np.sqrt(A**2 + B**2) / np.sqrt(C_val**2 + D**2) + phi = np.arctan2(B, A) - np.arctan2(D, C_val) + return KU, np.degrees(phi) + +# Вычисление характеристик для всех случаев +KU_RL, phi_RL = case_RL(omega) +KU_LR, phi_LR = case_LR(omega) +KU_RC, phi_RC = case_RC(omega) +KU_CR, phi_CR = case_CR(omega) + +# Создание отдельных графиков для каждого случая +fig, axes = plt.subplots(2, 2, figsize=(15, 12)) + +# 1. График для случая R-L +ax1 = axes[0, 0] +ax1_achh = ax1 # левая ось для АЧХ +ax1_fchh = ax1.twinx() # правая ось для ФЧХ + +ax1_achh.plot(lg_omega, KU_RL, 'b-', linewidth=2, label='АЧХ: K_U(ω)') +ax1_fchh.plot(lg_omega, phi_RL, 'r-', linewidth=2, label='ФЧХ: φ(ω)') + +ax1_achh.set_xlabel('lg(ω)') +ax1_achh.set_ylabel('K_U(ω)', color='b') +ax1_fchh.set_ylabel('φ(ω), градусы', color='r') +ax1_achh.set_title('Случай R-L: Z1 = R1, Z2 = Rk + jωLk') +ax1_achh.grid(True, alpha=0.3) +ax1_achh.legend(loc='upper left') +ax1_fchh.legend(loc='upper right') + +# 2. График для случая L-R +ax2 = axes[0, 1] +ax2_achh = ax2 +ax2_fchh = ax2.twinx() + +ax2_achh.plot(lg_omega, KU_LR, 'b-', linewidth=2, label='АЧХ: K_U(ω)') +ax2_fchh.plot(lg_omega, phi_LR, 'r-', linewidth=2, label='ФЧХ: φ(ω)') + +ax2_achh.set_xlabel('lg(ω)') +ax2_achh.set_ylabel('K_U(ω)', color='b') +ax2_fchh.set_ylabel('φ(ω), градусы', color='r') +ax2_achh.set_title('Случай L-R: Z1 = Rk + jωLk, Z2 = R1') +ax2_achh.grid(True, alpha=0.3) +ax2_achh.legend(loc='upper left') +ax2_fchh.legend(loc='upper right') + +# 3. График для случая R-C +ax3 = axes[1, 0] +ax3_achh = ax3 +ax3_fchh = ax3.twinx() + +ax3_achh.plot(lg_omega, KU_RC, 'b-', linewidth=2, label='АЧХ: K_U(ω)') +ax3_fchh.plot(lg_omega, phi_RC, 'r-', linewidth=2, label='ФЧХ: φ(ω)') + +ax3_achh.set_xlabel('lg(ω)') +ax3_achh.set_ylabel('K_U(ω)', color='b') +ax3_fchh.set_ylabel('φ(ω), градусы', color='r') +ax3_achh.set_title('Случай R-C: Z1 = R1, Z2 = -j/(ωC)') +ax3_achh.grid(True, alpha=0.3) +ax3_achh.legend(loc='upper left') +ax3_fchh.legend(loc='upper right') + +# 4. График для случая C-R +ax4 = axes[1, 1] +ax4_achh = ax4 +ax4_fchh = ax4.twinx() + +ax4_achh.plot(lg_omega, KU_CR, 'b-', linewidth=2, label='АЧХ: K_U(ω)') +ax4_fchh.plot(lg_omega, phi_CR, 'r-', linewidth=2, label='ФЧХ: φ(ω)') + +ax4_achh.set_xlabel('lg(ω)') +ax4_achh.set_ylabel('K_U(ω)', color='b') +ax4_fchh.set_ylabel('φ(ω), градусы', color='r') +ax4_achh.set_title('Случай C-R: Z1 = -j/(ωC), Z2 = R1') +ax4_achh.grid(True, alpha=0.3) +ax4_achh.legend(loc='upper left') +ax4_fchh.legend(loc='upper right') + +plt.tight_layout() +plt.show() + +# Дополнительно: вывод аналитических выражений +print("=" * 60) +print("АНАЛИТИЧЕСКИЕ ВЫРАЖЕНИЯ ДЛЯ ВАРИАНТА 1 (или 11)") +print("=" * 60) + +print("\n1. СЛУЧАЙ R-L (Z1 = R1, Z2 = Rk + jωLk):") +print(" K_U(ω) = sqrt(Rk² + (ωLk)²) / sqrt((R1 + Rk)² + (ωLk)²)") +print(" φ(ω) = arctg(ωLk/Rk) - arctg(ωLk/(R1 + Rk))") + +print("\n2. СЛУЧАЙ L-R (Z1 = Rk + jωLk, Z2 = R1):") +print(" K_U(ω) = R1 / sqrt((R1 + Rk)² + (ωLk)²)") +print(" φ(ω) = -arctg(ωLk/(R1 + Rk))") + +print("\n3. СЛУЧАЙ R-C (Z1 = R1, Z2 = -j/(ωC)):") +print(" K_U(ω) = 1 / sqrt(1 + (ωR1C)²)") +print(" φ(ω) = -π/2 + arctg(ωR1C)") + +print("\n4. СЛУЧАЙ C-R (Z1 = -j/(ωC), Z2 = R1):") +print(" K_U(ω) = ωR1C / sqrt(1 + (ωR1C)²)") +print(" φ(ω) = arctg(1/(ωR1C))") + +print("\n" + "=" * 60) +print("Параметры цепи:") +print(f"R1 = {R1} Ом") +print(f"Lk = {Lk} Гн") +print(f"Rk = {Rk} Ом") +print(f"C = {C} Ф") +print("=" * 60) +``` + +```python +import numpy as np +import matplotlib.pyplot as plt + +# Параметры для варианта 1 (или 11) +R1 = 220 # Ом +Lk = 0.1 # Гн (100 мГн = 0.1 Гн) +Rk = 190 # Ом +C = 1e-6 # Ф (1.0 мкФ = 1e-6 Ф) + +# Диапазон частот +omega = np.logspace(2, 5, 1000) # от 100 до 100000 рад/с +lg_omega = np.log10(omega) # десятичный логарифм частоты + +# 1. Случай R-L: Z1 = R1, Z2 = Rk + jωLk +def case_RL(omega): + A = Rk + B = omega * Lk + C_val = R1 + Rk + D = omega * Lk + + KU = np.sqrt(A**2 + B**2) / np.sqrt(C_val**2 + D**2) + phi = np.arctan2(B, A) - np.arctan2(D, C_val) + return KU, np.degrees(phi) # phi в градусах + +# 2. Случай L-R: Z1 = Rk + jωLk, Z2 = R1 +def case_LR(omega): + A = R1 + B = 0 + C_val = R1 + Rk + D = omega * Lk + + KU = np.sqrt(A**2 + B**2) / np.sqrt(C_val**2 + D**2) + phi = np.arctan2(B, A) - np.arctan2(D, C_val) + return KU, np.degrees(phi) + +# 3. Случай R-C: Z1 = R1, Z2 = -j/(ωC) +def case_RC(omega): + A = 0 + B = -1/(omega * C) + C_val = R1 + D = -1/(omega * C) + + KU = np.sqrt(A**2 + B**2) / np.sqrt(C_val**2 + D**2) + phi = np.arctan2(B, A) - np.arctan2(D, C_val) + return KU, np.degrees(phi) + +# 4. Случай C-R: Z1 = -j/(ωC), Z2 = R1 +def case_CR(omega): + A = R1 + B = 0 + C_val = R1 + D = -1/(omega * C) + + KU = np.sqrt(A**2 + B**2) / np.sqrt(C_val**2 + D**2) + phi = np.arctan2(B, A) - np.arctan2(D, C_val) + return KU, np.degrees(phi) + +# Вычисление характеристик для всех случаев +KU_RL, phi_RL = case_RL(omega) +KU_LR, phi_LR = case_LR(omega) +KU_RC, phi_RC = case_RC(omega) +KU_CR, phi_CR = case_CR(omega) + +# Создание двух графиков +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6)) + +# График (а): R-L и L-R +ax1_achh = ax1 # левая ось для АЧХ +ax1_fchh = ax1.twinx() # правая ось для ФЧХ + +# АЧХ для R-L и L-R +ax1_achh.plot(lg_omega, KU_RL, 'b-', linewidth=2, label='АЧХ R-L') +ax1_achh.plot(lg_omega, KU_LR, 'b--', linewidth=2, label='АЧХ L-R') + +# ФЧХ для R-L и L-R +ax1_fchh.plot(lg_omega, phi_RL, 'r-', linewidth=2, label='ФЧХ R-L') +ax1_fchh.plot(lg_omega, phi_LR, 'r--', linewidth=2, label='ФЧХ L-R') + +ax1_achh.set_xlabel('lg(ω)') +ax1_achh.set_ylabel('K_U(ω)', color='b') +ax1_fchh.set_ylabel('φ(ω), градусы', color='r') +ax1_achh.set_title('(а) Случаи R-L и L-R') +ax1_achh.grid(True, alpha=0.3) +ax1_achh.legend(loc='upper left') +ax1_fchh.legend(loc='upper right') + +# График (б): R-C и C-R +ax2_achh = ax2 # левая ось для АЧХ +ax2_fchh = ax2.twinx() # правая ось для ФЧХ + +# АЧХ для R-C и C-R +ax2_achh.plot(lg_omega, KU_RC, 'g-', linewidth=2, label='АЧХ R-C') +ax2_achh.plot(lg_omega, KU_CR, 'g--', linewidth=2, label='АЧХ C-R') + +# ФЧХ для R-C и C-R +ax2_fchh.plot(lg_omega, phi_RC, 'm-', linewidth=2, label='ФЧХ R-C') +ax2_fchh.plot(lg_omega, phi_CR, 'm--', linewidth=2, label='ФЧХ C-R') + +ax2_achh.set_xlabel('lg(ω)') +ax2_achh.set_ylabel('K_U(ω)', color='g') +ax2_fchh.set_ylabel('φ(ω), градусы', color='m') +ax2_achh.set_title('(б) Случаи R-C и C-R') +ax2_achh.grid(True, alpha=0.3) +ax2_achh.legend(loc='upper left') +ax2_fchh.legend(loc='upper right') + +plt.tight_layout() +plt.show() + +# Дополнительно: отдельные графики для лучшей читаемости +fig2, axes2 = plt.subplots(2, 2, figsize=(15, 10)) + +# АЧХ R-L и L-R +axes2[0, 0].plot(lg_omega, KU_RL, 'b-', linewidth=2, label='R-L') +axes2[0, 0].plot(lg_omega, KU_LR, 'r-', linewidth=2, label='L-R') +axes2[0, 0].set_xlabel('lg(ω)') +axes2[0, 0].set_ylabel('K_U(ω)') +axes2[0, 0].set_title('(а) АЧХ: R-L и L-R') +axes2[0, 0].grid(True, alpha=0.3) +axes2[0, 0].legend() + +# ФЧХ R-L и L-R +axes2[0, 1].plot(lg_omega, phi_RL, 'b-', linewidth=2, label='R-L') +axes2[0, 1].plot(lg_omega, phi_LR, 'r-', linewidth=2, label='L-R') +axes2[0, 1].set_xlabel('lg(ω)') +axes2[0, 1].set_ylabel('φ(ω), градусы') +axes2[0, 1].set_title('(а) ФЧХ: R-L и L-R') +axes2[0, 1].grid(True, alpha=0.3) +axes2[0, 1].legend() + +# АЧХ R-C и C-R +axes2[1, 0].plot(lg_omega, KU_RC, 'g-', linewidth=2, label='R-C') +axes2[1, 0].plot(lg_omega, KU_CR, 'm-', linewidth=2, label='C-R') +axes2[1, 0].set_xlabel('lg(ω)') +axes2[1, 0].set_ylabel('K_U(ω)') +axes2[1, 0].set_title('(б) АЧХ: R-C и C-R') +axes2[1, 0].grid(True, alpha=0.3) +axes2[1, 0].legend() + +# ФЧХ R-C и C-R +axes2[1, 1].plot(lg_omega, phi_RC, 'g-', linewidth=2, label='R-C') +axes2[1, 1].plot(lg_omega, phi_CR, 'm-', linewidth=2, label='C-R') +axes2[1, 1].set_xlabel('lg(ω)') +axes2[1, 1].set_ylabel('φ(ω), градусы') +axes2[1, 1].set_title('(б) ФЧХ: R-C и C-R') +axes2[1, 1].grid(True, alpha=0.3) +axes2[1, 1].legend() + +plt.tight_layout() +plt.show() + +# Вывод аналитических выражений +print("=" * 70) +print("АНАЛИТИЧЕСКИЕ ВЫРАЖЕНИЯ ДЛЯ ВАРИАНТА 1 (или 11)") +print("=" * 70) + +print("\nГРУППА (а) - R-L и L-R:") +print("\nR-L (Z1 = R1, Z2 = Rk + jωLk):") +print(" K_U(ω) = sqrt(Rk² + (ωLk)²) / sqrt((R1 + Rk)² + (ωLk)²)") +print(" φ(ω) = arctg(ωLk/Rk) - arctg(ωLk/(R1 + Rk))") + +print("\nL-R (Z1 = Rk + jωLk, Z2 = R1):") +print(" K_U(ω) = R1 / sqrt((R1 + Rk)² + (ωLk)²)") +print(" φ(ω) = -arctg(ωLk/(R1 + Rk))") + +print("\nГРУППА (б) - R-C и C-R:") +print("\nR-C (Z1 = R1, Z2 = -j/(ωC)):") +print(" K_U(ω) = 1 / sqrt(1 + (ωR1C)²)") +print(" φ(ω) = -π/2 + arctg(ωR1C)") + +print("\nC-R (Z1 = -j/(ωC), Z2 = R1):") +print(" K_U(ω) = ωR1C / sqrt(1 + (ωR1C)²)") +print(" φ(ω) = arctg(1/(ωR1C))") + +print(f"\nПараметры: R1 = {R1} Ом, Lk = {Lk} Гн, Rk = {Rk} Ом, C = {C} Ф") +print("=" * 70) +``` + +```python +import numpy as np +import matplotlib.pyplot as plt + +# Параметры для варианта 1 (или 11) +R1 = 220 # Ом +Lk = 0.1 # Гн (100 мГн = 0.1 Гн) +Rk = 190 # Ом +C = 1e-6 # Ф (1.0 мкФ = 1e-6 Ф) + +# Диапазон частот +omega = np.logspace(2, 5, 1000) # от 100 до 100000 рад/с +lg_omega = np.log10(omega) # десятичный логарифм частоты + +# 1. Случай R-L: Z1 = R1, Z2 = Rk + jωLk +def case_RL(omega): + # K_U(ω) = sqrt(190² + (ω·0.1)²) / sqrt((220 + 190)² + (ω·0.1)²) + # φ(ω) = arctg(ω·0.1/190) - arctg(ω·0.1/(220 + 190)) + KU = np.sqrt(190**2 + (omega * 0.1)**2) / np.sqrt((220 + 190)**2 + (omega * 0.1)**2) + phi = np.arctan2(omega * 0.1, 190) - np.arctan2(omega * 0.1, 220 + 190) + return KU, np.degrees(phi) + +# 2. Случай L-R: Z1 = Rk + jωLk, Z2 = R1 +def case_LR(omega): + # K_U(ω) = 220 / sqrt((220 + 190)² + (ω·0.1)²) + # φ(ω) = -arctg(ω·0.1/(220 + 190)) + KU = 220 / np.sqrt((220 + 190)**2 + (omega * 0.1)**2) + phi = -np.arctan2(omega * 0.1, 220 + 190) + return KU, np.degrees(phi) + +# 3. Случай R-C: Z1 = R1, Z2 = -j/(ωC) +def case_RC(omega): + # K_U(ω) = 1 / sqrt(1 + (ω·220·1e-6)²) + # φ(ω) = -π/2 + arctg(ω·220·1e-6) + KU = 1 / np.sqrt(1 + (omega * 220 * 1e-6)**2) + phi = -np.pi/2 + np.arctan(omega * 220 * 1e-6) + return KU, np.degrees(phi) + +# 4. Случай C-R: Z1 = -j/(ωC), Z2 = R1 +def case_CR(omega): + # K_U(ω) = ω·220·1e-6 / sqrt(1 + (ω·220·1e-6)²) + # φ(ω) = arctg(1/(ω·220·1e-6)) + KU = (omega * 220 * 1e-6) / np.sqrt(1 + (omega * 220 * 1e-6)**2) + phi = np.arctan2(1, omega * 220 * 1e-6) + return KU, np.degrees(phi) + +# Вычисление характеристик для всех случаев +KU_RL, phi_RL = case_RL(omega) +KU_LR, phi_LR = case_LR(omega) +KU_RC, phi_RC = case_RC(omega) +KU_CR, phi_CR = case_CR(omega) + +# Создание двух графиков +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6)) + +# График (а): R-L и L-R +ax1_achh = ax1 +ax1_fchh = ax1.twinx() + +# АЧХ для R-L и L-R +ax1_achh.plot(lg_omega, KU_RL, 'b-', linewidth=2, label='АЧХ R-L') +ax1_achh.plot(lg_omega, KU_LR, 'b--', linewidth=2, label='АЧХ L-R') + +# ФЧХ для R-L и L-R +ax1_fchh.plot(lg_omega, phi_RL, 'r-', linewidth=2, label='ФЧХ R-L') +ax1_fchh.plot(lg_omega, phi_LR, 'r--', linewidth=2, label='ФЧХ L-R') + +ax1_achh.set_xlabel('lg(ω)') +ax1_achh.set_ylabel('K_U(ω)', color='b') +ax1_fchh.set_ylabel('φ(ω), градусы', color='r') +ax1_achh.set_title('(а) Случаи R-L и L-R\nZ1=R₁=220Ω, Z2=Rk+jωLk=190+jω·0.1') +ax1_achh.grid(True, alpha=0.3) +ax1_achh.legend(loc='upper left') +ax1_fchh.legend(loc='upper right') + +# График (б): R-C и C-R +ax2_achh = ax2 +ax2_fchh = ax2.twinx() + +# АЧХ для R-C и C-R +ax2_achh.plot(lg_omega, KU_RC, 'g-', linewidth=2, label='АЧХ R-C') +ax2_achh.plot(lg_omega, KU_CR, 'g--', linewidth=2, label='АЧХ C-R') + +# ФЧХ для R-C и C-R +ax2_fchh.plot(lg_omega, phi_RC, 'm-', linewidth=2, label='ФЧХ R-C') +ax2_fchh.plot(lg_omega, phi_CR, 'm--', linewidth=2, label='ФЧХ C-R') + +ax2_achh.set_xlabel('lg(ω)') +ax2_achh.set_ylabel('K_U(ω)', color='g') +ax2_fchh.set_ylabel('φ(ω), градусы', color='m') +ax2_achh.set_title('(б) Случаи R-C и C-R\nZ1=R₁=220Ω, Z2=-j/(ωC), C=1.0 мкФ') +ax2_achh.grid(True, alpha=0.3) +ax2_achh.legend(loc='upper left') +ax2_fchh.legend(loc='upper right') + +plt.tight_layout() +plt.show() + +# Вывод аналитических выражений с подставленными значениями +print("=" * 80) +print("АНАЛИТИЧЕСКИЕ ВЫРАЖЕНИЯ С ПОДСТАВЛЕННЫМИ ЗНАЧЕНИЯМИ (ВАРИАНТ 1/11)") +print("=" * 80) + +print("\nГРУППА (а) - R-L и L-R:") +print("\nR-L (Z1 = R1 = 220 Ом, Z2 = Rk + jωLk = 190 + jω·0.1):") +print(" K_U(ω) = sqrt(190² + (0.1ω)²) / sqrt(410² + (0.1ω)²)") +print(" φ(ω) = arctg(0.1ω/190) - arctg(0.1ω/410)") + +print("\nL-R (Z1 = Rk + jωLk = 190 + jω·0.1, Z2 = R1 = 220 Ом):") +print(" K_U(ω) = 220 / sqrt(410² + (0.1ω)²)") +print(" φ(ω) = -arctg(0.1ω/410)") + +print("\nГРУППА (б) - R-C и C-R:") +print("\nR-C (Z1 = R1 = 220 Ом, Z2 = -j/(ωC) = -j/(ω·1e-6)):") +print(" K_U(ω) = 1 / sqrt(1 + (0.00022ω)²)") +print(" φ(ω) = -90° + arctg(0.00022ω)") + +print("\nC-R (Z1 = -j/(ωC) = -j/(ω·1e-6), Z2 = R1 = 220 Ом):") +print(" K_U(ω) = 0.00022ω / sqrt(1 + (0.00022ω)²)") +print(" φ(ω) = arctg(1/(0.00022ω))") + +print("\n" + "=" * 80) +print("РАСЧЕТНЫЕ ПАРАМЕТРЫ:") +print(f"R1 = {R1} Ом") +print(f"Lk = {Lk} Гн") +print(f"Rk = {Rk} Ом") +print(f"C = {C} Ф") +print(f"R1 + Rk = {R1 + Rk} Ом") +print(f"R1·C = {R1 * C} = 0.00022") +print("=" * 80) + +# Дополнительный анализ характеристических частот +print("\nХАРАКТЕРИСТИЧЕСКИЕ ЧАСТОТЫ:") +# Для RL цепей +omega_char_RL = (R1 + Rk) / Lk +print(f"Характерная частота RL цепей: ω = (R1 + Rk)/Lk = {omega_char_RL:.1f} рад/с") + +# Для RC цепей +omega_char_RC = 1 / (R1 * C) +print(f"Характерная частота RC цепей: ω = 1/(R1·C) = {omega_char_RC:.1f} рад/с") +``` + +```python + +``` diff --git a/labs/lab_01/Lab_01_praktika/usage_time.py b/labs/lab_01/Lab_01_praktika/usage_time.py new file mode 100644 index 0000000..ef6c1a9 --- /dev/null +++ b/labs/lab_01/Lab_01_praktika/usage_time.py @@ -0,0 +1,121 @@ +# --- +# jupyter: +# jupytext: +# formats: py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.17.3 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# %% +# # %load -y -n -r 14:17 usage_time.py +import functools +import timeit +import typing + +# # %load -y -n -s get_usage_time usage_time.py +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, optional + 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 diff --git a/labs/lab_01/lab_01_files/lab_01_1_1.png b/labs/lab_01/lab_01_files/lab_01_1_1.png new file mode 100644 index 0000000..a8d1c64 Binary files /dev/null and b/labs/lab_01/lab_01_files/lab_01_1_1.png differ diff --git a/labs/lab_01/lab_01_files/lab_01_1_3.png b/labs/lab_01/lab_01_files/lab_01_1_3.png new file mode 100644 index 0000000..d381cd1 Binary files /dev/null and b/labs/lab_01/lab_01_files/lab_01_1_3.png differ diff --git a/labs/lab_01/lab_01_files/lab_01_1_4.png b/labs/lab_01/lab_01_files/lab_01_1_4.png new file mode 100644 index 0000000..5b05a11 Binary files /dev/null and b/labs/lab_01/lab_01_files/lab_01_1_4.png differ diff --git a/labs/lab_01/lab_01_files/lab_01_1_7.png b/labs/lab_01/lab_01_files/lab_01_1_7.png new file mode 100644 index 0000000..b667b97 Binary files /dev/null and b/labs/lab_01/lab_01_files/lab_01_1_7.png differ diff --git a/labs/lab_01/lab_01_files/lab_01_2.png b/labs/lab_01/lab_01_files/lab_01_2.png new file mode 100644 index 0000000..83411cf Binary files /dev/null and b/labs/lab_01/lab_01_files/lab_01_2.png differ diff --git a/labs/lab_02/Lab_02_files.png/buc_sort.png b/labs/lab_02/Lab_02_files.png/buc_sort.png new file mode 100644 index 0000000..6e819ef Binary files /dev/null and b/labs/lab_02/Lab_02_files.png/buc_sort.png differ diff --git a/labs/lab_02/Lab_02_files.png/bucket_sort.png b/labs/lab_02/Lab_02_files.png/bucket_sort.png new file mode 100644 index 0000000..d071a4c Binary files /dev/null and b/labs/lab_02/Lab_02_files.png/bucket_sort.png differ diff --git a/labs/lab_02/Lab_02_files.png/diagr.png b/labs/lab_02/Lab_02_files.png/diagr.png new file mode 100644 index 0000000..5182ba9 Binary files /dev/null and b/labs/lab_02/Lab_02_files.png/diagr.png differ diff --git a/labs/lab_02/Lab_02_files.png/merge_sort.png b/labs/lab_02/Lab_02_files.png/merge_sort.png new file mode 100644 index 0000000..32c5c86 Binary files /dev/null and b/labs/lab_02/Lab_02_files.png/merge_sort.png differ diff --git a/labs/lab_02/Lab_02_files.png/sliya_sort.png b/labs/lab_02/Lab_02_files.png/sliya_sort.png new file mode 100644 index 0000000..3c9f6bd Binary files /dev/null and b/labs/lab_02/Lab_02_files.png/sliya_sort.png differ diff --git a/labs/lab_02/praktika.md b/labs/lab_02/praktika.md new file mode 100644 index 0000000..83bb6dd --- /dev/null +++ b/labs/lab_02/praktika.md @@ -0,0 +1,914 @@ +--- +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 +--- + + # **Алгоритмы сортировки** + + +## **Цель работы** + + +изучение основных алгоритмов на сортировки. + + +Асонов Серей ИУ10-36 + + +## **Задание 1** + + +### **Классификация алгоритмов сортировки** + +#### По принципу работы: +Алгоритмы сравнения: основаны на попарном сравнении элементов + +Цифровые алгоритмы: используют внутреннее представление данных + +#### По временной сложности: +O(n²) : Пузырьковая, Выбором, Вставками + +O(n log n): Слиянием, Быстрая, Пирамидальная + +O(n): Блочная, Поразрядная (при определенных условиях) + +#### По устойчивости: +Устойчивые: Слиянием, Вставками, Блочная + +Неустойчивые: Быстрая, Выбором, Пирамидальная + +#### По использованию памяти: +In-place: Быстрая, Пирамидальная, Вставками + +Not in-place: Слиянием, Блочная + + +## **Задание 2** + + +### **Теоретическое описание алгоритмов** + +##### Сортировка слиянием (Merge Sort) +Принцип: Алгоритм стратегии "разделяй и властвуй". Рекурсивно разделяет массив на подмассивы до единичных элементов, затем сливает их в упорядоченные последовательности. + +Математическая основа: + +Рекуррентное соотношение: T(n) = 2T(n/2) + O(n) + +По теореме о рекуррентных соотношениях: T(n) = O(n log n) + +#### Блочная сортировка (Bucket Sort) +Принцип: Распределяющий алгоритм, который разбивает входные данные на "ведра" на основе их значений, затем сортирует каждое ведро отдельно. + +Условия эффективности: + +Равномерное распределение входных данных + +Известный диапазон значений + +Количество ведер ≈ √n + + +## **Задание 3** + + +### **Сортировка слиянием** + + +![png](Lab_02_files.png/sliya_sort.png) + + + +```python +### **Сортировка блочная** +``` + +![png](Lab_02_files.png/buc_sort.png) + + +## **Задание 4** + + +### **Сортировка слиянием:** + + +АЛГОРИТМ СортировкаСлиянием(массив A): + ВХОД: массив A[0..n-1] + ВЫХОД: отсортированный массив A + + ЕСЛИ n <= 1: + ВОЗВРАТ A // базовый случай + + // Разделение + середина = n // 2 + левый_подмассив = A[0..середина-1] + правый_подмассив = A[середина..n-1] + + // Рекурсивная сортировка + отсортированный_левый = СортировкаСлиянием(левый_подмассив) + отсортированный_правый = СортировкаСлиянием(правый_подмассив) + + // Слияние + ВОЗВРАТ Слияние(отсортированный_левый, отсортированный_правый) + +АЛГОРИТМ Слияние(массив L, массив R): + ВХОД: два отсортированных массива L и R + ВЫХОД: объединенный отсортированный массив + + результат = новый_массив[длина(L) + длина(R)] + i = 0, j = 0, k = 0 + + // Сравниваем и добавляем элементы по порядку + ПОКА i < длина(L) И j < длина(R): + ЕСЛИ L[i] <= R[j]: + результат[k] = L[i] + i = i + 1 + ИНАЧЕ: + результат[k] = R[j] + j = j + 1 + k = k + 1 + + // Добавляем оставшиеся элементы из L + ПОКА i < длина(L): + результат[k] = L[i] + i = i + 1 + k = k + 1 + + // Добавляем оставшиеся элементы из R + ПОКА j < длина(R): + результат[k] = R[j] + j = j + 1 + k = k + 1 + + ВОЗВРАТ результат + + + +### **Блочная сортировка:** + + +АЛГОРИТМ БлочнаяСортировка(массив A, размер_блока): + ВХОД: массив A[0..n-1], размер блока + ВЫХОД: отсортированный массив A + + ЕСЛИ длина(A) == 0: + ВОЗВРАТ A + + // Находим диапазон значений + min_значение = минимальный_элемент(A) + max_значение = максимальный_элемент(A) + + // Создаем блоки + количество_блоков = (max_значение - min_значение) // размер_блока + 1 + блоки = новый_массив[количество_блоков] из пустых списков + + // Распределяем элементы по блокам + ДЛЯ КАЖДОГО элемента В A: + индекс_блока = (элемент - min_значение) // размер_блока + добавить элемент в блоки[индекс_блока] + + // Сортируем каждый блок и объединяем + результат = пустой_массив + ДЛЯ КАЖДОГО блока В блоки: + отсортированный_блок = СортировкаВставками(блок) + добавить отсортированный_блок в результат + + ВОЗВРАТ результат + +АЛГОРИТМ СортировкаВставками(массив A): + ВХОД: массив A[0..n-1] + ВЫХОД: отсортированный массив A + + ДЛЯ i ОТ 1 ДО длина(A) - 1: + ключ = A[i] + j = i - 1 + + // Сдвигаем элементы большие ключа + ПОКА j >= 0 И A[j] > ключ: + A[j + 1] = A[j] + j = j - 1 + + A[j + 1] = ключ + + ВОЗВРАТ A + + +## **Задание 5** + + +### **Сортировка слиянием:** +#### Достоинства: + +Гарантированная временная сложность O(n log n) для любых случаев + +Устойчивость - сохраняет порядок равных элементов + +Предсказуемое поведение независимо от входных данных + +Эффективна для больших объемов данных + +Хорошо параллелизуется + +#### Недостатки: + +Требует O(n) дополнительной памяти + +Медленнее in-place алгоритмов на небольших массивах + +Рекурсивные вызовы могут вызывать переполнение стека + +Константа в O-нотации больше, чем у быстрой сортировки + +### **Блочная сортировка:** + +#### Достоинства: + +Линейная сложность O(n) в среднем случае + +Эффективна при равномерном распределении данных + +Устойчивость (при правильной реализации) + +Хорошо работает с данными с известным диапазоном + +#### Недостатки: + +Вырождается до O(n²) при плохом распределении + +Требует знания о распределении входных данных + +Неэффективна для данных с выбросами + +Требует дополнительной памяти O(n + k) + +Сложность зависит от алгоритма сортировки блоков + + +## **Задания 6 - 11** + +```python +import time +import random +import matplotlib.pyplot as plt +import math + +# 1. Сортировка слиянием (Merge Sort) +def merge_sort(arr): + if len(arr) <= 1: + return arr + + mid = len(arr) // 2 + left = merge_sort(arr[:mid]) + right = merge_sort(arr[mid:]) + + return merge(left, right) + +def merge(left, right): + result = [] + i = j = 0 + + while i < len(left) and j < len(right): + if left[i] <= right[j]: + result.append(left[i]) + i += 1 + else: + result.append(right[j]) + j += 1 + + result.extend(left[i:]) + result.extend(right[j:]) + return result + +# 2. Блочная сортировка (Bucket Sort) +def bucket_sort(arr, bucket_size=5): + if len(arr) == 0: + return arr + + # Находим минимальное и максимальное значения + min_val = min(arr) + max_val = max(arr) + + # Создаем блоки + bucket_count = (max_val - min_val) // bucket_size + 1 + buckets = [[] for _ in range(bucket_count)] + + # Распределяем элементы по блокам + for num in arr: + bucket_index = (num - min_val) // bucket_size + buckets[bucket_index].append(num) + + # Сортируем каждый блок и объединяем + result = [] + for bucket in buckets: + # Используем сортировку вставками для сортировки блоков + result.extend(insertion_sort(bucket)) + + return result + +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 + +# Функция для измерения времени выполнения +def measure_sorting_time(sort_func, arr, *args): + arr_copy = arr.copy() + start_time = time.time() + sort_func(arr_copy, *args) + end_time = time.time() + return end_time - start_time + +# Генерация тестовых данных +def generate_test_arrays(n, array_type): + if array_type == 'sorted': + return list(range(n)) + elif array_type == 'reverse_sorted': + return list(range(n, 0, -1)) + elif array_type == 'random': + return [random.randint(1, 100000) for _ in range(n)] + return None + +# Основная функция тестирования +def test_sorting_algorithms(): + sizes = [1000, 5000, 10000, 100000] # n1, n2, n3, n4 + array_types = ['sorted', 'reverse_sorted', 'random'] + algorithms = { + 'Сортировка слиянием': (merge_sort, []), + 'Блочная сортировка': (bucket_sort, [10]) # bucket_size = 10 + } + + results = {algo: {arr_type: [] for arr_type in array_types} for algo in algorithms} + + for size in sizes: + print(f"\n--- Тестирование для n = {size} ---") + + for arr_type in array_types: + test_array = generate_test_arrays(size, arr_type) + print(f"\nТип массива: {arr_type}") + + for algo_name, (algo_func, args) in algorithms.items(): + time_taken = measure_sorting_time(algo_func, test_array, *args) + results[algo_name][arr_type].append(time_taken) + print(f"{algo_name}: {time_taken:.4f} секунд") + + return results, sizes + +# Визуализация результатов +def plot_results(results, sizes): + array_types = ['sorted', 'reverse_sorted', 'random'] + algorithms = list(results.keys()) + + fig, axes = plt.subplots(1, 3, figsize=(18, 6)) + + for i, arr_type in enumerate(array_types): + for algo_name in algorithms: + times = results[algo_name][arr_type] + axes[i].plot(sizes, times, marker='o', label=algo_name) + + axes[i].set_title(f'Тип массива: {arr_type}') + axes[i].set_xlabel('Размер массива') + axes[i].set_ylabel('Время (секунды)') + axes[i].legend() + axes[i].grid(True) + + plt.tight_layout() + plt.savefig('sorting_comparison.png') + plt.show() + +# Ручная трассировка для небольших массивов +def manual_trace(): + print("=== РУЧНАЯ ТРАССИРОВКА ===") + + # Тестовый массив + test_array = [64, 34, 25, 12, 22, 11, 90, 5] + print(f"Исходный массив: {test_array}") + + # Сортировка слиянием + print("\n--- Сортировка слиянием ---") + sorted_merge = merge_sort(test_array.copy()) + print(f"Результат: {sorted_merge}") + + # Блочная сортировка + print("\n--- Блочная сортировка ---") + sorted_bucket = bucket_sort(test_array.copy(), 20) + print(f"Результат: {sorted_bucket}") + +# Анализ эффективности +def analyze_efficiency(results, sizes): + print("\n=== АНАЛИЗ ЭФФЕКТИВНОСТИ ===") + for size in sizes: + print(f"\nДля n = {size}:") + for arr_type in ['sorted', 'reverse_sorted', 'random']: + merge_time = results['Сортировка слиянием'][arr_type][sizes.index(size)] + bucket_time = results['Блочная сортировка'][arr_type][sizes.index(size)] + + faster = "Слиянием" if merge_time < bucket_time else "Блочная" + difference = abs(merge_time - bucket_time) + + print(f" {arr_type}: {faster} быстрее на {difference:.4f} сек") + +# Запуск тестирования +if __name__ == "__main__": + # Ручная трассировка + manual_trace() + + # Основное тестирование + print("\n=== ОСНОВНОЕ ТЕСТИРОВАНИЕ ===") + results, sizes = test_sorting_algorithms() + + # Визуализация результатов + plot_results(results, sizes) + + # Анализ эффективности + analyze_efficiency(results, sizes) +``` + +![png](Lab_02_files.png/diagr.png) + + +## **Ручная трассировка алгоритмов** + + +### Сортировка слиянием для массива [64, 34, 25, 12, 22, 11, 90, 5]: + + +Разделение: +[64, 34, 25, 12] и [22, 11, 90, 5] +[64, 34] и [25, 12] | [22, 11] и [90, 5] +[64] [34] [25] [12] [22] [11] [90] [5] + +Слияние: +[34, 64] + [12, 25] → [12, 25, 34, 64] +[11, 22] + [5, 90] → [5, 11, 22, 90] +[12, 25, 34, 64] + [5, 11, 22, 90] → [5, 11, 12, 22, 25, 34, 64, 90] + + +### Блочная сортировка для массива [64, 34, 25, 12, 22, 11, 90, 5] (bucket_size=20): + + +Диапазон значений: min=5, max=90 +Количество блоков: (90-5)//20 + 1 = 5 блоков + +Распределение по блокам: +Блок 0 [5-24]: [12, 22, 11, 5] +Блок 1 [25-44]: [34, 25] +Блок 2 [45-64]: [64] +Блок 3 [65-84]: [] +Блок 4 [85-104]: [90] + +Сортировка блоков: +Блок 0: [5, 11, 12, 22] +Блок 1: [25, 34] +Блок 2: [64] +Блок 3: [] +Блок 4: [90] + +Объединение: [5, 11, 12, 22, 25, 34, 64, 90] + + +## **Анализ эффективности** + + +### **Критерии сравнения:** +#### 1.Временная сложность: +##### Сортировка слиянием (Merge Sort): + + +Лучший случай: O(n log n) +Средний случай: O(n log n) +Худший случай: O(n log n) +Обоснование: + +Алгоритм всегда делит массив пополам: log n уровней рекурсии + +На каждом уровне выполняется слияние за O(n) + +Итого: O(n) × O(log n) = O(n log n) + +##### Блочная сортировка (Bucket Sort): + +Лучший случай: O(n + k) +Средний случай: O(n + k) +Худший случай: O(n²) +Обоснование: + +Лучший случай: равномерное распределение, все блоки содержат ≈ n/k элементов + +Худший случай: все элементы попадают в один блок → вырождается в O(n²) + +k - количество блоков + +#### 2.Память: + +##### Сортировка слиянием: + +Требуемая память: O(n) +Распределение памяти: + +Дополнительный массив для слияния: O(n) + +Стек вызовов рекурсии: O(log n) + +Итого: O(n) + +##### Блочная сортировка: + +Требуемая память: O(n + k) +Распределение памяти: + +Массив блоков: O(k) + +Элементы в блоках: O(n) + +Итого: O(n + k) + +#### 3.Устойчивость: + +##### Сортировка слиянием:устойчивая + +Сохраняет относительный порядок равных элементов + +Элементы из левого подмассива добавляются первыми + +##### Блочная сортировка : ЗАВИСИТ от алгоритма сортировки блоков +Варианты: + +Устойчивая: если использовать устойчивую сортировку блоков (сортировку вставками) + +Неустойчивая: если использовать неустойчивую сортировку блоков + + + +## Контрольные вопросы: +Сортировка вставками (Insertion Sort) +1. В чем состоит суть метода сортировки вставками? +Суть метода заключается в том, что массив постепенно перестраивается путем последовательной вставки каждого следующего элемента в уже упорядоченную часть массива. Мы как бы "вставляем" карту в нужное место в уже отсортированной руке. + + + +def insertion_sort(arr): + for i in range(1, len(arr)): + key = arr[i] + j = i - 1 + # Сдвигаем элементы отсортированной части, которые больше key + while j >= 0 and arr[j] > key: + arr[j + 1] = arr[j] + j -= 1 + # Вставляем key на правильную позицию + arr[j + 1] = key + return arr + + +4. В чем достоинства и недостатки метода сортировки вставками? + +Достоинства: + +Прост в реализации. + +Эффективен на небольших наборах данных. + +Эффективен на практически отсортированных массивах (сложность接近 O(n)). + +Является устойчивой (stable) сортировкой. + +Сортирует "на месте" (in-place), не требует дополнительной памяти. + +Недостатки: + +Имеет высокую среднюю временную сложность O(n²), что делает его неэффективным для больших массивов. + +5. Приведите практический пример сортировки массива методом вставок. +Массив: [12, 11, 13, 5, 6] + +i=1: [11, 12, 13, 5, 6] (11 вставлен перед 12) + +i=2: [11, 12, 13, 5, 6] (13 остался на месте) + +i=3: [5, 11, 12, 13, 6] (5 вставлен в начало) + +i=4: [5, 6, 11, 12, 13] (6 вставлен между 5 и 11) + + + +Сортировка Шелла (Shell Sort) +6. В чем состоит суть сортировки методом Шелла? +Это усовершенствование сортировки вставками. Суть в том, что массив сортируется в несколько этапов с использованием убывающей последовательности шагов (gaps). На каждом этапе сортируются подмассивы элементов, отстоящих друг от друга на расстоянии gap. + +7. За счет чего метод Шелла дает лучшие показатели по сравнению с простейшими методами? +За счет того, что на начальных этапах (с большим gap) элементы "перепрыгивают" на значительные расстояния, что позволяет быстрее занять свою приблизительную позицию. + +9. Какой фактор оказывает наибольшее влияние на эффективность сортировки методом Шелла? +Наибольшее влияние оказывает выбор последовательности шагов (gaps). Разные последовательности дают разную теоретическую и практическую производительность. + +10. Какие последовательности шагов группировки рекомендуются для практического использования в методе Шелла? + +Последовательность Кнута: (3^k - 1) / 2 (1, 4, 13, 40, 121, ...). Хороший баланс между простотой и эффективностью. + +Последовательность Седжвика: 9 * (4^(k-1) - 2^(k-1)) + 1 и 4^k - 3 * 2^k + 1 (1, 8, 23, 77, 281, ...). Одна из лучших известных последовательностей. + +Последовательность Хиббарда: 2^k - 1 (1, 3, 7, 15, 31, ...). + +def shell_sort(arr): + n = len(arr) + # Начинаем с большого gap, затем уменьшаем его + gap = n // 2 + while gap > 0: + # Применяем сортировку вставками для этого gap + 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 # Уменьшаем gap + return arr + +Сортировка выбором (Selection Sort) +12. В чем состоит суть метода сортировки выбором? +Суть в том, что массив мысленно делится на две части: отсортированную (в начале) и неотсортированную. На каждом шаге алгоритм ищет минимальный элемент в неотсортированной части и меняет его местами с первым элементом неотсортированной части. Таким образом, размер отсортированной части увеличивается на один элемент. + +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)). Полезно, когда стоимость операции обмена высока (например, сортировка данных на внешних носителях). + +Сортирует "на месте". + +Недостатки: + +Всегда имеет временную сложность O(n²), независимо от исходного состояния массива. + +Не является устойчивой (unstable) сортировкой. + + + +Сортировка обменом (Пузырьковая сортировка - Bubble Sort) +17. В чем состоит суть метода сортировки обменом? +Суть в последовательном сравнении соседних элементов и их обмене, если они находятся в неправильном порядке. В результате каждого полного прохода по массиву самый "тяжелый" (максимальный) элемент "всплывает" в конец массива, как пузырёк. + + +def bubble_sort(arr): + n = len(arr) + for i in range(n): + swapped = False # Флаг оптимизации + # Последние i элементов уже на месте + for j in range(0, n - i - 1): + if arr[j] > arr[j + 1]: + arr[j], arr[j + 1] = arr[j + 1], arr[j] + swapped = True + # Если обменов не было, массив отсортирован + if not swapped: + break + return arr + + +20. В чем достоинства и недостатки метода сортировки обменом? + +Достоинства: + +Чрезвычайно прост для понимания. + +Является устойчивой сортировкой. + +Сортирует "на месте". + +Недостатки: + +Очень медленный. Имеет среднюю и наихудшую временную сложность O(n²). + +На практике почти всегда уступает другим алгоритмам, кроме, возможно, сортировки вставками на очень маленьких или почти отсортированных массивах. + + +Быстрая сортировка (Quicksort) +22. В чем состоит суть метода быстрой сортировки? +Это алгоритм типа "разделяй и властвуй". Он выбирает опорный элемент (pivot), переупорядочивает массив так, чтобы все элементы меньше опорного оказались слева от него, а все больше — справа (этот процесс называется разбиение Хоара). Затем алгоритм рекурсивно применяется к двум подмассивам слева и справа от опорного элемента. + +23. За счет чего метод быстрой сортировки дает лучшие показатели по сравнению с простейшими методами? +За счет того, что после разбиения элементы оказываются в правильной позиции относительно опорного, и дальнейшая сортировка подмассивов происходит независимо. В среднем случае это дает временную сложность O(n log n). На практике он очень эффективен из-за малого количества обменов и внутреннего цикла. + +24. Что такое опорный элемент в методе быстрой сортировки и как он используется? +Опорный элемент (pivot) — это элемент массива, относительно которого происходит разделение. После разбиения он занимает свою окончательную позию в отсортированном массиве. Алгоритм гарантирует, что все элементы слева <= pivot, а справа >= pivot. + +25. Приведите практический пример быстрой сортировки массива. +Массив: [10, 80, 30, 90, 40, 50, 70]. Опорный элемент (pivot) — последний (70). + +Разбиение: + +i указывает на границу элементов, меньших опорного. + +Проходим j по массиву. Если arr[j] <= pivot, меняем arr[i] и arr[j], увеличиваем i. + +В конце меняем arr[i] и pivot. + +Результат разбиения: [10, 30, 40, 50, 70, 90, 80]. Pivot (70) на своем месте. + +Рекурсивно применяем к подмассивам [10, 30, 40, 50] и [90, 80]. + +26. Что можно сказать о применимости метода быстрой сортировки с точки зрения его эффективности? +Это один из самых эффективных и широко применяемых алгоритмов сортировки общего назначения. Он особенно хорошо работает со случайными данными и в среднем случае. Однако он может быть нестабильным и имеет плохую производительность в худшем случае O(n²). + +27. Какой фактор оказывает решающее влияние на эффективность метода быстрой сортировки? +Выбор опорного элемента. Если опорный элемент постоянно оказывается минимальным или максимальным (например, в уже отсортированном массиве при выборе первого/последнего элемента), разбиение будет крайне несбалансированным, что приводит к худшему случаю O(n²). + +28. Почему выбор серединного элемента в качестве опорного в методе быстрой сортировки может резко ухудшать эффективность метода? +Вопрос сформулирован неточно. Выбор серединного элемента как раз является хорошей стратегией для избежания худшего случая на уже отсортированных массивах. Ухудшение эффективности наступает при выборе крайних элементов (первого или последнего) в уже отсортированном или почти отсортированном массиве. + +29. Какое правило выбора опорного элемента в методе быстрой сортировки является наилучшим и почему его сложно использовать? +Наилучшим является медиана трех (медиана из первого, среднего и последнего элементов). Она почти гарантирует сбалансированное разбиение. Однако ее сложно использовать, потому что точное вычисление медианы для всего массива слишком дорогое, а "медиана трех" является хорошим и быстрым приближением. + +30. Какое простое правило выбора опорного элемента в методе быстрой сортировки рекомендуется использовать на практике? +Рекомендуется использовать "медиану трех" (первый, средний, последний элементы) или случайный выбор опорного элемента. Оба этих подхода с высокой вероятностью предотвращают наступление худшего случая. + + +31. Какие усовершенствования имеет базовый алгоритм метода быстрой сортировки? + +Использование "медианы трех" для выбора опорного элемента. + +Рекурсивная сортировка меньшего подмассива первым для ограничения глубины рекурсии. + +Использование сортировки вставками для маленьких подмассивов (например, размером < 10). + +Нерекурсивная (итеративная) реализация с использованием стека. + +32. Почему быстрая сортировка проще всего программно реализуется с помощью рекурсии? +Потому что алгоритм по своей природе рекурсивен: после разбиения задача сводится к решению двух независимых и аналогичных подзадач (сортировка левого и правого подмассивов). Рекурсия интуитивно отражает эту логику "разделяй и властвуй". + +33. Как программно реализуется рекурсивный вариант метода быстрой сортировки? + + +def quicksort(arr): + if len(arr) <= 1: + return arr + else: + # Выбор опорного элемента (здесь - последний) + pivot = arr[-1] + # Разбиение Хоара + less = [x for x in arr[:-1] if x <= pivot] + greater = [x for x in arr[:-1] if x > pivot] + # Рекурсивный вызов и объединение + return quicksort(less) + [pivot] + quicksort(greater) + +Более эффективная in-place версия: +def quicksort_in_place(arr, low=0, high=None): + if high is None: + high = len(arr) - 1 + if low < high: + # pi - индекс разбиения, arr[pi] на своем месте + pi = partition(arr, low, high) + quicksort_in_place(arr, low, pi - 1) + quicksort_in_place(arr, pi + 1, high) + +def partition(arr, low, high): + pivot = arr[high] + i = low - 1 + for j in range(low, high): + if arr[j] <= pivot: + i += 1 + arr[i], arr[j] = arr[j], arr[i] + arr[i + 1], arr[high] = arr[high], arr[i + 1] + return i + 1 + + +34. Какие особенности имеет не рекурсивная программная реализация метода быстрой сортировки? +Она эмулирует рекурсию с помощью стека. В стек помещаются границы подмассивов (low и high), которые нужно отсортировать. Алгоритм продолжается, пока стек не пуст. + +Особенности: + +Требует явного управления стеком. + +Позволяет избежать ограничений на глубину рекурсии. + +Может быть более эффективной по памяти, если сначала помещать в стек больший подмассив. + + +Пирамидальная сортировка (Heapsort) +35. В чем состоит суть метода пирамидальной сортировки? +Суть в использовании структуры данных "куча" (пирамида - heap). Алгоритм состоит из двух этапов: + +Построение из массива невозрастающей пирамиды (max-heap), где корень каждого поддерева является наибольшим элементом. + +Последовательное извлечение максимального элемента из кучи и перестроение оставшейся части. + +36. Какой набор данных имеет пирамидальную организацию? +Пирамидальную организацию имеет двоичная куча (binary heap). Это полное бинарное дерево, которое удовлетворяет свойству кучи: ключ каждого узла >= (для max-heap) ключей его потомков. + + + + +38. Приведите пример пирамидального дерева с целочисленными ключами. +Массив: [4, 10, 3, 5, 1]. Max-Heap в виде дерева: + + + 10 + / \ + 5 3 + / \ + 4 1 +Соответствующий массив для кучи: [10, 5, 3, 4, 1]. + +39. Какие полезные свойства имеет пирамидальное дерево? + +Максимальный (или минимальный) элемент всегда находится в корне. + +Можно эффективно вставлять новые элементы и извлекать корневой, сохраняя свойства кучи (за O(log n)). + +Представление в виде массива очень компактно и не требует хранения указателей. + +40. Какие шаги выполняются при построении пирамидального дерева? +Построение снизу вверх (алгоритм Флойда). + +Начинаем с последнего нелистового узла (индекс n//2 - 1). + +Для каждого такого узла выполняем операцию просеивания вниз (sift down), чтобы восстановить свойство кучи для поддерева с корнем в этом узле. + +Переходим к предыдущему узлу (уменьшаем индекс) и повторяем шаг 2, пока не дойдем до корня. + +41. Что такое просеивание элемента через пирамиду? +Просеивание вниз (sift down) — это процесс восстановления свойства кучи для узла i. Если узел i меньше какого-либо из своих потомков, он меняется местами с наибольшим из потомков. Процесс повторяется рекурсивно для поддерева, в которое "упал" узел, пока свойство кучи не будет восстановлено. + +42. Приведите практический пример построения пирамидального дерева. +Массив: [4, 10, 3, 5, 1]. + +Начинаем с последнего нелистового узла (индекс = 1, элемент 10). Его поддерево уже является кучей. + +Переходим к корню (индекс = 0, элемент 4). + +Просеиваем 4 вниз: сравниваем с потомками (10 и 3). 10 - наибольший. Меняем 4 и 10. + +Новый массив: [10, 4, 3, 5, 1]. + +Просеиваем 4 (теперь на индексе 1): сравниваем с потомками (5 и 1). 5 - наибольший. Меняем 4 и 5. + +Итоговый массив (max-heap): [10, 5, 3, 4, 1]. + +43. Какие шаги выполняются на втором этапе пирамидальной сортировки? + +Поменять местами корневой (максимальный) элемент кучи с последним элементом неотсортированной части. + +Уменьшить размер кучи (мысленно отрезать последний элемент, который теперь на своем месте). + +Выполнить sift down для нового корня, чтобы восстановить свойства кучи в уменьшенной куче. + +Повторять шаги 1-3, пока размер кучи не станет равен 1. + +44. Приведите практический пример реализации второго этапа пирамидальной сортировки. +Начальная куча (и массив): [10, 5, 3, 4, 1] + +Шаг 1: Меняем 10 и 1. Массив: [1, 5, 3, 4, | 10]. Просеиваем 1. + +[5, 1, 3, 4, | 10] -> [5, 4, 3, 1, | 10]. + +Шаг 2: Меняем 5 и 1. Массив: [1, 4, 3, | 5, 10]. Просеиваем 1. + +[4, 1, 3, | 5, 10]. + +Шаг 3: Меняем 4 и 3. Массив: [3, 1, | 4, 5, 10]. Просеиваем 3. + +[3, 1, | 4, 5, 10] (не изменилось). + +Шаг 4: Меняем 3 и 1. Массив: [1, | 3, 4, 5, 10]. Сортировка завершена. + +45. Что можно сказать о трудоемкости метода пирамидальной сортировки? + +Временная сложность: Всегда O(n log n), как в среднем, так и в худшем случае. + +Сложность по памяти: O(1), так как сортировка происходит "на месте". + +Это делает пирамидальную сортировку надежной и предсказуемой, хотя на практике она часто немного медленнее, чем хорошо оптимизированная быстрая сортировка, из-за большего количества сравнений и худшей локализации ссылок (cache performance). + + + + +```python + +``` diff --git a/labs/lab_02/sorting_comparison.png b/labs/lab_02/sorting_comparison.png new file mode 100644 index 0000000..28fff85 Binary files /dev/null and b/labs/lab_02/sorting_comparison.png differ diff --git a/labs/lab_03/Untitled.md b/labs/lab_03/Untitled.md new file mode 100644 index 0000000..cf2b825 --- /dev/null +++ b/labs/lab_03/Untitled.md @@ -0,0 +1,2133 @@ +--- +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 +--- + +# **Линейные списки (Linked list)** + + +Асонов Сергей ИУ10-36 + + +## **Цель работы** + + +изучение структуры данных «Линейные списки», а также основных операций над ними. + + +### Задание 1 + +```python +from typing import Any, Self + +class Node: + def __init__(self, data:Any=None, next:'Node'=None): + self.data = data + self.next = next + + def __repr__(self): + return f'{self.__class__.__name__}(data={self.data}, next={self.next})' + +# ===== ВЕРСИЯ 1 ===== +class SingleLinkedList_v1: + 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: + '''Удалить первый элемент списка''' + 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.next is None: + return self.remove_first_node() + else: + 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'{self.__class__.__name__}({self._head})' + + def __str__(self): + node = self._head + l = [] + while node: + l.append(str(node.data)) + node = node.next + return 'LinkedList.head -> ' + ' -> '.join(l) + ' -> None' + +# ===== ВЕРСИЯ 2 ===== +class SingleLinkedList_v2(SingleLinkedList_v1): + def get_size(self) -> int: + '''Вернуть длину списка''' + count = 0 + current_node = self._head + while current_node is not None: + count += 1 + current_node = current_node.next + return count + + def find_node(self, value: Any) -> Any: + '''Найти (первый) узел по его значению и вернуть значение''' + current_node = self._head + while current_node is not None: + if current_node.data == value: + return current_node.data + current_node = current_node.next + return None + + def replace_node(self, old_value: Any, new_value: Any) -> None: + '''Найти (первый) узел по его значению и заменить его значение новым''' + current_node = self._head + while current_node is not None: + if current_node.data == old_value: + current_node.data = new_value + return + current_node = current_node.next + + def remove_node(self, value: Any) -> Any: + '''Найти (первый) узел по его значению и удалить его''' + if self._head is None: + return None + + if self._head.data == value: + return self.remove_first_node() + + current_node = self._head + while current_node.next is not None: + if current_node.next.data == value: + temp = current_node.next.data + current_node.next = current_node.next.next + return temp + current_node = current_node.next + + return None + +# ===== ВЕРСИЯ 3 ===== +class SingleLinkedList_v3(SingleLinkedList_v2): + def __init__(self) -> None: + '''Возвращает пустой список''' + super().__init__() + self._size = 0 + + def insert_first_node(self, value: Any) -> None: + '''Добавить элемент в начало списка''' + super().insert_first_node(value) + self._size += 1 + + def remove_first_node(self) -> Any: + '''Удалить первый элемент списка''' + if self._head is None: + return None + result = super().remove_first_node() + self._size -= 1 + return result + + def insert_last_node(self, value: Any) -> None: + '''Добавить элемент в конец списка''' + super().insert_last_node(value) + self._size += 1 + + def remove_last_node(self) -> Any: + '''Удалить последний элемент списка''' + if self._head is None: + return None + result = super().remove_last_node() + self._size -= 1 + return result + + def remove_node(self, value: Any) -> Any: + '''Найти (первый) узел по его значению и удалить его''' + if self._head is None: + return None + + if self._head.data == value: + return self.remove_first_node() + + current_node = self._head + while current_node.next is not None: + if current_node.next.data == value: + temp = current_node.next.data + current_node.next = current_node.next.next + self._size -= 1 + return temp + current_node = current_node.next + + return None + + def get_size(self) -> int: + '''Вернуть длину списка - O(1)''' + return self._size + + def __repr__(self) -> str: + return f'{self.__class__.__name__}(size={self._size}, {self._head})' + + def __str__(self): + return f'LinkedList(size={self._size}).head -> ' + super().__str__().split(' -> ', 1)[1] + +# ===== ВЕРСИЯ 4 ===== +class SingleLinkedList_v4(SingleLinkedList_v3): + def find_previous_node(self, value: Any) -> Any: + '''Найти (первый) узел по его значению и вернуть значение из предыдущего узла''' + if self._head is None or self._head.next is None: + return None + + if self._head.next.data == value: + return self._head.data + + current_node = self._head + while current_node.next is not None and current_node.next.next is not None: + if current_node.next.next.data == value: + return current_node.next.data + current_node = current_node.next + + return None + + def find_next_node(self, value: Any) -> Any: + '''Найти (первый) узел по его значению и вернуть значение из следующего узла''' + current_node = self._head + while current_node is not None: + if current_node.data == value and current_node.next is not None: + return current_node.next.data + current_node = current_node.next + return None + + def insert_before_node(self, target_value: Any, new_value: Any) -> None: + '''Найти (первый) узел по его значению и добавить узел перед ним''' + if self._head is None: + return + + if self._head.data == target_value: + self.insert_first_node(new_value) + return + + current_node = self._head + while current_node.next is not None: + if current_node.next.data == target_value: + new_node = Node(new_value, current_node.next) + current_node.next = new_node + self._size += 1 + return + current_node = current_node.next + + def insert_after_node(self, target_value: Any, new_value: Any) -> None: + '''Найти (первый) узел по его значению и добавить узел после него''' + current_node = self._head + while current_node is not None: + if current_node.data == target_value: + new_node = Node(new_value, current_node.next) + current_node.next = new_node + self._size += 1 + return + current_node = current_node.next + + def replace_previous_node(self, target_value: Any, new_value: Any) -> None: + '''Найти (первый) узел по его значению и заменить значение в предыдущем узле''' + if self._head is None or self._head.next is None: + return + + if self._head.next.data == target_value: + self._head.data = new_value + return + + current_node = self._head + while current_node.next is not None and current_node.next.next is not None: + if current_node.next.next.data == target_value: + current_node.next.data = new_value + return + current_node = current_node.next + + def replace_next_node(self, target_value: Any, new_value: Any) -> None: + '''Найти (первый) узел по его значению и заменить значение в следующем узле''' + current_node = self._head + while current_node is not None and current_node.next is not None: + if current_node.data == target_value: + current_node.next.data = new_value + return + current_node = current_node.next + + def remove_previous_node(self, target_value: Any) -> Any: + '''Найти (первый) узел по его значению и удалить предыдущий узел''' + if self._head is None or self._head.next is None: + return None + + if self._head.next.data == target_value: + return self.remove_first_node() + + current_node = self._head + while current_node.next is not None and current_node.next.next is not None: + if current_node.next.next.data == target_value: + temp = current_node.next.data + current_node.next = current_node.next.next + self._size -= 1 + return temp + current_node = current_node.next + + return None + + def remove_next_node(self, target_value: Any) -> Any: + '''Найти (первый) узел по его значению и удалить следующий узел''' + current_node = self._head + while current_node is not None and current_node.next is not None: + if current_node.data == target_value: + temp = current_node.next.data + current_node.next = current_node.next.next + self._size -= 1 + return temp + current_node = current_node.next + return None + +# ===== ВЕРСИЯ 5 ===== +class SingleLinkedList_v5(SingleLinkedList_v4): + def __init__(self) -> None: + '''Возвращает пустой список''' + 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 remove_first_node(self) -> Any: + '''Удалить первый элемент списка''' + if self._head is None: + return None + + if self._head.next is None: + self._tail = None + + result = super().remove_first_node() + return result + + 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_last_node(self) -> Any: + '''Удалить последний элемент списка''' + if self._head is None: + return None + + 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 + self._tail = current_node + self._size -= 1 + return temp + + def insert_after_node(self, target_value: Any, new_value: Any) -> None: + '''Найти (первый) узел по его значению и добавить узел после него''' + current_node = self._head + while current_node is not None: + if current_node.data == target_value: + new_node = Node(new_value, current_node.next) + current_node.next = new_node + self._size += 1 + if current_node == self._tail: + self._tail = new_node + return + current_node = current_node.next + + def remove_node(self, value: Any) -> Any: + '''Найти (первый) узел по его значению и удалить его''' + if self._head is None: + return None + + if self._head.data == value: + return self.remove_first_node() + + current_node = self._head + while current_node.next is not None: + if current_node.next.data == value: + if current_node.next == self._tail: + self._tail = current_node + + temp = current_node.next.data + current_node.next = current_node.next.next + self._size -= 1 + return temp + current_node = current_node.next + + return None + + def remove_next_node(self, target_value: Any) -> Any: + '''Найти (первый) узел по его значению и удалить следующий узел''' + current_node = self._head + while current_node is not None and current_node.next is not None: + if current_node.data == target_value: + if current_node.next == self._tail: + self._tail = current_node + + temp = current_node.next.data + current_node.next = current_node.next.next + self._size -= 1 + return temp + current_node = current_node.next + return None + + def get_tail(self) -> Any: + '''Получить значение последнего элемента''' + return self._tail.data if self._tail else None + + def clear(self) -> None: + '''Очистить весь список''' + super().clear() + self._tail = None + + def __repr__(self) -> str: + tail_value = self._tail.data if self._tail else None + return f'{self.__class__.__name__}(size={self._size}, head={self._head}, tail={tail_value})' + + def __str__(self): + tail_info = f", tail={self._tail.data}" if self._tail else "" + return f'LinkedList(size={self._size}{tail_info}).head -> ' + super().__str__().split(' -> ', 1)[1] + +# ===== ВЕРСИЯ 6 ===== +class SingleLinkedList_v6(SingleLinkedList_v5): + def insert_before_node(self, target_value: Any, new_value: Any) -> None: + '''Найти (первый) узел по его значению и добавить узел перед ним через замену''' + current_node = self._head + while current_node is not None: + if current_node.data == target_value: + old_value = current_node.data + current_node.data = new_value + new_node = Node(old_value, current_node.next) + current_node.next = new_node + self._size += 1 + if current_node == self._tail: + self._tail = new_node + return + current_node = current_node.next + + def replace_previous_node(self, target_value: Any, new_value: Any) -> None: + '''Найти (первый) узел по его значению и заменить значение в предыдущем узле''' + prev_value = self.find_previous_node(target_value) + if prev_value is not None: + current_node = self._head + while current_node is not None: + if current_node.data == prev_value: + current_node.data = new_value + return + current_node = current_node.next + + def remove_previous_node(self, target_value: Any) -> Any: + '''Найти (первый) узел по его значению и удалить предыдущий узел''' + if self._head is None or self._head.next is None: + return None + + if self._head.next.data == target_value: + return self.remove_first_node() + + current_node = self._head + while current_node.next is not None and current_node.next.next is not None: + if current_node.next.next.data == target_value: + temp = current_node.next.data + current_node.next = current_node.next.next + self._size -= 1 + return temp + current_node = current_node.next + + return None + + + + """Tестирование связных списков""" +def test_linked_lists(): + + + print("ТЕСТИРОВАНИЕ СВЯЗНЫХ СПИСКОВ") + print("=" * 40) + + # Тестируем основные версии + versions = [SingleLinkedList_v1, SingleLinkedList_v3, SingleLinkedList_v5] + + for version_class in versions: + print(f"\nТестируем {version_class.__name__}:") + print("-" * 30) + + # Создаем список + lst = version_class() + + # 1. Базовые операции + print("1. Базовые операции:") + lst.insert_first_node(3) + lst.insert_first_node(2) + lst.insert_first_node(1) + print(f" После добавления в начало: {lst}") + + lst.insert_last_node(4) + lst.insert_last_node(5) + print(f" После добавления в конец: {lst}") + + removed_first = lst.remove_first_node() + print(f" Удален из начала: {removed_first}") + + removed_last = lst.remove_last_node() + print(f" Удален из конца: {removed_last}") + print(f" Текущий список: {lst}") + + # 2. Дополнительные возможности + if hasattr(lst, 'get_size'): + print(f" Размер списка: {lst.get_size()}") + + if hasattr(lst, 'find_node'): + found = lst.find_node(3) + print(f" Поиск значения 3: {found}") + + if hasattr(lst, 'get_tail'): + tail = lst.get_tail() + print(f" Значение хвоста: {tail}") + + print(" [ПРОЙДЕНО]") + +def test_edge_cases(): + """Тестирование граничных случаев""" + print("\nТЕСТИРОВАНИЕ ГРАНИЧНЫХ СЛУЧАЕВ") + print("=" * 40) + + lst = SingleLinkedList_v3() + + # 1. Пустой список + print("1. Пустой список:") + print(f" {lst}") + + # 2. Удаление из пустого списка + print("2. Удаление из пустого списка:") + try: + lst.remove_first_node() + except Exception as e: + print(f" Ошибка: {type(e).__name__}") + + # 3. Один элемент + print("3. Работа с одним элементом:") + lst.insert_first_node(999) + print(f" Добавлен: {lst}") + removed = lst.remove_first_node() + print(f" Удален: {removed}, список: {lst}") + + print(" [ГРАНИЧНЫЕ СЛУЧАИ ПРОЙДЕНЫ]") + +def test_advanced_operations(): + """Тестирование продвинутых операций""" + print("\nТЕСТИРОВАНИЕ ПРОДВИНУТЫХ ОПЕРАЦИЙ") + print("=" * 40) + + # Тестируем v4 с дополнительными функциями + lst = SingleLinkedList_v4() + + # Создаем тестовый список + for i in range(1, 6): + lst.insert_last_node(i * 10) + + print("1. Исходный список:") + print(f" {lst}") + + # Тестируем поиск соседей + print("2. Поиск соседних узлов:") + prev = lst.find_previous_node(30) + next_val = lst.find_next_node(30) + print(f" Предыдущий для 30: {prev}") + print(f" Следующий для 30: {next_val}") + + # Тестируем вставку + print("3. Вставка узлов:") + lst.insert_before_node(30, 25) + lst.insert_after_node(30, 35) + print(f" После вставки 25 и 35: {lst}") + + # Тестируем замену + print("4. Замена узлов:") + lst.replace_node(25, 26) + print(f" После замены 25 на 26: {lst}") + + print(" [ПРОДВИНУТЫЕ ОПЕРАЦИИ ПРОЙДЕНЫ]") + +def test_v5_tail_operations(): + """Специфичное тестирование для v5 с хвостом""" + print("\nТЕСТИРОВАНИЕ V5 (С ХВОСТОМ)") + print("=" * 40) + + lst = SingleLinkedList_v5() + + print("1. Проверка хвоста при операциях:") + lst.insert_last_node(100) + lst.insert_last_node(200) + lst.insert_last_node(300) + print(f" Список: {lst}") + print(f" Хвост: {lst.get_tail()}") + + lst.insert_last_node(400) + print(f" После добавления 400, хвост: {lst.get_tail()}") + + lst.remove_last_node() + print(f" После удаления последнего, хвост: {lst.get_tail()}") + + print(" [ТЕСТ V5 ПРОЙДЕН]") + +def run_all_tests(): + """Запуск всех тестов""" + print("НАЧАЛО ТЕСТИРОВАНИЯ") + print("=" * 50) + + test_linked_lists() + test_edge_cases() + test_advanced_operations() + test_v5_tail_operations() + + print("\n" + "=" * 50) + print("ВСЕ ТЕСТЫ ЗАВЕРШЕНЫ УСПЕШНО!") + +# Запускаем тесты +if __name__ == "__main__": + run_all_tests() +``` + + +**Линейный односвязный список (Версия 1)** +Линейный односвязный список — абстрактный тип данных, который поддерживает следующие операции: + +init() -> SingleLinkedList +Возвращает пустой односвязный список. + +insert_first_node(ValueType value) -> None +Добавить узел в начало списка. + +remove_first_node() -> ValueType +Удалить первый узел списка и вернуть его значение. + +insert_last_node(ValueType value) -> None +Добавить узел в конец списка. + +remove_last_node() -> ValueType +Удалить последний элемент списка. + +**Линейный односвязный список (Версия 2). Наследуется от версии 1** +get_size() -> Integer +Вернуть длину списка + +find_node(ValueType value) -> ValueType +Найти (первый) узел по его значению и вернуть значение (связанные элементы). + +replace_node(ValueType old_value, ValueType new_value) -> None +Найти (первый) узел по его значению и заменить его значение новым. + +remove_node(ValueType value) -> ValueType +Найти (первый) узел по его значению и удалить его. + +**Линейный односвязный список (Версия 3). Наследуется от версии** +Cохраняем значения длины и обновляем его при выполнении каждой операции + +**Линейный односвязный список (Версия 4). Наследуется от версии 3** +find_previos_node(ValueType value) -> ValueType +Найти (первый) узел по его значению и вернуть значение из предудущего узла (если такой есть). + +find_next_node(ValueType value) -> ValueType +Найти (первый) узел по его значению и вернуть значение из следующего узла (если такой есть). + +insert_before_node(ValueType value) -> None +Найти (первый) узел по его значению и добавить узел перед ним. (Если узел не найден, ничего не делать) + +insert_after_node(ValueType value) -> None +Найти (первый) узел по его значению и добавить узел после него. (Если узел не найден, ничего не делать) + +replace_previos_node(ValueType old_value, ValueType new_value) -> None +Найти (первый) узел по его значению и заменить значение в предыдущем узле на новое. + +replace_next_node(ValueType old_value, ValueType new_value) -> None +Найти (первый) узел по его значению и заменить значение в следующем узле на новое. + +remove_previos_node(ValueType value) -> ValueType +Найти (первый) узел по его значению и удалить предыдущий узел (если такой есть). + +remove_next_node(ValueType value) -> ValueType +Найти (первый) узел по его значению и удалить следующий узел (если такой есть). +Вставка/замена/удаление соседних узлов + +**Линейный односвязный список (Версия 5). Наследуется от версии 4** +Дополнительно храним ссылку на последний элемент списка + +**Линейный односвязный список (Версия 6). Наследуется от версии 5** +При необходимости модифицировать (вставить) узел перед текущим, меняем значение в текущем узле + +Каждая последующая версия наследуется от предыдущей и добавляет новую функциональность или оптимизации, соответствующие требованиям в описании задачи. + + +### **Задание 2-3** + +```python +class SingleLinkedList_v7(SingleLinkedList_v6): + def reverse(self) -> None: + '''Перевернуть линейный список на месте''' + if self._head is None or self._head.next is None: + return # Пустой список или список с одним элементом + + prev_node = None + current_node = self._head + self._tail = self._head # Голова становится хвостом + + while current_node is not None: + next_node = current_node.next # Сохраняем следующий узел + current_node.next = prev_node # Разворачиваем указатель + prev_node = current_node # Перемещаем prev на текущий + current_node = next_node # Перемещаем current на следующий + + self._head = prev_node # Новая голова - бывший последний элемент + + def sort(self) -> None: + '''Сортировка линейного списка на месте (пузырьковая сортировка)''' + if self._head is None or self._head.next is None: + return # Пустой список или список с одним элементом + + swapped = True + while swapped: + swapped = False + current_node = self._head + + while current_node.next is not None: + if current_node.data > current_node.next.data: + # Меняем данные местами + current_node.data, current_node.next.data = current_node.next.data, current_node.data + swapped = True + current_node = current_node.next + + +def test_v7_operations(): + """Тестирование SingleLinkedList_v7""" + print("ТЕСТИРОВАНИЕ V7 (РЕВЕРС И СОРТИРОВКА)") + print("=" * 45) + + # Создаем список v7 + lst = SingleLinkedList_v7() + + # Тест 1: Реверс пустого списка и списка с одним элементом + print("1. Реверс особых случаев:") + lst.reverse() + print(f" Реверс пустого: {lst}") + + lst.insert_last_node(5) + lst.reverse() + print(f" Реверс одного элемента: {lst}") + lst.remove_first_node() + + # Тест 2: Реверс обычного списка + print("\n2. Реверс списка:") + for i in [1, 2, 3, 4, 5]: + lst.insert_last_node(i) + print(f" Исходный: {lst}") + + lst.reverse() + print(f" После реверса: {lst}") + print(f" Голова: {lst._head.data}, Хвост: {lst.get_tail()}") + + # Тест 3: Проверка целостности после реверса + print("\n3. Проверка целостности:") + # Добавляем новый элемент в конец + lst.insert_last_node(0) + print(f" После добавления 0: {lst}") + print(f" Хвост после добавления: {lst.get_tail()}") + + print("\n [ТЕСТ V7 ПРОЙДЕН]") + + +def complete_sort(lst): + """Полная версия пузырьковой сортировки для тестирования""" + if lst._head is None or lst._head.next is None: + return + + swapped = True + while swapped: + swapped = False + current_node = lst._head + + while current_node.next is not None: + if current_node.data > current_node.next.data: + current_node.data, current_node.next.data = current_node.next.data, current_node.data + swapped = True + current_node = current_node.next + +"""Тестирование сортировки для V7""" +def test_v7_sorting(): + + print("\nТЕСТИРОВАНИЕ СОРТИРОВКИ V7") + print("=" * 45) + + # Тест 1: Сортировка несортированного списка + print("1. Сортировка несортированного списка:") + lst = SingleLinkedList_v7() + for i in [5, 2, 8, 1, 9]: + lst.insert_last_node(i) + print(f" До сортировки: {lst}") + + complete_sort(lst) + print(f" После сортировки: {lst}") + + # Тест 2: Сортировка отсортированного списка + print("\n2. Сортировка отсортированного списка:") + lst2 = SingleLinkedList_v7() + for i in [1, 2, 3, 4, 5]: + lst2.insert_last_node(i) + print(f" Исходный: {lst2}") + + complete_sort(lst2) + print(f" После сортировки: {lst2}") + + # Тест 3: Сортировка с одинаковыми элементами + print("\n3. Сортировка с одинаковыми элементами:") + lst3 = SingleLinkedList_v7() + for i in [3, 1, 3, 2, 3]: + lst3.insert_last_node(i) + print(f" До сортировки: {lst3}") + + complete_sort(lst3) + print(f" После сортировки: {lst3}") + + print("\n [ТЕСТ СОРТИРОВКИ ПРОЙДЕН]") + + +def test_v7_combined(): + """Тестирование комбинированных операций""" + print("\nТЕСТИРОВАНИЕ КОМБИНИРОВАННЫХ ОПЕРАЦИЙ V7") + print("=" * 45) + + lst = SingleLinkedList_v7() + + # Тест: Сортировка -> Реверс -> Добавление + print("1. Сортировка -> Реверс -> Добавление:") + for i in [7, 3, 9, 1, 5]: + lst.insert_last_node(i) + print(f" Исходный: {lst}") + + complete_sort(lst) + print(f" После сортировки: {lst}") + + lst.reverse() + print(f" После реверса: {lst}") + + lst.insert_last_node(10) + print(f" После добавления 10: {lst}") + print(f" Хвост: {lst.get_tail()}") + + # Тест: Реверс -> Сортировка + print("\n2. Реверс -> Сортировка:") + lst2 = SingleLinkedList_v7() + for i in [1, 2, 3, 4, 5]: + lst2.insert_last_node(i) + print(f" Исходный: {lst2}") + + lst2.reverse() + print(f" После реверса: {lst2}") + + complete_sort(lst2) + print(f" После сортировки: {lst2}") + + print("\n [КОМБИНИРОВАННЫЕ ОПЕРАЦИИ ПРОЙДЕНЫ]") + + +# Запуск всех тестов +def run_v7_tests(): + """Запуск всех тестов для V7""" + print("НАЧАЛО ТЕСТИРОВАНИЯ SINGLELINKEDLIST_V7") + print("=" * 50) + + test_v7_operations() + test_v7_sorting() + test_v7_combined() + + print("\n" + "=" * 50) + print("ВСЕ ТЕСТЫ V7 ЗАВЕРШЕНЫ УСПЕШНО!") + + +# Запускаем тесты +if __name__ == "__main__": + run_v7_tests() +``` + +### **Задание 4** + +```python +class SingleLinkedList_v8(SingleLinkedList_v7): + + # 1. Элементы, входящие в L1, но не входящие в L2 + def difference(self, other: 'SingleLinkedList_v8') -> 'SingleLinkedList_v8': + result = SingleLinkedList_v8() + current = self._head + while current: + if not other.contains(current.data): + result.insert_last_node(current.data) + current = current.next + return result + + # 2. Количество максимальных элементов + def count_max_elements(self) -> int: + if self._head is None: + return 0 + + max_val = self._head.data + count = 1 + + current = self._head.next + while current: + if current.data > max_val: + max_val = current.data + count = 1 + elif current.data == max_val: + count += 1 + current = current.next + + return count + + # 3. Список без повторений + def input_unique_numbers(self) -> None: + """Ввод чисел пользователем без повторений""" + seen = set() + + print("Введите числа (пустая строка для завершения):") + while True: + try: + user_input = input().strip() + if not user_input: + break + num = int(user_input) + if num not in seen: + seen.add(num) + self.insert_last_node(num) + except ValueError: + print("Ошибка: введите целое число") + + # 4. Отсортированная копия + def sorted_copy(self) -> 'SingleLinkedList_v8': + result = SingleLinkedList_v8() + current = self._head + + # Копируем элементы + while current: + result.insert_last_node(current.data) + current = current.next + + # Сортируем + result.sort() + return result + + # 5. Оставить только последние вхождения + def keep_last_occurrences(self) -> None: + if self._head is None: + return + + seen = set() + prev = None + current = self._head + + # Проходим с конца (реверсируем, обрабатываем, снова реверсируем) + self.reverse() + + current = self._head + prev = None + seen = set() + + while current: + if current.data in seen: + # Удаляем дубликат + if prev: + prev.next = current.next + else: + self._head = current.next + self._size -= 1 + else: + seen.add(current.data) + prev = current + current = current.next + + self.reverse() + + # 6. Убрать подряд идущие дубликаты + def remove_consecutive_duplicates(self) -> None: + if self._head is None: + return + + current = self._head + while current and current.next: + if current.data == current.next.data: + current.next = current.next.next + self._size -= 1 + else: + current = current.next + + # 7. Пересечение двух списков + def intersection(self, other: 'SingleLinkedList_v8') -> 'SingleLinkedList_v8': + result = SingleLinkedList_v8() + current = self._head + seen_in_other = set() + + # Собираем элементы из other + other_current = other._head + while other_current: + seen_in_other.add(other_current.data) + other_current = other_current.next + + # Добавляем только те, что есть в обоих + while current: + if current.data in seen_in_other: + result.insert_last_node(current.data) + current = current.next + + return result + + # 8. Разделить на положительные и отрицательные + def split_positive_negative(self) -> tuple: + positive = SingleLinkedList_v8() + negative = SingleLinkedList_v8() + + current = self._head + while current: + if current.data >= 0: + positive.insert_last_node(current.data) + else: + negative.insert_last_node(current.data) + current = current.next + + return positive, negative + + # 9. Продублировать четные числа + def duplicate_even_numbers(self) -> None: + current = self._head + while current: + if current.data % 2 == 0: + new_node = Node(current.data, current.next) + current.next = new_node + current = new_node.next + self._size += 1 + else: + current = current.next + + # Обновляем tail + self._update_tail() + + # 10. Проверка на упорядоченность по возрастанию + def is_sorted_ascending(self) -> bool: + if self._head is None or self._head.next is None: + return True + + current = self._head + while current.next: + if current.data > current.next.data: + return False + current = current.next + + return True + + # 11. Удалить нечетные числа + def remove_odd_numbers(self) -> None: + # Удаляем с начала пока голова нечетная + while self._head and self._head.data % 2 != 0: + self._head = self._head.next + self._size -= 1 + + if self._head is None: + self._tail = None + return + + # Удаляем нечетные в середине/конце + current = self._head + while current and current.next: + if current.next.data % 2 != 0: + current.next = current.next.next + self._size -= 1 + else: + current = current.next + + self._update_tail() + + # 12. Оставить только первые вхождения + def keep_first_occurrences(self) -> None: + if self._head is None: + return + + seen = set() + prev = None + current = self._head + + while current: + if current.data in seen: + # Удаляем дубликат + if prev: + prev.next = current.next + else: + self._head = current.next + self._size -= 1 + else: + seen.add(current.data) + prev = current + current = current.next + + self._update_tail() + + # 13. Префиксные суммы + def prefix_sums(self) -> 'SingleLinkedList_v8': + result = SingleLinkedList_v8() + if self._head is None: + return result + + current_sum = 0 + current = self._head + + while current: + current_sum += current.data + result.insert_last_node(current_sum) + current = current.next + + return result + + # 14. Пересечение (альтернативная реализация) + def intersection_alt(self, other: 'SingleLinkedList_v8') -> 'SingleLinkedList_v8': + return self.intersection(other) + + # 15. Удалить уникальные элементы + def remove_unique_elements(self) -> None: + if self._head is None: + return + + # Считаем частоты + frequency = {} + current = self._head + while current: + frequency[current.data] = frequency.get(current.data, 0) + 1 + current = current.next + + # Удаляем элементы с частотой 1 + prev = None + current = self._head + while current: + if frequency[current.data] == 1: + if prev: + prev.next = current.next + else: + self._head = current.next + self._size -= 1 + else: + prev = current + current = current.next + + self._update_tail() + + # 16. Количество различных в возрастающей последовательности + def count_distinct_in_sorted(self) -> int: + if self._head is None: + return 0 + + count = 1 + current = self._head + + while current.next: + if current.data != current.next.data: + count += 1 + current = current.next + + return count + + # 17. Удалить четные числа + def remove_even_numbers(self) -> None: + # Удаляем с начала пока голова четная + while self._head and self._head.data % 2 == 0: + self._head = self._head.next + self._size -= 1 + + if self._head is None: + self._tail = None + return + + # Удаляем четные в середине/конце + current = self._head + while current and current.next: + if current.next.data % 2 == 0: + current.next = current.next.next + self._size -= 1 + else: + current = current.next + + self._update_tail() + + # 18. Преобразовать четные/нечетные + def transform_even_odd(self) -> None: + current = self._head + while current: + if current.data % 2 == 0: + current.data = current.data // 2 + else: + current.data = current.data * 2 + current = current.next + + # 19. Есть ли элементы больше суммы всех + def has_elements_greater_than_sum(self) -> bool: + if self._head is None: + return False + + total_sum = 0 + current = self._head + while current: + total_sum += current.data + current = current.next + + current = self._head + while current: + if current.data > total_sum: + return True + current = current.next + + return False + + # 20. Удалить элементы, входящие более двух раз + def remove_elements_more_than_twice(self) -> None: + if self._head is None: + return + + # Считаем частоты + frequency = {} + current = self._head + while current: + frequency[current.data] = frequency.get(current.data, 0) + 1 + current = current.next + + # Удаляем элементы с частотой > 2 + count = {} + prev = None + current = self._head + + while current: + count[current.data] = count.get(current.data, 0) + 1 + + if frequency[current.data] > 2 and count[current.data] > 2: + # Удаляем лишние вхождения + if prev: + prev.next = current.next + else: + self._head = current.next + self._size -= 1 + else: + prev = current + + current = current.next + + self._update_tail() + + # 21. Среднее арифметическое + def arithmetic_mean(self) -> float: + if self._head is None: + return 0.0 + + total_sum = 0 + count = 0 + current = self._head + + while current: + total_sum += current.data + count += 1 + current = current.next + + return total_sum / count + + # 22. Слияние двух отсортированных списков + def merge_sorted(self, other: 'SingleLinkedList_v8') -> 'SingleLinkedList_v8': + result = SingleLinkedList_v8() + current1 = self._head + current2 = other._head + + while current1 and current2: + if current1.data <= current2.data: + result.insert_last_node(current1.data) + current1 = current1.next + else: + result.insert_last_node(current2.data) + current2 = current2.next + + # Добавляем оставшиеся элементы + while current1: + result.insert_last_node(current1.data) + current1 = current1.next + + while current2: + result.insert_last_node(current2.data) + current2 = current2.next + + return result + + # 23. Удалить каждый третий элемент + def remove_every_third(self) -> None: + if self._head is None: + return + + count = 1 + prev = None + current = self._head + + while current: + if count % 3 == 0: + if prev: + prev.next = current.next + else: + self._head = current.next + self._size -= 1 + else: + prev = current + + current = current.next + count += 1 + + self._update_tail() + + # 24. Простые числа + def filter_primes(self) -> 'SingleLinkedList_v8': + def is_prime(n): + if n < 2: + return False + for i in range(2, int(n**0.5) + 1): + if n % i == 0: + return False + return True + + result = SingleLinkedList_v8() + current = self._head + + while current: + if is_prime(int(current.data)): + result.insert_last_node(current.data) + current = current.next + + return result + + # 25. Проверка на равенство множеств + def sets_equal(self, other: 'SingleLinkedList_v8') -> bool: + set1 = set() + set2 = set() + + current = self._head + while current: + set1.add(current.data) + current = current.next + + current = other._head + while current: + set2.add(current.data) + current = current.next + + return set1 == set2 + + # 26. Среднее арифметическое (дубликат) + def average(self) -> float: + return self.arithmetic_mean() + + # 27. Элементы, делящиеся на 3, в порядке убывания + def divisible_by_three_descending(self) -> 'SingleLinkedList_v8': + result = SingleLinkedList_v8() + current = self._head + + while current: + if current.data % 3 == 0: + result.insert_last_node(current.data) + current = current.next + + result.sort() + result.reverse() + return result + + # 28. Удалить все минимальные элементы + def remove_all_minimal(self) -> None: + if self._head is None: + return + + # Находим минимум + min_val = self._head.data + current = self._head.next + while current: + if current.data < min_val: + min_val = current.data + current = current.next + + # Удаляем все минимальные + while self._head and self._head.data == min_val: + self._head = self._head.next + self._size -= 1 + + if self._head is None: + self._tail = None + return + + current = self._head + while current and current.next: + if current.next.data == min_val: + current.next = current.next.next + self._size -= 1 + else: + current = current.next + + self._update_tail() + + # 29. Среднее геометрическое + def geometric_mean(self) -> float: + if self._head is None: + return 0.0 + + product = 1 + count = 0 + current = self._head + + while current: + product *= current.data + count += 1 + current = current.next + + return product ** (1 / count) + + # 30. Добавить сумму трех предыдущих после каждого третьего + def add_sum_of_three_after_every_third(self) -> None: + if self._size < 3: + return + + count = 1 + current = self._head + + while current and current.next and current.next.next: + if count % 3 == 0: + # Находим три предыдущих элемента + prev3 = current + prev2 = current.next if current.next else None + prev1 = current.next.next if current.next and current.next.next else None + + if prev3 and prev2 and prev1: + sum_three = prev3.data + prev2.data + prev1.data + new_node = Node(sum_three, current.next) + current.next = new_node + self._size += 1 + current = new_node + + current = current.next + count += 1 + + self._update_tail() + + # 31. Разделить на четные и нечетные + def split_even_odd(self) -> tuple: + even = SingleLinkedList_v8() + odd = SingleLinkedList_v8() + + current = self._head + while current: + if current.data % 2 == 0: + even.insert_last_node(current.data) + else: + odd.insert_last_node(current.data) + current = current.next + + return even, odd + + # 32. Продублировать простые числа + def duplicate_primes(self) -> None: + def is_prime(n): + if n < 2: + return False + for i in range(2, int(n**0.5) + 1): + if n % i == 0: + return False + return True + + current = self._head + while current: + if is_prime(int(current.data)): + new_node = Node(current.data, current.next) + current.next = new_node + current = new_node.next + self._size += 1 + else: + current = current.next + + self._update_tail() + + # 33. Попарные произведения + def pairwise_product(self, other: 'SingleLinkedList_v8') -> 'SingleLinkedList_v8': + result = SingleLinkedList_v8() + current1 = self._head + current2 = other._head + + while current1 and current2: + result.insert_last_node(current1.data * current2.data) + current1 = current1.next + current2 = current2.next + + return result + + # 34. Количество различных значений + def count_distinct(self) -> int: + distinct = set() + current = self._head + + while current: + distinct.add(current.data) + current = current.next + + return len(distinct) + + # 35. Удалить все максимальные элементы + def remove_all_maximal(self) -> None: + if self._head is None: + return + + # Находим максимум + max_val = self._head.data + current = self._head.next + while current: + if current.data > max_val: + max_val = current.data + current = current.next + + # Удаляем все максимальные + while self._head and self._head.data == max_val: + self._head = self._head.next + self._size -= 1 + + if self._head is None: + self._tail = None + return + + current = self._head + while current and current.next: + if current.next.data == max_val: + current.next = current.next.next + self._size -= 1 + else: + current = current.next + + self._update_tail() + + # 36. Проверка на геометрическую прогрессию + def is_geometric_progression(self) -> bool: + if self._head is None or self._head.next is None: + return True + + current = self._head + ratio = None + + while current and current.next: + if current.data == 0: + return False # Ноль нарушает прогрессию + + current_ratio = current.next.data / current.data + + if ratio is None: + ratio = current_ratio + elif abs(current_ratio - ratio) > 1e-10: # Учет погрешности для float + return False + + current = current.next + + return True + + # 37. Поменять местами максимальный и минимальный элементы + def swap_min_max(self) -> None: + if self._head is None or self._head.next is None: + return + + # Находим min и max узлы + min_node = max_node = self._head + current = self._head + + while current: + if current.data < min_node.data: + min_node = current + if current.data > max_node.data: + max_node = current + current = current.next + + # Меняем значения + min_node.data, max_node.data = max_node.data, min_node.data + + # Вспомогательные методы + def contains(self, value: Any) -> bool: + current = self._head + while current: + if current.data == value: + return True + current = current.next + return False + + def _update_tail(self) -> None: + if self._head is None: + self._tail = None + return + + current = self._head + while current and current.next: + current = current.next + self._tail = current +``` + +### ** Задание 5** + +```python +class Node: + """Узел кольцевого двусвязного списка""" + def __init__(self, data=None): + self.data = data + self.next = None + self.prev = None + + def __str__(self): + return f"Node({self.data})" + + +class CircularDoublyLinkedList: + """Кольцевой двусвязный список с выделенным головным элементом""" + + def __init__(self): + # Создаем головной элемент (не содержит данных) + self.head = Node() + # В пустом списке head ссылается сам на себя + self.head.next = self.head + self.head.prev = self.head + self.size = 0 + + def is_empty(self): + """Проверка на пустоту""" + return self.size == 0 + + def __len__(self): + """Возвращает количество элементов (исключая головной)""" + return self.size + + def insert_first(self, data): + """Вставка элемента в начало списка (после головного)""" + new_node = Node(data) + + # Связываем новый узел + new_node.next = self.head.next + new_node.prev = self.head + + # Обновляем связи соседних узлов + self.head.next.prev = new_node + self.head.next = new_node + + self.size += 1 + return new_node + + def insert_last(self, data): + """Вставка элемента в конец списка (перед головным)""" + new_node = Node(data) + + # Связываем новый узел + new_node.next = self.head + new_node.prev = self.head.prev + + # Обновляем связи соседних узлов + self.head.prev.next = new_node + self.head.prev = new_node + + self.size += 1 + return new_node + + def insert_after(self, node, data): + """Вставка элемента после указанного узла""" + if node is None: + return self.insert_first(data) + + new_node = Node(data) + + # Связываем новый узел + new_node.next = node.next + new_node.prev = node + + # Обновляем связи соседних узлов + node.next.prev = new_node + node.next = new_node + + self.size += 1 + return new_node + + def insert_before(self, node, data): + """Вставка элемента перед указанным узлом""" + if node is None or node == self.head: + return self.insert_last(data) + + new_node = Node(data) + + # Связываем новый узел + new_node.next = node + new_node.prev = node.prev + + # Обновляем связи соседних узлов + node.prev.next = new_node + node.prev = new_node + + self.size += 1 + return new_node + + def remove(self, node): + """Удаление указанного узла""" + if node is None or node == self.head or self.is_empty(): + return None + + # Сохраняем данные для возврата + data = node.data + + # Перелинковываем соседние узлы + node.prev.next = node.next + node.next.prev = node.prev + + # Очищаем связи удаляемого узла + node.next = None + node.prev = None + + self.size -= 1 + return data + + def remove_first(self): + """Удаление первого элемента""" + return self.remove(self.head.next) if not self.is_empty() else None + + def remove_last(self): + """Удаление последнего элемента""" + return self.remove(self.head.prev) if not self.is_empty() else None + + def find(self, data): + """Поиск первого узла с указанными данными""" + current = self.head.next + while current != self.head: + if current.data == data: + return current + current = current.next + return None + + def traverse_forward(self): + """Обход списка в прямом направлении""" + result = [] + current = self.head.next + while current != self.head: + result.append(current.data) + current = current.next + return result + + def traverse_backward(self): + """Обход списка в обратном направлении""" + result = [] + current = self.head.prev + while current != self.head: + result.append(current.data) + current = current.prev + return result + + def get_first(self): + """Получить первый элемент""" + return self.head.next if not self.is_empty() else None + + def get_last(self): + """Получить последний элемент""" + return self.head.prev if not self.is_empty() else None + + def rotate_forward(self, steps=1): + """Повернуть список вперед на указанное количество шагов""" + if self.is_empty() or steps == 0: + return + + steps = steps % self.size + for _ in range(steps): + # Перемещаем первый элемент в конец + first = self.head.next + last = self.head.prev + + # Перелинковываем + self.head.next = first.next + first.next.prev = self.head + + first.next = self.head + first.prev = last + + last.next = first + self.head.prev = first + + def __str__(self): + """Строковое представление списка""" + if self.is_empty(): + return "CircularDoublyLinkedList: []" + + elements = [] + current = self.head.next + while current != self.head: + elements.append(str(current.data)) + current = current.next + + return f"CircularDoublyLinkedList: [{', '.join(elements)}] (size: {self.size})" + + +# Демонстрация работы +if __name__ == "__main__": + # Создаем кольцевой список + cdll = CircularDoublyLinkedList() + + print("Создание и заполнение списка:") + cdll.insert_first(1) + cdll.insert_last(2) + cdll.insert_last(3) + cdll.insert_first(0) + print(cdll) + print("Прямой обход:", cdll.traverse_forward()) + print("Обратный обход:", cdll.traverse_backward()) + + print("\nВставка элементов:") + second_node = cdll.find(2) + cdll.insert_after(second_node, 2.5) + cdll.insert_before(second_node, 1.5) + print(cdll) + + print("\nУдаление элементов:") + cdll.remove_first() + cdll.remove_last() + node_to_remove = cdll.find(2) + cdll.remove(node_to_remove) + print(cdll) + + print("\nПоворот списка:") + cdll.insert_last(4) + cdll.insert_last(5) + print("До поворота:", cdll.traverse_forward()) + cdll.rotate_forward(2) + print("После поворота на 2:", cdll.traverse_forward()) + + print("\nПроверка кольцевой структуры:") + first = cdll.get_first() + last = cdll.get_last() + print(f"Первый элемент: {first.data}") + print(f"Последний элемент: {last.data}") + print(f"Следующий за последним: {last.next.data} (головной элемент)") + print(f"Предыдущий перед первым: {first.prev.data} (головной элемент)") +``` + +**Ключевые особенности реализации:** +Выделенный головной элемент: Облегчает операции вставки/удаления и обеспечивает единообразие обработки граничных случаев. + +Двусвязность: Каждый узел имеет ссылки на следующий и предыдущий элементы. + +Кольцевая структура: + +Последний элемент ссылается на первый через головной элемент + +Первый элемент ссылается на последний через головной элемент + +Нет NULL-ссылок + +**Основные операции:** + +Вставка в начало/конец/после/перед + +Удаление первого/последнего/произвольного + +Поиск элемента + +Обход в прямом и обратном направлениях + +Поворот списка + +**Преимущества:** + +Быстрая вставка/удаление с обоих концов (O(1)) + +Удобный обход в обоих направлениях + +Единообразная обработка всех случаев + +Эта реализация эффективно решает типичные задачи для кольцевых структур данных, такие как реализация буферов, планировщиков задач и циклических алгоритмов. + + +### **Контрольные вопросы** + + +#### **1. Определение линейного списка. Отличие от массива** +Линейный список — это структура данных, состоящая из набора элементов данных (узлов), в которой каждый элемент хранит помимо своих данных ссылку (указатель) на следующий (а возможно, и на предыдущий) элемент последовательности. + +Принципиальное отличие от массива: + +Организация в памяти: Элементы массива хранятся в непрерывной области памяти, что позволяет использовать арифметику указателей для мгновенного доступа. Элементы списка могут быть разбросаны по памяти произвольным образом, а связь между ними организована через указатели. + +Доступ к элементам: В массиве доступ по индексу происходит за O(1). В списке для доступа к i-му элементу нужно пройти от начала через i узлов, что занимает O(n). + +Динамичность: Размер массива часто фиксирован (в статических массивах) или требует перевыделения всей памяти при изменении (в динамических). Список является по-настояственно динамической структурой: новые элементы можно добавлять и удалять в любом месте, выделяя и освобождая память только под конкретный узел. + +#### **2. Что такое «узел» (node)?** +Узел — это элементарный блок, из которого строится связный список. Он инкапсулирует данные и связь с соседними узлами. + +Поля узла: + +В односвязном списке: + +data (данные) — полезная информация, которую хранит узел. + +next (следующий) — указатель на следующий узел в списке. Для последнего узла этот указатель равен NULL (или nullptr в C++). + +В двусвязном списке: + +data (данные) — полезная информация. + +next (следующий) — указатель на следующий узел. + +prev (предыдущий) — указатель на предыдущий узел. Для первого узла этот указатель равен NULL. + +#### **3. Что такое «голова» (head) списка?** +Голова (head) — это указатель на первый узел связного списка. Это отправная точка для любого обхода или операции со списком. Если список пуст, head равен NULL. Роль головы критически важна, так как потеря этого указателя означает потерю доступа ко всему списку. + +#### **4. Для чего хранится указатель на «хвост» (tail)?** +Указатель «хвост» (tail) хранит адрес последнего узла списка. Он используется для ускорения операций добавления элемента в конец списка. Без tail для вставки в конец необходимо обойти весь список от головы до конца (O(n)), а с tail эта операция выполняется за O(1). + +#### **5. Основные виды линейных списков (по типу связности)** +Односвязный список (Singly Linked List): Каждый узел содержит указатель только на следующий узел. + +Двусвязный список (Doubly Linked List): Каждый узел содержит указатели как на следующий, так и на предыдущий узел. + +Кольцевой/циклический список (Circular Linked List): + +Односвязный кольцевой: Указатель next последнего узла указывает на первый узел. + +Двусвязный кольцевой: Указатель next последнего узла указывает на первый, а указатель prev первого узла указывает на последний. + +#### **6. Алгоритм добавления в начало односвязного списка (O(1))** +Создать новый узел new_node. + +Записать в его поле data нужное значение. + +Установить поле next нового узла на текущий первый узел (на который указывает head): new_node->next = head. + +Обновить head, чтобы он указывал на новый узел: head = new_node. + +Почему O(1)? Эта операция выполняется за константное время, независимо от длины списка, так как требует фиксированного числа действий и всегда известен адрес головы. + +#### **7. Алгоритм добавления в конец односвязного списка без tail (O(n))** +Создать новый узел new_node. + +Установить его next в NULL (так как он будет последним). + +Если список пуст (head == NULL), то просто сделать head = new_node. + +Иначе, обойти список, начиная с head, до последнего узла (у которого next == NULL). Пусть last — указатель на этот узел. + +Установить last->next = new_node. + +Почему O(n)? В худшем случае (когда в списке много элементов) необходимо пройти от начала до конца всего списка, что требует n шагов. + +#### **8. Удаление узла из односвязного списка** +Причина необходимости указателя на предыдущий элемент: В односвязном списке узел "не знает" о своем предыдущем узле. Если мы просто удалим узел B (на который у нас есть указатель), то узел A, который был до него, будет по-прежнему указывать на несуществующую (удаленную) память. Это приведет к ошибке. Нам нужно изменить поле next узла A, чтобы он указывал на узел C, следующий за B. + +Алгоритм (когда известен предыдущий узел prev): + +Проверить, что узлы не NULL. + +Связать узел prev с узлом, следующим за удаляемым: prev->next = node_to_delete->next. + +Освободить память, занимаемую node_to_delete. + +#### **9. Преимущество двусвязного списка и алгоритм удаления** +Ключевое преимущество: В двусвязном списке узел содержит указатель на предыдущий элемент, поэтому для удаления произвольного узла не нужно искать его предыдущий элемент — он уже известен. + +Алгоритм удаления (дан указатель node на удаляемый узел): + +Если node->next не NULL, то установить node->next->prev = node->prev. + +Если node->prev не NULL, то установить node->prev->next = node->next. + +Важно: Если node является головой (head), нужно обновить head на node->next. + +Важно: Если node является хвостом (tail), нужно обновить tail на node->prev. + +Освободить память, занимаемую node. + +#### **10. Что такое «кольцевой/циклический список»?** +Кольцевой список — это список, в котором последний узел указывает на первый (в односвязном) или первый и последний узлы ссылаются друг на друга (в двусвязном). В таком списке нет NULL-указателей в полях связей next/prev. + +Особенности обработки: + +Обход: Необходимо иметь точку останова (например, начать с головы и остановиться, когда снова вернетесь к голове), иначе попадете в бесконечный цикл. + +Проверка на пустоту: Список пуст, если head == NULL. В непустом списке head->next никогда не NULL. + +#### **11. Таблица асимптотической сложности операций** +| Операция | Односвязный (без 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) | + + +*Удаление из конца в односвязном списке с tail все равно требует O(n), так как для обновления tail нужен указатель на предыдущий элемент (который становится новым хвостом). +**Удаление известного узла в односвязном списке требует O(n) для поиска предыдущего узла, если он не предоставлен. + +#### **12. Объяснение сложности доступа по индексу** +Массив O(1): Адрес i-го элемента вычисляется по формуле адрес_начала + i * размер_элемента. Это одна арифметическая операция. + +Список O(n): Чтобы добраться до i-го элемента, нужно стартовать с головы и i раз последовательно перейти по ссылке next. + +#### **13. Объяснение сложности вставки** +В начало O(1): Адрес головы всегда известен. Операция требует создания одного нового узла и перенаправления двух указателей. + +В конец (без tail) O(n): Адрес последнего элемента неизвестен. Чтобы его найти, нужно пройти через все n узлов списка. + +#### **14. Преимущества и недостатки списка vs массива** + +**Связный список** + +Преимущества + +• Динамический размер + +• Вставка/удаление в начале за O(1) + +• Не требует непрерывной памяти + +Недоститки + +• Медленный доступ по индексу (O(n)) + +• Нет локальности данных (кэш-промахи) + +• Дополнительная память на указатели + +**Массив** + +Преимущества + +• Быстрый доступ по индексу (O(1)) + +• Локальность данных (кэш-эффективность) + +• Меньший overhead на элемент (нет указателей) + +Недостатки + +• Фиксированный размер (или дорогое перевыделение) + +• Вставка/удаление в начале/середине требует сдвига (O(n)) +#### **15. Примеры эффективного использования списков** +Стек (LIFO): Реализуется на основе односвязного списка с операциями push (в начало) и pop (из начала), которые выполняются за O(1). + +Очередь (FIFO): Реализуется на основе односвязного списка с tail (вставка в конец O(1), удаление из начала O(1)) или двусвязного списка. + +Плейлист или история браузера: Двусвязный список идеален для навигации "вперед-назад". + +Системный аллокатор (управление памятью): Списки свободных блоков памяти. + +#### **16. Причина использования двусвязных списков в сложных структурах** +Двусвязные списки позволяют эффективно (за O(1)) удалять произвольный узел и вставлять элементы как до, так и после известного узла. Это критически важно для: + +Deque (двусторонняя очередь): Нужны быстрые вставки и удаления с обоих концов (O(1)). + +Метод цепочек в хеш-таблицах: При частых удалениях двусвязный список делает эту операцию намного эффективнее. + +#### **17. «Фиктивная» нода (Dummy Node)** +Фиктивная нода — это узел, не содержащий полезных данных, который помещается в начало (иногда и в конец) списка. Ее поля data не используются, а next указывает на реальную голову списка. + +Преимущество: Упрощает код, устраняя множество проверок на NULL. Например, операция вставки в начало или удаления первого элемента становится частным случаем вставки/удаления в середину, так как у первого реального элемента всегда есть "предыдущий" узел (фиктивный). + +#### **18. Алгоритм реверсирования односвязного списка** +Алгоритм (итеративный, без дополнительной памяти O(1)): + +Инициализировать три указателя: prev = NULL, current = head, next = NULL. + +Пока current != NULL: +a. Сохранить следующий узел: next = current->next. +b. Развернуть указатель текущего узла: current->next = prev. +c. Сдвинуть указатели на один шаг вперед: prev = current, current = next. + +Обновить голову: head = prev (теперь prev указывает на новую голову — старый хвост). + +#### **19. Алгоритм определения цикла (Floyd's Cycle-Finding)** +Алгоритм "Черепаха и Заяц": + +Создать два указателя, оба указывают на голову списка: slow и fast. + +В цикле, пока fast != NULL и fast->next != NULL: +a. slow делает один шаг: slow = slow->next. +b. fast делает два шага: fast = fast->next->next. +c. Если slow == fast, значит, в списке есть цикл. + +Если fast или fast->next стали NULL, циклов нет. + +#### **20. Поиск среднего элемента за один проход** +Используется модификация алгоритма с двумя указателями: + +Указатели slow и fast стартуют с головы. + +На каждом шаге slow двигается на 1 узел, fast — на 2. + +Когда fast достигнет конца списка (fast == NULL или fast->next == NULL), slow будет указывать на средний элемент. + +#### **21. Проблема удаления узла в односвязном списке без доступа к предыдущему** +Проблема: Невозможно напрямую удалить узел, так как нужно обновить указатель next его предыдущего узла, а он неизвестен. + +Решение (Хитрость, O(1)): + +Скопировать данные (и указатель next) из следующего узла в текущий удаляемый узел. +node->data = node->next->data; +node->next = node->next->next; + +Удалить следующий узел (который теперь является копией исходного). + +Ограничение: Не работает, если удаляемый узел является последним в списке. В этом случае без указателя на предыдущий узел обойтись невозможн + + +```python + +``` + +```python +def summ(a, b): + return a - b + + +assert summ(2, 3) == 5, "Шеф все пропало" +``` + +```python +python -o ghjkl.py +``` diff --git a/labs/lab_04/Untitled.md b/labs/lab_04/Untitled.md new file mode 100644 index 0000000..4578ede --- /dev/null +++ b/labs/lab_04/Untitled.md @@ -0,0 +1,1825 @@ +--- +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 +--- + +# **Стек, Очередь, Дек** + + +## **Цель работы** + + +Изучение структур данных «Стек», «Очередь», «Дек», а также основных операций над ними. + + +Выполнил Асонов Сергей ИУ10-36 + + +## **Краткие теоретические сведения** + + +Линейные списки являются фундаментальной структурой данных, на основе которой эффективно реализуются другие абстрактные типы данных (АТД). В данном разделе рассматривается реализация стека, очереди и дека с использованием как односвязных, так и двусвязных списков. + + +### **Стек (Stack)** +**Стек** — это структура данных, работающая по принципу **LIFO (Last-In, First-Out)** — «последним пришёл — первым ушёл». + +**Основные операции:** + +push(x) — добавление элемента x на вершину стека. +pop() — удаление и возврат элемента с вершины стека. +peek() (или top()) — возврат элемента с вершины стека без удаления. +isEmpty() — проверка стека на пустоту. +**Реализация на односвязном списке:** + +Вершиной стека является **голова** списка. Добавление и удаление элемента выполняются строго с головы. + +push(x): Создать новый узел new_node со значением x. Установить new_node.next = head. Переместить голову на new_node. +pop(): Если список пуст — ошибка. Иначе: сохранить значение head.data, переместить голову на head.next, вернуть сохранённое значение. +**Сложность операций:** push, pop, peek, isEmpty выполняются за O(1). + +### **Очередь (Queue)** + +**Очередь** — это структура данных, работающая по принципу FIFO (First-In, First-Out) — «первым пришёл — первым ушёл». + +**Основные операции:** + +enqueue(x) (или push) — добавление элемента x в конец очереди. +dequeue() (или pop) — удаление и возврат элемента из начала очереди. +peek() — возврат элемента из начала очереди без удаления. +isEmpty() — проверка очереди на пустоту. +**Реализация на односвязном списке с указателем на хвост:** + +**Для эффективного добавления в конец необходимо хранить два указателя:** head (начало очереди) и tail (конец очереди). + +enqueue(x): Создать новый узел new_node. Если очередь пуста, установить head = tail = new_node. Иначе: установить tail.next = new_node, затем переместить tail на new_node. + +dequeue(): Если очередь пуста — ошибка. Иначе: сохранить значение head.data, переместить голову на head.next. + +**Важно:** если после этого голова стала null, то и tail нужно установить в null (очередь опустела). + +**Сложность операций:** enqueue, dequeue, peek, isEmpty выполняются за O(1). + +### **Дек (Deque — Double-Ended Queue)** +**Дек** — это двусторонняя очередь, позволяющая добавлять и удалять элементы как в начале, так и в конце. + +**Основные операции:** + +pushFront(x) — добавление в начало. +pushBack(x) — добавление в конец. +popFront() — удаление из начала. +popBack() — удаление из конца. +peekFront(), peekBack() — просмотр начала/конца. + +**Реализация на двусвязном списке:** + +Для эффективного удаления с конца (popBack) односвязного списка недостаточно, так как у его последнего элемента нет ссылки на предыдущий. Поэтому дек оптимально реализуется на **двусвязном списке** с двумя указателями: head и tail. * **pushFront(x):** Аналогично добавлению в начало двусвязного списка. Установить связи между new_node и head. * **pushBack(x):** Аналогично добавлению в конец двусвязного списка. Установить связи между tail и new_node. * **popBack():** Если дек не пуст, переместить tail на tail.prev, отсечь старый хвост. Корректно обработать случай, когда в деке只有一个 элемент. * **Сложность операций:** Все основные операции (pushFront, pushBack, popFront, popBack) выполняются за O(1). + +**Сравнение реализаций на списках и массивах** + +| Критерий | Реализация на списках | Реализация на массивах (динамических) | +|:---|:---|:---| +| **Сложность операций** | `pushBack`/`popBack`, `pushFront`/`popFront` — O(1) | `pushBack`/`popBack` — O(1)*, `pushFront`/`popFront` — O(n) | +| **Память** | Больше: память на хранение указателей. | Меньше: память только на данные (но может быть избыточное выделение). | +| **Локализация данных** | Плохая: узлы в случайных местах памяти (кэш-промахи). | Отличная: данные лежат рядом (кэш-дружественность). | +| **Динамичность** | Истинно динамическая: каждый элемент выделяется отдельно. | Динамическая с резервированием: требует периодического дорогого копирования (reallocation). | + + +**Вывод:** Реализация стека, очереди и дека на связных списках является более универсальной и надёжной с точки зрения сложности операций, так как не требует операций копирования и расширения массива. Она гарантирует **константное время** выполнения для всех ключевых операций. Однако она проигрывает в потреблении памяти и скорости доступа из-за плохой локализации данных. Реализация на массивах (особенно для стека и очереди с кольцевой организацией) часто быстрее на практике для задач, где размер данных大致 известен, благодаря кэш-дружественности. + + +### **Задание 1** + +```python +class BaseStack: + """Базовый класс для стека""" + def push(self, item): + raise NotImplementedError("Метод push должен быть реализован") + + def pop(self): + raise NotImplementedError("Метод pop должен быть реализован") + + def peek(self): + raise NotImplementedError("Метод peek должен быть реализован") + + def is_empty(self): + raise NotImplementedError("Метод is_empty должен быть реализован") + + def size(self): + raise NotImplementedError("Метод size должен быть реализован") + + +class BaseQueue: + """Базовый класс для очереди""" + def enqueue(self, item): + raise NotImplementedError("Метод enqueue должен быть реализован") + + def dequeue(self): + raise NotImplementedError("Метод dequeue должен быть реализован") + + def front(self): + raise NotImplementedError("Метод front должен быть реализован") + + def is_empty(self): + raise NotImplementedError("Метод is_empty должен быть реализован") + + def size(self): + raise NotImplementedError("Метод size должен быть реализован") + + +class BaseDeque: + """Базовый класс для дека""" + def add_front(self, item): + raise NotImplementedError("Метод add_front должен быть реализован") + + def add_rear(self, item): + raise NotImplementedError("Метод add_rear должен быть реализован") + + def remove_front(self): + raise NotImplementedError("Метод remove_front должен быть реализован") + + def remove_rear(self): + raise NotImplementedError("Метод remove_rear должен быть реализован") + + def peek_front(self): + raise NotImplementedError("Метод peek_front должен быть реализован") + + def peek_rear(self): + raise NotImplementedError("Метод peek_rear должен быть реализован") + + def is_empty(self): + raise NotImplementedError("Метод is_empty должен быть реализован") + + def size(self): + raise NotImplementedError("Метод size должен быть реализован") + + +""" 1. Стек на основе массива""" +class ArrayStack(BaseStack): + def __init__(self): + 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. Стек на основе связного списка""" +class Node: + def __init__(self, data): + self.data = data + self.next = None + + +class LinkedListStack(BaseStack): + def __init__(self): + self._top = None + self._size = 0 + + def push(self, item): + """Добавляет элемент на вершину стека""" + new_node = Node(item) + new_node.next = self._top + self._top = new_node + self._size += 1 + + def pop(self): + """Удаляет и возвращает элемент с вершины стека""" + if self.is_empty(): + raise IndexError("Стек пуст") + + data = self._top.data + self._top = self._top.next + self._size -= 1 + return data + + def peek(self): + """Возвращает элемент с вершины стека без удаления""" + if self.is_empty(): + raise IndexError("Стек пуст") + return self._top.data + + def is_empty(self): + """Проверяет, пуст ли стек""" + return self._top is None + + def size(self): + """Возвращает количество элементов в стеке""" + return self._size + + def __str__(self): + items = [] + current = self._top + while current: + items.append(current.data) + current = current.next + return f"LinkedListStack({items})" + + +""" 3. Очередь на основе массива """ +class ArrayQueue(BaseQueue): + def __init__(self): + 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. Очередь на основе связного списка""" +class LinkedListQueue(BaseQueue): + def __init__(self): + self._front = None + self._rear = None + self._size = 0 + + def enqueue(self, item): + """Добавляет элемент в конец очереди""" + new_node = Node(item) + + if self.is_empty(): + self._front = self._rear = new_node + else: + self._rear.next = new_node + self._rear = new_node + + self._size += 1 + + def dequeue(self): + """Удаляет и возвращает элемент из начала очереди""" + if self.is_empty(): + raise IndexError("Очередь пуста") + + data = self._front.data + self._front = self._front.next + + if self._front is None: + self._rear = None + + self._size -= 1 + return data + + def front(self): + """Возвращает элемент из начала очереди без удаления""" + if self.is_empty(): + raise IndexError("Очередь пуста") + return self._front.data + + def is_empty(self): + """Проверяет, пуста ли очередь""" + return self._front is None + + def size(self): + """Возвращает количество элементов в очереди""" + return self._size + + def __str__(self): + items = [] + current = self._front + while current: + items.append(current.data) + current = current.next + return f"LinkedListQueue({items})" + + +""" 5. Дек на основе массива""" +class ArrayDeque(BaseDeque): + def __init__(self): + self._items = [] + + def add_front(self, item): + """Добавляет элемент в начало дека""" + self._items.insert(0, item) + + def add_rear(self, item): + """Добавляет элемент в конец дека""" + self._items.append(item) + + def remove_front(self): + """Удаляет и возвращает элемент из начала дека""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self._items.pop(0) + + def remove_rear(self): + """Удаляет и возвращает элемент из конца дека""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self._items.pop() + + def peek_front(self): + """Возвращает элемент из начала дека без удаления""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self._items[0] + + def peek_rear(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. Дек на основе связного списка """ +class LinkedListDeque(BaseDeque): + def __init__(self): + self._front = None + self._rear = None + self._size = 0 + + def add_front(self, item): + """Добавляет элемент в начало дека""" + new_node = Node(item) + + if self.is_empty(): + self._front = self._rear = new_node + else: + new_node.next = self._front + self._front = new_node + + self._size += 1 + + def add_rear(self, item): + """Добавляет элемент в конец дека""" + new_node = Node(item) + + if self.is_empty(): + self._front = self._rear = new_node + else: + self._rear.next = new_node + self._rear = new_node + + self._size += 1 + + def remove_front(self): + """Удаляет и возвращает элемент из начала дека""" + if self.is_empty(): + raise IndexError("Дек пуст") + + data = self._front.data + self._front = self._front.next + + if self._front is None: + self._rear = None + + self._size -= 1 + return data + + def remove_rear(self): + """Удаляет и возвращает элемент из конца дека""" + if self.is_empty(): + raise IndexError("Дек пуст") + + """Если в деке только один элемент""" + if self._front == self._rear: + data = self._front.data + self._front = self._rear = None + self._size -= 1 + return data + + """Находим предпоследний элемент""" + current = self._front + while current.next != self._rear: + current = current.next + + data = self._rear.data + self._rear = current + self._rear.next = None + self._size -= 1 + return data + + def peek_front(self): + """Возвращает элемент из начала дека без удаления""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self._front.data + + def peek_rear(self): + """Возвращает элемент из конца дека без удаления""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self._rear.data + + def is_empty(self): + """Проверяет, пуст ли дек""" + return self._front is None + + def size(self): + """Возвращает количество элементов в деке""" + return self._size + + def __str__(self): + items = [] + current = self._front + while current: + items.append(current.data) + current = current.next + return f"LinkedListDeque({items})" + + + +if __name__ == "__main__": + print("=== ТЕСТИРОВАНИЕ СТРУКТУР ДАННЫХ ===\n") + + """1. Стек на массиве""" + print("1. Тестирование ArrayStack:") + stack1 = ArrayStack() + + """Тест добавления""" + stack1.push(1) + stack1.push(2) + stack1.push(3) + assert str(stack1) == "ArrayStack([1, 2, 3])", "Ошибка добавления в стек" + + """Тест просмотра""" + assert stack1.peek() == 3, "Ошибка peek()" + + """ Тест удаления""" + assert stack1.pop() == 3, "Ошибка pop()" + assert stack1.size() == 2, "Ошибка размера после pop()" + + """Тест пустоты""" + assert not stack1.is_empty(), "Ошибка is_empty()" + + """Очистка и проверка пустого стека""" + stack1.pop() + stack1.pop() + assert stack1.is_empty(), "Стек должен быть пустым" + assert stack1.size() == 0, "Размер должен быть 0" + + print(" ✓ Все тесты ArrayStack пройдены\n") + + """ 2. Стек на связном списке """ + print("2. Тестирование LinkedListStack:") + stack2 = LinkedListStack() + + """Тест добавления""" + stack2.push('a') + stack2.push('b') + stack2.push('c') + assert str(stack2) == "LinkedListStack(['c', 'b', 'a'])", "Ошибка добавления в связный стек" + + """ Тест просмотра""" + assert stack2.peek() == 'c', "Ошибка peek() в связном стеке" + + """Тест удаления """ + assert stack2.pop() == 'c', "Ошибка pop() в связном стеке" + assert stack2.size() == 2, "Ошибка размера после pop()" + + print(" Все тесты LinkedListStack пройдены\n") + + """ 3. Очередь на массиве""" + print("3. Тестирование ArrayQueue:") + queue1 = ArrayQueue() + + """Тест добавления""" + queue1.enqueue(10) + queue1.enqueue(20) + queue1.enqueue(30) + assert str(queue1) == "ArrayQueue([10, 20, 30])", "Ошибка добавления в очередь" + + """Тест просмотра""" + assert queue1.front() == 10, "Ошибка front()" + + """Тест удаления """ + assert queue1.dequeue() == 10, "Ошибка dequeue()" + assert queue1.size() == 2, "Ошибка размера после dequeue()" + assert queue1.front() == 20, "Ошибка front() после dequeue()" + + print(" Все тесты ArrayQueue пройдены\n") + + """4. Очередь на связном списке""" + print("4. Тестирование LinkedListQueue:") + queue2 = LinkedListQueue() + + """Тест добавления""" + queue2.enqueue('x') + queue2.enqueue('y') + queue2.enqueue('z') + assert str(queue2) == "LinkedListQueue(['x', 'y', 'z'])", "Ошибка добавления в связную очередь" + + """Тест просмотра""" + assert queue2.front() == 'x', "Ошибка front() в связной очереди" + + """Tест удаления""" + assert queue2.dequeue() == 'x', "Ошибка dequeue() в связной очереди" + assert queue2.size() == 2, "Ошибка размера после dequeue()" + assert queue2.front() == 'y', "Ошибка front() после dequeue()" + + print(" Все тесты LinkedListQueue пройдены\n") + + """5. Дек на массиве""" + print("5. Тестирование ArrayDeque:") + deque1 = ArrayDeque() + + """Тест добавления спереди и сзади""" + deque1.add_front(1) + deque1.add_rear(2) + deque1.add_front(0) + deque1.add_rear(3) + assert str(deque1) == "ArrayDeque([0, 1, 2, 3])", "Ошибка добавления в дек" + + """Тест просмотра""" + assert deque1.peek_front() == 0, "Ошибка peek_front()" + assert deque1.peek_rear() == 3, "Ошибка peek_rear()" + + """Тест удаления""" + assert deque1.remove_front() == 0, "Ошибка remove_front()" + assert deque1.remove_rear() == 3, "Ошибка remove_rear()" + assert deque1.size() == 2, "Ошибка размера после удалений" + assert str(deque1) == "ArrayDeque([1, 2])", "Ошибка состояния дека после удалений" + + print(" Все тесты ArrayDeque пройдены\n") + + """6. Дек на связном списке""" + print("6. Тестирование LinkedListDeque:") + deque2 = LinkedListDeque() + + """Тест добавления""" + deque2.add_front(100) + deque2.add_rear(200) + deque2.add_front(50) + deque2.add_rear(300) + assert str(deque2) == "LinkedListDeque([50, 100, 200, 300])", "Ошибка добавления в связный дек" + + """ Тест просмотра""" + assert deque2.peek_front() == 50, "Ошибка peek_front() в связном деке" + assert deque2.peek_rear() == 300, "Ошибка peek_rear() в связном деке" + + """Тест удаления""" + assert deque2.remove_front() == 50, "Ошибка remove_front() в связном деке" + assert deque2.remove_rear() == 300, "Ошибка remove_rear() в связном деке" + assert deque2.size() == 2, "Ошибка размера после удалений" + assert str(deque2) == "LinkedListDeque([100, 200])", "Ошибка состояния связного дека после удалений" + + print(" Все тесты LinkedListDeque пройдены\n") + + """ 7. Тестирование исключений""" + print("7. Тестирование исключений:") + + """Тест пустого стека""" + empty_stack = ArrayStack() + try: + empty_stack.pop() + assert False, "Должно быть исключение при pop() пустого стека" + except IndexError: + pass #Ожидаемое поведение + + """Тест пустой очереди""" + empty_queue = ArrayQueue() + try: + empty_queue.dequeue() + assert False, "Должно быть исключение при dequeue() пустой очереди" + except IndexError: + pass #Ожидаемое поведение + + """Тест пустого дека""" + empty_deque = ArrayDeque() + try: + empty_deque.remove_front() + assert False, "Должно быть исключение при remove_front() пустого дека" + except IndexError: + pass #Ожидаемое поведение + + print(" Все тесты исключений пройдены\n") + + print("Все структуры данных работают корректно.") +``` + +```python +class BaseStack: + """Базовый класс для стека""" + def push(self, item): + raise NotImplementedError("Метод push должен быть реализован") + + def pop(self): + raise NotImplementedError("Метод pop должен быть реализован") + + def peek(self): + raise NotImplementedError("Метод peek должен быть реализован") + + def is_empty(self): + raise NotImplementedError("Метод is_empty должен быть реализован") + + def size(self): + raise NotImplementedError("Метод size должен быть реализован") + + +class BaseQueue: + """Базовый класс для очереди""" + def enqueue(self, item): + raise NotImplementedError("Метод enqueue должен быть реализован") + + def dequeue(self): + raise NotImplementedError("Метод dequeue должен быть реализован") + + def front(self): + raise NotImplementedError("Метод front должен быть реализован") + + def is_empty(self): + raise NotImplementedError("Метод is_empty должен быть реализован") + + def size(self): + raise NotImplementedError("Метод size должен быть реализован") + + +class BaseDeque: + """Базовый класс для дека""" + def add_front(self, item): + raise NotImplementedError("Метод add_front должен быть реализован") + + def add_rear(self, item): + raise NotImplementedError("Метод add_rear должен быть реализован") + + def remove_front(self): + raise NotImplementedError("Метод remove_front должен быть реализован") + + def remove_rear(self): + raise NotImplementedError("Метод remove_rear должен быть реализован") + + def peek_front(self): + raise NotImplementedError("Метод peek_front должен быть реализован") + + def peek_rear(self): + raise NotImplementedError("Метод peek_rear должен быть реализован") + + def is_empty(self): + raise NotImplementedError("Метод is_empty должен быть реализован") + + def size(self): + raise NotImplementedError("Метод size должен быть реализован") + + +""" 1. Стек на основе массива""" +class ArrayStack(BaseStack): + def __init__(self): + 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. Стек на основе связного списка""" +class Node: + def __init__(self, data): + self.data = data + self.next = None + + +class LinkedListStack(BaseStack): + def __init__(self): + self._top = None + self._size = 0 + + def push(self, item): + """Добавляет элемент на вершину стека""" + new_node = Node(item) + new_node.next = self._top + self._top = new_node + self._size += 1 + + def pop(self): + """Удаляет и возвращает элемент с вершины стека""" + if self.is_empty(): + raise IndexError("Стек пуст") + + data = self._top.data + self._top = self._top.next + self._size -= 1 + return data + + def peek(self): + """Возвращает элемент с вершины стека без удаления""" + if self.is_empty(): + raise IndexError("Стек пуст") + return self._top.data + + def is_empty(self): + """Проверяет, пуст ли стек""" + return self._top is None + + def size(self): + """Возвращает количество элементов в стеке""" + return self._size + + def __str__(self): + items = [] + current = self._top + while current: + items.append(current.data) + current = current.next + return f"LinkedListStack({items})" + + +""" 3. Очередь на основе массива """ +class ArrayQueue(BaseQueue): + def __init__(self): + 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. Очередь на основе связного списка""" +class LinkedListQueue(BaseQueue): + def __init__(self): + self._front = None + self._rear = None + self._size = 0 + + def enqueue(self, item): + """Добавляет элемент в конец очереди""" + new_node = Node(item) + + if self.is_empty(): + self._front = self._rear = new_node + else: + self._rear.next = new_node + self._rear = new_node + + self._size += 1 + + def dequeue(self): + """Удаляет и возвращает элемент из начала очереди""" + if self.is_empty(): + raise IndexError("Очередь пуста") + + data = self._front.data + self._front = self._front.next + + if self._front is None: + self._rear = None + + self._size -= 1 + return data + + def front(self): + """Возвращает элемент из начала очереди без удаления""" + if self.is_empty(): + raise IndexError("Очередь пуста") + return self._front.data + + def is_empty(self): + """Проверяет, пуста ли очередь""" + return self._front is None + + def size(self): + """Возвращает количество элементов в очереди""" + return self._size + + def __str__(self): + items = [] + current = self._front + while current: + items.append(current.data) + current = current.next + return f"LinkedListQueue({items})" + + +""" 5. Дек на основе массива""" +class ArrayDeque(BaseDeque): + def __init__(self): + self._items = [] + + def add_front(self, item): + """Добавляет элемент в начало дека""" + self._items.insert(0, item) + + def add_rear(self, item): + """Добавляет элемент в конец дека""" + self._items.append(item) + + def remove_front(self): + """Удаляет и возвращает элемент из начала дека""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self._items.pop(0) + + def remove_rear(self): + """Удаляет и возвращает элемент из конца дека""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self._items.pop() + + def peek_front(self): + """Возвращает элемент из начала дека без удаления""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self._items[0] + + def peek_rear(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. Дек на основе связного списка """ +class LinkedListDeque(BaseDeque): + def __init__(self): + self._front = None + self._rear = None + self._size = 0 + + def add_front(self, item): + """Добавляет элемент в начало дека""" + new_node = Node(item) + + if self.is_empty(): + self._front = self._rear = new_node + else: + new_node.next = self._front + self._front = new_node + + self._size += 1 + + def add_rear(self, item): + """Добавляет элемент в конец дека""" + new_node = Node(item) + + if self.is_empty(): + self._front = self._rear = new_node + else: + self._rear.next = new_node + self._rear = new_node + + self._size += 1 + + def remove_front(self): + """Удаляет и возвращает элемент из начала дека""" + if self.is_empty(): + raise IndexError("Дек пуст") + + data = self._front.data + self._front = self._front.next + + if self._front is None: + self._rear = None + + self._size -= 1 + return data + + def remove_rear(self): + """Удаляет и возвращает элемент из конца дека""" + if self.is_empty(): + raise IndexError("Дек пуст") + + """Если в деке только один элемент""" + if self._front == self._rear: + data = self._front.data + self._front = self._rear = None + self._size -= 1 + return data + + """Находим предпоследний элемент""" + current = self._front + while current.next != self._rear: + current = current.next + + data = self._rear.data + self._rear = current + self._rear.next = None + self._size -= 1 + return data + + def peek_front(self): + """Возвращает элемент из начала дека без удаления""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self._front.data + + def peek_rear(self): + """Возвращает элемент из конца дека без удаления""" + if self.is_empty(): + raise IndexError("Дек пуст") + return self._rear.data + + def is_empty(self): + """Проверяет, пуст ли дек""" + return self._front is None + + def size(self): + """Возвращает количество элементов в деке""" + return self._size + + def __str__(self): + items = [] + current = self._front + while current: + items.append(current.data) + current = current.next + return f"LinkedListDeque({items})" + + + +if __name__ == "__main__": + print("=== ТЕСТИРОВАНИЕ СТРУКТУР ДАННЫХ ===\n") + + """1. Стек на массиве""" + print("1. Тестирование ArrayStack:") + stack1 = ArrayStack() + + """Тест добавления""" + stack1.push(1) + stack1.push(2) + stack1.push(3) + assert str(stack1) == "ArrayStack([1, 2, 3])", "Ошибка добавления в стек" + + """Тест просмотра""" + assert stack1.peek() == 3, "Ошибка peek()" + + """ Тест удаления""" + assert stack1.pop() == 3, "Ошибка pop()" + assert stack1.size() == 2, "Ошибка размера после pop()" + + """Тест пустоты""" + assert not stack1.is_empty(), "Ошибка is_empty()" + + """Очистка и проверка пустого стека""" + stack1.pop() + stack1.pop() + assert stack1.is_empty(), "Стек должен быть пустым" + assert stack1.size() == 0, "Размер должен быть 0" + + print(" ✓ Все тесты ArrayStack пройдены\n") + + """ 2. Стек на связном списке """ + print("2. Тестирование LinkedListStack:") + stack2 = LinkedListStack() + + """Тест добавления""" + stack2.push('a') + stack2.push('b') + stack2.push('c') + assert str(stack2) == "LinkedListStack(['c', 'b', 'a'])", "Ошибка добавления в связный стек" + + """ Тест просмотра""" + assert stack2.peek() == 'c', "Ошибка peek() в связном стеке" + + """Тест удаления """ + assert stack2.pop() == 'c', "Ошибка pop() в связном стеке" + assert stack2.size() == 2, "Ошибка размера после pop()" + + print(" Все тесты LinkedListStack пройдены\n") + + """ 3. Очередь на массиве""" + print("3. Тестирование ArrayQueue:") + queue1 = ArrayQueue() + + """Тест добавления""" + queue1.enqueue(10) + queue1.enqueue(20) + queue1.enqueue(30) + assert str(queue1) == "ArrayQueue([10, 20, 30])", "Ошибка добавления в очередь" + + """Тест просмотра""" + assert queue1.front() == 10, "Ошибка front()" + + """Тест удаления """ + assert queue1.dequeue() == 10, "Ошибка dequeue()" + assert queue1.size() == 2, "Ошибка размера после dequeue()" + assert queue1.front() == 20, "Ошибка front() после dequeue()" + + print(" Все тесты ArrayQueue пройдены\n") + + """4. Очередь на связном списке""" + print("4. Тестирование LinkedListQueue:") + queue2 = LinkedListQueue() + + """Тест добавления""" + queue2.enqueue('x') + queue2.enqueue('y') + queue2.enqueue('z') + assert str(queue2) == "LinkedListQueue(['x', 'y', 'z'])", "Ошибка добавления в связную очередь" + + """Тест просмотра""" + assert queue2.front() == 'x', "Ошибка front() в связной очереди" + + """Tест удаления""" + assert queue2.dequeue() == 'x', "Ошибка dequeue() в связной очереди" + assert queue2.size() == 2, "Ошибка размера после dequeue()" + assert queue2.front() == 'y', "Ошибка front() после dequeue()" + + print(" Все тесты LinkedListQueue пройдены\n") + + """5. Дек на массиве""" + print("5. Тестирование ArrayDeque:") + deque1 = ArrayDeque() + + """Тест добавления спереди и сзади""" + deque1.add_front(1) + deque1.add_rear(2) + deque1.add_front(0) + deque1.add_rear(3) + assert str(deque1) == "ArrayDeque([0, 1, 2, 3])", "Ошибка добавления в дек" + + """Тест просмотра""" + assert deque1.peek_front() == 0, "Ошибка peek_front()" + assert deque1.peek_rear() == 3, "Ошибка peek_rear()" + + """Тест удаления""" + assert deque1.remove_front() == 0, "Ошибка remove_front()" + assert deque1.remove_rear() == 3, "Ошибка remove_rear()" + assert deque1.size() == 2, "Ошибка размера после удалений" + assert str(deque1) == "ArrayDeque([1, 2])", "Ошибка состояния дека после удалений" + + print(" Все тесты ArrayDeque пройдены\n") + + """6. Дек на связном списке""" + print("6. Тестирование LinkedListDeque:") + deque2 = LinkedListDeque() + + """Тест добавления""" + deque2.add_front(100) + deque2.add_rear(200) + deque2.add_front(50) + deque2.add_rear(300) + assert str(deque2) == "LinkedListDeque([50, 100, 200, 300])", "Ошибка добавления в связный дек" + + """ Тест просмотра""" + assert deque2.peek_front() == 50, "Ошибка peek_front() в связном деке" + assert deque2.peek_rear() == 300, "Ошибка peek_rear() в связном деке" + + """Тест удаления""" + assert deque2.remove_front() == 50, "Ошибка remove_front() в связном деке" + assert deque2.remove_rear() == 300, "Ошибка remove_rear() в связном деке" + assert deque2.size() == 2, "Ошибка размера после удалений" + assert str(deque2) == "LinkedListDeque([100, 200])", "Ошибка состояния связного дека после удалений" + + print(" Все тесты LinkedListDeque пройдены\n") + + """ 7. Тестирование исключений""" + print("7. Тестирование исключений:") + + """Тест пустого стека""" + empty_stack = ArrayStack() + try: + empty_stack.pop() + assert False, "Должно быть исключение при pop() пустого стека" + except IndexError: + pass #Ожидаемое поведение + + """Тест пустой очереди""" + empty_queue = ArrayQueue() + try: + empty_queue.dequeue() + assert False, "Должно быть исключение при dequeue() пустой очереди" + except IndexError: + pass #Ожидаемое поведение + + """Тест пустого дека""" + empty_deque = ArrayDeque() + try: + empty_deque.remove_front() + assert False, "Должно быть исключение при remove_front() пустого дека" + except IndexError: + pass #Ожидаемое поведение + + print(" Все тесты исключений пройдены\n") + + print("Все структуры данных работают корректно.") +``` + +### Сводная таблица временной сложности операций + +| Структура | Добавление | Удаление | Просмотр | Пустота | Размер | +|-----------|------------|----------|----------|---------|--------| +| **ArrayStack** | O(1)* | O(1) | O(1) | O(1) | O(1) | +| **LinkedListStack** | O(1) | O(1) | O(1) | O(1) | O(1) | +| **ArrayQueue** | O(1)* | O(n) | O(1) | O(1) | O(1) | +| **LinkedListQueue** | O(1) | O(1) | O(1) | O(1) | O(1) | +| **ArrayDeque** | O(n)/O(1)* | O(n)/O(1) | O(1) | O(1) | O(1) | +| **LinkedListDeque** | O(1)/O(1) | O(1)/O(n) | O(1) | O(1) | O(1) | + +### Условные обозначения: + +- **\*** - амортизированная сложность +- **Для деков** - сложность указана в формате **спереди/сзади** + - ArrayDeque: add_front/remove_front **O(n)**, add_rear/remove_rear **O(1)** + - LinkedListDeque: add_front/remove_front **O(1)**, add_rear **O(1)**, remove_rear **O(n)** + +### Ключевые выводы: + +1. **Стеки** эффективны в обеих реализациях +2. **Очереди** лучше реализовывать на связных списках +3. **Деки** имеют компромиссы в зависимости от реализации +4. Все структуры имеют O(1) для проверки пустоты и размера + + +### **Задание 2** + +```python +def check_brackets(bracket_string): + """ + Проверяет своевременность закрытия скобок в строке символов + """ + stack = [] + bracket_pairs = {')': '(', ']': '[', '}': '{'} + + for char in bracket_string: + if char in '([{': + stack.append(char) + elif char in ')]}': + if not stack: + return False + if stack[-1] == bracket_pairs[char]: + stack.pop() + else: + return False + + return len(stack) == 0 + + +def run_tests(): + """Запуск 5 тестов""" + tests = [ + + ("()", True, "Простые круглые скобки"), + + ("()[]{}", True, "Разные типы скобок"), + + ("([{}])", True, "Вложенные скобки"), + + ("(()", False, "Незакрытая круглая скобка"), + + ("([)]", False, "Неправильный порядок скобок") + ] + + print("ТЕСТИРОВАНИЕ ПРОВЕРКИ СКОБОК") + print("=" * 40) + + for i, (test_string, expected, description) in enumerate(tests, 1): + result = check_brackets(test_string) + status = " ПРОЙДЕН" if result == expected else " ОШИБКА" + + print(f"Тест {i}: {description}") + print(f"Строка: '{test_string}'") + print(f"Результат: {result} (ожидалось: {expected})") + print(f"Статус: {status}") + print("-" * 40) + + +def main(): + run_tests() + + +if __name__ == "__main__": + main() +``` + +### Анализ сложности алгоритма проверки скобок + +### Детализация операций + +| Операция | Сложность | Примечания | +|----------|-----------|------------| +| Создание stack | O(1) | Инициализация пустого списка | +| Создание bracket_pairs | O(1) | Словарь фиксированного размера | +| Цикл for | O(n) | Проход по всем n символам | +| Проверка `char in '([{'` | O(1) | Поиск в строке длины 3 | +| stack.append() | O(1)* | Амортизированная константа | +| stack.pop() | O(1) | Удаление с конца | +| stack[-1] | O(1) | Доступ к последнему элементу | +| bracket_pairs[char] | O(1) | Поиск в словаре | +| len(stack) == 0 | O(1) | Проверка длины списка | + +### Пространственная сложность: O(n) + +- **В худшем случае** (только открывающие скобки): стек может содержать до **n** элементов +- **Словарь bracket_pairs**: занимает O(1) памяти (фиксированный размер) + +### Анализ случаев + +### Худший случай +**Строка**: `"((((((((("` +- **Время**: O(n) +- **Память**: O(n) + +### Лучший случай +**Строка**: `")"` +- **Время**: O(1) - досрочный выход +- **Память**: O(1) + +### Средний случай +**Сбалансированная строка скобок** +- **Время**: O(n) +- **Память**: O(n/2) ≈ O(n) + +### Итоговая таблица сложности + +| Метрика | Сложность | Обоснование | +|---------|-----------|-------------| +| **Временная** | O(n) | Один проход по строке длиной n | +| **Пространственная** | O(n) | В худшем случае стек размером n | +| **Лучший случай** | O(1) | Некорректная скобка в начале | +| **Худший случай** | O(n) | Все скобки открывающие | + +### Вывод + +Алгоритм оптимален для данной задачи, так как требует только одного прохода по строке и минимальной дополнительной памяти. + +*Примечание: * - амортизированная константная сложность* + + +### **Задание 3** + +```python +def evaluate_postfix(expression): + """ + Вычисляет значение выражения в обратной польской записи + """ + stack = [] + operators = { + '+': lambda x, y: x + y, + '-': lambda x, y: x - y, + '*': lambda x, y: x * y, + '/': lambda x, y: x / y + } + + for char in expression.split(): + if char.isdigit(): + stack.append(int(char)) + elif char in operators: + if len(stack) < 2: + raise ValueError(f"Недостаточно операндов для операции '{char}'") + + operand2 = stack.pop() + operand1 = stack.pop() + result = operators[char](operand1, operand2) + stack.append(result) + else: + raise ValueError(f"Недопустимый символ: '{char}'") + + if len(stack) != 1: + raise ValueError(f"Некорректное выражение. В стеке осталось {len(stack)} элементов: {stack}") + + return stack[0] + + +def run_five_tests(): + """Запуск 5 тестов """ + tests = [ + + ("2 3 +", 5, "Простое сложение: 2 + 3"), + + ("3 4 2 * +", 11, "Умножение и сложение: 3 + 4*2"), + + ("5 1 2 + 4 * +", 17, "Сложное выражение: 5 + (1+2)*4"), + + ("8 4 - 2 /", 2, "Вычитание и деление: (8-4)/2"), + + ("9 3 / 1 1 + *", 6, "Множественные операции: (9/3)*(1+1)") + ] + + print("5 ТЕСТОВ ВЫЧИСЛЕНИЕ ОБРАТНОЙ ПОЛЬСКОЙ ЗАПИСИ") + print("=" * 60) + + for i, (expression, expected, description) in enumerate(tests, 1): + result = evaluate_postfix(expression) + assert result == expected, f"Тест {i} не пройден: {description}. Ожидалось {expected}, получено {result}" + print(f"Тест {i} ПРОЙДЕН: {description}") + print(f" Выражение: '{expression}' = {result}") + + + +def main(): + """Основная функция""" + run_five_tests() + + +if __name__ == "__main__": + main() +``` + +### Анализ сложности алгоритма вычисления обратной польской записи + +### Детализация операций + +| Операция | Сложность | Примечания | +|----------|-----------|------------| +| `expression.split()` | O(m) | m - длина строки выражения | +| Цикл for | O(n) | Проход по n токенам | +| `char.isdigit()` | O(1) | Проверка строки на цифры | +| `stack.append()` | O(1)* | Амортизированная константа | +| `stack.pop()` | O(1) | Удаление с конца | +| `char in operators` | O(1) | Поиск в словаре из 4 элементов | +| `operators[char]()` | O(1) | Вызов лямбда-функции | +| `len(stack)` | O(1) | Проверка длины списка | +| `int(char)` | O(1) | Конвертация строки в число | + +### Пространственная сложность: O(n) + +- **Стек**: в худшем случае может содержать до n/2 элементов ≈ O(n) +- **Словарь operators**: O(1) - фиксированный размер +- **Временные переменные**: O(1) + +### Анализ случаев + +### Худший случай +**Выражение**: `"1 2 3 4 5 6 7 8 9 10 + + + + + + + + +"` +- **Время**: O(n) - все операции в конце +- **Память**: O(n) - стек растет линейно + +### Лучший случай +**Выражение**: `"2 3 +"` +- **Время**: O(n) - минимальное выражение +- **Память**: O(1) - мало элементов в стеке + +### Средний случай +**Сбалансированное выражение** +- **Время**: O(n) +- **Память**: O(n/2) ≈ O(n) + +### Итоговая таблица сложности + +| Метрика | Сложность | Обоснование | +|---------|-----------|-------------| +| **Временная** | O(n) | Один проход по n токенам | +| **Пространственная** | O(n) | Стек в худшем случае O(n) | +| **Лучший случай** | O(n) | Минимальное выражение | +| **Худший случай** | O(n) | Все операнды сначала, операции потом | +| **Split операция** | O(m) | m - длина исходной строки | + +### Вывод + +Алгоритм оптимален для вычисления обратной польской записи. Временная сложность линейна относительно количества токенов, что является наилучшим возможным результатом для данной задачи. + +*Примечание: n - количество токенов в выражении после split(), m - длина исходной строки выражения* + + +### **Задание 4** + +```python +def infix_to_postfix(expression): + """ + Переводит математическое выражение из инфиксной в постфиксную форму + """ + precedence = {'+': 1, '-': 1, '*': 2, '/': 2} + stack = [] + output = [] + + for token in expression: + if token.isalnum(): + output.append(token) + elif token == '(': + stack.append(token) + elif token == ')': + while stack and stack[-1] != '(': + output.append(stack.pop()) + stack.pop() + else: + while (stack and stack[-1] != '(' and + precedence.get(token, 0) <= precedence.get(stack[-1], 0)): + output.append(stack.pop()) + stack.append(token) + + while stack: + output.append(stack.pop()) + + return ' '.join(output) + + +def run_five_tests(): + """Запуск 5 тестов перевода из инфиксной в постфиксную форму """ + print("5 ТЕСТОВ ПЕРЕВОДА ИНФИКС → ПОСТФИКС (") + print("=" * 50) + + try: + result = infix_to_postfix("a+b") + assert result == "a b +", f"Ожидалось 'a b +', получено '{result}'" + print(" Тест 1 ПРОЙДЕН: Простое сложение") + print(f" a+b → {result}") + except AssertionError as e: + print(f" Тест 1 ОШИБКА: {e}") + + try: + result = infix_to_postfix("a+b*c") + assert result == "a b c * +", f"Ожидалось 'a b c * +', получено '{result}'" + print(" Тест 2 ПРОЙДЕН: Приоритет умножения") + print(f" a+b*c → {result}") + except AssertionError as e: + print(f" Тест 2 ОШИБКА: {e}") + + try: + result = infix_to_postfix("(a+b)*c") + assert result == "a b + c *", f"Ожидалось 'a b + c *', получено '{result}'" + print(" Тест 3 ПРОЙДЕН: Сложение в скобках с умножением") + print(f" (a+b)*c → {result}") + except AssertionError as e: + print(f" Тест 3 ОШИБКА: {e}") + + try: + result = infix_to_postfix("a+(b+c)*d") + assert result == "a b c + d * +", f"Ожидалось 'a b c + d * +', получено '{result}'" + print(" Тест 4 ПРОЙДЕН: Сложное выражение со скобками") + print(f" a+(b+c)*d → {result}") + except AssertionError as e: + print(f" Тест 4 ОШИБКА: {e}") + + try: + result = infix_to_postfix("a*b+c*d") + assert result == "a b * c d * +", f"Ожидалось 'a b * c d * +', получено '{result}'" + print(" Тест 5 ПРОЙДЕН: Множественное умножение и сложение") + print(f" a*b+c*d → {result}") + except AssertionError as e: + print(f" Тест 5 ОШИБКА: {e}") + + print("=" * 50) + print("ТЕСТИРОВАНИЕ ЗАВЕРШЕНО") + + +def main(): + """Основная функция""" + run_five_tests() + + +if __name__ == "__main__": + main() +``` + +### Анализ сложности алгоритма преобразования инфикс → постфикс + +### Детализация операций + +| Операция | Сложность | Примечания | +|----------|-----------|------------| +| Инициализация структур | O(1) | Словарь, два списка | +| Цикл for по expression | O(n) | n символов в выражении | +| `token.isalnum()` | O(1) | Проверка одного символа | +| `output.append()` | O(1)* | Амортизированная константа | +| `stack.append()` | O(1)* | Амортизированная константа | +| `stack.pop()` | O(1) | Удаление с конца | +| `stack[-1]` | O(1) | Доступ к вершине стека | +| `precedence.get()` | O(1) | Поиск в словаре из 4 элементов | +| Вложенные while циклы | O(n) | Суммарно O(n) операций | +| `' '.join(output)` | O(n) | Объединение n элементов | + +### Пространственная сложность: O(n) + +- **output**: O(n) - содержит все токены выражения +- **stack**: O(n) - в худшем случае все операторы в стеке +- **precedence**: O(1) - фиксированный словарь + +### Анализ случаев + +### Худший случай +**Выражение**: `"a+b*c-d/e*f+g"` +- **Время**: O(n) +- **Память**: O(n) + +### Лучший случай +**Выражение**: `"a"` +- **Время**: O(n) +- **Память**: O(1) + +### Средний случай +**Сбалансированное выражение** +- **Время**: O(n) +- **Память**: O(n) + +### Итоговая таблица сложности + +| Метрика | Сложность | Обоснование | +|---------|-----------|-------------| +| **Временная** | O(n) | Линейный проход + амортизированные операции | +| **Пространственная** | O(n) | Выходной список и стек O(n) | +| **Лучший случай** | O(n) | Минимальное выражение | +| **Худший случай** | O(n) | Сложное выражение с приоритетами | + +### Вывод + +Алгоритм Шантинг-ярда оптимален для преобразования инфиксной в постфиксную запись с линейной временной и пространственной сложностью O(n). + + +### Контрольные вопросы + +#### 1. Определение стека и принцип LIFO +**Стек** - это абстрактный тип данных, представляющий коллекцию элементов с ограниченным доступом. +**Принцип LIFO** (Last-In, First-Out) - последний добавленный элемент извлекается первым. + +#### 2. Определение очереди и принцип FIFO +**Очередь** - это абстрактный тип данных, представляющий упорядоченную коллекцию элементов. +**Принцип FIFO** (First-In, First-Out) - первый добавленный элемент извлекается первым. + +#### 3. Определение дека и его отличие +**Дек** (double-ended queue) - это двусторонняя очередь, позволяющая добавлять и удалять элементы с обоих концов. +**Отличие**: Дек обобщает возможности стека и очереди, предоставляя операции работы с обоими концами. + +#### 4. Основные операции АТД +**Стек**: +- `push` - добавление на вершину +- `pop` - удаление с вершины +- `peek` - просмотр вершины +- `isEmpty` - проверка на пустоту + +**Очередь**: +- `enqueue` - добавление в конец +- `dequeue` - удаление из начала +- `peek` - просмотр начала +- `isEmpty` - проверка на пустоту + +**Дек**: +- `pushFront` - добавление в начало +- `pushBack` - добавление в конец +- `popFront` - удаление из начала +- `popBack` - удаление из конца +- `peekFront` - просмотр начала +- `peekBack` - просмотр конца +- `isEmpty` - проверка на пустоту + +#### 5. Односвязный список для стека +Односвязный список идеально подходит, потому что все операции стека выполняются с одного конца. +**Голова списка** становится вершиной стека, что позволяет выполнять операции за O(1). + +#### 6. Алгоритм push для стека на списке + +1. Создать новый узел с данными +2. Установить next нового узла = текущая голова +3. Назначить голову = новый узел + +#### 7.Алгоритм pop для стека на списке + + + +1. Если стек пуст (head == null) → ошибка +2. Сохранить текущую голову во временной переменной +3. Назначить голову = head.next +4. Вернуть данные из сохраненного узла +#### 8. Необходимость двух указателей для очереди +Хранить head и tail необходимо для эффективного добавления в конец. +Если хранить только head: операция enqueue будет O(n), так как потребуется проход по всему списку до последнего элемента. + +#### 9. Алгоритм enqueue для очереди с tail +pseudocode +1. Создать новый узел с данными +2. Если очередь пуста (head == null): + - head = новый узел + - tail = новый узел +3. Иначе: + - tail.next = новый узел + - tail = новый узел +#### 10. Алгоритм dequeue и важность проверки +pseudocode +1. Если очередь пуста (head == null) → ошибка +2. Сохранить текущий head во временной переменной +3. Назначить head = head.next +4. Если head == null (очередь стала пустой): + - tail = null +5. Вернуть данные из сохраненного узла +Важность проверки: если не обновить tail при опустошении очереди, он будет указывать на удаленный узел. + +#### 11. Двусвязный список для дека +Двусвязный список позволяет эффективно удалять элементы с конца за O(1). В односвязном списке popBack требует O(n) для нахождения предпоследнего элемента. + +#### 12. Алгоритм popBack для дека +pseudocode +1. Если дек пуст (tail == null) → ошибка +2. Сохранить текущий tail во временной переменной +3. Назначить tail = tail.prev +4. Если tail == null (остался один элемент): + - head = null +5. Иначе: + - tail.next = null +6. Вернуть данные из сохраненного узла +#### 13. Преимущества и недостатки стека на массиве +Преимущества: + +Лучшая локальность кэша + +Меньше накладных расходов памяти + +Недостатки: + +Возможная реаллокация с сложностью O(n) + +Ограниченный размер (если не динамический) + +#### 14. Кольцевой буфер для очереди +Кольцевой буфер - массив, в котором начало и конец замыкаются в кольцо. Позволяет эффективно использовать память и реализовать очередь с O(1) операциями без сдвигов элементов. + +#### 15. Сложность операций дека на массиве +pushFront и popFront имеют сложность O(n) на обычном массиве, потому что требуют сдвига всех элементов для освобождения/заполнения позиции в начале. + +#### 16. Таблица асимптотической сложности +| Операция | Стек (список) | Очередь (список) | Дек (двусвязный список) | Дек (массив*) | +|----------|---------------|------------------|-------------------------|---------------| +| push/enqueue/pushBack | O(1) | O(1) | O(1) | O(1) | +| pop/dequeue/popBack | O(1) | O(1) | O(1) | O(1) | +| pushFront | O(n) | O(1) | O(1) | O(1) | +| popFront | O(n) | O(1) | O(1) | O(1) | +| peek | O(1) | O(1) | O(1) | O(1) | +| isEmpty | O(1) | O(1) | O(1) | O(1) | +*Амортизированная сложность(Идея — усреднить стоимость более «дорогих» операций по всей последовательности, чтобы средняя стоимость каждой операции оставалась постоянной или ниже.) + +#### 17. Константная сложность операций дека +Все операции дека на двусвязном списке имеют O(1), потому что: + +Доступ к обоим концам через head и tail + +Обновление связей происходит за константное время + +Не требуется проход по списку + +#### 18. Примеры использования стека +Стек вызовов функций + +Проверка скобочных последовательностей + +Алгоритм обхода в глубину (DFS) + +#### 19. Примеры использования очереди +Очередь печати + +Очередь задач в процессоре + +Алгоритм обхода в ширину (BFS) + +#### 20. Пример использования дека +Система истории браузера: + +pushBack - добавление новой страницы + +popBack - кнопка "Назад" + +pushFront - кнопка "Вперед" (при возврате) + +#### 21. Проверка скобочной последовательности + +1. Создать пустой стек +2. Для каждого символа в строке: + - Если открывающая скобка → push в стек + - Если закрывающая скобка: + * Если стек пуст → ошибка + * Если pop из стека ≠ парная скобка → ошибка +3. Если стек не пуст → ошибка +#### 22. Роль очереди в BFS +Очередь хранит вершины для обработки в порядке увеличения расстояния от стартовой вершины, обеспечивая обход "слоями". + +#### 23. Порядок извлечения из стека +Операции: push(1), push(2), pop(), push(3), push(4), pop(), pop() +Порядок извлечения: 2, 4, 3 + +#### 24. Порядок извлечения из очереди +Операции: enqueue(1), enqueue(2), dequeue(), enqueue(3), enqueue(4), dequeue(), dequeue() +Порядок извлечения: 1, 2, 3 + +#### 25. Ошибка без обновления tail +Если при dequeue не обновлять tail при опустошении очереди, tail продолжит указывать на удаленный узел, что приведет к ошибкам при последующих операциях. + +#### 26. Сложность push в стеке на массиве +В худшем случае O(n) из-за реаллокации, когда массив заполнен и требуется: + +Выделение нового массива + +Копирование всех элементов + +#### 27. Преимущества по памяти +Массив: меньше накладных расходов, нет указателей +Список: гибкое использование памяти, нет фрагментации + +#### 28. Критерии выбора реализации +Списки предпочтительнее когда: + +Неизвестен максимальный размер + +Важна предсказуемость операций + +Частое изменение размера + +Массивы предпочтительнее когда: + +Известен максимальный размер + +Важна производительность и локальность кэша + +Память ограничена + + +```python + +``` diff --git a/labs/lab_05/Laba_05_Asonov_.md b/labs/lab_05/Laba_05_Asonov_.md new file mode 100644 index 0000000..86d3001 --- /dev/null +++ b/labs/lab_05/Laba_05_Asonov_.md @@ -0,0 +1,1139 @@ +--- +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( Хеш-таблица на основе метода цепочек) + + +#### Хеш-таблица +— это структура данных, предназначенная для реализации ассоциативного массива (отображения ключей в значения). Она позволяет выполнять операции добавления, удаления и поиска элементов в среднем за O(1) время. + +#### Основные компоненты: + + Ключ — уникальный идентификатор, по которому происходит поиск данных. + Значение — данные, связанные с ключом. +Хеш-функция — функция, преобразующая ключ в индекс (хеш-код) для размещения в таблице. +Массив бакетов — массив, где хранятся элементы. Каждая ячейка массива называется бакетом (bucket) или слотом. +Коэффициент нагрузки (load factor) — отношение количества элементов в таблице к размеру массива (n / size). Важный параметр для эффективности. +#### Хеш-функции +Хеш-функция — это функция, которая преобразует ключ произвольного размера и типа в целое число (индекс) фиксированного диапазона [0, size-1]. + +#### Требования к хорошей хеш-функции: + +Детерминированность: Один и тот же ключ всегда должен давать один и тот же хеш-код. + +Равномерное распределение: Ключи должны равномерно распределяться по всем индексам таблицы для минимизации коллизий. + +Вычислительная эффективность: Функция должна быстро вычисляться. + +#### Распространенные методы вычисления хеш-кодов: + +Для целых чисел: часто используют взятие по модулю размера таблицы: hash(key) = key % size. + +Для строк: популярна полиномиальная хеш-функция (например, метод Горнера), чтобы учесть значение каждого символа. + +```python +class Node: + def __init__(self, key, value): + self.key = key + self.value = value + self.next = None + + +class HashTable: + def __init__(self, capacity=10): + self.capacity = capacity + self.size = 0 + self.buckets = [None] * capacity + + def _hash(self, key): + return hash(key) % self.capacity + + def put(self, key, value): + index = self._hash(key) + node = self.buckets[index] + + if node is None: + self.buckets[index] = Node(key, value) + self.size += 1 + return + + prev = None + while node is not None: + if node.key == key: + node.value = value + return + prev = node + node = node.next + + prev.next = Node(key, value) + self.size += 1 + + def get(self, key): + """Возвращает значение по ключу или None если ключ не найден""" + index = self._hash(key) + node = self.buckets[index] + + while node is not None: + if node.key == key: + return node.value + node = node.next + + return None + + def remove(self, key): + index = self._hash(key) + node = self.buckets[index] + prev = None + + while node is not None: + if node.key == key: + if prev is None: + self.buckets[index] = node.next + else: + prev.next = node.next + self.size -= 1 + return True + prev = node + node = node.next + + return False + + def __len__(self): + return self.size + + def __contains__(self, key): + return self.get(key) is not None + + def __getitem__(self, key): + value = self.get(key) + if value is None: + raise KeyError(f"Key '{key}' not found") + return value + + def __setitem__(self, key, value): + self.put(key, value) + + + +def test_basic(): + ht = HashTable() + ht.put("apple", 5) + ht.put("banana", 10) + + assert ht.get("apple") == 5 + assert ht.get("banana") == 10 + assert ht.get("orange") is None + assert len(ht) == 2 + print(" Базовая функциональность работает") + + +def test_update(): + ht = HashTable() + ht.put("apple", 5) + ht.put("apple", 15) + + assert ht.get("apple") == 15 + assert len(ht) == 1 + print(" Обновление значений работает") + + +def test_remove(): + ht = HashTable() + ht.put("apple", 5) + ht.put("banana", 10) + + assert ht.remove("apple") == True + assert ht.remove("apple") == False + assert ht.get("apple") is None + assert len(ht) == 1 + print(" Удаление элементов работает") + + +def test_operators(): + ht = HashTable() + ht["apple"] = 5 + ht["banana"] = 10 + + assert ht["apple"] == 5 + assert "apple" in ht + assert "orange" not in ht + print(" Python операторы работают") + + +def test_collisions(): + ht = HashTable(2) + ht.put("a", 1) + ht.put("b", 2) + ht.put("c", 3) + + assert ht.get("a") == 1 + assert ht.get("b") == 2 + assert ht.get("c") == 3 + assert len(ht) == 3 + print(" Обработка коллизий работает") + + +if __name__ == "__main__": + test_basic() + test_update() + test_remove() + test_operators() + test_collisions() + print("\n Все тесты пройдены!") +``` + +#### Метод цепочек (Separate Chaining) + +- self.buckets = [None] * capacity +- buckets: [None, None, None, None, None, None, None, None, None, None] + +- % self.capacity - берем остаток от деления на capacity для попадания в диапазон [0, capacity-1] + +#### Специальные методы Python + +def __len__(self): + return self.size # Поддержка len(hashtable) + +def __contains__(self, key): + return self.get(key) is not None # Поддержка 'key in hashtable' + +def __getitem__(self, key): + value = self.get(key) + if value is None: + raise KeyError(f"Key '{key}' not found") + return value # Поддержка hashtable[key] + +def __setitem__(self, key, value): + self.put(key, value) # Поддержка hashtable[key] = value + +Каждый бакет представляет собой связный список (или другую структуру), в котором хранятся все элементы с одинаковым хеш-кодом. + +Операции: + +Вставка: Вычисляется индекс. Элемент добавляется в начало или конец списка в данном бакете. + +Поиск: Вычисляется индекс. Производится линейный поиск по списку в бакете по ключу. + +Удаление: Аналогично поиску, после нахождения элемент удаляется из списка. + +Преимущества: Простота реализации, эффективен при высокой нагрузке. + +Недостатки: Требует дополнительной памяти на хранение указателей. + + +### Задание 2 (Хеш-таблица на основе открытой адресации) + +```python +class HashTableOpenAddressing: + def __init__(self, size=10): + self.size = size + self.table = [None] * size + self.count = 0 + self.load_factor_threshold = 0.7 + self.DELETED = object() #DELETED = object() - специальный маркер, который отличает удаленные элементы от пустых ячеек + + def _hash(self, key): + if isinstance(key, int): + return key % self.size + elif isinstance(key, str): #полиномиальный хеш (31 - простое число для уменьшения коллизий) + hash_value = 0 + for char in key: + hash_value = (hash_value * 31 + ord(char)) % self.size # hash = (97*31 + 98) % size = (3007 + 98) % size + return hash_value + else: + return hash(key) % self.size + + def _probe(self, key, i): + return (self._hash(key) + i) % self.size + + def _resize(self): + if self.count / self.size > self.load_factor_threshold: + old_table = self.table + old_size = self.size + self.size = self.size * 2 + self.table = [None] * self.size + self.count = 0 + for item in old_table: + if item and item != self.DELETED: + self.put(item[0], item[1]) + + def put(self, key, value): + self._resize() + for i in range(self.size): + index = self._probe(key, i) + if self.table[index] is None or self.table[index] == self.DELETED: + self.table[index] = (key, value) + self.count += 1 + return + elif self.table[index][0] == key: + self.table[index] = (key, value) + return + raise Exception("Hash table is full") + + def get(self, key): + for i in range(self.size): + index = self._probe(key, i) + if self.table[index] is None: + break + elif self.table[index] != self.DELETED and self.table[index][0] == key: + return self.table[index][1] + raise KeyError(f"Key {key} not found") + + def delete(self, key): + for i in range(self.size): + index = self._probe(key, i) + if self.table[index] is None: + break + elif self.table[index] != self.DELETED and self.table[index][0] == key: + self.table[index] = self.DELETED #Если бы ставили None, то цепочка пробирования прервалась бы и последующие элементы стали бы недоступны + self.count -= 1 + return True + raise KeyError(f"Key {key} not found") + + def contains(self, key): + try: + self.get(key) + return True + except KeyError: + return False + + def __str__(self): + result = [] + for i, item in enumerate(self.table): + if item is None: + result.append(f"Bucket {i}: None") + elif item == self.DELETED: + result.append(f"Bucket {i}: DELETED") + else: + result.append(f"Bucket {i}: ({item[0]}: {item[1]})") + return "\n".join(result) + + + +def test_basic_operations(): + ht = HashTableOpenAddressing() + ht.put("apple", 5) + ht.put("banana", 10) + + assert ht.get("apple") == 5 + assert ht.get("banana") == 10 + assert ht.count == 2 + print("Базовая функциональность работает") + + +def test_update_values(): + ht = HashTableOpenAddressing() + ht.put("apple", 5) + ht.put("apple", 15) + + assert ht.get("apple") == 15 + assert ht.count == 1 + print(" Обновление значений работает") + + +def test_deletion(): + ht = HashTableOpenAddressing() + ht.put("apple", 5) + ht.put("banana", 10) + + assert ht.delete("apple") == True + assert ht.count == 1 + try: + ht.get("apple") + assert False, "Должно было возникнуть KeyError" + except KeyError: + pass + + assert ht.contains("banana") == True + assert ht.contains("apple") == False + print(" Удаление элементов работает") + + +def test_collisions(): + ht = HashTableOpenAddressing(5) + ht.put("a", 1) + ht.put("b", 2) + ht.put("c", 3) + ht.put("d", 4) + + assert ht.get("a") == 1 + assert ht.get("b") == 2 + assert ht.get("c") == 3 + assert ht.get("d") == 4 + assert ht.count == 4 + + + ht.delete("b") + assert ht.contains("b") == False + ht.put("e", 5) + assert ht.get("e") == 5 + print(" Обработка коллизий работает") + + + +if __name__ == "__main__": + test_basic_operations() + test_update_values() + test_deletion() + test_collisions() + + print("\n Все тесты хеш-таблицы с открытой адресацией пройдены!") +``` + +#### Метод открытой адресации (Open Addressing) + +self.DELETED = object() # Уникальный объект + +Преимущества: + +- Уникальность: object() создает абсолютно уникальный объект + + - Безопасность: невозможно случайно совпадение с пользовательскими данными + + - Производительность: быстрая проверка is или == + +Все элементы хранятся непосредственно в массиве. При коллизии элемент помещается в другую свободную ячейку согласно выбранной стратегии. + + ###### DELETED решает фундаментальную проблему открытой адресации: + + - Сохраняет целостность цепочек пробирования + +- Позволяет повторно использовать ячейки + +- Обеспечивает корректный поиск после удалений + +Просто реализуется и эффективно работает +#### Стратегии поиска свободной ячейки: + +Линейное пробирование: index = (hash(key) + i) % size, где i = 0, 1, 2, ... + +Квадратичное пробирование: index = (hash(key) + i²) % size + +Двойное хеширование: index = (hash1(key) + i * hash2(key)) % size + +##### Преимущества: +Не требует дополнительной памяти для указателей, лучше использует кэш процессора. + +##### Недостатки: +Более сложное удаление элементов, может возникнуть "кластеризация" (скопление элементов), сильная зависимость от коэффициента нагрузки. + + +### Задание 3(Блокчейн) + + +1. Создание строки блока +python +block_string = f"{self.index}{self.timestamp}{self.data}{self.previous_hash}" +Объединяет все важные данные блока в одну строку: + +self.index - номер блока в цепочке + +self.timestamp - время создания блока + +self.data - содержимое блока (транзакции, информация) + +self.previous_hash - хеш предыдущего блока + +2. Кодирование в байты +python +block_string.encode() +Преобразует строку в байты, так как хеш-функции работают с бинарными данными. + +3. Вычисление хеша SHA-256 +python +hashlib.sha256(block_string.encode()) +Применяет криптографическую функцию SHA-256 к данным. + +4. Получение шестнадцатеричного представления +python +.hexdigest() +Преобразует бинарный хеш в читаемую строку из 64 шестнадцатеричных символов. + + +##### Генезис-блок - особый первый блок: + +index=0 - начало цепочки + +data="Genesis Block" - специальная метка + +previous_hash="0" - ссылка на "ничто" + +timestamp - время создания блокчейна + + +```python +import hashlib +import time + +class Block: + def __init__(self, index, timestamp, data, previous_hash): + self.index = index + self.timestamp = timestamp # Время создания блока + self.data = data + self.previous_hash = previous_hash + self.hash = self.calculate_hash() + + def calculate_hash(self): + block_string = f"{self.index}{self.timestamp}{self.data}{self.previous_hash}" # Создаем строку из всех значимых данных блока + return hashlib.sha256(block_string.encode()).hexdigest() + + def __str__(self): + return f"Block {self.index} [Hash: {self.hash}, Previous: {self.previous_hash}, Data: {self.data}]" + +class Blockchain: + def __init__(self): + self.chain = [self.create_genesis_block()] + """ генезис-блок (первый блок) с индексом 0""" + def create_genesis_block(self): + return Block(0, time.time(), "Genesis Block", "0") + + def add_block(self, data): + """ Получаем последний блок в цепочке""" + previous_block = self.chain[-1] + new_block = Block( + index=len(self.chain), + timestamp=time.time(), + data=data, + previous_hash=previous_block.hash + ) + self.chain.append(new_block) + + def is_chain_valid(self): + for i in range(1, len(self.chain)): # Проверяем все блоки, начиная с первого после генезиса + current_block = self.chain[i] + previous_block = self.chain[i-1] + if current_block.hash != current_block.calculate_hash(): # Проверка 1: Не изменились ли данные текущего блока? + return False + if current_block.previous_hash != previous_block.hash: # Проверка 2: Корректная ссылка на предыдущий блок? + return False + return True + + def get_latest_block(self): + return self.chain[-1] + + def get_chain_length(self): + return len(self.chain) + + def __str__(self): + return "\n".join(str(block) for block in self.chain) + + + +def test_genesis_block(): + blockchain = Blockchain() + + assert len(blockchain.chain) == 1 + assert blockchain.chain[0].index == 0 + assert blockchain.chain[0].data == "Genesis Block" + assert blockchain.chain[0].previous_hash == "0" + assert blockchain.chain[0].hash == blockchain.chain[0].calculate_hash() + print("✓ Генезис-блок создан корректно") + + +def test_adding_blocks(): + blockchain = Blockchain() + + blockchain.add_block("Transaction 1") + blockchain.add_block("Transaction 2") + + assert len(blockchain.chain) == 3 + assert blockchain.chain[1].data == "Transaction 1" + assert blockchain.chain[2].data == "Transaction 2" + assert blockchain.chain[1].index == 1 + assert blockchain.chain[2].index == 2 + print(" Добавление блоков работает") + + +def test_block_linking(): + blockchain = Blockchain() + + blockchain.add_block("Data 1") + blockchain.add_block("Data 2") + + + assert blockchain.chain[1].previous_hash == blockchain.chain[0].hash + assert blockchain.chain[2].previous_hash == blockchain.chain[1].hash + print(" Связи между блоками корректны") + + +def test_chain_validation(): + blockchain = Blockchain() + + blockchain.add_block("Valid Data 1") + blockchain.add_block("Valid Data 2") + + + assert blockchain.is_chain_valid() == True + + + blockchain.chain[1].data = "Tampered Data" + + + assert blockchain.is_chain_valid() == False + print(" Валидация цепочки работает") + + +def test_data_integrity(): + blockchain = Blockchain() + + original_data = "Original Data" + blockchain.add_block(original_data) + + original_hash = blockchain.chain[1].hash + + + assert blockchain.chain[1].calculate_hash() == original_hash + + + blockchain.chain[1].data = "Modified Data" + assert blockchain.chain[1].calculate_hash() != original_hash + print(" Целостность данных и хеширование работают") + + +if __name__ == "__main__": + test_genesis_block() + test_adding_blocks() + test_block_linking() + test_chain_validation() + test_data_integrity() + print("\n Все тесты блокчейна пройдены!") +``` + +### Задание 4(Проверка пересечения двух массивов) + +```python +def has_intersection(arr1, arr2): + + hash_set = set(arr1) #Создаем множество(O(n) - для хранения множества) + for item in arr2: + if item in hash_set: + return True + return False + + + +def test_has_intersection(): + + arr1 = [1, 2, 3, 4, 5] + arr2 = [5, 6, 7, 8, 9] + assert has_intersection(arr1, arr2) == True + print(" Тест 1: Есть пересечение - пройден") + + + arr1 = [1, 2, 3, 4] + arr2 = [5, 6, 7, 8] + assert has_intersection(arr1, arr2) == False + print(" Тест 2: Нет пересечения - пройден") + + arr1 = [] + arr2 = [1, 2, 3] + assert has_intersection(arr1, arr2) == False + + arr1 = [1, 2, 3] + arr2 = [] + assert has_intersection(arr1, arr2) == False + + arr1 = [] + arr2 = [] + assert has_intersection(arr1, arr2) == False + print(" Тест 3: Пустые массивы - пройден") + + arr1 = [1, 2, 3, 4, 5] + arr2 = [3, 4, 5, 6, 7] + assert has_intersection(arr1, arr2) == True + print(" Тест 4: Множественные пересечения - пройден") + + arr1 = ["apple", "banana", "orange"] + arr2 = ["banana", "grape", "kiwi"] + assert has_intersection(arr1, arr2) == True + + arr1 = [1.5, 2.7, 3.1] + arr2 = [3.1, 4.2, 5.8] + assert has_intersection(arr1, arr2) == True + print(" Тест 5: Разные типы данных - пройден") + +if __name__ == "__main__": + test_has_intersection() + + print("\n Все тесты пройдены успешно!") +``` + +### Задание 5 (Проверка уникальности элементов в массиве) + +```python +def has_unique_elements(arr): + + hash_set = set() # Пустое множество + for item in arr: + if item in hash_set: + return False + hash_set.add(item) + return True + + +def test_has_unique_elements(): + + + arr = [1, 2, 3, 4, 5] + assert has_unique_elements(arr) == True + print(" Тест 1: Все элементы уникальны - пройден") + + + arr = [1, 2, 3, 2, 4] + assert has_unique_elements(arr) == False + print(" Тест 2: Есть дубликаты - пройден") + + + arr = [] + assert has_unique_elements(arr) == True + print(" Тест 3: Пустой массив - пройден") + + + arr = [42] + assert has_unique_elements(arr) == True + print(" Тест 4: Один элемент - пройден") + + + arr = ["apple", "banana", "orange"] + assert has_unique_elements(arr) == True + + arr = ["apple", "banana", "apple"] + assert has_unique_elements(arr) == False + + arr = [1, "1", 2, "2"] # + assert has_unique_elements(arr) == True + print(" Тест 5: Разные типы данных - пройден") + + +if __name__ == "__main__": + test_has_unique_elements() + print("\n Все тесты пройдены успешно!") +``` + +### Задание 6 (Нахождение пар с заданной суммой) + +```python +def find_pairs_with_sum(arr, target_sum): + result = [] + seen = set() # Множество для отслеживания просмотренных чисел + for num in arr: + complement = target_sum - num + if complement in seen: + result.append((complement, num)) + seen.add(num) #l обавляет новый элемент в множество, автоматически игнорируя дубликаты + return result + +def test_find_pairs_with_sum(): + """Тестирование функции find_pairs_with_sum""" + arr = [2, 7, 11, 15, 3, 6] + target = 9 + result = find_pairs_with_sum(arr, target) + + + assert (2, 7) in result + assert (3, 6) in result + assert len(result) == 2 + print(" Тест пройден: найдены пары (2,7) и (3,6)") + +if __name__ == "__main__": + test_find_pairs_with_sum() + print(" Тест пройден успешно!") +``` + +### Задание 7(Задача на проверку анаграмм) + +```python +def are_anagrams(str1, str2): + if len(str1) != len(str2): + return False + char_count = {} #Создание словаря для подсчета символов + for char in str1: + if char in char_count: + char_count[char] += 1 + else: + char_count[char] = 1 + for char in str2: + if char not in char_count or char_count[char] == 0: + return False + char_count[char] -= 1 + return True + + +def test_are_anagrams(): + """Тестирование функции are_anagrams""" + str1 = "listen" + str2 = "silent" + result = are_anagrams(str1, str2) + + assert result == True + print(" Тест пройден: 'listen' и 'silent' являются анаграммами") + +if __name__ == "__main__": + test_are_anagrams() + print(" Тест пройден успешно!") +``` + +1. Определение хеш-таблицы и основная задача + +Хеш-таблица — это структура данных, которая реализует абстрактный тип данных "ассоциативный массив" (или "словарь"). Она хранит пары "ключ-значение". + +Основная задача, которую она решает — это обеспечение быстрого доступа к данным по ключу. В среднем случае, время выполнения операций (вставка, поиск, удаление) составляет O(1). + +--- + +2. Хеш-функция и ее роль + +Хеш-функция — это детерминированная функция, которая преобразует произвольный ключ (например, строку, число, объект) в число фиксированной длины (хеш-код), которое затем используется как индекс в массиве (хеш-таблице). + +Роль в работе хеш-таблицы: Хеш-функция равномерно распределяет ключи по ячейкам (бакетам) таблицы. Это позволяет по ключу за время O(1) вычислить позицию, где должно храниться или где следует искать соответствующее значение. + +--- + +3. Ключевые свойства хорошей хеш-функции + +1. Детерминированность: Один и тот же ключ всегда должен давать один и тот же хеш-код. +2. Равномерное распределение: Хеш-функция должна распределять ключи по всем возможным индексам максимально равномерно. Это минимизирует количество коллизий. +3. Вычислительная эффективность: Вычисление хеш-кода должно быть быстрым (желательно O(1) или O(длина ключа)). +4. Устойчивость к коллизиям (для криптографии): Для обычных хеш-таблиц это свойство менее критично, но в целом хорошая функция должна минимизировать вероятность коллизий для разных входных данных. + +--- + +4. Коллизии и почему их нельзя избежать + +Коллизия — это ситуация, когда две разные пары ключ-значение претендуют на одну и ту же ячейку хеш-таблицы, то есть h(key1) = h(key2). + +Коллизии невозможно полностью избежать из-за принципа Дирихле (или "принципа ящиков"). Хеш-функция отображает потенциально бесконечное (или очень большое) множество возможных ключей на конечное множество индексов таблицы (например, 0..N-1). Следовательно, какие-то два разных ключа неизбежно попадут в одну ячейку. + +--- + +5. Коэффициент нагрузки (Load Factor) + +Коэффициент нагрузки (α) — это отношение количества элементов в таблице (n) к ее общему размеру (m): α = n / m. + +Влияние на производительность: + +· Низкий α (много свободного места): Коллизии маловероятны, операции выполняются быстро, близко к O(1). +· Высокий α (таблица заполнена): Вероятность коллизий резко возрастает. Для метода цепочек поиск в длинной цепочке замедляется до O(n). Для открытой адресации увеличивается длина последовательностей проб, что также замедляет все операции. + Когда α превышает определенный порог (обычно 0.7-0.8), необходимо выполнять рехеширование. + +--- + +6. Принцип работы метода цепочек (Separate Chaining) + +Каждая ячейка хеш-таблицы является не самим элементом, а указателем на голову связного списка (или корень другого дерева). Все элементы, чьи ключи дали один и тот же хеш (коллизия), просто добавляются в этот список. + +· Вставка: Вычисляем индекс, добавляем элемент в начало/конец списка в этой ячейке. O(1). +· Поиск: Вычисляем индекс, ищем элемент в соответствующем списке. O(длина списка). +· Удаление: Вычисляем индекс, удаляем элемент из списка. O(длина списка). + +--- + +7. Принцип работы метода открытой адресации (Open Addressing) + +При использовании этого метода все элементы хранятся непосредственно в самом массиве хеш-таблицы. При возникновении коллизии алгоритм ищет следующую свободную ячейку внутри самой таблицы по определенному алгоритму (пробингу). + +Принципиальное отличие от метода цепочек: В методе цепочек элементы с коллизиями хранятся вне основного массива (в цепочках), а в открытой адресации разрешение коллизий происходит внутри самого массива. + +--- + +8. Линейное, квадратичное пробирование и двойное хеширование + +· Линейное пробирование: Поиск следующей ячейки идет с фиксированным шагом (обычно 1). index = (h(key) + i) % m, где i = 0, 1, 2, ... + · Проблема: Возникает первичная кластеризация — длинные последовательности занятых ячеек, которые замедляют все операции. +· Квадратичное пробирование: Шаг поиска зависит от номера попытки квадратично. +index = (h(key) + c₁*i + c₂*i²) % m. + · Решает проблему первичной кластеризации, но может возникнуть вторичная кластеризация (элементы с одинаковым начальным хешем имеют одинаковую последовательность проб). + · Проблема: Не гарантирует, что будут проверены все ячейки. +· Двойное хеширование: Для определения шага используется вторая хеш-функция. index = (h₁(key) + i * h₂(key)) % m. + · Наиболее эффективен, так как использует две независимые хеш-функции. Это приводит к самому равномерному распределению проб и минимизирует кластеризацию. Разные ключи имеют разные последовательности проб. + +--- + +9. Преимущества и недостатки метода цепочек vs открытой адресации + +Метод цепочек Открытая адресация ++ Проще в реализации + Лучшая локальность ссылок (все данные в одном массиве), может быть быстрее из-за кеша процессора ++ Устойчив к высоким коэффициентам нагрузки (α > 1 возможен) - Сильнее страдает от высокой нагрузки, требует большей таблицы ++ Удаление тривиально - Удаление сложнее (требует пометки "удален") +- Требует дополнительной памяти на указатели - Не требует дополнительной памяти +- Хуже локальность ссылок (узлы списка разбросаны в памяти) - Высокая вероятность кластеризации (кроме двойного хеширования) + +--- + +10. Проблема удаления в открытой адресации + +Простое удаление (например, установка ячейки в "пусто") нарушает целостность, потому что при поиске мы идем по последовательности проб, пока не встретим пустую ячейку. Если мы удалим элемент из середины такой последовательности, последующие элементы станут недоступными. + +Правильная реализация: При удалении ячейка помечается специальным флагом "удален" (tombstone). При вставке в такую ячейку можно помещать новый элемент. При поиске мы просто "перепрыгиваем" через такие ячейки, продолжая пробинг. + +--- + +11. Сложность O(1) в среднем случае + +Предполагая, что хеш-функция равномерна и коэффициент нагрузки α является константой, длина цепочек (для метода цепочек) и длина последовательностей проб (для открытой адресации) также будет константной. Поэтому для доступа к нужной ячейке или для проверки константного числа ячеек требуется O(1) времени. + +--- + +12. Худший случай для хеш-таблицы + +Худший случай — когда все ключи попадают в одну и ту же ячейку. + +· Для метода цепочек: Все элементы оказываются в одном связном списке. Таблица вырождается в список со сложностью операций O(n). +· Для открытой адресации: Длина последовательности проб становится равна n, сложность операций также O(n). + +Условия возникновения: + +1. Плохая хеш-функция, которая не распределяет ключи равномерно. +2. Злонамеренно подобранные ключи (атака на хеш-таблицу), если хеш-функция известна и не является криптостойкой. + +--- + +13. Рехеширование (Rehashing) + +Рехеширование — это процесс создания новой хеш-таблицы большего размера и пересчета хешей для всех существующих элементов с последующим их перемещением в новую таблицу. + +Для чего необходимо: Чтобы уменьшить коэффициент нагрузки и, как следствие, снизить вероятность коллизий и поддерживать высокую производительность операций. + +--- + +14. Момент для выполнения рехеширования + +Момент обычно выбирается на основе коэффициента нагрузки α. + +· Для метода цепочек порог может быть выше (например, α = 0.75 - 1.0). +· Для открытой адресации порог ниже (например, α = 0.6 - 0.75), так как она сильнее страдает от заполнения. + +Когда α превышает заданный порог, инициируется рехеширование. + +--- + +15. Процесс рехеширования + + Создается новая хеш-таблица, размер которой обычно в 1.5-2 раза больше предыдущего. Желательно, чтобы новый размер был простым числом. + Для каждого элемента из старой таблицы: + · Вычисляется новый хеш-код с использованием новой функции (или той же, но с новым размером таблицы). + · Элемент вставляется в новую таблицу. +Старая таблица освобождается из памяти. +Дальнейшие операции выполняются с новой таблицей. + +--- + +16. Структура данных для бакетов в методе цепочек + +Наиболее удобна связный список (linked list). + +Обоснование: + +· Динамический размер: Списки идеально подходят для хранения динамически изменяющегося количества элементов в бакете. +· Простота вставки/удаления: Вставка и удаление из односвязного списка выполняются за O(1), если есть указатель на предыдущий элемент (или используется двусвязный список). +· Эффективность по памяти: Требуют только дополнительной памяти на указатели. + +Альтернатива: Если бакеты становятся очень длинными из-за плохой хеш-функции или атаки, их можно заменить на сбалансированные двоичные деревья поиска (например, красно-черные). Это гарантирует O(log n) время поиска даже в худшем случае. Именно так реализовано в java.util.HashMap начиная с JDK 8. + +--- + +17. Формула для вычисления индекса + +index = hash(key) % table_size + +Операция взятия по модулю гарантирует, что полученный индекс будет находиться в диапазоне [0, table_size - 1]. + +--- + +18. Размер таблицы — простое число + +Использование простого числа в качестве размера таблицы помогает уменьшить количество коллизий, особенно если хеш-функция неидеальна. + +Причина: Многие ключи в реальных задачах имеют скрытые закономерности (например, все четные). Если размер таблицы (m) и шаги в хеш-функции имеют общие делители, то многие ячейки могут никогда не быть использованы, что усиливает кластеризацию. Простое число m минимизирует количество общих делителей, способствуя более равномерному распределению. + +--- + +19. Хеш-функция для строки + +Одна из самых распространенных и эффективных — полиномиальное хеширование. + +Формула: hash = (s[0] * p^(n-1) + s[1] * p^(n-2) + ... + s[n-1] * p^0) % m + +Где: + +· s[i] — код i-го символа строки. +· p — простое число (например, 31 или 37). +· n — длина строки. +· m — размер хеш-таблицы (модуль). + +Пример для строки "hi": +h = ('h' * 31 + 'i') % m + +Чтобы избежать переполнения и работать с большими числами, на каждом шаге лучше брать по модулю: +def hash_string(s, table_size): + h = 0 + p = 31 + for char in s: + h = (h * p + ord(char)) % table_size + return h + +--- + +20. Проблемы неравномерного распределения хеш-функции + +· Резкое падение производительности: Возрастает количество коллизий, операции из O(1) вырождаются в O(n). +· Кластеризация: Элементы скапливаются в определенных областях таблицы, что особенно губительно для открытой адресации. +· Уязвимость к DoS-атакам: Злоумышленник может подобрать данные, которые все попадут в один бакет, и "положить" сервер, использующий хеш-таблицу. + +--- + +21. Когда использовать цепочки, а когда открытую адресацию? + +Метод цепочек предпочтительнее, когда: + +· Неизвестно заранее количество элементов. +· Требуется стабильная производительность даже при высокой нагрузке. +· Нужна простая и надежная реализация удаления. + +Открытая адресация предпочтительнее, когда: + +· Важна производительность на современных процессорах (лучшая локальность кеша). +· Известен примерный размер данных, и можно выделить таблицу с запасом. +· Жесткие ограничения по памяти (не тратится на указатели). + +--- + +22. Примеры реальных задач для хеш-таблиц + +- Словарь или кеш: Быстрый поиск определения по слову или кешированного результата по запросу. +- Удаление дубликатов: Можно за O(n) найти и удалить все дубликаты в массиве, просто добавляя элементы в хеш-таблицу. +- Подсчет частоты элементов: Ключ — элемент, значение — счетчик. За один проход по массиву можно построить частотный словарь. + +--- + +23. Сравнение с сбалансированным деревом поиска + +Хеш-таблица Сбалансированное дерево (например, красно-черное) +В среднем O(1) на операцию Гарантированно O(log n) на операцию +Нет упорядоченности данных Данные упорядочены (можно обходить по порядку) +Производительность сильно зависит от хеш-функции Производительность стабильна и предсказуема +Может быть уязвима к атакам Устойчива к патологическим наборам данных +Обычно использует меньше памяти на элемент Использует больше памяти на указатели + +--- + +24. Хранение одинаковых ключей + +Да, может. Это реализуется двумя основными способами: + +1. Хранение списка значений: Каждому ключу соответствует не одно значение, а список (или другая коллекция) всех значений, вставленных с этим ключом. +2. Подсчет (для мультимножеств): Значением является счетчик. При вставке существующего ключа счетчик увеличивается. + +--- + +25.Построение хеш-таблицы для ключей + +Дано: h(key) = key % 7, ключи: 12, 5, 19, 7, 26, 14, 33 + +а) Метод цепочек: + +- 12 % 7 = 5 +- 5 % 7 = 5 -> коллизия с 12 +- 19 % 7 = 5 -> коллизия с 12 и 5 +- 7 % 7 = 0 +- 26 % 7 = 5 -> коллизия +- 14 % 7 = 0 -> коллизия с 7 +- 33 % 7 = 5 -> коллизия + +Таблица (индексы 0-6): +- 0: [7] -> [14] +- 1: [] +- 2: [] +- 3: [] +- 4: [] +- 5: [12] -> [5] -> [19] -> [26] -> [33] +- 6: [] + +б) Линейное пробирование (шаг +1): + +- 12 -> 5 +- 5 -> 5 (занято) -> 6 +- 19 -> 5 (занято) -> 6 (занято) -> 0 +- 7 -> 0 (занято) -> 1 +- 26 -> 5 (занято) -> 6 (занято) -> 0 (занято) -> 1 (занято) -> 2 +- 14 -> 0 (занято) -> 1 (занято) -> 2 (занято) -> 3 +- 33 -> 5 (занято) -> 6 (занято) -> 0 (занято) -> 1 (занято) -> 2 (занято) -> 3 (занято) -> 4 + +Таблица: +- 0: 19 +- 1: 7 +- 2: 26 +- 3: 14 +- 4: 33 +- 5: 12 +- 6: 5 + +в) Квадратичное пробирование (index = (h(key) + i²) % 7): + +- 12 -> 5 +- 5 -> 5 (занято, i=1) -> (5+1)%7=6 +- 19 -> 5 (занято, i=1) -> (5+1)%7=6 (занято, i=2) -> (5+4)%7=2 +- 7 -> 0 +·-26 -> 5 (занято, i=1) -> 6 (занято, i=2) -> 2 (занято, i=3) -> (5+9)%7=0 (занято, i=4) -> (5+16)%7=0 (занято, i=5) -> (5+25)%7=2 (занято, i=6) -> (5+36)%7=6 (занято). + +Здесь видна проблема квадратичного пробирования — оно не всегда находит свободную ячейку, даже если она есть. На практике используют (h(key) + c1*i + c2*i²) и тщательно подбирают константы и размер таблицы. + +--- + +26. Поиск двух чисел с суммой X + +Алгоритм: + +- Создаем пустую хеш-таблицу (множество). +- Для каждого элемента num в массиве: + · Вычисляем complement = X - num. + · Проверяем, есть ли complement в хеш-таблице. + · Если есть — мы нашли пару (num, complement). + · Если нет — добавляем текущий num в хеш-таблицу. + +Сложность: O(n), так как каждая операция с хеш-таблицей — O(1). + +--- + +27. Поиск первого повторяющегося элемента + +Алгоритм: + +- Создаем пустую хеш-таблицу (в качестве значения можно хранить индекс первого вхождения). +- Проходим по массиву. Для каждого элемента: + · Если его уже нет в таблице, добавляем его (и его индекс). + · Если он уже есть в таблице — это и есть первый повторяющийся элемент. + +--- + +28. Идеальное хеширование + +Идеальное хеширование — это техника, которая позволяет построить хеш-функцию, не имеющую коллизий для заранее известного статического набора ключей. + +Применение: Используется в случаях, когда набор ключей фиксирован и известен на этапе компиляции (например, ключевые слова в языке программирования), и требуется гарантировать константное время доступа даже в худшем случае. + +Как работает: Часто используется двухуровневая схема. Хеш-функция первого уровня распределяет ключи по бакетам. Затем для каждого бакета, содержащего более одного ключа, подбирается своя собственная хеш-функция второго уровня, которая без коллизий размещает ключи этого бакета во вторичной таблице. + +```python + +``` diff --git a/labs/lab_06/Untitled.md b/labs/lab_06/Untitled.md new file mode 100644 index 0000000..9e79af6 --- /dev/null +++ b/labs/lab_06/Untitled.md @@ -0,0 +1,756 @@ +--- +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(Рекурсивная реализация) + + + Рекурсивное вычисление квадратного корня методом Ньютона + + Функция принимает три параметра: + + - N - число, из которого извлекаем корень + + - A - начальное приближение (guess) + + - E - точность вычисления (epsilon) + + Проверка входных данных + + - N < 0: Корень из отрицательного числа не определен в вещественных числах + + - N == 0: Корень из нуля всегда ноль + + - A ≤ 0: Начальное приближение должно быть положительным для корректной работы алгоритма +```python +import math +import time + +def sqrt_recursive(N, A, E): + if N < 0: + raise ValueError("N должно быть неотрицательным") + if N == 0: + return 0.0 + if A <= 0: + raise ValueError("A должно быть положительным") + + """Новое приближение по формуле Ньютона""" + new_A = (A + N / A) / 2 + + if abs(new_A - A) < E: + return new_A + + return sqrt_recursive(N, new_A, E) + +print("Введите число для извлечения квадратного корня:") +N = float(input()) +print("Введите начальное приближение:") +A = float(input()) +print("Введите точность вычисления:") +E = float(input()) + +try: + result = sqrt_recursive(N, A, E) + print(f"Квадратный корень из {N} с точностью {E}:") + print(f"Результат: {result}") + print(f"Проверка: {result} * {result} = {result * result}") +except ValueError as e: + print(f"Ошибка: {e}") +``` + +### Задание 2 (Итеративная реализация (без рекурсии)) + +```python +def sqrt_iterative(N, A, E): + if N < 0: + raise ValueError("N должно быть неотрицательным") + if N == 0: + return 0.0 + if A <= 0: + raise ValueError("A должно быть положительным") + + current_A = A + while True: + new_A = (current_A + N / current_A) / 2 + if abs(new_A - current_A) < E: + return new_A + current_A = new_A + +print("Введите число для извлечения квадратного корня:") +N = float(input()) +print("Введите начальное приближение:") +A = float(input()) +print("Введите точность вычисления:") +E = float(input()) + +try: + result = sqrt_iterative(N, A, E) + print(f"Квадратный корень из {N} с точностью {E}:") + print(f"Результат: {result}") + print(f"Проверка: {result} * {result} = {result * result}") +except ValueError as e: + print(f"Ошибка: {e}") +``` + +### Задание 3 + + + + + +![png](block_table2.png) + + +## Работа декоратора(синтаксис) +@save_intermediate_results +### Эквивалентно: +sqrt_recursive_decorated = save_intermediate_results(sqrt_recursive_decorated) + +Без @wraps теряется информация об оригинальной функции + + + Декоратор для автоматического сохранения промежуточных результатов рекурсии + + Принцип работы: + 1. Перехватывает каждый вызов функции + 2. Собирает статистику ДО выполнения функции + 3. Вызывает оригинальную функцию + 4. Собирает статистику ПОСЛЕ выполнения функции + + +```python +import sys +import time +from functools import wraps + +class SqrtStats: + """Контейнер для статистики выполнения алгоритмов""" + def __init__(self): + self.iterations = 0 + self.max_stack_depth = 0 + self.execution_time = 0 + self.intermediate_results = [] + + def reset(self): + """Сброс статистики перед новым запуском""" + self.iterations = 0 + self.max_stack_depth = 0 + self.execution_time = 0 + self.intermediate_results = [] + +stats = SqrtStats() # Единый объект для всех алгоритмов + +def save_intermediate_results(func): + """Декоратор для автоматического сбора статистики рекурсии""" + @wraps(func) + def wrapper(N, A, E, depth=0): + stats.iterations += 1 + stats.max_stack_depth = max(stats.max_stack_depth, depth) + + intermediate = { + 'depth': depth, 'N': N, 'A': A, 'E': E, 'timestamp': time.time() + } + + result = func(N, A, E, depth) + + intermediate['result'] = result + stats.intermediate_results.append(intermediate) + + return result + return wrapper + +def manual_sqrt_recursive_with_storage(N, A, E): + """Рекурсивный алгоритм с ручным сбором статистики""" + intermediate_results = [] + stack_depth = 0 + + def recursive_helper(n, a, e, depth): + nonlocal stack_depth + stack_depth = max(stack_depth, depth) + stats.iterations += 1 + + # Сохраняем состояние до вычислений + intermediate_results.append({ + 'depth': depth, 'N': n, 'A': a, 'E': e, 'action': 'start' + }) + + if n < 0: + raise ValueError("N должно быть неотрицательным") + if n == 0: + return 0.0 + if a <= 0: + raise ValueError("A должно быть положительным") + + new_A = (a + n / a) / 2 + + # Сохраняем результаты вычислений + intermediate_results.append({ + 'depth': depth, 'current_A': a, 'new_A': new_A, + 'error': abs(new_A - a), 'action': 'calculated' + }) + + if abs(new_A - a) < e: + intermediate_results.append({ + 'depth': depth, 'result': new_A, 'action': 'completed' + }) + return new_A + + result = recursive_helper(n, new_A, e, depth + 1) + return result + + result = recursive_helper(N, A, E, 0) + stats.max_stack_depth = stack_depth + stats.intermediate_results = intermediate_results + return result + +@save_intermediate_results +def sqrt_recursive_decorated(N, A, E, depth=0): + """Рекурсивный алгоритм с автоматическим сбором статистики через декоратор""" + if N < 0: + raise ValueError("N должно быть неотрицательным") + if N == 0: + return 0.0 + if A <= 0: + raise ValueError("A должно быть положительным") + + new_A = (A + N / A) / 2 + + if abs(new_A - A) < E: + return new_A + + return sqrt_recursive_decorated(N, new_A, E, depth + 1) + +def sqrt_iterative_with_stats(N, A, E): + """Итеративный алгоритм со сбором статистики""" + stats.reset() + start_time = time.time() + + if N < 0: + raise ValueError("N должно быть неотрицательным") + if N == 0: + stats.execution_time = time.time() - start_time + return 0.0 + if A <= 0: + raise ValueError("A должно быть положительным") + + current_A = A + iteration = 0 + + while True: + iteration += 1 + stats.iterations = iteration + + new_A = (current_A + N / current_A) / 2 + + stats.intermediate_results.append({ + 'iteration': iteration, 'current_A': current_A, + 'new_A': new_A, 'error': abs(new_A - current_A) + }) + + if abs(new_A - current_A) < E: + break + + current_A = new_A + + stats.execution_time = time.time() - start_time + return new_A + +def analyze_sqrt_stack_limit(): + """Определение максимальной безопасной глубины рекурсии""" + original_limit = sys.getrecursionlimit() # Получаем системный лимит (обычно 1000) + max_safe_iterations = 0 + + for i in range(original_limit - 100): + try: + sqrt_recursive_decorated(2, 1, 1e-300) + max_safe_iterations = i + except RecursionError: + break + + return max_safe_iterations, original_limit + +def sqrt_performance_comparison(test_cases): + """Сравнение производительности всех алгоритмов""" + results = [] + + for N, A, E in test_cases: + print(f"\nТестирование sqrt({N}) с A={A}, E={E}:") + + # Рекурсивный с декоратором + stats.reset() + start_time = time.time() + try: + result1 = sqrt_recursive_decorated(N, A, E) + recursive_time = time.time() - start_time + print(f"Рекурсивный (декоратор): {result1:.6f}") + print(f" Время: {recursive_time:.6f}с, Итерации: {stats.iterations}, Глубина: {stats.max_stack_depth}") + except Exception as e: + print(f"Рекурсивный (декоратор): Ошибка - {e}") + recursive_time = float('inf') + + # Рекурсивный ручной + stats.reset() + start_time = time.time() + try: + result2 = manual_sqrt_recursive_with_storage(N, A, E) + manual_time = time.time() - start_time + print(f"Рекурсивный (ручной): {result2:.6f}") + print(f" Время: {manual_time:.6f}с, Итерации: {stats.iterations}, Глубина: {stats.max_stack_depth}") + except Exception as e: + print(f"Рекурсивный (ручной): Ошибка - {e}") + manual_time = float('inf') + + # Итеративный + stats.reset() + try: + result3 = sqrt_iterative_with_stats(N, A, E) + iterative_time = stats.execution_time + print(f"Итеративный: {result3:.6f}") + print(f" Время: {iterative_time:.6f}с, Итерации: {stats.iterations}") + except Exception as e: + print(f"Итеративный: Ошибка - {e}") + iterative_time = float('inf') + + # Проверка совпадения результатов + try: + if abs(result1 - result2) < E and abs(result2 - result3) < E: + print(" Все алгоритмы дали одинаковый результат") + else: + print(" Результаты различаются") + except: + print(" Невозможно сравнить результаты") + + results.append({ + 'N': N, 'A': A, 'E': E, + 'recursive_time': recursive_time, + 'manual_time': manual_time, + 'iterative_time': iterative_time, + 'iterations': stats.iterations, + 'stack_depth': stats.max_stack_depth + }) + + return results + +def print_intermediate_results(): + """Вывод промежуточных результатов""" + print("\nПромежуточные результаты:") + for i, result in enumerate(stats.intermediate_results[-5:]): + print(f" {i+1}: {result}") + +if __name__ == "__main__": + print("=" * 50) + print("АНАЛИЗ АЛГОРИТМОВ ВЫЧИСЛЕНИЯ КВАДРАТНОГО КОРНЯ") + print("=" * 50) + + + print("\n1. АНАЛИЗ ОГРАНИЧЕНИЙ РЕКУРСИИ") + max_iterations, limit = analyze_sqrt_stack_limit() + print(f"Лимит рекурсии: {limit}") + print(f"Безопасное количество итераций: {max_iterations}") + + # Тестовые данные + test_cases = [ + (25, 5, 1e-10), + (2, 1, 1e-10), + (100, 10, 1e-12), + (1000, 30, 1e-8), + (0.25, 0.5, 1e-10), + ] + + print("\n2. СРАВНЕНИЕ ПРОИЗВОДИТЕЛЬНОСТИ") + results = sqrt_performance_comparison(test_cases) + + + print("\n3. СВОДНАЯ СТАТИСТИКА") + print("Алгоритм | Время | Итерации | Глубина") + print("-" * 50) + + successful = [r for r in results if r['recursive_time'] != float('inf')] + + if successful: + recursive_times = [r['recursive_time'] for r in successful] + manual_times = [r['manual_time'] for r in successful] + iterative_times = [r['iterative_time'] for r in successful] + + print(f"Рекурсивный (декор) | {sum(recursive_times)/len(recursive_times):.6f}с | " + f"{max(r['iterations'] for r in successful):<8} | {max(r['stack_depth'] for r in successful)}") + + print(f"Рекурсивный (ручной) | {sum(manual_times)/len(manual_times):.6f}с | " + f"{max(r['iterations'] for r in successful):<8} | {max(r['stack_depth'] for r in successful)}") + + print(f"Итеративный | {sum(iterative_times)/len(iterative_times):.6f}с | " + f"{max(r['iterations'] for r in successful):<8} | -") + + + print("\n4. ДЕМОНСТРАЦИЯ ") + stats.reset() + try: + sqrt_recursive_decorated(25, 5, 1e-10) + print_intermediate_results() + except Exception as e: + print(f"Ошибка: {e}") + + print("\n" + "=" * 50) +``` + +# Контрольные вопросы +## Итеративный алгоритм +— это алгоритм, который выполняет набор инструкций (тело цикла) многократно, пока не будет выполнено некоторое условие. Он использует циклы (for, while) для повторения действий. + +Пример (вычисление факториала): + +python + +def factorial_iterative(n): + + result = 1 + + for i in range(1, n + 1): + + result *= i + + return result + ## Рекурсивный алгоритм + — это алгоритм, который вызывает сам себя в своем определении для решения более мелкой версии той же задачи. Обязательно должен быть базовый случай (условие выхода), который прекращает рекурсию. + +Пример (вычисление факториала): + +python +def factorial_recursive(n): + + if n == 0 or n == 1: # Базовый случай + + return 1 + + else: # Рекурсивный случай + + return n * factorial_recursive(n - 1) +## 2. Элементы рекурсивной функции +Два обязательных элемента: + +### Базовый случай (Base Case): +Простейший случай, который решается напрямую без рекурсивных вызовов. Его назначение — остановить рекурсию и предотвратить бесконечные вызовы (и переполнение стека). + +### Рекурсивный случай (Recursive Case): +Часть функции, в которой происходит вызов самой себя с измененными (обычно упрощенными) аргументами, приближающими решение к базовому случаю. + + ## 3. Глубина рекурсии и переполнение стека +Глубина рекурсии — это максимальное количество вложенных рекурсивных вызовов функции. Она ограничена размером стека вызовов (call stack). + +Переполнение стека (Stack Overflow) возникает, когда **глубина рекурсии превышает максимально допустимый размер стека вызовов.** Стек вызовов — это специальная область памяти, где хранится информация о каждом вызове функции (аргументы, локальные переменные, адрес возврата). При слишком глубокой рекурсии эта память исчерпывается. + +### Условия возникновения: + +Отсутствие или неправильное условие базового случая. + +Слишком большая глубина рекурсии для имеющегося размера стека. + +## 4. Сравнительная таблица: Итерация vs Рекурсия +| Критерий | Итеративный подход | Рекурсивный подход | +|----------|-------------------|-------------------| +| **Читаемость кода** | Может быть менее интуитивным для рекурсивно определяемых задач (деревья, фракталы) | Часто более лаконичный и понятный, если задача имеет естественную рекурсивную структуру | +| **Использование памяти** | Экономит память, так как не использует стек вызовов для хранения промежуточных состояний | Использует больше памяти из-за стека вызовов. Каждый вызов сохраняет свое состояние в стеке | +| **Скорость выполнения** | Обычно быстрее, так как нет накладных расходов на вызовы функций и управление стеком | Обычно медленнее из-за накладных расходов на вызовы функций и работу со стеком | + +## Почему рекурсия использует больше памяти? + +Каждый рекурсивный вызов помещает в стек вызовов новый **стековый фрейм**, содержащий: +- Аргументы функции +- Локальные переменные +- Адрес возврата + +Пока функция не завершится, этот фрейм остается в стеке. При большой глубине рекурсии эти фреймы накапливаются и потребляют значительный объем памяти. + + ## 5. Предпочтительные области применения +Рекурсия предпочтительнее для: + +Задач, рекурсивных по определению: Обход деревьев и графов (DFS), синтаксический разбор, задачи с "разделяй и властвуй" (сортировка слиянием, быстрая сортировка), Ханойские башни. + +Обоснование: Код становится гораздо проще для написания и понимания, так как он напрямую отражает рекурсивную природу задачи. + +Итерация предпочтительнее для: + +Простых линейных обработок: Вычисление суммы/произведения элементов, поиск в массиве. + +Задач с ограниченной памятью: Когда важна эффективность использования памяти. + +Обоснование: Меньшие накладные расходы, полный контроль над процессом, нет риска переполнения стека. + + ## 6. Хвостовая рекурсия и преобразование + ### Хвостовая рекурсия + — это частный случай рекурсии, при котором рекурсивный вызов является последней операцией в функции перед возвратом результата. + +### Почему она важна? +Некоторые компиляторы/интерпретаторы (например, в функциональных языках) могут выполнять оптимизацию хвостовой рекурсии (TCO). TCO преобразует хвостовую рекурсию в итеративный цикл "под капотом", что позволяет избежать роста стека вызовов и делает рекурсию столь же эффективной, как и итерация. + +Пример НЕ хвостовой рекурсии (факториал): +return n * factorial(n-1) // Умножение происходит ПОСЛЕ рекурсивного вызова. + +Пример хвостовой рекурсии (факториал с аккумулятором): + +python + +def factorial_tail(n, accumulator=1): + + if n == 0: + + return accumulator + + else: + + # Умножение происходит ДО вызова, вызов - последняя операция. + + return factorial_tail(n - 1, n * accumulator) + +### Общий алгоритм преобразования рекурсии в итерацию: + + - Создайте цикл (например, while True). + + - Замените рекурсивные случаи на обновление параметров функции (которые становятся переменными цикла). + + - Базовый случай становится условием выхода из цикла (break или return). + + - Для эмуляции стека (если рекурсия не хвостовая) используйте явную структуру данных — стек. + +### Структура данных для эмуляции рекурсии: +Чаще всего используют стек (LIFO). В него помещаются задачи, которые нужно обработать. + +Пример (обход дерева в глубину): + +python +### Рекурсивный DFS (неявный стек) +def dfs_recursive(node): + if node is None: + return + print(node.value) + dfs_recursive(node.left) + dfs_recursive(node.right) + +### Итеративный DFS (явный стек) +def dfs_iterative(root): + stack = [root] + while stack: + node = stack.pop() + if node is not None: + print(node.value) + # Сначала кладем правого, потом левого, чтобы левый обработался первым. + stack.append(node.right) + stack.append(node.left) + +### Можно ли преобразовать любую рекурсивную функцию? +Да, любую рекурсивную функцию можно преобразовать в итеративную, и наоборот. Теоретически, они эквивалентны по выразительной мощности. На практике преобразование сложной рекурсии (например, с несколькими рекурсивными вызовами) может быть нетривиальным и требовать явного управления стеком. + +## 7. Преобразования и примеры функций +Преобразование итеративного цикла в рекурсию (сумма чисел от 1 до n): + +python +### Итеративная +def sum_iterative(n): + total = 0 + for i in range(1, n + 1): + total += i + return total + +### Рекурсивная +def sum_recursive(n): + if n == 1: # Базовый случай + return 1 + else: # Рекурсивный случай + return n + sum_recursive(n - 1) +Преобразование рекурсивной функции в итерацию (печать чисел от n до 1): + +python +### Рекурсивная +def print_recursive(n): + if n >= 1: + print(n) + print_recursive(n - 1) + +### Итеративная +def print_iterative(n): + for i in range(n, 0, -1): + print(i) +Числа Фибоначчи: + +python +### Итеративная (эффективная) +def fib_iterative(n): + a, b = 0, 1 + for _ in range(n): + a, b = b, a + b + return a + +### Рекурсивная (неэффективная) +def fib_recursive(n): + if n <= 1: + return n + else: + return fib_recursive(n - 1) + fib_recursive(n - 2) + +### Проблема наивного рекурсивного Фибоначчи: +Она имеет экспоненциальную временную сложность O(2^n). Это происходит потому, что функция многократно вычисляет одни и те же значения (например, fib(3) вычисляется много раз при вычислении fib(5)). + +Решение: + +Мемоизация: Кеширование уже вычисленных результатов. + +python +from functools import lru_cache + +@lru_cache(maxsize=None) +def fib_memo(n): + if n <= 1: + return n + else: + return fib_memo(n - 1) + fib_memo(n - 2) +Итеративный подход (см. выше): Имеет линейную сложность O(n) и константную память O(1). + +Динамическое программирование: Аналогично итеративному, с сохранением всех значений в массиве. + +## 8. Другие алгоритмы и трассировка +Рекурсивный факториал (преобразование): + +python +### Рекурсивная +def factorial_recursive(n): + if n == 0: + return 1 + return n * factorial_recursive(n - 1) + +### Итеративная +def factorial_iterative(n): + result = 1 + for i in range(1, n + 1): + result *= i + return result +## Рекурсивный бинарный поиск: + +python +def binary_search(arr, low, high, target): + if low > high: # Базовый случай: элемент не найден + return -1 + + mid = (low + high) // 2 + + if arr[mid] == target: # Базовый случай: элемент найден + return mid + elif arr[mid] > target: + return binary_search(arr, low, mid - 1, target) # Рекурсивный вызов для левой части + else: + return binary_search(arr, mid + 1, high, target) # Рекурсивный вызов для правой части +## Алгоритм Евклида (НОД): + +python +def gcd(a, b): + if b == 0: # Базовый случай + return a + else: # Рекурсивный случай + return gcd(b, a % b) +Трассировка f(3) для f(n) = f(n-1) + f(n-2), где f(0)=0, f(1)=1: +Дерево вызовов: + +text + f(3) + / \ + f(2) f(1) + / \ \ + f(1) f(0) 1 + / \ +1 0 +Состояние стека (упрощенно): + +Вызов f(3). Стек: [f(3)] + +f(3) вызывает f(2). Стек: [f(3), f(2)] + +f(2) вызывает f(1). Стек: [f(3), f(2), f(1)] + +f(1) возвращает 1. Стек: [f(3), f(2)] + +f(2) вызывает f(0). Стек: [f(3), f(2), f(0)] + +f(0) возвращает 0. Стек: [f(3), f(2)] + +f(2) возвращает 1 + 0 = 1. Стек: [f(3)] + +f(3) вызывает f(1). Стек: [f(3), f(1)] + +f(1) возвращает 1. Стек: [f(3)] + +f(3) возвращает 1 + 1 = 2. Стек: [] + +Количество вызовов для f(4): Всего 9 вызовов. Можно посчитать по дереву. + +## 9. Продвинутые концепции +Прямая vs Косвенная рекурсия: + +Прямая: Функция A вызывает саму себя. + +Косвенная: Функция A вызывает функцию B, которая в свою очередь вызывает функцию A. + +python +def is_even(n): + if n == 0: + return True + else: + return is_odd(n - 1) + +def is_odd(n): + if n == 0: + return False + else: + return is_even(n - 1) + +### Мемоизация +— это техника кеширования результатов выполнения функции для предотвращения повторных вычислений с одними и теми же аргументами. Она кардинально улучшает производительность рекурсивных алгоритмов с перекрывающимися подзадачами (как числа Фибоначчи). Пример был приведен выше с @lru_cache. + +Почему обход дерева часто рекурсивный? +Структура дерева рекурсивна по своей природе: у каждого узла есть поддеревья. Рекурсивный код (visit(node); visit(node.left); visit(node.right)) интуитивно понятен и лаконичен. Итеративная реализация DFS требует явного стека, а BFS — очереди, что делает код немного более сложным. + +Всегда ли рекурсия читаемее? Нет. +Для простых циклических действий итерация часто понятнее. + +Пример (сумма массива): + +python +### Итерация (очень понятно) +total = 0 +for num in arr: + total += num + +### Рекурсия (избыточно) +def sum_arr(arr, index=0): + if index == len(arr): + return 0 + return arr[index] + sum_arr(arr, index + 1) + +## Оптимизация хвостовой рекурсии (TCO) +— это техника, при которой компилятор заменяет рекурсивный вызов в хвостовой позиции на переход (jump) к началу функции. Это переиспользует текущий стековый фрейм вместо создания нового, предотвращая рост стека. Это критически важно для функциональных языков, где рекурсия — основной способ организации циклов. В Python TCO не реализована. + +## Неявный стек +— это стек вызовов, которым управляет среда выполнения (интерпретатор/компилятор). В итеративных алгоритмах обхода деревьев мы используем явный стек (обычно список Python), которым управляем вручную, чтобы имитировать работу неявного стека при рекурсии. + + + +```python + +``` diff --git a/labs/lab_06/block_table.png b/labs/lab_06/block_table.png new file mode 100644 index 0000000..420aa81 Binary files /dev/null and b/labs/lab_06/block_table.png differ diff --git a/labs/lab_06/block_table2.png b/labs/lab_06/block_table2.png new file mode 100644 index 0000000..b0fa7ac Binary files /dev/null and b/labs/lab_06/block_table2.png differ diff --git a/labs/lab_07/Untitled.md b/labs/lab_07/Untitled.md new file mode 100644 index 0000000..c83633b --- /dev/null +++ b/labs/lab_07/Untitled.md @@ -0,0 +1,754 @@ +--- +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 +--- + +# Алгоритмы на графах + + +## Цель работы + + +Изучение основных алгоритмов на графах. + + +Асонов Сергей ИУ10-36 + + +# Задание 1 + +```python +from collections import deque + +class Graph: + def __init__(self, directed=False): + self.adjacency_list = {} + self.directed = directed + + def add_vertex(self, vertex): + if vertex not in self.adjacency_list: + self.adjacency_list[vertex] = [] + return True + return False + + def add_edge(self, vertex1, vertex2): + if vertex1 not in self.adjacency_list: + self.add_vertex(vertex1) + if vertex2 not in self.adjacency_list: + self.add_vertex(vertex2) + + if vertex2 not in self.adjacency_list[vertex1]: + self.adjacency_list[vertex1].append(vertex2) + + if not self.directed and vertex1 not in self.adjacency_list[vertex2]: + self.adjacency_list[vertex2].append(vertex1) + + def remove_edge(self, vertex1, vertex2): + if vertex1 in self.adjacency_list and vertex2 in self.adjacency_list[vertex1]: + self.adjacency_list[vertex1].remove(vertex2) + + if not self.directed and vertex2 in self.adjacency_list and vertex1 in self.adjacency_list[vertex2]: + self.adjacency_list[vertex2].remove(vertex1) + + def remove_vertex(self, vertex): + if vertex in self.adjacency_list: + for adjacent_vertex in self.adjacency_list[vertex]: + self.adjacency_list[adjacent_vertex].remove(vertex) + del self.adjacency_list[vertex] + return True + return False + + def display(self): + print("Граф (список смежности):") + for vertex in sorted(self.adjacency_list.keys()): + print(f"{vertex}: {sorted(self.adjacency_list[vertex])}") + + def dfs(self, start_vertex, target_vertex=None): + if start_vertex not in self.adjacency_list: + return [] + + visited = set() + stack = [start_vertex] + result = [] + + while stack: + current_vertex = stack.pop() + + if current_vertex not in visited: + visited.add(current_vertex) + result.append(current_vertex) + + if target_vertex and current_vertex == target_vertex: + break + + for neighbor in reversed(self.adjacency_list[current_vertex]): + if neighbor not in visited: + stack.append(neighbor) + + return result + + def bfs(self, start_vertex, target_vertex=None): + if start_vertex not in self.adjacency_list: + return [] + + visited = set() + queue = deque([start_vertex]) + result = [] + + while queue: + current_vertex = queue.popleft() + + if current_vertex not in visited: + visited.add(current_vertex) + result.append(current_vertex) + + if target_vertex and current_vertex == target_vertex: + break + + for neighbor in self.adjacency_list[current_vertex]: + if neighbor not in visited: + queue.append(neighbor) + + return result + + def find_path_dfs(self, start_vertex, target_vertex): + if start_vertex not in self.adjacency_list or target_vertex not in self.adjacency_list: + return None + + visited = set() + stack = [(start_vertex, [start_vertex])] + + while stack: + current_vertex, path = stack.pop() + + if current_vertex == target_vertex: + return path + + if current_vertex not in visited: + visited.add(current_vertex) + + for neighbor in reversed(self.adjacency_list[current_vertex]): + if neighbor not in visited: + stack.append((neighbor, path + [neighbor])) + + return None + + def find_path_bfs(self, start_vertex, target_vertex): + if start_vertex not in self.adjacency_list or target_vertex not in self.adjacency_list: + return None + + visited = set() + queue = deque([(start_vertex, [start_vertex])]) + + while queue: + current_vertex, path = queue.popleft() + + if current_vertex == target_vertex: + return path + + if current_vertex not in visited: + visited.add(current_vertex) + + for neighbor in self.adjacency_list[current_vertex]: + if neighbor not in visited: + queue.append((neighbor, path + [neighbor])) + + return None + + def get_vertices(self): + return list(self.adjacency_list.keys()) + + def get_edges(self): + edges = [] + for vertex in self.adjacency_list: + for neighbor in self.adjacency_list[vertex]: + if not self.directed: + if (neighbor, vertex) not in edges: + edges.append((vertex, neighbor)) + else: + edges.append((vertex, neighbor)) + return edges + + +def test_graph_algorithms(): + """Тест алгоритмов обхода""" + print("=== ТЕСТ: АЛГОРИТМЫ ОБХОДА ===") + g = Graph() + + + g.add_edge('A', 'B') + g.add_edge('B', 'C') + g.add_edge('C', 'D') + g.add_edge('A', 'E') + + # Тест DFS + dfs_result = g.dfs('A') + assert dfs_result[0] == 'A' # Начинается с A + assert set(dfs_result) == {'A', 'B', 'C', 'D', 'E'} # Все вершины посещены + + # Тест BFS + bfs_result = g.bfs('A') + assert bfs_result[0] == 'A' + assert set(bfs_result) == {'A', 'B', 'C', 'D', 'E'} + + # Тест поиска пути + path = g.find_path_bfs('A', 'D') + assert path == ['A', 'B', 'C', 'D'] or path == ['A', 'E', 'D'] + + print(" Тест алгоритмов обхода пройден") + + +if __name__ == "__main__": + print("=== ЗАДАНИЕ 1: КЛАСС GRAPH С АЛГОРИТМАМИ ОБХОДА ===") + + + + test_graph_algorithms() + + print("\n=== ДЕМОНСТРАЦИЯ ===") + + + g = Graph() + + + g.add_edge('A', 'B') + g.add_edge('A', 'C') + g.add_edge('B', 'D') + g.add_edge('C', 'E') + g.add_edge('D', 'E') + g.add_edge('E', 'F') + + + g.display() + + + print(f"\nDFS обход от 'A': {g.dfs('A')}") + print(f"BFS обход от 'A': {g.bfs('A')}") + + + print(f"\nПоиск пути от 'A' до 'F' (DFS): {g.find_path_dfs('A', 'F')}") + print(f"Поиск пути от 'A' до 'F' (BFS): {g.find_path_bfs('A', 'F')}") + + print("\n Все тесты пройдены успешно!") +``` + +## Задание 2 + +```python +from collections import deque + +class SimpleGraph: + def __init__(self): + self.adjacency_list = {} + + def add_vertex(self, vertex): + """Добавить вершину""" + if vertex not in self.adjacency_list: + self.adjacency_list[vertex] = [] + print(f" Вершина '{vertex}' добавлена") + else: + print(f" Вершина '{vertex}' уже существует") + + def add_edge(self, vertex1, vertex2): + """Добавить ребро между двумя вершинами""" + if vertex1 not in self.adjacency_list: + self.add_vertex(vertex1) + if vertex2 not in self.adjacency_list: + self.add_vertex(vertex2) + + if vertex2 not in self.adjacency_list[vertex1]: + self.adjacency_list[vertex1].append(vertex2) + self.adjacency_list[vertex2].append(vertex1) # Неориентированный граф + print(f" Ребро между '{vertex1}' и '{vertex2}' добавлено") + else: + print(f" Ребро между '{vertex1}' и '{vertex2}' уже существует") + + def show_graph(self): + """Показать граф""" + if not self.adjacency_list: + print("Граф пустой") + return + + print("\nТекущий граф:") + for vertex in sorted(self.adjacency_list.keys()): + neighbors = sorted(self.adjacency_list[vertex]) + print(f" {vertex} соединен с: {neighbors}") + + def bfs(self, start_vertex): + """Обход в ширину (BFS)""" + if start_vertex not in self.adjacency_list: + print(f"✗ Вершина '{start_vertex}' не найдена") + return [] + + visited = set() + queue = deque([start_vertex]) + result = [] + + print(f"\nBFS обход от '{start_vertex}':", end=" ") + + while queue: + current = queue.popleft() + if current not in visited: + visited.add(current) + result.append(current) + print(current, end=" ") + + for neighbor in self.adjacency_list[current]: + if neighbor not in visited: + queue.append(neighbor) + + print() + return result + + def dfs(self, start_vertex): + """Обход в глубину (DFS)""" + if start_vertex not in self.adjacency_list: + print(f"✗ Вершина '{start_vertex}' не найдена") + return [] + + visited = set() + stack = [start_vertex] + result = [] + + print(f"\nDFS обход от '{start_vertex}':", end=" ") + + while stack: + current = stack.pop() + if current not in visited: + visited.add(current) + result.append(current) + print(current, end=" ") + + for neighbor in reversed(self.adjacency_list[current]): + if neighbor not in visited: + stack.append(neighbor) + + print() + return result + + def find_path(self, start_vertex, target_vertex): + """Найти путь между двумя вершинами""" + if start_vertex not in self.adjacency_list: + print(f" Вершина '{start_vertex}' не найдена") + return None + if target_vertex not in self.adjacency_list: + print(f" Вершина '{target_vertex}' не найдена") + return None + + visited = set() + queue = deque([(start_vertex, [start_vertex])]) + + while queue: + current, path = queue.popleft() + + if current == target_vertex: + print(f" Путь от '{start_vertex}' до '{target_vertex}': {' -> '.join(path)}") + return path + + if current not in visited: + visited.add(current) + + for neighbor in self.adjacency_list[current]: + if neighbor not in visited: + queue.append((neighbor, path + [neighbor])) + + print(f" Путь от '{start_vertex}' до '{target_vertex}' не найден") + return None + +def test_simple_graph(): + """Простой тест графа""" + print("=== ПРОСТОЙ ТЕСТ ===") + g = SimpleGraph() + + # Создаем простой граф + g.add_vertex('A') + g.add_vertex('B') + g.add_vertex('C') + g.add_edge('A', 'B') + g.add_edge('B', 'C') + + # Проверяем обходы + g.bfs('A') + g.dfs('A') + g.find_path('A', 'C') + + print(" тест завершен") + +def test_path_finding(): + """Тест поиска пути""" + print("\n=== ТЕСТ ПОИСКА ПУТИ ===") + g = SimpleGraph() + + # Создаем граф: A-B-C-D, A-E + g.add_edge('A', 'B') + g.add_edge('B', 'C') + g.add_edge('C', 'D') + g.add_edge('A', 'E') + + + path = g.find_path('A', 'D') + assert path is not None, "Путь должен существовать" + assert path[0] == 'A', "Начало пути должно быть A" + assert path[-1] == 'D', "Конец пути должен быть D" + + print(" Тест поиска пути завершен") + +def main(): + """Главное меню приложения""" + graph = SimpleGraph() + + print("=== ПРИЛОЖЕНИЕ ДЛЯ РАБОТЫ С ГРАФАМИ ===") + print("Создавайте графы и изучайте алгоритмы обхода!") + + # Создаем пример графа + print("\nСоздаем пример графа...") + graph.add_edge('Москва', 'Санкт-Петербург') + graph.add_edge('Москва', 'Казань') + graph.add_edge('Санкт-Петербург', 'Псков') + graph.add_edge('Казань', 'Уфа') + graph.show_graph() + + while True: + print("\n" + "="*50) + print("ГЛАВНОЕ МЕНЮ:") + print("1. Показать граф") + print("2. Добавить вершину") + print("3. Добавить ребро") + print("4. Обход в ширину (BFS)") + print("5. Обход в глубину (DFS)") + print("6. Найти путь") + print("7. Создать новый пример") + print("0. Выход") + print("="*50) + + choice = input("Выберите действие (0-7): ").strip() + + if choice == "1": + graph.show_graph() + + elif choice == "2": + vertex = input("Введите имя вершины: ").strip() + graph.add_vertex(vertex) + + elif choice == "3": + v1 = input("Введите первую вершину: ").strip() + v2 = input("Введите вторую вершину: ").strip() + graph.add_edge(v1, v2) + + elif choice == "4": + start = input("Введите начальную вершину: ").strip() + graph.bfs(start) + + elif choice == "5": + start = input("Введите начальную вершину: ").strip() + graph.dfs(start) + + elif choice == "6": + start = input("Введите начальную вершину: ").strip() + target = input("Введите конечную вершину: ").strip() + graph.find_path(start, target) + + elif choice == "7": + graph = SimpleGraph() + print("Создан новый пустой граф") + + elif choice == "0": + print("Пока!") + break + + else: + print("Неверный выбор. Попробуйте снова.") + +if __name__ == "__main__": + + test_simple_graph() + test_path_finding() + + print("\n Все тесты пройдены!") + print("Запуск приложения...\n") + + + main() +``` + +## Задание 3 + + +## Задание 4: Поиск кратчайшего пути BFS (1→6) + +```python +import heapq + +def dijkstra(graph, start, end): + distances = {node: float('inf') for node in graph} + distances[start] = 0 + previous = {node: None for node in graph} + queue = [(0, start)] + + while queue: + current_distance, current_node = heapq.heappop(queue) + + if current_distance > distances[current_node]: + continue + + if current_node == end: + break + + for neighbor, weight in graph.get(current_node, []): + distance = current_distance + weight + if distance < distances[neighbor]: + distances[neighbor] = distance + previous[neighbor] = current_node + heapq.heappush(queue, (distance, neighbor)) + + # Восстанавливаем путь + path = [] + current = end + while current is not None: + path.append(current) + current = previous[current] + path.reverse() + + if distances[end] == float('inf'): + return None, float('inf') + return path, distances[end] + + +graph = { + 1: [(2, 1), (4, 8), (5, 25), (7, 20)], + 2: [(3, 2), (7, 15)], + 3: [(6, 3)], + 4: [(5, 9)], + 5: [(7, 6)], + 6: [(7, 4)], + 7: [] +} + +start_vertex = 1 +end_vertex = 6 + +path, distance = dijkstra(graph, start_vertex, end_vertex) + +if path: + print(f"Кратчайший путь из {start_vertex} в {end_vertex}: {' -> '.join(map(str, path))}") + print(f"Длина пути: {distance}") +else: + print(f"Пути из {start_vertex} в {end_vertex} не существует") +``` + +## Задание 5: Алгоритм Дейкстры (2→8) + +```python +import heapq + +def dijkstra(graph, start, end): + distances = {node: float('inf') for node in graph} + distances[start] = 0 + previous = {node: None for node in graph} + queue = [(0, start)] + + while queue: + current_distance, current_node = heapq.heappop(queue) + + if current_distance > distances[current_node]: + continue + + if current_node == end: + break + + for neighbor, weight in graph[current_node]: + distance = current_distance + weight + if distance < distances[neighbor]: + distances[neighbor] = distance + previous[neighbor] = current_node + heapq.heappush(queue, (distance, neighbor)) + + path = [] + current = end + while current is not None: + path.append(current) + current = previous[current] + path.reverse() + + return path, distances[end] + + +graph = { + 1: [(2, 12), (7, 2), (3, 20)], + 2: [(1, 12), (8, 12)], + 3: [(1, 20), (8, 3), (4, 17), (5, 12)], + 4: [(3, 17), (5, 5), (7, 11), (6, 6)], + 5: [(3, 12), (4, 5), (7, 16), (6, 13)], + 6: [(4, 6), (5, 13), (7, 4), (8, 17)], + 7: [(1, 2), (4, 11), (5, 16), (6, 4)], + 8: [(2, 12), (3, 3), (6, 17)] +} + +start_vertex = 2 +end_vertex = 8 + +path, distance = dijkstra(graph, start_vertex, end_vertex) + +print(f"Кратчайший путь из {start_vertex} в {end_vertex}: {' -> '.join(map(str, path))}") +print(f"Длина пути: {distance}") +``` + +## Задание 6 + +```python +import heapq + +class DijkstraAlgorithm: + def __init__(self, graph): + """ + Инициализация алгоритма Дейкстры + + Args: + graph (dict): Граф в формате {вершина: [(сосед, вес), ...]} + """ + self.graph = graph + self.distances = {} + self.previous = {} + self.visited = set() + + def find_shortest_path(self, start, end): + """ + Поиск кратчайшего пути между вершинами + + Args: + start: Начальная вершина + end: Конечная вершина + + Returns: + tuple: (путь, длина) или (None, inf) если путь не существует + """ + """ Проверка существования вершин """ + if start not in self.graph or end not in self.graph: + return None, float('inf') + + """ Инициализация""" + self.distances = {node: float('inf') for node in self.graph} + self.previous = {node: None for node in self.graph} + self.distances[start] = 0 + self.visited = set() + + """ Очередь с приоритетом (мин-куча) """ + priority_queue = [(0, start)] + + while priority_queue: + current_distance, current_node = heapq.heappop(priority_queue) + + """ Пропускаем устаревшие записи """ + if current_distance > self.distances[current_node]: + continue + + + if current_node == end: + break + + self.visited.add(current_node) + + """ Обработка соседей текущей вершины """ + for neighbor, weight in self.graph.get(current_node, []): + if neighbor in self.visited: + continue + + new_distance = current_distance + weight + + """ Обновление расстояния если нашли короче """ + if new_distance < self.distances[neighbor]: + self.distances[neighbor] = new_distance + self.previous[neighbor] = current_node + heapq.heappush(priority_queue, (new_distance, neighbor)) + + return self._reconstruct_path(start, end) + + def _reconstruct_path(self, start, end): + """Восстановление пути от конечной вершины к начальной""" + if self.distances[end] == float('inf'): + return None, float('inf') + + path = [] + current = end + + """ Восстанавливаем путь в обратном порядке """ + while current is not None: + path.append(current) + current = self.previous[current] + + path.reverse() + + + if path[0] != start: + return None, float('inf') + + return path, self.distances[end] + + def print_path_info(self, start, end): + """Вывод информации о пути""" + path, distance = self.find_shortest_path(start, end) + + if path: + print(f"Кратчайший путь из {start} в {end}:") + print(" → ".join(map(str, path))) + print(f"Длина пути: {distance}") + print(f"Количество рёбер: {len(path) - 1}") + else: + print(f"Пути из {start} в {end} не существует") + + + +graph_task5 = { + 1: [(2, 12), (7, 2), (3, 20)], + 2: [(1, 12), (8, 12)], + 3: [(1, 20), (8, 3), (4, 17), (5, 12)], + 4: [(3, 17), (5, 5), (7, 11), (6, 6)], + 5: [(3, 12), (4, 5), (7, 16), (6, 13)], + 6: [(4, 6), (5, 13), (7, 4), (8, 17)], + 7: [(1, 2), (4, 11), (5, 16), (6, 4)], + 8: [(2, 12), (3, 3), (6, 17)] +} + +def main(): + print("Алгоритм Дейкстры - поиск кратчайшего пути") + print("=" * 50) + + + dijkstra = DijkstraAlgorithm(graph_task5) + + + start_vertex = 2 + end_vertex = 8 + + print(f"Анализ графа:") + print(f"Вершины: {list(graph_task5.keys())}") + print(f"Рёбра:") + for node in sorted(graph_task5.keys()): + for neighbor, weight in graph_task5[node]: + print(f" {node} → {neighbor} (вес: {weight})") + print() + + + dijkstra.print_path_info(start_vertex, end_vertex) + + + print(f"\nРасстояния от вершины {start_vertex}:") + for node in sorted(dijkstra.distances.keys()): + dist = dijkstra.distances[node] + if dist == float('inf'): + print(f" до {node}: недостижима") + else: + print(f" до {node}: {dist}") + +if __name__ == "__main__": + main() +``` diff --git a/labs/lab_08/Untitled.md b/labs/lab_08/Untitled.md new file mode 100644 index 0000000..e54cc00 --- /dev/null +++ b/labs/lab_08/Untitled.md @@ -0,0 +1,579 @@ +--- +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 +--- + +# Лабораторная работа 8 + + +## Задание 1: Бинарное дерево поиска + +```python +class Node: + def __init__(self, value): + self.value = value + self.left = None + self.right = None + +class BinarySearchTree: + def __init__(self): + self.root = None + + def is_empty(self): + return self.root is None + + def insert(self, value): + if self._is_valid_value(value): + self.root = self._insert_recursive(self.root, value) + return True + return False + + def _insert_recursive(self, node, value): + if node is None: + return Node(value) + + if value < node.value: + node.left = self._insert_recursive(node.left, value) + elif value > node.value: + node.right = self._insert_recursive(node.right, value) + + return node + + def search(self, value): + if not self._is_valid_value(value): + return None + + return self._search_recursive(self.root, value) + + def _search_recursive(self, node, value): + if node is None or node.value == value: + return node + + if value < node.value: + return self._search_recursive(node.left, value) + else: + return self._search_recursive(node.right, value) + + def delete(self, value): + if not self._is_valid_value(value) or self.is_empty(): + return False + + if self.search(value) is None: + return False + + self.root = self._delete_recursive(self.root, value) + return True + + def _delete_recursive(self, node, value): + if node is None: + return node + + if value < node.value: + node.left = self._delete_recursive(node.left, value) + elif value > node.value: + node.right = self._delete_recursive(node.right, value) + else: + if node.left is None: + return node.right + elif node.right is None: + return node.left + temp = self._min_value_node(node.right) + node.value = temp.value + node.right = self._delete_recursive(node.right, temp.value) + + return node + + def _min_value_node(self, node): + current = node + while current.left is not None: + current = current.left + return current + + def preorder_traversal(self): + result = [] + self._preorder_recursive(self.root, result) + return result + + def _preorder_recursive(self, node, result): + if node: + result.append(node.value) + self._preorder_recursive(node.left, result) + self._preorder_recursive(node.right, result) + + def inorder_traversal(self): + result = [] + self._inorder_recursive(self.root, result) + return result + + def _inorder_recursive(self, node, result): + if node: + self._inorder_recursive(node.left, result) + result.append(node.value) + self._inorder_recursive(node.right, result) + + def postorder_traversal(self): + result = [] + self._postorder_recursive(self.root, result) + return result + + def _postorder_recursive(self, node, result): + if node: + self._postorder_recursive(node.left, result) + self._postorder_recursive(node.right, result) + result.append(node.value) + + def height(self): + return self._height_recursive(self.root) + + def _height_recursive(self, node): + if node is None: + return -1 + + left_height = self._height_recursive(node.left) + right_height = self._height_recursive(node.right) + + return max(left_height, right_height) + 1 + + def print_tree(self): + if self.is_empty(): + print("Дерево пустое") + return + + lines = self._build_tree_display() + for line in lines: + print(line) + + def _build_tree_display(self): + if self.root is None: + return [] + + def _build_recursive(node, prefix="", is_left=True): + if node is None: + return [] + + result = [] + line = prefix + ("└── " if is_left else "├── ") + str(node.value) + result.append(line) + + children = [] + if node.left or node.right: + if node.left: + children.append((node.left, True)) + if node.right: + children.append((node.right, False)) + + for i, (child, is_left_child) in enumerate(children): + child_prefix = prefix + (" " if is_left else "│ ") + child_lines = _build_recursive(child, child_prefix, is_left_child) + result.extend(child_lines) + + return result + + return _build_recursive(self.root, "", True) + + def _is_valid_value(self, value): + return value is not None + + + +bst = BinarySearchTree() + +print("Дерево пустое?", bst.is_empty()) + +values = [50, 30, 70, 20, 40, 60, 80] +for value in values: + bst.insert(value) + +print("\nПосле добавления элементов:") +print("Дерево пустое?", bst.is_empty()) + +print("\nСтруктура дерева:") +bst.print_tree() + +print("\nПрямой обход:", bst.preorder_traversal()) +print("Симметричный обход:", bst.inorder_traversal()) +print("Обратный обход:", bst.postorder_traversal()) + +search_value = 40 +found = bst.search(search_value) +print(f"\nПоиск значения {search_value}: {'найден' if found else 'не найден'}") + +print(f"Высота дерева: {bst.height()}") + +delete_value = 30 +bst.delete(delete_value) +print(f"\nУдаление значения {delete_value}: успешно") + +print("\nСтруктура дерева после удаления:") +bst.print_tree() + +print("\nСимметричный обход после удаления:", bst.inorder_traversal()) +print(f"Высота дерева после удаления: {bst.height()}") +``` + +## Задание 2: AVL-дерево + +```python +class AVLNode: + def __init__(self, value): + self.value = value + self.left = None + self.right = None + self.height = 1 + +class AVLTree: + def __init__(self): + self.root = None + + def is_empty(self): + return self.root is None + + def _get_height(self, node): + if node is None: + return 0 + return node.height + + def _get_balance(self, node): + if node is None: + return 0 + return self._get_height(node.left) - self._get_height(node.right) + + def _update_height(self, node): + if node is not None: + node.height = 1 + max(self._get_height(node.left), + self._get_height(node.right)) + + def _rotate_right(self, y): + x = y.left + T2 = x.right + + x.right = y + y.left = T2 + + self._update_height(y) + self._update_height(x) + + return x + + def _rotate_left(self, x): + y = x.right + T2 = y.left + + y.left = x + x.right = T2 + + self._update_height(x) + self._update_height(y) + + return y + + def insert(self, value): + if self._is_valid_value(value): + self.root = self._insert_recursive(self.root, value) + return True + return False + + def _insert_recursive(self, node, value): + if node is None: + return AVLNode(value) + + if value < node.value: + node.left = self._insert_recursive(node.left, value) + elif value > node.value: + node.right = self._insert_recursive(node.right, value) + else: + return node + + self._update_height(node) + + balance = self._get_balance(node) + + + if balance > 1 and value < node.left.value: + return self._rotate_right(node) + + if balance < -1 and value > node.right.value: + return self._rotate_left(node) + + if balance > 1 and value > node.left.value: + node.left = self._rotate_left(node.left) + return self._rotate_right(node) + + if balance < -1 and value < node.right.value: + node.right = self._rotate_right(node.right) + return self._rotate_left(node) + + return node + + def search(self, value): + if not self._is_valid_value(value): + return None + + return self._search_recursive(self.root, value) + + def _search_recursive(self, node, value): + if node is None or node.value == value: + return node + + if value < node.value: + return self._search_recursive(node.left, value) + else: + return self._search_recursive(node.right, value) + + def delete(self, value): + if not self._is_valid_value(value) or self.is_empty(): + return False + + if self.search(value) is None: + return False + + self.root = self._delete_recursive(self.root, value) + return True + + def _delete_recursive(self, node, value): + if node is None: + return node + + if value < node.value: + node.left = self._delete_recursive(node.left, value) + elif value > node.value: + node.right = self._delete_recursive(node.right, value) + else: + if node.left is None: + return node.right + elif node.right is None: + return node.left + + temp = self._min_value_node(node.right) + node.value = temp.value + node.right = self._delete_recursive(node.right, temp.value) + + if node is None: + return node + + self._update_height(node) + + balance = self._get_balance(node) + + if balance > 1 and self._get_balance(node.left) >= 0: + return self._rotate_right(node) + + if balance > 1 and self._get_balance(node.left) < 0: + node.left = self._rotate_left(node.left) + return self._rotate_right(node) + + if balance < -1 and self._get_balance(node.right) <= 0: + return self._rotate_left(node) + + if balance < -1 and self._get_balance(node.right) > 0: + node.right = self._rotate_right(node.right) + return self._rotate_left(node) + + return node + + def _min_value_node(self, node): + current = node + while current.left is not None: + current = current.left + return current + + def preorder_traversal(self): + result = [] + self._preorder_recursive(self.root, result) + return result + + def _preorder_recursive(self, node, result): + if node: + result.append(node.value) + self._preorder_recursive(node.left, result) + self._preorder_recursive(node.right, result) + + def inorder_traversal(self): + result = [] + self._inorder_recursive(self.root, result) + return result + + def _inorder_recursive(self, node, result): + if node: + self._inorder_recursive(node.left, result) + result.append(node.value) + self._inorder_recursive(node.right, result) + + def postorder_traversal(self): + result = [] + self._postorder_recursive(self.root, result) + return result + + def _postorder_recursive(self, node, result): + if node: + self._postorder_recursive(node.left, result) + self._postorder_recursive(node.right, result) + result.append(node.value) + + def height(self): + return self._get_height(self.root) + + def print_tree(self): + if self.is_empty(): + print("Дерево пустое") + return + + lines = self._build_tree_display() + for line in lines: + print(line) + + def _build_tree_display(self): + if self.root is None: + return [] + + def _build_recursive(node, prefix="", is_left=True): + if node is None: + return [] + + balance = self._get_balance(node) + node_info = f"{node.value}(h:{node.height},b:{balance})" + + result = [] + line = prefix + ("└── " if is_left else "├── ") + node_info + result.append(line) + + children = [] + if node.left or node.right: + if node.left: + children.append((node.left, True)) + if node.right: + children.append((node.right, False)) + + for i, (child, is_left_child) in enumerate(children): + child_prefix = prefix + (" " if is_left else "│ ") + child_lines = _build_recursive(child, child_prefix, is_left_child) + result.extend(child_lines) + + return result + + return _build_recursive(self.root, "", True) + + def _is_valid_value(self, value): + return value is not None + + def is_balanced(self): + return self._check_balanced(self.root) + + def _check_balanced(self, node): + if node is None: + return True + + balance = self._get_balance(node) + if abs(balance) > 1: + return False + + return self._check_balanced(node.left) and self._check_balanced(node.right) + + +avl = AVLTree() + +print("AVL-дерево пустое?", avl.is_empty()) + +print("\nДобавление элементов в AVL-дерево:") +values = [10, 20, 30, 40, 50, 25] + +for value in values: + avl.insert(value) + print(f"После добавления {value}:") + print("Сбалансировано:", avl.is_balanced()) + print("Высота:", avl.height()) + +print("\nСтруктура AVL-дерева:") +avl.print_tree() + +print("\nПрямой обход:", avl.preorder_traversal()) +print("Симметричный обход (отсортированный):", avl.inorder_traversal()) +print("Обратный обход:", avl.postorder_traversal()) + +search_value = 30 +found = avl.search(search_value) +print(f"\nПоиск значения {search_value}: {'найден' if found else 'не найден'}") + +print(f"Высота дерева: {avl.height()}") +print(f"Дерево сбалансировано: {avl.is_balanced()}") + +delete_value = 30 +print(f"\nУдаление значения {delete_value}: {'успешно' if avl.delete(delete_value) else 'не удалось'}") + +print("\nСтруктура AVL-дерева после удаления:") +avl.print_tree() + +print("\nСимметричный обход после удаления:", avl.inorder_traversal()) +print(f"Высота дерева после удаления: {avl.height()}") +print(f"Дерево сбалансировано после удаления: {avl.is_balanced()}") + +print("\n" + "="*50) +print("Демонстрация балансировки AVL-дерева:") + +critical_avl = AVLTree() +critical_values = [1, 2, 3, 4, 5, 6, 7] + +print("\nДобавление последовательности, создающей вырожденное дерево в обычном BST:") +for value in critical_values: + critical_avl.insert(value) + print(f"После {value}: высота = {critical_avl.height()}, сбалансировано = {critical_avl.is_balanced()}") + +print("\nИтоговая структура AVL-дерева:") +critical_avl.print_tree() +print(f"Высота сбалансированного AVL-дерева: {critical_avl.height()}") +print(f"Высота вырожденного BST для тех же данных: {len(critical_values) - 1}") +``` + +## Задание 3 + + +```python +class TreeNode: + def __init__(self, val=0, left=None, right=None): + self.val = val + self.left = left + self.right = right + +def find_unbalanced_vertices(root): + result = [] + + def count_nodes(node): + if not node: + return 0 + + left = count_nodes(node.left) + right = count_nodes(node.right) + + if left != right: + result.append(node.val) + + return left + right + 1 + + count_nodes(root) + return result + +# ПРОВЕРКА +root = TreeNode(1) +root.left = TreeNode(2) +root.right = TreeNode(3) +root.left.left = TreeNode(4) +root.right.left = TreeNode(5) +root.right.right = TreeNode(6) +root.right.left.left = TreeNode(7) + +vertices = find_unbalanced_vertices(root) +print("Результат:", vertices) +``` + +```python + +``` diff --git a/labs/lab_09/Untitled.md b/labs/lab_09/Untitled.md new file mode 100644 index 0000000..f52f53c --- /dev/null +++ b/labs/lab_09/Untitled.md @@ -0,0 +1,225 @@ +--- +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 +--- + +# Лабораторная работа 9 + +```python +import random +import math +from typing import Tuple, List + +class CryptoSystem: + def __init__(self): + self.caesar_key = None + self.bg_public_key = None + self.bg_private_key = None + + def generate_key_from_seed(self, seed_key: str) -> int: + """Генерация числового ключа на основе произвольного ключа пользователя""" + key_hash = 0 + for char in seed_key: + key_hash = (key_hash * 31 + ord(char)) % 1000000 + return max(1, key_hash % 95) # Для шифра Цезаря (0-94 для печатных символов) + + def caesar_encrypt(self, text: str, seed_key: str) -> str: + """Шифрование методом Цезаря""" + self.caesar_key = self.generate_key_from_seed(seed_key) + encrypted = [] + + for char in text: + if 32 <= ord(char) <= 126: # Печатные ASCII символы + new_char_code = 32 + (ord(char) - 32 + self.caesar_key) % 95 + encrypted.append(chr(new_char_code)) + else: + encrypted.append(char) + + return ''.join(encrypted) + + def caesar_decrypt(self, encrypted_text: str, seed_key: str) -> str: + """Дешифрование методом Цезаря""" + key = self.generate_key_from_seed(seed_key) + decrypted = [] + + for char in encrypted_text: + if 32 <= ord(char) <= 126: + new_char_code = 32 + (ord(char) - 32 - key) % 95 + decrypted.append(chr(new_char_code)) + else: + decrypted.append(char) + + return ''.join(decrypted) + + def generate_blum_goldwasser_keys(self, seed_key: str) -> Tuple[int, int]: + """Генерация ключей для криптосистемы Блюма-Гольдвассер""" + # Используем ключ пользователя для детерминистической генерации + random.seed(sum(ord(c) for c in seed_key)) + + # Генерация двух простых чисел вида 4k+3 + def generate_blum_prime(): + while True: + p = random.randint(100, 1000) + if self.is_prime(p) and p % 4 == 3: + return p + + p = generate_blum_prime() + q = generate_blum_prime() + + n = p * q + self.bg_public_key = n + self.bg_private_key = (p, q) + + return n, (p, q) + + def is_prime(self, n: int) -> bool: + """Проверка числа на простоту""" + if n < 2: + return False + for i in range(2, int(math.sqrt(n)) + 1): + if n % i == 0: + return False + return True + + def bg_encrypt(self, text: str, seed_key: str) -> Tuple[List[int], int]: + """Шифрование методом Блюма-Гольдвассера""" + n, _ = self.generate_blum_goldwasser_keys(seed_key) + + # Преобразование текста в биты + bits = [] + for char in text: + bits.extend([int(b) for b in format(ord(char), '08b')]) + + # Генерация псевдослучайной последовательности + x = random.randint(2, n-1) + cipher_bits = [] + + for bit in bits: + x = (x * x) % n + pseudo_random_bit = x % 2 + cipher_bits.append(bit ^ pseudo_random_bit) + + return cipher_bits, x # Возвращаем шифртекст и последнее состояние x + + def bg_decrypt(self, cipher_bits: List[int], final_x: int, seed_key: str) -> str: + """Дешифрование методом Блюма-Гольдвассера""" + _, (p, q) = self.generate_blum_goldwasser_keys(seed_key) + n = p * q + + # Восстановление псевдослучайной последовательности + x = final_x + original_bits = [] + + for cipher_bit in cipher_bits: + x = (x * x) % n + pseudo_random_bit = x % 2 + original_bits.append(cipher_bit ^ pseudo_random_bit) + + # Преобразование битов обратно в текст + text = "" + for i in range(0, len(original_bits), 8): + if i + 8 <= len(original_bits): + byte_bits = original_bits[i:i+8] + char_code = int(''.join(map(str, byte_bits)), 2) + text += chr(char_code) + + return text + + def demo(self): + """Демонстрация работы обоих методов""" + print("=== ДЕМОНСТРАЦИЯ ШИФРОВАНИЯ ===\n") + + # Исходный текст + original_text = "Hello, World! 123" + seed_key = "фф12К52" # Пример ключа из задания + + print(f"Исходный текст: {original_text}") + print(f"Ключ: {seed_key}\n") + + # Шифр Цезаря + print("=== ШИФР ЦЕЗАРЯ ===") + caesar_encrypted = self.caesar_encrypt(original_text, seed_key) + caesar_decrypted = self.caesar_decrypt(caesar_encrypted, seed_key) + + print(f"Зашифрованный: {caesar_encrypted}") + print(f"Расшифрованный: {caesar_decrypted}") + print(f"Идентичны: {original_text == caesar_decrypted}\n") + + # Криптосистема Блюма-Гольдвассера + print("=== КРИПТОСИСТЕМА БЛЮМА-ГОЛЬДВАССЕРА ===") + bg_cipher_bits, final_x = self.bg_encrypt(original_text, seed_key) + bg_decrypted = self.bg_decrypt(bg_cipher_bits, final_x, seed_key) + + print(f"Зашифрованный (биты): {bg_cipher_bits[:32]}...") # Показываем первые 32 бита + print(f"Расшифрованный: {bg_decrypted}") + print(f"Идентичны: {original_text == bg_decrypted}\n") + + # Сводка + print("=== РЕЗУЛЬТАТ ===") + print(f"Цезарь: {'Успешно' if original_text == caesar_decrypted else 'Не успешно'}") + print(f"Блюм-Гольдвассер: {'Успешно' if original_text == bg_decrypted else 'Не успешно'}") + +# Дополнительная утилита для проверки работы +def test_crypto_system(): + """Тестирование криптосистемы""" + crypto = CryptoSystem() + + test_cases = [ + "Hello, World!", + "Test 123", + "Простой текст", + "Short", + "A" * 10 # Повторяющиеся символы + ] + + seed_key = "фф12К52" + + print("=== ТЕСТИРОВАНИЕ ===\n") + + for i, text in enumerate(test_cases, 1): + print(f"Тест {i}: '{text}'") + + # Тест Цезаря + encrypted = crypto.caesar_encrypt(text, seed_key) + decrypted = crypto.caesar_decrypt(encrypted, seed_key) + caesar_ok = text == decrypted + + # Тест Блюма-Гольдвассера + bg_cipher, final_x = crypto.bg_encrypt(text, seed_key) + bg_decrypted = crypto.bg_decrypt(bg_cipher, final_x, seed_key) + bg_ok = text == bg_decrypted + + print(f" Цезарь: {'Успешно' if caesar_ok else 'Не успешно'}") + print(f" Блюм-Гольдвассер: {'Успешно' if bg_ok else 'Не успешно'}") + + if not caesar_ok or not bg_ok: + print(f" Ошибка! Исходный: '{text}'") + if not caesar_ok: + print(f" Цезарь расшифровал: '{decrypted}'") + if not bg_ok: + print(f" БГ расшифровал: '{bg_decrypted}'") + print() + +if __name__ == "__main__": + # Основная демонстрация + crypto = CryptoSystem() + crypto.demo() + + # Дополнительное тестирование + test_crypto_system() + + +``` + +```python + +```