Применение стека — описание структур данных в JavaScript

внешний интерфейс алгоритм JavaScript

Стек, также известный как стек, представляет собой линейный список с ограниченными операциями. Его пределОперации вставки и удаления разрешены только на одной стороне таблицы..这一端被称为栈顶,相对地,把另一端称为栈底。

1. Реализуйте стек класса Stack

Исходя из характеристик стека, массив можно использовать как линейную таблицу для хранения. инициализацияStackСтруктура класса следующая:

function Stack(){
    this.space = [];
}

Stack.prototype = {
    constructor: Stack,
    /* 接口code */
};

Далее на прототипе да入栈,出栈,清空栈,读取栈顶,读取整个栈数据реализации этих интерфейсов.StackПо умолчанию класс использует начало массива в качестве нижней части стека, а хвост — в качестве вершины стека.

1.1 Протолкнуть стекpush

Вставка в стек может использовать массив jspushметод для отправки данных в конец массива.

Stack.prototype = {
    push: function(value){
        return this.space.push(value);
    }
}

1.2 Вскрыть стекpop

Извлечение стека также использует массивы jspopметод для отправки данных в конец массива.

Stack.prototype = {
    pop: function(){
        return this.space.pop();
    }
}

1.3 Очистить стекclear

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

Stack.prototype = {
    clear: function(){
        this.space = [];
    }
}

1.4 Чтение вершины стекаreadTop

Прочитайте данные в верхней части стека и получите их, подписав массив. Одним из преимуществ является то, что когда нижний индекс превышает допустимый диапазон массива, возвращаемое значение равноundefined.

Stack.prototype = {
    readTop: function(){
        return this.space[this.space.length - 1];
    }
}

1.4 Чтение всего стекаread

Прочитайте все данные стека и верните текущий массив напрямую.

Stack.prototype = {
    read: function(){
        return this.space;
    }
}

1.5 Агрегация

Наконец, после агрегирования всех функций, как показано ниже, создается структура данных стека.

function Stack(){
    this.space = [];
}

Stack.prototype = {
    constructor: Stack,
    push: function(value){
        return this.space.push(value);
    },
    pop: function(){
        return this.space.pop();
    },
    clear: function(){
        this.space = [];
    },
    readTop: function(){
        return this.space[this.space.length - 1];
    },
    read: function(){
        return this.space;
    }
};

2. Настоящий бой

Цель изучения структур данных и алгоритмов состоит в том, чтобы лучше и эффективнее решать инженерные задачи. Здесь мы применяем то, что узнали, и приводим несколько реальных кейсов, чтобы испытать прелесть структур данных и алгоритмов :)

2.1 Массивыreverseреализация

В данном случае стек будет использоваться для реализации обратной функции массива.

function reverse(arr){
    var ArrStack = new Stack();

    for(var i = arr.length - 1; i >= 0; i--){
        ArrStack.push(arr[i]);
    }

    return ArrStack.read();
}

Как показано в коде, его можно разделить на следующие шаги:

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

Вроде бы все очень просто, не волнуйтесь, сложность будет позже :)

2.2 Преобразование десятичного числа в двоичное

Задача перевода чисел в основания — это небольшой тест для стеков.
Прежде чем объяснять метод преобразования, давайте рассмотрим небольшой пример:

Преобразование десятичного числа 13 в двоичное

    2 | 13      1
       ̄ ̄ ̄
    2 |  6      0
       ̄ ̄ ̄
    2 |  3      1
       ̄ ̄ ̄ ̄
         1      1

Как показано выше: двоичный код числа 131101.
Преобразование ручного преобразования в хранилище стека, просто преобразованиеостаток 2Результаты последовательно помещаются в стек для сохранения, и, наконец, вывод может быть реверсирован.

