【Java数据结构】哈希表

哈希表

1.哈希表

哈希散列(散列表):不经过任何比较,一次直接从表中得到要搜索的元素,时间复杂度是O(1)。哈希函数的设置hash(key) = key % capacity,capacity是存储元素底层空间总的大小。

2.哈希冲突

当数据集合中可能存在多个数据都被插在一块区域,如上面例题23和3取模后都放在了数组下标3的位置,这是就存在冲突,也就被称为哈希冲突冲突的解决方法:避免冲突、解决冲突。

3.避免冲突

冲突是难免的,我们需要做的就是减少冲突。引起哈希冲突的可能原因就是哈希函数设计不合理:

  1. 哈希函数的定义域必须包括需要存储的全部关键码,简而言之就是,如果数据集合中有m个元素,数组的值域就必须在0~m-1之间
  2. 哈希函数计算出来的地址能均匀分布在整个空间中
  3. 哈希函数要比较简单一点
常见的哈希函数

1.直接定制法(常用)

在使用这个方法时需要知道关键自的分布情况(需要找差距比较小且连续的情况),散列地址为hash(key) = A*key+B。大概就是找其数据集合中最小的为B,然后再求A。

这个方法对应的面试题:387. 字符串中的第一个唯一字符 - 力扣(LeetCode)

这题的大致做法就是先设置一个存放26个字母的数组,然后每遍历一个字符都在数组对应的下标中加1,遍历完后再遍历一遍,如果返现字符中数组对应的值为1那就返回该下标,如果遍历完这个字符串都没有只出现一次的字符那就返回-1。

public int firstUniqChar(String s) {
    int[] ch = new int[26];
    for (int i = 0; i < s.length(); i++){
        ch[s.charAt(i)-'a']++;
    }
    for (int i = 0; i < s.length(); i++){
        if (ch[s.charAt(i)-'a'] == 1){
            return i;
        }
    }
    return -1;
}  

2.除留余数法(常用)

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

3.平方取中法(了解)

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

负载因子

随着负载因子的增加,冲突率也随着升高,所以想要降低冲突率就可以通过降低负载因子来实现。

4.解决哈希冲突

闭散列

线性探测:从发生冲突的位置开始,一次向后探测,知道找到下一个空位置。缺点就是:当该下标的数被删除后,再找与它冲突的数时可能会显示不存在。

插入:通过哈希函数获取待插入元素再哈希表中的位置,如果该位置中没有元素则直接插入新的元素即可,但是如果该位置有元素发生了哈希冲突,就需要使用下一个空位置插入新元素。

二次探测: 二次探测就避免了线性探测所出现的问题,它可以较快的找到冲突的数。:Hi = (H0 +i^2 )% m,(i = 1,2,3......),再使用哈希函数。。

开散列

开散列就是在一个数组中每个位置加了一个链表(尾插法,也可以使用头插法)。

下面我们具体实现以下链表+数组的这类代码

首先需要先创建结点信息、创建数组以及计数器这些初步信息。

    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 size;
    public HashBuck(){
         arr = new Node[10];
    }

然后在数组中添加结点 使其构成链表结构,这个是步骤最多的操作。首先需要知道这个键的数组下标index(这个通过取模求得),然后就是找到数组对应的下标进行插入操作,但是在开始插入前需要知道一些信息,像数组该下标是否为空,那就需要找到其末尾并将其插入进去;又或者像该数组下标中已经存在与之相同的键值那就更新其值、最后返回即可。最后就是插入了,先要创建这个新的结点,然后将其连接到链表的尾端,最后计数器加一就好啦!

