一、什么是栈
1.1 物理结构和逻辑结构
在说什么是栈之前, 先理解一个概念:物理结构和逻辑结构。
什么是数据存储的物理结构?
- 如果把数据结构比作活生生的人,那么物理结构就是人的血肉和骨骼,看得见,摸得着,实实在在。例如:数组、链表,都是在内存中实实在在的存储结构。
- 如果把物质层面的人体比作数据存储的物理结构,那么精神层面的人格则是数据存储中的逻辑结构。逻辑结构是抽象概念,它依赖于物理结构而存在。
1.2 栈的概念
举个例子,假如有一个又细又长的圆筒,圆筒一端封闭,另一端开口。往圆筒里放入乒乓球,先放入的靠近圆筒底部,后放入的靠近圆筒入口。
如果,想要取出这些乒乓球,只能按照和放入顺序相反的顺序来取,先取出后放入的,再取出先放入的,而不可能把最里面最先放入的乒乓球优先取出。
栈(stack)是一种线性数据结构,它就像是上图所示的放入乒乓球的圆筒容器,栈中的元素只能先入后出 (First In Last Out,简称FILO)。
最早进入的元素存放的位置叫做栈底(bottom) ,最后进入的元素存放的位置叫做栈顶(top)
特点:先入后出(FILO,First In Last Out)。
栈的数据结构,既可以用数组来实现,也可以用链表来实现。
栈的数组实现如下:
栈的链表实现如下:
1.3 简单了解下数组
数组可以说是最基本最常见的数据结构。数组一般用来存储相同类型的数据,可通过数组名和下标进行数据的访问和更新。数组中元素的存储是按照先后顺序进行的,同时在内存中也是按照这个顺序进行连续存放。数组相邻元素之间的内存地址的间隔一般就是数组数据类型的大小。
1.4 简单了解下链表
链表相较于数组,除了数据域,还增加了指针域用于构建链式的存储数据。链表中每一个节点都包含此节点的数据和指向下一节点地址的指针。由于是通过指针进行下一个数据元素的查找和访问,使得链表的自由度更高。
这表现在对节点进行增加和删除时,只需要对上一节点的指针地址进行修改,而无需变动其它的节点。不过事物皆有两极,指针带来高自由度的同时,自然会牺牲数据查找的效率和多余空间的使用。
一般常见的是有头有尾的单链表,对指针域进行反向链接,还可以形成双向链表或者循环链表。
二、栈的基本操作
2.1 入栈
入栈操作(push),就是把新元素放入栈中,只允许从栈顶一侧放入元素,新元素的位置将会成为新的栈顶。
以数组实现为例:
2.2 出栈
出栈操作(pop)就是把元素从栈中弹出,只有栈顶元素才允许出栈,出栈元素的前一个元素将会成为新的栈顶。
以数组实现为例:
基于数组实现的顺序栈:
// 基于数组实现的顺序栈
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;
}
}
2.3 复杂度
因为出栈和入栈都只会影响到最后一个元素,不涉及其他元素的整体移动,所以无论是以数组还是以链表实现的栈。其入栈和出栈的时间复杂度都是O(1)
而栈的空间复杂度是O(n)
三、栈的应用
3.1题目:20.有效的括号
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
示例 4:
输入:s = "([)]"
输出:false
示例 5:
输入:s = "{[]}"
输出:true
提示:
- 1 <= s.length <= 104
- s 仅由括号 '()[]{}' 组成
当开始接触题目时,我们会不禁想到如果计算出左括号的数量,和右括号的数量,如果每种括号左右数量相同,会不会就是有效的括号了呢?
事实上不是的,假如输入是[{]}
,每种括号的左右数量分别相等,但不是有效的括号。这是因为结果还与括号的位置有关。
仔细分析我们发现,对于有效的括号,它的部分子表达式仍然是有效的括号,比如 {()[()]}
是一个有效的括号,()[{}]
是有效的括号,[()]
也是有效的括号。并且当我们每次删除一个最小的括号对时,我们会逐渐将括号删除完。比如下面的例子。
这个思考的过程其实就是栈的实现过程。因此我们考虑使用栈,当遇到匹配的最小括号对时,我们将这对括号从栈中删除(即出栈),如果最后栈为空,那么它是有效的括号,反之不是。
栈的解法:
/**
* @param {string} s
* @return {boolean}
*/
var isValid = function(s) {
const n = s.length;
if(n%2 === 1){
return false;
}
const map1 = new Map([[')', '('],[']', '['],['}', '{']]);
const stack1 = [];
for(let ch of s){
if(map1.has(ch)){
if(!stack1.length || stack1[stack1.length-1]!== map1.get(ch)){
return false;
}
stack1.pop();
}else{
stack1.push(ch);
}
};
return !stack1.length;
};
- 时间复杂度:
O(N)
。遍历了一遍字符串。 - 空间复杂度:
O(N)
。最坏情况下,假如输入是(((((((
,栈的大小将是输入字符串的长度。
非栈的解法:
var isValid = function(s) {
let length;
do{
length = s.length;
s = s.replace("()","").replace("{}","").replace("[]","");
}while(length != s.length);
return s.length == 0;
}
时间复杂度不好判断,平均的情况下会达到O(n^2/2)
的复杂度。因为replace
本身的操作是 o(n)
的,所以最坏情况下会有产生n^2
的复杂度。
3.2 浏览器的前进和后退功能
浏览器的前进、后退功能,我们一定很熟悉。
- 当我们依次访问完一串页面
a-b-c
之后 - 点击浏览器的后退按钮,就可以查看之前浏览过的页面
b
和a
。 - 当你后退到页面
a
,点击前进按钮,就可以重新查看页面b
和c
。 - 如果你后退到页面
b
后,点击了新的页面d
,那就无法再通过前进、后退功能查看页面c
了。
浏览器中的前进后退表现出的特征:后进者先出,先进者后出,这就是典型的“栈”结构。
- 打开 a,b,c 三个页面,X 和 Y 的栈帧如下
|c| | |
|b| | |
|a| | |
--- ---
X Y
- 点击两次后退,从 c 退 b 再退到 a 页面
| | | |
| | |b|
|a| |c|
--- ---
X Y
- 点击前进,回到 b页面
| | | |
|b| | |
|a| |c|
--- ---
X Y
- 打开新页面 d,从逻辑上讲,d时最新页面,不存在更前面的页面了,需要清空 Y
|c| | |
|b| | |
|a| | |
--- ---
X Y
3.3 函数调用栈
在软件工程的实际应用中,栈作为一个比较基础的数据结构,应用场景还是很多,经典的一个应用场景就是函数调用栈。
操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。
为了让你更好地理解,我们一块来看下这段代码的执行过程。
function main() {
let a = 1;
let ret = 0;
let res = 0;
ret = add(3, 5);
res = a + ret;
console.log(res);
return 0;
}
function add(x, y) {
int sum = 0;
sum = x + y;
return sum;
}
从代码中可以看出,main()
函数调用了 add()
函数,获取计算结果,并且与临时变量 a
相加,最后打印 res
的值。上述代码对应的函数栈里出栈、入栈的操作,如下图所示。图中显示的是,在执行到 add()
函数时,函数调用栈的情况。
四、课后练习
- leetcode 739.每日温度 leetcode.cn/problems/da…