Java哈希表

简介: Java哈希表


什么是哈希

顺序存储结构或者是链式存储结构中,元素关键码与其存储的位置之间没有明显的对应关系。因此在查找一个元素的时候, 需要和其他元素进行多次比较,顺序存储结构的查找时间复杂度为O(n),链式存储结构中,平衡树的查找时间复杂度可以达到O( )。

如果可以不经过比较,就可以获得想要搜索的元素,那么查找的时间复杂度就可以降低到O(1)。构造一种数据结构,可以让元素存储的位置和它的关键码之间能够建立映射关系,那么就可以很快的查找到该元素。

该方法就称为哈希方法散列

哈希函数

hash(key) = key % capacity

  • 其中key关键码
  • capacity为存储元素底层空间的总大小

如何使用这个哈希函数?

例如,这里有一组数据{1,7,6,4,5,9}

利用哈希函数,可以得到如下:

通过哈希函数计算,获得的hash值就为该数据存放的地址:

Hash冲突

什么是冲突

例如哈希函数:hash(key) = key % capacity进行插入一定不会出现问题吗?

如果还是用这组数据:{1,7,6,4,5,9},然后向里面插入11会如何?

如果要插入11,利用哈希函数,可以得到hash(11) = 11 % 10 = 1

这个结果和插入1 : hash(1) = 1 % 10 = 1 得到了相同的哈希地址,但是1下标这个位置已经被数据“1”所占据,那么该如何处理这种情况?

为什么会冲突

使用hash存储数据的时候,难免会有多个相同hash地址的数据,但是他们不可能存放在同一个hash地址上,这种冲突就不可避免的产生了。

我们称这种具有相同的哈希地址的数据元素为“同义词”,这种冲突现象我们称之为哈希冲突或者哈希碰撞

这种冲突是必然的,我们不可能完全避免,只能尽量做到降低冲突的概率。

如何降低冲突率

引起哈希冲突的原因可能是哈希函数设置的不合理, 哈希函数的设计应该遵循一些原则:

  • 哈希函数的定义域必须包含需要存储的全部关键码, 如果散列允许有m个地址的时候, 其值域必须在0到(m-1)之间
  • 哈希函数计算出来的地址能均匀的分布在整个空间之中, 而不是集中在一个地址
  • 哈希函数设计和计算起来应该更加的简单

常用的解决冲突的方法:

  1. 直接定制法:

去关键字的某个线性函数为散列地址

哈希函数为: Hash(key) = A* key + B;

优点: 简单, 均匀.

缺点,:需要实现知道数据的分布情况.

  1. 除留余数法:

设散列地址数位m, 去一个不大于m的数, 但是最接近或者等于m的质数 p作为除数

哈希函数为: Hash(key) = key % p

  1. 平方取中法:

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

还有开放地址法和分离链接法等等, 下面会详细讲解最常用的开放地址法.

负载因子

散列表负载因子的定义为: fac = 填入表中的元素个数 / 散列表的长度

也就是说, 负载因子fac是散列表装满程度的表示方法, 由于表长度时定值, 当填入表中的元素个数越多, 或者散列表的长度越短的时候, 表名表的装满程度就越大, 产生冲突的可能性就越大, 反之越小, 所以解决哈希冲突的办法就是尽量去降低负载因子.

开放地址法

线性探测

如果我们使用哈希函数hash(key) = key % 10, 如果现在有一个数据4 和14需要存储到散列表当中去, 那么毫无疑问的他们会产生冲突, 所以我们就把14 放在4的地址的右边一位:

这样就解决了冲突, 同理如果有个数据是44, 那么就把他放在14后面, 以此类推, 这个被称为线性探测

缺点: 如果我们要查找的话就会非常的麻烦, 如果要删除呢? 例如, 如果直接删除4 , 那么44 的查找就会很麻烦, 如果使用hash函数去查找44, 它首先是去查找4这个值, 发现这个地方没有值则会直接认为44不存在.

二次探测

为了解决线性探测带来的问题, 我们采用以下哈希函数:

= ( + ) % m

例如: 在已经插入4的情况下, 插入14, 24,44. 插入14的时候为第一次冲突, i = 1, h0 = 4, 所以hash值为 5, 以此类推.

链地址法

原理

如果每一个地址只能存放一个值的话, 那么势必会有地址冲突的种情况, 对于二次探测这种方法, 也难免会造成空间浪费的情况, 而且空间的利用度也很低.

接下来介绍的这种链地址法, 来解决上述的问题:

原理, 也就是设置一个数组, 数组里面存放着一个类, 这个类里面包含着哈希地址和一个链表的头节点, 这个链表里面存放着 等于这个数组里面类对应哈希地址的哈希值, 例如:

实现

  1. 构造一个哈希类
public class HashBack {
    // 内部类
    static class Node {
        public int key;
        public int value;
        public Node next;
 
        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }
    public Node[] arr;
    public int usedSize; 
    
    // 负载因子
    public static final double LOADFACTOR = 0.75;
}
  1. put方法
public void put(int key, int val) {
        int index = key % arr.length;
        Node cur = arr[index];
        while(cur != null) {
            if (cur.key == key) {
                cur.value = val;
                return;
            }
            cur = cur.next;
        }
        Node tem = new Node(key, val);
        tem.next = cur;
        arr[index] = tem;
        usedSize++;
 
        if (calculateLoadFactor() >= LOADFACTOR) {
            reSize();
        }
    }
  1. get方法
    public int get(int key) {
        int index = key % arr.length;
        Node cur = arr[index];
        while ( cur != null) {
            if (cur.key == key) {
                return cur.value;
            }
        }
        return -1;
    }
  1. 计算负载因子
    // 计算负载因子
    private double calculateLoadFactor() {
        return usedSize * 1.0 / arr.length;
    }
  1. reSize方法(扩容)

