Prosto Python | вопросы с собесов
363 subscribers
184 photos
1 video
2 files
559 links
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
Download Telegram
⏱️ Big O Breakdown

Поиск в словаре по вложенным ключам. Где прячется KeyError

Какова сложность и что выведет код?

data = {
"users": {
"alice": {
"age": 30
}
}
}

print(data["users"]["bob"]["age"])
print(data.get("users").get("bob").get("age"))


Варианты:
А) Оба выведут None
Б) Первый — KeyError, второй — None
В) Первый — KeyError, второй — AttributeError
Г) Оба бросят исключение

Ответ: В) Первый — KeyError, второй — AttributeError

Первый падает сразу — ключа "bob" нет.

Второй падает тоже — но по другой причине:

data.get("users")          # {"alice": {...}}
.get("bob") # None — ключа нет
.get("age") # AttributeError: None не имеет метода get


Цепочка get() не защищает от None в середине.

Правильные варианты:

# Вариант 1 — явная проверка
user = data.get("users", {}).get("bob", {}).get("age")
print(user) # None — безопасно

# Вариант 2 — try/except
try:
age = data["users"]["bob"]["age"]
except KeyError:
age = None

# Вариант 3 — если структура глубокая, используй reduce
from functools import reduce

def deep_get(d, *keys):
return reduce(lambda x, k: x.get(k, {}) if isinstance(x, dict) else {}, keys, d)

deep_get(data, "users", "bob", "age") # {}


Сложность каждого обращения:

Каждый get или [] — O(1). Цепочка из n ключей — O(n) где n количество уровней вложенности, не размер словаря.

🐍Вопросы с собесов -> ProstoPython
👍4
🧩 Code Cleanup

Избавься от вложенных условий через ранний выход

Типичный код:

def process_order(order):
if order:
if order.is_paid():
if order.items:
if order.user.is_active():
ship(order)
else:
raise UserInactiveError()
else:
raise EmptyOrderError()
else:
raise PaymentError()
else:
raise OrderNotFoundError()


Пирамида вложенности. Чтобы понять что происходит в центре — нужно держать в голове все условия снаружи.

После — инвертируй условия, выходи раньше:

def process_order(order):
if not order:
raise OrderNotFoundError()
if not order.is_paid():
raise PaymentError()
if not order.items:
raise EmptyOrderError()
if not order.user.is_active():
raise UserInactiveError()

ship(order)


Та же логика — но читается сверху вниз. Каждая проверка независима. Основной сценарий — в самом конце, без вложенности.

Правило:

Обрабатывай граничные случаи и ошибки в начале. Основная логика — последняя, без вложенности.

Это называется guard clauses — охранные условия.

Ещё пример — возврат значения:

# До
def get_discount(user):
if user:
if user.is_premium():
return 0.3
else:
return 0.1
else:
return 0

# После
def get_discount(user):
if not user:
return 0
if user.is_premium():
return 0.3
return 0.1


🐍Вопросы с собесов -> ProstoPython
👍3
Rookie Mistakes

Не игнорируй типы возвращаемых значений

Частая ошибка — не проверять что возвращает функция:

nums = [3, 1, 2]
sorted_nums = nums.sort()
print(sorted_nums) # None


list.sort() сортирует на месте и возвращает None. Многие ожидают отсортированный список.

Ещё примеры методов которые возвращают None:

result = my_list.append(5)     # None
result = my_list.reverse() # None
result = my_dict.update({}) # None
result = my_set.add(1) # None


Все методы которые мутируют объект — возвращают None. Это соглашение Python: либо мутируешь, либо возвращаешь новое.

Ловушка в цепочках:

# Пытаемся отсортировать и взять первый элемент
first = [3, 1, 2].sort()[0] # TypeError: NoneType не поддерживает индексы


Правильно:

nums = [3, 1, 2]
nums.sort()
first = nums[0] # 1

# Или через sorted() если нужен новый список
first = sorted(nums)[0]


Как не попасться:

Методы без возвращаемого значения в документации помечены как "-> None". Мутирующие методы никогда не возвращают результат.

Простое правило — если метод изменяет объект, он возвращает None. Если создаёт новый — возвращает его.

sorted(nums)      # создаёт новый → возвращает список
nums.sort() # мутирует → возвращает None

"hello".upper() # создаёт новую строку → возвращает строку


🐍Вопросы с собесов -> ProstoPython
🔥3
Попробуйте ответить без запуска, что выведет код, разбор будет через 2 часа

🐍Вопросы с собесов -> ProstoPython
🧠 ЧТО ВЫВЕДЕТ КОД

Изменяемый аргумент по умолчанию. Ловушка которую все знают но все равно попадаются

def add_item(item, items=[]):
items.append(item)
return items

print(add_item("a"))
print(add_item("b"))
print(add_item("c"))


Варианты:
А) ["a"], ["b"], ["c"]
Б) ["a"], ["a", "b"], ["a", "b", "c"]
В) ["a"], ["b"], ["a", "b", "c"]
Г) ошибка

Ответ: Б) ["a"], ["a", "b"], ["a", "b", "c"]

Дефолтный аргумент создаётся один раз — при определении функции, а не при каждом вызове. Все вызовы без явного items делят один и тот же список.

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"]
print(add_item("c")) # ["c"]


