欢迎各位大佬光临本文章!!!
还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。
本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。
我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog
我的gitee:冰冰棒 (bingbingsupercool) - Gitee.comhttps://gitee.com/bingbingsurercool
系列文章推荐
目录
前言
在学习栈与队列的使用时我们发现,文档中并没有把这两个归纳为容器,而是出现了新的概念,适配器。那什么是适配器呢?
1.容器的适配器
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。其最大的特点就是复用!!!
在栈与队列中,我们发现栈与队列这两种数据结构为了保证他们本身的性质,并不会提供迭代器来支持随机访问。拿栈来说,栈是一种先入后出的容器,因此栈的接口函数只有很少的几个。而这些接口中vector与list都是支持的,因此栈完全可以使用vector和list进行封装,而不用自己实现。
因此我们在实现栈或者队列的时候,只需要增加一个模板参数,并提供默认缺省值,既能让用户可以显示提供适配器来构建栈或者队列,也可以直接使用默认提供的适配器进行构建。
当用户调用时既可以显示调用,也可以直接调用。
因此,我们的模拟实现也非常简单,甚至连构造函数都不用写,因为构造时会去调用适配器的默认构造函数。
栈的模拟实现
template<class T,class Container=deque<T>>
class stack
{
public:
void push(const T& x) { _st.push_back(x);}
void pop() { _st.pop_back();}
T& top() { return _st.back();}
const T& top()const { return _st.back();}
bool empty() { return _st.empty();}
size_t size()const { return _st.size();}
private:
Container _st;
};
2.deque的了解
但是栈与队列并没有使用vector或者list作为自己的适配器,尤其是队列,vector本身就不提供头删更不能使用。所以库中这两个容器的适配器都采用了deque。
deque(双端队列)是一种双开口的"连续"空间的数据结构,它支持在头尾两端进行O(1)的插入和删除,并且还支持随机访问。与vector相比,deque头插效率高,不用频繁挪动数据,与list相比,其内部空间利用率比较高,且支持随机访问。
deque真的是连续的空间吗?其实不然,deque所谓的连续空间是由一块块连续的小空间拼接而成,我们可以将其理解为list中存储了大量一摸一样的vector,每一段小空间都是连续的,并且大小一样,小空间之间通过指针相连接。
而deque为了维护空间的随机访问,使其迭代器非常复杂,其迭代器内部具备4个指针,使用4个指针来进行区间的维护。
实现原理非常复杂,简单来说就是使用前三个指针维护每一段buff区间,node指针则用来维护buffer之间的联系,当一个buffer已经遍历完毕时,node将向后迭代,指向另一块连续的空间,并且更新cur,first,last指针。
在进行随机访问时,首先deque先计算出该下表位于第几个buffer,然后去相应的buffer进行寻找。但是频繁的随机访问使得deque的效率并不高。
deque的优缺点比较明显,与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是比vector高的。 与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
3.优先级队列与仿函数
优先级队列是队列结构的一种,其底层就是我们之前实现的堆结构。我们使用优先级队列取出元素时,取到的元素必然为堆顶的元素。优先级队列的默认实现是大堆,当我们想实现小堆时就需要使用仿函数来实现。
那什么是仿函数呢?仿函数并不是函数,而是对()运算符的重载。本质是一个类,使用时需要创建对象进行调用,看起来形如函数调用。
通过仿函数的使用,我们可以进行泛型编程,使用显示传递的仿函数来控制某些判断条件,从而实现不同的排列方式。
例如优先级队列中的调整算法,我们不传参数默认提供的是less算法,意味着当父亲节点数据小于孩子节点数据时就会向上调整,那么构建出来就是大堆。当我们显示传递greater算法时,判断条件就变为当父亲节点数据大于孩子节点数据时才会进行调整,从而构建出小堆,同一个算法不同的参数就构成了两种不同的结果,极大提高了代码的复用率。
最后附上优先级队列的实现代码:
namespace lb
{
//仿函数
template<class T>
struct less
{
bool operator() (const T& l, const T& r)const {return l < r;}
};
template<class T>
struct greater
{
bool operator() (const T& l, const T& r)const {return l > r;}
};
template<class T, class Container = vector<T>,class Compare = std::less<T>>
//默认大堆--小于--less//小堆--大于--greater
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 push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);
}
void pop()
{
std::swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
adjust_down(0);
}
T& top() {return _con[0];}
const T& top()const {return _con[0];}
bool empty() {return _con.empty();}
size_t size()const {return _con.size();}
void adjust_up(size_t child)
{
Compare com;
size_t parent = (child - 1) / 2;
while (child>0)
{
//if (_con[child] > _con[parent])
//if (_con[parent] < _con[child])
if (com(_con[parent] , _con[child]))
{
std::swap(_con[child], _con[parent]);
child = parent;
parent= (child - 1) / 2;
}
else
break;
}
}
void adjust_down(size_t parent)
{
Compare com;
size_t child = parent * 2 + 1;
while (child < _con.size())
{
//if(child + 1 < _con.size() && _con[child+1] > _con[child])
//if (child + 1 < _con.size() && _con[child ] < _con[child+1])
if (child + 1 < _con.size() && com(_con[child] , _con[child + 1]))
{
++child;
}
//if (_con[child] > _con[parent])
//if (_con[parent] < _con[child])
if (com(_con[parent], _con[child]))
{
std::swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
private:
Container _con;
};
}