.栈和队列的概念以及实现


今天,我们介绍最后两个特殊的线性结构---->栈和队列。栈和队列一讲完,我们接下来就要进入二叉树的学习,其实如果前面的博客你能够理解的话,你就会惊奇的发现:栈和队列的所有接口我们都已经在顺序表和链表中实现过了!接下来,我就带大家走进这两个特殊的数据结构。

1.1栈的概念及结构

栈是一种特殊的线性结构,它遵循的是先进后出的原则,对于一个栈来说,只允许在栈的一端入数据和出数据,这一端的学名叫做栈顶,而另外一端则称为栈底。而在将数据插入栈的操作我们叫做压栈(入栈),在栈顶删除数据的操作我们叫做出栈。

值得一提的是:在操作系统这门课里也有有关栈的概念,我们数据结构里的栈和操作系统里讲的栈是两个东西,只是二者的行为类似,并不能为二者强行画上等号!!!!

我们可以用一张图片来看栈是怎么进行数据的插入和删除的

从图片不难看出,越早入栈的数据越晚才能取出,但是读者要注意,栈的后入先出是相对而言的,接下来我们通过两道选择题来好好体会一下这句话

第一道题就是简单的讲数据全部入栈取出后是入栈元素的逆序,所以答案是B

但是第二题就是对栈先入后出是相对而言的很好的考察!

我们来仔细分析题目:

讲完了有关栈的概念和性质还有两道选择题,相信大家对栈应该有一个初步的了解了,接下来我们开始着手实现以下栈这个数据结构:

首先,栈这个数据结构的实现通常有两种方式,一种是采用数组的方式,另一种是采用链表的方式,接下来我们来分析一下两种方式的优缺点:

数组结构:

使用数组结构的优点是我们可以用下标来表示栈顶指针top,并且由于栈只能在栈顶删除数据,相当于尾删,顺序表的尾删的效率极高,所以用数组实现栈是很好的选择!!!缺点就是可能在扩容的时候有性能消耗和一定的空间浪费,不过问题不是很大

链表结构:

使用链表结构来模拟栈,有点是空间的利用率高,不会造成浪费,比较好的方式就是将top指针当作当前链表的头节点,然后利用头插的方式插入元素,头删的方式弹出元素,相对于数组结构,链式的结构就相对复杂,感兴趣的读者可以自行实现。

接下来,我们来实现一个栈,和前面顺序表一样,静态的栈的实际应用价值不大,所以我们实现的是一个支持动态增长的栈

在这里在提一下,对于数组来模拟栈,栈顶指针通常有两种初始化的方式,一种是初始化成-1,表示栈顶指针指向当前元素的位置(开始的时候没有元素,就是-1),另一种方式是初始化成0,表示栈顶指针指向当前元素的下一个位置,两种方式没有优劣之分,我是使用初始化成0的版本进行讲解

Stack.h---->栈的定义和函数接口的声明

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{STDataType* a;size_t top;//栈顶指针size_t capacity;//容量
}Stack;
//初始化
void StackInit(Stack* ps);
//释放
void StackDestroy(Stack* ps);
//插入
void StackPush(Stack* ps, STDataType x);
//删除
void StackPop(Stack* ps);
//取栈顶数据
STDataType StackTop(Stack* ps);
//判断栈是否为空
bool StackEmpty(Stack* ps);
//获取栈元素的个数
size_t StackSize(Stack* ps);

接下来我们来实现各个功能:

首先是初始化和释放,和顺序表几乎一致,这里就不在赘述:

//初始化
void StackInit(Stack* ps)
{assert(ps);ps->a = NULL;ps->top = ps->capacity = 0;
}
//释放
void StackDestroy(Stack* ps)
{assert(ps);free(ps->a);ps->a = NULL;ps->top = ps->capacity = 0;
}

那么接下来就是push和pop操作,其实就是我们顺序表尾插和尾删的操作

