初级数据结构:栈和队列

一、栈

(一)、栈的定义

栈是一种遵循后进先出(LIFO,Last In First Out)原则的数据结构。栈的主要操作包括入栈(Push)和出栈(Pop)。入栈操作是将元素添加到栈顶,这一过程中,栈顶指针上移,新元素被放置在栈顶位置;出栈操作则是移除栈顶元素,同时栈顶指针下移。此外,还可以通过获取栈顶元素(Top)操作来查看栈顶元素但不将其移除。

形象的来说,压栈操作就像堆叠盘子,一个盘子放在另一个盘子上。当你想取出盘子时,你必定会从顶部取出。如果从中间取盘子,那就有盘子打烂的风险,这也就是出栈操作。

栈也是一种线性表,在对于表达式求值和符号匹配等方面有很大用途。

如果你已经学完了顺序表,那么栈的实现对你来说轻而易举。

(二)、栈的功能

1、压栈(Push):将一个元素添加到栈顶。比如我们往弹夹里装子弹的动作就相当于入栈操作。

2、出栈(Pop):从栈顶移除一个元素。对应弹夹射击时弹出子弹的过程。

3、获取栈顶元素(Peek):获取栈顶元素,但不将其从栈中移除。这就像是我们查看弹夹最上面的子弹是什么类型,但不把它射出。

4、获取栈中有效元素个数(Size):就像我们查看弹夹中的子弹数目。

5、判断栈是否为空(IsEmpty):检查栈中是否没有元素。当弹夹里没有子弹时,就可以说栈为空。

(三)、栈的实现

对于栈来说,我们可以使用数组或者链表来实现栈。相对而言,使用数组来实现栈比用链表来实现更优。因为进行栈的压栈操作时,数组尾部插入数据的代价更小。而如果使用双向链表又过于麻烦。因此,我们对于栈的结构体定义如下:

cpp 复制代码
typedef struct Stack
{
	int* data;//动态数组
	int top;//指向栈顶元素,在后续的初始化中,将其初始化为0还是-1,决定着top指向栈顶元素还是指向栈顶元素后一位
	int capicity;//容量大小

}Stack;

1.栈的初始化

和顺序表一样,我们需要先进行动态内存申请一定的空间代码如下:

cpp 复制代码
void StackInit(Stack* ps)
{
    //动态内存申请4个整形空间,空间申请小一些方便检查后续扩容是否正确
	int* ptr = (int*)malloc(sizeof(int) * 4);
	if (ptr == NULL)
	{
        //判断空间是否申请成功,失败则打印错误信息
    	perror("StackInit::malloc");
		return;
	}
	ps->data = ptr;
	ps->capicity = 4;
    //我这里是让top指向栈顶元素的后一位,看自己的想法
    //这里top的指向如何会影响后续获取栈顶元素功能实现的代码
	ps->top = 0;
}

2.动态扩容

这个动态扩容由于我们使用数组来实现栈,因此动态扩容函数与顺序表基本一致,代码如下:

cpp 复制代码
void Expansion(Stack* ps)
{
	assert(ps);
	if (ps->top == ps->capicity)
	{
    	printf("空间不足\n");
		int* ptr = (int*)realloc(ps->data, sizeof(int) * (ps->capicity * 2));
		if (ptr == NULL)
		{
			perror("Expansion::realloc");
			return;
		}
		ps->data = ptr;
        capicity*=2;
	}
}

3.压栈操作

实质上就是顺序表的尾插。代码如下:

cpp 复制代码
void StackPush(Stack* ps)
{
	assert(ps);
    //判断是否需要扩容
	Expansion(ps);
	printf("请输入数字\n");
    //如果你的top初始化为-1,那么这里就需要先top++,再赋值
	scanf("%d", &ps->data[ps->top]);
	printf("入栈成功\n");
	ps->top++;
}

4.出栈操作

实质上就是顺序表的尾删,直接使top指针往前移一步,等下次压栈操作后,数据覆盖即可达到出栈作用,代码如下:

