news 2026/5/25 21:23:09

【数据结构与算法】数据结构基础——栈和队列

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【数据结构与算法】数据结构基础——栈和队列

目录

  • 栈和队列
    • 1. 栈
      • 1.1 栈的概念
      • 1.2 栈的实现方式分析
      • 1.3 栈的实现
        • 1.3.1 栈的初始化与销毁
        • 1.3.2 入栈与出栈
        • 1.3.3 栈的判空与有效元素个数
        • 1.3.4 栈顶元素
      • 1.4 栈的扩展
        • 1.4.1 两栈共享空间
    • 2. 队列
      • 2.1 队列的概念
      • 2.2 队列的实现方式分析
      • 2.3 队列的实现
        • 2.3.1 队列的初始化与销毁
        • 2.3.2 入队与出队
        • 2.3.3 队列的判空与队头队尾
        • 2.3.4 队列的有效元素个数
      • 2.4 循环队列

栈和队列

1. 栈

1.1 栈的概念

栈是一个特殊的线性表。栈只能在一端进行插入元素和删除元素的操作,其中能进行操作的一端称为栈顶,另一端称作栈底。其具有先进后出LIFO(last in first out)的性质


栈的示意图 栈的示意图栈的示意图

特殊术语

  • 入栈(压栈):在栈顶插入元素称为入栈
  • 出栈:在栈顶删除元素称为出栈

1.2 栈的实现方式分析

【分析】我们根据栈的特殊性质需要选一个更适合的线性结构来实现它

对比维度顺序表链表
访问元素随机访问,时间复杂度 O(1)遍历链表,时间复杂度 O(n)
插入/删除(头部)需要移动后面的所有元素,O(n)只需修改指针,O(1)
插入/删除(尾部)若空间足够,O(1)需要遍历到尾部,O(n)(单链表)
插入/删除(中间)需要移动元素,O(n)找到位置后修改指针,O(1)
应用场景需要频繁随机访问、尾部操作密集、数据量相对固定需要频繁在任意位置插入删除、数据量变化大

经过顺序表和链表的对比我们发现,因为栈只能在栈顶(尾部)进行插入和删除操作,顺序表在尾部的操作时间复杂度是O(1)而链表为O(n)所以我们更适合使用顺序表来实现栈

——>【动态顺序表详解】<——

1.3 栈的实现

实现方式:顺序表实现,动态分配内存

typedefintSTDataType;typedefstructStack{STDataType*data;intsize;//顺序表有效元素个数intcapacity;//顺序表的容量}Stack;

这里与动态顺序表的定义方式一样,就不过多赘述了……


1.3.1 栈的初始化与销毁

栈的初始化时间复杂度:O(1)

voidStackInit(Stack*st){assert(st);st->data=NULL;st->size=st->capacity=0;};

这里关于assert(断言)防止空指针的解引用是一个好习惯,可以防止很多莫名其妙的BUG

栈的销毁时间复杂度:O(1)

voidStackDestroy(Stack*st){free(st->data);st->data=NULL;st->size=st->capacity=0;}

1.3.2 入栈与出栈

入栈时间复杂度:O(1)

voidStackPush(Stack*st,STDataType x){assert(st);//判断容量是否足够 不够就扩容if(st->size==st->capacity){intnewcapacity=st->capacity==0?4:st->capacity*2;STDataType*tmp=(STDataType*)realloc(st->data,newcapacity*sizeof(STDataType));if(tmp==NULL)exit(-1);st->data=tmp;st->capacity=newcapacity;}//插入元素st->data[st->size]=x;st->size++;}

出栈时间复杂度:O(1)

voidStackPop(Stack*st){assert(st);assert(!empty(st));//判断栈非空st->size--;}

1.3.3 栈的判空与有效元素个数

判空时间复杂度:O(1)

boolempty(Stack*st){assert(st);returnst->size==0;}

有效元素个数时间复杂度:O(1)

intsize(Stack*st){returnst->size;}

1.3.4 栈顶元素

获取栈顶元素时间复杂度:O(1)

STDataTypetop(Stack*st){returnst->data[st->size-1];//从下标0开始存储元素}

小结:以上就是栈的基本的实现,操作起来并不难,不过再提醒一句,因为实现数据结构用了大量指针,所以在操作时一定要给指针判空将没有用的指针及时置为NULL

1.4 栈的扩展

1.4.1 两栈共享空间

根据栈的特性,我们使用顺序表来实现栈。可是当栈的存储空间满了的时候,我们需要去为这个栈扩容,每次一满就要扩容这会有很多时间上的消耗。

如果此时有两个相同类型的栈,一个栈的存储空间快溢出了,另外一个确还是有很多空闲的空间,那我们何不根据栈的特性让两个栈合并,使得空间的使用率更高

两个栈合并实际上就是让两个栈共同使用同一个数组(顺序表),栈1的栈顶指针top1从-1开始,栈2的栈顶指针top2从capacity(数组的最大容量)开始,如下图


当栈1需要入栈的时候top1指针就像右移,栈2需要入栈时top2向左移

那么要如何判断栈是否满了呢,我们先看两个特殊情况【1】top1直接走到了最右边,即两栈共享的空间全部都是栈1的元素,此时情况如下

此时如果栈满了就会满足top2 - top1 == 1【2】与第一种情况相反top2直接走到了最左边,同理栈满时有top2 - top1 == 1。此时还有一种一般情况如下

也是当top2 - top1 == 1时栈满了。所以得出结论:当top2 - top1 == 1时栈就满了

应用场景
两栈共享空间一般在两个栈满足此消彼长的条件时使用,即栈1元素增加时栈2的元素就要减少,就像买股票,当你买入了一份股票之后那一定有人持有的股票减少了
相反的,如果两个栈都是一直在插入元素的话空间很快就会满,而设计这样的结构也就没意义了。两栈共享空间只是一个技巧,适合两个栈存储的是相同的数据类型,如果是不同的数据类型使用这种存储方式只会使操作更复杂

2. 队列

2.1 队列的概念

队列是一种只能在一端插入数据另一端删除数据FIFO(first in first out)的特殊的线性表。其中插入数据的一端称为队尾,删除数据的一端称作队头


队列结构示意图 队列结构示意图队列结构示意图


关于队列的特殊术语

  • 队头:队列出队一端的第一个元素
  • 队尾:队列入队一端的第一个元素
  • 入队:在队尾插入元素称为入队
  • 出队:在队头删除元素称为出队

2.2 队列的实现方式分析

分析】根据队列的特殊性质,找一种最适合的数据结构来实现它,这里再次拿到上面的对比表格

对比维度顺序表链表
访问元素随机访问,时间复杂度 O(1)遍历链表,时间复杂度 O(n)
插入/删除(头部)需要移动后面的所有元素,O(n)只需修改指针,O(1)
插入/删除(尾部)若空间足够,O(1)需要遍历到尾部,O(n)(单链表)
插入/删除(中间)需要移动元素,O(n)找到位置后修改指针,O(1)
应用场景需要频繁随机访问、尾部操作密集、数据量相对固定需要频繁在任意位置插入删除、数据量变化大

队列与栈不同,栈只能在栈顶的一端进行插入删除等操作。而队列是在队头和队尾两端都进行频繁的操作,所以考虑两种线性表两端的操作


经过对比发现,顺序表和链表在头部操作和尾部操作都各自有优势,所以我们此时就要考虑使用哪个可以优化或者更方便优化


优化】考虑到链表在尾部的操作是O(n)的原因是每次都需要遍历一遍链表才可以找到尾节点,那何不干脆就直接将尾节点保存下来。将链表的尾节点保存下来之后就可以将链表尾部的操作优化到O(1)


结论】使用链表来实现队列


2.3 队列的实现

队列的结构

typedefintQDataType;//链表节点的结构typedefstructQueueNode{QDataType data;structQueueNode*next;}QueueNode;//队列的结构typedefstructQueue{QueueNode*phead;//指向队列的头节点QueueNode*ptail;//指向队列的尾节点}Queue;
2.3.1 队列的初始化与销毁

队列的初始化

voidQueueInit(Queue*pq){assert(pq);pq->phead=p->ptail=NULL;}

队列的销毁

voidQueueDestory(Queue*pq){assert(pq);QueueNode*pcur=pq->phead;while(pcur){QueueNode*pnext=pcur->next;free(pcur)pcur=pnext;}//最后将头指针和尾指针置为NULL 防止野指针pq->phead=pq->ptail=NULL;}

【注意】销毁队列后要记得将pheadptail置为NULL防止野指针

2.3.2 入队与出队

入队

voidpush(Queue*pq,QDataType x){assert(pq);//动态申请节点QueueNode*newNode=(QueueNode*)malloc(sizeof(QueueNode));if(newNode==NULL){perror("malloc fail");exit(-1);}newNode->data=x;newNode->next=NULL;//如果队列不为空if(pq->phead){pq->ptail->next=newNode;pq->ptail=newNode;}else{//队列为空pq->phead=pq->ptail=newNode;}}

出队

voidpop(Queue*pq){assert(pq);assert(!empty(pq));//当队列只有一个节点时if(pq->phead==pq->ptail){free(pq->phead);pq->phead=pq->ptail=NULL;}else{QueueNode*pnext=pq->phead->next;free(pq->phead);pq->phead=pnext;}}

这里要注意出队是要分两种情况!
【1】当队列只有一个节点时【2】当队列有多个节点时

2.3.3 队列的判空与队头队尾
//判空boolempty(Queue*pq){returnpq->phead=NULL;}//返回队头元素QDataTypeQueueFront(Queue*pq){assert(pq);assert(!empty(pq));returnpq->phead->data;}//返回队尾元素QDataTypeQueueback(Queue*pq){assert(pq);assert(!empty(pq));returnpq->ptail->data;}
2.3.4 队列的有效元素个数
intsize(Queue*pq){assert(pq);QueueNode*pcur=phead;intsize=0;while(pcur){size++;pcur=pcur->next;}returnsize;}

读到这里可能就会有读者有疑惑,前面所有的操作时间复杂度都是O(1)怎么到这里时间复杂度就变成O(n)了,或许有的人会觉得这就是链表的缺陷是不可更改的

但是优化的方法其实很简单。既然前面队列的结构都已经维护头指针和尾指针,那干脆就再维护一个队列的长度

但是维护一个size就意味着代码会更复杂,所以也需要分情况来定义和维护
【1】如果在不需要频繁的获取队列的长度的情况下就继续使用之前的方法【2】如过需要频繁获取队列的长度,就再维护一个队列长度size

typedefstructQueue{QueueNode*phead;//指向队列的头节点QueueNode*ptail;//指向队列的尾节点intsize;}Queue;

维护 s i z e 的版本 维护size的版本维护size的版本

2.4 循环队列

引入:上文说到,实现队列这个数据结构时使用链表实现会更好。原因在于使用顺序表实现队列的优化没有链表好,但是使用顺序表来实现队列其实也是有它自己的优化方式的

顺序表示实现普通队列

如下,是一个使用顺序表实现队列的结构,初始时定义队头指针front和队尾指针rear指向顺序表的开头

上文中提到使用顺序表实现队列最大的问题就在于顺序表头部操作的时间复杂度为O(n),也就是出队需要花费很多时间。但是其实是因为顺序表每次进行头部操作时要移动后面的元素才使得时间效率低,所以就在这里对头部操作的优化,对于每次出队,只需将front指针向后移动即可

顺序表实现循环队列

到这里用顺序表实现队列在时间上的问题解决了,但是此时又发现,当rear指针走到顺序表的最后时,队列就算满了,而队列的前面还有很多的空闲的位置没有使用这样就导致了很多的空间浪费


此时,就应该想一个方法使得顺序表前面的空间也可以被使用。将队列的整体看成是一个环,当rearfront要越界的时候,再让它们跳回到顺序表的起始位置,就跟一个环一样

实现方式:

  • 每当入队完成时(push):rear = (rear + 1) % size
  • 每当出队完成时(pop):front = (front + 1) % size

【注】size是顺序表的长度


循环队列初始状态 循环队列初始状态循环队列初始状态

由上述可知,当队列为空时front == rear,再看当队列满了的时候

此时的判断条件还是front == rear这样的话,当front指针等于rear指针时,根本就不知道队列此时是空的还是满的,所以还需要修改。在顺序表中一直保留一个位置不使用,此时判断队列为满的条件就变成了front - rear = 1

特别的如果需要队列的有效元素的个数的话:int length = (rear - front + size) % size

循环队列的代码实现

typedefstruct{int*data;intcapacity;intfront;intrear;}MyCircularQueue;//初始化MyCircularQueue*myCircularQueueCreate(intk){//创建循环队列MyCircularQueue*cq=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));cq->data=NULL;cq->front=cq->rear=0;//分配内存int*tmp=(int*)malloc((k+1)*sizeof(int));if(!tmp)exit(-1);cq->data=tmp;cq->capacity=k+1;returncq;}//入队 如果入队成功返回true反之返回falseboolmyCircularQueueEnQueue(MyCircularQueue*obj,intvalue){assert(obj);intsize=obj->capacity;//如果队列满了 就返回falseif((obj->rear+1)%size==obj->front){returnfalse;}else{obj->data[obj->rear]=value;obj->rear=(obj->rear+1)%size;//更新rear的值returntrue;}}//出队 如果出队成功返回true反之返回falseboolmyCircularQueueDeQueue(MyCircularQueue*obj){assert(obj);intsize=obj->capacity;if(obj->rear==obj->front){returnfalse;}else{obj->front=(obj->front+1)%size;returntrue;}}//返回队列头部元素intmyCircularQueueFront(MyCircularQueue*obj){assert(obj);if(obj->rear==obj->front){return-1;}else{returnobj->data[obj->front];}}//返回队列队尾元素intmyCircularQueueRear(MyCircularQueue*obj){assert(obj);intsize=obj->capacity;if(obj->rear==obj->front){return-1;}else{returnobj->data[(obj->rear-1+size)%size];}}//判断是否为空队列boolmyCircularQueueIsEmpty(MyCircularQueue*obj){assert(obj);returnobj->front==obj->rear;}//判断队列是否满了boolmyCircularQueueIsFull(MyCircularQueue*obj){assert(obj);intsize=obj->capacity;return(obj->rear+1)%size==obj->front;}//销毁队列voidmyCircularQueueFree(MyCircularQueue*obj){assert(obj);free(obj->data);obj->data=NULL;obj->rear=obj->front=obj->capacity=0;free(obj);}
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/25 21:20:32

Python 3、VS Code、PyCharm 安装常见问题及解决方案大全(Windows/Mac/Linux)

摘要:本文整理了在 Windows、macOS、Linux 三大操作系统上安装 Python 3、配置 VS Code 和 PyCharm 时最常见的错误及解决方案,帮助开发者快速定位并解决问题,顺利完成开发环境搭建。 目录 Python 3 安装常见问题 Windows 系统 macOS 系统 Linux 系统 VS Code Python 环境配…

作者头像 李华
网站建设 2026/5/25 21:19:58

探析数字孪生的核心特性与应用价值

随着物联网、大数据、人工智能技术的深度融合&#xff0c;数字孪生技术打破了物理世界与虚拟世界的壁垒&#xff0c;成为智能制造、智慧城市、基建运维等领域的核心赋能技术。数字孪生并非简单的三维建模或虚拟仿真&#xff0c;而是对物理实体全维度、全周期、全要素的数字化镜…

作者头像 李华
网站建设 2026/5/25 21:13:59

2026年横评10款降AIGC网站:一键锁定高效助手!

这两年&#xff0c;AI写作工具让论文撰写和内容创作变得高效又便捷&#xff0c;很多学生和职场人都从中受益。但随着技术发展&#xff0c;高校、平台和期刊对AI生成内容的检测标准也越来越严格。不少用户发现&#xff0c;自己用AI写的文章常常被系统标记为“存在AI痕迹”&#…

作者头像 李华
网站建设 2026/5/25 21:12:07

告别广色域显示器色彩失真:novideo_srgb硬件级校准方案

告别广色域显示器色彩失真&#xff1a;novideo_srgb硬件级校准方案 【免费下载链接】novideo_srgb Calibrate monitors to sRGB or other color spaces on NVIDIA GPUs, based on EDID data or ICC profiles 项目地址: https://gitcode.com/gh_mirrors/no/novideo_srgb …

作者头像 李华
网站建设 2026/5/25 21:10:41

Inkscape中SVG笔画字体应用:复刻Elektuur复古八角形字体

1. 项目概述&#xff1a;重现80年代的复古科技美学如果你和我一样&#xff0c;是个对上世纪七八十年代电子杂志、复古科技美学着迷的人&#xff0c;那么“Elektuur”这个名字一定不会陌生。作为欧洲极具影响力的电子技术杂志&#xff0c;它在那个黄金年代塑造了一代工程师和爱好…

作者头像 李华
网站建设 2026/5/25 21:10:01

选好钢格板?通风口省钱不踩坑!

在工业厂房、光伏平台乃至市政工程的规划建设中&#xff0c;钢格板凭借其卓越的承重、通风、透光及耐腐蚀性能&#xff0c;早已成为平台铺设、沟盖板、检修通道等领域不可或缺的关键建材。然而&#xff0c;面对市面上琳琅满目的产品&#xff0c;如何选择一款既能满足严苛工况&a…

作者头像 李华