【数据结构】栈与队列:后进先出与先进先出到底是啥?

【数据结构】栈与队列:后进先出与先进先出到底是啥?栈和队列是两种常见且重要的线性数据结构 它们在解决各种实际问题和算法实现中发挥着关键作用

大家好,欢迎来到IT知识分享网。

在这里插入图片描述

  • 👑专栏内容:数据结构
  • ⛪个人主页:子夜的星的主页
  • 💕座右铭:日拱一卒,功不唐捐


一、前言

栈和队列是两种常见且重要的线性数据结构,它们在解决各种实际问题和算法实现中发挥着关键作用。本文将详细介绍栈和队列的概念、特点以及各自的应用场景。我们将从生活中的例子出发,形象地解释栈和队列的工作原理,也会使用不同的数据结构实现栈和队列,以及它们的优缺点。
【数据结构】栈与队列:后进先出与先进先出到底是啥?

二、栈的概念

1、定义

栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。

栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。

我们可以简单的把栈形象地看成一筒羽毛球,当往筒里放球时,球从筒底排到筒口,当从筒里拿球的时候,球从筒口往下依次被拿出来,直到筒底的最后一个球被拿出来。

【数据结构】栈与队列:后进先出与先进先出到底是啥?

2、操作

通常对栈执行以下两种操作:
向栈中添加元素,此过程被称为”进栈”(入栈或压栈);
从栈中提取出指定元素,此过程被称为”出栈”(或弹栈);
【数据结构】栈与队列:后进先出与先进先出到底是啥?

三、栈的实现

1、定义