cpp 复制代码
void StackPop(Stack* ps)
{
	assert(ps);
	if (ps->top > 0)
	{
		ps->top--;
		printf("出栈成功\n");
	}
	else
	{
		printf("栈中无元素\n");
	}
}

5.获取栈顶元素

因为我们这里top初始化为0,所以top一直指向栈顶元素后一位,如果我们想要获取栈顶元素就需要使top减一,代码如下:

cpp 复制代码
int StackTop(Stack* ps)
{
	assert(ps);
	return ps->data[ps->top-1];
}

6.获取栈顶元素的有效个数

直接返回top即可,代码如下:

cpp 复制代码
int StackSize(Stack* ps)
{
	assert(ps);
	return ps->top;
}

7.检查栈是否为空

当top与初始化的top数相等时,栈就为空,代码如下:

cpp 复制代码
int StackEmpty(Stack* ps)
{
	assert(ps);
	if (ps->top == 0)
	{
		return 0;
	}
	else
	{
		return ps->top;
	}
}

8.栈的销毁

和顺序表的销毁一致,先释放栈的空间,然后将指针data置空,容量也即capicity和top都置为0。代码如下:

cpp 复制代码
void StackDestroy(Stack* ps)
{
	assert(ps);
	free(ps->data);
	ps->data = NULL;
	ps->capicity = 0;
	ps->top = 0;
	printf("销毁成功\n");
}

至此,一个基础的栈就实现了,完整代码如下。

9.完整代码

stack.h中:

cpp 复制代码
#pragma once
#pragma warning(disable : 4996)
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef struct Stack
{
	int* data;
	int top;
	int capicity;
}Stack;
// 初始化栈 
void StackInit(Stack* ps);
//动态扩容
void Expansion(Stack* ps);
// 入栈 
void StackPush(Stack* ps);
// 出栈 
void StackPop(Stack* ps);
// 获取栈顶元素 
int StackTop(Stack* ps);
// 获取栈中有效元素个数 
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps);
// 销毁栈 
void StackDestroy(Stack* ps);

Fstack.c中:

cpp 复制代码
#include"stack.h"
void StackInit(Stack* ps)
{
	int* ptr = (int*)malloc(sizeof(int) * 4);
	if (ptr == NULL)
	{
		perror("StackInit::malloc");
		return;
	}
	ps->data = ptr;
	ps->capicity = 4;
	ps->top = 0;
}

void Expansion(Stack* ps)
{
	assert(ps);
	if (ps->top == ps->capicity)
	{
		printf("空间不足\n");
		int* ptr = (int*)realloc(ps->data, sizeof(int) * (ps->capicity * 2));
		if (ptr == NULL)
		{
			perror("Expansion::realloc");
			return;
		}
		ps->data = ptr;
		ps->capicity*=2; 
	}
}

void StackPush(Stack* ps)
{
	assert(ps);
	Expansion(ps);
	printf("请输入数字\n");
	scanf("%d", &ps->data[ps->top]);
	printf("入栈成功\n");
	ps->top++;
}

void StackPop(Stack* ps)
{
	assert(ps);
	if (ps->top > 0)
	{
		ps->top--;
		printf("出栈成功\n");
	}
	else
	{
		printf("栈中无元素\n");
	}
}

int StackTop(Stack* ps)
{
	assert(ps);
	return ps->data[ps->top-1];
}

int StackSize(Stack* ps)
{
	assert(ps);
	return ps->top;
}

int StackEmpty(Stack* ps)
{
	assert(ps);
	if (ps->top == 0)
	{
		return 0;
	}
	else
	{
		return ps->top;
	}
}

void StackDestroy(Stack* ps)
{
	assert(ps);
	free(ps->data);
	ps->data = NULL;
	ps->capicity = 0;
	ps->top = 0;
	printf("销毁成功\n");
}

stack.h:

