目录
一、单链表
动态链表
struct Node
{
int val;
struct Node* next;
}
静态链表
采用数组模拟链表,使用数组 e [ N ] e[N] e[N] 存储节点的值, n e [ N ] ne[N] ne[N] 存储该节点下一节点的下标。
两者优劣
算法答题与笔试中一般使用静态链表,追求速度快,但是会导致空间的浪费。工程中一般使用动态链表,追求对空间的利用率,但相应的使用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
。
D k
,表示删除第k
个插入的数后面的数(当k
为0
时,表示删除头结点)。
I k x
,表示在第k
个插入的数后面插入一个数x
(此操作中k
均大于0
)。
输出格式:
共一行,将整个链表从头到尾输出。
数据范围:
1
≤
m
≤
100000
所有操作保证合法。
1≤m≤100000所有操作保证合法。
1≤m≤100000所有操作保证合法。
输入样例:
10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6
输出样例:
6 4 6 5
代码实现
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int e[N], ne[N], idx, head;
void init()
{
head = -1;
idx = 0;
}
void push_front(int x)
{
e[idx] = x;
ne[idx] = head;
head = idx++;
}
void insert(int k, int x)
{
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx++;
}
void erase(int k)
{
ne[k] = ne[ne[k]];
}
int main()
{
int m = 0;
init();
cin >> m;
while (m--)
{
int k, x;
char op;
cin >> op;
switch (op)
{
case 'H':
cin >> x;
push_front(x); break;
case 'D':
cin >> k;
if (!(k - 1)) head = ne[head]; // 注意头节点的删除
erase(k - 1); break;
case 'I':
cin >> k >> x;
insert(k - 1, x); break;
default:
cout << "Please enter seriously!" << endl; break;
}
}
for (int i = head; ~i; i = ne[i]) cout << e[i] << ' ';
return 0;
}
二、双链表
模拟双链表
题目
题目描述:
实现一个 双链表,双链表初始为空,支持 5 种操作:
①在最左侧插入一个数;
②在最右侧插入一个数;
③将第
k
k
k 个插入的数删除;
④在第
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 行,每行包含一个操作命令,操作命令可能为以下几种:
L x
,表示在链表的最左端插入数x
。
R x
,表示在链表的最右端插入数x
。
D k
,表示将第k
个插入的数删除。
IL k x
,表示在第k
个插入的数左侧插入一个数。
IR k x
,表示在第k
个插入的数右侧插入一个数。
输出格式:
共一行,将整个链表从左到右输出。
数据范围:
1
≤
m
≤
100000
1≤m≤100000
1≤m≤100000 所有操作保证合法。
输入样例:
10
R 7
D 1
L 3
IL 2 10
D 3
IL 2 7
L 8
R 9
IL 4 7
IR 2 2
输出样例:
8 7 7 3 2 9
代码实现
在 “
D
”, “IL
”, “IR
” 要用k+1
的原因是 双链表的起始点是2
,每个插入位置k
的真实位置应该为k-1+2 = k+1
(在单链表中为k-1
)。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int e[N], l[N], r[N], idx;
void init()
{
r[0] = 1, l[1] = 0, idx = 2; // 0这个位置标识,留给了head; 1这个位置标识,留给了tail
}
void insert(int k, int x)
{
e[idx] = x;
l[idx] = k;
r[idx] = r[k];
l[r[k]] = idx;
r[k] = idx++;
}
void erase(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
int main()
{
init();
int m, k, x;
cin >> m;
while (m--)
{
string op;
cin >> op;
if (op == "L")
{
cin >> x;
insert(0, x);
}
else if (op == "R")
{
cin >> x;
insert(l[1], x);
}
else if (op == "D")
{
cin >> k;
erase(k + 1);
}
else if (op == "IL")
{
cin >> k >> x;
insert(l[k + 1], x);
}
else if (op == "IR")
{
cin >> k >> x;
insert(k + 1, x);
}
else
cout << "Please enter seriously!" << endl;
}
for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
return 0;
}
三、栈
模拟栈
题目
题目描述:
实现一个栈,栈初始为空,支持四种操作:
push x
– 向栈顶插入一个数x
;pop
– 从栈顶弹出一个数;empty
– 判断栈是否为空;query
– 查询栈顶元素。
现在要对栈进行 m m m 个操作,其中的每个操作 3 3 3 和操作 4 4 4 都要输出相应的结果。
输入格式
第一行包含整数
m
m
m,表示操作次数。
接下来
m
m
m 行,每行包含一个操作命令,操作命令为 push x
,pop
,empty
,query
中的一种。
输出格式
对于每个 empty
和 query
操作都要输出一个查询结果,每个结果占一行。
其中,empty
操作的查询结果为 YES
或 NO
,query
操作的查询结果为一个整数,表示栈顶元素的值。
数据范围:
1
≤
m
≤
100000
1≤m≤100000
1≤m≤100000
1
≤
x
≤
1
0
9
1≤x≤10^9
1≤x≤109
所有操作保证合法。
输入样例:
10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty
输出样例:
5
5
YES
4
NO
代码实现
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int stack[N], top;
int main()
{
int m, x;
cin >> m;
string op;
top = -1;
while (m--)
{
cin >> op;
if (op == "push")
{
cin >> x;
stack[++top] = x;
}
else if (op == "pop")
--top;
else if (op == "empty")
cout << (top == -1 ? "YES" : "NO") << endl;
else if (op == "query")
if (top != -1) cout << stack[top] << endl;
else
cout << "Please enter seriously!" << endl;
}
return 0;
}
表达式求值
题目
题目描述:
给定一个表达式,其中运算符仅包含 +, -, *, /
(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。
注意:
- 数据保证给定的表达式合法。
- 题目保证符号
−
只作为减号出现,不会作为负号出现,例如, − 1 + 2 −1+2 −1+2, ( 2 + 2 ) ∗ ( − ( 1 + 1 ) + 2 ) (2+2)∗(−(1+1)+2) (2+2)∗(−(1+1)+2) 之类表达式均不会出现。 - 题目保证表达式中所有数字均为正整数。
- 题目保证表达式在中间计算过程以及结果中,均不超过 2 31 − 1 2^{31}−1 231−1。
- 题目中的整除是指向
0
0
0 取整,也就是说对于大于
0
0
0 的结果向下取整,例如
5
/
3
=
1
5/3=1
5/3=1,对于小于
0
0
0 的结果向上取整,例如
5
/
(
1
−
4
)
=
−
1
5/(1−4)=−1
5/(1−4)=−1。C++ 和 Java 中的整除默认是向零取整;Python 中的整除
//
默认向下取整,因此 Python 的eval()
函数中的整除也是向下取整,在本题中不能直接使用。
输入格式:
共一行,为给定表达式。
输出格式:
共一行,为表达式的结果。
数据范围:
表达式的长度不超过
1
0
5
10^5
105。
输入样例:
(2+2)*(1+1)
输出样例:
8
代码实现
创建两个栈,一个用来装符号,一个用来装数字。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<stack>
#include<unordered_map>
using namespace std;
stack<int> num;
stack<char> op;
unordered_map<char, int> h{{ '+', 1}, { '-', 1 }, { '*', 2 }, { '/', 2 }, { '(', 0 }};
void eval()
{
int a = num.top();
num.pop();
int b = num.top();
num.pop();
char p = op.top();
op.pop();
int res = 0;
switch (p)
{
case '+':
res = a + b;
break;
case '-':
res = a - b;
break;
case '/':
res = a / b;
break;
case '*':
res = a * b;
break;
}
num.push(res);
}
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
string s;
cin >> s;
for (int i = 0; i < s.size(); ++i)
{
if (isdigit(s[i]))
{
int x = 0;
while (i < s.size() && isdigit(s[i]))
{
x = x * 10 + s[i] - '0';
i++;
}
i--;
num.push(x);
}
else if (s[i] == '(') op.push(s[i]);
else if (s[i] == ')')
{
while (op.top() != '(') eval();
op.pop();
}
else
{
while (op.size() && h[op.top()] >= h[s[i]]) eval();
op.push(s[i]);
}
}
while (op.size()) eval();
cout << num.top() << endl;
return 0;
}
四、队列
模拟队列
题目
题目描述:
实现一个队列,队列初始为空,支持四种操作:
push x
– 向队尾插入一个数x
;pop
– 从队头弹出一个数;empty
– 判断队列是否为空;query
– 查询队头元素。
现在要对队列进行 m m m 个操作,其中的每个操作 3 3 3 和操作 4 4 4 都要输出相应的结果。
输入格式:
第一行包含整数
m
m
m,表示操作次数。
接下来
m
m
m 行,每行包含一个操作命令,操作命令为 push x
,pop
,empty
,query
中的一种。
输出格式:
对于每个 empty
和 query
操作都要输出一个查询结果,每个结果占一行。
其中,empty
操作的查询结果为 YES
或 NO
,query
操作的查询结果为一个整数,表示队头元素的值。
数据范围:
1
≤
M
≤
100000
1≤M≤100000
1≤M≤100000
1
≤
x
≤
1
0
9
1≤x≤10^9
1≤x≤109
所有操作保证合法。
输入样例:
10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6
输出样例:
NO
6
YES
4
代码实现
普通队列解法
左开右闭的区间——[hh, tt)
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N], hh, tt;
int main()
{
int m, x;
cin >> m;
string op;
while (m--)
{
cin >> op;
if (op == "push")
{
cin >> x;
q[tt++] = x;
}
else if (op == "pop")
++hh;
else if (op == "empty")
cout << (hh >= tt ? "YES" : "NO") << endl;
else if (op == "query")
cout << q[hh] << endl;
else
cout << "Please enter seriously!" << endl;
}
}
循环队列解法
相比普通队列的解法,循环队列可以在再利用空间, 同时也可以增加判断队列是否占满的函数。
if (tt == N) tt = 0; // 循环:从N - 1后回到0
if (hh == N) hh = 0; // 循环:从N - 1后回到0
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N], hh, tt;
int main()
{
int m, x;
cin >> m;
string op;
while (m--)
{
cin >> op;
if (op == "push")
{
cin >> x;
q[tt++] = x;
if (tt == N) tt = 0; // 循环:从N - 1后回到0
}
else if (op == "pop")
{
++hh;
if (hh == N) hh = 0; // 循环:从N - 1后回到0
}
else if (op == "empty")
cout << (hh >= tt ? "YES" : "NO") << endl;
else if (op == "query")
cout << q[hh] << endl;
else
cout << "Please enter seriously!" << endl;
}
}
五、单调栈
实现单调栈
题目
题目描述:
给定一个长度为
N
N
N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出
−
1
−1
−1。
输入格式:
第一行包含整数
n
n
n,表示数列中元素个数。
第二行包含 n n n 个整数,表示整数数列。
输出格式:
共一行,包含
n
n
n 个整数,其中第
i
i
i 个数表示第
i
i
i 个数的左边第一个比它小的数,如果不存在则输出
−
1
−1
−1。
数据范围:
1
≤
N
≤
1
0
5
1≤N≤10^5
1≤N≤105
1 ≤ n ≤ 1 0 9 1≤n≤10^9 1≤n≤109
输入样例:
5
3 4 2 7 5
输出样例:
-1 3 -1 2 2
单调栈原理
单调栈里装的是什么?
动态维护一个栈,把后续问题的答案都维持在这个栈中,把肯定不再有用的数字从栈中去掉。
-
动态维护,随进随维护,不需预处理。
-
将 O ( N 2 ) O(N^2) O(N2) 的时间复杂度降为 O ( N ) O(N) O(N)。
-
此类 左侧(或右侧)最近,比自己大的(或小的),用单调栈。
代码实现
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int stk[N], tt = -1;
int main()
{
cin.tie(nullptr);
ios::sync_with_stdio(false); // 优化输入\输出速度
int n, x;
cin >> n;
for (int i = 0; i < n; ++i)
{
cin >> x;
while (tt != -1 && stk[tt] >= x) tt--;
if (tt != -1) cout << stk[tt] << ' ';
else cout << -1 << ' ';
stk[++tt] = x;
}
return 0;
}
优化输入输出速度:
cin
cout
相比于 scanf
printf
,速度较为缓慢。
可以使用以下代码进行优化:
cin.tie(nullptr);
ios::sync_with_stdio(false);
cin.tie(0)
的作用和 cin.tie(nullptr)
是等价的,用于将cin与其他输出流的绑定解除,同时将cin的缓冲区设置为无缓冲(unbuffered),这样在读取输入时不会有额外的I/O操作。因为 cin
默认与 cout
绑定,并且在每次输入之前都会刷新cout的缓冲区,因此会导致额外的I/O操作,从而降低输入效率。
ios::sync_with_stdio(false)
的作用同样是为了提高输入输出效率,但是和cin.tie(0)
的作用不同。ios::sync_with_stdio(false)
是将C++标准流和C标准流的同步关闭,这样可以提高C++标准流的输入输出效率,但是会影响到C标准流(如 printf
和 scanf
)的输出效率。因此,如果需要同时使用C++标准流和C标准流,不应该关闭同步。
六、单调队列
滑动窗口
题目
题目描述:
有一个大小为
k
k
k 的滑动窗口,它从数组的最左边移动到最右边。
您只能在窗口中看到 k k k个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为[1 3 −1 −3 5 3 6 7]
,
k
k
k 为
3
3
3。
窗口位置 | 最小值 | 最大值 |
---|---|---|
[ [ [ 1 1 1 3 3 3 − 1 −1 −1 ] ] ] − 3 −3 −3 5 5 5 3 3 3 6 6 6 7 7 7 | − 1 −1 −1 | 3 3 3 |
1 1 1 [ [ [ 3 3 3 − 1 −1 −1 − 3 −3 −3 ] ] ] 5 5 5 3 3 3 6 6 6 7 7 7 | − 3 −3 −3 | 3 3 3 |
1 1 1 3 3 3 [ [ [ − 1 −1 −1 − 3 −3 −3 5 5 5 ] ] ] 3 3 3 6 6 6 7 7 7 | − 3 −3 −3 | 5 5 5 |
1 1 1 3 3 3 − 1 −1 −1 [ [ [ − 3 −3 −3 5 5 5 3 3 3 ] ] ] 6 6 6 7 7 7 | − 3 −3 −3 | 5 5 5 |
1 1 1 3 3 3 − 1 −1 −1 − 3 −3 −3 [ [ [ 5 5 5 3 3 3 6 6 6 ] ] ] 7 7 7 | 3 3 3 | 6 6 6 |
1 1 1 3 3 3 − 1 −1 −1 − 3 −3 −3 5 5 5 [ [ [ 3 3 3 6 6 6 7 7 7 ] ] ] | 3 3 3 | 7 7 7 |
您的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式:
输入包含两行。
第一行包含两个整数 n n n 和 k k k ,分别代表数组长度和滑动窗口的长度。
第二行有 n n n 个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式:
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
输入样例:
8 3
1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
代码实现
这里使用的是双端队列。
双端队列(Double Ended Queue,简称为Deque)是一种允许在队列的两端进行插入和删除操作的数据结构。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int q[N], hh, tt;
int a[N], k;
int main()
{
cin.tie(nullptr);
ios::sync_with_stdio(false);
int n, k;
cin >> n >> k;
for (int i = 0; i < n; ++i) cin >> a[i];
hh = 0, tt = -1;
for (int i = 0; i < n; ++i)
{
if (hh <= tt && q[hh] < i + 1 - k) hh++;
while (hh <= tt && a[q[tt]] >= a[i]) tt--; // 注意是从队尾往前去除较大数
q[++tt] = i;
if (i + 1 - k >= 0) cout << a[q[hh]] << ' ';
}
cout << endl;
hh = 0, tt = -1;
for (int i = 0; i < n; ++i)
{
if (hh <= tt && q[hh] < i + 1 - k) hh++;
while (hh <= tt && a[q[tt]] <= a[i]) tt--; // 注意是从队尾往前去除较小数
q[++tt] = i;
if (i + 1 - k >= 0) cout << a[q[hh]] << ' ';
}
}