【数据结构】HashSet的底层数据结构

简介: 【数据结构】HashSet的底层数据结构

一、 HashSet 集合的底层数据结构

  • HashSet :无序、不重复、无索引
  • HashSet 底层是采用哈希表存储数据的,哈希表是一种对于增删改查数据性能都较好的结构
  • 哈希表在JDK8之前是由数组+链表组成的,在JDK8之后是由数组+链表+红黑树组成的
  • 在哈希表中,最重要的是哈希值,哈希值就是对象的整数表现形式,HashSet 在存数据的时候,会根据数组长度和哈希值计算出要存入的位置,哈希值是根据hashCode()方法计算出来的int型的整数,hashCode()方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算,一般情况下,自定义的对象都要重写hashCode()方法,利用对象内部的属性值计算哈希值。
int index = (数组长度 - 1) & 哈希值;
  • 对象的哈希值特点:
  • 如果没有重写hashCode()方法,同一个类创建的不同对象计算出的哈希值是不同的
public class Student {
    private String name;
    private int age;
    public Student() {
    }
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }
    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }
    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }
    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }
    public String toString() {
        return "Student{name = " + name + ", age = " + age + "}";
    }
}
public static void main(String[] args) {
        //创建对象
        //没有重写hashCode方法,计算出的哈希值是不同的
        Student s1 = new Student();
        Student s2 = new Student();
        System.out.println(s1.hashCode());//460141958
        System.out.println(s2.hashCode());//1163157884
    }

  • 如果已经重写hashcode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的
public class Student {
    private String name;
    private int age;
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    public Student() {
    }
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }
    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }
    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }
    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }
    public String toString() {
        return "Student{name = " + name + ", age = " + age + "}";
    }
}
public static void main(String[] args) {
        //创建对象
        //如果已经重写hashcode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的
        Student s1 = new Student();
        Student s2 = new Student();
        System.out.println(s1.hashCode());//961
        System.out.println(s2.hashCode());//961
    }

  • 在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样(哈希碰撞)
public static void main(String[] args) {
        //在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样。(哈希碰撞)
        System.out.println("abc".hashCode());//96354
        System.out.println("acD".hashCode());//96354
    }

二、 HashSet 添加元素的过程

HashSet在JDK8以后的底层原理:

  • 创建一个默认长度为16,默认加载因子为0.75的数组,数组名为table
  • 根据元素的哈希值跟数组长度计算处应存入的位置
int index = (数组长度 - 1) & 哈希值;
  • 判断当前位置是否为null,如果是null,则直接存入
  • 如果当前位置不是null,表示有元素,则调用equals()方法与当前位置的属性进行比较
  • 如果相同,则舍弃不存
  • 如果不同,则存入数组,形成链表
  • JDK8以前,新元素存入数组,老元素挂在新元素下面形成链表
  • JDK8之后,新元素挂在老元素下面形成链表
  • 当链表长度大于8且数组长度大于等于64时,当前链表会自动转成红黑树
  • 如果集合中存储的是自定义对象,必须重写 hashCode 和 equals 方法

三、 HashSet 为什么存和取的顺序不一样

HashSet 在遍历的时候是从数组的0索引开始遍历的,每个索引下都要遍历该索引下对应的链表,当遍历到一个索引,这个索引的值为空时,会跳过,遍历下一个索引,该索引下对应有链表时,就会遍历这个链表,若是红黑树,也会遍历这个红黑树,按这个原则遍历数组,因为某个索引下对应的元素不一定就是存入时的顺序,所以HashSet 在存和取时的顺序也不一定相同。



四、 HashSet 为什么没有索引

HashSet 是由数组+链表+红黑树组成的,数组是有索引的,但是存在HashSet 中的元素是通过链表或红黑树的形式挂在数组的每个索引下的,也就是每个索引可能对应多个元素,所以HashSet 不能由索引找到对应的元素。



五、 HashSet 的去重机制

HashSet 是通过HashCode计算出每个元素应该存放的位置,,然后通过equals方法去比较对象内部的属性值是否一致,保证不会出现重复的元素。

相关文章
|
存储 算法 Java
Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性
Java Set因其“无重复”特性在集合框架中独树一帜。本文解析了Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性,并提供了最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的hashCode()与equals()方法。
287 4
|
Java
【JAVA数据结构】哈希表-HashSet and HashMap(二)
JAVA数据结构 & 哈希表 -HashSet and HashMap
186 0
|
Java
【JAVA数据结构】哈希表-HashSet and HashMap
JAVA数据结构 & 哈希表 -HashSet and HashMap
144 0
|
存储 Java 程序员
面试官:HashSet 的实现原理是怎样的?底层是什么数据结构?被问到了。。
面试官:HashSet 的实现原理是怎样的?底层是什么数据结构?被问到了。。
808 0
面试官:HashSet 的实现原理是怎样的?底层是什么数据结构?被问到了。。
|
存储 安全 Java
面试宝典:数据结构-HashSet
面试宝典:数据结构-HashSet
141 0
面试宝典:数据结构-HashSet
|
算法 Java
《恋上数据结构第1季》集合 ListSet、TreeSet、HashSet
《恋上数据结构第1季》集合 ListSet、TreeSet、HashSet
171 0
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
274 59
|
5月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
107 0
栈区的非法访问导致的死循环(x64)
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
10月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
475 77

热门文章

最新文章