有关栈和队列的内容想必大家也接触过一些,它们的核心内容,都可以用几个字概括,比如栈,先进后出,队列,先进先出。那么下面的内容将以栈为主,讲讲什么是栈?什么是先进后出?为什么要先进后出?
什么是栈
栈是一个数据容器,且提供以下功能
- 只能在容器的最后插入元素
- 只能读取容器最后的元素
- 只能从容器的最后依次移除元素
什么是先进后出
栈的功能中已经提到,只能在容器的最后插入元素,且只能从容器的最后依次移除,则代表,先被放入容器的数据,反而会在后面被取出或者被移除
为什么要先进后出
你可能会说,这不就是一个残次版的数组吗,即只有数组的push和pop方法,还不能通过下标来访问元素。
是的,但这正是栈要做的事情,因为栈只想让你关注两件事:
- 你的数据是有顺序的,没有人可以通过任何方法修改已有的顺序
- 操作的关键应该放在最后的数据上,因为它是你唯一可访问的数据
下面我们通过一个很经典的算法题来看:
题目
给一个字符串,如({(())[]}),问所出现的(), [], {}是否是匹配的,如([)]是不匹配的,()[]是匹配的([])是匹配的,{()[]}是匹配的
思路
以字符串({(())[]})为例,这个问题的思路应该是这样:
- 每一个右扩都与一个左扩匹配,则整个字符串匹配
- 如何确定每一个右扩与左扩匹配【设10个字符的下标分别为1-10】?
- 任何一个右扩,其左侧的第一个字符一定要与其匹配【如5和4匹配】
- 匹配成功后,去掉匹配成功的括号【移除5和4】
- 重复上述动作,直到所有都匹配
上述流程的执行结果应该是,5和4匹配,6和3匹配,8和7匹配,9和2匹配,10和1匹配,最终所有括号都匹配,整个字符串是匹配的,以下是代码实现:
实现
function isMatch(source) {
let stack = {
data: [],
length: 0,
getTop() {
let lastIndex = Math.max(this.length - 1, 0);
return this.data[lastIndex];
},
push(val) {
this.data.push(val);
this.length++;
},
pop() {
this.length = Math.max(this.length - 1, 0);
return this.data.pop();
},
isEmpty() {
return this.length <= 0;
}
}
for (let i = 0; i < source.length; i++) {
let c = source[i]
if (c === '[' || c === '(' || c === '{') {
// 左括号,入栈
stack.push(c);
} else {
// 右括号,寻找左侧第一个字符是否是左括号
if (c === ')') {
// 此处将getTop和pop合在一起使用了,因为pop的时候会返回栈顶部【最后放入】的元素
if (stack.pop() === '(') {
continue;
} else {
return false;
}
} else if (c === ']') {
if (stack.pop() === '[') {
continue;
} else {
return false;
}
} else {
if (stack.pop() === '{') {
continue;
} else {
return false;
}
}
}
}
// 所有字符都匹配时,应该是一个空的栈
return stack.isEmpty();
}
总结
可以发现,上面的应用,这其实就很契合我们对栈的理解: 我们维护了一份有顺序的数据,但我每次操作【关心】的只有最后一个数据。所以把栈当作一种数据结构的同时,我更倾向于把它当作一种思路,它让我在一份有顺序的数据中,只关心最后的数据。
由于栈本身就比较简单,这里就不做过多的说明,队列和栈的性质差不多,但同样,它有自己关注的点,希望你自己可以做一遍推导,这样可以同时加深对两者的理解。
ps:
- 文章没有写出栈的标准写法,代码中使用了数组实现,但你可以用链表或者其它数据结构,实现其核心功能即可,不要拘泥于内部的形式
- 如果有时间,你也可以试试其它题目,比如,计算一个字符串表达式【如计算
(3 + 2 * 4 - (10 / 2)) + 2 * 3的值】,或者用队列写一个栈,用栈写一个队列
下一章
排序算法