数据结构
写在前面
感谢y总。
链表
单链表
#include <iostream>
using namespace std;
const int N = 1e6+10;
//head表示头指针,e[i]表示节点i的值,ne[i]表示节点i的next指针,idx存储当前未使用的那个点
//idx = 0,1,2,3,...
int head = -1,idx = 0, e[N], ne[N];
//将x插入到头节点
void add_to_head(int x){
e[idx] = x;
ne[idx] = head;
head = idx;
idx++;
}
//将x插入到下标是k的点的后面
void add(int k,int x){
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
idx++;
}
//将下标是k的点的后面的点删掉
void remove_k(int k){
ne[k] = ne[ne[k]];
}
//删除头节点
void remove_head(){
head = ne[head];
}
双链表
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;
// 初始化
void init()
{
//0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
}
// 在节点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];
}
栈
// tt表示栈顶
int stk[N], tt = 0;
// 向栈顶插入一个数
stk[ ++ tt] = x;
// 从栈顶弹出一个数
tt -- ;
// 栈顶的值
stk[tt];
// 判断栈是否为空,如果 tt > 0,则表示不为空
if (tt > 0)
{
}
#include <iostream>
#include <unordered_map>
using namespace std;
const int N = 100010;
int st_num[N], st_op[N];
int top1 = 0, top2 = 0;
void eval(){
auto b = st_num[top1]; top1--;
auto a = st_num[top1]; top1--;
auto x = st_op[top2]; top2--;
if (x == '+') st_num[++top1] = a+b;
if (x == '-') st_num[++top1] = a-b;
if (x == '*') st_num[++top1] = a*b;
if (x == '/') st_num[++top1] = a/b;
}
int main(){
unordered_map<char, int> pr{
{'+' , 1}, { '-' , 1 }, {'*', 2}, {'/', 2}
};
string str;
cin>>str;
for (int i = 0; i < str.length(); i++){
auto c = str[i];
if (isdigit(c)){
int x = 0, j = i;
while (j < str.length() && isdigit(str[j])){
x = x * 10 + str[j++] - '0';
}
i = j-1;
st_num[++top1] = x;
}
else if (c == '(') st_op[++top2] = c;
else if (c == ')') {
while (st_op[top2] != '(') eval();
top2--;
}
else {
while (top2 && pr[st_op[top2]] >= pr[c]) eval();
st_op[++top2] = c;
}
}
while (top2) eval();
cout<< st_num[top1]<<endl;
}
队列
// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;
// 向队尾插入一个数
q[ ++ tt] = x;
// 从队头弹出一个数
hh ++ ;
// 队头的值
q[hh];
// 判断队列是否为空,如果 hh <= tt,则表示不为空
if (hh <= tt)
{
}
单调栈
常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
while (tt && check(stk[tt], i)) tt -- ;
stk[ ++ tt] = i;
}
单调队列
常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
while (hh <= tt && check_out(q[hh])) hh ++ ; // 判断队头是否滑出窗口
while (hh <= tt && check(q[tt], i)) tt -- ;
q[ ++ tt] = i;
}
KMP算法
时间复杂度为\(O(n + m)\)
精髓是主串的指针i永远不回退,当发现不匹配时,子串指针j按照next数组进行回退。若回退到0,i++。
假设主串和子串都是下标从1开始匹配。
#include <iostream>
using namespace std;
const int N = 100010, M = 1000010;
char s[M], p[N];
int m, n, ne[N];
int main(){
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[j+1]) j++;
ne[i] = j;
}
// kmp匹配过程
for (int i= 1, j = 0; i <= m; i ++){
while (j && s[i] != p[j+1]) j = ne[j];
if (s[i] == p[j+1]) j++;
if (j == n){
// cout << i - j + 1 << ' ';
cout << i - j<< ' '; // 由于在读入时下标为1开始,所以最终将i-j+1改成i-j
j = ne[j];
}
}
}
Trie树
用于高效存储和查找某一字符串是否存在集合中。
常用于解决前缀的快速合并和查找。
节点标记:表示以当前字母结尾的是有单词的。
字符串存储
#include <iostream>
using namespace std;
const int N = 100010;
int son[N][26], cnt[N], idx = 0; // 存储子节点、以i结尾的字符串个数、节点编号
char str[N];
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;
scanf("%d", &n);
while (n--){
char op[2];
scanf("%s%s", op, str);
if (*op == 'I') insert(str);
else cout<<query(str)<<endl;
}
}
整数存储
大致思路:
将每个数的二进制状态由高位到低位保存到Trie树中。对于数x中的第i位u,Trie树中有存储son[p][!u]则向该方向搜索,此时第i位的异或结果为1,保证高位异或结果尽可能大;如若没有,则只能走向son[p][u]。
一些要注意的点:
- 异或操作:^
- 取出二进制数的第i位的简单写法:int u = x >> i & 1;
- 建议边插入边查询(\(C^2_n\)),依旧能够保证结果。
#include <iostream>
using namespace std;
const int N = 100010, M = N * 31;
int son[M][2], idx = 0;
void insert(int x){
int p = 0; // 根节点
for (int i = 30; i >= 0; i -- ){ // 插入操作
int u = x >> i & 1; // 取出x二进制下第i位的数
if (!son[p][u]) son[p][u] = ++ idx;
p = son[p][u];
}
}
int query(int x){
int p = 0, ans = 0;
for (int i = 30; i >= 0; i -- ){
int u = x >> i & 1;
if (son[p][!u]){
p = son[p][!u];
ans = ans * 2 + !u;
}
else{
p = son[p][u];
ans = ans * 2 + u;
}
}
return ans;
}
int main(){
int n, res = 0;
scanf("%d", &n);
for (int i = 0; i < n; i ++ ){ // 每次读入一个数,和它之前的进行异或比较即可
int num;
scanf("%d", &num);
insert(num);
res = max(res, num ^ query(num)); // 更新最大异或结果
}
cout << res << endl;
}
并查集
近乎O(1)常见操作:
- 将两个集合合并
- 询问两个元素是否在同一个集合当中
基本原理:
- 每个集合用一棵树表示。树根的编号就是集合的编号。
- 每个节点存储它的父节点p[x]
如何判断树根:if (p[x] == x) 父节点编号和节点编号相同
如何求x的集合编号:while (p[x] != x) x = p[x];
如何将两个集合合并:px是x的集合编号,py是y的集合编号,让p[x] = y。
优化:路经压缩。查询x之后,将x到根节点路径上的所有点全部指向根节点。
#include <iostream>
using namespace std;
const int N = 100010;
int p[N];
int find(int x){ //返回x的祖宗节点+路经压缩
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main(){
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++) p[i] = i;
while (m--){
char op[2];
int a, b;
scanf("%s%d%d", op, &a, &b); // scanf读字符串自动忽略空格和回车
if (op[0] == 'M') p[find(a)] = find(b); // 集合合并
else {
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
}
}
如何根据已知元素的两两关系得到任意元素之间的两两关系?
确定并查集树中每个节点和根节点之间的关系,用它到根节点的距离表示:
- 模三余一:可以吃根节点
- 模三余二:可以吃一,也可以被根节点吃
- 模三余零:和根节点同类
d[N]:维护当前节点到父节点之间的距离。路径压缩之后,父节点也就是根节点。
#include <iostream>
using namespace std;
const int N = 100010;
int p[N], d[N];
int find(int x){
if (p[x] != x){
int t = find(p[x]); // 找到根节点
d[x] += d[p[x]]; // x到父节点的距离加上p[x]到根节点的距离
p[x] = t; // 将x的父节点指向根节点
}
return p[x];
}
int main(){
int n, k, ans = 0;
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++) p[i] = i;
while (k--){
int l, x, y;
cin>>l>>x>>y;
if (x > n || y > n) {
ans++;
continue;
}
int px = find(x), py = find(y);
if (l == 1){ // 操作类别为1
if (px == py && (d[x] - d[y]) % 3) ans++;// x和y在同一集合中,此时判断两两关系
else if (px != py){ // 此时x和y还不在集合中。对二者合并,并修改到根节点的距离。
p[px] = py;
d[px] = d[y] - d[x];
}
}
else { // 操作类别为2
if (px == py && (d[x] - d[y] - 1) % 3) ans++;
else if (px != py){
p[px] = py;
d[px] = d[y] - d[x] + 1;
}
}
}
cout<<ans<<endl;
}
- 坐标(x, y)转id
- set的维护和插入、判断是否含有某个数
- 坐标是否越界的判断
#include <iostream>
#include <set>
#include <algorithm>
using namespace std;
const int M = 1010;
const int N = 1e6 + 10;
int n, m, p[N], size_[N];
char g[M][M];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, -1, 0, 1};
int find(int x){
if (p[x] != x)
p[x] = find(p[x]);
return p[x];
}
int get(int x, int y){
return (x - 1) * m + y;
}
int main(){
scanf("%d%d", &n, &m);
for (int i = 1; i <= n * m; i++)
p[i] = i, size_[i] = 1;
char ch;
scanf("%c", &ch);
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
scanf("%c", &g[i][j]);
}
scanf("%c", &ch);
}
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
if (g[i][j] == '*')
continue;
int id1 = get(i, j);
for (int k = 0; k < 4; k++){
int x = i + dx[k], y = j + dy[k];
if (x >= 1 && x <= n && y >= 1 && y <= m && g[x][y] == '.'){
int id2 = get(x, y);
if (find(id1) != find(id2)){
size_[find(id2)] += size_[find(id1)];
p[find(id1)] = find(id2);
}
}
}
}
}
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
if (g[i][j] == '.'){
printf("%c", g[i][j]);
continue;
}
int ans = 1;
set<int> nodes;
for (int k = 0; k < 4; k++){
int x = i + dx[k], y = j + dy[k];
if (x >= 1 && x <= n && y >= 1 && y <= m && g[x][y] == '.'){
int id = get(x, y);
int father = find(id);
if (! nodes.count(father)){
nodes.insert(father);
ans += size_[father];
}
}
}
printf("%d", ans % 10);
}
printf("\n");
}
}
堆
手写一个堆,支持的操作:
- 插入一个数:heap[++size] = x; up(size);
- 求集合当中的最小值:heap[1];
- 删除最小值:heap[1] = heap[size]; size --; down(1);
- 删除任意一个数(stl不支持):heap[k] = heap[size]; size--; up(k); down(k);
- 修改任意一个数(stl不支持):heap[k] = x; up(k); down(k);
#include <iostream>
#include <string.h>
using namespace std;
const int N = 100010;
int h[N], ph[N], hp[N], size_ = 0;
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 <= size_ && h[u * 2] < h[t])
t = u * 2;
if (u * 2 + 1 <= size_ && h[u * 2 + 1] < h[t])
t = u * 2 + 1;
if (t != u){
heap_swap(t, u);
down(t);
}
}
void up(int u){
while (u / 2 && h[u / 2] > h[u]){
heap_swap(u, u / 2);
u /= 2;
}
}
int main(){
int n, m = 0;
scanf("%d", &n);
while (n--){
char str[2];
int k, x;
scanf("%s", str);
if (!strcmp(str, "I")){
cin >> x;
m++;
h[++size] = x, ph[m] = size_, hp[size_] = m;
up(size_);
}
else if (!strcmp(str, "PM"))
cout << h[1] << endl;
else if (!strcmp(str, "DM")){
heap_swap(1, size_);
size_--;
down(1);
}
else if (!strcmp(str, "D")){
cin >> k;
k = ph[k];
heap_swap(k, size_);
size_--;
down(k), up(k);
}
else{
cin >> k >> x;
k = ph[k];
h[k] = x;
down(k), up(k);
}
}
}
哈希表
大规模数据向小规模值域的映射, h[k] = x。
常见操作:添加一个数、查找一个数。
对于删除操作:不是真的删除,开一个bool变量表示。
时间复杂度趋近于O(1)。
哈希冲突:
-
把两个不同的数映射为了同一个数。
-
通过开放寻址法和拉链法解决。
-
存储结构
- 开放寻址法
- 拉链法
-
字符串哈希方式
开放寻址法
只开一个一维数组,没开链表。数组长度为数据规模的2-3倍。
h(x) = k。从第k个坑位开始向后遍历,直到有坑位为止。
int h[N];
int null = 0x3f3f3f3f;
memset(h, 0x3f, sizeof h); // 按字节进行0x3f,int型数据有四个字节
// 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
int find(int x){
int t = (x % N + N) % N;
while (h[t] != null && h[t] != x) {
t ++ ;
if (t == N) t = 0;
}
return t;
}
int k = find(x);
判断h[k]是否等于null。
拉链法
开一个数组存储所有的哈希值。在第i个哈希值拉一条链,附上原数值。
模的数尽量取质数,降低哈希冲突几率。
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100003;
int h[N], e[N], ne[N], idx = 0;
void insert(int x){
int k = (x % N + N) % N; //考虑x取负的情况,要将值域全映射为正数。
e[idx] = x, ne[idx] = h[k], h[k] = idx ++; # 单链表的头插法
}
bool find(int x){
int k = (x % N + N) % N;
for (int i = h[k]; i != -1; i = ne[i]){
if (e[i] == x) return true;
}
return false;
}
int main(){
memset(h, -1, sizeof h);
int n;
cin>>n;
while (n--){
char op[2];
int x;
scanf("%s %d",op, &x);
if (*op == 'I') insert(x);
else {
if (find(x)) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
}
}
字符串前缀哈希法
快速判断两个区间的字符子串是否相同。
前缀哈希值的定义:将n位字符串映射为n位p进制的数,再模上Q。这样就将字符串映射到了0~Q-1的数字。
- 在单个字母映射规则中,不能映射为0。(如'A', 'AA', ..., 都是0)
- 通常情况下,完全不考虑冲突情况。
- 经验值:P取131 or 13331, Q取\(2^{64}\)。
- 好处:可以由求得的前缀哈希得到某一指定的子串哈希。
- 用unsighed long long来存储所有的h,这样不用取模。
#include <iostream>
using namespace std;
typedef unsigned long long ULL;
const int N = 100010, P = 131;
char str[N];
ULL h[N], p[N];
ULL get(int l, int r){ // 获得子串区间l-r的哈希值,由于使用了ULL,不用再模上Q
return h[r] - h[l-1] * p[r - l + 1];
}
int main(){
int n, m;
cin>>n>>m;
scanf("%s", str+1);
p[0] = 1;
for (int i = 1; i <= n; i ++){
h[i] = h[i-1]*P + str[i];
p[i] = p[i-1] * P;
}
while (m--){
int l1, r1, l2, r2;
cin>>l1>>r1>>l2>>r2;
if (get(l1, r1) == get(l2, r2)) puts("Yes");
else puts("No");
}
}
C++ STL
string
- size()/length()
- clear()
- empty()
- string a = "zzy"; a += "cs"
- a.substr(i, len): 从第i个位置开始,长度为n的子串。省略第二个参数,一直到串尾。
pair
pair<type1, type2>p用于存储二元组。前后两个数据类型任意。(如pair<int, string>p)
- 取得第一个元素:p.first
- 取得第二个元素:p.second
- 支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
- 定义:p =
- 三种属性的存储:pair<int, pair<int, int>>p
stack
- st.size()
- st.empty()
- st.top()
- st.push()
- st.pop()
queue
- q.size()
- q.empty()
- q.push()
- q.pop()
- q.front()
- q.back()
deque
- q.size()
- q.empty()
- q.push_front(), q.pop_front()
- q.push_back(), q.pop_back()
- q.front(), q.back()
- q.begin(), q.end()
- 支持按索引寻址[]
优先队列
priority_queue:默认是大根堆。
小根堆的定义:
priority_queue<int, vector<int>, greater<int>> heap;
常见方法:
priority_queue<int> heap;
heap.size()
heap.empty()
heap.push() 插入一个元素
heap.top() 返回堆顶元素
heap.pop() 弹出堆顶元素
set, map
set<int> s
multi_set<int> ms
set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
size()
empty()
clear()
begin()/end()
++, -- 返回前驱和后继,时间复杂度 O(logn)
set/multiset
insert() 插入一个数
find() 查找一个数
count() 返回某一个数的个数
erase()
(1) 输入是一个数x,删除所有x O(k + logn)
(2) 输入一个迭代器,删除这个迭代器
lower_bound()/upper_bound()
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器
map/multimap
insert() 插入的数是一个pair
erase() 输入的参数是pair或者迭代器
find()
[]用作赋值和查找,可以像数组一样操作!
注意multimap不支持此操作。 时间复杂度是 O(logn)
lower_bound()/upper_bound()

浙公网安备 33010602011771号