C++数据结构~学习记录~Code注释丰富!【AcWing】

写在前面(持续更新中,会补充很多细节的尤其是一些总结&&知识点干货,还有一些dalao的图解/ 小方法,可以先收藏 等后续看完整滴!)

题源:AcWing算法基础课,本文谨以记录学习过程 + 复盘,如有写的模糊 or 错误的地方敬请谅解 and 在评论区指正,谢谢~

      博主大一(备战明年蓝桥杯ing),周末&&寒假 有兴趣互相打卡学习进度的同学可以私信我!互相监督进步hhhh(也不一定必须备战蓝桥杯嗷,其他相关学习/运动都可以一起打卡!!


目录

写在前面(持续更新中,会补充很多细节的尤其是一些总结&&知识点干货,还有一些dalao的图解/ 小方法,可以先收藏 等后续看完整滴!)

单链表

AcWing 826.单链表

双链表

AcWing 827.双链表

队列

AcWing 829.模拟队列

单调栈

AcWing 830.单调栈

单调队列

AcWing 154.滑动窗口

KMP

AcWing 831.KMP字符串

Trie

并查集

AcWing 836.合并集合

AcWing 837. 连通块中点的数量

AcWing 838.堆排序

AcWing 839.模拟堆

哈希表

未完待续…


单链表

AcWing 826.单链表

#include <iostream>
#include <algorithm>

using namespace std;

const int N=100010;

int head,idx,e[N],ne[N];

void init()//初始化
{
    head=-1;//-1为空.最后的空节点不会消失,在第一次头插后ne[idx]=-1即指向它
    idx=0;
}
//头插
void add_to_head(int x)
{
    e[idx]=x;
    ne[idx]=head;
    head=idx++;
}
//插入下标为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, k, x;
    char op;
    init();
    cin>>m;
    while(m--)
    {
        cin>>op;
        if(op=='H')
        {
            cin>>x;
            add_to_head(x);
        }
        else if(op=='D')
        {
            cin>>k;
            if(k==0) head=ne[head];//head指向头节点的下标
            else remove(k-1);//第一个插入的数idx=0,所以第k个插入的数下标为k-1
        }
        else
        {
            cin>>k>>x;
            add(k-1,x);
        }
    }
    //遍历list
    for(int i=head;i!=-1;i=ne[i]) cout<<e[i]<<" ";
    cout<<endl;
    return 0;
}

头指针head初始化为-1表示链表为空,idx表示当前节点的下标(参考了数组下标从0开始),无论是头插还是一般的插入,都是  1.存储数据 2.新节点的指针指向右侧 3.左侧节点(或头指针)指向新节点 三个步骤。删除某节点后面的节点,那就直接让这个节点的指针指向下一个的下一个(至于被删的节点就孤零零留那里咯·)

这题要注意操作的是“第k个插入的数”,第一个插入的idx为0,所以注意写码是k-1

双链表

AcWing 827.双链表

#include <iostream>
#include <algorithm>

using namespace std;

const int N=100010;

int m;
int e[N], l[N], r[N], idx;

//在节点k的右边插入一个点x
//写code之前可以画个图!
void insert(int k,int x)
{
    e[idx]=x;
    r[idx]=r[k];
    l[idx]=k;
    l[r[k]]=idx;//下面这两行不能改变顺序,因为要在修改r[k]之前完成这一步
    r[k]=idx++;
}
//删除下标为k的节点
void remove(int k)
{
    l[r[k]]=l[k];
    r[l[k]]=r[k];
}
int main()
{
    r[0]=1, l[1]=0;//0是左端点,1是右端点
    idx=2;
    cin>>m;
    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);//第一个插入的数下标为2,第k个插入的数下标为k+1
        }
        else if(op=="IL")
        {
            cin>>k>>x;
            insert(l[k+1],x);//在节点左边插入相当于在l[k]的右边插
        }
        else if(op=="IR")
        {
            cin>>k>>x;
            insert(k+1,x);
        }
    }
    //遍历时初始化为r[0],避免多打印了无效的0
    for(int i=r[0];i!=1;i=r[i]) cout<<e[i]<<" ";
    cout<<endl;
    return 0;
}

左端点是0,右端点是1。实现了只写在某节点右边插入新节点的函数insert,却可以实现头插/尾插/一般情况插入。插入时要记住先把右侧节点的 l [ r[k] ] = idx,再r[k]=idx++(后修改r[k])。遍历时 i 初始化为r[0](最左端节点),直到 i==1停止(最右端的指针),每次i=r[i]即可实现从左到右遍历

