Trie、并查集、堆、Hash表学习过程以及遇到的问题

Trie、并查集、堆、Hash表:

Trie

快速存储和查找字符串集合 字符类型统一,将单词在最后一个字母结束的位置上打上标记

image-20210225140550936

练习题:Trie字符串统计

import java.util.*;

public class Main{
    static int N = 100010;
    static int[][] son = new int[N][26];
    static int[] con = new int[N];
    static int idx =0;
    static char[] str = new char[N];
    // 插入操作
    public static void insert(char[] str){
        // 初始化根节点
        int p = 0;
        // 遍历字符串的每个字符
        for(int i = 0; i < str.length; i++){
            //将字母映射为数组  'a'-->97;
            int u = str[i] -'a';
            //如果子节点没有 及:son[p][u] ==0;
            //那么 添加节点
            if(son[p][u] == 0) son[p][u] = ++idx;
            //更新节点位置
            p = son[p][u];
        }
        //统计最后以str[p]这个结尾的字母
        con[p]++;
    }
    // 查找操作
    public static int select(char[] str){
        //初始化根节点
        int p = 0;
        for(int i = 0; i < str.length; i++){
            int u = str[i] - 'a';
            //如果子节点==0;代表没有所查找的字母,及没有该单词,返回0次
            if(son[p][u] == 0) return 0;
            p = son[p][u];
        }
        //最后遍历完成后p就是以str[p]的字母
        //返回单词的次数
        return con[p];
        
    }
    
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        
        int n = sc.nextInt();
        while(n-- != 0){
            String common = sc.next();
            String str = sc.next();
            if(common.equals("I")){
                //toCharArray() -->将字符串转为字符数组
                insert(str.toCharArray());
            }else if(common.equals("Q")){
                System.out.println(select(str.toCharArray()));
            }
        }
        
    }
}

最大异或对:

image-20210226151351110

暴力方法:

public class Main{
    static int res = 0;
    public static void main(String[] args){
        for(int i = 0; i < n; i++){  //枚举第一个数
            for(int j = 0; j < i; j++){  //枚举第二个数
                res  = Math.max(res,a[i] ^ a[j]); 
            }
        }
        System.out.println(res);
    }
    
}

使用Trie树来做:

import java.util.*;

public class Main{
    static int N = 100010,M = 3000000;
    static int[] a = new int[N];
    //树的长度最多不超过N*31,节点为0,1;
    static int[][] son = new int[M][2];
    static int idx;
    
    // 创建Trie
    public static void insert(int a){
        int p = 0;
        for(int i = 30; i >= 0; i--){
            // 判断a的二进制位的第i个数是0还是1;
            int s = a >> i & 1;
            if(son[p][s] == 0) son[p][s] = ++idx;
            p = son[p][s];  //把当前节点移动到下一节点;
        }
    }
    // 查询
    public static int query(int a){
        int p = 0,res = 0;
        for(int i = 30; i >= 0; i--){
            //判断数字a在第i位的二进制是0还是1;
            int s = a >> i&1;
            //要想使得a^x最大,那么x要最小,也就是x得二进制与a二进制相反才行
            //判断子节点与当前a中二进制相反得分支存不存在(不存在 son[p][1-s] ==0);
            if(son[p][1-s] != 0){
                //二进制转十进制操作
                //原:3-->110,查:001; index:2、1、0;
                //查到一位转换成十进制相加即等于最后与原数异或为最大值
                res = res + (1<< i);
                //更新p位置,即往下走下一节点(相反得一条路)
                p = son[p][1-s];
            //查找得分支点没有得话,只能走已存在得分支;
            }else p = son[p][s];
        }
        //返回结果
        return res;
    }
    
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int res = 0;
        for(int i = 0; i < n; i++){  //初始化数组
            a[i] = sc.nextInt();
            insert(a[i]);
        }
        //遍历数组中所有得元素,找到数组中异或得最大得数;
        for(int i = 0; i < n; i++){
            //query返回得使a[i]^x最大得值
            res  = Math.max(res,query(a[i]));
        }
        System.out.println(res);
        
    }
    
}

问题汇总:

