栈数据结构详解:从原理到C语言实现与核心应用场景
2026/8/22 6:12:05 网站建设 项目流程

1. 从“叠盘子”到程序世界:栈的直观理解

如果你在餐厅后厨打过工,或者在家里收拾过碗柜,那你对“栈”这个概念一定不陌生。想象一下,你刚洗好一摞盘子,是不是总是从下往上一个一个地摞起来?当厨师需要盘子时,他是不是总是从最上面直接拿走一个?他绝不会从中间或者最底下抽一个出来,因为那样整个摞好的盘子可能会倒掉。这个“后进先出”的取放规则,就是栈(Stack)这种数据结构最核心、最生动的体现。

在计算机科学里,栈是一个极其基础且重要的概念。它就像程序世界里的“临时工作台”,专门用来存放那些需要暂时保存、并且使用顺序有严格要求的“中间数据”。比如,你在写一个函数,函数里又调用了另一个函数,那么第一个函数的执行现场(比如变量值、返回地址)就需要被“压”入栈中保存起来,等第二个函数执行完毕,再把这些现场信息从栈顶“弹”出来,恢复第一个函数的执行。这个过程,就是著名的“函数调用栈”。几乎所有编程语言,从C、Java到Python,其底层运行机制都离不开栈的支持。

所以,当我们谈论栈的“压栈/入栈”(Push)和“弹栈/出栈”(Pop)时,本质上就是在描述这个“临时工作台”的两种基本操作:往最上面放一个东西,或者从最上面拿走一个东西。而“获取栈顶元素”(Peek/Top)则只是看一眼最上面是什么,并不把它拿走。这三个操作,构成了栈最核心的API。理解它们,不仅是学习数据结构的敲门砖,更是理解程序如何运行、如何管理内存的钥匙。无论你是正在备战期末考试的学生,还是希望夯实基础的开发者,彻底搞懂栈的来龙去脉,都至关重要。

2. 栈的抽象定义与核心特性

在正式动手操作之前,我们必须从抽象层面把栈的定义和规矩讲清楚。栈是一种操作受限的线性表。所谓“线性表”,就是数据元素排成像一条线一样的序列,比如我们熟悉的数组和链表。而“操作受限”,指的是这种结构只允许在一端进行插入和删除操作。

这一端被称为栈顶(Top)。相应地,另一端则被称为栈底(Bottom)。这个设计决定了栈独一无二的行为特性:后进先出(Last In, First Out, LIFO)。最后被放入栈的元素,会最先被取出来。这和我们日常的电梯有点类似(假设电梯只有一扇门):最后进电梯的人,往往在电梯到达时最先出去。

栈支持的基本操作通常包括:

  1. 初始化:创建一个空栈。
  2. 判断栈空:检查栈中是否没有任何元素。
  3. 入栈:将一个新元素放入栈顶。
  4. 出栈:将栈顶元素移除,并返回这个元素。
  5. 获取栈顶元素:仅返回栈顶元素的值,但不移除它。
  6. 获取栈大小:返回栈中当前元素的个数。

这里需要特别强调“获取栈顶元素”和“出栈”的区别,这是初学者容易混淆的地方。PeekTop操作只是“读取”,栈的状态没有任何改变,元素数量不变。而Pop操作是“读取并删除”,执行后栈顶元素被移除,栈的大小减一。用一个生活类比:Peek就像你看一眼桌上最上面一本书的名字;Pop则是你把那本书拿起来读,并且从书堆里移走。

栈的底层实现通常有两种:顺序栈(基于数组)和链式栈(基于链表)。顺序栈就像预定好大小的储物格,存取速度快,但容量固定。链式栈则像用绳子串起来的储物盒,可以动态增长,但每个元素需要额外的空间来存储“绳子”(即指针)。选择哪种实现,取决于你对性能和内存灵活性的权衡。

3. 顺序栈的实现与操作细节

顺序栈是使用数组(或类似数组的连续内存空间)来实现的。我们需要维护一个数组data[]来存储元素,一个整型变量top来指示栈顶的位置。

3.1 栈的初始化与状态判断

初始化时,我们分配一个固定大小的数组,并将top指针设置为-1(这是一种常见的约定,表示栈为空)。为什么是-1而不是0?因为数组索引从0开始。当top == -1时,栈空;当top == arraySize - 1时,栈满。

