如何实现一个优秀的散列表!

文章内容收录到个人网站,方便阅读:http://hardyfish.top/

文章内容收录到个人网站,方便阅读:http://hardyfish.top/

文章内容收录到个人网站,方便阅读:http://hardyfish.top/

在这里插入图片描述

前言

假设现在有一篇很长的文档,如果希望统计文档中每个单词在文档中出现了多少次,应该怎么做呢?

很简单!

我们可以建一个HashMap,以String类型为Key,Int类型为Value;

  • 遍历文档中的每个单词 word ,找到键值对中key为 word 的项,并对相关的value进行自增操作。
  • 如果该key= word 的项在 HashMap中不存在,我们就插入一个(word,1)的项表示新增。
  • 这样每组键值对表示的就是某个单词对应的数量,等整个文档遍历完成,我们就可以得到每个单词的数量了。

简单实现下,代码示例如下:

import java.util.HashMap;
import java.util.Map;
public class Test {public static void main(String[] args) {Map map = new HashMap<>();String doc = "yue ban fei yu";String[] words = doc.split(" ");for (String s : words) {if (!map.containsKey(s)) {map.put(s, 1);} else {map.put(s, map.get(s) + 1);}}System.out.println(map);}
}

那HashMap是怎么做到高效统计单词对应数量的?我们下面会逐步来研究一下!

首先我们先来看看如果只统计某一个单词的数量?

只需要开一个变量,同样遍历所有单词,遇到和目标单词一样的,才对这个变量进行自增操作;

  • 等遍历完成,我们就可以得到该单词的数量了。
  • 我们可以把所有可能出现的单词都列出来,每个单词,单独用一个变量去统计它出现的数量,遍历所有单词,判断当前单词应该被累计到哪个变量中。
import java.util.HashMap;
import java.util.Map;
public class Main {public static void main(String[] args) {int[] cnt = new int[20000];String doc = "a b c d";String[] words = doc.split(" ");int a = 0;int b = 0;int c = 0;int d = 0;for (String s : words) {if (s == "a") a++;if (s == "b") b++;if (s == "c") c++;if (s == "d") d++;   }}
}

注意:这样的代码显然有两个很大的问题:

  1. 对单词和计数器的映射关系是通过一堆if-else写死的,维护性很差;
  2. 必须已知所有可能出现的单词,如果遇到一个新的单词,就没有办法处理它了。

优化1

我们可以开一个数组去维护计数器。

具体做法就是,给每个单词编个号,直接用编号对应下标的数组元素作为它的计数器就好啦。

我们可以建立两个数组:

  • 第一个数组用于存放所有单词,数组下标就是单词编号了,我们称之为字典数组;
  • 第二个数组用于存放每个单词对应的计数器,我们称之为计数数组。

每遇到一个新的单词,都遍历一遍字典数组,如果没有出现过,我们就将当前单词插入到字典数组结尾。

这样做,整体的时间复杂度较高,还是不行。

优化2

优化方式:

  • 一种是我们维护一个有序的数据结构,让比较和插入的过程更加高效,而不是需要遍历每一个元素判断逐一判断。
  • 另一种思路就是我们是否能寻找到一种直接基于字符串快速计算出编号的方式,并将这个编号映射到一个可以在O(1)时间内基于下标访问的数组中。

以单词为例,英文单词的每个字母只可能是 a-z。

我们用0表示a、1表示b,以此类推,用25表示z,然后将一个单词看成一个26进制的数字即可。

import java.util.HashMap;
import java.util.Map;
public class Main {public static void main(String[] args) {int[] cnt = new int[20000];String doc = "a b c d";String[] words = doc.split(" ");for (String s : words) {int tmp = 0;for (char c: s.toCharArray()) {tmp *= 26;tmp += (c - 'a');}cnt[tmp]++;}String target = "a";int hash = 0;for (char c: target.toCharArray()) {hash *= 26;hash += c - 'a';}System.out.println(cnt[hash]);}
}

这样我们统计N个单词出现数量的时候,整体只需要O(N)的复杂度,相比于原来的需要遍历字典的做法就明显高效的多。

这其实就是散列的思想了。

优化3

使用散列!

