目录
1. deque
deque( 双端队列 ) :是一种双开口的 " 连续 " 空间的数据结构 ,双开口的含义是:可以在头尾两端进行插入和 删除操作,且时间复杂度为O(1) ,与 vector 比较,头插效率高,不需要搬移元素;与 list 比较,空间利用率比较高。
尽管名字叫双端队列,但是deque和队列FIFO的特性没有太多关系,我们抛开先进先出的特性进行学习。
deque是一个顺序容器(sequence container),而不是容器适配器。也就是说deque更像是list和vector,而非stack和queue
并且在功能上,deque就是list和vector的结合:
而deque什么都可以:
deque 并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际 deque 类似于一个动态的二维 数组。有许多个buff数组的数组指针组成作为 中控 的指针数组。buff数组从中间位置开始存放,这样就能头插和尾差。其中buff数组的大小N会根据每一次使用的不同而调整,专门有一个成员函数计算buff的大小。
设计deque
虽然中控数组满了会扩容,但是中控数组是指针数组,拷贝代价很低。
deque的设计与功能的联系
方括号访问的方法:
但是下标访问不如vector的极致,速度会相对慢一些。
头尾插删是不错,但是中间位置的插入删除就很麻烦了。
可以选择全部挪动数据,但是插入效率就和vector一样低;
或选择对插入数据的buff数组扩容,这样就只扩容一个小数组,而不影响大环境,但是扩容就会导致上面蓝字的方括号访问方法行不通。
我们通过sort来测试vector和deque的下标访问的速度(快速排序一直都在使用下标访问)
#include <iostream>
#include <deque>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
deque<int> dq;
vector<int> v;
srand(time(0));
int N = 1000000;
while (N) {
dq.push_back(rand() + N);
v.push_back(rand() + N);
--N;
}
size_t begin1 = clock();
sort(dq.begin(), dq.end());
size_t end1 = clock();
size_t begin2 = clock();
sort(v.begin(), v.end());
size_t end2 = clock();
cout << "time of deque : " << end1 - begin1 << endl << "time of vector : " << end2 - begin2;
return 0;
}
vector的速度是deque的两到三倍。
再来使用一下将deque拷贝给vector再排序:
有大量数据需要排序或者访问的时候是不建议用deque的,偶尔用用还行。
iterator
deque的iterator比较复杂:
由四个指针封装构成。map_pointer是一个T** , 是中控数组的指针
start指向的是第一个buff数组的第一个位置(但是第一个数组不会放到中控数组的第一个,而是放到中间位置)
finish指向的是最后一个buff数组的最后一个数据的下一个位置
关于解引用,迭代器解引用*it, 也就是 *cur
在cur等于last之前,就直接对指针进行++即可,当cur == last时
就让cur指向下一个buff的first并返回。
当前buff走完之后:
尾插的思路和遍历类似。
头插:
头插并非和我们所想到在start前的数组再从头加起,而是倒着加。
例如,在当前的deque下先头插-1 再头插-2
所以第一个buff不一定是满的。
为了让这样的头插和方括号的访问方法相匹配,我们需要调整一下下标访问:
比如我们要访问第n个元素:
那我们其实访问的是n+(cur-first)个元素 ,假装第一个buff是满的
综上所述,deque更适合用来适配stack和queue
与 vector 比较 , deque 的优势是:头部插入和删除时, 不需要搬移元素,效率特别高 ,而且在 扩容时,也不 需要搬移大量的元素 ,因此其效率是必 vector 高的。与 list 比较 ,其底层是连续空间, 空间利用率比较高 ,不需要存储额外字段。
缺点:
deque 有一个致命缺陷:不适合遍历,因为在遍历时, deque 的迭代器要频繁的去检测其是否移动到 某段小空间的边界,导致效率低下 ,而序列式场景中,可能需要经常遍历,因此 在实际中,需要线性结构 时,大多数情况下优先考虑 vector 和 list , deque 的应用并不多,而 目前能看到的一个应用就是, STL 用其作 为 stack 和 queue 的底层数据结构
2. 优先级队列
优先级队列是一种容器适配器,priority_queue的底层选择的是vector
目前为止我们已经了解了三种容器和三种容器适配器。分别是vector deque list和 stack queue priority_queue
优先级就是去取大的来优先进出队列或者取小的来优先进出队列。
回忆我们学过的数据结构,只有大堆和小堆符合要求。
优先级队列的本质就是堆(默认是大堆)。
优先级队列也是被包含在头文件queue中的。并且在命名上,虽然优先级队列和队列关系不大,但我们还是多用q来表示它。
2.1 优先级队列的使用
关于构造:
直接用迭代器区间就可以建堆(建堆就是建priority_queue),一个一个push就会一遍一遍调整。
一个一个push的时间复杂度:O(nlogn)
直接建堆的时间复杂度: O(n)
因为一个一个push是采用的向上调整算法;而直接将存在的数组建堆是采用的向上遍历的向下调整算法。后文会有具体的计算。
原生指针也可以用于建堆:
因为迭代器就是像指针一样的东西 ,指针支持解引用、++、--等一些列迭代器的功能。
但是不支持initializer_list
默认是大堆:
可观察到有三个模版参数,第一个class T表示元素类型,第二个表示底层容器类型并默认是vector
其中第三个涉及新内容:仿函数
less默认是大堆,greater是小堆
如果要使用小堆,就要加入第三个模版参数。而根据半缺省的规则,写第三个参数就得写第二个。
将第三个参数传成greater<int>
3. 实现优先级队列(堆)
先不管仿函数的内容:
对于push的实现,先将新元素塞到容器的末尾,再向上调整:
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);
}
紧接着就需要实现向上调整算法
(本文为了方便理解,实现的都是只支持vector或者deque等可以使用下标访问的容器,但是真正的priority_queue一定是可以用list的,只是不推荐将list作为底层)
根据我们在C语言数据结构中对树的学习,实现出代码:C语言数据结构基础笔记——树、二叉树简介_c语言的树-CSDN博客
void adjust_up(int child) {
assert(child >= 0);
int parent = (child - 1) / 2;
while (child != 0) {
if (_con[parent] < _con[child]) {
swap(_con[parent], _con[child]);
child = parent;
parent = (parent - 1) / 2;
}
else {
break;
}
}
}
while后面的大条件只能用child > 0,因为根据int的特性,父亲无论如何都不会小于0
因为(0-1)/2=0;
同理向下调整:
void adjust_down(int parent) {
assert(parent >= 0);
int child = parent * 2 + 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 = child * 2 + 1;
}
else {
break;
}
}
}
除了push还有pop
然后是几个直接封装vector即可的简单接口:
T& top() {
return _con[0];
}
const T& top() const {
return _con[0];
}
bool empty() {
return _con.empty();
}
size_t size() {
return _con.size();
}
迭代器区间构造:
实现方法一:一个一个push
本质就是向上调整,挨个插入。
因为push是调用向上调整,总体复杂度是O(nlogn)
并不是因为每次都是logn,一共有n次,而是因为
我们一般取N和h的关系为h = logN
而利用向上遍历的向下调整算法来直接建堆是O(n)
而向上遍历的向下调整算法就是从 倒数的第一个非叶子节点开始建堆
也就是从(_con.size()-1-1)/2开始调整。
template<typename InputIterator>
priority_queue(InputIterator first, InputIterator last) {
/*while (first != last) {
*this->push(*first);
++first;
}*/
while (first != last) {
_con.push_back(*first);
++first;
}
for (int i = (_con.size() - 1-1) / 2; i >= 0;--i) {
adjust_down(i);
}
}
最后,由于我们只实现了一个范围拷贝,还希望继续使用编译器提供的其他构造。
priority_queue = default();
这样建好了大堆。
如果想改成小堆呢?
以前的做法是改大小于符号,
而C++的处理方法是仿函数。
4. 仿函数
仿函数,也叫函数对象,仿函数是一个类,本质是对operator()的重载
仿函数是一个重载了operator()的类,类的对象可以像函数一样使用
f1是一个类,f1()就是对于f1.operator() () 的调用。
仿函数是class或者struct都可以,但是class需要使用public修饰,建议直接改成struct
我们以一个比较大小的函数为例:
如果不加public,默认是private:
所以一般建议使用struct来控制仿函数
为了比较不同的类型,我们也可以通过模版自己传数据类型
回到优先级队列中:
在prority_queue中刚刚的基础上再传一个参数:
也就是:
接着就可以使用了:
也可以不在函数中单独定义func,而是在private下直接新增这样一个变量。
想从大堆变小堆,就可以传mygreater而不是myless即可。
仿函数第一层作用:替代并加强函数指针的作用。仿函数被当作一个模版参数传入类中,可以在类中使用。
再来观察一下priority_queue的优势:
每次运行都不一样。
因为传的是Date* ,存进去的是指针,比较的就是地址,而地址大小每一次new出来都不一样(没有规定先new的和后new的有什么不同)
但是我们可以自己完成一个由仿函数控制的新的比较逻辑
最后贴一下优先级队列的整体代码:
namespace lsnm {
template<class T>
struct myless {
bool operator()(const T& x, const T& y) {
return x < y;
}
};
template<class T>
struct mygreater {
bool operator()(const T& x, const T& y) {
return x > y;
}
};
template<typename T,typename Container = vector<int>,typename Compare = myless<int>>
class priority_queue {
public:
priority_queue = default();
template<typename InputIterator>
priority_queue(InputIterator first, InputIterator last) {
/*while (first != last) {
*this->push(*first);
++first;
}*/
while (first != last) {
_con.push_back(*first);
++first;
}
for (int i = (_con.size() - 1-1) / 2; i >= 0;--i) {
adjust_down(i);
}
}
//先都和默认一样,按照大堆实现
void adjust_up(int child) {
assert(child >= 0);
//Compare func;
int parent = (child - 1) / 2;
while (child != 0) {
//if (_con[parent] < _con[child]) {
if (func(_con[parent],_con[child]) {
swap(_con[parent], _con[child]);
child = parent;
parent = (parent - 1) / 2;
}
else {
break;
}
}
}
void adjust_down(int parent) {
assert(parent >= 0);
int child = parent * 2 + 1;
while (child < _con.size()) {
if (child + 1 < _con.size() && func(_con[child],_con[child+1]) {
++child;
}
//if (_con[parent] < _con[child]) {
if (func(_con[parent], _con[child]) {
swap(_con[parent], _con[child]);
parent = child;
child = child * 2 + 1;
}
else {
break;
}
}
}
void push(const T& x) {
_con.push_back(x);
adjust_up(_con.size()-1);
}
T& top() {
return _con[0];
}
const T& top() const {
return _con[0];
}
bool empty() {
return _con.empty();
}
size_t size() {
return _con.size();
}
private:
Container _con;
Compare _func;
};
}