STL设计模式探秘:容器适配器仿函数
目录
一、前言
二、容器适配器
1、类型
2、stack
(1)实现原理
(2)接口实现
(3)测试
3、queue
(1)实现原理
(2)接口实现
(3)测试
4、deque
(1)结构特点
(2)迭代器
5、priority_queue
(1)接口实现
(2)向上调整建堆
(3)向下调整建堆
(4)push
(5)pop
(6)测试
(7)TopK
三、仿函数
1、定义
2、应用
四、结语
一、前言
当了解和掌握STL各种容器的相关接口和用法时,就可以尝试模拟实现STL容器的结构了,例如当模拟实现string、vector容器结构时,可利用string、vector底层内存地址连续的特点来模拟实现,通过原生指针、动态内存开辟等方法来实现string、vector的容器结构,list的实现与string、vector有所不同,由于list以带头双向循环链表为结构基础,各结点之间地址并不是连续的,无法直接通过原生指针来实现迭代器,需要对++、--、*、->等操作符进行重载来实现对迭代器的封装,进而实现list容器结构,实现过程也较为复杂。除了通过以上"造轮子"的方法来实现容器结构,STL还引入了容器适配器的概念,容器适配器并不是一种特定的容器,而是通过对已有的容器如vector、list进行限制和封装,以此来适配实现其他容器,如栈、队列、优先级队列等结构,即通过已有容器来适配实现其他容器,体现了转化的思想,也是一种高效的方法,在STL中也有广泛的应用,本文将围绕容器适配器适配原理、以及如何实现其他容器结构具体展开分析。
二、容器适配器
1、类型
STL中stack、queue、priority_queue等结构都是通过已有容器结构来适配实现的,由于stack、queue、priority_queue都是基于对其他容器的接口进行了适配和封装来实现,故stack、queue、priority_queue均为容器适配器,下面将围绕这3种容器适配器的实现原理、相关应用展开介绍。
2、stack
(1)实现原理
stack是一种很常见的数据结构了,基于LIFO的设计模式,栈只允许在栈的一端入数据和出数据,称为栈顶,数据出栈顺序为后进先出,在STL中通过容器适配器模式来实现,即通过对其他容器的接口进行了适配和封装,以此来实现stack结构。
namespace Kzy
{template<class K, class container>class stack{private:container _con;};
}
stack实现为模板类型,第1个模板参数为栈存放的数据类型,第2个模板参数为实现栈结构的容器类型,stack的成员变量即为该container容器类型变量_con,通过该容器的相关接口来适配实现栈的相关接口,以此来实现栈。
实现文件包括:stack.h负责栈相关接口的声明和实现,test.cpp对实现接口进行相关的测试。
(2)接口实现
namespace Kzy
{template<class K, class container>class stack{public:void push(const K& x){_con.push_back(x);}void pop(){_con.pop_back();}const K& top() const{ return _con.back();}size_t size() const{return _con.size();}bool empty() const{return _con.empty();}private:container _con;};
}
接下来就可以通过_con容器来适配实现stack的相关接口了,push为入栈接口,将数据压栈,以_con容器的尾部为栈顶,可调用_con的push_back接口完成_con数据的尾插,从而完成数据的入栈,实现push,同理实现出栈pop接口,即可调用_con的pop_back接口完成数据的尾删,从而完成数据的出栈,实现pop,top用于访问栈顶元素,即访问_con的最后一个元素,可通过_con的back接口实现访问,从而实现top,size用于返回栈的元素个数,可通过_con的size来实现,empty用于判断栈是否为空,可通过_con的empty接口来判断实现,这样就通过contain容器类型_con,适配实现了stack,这就是容器适配器stack的实现。
(3)测试
#include<iostream>
#include<vector>
#include<stack>
using namespace std;
#include"stack.h"
int main()
{Kzy::stack<int, vector<int>> st;st.push(1);st.push(2);st.push(3);st.push(4);cout << st.size() << endl;cout << st.empty() << endl;cout << st.top() << endl;st.pop();cout << st.top() << endl;cout << st.size() << endl;cout<<st.empty() << endl;return 0;
}
对stack接口进行测试,Kzy::stack<int,vector<int>> st,由于stack只在一端进行操作,且vector尾插、尾删效率也较高,因此可以采取vector容器适配实现stack,st.push(1),st.push(2),st.push(3),st.push(4),将数据1、2、3、4依次入栈,st的元素个数为4,则st.size()大小为4,st不为空,故st.empty()为0,栈顶数据为4,则st.top()为4,st.pop()取出栈顶数据4,则栈顶数据变为3,st.top()为3,数据个数st.size()变为3,st不为空,st.empty()为0,如(1)所示,结果正确,测试通过。

