前言
本章节较为抽象,我们先讲解概念,再进行举例。如有问题,欢迎评论区提问质疑。
栈
1.概念与结构
栈:只允许在固定的一端进行插入和删除元素操作的线性表。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出(Last In First Out)的原则。
大家不妨把它想象成这么个杯子(栈),杯子里面是不同的液体(数据)。我们在加入新液体(入栈)的时候呢,新加入的液体会在最上层(大家别较真密度问题,就是一个比喻帮助大家理解)。我们如果想取出下层的液体呢,就需要将上面的液体倒掉(这就是栈结构的先进后出原则)。
入栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
2.栈的框架
一个数据结构通常具备增、删、查、改等基本功能。栈遵循后进先出原则,操作主要集中在栈顶,其入栈和出栈操作分别对应增和删。而由于栈不能遍历且只能在一端操作,它没有常规意义上的查找功能。
typedef char STDataType;//方便更改存放数据的类型
typedef struct Stack
{
STDataType* a;//数组指针
int top;//栈顶位置
int capacity;//数据结构容量
}ST;
// 初始化栈
void STInit(ST* ps);
// 销毁栈
void STDestroy(ST* ps);
// ⼊栈
void STPush(ST* ps, STDataType x);
//出栈
void STPop(ST* ps);
//取栈顶元素
STDataType STTop(ST* ps);
//栈是否为空
bool STEmpty(ST* ps);
3.栈的详解
(1)初始化栈
顾名思义,该函数的作用是将结构体中的数据全部初始化,指针置为空,整型置为0。
void STInit(ST* ps)
{
assert(ps);//判断ps不为空
ps->arr = NULL;
ps->capacity = ps->top = 0;
}
(2)销毁栈
该函数是在不再需要某个栈时的操作。将动态分配的内存释放掉,指针指向空,将其余数据归为初始状态。
void STDestroy(ST* ps)
{
assert(ps);
if (ps->arr)
{
free(ps->arr);//释放内存
}
ps->arr = NULL;//指针指向空,防止野指针出现
ps->top = ps->capacity = 0;
}
(3)入栈
该指针是向栈中压入数据时使用的。其主要功能是判断空间是否足够,如果不够,开辟新的空间。足够则将数据压入栈中即可。
void StackPush(ST* ps, STDataType x)
{
assert(ps);
//1.判断空间是否足够
//满足该if条件,则空间已满
if (ps->capacity == ps->top)
{
//若空间为0,则赋初值为4.若非0,则开辟出原有空间2倍的空间
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
STDataType* tmp = (STDataType*)realloc(ps->arr, newCapacity * sizeof(STDataType));
if (tmp == NULL)//判断空间是否开辟成功
{
//未成功开辟
perror("realloc fail!");//显示报错
exit(1);//若空间不足,非正常退出程序
}
//成功开辟
ps->arr = tmp;//赋值为新开辟的地址
ps->capacity = newCapacity;//将容量更新
}
//空间足够,直接赋值即可
ps->arr[ps->top++] = x;
}
(4)判断栈是否为空
这个函数通常在出栈函数和获取栈顶元素时被调用。因为当栈为空时,我们将不能再执行出栈或者获取栈顶元素操作。
bool StackEmpty(ST* ps)
{
assert(ps);//判断ps指针不指向空
return ps->top == 0;
}
(5)出栈
该出栈函数执行的是栈的标准出栈操作。遵循后进先出原则,出栈时将栈顶指针top
减 1,使原栈顶元素被移除,新栈顶变为原栈顶元素的下一个。值得一提的是,出栈操作需在栈不为空时进行。
void StackPop(ST* ps)
{
//判ps指针是否为空
assert(ps);
//判该数据结构是否为空
assert(!StackEmpty(ps));
//出栈时别忘了给栈顶减1
--ps->top;
}
(6)取栈顶元素
前面说过,我们的栈是不能被遍历的。那么我们在操作时就不能直接去对指针解引用访问数组,不然这和顺序表也就没什么区别了。所有我们需要封装一个专门调取栈顶元素的函数。
//取栈顶元素
STDataType StackTop(ST* ps)
{
assert(ps);
assert(!StackEmpty(ps));
return ps->arr[ps->top - 1];
}
4.小试牛刀
题目链接:. - 力扣(LeetCode)
题目信息
源码附注释
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include <string.h>
typedef struct stack
{
char* arr;
int capacity;//栈容量
int top;//数据个数
}ST;
void StackPush(char x,ST* ps)//进栈
{
assert(ps);
if(ps->capacity==ps->top)//满栈
{
int newcapacity=ps->capacity==0?4:ps->capacity*2;
char* tem=(char*)realloc(ps->arr,newcapacity);
assert(tem);//创建新空间失败
ps->arr=tem;
}
ps->arr[ps->top++]=x;//将栈顶放置为新数据并将栈顶加一
}
bool StackEmpty(ST* ps)
{
// 检查传入的指针是否有效,如果指针为空,则程序会在断言处终止并给出错误提示
assert(ps);
// 如果栈顶指针为 0,表示栈中没有任何元素,即栈为空
return ps->top == 0;
}
void StackPop(ST* ps)//出栈
{
assert(ps);
assert(!StackEmpty(ps));//如果top==0返回值为ture,故非
--ps->top;
}
void InitStack(ST* ps)//初始化栈
{
assert(ps);
ps->arr = NULL;
ps->capacity=0;
ps->top=0;
}
void DestroyStack(ST* ps)//销毁栈
{
assert(ps);
free(ps->arr);
ps->arr = NULL;
ps->capacity = 0;
ps->top = 0;
}
char StackTop(ST* ps)//用于调取栈顶元素的函数
{
assert(ps);
assert(!StackEmpty(ps));//如果top==0返回值为ture,故非
return ps->arr[ps->top-1];//返回栈顶元素
}
int main()
{
ST stack;
InitStack(&stack);
char arr[100001];
gets(arr);
int flag=1;
for(int i=0;i<strlen(arr);i++)
{
if(arr[i]=='{'||arr[i]=='['||arr[i]=='(')//如果是左半括号
{
StackPush(arr[i],&stack);//入栈
flag=0;
}
else
{
if(!StackEmpty(&stack)==false)//
{
flag=0;
break;
}
//如果arr数组中有右半括号,看栈顶元素是否与之对应
if(arr[i]=='}'&&StackTop(&stack)=='{')
{
StackPop(&stack);
flag=1;
}
else if(arr[i]==']'&&StackTop(&stack)=='[')
{
StackPop(&stack);
flag=1;
}
else if(arr[i]==')'&&StackTop(&stack)=='(')
{
StackPop(&stack);
flag=1;
}
else
{
flag=0;
DestroyStack(&stack);
break;
}
}
}
if(strlen(arr)==0)//当什么都没有输入,结果为false
{
flag=0;
}
if(flag==1)
{
printf("true");
}
else
{
printf("false");
}
return 0;
}
需要注意的是,我这里写的是完整代码,而力扣上提交则是提交一个函数。代码截图如下:
队列
1.概念与结构
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊性表,队列具有新进先出的特性。
队列嘛,顾名思义,就像是一排数据结构在站队。新增添的数据只能排在队伍的最后,而想要出队列则需要从最前面出来。如图:
如果将栈喻为杯子,那队列就是管子,从一端进,另一端出。
入队列:进行插入操作的一端成为队尾。
出队列:进行删除操作的一端称为队头。
2.队列框架
队列遵循先进先出原则,操作主要在队首(出队)和队尾(入队)。其入队操作(QueuePush)
对应增加元素,出队操作(QueuePop)
对应删除元素。对于查找功能,由于队列一般只能从队首开始依次遍历节点,相对效率较低,但也可通过遍历队列节点来查找特定元素。队列没有像顺序表那样可直接定位的高效查找方式。整体而言,队列的操作主要围绕入队、出队以及获取队首和队尾元素展开。
//队列结点结构
typedef struct QueueNode
{
//存放队列的结点所需变量
}QNode;
//队列结构
typedef struct Queue
{
//存放队列所需变量
}Queue;
//初始化队列
void QueueInit(Queue* pq);
//销毁队列
void QueueDestroy(Queue* pq);
// ⼊队列,队尾
void QueuePush(Queue *pq, QDataType x);
// 出队列,队头
void QueuePop(Queue* pq);
//取队头数据
int QueueFront(Queue* pq);
//取队尾数据
int QueueBack(Queue* pq);
//队列判空
bool QueueEmpty(Queue* pq);
3.队列详解
(1)队列节点结构讲解
我们的队列是通过链表来实现的。因此包含两个元素,一是我们存放的数据,二便是指向下一节点的指针。代码如下
//我们这里用链表举例来讲解队列
typedef struct QueueNode//队列节点
{
int data;//存放数据
struct QueueNode* next;//指向下一节点的指针
}QueueNode;
(2)队列结构体讲解
这里的队列结构体包含头结点的地址,可在此位置进行数据的插入操作;还包含尾节点的地址,用于在该位置进行数据的删除操作;同时,还有一个整型变量size
,用于统计队列中的数据个数。
//完整队列包含的基本数据
typedef struct Queue
{
struct QueueNode* phead;//该指针指向队列头
struct QueueNode* ptail;//该指针指向队列尾
int size;//队列中存放的数据个数
}Queue;
(3)队列初始化
不用我过多解释了吧?指针置空,整型归0。
//队列初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead=pq->ptail=NULL;
pq->size=0;
}
(4) 队列判空
我们虽然判断了指针pq不为空,但是不排除pq指向了一个空队列的情况。这种情况下我们判断一下头指针只想是否为空即可。
//队列判空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->phead==NULL;
}
(5)入队列
这里的逻辑就是尾插。做法很简单,创建新节点以后将存储的数据放入节点即可。最后别忘了将为节点更新为最新创建的节点并给size++,我们来看一下示意图:
若队列为空,我们直接创建一个新的节点,并让头尾指针都直系那个该节点即可,如图:
若队列不为空,我们就创建一个新节点,先让当前尾节点指向最新结点,再将最新节点作为尾结点,示意图如下:
插入前:
插入后:
详细步骤请看源码:
//入队列,从队尾入
void QueuePush(Queue* pq, int x)
{
assert(pq);
//申请新结点
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(1);//非正常退出
}
newnode->data = x;
newnode->next = NULL;
//队列为空
if (pq->phead == NULL)
{
//队列为空时插入的第一个元素既是队头也是队尾
pq->phead = pq->ptail = newnode;
}
//队列不为空
else
{
pq->ptail->next = newnode;//先让当前尾节点指向最新结点
pq->ptail = pq->ptail->next;//再将最新节点作为尾结点
}
pq->size++;
}
(6)出队列
这里的出队列要从头出。换句话说,就是将头结点删除了。示意图如下:
删除前:
删除后:
详细步骤请见代码:
//出队列,从队头出
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
//当前队列只有一个结点的情况
if (pq->ptail == pq->phead)
{
free(pq->phead);//释放该结点
pq->phead = pq->ptail = NULL;//置空
}
else
{
//删除队头元素
QueueNode* next = pq->phead->next;//将第二个结点的地址存入next
free(pq->phead);//释放当前头结点
pq->phead = next;//将next作为头结点
}
pq->size--;
}
(7)功能演示
其实就是刚刚的代码片段拼凑起来,并调用了几个出入队列的函数
演示代码如下:
#include<stdio.h>
#include<stdbool.h>
#include<stdlib.h>
#include<assert.h>
//我们这里用链表举例来讲解队列
typedef struct QueueNode//队列节点
{
int data;//存放数据
struct QueueNode* next;//指向下一节点的指针
}QueueNode;
typedef struct Queue
{
struct QueueNode* phead;//该指针指向队列头
struct QueueNode* ptail;//该指针指向队列尾
int size;//队列中存放的数据个数
}Queue;
//队列初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
//入队列,从队尾入
void QueuePush(Queue* pq, int x)
{
assert(pq);
//申请新结点
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(1);//非正常退出
}
newnode->data = x;
newnode->next = NULL;
//队列为空
if (pq->phead == NULL)
{
//队列为空时插入的第一个元素既是队头也是队尾
pq->phead = pq->ptail = newnode;
}
//队列不为空
else
{
pq->ptail->next = newnode;//先让当前尾节点指向最新结点
pq->ptail = pq->ptail->next;//再将最新节点作为尾结点
}
pq->size++;
}
//队列判空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->phead == NULL;
}
//出队列,从队头出
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
//当前队列只有一个结点的情况
if (pq->ptail == pq->phead)
{
free(pq->phead);//释放该结点
pq->phead = pq->ptail = NULL;//置空
}
else
{
//删除队头元素
QueueNode* next = pq->phead->next;//将第二个结点的地址存入next
free(pq->phead);//释放当前头结点
pq->phead = next;//将next作为头结点
}
pq->size--;
}
main()
{
Queue q;//创建队列q
QueueInit(&q);//初始化q队列
QueuePush(&q, 1);//入队列
QueuePush(&q, 2);//入队列
QueuePush(&q, 3);//入队列
QueuePop(&q);//出队列
QueuePop(&q);//出队列
}
演示结果
我们先看一下,将1 2 3都放入队列中队列中的队列,如下图:
下面我们将出队列两次,看一下现在的队列:
队列是一种遵循先进先出原则的数据结构,通常由队首指针、队尾指针和元素个数等组成,支持入队(在队尾添加元素)、出队(从队首移除元素)等操作。
4.小试牛刀
今天搞累了,数据好的话明天就拓展一下。