public void put(int key, int value){
        //头插法
//        int index = key %arr.length;
//        Node cur = arr[index];
//        //判断链表中是否存在相同的key,存在就更新value
//        while (cur != null) {
//            if (cur.key == key) {
//                cur.value = value;
//                return;
//            }
//            cur = cur.next;
//        }
//        //如果不存在就 头插法 插入
//        Node node = new Node(key, value);
//        node.next = arr[index];
//        arr[index] = node;
//        size++;


        //尾插法
        int index = key %arr.length;
        Node cur = arr[index];
        //判断链表中是否存在相同的key,存在就更新value
        while (cur != null) {
            if (cur.key == key) {
                cur.value = value;
                return;
            }
            cur = cur.next;
        }
        //如果不存在就 尾插法 插入
        Node cur1 = arr[index];
        if (cur1 == null){
            Node node = new Node (key, value);
            arr[index] = node;
            size++;
        }else {
            while (cur1.next != null) {
                cur1 = cur1.next;
            }
            //尾插法
            Node node = new Node(key, value);
            cur1.next = node;
            size++;
        }

还有一点就是如果当负载因子很高时就需要将数组扩容,但是有一点不太好的就是需要将之前数组中所有元素重新哈希一遍也就是重新插入新的数组中。这个可以写到添加完结点后进行判断,所以我就接着上面继续写。

扩容一个新的数组tempArr,容量是原先数组的的两倍(看情况可以改),再遍历一遍原先的数组arr,将其每个位置的下标都重新哈希一次,由于是链表结构所以需要先保存当前结点的next值curNext,然后记录当前节点在新数组中的新位置,然后进行尾插法。

在进行尾插法时需要注意新位置中是否为空,为空时:将新数组中新位置修改为当前节点;不为空时:需要先找找链表中的尾节点,然后将结点加入进行。但是不管为不为空都需要将当前节点的next置为空(不然下次找时会存在之前数组的next地址),在继续遍历当前结点的下一个结点(前一步已经置为空了,这就是为什么要保存下一个节点的意义)【如果这两步顺序换一下结果是不是一样的?也行,但是需要保存一下cur,第一步已经将cur改变了,所以再对cur的next进行置空就与结果不一致了】。

            if (loadFactor() >= 0.75){
            //超载了需要扩容,然而扩容时需要把所有的元素都重新哈希(因为数组的容量变了对应取模的标准也变了)
            resize();
        }
    }

       private void resize(){
        Node[] tempArr = new Node[arr.length*2];
        for (int i = 0; i < arr.length; i++) {
            //头插
//            Node cur = arr[i];
//            while (cur != null){
//                //记录当前结点的下一个结点位置
//                Node curNext =cur.next;
//                //结点新的位置
//                int newIndex = cur.key%tempArr.length;
//                //头插法
//                cur.next = tempArr[newIndex];
//                tempArr[newIndex] = cur;
//                cur = curNext;
//          }


            //尾插法
            Node cur = arr[i];
            while (cur != null) {
                Node curNext = cur.next;
                int newIndex = cur.key % tempArr.length;
                Node newCur = tempArr[newIndex];
                if (newCur == null) {
                    tempArr[newIndex] = cur;
                } else {
                    while (newCur.next != null) {
                        newCur = newCur.next;
                    }
                    newCur.next = cur;
                }
                    cur.next = null;
                    cur = curNext;
            }
        }
        arr = tempArr;
    }

    private double loadFactor(){
        return size*1.0/arr.length;
    }

还有一个简单的get方法(获取对应key的value值)。

    public int get(int key){
        int index = key %arr.length;
        Node cur = arr[index];
        //判断链表中是否存在相同的key,并返回该节点的value值
        while (cur != null) {
            if (cur.key == key) {
                return cur.value;
            }
            cur = cur.next;
        }
        return -1;
    }
小知识:
  1. 键(Key)
    • 键是用于在哈希表(或类似的数据结构)中定位值的唯一标识符。
    • 在Java的HashMap中,键是用来计算哈希码的,这个哈希码决定了值存储的位置。
    • 键需要实现hashCode()方法,并且应该与equals()方法保持一致,以确保哈希表的正确性和效率。
  2. 值(Value)
    • 值是与键相关联的数据。
    • 在HashMap中,值是你实际存储和检索的数据。
    • 值不需要实现任何特定的方法,因为它们是通过键来访问的。

