栈与队列综合实战(二):最小栈设计、循环双端队列设计

好,咱们接着往下聊。上一章我们把栈和队列的基本操作捋了一遍,今天要玩点更实际的——两个经典的设计题:最小栈循环双端队列

说实话,这两个东西在面试里出现的频率极高。我当年去某大厂面试,第一轮手撕代码就是最小栈。当时我心想:这不就是个栈吗?加个变量记录最小值不就完了?结果一写就掉坑里了。嗯,咱们今天就把这些坑一个一个填平。

一、最小栈设计

1.1 问题本质

最小栈,说白了就是:一个普通的栈,加一个能O(1)返回最小值的功能

你可能会想:那我用一个变量 min 记录当前最小值,每次 push 的时候更新一下,不就行了?

问题来了——当最小值被 pop 出去之后,新的最小值是什么? 你没法知道。这就是核心难点。

核心需求:push、pop、top、getMin 四个操作,时间复杂度全部 O(1)。

1.2 我的解法:辅助栈

我个人习惯用双栈法。一个栈存数据,另一个栈存当前的最小值。

思路很简单:

  • push 时,数据栈正常压入;辅助栈压入当前最小值(即 min(当前值, 辅助栈栈顶))
  • pop 时,两个栈同时弹出
  • getMin 时,直接返回辅助栈栈顶

这样,两个栈的深度始终一致,pop 操作不会丢失最小值信息。

typedef struct {
    int *data;      // 数据栈
    int *minStack;  // 辅助最小值栈
    int top;        // 栈顶指针
    int capacity;   // 容量
} MinStack;

MinStack* minStackCreate(int capacity) {
    MinStack *obj = (MinStack*)malloc(sizeof(MinStack));
    obj->data = (int*)malloc(sizeof(int) * capacity);
    obj->minStack = (int*)malloc(sizeof(int) * capacity);
    obj->top = -1;
    obj->capacity = capacity;
    return obj;
}

void minStackPush(MinStack* obj, int val) {
    if (obj->top == obj->capacity - 1) return; // 栈满
    obj->top++;
    obj->data[obj->top] = val;
    // 辅助栈:压入当前最小值
    if (obj->top == 0) {
        obj->minStack[obj->top] = val;
    } else {
        int curMin = obj->minStack[obj->top - 1];
        obj->minStack[obj->top] = (val < curMin) ? val : curMin;
    }
}

void minStackPop(MinStack* obj) {
    if (obj->top == -1) return;
    obj->top--;
}

int minStackTop(MinStack* obj) {
    return obj->data[obj->top];
}

int minStackGetMin(MinStack* obj) {
    return obj->minStack[obj->top];
}

小技巧:如果你不想用两个栈,也可以用差值法。栈里存的是当前值与最小值的差值,通过数学运算还原。但可读性差,我一般不推荐。

1.3 避坑指南

我曾经在项目里用最小栈做任务优先级管理,结果出了个bug——辅助栈没同步pop。数据栈pop了,辅助栈没动,getMin返回的全是历史最小值。排查了半小时才发现。

所以记住:两个栈的push和pop必须成对出现,一个都不能少。

二、循环双端队列设计

2.1 什么是循环双端队列?

双端队列(Deque)就是两端都能插入和删除的队列。循环双端队列,就是底层用环形数组实现的双端队列。

你想想看,如果用链表实现双端队列,插入删除确实方便,但内存碎片和缓存不友好。用环形数组,空间连续、访问快、没有碎片,是嵌入式场景的首选。

2.2 核心设计要点

环形数组的关键就两个:头指针尾指针。我习惯用头指针指向第一个有效元素,尾指针指向最后一个有效元素的下一个位置

这样设计的好处是:空队列时 head == tail,满队列时 (tail + 1) % capacity == head。注意,这里会浪费一个元素空间来区分空和满。

typedef struct {
    int *arr;
    int head;       // 指向第一个有效元素
    int tail;       // 指向最后一个有效元素的下一个位置
    int capacity;   // 容量(实际可用 capacity - 1)
} MyCircularDeque;

MyCircularDeque* myCircularDequeCreate(int k) {
    MyCircularDeque *obj = (MyCircularDeque*)malloc(sizeof(MyCircularDeque));
    obj->arr = (int*)malloc(sizeof(int) * (k + 1)); // 多分配一个
    obj->head = 0;
    obj->tail = 0;
    obj->capacity = k + 1;
    return obj;
}

bool myCircularDequeInsertFront(MyCircularDeque* obj, int value) {
    if (myCircularDequeIsFull(obj)) return false;
    obj->head = (obj->head - 1 + obj->capacity) % obj->capacity;
    obj->arr[obj->head] = value;
    return true;
}

bool myCircularDequeInsertLast(MyCircularDeque* obj, int value) {
    if (myCircularDequeIsFull(obj)) return false;
    obj->arr[obj->tail] = value;
    obj->tail = (obj->tail + 1) % obj->capacity;
    return true;
}

bool myCircularDequeDeleteFront(MyCircularDeque* obj) {
    if (myCircularDequeIsEmpty(obj)) return false;
    obj->head = (obj->head + 1) % obj->capacity;
    return true;
}

bool myCircularDequeDeleteLast(MyCircularDeque* obj) {
    if (myCircularDequeIsEmpty(obj)) return false;
    obj->tail = (obj->tail - 1 + obj->capacity) % obj->capacity;
    return true;
}

int myCircularDequeGetFront(MyCircularDeque* obj) {
    if (myCircularDequeIsEmpty(obj)) return -1;
    return obj->arr[obj->head];
}

int myCircularDequeGetRear(MyCircularDeque* obj) {
    if (myCircularDequeIsEmpty(obj)) return -1;
    int rear = (obj->tail - 1 + obj->capacity) % obj->capacity;
    return obj->arr[rear];
}

bool myCircularDequeIsEmpty(MyCircularDeque* obj) {
    return obj->head == obj->tail;
}

bool myCircularDequeIsFull(MyCircularDeque* obj) {
    return (obj->tail + 1) % obj->capacity == obj->head;
}

注意:取模运算在C语言里对负数处理要小心。比如 (head - 1) % capacity,如果 head 是 0,结果可能是 -1。所以一定要写成 (head - 1 + capacity) % capacity。

三、知识体系总览

下面这张图把本章的核心逻辑串起来了。你看一眼,心里就有谱了。

栈与队列综合实战(二)核心结构 最小栈 核心思想: • 数据栈 + 辅助最小值栈 • 两个栈同步 push/pop • getMin 直接返回辅助栈顶 时间复杂度: • push/pop/top/getMin 均为 O(1) 空间复杂度:O(n) 循环双端队列 核心思想: • 环形数组实现 • head 指向第一个有效元素 • tail 指向最后一个的下一个 关键操作: • 头插/头删、尾插/尾删 • 判空:head == tail • 判满:(tail+1)%cap == head

四、总结与经验

这两个设计题,说白了就是考你对数据结构的理解深度。最小栈考的是「如何用空间换时间」,循环双端队列考的是「环形缓冲区的边界处理」。

我在实际项目中,最小栈用在实时任务调度器里,用来快速获取当前优先级最高的任务。循环双端队列则用在串口数据缓冲里,两端都能读写,非常灵活。

最后送你一句话:写代码容易,写对边界条件的代码难。多花点时间在空、满、边界这三种情况上,你的代码质量会上一个台阶。

核心要点回顾:

  • 最小栈:辅助栈同步,pop不丢最小值
  • 循环双端队列:环形数组,取模防负,浪费一个空间判满
  • 所有操作保持 O(1) 时间复杂度

公众号:蓝海资料掘金营,微信deep3321