大家好,我是 「前端下饭菜」,80后大龄程序员。我会在算法系列专栏记录leetcode高频算法题,感兴趣的小伙伴可收藏吃灰。
删除无效的括号
给你一个由若干括号和字母组成的字符串 s ,删除最小数量的无效括号,使得输入的字符串有效。
返回所有可能的结果。答案可以按 任意顺序 返回。
示例 1:
输入:s = "()())()"
输出:["(())()","()()()"]
示例 2:
输入:s = "(a)())()"
输出:["(a())()","(a)()()"]
栈
通过栈,我们可以在遍历给定字符串的过程中去判断到目前为止扫描的子串的有效性,同时能得到最长有效括号的长度。
具体做法是我们始终保持栈底元素为当前已经遍历过的元素中「最后一个没有被匹配的右括号的下标」,这样的做法主要是考虑了边界条件的处理,栈里其他元素维护左括号的下标:
- 对于遇到的每个 ‘(’ ,我们将它的下标放入栈中
- 对于遇到的每个 ‘)’,我们先弹出栈顶元素表示匹配了当前右括号:
- 如果栈为空,说明当前的右括号为没有被匹配的右括号,我们将其下标放入栈中来更新我们之前提到的「最后一个没有被匹配的右括号的下标」
- 如果栈不为空,当前右括号的下标减去栈顶元素即为「以该右括号为结尾的最长有效括号的长度」
我们从前往后遍历字符串并更新答案即可。
需要注意的是,如果一开始栈为空,第一个字符为左括号的时候我们会将其放入栈中,这样就不满足提及的「最后一个没有被匹配的右括号的下标」,为了保持统一,我们在一开始的时候往栈中放入一个值为−1的元素。
/**https://leetcode.cn/problems/longest-valid-parentheses/solutions/314683/zui-chang-you-xiao-gua-hao-by-leetcode-solution/?envType=featured-list&envId=2cktkvj
* 方法2
* @param {string} s
* @return {number}
*/
var longestValidParentheses = function(s) {
if (s.length <= 1) {
return 0;
}
const stack = [-1];
let len = 0;
for (let i = 0; i < s.length; i++) {
if (s[i] === '(') {
stack.push(i);
} else {
stack.pop();
if (!stack.length) {
stack.push(i);
} else {
len = Math.max(len, i - stack.at(-1));
}
}
}
return len;
}
复杂度分析
时间复杂度: O(n),n 是给定字符串的长度。我们只需要遍历字符串一次即可。 空间复杂度: O(n)。栈的大小在最坏情况下会达到n,因此空间复杂度为O(n) 。
通配符匹配
给你一个输入字符串 (s) 和一个字符模式 (p) ,请你实现一个支持 '?' 和 '*' 匹配规则的通配符匹配:
- '?' 可以匹配任何单个字符。
- '*' 可以匹配任意字符序列(包括空字符序列)。
判定匹配成功的充要条件是:字符模式必须能够 完全匹配 输入字符串(而不是部分匹配)。
示例 1:
输入: s = "aa", p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
示例 2:
输入: s = "aa", p = "*"
输出: true
解释: '*' 可以匹配任意字符串。
动态规划
在给定的模式 ppp 中,只会有三种类型的字符出现:
- 小写字母 a−z,可以匹配对应的一个小写字母;
- 问号?,可以匹配任意一个小写字母;
- 星号∗,可以匹配任意字符串,可以为空,也就是匹配零或任意多个小写字母。
其中「小写字母」和「问号」的匹配是确定的,而「星号」的匹配是不确定的,因此我们需要枚举所有的匹配情况。为了减少重复枚举,我们可以使用动态规划来解决本题。
我们用 dp[i][j]表示字符串 s 的前 i 个字符和模式 p 的前 j 个字符是否能匹配。在进行状态转移时,我们可以考虑模式 p 的第 j 个字符 pj,与之对应的是字符串 s 中的第 i 个字符 si:
- 如果 pj 是小写字母,那么 s 必须也为相同的小写字母,状态转移方程为:
- dp[i][j]=(si与 pj相同) ∧ dp[i−1][j−1]
其中 ^ 表示逻辑与运算。也就是说,dp[i][j] 为真,当且仅当 dp[i-1][j-1] 为真,并且 si与pj相同。
- 如果 pj是问号,那么对 si没有任何要求,状态转移方程为:
- dp[i][j]=dp[i−1][j−1]
- 如果 pj是星号,那么同样对 si 没有任何要求,但是星号可以匹配零或任意多个小写字母,因此状态转移方程分为两种情况,即使用或不使用这个星号:
- dp[i][j]=dp[i][j−1] ∨ dp[i−1][j]
其中 v 表示逻辑或运算。如果我们不使用这个星号,那么就会从dp[i][j-1]转移而来;如果我们使用这个星号,那么就会从 dp[i-1][j] 转移而来。
最终的状态转移方程如下:
/**
* 采用动态规划
* @param {string} s
* @param {string} p
* @return {boolean}
*/
var isMatch = function(s, p) {
// 用dp[i][j]存储动态规划结果
// 先计算边界值,dp[0][0]=true,dp[i][0]为false,dp[0][j]只有前j个字符连续为*才为true
const m = s.length, n = p.length;
const dp = new Array(m + 1).fill(undefined);
dp.forEach((val, i) => {
dp[i] = new Array(n + 1).fill(false);
});
dp[0][0] = true;
for (let j = 1; j <= n; j++) {
if (p[j - 1] === '*') {
dp[0][j] = true;
} else {
break;
}
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (s[i - 1] === p[j - 1] || p[j - 1] === '?') {
dp[i][j] = dp[i - 1][j - 1];
} else if (p[j - 1] === '*') {
dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
} else {
dp[i][j] = false;
}
}
}
return dp[m][n];
};
复杂度分析
- 时间复杂度:O(mn),其中 m 和 n分别是字符串 s 和模式 p 的长度。
- O(mn),即为存储所有 (m+1)(n+1) 个状态需要的空间。此外,在状态转移方程中,由于 dp[i][j]只会从 dp[i][..] 以及 dp[i−1][..] 转移而来,因此我们可以使用滚动数组对空间进行优化,即用两个长度为 n+1 的一维数组代替整个二维数组进行状态转移,空间复杂度为 O(n)。
正则表达式匹配
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。
'.'匹配任意单个字符'*'匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
示例 1:
输入: s = "aa", p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串
示例 2:
输入: s = "aa", p = "a*"
输出: true
解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
动态规划
题目中的匹配是一个「逐步匹配」的过程:我们每次从字符串 p 中取出一个字符或者「字符 + 星号」的组合,并在 s 中进行匹配。对于 p 中一个字符而言,它只能在 s 中匹配一个字符,匹配的方法具有唯一性;而对于 p 中字符 + 星号的组合而言,它可以在 s 中匹配任意自然数个字符,并不具有唯一性。因此我们可以考虑使用动态规划,对匹配的方案进行枚举。
我们用 f[i][j]表示 s 的前 i 个字符与 p 中的前 j 个字符是否能够匹配。在进行状态转移时,我们考虑 p 的第 j 个字符的匹配情况:
- 如果 p 的第 j 个字符是一个小写字母,那么我们必须在 s 中匹配一个相同的小写字母,即
- 如果 p 的第 j 个字符是 *,那么就表示我们可以对 p 的第 j−1 个字符匹配任意自然数次。在匹配 0 次的情况下,我们有:
- f[i][j]=f[i][j−2]
也就是我们「浪费」了一个字符 + 星号的组合,没有匹配任何 $s$ 中的字符。
- f[i][j]=f[i][j−2]
- 在任意情况下,只要 p[j] 是
.,那么 p[j]一定成功匹配 s 中的任意一个小写字母。
最终的状态转移方程如下:
- 在任意情况下,只要 p[j]p[j]p[j] 是
.,那么 p[j]p[j]p[j] 一定成功匹配 sss 中的任意一个小写字母。
最终的状态转移方程如下:
其中 matches(x,y) 判断两个字符是否匹配的辅助函数。只有当 y 是 . 或者 x 和 y 本身相同时,这两个字符才会匹配。
/**
* 采用动态规划
* 细节:*号前面肯定是一个元素,并且前面的元素不能也为*号
* @param {string} s
* @param {string} p
* @return {boolean}
*/
var isMatch = function(s, p) {
// 用dp[i][j]存储动态规划结果
// 先计算边界值,dp[0][0]=true,dp[i][0]为false,dp[0][j]只有前j个字符连续为*才为true
const m = s.length, n = p.length;
const dp = new Array(m + 1).fill(undefined);
dp.forEach((val, i) => {
dp[i] = new Array(n + 1).fill(false);
});
dp[0][0] = true;
function matchs(i, j) {
if (i === 0) {
return false;
}
if (p[j - 1] === '.') {
return true
} else {
return p[j - 1] === s[i - 1];
}
}
for (let i = 0; i <= m; i++) {
for (let j = 1; j <=n; j++) {
if (p[j - 1] === '*') {
// 假设 *和j-1匹配0次,则i,j的结果为i,j - 2的匹配结果
dp[i][j] = dp[i][j - 2];
//假如*和j - 1匹配一次或者多次, 如果 i和j-1匹配,则dp[i][j]的结果为dp[i - 1][j]的结果
if (matchs(i, j - 1)) {
dp[i][j] |= dp[i - 1][j];
}
} else if (matchs(i, j)) {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
return !!dp[m][n];
};
复杂度分析
- 时间复杂度:O(mn),其中 m 和 n 分别是字符串 s 和 p 的长度。我们需要计算出所有的状态,并且每个状态在进行转移时的时间复杂度为 O(1)。
- 空间复杂度:O(mn),即为存储所有状态使用的空间。