哈希表设计思想及实现

2023-11-19

哈希表设计思想及实现

定义

哈希表在《算法4》这本书中是这么介绍的:哈希表其实又叫散列表,是算法在时间和空间上做出权衡的经典例子。如果一个表所有的键都是小整数,我们就可以用一个数组来实现无序的符号表,将键作为数组的索引而数组中i出存储的值就是它对应的值。其实散列表的思想也是这样的,只不过他的键的类型更加复杂,是这种简易方法的一种扩展。

使用散列查找分为两步:

  1. 用散列函数将被查找的键转换为数组的一个索引。理想情况下,不同的键值都会转换成为不同的索引值,但是一般情况下我们会遇到多个键对应相同的索引值,这也就是所谓的散列冲突。
  2. 处理碰撞冲突的情况,常用得方法有两种:拉链法(链地址法)和线性探测法,下面也会介绍这两种方法。

哈希函数

其实要设计一个哈希表首先要做的就是散列函数的计算,通过散列函数就可以将键转换为数组的索引。其实哈希函数的设计是一个非常常见的一个研究方向,就像在我们的网络安全的密码设计的过程中,散列函数就是一个非常重要的理论。但是这里介绍的是最一般的哈希函数的设计思想。

哈希函数其实就是将键准换为索引,那么这里的键可以用什么数据类型呢?其实常见的基本数据类型都可以,甚至是我们自己定义的数据类型也是可以的。下面将分开展开介绍。

正整数

对于小正整数的话直接用就可以了,对于小负整数的话进行一定的偏移就可以了,例如-100 - 100 —> 0 - 200

对于大整数散列最常用的方法是除留余数法。

例如一个身份证号:332112200007076666这个的话取模,让他模10000,取后4位6666作为索引,这样带来好处是节省空间,因为我们显然不可能开辟那么大的空间去让身份证号称为索引,太浪费空间了。但是这样也带来了一定的问题就是碰撞冲突的问题。还有一个问题就是直接取模的话容易造成键值的分布不均匀。所以一般采用的方式是模一个大素数,为什么是模一个素数就可以避免不均匀的情况,这个其实是数论里的问题,不探讨,知道方法就可以了。

关于上面的情况举个例子介绍下:

10 % 4 = 2         10 % 7 = 3
20 % 4 = 0         20 % 7 = 6
30 % 4 = 2         30 % 7 = 2
40 % 4 = 0         40 % 7 = 5
50 % 4 = 2         50 % 7 = 1

那素数怎么取呢?给出一般的参考的取值

这里写图片描述

浮点数

浮点数的话其实也是转换成为整数,然后使用除留余数法。

那浮点数怎么转换成为整数呢?我们知道浮点数其实在计算机中是二进制数存在的,将二进制数变为整数就可以了。

这里写图片描述

字符串

其实字符串我们也可以将其看成是大整数来处理。

举个例子:

166=1102+6101+6100 166 = 1 ∗ 10 2 + 6 ∗ 10 1 + 6 ∗ 10 0

code=cB3+oB2+dB1+eB0 c o d e = c ∗ B 3 + o ∗ B 2 + d ∗ B 1 + e ∗ B 0

hash(code)=(cB3+oB2+dB1+eB0)%M h a s h ( c o d e ) = ( c ∗ B 3 + o ∗ B 2 + d ∗ B 1 + e ∗ B 0 ) % M

hash(code)=((((cB)+o)B+d)B+e))%M h a s h ( c o d e ) = ( ( ( ( c ∗ B ) + o ) ∗ B + d ) ∗ B + e ) ) % M

hash(code)=((((c%M)B+o)%MB+d)%MB+e))%M h a s h ( c o d e ) = ( ( ( ( c % M ) ∗ B + o ) % M ∗ B + d ) % M ∗ B + e ) ) % M

其实左后就准换成为上面公式的样子,其中B表示进制,M表示模的素数。

其实转换成代码就是下面这样的:

int hash = 0;
for(int i = 0; i < s.length(); i++ )
    hash = (hash * B + s.charAt(i))%M

组合类型

所谓的组合类型也就是自定义的数据类型,也是可以转换成为大整数来处理。

例如我定义了一个Student的数据类型

Student:name , number,score:

计算student的散列值如下:

hash(student)=(((student.name%M)B+student.number)%MB+student.socre)%M h a s h ( s t u d e n t ) = ( ( ( s t u d e n t . n a m e % M ) ∗ B + s t u d e n t . n u m b e r ) % M ∗ B + s t u d e n t . s o c r e ) % M

链地址法(拉链法)

