LeetCode 224, Basic Calculator, просит вычислить строку, содержащую цифры, +, -, скобки и пробелы, без использования eval. Умножения и деления здесь нет, поэтому приоритет операторов учитывать не нужно. Единственная сложность — скобки, и стек сохранённых состояний элегантно решает эту задачу за один проход.
Задача
Дана строка s, представляющая выражение с неотрицательными целыми числами, +, -, (, ) и пробелами. Верните его значение. Знак минус также может быть унарным оператором, как в -(2 + 3).
Примеры:
| Вход | Выход |
|---|---|
| "1 + 1" | 2 |
| " 2-1 + 2 " | 3 |
| "(1+(4+5+2)-3)+(6+8)" | 23 |
| "-(3 + (4 + 5))" | -12 |
Ключевая идея
Идём слева направо, отслеживая три вещи: текущий результат, текущий знак и накапливаемое число. Когда встречаем (, начинается подвыражение. Сохраняем результат и знак перед скобкой в стек, затем начинаем заново с результатом 0 и знаком +1. Когда встречаем ), завершаем подвыражение, умножаем его на сохранённый знак и прибавляем к сохранённому результату.
Обрабатываем каждый символ:
| Символ | Действие |
|---|---|
| Цифра | num = num * 10 + digit |
| + или - | Прибавить sign * num к результату, сбросить num в 0, установить новый знак |
| ( | Положить результат и знак в стек, затем сбросить оба |
| ) | Прибавить sign * num к результату, затем умножить на сохранённый знак и прибавить сохранённый результат |
| Пробел | Игнорировать |
В конце прибавляем последнее sign * num.
def calculate(s):
result, sign, num = 0, 1, 0
stack = []
for ch in s:
if ch.isdigit():
num = num * 10 + int(ch)
elif ch in "+-":
result += sign * num
num = 0
sign = 1 if ch == "+" else -1
elif ch == "(":
stack.append(result)
stack.append(sign)
result, sign = 0, 1
elif ch == ")":
result += sign * num
num = 0
result *= stack.pop() # the sign before the parenthesis
result += stack.pop() # the result before the parenthesis
return result + sign * num
Порядок извлечения из стека важен: знак был положен последним, поэтому извлекается первым.
Трассировка
Выражение: 1-(2+3).
| Символ | result | sign | num | стек |
|---|---|---|---|---|
| 1 | 0 | 1 | 1 | [] |
| - | 1 | -1 | 0 | [] |
| ( | 0 | 1 | 0 | [1, -1] |
| 2 | 0 | 1 | 2 | [1, -1] |
| + | 2 | 1 | 0 | [1, -1] |
| 3 | 2 | 1 | 3 | [1, -1] |
| ) | 5, затем 5 x (-1) = -5, затем -5 + 1 = -4 | 1 | 0 | [] |
| конец | -4 |
1 - (2 + 3) = -4, что совпадает.
Унарный минус
Ведущий минус работает без специального кода. В начале num равен 0, поэтому первый - прибавляет sign * 0 к результату, а затем устанавливает знак -1. То же самое применяется после (: сохранённое состояние сбрасывает result в 0 и sign в +1, поэтому (-2) обрабатывается так же.
Сложность
- Время: O(n), один проход по строке.
- Память: O(n) в худшем случае при глубоко вложенных скобках, так как стек хранит по два значения на каждую открывающую скобку.
Я протестировал эту функцию на 4 000 случайных выражениях с вложенными скобками и унарными минусами, сравнивая с собственным вычислением Python, и все совпали.
Альтернатива: рекурсия
Можно также написать рекурсивную функцию, которая разбирает выражение и вызывает себя при встрече с (. Она читается естественно, но рискует упереться в лимит рекурсии на глубоко вложенном входе, поэтому версия со стеком безопаснее в Python.
Расширения, о которых спрашивают интервьюеры
| Вариация | Что меняется |
|---|---|
| Basic Calculator II (LeetCode 227) | Добавляются * и / с приоритетом и без скобок. Используйте стек слагаемых, применяя * и / сразу к вершине |
| Basic Calculator III (LeetCode 772) | Есть всё: скобки и * и /. Объедините обе идеи, часто через рекурсию |
| Переменные или функции | Нужен токенизатор и парсер |
| Большие числа | Используйте целые числа Python или обрабатывайте переполнение в Java и C++ |
Краевые случаи
| Вход | Результат |
|---|---|
| "0" | 0 |
| " 7 " | 7 |
| "-1" | -1 |
| "(1)" | 1 |
| "2-(5-6)" | 3 |
| Много вложенных скобок | Обрабатывается стеком |
| Многоразрядные числа вроде "123+4" | 127, потому что цифры накапливаются |
Частые ошибки
- Обработка каждой цифры как отдельного числа.
- Забыли прибавить последнее sign * num в конце.
- Извлечение из стека в неправильном порядке.
- Не сбросили num после оператора.
- Неправильная обработка ведущего минуса.
Как объяснить это на интервью
- Уточните допустимые символы и встречается ли унарный минус.
- Скажите, что проблемы приоритета нет, поэтому скобки — единственная сложность.
- Опишите три переменные и что сохраняет стек.
- Пройдитесь по 1-(2+3) на доске.
- Назовите сложность, затем упомяните расширение Calculator II.
Связанная практика
Смотрите нашу шпаргалку по Big O для терминов сложности, руководство по AI-тренажёру LeetCode и руководство по ассистенту для собеседований по программированию. Valid Parentheses и Decode String используют ту же идею со стеком.
Проговаривайте трассировку вслух во время практики. Транскрипт mock-интервью показывает, объяснили ли вы назначение стека. Соблюдайте правила работодателя об AI-инструментах на любом живом ассессменте.
Практикуйте собеседования по программированию с YesToTheOffer
Пройдите mock-собеседование по программированию, проговаривайте свою трассировку вслух и просмотрите расшифровку afterward. 30 бесплатных минут, без подписки.



