双生算法:栈与队列的时空博弈论

凌晨三点的硅谷,工程师Alex同时收到两条警报:游戏服务器因星号解析崩溃,支付系统因请求洪峰瘫痪。当他发现两个看似无关的故障竟能用同一套数据结构思想解决时,咖啡杯在半空凝固——原来算法世界存在着如此精妙的镜像对称...

正文

一、星号消除:栈的完美狩猎场

给定一个包含若干星号 * 的字符串 s,在一步操作中,可以选择一个星号,移除其左侧最近的非星号字符,并移除该星号自身。返回移除所有星号后的字符串。

问题本质:在字符序列中实现"就近匹配"的实时消除
核心挑战:处理动态变化的非连续匹配关系

算法哲学解析

这道题的核心在于找到每个星号左侧最近的非星号字符,并将其一并移除。通过栈结构,我们可以高效地处理这种需要动态维护上下文的场景。

  1. 栈的逆向时空观

    • 后进先出(LIFO)特性天然匹配"最近非星号字符"的消除逻辑

    • 时间复杂度:O(n) 线性遍历的极致优雅

    • 空间复杂度:O(n) 最坏情况的全栈存储

  2. 关键操作分解

    遍历字符序列:
      if(当前字符!='*') → 入栈    // 收集待命士兵
      else → 栈顶出栈           // 星号触发精确斩首
    最终栈序列=结果字符串         // 幸存者集结
  3. 现实映射
    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表示程序正常结束
}

输出结果:双生算法:栈与队列的时空博弈论_第1张图片


二、请求计数:队列的时空结界

实现一个 RecentCounter 类,用于计算特定时间范围内的请求数。每次调用 ping 方法时,会添加一个新请求,并返回过去 3000 毫秒内的请求数。

问题本质:在时间轴上维护动态滑动窗口
核心挑战:高效剔除过期元素同时统计窗口密度

算法哲学解析
这道题的核心在于高效地维护一个时间窗口内的请求记录,并能够快速查询窗口内的请求数。通过队列结构,我们可以动态维护请求的时间,并快速计算满足条件的请求数。

  1. 队列的时空结界

    • 先进先出(FIFO)特性完美匹配时间流逝方向

    • 时间复杂度:均摊O(1)的神奇效率

    • 空间复杂度:O(n) 窗口最大容量

  2. 关键操作分解

    初始化空队列
    ping(t)操作:
      元素t入队尾       // 新事件注册
      while(队首 
  3. 现实映射
    金融交易系统中的实时风控窗口、物联网设备的时序数据处理

示例精析
示例 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;  // 程序正常退出
}

输出结果:双生算法:栈与队列的时空博弈论_第2张图片


三、双生算法的镜像宇宙

核心对比矩阵

维度 星号消除(栈) 请求计数(队列)
数据结构 LIFO(后进先出) FIFO(先进先出)
操作核心 就近匹配 过期清理
时间复杂度 O(n) 单次遍历 均摊O(1) 魔幻效率
空间边界 最坏O(n) 窗口最大O(3000)
触发机制 遇到*立即执行 新元素触发延迟执行
现实隐喻 俄罗斯方块消除 沙漏流沙计时

哲学对话

  1. 时间观的终极对决

    • 栈:时间倒流者,始终关注最新事件

    • 队列:时间守护者,严格遵循线性流逝

  2. 空间管理的艺术

    • 栈:空间消耗与输入规模强相关

    • 队列:空间消耗由时间窗口决定

  3. 操作触发范式

    • 星号消除:事件驱动型(星号即命令)

    • 请求计数:数据驱动型(新请求即心跳)

当我们将两个算法并置观察,发现它们竟构成完美的阴阳太极图:栈处理空间中的最近关系,队列处理时间中的最近关系——这正是计算机科学中时空一致性的绝妙体现。

三、总结与思考

通过对比这两个题目,我们可以发现,算法题的解法往往需要根据问题的具体要求灵活调整。栈和队列是两种常见的数据结构,它们在不同的场景下各有优劣。

  •  适用于需要动态维护上下文的场景,如字符串处理。
  • 队列 适用于需要维护时间窗口的场景,如请求计数。

作为开发者,我们需要根据具体问题的特点选择合适的数据结构,才能写出高效、简洁且易于维护的代码。


博客谢言:

希望通过今天的分享,你对栈和队列的应用有了更深入的理解。如果你有任何问题或想法,欢迎在评论区留言,我们一起探讨!下次见!

你可能感兴趣的:(算法,java,开发语言,职场和发展,生活,哈希算法)