从 0 到 1 读懂:哈希表

news2024/11/23 12:15:25

哈希表

  • 一、什么是哈希表?
  • 二、两种散列函数构造方法
    • 1、直接定址法
    • 2、除留余数法(常用)
  • 三、散列地址冲突
  • 四、常用冲突处理
    • 1、负载因子调节(减少冲突概率)
    • 2、开放定址法(闭散列)
      • (1)线性探测
      • (2)二次探测
    • 3、链地址法(开散列)
    • 4、冲突严重时的解决办法
  • 五、Java 中 HashMap 的实现
    • 1、定义map结点
    • 2、put 插入方法的实现
    • 3、get 查询方法的实现

一、什么是哈希表?

一般来说,搜索的效率取决于搜索过程中元素的比较次数。例如顺序结构中,查找一个元素时需要挨个比对元素,因此时间复杂度为 O ( N ) O(N) O(N)。对于一颗平衡的搜索树,查找的时间复杂度为树的高度,即 O ( l o g 2 ( N ) ) O(log_2(N)) O(log2(N)).

而上述都并不是理想的搜索方法,理想的搜索方法是,可以不经过任何比较,一次直接从表中得到要搜索的元素。 这时就引出了哈希表的数据结构。哈希表就是通过(散列函数)哈希函数,使元素的存储位置与它的关键码 key 之间能够建立一一映射的关系,那样我们在查找关键字时,不需要比较就可获得需要的记录的存储位置。时间复杂度可达到 O ( 1 ) O(1) O(1).

采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(HashTable).

二、两种散列函数构造方法

1、直接定址法

如果我们现在要对 0~100 岁的人口数字统计表,那么我们对年龄这个关键字就可以直接用年龄的数字作为地址。此时 f ( k e y ) = k e y f ( key) =key f(key)=key.

如果我们现在要统计的是 80 后出生年份的人口数,那么我们对出生年份这个关键字可以用年份减去 1980 来作为地址。此时 f ( k e y ) = k e y − 1980 f ( key) = key-1980 f(key)=key1980.

也就是说,我们可以取关键字的某个线性函数值为散列地址,即 f ( k e y ) = a × k e y + b f ( key ) =a \times key+b f(key)=a×key+b (a 为常数).

这样的散列函数优点就是简单,均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合查找表较小且连续的情况。由于这样的限制,在现实应用中,此方法虽然简单,但却并不常用

2、除留余数法(常用)

此方法为最常用的构造散列函数方法,对于散列表长为 m 的散列函数公式为: f ( k e y ) = k e y / p f ( key) = key / p f(key)=key/p ,(p <= m).

很显然,本方法的关键就在于选择合适的 p 如果选得不好,就可能会容易产生冲突。根据前辈们的经验,若散列表表长为 m 通常 p 为小于或等于表长(最好接近 m )的最小质数。

三、散列地址冲突

在上文中,提到了冲突这个概念,那么到底什么是冲突呢?

在理想的情况下,每一个关键字,通过散列函数计算出来的地址都是不一样的,可现实中,这只是个理想。我们时常会碰到两个关键字 k e y 1 ≠ k e y 2 key_1 \neq key_2 key1=key2,但是却有 f ( k e y 1 ) = f ( k e y 2 ) f(key_1) = f(key_2) f(key1)=f(key2),这种现象我们称为冲突 (oollision) ,并把 k e y 1 key_1 key1 k e y 2 key_2 key2 称为这个散列函数的同义词 (synonym)。出现了冲突当然非常糟糕,那将造成数据查找错误。尽管我们可以通过精心设计的散列函数让冲突尽可能的少,但是不能完全避免。

四、常用冲突处理

1、负载因子调节(减少冲突概率)

散列表的负载因子定义为:loadFactor = 填入表中的元素个数 /散列表的长度

loadFactor 是散列表装满程度的标志因子。由于表长是定值,loadFactor 与“填入表中的元素个数”成正比,所以,loadFactor 越大,表明填入表中的元素越多,产生冲突的可能性就越大;反之,Q越小,标明填入表中的元素越少,产生冲突的可能性就越小。

常见的负载因子阈值通常为 0.7 或 0.8,一些采用开放定址法的hash库,如Java的系统库限制了荷载因子为0.75。当负载因子超过一定阈值时,可能会导致哈希冲突的增加,因此我们会对负载因子进行调节,由于表长是一定的,所以一般采用扩容的方式增加哈希表的大小,从而降低负载因子。扩容后,原有的元素需要重新计算哈希值和位置,并存储到新的哈希表中。

2、开放定址法(闭散列)

开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。根据寻找空散列的方式,可以分为以下两种:

(1)线性探测

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

公式 f i ( k e y ) = ( f ( k e y ) + d i ) / m , ( d = 1 , 2 , . . . . , m − 1 ) f_i ( key ) = ( f ( key ) +d_i ) / m,(d =1,2,....,m-1) fi(key)=(f(key)+di)/m(d=12....m1)

(2)二次探测

从上面例子中可以看到,我们在解决冲突的时候,还会碰到如 25 这种本来都不是同义词却要争夺一个地址的情况 ,称这种现象为堆积。很显然,堆积的出现,使得我们需要不断处理冲突,无论是存入还是查找效率都会大大降低。为了解决上述种问题,提出了二次探测法,双向寻找到可能的空位置。

公式 f i ( k e y ) = ( f ( k e y ) + d i ) / m , ( d i = 1 2 , − 1 2 , 2 2 , − 2 2 , . . . , q 2 , − q 2 , q < = m / 2 ) f_i ( key ) = ( f (key) +d_i) / m ,(d_i=1^2,-1^2,2^2,-2^2,...,q^2,-q^2,q<=m/2) fi(key)=(f(key)+di)/m(di=12122222...q2q2q<=m/2)

例如使用二次探测,再向上述HashTable中插入 34

3、链地址法(开散列)

首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。此时,已经不存在什么冲突换址的问题了,无论有多少个冲突,都只是在当前位置给单链表增加结点的问题。

4、冲突严重时的解决办法

上面提到了,哈希桶其实可以看作将大集合的搜索问题转化为小集合的搜索问题了,那如果冲突严重,就意味
着小集合的搜索性能其实也时不佳的,这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,例如:

  1. 每个桶的背后是另一个哈希表
  2. 每个桶的背后是一棵搜索树

五、Java 中 HashMap 的实现

1、定义map结点

HashMap底层使用哈希表,下面就使用哈希表简单实现一下,HashMap 中的 插入查询 方法。首先是定义 map 结点:

public class MyHashMap<K,V> {
    // 定义 map 结点:key-value 模型
    static class Node<K,V> {
        public K key;
        public V value;
        public Node<K,V> next;

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }
    // 创建哈希表,初始容量为 10
    public Node<K,V>[] hashTable = (Node<K, V>[]) new Node[10];
    // 哈希表中有效元素个数
    public int usedSize;

    // 负载因子,此处取0.75(负载因子 = 填入表中元素个数/散列表长度)
    public static final double LOAD_FACTOR = 0.75;
}

2、put 插入方法的实现

再进行插入操作时,我们需要根据结点 key 值计算散列位置,由于 key 不一定为数值类型,我们不能直接使用它散列函数的计算。此时我们需要用到 hashCode() 方法:

hashCode() 方法可以获取对象的哈希码,它是一个整数,用来表示对象的状态,且哈希码具有唯一性。

所以我们可以通过 hashCode() 方法求得 key 的哈希码,然后根据得到的哈希码带入散列函数去计算散列位置。这里需要注意一点是:

使用 hashCode() 获取到的哈希码是一个有符号的整形,这也就意味着得到的哈希码有可能是负数,而哈希表的底层是一个数组,它的下标值不可能为负数,因此需要将得到的哈希码按位与上Integer.MAX_VALUE 也就是 0x7FFFFFFF,使得到的值是一个正数。

	// 1.put(K key,V value):插入操作
    public void put(K key,V value) {
        // 计算 key 值的 hash 值
        int hash = key.hashCode() & Integer.MAX_VALUE;
        // 取留余数法计算散列位置
        int index = hash % hashTable.length;
		// 拿到散列位置的哈希桶
        Node<K,V> cur = hashTable[index];
        // 判断是否已经存在key值(哈希表中不允许存在相同key值)
        while (cur != null) {
            if (cur.key.equals(key)) {
                // 如果存在 key 相同的元素,更新 value 值
                cur.value = value;
                return;
            }
            cur = cur.next;
        }

        // 采用头插法
        Node<K,V> newNode = new Node<>(key, value);
        newNode.next = hashTable[index];
        hashTable[index] = newNode;
        usedSize++;

        // 为减少冲突率,我们要判断负载因子是否超过预设值
        if (calculate_LoadFactor() >= LOAD_FACTOR) {
            // 如果大于等于负载因子,需要扩容
            resize();
        }
    }

	// (1) calculate_LoadFactor():计算此时负载因子
    private double calculate_LoadFactor() {
        return usedSize * 1.0 / hashTable.length;
    }

    // (2) resize():扩容并重新计算哈希值和位置,并存储到新的哈希表中。
    private void resize() {
        // 扩容
        Node<K,V>[] newhashTable = (Node<K, V>[]) new Node[2* hashTable.length];
        // 因为改变了散列表长度,需要重新将每一个结点散列到新的位置
        for (int i = 0; i < hashTable.length; i++) {
            Node<K,V> cur = hashTable[i];
            while (cur != null) {
                // 记录curNext的位置,防止后续调整丢失
                Node<K,V> curNext = cur.next;
                // 重新计算哈希值
                int hash = cur.key.hashCode() & Integer.MAX_VALUE;
                // 根据散列函数计算散列位置
                int index = hash % newhashTable.length;
                // 头插法
                cur.next = newhashTable[index];
                newhashTable[index] = cur;
                cur = curNext;
            }
        }
        // 更新哈希表
        hashTable = newhashTable;
    }