散列函数的本质,就是将一个更大且可能不连续空间(比如所有的单词),映射到一个空间有限的数组里,从而借用数组基于下标O(1)快速随机访问数组元素的能力

但设计一个合理的散列函数是一个非常难的事情。

  • 比如对26进制的哈希值再进行一次对大质数取mod的运算,只有这样才能用比较有限的计数数组空间去表示整个哈希表。

取了mod之后,我们很快就会发现,现在可能出现一种情况,把两个不同的单词用26进制表示并取模之后,得到的值很可能是一样的。

这个问题被称之为哈希碰撞

如何实现

最后我们考虑一下散列函数到底需要怎么设计。

以JDK(JDK14)的HashMap为例:

  • 主要实现在 java.util 下的 HashMap 中,这是一个最简单的不考虑并发的、基于散列的Map实现。

找到其中用于计算哈希值的hash方法:

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

可以发现就是对key.hashCode()进行了一次特别的位运算。

hashcode方法

在Java中每个对象生成时都会产生一个对应的hashcode。

  • 当然数据类型不同,hashcode的计算方式是不一样的,但一定会保证的是两个一样的对象,对应的hashcode也是一样的;

所以在比较两个对象是否相等时,我们可以先比较hashcode是否一致,如果不一致,就不需要继续调用equals,大大降低了比较对象相等的代价。

我们就一起来看看JDK中对String类型的hashcode是怎么计算的,我们进入 java.lang 包查看String类型的实现:

public int hashCode() {// The hash or hashIsZero fields are subject to a benign data race,// making it crucial to ensure that any observable result of the// calculation in this method stays correct under any possible read of// these fields. Necessary restrictions to allow this to be correct// without explicit memory fences or similar concurrency primitives is// that we can ever only write to one of these two fields for a given// String instance, and that the computation is idempotent and derived// from immutable stateint h = hash;if (h == 0 && !hashIsZero) {h = isLatin1() ? StringLatin1.hashCode(value): StringUTF16.hashCode(value);if (h == 0) {hashIsZero = true;} else {hash = h;}}return h;
}

Latin和UTF16是两种字符串的编码格式,实现思路其实差不多,我们来看看StringUTF16中hashcode的实现:

public static int hashCode(byte[] value) {int h = 0;int length = value.length >> 1;for (int i = 0; i < length; i++) {h = 31 * h + getChar(value, i);}return h;
}

其实就是对字符串逐位按照下面的方式进行计算,和展开成26进制的想法本质上是相似的。

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

为什么选择了31?

首先在各种哈希计算中,我们比较倾向使用奇素数进行乘法运算,而不是用偶数。

因为用偶数,尤其是2的幂次,进行乘法,相当于直接对原来的数据进行移位运算;这样溢出的时候,部分位的信息就完全丢失了,可能增加哈希冲突的概率。

为什么选择了31这个奇怪的数,这是因为计算机在进行移位运算要比普通乘法运算快得多,而31*i可以直接转化为(i << 5)- i ,这是一个性能比较好的乘法计算方式,现代的编译器都可以推理并自动完成相关的优化。

具体可以参考《Effective Java》中的相关章节。

h>>>16

我们现在来看 ^ h >>> 16 又是一个什么样的作用呢?

它的意思是就是将h右移16位并进行异或操作,为什么要这么做呢?

因为那个hash值计算出来这么大,那怎么把它连续地映射到一个小一点的连续数组空间呢?

所以需要取模,我们需要将hash值对数组的大小进行一次取模。

我们需要对2的幂次大小的数组进行一次取模计算。

但对二的幂次取模相当于直接截取数字比较低的若干位,这在数组元素较少的时候,相当于只使用了数字比较低位的信息,而放弃了高位的信息,可能会增加冲突的概率。

所以,JDK的代码引入了^ h >>> 16 这样的位运算,其实就是把高16位的信息叠加到了低16位,这样我们在取模的时候就可以用到高位的信息了。

如何处理哈希冲突呢?

JDK中采用的是开链法。

哈希表内置数组中的每个槽位,存储的是一个链表,链表节点的值存放的就是需要存储的键值对。

如果碰到哈希冲突,也就是两个不同的key映射到了数组中的同一个槽位,我们就将该元素直接放到槽位对应链表的尾部。

