哈希表

174 阅读2分钟

哈希表

  • 哈希表一般用于将一个大的空间映射成一个比较小的空间,在映射的时候总会有元素冲突(哈希碰撞),这时候如何解决这种元素冲突的问题呢,有两种方法,一种是拉链法,一种是开放寻址法,这也就是哈希表的两种存储结构

拉链法

Snipaste_2023-03-07_18-48-48.png

  • 拉链法是一种数组加链表的结构,将映射成的比较小的空间开成一个数组,将每个元素进行哈希,通过链表的方式插在数组下面,如果发生哈希碰撞,通过头插法插入
  • 对于删除操作,一般是在对应元素上打一个标记,并不会真的删除
  • C++
//N取成尽可能远离2的幂次的质数,一般就是大于数据的第一个质数    
int h[N], e[N], ne[N], idx; //记得将h[N]内的元素全部赋成-1

    // 向哈希表中插入一个数
    void insert(int x)
    {
        int k = (x % N + N) % N;  //取模后再加N是为了去除负数
        e[idx] = x;
        ne[idx] = h[k];
        h[k] = idx ++ ;
    }

    // 在哈希表中查询某个数是否存在
    bool find(int x)
    {
        int k = (x % N + N) % N;
        for (int i = h[k]; i != -1; i = ne[i])
            if (e[i] == x)
                return true;

        return false;
    }
  • Java
//N取成尽可能远离2的幂次的质数,一般就是大于数据的第一个质数
public static final int N = 100003;
public static int[] h = new int[N];  //记得将h[N]内的元素全部初始化赋成-1
public static int[] e = new int[N];
public static int[] ne = new int[N];
public static int idx;

public static void insert(int x) {
    int k = (x % N + N) % N;
    e[idx] = x;
    ne[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 = ne[i]) {
        if (e[i] == x) {
            return true;
        }
    }
    return false;
}
  • 寻找质数
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) {
        pw.println(i);
        break;
    }
}

开放寻址法

  • 开放寻址法只需要开一个数组,这个数组的长度一般在题目数据范围的2~3倍

  • 存储:通过哈希找到索引后,如果该位置有元素,就存放到后一位,以此类推

  • 查找:通过哈希找到索引后,如果该位置非空且不为当前元素,就向后一位查找,直到找到该元素或该位置为空为止

  • 删除:在该位置上打一个标记,并不会真的删除

  • 数组初始化为null = 0x3f3f3f3f

    • 在算法竞赛中,我们常常需要用到设置一个常量用来代表“无穷大”。

      比如对于int类型的数,有的人会采用INT_MAX,即0x7fffffff作为无穷大。但是以INT_MAX为无穷大常常面临一个问题,即加一个其他的数会溢出。

      而这种情况在动态规划,或者其他一些递推的算法中常常出现,很有可能导致算法出问题。

      所以在算法竞赛中,我们常采用0x3f3f3f3f来作为无穷大。0x3f3f3f3f主要有如下好处:

      0x3f3f3f3f的十进制为1061109567,和INT_MAX一个数量级,即10^9^ 数量级,而一般场合下的数据都是小于10^9^的。 0x3f3f3f3f * 2 = 2122219134,无穷大相加依然不会溢出。

    • 在C++中,可以使用memset(array, 0x3f, sizeof(array))来为数组设初值为0x3f3f3f3f,因为这个数的每个字节都是0x3f。

    • 在Java中,可以使用Arrays.fill(array, 0x3f3f3f3f)来为数组设初值为0x3f3f3f3f

  • C++

    int h[N];  //记得初始化h[N]内元素全部初始化赋为0x3f3f3f3f

    // 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
    int find(int x)
    {
        int t = (x % N + N) % N;
        while (h[t] != null && h[t] != x)
        {
            t ++ ;
            if (t == N) t = 0;
        }
        return t;
    }
  • Java
public static int[] h = new int[N];

