掌握及手把手实现哈希表

概念

在一组数据当中,想要找到关键字,最差得 O(N) 的时间复杂度。如果要在二叉搜索树当中找的话,最好也是:log 以2为底 N。最差是 n 。而 哈希表 就可以不经过任何比较就拿到数据。存的时候按照某个方法去存,拿的时候也是按照某个方法去拿。哈希表也叫散列表。时间复杂度是 O(1)。

哈希函数、冲突

插入、查找元素

根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。查找的时候,也是根据放入的方法进行查找。
例如:数据集合{1,7,6,4,5,9};哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。放入的结果就是这样:
在这里插入图片描述

如果要放入的元素在通过哈希函数之后,发现那个位置已经有了元素,就说明产生冲突了。

常见的哈希函数

直接定制法

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关
键字的分布情况 使用场景:适合查找比较小且连续的情况。

除留余数法

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:
Hash(key) = key% p(p<=m),将关键码转换成哈希地址。

平方取中法

假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对
它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知道关键字的分
布,而位数又不是很大的情况。

折叠法

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。

哈希函数和导致冲突的原因

不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。因为哈希数组的长度是小于实际数组长度的,我们只能尽力去避免冲突

  1. 在存储到哈希表当中的时候,要通过哈希函数来确定存的位置,哈希函数可以由我们自己来设计。
  2. 不同的的关键字,通过相同的哈希函数,有可能找到相同的位置。此时的情况:哈希冲突/哈希碰撞。
  3. 如果是负数的话,把它加上某个值,然后变为正数。

避免冲突

哈希表底层的数组长度,往往是小于关键字个数的。所以就应该尽力降低冲突。所以每个位置都可以存好几个数据,所以每个位置就是链表了。避免冲突的方法如下:

  1. 哈希函数的设计:哈希函数的定义域,必须包含需要存储的全部码,比如说值最大到10,那么哈希函数必须是 0-9 之间。只要能满足线性函数的散列地址就好了。
  2. 调节负载因子:负载因子就是存入散列表元素的个数/散列表的长度。一般的哈希表的负载因子是 0.75 负载因子越大,冲突率越大,负载因子想变小的话,就应该调整哈希表的长度了。

负载因子

负载因子和冲突了有关,负载因子越大,冲突率越大。负载因子和冲突率的关系如下图:
在这里插入图片描述
因为负载因子是非常重要的因素,所以应该严格限制在 0.7 - 0.8 在 Java 系统当中限制了负载因子是 0.75 。所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。因为哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。

解决冲突

解决冲突的方法有两种:

闭散列

闭散列,又叫开放地址法。有两种方法。

  1. 线性探测:当前位置被占领之后,向后找,找到第一个为空的位置,然后把它放进去。把冲突元素全放在一起了,但是删除就麻烦,删掉之后当前位置的冲突元素就找不到了,得做一个标志位。
  2. 二次探测:(H+i^2)%m 通过公式得到这个位置。最大局限性,装载英子,不能超过 0.5 也就是会浪费一半的空间。

开散列

开散列就是使用哈希桶的链地址法:哈希桶(HashBuck) 链地址法。底层就是数组+链表的形式。因为有负载因子,所以链表的长度控制在常数范围,从 JDK1.8开始,链表长度超过 8,数组长度超过 64,这个链表长度就会变成红黑树。开散列也是 Hash Map 采用的方法

实现哈希表

Node 结点

我们使用 链表 来实现哈希表。因为一个结点里面有 key 和 value ,所以在定义结点的时候,定义 key 和 value 和 next。代码如下:

static class Node {
    public int key;
    public int val;
    public  Node next;

    public Node(int key, int val) {
        this.key = key;
        this.val = val;
    }
}

其它参数

因为我们的哈希表是用基于数组来实现的,负载因子也定义成 0.75 。

public Node[] array;
public int usedSize;

public static final double DEFAULT_LOAD_FACTOR = 0.75;

public HashBuck() {
    this.array = new Node[8];
}

负载因子

负载因子会随着数据的放入,然后变大,当负载因子大于 0.75 的时候,就需要扩容了。负载因子代码如下:

private double loadFactor(){
    return 1.0*usedSize/array.length;
}

扩容方法

当插入元素太多的时候,负载因子就会变大,负载因子变大之后就需要对数组进行扩容了。在扩容的时候要重新获取下标,然后把结点以头插或尾插的方法放入到新的下标当中。代码如下:

private void resize() {
    Node[] newArray = new Node[array.length*2];
    for (int i = 0; i < array.length; i++) {
        Node cur = array[i];
        while (cur != null) {
            int index = cur.key % newArray.length;//获取新的下标
            //把 cur 这个节点,以头插/尾插的方法,插入到新的数组对应的下标当中
            Node curNext = cur.next;
            cur.next = newArray[index];
            newArray[index] = cur;
            cur = curNext;
        }
    }
    array = newArray;
}

put 方法

在使用 put 方法的时候,要先找到 key 对应的位置,然后遍历当前位置的链表,看看也没有这个 key ,如果有的话,就更新 value 的值,如果没有就放入。代码如下:

