⏱️ Big O Breakdown
Классическое «красивое» решение:
Выглядит как определение из учебника. Какая сложность?
A) O(n)
B) O(n log n)
C) O(n²)
D) O(2ⁿ)
Правильный ответ:D — O(2ⁿ)
Разбор
Каждый вызов
Дерево вызовов растёт экспоненциально:
Заметь, как
Почему именно 2ⁿ
Дерево вызовов имеет высоту
Точнее —
🐍Вопросы с собесов -> ProstoPython
Классическое «красивое» решение:
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
Выглядит как определение из учебника. Какая сложность?
A) O(n)
B) O(n log n)
C) O(n²)
D) O(2ⁿ)
Правильный ответ:
Разбор
Каждый вызов
fib(n) порождает два новых: fib(n-1) и fib(n-2). Те — ещё по два. И так далее.Дерево вызовов растёт экспоненциально:
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ ... ...
fib(2) fib(1)
Заметь, как
fib(3) считается дважды, fib(2) — трижды, fib(1) — ещё больше. Мы пересчитываем одно и то же.fib(40) — уже больше миллиарда вызовов. Почему именно 2ⁿ
Дерево вызовов имеет высоту
n и каждый уровень удваивается. Узлов в нём — порядка 2ⁿ.Точнее —
O(φⁿ), где φ ≈ 1.618 (золотое сечение), но для big-O это эквивалентно O(2ⁿ).🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🏆3
📈 From O(n) to O(1)
Задача
Реализовать очередь: добавляем элементы в конец, забираем — с начала. Классический FIFO.
Наивное решение через
Почему? Список в Python — это массив в памяти. Когда удаляешь нулевой элемент, все остальные сдвигаются на одну позицию влево. На миллионе элементов — миллион операций копирования.
При активной работе с очередью получаем O(n²) суммарно. На больших данных — катастрофа.
Решение:
🐍Вопросы с собесов -> ProstoPython
Задача
Реализовать очередь: добавляем элементы в конец, забираем — с начала. Классический FIFO.
queue.add("a") → ["a"]
queue.add("b") → ["a", "b"]
queue.add("c") → ["a", "b", "c"]
queue.pop_front() → "a" осталось ["b", "c"]Наивное решение через
list — O(n) на popqueue = []
queue.append("a")
queue.append("b")
queue.append("c")
first = queue.pop(0) # ← вот тут проблема
append() — это O(1), отлично. А вот pop(0) — O(n).Почему? Список в Python — это массив в памяти. Когда удаляешь нулевой элемент, все остальные сдвигаются на одну позицию влево. На миллионе элементов — миллион операций копирования.
При активной работе с очередью получаем O(n²) суммарно. На больших данных — катастрофа.
Решение:
collections.deque — O(1) на оба концаfrom collections import deque
queue = deque()
queue.append("a")
queue.append("b")
queue.append("c")
first = queue.popleft() # O(1) ✅
deque (double-ended queue) внутри устроен как связный список блоков. Добавление и удаление с обоих концов — гарантированно O(1).🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
🧰 Code Cleanup
Плохой код
Работает. Но это бомба замедленного действия.
Что не так
Если между
Один такой баг в цикле — и через час процесс падает с
Это касается не только файлов: сокеты, соединения с БД, lock'и, любые ресурсы, которые нужно явно освобождать.
Чистый вариант —
🐍Вопросы с собесов -> ProstoPython
Плохой код
f = open("data.txt")
content = f.read()
f.close()Работает. Но это бомба замедленного действия.
Что не так
Если между
open и close вылетит исключение — close() никогда не выполнится. Файловый дескриптор остаётся открытым.f = open("data.txt")
data = json.loads(f.read()) # ← упало с JSONDecodeError
f.close() # ← сюда мы уже не дошлиОдин такой баг в цикле — и через час процесс падает с
OSError: Too many open files. Лимит файловых дескрипторов в системе не резиновый.Это касается не только файлов: сокеты, соединения с БД, lock'и, любые ресурсы, которые нужно явно освобождать.
Чистый вариант —
withwith open("data.txt") as f:
content = f.read()with гарантирует, что close() вызовется при выходе из блока — независимо от того, было ли исключение.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥4
🧠 Interview Thinking
Задача
Даны две строки. Вернуть
Как думает junior
«Отсортирую обе и сравню»:
Одна строка. Работает. Сложность — O(n log n) из-за сортировки.
На собесе это пройдёт. Но если интервьюер спросит «а быстрее можно?» — junior часто застревает.
Как думает сильный кандидат
Перед кодом задаёт себе вопрос: «Что я вообще проверяю?»
Не порядок символов. А то, что у каждой буквы одинаковое количество вхождений в обеих строках.
Это сравнение частот. Значит — счётчик.
Edge case, который ловит интервьюер
Что если строки разной длины? Сортировка и
Но умный кандидат проверяет длину явно в начале:
Зачем? Это ранний выход за O(1). Если длины разные — даже Counter строить не нужно. На больших строках экономит время.
Это маленький штрих, но он показывает: ты не просто решаешь, а думаешь про производительность.
Уточнения, которые поднимают тебя выше
Сильный кандидат сам спросит ещё до кода:
«Регистр важен?» —
«Пробелы учитываем?» —
🐍Вопросы с собесов -> ProstoPython
Задача
Даны две строки. Вернуть
True, если одна — анаграмма другой (содержит те же символы в любом порядке)."listen", "silent" → True
"hello", "world" → False
"rat", "car" → False
Как думает junior
«Отсортирую обе и сравню»:
def is_anagram(s1, s2):
return sorted(s1) == sorted(s2)
Одна строка. Работает. Сложность — O(n log n) из-за сортировки.
На собесе это пройдёт. Но если интервьюер спросит «а быстрее можно?» — junior часто застревает.
Как думает сильный кандидат
Перед кодом задаёт себе вопрос: «Что я вообще проверяю?»
Не порядок символов. А то, что у каждой буквы одинаковое количество вхождений в обеих строках.
Это сравнение частот. Значит — счётчик.
from collections import Counter
def is_anagram(s1, s2):
return Counter(s1) == Counter(s2)
Counter строится за O(n), сравнение двух Counter'ов — тоже O(n). Итого — O(n) по времени.Edge case, который ловит интервьюер
Что если строки разной длины? Сортировка и
Counter оба корректно вернут False — длина учтётся автоматически.Но умный кандидат проверяет длину явно в начале:
def is_anagram(s1, s2):
if len(s1) != len(s2):
return False
return Counter(s1) == Counter(s2)
Зачем? Это ранний выход за O(1). Если длины разные — даже Counter строить не нужно. На больших строках экономит время.
Это маленький штрих, но он показывает: ты не просто решаешь, а думаешь про производительность.
Уточнения, которые поднимают тебя выше
Сильный кандидат сам спросит ещё до кода:
«Регистр важен?» —
"Listen" и "silent" — анаграммы?«Пробелы учитываем?» —
"conversation" и "voices rant on"?🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
🎭 Red Flag
Перехватываем ошибку и кидаем свою:
Выглядит чисто. На вызывающей стороне — понятное сообщение. Вроде всё хорошо.
Пока что-то не сломается в проде.
Что не так
Ты только что выкинул всю информацию о реальной причине.
Было:
Стало:
Дебажить такое — кошмар. В логах нет ни traceback'а оригинала, ни строки в JSON, ни пути файла. Только бесполезное сообщение.
Как надо —
Сохраняется обе части: твоё человекочитаемое сообщение и техническая причина.
🐍Вопросы с собесов -> ProstoPython
Перехватываем ошибку и кидаем свою:
def load_config(path):
try:
with open(path) as f:
return json.loads(f.read())
except Exception:
raise ValueError("Не удалось загрузить конфиг")
Выглядит чисто. На вызывающей стороне — понятное сообщение. Вроде всё хорошо.
Пока что-то не сломается в проде.
Что не так
Ты только что выкинул всю информацию о реальной причине.
Было:
FileNotFoundError: [Errno 2] No such file: '/etc/app.conf'JSONDecodeError: Expecting value: line 3 column 1 (char 47)PermissionError: [Errno 13] Permission deniedСтало:
ValueError: Не удалось загрузить конфигДебажить такое — кошмар. В логах нет ни traceback'а оригинала, ни строки в JSON, ни пути файла. Только бесполезное сообщение.
Как надо —
raise ... fromtry:
with open(path) as f:
return json.loads(f.read())
except Exception as e:
raise ValueError("Не удалось загрузить конфиг") from e
from e прицепляет оригинальное исключение к новому. В логах теперь увидишь:JSONDecodeError: Expecting value: line 3 column 1
The above exception was the direct cause of the following exception:
ValueError: Не удалось загрузить конфиг
Сохраняется обе части: твоё человекочитаемое сообщение и техническая причина.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
📈 From O(n) to O(log n)
Задача
Дан отсортированный массив. Найти индекс числа
Наивное решение — O(n)
Проходит весь массив. На миллионе элементов — миллион сравнений. Сортировка не используется.
Бинарный поиск — O(log n)
Идея: каждый шаг отбрасываем половину массива.
Смотрим на средний элемент:
равен target → нашли
target меньше → ищем в левой половине
target больше → ищем в правой половине
O(log n) — потому что на каждом шаге размер задачи делится пополам.
Насколько это быстрее
На миллиарде элементов — 30 шагов вместо миллиарда.
🐍Вопросы с собесов -> ProstoPython
Задача
Дан отсортированный массив. Найти индекс числа
target или вернуть -1, если его нет.nums = [1, 3, 5, 7, 9, 11, 13, 15], target = 11
→ 5
Наивное решение — O(n)
def find(nums, target):
for i, n in enumerate(nums):
if n == target:
return i
return -1
Проходит весь массив. На миллионе элементов — миллион сравнений. Сортировка не используется.
Бинарный поиск — O(log n)
Идея: каждый шаг отбрасываем половину массива.
Смотрим на средний элемент:
равен target → нашли
target меньше → ищем в левой половине
target больше → ищем в правой половине
def find(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
O(log n) — потому что на каждом шаге размер задачи делится пополам.
Насколько это быстрее
n линейный поиск бинарный поиск
─────────────────────────────────────────────────
1 000 1 000 ~10
1 000 000 1 000 000 ~20
1 000 000 000 1 миллиард ~30
На миллиарде элементов — 30 шагов вместо миллиарда.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥4
🧠 Interview Thinking
Все знают FizzBuzz. Это не задача на алгоритмы. Это задача на то, как ты пишешь код.
Условие
Числа от 1 до n. Для каждого:
кратно 3 →
кратно 5 →
кратно и 3, и 5 →
иначе → само число
Как пишет junior
Работает. Принято.
Но интервьюер уже видит проблемы:
условие
что будет, если завтра добавят
Как пишет сильный кандидат
Сначала проговаривает структуру:
И пишет:
Заметь:
никаких
никакой проблемы порядка — Fizz всегда перед Buzz
никакого
А если масштабировать?
Сильный кандидат предложит сам:
Теперь добавить новое правило — это одна строка в списке, а не новая ветка в функции.
🐍Вопросы с собесов -> ProstoPython
Все знают FizzBuzz. Это не задача на алгоритмы. Это задача на то, как ты пишешь код.
Условие
Числа от 1 до n. Для каждого:
кратно 3 →
"Fizz"кратно 5 →
"Buzz"кратно и 3, и 5 →
"FizzBuzz"иначе → само число
Как пишет junior
for i in range(1, n + 1):
if i % 3 == 0 and i % 5 == 0:
print("FizzBuzz")
elif i % 3 == 0:
print("Fizz")
elif i % 5 == 0:
print("Buzz")
else:
print(i)
Работает. Принято.
Но интервьюер уже видит проблемы:
условие
i % 3 == 0 and i % 5 == 0 — это i % 15 == 0, но junior это не упростилчто будет, если завтра добавят
i % 7 == 0 → "Bazz"? Получим лестницу из 8 веток.Как пишет сильный кандидат
Сначала проговаривает структуру:
«Это не три независимых условия, а накопление строки. Для каждого числа я хочу собрать суффиксы: если делится на 3 — добавить Fizz, если на 5 — добавить Buzz. Если ничего не накопилось — вернуть само число.»
И пишет:
for i in range(1, n + 1):
out = ""
if i % 3 == 0:
out += "Fizz"
if i % 5 == 0:
out += "Buzz"
print(out or i)
Заметь:
никаких
and — два независимых условия сами дают «FizzBuzz»никакой проблемы порядка — Fizz всегда перед Buzz
никакого
else — out or i элегантно подставляет число, если строка пустаяА если масштабировать?
Сильный кандидат предложит сам:
«Если бы правил было больше, я бы вынес их в данные.»
RULES = [(3, "Fizz"), (5, "Buzz"), (7, "Bazz")]
for i in range(1, n + 1):
out = "".join(word for div, word in RULES if i % div == 0)
print(out or i)
Теперь добавить новое правило — это одна строка в списке, а не новая ветка в функции.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
🧰 Code Cleanup
Плохой код
Работает. Но связь между списками неявная. Чтобы понять код, нужно мысленно держать индекс
Чистый вариант
В цикле сразу распаковываем — и работаем с именованными переменными, а не с индексами.
🐍Вопросы с собесов -> ProstoPython
Плохой код
names = ["Anna", "Boris", "Carol"]
ages = [30, 25, 40]
for i in range(len(names)):
print(f"{names[i]} — {ages[i]} лет")
Работает. Но связь между списками неявная. Чтобы понять код, нужно мысленно держать индекс
i и индексировать оба списка вручную.Чистый вариант
for name, age in zip(names, ages):
print(f"{name} — {age} лет")
zip склеивает списки в пары: ("Anna", 30), ("Boris", 25), ("Carol", 40).В цикле сразу распаковываем — и работаем с именованными переменными, а не с индексами.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥4
🎭 Red Flag
Работает. На простых случаях. Но это хрупкая проверка типа, и она ломается в самых неприятных местах.
Что не так
Это нарушает принцип подстановки Лисков (LSP): код, который работает с базовым типом, должен работать с наследниками.
🐍Вопросы с собесов -> ProstoPython
def process(value):
if type(value) == list:
return sum(value)
if type(value) == int:
return value * 2
Работает. На простых случаях. Но это хрупкая проверка типа, и она ломается в самых неприятных местах.
Что не так
type(x) == X проверяет точное совпадение типа. Любой наследник провалит проверку:class MyList(list):
pass
m = MyList([1, 2, 3])
type(m) == list # False ❗️
isinstance(m, list) # True ✅
MyList — это список во всех смыслах. У него те же методы, то же поведение. Но type(m) == list его не пропускает.Это нарушает принцип подстановки Лисков (LSP): код, который работает с базовым типом, должен работать с наследниками.
type == этот принцип ломает.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🏆3
⏱️ Big O Breakdown
Какая сложность?
A) O(n)
B) O(2n)
C) O(n²)
D) Зависит от размера
Правильный ответ:A — O(n)
Разбор
Глаз видит два цикла и хочет сказать «O(n²)». Но n² появляется только тогда, когда циклы вложены — один внутри другого.
Здесь циклы последовательные: сначала отработал первый (n шагов), потом второй (ещё n шагов). Итого —
В big-O константы отбрасываются:
🐍Вопросы с собесов -> ProstoPython
def process(nums):
for n in nums:
print(n)
for n in nums:
print(n * 2)
nums длины n. Какая сложность?
A) O(n)
B) O(2n)
C) O(n²)
D) Зависит от размера
Правильный ответ:
Разбор
Глаз видит два цикла и хочет сказать «O(n²)». Но n² появляется только тогда, когда циклы вложены — один внутри другого.
Здесь циклы последовательные: сначала отработал первый (n шагов), потом второй (ещё n шагов). Итого —
2n шагов.В big-O константы отбрасываются:
2n — это O(n). Не O(2n), а именно O(n).🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥3
🧰 Code Cleanup
Плохой код
Работает. Но это двойная проверка там, где хватит одной.
Чистый вариант
🐍Вопросы с собесов -> ProstoPython
Плохой код
if is_active == True:
do_something()
if items == []:
return None
Работает. Но это двойная проверка там, где хватит одной.
Чистый вариант
if is_active:
do_something()
if not items:
return None
if сам приводит значение к булеву. Не нужно сравнивать с True или с пустым контейнером — питон уже понимает, что считать «истиной».🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍3
📈 From O(n·m) to O(n+m)
Задача
Даны два отсортированных массива. Слить их в один отсортированный.
Наивное решение — O((n+m) log(n+m))
Работает. Но мы выбрасываем то, что массивы уже отсортированы. Платим за сортировку «с нуля».
Сравнить через
Вложенный цикл по парам — катастрофа.
Два указателя — O(n+m)
Идём по обоим массивам одновременно. На каждом шаге берём меньший элемент:
Каждый элемент обоих массивов посещается ровно один раз. Итого — O(n+m) по времени.
🐍Вопросы с собесов -> ProstoPython
Задача
Даны два отсортированных массива. Слить их в один отсортированный.
a = [1, 4, 7, 10]
b = [2, 3, 8, 11]
→ [1, 2, 3, 4, 7, 8, 10, 11]
Наивное решение — O((n+m) log(n+m))
def merge(a, b):
return sorted(a + b)
Работает. Но мы выбрасываем то, что массивы уже отсортированы. Платим за сортировку «с нуля».
Сравнить через
in? Ещё хуже — O(n·m)result = []
for x in a:
for y in b:
if y < x and y not in result:
result.append(y)
result.append(x)
Вложенный цикл по парам — катастрофа.
Два указателя — O(n+m)
Идём по обоим массивам одновременно. На каждом шаге берём меньший элемент:
def merge(a, b):
i = j = 0
result = []
while i < len(a) and j < len(b):
if a[i] <= b[j]:
result.append(a[i])
i += 1
else:
result.append(b[j])
j += 1
result.extend(a[i:]) # докинуть остаток
result.extend(b[j:])
return result
Каждый элемент обоих массивов посещается ровно один раз. Итого — O(n+m) по времени.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍3
⏱️ Big O Breakdown
Какая сложность?
A) O(n)
B) O(n log n)
C) O(n²)
D) O(n²) только в худшем случае
Правильный ответ: C — O(n²)
Разбор
Внешний цикл —
А вот внутри прячется коварство:
Сумма:
🐍Вопросы с собесов -> ProstoPython
def running_average(nums):
result = []
for i in range(1, len(nums) + 1):
result.append(sum(nums[:i]) / i)
return result
nums длины n. Какая сложность?
A) O(n)
B) O(n log n)
C) O(n²)
D) O(n²) только в худшем случае
Правильный ответ:
Разбор
Внешний цикл —
n шагов. Это очевидно.А вот внутри прячется коварство:
sum(nums[:i]) на каждом шаге проходит по всем элементам до i. Это O(i) операций.i=1: sum по 1 элементу
i=2: sum по 2 элементам
i=3: sum по 3 элементам
...
i=n: sum по n элементам
Сумма:
1 + 2 + 3 + ... + n ≈ n²/2. Итого — O(n²).🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🏆3
🧠 Interview Thinking
Задача
Дан массив цен акции по дням. Купить один раз, продать позже — максимизировать прибыль.
Как думает junior — O(n²)
Перебирает все пары «день покупки / день продажи»:
Работает. На большом входе — таймаут.
Как думает сильный кандидат — O(n)
Главный вопрос: что мне нужно знать, чтобы посчитать прибыль в день
Только одно — самая низкая цена среди предыдущих дней. Если я её помню, то прибыль =
Значит, идём слева направо, на каждом шаге обновляем две вещи:
минимальную цену из увиденных
лучшую прибыль из увиденных
Один проход. O(n) время, O(1) память.
🐍Вопросы с собесов -> ProstoPython
Задача
Дан массив цен акции по дням. Купить один раз, продать позже — максимизировать прибыль.
prices = [7, 1, 5, 3, 6, 4]
→ 5 (купить за 1 в день 1, продать за 6 в день 4)
Как думает junior — O(n²)
Перебирает все пары «день покупки / день продажи»:
best = 0
for i in range(len(prices)):
for j in range(i + 1, len(prices)):
best = max(best, prices[j] - prices[i])
Работает. На большом входе — таймаут.
Как думает сильный кандидат — O(n)
Главный вопрос: что мне нужно знать, чтобы посчитать прибыль в день
i?Только одно — самая низкая цена среди предыдущих дней. Если я её помню, то прибыль =
prices[i] - min_so_far.Значит, идём слева направо, на каждом шаге обновляем две вещи:
минимальную цену из увиденных
лучшую прибыль из увиденных
def max_profit(prices):
min_price = float("inf")
best = 0
for price in prices:
min_price = min(min_price, price) # самый дешёвый день до сих пор
best = max(best, price - min_price) # лучшая прибыль до сих пор
return best
Один проход. O(n) время, O(1) память.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍3
🧰 Code Cleanup
Плохой код
Откуда взялись
Это и есть магические числа: непонятные литералы прямо в коде.
Чистый вариант
Та же логика. Но теперь код читается как описание правил, а не как ребус.
Что мы получили
1. Самодокументируемость.
2. Изменения в одном месте. Если VAT поменяется с 20% на 21% — правишь одну строку, а не охотишься за
🐍Вопросы с собесов -> ProstoPython
Плохой код
def calculate_price(items):
total = sum(item.price for item in items)
if total > 5000:
total *= 0.9
if len(items) > 10:
total -= 200
return total + total * 0.2
Откуда взялись
5000, 0.9, 10, 200, 0.2? Что они значат? Без знания бизнес-логики — никак не понять.Это и есть магические числа: непонятные литералы прямо в коде.
Чистый вариант
DISCOUNT_THRESHOLD = 5000
DISCOUNT_RATE = 0.9
BULK_ITEM_THRESHOLD = 10
BULK_DISCOUNT = 200
VAT_RATE = 0.2
def calculate_price(items):
total = sum(item.price for item in items)
if total > DISCOUNT_THRESHOLD:
total *= DISCOUNT_RATE
if len(items) > BULK_ITEM_THRESHOLD:
total -= BULK_DISCOUNT
return total + total * VAT_RATE
Та же логика. Но теперь код читается как описание правил, а не как ребус.
Что мы получили
1. Самодокументируемость.
total > DISCOUNT_THRESHOLD объясняет сам себя.2. Изменения в одном месте. Если VAT поменяется с 20% на 21% — правишь одну строку, а не охотишься за
0.2 по всему файлу.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥3
⚖️ This vs That
Оба сравнивают. Разница ловится в одной фразе.
«Эти два объекта одинаковые по содержанию?»
«Это один и тот же объект в памяти?»
Где это путают
Самая частая ошибка — сравнение с числами и строками:
CPython кэширует маленькие числа (от -5 до 256) и короткие строки. Поэтому:
Никогда не используй
Когда
Для синглтонов Python:
🐍Вопросы с собесов -> ProstoPython
Оба сравнивают. Разница ловится в одной фразе.
== — равенство значений«Эти два объекта одинаковые по содержанию?»
[1, 2, 3] == [1, 2, 3] # True
"abc" == "abc" # True
is — идентичность объектов«Это один и тот же объект в памяти?»
a = [1, 2, 3]
b = [1, 2, 3]
c = a
a == b # True — содержимое одинаковое
a is b # False — разные объекты в памяти
a is c # True — одна и та же ссылка
== смотрит что внутри. is смотрит где живёт.Где это путают
Самая частая ошибка — сравнение с числами и строками:
x = 1000
y = 1000
x is y # False (или True — зависит от реализации!)
CPython кэширует маленькие числа (от -5 до 256) и короткие строки. Поэтому:
a = 100; b = 100
a is b # True — оба указывают на кэшированный объект
a = 1000; b = 1000
a is b # False — разные объекты
Никогда не используй
is для сравнения значений. Поведение зависит от внутренней оптимизации интерпретатора. Сегодня работает — завтра нет.Когда
is — единственно правильный выборДля синглтонов Python:
None, True, False.if x is None: # ✅ правильно
if x == None: # ❌ работает, но не идиоматично
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
🧠 Interview Thinking
Задача
Дан массив длины
Как думает junior — O(n) время, O(n) память
Через
Работает, O(n) по времени. Но память — тоже O(n): храним частоты всех уникальных элементов.
На собесе это пройдёт. Но если интервьюер спросит «а можешь без дополнительной памяти?» — junior застревает.
Как думает сильный кандидат — алгоритм Бойера-Мура
Ключевой инсайт: раз мажоритарный элемент встречается больше половины раз, его «голосов» хватит, чтобы перебить все остальные элементы вместе взятые.
Идея: идём по массиву и ведём «счётчик силы» одного кандидата. Если встречаем его же —
O(n) время, O(1) память. Один проход.
🐍Вопросы с собесов -> ProstoPython
Задача
Дан массив длины
n. Найти элемент, который встречается более n/2 раз. Гарантируется, что он есть.nums = [3, 2, 3, 3, 1, 3, 3] (n = 7, нужен элемент > 3 раз)
→ 3
Как думает junior — O(n) время, O(n) память
Через
Counter:from collections import Counter
def majority(nums):
return Counter(nums).most_common(1)[0][0]
Работает, O(n) по времени. Но память — тоже O(n): храним частоты всех уникальных элементов.
На собесе это пройдёт. Но если интервьюер спросит «а можешь без дополнительной памяти?» — junior застревает.
Как думает сильный кандидат — алгоритм Бойера-Мура
Ключевой инсайт: раз мажоритарный элемент встречается больше половины раз, его «голосов» хватит, чтобы перебить все остальные элементы вместе взятые.
Идея: идём по массиву и ведём «счётчик силы» одного кандидата. Если встречаем его же —
+1. Другой — -1. Если счётчик ушёл в ноль — меняем кандидата.def majority(nums):
candidate = None
count = 0
for n in nums:
if count == 0:
candidate = n
count += 1 if n == candidate else -1
return candidate
O(n) время, O(1) память. Один проход.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍5
🧰 Code Cleanup
Плохой код
Все три работают. Но это наследие из старого Python, когда f-strings ещё не было.
Чистый вариант
Короче. Значения прямо на своих местах — не нужно держать в голове, какой
Что ещё умеют f-strings
Выражения прямо внутри:
Форматирование — после двоеточия:
🐍Вопросы с собесов -> ProstoPython
Плохой код
msg = "User %s has %d points" % (name, score)
msg = "User {} has {} points".format(name, score)
msg = "User {name} has {score} points".format(name=name, score=score)
Все три работают. Но это наследие из старого Python, когда f-strings ещё не было.
Чистый вариант
msg = f"User {name} has {score} points"Короче. Значения прямо на своих местах — не нужно держать в голове, какой
{} соответствует какому аргументу.Что ещё умеют f-strings
Выражения прямо внутри:
f"Discount: {price * 0.9:.2f}"
f"Items: {len(cart)}"
f"Greeting: {'Hi' if user else 'Hello'}"Форматирование — после двоеточия:
f"{value:.2f}" # 2 знака после запятой → "3.14"
f"{n:>10}" # выравнивание вправо в 10 символов
f"{n:,}" # разделители тысяч → "1,000,000"
f"{n:08b}" # двоичное с ведущими нулями → "00001010"🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥4
⏱️ Big O Breakdown
Какая сложность
A) O(1)
B) O(n)
C) O(log n)
D) Зависит от типа объекта
Правильный ответ:D — зависит от типа
Для большинства встроенных типов — O(1). Но не для всего.
Разбор
В Python
Так устроены:
У каждого из них внутри уже хранится длина.
Но есть исключения — генераторы
У генератора нет длины, потому что элементы ещё не сгенерированы.
Если очень нужно — конвертируй в список (
🐍Вопросы с собесов -> ProstoPython
nums = [1, 2, 3, ..., 1_000_000]
n = len(nums)
Какая сложность
len()?A) O(1)
B) O(n)
C) O(log n)
D) Зависит от типа объекта
Правильный ответ:
Для большинства встроенных типов — O(1). Но не для всего.
Разбор
В Python
len() зовёт метод __len__ объекта. Если он реализован как прямое чтение поля — это O(1).Так устроены:
list, tuple, str, bytes, dict, set, frozenset, range
У каждого из них внутри уже хранится длина.
len() просто её возвращает — за константное время.len([1] * 1_000_000) # O(1) — мгновенно
len("a" * 1_000_000) # O(1) — мгновенно
Но есть исключения — генераторы
gen = (x for x in range(1000))
len(gen) # ❌ TypeError: object of type 'generator' has no len()
У генератора нет длины, потому что элементы ещё не сгенерированы.
len() не работает в принципе.Если очень нужно — конвертируй в список (
len(list(gen))), но это O(n) и съест всю память.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍5
🧠 Interview Thinking
Задача
Лестница из
Как думает junior — рекурсия в лоб, O(2ⁿ)
«До n-й ступени можно прийти либо с (n-1), либо с (n-2). Значит:»
Работает. И это те же самые числа Фибоначчи.
Сложность — O(2ⁿ). На
Как думает сильный кандидат
Замечает: формула
Дальше — два пути:
Мемоизация (top-down):
Каждое значение считается один раз. O(n) время, O(n) память.
Итеративно (bottom-up):
O(n) время, O(1) память. Лучший вариант.
Главный инсайт
Когда в рекурсии видишь разветвление с повторяющимися подзадачами — это сигнал к динамическому программированию.
🐍Вопросы с собесов -> ProstoPython
Задача
Лестница из
n ступеней. За один шаг можно подняться на 1 или 2 ступени. Сколько различных способов добраться до вершины?n = 4
→ 5
Способы: 1+1+1+1, 1+1+2, 1+2+1, 2+1+1, 2+2
Как думает junior — рекурсия в лоб, O(2ⁿ)
«До n-й ступени можно прийти либо с (n-1), либо с (n-2). Значит:»
def climb(n):
if n <= 2:
return n
return climb(n - 1) + climb(n - 2)
Работает. И это те же самые числа Фибоначчи.
Сложность — O(2ⁿ). На
n = 40 уже секунды, на n = 50 — минуты.Как думает сильный кандидат
Замечает: формула
f(n) = f(n-1) + f(n-2) — это Фибоначчи. И сразу понимает, что подзадачи повторяются.Дальше — два пути:
Мемоизация (top-down):
from functools import lru_cache
@lru_cache(maxsize=None)
def climb(n):
if n <= 2:
return n
return climb(n - 1) + climb(n - 2)
Каждое значение считается один раз. O(n) время, O(n) память.
Итеративно (bottom-up):
def climb(n):
a, b = 1, 2
for _ in range(n - 1):
a, b = b, a + b
return a
O(n) время, O(1) память. Лучший вариант.
Главный инсайт
Когда в рекурсии видишь разветвление с повторяющимися подзадачами — это сигнал к динамическому программированию.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥4