目录
1. stack的介绍和使用
1.1 stack的介绍
1.stack是一种容器适配器(适配器的本质是一种复用,不用从头实现),专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
2.stack是作为容器适配器被发现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部被压入和弹出。
3.stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类支持以下操作:
- empty:判空
- back:获取尾部元素
- push_back:尾插
- pop_back:尾删
不支持随便访问,所以没有迭代器。
4.标准容器vector、deque、list均符号这些要求,默认情况下,如果没有为stack指定特定的底层容器,默认使用deque。
1.2 stack的使用
函数说明 | 接口说明 |
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push() | 尾插 |
pop() | 尾删 |
1.3 举个例子:最小栈
请你设计一个 最小栈 。它提供 push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack
类:
MinStack()
初始化堆栈对象。void push(int val)
将元素val推入堆栈。void pop()
删除堆栈顶部的元素。int top()
获取堆栈顶部的元素。int getMin()
获取堆栈中的最小元素。
#include <iostream>
#include <stack>
using namespace std;
class MinStack
{
public:
/** initialize your data structure here. */
MinStack()
= default;
void push(int x)
{
if(_min.empty() || x <= _min.top())
{
_min.push(x);
}
_elem.push(x);
}
void pop()
{
if(_elem.top() == _min.top())
{
_min.pop();
}
_elem.pop();
}
int top()
{
return _elem.top();
}
int getMin()
{
return _min.top();
}
private:
std::stack<int> _elem;
std::stack<int> _min;
};
- _elem保存栈中的所有元素,_min保存栈的较小值。
- push函数中:只要是压栈,先将元素保存到_elem中,如果_min为空或者这个值小于等于此时_min的栈顶元素,那么也入进去,小于很好理解,为什么等于也要入呢?
- 比如我的_elem先后入了两个相同最小值1,如果没有等号,_min中只有一个1,在出栈时,_elem 和 _min 各自出了一个1,这时计算栈的最小值,本应还是最小值1,但是_min 里已经没有1了,所以要写小于等于。
1.4 栈的弹出压入序列
给定 pushed
和 popped
两个序列,每个序列中的 值都不重复,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返回 true
;否则,返回 false
。
示例 1:
输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1] 输出:true 解释:我们可以按以下顺序执行: push(1), push(2), push(3), push(4), pop() -> 4, push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
示例 2:
输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2] 输出:false 解释:1 不能在 2 之前弹出。
思路:肯定要先有一个栈去接受pushed中的数据,这里设置成st。入了以后,比较出栈序列的第一个元素,如果不相等,则继续按pushed的序列入栈(1对4,2对4,3对4);如果相等(4对4)则将元素4出栈,popi加1,对比当前栈中的值和第二个popped中的元素,此时仍有相等与不相等两种情况。。。
class Solution
{
public:
static bool validateStackSequences(vector<int>& pushed, vector<int>& popped)
{
stack<int> st;
int pushi = 0;
int popi = 0;
while(pushi < pushed.size())
{
st.push(pushed[pushi++]);
while(!st.empty() && st.top() == popped[popi])
{
st.pop();
++popi;
}
}
return st.empty();
}
};
1.5 逆波兰表达式求值
LCR 036. 逆波兰表达式求值 - 力扣(LeetCode)
根据 逆波兰表示法,求该后缀表达式的计算结果。
有效的算符包括 +
、-
、*
、/
。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
- 整数除法只保留整数部分。
- 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
思路:1.若是操作数,入栈。2.若是操作符,取栈中前两个数作为左右操作数,进行运算,运算结果入栈。
class Solution1
{
public:
int evalRPN(vector<string>& tokens)
{
std::stack<int> st;
for(int i=0; i<tokens.size(); i++)
{
string& str = tokens[i];
if(!(str=="+" || str=="-" || str=="*" || str=="/"))
{
st.push(atoi(str.c_str()));
}
else
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
switch(str[0])
{
case '+':
st.push(left+right);
break;
case '-':
st.push(left-right);
break;
case '*':
st.push(left*right);
break;
case '/':
st.push(left/right);
break;
}
}
}
return st.top();
}
};
class Solution2
{
public:
int evalRPN(vector<string>& tokens)
{
stack<int> st;
map<string, function<int(int, int)>> funcmap =
{
{"+", [](int i, int j){return i+j;}},
{"-", [](int i, int j){return i-j;}},
{"*", [](int i, int j){return i*j;}},
{"/", [](int i, int j){return i/j;}}
};
for(auto& str: tokens)
{
if(funcmap.find(str) != funcmap.end())//在funcmap中找到了运算符
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
st.push(funcmap[str](left, right));
}
else
{
st.push(stoi(str));
}
}
return st.top();
}
};
在判断字符串是否为数字时,不能使用str[0],要使用str=="+" || str=="-" || str=="*" || str=="/",用字符串直接判断,而不用其中字符,因为如果字符串是"-11",str[0]是符号,但应该是个数字。
- st.push(atoi(str.c_str()));为什么要使用str.c_str()?
str
是一个 std::string
类型的对象。c_str()
方法返回 str
中底层的 C 风格字符串指针 (const char*
),供需要使用 C 风格字符串的函数(如 atoi
)调用。
注意事项:
-
atoi
的安全性:
atoi
不会检查输入字符串是否合法。如果字符串无法被转换为整数(例如为空字符串或非数字字符),会返回 0。因此建议使用更安全的std::stoi
-
效率:
std::stoi
比atoi
更现代化,并支持面向 C++ 的编程风格,因此推荐用std::stoi
。
st.push(std::stoi(str)); // 替代 atoi 的用法
1.6 用栈实现队列
class MyQueue
{
stack<int> st1;
stack<int> st2;
public:
MyQueue()
{}
void push(int x)
{
st1.push(x);
return;
}
int pop()
{
if(st2.empty())//注意先判空,如果要出的不为空,别入,先把该出的出了
{
while(!st1.empty())
{
int tmp = st1.top();
st2.push(tmp);
st1.pop();
}
}
int ret = st2.top();
st2.pop();
return ret;
}
int peek()
{
if(st2.empty())//同理
{
while(!st1.empty())
{
int tmp = st1.top();
st2.push(tmp);
st1.pop();
}
}
return st2.top();
}
bool empty()
{
return st1.empty() && st2.empty();
}
};
1.7 括号匹配问题 20. 有效的括号
class Solution
{
public:
bool isValid(string s)
{
if(s.size()%2 != 0)
return false;
stack<char> st;
for(auto ch : s)
{
if(ch == '(' || ch == '{' || ch == '[')
st.push(ch);
else
{
if(st.empty())
return false;
if(ch == ')' && st.top() != '(')
return false;
else if(ch == ']' && st.top() != '[')
return false;
else if(ch == '}' && st.top() != '{')
return false;
else
st.pop();
}
}
return st.empty();
}
};
1.8 stack模拟实现
namespace zzy
{
template<class T, class Container>
class stack
{
public:
void push(const T& t)
{
_con.push_back(t);
}
void pop()
{
_con.pop_back();
}
T& top()
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
Container _con;
};
}
T为栈中存的数据的类型,Container是用的什么容器。上述代码存在Stack.h中,当我们在一个测试文件中写测试代码时,为了确保代码的正确性和可维护性,建议将 #include "stack.h" 放在 using namespace std; 之前。
因为_con是自定义类型,不用在public中提供拷贝构造,析构等,会调用某种容器自己的那些函数。
#include <iostream>
#include <vector>
#include "Stack.h"
using namespace std;
void teststack()
{
zzy::stack<int, vector<int>> s;
s.push(1);
s.push(2);
s.push(3);
s.push(4);
s.push(5);
while (!s.empty())
{
cout << s.top() << " ";
s.pop();
}
cout << endl;
zzy::stack<int,list<int>> s2;
}
int main()
{
teststack();
return 0;
}
每次创建一个新的栈都要给容器的类型,有点烦,库中是用缺省值来解决的。
2.queue的介绍和使用
2.1 queue的介绍
1.队列是一种容器适配器,专用于先进先出操作,从容器一端插入元素,另一端提取元素。
2.底层容器可以是标准容器类模板之一,亦可以是其他专门设计的容器类,该容器应至少支持以下操作:
- empty:检测队列是否为空
- size:返回队列中有效元素个数
- front:返回队头元素的引用
- back:返回队尾元素的引用
- push_back:在队列尾部入队列
- pop_back:在队列头部出队列
3.标准容器类deque和list满足这些要求,默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。
2.2 queue的使用
函数声明 | 接口说明 |
queue() | 构造空的队列 |
empty() | 检测队列是否为空,bool返回 |
size() | 返回队列中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队 |
pop() | 将队头元素出队列 |
2.3 queue的模拟实现
因为queue的接口中存在头删和尾插,如果用vector来封装效率太低,故可以用list来模拟实现queue
#include <list>
namespace zzy
{
template<class T>
class queue
{
public:
queue() = default;
void push(const T &t)
{
_c.push_back(t);
}
void pop()
{
_c.pop_front();
}
T& front()
{
return _c.front();
}
T& back()
{
return _c.back();
}
bool empty()
{
return _c.empty();
}
size_t size()
{
return _c.size();
}
private:
std::list<T> _c;
};
}
2.4 用队列实现栈225. 用队列实现栈
class MyStack
{
queue<int> q1;
queue<int> q2;
public:
MyStack()
{}
void push(int x)
{
if(q1.empty())
q1.push(x);
else
{
q2.push(x);
while(!q1.empty())
{
q2.push(q1.front());
q1.pop();
}
swap(q1, q2);
}
}
int pop()
{
int tmp = q1.front();
q1.pop();
return tmp;
}
int top()
{
return q1.front();
}
bool empty()
{
return q1.empty();
}
};
3. priority_queue的介绍和使用
3.1 优先队列的介绍
1. 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
2.类似于堆,默认是大堆,在堆中可以随时插入元素,并且只能检索最大堆元素
3.底层容器应该可以通过随机访问迭代器访问,并支持以下操作:
- empty()
- size()
- front()
- push_back()
- pop_back()
4.标准容器类vector和deque满足这些要求,默认情况下,使用vector
3.2 优先队列的使用
优先队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此优先队列就是堆,所有需要用到堆的位置,都可以考虑优先队列。注意:默认情况下是大堆。
函数声明 | 接口说明 |
priority_queue() | 构造一个空的优先级队列 |
empty() | bool返回检测空 |
top() | 返回队列中最大(小元素),即堆顶元素 |
push() | 在优先队列中插入x |
pop() | 删除堆顶元素 |
3.3 在OJ中的使用
215. 数组中的第K个最大元素 - 力扣(LeetCode)
class Solution
{
public:
int findKthLargest(vector<int>& nums, int k)
{
priority_queue<int> pq(nums.begin(), nums.end());
for(int i=0; i<k-1; i++)
{
pq.pop();
}
return pq.top();
}
};
将数组中的元素放入优先级队列中,再将前k-1个元素删掉,即要找第二大的,删一个;找第三大的,删两个;找第k大的,删k-1个。
3.4 priority_queue 模拟实现
#include<vector>
#include<functional>
using namespace std;
namespace zzy
{
template <class T, class Container = vector<T>, class Compare = less<T> >
class priority_queue
{
public:
priority_queue()
{
c.reserve(10);
}
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
{
c.reserve(last - first);
for (auto it = first; it != last; ++it)
{
c.push_back(*it);
}
make_heap(c.begin(), c.end(), comp);
}
bool empty() const
{
return c.empty();
}
size_t size() const
{
return c.size();
}
T& top() const
{
return c.front();
}
void push(const T& x)
{
c.push_back(x);
push_heap(c.begin(), c.end(), comp);
}
void pop()
{
pop_heap(c.begin(), c.end(), comp);
c.pop_back();
}
private:
Container c;
Compare comp;
};
};
make_heap(c.begin(), c.end(), comp);
这段代码的功能是将容器 c 中的元素构建成一个堆(heap),使用自定义比较函数 comp。具体来说:
- make_heap 是 C++ 标准库中的一个算法,用于将给定范围内的元素构建成一个最大堆或最小堆。
- c.begin() 和 c.end() 分别表示容器 c 的起始和结束迭代器,定义了要构建堆的范围。
- comp 是一个比较函数对象或函数指针,用于定义堆中元素的排序规则。
push_heap(c.begin(), c.end(), comp);
这段代码的功能是将容器 c 中的元素调整为堆结构,并使用自定义比较函数 comp 来确定元素的顺序。具体步骤如下:
- push_heap:将容器 c 的最后一个元素插入到现有的堆中,保持堆的性质。
- c.begin() 和 c.end():指定容器的起始和结束迭代器,表示整个容器范围。
- comp:自定义比较函数,用于确定元素之间的顺序。
数组和链表哪个来实现栈和队列更好?
-
栈:(一般使用vector)
-
使用数组实现栈适合栈大小已知或变化不大,且需要高效访问栈顶元素的场景。
-
使用链表实现栈适合栈大小变化较大,且需要频繁插入和删除操作的场景。
-
-
队列:(一般使用list)
-
使用数组实现队列适合队列大小已知或变化不大,且需要高效访问队头和队尾元素的场景。
-
使用链表实现队列适合队列大小变化较大,且需要频繁插入和删除操作的场景。
-
4. 容器适配器
4.1 适配器
适配器(Adapter)是一种设计模式,用于将一个类的接口转换成客户端所期望的另一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
4.2 STL中stack和queue的底层结构
虽然stack和queue可以存放元素,但是STL并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和queue只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque。那deque是什么呢?
4.3 deque的介绍
deque(双端队列),是一种双开口的连续空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector相比,头插效率高,不需要搬移元素;与list比较,空间利用率高。
不过deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成,实际deque类似于一个动态的二维数组。
相比于vector:
- 极大的缓解了扩容问题/头插头删问题
- [ ]不够极致,虽然 deque 的 operator[] 访问仍然是常数时间复杂度 O(1),但由于需要计算元素位于哪个缓冲区以及在该缓冲区中的具体位置。实际性能可能会比 vector 稍差一些。此外,由于 deque 的元素不是连续存储的,缓存命中率也可能较低,从而影响遍历和随机访问的性能。
相比于list:
- 可以支持下标随机访问
- cpu高速缓存效率不错
- 头尾插入删除都不错,但是中间插入删除很拉跨
deque的致命缺陷在于不适合遍历,遍历时deque 的内部实现:deque 内部由多个固定大小的缓冲区(blocks)组成,这些缓冲区不是连续存储在内存中的。每个缓冲区之间通过指针或索引链接。由于元素不是连续存储的,遍历时需要频繁跨越不同的缓冲区,导致更多的缓存未命中(cache miss),从而降低遍历性能。
所以deque用来适配stack和queue的默认容器非常的合适:
- stack 和 queue 不需要遍历,因此stack和queue没有迭代器,只需要在固定的一段或者两端进行操作。
- 在stack中增长元素时,deque比vector的效率高;queue中元素增长时,deque不仅效率高,而且内存使用率高。
5. 仿函数、函数对象的本质
仿函数(Functor),也称为函数对象,是C++中一种特殊的类。它通过重载operator()操作符,使得该类的对象可以像普通函数一样被调用。
特点:
- 可调用性:仿函数对象可以通过类似函数调用的方式使用。
- 状态保持:与普通函数不同,仿函数可以拥有自己的状态(成员变量),因此可以在多次调用之间保持某些信息。
- 灵活性:仿函数可以作为参数传递给其他函数或算法,并且可以根据需要动态调整行为。
class Less
{
public:
bool operator()(int a, int b) const
{
return a < b;
}
};
int main()
{
Less lessfunc;
cout << lessfunc(1, 2) << endl;
cout << lessfunc.operator()(2, 1) << endl;
}
仿函数(函数对象)的本质就是这个类可以像函数一样调用。
6. 反向迭代器
与正向迭代器的区别:反向的++是倒着走的。
库中的反向迭代器其实是一个迭代器适配器,它通过包装一个正向迭代器来提供反向遍历的功能。
具体来说,它将对反向迭代器的operator++ 和 operator-- 操作反转,使得他们的行为与正向迭代器相反。正向迭代器的operator++会使迭代器指向容器的下一个元素,反向迭代器的operator++会指向前一个位置。
template <typename Iterator>
class reverse_iterator {
public:
// 构造函数
reverse_iterator() : current() {}
explicit reverse_iterator(Iterator it) : current(it) {}
// 返回基础迭代器
Iterator base() const { return current; }
// 解引用操作符
reference operator*() const {
Iterator tmp = current;
return *--tmp;
}
// 成员访问操作符
pointer operator->() const {
return &(operator*());
}
// 前置递增
reverse_iterator& operator++() {
--current;
return *this;
}
// 后置递增
reverse_iterator operator++(int) {
reverse_iterator tmp = *this;
--current;
return tmp;
}
// 前置递减
reverse_iterator& operator--() {
++current;
return *this;
}
// 后置递减
reverse_iterator operator--(int) {
reverse_iterator tmp = *this;
++current;
return tmp;
}
private:
Iterator current; // 内部存储的正向迭代器
};
反向迭代器的 operator* 需要先将内部的正向迭代器向前移动一位再解引用,以确保正确访问前一个元素。库中的rbegin和end是对应的,rend和begin是对应的,所以要先前移一位。