Skip to content

顺序栈

2026 大纲 三(二)栈和队列的顺序存储结构 · 单个栈的数组实现(两栈共享见《共享栈》,队列见《循环队列》)。

top 指哪儿,决定其余一切

顺序栈就是"一个数组 + 一个栈顶指针 top"。但 top 到底指向栈顶元素本身,还是指向栈顶元素的下一个空位,是两套不同的约定——约定一旦定下,栈空、栈满、入栈出栈的语句顺序就全部被确定了,不需要分开背两套公式。

约定初始值栈顶元素栈空条件栈满条件元素个数入栈出栈
甲:top 指向栈顶元素top = -1data[top]top == -1top == MaxSize - 1top + 1++top,再赋值先取值,再 top--
乙:top 指向栈顶元素的下一个位置top = 0data[top - 1]top == 0top == MaxSizetop先赋值,再 top++--top,再取值

⚠️ top 在这里是数组下标,不是 C 语言的指针。教材里说的"栈顶指针"是个逻辑称呼;教材另有用真指针写的版本,那里的 S.top - S.base 就等于这里的 top

另外,题面不特别说明时通常按约定甲(top = -1),但只要题面给出了初值或栈空条件,一律以题面为准

先看一眼

加载可视化中...

压几个再弹几个,盯住两件事:top 每次移动的方向,以及弹出之后数组里那一格的值有没有变。第二点下面会专门说——那里有个容易判反的结论。

存储结构与五个基本操作(约定甲)

c
#define MaxSize 50          // 栈的最大容量,编译期确定
typedef struct {
    int data[MaxSize];      // 存放栈中元素,静态分配
    int top;                // 栈顶指针(这里是数组下标,不是 C 指针)
} SqStack;

void InitStack(SqStack *S) {
    S->top = -1;               // 约定甲:-1 表示"还没有任何有效元素"
}

bool StackEmpty(SqStack S) {
    return S.top == -1;        // 与初始化保持同一口径,二者必须成对改动
}

bool Push(SqStack *S, int x) {
    if (S->top == MaxSize - 1)  // 栈满,上溢;注意是 MaxSize-1 不是 MaxSize
        return false;
    S->data[++S->top] = x;      // 前缀 ++:先把指针移到空位,再写入
    return true;
}

bool Pop(SqStack *S, int *x) {
    if (S->top == -1)           // 栈空,下溢
        return false;
    *x = S->data[S->top--];     // 后缀 --:先用当前 top 取值,再移指针
    return true;
}

bool GetTop(SqStack S, int *x) {
    if (S.top == -1)
        return false;
    *x = S.data[S.top];         // 只读取,不移动指针
    return true;
}

三处细节值得单独说明。MaxSize 是编译期常量,数组随结构体一起分配——这决定了顺序栈一定存在栈满,是它与链栈最本质的区别。GetTop 用传值而非指针,因为它不应该修改栈,传值在语义上就杜绝了误改;Push/Pop 则必须传指针,否则修改传不回调用方。初始化与判空是同一条约定的两面——凡是把 InitStack 改成 top = 0 的题目,StackEmpty 必然同步改成 top == 0

那些前缀 / 后缀的顺序也不用死记,把约定当成一个不变量,要求它在每次操作前后都成立,顺序就被唯一确定了:

  • 约定甲的不变量是"top 处存的是有效元素"。 入栈时新元素还没地方放,必须先把指针挪到空位++top)再写入,写完不变量恢复;出栈时 top 处正是要取的元素,所以先取值,取完那格失效,再 top--
  • 约定乙的不变量是"top 处是空位"。 入栈时 top 已经指着空位,直接写入,写完这格不再是空位,指针后移(top++);出栈时 top 处是空位、top-1 才是元素,所以--top 落到元素上,再取值。

🔴 约定甲下,入栈必须用前缀 ++top 写成 data[top++] = x,空栈时会先用旧值 1 做下标写 data[-1]——越界写,随后 top 才变成 0;接着 Pop 取到的是从未赋值的 data[0]。同理出栈误写成 data[--top] 会跳过真正的栈顶元素。

