哈希表的详细介绍及其实现~

哈希冲突:不同的关键字通过相同的哈希函数,有可能找到相同的位置

如何避免冲突:冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率

1、设计哈希函数来比避免冲突

(1)哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0m-1 之间,就是散列表有多少格子就用多少格子的意思

(2)哈希函数计算出来的地址能均匀分布在整个空间

(3)哈希函数应该比较简单
2、调节负载因子来比避免冲突 (重点)

常见哈希函数:

1. 直接定制法 --(常用 )
取关键字的某个线性函数为散列地址: Hash Key = A*Key + B 优点:简单、均匀
缺点:需要 事先知道关键字的分布情况         使用场景: 适合查找比较小且连续的情况
思路:因为知道了字符串中都是小写字母,就是 事先知道关键字的分布情况 
           小写字母还是连续的并且范围较小,所以可以用哈希的方法
    public int firstUnique(String s){
        if(s==null){
            return -1;
        }
        int[] array=new int[26];
        for (int i = 0; i < s.length(); i++) {//第一次遍历str,把里面的元素都放入array里
            char ch=s.charAt(i);
            array[ch-97]++;
        }
        for (int i = 0; i <s.length() ; i++) {//第二次遍历str,每遍历一个元素上array里检查一下,若为1,则返回
            char ch=s.charAt(i);
            if(array[ch-97]==1){
                return i;
            }
        }
        return -1;
    }

2. 除留余数法--(常用)

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

负载因子=存储散列表元素的个数/散列表的长度(负载因子=0.75,一旦大于0.75需要调节)

负载因子和冲突的关系:

 可知:负载因子越小,冲突率越低

要想降低冲突,需要将负载因子降低,由于存储散列表元素的个数在慢慢增加,所以要想负载因子降低只能提高散列表的长度,因此要想降低冲突,需要提高散列表的长度


由于冲突是避免不了的,那真正发生冲突又该怎么解决呢?

1、闭散列(开放地址法):就是去寻找下一个为空的位置

   (1)线性探测:从冲突的位置开始向后找找到第一个为空的位置将元素放进去

 

缺点:会把冲突的元素都放到一起了,这样就不能随便删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。

 (2)二次探测:

 缺点:空间利用率较低,只能装一半的元素,如果超出必须考虑增容。

2、(重点)开散列(哈希桶HashBuck/链地址法/开链法)

底层就是:数组(数组里面存的是Node)+链表

有人会担心,链表长度万一很长,遍历的时候时间复杂度不就会变成O(N)嘛? 

这点请放心,因为链表的长度不会很长,控制在常数范围内,因为需要控制负载因子。

从JDK1.8开始,当链表长度超过8,并且数组的长度超过64时,这个链表就会变成红黑树

红黑树查找非常高效,不用担心时间复杂度问题。

通常认为哈希表的插入/删除/查找时间复杂度是 O(1)


 1、简单实现哈希表~

public class HashBuck {
    static class Node{
        public int key;
        public int val;
        public Node next;
        public Node(int key,int val){
            this.key=key;
            this.val=val;
        }
    }
    public Node[] array;
    public int usedSize;
 
    public static final double DEFAULT_LOAD_FACTOR=0.75;//规定负载因子,添加数据时检查一下,防止冲突
 
    public HashBuck(){
        this.array=new Node[10];
    }
    //添加元素
    public void put(int key,int val){
        //1、找到key所对应在array中的位置
        int index=key% array.length;
        //2、遍历index位置的链表,看key是否重复,重复则替换,若key不在则插入链表中
        //2.1重复的时候
        Node cur=array[index];
        while(cur!=null){
            if(cur.key==key){
                cur.val=val;//key重复,替换val
                return;
            }
            cur=cur.next;
        }
        //2.2不重复的时候:头插法插入
        Node node=new Node(key, val);
        node.next=array[index];//绑定尾部
        array[index]=node;//绑定头部
        usedSize++;
        //3、检查负载因子,超出负载因子的值,则需要扩大array的容量来减小负载因子的值,防止冲突
        if(loadFactor()>=DEFAULT_LOAD_FACTOR){
            resize();//扩容array的方法
        }
    }
    //扩容array需要注意:此时由于array扩容变长 需要将原来没扩容之前的节点都拿出来,
    // 然后重新哈希每个节点找在扩容后数组中的位置
    private void resize(){
        Node[]newArray=new Node[2*array.length];
        for (int i = 0; i < array.length; i++) {//遍历原来的数组,将里面的节点全都拿出来
            Node cur=array[i];//拿里面的节点
            while(cur!=null){//有可能array里的每个位置里都不止一个节点,而是一个链表
                int index=cur.key%newArray.length;//找到节点在扩容后数组中的位置
                Node curNext=cur.next;
                cur.next=newArray[index];//绑定后面
                newArray[index]=cur;//绑定前面
                cur=curNext;
            }
        }
        array=newArray;//让扩容后的数组代替原来的数组
    }
 