#define MAX_SIZE 100 // 预定义栈的最大容量 typedef struct { int data[MAX_SIZE]; int top; // 栈顶指针 } SeqStack; // 初始化栈 void InitStack(SeqStack *S) { S->top = -1; // 栈空标志 } // 判断栈是否为空 int IsEmpty(SeqStack *S) { return S->top == -1; } // 判断栈是否已满 int IsFull(SeqStack *S) { return S->top == MAX_SIZE - 1; }

注意top指针指向的是当前栈顶元素的位置top = -1代表空栈,top = 0代表栈中有一个元素,它存储在data[0]。这种设计使得入栈和出栈的操作非常直观。

3.2 入栈操作:边界检查与数据写入

入栈操作,核心就是两步:先检查栈是否已满,然后将元素放入top指针的下一个位置,并更新top

// 入栈操作 int Push(SeqStack *S, int value) { if (IsFull(S)) { printf("栈已满,无法入栈!\n"); return 0; // 入栈失败 } S->top++; // 栈顶指针先上移 S->data[S->top] = value; // 新元素放入栈顶 return 1; // 入栈成功 }

这里有一个关键细节:为什么是先S->top++再赋值?因为top指向的是当前栈顶元素的位置。对于空栈(top=-1),要放入第一个元素,我们必须先将指针移动到有效索引0的位置,然后再存放数据。这个顺序不能颠倒,否则你会尝试向data[-1]写入数据,导致内存错误。

3.3 出栈操作:状态检查与数据返回

出栈是入栈的逆过程:先检查栈是否为空,然后取出top指针当前位置的元素,最后将top指针下移。

// 出栈操作 int Pop(SeqStack *S, int *value) { if (IsEmpty(S)) { printf("栈为空,无法出栈!\n"); return 0; // 出栈失败 } *value = S->data[S->top]; // 获取栈顶元素值 S->top--; // 栈顶指针下移 return 1; // 出栈成功 }

实操心得:出栈时,从逻辑上讲,元素已经被“移除”了,但物理上它可能还残留在数组data[S->top+1]的位置。不过因为top指针已经下移,这个位置被标记为“可覆盖”,下次入栈时就会被新数据覆盖。所以,我们不需要(也不应该)手动去清空那个位置的数据,那是一种无意义的性能损耗。

3.4 获取栈顶元素:只读不删

这个操作最简单,但也最容易出错。它必须检查栈是否为空,然后直接返回data[top]的值,绝对不修改top指针

// 获取栈顶元素 int GetTop(SeqStack *S, int *value) { if (IsEmpty(S)) { printf("栈为空,无栈顶元素!\n"); return 0; } *value = S->data[S->top]; return 1; }

我曾经在初学时就犯过一个错误:在GetTop函数里习惯性地写了S->top--,导致后续操作全部错乱。记住,PeekPop是兄弟,但性格迥异,一个温和只读,一个霸道读写。

4. 链式栈的实现与内存考量

当栈的最大容量无法预估,或者需要频繁动态变化时,顺序栈的固定数组就显得力不从心了。这时,链式栈是更好的选择。链式栈的本质就是一个单链表,只不过我们规定所有的插入(入栈)和删除(出栈)操作都只能在链表的头部进行。链表的头指针就扮演了top指针的角色。

4.1 节点定义与栈的初始化

