栈和队列是两种重要的线性结构。从数据结构的角度看,栈和队列也是线性表,其特殊性在于栈和队列的基本操作是线性表操作的子集,他们都是操作受限的线性表(只能从两端操作,不可以在中间进行插入或删除操作)。
数据结构中栈和队列的那些事
1.栈
1.1栈的概念和结构
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
1.2 栈的实现
栈的实现一般可以使用数组或者链表实现,相对而言数组的结构实现更优一些。因为数组在尾上插入数据的代价比较小。
// 下面是定长的静态栈的结构,实际中一般不实用,所以我们主要实现下面的支持动态增长的栈
typedef int StackDataType;
#define N 10
typedef struct Stack
{
StackDataType a[N];
int top; // 栈顶
}ST;
// 支持动态增长的栈
typedef int StackDataType;
typedef struct Stack
{
StackDataType* a;
int capacity;
int top;
}ST;
// 初始化栈
void StackInit(ST* ps);
// 入栈
void StackPush(ST* ps, StackDataType x);
// 出栈
void StackPop(ST* ps);
// 获取栈顶元素
StackDataType StackTop(ST* ps);
// 获取栈中有效元素个数
int StackSize(ST* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
bool StackEmpty(ST* ps);
// 销毁栈
void StackDestroy(ST* ps);
1.2.1 初始化栈
栈初始化的是栈的初始状态,你可以一开始就malloc一些空间,等栈满需要插入的时候再增容,也可以一开始不直接malloc空间,插入的时候再开辟空间,这个随大家的心情,没有唯一的框架。
我们这里选择的是第一种。需要注意的是,我们在做OJ题的时候使用malloc可以不判断是否malloc成功,因为一般不存在失败的时候,但是我们在自己写代码的时候,最好加上判断,我们在写工程的时候,可能会存在内存不足的时候,保持这样一个好习惯。
代码如下:
void StackInit(ST* ps)
{
assert(ps);
ps->a = (StackDataType*)malloc(sizeof(StackDataType) * 4);
if (ps->a == NULL)
{
perror("malloc fail");
exit(-1);
}
ps->top = 0;
ps->capacity = 4;
}
1.2.2 入栈
入栈操作很简单,因为我们是使用顺序表实现的,直接尾插就可以了,唯一需要注意的是,如果栈满了,则需要扩容。
这里我们在实现的时候,其实有两种情况,就是ps->top指针指向的是栈顶的位置,还是栈顶的下一个位置。我们这里是指向栈顶的下一个位置,如果是指向栈顶的位置,实现其他接口的时候,代码是需要修改的,具体的情况随你自己的代码而定。
代码如下:
void StackPush(ST* ps, StackDataType x)
{
assert(ps);
//扩容
if (ps->top == ps->capacity)
{
StackDataType* tmp = (StackDataType*)realloc(ps->a, sizeof(StackDataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->a[ps->top++] = x;
}
1.2.3 出栈
出栈操作同样简单,他不需要考虑是否需要扩容,直接ps->top指针减一即可,这里大家可能会有一个疑问,就是需不需要将原来栈顶的值置0,其实是没有必要的,有两个原因,首先,他可能本来的值就是0,再者就是,我们只访问到栈顶的元素,至于他的下一个元素是什么我们根本不关心,也取不到他的值,不影响我们接下来的所有操作,比如插入操作。
代码如下:
void StackPop(ST* ps)
{
assert(ps);
assert(!StackEmpty(ps));//判空,如果栈为空,不能进行出栈操作
ps->top--;
}
1.2.4 获取栈顶元素
取栈顶元素,看我们ps->top指针具体指向的位置,我们这里指向的是栈顶的下一个元素,所以我们在获取栈顶元素的时候需要返回ps->top指针的前一个位置。
这里大家可能还有一个问题,这么简单的一句代码,还有没有必要将其封装起来,回答是有必要的,因为我们是代码编写者,我们知道代码的逻辑,但是使用的人他们并不知道,如果需要使用的时候,还需要将我们写的代码读懂,这太麻烦了,所以即使是简单的一句代码,我们还是需要将其封装起来的。
代码如下:
StackDataType StackTop(ST* ps)
{
assert(ps);
assert(!StackEmpty(ps));
return ps->a[ps->top-1];
}
1.2.5 获取栈中有效元素个数
获取栈中有效元素的个数,我们直接可以返回ps->top指向的顺序表的下标,因为我们ps->top指针指向的就是栈顶的下一个位置,就是栈中元素的个数。
如果实现的时候,ps->top指针指向的是栈顶的位置,则需要返回ps->top+1;
代码如下:
int StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
1.2.6 检测栈是否为空
判空,我们可以直接判断ps->top是否等于0,等于0的时候,返回true,不等于0的时候,说明栈不为空,返回false。
代码如下:
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
1.2.7 销毁栈
销毁栈的时候,我们需要将栈开辟的空间销毁,将ps->top和ps->capacity置为0。
代码如下:
void StackDestroy(ST* ps)
{
assert(ps);
free(ps->a);
ps->capacity = 0;
ps->top = 0;
}
1.3 原码
链接: 栈代码实现的Gitee仓库
2. 队列
2.1 队列的概念和结构
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头
2.2 队列的实现
队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。
typedef int QLDataType;
// 链式结构:表示队列
typedef struct QueueListNode
{
QLDataType data;
struct QueueListNode* next;
}QNode;
// 队列的结构
typedef struct Queue
{
QNode* front;//队头
QNode* tail;//队尾
int size;
}Queue;
// 初始化队列
void QueueInit(Queue* q);
// 队尾入队列
void QueuePush(Queue* q, QLDataType x);
// 队头出队列
void QueuePop(Queue* q);
// 获取队列头部元素
QLDataType QueueFront(Queue* q);
// 获取队列队尾元素
QLDataType QueueBack(Queue* q);
// 获取队列中有效元素个数
int QueueSize(Queue* q);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0
bool QueueEmpty(Queue* q);
// 销毁队列
void QueueDestroy(Queue* q);
2.2.1 初始化队列
队列的初始化,我们选择的是使用链式结构存储队列,并且定义了两个指针,分别指向队头和对尾,方便我们进行插入和删除操作,队头进行出队操作,队尾进行入队操作。
当队列为空的时候,可以将这两个指针都置空,ps->size置0。
代码如下:
void QueueInit(Queue* q)
{
assert(q);
q->front = q->tail = NULL;
q->size = 0;
}
2.2.2 队尾入队列
队尾入队列,首先我们需要开辟一个新的节点,将新节点的值置为x(我们想要插入的值),将新节点的next指针置空。
在入队的时候,我们需要判断,是否是第一次入队,第一次入队的时候,需要将新节点赋值给队头指针和队尾指针,如果不是第一次入队,则只需要将新节点赋给队尾指针的指针域,再将新节点赋给队尾指针,让新节点成为新的队尾。
代码如下:
void QueuePush(Queue* q, QLDataType x)
{
assert(q);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (!newnode)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
if (q->front == NULL)
{
q->front = q->tail = newnode;
q->size++;
}
else
{
q->tail->next = newnode;
q->tail = newnode;
q->size++;
}
}
2.2.3 队头出队列
队头出队列,此时我们需要判断两种情况,当队列只有一个结点的时候和当队列有多个结点的时候,当队列只有一个结点的时候,队列的头指针和为指针指向那同一个位置,我们需要先将此空间释放,再将头指针和尾指针都置空。当队列有多个结点的时候,我们可以先记录下头指针的下一个位置,然后将头结点释放,再让头指针指向记录的那个位置,也可以用一个指针先指向头结点的位置,然后让头指针指向下一个结点,再释放记录的头结点。
代码如下:
void QueuePop(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
if (q->front == q->tail)
{
free(q->front);
q->front = q->tail = NULL;
q->size--;
}
else
{
QNode* cur = q->front;
q->front = q->front->next;
free(cur);
q->size--;
}
}
2.2.4 获取队列头部元素
获取队列头部元素,这个很简单,直接返回头指针指向结点的数据域就可以了。
代码如下:
QLDataType QueueFront(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->front->data;
}
2.2.5 获取队列队尾元素
获取队列队尾元素,这个同上,返回尾指针指向结点的数据域即可。
代码如下:
QLDataType QueueBack(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->tail->data;
}
2.2.6 获取队列中有效元素个数
获取队列中有效元素个数,这个也同上,我们在定义的时候,就定义了size(队列中元素的个数),直接返回即可。
代码如下:
int QueueSize(Queue* q)
{
assert(q);
return q->size;
}
2.2.7 检测队列是否为空
检测队列是否为空,我们可以判断头指针和尾指针是否为空,为空则说明队列为空,返回true,不为空则说明队列不为空,返回false。
代码如下:
bool QueueEmpty(Queue* q)
{
assert(q);
return q->front == NULL && q->tail == NULL;
}
2.2.8 销毁队列
销毁队列,销毁队列比较麻烦,我们需要将队列中的节点,一个一个的释放,否则会造成内存的泄露。最后将头指针和尾指针都置空,再将size置为0。
代码如下:
void QueueDestroy(Queue* q)
{
assert(q);
QNode* cur = q->front;
while (cur)
{
QNode* del = cur->next;
free(cur);
cur = del;
}
q->front = q->tail = NULL;
q->size = 0;
}
2.3 队列的原码
链接: 队列原码的Gitee仓库
结语:
感谢大家看到这里,如有错误,希望各位大佬多多指正!
祝大家心情愉悦!