链表
单链表
静态链表
用数组模拟静态链表不容易超时,动态链表中
new
一个node
花费的时间开销很大,很容易超时。
// 基础元素
int head, e[N], ne[N], idx;
// head头指针,指向第一个节点
// e[N],记录第i个节点的值
// ne[N],记录第i个节点指向的下一个节点的索引
// idx, 全局变量,分配给下一个节点的指针。
// 初始化
void init()
{
head = -1; //表明链表为空
idx = 0; //从0号位置开始存储
}
// 在链表头部加入一个数
void insert(int a)
{
e[idx] = a;
ne[idx] = ne[head];
head = idx ++;
}
// 删除第k个插入的数后面的数
void delete(int k)
{
ne[k] = ne[ne[k]];
}
// 在第k个插入的数后面插入一个数
void insert(int k, int x)
{
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx ++;
}
// 输出链表
void print()
{
for (int i = head; ~i; i = ne[i]) print("%d\n", e[i]);
}
注意:
- 链表中前后节点的位置和真实数组中的位置不一定是连续的。
主要用处: 邻接表(存储图、树)
双链表
静态双链表
//基础元素
int e[N], l[N], r[N], idx;
// l[N]:表示第i个节点的左指针指向的元素
// r[N]:表示第i个节点的右指针指向的元素
// 初始化
void init()
{
// idx=0为头节点,idx=1为尾节点,下标从2开始存储真正的元素
l[1] = 0;
r[0] = 1;
idx = 2;
}
// 在第k个插入的数后插入x
void insert(int k, int x)
{
e[idx] = x;
l[idx] = k;
r[idx] = r[k];
l[r[k]] = idx;
r[k] = idx ++;
}
// 在第k个插入的数前边插入x
void insertL(int k, int x)
{
insert(l[k], x);
}
// 删除第k个元素
void deleteK(int k)
{
l[r[k]] = l[k];
r[l[k]] = r[k];
}
// 遍历链表
void print()
{
// 正序遍历
for (int i = r[0]; ~i; i = r[i]) printf("%d ", e[i]);
// 逆序遍历
// for (int i = l[1]; ~i; i = l[i]) printf("%d", e[i]);
}
栈
// 基本元素
int e[N], tt;
// tt:表示栈顶
void init()
{
tt = -1;
}
void push(int x)
{
e[++ tt] = x;
}
void pop()
{
tt --;
}
int top()
{
return e[tt];
}
bool empty()
{
return tt == -1;
}
队列
// 基本元素
int q[N], hh = 0, tt = -1;
// 默认头指针是0,尾指针是-1
// 在队尾插入x
void push_back()
{
q[++ tt] = x;
}
// 队头弹出元素
void pop_front()
{
if (hh <= tt) hh ++;
}
// 返回队头元素
int front()
{
return q[hh];
}
// 返回队尾元素
int back()
{
return q[tt];
}
// 返回队列的大小
int size()
{
return tt - hh + 1;
}
// 队列是否为空
bool empty()
{
if (hh <= tt) return false;
return true;
}
单调栈
使用场景: 找出每个数左边离他最近的比它大(小)的数。
模板题:
输出每个数左边第一个比他小的数。
// 基本元素
int stk[N], tt = 0;
for (int i = 1; i <= n; i ++)
{
// 在栈非空的情况下,弹出比我大(小)的元素的索引
while(tt && check(stk[tt], i)) tt --;
printf("%d", stk[])
// 将自己的索引加入到栈中
stk[++ tt] = i;
}
单调队列
应用场景: 找出晃动窗口中的最大值和最小值
模板题:
找出滑动窗口位于每个位置时,窗口中的最大值和最小值。
// 基本元素
int q[N];
int hh = 0, tt = -1;
// 找最小值
for (int i = 0; i < n; i ++)
{
// 队头元素是否还在滑动窗口中,若不在,出队。
if (hh <= tt && is_out(q[hh])) hh ++;
// 将比我大的元素出队
while(hh <= tt && a[i] >= a[q[t]]) tt--;
// 将我自己加入到队列中
q[++ tt] = i;
// 输出窗口中的最小值
if (i >= k - 1) printf("%d ", a[q[hh]]);
}
// 清空队列
hh = 0, tt = -1;
// 找最大值
for (int i = 0; i < n; i ++)
{
// 队头元素是否还在滑动窗口中,若不在,出队。
if (hh <= tt && is_out(q[hh])) hh ++;
// 将比我大的元素出队
while(hh <= tt && a[i] >= a[q[tt]]) tt --;
// 将我自己加入到队列中
q[++ tt] = i;
// 输出窗口中的最大值
if (i >= k - 1) printf("%d", a[q[hh]]);
}
字符串匹配
暴力字符串匹配
for (int i = 1; i <= n; i ++)
{
bool flag = true;
for (int j = 1; j <= m; j ++)
if (s[i + j] != p[j]) {flag = false; break;}
}
Ps: 暴力做法时间复杂度O(n*m),太耗时。
KMP
next[i] = j
表示:p[1 ~ j] = p[i-j+1 ~ i]
主要用处: 主要用于字符串匹配,同时也可以优化某些问题。
// s[] 长文本, p[]为模式串, n为s的长度, m为p的长度
// ne[] 为p的next数组
// 计算模式串p的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;
}
// Match
for (int i = 1, j = 0; i <= n; i ++)
{
while(j && s[i] != p[j + 1)) j = ne[j];
if (s[i] == p[j + 1]) j ++;
if (j == m) do something //匹配成功
}
Trie
高效的存储和查找字符串。
基本模板
int son[N][26], cnt[N], idx;
// 插入该字符串
void insert(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++)
{
int o = str[i] - 'a';
if (!son[p][o]) son[p][o] = ++ idx;
p = son[p][o];
}
cnt[p] ++;
}
// 查询该字符串出现得次数
int query(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++)
{
int o = str[i] - 'a';
if (!son[p][o]) return 0;
p = son[p][o];
}
return cnt[p];
}
并查集
-
将两个集合合并。
-
询问两个元素是否属于同一个集合。
基本原理: 每个集合用一颗树来表示。树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点。
朴素并查集
// 基本元素
// 存储每个节点的祖宗节点
int q[N];
// 查找x的父节点
int find(int x)
{
if (q[x] != x) q[x] = find(q[x]);// 路径压缩
return q[x];
}
// 合并a和b所在的集合
void union(int a, int b)
{
int x = find(a), y = find(b);
q[x] = b;
}
// a、b是否在同一个集合
return find(a) == find(b);
// 并查集初始化
// 每个节点的祖宗节点都是自己,我命由我不由天
for (int i = 1; i <= n; i ++) q[i] = i;
维护size的并查集
// q存储每个节点的祖宗节点,size存储祖宗节点领导的集合中点的个数
int q[N], size[N];
int find(int x)
{
if (q[x] != x) q[x] = find(q[x]);
return q[x];
}
void union(int a, int b)
{
int x = find(a), y = find(b);
if (x == y) return ;
p[x] = y;
// 注意 集合合并,老大二合一
size[x] += size[y];
}
// a、b是否在同一个集合
return find(a) == find(b);
// 并查集初始化
// 每个节点的祖宗节点都是自己,我命由我不由天,都是为1的小团体
for (int i = 1; i <= n; i ++) q[i] = i, size[i] = 1;
维护到祖宗节点距离的并查集
// q存储祖宗节点,dis存储每个节点x到祖宗节点p[x]的距离
int q[N], d[N];
int find(int x)
{
if (q[x] != x)
{
int u = find(q[x]); // 找到祖宗节点
d[x] += d[p[x]]; // 将自己到祖宗节点的距离加上p[x]到祖宗节点的距离
q[x] = u; //路径压缩
}
return q[x];
}
// 初始化
for (int i = 1; i <= n; i ++)
{
q[i] = i;
d[i] = 0;
}
// 合并a和b所在的两个集合:
q[find(a)] = find(b);
d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量
堆
- 插入一个数
- 求集合中的一个最小值
- 删除最小值
- 删除任意一个元素
- 修改任意一个元素
Ps: 堆是完全二叉树。
分类:
- 小根堆:每一个节点都小于等于左右儿子 。
- 大根堆:每一个节点都大于等于左右儿子。
存储: 一维数组
- x的左儿子:
2x
- x的右儿子:
2x + 1
PS: 上边是下标从1开始,若从0开始,则左儿子2x+1
、右儿子2x+2
。
普通堆
int p[N], ss;
void down(int i)
{
int t = i;
if (2 *i <= ss && p[t] > p[2 * i]) t = 2 * i;
if (2 *i + 1 <= ss && p[t] > p[2 * i + 1]) t = 2 * i + 1;
if (t != i)
{
swap(h[t], h[i]);
down(t);
}
}
void up(int i)
{
while(i / 2 && p[i / 2] > p[i])
{
swap(h[i], h[i / 2]);
i = i >> 1;
}
}
维护插入次序的堆
//基本元素
int p[N], ph[N], hp[N], ss = 0;
// p存储堆中的值
// ph存储第k个插入的值
// hp存储堆中下标为k的点是第几个插入的
// swap三对值
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 i)
{
int t = i;
if (2 * i <= ss && p[2 * i] < p[i]) t = 2 * i;
if (2 * i + 1 <= ss && p[2 * i + 1] < p[t]) t = 2 * i + 1;
if (t != i)
{
heap_swap(i, t);
down(t);
}
}
// 向上调整
void up(int i)
{
while(i / 2 && p[i] < p[i / 2])
{
heap_swap(i, i / 2);
i = i >> 1;
}
}
// O(n)建堆
for (int i = n / 2; i; i --)
down(i);
哈希表
模拟哈希表
按存储结构后划分:
- 开放寻址法
- 一维数组的长度最好开到题目数据范围的2~3倍
- 因为坑位是数据范围的2~3倍,所以一定有空位置,则
find()
函数一定能够保证停止。
- 拉链法
**Ps:**模最好取成质数。
拉链法
// 基本元素
int h[N], ne[N], e[N], idx;
const int N = 100003;// 一般取大于maxn的最小质数
// h表示hash的每个坑位,h[i]指向该坑位下链表的指针。
// ne[i]表示链表中第i个元素指向的下一个元素
void insert(int x)
{
int k = (x % N + N) % N; // 避免x小于0导致的数组越界问题的出现
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; i = ne[i])
if (x == e[i]) return true;
return false;
}
开放寻址法
const int N = 300007, null = 0x3f3f3f3f; // N一般取大于3 * maxN的最小质数,null表示这个位置没有用过
int h[N];
// 如果x在哈希表中,返回x的下标
// 否则,返回x应该插入的位置。
int find(int x)
{
int k = (x % N + N) % N;
while(h[k] != null && h[k] != x)
{
k ++;
if (k == N) k = 0;
}
return k;
}
字符串前缀哈希法
题外话:除了求循环节之外,其余场合该算法都是KMP的劲敌。
该方法是将字符串映射为一个P进制的数字。
Hash方法:
H a s h ( S ) = ( S 1 × P n − 1 + S 2 × P n − 2 + . . . + S n − 1 × P 1 + S n × P 0 ) m o d Q Hash(S) = ( S_1 × P^{n-1} + S_2 × P^{n - 2} + ... + S_{n-1} × P^1 + S_{n} × P^0) mod Q Hash(S)=(S1×Pn−1+S2×Pn−2+...+Sn−1×P1+Sn×P0)modQ
Ps:
-
S
i
S_i
Si 代表字符串中每个字符的
ascii码
- 经验:当P取
131
或13331
,Q取2^64
,99%是不会出现冲突的。
则比较不同区间的子串是否相同就转化为对应的哈希值是否相同:
-
求一个字符串的哈希值就相当于求前缀和
- 前缀和公式: h [ i + 1 ] = h [ i ] × P + s [ i ] , i ∈ [ 0 , n − 1 ] h[i+1]=h[i]×P+s[i], i∈[0,n−1] h[i+1]=h[i]×P+s[i],i∈[0,n−1]
-
求一个字符串的子串哈希值就相当于求区间和和。
- 区间和公式: h [ l , r ] = h [ r ] − h [ l − 1 ] × P r − l + 1 h[l,r]=h[r]−h[l−1]×P^{r−l+1} h[l,r]=h[r]−h[l−1]×Pr−l+1
借用大佬的理解:
区间和公式的理解: ABCDE 与 ABC 的前三个字符值是一样,只差两位,
乘上 p 2 p^2 p2 把 ABC 变为 ABC00,再用 ABCDE - ABC00 得到 DE 的哈希值。
题目:字符串哈希
Q取2^64
时,在C++中可以用unsigned long long
类型的数组,这样如果hash之后的结果超过了2^64
,那这个整数就溢出了,相当于取模2^64
。
ull
范围:[0,2^64-1],ll
范围:[-2^63, 2^63-1]
#include<cstdio>
using namespace std;
typedef unsigned long long ULL;
const int N = 100003, P = 131;
int h[N], p[N];
char s[N];
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
scanf("%s", s + 1);
p[0] = 1;
for (int i = 1; i <= n; i ++)
{
h[i] = h[i - 1] * P + s[i];
p[i] = p[i - 1] * P;
}
while(m --)
{
int l1, r1, l2, r2;
scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
printf("%s\n", get(l1, r1) == get(l2, r2)? "Yes" : "No");
}
return 0;
}