链表
C++ STL - list
#include<iostream>
#include<list>
using namespace std;
int main(){
int n, m;
cin >> n >> m;
list<int>node;
//创建链表
for(int i = 1; i <= n; i ++){
node.push_back(i);
}
//通过迭代器it去遍历链表
list<int>:: iterator it = node.begin();
//模拟约瑟夫环的过程
while(node.size() > 1){
for(int i = 1; i < m; i ++){
it ++;
if(it == node.end()){
it = node.begin();
}
}
//输出第m个元素(当前要删除的元素)
cout << *it << " ";
//更新迭代器指向下一个元素,并删除当前元素
list<int>:: iterator next = ++it;
if(next == node.end())
next = node.begin();
node.erase(--it);
it = next;
}
//while循环结束,此时只剩下一个元素
cout << * it;
return 0;
}
队列
队列是先进先出,从队尾添加元素,从队头删除元素
C++ STL - queue
#include<iostream>
#include<queue>
using namespace std;
int Hash[1003] = {0}; //Hash[i] == 0 表示i不在内存中,否则i在内存中
queue<int> mem;//用队列模拟内存
int main(){
int n, m;
cin >> m >> n;
int cnt = 0;//统计次数
while(n--){
int en;
cin >> en;//输入一个英文单词
if(!Hash[en]){
++ cnt;// 内存中没有,去外存中查找,查找次数加1
mem.push(en);
Hash[en] = 1;// 内存中有这个单词
while(mem.size() > m){//队列满了
Hash[mem.front()] = 0;
mem.pop();// 从队头去掉
}
}
}
printf("%d\n", cnt);
return 0;
}
手写循环队列
竞赛中一般采用静态分配
#include<iostream>
using namespace std;
#define N 1003 // 队列大小
int Hash[N]; //用哈希检查内存中有没有单词
//分配静态空间
struct myQueue{
int data[N];
int head, rear;// 队头、队尾
bool init(){
head = rear = 0;
return true;
}
int size(){ // 返回队列的长度
return (rear - head + N) % N;
}
bool empty(){ // 队列的判空
if(size() == 0){
return true;
}else{
return false;
}
}
bool push(int e){ //队列的插入
//如果队满,则插入失败
if((rear + 1) % N == head){ //队满的判断
return false;
}
data[rear] = e;
rear = (rear + 1) % N;
return true;
}
bool pop(int &e){ // 删除队头元素,结果用e返回
if(head == rear){
return false;
}
e = data[head];
head = (head + 1) % N;
return true;
}
int front(){ //返回队首元素,但是不删除
return data[head];
}
}Q;
int main(){
Q.init(); // 初始化队列
int m, n;
cin >> m >> n;
int cnt = 0;
while(n -- ){
int en;
cin >> en;
if(!Hash[en]){ //内存中没有这个单词
++ cnt;// 先去外存
Q.push(en);
Hash[en] = 1;
while(Q.size() > m){
int tmp;
Q.pop(tmp);
Hash[tmp] = 0;
}
}
}
cout << cnt << endl;
return 0;
}
双端队列和单调队列
双端队列和单调队列的概念
能够且只能在两端进行插入和删除的数据结构,双端队列的典型应用是单调队列,单调队列可以优化问题的求解
双端队列的实现方式
c++ STL - deque
dq[i]
:返回队列中下标为i的元素dq.front()
:返回队头dq.back()
:返回队尾dq.pop_back()
:删除队尾,不返回值dq.pop_front()
:删除队头,不返回值dq.push_back(e)
:在队尾添加一个元素edq.push_front(e)
:在队头添加一个元素e
单调队列和滑动窗口
解题思路:
思路一:暴力解
从头到尾扫描,总共经过n次,每次遍历k个元素,所以时间复杂度为O(n * k)
,这样会超时。
思路二:单调队列求解
时间复杂度为O(n)
本题中单调队列的特征
- 队头的元素始终是单调队列中最小的。根据需要输出队头元素,但是不一定要弹出
- 元素只能从队尾进入队列,从队头、队尾都可以弹出(限定插入的双端队列)
- 序列中的每一个元素都必须进入队列。例如:x进入队尾时,和原来队尾元素y进行比较,如果
x <= y
就从队尾弹出y;一直弹出队尾所有比x大的元素,最后x进入队尾。这个入队操作保证队头元素是队列中最小的 - 队伍的长度不会超过k,如果大于k,队头元素出队
上述的操作就会保证这个队列始终满足单调性:从队头到队尾,从小到大,且队伍始终只有k个元素,每次输出队头和队尾元素,就是当前滑动窗口的最小值和最大值。每个元素只会入队一次,出队一次,总共有n个元素,所以时间复杂度为O(n)
#include<iostream>
#include<deque>
using namespace std;
const int N = 1e6 + 5;
int a[N];
deque<int>q;// 定义了一个双端队列,存储元素的下标
int main(){
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++){ //从下标为1的地方开始读入的原因
cin >> a[i];
}
//求最小值
for(int i = 1; i <= n; i ++){
// x进入队尾时,和原来队尾元素y进行比较,如果`x <= y`就从队尾弹出y;一直弹出队尾所有比x大的元素,最后x进入队尾。
while(!q.empty() && a[q.back()] > a[i]) {
q.pop_back();
}
q.push_back(i);
//确保窗口始终为m
if(i >= m){
while(!q.empty() && q.front() <= i - m){
q.pop_front(); // 删头
}
cout << a[q.front()] <<" ";//输出队头元素,就是输出当前滑动窗口的最小值
}
}
cout << endl;
//求最大值,保证队头的最大值
while(!q.empty()){
q.pop_front();//清空
}
for(int i = 1; i <= n; i ++){
while(!q.empty() && a[q.back()] < a[i]){
q.pop_back();//去尾
}
q.push_back(i);
if(i >= m){
while(!q.empty() && q.front() <= i - m){
q.pop_front();//删头
}
cout << a[q.front()] << " ";
}
}
cout << endl;
return 0;
}
单调队列和最大子序和问题
子序和:对于给定长度为n的整数序列A,子序和就是A中非空的一段连续的元素之和。例如
(6, -1, 5, 4, -7)
前两个元素的子序和为6 + (-1) = 5
最大子序和问题分类以及对应的解题方法
- 不限制子序列的长度: 在所有可能的子序列中找到一个子序列,该子序列和最大。
- 方法一:贪心法
- 方法二:动态规划法
- 限制子序列的长度:给定一个限制长度m,找出一段长度不超过m的子序和最大的连续子序列。
- 方法一:单调队列
优先队列
优先队列:每次让优先级最高的元素出队列,是利用堆实现的,一般使用STL完成。
栈
后进先出
STL stack
常见操作:
stack<Type>s
:定义栈s.push(item)
:将item放到栈顶s.top()
:返回栈顶元素,但不会删除s.pop()
:删除栈顶元素,但不会返回s.size()
:返回栈中元素的个数s.empty()
:检查栈是否为空,为空返回true
,否则返回false
题目链接:Text Reverse - 杭电题库
#include<iostream>
#include<stack>
using namespace std;
int main(){
int t;
cin >> t;//次数
getchar();//读取换行符(输入t之后的那个换行符号),防止后序读入字符受到影响
while(t --){
stack<char>s;
while(true){
char ch = getchar();//读入一个字符
if(ch == ' ' || ch == '\n' || ch == EOF){// 当遇到空格、换行符或文件结束符时
while(!s.empty()){
printf("%c",s.top());
s.pop();
}
if(ch == '\n' || ch == EOF){
break;
}
}else{
s.push(ch); // 入栈
}
}
printf("\n");
}
return 0;
}
手写栈
题目链接:Text Reverse - 杭电题库
确实手写栈会节省空间,但是TLE
了
#include<iostream>
using namespace std;
const int N = 1e5 + 100;
struct mystack{
char a[N];
int t = 0;
void push(char x){ //入栈
a[++ t] = x;
}
char top(){ //取栈顶元素
return a[t];
}
char pop(){//出栈
t --;
}
int empty(){ //判空
return t == 0 ? 1 : 0;
}
}st;
int main(){
int n;
cin >> n;
getchar();
while(n -- ){
while (true){
char ch = getchar();
if(ch == ' ' || ch == '\n' || ch == EOF){
while(!st.empty()){
printf("%c",st.top());
st.pop();
}
if(ch == '\n' || ch == EOF){
break;
}
cout << " ";
}else{
st.push(ch);
}
}
cout << endl;
}
return 0;
}
单调栈
结构上,任然是一个普通的栈,它是栈的一种使用方式。
单调栈根据元素之间的关系,可以分为单调递增栈和单调递减栈,单调栈可以用来处理比较问题。
保持单调性的操作:例如单调递减栈,从栈底到栈定,单调递减(大到小),此时,元素入栈,如果元素比第一个元素还要小,则入栈,否则将栈顶元素弹出,直到这个数能入栈为止。
解题思路:
例如:6头奶牛依次为3 2 6 1 1 2
,那么输出结果为3 3 0 6 6 0
(仰望对象的下标,下标从1开始,没有仰望对象的话就是0)。如果常规思路,每一个位置,都去查找其后面的仰望对象,那么时间复杂度是O(n!)
,这样做会超时。可以用单调栈来优化。
单调栈的做法:从后向前遍历奶牛,用栈保存从低到高的奶牛,栈顶的最矮,栈底的最高(单调栈)。
- 从后向前遍历奶牛,和栈顶的奶牛进行比较,如果栈顶的元素没有奶牛i高,则出栈,直到栈顶的奶牛比奶牛i高,这就是奶牛i的仰望对象
- 将奶牛i放进栈顶,此时栈中的奶牛依然保持从低到高。
- 由于每头奶牛只进出栈一次,所以时间复杂度为
O(n)
#include<iostream>
#include<stack>
using namespace std;
const int N = 1e5 + 5;
int h[N], ans[N];
int main(){
//完成读入
int n;
cin >> n;
for(int i = 1; i <= n; i ++){
cin >> h[i];
}
stack<int>st;//存下标
//从后向前遍历奶牛
for(int i = n; i >= 1; i --){
//循环后,保证奶牛i小于栈顶元素
while (!st.empty() && h[st.top()] <= h[i]){
st.pop();
}
//如果栈为空,没有仰望对象,存0
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;
}
return 0;
}
二叉树和哈夫曼树
树是非线性结构,能够描述数据的层次关系。
二叉树是常见的树形结构,相关的算法比较多,一般的树都转换为二叉树来解决问题
哈夫曼树是二叉树的一个应用