cpp 复制代码
#include"stack.h"
Stack ps;
int main()
{
	// 初始化栈 
	StackInit(&ps);
	int a,b;
	do
	{
		printf("请输入数字\n");
		scanf("%d", &a);
		switch (a)
		{
		case 1:
			// 入栈 
			StackPush(&ps);
			break;
		case 2:
			// 出栈 
			StackPop(&ps);
			break;
		case 3:
			// 获取栈顶元素 
			b = StackTop(&ps);
			printf("栈顶元素:%d\n", b);
			break;
		case 4:
			// 获取栈中有效元素个数 
			printf("%d\n", StackSize(&ps));
			break;
		case 5:
			// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
			printf("%d\n", StackEmpty(&ps));
			break;
		case 0:
			// 销毁栈 
			StackDestroy(&ps);
			printf("退出\n");
			break;
		}
	} while (a);
	return 0;
}

二、队列

(一)、队列的定义

在数据结构的大家庭中,队列是一位独特而重要的成员。想象一下,在超市结账的队伍里,先来的顾客排在前面先结账离开,后来的顾客则依次在队尾加入等待,这就是典型的 "先进先出(FIFO,First In First Out)原则,而队列正是这种原则在计算机领域的完美体现。

队列作为一种特殊的线性表,它的操作被严格限定在两端进行。一端被称为队尾(rear),专门用于插入新元素,就像新顾客加入结账队伍的末尾;另一端是队头(front),负责删除元素,恰似排在队伍最前面的顾客完成结账后离开。这种操作受限的特性,赋予了队列先进先出的独特性质,也使得它在众多算法和实际应用中发挥着关键作用 。无论是操作系统中的任务调度、网络通信中的数据包处理,还是广度优先搜索算法中的节点遍历,队列都扮演着至关重要的角色

和栈一样,它的实现同样可以使用数组和链表来实现。因为上述我们使用了数组来实现栈,故此次队列的实现我们采用链表来实现,也即链式队列。

在链式队列中,每个节点包含数据域和指针域,数据域用于存储队列元素,指针域则指向下一个节点。队列通过两个指针来管理:头指针(front)指向链表的头节点,代表队头;尾指针(rear)指向链表的尾节点,代表队尾 。

入队操作时,创建一个新节点存储新元素,然后将其插入到链表的尾部,同时更新尾指针指向新节点;出队操作则是删除链表头部的节点,返回该节点的数据,并更新头指针指向下一个节点。比如,有一个初始为空的链式队列,当元素 3 入队时,创建一个新节点存储 3,此时头指针和尾指针都指向这个新节点 。接着元素 4 入队,创建新节点并插入到链表尾部,尾指针更新指向新节点。当出队时,删除头指针指向的节点(包含元素 3),头指针移动到下一个节点(包含元素 4) 。

链式队列的优点是不需要预先知道队列的最大容量,因为链表可以动态地分配内存,避免了顺序队列可能出现的溢出问题;而且在进行插入和删除操作时,只需要修改指针,不需要移动大量元素,效率较高。然而,链式队列也有缺点,由于每个节点都需要额外的指针域来指向下一个节点,这会占用更多的内存空间;并且链表不支持随机访问,访问特定位置的元素需要从头开始遍历链表,时间复杂度较高 。

(二)、队列的功能

1、队尾入队列

2、队头出队列

3、获取队列头部元素

4、获取队列尾部元素

5、获取队列有效元素个数

6、检查队列是否为空

(三)、队列的实现

因为链式队列需要头指针和尾指针,因此我们不能只像链表那样只用一个结构体。我们需要再使用一个结构体以使进行函数传参时更方便,故结构体的构造如下:

cpp 复制代码
//节点
typedef struct QueueNode
{
	int data;
	struct QueueNode* next;
}Qnode;
typedef struct Queue
{
	//头指针
	Qnode* head;
	//尾指针
	Qnode* tail;
	//计数存储数据个数
	int size;
}Queue;

1、队列初始化

cpp 复制代码
void QueueInit(Queue* q)
{
	assert(q);
	q->head = NULL;
	q->tail = NULL;
	q->size = 0;
}

因为还未存储数据,故头指针和尾指针都置空,以防止野指针出现。不要忘记将size也初始化为0;

2、队尾入队列

先看图:

如果我们是往队列中插入第一个节点,此时头指针和尾指针都指向空。我们就需要将头指针和尾指针都指向新节点,再将节点的next指针指向NULL;

