⏱️ Big O Breakdown
Trie: поиск строк за O(m) вместо O(n·m)
Задача: есть словарь из n слов. Нужно проверять — есть ли слово в словаре, и находить все слова с заданным префиксом.
Какова сложность для каждого подхода?
Варианты:
А) Оба O(m) где m — длина слова
Б) Список — O(n·m), Trie — O(m)
В) Список — O(n), Trie — O(m·n)
Г) Оба O(n)
Ответ:Б) Список — O(n·m), Trie — O(m)
Список перебирает все n слов, сравнивая каждое символ за символом — O(n·m).
Trie идёт по символам запроса — ровно m шагов. Размер словаря не важен.
Реализация:
Визуализация для ["app", "apple"]:
Общий префикс хранится один раз.
Цена — память:
O(n·m) — каждый символ каждого слова. При большом словаре с короткими общими префиксами — дорого.
🐍Вопросы с собесов -> ProstoPython
Trie: поиск строк за O(m) вместо O(n·m)
Задача: есть словарь из n слов. Нужно проверять — есть ли слово в словаре, и находить все слова с заданным префиксом.
words = ["apple", "app", "application", "apply", "banana"]
# Есть ли "app"?
# Все слова с префиксом "app"?
Какова сложность для каждого подхода?
# Вариант 1 — список
"app" in words # поиск слова
[w for w in words if w.startswith("app")] # поиск по префиксу
# Вариант 2 — Trie
trie.search("app")
trie.starts_with("app")
Варианты:
А) Оба O(m) где m — длина слова
Б) Список — O(n·m), Trie — O(m)
В) Список — O(n), Trie — O(m·n)
Г) Оба O(n)
Ответ:
Список перебирает все n слов, сравнивая каждое символ за символом — O(n·m).
Trie идёт по символам запроса — ровно m шагов. Размер словаря не важен.
Реализация:
class TrieNode:
def __init__(self):
self.children = {}
self.is_end = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for ch in word:
if ch not in node.children:
node.children[ch] = TrieNode()
node = node.children[ch]
node.is_end = True
def search(self, word):
node = self.root
for ch in word:
if ch not in node.children:
return False
node = node.children[ch]
return node.is_end
def starts_with(self, prefix):
node = self.root
for ch in prefix:
if ch not in node.children:
return False
node = node.children[ch]
return True
Визуализация для ["app", "apple"]:
root
└── a
└── p
└── p [end]
└── l
└── e [end]
Общий префикс хранится один раз.
Цена — память:
O(n·m) — каждый символ каждого слова. При большом словаре с короткими общими префиксами — дорого.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥3
📈 FROM O(...) TO O(...)
Union Find: от O(n) до O(α(n)) для задач на связность
Задача: есть n элементов и список связей. Нужно быстро отвечать — связаны ли два элемента?
Наивное решение — обход графа:
O(n) на каждый запрос. При тысячах запросов — медленно.
Union Find — почти O(1) на запрос:
Две оптимизации:
Path compression — при find() делаем все узлы на пути дочерними корня напрямую. Дерево становится плоским.
Union by rank — присоединяем меньшее дерево к большему. Высота не растёт.
Вместе дают O(α(n)) — функция Акермана, растёт настолько медленно что практически константа.
Когда применять:
Задачи на связные компоненты, обнаружение циклов в графе, задача с островами если нужны множественные запросы.
🐍Вопросы с собесов -> ProstoPython
Union Find: от O(n) до O(α(n)) для задач на связность
Задача: есть n элементов и список связей. Нужно быстро отвечать — связаны ли два элемента?
# Элементы: 0, 1, 2, 3, 4
# Связи: (0,1), (1,2), (3,4)
# Связаны ли 0 и 2? → True
# Связаны ли 0 и 3? → False
Наивное решение — обход графа:
def are_connected(graph, a, b):
visited = set()
queue = [a]
while queue:
node = queue.pop()
if node == b:
return True
if node not in visited:
visited.add(node)
queue.extend(graph[node])
return False
O(n) на каждый запрос. При тысячах запросов — медленно.
Union Find — почти O(1) на запрос:
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # path compression
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py:
return
if self.rank[px] < self.rank[py]:
px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
def connected(self, x, y):
return self.find(x) == self.find(y)
uf = UnionFind(5)
uf.union(0, 1)
uf.union(1, 2)
uf.union(3, 4)
print(uf.connected(0, 2)) # True
print(uf.connected(0, 3)) # False
Две оптимизации:
Path compression — при find() делаем все узлы на пути дочерними корня напрямую. Дерево становится плоским.
Union by rank — присоединяем меньшее дерево к большему. Высота не растёт.
Вместе дают O(α(n)) — функция Акермана, растёт настолько медленно что практически константа.
Когда применять:
Задачи на связные компоненты, обнаружение циклов в графе, задача с островами если нужны множественные запросы.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥3
🧠 Interview Thinking
Минимальный путь в графе. BFS vs Dijkstra
Задача: найти кратчайший путь между двумя узлами.
Как думает junior:
BFS — он же находит кратчайший путь:
Находит путь с наименьшим количеством рёбер — но игнорирует веса. A→B→C→D даст 3 ребра, но суммарный вес 4. A→C→D — 2 ребра, вес 5.
BFS не знает про веса.
Как думает strong middle:
Если рёбра с весами — нужен Dijkstra:
O((V + E) log V) — приоритетная очередь выбирает узел с минимальной стоимостью.
Что хочет услышать интервьюер:
1. BFS — для невзвешенных графов или когда все веса равны
2. Dijkstra — для взвешенных с неотрицательными весами
3. Bellman-Ford — если веса могут быть отрицательными
🐍Вопросы с собесов -> ProstoPython
Минимальный путь в графе. BFS vs Dijkstra
Задача: найти кратчайший путь между двумя узлами.
# Граф с весами рёбер
graph = {
"A": [("B", 1), ("C", 4)],
"B": [("C", 2), ("D", 5)],
"C": [("D", 1)],
"D": []
}
# Кратчайший путь A → D?
Как думает junior:
BFS — он же находит кратчайший путь:
from collections import deque
def bfs(graph, start, end):
queue = deque([(start, [start])])
visited = set()
while queue:
node, path = queue.popleft()
if node == end:
return path
for neighbor, weight in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor]))
Находит путь с наименьшим количеством рёбер — но игнорирует веса. A→B→C→D даст 3 ребра, но суммарный вес 4. A→C→D — 2 ребра, вес 5.
BFS не знает про веса.
Как думает strong middle:
Если рёбра с весами — нужен Dijkstra:
import heapq
def dijkstra(graph, start, end):
heap = [(0, start, [start])]
visited = set()
while heap:
cost, node, path = heapq.heappop(heap)
if node in visited:
continue
visited.add(node)
if node == end:
return cost, path
for neighbor, weight in graph[node]:
if neighbor not in visited:
heapq.heappush(heap, (cost + weight, neighbor, path + [neighbor]))
return float("inf"), []
cost, path = dijkstra(graph, "A", "D")
# cost=4, path=["A","B","C","D"]
O((V + E) log V) — приоритетная очередь выбирает узел с минимальной стоимостью.
Что хочет услышать интервьюер:
1. BFS — для невзвешенных графов или когда все веса равны
2. Dijkstra — для взвешенных с неотрицательными весами
3. Bellman-Ford — если веса могут быть отрицательными
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍3
📈 FROM O(...) TO O(...)
Задача о рюкзаке: от O(2ⁿ) до O(n·W)
Задача: есть предметы с весом и ценностью. Рюкзак вмещает W кг. Максимизировать ценность.
Наивное решение — перебор всех подмножеств:
O(2ⁿ) — на 40 предметах уже триллион комбинаций.
Динамическое программирование — O(n·W):
O(n·W) время, O(W) память.
Идея:
dp[w] — максимальная ценность при вместимости w. Для каждого предмета обновляем: брать или не брать?
Идём справа налево — чтобы не использовать предмет дважды.
Сравнение на n=20, W=100:
В 500 раз быстрее.
🐍Вопросы с собесов -> ProstoPython
Задача о рюкзаке: от O(2ⁿ) до O(n·W)
Задача: есть предметы с весом и ценностью. Рюкзак вмещает W кг. Максимизировать ценность.
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
W = 8
# Ответ: 10 (предметы 0+3: вес 2+5=7, ценность 3+6=9... или 1+2: 3+4=7, ценность 4+5=9)
# Правильный: предметы 0+1+? — нужно считать
Наивное решение — перебор всех подмножеств:
def knapsack_brute(weights, values, W):
n = len(weights)
best = 0
for mask in range(1 << n):
total_w = total_v = 0
for i in range(n):
if mask & (1 << i):
total_w += weights[i]
total_v += values[i]
if total_w <= W:
best = max(best, total_v)
return best
O(2ⁿ) — на 40 предметах уже триллион комбинаций.
Динамическое программирование — O(n·W):
def knapsack(weights, values, W):
n = len(weights)
dp = [0] * (W + 1)
for i in range(n):
for w in range(W, weights[i] - 1, -1):
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
return dp[W]
O(n·W) время, O(W) память.
Идея:
dp[w] — максимальная ценность при вместимости w. Для каждого предмета обновляем: брать или не брать?
dp[w] = max(
dp[w], # не берём предмет
dp[w - weights[i]] + values[i] # берём предмет
)
Идём справа налево — чтобы не использовать предмет дважды.
Сравнение на n=20, W=100:
перебор: 2²⁰ = 1 048 576 операций
DP: 20 × 100 = 2 000 операций
В 500 раз быстрее.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
⏱️ Big O Breakdown
Тема: сложность, которая выглядит как O(n log n), но это не так
Посмотри на код:
❓ Какая сложность?
A) O(n)
B) O(n log n)
C) O(n²)
D) O(log n)
✅ Правильный ответ: A) O(n)
🧠 Разбор
На первый взгляд:
* внешний цикл → n
* внутренний → log n
Значит вроде бы O(n log n).
Но здесь есть нюанс.
Внутренний цикл работает не всегда log n раз.
Для каждого i:
То есть общее количество операций:
А это ≈ n
📌 Итог
Суммарная сложность:
💡 Что проверяют таким вопросом
🔹 умеешь ли ты анализировать сумму операций
🔹 не делаешь ли вывод «на глаз»
🔹 понимаешь ли, что сложность считается суммарно
Сильный разработчик не смотрит на форму кода.
Он считает реальные операции 🐍
🐍Вопросы с собесов -> ProstoPython
Тема: сложность, которая выглядит как O(n log n), но это не так
Посмотри на код:
def example(n):
for i in range(n):
j = i
while j > 0:
print(j)
j //= 2
❓ Какая сложность?
A) O(n)
B) O(n log n)
C) O(n²)
D) O(log n)
✅ Правильный ответ:
🧠 Разбор
На первый взгляд:
* внешний цикл → n
* внутренний → log n
Значит вроде бы O(n log n).
Но здесь есть нюанс.
Внутренний цикл работает не всегда log n раз.
Для каждого i:
i = 1 → ~1 шаг
i = 2 → ~2 шага
i = 4 → ~3 шага
...
i = n → ~log n шагов
То есть общее количество операций:
log(1) + log(2) + log(3) + ... + log(n)
А это ≈ n
📌 Итог
Суммарная сложность:
O(n)
💡 Что проверяют таким вопросом
🔹 умеешь ли ты анализировать сумму операций
🔹 не делаешь ли вывод «на глаз»
🔹 понимаешь ли, что сложность считается суммарно
Сильный разработчик не смотрит на форму кода.
Он считает реальные операции 🐍
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
❤3🔥2
Singleton — паттерн, который гарантирует, что у класса существует только один экземпляр и предоставляет к нему глобальную точку доступа.
Зачем нужен
🔹 общий доступ к ресурсу (например, конфиг, логгер)
🔹 контроль над созданием объекта
Особенности
🔹 всегда возвращается один и тот же объект
🔹 скрывает создание экземпляра
🐍Вопросы с собесов -> ProstoPython
Зачем нужен
🔹 общий доступ к ресурсу (например, конфиг, логгер)
🔹 контроль над созданием объекта
Особенности
🔹 всегда возвращается один и тот же объект
🔹 скрывает создание экземпляра
🐍Вопросы с собесов -> ProstoPython
🔥3💯1
🧩 Code Cleanup
Используй typing — код становится документацией
Типичный код без аннотаций:
Что такое users? Список чего? Что возвращает функция? Непонятно без чтения тела.
После:
Сигнатура сама объясняет что происходит. IDE подскажет ошибки до запуска.
Полезные типы:
Python 3.10+ — короче:
Аннотации не замедляют код:
Python проверяет типы только статическими анализаторами — mypy, pyright. В рантайме аннотации игнорируются.
🐍Вопросы с собесов -> ProstoPython
Используй typing — код становится документацией
Типичный код без аннотаций:
def process(users, limit, active):
result = []
for user in users:
if user["active"] == active:
result.append(user)
return result[:limit]
Что такое users? Список чего? Что возвращает функция? Непонятно без чтения тела.
После:
from typing import TypedDict
class User(TypedDict):
name: str
age: int
active: bool
def process(
users: list[User],
limit: int,
active: bool
) -> list[User]:
return [u for u in users if u["active"] == active][:limit]
Сигнатура сама объясняет что происходит. IDE подскажет ошибки до запуска.
Полезные типы:
from typing import Optional, Union, Callable
def find(items: list[int], key: int) -> Optional[int]:
# возвращает int или None
...
def transform(
data: list[str],
fn: Callable[[str], str] # функция str → str
) -> list[str]:
return [fn(item) for item in data]
def parse(value: Union[str, int]) -> str:
# принимает str или int
return str(value)
Python 3.10+ — короче:
# Union[str, int] → str | int
# Optional[int] → int | None
def find(items: list[int], key: int) -> int | None:
...
Аннотации не замедляют код:
Python проверяет типы только статическими анализаторами — mypy, pyright. В рантайме аннотации игнорируются.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥4
📈 From O(n²) to O(n): поиск двух чисел с заданной суммой
Задача: дан список чисел и целевая сумма
Наивное решение — O(n²) по времени, O(1) по памяти
Два вложенных цикла. Для каждого элемента проверяем все остальные.
При n = 10 000 это ~50 000 000 операций. При n = 100 000 — уже ~5 000 000 000.
Оптимальное решение — O(n) по времени, O(n) по памяти
Один проход. Для каждого числа спрашиваем: "а его пара уже встречалась?"
При n = 10 000 это 10 000 операций. При n = 100 000 — 100 000.
Что изменилось под капотом
Наивное решение ищет пару перебором. Оптимальное превращает задачу поиска в задачу проверки — а
Мы платим памятью O(n) за словарь и получаем скорость O(n) вместо O(n²).
Trade-off явный: если память критична и n небольшое — наивный вариант может быть лучше. Об этом стоит сказать на собесе сам, не дожидаясь вопроса.
Edge cases которые нельзя забыть:
🐍Вопросы с собесов -> ProstoPython
Задача: дан список чисел и целевая сумма
target. Найти индексы двух чисел, которые в сумме дают target.nums = [2, 7, 11, 15]
target = 9
# ответ: [0, 1], потому что nums[0] + nums[1] == 9
Наивное решение — O(n²) по времени, O(1) по памяти
def two_sum_naive(nums, target):
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
Два вложенных цикла. Для каждого элемента проверяем все остальные.
При n = 10 000 это ~50 000 000 операций. При n = 100 000 — уже ~5 000 000 000.
Оптимальное решение — O(n) по времени, O(n) по памяти
def two_sum(nums, target):
seen = {} # значение -> индекс
for i, num in enumerate(nums):
complement = target - num # ищем не пару, а дополнение
if complement in seen:
return [seen[complement], i]
seen[num] = i # запоминаем что видели
Один проход. Для каждого числа спрашиваем: "а его пара уже встречалась?"
При n = 10 000 это 10 000 операций. При n = 100 000 — 100 000.
Что изменилось под капотом
Наивное решение ищет пару перебором. Оптимальное превращает задачу поиска в задачу проверки — а
in для словаря это O(1).Мы платим памятью O(n) за словарь и получаем скорость O(n) вместо O(n²).
Trade-off явный: если память критична и n небольшое — наивный вариант может быть лучше. Об этом стоит сказать на собесе сам, не дожидаясь вопроса.
Edge cases которые нельзя забыть:
# Один и тот же элемент дважды?
nums = [3, 3], target = 6 # должно вернуть [0, 1] — работает корректно
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
❌ Rookie Mistakes: изменяемый аргумент по умолчанию
Этот баг живёт в продакшене чаще, чем хочется признавать.
Функция ведёт себя как будто помнит предыдущие вызовы. Но откуда?
Почему это происходит
Аргументы по умолчанию вычисляются один раз — в момент определения функции, а не при каждом вызове.
Список
Проверить можно напрямую:
Список накапливается прямо там.
Правильный вариант
🐍Вопросы с собесов -> ProstoPython
Этот баг живёт в продакшене чаще, чем хочется признавать.
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("a")) # ["a"]
print(add_item("b")) # ["a", "b"] -- ожидали ["b"]
print(add_item("c")) # ["a", "b", "c"] -- ожидали ["c"]
Функция ведёт себя как будто помнит предыдущие вызовы. Но откуда?
Почему это происходит
Аргументы по умолчанию вычисляются один раз — в момент определения функции, а не при каждом вызове.
Список
[] создаётся ровно один раз и живёт внутри объекта функции. Каждый вызов без аргумента получает ссылку на один и тот же список.Проверить можно напрямую:
print(add_item.__defaults__) # (["a", "b", "c"],)
Список накапливается прямо там.
Правильный вариант
def add_item(item, items=None):
if items is None:
items = [] # новый список при каждом вызове
items.append(item)
return items
print(add_item("a")) # ["a"]
print(add_item("b")) # ["b"]
None — это иммутабельный синглтон. Он не накапливает состояние. Проверка if items is None создаёт свежий список при каждом вызове без аргумента.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍3
🧩 Code Cleanup
Protocol — утиная типизация со статической проверкой
Типичная ситуация — хочешь принимать любой объект с нужным методом:
Работает — Python не проверяет тип, только наличие метода. Но IDE не подскажет ошибку если передать неправильный объект. И из сигнатуры непонятно что ожидается.
После — Protocol:
Теперь mypy и IDE знают что ожидается. Duck и Person подходят автоматически — без наследования.
Чем отличается от ABC:
Protocol — структурная типизация. ABC — номинальная.
Реальный кейс:
Любой класс с этими методами подходит — без изменения его кода.
🐍Вопросы с собесов -> ProstoPython
Protocol — утиная типизация со статической проверкой
Типичная ситуация — хочешь принимать любой объект с нужным методом:
class Duck:
def quack(self):
return "quack"
class Person:
def quack(self):
return "I'm quacking like a duck"
def make_it_quack(obj):
print(obj.quack())
Работает — Python не проверяет тип, только наличие метода. Но IDE не подскажет ошибку если передать неправильный объект. И из сигнатуры непонятно что ожидается.
После — Protocol:
from typing import Protocol
class Quackable(Protocol):
def quack(self) -> str:
...
def make_it_quack(obj: Quackable) -> None:
print(obj.quack())
Теперь mypy и IDE знают что ожидается. Duck и Person подходят автоматически — без наследования.
make_it_quack(Duck()) # OK
make_it_quack(Person()) # OK
make_it_quack("string") # mypy: ошибка — нет метода quack
Чем отличается от ABC:
# ABC — явное наследование обязательно
class Animal(ABC):
@abstractmethod
def speak(self): ...
class Dog(Animal): # должен явно наследовать
def speak(self): return "woof"
# Protocol — наследование не нужно
class Speakable(Protocol):
def speak(self) -> str: ...
class Dog: # ничего не наследует
def speak(self): return "woof" # автоматически подходит
Protocol — структурная типизация. ABC — номинальная.
Реальный кейс:
from typing import Protocol
class Serializable(Protocol):
def to_dict(self) -> dict: ...
def to_json(self) -> str: ...
def save(obj: Serializable, path: str) -> None:
data = obj.to_dict()
...
Любой класс с этими методами подходит — без изменения его кода.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🏆3
📈 FROM O(...) TO O(...)
Топологическая сортировка: от O(n²) до O(V+E)
Задача: есть список задач с зависимостями. Найти порядок выполнения.
Наивное решение — повторные проходы:
На каждом шаге ищем задачу без зависимостей — O(n²) в худшем случае.
Алгоритм Кана — O(V+E):
O(V+E) — каждая вершина и каждое ребро обрабатываются ровно один раз.
Идея:
Считаем входящие зависимости для каждой задачи. Начинаем с тех у кого 0 зависимостей. Обрабатываем — уменьшаем счётчик зависимостей у соседей. Новые нули добавляем в очередь.
Бонус — обнаружение цикла:
Если в результате меньше вершин чем в графе — есть цикл. Задачи зависят друг от друга и никогда не получат нулевой счётчик.
🐍Вопросы с собесов -> ProstoPython
Топологическая сортировка: от O(n²) до O(V+E)
Задача: есть список задач с зависимостями. Найти порядок выполнения.
# Задачи и их зависимости
# "одеться" требует "надеть носки" и "надеть рубашку"
# "надеть пиджак" требует "одеться"
tasks = {
"носки": [],
"рубашка": [],
"одеться": ["носки", "рубашка"],
"пиджак": ["одеться"],
"галстук": ["рубашка"],
}
# Ответ: ['носки', 'рубашка', 'одеться', 'галстук', 'пиджак']
# или любой другой валидный порядок
Наивное решение — повторные проходы:
def topo_sort_naive(tasks):
result = []
remaining = dict(tasks)
while remaining:
for task, deps in list(remaining.items()):
if all(d in result for d in deps):
result.append(task)
del remaining[task]
break
else:
raise ValueError("Цикл в зависимостях")
return result
На каждом шаге ищем задачу без зависимостей — O(n²) в худшем случае.
Алгоритм Кана — O(V+E):
from collections import deque
def topo_sort(tasks):
in_degree = {task: 0 for task in tasks}
for task, deps in tasks.items():
for dep in deps:
in_degree[task] += 1
queue = deque([t for t, d in in_degree.items() if d == 0])
result = []
while queue:
task = queue.popleft()
result.append(task)
for dependent, deps in tasks.items():
if task in deps:
in_degree[dependent] -= 1
if in_degree[dependent] == 0:
queue.append(dependent)
if len(result) != len(tasks):
raise ValueError("Цикл в зависимостях")
return result
O(V+E) — каждая вершина и каждое ребро обрабатываются ровно один раз.
Идея:
Считаем входящие зависимости для каждой задачи. Начинаем с тех у кого 0 зависимостей. Обрабатываем — уменьшаем счётчик зависимостей у соседей. Новые нули добавляем в очередь.
Бонус — обнаружение цикла:
Если в результате меньше вершин чем в графе — есть цикл. Задачи зависят друг от друга и никогда не получат нулевой счётчик.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍3
В Django есть 3 типа наследования.
Abstract Base Class
🔹 Не создаёт таблицу в БД
🔹 Используется для переиспользования полей
Multi-table inheritance
🔹 Каждая модель → отдельная таблица
🔹 Связь через
Proxy model
🔹 Таблица не создаётся
🔹 Меняется только поведение (методы, менеджеры)
🐍Вопросы с собесов -> ProstoPython
Abstract Base Class
🔹 Не создаёт таблицу в БД
🔹 Используется для переиспользования полей
class Base(models.Model):
created_at = models.DateTimeField()
class Meta:
abstract = True
Multi-table inheritance
🔹 Каждая модель → отдельная таблица
🔹 Связь через
OneToOneclass Base(models.Model):
name = models.CharField(max_length=100)
class Employee(Base):
salary = models.IntegerField()
Proxy model
🔹 Таблица не создаётся
🔹 Меняется только поведение (методы, менеджеры)
class UserProxy(User):
class Meta:
proxy = True
🐍Вопросы с собесов -> ProstoPython
👍2
⏱️ Big O Breakdown — выпуск 24
Посмотри на код:
❓ Какая сложность?
A) O(n)
B) O(n log n)
C) O(n²)
D) O(1)
✅ Правильный ответ: A) O(n)
🧠 Разбор
Первый цикл:
Второй цикл:
Теперь объединяем:
В Big O константы отбрасываются:
📌 Итог
Даже если циклов два,
это не значит O(n²).
Важно:
они выполняются последовательно,
а не вложенно.
🐍Вопросы с собесов -> ProstoPython
Посмотри на код:
def example(n):
i = 0
j = 0
while i < n:
i += 1
while j < n:
j += 1
❓ Какая сложность?
A) O(n)
B) O(n log n)
C) O(n²)
D) O(1)
✅ Правильный ответ: A) O(n)
🧠 Разбор
Первый цикл:
выполняется n раз → O(n)
Второй цикл:
выполняется n раз → O(n)
Теперь объединяем:
O(n) + O(n) = O(2n)
В Big O константы отбрасываются:
O(2n) → O(n)
📌 Итог
Даже если циклов два,
это не значит O(n²).
Важно:
они выполняются последовательно,
а не вложенно.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥4
📈 FROM O(...) TO O(...)
Edit Distance: от O(3ⁿ) до O(n·m)
Задача: найти минимальное количество операций чтобы превратить одну строку в другую. Операции — вставка, удаление, замена символа.
Это называется расстояние Левенштейна. Используется в spell checker, git diff, поиске похожих строк.
Наивное решение — рекурсия:
O(3ⁿ) — три рекурсивных вызова на каждом шаге. На строках длиной 10 — тысячи повторных вычислений.
DP — O(n·m):
Визуализация для "cat" и "cut":
Символы совпали — берём диагональ. Не совпали — минимум из трёх соседей плюс 1.
Оптимизация до O(m) памяти:
Как и в LCS — хранить только две строки таблицы.
🐍Вопросы с собесов -> ProstoPython
Edit Distance: от O(3ⁿ) до O(n·m)
Задача: найти минимальное количество операций чтобы превратить одну строку в другую. Операции — вставка, удаление, замена символа.
s1 = "kitten"
s2 = "sitting"
# Ответ: 3
# kitten → sitten (замена k→s)
# sitten → sittin (замена e→i)
# sittin → sitting (вставка g)
Это называется расстояние Левенштейна. Используется в spell checker, git diff, поиске похожих строк.
Наивное решение — рекурсия:
def edit_distance(s1, s2, i, j):
if i == 0:
return j # вставить j символов
if j == 0:
return i # удалить i символов
if s1[i-1] == s2[j-1]:
return edit_distance(s1, s2, i-1, j-1)
return 1 + min(
edit_distance(s1, s2, i-1, j), # удаление
edit_distance(s1, s2, i, j-1), # вставка
edit_distance(s1, s2, i-1, j-1) # замена
)
O(3ⁿ) — три рекурсивных вызова на каждом шаге. На строках длиной 10 — тысячи повторных вычислений.
DP — O(n·m):
def edit_distance(s1, s2):
n, m = len(s1), len(s2)
dp = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(n + 1):
dp[i][0] = i # удалить i символов
for j in range(m + 1):
dp[0][j] = j # вставить j символов
for i in range(1, n + 1):
for j in range(1, m + 1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1 + min(
dp[i-1][j], # удаление
dp[i][j-1], # вставка
dp[i-1][j-1] # замена
)
return dp[n][m]
Визуализация для "cat" и "cut":
"" c u t
"" 0 1 2 3
c 1 0 1 2
a 2 1 1 2
t 3 2 2 1 ← ответ
Символы совпали — берём диагональ. Не совпали — минимум из трёх соседей плюс 1.
Оптимизация до O(m) памяти:
Как и в LCS — хранить только две строки таблицы.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
❤5
🧰 Code Cleanup
На первый взгляд — обычный код.
Но именно так пишут на автомате… и теряют читаемость.
❌ Плохой код
✅ Улучшенный код
💥 Объяснение
Работает и так, и так.
Но есть нюанс.
👉 В первом варианте ты заставляешь мозг держать:
🔷 временный список
🔷 цикл
🔷 условие
🔷 мутацию (
👉 Во втором — всё выражено как одна мысль:
«возьми имена пользователей старше 18»
И это ключевое отличие.
🧠 Где здесь ловушка
Многие думают:
🔷 «list comprehension — это просто короче»
На самом деле:
🔷 Это декларативный стиль, а не просто синтаксический сахар
Ты описываешь *что хочешь получить*, а не *как это собирать по шагам*.
⚠️ Но не переусердствуй
Вот так — уже плохо:
Становится сложно читать.
🐍Вопросы с собесов -> ProstoPython
На первый взгляд — обычный код.
Но именно так пишут на автомате… и теряют читаемость.
❌ Плохой код
users = [
{"name": "Alice", "age": 17},
{"name": "Bob", "age": 25},
{"name": "Charlie", "age": 16},
]
result = []
for u in users:
if u["age"] >= 18:
result.append(u["name"])
print(result)
✅ Улучшенный код
result = [user["name"] for user in users if user["age"] >= 18]
💥 Объяснение
Работает и так, и так.
Но есть нюанс.
👉 В первом варианте ты заставляешь мозг держать:
🔷 временный список
🔷 цикл
🔷 условие
🔷 мутацию (
append)👉 Во втором — всё выражено как одна мысль:
«возьми имена пользователей старше 18»
И это ключевое отличие.
🧠 Где здесь ловушка
Многие думают:
🔷 «list comprehension — это просто короче»
На самом деле:
🔷 Это декларативный стиль, а не просто синтаксический сахар
Ты описываешь *что хочешь получить*, а не *как это собирать по шагам*.
⚠️ Но не переусердствуй
Вот так — уже плохо:
result = [u["name"].upper() for u in users if u["age"] >= 18 and u["name"].startswith("A")]Становится сложно читать.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
UDP (User Datagram Protocol) — транспортный протокол для передачи данных без установления соединения.
Особенности
🔹 нет установки соединения (connectionless)
🔹 нет гарантии доставки
🔹 нет порядка доставки
🔹 минимальные накладные расходы → высокая скорость
Когда используется
🔹 стриминг (видео/аудио)
🔹 онлайн-игры
🔹 DNS-запросы
Итог: UDP — быстрый, но ненадёжный протокол передачи данных без подтверждений и контроля доставки.
🐍Вопросы с собесов -> ProstoPython
Особенности
🔹 нет установки соединения (connectionless)
🔹 нет гарантии доставки
🔹 нет порядка доставки
🔹 минимальные накладные расходы → высокая скорость
Когда используется
🔹 стриминг (видео/аудио)
🔹 онлайн-игры
🔹 DNS-запросы
Итог: UDP — быстрый, но ненадёжный протокол передачи данных без подтверждений и контроля доставки.
🐍Вопросы с собесов -> ProstoPython
🔥3
❌ Rookie Mistakes
На первый взгляд — код абсолютно безопасный.
Но именно здесь многие ловят странные баги.
❌ Пример с ошибкой
🤯 Ожидание
💥 Реальность
🧠 Почему это ошибка
На первый взгляд кажется:
data={} создаётся заново при каждом вызове.
Но…
👉 Значение по умолчанию создаётся один раз
👉 И это один и тот же словарь
Ты не создаёшь новый dict — ты каждый раз дописываешь в старый
⚠️ Именно здесь чаще всего ошибаются
Многие думают:
«Функция вызвалась → значит всё внутри новое»
На самом деле:
Дефолтные аргументы живут между вызовами
✅ Исправленный вариант
Теперь каждый вызов создаёт новый словарь.
⚡️ Краткий вывод
Если видишь в аргументах:
👉 это потенциальный баг
🐍Вопросы с собесов -> ProstoPython
На первый взгляд — код абсолютно безопасный.
Но именно здесь многие ловят странные баги.
❌ Пример с ошибкой
def add_user(name, data={}):
data[name] = len(name)
return data
print(add_user("Alice"))
print(add_user("Bob"))
print(add_user("Charlie"))🤯 Ожидание
{'Alice': 5}
{'Bob': 3}
{'Charlie': 7}💥 Реальность
{'Alice': 5}
{'Alice': 5, 'Bob': 3}
{'Alice': 5, 'Bob': 3, 'Charlie': 7}🧠 Почему это ошибка
На первый взгляд кажется:
data={} создаётся заново при каждом вызове.
Но…
👉 Значение по умолчанию создаётся один раз
👉 И это один и тот же словарь
Ты не создаёшь новый dict — ты каждый раз дописываешь в старый
⚠️ Именно здесь чаще всего ошибаются
Многие думают:
«Функция вызвалась → значит всё внутри новое»
На самом деле:
Дефолтные аргументы живут между вызовами
✅ Исправленный вариант
def add_user(name, data=None):
if data is None:
data = {}
data[name] = len(name)
return data
Теперь каждый вызов создаёт новый словарь.
⚡️ Краткий вывод
Если видишь в аргументах:
{} / [] / set()👉 это потенциальный баг
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
🧩 Code Cleanup
itertools — перестань писать вложенные циклы руками
Типичный код когда нужно перебрать все пары:
Работает. Но две вложенности только ради декартова произведения — многовато.
После:
Одна строка. Читается как задача — "произведение цветов и размеров".
🐍Вопросы с собесов -> ProstoPython
itertools — перестань писать вложенные циклы руками
Типичный код когда нужно перебрать все пары:
colors = ["red", "blue"]
sizes = ["S", "M", "L"]
result = []
for color in colors:
for size in sizes:
result.append((color, size))
Работает. Но две вложенности только ради декартова произведения — многовато.
После:
from itertools import product
result = list(product(colors, sizes))
# [('red','S'),('red','M'),('red','L'),('blue','S'),('blue','M'),('blue','L')]
Одна строка. Читается как задача — "произведение цветов и размеров".
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
❌ Rookie Mistakes
Не сравнивай float через ==
Код который ломается в самый неожиданный момент:
Выведет "не равно". Всегда.
Числа с плавающей точкой хранятся в бинарном виде — и большинство десятичных дробей не представимы точно. 0.1 в памяти это не ровно 0.1 — это ближайшее представимое число.
🐍Вопросы с собесов -> ProstoPython
Не сравнивай float через ==
Код который ломается в самый неожиданный момент:
total = 0.1 + 0.2
if total == 0.3:
print("равно")
else:
print("не равно")
Выведет "не равно". Всегда.
print(0.1 + 0.2) # 0.30000000000000004
Числа с плавающей точкой хранятся в бинарном виде — и большинство десятичных дробей не представимы точно. 0.1 в памяти это не ровно 0.1 — это ближайшее представимое число.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍3
В множество (set) можно помещать только hashable (хешируемые) объекты.
Требования
🔹 объект имеет
🔹 объект неизменяемый (immutable)
🔹 корректно реализует
Можно
🔹
🔹
🔹
Нельзя
🔹
🔹
🔹
(они изменяемые → нет стабильного хеша)
🐍Вопросы с собесов -> ProstoPython
Требования
🔹 объект имеет
__hash__()🔹 объект неизменяемый (immutable)
🔹 корректно реализует
__eq__()Можно
🔹
int, float, str, bool🔹
tuple (если внутри тоже hashable)🔹
frozensetНельзя
🔹
list🔹
dict🔹
set(они изменяемые → нет стабильного хеша)
🐍Вопросы с собесов -> ProstoPython
🔥4