总结一下

手写数据结构统计单词的数量正确的思路就是:

根据全文长度大概预估一下会有多少个单词,开一个数倍于它的数组,再设计一个合理的hash函数,把每个单词映射到数组的某个下标,用这个数组计数统计就好啦。

当然在实际工程中,我们不会为每个场景都单独写一个这样的散列表实现,也不用自己去处理复杂的扩容场景。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1551349.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

锦囊妙计系列:没有项目支撑的情况下怎么从java到Python平稳过度并就业成功

从Java转向Python&#xff0c;并在没有项目支撑的情况下平稳过渡并实现就业&#xff0c;尽管有挑战&#xff0c;但完全可以通过系统学习、项目积累、技能展示和策略性求职来达成目标。以下是详细的步骤和策略&#xff0c;帮助你在不依赖现有项目的情况下实现从Java到Python的成…

24.9.29

星期一&#xff1a; 补 cf round974 div3 H cf传送门 题意&#xff1a;给一数组&#xff0c;q次询问&#xff0c;每次询问l-r区间内是否每个数字都出现偶数次 终于找到了梦中的随机数函数&#xff0c;这随机数真是非常顶级口牙 思路&a…

在线SQL模拟器

前言 有时候我们想学习下SQL&#xff0c;但是缺少数据库环境&#xff0c;多种数据库类型&#xff0c;MySQL&#xff0c;SQL server&#xff0c;Oracle&#xff0c;PostgreSQL等等&#xff0c;自己安装&#xff1f;耗时费力&#xff0c;占用电脑资源&#xff0c;要是有一个在线…

理解 Vue 的 setup 应用程序钩子

title: 理解 Vue 的 setup 应用程序钩子 date: 2024/9/30 updated: 2024/9/30 author: cmdragon excerpt: 摘要:本文详细介绍了Vue 3中setup函数的应用,包括其概念、特性、使用方法及重要性。setup函数作为组合API的核心,在组件实例化前被调用,用于设置响应式状态、计算…

如何构建一个生产级的AI平台(2)?

书接上回&#xff0c;继续往下讲,本节会说一下如何给大模型应用构建安全防护机制 为大模型应用构建安全防护 构建安全防护有助于降低 AI 风险&#xff0c;不仅可以保护您的用户&#xff0c;还可以保护您&#xff08;开发人员&#xff09;。只要有可能发生故障&#xff0c;就应…

Redis篇(应用案例 - UV统计)(持续更新迭代)

目录 一、HyperLogLog 二、测试百万数据的统计 一、HyperLogLog 首先我们搞懂两个概念&#xff1a; UV&#xff1a;全称Unique Visitor&#xff0c;也叫独立访客量&#xff0c;是指通过互联网访问、浏览这个网页的自然人。 1天内同一个用户多次访问该网站&#xff0c;只记录…

大盘点|9月独家爆款SVG模版(互斥伸长、扑克出牌、预感应滑动等)

九月即将结束&#xff0c;黑科技 SVG 编辑器作为业界天花板&#xff0c;在本月又发明了哪些一骑绝尘的 SVG 特效大杀器&#xff1f;一起来看看我们的盘点与推荐吧&#xff01;如需体验&#xff0c;不妨直接登陆黑科技编辑器一探究竟。 1️⃣互斥伸长/互斥切换-伸长 E2 平台的…

C# C++ 笔记

第一阶段知识总结 lunix系统操作 1、基础命令 &#xff08;1&#xff09;cd cd /[目录名] 打开指定文件目录 cd .. 返回上一级目录 cd - 返回并显示上一次目录 cd ~ 切换到当前用户的家目录 &#xff08;2&#xff09;pwd pwd 查看当前所在目录路径 pwd -L 打印当前物理…

如何使用 Python 读取数据量庞大的 excel 文件

使用 pandas.read_excel 读取大文件时&#xff0c;的确会遇到性能瓶颈&#xff0c;特别是对于10万行20列这种规模的 .xlsx 文件&#xff0c;常规的 pandas 方法可能会比较慢。 要提高读取速度&#xff0c;关键是找到更高效的方式处理 Excel 文件&#xff0c;特别是在 Python 的…

Android Stuido中编译信息出现乱码的解决方式

