c++ 队列相关知识总结
预备
队列,是一种先进先出(FIFO)的线性表,一般来说会使用链表或者数组来实现它。
队列被允许从后端(rear)插入(insert)新元素,称作入列(push,enqueue);而从前端(front)弹出(pop)元素,称作出列(pop,dequeue)。
学术上说它和堆栈常常被同时提起,因为堆栈与队列几乎一摸一样,除了出栈时也在后端弹出元素,从而构成了后进先出(LIFO)的数据结构。
古典的单向链表/单向队列
单向链表表示的队列,出列时必须遍历整个链表,直至链尾的前一位,然后摘除链尾来实现出列操作。
毫无疑问,这一定是代价很高的。
但这种方案的优势在于任何时候任何人都能够随手写得出来。也就是说,它是最具备可手撸性的一种实现方案,虽然出列代价较高,但对于相当多的场景来说那点代价CPU根本不当作是事。
双向链表/双向队列
双向链表的好处是在链表头尾的操作都是O(1)的,代价极低,可以视作毫无代价。但这是用额外的两个指针的空间代价来达成的。对于像std::deque
双向链表结构的手写,难点在于指针的同步修改,正确指向。
对于高频交易场景来说,双向链表的指针维护操作是导致锁开销的典型瓶颈,所以需要下力气研究和解决锁问题。
一般来说在现实世界中大家都是借助于STL来实现队列算法及其应用的。典型的例子是通过deque:
#include#include intmain() { //创建容纳整数的deque std::deque d{}; //约定push_back为入列操作 d.push_back(13); d.push_back(25); d.push_back(7); d.push_back(2); //约定pop_front为出列操作 autotmp=d.pop_front(); std::cout<<"FRONT:"< 得益于deque的强健的基础能力,我们实际上可以得到一个工程强度的有效的队列模板类。
同时,deque也可以被毫无代价地实现强健的Stack数据结构,仅仅是将出栈操作从上文的pop_front改为pop_back()即可。
所以,deque的强大可见一斑。
不过,无论如何,我们有必要手写一份来完成自己的理解。在下面我们简单地实现了一份理论上的典型队列方案:
templateclassti_queue{ public: structelement{ T_data; element*_last; element*_next; }; public: ti_queue(){ _head=newelement; _tail=newelement; _head->_next=_tail; _tail->_last=_head; } ~ti_queue(){ auto*p=_head; while(p!=nullptr){ auto*t=p; p=p->_next; deletet; } } voidpush(Tconst&data){ autoh=_tail->_last; auto*ptr=newelement{data,h,_tail}; _tail->_last=ptr; h->_next=ptr; } Tpop(){ autop=_head->_next; if(p==_tail){ returnT{}; } p->_last->_next=p->_next; p->_next->_last=p->_last; Tt=p->_data; deletep; returnt; } boolempty()const{return_tail->_last==_head;} Tconst&peek()const{ if(empty())return_tail->_data; return_tail->_last->_data; } private: element*_head{}; element*_tail{}; }; 这是一份不采用智能指针的实现。因为智能指针在这里只有坏处没有好处。然而这就会使得这份实现显得低级,很有趣吧,现代C++在很多时候其实是扭曲了的,它虽然很现代,很时髦,但也很脱了裤子放屁。
在这份实现中有一个重要的约定:我们认为你在提供T的具现类型时,你会隐含地约束零值初始化的T示例代表着空元素。所以当你pop/peek得到一个T实例时,如果它是零值将代表着队列为空。
所以我们的测试片段为:
intmain(){ std::cout<<'\n'<<"queue:"; bet::ti_queuetiq; tiq.push(31); tiq.push(17); tiq.push(19); tiq.push(73); autotmp=tiq.pop(); std::cout< 其结果应该形如这样:
queue:31,17,19,73OK,继续我们的思考线路:
检测队列有否为空很重要,你不可能无代价地避免空访问,因为这是数据结构本身的特性决定的。而在具体实现方面,上面之所以要做出那样的约定,是因为我们没有使用指针方案来存储T,所以你需要检查pop/peek得到的T实例是否为空元素。或者,你在pop/peek()之前首先采用empty()进行队列非空判断。
如果采用指针存储方案的话,你可以顺理成章地检查pop/peek返回值是否nullptr,这其实是C/C++98时代的典型实现方案。
如果是在C++11之后,尽管我们的实现有上述约定,但你也可以通过给T实例化为std::shared_ptr
的方式来建立一个智能指针的缓冲层,因此你可以判断该指针的非空性来鉴别返回值的非空性。 借助于共享指针的强力特性,很多时候我们无需在做容器实现时考虑太多的元素如何存储的问题,因为模板的一个重要能力就是透明地引入中间层来提供额外的供给能力。这种时候,在这个例子里,中间层的智能指针包装实际上提供了一种装饰器的设计模式运用机制。
所以,
我们认为,在容器等数据结构等实现部分,如果有可能,你应该负起责来,自行处理C++/C指针的各种运算,保证你的实现边界之内没有泄漏问题。这样做的好处在于你的实现将会非常可读可维护,因为它通常能够和理论上的数据结构的实现近似于等价,如同我们上面的实现一样。
然而,如果你的目标是在面向两个目标时则例外了:第一个目标是工程性的高性能线程安全性要求,此时应该加上的锁、优化则取决于具体实现;第二个目标是用户的使用简便性,很显然让用户直接具现他们的T而不是shared_ptr
是绝对有助于用户的使用简便性的,那么你就可能要在精打细算和借助智能指针方面做个选择了。 环形队列
此前,我其实在Golang针对高频交易提出了一种无锁环形队列的实现方案,在那里使用了一种程序员直觉化的代码编写方法及直觉化的算法设计思路。事实上我个人偏好于那种直觉化的算法设计思路,我讨厌搞委托交易,像Springframework这样的框架,在做无论什么的集成的时候如同脱了裤子放屁一般的老奶奶裹脚布似的做法,我鄙视之,所以我现在根本没兴趣讨论Java的任何事情,这是被我判定了不值得的一种技术体系。
不过,RxJava的思路是符合我的审美观的。这才是对世界做抽象的正确态度。
那种顽固地拿着XML搞出一个庞大的shit堆的方法,我只能是表示敬而远之。不但是Java的多数玩意如此地垃圾,.NET画界面XAML,macOSXCode画界面的Storyboard统统都是毫无疑问的垃圾。任何和XML紧密捆绑的东西,没说的,都是在创造垃圾。
那是一种将任何事情复杂化的思维模式,我拒绝的正是如此这般明显愚蠢的模式。
所以我这样的人,一边不爽Golang的简陋,一边不也还是投入其怀抱了吗——就因为它能够极低代价的ELF化啊。程序员要干的事情就是简化这个世界。创造垃圾这样的事是干不得的。
所以,环形队列在理论研究层面看,实际上就是一个数组,它是固定大小的,而后我们将其首尾相接(通过put-pointer和get-pointer自动回绕的方式),形成了抽象层面上的环形状态。它的特点在于,当排除了分配每个元素的代价之后,它可以通过自己的固定缓冲区的特点来去除对象入列和出列时的潜在可能的复制开销。
关于这一点,即使是充分利用C++11甚至C++17的现代语法,在借用STL来实现队列的时候,你可能也往往无法避免出列时的复制开销。如果考虑到高频交易场景下想要避免锁代价的影响的话,往往你可能连入列时的额外开销也无法避免——只不过,很多时候锁的代价更高过内存分配已经内存复制的总线级代价,所以我们不得不两害相权取其轻罢了。
为了解决这种特定的问题,一种方案是通过环形队列来避免内存分配问题;另一种则是自行构造自己的队列模板,通过专门的设计和实现来取得内存分配、内存复制与读写访问、CPUCache等层面的锁的问题等等的平衡。
再有一种方案,那就简单了:用指针吧。鉴于C++11之后用裸指针被认为是跟不上时代的表现,所以:用智能指针吧。借助智能指针的移动语义或共享计数值技术,通常可以较低代价地避免内存反复频繁分配以及内存复制带来的额外开销问题。这种方案是有一点投机取巧,但是语言的基础设施本就应该提供给程序员以这样的能力。反过来思考,如果一个程序员根本经受不了C指针的蹂躏的话,他也不配被称作程序员。话说今天这话也很没意思,阿猫阿狗都是程序员的——劣币正在驱逐良币。
应用
由于环形队列是固定大小的,所以它极其适合这样的场景:数据采样工具,或者遥测场景。在数据采样的场景,常常需要的是最最近几十条记录进行数学分析。当使用环形队列来做相应的数据缓冲时,老的数据不断随着插入指针的推进而被覆盖掉,所以在取出指针一侧总是最新的N条记录,N是环形队列的预设大小。于是分析器可以面对这个环形队列顺利地提取到最新N条记录用于计算和分析。
除此而外,我们还可以让环形队列的插入行为是非覆盖的,于是当队列满时,插入操作就会在队尾被阻塞,这是另一种环形队列的应用方式。
下几期我将可能会考虑实现一份C++版本的环形队列,但这次可能不会做高频版了,大约只会做适合毫秒级例如典型工业TCP交易的版本。
Conclusion
队列这种数据结构,总体上来说非常简单,也不会牵涉到高深的数学证明,所以实现一个队列往往会是初学者上手编程的好题目。
以上就是c++队列相关知识总结的详细内容,更多关于c++队列的资料请关注毛票票其它相关文章!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。