🔴 约定甲的栈满是 top == MaxSize - 1,不是 MaxSize top 指的是元素,最大合法下标就是 MaxSize-1;写成 MaxSize 等于先越界写一格才发现满

一段操作序列的完整跟踪(第一次学、或想手动模拟时展开)

MaxSize = 4、采用约定甲,执行 Push(A) → Push(B) → Pop → Push(C) → Push(D) → Push(E) → Pop → Pop

操作判断top数组 data[0..3]返回
0初始化1_ _ _ _
1Push(A)13,不满0A _ _ _true
2Push(B)031A B _ _true
3Pop11,非空0A B _ _(B 仍在,但已不属于栈)true,取回 B
4Push(C)不满1A C _ _C 覆盖了 Btrue
5Push(D)不满2A C D _true
6Push(E)不满3A C D Etrue
7Pop非空2A C D Etrue,取回 E
8Pop非空1A C D Etrue,取回 D

上面这段跟踪里有两个观察值得单独拎出来。一是第 3 步之后 data[1] 里还留着 B,直到第 4 步才被 C 覆盖——出栈只是移动指针,并不擦除数组里的数据,所以"出栈后数组内容不变、只有 top 变了"是一句正确的描述。二是第 6 步之后 top == 3 == MaxSize - 1,此时再 Push 会被判满拒绝,而数组确实已经写满 4 格,判满条件与实际容量恰好吻合

约定乙的完整实现与教材的动态分配指针版(题面给的是另一套写法时展开)
c
// 约定乙:top 初值 0,指向栈顶元素的下一个空位
void InitStack2(SqStack *S) { S->top = 0; }

bool Push2(SqStack *S, int x) {
    if (S->top == MaxSize) return false;   // 栈满:空位已经跑到数组外
    S->data[S->top++] = x;                 // 后缀 ++:先写入,再移指针
    return true;
}

bool Pop2(SqStack *S, int *x) {
    if (S->top == 0) return false;         // 栈空
    *x = S->data[--S->top];                // 前缀 --:先移指针,再取值
    return true;
}

把两组代码并排看,四个前缀/后缀恰好全部相反——这就是上面"不变量"分析的直接结果。

教材的动态分配写法用两个真指针 basetop 加一个 stacksize

c
#define MAXSIZE 100
typedef struct {
    int *base;        // 栈底指针,初始化后始终指向栈底
    int *top;         // 栈顶指针,指向栈顶元素的上一个位置
    int stacksize;    // 栈可使用的最大容量
} SqStack;

bool InitStack(SqStack *S) {
    S->base = (int *)malloc(MAXSIZE * sizeof(int));
    if (S->base == NULL) return false;   // 分配失败,栈结构不存在
    S->top = S->base;                    // 初值:top 与 base 相等 → 栈空
    S->stacksize = MAXSIZE;
    return true;
}

bool Push(SqStack *S, int e) {
    if (S->top - S->base == S->stacksize) return false;  // 栈满
    *S->top++ = e;                                       // 先写入,再后移
    return true;
}

bool Pop(SqStack *S, int *e) {
    if (S->top == S->base) return false;                 // 栈空
    *e = *--S->top;                                      // 先前移,再取值
    return true;
}

它与本篇的约定乙逐条对应:

下标版(约定乙)教材的指针版
栈空top == 0S.top == S.base
栈满top == MaxSizeS.top - S.base == S.stacksize
元素个数topS.top - S.base
栈顶元素data[top-1]*(S.top - 1)
入栈data[top++] = x*S.top++ = e
出栈x = data[--top]e = *--S.top

"指针差就是下标"——S.top - S.base 与下标版的 top 是同一个量。 看到教材式写法不必重新理解,把 S.top - S.base 在心里换成 top 即可。

