【算法基础:数据结构】2.4 堆 和 哈希表(堆排序&模拟堆、散列表)

堆 / 优先队列

如何手写一个堆?(使用一维数组存储)

堆的操作:

  1. 插入一个数
  2. 求集合当中的最小值
  3. 删除最小值
  4. 删除任意一个元素
  5. 修改任意一个元素


后面两个 4 和 5 是 stl 里的堆无法实现的
但是 Java 的 API 可以实现,有 remove() 方法。

堆 是一棵 完全二叉树

最后一层节点是从左到右依次排布的。
小根堆:每一个节点都是小于等于左右儿子的。

在这里插入图片描述
Q:如何存储堆?
A:使用一个一维数组就可以。


堆的两个基本操作 up 和 down

down(x)up(x)

e.g. down(6)
在这里插入图片描述
在 6,3,4 里找到最小值 3,将 3 和 6 交换位置。
在 6,3,5 里找到最小值 3,将 3 和 6 交换位置。

最后变成了
在这里插入图片描述

e.g. up(2)

在这里插入图片描述

每次往上走的时候只需要和根节点比较就好了。

最后变成了
在这里插入图片描述

用两个基本操作实现堆⭐

堆的操作:

  1. 插入一个数
  2. 求集合当中的最小值
  3. 删除最小值
  4. 删除任意一个元素
  5. 修改任意一个元素

在这里插入图片描述

例题列表

838. 堆排序(手写堆)⭐⭐⭐ 模板(数组存储 + down操作)

https://www.acwing.com/activity/content/problem/content/888/
在这里插入图片描述

import java.util.Scanner;

public class Main {
    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(), m = scanner.nextInt(), a, size = 0;
        int[] heap = new int[n + 1];
        while (n-- != 0) {
            a = scanner.nextInt();
            heap[size++] = a;
        }
        for (int i = size / 2; i >= 0; --i) down(i, heap, size);    // 只需要从 size / 2 开始就可以了

        while (m-- != 0) {
            System.out.print(heap[0] + " ");
            heap[0] = heap[--size];		// 每次将最后一个元素放在堆顶,然后down操作
            down(0, heap, size);
        }
    }

    static void down(int x, int[] heap, int size) {
        // 将 x 与两个子节点进行比较
        int t = x;
        if (2 * x + 1 < size && heap[2 * x + 1] < heap[t]) t = 2 * x + 1;
        if (2 * x + 2 < size && heap[2 * x + 2] < heap[t]) t = 2 * x + 2;

        if (x != t) {
            swap(heap, x, t);
            down(t, heap, size);
        }
    }

    static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

Q:为什么在初始化时从 size / 2 开始 down 就可以了?
A:
在这里插入图片描述
O ( n ) O(n) O(n) 的。(用高中数列知识可以算出来是 O ( n ) O(n) O(n) 的)

839. 模拟堆(手写堆 plus)

https://www.acwing.com/problem/content/841/

在这里插入图片描述

TODO

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
int h[N], siz = 0;
int ph[N], hp[N];   // k/idx与x(堆数组的下标)的双向对应,ph是k->x,hp是x->k的。

void heap_swap(int x, int y) {
    // 这三个顺序应该是无所谓的,因为反正都是交换2对2的
    swap(hp[x], hp[y]);
    swap(ph[hp[x]], ph[hp[y]]);
    swap(h[x], h[y]);
}

void up(int x) {
    while (x / 2 && h[x / 2] > h[x]) {
        heap_swap(x / 2, x);
        x >>= 1;
    }
}

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

int main()
{
    scanf("%d", &n);
    string opt;
    int k, x, idx = 0;
    while (n--) {
        cin >> opt;
        if (opt == "I") {
            scanf("%d", &x);
            siz++;
            idx++;
            ph[idx] = siz;
            hp[siz] = idx;
            h[siz] = x;
            up(siz);
        } else if (opt == "PM") {
            printf("%d\n", h[1]);
        } else if (opt == "DM") {
            heap_swap(1, siz);
            siz--;
            down(1);
        } else if (opt == "D") {
            scanf("%d", &k);
            k = ph[k];
            heap_swap(k, siz);
            siz--;
            up(k);
            down(k);
        } else {
            scanf("%d%d", &k, &x);
            k = ph[k];
            h[k] = x;
            down(k);
            up(k);
        }
    }
    return 0;
}

哈希表

https://oi-wiki.org/ds/hash/ (写的很好)

在这里插入图片描述

例题列表

840. 模拟散列表

