1.单链表
为什么使用数组来模拟链表
如果数据规模很大, 一个一个的new操作太慢了, 会超时, 使用数组会大大加快速度
单链表
数组模拟单链表
例题
实现一个单链表,链表初始为空,支持三种操作:
- 向链表头插入一个数;
- 删除第 k k k 个插入的数后面的数;
- 在第 k k k 个插入的数后插入一个数。
现在要对该链表进行 M M M 次操作,进行完所有操作后,从头到尾输出整个链表。
注意: 题目中第 k k k 个插入的数并不是指当前链表的第 k k k 个数。例如操作过程中一共插入了 n n n 个数,则按照插入的时间顺序,这 n n n 个数依次为:第 1 1 1 个插入的数,第 2 2 2 个插入的数,…第 n n n 个插入的数。
输入格式
第一行包含整数
M
M
M,表示操作次数。
接下来 M M M 行,每行包含一个操作命令,操作命令可能为以下几种:
H x
,表示向链表头插入一个数 x x x。D k
,表示删除第 k k k 个插入的数后面的数(当 k k k 为 0 0 0 时,表示删除头结点)。I k x
,表示在第 k k k 个插入的数后面插入一个数 x x x(此操作中 k k k 均大于 0 0 0)。
输出格式
共一行,将整个链表从头到尾输出。
数据范围
1
≤
M
≤
100000
1≤M≤100000
1≤M≤100000
所有操作保证合法。
代码
#include <iostream>
using namespace std;
const int N = 100010;
// head 表示头结点的下标
// e[i] 表示节点i的值
// ne[i] 表示节点i的next指针是多少
// idx 存储当前已经用到了哪个点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 将x插到头结点
void add_to_head(int x)
{
e[idx] = x, ne[idx] = head, head = idx ++ ;
}
// 将x插到下标是k的点后面
void add(int k, int x)
{
e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++ ;
}
// 将下标是k的点后面的点删掉
void remove(int k)
{
ne[k] = ne[ne[k]];
}
int main()
{
int m;
cin >> m;
init();
while (m -- )
{
int k, x;
char op;
cin >> op;
if (op == 'H')
{
cin >> x;
add_to_head(x);
}
else if (op == 'D')
{
cin >> k;
if (!k) head = ne[head];
else remove(k - 1);
}
else
{
cin >> k >> x;
add(k - 1, x);
}
}
for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
cout << endl;
return 0;
}
2.双链表
代码
//
// Created by Genes on 2020/12/13.
//
// 双向链表, 数组模拟
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int e[N], l[N], r[N], idx;
void initialize() {
r[1] = 2;
l[2] = 1;
idx = 3;
}
void add(int k, int x) {
e[idx] = x;
r[idx] = r[k];
l[r[k]] = idx;
l[idx] = k;
r[k] = idx++;
}
void remove(int k) {
r[l[k]] = r[k];
l[r[k]] = l[k];
}
int m;
int main() {
cin >> m;
initialize();
string op;
int k, x;
while (m--) {
cin >> op;
if (op == "L") {
cin >> x;
add(1, x);
} else if (op == "R") {
cin >> x;
add(l[2], x);
} else if (op == "D") {
cin >> k;
remove(k + 2); //idx从3开始, 因此第一个插入的数应该是3=1+2 --> k+2
} else if (op == "IL") {
cin >> k >> x;
add(l[k + 2], x);
} else {
cin >> k >> x;
add(k + 2, x);
}
}
for (int i = r[1]; i != 2; i = r[i]) {
cout << e[i] << " ";
}
return 0;
}
3.模拟栈
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int m;
int tt = -1;
int stk[N];
void push(int x) {
stk[++tt] = x;
}
bool is__empty() {
return tt == -1;
}
int query() {
return stk[tt];
}
void pop() {
tt--;
}
int main() {
cin >> m;
string op;
while (m--) {
cin >> op;
if (op == "push") {
int x;
cin >> x;
push(x);
} else if (op == "pop") {
pop();
} else if (op == "empty") {
is__empty() ? (cout << "YES" << endl) : (cout << "NO" << endl);
} else {
cout << stk[tt] << endl;
}
}
return 0;
}
4.模拟队列
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int n;
int q[N];
int hh, tt;
int main() {
cin >> n;
while (n--) {
string op;
cin >> op;
if (op == "push") {
int x;
cin >> x;
q[tt++] = x;
} else if (op == "pop") {
if (hh != tt) {
hh++;
}
} else if (op == "empty") {
if (hh == tt) {
cout << "YES" << endl;
} else {
cout << "NO" << endl;
}
} else {
cout << q[hh] << endl;
}
}
return 0;
}
5.单调栈
5.1什么是单调栈
5.2如何维护一个单调栈
单调递增栈:在保持栈内元素单调递增的前提下(如果栈顶元素大于要入栈的元素,将将其弹出),将新元素入栈。
单调递减栈:在保持栈内元素单调递减的前提下(如果栈顶元素小于要入栈的元素,则将其弹出),将新元素入栈。
5.3单调栈有什么规律
单调栈的时间复杂度是
O
(
n
)
O(n)
O(n)
对于将要入栈的元素来说,在对栈进行更新后(即弹出了所有比自己大的元素),此时栈顶元素就是数组中左侧第一个比自己小的元素;
对于将要入栈的元素来说,在对栈进行更新后(即弹出了所有比自己小的元素),此时栈顶元素就是数组中左侧第一个比自己大的元素;
5.4什么时候使用单调栈
给定一个序列,求序列中的每一个数左边或右边第一个比他大或比他小的数在什么地方;
例题
给定一个长度为
N
N
N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出
−
1
−1
−1。
输入格式
第一行包含整数 N,表示数列长度。
第二行包含 N 个整数,表示整数数列。
输出格式
共一行,包含
N
N
N 个整数,其中第 i 个数表示第
i
i
i 个数的左边第一个比它小的数,如果不存在则输出
−
1
−1
−1。
数据范围
1
≤
N
≤
1
0
5
1≤N≤10^{5}
1≤N≤105
1
≤
数列中元素
≤
1
0
9
1≤数列中元素≤10^{9}
1≤数列中元素≤109
代码
//双重循环===》单调栈
#include<iostream>
using namespace std;
const int N = 100010;
int n;
int stk[N],tt;//tt栈顶zhiz
int main()
{
cin>>n;
for(int i = 0;i < n;i++)
{
int x;
cin>>x;
//栈顶存在并且栈顶的值大于当前输入的值,则出栈
while(tt && stk[tt] >= x) tt--;
if(tt) cout<<stk[tt]<<" ";
else cout<<-1<<" ";
stk[++tt] = x;
}
6.单调队列
6.1滑动窗口
遍历数组,然后再遍历每一个窗口
时间复杂度
O
(
n
k
)
O(nk)
O(nk)
优化
通过单调性优化。
我们观察一个规律,对于3 -1 -3
, 当-3
存在, 并且寻找滑动窗口内的最小值时,3 -1
是无效的, 也就是说,当-3
加入队列时,-1
删去, 3
也删去。
抽象一些,当入队元素小于等于队尾元素, 队尾元素出队。直到入队元素大于队尾元素a[q[tt]] >= a[i]
, 或者队列为空,则停止删除操作。
while (hh <= tt && a[q[tt]] >= a[i]) tt -- ;
k
表示滑动窗口大小, q[N]
存储的是数组下标
需要保证q
数组里面存储的元素小于等于k
if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;
当滑动窗口里面的元素个数等于k
个的时候才能输出
当k=3
, 滑动窗口需要有三个元素才能开始输出最值
if (i >= k - 1) printf("%d ", a[q[hh]]);
代码
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 1000010;
int n,k;
int a[N],q[N];//a数组存放原始值,q数组存放下标
int main()
{
scanf("%d%d",&n,&k);
for(int i = 0;i < n;i++) scanf("%d",&a[i]);
int hh = 0,tt = -1;//队列的头指针和尾指针
for(int i = 0;i < n;i++)
{
//判断对头是否已经滑出窗口
if(hh <= tt && i - k + 1> q[hh]) hh++;//因为一次移进一个数,所以不需要循环
while(hh <= tt && a[q[tt]] >= a[i]) tt--;
q[++tt] = i;
if(i >= k - 1) printf("%d ",a[q[hh]]);
}
puts("");
hh = 0,tt = -1;
for(int i = 0;i < n;i++)
{
//判断对头是否已经滑出窗口
if(hh <= tt && i - k + 1> q[hh]) hh++;//因为一次移进一个数,所以不需要循环
while(hh <= tt && a[q[tt]] <= a[i]) tt--;//求最大值,修改处
q[++tt] = i;
if(i >= k - 1) printf("%d ",a[q[hh]]);
}
puts("");
return 0;
}