Как реализовать стек и очередь в JavaScript
Узнайте, как создать стек и очередь в JavaScript для алгоритма шунтирования. Полное руководство с примерами кода и оптимизациями производительности для быстрой работы.
Как реализовать стек и очередь в JavaScript? Мне нужны эти структуры данных для реализации алгоритма shunting-yard.
Стек и очередь в JavaScript можно реализовать, используя массивы с определёнными комбинациями методов: стек использует операции push() и pop(), а очередь — либо push() с shift(), либо unshift() с pop(). Для алгоритма Шунтинг‑Йарда понадобится стек‑операции (push/pop) для операторов и очередь‑операции (enqueue/dequeue) для вывода, с правильной обработкой приоритетов и скобок.
Содержание
- Основы реализации стека
- Основы реализации очереди
- Требования к алгоритму Шунтинг‑Йарда
- Полный пример реализации
- Проблемы производительности
- Расширенные возможности
Основы реализации стека
Стек — это структура данных LIFO (Last‑In‑First‑Out), которая следует принципу «последний пришёл, первый ушёл». В JavaScript массивы естественно предоставляют функциональность, необходимую для операций со стеком.
Базовый стек на массиве
class Stack {
constructor() {
this.items = [];
}
// Добавить элемент в вершину стека
push(element) {
this.items.push(element);
}
// Удалить и вернуть верхний элемент
pop() {
if (this.isEmpty()) {
return "Underflow";
}
return this.items.pop();
}
// Посмотреть верхний элемент без удаления
peek() {
return this.items[this.items.length - 1];
}
// Проверить, пуст ли стек
isEmpty() {
return this.items.length === 0;
}
// Получить размер стека
size() {
return this.items.length;
}
// Очистить стек
clear() {
this.items = [];
}
}
Альтернативная реализация стека
Согласно обсуждениям на Stack Overflow, стек можно реализовать и с помощью unshift() и pop(), хотя это реже используется из‑за проблем с производительностью unshift().
class StackAlternative {
constructor() {
this.items = [];
}
push(element) {
this.items.unshift(element);
}
pop() {
if (this.isEmpty()) {
return "Underflow";
}
return this.items.pop();
}
// ... остальные методы такие же, как выше
}
Основы реализации очереди
Очередь — это структура данных FIFO (First‑In‑First‑Out), где элементы добавляются в конец и удаляются с начала. В JavaScript массивы предоставляют несколько способов реализации очереди.
Базовая очередь на массиве
class Queue {
constructor() {
this.items = [];
}
// Добавить элемент в конец очереди
enqueue(element) {
this.items.push(element);
}
// Удалить и вернуть первый элемент
dequeue() {
if (this.isEmpty()) {
return "Underflow";
}
return this.items.shift();
}
// Посмотреть первый элемент без удаления
front() {
if (this.isEmpty()) {
return "No elements in queue";
}
return this.items[0];
}
// Проверить, пуста ли очередь
isEmpty() {
return this.items.length === 0;
}
// Получить размер очереди
size() {
return this.items.length;
}
// Очистить очередь
clear() {
this.items = [];
}
}
Альтернативная реализация очереди
Как упомянуто в обсуждениях на Stack Overflow, очередь можно реализовать с помощью unshift() и pop():
class QueueAlternative {
constructor() {
this.items = [];
}
enqueue(element) {
this.items.unshift(element);
}
dequeue() {
if (this.isEmpty()) {
return "Underflow";
}
return this.items.pop();
}
// ... остальные методы такие же, как выше
}
Требования к алгоритму Шунтинг‑Йарда
Алгоритм Шунтинг‑Йарда требует конкретных операций со стеком и очередью для преобразования инфиксных выражений в постфиксную нотацию. Согласно Wikipedia, алгоритм обрабатывает токены слева направо, используя стек для операторов и выводя результат в очередь.
Ключевые требования
- Стек для операторов: должен поддерживать операции push и pop
- Очередь для вывода: должна поддерживать операции enqueue
- Обработка приоритетов: необходимо сравнивать приоритеты операторов
- Управление скобками: необходимо обрабатывать открывающие и закрывающие скобки
Шаги алгоритма
Алгоритм работает следующим образом:
- Чтение токенов слева направо
- Если токен — число, помещаем его в очередь вывода
- Если токен — оператор:
- Пока на вершине стека находится оператор с более высоким приоритетом, извлекаем его в очередь вывода
- Пушим текущий оператор в стек
- Если токен — открывающая скобка, пушим её в стек
- Если токен — закрывающая скобка:
- Извлекаем операторы из стека в очередь вывода до тех пор, пока не найдём открывающую скобку
- Удаляем и игнорируем открывающую скобку
- После чтения всех токенов извлекаем оставшиеся операторы из стека в очередь вывода
Полный пример реализации
Создадим полную реализацию алгоритма Шунтинг‑Йарда, используя наши классы стека и очереди:
class Stack {
constructor() {
this.items = [];
}
push(element) {
this.items.push(element);
}
pop() {
if (this.isEmpty()) {
throw new Error("Stack underflow");
}
return this.items.pop();
}
peek() {
if (this.isEmpty()) {
throw new Error("Stack is empty");
}
return this.items[this.items.length - 1];
}
isEmpty() {
return this.items.length === 0;
}
size() {
return this.items.length;
}
}
class Queue {
constructor() {
this.items = [];
}
enqueue(element) {
this.items.push(element);
}
dequeue() {
if (this.isEmpty()) {
throw new Error("Queue underflow");
}
return this.items.shift();
}
isEmpty() {
return this.items.length === 0;
}
size() {
return this.items.length;
}
toArray() {
return [...this.items];
}
}
class ShuntingYard {
constructor() {
this.operatorPrecedence = {
'^': 4,
'*': 3,
'/': 3,
'+': 2,
'-': 2,
'(': 1
};
}
isOperator(token) {
return token in this.operatorPrecedence;
}
hasHigherPrecedence(op1, op2) {
return this.operatorPrecedence[op1] >= this.operatorPrecedence[op2];
}
tokenize(expression) {
// Простой токенизатор — обрабатывает числа, операторы и скобки
const tokens = [];
let currentToken = '';
for (let i = 0; i < expression.length; i++) {
const char = expression[i];
if (char === ' ') {
continue;
}
if (/\d/.test(char) || char === '.') {
currentToken += char;
} else {
if (currentToken) {
tokens.push(currentToken);
currentToken = '';
}
tokens.push(char);
}
}
if (currentToken) {
tokens.push(currentToken);
}
return tokens;
}
convertToPostfix(infixExpression) {
const tokens = this.tokenize(infixExpression);
const operatorStack = new Stack();
const outputQueue = new Queue();
for (const token of tokens) {
if (!this.isOperator(token) && token !== '(' && token !== ')') {
// Числа и идентификаторы сразу помещаем в вывод
outputQueue.enqueue(token);
} else if (token === '(') {
// Открывающая скобка помещаем в стек
operatorStack.push(token);
} else if (token === ')') {
// Закрывающая скобка — извлекаем операторы до открывающей скобки
while (!operatorStack.isEmpty() && operatorStack.peek() !== '(') {
outputQueue.enqueue(operatorStack.pop());
}
if (!operatorStack.isEmpty() && operatorStack.peek() === '(') {
operatorStack.pop(); // Удаляем открывающую скобку
} else {
throw new Error("Mismatched parentheses");
}
} else if (this.isOperator(token)) {
// Оператор — извлекаем операторы с более высоким или равным приоритетом
while (!operatorStack.isEmpty() &&
operatorStack.peek() !== '(' &&
this.hasHigherPrecedence(operatorStack.peek(), token)) {
outputQueue.enqueue(operatorStack.pop());
}
operatorStack.push(token);
}
}
// Извлекаем оставшиеся операторы из стека
while (!operatorStack.isEmpty()) {
const op = operatorStack.pop();
if (op === '(') {
throw new Error("Mismatched parentheses");
}
outputQueue.enqueue(op);
}
return outputQueue.toArray();
}
}
// Пример использования
const shuntingYard = new ShuntingYard();
const infix = "3 + 4 * 2 / ( 1 - 5 ) ^ 2 ^ 3";
const postfix = shuntingYard.convertToPostfix(infix);
console.log("Infix:", infix);
console.log("Postfix:", postfix.join(' '));
Обработка более сложных случаев
Для более сложных выражений с функциями и несколькими типами операторов можно расширить реализацию:
// Расширенный класс ShuntingYard с поддержкой функций
class ExtendedShuntingYard extends ShuntingYard {
constructor() {
super();
this.operatorPrecedence = {
'^': 4,
'*': 3,
'/': 3,
'+': 2,
'-': 2,
'sin': 5,
'cos': 5,
'tan': 5,
'(': 1
};
}
isFunction(token) {
return ['sin', 'cos', 'tan'].includes(token);
}
convertToPostfix(infixExpression) {
const tokens = this.tokenize(infixExpression);
const operatorStack = new Stack();
const outputQueue = new Queue();
for (const token of tokens) {
if (!this.isOperator(token) && !this.isFunction(token) &&
token !== '(' && token !== ')' && token !== ',') {
// Числа и идентификаторы сразу помещаем в вывод
outputQueue.enqueue(token);
} else if (this.isFunction(token)) {
// Функции помещаем в стек
operatorStack.push(token);
} else if (token === '(') {
// Открывающая скобка помещаем в стек
operatorStack.push(token);
} else if (token === ')') {
// Закрывающая скобка — извлекаем операторы до открывающей скобки
while (!operatorStack.isEmpty() && operatorStack.peek() !== '(') {
outputQueue.enqueue(operatorStack.pop());
}
if (!operatorStack.isEmpty() && operatorStack.peek() === '(') {
operatorStack.pop(); // Удаляем открывающую скобку
// Проверяем, является ли следующий элемент функцией
if (!operatorStack.isEmpty() && this.isFunction(operatorStack.peek())) {
outputQueue.enqueue(operatorStack.pop());
}
} else {
throw new Error("Mismatched parentheses");
}
} else if (token === ',') {
// Разделитель аргументов функции — извлекаем операторы до открывающей скобки
while (!operatorStack.isEmpty() && operatorStack.peek() !== '(') {
outputQueue.enqueue(operatorStack.pop());
}
} else if (this.isOperator(token)) {
// Оператор — извлекаем операторы с более высоким или равным приоритетом
while (!operatorStack.isEmpty() &&
operatorStack.peek() !== '(' &&
this.hasHigherPrecedence(operatorStack.peek(), token)) {
outputQueue.enqueue(operatorStack.pop());
}
operatorStack.push(token);
}
}
// Извлекаем оставшиеся операторы из стека
while (!operatorStack.isEmpty()) {
const op = operatorStack.pop();
if (op === '(') {
throw new Error("Mismatched parentheses");
}
outputQueue.enqueue(op);
}
return outputQueue.toArray();
}
}
Проблемы производительности
При реализации стеков и очередей для алгоритма Шунтинг‑Йарда стоит учитывать следующие аспекты производительности:
Производительность массивов
- Операции со стеком:
push()иpop()— O(1) в массивах JavaScript - Операции с очередью:
push()иshift()— O(n) из‑за переиндексации массива - Альтернативная очередь:
unshift()иpop()также O(n)
Техники оптимизации
Для более эффективной работы с большими выражениями:
// Оптимизированная очередь, использующая два стека
class OptimizedQueue {
constructor() {
this.stack1 = [];
this.stack2 = [];
}
enqueue(element) {
this.stack1.push(element);
}
dequeue() {
if (this.stack2.length === 0) {
while (this.stack1.length > 0) {
this.stack2.push(this.stack1.pop());
}
}
return this.stack2.pop();
}
isEmpty() {
return this.stack1.length === 0 && this.stack2.length === 0;
}
size() {
return this.stack1.length + this.stack2.length;
}
}
Память
- Память стека: обычно не растёт слишком сильно
- Память очереди: растёт линейно с размером входа
- Токенизация: предварительная токенизация выражений помогает избежать повторной обработки
Расширенные возможности
Обработка ошибок
class ShuntingYardWithValidation extends ShuntingYard {
validateExpression(expression) {
// Проверка сбалансированных скобок
let parenCount = 0;
for (const char of expression) {
if (char === '(') parenCount++;
if (char === ')') parenCount--;
if (parenCount < 0) return false;
}
if (parenCount !== 0) return false;
// Проверка допустимых операторов и операндов
const validTokens = /[\d\+\-\*\/\^\(\)\s]+/;
if (!validTokens.test(expression.replace(/sin|cos|tan/g, ''))) {
return false;
}
return true;
}
convertToPostfix(infixExpression) {
if (!this.validateExpression(infixExpression)) {
throw new Error("Invalid expression");
}
return super.convertToPostfix(infixExpression);
}
}
Поддержка многозначных чисел
class AdvancedShuntingYard extends ShuntingYard {
tokenize(expression) {
const tokens = [];
let currentToken = '';
for (let i = 0; i < expression.length; i++) {
const char = expression[i];
if (char === ' ') {
continue;
}
// Обработка многозначных чисел и десятичных дробей
if (/\d/.test(char) || char === '.') {
currentToken += char;
} else {
if (currentToken) {
tokens.push(currentToken);
currentToken = '';
}
tokens.push(char);
}
}
if (currentToken) {
tokens.push(currentToken);
}
return tokens;
}
}
Поддержка переменных
class VariableSupportShuntingYard extends ShuntingYard {
constructor() {
super();
this.variables = new Map();
}
setVariable(name, value) {
this.variables.set(name, value);
}
tokenize(expression) {
// Расширенный токенизатор, обрабатывающий переменные
const tokens = [];
let currentToken = '';
for (let i = 0; i < expression.length; i++) {
const char = expression[i];
if (char === ' ') {
continue;
}
// Переменные могут содержать буквы и цифры
if (/[a-zA-Z]/.test(char)) {
currentToken += char;
} else if (/\d/.test(char) || char === '.') {
currentToken += char;
} else {
if (currentToken) {
tokens.push(currentToken);
currentToken = '';
}
tokens.push(char);
}
}
if (currentToken) {
tokens.push(currentToken);
}
return tokens;
}
}
Заключение
Реализация стеков и очередей в JavaScript для алгоритма Шунтинг‑Йарда проста благодаря методам массивов. Ключевые выводы:
- Стек использует
push()иpop()для LIFO‑поведения - Очередь использует
push()иshift()для FIFO‑поведения - Алгоритм Шунтинг‑Йарда требует корректной обработки приоритетов операторов и скобок
- Проблемы производительности включают выбор подходящей реализации очереди
- Расширенные возможности: проверка ошибок, поддержка переменных, многозначных чисел и функций
Для продакшн‑использования рекомендуется реализовать надёжную обработку ошибок, полноценную токенизацию и валидацию входных выражений. Алгоритм Шунтинг‑Йарда работает за O(n) времени, что делает его эффективным для парсинга математических выражений.
Источники
- Implementing Stack and Queue in JavaScript - Stack Overflow
- Shunting Yard Algorithm - Wikipedia
- Shunting Yard Algorithm in JavaScript - InspirNathan
- Implementing Shunting Yard Algorithm in JavaScript - Stack Overflow
- Parsing/Shunting-yard algorithm - Rosetta Code
- Shunting Yard Algorithm - Brilliant Math & Science Wiki
- How can you implement a Stack and a Queue in JavaScript? - LambdaTest Community
- Implementing Stack and Queue in JavaScript for the Shunting-Yard Algorithm - LambdaTest Community