开的son中,第一个取得M是什么含义。
Trie树得深度不是31吗,那只开31个空间不久好了吗?

答:

son的第一维度存的是trie数一共有多少节点,如果是存储一个数的话,确实开31个空间就好了,但是存储的是N = 100000个数,每个数循环31次,那就是31*100000 = 310w,因为会有复用的节点,用不上这么多,300w就可以了

问:

int 是32位的,请问在对一个数进行遍历,判断该位是否为1时,为啥是从30~0;不用关心第31位吗?

答:

题目中规定了 0 ≤ Ai <2^31,所以循环到30就够了

问:

什么是从i=30开始而不是0

答:

因为是求最大值,所以从最高位开始比较,要有限保证最高位为1

并查集:O(1)

1、 将两个集合合并

2、 询问两个元素是否再一个集合当中

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

问题一:如何判断树根:if(p[x] == x) x就是树根

问题二:如何求x的集合编号:while(p[x] != x) x = p[x]---->包含路径压缩算法(优化)

问题三:如何合并两个集合:px是x的集合编号,py是y的集合编号,p[x] = y

核心操作:

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

朴素并查集--无扩展

import java.util.*;
public class Main{
    // 每个集合用树来存储
    static int N = 100010;
    // 建立父节点数组
    static int[] p = new int[N];
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int m = sc.nextInt();
        
        // 初始化父节点数组
        for(int i = 1; i <= n; i++) p[i] = i;
        
        while(m-- != 0){
            char s = sc.next().charAt(0);
            
            int a = sc.nextInt();
            int b = sc.nextInt();
            
            if(s == 'M'){
                //将a根节点的父节点指向b的根节点--》实现两个集合的合并
                p[find(a)] = find(b);
            }else{
                // 如果a的根节点等于b的根节点-->在同一个集合中
                if(find(a) == find(b)) System.out.println("Yes");
                else System.out.println("No");
            }
        }
    }
    // 返回x的根节点
    public static int find(int x){
        // 如果父节点不等于根节点,则递归寻找
        if(p[x] != x)  p[x] = find(p[x]);
        //返回x的所在的根节点
        return p[x];
    }
    
}

以下扩展情况:维护集合大小

import java.util.*;

public class Main{
    static int N = 100010;
    // 每个集合
    static int[] p = new int[N];
    // 每个集合的大小
    static int[] size = new int[N];
    
    // 返回集合(x)的跟节点
    public static int find(int x){
        if(p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        
        int n = sc.nextInt();
        int m = sc.nextInt();
        
        // 对每个集合以及集合大小进行初始化
        for(int i = 0; i < n; i++){
            p[i] = i;
            size[i] = 1;
        } 
        
        while(m -- != 0){
            
            String s = sc.next();
            if(s.equals("C")){
                int a = sc.nextInt();
                int b = sc.nextInt();
                //不加判断,当a与b集合相同时, 执行了自己加自己,不符合题意,需要特判
                if(find(a) != find(b)){
                    // 合并后的连通块数量(有多少个点)
                    size[find(b)] += size[find(a)];
                    // a集合的父节点执行b集合
                    p[find(a)] = find(b);
                }
                
            }
            else if(s.equals("Q1")){
                int a = sc.nextInt();
                int b = sc.nextInt();
                
                if(find(a) == find(b)) System.out.println("Yes");
                else System.out.println("No");
            }
            else{
                int a = sc.nextInt();
                // 查询a所在集合的连通块大小,找到a的根,并统计根的数量
                System.out.println(size[find(a)]);
            }
        }
    }
    
}

堆:

:就是一个用一维数组来表示一个完全二叉树的这么一个数据结构。所谓二叉树就是一种树,每一个父节点,有最多两个子节点,一般叫做左右子树

完美二叉树:是一个二叉树层数为k的时候,它的元素数量等于2k-1

image-20210308141724961

而一个完全二叉树可以理解为是一个完美二叉树缺少一部分或者不缺少一部分的二叉树,但是内容一定是从上到下,从左到右的填充,也就是缺少的部分总在右边;

image-20210308141802366

小根堆: 即根节点小于等于它的左孩子,也小于等于它的右孩子,且每个点都小于左右子节点;左孩子是左边集合的最小值,右孩子是右边的最小值

根节点是左右孩子的最小值--->推论出:根节点为堆的最小值

如何手写一个堆:

size-->表示堆的大小;

  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);
    

