【数据结构】哈希表

【概念】

哈希表是一种特殊的存储方法,通过“哈希函数”使元素的存储位置与它的关键码之间建立起一一映射的关系,在查找时可以通过该函数快速地找到该元素

//可以把所有函数比作一堆锁,目前想开一把锁,方法比作钥匙,其他方法的钥匙只能一把一把挨个试,而哈希方法的钥匙和锁之间有编号,想开几号锁只需要找到对应编号的钥匙即可开启

当向哈希表中:

插入元素

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

搜索元素

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

该方式叫“哈希方法”(散列方法),哈希方法中使用的转换函数为“哈希(散列)函数”,构建出的结构为“哈希表(散列表)”

【哈希函数】

//capacity为容量

【哈希冲突】

key1和key2关键字不同,使用了相同的哈希函数 key % 容量 ==> index,得到的index下标相同,这种现象为“哈希冲突”或者“哈希碰撞”

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”

【避免哈希冲突方法】

由于哈希表底层数组的容量小于实际要存储的关键字数量,因此冲突的发生是必然的,我们能做的是“尽量降低冲突率”

1.设计合理的哈希函数

哈希函数的设计原则是:
1、哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值
域必须在 0 到 m-1 之间
2、哈希函数计算出来的地址能均匀分布在整个空间中
3、哈希函数应该比较简单

常见的哈希函数有直接定址法除留余数法

直接定址法:

取关键字的某个线性函数为散列地址: Hash ( Key ) = A*Key + B
其简单、均匀,但需要事先知道关键字的分布情况,适合查找比较小且连续的情况
除留余数法:

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

2.较低的载荷因子

散列表越长。载荷因子越低,越小的载荷因子代表着越低的冲突率,当载荷因子为0.75时,就需要扩容了

【解决哈希冲突方法】

1.闭散列(开放定址法)

当发生哈希冲突时,如果哈希表未被装满,那么说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去,下面将介绍寻找下一个空位置的方法

1.1:线性探测

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

举一个例子,上厕所时,在还有空位的情况下,第一个选择的坑位已经有人了,这个时候肯定不会站在门口等着,而是会选择旁边的位置。

1.2:二次探测

公式:

H0:冲突的位置       i:冲突的次数(从1开始)      m:容量

从发生冲突的位置开始,以一定规律的变化向后依次探测空位

在决定好寻找下一个空位置的方法后,设定好比例,当空位已经不足一个比例时(比如已经有十分之七的位置被使用),这样哈希冲突产生的概率就会大大提高,因此就需要对其进行扩容操作,随后把原本的值依照新的存储空间大小来进行新的操作

2.开散列(开链法)

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

从上图中可以看出,开散列中每个桶中放的都是发生哈希冲突的元素

【哈希表的实现】

package BinaryTree;

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 double loadFactor = 0.75;//负载因子

    //进行HashBuck的构造方法
    public HashBuck()
    {
        array = new Node[10];
    }

    public void put(int key, int val) {
        int index = key % array.length;放元素,首先需要找元素的位置,index位置 = 当前key % 整个数组的长度
        Node cur = array[index];//然后要采取头插法(头插尾插都可以,这里以头插法为例)
        //1.遍历链表是否存在当前值
        while (cur != null) {
            if (cur.key == key) {
                cur.val = val;//如果key相同,就更新val
                return;
            }
            cur = cur.next;//如果不相同,cur继续往后走
        }
        //2.走到这里说明没有当前值,此时进行头插
        Node node = new Node(key, val);//定义一个要插入的节点node
        node.next = array[index];//将node的next域指向头节点array[index]
        array[index] = node;//头节点array[index]指向node,达成头插
        usedSize++;//里面存放的元素个数增加

        //3.判断负载因子
        if (loadFactorCount() >= 0.75) // 负载因子大于0.75,说明要进行扩容了,扩容代表着遍历每个数组元素下的链表的每个元素,每个元素都需要进行重新哈希
        {
            resize();
        }
    }

    public void resize() // 扩容方法
    {
        Node[] newArray = 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 % newArray.length;//重新哈希,把数据存放在新的数组的newIndex位置
                Node curN = cur.next;
                cur.next = newArray[newIndex];//把cur指向的头节点地址放到newArray的newIndex中
                newArray[newIndex] = cur;//newArray的newIndex指向了cur
                cur = curN;
            }
        }
        array = newArray;//array指向了新的数组
    }

    public double loadFactorCount()//计算负载因子
    {
        return usedSize * 1.0 / array.length; // 填入表中的元素个数 / 哈希表长度
    }

    public int get(int key)
    {
        int index = key % array.length;
        Node cur = array[index];
        while (cur != null) {
            if (cur.key == key) {
                return cur.val; //如果key相同,直接返回key的value
            }
            cur = cur.next;//如果不相同,cur继续往后走
        }
        return -1;//没找到,返回-1
    }
}

