文章目录
1.stack
简介
严格来说,stack和queue不能称之为容器,他们只是容器适配器。
- stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
- stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
- stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
empty:判空操作
back:获取尾部元素操作
push_back:尾部插入元素操作
pop_back:尾部删除元素操作 - 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。
常用接口
函数说明 | 接口说明 |
---|---|
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push() | 将元素val压入stack中 |
pop() | 将stack中尾部的元素弹出 |
void test_stack1()
{
stack<int> s;
s.push(1);
s.push(2);
s.push(3);
s.push(4);
while (!s.empty())
{
cout << s.top() << "-";
cout << s.size() << " ";
s.pop();
}
cout << endl; // 4-4 3-3 2-2 1-1
}
// 也可以使用特定的适配器去定义栈
stack<int, vector<int>> st2;
stack<int, list<int>> st3;
模拟实现
之前我们自己实现的栈是用数组实现的,那么这里同理,我们也是需要选择相应的载体来实现。
#pragma once
#include <iostream>
#include <stack>
#include <queue>
#include <functional>
#include <vector>
#include <string>
#include <list>
#include <deque>
using namespace std;
namespace yzq
{
template<class T, class Container = deque<T>>
class stack
{
public:
// 由于是自定义类型,构造析构什么的用默认的就够用了。
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
// 对于栈,插入数据就是尾插
void push(const T& val)
{
_con.push_back(val);
}
// 对于栈,删除数据就是尾删
void pop()
{
_con.pop_back();
}
const T& top()
{
return _con.back();
}
private:
Container _con;
};
void test_stack1()
{
//stack<int> s;
//stack<int, vector<int>> s;
//stack<int, list<int>> s;
stack<int, string> s; // string内部是char 可能会发生数据截断
s.push(1);
s.push(2);
s.push(3);
s.push(4);
s.push(128); // 超过127就会截断 -128
s.push(255); // -1
s.push(256); // 0
while (!s.empty())
{
cout << s.top() << "-";
cout << s.size() << " ";
s.pop();
}
cout << endl;
}
}
当然,用string可以,但要考虑数据截断的问题。
2.queue
简介
- 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
- 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
- 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
empty:检测队列是否为空
size:返回队列中有效元素的个数
front:返回队头元素的引用
back:返回队尾元素的引用
push_back:在队列尾部入队列
pop_front:在队列头部出队列 - 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。vector不适配。
常用接口
函数说明 | 接口说明 |
---|---|
queue() | 构造空的队列 |
empty() | 检测queue是否为空 |
size() | 返回queue中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val压入queue |
pop() | 出队头元素 |
void test_queue1()
{
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
while (!q.empty())
{
cout << q.front() << "-";
cout << q.size() << " ";
q.pop();
}
cout << endl; // 1-4 2-3 3-2 4-1
}
模拟实现
队列不支持vector
适配,因为vector
没有pop_front
的接口。
#pragma once
#include <iostream>
#include <stack>
#include <queue>
#include <functional>
#include <vector>
#include <string>
#include <list>
#include <deque>
using namespace std;
namespace yzq
{
template<class T, class Container = deque<T>>
class queue
{
public:
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_front();
}
const T& front()
{
return _con.front();
}
const T& back()
{
return _con.back();
}
private:
Container _con;
};
void test_queue1()
{
queue<int> q;
//queue<int, list<int>> q;
//queue<int, vector<int>> q; // 不能用vector适配 没有pop_front
q.push(1);
q.push(2);
q.push(3);
q.push(4);
while (!q.empty())
{
cout << q.front() << "-";
cout << q.size() << " ";
q.pop();
}
cout << endl; // 1-4 2-3 3-2 4-1
}
}
3.priority_queue
简介
- 优先级队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
- 类似于堆(底层就是大堆),在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。
- 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。
- 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
empty():检测容器是否为空
size():返回容器中有效元素个数
front():返回容器中第一个元素的引用
push_back():在容器尾部插入元素
pop_back():删除容器尾部元素 - 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
- 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。
常用接口
函数声明 | 接口说明 |
---|---|
priority_queue() / priority_queue(first, last) | 构造空的优先级队列 |
empty | 检测优先级队列是否为空 |
top() | 返回优先级队列中最大(最小元素),即堆顶元素 |
push(x) | 在优先级队列中插入元素x |
pop() | 删除优先级队列中最大(最小)元素,即堆顶元素 |
void test_priority_queue1()
{
priority_queue<int> pq;
pq.push(2);
pq.push(5);
pq.push(1);
pq.push(6);
pq.push(8);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl; //8 6 5 2 1
}
优先级队列默认传的是less仿函数,大的优先级高,底层是大堆,要想让小的优先级高,传greater仿函数,底层是小堆。
这里应该算是设计的失误,非常反直觉。
类比
sort less < 升序
sort greater > 降序
void test_priority_queue1()
{
priority_queue<int, vector<int>, greater<int>> pq;
pq.push(2);
pq.push(5);
pq.push(1);
pq.push(6);
pq.push(8);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl; /
}
模拟实现
仿函数
也叫做函数对象,它的对象可以像调用函数那样去使用。
其实可以理解成是为了替代C语言中烦人的函数指针才搞出来的。
举个函数指针的例子:
在早年的源码里面,到处都是这样的代码:
这个函数参数是个函数指针void (*f)()
,返回值也是一个函数指针void (* )()
// 最简单的仿函数
struct less{
bool operator()(int x, int y)
{
return x < y;
}
}
int main()
{
less lessCompare;
cout << lessCompare(1, 2) << endl; // 使用很像函数,因此叫仿函数
}
// 支持泛型
template <class T>
struct less
{
bool operator()(const T& x, const T& y) const //支持const对象调用
{
return x < y;
}
};
关于仿函数:
A.仿函数是模板函数,可以根据不同的类型代表不同的状态
B.仿函数是模板函数,可以有不同类型
C.仿函数是模板函数,其速度比一般函数要慢,故错误
D.仿函数在一定程度上使代码更通用,本质上简化了代码
堆的调整
堆的调整中涉及大小比较的,就用仿函数去比较。
仿函数类型不同,就能相应控制大小堆。
less控制大堆,greater控制小堆
// 底层是个堆,默认是大堆
void AdjustUp(int child)
{
int parent = (child - 1) / 2;
// parent始终>=0,不能用parent判断 (0-1)/2 = 0
while (child > 0)
{
//if (_con[parent] < _con[child])
if (_comFunc(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
// 更新孩子、父亲
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void AdjustDown(int parent)
{
int child = parent * 2 + 1;
while (child < _con.size())
{
// 判断右孩子是否更大
// 注意避免右孩子越界
//if (child + 1 < _con.size() && _con[child] < _con[child + 1])
if (child + 1 < _con.size() && _comFunc(_con[child], _con[child + 1]))
{
++child;
}
//if (_con[parent] < _con[child])
if (_comFunc(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// Container需要支持随机访问
// Compare是模板类型,也叫虚拟类型
template<class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue
{
private:
Compare _comFunc;
Container _con;
public:
// 构造函数这么写是为了兼容C的函数指针
// bool*()(int, int)是内置类型,通过默认构造传过来是空指针,因此传参还得传一个函数的地址过来,让函数指针有具体的指向
// 如果不通过构造函数完成初始化,那么_comFunc是随机值
priority_queue(const Compare& comFunc = Compare()) // 缺省参数是Compare调用自己的构造函数完成
:_comFunc(comFunc)
{}
// 底层是个堆,默认是大堆
void AdjustUp(int child)
{
//。。。
}
void AdjustDown(int parent)
{
//。。。
}
void push(const T& x)
{
_con.push_back(x);
// 插完数据需要调整建堆
AdjustUp(_con.size() - 1);
}
void pop()
{
assert(!_con.empty());
swap(_con[0], _con[size() - 1]);
_con.pop_back();
AdjustDown(0); //从根节点开始调整
}
const T& top()
{
return _con[0];
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
};
void test_priority_queue2()
{
priority_queue<int, vector<int>, greater<int>> pq; // 小堆
//priority_queue<int> pq; // 大堆
//priority_queue<int, vector<int>, bool(*)(int, int)> pq(ComIntGreater); // 将函数地址传过去,让函数指针有具体的函数指向
pq.push(2);
pq.push(5);
pq.push(1);
pq.push(6);
pq.push(8);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
}
仿函数取greater
时,建的是小堆,每次取堆顶的数出来,就变成升序了。
仿函数默认是less
,建的是大堆,降序。
C++中,如果我们非要去玩函数指针呢?也是有办法的!借助构造函数。
快速建堆
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last
, const Compare& comFunc = Compare())
: _comFunc(comFunc)
{
while (first != last)
{
_con.push_back(*first);
++first;
}
// 插完数据需要建堆 每插一个往下调整
// 从最后一个非叶子节点开始
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(i);
}
}
void test_priority_queue3()
{
int a[] = { 1,3,4,2,1,6 };
// 借助优先级队列快速建堆,默认大堆
priority_queue<int> pq(a, a + 6);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl; // 6 4 3 2 1 1
}
void test_priority_queue3()
{
int a[] = { 1,3,4,2,1,6 };
// 借助优先级队列快速建堆,默认大堆
//priority_queue<int> pq(a, a + 6);
priority_queue<int, vector<int>, greater<int>> pq(a, a + 6);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
}
源代码
template <class T>
struct less
{
bool operator()(const T& x, const T& y) const //支持const对象调用
{
return x < y;
}
};
template <class T>
struct greater
{
bool operator()(const T& x, const T& y) const
{
return x > y;
}
};
bool ComIntLess(int x, int y)
{
return x < y;
}
bool ComIntGreater(int x, int y)
{
return x > y;
}
// Container需要支持随机访问
template<class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue
{
private:
Compare _comFunc;
Container _con;
public:
priority_queue(const Compare& comFunc = Compare()) // 缺省参数是Compare调用自己的构造函数完成
:_comFunc(comFunc)
{}
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last
, const Compare& comFunc = Compare())
: _comFunc(comFunc)
{
while (first != last)
{
_con.push_back(*first);
++first;
}
// 插完数据需要建堆 每插一个往下调整
// 从最后一个非叶子节点开始
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(i);
}
}
// 底层是个堆,默认是大堆
void AdjustUp(int child)
{
int parent = (child - 1) / 2;
// parent始终>=0,不能用parent判断 (0-1)/2 = 0
while (child > 0)
{
//if (_con[parent] < _con[child])
if (_comFunc(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
// 更新孩子、父亲
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void AdjustDown(int parent)
{
int child = parent * 2 + 1;
while (child < _con.size())
{
// 判断右孩子是否更大
// 注意避免右孩子越界
//if (child + 1 < _con.size() && _con[child] < _con[child + 1])
if (child + 1 < _con.size() && _comFunc(_con[child], _con[child + 1]))
{
++child;
}
//if (_con[parent] < _con[child])
if (_comFunc(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void push(const T& x)
{
_con.push_back(x);
// 插完数据需要调整建堆
AdjustUp(_con.size() - 1);
}
void pop()
{
assert(!_con.empty());
swap(_con[0], _con[size() - 1]);
_con.pop_back();
AdjustDown(0); //从根节点开始调整
}
const T& top()
{
return _con[0];
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
};
sort
默认传的是less
,排升序
void test_funtional()
{
std::vector<int> v;
v.push_back(2);
v.push_back(1);
v.push_back(5);
v.push_back(3);
v.push_back(4);
for (auto e : v)
{
cout << e << " ";
}
cout << endl; // 2 1 5 3 4
// less
sort(v.begin(), v.end());
for (auto e : v)
{
cout << e << " ";
}
cout << endl; // 1 2 3 4 5
// 这里需要传的是greater对象,为了简便直接传个匿名对象
sort(v.begin(), v.end(), greater<int>());
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
// 指向数组的原生指针,本身就是天然的迭代器,这也要求底层是连续的物理空间
int a[] = { 1,3,7,5,3,2,1 };
for (auto e : a)
{
cout << e << " ";
}
cout << endl;
sort(a, a + 7);
for (auto e : a)
{
cout << e << " ";
}
cout << endl;
sort(a, a + 7, greater<int>());
for (auto e : a)
{
cout << e << " ";
}
cout << endl;
}
注意:优先级队列的时候,传的是类型,而sort传的则是对象
sort(v.begin(), v.end(), greater<int>());
priority_queue<int, vector<int>, greater<int>> pq;
针对自定义类型也是可以的,只不过需要我们自己写仿函数。
// 商品
struct Goods
{
string _name;
double _price;
size_t _saleNum;
//...
bool operator<(const Goods& g) const
{
return _price < g._price;
}
};
void test_funtional2()
{
Goods gds[4] = { { "苹果", 2.1, 1000}, { "香蕉", 3.0, 200}, { "橙子", 2.2,300}, { "菠萝", 1.5,50} };
sort(gds, gds + 4); // 默认是用的operator<进行比较,需要我们手动实现一个
}
如果有多种排序需求时,operator< 怎么重载呢?
实现多个仿函数!
struct LessPrice
{
bool operator()(const Goods& g1, const Goods& g2) const
{
return g1._price < g2._price;
}
};
struct GreaterPrice
{
bool operator()(const Goods& g1, const Goods& g2) const
{
return g1._price > g2._price;
}
};
struct LessSaleNum
{
bool operator()(const Goods& g1, const Goods& g2) const
{
return g1._saleNum < g2._saleNum;
}
};
struct GreaterSaleNum
{
bool operator()(const Goods& g1, const Goods& g2) const
{
return g1._saleNum > g2._saleNum;
}
};
void test_funtional2()
{
Goods gds[4] = { { "苹果", 2.1, 1000}, { "香蕉", 3.0, 200}, { "橙子", 2.2,300}, { "菠萝", 1.5,50} };
sort(gds, gds + 4, LessPrice());
sort(gds, gds + 4, GreaterPrice());
sort(gds, gds + 4, LessSaleNum());
sort(gds, gds + 4, GreaterSaleNum());
}
4.适配器
定义
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总
结),该种模式是将一个类的接口转换成客户希望的另外一个接口。
适配器的核心就是转换。
deque
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维
数组,其底层结构如下图所示:
关于vector、list
优缺点:
deque
就是为了结合vector、list
的优点应运而生。
不过也正是deque
的两头都沾一点,优点不够极致,因此外强中干。
我们来测试一下deque
的随机访问效率👇
void testOP()
{
srand(time(0));
const int N = 10000000;
vector<int> v;
v.reserve(N);
deque<int> dq1;
deque<int> dq2;
for (int i = 0; i < N; ++i)
{
auto e = rand();
v.push_back(e);
dq1.push_back(e);
dq2.push_back(e);
}
int begin1 = clock();
sort(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
sort(dq1.begin(), dq1.end());
int end2 = clock();
printf("vector Sort:%d\n", end1 - begin1);
printf("deque Sort:%d\n", end2 - begin2);
}
release版本下:
即使vector不提前开辟N个空间,效率也比较高:
void testOP2()
{
srand(time(0));
const int N = 10000000;
vector<int> v;
v.reserve(N);
deque<int> dq1;
deque<int> dq2;
for (int i = 0; i < N; ++i)
{
auto e = rand();
v.push_back(e);
dq1.push_back(e);
dq2.push_back(e);
}
int begin1 = clock();
sort(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
// 拷贝给vector,排序后再拷贝回来,对比效率
vector<int> copy(dq1.begin(), dq1.end());
sort(copy.begin(), copy.end());
dq1.assign(copy.begin(), copy.end());
int end2 = clock();
printf("vector Sort:%d\n", end1 - begin1);
printf("deque Sort:%d\n", end2 - begin2);
}
拷贝过去排序好再拷贝回来,都比直接自己排序快。
实际中,只有在大量头尾插入删除,偶尔随机访问的场景中,才需要用到deque。
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的优点,而完美的避开了其缺陷。
反向迭代器
反向迭代器也是一个适配器模式。
跟正向迭代器相比,除了++ --
时方向相反,其他操作基本一致。
每一种容器我们都实现了各自的正向迭代器,难道反向迭代器还是要再重新再实现一遍吗?
能不能利用已有的正向迭代器,适配出来呢?
stl_iterator.h
看一看库里如何实现。
int main()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
list<int>::reverse_iterator rit = lt.rbegin();
while(rit != lt.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl; // 4 3 2 1
}
注意operator* 取的是当前迭代器的前一个位置。
当rit指向头结点, *rit指向的是4,然后++rit,rit指向4
*rit指向的是3,继续++rit,rit指向3
*rit指向的是2,++rit,rit指向2
*rit指向的是1,++rit,rit指向1,也就是rend()。循环结束
// ReverseIterator.h
#pragma once
namespace yzq
{
// 当适配list时,list传过来的参数是iterator 也就是Node*的封装
// 也就是_it 也是一个结点指针
template<class Iterator, class Ref, class Ptr>
struct Reverse_iterator
{
Iterator _it;
typedef Reverse_iterator<Iterator, Ref, Ptr> Self;
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;
}
};
}
vector、list相同的。
rbegin就是end,rend就是begin
void test_list8()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
list<int>::reverse_iterator rit = lt.rbegin();
while (rit != lt.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl; // 4 3 2 2 1
}
尾声
🌹🌹🌹
写文不易,如果有帮助烦请点个赞~ 👍👍👍
Thanks♪(・ω・)ノ🌹🌹🌹
😘😘😘
👀👀由于笔者水平有限,在今后的博文中难免会出现错误之处,本人非常希望您如果发现错误,恳请留言批评斧正,希望和大家一起学习,一起进步ヽ( ̄ω ̄( ̄ω ̄〃)ゝ,期待您的留言评论。
附GitHub仓库链接