数据结构—栈
文章目录
栈的特点
- 先进后出 first in last out (FILO) 结构
- 只能在栈定进行插入和删除的操作
栈的一些概念
- 栈顶与栈底:允许元素插入与删除的一端称为栈顶,另一端称为栈底
- 压栈:栈的插入操作,叫做进栈,也称压栈、入栈
- 弹栈:栈的删除操作,也叫做出栈
栈的操作
-
弹栈,通常命名为pop
-
压栈,通常命名为push
-
求栈的大小
-
判断栈是否为空
-
获取栈顶元素的值
c++ STL中定义的一些操作 #include<stack> s.empty(); //如果栈为空则返回true, 否则返回false; s.size(); //返回栈中元素的个数 s.top(); //返回栈顶元素, 但不删除该元素 s.pop(); //弹出栈顶元素, 但不返回其值 s.push(); //将元素压入栈顶
栈的分类
- 基于数组的栈:以数组为底层数据结构时,通常以数组头为栈底,数组头到数组尾为栈顶的生长方向;
-
支持动态扩容的顺序栈
刚才那个基于数组实现的栈,是一个固定大小的栈,也就是说,在初始化栈时需要事先指定栈的大小。当栈满之后,就无法再往栈里添加数据了。尽管链式栈的大小不受限,但要存储 next 指针,内存消耗相对较多。那我们如何基于数组实现一个可以支持动态扩容的栈呢?
如果要实现一个支持动态扩容的栈,我们只需要底层依赖一个支持动态扩容的数组就可以了。当栈满了之后,我们就申请一个更大的数组,将原来的数据搬移到新数组中。如:
-
基于单链表的栈:以链表为底层的数据结构时,以链表头为栈顶,便于节点的插入与删除,压栈产生的新节点将一直出现在链表的头部。
栈的实现
-
基于数组的栈
#include <stack> #include <iostream> using namespace std; int main() { stack<int> mystack; int sum = 0; for (int i = 0; i <= 10; i++){ mystack.push(i); } cout << "size is " << mystack.size() << endl; while (!mystack.empty()){ cout << " " << mystack.top(); mystack.pop(); } cout << endl; system("pause"); return 0; }
-
基于链表的栈
#include <iostream> using namespace std; template<class T>class Stack { private: struct Node { T data; Node *next; }; Node *head; Node *p; int length; public: Stack() { head = NULL; length = 0; } void push(T n)//入栈 { Node *q = new Node; q->data = n; if (head == NULL) { q->next = head; head = q; p = q; } else { q->next = p; p = q; } length++; } T pop()//出栈并且将出栈的元素返回 { if (length <= 0) { abort(); } Node *q; int data; q = p; data = p->data; p = p->next; delete(q); length--; return data; } int size()//返回元素个数 { return length; } T top()//返回栈顶元素 { return p->data; } bool isEmpty()//判断栈是不是空的 { if (length == 0) { return true; } else { return false; } } void clear()//清空栈中的所有元素 { while (length > 0) { pop(); } } }; int main() { Stack<char> s; s.push('a'); s.push('b'); s.push('c'); while (!s.isEmpty()) { cout << s.pop() << endl; } system("pause"); return 0; }
应用
-
函数调用栈
我们知道,操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。为了让你更好地理解,我们一块来看下这段代码的执行过程。
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 这个表达式的计算过程画成了一张图,你可以结合图来理解我刚讲的计算过程。
-
栈在括号匹配中的应用
我们假设表达式中只包含三种括号,圆括号 ()、方括号[]和花括号{},并且它们可以任意嵌套。比如,{[] ()[{}]}或[{()}([])]等都为合法格式,而{[}()]或[({)]为不合法的格式。那我现在给你一个包含三种括号的表达式字符串,如何检查它是否合法呢?
这里也可以用栈来解决。我们用栈来保存未匹配的左括号,从左到右依次扫描字符串。当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号。如果能够匹配,比如“(”跟“)”匹配,“[”跟“]”匹配,“{”跟“}”匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明有未匹配的左括号,为非法格式。
如何实现浏览器中的前进后退功能?
用两个栈即可解决此问题。
我们使用两个栈,X 和 Y,我们把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。
当我们点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。
当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。比如你顺序查看了 a,b,c 三个页面,我们就依次把 a,b,c 压入栈,这个时候,两个栈的数据就是这个样子:
当你通过浏览器的后退按钮,从页面 c 后退到页面 a 之后,我们就依次把 c 和 b 从栈 X 中弹出,并且依次放入到栈 Y。这个时候,两个栈的数据就是这个样子:
这个时候你又想看页面 b,于是你又点击前进按钮回到 b 页面,我们就把 b 再从栈 Y 中出栈,放入栈 X 中。此时两个栈的数据是这个样子:
这个时候,你通过页面 b 又跳转到新的页面 d 了,页面 c 就无法再通过前进、后退按钮重复查看了,所以需要清空栈 Y。此时两个栈的数据这个样子:
为什么函数调用需要用“栈”来存储临时变量?
其实,我们不一定非要用栈来保存临时变量,只不过如果这个函数调用符合后进先出的特性,用栈这种数据结构来实现,是最顺理成章的选择。
从调用函数进入被调用函数,对于数据来说,变化的是什么呢?是作用域。所以根本上,只要能保证每进入一个新的函数,都是一个新的作用域就可以。而要实现这个,用栈就非常方便。在进入被调用函数的时候,分配一段栈空间给这个函数的变量,在函数结束的时候,将栈顶复位,正好回到调用函数的作用域内。
JVM 里面的“栈”跟我们这里说的“栈”是不是一回事呢?
JVM里面的栈和我们这里说的是一回事,被称为方法栈。和前面函数调用的作用是一致的,用来存储方法中的局部变量。
https://blog.csdn.net/qq_32258777/article/details/81353638这里有更加详细的解释。