function binary(number){
    var tmp = number;
    var ArrStack = new Stack();

    if(number === 0){
        return 0;
    }

    while(tmp){
        ArrStack.push(tmp % 2);
        tmp = parseInt(tmp / 2, 10);
    }

    return reverse(ArrStack.read()).join('');
}

binary(14); // 输出=> "1110"
binary(1024); // 输出=> "10000000000"

2.3 Оценка выражения

Этот случай на самом деле можно понимать как упрощенную версиюevalметод.
Дело верно1+7*(4-2)оценка .

Перед входом в тему необходимо разобраться со следующими математическими теориями:

  1. инфиксная нотация(или инфиксная нотация) — это общий способ выражения арифметических или логических формул, в которых операторы находятся в середине своих операндов в инфиксной форме (например: 3 + 4).
  2. Обратная польская запись(обратная польская нотация, RPN или обратная польская нотация) — это метод математического выражения, введенный польским математиком Яном Вукасевичем в 1920 г. В обратной польской нотации все операторы помещаются в операнды позади, и поэтому также называетсяпостфиксная нотация. Обратная польская нотация не требует круглых скобок для определения приоритета оператора. «3 - 4 + 5» в обычной инфиксной записи записывается «3 4 - 5 +» в обратной польской записи.
  3. Алгоритм планирования поля(Алгоритм сортировочной станции) — это классический алгоритм преобразования инфиксных выражений в постфиксные выражения, представленный Эзге Дейкстрой и названный в честь его работы, аналогичной сортировочной станции поездов.

Чтобы было ясно заранее, это просто упрощенная реализация. Итак, есть два правила:

  1. Числа должны быть целыми
  2. Не допускайте лишних пробелов в выражениях

Код реализации выглядит следующим образом:

function calculate(exp){
    var valueStack = new Stack(); // 数值栈
    var operatorStack = new Stack(); // 操作符栈 
    var expArr = exp.split(''); // 切割字符串表达式
    var FIRST_OPERATOR = ['+', '-']; // 加减运算符
    var SECOND_OPERATOR = ['*', '/']; // 乘除运算符
    var SPECIAL_OPERATOR = ['(', ')']; // 括号
    var tmp; // 临时存储当前处理的字符
    var tmpOperator; // 临时存储当前的运算符

    // 遍历表达式
    for(var i = 0, len = expArr.length; i < len; i++){
        tmp = expArr[i];
        switch(tmp){
            case '(':
                operatorStack.push(tmp);
                break;
            case ')':
                // 遇到右括号,先出栈括号内数据
                while( (tmpOperator = operatorStack.pop()) !== '(' && 
                    typeof tmpOperator !== 'undefined' ){
                    valueStack.push(calculator(tmpOperator, valueStack.pop(), valueStack.pop()));
                }
                break;
            case '+':
            case '-':
                while( typeof operatorStack.readTop() !== 'undefined' && 
                    SPECIAL_OPERATOR.indexOf(operatorStack.readTop()) === -1 &&
                    (SECOND_OPERATOR.indexOf(operatorStack.readTop()) !== -1 || tmp != operatorStack.readTop()) ){
                    // 栈顶为乘除或相同优先级运算,先出栈
                    valueStack.push(calculator(operatorStack.pop(), valueStack.pop(), valueStack.pop()));
                }
                operatorStack.push(tmp);
                break;
            case '*':
            case '/':
                while( typeof operatorStack.readTop() != 'undefined' && 
                    FIRST_OPERATOR.indexOf(operatorStack.readTop()) === -1 && 
                    SPECIAL_OPERATOR.indexOf(operatorStack.readTop()) === -1 && 
                    tmp != operatorStack.readTop()){
                    // 栈顶为相同优先级运算,先出栈
                    valueStack.push(calculator(operatorStack.pop(), valueStack.pop(), valueStack.pop()));
                }
                operatorStack.push(tmp);
                break;
            default:
                valueStack.push(tmp);
        }
    }

    // 处理栈内数据
    while( typeof (tmpOperator = operatorStack.pop()) !== 'undefined' ){
        valueStack.push(calculator(tmpOperator, valueStack.pop(), valueStack.pop()));
    }

    return valueStack.pop(); // 将计算结果推出

    /*
        @param operator 操作符
        @param initiativeNum 主动值
        @param passivityNum 被动值
    */
    function calculator(operator, passivityNum, initiativeNum){
        var result = 0;

        initiativeNum = typeof initiativeNum === 'undefined' ? 0 : parseInt(initiativeNum, 10);
        passivityNum = typeof passivityNum === 'undefined' ? 0 : parseInt(passivityNum, 10);

        switch(operator){
            case '+':
                result = initiativeNum + passivityNum;
                console.log(`${initiativeNum} + ${passivityNum} = ${result}`);
                break;
            case '-':
                result = initiativeNum - passivityNum;
                console.log(`${initiativeNum} - ${passivityNum} = ${result}`);
                break;
            case '*':
                result = initiativeNum * passivityNum;
                console.log(`${initiativeNum} * ${passivityNum} = ${result}`);
                break;
            case '/':
                result = initiativeNum / passivityNum;
                console.log(`${initiativeNum} / ${passivityNum} = ${result}`);
                break;
            default:;
        }

        return result;
    }
}

