5

数据结构篇——哈希表 - 秋落雨微凉

 1 year ago
source link: https://www.cnblogs.com/qiuluoyuweiliang/p/16910284.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

数据结构篇——哈希表

本次我们介绍数据结构中的哈希表,我们会从下面几个角度来介绍:

  • 哈希表介绍
  • 例题模拟散列表的两种方法
  • 字符串前缀哈希法

哈希表介绍

首先我们先来简单介绍一下哈希表:

  • 哈希表主要负责将空间较大的离散的数压缩为空间较小的数
  • 例如我们将10-9~109之间的离散数可以压缩到10^5数组中

我们哈希表的主要算法为:

  • 将x mod 10^5 得出余数,按照余数放在压缩后的数组中去
  • 如果遇到冲突问题,我们采用两种方法来解决:拉链法和开放寻址法

我们给出两种解决方式:

  • 拉链法:整个数组额外创建e[n]和ne[n]来当作链表存储点和下一个链表点来使用
  • 开放寻址法:我们创造较多的数组,并按照正常方法放置,若当前点位已被放置就向后存放直到存放成功

模拟散列表的两种方法

首先我们给出例题:

  • 维护一个集合,支持如下几种操作:
  • I x,插入一个数 xx;
  • Q x,询问数 xx 是否在集合中出现过;

我们分别给出两种解决方法:

/*拉链法*/

import java.util.Scanner;

public class Main {

    // 注意:这里的N尽量取到大于范围的第一个质数,因为质数是最不容易出现冲突的
    public static final int N = 100003;

    // 创建数组,创建拉链法的链表和下一个链表位(h存放的是e的位置,ne存放的当前e的下一个e的位置)
    public static int[] h = new int[N];
    public static int[] e = new int[N];
    public static int[] ne = new int[N];
    public static int idx = 0;

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        int n = scanner.nextInt();

        // 初始化定义
        for(int i = 0 ; i < N ; i++ ){
            h[i] = -1;
        }

        // 执行方法
        while (n-- > 0){
            String op = scanner.next();

            if(op.equals("I")){
                int x = scanner.nextInt();
                insert(x);
            }else{
                int x = scanner.nextInt();
                if(find(x)) System.out.println("Yes");
                else System.out.println("No");
            }


        }
    }

    public static void insert(int x){

        // 根据x判断其在h的位置(下面的运算是保证即使是负数其运算值也要为正数)
        int index = (x % N + N) % N;

        // insert操作(链表操作)
        e[idx] = x;
        ne[idx] = h[index];
        h[index] = idx;
        idx++;

    }

    public static boolean find(int x){

        // 根据x判断其在h的位置(下面的运算是保证即使是负数其运算值也要为正数)
        int index = (x % N + N) % N;

        // 循环判断
        for (int i = h[index]; i != -1; i = ne[i]){
            // 找到了返回true
            if (e[i] == x){
                return true;
            }
        }

        // 找不到返回false
        return false;
    }
}

/*开放寻址法*/

import java.util.Scanner;

public class Main {

    // 我们采用开放寻址法,需要将数据扩大一定倍数用于存放
    // 注意:这里的N尽量取到大于范围的第一个质数,因为质数是最不容易出现冲突的
    public static final int N = 200003;

    // 我们需要定义一个超出范围的数,作为数组的各部分的初始值
    public static final int NULL = 0x3f3f3f3f;

    // 我们只需要创建数组即可
    public static int[] h = new int[N];
    public static int idx = 0;

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        int n = scanner.nextInt();

        // 初始化定义
        for(int i = 0 ; i < N ; i++ ){
            h[i] = 0x3f3f3f3f;
        }