链地址法的原理时如果遇到冲突,他就会在原地址新建一个空间,然后以链表结点的形式插入到该空间。下面从百度上截取来一张图片,可以很清晰明了反应下面的结构。比如说我有一堆数据{1,12,26,337,353…},而我的哈希算法是H(key)=key mod 16,第一个数据1的哈希值f(1)=1,插入到1结点的后面,第二个数据12的哈希值f(12)=12,插入到12结点,第三个数据26的哈希值f(26)=10,插入到10结点后面,第4个数据337,计算得到哈希值是1,遇到冲突,但是依然只需要找到该1结点的最后链结点插入即可,同理353。

  这里写图片描述

把一个值得hash值转换为一个数组的索引的代码如下:

private int hash(K key){
    return (key.hashCode() & 0x7ffffff)%M;
}

通过& 0x7ffffff这种方式就可以将符号位屏蔽,也就是说我们就算是传入负数也没有关系。

上面的图是每个地址存的是一个链表,在java8中当冲突达到一定的数量的时候就会将链表转换为红黑树,我们下面的实现用地址存一个TreeMap(java中的TreeMap的底层其实就是用红黑树来实现的)来具体的实现。

这里写图片描述

import java.util.TreeMap;

public class HashTable<K, V> {

    private TreeMap<K, V>[] hashtable;
    private int size;
    private int M;

    public HashTable(int M){
        this.M = M;
        size = 0;
        hashtable = new TreeMap[M];
        for(int i = 0 ; i < M ; i ++)
            hashtable[i] = new TreeMap<>();
    }

    public HashTable(){
        this(97);
    }

    private int hash(K key){
        return (key.hashCode() & 0x7fffffff) % M;
    }

    public int getSize(){
        return size;
    }

    public void add(K key, V value){
        TreeMap<K, V> map = hashtable[hash(key)];
        if(map.containsKey(key))
            map.put(key, value);
        else{
            map.put(key, value);
            size ++;
        }
    }

    public V remove(K key){
        V ret = null;
        TreeMap<K, V> map = hashtable[hash(key)];
        if(map.containsKey(key)){
            ret = map.remove(key);
            size --;
        }
        return ret;
    }

    public void set(K key, V value){
        TreeMap<K, V> map = hashtable[hash(key)];
        if(!map.containsKey(key))
            throw new IllegalArgumentException(key + " doesn't exist!");

        map.put(key, value);
    }

    public boolean contains(K key){
        return hashtable[hash(key)].containsKey(key);
    }

    public V get(K key){
        return hashtable[hash(key)].get(key);
    }
}

动态空间处理

我们上实现的时候把M的值取成了固定值,这个时候我们来分析下他的时间复杂度:

如果放入哈希表中的元素的个数使N,每个地址中存放的元素的个数是N/M的,然后每个地址中存放的是TreeMap,也就是底层是红黑树,对于一个红黑树的查询一个元素的时间复杂度是O(logN/M)的。然而对于哈希表其实是查询的复杂度应该是O(1)的复杂度,因为底层就是数组,直接索引一个数组中的值也就是O(1)的复杂度。

但是这里明显不符合啊?其实就是开辟的空间是固定的导致的,我们需要进行动态空间处理。怎么处理呢?其实就是把和动态数组的处理是一样的。当元素的个数大于阈值的时候就扩大空间,小于阈值时就压缩空间,具体如下:

当平均每个地址承载的元素多过一定程度时就扩容:n/M >= upperTol;

当平均每个地址承载的元素少于一定程度时就缩容:n/M < lowerTol;

扩容的时候怎么去扩大,扩大成多少呢?因为空间的大小其实也就是我们要模的那个值,所以选取扩大空间大小是有要求的。其实扩大的参考值就是上面给出素数取值时一般参考的取值。

具体的实现如下:

private void resize(int newM){
        TreeMap<K, V>[] newHashTable = new TreeMap[newM];
        for(int i = 0 ; i < newM ; i ++)
            newHashTable[i] = new TreeMap<>();

        int oldM = M;
        this.M = newM;
        for(int i = 0 ; i < oldM ; i ++){
            TreeMap<K, V> map = hashtable[i];
            for(K key: map.keySet())
                newHashTable[hash(key)].put(key, map.get(key));
        }

        this.hashtable = newHashTable;
    }

开放地址法

实现哈希表的另一种方式就是用大小为M的数组保存N个键值对,其中M > N。我们需要依靠数组中的空位解决碰撞冲突。基于这种策略的所有方法给统称为开放地址散列表。

其实就是使用开放地址法来解决碰撞冲突的问题。具体的开放地址法是怎么来解决碰撞冲突的,书上给出的解释是这样的:

这里写图片描述

完整代码