typedef struct Stack { 
    int *data; //(1) int size; //(2) int top; //(2) }Stack; 

(1)用来存储数据的数组
(2)用来表示线性表的最大存储容量的值
(3)用来标识栈顶元素的栈顶下标

2、栈的初始化

Stack *initStack(int n) { 
    Stack *s = (Stack *)malloc(sizeof(Stack)); s->data = (int *)malloc (sizeof(int)*n); s->size = n; s->top = -1; return s; } 

为什么栈顶的下表要初始化为-1 呢?

【数据结构】栈与队列:后进先出与先进先出到底是啥?

3、栈的销毁

void clearStack(Stack *s) { 
    if(s ==NULL) return ; //(1) free(s->data); //(2) free(s); //(3) return ; } 

(1)如果栈为空,直接返回
(2)销毁存储的数据
(3)销毁栈

4、栈的判空

int empty (Stack *s) { 
    return s->top == -1; } 

5、查看栈顶元素

int top(Stack *s) { 
    if(empty(s)) return 0; return s->data[s->top]; } 

6、入栈与出栈

Ⅰ、入栈

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

【数据结构】栈与队列:后进先出与先进先出到底是啥?

int push(Stack *s,int val) { 
    if(s->top + 1==s->size )//(1) return 0; s->top = s->top+1; //(2) s->data[s->top] = val; //(3) return 1; } 

(1)如果栈已满

(2)栈顶指针向上移动

(3)将数据让如栈中

Ⅱ、出栈

出栈:栈的删除操作叫做出栈。出数据在栈顶
【数据结构】栈与队列:后进先出与先进先出到底是啥?

int pop(Stack *s) { 
    if(empty(s)) return 0; //(1) s->top = s->top -1; //(2) return 1; } 

(1)如果栈为空,直接结束

(2)出栈将栈顶 减一 即可

四、队列的概念

1、定义

只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 。

想象一下排队买票的场景。每个人都会排在队伍的尾部,而排在队伍最前面的人会最先买到票并离开队伍。这个过程类似于队列的入队和出队操作。最先进入队伍的人会最先离开队伍,而最后进入队伍的人则会最后离开队伍。

【数据结构】栈与队列:后进先出与先进先出到底是啥?

2、操作

通常对队列执行以下两种操作:
向队列中添加元素,此过程被称为”入队”;
从队列中提取出指定元素,此过程被称为”出队”;

【数据结构】栈与队列:后进先出与先进先出到底是啥?

【数据结构】栈与队列:后进先出与先进先出到底是啥?

五、队列的实现

1、顺序表实现

Ⅰ、队列的假溢出

队列是一种数据结构,可以将其看作是一个排队等待的队伍。在队列中,元素的添加(入队)和移除(出队)遵循先进先出(FIFO)的原则,即最早进入队列的元素最先被移除。队列有一个队头和队尾,新元素从队尾添加,而元素从队头移除。

队列溢出指的是当队列已满,也就是说它已经包含了最大允许的元素数量,但我们仍试图向其中添加新元素时发生的现象。此时,由于队列无法容纳更多的元素,队列溢出就发生了。而队列的假溢出是指队列中,队列尾部的空闲空间实际上可以容纳新的元素,但由于队列的逻辑结构限制(队列头和尾是相邻的),导致空间不能被利用,从而产生的一种溢出现象。实际上,这种溢出并非真正意义上的溢出,因为队列仍然具有可用空间。这就是为什么称为“假溢出”。

如下图所示,数组的前两个位置明明就是空的,但是却由于数组的最后一个位置有数据导致新数据无法正常加入。

【数据结构】栈与队列:后进先出与先进先出到底是啥?

【数据结构】栈与队列:后进先出与先进先出到底是啥?

为了解决假溢出的问题,可以使用循环队列的概念。循环队列是一种特殊类型的队列,其中队尾可以连接到队头,从而实现有效地使用队列空间。在循环队列中,当队尾指针到达数组的末尾时,它将循环回到数组的起始位置,继续使用队列中的空闲空间。这样就避免了假溢出的情况。

Ⅱ、队列的定义

为了用顺序表实现队列,我们还要定义一下顺序表。

typedef struct vector { 
    int *data; //数据区 int size; //数据大小 } vector; 

使用顺序表定义队列

typedef struct Queue { 
    vector *data; //数据存储区域 int size, head, tail, count; //总大小、队列头指针、队列尾指针、循环 } Queue; 

Ⅲ、初始化队列

//初始化有一个大小为n的顺序表 vector *initVector(int n) { 
    vector *v = (vector *)malloc(sizeof(vector)); v->size = n; //初始化一个顺序表的数据区域 v->data = (int *)malloc(sizeof(int) * n); return v; } 
Queue *initQueue(int n) { 
    //申请出一个队列空间 Queue *q = (Queue *)malloc(sizeof(Queue)); //初始化数据区域 q->data = initVector(n); //初始化顺序表大小 q->size = n; //初始化头尾指针,循环队列 q->head = q->tail = q->count = 0; return q; } 

Ⅳ、销毁队列

void clearVector(vector *v) { 
    if (v == NULL) return ; free(v->data); free(v); return ; } 
void clearQueue(Queue *q) { 
    if (q == NULL) return ; //清理数据区域 clearVector(q->data); //释放队列本身的空间 free(q); return ; } 

Ⅴ、队列判空

//队列判空 int empty(Queue *q) { 
    return q->count == 0; } 

Ⅵ、查看队首元素

//查看顺序表中的某一个值 int vectorSeek(vector *v, int pos) { 
    if (pos < 0 || pos >= v->size) return -1; return v->data[pos]; } 
int front(Queue *q) { 
    //直接返回指向的那个元素值 return vectorSeek(q->data, q->head); } 

Ⅶ、入队出队

入队操作的返回值代表入队成功。

什么时候入队不成功?当队列满了的时候就不成功。

【数据结构】栈与队列:后进先出与先进先出到底是啥?

//入队操作 int push(Queue *q, int val) { 
    if (q->count == q->size) return 0; //队列满了,入队失败 insertVector(q->data, q->tail, val);//调用顺序表的插入操作 q->tail += 1; //完成数据插入,调整tail指针位置 if (q->tail == q->size) q->tail = 0; //调整位置,形成循环利用,防止假溢出 q->count += 1; //count + 1 return 1; //入队成功 } 
//出队操作 int pop(Queue *q) { 
    if (empty(q)) return 0; //队列为空,直接返回0,出队失败 q->head += 1; //头指针向后移动一位  // 注意这里有个小 bug,由于是循环队列,别忘了判断 head 是否越界 if (q->head == q->size) q->head = 0; q->count -= 1; return 1; } 

2、单链表实现

在顺序表实现的队列中,队列的大小是预先固定的,当队列满时,即使实际上有空闲的空间,也无法将新元素插入队列。这种现象被称为“假溢出”。为了解决这个问题,我们通常需要实现一个循环队列,这样可以在队列满时使用空闲空间。

然而,在链表实现的队列中,我们并不需要预先分配固定大小的内存。当需要插入新元素时,我们可以动态地为新节点分配内存,并将其添加到链表中。同样,当删除元素时,我们可以释放相应节点的内存。因此,链表实现的队列不会遇到假溢出现象,因为只要系统内存允许,我们可以继续分配空间来扩展队列。

【数据结构】栈与队列:后进先出与先进先出到底是啥?

Ⅰ、队列的定义

//链表的节点结构 typedef struct Node { 
    int data; Node *next; } Node; //实现一个有头链表 typedef struct LinkList { 
    Node head, *tail; } LinkList; 
typedef struct Queue { 
    LinkList *l; //因为底层结构变成了链表,所以不在乎存储空间大小,不用考虑假溢出 int count; } Queue; 

Ⅱ、初始化队列

//初始化链表 LinkList *initLinkList() { 
    LinkList *l = (LinkList *)malloc(sizeof(LinkList)); l->head.next = NULL; // 头节点的下一个节点指针初始化为 NULL l->tail = &(l->head); // 尾节点指针指向头节点 return l; } 
Queue *initQueue() { 
    Queue *q = (Queue *)malloc(sizeof(Queue)); q->l = initLinkList(); //初始化队列中的链表 q->count = 0; return q; } 

Ⅲ、销毁队列

//销毁链表 void clearLinkList(LinkList *l) { 
    Node *p = l->head.next, *q; while (p) { 
    q = p->next; free(p); p = q; } free(l); return ; } 
//销毁队列 void clearQueue(Queue *q) { 
    if (q == NULL) return ; clearLinkList(q->l); //销毁队列中的链表 free(q); //销毁队列本身 return ; } 

Ⅳ、队列判空

int empty(Queue *q) { 
    return q->count == 0; } 

Ⅴ、查看队首元素

//链表判空 int emptyList(LinkList *l) { 
    return l->head.next == NULL; } //查看链表首元素 int frontList(LinkList *l) { 
    if (emptyList(l)) return 0; return l->head.next->data; } 
int front(Queue *q) { 
    if (empty(q)) return 0; return frontList(q->l); //返回链表存储的第一个数据 } 

Ⅵ、 入队出队

入队操作:

  • 使用 getNewNode 函数创建一个新节点,将入队元素存储在新节点的 data 成员中。
  • 更新链表尾节点指针 tailnext 成员,使其指向新节点。
  • 更新尾节点指针 tail,使其指向新节点。
  • 增加队列元素计数。

    【数据结构】栈与队列:后进先出与先进先出到底是啥?

//创建一个新的链表节点 Node *getNewNode(int val) { 
    Node *p = (Node *)malloc(sizeof(Node)); p->data = val; p->next = NULL; return p; } //在链表的最后位置加入一个新链表 int insertTail(LinkList *l, int val) { 
    Node *node = getNewNode(val); l->tail->next = node; l->tail = node; return 1; } //队列的入队 int push(Queue *q, int val) { 
    insertTail(q->l, val); //在链表的末尾增加一个值 q->count += 1; // 更新队列元素个数 return 1; } 

出队操作:

  • 检查链表是否为空。如果为空,说明队列中没有元素可以出队。
  • 保存链表头节点的下一个节点指针(即队头元素的节点)。
  • 更新链表头节点的 next 成员,使其指向队头元素的下一个节点。
  • 如果队头元素是尾节点,更新尾节点指针 tail,使其指向头节点。
  • 释放队头元素的节点内存。
  • 减少队列元素计数。

    【数据结构】栈与队列:后进先出与先进先出到底是啥?

    //链表判空 int emptyList(LinkList *l) { 
          return l->head.next == NULL; } //删除链表的头元素 int eraseHead(LinkList *l) { 
          if (emptyList(l)) return 0; // 如果链表为空,返回 0,表示没有元素可删除 Node *p = l->head.next; // 保存头部节点(即队头元素所在的节点) l->head.next = l->head.next->next; // 更新头节点的下一个节点指针,使其指向队头元素的下一个节点 if (p == l->tail) l->tail = &(l->head); // 如果删除的是尾节点,更新尾节点指针为指向头节点 free(p); // 释放头部节点(队头元素所在的节点)的内存 return 1; } //出队操作 int pop(Queue *q) { 
          eraseHead(q->l); //删除链表中的第一个元素 q->count -= 1; return 1; } 

    【数据结构】栈与队列:后进先出与先进先出到底是啥?

    3、两者区别

    Ⅰ、顺序表实现的队列

    顺序表(基于数组)实现队列时,通常采用循环队列的策略。队列的头部和尾部都会随着入队和出队操作移动,当达到数组边界时会回到数组起始位置。为了避免队列满和队列空的判断条件冲突,通常会预留一个空间不用于存储元素。

​ 优点:

  • 访问速度快:数组具有较好的随机访问性能,可以通过下标直接访问元素。
  • 空间利用率高:顺序表的存储空间是连续的,没有额外的指针开销。

    缺点:

  • 产生假溢出:当队列未满但数组已经无法容纳新元素时,需要进行循环队列调整,否则会产生假溢出。
  • 大小固定:顺序表的大小在创建时就已经确定,不能动态调整。如果空间过大,会造成空间浪费;如果空间过小,可能导致溢出。

    Ⅱ、单链表实现的队列

    单链表实现队列: 单链表实现队列时,头部节点用于出队,尾部节点用于入队。每次入队时,在尾部添加新节点;每次出队时,删除头部节点并释放其内存。

优点:

  • 动态分配空间:单链表实现的队列可以在运行时动态分配和释放内存,避免了假溢出现象。
  • 实现简单:单链表实现队列时,只需在头部和尾部进行操作,不需要循环队列策略,实现相对简单。

缺点:

  • 空间开销较大:单链表需要额外的指针存储节点之间的关系,导致额外的空间开销。
  • 访问速度相对较慢:链表无法像数组那样直接通过下标访问元素,需要遍历链表节点。

【数据结构】栈与队列:后进先出与先进先出到底是啥?

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/147536.html

(0)
上一篇 2025-04-06 19:45
下一篇 2025-04-06 20:00

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信