ОП.04 · Тема 05Читать → делать → проверять
Дополнительносамостоятельная практика · 8 разделов

Тренажёр рассуждений

Упражнения этой страницы разбирают, как по условию и ограничениям увидеть, где решение может ошибиться, до того как написан код. Ответы в таблицах получены запуском на Python 3.

Порядок рассуждения

Упражнения ниже разбирают отдельные шаги этого порядка. На собеседовании все шесть шагов укладываются в несколько минут.

  1. УсловиеПересказать задачу своими словами и назвать вход и выход.
  2. ВопросыНайти пробелы: пустой вход, дубликаты, размер, изменение данных.
  3. ПримерыОбычный случай, граница, особый вход — с ожидаемыми ответами.
  4. Идея и оценкаВыбрать алгоритм под ограничения и оценить время и память.
  5. ПрогонПройти код на коротком примере и назвать инвариант.
  6. Код и тестыНаписать код и запустить примеры из третьего шага.

Какой вопрос меняет решение

К любой задаче можно задать много вопросов, а время собеседования ограничено. Полезен вопрос, ответ на который меняет алгоритм, контракт или набор тестов. Если при любом ответе код останется прежним, такой вопрос можно не задавать. В каждой задаче ниже четыре вопроса: решите, какие из них полезны, и сверьтесь с пояснениями.

01

Найдите индекс числа x в массиве.

Выберите вопрос, который задали бы первым. Откройте остальные и сравните.

02

Удалите дубликаты из списка.

Выберите вопрос, который задали бы первым. Откройте остальные и сравните.

03

Найдите самое частое слово в тексте.

Выберите вопрос, который задали бы первым. Откройте остальные и сравните.

Одна задача при разных ограничениях

Задача: вернуть True, если в списке есть повторяющиеся элементы. У неё четыре правильных решения, и какое выбрать, зависит от ответов на уточняющие вопросы. Переключайте ограничения и сравнивайте код, время и память.

Задача · есть ли в списке повторы
Время O(n²)Память O(1)
has_duplicate · n ≤ 1000
def has_duplicate(numbers):
    for i in range(len(numbers)):
        for j in range(i + 1, len(numbers)):
            if numbers[i] == numbers[j]:
                return True
    return False

Почему подходитОколо 500 тысяч сравнений в худшем случае. Код короткий, в нём мало мест для ошибки, и дополнительной памяти он не требует.

Где перестаёт подходитьПри n = 10⁵ сравнений уже около 5·10⁹. На Python это минуты.

Все четыре функции запускались на одних и тех же входах и дали одинаковые ответы:

ВходОтвет всех четырёх версий
[]False
[7]False
[1, 2, 3]False
[3, 1, 3]True
[5, 5]True
[100, 1, 50, 1]True
Грубая прикидка по размеру входа

Для прикидки считают, что простой цикл на Python выполняет порядка 10⁷ шагов в секунду. Точное время показывает только замер. Подставьте n в формулу сложности и сравните результат с этим числом.

nn² шаговn log₂ n шаговЧто это значит
1 00010⁶≈ 10⁴Подходит любое из решений
100 00010¹⁰≈ 1,7·10⁶O(n²) слишком медленно, O(n log n) быстро
1 000 00010¹²≈ 2·10⁷Нужно O(n) или O(n log n)

Поиск контрпримера

В каждой функции ниже одна ошибка. Прочитайте, что функция должна делать, и её код, затем выберите входы, на которых она, по-вашему, ошибётся. После каждого выбора появится строка с результатом запуска. Найдите хотя бы один такой вход и объясните, почему функция на нём ошибается.

01

Среднее значение

Функция возвращает среднее; для пустого списка — None.

Код кандидата
def average(numbers):
    return sum(numbers) / len(numbers)

На каких входах функция ошибётся? Выберите входы и проверьте.

02

Максимум

Функция возвращает наибольший элемент непустого списка.

Код кандидата
def find_max(numbers):
    best = 0
    for value in numbers:
        if value > best:
            best = value
    return best

На каких входах функция ошибётся? Выберите входы и проверьте.

03

Есть ли ноль

Функция возвращает True, если в списке есть 0.

Код кандидата
def contains_zero(numbers):
    for i in range(len(numbers) - 1):
        if numbers[i] == 0:
            return True
    return False

На каких входах функция ошибётся? Выберите входы и проверьте.

04

Палиндром

Функция проверяет, читается ли строка одинаково в обе стороны.

Код кандидата
def is_palindrome(text):
    for i in range(len(text) // 2):
        if text[i] != text[len(text) - i]:
            return False
    return True

На каких входах функция ошибётся? Выберите входы и проверьте.

05

Количество положительных

Функция считает числа строго больше нуля.

Код кандидата
def count_positive(numbers):
    count = 0
    for value in numbers:
        if value >= 0:
            count += 1
    return count

На каких входах функция ошибётся? Выберите входы и проверьте.

06

Бинарный поиск

Функция возвращает индекс target в отсортированном списке или −1.

Код кандидата
def binary_search(numbers, target):
    left, right = 0, len(numbers) - 1
    while left < right:
        middle = (left + right) // 2
        if numbers[middle] == target:
            return middle
        if numbers[middle] < target:
            left = middle + 1
        else:
            right = middle - 1
    return -1

На каких входах функция ошибётся? Выберите входы и проверьте.

Сверка с медленным решением

Быстрое решение обычно устроено сложнее простого, и ошибку в нём труднее заметить при чтении. Если есть медленное решение, в правильности которого вы уверены, оба решения можно запустить на тысяче маленьких случайных входов и сравнить ответы. Первый вход, на котором ответы разошлись, становится коротким контрпримером. Такую проверку называют стресс-тестом.

Стресс-тест · медленная и быстрая версии
import random


def has_pair_slow(numbers, target):
    for i in range(len(numbers)):
        for j in range(i + 1, len(numbers)):
            if numbers[i] + numbers[j] == target:
                return True
    return False


def has_pair_fast(numbers, target):
    seen = set()
    for value in numbers:
        seen.add(value)
        if target - value in seen:
            return True
    return False


random.seed(1)
for attempt in range(1, 1001):
    numbers = [random.randint(-3, 3) for _ in range(random.randint(0, 4))]
    target = random.randint(-6, 6)
    slow = has_pair_slow(numbers, target)
    fast = has_pair_fast(numbers, target)
    if slow != fast:
        print(f"попытка {attempt}: numbers={numbers} target={target}")
        print(f"медленное: {slow}, быстрое: {fast}")
        break
else:
    print("расхождений нет")
Вывод в терминал$ python3 stress.py
попытка 28: numbers=[1, 0, 2, -2] target=-4
медленное: False, быстрое: True

random.seed(1) делает случайные входы одинаковыми при каждом запуске, поэтому расхождение воспроизводится. Входы маленькие: до четырёх чисел от −3 до 3. На таком входе каждый шаг можно проследить вручную.

Попробуйте самостоятельно

Почему быстрая версия ответила иначе

Пройдите has_pair_fast([1, 0, 2, -2], -4) по шагам. Какой элемент оказался в паре сам с собой?

Открыть разбор

Функция добавляет value в seen до проверки. Когда target - value == value, проверка находит в множестве только что добавленный элемент, и один индекс используется дважды. Если сначала проверить, а потом добавить, в seen будут только предыдущие элементы.

Исправление
def has_pair_fast(numbers, target):
    seen = set()
    for value in numbers:
        if target - value in seen:
            return True
        seen.add(value)
    return False
Вывод после исправления$ python3 stress.py
расхождений нет

Трудные моменты на собеседовании

В этих ситуациях код не нужен. Прочитайте описание, запишите, как бы вы поступили, и затем откройте разбор.

Ситуация 1

«Решите сами»

Вы спросили, что возвращать для пустого списка. Интервьюер отвечает: «Как считаете правильным».

Открыть разбор

Выберите вариант и назовите его: «Верну None и добавлю это в docstring». Коротко объясните выбор и скажите, что изменится при другом решении: «Если нужно исключение, поменяется одна строка и один тест». Так интервьюер видит, что вы заметили неполное условие и можете сами выбрать разумное поведение.

Ситуация 2

Тесты прошли, но ошибка есть

Все ваши примеры дали верные ответы. Интервьюер говорит: «В коде есть ошибка».

Открыть разбор

Перечитайте контракт и сравните с ним тесты: какие классы входов не проверены? Обычно это пустой вход, один элемент, отрицательные числа, дубликаты, ответ на первой или последней позиции. Возьмите самый короткий непроверенный вход и пройдите код вручную, записывая переменные.

Ситуация 3

Решение есть, но медленное

Вы написали перебор пар за O(n²). Интервьюер напоминает, что n до 10⁵.

Открыть разбор

Посчитайте: 10¹⁰ шагов не уложатся в лимит. Назовите, какая работа повторяется: для каждого элемента заново просматривается весь список. Повторный просмотр убирают с помощью set или сортировки. Перебор при этом пригодится как медленное решение для стресс-теста.

Ситуация 4

Контрпример от интервьюера

Интервьюер даёт вход, на котором ваш код ошибается, и молчит.

Открыть разбор

Прогоните код на этом входе по строкам, называя значения переменных. Найдите первое место, где состояние перестало совпадать с инвариантом. Исправьте причину и снова проверьте все прежние тесты: исправление одного случая может сломать другой.

Тест: выберите главное

Для каждой задачи дан длинный список вопросов. Часть из них меняет алгоритм или контракт, часть полезна для тестов, остальные на решение не влияют. Выберите пять самых важных и нажмите «Проверить».

Тест · выберите 5 главныхПара с суммой target

Интервьюер: «Найдите в списке два числа с суммой target». Выберите пять вопросов, от которых зависит решение.

Выбрано 0 из 5
Тест · выберите 5 главныхСжатие строки

Интервьюер: «Сожмите строку: aaabb → a3b2». Выберите пять вопросов, от которых зависит решение.

Выбрано 0 из 5
Тест · выберите 5 главныхСлияние отсортированных списков

Интервьюер: «Слейте два отсортированных списка в один отсортированный». Выберите пять вопросов, от которых зависит решение.

Выбрано 0 из 5

Карточка рассуждения

Карточку можно распечатать и держать перед собой, когда решаете задачи из банка кейсов. После нескольких задач эти вопросы запоминаются.

До кода
  • Что на входе и что на выходе?
  • Бывает ли вход пустым, из одного элемента, с дубликатами, с отрицательными?
  • Какой наибольший размер входа и значений?
  • Можно ли менять входные данные?
  • Что вернуть, если ответа нет?
Во время
  • Какая сложность получится при наибольшем n?
  • Что хранит каждая переменная после k шагов?
  • Откуда начинается и где кончается цикл?
  • Какой тип у промежуточных значений?
После
  • Какой класс входов проверяет каждый тест?
  • Какой вход короче всех может сломать код?
  • Совпал ли ручной прогон с запуском?
  • Не сломало ли исправление прежние тесты?