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

简介: 引言:栈和队列的讲解(一、)什么是栈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)以上就是队列的链表基本实现

(三、)总结:

就是要多写,多练,多画图,睡觉啦!
相关文章
|
6月前
|
前端开发 Java
java实现队列数据结构代码详解
本文详细解析了Java中队列数据结构的实现,包括队列的基本概念、应用场景及代码实现。队列是一种遵循“先进先出”原则的线性结构,支持在队尾插入和队头删除操作。文章介绍了顺序队列与链式队列,并重点分析了循环队列的实现方式以解决溢出问题。通过具体代码示例(如`enqueue`入队和`dequeue`出队),展示了队列的操作逻辑,帮助读者深入理解其工作机制。
168 1
|
4月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
61 0
栈区的非法访问导致的死循环(x64)
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
6月前
|
存储 前端开发 Java
线性数据结构详解
本文介绍了线性数据结构中的核心概念——节点,以及基于节点构建的链表、队列和栈等重要数据结构。节点是计算机科学中基本的构建单元,包含数据和指向其他节点的链接。通过添加约束或行为,可以构建出单向链表、双向链表、队列和栈等复杂结构。
158 1
|
8月前
|
算法 调度 C++
STL——栈和队列和优先队列
通过以上对栈、队列和优先队列的详细解释和示例,希望能帮助读者更好地理解和应用这些重要的数据结构。
171 11
|
8月前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
11月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
889 9
|
11月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
229 59
|
9月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
338 77
|
9月前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
247 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】