(1)
3、queue
(1)实现原理
queue也是一种常见的数据结构,数据从队尾插入,队头取出,数据出队列的先后顺序为先进先出,与stack类似,queue在STL中也是以容器适配器的方式实现,实现方式也大体与stack类似。
实现文件:queue.h负责queue相关接口的声明和实现,test.cpp对已实现的接口进行测试。
(2)接口实现
#pragma once
namespace Kzy
{template<class K,class container>class queue{public:void push(const K& x){_con.push_back(x);}void pop(){_con.pop_front();}const K& front() const{return _con.front();}const K& back() const{return _con.back();}size_t size() const{return _con.size();}bool empty() const{return _con.empty();}private:container _con;};
}
queue接口实现与stack大体类似,也是通过container容器类型_con成员变量来适配实现,push完成数据的入队列,即在队尾插入数据,可通过调用_con的push_back实现,pop完成数据的出队列,即在队头删除数据,可通过调用_con的pop_front实现,front用于访问队头数据,可通过调用_con的front实现,back用于访问队尾数据,可通过调用_con的back实现,size用于返回队列的数据个数,可通过调用_con的size实现,empty用于判断当前队列是否为空,可通过调用_con的empty实现,这样就通过_con容器接口适配实现了queue的相关接口。
(3)测试
#include<iostream>
#include<list>
#include<queue>
using namespace std;
#include"queue.h"
int main()
{Kzy::queue<int, list<int>> q;q.push(1);q.push(2);q.push(3);q.push(4);q.push(5);cout << q.front() << endl;cout << q.back() << endl;cout << q.empty() << endl;cout << q.size() << endl;q.pop();cout << q.front() << endl;cout << q.back() << endl;cout << q.empty() << endl;cout << q.size() << endl;return 0;
}
对queue相关接口进行测试,Kzy::queue<int,list<int>> q,由于vector容器没有pop_front接口,且头删需要挪动大量数据,效率不高,因此采取list适配实现queue,q.push(1),q.push(2),q.push(3),q.push(4),q.push(5),将数据1,2,3,4,5依次入队列,则队头数据为1,q.front()为1,队尾数据为5,q.back()为5,queue不为空,q.empty()为0,queue数据个数为5,则q.size()为5,q.pop()取出队头数据1,则队头数据变为2,q.front()为2,队尾元素不变,q.back()为5,队列不为空,q.empty()为0,队列数据个数q.size()变为4,结果如(2)所示,结果正确,测试通过。

(2)
4、deque
上面我们分别借助了vector容器来适配实现stack,以及list容器来适配实现queue,但在STL中实现stack、queue结构默认使用双端队列deque来适配,因此有必要了解一下deque的容器结构。

(1)结构特点

vector的尾插尾删效率高,但头删头插由于需要挪动大量数据,效率较低,且往往需要扩容,list支持任意位置的插入删除,效率较高,但不支持随机访问,deque的结构可以说是弥补了vector和list在结构上的不足,可以说是vector和list的缝合怪,deque两端插入删除数据效率都很高,且也支持随机访问,由于stack和queue都是在一端或者两端插入删除数据,deque这种结构就恰恰满足了这个特点,因此在STL中默认使用deque来适配实现stack和queue。

(3)
deque底层结构上是一段假想的连续空间,实际上并不连续,而是通过一个中控指针数组来实现"连续",如(3)所示,数组中的指针分别指向一个一维数组,通过中控指针数组就可实现对deque的随机访问,deque的扩容也是针对中控指针数组进行的,相比vector扩容没有那么频繁,效率较高。

(4)
如(4)所示,中控指针数组的每个指针分别指向一段buff数组空间,每个buff数组的size是相等的,假设为N,若想获取第i个元素,可先求出i/N,获取到该元素在第x个buff数组中,再对i模N,算出该元素在buff数组的下标y,最后通过[ ]来进行访问,即ptr[x][y],即可访问到第i个元素,实现随机访问。
(2)迭代器
由于deque结构的特殊性,deque的迭代器与其他容器有所不同,也较为复杂,通过了解deque的迭代器可以更好理解deque的结构。

