数据结构(二)

数据结构二

trie树

类似于数据结构中的树,但不是二叉树,一个节点可以有多于两个的子节点
其完成功能主要是存储和查找,(可以通过维护特殊的变量解决特定的题目,以下例题中有讲)

  • 存储:
    从根节点开始idx==0;根节点不存储数据,利用二位数组son[p][u]存储数据
    p代表当前节点的深度(p从0开始),u代表当前的深度中叶子的个数(种类)
    最后在最后结尾字母处,应当使计数数组cnt[idx]++,即图中星标,代表当前节点中有字符串的结尾

  • 查找:
    从根结点处向下遍历,若没有找到p深度下的u那么返回
    找到则执行下一层,直到最后找到元素返回cnt[idx]

在这里插入图片描述

在这里插入图片描述

实例插入结构
先后插入abc,abcf,efg三个字符串

在这里插入图片描述

例题1:trie字符串统计

在这里插入图片描述

#include <iostream>
#include <string>

using namespace std;

const int N=1e5+10;

int  son[N][26];           //N记录的是树的深度,本题也就是单词的长度,26是每层最多出现字母的可能性,就是26个小写字母

int cnt[N];            // 以“abc”字符串为例,最后一个字符---'c'对应的idx作为cnt数组的下标。数组的值是该idx对应的个数,由于idx不重复,所以每个单词对应的下标都是唯一的

int idx;               //记录开辟空间的下标,计数从1开始

char str[N];

void insert(char str[]){
    
    int p=0;                                     //定义数的根节点是0
    
    for(int i=0;str[i];i++){
        
        int u=str[i]-'a';                        //把每个字母转换成0-25的数字
        
        if(!son[p][u])  son[p][u]=++idx;         //如果该单词不存在,给他开辟一块空间,让他加入树
        
        p=son[p][u];                            //把son[p][u]同时赋值给p ,son[p][u]的值idx可以指示下一层的深度在什么位置
        
        
    }
    
    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;
    
    cin>>n;
    
    char op;
    
    while(n--){
        
        cin>>op>>str;
        
        if(op=='I')insert(str);
        
        else  cout<<query(str)<<endl;
        
        
    }
    
    
    
}


例题2:最大异或对

在这里插入图片描述
思路:
利用trie树求解
首先每个十进制数都可以通过x>>i&1的方式转化为二进制数
首先明确一点在n个数中寻找两个异或值最大的数,两两比较只需要比较 C n ( 2 ) Cn(2) Cn(2)
n ∗ ( n − 1 ) / 2 n*(n-1)/2 n(n1)/2次 就不需要全部拿来比较
我们采用边向trie树中插入,一边查找的方式,每一个插入的数只需要同他前面的所有数比较即可
在查找时,尽量向着不同于当前数的位置前进
例如如果想找到5(101)异或最大的值,我们期望能够去找到2(010)
如果当前位没有,只能被迫前进
最后返回最大数计数res
在这里插入图片描述

#include <iostream>
#include<algorithm>

using namespace std;

const int N=1e5+10,M=31*N;

int n,idx;

int son[M][2];    //记录trie树,总计最多有N个数输入,每个数最多31位,所以树的深度最多是31*M

int  q[N];      //存放整数

//插入操作
void  insert(int x){
    
    int p=0;
  
    for(int i=30;i>=0;i--){           //注意这里即使最高位没有数,插入进树中,对最后结果也咩有影响
        
        int u=x>>i&1;
        
        if(!son[p][u])son[p][u]=++idx;
        
        p=son[p][u];
        
    }
    
    
     
    
}


//查找与x异或最大的数操作
int query(int x){
    
    int  p=0,res=0;
    
    for(int i=30;i>=0;i--){
        
        int u=x>>i&1;
        
        if(son[p][!u]){
            
            p=son[p][!u];
            
            res=res*2+!u;
            
        }
        
        else  {
            
            p=son[p][u];
            
            res=res*2+u;
            
        }
        
        
        
    }
    
    return res;
        
}