https://www.acwing.com/problem/content/842/

在这里插入图片描述

开放寻址法 和 拉链法 都很不错。

解法1——开放寻址法

只需要开一个一维数组,不需要再开链表。(经验上,数组要开到数据范围的2~3倍)

import java.util.Arrays;
import java.util.Scanner;

// 开放寻址法
public class Main {
    final static int N = 200003, inf = 0x3f3f3f3f;

    static int[] h = new int[N];
    static {
        Arrays.fill(h, inf);    // 设置inf表示这个位置没有元素
    }

    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(), N = 100001;
        while (n-- != 0) {
            char op = scanner.next().charAt(0);
            int x = scanner.nextInt();
            if (op == 'I') h[find(x)] = x;
            else System.out.println(h[find(x)] != inf? "Yes": "No");
        }
    }

    // 找到 x 在的位置
    static int find(int x) {
        int t = (x % N + N) % N;    // 哈希函数映射
        while (h[t] != inf && h[t] != x) t = (t + 1) % N;       // 这个位置有元素而且不是 x
        return t;
    }
}

开放寻址法的核心是 find(x) 方法。即找到元素 x 所在的位置。

解法2——拉链法

下面的写法相当于,用数组模拟哈希表。
对于哈希表数组的每个位置,使用数组模拟了一个单链表。

在插入时使用头插法。

import java.util.Arrays;
import java.util.Scanner;

// 拉链法
public class Main {
    final static int N = 100003;     // 大于100000的第一个质数(尽量取和2的幂次比较远的质数)

    // h存储当前位置的链表头,e存储各个idx对应的元素x,ne存储各个idx的指针指向的idx
    static int[] h = new int[N], e = new int[N], ne = new int[N];
    static int idx;
    static {
        Arrays.fill(h, -1);
    }

    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(), N = 100001;
        while (n-- != 0) {
            char op = scanner.next().charAt(0);
            int x = scanner.nextInt();
            if (op == 'I') insert(x);
            else System.out.println(find(x)? "Yes": "No");
        }
    }

    static void insert(int x) {
        int k = (x % N + N) % N;        // x 可能是负数,所以 + N
        e[idx] = x;                     // 把数据 x 存储 e 数组中
        ne[idx] = h[k];                 // 把 idx 的 next 指针指向当前的插槽 h[k]对应的idx(头插法)
        h[k] = idx++;                   // 把插槽 h[k] 的头节点换成 idx
    }

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

insert(x) 中使用头插法。
find(x) 中枚举当前插槽上的链表中的所有元素,看是否有元素 x。

841. 字符串哈希(字符串前缀哈希法)(P进制表示字符串)

https://www.acwing.com/problem/content/843/

在这里插入图片描述

解法思路

预处理出所有前缀的哈希
在这里插入图片描述

利用前缀哈希可以求得所有子串的哈希。

注意点
  1. 不能把某个数字映射成 0。(比如 a 映射成 0, 那 aa 还是 0)。

  2. 当 P = 131 或 13331 ,Q 取 2^64 时,一般情况下(99.9%的情况下)可以假定不会发生冲突。(P 是进制,Q 是取模数)

代码
import java.util.Scanner;

public class Main {
    final static int N = 100010, P = 131;       // 把字符串当成P进制的数字
    static long[] h = new long[N], p = new long[N];     // 溢出就相当于取模了(ULL)

    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(), m = scanner.nextInt();
        String s = scanner.next();

        // 左边是高位,右边是低位
        p[0] = 1;
        for (int i = 1; i <= n; ++i) {
            h[i] = h[i - 1] * P + s.charAt(i - 1);
            p[i] = p[i - 1] * P;
        }

        while (m-- != 0) {
            int l1 = scanner.nextInt(), r1 = scanner.nextInt(), l2 = scanner.nextInt(), r2 = scanner.nextInt();
            System.out.println(get(l1, r1) == get(l2, r2)? "Yes": "No");
        }
    }

    // 计算出从 l 到 r 的哈希值
    static long get(int l, int r) {
        return h[r] - h[l - 1] * p[r - l + 1];      // 公式
    }
}

关于h[r] - h[l - 1] * p[r - l + 1]

在这里插入图片描述
h [ l ] h[l] h[l] 中的 0 ~ l 相比于 h [ r ] h[r] h[r] 中的 0 ~ l 少乘了 p r − l + 1 p^{r - l + 1} prl+1 ,所以相乘之后再减去就相当于去掉了 从 0 ~ r 中的 0 ~ l 这一段。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wei *

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值