目录
前言
适配器介绍
deque介绍(了解)
容器适配器与普通容器的联系
stack模拟实现
queue模拟实现
priority_queue模拟实现
介绍
实现
反向迭代器模拟实现
介绍
实现
在list类中调用
在vector类中调用
后记
前言
在介绍完string、vector、list类之后,对应着数据结构,应该介绍栈和队列了吧,yes!但是string、vector、list类是容器,而这里的栈和队列是属于容器适配器,既然谈到了适配器,这篇文章就把适配器的相关内容介绍一下吧,包括基本介绍、deque、stack、queue、priority_queue以及反向迭代器,难以理解的内容不多,主要通过这些内容将适配器是什么、适配器有什么用理解明白即可,而这些内容本身并没有什么复杂的东西,快往下看吧!
适配器介绍
适配器(Adapter)是一种软件设计模式,它允许将接口不兼容的类或对象组合在一起工作。适配器模式相当于两个不相容的接口之间的中间层,它转换一个接口,以便让另一个接口能够与之兼容,从而使得两个不兼容的接口可以协同工作,即将一个类的接口转换为客户希望的另外一个接口。 ——摘抄引用
适配器分为多种,今天我们讨论的是容器适配器,其他种后面遇到再总结,容器适配器是一种特殊类型的容器,它们提供了一种不同于标准容器的接口,但基于现有的 STL容器实现,以支持特定类型的操作。
容器适配器可以被认为是容器的封装,它们使用已有容器的接口来实现常用的数据结构。比如栈、队列、优先队列,它们都是基于vector、deque或list等现有容器实现的。使用容器适配器可以更轻松地实现常见的数据结构,并且可以避免手动实现底层数据结构的复杂性和错误。
deque介绍(了解)
deque(double-ended queue)是双端队列,可以在两端进行插入和删除操作,是高效的O(1)时间复杂度的操作,也可以在中间插入删除,但时间复杂度是O(N),deque可以看作是一个“数组”,但它并不是真正连续的空间,而是一段段连续的空间拼接,拼接的方法就是使用了链表的思想,连续的空间是使用了顺序表的思想,所以说,deque是与顺序表和链表对齐,而不是与栈和队列对齐。
deque的常用操作包括push_front、push_back、pop_front、pop_back、insert、erase等(如图一),逻辑结构如图二所示。
比如说,一小块连续空间的大小能存放3个(实际情况可以改,这里为了简化理解),先尾插1,2,3,4,再头插5,6,如下图。
容器适配器与普通容器的联系
说了半天适配器,又介绍了deque容器,那容器适配器与普通容器到底有啥区别,或者有啥联系,我们先看看官方文档中的栈与队列类的模板列表,如下图。
可以发现,栈和队列的类模板列表中出现了Container的类型名,也就是在定义栈和队列时不仅需要传入数据类型,还要传入所使用的底层容器,而且可以发现,栈和队列的默认底层容器是deque,当然也可以使用vector或者list容器,这就是容器适配器与普通容器的联系。
那为什么源码会使用deque作为栈和队列的默认底层容器呢?先来看一下deque对比vector和list的优势和劣势吧。①deque更适合头尾的插入删除,复杂度都是O(1),因为vector头尾插入时需要移动大量元素,list插入过多时需要扩容,不断向OS申请空间;②deque在中间插入删除效率并不高,而且并不适合遍历,这两点加起来就能充分说明deque很适合作为栈和队列的默认底层容器,而且可以看出栈和队列正是deque优点的结晶,这是deque应用之一,实则能应用到deque的地方并不多。
stack模拟实现
通过数据结构课程的学习我们知道,栈可以使用数组也可以使用链表形式实现,所以这里底层容器Container可以使用vector或者list,但是官方使用了上面介绍的deque容器实现。
stack类成员就是使用传过来的容器类定义的一个对象,无需构造函数和析构函数,因为成员变量只有一个自定义类型,构造和析构都是去调用它自己的函数,然后就是将stack所需的接口使用指定类实现即可,包括压栈、出栈、访问栈顶、以及对应const对象所对应的函数。
代码:
template <class T, class Container = deque<T>>
class Stack
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
T& top()
{
return _con.back();
}
const T& top() const
{
return _con.back();
}
bool empty() const
{
return _con.empty();
}
size_t size() const
{
return _con.size();
}
private:
Container _con;
};
queue模拟实现
队列的实现与栈一致,默认底层容器也是deque,唯一不同点就是队列的接口与栈不同,注意即可。
代码:
template <class T, class Container = deque<T>>
class Queue
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_front();
}
T& front()
{
return _con.front();
}
const T& front() const
{
return _con.front();
}
T& back()
{
return _con.back();
}
const T& back() const
{
return _con.back();
}
bool empty() const
{
return _con.empty();
}
size_t size() const
{
return _con.size();
}
private:
Container _con;
};
priority_queue模拟实现
-
介绍
priority_queue叫做优先队列,是队列的一种,也就说大部分实现也是与普通队列一致,核心不同点在于优先队列中的元素被赋予优先级。当出队时,优先级最高的元素先出队。在一个优先队列中,元素的顺序不仅仅取决于它们进入队列的顺序,还取决于它们的优先级。这里的优先级可以自己通过仿函数Com设置,比如整数中越大的优先级越高,或者字符串相比越大优先级越高,当然,底层容器Container也是可以自己设置,可以设置为vector、list、deque。
-
实现
官方设置vector作为priority_queue的底层容器,在vector的基础上又设置了堆算法将vector的元素构造成堆的结构,也可以说priority_queue就是个堆,且默认是大堆。
priority_queue的成员变量也是底层容器定义的一个对象,而且上面提到priority_queue就是个堆,所以需要构造函数将传入的元素先构造成一个堆,这里使用传迭代器构造的方式,实现过程就是在数据结构课程中学习的建队的过程,即从最后一个非叶节点开始从后向前对每个节点调用向下调整算法。值得注意的是,同时要写一个默认构造函数(无参或者全缺省的普通构造函数),否则就会报“没有默认构造函数”的错。
入队操作(push)就是先尾插元素,再对此元素调用向上调整算法形成一个堆,出队操作(pop)就是将优先级最高的元素(即堆顶)与最后一个元素交换,将优先级最高元素pop出去,再对新堆顶元素调用向下调整算法形成一个堆,其他操作包括但不限于访问堆顶元素、判空等。
代码:
template <class T, class Container = vector<T>, class Com = less<T> >
class Priority_queue
{
public:
Priority_queue()
{
}
template <class InputIterator>
Priority_queue(InputIterator first, InputIterator last)
{
//插入数据
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(size_t child)
{
Com com;
size_t p = (child - 1) / 2;
while (child != 0)
{
//if (_con[child] > _con[p])
//if (_con[p] < _con[child])
if (com(_con[p], _con[child]))
{
std::swap(_con[child], _con[p]);
}
else
break;
child = p;
p = (child - 1) / 2;
}
}
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);
}
void adjust_down(size_t parent)
{
Com com;
size_t ch = parent * 2 + 1;
while (ch < _con.size())
{
//if (ch < _con.size() - 1 && _con[ch + 1] > _con[ch])
//if (ch < _con.size() - 1 && _con[ch] < _con[ch + 1])
if (ch<_con.size() - 1 && com(_con[ch], _con[ch + 1]))
{
ch++;
}
//if (_con[ch] > _con[parent])
//if (_con[parent] < _con[ch])
if (com(_con[parent] , _con[ch]))
{
std::swap(_con[parent], _con[ch]);
}
else
break;
parent = ch;
ch = parent * 2 + 1;
}
}
void pop()
{
std::swap(_con.front(), _con.back());
_con.pop_back();
adjust_down(0);
}
bool empty() const
{
return _con.empty();
}
T& top()
{
return _con.front();
}
const T& top() const
{
return _con.front();
}
private:
Container _con;
};
反向迭代器模拟实现
-
介绍
在介绍string、vector、list的迭代器过程中,无论是原生指针还是包装成一个类,我们都只了解到了普通迭代器和const迭代器,当初在了解stl的六大件时得知,迭代器不止有那些,还有反向迭代器以及反向const迭代器,之前是因为还未了解到适配器,所以将反向迭代器的介绍留在了这里。那为什么将反向迭代器留在适配器的地方讲呢?是因为反向迭代器是一个迭代器适配器,传进一个容器的迭代器,就能适配出对应的反向迭代器。
与正向迭代器类似,反向迭代器可以倒序遍历容器中的元素,通过调用rbegin()和rend()方法获取反向迭代器的起始和结束位置,同时也可以使用*、->、++等操作符进行访问和移动。
注意:也不是所有容器的迭代器都可以适配出反向迭代器,比如<forward_list>(单链表)、<unordered_map>、<unordered_set>,这些容器都不能逆向遍历。
-
实现
根据介绍,反向迭代器也是使用类模板实现,模板参数中传入对应容器迭代器,再传入操作符*、->所需的数据类型的引用和指针形式。
提到说反向迭代器是适配器,去调用对应容器适配器的接口以实现自己的接口,所以成员变量就是容器迭代器的类所定义的一个对象,构造函数则是根据传入的容器迭代器对象初始化反向迭代器对象。运算符++、--也是很简单,反向迭代器的++对应正向的--,反向的--对应正向的++。
stl规定:rbegin对应end,rend对应begin,如下图
这样的规定就导致了 解引用操作就是解引用当前迭代器的前一个迭代器,也是得到前一个迭代器的值,即先让迭代器--,再取值(如代码所示),同时->操作是取前一个迭代器的值的地址。而对于关系操作符则是比较成员对象(容器迭代器)是否相等,不做过多赘述。
代码:
template <class Iterator, class Ref, class Ptr>
class __reverse_iterator
{
public:
//此typedef仅是简化反向迭代器的名字,无其他作用
typedef __reverse_iterator<Iterator, Ref, Ptr> RIterator;
Iterator _cur;
__reverse_iterator(Iterator x)
:_cur(x)
{
}
RIterator& operator++()
{
--_cur;
return *this;
}
RIterator& operator--()
{
++_cur;
return *this;
}
Ref operator*()
{
Iterator tmp = _cur;
return *--tmp;
}
Ptr operator->()
{
return &(operator*());
}
bool operator!=(const RIterator& x)
{
return _cur != x._cur;
}
};
-
在list类中调用
在有了list迭代器和const迭代器的基础上,传入反向迭代器类模板,形成了list的反向迭代器的类__reverse_iterator<iterator, T&, T*>,根据stl规定,rbegin对应end,rend对应begin,对应情况如下图。
注意:对于rbegin()的返回值reverse_iterator(end()),是通过传入end()迭代器构造一个反向迭代器传回,不过不显式构造也行,直接return end()就存在隐式类型转换,也是会调用构造函数构造,其他成员函数也如此。
代码:
//这里的iterator就是list迭代器,const_iterator就是list的const迭代器
//typedef作用是简化类名
typedef __reverse_iterator<iterator, T&, T*> reverse_iterator;
typedef __reverse_iterator<const_iterator, const T&, const T*> const_reverse_iterator;
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
const_reverse_iterator rbegin() const
{
return const_reverse_iterator(end());
}
const_reverse_iterator rend() const
{
return const_reverse_iterator(begin());
}
-
在vector类中调用
调用情况与list中一致,多写一份调用就多份理解,仔细琢磨一下。
代码:
//这里的iteratior是vector类的迭代器
typedef __reverse_iterator<iterator, T&, T*> reverse_iterator;
typedef __reverse_iterator<const_iterator, const T&, const T*> const_reverse_iterator;
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
const_reverse_iterator rbegin() const
{
return const_reverse_iterator(end());
}
const_reverse_iterator rend() const
{
return const_reverse_iterator(begin());
}
后记
目前阶段,适配器相关内容大概就包括以上内容,完整看下来的话可以发现,适配器就是借用已有容器的接口实现自己专用的接口,比如,你有苹果13(有苹果13的充电器),有一天你又买了一部苹果14,但没有苹果14的充电线,就用13的充电器,的确也可以冲,但是冲得慢且有时接触不良,效果不好,所以你就想,有苹果13的充电器,能不能买个转接头,插在13的充电器的头上就可以给14充电了,而且充电迅速且效果好。此时,转换头就是所谓的适配器。
这么说,应该就很好理解了,上面常见的适配器相关介绍还有不懂的可以私我也可以评论区,加油,拜拜!