【算法基础课】第二章《数据结构》模板总结

链表

单链表

静态链表

用数组模拟静态链表不容易超时,动态链表中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;
}

单调栈

在这里插入图片描述

图片来源:https://www.acwing.com/user/myspace/index/55289/

使用场景: 找出每个数左边离他最近的比它大(小)的数。

模板题:

输出每个数左边第一个比他小的数。

// 基本元素
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];
}

并查集

  1. 将两个集合合并。

  2. 询问两个元素是否属于同一个集合。

基本原理: 每个集合用一颗树来表示。树根的编号就是整个集合的编号。每个节点存储它的父节点,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)的偏移量

  1. 插入一个数
  2. 求集合中的一个最小值
  3. 删除最小值
  4. 删除任意一个元素
  5. 修改任意一个元素

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×Pn1+S2×Pn2+...+Sn1×P1+Sn×P0)modQ

Ps:

  • S i S_i Si 代表字符串中每个字符的ascii码
  • 经验:当P取13113331,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,n1]
  • 求一个字符串的子串哈希值就相当于求区间和和。

    • 区间和公式: 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[l1]×Prl+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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Honyelchak

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值