typedef struct StackNode { int data; struct StackNode *next; } StackNode; typedef struct { StackNode *top; // 栈顶指针,指向链表头节点 int size; // 栈的长度,非必需但很有用 } LinkStack; // 初始化链栈 void InitLinkStack(LinkStack *S) { S->top = NULL; S->size = 0; } // 判断链栈是否为空 int IsLinkStackEmpty(LinkStack *S) { return S->top == NULL; // 或者 S->size == 0 }

链式栈的“空”状态就是top指针为NULL,非常直观。

4.2 链式栈的入栈:头插法

链式栈的入栈,对应的是单链表的头插法。创建一个新节点,让其指向当前的头节点(即当前的栈顶),然后更新栈顶指针指向这个新节点。

// 链式栈入栈 int LinkPush(LinkStack *S, int value) { StackNode *newNode = (StackNode*)malloc(sizeof(StackNode)); if (newNode == NULL) { printf("内存分配失败!\n"); return 0; } newNode->data = value; newNode->next = S->top; // 新节点指向原栈顶 S->top = newNode; // 栈顶指针更新为新节点 S->size++; return 1; }

这个过程就像给一列火车加一个新的车头。新节点newNode就是新车头,它的next挂钩挂上原来的火车头(S->top),然后整个火车的标识(S->top)指向这个新车头。

4.3 链式栈的出栈:释放头节点

出栈操作就是删除单链表的头节点。需要先保存头节点的数据和下一个节点的地址,然后释放头节点内存,最后更新栈顶指针。

// 链式栈出栈 int LinkPop(LinkStack *S, int *value) { if (IsLinkStackEmpty(S)) { printf("栈为空,无法出栈!\n"); return 0; } StackNode *temp = S->top; // 临时保存待删除的栈顶节点 *value = temp->data; // 获取栈顶数据 S->top = temp->next; // 栈顶指针指向下一个节点 free(temp); // 释放原栈顶节点内存 S->size--; return 1; }

重要注意事项:这里有一个经典的内存管理坑。一定要先用一个临时指针temp保存S->top,然后再更新S->top = S->top->next。如果先更新S->top,你就丢失了原来栈顶节点的地址,无法再free它,导致内存泄漏。顺序很重要!

4.4 链式栈的获取栈顶与销毁

获取栈顶元素和顺序栈逻辑一致,只是访问方式不同。而链式栈由于使用了动态内存,必须提供一个额外的Destroy函数来遍历释放所有节点内存,防止内存泄漏。

// 获取链式栈栈顶元素 int GetLinkTop(LinkStack *S, int *value) { if (IsLinkStackEmpty(S)) { printf("栈为空!\n"); return 0; } *value = S->top->data; return 1; } // 销毁链式栈 void DestroyLinkStack(LinkStack *S) { while (S->top != NULL) { StackNode *temp = S->top; S->top = S->top->next; free(temp); } S->size = 0; }

5. 栈的核心应用场景剖析

理解了栈的基本操作,我们来看看它到底能干什么。栈的应用无处不在,下面几个是其中最经典、面试最高频的场景。

5.1 场景一:函数调用栈与递归

这是栈最根本的应用。每次函数调用,系统都会在栈上分配一块内存,称为“栈帧”,用来保存函数的参数、局部变量、返回地址等信息。当函数调用另一个函数时,当前函数的栈帧被压入栈中暂停;新函数开始执行,创建自己的栈帧。当新函数返回时,其栈帧出栈,系统根据之前保存的返回地址,回到上一个函数继续执行。

递归函数是这种机制的极致体现。递归的每一层,都对应一个独立的栈帧。以计算阶乘factorial(n)为例:

factorial(3) 调用 factorial(2) -> factorial(2) 调用 factorial(1) -> factorial(1) 返回 1 <- factorial(2) 收到1,计算 2*1=2 返回 <- factorial(3) 收到2,计算 3*2=6 返回

这个过程就像一叠便签,最上面是当前正在处理的任务(factorial(1)),下面压着所有未完成的上层任务。递归深度过深导致的“栈溢出”错误,就是因为栈帧数量超过了系统为线程预留的栈空间大小。

5.2 场景二:表达式求值与括号匹配

编译器如何计算(1 + 2) * (3 - 4)这样的表达式?它需要两个栈:一个操作数栈,一个运算符栈。

  1. 从左到右扫描表达式。
  2. 遇到数字,压入操作数栈。
  3. 遇到运算符,与运算符栈顶的运算符比较优先级。
    • 如果当前运算符优先级更高,直接压入运算符栈。
    • 如果更低或相等,则从运算符栈弹出栈顶运算符,从操作数栈弹出两个操作数进行计算,将结果压回操作数栈,然后继续比较当前运算符与新的栈顶运算符。
  4. 遇到左括号(,直接压入运算符栈。
  5. 遇到右括号),不断弹出运算符栈顶的运算符并计算,直到弹出左括号为止。
  6. 表达式扫描完后,清空运算符栈,进行剩余计算。

括号匹配是表达式求值的一个子问题。算法更简单:遍历字符串,遇到左括号(, [, {就入栈;遇到右括号) , ], }就检查栈顶的左括号是否与之匹配,如果匹配则出栈,否则说明不匹配。最后,如果栈为空,则括号完全匹配。

5.3 场景三:浏览器的前进与后退

这个场景完美体现了栈的LIFO特性。我们使用两个栈:Stack AStack B

  • 访问新页面:将新页面URL压入Stack A,同时清空Stack B(因为有了新的浏览路径,旧的后退记录失效)。
  • 点击后退:将Stack A的栈顶页面出栈,并压入Stack B。然后显示Stack A新的栈顶页面。
  • 点击前进:将Stack B的栈顶页面出栈,并压入Stack A。然后显示Stack A新的栈顶页面(即刚出栈的那个)。

Stack A可以看作是“已访问页面的历史栈”,栈顶是当前页面。Stack B是“后退栈”,存放着从Stack A中后退出去的页面。这个双栈模型清晰、高效地管理了线性的浏览历史。

5.4 场景四:深度优先搜索与回溯算法

在图和树的遍历中,深度优先搜索(DFS)天然地使用栈来记录访问路径。以二叉树的前序遍历为例(递归版本隐式使用了系统调用栈):

// 非递归前序遍历,显式使用栈 void preOrderTraversal(TreeNode* root) { if (root == NULL) return; SeqStack S; InitStack(&S); Push(&S, root); // 根节点入栈 while (!IsEmpty(&S)) { TreeNode* node; Pop(&S, &node); // 出栈访问 printf("%d ", node->val); // 注意:右孩子先入栈,左孩子后入栈,这样才能保证出栈顺序是根->左->右 if (node->right != NULL) Push(&S, node->right); if (node->left != NULL) Push(&S, node->left); } }

在回溯算法(如八皇后、迷宫问题)中,栈用来保存当前的尝试路径。当走到死胡同时,通过出栈操作回退到上一个决策点,尝试其他可能性。栈在这里充当了“后悔药”的角色。

6. 栈的边界条件与常见“坑点”

在实际编码和面试中,栈相关的错误大多源于对边界条件处理不当。下面我总结几个最容易踩坑的地方。

6.1 栈空时执行出栈或获取栈顶操作

这是最经典的运行时错误。无论是顺序栈还是链式栈,在执行PopGetTop操作前,必须检查栈是否为空。对于顺序栈,空栈时top-1,如果尝试访问data[-1],会引发数组越界。对于链式栈,空栈时topNULL,如果尝试访问NULL->data,会导致程序崩溃(空指针解引用)。

防御性编程:永远把IsEmpty检查作为PopGetTop函数的第一行。在团队协作中,甚至可以约定这些函数返回一个布尔值表示操作是否成功,并通过指针参数返回实际数据,就像我们前面代码示例中做的那样。

6.2 栈满时执行入栈操作(针对顺序栈)

顺序栈有容量限制。当top == MAX_SIZE - 1时,栈已满。此时再执行Push操作,如果直接写入data[MAX_SIZE],会导致缓冲区溢出,破坏相邻内存的数据,这是非常严重的安全隐患(著名的栈溢出攻击就利用了类似的原理)。

解决方案

  1. 严格检查:入栈前必须调用IsFull
  2. 动态扩容:更健壮的做法是实现动态顺序栈。当栈满时,申请一个更大的数组(比如原大小的2倍),将旧数据拷贝过去,然后释放旧数组。这增加了复杂度,但提供了灵活性。C++中的std::vector、Java中的ArrayList在背后就是这样做的。

6.3 链式栈的内存泄漏

这是链式结构特有的问题。每次Push都需要malloc,每次Pop都需要free。如果只Popfree,或者整个栈使用完后没有遍历free所有节点,就会造成内存泄漏。在长时间运行的服务中,微小的泄漏累积起来可能导致内存耗尽。

排查与习惯

  • Pop函数中,确保在更新top指针之前,用临时变量保存待删除节点的地址。
  • 为链式栈提供一个DestroyStack函数,并在栈的生命周期结束时调用它。
  • 使用Valgrind等内存检测工具定期检查程序。

6.4 多线程环境下的栈操作

栈本身不是线程安全的数据结构。如果多个线程同时操作同一个栈(一个在Push,另一个在Pop),而没有同步机制,会导致数据竞争,引发不可预知的结果,比如数据丢失或程序崩溃。

常见策略

  • 互斥锁:在栈的每个操作函数开始处加锁,结束处解锁。这是最简单粗暴的方法,但会降低并发性能。
  • 使用线程安全的数据结构:如Java中的java.util.concurrent.ConcurrentLinkedDeque(可以当作栈来用),它使用了更高效的CAS(Compare-And-Swap)等无锁算法。

7. 栈、堆与函数栈帧:深入内存模型

“栈”和“堆”是程序员口中常说的两个词,但它们指代的是完全不同的概念,极易混淆。这里结合函数栈帧,做一个彻底的厘清。

特性栈 (Stack)堆 (Heap)
管理方式由编译器/系统自动分配和释放。函数调用时分配栈帧,函数返回时自动回收。由程序员手动申请和释放(如C的malloc/free,C++的new/delete)。
生长方向通常从高地址向低地址生长(“自上而下”)。通常从低地址向高地址生长。
分配效率速度快,仅需移动栈指针。速度慢,需要寻找合适的内存块,并可能引发碎片整理。
内存大小大小有限。每个线程的栈空间是预先设定好的(如Linux默认8MB)。大小受限于系统虚拟内存总量,理论上很大。
存储内容函数调用栈帧(局部变量、参数、返回地址等)。动态分配的对象、大型数组等。
碎片问题无内存碎片。有内存碎片问题。
线程安全每个线程有自己的栈,线程私有,天然安全。堆是进程内所有线程共享的,需要同步控制。

函数栈帧是栈内存中的一个逻辑块。当一个函数被调用时,会压入一个新的栈帧。这个栈帧里通常包含:

  • 返回地址:函数执行完后,应该回到调用它的下一条指令地址。
  • 调用者的栈帧基址:用于在函数返回后恢复调用者的栈环境。
  • 函数的参数:从右向左依次压栈(取决于调用约定)。
  • 函数的局部变量:在栈帧内分配空间。
  • 临时存储区:用于保存寄存器值或中间计算结果。

以一段简单的C代码为例:

int add(int a, int b) { int result = a + b; return result; } int main() { int x = 5, y = 10; int sum = add(x, y); return 0; }

main调用add时,系统会:

  1. 将参数y(10)和x(5)压入栈(或存入寄存器,取决于ABI)。
  2. main函数中call add指令的下一条地址(返回地址)压栈。
  3. 跳转到add函数的代码。
  4. add的栈帧中为局部变量result分配空间。
  5. 执行计算,将结果存入result
  6. 函数返回前,将返回值(通常通过特定寄存器如EAX)设置好。
  7. 弹出add的栈帧,根据返回地址跳回main函数。
  8. main函数从栈或寄存器中获取返回值,赋给变量sum

理解栈帧对于调试至关重要。当程序崩溃产生“核心已转储”文件时,调试器(如GDB)就是通过分析栈帧的链式结构(栈回溯)来告诉你函数调用链,从而定位问题所在的。

8. 栈在算法竞赛与面试中的实战技巧

栈不仅是基础数据结构,更是解决一系列特定算法问题的利器。掌握下面几个经典栈算法模板,能让你在面试和竞赛中游刃有余。

8.1 单调栈:寻找下一个更大/更小元素

单调栈是指栈内元素保持单调递增或单调递减的栈。它常用于解决“寻找每个元素右边第一个比它大(或小)的元素”这类问题,时间复杂度可以优化到O(n)。

问题:给定一个数组nums,返回一个等长的数组answer,其中answer[i]nums[i]右边第一个比它大的元素的下标,如果没有则为-1

思路与代码: 维护一个栈底到栈顶单调递减的栈,栈里存放的是元素的下标(因为我们需要知道位置信息)。 遍历数组:

  1. 如果当前元素nums[i]小于等于栈顶下标对应的元素,说明当前元素不破坏单调性,直接将其下标i入栈。
  2. 如果当前元素nums[i]大于栈顶下标对应的元素,说明我们找到了栈顶元素的下一个更大元素。弹出栈顶下标topIndex,并设置answer[topIndex] = i。重复此过程,直到栈空或当前元素不再大于栈顶元素。
  3. 遍历结束后,栈中剩余的下标对应的元素,其右边都没有更大的元素,将它们的answer值设为-1
void nextGreaterElement(int nums[], int n, int answer[]) { int stack[n]; // 用数组模拟栈,存储下标 int top = -1; for (int i = 0; i < n; i++) { answer[i] = -1; // 先初始化为-1 } for (int i = 0; i < n; i++) { // 当前元素大于栈顶下标对应的元素 while (top != -1 && nums[i] > nums[stack[top]]) { int topIndex = stack[top]; top--; // 出栈 answer[topIndex] = i; // 找到了下一个更大元素的位置 } stack[++top] = i; // 当前下标入栈 } // 栈中剩余的元素,answer值保持为-1 }

实战心得:单调栈的难点在于想清楚栈里应该存什么(值还是下标?),以及维护单调性的判断条件(是>还是>=?)。对于“下一个更小元素”问题,只需将判断条件中的>改为<,并维护一个单调递增栈即可。多画图模拟过程,是掌握它的不二法门。

8.2 使用栈实现队列

这是一个经典的面试题,考察对栈和队列性质的理解。队列是FIFO(先进先出),栈是LIFO(后进先出)。用栈实现队列,需要两个栈来“扭转”顺序。

思路: 设置两个栈:stackInstackOut

  • 入队:所有新元素都压入stackIn
  • 出队/查看队首
    • 如果stackOut不为空,则直接从stackOut弹出或查看栈顶元素。
    • 如果stackOut为空,则将stackIn中的所有元素依次弹出并压入stackOut。这样,最早进入stackIn的元素(即队首)就位于stackOut的栈顶了,然后对其进行出栈或查看操作。
typedef struct { SeqStack stackIn; SeqStack stackOut; } MyQueue; void myQueuePush(MyQueue* obj, int x) { Push(&(obj->stackIn), x); // 入队直接压入输入栈 } int myQueuePop(MyQueue* obj) { // 如果输出栈为空,则将输入栈的所有元素倒入输出栈 if (IsEmpty(&(obj->stackOut))) { while (!IsEmpty(&(obj->stackIn))) { int temp; Pop(&(obj->stackIn), &temp); Push(&(obj->stackOut), temp); } } int value; Pop(&(obj->stackOut), &value); // 从输出栈弹出 return value; } int myQueuePeek(MyQueue* obj) { // 同Pop,只是获取而不删除 if (IsEmpty(&(obj->stackOut))) { while (!IsEmpty(&(obj->stackIn))) { int temp; Pop(&(obj->stackIn), &temp); Push(&(obj->stackOut), temp); } } int value; GetTop(&(obj->stackOut), &value); return value; }

复杂度分析:虽然PopPeek操作在最坏情况下是O(n)(需要倒栈),但每个元素只会从stackIn进入stackOut一次,因此摊还时间复杂度是O(1)。这是一个非常重要的分析角度,面试时一定要能说出来。

8.3 最小栈:在常数时间内检索到最小元素

设计一个栈,除了支持常规的PushPopTop,还能在**O(1)**时间内检索到栈中的最小元素。

思路:使用一个辅助栈minStack,与主栈dataStack同步操作。

  • Push(x)时:将x压入dataStack。同时,比较x与当前minStack栈顶元素minTop。如果x <= minTop(注意是<=而不是<,是为了处理多个相同最小值的情况),则将x也压入minStack
  • Pop()时:从dataStack弹出栈顶元素topValue。如果topValue等于当前minStack栈顶元素,则minStack也弹出栈顶。
  • GetMin()时:直接返回minStack的栈顶元素。
typedef struct { SeqStack dataStack; SeqStack minStack; // 辅助栈,栈顶始终是当前数据栈中的最小值 } MinStack; void minStackPush(MinStack* obj, int x) { Push(&(obj->dataStack), x); // 如果辅助栈为空,或者x小于等于辅助栈栈顶 if (IsEmpty(&(obj->minStack)) || x <= GetTopValue(&(obj->minStack))) { Push(&(obj->minStack), x); } } void minStackPop(MinStack* obj) { if (IsEmpty(&(obj->dataStack))) return; int topValue; Pop(&(obj->dataStack), &topValue); int minTop; GetTop(&(obj->minStack), &minTop); if (topValue == minTop) { int dummy; Pop(&(obj->minStack), &dummy); } } int minStackGetMin(MinStack* obj) { int minValue; GetTop(&(obj->minStack), &minValue); return minValue; }

关键点:辅助栈minStack的栈顶元素,永远是当前dataStack中所有元素的最小值。当最小值被弹出时,minStack也随之弹出,新的栈顶就是次小值(或另一个相同的最小值)。这个设计巧妙地用空间(O(n)最坏情况)换取了时间(O(1)查询最小值)。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询