Идеи реализации:

  1. использовать调度场算法, прочитать инфиксное выражение и выполнить разумную операцию над результатом.
  2. принятие критической точкиoperatorStack.readTop() !== 'undefined'вынести приговор. В некоторых книгах используется#Выполнение конечного знака немного громоздко лично.
  3. использовать строковые выраженияsplitСделайте разделение, затем выполните обходное чтение, поместите в стек. Если есть результат, который нужно вычислить заранее, выполняется соответствующая всплывающая обработка.
  4. Инкапсулировать метод вычисления частичных результатов в отдельный методcalculator. Так как числа до и после операторов умножения и деления различны в работе, их позиции не могут быть изменены по желанию.

2.4 Преобразование инфиксных выражений в постфиксные выражения (обратная польская нотация)

Обратная польская нотация, удобная для компьютера нотация, не требующая скобок.
Следующий случай является модификацией предыдущего случая и также использует调度场算法, который преобразует инфиксное выражение в постфиксное выражение.

function rpn(exp){
    var valueStack = new Stack(); // 数值栈
    var operatorStack = new Stack(); // 操作符栈 
    var expArr = exp.split('');
    var FIRST_OPERATOR = ['+', '-'];
    var SECOND_OPERATOR = ['*', '/'];
    var SPECIAL_OPERATOR = ['(', ')'];
    var tmp;
    var tmpOperator;

    for(var i = 0, len = expArr.length; i < len; i++){
        tmp = expArr[i];
        switch(tmp){
            case '(':
                operatorStack.push(tmp);
                break;
            case ')':
                // 遇到右括号,先出栈括号内数据
                while( (tmpOperator = operatorStack.pop()) !== '(' && 
                    typeof tmpOperator !== 'undefined' ){
                    valueStack.push(translate(tmpOperator, valueStack.pop(), valueStack.pop()));
                }
                break;
            case '+':
            case '-':
                while( typeof operatorStack.readTop() !== 'undefined' && 
                    SPECIAL_OPERATOR.indexOf(operatorStack.readTop()) === -1 &&
                    (SECOND_OPERATOR.indexOf(operatorStack.readTop()) !== -1 || tmp != operatorStack.readTop()) ){
                    // 栈顶为乘除或相同优先级运算,先出栈
                    valueStack.push(translate(operatorStack.pop(), valueStack.pop(), valueStack.pop()));
                }
                operatorStack.push(tmp);
                break;
            case '*':
            case '/':
                while( typeof operatorStack.readTop() != 'undefined' && 
                    FIRST_OPERATOR.indexOf(operatorStack.readTop()) === -1 && 
                    SPECIAL_OPERATOR.indexOf(operatorStack.readTop()) === -1 && 
                    tmp != operatorStack.readTop()){
                    // 栈顶为相同优先级运算,先出栈
                    valueStack.push(translate(operatorStack.pop(), valueStack.pop(), valueStack.pop()));
                }
                operatorStack.push(tmp);
                break;
            default:
                valueStack.push(tmp);
        }
    }

    while( typeof (tmpOperator = operatorStack.pop()) !== 'undefined' ){
        valueStack.push(translate(tmpOperator, valueStack.pop(), valueStack.pop()));
    }

    return valueStack.pop(); // 将计算结果推出

    /*
        @param operator 操作符
        @param initiativeNum 主动值
        @param passivityNum 被动值
    */
    function translate(operator, passivityNum, initiativeNum){
        var result = '';

        switch(operator){
            case '+':
                result = `${initiativeNum} ${passivityNum} +`;
                console.log(`${initiativeNum} + ${passivityNum} = ${result}`);
                break;
            case '-':
                result = `${initiativeNum} ${passivityNum} -`;
                console.log(`${initiativeNum} - ${passivityNum} = ${result}`);
                break;
            case '*':
                result = `${initiativeNum} ${passivityNum} *`;
                console.log(`${initiativeNum} * ${passivityNum} = ${result}`);
                break;
            case '/':
                result = `${initiativeNum} ${passivityNum} /`;
                console.log(`${initiativeNum} / ${passivityNum} = ${result}`);
                break;
            default:;
        }

        return result;
    }
}

