数据结构之线性表中的栈和队列【详解】

简介: 引言:栈和队列的讲解(一、)什么是栈1.栈的概念、结构和图解:(1.)顺序表和链表的对比(严格来说这两个结构是相辅相成的)(2.)栈的概念和结构(3.)栈的图解2.使用数组的形式实现栈:(二、)什么是队列1.队列的概念、结构和图解:(1.)队列的概念和结构(2.)队列的图解2.使用链表的形式实现队列(三、)总结:

引言:

北京时间2022/11/29 凌晨1点06分,今天我们来讲一下什么是栈和队列,写完这个博客,明天复习自我实现一下栈和队列,我们看一下能不能找到有关二叉树的教学视屏,假如找到了,我们就朝二叉树进发,找不到,我们就进行链表这部分的题目练习


栈和队列的讲解

(一、)什么是栈

1.栈的概念、结构和图解:

首先在这边我们学习什么是栈和队列的前提之下,我们先把双循环链表给收尾一下


(1.)顺序表和链表的对比(严格来说这两个结构是相辅相成的)

(1.1)首先是顺序表的优点:1.支持随机访问,需要随机访问结构的算法可以很好的适用 2.CPU告诉缓存的命中率更高

(1.2.)顺序表的缺点: 1.头部中部插入删除时间效率低 时间复杂度O(N) 2.连续的物理空间,空间不够了以后需要增容(增容有一定的消耗)3.按倍数增容,用不完就有一定的空间的浪费

(1.3.)(双向带头循环链表)链表的优点:1.任意位置插入删除效率高 时间复杂度 O(1) 2.按需申请释放空间

(1.4.)链表的缺点:1.不支持随机访问(用下标访问),意为着:一些排序在这种结构上不适用 2.链表存储一个值,同时也要储存链接指针,也有一点的消耗 3.CPU告诉缓存的命中率更低


(2.)栈的概念和结构

(2.1).栈的概念和结构:

栈:一种特殊的线性表,其只允许在固定的一段进行插入和删除元素的操作。进行数据插入和删除的操作的一端称为栈顶,另一端称为栈底,栈中的数据元素遵守后进先出(就像是电梯)的原则

压栈:栈的插入操作叫做进栈/压栈/入栈。入数据在栈顶

出栈:栈的删除操作叫做出栈。出数据也在栈顶(因为要遵守原则:先进后出,后进先出)

这边讲完了我么就来讲一下为什么要用数组来实现我的栈


(2.2)我们可以通过数组的形式来把栈给实现(也可以使用链表,但是链表没有数组好,所以我们就用数组实现就行)


(2.3)使用链表实现栈的方式:

如果用尾做栈顶,尾插尾删,要设计双向链表,否则删除数据效率低

如果用头做栈顶,头插头删,就可以设计成单链表

所以这些都有一些的不好,所以我们使用数组来实现我的栈就是最好的


(3.)栈的图解


113.jpeg

2.使用数组的形式实现栈:

(4.1)具体的功能如下:(包括与栈相关的结构体形式的创建)

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/8b77a23d14294724b263f8b3d5865b26.png#pic_center)
typedef int STDataType;
typedef struct Stack
{
  STDataType* a;
  int top;//栈顶的意思(位置)
  int capacity;
}ST;
void StackInist(ST* ps);
void StackDestroy(ST* ps);
void StackPush(ST* ps,STDataType x);//此时的插入是在栈顶位置(栈的特点:先出后进,先进后出一定要理解透彻了)
void StackPop(ST* ps);
STDataType StackTop(ST* ps);//这个就是表明此时我要取我的栈顶的数据,在top位置(栈顶位置)获取我的数据
int StackSize(ST* ps);//这个是意思就是表示我的栈中有几个数据
bool StackEmpty(ST* ps);//此时这个函数的作用就是判断我的栈是否为空(bool这个返回值的意思就是返回真和假的意思,用int也是一样)

(4.2)具体各种接口函数的实现

(4.2.1)初始化



0.png

(4.2.2)销毁

1.png


(4.2.3)放数据

2.png


(4.2.4)删除

3.png

(4.2.5)找top位置‘4.png

(4.2.6)计算栈的大小

5.png



(4.2.7)判断栈是否为空

6.png

(4.2.8)遍历栈

7.png


(4.3)以上就是栈的数组实现

(二、)什么是队列

1.队列的概念、结构和图解:

(1.)队列的概念和结构


(1.1).队列的概念及其结构:

队列:只允许在一端进行插入数据的操作,在另一端进行删除数据的操作,队列具有先进先出的原则

入队列:进行插入操作的一端称为队尾

出队列:进行删除操作的一端称为队头

就是区别于栈就对了(一个是同一端进同一端出,一个是一端进,另一端出)

此时可以明显的看出用数组和单链表都不适合实现我们的队列,所以这边最好就是使用双向循环链表是最合适的

(1.2)假如使用链表的结构:

