数据结构与算法 - 栈,队列

97 阅读3分钟

有关栈和队列的内容想必大家也接触过一些,它们的核心内容,都可以用几个字概括,比如栈,先进后出,队列,先进先出。那么下面的内容将以栈为主,讲讲什么是栈?什么是先进后出?为什么要先进后出?

什么是栈

栈是一个数据容器,且提供以下功能

  • 只能在容器的最后插入元素
  • 只能读取容器最后的元素
  • 只能从容器的最后依次移除元素

什么是先进后出

栈的功能中已经提到,只能在容器的最后插入元素,且只能从容器的最后依次移除,则代表,先被放入容器的数据,反而会在后面被取出或者被移除

为什么要先进后出

你可能会说,这不就是一个残次版的数组吗,即只有数组的pushpop方法,还不能通过下标来访问元素。
是的,但这正是栈要做的事情,因为栈只想让你关注两件事:

  • 你的数据是有顺序的,没有人可以通过任何方法修改已有的顺序
  • 操作的关键应该放在最后的数据上,因为它是你唯一可访问的数据

下面我们通过一个很经典的算法题来看:

题目

给一个字符串,如({(())[]}),问所出现的(), [], {}是否是匹配的,如([)]是不匹配的,()[]是匹配的([])是匹配的,{()[]}是匹配的

思路

以字符串({(())[]})为例,这个问题的思路应该是这样:

  • 每一个右扩都与一个左扩匹配,则整个字符串匹配
  • 如何确定每一个右扩与左扩匹配【设10个字符的下标分别为1-10】?
    • 任何一个右扩,其左侧的第一个字符一定要与其匹配【如5和4匹配】
    • 匹配成功后,去掉匹配成功的括号【移除5和4】
    • 重复上述动作,直到所有都匹配

上述流程的执行结果应该是,5和4匹配,6和3匹配,8和7匹配,9和2匹配,10和1匹配,最终所有括号都匹配,整个字符串是匹配的,以下是代码实现:

实现

function isMatch(source) {
    let stack = {
        data: [],
        length: 0,
        getTop() {
            let lastIndex = Math.max(this.length - 1, 0);
            return this.data[lastIndex];
        },
        push(val) {
            this.data.push(val);
            this.length++;
        },
        pop() {
            this.length = Math.max(this.length - 1, 0);
            return this.data.pop();
        },
        isEmpty() {
            return this.length <= 0;
        }
    }

    for (let i = 0; i < source.length; i++) {
        let c = source[i]
        if (c === '[' || c === '(' || c === '{') {
            // 左括号,入栈
            stack.push(c);
        } else {
            // 右括号,寻找左侧第一个字符是否是左括号
            if (c === ')') {
                 // 此处将getTop和pop合在一起使用了,因为pop的时候会返回栈顶部【最后放入】的元素
                 if (stack.pop() === '(') {
                     continue;
                 } else {
                     return false;
                 }
            } else if (c === ']') {
                 if (stack.pop() === '[') {
                     continue;
                 } else {
                     return false;
                 }
            } else {
                if (stack.pop() === '{') {
                     continue;
                 } else {
                     return false;
                 }
            }
        }
    }
    
    // 所有字符都匹配时,应该是一个空的栈
    return stack.isEmpty();
}

总结

可以发现,上面的应用,这其实就很契合我们对栈的理解: 我们维护了一份有顺序的数据,但我每次操作【关心】的只有最后一个数据。所以把栈当作一种数据结构的同时,我更倾向于把它当作一种思路,它让我在一份有顺序的数据中,只关心最后的数据。
由于栈本身就比较简单,这里就不做过多的说明,队列和栈的性质差不多,但同样,它有自己关注的点,希望你自己可以做一遍推导,这样可以同时加深对两者的理解。
ps:

  • 文章没有写出栈的标准写法,代码中使用了数组实现,但你可以用链表或者其它数据结构,实现其核心功能即可,不要拘泥于内部的形式
  • 如果有时间,你也可以试试其它题目,比如,计算一个字符串表达式【如计算(3 + 2 * 4 - (10 / 2)) + 2 * 3的值】,或者用队列写一个栈,用栈写一个队列

下一章

排序算法