(5)
deque的迭代器iterator由4个部分组成,cur指向当前数据的位置,first指向当前buff数组首元素的位置,last指向当前buff数组最后一个元素的下一个位置,node指向中控指针数组指向当前buff数组的指针,如(5)所示,当遍历完当前buff数组时,node++,更新下一个buff数组,继续遍历,从而实现"连续"结构。

(6)
如(6)所示,deque的迭代器start类似begin,cur和first指向第1个buff数组的首元素,last指向第1个buff数组最后一个元素的下一个位置,node指向中控指针数组的第1个指针,该指针指向的就是第1个buff数组,finish类似end,first指向最后一个buff数组的首元素,cur、last指向队尾,node指向中控指针数组的最后1个指针,该指针指向最后1个buff数组,通过deque的迭代器,搭配中控指针数组就可实现数据的随机访问了,由于deque结构的灵活性,deque两端数据的插入删除效率较高,只需对两端的buff数组进行插入删除操作即可,无需扩容,deque的扩容也是针对中控指针数组的扩容,扩容效率较高,且能实现随机访问数据,这也就是STL采取deque作为容器适配器来实现stack和queue的原因了,实现stack、queue可以将deque作为缺省容器来适配实现其结构,即template<class K,class container=deque<K>>。
stack.h
#pragma once
namespace Kzy
{template<class K, class container=deque<K>>class stack{public:void push(const K& x){_con.push_back(x);}void pop(){_con.pop_back();}const K& top() const{ return _con.back();}size_t size() const{return _con.size();}bool empty() const{return _con.empty();}private:container _con;};
}
queue.h
#pragma once
namespace Kzy
{template<class K,class container=deque<K>>class queue{public:void push(const K& x){_con.push_back(x);}void pop(){_con.pop_front();}const K& front() const{return _con.front();}const K& back() const{return _con.back();}size_t size() const{return _con.size();}bool empty() const{return _con.empty();}private:container _con;};
}
5、priority_queue
priority_queue在STL中称为优先级队列,实际上就是数据结构中的堆,堆在STL中也是以容器适配器的方式实现,堆在逻辑结构上为完全二叉树,物理结构上为一维数组,因此可借助vector适配实现priority_queue。

实现文件:priority_queue.h负责相关接口的声明和实现,test.cpp进行相关接口的测试。
(1)接口实现
#pragma once
#include<vector>
namespace Kzy
{template<class K, class container=vector<K>>class priority_queue{public:const K& top(){return _con[0];}size_t size() const{return _con.size();}bool empty() const{return _con.empty();}private:container _con;};
}
priority_queue与stack、queue接口实现类似,top用于访问堆顶元素,即返回_con[0],size用于返回堆的数据个数,可调用_con的size来访问实现,empty用于判断堆是否为空,可调用_con的empty来判断实现。
(2)向上调整建堆
堆的向上调整算法思想为先将元素插入到堆的最后一个结点之后,再依次比较新结点与双亲结点的大小关系,观察堆的结构是否遭到破坏,如果遭到破坏,则向上调整,交换该结点与双亲结点的值,继续向上调整比较,直到调整到合适位置,保持堆的结构不被破坏。
void adjustup(int child)
{int parent = (child - 1) / 2;while (child > 0){if(_con[parent]<_con[child]){swap(_con[parent], _con[child]);child = parent;parent = (child - 1) / 2;}else{break;}}
}
向上调整算法思想即找到双亲结点,然后比较双亲结点与孩子结点的大小关系,双亲结点下标i与孩子结点下标n的关系为:i=(n-1)/2,若孩子结点大于双亲结点,则向上调整建大堆,直到调整到合适位置,通过while循环,即可实现堆的向上调整算法。
(3)向下调整建堆
向下调整建堆与向上调整类似,向下调整则是通过双亲结点找到孩子结点,比较双亲结点与孩子结点的大小关系,若堆的结构遭到破坏,则向下调整,交换双亲结点与孩子结点的值,继续向下比较调整,直到调整到合适位置,保持堆的结构不被破坏。
void adjustdown(int parent)
{size_t child = 2 * parent + 1;while (child < _con.size()){if (child + 1 < _con.size() && _con[child] < _con[child + 1]){++child;}if (_con[parent] < _con[child]){swap(_con[parent], _con[child]);parent = child;child = 2 * parent + 1;}else{break;}}
}
向下调整是通过双亲结点找到孩子结点,进而比较双亲结点和孩子结点的大小关系,双亲结点下标i与左孩子结点下标n的关系为:n=2i+1,右孩子下标即为2i+2,向下调整建大堆,先通过假设法,找出最大的孩子结点,再与双亲结点进行比较,若孩子结点大于双亲结点,则向下调整,交换双亲结点与孩子结点的值,继续向下调整,直到调整到合适位置,通过while循环即可实现堆的向下调整算法。
(4)push

