索引
什么是容器适配器
适配器其实可以和生活中的电源适配器相比较,不管是手机还是电脑还是其他电器,充电时都无法直接使用220V的交流电,为了方便使用,各个电器厂都会提供一个适用于自己产品的电源线,可以将220V的交流电转换成电器使用的低压直流电。
从我们用户的角度看,电源线扮演的角色就是将原本不适用的交流电变得使用,因此其又被称为电源适配器
eg:
假设有一个模板A
class A{
public:
void f1(){}
void f2(){}
void f3(){}
void f4(){}
};
这个类A是基础类,假设我们现在要设计一个模板B,但发现,只需要组合模板A中的两个函数f1(),f3()即可
此时便可以这样设计
class B
{
public:
con->f1();
private:
con->f3();
private:
A* _con;
};
而容器适配器就是将不适用的序列式容器包括(vector,deque,和list)变得适用,容器适配器的底层实现和模板A,B完全相同,即通过封装某个序列式容器,并重新组合该容器中包含的成员函数,使其满足某些特定场景的需要。
容器适配器本质上还是容器,只不过此容器模板类的实现,利用了大量其他基础容器模板类中已经写好的成员函数,当然,如果有需要,容器适配器也可以自我添加新的成员函数。
stack/queue/priority_queue
1,stack
stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其只能从容器的一端进行元素的插入与删除操作。(FILO)
容器适配器底层可以是任何标准的容器类模板或者其他一些特定的容器类,只需要实现以下操作即可
2.queue
queue也是一种容器适配器,专门用于在先进先出的操作,从容器一段插入,另一端提取元素
其和stack一样,无论其底层如何实现,只要满足以下操作即可
3,priority_queue
优先队列也是一种容器适配器,他的队顶元素默认总是元素中最大的(与其仿函数实现有关)
其类似于堆,可以随时插入元素,但只能检索最大(或小)堆元素,容器可以通过随机访问迭代器访问,并支持如下操作
通过源代码我们可以发现默认情况下:
stack与queue底层都是deque(双端队列)
但priority_queue默认情况下是vector,但是可以看到其后面还有第三个参数仿函数**(后续详解)**
deque
deque(双端队列):是一种双开口的“连续”空间的数据结构,它允许在其首尾两端快速插入及删除,且时间复杂度为O(1),与vector比较,头插效率较高,并且其不需要搬迁元素,与list相比,空间利用率较高,但其并不是真正连续的空间,否则就没有其他容器什么事了。
deque并不是真正连续的空间,而是由一段连续的小空间拼接而成,实际deque实现类似于一个动态的二维数组,这表示下标访问必须进行二次指针解引用,与之相比vector的下标访问只进行一次。
为了维护其“空间整体连续的假象,这个任务自然落在了deque迭代器身上
由于deque是在两端维护的,所以其迭代器自然也在两端,不深究。
deque的优势
1,与vector比较,头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,效率比vector高
2,与list相比其底层是“连续空间”空间利用率比较高,不需要存储额外字段。
deque缺陷
致命缺陷:不适合遍历,因为在遍历时,deque的迭代器会频繁的去检测是否移动到某段小空间的边界,导致效率低下
如图所示:这一段内容明显是属于中间部分,遍历的时候,迭代器会一直检查指向的空间是否是8—15之间,由于迭代器如上图所示,所以其效率十分低下,所以这是一个致命的缺陷 而在序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,目前暂时可以看到的就是STL用其作为stack和queue的底层数据结构
为什么选择deque作为stack和queue的底层默认容器?
stack是一种后进先出的特殊性线性数据结构,因此只要具有push_back和pop_back()操作的线性结构都可以作为stack的底层容器,比如vector和list都可以。
queue是先进先出的特殊性容器,只要有push_back和pop_front操作的线性结构都可以作为queue的底层容器,比如list。但stack和queue选择deque作为其底层容器是因为:
1,stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作
2.在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中元素增长时,deque不仅仅效率高,而且内存使用率高。
结合了deque的优点,完美的避开了其缺陷
仿函数(priority_queue)
priority_queue的第三个参数传的是一个仿函数
什么是仿函数?
仿函数在c++标准中采用的名称是函数对象,仿函数主要用于STL中的算法中,虽然函数指针也可以作为算法的参数,但是函数指针不能满足STL对抽象性的要求。仿函数的本质是一个类重载了(),创建一个行为类似函数的对象,实际上仿函数对象仅仅占用一个字节,因为内部没有数据成员,仅仅是一个重载的方法而已。,实际上可以通过传递函数指针实现类似的功能(不好)。
仿函数举例使用
namespace zjt
{
template<class T>
struct less
{
bool operator()(const T& x, const T& y)const
{
return x < y;
}
};
template<class T>
struct greater
{
bool operator()(const T& x, const T& y)const
{
return x < y;
}
};
}
int main()
{
int a = 2;
int b = 3;
zjt::less < int>q;
cout << q(a, b) << endl;
//cout<<(a<b)<<endl
return 0;
}
priority_queue默认是大堆,其仿函数默认是less。
priority_queue模拟实现
namespace zjt
{
template<class T>
struct less
{
bool operator()(const T& x, const T& y)const
{
return x < y;
}
};
template<class T>
struct greater
{
bool operator()(const T& x, const T& y)const
{
return x < y;
}
};
//优先队列 大堆< 小堆>
template<class T,class Container = vector<T>,class Compare = less<T>>
class priority_queue
{
private:
Container _con;//储存堆中元素
public:
void AdjustUp(int child)
{
Compare conFuc;
int parent = (child - 1) / 2;
while (child > 0)
{
//if(_con[parent]<_con[child])
if (conFuc(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
void push(const T& x)
{
_con.push_back(x);
AdjustUp(_con.size() - 1);
}
void AdjustDown(int parent)
{
Compare conFuc;
size_t child = parent * 2 + 1;
while (child < _con.size())
{
//先判断是否有有孩子,然后再判断如果左孩子小于右孩子
//child++
if (child + 1 < _con.size() && conFuc(_con[child], _con[child + 1]))
{
child++;
}
if (conFuc(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void pop()
{
assert(!_con.empty());
std::swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
const T& top()
{
return _con[0];
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
};
}
仿函数的调用过程
其实上述仿函数能做的事情,用函数指针也能完成,但是艰难。
一个小于比较函数
仿函数与函数指针的区别
bool ComLess(int x1, int x2)
{
return x1 < x2;
}
//该函数指针为 bool(*)(int,int)
综上:传函数指针虽然也行,但是非常的麻烦,所以还是摒弃
同时
假设有这个结构体
struct goods
{
string _name;
double _price;//价格
size_t saleNum;//销售额
}
现在要根据某种规则将商品排序,如果是仅靠运算符重载的话,由于此时函数名和参数都是相同的,都是bool operator<(const goods& x)
,所以无法构成运算符函数重载,那么此时仿函数的作用就很大了。