而如果我们插入新节点时,队列中已有数据存储,也即有节点存在,将上述两图结合起来看,第一张代表插入新节点前,第二张代表插入新节点后。我们需要先使尾指针指向的节点的next指针指向新节点,再让尾指针指向新节点,最后再使新节点置空即可。代码如下:

cpp 复制代码
void QueuePush(Queue* q)
{
	assert(q);
	//申请新节点
	Qnode* ptr = (Qnode*)malloc(sizeof(Qnode));
	if (ptr == NULL)
	{
		perror("QueuePush::malloc");
		return;
	}
	printf("请输入数字\n");
	scanf("%d", &ptr->data);
	//提前将新节点next指针置空
	ptr->next = NULL;
	//判断队列是否为空
	if (q->head == NULL && q->tail == NULL)
	{
		q->head = q->tail = ptr;
	}
	else
	{
		q->tail->next = ptr;
		q->tail = ptr;
	}
	q->size++;
}

3、队头出队列

先看代码:

cpp 复制代码
void QueuePop(Queue* q)
{
	assert(q);
	//判断头指针是否为空,为空那还出什么队列
	assert(q->head);
	//先存储头指针指向的节点的下一个节点的位置
	Qnode* headnext = q->head->next;
	//释放头指针指向的节点空间
	free(q->head);
	//再让头指针指向之前存储的节点
	q->head = headnext;
	//如果队列中只有一个节点,那释放空间后,头指针是空,但
	//尾指针没有被置为空,而是处于野指针状态,因此也要将
	//尾指针置空
	if (q->head == NULL)
	{
		q->tail = NULL;
	}
	q->size--;
	printf("出队列成功\n");
}

4、获取队列头部元素

我们知道在链式队列中,链表头即是队头,链表尾即是队尾。获取队列头部元素即可以直接通过头指针获取。代码如下:

cpp 复制代码
int QueueFront(Queue* q)
{
	assert(q);
	if (q->head == NULL)
	{
		printf("队列无元素\n");
		return NULL;
	}
	return q->head->data;
}

5、获取队列尾部元素

和获取队列头部元素一致,更改指针即可。代码如下:

cpp 复制代码
int QueueBack(Queue* q)
{
	assert(q);
	if (q->tail == NULL)
	{
		printf("队列无元素\n");
		return NULL;
	}
	return q->tail->data;
}

6、获取队列有效元素个数

cpp 复制代码
int QueueSize(Queue* q)
{
	assert(q);
	return q->size;
}

7、检查队列是否为空

为空返回0,不为空返回非零结果。

cpp 复制代码
int QueueEmpty(Queue* q)
{
	assert(q);
	return q->size;
}

8、队列的销毁

队列的销毁和链表的销毁一致,遍历一遍,在遍历中存储下一个节点的位置,销毁当前节点,更新条件即可。代码如下:

cpp 复制代码
void QueueDestroy(Queue* q)
{
	assert(q);
	Qnode* ptr = q->head;
	if (q->head == NULL)
	{
		return;
	}
	while (ptr)
	{
		Qnode* ptrnext = ptr->next;
		free(ptr);
		ptr = ptrnext;
	}
	q->head = q->tail = NULL;
	printf("队列销毁成功\n");
	q->size = 0;
}

至此,一个基础的队列就完成了,完整代码如下:

9、完整代码

queue.h:

cpp 复制代码
#pragma once
#pragma warning(disable : 4996)
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//节点
typedef struct QueueNode
{
	int data;
	struct QueueNode* next;
}Qnode;
typedef struct Queue
{
	//头指针
	Qnode* head;
	//尾指针
	Qnode* tail;
	//计数存储数据个数
	int size;
}Queue;
// 初始化队列 
void QueueInit(Queue* q);
// 队尾入队列 
void QueuePush(Queue* q);
// 队头出队列 
void QueuePop(Queue* q);
// 获取队列头部元素 
int QueueFront(Queue* q);
// 获取队列队尾元素 
int QueueBack(Queue* q);
// 获取队列中有效元素个数 
int QueueSize(Queue* q);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* q);
// 销毁队列 
void QueueDestroy(Queue* q);

