Acwing基础-数据结构

数据结构

单链表

#include<bits/stdc++.h>

using namespace std;

const int N = 100010;
int e[N], ne[N], idx = 1;
//e[]为链表的元素
//ne[]为链表的序号(第几次插入的)
//idx为操作次数,(第一次默认为头结点为0了)

void insert(int k,int x)
{
    e[idx] = x, ne[idx] = ne[k],ne[k] = idx ++;
}

void remove(int k)
{
    ne[k] = ne[ne[k]];
}

int main()
{
    int n;
    cin >> n;

    int k, x;
    char op;
    while(n --)
    {
        cin >> op;

        //在表头插入
        if(op == 'H'){
            cin >> x;
            insert(0,x);
        }else if(op == 'I'){//在第k个插入的数后面插入一个数
            cin >> k >> x;
            insert(k,x);//第k个,就是k-1
        }else if(op == 'D'){//删除第k个插入的数后面的数 (k=0时删除头结点)
            cin >> k;
            //if(!k) head = ne[head];//删除头结点
            remove(k);//第k个,就是k-1
        }
    }

    for(int i = ne[0]; i; i = ne[i]){
        cout << e[i] << ' ';
    }
    cout << endl;

    return 0;
}

双链表

#include<bits/stdc++.h>

using namespace std;

const int N = 100010;
int e[N],l[N],r[N],idx;
//e[]为链表的节点
//l[i]为链表的左序下标数组  <---  i节点的左元素
//r[i]为链表的右序下标数组  --->  i节点的右元素
//idx为操作次数

void init()
{
    r[0] = 1, l[1] = 0;//第一个点的右边是 1   第二个点的左边是 0
    idx = 2;//idx 此时已经用掉两个点
    // 此时的链表: 0  1,
    //若干次操作后  0 .........  1
}

void insert(int k,int x)
{
    e[idx] = x;//创建新结点
    //把新元素伸向左右的创建好
    r[idx] = r[k];//idx的右元素是没插入时k的右元素(下标)
    l[idx] = k;//左元素的下标是k
    //把左右元素指向自己
    l[r[k]] = idx;//让右侧指向新元素
    r[k] = idx ++;//左侧指向新元素
}

//删除k节点
void remove(int k)
{
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);

    int n;
    cin >> n;

    init();

    while(n --)
    {
        string op;
        cin >> op;
        int k, x;
        if(op == "L"){//在链表的最左端插入x
            cin >> x;
            insert(0,x);
        }else if(op == "R"){//在链表的最右端插入x
            cin >> x;
            insert(l[1],x);
        }else if(op == "D"){//将第k个插入的数后面的数删除
            cin >> k;
            remove(k + 1);//初始值就是2了
        }else if(op == "IL"){//表示在第k个插入的数左侧插入一个数
            cin >> k >> x;
            insert(l[k + 1], x);
        }else if(op == "IR"){//表示在第k个插入的数右侧插入一个数
            cin >> k >> x;
            insert(k + 1, x);
        }
    }

    for(int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';


    return 0;
}

模拟栈

#include<bits/stdc++.h>

using namespace std;

const int N = 1e5 + 10;
int a[N];

int main()
{
    //ios::sync_with_stdio(0);	cin.tie(0);

    int n,idx = 0;
    cin >> n;

    while(n --)
    {
        string op;
        cin >> op;

        if(op == "push"){
            int x;
            cin >> x;
            a[++ idx] = x;//此处的++idx 比 idx ++ 好用
        }else if(op == "pop"){
            idx --;
        }else if(op == "empty"){
            if(!idx) cout << "YES" << endl;
            else cout << "NO" << endl;
        }else if(op == "query"){
            cout << a[idx] << endl;
        }
        //cout << "idx = " << idx << endl;
    }

    return 0;
}

模拟队列

#include<bits/stdc++.h>

using namespace std;

const int N = 100010;
int a[N];

int main()
{
    int i = 0, j = 0, n;
    cin >> n;
    string op;
    while(n --)
    {
        cin >> op;

        if(op == "push"){
            cin >> a[++ j] ;
        }else if(op == "pop"){
            ++ i;
        }else if(op == "empty"){
            if(i == j) cout << "YES" << endl;
            else cout << "NO" << endl;
        }else if(op == "query"){
            cout << a[i + 1] << endl;

        }
    }


    return 0;
}

单调栈

#include<bits/stdc++.h>

using namespace std;

const int N = 100010;
int a[N];