打开菜单File -> Settings&#xff0c;选择Editor -> File Encodings 窗口&#xff0c;将编码设置为正确的字符集&#xff0c;保证 Global Encoding、Project Encoding 和 Default Encoding for properties files 都设置为 UTF-8。

当今爆火的RPA其实就是自动化测试

最近有机会看到了 RPA 在实际工作中的重度应用&#xff0c;深刻感受到了自动化的强大实力&#xff0c;以后的应用前景时完全可期的。 RPA (Robotic Process Automation) 简介 Robotic Process Automation (RPA) 是一种技术&#xff0c;使用软件机器人&#xff08;或称“机器人…

APO v0.5.0 发布:可视化配置告警规则;优化时间筛选器;支持自建的ClickHouse和VictoriaMetrics

APO 新版本 v0.5.0 正式发布&#xff01;本次更新主要包含以下内容&#xff1a; 新增页面配置告警规则和通知 在之前的版本中&#xff0c;APO 平台仅支持展示配置文件中的告警规则&#xff0c;若用户需要添加或调整这些规则&#xff0c;必须手动编辑配置文件。而在新版本中&a…

09_OpenCV彩色图片直方图

import cv2 import numpy as np import matplotlib.pyplot as plt %matplotlib inlineimg cv2.imread(computer.jpeg, 1) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) plt.imshow(img) plt.show()plot绘制直方图 plt.hist(img.ravel(), 256) #ravel() 二维降一维 256灰度级…

【使用resnet18训练自己的数据集】

1.背景及准备 书接上文【以图搜图代码实现】–犬类以图搜图示例 总结了一下可以优化的点&#xff0c;其中提到使用自己的数据集训练网络&#xff0c;而不是单纯使用预训练的模型&#xff0c;这不就来了&#xff01;&#xff01; 使用11类犬类微调resnet18网络模型&#xff1a…

如何构建一个生产级的AI平台(1)?

本文概述了生成式 AI 平台的常见组件、它们的作用以及它们的实现方式。 本文重点介绍部署 AI 应用程序的整体架构。 它讨论了需要哪些组件以及构建这些组件时的注意事项。 它不是关于如何构建 AI 应用程序。 这就是整体架构的样子。 这是一个相当复杂的系统。 这篇文章将从最…

css 中 ~ 符号、text-indent、ellipsis、ellipsis-2、text-overflow: ellipsis、::before的使用

1、~的使用直接看代码 <script setup> </script><template><div class"container"><p><a href"javascript:;">纪检委</a><a href"javascript:;">中介为</a><a href"javascript:…

SpringBoot技术栈:打造下一代网上租赁系统

第2章 关键技术简介 2.1 Java技术 Java是一种非常常用的编程语言&#xff0c;在全球编程语言排行版上总是前三。在方兴未艾的计算机技术发展历程中&#xff0c;Java的身影无处不在&#xff0c;并且拥有旺盛的生命力。Java的跨平台能力十分强大&#xff0c;只需一次编译&#xf…

传统操作系统和分布式操作系统的区别

分布式操作系统和传统操作系统之间的区别&#xff0c;根植于它们各自的设计哲学和目标。要理解这些差异&#xff0c;需要从操作系统的基本定义、结构、功能以及它们在不同计算环境中的表现进行分析。每种系统都试图解决特定的计算挑战&#xff0c;因此在不同的使用场景下具有各…

基于springboot+vue的社区流浪动物救助系统

摘要 本文介绍了一个基于Spring Boot和Vue.js技术的社区流浪动物救助系统。该系统采用前后端分离架构&#xff0c;后端使用Spring Boot框架进行开发&#xff0c;负责业务逻辑的处理和数据的交互&#xff1b;前端则使用Vue.js框架&#xff0c;为用户提供友好的交互界面。系统实现…

Springboot学习笔记(4)MybatisPlus

1. MybatisPlus 1.1 ORM介绍 ORM&#xff08;Object Relational Mapping&#xff0c;对象关系映射&#xff09;是为了解决面向对象与关系数据库存在的互不匹配现象的一种技术。 比如&#xff0c;将java中的对象传递到关系型数据库中去&#xff0c;或者将关系型数据库传递到jav…