Gernar
Frontend DeveloperАлгоритмы и структуры данных

Интервью-вопрос

Что такое стек

Стек это структура данных с доступом через вершину по принципу LIFO. На практике вы встретите его в call stack, undo, проверке вложенности и задачах с глубокой рекурсией.

Добавлен
Редакция

Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.

🐰0
🥚0

Мини-квиз

Проверка перед разбором

Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.

Вопрос 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. 1Кладете открывающую скобку в стек
  2. 2Для закрывающей скобки сначала проверяете пустоту
  3. 3Сравниваете пару с верхним элементом
  4. 4В конце проверяете, что стек пуст
Опасная проверка
  1. 1Делаете pop без проверки пустоты
  2. 2Не проверяете тип закрывающей скобки
  3. 3Игнорируете остаток стека в конце
  4. 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. 1

    Называть стек обычным массивом

    Массив может быть основой реализации, но стек это контракт доступа. Если вы говорите, что можно брать любой индекс, вы ломаете идею LIFO. Лучше сказать: "В JavaScript я могу реализовать стек массивом, но использую только push, pop и чтение вершины".
  2. 2

    Не проверять пустой стек

    pop из пустого стека может вернуть undefined или привести к ошибке в вашей реализации. В алгоритмах вроде проверки скобок это дает ложный результат. Безопаснее явно проверять stack.length === 0 перед снятием элемента.
  3. 3

    Путать стек и очередь

    В очереди первым выходит самый старый элемент, а в стеке самый новый. Если перепутать эти структуры, можно выбрать неправильный алгоритм: например, DFS превратится в BFS или история undo начнет отменять не последнее действие. На интервью сразу проговорите LIFO и сравните с FIFO.
  4. 4

    Забывать про переполнение call stack

    Рекурсивный код выглядит компактно, но каждый вызов занимает место в call stack. На большом дереве данных это может закончиться ошибкой переполнения стека и падением сценария. Если глубина непредсказуема, лучше обсудить итеративный обход со своим стеком.
  5. 5

    Не очищать redo после нового действия

    В редакторе после undo пользователь может сделать новое действие. В этот момент стек redo нужно сбросить. Иначе интерфейс сможет вернуть действие из старой ветки истории, и состояние UI станет неожиданным для пользователя.
  6. 6

    Использовать shift и unshift без причины

    Для стека на массиве удобнее работать с концом через push и pop. shift и unshift работают с началом массива и часто требуют сдвига элементов. Это не всегда заметно на маленьких данных, но плохой выбор для горячего пути.

Follow-up

Что могут спросить дальше

Короткие ответы на вопросы, которыми проверяют понимание стека, его операций и практических ограничений.

Живые ответы

Видео с похожим вопросом

Если найдем публичные интервью с таким вопросом, добавим их сюда. Их удобно смотреть после теории, чтобы свериться с живыми ответами.

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

Содержание