队列

AcWing 829.模拟队列

#include <iostream>
#include <algorithm>

using namespace std;

const int N=100010;

int q[N];
int hh=0,tt=-1;

void push(int x)
{
    q[++tt]=x;
}
void pop()
{
    hh++;
}
void empty()
{
    if(tt>=hh) printf("NO\n");//队尾大于等于队头时队列不为空
    else printf("YES\n");
}
void query()
{
    printf("%d\n",q[hh]);
}
int main()
{
    int m, x;
    cin>>m;
    string s;
    while(m--)
    {
        cin>>s;
        if(s=="push")
        {
            cin>>x;
            push(x);
        }
        else if(s=="empty") empty();
        else if(s=="pop") pop();
        else query();
    }
    return 0;
}

队列是一种先进先出的数据结构,用hh和tt分别代表队头和队尾。出队只需++hh,入队只需q[++tt],纯 模板题 啦

单调栈

AcWing 830.单调栈

//性质:一旦右边的数比左边的数小,左边的数将不再有用,出栈!

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N=100010;

int tt, stk[N];

int main()
{
    int n, x;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        scanf("%d",&x);
        while(tt && stk[tt]>=x) tt--;
        if(!tt) printf("-1 ");//栈为空,tt==0
        else printf("%d ",stk[tt]);//栈顶即为左边第一个满足性质的数
        stk[++tt]=x;//新元素入栈
    }
    return 0;
}

栈是一种先进后出的数据结构。(相当于一个垃圾桶只能放进去 or 拿出最上面的东西)数组模拟栈就肥肠煎蛋,tt表示栈中元素个数

单调队列

AcWing 154.滑动窗口

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N=1000010;//看清数据范围!!1e6

int n,k;
int a[N],q[N];//q[]存放的是下标

int main()
{
    int hh=0,tt=-1;//队头在左,队尾在右,窗口向右滑
    cin>>n>>k;
    for(int i=0;i<n;i++) scanf("%d",&a[i]);
    //处理最小值
    for(int i=0;i<n;i++)
    {
        //判断队头是否出队
        if(hh<=tt && q[hh]<i-k+1 ) ++hh;
        while(hh<=tt && a[q[tt]]>= a[i]) --tt;//若队尾元素大于新元素,则这个元素后面将一点用都没有,出队
        q[++tt]=i;//把当前元素入队
        if(i+1>=k) printf("%d ",a[q[hh]]);//在形成窗口后,队头一直都是当下的最小值
    }
    puts("");
    hh=0,tt=-1;//不要忘记重置队列
    
    for(int i=0;i<n;i++)
    {
        if(hh<=tt && q[hh]<i-k+1 ) ++hh;
        while(hh<=tt && a[q[tt]]<= a[i]) --tt;//完全对称的,只需改动队尾元素和新元素的判断
        q[++tt]=i;
        if(i+1>=k) printf("%d ",a[q[hh]]);
    }
    return 0;
}

KMP

AcWing 831.KMP字符串

/*KMP的思考方式和单调队列、双指针算法是类似的,先想清楚朴素算法怎么做,然后如何去优化
求next数组时,我们关心对于每个不同的下标i,j能走多远;匹配时,我们只关心j是否走到末尾

ne[1]=0,因为第一个元素如果匹配失败,那只能从零开始
所以求ne[i]时i从2开始即可*/

//对模板串的每一个点都预处理出来:后缀和前缀相等的最大长度,即ne[i]

#include <iostream>
#include <algorithm>

using namespace std;

const int N=100010,M=1000010;

int n,m;
int ne[N];
char p[N],s[M];

int main()
{
    cin >> n >> p+1 >> m >> s+1;
    //处理ne[],j表示已经成功匹配的个数
    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;//i是不会倒退的,所以每次最后更新出来的j一定是最优的
    }

    //匹配字符串
    for(int i=1,j=0;i<=m;i++)
    {
        while(j && s[i]!=p[j+1]) j=ne[j];//注意是一旦匹配不成功就要一直倒退(while)
        if(s[i]==p[j+1]) j++;
        if(j==n)
        {
            printf("%d ",i-n);//不妨考虑直接匹配成功,此时i=n,输出0
            j=ne[j];//继续尝试下一次匹配
        }
    }

    return 0;
}

Trie

并查集

AcWing 836.合并集合