int main(){
    
    cin>>n;
    
    int res=0;
    
    for(int i=0;i<n;i++)scanf("%d",&q[i]);
        
    for(int i=0;i<n;i++){                  //该步骤是边插入边查询,因为共有n个数,两两比较共需比较Cn(2)次,即n(n-1)/2次
        
        insert(q[i]);
        
        int t=query(q[i]);
        
        res=max(res,t^q[i]);            //更新答案的最大值
    }
    
    
    
    cout<<res;
    
    
    return 0;
    
}

并查集

在这里插入图片描述

在这里插入图片描述

例题1:合并集合

在这里插入图片描述

  • 注意只有根节点的p[x]==x,所以路径压缩这一步if(p[x]!=x)p[x]=find(p[x])翻译成中文就是如果当前节点的父节点不是等于他本身(即他不是根节点),那么就递归的找寻他的根节点,并将所有点都指向了根节点,最后返回的p[x]代表的是该集合的编号,而不是根节点的父节点
#include <iostream>

using  namespace std;

const int N=1e5+10;

int n,m;

int p[N];


//查询祖宗根节点
int  find(int x){
    
    if(p[x]!=x)p[x]=find(p[x]);     //把祖宗节点赋值给当前节点的父亲节点,即让该节点直接指向祖宗节点,,该步结束之后,该路径上的每个节点都会指向祖宗节点(即路径压缩),,下一次再有该路径上的点寻找祖宗节点时,就会直接返回祖宗节点,节约了搜索的时间
    
    return  p[x];
}

int main(){
    
    scanf("%d%d",&n,&m);
    
    for(int i=1;i<=n;i++)p[i]=i;            //初始状态每个节点都作为根节点
    
    while(m--){
        
        char op[2];
        
        int i,j;
        
        scanf("%s%d%d",op,&i,&j);   //记得用%s来读取字符串,如果用%c读取单独的字符,可能会读取末尾的空格或者回车
       
        if(op[0]=='M')p[find(i)]=find(j);  //让j的祖宗节点成为i节点祖宗节点的父亲节点
        
        else
        { 
            
            if(find(i)==find(j))puts("Yes");
            
            else  puts("No");
            
            
        }
            
    }
    
    return 0;
}

例题2:连通块中点的数量

在这里插入图片描述
合并集合的变形:前两个操作和插入与查询操作一致
只需要在根节点加入一个cnt[N]去维护树中元素的数量即可

#include <iostream>

using namespace std;

const int N =1e5+10;

int p[N],cnt[N];

int n,m;

int find(int x){
    
    if(p[x]!=x)p[x]=find(p[x]);
    
    return p[x];
    
}


int main(){
    
    scanf("%d%d",&n,&m);
    
    for(int i=0;i<n;i++){
        p[i]=i;
        
        cnt[i]=1;                                //只有根节点数据有效,记录当前树共有多少元素
        
    }
    
    char op[5];
    
    int a,b;
    
    while(m--){
        
        scanf("%s",op);
        
        if(op[0]=='C'){
            
            scanf("%d%d",&a,&b);
            
            if(find(a)==find(b))continue;          //防止a,b是同一个节点时,树的元素翻倍
            
            cnt[find(b)]+=cnt[find(a)];
            
            p[find(a)]=find(b);
            
          
        }
        
        else  if(op[1]=='1'){
            
            scanf("%d%d",&a,&b);
            
            if(find(a)==find(b))cout<<"Yes"<<endl;
            
            else cout<<"No"<<endl;
            
        }
        
        else{
            
            scanf("%d",&a);
            
            int res =cnt[find(a)];                //找到该节点的根节点,并返回根节点的计数cnt
            
            cout<<res<<endl;
            
            
        }
        
    }
    
    return 0;
}

例题3. 食物链**

在这里插入图片描述

  • 明确:前面说的话一定是真话,一旦后面的话与前面的话发生冲突,那么他就是假话
  • 这道题的关键在于利用trie树解题的同时,维护一个怎么样的变量,可以去表达A,B,C
    之间的食物链关系
  • 本题通过利用每个子节点与根节点的距离d[N]表达他们之间的关系,无论如何都将所有点插入到一个树内,比较他们与根节点的关系,也可以得到各点之间的关系
  • 假定根节点是0号生物距离他“1”的生物吃他,距离他“2”的生物吃1,距离他“3”的生物去吃2,不难推断“3”和根节点0是同一种生物,如何表示他们是同一种生物?
    只需要使其距离%3即可…也就是说距离为3,6,9,12...的生物均和0同类,,距离为1,4,7,11…的生物均吃0,2,5,8的生物均吃1以此类推
    理清这个思路本题得解

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#include<iostream>

