在本次的博客当中我们来进行讲解栈和队列相关的内容。首先要认识一下栈和队列的使用方法。
栈和队列的使用
我们可以看到相比于我们之前使用的list,vector还有string类来说栈和队列就简单了很多,没有太多复杂的接口。
因为对于栈和队列来说输入和输出都有限制,由于我们只能在特定的位置进行插入数据以及输出数据,那么就代表我们不能拥有其他的数据插入接口和数据输出接口以及数据打印接口。这就造成了我们的栈和队列不需要有迭代器等复杂的结构的特点。对于这么简单的数据结构我们就不进行过多的介绍了,直接在实现其底层逻辑的时候顺便了解其使用的方法。
stack类的实现
首先观察stack的构造函数可以发现:我们有了一个新的内容container。这个变量实际上是我们的模板参数所添加的。
我们的stack的模板参数一共有两个,第一个模板参数是stack栈当中存放的数据类型,第二个模板参数是我们的container容器。也就是使用什么样的容器实现我们的栈,因为实现栈的方式有很多种,想象一下我们既可以使用vector实现一个顺序栈也可以使用list实现一个链式栈。
我们需要创建一个container的成员变量,我们这个成员变量可以进行实例化成为各种类型,例如vector<int>或者list<int>等。对于stack当中的函数功能我们直接调用我们container所对应的容器当中已经实现的内置函数即可。需要注意的是:我们在返回栈顶元素的时候不能够使用 [ ] 直接找到链表当中最后一个元素的位置,因为我们的list容器并不支持该功能,如果使用 [ ] list容器就无法进行适配操作了。
代码如上图所示,我们来进行测试我们的代码是否可以正常运行。
代码运行一切正常。
queue类的实现
对于队列的实现逻辑上跟我们的栈很相似,我们只需要对插入数据以及删除数据等接口进行简单的修改即可。
首先我们需要修改返回某位置元素的函数,将top函数修改成为front函数。修改我们删除数据的位置,需要从队列的头部进行删除数据。其他函数照旧。程序运行效果如下:
代码运行一切正常。
deque函数的介绍
但是相信大家肯定会有一个疑惑:我记得我们在使用stack栈的时候不需要传两个参数呀?我们只需要传入一个参数不久可以了吗?我们仔细观察一下stack和queue的模板参数就会发现:在系统库实现的过程当中我们设置了一个默认的模板参数deque。
这个默认参数的实现就使得我们不需要手动传入参数就可以正常使用stack。我们同样可以设置一个默认的模板参数。
难道这个默认的模板参数只能选择deque吗?我们选择其他的数据容器行不行呢?我们测试一下会发现是完全可以的。
对于我们的代码即使使用的是list容器作为默认参数也完全可以胜任stack的所有功能的。那么我们为什么要选择deque作为默认的模板参数呢?
我们来仔细阅读以下deque的官方文档。
我们会发现一个很神奇的现象,那就是我们的deque容器很神奇,就好像是vector和list的融合体一样,既可以使用 [ ] 进行访问,又可以使用push_front函数进行头插操作。就好像没有限制一样。难道就没有效率上的负担吗?
我们会发现deque类是使用一个支持从两侧插入数据以及从两侧输出的queue实现的。其真实的底层逻辑结构实际上是一个分段式的小型数组。
结构类似于上图。我们通过一个指针数组将这些小型的数组链接起来构成了一个新的容器也就是deque。
这样有什么优势呢?我们会发现这样的结构让我们兼备了vector数组以及链表的优点,如果我们想头插数据的时候就可以直接在最前面创建一个数组,将指针保存进入我们的指针数组当中即可,不需要我们重新移动数据。尾插操作相同。
并且由于我们同样采用数组的形式也可以很快的计算出我们某个元素所在的位置。相比于我们的list链表也支持了随机访问 [ ]。
但是deque真的有这么方便吗?其实不然,我们仔细分析以下就会发现:即使deque支持了 [ ] 随机访问效率也很低,我们需要进行大量的计算,首先我们需要减去第一个数组当中的元素,因为我们进行头插操作的数组存储数据可能不满,之后再使用剩下的数据对小数组进行除法,求出我们元素应该位于哪一个数组当中,最后求余找到我们数据的具体的位置。相对于我们的vector容器来说 [ ] 的执行效率大大降低。
相比于list虽然头插跟头删,尾插尾删的效率很高,但是对于其他位置的数据呢?对于其他位置的数据的插入和删除操作来说,如果我们同样重新申请了一个数组数组当中的数据存储的位置就进行了偏差,那么我们的 [ ] 操作的效率就会有更大的降低,因为我们计算数据的位置的时候使用了除法取余操作,默认的就是中间的数据都是连续进行存储的。所以对于中间数据的插入操作来说同样需要花费大量的时间进行移动修改操作。
深究底层逻辑我们会发现,deque在使用的时候很方便,但是做不到完全的替代vector或者list进行使用,其特有的工作形式并不适合我们日常的数据存储形式。但是这种存储形式却适合我们实现双端队列,因为如果我们仅仅进行头插头删尾插尾删操作效率就会很高。
作为双端队列的容器,相对于vector来说支持头插头删操作,相对于list来说我们不需要插入一个数据就进行扩容一次,因为我们每一次开辟都会开辟一个小数组,插入到头部之后当数组满的时候才会进行下一次的开辟。还有就是CPU的访存效率也会得到提高,因为CPU在读取数据的时候会直接将连续的一段数据读取到内存当中,减少访存次数得到效率的提升。
综上所述我们的双端队列作为我们需要进行多次头尾操作的底层容器来说再适合不过了,这也就是为什么在系统库当中stack和queue的默认模板参数式deque的原因。