/* 并查集
1.将两个集合合并
2.询问两个元素是否在集合中

基本原理:每个集合都用一棵树表示,树根的编号就是整棵树的编号
每个节点存储它的父节点
Q1:如何判断树根:if(p[x]==x)
Q2:如何求x的集合编号:while(p[x]!=x) x=p[x];
Q3:如何合并两个集合(编号px、py) :p[x]=y */
//优化:路径压缩

#include <iostream>

using namespace std;

const int N=100010;

int n, m;
int p[N];

int find(int x)//返回祖宗节点+路径压缩
{
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}

int main()
{
    char op[2];
    int a, b;
    cin>>n>>m;
    for(int i=1;i<=n;i++) p[i]=i;
    while(m--)
    {
        scanf("%s%d%d",op, &a, &b);
        if(*op=='M') p[find(a)]=find(b);//如果a、b已经在一个集合内,相当于将祖宗节点自身赋给自己
        else 
        {
            if(find(a)==find(b)) printf("Yes\n");
            else printf("No\n");
        }
    }
    return 0;
}

AcWing 837. 连通块中点的数量

//询问连通块中点的数量,只需让根节点的size有意义即可
//合并后只需size[b]+=size[a];

#include <iostream>

using namespace std;

const int N=100010;

int n, m;
int p[N], cnt[N];

int find(int x)//返回祖宗节点+路径压缩
{
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}

int main()
{
    char op[3];
    int a, b;
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        p[i]=i;
        cnt[i]=1;
    }
    while(m--)
    {
        scanf("%s",op);
        if(*op=='C')
        {
            cin>>a>>b;
            if(find(a)==find(b)) continue;//如果在同一集合中就不要再合并了,不然下一行cnt又要多加
            cnt[find(b)]+=cnt[find(a)];//把一棵树插过去,只改变根节点的size即可(一定是根节点!)
            p[find(a)]=find(b);
        }
        else if(op[1]=='1')//Q1和Q2的区别只在于op[1]
        {
            cin>>a>>b;
            if(find(a)==find(b)) printf("Yes\n");
            else printf("No\n");
        }
        else
        {
            cin>>a;
            printf("%d\n",cnt[find(a)]);
        }
    }
    return 0;
}

AcWing 838.堆排序

/* 手写一个堆

1.插入一个数 heap[++size]=x,up(size)
2.求集合中的最小值 heap[1]
3.删除最小值 heap[1]=heap[size], size--,down(1)
4.删除任意一个元素 heap[k]=heap[size],size--; down(k);up(k);
5.修改任意一个元素 heap[k]=x; down(k);up(k);

用一维数组存下一个堆,根节点是1而不是0是因为这样比较方便,也避免了0*2=0
x的左儿子是2x,右儿子是2x+1
*/

#include <iostream>
#include <algorithm>
#include <cstdio>

using namespace std;

const int N=100010;

int n, m;
int h[N], cnt;

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)
    {
        swap(h[u], h[t]);
        down(t);//递归
    }
}
void up(int u)
{
    while(u/2 && h[u/2]<h[u])
    {
        swap(h[u/2], h[u]);
        u/=2;
    }
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) scanf("%d",&h[i]);
    cnt=n;

    for(int i=n/2; i; i--) down(i);

    while(m--)
    {
        printf("%d ",h[1]);
        h[1]=h[cnt],cnt--;
        down(1);
    }
    return 0;
}

AcWing 839.模拟堆

/*
这题要额外开两个数组 ph[k]存第k个数的下标, hp[k]存堆里下标是k的点在ph[]中下标
能通过hp[a]反找ph[]的下标,它们互为反函数
更新的时候记得两个数组也都要改
*/

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>

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/2]>h[u])
    {
        heap_swap(u/2, u);
        u/=2;
    }
}
int main()
{
    int n, m=0;
    cin>>n;
    while(n--)
    {
        char op[10];
        int k, x;

        scanf("%s",op);
        if(!strcmp(op,"I"))
        {
            cin>>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"))//删除最小值把最后一个元素交换后删除堆中最后一个元素,down(1)
        {
            heap_swap(1,cnt);
            cnt--;
            down(1);
        }
        else if(!strcmp(op,"D"))
        {
            cin>>k;
            k=ph[k];
            heap_swap(k,cnt);
            cnt--;
            down(k), up(k);
        }
        else
        {
            cin>>k>>x;
            k=ph[k];
            h[k]=x;
            down(k), up(k);
        }
    }

    return 0;
}

哈希表

未完待续…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值