void push(const K& x)
{_con.push_back(x);adjustup(_con.size() - 1);
}
push实现数据的入堆操作,可先调用_con的push_back将数据尾插到堆中,再调用adjustup进行向上调整,将数据向上调整到合适的位置,保持堆的结构不被破坏,就实现了push。
(5)pop

void pop()
{swap(_con[0], _con[_con.size() - 1]);_con.pop_back();adjustdown(0);
}
pop取出堆顶数据,先调用swap交换堆顶数据与堆的最后一个数据,再调用_con.pop_back()将堆顶数据删除,最后对堆顶数据向下调整,使之调整到合适位置,保持堆的结构不被破坏,就实现了pop。
(6)测试
int main()
{Kzy::priority_queue<int,vector<int>> pq;pq.push(4);pq.push(1);pq.push(5);pq.push(7);pq.push(9);while (!pq.empty()){cout << pq.top() << " ";pq.pop();}cout << endl;return 0;
}
对priority_queue进行测试,Kzy::priority_queue<int,vector<int>> pq,构造优先级队列pq,通过vector适配实现,pq.push(4),pq.push(1),pq.push(5),pq.push(7),pq.push(9),将4,1,5,7,9依次入堆,构造大堆,再通过while循环依次访问堆顶数据,取出堆顶数据,则数据将成降序排列,为9,7,5,4,1,如(7)所示,结果正确,测试通过。

(7)
(7)TopK
利用priority_queue也可求topK问题,即求第K大的数:
class solution
{
public:int find(vector<int>& num, int k){priority_queue<int> pq(num.begin(), num.end());for (int i = 0;i < k - 1;i++){pq.pop();}return pq.top();}
};
先将num的数据拷贝构造给pq,即priority_queue<int> pq(num.begin(),num.end()),再通过for循环取出堆的前k-1个数,则剩下堆顶的数据即为第k大的数据,return pq.top()即可。
三、仿函数
1、定义
在C++中还引入了仿函数的概念,仿函数并不是函数,仿函数本质是一个类,这个类重载了operator(),使得它的对象可以像函数一样使用。
template<class K>
class Less
{
public:bool operator()(const K& x, const K& y){return x < y;}
};
template<class K>
class Greater
{
public:bool operator()(const K& x, const K& y){return x > y;}
};
如上,Less和Greater均为仿函数,Less中重载了operator(),返回x<y,Greater中也重载了operator(),返回x>y。
int main()
{Less<int> lessfunc;Greater<int> greaterfunc;cout << lessfunc(1, 2) << endl;cout << lessfunc.operator()(1, 2) << endl;cout << greaterfunc(3, 4) << endl;cout << greaterfunc.operator()(3, 4) << endl;return 0;
}
仿函数的对象可以像普通函数一样使用,如lessfunc(1,2),greaterfunc(3,4),具体表示为lessfunc.operator()(1,2),greaterfunc.operator()(3,4),两种表示方法都可以,可知结果为1,1,0,0,如(8)所示,结果正确,测试通过。

(8)
2、应用
仿函数在C++中的应用也很广泛,其中一个方面就是排序,C语言中的qsort排序是通过传函数指针来决定排序是升序还是降序,但函数指针使用起来较为麻烦,现在可以通过仿函数来实现排序是升序还是降序,以冒泡排序为例:
template<class compare>
void Bubblesort(int* arr, int n, compare com)
{for (int i = 0;i < n;i++){int flag = 0;for (int j = 1;j < n - i;j++){if (com(arr[j], arr[j-1])){swap(arr[j - 1], arr[j]);flag = 1;}}if (flag == 0){break;}}
}
将冒泡排序实现为模板类型,排序大体逻辑不变,即相邻元素的两两交换,com(arr[j],arr[j-1])对二者进行比较,模板参数com用于决定排序是升序还是降序,可以通过传具体仿函数对象来实现升序或者降序。
int main()
{Less<int> lessfunc;Greater<int> greaterfunc;int a[] = {9,1,2,5,7,4,6,3};Bubblesort(a, 8, lessfunc);for (auto e : a){cout << e << " ";}cout << endl;Bubblesort(a, 8, greaterfunc);for (auto e : a){cout << e << " ";}cout << endl;return 0;
}
例如,Bubblesort(a,8,lessfunc),将进行升序排序,结果为:1,2,3,4,5,6,7,9,Bubblesort(a,8,greaterfunc),将进行降序排序,结果为:9,7,6,5,4,3,2,1。如(9)所示,结果正确,测试通过。