堆排序:

步骤:(输出前m个的最小值)

  1. 初始化堆
  2. 建堆
  3. down操作

其中down操作的实现过程:

比较三个点的最小值,如果不符合堆的定义那么就交换、递归执行down操作

import java.util.*;
import java.io.*;

public class Main{
    static int N = 100010;
    // 定义堆
    static int[] h = new int[N];
    // 确定堆的大小
    static int size;
    
    // 当数在三个数中大时,使数往下沉
    public static void down(int u){
        // 设三个数的最小值为t;
        int t = u;
        // u*2为 u的左儿子; u*2+1 为u的右儿子;
        // 如果左儿子的下标小于堆的大小,则表示存在这个点;
        // 并且左儿子值比最小t的值小,则将t指向左儿子
        if(u*2 <= size && h[u*2] < h[t]) t = u * 2;
        // 如果右儿子的下标小于堆的大小,则表示存在这个点;
        // 并且右儿子值比最小t的值小,则将t指向右儿子
        if(u*2+1 <= size && h[u*2+1] < h[t]) t = u*2+1;
        // 如果最后t的最小值不是自己(u);
        // 那么交换两个下标所在的值;交换完在down一下,防止破坏堆结构;
        if(t != u){
            int temp = h[u];
            h[u] = h[t];
            h[t] = temp;
            down(t);
        }
        
    }
    
    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        
        String[] s = br.readLine().split(" ");
        
        int n = Integer.parseInt(s[0]);
        int m = Integer.parseInt(s[1]);
        
        String[] str = br.readLine().split(" ");
        // 初始化堆
        for(int i = 1; i <= n; i++){
            h[i] = Integer.parseInt(str[i-1]);
        }
        // 设置堆的大小
        size = n;
        //通过递推可得到时间复杂度:建堆-->时间复杂度为O(n)
        for(int i = n/2;i != 0;i--){
            down(i);
        }
        
        while(m-- != 0){
            // 输出最当前最小值,也就是堆顶
            bw.write(h[1]+" ");
            // 将最后的值覆盖掉堆顶--就是删掉堆顶
            h[1]  =h[size];
            // 堆大小--
            size--;
            // 在把堆顶down一下  找出最小值;
            down(1);
        }
        
        bw.flush();
        br.close();
        bw.close();
    }
    
}

实现up操作:

u/2为u的父节点,h[u] < h[u/2]-->子节点小于父节点;交换完后,up(u父节点的父节点);

首先堆是完全二叉树:(以下是编号)

  1
 2  3
4 5 6 7

2 / 2 = 1, 3 / 2 = 1.
4 / 2 = 2, 5 / 2 = 2, 6 / 2 = 3, 7 / 2 = 3

通过上面操作就能找到父节点;

public static void up(int u){
        if(u / 2 > 0 && h[u] < h[u / 2]){
            heapSwap(u, u / 2);
            up(u/2);
        }
    }

模拟堆:

import java.util.*;
import java.io.*;

public class Main{
    static int N = 100010;
    static int[] h = new int[N];
    static int[] ph = new int[N]; //存放第k个点的值的下标
    static int[] hp = new int[N]; //存放队中点的值是第几个插入的
    static int size;  //size 记录的是堆当前的数据多少
    
    public static void down(int u){
        int t = u;
        if(u*2 <=size && h[u*2] < h[t]) t = u*2;
        if(u*2+1 <= size && h[u*2+1] < h[t]) t = u*2+1;
        if(t != u){
            heap_swap(u,t);
            down(t);
        }
    }
    public static void up(int u){
        if(u / 2 > 0 && h[u] < h[u / 2]){
            heap_swap(u,u/2);
            up(u/2);
        }
    }
    