3、get 查询方法的实现

    // 2.get(K key):查询
    public V get(K key) {
        // 计算哈希值
        int hash = key.hashCode() & Integer.MAX_VALUE;
        // 根据散列函数计算散列位置
        int index = hash % hashTable.length;
        // 遍历哈希桶,寻找元素
        Node<K,V> cur = hashTable[index];
        while (cur != null) {
            if (cur.key.equals(key)) {
                // 找到返回 值value
                return cur.value;
            }
            cur = cur.next;
        }
        // 找不到返回 null
        return null;
    }

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

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

相关文章

【C++奇遇记】内存模型

&#x1f3ac; 博客主页&#xff1a;博主链接 &#x1f3a5; 本文由 M malloc 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f384; 学习专栏推荐&#xff1a;LeetCode刷题集 数据库专栏 初阶数据结构 &#x1f3c5; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如…

VB.NET通过VB6 ActiveX DLL调用PowerBasic及FreeBasic动态库

前面说的Delphi通过Activex DLL同时调用PowerBasic和FreeBasic写的DLL&#xff0c;是在WINDOWS基础平台上完成的。 而 .NET平台是架在WINDOWS基础平台之上的&#xff0c;它的上面VB.NET或C#等开发的APP程序&#xff0c;下面写一下用VB.NET&#xff0c;通过VB6注册的Activex DLL…

16.遍历二叉树,线索二叉树

目录 一. 遍历二叉树 &#xff08;1&#xff09;三种遍历方式 &#xff08;2&#xff09;递归遍历算法 &#xff08;3&#xff09;非递归遍历算法 &#xff08;4&#xff09;层次遍历算法 二. 基于递归遍历算法的二叉树有关算法 &#xff08;1&#xff09;二叉树的建立 …

小程序中的页面配置和网络数据请求

页面配置文件和常用的配置项 1.在msg.json中配置window中的颜色和背景色 "navigationBarBackgroundColor": "#efefef","navigationBarTextStyle": "black" 2.可以看到home中的没有发生变化但是msg的发生变化了&#xff0c;这个和前面的…

Mysql查询重复数据常用方法

在平常的开发工作中&#xff0c;我们经常需要查询数据&#xff0c;比如查询某个表中重复的数据&#xff0c;那么&#xff0c;具体应该怎么实现呢&#xff1f;常用的方法都有哪些呢&#xff1f; 测试表中数据&#xff1a; 1&#xff1a;查询名字重复的数据 having&#xff1a; …

面试之快速学习计算机网络-http

1. HTTP常见状态码 2. 3开头重定向&#xff0c;4开头客户端错误&#xff0c;5开头服务端错误 2. HTTP 报文 1. start-line&#xff1a;请求行&#xff0c;可以为以下两者之一&#xff1a; 请求行&#xff1a; GET /hello-world2.html HTTP/1.1状态行&#xff1a;HTTP/1.1 200…

关于模板的大致认识【C++】

文章目录 函数模板函数模板的原理函数模板的实例化模板参数的匹配原则 类模板类模板的定义格式类模板的实例化 非类型模板参数typename 与class模板的特化函数模板特化类模板特化全特化偏特化 模板的分离编译 函数模板 函数模板的原理 template <typename T> //模板参数…

el-input输入框 输入数字中文 来回切换之后 监听失效问题如何解决

实现一个vue自定义指令——输入框&#xff08;input,el-input&#xff09;输入内容类型限制&#xff0c;解决中文输入法双向绑定失效问题&#xff0c;多种类型支持&#xff0c;数字类型&#xff0c;浮点类型、英文类型、整数类型、四则运算等 直接上代码 首先新建input.js ex…

Git如何操作本地分支仓库?

