凌晨三点的硅谷,工程师Alex同时收到两条警报:游戏服务器因星号解析崩溃,支付系统因请求洪峰瘫痪。当他发现两个看似无关的故障竟能用同一套数据结构思想解决时,咖啡杯在半空凝固——原来算法世界存在着如此精妙的镜像对称...
给定一个包含若干星号 *
的字符串 s
,在一步操作中,可以选择一个星号,移除其左侧最近的非星号字符,并移除该星号自身。返回移除所有星号后的字符串。
问题本质:在字符序列中实现"就近匹配"的实时消除
核心挑战:处理动态变化的非连续匹配关系
算法哲学解析:
这道题的核心在于找到每个星号左侧最近的非星号字符,并将其一并移除。通过栈结构,我们可以高效地处理这种需要动态维护上下文的场景。
栈的逆向时空观:
后进先出(LIFO)特性天然匹配"最近非星号字符"的消除逻辑
时间复杂度:O(n) 线性遍历的极致优雅
空间复杂度:O(n) 最坏情况的全栈存储
关键操作分解:
遍历字符序列: if(当前字符!='*') → 入栈 // 收集待命士兵 else → 栈顶出栈 // 星号触发精确斩首 最终栈序列=结果字符串 // 幸存者集结
现实映射:
DNA序列编辑中的碱基对消除、编译器语法树构建中的括号匹配
示例精析:
示例 1:
输入:s = "leet**cod*e"
输出:"lecoe"
解释:从左到右执行移除操作:
- 距离第 1 个星号最近的字符是 "leet**cod*e" 中的 't' ,s 变为 "lee*cod*e" 。
- 距离第 2 个星号最近的字符是 "lee*cod*e" 中的 'e' ,s 变为 "lecod*e" 。
- 距离第 3 个星号最近的字符是 "lecod*e" 中的 'd' ,s 变为 "lecoe" 。
不存在其他星号,返回 "lecoe" 。
示例 2:
输入:s = "erase*****"
输出:""
解释:整个字符串都会被移除,所以返回空字符串。
#include // 标准输入输出库,用于printf、fgets等函数
#include // 标准库,用于malloc、free等内存管理函数
#include // 字符串处理库,用于strlen、strcpy等函数
// 定义函数removeStars,接收一个字符指针参数s,返回处理后的字符串指针
char* removeStars(char* s) {
int len = strlen(s); // 计算输入字符串s的长度
// 分配栈空间(大小与输入相同),多分配1个字节用于字符串结束符'\0'
char* stack = (char*)malloc(sizeof(char) * (len + 1));
int top = -1; // 初始化栈顶指针为-1,表示栈为空
// 遍历字符串s的每一个字符
for (int i = 0; i < len; i++) {
if (s[i] != '*') { // 如果当前字符不是星号'*'
// 非星号字符:压入栈中,栈顶指针先加1再赋值
stack[++top] = s[i];
} else if (top >= 0) { // 如果当前字符是星号'*'且栈不为空
// 星号字符:弹出左侧最近的非星号字符,栈顶指针减1
top--;
}
}
// 在栈顶指针的下一个位置设置字符串结束符'\0'
stack[top + 1] = '\0';
// 优化内存:重新分配精确大小的空间,大小为栈中剩余字符数+1('\0')
char* result = (char*)malloc(sizeof(char) * (top + 2));
if (result) { // 如果内存分配成功
strcpy(result, stack); // 将栈中的内容复制到result中
}
free(stack); // 释放临时栈空间
// 返回结果指针,如果result为NULL则返回空字符串的副本
return result ? result : strdup("");
}
// 主函数,程序入口
int main() {
char s[100001]; // 定义字符数组s,最大支持100000个字符+1个结束符
printf("Enter a string: "); // 打印提示信息
fgets(s, sizeof(s), stdin); // 从标准输入读取一行字符串到s中
// 移除可能的换行符:检查字符串末尾是否有换行符
size_t len = strlen(s); // 计算字符串s的长度
if (len > 0 && s[len - 1] == '\n') { // 如果长度大于0且最后一个字符是换行符
s[len - 1] = '\0'; // 将换行符替换为字符串结束符
}
char* result = removeStars(s); // 调用removeStars函数处理字符串s
printf("Result: %s\n", result); // 打印处理后的结果
free(result); // 释放result指针指向的内存
return 0; // 返回0表示程序正常结束
}
实现一个 RecentCounter
类,用于计算特定时间范围内的请求数。每次调用 ping
方法时,会添加一个新请求,并返回过去 3000 毫秒内的请求数。
问题本质:在时间轴上维护动态滑动窗口
核心挑战:高效剔除过期元素同时统计窗口密度
算法哲学解析::
这道题的核心在于高效地维护一个时间窗口内的请求记录,并能够快速查询窗口内的请求数。通过队列结构,我们可以动态维护请求的时间,并快速计算满足条件的请求数。
队列的时空结界:
先进先出(FIFO)特性完美匹配时间流逝方向
时间复杂度:均摊O(1)的神奇效率
空间复杂度:O(n) 窗口最大容量
关键操作分解:
初始化空队列 ping(t)操作: 元素t入队尾 // 新事件注册 while(队首
现实映射:
金融交易系统中的实时风控窗口、物联网设备的时序数据处理
示例精析:
示例 1:
输入:
["RecentCounter", "ping", "ping", "ping", "ping"]
[[], [1], [100], [3001], [3002]]
输出:
[null, 1, 2, 3, 3]
解释:
RecentCounter recentCounter = new RecentCounter();
recentCounter.ping(1); // requests = [1],范围是 [-2999,1],返回 1
recentCounter.ping(100); // requests = [1, 100],范围是 [-2900,100],返回 2
recentCounter.ping(3001); // requests = [1, 100, 3001],范围是 [1,3001],返回 3
recentCounter.ping(3002); // requests = [1, 100, 3001, 3002],范围是 [2,3002],返回 3
#include // 标准输入输出库,用于printf等函数
#include // 标准库,用于malloc、free等内存管理函数
// 定义队列结构,用于存储时间请求
typedef struct {
int* data; // 存储请求时间的动态数组指针
int front; // 队列头指针,指向第一个有效元素
int rear; // 队列尾指针,指向下一个插入位置
int capacity; // 队列总容量
} RecentCounter; // 结构体类型名
// 创建并初始化RecentCounter实例
RecentCounter* recentCounterCreate() {
// 分配RecentCounter结构体内存
RecentCounter* obj = (RecentCounter*)malloc(sizeof(RecentCounter));
if (!obj) return NULL; // 内存分配失败检查
obj->capacity = 10001; // 设置队列容量(题目最大调用次数10000+1)
// 分配存储请求时间的数组内存
obj->data = (int*)malloc(obj->capacity * sizeof(int));
if (!obj->data) { // 内存分配失败检查
free(obj); // 释放已分配的结构体
return NULL;
}
obj->front = 0; // 初始化队列头指针
obj->rear = 0; // 初始化队列尾指针
return obj; // 返回初始化好的实例
}
// 处理新请求并返回最近3000ms内的请求数
int recentCounterPing(RecentCounter* obj, int t) {
// 将新请求时间t存入队列尾部
obj->data[obj->rear] = t;
// 尾指针后移(循环队列,使用取模运算)
obj->rear = (obj->rear + 1) % obj->capacity;
// 移除所有超过3000ms的旧请求(队列头元素时间 < t-3000)
while (obj->data[obj->front] < t - 3000) {
// 头指针后移(循环队列,使用取模运算)
obj->front = (obj->front + 1) % obj->capacity;
}
// 计算并返回当前有效请求数(分两种情况)
if (obj->rear >= obj->front) {
// 情况1:尾指针在头指针后面,直接相减
return obj->rear - obj->front;
}
// 情况2:尾指针绕回头指针前面,计算两段长度之和
return obj->capacity - obj->front + obj->rear;
}
// 释放RecentCounter实例及其资源
void recentCounterFree(RecentCounter* obj) {
if (obj) { // 检查指针有效性
free(obj->data); // 先释放数据数组内存
free(obj); // 再释放结构体内存
}
}
// 主函数:测试RecentCounter功能
int main() {
// 创建计数器实例
RecentCounter* counter = recentCounterCreate();
// 定义测试用例数组
int test_cases[] = {1, 100, 3001, 3002};
// 计算测试用例数量
int test_size = sizeof(test_cases) / sizeof(test_cases[0]);
// 开始输出测试结果
printf("Output: [null"); // 初始null表示创建操作
for (int i = 0; i < test_size; i++) {
// 处理每个测试用例并获取结果
int result = recentCounterPing(counter, test_cases[i]);
printf(", %d", result); // 输出每个ping的结果
}
printf("]\n"); // 输出结束
// 释放计数器资源
recentCounterFree(counter);
return 0; // 程序正常退出
}
核心对比矩阵:
维度 | 星号消除(栈) | 请求计数(队列) |
---|---|---|
数据结构 | LIFO(后进先出) | FIFO(先进先出) |
操作核心 | 就近匹配 | 过期清理 |
时间复杂度 | O(n) 单次遍历 | 均摊O(1) 魔幻效率 |
空间边界 | 最坏O(n) | 窗口最大O(3000) |
触发机制 | 遇到*立即执行 | 新元素触发延迟执行 |
现实隐喻 | 俄罗斯方块消除 | 沙漏流沙计时 |
哲学对话:
时间观的终极对决:
栈:时间倒流者,始终关注最新事件
队列:时间守护者,严格遵循线性流逝
空间管理的艺术:
栈:空间消耗与输入规模强相关
队列:空间消耗由时间窗口决定
操作触发范式:
星号消除:事件驱动型(星号即命令)
请求计数:数据驱动型(新请求即心跳)
当我们将两个算法并置观察,发现它们竟构成完美的阴阳太极图:栈处理空间中的最近关系,队列处理时间中的最近关系——这正是计算机科学中时空一致性的绝妙体现。
通过对比这两个题目,我们可以发现,算法题的解法往往需要根据问题的具体要求灵活调整。栈和队列是两种常见的数据结构,它们在不同的场景下各有优劣。
作为开发者,我们需要根据具体问题的特点选择合适的数据结构,才能写出高效、简洁且易于维护的代码。
希望通过今天的分享,你对栈和队列的应用有了更深入的理解。如果你有任何问题或想法,欢迎在评论区留言,我们一起探讨!下次见!