Структурное программирование
Структурное программирование — это не просто набор правил, а философия, которая превращает хаос кода в организованную архитектуру, понятную не только компьютеру, но и другим людям.
Введение: Когда код становится архитектурой
Представь, что тебе нужно построить небоскрёб. Можно ли просто начать складывать кирпичи один на другой, надеясь, что всё как-нибудь само сложится? Конечно, нет. Нужен чертёж, фундамент, каркас, система коммуникаций. То же самое и с программами: когда они разрастаются до тысяч строк кода, без продуманной архитектуры превращаются в хаос.
💡 Определение
Структурное программирование — технология разработки программного обеспечения, в основе которой лежит представление программы в виде иерархической структуры логически целостных фрагментов (блоков).
Исторический контекст: Революция Дейкстры
В начале 1960-х программирование было похоже на дикий запад. Программисты использовали команду goto, что превращало код в «спагетти»: следить за логикой было почти невозможно.
От «кодовых спагетти» к структурированным алгоритмам: революция читаемости
📜 Прорыв 1968 года
Нидерландский учёный Эдсгер Дейкстра опубликовал знаменитую статью «Go To Statement Considered Harmful».
Его идея: любую программу можно построить из трёх базовых конструкций — последовательность, ветвление, цикл — без использования goto.
🎯 Почему это важно для тебя?
- Современные языки (Python, JavaScript, C) построены на этих принципах
- Без структурного подхода невозможно работать в команде
- Язык C стал одним из первых массовых языков, реализовавших идеи структурного программирования
Пять принципов структурного программирования
Эти принципы — фундамент современной разработки ПО. Понимание их делает тебя не просто кодером, а инженером.
Принцип 1Три базовые конструкции
Любую задачу можно решить, комбинируя:
- Последовательность — шаги один за другим
- Ветвление — выбор пути:
if (...) {...} else {...} - Цикл — повторение:
while (...),for (...)
Почему три? Это минимальный набор, достаточный для выражения любой вычислимой функции (доказано математически!).
Принцип 2Вложенность конструкций
Можно вкладывать циклы в условия, условия в циклы произвольным образом.
Аналогия: Как в матрёшке — одна кукла внутри другой, но каждая сама по себе целостный объект.
Принцип 3Функции
Если фрагмент кода повторяется или логически целостен, выноси его в отдельную функцию.
Пример из жизни: Ты не описываешь каждый раз, как открыть дверь, а говоришь: «Открой дверь». Это — функция.
Принцип 4Один вход, один выход
Каждый блок (функция, цикл, условие) должен иметь чётко определённое начало и конец.
Это упрощает отладку: ты точно знаешь, где что начинается и заканчивается.
Принцип 5Метод «сверху вниз»
Сначала пиши общую структуру программы, используя «заглушки» (пустые функции). Затем постепенно заполняй детали.
Аналогия: Сначала пишешь план эссе, потом раскрываешь каждый пункт.
Метод «сверху вниз»: от общего замысла к деталям, сохраняя видимость целого
Функции: кирпичики сложности
Примеры ниже — на Си, Паскале и Python: у каждого есть вкладки, выбери свой язык.
Функция — это логически целостный фрагмент кода, который решает подзадачу. Вместо копирования формулы три раза, создаём функцию.
✨ Преимущества функций
- Переиспользование: Написал один раз — используешь многократно
- Читаемость: Вместо формулы — понятное имя функции
- Отладка: Ошибка ищется в одном месте
📋 Синтаксис функции
тип_результата имя_функции(параметры) {
// тело функции
return результат;
}
function имя_функции(параметры): тип_результата;
begin
{ тело функции }
имя_функции := результат;
end;
def имя_функции(параметры):
# тело функции
return результат
💡 Пример: Вычисление длины отрезка
Формула из геометрии для вычисления длины отрезка по координатам концов:
#include <math.h>
double dlina_otrezka(double x1, double y1, double x2, double y2) {
return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
function dlina_otrezka(x1, y1, x2, y2: real): real;
begin
dlina_otrezka := sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
end;
import math
def dlina_otrezka(x1, y1, x2, y2):
return math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
Функции — модульные блоки, которые легко комбинировать благодаря чётким интерфейсам
Прототипы функций и порядок объявления
Функцию нужно объявить до её использования. В Си, если функция определена после main(), пишут прототип; в Паскале для того же служит слово forward; в Python функция должна быть описана раньше, чем выполнится её вызов.
🔍 Структура программы с прототипом
#include <stdio.h>
#include <math.h>
// Прототип функции (объявление)
double dlina_otrezka(double x1, double y1, double x2, double y2);
int main() {
double d = dlina_otrezka(0, 0, 3, 4);
printf("Длина: %.2f\n", d); // Выведет: 5.00
return 0;
}
// Определение функции (реализация)
double dlina_otrezka(double x1, double y1, double x2, double y2) {
return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
program Segment;
{ предварительное объявление — аналог прототипа в Си }
function dlina_otrezka(x1, y1, x2, y2: real): real; forward;
procedure run;
var
d: real;
begin
d := dlina_otrezka(0, 0, 3, 4);
writeln('Длина: ', d:0:2); { выведет: 5.00 }
end;
{ определение функции }
function dlina_otrezka(x1, y1, x2, y2: real): real;
begin
dlina_otrezka := sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
end;
begin
run;
end.
import math
# в Python функция должна быть описана до того, как её вызов выполнится
def dlina_otrezka(x1, y1, x2, y2):
return math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
d = dlina_otrezka(0, 0, 3, 4)
print(f"Длина: {d:.2f}") # выведет: 5.00
Параметры: передача по значению и по ссылке
Обычно параметры передаются по значению: подпрограмма получает копию. Чтобы подпрограмма могла изменить саму переменную, в Си передают её адрес — указатель, в Паскале описывают параметр словом var. В Python так изменить целое число нельзя: функция возвращает новое значение, и его присваивают.
📋 Передача по значению
Изменяется только локальная копия:
void increment(int x) {
x = x + 1; // изменяется копия
}
int main() {
int a = 5;
increment(a);
printf("%d\n", a); // Выведет: 5
return 0;
}
procedure increment(x: integer);
begin
x := x + 1; { изменяется копия }
end;
var
a: integer;
begin
a := 5;
increment(a);
writeln(a); { выведет: 5 }
end.
def increment(x):
x = x + 1 # изменяется локальная переменная
a = 5
increment(a)
print(a) # выведет: 5
📍 Передача по ссылке (указатель в Си, var в Паскале)
Изменяется оригинальная переменная:
void increment(int *x) {
*x = *x + 1; // изменяется оригинал
}
int main() {
int a = 5;
increment(&a); // передаём адрес
printf("%d\n", a); // Выведет: 6
return 0;
}
{ в Паскале вместо указателя — параметр-переменная var }
procedure increment(var x: integer);
begin
x := x + 1; { изменяется оригинал }
end;
var
a: integer;
begin
a := 5;
increment(a); { передаём саму переменную }
writeln(a); { выведет: 6 }
end.
# в Python целое число внутри функции не изменить —
# функция возвращает новое значение, а его присваивают
def increment(x):
return x + 1
a = 5
a = increment(a)
print(a) # выведет: 6
💡 Аналогия
Передача по значению — это как дать другу ксерокопию документа. Он может делать с ней что угодно, оригинал не изменится.
Передача по указателю — дать адрес сейфа с оригиналом. Теперь друг может изменить сам документ.
Рекурсия: Когда функция вызывает саму себя
Рекурсия — это метод решения задач, при котором функция вызывает саму себя с изменёнными параметрами.
Рекурсия: каждый шаг упрощает задачу до базового случая, затем результаты собираются обратно
📌 Классический пример: Факториал
Математическое определение: n! = 1 × 2 × 3 × ... × n
Рекурсивное определение:
- F(n) = 1, если n ≤ 1
- F(n) = n × F(n-1), если n > 1
Код:
#include <stdio.h>
int factorial(int n) {
if (n <= 1) {
return 1; // базовый случай
} else {
return n * factorial(n - 1); // рекурсивный вызов
}
}
int main() {
printf("5! = %d\n", factorial(5)); // Выведет: 120
return 0;
}
program Factorial;
function factorial(n: integer): integer;
begin
if n <= 1 then
factorial := 1 { базовый случай }
else
factorial := n * factorial(n - 1); { рекурсивный вызов }
end;
begin
writeln('5! = ', factorial(5)); { выведет: 120 }
end.
def factorial(n):
if n <= 1:
return 1 # базовый случай
else:
return n * factorial(n - 1) # рекурсивный вызов
print("5! =", factorial(5)) # выведет: 120
⚠️ Критически важно: Граничное условие
Без условия остановки рекурсия будет вызывать себя бесконечно (переполнение стека!).
Граничное условие — это момент, когда функция перестаёт обращаться к себе и возвращает результат напрямую.
Практический пример: Сумма цифр числа
Вычислим сумму цифр натурального числа с помощью рекурсии.
💭 Рекурсивная логика
- Если n < 10, то сумма цифр равна самому числу (базовый случай)
- Иначе: сумма цифр n = последняя цифра (n % 10) + сумма цифр оставшейся части (n / 10)
Код:
#include <stdio.h>
int summa_cifr(int n) {
if (n < 10) {
return n; // базовый случай
} else {
return (n % 10) + summa_cifr(n / 10); // рекурсия
}
}
int main() {
printf("Сумма цифр 12345: %d\n", summa_cifr(12345));
// Выведет: 15
return 0;
}
program DigitSum;
function summa_cifr(n: integer): integer;
begin
if n < 10 then
summa_cifr := n { базовый случай }
else
summa_cifr := (n mod 10) + summa_cifr(n div 10); { рекурсия }
end;
begin
writeln('Сумма цифр 12345: ', summa_cifr(12345)); { выведет: 15 }
end.
def summa_cifr(n):
if n < 10:
return n # базовый случай
else:
return (n % 10) + summa_cifr(n // 10) # рекурсия
print("Сумма цифр 12345:", summa_cifr(12345)) # выведет: 15
🔄 Рекурсия
- Элегантна для задач с естественной вложенностью
- Идеальна для деревьев, обхода графов
- Использует стек вызовов
➿ Итерация
- Эффективнее по памяти
- Не создаёт множество вызовов в стеке
- Подходит для простых циклических задач
📊 Факториал итеративно (для сравнения)
int factorial_iter(int n) {
int result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
}
function factorial_iter(n: integer): integer;
var
i, f: integer;
begin
f := 1;
for i := 1 to n do
f := f * i;
factorial_iter := f;
end;
def factorial_iter(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
Практика: периметр треугольника
Применим метод «сверху вниз» для решения реальной задачи.
📝 Задача
Дано три точки A, B, C с координатами. Найти периметр треугольника ABC.
🔷 Шаг 1: Общая структура с заглушкой
#include <stdio.h>
#include <math.h>
// Прототип функции
double dlina_otrezka(double x1, double y1, double x2, double y2);
int main() {
double xa, ya, xb, yb, xc, yc, p;
// Ввод координат
printf("Введите координаты точки A (xa ya): ");
scanf("%lf %lf", &xa, &ya);
printf("Введите координаты точки B (xb yb): ");
scanf("%lf %lf", &xb, &yb);
printf("Введите координаты точки C (xc yc): ");
scanf("%lf %lf", &xc, &yc);
// Вычисление периметра
p = dlina_otrezka(xa, ya, xb, yb) +
dlina_otrezka(xb, yb, xc, yc) +
dlina_otrezka(xc, yc, xa, ya);
printf("Периметр треугольника: %.2f\n", p);
return 0;
}
// Заглушка (пока возвращает 0)
double dlina_otrezka(double x1, double y1, double x2, double y2) {
return 0;
}
program Perimeter;
var
xa, ya, xb, yb, xc, yc, p: real;
{ заглушка (пока возвращает 0) }
function dlina_otrezka(x1, y1, x2, y2: real): real;
begin
dlina_otrezka := 0;
end;
begin
{ ввод координат }
write('Введите координаты точки A (xa ya): ');
readln(xa, ya);
write('Введите координаты точки B (xb yb): ');
readln(xb, yb);
write('Введите координаты точки C (xc yc): ');
readln(xc, yc);
{ вычисление периметра }
p := dlina_otrezka(xa, ya, xb, yb) +
dlina_otrezka(xb, yb, xc, yc) +
dlina_otrezka(xc, yc, xa, ya);
writeln('Периметр треугольника: ', p:0:2);
end.
# заглушка (пока возвращает 0)
def dlina_otrezka(x1, y1, x2, y2):
return 0
# ввод координат
xa, ya = map(float, input("Введите координаты точки A (xa ya): ").split())
xb, yb = map(float, input("Введите координаты точки B (xb yb): ").split())
xc, yc = map(float, input("Введите координаты точки C (xc yc): ").split())
# вычисление периметра
p = (dlina_otrezka(xa, ya, xb, yb) +
dlina_otrezka(xb, yb, xc, yc) +
dlina_otrezka(xc, yc, xa, ya))
print(f"Периметр треугольника: {p:.2f}")
Преимущество подхода: Сначала проверили логику программы (правильно ли вызываются функции?), затем заполнили детали.
🔷 Шаг 2: Реализация функции
double dlina_otrezka(double x1, double y1, double x2, double y2) {
return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
function dlina_otrezka(x1, y1, x2, y2: real): real;
begin
dlina_otrezka := sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
end;
def dlina_otrezka(x1, y1, x2, y2):
return math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
Обобщение: n-угольник
Как модифицировать программу для n-угольника? Храним координаты в массивах и используем цикл.
🔺 Программа для n-угольника
#include <stdio.h>
#include <math.h>
double dlina_otrezka(double x1, double y1, double x2, double y2);
int main() {
int n;
printf("Введите количество вершин: ");
scanf("%d", &n);
double x[n], y[n];
// Ввод координат
for (int i = 0; i < n; i++) {
printf("Координаты вершины %d (x y): ", i + 1);
scanf("%lf %lf", &x[i], &y[i]);
}
// Вычисление периметра
double p = 0;
for (int i = 0; i < n; i++) {
int next = (i + 1) % n; // следующая вершина
p += dlina_otrezka(x[i], y[i], x[next], y[next]);
}
printf("Периметр %d-угольника: %.2f\n", n, p);
return 0;
}
double dlina_otrezka(double x1, double y1, double x2, double y2) {
return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
program Polygon;
const
MAXN = 100;
var
x, y: array[1..MAXN] of real;
n, i, next: integer;
p: real;
function dlina_otrezka(x1, y1, x2, y2: real): real;
begin
dlina_otrezka := sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
end;
begin
write('Введите количество вершин: ');
readln(n);
{ ввод координат }
for i := 1 to n do
begin
write('Координаты вершины ', i, ' (x y): ');
readln(x[i], y[i]);
end;
{ вычисление периметра }
p := 0;
for i := 1 to n do
begin
next := i mod n + 1; { следующая вершина; после n — снова 1 }
p := p + dlina_otrezka(x[i], y[i], x[next], y[next]);
end;
writeln('Периметр ', n, '-угольника: ', p:0:2);
end.
import math
def dlina_otrezka(x1, y1, x2, y2):
return math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
n = int(input("Введите количество вершин: "))
x = [0.0] * n
y = [0.0] * n
# ввод координат
for i in range(n):
x[i], y[i] = map(float, input(f"Координаты вершины {i + 1} (x y): ").split())
# вычисление периметра
p = 0
for i in range(n):
nxt = (i + 1) % n # следующая вершина
p += dlina_otrezka(x[i], y[i], x[nxt], y[nxt])
print(f"Периметр {n}-угольника: {p:.2f}")
Почему (i + 1) % n? Чтобы замкнуть контур: после последней вершины (i = n-1) идёт первая (индекс 0).
Продвинутая рекурсия: Динамическое программирование
Рассмотрим задачу подсчёта количества программ для исполнителя Плюс.
🎯 Постановка задачи
Исполнитель Плюс имеет три команды:
- +1 (прибавить 1)
- +2 (прибавить 2)
- +4 (прибавить 4)
Вопрос: Сколько существует программ, превращающих число 20 в число 30?
Рекурсивное дерево вычислений: каждый путь от листа к корню — возможная программа
🧮 Рекурсивное решение
Обозначим количество программ для получения числа n как K(n).
Базовый случай:
- K(n) = 0 при n < 20 (невозможно получить)
- K(20) = 1 (пустая программа)
Рекурсивный шаг:
Число n можно получить из n-1, n-2 или n-4:
K(n) = K(n-1) + K(n-2) + K(n-4) при n > 20
❌ Наивная рекурсия (неэффективная)
#include <stdio.h>
int K(int n) {
if (n < 20) {
return 0;
} else if (n == 20) {
return 1;
} else {
return K(n-1) + K(n-2) + K(n-4);
}
}
int main() {
printf("K(30) = %d\n", K(30));
return 0;
}
program KRecursive;
function K(n: integer): integer;
begin
if n < 20 then
K := 0
else if n = 20 then
K := 1
else
K := K(n - 1) + K(n - 2) + K(n - 4);
end;
begin
writeln('K(30) = ', K(30));
end.
def K(n):
if n < 20:
return 0
elif n == 20:
return 1
else:
return K(n - 1) + K(n - 2) + K(n - 4)
print("K(30) =", K(30))
Проблема: Функция многократно пересчитывает одни и те же значения!
✅ Итеративный подход (оптимизация)
#include <stdio.h>
int main() {
int K[31];
// Базовые случаи
for (int i = 0; i < 20; i++) {
K[i] = 0;
}
K[20] = 1;
// Заполнение таблицы
for (int i = 21; i <= 30; i++) {
K[i] = K[i-1] + K[i-2] + K[i-4];
}
printf("Ответ: K(30) = %d\n", K[30]);
return 0;
}
program KTable;
var
K: array[0..30] of integer; { индексы от 0 — как в формуле }
i: integer;
begin
{ базовые случаи }
for i := 0 to 19 do
K[i] := 0;
K[20] := 1;
{ заполнение таблицы }
for i := 21 to 30 do
K[i] := K[i - 1] + K[i - 2] + K[i - 4];
writeln('Ответ: K(30) = ', K[30]);
end.
K = [0] * 31
# базовые случаи: K[0..19] = 0 уже есть
K[20] = 1
# заполнение таблицы
for i in range(21, 31):
K[i] = K[i - 1] + K[i - 2] + K[i - 4]
print("Ответ: K(30) =", K[30])
Преимущество: Каждое значение вычисляется ровно один раз. Сложность O(n)!
📊 Результаты вычислений
| n | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| K(n) | 1 | 1 | 2 | 3 | 6 | 10 | 18 | 31 | 55 | 96 | 169 |
Ответ: 169 различных программ
Рекурсия в природе и культуре
Рекурсивные структуры окружают нас повсюду!
Фракталы — геометрическое воплощение рекурсии: каждая часть повторяет структуру целого
🌿 Природные примеры
- Фракталы в растениях: Лист папоротника — каждая ветвь повторяет форму всего листа
- ДНК: Процесс репликации — молекула копирует саму себя
- Снежинки: Кристаллическая структура с повторяющимися паттернами
🎨 Культурные примеры
- «Дом, который построил Джек»: Каждая строфа включает предыдущую + новый элемент
- Эффект Дросте: Изображение, содержащее само себя (упаковка какао)
- Мемы: Мем про мем про мем...
🔺 Треугольник Серпинского
Алгоритм построения:
- Возьми равносторонний треугольник
- Раздели его на 4 маленьких (средними линиями)
- Удали центральный
- Повтори шаги 2-3 для каждого оставшегося треугольника
Базовый случай: Когда размер треугольника < 1 пиксель — прекрати.
Работа с массивами и указателями
В Си массивы передаются по указателю (даже если синтаксически это выглядит как передача массива), поэтому функция может изменить элементы. В Паскале для этого массив передают как var-параметр (или const, если менять не нужно), в Python функция получает сам список и тоже может менять его элементы.
📊 Пример: Функция нахождения максимума в массиве
#include <stdio.h>
int find_max(int arr[], int size) {
int max = arr[0];
for (int i = 1; i < size; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
int main() {
int numbers[] = {3, 7, 2, 9, 1};
int size = sizeof(numbers) / sizeof(numbers[0]);
printf("Максимум: %d\n", find_max(numbers, size));
// Выведет: 9
return 0;
}
program FindMax;
type
TArr = array[1..5] of integer;
var
numbers: TArr;
function find_max(const arr: TArr; size: integer): integer;
var
i, max: integer;
begin
max := arr[1];
for i := 2 to size do
if arr[i] > max then
max := arr[i];
find_max := max;
end;
begin
numbers[1] := 3; numbers[2] := 7; numbers[3] := 2;
numbers[4] := 9; numbers[5] := 1;
writeln('Максимум: ', find_max(numbers, 5)); { выведет: 9 }
end.
def find_max(arr):
biggest = arr[0]
for i in range(1, len(arr)): # размер список знает сам
if arr[i] > biggest:
biggest = arr[i]
return biggest
numbers = [3, 7, 2, 9, 1]
print("Максимум:", find_max(numbers)) # выведет: 9
Важно: в Си размер массива нужно передавать отдельным параметром — функция его не знает. В Python размер списка даёт len().
🔄 Пример: Функция обмена значений
#include <stdio.h>
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 5, y = 10;
printf("До обмена: x = %d, y = %d\n", x, y);
swap(&x, &y);
printf("После обмена: x = %d, y = %d\n", x, y);
// Выведет: x = 10, y = 5
return 0;
}
program SwapDemo;
var
x, y: integer;
procedure swap(var a, b: integer); { var — изменяем сами переменные }
var
temp: integer;
begin
temp := a;
a := b;
b := temp;
end;
begin
x := 5;
y := 10;
writeln('До обмена: x = ', x, ', y = ', y);
swap(x, y);
writeln('После обмена: x = ', x, ', y = ', y);
{ выведет: x = 10, y = 5 }
end.
x, y = 5, 10
print(f"До обмена: x = {x}, y = {y}")
# в Python функция не может поменять переменные вызывающего,
# зато обмен делается одной строкой
x, y = y, x
print(f"После обмена: x = {x}, y = {y}")
# выведет: x = 10, y = 5
🔁 Рекурсивный поиск максимума
#include <stdio.h>
int max_recursive(int arr[], int n) {
// Базовый случай: один элемент
if (n == 1) {
return arr[0];
}
// Рекурсивный случай
int max_rest = max_recursive(arr, n - 1);
return (arr[n - 1] > max_rest) ? arr[n - 1] : max_rest;
}
int main() {
int numbers[] = {3, 7, 2, 9, 1, 5};
int size = sizeof(numbers) / sizeof(numbers[0]);
printf("Максимум: %d\n", max_recursive(numbers, size));
return 0;
}
program MaxRecursive;
type
TArr = array[1..6] of integer;
var
numbers: TArr;
function max_recursive(const arr: TArr; n: integer): integer;
var
max_rest: integer;
begin
if n = 1 then
max_recursive := arr[1] { базовый случай: один элемент }
else
begin
max_rest := max_recursive(arr, n - 1); { рекурсивный случай }
if arr[n] > max_rest then
max_recursive := arr[n]
else
max_recursive := max_rest;
end;
end;
begin
numbers[1] := 3; numbers[2] := 7; numbers[3] := 2;
numbers[4] := 9; numbers[5] := 1; numbers[6] := 5;
writeln('Максимум: ', max_recursive(numbers, 6));
end.
def max_recursive(arr, n):
# базовый случай: один элемент
if n == 1:
return arr[0]
# рекурсивный случай
max_rest = max_recursive(arr, n - 1)
return arr[n - 1] if arr[n - 1] > max_rest else max_rest
numbers = [3, 7, 2, 9, 1, 5]
print("Максимум:", max_recursive(numbers, len(numbers)))
Логика: Максимум массива из n элементов — это больший из последнего элемента и максимума первых (n-1) элементов.
🎯 Ключевые выводы
Подведём итоги: что важно запомнить о структурном программировании.
Структурное программирование — это дисциплина мышления: разбивай сложное на простое, используй универсальные блоки Функции — основа переиспользования и модульности кода Передача параметров: по значению — копия, по ссылке (указатель в Си,var в Паскале) — сама переменная
Рекурсия — мощный инструмент для задач с вложенностью (деревья, фракталы, динамическое программирование)
Граничные условия — обязательны в рекурсии, иначе бесконечность!
Метод «сверху вниз» — стратегия победы над сложностью: сначала план, потом детали
🤔 Проверь себя
Вопросы для самопроверки и закрепления материала
1. В чём заключается сущность структурного программирования? Какие преимущества обеспечивает эта технология?
Подумай о читаемости кода, модульности, возможности работы в команде...
2. Какие три базовые конструкции используются в структурном программировании?
Вспомни: последовательность, ветвление, цикл. Почему именно эти три?
3. Чем отличается передача параметров по значению от передачи по указателю?
Что происходит с оригинальной переменной в каждом случае?
4. Что такое рекурсия? Приведи примеры рекурсивных структур из природы или культуры.
Вспомни фракталы, матрёшки, эффект Дросте...
5. Почему в рекурсивных функциях обязательно должно быть граничное условие?
Что случится без базового случая?
6. Опиши метод разработки программы «сверху вниз». В чём его преимущества?
Как работают заглушки? Зачем сначала создавать общую структуру?
7. Зачем в C нужны прототипы функций?
Что происходит, если функция вызывается до её определения?
8. В каких случаях рекурсивное решение лучше итерационного, а в каких — хуже?
Критерии для сравнения:
- Скорость выполнения
- Потребление памяти
- Читаемость кода
- Естественность алгоритма
📝 Практические задания
Применение знаний на практике — лучший способ обучения!
✍️ Задание 1: Рекурсивная функция
Дана функция:
int F(int n) {
if (n <= 0) return 2;
return F(n-2) + F(n-1) + F(n/2);
}
function F(n: integer): integer;
begin
if n <= 0 then
F := 2
else
F := F(n - 2) + F(n - 1) + F(n div 2);
end;
def F(n):
if n <= 0:
return 2
return F(n - 2) + F(n - 1) + F(n // 2)
Вопрос: Чему равно F(10)?
Подсказка: Построй таблицу значений от F(0) до F(10).
🎯 Задание 2: Исполнитель Калькулятор
Команды:
- +1 (прибавить 1)
- ×2 (умножить на 2)
Вопросы:
- Сколько программ превращают 1 в 20?
- Сколько из них проходят через число 15?
🔢 Задание 3: Модификация программы
Напиши программу на своём языке, которая:
- Вычисляет площадь треугольника по координатам вершин (формула Герона)
- Обобщает решение для n-угольника
🔍 Задание 4: Анализ рекурсии
Дана программа:
void F(int n) {
if (n > 0) {
F(n - 4);
printf("%d ", n);
F(n / 3);
}
}
int main() {
F(9);
return 0;
}
procedure F(n: integer);
begin
if n > 0 then
begin
F(n - 4);
write(n, ' ');
F(n div 3);
end;
end;
begin
F(9);
end.
def F(n):
if n > 0:
F(n - 4)
print(n, end=" ")
F(n // 3)
F(9)
Вопрос: Что выведет программа? Нарисуй дерево вызовов.
💻 Задание 5: Числа Фибоначчи
Напиши на своём языке:
- Рекурсивную функцию для вычисления чисел Фибоначчи
- Итеративную версию той же функции
- Сравни время выполнения для n = 40
🧮 Задание 6: Комбинаторика
Напиши программу вычисления биномиальных коэффициентов:
Cnk = n! / ((n-k)! × k!)
Используй функцию для вычисления факториала.
🚀 Следующие шаги
Куда двигаться дальше для углубления знаний?
📚 Изучить глубже
- Указатели и динамическое выделение памяти (
malloc,free) - Структуры данных (стек, очередь, связные списки)
- Работа с файлами в C
- Многомерные массивы и их применение
🎨 Попробовать на практике
- Создать рекурсивный алгоритм рисования фракталов
- Реализовать алгоритмы сортировки (быструю, пирамидальную)
- Написать программу для работы с текстовыми файлами
- Изучить алгоритмы на графах (поиск в глубину/ширину)
💡 Философия структурного мышления
Когда ты пишешь код, задавай себе вопросы:
- Можно разбить на подзадачи? → Используй функции
- Есть повторяющаяся структура? → Применяй циклы или рекурсию
- Логика понятна через месяц? → Если нет, рефактори
Эти навыки универсальны: они применимы к дизайну приложений, управлению проектами, даже к планированию жизни. Структурное мышление — это инструмент, который превращает хаос в порядок.
Переход от хаоса к структуре — это путь от разочарования к мастерству
🎓 Отличная работа! Теперь ты понимаешь принципы структурного программирования и можешь создавать чистый, читаемый и эффективный код на языке C. Язык C — это фундамент, на котором построено множество современных технологий. Понимание C даёт тебе контроль над памятью, понимание архитектуры и основу для изучения C++, Rust, Go.
Удачи в путешествии от кода к архитектуре! 🚀
Проверь себя
5 случайных вопросов по этому уроку — при следующем открытии будут другие. Ответы проверяются сразу, без регистрации.