在进行出数据(在链表的头上进行)的时候就是把一个数据出完之后(然后把它删除掉,然后把phead头指针指向下一个,然后再出另一个数据)

在入数据(在链表的尾部进行)的时候就是找尾,然后一直更新这个尾


(2.)队列的图解

8.jpeg


2.使用链表的形式实现队列

(3.1)具体功能如下:

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//定义各种结构(不管是顺序表还是链表都是在模仿老师的定义结构的方法),以后在定义结构的时候一定要学会自己去定义结构
//此时这个结构体的定义就很像是单量表实现的定义
typedef int QDataType;
typedef struct QueueNode //这个就是队列中的单链表的结构的定义(用单链表实现队列就一定要有单链表结构的定义)
{
  struct QueueNode* next;
  QDataType data;
}QueueNode;
typedef struct Queue //此时的这个结构体应该才是我(队列)的主结构体,上面那个结构体应该只是一个单量表的结构体(因为此时我是想要用单链表来实现我的队列),上面那个结构体也就是我的单链表中的一个一个的结点而已(就是用上面的这个结构体来设计我的队列中的每一个结点而已,这样就有点像是我的单链表)
{
  QueueNode* head;
  QueueNode* tail;//虽然这个结构体是队列的关键,但是这个结构体中的成员,还是要根据我的需求来定义的
}Queue;
//typedef struct Queue //此时的这个结构体应该才是我(队列)的主结构体,上面那个结构体应该只是一个单量表的结构体(因为此时我是想要用单链表来实现我的队列),上面那个结构体也就是我的单链表中的一个一个的结点而已(就是用上面的这个结构体来设计我的队列中的每一个结点而已,这样就有点像是我的单链表)
//{
//  QueueNode* head;
//  QueueNode* tail;//虽然这个结构体是队列的关键,但是这个结构体中的成员,还是要根据我的需求来定义的
//}Queue;
//并且此时的这个双指针可以不使用结构体定义(但是如果不使用结构体的话,此时的传参就要进行一定的改变了)
//例如:void QueueInit(Queue* pq); 不使用结构体此时就要写成 void QueueInit(QueueNode** pphead,QueueNode** pptail);
//void QueueInit(QueueNode** pphead, QueueNode** pptail);不仅要传二级指针(因为我会改变函数外部的值),而且要进行两个指针参数的传递,可谓是非常的复杂
//此时以上的这些代码的意思(就是创建了两个结构体(也就是两种类型),有了这两种类型,我就可以实现我的队列了),所以这个也就是我的队列的基本的结构体定义
//为什么一定是以下的接口:这个就是性质决定的
void QueueInit(Queue* pq);
void QueueDestory(Queue* pq);
void QueuePush(Queue* pq, QDataType x);
void QueuePop(Queue* pq);
QDataType QueueFront(Queue* pq);//这个就是表示取队头数据的意思
QDataType QueueBack(Queue* pq); //这个就是表示取队尾数据的意思
size_t QueueSize(Queue* pq);//这个就是计算队列中的数据个数
bool QueueEmpty(Queue* pq);//这个就是用来判断这个队列是否为空

(3.2)具体函数接口的是实现】

