栈是什么?栈是一种线性数据结构,用于存储对象的集合。它基于后进先出(LIFO)。

image.png

Java 文档为例,它有如下方法:

  • empty() 测试是否为空
  • peek() 返回栈顶元素
  • pop() 把栈顶元素取出并返回
  • push(item) 把 item 加入到栈中
  • search(obj) 搜索元素

栈数据结构有两个最重要的操作,即压入和弹出。 push 操作将一个元素插入栈中,pop 操作从栈顶移除一个元素。

分别将 20、13、89、90、11、45、18 push 进栈。

image.png

之后再 pop 3 次

image.png

此时栈顶元素,即 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.  有效的括号 (这绝对是面试高频题了)

image.png

是有效的,就要左边对应右边,并且还得是相同类型的括号,如:

"([)]" -> 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
};

image.png

居然过了 😂,我们算法时间复杂度,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),舒服了 😀

image.png

参考:Java Stack