两种写法唯一的实质差别是空间来源:教材版用 malloc 动态申请,容量可以在运行时决定 (甚至在栈满时 realloc 扩容);本篇的静态数组版容量在编译期就定死了。 但两者都属于顺序存储、都会栈满,与链栈仍然是两回事。

复杂度与它的空间困境

PushPopGetTop、判空判满、求栈长全部是 O(1)——每个操作都只是"一次比较 + 一次下标加减 + 一次读写内存",操作数与栈里有几个元素完全无关。栈本身占 O(MaxSize) 空间,与实际存了几个元素无关(静态数组一次分配到位),单次操作的辅助空间是 O(1)

和顺序表比一比就更清楚了:顺序表在第 i 个位置插入要把后面 ni 个元素整体后移,平均 O(n)顺序栈能做到 O(1),唯一的原因是它把插入删除的位置锁死在了末端——限制换来了效率。这也说明"操作受限"不是缺点,而是设计目的。

代价则是必须事先猜一个 MaxSize:猜大了浪费,猜小了上溢。若程序里要同时用两个栈,各开一个 MaxSize/2 的数组,很可能出现"一个已经满了、另一个还空着一大半"——解法是共享栈;若元素个数完全无法预估,解法是链栈,代价是每个结点多一个指针域、存储密度下降、缓存局部性变差。

考点速记

三条会被反复调用的结论:

  1. 一切都从"top 指哪儿"推出来,四条结论加前后缀顺序是被约定唯一确定的,别分开背两套。
  2. 约定甲的栈满是 top == MaxSize - 1,写成 MaxSize 会先越界再报满。
  3. 上溢是顺序存储独有的(链栈没有),下溢是逻辑层面的(链栈也有)。

这一节在真题里被考过的形式(下方「真题练习」逐题对应):这里有个反直觉的现象值得说明——挂在这个考点下的真题,考的几乎都不是 top 指针的实现细节,而是出栈序列。也就是说,顺序栈的代码是用来写对的,不是用来考的;真正被反复出题的是栈这个逻辑结构本身的性质

  • 以某个指定元素开头的出栈序列有多少个——固定开头之后,剩下的元素被切成互不干扰的两段,各自再数。
  • 栈与队列组合使用时,哪个输出序列得不到——题面给出"出队直接输出/出队入栈/出栈输出"三种操作,本质仍是模拟。
  • 关于入栈序列与出栈序列关系的命题判断——比如"两者是否一定不同""能否互为倒序"。

易错"出栈就把数组里那格清空了"是错的。 出栈只移动 top,旧值原样留着,直到下次入栈把它覆盖。

易错约定甲入栈写成 data[top++] 空栈时会写到 data[-1],越界。约定甲必须用前缀 ++top

易错判满写成 top == MaxSize 那是约定乙的条件;约定甲写成这样等于先越界一格才发现满。改约定时,初始化、判空、判满、入栈、出栈五处必须一起改

教材出处
  • 顺序栈的存储结构(base / top / stacksize 三个分量,top 初值指向栈底、 非空时指向栈顶元素的上一个位置):严蔚敏《数据结构(C 语言版)》(第 2 版)p58 「3.2.1 顺序栈——栈的顺序存储表示」
  • 入栈算法 3.2(判满 S.top - S.base == S.stacksize*S.top++ = e)与 出栈算法 3.3(判空 S.top == S.basee = *--S.top):同书 p59
  • 取栈顶算法 3.4(不修改栈顶指针):同书 p59

教材采用的是"top 指向栈顶元素的上一个位置"(即本篇的约定乙), 所以它的入栈是后缀 top++、出栈是前缀 --top,与本篇约定乙的代码完全对应。

相关知识

栈和队列的基本概念(ADT 定义与合法出栈序列的判定/计数)| 共享栈(两个栈共享一个数组,栈满改为 top1 + 1 == top2)| 链栈(不存在上溢)| 顺序表(同样用数组,但允许任意位置插入删除)| 括号匹配表达式求值(本篇代码的直接使用者)

真题练习

相关真题(3题)