目录
一、栈
1.栈的概念
首先,此处所指的栈是数据结构中的一种数据存储方式,与操作系统
栈是一种特殊的线性表,其只允许在固定的一段进行插入和删除元素操作。其中进行数据插入和删除的一段称为“栈顶”,另一端则称为“栈底”。栈中的元素遵守“后进先出”或者“先进后出”的原则。
而在栈的操作中,主要分为两种操作方式,分别是压栈和出栈。
压栈:顾名思义,就是将数据压如栈中。栈的插入操作就叫做压栈。当然,有时又被叫做进栈或者入栈。数据从栈顶进入。
出栈:出栈是压栈的相反的操作,即栈的删除就叫做出栈。数据从栈顶出去。
栈的逻辑结构就如上图所示,栈的操作遵循“后进先出”原则,数据从栈顶入,同时也从栈底出。
2、栈的实现
实现栈可以用两种比较基础的数据结构——顺序表或链表的形式实现。但这两种方式各有优点。到底选用哪种形式便值得思考。
首先我们可以明确的是栈是遵循“后进先出”原则,这也就说明栈从根本上来说是需要“尾插尾删”。有了这一条件后便很好的做出抉择了。
顺序表是使用数组的方式实现的,而数组便是对“尾插尾删”有着较强的适用性。在使用数组的形式时,我们只需要将记录数组末尾下标的值++或--即可。
链表则需要找到链表的尾部,就算同样采用了指针去记录末尾的数据位置,但在++或--后都需要手动将末尾数据的指针指向NULL。相比起来便不如数组便利。
因此此处采用的是顺序表的方式实现。
(1).头文件
以下代码是一个相对简单的栈中需要包含的头文件和主函数
但这里值得一提的是使用了tyepdef int STDataType;来对int重命名。此处的重命名旨在为下面的函数中的数据传入做铺垫。这样在后续的使用中如果需要改变数据的传入类型,则只需将int修改即可,而无需在函数中一个个的去修改
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* a;//数据
int top;//栈顶
int capacity;//容量
}Stack;
void StackInit(Stack* st);//数据初始化
void StackDistory(Stack* st);//数据销毁
void StackPrint(Stack* st);//数据打印
void StackPush(Stack* st, STDataType x);//从栈顶插入数据并放到栈底(入栈)
void StackPop(Stack* st);//删除栈顶数据(出栈)
STDataType StackTop(Stack* st);//获取栈顶数据
size_t StackSize(Stack* st);//获取栈中有效元素个数
bool StackEmpty(Stack* st);//检测栈是否为空,为空返回非0结果,不为空返回0
(2).数据初始化和数据销毁
这两个函数相对简单,就不多赘述
void StackInit(Stack* st)//数据初始化
{
assert(st);
st->a = NULL;
st->top = st->capacity = 0;
}
void StackDistory(Stack* st)//数据销毁
{
assert(st);
free(st->a);
st->a = NULL;
st->top = st->capacity = 0;
}
(3).数据打印
此函数主要是为了方便对写好的其他函数进行测试时能够更为直观的看到数据的变动,可写可不写
void StackPrint(Stack* st)//数据打印
{
int i = 0;
while (i < st->top)
{
printf("%d ", st->a[i]);
++i;
}
printf("\n");
}
(4).栈的尾插(入栈)
void StackPush(Stack* st, STDataType x)//栈的尾插(入栈)
{
assert(st);
if (st->top == st->capacity)
{
int newcapacity = (st->capacity == 0) ? 4 : 2 * st->capacity;
STDataType* tmp = (STDataType*)realloc(st->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
}
st->a = tmp;
st->capacity = newcapacity;
}
st->a[st->top] = x;
st->top++;
}
(1)在栈的尾插中,首先需要对传入的指针st进行断言,在st不为NULL时才能继续。
assert(st);
(2)在前面也讲到过栈的实现此处采用的是顺序表形式,因此,何时进行栈的扩容,扩容为多少则是一个问题。
前一个问题相对简单,用结构体中记录栈顶位置的top与capacity相比,相等则扩容。
if (st->top == st->capacity)
(3)在此处,我们在结构体中已经设置了capacity记录栈空间的大小。因此我们便定义一个临时变量newcapacity来决定扩容的大小,在capacity=0时,赋予其4,反之,则进行二倍的扩容。二倍扩容本质上是以2^n进行栈容量扩容。,因此就算后续的栈空间存在浪费,也顶多是数据总数量的二倍,且扩容的大小呈指数增长,哪怕20亿的数据空间也不过扩容30次
int newcapacity = (st->capacity == 0) ? 4 : 2 * st->capacity;
(4)此处我们也新定义了一个STDataType*类型的tmp变量,此变量使用realloc()函数开辟空间,值得一提是,在该函数的定义中,当传入的数据指针指向NULL时,会有和malloc()函数一样的作用,可以为其开辟初始空间容量。同时,因为realloc()是在堆上开辟一个连续的空间,因此在连续空间不够时可能会开辟失败,在此处最好检查一下
STDataType* tmp = (STDataType*)realloc(st->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
}
(5)最后一步则很简单,只需将数据传入结构体st中的指针a,并让top往前走一步即可
st->a[st->top] = x;
st->top++;
(5).栈的删除(出栈)
栈的删除比较简单,因为是以数组的形式存储,因此只需--top即可
void StackPop(Stack* st)//删除栈顶数据(出栈)
{
assert(st);
assert(!StackEmpty(st));
st->top--;
}
(6).获取栈顶数据及获取栈中有效元素个数
这两个函数也非常简单,栈顶数据只需返回下标top-1的数据,而元素个数返回top即可
STDataType StackTop(Stack* st)//获取栈顶数据
{
assert(st);
assert(!StackEmpty(st));
return st->a[st->top - 1];
}
size_t StackSize(Stack* st)//获取栈中有效元素个数
{
assert(st);
return st->top;
}
(7).判断栈是否为空
当栈为空时,top的值为0,因此只需返回st->top==0即可,top为0返回真,反之为假
bool StackEmpty(Stack* st)//检测栈内数据是否为空,为空返回非0结果,不为空返回0
{
assert(st);
return st->top == 0;
}
3.题目实践
这是一道LeetCode上的oj题,我们用这道比较简单的题来讲解一下栈的基本使用
在这道题中我们可以看到,主要的要求就是对相同类型的左右括号进行匹配。难点就在于当相同类型的左右括号中间掺杂了一些其他类型的括号该如何处理。因此,我们可以利用栈“先进后出”的特性,当栈顶的数据与下一个即将入栈的数据类型相匹配时,就将数据出栈,反之则插入。
要注意的是,此处是使用c语言实现的,c语言的库中并未包含有栈的实现,因此需要我们自行实现栈后将栈的代码拷入
(1)定义栈的结构体并初始化
Stack st;
StackInit(&st);
(2)判断入栈数据
入栈数据是左括号便入栈,反之则将其与前一个数据相比较。
if(StackEmpty(&st))这条语句解决的是入栈数据为右括号时如果栈中没有数据时的情况。后面的则就是简单的判断,如果符号不匹配则返回false,反之继续走
while(*s)
{
if(*s == '(' || *s == '{' || *s == '[')
{
StackPush(&st, *s);
}
else
{
if(StackEmpty(&st))//检测数据此时是否为空,为空则返回false
{
return false;
}
STDataType tmp = StackTop(&st);
StackPop(&st);
if((*s == ')' && tmp != '(')
|| (*s == ']' && tmp != '[')
|| (*s == '}' && tmp != '{'))
{
return false;
}
}
++s;
}
(3)对栈的收尾处理和销毁
此处主要是处理只传入左括号而不传出右括号导致的栈中数据残留和对栈的销毁
if(StackEmpty(&st) == 0)//数据不为空,则返回false
{
return false;
}
StackDistory(&st);
二、队列
1.队列的概念
队列与栈是相反的,栈是“先进后出”,队列则是“先进先出”。在队列中,数据从队尾进入,队头删除,简单来说,就是“尾插头删”。
队列的操纵也主要是两项,即“入队”和“出队”
入队:进行插入操作的一端称为队尾
出队:进行删除操作的一段称为队头
逻辑结构就如图所示,数据从队尾进入,队头出去
2.队列的实现
队列的实现与栈不同,队列这里是采用的链表的形式实现,而非数组。
原因在于队列的主要操作就是“尾插”和“头删”。如果和栈一样采取数组的形式实现,虽然尾插非常轻松,但是当要进行头删时,便需要将每个数组的数据都进行挪动,其中的时间复杂度为O(N),无疑会浪费许多的时间
而采用链表的形式,头删只需将链表头节点head++,并将需要删除的头部节点空间free掉即可。而尾插虽然面临着找尾结点的问题,但可以用在定义结构体时定义一个尾部指针指向尾部空间的方式加以解决,这样整个程序的效率也会相应提升。
(1)头文件及函数声明
在此处,和栈的实现一样,同样是typedef int AEDataTyoe;用以后续方便修改传入的数据类型。但此处和栈不同的是,定义了两个结构体,一个结构体是存储的数据和链表指针,另一个结构体则是包含了head与tail两个指向QueueNode的结构体头指针和尾指针
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int QEDataType;
typedef struct QueueNode
{
QEDataType data;
struct QueueNode* next;
}QNode;
typedef struct Queue
{
QNode* head;
QNode* tail;
}Queue;
void QueueInit(Queue* qn);//初始化队列
void QueueDistory(Queue* qn);//销毁队列
void QueuePrint(Queue* qn);//打印数据
void QueuePush(Queue* qn, QEDataType x);//从尾部向队列插入数据(入队列)
void QueuePop(Queue* qn);//从头部删除队列数据(出队列)
QEDataType QueueFront(Queue* qn);//获取队列头部数据
QEDataType QueueBack(Queue* qn);//获取队列尾部数据
int QueueSize(Queue* qn);//获取队列中的元素个数
int QueueEmpty(Queue* qn);//判断队列是否为空,为空返回非0值,非空则返回0
(2)队列初始化和销毁
此处要将Queue结构体内的两个结构体指针初始化为NULL。销毁也是将链表遍历一遍,逐个销毁即可。
void QueueInit(Queue* qn)//初始化队列
{
assert(qn);
qn->head = NULL;
qn->tail = NULL;
}
void QueueDistory(Queue* qn)//销毁队列
{
assert(qn);
QNode* cur = qn->head;
while (cur)
{
QNode* del = cur;
cur = cur->next;
free(del);
}
qn->head = qn->tail = NULL;
}
(3)数据打印
此函数和栈的打印是一个作用,都是为了后续测试函数方便,可写可不写
void QueuePrint(Queue* qn)//数据打印
{
assert(qn);
QNode* tmp = qn->head;
while (tmp)
{
printf("%d ", tmp->data);
tmp = tmp->next;
}
printf("\n");
}
(4)数据插入(尾插)
void QueuePush(Queue* qn, QEDataType x)//从尾部向队列插入数据(入队列)
{
assert(qn);
QNode* tmp = (QNode*)malloc(sizeof(QNode));
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
if (qn->head == NULL)
{
qn->head = qn->tail = tmp;
qn->head->data = x;
}
else
{
qn->tail->next = tmp;
qn->tail = qn->tail->next;
qn->tail->data = x;
}
qn->tail->next = NULL;
}
(1)数据插入时,对每个数据空间都要开辟,使用malloc()函数开辟。并定义一个临时变量tmp进行接收,在开辟完后为防止开辟失败最好检验一下tmp==NULL,成立则开辟失败,直接结束程序
QNode* tmp = (QNode*)malloc(sizeof(QNode));
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
(2)在插入时,首先要判断head是否为NULL,为NULL则是链表为空,只需将head和tail都指向tmp即可。反之head不为空,则只需将tail->next指向tmp,并修改tail的值,将数据传入即可。当然,在最后都需要将tail->next指向NULL,标志链表的结束
if (qn->head == NULL)
{
qn->head = qn->tail = tmp;
qn->head->data = x;
}
else
{
qn->tail->next = tmp;
qn->tail = qn->tail->next;
qn->tail->data = x;
}
qn->tail->next = NULL;
(5)数据删除(出队列)
队列的数据删除是头删,因此先定义一个临时变量del记录head的值,再将head修改为head->next,并将del释放掉并使del=NULL即可。
void QueuePop(Queue* qn)//从头部删除队列数据(出队列)
{
assert(qn);
assert(!QueueEmpty(qn));
QNode* del = qn->head;
qn->head = qn->head->next;
if (qn->head == NULL)
{
qn->tail = qn->tail->next;
}
free(del);
del = NULL;
}
(6)获取头部数据和尾部数据
这两个函数非常简单,只需返回head和tail中记录的data的值即可
QEDataType QueueFront(Queue* qn)//获取队列头部数据
{
assert(qn);
assert(!QueueEmpty(qn));
return qn->head->data;
}
QEDataType QueueBack(Queue* qn)//获取队列尾部数据
{
assert(qn);
assert(!QueueEmpty(qn));
return qn->tail->data;
}
(7)获取队列中元素个数
该函数定义一个临时变量i,遍历队列并相应++i,然后返回i即可
int QueueSize(Queue* qn)//获取队列中的元素个数
{
assert(qn);
int i = 0;
QNode* sz = qn->head;
while (sz)
{
++i;
sz = sz->next;
}
return i;
}
(8)判断队列是否为空
直接返回判断head==NULL和tail==NULL的值即可,为空则为真,反之为假
int QueueEmpty(Queue* qn)//判断队列是否为空,为空返回非0值,非空则返回0
{
assert(qn);
return qn->head == NULL && qn->tail == NULL;
}
3.题目实践
在该题中,题目要求用队列实现栈。而栈和队列的主要区别就在于“栈”是“后进先出”,要求尾插尾删,而队列则是“先进先出”,要求头删尾插 。
而题目中也已经清楚的说明了,要使用两个队列实现栈。因此就要从队列的性质中出发。
队列是“先出先出”,因此,我们就可以将所有要传入的数据都放入一个队列中,此处就是模拟栈的入栈。当需要删除栈中数据时,若队列中的数据个数为n,就将该队列中n-1个数据依次放入另一个为空的队列中,剩下的第n个数据就是栈中需要删除的“栈顶数据”。
在将该数据删除后,若后续需要继续删除,则再次重复该步骤即可。而在插入数据时,就要把数据插入到不为空的队列中。
此处的关键就在于要始终保持一个队列为空,以便于数据的放入
(1)创建结构体
此处因为使用的是c语言,库中并没有存在队列,因此在该题前面直接复制了队列的实现。此处的结构体中包含的是两个队列中所使用的结构体
typedef struct {
Queue p1;
Queue p2;
} MyStack;
(2)结构体空间申请
此处与前面的队列不同的是,需要手动为结构体申请一块空间
MyStack* myStackCreate() {
MyStack* obj = (MyStack*)malloc(sizeof(MyStack));
QueueInit(&obj->p1);
QueueInit(&obj->p2);
return obj;
}
(3)插入数据(入栈)
哪个队列为空,便将数据放入该队列
void myStackPush(MyStack* obj, int x) {
assert(obj);
if(!QueueEmpty(&obj->p1))
{
QueuePush(&obj->p1, x);
}
else
{
QueuePush(&obj->p2, x);
}
}
(4)数据删除(出栈)
在此处,为避免判断哪个队列为空则删除哪个队列导致的需要将删除代码写两遍的问题,直接定义了Empty和NoEmpty两个结构体指针分别指向两个结构体,将空队列放入Empty中,不为空的队列放入NoEmpty中。
随后便是遍历队列,将队列中的n-1个数据拷贝到空队列中,并在NoEmpty中的第n个数据删除。因为此处要返回栈顶数据,所以在删除前定义一个临时变量top记录栈顶数据后再删除,最后返回top即可
int myStackPop(MyStack* obj) {
assert(obj);
Queue* Empty = &obj->p1;
Queue* NoEmpty = &obj->p2;
if(!QueueEmpty(&obj->p1))
{
Empty = &obj->p2;
NoEmpty = &obj->p1;
}
int time = QueueSize(NoEmpty);
while(time > 1)
{
QueuePush(Empty, QueueFront(NoEmpty));
QueuePop(NoEmpty);
--time;
}
int top = QueueFront(NoEmpty);
QueuePop(NoEmpty);
return top;
}
(5)返回栈顶数据
直接返回不为空的队列中的队尾数据即可
int myStackTop(MyStack* obj) {
assert(obj);
if(!QueueEmpty(&obj->p1))
{
return QueueBack(&obj->p1);
}
else
{
return QueueBack(&obj->p2);
}
}
(6)判断队列是否为空和队列释放
bool myStackEmpty(MyStack* obj) {
return QueueEmpty(&obj->p1) && QueueEmpty(&obj->p2);
}
void myStackFree(MyStack* obj) {
assert(obj);
QueueDistory(&obj->p1);
QueueDistory(&obj->p2);
free(obj);
}
该题为用栈实现队列。同该题也已经明确给出要使用两个栈。同样的,在模拟队列的输入时,直接将数据放入一个栈中即可。重点依然在模拟队列中数据的删除。
用栈模拟的队列的删除,假设定义了pushST和popST两个栈,可以先将输入的n个数据都放入到pushST中,在需要删除时,将栈中的n个数据全部放入popST中。但在此处与用队列模拟栈不同的是,再次删除时不需要将数据重新放回pushST中,而是继续删除popST中的数据。
如果后续又在pushST中插入数据,在删除时则需确保popST为空
(1)结构体
声明两个Stack结构体,pushST为插入数据的栈,popST为删除数据的栈
typedef struct {
Stack pushST;
Stack popST;
} MyQueue;
(2)初始化
同样的要手动申请一块MyQueue结构体空间
MyQueue* myQueueCreate() {
MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue));
StackInit(&obj->pushST);
StackInit(&obj->popST);
return obj;
}
(3)数据插入(入队列)
直接将数据插入pushST中即可
void myQueuePush(MyQueue* obj, int x) {
assert(obj);
StackPush(&obj->pushST, x);
}
(4)数据删除(出队列)
该函数也比较简单,唯一要注意的就是在已经拷贝数据到popST中后,如果要再次拷贝数据进去,必须在popST中的数据全部删除后才行
int myQueuePop(MyQueue* obj) {
assert(obj);
if(StackEmpty(&obj->popST))
{
STDataType time = StackSize(&obj->pushST);
while(time)
{
StackPush(&obj->popST, StackTop(&obj->pushST));
StackPop(&obj->pushST);
--time;
}
}
int top = StackTop(&obj->popST);
StackPop(&obj->popST);
return top;
}
(5)返回队头数据
与删除数据的函数基本一样,唯一的区别就是拷贝完后只需返回popST中的栈顶数据,无需删除
int myQueuePeek(MyQueue* obj) {
assert(obj);
if(StackEmpty(&obj->popST))
{
STDataType time = StackSize(&obj->pushST);
while(time)
{
StackPush(&obj->popST, StackTop(&obj->pushST));
StackPop(&obj->pushST);
--time;
}
}
int peek = StackTop(&obj->popST);
return peek;
}
(6)判断队列是否为空和释放
bool myQueueEmpty(MyQueue* obj) {
return StackEmpty(&obj->pushST) && StackEmpty(&obj->popST);
}
void myQueueFree(MyQueue* obj) {
StackDistory(&obj->pushST);
StackDistory(&obj->popST);
free(obj);
obj = NULL;
}
循环队列,顾名思义就是一个可以循环使用的队列,无需队列的删除和空间释放,空间一直存在。如果队列满了再插入数据,就从队头重新插入数据,将原数据覆盖。
在循环队列的实现中,与普通队列不同,此处选择的是数组的形式。原因在于循环队列只需尾插,头删时不需要修改原空间数据,因此就避免了数组头插头删时需要挪动数据的弊端。
在实现循环队列时,因为其没有一个队尾的标志,如果单纯依靠“队头的下标等于队尾的下标”来判断结束,但是因为队列为空时,队头的下标也会等于队尾的下标,因此这两种情况就难以区别。
为了处理这种状况,便采用了多开辟一个空间的做法。如循环队列的长度为4,那么我们就开辟5个空间。在返回时,始终只返回head和tail之间的数据。这样当队列满时,则会有tail减去head加上开辟空间数得到的值再模上开辟空间数,得到的值如果等于队列长度,则满,反之不满。而为空时则只需判断head与tail是否相等即可。
(1)结构体定义
typedef struct {
int* a;//队列数据存储空间
int front;//队头
int back;//队尾
int length;//队列存储空间个数
} MyCircularQueue;
(2)声明判断队列为满和队列为空函数
此处一般是不需要声明的,但该题中的代码可以看做是放在一个文件中,而题目中这两个函数放在最后面,所以前面的函数要使用的话就需要提前声明,或者自己改变这两个函数的位置
bool myCircularQueueIsEmpty(MyCircularQueue* obj);
bool myCircularQueueIsFull(MyCircularQueue* obj);
(3)结构体申请空间和初始化
MyCircularQueue* myCircularQueueCreate(int k) {
assert(k);
MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
obj->a = (int*)malloc(sizeof(int) * (k + 1));
obj->front = 0;
obj->back = 0;
obj->length = k + 1;
return obj;
}
(4)插入数据(入队)
因为是循环队列,存储空间需要反复使用,因此此处就用了obj->back = obj->back % obj->length;来使back的值始终处于队列长度之内
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {//队尾插入元素
if(myCircularQueueIsFull(obj))//检查队列是否满
return false;
obj->a[obj->back] = value;//向队列中插入元素
++obj->back;
obj->back %= obj->length;//当back超过k+1时使back回到队列中
return true;
}
(4)删除数据(出队列)
采用的是数组的形式存储,删除数据只需++front即可。但要注意是front的值也必须始终处于队列长度之内
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
{
return false;
}
++obj->front;
obj->front %= obj->length;
return true;
}
(5)获取队头数据
此处存在一个bug,当队头数据为-1时,便无法判断是否成功删除了数据。但因为题目要求返回-1,此处可不管。但自己写的时候要注意删除失败时不要返回一个固定值。
int myCircularQueueFront(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
{
return -1;
}
else
return obj->a[obj->front];
}
(6)获取队尾数据
此处与获取队头数据存在一样的bug
int myCircularQueueRear(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
{
return -1;
}
else
return obj->a[(obj->back - 1 + obj->length) % obj->length];
}
(7)判断队列是否为满和是否为空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->front == obj->back;
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return (obj->back - obj->front + obj->length) % obj->length == obj->length - 1;
}
(8)释放空间
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
obj->a = NULL;
obj->front = 0;
obj->back = 0;
obj->length = 0;
free(obj);
obj = NULL;
}