数据结构初阶第五篇——栈和队列(实现+图解)

Posted 呆呆兽学编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构初阶第五篇——栈和队列(实现+图解)相关的知识,希望对你有一定的参考价值。

本篇博客我要来和大家一起聊一聊数据结构中的栈和队列相关知识,一种是先进后出的结构,另一种是先进先出的结构。
博客代码已上传至:https://gitee.com/byte-binxin/data-structure/tree/master/stack_queue


栈的概念和结构

栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)加粗样式的原则。
入栈:从栈顶放入数据的操作。
出栈:从栈顶取出元素的操作。

栈的实现

栈用链表和顺序表两种数据结构都可以实现,我们要确定选择哪一种更优,我们来分析一下。
链表栈:如果选择单链表的话,我们应该选择头当栈顶,尾当栈底,不然的话,每次存取数据都要遍历一遍链表。所以选双链表会比较好一点。
数组栈:访问栈顶的时间复杂度为O(1),相比链表栈比较优。
所以下面我们用顺序表来实现栈的这种数据结构。
结构如下:

typedef int STDatatype;
typedef struct Stack
{
	STDatatype* a;
	int top;
	int capcaity;
}Stack;

栈的接口

栈要实现的接口有以下几个:

//初始化栈
void StackInit(Stack* ps);
//销毁栈
void StackDestroy(Stack* ps);
//压栈
void StackPush(Stack* ps, STDatatype x);
//出栈
void StackPop(Stack* ps);
//取出栈顶元素
STDatatype StackTop(Stack* ps);
//栈的大小
int StackSize(Stack* ps);
//判断栈是否为空
bool StackEmpty(Stack* ps);

栈的初始化

初始化栈就是把结构体中的成员都初始化一下,方便后续的扩容等操作。
具体实现如下:

void StackInit(Stack* ps)
{
	assert(ps);
	
	ps->a = NULL;
	ps->capcaity = ps->top = 0;
}

压栈

压栈就是在栈顶插入元素,其中是肯定要考虑到扩容的问题,当ps->top == ps->capcaity时,就要考虑到扩容了,扩容也是像之前顺序表那样每次扩一倍,这样可以一定程度地减少扩容次数,但同时是会带来一定的空间消耗的。

具体实现如下:

void StackPush(Stack* ps, STDatatype x)
{
	assert(ps);
	if (ps->top == ps->capcaity)
	{
		ps->capcaity = ps->capcaity == 0 ? 4 : 2 * ps->capcaity;
		STDatatype* tmp = (STDatatype*)realloc(ps->a ,ps->capcaity*sizeof(STDatatype));
		if (tmp == NULL)
		{
			printf("realloc fail\\n");
			exit(-1);
		}
		ps->a = tmp;
	}
	ps->a[ps->top] = x;
	ps->top++;

}

出栈

出栈就是在栈顶pop掉一个元素,也就是top-1指向的位置,只需要将top进行一个减1的操作即可。
与此同时,我们要确保此次栈不为空,所以要进行断言的操作,防止程序崩溃。

具体实现如下:

void StackPop(Stack* ps)
{
	assert(ps);
	assert(ps->top > 0);
	ps->top--;
}

取出栈顶元素

直接返回top-1位置的元素即可,与此同时,我们要确保此次栈不为空,所以要进行断言的操作,防止程序崩溃。
具体实现如下:

STDatatype StackTop(Stack* ps)
{
	assert(ps);
	assert(ps->top > 0);
	return ps->a[ps->top - 1];
}

栈的大小

top的大小就是栈的大小,所以我们直接返回top的大小就可以了。
具体实现如下;

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

判断栈是否为空

我们返回ps->top == 0表达式的值,为真就是空,为假就是不为空。
具体实现如下:

bool StackEmpty(Stack* ps)
{
	return ps->top == 0;
}

栈的销毁

为了防止内存泄漏,动态内存申请的空间一定要我们自己手动释放,养成一个良好的习惯。

void StackDestroy(Stack* ps)
{
	assert(ps);

	free(ps->a);
	ps->capcaity = ps->top = 0;
}

到这里,栈的实现就聊完了,接下来我们来看一看队列的结构和实现。

队列

队列的概念和结构

队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头。

队列的结构,我们选取单链表来实现,秩序进行头删和为插的不足即可。如果选数组,那么每一次删头我们都要挪动一遍数据,这种方式不优,所以我们还是选取用单链表来实现。
定义的结构如下:

typedef int QDataType;
//节点
typedef struct QueueNode
{
	QDataType data;
	struct QueueNode* next;
}QueueNode;
//队列
typedef struct Queue
{
	QueueNode* head;
	QueueNode* tail;
}Queue;

队列的实现

队列的接口

// 初始化队列
void QueueInit(Queue* q);
// 队尾入队列
void QueuePush(Queue* q, QDataType data);
// 队头出队列
void QueuePop(Queue* q);
// 获取队列头部元素
QDataType QueueFront(Queue* q);
// 获取队列队尾元素
QDataType QueueBack(Queue* q);
// 获取队列中有效元素个数
int QueueSize(Queue* q);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0
int QueueEmpty(Queue* q);
// 销毁队列
void QueueDestroy(Queue* q);

队列的初始化

初始化很简单,只要将头指针和尾指针都置空。

void QueueInit(Queue* q)
{
	assert(q);

	q->head = q->tail = NULL;
}

入队

入队其实就是单链表尾插的操作,要分链表为空和不为空两种情况讨论。
为空时要改变头指针的指向,不为空就不需要了。

具体实现如下:

void QueuePush(Queue* q, QDataType x)
{
	assert(q);

	QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
	if (newNode == NULL)
	{
		printf("malloc fail\\n");
		exit(-1);
	}
	newNode->data = x;
	newNode->next = NULL;
	//队列为空时
	if (q->head == NULL)
	{
		q->head = q->tail = newNode;
	}
	else
	{
		q->tail->next = newNode;
		q->tail = newNode;
	}

}

出队

出队就是进行单链表尾删的操作,要考虑链表为空时不能进行删除,还要注意的是只有一个节点进行删除是要改变尾指针的指向。

具体实现如下:

void QueuePop(Queue* q)
{
	assert(q);
	assert(q->head != NULL);

	QueueNode* next = q->head->next;
	free(q->head);
	q->head = next;
	//删最后一个节点要将tail置空
	if (q->head == NULL)
	{
		q->tail = NULL;
	}

}

获取队头元素和队尾元素

首先要确保链表不为空,对头就是返回头节点的大小,队尾就是返回尾节点的大小。
具体实现如下:

// 获取队列头部元素
QDataType QueueFront(Queue* q)
{
	assert(q);
	assert(q->head != NULL);

	return q->head->data;
}
// 获取队列队尾元素
QDataType QueueBack(Queue* q)
{
	assert(q);
	assert(q->tail != NULL);

	return q->tail->data;
}

获取队列元素个数

遍历一遍链表,同时进行计数操作,最后返回计数结果即可。
具体实现如下:

int QueueSize(Queue* q)
{
	assert(q);
	int size = 0;
	QueueNode* cur = q->head;
	while (cur)
	{
		cur = cur->next;
		size++;
	}
	return size;
}

判断队列是否为空

我们可以直接返回表达式QueueSize(q) == 0的值,空就返回1,否则就返回0。
具体试下如下:

int QueueEmpty(Queue* q)
{
	assert(q);
	return QueueSize(q) == 0;
}

队列的销毁

为了防止内存泄漏,动态内存申请的空间一定要我们自己手动释放,养成一个良好的习惯。所以要将链表的空间逐个释放。
具体实现如下:

void QueueDestroy(Queue* q)
{
	assert(q);

	QueueNode* cur = q->head;
	while (cur)
	{
		QueueNode* next = cur->next;
		free(cur);
		cur = next;
	}
	q->head = q->tail = NULL;
}

总结

栈和队列就先聊到这,队列里面还有一种循环队列,我没有介绍,在后期博客中我会跟大家聊一聊这个话题,还有如何用两个栈实现队列,用两个队列实现栈,我在后面的博客中会给大家介绍。如果喜欢的话,欢迎点赞支持和指正~

以上是关于数据结构初阶第五篇——栈和队列(实现+图解)的主要内容,如果未能解决你的问题,请参考以下文章

数据结构初阶第九篇——八大经典排序算法总结(图解+动图演示+代码实现+八大排序比较)

数据结构初阶第九篇——八大经典排序算法总结(图解+动图演示+代码实现+八大排序比较)

数据结构初阶第四篇——双链表(实现+图解)

数据结构第五篇——栈和队列

数据结构第五篇——栈和队列

数据结构初阶文章汇总!