开开森森学前端之括号匹配问题

753 阅读3分钟

前言

大家好,本人在掘金待了有3个多月了,觉得前端大佬们的步伐很整齐,故决定记录自己的成长轨迹。

我们今天要看的是一个简单的数据结构应用问题

题目要求

给定一个只包括 ( ),{ },[ ] 的字符串,判断括号匹配(闭合)是否合法。

有效括号需满足 左括号必须用相同类型的右括号闭合 左括号必须以正确的顺序闭合

示例:

输入: "()[]{}" 输出true

输入:"([)]" 输出false

输入:")(()))" 输出false

输入:"()" 输出true

输入:"((([])))" 输出true

输入:"]][[" 输出false

输入:([)] 输出false

这里之所以给大家这么多示例,是为了让大家刚开始先理解这道题的用意!

好了,我相信以咱们前端开发工程师的脑细胞,理解这道题还是绰绰有余的。

大家想一想,我们平时用的编辑器肯定也是做了括号匹配操作了,咱们要是写出这个小功能是不是也很厉害了呢!理解这道题的时候大家多想想我们平时用的编辑器的括号匹配规则就很容易想明白了!

解法分析

咱们可以把上面的例子拿过来分析,假设咱们拿第一个来看

输入: "()[]{}" 输出true

咱们可以清楚的看到上面这种情况:

第一个小括号是左括号,第二个小括号是右括号,所以他们已经匹配成功(合法),那么再往后看,第三个中括号是左括号,第四个小括号是右括号,所以他们也匹配成功,再往后看,第五个花括号是左括号,第六个花括号是右括号,所以也匹配成功,最后他们都匹配成功所以返回true.

这次咱们来找个反例分析一下

输入:")(()))" 输出false

输入:"]][[" 输出false

咱们再来看上面这种反例情况

括号一开头就是右括号,那么这种情况如果一出现,咱们就可以直接判定这个就是非法的,因为他的后面不可能再有能和他匹配的括号了。

其实上面这种情况就是这道题的边界情况

那么上面我们分析了这么多,直接上代码!

计数器解法

function valid(str) {
  let small = 0, big = 0, brace = 0; //small小括号 big大括号 brace花括号
  for (let i = 0; i < str.length; i++) {
    if (str[i] === '(') small++;
    if (str[i] === ')') small--;
    if (str[i] === '[') big++;
    if (str[i] === ']') big--;
    if (str[i] === '{') brace++;
    if (str[i] === '}') brace--;
    if (small < 0 || big < 0 || brace < 0) return false;
  }
  return small === 0 && big === 0 && brace === 0;
}
let isValid = valid("(())")//true
let isValid = valid("()[]{}")//true
let isValid = valid("]][[")//false
let isValid = valid("([)]")//true

咱们来看一下这个代码是用了什么思路解决的呢,我们应该可以快速看出来他是用了计数法,给每种括号分别给定一个计数器,当遇到左括号就加1,遇到右括号减1.如果最后他们的计数器都为0说明都统统抵消掉,那就是合法的。刚开始是右括号的 情况也考虑到了,如果每次循环完一旦有一个是负数,那就说明遇到了刚开始就是右括号的情况,直接返回false(括号不匹配)

但是仔细的读者已经发现了一些端倪,在文章开头的例子中 输入:([)] 输出false,但是使用了上述代码后返回结果为true

大家仔细想一想,其实这里括号的相对位置很重要。
如果我们只是在这里维持计数器,只要我们遇到闭合小括号,我们就会知道此处有一个可用的未配对的开口方括号。但是,最近的未配对的开括号是一个方括号,而不是一个闭口小括号,因此计数方法在这里被打破了。

再者,我们先不看结果如何我们来分析下他的算法复杂度

我们看到

for (let i = 0; i < str.length; i++) {
    if (str[i] === '(') small++;
    if (str[i] === ')') small--;
    if (str[i] === '[') big++;
    if (str[i] === ']') big--;
    if (str[i] === '{') brace++;
    if (str[i] === '}') brace--;
}

传进来了一个字符串,字符串长度是可变化的

我们使用大O表示法:如果一个问题的规模是n,解这一问题的某一算法所需要的时间为T(n),T(n)称为这一算法的“时间复杂度”。

那么我们可以看出这段代码中有一个for循环,且str.length是未知的看作n,因为循环体中的代码须要执行n(str.length)次, 所以我们把这种时间复杂度称之为线性阶O(n),。

注意:时间复杂度不是用来计算程序具体耗时的

那么他的空间复杂度呢?

