栈和队列是什么
栈和队列它们两其实都是一种数据结构。
栈和队列的性质
栈的性质
栈这个数据结构的性质是先入后出,只有栈顶才可以进行插入删除操作,这就是栈的最主要的性质。所以,先进入栈的数据要最后才会出栈。
下面这张图示能很清楚地说明栈这个数据结构的特点:
当我们的data1这个数据进入栈时会直接沉到栈底,而一旦有第二个数据入栈,我们的第一个数据就无法出栈,必须等待第二个数据出栈之后我们的第一个进入栈的数据才有可能出栈。
数据结构中的栈和内存中的栈的区别
我们听到栈这个词,我们很容易联想到内存中是不是也有一个地方叫栈?
以下是内存的示意图:
我们发现内存中有三个区域,一个是栈区,一个是堆区,一个是静态区。这里的栈和数据结构中的栈其实是两个东西。这里的栈和数据结构中的栈是不同的意思。数据结构中的栈指的是一种数据结构,这种数据结构是先进后出的,但是内存中的栈指的是一块内存区域。这两个虽然是一个名词但是代表的不是同一个意思。这里要特别注意这一点。
队列的性质
队列则正好相反,队列这个数据结构的特征就是先入先出,队头进入数据,队尾出数据,这就是队列最主要的性质。是不是像极了我们排队买东西,我们日常生活中买东西就是通过排队来完成的,从队尾进然后从队头买完东西就出去。这也是为什么这个数据结构叫队列。
以下是队列的示意图:
队列的理解比较简单,在此不再赘述。
栈和队列的实现
前面我们已经介绍完了栈和队列的性质了,现在我们来实现一下我们的栈和队列这两个数据结构。
栈的实现
首先我们先来实现一下栈。
栈的底层数据结构
在这里我们有两种数据结构来帮助我们实现栈,第一个就是我们上一篇文章所写的顺序表,第二个也是我们上一篇文章所写的链表。那么这两种数据结构我们应当用哪一种去作为我们栈的底层呢?其实两种数据结构都可以很方便地实现栈,因为顺序表可以很方便地实现尾插尾删,而栈的插入删除都是在同一边,所以顺序表是很适合实现栈的。单链表和顺序表一样很适合头插头删,栈的插入删除操作也都是在同一边的,所以单链表也是很适合实现栈的。在此我们会使用顺序表来实现栈,不过博主也用过单链表来实现栈,源码已经放在这里了:https://gitee.com/g_666/c-language-and-cplusplus-data-structures
栈的实现
在了解完栈的底层数据结构之后我们就可以来试着实现栈了。在以上的分析,我们了解到了栈只是使用了顺序表的尾插尾删,所以,栈的实现和顺序表其实差不多,就是少了头插头删操作和指定位置插入删除操作。
栈顶的确定
如果用顺序表来实现栈的话,我们最好是用尾部来表示栈顶,栈的插入删除操作是在同一边的而顺序表又适合尾插尾删,如果用顺序表头表示栈顶,那么就会导致我们插入删除栈中的数据元素的时候需要遍历整个顺序表移动数据元素。
如果不理解的话请看图示:
这里是栈的实现的头文件的源码:
//栈的实现,基于顺序表
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top; //栈顶元素所在位置的下一个位置
int capcity; //栈的大小
}ST;
//栈的初始化和销毁
void STInit(ST* pst);
void STDestroy(ST* pst);
//栈的插入删除操作
void STPush(ST* pst, STDataType x);
void STPop(ST* pst);
//返回栈顶元素
STDataType STTop(ST* pst);
//检查栈是否为空
bool STEmpty(ST* pst);
//查看栈的大小
int STSize(ST* pst);
栈只是多了一个返回栈顶元素的操作,这个操作很好实现。
这里是栈的实现的源码:
//栈的初始化和销毁
//栈的初始化
void STInit(ST* pst)
{
assert(pst);
pst->a = NULL;
pst->capcity = 0;
pst->top = 0;
}
//栈的销毁
void STDestroy(ST* pst)
{
assert(pst);
//由于是顺序表,故直接free即可
free(pst->a);
pst->a = NULL; //管理野指针
pst->top = pst->capcity = 0;
}
//栈的插入删除操作
//栈的插入(和顺序表的尾插操作一致)
void STPush(ST* pst, STDataType x)
{
assert(pst);
if (pst->top == pst->capcity)
{
//扩容
int newcapcity = pst->capcity == 0 ? 4 : 2 * pst->capcity;
STDataType* tmp = (STDataType*)realloc(pst->a, newcapcity * sizeof(STDataType));
if (tmp == NULL)
{
//扩容失败
perror("realloc fail");
exit(-1);
}
pst->a = tmp;
pst->capcity = newcapcity;
}
//插入数据在top位置并将top位置后移
pst->a[pst->top++] = x;
}
//栈的删除(与顺序表的尾删操作一致)
void STPop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
pst->top--;
}
//返回栈顶元素
STDataType STTop(ST* pst)
{
assert(pst);
//判空
assert(pst->top);
return pst->a[pst->top - 1];
}
//检查栈是否为空
bool STEmpty(ST* pst)
{
assert(pst);
return pst->top == 0;
}
//查看栈的大小
int STSize(ST* pst)
{
assert(pst);
return pst->top;
}
栈的实现和顺序表的唯一一个区别就是栈需要取出栈顶元素。但是顺序表不用,所以栈提供了一个函数来实现取出栈顶元素。由于以上代码均在顺序表中实现过或比较简单,在这里便不再赘述。
队列的实现
在栈的实现我们可以用顺序表和单链表,但是在队列里,我们最好是用单链表去实现,因为队列这个数据结构的特点是在一边插入数据另一边删除数据。如果我们使用顺序表的话,我们就需要遍历元素去移动元素的位置。但是单链表不需要,单链表只需要在尾部增加一个指针让我们可以直接访问这个数据元素就行了。
队头的确定
由于队列是一个队尾插入队头删除数据元素的数据结构,但是由于我们会在队尾增加一个指针,所以单链表的头和尾我们随意选择一个为队头就行了。这里博主选用的是单链表的头作为队头。
以下是队列的图示:
这里是队列的头文件的源码:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int QDataType;
typedef struct QListNode
{
//数据域
QDataType data;
//指针域
struct QListNode* next;
}QNode;
typedef struct Queue
{
QNode* front;
QNode* rear;
}Queue;
//队列的初始化与销毁
void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
//队列的插入删除
void QueuePush(Queue* pq, QDataType x);
void QueuePop(Queue* pq);
//获取队头和队尾元素
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
//获取队列有效元素个数
int QueueSize(Queue* pq);
//检查队列是否为空
bool QueueEmpty(Queue* pq);
我们这里通过使用一个结构体来封装队列的前指针和后指针。
以下是队列的源文件的源码:
//队列的初始化与销毁
void QueueInit(Queue* pq)
{
assert(pq);
pq->front = NULL;
pq->rear = NULL;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->front, * next = cur;
//遍历链表删除一个个结点
while (cur)
{
next = cur->next;
free(cur);
cur = next;
}
//管理野指针
pq->front = pq->rear = NULL;
}
//队列的插入删除
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
//创建新节点
QNode* tmp = (QNode*)malloc(sizeof(QNode));
if (tmp == NULL)
{
perror("malloc fail\n");
exit(-1);
}
tmp->data = x;
tmp->next = NULL;
//队列为空的时候就将front和rear指针指向同一个结点
if (pq->front == NULL)
{
pq->front = pq->rear = tmp;
}
else
{
//尾插
pq->rear->next = tmp; //在队尾后插入新结点
pq->rear = tmp; //更新队尾结点地址
}
}
void QueuePop(Queue* pq)
{
assert(pq);
//检查队列是否为空
if (pq->front == NULL)
{
return;
}
else
{
//头删
QNode* cur = pq->front; //将队头元素的地址存起来
pq->front = cur->next; //更改队头元素的地址
//删除原来的队头元素
free(cur);
cur = NULL;
}
}
//获取队头和队尾元素
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(pq->front);
return pq->front->data;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(pq->rear);
return pq->rear->data;
}
//获取队列有效元素个数
int QueueSize(Queue* pq)
{
assert(pq);
QNode* cur = pq->front;
int count = 0; //计数器
//遍历链表
while (cur != pq->rear)
{
cur = cur->next;
count++;
}
return count;
}
//检查队列是否为空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->front == NULL;
}
为了减少篇幅,博主在这里仅介绍几个比较重要且理解较为困难的函数。
第一个就是队列的插入操作的函数:创建新节点这个步骤就是单链表中创建新节点的步骤,在这里就不多赘述。第二步就是我们的插入数据元素的操作,当我们的队列不为空时,我们只需要先将数据元素插入到队列中,再更改我们的尾指针指向的结点就行了。但是当我们的链表为空时,我们的头指针和尾指针都是指向空指针的,此时如果按照上面的操作,我们就会发现我们无法将新的数据元素插入到单链表中,所以我们这里得重新讨论,直接将队头指针和队尾指针指向新节点就行了。
以下是队列的插入操作图示:
以上就是队列的插入操作的介绍。
第二个就是队列的删除操作的函数:当队列中没有数据元素时,我们就不能进行删除操作,所以我们直接返回空退出函数即可。当队列中有数据元素时,我们就先把队列的头指针指向的地址存起来,然后更新队列的头指针指向的结点的地址,最后直接删除原队列指向的头指针指向的地址就行了。
以下是队列的删除操作图示:
以上就是队列的插入删除操作的实现。
以上就是栈和队列这两个数据结构的实现。