//插入
void StackPush(Stack* ps, STDataType x)
{assert(ps);if (ps->top == ps->capacity){size_t newCapacity = ps->capacity == 0 ? 2 : ps->capacity * 2;STDataType* tmp= (STDataType*)realloc(ps->a, sizeof(STDataType) * newCapacity);if (NULL == tmp){printf("realloc fail\n");exit(-1);}else{ps->a = tmp;ps->capacity = newCapacity;}}ps->a[ps->top++] = x;
}
//删除
void StackPop(Stack* ps)
{assert(ps);assert(ps->top > 0);ps->top--;}

接下来是取栈顶的数据top,由于我们的top指针总是指向当前元素的下一个位置,所以栈顶元素的下标是top-1

//取栈顶数据
STDataType StackTop(Stack* ps)
{assert(ps);assert(ps->top > 0);return ps->a[ps->top - 1];
}

接下来栈有两个特殊的接口,一个是判断栈是否为空,以及栈元素的个数,判断栈是不是为空,只要判断栈顶指针是不是0即可,而栈元素的个数恰好是top指向的位置:

//判断栈是否为空
bool StackEmpty(Stack* ps)
{assert(ps);return ps->top == 0;
}
//获取栈元素的个数
size_t StackSize(Stack* ps)
{assert(ps);return ps->top;
}

接下来我们测试一下我们的栈

#define  _CRT_SECURE_NO_WARNINGS 1
#include "Stack.h"
void TestStack()
{Stack st;StackInit(&st);StackPush(&st, 1);StackPush(&st, 2);StackPush(&st, 3);StackPush(&st, 4);while (!StackEmpty(&st)){printf("%d ", StackTop(&st));StackPop(&st);}printf("\n");StackDestroy(&st);
}
int main()
{TestStack();return 0;
}

注意,栈的遍历和通常的顺序表的遍历是不一样的,我们不能写一个print来遍历,而是栈非空则取出数据然后再删除,这点要特别注意!!!!

我们的栈没有问题,那么关于栈我们就先告一个段落,接下来我们进入另外一个数据结构--->队列的学习

队列:和栈的先进后出不一样,队列的数据是先进先出的!队列规定:数据在队尾插入,在队头的位置删除,而且不同于栈只能取栈顶的数据,队列是可以取队头的数据,也可以取队尾的数据。

我们可以用一张形象的图片来看一看队列的结构:

那么了解了队列的特性,接下来我们就着手实现队列这个数据结构:和栈相似,队列也有数组版本和链表版本,但是由于队列需要删除队头的数据,而数组结构删除头部的数据是比较耗时的操作,基于这一点我们采取链表来实现这个队列

Queue.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
//链表实现队列
typedef int QDataType;
typedef struct QueueNode
{QDataType val;struct QueueNode* next;
}QNode;
typedef struct Queue
{QNode* head;//队头指针QNode* tail;//队尾指针
}Queue;
//队列初始化
void QueueInit(Queue* pq);
//队列释放
void QueueDestroy(Queue* pq);
//队列插入数据
void QueuePush(Queue* pq, QDataType x);
//队列出数据
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
//队列大小
size_t QueueSize(Queue* pq);
//队列判空
bool QueueEmpty(Queue* pq);
//删除数据(队头出数据)
void QueuePop(Queue* pq);

这里可能有读者就会不明白这个队列的具体结构,我们可以画一张图片来帮助理解这个结构:

那么为什么我们额外定义了这个结构体呢?原因如下:

如果第一次插入,需要改变队头和队尾指针,然而从我们单链表增删查改可以看出,我们需要使用二级指针来进行第一次插入!而使用结构体Queue,我们只要使用Queue结构体的指针就可以改变头尾指针!

接下来,我们开始实现队列这个数据结构(主体的操作和单链表的增删查改类似,注意一些细节)

队列的初始化和释放:

//队列初始化
void QueueInit(Queue* pq)
{assert(pq);pq->head = pq->tail = NULL;
}
//队列释放
void QueueDestroy(Queue* pq)
{assert(pq);QNode* cur = pq->head;while (cur){QNode* next = cur->next;free(cur);cur = next;}pq->head = pq->tail = NULL;
}

队列入数据,单链表的尾插

//队尾插入数据
void QueuePush(Queue* pq, QDataType x)
{assert(pq);QNode* newnode = (QNode*)malloc(sizeof(QNode));if (NULL == newnode){printf("malloc fail\n");exit(-1);}else{  newnode->val = x;newnode->next = NULL;if (NULL == pq->tail ){   //防止野指针assert(NULL==pq->head);pq->head = pq->tail = newnode;}else{pq->tail->next = newnode;pq->tail = newnode;}}
}

队列判空:当队头指针是空的时候队列才是空!

//队列判空
bool QueueEmpty(Queue* pq)
{return pq->head == NULL;
}

接下来的删除,取队头和 队尾数据都需要判空这个接口

队列删除:从队头出数据--->队列的头部出数据(单链表的头删)

//队头出节点
void QueuePop(Queue* pq)
{assert(pq);assert(!QueueEmpty(pq));QNode* next = pq->head->next;free(pq->head);pq->head = next;
}

但是这样的代码有问题,当只有一个节点的时候,虽然head被置空了,但是tail依旧指向原来已经被释放的空间,这时候就出现了野指针!所以只有一个节点的情况我们需要单独讨论

//队头出节点
void QueuePop(Queue* pq)
{assert(pq);assert(!QueueEmpty(pq));if (pq->head == pq->tail){free(pq->head);pq->head = pq->tail = NULL;}else{QNode* next = pq->head->next;free(pq->head);pq->head = next;}
}

接下来就是取队头的数据和队尾的数据,我们分别取名为front和back

//队头和队尾都可以取数据
QDataType QueueFront(Queue* pq)
{assert(pq);assert(!QueueEmpty(pq));return pq->head->val;
}
QDataType QueueBack(Queue* pq)
{assert(pq);assert(!QueueEmpty(pq));assert(pq->tail);return pq->tail->val;
}

最后就是求队列大小的接口size

//队列大小
size_t QueueSize(Queue* pq)
{assert(pq);size_t size = 0;QNode* cur = pq->head;while (cur){++size;cur = cur->next;}return size;
}

接下来我们测试一下我们的队列

void TestQueue()
{Queue q;QueueInit(&q);QueuePush(&q, 1);QueuePush(&q, 2);QueuePush(&q, 3);QueuePush(&q, 4);while (!QueueEmpty(&q)){printf("%d ", QueueFront(&q));QueuePop(&q);}printf("\n");QueueDestroy(&q);
}

和栈类似,队列取出数据也是只能边取边删,因为是先入先出,所以最后出来的数据顺序是1,2,3,4

程序运行出了预期结果!说明我们的队列是没有问题的!!!

完整的栈的接口代码如下:

Stack.c

#define  _CRT_SECURE_NO_WARNINGS 1
#include "Stack.h"
//初始化
void StackInit(Stack* ps)
{assert(ps);ps->a = NULL;ps->top = ps->capacity = 0;
}
//释放
void StackDestroy(Stack* ps)
{assert(ps);free(ps->a);ps->a = NULL;ps->top = ps->capacity = 0;
}
//插入
void StackPush(Stack* ps, STDataType x)
{assert(ps);if (ps->top == ps->capacity){size_t newCapacity = ps->capacity == 0 ? 2 : ps->capacity * 2;STDataType* tmp= (STDataType*)realloc(ps->a, sizeof(STDataType) * newCapacity);if (NULL == tmp){printf("realloc fail\n");exit(-1);}else{ps->a = tmp;ps->capacity = newCapacity;}}ps->a[ps->top++] = x;
}
//删除
void StackPop(Stack* ps)
{assert(ps);assert(ps->top > 0);ps->top--;}
//取栈顶数据
STDataType StackTop(Stack* ps)
{assert(ps);assert(ps->top > 0);return ps->a[ps->top - 1];
}
//判断栈是否为空
bool StackEmpty(Stack* ps)
{assert(ps);return ps->top == 0;
}
//获取栈元素的个数
size_t StackSize(Stack* ps)
{assert(ps);return ps->top;
}

Queue.c

#define  _CRT_SECURE_NO_WARNINGS 1
#include "Queue.h"
//队列初始化
void QueueInit(Queue* pq)
{assert(pq);pq->head = pq->tail = NULL;
}
//队列释放
void QueueDestroy(Queue* pq)
{assert(pq);QNode* cur = pq->head;while (cur){QNode* next = cur->next;free(cur);cur = next;}pq->head = pq->tail = NULL;
}
//队尾插入数据
void QueuePush(Queue* pq, QDataType x)
{assert(pq);QNode* newnode = (QNode*)malloc(sizeof(QNode));if (NULL == newnode){printf("malloc fail\n");exit(-1);}else{  newnode->val = x;newnode->next = NULL;if (NULL == pq->tail ){   assert(NULL==pq->head);pq->head = pq->tail = newnode;}else{pq->tail->next = newnode;pq->tail = newnode;}}
}
//队头和队尾都可以取数据
QDataType QueueFront(Queue* pq)
{assert(pq);assert(!QueueEmpty(pq));return pq->head->val;
}
QDataType QueueBack(Queue* pq)
{assert(pq);assert(!QueueEmpty(pq));assert(pq->tail);return pq->tail->val;
}
//队列大小
size_t QueueSize(Queue* pq)
{assert(pq);size_t size = 0;QNode* cur = pq->head;while (cur){++size;cur = cur->next;}return size;
}
//队列判空
bool QueueEmpty(Queue* pq)
{return pq->head == NULL;
}
//队头出节点
void QueuePop(Queue* pq)
{assert(pq);assert(!QueueEmpty(pq));if (pq->head == pq->tail){free(pq->head);pq->head = pq->tail = NULL;}else{QNode* next = pq->head->next;free(pq->head);pq->head = next;}
}

结语:

本篇文章主要介绍了栈和队列两个数据结构特殊的性质以及模拟实现。栈的先进后出,队列的先进先出是这两个数据特有的性质,那么在下一篇博客中,我们会利用这两个数据结构独有的特殊性质来做几道OJ题,通过OJ题来更好的理解栈的先入后出,队列的先进先出。如果本篇博客有不足或错误之处,请大家可以指出,希望能够一同进步

纸上得来终觉浅,可以这里看一眼(一)---->栈和队列相关推荐

  1. 纸上得来终觉浅,可以这里看一眼---->栈和队列(下)

    目录 括号匹配问题 用两个队列实现栈 用两个栈实现队列 设计循环队列 在上一篇博客里面,我们讲了栈和队列的性质以及栈和队列的模拟实现,那么这两个数据结构的特殊性质有什么应用场景呢?或者更直接一点,面试 ...

  2. 纸上得来终觉浅,绝知此事要躬行。

    在ITPUB上看帖子的时候,有个网友的回复就是标题中的一句诗:纸上得来终觉浅,绝知此事要躬行. 为弄明白这首诗的含义,我GOOGLE了,在百度知道中找到相关解释.觉得其中的意思比较适合我目前的状态. ...

  3. 纸上得来终觉浅,绝知此事要躬行

    周末时,领导在团队一个小群分享了一篇文章,全文很长 周二时打开了看了十几分钟,因为其他事情被打断,只看了1/5不到就搁置了 今天是周三,负责技术管理的同事将文章转到了大群,一起发出来的还有一张思维导图 ...

  4. 纸上得来终觉浅(experience is the best teacher)

    原本以为,写博客这么简单的东西,一看就会,但是我还是没想到,写第一篇博客是这么费时. 所以说,纸上得来终觉浅,绝知此事要躬行! good night!

  5. 学内核之十八:纸上得来终觉浅,绝知此事要躬行

    目录 0 前言 1 ioremap.vmalloc与原子上下文 2 copy_to_user与进程上下文 3 fasync与指针初始化 4 wait_event_interruptible与条件变量 ...

  6. 纸上得来终觉浅,构建之法东北师大站2016秋季学期

    2016年,构建之法教材在东北师大使用了两次.上学期是软件项目管理,下学期是软件工程.引用郑同学的话,这两门课占用了我几乎"全部"业余时间,可见的数据是我读书和看电影的数量都锐减1 ...

  7. 【纸上得来终觉浅】RoundRobinRule源码分析后,自己手写了轮询算法

    前言 今天自己查看了RoundRobinRule的源码,通过读源码走断点,大概自己知道了源码中,参数调用,下面我是查阅相关源代码,反正看过源代码以后总感觉纸上得来终觉浅,绝知此事要躬行,然后编写了轮询 ...

  8. 纸上得来终觉浅 绝知此事要躬行

    摘自:https://baike.baidu.com/item/%E7%BA%B8%E4%B8%8A%E5%BE%97%E6%9D%A5%E7%BB%88%E8%A7%89%E6%B5%85%EF%B ...

  9. 【人生参悟】纸上得来终觉浅,绝知此事要躬行

    这几天一直在研究saltstack和zabbix,参看了不少文档和博客,终于saltstack的部署研究得7788,zabbix所需要的LNMP环境也搭建完毕了.纵观这几天的工作,我有一个很深的感悟, ...

  10. 就业感言:纸上得来终觉浅,绝知此事要躬行

    来源:华清远见嵌入式学院就业部   学员姓名:曲仕辉   所在班级:北京1001期班   就业单位:朝歌宽带   工作职位:应用开发工程师 转眼间在华清远见嵌入式培训结束了,我也找到了一份比较满意的工 ...

最新文章

  1. 小程序全局状态管理,在页面中获取globalData和使用globalSetData
  2. Ubuntu 对比 CentOS 后该如何选择?
  3. 【有图有真相】静态NAT、动态NAT、PAT、端口映射的详细配置过程
  4. Leader/Follower多线程网络模型介绍
  5. Activity(活动)之Intent(意图)(显式与隐式)的使用
  6. python保存文件到指定文件夹_python实现指定文件夹下的指定文件移动到指定位置...
  7. PostgreSQL 9.6 IO Hang问题浅析与优化
  8. 重新一步一步学习Lucene.NET 一个简单的程序开始(1)
  9. Linux设备模型(4)_sysfs
  10. 新来的妹纸问我,如果把几百万数据放入内存,会不会把系统撑爆?
  11. 状态机finite-state machine学习笔记2——按键消抖初步(1)
  12. 新兴视频处理工具VapourSynth压制教程
  13. 经典信息图表:2013 扁平设计 VS 拟物设计
  14. 天下难事始于易,天下大事始于细。
  15. 深度丨边缘计算,星火燎原
  16. linux网络接口是什么,网络接口是什么?What Is A Network Interface?--用Enki学Linux系列(1)...
  17. L1-002打印沙漏C语言,沙漏
  18. 通过面试思考平时的学习
  19. libvirt零知识学习4 —— libvirt源码编译安装(2)
  20. kubernetes健康检查配置解析

热门文章

  1. tronclass计算机答案,TronClass电脑版
  2. 3.美国哲学集大成者:约翰.杜威
  3. 激光SLAM面试合集
  4. 推荐几个机器学习和数据挖掘领域相关的中国大牛
  5. #Java# 关于“+”加号的详解
  6. zookeeper 学习笔记 (C语言版本)
  7. Python编码encode()与解码decode()介绍与示例演示
  8. 2021-05-24,2020版PS抹除海报文字方法
  9. 2020 PostgreSQL亚洲大会-PostgreSQL授权培训机构公益专场精彩回顾
  10. 什么是 MuleSoft 和 Anypoint 平台功能和优势