public void put(int key, int value) {
    //先找到 key 所对应的位置
    int index = key % this.array.length;
    //遍历这个下标的链表,看看有没有这个 key ,有的话更新 value
    Node cur = array[index];
    while (cur!= null) {
        if (cur.key == key) {
            cur.val = value;//更新 value 值
            //更新完之后就不用插入了,所以 return
            return;
        }
        cur = cur.next;
    }
    //没有这个 key 元素,这里头插法
    Node node = new Node(key, value);
    node.next = array[index];
    array[index] = node;
    this.usedSize++;
    //插入元素后,检查当前散列表的负载因子
    if (loadFactor() > DEFAULT_LOAD_FACTOR) {
        //大了,说明要扩容了。如果扩容了,那么里面的所有元素,都要重新哈希到新的数组
        resize();
    }
}

get 方法

get 方法,根据 key 值拿到 value 值。主要方法就是先找到 key 对应的位置,然后遍历这个下标的链表,查看有没有对应的 key,如果有的话就返回对应的 value,如果没有就返回 -1。代码如下:

public int get(int key) {
    //找到 key 所对应的位置
    int index = key % this.array.length;
    //遍历这个下标的链表
    Node cur = array[index];
    while (cur!= null) {
        if (cur.key == key) {
            return cur.val;
        }
        cur = cur.next;
    }
    return -1;
}

测试

代码如下:

public static void main(String[] args) {
    HashBuck hashBuck = new HashBuck();
    hashBuck.put(1,1);
    hashBuck.put(12,12);
    hashBuck.put(3,3);
    hashBuck.put(6,6);
    hashBuck.put(7,7);
    hashBuck.put(2,2);
    hashBuck.put(11,11);
    hashBuck.put(8,8);
    System.out.println(hashBuck.get(11));
    System.out.println(hashBuck.get(6));
}

运行结果如下:
在这里插入图片描述

哈希表遇到引用

如果哈希表当中放的内容都是引用类型的话,就需要用到泛型了。在这里先讲一下 hashcode() 创建一个 person 类:

class Person {
    public String ID;

    public Person(String ID) {
        this.ID = ID;
    }

    @Override
    public String toString() {
        return "Person{" +
                "ID='" + ID + '\'' +
                '}';
    }
}
public static void main(String[] args) {
    Person person1 = new Person("123");
    Person person2 = new Person("123");
}

ID 表示身份证号。我们认为,如果身份证号一样的话,就表示同一个人。又因为要把 person1 和 person2 放到散列表当中,所以就需要使用 hashcode 方法。hashcode 调用完成之后,会生成一个整数。hashcode:生成一个整数 % length = index

public static void main(String[] args) {
    Person person1 = new Person("123");
    Person person2 = new Person("123");
    System.out.println(person1.hashCode());
    System.out.println(person2.hashCode());
    Map<Person,String> map = new HashMap<>();
}

但是输出之后就是这个结果:
在这里插入图片描述
因为输出不一样,所以要生成一个 hashcode ,通过重写 hashcode 方法和 equals 方法来完成。

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    return Objects.equals(ID, person.ID);
}

@Override
public int hashCode() {
    return Objects.hash(ID);
}

重写之后输出结果就一样了:
在这里插入图片描述

泛型哈希表

定义结点的时候,就定义成泛型数据。其它方法也使用泛型就可以。

定义泛型结点

代码如下:

static class Node<K,V> {
    public K key;
    public V val;
    public Node<K,V> next;
    public Node(K key, V val) {
        this.key = key;
        this.val = val;
    }
}

定义数组和使用大小

代码如下:

public Node<K,V>[] array = (Node<K, V>[]) new Node[10];
public int usedSize;

put 方法

和之前的方法没什么区别,只是把数据类型换为泛型了。不过要用 hashcode 来确定位置。代码如下:

public void put(K key, V value) {
    int hash = key.hashCode();
    int index = hash % array.length;
    Node<K,V> cur = array[index];
    while (cur!= null) {
        if (cur.key.equals(key)) {
            cur.val = value;
            return;
        }
        cur = cur.next;
    }
    Node<K,V> node = new Node<>(key, value);
    node.next = array[index];
    array[index] = node;
    this.usedSize++;
}

put 方法

代码如下:

public V get(K key) {
    int hash = key.hashCode();
    int index = hash % array.length;
    Node<K,V> cur = array[index];
    while (cur!= null) {
        if (cur.key.equals(key)) {
            return cur.val;
        }
        cur = cur.next;
    }
    return null;
}

测试

测试代码如下:

public static void main(String[] args) {
    Person person1 = new Person("123");
    Person person2 = new Person("123");
    HashBuck2<Person,String> hashBuck2 = new HashBuck2<>();
    hashBuck2.put(person1,"Lockey");
    System.out.println(hashBuck2.get(person2));
}

运行结果如下:
在这里插入图片描述

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Lockey-s

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

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

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

打赏作者

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

抵扣说明:

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

余额充值