每日算法-250403
记录今天完成的几道 LeetCode 算法题。
713. 乘积小于 K 的子数组
题目
思路
滑动窗口
解题过程
我们可以使用滑动窗口来解决这个问题。维护一个窗口
[left, right]
,并记录窗口内元素的乘积sum
。
- 初始化
left = 0
,right = 0
,sum = 1
,ret = 0
。right
指针向右遍历数组nums
。- 每次
right
移动,将nums[right]
乘入sum
。- 使用
while
循环检查sum
是否大于等于k
。如果sum >= k
,说明当前窗口不合法(乘积过大),需要缩小窗口。将nums[left]
从sum
中除掉,并将left
指针右移 (left++
)。持续这个过程直到sum < k
或者left > right
。- 当
sum < k
时,以right
结尾的所有子数组(从nums[left..right]
到nums[right..right]
)的乘积都小于k
。这些子数组的数量等于当前窗口的长度right - left + 1
。将这个数量累加到结果ret
中。right
指针继续移动,重复步骤 3-5,直到right
到达数组末尾。注意:
while
循环的条件left <= right
确保了即使单个元素nums[right]
就大于等于k
,left
也能正确地移动到right + 1
,使得窗口长度为 0,不会错误地增加计数。
复杂度
- 时间复杂度:
O
(
n
)
O(n)
O(n),其中
n
n
n 是数组
nums
的长度。left
和right
指针都最多移动 n n n 次。 - 空间复杂度: O ( 1 ) O(1) O(1),只使用了常数级别的额外空间。
Code
class Solution {
public int numSubarrayProductLessThanK(int[] nums, int k) {
// 处理 k <= 1 的情况,因为数组元素都是正数,乘积不可能小于等于 1(除非是空数组,但题目约束 nums.length >= 1)
// 如果 k <= 1,任何正整数的乘积都不可能小于 k,所以结果为 0。
if (k <= 1) {
return 0;
}
int ret = 0, sum = 1;
for (int left = 0, right = 0; right < nums.length; right++) {
sum *= nums[right];
// 当 sum >= k 时,需要缩小窗口
while (sum >= k) {
// 注意:因为上面已经判断 k > 1 且 nums[i] >= 1,所以 sum 不会是 0,可以安全地做除法
// 同时,因为 sum >= k >= nums[left],所以 left 不会超过 right
sum /= nums[left++];
}
// 此时窗口 [left, right] 内所有元素的乘积 < k
// 以 right 结尾的有效子数组有 right - left + 1 个
ret += (right - left + 1);
}
return ret;
}
}
622. 设计循环队列
题目
思路
使用数组模拟循环队列。
解题过程
- 使用一个固定大小的数组
elem
来存储队列元素。length
记录数组容量(即队列容量k
)。size
记录当前队列中的元素个数。head
指向队首元素的索引。tail
指向下一个待插入位置的索引。- 循环的关键:当
head
或tail
需要移动时,使用取模运算% length
来实现循环。例如,入队后tail = (tail + 1) % length
,出队后head = (head + 1) % length
。- 判空/判满:队列为空的条件是
size == 0
;队列已满的条件是size == length
。- 获取队尾元素
Rear()
:队尾元素实际存储在tail
指向位置的前一个索引。为了正确处理tail
为 0 时(此时逻辑上的队尾在数组末尾length - 1
处)的情况,使用(tail - 1 + length) % length
来计算队尾元素的实际索引。
复杂度
- 时间复杂度: 初始化和各个方法(
enQueue
,deQueue
,Front
,Rear
,isEmpty
,isFull
)都是 O ( 1 ) O(1) O(1)。 - 空间复杂度: O ( k ) O(k) O(k),需要一个大小为 k k k 的数组来存储队列元素。
Code
class MyCircularQueue {
private int[] elem; // 存储元素的数组
private int head; // 队首指针(索引)
private int tail; // 队尾指针(下一个要插入的位置的索引)
private int size; // 当前队列中的元素数量
private int length; // 队列容量
public MyCircularQueue(int k) {
length = k;
elem = new int[length];
head = 0;
tail = 0;
size = 0;
}
// 入队操作
public boolean enQueue(int value) {
if (isFull()) { // 队列已满,无法入队
return false;
}
elem[tail] = value; // 在队尾指针处放入元素
tail = (tail + 1) % length; // 队尾指针后移(循环)
size++; // 元素数量增加
return true;
}
// 出队操作
public boolean deQueue() {
if (isEmpty()) { // 队列为空,无法出队
return false;
}
// 队首元素不需要显式移除,只需移动 head 指针
head = (head + 1) % length; // 队首指针后移(循环)
size--; // 元素数量减少
return true;
}
// 获取队首元素
public int Front() {
if (isEmpty()) {
return -1; // 队列为空
}
return elem[head]; // 返回队首指针处的元素
}
// 获取队尾元素
public int Rear() {
if (isEmpty()) {
return -1; // 队列为空
}
// 队尾元素在 tail 指针的前一个位置
// 需要处理 tail = 0 的情况,此时队尾在数组末尾
int rearIndex = (tail - 1 + length) % length;
return elem[rearIndex];
}
// 检查队列是否为空
public boolean isEmpty() {
return size == 0;
}
// 检查队列是否已满
public boolean isFull() {
return size == length;
}
}
/**
* Your MyCircularQueue object will be instantiated and called as such:
* MyCircularQueue obj = new MyCircularQueue(k);
* boolean param_1 = obj.enQueue(value);
* boolean param_2 = obj.deQueue();
* int param_3 = obj.Front();
* int param_4 = obj.Rear();
* boolean param_5 = obj.isEmpty();
* boolean param_6 = obj.isFull();
*/
2904. 最短且字典序最小的美丽子字符串 (复习)
题目
思路回顾
这次复习完美解决了问题。核心思路仍然是滑动窗口,找到所有包含
k
个 ‘1’ 的子串,然后在这些子串中比较长度和字典序。详细的题解请看之前的博文:每日算法-250329
代码
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;
}
}
1234. 替换子串得到平衡字符串 (复习)
题目
思路回顾
这次复习时遇到了一个小问题:忘记处理输入字符串本身就已经平衡的情况(即每个字符出现次数都等于
n/4
)。如果一开始就平衡,应该直接返回 0。不处理这个边界情况,会导致后续滑动窗口的while
循环条件一直满足(因为所有字符的计数都<= m
),使得left
指针不断自增,最终可能导致数组越界。核心思路是滑动窗口,目标是找到一个最短的子串,替换掉这个子串后,原字符串中剩余部分的 Q, W, E, R 字符数量都不超过
n/4
。这等价于找到一个最短的窗口[left, right]
,使得窗口外的字符计数满足平衡条件。详细的题解请看之前的博文:每日算法-250330
代码
class Solution {
public int balancedString(String ss) {
char[] s = ss.toCharArray();
int n = s.length;
int ret = n + 1, m = n / 4;
int[] hash = new int[128];
for (char c : s) {
hash[c]++;
}
if (hash['Q'] == m && hash['W'] == m && hash['E'] == m && hash['R'] == m) {
return 0;
}
for (int left = 0, right = 0; right < n; right++) {
hash[s[right]]--;
while (hash['Q'] <= m && hash['W'] <= m && hash['E'] <= m && hash['R'] <= m) {
ret = Math.min(ret, right - left + 1);
hash[s[left++]]++;
}
}
return ret;
}
}