(9)
int main()
{int a[] = { 9,1,2,5,7,4,6,3 };Bubblesort(a, 8, Less<int>());for (auto e : a){cout << e << " ";}cout << endl;Bubblesort(a, 8, Greater<int>());for (auto e : a){cout << e << " ";}cout << endl;return 0;
}
也可传匿名对象,如Bubblesort(a,8,Less<int>()),Bubblesort(a,8,Greater<int>()),也可实现数据的升序、降序排序。
priority_queue的实现也可借助仿函数进一步完善,通过传具体的仿函数来决定priority_queue是大堆、还是小堆,以默认建大堆为例:
#pragma once
#include<vector>
template<class K>
class Less
{
public:bool operator()(const K& x, const K& y){return x < y;}
};
template<class K>
class Greater
{
public:bool operator()(const K& x, const K& y){return x > y;}
};
namespace Kzy
{template<class K, class container=vector<K>,class compare=Less<K>>class priority_queue{public:void adjustup(int child){compare com;int parent = (child - 1) / 2;while (child > 0){//if(_con[parent]<_con[child])if (com(_con[parent],_con[child])){swap(_con[parent], _con[child]);child = parent;parent = (child - 1) / 2;}else{break;}}}void push(const K& x){_con.push_back(x);adjustup(_con.size() - 1);}void adjustdown(int parent){size_t child = 2 * parent + 1;compare com;while (child < _con.size()){//if (child + 1 < _con.size() && _con[child] < _con[child + 1])if(child+1<_con.size() && com(_con[child],_con[child+1])){++child;}//if (_con[parent] < _con[child])if(com(_con[parent],_con[child])){swap(_con[parent], _con[child]);parent = child;child = 2 * parent + 1;}else{break;}}}void pop(){swap(_con[0], _con[_con.size() - 1]);_con.pop_back();adjustdown(0);}const K& top(){return _con[0];}size_t size() const{return _con.size();}bool empty() const{return _con.empty();}private:container _con;};
}
则模板定义为template<class K,class container=vector<K>,class compare=less<K>>,这样adjustup向上调整和adjustdown向下调整都传compare对象com,com(_con[parent],_con[child]),从而实现根据具体com对象决定是大堆还是小堆,默认缺省参数为less<K>,即默认建大堆,这样通过仿函数就可以根据需要来具体实现大堆或者小堆。
四、结语
本文主要围绕STL的容器适配器与仿函数展开介绍,STL的容器适配器有3种,stack、queue、priority_queue,容器适配器是基于已有容器进行适配而实现的结构,在STL中,stack、queue是通过deque来适配实现的,deque双端队列在结构上弥补了vector和list的不足,deque两端数据的插入删除效率都很高,且也支持随机访问,与stack、queue都是在一端、两端插入删除数据的特点一致,因此STL通过deque来适配实现stack、queue,priority_queue为优先级队列,以数据结构中的堆为雏形,堆在逻辑结构上为完全二叉树,物理结构为一维数组,因此STL通过vector适配实现priority_queue,仿函数并不是函数,仿函数本质上是类,这个类重载了operator(),使得仿函数的对象能够像普通函数一样调用,仿函数在C++中也有着广泛的应用,priority_queue的实现可以说是容器适配器与仿函数相结合的范例,以vector为底层容器,less为仿函数,仿函数决定了priority_queue是建大堆还是小堆,简而言之,容器适配器是对现有容器的接口进行了限制和封装,让这些容器更易用,更专注,而仿函数则是为操作这些数据的算法提供了强大的灵魂和灵活性,二者都是构建高效、可复用C++程序不可或缺的基石,也是STL设计模式的核心所在。
