文章目录
一、栈
1.何为栈
2.栈的模拟实现
二、队列
1.何为队列
2.队列的模拟实现
前言
在之前我们已经学习了线性表中的顺序表和链表,我们学习了它们的一些基本性质以及如何去实现它们。但是我们在工作时那两种数据类型还远远不够,今天我们再来学习两种线性表(操作受限的线性表),这两种线性表有时候能起到事半功倍的效果。
一、栈
1.何为栈
首先我们先来学习栈,这是一种特殊的线性表,我们规定它只能够从一端存入数据或者取出数据。并且它存储取出还有着 先进后出FILO(first in last out)或者 后进先出LIFO(last in first out) 的顺序特点。
首先我们来介绍几个栈的专用名词:
栈底:我们将栈的一端称作为栈底,我们一般不对栈底数据进行操作;
栈顶:我们将栈的一端称作为栈顶,我们一般都是在栈顶进行各种操作。我们会定义一个指针top用来表示栈顶,top一般指向最后一个有效数据的上面那个空闲空间(这个是重点);
压栈/入栈/进栈:我们将数据从栈顶存入操作叫做压栈;
出栈:我们将数据从栈顶取出操作叫做出栈;
2.栈的模拟实现
在之前,我们学习顺序表和链表两种线性表,这两种线性表分别是使用数组和链表来实现。现在我们来考虑一下栈该用啥来实现呢?
a)当我们使用数组来实现:我们可以在数组的一端作为栈顶进行插入或者删除数据,其时间复杂度都是O(1),另一端数组下标为0咱们就把它当作栈底。虽然数组大小是一开始就定义好的,但是我们可以使用增容操作(我们增容操作时一般都是成倍增加的,因此增容几次就足够了)
b)当我们使用单链表来实现:当我们对单链表的表尾作为栈顶,然后我们进行插入操作时,其时间复杂度是O(1),但是我们进行删除操作时,其时间复杂度是O(N),因为我们要从头开始遍历整个链表来找到要删除结点的前一个结点。尽管,单链表咱们可以插入一个数据申请一个结点空间,只有不容易浪费空间,但这些结点的空间并不是连续的,因此并不是很好。主要还是时间复杂度上的差异。
c)当我们使用双向链表来实现:当我们将双向链表的一端作为栈顶,另一端作为栈底时,在我们进行插入删除时时间复杂度可能是O(1)。但是,我们将栈底与栈顶换过来时时间复杂度可能就是O(N).
综上所述,我们还是使用数组来实现栈比较合适。接下来,我来用代码进行模拟实现栈,同样的我们需要创建三个文件Stack.c , Stack.h ,text.c。
这个栈的实现与我们之前的顺序表实现基本是一样的,咱们就不做过多赘述了。
Stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* arr;//用数组来模拟实现栈
int capacity;//栈的空间大小
int top;//栈顶
}ST;
//初始化
void STInit(ST* ps);
//销毁
void STDesTroy(ST* ps);
//入栈
void STPush(ST* ps, STDataType x);
//出栈
void STPop(ST* ps);
//检查是否为空栈
bool STEmpty(ST* ps);
//读取栈顶数据
STDataType STTop(ST* ps);
//查询有效数据个数
int STSize(ST* ps);
Stack.c
#define _CRT_SECURE_NO_WARNINGS
#include"Stack.h"
//初始化
void STInit(ST* ps)
{
assert(ps);
ps->arr = NULL;
ps->capacity = ps->top = 0;//当top=0时,即栈顶==栈底时,则表示栈空
}
//销毁
void STDesTroy(ST* ps)
{
assert(ps);
if (ps->arr == NULL)
{
free(ps->arr);
}
ps->capacity = ps->top =0;
}
//入栈
void STPush(ST* ps, STDataType x)
{
assert(ps);
//1.判断栈的空间是否足够
if (ps->capacity == ps->top)//表示栈满了
{
//2.增容
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->arr, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("relloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newcapacity;
}
//3.入栈
ps->arr[ps->top++]= x;
}
//出栈
void STPop(ST* ps)
{
assert(ps);
assert(!STEmpty(ps));//这里面STEmpty的参数是传递地址的,由于ps是栈的指针即地址,故直接传递过去
ps->top--;
}
//检查是否为空栈
bool STEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;//当这个表达式成立时即为空栈,返回true
}
//读取栈顶数据
STDataType STTop(ST* ps)
{
assert(ps);
assert(!STEmpty(ps));
return ps->arr[ps->top - 1];
//注意:咱们将top指向最后一个有效数据的上面一个空闲位置,故我们想要查询时,根据下标就是top-1,注意不能是top--
//top--==top=top-1,这样就把栈顶改变了
}
//查询有效数据个数
int STSize(ST* ps)
{
assert(ps);
return ps->top;//由于数组是从0开始的,并且top指针是指向最后一个有效数据的上面一个空闲位置,所以ps->top就是有效数据的个数
}
test.c
#define _CRT_SECURE_NO_WARNINGS
#include"Stack.h"
void STtest()
{
ST st;//定义一个栈
STInit(&st);
//入栈4个数字
STPush(&st, 1);
STPush(&st, 2);
STPush(&st, 3);
STPush(&st, 4);
//printf("size:%d\n", STTop(&st));
//STPop(&st);
//printf("size:%d\n", STTop(&st));
//STDesTroy(&st);
//printf("size:%d", STTop(&st));
//编写一个循环将栈中的所有数据打印出来,注意只能一个一个从栈顶出来,出来一个打印一个直到栈空
while(!STEmpty(&st))
{
STDataType data = STTop(&st);
printf("%d ", data);
STPop(&st);
}
}
int main()
{
STtest();
return 0;
}
二、队列
1.何为队列
队列与栈一样,也是一种操作受限的线性表。它只能在一端存入数据,一端取出数据。它有着先进先出FIFO(first in last out)的特点。其实咱们只要稍微联想一下就行,我们平时排队的时候,是不是第一个来的排队首,然后队首的第一个人来进行操作,后面的人只能排到队尾,只能等前面的人都搞完才能轮到他。我们也来介绍一下关于队列的几个名词:
队头:我们把队列的一端当作为队头,我们只能在这一端进行插入操作;
队尾:我们把队列的另一端当作为队尾,我们只能在这一端进行删除操作;
入队:我们将数据存入队列的操作叫做入队。这个操作只能够在队头进行;
出队:我们将数据取出队列的操作叫做出队。这个操作只能在队尾进行。
2.队列的模拟实现
这个队列我们该用什么来进行实现呢?如果使用数组来实现的话,由于数组一端是不变的,我们无法对那一端进行操作,因此数组这个不行。然后,我们可以使用单链表来实现。链表中有头插尾插,头删尾删等操作,但是,尾插操作中,其时间复杂度是O(n),于是我们可以创建两个指针,一个头指针head,一个尾指针tail,那么我们尾删,头删的时间复杂度都是O(1)。接下来我们用代码来实现队列,同样我们用三个文件Queue.c,Queue.h,test.c来进行实现
Queue.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int QDatatype;
//定义队列结点
typedef struct QueueNode
{
QDatatype data;
struct QueueNode* next;
}QNode;
//定义队列
typedef struct Queue
{
QNode* head;
QNode* tail;
int size;
}Queue;
//初始化
void QInit(Queue* pq);
//销毁
void QDetroy(Queue* pq);
//判断队列是否为空
bool QEmepty(Queue* pq);
//入队列
void QPush(Queue* pq,QDatatype x);
//出队列
void QPop(Queue* pq);
//读队头数据
QDatatype QFront(Queue* pq);
//读队尾数据
QDatatype QBack(Queue* pq);
//队列有效数据个数
int QSize(Queue* pq);
Queue.c
#define _CRT_SECURE_NO_WARNINGS
#include"Queue.h"
//初始化
void QInit(Queue* pq)
{
assert(pq);
pq->head = pq->tail = NULL;
pq->size = 0;
}
//销毁
void QDetroy(Queue* pq)
{
assert(pq);
assert(!QEmepty(pq));
QNode* pcur = pq->head;
while (pcur)
{
QNode* next = pcur->next;
free(pcur);
pcur = next;
}
pq->head = pq->tail = NULL;
pq->size = 0;
}
//判断队列是否为空
bool QEmepty(Queue* pq)
{
assert(pq);
return pq->head == NULL && pq->tail == NULL;
}
//入队列(队尾入)
void QPush(Queue* pq, QDatatype x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
//将新的结点初始化一下
newnode->data = x;
newnode->next = NULL;
//队列为空
if (pq->head == NULL)
{
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = pq->tail->next;
}
pq->size++;
}
//出队列(队头出)
void QPop(Queue* pq)
{
assert(pq);
assert(!QEmepty(pq));
//当只有一个结点时
if (pq->head == pq->tail)
{
free(pq->head);
//将尾指针也置为NULL,否则就成为野指针了
pq->head = pq->tail = NULL;
}
else
{
QNode* NEXT = pq->head->next;
free(pq->head);
pq->head = NEXT;
}
pq->size--;
}
//读队头数据
QDatatype QFront(Queue* pq)
{
assert(pq);
assert(!QEmepty(pq));
return pq->head->data;
}
//读队尾数据
QDatatype QBack(Queue* pq)
{
assert(pq);
assert(!QEmepty(pq));
return pq->tail->data;
}
//队列有效数据个数
int QSize(Queue* pq)
{
assert(pq);
assert(!QEmepty(pq));
return pq->size;
}
test.c
#define _CRT_SECURE_NO_WARNINGS
#include"Queue.h"
void Queuetest()
{
Queue q;
QInit(&q);
QPush(&q, 1);
QPush(&q, 2);
QPush(&q, 3);
QPush(&q, 4);
printf("head: %d\n", QFront(&q));
printf("tail: %d\n", QBack(&q));
}
int main()
{
Queuetest();
return 0;
}
总结
这次我们学习的这两种线性表是两种操作受限的线性表,但是它们两个的实现方式是基于之前的顺序表和链表,因此,我们不仅要学习现在的,也要熟练掌握之前所学过的。有句老话怎么说来着:温故而知新,可以为师矣。