前端算法与数据结构之栈(一)

540 阅读5分钟

1. 定义

栈是一种遵从后进先出(LIFO)原则的有序集合。新添加或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。

下图模拟了一个栈的pushpop操作。

2. 模拟一个栈

JavaScript本身是没有栈的,所以我们尝试用JS来模拟一个栈的实现。

一个基本栈包含如下方法

push(element(s)):添加一个(或几个)新元素到栈顶。
pop():移除栈顶的元素,同时返回被移除的元素。
peek():返回栈顶的元素,不对栈做任何修改(不会移除栈顶的元素,仅仅返回它)。
isEmpty():如果栈里没有任何元素就返回 true,否则返回 false。
clear():移除栈里的所有元素。
size():返回栈里的元素个数。该方法和数组的 length 属性很类似。

2.1 数组实现

我们首先尝试用数组实现一个栈。我们可以通过数组的pushpop方法模拟栈的添加与删除操作。

class Stack { 
    constructor() { 
        this._items = [];
    }

    push(element) { 
        this._items.push(element); 
    }

    pop() { 
        return this._items.pop(); 
    }

    peek() { 
        return this._items[this._items.length - 1]; 
    }

    isEmpty() { 
        return this._items.length === 0; 
    }

    clear() { 
        this._items = []; 
    }

    size() { 
        return this._items.length; 
    }
}

2.2 对象实现

上面我们用数组实现了栈的模拟操作,但是数组有两个问题,首先数组大部分操作都是O(n)的,另外我们的栈是不固定大小的,使用数组会占用一段连续的内存空间,对内存是一种浪费。

基于数组的以上问题,我们考虑用对象来模拟实现。

class Stack { 
    constructor() { 
        this._count = 0; 
        this._items = {}; 
    } 

    push (element) { 
        this._items[this._count] = element; 
        this._count++; 
    }

    pop() { 
        if (this.isEmpty()) {
            return undefined; 
        } 
        this._count--;
        const result = this._items[this._count];
        delete this._items[this._count];
        return result;
    }

    peek() { 
        if (this.isEmpty()) { 
            return undefined; 
        } 
        return this._items[this._count - 1]; 
    }

    isEmpty() { 
        return this._count === 0; 
    }

    clear() { 
        this._items = {}; 
        this._count = 0; 
        // LIFO 原则
        // while (!this.isEmpty()) { 
            // this.pop(); 
        // }
    }

    size() { 
        return this._count; 
    }
}

2.3 用链表实现

JavaScript对象是基于散列表+数组实现的(字典有散列表部分有详细讲解),所以还是需要连续的内存空间,为了减少空间的占用,我们考虑使用链表实现(链表不需要连续的内存空间,链表部分详细讲解)。使用单链表,通过在链表顶端插人来实现 push,删除链表顶端元素实现 pop

interface INodeList {
    value: number
    next: INodeList | null
}

class MyStack {
    private head: INodeList | null = null
    private len: number = 0

    add(x: number) {
        const newNode: INodeList = {
            value: x,
            next: this.head,
        }

        this.head = newNode;

        // 记录长度
        this.len++
    }

    delete(): number | null {
        const headNode = this.head
        if (headNode == null) return null
        if (this.len <= 0) return null

        // 取值
        const value = headNode.value

        // 处理 head
        this.head = headNode.next

        // 记录长度
        this.len--

        return value
    }

    get length(): number {
        return this.len;
    }
}

由于栈是一个表,因此任何实现表的方法都能实现栈。

另外可以参考下Java栈的源码实现 JAVA stack源码

3. 常见的栈问题

3.1 从十进制到二进制

上图是一个十进制到二进制的计算,我们可以发现后出来的余数要排到前面。这符合栈后入先出的特点,把余数依次入栈,然后再出栈,就可以实现余数倒序输出。

代码如下:

function decimalToBinary(decNumber) {
    const remStack = [];
    let number = decNumber, rem, binaryString = '';
    while(number > 0) {
        rem = Math.floor(number % 2); // 取余
        remStack.push(rem);
        number = Math.floor(number / 2);
    }
    while (remStack.length) {
        binaryString += +remStack.pop(); 
    } 
    return binaryString;
}

console.log(decimalToBinary(233)); // 11101001 
console.log(decimalToBinary(10)); // 1010 
console.log(decimalToBinary(1000)); // 1111101000
console.log(decimalToBinary(1000.99)); // 1111101000

3.2 十进制转任意进制

解决了十进制转二进制,我们再升级一下刚才的问题,十进制转任意进制。很简单,我们只需要再设置一个进制的参数base替换2即可。