import java.util.TreeMap;

public class HashTable<K extends Comparable<K>, V> {

    private final int[] capacity
            = {53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
            49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 6291469,
            12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741};

    private static final int upperTol = 10;
    private static final int lowerTol = 2;
    private int capacityIndex = 0;

    private TreeMap<K, V>[] hashtable;
    private int size;
    private int M;

    public HashTable(){
        this.M = capacity[capacityIndex];
        size = 0;
        hashtable = new TreeMap[M];
        for(int i = 0 ; i < M ; i ++)
            hashtable[i] = new TreeMap<>();
    }

    private int hash(K key){
        return (key.hashCode() & 0x7fffffff) % M;
    }

    public int getSize(){
        return size;
    }

    public void add(K key, V value){
        TreeMap<K, V> map = hashtable[hash(key)];
        if(map.containsKey(key))
            map.put(key, value);
        else{
            map.put(key, value);
            size ++;

            if(size >= upperTol * M && capacityIndex + 1 < capacity.length){
                capacityIndex ++;
                resize(capacity[capacityIndex]);
            }
        }
    }

    public V remove(K key){
        V ret = null;
        TreeMap<K, V> map = hashtable[hash(key)];
        if(map.containsKey(key)){
            ret = map.remove(key);
            size --;

            if(size < lowerTol * M && capacityIndex - 1 >= 0){
                capacityIndex --;
                resize(capacity[capacityIndex]);
            }
        }
        return ret;
    }

    public void set(K key, V value){
        TreeMap<K, V> map = hashtable[hash(key)];
        if(!map.containsKey(key))
            throw new IllegalArgumentException(key + " doesn't exist!");

        map.put(key, value);
    }

    public boolean contains(K key){
        return hashtable[hash(key)].containsKey(key);
    }

    public V get(K key){
        return hashtable[hash(key)].get(key);
    }

    private void resize(int newM){
        TreeMap<K, V>[] newHashTable = new TreeMap[newM];
        for(int i = 0 ; i < newM ; i ++)
            newHashTable[i] = new TreeMap<>();

        int oldM = M;
        this.M = newM;
        for(int i = 0 ; i < oldM ; i ++){
            TreeMap<K, V> map = hashtable[i];
            for(K key: map.keySet())
                newHashTable[hash(key)].put(key, map.get(key));
        }

        this.hashtable = newHashTable;
    }
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

哈希表设计思想及实现 的相关文章

  • Vim中多行删除

    在操作虚拟机的时候 都会出错 当在vim中出现问题的时候 可以在dw普通模式下删除对应的单词 如果在vim中使用多行删除 可以使用dd vim 命令 将行数添加到该命令中 如10dd将从光标底部删除10行 包含光标行在内 删除单行 1 按
  • The Necklace 【UVA - 10054】【欧拉回路详解】

    题目链接1 题目链接2 题目求的是一串珠子 要让它们首尾相互照应才能串起来 并且 最后要连成一个环 使得最后的珠子的尾与第一个珠子的头相互对应 那么 这道题就是道求欧拉回路的题了 我们要先判断这是不是能够构成欧拉回路 这是个无向图 再对于需

随机推荐

  • 密码学原语如何应用?解析密文同态性的妙用

    隐私数据在密文形式下是否依旧可以加减乘除 其背后的同态性原理具体指什么 半同态性和全同态性有什么区别 单密钥和多密钥同态加密有哪些奇妙的应用场景 隐私保护方案设计 往往需要在密文状态下 对隐私数据进行特定的业务操作 以此保障数据的机密性 沿
  • Tomcat单实例安装部署

    自说 Tomcat 服务器是一个免费的开放源代码的Web 应用服务器 属于轻量级应用服务器 主要用于处理动态web数据 部署java环境 上传jdk包 使用xftp上传 解压 tar zxvf u01 jdk 8u333 linux i58
  • 傻傻分不清楚的sort,sorted,reverse,reversed

    前言 在平常编码过程中 列表是经常用的 而常用的方法也基本就是遍历循环进行元素的append 还有很多方法不熟悉 比如有一次遇到一个问题 将一个列表进行反转 拿起百度 得到答案 方法1 列表切片 步长设置为 1 方法2 列表自带方法 lst
  • 软件测试面试经验及面试题目分享

    关于面试 不同的公司千差万别 面试bat 创业公司 外包 难度和轮数差异巨大 但是有个共同点就是都是面试造航母 工作拧螺丝 意思就是说工作起来其实很容易 但是要过面试却很难 包括每天大家学习新技能很大一部分就是为了跳槽加薪 直接一点来说 其
  • 监控程序运行状态,并根据状态启动或重启进程

