每日算法-250323
💣 拆炸弹:滑动窗口的巧妙应用
拆炸弹
题目描述:
给定一个循环数组
code
和一个整数k
,你需要根据k
的值来修改code
数组:
- 如果
k > 0
,将code[i]
替换为 后k
个数 的和。- 如果
k < 0
,将code[i]
替换为 前k
个数 的和。- 如果
k = 0
,将code[i]
替换为 0。
题目图片:
思路分析:
这道题的核心在于如何高效地计算 code[i]
对应的替换值。直接暴力求解会导致大量重复计算。观察到,我们可以利用 滑动窗口 的思想,维护一个大小为 k
的窗口,计算窗口内元素的和,从而避免重复计算。
解题过程:
-
特殊情况处理: 当
k = 0
时,直接返回一个全为 0 的新数组。 -
处理负数
k
: 为了统一处理逻辑,当k
为负数时,我们将code
数组 倒置,并将k
取绝对值。这样,负数k
的问题就转化为了正数k
的问题。 -
初始化滑动窗口: 计算初始窗口的和
sum
,即code
数组前a
个元素的和(a
为k
的绝对值)。 -
滑动窗口:
- 维护一个左边界
left
和右边界right
,初始时left = 1
,right = a + 1
。 - 每次迭代,将窗口向右移动一位:
- 由于
code
是循环数组,当right
超出数组范围时,将其置为 0。 - 更新窗口的和
sum
:sum = sum - code[left] + code[right]
。 - 更新
ret[left]
的值为sum
。 left
和right
分别加 1。
- 由于
- 维护一个左边界
-
处理负数
k
的结果: 如果k
为负数,在返回结果前,需要将ret
数组 倒置。
复杂度分析:
- 时间复杂度:O(n),其中 n 是
code
数组的长度。 - 空间复杂度:O(n),用于存储结果数组
ret
。
代码实现(Java):
class Solution {
public int[] decrypt(int[] code, int k) {
int n = code.length;
int[] ret = new int[n];
if (k == 0) {
return ret;
}
int a = k;
if (a < 0) {
a = -a;
reverse(code);
}
for (int i = 1; i <= a; i++) {
ret[0] += code[i];
}
int sum = ret[0];
for (int left = 1, right = a + 1; left < n; right++) {
if (right > n - 1) {
right = 0;
}
sum = sum - code[left] + code[right];
ret[left] = sum;
left++;
}
if (k < 0) {
reverse(ret);
}
return ret;
}
private void reverse(int[] code) {
int l = 0, r = code.length - 1;
while (l < r) {
int tmp = code[l];
code[l] = code[r];
code[r] = tmp;
l++;
r--;
}
}
}
🔤 无重复字符的最长子串:滑动窗口的经典应用
3. 无重复字符的最长子串
题目描述:
给定一个字符串
s
,请你找出其中不含有重复字符的 最长子串 的长度。
题目图片:
思路分析:
这道题同样可以使用 滑动窗口 来解决。我们维护一个窗口,窗口内的字符都是不重复的。当遇到重复字符时,我们需要移动窗口的左边界,直到窗口内没有重复字符为止。
解题过程:
-
使用哈希表记录字符出现次数: 由于字符串
s
由英文字母、数字、符号和空格组成,我们可以使用一个长度为 128 的数组map
来记录每个字符出现的次数。 -
滑动窗口:
- 维护一个左边界
left
和右边界right
,初始时都为 0。 - 每次迭代,将右边界
right
向右移动一位:- 将
s[right]
对应的字符在map
中的值加 1。 - 如果
map[s[right]] > 1
,说明出现了重复字符,需要移动左边界:- 不断将
s[left]
对应的字符在map
中的值减 1,并将left
加 1,直到map[s[right]] <= 1
为止。
- 不断将
- 更新最长无重复子串的长度
ret
:ret = Math.max(ret, right - left + 1)
。
- 将
- 维护一个左边界
复杂度分析:
- 时间复杂度:O(n),其中 n 是字符串
s
的长度。 - 空间复杂度:O(1),
map
数组的长度是固定的。
代码实现(Java):
class Solution {
public int lengthOfLongestSubstring(String ss) {
char[] s = ss.toCharArray();
int ret = 0, n = s.length;
int[] map = new int[128];
for (int left = 0, right = 0; right < n; right++) {
char in = s[right];
map[in]++;
while (map[in] > 1) {
map[s[left]]--;
left++;
}
// 窗口合法,更新结果
ret = Math.max(ret, right - left + 1);
}
return ret;
}
}
➕ 最大子数组和:Kadane 算法的魅力
53. 最大子数组和
题目描述:
给你一个整数数组
nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
题目图片:
思路分析:
这道题可以使用 Kadane 算法 来解决。Kadane 算法是一种动态规划算法,它的核心思想是:
- 维护两个变量:
curMax
(当前结束位置的最大子数组和)和globalMax
(全局最大子数组和)。 - 对于每个元素,我们有两种选择:
- 以当前元素作为新子数组的起点: 如果
nums[i]
比curMax + nums[i]
大,说明从当前元素开始新的子数组会得到更大的和。 - 将当前元素加入之前的子数组: 否则,将当前元素加入之前的子数组。
- 以当前元素作为新子数组的起点: 如果
复杂度分析:
- 时间复杂度:O(n),其中 n 是数组
nums
的长度。 - 空间复杂度:O(1)。
代码实现(Java):
class Solution {
public int maxSubArray(int[] nums) {
int curMax = nums[0]; // 当前最大值
int globalMax = nums[0]; // 全局最大值
for (int i = 1; i < nums.length; i++) {
// 对于每一个元素,要么新开一个子区间,要么加入已有的子区间
curMax = Math.max(nums[i], curMax + nums[i]);
// 更新全局最大值
globalMax = Math.max(globalMax, curMax);
}
return globalMax;
}
}
总结
今天的算法练习就到这里了。我们学习了如何使用 滑动窗口 解决 “拆炸弹” 和 “无重复字符的最长子串” 问题,以及如何使用 Kadane 算法 解决 “最大子数组和” 问题。希望大家有所收获!💪