文章目录
- 前言
- 1. 栈
- 1.1 栈的概念及结构
- 1.2 栈的实现
- 1.2.1 "栈"实现的选择
- 1.3 栈的代码实现
- 1.3.1 栈的结构体定义(用的是顺序表)
- 1.3.2 栈的头文件设置
- 1.3.3 栈的各功能的实现
- 2. 队列
- 2.1 队列的概念及结构
- 2.2 "队列"实现的选择
- 2.3 队列的代码实现
- 2.3.1 队列的结构体定义
- 2.3.2 队列的头文件
- 2.3.3 队列各功能的实现
前言
在学习栈和队列中,你是否会被人提问过什么是栈和队列?是否知道栈和队列的特征以及栈和队列的代码实现?
通过本文的讲解,以上的问题都会一扫而空的!!!💖
话不多说,让我们开启轻松而愉悦的探索之旅吧。
1. 栈
1.1 栈的概念及结构
栈:一种特殊的线性表,其只允许再固定的一端进行插入和删除数据的操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。
栈中的元素遵循着**后进先出(LIFO:Last In First Out)**的原则。
压栈:栈的插入操作叫做压栈(进栈/入栈),入数据在栈顶。
出栈:栈的删除操作叫做出栈,出数据也在栈顶。
1.2 栈的实现
我们根据栈的特点,以及我们所学过的数据结构(顺序表、链表、双向链表)中选择,我们会选择哪种数据结构呢?
我会更倾向于顺序表。但其实,栈的实现一般可以用数组或者链表。
1.2.1 "栈"实现的选择
之所以选择顺序表,主要有以下的原因:
- 容易找到栈顶的位置。
为什么会这样说呢?你可以想一下**,顺序表的底层是数组,数组如果我要查找到最后一个元素是比较容易的。而相较于链表来说,就要遍历整个链表,从而遭到链表的为节点,在进行数据的操作,那此时的算法时间复杂度就为O(n)了**。那可能有的人思维比较活跃,他说那我直接把链表的头部当作栈顶的位置,那我就不需要找链表的尾节点,那时间复杂度就变为O(1)了。没错,这个就是用链表实现栈的一种优化方式。不过单凭这一点,还不足以让我放弃使用顺序表这个念头!- 缓存命中率高。
我们都知道数组在内存中是连续存储的。那根据空间的局部性原理,缓存的命中率就会高,这也就意味着CPU读取数据的速度就会加快,从而给程序的运行提高了速度。这个特点是链表无法比拟的。
1.3 栈的代码实现
1.3.1 栈的结构体定义(用的是顺序表)
//使用动态顺序表实现栈
typedef int STDataType;
typedef struct Stack
{
STDataType* arr;
int top; //可以看作栈顶指针
int capacity; //记录当前栈可用的空间
}Stack;
1.3.2 栈的头文件设置
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//栈的接口实现
//栈的初始化
void STInit(Stack* ps);
//添加数据 - 栈
void STPush(Stack* ps,STDataType x);
//删除数据 - 栈
void STPop(Stack* ps);
//栈的销毁
void STDestory(Stack* ps);
//查看栈顶元素
STDataType STTop(Stack* ps);
//判断栈是否未空
int STEmpty(Stack* ps);
//查看栈的大小
int STSize(Stack* ps);
1.3.3 栈的各功能的实现
#define _CRT_SECURE_NO_WARNINGS
#include"Stack.h"
//栈的初始化
void STInit(Stack* ps)
{
ps->arr = (STDataType*)malloc(sizeof(STDataType)*4); //一开始先给4个大小的空间
ps->top = 0;
ps->capacity = 4;
}
//添加数据 - 栈
void STPush(Stack* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
//说明得扩容了
STDataType* tmp = (STDataType*)realloc(ps->arr,sizeof(STDataType)* 2* ps->capacity);
if (tmp == NULL)
{
perror("realloc fail");
exit(1);
}
ps->arr = tmp;
ps->capacity*=2;
}
ps->arr[ps->top] = x;
ps->top++;
}
//删除数据 - 栈
void STPop(Stack* ps)
{
assert(ps && ps->arr);
assert(!STEmpty(ps));
ps->top--;
}
//栈的销毁
void STDestory(Stack* ps)
{
if (ps->arr != NULL)
{
free(ps->arr);
}
ps->arr = NULL;
ps->capacity = 0;
ps->top = 0;
}
//查看栈顶元素
STDataType STTop(Stack* ps)
{
return ps->arr[ps->top-1];
}
//判断栈是否未空
int STEmpty(Stack* ps)
{
if (ps->top == 0)
{
return 1;
}
return 0;
}
//查看栈的大小
int STSize(Stack* ps)
{
return ps->top + 1;
}
好了,到这里,关于栈的知识已经全部讲解完毕了。接下来,我们再来看看另一个数据结构——“队列”!
2. 队列
2.1 队列的概念及结构
队列:只允许在一端进行插入数据的操作,在另一端进行数据删除的操作的特殊线性表。队列具有先进先出的特点FIFO(First In First Out)。
队列的一些基本操作:
- 入队列:进行插入数据的操作。
- 进行插入操作的一端称为队尾;
- 出队列:进行伤处数据的操作。
- 进行删除数据的一端称为队头;
为了让大家对队列有个形象的认知,我会给大家展示一副关于的队列的图:
2.2 "队列"实现的选择
经过了栈的洗礼,大家或多或少都应该猜出来了,队列是使用链表来实现的。
如果你没有猜到,没关系,听我给你解释一下是为什么。
我们可以知道的一个信息就是:队列只能在一端进行插入操作,在另一端进行删除操作。如果我们使用顺序表来实现,在插入操作环节比较容易实现,但是在删除操作用数组实现就比较繁琐了。但是,如果我们使用链表来实现的话,只要我们合理的控制头节点和尾节点,就能够很方便的实现插入和删除操作。这个就是我们为什么会推荐选择用链表来实现队列。
以下就是用链表来实现队列的示例图:
那接下来,我们就用代码实现队列。
2.3 队列的代码实现
这个实现过程中,可能有一个点让大家比较难以理解,其它点都是比较简单的。
2.3.1 队列的结构体定义
typedef int QDataType;
typedef struct QNode
{
QDataType data;
struct QNode* next;
}QNode;
typedef struct Queue
{
QNode* head;
QNode* tail;
int size;
}Queue;
这里就是那个难点所在!为什么这里会是要定义两个结构体,你在"栈"实现的时候,才定义一个结构体啊?如果有这个问题的读者,那么恭喜你们跟上了本文的思路了。
我在之前说过,实现队列的插入或者删除操作时,只要我们能够合理的控制头节点和尾节点的指针,就足以能够实现队列。那此时,我们就要想一下,能不能有个更简单的方式,一起控制着头指针和尾指针。方法就是:将头节点的指针和尾节点的指针用一个结构体给打包起来,只要我们使用头节点和尾节点的指针时,就不要额外再定义其它变量了。
如果你还不理解的话,我再跟你讲一下,如果不使用这个方式定义结构体的危害。
如果你不这样做的话,你再给函数传递参数时,你就得往函数里面多传递两个参数或者是每当进行删除或插入数据时,我们都得先定义两个变量分别代表头节点和尾节点,十分的繁琐!
相信经过以上的讲解,你已经理解了为什么要使用两个结构体。
2.3.2 队列的头文件
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
//队列的初始化
void QInit(Queue* p);
//销毁队列
void QDestory(Queue* p);
//添加数据
void QPush(Queue* p, QDataType x);
//删除数据
void QPop(Queue* p);
//查看队头元素
QDataType QFront(Queue* p);
//查看队尾元素
QDataType QBack(Queue* p);
//队列的大小
int QSize(Queue* p);
//判断列表是否为空
bool QEmpty(Queue* p);
2.3.3 队列各功能的实现
#define _CRT_SECURE_NO_WARNINGS
#include"queue.h"
//队列的初始化
void QInit(Queue* p)
{
assert(p);
p->head = p->tail = NULL;
p->size = 0;
}
//销毁队列
void QDestory(Queue* p)
{
assert(p);
QNode* pcur = p->head->next;
while (pcur != NULL)
{
QNode* next = pcur->next;
free(pcur);
pcur = next;
}
pcur = NULL;
}
//添加数据
void QPush(Queue* p, QDataType x)
{
assert(p);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
//插入数据,只能在队尾插入
if (p->head == NULL)
{
assert(p->tail == NULL);
p->head = p->tail = newnode;
}
else
{
p->tail->next = newnode;
p->tail = p->tail->next;
}
p->size++;
}
//删除数据
void QPop(Queue* p)
{
assert(p && p->head);
//删除数据只能够在队头删除
QNode* pcur = p->head;
//但只有一个节点的情况
if (p->head == p->tail)
{
free(p->head);
p->head = p->tail = NULL;
}
else
{
p->head = pcur->next;
free(pcur);
pcur = NULL;
}
p->size--;
}
//查看队头元素
QDataType QFront(Queue* p)
{
assert(p && p->head);
return p->head->data;
}
//查看队尾元素
QDataType QBack(Queue* p)
{
assert(p && p->tail);
return p->tail->data;
}
//队列的大小
int QSize(Queue* p)
{
assert(p);
return p->size;
}
//判断列表是否为空
bool QEmpty(Queue* p)
{
assert(p);
if (QSize(p) == 0)
{
return true;
}
return false;
}
好了,到这里,栈和队列的知识就已经全部讲解完毕了。之后我会出关于这个知识点一系列的编程题详解,希望大家能够持续的关注!
如果觉得本文将的还不错的话,麻烦给偶点个赞吧!!!