Fqueue.c:

cpp 复制代码
#include"queue.h"
void QueueInit(Queue* q)
{
	assert(q);
	q->head = NULL;
	q->tail = NULL;
	q->size = 0;
}

void QueuePush(Queue* q)
{
	assert(q);
	//申请新节点
	Qnode* ptr = (Qnode*)malloc(sizeof(Qnode));
	if (ptr == NULL)
	{
		perror("QueuePush::malloc");
		return;
	}
	printf("请输入数字\n");
	scanf("%d", &ptr->data);
	//提前将新节点next指针置空
	ptr->next = NULL;
	//判断队列是否为空
	if (q->head == NULL && q->tail == NULL)
	{
		q->head = q->tail = ptr;
	}
	else
	{
		q->tail->next = ptr;
		q->tail = ptr;
	}
	q->size++;
}

void QueuePop(Queue* q)
{
	assert(q);
	//判断头指针是否为空,为空那还出什么队列
	assert(q->head);
	//先存储头指针指向的节点的下一个节点的位置
	Qnode* headnext = q->head->next;
	//释放头指针指向的节点空间
	free(q->head);
	//再让头指针指向之前存储的节点
	q->head = headnext;
	//如果队列中只有一个节点,那释放空间后,头指针是空,但
	//尾指针没有被置为空,而是处于野指针状态,因此也要将
	//尾指针置空
	if (q->head == NULL)
	{
		q->tail = NULL;
	}
	q->size--;
	printf("出队列成功\n");
}

int QueueFront(Queue* q)
{
	assert(q);
	if (q->head == NULL)
	{
		printf("队列无元素\n");
		return NULL;
	}
	return q->head->data;
}

int QueueBack(Queue* q)
{
	assert(q);
	if (q->tail == NULL)
	{
		printf("队列无元素\n");
		return NULL;
	}
	return q->tail->data;
}

int QueueSize(Queue* q)
{
	assert(q);
	return q->size;
}

int QueueEmpty(Queue* q)
{
	assert(q);
	return q->size;
}

void QueueDestroy(Queue* q)
{
	assert(q);
	Qnode* ptr = q->head;
	if (q->head == NULL)
	{
		return;
	}
	while (ptr)
	{
		Qnode* ptrnext = ptr->next;
		free(ptr);
		ptr = ptrnext;
	}
	q->head = q->tail = NULL;
	printf("队列销毁成功\n");
	q->size = 0;
}

queue.c:

cpp 复制代码
#include"queue.h"
Queue Que;
int main()
{
	int a;
	// 初始化队列 
	QueueInit(&Que);
	do
	{
		printf("输入数字\n");
		scanf("%d", &a);
		switch (a)
		{
		case 1:
			// 队尾入队列 
			QueuePush(&Que);
			break;
		case 2:
			// 队头出队列 
			QueuePop(&Que);
			break;
		case 3:
			// 获取队列头部元素 
			printf("%d\n",QueueFront(&Que));
			break;
		case 4:
			// 获取队列队尾元素 
			printf("%d\n",QueueBack(&Que));
			break;
		case 5:
			// 获取队列中有效元素个数 
			printf("%d\n",QueueSize(&Que));
			break;
		case 6:
			// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
			printf("%d\n",QueueEmpty(&Que));
			break;
		case 0:
			// 销毁队列 
			QueueDestroy(&Que);
			break;
		}
	} while (a);
	return 0;
}

(四)、循环队列

在了解顺序队列时,我们提到了假溢出问题,而循环队列就是为了解决这个问题而引入的。循环队列是一种特殊的顺序队列,它将顺序队列的首尾相连,把存储队列元素的表从逻辑上看成一个环。

在循环队列中,我们仍然使用front指针指示队头位置,rear指针指示队尾位置。当rear指针到达数组末尾时,如果数组前面还有空闲空间,它可以重新回到数组开头,继续利用前面的空闲空间。例如,假设我们有一个大小为 5 的循环队列数组,初始时front和rear都为 0 。当进行入队操作时,rear指针依次移动到 1、2、3、4 位置。当rear到达 4 时,如果再进行入队操作,rear就会回到 0 位置(通过取模运算实现)。