public static int find(int x) {
    int t = (x % N + N) % N;
    while (h[t] != null && h[t] != x) {
        t++;
        if (t == N) {
            t = 0;
        }
    }
    return t;
}

字符串哈希

  • 字符串前缀哈希法
    • h[i]存储字符串前i个字符构成的字串的哈希值
    • 将字符串看成P进制的数,P的经验值是131或13331,取这两个值的造成哈希碰撞的概率低
    • 将P进制的数变成十进制的数,此时的数非常大,因此要再模上一个数Q,小技巧:取模的数Q用2^64^,这样直接用unsigned long long存储,溢出的结果就是取模的结果
    • 这样的哈希方式配上前缀哈希有什么样的好处?
      • 可以利用前缀哈希算出任意一个子串的哈希值

Snipaste_2023-03-07_22-59-04.png

Snipaste_2023-03-07_23-12-15.png

  • C++
//核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
//小技巧:取模的数Q用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果

typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64

// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
    h[i] = h[i - 1] * P + str[i];
    p[i] = p[i - 1] * P;
}

// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}
  • Java
public static final int P = 131;     //P的经验值是131或13331,取这两个值的冲突概率低
public static long h = new long[N];  //存储前i个字符组成的字符串的哈希值
public static long p = new long[N];  //存储P的i次方

//初始化
p[0] = 1;
for (int i = 1; i <= n; i++) {
    h[i] = h[i - 1] * P + str.charAt(i - 1);
    p[i] = p[i - 1] * P;
}

//计算字串str[l ~ r]的哈希值
public static long get(int l, int r) {
    return h[r] - h[l - 1] * p[r - l + 1];
}

练习

01 模拟散列表

  • 题目

Snipaste_2023-03-10_23-52-49.png

  • 题解
import java.io.*;
import java.util.*;

public class Main {
    public static final int N = 200003;
    public static final int NULL = 0x3f3f3f3f;
    public static int[] h = new int[N];
    public static int n;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
        n = Integer.parseInt(br.readLine());
        Arrays.fill(h, NULL);

        while (n-- > 0) {
            String[] str1 = br.readLine().split(" ");
            int x = Integer.parseInt(str1[1]);
            int k = find(x);
            if (str1[0].equals("I")) {
                h[k] = x;
            } else {
                if (h[k] != NULL) {
                    pw.println("Yes");
                } else {
                    pw.println("No");
                }
            }
        }
        pw.close();
        br.close();
    }

    public static int find(int x) {
        int t = (x % N + N) % N;
        while (h[t] != NULL && h[t] != x) {
            t++;
            if (t == N) {
                t = 0;
            }
        }
        return t;
    }
}

02 字符串哈希

  • 题目

Snipaste_2023-03-10_23-53-42.png

  • 题解
import java.io.*;

public class Main {
    public static final int N = 100010;
    public static final int P = 131;
    public static long[] h = new long[N];
    public static long[] p = new long[N];
    public static int n, m;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
        String[] str1 = br.readLine().split(" ");
        n = Integer.parseInt(str1[0]);
        m = Integer.parseInt(str1[1]);
        String str2 = br.readLine();

        p[0] = 1;
        for (int i = 1; i <= n; i++) {
            h[i] = h[i - 1] * P + str2.charAt(i - 1);
            p[i] = p[i - 1] * P;
        }

        while (m-- > 0) {
            String[] str3 = br.readLine().split(" ");
            int l1 = Integer.parseInt(str3[0]);
            int r1 = Integer.parseInt(str3[1]);
            int l2 = Integer.parseInt(str3[2]);
            int r2 = Integer.parseInt(str3[3]);
            if (get(l1, r1) == get(l2, r2)) {
                pw.println("Yes");
            } else {
                pw.println("No");
            }
        }
        pw.close();
        br.close();
    }

    public static long get(int l, int r) {
        return h[r] - h[l - 1] * p[r - l + 1];
    }
}