赞
踩
目录
大家好,我是爱好编程的斌斌.
在使用浏览器的时候,假设你连续访问了a-b-c三个网页,点击后退按钮就可以浏览之前的网页a、b。当你后退到a之后,点击前进按钮,又可以回到b、c页面。但当你在b页面访问了新页面d,就不能向前访问c了。
假设你是一名浏览器的开发者,你要如何实现这个功能呢?
这就与这篇文章所描述的内容息息相关,所以带着这个问题看下去吧,相信你会找到答案!
对于栈而言,生活中有一个非常形象的例子。想象你此刻正在洗碗,然后把碗一个一个地叠放到一起。把碗从下到上叠到一起的过程,可以理解为入栈,放在中间的碗不能直接被抽出来,只能一个一个从上往下取出,这就类似于出栈的过程。这种先进后出,后进先出的数据结构就是栈。
从功能的角度出发,栈是一种"受限"的数据结构,它只允许在一端插入和删除。
那么问题来了,既然栈没有数组和链表那么灵活,功能没有那么丰富,为什么栈还会存在,甚至有使用的场景呢?众所周知,特定的数据结构应用于特定的场景。那栈有哪些使用场景呢?有些时候功能多不一定好,就会更容易出错。
当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,我们就应该首选“栈”这种数据结构
首先,思考一下,我们需要为栈提供哪些接口(方法)?这就要回到栈的功能,从前面的分析可以看出,栈只涉及两种操作:入栈和出栈。翻译一下就是:往栈顶放入一个元素和从栈顶取出一个元素。既然已经知道了对元素的操作,那我们要用什么类型来存栈的元素呢?这里有两种解决方案,一种是数组,也就是顺序栈;另一种是链表,也就是链式栈。
废话少说,上代码,每个方法的实现都配有注解,相信你很聪明,能够理解:
- //顺序栈,基于数组实现
- public class ArrayStack {
-
- private String[] items;//存储元素的数组
- private int count;//栈中元素的个数
- private int capacity;//栈的大小
-
- /**
- * 初始化,根据大小创建顺序栈
- * @param capacity
- */
- public ArrayStack(int capacity) {
- items = new String[capacity];
- this.capacity = capacity;
- this.count = 0;
- }
-
- /**
- * 入栈,传递入栈的数据
- * @param item
- * @return
- */
- public boolean push(String item) {
- //栈满
- if (capacity == count) return false;
- //栈未满,往数组最后添加,发现capacity正好指向末尾
- items[capacity] = item;
- count++;
- return true;
- }
-
- /**
- * 出栈,返回出栈的元素
- * @return
- */
- public String pop() {
- //如果栈为空,直接返回null
- if (count == 0) return null;
- //如果栈不为空,返回数组最后一个元素
- String result = items[count - 1];
- //出去一个元素,长度相对减去1
- count--;
- return result;
- }
- }
- //链式栈,基于单链表的头插法实现
- public class LinkedStack2 {
- private int count;//记录栈的大小
- private Node head;//链表头结点
-
- private static class Node {
- Object object;
- Node next;
-
- public Node() {
- }
-
- public Node(Object object) {
- this.object = object;
- }
-
- public Node(Object object, Node next) {
- this.object = object;
- this.next = next;
- }
- }
-
- public LinkedStack2() {
- this.count = 0;
- head = new Node();
- }
-
- public void push(Object object) {
- if (count == 0) {
- head.object = object;
- return;
- }
- Node newNode = new Node(object);
- newNode.next = head;
- head = newNode;
- }
-
- public Object pop() {
- if (count == 0) return null;
- Node oldNode = head;
- head = head.next;
- return oldNode;
- }
-
- }
了解了栈定义和基本操作,那它的操作的时间、空间复杂度是多少呢?
不管是顺序栈还是链式栈,在入栈和出栈过程中,只需要一两个临时变量存储空间,所以空间复杂度是 O(1)。
这里存储数据需要一个大小为 n 的数组,并不是说空间复杂度就是 O(n)。因为我们所说的空间复杂度,是指除了原本的数据存储空间外,算法运行还需要的额外的存储空间。
空间复杂度分析是不是很简单?时间复杂度也不难。不管是顺序栈还是链式栈,入栈、出栈只涉及栈顶个别数据的操作,所以时间复杂度都是 O(1)。
不支持动态扩容的顺序栈,但数组中元素存满时,就不能往里面添加新的元素了。虽然说链式栈的元素个数不受限制,但它要存储next属性,需要消耗大量的内存空间。那如何实现支持动态扩容的顺序栈呢?只需要将数组变成动态数组即可。
当一个函数被调用之后,系统会为该函数申请一块存储空间,这个存储空间基于栈这种数据结构,用来存储函数中的临时变量。为了更好的理解这个过程,请看下面这段代码:
- int main() {
- int a = 1;
- int ret = 0;
- int res = 0;
- ret = add(3, 5);
- res = a + ret;
- printf("%d", res);
- reuturn 0;
- }
-
- int add(int x, int y) {
- int sum = 0;
- sum = x + y;
- return sum;
- }
从代码中我们可以看出,main() 函数调用了 add() 函数,获取计算结果,并且与临时变量 a 相加,最后打印 res 的值。为了让你清晰地看到这个过程对应的函数栈里出栈、入栈的操作,我画了一张图。图中显示的是,在执行到 add() 函数时,函数调用栈的情况。
举个例子,现在我们要求34+13*9+44-12/3,这个表达式的值。相信你很容易就可以口算出结果,那编译器是如何计算的呢?
实际上,编译器就是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。它会从左向右遍历表达式,当遇到数字,直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。
我将 3+5*8-6 这个表达式的计算过程画成了一张图,你可以结合图来理解我刚讲的计算过程。
假设我们现在要判断一串符号{[}()]是否合规,思考一下,怎么使用今天所学的知识来实现这个功能呢?
没错,这里又用到了栈。我们从头到尾遍历这串符号,当遇到左括号时,将其压入栈中;当是右括号时,取栈顶元素与其判断,如果与其不匹配(比如“(”跟“)”匹配,“[”跟“]”匹配,“{”跟“}”)或栈中没有元素,那么该串符号不合规。
到这里栈的内容就告一段落喽,既然已经掌握了栈的知识,不妨思考一下开篇的问题:浏览器的前进和后退功能是如何实现的呢?
它是基于两个栈实现的,就拿开头的例子,访问a-b-c网页来说。首先,浏览器会申请两个栈的X和Y,当你按顺序访问a-b-c时,将abc逐一压入栈X。当点击后退按钮访问页面b时,将栈X的栈顶元素c压入栈Y中;当你点击前进按钮回到页面c时,就将栈Y中的元素压回栈X。浏览器就是这样来实现的,是不是很奇妙?
栈只能在一端操作数据,是一种受限的数据结构,只有入栈和出栈两种操作。栈既可以用数组实现,也可以基于单链表实现,两种实现的入栈、出栈的时间、空间复杂度都是O(1).
数据结构与算法之美
为什么函数调用要用“栈”来保存临时变量呢?用其他数据结构不行吗?
其他数据结构也不一定不行,只不过函数调用正好符合栈后进先出的特点。函数调用喜欢嵌套,也就是函数a中调用函数b,函数b中调用函数c,嵌套的越深的函数越深执行,执行完毕出栈,并将结果返回给调用处。因此每次函数调用,只需要压栈,然后依次执行完毕出栈即可。
我们都知道,JVM 内存管理中有个“堆栈”的概念。栈内存用来存储局部变量和方法调用,堆内存用来存储 Java 中的对象。那 JVM 里面的“栈”跟我们这里说的“栈”是不是一回事呢?如果不是,那它为什么又叫作“栈”呢?
内存中的堆栈和数据结构堆栈不是一个概念,内存中的堆栈是真实存在的物理空间,而数据结构中的堆栈是抽象出来的数据存储结构。它为什么又叫作“栈”呢?这里可以用第一个问题的答案来回答,JVM中的栈也是在函数调用时使用,来存储方法的形参、局部变量、返回值。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。