目录
前言
大家好啊❥(ゝω・✿ฺ)~ 欢迎来到我的数据结构系列文章(〃'▽'〃)~
本文将介绍栈和队列这两个数据结构的特点和实现,实现分别是用数组和单链表实现的哦,我们不废话,直接开始吧~
有需要不想直接看文字可以配合目录直接跳到查看代码区域哦~
一、栈
栈,数据结构中的一种。此数据结构的操作方式和计算机环环相扣,也是我们学习线性表需要了解到并且实现的一种数据结构。
1.理解栈
入栈动画演示:
出栈动画演示:
栈的特点
从上面的动画演示不难总结出:
栈数据结构具有的特点是:
先进后出,后进先出。
储存顺序就是按照线性表的尾插即可。
取出顺序即每次都是尾删,然后返回尾部数据。
那么,我们现在就开启它的实现吧:
2.实现栈
实现思路
1.首先定义其结构。因为是线性表,所以链表和动态数组均可选择,本篇实现栈选择动态数组进行实现。
2.结构实现后首先就需要进行初始化。
3.初始化过后就可以实现压栈和出栈操作了。
4.最后别忘了将从堆空间申请的空间给释放了。
C语言实现
1.Stack.h
此文件定义结构,声明函数。
所需要包含的头文件:
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
stdio.h
基本输入输出的函数所含的头文件
assert.h
使用assert();来进行检测是否为假(对于指针为NULl),如果是就报错
stdlib.h
方便使用申请堆内存的函数。malloc、realloc......
stdbool.h
让c语言可以使用布尔类型(true、false)
当然上面只是包含的头文件,不要忘记了#pragma once防止重复包含定义。
重定义其数据类型名字--方便对不同的数据进行存储;然后就是一个动态数组基本定义结构了--线性表里面同样的定义方式:一个储存动态数组的指针,一个统计实时数据的变量,一个统计当前数组最大储存数量,方便为之后的扩容做准备。
typedef int StackDate; // 定义存储到栈内的数据为栈数据
typedef struct Stack
{
StackDate* x;// 存放数据的数组
int top;// 存放实时数据个数
int capacity;// 容量 当前最大的储存数据量
}Stack;
typedef
重定义类型--起别名:在定义的结构(枚举、结构体、联合)或者基本数据前,后面跟上新名字,注意;
然后就是初始化、摧毁、入栈、出栈等函数的实现声明:
//栈的初始化
void StackInit(Stack* pt);
//栈的摧毁
void StackDestroy(Stack* pt);
//入栈
void StackPush(Stack* pt, StackDate x);
//出栈
StackDate StackPop(Stack* pt);
//检测是否存在数据
bool StackDetect(Stack* pt);
因为需要修改里面的x结构(数组),首先不能直接传Stack属性的变量,因为这样是传值拷贝,不会改变原本变量(main)的属性值,所以需要传其地址进行修改。--1级指针。
综上:
★Stack.h(头文件)代码实现:
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
/*<栈> 使用数组实现(也可以使用链表进行实现)
* 实现基本功能:1.入栈 2.出栈 上述两者遵循先入后出,后进先出原则
*3.栈的摧毁
*/
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int StackDate; // 定义存储到栈内的数据为栈数据
typedef struct Stack
{
StackDate* x;// 存放数据的数组
int top;// 存放实时数据个数
int capacity;// 容量 当前最大的储存数据量
}Stack;
//栈的初始化
void StackInit(Stack* pt);
//栈的摧毁
void StackDestroy(Stack* pt);
//入栈
void StackPush(Stack* pt, StackDate x);
//出栈
StackDate StackPop(Stack* pt);
//检测是否存在数据
bool StackDetect(Stack* pt);
注意#define _CRT_SECURE_NO_WARNINGS 1,此是我使用vs2019为了能使用printf和scanf函数所包含的,如果不包含会被迫使用优化后的函数哦~
2.Stack.c
此文件实现栈的核心代码。
栈的初始化
//初始化栈 置空
void StackInit(Stack* pt)
{
assert(pt);//防止造成空指针引用
pt->x = NULL;
pt->top = pt->capacity = 0;
}
栈的摧毁
//栈的摧毁 对于在堆内存申请空间的数组进行回收
void StackDestroy(Stack* pt)
{
assert(pt);
free(pt->x);
pt->x = NULL;
pt->top = pt->capacity = 0;
}
注意free后把实时数据和容量也置为空哦~
入栈
//入栈 常规线性表那样进行尾插
void StackPush(Stack* pt, StackDate x)
{
assert(pt);
//如果数组为空或者已经满了
if (pt->top == pt->capacity)
{
int newquantity = (pt->capacity == 0) ? 4 : (pt->capacity) * 2;//三目运算符确定是否空还是满状态
StackDate* temp = (StackDate*)realloc(pt->x, sizeof(StackDate) * newquantity);//重新开辟或者扩展内存
if (temp == NULL)//没有申请到内存
{
perror("StackPush");//报出此处函数的问题。
exit(-1);//结束进程 0正常结束,除此之外其他数为不正常结束
}
pt->x = temp;
pt->capacity = newquantity;
}
//正常插入
pt->x[pt->top] = x;
pt->top++;
}
需要注意判断是否第一次进入(空数组)和满数组有一个共同特点--容量和实时数据相等,利用这个进行区分。后面正常插入是公共代码,不管是否相等都要执行的。
出栈
//出栈 弹出数据的拷贝值,然后栈内弹出数据
StackDate StackPop(Stack* pt)
{
assert(pt&&(!StackDetect(pt)));
return pt->x[--pt->top];
}
注意这里是出数据的同时进行弹出数据(删除),所谓的删除只是把实时数据变量往后进行移动,这样新加的时候直接覆盖掉原本数据即可。StackDetect是后面要实现的函数,如果数据为空就返回真,所以需要加上一个逻辑反进行限制,防止没有数据造成数组越界。
检测是否存在数据
//检测是否存在数据 判断top是否为0
bool StackDetect(Stack* pt)
{
assert(pt);
return pt->top == 0;//返回true 表示没有数据了
}
这里的return可以直接这样写,需要熟练一点。不熟练的话可以使用if结构进行判断返回true或者false。
综上:
★Stack.c(实现功能文件)代码实现:
#include"Stack.h"
//栈功能实现区:
//初始化栈 置空
void StackInit(Stack* pt)
{
assert(pt);//防止造成空指针引用
pt->x = NULL;
pt->top = pt->capacity = 0;
}
//栈的摧毁 对于在堆内存申请空间的数组进行回收
void StackDestroy(Stack* pt)
{
assert(pt);
free(pt->x);
pt->x = NULL;
pt->top = pt->capacity = 0;
}
//入栈 常规线性表那样进行尾插
void StackPush(Stack* pt, StackDate x)
{
assert(pt);
//如果数组为空或者已经满了
if (pt->top == pt->capacity)
{
int newquantity = (pt->capacity == 0) ? 4 : (pt->capacity) * 2;//三目运算符确定是否空还是满状态
StackDate* temp = (StackDate*)realloc(pt->x, sizeof(StackDate) * newquantity);//重新开辟或者扩展内存
if (temp == NULL)//没有申请到内存
{
perror("StackPush");//报出此处函数的问题。
exit(-1);//结束进程 0正常结束,除此之外其他数为不正常结束
}
pt->x = temp;
pt->capacity = newquantity;
}
//正常插入
pt->x[pt->top] = x;
pt->top++;
}
//出栈 弹出数据的拷贝值,然后栈内弹出数据
StackDate StackPop(Stack* pt)
{
assert(pt&&(!StackDetect(pt)));
return pt->x[--pt->top];
}
//检测是否存在数据 判断top是否为0
bool StackDetect(Stack* pt)
{
assert(pt);
return pt->top == 0;//返回true 表示没有数据了
}
然后就是测试环节了,大家可以随便测试,若有问题欢迎进行讨论指出!谢谢,下面是我的测试代码:
测试代码:
#include"Stack.h"
void test()
{
Stack a;
StackInit(&a);
//测试入栈
StackPush(&a, 1);
StackPush(&a, 2);
StackPush(&a, 3);
StackPush(&a, 4);
StackPush(&a, 5);
//测试出栈
StackDate x[5] = { 0 };
for (int i = 0; i < 5; i++)
{
x[i] = StackPop(&a);
printf("%d ", x[i]);
}
printf("\n");
//StackDate x6 = StackPop(&a);
StackDestroy(&a);
}
int main()
{
//主进程
test();//测试函数
return 0;
}
后序相关栈的实现我还会补充的!我们一起努力学习吧~
二、队列
队列,同样是数据结构之一,但是其特性和栈有许多相反之处。
1.理解队列
入队列动画演示:
出队列动画演示:
队列的特点
由上述动图演示不难看出队列的特点:
先进先出,后进后出。果然和栈的特性完全相反。
储存顺序同样是线性表的普通尾插。
取出顺序就是线性表的头部数据以及头删了。
2.实现队列
实现思路
1.由于队列的特性,结构选择链表会相对好一些,因为设计到头删,数组不好进行头删。
2.结构由单链表决定,但是为了保证头删和取出头部数据,在单独建立一个结构用来存放链表头地址和尾地址。
3.初始化将头和尾指针置为空
4.实现入队列和出队列操作。出队列注意判断头指针和尾指针指向同一位置。
5.最后别忘了摧毁回收内存。
C语言实现
1.Queue.h
此文件定义结构,声明函数。
首先声明的头文件:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
(详细细节可以查看栈实现的头文件相关介绍)
重定义储存数据类型(默认int)和单链表结构:
typedef int QueueDate;
//下面定义链表结构 -- 单链表 单链表的话需要单独定义两个指针一个指向头一个指向尾,方便进行插入和出列
typedef struct QListNode
{
QueueDate x;
struct QListNode* next;//存放下一指针的
}QNode;
单链表结构,一个储存数据,一个储存下一个节点地址,但是仅仅这样是拿不到头节点的,所以需要定义下面的结构;
定义存放队列链表的头节点和尾结点的结构:
typedef struct Queue
{
QNode* head;//存放队首
QNode* tail;//存放队尾
}Queue;
初始化、摧毁、入队、出队...函数声明:
//队列初始化
void QueueInit(Queue* pq);
//队列摧毁
void QueueDestroy(Queue* pq);
//入队
void QueuePush(Queue* pq, QueueDate x);
//出队
QueueDate QueuePop(Queue* pq);
//判断是否存在元素:
bool QueueEmpty(Queue* pq);
综上:
★Queue.h(头文件)代码实现:
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
/*<队列> 实现:链表
* 1.入列 2.出列 3.摧毁列表 上述遵循队列的先进先出原则
*/
typedef int QueueDate;
//下面定义链表结构 -- 单链表 单链表的话需要单独定义两个指针一个指向头一个指向尾,方便进行插入和出列
typedef struct QListNode
{
QueueDate x;
struct QListNode* next;//存放下一指针的
}QNode;
typedef struct Queue
{
QNode* head;//存放队首
QNode* tail;//存放队尾
}Queue;
//队列初始化
void QueueInit(Queue* pq);
//队列摧毁
void QueueDestroy(Queue* pq);
//入队
void QueuePush(Queue* pq, QueueDate x);
//出队
QueueDate QueuePop(Queue* pq);
//判断是否存在元素:
bool QueueEmpty(Queue* pq);
注意#define _CRT_SECURE_NO_WARNINGS 1的细节可以了解上面的Stack.h文件里面的介绍。
2.Queue.c
队列实现的核心代码文件。
队列初始化:
//队列初始化,让队列的头和尾指向空
void QueueInit(Queue* pq)
{
assert(pq);//防止空指针引用
pq->head = pq->tail = NULL;
}
队列摧毁:
//队列摧毁 将堆内存申请的空间回收
void QueueDestroy(Queue* pq)
{
assert(pq);//查看是否空指针引用
while (pq->head)
{
QNode* temp = pq->head;
pq->head = temp->next;
free(temp);
}
pq->tail = NULL;
}
注意此时要摧毁的是链表,可不只是把头和尾空间释放掉,因为有可能出现两个以上啊,所以需要循环遍历,把链表的每一个节点空间都释放掉,然后将头和尾置为空即可。
入队:
//入队 普通单链表进行尾插
void QueuePush(Queue* pq, QueueDate x)
{
assert(pq);
QNode* newNode = (QNode*)malloc(sizeof(QNode));
if (newNode == NULL)
{
perror("QueuePush");
exit(-1);//不正常情况退出进程
}
newNode->x = x;
newNode->next = NULL;
if (pq->head == NULL)//第一次进
{
pq->head = pq->tail = newNode;
}
else//正常入队
{
pq->tail->next = newNode;
pq->tail = newNode;//保存尾元素
}
}
正常单链表进行尾插,注意区分是否第一次进即可,然后时刻记住tail是保持尾结点的地址的。
出队:
//出队 先进先出出头节点,然后将头节点更换成下一个节点
QueueDate QueuePop(Queue* pq)
{
assert(pq && !QueueEmpty(pq));//判断是否空指针引用或者是否空队列
QueueDate x = pq->head->x;
if (pq->head == pq->tail)//只有一个元素
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QNode* temp = pq->head->next;
free(pq->head);
pq->head = temp;
}
return x;
}
出队可不能像数组实现的那样简简单单覆盖就好了,此时这个单独的堆空间需要被回收,当然,此时要注意是否头和尾指向同一处(空或者只有一个数据),此时头改变尾也要改变。
判断是否存在元素:
//判断是否存在元素
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL;//是空表明没有元素,返回真,否则为假
}
综上:
★Queue.c(队列实现文件)代码实现:
#include"Queue.h"
//队列实现区
//队列初始化,让队列的头和尾指向空
void QueueInit(Queue* pq)
{
assert(pq);//防止空指针引用
pq->head = pq->tail = NULL;
}
//队列摧毁 将堆内存申请的空间回收
void QueueDestroy(Queue* pq)
{
assert(pq);//查看是否空指针引用
while (pq->head)
{
QNode* temp = pq->head;
pq->head = temp->next;
free(temp);
}
pq->tail = NULL;
}
//入队 普通单链表进行尾插
void QueuePush(Queue* pq, QueueDate x)
{
assert(pq);
QNode* newNode = (QNode*)malloc(sizeof(QNode));
if (newNode == NULL)
{
perror("QueuePush");
exit(-1);//不正常情况退出进程
}
newNode->x = x;
newNode->next = NULL;
if (pq->head == NULL)//第一次进
{
pq->head = pq->tail = newNode;
}
else//正常入队
{
pq->tail->next = newNode;
pq->tail = newNode;//保存尾元素
}
}
//出队 先进先出出头节点,然后将头节点更换成下一个节点
QueueDate QueuePop(Queue* pq)
{
assert(pq && !QueueEmpty(pq));//判断是否空指针引用或者是否空队列
QueueDate x = pq->head->x;
if (pq->head == pq->tail)//只有一个元素
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QNode* temp = pq->head->next;
free(pq->head);
pq->head = temp;
}
return x;
}
//判断是否存在元素
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL;//是空表明没有元素,返回真,否则为假
}
然后就是测试环节了,大家可以随便测试,若有问题欢迎进行讨论指出!谢谢,下面是我的测试代码:
测试代码:
#include"Queue.h"
//队列测试区
void test()
{
Queue q;
QueueInit(&q);
//测试入队
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
QueuePush(&q, 4);
//出队测试
while (!QueueEmpty(&q))
{
printf("%d ", QueuePop(&q));
}
printf("\n");
QueueDestroy(&q);
}
int main()
{
test();//测试代码
return 0;
}