题目名称:删除最外层的括号
有效括号字符串为空 (""), "(" + A + ")" 或 A + B, 其中 A 和 B 都是有效的括号字符串, + 代表字符串的连接. 例如, "", "()", "(())()" 和 "(()(()))" 都是有效的括号字符串.
如果有效字符串 S 非空, 且不存在将其拆分为 S = A+B 的方法, 我们称其为原语 (primitive) , 其中 A 和 B 都是非空有效括号字符串.
给出一个非空有效字符串 S, 考虑将其进行原语化分解, 使得: S = P_1 + P_2 + ... + P_k, 其中 P_i 是有效括号字符串原语.
对 S 进行原语化分解, 删除分解中每个原语字符串的最外层括号, 返回 S .
示例 1:
输入: "(()())(())" 输出: "()()()" 解释: 输入字符串为 "(()())(())", 原语化分解得到 "(()())" + "(())", 删除每个部分中的最外层括号后得到 "()()" + "()" = "()()()". 示例 2:
输入: "(()())(())(()(()))" 输出: "()()()()(())" 解释: 输入字符串为 "(()())(())(()(()))", 原语化分解得到 "(()())" + "(())" + "(()(()))", 删除每个部分中的最外层括号后得到 "()()" + "()" + "()(())" = "()()()()(())". 示例 3:
输入: "()()" 输出: "" 解释: 输入字符串为 "()()", 原语化分解得到 "()" + "()", 删除每个部分中的最外层括号后得到 "" + "" = "".
解法
1、单调栈识别有效括号
从前向后依次匹配有效括号, 将整个有效括号删除外层括号后保存到结果集中; 结果集最后 flat 并 join 即可
tips: 所谓单调栈, 就是只存储一种类型数据或者一种数据的栈 (数组)
/**
* @param {string} S
* @return {string}
* * 0、单调栈解法(stack栈中只存放左括号,右括号是用来消消乐的);
* * 从前向后依次匹配有效括号,将整个有效括号删除外层括号后保存到结果集中;结果集最后flat并join即可
*/
var removeOuterParentheses = function (S) {
const bracket = { '(': ')' }
const stack = []
let res = []
const result = []
for (let i = 0; i < S.length; i++) {
const val = S[i]
if (bracket[val]) { // 左括号
stack.push(val)
} else { // 右括号
stack.pop()
}
if (stack.length === 0) {
// 存在一组有效括号
res.push(val)
const cur = [...res]
cur.shift()
cur.pop()
result.push(cur)
res = []
continue
}
res.push(val)
}
return result.flat().join('')
};
2、单调栈改进——直接识别括号进行push
/**
* @param {string} S
* @return {string}
* * 1、单调栈解法的改进版;
* * 不需要获取完成有效括号,只要把“非外层括号”push到结果集中即可
*/
var removeOuterParentheses1 = function (S) {
const stack = []
const result = []
for (let i = 0; i < S.length; i++) {
const char = S[i]
if (char === '(') {
if (stack.length) {
result.push(char)
}
stack.push(char)
} else {
if (stack.length >= 2) {
result.push(char)
}
stack.pop()
}
}
return result.join('')
}
3、双指针法+计数法
- 双指针用来标记最外层括号的下标
- 计数用来判断是否为最外层口号
/**
* @param {string} S
* @return {string}
* * 2、双指针计数法;
* * 定义左右两个指针,找到最外层括号的下标,依次排除
* ! 耗时较长,并不可取
*/
var removeOuterParentheses2 = function (S) {
const subscript = []
let left = 0
let right = 0
let count = 1
for (let i = 1; i < S.length; i++) {
const char = S[i]
if (char === '(') {
// 如果遇到左括号,则技术+1
count++
} else {
count--
}
// * 检测count是否为0
if (count === 0) {
// 找到右侧下标了
right = i
subscript.push(left, right)
right = 0
left = i + 1
}
}
console.log(subscript);
return [...S].filter((v, k) => !subscript.includes(k)).join('')
}
4、单指针计数法(双指针优化)
- 同理
单调栈改进,只需要根据计数识别是否为内层括号
/**
* @param {string} S
* @return {string}
* * 3、单指针计数法;
* * 只用count计数,如果>0则为内层括号
* ! 注意:需要检测下一个是否有值,如果有值,就要重置count为1,并且跳过下一次比较
*/
var removeOuterParentheses3 = function (S) {
const result = []
let count = 1
for(let i = 1; i < S.length; i++){
const char = S[i]
if(char === '('){
count++
}else {
count--
}
if(count !== 0){
result.push(char)
} else if(S[i+1]) {
// * 第一组有效括号匹配完成,第二组也有值
count = 1
i++
}
}
return result.join('')
}
5、集大成简洁解法
- 巧妙利用js的++和--特性,完美实现第一个口号和最后一个括号排除
/**
* * 4、更加简洁的js计数思路,巧妙利用++和--效果进行首尾字符排除
* @param {*} S
*/
var removeOuterParentheses4 = function(S) {
let res = '';
let opened = 0;
for(let c of S) {
if(c === '(' && opened ++ > 0) res += c;
if(c === ')' && opened -- > 1) res += c;
}
return res;
};
测试用例
let test = "(()())(())"
let test1 = "(()())(())(()(()))"
console.time('执行用时');
console.log(removeOuterParentheses4(test));
console.timeEnd('执行用时');
总结
- 单调栈概念:只存储一种类型数据或者一种数据的栈 (数组)
计数法和单调栈的结合应用- 化整为零的思维方式降低时间复杂度
- 找到最外层括号 ---> 只找最内层括号
- 识别整个有效括号 ---> 只识别非最外层括号
- 巧妙利用js的++和--特性
- 数组和字母串操作上的区别思考
- 数组操作提高性能减少频繁创建内存地址
- 字符串直接操作,牺牲频繁创建的性能节省join操作
说明
- 本题解已同步GitHub地址,可以复制代码调试。
- 总结出了一套亲测有效的前端无痛刷题项目,有助于算法小白平稳入门,详见leetcode-js-roadmap,希望对从零开始刷题的前端小伙伴们带来帮助~
本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情