【算法初探】前端学算法之有效的括号

208 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 5 天,点击查看活动详情

前因

前面的文章中,我们介绍了算法在面试及工作中的重要性,今天我们继续来学习一道算法题 - 有效的括号,原题的地址在这里,我们一起来看看关于这道题的描述:

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。

来源:力扣(LeetCode)

根据上面的题目描述,我们大概知道意思是:一个号字符串s中包含{}[]()这三种括号,需要判断字符串s是否是括号匹配的。例如:(a{b}c)匹配成功;(a{b}c{a[b]c)匹配失败。

既然我们已经知道最终要实现的效果了,那我们该从什么地方下手呢?可能有的童鞋会想:我是不是可以通过循环遍历的方式来对字符串进行匹配呢?这样其实是有问题的,如果是我们前面例子中的匹配成功的字符串,那么循环遍历自然是可以匹配成功的,但是如果是后面的两种方式呢?那么肯定就不能用循序来做了,这里我们需要用来完成这道题,那什么是呢?

 ,是一种先进后出的数据结构。在js\ts中,我们主要用数组来模拟,相关的API有:pushpoplength等。当然是先进后出的数据结构,也有先进先出的数据结构,后续我们遇到了会继续进行补充。下图即为栈的表现形似:

image.png

我们通过数组来模拟栈,那么数组的入栈可以用push来完成,数组的出栈可以用pop来完成,获取数组的长度则是length,代码如下:

// 模拟栈
const stack = [];
// 入栈
stack.push(1);
stack.push(2);
stack.push(3);
// 出栈
stack.pop();
//获取栈的长度
console.log(stack.length);

这里我们虽然通过数组来模拟栈,但是大家一定要搞清楚,数组其实是没有任何区别的,它们也不具备一定的可比性,因为js中本身是没有这个数据结构,所以我们才用数组来进行模拟的。而其实是一个理论模型,它不会受任何语言的限制,在任何语言中都可以通过对应的API来模拟它。

前置的知识已经准备完毕了,接下来我们要开始对这个题目进行解答了,那么我们该如何使用来完成括号的匹配呢?

思路

我们知道题目的核心就是要匹配字符串中的左右括号是否对等,那么我们在遇到左括号(({[)时就入栈(push),将左括号推入栈中;遇到右括号(]}))时就判断栈顶的括号与当前的括号是否匹配,如果匹配就出栈(pop);最后如果栈的长度(length)为0,则表示全部匹配完成。

上面我们将大致的思路描述了一下,接下来我们就一起看一下具体的代码该如何实现吧!

代码实现

下面我们一起用代码来实现一下这个题目,代码如下:

/*
 * 匹配左右括号
 * @param left string 左括号
 * @param right string 右括号
 * @return boolean
 */
function isMatch (left: string, right: string): boolean {
    if (left === '{' && right === '}') return true;
    if (left === '[' && right === ']') return true;
    if (left === '(' && right === ')') return true;
    return false;
}

/*
 * 判断括号是否匹配
 * @param str string
 * @return boolean
 */
function matchBracket (str: string): boolean {
    // 首先获取到字符串的长度
    const len = str.length;
    // 如果字符串的长度是0,则直接返回 true
    if (len === 0) return true;
    
    // 定义栈
    const stack = [];
    // 左括号
    const letfBracket = '{[(';
    // 右括号
    const rightBracket = ')]}';
    // 遍历循环字符串
    for (let i = 0; i < len; i++) {
        const cur = str[i];
        // 如果在左括号中匹配到,则需要入栈
        if (letfBracket.includes(cur)) {
            stack.push(cur);
        } else if (rightBracket.includes(cur)) {
            // 如果在右括号中匹配到,则判断栈顶是否需要出栈
            const stackTop = stack[stack.length - 1];
            // 判断左右括号是否匹配
            if (isMatch(stackTop, cur)) {
                stack.pop();
            } else {
                // 字符串中只要有一个括号不匹配,则直接返回false,例如:(a{b[c}d}e)
                return false;
            }
        }
    }
    
    // 当循环结束后,需要判断栈的长度是否为0,为0则表示完全匹配上,返回true;否则返回false
    return stack.length === 0;
}

上面我们完成了这个算法的基本实现,我们也可以在TypeScript官网的Playground中测试一下该方法是否能够正常运行,运行的结果如下图所示:

image.png

具体的执行效果可以狠戳这里

上面我们已经实现了这个算法,那么我们按照惯例还是需要对我们实现的算法来做一个性能的分析。

性能分析

在这个算法的实现中,时间的复杂度是O(n),空间复杂度也是O(n)。因为我们在这个算法中只做了一次for循环遍历,所以时间复杂度是O(n),虽然在循环中我们还用到了inclueds关键字,但是左括号和右括号的变量是一个常量,长度是固定的,因此它本身的时间复杂度就跟前面保持一致;空间复杂度为O(n)是因为我们定义了栈(stack)变量,所以空间复杂度就是O(n)

最后

我们总结一下这一节的大致内容。

首先我们的通过模拟来做数据的处理,要理解的本质是什么。其次要知道数组之间的关系是什么,这在实际开发或面试中对我们都会有很大的帮组。最后,如果对这个算法还不理解,可以自己动手敲一遍代码,基本就能够理解文中所说的出栈入栈,以及括号是如何匹配的了,只要理解了,那么下次遇到类似的题目基本就都能解答出来了。

如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,谢谢大家

往期回顾

【算法初探】前端学算法之旋转数组(1)

【算法初探】前端学算法之旋转数组(2)

参考文献

2周刷完100道前端优质面试真题 - 第二章2-7小结