Hash表与Hashset

散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。

实现哈希表我们可以采用两种方法:

数组+链表(java常用)
数组+二叉树(红黑树,当冲突达到一定数量级后,链表转为红黑树——java改良)

Hash函数

比如说你在微信好友中找人,张三,如果一个一个去找那太慢了,如果直接定位到z这个首字母去找就快的多,不用挨个去查找。

常用的哈希函数设计方法——>取模(模数为素数)

哈希函数的设计原则:计算高效简便“键”通过哈希函数得到的“索引”分布越均匀越好。

键值对
哈希表本质上是个数组,难道就跟数组的基本使用那样,存个数值,然后通过下表读取之类的嘛?当然不是,对于哈希表,它经常存放的是一些键值对的数据,什么是键值对呢?就是我们经常说的key-value,简单点说就是一个值对应另外一个值,比如a对应b,那么a就是key,b是value,哈希表存放的就是这样的键值对,在哈希表中是通过哈希函数将一个值映射到另外一个值的,所以在哈希表中,a映射到b,a就叫做键值,而b呢?就叫做a的哈希值,也就是hash值,这也类似于我们所学的函数,函数是一对一的存在,函数之间的映射关系,y=f(x) 相当于 x=>key ,y=>value;一个y可以对应多个x的值,y=x^2;一个value也可以对应多个key,但是一个key只能对应一个value.

Hash冲突

不管hash函数设计的如何巧妙,总会有特殊的key导致hash冲突,特别是对动态查找表来说。解决Hash冲突的方法有很多,这里介绍以下拉链法。java中,Hashset,Hashmap就是使用了这种方法,如图:

例如 存储的key的hash值为10

Value是张三 通过取模获得数组索引 10%5=0

存储的key的hash值通过某种算取得的数组索引为0

key的hash值为15

Value是李四 通过取模 过取模获得数组索引15%5=0

这时候就出现冲突了,出现冲突后我们就通过拉链法解决出现的hash冲突,张三这时候的数组索引为0,李四的索引也为0。不能张三先到,张三存进去不去管李四,那么就需要引入指针,这个指针指向数组外的另外一个位置,将李四安排在这里,然后张三next指针就指向李四的这个位置,也就是保存的这个位置的内存地址,如果还有冲突,继续加入,然后李四的next指向它,这样就形成了一个链表。

哈希表如何读取数据:

查找的话以上述为例子,首先我们根据key=15来进行查找,我们需要通过某种算法来获取这个数组中的索引,这个索引位0,我们很快的定位到这个数组为0对应的值,我们查到一个张三,我们要找的不是张三而是李四。不对啊是吧,然后我们通过key=10指向下一个next()指针指定位置,继续找,这是我们找到了李四。

HashSet

HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。 HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除 性能。

HashSet 具有以下特点: 不能保证元素的排列顺序 HashSet 不是线程安全的 集合元素可以是 null HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法比较相 等,并且两个对象的 equals() 方法返回值也相等。

对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。

向HashSet中添加元素的过程:

我们向hashset中添加元素a,首先调用元素a所在类的hashcode方法,计算元素a的hash值
* 此hash值接着通过某种计算算出在hashset底层数组中的存位置,即为索引位
* 判断数组此位置上是否已经有元素,如果此位置没有其他元素,则元素添加成功 情况1
* 如果此位置上有其他元素b或已经存在以链表形式存在的其他元素,首先比较元素a和元素b的hash值。
* 如何hash值不同 则元素添加成功 情况2
* 如果相同进而需要通过调用元素a所在类的equals()方法
* 如果equals返回true 元素添加失败每一个元素挨着比较
* 如果equals返回false 元素添加成功 情况三
*对于添加成功的情况2和情况三而言 与已经存在索引位置上的数据以链表的形式存储的
* jdk7 元素a放到数组中 指向原来的元素,来一个变成第一个,来一个变成第一个
* jdk8 原来元素在数组中,指向元素a 七上八下 拉链表向下放
* hash底层是数组加链表的形式
* 如果不算hashcode这个方法去算hash值,会默认使用Object类中的hashcode去分配这个值
* 而object分配这个hash值是随机的一个数,随机的一个数可能就会出现问题,可能出现重复的hash值
* 重写hashcode()方法之后这个hash值是一个固定的数值,存储方式如上
*1.set接口中没有额外定义新的方法,使用的都是collectio中声名过得方法
* 2.要求向set中添加的数据,其实所在的类一定要重写hashcode()和equals()方法
* 即相等的对象,相等的对象必须具有相等的散列码

上代码:

定义一个User类

package Set;