扩容的时需要注意将原有key重新排列如新的数组当中

    void reSize() {
        Node[] newArr = new Node[2 * arr.length];
        for(int i = 0; i < arr.length; i++) {
            Node cur = arr[i];
            while(cur != null) {
                Node curNext = cur.next;
                int index = cur.key % newArr.length; // 找到了在新的数组当中位置
                cur.next = newArr[index];
                newArr[index] = cur;
                cur = curNext;
            }
        }
        arr = newArr;
        System.out.println("扩容成功");
    }

6.总览

public class HashBack {
    static class Node {
        public int key;
        public int value;
        public Node next;
 
        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }
    public Node[] arr;
    public int usedSize;
 
    public static final double LOADFACTOR = 0.75;
 
    // 构造方法
    public HashBack() {
        arr = new Node[10];
    }
 
    // 扩容
 
    /**
     * 这地方需要对所有的因子进行重组
     * @return
     */
    void reSize() {
        Node[] newArr = new Node[2 * arr.length];
        for(int i = 0; i < arr.length; i++) {
            Node cur = arr[i];
            while(cur != null) {
                Node curNext = cur.next;
                int index = cur.key % newArr.length; // 找到了在新的数组当中位置
                cur.next = newArr[index];
                newArr[index] = cur;
                cur = curNext;
            }
        }
        arr = newArr;
        System.out.println("扩容成功");
    }
 
    // 插入
    public void put(int key, int val) {
        int index = key % arr.length;
        Node cur = arr[index];
        while(cur != null) {
            if (cur.key == key) {
                cur.value = val;
                return;
            }
            cur = cur.next;
        }
        Node tem = new Node(key, val);
        tem.next = cur;
        arr[index] = tem;
        usedSize++;
 
        if (calculateLoadFactor() >= LOADFACTOR) {
            reSize();
        }
    }
 
    // 计算负载因子
    private double calculateLoadFactor() {
        return usedSize * 1.0 / arr.length;
    }
 
    //get方法
    /**
     * 获取
     */
    public int get(int key) {
        int index = key % arr.length;
        Node cur = arr[index];
        while ( cur != null) {
            if (cur.key == key) {
                return cur.value;
            }
        }
        return -1;
    }
}

对于class类型的key参数

我们可以使用泛型来修改这个实现, 对于单个类的对象, 我们需要重写hashCode方法和equals方法.

性能分析

在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是O(1)

实际应用

oj练习:

使用

1. 只出现一次的数字

2. 复制带随机指针的链表

3. 宝石与石头

4. 坏键盘打字

5. 前K个高频单词

其中1235为leetcode, 4为牛客.


目录
相关文章
|
9月前
71.【Java.哈希表(散列表)】
71.【Java.哈希表(散列表)】
36 1
|
15天前
|
存储 Java 索引
JAVA中的哈希表实现与应用
JAVA中的哈希表实现与应用
15 1
|
1月前
|
存储 安全 Java
【亮剑】`ConcurrentHashMap`是Java中线程安全的哈希表,采用锁定分离技术提高并发性能
【4月更文挑战第30天】`ConcurrentHashMap`是Java中线程安全的哈希表,采用锁定分离技术提高并发性能。数据被分割成多个Segment,每个拥有独立锁,允许多线程并发访问不同Segment。当写操作发生时,计算键的哈希值定位Segment并获取其锁;读操作通常无需锁定。内部会根据负载动态调整Segment,减少锁竞争。虽然使用不公平锁,但Java 8及以上版本提供了公平锁选项。理解其工作原理对开发高性能并发应用至关重要。
|
8月前
|
存储 算法 Java
《代码随想录》刷题笔记——哈希表篇【java实现】
《代码随想录》刷题笔记——哈希表篇【java实现】
60 0
|
1月前
|
存储 算法 安全
【Java编程进阶之路 02】深入探索:红黑树如何重塑哈希表的性能边界
JDK 1.8之后,HashMap引入红黑树来优化性能,当链表长度超过阈值(默认为8)时,链表会转换为红黑树,从而提高高冲突时的查询效率。同时,HashMap也采用了扰动函数来增加哈希值的随机性,使键值对更均匀分布,提升性能。
40 0
|
1月前
|
存储 缓存 安全
Java ConcurrentHashMap:线程安全的哈希表实现
Java ConcurrentHashMap:线程安全的哈希表实现
|
1月前
|
存储 缓存 Java
Java LinkedHashMap:保持插入顺序的哈希表解析
Java LinkedHashMap:保持插入顺序的哈希表解析
|
1月前
|
存储 缓存 安全
Java HashMap:哈希表原理、性能与优化
Java HashMap:哈希表原理、性能与优化
|
1月前
|
存储 Java Serverless
哈希表原理与Java HashSet、LinkedHashSet实现
哈希表原理与Java HashSet、LinkedHashSet实现
|
1月前
|
存储 Java Serverless
二叉搜索树 和 哈希表 (JAVA)
【1月更文挑战第1天】阿里云初体验 二叉搜索树 二叉搜索树的插入 二叉搜索树的查找 二叉搜索树的删除 哈希表 哈希冲突 闭散列 线性探测法 二次探测法 开散列 二叉搜索树又称二叉排序树,它具有以下性质的二叉树或空树: 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值 它的每颗子树也分别为二叉搜索树 而哈希表是一种插入/删除/查找时间复杂度都是O(1)的数据结构,它的查询之所以也可以如此之快的的原因就是它在查找时不需要经过任何比较,直接一次从表中得到要搜索的元素。
40 0
二叉搜索树 和 哈希表 (JAVA)