首页 > 其他分享 >栈与队列 408相关

栈与队列 408相关

时间:2024-11-25 09:57:54浏览次数:11  
标签:return 队列 top int 相关 节点 rear 408

栈与队列

一、栈的全面解析

(一)栈的基本概念

栈(Stack)作为一种特殊的线性表,其核心特性是遵循后进先出(Last In First Out,LIFO)原则。想象一个垂直放置的容器,只能在顶端进行元素的插入与移除操作,这个顶端就是所谓的栈顶(top),而底部则为栈底(bottom)。例如,一摞叠放的餐盘,最后放置上去的餐盘总是最先被取用,最先放置的餐盘则在最底部,最后才会被拿到,这生动地诠释了栈的工作原理。在日常生活中,浏览器的后退功能便是栈应用的典型实例。每访问一个新网页时,该网页信息就被“压入”栈中,当点击后退按钮时,从栈顶取出最近一次压入的网页信息,从而实现页面的回退浏览。

(二)栈的物理存储结构

  1. 顺序存储
    • 在顺序存储结构下,栈通常借助数组来实现。通过定义一个固定大小的数组来容纳栈元素,并使用一个变量(如 top)来指示栈顶元素的位置。例如,在 C 语言中,可以如下定义一个顺序栈结构体:
#include <stdio.h>
#include <stdlib.h>

// 定义栈的最大容量
#define MAX_SIZE 100

// 栈结构体
typedef struct {
    int data[MAX_SIZE];
    int top;
} Stack;
  • 初始化时,将 top 设置为 -1,表示栈为空。当有元素入栈时,top 自增 1,并将元素存储在 data[top] 位置;出栈时,先取出 data[top] 的元素,然后 top 自减 1。这种顺序存储方式的优点是访问元素速度快,因为可以直接通过数组下标定位元素。然而,其缺点也较为明显,由于数组大小固定,一旦栈元素数量超过数组容量,就可能引发栈溢出问题,且在栈元素频繁进出时,可能需要进行大量的数据移动操作。例如,在一个深度优先搜索(DFS)算法中,如果递归调用层级过深,而使用顺序栈来存储递归状态,就可能因栈空间不足导致程序出错。
  1. 链式存储
    • 链式存储的栈通过链表来构建。每个节点包含数据域和指向下一个节点的指针域。在这种结构中,栈顶即为链表的头节点。以 C 语言为例,节点结构体可定义为:
typedef struct StackNode {
    int data;
    struct StackNode *next;
} StackNode;
  • 入栈操作就是在链表头部插入新节点,出栈操作则是删除链表头部节点。链式存储的栈不存在固定大小的限制,能够根据需要动态分配内存空间,有效地避免了顺序存储中可能出现的栈溢出问题。但是,由于需要额外的指针域来维护节点间的链接关系,其空间开销相对较大,并且在访问栈中特定位置元素时,需要从栈顶(链表头)开始遍历,时间复杂度相对较高。例如,在实现一个表达式求值算法时,如果采用链式栈来存储操作数和运算符,能够灵活处理各种复杂表达式,而不用担心栈容量不足的问题。

(三)栈的操作实现

1. C 语言实现
// 初始化栈
void InitStack(Stack *s) {
    s->top = -1;
}

// 判断栈是否为空
int IsEmpty(Stack *s) {
    return s->top == -1;
}

// 判断栈是否已满
int IsFull(Stack *s) {
    return s->top == MAX_SIZE - 1;
}

// 入栈操作
void Push(Stack *s, int value) {
    if (IsFull(s)) {
        printf("栈已满,无法入栈\n");
        return;
    }
    s->top++;
    s->data[s->top] = value;
}

// 出栈操作
int Pop(Stack *s) {
    if (IsEmpty(s)) {
        printf("栈为空,无法出栈\n");
        return -1;
    }
    int value = s->data[s->top];
    s->top--;
    return value;
}

// 获取栈顶元素
int Peek(Stack *s) {
    if (IsEmpty(s)) {
        printf("栈为空,无栈顶元素\n");
        return -1;
    }
    return s->data[s->top];
}
2. Java 语言实现
class Stack {
    private int[] data;
    private int top;
    private static final int MAX_SIZE = 100;

    // 构造函数,初始化栈
    public Stack() {
        data = new int[MAX_SIZE];
        top = -1;
    }

    // 判断栈是否为空
    public boolean isEmpty() {
        return top == -1;
    }

    // 判断栈是否已满
    public boolean isFull() {
        return top == MAX_SIZE - 1;
    }

    // 入栈操作
    public void push(int value) {
        if (isFull()) {
            System.out.println("栈已满,无法入栈");
            return;
        }
        top++;
        data[top] = value;
    }