        // 执行方法
        while (n-- > 0){
            String op = scanner.next();
            int x = scanner.nextInt();
            int index = find(x);

            if(op.equals("I")){
                h[index] = x;
            }else{
                if(h[index] != NULL) System.out.println("Yes");
                else System.out.println("No");
            }

        }
    }

    public static int find(int x){

        // 根据x判断其在h的位置(下面的运算是保证即使是负数其运算值也要为正数)
        int index = (x % N + N) % N;

        // 开始寻找位置(为空时插入或者查找失败;为x时为查找成功)
        while (h[index] != NULL && h[index] != x){
            // 没有找到位置,向后移动;若到头回到开头继续匹配
            index++;
            if (index == N) index = 0;
        }

        // 找到位置
        return index;

    }
}

字符串前缀哈希法

我们首先来介绍一下字符串哈希:

  • 字符串哈希和正常哈希方法相同
  • 但是通常为了防止冲突,会采用特定的赋值哈希值的方法

我们来介绍P进制法赋值:

/*P进制法赋值介绍*/

// 我们首先给每个字符指定一个数,例如a=1,b=2,c=3...

// 然后我们需要指定一个p,作为进制数,让每一位在相应位置上乘上p的n次方

// 例如我们的"abc" = (1 * p^2)+(2 * p^1)+(3 * p^0)

/*P进制法赋值注意*/

// 首先我们需要注意字符尽量不为0,因为这样a,aa,aaa等的哈希值均为0,就会导致冲突

// 此外我们为了减少冲突,我们的p和q(进制以及哈希数组大小)应该设置为131和2^64,这是网上查询的最佳数据

在介绍过字符串哈希之后,我们来对习题进行更加细腻的分析:

  • 给定一个长度n的字符串,再给定m个询问,每个询问包含四个整数 l1,r1,l2,r2。
  • 请你判断 [l1,r1]和 [l2,r2] 这两个区间所包含的字符串子串是否完全相同。
  • 字符串中只包含大小写英文字母和数字。

我们首先进行分析:

/*字符串前缀法*/

// 我们进行字符串匹配并使用哈希方法就需要采用P进制辅助

// 但是为了一次性获得所有该字符串的哈希数据,我们可以采用字符串前缀法,也就是kmp思想

// 我们对于n的字符串,只需要求n次字符串的值(复杂)就可以根据特定的方法来求出内部字符串的哈希值

// 例如我们需要求1234 中的 34,我们只需要用1234哈希值来减去12*p^2的哈希值(需要乘上两者位数之差)

// 转换为代码就是 h[r] - (h[l-1] * p^(r-l+1))

我们给出问题解答代码:

import java.util.Scanner;

public class Main {

    // 首先我们需要一个N,P

    public static final int N = 1000010;
    public static final int P = 131;

    // 开的是long类型数组,本来是需要进行前缀hash求完之后需要进行模2的64次方来防止相同的冲突,可能超过我们给的数组大小
    // h存放数据,p存放p的n次方
    public static long[] h = new long[N];
    public static long[] p = new long[N];

    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);

        int n = scanner.nextInt();
        int m = scanner.nextInt();
        String s = scanner.next();

        // p的0次方
        p[0] = 1;

        // 首先需要初始化,以及制作h前缀和哈希
        for (int i = 1;i <= n;i++){
            p[i] = p[i-1] * P;//这里对应每一个下标对应对应P的多少次方
            h[i] = h[i-1] * P + s.charAt(i-1);// 预处理前缀哈希的值,因为是P进制,所以中间乘的是P
        }

        // 开始运算
        while (m-- > 0){
            
            int l1,r1,l2,r2;

            l1 = scanner.nextInt();
            r1 = scanner.nextInt();
            l2 = scanner.nextInt();
            r2 = scanner.nextInt();

            // 分别获得hash值,然后比较
            long hashcode1 = get(l1,r1);
            long hashcode2 = get(l2,r2);

            if (hashcode1 == hashcode2){
                System.out.println("Yes");
            }else {
                System.out.println("No");
            }
        }

    }

    // 获得区间哈希值,采用字符串前缀和法,用前缀和之差来获得当前区间的哈希值
    public static long get(int l,int r){
        return h[r] - h[l-1]*p[r-l+1];
    }
}

好的,关于数据结构篇的哈希表就介绍到这里,希望能为你带来帮助~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK