Обзор алгоритмов и задач с решениями на Python по разным темам
Здесь представлена подборка из 8 классических и наглядных задач по ключевым темам, разбитая. Задачи подобраны так, чтобы демонстрировать саму суть алгоритма или структуры данных максимально простым способом.
Часть 1: Строки, Простые списки, Числа, Матрицы
1. Строки: Проверка на палиндром
Задача: Определить, является ли строка палиндромом (читается одинаково слева направо и справа налево). Игнорируем регистр и пробелы.
Принцип решения: Сравниваем строку с её перевёрнутой версией. Для этого очищаем строку от лишних символов и приводим к одному регистру.
def is_palindrome(s: str) -> bool:
"""
Проверяет, является ли строка палиндромом.
"""
# 1. Очищаем строку: убираем пробелы и приводим к нижнему регистру
cleaned_str = ''.join(ch.lower() for ch in s if ch.isalnum())
# 2. Сравниваем очищенную строку с её перевёрнутой версией
return cleaned_str == cleaned_str[::-1] # [::-1] — это срез для разворота строки
# Примеры использования
print(is_palindrome("А роза упала на лапу Азора")) # True
print(is_palindrome("Hello")) # False
print(is_palindrome("racecar")) # True
2. Простые списки (массивы): Поиск второго максимального элемента
Задача: Найти второй по величине элемент в списке чисел. Список содержит не менее двух уникальных элементов.
Принцип решения: Проходим по списку один раз, отслеживая наибольший (max1) и второй наибольший (max2) элементы.
def second_max(arr: list) -> int:
"""
Находит второй максимальный элемент в списке.
"""
# Инициализируем два наименьших возможных значения
max1 = max2 = float('-inf') # -бесконечность
for num in arr:
if num > max1:
# Нашли новый максимум, старый максимум становится вторым
max2 = max1
max1 = num
elif num > max2 and num != max1:
# Число меньше максимума, но больше текущего второго максимума
max2 = num
return max2
# Пример использования
numbers = [3, 5, 1, 7, 2, 7, 4]
print(second_max(numbers)) # Вывод: 5 (второй максимум после 7)
3. Числа: Проверка числа на простоту
Задача: Определить, является ли натуральное число простым (делится только на 1 и на себя).
Принцип решения: Проверяем делители числа от 2 до квадратного корня из числа. Если хотя бы один делитель найден — число составное.
import math
def is_prime(n: int) -> bool:
"""
Проверяет, является ли число простым.
"""
if n < 2:
return False
if n == 2: # 2 — единственное чётное простое число
return True
if n % 2 == 0: # Отсекаем все остальные чётные числа
return False
# Проверяем нечётные делители от 3 до корня из n
limit = int(math.isqrt(n)) # Квадратный корень, округлённый вниз
for i in range(3, limit + 1, 2): # Шаг 2 — только нечётные
if n % i == 0:
return False
return True
# Примеры использования
print(is_prime(17)) # True
print(is_prime(100)) # False
print(is_prime(2)) # True
4. Матрицы: Поворот матрицы на 90 градусов
Задача: Повернуть квадратную матрицу (список списков) на 90 градусов по часовой стрелке.
Принцип решения: Создаём новую матрицу, где элементы из исходной матрицы переставляются по правилу: new_matrix[i][j] = old_matrix[n-1-j][i].
def rotate_matrix(matrix: list[list]) -> list[list]:
"""
Поворачивает квадратную матрицу на 90 градусов по часовой стрелке.
"""
n = len(matrix) # Размер квадратной матрицы (n x n)
# Создаём новую матрицу такого же размера, заполненную нулями
rotated = [[0] * n for _ in range(n)]
for i in range(n):
for j in range(n):
# Ключевая формула поворота:
# Элемент в строке i, столбце j новой матрицы берётся
# из строки n-1-j, столбца i исходной матрицы
rotated[i][j] = matrix[n - 1 - j][i]
return rotated
# Пример использования
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
print("Исходная матрица:")
for row in matrix:
print(row)
print("\nПовёрнутая матрица:")
for row in rotate_matrix(matrix):
print(row)
# Вывод:
# [7, 4, 1]
# [8, 5, 2]
# [9, 6, 3]
Часть 2: Связанные списки, Деревья, Кучи (очереди с приоритетом)
5. Связанные списки: Разворот односвязного списка
Задача: Развернуть односвязный список (изменить направление ссылок на противоположное).
Принцип решения: Итеративно проходим по списку, меняя направление ссылок. Нужно хранить три указателя: на предыдущий, текущий и следующий узлы.
class ListNode:
"""Узел односвязного списка."""
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def reverse_list(head: ListNode) -> ListNode:
"""
Разворачивает односвязный список и возвращает новую голову.
"""
prev = None # Предыдущий узел, изначально None
current = head # Текущий узел
while current:
# Сохраняем следующий узел, чтобы не потерять
next_node = current.next
# Меняем направление ссылки: текущий узел теперь указывает на предыдущий
current.next = prev
# Сдвигаем указатели: предыдущий становится текущим,
# текущий становится следующим
prev = current
current = next_node
# В конце prev указывает на новую голову (бывший хвост)
return prev
# Вспомогательная функция для создания и печати списка
def create_list(values):
"""Создаёт связанный список из списка значений."""
dummy = ListNode()
current = dummy
for val in values:
current.next = ListNode(val)
current = current.next
return dummy.next
def print_list(head):
"""Печатает значения связанного списка."""
result = []
while head:
result.append(head.val)
head = head.next
print(" -> ".join(map(str, result)))
# Пример использования
original = create_list([1, 2, 3, 4, 5])
print("Исходный список:")
print_list(original)
reversed_head = reverse_list(original)
print("\nРазвёрнутый список:")
print_list(reversed_head) # 5 -> 4 -> 3 -> 2 -> 1
6. Деревья: Поиск в глубину (DFS) в бинарном дереве
Задача: Обойти все узлы бинарного дерева в порядке "корень-левый-правый" (pre-order traversal).
Принцип решения: Рекурсивно посещаем корень, затем левое поддерево, затем правое поддерево.
class TreeNode:
"""Узел бинарного дерева."""
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def dfs_preorder(root: TreeNode) -> list:
"""
Возвращает значения узлов в порядке pre-order (корень-левый-правый).
"""
result = []
def traverse(node):
if not node:
return
# 1. Посещаем корень (добавляем значение)
result.append(node.val)
# 2. Рекурсивно обходим левое поддерево
traverse(node.left)
# 3. Рекурсивно обходим правое поддерево
traverse(node.right)
traverse(root)
return result
# Создаём бинарное дерево:
# 1
# / \
# 2 3
# / \
# 4 5
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
print("Pre-order обход (DFS):")
print(dfs_preorder(root)) # [1, 2, 4, 5, 3]
7. Кучи: Реализация минимальной кучи (Min-Heap)
Задача: Реализовать минимальную кучу (min-heap) с основными операциями: добавление элемента и извлечение минимума.
Принцип решения: Используем список для хранения элементов. Поддерживаем свойство кучи: родитель всегда меньше своих потомков.
class MinHeap:
"""
Реализация минимальной кучи (min-heap).
"""
def __init__(self):
self.heap = []
def parent_index(self, i):
"""Индекс родителя узла i."""
return (i - 1) // 2
def left_child_index(self, i):
"""Индекс левого потомка узла i."""
return 2 * i + 1
def right_child_index(self, i):
"""Индекс правого потомка узла i."""
return 2 * i + 2
def insert(self, value):
"""Добавляет элемент в кучу."""
# 1. Добавляем элемент в конец списка
self.heap.append(value)
# 2. Восстанавливаем свойство кучи, "поднимая" элемент
self._sift_up(len(self.heap) - 1)
def _sift_up(self, i):
"""Поднимает элемент на правильную позицию."""
while i > 0 and self.heap[i] < self.heap[self.parent_index(i)]:
# Меняем местами с родителем, если родитель больше
parent_idx = self.parent_index(i)
self.heap[i], self.heap[parent_idx] = self.heap[parent_idx], self.heap[i]
i = parent_idx
def extract_min(self):
"""Извлекает и возвращает минимальный элемент (корень кучи)."""
if not self.heap:
return None
# 1. Сохраняем минимум (это корень)
min_val = self.heap[0]
# 2. Перемещаем последний элемент в корень
last_val = self.heap.pop()
if self.heap: # Если куча не пуста после извлечения
self.heap[0] = last_val
# 3. Восстанавливаем свойство кучи, "опуская" корень
self._sift_down(0)
return min_val
def _sift_down(self, i):
"""Опускает элемент на правильную позицию."""
size = len(self.heap)
while True:
left = self.left_child_index(i)
right = self.right_child_index(i)
smallest = i
# Ищем наименьший среди текущего узла и его потомков
if left < size and self.heap[left] < self.heap[smallest]:
smallest = left
if right < size and self.heap[right] < self.heap[smallest]:
smallest = right
# Если текущий узел уже наименьший — свойство кучи восстановлено
if smallest == i:
break
# Меняем местами с наименьшим потомком
self.heap[i], self.heap[smallest] = self.heap[smallest], self.heap[i]
i = smallest # Продолжаем проверку с новой позиции
# Пример использования
heap = MinHeap()
heap.insert(5)
heap.insert(3)
heap.insert(8)
heap.insert(1)
heap.insert(2)
print("Извлечение элементов из min-heap в порядке возрастания:")
for _ in range(5):
print(heap.extract_min(), end=" ") # 1 2 3 5 8
print()
8. Графы: Поиск в ширину (BFS) — простейший вариант
Задача: Обойти все узлы невзвешенного графа, начиная с заданной вершины, в порядке увеличения расстояния от неё.
Принцип решения: Используем очередь. Посещаем вершину, добавляем всех её непосещённых соседей в очередь.
from collections import deque
def bfs(graph, start):
"""
Поиск в ширину (BFS) по графу, представленному списками смежности.
Возвращает вершины в порядке их посещения.
"""
visited = set() # Множество посещённых вершин
queue = deque([start]) # Очередь для BFS
result = [] # Результат обхода
visited.add(start)
while queue:
# Извлекаем вершину из начала очереди
vertex = queue.popleft()
result.append(vertex)
# Добавляем всех непосещённых соседей в конец очереди
for neighbor in graph[vertex]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
return result
# Граф представлен списками смежности
# Индексы: 0:A, 1:B, 2:C, 3:D, 4:E
graph = {
0: [1, 2], # A соединена с B и C
1: [0, 3, 4], # B соединена с A, D и E
2: [0, 4], # C соединена с A и E
3: [1], # D соединена с B
4: [1, 2] # E соединена с B и C
}
print("BFS обход графа, начиная с вершины 0:")
print(bfs(graph, 0)) # [0, 1, 2, 3, 4] или [0, 2, 1, 4, 3] — зависит от порядка соседей
Ключевые выводы:
- Строки: Используйте срезы [::-1] для разворота
- Списки: Часто достаточно одного прохода с несколькими переменными
- Числа: Для простоты проверяйте делители до корня
- Матрицы: Формула [n-1-j][i] для поворота на 90°
- Списки: Три указателя — ключ к развороту связного списка
- Деревья: Рекурсия — естественный способ обхода
- Кучи: Поддержание свойства кучи через "подъем" и "спуск"
- Графы: Очередь обеспечивает обход по уровням
Эти задачи охватывают фундаментальные алгоритмические концепции и часто встречаются на собеседованиях. Их понимание даст прочную основу для решения более сложных задач.
Дополнительный материал
Вопросы учеников к ИИ-помощнику и ответы по теме урока
Этот код создаёт цикл, который выполняется n раз, не используя счётчик внутри цикла.
Как это работает:
- range(n) — создаёт последовательность чисел от 0 до n-1
- _ — это имя переменной, которое в Python означает, что значение нам не важно
- for _ in range(n): — цикл выполнится ровно n раз, но мы не используем текущий номер итерации
Пример:
# Выведет "Привет!" 3 раза
for _ in range(3):
print("Привет!")
Упражнение:
Напиши цикл, который 5 раз выводит твоё имя, используя for _ in range().