    private double loadFactor(){//求当前的负载因子值
        return 1.0*usedSize/array.length;//负载因子=当前数据长度/数组长度
    }
 
    //根据key获取val值
    public int get(int key){
        //1、找key的位置(大概找到key所在array数组的哪个下标里)
        int index=key%array.length;
        //2、遍历这个下标的链表具体找到key
        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(2,2);
        hashBuck.put(4,4);
        hashBuck.put(6,6);
        hashBuck.put(13,13);
        hashBuck.put(12,12);
        hashBuck.put(11,11);
        hashBuck.put(8,8);
        System.out.println(hashBuck.get(11));
    }
}

关于hashCode函数:可以使一个引用变成一个合法的整数

//假设接下来的key是一个person,身份证号是一样的,我们认为是同一个人
//又因为,要把person1和person2放到散列表中,需要找到index,所以需要调用person.hashcode(),找到下标
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");
          System.out.println(person1.hashCode());
          System.out.println(person2.hashCode());
}

 因为是同一个人,所以需要生成的值是一样的,这样才能存到同一位置,可是按照上面显然是不能达到预期的,因此还需要加东西,让生成的hashCode值相同

class Person{
     public String ID;
     public Person(String ID){
          this.ID=ID;
     }
     @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);
     }
     @Override
     public String toString() {  
          return "Person{" +
                  "ID='" + ID + '\'' +
                  '}';
     }
}

public class HashBuck2 {
     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());
     }
}

 重写了hashCode和equals后发现生成的值相等了,满足了预期

总结:因为HashMap底层是一个哈希表,所以在使用HashMap时如果map里面存的key值是自定义类型,一定要重写HashCode

像这种情况就要重写。 否则就会出现本意是一个人的两个人,最终被误认为不是一个人,被放到了不同位置

2、改进实现哈希表

import java.util.Objects;
//改进哈希表(由于哈希表里面的值不一定是整数)
class Person{
    public String ID;
    public Person(String ID){
        this.ID=ID;
    }
 
    @Override
    public String toString() {
        return "Person{" +
                "ID='" + ID + '\'' +
                '}';
    }
    //如果哈希表里存的树自定义对象时,在进行插入和查找时要重写equals()和hashCode()两个方法
    @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);
    }
}
public class HashBuck2 <K,V>{
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;
public void put(K key,V val){
    int hash=key.hashCode();//通过hashCode()来将一个字符串转换成对应值,
    // 进而通过生成的值来找其在array数组中的位置
    int index=hash%array.length;
    Node<K,V> cur=array[index];
    while(cur!=null){
        if(cur.key.equals(key)){
            cur.val=val;
            return;//此时相同的值再次插入,替换完val值后return
        }
        cur=cur.next;
    }
    Node<K,V> node=new Node<>(key,val);
    node.next=array[index];
    array[index]=node;
    this.usedSize++;
}
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,"ly");
        System.out.println(hashBuck2.get(person2));
    }
 
    public static void main1(String[] args) {
        Person person1=new Person("123");
        Person person2=new Person("123");
        System.out.println(person1.hashCode());
        System.out.println(person2.hashCode());
    }
 
 
}

总结:
1. HashMap HashSet java 利用哈希表实现的 Map Set
2. java 中使用的是哈希桶方式解决冲突
3. java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
4. java 计算哈希值 实际上是 调用的类的 hashCode 方法 进行 key 的相等性比较 是调用 key equals方 法
所以 如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值必须覆写 hashCode 和 equals 方法 ,而且要做到 equals 相等的对象, hashCode 一定是一致的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值