2021-08-09 设计哈希集合

leetcode每日一题之设计哈希集合

题目链接:https://leetcode-cn.com/problems/design-hashset/

题目描述:不使用任何内建的哈希表库设计一个哈希集合(HashSet)。

实现 MyHashSet 类:

void add(key) 向哈希集合中插入值 key 。
bool contains(key) 返回哈希集合中是否存在这个值 key 。
void remove(key) 将给定值 key 从哈希集合中删除。如果哈希集合中没有这个值,什么也不做。

分析:

为了实现哈希集合这一数据结构,有以下几个关键问题需要解决:

  • 哈希函数:能够将集合中任意可能的元素映射到一个固定范围的整数值,并将该元素存储到整数值对应的地址上。

  • 冲突处理:由于不同元素可能映射到相同的整数值,因此需要在整数值出现「冲突」时,需要进行冲突处理。总的来说,有以下几种策略解决冲突:

    • 链地址法:为每个哈希值维护一个链表,并将具有相同哈希值的元素都放入这一链表当中。

    • 开放地址法:当发现哈希值 hh 处产生冲突时,根据某种策略,从 hh 出发找到下一个不冲突的位置。例如,一种最简单的策略是,不断地检查 h+1,h+2,h+3,\ldotsh+1,h+2,h+3,… 这些整数对应的位置。

    • 再哈希法:当发现哈希冲突后,使用另一个哈希函数产生一个新的地址。

  • 扩容:当哈希表元素过多时,冲突的概率将越来越大,而在哈希表中查询一个元素的效率也会越来越低。因此,需要开辟一块更大的空间,来缓解哈希表中发生的冲突。

package com.tao.hashtable;
import java.util.Iterator;
import java.util.LinkedList;

/**
 * @Classname MyHashSet
 * @Description 利用双链表设计哈希集合
 * Java 标准库的 HashMap 基本上就是用 拉链法 实现的。
 * 拉链法 的实现比较简单,将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。
 * 若遇到哈希冲突,则将冲突的值加到链表中即可。
 * <p>
 * 实现步骤
 * <p>
 * *得到一个 key
 * *计算 key 的 hashValue
 * *根据 hashValue 值定位到 data[hashValue]。(data[hashValue] 是一条链表)
 * *若data[hashValue]为空则直接插入
 * *不然则添加到链表末尾
 * 这里需要注意的是,哈希函数必须保证哈希值的均匀分布 ,若全部集中在一条链表中,则时间复杂度和顺序链表相同。
 * <p>
 * 还有一点则是数组的大小,若你能估计数据的大小,则直接指定即可,否则就需要 动态扩充 数组。
 * int[] newArray = new int [array.length*2];
 * //将array数组从0位置至array.length位置,复制到newArray数组0位置到array.length位置。
 * System.arraycopy(array,0,newArray,0,array.length);
 *
 * @Date 2021/8/4 21:36
 * @Author Anonymous
 */
@SuppressWarnings("all")
public class MyHashSet2 {
    public static void main(String[] args) {

    }

    private static final int BASE = 769;//基准值,作为哈希取模运算
    private LinkedList<Integer>[] data;//数组中每一格就是一个链表。

    /**
     * Initialize your data structure here.
     */

    public MyHashSet2() {
        data = new LinkedList[BASE];
        for (int i = 0; i < data.length; i++) {
            data[i] = new LinkedList<>();
        }
    }

    /*
     * @Author Anonymous
     * @Description //向集合中添加元素
     * @Date 10:04 2021/8/9
     * @Param [key]  表示要插入的值
     * @return void
     **/
    public void add(int key) {
        int hash = hashValue(key);//先计算哈希值
        Iterator<Integer> iterator = data[hash].iterator();//获得hash所在链表的迭代器
        while (iterator.hasNext()) {//遍历hash值为hash所在的链表
            Integer next = iterator.next();//得到下一个值
            if (next == key) {//如果里面有重复的元素直接返回
                return;
            }
        }
        data[hash].offerLast(key);//如果没有重复元素就添加到链表末尾
    }

    /*
     * @Author Anonymous
     * @Description //从集合中删除元素
     * @Date 10:05 2021/8/9
     * @Param [key] 表示要删除的元素
     * @return void
     **/
    public void remove(int key) {
        int hash = hashValue(key);//获得哈希值
        Iterator<Integer> iterator = data[hash].iterator();
        while (iterator.hasNext()) {
            Integer next = iterator.next();//得到下一个元素
            if (next == key) {
                data[hash].remove(next);
                return;
            }
        }
    }

    /**
     * Returns true if this set contains the specified element
     */
    public boolean contains(int key) {
        int hash = hashValue(key);
        Iterator<Integer> iterator = data[hash].iterator();
        while (iterator.hasNext()) {
            Integer next = iterator.next();
            if (next == key) {
                return true;
            }
        }
        return false;
    }

    /*
     * @Author Anonymous
     * @Description //
     * 设哈希表的大小为base,则可以设计一个简单的哈希函数:hash(x)=x mod base。
     * 我们开辟一个大小为base的数组,数组的每个位置是一个链表。当计算出哈希值之后,就插入到对应位置的链表当中。
     * 由于我们使用整数除法作为哈希函数,为了尽可能避免冲突,应当将 base 取为一个质数。在这里,我们取base=769。
     * @Date 10:06 2021/8/9
     * @Param [value] 要要计算的插入的数字的哈希
     * @return int  哈希值
     **/
    public int hashValue(int value) {
        return value % BASE;
    }
}

查了下质数取模,其实是利用了同余的概念:当元素是个有规律的等差数列时,并且和基数(数组大小)最大公约数不为1时,就会造成哈希映射时冲突变高(数组某些位置永远不会有值)。比如数列0,6,12,18,24,30…,

  • base为10,取模(0,6,2,8,4,0…)后,放入哈希表中位置将只能在0,2,4,6,8这几个数组位置上;
  • 但我们如果把base取7(数组大小甚至比10小),同样数列取模后(0,6,5,4,3,2,1,0,…),可以分布在哈希表中的0,1,2,3,4,5,6所有位置上;

后续:若x和y的最大公约为z,x和y的最小公倍数就为(xy)/z,很明显,若z为1,也就是俩数的最大公约数为1的时候,那么俩数的最小公倍数就为xy。

那么当一个数为质数时,除了其自身的倍数外,其余数和其的最大公约数都将是1,这时,步长选任何数(除了倍数)都可以满足桶的均匀分布。

所以,以取模计算哈希值在桶中的位置是,用一个质数当作基数时可以使得哈希表中每个位置都“有用武之地”。

Hash的用途很多,我们在使用Ngnix做负载均衡的时候,同样用的也是Hash的方式。总的来说,要是数据分布均匀一些,在这种时候就可以考虑使用Hash的方式对数据进行处理。

当然质数也不一定非要是769:可以参考:https://planetmath.org/goodhashtableprimes

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值