Интервью-вопрос
Что такое стек
Стек это структура данных с доступом через вершину по принципу LIFO. На практике вы встретите его в call stack, undo, проверке вложенности и задачах с глубокой рекурсией.
- Добавлен
- Редакция
Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.
Мини-квиз
Проверка перед разбором
Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.
Вопрос 1 из 50 правильно
Разбор
Разобраться, а не зазубрить
Дальше разбираем суть, типичные уточнения и места, где легко сказать лишнее или перепутать термины.
Базовая идея
Представьте стек как стопку карточек. Вы кладете новую карточку сверху и первой снимаете тоже верхнюю. Поэтому порядок называется LIFO, Last In, First Out.
На интервью не уходите в длинное определение. Назовите три вещи: порядок LIFO, операции push и pop, доступ только через вершину. Затем добавьте пример из фронтенда. Так ответ звучит не как заученный термин, а как рабочее понимание.
Короткая формулировка:
Стек это структура данных с принципом LIFO. Последний добавленный элемент извлекается первым. Обычно у него есть
push,pop,peekи проверка пустоты. Во фронтенде это видно в call stack, undo и алгоритмах проверки вложенности.
Операции и контракт
Главный смысл стека не в том, что данные лежат в массиве или списке. Смысл в контракте доступа. Вы работаете только с вершиной.
class Stack {
#items = [];
push(value) {
this.#items.push(value);
}
pop() {
if (this.#items.length === 0) {
return undefined;
}
return this.#items.pop();
}
peek() {
return this.#items[this.#items.length - 1];
}
isEmpty() {
return this.#items.length === 0;
}
}Такой пример хорошо подходит для интервью, потому что показывает границу API. Внутри используется массив, но внешний код не получает доступ к произвольному индексу. Так вы снижаете риск случайно нарушить LIFO и получить баг, который сложно отследить в алгоритме.
Где стек полезен во фронтенде
Первый пример это call stack JavaScript. Когда функция вызывает другую функцию, новый вызов попадает на вершину стека вызовов. Когда функция завершается, ее frame снимается, и выполнение возвращается к предыдущему вызову.
Второй пример это undo в редакторе. Последнее действие пользователя должно отменяться первым. Это ровно LIFO. Если вы добавляете redo, часто нужен второй стек для отмененных действий. После нового действия redo-стек очищают, чтобы UI не вернул устаревшую ветку истории.
Третий пример это проверка вложенности. Скобки, HTML-подобные теги, вложенные токены в парсере удобно проверять стеком: открывающий элемент кладете наверх, закрывающий должен совпасть с верхним элементом.
Пример с проверкой скобок
В этом примере стек помогает поймать неверный порядок вложенности. Обратите внимание на две проверки: нельзя делать pop из пустого стека, и в конце стек должен быть пустым.
function isBalanced(input) {
const stack = [];
const pairs = {
")": "(",
"]": "[",
"}": "{",
};
for (const char of input) {
if (char === "(" || char === "[" || char === "{") {
stack.push(char);
continue;
}
if (char === ")" || char === "]" || char === "}") {
if (stack.length === 0) {
return false;
}
const lastOpen = stack.pop();
if (lastOpen !== pairs[char]) {
return false;
}
}
}
return stack.length === 0;
}Плохой вариант: сразу вызвать stack.pop() и сравнивать результат без проверки пустоты. На строке ")(" такой код легко даст неверную логику. Без финальной проверки остатка стека строка "((" тоже может пройти ошибочно.
- 1Кладете открывающую скобку в стек
- 2Для закрывающей скобки сначала проверяете пустоту
- 3Сравниваете пару с верхним элементом
- 4В конце проверяете, что стек пуст
- 1Делаете pop без проверки пустоты
- 2Не проверяете тип закрывающей скобки
- 3Игнорируете остаток стека в конце
- 4Получаете true для неверно вложенной строки
Стек и рекурсия
Рекурсия связана со стеком через call stack. Каждый рекурсивный вызов хранит свой контекст: параметры, локальные переменные и точку возврата. Когда глубина небольшая, это удобно и читаемо.
Риск появляется на очень глубоких данных. Например, обход дерева комментариев, DOM-подобной структуры или вложенного меню может быть безопасным на тестовых данных и падать на реальном большом вводе. В такой ситуации вы можете заменить рекурсию на цикл и собственный стек.
function walkTree(root) {
const stack = [root];
while (stack.length > 0) {
const node = stack.pop();
handleNode(node);
const children = node.children ?? [];
for (let i = children.length - 1; i >= 0; i -= 1) {
stack.push(children[i]);
}
}
}Такой код не делает рекурсивные вызовы и лучше контролирует память. Проверка node.children ?? [] нужна, чтобы обход не падал на листовом узле и не ломал отрисовку дерева или меню. На интервью скажите, что это альтернатива для данных с непредсказуемой глубиной.
Практический вывод
Хороший ответ не заканчивается фразой "стек это LIFO". Добавьте, что стек выбирают, когда нужно всегда работать с последним добавленным элементом: последний вызов функции, последнее действие пользователя, последняя открытая скобка, последний узел для DFS.
Если задача требует обрабатывать элементы в порядке поступления, стек не подходит. Там чаще нужна очередь. Если нужно часто искать произвольный элемент, стек тоже не лучший интерфейс. Его сила именно в ограниченном доступе через вершину.
Частые ошибки
Где обычно ошибаются
Проверьте формулировки, которые звучат уверенно, но на интервью быстро выдают пробелы.
- 1
Называть стек обычным массивом
Массив может быть основой реализации, но стек это контракт доступа. Если вы говорите, что можно брать любой индекс, вы ломаете идею LIFO. Лучше сказать: "В JavaScript я могу реализовать стек массивом, но использую толькоpush,popи чтение вершины". - 2
Не проверять пустой стек
popиз пустого стека может вернутьundefinedили привести к ошибке в вашей реализации. В алгоритмах вроде проверки скобок это дает ложный результат. Безопаснее явно проверятьstack.length === 0перед снятием элемента. - 3
Путать стек и очередь
В очереди первым выходит самый старый элемент, а в стеке самый новый. Если перепутать эти структуры, можно выбрать неправильный алгоритм: например, DFS превратится в BFS или история undo начнет отменять не последнее действие. На интервью сразу проговорите LIFO и сравните с FIFO. - 4
Забывать про переполнение call stack
Рекурсивный код выглядит компактно, но каждый вызов занимает место в call stack. На большом дереве данных это может закончиться ошибкой переполнения стека и падением сценария. Если глубина непредсказуема, лучше обсудить итеративный обход со своим стеком. - 5
Не очищать redo после нового действия
В редакторе послеundoпользователь может сделать новое действие. В этот момент стекredoнужно сбросить. Иначе интерфейс сможет вернуть действие из старой ветки истории, и состояние UI станет неожиданным для пользователя. - 6
Использовать shift и unshift без причины
Для стека на массиве удобнее работать с концом черезpushиpop.shiftиunshiftработают с началом массива и часто требуют сдвига элементов. Это не всегда заметно на маленьких данных, но плохой выбор для горячего пути.
Follow-up
Что могут спросить дальше
Короткие ответы на вопросы, которыми проверяют понимание стека, его операций и практических ограничений.
Живые ответы
Видео с похожим вопросом
Если найдем публичные интервью с таким вопросом, добавим их сюда. Их удобно смотреть после теории, чтобы свериться с живыми ответами.
Пока видео нет. Когда появятся подходящие публичные интервью, добавим их в этот блок, чтобы можно было сравнить разбор с тем, как отвечают реальные кандидаты.
Что такое алгоритм 😎
Алгоритм это конечная последовательность шагов для решения задачи. Разбираем, как ответить на интервью, привести frontend-пример и не забыть про сложность, входные данные и ограничения.
Что такое очередь 😎
Очередь хранит элементы по принципу FIFO: первым добавили, первым обработали. Разбираем, как это объяснить на интервью, где очередь встречается во фронтенде и почему простая реализация через shift может быть опасной.