数据结构
目录
- 数据结构
- 1.链表实现
- 单链表
- 双链表
- 2.栈(先进后出,后进先出)
- 3.单调栈
- 4.队列(先进先出)
- 5.单调队列
- 6.小根堆
- 操作
- 7.KMP
- 8.Trie树(字典树)
1.链表实现
单链表
#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;
}
双链表
#include <iostream>
using namespace std;
const int N = 100010;
int m;
int e[N], l[N], r[N], idx;
// 在节点a的右边插入一个数x
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx ++ ;
}
// 删除节点a
void remove(int a)
{
l[r[a]] = l[a];
r[l[a]] = r[a];
}
int main()
{
cin >> m;
// 0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
while (m -- )
{
string op;
cin >> op;
int k, x;
if (op == "L")
{
cin >> x;
insert(0, x);
}
else if (op == "R")
{
cin >> x;
insert(l[1], x);
}
else if (op == "D")
{
cin >> k;
remove(k + 1);
}
else if (op == "IL")
{
cin >> k >> x;
insert(l[k + 1], x);
}
else
{
cin >> k >> x;
insert(k + 1, x);
}
}
for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
cout << endl;
return 0;
}
2.栈(先进后出,后进先出)
数组模拟栈:
用top表示栈顶所在的索引。初始时,top = -1。表示没有元素。
push x :栈顶所在索引往后移动一格,然后放入x。st[++top] = x。
pop : top 往前移动一格。top–。
empty :top 大于等于 0 栈非空,小于 0 栈空。top == -1 ? “YES” : “NO”
query : 返回栈顶元素。st[top]
#include <iostream>
using namespace std;
const int N = 100010;
int m;
int stk[N], tt;
int main()
{
cin >> m;
while (m -- )
{
string op;
int x;
cin >> op;
if (op == "push")
{
cin >> x;
stk[ ++ tt] = x;
}
else if (op == "pop") tt -- ;
else if (op == "empty") cout << (tt ? "NO" : "YES") << endl;
else cout << stk[tt] << endl;
}
return 0;
}
// stl版本
#include <iostream>
#include <stack>
using namespace std;
stack<int> stk;
int main() {
int n;
cin >> n;
while (n--) {
string s;
cin >> s;
int x;
if (s == "push") {
cin >> x;
stk.push(x);
}
else if (s == "pop") {
stk.pop();
}
else if (s == "query")
cout << stk.top() << endl;
else
cout << ((stk.empty()) ? "YES": "NO") << endl;
}
return 0;
}
3.单调栈
- 常见模型: 找出每个数左边(右边)离他最近的比他大/小的数
int tt = 0;
for (int i = 1; i <= n; ++i) {
while (tt && check(q[tt], i)) tt --;
stk[++ tt] = i
}
**实例:**给定一个长度为 N的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 1e5 + 10;
int stk[N], tt;
int main() {
int n;
scanf("%d", &n);
while (n--) {
int x;
cin >> x;
while (tt && stk[tt] >= x) tt --;
if (!tt) printf("-1 ");
else printf("%d ", stk[tt]);
stk[++tt] = x;
}
return 0;
}
4.队列(先进先出)
用一个数组 q 保存数据。
用 hh 代表队头,q[hh] 就是队头元素, q[hh + 1] 就是第二个元素。
用 tt 代表队尾, q[tt] 就是队尾元素, q[tt + 1] 就是下一次入队,元素应该放的位置。
[hh, tt] 左闭右闭,代表队列中元素所在的区间。
出队pop:因为 hh 代表队头,[hh, tt] 代表元素所在区间。所以出队可以用 hh++实现,hh++后,区间变为[hh + 1, tt]。
入队push:因为 tt 代表队尾,[hh, tt] 代表元素所在区间。所以入出队可以用 tt++实现,tt++后,区间变为[hh, tt + 1], 然后在q[tt+1]位置放入入队元素。
是否为空empty:[hh, tt] 代表元素所在区间,当区间非空的时候,对列非空。也就是tt >= hh的时候,对列非空。
询问队头query:用 hh 代表队头,q[hh] 就是队头元素,返回 q[hh] 即可。
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N];
int hh, tt = -1;
int main() {
int n;
cin >> n;
while (n--) {
string s;
cin >> s;
int x;
if (s == "push") {
cin >> x;
q[++tt] = x;
}
else if (s == "empty")
cout << (hh <= tt? "NO": "YES") << endl;
else if (s == "query")
cout << q[hh] << endl;
else ++hh;
}
return 0;
}
// STL版本
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <vector>
#include <queue>
#include <algorithm>
#include <unordered_map>
#include <unordered_set>
#include <set>
#include <map>
#define ll long long
#define PII pair<int, int>
using namespace std;
queue<int> q;
int main()
{
int n;
cin >> n;
while (n--) {
int x; string s;
cin >> s;
if (s == "push")
{
cin >> x;
q.push(x);
}
else if (s == "pop") {
q.pop();
}
else if (s == "empty") {
cout << (q.empty()? "YES": "NO") << endl;
}
else {
cout << q.front() << endl;
}
}
return 0;
}
5.单调队列
滑动窗口
给定一个大小为 n ≤ 1 0 6 n≤10^6 n≤106 的数组。
有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k 个数字。
每次滑动窗口向右移动一个位置。
只要后一个进队的比前面的小,前面的将永无出头之日
#include <iostream>
#include <cstdio>
#include <deque>
using namespace std;
const int N = 1e6 + 10;
int q[N], a[N];
int hh, tt = -1;
int main() {
int n, k;
scanf("%d%d", &n, &k);
for (int i = 0; i < n; ++i) scanf("%d", &a[i]);
for (int i = 0; i < n; ++i) {
while (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]]);
}
printf("\n");
hh = 0, tt = -1;
for (int i = 0; i < n; ++i) {
while (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]]);
}
return 0;
}
// stl版本
#include <iostream>
#include <cstdio>
#include <deque>
using namespace std;
const int N = 1e6 + 10;
int q[N], a[N];
int hh, tt = -1;
int main() {
int n, k;
scanf("%d%d", &n, &k);
for (int i = 0; i < n; ++i) scanf("%d", &a[i]);
deque<int> dq;
for (int i = 0; i < n; ++i) {
// 判断队头是否划出窗口
while (!dq.empty() && i - k + 1 > dq.front()) dq.pop_front();
while (!dq.empty() && a[dq.back()] >= a[i]) dq.pop_back();
dq.push_back(i);
if (i >= k - 1) printf("%d ", a[dq.front()]);
}
printf("\n");
dq.clear();
for (int i = 0; i < n; ++i) {
// 判断队头是否划出窗口
while (!dq.empty() && i - k + 1 > dq.front()) dq.pop_front();
while (!dq.empty() && a[dq.back()] <= a[i]) dq.pop_back();
dq.push_back(i);
if (i >= k - 1) printf("%d ", a[dq.front()]);
}
return 0;
}
6.小根堆
优先队列–大根堆
完全二叉树, 每个父节点都小于两个子节点。
存储:x的做儿子2x,有儿子2x+1(下标从1 开始)
**down操作:**把某个数变大,将其往下移
void down(int u) {
int t = u;
if (2 * u <= size && h[u] < h[2 * u]) t = 2 * u;
if (2 * u + 1 <= size && h[u] < h[2 * u + 1]) t = 2 * u + 1;
if (u != t) {
swap(h[u], h[t]);
down(t);
}
}
void up(int u) {
while (u / 2 && h[u/2] > h[u]) {
swap(h[u], h[u/2]);
u
}
}
操作
- 插入一个数
heap[++size] = x;
up(size);
- 求集合最小值
heap[1]
- 删除最小值
- 把最后元素移到堆顶
size--
- down
- 把最后元素移到堆顶
heap[1] = heap[size];
size--;
down(1);
- 删除任意一个元素
heap[k] = heap[size];
size --;
down(k);
up(k);
- 修改任意一个元素
heap[k] = x;
down(k);
up(k);
维护一个集合,初始时集合为空,支持如下几种操作:
I x
,插入一个数 x;PM
,输出当前集合中的最小值;DM
,删除当前集合中的最小值(数据保证此时的最小值唯一);D k
,删除第 k 个插入的数;C k x
,修改第 k 个插入的数,将其变为 x;现在要进行 N� 次操作,对于所有第 22 个操作,输出当前集合的最小值。
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 100010;
int h[N], ph[N], hp[N], cnt;
void heap_swap(int a, int b)
{
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}
void down(int u)
{
int t = u;
if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t)
{
heap_swap(u, t);
down(t);
}
}
void up(int u)
{
while (u / 2 && h[u] < h[u / 2])
{
heap_swap(u, u / 2);
u >>= 1;
}
}
int main()
{
int n, m = 0;
scanf("%d", &n);
while (n -- )
{
char op[5];
int k, x;
scanf("%s", op);
if (!strcmp(op, "I"))
{
scanf("%d", &x);
cnt ++ ;
m ++ ;
ph[m] = cnt, hp[cnt] = m;
h[cnt] = x;
up(cnt);
}
else if (!strcmp(op, "PM")) printf("%d\n", h[1]);
else if (!strcmp(op, "DM"))
{
heap_swap(1, cnt);
cnt -- ;
down(1);
}
else if (!strcmp(op, "D"))
{
scanf("%d", &k);
k = ph[k];
heap_swap(k, cnt);
cnt -- ;
up(k);
down(k);
}
else
{
scanf("%d%d", &k, &x);
k = ph[k];
h[k] = x;
up(k);
down(k);
}
}
return 0;
}
7.KMP
next数组的值是代表着字符串(模式串)的前缀与后缀相同的最大长度,(不能包括自身)
给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模式串 P 在字符串 S中多次作为子串出现。
求出模式串 P 在字符串 S 中所有出现的位置的起始下标。
#include <iostream>
using namespace std;
const int N = 1e5 + 10, M = 1e6 + 10;
int ne[N];
char p[N], s[M];
int main() {
int n, m;
cin >> n >> p + 1 >> m >> s + 1;
// next数组
for (int i = 2, j = 0; i <= n; i ++)
{
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[1 + j]) j ++;
ne[i] = j;
}
for (int i = 1, j = 0; i <= m; i ++)
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[1 + j]) j ++;
if (j == n) cout << i - n << ' ';
}
return 0;
}
8.Trie树(字典树)
维护一个字符串集合,支持两种操作:
I x
向集合中插入一个字符串 x;Q x
询问一个字符串在集合中出现了多少次。共有 N 个操作,所有输入的字符串总长度不超过 105105,字符串仅包含小写英文字母。
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int son[N][26], cnt[N], idx;
void insert(char str[]) {
int p = 0;
for (int i = 0; str[i]; ++i) {
int u = str[i] - 'a';
if (!son[p][u]) son[p][u] = ++ idx;
p = son[p][u];
}
cnt[p]++;
}
int query(char str[]) {
int p = 0;
for (int i = 0; str[i]; ++i) {
int u = str[i] - 'a';
if (!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}
int main() {
int n;
cin >> n;
while (n--) {
char c;
char s[N];
cin >> c >> s;
if (c == 'I')
insert(s);
else
cout << query(s) << endl;
}
return 0;
}