基本使用TortoiseGit 操作本地仓库(分支) 分支的概念 几乎所有的版本控制系统都以某种形式支持分支。 使用分支意味着你可以把你的工作从开发主线上分离开来&#xff0c;避免影响开发主线。多线程开发,可以同时开启多个任务的开发&#xff0c;多个任务之间互不影响。 为何要…

10个好用的网络画图工具推荐,专业办公绘图必备!

在当今数字化时代&#xff0c;网络画图工具成为了各行各业的重要辅助工具。无论是制作流程图、思维导图、原型设计&#xff0c;还是插图绘制、数据可视化&#xff0c;网络画图工具为用户提供了便捷、高效的创作平台。本文将向大家推荐10个好用的网络画图工具&#xff0c;帮助你…

深度解析淘宝API商品评论接口的实现原理与使用方法

淘宝API商品评论接口&#xff0c;主要用于获取某个商品的评价信息。通过该接口&#xff0c;我们可以获取到商品的所有评价内容、评价时间、评价等级等相关信息&#xff0c;帮助我们更好地了解用户对商品的反馈&#xff0c;进而进行数据分析和业务优化。 一、接口鉴权 在使用淘…

波奇学C++:stl的list模拟实现

list是双向带头链表。所以迭代器end()相当于哨兵卫的头。 list不支持和[]重载&#xff0c;原因在于list空间不是连续的&#xff0c;和[]的代价比较大。 访问第n个节点&#xff0c;只能用for循环&#xff0c;来实现 list<int> l; l.push_back(0); l.push_back(1); l.pu…

代码随想录算法训练营之JAVA|第三十五天|343. 整数拆分

今天是第 天刷leetcode&#xff0c;立个flag&#xff0c;打卡60天&#xff0c;如果做不到&#xff0c;完成一件评论区点赞最高的挑战。 算法挑战链接 343. 整数拆分https://leetcode.cn/problems/integer-break/ 第一想法 题目理解&#xff1a;将一个整数拆分为k个整数&…

python matlab 画坐标图

画一个坐标系&#xff0c;同时显示两条直线&#xff0c;效果图如下&#xff1a; 功能点&#xff1a; 同时显示两个纵坐标数据 显示图片名称 图片最大化保存 到本地 在图片某个位置显示字符信息 不同的线名称提示 代码如下&#xff1a; import matplotlib.pyplot as pltde…

学习左耳听风栏目90天——第七天 7/90(学习左耳朵耗子的工匠精神,对技术的热爱)【每个程序员都该知道的事】

每个程序员都该知道的事 每个程序员都应该要读的书每个搞计算机专业的学生应有的知识LinkedIn 高效的代码复查技巧编程语言和代码质量的研究报告 每个程序员都应该要读的书 每个搞计算机专业的学生应有的知识 LinkedIn 高效的代码复查技巧 编程语言和代码质量的研究报告

C++头文件和std命名空间

C 是在C语言的基础上开发的&#xff0c;早期的 C 还不完善&#xff0c;不支持命名空间&#xff0c;没有自己的编译器&#xff0c;而是将 C 代码翻译成C代码&#xff0c;再通过C编译器完成编译。 这个时候的 C 仍然在使用C语言的库&#xff0c;stdio.h、stdlib.h、string.h 等头…

CentOS7网络配置

本文是我从另外三个文章中整合而来&#xff0c;用于自存&#xff0c;如有侵权请联系我删除。 CentOS 7教程&#xff08;二&#xff09;-网络设置 - 知乎 (zhihu.com) VMware安装、Linux下CentOS7的配置及网络环境的配置&#xff08;最新版特别全&#xff09;_centos7 配置_Co…

田间气象站的优势与应用

在农业生产中&#xff0c;田间气象站是重要的气象监测工具&#xff0c;它能够对农田间的气象信息进行实时监测和记录&#xff0c;为农民伯伯提供农业生产科学依据。 田间气象站是由多个传感器共同组成&#xff0c;能够收集各项气象参数&#xff0c;包括我们常见的风速、风向、…

STM32--MPU6050与I2C外设

文章目录 前言MPU6050参数电路MPU6050框图 IIC外设框图 IIC的基本结构软件IIC实现MPU6050硬件IIC实现MPU6050 前言 在51单片机专栏中&#xff0c;用过I2C通信来进行实现AT24C02的数据存储&#xff1b; 里面介绍的是利用程序的编程来实现I2C的时序&#xff0c;进而实现AT24C02与…

关于android studio 几个简单的问题说明

自信是成功的第一步。——爱迪生 1. android studio 如何运行不同项目是否要更换不同的sdk 和 gradle 2.编译Gradle总是错误为什么 3.如何清理android studio 的缓存 4. 关于android Studio中的build 下面的rebuild project