1.1 stack的使用
下面这些接口的使用我相信大家已经是游刃有余了,这里就不用过多演示,若不熟悉查文档即可。
函数说明 | 接口说明 |
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push() | 将元素val压入stack中 |
pop() | 将stack中尾部的元素弹出 |
void test_stack()
{
stack<int> s;
s.push(1);
s.push(2);
s.push(3);
s.push(4);
while (!s.empty())
{
cout << s.top() << " ";
s.pop();
}
}
1.2stack的模拟实现
对stack的实现,现在只关注它的实现全是调用。关于deque,Container下面我们就会进行探究。
#pragma once
#include<vector>
#include <list>
namespace qhx
{
template<class T, class Container = deque<T>>
class stack
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
const T& top()
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
Container _con;
};
}
二、queue-队列
队列也是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
2.1queue的使用
队列的接口其实与栈的接口基本一样,而且使用方法也是一样。
函数声明 | 接口说明 |
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回true,否则返回false |
size() | 返回队列中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队列 |
pop() | 将队头元素出队列 |
void test_queue()
{
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
while (!q.empty())
{
cout << q.front() << " ";
q.pop();
}
cout << endl;
}
2.2queue的模拟实现
#pragma once
#include<vector>
#include <list>
namespace qhx
{
template<class T, class Container = deque<T>>
class queue
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_front();
}
const T& front()
{
return _con.front();
}
const T& back()
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
Container _con;
};
}
queue的模拟实现也是非常简单的,都是复用其他人代码。栈和队列的实现中,我们发现都有class Container = deque。
在queue和stack中都有这样一段话。
Queue:
queues are a type of container adaptor, specifically designed to operate in a FIFO context (first-in first-out), where elements are inserted into one end of the container and extracted from the other.
翻译:队列是一种容器适配器,专门设计用于在 FIFO 上下文(先进先出)中运行,其中元素插入容器的一端并从另一端提取。
Stacks:
Stacks are a type of container adaptor, specifically designed to operate in a LIFO context (last-in first-out), where elements are inserted and extracted only from one end of the container.
翻译:
堆栈是一种容器适配器,专门设计用于在后进先出(后进先出)环境中操作,其中元素仅从容器的一端插入和提取。
class Container = deque在这里就是容器适配器。
2.3容器适配器
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。
设计模式的使用将提高软件系统的开发效率和软件质量,节省开发成本。有助于初学者深入理解面向对象思想,设计模式使设计方案更加灵活,方便后期维护修改。
在stack,queue中 deque接口转换到Container中。deque是什么呢?我们不是说stack可以用vector,queue用list吗,怎么这里用的deque。
三、deque
在容器适配器为什么会选择deque,那么就必须得从vector,list的优缺点说起
3.1vector,list的优缺点
vector:
stack可以随机访问,但是头部中部插入删除效率低,并且还需要扩容
list:
虽然queue在任何地方插入删除效率高,但是不支持随机访问,CPU高速缓存命中率低
对于deque就完美兼容vector,list的优点。所以对于接口选择就是deque。
3.2deque的原理介绍
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和 删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
这个是deque一段的buffer数组,所以deque并不是真正连续的空间,它是由一段一段这样的buffer数组链接而成,一段一段的buffer数组被放在中控,这个中控就是一个指针数组,实际上deque类似于一个动态的二维数组, 如图:
这里的缓冲区就是buffer数组,用于存放数据。map就是中控器,就是存放指针。当map空间不够后,会再开辟一个中控-map。
3.3deque–插入
插入操作–头插
插入操作–尾插
**查找:**即相当于二维数组一样,先找map中的地址(第一层),然后在找buffer(第二层)
缺点:
那么我们发现它下标访问有一定的消耗,没有vector快。当我们中间插入时候,它的中间插入的时候需要挪动数据,与list相比也是有消耗的。
deque不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到 某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作 为stack和queue的底层数据结构。
我们通过发现deque其实是没有想象中那样完美的,它与vector和list相比是不够极致的。vector是吕布,list是诸葛亮,那么deque就是魏延。所以更多的时候我们更需要极致。
deque的底层实现是比较复杂的,不仅仅是上诉简单两句的问题。
根据上图,对于deque的维护是通过两个迭代器,start和finsh。因为daque是作为stack和queue的底层默认容器,一般来说deque是不需要进行中间插入的,那么start和finsh就很好的处理头插和尾插。它通过frist和last指向头尾,头插通过start的frist,如果满了node链接map新开辟buffer的指针位置。尾插通过finish的last控制。如果top()和back(),即通过start的cur和finish的cur控制。、
3.4deque的接口
通过stack,queue的接口与deque的接口对比,发现直接调用deque是非常适合充当stack,queue的默认容器。stack,queue就是直接调用deque的接口。
四、priority_queue-优先级队列
优先队列是一种容器适配器,而它实质就是堆。是否还记得堆是完全二叉树中用数组实现的,因为数组正好满足堆下标随机存取的需求,标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。相对deque,vector更加极致。priority_queue是默认大根堆。
4.1priority_queue的使用
priority_queue的使用来说也是比较简单的,接口也比较少。
函数声明 | 接口说明 |
priority_queue()/priority_queue(first, last) | 构造一个空的优先级队列 |
empty( ) | 检测优先级队列是否为空,是返回true,否则返回 false |
top( ) | 返回优先级队列中最大(最小元素),即堆顶元素 |
push(x) | 在优先级队列中插入元素x |
pop() | 删除优先级队列中最大(最小)元素,即堆顶元素 |
对于priority_queue的头文件,我们通过手册发现,priority_queue与queue都是一个头文件。
接口演示:
//默认大根堆
void test()
{
priority_queue<int> p;
p.push(7);
p.push(1);
p.push(9);
p.push(2);
p.push(3);
p.push(4);
while (!p.empty())
{
cout << p.top() << " ";
p.pop();
}
}
结果:9 7 4 3 2 1
小根堆 --greater
void test()
{
priority_queue<int, vector<int>, greater<int> > p;
p.push(7);
p.push(1);
p.push(9);
p.push(2);
p.push(3);
p.push(4);
while (!p.empty())
{
cout << p.top() << " ";
p.pop();
}
}
结果:1 2 3 4 7 9
4.2模拟实现
#pragma once
namespace qhx
{
template <class T,class Container = vector<int>>
class priority_queue
{
public:
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
:_con(first,last)
{
//建堆-推荐向下调整建堆,时间复杂度更小
for (size_t i = (_con.size() - 1 - 1) / 2; i >= 0; --i)//
{
adjust_down(i);
}
}
void adjust_up(size_t child)
{
size_t parent = (child - 1) / 2;
while (child > 0)
{
if (_con[parent] < con[child])
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);
}
void adjust_down(size_t parent)
{
size_t michild = parent * 2 + 1;
while (michild < _con.size())
{
if (michild< _con.size() && _con[michild]>_con[michild + 1])
{
michild++;
}
if ( _con[michild]>] < _con[parent])
{
swap(_con[michild], _con[parent]);
parent = michild;
michild = parent * 2 + 1;
}
else
{
break;
}
}
}
void pop()
{
swap(_con[0], _con(_con.size(-1)));
_con.pop_back();
adjust_down(0);
}
const T&top()const
{
return _con[0];
}
const empty()const
{
return _con.empty();
}
size_t size()const
{
return _con.size();
}
private:
Container _con;
};
};
如果对向上/向下调整忘记了的,就可以看下面图片回忆。
向上调整
向下调整
五、仿函数/函数对象
仿函数(functor),就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了。
5.1仿函数的实现
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
向下调整
五、仿函数/函数对象
仿函数(functor),就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了。
5.1仿函数的实现
[外链图片转存中…(img-77KzPWIu-1714697646802)]
[外链图片转存中…(img-baGCFPVm-1714697646802)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!