目录
1. 首先由于栈是受限的操作结构,因此对于不想暴露过多操作接口的需求就很适合应用了。
前言:
在对栈这种数据结构学习之前,记忆中对栈的理解就只是停留在FILO(First In Last Out 先进后出)的层面。
仔细想想,栈这种操作受限的数据结构在实际应用中有那些实际应用场景呢?
栈、队列与数组、链表这些数据结构有什么区别呢?
物理结构与逻辑结构
我们常说的数据结构分成物理结构与逻辑结构。
物理结构:指的数据实实在在可以看得见的存储结构,例如数组或链表这类结构。
逻辑结构:指的是一种抽象的概念,它依赖于物理结构而存在。例如我们通常说的栈、队列、树、图等。像栈可通过数组或链表来实现。
引用小灰的说法:如果把数据结构比作活生生的人,那么物理结构就是人的血肉和骨骼,看得见,摸得着,实实在在。而在物质之上,还存在在人的思想和精神,这些看不见,摸不着,依赖与血肉与骨骼而存在,这些就可以算是逻辑结构。
如下图所示:
一. 什么是栈
栈是一种逻辑结构,只支持FILO。其中最开始进入栈的元素称为栈底,最后进入栈的元素称为栈顶。其实现可以通过数组方式实现也可通过链表方式实现。
引用小灰的例子:假如有一根长长的圆筒,圆筒的一端是封闭的,另一端是开口。往圆筒里放入小球,先放入的球一定是在底部,后放入的靠近出口。当从圆筒中取出球时只有先取出靠近圆筒外面的球。
栈的基本操作:只有简单的入栈Push操作与出栈Pop操作。由于栈的入栈操作与出栈操作都是没有产生数据的移动,因此其时间复杂度都为O(1);
由上述,我们可知栈是一种操作受限的数据结构,那么为啥我们还要使用栈呢?下文,我们来看看栈的实际应用场景。
二. 栈的应用场景
1. 不想对外暴露过多操作能力的场景
首先由于栈是受限的操作结构,因此对于不想暴露过多操作接口的需求就很适合应用了。
2. 实现浏览器的前进与后退
思想:其实用两个栈就可以比较容易解决问题了。
主体思想是一个栈记录新访问的页面,另一个栈记录后退弹出的页面。
3. 栈在函数调用中的应用
操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构,用来存储函数调用时的临时变量。每进入一个函数,就会将其中的临时变量作为栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。
4. 栈在括号匹配中的应用
思想:用一个栈来存储括号,遇到左括号入栈,遇到右括号与栈顶元素对比。
假设表达式中只包含三种括号,圆括号 ()、方括号 [] 和花括号 {},并且它们可以任意嵌套。比如,{ [ {} ] }或[ { [] } { [] } ] 等都为合法格式,而 { [ } ( ) ]或 [ ({ ) ]为不合法的格式。那我现在给你一个包含三种括号的表达式字符串,如何检查它是否合法呢?
用栈来保存未匹配的左括号,从左到右依次扫描字符串。当扫描到左括号时,则将其压入栈中;
当扫描到右括号时,从栈顶取出一个左括号。如果能够匹配,比如“(”跟“)”匹配,“[”跟“]”匹配,“(”跟“)”匹配,则继续扫描剩下的字符串。
如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。
当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明有未匹配的左括号,为非法格式。
5. 栈在表达式求值中的应用
例子:3+5*8-6 如何用栈实现其运算的呢?
思想:利用两个栈,其中一个用来保存操作数,另一个用来保存运算符。
从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较,若比运算符栈顶元素优先级高,就将当前运算符压入栈,若比运算符栈顶元素的优先级低或者相同,从运算符栈中取出栈顶运算符,从操作数栈顶取出2个操作数,然后进行计算,把计算完的结果压入操作数栈,继续比较。
简单地来说在运算符栈中运算符优先级高的放到非栈顶位置时,那么就需要将操作数栈中的栈顶两个数取出来做运算再进行压栈处理了。
操作示意图如下所示:
三. 栈操作的实现代码
基于数组实现的顺序栈(同样可以通过ArrayList这类容器实现可动态扩容的栈)
public class ArrayStack {
private String[] items; // 数组
private int count; // 栈中元素个数
private int n; // 栈的大小
// 初始化数组,申请一个大小为 n 的数组空间
public ArrayStack(int n) {
this.items = new String[n];
this.n = n;
this.count = 0;
}
// 入栈操作
public boolean push(String item) {
// 数组空间不够了,直接返回 false,入栈失败。
if (count == n) return false;
// 将 item 放到下标为 count 的位置,并且 count 加一
items[count] = item;
++count;
return true;
}
// 出栈操作
public String pop() {
// 栈为空,则直接返回 null
if (count == 0) return null;
// 返回下标为 count-1 的数组元素,并且栈中元素个数 count 减一
String tmp = items[count-1];
--count;
return tmp;
}
}
基于链表实现的栈
import java.util.LinkedList;
public class Stack<T> {
private LinkedList<T> storage = new LinkedList<T>();
public void push(T v) {
storage.addFirst(v);
}
public T peek() {
return storage.getFirst();
}
public T pop() {
return storage.removeFirst();
}
public boolean empty() {
return storage.isEmpty();
}
public String toString() {
return storage.toString();
}
}
通过使用泛型,引入了栈在类定义中最简单的可行示例。push()接受T类型对象并压入栈中,而peek()和pop()将返回T类型的对象。区别是前者仅返回栈顶元素,后者返回并移除栈顶元素。
该系列博文为笔者学习《数据结构与算法之美》的个人学习笔记小结