Классификация алгоритмов в Python
Типы алгоритмов в Python: обзор и примеры
Python позволяет реализовывать алгоритмы любой сложности. Рассмотрим основные типы: сортировка, поиск, графовые алгоритмы и динамическое программирование. Для каждого типа приведено эффективное решение, альтернативы и возможные проблемы.
Как эффективно отсортировать список чисел в Python?
Сортировка - одна из самых частых операций. Наиболее эффективный способ - использовать быструю сортировку (QuickSort) со средним временем O(n log n).
Быстрая сортировка (QuickSort)
def quicksort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr)//2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quicksort(left) + middle + quicksort(right)
print(quicksort([3,6,8,10,1,2,1]))алгоритмы и структуры данных python (алгоритмы и структуры данных в python)
[1, 1, 2, 3, 6, 8, 10]
динамическое программирование python (решение задач динамического программирования на python)
Алгоритм выбирает опорный элемент, делит список на три части, рекурсивно сортирует левую и правую части. Встроенная функция sorted() реализует Timsort, который также эффективен.
Альтернативы:
- Пузырьковая сортировка (O(n^2)) - простой, но медленный алгоритм.
- Сортировка вставками (O(n^2)) - эффективна для почти отсортированных данных.
- Использование встроенной
sorted()или.sort()- рекомендуется для большинства случаев.
# Пузырьковая сортировка
def bubble_sort(arr):
n = len(arr)
for i in range(n):
for j in range(0, n-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
print(bubble_sort([64, 34, 25, 12, 22, 11, 90]))типы алгоритмов в python (типы алгоритмов в python)
[11, 12, 22, 25, 34, 64, 90]
алгоритм выбора python (алгоритм выбора (selection) на python)
Типичные проблемы:
- Рекурсия в QuickSort может превысить глубину стека при больших массивах (можно использовать итеративную версию).
- Пузырьковая сортировка слишком медленная для больших данных.
sorted()создает новый список, а.sort()изменяет исходный - путаница может привести к потере данных.
Как выполнить поиск элемента в отсортированном массиве?
Бинарный поиск обеспечивает логарифмическую скорость и является стандартом для отсортированных данных.
Бинарный поиск (итеративная версия)
def binary_search(arr, target):
left, right = 0, len(arr)-1
while left <= right:
mid = (left + right)//2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid+1
else:
right = mid-1
return -1
arr = [2,3,5,7,11,13,17]
print(binary_search(arr, 13))алгоритм евклида на python (алгоритм евклида на python)
5
квадраты python (вычисление квадратов чисел в python)
Альтернативы:
- Линейный поиск (O(n)) - работает на неотсортированных данных, но медленнее.
- Рекурсивный бинарный поиск.
- Использование модуля
bisectдля вставки и поиска.
# Линейный поиск
def linear_search(arr, target):
for i, val in enumerate(arr):
if val == target:
return i
return -1
print(linear_search([3,5,7,9], 7))2
Типичные проблемы:
- Бинарный поиск требует предварительной сортировки массива; если массив не отсортирован, результат будет неверным.
- Ошибки при вычислении середины (переполнение целых чисел в некоторых языках, но в Python это не проблема).
- Не обрабатывается случай пустого массива.
Как найти кратчайший путь во взвешенном графе?
Алгоритм Дейкстры находит кратчайшие пути от начальной вершины до всех остальных в графе с неотрицательными весами.
Алгоритм Дейкстры с использованием кучи
import heapq
def dijkstra(graph, start):
distances = {node: float('infinity') for node in graph}
distances[start] = 0
pq = [(0, start)] # (расстояние, вершина)
while pq:
current_dist, current_node = heapq.heappop(pq)
if current_dist > distances[current_node]:
continue
for neighbor, weight in graph[current_node].items():
distance = current_dist + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(pq, (distance, neighbor))
return distances
graph = {
'A': {'B': 1, 'C': 4},
'B': {'A': 1, 'C': 2, 'D': 5},
'C': {'A': 4, 'B': 2, 'D': 1},
'D': {'B': 5, 'C': 1}
}
print(dijkstra(graph, 'A')){'A': 0, 'B': 1, 'C': 3, 'D': 4}Другие алгоритмы для графов:
- Поиск в ширину (BFS) - для невзвешенных графов или поиска кратчайшего пути по количеству ребер.
- Поиск в глубину (DFS) - для обхода и поиска путей, без гарантии кратчайшего.
- Алгоритм Беллмана-Форда - для графов с отрицательными весами (но медленнее).
# BFS для невзвешенного графа
from collections import deque
def bfs(graph, start):
visited = set()
queue = deque([start])
while queue:
node = queue.popleft()
if node not in visited:
visited.add(node)
queue.extend(neighbor for neighbor in graph[node] if neighbor not in visited)
return visited
print(bfs({0:[1,2],1:[2],2:[0,3],3:[3]}, 0)){0, 1, 2, 3}Типичные проблемы:
- Алгоритм Дейкстры не работает с отрицательными весами (используется Беллмана-Форда).
- Без использования кучи (через простой массив) сложность растет до O(V^2).
- Рекурсивный DFS может привести к переполнению стека для больших графов.
Как решить задачу о рюкзаке с помощью динамического программирования?
Классическая задача: выбор предметов с максимальной суммарной ценностью при ограниченной вместимости. Динамическое программирование (DP) позволяет решить ее за O(n*W).
Итеративное DP снизу вверх
def knapsack(weights, values, capacity):
n = len(weights)
dp = [[0]*(capacity+1) for _ in range(n+1)]
for i in range(1, n+1):
for w in range(1, capacity+1):
if weights[i-1] <= w:
dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
else:
dp[i][w] = dp[i-1][w]
return dp[n][capacity]
weights = [2,3,4,5]
values = [3,4,5,6]
capacity = 5
print(knapsack(weights, values, capacity))7
Альтернативные подходы:
- Рекурсивный перебор (экспоненциальная сложность) - простой для малого n.
- Мемоизация (рекурсия + кеш) - сочетает простоту рекурсии с эффективностью DP.
- Жадный алгоритм (например, по удельной ценности) - не гарантирует оптимальность, но быстрее.
# Рекурсивный перебор с мемоизацией
def knapsack_rec(weights, values, capacity, n, memo={}):
if n == 0 or capacity == 0:
return 0
key = (n, capacity)
if key in memo:
return memo[key]
if weights[n-1] > capacity:
result = knapsack_rec(weights, values, capacity, n-1, memo)
else:
result = max(knapsack_rec(weights, values, capacity, n-1, memo),
values[n-1] + knapsack_rec(weights, values, capacity-weights[n-1], n-1, memo))
memo[key] = result
return result
print(knapsack_rec([2,3,4,5],[3,4,5,6],5,4))7
Типичные проблемы:
- Неправильная индексация в DP-таблице (часто путают i и w).
- Использование рекурсии без мемоизации приводит к экспоненциальному росту.
- Жадный алгоритм может дать неоптимальный результат (например, если предметы не делятся).
Расширенные примеры алгоритмов
Дополнительные примеры демонстрируют реализацию менее распространенных, но важных алгоритмов.
Сортировка слиянием (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
print(merge_sort([38,27,43,3,9,82,10]))[3, 9, 10, 27, 38, 43, 82]
Сортировка слиянием использует принцип "разделяй и властвуй". Она стабильна и эффективна, но требует O(n) дополнительной памяти.
Поиск подстроки с помощью Z-функции
def z_function(s):
n = len(s)
z = [0]*n
l = r = 0
for i in range(1, n):
if i <= r:
z[i] = min(r-i+1, z[i-l])
while i+z[i] < n and s[z[i]] == s[i+z[i]]:
z[i] += 1
if i+z[i]-1 > r:
l, r = i, i+z[i]-1
return z
def search_pattern(text, pattern):
concat = pattern + '$' + text
z = z_function(concat)
pattern_len = len(pattern)
positions = []
for i in range(pattern_len+1, len(concat)):
if z[i] == pattern_len:
positions.append(i - pattern_len - 1)
return positions
print(search_pattern("abcabcabc", "abc"))[0, 3, 6]
Z-функция позволяет найти все вхождения образца за O(n+m) времени. Это эффективнее прямого сравнения.
Минимальное остовное дерево: алгоритм Прима
import heapq
def prim(graph, start):
mst = []
visited = set([start])
edges = [(weight, start, neighbor) for neighbor, weight in graph[start].items()]
heapq.heapify(edges)
while edges:
weight, u, v = heapq.heappop(edges)
if v not in visited:
visited.add(v)
mst.append((u, v, weight))
for neighbor, w in graph[v].items():
if neighbor not in visited:
heapq.heappush(edges, (w, v, neighbor))
return mst
graph = {
'A': {'B': 1, 'C': 3},
'B': {'A': 1, 'C': 1, 'D': 4},
'C': {'A': 3, 'B': 1, 'D': 1},
'D': {'B': 4, 'C': 1}
}
print(prim(graph, 'A'))[('A', 'B', 1), ('B', 'C', 1), ('C', 'D', 1)]Алгоритм Прима находит минимальное остовное дерево. Использует приоритетную очередь для выбора минимального ребра, ведущего к новой вершине.
Ханойские башни (рекурсивный алгоритм)
def hanoi(n, source, target, auxiliary):
if n == 1:
print(f"Переместить диск 1 с {source} на {target}")
return
hanoi(n-1, source, auxiliary, target)
print(f"Переместить диск {n} с {source} на {target}")
hanoi(n-1, auxiliary, target, source)
hanoi(3, 'A', 'C', 'B')Переместить диск 1 с A на C Переместить диск 2 с A на B Переместить диск 1 с C на B Переместить диск 3 с A на C Переместить диск 1 с B на A Переместить диск 2 с B на C Переместить диск 1 с A на C
Задача Ханойских башен - классический пример рекурсии. Количество ходов равно 2^n - 1. Рекурсивный подход элегантен, но при больших n вызовет ошибку переполнения стека.