我的编程空间,编程开发者的网络收藏夹
学习永远不晚

如何在Java中实现一个散列表

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

如何在Java中实现一个散列表

前言:

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

很简单!

我们可以建一个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++;   
        }
    }
}

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

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

优化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 state
    int 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函数,把每个单词映射到数组的某个下标,用这个数组计数统计就好啦。

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

到此这篇关于如何在Java中实现一个散列表的文章就介绍到这了,更多相关Java散列表内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

如何在Java中实现一个散列表

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

在Java项目中如何实现一个可变参数列表

这篇文章给大家介绍在Java项目中如何实现一个可变参数列表,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。Java可变参数列表详解1、接受的传入参数情况:如public void test(String ...args)
2023-05-31

如何用C语言写一个散列表

本篇文章为大家展示了如何用C语言写一个散列表,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。一、快速理解散列表散列表,就是下标可以为字母的数组。假设现有一个数组int a[100],想查找其中第40个
2023-06-22

怎么在PHP中实现一个密码散列算法

这期内容当中小编将会给大家带来有关怎么在PHP中实现一个密码散列算法,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。查看密码散列函数的加密算法首先,我们还是看看当前环境中所支持的 password_hash
2023-06-15

如何在Android应用中实现一个列表悬浮效果

这期内容当中小编将会给大家带来有关如何在Android应用中实现一个列表悬浮效果,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。具体方法如下:package com.xiaos.view;import an
2023-05-31

如何在android中利用listview实现一个列表展示效果

今天就跟大家聊聊有关如何在android中利用listview实现一个列表展示效果,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。1.设置条目点击事件package com.ithei
2023-05-31

Java如何实现一个顺序表

这篇文章主要介绍Java如何实现一个顺序表,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!实现一个顺序表接口实现定义一个MyArrayList类,在类中实现以下函数public class MyArrayList {
2023-06-14

如何在Java中实现一个大数类

如何在Java中实现一个大数类?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。Java中的大数类简单实现Java中的大数还是挺好用,而且很方便,所以将其罗列如下,
2023-05-31

C语言如何实现一个链表队列

本篇内容主要讲解“C语言如何实现一个链表队列”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C语言如何实现一个链表队列”吧!C语言数据结构链表队列的实现1.写在前面  队列是一种和栈相反的,遵循先
2023-06-16

如何在Android应用中利用Spinner实现一个下拉列表功能

这期内容当中小编将会给大家带来有关如何在Android应用中利用Spinner实现一个下拉列表功能,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。Spinner  Spinner是一个列表选择框,会在用户选
2023-05-31

如何在PHP项目中实现一个队列场景

本篇文章为大家展示了如何在PHP项目中实现一个队列场景,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。声明概念:资源管理器(resource manager):用来管理系统资源,是通向事务资源的途径。
2023-06-06

java如何实现反转列表

可以使用递归或迭代的方式来实现反转链表。递归方式:class ListNode {int val;ListNode next;ListNode(int val) {this.val = val;}}public class Solut
2023-10-22

在Java项目中如何实现一个同步锁

在Java项目中如何实现一个同步锁?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。Java 同步锁(synchronized)详解及实例Java中cpu分给每个线程的时间片是
2023-05-31

sql如何在表中增加一列

如何在 sql 表中增加一列?使用 alter table 语句,指定表名、列名和数据类型;可选设置 not null 约束和默认值;运行 alter table 语句将新列添加到表中。如何在 SQL 表中增加一列前言在 SQL 中,增
sql如何在表中增加一列
2024-06-06

oracle如何在表中间加一列

要在表中间添加一列,可以使用ALTER TABLE语句。例如,要在表中间的某个位置添加一列,可以使用以下语法:ALTER TABLE table_nameADD column_name datatype AFTER existing_c
oracle如何在表中间加一列
2024-04-09

如何在Java中实现阻塞队列

如何在Java中实现阻塞队列?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。Java阻塞队列阻塞队列和普通队列主要区别在阻塞二字:阻塞添加:队列已满时,添加元素线
2023-06-15

利用Java如何实现一个双向链表

利用Java如何实现一个双向链表?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。Java中双向链表详解及实例写在前面:  双向链表是一种对称结构,它克服了单链表上
2023-05-31

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录