一、stack和queue
1.1栈和队列的模板声明
栈的模板声明为
template <class T, class Container = deque<T> > class stack;
队列的模板声明为
template <class T, class Container = deque<T> > class queue;
可以发现,它们都是由两个模板参数构成的,其中一个是类型T,还有一个是适配器
1.2STL的复用:适配器
适配器的本质是为了:转换
栈和队列第二个类模板参数本质就是为了把其中的容器"deque"转换为栈和队列,其实list,vector也可以进行类似的转换
有了这一适配器,我们就可以在stack中使用一个成员变量
Container _con;
然后通过_con的尾插尾删等操作实现栈的入栈和出栈
1.2注:
用适配器实现的容器,通常不支持迭代器
1.3设计模式的概念
设计模式其实就是写代码的特殊方法,就像战争中的兵法
1.3.1适配器(转换)模式
利用适配器进行转换,简化代码的书写
1.3.2迭代器(封装)模式
利用迭代器进行多种代码的封装,实现更多功能
其实迭代器模式就是封装思想的第二层体现,所有支持迭代器的容器都可以使用iterator,统一使用如
iterator it=Container1.begin();
等类似操作方式进行访问
1.3补:对面向对象和面向过程的加深理解
他们很大的一个区别是:函数重载
面向对象,我们是通过一个封装好的类来创建对象,如一个vector<int>类型的v1,接下来我们只需要通过对v1进行操作,如v1.push_back(17);等方式来进行操作
面向过程,我们是直接通过函数来进行操作,创建的变量等等只能通过参数等方式传递给函数
1.4C语言进行"封装"的方式
在C语言中,我们其实也可以通过函数指针来实现类似
v1.push_back(17);
的操作:
首先需要在结构体中定义一个函数指针
void (*push_back)(int);
然后实现尾插的函数PushBack,传参一个int
之后在Init函数中规定指针的指向
void Init(int* pv)
{
//...
pv.push_back=PushBack;
//...
}
最后通过结构体访问操作符把结构体------函数指针------函数调用串接起来
struct vector v;//C语言不可省略struct,同时没有模板的概念
Init(&v);//必需要传v的地址过去对v初始化,这是无论如何也省不了的
v.push_back(17);
二、为适配器而生的deque
2.1deque的初步理解
deque在功能上可以看作是vector和list的结合体,他最关键的价值之一就是拥有相对高效的头插和尾插以及略逊与vector的下标访问,这是前述两者之一不具备的。
deque在结构上由一个中控数组(指针数组)与几段空间(统称buff小数组)组成
图示:
2.2deque头插的过程
当进行头插的时候,只要在当前起始数据对应的buff小数组在指针数组中对应位置的前一个位置,开一个指向新的buff小数组的空间,把数据放到buff小数组的尾即可
2.3[]访问数据的方式
访问的方式就是确定对应下标的位置再访问其中内容,
因为当头插之后,第一个buff小数组与其他的数据数量就不同了,为此源码中进行了假装满的操作,即通过把第一个数组中空位置的数量加在下标上,
再通过 下标/每个buff中数量 确定第几个buff数组
下标%每个buff中数量 确定在buff数组中第几个位置
2.4deque的缺陷与优点
2.4.1缺陷
①下标访问效率不够极致,比起顺序表要慢不少(例如使用库里的sort验证,因为其底层是快速排序,需要大量下标访问的操作,可以很好的说明问题),他们的效率差大概一倍左右
②insert和erase操作的效率低,也需要频繁挪动数据,远不如链表
2.4.2优点
它的头尾插入删除的效率都很不错
2.5deque的原理(迭代器的灵活使用)
2.5.1迭代器类
deque的源码也是利用了迭代器模式,他的迭代器类中有四个共有成员变量:
T* cur;
T* first;
T* last;
map_pointer node;//map_pointer即T**
其中first与last对应的是当前buff数组的首元素地址和最后一个元素地址的下一位,cur对应的是处于当前buff数组中该数据对应位置的地址,node对应的是当前buff数组在指针数组中对应的位置
2.5.2deque类
其中有着四个保护乘员变量
iterator start;
iterator finish;
map_pointer map;
size_type map_size;
其中start和finish共同起作用,确定了指针数组中有效数据的区间
map指向指针数组的首元素对应地址,map_size标划出当前指针数组的总长度
2.6了解原理后,简述尾插与头插的过程
2.6.1尾插
只要改变finish即可,分两种情况
①尾插完以后还在当前buff中,只需要改变cur指向的位置即可
②尾插后到下一个buff中,需要申请一个新的buff数组,更新一下first和last指向的位置,++node
2.6.2头插
只要改变start即可,分两种情况
①头插完了以后还在当前buff中,只要把cur指向的位置往前移一步即可
②头插到新的数组中,--node,申请新的数组,更新一下first和last的指向,cur指向last的前一个位置(last是最后一个数据的下一个位置)
三、优先级队列priority_queue
3.1priority_queue的本质
它的本质其实就是堆这一数据结构,而他的接口实现功能正是堆的功能
默认为大堆,若是取堆顶元素+pop可以打印出降序数组
3.2支持迭代器区间的构造以及原理
并不是它采用了迭代器模式,而是单独实现了一个模板函数,可以用入vector的迭代器区间进行构造,当然,传入数组的首尾元素地址也可以适配
其实现原理可以是全部数据尾插到适配器vector中再进行建堆
3.3类模板原型
template <class T, class Container = vector<T>,
class Compare = less<typename Container::value_type> > class priority_queue;
①之所以使用vector作为适配器是因为下标的随机访问使用频繁,如:
swap(_con[child],_con[parent]);
在向上调整算法和向下调整算法中出现频率很高
②第三个模板参数就是仿函数,虽然看着很长,其实less<typename Container::value_type>也可以直接理解为功能上的less<T>
3.4仿函数:改小堆的时候用得上
需要改小堆可以传入一个greater<int> ,如:
priority_queue<int,vector<int>,greater<int>>
因为类模板参数和函数参数类似,都不可以跳过中间的参数给两边参数传值,所以需要写明vector<int>
3.5优先级队列的实现
3.5.1成员变量的要求
只需要一个private修饰的适配器vector即可
3.5.2push入队(入堆)
push到vector以后向上调整,因为在类priority_queue中实现,只需要传从第几个位置的下标开始进行向上调整这一个参数给向上调整算法即可,在C语言中我们需要传_arr首元素地址和下标
3.5.3pop出队(出堆)
首元素和最后一个元素交换位置以后,vector的size--,这之后进行向下调整,因为在类priority_queue中实现,只需要传从第几个位置的下标开始进行向下调整这一个参数给向下调整算法即可,在C语言中我们需要传_arr首元素地址,_arr数组长度和下标
3.6仿函数
3.6.1出现原因
在C语言中,我们要么通过依次修改上下调整算法中的大于小于号来实现大小堆的更改,要么使用函数指针来实现,但是其实他们都比较冗余,为此C++给出了 仿函数/函数对象 这一概念:
给出一个类模板重载了operator(),使类的对象可以像函数一样直接使用
3.6.2使用举例
class Func
{
public:
void operator()(size_t n)
{
for(size_t i=0;i<n;++i)
{
cout<<"Func调用"<<endl;
}
}
}
int main()
{
Func f1;
f1(19);
return 0;
}
可以实现打印19行Func调用的功能
当然,若是无参也是可以的,f1()可以直接用来调用,
因此我个人认为构造函数之所以不允许显式调用,就是为了使仿函数的逻辑可以正常走通
3.6.3特点
参数个数和返回值类型可以根据我们的需求来确定,不是固定的,十分灵活
3.6补:
仿函数可以有很多的控制方案,例如在优先级队列中,模板参数我们存的不是整形而是整形的指针,这时候我们就不能继续走原来仿函数中直接比较的逻辑了,但此时,完全可以直接修改仿函数中operator()的逻辑,再把我们新实现的仿函数直接传给库中的priority_queue,也是可以达到比较指针指向数据大小的目的的