前言
之前鸽了真的抱歉,肝了一段时间把栈和队列全部更完。
简介:
你有没有想过,为什么我们吃饭的时候,总是先吃掉最后放进嘴里的那一口?为什么我们排队的时候,总是先让最先来的人先走?这些生活中的现象,其实都和栈和队列的数据结构有关。
栈和队列是两种常见的数据结构,它们分别对应了后进先出(LIFO)和先进先出(FIFO)的原则。
栈就像是一叠盘子,你只能从最上面拿走或放上一个盘子,这个盘子就是栈顶。
队列就像是一条长龙,你只能从尾巴加入或从头部离开,这两个位置就是队尾和队首。
本文将向你大致介绍栈和队列的概念、特征和应用场景。
注:
本博客为本人在学习数据结构路途上的知识整理,如觉得对有你有所帮助,还希望不要吝啬你的赞,整理知识点是真的很累|*´Å`)ノ 。由于博主只是一名大一新生,在文章难免会出现错误,还希望指正。如果想要转载,附上链接就行。
本文中的颜色标记
- 红色部分为重点内容
- 蓝色部分为注释
目录
一、栈和队列的概念
栈
定义:
- 栈(stack)是一个特殊的线性表,是限定仅在一端(通常是表尾)
进行插入和删除操作的线性表。 - 又称为后进先出(Last In First Out) 的线性表,简称LIFO结构
相关概念:
- 表尾(即an端)称为栈顶Top
- 表头(即a1端)称为栈底Base
例如:栈S=(a1,a2,a3...,an1,an)a1称为栈底元素,an称为栈顶元素
- 插入元素到栈顶(即表尾)的操作,称为入栈(PUSH)
- 从栈顶(即表尾)删除最后一个元素的操作,称为出栈(POP)
栈只能在栈顶进行插入和删除的操作,只能在an-1尾部进行插入,插入an之后,只能先删除an然后再删除an-1
逻辑结构
与同线性表相同,仍为一对一关系。
存储结构
用顺序栈或链栈存储均可,但以顺序栈更常见
运算规则
只能在栈顶运算,且访问结点时依照后进先出(LIFO)的原则。
实现方式
关键是编写入栈和出栈函数,具体实现依顺序栈或链栈的不同而不同
栈与一般线性表的区别
- 仅在于运算规则不同
队列
定义:
- 队列(queue)是一种在表一端插入(表尾),在另一端(表头)删除的线性表。
- 又称为先进先出(FristInFristOut)的线性表,简称FIFO结构。
相关概念
- 表尾(即an端)称为队尾
- 表头(即a1端)称为队首
- 插入元素到队尾(即表尾)的操作,称为入队(PUSH)
- 从队首(即表头)删除第一个元素的操作,称为出队(POP)
逻辑结构
与同线性表相同,仍为一对一关系。
存储结构
顺序队或链队,以循环顺序队列更常见。
运算规则
只能在队首和队尾运算,直访问结点时依照先进先出(FIFO)的原则
实现方式
关键是掌握入队和出队操作,具体实现依顺序队或链队的不同而不同
栈和队列的实际运用
栈的实际运用
- 括号匹配问题(20. 有效的括号 - 力扣(Leetcode))
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()" 输出:true
示例 2:
输入:s = "()[]{}" 输出:true
示例 3:
输入:s = "(]" 输出:false
解题步骤:
①将s依次栈入栈
②将s栈入的时候进行判段,为左括号直接栈入
③如果为右括号,判断是否与栈顶匹配,匹配则将栈顶栈出
④如果右括号与栈顶不匹配,则为非有效的括号,直接结束函数。
队列的实际运用
舞伴问题
假设在舞会上,男士和女士各自排成一队。舞会开始依次从男队和女队的队头各出一人配成舞伴。如果两队初始人数不相同,则较长的那一队中未配对者等待下一轮舞曲。现要求写一算法模拟上述舞伴配对问题。
显然,先入队的男士或女士先出队配成舞伴。因此该问题具有典型的先进先出特性,可以用队列作为算法的数据结构。
①首先构造两个队列
②依次将队头元素出队配成舞伴
③某队为空,则另外一队等待着则是下一舞曲第一个
可获得舞伴的人。
二、栈的表示和实现
栈的抽象数据类型定义
ADT Stack {
数据对象:
D= (ai /ai EElemSet, i=1,2..,n, n≥0)
数据关系:
R1={ <ai-1,ai> | ai-1,ai ∈D,i=2...,n}
约定an端为栈顶,a1端为栈底。
基本操作:初始化、进栈、出栈、取栈顶元素等
} ADT Stack
栈的基本操作
InitStack(&S)
初始化操作
操作结果:构造一个空栈S
DestroyStack(&S)
销毁栈操作
初始条件:栈S已存在。
操作结果:栈S被销毁。
StackEmpty(S)判定S是否为空栈
初始条件:栈S已存在。
操作结果:若栈S为空栈,则返回TRUE,否则返回FALSE
StackLength(S)
求栈的长度
初始条件:栈S
初始条件:栈S已存在且非空
操作结果:用e返回S的栈顶元素。
ClearStack(&S)
栈置空操作
初始条件:栈S已存在
操作结果:将S清为空栈
Push(&S,e)
入栈操作
初始条件:栈S已存在
操作结果:插入元素e为新的栈顶元素。
Pop(&S &e)
出栈操作
初始条件:栈S已存在且非空。
操作结果:删除S的栈顶元素an,并用e返回其值。
注意事项
- 这里传参e来获取返回值只是伪代码方便,后面使用C语言写也使用e来得到返回值只是为了和伪代码相符,可以直接将结果return。
栈的顺序实现
由于栈本身就是线性表,于是栈也有顺序存储和链式存储两种实现方式。
-
栈的顺序存储-顺序栈
-
栈的链式存储-链栈
使用数组作为顺序栈存储方式的特点:
-
简单方便、但易产生溢出(数组大小固定)
-
上溢(stack overflow):栈已经满,又要压入元素
-
下溢(stack underflow):栈已经空,还要弹出元素 注:上溢是一种错误,使问题的处理无法进行;而下溢一般认为是一种结束条件,即问题处理结束。
存储方式:
同一般线性表的顺序存储结构完全相同利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素。栈底一般在低地址端
-
设top指针,指示栈顶元素在顺序栈中的位置
-
设base指针,指示栈底元素在顺序栈中的位置
-
为了方便操作,通常top指示真正的栈顶元素之上的下标地址
-
用stacksize表示栈可使用的最大容量
栈的状态表示
- 栈空:base==top
- 栈满:top-base==stacksize
顺序栈的代码实现
- 创建链栈
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int ElemType; // 定义栈元素的数据类型
typedef struct
{
ElemType data; // 数据域
LinkStack next;
}StackNode, *LinkStack; // 定义链栈栈的类型
- 初始化操作 InitStack(&S)
void InitStack(SqStack* S)
{
S->base = (ElemType*)malloc(MAXSIZE * sizeof(ElemType)); // 分配存储空间
if (!S->base) // 分配失败
{
exit(1);
}
S->top = S->base; // 栈顶和栈底指针初始指向同一位置
S->stacksize = MAXSIZE; // 栈的最大容量
}
- 销毁栈操作 DestroyStack(&S)
void DestroyStack(SqStack* S)
{
free(S->base); // 释放存储空间
S->base = S->top = NULL; // 将栈底和栈顶指针置为NULL
S->stacksize = 0; // 当前栈的容量为0
}
- 判定S是否为空栈 StackEmpty(S)
bool StackEmpty(SqStack S)
{
if (S.top == S.base) // 栈顶和栈底指针相同时,即栈为空
return true; // 返回TRUE
else
return false; // 返回FALSE
}
- 求栈的长度 StackLength(S)
int StackLength(SqStack S)
{
return (S.top - S.base); // 栈顶指针减去栈底指针就是栈的长度
}
- 栈置空操作 ClearStack(&S)
oid ClearStack(SqStack* S)
{
S->top = S->base; // 将栈顶和栈底指针指向同一位置
}
- 取栈顶元素操作 GetTop(&S,&e)
bool GetTop(SqStack S, ElemType* e)
{
if (S.top == S.base) // 栈为空时,返回FALSE
return false;
*e = *(S.top - 1); // 栈顶元素的值赋给e
return true;
}
- 入栈操作 Push(&S,e)
void Push(SqStack* S, ElemType e)
{
if (S->top - S->base == S->stacksize) // 栈满时,扩充存储空间
{
S->base = (ElemType*)realloc(S->base, (S->stacksize + MAXSIZE) * sizeof(ElemType));
if (!S->base)
{
exit(1);
}
S->top = S->base + S->stacksize; // 栈底指针不变,栈顶指针指向新的位置
S->stacksize += MAXSIZE; // 栈的最大容量增加 MAXSIZE
}
*(S->top++) = e; // 元素 e 入栈,栈顶指针加1
}
- 出栈操作 Pop(&S)
bool Pop(SqStack* S)
{
if (S->top == S->base) // 栈为空时,返回FALSE
{
return false;
}
S--;
return true;
}
栈的链式存储实现
- 链栈是运算受限的单链表,只能在链表头部进行操作
使用链表作为顺序栈存储方式的特点:
- 链表的头指针就是栈顶
- 不需要头结点
- 基本不存在栈满的情况
- 空栈相当于头指针指向空
- 插入和删除仅在栈顶处执行
存储方式:
- 利用尾插法在单链表中存储
- 头指针指向链表尾部(栈顶)
链栈的代码实现
- 创建顺序栈
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int ElemType; // 定义栈元素的数据类型
typedef struct StackNode
{
ElemType data; // 数据域
struct StackNode* next; // 指向下一个结点的指针
} StackNode, * LinkStack; // 定义链栈栈的类型
- 初始化操作 InitStack(&S)
void InitStack(LinkStack* S)
{
*S = NULL; // 初始化栈顶指针为空
}
- 销毁栈操作 DestroyStack(&S)
void ClearStack(LinkStack* S)
{
StackNode* p;
while (*S != NULL)
{
p = *S; // 指向栈顶结点
*S = (*S)->next; // 栈顶指针指向下一个结点
free(p); // 释放当前结点的内存空间
}
}
- 判定S是否为空栈 StackEmpty(S)
bool StackEmpty(LinkStack S)
{
if (S == NULL) // 栈顶指针为空时,即栈为空
return true;
else
return false;
}
- 求栈的长度 StackLength(S)
int StackLength(LinkStack S)
{
int len = 0; // 栈的长度
while (S != NULL)
{
S = S->next; // 栈顶指针向下移动
len++; // 栈的长度加1
}
return len;
}
- 取栈顶元素操作 GetTop(&S,&e)
bool GetTop(LinkStack S, ElemType* e)
{
if (S == NULL) // 栈为空时,返回FALSE
return false;
*e = S->data; // 栈顶元素的值赋给e
return true;
}
- 入栈操作 Push(&S,e)
void Push(LinkStack* S, ElemType e)
{
StackNode* p;
p = (StackNode*)malloc(sizeof(StackNode)); // 创建新结点
if (!p) // 分配内存失败
{
exit(1);
}
p->data = e; // 将元素e赋值给新结点的数据域
p->next = *S; // 新结点的next指针指向原栈顶指针指向的结点
*S = p; // 修改栈顶指针指向新的结点
}
- 出栈操作 Pop(&S &e)
bool Pop(LinkStack* S)
{
StackNode* p;
if (*S == NULL) // 栈为空时,返回FALSE
return false;
p = *S; // p指向栈顶结点
*S = p->next; // 栈顶指针指向下一个结点
free(p); // 释放当前结点的内存空间
return true;
}
三、队列的实现和操作
队列的抽象数据类型定义
ADT Queuel {
数据对象:D={ai | ai∈ElemSet,=1,2,...,n≥0}
数据关系:R={<ai-1,ai> | ai-1,aI∈D,=2..,n) 约定a1端为队列头,an端为队列尾。
基本操作:初始化、入队、出队、将队列置空、遍历队列等操作...
} ADT Queue
基本操作:
InitQueue(&Q)
操作结果:构造空队列Q
DestroyQueue(&Q)
条件:队列Q已存在
操作结果:队列Q被销毁
ClearQueue(&Q)
条件:队列Q已存在
操作结果:将Q清空
QueueLength(Q)
条件:队列Q已存在
操作结果:返回Q的元素个数,即队长
GetHead(Q,&e)
条件:Q为非空队列
操作结果:用e返回Q的队头元素
EnQueue(&Q,e)
条件:队列Q已存在
操作结果:插入元素e为Q的队尾元素
DeQueue(&Q,&e)
条件:Q为非空队列
操作结果:删除Q的队头元素,用e返回值
队列的顺序存储方式实现
存储方式:
同一般线性表的顺序存储结构完全相同利用一组地址连续的存储单元依次存放自队列头到队尾的数据元素。队尾一般在低地址端
-
设front指针,指示队头元素在顺序栈中的位置
-
设rear指针,指示队尾元素在顺序栈中的位置
状态:
- front==rear表示队空
- rear=QLength(队列长度)表示队满
顺序队的代码实现
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define MAX_SIZE 100 // 队列的最大长度
typedef int ElemType; // 定义队列元素的数据类型
typedef struct
{
ElemType* data; // 存储队列元素的数组
int front; // 队头指针,指向队头元素
int rear; // 队尾指针,指向队尾元素的下一个位置
} SqQueue;
// 构造空队列Q
void InitQueue(SqQueue* Q)
{
Q->data = (ElemType*)malloc(MAX_SIZE * sizeof(ElemType)); // 动态分配数组空间
Q->front = Q->rear = 0; // 初始化队头和队尾指针
}
// 队列Q销毁
void DestroyQueue(SqQueue* Q)
{
free(Q->data); // 释放队列的数组空间
Q->data = NULL; //将指针置空
Q->front = Q->rear = 0; // 初始化队头和队尾指针
}
// 将队列Q清空
void ClearQueue(SqQueue* Q)
{
Q->front = Q->rear = 0; // 将队头和队尾指针置为0,表示队列为空
}
// 返回队列Q的元素个数,即队列的长度
int QueueLength(SqQueue Q)
{
return Q.rear - Q.front;
}
// 用e返回队列Q的队头元素
bool GetHead(SqQueue Q, ElemType* e)
{
if (Q.front == Q.rear) // 队列为空,无法取出队头元素
{
return false;
}
*e = Q.data[Q.front]; // 将队头元素赋值给e
return true;
}
// 插入元素e为队列Q的队尾元素
bool EnQueue(SqQueue* Q, ElemType e)
{
if ((Q->rear + 1) % MAX_SIZE == Q->front) // 队列已满,插入失败
{
return false;
}
Q->data[Q->rear] = e; // 将元素e插入队尾
Q->rear++; // 更新队尾指针
return true;
}
// 删除队列Q的队头元素,并用e返回值
bool DeQueue(SqQueue* Q, ElemType* e)
{
if (Q->front == Q->rear) // 队列为空,删除失败
{
return false;
}
*e = Q->data[Q->front]; // 将队头元素赋值给e
Q->front++; // 更新队头指针
return true;
}
循环队列
如果仔细观察上面的代码,不难发一个问题:
当我门进行出队操作时,front+1后之前的空间便无法利用,如果此时队满了,出现上溢情况,但实际数组中却还有空间无法被我们使用,我们把这种情况称为假上溢。
假上溢会极大地浪费我们的内存空间,我们该如何解决这个问题呢?
解决假上溢的方法
- 将队中元素依次向队头方向移动。缺点:浪费时间每移动一次,队中元素都要移动
- 将队空间设想成一个循环的表,即分配给队列的n个存储单元可以循环使用,当rear为maxqsize时,若数组的开始端空着,从头开始使用空着的空间。
- base[0]接在base[MAXQSiZE-1]之后,若rear+1==M,则令rear=0
实现方法:利用模(%)运算。
- 插入元素:
Q.base[Q.rear]=x;
Q.rear=(Q.rear+1)% MAXQSIZE; - 删除元素:
X=Q.base[s.front];
Q.front=(Q.front+1)%MAXQSIZE;
不难发现,循环队列中队空为:rear==front,队满也为rear==front,该如判断队空队满?
- 另外设一个标志以区别队空、队满
- 另设一个变量,记录元素个数
- 少用一个先素空间
- 队空:rear==front
- 队满:(rear+1)%MAXQSIXZE==front
代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define MAX_SIZE 100 // 循环队列的最大长度
typedef int ElemType; // 定义队列元素的数据类型
typedef struct
{
ElemType* data; // 存储队列元素的数组
int front; // 队头指针,指向队头元素
int rear; // 队尾指针,指向队尾元素的下一个位置
int size; // 队列元素个数
} SqQueue;
// 构造空队列Q
void InitQueue(SqQueue* Q)
{
Q->data = (ElemType*)malloc(MAX_SIZE * sizeof(ElemType)); // 动态分配数组空间
Q->front = Q->rear = 0; // 初始化队头和队尾指针
Q->size = 0; // 初始化队列元素个数
}
// 队列Q销毁
void DestroyQueue(SqQueue* Q)
{
free(Q->data); // 释放队列的数组空间
Q->data = NULL; //将指针置空
Q->front = Q->rear = 0; // 初始化队头和队尾指针
Q->size = 0; // 将队列元素个数置为0,表示队列为空
}
// 将队列Q清空
void ClearQueue(SqQueue* Q)
{
Q->front = Q->rear = 0; // 将队头和队尾指针置为0,表示队列为空
Q->size = 0; // 将队列元素个数置为0,表示队列为空
}
// 返回队列Q的元素个数,即队列的长度
int QueueLength(SqQueue Q)
{
return Q.size;
}
// 用e返回队列Q的队头元素
bool GetHead(SqQueue Q, ElemType* e)
{
if (Q.front == Q.rear) // 队列为空,无法取出队头元素
return false;
*e = Q.data[Q.front]; // 将队头元素赋值给e
return true;
}
// 插入元素e为队列Q的队尾元素
bool EnQueue(SqQueue* Q, ElemType e)
{
if ((Q->rear + 1) % MAX_SIZE == Q->front) // 队列已满,插入失败
return false;
Q->data[Q->rear] = e; // 将元素e插入队尾
Q->rear = (Q->rear + 1) % MAX_SIZE; // 更新队尾指针
Q->size++; // 队列元素个数加1
return true;
}
// 入队,插入元素e为队列Q的队尾元素
bool EnQueue(SqQueue* Q, ElemType e)
{
if ((Q->rear + 1) % MAX_SIZE == Q->front) // 队列已满,插入失败
return false;
Q->data[Q->rear] = e; // 将元素e插入队尾
Q->rear = (Q->rear + 1) % MAX_SIZE; // 更新队尾指针
Q->size++; // 队列元素个数加1
return true;
}
// 出队,删除队列Q的队头元素,并用e返回值
bool DeQueue(SqQueue* Q, ElemType* e)
{
if (Q->front == Q->rear) // 队列为空,删除失败
return false;
*e = Q->data[Q->front]; // 将队头元素赋值给e
Q->front = (Q->front + 1) % MAX_SIZE; // 更新队头指针
Q->size--; // 队列元素个数减1
return true;
}
队列的链式存储方式
- 当我们无法估计队列长度时,一般采用链式队列
存储方式
- 链栈是运算受限的单链表,在链表头部和尾部进行操作
代码实现
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int ElemType; // 定义队列元素的数据类型
// 队列结点类型
typedef struct QNode
{
ElemType data; // 结点的数据域
struct QNode* next; // 结点的指针域
}QNode, * QuenePtr;
// 链式队列类型
typedef struct
{
QuenePtr front; // 队头指针,指向队头结点
QuenePtr rear; // 队尾指针,指向队尾结点
}LinkQuene;
// 初始化队列Q
void InitQuene(LinkQuene* Q)
{
Q->front = Q->rear = (QuenePtr)malloc(sizeof(QNode)); // 创建头结点
if (!Q->front) // 内存分配失败
exit(1);
Q->front->next = NULL; // 头结点的指针域置空
}
// 销毁队列Q
void DestroyQuene(LinkQuene* Q)
{
QuenePtr p = Q->front;
while (p) // 逐个释放结点内存
{
Q->front = p->next;
free(p);
p = Q->front;
}
Q->front = Q->rear = NULL; // 将队列指针置空
}
// 返回队列Q的元素个数,即队列的长度
int QueneLength(LinkQuene Q)
{
int count = 0;
QuenePtr p = Q.front->next;
while (p) // 遍历队列计数
{
count++;
p = p->next;
}
return count;
}
// 用e返回队列Q的队头元素
bool GetHead(LinkQuene Q, ElemType* e)
{
if (Q.front == Q.rear) // 队列为空,无法取出队头元素
return false;
*e = Q.front->next->data; // 将队头元素赋值给e
return true;
}
// 入队操作
bool EnQuene(LinkQuene* Q, ElemType e)
{
QuenePtr p = (QuenePtr)malloc(sizeof(QNode)); // 创建新结点
if (!p) // 内存分配失败
return false;
p->data = e; // 将元素e存储到新结点中
p->next = NULL; // 将新结点的next指针置为NULL
Q->rear->next = p; // 将新结点链接到队尾结点的后面
Q->rear = p; // 将队尾指针指向新结点
return true;
}
// 出队操作
bool DeQuene(LinkQuene* Q, ElemType* e)
{
if (Q->front == Q->rear) // 队列为空,无法出队
return false;
QuenePtr p = Q->front->next; // 指向队头结点
*e = p->data; // 将队头元素赋值给e
Q->front->next = p->next; // 将队头结点删除
if (Q->rear == p) // 如果队列只有一个元素,出队后队列为空,需要将队尾指针指向头结点
Q->rear = Q->front;
free(p); // 释放出队的结点的空间
return true;
}
总结
栈和队列是两种常用的数据结构,都是操作方式受限的线性表,都支持快速插入和删除元素,但它们的特点和应用场景略有不同。
栈(Stack)是一种后进先出(Last In First Out, LIFO)的数据结构,通常用数组或链表实现。栈有两个基本操作:入栈(push)和出栈(pop)。在栈中,只能访问最后一个入栈的元素,不能访问其他元素。栈的典型应用包括:表达式求值、函数调用、括号匹配等。
队列(Queue)是一种先进先出(First In First Out, FIFO)的数据结构,通常用数组或链表实现。队列有两个基本操作:入队(enQueue)和出队(deQueue)。在队列中,只能访问最先入队的元素,不能访问其他元素。队列的典型应用包括:进程调度、广度优先搜索、缓存等。
在实际应用中,栈和队列的应用十分广泛。例如,在计算机底层,操作系统利用栈来保存程序调用信息和临时变量;在图形学中,使用栈来实现画图的撤销和重做操作;在网络通信中,使用队列来实现数据包的存储和转发。掌握栈和队列的基本操作及其应用,是编程中必不可少的基本功。