目录
3. 为什么选择deque作为stack和queue的底层默认容器
一. deque容器
由于stack和queue都选择deque作为容器,这里就简单了解一下deque
1. deque的原理介绍
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1)。
我们知道:vector适合尾插尾删,支持随机访问,cpu高速缓存命中高;list任意位置插入删除效率高(O(1))。但是,vector头部和中部插入删除效率低,需要挪动数据,而且扩容有性能消耗,存在空间浪费;list不支持随机访问,cpu高速缓存命中低。
所以,基于两者的优缺点,deque就被发明出来了。
那么deque实际上是怎样的?
其实deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:
那deque是如何借助其迭代器维护其假想连续的结构呢?
所以,我们会发现它其实是由一个中控数组来并且是指针数组,一个个指针指向并维护一段连续的空间。
也就能看出来它的优点:头部尾部插入删除数据效率不错,支持随机访问,扩容代价也小,cpu高速缓存命中率高。
2. deque的致命缺陷
它的缺点也是致命的缺点:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,并且中部插入删除效率不行,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,使用场景也就只有大量的头尾插入删除,偶尔随机访问,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
3. 为什么选择deque作为stack和queue的底层默认容器
stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据结构,只要具有 push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。
但是STL中对stack和queue默认选择deque作为其底层容器,原因:
- stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
- 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。
我们会发现,结合了deque的优点,完美避开了deque的缺陷。
二. 反向迭代器
在学习vector和list时没有去涉及反向迭代器,这里统一学习。
有了容器适配器的思想,反向迭代器也就很好实现了,这里反向迭代器的实现方式就是适配器方式,对正向迭代器进行了包装适配了一下,跟正向迭代器相比,除了++/--时方向相反,其他操作基本一致。这里解引用返回的是正向迭代器的前一个位置,采用的是对称式设计。
需要注意的是这里为了处理const的问题和list一样采用的是传三个模板参数(分别是正向迭代器,引用,指针),引用和指针是为了针对需要返回const类型的问题。结合list的迭代器的实现稍作改进后,反向迭代器的实现如下:
//反向迭代器,返回当前位置的上一个元素
//反向迭代器
template<class Iterator, class Ref, class Ptr>
struct Reverse_iterator
{
typedef Reverse_iterator<Iterator, Ref, Ptr> self;//三个模板参数用来处理const的问题
//带参构造函数
Reverse_iterator(Iterator it)
:_it(it)
{}
Ref operator*()
{
Iterator tmp(_it);
return *(--tmp);//返回的是前一个元素
}
Ptr operator->()
{
return &(operator*());//调用*返回元素的地址
}
//前置++
self& operator++()
{
--_it;
return *this;
}
//前置--
self& operator--()
{
++_it;
return *this;
}
//!=
bool operator!=(const self& s)
{
return _it != s._it;//顺序不能调换,左操作数是*this
}
Iterator _it;
};
由此一个通用反向迭代器的实现也就完成了。
三. 迭代器萃取(了解向)
虽然和list迭代器的实现一样不是很难,但是,查看源代码会发现源码里并不是采用三个模板参数处理的,只传了一个正向迭代器的模板。
事实上,这里就牵引出一个新玩意——迭代器萃取。
1. list单模板参数迭代器
首先对list的迭代器实现进行修改,由于stl库规定迭代器的实现是必须得要有以下类型:
我们需要对list的迭代器实现增加最后两个类型,由于只传了正向迭代器一个模板参数,那么我们就要对于*和->的返回值就需要像如下:
因为迭代器的实现是struct,所以可以读取其中的成员变量,但是这样也是有问题的,从图中也能看出来(pointer和reference都是白色的),一旦编译就会报一堆错误,因为编译并不能识别出这两个函数的返回值是什么东西。
为什么识别不出?
因为本身我们传过来的正向迭代器是一个类模板,而这里的正向迭代器类模板还没有实例化,编译器规定不能去还没有实例化的类模板里寻找东西,因为如果允许的话,类模板没有实例化,找出来也是虚拟类型,后期无法处理,因为在实例化的时候,只会实例化本模板类的模板参数,而因为我们的反向迭代器如果只有一个正向迭代器的模板参数,所以并不会去实例化T,所以这样写就会有问题。
解决方案:在上图Iterator的前面加上一个typename,告诉编译器后面这一串是一个类型,等Iterator实例化后再去它里面去找内嵌类型。
这样list的反向迭代器只传一个参数的问题就解决了。
2. vector的单参数迭代器——迭代器萃取
如果像上面那样处理vector的迭代器,是会报错的,我们能像上面那样处理迭代器的前提是我们list的迭代器是自定义类型的迭代器,自定义类型里面有我们的自己定义的Ref和Ptr,但是vector的迭代器是T*和const T*,这是原生指针,而我们并没有Ref和Ptr这种的内嵌类型。
那原生指针怎么办?
两种解决方式:
- 不用原生指针做迭代器,像list那样,将原生指针封装起来,实现一个自定义类型的迭代器,就可以像list那样提取迭代器里成员变量(stl库并没有采用这个方式)
- 用萃取类套层数,需要使用特化,自定义类型不做处理,针对原生指针进行了特化,即特殊处理
如上图:
就是套层数,我们会发现这里针对T*和const T*是迭代器萃取类进行了特化,其实这两个特化就是为vector和string的原生指针迭代器准备的。
我们还能发现,像list这种自定义的迭代器进来就会走第一个萃取类,也就是直接拿list迭代器里的成员变量,而原生指针就会走下面特化的版本,对原生指针进行封装。
因此,本质上迭代器的萃取实质上是使用了类模板的特化。
为什么明明三个参数能解决的事情要搞这么麻烦?事实上,这样处理不仅仅是因为反向迭代器,这样处理方便取出迭代器的类型。因为有些算法会需要取迭代器的类型。例:distance函数计算两个迭代器之间的距离。
四. 类模板中的typename
上面提及typename除了可以在类模板和函数模板处使用声明是一个模板(也可以使用class)。其实它还有一个独特的功能:告诉编译器后面这一串是一个类型,等内嵌类型的模板类实例化后再去它里面去找内嵌类型。
由此可以衍生出以下的问题:
我们要实现一个用list去存储两个类型的数据,但是只想用一个打印函数,那么这里很容易想到使用模板,于是会有以下的代码:
template<class T>
void print(const list<T>& lt)
{
list<int>::const_iterator cit = lt.begin();
while (cit != lt.end())
{
cout << *cit << " ";
++cit;
}
cout << endl;
}
void test1()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
print(lt1);
list<string> lt2;
lt2.push_back("abc");
lt2.push_back("def");
lt2.push_back("ghi");
lt2.push_back("jkl");
print(lt2);
}
但是这个程序一旦进行编译就会出现以下报错:
其实从报错里我们会发现,我们需要加上一个typename,为什么?观察代码能发现:
const list<T>& lt
list<T>::const_iterator cit = lt.begin();
这两行是类模板,也就是虚拟类型,这就回到上面的:编译器规定不能去还没有实例化的类模板里寻找东西。所以我们需要在前面加上typename。如下:
问题就解决了。
所以说,当我们实例化时想要使用模板或者包含模板参数的这种虚拟类型,我们都需要在前面加上typename,本质上就是:告诉编译器typename后面这一串是一个类型,等内嵌类型的模板实例化后再去它里面去找内嵌类型。