【自定义类型的比较】

哈希桶内部自带比较方法,但这里Student是一个自定义类型,没办法比较,我们需要把一个引用对象转变为整数,这个转变整数的方法叫“hashCode”
package BinaryTree;

class Student
{
    public String stuN;

    public Student(String stuN)
    {
        this.stuN = stuN;
    }
}
public class Test {
    public static void main(String[] args)
    {
        Student student1 = new Student("240115");
        Student student2 = new Student("240115");
        
        int hashCode1 = student1.hashCode();
        int hashCode2 = student2.hashCode();

        System.out.println(hashCode1);
        System.out.println(hashCode2);
    }
}
如果认为这两个是同一个学生,那么key % array.length得到的整数index应该是一样的整数,但实际上并非如此

这意味着,虽然student1和student2学号相同,但并不被认为是同一个学生,这样一来我们就需要重写hashCode方法

通过idea编译器右键——Generate——equals() and hashCode()来快速生成

package BinaryTree;

import java.util.Objects;

class Student
{
    public String stuN;

    public Student(String stuN)
    {
        this.stuN = stuN;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return Objects.equals(stuN, student.stuN);
    }

    @Override 
    public int hashCode() {
        return Objects.hash(stuN);
    }
}
public class Test {
    public static void main(String[] args)
    {
        Student student1 = new Student("240115");
        Student student2 = new Student("240115");

        int hashCode1 = student1.hashCode();
        int hashCode2 = student2.hashCode();

        System.out.println(hashCode1);
        System.out.println(hashCode2);
    }
}

这样一来在运行后,两个输出值就一样了

因此,自定义类型比较时一定要重写equals方法和hashCode方法

哈希表是一种基于哈希函数进行快速查找的数据结构,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。哈希表的设计思路如下: 1. 哈希函数的设计:哈希函数是哈希表的核心,它将关键字映射到哈希表中的位置。一个好的哈希函数应该具有以下特点: - 映射范围广:哈希函数应该将关键字均匀地映射到哈希表中的位置,避免出现大量的哈希冲突。 - 计算速度快:哈希函数的计算速度应该尽可能快,以提高哈希表的访问速度。 - 低冲突率:哈希函数应该尽可能地避免哈希冲突,以提高哈希表的访问效率。 2. 哈希冲突的解决:由于哈希函数的映射范围是有限的,所以不同的关键字可能会映射到同一个位置,这就是哈希冲突。哈希冲突的解决方法有以下两种: - 链地址法:将哈希表中的每个位置都连接一个链表,当发生哈希冲突时,将新的关键字插入到链表的末尾。 - 开放地址法:当发生哈希冲突时,通过某种算法找到哈希表中的下一个空位置,将新的关键字插入到该位置。 3. 哈希表的增删查改操作:哈希表的增删查改操作都需要先通过哈希函数找到关键字在哈希表中的位置,然后再进行相应的操作。具体操作如下: - 插入操作:将新的关键字插入到哈希表中的对应位置,如果发生哈希冲突,则按照链地址法或开放地址法进行解决。 - 删除操作:将关键字从哈希表中对应位置删除,如果该位置上有链表,则需要遍历链表找到对应的关键字进行删除。 - 查找操作:通过哈希函数找到关键字在哈希表中的位置,如果该位置上有链表,则需要遍历链表找到对应的关键字进行查找。 - 修改操作:通过哈希函数找到关键字在哈希表中的位置,如果该位置上有链表,则需要遍历链表找到对应的关键字进行修改。 下面是一个使用链地址法实现的哈希表的Python代码示例: ```python class ListNode: def __init__(self, key=None, value=None): self.key = key self.value = value self.next = None class MyHashMap: def __init__(self): self.size = 1000 self.table = [None] * self.size def _hash(self, key): return key % self.size def put(self, key, value): index = self._hash(key) if not self.table[index]: self.table[index] = ListNode(key, value) else: node = self.table[index] while node: if node.key == key: node.value = value return if not node.next: break node = node.next node.next = ListNode(key, value) def get(self, key): index = self._hash(key) node = self.table[index] while node: if node.key == key: return node.value node = node.next return -1 def remove(self, key): index = self._hash(key) node = prev = self.table[index] if not node: return if node.key == key: self.table[index] = node.next else: node = node.next while node: if node.key == key: prev.next = node.next break node, prev = node.next, prev.next ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值