目录
1.单调栈
1.1.栈
栈(Stack)是一种具有特定行为的数据结构,遵循"后进先出"(Last-In-First-Out,LIFO)的原则。栈可以被看作是一种容器,其中元素的插入和删除操作仅限于栈顶(top)位置。
栈的主要操作包括:
- 入栈(Push):将元素压入栈顶。
- 出栈(Pop):从栈顶移除元素。
- 查看栈顶元素(Top):获取栈顶元素的值,但不对栈进行修改。
- 判断栈是否为空(Empty):检查栈是否为空。
- 获取栈的大小(Size):获取栈中元素的数量。
栈的实现可以使用数组或链表等数据结构,C++中有STL容器可以实现栈。在使用数组实现时,需要注意栈的大小限制,即栈的最大容量。当栈的大小达到最大容量时,继续入栈会导致栈上溢(Stack Overflow)。
栈常用于需要按照特定顺序处理元素的场景,例如函数调用栈、表达式求值、括号匹配等。在解决问题时,可以使用栈来存储和操作元素,以满足特定的需求。
1.2.STL stack
相关操作如下:
操作 | 说明 |
stack<Type> s | 定义栈,Type为数据类型,如int,float等。 |
s.push(item) | 把item放到栈顶。 |
s.top() | 返回栈顶的元素,但不会删除。 |
s.pop() | 删除栈顶元素,但不会返回。在出栈时需要先使用s.top()获得栈顶元素,再用s.pop()删除栈顶元素。 |
s.size() | 返回栈中的元素个数。 |
s.empty() | 检查栈是否为空,如果为空则返回true,否则返回false。 |
例题
问题描述:翻转字符串(#号结束输入)
输入:olleh# 输出:hello
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve() {
stack<char> s;
char ch;
cin >> ch;
while (ch != '#') {
s.push(ch);
cin >> ch;
}
while (!s.empty()) {
cout << s.top();
s.pop();
}
}
int main() {
ios::sync_with_stdio;
cin.tie(0);
cout.tie(0);
solve();
return 0;
}
1.3.单调栈
单调栈不是新的数据结构,仍是普通的栈,是栈的一种使用方式。单调栈内的元素是单调递增或者单调递减的,有单调递增栈,也有单调递减栈。单调栈可以用来处理比较问题。
单调栈实际上就是普通栈,只是使用时始终保持栈内元素时单调的。例如:单调递减栈从栈顶到栈底是从小到大的顺序。当一个数入栈时,与栈顶比较,若比栈顶小,则入栈;若比栈顶大,则弹出栈顶,直到这个数能入栈为止。
例题
波谷 P2947:[USACO09MAR] Look Up S
问题描述:
Farmer John's N (1 <= N <= 100,000) cows, conveniently numbered 1..N, are once again standing in a row. Cow i has height H_i (1 <= H_i <= 1,000,000).Each cow is looking to her left toward those with higher index numbers. We say that cow i 'looks up' to cow j if i < j and H_i < H_j. For each cow i, FJ would like to know the index of the first cow in line looked up to by cow i.Note: about 50% of the test data will have N <= 1,000.
约翰的N(1≤N≤105) 头奶牛站成一排,奶牛i的身高是(1≤≤)。现在,每只奶牛都在向右看齐。对于奶牛i,如果奶牛j满足i<j 且<,我们可以说奶牛i可以仰望奶牛j。 求出每只奶牛离她最近的仰望对象。
输入格式:
第1行输入N,之后每行输入一个身高。
输出格式:
共N行,按顺序每行输出一只奶牛的最近仰望对象,如果没有仰望对象,输出0。
输入
6 3 2 6 1 1 2
输出
3 3 0 6 6 0
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve() {
int n;
cin >> n;
vector<int> a(n + 1), ans(n + 1);
for (int i = 1; i <= n; i++)cin >> a[i];
stack<int> st;
for (int i = n; i >= 1; i--) {
while (!st.empty() && a[st.top()] <= a[i]) st.pop();
if (st.empty())ans[i] = 0;
else ans[i] = st.top();
st.push(i);
}
for (int i = 1; i <= n; i++)cout << ans[i] << endl;
}
int main() {
ios::sync_with_stdio;
cin.tie(0);
cout.tie(0);
solve();
return 0;
}
2.单调队列和优先队列
2.1.队列
队列(Queue)是一种具有特定行为的数据结构,遵循"先进先出"(First-In-First-Out,FIFO)的原则。队列可以被看作是一种容器,其中元素的插入操作发生在队尾(rear),而元素的删除操作发生在队头(front)。
队列的主要操作包括:
- 入队(Enqueue):将元素插入队尾。
- 出队(Dequeue):从队头移除元素。
- 查看队头元素(Front):获取队头元素的值,但不对队列进行修改。
- 查看队尾元素(Rear):获取队尾元素的值,但不对队列进行修改。
- 判断队列是否为空(Empty):检查队列是否为空。
- 获取队列的大小(Size):获取队列中元素的数量。
队列的实现可以使用数组或链表等数据结构,C++中有STL容器可以实现队列。在使用数组实现时,需要注意队列的大小限制,即队列的最大容量。当队列的大小达到最大容量时,继续入队会导致队列上溢(Queue Overflow)。
队列常用于需要按照先后顺序处理元素的场景,例如任务调度、消息传递、广度优先搜索等。在解决问题时,可以使用队列来存储和操作元素,以满足特定的需求。
2.2.STL queue
相关操作如下:
操作 | 说明 |
queue<Type> q | 定义队列,Type为数据类型,如int,float等。 |
q.push(item) | 把item放到进队列。 |
q.front() | 返回队首元素,但不会删除。 |
q.pop() | 删除队首元素。 |
q.back() | 返回队尾元素。 |
q.size() | 返回队列中的元素个数。 |
q.empty() | 检查队列是否为空,如果为空则返回true,否则返回false。 |
2.3.手写循环队列
const int N = 1000;
struct myqueue {
Type data[N];//分配静态内存
/*如果要动态分配,则写为Type *data*/
int head, rear;//对头队尾
bool init() {//初始化
/*如果要动态分配
Q.data = (Type*)malloc(N * sizeof(Type));
if (!Q.data)return false;*/
rear = head = 0;
return true;
}
int size() {//返回队列长度
return (rear - head + N) % N;
}
bool empty() {//判断队列是否为空
if (size() == 0)return true;
else return false;
}
bool push(Type item) {//队尾插入新元素,新的rear指向下一个空的位置
if ((rear + 1) % N == head)return false;//队满
data[rear] = item;
rear = (rear + 1) % N;
return false;
}
bool pop() {//删除队尾元素
if (head == rear)return false;//队空
head = (head + 1) % N;
return true;
}
Type front() {//返回队首元素
return data[head];
}
Type back() {//返回队尾元素
return data[rear];
}
}Q;
2.4.双端队列和单调对队列
双端队列(Deque,全称为Double-ended Queue)是一种允许在队列两端进行插入和删除操作的数据结构,C++中有STL容器可以实现双端队列。它可以在队头和队尾同时进行元素的入队和出队操作。
双端队列支持以下操作:
-
入队操作:
- 从队头插入元素(push_front)
- 从队尾插入元素(push_back)
-
出队操作:
- 从队头删除元素(pop_front)
- 从队尾删除元素(pop_back)
-
查看队头和队尾元素:
- 获取队头元素(front)
- 获取队尾元素(back)
-
判断双端队列是否为空:
- 检查双端队列是否为空(empty)
双端队列可以用于需要在两端进行插入和删除操作的场景,例如实现滑动窗口、双端搜索等算法。
单调队列(Monotonic Queue)是一种特殊类型的队列,它具有维持队列内元素单调性(递增或递减)的特性。单调队列通常用于解决一些需要维护滑动窗口内最大值或最小值的问题。双端队列的经典应用是单调队列。
2.5.STL deque
相关操作如下:
操作 | 说明 |
deque<Type> dq | 定义双端队列,Type为数据类型,如int,float等。 |
dq.back() | 返回队尾元素,但不会删除。 |
dq.front() | 返回队首元素,但不会删除。 |
dq.pop_front() | 删除队首元素,不返回值。 |
dq.pop_back() | 删除队尾元素,不返回值。 |
dq.push_front(item) | 在队首添加元素item。 |
dq.push_back(item) | 在队尾添加元素item。 |
dq.empty() | 检查队列是否为空,如果为空则返回true,否则返回false。 |
dq[i] | 返回队列中下标为i的元素 |
2.6.单调队列与滑动窗口
例题
解析
本题采用暴力法解决很容易实现,从头扫到尾,每次检查k个数,一共检查O(nk)次,显然会超时。用单调队列求解,复杂度为O(n)。
在本题中,求最小值时单调队列有以下特征:
(1)队头的元素始终是最小的。根据需要输出队头,但是不一定弹出。
(2)元素只能从队尾进入队列,从队头、队尾都可以弹出。
(3)序列中的每个元素都必须进入队列。
此题单调队列的两个重要操作:删头、去尾。
(1)删头:如果队头的元素脱离了窗口,这个元素就没有用了,弹出它。
(2)去尾:如果新元素进队尾,原队尾的存在破坏队列的单调性,就弹出它。
样例说明:
元素进入队尾 | 元素进队顺序 | 队列 | 窗口范围 | 队首是否在窗口内 | 输出队首 | 弹出队尾 | 弹出队首 |
1 | 1 | {1} | [1] | 是 | |||
3 | 2 | {1,3} | [1 2] | 是 | |||
-1 | 3 | {-1} | [1 2 3] | 是 | -1 | 3,1 | |
-3 | 4 | {-3} | [2 3 4] | 是 | -3 | -1 | |
5 | 5 | {-3,5} | [3 4 5] | 是 | -3 | ||
3 | 6 | {-3,3} | [4 5 6] | 是 | -3 | 5 | |
6 | 7 | {3,6} | [5 6 7] | -3否,3是 | 3 | -3 | |
7 | 8 | {3,6,7} | [6 7 8] | 是 | 3 |
求最大值类似。
代码如下:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1000005;
int a[N];
deque<int> dq;
void solve() {
int n, k;
cin >> n >> k;
for (int i = 1; i <= n; i++)cin >> a[i];
for (int i = 1; i <= n; i++) {
while (!dq.empty() && a[dq.back()] > a[i])dq.pop_back();
dq.push_back(i);
if (i >= k) {
while (!dq.empty() && dq.front() <= i - k) dq.pop_front();
cout << a[dq.front()] << " ";
}
}
cout << endl;
while (!dq.empty())dq.pop_back();
for (int i = 1; i <= n; i++) {
while (!dq.empty() && a[dq.back()] < a[i])dq.pop_back();
dq.push_back(i);
if (i >= k) {
while (!dq.empty() && dq.front() <= i - k) dq.pop_front();
cout << a[dq.front()] << " ";
}
}
}
int main() {
ios::sync_with_stdio;
cin.tie(0);
cout.tie(0);
solve();
return 0;
}
1.3.单调栈中的例题也可以用双端队列解决
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve() {
int n;
cin >> n;
vector<int> a(n + 1), ans(n + 1);
for (int i = 1; i <= n; i++)cin >> a[i];
deque<int> st;
for (int i = n; i >= 1; i--) {
while (!st.empty() && a[st.back()] <= a[i]) st.pop_back();
if (st.empty())ans[i] = 0;
else ans[i] = st.back();
st.push_back(i);
}
for (int i = 1; i <= n; i++)cout << ans[i] << endl;
}
int main() {
ios::sync_with_stdio;
cin.tie(0);
cout.tie(0);
solve();
return 0;
}
2.7.优先队列
优先队列(Priority Queue)是一种特殊类型的队列,其中每个元素都关联有一个优先级,C++中有STL容器可以实现优先队列。与普通队列不同,优先队列中的元素不是按照插入的顺序进行处理,而是根据其优先级来确定处理顺序。具有较高优先级的元素会先被处理。
优先队列的主要操作是插入和删除最高优先级的元素。常见的实现方式有二叉堆、斐波那契堆、红黑树等。这些实现方式可以保证在插入和删除操作时,维护队列中元素的有序性。
优先队列的常见操作包括:
-
入队操作:
- 将元素插入到优先队列中,并根据其优先级进行调整,以保持队列的有序性。
-
出队操作:
- 删除优先队列中优先级最高的元素,并返回该元素。
-
查看最高优先级元素:
- 获取优先队列中优先级最高的元素,但不进行删除操作。
-
判断优先队列是否为空:
- 检查优先队列是否为空。
优先队列常用于需要按照优先级进行处理的场景,例如任务调度、事件处理、最短路径算法等。通过使用优先队列,可以确保高优先级的任务或事件能够被优先处理,提高系统的效率和响应性。
需要注意的是,优先队列并不保证元素在队列中的顺序,只保证按照优先级处理。如果需要按照插入顺序进行处理,可以使用普通队列或其他数据结构。
2.8.堆与STL priority_queue
堆(Heap)是一种特殊的树状数据结构,通常用于实现优先队列。堆具有以下特点:
-
完全二叉树:堆是一种完全二叉树,即除了最后一层外,其他层都是满的,最后一层的节点从左到右排列。
-
堆序性质:在最大堆(Max Heap)中,父节点的值大于或等于其子节点的值;在最小堆(Min Heap)中,父节点的值小于或等于其子节点的值。这意味着在堆中,根节点的值是最大值(最大堆)或最小值(最小堆)。
堆常用于实现优先队列,其中优先级较高的元素被放置在堆的根节点,而较低优先级的元素被放置在子节点。这样可以方便地找到并删除优先级最高的元素,同时保持堆的结构不变。
堆的常见操作包括:
-
插入操作:将元素插入堆中,并根据堆的性质进行调整,以保持堆的结构和堆序性质。
-
删除操作:删除堆中的根节点,并根据堆的性质进行调整,以保持堆的结构和堆序性质。
-
获取堆顶元素:获取堆中的根节点元素,即优先级最高的元素。
堆可以通过数组或链表等数据结构来实现。在数组实现中,通常使用完全二叉树的性质来存储堆的元素。通过索引的计算,可以快速定位到父节点、左子节点和右子节点。
在C++中,STL提供了std::priority_queue
作为优先队列的实现,底层使用堆来维护元素的有序性。对于手动实现堆的操作,可以使用数组或自定义的数据结构来表示堆,并编写相应的插入、删除和调整操作。
需要注意的是,堆并不保证堆中元素的顺序,只保证堆的性质。如果需要按照顺序访问堆中的元素,可以进行排序操作或使用其他数据结构进行存储。
操作 | 说明 |
priority_queue<Type> q | 定义优先队列,Type为数据类型,如int,float等,从大到小排列。 |
priority_queue<Type,vector<Type>,greater<Type>> q | 定义优先队列,Type为数据类型,如int,float等,从小到大排列。 |
q.push(item) | 在队列中添加元素item。 |
q.top() | 返回优先级最高的元素的值,但不删除。 |
q.pop() | 删除优先队列中优先级最高的元素。 |
q.empty() | 检查队列是否为空,如果为空则返回true,否则返回false。 |
例题
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve() {
priority_queue<int, vector<int>, greater<int>> q;
int n;
cin >> n;
while (n--) {
int op;
cin >> op;
if (op == 1) {
int x;
cin >> x;
q.push(x);
}
else if (op == 2)cout << q.top() << endl;
else if (op == 3)q.pop();
}
}
int main() {
ios::sync_with_stdio;
cin.tie(0);
cout.tie(0);
solve();
return 0;
}