既然时间复杂度不是用来计算程序具体耗时的,那么我们应该明白,空间复杂度也不是用来计算程序实际占用的空间的。空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) 来定义

我们可以看到这三个变量不会随着n(str.length的增长而再创建其他变量,所以这里的临时空间不会随着n的增长而增长,即 S(n) = O(1)

正则解法

function valid(str) {
  let length;
  do{
    length = str.length;
    str = str.replace("()","").replace("{}","").replace("[]","");
  }while(length != str.length);
  return str.length == 0
}
let isValid = valid("(())")//true
let isValid = valid("()[]{}")//true
let isValid = valid("]][[")//false
let isValid = valid("([)]")//false

可以看到这次输出结果都是我们想要的, 那这个解法是什么思路呢,其实很简单,最关键的逻辑就是replace那一整句,如果他有左右匹配的括号的话,将他替换为空的,然后再重复这个操作,直到最后他是空的,就说明他是匹配的。 这里其实他的时间复杂度比较难判断,他的时间复杂度在平均情况下,会达到2分之n平方的这样的时间复杂度,那么他的空间复杂度同样是O(1)

栈(数据结构)解法

大家应该都知道在js中我们可以通过数组去模拟栈数据结构。

这里先提供思路

左括号肯定是合法的,只是我们要看他后续有没有右括号和他匹配。所以对于左括号我们暂时没法判断,所以我们把它存起来,也就是压入栈(push)

如果是右括号,右括号是不能独立存在的,右括号是必须用来和左括号匹配的,所以我们要去查看(peek)栈顶是否有和它相匹配的括号,如果是右小括号,就找左小括号...其他以此类推,如果匹配到了,那就把栈顶元素给推出去(pop),如果不匹配说明后面已经没有必要再继续了,所以直接返回false(不匹配)

如果整个流程都可以走完,都是一一匹配的话,这个栈本身应该是空的。

我们拿来一个示例做图示讲解 (){} 刚开始我们的栈是这样的

刚进来,我们把左小括号放到栈里 如图

再接着走,发现了右小括号,因为栈顶现在是左小括号,所以他们两个匹配成功了,所以把左小括号弹出栈 弹出之后,栈变成了这个样子

接着遇到了左花括号,放入栈

再接着走,发现了右花括号,因为栈顶现在是左花括号,所以他们两个匹配成功了,所以把左花括号弹出栈 弹出之后,栈变成了这个样子

整个字符串也遍历完毕了,栈为空,所以这一串括号是合法的!

其他的示例大家可以试一下

接下来又到了分析时间复杂度的时间了

不难发现,每一个元素进栈和出栈的时间复杂度都是O(1)的时间复杂度。其次每一个元素都会进整个栈一次,且只进入一次,所以最后就是O(1)*n => O(n)的时间复杂度,因为时间复杂度要取最大的那个时间复杂度。

空间复杂度:最坏情况:所有元素都会压在这个栈里,所以也是O(n)的空间复杂度

接下来是实现代码

function valid(str) {
  let stack = [];
  let paren_map = {')': '(', ']': '[', '}': '{'};
  for (let i = 0, len = str.length; i < len; i++) {
    let tmpStr = str.charAt(i);
    if (!(tmpStr in paren_map)) {
      stack.push(tmpStr);
    } else if (stack.length !== 0 && paren_map[tmpStr] !== stack.pop()) {
      return false;
    }
  }
  return stack.length === 0;
}
let isValid = valid("(())")//true
let isValid = valid("()[]{}")//true
let isValid = valid("]][[")//false
let isValid = valid("([)]")//false

if (!(tmpStr in paren_map)) 这句是是说如果当前遍历到的括号他不在定义的map当中,就说明他不是右括号,他是左括号, 只要是左括号我们就放到stack当中去,否则说明是右括号,所以我们和栈顶的元素进行对比,看是否匹配,stack.length === 0这句是判断栈是否有元素,如果有的话,那就看栈顶的stack.pop()是不是和一开始存进去的paren_map[tmpStr]相匹配,如果不匹配直接返回false(不合法),如果都匹配完了,就需要判断它是否为空return stack.length === 0,如果栈为空,说明就是合法的。

这块逻辑可能比较抽象,大家去debug下就明白啦!

总结

我们通过做上面的题找出了好几种解法,当然我相信可能还有好多种解法,但我们的目标不在于把这道题目解决了就ok了,而是要写出性能相比较最优的! 简单常用算法咱们前端来说也是很重要的,平时大家可以多刷刷这类题。

大佬们如果发现了文中的错误,请在评论区指出,我会及时修正!

如果觉得对您有用请点个赞,谢谢大佬!