博客主页:️自信不孤单
文章专栏:数据结构与算法
代码仓库:破浪晓梦
欢迎关注:欢迎大家点赞收藏+关注
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈。入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
栈一般可以使用数组或者链表实现,相对而言数组的结构实现更优一些。因为数组在尾上插入数据的代价比较小。
首先创建两个文件来实现栈:
- Stack.h(节点的声明、接口函数声明、头文件的包含)
- Stack.c(栈接口函数的实现)
接着创建 test.c 文件来测试各个接口
如图:
Stack.h 文件内容如下:
#pragma once
#include
#include
#include
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
// 初始化栈
void STInit(ST* pst);
// 入栈
void STPush(ST* pst, STDataType x);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
bool STEmpty(ST* pst);
// 出栈
void STPop(ST* pst);
// 获取栈顶元素
STDataType STTop(ST* pst);
// 获取栈中有效元素个数
int STSize(ST* pst);
// 销毁栈
void STDestroy(ST* pst);
接下来,我们在 Stack.c 文件中实现各个接口函数。
对栈进行置空。
void STInit(ST* pst)
{
assert(pst);
pst->a = NULL;
pst->top = 0;
pst->capacity = 0;
}
判断扩容,然后直接尾插即可。
void STPush(ST* pst, STDataType x)
{
assert(pst);
if (pst->capacity == pst->top)
{
STDataType NewCapacity = (pst->capacity == 0 ? 4 : (pst->capacity) * 2);
STDataType* tmp = (STDataType*)realloc(pst->a, NewCapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
pst->a = tmp;
pst->capacity = NewCapacity;
}
pst->a[pst->top++] = x;
}
栈为空返回 ture,否则返回 false。
bool STEmpty(ST* pst)
{
return pst->top == 0;
}
断言检测栈是否为空,非空直接尾删即可。
void STPop(ST* pst)
{
assert(pst);
assert(!STEmpty(pst));
pst->top--;
}
断言检测栈是否为空,非空返回栈顶元素。
STDataType STTop(ST* pst)
{
assert(pst);
assert(!STEmpty(pst));
return pst->a[pst->top - 1];
}
返回栈中元素个数。
int STSize(ST* pst)
{
assert(pst);
return pst->top;
}
释放动态开辟好的内存,并对数据进行置空。
void STDestroy(ST* pst)
{
assert(pst);
free(pst->a);
pst->a = NULL;
pst->top = pst->capacity = 0;
}
注:在每个接口函数中一定要合理地使用assert函数断言防止对空指针的引用。
test.c 文件内容如下:
#include "Stack.h"
#include
void TestStack()
{
ST stack;
STInit(&stack);
STPush(&stack, 4);
STPush(&stack, 1);
printf("%d ", STTop(&stack));
STPop(&stack);
STPush(&stack, 3);
STPush(&stack, 2);
while (!STEmpty(&stack))
{
printf("%d ", STTop(&stack));
STPop(&stack);
}
STDestroy(&stack);
}
int main()
{
TestStack();
return 0;
}
运行结果:
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表。 进行插入操作的一端称为队尾,进行删除操作的一端称为队头。队列中的数据元素遵循先进先出FIFO(First In First Out)的原则。
- 入队列:队列的插入操作叫做入队列。从队尾插入数据。
- 出队列:队列的删除操作叫做出队列。从对头删除数据。
队列也可以用数组或者链表来实现,相对而言使用链表结构的实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。
首先创建两个文件来实现队列:
- Queue.h(节点的声明、接口函数声明、头文件的包含)
- Queue.c(队列接口函数的实现)
接着创建 test.c 文件来测试各个接口
如图:
Queue.h 文件内容如下:
#pragma once
#include
#include
#include
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
// 初始化队列
void QueueInit(Queue* pq);
// 队尾入队列
void QueuePush(Queue* pq, QDataType x);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0
int QueueEmpty(Queue* pq);
// 队头出队列
void QueuePop(Queue* pq);
// 获取队列头部元素
QDataType QueueFront(Queue* pq);
// 获取队列队尾元素
QDataType QueueBack(Queue* pq);
// 获取队列中有效元素个数
int QueueSize(Queue* pq);
// 销毁队列
void QueueDestroy(Queue* pq);
接下来,我们在 Queue.c 文件中实现各个接口函数。
对队列进行置空。
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
在堆上申请一个节点,判断是否为第一个节点。如果是第一个节点,就让队列的头尾指针指向该节点;如果不是第一个节点,直接尾插即可。
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* NewNode = (QNode*)malloc(sizeof(QNode));
if (NewNode == NULL)
{
perror("malloc fail");
return;
}
NewNode->data = x;
NewNode->next = NULL;
if (pq->phead == NULL)
{
pq->phead = pq->ptail = NewNode;
}
else
{
pq->ptail->next = NewNode;
pq->ptail = NewNode;
}
pq->size++;
}
队列为空返回 ture,否则返回 false。
int QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
断言检测队列是否为空,非空直接头删,最后判断删除的是否是最后一个节点。如果是最后一个节点,需要将尾指针置空。
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
pq->size--;
if (pq->size == 0)
{
pq->ptail = NULL;
}
}
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->phead->data;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->ptail->data;
}
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
将队列中的所有元素都执行出队列操作即可。
void QueueDestroy(Queue* pq)
{
assert(pq);
while (!QueueEmpty(pq))
{
QueuePop(pq);
}
}
注:在每个接口函数中一定要合理地使用assert函数断言防止对空指针的引用。
test.c 文件内容如下:
#include "Queue.h"
#include
void TestQueue()
{
Queue qu;
QueueInit(&qu);
QueuePush(&qu, 5);
QueuePush(&qu, 0);
QueuePush(&qu, 2);
printf("%d ", QueueFront(&qu));
printf("%d ", QueueBack(&qu));
QueuePop(&qu);
printf("%d ", QueueFront(&qu));
QueuePop(&qu);
QueuePop(&qu);
QueuePush(&qu, 1);
QueuePush(&qu, 3);
QueuePush(&qu, 1);
QueuePush(&qu, 4);
while (!QueueEmpty(&qu))
{
printf("%d ", QueueFront(&qu));
QueuePop(&qu);
}
QueueDestroy(&qu);
}
int main()
{
TestQueue();
return 0;
}
运行结果:
实际中我们有时还会使用一种队列叫循环队列。
循环队列是一种线性数据结构,其操作表现基于 先进先出 原则,并且 队尾被连接在队首之后以形成一个循环。它也被称为 “环形缓冲器”。
注意:
(1)为了避免 空 和 满 混淆,无法区分,那么可以 多开一个空间。
(2)当
head == tail
时,就为 空。(3)当 tail 的下一个位置是 head 时,就是满。
循环队列如果用链表实现的话,不容易找尾,所以一般我们用数组来实现。