#include"Stack.h"
//初始化
void QueueInit(Queue* pq)
{
  //此时为什么传参接收参数的时候不需要二级指针呢?
  //原因就是:我使用了两个结构体类型
  assert(pq);
  pq->head = NULL;
  pq->tail = NULL;
}
//销毁(此时销毁的就是一个一个动态内存开辟出来的结点,这个结点就是从另一个结构体的定义之中来的)
void QueueDestory(Queue* pq)
{
  assert(pq);
  QueueNode* cur = pq->head;
  while (cur != NULL)
  {
    QueueNode* next = cur->next;//保存下一个(因为我是一个链表)
    free(cur);
    cur = next;
  }
  pq->head = pq->tail = NULL;
}
//插入
void QueuePush(Queue* pq, QDataType x)
{
  //因为此时已经把结构体的地址传过来了,所以此时就可以对结构体进行改变了
  assert(pq);
  QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
  newnode->data = x;
  newnode->next = NULL;//这个就是结点是初始化
  //此时先进行队列有无数据的判断
  if (pq->head == NULL)//这个表示无
  {
    //这个就是此时的把我的结构体进行改变,但是要注意进行结构体的改变一定要进行结构体地址的传递,然后要用一级指针接收
    pq->head = pq->tail = newnode;//这个就是表示此时的这个队列一个值都还没有,所以此时就可以把这个newnode给我的head和tail
  }
  else//这个就是有(然后有数据,此时因为是一个队列,所以此时我们就需要在这个队列的后面插入一个新的结点,因为队列的特点(一头进一头出)),然后此时就需要在tail这个表示尾的指针后面进行一个尾插,这样就可以完成队列的插入功能
  {
    pq->tail->next = newnode;
    pq->tail = newnode;//上面那个是主要的完成插入的步骤,下面这个没什么主要的功能(其实就只是想让我的tail继续保持在最后一个结点的位置而已)
  }
}
//删除
void QueuePop(Queue* pq)
{
  //因为队列的特性(队尾入,队头出)
  //所以删除数据是已经规定死了的,只能在队头删
  assert(pq);
  //还是那两种写法:1.温柔的写法  2.暴力的写法
  //if (pq->head == NULL)
  //{
  //  return;
  //}
  //assert(pq->head != NULL);
  //并且下面这个函数 QueueEmpty(pq) 为真就是表示此时的pq->head为空,为假就表示此时的pq->head不为空
  assert(!QueueEmpty(pq));//这个在报错(就是说明此时的这个QueueEmpty(pq)函数为真,然后(!QueueEmpty(pq))加了一个感叹号就表示此时整个为假),所以此时这个断言的意思就是判断这个pq->head不为空,为空就报错
  Queue* next = pq->head->next;
  free(pq->head);
  pq->head = NULL;
  pq->head = next;//完成这些基本的操作,我们一定还要注意几个注意点
  //因为此时可能会把整个队列都给删空,所以此时当删到只有一个数据的时候,此时就要进行tail的置空,不然就有野指针问题
  if (pq->head == NULL)//此时这个为空,就是表示已经删完了(但是此时只是把head给处理完了,这边还剩了一个tail指针(此时如果把最后一个数据也删掉,那么此时的tail就会变成一个野指针),所以我们在删除最后一个数据的时候,一定要注意tail的置空,不然就是野指针)
  {
    pq->tail = NULL;
  }
}
//取队头的数据
QDataType QueueFront(Queue* pq)
{
  assert(pq);
  assert(!QueueEmpty(pq));//此时就是表示我的头已经为空了,所以我已经不能再去取头数据了
  return pq->head->data;//这个就是表示队列头的数据
}
//取队尾的数据
QDataType QueueBack(Queue* pq)
{
  assert(pq);
  assert(!QueueEmpty(pq));//判断队列不为空
  return pq->tail->data;//此时为了寻找我队列的尾(也就是入口),并且此时的tail就是指向我队列的尾数据,所以想要找尾中的数据,只需要把tail->data给返回去就行了
}
//计算队列中的数据个数
size_t QueueSize(Queue* pq)
{
  assert(pq);
  int n = 0;
  QueueNode* cur = pq->head;
  while (cur != NULL)//想要计算数据个数,自然要遍历我的队列
  {
    n++;
    cur = cur->next;//有了这步上面那步就不需要写成cur->next != NULL;了
  }
  return n;
}
//判断队列是否为空(在删除的时候会用到,因为删除的时候队列是不可以为空的,所以要进行一定的判断)
bool QueueEmpty(Queue* pq)
{
  assert(pq);
  return pq->head == NULL;
}

(3.3)以上就是队列的链表基本实现

(三、)总结:

就是要多写,多练,多画图,睡觉啦!
相关文章
|
3天前
栈的几个经典应用,真的绝了
文章总结了栈的几个经典应用场景,包括使用两个栈来实现队列的功能以及利用栈进行对称匹配,并通过LeetCode上的题目示例展示了栈在实际问题中的应用。
栈的几个经典应用,真的绝了
|
4天前
|
存储 网络协议 Linux
用户态协议栈06-TCP三次握手
用户态协议栈06-TCP三次握手
|
3天前
|
算法
【数据结构与算法】优先级队列
【数据结构与算法】优先级队列
6 0
|
3天前
|
存储 算法
【数据结构与算法】队列(顺序存储)
【数据结构与算法】队列(顺序存储)
5 0
|
4天前
|
存储
全局变量和局部变量在堆和栈的区别
全局变量和局部变量在堆和栈的区别
11 0
|
5天前
|
存储 人工智能 运维
高质量存储力发展问题之浪潮信息发布的大模型智算软件栈的定义如何解决
高质量存储力发展问题之浪潮信息发布的大模型智算软件栈的定义如何解决
8 0
【数据结构】栈和队列
【数据结构】栈和队列
|
5天前
|
算法 C语言 C++
【practise】栈的压入和弹出序列
【practise】栈的压入和弹出序列
|
6天前
|
C语言
用栈实现将一个十进制数值转换成八进制数值。即用该十进制数值除以8,并保留其余数;重复此操作,直到该十进制数值为0为止。最后将所有的余数反向输出就是所对应的八进制数值
这篇文章展示了如何使用栈(包括顺序栈和链栈)实现将十进制数值转换成八进制数值的方法,通过C语言编程演示了两种栈的实现方式和使用场景。
用栈实现将一个十进制数值转换成八进制数值。即用该十进制数值除以8,并保留其余数;重复此操作,直到该十进制数值为0为止。最后将所有的余数反向输出就是所对应的八进制数值
|
8天前
|
存储
数据结构——栈(Stack)
栈(Stack)是一种常见且重要的数据结构,它遵循后进先出(Last-In-First-Out, LIFO)的原则,即最后加入的元素会是第一个被移除的。
23 4