适配器是一种设计模式(代码设计经验),该种模式是把一个类的接口转换成用户希望的另一个接口。像本文介绍的容器底层其实是用其它容器实现的,提供给用户的接口只是对其它容器接口的简单封装。
stack是栈。
stack是一种容器适配器,特点是后进先出,其删除只能从容器的一端进行元素的插入与提取操作。
stack是一个类模板(template
(1)empty:判空操作
(2)back:获取尾部元素操作
(3)push_back:尾部插入元素操作
(4)pop_back:尾部删除元素操作
标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque(双端队列,后面再讲)。
常用接口说明:
函数 | 说明 |
---|---|
stack() | 构造函数,构造空栈 |
empty() | 判断栈是否为空,空返回真,否则返回假 |
size() | 返回栈中的元素个数 |
top() | 取到栈顶数据的引用 |
push(val) | 把元素val压入栈中 |
pop() | 弹出栈顶元素 |
练习:
最小栈
//这个题目的要点是记录最小值,但出栈后可能最小值可能变化,只用一个变量记录不够
//我们可以设计两个栈:
//(1)s栈,正常出入数据
//(2)min栈,在s栈入栈时进行判断,如果自身为空或者新入栈的元素小于等于min栈顶就入栈
//在s栈出栈时也进行判断,如果出栈的元素等于min栈顶,min也要出栈
//不过这个题目还有优化的空间,那就是[1,1,1,1,1]这样多个相同数的情况
//为避免空间浪费,可以设计一个count计数记录每个数,多次出现的数入、出栈只需要减计数
//计数归0才真正的出栈
class MinStack {
public:
MinStack()
{}
void push(int val)
{
if(min.empty() || min.top() >= val)
{
min.push(val);
}
s.push(val);
}
void pop()
{
if(s.top() == min.top())
{
min.pop();
}
s.pop();
}
int top()
{
return s.top();
}
int getMin()
{
return min.top();
}
private:
stack<int> s;
stack<int> min;
};
栈的弹出压入序列
//这个题目最好的办法就是模拟栈的压入弹出过程
//比如pushv[1,2,3,4,5]这个序列得到popv[4,5,3,2,1]
//先入栈s,1,1 != popV.top(),继续入栈
//入栈s,2, 2 != popV.top(),继续入栈
//入栈s,3,3 != popV.top(),继续入栈
//入栈s,4, 4 == popV.top(),同时出栈,popV是一个数组,下标加1视为出栈
//出栈结束栈s.top() = 3 != popV.top() = 5,继续入栈
//入栈, 5, 5 == popV.top(),同时出栈
//出栈结束s.top() = 3 == popV.top() = 3,同时出栈
//………………………………………………………………………………
//最后pushV的所有元素都入栈,并且s栈刚好出空,说明pushV可以得到popV
//如果pushV所有元素入栈,s栈没法出空,说明pushV无法得到popV
class Solution {
public:
bool IsPopOrder(vector<int>& pushV, vector<int>& popV)
{
size_t pushi = 0;
//popi下标加1视为popV出栈
size_t popi = 0;
stack<int> s;
while (pushi < pushV.size())
{
s.push(pushV[pushi++]);
while(!s.empty() && s.top() == popV[popi])
{
s.pop();
popi++;
}
}
return s.empty();
}
};
逆波兰表达式求值
//这个题目思路并不难,借助一个栈s即可
//(1)遇到数字:直接入s栈
//(2)遇到运算符,把栈中的两个左右数出栈,计算完把结果入s栈即可
//重复上面的步骤,一直到遍历完tokens即可,最后s栈顶即为结果
class Solution {
public:
int evalRPN(vector<string>& tokens)
{
stack<int> s;
for(auto str : tokens)
{
if(!(str == "+" || str == "-" || str == "*" || str == "/"))
{
s.push(atoi(str.c_str()));
}
else
{
int right = s.top(); s.pop();
int left = s.top(); s.pop();
switch(str[0])
{
case '+':
s.push(left + right);
break;
case '-':
s.push(left - right);
break;
case '*':
s.push(left * right);
break;
case '/':
s.push(left / right);
break;
}
}
}
return s.top();
}
};
queue是队列。
queue是一种容器适配器,特点为先进先出,其中从容器一端插入元素,另一端提取元素。
queue是一个类模板(template
(1)empty:检测队列是否为空
(2)size:返回队列中有效元素的个数
(3)front:返回队头元素的引用
(4)back:返回队尾元素的引用
(5)push_back:在队列尾部入队列
(6)pop_front:在队列头部出队列
标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。
使用要包含头文件
常用接口说明:
函数 | 说明 |
---|---|
queue() | 构造函数,构造一个空队列 |
empty() | 判断队列是否为空,空返回真,否则返回假 |
size() | 返回队列的元素个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push(val) | 队尾入元素val |
pop() | 队头元素出队 |
练习:
用队列实现栈
//这个题目的思路是有两个队列实现栈(其实也可以单队列实现,这里主要不是讲题目)
//需要始终有一个队列为空,比如[1,2,3]这个顺序队列
//队列q1为1 -> 2 - > 3,q2为空,top()只需要返回不为空的队列的队尾即可
//难点在出栈,出栈的话把1 -> 2转移到另一个队列q2,剩余一个元素即为栈顶,保存了再出栈即可
class MyStack {
public:
MyStack() {}
void push(int x) {
if(q2.empty())
q1.push(x);
else
q2.push(x);
}
int pop() {
//默认q1为空栈
queue<int>* empty_q = &q1;
queue<int>* non_empty_q = &q2;
if(q2.empty())
{
empty_q = &q2;
non_empty_q = &q1;
}
while(non_empty_q->size() > 1)
{
empty_q->push(non_empty_q->front());
non_empty_q->pop();
}
int ret = non_empty_q->front();
non_empty_q->pop();
return ret;
}
int top() {
if(q1.empty())
return q2.back();
else
return q1.back();
}
bool empty() {
return q1.empty() && q2.empty();
}
queue<int> q1;
queue<int> q2;
};
后面需要用到,就先简单介绍一下,以后会详细的讲各种用途。
//仿函数,又称函数对象,其实就是一个类,重载了operator()
// 使得类对象可以像函数一样使用
// 相比传函数指针,传仿函数要更加灵活一些
//开一个命名空间,和库里面的区分开
namespace my_compare
{
template<class T>
class less
{
public:
bool operator()(const T& x, const T& y)const
{
return x < y;
}
};
template<class T>
class greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
}
priority_queue(优先级队列)是堆。
优先级队列是一种容器适配器,如果根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
在优先级队列中可以随时插入元素,并且只能检索最大(小)元素(优先队列中位于顶部的元素)。
底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该支持随机访问(下标加[]),并支持以下操作:
(1)empty():检测容器是否为空
(2)size():返回容器中有效元素个数
(3)front():返回容器中第一个元素的引用
(4)push_back():在容器尾部插入元素
(5)pop_back():删除容器尾部元素
标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
需要支持随机访问,以便始终在内部保持堆结构。
priority_queue是一个模板类,template
使用前要包含
常用接口说明:
函数 | 说明 |
---|---|
priority_queue() | 构造一个空的优先级队列 |
priority_queue(begin(), end()) | 传迭代器区间初始化 |
empty() | 判断优先级队列是否为空,空返回真,否则返回假 |
top() | 返回优先级队列中最大(小)元素,即堆顶元素 |
push(val) | 在优先级队列中插入元素val |
pop() | 删除优选级队列中最大(小)元素,即堆顶元素 |
注意:
#include
#include
#include // greater算法的头文件
void TestPriorityQueue()
{
// 默认情况下,创建的是大堆,其底层按照小于号比较
vector<int> v{3,2,7,6,0,4,1,9,8,5};
priority_queue<int> q1;
for (auto& e : v)
q1.push(e);
cout << q1.top() << endl;
// 如果要创建小堆,将第三个模板参数换成greater比较方式
priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end());
cout << q2.top() << endl;
}
练习:
数组中的第K个最大元素
//对于取数组中前K个最大的元素(TOPK),很容易就能想到利用堆
//先把原数组构造一个堆,要求第K大的元素
//只需要pop() K - 1次即可,最后堆顶一定是第k个大的元素
//其中建堆的时间复杂度为O(N),K - 1次的删除后调整为 (K-1) * logN
//在 K比较小的情况下,时间复杂度接近于O(N)
class Solution {
public:
int findKthLargest(vector<int>& nums, int k)
{
//其实这个题目也可以利用快速排序,不过必须选择性的排序区间
priority_queue<int> p(nums.begin(), nums.end());
for(int i = 0; i < k - 1; i++)
{
p.pop();
}
return p.top();
}
};
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:
namespace my_std
{
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();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
container _con;
};
}
namespace my_std
{
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();
}
T& back()
{
return _con.back();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
container _con;
};
}
本文主要讲的不是堆相关的算法,如果大家对建堆和调整有疑问,可以看往期的堆讲解:
堆的实现
堆实际应用和时间复杂度分析
namespace my_std
{
//优先级队列
template<class T, class container = vector<T>, class comparation = less<T>>
class priority_queue
{
public:
//向上调整
void adjust_up(size_t child)
{
comparation com; //用于比较的仿函数
size_t parent = (child - 1) / 2;
while (child > 0)
{
// _con[parent] < _con[child]
//默认大堆,如果孩子大,就交换和父亲的值
//然后更新孩子和父亲的下标
if (com(_con[parent], _con[child]))
{
std::swap(_con[parent], _con[child]);
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 child = parent * 2 + 1;
comparation com;
while (child < _con.size())
{
//_con[child] < _con[child + 1]
//默认左孩子大,如果右孩子大就让孩子下标加1,注意右孩子不存在的情况
if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
{
child++;
}
//_con[parent] < _con[child]
//默认大堆。如果孩子大就和父亲交换,然后更新孩子下标和父亲下标
if (com(_con[parent], _con[child]))
{
std::swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void pop()
{
std::swap(_con.front(), _con.back());
_con.pop_back();
adjust_down(0);
}
//无参构造,这个必须写,不然就没有默认构造用了
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);
}
}
/// ///
T& top()
{
return _con.front();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
container _con;
};
}