数据结构与算法基础篇(二)trie树、并查集、堆


内容:trie、并查集和堆
手写堆
可以删除任意一个数 和 修改任意一个元素的操作(主要用在 迪杰斯特拉里)


一、 tries树 (字典树)

用来高效地存储 查找 字符串集合的数据结构
(一般字符串的 类型 不会很多)
如题:
在这里插入图片描述

AC代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int son[N][26]; //存子节点
int cnt[N];//记录   以i位 结尾 的  串数目
int idx;//此时新加结点  的总位数 
//插入
void insert(string s){
    int p = 0;//当前结点的指针 
    int len = s.size();
    for(int i = 0;i < len;i++){
        int u = s[i] - '0';
        if(!son[p][u]) son[p][u] = ++idx; //加入结点
        p = son[p][u]; 

    }
    cnt[p] ++;      
} 
//查询
int query(string s){
    int p = 0;
    int len = s.size();
    for(int i = 0; i < len; i++){
        int u = s[i] - '0';
        if(!son[p][u]) return 0; //中间但凡有一个结点找不到 就返回 
        p = son[p][u];//继续深入查找
    }
    return cnt[p];  
} 
int main(){
    // 75 25 打字   
    ios::sync_with_stdio(false);
    int n;
    cin>>n;
    string s;
    char ch;
    while(n--){
        //ch = getchar();  会错!! 
        cin>>ch>>s;
        if(ch=='Q'){
            printf("%d\n",query(s));
        }
        if(ch=='I'){
            insert(s);
        } 
    }   
    return 0;
} 

图形助解:
在这里插入图片描述


二、并查集

1.基础用法

(面试 常考 因为考思维 且 代码短)
思维强 代码短
思路: 用树来维护两个集合

用处:
1.将两个集合合并
2.询问两个元素 是否在一个集合中

在这里插入图片描述


最重要优化:路径压缩
通过让这条路径上的每一个点都指向根节点实现的
如图:

在这里插入图片描述


题目:
在这里插入图片描述
AC代码:

#include<iostream>
using namespace std;
const int N = 100010;
int p[N];//父节点数组;
int n,m; 
//返回祖宗节点  路径压缩
int find (int x){
	if(p[x]!=x) p[x] = find(p[x]);
	//如果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 a,b;
		scanf("%s%d%d",op,&a,&b);
		if(op[0]=='M') p[find(a)] = find(b);
		//a所属的祖宗节点的父节点  等于b的祖宗节点
		//反过来也可以
		else{
			if(find(a)==find(b)) puts("Yes");
			else puts("No");
		} 
	} 	
	return 0;
}

提示: C++的字符 如果是一个字符也用字符串儿来读入
因为如果是scanf来读的话 读入字符 会多读入空格
小技巧 一个字符也用 %s来读入 用的时候用首位就好。


2.顺便维护一些其他 信息

在这里插入图片描述
做法:用一个size数组来记
size : 每一个集合里面 点的数量
AC代码:

#include<iostream>
using namespace std;
const int N = 100010;
int n,m;
int p[N];
int si[N]; //只保证根结点的size有意义
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; 
        si[i] = 1;
    } 
    while(m--){
        char op[2];
        int a,b;
        scanf("%s",op);
        if(op[0]=='C'){
            scanf("%d%d",&a,&b);
            if(find(a)==find(b)) continue;
            //在同一个两通块中就不用
            si[find(b)] += si[find(a)]; //a接到b的上面
            //把集合a的数目加到   b(新的根结点)上
            p[find(a)] = find(b);
        }else if(op[1]=='1'){
            scanf("%d%d",&a,&b);
            if(find(a)==find(b)) puts("Yes");
            else puts("No");
        }else if(op[1]=='2'){
            scanf("%d",&a);
            printf("%d\n",si[find(a)]);
        }
        //找到根结点  输出它的size  就可以了
    }
    return 0;
}

三、堆

1.存储与表示方法

堆是一个完全二叉树

背景:
堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,
并同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。分别是小根堆和大根堆
一层一层的存
在这里插入图片描述

由于堆是一个完全二叉树,所以它可以用非常简便的数组来进行存储。
数组存储的元素就是节点的值,而数组下标位置就代表着该结点在堆中的位置。

在这里插入图片描述
如果某一个结点存储时在数组中下标为i

  1. 它的父节点位置在 i/2
  2. 它的左子结点位置在 2i
  3. 它的右子结点位置在 2i+1