function baseConverter(decNumber, base) {
    const remStack = [];
    const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    let number = decNumber, rem, binaryString = '';
    if(!(base >= 2 && base <= 36)) { return '' };
    while(number > 0) {
        rem = Math.floor(number % base);
        remStack.push(rem);
        number = Math.floor(number / base);
    }
    while(remStack.length) {
        binaryString += digits[remStack.pop()]
    }
    return binaryString;
}

console.log(baseConverter(100345, 2)); // 11000011111111001 
console.log(baseConverter(100345, 8)); // 303771 
console.log(baseConverter(100345, 16)); // 187F9 
console.log(baseConverter(100345, 35)); // 2BW0

3.3 函数调用堆栈

最后调用的函数,最先执行完。
JS解释器使用栈来控制函数的调用顺序。

3.4 回文检查器

回文是正反都能读通的单词、词组、数或一系列字符的序列,例如 madamracecar

解题思路:如果字符倒序和正序相等,就是回文。考虑使用栈。遍历输入的字符,依次押入栈中,然后再依次出栈,即可获取倒序字符。如果与输入字符相等,即是回文。代码如下:

var isPalindrome = function(x) {
    let startString = x + '';
    if (startString == null || (startString != null && startString.length === 0)) {
        return false
    }

    const stack = [];
    let i = 0;
    let lowerString = '';
    while(i < startString.length) {
        stack.push(startString[i++]);
    }

    while(stack.length) {
        lowerString += stack.pop();
    }

    return lowerString === startString;
};

4. 单调栈问题

单调栈是指栈内元素保持递增或递减。一般用来处理next greater element(下一个更大的元素)问题。

例题:输入一个数组,返回一个等长数组,对应索引存储着元数组中下一个比它大的元素,如果没有,返回-1。如输入[2, 1, 2, 4, 3], 返回[4, 2, 4, -1, -1]

如果把数组里的数组比作人站队拍成一列,那么第一个能看到的人就是下一个最大元素,如下图所示: image.png

代码模版如下:

var nextGreaterElement = function (nums) {
    const len = nums.length;
    const result = new Array(len).fill(-1); // 存放答案的数组
    const stack = [];
    for (let i = len - 1; i >= 0; i--) { // 倒着往栈里放
        // 判定高矮
        while (stack.length && stack[stack.length - 1] <= nums[i]) {
            // 矮个起开,反正也被挡着了。。。
            stack.pop();
        }
        // nums[i] 身后的第一个高的
        result[i] = stack.length ? stack[stack.length - 1] : -1;
        // 进队,接受之后的⾝⾼判定吧
        stack.push(nums[i]);
    }
    return result;
}

for 循环之所以要从后往前扫描元素,是因为我们借助的是栈的结构,倒着⼊栈,其实是正着出栈。while 循环是把两个“⾼个”元素之间的元素排除,因为他们的存在没有意义,前⾯挡着个“更⾼”的元素,所以他们不可能被作为后续进来的元素的 Next Great Number 了。 这个算法的时间复杂度不是 O(n^2),而是 O(n)。因为从整体来看:总共有 n 个元素,每个元素都被 push ⼊栈了⼀次,⽽最多会被 pop ⼀次。所以总的计算规模是和元素规模 n 成正⽐的,也就是 O(n) 的复杂度

以上摘自labuladong的算法小抄

单调栈相关题目:
每日温度
下一个更大元素I
下一个更大元素II

5. leetcode相关题目

5.1 easy

1. 有效的括号

原题地址:有效的括号

难度:简单

题解:有效的括号(栈+字典优化)

2. 最小栈

原题地址:最小栈

难度:简单

题解:最小栈

3. 用栈实现队列

原题地址:用栈实现队列

难度:简单

题解:用栈实现队列

4. 用两个栈实现队列

原题地址:用两个栈实现队列

难度:简单

题解:用两个栈实现队列

5. 删除字符串中的所有相邻重复项

难度:简单

6. 下一个更大元素 I

难度:简单

题解:下一个更大元素I(单调栈)

5.2 medium

1. 下一个更大元素II

难度:中等

题解:下一个更大元素 II(单调栈)

2. 每日温度

原题地址:每日温度

难度:中等

题解:每日温度(单调栈)

3. 二叉树的前序遍历

原题地址:二叉树的前序遍历

难度:中等

题解:二叉树的前序遍历(递归、迭代解法)

  1. 逆波兰表达式求值

难度:中等

5.3 hard

1. 柱状图中最大的矩形

原题地址:柱状图中最大的矩形

难度:困难

题解:柱状图中最大的矩形(单调栈+哨兵技巧)

2. 接雨水

原题地址: 接雨水

难度:困难

题解:接雨水(单调栈解法)

以上题解源码请参考:leetcode刷题之路

持续更新ing...