容器deque(了解向)和反向迭代器

目录

一. deque容器

1. deque的原理介绍

2. deque的致命缺陷

3. 为什么选择deque作为stack和queue的底层默认容器

二. 反向迭代器

三. 迭代器萃取(了解向)

1. list单模板参数迭代器

2. vector的单参数迭代器——迭代器萃取

四. 类模板中的typename


一. 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作为其底层容器,原因:

  1. stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
  2. 在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这种的内嵌类型。

那原生指针怎么办?

两种解决方式:

  1. 不用原生指针做迭代器,像list那样,将原生指针封装起来,实现一个自定义类型的迭代器,就可以像list那样提取迭代器里成员变量(stl库并没有采用这个方式)
  2. 用萃取类套层数,需要使用特化,自定义类型不做处理,针对原生指针进行了特化,即特殊处理

 如上图: 

就是套层数,我们会发现这里针对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后面这一串是一个类型,等内嵌类型的模板实例化后再去它里面去找内嵌类型

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hiland.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值