本文是学习算法的笔记,《数据结构与算法之美》,极客时间的课程
我们浏览网页,经常会用到前进,后退的功能。比如依次浏览了a-b-c 三个页面,这时点后退,可以回到 b 页面,再次点击回到 a 页面。如果这时,又进入 新页面d ,那就无法通过前进,后退功能回到b、c页面了。
这样一个功能,要如何实现呢?
其实这样一个功能,可以借助栈来实现。
什么是栈?它是一个数据结构,数据先进后出,后进先出。打一个比方,放一叠盘子,放的时候,都放最上面,取的时候也从最上面取。典型的后进先出。
栈,可以用数组来实现,也可以用链表来实现。前者叫顺序栈,后者是链式栈。
为什么会有栈这种数据结构,直接用数组或是链表不就好了么?栈有严格的限制,只能从栈顶压入数据,从栈顶取数据,可以满足特定场景的需求,而链表或是数组暴露了太多的接口,容易出错。
如何实现一个栈
public class ArrayStack{
private String [] items; // 数组
private int count; // 栈中元素个数
private int n; //栈的大小
// 初始化数组,申请一个大小为 n 的数组空间
public ArrayStack(int n){
items = new String [n];
this.n = n;
count = 0;
}
// 入栈操作
public boolean push(String item){
if(count == n){
return false;
}
items[count-1] = item;
count++;
return true;
}
// 出栈操作
public String pop(){
if(count == 0){
return null;
}
String temp = items[count-1];
count--;
return temp;
}
}
栈的实际应用
栈作为一个基础的数据结构,经典的应用场景是函数调用。
我们知道,操作系统,为每个线程分配了独立的内存空间,这些内存空间组织成栈的结构,来存储函数调用产生的临时变量。每次进入一个函数,将临时变量作为栈桢入栈,当函数调用完成,有结果返回时,函数对应的栈桢出栈。便于理解,我们看一看下面的一段代码
int main() {
int a = 1;
int ret = 0;
int res = 0;
ret = add(3, 5);
res = a+ret;
printf("%d", res);
return 0;
}
int add(int x, int y){
int sum = 0;
sum = x + y;
return sum;
}
为了清晰地看到这个函数调用的过程中,出栈、入栈的操作,贴一张图
栈在表达式求值中的应用
再看一个常见的应用场景,编译器如何利用栈来实现表达式求值。
我们用一个简单的四则运算来说明这个问题,比如3+5*8-6,对于计算机来说,如何理解这个表达式,是个不太容易的事儿。
事实上,编译器是通过两个栈来实现的,其中一个保存数字,暂且叫A,一个保存运算符暂且叫B。从左到右遍历表达式,当遇到数字,直接压入A栈,当遇到运符时,就与B栈中栈顶元素进行比较。
如果比栈顶元素的优先级高,就将运算符压入栈,如果相同或者低,从B栈中取出栈顶元素,从A栈的栈顶取出两个数字,进行计算,再把计算结果压入A栈,继续比较。如下图
解答开篇的那个问题
我们使用两个栈,X和Y,首次浏览的页面,依次压入栈X。当点击回退时,依次从X栈中取出数据,并依次放入Y栈。点击前进时,从Y栈中取出数据,并依次放入X栈。当X栈中没有数据时,就说明不能再回退了;当Y中没有数据时,就说明不能再前进了。
比如你依次浏览了a b c 三个页面,我们依次把 a b c 压入栈,这时两个栈是这个样子
浏览器通过后退按钮,回退到页面 a 时,我们把 c b 依次压入Y栈,这时两个栈是这个样子
这时候,你又通过前进按钮,又回到 b 页面,这时两个栈的数据是这个样子
这时,你由 b 页面打开新的页面 d,那么你就不能再通过后退回到 c 页面,Y栈数据要清空。这时两个栈是这个样子