int main()
{
    //ios::sync_with_stdio(0);	cin.tie(0);

    int n;
    cin >> n;
    int i = 0;

    while(n --)
    {
        int x;
        cin >> x;
        while(x <= a[i] && i > 0) i --;
        if(!i) cout << "-1 ";
        else cout << a[i] << ' ';
        a[++ i] = x;
    }

    return 0;
}

滑动窗口

思路: 最大值和最小值分开做,两次代码几乎相同

  1. 解决队首已经出窗口的问题;

  2. 解决队尾与当前元素a[i]不满足单调性的问题;

  3. 将当前元素下标加入队尾;

  4. 如果满足条件则输出结果;

需要注意的细节:

  1. 上面四个步骤中一定要先3后4,因为有可能输出的正是新加入的那个元素;
  2. 队列中存的是原数组的下标,取值时要再套一层,a[q[]];
  3. 算最大值前注意将hh和tt重置;
  4. hh从0开始,数组下标也要从0开始。
#include<bits/stdc++.h>

using namespace std;

const int N = 1000010;
int a[N],q[N];//a是存储原数组,q存储窗口中的元素的 下标
int n, k;

int main()
{
    ios::sync_with_stdio(0);	cin.tie(0);
    cin >> n >> k;
    for(int i = 0; i < n; i ++) cin >> a[i];
    //输出窗口中的最小值,就要维持一个单调递增的队列
    int hh = 0, tt = -1;
    for(int i = 0; i < n; i++){
        if(i - k + 1 > q[hh] ) ++ hh;//若队首滑出窗口,hh++
        while(hh <= tt && a[i] <= a[q[tt]]) -- tt;//若队尾不单调,tt--
        q[++ tt] = i;//下标加到队尾
        if(i + 1 >= k) cout << a[q[hh]] << ' ';//满足窗口内有k个元素了,就该输出了
    }
    cout << endl;
    hh = 0, tt = -1;
    for(int i = 0; i < n; i ++){
        if(i - k + 1 > q[hh]) ++ hh;
        while(hh <= tt && a[i] >= a[q[tt]]) -- tt;
        q[++ tt] = i;
        if(i + 1 >= k) cout << a[q[hh]] << ' ';
    }

    return 0;
}

Trie字符串统计

Trie:高效的存储和查找字符串集合的数据结构

image-20230209202252416

编号为 u 的节点指向了编号为 v 的节点,边对应了字母 c ,反映到 son 数组中就是:

son[u][c] = v;

变量解释:

//cnt[]:以当前节点为结尾的字符串有多少个
//son[i][j] = a: 在i后面的字符是a
//idx:idx相当于一个分配器,如果需要加入新的结点就用++idx分配出一个下标
#include<iostream>

using namespace std;

const int N = 100010;
int son[N][26],cnt[N],idx;
string str;

void insert(string 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(string 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 --)
    {
        string op,str;
        cin >> op >> str;
        if(op == "I"){
            insert(str);
        }else if(op == "Q"){
            int a = query(str);
            cout << a << endl;
        }
    }
    
    return 0;
}

并查集 - 合并集合

并查集的用处:

  1. 合并集合
  2. 查询两个元素是否在同一集合当中

基本原理:每个集合用一棵树来表示。树的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点

操作:

  1. 判断树根 if(p[x] = x)
  2. 求x的集合编号 while(p[x] != x) x = p[x]
  3. 合并两个集合,这两将x的根节点嫁接到y的根节点, px为x的根节点, py为y的根节点,嫁接p[px] = py
#include<bits/stdc++.h>

using namespace std;

const int N = 100010;
int n, m;
int p[N];

int find(int k) //返回k的祖宗节点 + 路径压缩
{
    if(k != p[k]){  //如果这个元素不是祖宗节点
        p[k] = find(p[k]);
    }
    return p[k];
}


int main()
{
    scanf("%d%d", &n, &m);
    //从1开始!
    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);
        
        //合并
        if(op[0] == 'M')
        {
            p[find(a)] = find(b);
        }
        else if(op[0] == 'Q')
        {
            if(find(a) == find(b)) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

模拟堆

堆是一个完全二叉树,

小根堆:根节点是整个堆中的最小值; 大根堆:根节点是整个堆中的最大值

主要操作是简单的down和up,复杂点的是有修改操作的(涉及到第几次插入,需要单独开辟数组存储)

主要变量注释:

  • h[x] 表示树中位置 x 的元素
  • ph[k] = x 表示第 k 个插入的元素在树中存放的位置 x
  • 此时如果要交换 ph 中的两个元素需要知道树中位置 x 是第几个被插入的, 于是便引入了数组 hp
  • hp[x] = k 表示树中位置 x 存放的为第 k 个插入的元素
#include<bits/stdc++.h>

using namespace std;

const int N = 100010;
int h[N],ph[N],hp[N],idx;
//h:维护的堆
//ph: 从下标找到堆中元素
//hp: 从堆中的元素找到其下标 这俩主要是查找第几个插入的数
//idx是共多少节点


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 <= idx && h[u * 2] < h[t]) t = u * 2;
    if(u * 2 + 1 <= idx && 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 / 2] > h[u]){
        heap_swap(u / 2, u);
        u /= 2;
    }
}