    public static void heap_swap(int u,int v)
    {   
        // 相对照
        // swap(h[u],h[v]);   //值交换 
        // swap(hp[u],hp[v]);  //堆中点的插入顺序(编号)交换
        // swap(ph[hp[u]],ph[hp[v]]); //对编号第h[u] h[v]的值交换
        
        swap(h,u,v);
        swap(hp, u, v);
        swap(ph, hp[u], hp[v]);
    
    }
    
    public static void swap(int[] a, int u, int v){
        int tmp = a[u];
        a[u] = a[v];
        a[v] = tmp;
    }
    
    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        int n = Integer.parseInt(br.readLine());
        size = 0;
        int m = 0;  
        
        while(n-- != 0){
            String[] s = br.readLine().split(" ");
            String op = s[0];
            if("I".equals(op)){
                int x = Integer.valueOf(s[1]);
                m++;
                h[++size]=x;
                // ph[m]=size;
                // hp[size]=m;
                // 建立映射关系即:第m个插入的数的编号是size;
                ph[m] = size;
                //第size编号下的是第m个插入的数;
                hp[size] = m;
                // 将插入的数向上调整
                up(size);
            }else if("PM".equals(op))    bw.write(h[1]+"\n");
            else if("DM".equals(op)){
                heap_swap(1,size);
                size--;
                down(1);
            }else if("D".equals(op)){
                int k = Integer.parseInt(s[1]);
                int u=ph[k];                //这里一定要用u=ph[k]保存第k个插入点的下标
                heap_swap(u,size);          //因为在此处heapSwap操作后ph[k]的值已经发生 
                size--;                    //如果在up,down操作中仍然使用ph[k]作为参数就会发生错误
                up(u);
                down(u);
            }else if("C".equals(op)){
                int k = Integer.parseInt(s[1]);
                int x = Integer.parseInt(s[2]);
                h[ph[k]]=x;                 //此处由于未涉及heapSwap操作且下面的up、down操作只会发生一个所以
                down(ph[k]);                //所以可直接传入ph[k]作为参数
                up(ph[k]);
            }
        }
        bw.flush();
        br.close();
        bw.close();
        
    }
    
}

哈希表:

求质数:

import java.util.Scanner;

public class 求质数 {
    public static void main(String[] args) {
       for(int i = 100000;;i++){
           boolean flag = true;
           for(int j = 2; j* j <= i; j++){
               if(i % j == 0){
                   flag = false;
                   break;
               }
           }
           if(flag){
               System.out.println(i);
               break;
           }
       }
    }
}

拉链法:

import java.util.*;
import java.io.*;
public class Main{
    //h[]是哈希函数的一维数组
    //N为数据范围外的最小质数
    //模N这个数一般要取成质数且离2的整次幂尽可能的远---减少哈希冲突的概率
    static int N = 100003;
    static int[] h = new int[N];
    //e[]是链表中存的值
    static int[] e = new int[N];
    //ne[]是指针存的指向的地址
    static int[] next = new int[N];
    //idx是当前指针
    static int idx;
    // 插入操作
    public static void insert(int x){
        //对负数的处理,k是哈希值
        //如果x%N的余数为零,+N则一定为正数,然后再取模
        int k = (x % N + N) % N;
        // 单链表的实现
        //头插法
        e[idx] = x;
        next[idx] = h[k];
        h[k] = idx++;
    }
    // 查找元素是否存在
    public static boolean find(int x){
        int k = (x% N + N) % N;
        for(int i = h[k]; i != -1; i=next[i]){
            //找到返回true
            if(e[i] == x) return true;
        }
        return false;
    }
    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        //初始化h[]
        for(int i=0;i<N;i++){
            h[i]=-1;
        }
        while(n-->0){
            String[] s = br.readLine().split(" ");
            int x = Integer.parseInt(s[1]);
            if(s[0].equals("I")){
                insert(x);
            }else{
                if(find(x))System.out.println("Yes");
                else    System.out.println("No");
            }
        }
    }
    
}

开放寻址法:

好处:只开一个数组即可;

