目录
1.stack和queue应用
stack(栈):先进后出
queue(队列):先进先出
1.1stack的使用
函数说明 | 接口说明 |
stack() | 构造空栈 |
empty | 判断是否为空 |
size | 返回栈中有效数据个数 |
top | 返回栈顶元素 |
push | 将元素val压入栈中 |
pop | 将栈顶元素弹出 |
#include<stack>
#include<iostream>
using namespace std;
int main()
{
stack<int> s1;
s1.push(1);
s1.push(2);
s1.push(3);
s1.push(4);
while (!s1.empty())
{
cout << s1.top() << " ";
s1.pop();
}
return 0;
}
1.2stack的模拟实现
对于stack的模拟实现,从栈的接口中可以看出,栈实际是一种特殊的vector,因此使用vector完全可以模拟实现stack
#include<iostream>
#include<vector>
using namespace std;
namespace chuxin
{
template<class T>
class stack
{
public:
stack()
{}
void push(const T& x)
{
_s.push_back(x);
}
void pop() { _s.pop_back(); }
T& top() { return _s.back(); }
const T& top()const { return _s.back(); }
size_t size()const { return _s.size(); }
bool empty()const { return _s.empty(); }
private:
std::vector<T> _s;
};
}
1.3queue的使用
函数说明 | 接口说明 |
queue() | 构造空的队列 |
empty |
检测队列是否为空,是返回 true ,否则返回 false
|
size |
返回队列中有效元素的个数
|
front |
返回队头元素的引用
|
back |
返回队尾元素的引用
|
push |
在队尾将元素 val 入队列
|
pop |
将队头元素出队列
|
#include<queue>
#include<iostream>
using namespace std;
int main()
{
queue<int> s1;
s1.push(1);
s1.push(2);
s1.push(3);
s1.push(4);
cout << "s1.back()"<<" :";
while (!s1.empty())
{
cout<< s1.back() << " ";
s1.pop();
}
cout << endl;
s1.push(1);
s1.push(2);
s1.push(3);
s1.push(4);
cout << "s1.front()" << " :";
while (!s1.empty())
{
cout <<s1.front()<<" ";
s1.pop();
}
return 0;
}
1.4queue的模拟实现
因为 queue 的接口中存在头删和尾插,因此使用 vector 来封装效率太低,故可以借助 list 来模拟实
现 queue
#include <list>
namespace chuxin
{
template<class T>
class queue
{
public:
queue() {}
void push(const T& x) { _c.push_back(x); }
void pop() { _c.pop_front(); }
T& back() { return _c.back(); }
const T& back()const { return _c.back(); }
T& front() { return _c.front(); }
const T& front()const { return _c.front(); }
size_t size()const { return _c.size(); }
bool empty()const { return _c.empty(); }
private:
std::list<T> _c;
};
}
2.容器适配器
2.1什么是适配器
适配器是一种设计模式 ( 设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设
计经验的总结 ) ,该种模式是将一个类的接口转换成客户希望的另外一个接口, 虽然 stack 和 queue 中也可以存放元素,但在 STL 中并没有将其划分在容器的行列,而是将其称为 容器适配器 ,这是因为 stack 和队列只是对其他容器的接口进行了包装, STL 中 stack 和 queue 默认 使用 deque, 对于stack,queue在上文中,容器的类型已经写死了,stack只能用vector类初始化,queue只能用list初始化,此时就可以利用模版进行改动
stack:
#include<iostream>
#include<deque>
using namespace std;
namespace chuxin
{
template<class T, class Container = deque<T>>//适配多种容器
class stack
{
public:
stack()
{}
void push(const T& x)
{
_s.push_back(x);
}
void pop() { _s.pop_back(); }
T& top() { return _s.back(); }
const T& top()const { return _s.back(); }
size_t size()const { return _s.size(); }
bool empty()const { return _s.empty(); }
private:
Container _s;
};
}
queue:
#include <deque>
#include<iostream>
using namespace std;
namespace chuxin
{
template<class T, class Container = deque<T>>
class queue
{
public:
queue() {}
void push(const T& x) { _c.push_back(x); }
void pop() { _c.pop_front(); }
T& back() { return _c.back(); }
const T& back()const { return _c.back(); }
T& front() { return _c.front(); }
const T& front()const { return _c.front(); }
size_t size()const { return _c.size(); }
bool empty()const { return _c.empty(); }
private:
Container _c;
};
}
2.2 deque介绍
在上文,发现stack和queue容器的缺省值是deque,为什么用这个容器呢?
优点:
deque( 双端队列 ) :是一种双开口的 " 连续 " 空间的数据结构 ,双开口的含义是:可以在头尾两端 进行插入和删除操作,且时间复杂度为O(1) ,与 vector 比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高,相当于vector和list的缝合怪
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 类似于动态的二维数组,deque的插入,删除则涉及它的迭代器,由于deque的迭代器较复杂本文不做叙述
缺陷:
与 vector 比较 , deque 的优势是:头部插入和删除时, 不需要搬移元素,效率特别高 ,而且在 扩容时,也不需要搬移大量的元素,因此其效率是必 vector 高的。与 list 比较 ,其底层是连续空间, 空间利用率比较高 ,不需要存储额外字段。但是, deque 有一个致命缺陷:不适合遍历,因为在遍历时, deque 的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此 在实 际中,需要线性结构时,大多数情况下优先考虑vector 和 list , deque 的应用并不多,而 目前能看到的一个应用就是,STL 用其作为 stack 和 queue 的底层数据结构 。
2.3 list和vector的区别
vector 与 list 都是 STL 中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及
应用场景不同,其主要不同如下:
vector
|
list
|
|
底 层 结 构
|
动态顺序表,一段连续空间
|
带头结点的双向循环链表
|
随 机 访 问
|
支持随机访问,访问某个元素效率 O(1)
|
不支持随机访问,访问某个元素效率 O(N)
|
插 入
和 删 除
|
任意位置插入和删除效率低,需要搬移元素,时间 复杂度为 O(N) ,插入时有可能需要增容,增容: 开辟新空间,拷贝元素,释放旧空间,导致效率更 低
|
任意位置插入和删除效率高, 不需要搬移元素,时间复杂度 为 O(1)
|
空 间
利 用
率
|
任意位置插入和删除效率高, 不需要搬移元素,时间复杂度 为 O(1)
|
底层节点动态开辟,小节点容
易造成内存碎片,空间利用率
低,缓存利用率低
|
迭 代
器
|
原生态指针
|
对原生态指针 ( 节点指针 ) 进行
封装
|
迭 代
器 失
效
|
在插入元素时,要给所有的迭代器重新赋值,因为 插入元素有可能会导致重新扩容,致使原来迭代器 失效,删除时,当前迭代器需要重新赋值否则会失 效
|
插入元素不会导致迭代器失
效,删除元素时,只会导致当
前迭代器失效,其他迭代器不
受影响
|
使 用
场 景
|
需要高效存储,支持随机访问,不关心插入删除效 率
|
大量插入和删除操作,不关心
随机访问
|
3.priority_queue
3.1priority_queue的使用
priority_queue的使用和queue的一样,不同的是他们的底层的一些结构,以及处理数据不同,
优先级队列默认使用 vector 作为其底层存储数据的容器,在 vector 上又使用了堆算法将 vector 中 元素构造成堆的结构,因此 priority_queue 就是堆,所有需要用到堆的位置,都可以考虑使用 priority_queue 。注意:默认情况下 priority_queue 是大堆 。
函数声明 | 接口说明 |
priority_queue()
|
构造一个空的优先级队列
|
priority_queue(first, last) | 利用迭代器区间建立 |
empty |
检测优先级队列是否为空,是返回true,否
则返回false
|
top |
返回优先级队列中最大(最小元素),即堆顶元
素
|
push |
在优先级队列中插入元素x
|
pop |
删除优先级队列中最大(最小)元素,即堆顶元
素
|
#include<queue>
#include<iostream>
#include<list>
using namespace std;
int main()
{
priority_queue<int> s1;
s1.push(5);
s1.push(3);
s1.push(7);
s1.push(2);
s1.push(6);
while (!s1.empty())
{
cout <<s1.top()<<" ";
s1.pop();
}
return 0;
}
3.2 priority_queue的模拟实现
priority_queue的底层就是一个堆,因此它的模拟实现,和堆的类似
#include<vector>
namespace chuxin
{
// 默认是大堆
template<class T, class Container = vector<T>>
class priority_queue
{
public:
void AdjustUp(int child)
{
int 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);
AdjustUp(_con.size() - 1);
}
void AdjustDown(int parent)
{
// 先假设左孩子小
size_t child = parent * 2 + 1;
while (child < _con.size()) // child >= n说明孩子不存在,调整到叶子了
{
// 找出小的那个孩子
if (child + 1 < _con.size() && _con[child] < _con[child + 1])
{
++child;
}
if (_con[parent] < _con[child])
{
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
const T& top()
{
return _con[0];
}
size_t size() const
{
return _con.size();
}
bool empty() const
{
return _con.empty();
}
private:
Container _con;
};
}
这种方法还是有缺陷,发现只能排降序,有没有一种方法既能排升序又能排降序?这时候就需要仿函数了
3.3 仿函数
来看看下面这段冒泡排序
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n; j++)
{
// 单趟
int flag = 0;
for (int i = 1; i < n - j; i++)
{
if (a[i] < a[i - 1])
{
swap(a[i - 1], a[i]);
flag = 1;
}
}
if (flag == 0)
{
break;
}
}
}
发现它的逻辑是排一个升序,如果要排一个降序怎么办,为了一个比较重载一个大部分相似的函数,显然不怎么好,而且比较的数据可能不同,有时候比较整形,有时候是浮点型,这时候就可以利用模版自己写一个比较函数,而这个函数称为仿函数
// 仿函数:本质是一个类,这个类重载operator(),他的对象可以像函数一样使用
template<class T>
class Less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
template<class T>
class Greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
template<class Compare>
void BubbleSort(int* a, int n, Compare com)
{
for (int j = 0; j < n; j++)
{
// 单趟
int flag = 0;
for (int i = 1; i < n - j; i++)
{
//if (a[i] < a[i - 1])
if (com(a[i], a[i - 1]))
{
swap(a[i - 1], a[i]);
flag = 1;
}
}
if (flag == 0)
{
break;
}
}
}
int main()
{
Less<int> LessFunc;
Greater<int> GreaterFunc;
// 函数对象
cout << LessFunc(1, 2) << endl;
cout << LessFunc.operator()(1, 2) << endl;
int a[] = { 9,1,2,5,7,4,6,3 };
BubbleSort(a, 8, LessFunc);
BubbleSort(a, 8, GreaterFunc);
BubbleSort(a, 8, Less<int>());
BubbleSort(a, 8, Greater<int>());
return 0;
}
此时就发现,改变是否是升降序,就可以利用两个类对象就可以做到,此时priority_queue的比较就是这样
#include<vector>
template<class T>
class Less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
template<class T>
class Greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
namespace chuxin
{
// 默认是大堆
template<class T, class Container = vector<T>, class Compare = Less<T>>
class priority_queue
{
public:
void AdjustUp(int child)
{
Compare com;
int parent = (child - 1) / 2;
while (child > 0)
{
//if (_con[parent] < _con[child])
if (com(_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);
AdjustUp(_con.size() - 1);
}
void AdjustDown(int parent)
{
// 先假设左孩子小
size_t child = parent * 2 + 1;
Compare com;
while (child < _con.size()) // child >= n说明孩子不存在,调整到叶子了
{
// 找出小的那个孩子
//if (child + 1 < _con.size() && _con[child] < _con[child + 1])
if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
{
++child;
}
//if (_con[parent] < _con[child])
if (com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
const T& top()
{
return _con[0];
}
size_t size() const
{
return _con.size();
}
bool empty() const
{
return _con.empty();
}
private:
Container _con;
};
}