Top K частых символов
Реализуйте функцию find_top_k(s, k), которая возвращает k самых часто встречающихся символов в строке.
Нельзя использовать collections.Counter — частоты нужно посчитать обычным Python-кодом.
Если у символов одинаковая частота, верните их в лексикографическом порядке. Это правило добавлено для детерминированных автотестов.
Сигнатура
def find_top_k(s: str, k: int) -> list[str]:
Пример
find_top_k("abracadabra", 3) -> ["a", "b", "r"]Решение прямо на странице
Напишите код, запустите проверки и только потом открывайте разбор.
Нажмите «Запустить проверки» или Ctrl+Enter.
Показать разбор
Подсказки
- Сначала частоты
Используйте dict: freq[ch] = freq.get(ch, 0) + 1.
- Сортировка
Сортируйте пары (символ, частота) по ключу (-частота, символ).
Идея решения
Подход: посчитать частоты в словаре, затем отсортировать символы по двум ключам: частота по убыванию и сам символ по возрастанию.
Почему не Counter? На собеседовании просили написать подсчет вручную, чтобы проверить работу со словарем и оценку сложности.
Оптимизация: если уникальных символов много, а k маленькое, можно использовать heap и получить O(n + m log k).
Эталонный код
def find_top_k(s: str, k: int) -> list[str]:
freq: dict[str, int] = {}
for ch in s:
freq[ch] = freq.get(ch, 0) + 1
ordered = sorted(freq.items(), key=lambda item: (-item[1], item[0]))
return [ch for ch, _ in ordered[:k]]Retry-декоратор
Реализуйте decorator retry, который повторяет вызов функции при указанном типе исключения.
Требования:
- вернуть результат исходной функции при успехе;
- повторять только исключения типа
on; - остальные исключения пробрасывать сразу;
- после исчерпания попыток пробросить последнее подходящее исключение;
- сохранить metadata функции через
functools.wraps.
Решение прямо на странице
Напишите код, запустите проверки и только потом открывайте разбор.
Нажмите «Запустить проверки» или Ctrl+Enter.
Показать разбор
Подсказки
- Три уровня функций
retry(on, num_attempts) возвращает decorator, decorator(func) возвращает wrapper.
- Последняя попытка
Если это последняя попытка, внутри except нужно снова поднять исключение через raise.
- Метаданные
Используйте @wraps(func), иначе имя функции станет wrapper.
Идея решения
retry — это фабрика декораторов: внешний уровень принимает настройки, средний получает функцию, внутренний wrapper вызывает функцию в цикле.
Ловим только except on, чтобы не скрывать неожиданные ошибки. На последней попытке делаем raise, чтобы наружу ушло исходное исключение с нормальным traceback. functools.wraps сохраняет имя и метаданные исходной функции.
Эталонный код
from functools import wraps
def retry(on: type[Exception], num_attempts: int = 1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(num_attempts):
try:
return func(*args, **kwargs)
except on:
if attempt == num_attempts - 1:
raise
return wrapper
return decorator
def run_retry_case(outcomes: list[str], retry_on: str, num_attempts: int) -> dict:
exception_types = {
'KeyError': KeyError,
'ValueError': ValueError,
'RuntimeError': RuntimeError,
}
state = {'calls': 0, 'index': 0}
def flaky():
state['calls'] += 1
outcome = outcomes[state['index']]
state['index'] += 1
if outcome.startswith('ok:'):
return outcome[3:]
raise exception_types[outcome](outcome)
wrapped = retry(exception_types[retry_on], num_attempts)(flaky)
try:
value = wrapped()
return {'status': 'ok', 'value': value, 'calls': state['calls'], 'name': getattr(wrapped, '__name__', '')}
except Exception as exc:
return {'status': 'error', 'error': type(exc).__name__, 'calls': state['calls'], 'name': getattr(wrapped, '__name__', '')}Moving average за последние 5 минут
Реализуйте класс MovingAvg, который хранит значения метрики и возвращает среднее за последние 5 минут.
Интерфейс:
class MovingAvg:
def record(self, value: int) -> None: ...
def mean(self) -> float: ...
def now(self) -> int: ...
now() уже реализован внешней системой и возвращает текущее время в секундах. В проверках время подменяется, а ваш класс вызывается через helper.Решение прямо на странице
Напишите код, запустите проверки и только потом открывайте разбор.
Нажмите «Запустить проверки» или Ctrl+Enter.
Показать разбор
Подсказки
- Очередь событий
Нужна структура, из которой удобно удалять самые старые записи.
- Не пересчитывайте сумму
Поддерживайте текущую сумму и вычитайте значение, когда удаляете старую запись.
- Граница окна
Событие с timestamp ровно now() - 300 еще находится в последних 5 минутах.
Идея решения
Храним пары (timestamp, value) в очереди и текущую сумму актуальных значений.
Перед расчетом среднего и после записи удаляем из начала очереди все события старше 300 секунд относительно текущего now(). Тогда среднее считается за O(1), а удаление амортизированно O(1), потому что каждая запись удаляется только один раз.
Эталонный код
from collections import deque
class MovingAvg:
def __init__(self):
self.data = deque()
self.cur_sum = 0
def record(self, value: int) -> None:
ts = self.now()
self.data.append((ts, value))
self.cur_sum += value
self._clean_old(ts)
def mean(self) -> float:
ts = self.now()
self._clean_old(ts)
if not self.data:
return 0.0
return self.cur_sum / len(self.data)
def _clean_old(self, ts: int) -> None:
while self.data and ts - self.data[0][0] > 300:
_, value = self.data.popleft()
self.cur_sum -= value
def now(self) -> int:
raise NotImplementedError
def moving_average_results(events: list[list]) -> list[float]:
obj = MovingAvg()
current_time = 0
def fake_now():
return current_time
obj.now = fake_now
result = []
for event in events:
if event[0] == 'record':
current_time = event[1]
obj.record(event[2])
elif event[0] == 'mean':
current_time = event[1]
result.append(obj.mean())
return resultСчетчик попыток в retry-loop
Как считать attempts в retry-декораторе так, чтобы логи и метрики не искажали реальное число вызовов?
Сначала проговорите ответ вслух или тезисами.
Формулы, план решения, риски и примеры.
Откройте разбор только после своей попытки.
Показать разбор
Короткий ответ
Attempt нужно увеличивать перед каждой реальной попыткой вызова, логировать номер попытки, исключение и итоговый статус отдельно от общего числа retry.
Подробный разбор
В retry-loop полезно различать initial attempt и retries. Счетчик должен соответствовать фактическим вызовам функции: перед call фиксируется attempt number, после exception пишутся тип ошибки, elapsed time, delay до следующей попытки и решение retry/stop.
Метрики лучше разделить: attempts_total, retries_total, failures_after_retries, success_after_retry и latency. Тогда retry не маскирует нестабильность зависимости: успешный итог после трех попыток все равно виден как degraded behavior.
Метрики времени и значений ошибки
Что именно считать в метриках: timestamps, длительность операции или сами значения ошибки?
Сначала проговорите ответ вслух или тезисами.
Формулы, план решения, риски и примеры.
Откройте разбор только после своей попытки.
Показать разбор
Короткий ответ
Timestamps нужны для корреляции событий, duration - для latency/SLO, error labels - для группировки причин. Эти сигналы нельзя смешивать в одну метрику.
Подробный разбор
В observability разные поля отвечают на разные вопросы. Timestamp показывает, когда произошло событие и как оно коррелирует с deploy, нагрузкой или внешним incident. Duration измеряет latency операции и проверяет SLO. Error type/status/reason помогают группировать причины отказов.
Практичный набор: histogram latency, counter ошибок по типу, retry attempts, timeout count, success/failure outcome и trace id. Значение exception не всегда безопасно превращать в label: high cardinality ломает метрики, поэтому подробности часто уходят в logs/traces.
Cleanup и fallback при ошибке
Как не заблокировать систему, если fallback или cleanup тоже может упасть?
Сначала проговорите ответ вслух или тезисами.
Формулы, план решения, риски и примеры.
Откройте разбор только после своей попытки.
Показать разбор
Короткий ответ
Cleanup должен быть best-effort и изолирован от основного failure path. Fallback имеет timeout, bounded retries и понятный degraded result.
Подробный разбор
После ошибки нельзя бесконечно зависать в cleanup или fallback. Cleanup стоит делать идемпотентным и best-effort: закрыть ресурс, записать лог, отправить событие, но не блокировать основной поток без лимита времени.
Fallback должен иметь отдельный timeout, ограниченное число попыток и предсказуемый результат: cached value, default response, queued retry или явная ошибка. В логах важно различать original failure и failure fallback-ветки, иначе отладка станет непрозрачной.
Randomized backoff и jitter
Какой backoff выбрать для retry и зачем добавлять jitter?
Сначала проговорите ответ вслух или тезисами.
Формулы, план решения, риски и примеры.
Откройте разбор только после своей попытки.
Показать разбор
Короткий ответ
Exponential backoff снижает давление на зависимость, jitter рассинхронизирует клиентов, а общий budget ограничивает latency и число попыток.
Подробный разбор
Fixed delay прост, но при массовом сбое все клиенты могут повторять запросы синхронно. Exponential backoff увеличивает паузы после повторных ошибок и снижает нагрузку на деградирующую зависимость. Jitter добавляет случайность, чтобы избежать retry storm.
Нужны верхняя граница delay, максимум attempts и общий deadline. Retry уместен для transient ошибок: timeout, 429, 503. Для validation errors или deterministic failures retry только увеличит нагрузку и задержку.