1. 栈的基本概念与特性
栈(Stack)是一种特殊的线性表,它遵循"后进先出"(Last In First Out, LIFO)的原则。想象一下餐厅里叠放的餐盘——你总是从最上面取用餐盘,新洗好的餐盘也会放在最上面。这种结构在计算机科学中有着广泛的应用。
1.1 栈的核心特性
栈有两个基本操作端:
- 栈顶(Top):允许插入和删除元素的一端
- 栈底(Bottom):不允许操作的一端
栈的核心性质可以概括为:
- 只能在栈顶进行插入(入栈/Push)和删除(出栈/Pop)操作
- 中间位置的元素不可直接访问或操作
- 最后入栈的元素会最先出栈(LIFO原则)
注意:栈的"先进后出"特性与队列的"先进先出"形成鲜明对比,这是选择数据结构时需要重点考虑的因素。
1.2 栈的抽象数据类型
一个完整的栈ADT(抽象数据类型)通常包含以下基本操作:
- InitStack(): 初始化一个空栈
- Push(x): 将元素x压入栈顶
- Pop(): 弹出栈顶元素
- GetTop(): 获取栈顶元素(但不删除)
- IsEmpty(): 判断栈是否为空
- IsFull(): 判断栈是否已满(仅限顺序栈)
在实际应用中,我们可能还需要其他辅助操作,如获取栈大小、清空栈等,但这些都可以基于上述基本操作实现。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 顺序栈的实现
顺序栈使用数组作为底层存储结构,通过一个栈顶指针来跟踪当前栈顶位置。这种实现方式简单高效,但存在固定容量的限制。
2.1 顺序栈的结构设计
cpp复制#define MAXSIZE 100 // 栈的最大容量
typedef struct {
int *data; // 存储栈元素的数组
int top; // 栈顶指针
int capacity; // 栈的当前容量
} SeqStack;
这里有三种常见的栈顶指针设计方式:
-
top指向栈顶元素的下一个位置(初始top=0)
- 入栈:data[top++] = x
- 出栈:x = data[--top]
- 判空:top == 0
- 判满:top == capacity
-
top指向栈顶元素(初始top=-1)
- 入栈:data[++top] = x
- 出栈:x = data[top--]
- 判空:top == -1
- 判满:top == capacity-1
-
使用结构体封装(如上面的SeqStack)
- 更灵活,可以动态调整容量
- 可以添加其他辅助信息
提示:在实际工程中,建议使用第三种方式,因为它更灵活且易于扩展。第一种和第二种更适合教学示例或内存受限的环境。
2.2 顺序栈的基本操作实现
2.2.1 初始化栈
cpp复制SeqStack* InitSeqStack(int maxSize) {
SeqStack *s = (SeqStack*)malloc(sizeof(SeqStack));
if (!s) return NULL;
s->data = (int*)malloc(maxSize * sizeof(int));
if (!s->data) {
free(s);
return NULL;
}
s->top = 0;
s->capacity = maxSize;
return s;
}
2.2.2 入栈操作
cpp复制int Push(SeqStack *s, int x) {
if (s->top >= s->capacity) {
// 栈满,可以考虑动态扩容
int newCapacity = s->capacity * 2;
int *newData = (int*)realloc(s->data, newCapacity * sizeof(int));
if (!newData) return 0; // 扩容失败
s->data = newData;
s->capacity = newCapacity;
}
s->data[s->top++] = x;
return 1;
}
2.2.3 出栈操作
cpp复制int Pop(SeqStack *s, int *x) {
if (s->top <= 0) return 0; // 栈空
*x = s->data[--s->top];
return 1;
}
2.2.4 获取栈顶元素
cpp复制int GetTop(SeqStack *s, int *x) {
if (s->top <= 0) return 0;
*x = s->data[s->top - 1];
return 1;
}
2.3 顺序栈的优缺点分析
优点:
- 实现简单,逻辑清晰
- 存取速度快,时间复杂度O(1)
- 内存连续,缓存友好
缺点:
- 容量固定(除非实现动态扩容)
- 扩容时可能涉及数据搬移,性能开销大
- 可能造成内存浪费(分配过多但使用很少)
经验分享:在实际项目中,如果栈的大小可以预估且不会剧烈变化,顺序栈是首选。对于变化剧烈的场景,建议使用链式栈或实现动态扩容的顺序栈。
3. 链式栈的实现
链式栈使用链表作为存储结构,理论上可以无限扩展(受限于内存),但每个元素需要额外的指针空间。
3.1 链式栈的结构设计
cpp复制typedef struct StackNode {
int data;
struct StackNode *next;
} StackNode;
typedef struct {
StackNode *top; // 栈顶指针
int size; // 栈大小(可选)
} LinkStack;
链式栈通常不需要头节点,直接让top指针指向栈顶元素即可。添加size字段可以方便地获取栈的当前大小,但会增加一点维护开销。
3.2 链式栈的基本操作实现
3.2.1 初始化栈
cpp复制LinkStack* InitLinkStack() {
LinkStack *s = (LinkStack*)malloc(sizeof(LinkStack));
if (!s) return NULL;
s->top = NULL;
s->size = 0;
return s;
}
3.2.2 入栈操作
cpp复制int Push(LinkStack *s, int x) {
StackNode *newNode = (StackNode*)malloc(sizeof(StackNode));
if (!newNode) return 0;
newNode->data = x;
newNode->next = s->top;
s->top = newNode;
s->size++;
return 1;
}
3.2.3 出栈操作
cpp复制int Pop(LinkStack *s, int *x) {
if (s->top == NULL) return 0;