堆状数据结构  完全二叉树  都是这样 用一维数组存

2.应用

要求:

  • 插入一个数
  • 求集合中的最小值
  • 删除最小值

  • 删除任意元素
  • 修改任意元素

求解方法: 在这里插入图片描述
1.插入一个数:在堆的最后一个位置 加入一个x 不断上移
2.删除最小的一个数删除 heap【1】(删除堆顶元素) 用整个堆的最后一个元素 去替代对顶元素,再对堆 进行堆化
图,展示的大根堆 道理是一样的
在这里插入图片描述
3.删除和修改任意元素:
不管三七二十一 不判断是down还是up 反正都只会做一个

建堆
//从堆的倒数第二层,开始向下 堆化调整

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

堆化下调:

void down(int u){
    int t = u;
    if(2*u<=n&&h[2*u]<h[u]) t = 2*u;
    if(2*u+1<=n&&h[2*u+1]<h[u]) t = 2*u+1;
    if(u!=t){ //如果小标变了 那就该交换了
        swap(h[t],h[u]);
        down(1);
    }
}

堆化上调:

void up(int u){
    while(u/2&&h[u/2]>h[u]){
        swap(h[u/2],h[u]);
        u /= 2;
    }
}

3.例题

1)堆排序

在这里插入图片描述
题目链接
解析: 前m个最小的数 就是 前m个小根堆 堆顶

AC代码:

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int h[N],size; //h[]堆数组,size 最后一个元素
int n,m;
void down(int u){
    int t = u;
    if(2*u<=n&&h[2*u]<h[u]) t = 2*u;
    if(2*u+1<=n&&h[2*u+1]<h[u]) t = 2*u+1;
    if(u!=t){ //如果小标变了 那就该交换了
        swap(h[t],h[u]);
        down(1);
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i = 1; i <= n;i++) scanf("%d",&h[i]);
    int  size = n;
    //建堆
    for(int i=n/2;i;i--) down(i);
    //操做
    while(m--){
        printf("%d ",h[1]);
        h[1] = h[size];
        size--;
        down(1);
    }
    return 0;
}
2)模拟堆

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

题目链接

关键: 第k个插入的点
解决办法和难点: 存和维护两个数组
解析:

但是为什么会用ph和hp这两个数组呢?
1.原因在于在删除第k个插入元素的操作中,我们首先得知道第k个插入元素在堆数组中的什么位置,即堆数组下标是啥。
很显然,用一个ph数组来存储会方便查找。

2.似乎有了ph数组就可以完成所有操作了,但为什么还要有一个hp数组呢?
原因就在于在swap操作中我们输入是堆数组的下标,无法知道每个堆数组的下标对应的是第几个插入的元素。
所以需要hp数组方便查找

在这里插入图片描述
AC代码:
记住 递归的时候 变量一定要理清楚!!!! 我的天啊

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100005;
int ph[N],hp[N],h[N];
int si = 0;
void s_swap(int a,int b){
    swap(h[a],h[b]);
    swap(hp[a],hp[b]);
    swap(ph[hp[a]],ph[hp[b]]);
}
void down(int u){
    int t = u;
    //t不断变换的值
    if(2*u<=si&&h[2*u]<h[t]) t = 2*u;
    if(2*u+1<=si&&h[2*u+1]<h[t]) t = 2*u + 1;
    if(t!=u){
        s_swap(t,u); down(t);
    } //t的值变了 那么就要和原来的交换了
}
void up(int u){
    if(u/2>0&&h[u]<h[u/2]){
        s_swap(u,u/2);
        up(u>>1);
    }
}
int main(){
    int n,m=0;
    cin>>n;
    while(n--){
        string op;
        int k,x;
        cin>>op;
        if(op=="I"){  //在末尾插入
            scanf("%d",&x);
            m++;//第几个  插入
            h[++si] = x;  //目前堆里面的节点数
            ph[m] = si;
            hp[si] = m;
            up(si);
        }else if(op=="PM") cout<<h[1]<<endl;
        else if(op=="DM"){
            s_swap(si,1);
            si--;
            down(1);
        }
        else if(op=="D"){
            cin>>k;
            int u = ph[k]; //找到第k个的坐标位置
            s_swap(u,si);
            si--;
            up(u);
            down(u);
        } 
        else if(op=="C"){//修改第k个插入的数  改为x
            cin>>k>>x;
            h[ph[k]] =x; 
            down(ph[k]);
            up(ph[k]);
        }
    }
    return 0;
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值