为什么需要键和值?

  • 快速查找:哈希表利用键的哈希码快速定位相应的值,这使得插入和查找操作在平均情况下非常高效(接近O(1)复杂度)。
  • 处理冲突:当多个键产生相同的哈希码时(称为冲突),哈希表需要有机制来处理这些冲突,例如使用链表、红黑树等来存储多个值。
  • 灵活性:键值对的结构允许你根据键来更新或删除特定的值,而不需要重新组织整个数据结构。

键和值的区别

  • 唯一性:在大多数哈希表实现中(如HashMap),键必须是唯一的。这意味着你不能有两个相同的键存储不同的值。然而,值不需要是唯一的,多个键可以指向相同的值。
  • 不可变性:键通常是不可变的(如String、Integer等),以确保哈希码的一致性。如果键在插入后被修改,可能导致无法找到原来的值。

使用哈希表可以将时间复杂度转成空间复杂度(通过空间浪费来换取时间的过程)。

如果面对的key是引用类型时,对key进行取模就不适用了,那么就需要一个新的put方法进行插入,但是大概的操作还是一样的,需要注意的就是引用类型中key不能取模需要先获取它的哈希值(通过hashCode方法实现)然后再取模剩下的差不多一样了,当然引用类型比较时需要用到equals方法。

class Person{
    public String name;
    public Person (String name){
        this.name = name;
    }
    //hashCode和equals的区别
    //hashCode是为了定位数组下标的,equals是遍历下标比较key是否存在相同的key

}

public class HashBuckPerson<K, V> {
    static class Node <K, V>{
        public K key;
        public V value;
        public Node<K, V> next;
        public Node(K key, V value){
            this.key = key;
            this.value = value;
        }

    }

    public Node<K, V>[] arr;
    public int size;
    public HashBuckPerson(){
        arr = new Node[10];
    }

    public void put(K key, V value){
        //头插法
        int hash = key.hashCode();
        int index = hash %arr.length;
        Node<K, V> cur = arr[index];
        //判断链表中是否存在相同的key,存在就更新value
        while (cur != null) {
            if (cur.key.equals(key)) {
                cur.value = value;
                return;
            }
            cur = cur.next;
        }
        //如果不存在就 头插法 插入
        Node<K, V>  node = new Node<>(key, value);
        node.next = arr[index];
        arr[index] = node;
        size++;

    }
    public V get(K key){
        int hash = key.hashCode();
        int index = hash % arr.length;
        Node<K, V> cur = arr[index];
        //判断链表中是否存在相同的key,存在就更新value
        while (cur != null) {
            if (cur.key.equals(key)) {
                return cur.value;
            }
            cur = cur.next;
        }
        return null;
    }

    public static void main(String[] args) {
        HashBuckPerson<Person, Integer> hash = new HashBuckPerson<>();
        Person person1 = new Person("zhangsan");
        Person person2 = new Person("lisi");
        hash.put(person1, 12);
        hash.put(person2, 1);

        System.out.println(hash.get(person2));
    }
}
相关推荐
星空露珠13 分钟前
飞机大战lua迷你世界脚本
数据结构·游戏·lua
萌の鱼2 小时前
leetcode 240. 搜索二维矩阵 II
数据结构·c++·算法·leetcode·矩阵
竹木有心2 小时前
考研408数据结构线性表核心知识点与易错点详解(附真题示例与避坑指南)
数据结构·考研
轩源源3 小时前
数据结构——哈希表的实现
开发语言·数据结构·c++·算法·哈希算法·散列表·哈希桶
lili-felicity3 小时前
深入理解指针与回调函数:从基础到实践
c语言·数据结构·算法
X_StarX3 小时前
计算机基础面试(数据结构)
数据结构·面试·职场和发展
waves浪游4 小时前
vector详解
c语言·开发语言·数据结构·c++·算法
ヾChen4 小时前
数据结构——栈
开发语言·数据结构·物联网·学习
_GR5 小时前
2016年蓝桥杯第七届C&C++大学B组真题及代码
c语言·数据结构·c++·算法·蓝桥杯