【数据结构与算法基础】代码仓库:https://github.com/jinrunheng/datastructure-and-algorithm

一:栈与栈的应用

什么是栈?

栈(Stack)是一种运算受限的线性数据结构,所谓的运算受限指的是:栈这种数据结构仅允许在一端添加元素,删除元素,这一端被称作栈顶,而相对的另一端被称为栈底。

image.png

元素 A 最先进栈,最后出栈,元素 D 最后进栈,最先出栈。

所以,栈具有这种后进先出(LIFO-> Last In First Out)的特性。

无处不在的栈——栈的应用

在我们日常生活所能接触到的各种软件中,只要你留意,就会发现栈的身影。

1. Undo(撤销操作)

我们在文档编辑器中输入文字,当发现输入错误,想要撤销到前一步,这个操作就是Undo(撤销)。

撤销实际上就是通过栈这种数据结构来设计实现的。

2. 浏览器的前进与后退

浏览器页面的前进与后退到上一个页面的功能也是通过栈来实现的。

3. C语言的 printf() 函数

我们来看一个 C 程序:

  1. #include<stdio.h>
  2. int main(void){
  3. int i=1;
  4. printf("%d%d%d",i,i++,i++);
  5. return 0;
  6. }

你知道这个程序运行的结果是什么嘛?

如果你只是知道 i++ 与 ++i 的区别,而不了解 C 语言中 printf() 函数的底层实现原理,肯定是说不出正确答案的。

该程序的运行结果为:

  1. 321

因为 printf 函数的底层实现就是栈。

printf 函数会将传入参数以从右到左的顺序入栈,并将结果保存后出栈。

  • push(i++);保存结果为 1
  • push(i++);保存结果为 2
  • push(i);保存结果为 3

出栈顺序为:3,2,1

4. 程序调用系统栈

我们来看一个 Java 程序:

  1. package com.github.test;
  2. public class Test {
  3. public static void main(String[] args) {
  4. A();
  5. }
  6. public static void A() {
  7. B();
  8. }
  9. public static void B() {
  10. C();
  11. }
  12. public static void C() {
  13. System.out.println("In method C");
  14. }
  15. }

程序的 main 方法调用了 A 方法,A 方法中调用了 B 方法,B 方法中调用了 C 方法。

大家是否有想过,在这些方法的调用中,我们的编译器是如何记录程序运行到哪一步,并且应该带着返回值回到哪里的呢?

答案就是:大名鼎鼎的方法栈。

方法栈会以栈桢为单位进行压栈与出栈操作,每一个方法从调用开始到执行完成,都对应着一个栈桢在方法栈从入栈到出栈的过程。

栈桢存储了方法的局部变量表,操作数栈,方法返回地址等信息,因为我们的重点是数据结构,关于栈桢的一些详细的说明就不在这里展开了,我们来重点看一下方法栈发生了什么:

从 main 方法开始,方法逐个调用直至进入到 C 方法,方法栈会一直压栈,只有位于栈顶的栈桢才是有效的,称为当前栈桢,与这个栈桢相关联的方法称为当前方法。

image.png

现在,JVM 方法栈的当前栈桢为 C 方法的栈桢,在 C 方法执行完毕后,C 方法的栈桢出栈。

image.png

来到 B 方法的第 14 行,B 方法执行完毕后,代表 B 方法的栈桢出栈。

image.png

来到 A 方法的第 10 行,A 方法执行完毕后,代表 A 方法的栈桢出栈。

image.png

最后来到 main 方法的第 6 行,main 方法执行完毕后弹出,方法栈为空,代表所有方法已经执行完毕。

image.png

二:栈的基本实现

栈的设计上,只设计到如下几个方法:

  • void push(E)
  • E pop()
  • E peek()
  • int getSize()
  • boolean isEmpty()

在设计上,我们选用上一章完成的 动态数组 作为栈的底层实现。

代码地址

栈的复杂度分析:

push O(1)
pop O(1)
peek O(1)
getSize O(1)
isEmpty O(1)

三:括号匹配问题

我们来看一道 LeetCode 简单题目:有效的括号

该问题涉及到了栈这种数据结构在算法当中的具体应用。

给定一个只包括 '(',')','{','}','[',']'的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合。
  • 左括号必须以正确的顺序闭合。

示例1:

  1. 输入:s = "{[]}"
  2. 输出:true

示例2:

  1. 输入:s = "()[]{}"
  2. 输出:true

示例3:

  1. 输入:s = "([)]"
  2. 输出:false

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/valid-parentheses

本题的解题思路:

本题使用到了栈这种数据结构,我们的算法流程如下所示

  • 依次遍历字符串的每个字符
    • 如果当前的字符为左括号,就将这个字符压栈
    • 如果当前的字符为右括号:
      • 如果栈为空,则直接返回 false
      • 如果当前栈不为空,则出栈一个字符,将这个字符命名为 c_end:
        • 如果当前字符为 ')',c_end 应为 '(',不匹配则返回 false
        • 如果当前字符为 ']',c_end 应为 '[',不匹配则返回 false
        • 如果当前字符为 '}',c_end 应为 '{',不匹配则返回 false
  • 最后返回栈是否空

代码如下:

  1. class Solution {
  2. public boolean isValid(String s) {
  3. Stack<Character> stack = new Stack<>();
  4. char[] chars = s.toCharArray();
  5. for(char c : chars){
  6. if(c == '(' || c == '[' || c == '{'){
  7. stack.push(c);
  8. }else {
  9. if(!stack.isEmpty()){
  10. char c_end = stack.pop();
  11. if(c == ')' && c_end != '(') return false;
  12. if(c == ']' && c_end != '[') return false;
  13. if(c == '}' && c_end != '{') return false;
  14. }else {
  15. return false;
  16. }
  17. }
  18. }
  19. return stack.isEmpty();
  20. }
  21. }

在一些代码 IDE 中,有一类插件叫做:Rainbow Brackets(彩虹括号),用来检查括号是否闭合。它的实现原理和这道题差不多,就是应用了栈这种数据结构。

LeetCode 上有许多和栈这种数据结构相关的题目,请关注我的系列文章,在后续的【算法】章节中,我们会给出更多的示例。