判断循环队列是否为空和满的条件与普通顺序队列有所不同。在循环队列中,当front等于rear时,表示队列为空;而当(rear + 1) % 队列容量 == front时,表示队列已满。这里的取模运算保证了rear指针在到达数组末尾时能够回到开头,实现循环的效果。

三、栈和队列的比较

栈和队列虽然都是线性数据结构,但它们在很多方面存在差异:

  1. 进出顺序:栈遵循后进先出(LIFO)原则,最后进入栈的元素最先出栈;而队列遵循先进先出(FIFO)原则,最先进入队列的元素最先出队 。例如,在程序调用栈中,函数调用是按照后进先出的顺序进行的,而在网络请求队列中,请求是按照先进先出的顺序被处理的。
  2. 插入删除操作:栈的插入(入栈)和删除(出栈)操作都在栈顶进行;队列的插入(入队)操作在队尾进行,删除(出队)操作在队头进行 。比如,往栈中添加元素就像往一摞盘子上放盘子,只能放在最上面,从栈中取出元素也只能从最上面取;而队列中添加元素就像排队买票,新来的人站在队伍末尾,离开的人从队伍最前面离开。
  3. 遍历数据速度:栈只能从栈顶开始遍历,若要访问栈底元素,需要依次弹出栈顶元素,遍历过程中需要开辟临时空间来保存数据状态,以确保遍历前后数据的一致性;队列基于地址指针进行遍历,可以从队头或队尾开始遍历,但不能同时进行双向遍历,遍历过程中不会改变数据结构,所以无需开辟额外空间,遍历速度相对较快 。例如,在遍历一个包含 100 个元素的栈时,如果要获取栈底元素,需要将栈顶的 99 个元素依次弹出并保存,然后才能访问栈底元素,最后再将弹出的元素依次压回栈中;而遍历一个包含 100 个元素的队列时,从队头开始遍历,直接按照顺序访问每个元素即可。
  4. 限定条件:栈只允许在一端进行插入和删除操作;队列允许在一端插入,在另一端删除操作 。这是它们最基本的操作限制,决定了它们在不同场景下的适用性。例如,在实现表达式求值时,利用栈的特性可以方便地处理运算符优先级;而在实现任务调度时,利用队列的特性可以保证任务按照提交顺序依次执行。
  5. 应用场景:栈常用于处理具有后进先出特性的问题,如函数调用、表达式求值、括号匹配等;队列常用于处理具有先进先出特性的问题,如网络请求处理、消息队列、任务调度等 。例如,在编译器中,使用栈来处理函数调用和递归,确保函数的正确返回和局部变量的正确管理;在分布式系统中,使用消息队列来异步处理消息,提高系统的吞吐量和响应速度。

如果你认为你已经对栈和队列掌握完全,那你可以尝试做一下下面的题目看看:

1.有效的括号

2.用队列实现栈

3.用栈实现队列

4.设计循环队列

相关推荐
taopi202433 分钟前
android java系统弹窗的基础模板
android·java·开发语言
釉色清风36 分钟前
【matlab】绘图 离散数据--->连续函数
开发语言·matlab
松仔log1 小时前
Java多线程——对象的组合
java·开发语言·jvm
Qhumaing1 小时前
Python学习——函数参数详解
开发语言·python·学习
qq_433618441 小时前
哈夫曼树
数据结构·算法
Icomi_1 小时前
【PyTorch】7.自动微分模块:开启神经网络 “进化之门” 的魔法钥匙
c语言·c++·人工智能·pytorch·python·机器学习·计算机视觉
ElvInR2 小时前
【C语言】动态内存管理
c语言·开发语言
加油,旭杏2 小时前
【go语言】grpc 快速入门
开发语言·后端·golang
行路见知2 小时前
1.4 Go 数组
开发语言
2501_903238652 小时前
Java 9模块开发:Eclipse实战指南
java·开发语言·eclipse·个人开发