【栈和队列】栈和队列的C语言实现-宝藏级别数据结构教程-超详细的注释和解释
先赞后看好习惯 打字不容易,这都是很用心做的,希望得到支持你 大家的点赞和支持对于我来说是一种非常重要的动力 看完之后别忘记关注我哦!️️️
本篇博客干货满满,建议收藏后食用~
还没有学习链表的伙伴,可以通过传送门先学习
单链表教学:【链表】单链表的介绍和基本操作(C语言实现)【保姆级别详细教学】
双向链表教学:【链表】双向链表的介绍和基本操作(C语言实现)【保姆级别详细教学】
当我们学习完链表之后,我们接下来要学习的基础数据结构,就是栈和队列了。 栈和队列,是我们后续学习,解题中非常重要的一种基础数据结构。虽然有些学习过C++或其它编程语言的伙伴会认为,只要会用即可,不需要知道底层的实现。
但是博主在这里要说的是,虽然其它面向对象的编程语言里面会有包装好的栈或队列可以使用,但是对于底层的实现原理的思想,对于我们的学习是非常重要的。因此接下来,请跟着博主的节奏,学习这两个重要的数据结构。
栈
栈是一种特殊的线性表,只允许在固定的一段进行插入和删除的操作
进行数据插入和删除操作的一端称为栈顶,另一端称为栈底
栈中的数据遵循后进先出LIFO的原则
Last in First Out
压栈:栈的插入操作,在栈顶
出栈:栈的删除,出数据也在栈顶。
这个结构其实就像一个弹夹,只能在一端进行数据的插入和删除。
这里强调一下:栈的特点就是,后进先出!
这非常关键!
在实现之前,我们要思考:
首先,这是一个线性表实现的,所以我们有两个选择:顺序表,也就是数组实现;或者链表实现。
栈的实现
数组好还是链表好?
其实我们栈的插入和删除相当于尾插尾删
用数组的唯一缺陷就是不够的时候要增容
用单链表就不太好了:
- 尾插尾删要找尾
- 如果不找尾,用尾指针记录尾结点
但是即便这样,我们也要找到前一个所以要不就使用双链表实现
如果一定想使用单链表
把把栈顶栈底反一下,用头插,不要用尾插即可
所以用单链表也是ok的
- 如果用尾作栈顶,那么用双链表好
- 如果要用单链表实现,那么就用让头作栈顶
总和各方面的要素,使用数组(顺序表)实现是最合适的。
节点结构
typedef int STDataType;
typedef struct Stack {
STDataType* a;
int top;//栈顶
int capacity;//容量
}ST;
初始化接口:
我们初始化的数组容量是4
容量不够后我们使用两倍扩容法
//初始化
void StackInit(ST* ps) {
assert(ps);//判断ps的真假
ps->a = (STDataType*)malloc(sizeof(STDataType) * 4);
ps->capacity = 4;
ps->top = 0;
}
销毁栈接口:
注意: 释放后记得将指针置空,养成良好习惯,防止野指针的生成!
void StackDestory(ST* ps) {
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
数据压栈接口:
采用两倍扩容法
//压栈
void StackPush(ST* ps, STDataType x) {
assert(ps);
//扩容
if (ps->top==ps->capacity) {
STDataType* tmp = (STDataType*)realloc(ps->a, ps->capacity * 2 * sizeof(STDataType));
if (tmp == NULL) {
printf("realloc fail\n");
exit(-1);
}
ps->a = tmp;
ps->capacity *= 2;//容量不够的时候,容量乘2
}
ps->a[ps->top] = x;//赋值
ps->top++;//栈顶记得移动
}
数据出栈接口:
注意:当栈为空的时候,调用此接口肯定是不行的,因此出栈之前,我们还需要判断,栈是否为空。所以需要
assert(ps->top);
//出栈
void StackPop(ST* ps) {
assert(ps);
assert(ps->top > 0);//如果栈空了,调用Pop直接终止程序
//不需要改数据,反正不重要
//直接--top就可以了
ps->top--;
}
取栈顶元素接口:
对于我们来说,这些接口的实现都非常的简单,这里就不赘述了。
当然:要注意的点:top记得减一
//取栈顶元素
STDataType StackTop(ST* ps) {
assert(ps);
assert(ps->top);//同样
return ps->a[ps->top - 1];//记得减一
}
返回栈大小和验空接口:
代码实现和思路都非常简单,这里不赘述
//返回大小
int StackSize(ST* ps) {
assert(ps);
return ps->top;
}
//验空
bool StackEmpty(ST*ps) {
assert(ps);
return ps->top == 0;
}
#pragma once
#include
#include
#include
#include
typedef int STDataType;
typedef struct Stack {
STDataType* a;
int top;//栈顶
int capacity;//容量
}ST;
//初始化
void StackInit(ST* ps);
//销毁
void StackDestory(ST* ps);
//压栈
void StackPush(ST* ps, STDataType x);
//出栈
void StackPop(ST* ps);
//取栈顶元素
STDataType StackTop(ST* ps);
//大小
int StackSize(ST* ps);
//验空
bool StackEmpty(ST* ps);
#include"Stack.h"
int main() {
//从规定来说
//不能随便遍历
//按照规定后进先出
//如果能随便遍历就保不住它的性质了
ST st;
StackInit(&st);
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
StackPush(&st, 5);
//这个循环才是栈的取数据的方法,而不是什么Print接口
while (!StackEmpty(&st)) {
printf("%d\n", StackTop(&st));
StackPop(&st);
}
printf("\n");
StackDestory(&st);
return 0;
}
队列
只允许在一段进行插入数据操作,另一端进行删除数据操作的特殊线性表。
队列具有先进先出FIFO(Fist in First Out)的规则
入队列:队尾
出队列:队头
这个结构就是队列
它所遵循的原则是:先进先出。这个非常重要!一定要牢记
数组还是链表?
当然这里,我们毫不犹豫是要选择单链表的,因为单链表的头删和尾插都是非常高效率的。
在这里博主特别强调一下:相比于普通的单链表,我们再存一个尾指针
tail
,用来指向链表最后一个元素,这样我们尾插的效率就可以达到O(1)
了。
节点结构:
typedef int QDataType;
typedef struct QueueNode {
struct QueueNode* next;//指针域
QDataType data;//数据域
}QNode;
typedef struct Queue {
QNode* head;//头指针
QNode* tail;//尾指针
}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 (newnode == NULL) { printf("malloc fail\n"); exit(-1); }
newnode->data = x;
newnode->next = NULL;
//这里要分情况,如果链表一开始为空的时候,head和tail是一样的
if (pq->tail == NULL) {
pq->head = pq->tail = newnode;
}
//链表不为空:
else {
pq->tail->next = newnode;
pq->tail = newnode;
}
}
删除数据接口(队头出):
//队头出
void QueuePop(Queue* pq) {
assert(pq);
assert(pq->head);//判断队列不为空
//如果只剩一个节点时:
if (pq->head->next == NULL) {
free(pq->head);
pq->head = pq->tail = NULL;
}
//有两个或两个以上的节点时:
else {
//先保存下一个
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
//如果只剩一个之后,tail变成野指针了,所以处理一下tail,即上面的if
//因此要分情况
}
}
取数据接口:
思路非常简单,这里不赘述。
//取数据
QDataType QueueFront(Queue* pq) {
assert(pq);
assert(pq->head);
return pq->head->data;
}
QDataType QueueBack(Queue* pq) {
assert(pq);
assert(pq->head);
return pq->tail->data;
}
返回大小和验空接口:
int QueueSize(Queue* pq) {
//遍历
assert(pq);
int size = 0;
QNode* cur = pq->head;
//这里其实就是一个数组的遍历
while (cur) {
size++;//计数器
cur = cur->next;//迭代
}
return size;
}
bool QueueEmpty(Queue* pq) {
assert(pq);
return pq->head == NULL;
}
#pragma once
#include
#include
#include
#include
typedef int QDataType;
typedef struct QueueNode {
struct QueueNode* next;
QDataType data;
}QNode;
typedef struct Queue {
QNode* head;
QNode* tail;
}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);
#include"Queue.h"
void TestQueue() {
Queue q;
QueueInit(&q);
//QueuePop(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
QueuePush(&q, 5);
while (!QueueEmpty(&q)) {
printf("%d ", QueueFront(&q));
QueuePop(&q);
}
printf("\n");
QueueDestroy(&q);
}
int main() {
TestQueue();
return 0;
}
看到这里,相信伙伴们已经对栈和队列已经有了比较深入的了解,掌握了基本的操作接口实现方法。这对我们以后数据结构的学习是非常有帮助的!
如果看到这里的你感觉这篇博客对你有帮助,不要忘了收藏,点赞,转发,关注哦!