哈希表【数据结构】

哈希表

概念

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:

插入元素

根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放

搜索元素

对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

以上方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)

结构

在这里插入图片描述

冲突

概念

哈希冲突:两个不同关键字key通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突哈希碰撞

冲突-避免

哈希函数设计
常见哈希函数
  1. 直接定制法(常用)
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
    优点:简单、均匀 缺点:需要事先知道关键字的分布情况
    使用场景:适合查找比较小且连续的情况
调节负载因子
负载因子定义

负载因子和冲突率的关系

在这里插入图片描述
由图不难发现,只有降低负载因子,才能降低冲突率。那么想要降低冲突率,只能增加散列表长度。

冲突解决

冲突-解决-闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个”空位置中去。

线性探测

从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

过程
  1. 通过哈希函数获取待插入元素在哈希表中的位置
  2. 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。

如下,44与4发生哈希冲突,则向后找到哈希表中下一个空位置为下标8处。
在这里插入图片描述

缺点

通过以上概念及过程可知,线性探测法存在以下不足:

  1. 不好删除
    采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若**直接删除元素会影响其他元素的搜索**。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
  2. 冲突元素分布紧密。由于每次冲突发生都是向后 找空位置(遇到后就存进去),此举会使得 尽量冲突的元素都放在了一起
二次探测
概念

为了避免 线性探测冲突数据堆积的问题,二次探测提出找下一个空位置的方法

在这里插入图片描述
其中:H0 是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

  • 表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。
  • 在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
缺点

空间利用率低

冲突-解决-开散列/哈希桶

数组+链表+红黑树

概念

开散列又叫做链地址法,首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来冲突的元素),各链表的头结点存储在哈希表中。

结构

在这里插入图片描述

代码实现哈希桶
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 () {
        array  = new Node[8];
    }

    /**
     *
     * @param key
     * @param val
     * @return 代表你插入的元素的val
     */
    public boolean put(int key,int val) {

        Node node = new Node(key,val);
        //1、位置
        int index = key % array.length;

        //2、遍历这个下标的链表
        Node cur = array[index];//就是一个链表的头节点
        while (cur != null) {
            if(cur.key == key) {
                cur.val = val;//更新val值
                return false;
            }
            cur = cur.next;
        }
        //3、遍历完成了当前下标的链表,开始进行插入
        node.next = array[index];
        array[index] = node;
        this.usedSize++;
        //4、存放元素之后,判断当前哈希桶当中的负载因子 是否超过了默认的负载因子
        if(loadFactor() >= DEFAULT_LOAD_FACTOR) {
            //5、扩容
            resize();
        }
        return true;
    }

    /**
     * 扩容的同时 要进行重新哈希
     */
    private void resize() {
        //1、重新申请2倍的数组
        Node[] tmp = new Node[array.length*2];
        //2、遍历原来的数组,把每个下标的链表的节点,都重新进行哈希
        for (int i = 0; i < array.length; i++) {
            Node cur = array[i];
            while (cur != null) {
                int newIndex = cur.key % tmp.length;//新的数组的下标
                Node curNext = cur.next;//先要记录下来下一个
                cur.next = tmp[newIndex];
                tmp[newIndex] = cur;
                cur = curNext;//这里注意
            }
        }
        array = tmp;
    }

    public int get(int key) {
        //1、确定位置
        int index = key % array.length;
        Node cur = array[index];
        while (cur != null) {
            if(cur.key == key) {
                return cur.val;
            }
            cur = cur.next;
        }
        return -1;
    }

    /**
     * 计算当前哈希桶当中的负载因子
     * @return
     */
    private double loadFactor() {
        return this.usedSize*1.0 / this.array.length;
    }

}
public class TestDemo {
    public static void main2(String[] args) {
        HashBuck hashBuck = new HashBuck();
        hashBuck.put(1,1);
        hashBuck.put(2,2);
        hashBuck.put(3,3);
        hashBuck.put(6,6);
        hashBuck.put(14,14);// 14
        hashBuck.put(24,44);// 8
        System.out.println("hello");
    }
}

注意

哈希表扩容需注意:需要进行重新哈希!!!

如果哈希桶中存放引用类型

重写hashCode()和equals()方法

class Person {
    public String id;

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

    @Override
    public String toString() {
        return "Person{" +
                "id='" + id + '\'' +
                '}';
    }

    /**
     * 在下标底下 找哪个节点的key和我是一样的
     * @param o
     * @return
     */
    @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);
    }

    /**
     * 找节点的小标的
     * @return
     */
    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}
public class TestDemo {


    public static void main(String[] args) {
        HashBuck2<Person,String> map = new HashBuck2<>();
        Person person1 = new Person("123456");
        Person person2 = new Person("123456");
        map.put(person1,"xyy");

        System.out.println(map.get(person2));//xyy
    }

    public static void main3(String[] args) {
        HashMap<Person,String> map = new HashMap<>();
        Person person1 = new Person("123456");
        Person person2 = new Person("123456");

        System.out.println(person1.hashCode());//找位置
        System.out.println(person2.hashCode());

        map.put(person1,"xyy");

        System.out.println(map.get(person2));
    }
}

HashMap 第一次put元素时,分配内存。

hashcode 和 equals

1、hashcode一样equals一定一样吗?不一定
2、equals一样hashcode一定一样吗?一定

HashCode简介 ——参考HashCode()和equals()的区别

hashCode()方法的作用是获取哈希码,返回的是一个int整数

Object类中的hashCode()方法定义如下

public native int hashCode();

哈希码的作用是确定对象在哈希表的索引下标。比如HashSet和HashMap就是使用了hashCode方法确定索引下标。如果两个对象返回的hashCode相同,就被称为“哈希冲突”。

equals简介

equals()方法的作用很简单,就是判断两个对象是否相等,equals()方法是定义在Object类中,而所有的类的父类都是Object,所以如果不重写equals方法则会调用Object类的equals方法。

Object类中的equals()方法定义如下

public boolean equals(Object obj) {    
	return (this == obj);
}

在equals()方法中的==,那么在Java中有什么含义呢,

我们都知道在Java中分为基本数据类型和引用数据类型。那么==在这两个类型中作用是不一样的。

基本数据类型:比较的是 两边值是否相等 -》==
引用数据类型: 比较的是 两边内存地址是否相等-》equals()

问题

1.如果new HashMap(19),bucket数组多大? 32,数组长度一定要是接近2的n次幂
2.HashMap什么时候开辟bucket数组占用内存? 默认的构造方法,不带参数的第一次put的时候,大小为16
3.hashMap何时扩容? 超过了负载因子
4.当两个对象的hashcode相同会发生什么? 碰撞(哈希冲突)
5.如果两个键的hashcode相同,你如何获取值对象? 遍历与hashCode值相等时相连的链表,直到相等(通过equals()判断)或者null
6.你了解重新调整HashMap大小存在什么问题吗? 必须重新哈希

面试题:HashMap和concurrentHashMap的区别

看到其他博主一篇较好的总结:HashMap和concurrentHashMap的区别
之后自己进一步学习后,会将此部分整理更新。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值