当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,我们就应该首选“栈”这种数据结构。
如何实现一个“栈”?
注意:从数组中删除的操作 ,其实并不需要真正的把某个值删除,只要把我们访问下标给减一就可以了。
class Stack {
public:
int n;
int count;
int *data;
Stack(int num) :n(num),count(0) {
data = new int[n];
}
~Stack(){
delete data;
}
bool push(int k) {
if (n == count) {
return false;
}
data[count++] = k;
return true;
}
int pop() {
if (count == 0) return 0;
int t = data[count-1];
count--;
return t;
}
};
int main() {
return 0;
}
支持动态扩容的顺序栈
当数组空间不够时,我们就重新申请一块更大的内存,将原来数组中数据统统拷贝过去。这样就实现了一个支持动态扩容的数组。如果要实现一个支持动态扩容的栈,我们只需要底层依赖一个支持动态扩容的数组就可以了。当栈满了之后,我们就申请一个更大的数组,将原来的数据搬移到新数组中。
支持动态扩容的栈的push、pop时间复杂度
根据均摊法,前k次入栈直接push,第k+1次入栈,需要把前面k个数据全部进行搬移,所以均摊下到前面k次,每次入栈实际上 是一次直接入栈和一次数据搬移,时间复杂度:O(1)。
疑问:数据搬移的时间复杂度是O(1)吗?
- 栈在函数调用中的应用
每调用一个函数,OS都会分配一块栈空间,用来存储函数调用时的临时变量。
疑问:纠结一个问题,当调用一个函数时候,在栈上开辟一块空间,如果再调用一个函数,那么再开辟一个栈空间,两个空间之间怎么区别开呢?并且在函数中,如果连续两个局部变量存储,它们之间的位置关系是连续的吗?看了下面博客懂了一些,不过还是买到这本书再把这点弄清楚吧。
https://www.jianshu.com/p/b666213cdd8a
- 栈在表达式求值中的应用
编译器就是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。
- 栈在括号匹配中的应用
如何实现浏览器的前进、后退功能?
我们使用两个栈,X 和 Y,我们把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。当我们点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。
比如你顺序查看了 a,b,c 三个页面,我们就依次把 a,b,c 压入栈,这个时候,两个栈的数据就是图(1);
当你通过浏览器的后退按钮,从页面 c 后退到页面 a 之后,我们就依次把 c 和 b 从栈 X 中弹出,并且依次放入到栈 Y。这个时候,两个栈的数据就是图(2);
这个时候,你通过页面 b 又跳转到新的页面 d 了,页面 c 就无法再通过前进、后退按钮重复查看了,所以需要清空栈 Y。此时两个栈的数据就是图(3)。
课后思考:
为什么函数调用要用“栈”来保存临时变量呢?
其实,我们不一定非要用栈来保存临时变量,只不过如果这个函数调用符合后进先出的特性,用栈这种数据结构来实现,是最顺理成章的选择。
从调用函数进入被调用函数,对于数据来说,变化的是什么呢?是作用域。所以根本上,只要能保证每进入一个新的函数,都是一个新的作用域就可以。而要实现这个,用栈就非常方便。在进入被调用函数的时候,分配一段栈空间给这个函数的变量,在函数结束的时候,将栈顶复位,正好回到调用函数的作用域内。