【数据结构与算法】栈和队列

78 阅读7分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的11篇文章,点击查看活动详情

🔥 本文由 程序喵正在路上 原创,在稀土掘金首发!
💖 系列专栏:数据结构与算法
🌠 首发时间:2022年9月28日
🦋 欢迎关注🖱点赞👍收藏🌟留言🐾
🌟 一以贯之的努力 不得懈怠的人生

栈的定义

线性表是具有相同数据类型的 n (n≥0) 个数据元素的有限序列,其中 n 为表长,当 n=0 时线性表是一个空表。若用 L 命名线性表,则其一般表示为

L = (a1, ... , ai-1, ai, ai+1, ... , an

栈(Stack)是只允许在一端进行插入或删除操作的线性表,没有存放数据的栈称为空栈,栈顶允许插入和删除,栈底不允许插入和删除

image.png

栈的特点:后进先出

栈的基本操作

操作描述
InitStack(&S)初始化栈。构造一个空栈 S,分配内存空间
DestroyStack(&L)销毁栈。销毁并释放栈 S 所用的内存空间
Push(&S, x)进栈,若栈 S 未满,则将 x 加入使之成为新栈顶
Pop(&S, &x)出栈,若栈 S 非空,则弹出栈顶元素,并用 x 返回
GetTop(S, &x)读栈顶元素。若栈 S 非空,则用 x 返回栈顶元素
StackEmpty(S)判断一个栈 S 是否为空,若 S 为空,则返回 true,否则返回 false

栈的常考题型

进栈顺序为:a ➔ b ➔ c ➔ d ➔ e

有哪些合法的出栈顺序?

比如: e ➔ d ➔ c ➔ b ➔ a b ➔ e ➔ d ➔ c ➔ a

公式

n 个不同元素进栈,出栈顺序不同排列的个数为 1n+1C2nn\frac{1}{n + 1}{C^n_{2n}},这个公式成为卡特兰数,可采用数学归纳法证明

所以上面那个例子的出栈顺序不同排列的个数为

15+1C105=10×9×8×7×66×5×4×3×2×1=42\frac{1}{5 + 1}{C^5_{10}} = \frac{10 \times 9 \times 8 \times 7 \times 6}{6 \times 5 \times 4 \times 3 \times 2 \times 1} = 42

顺序栈的实现

顺序栈的初始化

#define MaxSize 10		//定义栈中元素的最大个数
typedef int ElemType;

typedef struct{
	ElemType data[MaxSize];	//静态数组存放栈中元素
	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, ElemType x) {
	if (S.top == MaxSize - 1)		//栈满
		return false;

	S.top = S.top + 1;		//指针先加1
	S.data[S.top] = x;		//新元素进栈
	return true;
}

下面这两行代码也可以写成一行

S.top = S.top + 1;		//指针先加1
S.data[S.top] = x;		//新元素进栈
S.data[++S.top] = x;

出栈操作

//出栈
bool Pop(SqStack &S, ElemType &x) {
	if (S.top == -1)	//栈空
		return false;

	x = S.data[S.top];	//栈顶元素先出栈
	--S.top;			//指针再减1
	return true;
}

同样地,下面这两行代码也可以写成一行

x = S.data[S.top];	//栈顶元素先出栈
--S.top;			//指针再减1
x = S.data[S.top--];

读取栈顶元素

//读取栈顶元素
bool GetTop(SqStack S, ElemType &x) {
	if (S.top == -1)	//栈空
		return false;

	x = S.data[S.top];		//x记录栈顶元素
	return true;
}

另一种方式

前面我们的顺序栈的栈顶指针是指向栈顶元素,下面我们用栈顶指针指向栈顶元素的上一个位置的方式来写

#define MaxSize 10		//定义栈中元素的最大个数
typedef int ElemType;

typedef struct{
	ElemType data[MaxSize];	//静态数组存放栈中元素
	int top;				//栈顶指针
}SqStack;

//初始化栈
void InitStack(SqStack &S) {
	S.top = 0;		//初始化栈顶指针
}

//判断栈是否为空
bool StackEmpty(SqStack S) {
	if (S.top == 0)	//栈空
		return true;
	else
		return false;
}

//进栈
bool Push(SqStack &S, ElemType x) {
	if (S.top == MaxSize)		//栈满
		return false;

	S.data[S.top++] = x;
	return true;
}

//出栈
bool Pop(SqStack &S, ElemType &x) {
	if (S.top == 0)	//栈空
		return false;

	x = S.data[--S.top];	
	return true;
}

//读取栈顶元素
bool GetTop(SqStack S, ElemType &x) {
	if (S.top == 0)	//栈空
		return false;

	x = S.data[--S.top];		//x记录栈顶元素
	return true;
}

共享栈

共享栈就是两个栈共享同一片空间

image.png

#define MaxSize 10
typedef int ElemType;

typedef struct{
	ElemType data[MaxSize];
	int top0;		//0号栈栈顶指针
	int top1;		//1号栈栈顶指针
}ShStack;

//初始化栈
void InitStack(ShStack &S) {
	S.top0 = -1;
	S.top1 = MaxSize;
}

栈满的条件:top0 + 1 = top1

是不是挺有意思的

链栈的实现

链栈的定义

typedef int ElemType;

typedef struct LinkNode{
	ElemType data;		//数据域
	struct LinkNode *next;	//指针域
}*LiStack;

你会发现,其实链栈的定义和单链表的定义一模一样,所以它们的基本操作的实现方式也大致一样

队列

队列(Queue)是只允许在一端进行插入,在另一端进行删除的线性表

特点 —— 先进队列的元素先出队

队列的基本操作

操作描述
InitQueue(&Q)初始化队列。构造一个空队列 Q,分配内存空间
DestroyQueue(&Q)销毁队列。销毁并释放栈 Q 所用的内存空间
EnQueue(&Q, x)入队,若队列 Q 未满,则将 x 加入使之成为新的队尾
DeQueue(&Q, &x)出队,若队列 Q 非空,则删除队头元素,并用 x 返回
GetHead(Q, &x)读对头元素。若队列 Q 非空,则用 x 返回队头元素
QueueEmpty(Q)判断一个队列 Q 是否为空,若 Q 为空,则返回 true,否则返回 false

队列的顺序实现

初始化

typedef int ElemType;
#define MaxSize 10

typedef struct{
	ElemType data[MaxSize];		//用静态数组存放队列元素
	int front, rear;			//队头指针和队尾指针
}SqQueue;

//初始化队列
void InitQueue(SqQueue &Q) {
	Q.front = Q.rear = 0;
}

//判断队列是否为空
bool QueueEmpty(SqQueue Q) {
	if (Q.rear == Q.front)	//队列为空
		return true;
	else
		return false;
}

循环队列——入队操作

//入队操作
bool EnQueue(SqQueue &Q, ElemType x) {
	if ((Q.rear + 1) % MaxSize == Q.front)	//判断队列是否已满
		return false;

	Q.data[Q.rear] = x;					//新元素插入队列 
	Q.rear = (Q.rear + 1) % MaxSize;	//队尾指针加1取模
	return true;
}

image.png

循环队列——出队操作

//出队操作
bool DeQueue(SqQueue &Q, ElemType &x) {
	if (Q.rear == Q.front)		//判断队列是否为空
		return false;

	x = Q.data[Q.front];
	Q.front = (Q.front + 1) % MaxSize;
	return true;
}

读取队头元素

//获得队头元素的值,用x返回
bool GetHead(SqQueue Q, ElemType &x) {
	if (Q.rear == Q.front)		//判断队列是否为空
		return false;

	x = Q.data[Q.front];
	return true;
}

队列元素个数:(rear + MaxSize - front) % MaxSize

判断队空/队满的其他方法

  • 可以定义一个 size 变量,初始值为 0 每次入队 size+1,每次出队 size-1,判断时根据 size 的值来判断即可
  • 定义一个 tag 变量 每次执行入队操作后,tag 的值变为 1;每次执行出队操作后,tag 的值变为 0 判断队满的条件——Q.rear == Q.front && tag == 1 判断队空的条件——Q.rear == Q.front && tag == 0

队列的链式实现

定义

typedef int ElemType;

typedef struct LinkNode{	//链式队列结点
	ElemType data;
	struct LinkNode *next;
};

typedef struct{
	LinkNode *front, *rear;		//队列的队头和队尾指针
}LinkQueue;

初始化

带头结点

//初始化队列(带头结点)
void InitQueue(LinkQueue &Q) {
	//初始时,front、rear 都指向头结点
	Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
	Q.front->next = NULL;
}

//判断队列是否为空
bool IsEmpty(LinkQueue Q) {
	if (Q.front == Q.rear)
		return true;
	else
		return false;
}

不带头结点

//初始化队列(不带头结点)
void InitQueue(LinkQueue &Q) {
	//初始时,front、rear 都指向NULL
	Q.front = Q.rear = NULL;
}

//判断队列是否为空
bool IsEmpty(LinkQueue Q) {
	if (Q.front == NULL)
		return true;
	else
		return false;
}

入队

带头结点

//入队(带头结点)
void EnQueue(LinkQueue &Q, ElemType x) {
	LinkNode *s = (LinkNode*)malloc(sizeof(LinkNode));
	s->data = x;
	s->next = NULL;
	Q.rear->next = s;	//新结点插入到rear之后
	Q.rear = s;			//修改表尾指针
}

不带头结点

//入队(不带头结点)
void EnQueue(LinkQueue &Q, ElemType x) {
	LinkNode *s = (LinkNode*)malloc(sizeof(LinkNode));
	s->data = x;
	s->next = NULL;

	//不带头结点的队列,第一个元素入队时需要特别处理
	if (Q.front == NULL) {
		Q.front = s;		//修改队头队尾指针
		Q.rear = s;
	}
	else {
		Q.rear->next = s;	//新结点插入到rear之后
		Q.rear = s;			//修改表尾指针
	}
}

出队

带头结点

//出队(带头结点)
bool DeQueue(LinkQueue &Q, ElemType &x) {
	if (Q.front == Q.rear)		//判断队列是否为空
		return false;

	LinkNode *p = Q.front->next;
	x = p->data;				//用x返回队头元素
	Q.front->next = p->next;	//修改头结点的next指针
	if (Q.rear == p)			//此次是最后一个结点出队
		Q.rear = Q.front;		//修改rear指针
	free(p);					//释放结点空间
	return true;
}

不带头结点

//出队(不带头结点)
bool DeQueue(LinkQueue &Q, ElemType &x) {
	if (Q.front == Q.rear)		//判断队列是否为空
		return false;

	LinkNode *p = Q.front;
	x = p->data;				//用x返回队头元素
	Q.front = p->next;			//修改front指针

	if (Q.rear == p)			//此次是最后一个结点出队
		Q.rear = Q.front = NULL;	

	free(p);					//释放结点空间
	return true;
}

双端队列

双端队列是允许从两端插入、两端删除的线性表,如果只从某一端插入和删除,那么它就相当于一个栈

双端队列有两种:

  1. 输入受限的双端队列——只允许从一端插入、两端删除的线性表
  2. 输出受限的双端队列——只允许从两端插入、一端删除的线性表

判断输出序列合法性

若数据元素输入序列为 1, 2, 3, 4,则哪些输出序列是合法的,哪些是非法的?

从栈、输入受限的双端队列、输出受限的双端队列 3 个方面依次判断