rpn('1+7*(4-2)'); // 输出=> "1 7 4 2 - * +"

2.5 Ханойская башня

汉诺塔

Ханойская башня (Гонконг и Тайвань: Ханойская башня) — математическая задача, сформированная согласно легенде:
Есть три полюса А, В, С. На стержне А имеется N (N>1) перфорированных дисков, и размер дисков уменьшается снизу вверх. Все диски необходимо перемещать на С-образную планку по следующим правилам:

  1. Одновременно можно перемещать только один диск;
  2. Большие тарелки нельзя ставить поверх маленьких тарелок.

4个圆盘的汉诺塔的移动

Классический алгоритм применения стека, первая рекомендация汉诺塔.

Для понимания алгоритма обратите внимание на следующие моменты:

  1. Не вникайте в каждое движение, понимайте его абстрактно
  2. Шаг 1. Все диски, не соответствующие требованиям, равномерно перемещаются из кэш-памяти Tower A в Tower B.
  3. Шаг 2. Переместите соответствующий диск в Tower C
  4. Шаг 3. Переместите все кэшированные диски из башни B в башню C.

Вот реализация кода:

var ATower = new Stack(); // A塔
var BTower = new Stack(); // B塔
var CTower = new Stack(); // C塔 (目标塔)
var TIER = 4; // 层数

for(var i = TIER; i > 0; i--){
    ATower.push(i);
}

function Hanoi(n, from, to, buffer){
    if(n > 0){
        Hanoi(n - 1, from, buffer, to);  // 所有不符合要求的盘(n-1),从A塔统一移到B塔缓存
        to.push(from.pop()); // 将符合的盘(n)移动到C塔
        Hanoi(n - 1, buffer, to, from); // 把B塔缓存的盘全部移动到C塔
    }
}

Hanoi(ATower.read().length, ATower, CTower, BTower);

В центре внимания Ханойской башни является достижение этого путем рекурсии. Разбейте большую проблему на более мелкие с помощью рекурсии. Затем сосредоточьтесь на решении мелких проблем.

3. Резюме

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

Ссылаться на

[1] инфиксная нотация
[2] постфиксная нотация
[3] Алгоритм планирования поля
[4] Ханойская башня


Друзья, которым нравятся мои статьи, могут подписаться на меня следующими способами: