Алгоритмические задачи с собеседований аналитика
Алгоритмы·middle

Проверь валидность скобочной последовательности

Валидные скобки — задача-обманка, которую обожают на алгоритмических секциях. Она выглядит настолько простой, что первое решение пишется за минуту. Проблема в том, что минимум два «очевидных» подхода к ней неверны — и собеседующий ждёт, что ты сам найдёшь контрпримеры. Это жемчужина: она проверяет не умение писать код, а умение его ломать.

Задача

Строка состоит из круглых скобок ( и ). Последовательность валидна, если каждая открывающая скобка закрыта соответствующей закрывающей в правильном порядке.

Напиши функцию is_valid(s), возвращающую True/False. Ожидаемые ответы на наборе:

check_list = ['()', ')(', '(())', '()()', '()))']
answer_list = [True, False, True, True, False]

Решение

def is_valid(s):
    stack = []
    for c in s:
        if c == "(":
            stack.append(c)
        elif stack:
            stack.pop()
        else:
            return False
    return not stack


cases = ["()", ")(", "(())", "()()", "()))"]
for s in cases:
    print(f"{s!r} -> {is_valid(s)}")

Разбор

Интереснее всего здесь не правильный ответ, а два неправильных — потому что оба приходят в голову первыми.

Неверная попытка №1: считать скобки. Логика кажется железной: если открывающих столько же, сколько закрывающих, — значит, всё сбалансировано.

def check_bracket(s):
    open_cnt = sum(1 for c in s if c == "(")
    close_cnt = len(s) - open_cnt
    return open_cnt == close_cnt

Контрпример — ')('. Здесь одна открывающая и одна закрывающая, счётчики равны, функция радостно возвращает True. Но последовательность-то невалидна: закрывающая идёт раньше открывающей. Счётчик игнорирует порядок — а порядок и есть вся суть задачи. Именно на ')(' этот подход разваливается.

Неверная попытка №2: рекурсия с краёв. Ещё одна красивая идея: валидная строка начинается на ( и кончается на ), а между ними — снова валидная строка. Значит, отрежем края и вызовем себя рекурсивно:

def check_bracket(s):
    if not s:
        return True
    if s[0] != "(" or s[-1] != ")":
        return False
    return check_bracket(s[1:-1])

Контрпример — '()()'. Первый символ (, последний ) — проверка проходит. Отрезаем края, остаётся ')(' — а вот у него первый символ ), и функция возвращает False. Но '()()' абсолютно валидна! Подход ломается, потому что валидная строка не обязана быть «одной большой скобкой» — она может быть склейкой нескольких независимых пар.

Правильный ответ — стек. Единственная структура, которая учитывает порядок: идём по строке, каждую ( кладём на стек, на каждую ) снимаем со стека пару. Если снимать нечего (стек пуст, а пришла закрывающая) — сразу False: закрылось то, что не открывали. В конце стек должен быть пустым — иначе остались незакрытые скобки.

Пройдёмся по контрпримерам: на ')(' первая же ) встречает пустой стек → False. На '()()' стек аккуратно наполняется и опустошается дважды, в конце пуст → True. Стек работает именно потому, что моделирует вложенность: последняя открытая скобка закрывается первой — это LIFO в чистом виде.

Сложность — O(n) по времени и памяти. Мораль задачи шире скобок: интуитивно «очевидное» решение алгоритмической задачи стоит первым делом попробовать сломать контрпримером. Тот же навык «а что если данные придут не в том порядке» пригодится в задаче про скользящее среднее.

Ожидаемый результат

'()' -> True
')(' -> False
'(())' -> True
'()()' -> True
'()))' -> False

Хочешь так же уверенно решать на собеседовании?

Читать разбор полезно, а настоящая прокачка — самому решать такие задачи и получать обратную связь. Потренируйся в симуляторе аналитика, лёгкий режим — бесплатно.

Прорешать в бесплатной песочнице

Смежные задачи

Вопросы по теме