.栈和队列的概念以及实现
今天,我们介绍最后两个特殊的线性结构---->栈和队列。栈和队列一讲完,我们接下来就要进入二叉树的学习,其实如果前面的博客你能够理解的话,你就会惊奇的发现:栈和队列的所有接口我们都已经在顺序表和链表中实现过了!接下来,我就带大家走进这两个特殊的数据结构。
1.1栈的概念及结构
栈是一种特殊的线性结构,它遵循的是先进后出的原则,对于一个栈来说,只允许在栈的一端入数据和出数据,这一端的学名叫做栈顶,而另外一端则称为栈底。而在将数据插入栈的操作我们叫做压栈(入栈),在栈顶删除数据的操作我们叫做出栈。
值得一提的是:在操作系统这门课里也有有关栈的概念,我们数据结构里的栈和操作系统里讲的栈是两个东西,只是二者的行为类似,并不能为二者强行画上等号!!!!
我们可以用一张图片来看栈是怎么进行数据的插入和删除的
从图片不难看出,越早入栈的数据越晚才能取出,但是读者要注意,栈的后入先出是相对而言的,接下来我们通过两道选择题来好好体会一下这句话
第一道题就是简单的讲数据全部入栈取出后是入栈元素的逆序,所以答案是B
但是第二题就是对栈先入后出是相对而言的很好的考察!
我们来仔细分析题目:
讲完了有关栈的概念和性质还有两道选择题,相信大家对栈应该有一个初步的了解了,接下来我们开始着手实现以下栈这个数据结构:
首先,栈这个数据结构的实现通常有两种方式,一种是采用数组的方式,另一种是采用链表的方式,接下来我们来分析一下两种方式的优缺点:
数组结构:
使用数组结构的优点是我们可以用下标来表示栈顶指针top,并且由于栈只能在栈顶删除数据,相当于尾删,顺序表的尾删的效率极高,所以用数组实现栈是很好的选择!!!缺点就是可能在扩容的时候有性能消耗和一定的空间浪费,不过问题不是很大
链表结构:
使用链表结构来模拟栈,有点是空间的利用率高,不会造成浪费,比较好的方式就是将top指针当作当前链表的头节点,然后利用头插的方式插入元素,头删的方式弹出元素,相对于数组结构,链式的结构就相对复杂,感兴趣的读者可以自行实现。
接下来,我们来实现一个栈,和前面顺序表一样,静态的栈的实际应用价值不大,所以我们实现的是一个支持动态增长的栈
在这里在提一下,对于数组来模拟栈,栈顶指针通常有两种初始化的方式,一种是初始化成-1,表示栈顶指针指向当前元素的位置(开始的时候没有元素,就是-1),另一种方式是初始化成0,表示栈顶指针指向当前元素的下一个位置,两种方式没有优劣之分,我是使用初始化成0的版本进行讲解
Stack.h---->栈的定义和函数接口的声明
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
size_t top;//栈顶指针
size_t capacity;//容量
}Stack;
//初始化
void StackInit(Stack* ps);
//释放
void StackDestroy(Stack* ps);
//插入
void StackPush(Stack* ps, STDataType x);
//删除
void StackPop(Stack* ps);
//取栈顶数据
STDataType StackTop(Stack* ps);
//判断栈是否为空
bool StackEmpty(Stack* ps);
//获取栈元素的个数
size_t StackSize(Stack* ps);
接下来我们来实现各个功能:
首先是初始化和释放,和顺序表几乎一致,这里就不在赘述:
//初始化
void StackInit(Stack* ps)
{
assert(ps);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
//释放
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
那么接下来就是push和pop操作,其实就是我们顺序表尾插和尾删的操作
//插入
void StackPush(Stack* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
size_t newCapacity = ps->capacity == 0 ? 2 : ps->capacity * 2;
STDataType* tmp= (STDataType*)realloc(ps->a, sizeof(STDataType) * newCapacity);
if (NULL == tmp)
{
printf("realloc fail\n");
exit(-1);
}
else
{
ps->a = tmp;
ps->capacity = newCapacity;
}
}
ps->a[ps->top++] = x;
}
//删除
void StackPop(Stack* ps)
{
assert(ps);
assert(ps->top > 0);
ps->top--;
}
接下来是取栈顶的数据top,由于我们的top指针总是指向当前元素的下一个位置,所以栈顶元素的下标是top-1
//取栈顶数据
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(ps->top > 0);
return ps->a[ps->top - 1];
}
接下来栈有两个特殊的接口,一个是判断栈是否为空,以及栈元素的个数,判断栈是不是为空,只要判断栈顶指针是不是0即可,而栈元素的个数恰好是top指向的位置:
//判断栈是否为空
bool StackEmpty(Stack* ps)
{
assert(ps);
return ps->top == 0;
}
//获取栈元素的个数
size_t StackSize(Stack* ps)
{
assert(ps);
return ps->top;
}
接下来我们测试一下我们的栈
#define _CRT_SECURE_NO_WARNINGS 1
#include "Stack.h"
void TestStack()
{
Stack st;
StackInit(&st);
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
while (!StackEmpty(&st))
{
printf("%d ", StackTop(&st));
StackPop(&st);
}
printf("\n");
StackDestroy(&st);
}
int main()
{
TestStack();
return 0;
}
注意,栈的遍历和通常的顺序表的遍历是不一样的,我们不能写一个print来遍历,而是栈非空则取出数据然后再删除,这点要特别注意!!!!
我们的栈没有问题,那么关于栈我们就先告一个段落,接下来我们进入另外一个数据结构--->队列的学习
队列:和栈的先进后出不一样,队列的数据是先进先出的!队列规定:数据在队尾插入,在队头的位置删除,而且不同于栈只能取栈顶的数据,队列是可以取队头的数据,也可以取队尾的数据。
我们可以用一张形象的图片来看一看队列的结构:
那么了解了队列的特性,接下来我们就着手实现队列这个数据结构:和栈相似,队列也有数组版本和链表版本,但是由于队列需要删除队头的数据,而数组结构删除头部的数据是比较耗时的操作,基于这一点我们采取链表来实现这个队列
Queue.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
//链表实现队列
typedef int QDataType;
typedef struct QueueNode
{
QDataType val;
struct QueueNode* next;
}QNode;
typedef struct Queue
{
QNode* head;//队头指针
QNode* tail;//队尾指针
}Queue;
//队列初始化
void QueueInit(Queue* pq);
//队列释放
void QueueDestroy(Queue* pq);
//队列插入数据
void QueuePush(Queue* pq, QDataType x);
//队列出数据
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
//队列大小
size_t QueueSize(Queue* pq);
//队列判空
bool QueueEmpty(Queue* pq);
//删除数据(队头出数据)
void QueuePop(Queue* pq);
这里可能有读者就会不明白这个队列的具体结构,我们可以画一张图片来帮助理解这个结构:
那么为什么我们额外定义了这个结构体呢?原因如下:
如果第一次插入,需要改变队头和队尾指针,然而从我们单链表增删查改可以看出,我们需要使用二级指针来进行第一次插入!而使用结构体Queue,我们只要使用Queue结构体的指针就可以改变头尾指针!
接下来,我们开始实现队列这个数据结构(主体的操作和单链表的增删查改类似,注意一些细节)
队列的初始化和释放:
//队列初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = pq->tail = NULL;
}
//队列释放
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
}
队列入数据,单链表的尾插
//队尾插入数据
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (NULL == newnode)
{
printf("malloc fail\n");
exit(-1);
}
else
{
newnode->val = x;
newnode->next = NULL;
if (NULL == pq->tail )
{ //防止野指针
assert(NULL==pq->head);
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
}
}
队列判空:当队头指针是空的时候队列才是空!
//队列判空
bool QueueEmpty(Queue* pq)
{
return pq->head == NULL;
}
接下来的删除,取队头和 队尾数据都需要判空这个接口
队列删除:从队头出数据--->队列的头部出数据(单链表的头删)
//队头出节点
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
但是这样的代码有问题,当只有一个节点的时候,虽然head被置空了,但是tail依旧指向原来已经被释放的空间,这时候就出现了野指针!所以只有一个节点的情况我们需要单独讨论
//队头出节点
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
if (pq->head == pq->tail)
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
}
接下来就是取队头的数据和队尾的数据,我们分别取名为front和back
//队头和队尾都可以取数据
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->val;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
assert(pq->tail);
return pq->tail->val;
}
最后就是求队列大小的接口size
//队列大小
size_t QueueSize(Queue* pq)
{
assert(pq);
size_t size = 0;
QNode* cur = pq->head;
while (cur)
{
++size;
cur = cur->next;
}
return size;
}
接下来我们测试一下我们的队列
void TestQueue()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
while (!QueueEmpty(&q))
{
printf("%d ", QueueFront(&q));
QueuePop(&q);
}
printf("\n");
QueueDestroy(&q);
}
和栈类似,队列取出数据也是只能边取边删,因为是先入先出,所以最后出来的数据顺序是1,2,3,4
程序运行出了预期结果!说明我们的队列是没有问题的!!!
完整的栈的接口代码如下:
Stack.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "Stack.h"
//初始化
void StackInit(Stack* ps)
{
assert(ps);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
//释放
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
//插入
void StackPush(Stack* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
size_t newCapacity = ps->capacity == 0 ? 2 : ps->capacity * 2;
STDataType* tmp= (STDataType*)realloc(ps->a, sizeof(STDataType) * newCapacity);
if (NULL == tmp)
{
printf("realloc fail\n");
exit(-1);
}
else
{
ps->a = tmp;
ps->capacity = newCapacity;
}
}
ps->a[ps->top++] = x;
}
//删除
void StackPop(Stack* ps)
{
assert(ps);
assert(ps->top > 0);
ps->top--;
}
//取栈顶数据
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(ps->top > 0);
return ps->a[ps->top - 1];
}
//判断栈是否为空
bool StackEmpty(Stack* ps)
{
assert(ps);
return ps->top == 0;
}
//获取栈元素的个数
size_t StackSize(Stack* ps)
{
assert(ps);
return ps->top;
}
Queue.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "Queue.h"
//队列初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = pq->tail = NULL;
}
//队列释放
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
}
//队尾插入数据
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (NULL == newnode)
{
printf("malloc fail\n");
exit(-1);
}
else
{
newnode->val = x;
newnode->next = NULL;
if (NULL == pq->tail )
{
assert(NULL==pq->head);
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
}
}
//队头和队尾都可以取数据
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->val;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
assert(pq->tail);
return pq->tail->val;
}
//队列大小
size_t QueueSize(Queue* pq)
{
assert(pq);
size_t size = 0;
QNode* cur = pq->head;
while (cur)
{
++size;
cur = cur->next;
}
return size;
}
//队列判空
bool QueueEmpty(Queue* pq)
{
return pq->head == NULL;
}
//队头出节点
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
if (pq->head == pq->tail)
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
}
结语:
本篇文章主要介绍了栈和队列两个数据结构特殊的性质以及模拟实现。栈的先进后出,队列的先进先出是这两个数据特有的性质,那么在下一篇博客中,我们会利用这两个数据结构独有的特殊性质来做几道OJ题,通过OJ题来更好的理解栈的先入后出,队列的先进先出。如果本篇博客有不足或错误之处,请大家可以指出,希望能够一同进步