public class User {
    private String name;
    private int age;

    public User() {
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        System.out.println("equals");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

        if (age != user.age) return false;
        return name != null ? name.equals(user.name) : user.name == null;
    }

    @Override
    public int hashCode() {//
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;//膨胀法减少概率
        return result;
    }
}

setTest:测试hashset

package Set;

import jihe.Person;
import org.junit.Test;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 *
 * set接口 无序的不可重复的
 * 理解这个无序和不可重复
 *
 * Hashset:set接口的主要实现类 线程不安全的
    * linkedhashset :是hashset的子类,在hashset的基础上加上链条,
 * 使得遍历其内部数据时,按照添加的顺序去遍历
 * treeset:存储结构是一个树的结构 红黑树的方式存储的
 * 要求存入treeset的数据必须是一个类建立的
 * 可以按照添加队形的指定属性,进行排序
 * set没有添加新的方法用的都是collection中的方法
 */
public class SetTest {
    /**
     * 无序性 不可重复性
     *hashset存储的数据在底层数据中并非按照数组索引的顺序添加
     * 而是根据hash值来添加的
     * 不可重复性 拿equals判断的 保证添加的元素按照equals()判断时不能返回true,即相同元素只能添加一个
     * hashset的存储原理过程
     * 二:add添加元素的过程 以hashset为例
     * set中添加的数据 不可重复性 每一个元素都与所有的元素equals一下 1000个数据这样比这效率太低
     * 考虑hash值 底层是一个数组 数组长度16
     * 第一个元素 算一下hash值 13462 数值决定了在数组中存储的位置 通过某种算法得到这个数组中的位置
     * 例如 取模求hash值 不可能是零 添加第二个元素,算hash值,找到在数组中的位置,看有无元素,
     * 如果下一个要添加的元素的hash值一样,也不是说hash值一样而是通过某种算法找到在数组中的位置一样,
     * 要以链表的形式存放,拉链的方式 jdk7 七上八下jdk8这个比较经典hash值不一样再说一次。在添加一个元素
     * hash值一样,那就的equals 两个hash值一样的元素进行比较,如果返回true真一样,那就不能添加,false添加进来以链表的形式添加
     * 拉链法比较 效率比较高
     *
     * 我们向hashset中添加元素a,首先调用元素a所在类的hashcode方法,计算元素a的hash值
     * 此hash值接着通过某种计算算出在hashset底层数组中的cun位置,即为索引位
     * 判断数组此位置上是否已经有元素,如果此位置没有其他元素,则元素添加成功 情况1
     * 如果此位置上有其他元素b或已经存在以链表形式存在的其他元素,首先比较元素a和元素b的hash值。
     * 如何hash值不同 则元素添加成功 情况2
     * 如果相同进而需要通过调用元素a所在类的equals()方法
     * 如果equals返回true 元素添加失败每一个元素挨着比较
     * 如果equals返回false 元素添加成功 情况三
     *对于添加成功的情况2和情况三而言 与已经存在索引位置上的数据以链表的形式存储的
     * jdk7 元素a放到数组中 指向原来的元素,来一个变成第一个,来一个变成第一个
     * jdk8 原来元素在数组中,指向元素a 七上八下 拉链式向下放
     * hash底层是数组加链表的形式
     * 如果不算hashcode这个方法去算hash值,会默认使用Object类中的hashcode去分配这个值
     * 而object分配这个hash值是随机的一个数,随机的一个数可能就会出现问题,可能出现重复的hash值
     * 重写hashcode()方法之后这个hash值是一个固定的数值,存储方式如上
     *1.set接口中没有额外定义新的方法,使用的都是collectio中声名过得方法
     * 2.要求向set中添加的数据,其实所在的类一定要重写hashcode()和equals()方法
     * 即相等的对象,相等的对象必须具有相等的散列码
     * 重写一个方法的小技巧 对选哪个中用作equals()方法比较field,都应该用来计算
     *
     */
    @Test
    public void test(){
        Set set=new HashSet();
        set.add(456);
        set.add(123);
        set.add("aa");
        set.add("cc");
        set.add("cc");//set接口中有int hashCode();重写
        set.add(new User("tom",12));
        set.add(new User("tom",12));
        set.add(123);
        //无序性不等于随机性 根据hash值来判断的相较于list有序是无序的
        //hashset的底层是数组存的通过hash值在数组中添加存储位置

        Iterator iterator = set.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }


    }
}

程序结果图:

hashcode存储的方式:

很不错的文章:

来吧!一文彻底搞定哈希表! 

来吧!一文彻底搞定哈希表! - 知乎

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值