深入浅出数据结构C语言版(7)——特殊的表:队列与栈
从(4)到(6),我们分别讨论了什么是线性表、什么是链表、为什么用链表以及如何用数组模拟链表(游标数组),而现在,我们要进入到对线性表的最终讨论,就是两种最有名的线性表:队列和栈。
在(4)中,我们说过,其实我们所说的“线性表”用“队列”来形容会更易懂,但“队列”之名被一个更特殊的“线性表”给占用了,所以我们用“线性表”来形容一维排列起来的元素集合(如A1,A2,A3,……An)。那么,队列究竟特殊在何处以至于我们将“队列”之名赋予了它呢?回顾我们之前的文章,可以发现,对于普通的线性表,我们总是“想怎么来就怎么来”:我们可以在任一位置插入、删除,我们处理表中任一位置的元素。但是,当我们回过头来看看现实中的队列,你会发现并不是“想怎么来就怎么来”的,比如排队结账,总是以先到先结账的方式一个一个处理,而后到的则排末尾。所以我们之前所说的“线性表”和我们日常生活中的“队列”还是有蛮大区别的。那么我们现在要讨论的“队列”和“表”相比有什么不同之处以至于它可以用“队列”来称呼呢?答案很简单,那就是它(队列)也符合“先到先处理(出),后到排末尾”的逻辑
想要理解队列是非常简单的,因为它的性质和我们日常生活中的队列是差不多的,线性、先到先处理(出)、后到排末尾。实现队列也是非常简单的,首先我们确定了队列是一种线性表,所以我们可以选择用数组、链表或游标数组来存储它,然后我们要限制可以对队列进行的操作,其实也就是限制Insert()和Delete()。根据队列的性质“先到先处理,后到排末尾”,很容易发现,我们只要令Insert()只能插到表尾,Delete()只能删除表头就行。不过为了更贴切,在队列中我们将使用名称Enqueue()和Dequeue()来代替Insert()和Delete(),并且我们令Dequeue()删除表头同时返回表头的值。
可能有初学的读者会疑惑:我们为什么要特意实现队列?因为根据我们之前定义的线性表,完全可以达到实现队列的要求,比如Insert()时我们令N=-1(代表插入到表尾),Delete()时我们令N=1就好。其实我们特意实现队列而不是直接用通用的线性表的原因有两个,首先是我们不希望使用者有意或无意的打破“先到先处理,后到排末尾”的规矩,其次是我们如果使用Enqueue()和Dequeue()则我们不需要再给出N,并且在确定数据结构为队列时,Enqueue()和Dequeue()的代码量会比Insert()和Delete()更少,因为我们不需要再考虑插入位置、删除位置。
也可能有读者会疑惑另一个问题:队列用于何处?在回答这个问题之前,让我们先回顾一下数据结构是什么,按照我们在(1)中的说法,数据结构就是研究如何存储数据的,但现在我们讨论的队列显然超出了这个范围。其实队列就是一种线性表,所以队列从数据结构的角度来说,它就是一个线性的元素集合也即线性表,而我们现在在讨论的,可以说是算法的范畴,我们讨论的是对于线性元素集合的特殊算法,这些特殊算法加上线性元素集合组成了我们所说的“队列”。因此,当我们问“队列用于何处”时,其实是问“什么时候要用队列这样的算法(思想)”?
对于这个问题,如果泛泛而谈,就是所有需要“先到先处理(出),后到排末尾”的情景都需要使用队列。如果举例来说,那么CPU执行命令时就会运用到队列思想,比如CPU准备执行如下命令:
显然这些命令组成了一个“命令队列”,CPU应该按照“先到先处理,后到排末尾”的方式来处理命令队列中的元素,否则就会出错。这就是符合队列需要的情景。更常见的一个例子是当我们输出字符串时,例如char s[]="Hello World!";printf("%s",s); 其中printf()就显然是使用了队列的算法(思想)。同时,这两个例子也给我们解释了为什么我们将Dequeue()设定为将当前表头元素返回的原因:因为我们要用它。比如CPU命令队列,我们删除表头表示该命令被CPU取走了,既然取走了,当然得“返回”给调用方来执行。
说到这儿,想必队列的理解与实现都已经讲清楚了,虽然我们没有写出队列的示例代码,但真正理解了队列思想后,写一个队列也就是分分钟的事情。接下来我们要说一说可能更有趣的另一个表——栈。
栈也叫做堆栈(但不是堆,堆是另一种数据结构,我们以后会说明),它的名字起源我们不得而知,但它的特性却比较有趣,可以说恰好是和队列“对着干”。队列是“先进先出”,而栈则是“先进后出”,或者说“先到进栈底,后到压上头”。你可以把栈想象成一个箱子,你先放进箱子里的东西显然会被后放进去的东西给压住,而当你拿东西时显然得先把后放进去的东西拿出来(因为它们在上面)。
(栈顶“标记”着栈中“最上面”元素的“上一个位置”,栈底则“标记”着栈的“最底部位置”,栈底是不变的,栈顶随着入栈的元素数量变化而变化)
我们已经知道,栈也是一种线性表,因此类似于我们之前讨论队列时的做法,我们只要将表的Insert()和Delete()“改装”一下,使它们都只能“插入到表尾”以及“删除表尾元素”即可实现栈。同样的,为了更贴切,我们将函数的名字分别改为Push()和Pop()。可能pop和push会让人觉得有点奇怪,为什么要用这两个词?原因就是栈与其说像个箱子,不如说像个弹匣更贴切。玩过玩具手枪的人应该都记得弹匣中是有弹簧和卡子的,当我们将一颗子弹“入栈”时,需要push,而当一颗子弹“出栈”时,显然就是pop出来的了。
根据我们讨论队列时的经验,想必理解和实现一个栈都不会是太难的事情,所以真正“有趣”的事情应该是栈能够做什么。
在我们讲解栈的“有趣”应用之前,我们先来说说比较无趣的一种栈的应用,那就是处理函数调用顺序。现在我们假设有这么一个函数:
int func (int N )
{
if(N<)
return -;
if(N==)
return ;
else
return N*func(N-);
}
我们都知道,当我们调用func(5)时,func(5)会调用func(4),func(4)又会调用func(3)……而真正的执行顺序则是反过来,我们先计算出了func(1),然后根据func(1)计算出了func(2),然后根据func(2)计算出func(3)……我们执行函数的顺序恰好是“反过来”的。而这,会带来一个问题,那就是当我们执行完func(x)后,我们应该返回然后继续执行func(x+1),即调用了func(x)的那个函数(此处我们知道是func(x+1),但如果更复杂更不具有规律的多层次函数调用呢?),但是我们该如何确定我们在函数返回后继续执行哪个函数呢?毕竟有很多个函数在等待着继续执行。
这个时候,就需要用到栈了。假设我们设计了一个栈,栈中的元素保存的是函数的相关信息(哪个函数,参数是什么,执行到哪了等等)。那么,对于上述多层次函数调用,我们就可以通过这个栈来确定执行完某个函数后应该继续执行哪个函数。
首先,我们执行main函数,执行过程中我们执行到了调用func(3)(为了更容易完整写出整个过程,我们假设不是调用func(5)而是调用func(3))的地方,因为我们现在要跳去执行func(3)了,所以我们把main函数相关信息保存为一个“元素”并Push()入栈:
然后func(3)中执行到了调用func(2)的地方,于是我们保存func(3)信息,Push(),然后执行func(2)
接着我们在func(2)中执行到了调用了func(1)的地方,于是我们保存func(2)的信息,Push(),然后执行func(1):
接着,我们执行func(1)并执行完毕,返回,于是我们检查栈,看其中是否还有未执行完毕的函数,如果有我们则执行它。显然地,我们检查到栈内有未执行完的函数,于是我们Pop()弹出栈顶的那个函数func(2),执行它。可以发现,这样的执行顺序是对的!它符合我们调用函数的顺序、逻辑关系!
执行func(2)没有遇到其它调用(如果遇到,则又把func(2)入栈,执行那个函数,其它函数调用也以此类推),因此我们执行完了func(2),检查栈,发现还有未执行完的函数,于是Pop()出栈顶的函数func(3)继续执行:
func(3)执行完后我们Pop()出main函数
当main函数执行完后,栈内已没有函数,所以程序执行完毕!
无趣但真实存在的栈的应用说完了,接下来我们来说说“有趣”的应用。之所以说“有趣”,是因为当你仔细看完学完后,可以在命令行界面实现一个四则运算计算器程序!
可能有些初学者乍一听会觉得实现一个计算器是很容易的事情,但是命令行界面的特殊性导致了它并不是那么容易的事情(如果是win32编程,可能会稍微容易一点,但此处不谈)。当说要实现计算器时,初学者脑海很容易浮现出:使用者键入“2+3=”,然后程序用scanf()接收一个int一个char一个int(也可能有更好一些的初学者会想着用float、double),然后判断char是+、-、*还是/,然后计算、输出的场景。但是,在此提醒一下初学者,如果使用者键入的是类似“2+(3*4)-12*(-1)=”的东西呢?或者使用者键入了非法输入比如“2++3=”呢?要知道,使用者键入的其实是“一个个字符”(这一点很关键,当你真正准备实现你的计算器程序时,请牢记使用者输入的应该看做“一个个字符”),scanf()中的占位符,比如%d等,表示的是我们希望怎样理解这些“字符”,但当用户输入的“字符”并没有绝对的规律、顺序时,不能指望scanf()。所以实现一个计算器并不是那么容易的事情。
不过为了关注于我们实现计算器的核心部分,或者说关注于栈的应用部分。我们暂且将如何获取输入以及如何判断输入的问题放一边,我们假定我们已经正确处理好了输入并将整个表达式存入了如下数组中(相关解释已在代码注释中):
#include <stdio.h> #define SIZE 50 struct elem {
int num = ; //当元素为操作数时,num保存操作数的值
char oper = '='; //当元素为操作符时,oper保存操作符的字符,如'+''-''*''/'
bool IsNum = false; //用来表示当前元素是否为操作数
};
typedef struct elem Elem; Elem Expression[SIZE]; //存放表达式的数组,根据元素的成员IsNum来判断元素是操作数还是操作符 //main()中的代码为示例,展示Expression[]的尝试性操作
int main()
{
Expression[].num = ;Expression[].IsNum = true;
Expression[].oper = '+';
Expression[].num = ;Expression[].IsNum = true; //遍历数组每个元素
for (int i = ;i < SIZE;++i)
{
if (Expression[i].IsNum) //若当前元素IsNum,则输出其成员num
printf("%u", Expression[i].num);
else
{
if (Expression[i].oper == '=') //若当前元素为'=',则表示表达式结束,输出'='并跳出循环
{
printf("%c\n", Expression[i].oper);
break;
}
else //若当前元素不是数字也不是'='则输出元素的成员oper
printf("%c", Expression[i].oper);
}
} return ;
}
这样一来,我们就可以较为简单地处理(输出)表达式。那么,我们现在已经有了表达式了(假设有了),下一步该如何计算它呢?由于篇幅问题,这一点我们这篇文章暂且不谈,只提示处理表达式需要用到栈,具体的方法或者说算法、原理,我们将在下一篇博文中讲解。
深入浅出数据结构C语言版(7)——特殊的表:队列与栈的更多相关文章
- 深入浅出数据结构C语言版(8)——后缀表达式、栈与四则运算计算器
在深入浅出数据结构(7)的末尾,我们提到了栈可以用于实现计算器,并且我们给出了存储表达式的数据结构(结构体及该结构体组成的数组),如下: //SIZE用于多个场合,如栈的大小.表达式数组的大小 #de ...
- 深入浅出数据结构C语言版(4)——表与链表
在我们谈论本文具体内容之前,我们首先要说明一些事情.在现实生活中我们所说的"表"往往是二维的,比如课程表,就有行和列,成绩表也是有行和列.但是在数据结构,或者说我们本文讨论的范围内 ...
- 深入浅出数据结构C语言版(5)——链表的操作
上一次我们从什么是表一直讲到了链表该怎么实现的想法上:http://www.cnblogs.com/mm93/p/6574912.html 而这一次我们就要实现所说的承诺,即实现链表应有的操作(至于游 ...
- 深入浅出数据结构C语言版(3)——递归简论
相信学习过C语言的读者都已经接触过递归(不论是谭浩强的C程序设计还是C Primer Plus都有递归程序),本文就是对递归的基本原则进行简要介绍.首先,我们写一个基本的递归函数作为例子: int ...
- 深入浅出数据结构C语言版(1)——什么是数据结构及算法
在很多数据结构相关的书籍,尤其是中文书籍中,常常把数据结构与算法"混合"起来讲,导致很多人初学时对于"数据结构"这个词的意思把握不准,从而降低了学习兴趣和学习信 ...
- 深入浅出数据结构C语言版(6)——游标数组及其实现
在前两次博文中,我们由表讲到数组,然后又由数组的缺陷提出了指针式链表(即http://www.cnblogs.com/mm93/p/6576765.html中讲解的带有next指针的链表).但是指针式 ...
- 深入浅出数据结构C语言版(10)——树的简介
到目前为止,我们一直在谈论的数据结构都是"线性结构",不论是普通链表.栈还是队列,其中的每个元素(除了第一个和最后一个)都只有一个前驱(排在前面的元素)和一个后继(排在后面的元素) ...
- 深入浅出数据结构C语言版(12)——从二分查找到二叉树
在很多有关数据结构和算法的书籍或文章中,作者往往是介绍完了什么是树后就直入主题的谈什么是二叉树balabala的.但我今天决定不按这个套路来.我个人觉得,一个东西或者说一种技术存在总该有一定的道理,不 ...
- 深入浅出数据结构C语言版(12)——平衡二叉查找树之AVL树
在上一篇博文中我们提到了,如果对普通二叉查找树进行随机的插入.删除,很可能导致树的严重不平衡 所以这一次,我们就来介绍一种最老的.可以实现左右子树"平衡效果"的树(或者说算法),即 ...
随机推荐
- MINA、Netty、Twisted一起学(十一):SSL/TLS
什么是SSL/TLS 不使用SSL/TLS的网络通信,一般都是明文传输,网络传输内容在传输过程中很容易被窃听甚至篡改,非常不安全.SSL/TLS协议就是为了解决这些安全问题而设计的.SSL/TLS协议 ...
- java.lang.ClassNotFoundException: org.apache.commons.logging.Log
严重: A child container failed during startjava.util.concurrent.ExecutionException: org.apache.catalin ...
- Spring Data JPA: 实现自定义Repository
一.前言 由于项目中的 实体(entity)默认都是继承一个父类(包含一些公共的属性,比如创建时间,修改时间,是否删除,主键id).为了实现逻辑删除,一般会自己实现RepositoryFactoryB ...
- 【翻译】LPeg编程指南
原文:http://www.inf.puc-rio.br/~roberto/lpeg/lpeg.html 译者序: 这个是官方的LPeg的文档.这段时间学习LPeg的时候发现国内关于LPeg的文章 ...
- <c>----<choose><when><otherwise>
<c:choose> <c:when test="${username== '1' && password== '2'}"> <jsp ...
- JS获取网站StatusCode,若存在写入文件
JS获取网站状态码,若网站存在,写入TXT文件,适用于IE. <script> //写文件 function writeFile(filename,filecontent){ ...
- iOS10构建版本不显示的问题
iOS10,构建版本问题: 在Xcode中->product->archive,进行相关操作后,upload后没有报错验证也成功的情况下,在开发者账号构建版本号里面迟迟没有显示的原因: i ...
- 列表总结Canvas和SVG的区别
参考链接: 菜鸟教程 HTML5 内联SVG 经典面试题(讨论canvas与svg的区别) Canvas | SVG ---|--- 通过 JavaScript 来绘制 2D 图形|是一种使用 XML ...
- 全新的 flow.ci Dashboard UI 上线
全新的 flow.ci Dashboard 页面上线了,更快捷地创建项目,构建列表页面新增分支,Pull Request 界面:侧边栏新增构建任务监控和项目监控,整个 Dashboard 界面焕然一新 ...
- java学习(一)静态代码块 构造代码块 构造方法的执行顺序及注意问题
今天我总结了一下java中静态代码块 构造代码块 构造方法的执行顺序及其注意问题 首先要知道静态代码块是随着类的加载而加载,而构造代码块和构造方法都是随着对象的创建而加载 当时做了这么一个小案例(想必 ...