跳到主要内容
数据结构基础:栈与队列的顺序及链式实现 | 极客日志
C 算法
数据结构基础:栈与队列的顺序及链式实现 综述由AI生成 栈和队列是特殊的线性表,栈遵循后进先出原则,队列遵循先进先出原则。详细讲解了顺序栈、共享栈、链栈的定义与操作,包括初始化、进栈、出栈及获取栈顶元素。同时涵盖了顺序队列的循环处理方案(如增加 size 或 tag 变量)以及链式队列的带头结点与不带头结点实现。通过对比顺序存储与链式存储的优缺点,帮助读者掌握数据结构的核心逻辑与代码实现细节。
漫步 发布于 2026/3/26 更新于 2026/6/5 19 浏览一、前言
我们在前两篇文章中讲到顺序表和链表其都是线性结构,我们今天讲的栈和队列也是特殊的线性表。顺序表和链表没有所谓的进出限制,但是我们今天要讲的栈就不一样,它有特殊的进栈和出栈顺序,只允许在一端进行插入和删除。也就是说后进先出,先进后出。但是队列呢,只允许从前面插入,后面出。也就是他俩是特殊的线性结构,所以在基本操作上和前文的线性表和链表有一定相似之处。
二、栈
只允许在一端进行插入和删除操作的线性表。
空栈,没有元素。
栈顶允许插入和删除,栈底不允许。
2.1 顺序栈
就是用顺序方式存储的栈就是顺序栈。
顺序栈的定义
这里用 top 指针来标记栈顶位置,初始化时 top = -1,表示栈是空的。就像刚买的空盘子架,还没放任何盘子。
#define MAXSIZE 100
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct {
int data[100 ];
int top;
} SqStack;
初始化
void InitStack (SqStack *S) {
S->top = -1 ;
}
bool StackEmpty (SqStack *S) {
if (S->top == -1 ) {
return true ;
} else {
return false ;
}
}
进栈操作
bool Push (SqStack *S, int x) {
if (S->top == MAXSIZE) {
return false ;
}
S->top++;
S->data[S->top] = x;
return true ;
}
先检查栈是不是满了(top 等于最大容量),没满的话就把 top 往上挪一位,再把数据放进去。
出栈操作
bool Pop (SqStack *S, int *x) {
if (S->top == -1 ) {
return false ;
}
*x = S->data[S->top];
S->top--;
return true ;
}
先检查栈是不是空的,不空的话就把栈顶元素取出来,再把 top 往下挪一位。
获取栈顶元素 有时候我们只想看看最上面的盘子是啥样,不想拿走它:
bool GetTop (SqStack *S, int *x) {
if (S->top == -1 ) {
return false ;
}
*x = S->data[S->top];
return true ;
}
这个操作和出栈的区别是,top 指针不会移动,只是'偷看'一眼。
2.2 共享栈 共享栈是个节省空间的小能手,它让两个栈共享同一块数组空间,栈底分别在数组的两端,向中间生长。
建立
typedef struct {
int data[100 ];
int top;
int top1;
} SharedStack;
初始化 初始化时,第一个栈的 top 在 -1,第二个栈的 top1 在最大容量处:
void InitSharedStack (SharedStack *S) {
S->top = -1 ;
S->top1 = MAXSIZE;
}
这样两个栈就可以向中间扩展,直到 top + 1 == top1 时,表示栈满。
2.3 链栈 链栈就是用链表实现的栈,链表的头结点作为栈顶,这样进栈和出栈操作都能在 O(1) 时间内完成,比顺序栈更灵活(不用提前规定大小)。
建立
typedef struct LinkNode {
int data;
struct LinkNode *next ;
} LinkNode;
typedef LinkNode* LinkStack;
这里栈顶指针就是链表的头指针,进栈就是在头结点前插入新节点,出栈就是删除头结点。
三、队列 队列和栈正好相反,它是'先进先出'(FIFO,First In First Out)的,就像排队买奶茶——先到的人先拿到奶茶。只能在队尾插入(入队),在队头删除(出队)。
顺序队列用数组实现,但有个小问题:如果单纯地让 front 指向队头,rear 指向队尾,随着入队和出队操作,front 和 rear 都会往后移动,可能导致数组前面的空间浪费。
定义
typedef struct {
int data[100 ];
int front;
int rear;
} SqQueue;
初始化队列
void InitQueue (SqQueue *Q) {
Q->front = Q->rear = 0 ;
}
判断队列是否为空
bool QueueEmpty (SqQueue *Q) {
if (Q->front == Q->rear) {
return true ;
} else {
return false ;
}
}
入队操作
bool EnQueue (SqQueue *Q, int x) {
if (Q->rear == MAXSIZE) {
return false ;
}
Q->data[Q->rear] = x;
Q->rear++;
return true ;
}
把数据放在 rear 指向的位置,再把 rear 往后挪一位。
出队操作
bool DeQueue (SqQueue *Q, int *x) {
if (Q->front == Q->rear) {
return false ;
}
*x = Q->data[Q->front];
Q->front++;
return true ;
}
取出 front 指向的元素,再把 front 往后挪一位。
判断队列的满和空
法一: 上面的方法有个缺陷:rear 到达数组末尾时,即使前面有空位,也会被判为队满。解决这个问题有几种方法:
bool GetHead (SqQueue Q, int *x) {
if (Q.rear == Q.front)
return false ;
*x = Q.data[Q.front];
return true ;
}
法二:定义长度问题 增加一个 size 变量记录队列长度,队满条件是 size == MaxSize,队空条件是 size == 0。
#define MaxSize 10
typedef struct {
int data[10 ];
int front, rear;
int size;
} SqQueue;
法三: 增加一个 tag 变量,记录最近操作是插入(1)还是删除(0)。队满条件是 front == rear && tag == 1,队空条件是 front == rear && tag == 0。
#define MaxSize 10
typedef struct {
int data[10 ];
int front, rear;
int tag;
} SqQueue;
链式存储实现队列 链式队列用链表实现,队头指针指向头结点,队尾指针指向最后一个节点,这样入队和出队操作都很方便。
定义一个链式队列
typedef struct LinkNode {
int data;
struct LinkNode *next ;
} LinkNode;
typedef struct {
LinkNode *front, *rear;
} LinkQueue;
初始化(带头结点)
void InitQueue (LinkQueue *Q) {
Q->front = Q->rear = (LinkNode *)malloc (sizeof (LinkNode));
Q->front->next = NULL ;
}
bool QueueEmpty (LinkQueue *Q) {
if (Q->front == Q->rear) {
return true ;
} else {
return false ;
}
}
初始化队列不带头结点
bool QueueEmpty (LinkQueue *Q) {
if (Q->front == Q->rear) {
return true ;
} else {
return false ;
}
}
入队(带头结点) 入队就是在队尾添加新节点,然后把 rear 移到新节点。
bool EnQueue (LinkQueue *Q, int x) {
LinkNode *s = (LinkNode *)malloc (sizeof (LinkNode));
if (s == NULL ) {
return false ;
}
s->data = x;
s->next = NULL ;
Q->rear->next = s;
Q->rear = s;
return true ;
}
入队(不带头结点)
bool EnQueue (LinkQueue *Q, int x) {
LinkNode *s = (LinkNode *)malloc (sizeof (LinkNode));
if (s == NULL ) {
return false ;
}
s->data = x;
s->next = NULL ;
if (QueueEmpty(Q)) {
Q->front = s;
Q->rear = s;
} else {
Q->rear->next = s;
Q->rear = s;
}
return true ;
}
不带头结点的链式队列入队操作,核心区别在于需要处理'首次入队'的特殊情况:
当队列是空的时候(front 和 rear 都为 NULL),新节点既是队头也是队尾,所以 front 和 rear 要同时指向这个新节点
非空队列时,操作和带头结点类似:让当前队尾的 next 指向新节点,再把 rear 移到新节点上
出队(带头结点)
bool DeQueue (LinkQueue *Q, int *x) {
if (QueueEmpty(Q)) {
return false ;
}
LinkNode *p = Q->front->next;
*x = p->data;
Q->front->next = p->next;
if (Q->rear == p) {
Q->rear = Q->front;
}
free (p);
return true ;
}
队头元素始终是 front->next 指向的节点(头结点不存储数据)
出队时只需修改头结点的 next 指针,最后一个元素出队时需将 rear 重置为头结点
出队(不带头结点)
bool DeQueue (LinkQueue *Q, int *x) {
if (QueueEmpty(Q)) {
return false ;
}
LinkNode *p = Q->front;
*x = p->data;
if (Q->front == Q->rear) {
Q->front = Q->rear = NULL ;
} else {
Q->front = Q->front->next;
}
free (p);
return true ;
}
队头指针 front 直接指向队头元素(首个数据节点)
出队时需直接移动 front 指针,最后一个元素出队后需将 front 和 rear 均置为 NULL
队列满的条件
四、总结 栈和队列都是特殊的线性表,只是对操作的位置做了限制:
栈:只允许在栈顶操作,后进先出
队列:只允许在队尾入队、队头出队,先进先出
它们的实现可以用数组(顺序存储)或链表(链式存储),各有优缺点:
顺序存储:访问快,但大小固定(或需要扩容)
链式存储:大小灵活,但访问需要遍历指针
就像两种不同的数据处理风格:栈是'后来者居上',队列是'按规矩办事',各有各的适用场景。掌握它们的逻辑和实现,对于理解更复杂的数据结构至关重要。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online