package ACWing.数据结构与算法;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class 开放寻址法_哈希表 {
    //一般开到范围的两到三倍
    //N的值也需要取模判断一下
    static int N= 200003;
    static int[] h = new int[N];
    //设当前值不在题目给出的范围中,表示当前无数据--空数据
    static int bound = (int)(1e9+1);

    public static int  find(int x){
        int k = ( x % N + N) % N;
        //如果当前空间有数据,且该空间的数据不等于我们要查找的数据
        //那么继续往下寻找
        while(h[k] != bound && h[k] != x){
            k++;
            if(k== N) k = 0;
        }
        //返回的情况有两种
        /*
        1、k指代的是当前的空间没有人
        2、k指代的是当前的空间有人且就是我们要查找的元素
        */
        return k;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());

        for(int i = 0; i < N; i++) h[i] = bound;
        while(n-- != 0){
            String[] s = br.readLine().split(" ");
            String op = s[0];
            int x = Integer.parseInt(s[1]);
            int k = find(x);
            if(op.equals("I")){
                h[k] = x;
            }else{
                //如果当前元素不为空
                if(h[k] != bound) System.out.println("Yes");
                //为空
                else System.out.println("No");
            }
        }
        br.close();
    }
}

字符串哈希方式:

  1. 先将字符串转换成P进制的数字;
  2. 然后求出前缀和的哈希值

怎么求前缀和的哈希值:

将字符串看成P进制的数:

例:求ABCD的哈希值

看成P进制的话,该字符串有四位:

image-20210317195338779

如果数据量太大的话,我们需要mod上一个Q,通过取模可以映射到(0-Q-1)上的数

注意:

不能映射为0,因为0与任何数进行运算都为零,这样会造成数据重复;

当映射的时候必定会出现两个不同的数取模成相同的数

解决的方法:假定人品足够好,不会出现冲突,且经验取值为:P=131或者13331,Q=2^64次方的时候,可以避免99.99%的冲突;

求哈希值:

image-20210317200918986

h[]数组表示前缀和的哈希值;

h[R] : 1-R的前缀和hash值

h[L-1] : 1-(L-1)的前缀和hash值

在h[R]中:

image-20210317201533425

在h[L-1]中:

image-20210317201849715

我们的目标时求出L-R的前缀和哈希值,以下看图说话

对于区间和公式的理解:h[l,r]=h[r]−h[l−1]×P^(r−l+1)

image-20210317212127591

求L-R 也就是求D-E,也就是求4-5

123*100= 12300;12345-12300 = 45;

ABC*100 = ABC00; ABCDE-ABC=DE;

h[R]-h[L-1]*P^(R-1-(L-2));

h[R] - h[L-1]*P^(R-L+1)

解题步骤:

  1. 先保存每位的权值;
  2. 求每个字符的前缀和哈希值
  3. 获取区间的前缀和哈希值
  4. 调用函数进行比较

字符串前缀哈希法:

import java.util.*;
import java.io.*;
public class Main{
    static int N = 100010;
    static int h[] = new int[N];
    static int p[] = new int[N];
    static int P = 131;
    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        String[] s = br.readLine().split(" ");
        //输入长度为n的字符串
        int n = Integer.parseInt(s[0]);
        //m次询问
        int m = Integer.parseInt(s[1]);
        String str = br.readLine();
        p[0] =1;   //一定不要忘记设置为1;
        for(int i = 1; i <= n; i++){
            //预处理保存每位的权值
            p[i] = p[i-1]*P;
            //获取前缀和哈希值
            h[i] = h[i-1]*P+str.charAt(i-1);
        }
        
        while(m-- != 0){
            String[] s1 = br.readLine().split(" ");
            int l1 = Integer.parseInt(s1[0]);
            int r1 = Integer.parseInt(s1[1]);
            int l2 = Integer.parseInt(s1[2]);
            int r2 = Integer.parseInt(s1[3]);
            if(getHash(l1,r1) == getHash(l2,r2)) bw.write("Yes"+"\n");
            else bw.write("No"+"\n");
        }
        bw.flush();
        br.close();
        bw.close();
    }
    public static long getHash(int l,int r){
        return h[r] - h[l-1]*p[r-l+1];
    }
}
posted @ 2021-03-30 15:42  Xbhog  阅读(131)  评论(0编辑  收藏  举报