    // 出栈操作
    public int pop() {
        if (isEmpty()) {
            System.out.println("栈为空,无法出栈");
            return -1;
        }
        int value = data[top];
        top--;
        return value;
    }

    // 获取栈顶元素
    public int peek() {
        if (isEmpty()) {
            System.out.println("栈为空,无栈顶元素");
            return -1;
        }
        return data[top];
    }
}

(四)栈的应用

  1. 函数调用栈
    • 在程序运行期间,当一个函数调用另一个函数时,系统会创建一个函数调用栈。将当前函数的执行状态信息(如局部变量、返回地址等)压入栈中。例如,在递归函数执行过程中,每一次递归调用都会将当前层的参数和局部变量等信息压入栈。以计算斐波那契数列的递归函数为例:
int Fibonacci(int n) {
    if (n == 0 || n == 1) {
        return n;
    }
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}
  • 在计算 Fibonacci(5) 时,首先将 Fibonacci(5) 的状态信息压入栈,然后在计算 Fibonacci(4)Fibonacci(3) 等过程中,不断将相应的状态压入栈。当递归到 Fibonacci(0)Fibonacci(1) 时开始返回,从栈顶依次弹出之前压入的状态信息并进行计算,最终得到 Fibonacci(5) 的结果。如果递归深度过大,可能会导致栈溢出,这也是使用递归时需要注意的问题。
  1. 表达式求值
    • 中缀表达式求值是栈应用的经典场景。例如对于表达式“3 + 4 * 2 / ( 1 - 5 )”,可以利用两个栈,一个操作数栈和一个运算符栈来实现求值。扫描表达式时,数字直接压入操作数栈,运算符则根据其优先级以及栈顶运算符的情况进行入栈或出栈操作,并对操作数栈的栈顶元素进行相应运算。比如,先扫描到“3”,压入操作数栈;再扫描到“+”,压入运算符栈;接着扫描到“4”,压入操作数栈;扫描到“”,因为“”优先级高于“+”,所以“”压入运算符栈;扫描到“2”,压入操作数栈;此时扫描到“/”,“/”优先级高于“”,“/”压入运算符栈;扫描到“(”,“(”压入运算符栈;扫描到“1”,压入操作数栈;扫描到“-”,“-”压入运算符栈;扫描到“5”,压入操作数栈;扫描到“)”,则将运算符栈中“-”对应的操作数栈顶两个元素(1 和 5)取出进行“-”运算,结果 -4 压入操作数栈,运算符栈中“-”出栈;然后继续处理运算符栈中剩余运算符与操作数栈顶元素的运算,最终得到表达式的值。

二、队列的深度解读

(一)队列的基本概念

队列(Queue)是一种遵循先进先出(First In First Out,FIFO)原则的线性表。可以类比为人们在银行排队办理业务的场景,先到达的客户先接受服务并离开队列,后到达的客户依次在队列末尾排队等待。队列有队头(front)和队尾(rear)两个关键端点,元素从队尾进入队列,从队头离开队列。在计算机操作系统的任务调度中,多个任务按照到达的先后顺序排队等待 CPU 资源,先到达的任务先被 CPU 处理,这充分体现了队列的先进先出特性。

(二)队列的物理存储结构

  1. 顺序存储
    • 顺序存储的队列利用数组来实现。定义一个数组来存储队列元素,并使用两个变量 frontrear 分别表示队头和队尾元素的索引。例如,在 C 语言中:
#include <stdio.h>
#include <stdlib.h>

// 队列的最大容量
#define MAX_SIZE 100

// 队列结构体
typedef struct {
    int data[MAX_SIZE];
    int front;
    int rear;
} Queue;
  • 初始化时,frontrear 通常都设置为 0。入队操作时,将元素存储在 data[rear] 位置,然后 rear 自增 1(在循环队列中,需要考虑 rear 到达数组末尾后的处理,通常采用取模运算);出队操作时,取出 data[front] 的元素,然后 front 自增 1(同样在循环队列中需取模)。顺序存储的队列访问元素较为高效,但存在队列假溢出问题。例如,当 rear 到达数组末尾,即使队列前端还有空位,也可能被误判为已满。为解决此问题,常采用循环队列的形式。
  1. 链式存储
    • 链式存储的队列通过链表构建。包含一个头指针指向队头节点,一个尾指针指向队尾节点。节点结构体类似栈的链式结构:
typedef struct QueueNode {
    int data;
    struct QueueNode *next;
} QueueNode;
  • 入队操作就是在链表尾部添加新节点,若队列为空,则头指针和尾指针都指向新节点;出队操作则是删除队头节点,并更新头指针。链式队列的优点是可以灵活地动态分配内存,不存在固定大小的限制,适用于元素数量不确定且可能较大的情况。但由于需要维护指针,其操作的时间和空间复杂度相对顺序队列略高。例如,在一个网络数据传输缓存队列中,如果数据流量不稳定,采用链式队列可以更好地适应数据量的动态变化,避免因队列容量限制导致数据丢失。

(三)特殊队列 - 循环队列

  1. 概念与原理
    • 循环队列是为了解决顺序队列的假溢出问题而设计的。它将数组视为一个环形结构,当队尾指针 rear 到达数组末尾时,若数组前端还有空位,则可将新元素绕回数组开头存放。在循环队列中,判断队列空的条件是 front == rear,而判断队列满的条件通常有两种方式,一种是牺牲一个存储单元,即 (rear + 1) % MAX_SIZE == front;另一种是设置一个计数器来记录队列中元素的数量。例如,在一个循环队列中,假设 MAX_SIZE = 5,初始时 front = rear = 0,依次入队元素 1、2、3,此时 rear = 3front = 0。当再入队一个元素 4 时,rear = (3 + 1) % 5 = 4。如果此时出队一个元素,front = (0 + 1) % 5 = 1,队列依然可以正常工作,不会因为 rear 到达末尾而出现假溢出情况。
  2. 操作实现与计算
    • 以下是 C 语言中循环队列的基本操作实现:
// 初始化循环队列
void InitQueue(Queue *q) {
    q->front = 0;
    q->rear = 0;
}

// 判断循环队列是否为空
int IsEmptyQueue(Queue *q) {
    return q->front == q->rear;
}

// 判断循环队列是否已满(牺牲一个存储单元法)
int IsFullQueue(Queue *q) {
    return (q->rear + 1) % MAX_SIZE == q->front;
}

// 入队操作(循环队列)
void Enqueue(Queue *q, int value) {
    if (IsFullQueue(q)) {
        printf("队列已满,无法入队\n");
        return;
    }
    q->data[q->rear] = value;
    q->rear = (q->rear + 1) % MAX_SIZE;
}

// 出队操作(循环队列)
int Dequeue(Queue *q) {
    if (IsEmptyQueue(q)) {
        printf("队列为空,无法出队\n");
        return -1;
    }
    int value = q->data[q->front];
    q->front = (q->front + 1) % MAX_SIZE;
    return value;
}
  • 在循环队列中,一些相关的计算也很重要。例如,计算队列中当前元素的数量,可以使用公式 (rear - front + MAX_SIZE) % MAX_SIZE(当采用牺牲一个存储单元判断队列满的方式时)。另外,在设计循环队列的应用场景时,需要根据实际需求合理确定队列的最大容量,以平衡内存使用和队列操作的效率。例如,在一个实时数据处理系统中,如果数据产生速度较快且处理速度相对较慢,需要设置较大的循环队列容量来缓存数据,但过大的容量可能会导致内存占用过高,影响系统整体性能。

(四)队列的应用

  1. 广度优先搜索(BFS)
    • 在图的遍历算法中,广度优先搜索使用队列来存储待访问的节点。从起始节点开始,将其入队,然后不断取出队头节点,访问其相邻节点并将未访问过的相邻节点入队,直到队列为空,这样可以按照距离起始节点由近及远的顺序遍历图中的节点。例如,在一个迷宫求解问题中,将迷宫的入口作为起始节点,每个节点表示迷宫中的一个位置,相邻节点就是通过上下左右移动可以到达的位置。使用队列进行广度优先搜索,可以找到从入口到出口的最短路径。具体操作时,先将入口节点入队,然后循环执行出队操作,对出队节点的相邻节点进行判断,如果是未访问过的通道节点,则将其入队,并记录其前驱节点,直到队列为空或者找到出口节点。通过回溯前驱节点,就可以得到从入口到出口的最短路径。
  2. 打印机任务管理
    • 在多任务操作系统中,打印机任务可以看作是一个队列。多个打印任务按照提交的先后顺序排队,打印机依次处理队列中的任务,先提交的任务先被打印输出。例如,在一个办公室网络打印环境中,不同用户提交的打印文档形成一个打印任务队列。打印机从队列头部取出任务进行打印,当一个任务完成后,继续处理队列中的下一个任务。这确保了打印任务的公平性和顺序性,避免了任务混乱和插队现象。同时,可以根据打印机的性能和任务的紧急程度等因素,对队列进行一些优化操作,如设置优先级队列,让紧急任务优先处理等。

无论是栈还是队列,它们都是计算机科学中非常基础且重要的数据结构。在实际的编程和算法设计中,根据不同的需求合理选择和运用它们,能够有效地解决各种复杂的问题,提高程序的效率和性能。对于深入学习数据结构与算法的人来说,熟练掌握栈和队列的相关知识,包括它们的各种存储结构、特殊类型以及应用场景等,是迈向更高层次的重要一步,尤其对于考研复习中的数据结构部分,这些知识更是重点考查内容,需要深入理解并能够灵活运用到各类算法题的解答中。

标签:return,队列,top,int,相关,节点,rear,408
From: https://blog.csdn.net/weixin_69477306/article/details/144019657

相关文章

  • springboot 整合 rabbitMQ (延迟队列)
    前言:延迟队列是一个内部有序的数据结构,其主要功能体现在其延时特性上。这种队列存储的元素都设定了特定的处理时间,意味着它们需要在规定的时间点或者延迟之后才能被取出并进行相应的处理。简而言之,延时队列被设计用于存放那些需要在特定时间到达时才处理的元素。使用场景:1、......
  • ros2学习日记_241124_ros相关链接
    前言提醒:文章内容为方便作者自己后日复习与查阅而进行的书写与发布,其中引用内容都会使用链接表明出处(如有侵权问题,请及时联系)。其中内容多为一次书写,缺少检查与订正,如有问题或其他拓展及意见建议,欢迎评论区讨论交流。文章目录前言Ros相关网址Ros相关网址鱼香......
  • 代码随想录算法训练营第十一天|LC150.逆波兰表达式求值|LC239.滑动窗口最大值|LC347.
    150.逆波兰表达式求值-力扣(LeetCode)题目要求:    1、整数除法只保留整数部分;    2、该表达式总会得出有效数值且部存在除数为0的情况;    3、逆波兰表达式:是一种后缀表达式,所谓后缀就是指运算符写在后面。fromtypingimportListfromoperato......
  • 如何使用PbootCMS内容详情页标签调用相关信息
    常用标签{content:id}:文章编号{content:scode}:栏目编码{content:subscode}:副栏目编码{content:sortname}:栏目名称{content:subsortname}:副栏目名称{content:sortlink}:栏目链接{content:subsortlink}:副栏目链接{content:title}:文章标题{content:titlecolor}:文章标题颜色......
  • 开源平台上有哪些和安全内容自动化协议(SCAP)相关的工具?
    以下是一些开源平台上与安全内容自动化协议(SCAP)相关的工具:OpenSCAPOpenSCAPBase:命令行工具,可实现显示特定安全内容信息、漏洞和配置扫描以及不同SCAP格式之间的转换等功能1.OpenSCAPDaemon:守护进程工具,能确保按照计划对机器和容器进行评估1.SCAPWorkbench:图形界面工具,......
  • 数据结构-链表、栈、动态数组、队列
    数据结构文章目录数据结构不透明指针定义优点应用场景不透明指针的实现定义不透明指针类型链表知识点节点(Node)头节点(Head)尾节点(Tail)单向链表双链表动态数组队列队列的链式存储队列的顺序存储栈栈的顺序存储栈的链式存储不透明指针定义不透明指针是指指向一个......
  • 并发编程(13)——无锁环形并发队列
    文章目录十三、day131.什么是无锁数据结构?2.环形队列3.实现线程安全的环形队列3.1实现有锁环形队列3.2实现无锁环形队列(有缺陷)3.3实现无锁环形队列(无缺陷)3.3.1pop函数3.3.2push函数3.3.3优化后的pop和push函数3.3.4完整代码4.无锁环形并发队列的优缺点十......
  • 安装部署系统是指将操作系统(OS)和相关应用程序配置并安装到计算机或虚拟机中,通常在大规
    安装部署系统是指将操作系统(OS)和相关应用程序配置并安装到计算机或虚拟机中,通常在大规模计算机系统、数据中心或云环境中进行。一个有效的系统部署方案不仅需要考虑操作系统的安装,还要涉及硬件配置、网络设置、软件应用、自动化和安全等多个方面。下面将详细介绍安装部署的技术细......
  • 科普文:软件架构之Linux系列【linux内核数据结构:链表、队列、映射、二叉树】
    概叙科普文:软件架构之Linux系列【linux内核数据结构汇总】-CSDN博客Linux内核提供了许多复杂的数据结构,这些结构被广泛用于各种不同的目的,例如存储设备管理、内存管理、进程管理等。以下是一些常见的数据结构以及它们的简要描述:双向链表(list):实现链表的数据结构,每个节点都......
  • 多线程 相关面试集锦
    什么是线程?1、线程是操作系统能够进⾏运算调度的最⼩单位,它被包含在进程之中,是进程中的实际运作单位,可以使⽤多线程对进⾏运算提速。⽐如,如果⼀个线程完成⼀个任务要100毫秒,那么⽤⼗个线程完成改任务只需10毫秒什么是线程安全和线程不安全?1、线程安全线程安全:就是......