栈与队列的进出顺序、存储结构选择及循环队列判满


栈与队列的进出顺序、存储结构选择及循环队列判满

  • 栈与队列的进出顺序、存储结构选择及循环队列判满
  • [1. 阅读前问题卡:栈和队列](#1. 阅读前问题卡:栈和队列)
    • [1.1 阅读前先看这几个问题](#1.1 阅读前先看这几个问题)
    • [1.2 读完后完成这 3 道高频问题](#1.2 读完后完成这 3 道高频问题)
    • [1.3 自检清单](#1.3 自检清单)
  • [2. 前言](#2. 前言)
    • [2.1 进出位置与数据顺序](#2.1 进出位置与数据顺序)
    • [2.2 存储结构与操作接口](#2.2 存储结构与操作接口)
    • [2.3 首尾相连后的判空与判满](#2.3 首尾相连后的判空与判满)
  • [3. 栈和队列](#3. 栈和队列)
    • [3.1 栈](#3.1 栈)
      • [3.1.1 概念与结构](#3.1.1 概念与结构)
      • [3.1.2 栈的实现](#3.1.2 栈的实现)
    • [3.2 队列](#3.2 队列)
      • [3.2.1 概念与结构](#3.2.1 概念与结构)
      • [3.2.2 队列的实现](#3.2.2 队列的实现)
    • [3.3 栈和队列算法题](#3.3 栈和队列算法题)
      • [3.3.1 有效的括号](#3.3.1 有效的括号)
      • [3.3.2 用队列实现栈](#3.3.2 用队列实现栈)
      • [3.3.3 用栈实现队列](#3.3.3 用栈实现队列)
      • [3.3.4 设计循环队列](#3.3.4 设计循环队列)

1. 阅读前问题卡:栈和队列


1.1 阅读前先看这几个问题

  1. 栈允许在哪一端插入和删除元素?这怎样决定元素的进出顺序?
  2. 压栈、出栈和取栈顶分别指什么?文中的栈接口还提供哪些操作?
  3. 为什么原文认为数组较适合实现栈?
  4. 队列的入队和出队分别发生在哪一端?它与栈的顺序有何不同?
  5. 为什么原文更倾向于用链表实现队列?队列声明中如何记录两端和有效元素个数?
  6. 循环队列为什么让 Q.rear 所指位置不存储数据?原文用什么关系描述队空和队满?

1.2 读完后完成这 3 道高频问题


1.2.1 高频问题 1:请比较栈与队列的插入、删除位置和元素进出顺序。


1.2.2 高频问题 2:原文为什么分别倾向于用数组实现栈、用链表实现队列?


1.2.3 高频问题 3:循环队列为什么要空出 rear 所指位置?原文如何区分队空与队满?


1.3 自检清单

  • 能说清栈顶、栈底以及后进先出的关系。
  • 能指出压栈、出栈和取栈顶分别对应什么操作。
  • 能说清队头、队尾以及先进先出的关系。
  • 能解释原文对栈和队列底层结构的不同倾向。
  • 能从两个头文件中辨认结构字段和基本操作声明。
  • 能说出原文给出的循环队列队空、队满判别关系。

2. 前言


2.1 进出位置与数据顺序

一摞盘子只从最上面添放、取走,最后放上去的盘子会最先被取走。排队时,新来的人站到队尾,离开队伍的人从队头走,先来的人先离开。对应到文中,盘子的顶端是栈唯一进行插入和删除的一端;队伍的尾部和头部分别对应队列的入队端和出队端。由进出位置,便能理解栈的后进先出和队列的先进先出。


2.2 存储结构与操作接口

若总在一排位置的末端放入物品,从末端取放很直接;若要从这一排位置的开头不断取走物品,维持连续排列就更费事。对应到文中的结构选择:栈在栈顶进出,原文认为数组尾部插入代价较小,因此倾向数组;队列从队头出队,原文认为数组头部出数据效率较低,因此倾向链表。两个头文件随后列出存储字段和操作接口,但没有给出函数内部实现。


2.3 首尾相连后的判空与判满

把一圈位置依次用于放物品时,两个位置标记碰到一起,单看标记重合就难分辨是已取空还是已放满。预留一个位置,可以让两种状态呈现不同的标记关系。对应到原文的循环队列,Q.rear 所指位置不存储数据;文中用 Q.rear = Q.front 讨论队空与队满的区分,并将 rear + 1 = front 所示状态视为队满。


3. 栈和队列


3.1 栈


3.1.1 概念与结构

栈是一种特殊的线性表,只允许在固定的一端插入和删除元素。进行插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出 LIFO(Last In First Out)的原则。

  • 压栈:栈的插入操作,也叫进栈、入栈;数据从栈顶进入。
  • 出栈:栈的删除操作;数据从栈顶离开。

栈底层结构选型

栈一般可以使用数组或者链表实现。相对而言,数组结构更优一些,因为数组在尾部插入数据的代价比较小。


3.1.2 栈的实现

stack.h:

c 复制代码
typedef int STDataType;

typedef struct Stack
{
    STDataType* a;
    int top;
    int capacity;
} ST;

// 初始化栈
void STInit(ST* ps);
// 销毁栈
void STDestroy(ST* ps);
// 入栈
void STPush(ST* ps, STDataType x);
// 出栈
void STPop(ST* ps);
// 取栈顶元素
STDataType STTop(ST* ps);
// 获取栈中有效元素个数
int STSize(ST* ps);
// 栈是否为空
bool STEmpty(ST* ps);

3.2 队列


3.2.1 概念与结构

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

  • 入队列:进行插入操作的一端称为队尾。
  • 出队列:进行删除操作的一端称为队头。

队列底层结构选型

队列也可以使用数组或链表实现。使用链表结构更优一些,因为如果使用数组结构,出队列要在数组头部取出数据,效率会比较低。


3.2.2 队列的实现

Queue.h:

c 复制代码
typedef int QDataType;

// 队列结点结构
typedef struct QueueNode
{
    int val;
    struct QueueNode* next;
} QNode;

// 队列结构
typedef struct Queue
{
    QNode* phead;
    QNode* ptail;
    int size;
} Queue;

// 初始化队列
void QueueInit(Queue* pq);
// 销毁队列
void QueueDestroy(Queue* pq);
// 入队列,队尾
void QueuePush(Queue *pq, QDataType x);
// 出队列,队头
void QueuePop(Queue* pq);
// 取队头数据
QDataType QueueFront(Queue* pq);
// 取队尾数据
QDataType QueueBack(Queue* pq);
// 队列判空
bool QueueEmpty(Queue* pq);
// 队列有效元素个数
int QueueSize(Queue* pq);

3.3 栈和队列算法题


3.3.1 有效的括号

https://leetcode.cn/problems/valid-parentheses/description/


3.3.2 用队列实现栈

https://leetcode.cn/problems/implement-stack-using-queues/description/


3.3.3 用栈实现队列

https://leetcode.cn/problems/implement-queue-using-stacks/description/


3.3.4 设计循环队列

https://leetcode.cn/problems/design-circular-queue/description/

循环队列的概念与结构

循环队列是一种特殊的队列,首尾相连成环。循环队列可以使用数组实现,也可以使用循环链表实现。

思考:队列满的情况下,为什么 Q.rear 不存储数据?

为了用 Q.rear = Q.front 区分队空与队满,文中将左图所示情况视为队满,此时 rear + 1 = front。

4.回答问题卡的问题


问题 1:栈允许在哪一端插入和删除元素?这怎样决定元素的进出顺序?

答:

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


问题 2:压栈、出栈和取栈顶分别指什么?文中的栈接口还提供哪些操作?

答:

压栈是栈的插入操作,出栈是栈的删除操作,取栈顶是读取栈顶元素

初始化栈STInit,销毁栈STDestroy,获取有效元素个数STSize和判断栈是否为空STEmpty。压栈、出栈和取栈顶分别对应 STPush、STPop、STTop。


问题 3:为什么原文认为数组较适合实现栈?

答:

因为数组在尾部插入数据的代价比较小

时间代价(常数项极低)

栈的入栈操作发生在数组尾部。如果数组容量足够,只需要执行 array[size] = value; size++;,也就是内存赋值和索引自增,对整数元素来说是 O(1)。如果触发扩容并重新分配存储空间,就可能要搬移已有元素;能否达到均摊 O(1) 要看扩容方案。std::vector 的尾部追加有这一保证,原文只给出 C 栈的接口,没有说明它采用什么扩容策略。

如果单链表没有维护尾指针,每次在尾部插入都需要从头遍历到尾节点,时间复杂度是 O(n);维护了尾指针后,连接新节点可以是 O(1)。链表栈也可以在表头插入和删除,避开尾部遍历。逐个申请节点的实现还会有内存分配开销,但是否每次重新申请、是否涉及系统调用、成本有多大,要看分配器和节点复用方式。

2. 空间代价(内存开销)

数组的内存是连续的。文中的栈结构除了存储数据,还维护数组指针 a、栈顶位置相关字段 top 和容量 capacity。

链表除了存储数据,每个节点还需要存储指针 next,这会带来额外开销。在 Windows x64 ABI 下,一个指针占 8 字节;具体大小要以目标平台为准。频繁申请和释放节点可能产生碎片,但不是必然产生大量碎片,分配器也会复用和管理空间。

3. 底层缓存(CPU Cache Locality)

数组在内存中是连续存储的,通常有利于利用相邻数据的缓存局部性。链表节点通过指针连接,不保证连续分布;如果节点分散,沿指针访问可能带来较差的局部性。实际速度仍取决于访问方式、分配布局和硬件,不能据此断言所有数组栈都比链表栈快。


问题 4:队列的入队和出队分别发生在哪一端?它与栈的顺序有何不同?

答:

队尾,队头

队列只允许在一端插入数据、在另一端删除数据,具有先进先出 FIFO(First In First Out)的特点。

队列先进入的先离开,栈最后进入的先离开。


问题 5:为什么原文更倾向于用链表实现队列?队列声明中如何记录两端和有效元素个数?

答:

因为如果使用数组结构,出队列要在数组头部取出数据,效率会比较低。

数组:

  • 出队操作 :把第 0 个元素拿走后,数组会留下一个空洞。为了保持数组的连续性,后面的所有元素必须整体向前移动一位 (array[i-1] = array[i])。
  • 代价 :假设队列里有 10000 个元素,出队一次,就要搬运 9999 个元素。所以普通数组出队的时间复杂度是 O(n),极其耗费 CPU。

即使有些实现为了不移动数据,只让一个 head 指针往后移,却又不回绕或复用前面的空间,这会导致出现有空位却不能继续入队(称为"假溢出")(可以使用循环数组处理)

链表是由分散在内存中的节点通过指针连接起来的。

  • 出队操作 :只需要把队头(head)指针指向下一个节点(head = head->next),然后将原来的头节点释放掉即可。

  • 代价 :全程只改变了几个指针的指向,完全不需要移动任何数据 。因此链表出队的时间复杂度是 O(1)。

  • 同时,删除最后一个节点时,还要把队尾置空。链表入队(在尾部添加)只要维护好一个 tail(尾指针),也是 O(1)。

    文中的队列声明用 phead 记录队头、ptail 记录队尾、size 记录有效元素个数,入队、出队时要相应更新计数


问题 6:循环队列为什么让 Q.rear 所指位置不存储数据?原文用什么关系描述队空和队满?

答:

"牺牲一个存储单元(即 Q.rear 所指位置不存数据)"是为了解决队空和队满判断条件冲突的问题

原文用 Q.rear = Q.front 表示头尾位置重合,对应队空;用 rear + 1 = front 表示队尾再往前走一格就到队头,对应队满。这里的 = 是关系表达,写成 C 的判断条件时要用 ==。


高频问题 1:请比较栈与队列的插入、删除位置和元素进出顺序。

答:

栈顶插入和删除元素,后进先出

队尾插入,队头删除,先进先出


高频问题 2:原文为什么分别倾向于用数组实现栈、用链表实现队列?

答:

原文倾向于用数组实现栈,是因为栈在栈顶进出,把栈顶安排在数组尾部时,尾部插入数据的代价比较小。

原文倾向于用链表实现队列,是因为队列从队头出队,而原文认为数组头部出数据效率较低。这是文中对结构选择的理由;原文也明确说循环队列可以使用数组实现,因此不能把这一倾向理解成队列只能使用链表。


高频问题 3:循环队列为什么要空出 rear 所指位置?原文如何区分队空与队满?

答:

空出 rear 所指位置,是为了让队空和队满对应不同的头尾关系,避免仅凭两个位置重合时无法区分状态。

按原文的关系表达,Q.rear = Q.front 对应队空,rear + 1 = front 对应队满,也就是队尾再沿环前进一格就到队头。这里的 = 表示相等关系;如果写 C 判断,要用 ==。由于首尾相连,"前进一格"也包括从末尾回到开头。

原文末句把左图称为队满,与配图标注冲突:图中左侧 (a) 标为队空,右侧 (b) 标为队满。

相关推荐
@#¥&~是乱码鱼啦1 小时前
ArkWeb开发手记02|权限、网络白名单与页面缓存控制
网络·缓存
微软技术分享12 小时前
面向二进制安全的大模型多智能体框架
网络·安全·web安全
搁浅小泽18 小时前
新能源汽车多合一压缩机控制器CAN网络测试规范
网络·汽车
xixiaoyunya20 小时前
虚拟机备份怎么做:镜像级与文件级搭配的完整实操方案
linux·运维·网络
anew___20 小时前
《从零手写操作系统 (30):环境变量与进程上下文——export/unset与继承语义》
java·开发语言·网络·jvm·算法
谢亮_vipxieliang20 小时前
Go GMP调度模型——从原理到实战的完整指南
开发语言·网络·golang
wdfk_prog21 小时前
LWIP教程 03:从 `low_level_input()` 到 `pbuf_free()`——`pbuf` 的数据视图、Chain 与引用计数
运维·网络·笔记·学习
福兮说21 小时前
IP 地址转整数的七个坑:192.168.1.1 算出负数、127.1 也是合法地址、存进 INT 直接溢出
javascript·网络·网络协议·tcp/ip·mysql·golang
xiaoye-duck21 小时前
《Linux 网络编程》深入理解 epoll(上):从接口到底层内核工作机制剖析
linux·网络