本篇博客我要来和大家一起聊一聊数据结构中的栈和队列相关知识,一种是先进后出的结构,另一种是先进先出的结构。
博客代码已上传至: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;
}
栈和队列就先聊到这,队列里面还有一种循环队列,我没有介绍,在后期博客中我会跟大家聊一聊这个话题,还有如何用两个栈实现队列,用两个队列实现栈,我在后面的博客中会给大家介绍。如果喜欢的话,欢迎点赞支持和指正~