每日算法-250404
记录今天完成的几道 LeetCode 算法题。
930. 和相同的二元子数组
题目
思路
滑动窗口
解题过程
这道题的目标是找到和恰好等于
goal
的子数组个数。我们可以利用滑动窗口,通过一个巧妙的转换来求解:和等于goal
的子数组数量 = (和小于等于goal
的子数组数量) - (和小于等于goal - 1
的子数组数量)。另一种思路是计算 (和大于等于
goal
的子数组数量) - (和大于等于goal + 1
的子数组数量),这正是代码中采用的方法。下面的
slidingWindow(nums, k)
函数用于计算和大于等于k
的子数组数量:
- 使用
right
指针遍历数组,扩展窗口右边界,累加sum
。- 使用
while
循环检查当前窗口和sum
是否大于等于k
(sum >= k
)。- 如果
sum >= k
,说明当前以right
为右边界的窗口和过大或正好,我们需要收缩左边界left
,直到sum < k
。- 在
while
循环收缩完毕后,left
指针的位置意味着从索引0
到left - 1
开始、以right
结尾的子数组,其和都曾经满足(或在收缩前满足)>= k
的条件。因此,有left
个这样的子数组。我们将left
累加到结果ret
中。最终,
slidingWindow(nums, goal) - slidingWindow(nums, goal + 1)
即为所求。
复杂度
- 时间复杂度:
O
(
n
)
O(n)
O(n),其中 n 是数组
nums
的长度。每个元素最多被left
和right
指针访问两次。 - 空间复杂度: O ( 1 ) O(1) O(1),只使用了常数级别的额外空间。
Code
class Solution {
public int numSubarraysWithSum(int[] nums, int goal) {
// 计算和 >= goal 的子数组个数
int a = slidingWindowAtLeastK(nums, goal);
// 计算和 >= goal + 1 的子数组个数
int b = slidingWindowAtLeastK(nums, goal + 1);
// 两者相减即为和 == goal 的子数组个数
return a - b;
}
// 计算数组 nums 中和大于等于 k 的子数组的数量
private int slidingWindowAtLeastK(int[] nums, int k) {
// 处理 k < 0 的情况,虽然本题 nums[i] >= 0, goal >= 0,但写健壮点没坏处
if (k < 0) {
k = 0; // 如果 k 是负数,没有意义,可以根据题目约束调整
}
int ret = 0, sum = 0;
for (int left = 0, right = 0; right < nums.length; right++) {
sum += nums[right];
// 当窗口和大于等于 k 时,收缩左边界
while (left <= right && sum >= k) {
// 注意:这里不能直接将 right - left + 1 加入结果
// 我们需要累加的是 left 的值,表示有多少个起点可以构成 >= k 的子数组
sum -= nums[left++];
}
// 此时 [left, right] 区间的和是 < k 的
// 但对于当前 right,所有以 0 到 left-1 为起点的子数组和都 >= k
// 所以累加 left
ret += left;
}
return ret;
}
}
2302. 统计得分小于 K 的子数组数目
题目
思路
滑动窗口
解题过程
题目要求统计满足
sum * length < k
的子数组数量。
- 我们使用
right
指针遍历数组,扩展窗口右边界,同时维护窗口内的元素和sum
以及窗口长度len
。- 使用
while
循环检查当前窗口[left, right]
是否满足条件sum * len < k
。- 如果
sum * len >= k
,说明当前窗口不满足条件,需要收缩左边界left
,同时更新sum
和len
,直到窗口满足条件为止。- 当
while
循环结束后,当前窗口[left, right]
是满足sum * len < k
的。由于子数组越短,得分通常越小(因为元素非负),所以所有以right
结尾,且起始位置在[left, right]
区间内的子数组,都满足条件。- 这样的子数组有
right - left + 1
个。将这个数量累加到结果ret
中。
复杂度
- 时间复杂度:
O
(
n
)
O(n)
O(n),其中 n 是数组
nums
的长度。每个元素最多被left
和right
指针访问两次。 - 空间复杂度:
O
(
1
)
O(1)
O(1),只使用了常数级别的额外空间(注意
sum
可能很大,使用long
类型)。
Code
class Solution {
public long countSubarrays(int[] nums, long k) {
long ret = 0;
long sum = 0;
// int len = 0; // 可以直接用 right - left + 1 计算长度
for (int left = 0, right = 0; right < nums.length; right++) {
sum += nums[right];
long len = right - left + 1; // 当前窗口长度
// 当窗口得分不满足条件时,收缩左边界
while (left <= right && sum * len >= k) {
sum -= nums[left++];
len = right - left + 1; // 更新长度
// len--; // 之前的写法也可以,但直接计算更清晰
}
// 此时窗口 [left, right] 满足条件 sum * len < k
// 所有以 right 结尾,起点在 [left, right] 内的子数组都满足
ret += (right - left + 1); // 或者 ret += len;
}
return ret;
}
}
3258. 统计满足 K 约束的子字符串数量 I
题目
思路
滑动窗口 + 计数数组
解题过程
题目要求统计满足“0 的数量不超过 k” 且 “1 的数量不超过 k” 的子字符串数量。
- 使用
right
指针遍历字符串,扩展窗口右边界。- 使用一个大小为 2 的数组
hash
来记录当前窗口内 ‘0’ 和 ‘1’ 的数量。hash[s[right] - '0']++
更新计数。- 使用
while
循环检查当前窗口[left, right]
是否满足约束条件。窗口不合法当hash[0] > k
或hash[1] > k
时。- 如果窗口不合法,收缩左边界
left
,并在hash
数组中减去s[left]
对应的计数 (hash[s[left++] - '0']--
),直到窗口重新满足约束条件。- 当
while
循环结束后,当前窗口[left, right]
是满足约束条件的(即hash[0] <= k
且hash[1] <= k
)。- 所有以
right
结尾,且起始位置在[left, right]
区间内的子字符串,都满足约束条件。- 这样的子字符串有
right - left + 1
个。将这个数量累加到结果ret
中。
复杂度
- 时间复杂度:
O
(
n
)
O(n)
O(n),其中 n 是字符串
s
的长度。每个字符最多被left
和right
指针访问两次。 - 空间复杂度:
O
(
1
)
O(1)
O(1),
hash
数组的大小是常数 2。
Code
class Solution {
public int countKConstraintSubstrings(String ss, int k) {
char[] s = ss.toCharArray();
int ret = 0;
int[] hash = new int[2];
for (int left = 0, right = 0; right < s.length; right++) {
hash[s[right] - '0']++;
while (hash[0] > k && hash[1] > k) {
hash[s[left++] - '0']--;
}
ret += (right - left + 1);
}
return ret;
}
}
641. 设计循环双端队列
题目
思路
使用数组模拟循环双端队列
解题过程
我们可以使用一个固定大小的数组来模拟循环双端队列的行为。
elem
: 用于存储队列元素的数组。length
: 队列的容量 (即数组大小k
)。size
: 队列中当前存储的元素数量。head
: 指向队首元素的索引。tail
: 指向队尾元素的下一个可用位置的索引。循环的关键:
使用模运算% length
来实现索引的循环。
- 尾部插入后,
tail = (tail + 1) % length
。- 头部删除后,
head = (head + 1) % length
。- 头部插入时,需要向前移动
head
,head = (head - 1 + length) % length
(加length
是为了处理head
为 0 时减 1 变成负数的情况)。- 尾部删除时,需要向前移动
tail
,tail = (tail - 1 + length) % length
。重要方法实现:
insertFront()
: 如果队列未满,计算新的head
索引(head - 1 + length) % length
,在该位置插入元素,并增加size
。insertLast()
: 如果队列未满,在当前的tail
索引处插入元素,更新tail
索引(tail + 1) % length
,并增加size
。deleteFront()
: 如果队列非空,更新head
索引(head + 1) % length
,并减少size
。deleteLast()
: 如果队列非空,更新tail
索引(tail - 1 + length) % length
,并减少size
。getFront()
: 如果队列非空,返回elem[head]
。getRear()
: 如果队列非空,返回队尾元素。队尾元素实际存储在tail
指针的前一个位置,即elem[(tail - 1 + length) % length]
。isEmpty()
: 检查size == 0
。isFull()
: 检查size == length
。
复杂度
- 时间复杂度: 构造方法和所有操作方法(插入、删除、获取、判断空/满)都是 O ( 1 ) O(1) O(1)。
- 空间复杂度: O ( k ) O(k) O(k),其中 k 是队列的容量,用于存储队列元素的数组。
Code
class MyCircularDeque {
private int[] elem; // 存储元素的数组
private int capacity; // 队列容量 (即 k)
private int size; // 当前元素数量
private int head; // 队首元素索引
private int tail; // 队尾元素下一个插入位置的索引
public MyCircularDeque(int k) {
capacity = k;
elem = new int[capacity];
size = 0;
head = 0; // 初始时 head 和 tail 可以在任意位置,只要它们的关系正确
tail = 0; // 通常 head=0, tail=0 表示空队列
}
public boolean insertFront(int value) {
if (isFull()) {
return false;
}
// 计算新的 head 位置,注意处理负数取模
head = (head - 1 + capacity) % capacity;
elem[head] = value;
size++;
return true;
}
public boolean insertLast(int value) {
if (isFull()) {
return false;
}
// 在当前 tail 位置插入
elem[tail] = value;
// 计算新的 tail 位置
tail = (tail + 1) % capacity;
size++;
return true;
}
public boolean deleteFront() {
if (isEmpty()) {
return false;
}
// head 后移一位
head = (head + 1) % capacity;
size--;
return true;
}
public boolean deleteLast() {
if (isEmpty()) {
return false;
}
// tail 前移一位
tail = (tail - 1 + capacity) % capacity;
size--;
return true;
}
public int getFront() {
if (isEmpty()) {
return -1;
}
return elem[head];
}
public int getRear() {
if (isEmpty()) {
return -1;
}
// 最后一个元素在 tail 的前一个位置
int lastElementIndex = (tail - 1 + capacity) % capacity;
return elem[lastElementIndex];
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == capacity;
}
}
/**
* Your MyCircularDeque object will be instantiated and called as such:
* MyCircularDeque obj = new MyCircularDeque(k);
* boolean param_1 = obj.insertFront(value);
* boolean param_2 = obj.insertLast(value);
* boolean param_3 = obj.deleteFront();
* boolean param_4 = obj.deleteLast();
* int param_5 = obj.getFront();
* int param_6 = obj.getRear();
* boolean param_7 = obj.isEmpty();
* boolean param_8 = obj.isFull();
*/