None — иммутабельный. Каждый раз создаём новый список внутри функции.

То же самое с dict и set:

def add_tag(key, value, tags={}):  # та же ловушка
tags[key] = value
return tags


Любой мутабельный объект как дефолт — потенциальный баг.

🐍Вопросы с собесов -> ProstoPython
👍2
📈 FROM O(...) TO O(...)

Число перестановок: от рекурсии к итерации

Задача: сгенерировать все перестановки списка.

nums = [1, 2, 3]
# Ответ: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]


Наивное решение — рекурсия:

def permutations(nums):
if len(nums) <= 1:
return [nums[:]]
result = []
for i in range(len(nums)):
rest = nums[:i] + nums[i+1:]
for p in permutations(rest):
result.append([nums[i]] + p)
return result


O(n!) время — не избежать, перестановок именно столько. Но O(n!) памяти — храним все результаты сразу. На n=10 это 3 628 800 списков.

Генератор — O(n) дополнительной памяти:

from itertools import permutations

for p in permutations([1, 2, 3]):
process(p)


Стандартная библиотека генерирует перестановки лениво — по одной. Не строит весь список в памяти.

Если нужна своя реализация — алгоритм Хипа:

def permutations(nums):
def generate(k, arr):
if k == 1:
yield arr[:]
return
yield from generate(k-1, arr)
for i in range(k-1):
if k % 2 == 0:
arr[i], arr[k-1] = arr[k-1], arr[i]
else:
arr[0], arr[k-1] = arr[k-1], arr[0]
yield from generate(k-1, arr)

yield from generate(len(nums), nums[:])

print(list(permutations([1, 2, 3])))


Генератор — каждая перестановка вычисляется только когда нужна.

Сравнение:

рекурсия с result[]  → O(n!) память — всё в RAM сразу
генератор → O(n) память — одна перестановка за раз


🐍Вопросы с собесов -> ProstoPython
👍3
🧩 Code Cleanup

Используй ABC чтобы объявить контракт явно

Типичная ситуация — базовый класс который нужно наследовать:


class Animal:
def speak(self):
raise NotImplementedError

class Dog(Animal):
def speak(self):
return "woof"

class Cat(Animal):
pass # забыли реализовать speak


Ошибка проявится только в рантайме — когда вызовем cat.speak(). До этого момента Cat выглядит валидным классом.

После — ABC:


from abc import ABC, abstractmethod

class Animal(ABC):
@abstractmethod
def speak(self):
pass

class Dog(Animal):
def speak(self):
return "woof"

class Cat(Animal):
pass # не реализовали speak

cat = Cat() # TypeError прямо здесь — до любого вызова


Ошибка при создании объекта — не при вызове метода. Намного раньше и понятнее.

Несколько абстрактных методов:


class DataProcessor(ABC):
@abstractmethod
def load(self, path):
pass

@abstractmethod
def process(self, data):
pass

@abstractmethod
def save(self, result, path):
pass


Любой наследник обязан реализовать все три. Это контракт — явный и проверяемый.

Можно миксовать с обычными методами:


class Animal(ABC):
@abstractmethod
def speak(self):
pass

def describe(self):
return f"Я говорю: {self.speak()}"


describe — обычный метод с реализацией. speak — обязателен для наследника.

🐍Вопросы с собесов -> ProstoPython
👍3
⏱️ Big O Breakdown

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)

Ответ: Б) Список — O(n·m), Trie — O(m)

Список перебирает все 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
🔥3
📈 FROM O(...) TO O(...)

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
🔥3
🧠 Interview Thinking

Минимальный путь в графе. 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
👍3
📈 FROM O(...) TO O(...)

Задача о рюкзаке: от 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
👍4
⏱️ Big O Breakdown
Тема: сложность, которая выглядит как 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)

Правильный ответ: A) O(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
3🔥2
Singleton — паттерн, который гарантирует, что у класса существует только один экземпляр и предоставляет к нему глобальную точку доступа.

Зачем нужен

🔹 общий доступ к ресурсу (например, конфиг, логгер)
🔹 контроль над созданием объекта

Особенности


🔹 всегда возвращается один и тот же объект
🔹 скрывает создание экземпляра

🐍Вопросы с собесов -> ProstoPython
🔥3💯1
🧩 Code Cleanup

Используй 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
🔥4
📈 From O(n²) to O(n): поиск двух чисел с заданной суммой

Задача: дан список чисел и целевая сумма 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
👍4
Rookie Mistakes: изменяемый аргумент по умолчанию

Этот баг живёт в продакшене чаще, чем хочется признавать.

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
👍3
🧩 Code Cleanup

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
🏆3
📈 FROM O(...) TO O(...)

Топологическая сортировка: от 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
👍3
В Django есть 3 типа наследования.

Abstract Base Class

🔹 Не создаёт таблицу в БД
🔹 Используется для переиспользования полей

class Base(models.Model):
created_at = models.DateTimeField()

class Meta:
abstract = True


Multi-table inheritance

🔹 Каждая модель → отдельная таблица
🔹 Связь через OneToOne

class 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

Посмотри на код:

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
🔥4