using namespace std;

const int N=50010;

int p[N],d[N];     //d[N]维护的是该节点到祖宗节点之间的距离
    
int n,k;

int find(int x){
    
    if(p[x]!=x){
        
        int  t=find(p[x]);//先将找到的祖宗节点的坐标保存到t,如果直接写p[x]=find(p[x]),d[x]+=d[p[x]]此时p[x]是根节点那么d[p[x]]等于0,就导致这步没有意义
        
        d[x]+=d[p[x]];  //这一步不仅将当前节点到父亲的距离更新成到根节点的距离,路径上所有的点都更新成了到根节点的距离,,,,**注意这一步千万不能写在int t=find(p[x])的前面否则在递归的时候,就会把每个节点的d[x]更新成到他父亲节点前一个节点的距离,而不是到根节点的距离**    find递归操作的本质不仅是返回祖宗节点,同样把每个节点的指向祖宗节点       
        
        p[x]=t;
        
    }
    
    return p[x];
    
}


int main(){
    
    int op,res=0;
    
    cin>>n>>k;
    
    for(int i=0;i<=n;i++)p[i]=i;
 
    while(k--){
      int x,y;
      
      cin>>op>>x>>y;
      
      if(x>n||y>n)res++;
      
      else{
          
          int px=find(x),py=find(y);
          
          if(op==1){
              
              if(px==py&&(d[x]-d[y])%3)res++;  //px==py表示之前说过有关x,y关系的话(即x,y已经在关系树内),判断他们是否为同一物种,注意不能写成d[x]%3!=d[y]%3(d[x]和d[y]都有可能是负数)
                                                //如果这样写,应保证其均为正数,应写成(d[x]%3+3)%3!=(d[y]%3+3)%3
              
              else if(px!=py){                  
                  
                  p[px]=py;
                  
                  d[px]=d[y]-d[x];             //如果x和y不在同一树内(即之前没有说过关于x和y关系的话),将他们插入到同一树内,,因为d[px]的值默认为0,所以d[px]的值由人为规定,见图三,,因为(d[x]+d[px]-d[y])%3==0所以 d[px]=d[y]-d[x]
                  
              }
             
            }
            
          else{
              
              if(px==py&&(d[x]-d[y]-1)%3)res++;  //此处同理不能写成(d[x]-d[y])%3!=1应写成((d[x]+d[y])%3+3)%3!=1
              
              else if(px!=py){
                  
                  p[px]=py;
                  
                  d[px]=d[y]+1-d[x];  //同理(d[x]+d[px]-d[y]-1)%3==0
                  
                }
              
              }
          
          }
      
     
    }
    
    cout<<res;
      
    
    return 0;
}

底层结构:完全二叉树
在这里插入图片描述

例题1:堆排序

在这里插入图片描述

在这里插入图片描述

  • downup时间复杂度均为log(2)n
  • 堆的性质:小根堆(根节点永远是当前根的最小值)
    并且每一个节点的值都要小于其子节点的值
  • 根节点坐标应当从1开始,如果从0开始左右儿子都是0,不便于计算下标

在这里插入图片描述

小根堆的down操作
在这里插入图片描述

在这里插入图片描述

  • 每一层想下执行down操作时 ,时间复杂度是当前节点所在层数(log2n)级别,如果倒数第二层的节点down一层,时间复杂度是1(最多执行一次swap操作),倒数第三层的节点向下down两层,时间复杂度是2(最多执行两次down操作)以此类推,最后计算出整个堆的时间复杂度应当是O(n)

在这里插入图片描述

以上图为例
初始化堆操作:使其从n/2元素位置即倒数第二层向下执行down操作 倒数第二层有n/4个元素向下down1层
时间复杂度为O(n)

#include <iostream>
#include <algorithm>

using namespace std;

