文章简要概述
- 本文主要进行栈相关的算法题刷题题解记录,记录栈相关算法以及如何解。
- 这文一共有6道题,主要介绍leetcode中
棒球比赛、比较含退格的字符串、验证栈序列、有效的括号、删除最外层的括号和移除无效的括号的解题思路。
栈相关概念
栈(stack)可以理解成一个收纳盒,放在最底层,最后才能拿出来,在 LIFO 数据结构中,将首先处理添加到队列中的最新元素。与队列不同,栈是一个 LIFO 数据结构。通常,插入操作在栈中被称作入栈 push 。与队列类似,总是在堆栈的末尾添加一个新元素。但是,删除操作,退栈 pop ,将始终删除队列中相对于它的最后一个元素。关于出栈入栈演示看下图。(图片资源来源leetcode)
与栈相关算法
棒球比赛
题目大意:
你现在是一场采用特殊赛制棒球比赛的记录员。这场比赛由若干回合组成,过去几回合的得分可能会影响以后几回合的得分。
比赛开始时,记录是空白的。你会得到一个记录操作的字符串列表 ops,其中 ops[i] 是你需要记录的第 i 项操作,ops 遵循下述规则:
整数 x - 表示本回合新获得分数 x
"+" - 表示本回合新获得的得分是前两次得分的总和。题目数据保证记录此操作时前面总是存在两个有效的分数。
"D" - 表示本回合新获得的得分是前一次得分的两倍。题目数据保证记录此操作时前面总是存在一个有效的分数。
"C" - 表示前一次得分无效,将其从记录中移除。题目数据保证记录此操作时前面总是存在一个有效的分数。
请你返回记录中所有得分的总和。
示例
输入:ops = ["5","2","C","D","+"]
输出:30
解释:
"5" - 记录加 5 ,记录现在是 [5]
"2" - 记录加 2 ,记录现在是 [5, 2]
"C" - 使前一次得分的记录无效并将其移除,记录现在是 [5]
"D" - 记录加 2 * 5 = 10 ,记录现在是 [5, 10]
"+" - 记录加 5 + 10 = 15 ,记录现在是 [5, 10, 15]
所有得分的总和 5 + 10 + 15 = 30
解题思路:
- 是一个典型的栈问题,创建一个栈,这里用数组模拟。
- 遍历数组,判断当前操作符是数,放入到栈中(push),如果是
C,就弹出栈中元素;如果是D,取出栈顶元素 * 2,如果是+,从栈中取出两位元素,进行求和,并将和值放入到栈中。 - 遍历栈中元素,求的和值。
代码:
/**
* @param {string[]} ops
* @return {number}
*/
function calPoints (ops) {
const stack = [];
for (let opt of ops) {
if (opt === 'C') {
stack.pop();
} else if (opt === 'D') {
const val = stack.pop();
stack.push(val, val * 2);
} else if (opt === '+') {
const val1 = stack.pop();
const val2 = stack.pop();
stack.push(val2, val1, val1 + val2);
} else {
stack.push(Number(opt));
}
}
return stack.reduce((total, val) => total + val, 0);
};
比较含退格的字符串
题目大意:
给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,请你判断二者是否相等。# 代表退格字符。
如果相等,返回 true ;否则,返回 false 。
注意:如果对空文本输入退格字符,文本继续为空。
示例:
输入: s = "ab#c", t = "ad#c"
输出: true
解释: S 和 T 都会变成 “ac”。
解题思路:
- 题目中说到
#出现会删除前一项值,很符合栈的结构特性。 - 定义一个栈,将字符串遍历,把每一个值放入到栈中,遇到
#从栈顶取出元素。 - 返回栈中剩余的元素,比较输入的两个字符,处理后的值是否相等。
代码:
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
function getStr (str) {
const stack = [];
for(let k in str) {
if (str[k] === '#') {
stack.pop();
} else {
stack.push(str[k]);
}
}
return stack.join('');
}
var backspaceCompare = function(s, t) {
const s1 = getStr(s);
const t1 = getStr(t);
return s1 === t1
};
验证栈序列
题目大意:
给定 pushed 和 popped 两个序列,每个序列中的 值都不重复,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false 。
示例:
输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
解题思路:
- 这道题很容易让人读完题,明白要做什么,但编码的时候不太容易想清楚怎么实现。
- 我们可以发现一个规律,pushed序列中的元素,我们按照顺序放入栈中,当放入的元素与popped序列中的第一个元素相等时,我们要将pushed序列中最后放入的这个元素从栈中弹出。
- 再比较popped序列下一位元素与pushed序列中最后一位元素是否相等,相等接着弹出元素,直到不相等或栈元素为空。
代码:
/**
* @param {number[]} pushed
* @param {number[]} popped
* @return {boolean}
*/
function validateStackSequences (pushed, popped) {
const stack = [];
let j = 0;
for(let i = 0; i < popped.length; i++) {
stack.push(pushed[i]);
while(stack.length && stack[stack.length - 1] === popped[j]) {
stack.pop();
j++;
}
}
return !stack.length;
};
有效的括号
题目大意:
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
示例:
案例一:
输入: s = "()[]{}"
输出: true
案例二:
输入: s = "(]"
输出: false
解题思路:
- 这道题如果读完题能想到用栈来处理,问题就解决一半了。
- 括号是相对应的。如下图形式,将元素入栈,如果出现右括号时判断栈是否为空,为空肯定不符合条件,或者栈顶元素是否是左括号,不是也不符合条件,弹出元素。
- 最后判断栈是否为空即可。
代码:
/**
* @param {string} s
* @return {boolean}
*/
function isValid (s) {
const l = s.length;
if (l % 2 === 1) return false;
const stack = [];
const symbol = new Map([
[')', '('],
['}', '{'],
[']', '['],
]);
for(let val of s) {
if (symbol.has(val)) {
if (!stack.length || stack[stack.length - 1] !== symbol.get(val)) return false;
stack.pop();
} else {
stack.push(val);
}
}
return !stack.length
};
删除最外层的括号
题目大意:
有效括号字符串为空 ""、"(" + A + ")" 或 A + B ,其中 A 和 B 都是有效的括号字符串,+ 代表字符串的连接。
例如,"","()","(())()" 和 "(()(()))" 都是有效的括号字符串。
如果有效字符串 s 非空,且不存在将其拆分为 s = A + B 的方法,我们称其为原语(primitive),其中 A 和 B 都是非空有效括号字符串。
给出一个非空有效字符串 s,考虑将其进行原语化分解,使得:s = P_1 + P_2 + ... + P_k,其中 P_i 是有效括号字符串原语。
对 s 进行原语化分解,删除分解中每个原语字符串的最外层括号,返回 s 。
示例:
输入:s = "(()())(())" 输出:"()()()" 解释: 输入字符串为 "(()())(())",原语化分解得到 "(()())" + "(())", 删除每个部分中的最外层括号后得到 "()()" + "()" = "()()()"。
解题思路:
- 需要去除外层括号,为了找到这些需要去除的外层括号,可以使用到计数器
- 遇到左括号,我们的计数器
+1,遇到右括号,我们的计数器-1。 - 可以得到一下规律,可以结合下图理解
- 遇到左括号,当前计数值大于 00 ,则属于有效的左括号。
- 遇到右括号,当前计数值大于 11 ,则属于有效的右括号。
图一
图二
图三
代码:
/**
* @param {string} s
* @return {string}
*/
var removeOuterParentheses = function(s) {
let count = 0;
let res = '';
for(let val of s) {
if (val === '(' && count++ > 0) res += val;
if (val === ')' && count-- > 1) res += val;
}
return res;
};
移除无效的括号
题目大意:
给你一个由 '('、')' 和小写字母组成的字符串 s。
你需要从字符串中删除最少数目的 '(' 或者 ')' (可以删除任意位置的括号),使得剩下的「括号字符串」有效。
请返回任意一个合法字符串。
有效「括号字符串」应当符合以下 任意一条 要求:
空字符串或只包含小写字母的字符串
可以被写作 AB(A 连接 B)的字符串,其中 A 和 B 都是有效「括号字符串」
可以被写作 (A) 的字符串,其中 A 是一个有效的「括号字符串」
示例:
输入:s = "lee(t(c)o)de)"
输出:"lee(t(c)o)de"
解释:"lee(t(co)de)" , "lee(t(c)ode)" 也是一个可行答案。
解题思路:
- 这道题可以先除去干扰项,本质是看括号,
lee(t(c)o)de)可以看成(())),右括号多一个 - 那么只要遍历字符串,标记多余括号的位置,在去除标记为的值,即可得到。
- 遍历到左括号时,放入标记,匹配到右括号时,查看栈中是否有左括号,存在就取消之前标记。
代码:
/**
* @param {string} s
* @return {string}
*/
function minRemoveToMakeValid (s) {
let leftDel = [];
let rightDel = [];
for(let i in s) {
if (s[i] === '(') {
leftDel.push(i);
} else if (s[i] === ')') {
if (leftDel.length) {
leftDel.pop();
} else {
rightDel.push(i);
}
}
}
const del = [...leftDel, ...rightDel];
const res = [...s];
for(let i = 0; i < del.length; i++) {
res[del[i]] = '';
}
return res.join('');
};
结束语
数据结构与算法相关的练习题会持续输出,一起来学习,持续关注。当前是栈部分。后期还会有其他类型的数据结构,题目来源于leetcode。往期文章:
有兴趣的可以一起来刷题,如果文章对你有帮助,感谢点赞👍,关注!