字符串问题在算法题中是非常重要的一个类型,而其中有不少题目的解法都使用到了栈这种数据结果。今天,我将通过两道字符串相关题目,深入探讨它们的解题思路与代码分析,并总结出它们是如何使用栈的,从而以点带面,提炼出此类字符串题目的共性思路。
题目1:括号补全问题
问题描述
小R有一个括号字符串 s,他想知道这个字符串是否是有效的。一个括号字符串如果满足以下条件之一,则是有效的:
- 它是一个空字符串;
- 它可以写成两个有效字符串的连接形式,即
AB; - 它可以写成
(A)的形式,其中A是有效字符串。
在每次操作中,小R可以在字符串的任意位置插入一个括号。你需要帮小R计算出,最少需要插入多少个括号才能使括号字符串 s 有效。
例如:当 s = "())" 时,小R需要插入一个左括号使字符串有效,结果为 1。
测试样例
样例1:
输入:
s = "())"
输出:1
样例2:
输入:
s = "((("
输出:3
样例3:
输入:
s = "()"
输出:0
样例4:
输入:
s = "()))(("
输出:4
解题思路
让我们首先考虑一个问题:假设我们已经处理了一段括号字符串,当一个新的括号增加到字符串末尾时,我们应该怎么做?
显然,如果增加的是左括号(,它并不会新增任何匹配的括号对,但我们应该在程序中以某种方式储存它,从而与之后可能有的右括号)匹配。如果增加的是右括号),我们应当找到位于最右边的尚未被匹配的左括号与它匹配,并更新储存左括号(的数据结构,使我们能够再次找到下一个位于最右边的尚未被匹配的左括号(。如果右边没有未匹配的左括号(,该右括号)则永远无法被成功匹配。
因此,我们可以得到这样一个结论:只要我们找到一个合适的数据结构,能够存放截止目前为止的所有左括号(的位置,方便地获取位于最右边的尚未被匹配的左括号(,同时删除并更新下一个符合条件的左括号(。很明显,栈刚好满足我们的要求,可以利用栈来追踪括号的匹配状态。
在遍历字符串时,我们进行如下的操作:
- 当遇到一个左括号
(,将其压入栈中; - 当遇到一个右括号
)时,如果栈不为空,则弹出栈顶元素,表示一对有效括号匹配成功; - 如果栈为空且遇到右括号
),说明需要插入一个左括号。
遍历结束后,栈中剩余的左括号数量即为未匹配的左括号数量,因此我们需要再插入相应数量的右括号。最终,需要插入的括号总数等于未匹配的右括号加上栈中未匹配的左括号数量。
代码实现
public static int solution(String s) {
Stack<Integer> st = new Stack<>();
int len = s.length();
int ans = 0;
for (int i = 0; i < len; i++) {
if (s.charAt(i) == '(') {
st.push(i); // 左括号入栈
} else {
if (st.empty()) {
ans++; // 若栈为空,记录需要插入的左括号
} else {
st.pop(); // 栈不为空时弹出栈顶,表示匹配成功
}
}
}
// 栈中剩余元素为未匹配的左括号数量
ans += st.size();
return ans;
}
代码详解
- 初始化一个空栈
st和计数器ans,用于记录需要插入的括号数。 - 遍历字符串,当遇到左括号
(时,将其入栈;遇到右括号)时,若栈不为空,则弹出栈顶元素。若栈为空,则增加计数器ans,表示需要插入左括号来匹配当前右括号。 - 遍历结束后,栈中的未匹配左括号数量为
st.size(),需要插入相应数量的右括号,因此将其累加到ans中。 - 最终返回
ans,即为所需的最小插入数。
示例
对于输入 s = "())(":
- 第一个字符
(入栈; - 第二个字符
)匹配栈顶左括号,栈顶弹出; - 第三个字符
)时,栈为空,因此记录一次插入左括号,答案累加1; - 第四个字符
(入栈。
最终答案为2。
题目2:相邻重复字母删除问题
问题描述
给定一个仅包含小写字母的字符串 s,可以进行以下操作:选择两个相邻且相同的字母并删除它们。该操作可以重复进行,直到无法再删除为止。要求返回最终处理完后的字符串,保证返回结果是唯一的。
测试样例
样例1:
输入:
s = "abbaca"
输出:'ca'
样例2:
输入:
s = "azxxzy"
输出:'ay'
样例3:
输入:
s = "a"
输出:'a'
解题思路
如果我们把该问题中不同的字符视作不同的括号,该问题就可以转换为与括号匹配相类似的问题。同样的,我们可以利用栈来存储未删除的字符,并在遇到相邻重复字符时将它们弹出,从而删除它们:
- 遍历字符串中的每个字符;
- 如果栈不为空且栈顶字符等于当前字符,则弹出栈顶元素;
- 否则,将当前字符压入栈中。
遍历结束后,栈中的所有字符即为删除重复字符后的最终结果。
代码实现
public static String solution(String s) {
Stack<Character> stack = new Stack<>();
for (char ch : s.toCharArray()) {
if (!stack.isEmpty() && stack.peek() == ch) {
stack.pop(); // 相邻且相同字符,移除栈顶
} else {
stack.push(ch); // 不相同则压入栈中
}
}
StringBuilder result = new StringBuilder();
for (char ch : stack) {
result.append(ch); // 拼接栈中的剩余字符
}
return result.toString();
}
代码详解
- 初始化一个空栈
stack。 - 遍历字符串
s中的每一个字符ch,若栈不为空且栈顶字符与当前字符相同,则弹出栈顶字符,表示删除该对相邻字符。否则,将当前字符压入栈。 - 最后,将栈中的字符依次添加到
StringBuilder中,并返回结果。
示例
对于输入 s = "abbaca":
- 首先将
a压入栈; - 遇到两个
b,栈顶弹出; - 遇到
a,栈顶弹出; c入栈;a入栈。 最终返回ca。
总结
这两道题目都使用到了栈的数据结构,尽管两题解法不完全相同,但它们有许多共同之处:
- 都利用了栈的“后进先出”特性来解决字符匹配问题;
- 通过遍历字符串和栈操作维护字符串的有效性;
总的来说,栈在字符串算法中的应用非常广泛,尤其是匹配、消除和维护字符有效性的场景。掌握栈的使用技巧,有助于我们处理类似的字符串问题。