记录今天学习的三道算法题:两道滑动窗口和一道栈的应用。
2904. 最短且字典序最小的美丽子字符串
题目描述
思路
滑动窗口
解题过程
题目要求找到包含
k
个 ‘1’ 的子字符串,并且需要满足两个条件:
- 最短长度:在所有包含
k
个 ‘1’ 的子字符串中,长度最小。- 字典序最小:如果存在多个长度最短的子字符串,选择字典序最小的那个。
我们可以使用滑动窗口来解决这个问题。维护一个窗口
[left, right]
,以及窗口内 ‘1’ 的计数count
。
- 扩展窗口:移动
right
指针向右扩展窗口。如果s[right]
是 ‘1’,则count
增加。- 检查与收缩窗口:当
count
达到k
时,当前窗口[left, right]
就包含恰好k
个 ‘1’ 了吗?不一定,可能count > k
。 我们需要一个while
循环,当count >= k
时:
- 找到有效子串:如果
count == k
,此时窗口[left, right]
是一个包含k
个 ‘1’ 的有效子字符串。我们计算其长度len = right - left + 1
。- 更新结果:
- 如果
len
小于当前记录的最短长度retLen
,则更新retLen = len
,并将当前子字符串s.substring(left, right + 1)
存为retString
。- 如果
len
等于retLen
,则比较当前子字符串s.substring(left, right + 1)
与retString
的字典序,如果当前子字符串字典序更小,则更新retString
。- 收缩窗口:为了寻找可能更短的或者字典序更小的有效子串(或者单纯为了让窗口继续滑动),我们需要移动
left
指针向右收缩窗口。如果滑出窗口的字符s[left]
是 ‘1’,则count
减少。然后left++
。- 循环与结束:
right
指针遍历完整个字符串后结束。- 返回结果:如果
retLen
仍然是初始值n + 1
,说明没有找到包含k
个 ‘1’ 的子字符串,返回空字符串""
;否则返回retString
。
复杂度
- 时间复杂度: O ( n ) O(n) O(n) - 每个字符最多进出窗口一次。字符串比较在最坏情况下可能达到 O ( n ) O(n) O(n),但均摊下来,总复杂度仍接近 O ( n ) O(n) O(n)。
- 空间复杂度: O ( 1 ) O(1) O(1) - 只使用了常数级别的额外空间(不算存储结果字符串的空间)。
Code
class Solution {
public String shortestBeautifulSubstring(String s, int k) {
String retString = "";
int n = s.length();
int retLen = n + 1;
int count = 0;
for (int left = 0, right = 0; right < n; right++) {
char in = s.charAt(right);
// 进窗口
if (in == '1') {
count++;
}
// 判断与收缩
while (count >= k) {
if (count == k) {
int len = right - left + 1;
// 更新结果
String currentSub = s.substring(left, right + 1);
if (len < retLen) {
retLen = len;
retString = currentSub;
} else if (len == retLen) {
if (retString.isEmpty() || currentSub.compareTo(retString) < 0) {
retString = currentSub;
}
}
}
// 出窗口
if (s.charAt(left) == '1') {
count--;
}
left++;
}
}
return (retLen == n + 1) ? "" : retString;
}
}
209. 长度最小的子数组
题目描述
思路
滑动窗口
解题过程
维护一个滑动窗口
[left, right]
和窗口内元素的和sum
。
- 扩展窗口:
right
指针向右移动,将nums[right]
加入sum
。- 检查与收缩窗口:使用
while
循环检查sum
是否大于等于target
。
- 如果
sum >= target
,说明当前窗口[left, right]
是一个满足条件的子数组。记录其长度right - left + 1
,并更新全局最小长度ret
。- 收缩窗口:因为题目要求最小长度,并且数组元素都是正数,所以当前窗口已经满足条件后,可以尝试缩短它。将
nums[left]
从sum
中减去,并将left
指针右移 (left++
)。继续在while
循环内检查sum
是否仍然满足>= target
。- 循环与结束:
right
指针遍历完整个数组后结束。- 返回结果:如果
ret
仍然是初始的最大值Integer.MAX_VALUE
,说明没有找到满足条件的子数组,返回 0;否则返回ret
。
复杂度
- 时间复杂度: O ( n ) O(n) O(n) - 每个元素最多进出窗口一次。
- 空间复杂度: O ( 1 ) O(1) O(1) - 只使用了常数级别的额外空间。
Code
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int ret = Integer.MAX_VALUE;
int sum = 0;
int n = nums.length;
for (int left = 0, right = 0; right < n; right++) {
// 进窗口
sum += nums[right];
// 判断与收缩
while (sum >= target) {
// 更新结果
ret = Math.min(ret, right - left + 1);
// 出窗口
sum -= nums[left];
left++;
}
}
// 返回结果
return ret == Integer.MAX_VALUE ? 0 : ret;
}
}
20. 有效的括号
题目描述
思路
栈 (Stack)
解题过程
利用栈的“后进先出”特性来匹配括号。
- 遍历字符串:逐个检查字符串中的字符。
- 左括号:如果遇到左括号(
(
、[
、{
),将其压入栈中。- 右括号:如果遇到右括号(
)
、]
、}
):
- 检查栈是否为空:如果此时栈为空,说明没有对应的左括号与之匹配,字符串无效,返回
false
。- 检查栈顶元素:查看(不弹出)栈顶的左括号
look = stack.peek()
。- 匹配判断:判断当前右括号
in
是否与栈顶左括号look
匹配。
- 如果匹配(例如
look == '('
且in == ')'
),则弹出栈顶元素stack.pop()
。- 如果不匹配,说明括号类型不对应,字符串无效,返回
false
。- 遍历结束:遍历完整个字符串后:
- 检查栈是否为空:如果栈为空,说明所有括号都成功匹配,字符串有效,返回
true
。- 如果栈不为空,说明有多余的左括号没有被匹配,字符串无效,返回
false
。优化: 可以在开始时检查字符串长度是否为奇数,如果是奇数,则肯定无效,可以直接返回
false
。
复杂度
- 时间复杂度: O ( n ) O(n) O(n) - 只需遍历一次字符串。
- 空间复杂度: O ( n ) O(n) O(n) - 最坏情况下,如果字符串全是左括号,栈的大小会等于字符串长度。
Code
class Solution {
public boolean isValid(String ss) {
if (ss == null || ss.length() % 2 != 0) {
return false;
}
Stack<Character> stack = new Stack<>();
char[] s = ss.toCharArray();
for (int i = 0; i < s.length; i++) {
char in = s[i];
// 左括号入栈
if (in == '(' || in == '[' || in == '{') {
stack.push(in);
} else {
// 右括号处理
// 栈为空,但遇到右括号
if (stack.isEmpty()) {
return false;
}
char look = stack.peek();
// 检查是否匹配
if ((look == '(' && in == ')') ||
(look == '[' && in == ']') ||
(look == '{' && in == '}')) {
stack.pop();
} else {
// 没匹配上
return false;
}
}
}
// 遍历结束后,栈应该是空的
return stack.isEmpty();
}
}