const int N=1e5+10;

int h[N],size;  //用一维数组h[n]实现堆,size为数组当前大小

int n,m;

void  down(int u){
    
    int t=u;
    
    if(2*u<=size&&h[2*u]<=h[t])t=2*u;
    
    if(2*u+1<=size&&h[2*u+1]<=h[t]) t=2*u+1;   //两次if找到三个数中的最小值,并保存节点下标
    
    if(t!=u){
        
        swap(h[u],h[t]);
        
        down(t);
        
    }
    
}


int main(){
    
    cin>>n>>m;
    
    for(int i=1;i<=n;i++)scanf("%d",&h[i]);
    
    size=n;
    
    for(int i=n/2;i>=1;i--)down(i);          //初始化堆使其从n/2层逐个向下沉
    
    for(int i=1;i<=m;i++){                   //输出根节点,用最后的数覆盖根节点,然后down操作重新排序
        
        cout<<h[1]<<" ";
        
        h[1]=h[size];
        
        size--;
        
        down(1);
        
    }
    
    return 0;
    
}

例题2 模拟堆

包含上题所写的堆的五个基本操作
本题难点在于要动态的删除和修改第k个插入的数
所以我们新开两个数组用于动态的跟踪第k个插入的数的位置

ph[N](POINT HEAP)该数组的下标表示第k个插入的数,内容是对应的堆中元素的下标
该数组的作用是给定一个k,可以迅速找到堆中的数

hp[N](HEAP POINT)该数组的下标与h下标相同,代表元素在堆中的位置,内容是对应的ph数组的下标
该数组的作用是在交换操作时,可以根据当前两元素下标找到对应的ph

所以在交换两元素时就不能是简单的交换值,同时也要交换其对应的ph和hp
在这里插入图片描述
在这里插入图片描述
交换操作的具体过程,可以不区分先后顺序

在这里插入图片描述

#include <iostream>
#include <algorithm>
#include  <string.h>

using namespace std;

const int N=1e5+10;

int n;

int h[N],ph[N],hp[N],size;//ph存放第k个插入的数对应的堆中的数的下标,,hp存放的是堆中的数对应的ph的下标(你是第几个插入堆的)


void  heap_swap(int x ,int y){      //传入需要交换数值的数的下标,,此步骤需要逐个交换ph,hp和h即可
    
    swap(h[x],h[y]);
    
    swap(ph[hp[x]],ph[hp[y]]);      //交换x,y对应的ph
    
    swap(hp[x],hp[y]);
}

void  up(int u){
    
    while(u/2&&h[u/2]>h[u]){
        
       heap_swap(u/2,u);             //注意传值是下标
        
        u/=2;
    }  
    
}

void  down(int u){
    
    
    int t=u;
   
    if(u*2<=size&&h[t]>=h[2*u])t=2*u;
    
    if(u*2+1<=size&&h[t]>=h[2*u+1])t=2*u+1;
    
    if(t!=u){
        
        heap_swap(t,u);
        
        down(t);
        
    }
}



int main(){
    
    int m=0;               //m代表当前插入的数是第几个
    
    scanf("%d",&n);
    
    while(n--){
        
        char op[5];
        
        int x,k;
        
        scanf("%s",op);
        
        if(!strcmp(op,"I")){
            
            cin>>x;
            
            m++,size++;
            
            ph[m]=size;
            
            hp[size]=m;
            
            h[size]=x;
            
            up(size);
        }
        
        else if(!strcmp(op,"PM")){
            
            printf("%d\n",h[1]);
            
        }
        
        else  if(!strcmp(op,"DM")){
            
            heap_swap(1,size);
            
            size--;
            
            down(1);
            
        }
        else  if(!strcmp(op,"D")){
            
            scanf("%d",&k);
            
            k=ph[k];                   //通过ph,,找到要删除的数在堆里对应的下标
            
            heap_swap(k,size);
            
            size--;
            
            down(k);
            
            up(k);
            
        }
        
        else{
            
            scanf("%d%d",&k,&x);
            
            k=ph[k];
            
            h[k]=x;
            
            down(k);
            
            up(k);
            
        }
        
    }
    
    
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值