栈和队列是STL(C++标准库)里面的两个数据结构。
C++标准库是有多个版本的,知道使用的STL是哪个版本,才能知道对应的栈和队列的实现原理。三个最为普遍的STL版本:
接下来介绍的栈和队列也是SGI STL里面的数据结构。
栈提供 push
,pop
,top
等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。
栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。
那么问题来了,STL 中栈是用什么容器实现的?
从下图中可以看出,栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。
我们常用的SGI STL,如果没有指定底层实现的话,默认以deque为缺省情况下栈的底层结构。
deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。
我们也可以指定vector为栈的底层实现,初始化语句如下:
std::stack<int, std::vector<int> > third; // 使用vector为底层容器的栈
队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。所以STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)。
也可以指定list 为起底层实现,初始化queue的语句如下:
std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列
使用栈来模拟队列的行为,需要两个栈,一个输入栈,一个输出栈,这里要注意输入栈和输出栈的关系。在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。
class MyQueue {
private:
stack<int> InStack;
stack<int> OutStack;
public:
MyQueue() {
}
void push(int x) {
InStack.push(x);
}
int pop() {
if(OutStack.empty()){
while(!InStack.empty()){
OutStack.push(InStack.top());
InStack.pop();
}
}
int ans = OutStack.top();
OutStack.pop();
return ans;
}
int peek() {
int ans = this->pop(); // peek操作中,实现了pop的代码复用
OutStack.push(ans); // 把pop的元素添加回输出栈
return ans;
}
bool empty() {
return (InStack.empty() && OutStack.empty());
}
};
用两个队列来模拟栈,其中一个队列用来备份,用两个队列que1和que2实现队列的功能,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,此时que1将为下一次操作的备份队列。
class MyStack {
private:
queue<int> Que0;
queue<int> Que1;
int size; // 记录栈的大小
int flag; // 备份标记
int front; // 记录top
public:
MyStack() {
size = 0;
flag = 0;
}
void push(int x) {
if(flag == 1){
Que0.push(x); // Que1 为备份队列
}
else{
Que1.push(x);
}
front = x;
++size;
}
int pop() {
int ans;
if(flag == 1){ // Que1 为备份队列
for(int i = 0; i < size - 1; ++i){
front = Que0.front(); // 弹出前i个元素,并记录top
Que1.push(Que0.front());
Que0.pop();
}
ans = Que0.front();
Que0.pop();
flag = 0;
}
else{
for(int i = 0; i < size - 1; ++i){
front = Que1.front();
Que0.push(Que1.front());
Que1.pop();
}
ans = Que1.front();
Que1.pop();
flag = 1;
}
--size;
return ans;
}
int top() {
return front;
}
bool empty() {
return Que0.empty() && Que1.empty();
}
};
一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。
class MyStack {
private:
queue<int> Que;
int size; // 记录栈的大小
int front; // 记录top
public:
MyStack() {
size = 0;
}
void push(int x) {
Que.push(x);
front = x;
++size;
}
int pop() {
for(int i = 0; i < size - 1; ++i){
front = Que.front(); // 弹出前i个元素,并记录top
Que.push(Que.front());
Que.pop();
}
int ans = Que.front();
Que.pop();
--size;
return ans;
}
int top() {
return front;
}
bool empty() {
return Que.empty();
}
};
后遇到的左括号要先闭合
时间复杂度: O ( n ) O(n) O(n),其中 n 是字符串 s 的长度。
空间复杂度: O ( n + ∣ Σ ∣ ) O(n+∣Σ∣) O(n+∣Σ∣),其中 Σ 表示字符集,本题中字符串只包含 6 种括号,∣Σ∣= 6。栈中的字符数量为 O(n),而哈希表使用的空间为 O(∣Σ∣),相加即可得到总空间复杂度。
最直接思路: 将左括号压入栈
class Solution {
public:
bool isValid(string s) {
int n = s.size();
if (n % 2 == 1) { // 奇数数组 直接返回false
return false;
}
stack<char> left;
for(char ch : s){
if(ch == '(' || ch == '[' || ch == '{') {
left.push(ch); // 左括号 入栈
}
else if(ch == ')') {
if(!left.empty() && left.top() == '('){ // 右括号 进行匹配判断
left.pop();
}
else{
return false;
}
}
else if(ch == ']') {
if(!left.empty() && left.top() == '['){
left.pop();
}
else{
return false;
}
}
else if(ch == '}') {
if(!left.empty() && left.top() == '{'){
left.pop();
}
else{
return false;
}
}
}
return left.empty();
}
};
class Solution {
public:
bool isValid(string s) {
int n = s.size();
if (n % 2 == 1) { // 奇数数组 直接返回false
return false;
}
stack<char> left;
unordered_map<char,char> table = {
{')','('}, // 哈希表存储键值对
{']','['},
{'}','{'}
};
for(char ch : s){
if(table.count(ch)){ // 判断是否为右括号
if(left.empty() || left.top() != table[ch]){
return false;
}
left.pop();
}
else{
left.push(ch);
}
}
return left.empty();
}
};
class Solution {
public:
bool isValid(string s) {
stack<char> st;
for(char ch : s){
if(ch == '{'){
st.push('}');
}
else if(ch == '('){
st.push(')');
}
else if(ch == '['){
st.push(']');
}
else{ // 右括号 判断
if(st.empty() || st.top() != ch){
return false;
}
st.pop();
}
}
return st.empty();
}
};
输入:“abbaca”
输出:“ca”
直接将string
作为栈,利用pop_back
和push_back
进行栈操作
class Solution {
public:
string removeDuplicates(string s) {
string ans;
for(char ch : s){
if(!ans.empty() && ans.back() == ch){
ans.pop_back();
}
else{
ans.push_back(ch);
}
}
return ans;
}
};
C语言:
char* removeDuplicates(char* s) {
int n = strlen(s);
char* stk = malloc(sizeof(char) * (n + 1));
int retSize = 0;
for (int i = 0; i < n; i++) {
if (retSize > 0 && stk[retSize - 1] == s[i]) {
retSize--;
} else {
stk[retSize++] = s[i];
}
}
stk[retSize] = '\0';
return stk;
}
逆波兰式
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
int ans = 0;
unordered_set<string> hash {"+","-","*","/"}; // 哈希表,判断是否为符号
for(string str : tokens){
if(hash.count(str)){ // 查找到符号,对栈上前两个数进行处理
int a = st.top();
st.pop();
int b = st.top();
st.pop();
if(str == "+") {st.push(a + b); continue;}
if(str == "*") {st.push(a * b); continue;}
if(str == "/") {st.push(b / a); continue;} // 先出栈作为除数
if(str == "-") {st.push(b - a);} // 先出栈作为减数
}
else{
st.push(stoi(str)); // 字符串转整数 stoi(str),入栈
}
}
return st.top(); // 遍历完成,返回栈顶
}
};
此时我们需要一个队列,放进窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。
队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队里里的元素数值是由大到小的。
那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来创建一个单调队列。
pop(value)
:如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作;push(value)
:如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止(保持单调特性);que.front()
就可以返回当前窗口的最大值。class Solution {
public:
class MyQueue{ // 创建单调双端队列类
public:
deque<int> que;
void pop(int value){ // 当滑动删除的元素等于队列最大值时,将其从队列中删除
if(!que.empty() && value == que.front()){
que.pop_front();
}
}
void push(int value){ // 将元素加入列队,移除更小的元素保持单调特性
while(!que.empty() && value > que.back()){
que.pop_back();
}
que.push_back(value);
}
int front(){ // 返回队列最大值
return que.front();
}
};
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
MyQueue q;
vector<int> ans;
for(int i = 0; i < k; ++i){ // 前k个元素,第一个窗口加入队列
q.push(nums[i]);
}
ans.push_back(q.front()); // 第一个窗口的最大值
for(int j = k; j < n; ++j){ // 移动窗口
q.pop(nums[j-k]); // 处理滑动删除的元素
q.push(nums[j]); // 处理新的元素
ans.push_back(q.front()); // 窗口的最大值
}
return ans;
}
};
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> hashmap_num; // 哈希表记录元素个数
unordered_map<int, int> hashmap_index; // 哈希表记录元素在ans数组出现的位置
vector<int> ans;
for(int i : nums){
++hashmap_num[i];
if(hashmap_num[i] == 1){
ans.emplace_back(i); // 插入首次出现的元素
hashmap_index[i] = ans.size()-1;
}
else{
int l = 0;
int r = ans.size()-1;
while(l <= r){ // 二分法找到边界位置,注意判断l<=r
int mid = l + (r-l)/2; // 注意下面判断 ans[mid] != i
if(hashmap_num[ans[mid]] > hashmap_num[i]-1 && ans[mid] != i){
l = mid + 1; // l 第一个小于或等于
}else{
r = mid - 1; // r 最后一个大于
}
}
swap(ans[l], ans[hashmap_index[i]]); // 进行位置交换
swap(hashmap_index[ans[hashmap_index[i]]],hashmap_index[i]); // 索引交换
}
}
ans.resize(k);
return ans;
}
};
先统计出现最多的次数n
,循环减小 n 判断哈希表中 map.second = n
的元素,直到填满 k 个。
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
// first对应元素value second对应元素出现次数
unordered_map<int, int>hashmap;
vector<int> ret;
// 记录元素出现次数
for(int i : nums) hashmap[i]++;
// 找到元素出现的最高频率次数
int maxtimes = 0;
for (auto i : hashmap) {
if (i.second > maxtimes) {
maxtimes = i.second;
}
}
// 从最高往低走 依次输出
while (k > 0){
for (auto i : hashmap){
if (i.second == maxtimes) {
ret.push_back(i.first);
k--;
}
}
maxtimes--;
}
return ret;
}
};
建立并维护大小为 k 的小根堆。
总体时间复杂度 O(nlogk):
class Solution {
public:
void sift_up(vector<vector<int>> &heap, int chlid){
vector<int> val = heap[chlid];
while (chlid >> 1 > 0 && val[1] < heap[chlid>>1][1]){
heap[chlid] = heap[chlid>>1];
chlid >>= 1;
heap[chlid] = val;
}
}
void sift_down(vector<vector<int>> &heap, int root, int k){
vector<int> val = heap[root];
while (root << 1 < k){
int chlid = root << 1;
// 注意这里位运算优先级要加括号
if ((chlid|1) < k && heap[chlid|1][1] < heap[chlid][1]) chlid |= 1;
if (heap[chlid][1] < val[1]){
heap[root] = heap[chlid];
root = chlid;
}
else break;
}
heap[root] = val;
}
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> stat;
for (auto &num : nums) stat[num]++;
vector<vector<int>> vec_stat;
for (auto &item : stat) vec_stat.push_back({item.first, item.second});
vector<vector<int>> heap;
heap.push_back({0, 0});
for (int i = 0; i < k; i++){
heap.push_back(vec_stat[i]);
sift_up(heap, heap.size()-1);
}
for (int i = k; i < vec_stat.size(); i++){
if (vec_stat[i][1] > heap[1][1]){
heap[1] = vec_stat[i];
sift_down(heap, 1, k+1);
}
}
vector<int> result;
for (int i = 1; i < k+1; i++) result.push_back(heap[i][0]);
return result;
}
};
统计最大频次,建立大小为 maxCnt 的桶集合。
// 桶排序(根据频率排序)
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> countMap;
int maxCount = 0;
for (const int& x : nums) {
maxCount = std::max(maxCount, ++countMap[x]); // 利用map统计每个元素的频率,同时得出最大频率
}
vector<vector<int>> buckets(maxCount + 1); // 桶的大小为maxCount+1 (即同一个桶中的元素频率相同)
for (const auto& pair : countMap) {
buckets[pair.second].push_back(pair.first); // 按照map中记录的每个元素频率 将元素放入相应桶中
}
vector<int> result;
for (int i = maxCount; i >= 0 && result.size() < k; --i) { // 从 最后一个桶(频率最高) 往前遍历,从而得出前k个高频元素
for (const auto& x : buckets[i]) {
result.push_back(x);
if (result.size() == k) break;
}
}
return result;
}
};