    监控程序运行状态 并根据状态启动或重启进程 需求 设计思路 实现 需求 根据运行环境要求 我们所做的程序常常会在无人监管的情况下运行几个月之久 所以为了保证程序的正常运行 决定添加一个附属的监控程序 监控程序要求如下 当检测到主程序未启动时
  • @RequestMapping、@PostMapping、@GetMapping的区别

    GetMapping 用于将HTTP GET请求映射到特定处理程序方法的注释 具体来说 GetMapping是一个作为快捷方式的组合注释 RequestMapping method RequestMethod GET PostMapping
  • QT_下拉选项框_Combo Box_使用

    添加选项 第一种 UI界面静态添加 如下 第二种 代码添加 如下 1 在mainwindow h头文件中添加创建用函数 2 定义函数 void MainWindow add combobox void ui gt comboBox gt a
  • JavaCollection集合

    5 Collection集合 5 1 Collection集合概述 是单列集合的顶层接口 它表示一组对象 这些对象也称Collection元素 JDK不提供此接口的直接实现 它提供更具体的子接口 Set 和 List 实现 package
  • 取消计算机系统密钥,BitLocker驱动器被加密怎么恢复密钥 忘了密码取消删除方法...

    由于最近电脑蓝屏 我需要U盘制作启动盘 结果U盘插入到我电脑 我发现此U盘被我以前用BitLocker加密过了 BitLocker密码我也忘记了 我只好去我以前旧电脑去找 BitLocker 驱动器加密恢复密钥 还好我旧电脑以前保存过了 我
  • WordPress(6)网站侧边栏倒计时进度小工具

    提示 文章写完后 目录可以自动生成 如何生成可参考右边的帮助文档 文章目录 效果图 在这里插入图片描述 一 添加位置 二 主题style css文件中添加美化 1 引入库 2 添加自定义的HTML模块 效果图 提示 以下是本篇文章正文内容
  • 重要-作为Android开发者必须了解的Gradle知识

    https www jianshu com p c31513f5f550
  • 【NOI 2015】程序自动分析

    题目 传送门 题目描述 在实现程序自动分析的过程中 常常需要判定一些约束条件是否能被同时满足 考虑一个约束满足问题的简化版本 假设 x 1 x 2
  • windows10和安装linux双系统安装教程(超简单)

    windows10和安装linux双系统安装教程 超简单 一共分三步 第一步了解自己电脑的BIOS 第二步安装windows10系统 第三步在windows10中安装ubuntu系统 第一步了解自己电脑的BIOS UEFI 是新式BIOS
  • 解决C++ unordered_map“nvalid use of incomplete type ‘struct std::hash“ 问题

    问题 G 使用unordered map时候 编译报错 invalid use of incomplete type struct std hash lt 放在G 6 5交叉编译环境是OK的 但是放在ubuntu14 04报错 解决 代码
  • uni-app开发微信小程序,在onShow()中获取onLoad(option)中的option页面路径值

    onShow 获取当前小程序的页面栈 let pages getCurrentPages 数组中索引最大的页面 当前页面 let currentPage pages pages length 1 打印出当前页面中的 options cons
  • DAY1:leetcode704二分查找 27移除元素

    目录 一 二分查找 注意区间与循环中的边界点取值 二 移除元素 双指针写法 一 二分查找 注意区间与循环中的边界点取值 class Solution public int search vector
  • 实验十四:Wireshark数据抓包分析之ARP协议

    实验十四 Wireshark数据抓包分析之ARP协议 目录 一 实验目的及要求 二 实验原理 1 什么是ARP 2 ARP工作流程 3 ARP缓存表 三 实验环境 四 实验步骤及内容 实验步骤一 1 使用netsh绑定IP和MAC地址 2
  • Air780E

    目录 基础资料 探讨重点 实现功能 硬件准备 软件版本 一 创建产品 1 1在onenet上创建产品 1 2创建设备 查看onenet接入协议 二 设备安全认证 1 鉴权参数 2 Token算法 3 sign算法 示例如下 4 参数编码 5
  • Mybatis与Spring的集成

    目录 一 Mybatis与spring集成 1 导入pom依赖 2 创建spring配置文件applicationContext 3 注解式开发 二 Aop整合pagehelper插件 4 Spring Test junit完美组合 一 M
  • 哈希表设计思想及实现

    哈希表设计思想及实现 定义 哈希表在 算法4 这本书中是这么介绍的 哈希表其实又叫散列表 是算法在时间和空间上做出权衡的经典例子 如果一个表所有的键都是小整数 我们就可以用一个数组来实现无序的符号表 将键作为数组的索引而数组中i出存储的值就