int main()
{
    //ios::sync_with_stdio(0);	cin.tie(0);

    int n, m = 0;
    cin >> n;

    while(n --)
    {
        string op;
        int k, x;
        cin >> op;

        if(op == "I")
        {
            cin >> x;
            m ++;
            idx ++;
            ph[m] = idx, hp[idx] = m;
            h[idx] = x;
            down(idx), up(idx);
        }
        else if(op == "PM") cout << h[1] << endl;
        else if(op == "DM")
        {
            heap_swap(1,idx);
            idx --;
            down(1);
        }else if(op == "D")
        {
            cin >> k;
            k = ph[k];//从ph中找到第k次插入的数在堆中的下标
            heap_swap(k,idx);
            idx --;
            down(k), up(k);
        }else {
            cin >> k >> x;
            k = ph[k];//得到第k个数的位置
            h[k] = x;//修改在堆中的元素
            down(k), up(k);
        }
    }

    return 0;
}

模拟散列表

哈希方法: k = (x % N + N) % N,N为常数,先模再加是为了防止爆int

  • 开放寻址法:先找到一个坑,如果坑里没人就进这个坑;如果有人就继续向后找坑

    一个长度为题目要求2~3倍的一维数组,通过循环找到哈希值k的位置,(如果哈希值冲突的情况位置也会冲突,那么k继续向后找位置)

    while(e[k] != null && e[k] != x){ //不为空 && 不是k的位置存的不是x
            k ++;//继续向后找位置
            if(k == N) k = 0;//到末尾从头再来
        }
    
  • 拉链法:找到一个坑.不管有人没人都进去,有人就去坑里排队

    开辟一个一维数组,并且每个一维数组都开出两个数组(共同组成链表)

#include<bits/stdc++.h>

using namespace std;

const int N = 200003, null = 0x3f3f3f3f;
int h[N];

int find(int x)
{
    int k = (x % N + N) % N;
    while(h[k] != null && h[k] != x){
        if(k == N) k = 0;
        k ++;
    }
    return k;
}

int main()
{
    //ios::sync_with_stdio(0);	cin.tie(0);

    memset(h,0x3f,sizeof h);//初始化


    int n;
    cin >> n;

    while(n --)
    {
        char op;
        int x; 
        cin >> op >> x;
        
        if(op == 'I')
        {
            int t = find(x);
            h[t] = x;
        }else{
            if(h[find(x)] == x) cout << "Yes" << endl;
            else cout << "No" << endl;
        }
    }

    return 0;
}

字符串哈希

把所有的字符都变成p进制数字(哈希值),实现不同的字符串映射到不同的数字

前缀和公式: h[i] = h[i - 1] * P + s[i] h为前缀和数组,P为进制,s为字符串数组

区间和公式:h[l,r] = h[r] - h[l - 1] * P^(r - l + 1) 可以p数组存储P的几次方

注意:

  • p[0] = 1,不能为0,否则假设hash(A) = 0,则hash(AA)==0也是成立的,无法区分
  • P 一般为131 or 13331,经验值
#include<bits/stdc++.h>

using namespace std;

typedef unsigned long long ULL;

const int N = 100010, P = 131;// or p = 13331  经验值

int n,m;
char s[N];
ULL h[N],p[N];

ULL query(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

int main()
{
    cin >> n >> m;

    p[0] = 1;//一定要注意,hash值不能为0,否则A  AA  AAA 的hash值都是0
    h[0] = 0;
    for(int i = 1; i <= n; i ++) cin >> s[i];

    for(int i = 1; i <= n; i ++)
    {
        p[i] = p[i - 1] * P;//存储p的几次方,便于计算区间和
        h[i] = h[i - 1] * P + s[i];//计算前缀和

    }

    while(m --)
    {
        int l1, r1, l2, r2;
        cin >> l1 >> r1 >> l2 >> r2;

        if(query(l1,r1) == query(l2,r2)) cout << "Yes" << endl;
        else cout << "No" << endl;
    }
    

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值