栈是什么?栈是一种线性数据结构,用于存储对象的集合。它基于后进先出(LIFO)。
以Java 文档为例,它有如下方法:
- empty() 测试是否为空
- peek() 返回栈顶元素
- pop() 把栈顶元素取出并返回
- push(item) 把 item 加入到栈中
- search(obj) 搜索元素
栈数据结构有两个最重要的操作,即压入和弹出。 push 操作将一个元素插入栈中,pop 操作从栈顶移除一个元素。
分别将 20、13、89、90、11、45、18 push 进栈。
之后再 pop 3 次
此时栈顶元素,即 peek() 就是 90
我们来实现一个栈(search 不常用,暂不实现)
class Stack<E> {
list: E[] = [];
push(item: E) {
this.list.push(item);
}
pop(): E | undefined {
return this.list.pop();
}
peek(): E {
return this.list[this.list.length - 1];
}
empty(): boolean {
return this.list.length === 0;
}
}
测试一下:
const s = new Stack<number>();
function log() {
console.log(s.list);
}
s.push(20);
s.push(13);
s.push(89);
log();
s.push(90);
s.push(11);
s.push(45);
s.push(18);
log();
s.pop();
log();
s.pop();
s.pop();
log();
console.log(s.peek());
输出
[ 20, 13, 89 ]
[ 20, 13, 89, 90, 11, 45, 18 ]
[ 20, 13, 89, 90, 11, 45 ]
[ 20, 13, 89, 90 ]
90
功能正常,那么下面实战一下题目吧: 20. 有效的括号 (这绝对是面试高频题了)
是有效的,就要左边对应右边,并且还得是相同类型的括号,如:
"([)]" -> false
"()[(){}]" -> true
注意哈,([)]
的答案是 false,虽然他们看着好像能都抵消,但是这不符合题目的条件「左括号必须以正确的顺序闭合」,你可以理解这是个消消乐,只有同类型的左边和右边并且相邻才能抵消,如果都抵消没了,那么返回 true,否则返回 false。 ([)]
里面的 [)
是不能抵消的,所以是 false。
你有想法吗?我们先以解决问题为主,不考虑时间复杂度。
既然想到了消消乐,那我们就用消消乐的思想呗:先用 i 遍历字符串,再找 i+1 个字符,看下他们两个是否能抵消,如果能抵消就删除这两个字符,之后继续,你会发现一次 n 的遍历不能全部抵消,所以外面还要套个 while 循环,得一直抵消,知道抵消不掉了,那么这个 while 循环就结束,代码如下:
function isValid(s: string): boolean {
const obj={
'(':')',
'{':'}',
'[':']',
}
let prevLen = s.length
const sList:string[]=[...s]
while(true){
const removeList:number[]=[]
// 此循环用于记录应该消掉的元素
for(let i=0;i<sList.length-1;i++){
if(obj[sList[i]]===sList[i+1]){
// unshift的原因是要倒着删除
// 如果正着删除的话,会导致后面的 index 对应不上,比较麻烦
removeList.unshift(i)
}
}
// 这里用来消除元素
removeList.forEach(item=>{
sList.splice(item,2)
})
// 如果长度和上次相比不变,那么就代表没有抵消掉
// 所以就应该退出 while 循环了,否则就会无限循环
if(prevLen===sList.length){
break
}
prevLen=sList.length
}
return !sList.length
};
居然过了 😂,我们算法时间复杂度,while 最多 n/2 次,for 最多 n 次,forEach 里的 splice 执行一次就要 O(n),但应该不会执行太多次,可以简单算作 O(n),所以总共时间复杂度是 O(n^2)。这并不是一个理想的复杂度(如果 LeetCode 的测试用例再严格些,应该就会超时),所以需要优化。
怎么优化?就得用到栈了,你可以把左括号当成入栈的操作,右括号当成出栈的操作(只有当栈顶元素和右括号是相同类型的括号时,才出栈),如果最后栈为空,那么就返回 true
class Stack<E> {
list: E[] = [];
push(item: E) {
this.list.push(item);
}
pop(): E | undefined {
return this.list.pop();
}
peek(): E {
return this.list[this.list.length - 1];
}
empty(): boolean {
return this.list.length === 0;
}
}
function isValid(s: string): boolean {
const obj={
'(':')',
'{':'}',
'[':']',
}
const stack=new Stack<string>()
for(let i=0;i<s.length;i++){
if(obj[s[i]]){
stack.push(s[i])
}else{
if(obj[stack.peek()]===s[i]){
stack.pop()
} else {
// 注意这里:如果不相等代表匹配不上,这时右括号多了,所以一定是false
return false
}
}
}
return stack.empty()
};
这里使用了栈操作,时间复杂度是 O(n),舒服了 😀
参考:Java Stack