哈希表(极速学习版)

哈希表的定义与实现

概述

哈希表是一种高效的数据结构,它提供了快速的数据插入、删除和查找操作。

通过使用哈希函数,哈希表将输入的键映射到一个指定位置(索引)以快速访问存储在该位置的值。

哈希表通常用于实现字典、集合、数据库索引等功能。

关键概念:

  1. 哈希函数(Hash Function):将任意大小的输入(键)通过某种算法转换为固定大小输出(哈希值)的函数。理想的哈希函数应具有确定性、均匀分布和快速计算的特性。

    一个简单的哈希函数可以是对键取模数组长度:

int hashFunction(int key, int arraySize) {return key % arraySize;
}
  1. 哈希值(Hash Value):哈希函数的输出,用于确定键在哈希表中的存储位置。

  2. 冲突(Collision):当两个不同的键映射到同一哈希值时发生冲突。

  3. 冲突解决策略:解决键冲突的方法。常见策略包括:

    • 链地址法(Chaining):每个哈希值对应一个链表,冲突的键存储在同一链表中。
    • 开放寻址法(Open Addressing):冲突时寻找表中的下一个空闲位置。
    • 双重哈希(Double Hashing):使用第二个哈希函数来确定下一个探查位置。

哈希表的冲突解决方法

  1. 链地址法 (Chaining)
    • 描述:在这种方法中,每个哈希表的桶(或槽)保存一个链表。每当发生冲突时,新的键值对就会被附加到对应桶的链表中。这意味着同一个哈希值的多个元素可以存在于同一个链表中。
    • 优点
      • 动态扩展能力强,因为链表可以根据需要增加节点,理论上不受负载因子的限制。
      • 实现相对简单。
    • 缺点
      • 在极端情况下(如所有键都哈希到同一位置),性能会下降到 O(n)。
      • 需要额外的指针空间来存储链表。
import java.util.LinkedList;class HashTableChaining {private LinkedList<Node>[] table;private int size;class Node {String key;String value;Node(String key, String value) {this.key = key;this.value = value;}}// 初始化哈希表public HashTableChaining(int size) {this.size = size;table = new LinkedList[size];for (int i = 0; i < size; i++) {table[i] = new LinkedList<>();}}// 哈希函数private int hash(String key) {return key.hashCode() % size;}// 插入元素public void put(String key, String value) {int index = hash(key);LinkedList<Node> list = table[index];// 更新已存在的键for (Node node : list) {if (node.key.equals(key)) {node.value = value;return;}}// 插入新节点list.add(new Node(key, value));}// 获取元素public String get(String key) {int index = hash(key);LinkedList<Node> list = table[index];for (Node node : list) {if (node.key.equals(key)) {return node.value;}}return null; // 未找到}
}
  1. 开放寻址法 (Open Addressing)
    • 描述:与链地址法不同,这种方法在发生冲突时,通过特定探查策略(如线性探查、平方探查等)找到哈希表中的下一个空位来存储新的键值对。
    • 探查方式
      • 线性探查:每次冲突时,检查下一个位置(i+1、i+2等)直到找到空位。
      • 平方探查:探查位置是基于冲突发生次数的平方,比如 i^2。
    • 优点
      • 避免了额外存储链表的开销,存储更加紧凑。
      • 不需要额外的指针或链表结构。
    • 缺点
      • 随着负载因子的增加,查找、插入和删除的性能会明显下降,可能达到 O(n)。
      • 一旦表格接近满,聚集现象会增加查找冲突的概率。
class HashTableOpenAddressing {private String[] table;private int size;// 初始化哈希表public HashTableOpenAddressing(int size) {this.size = size;table = new String[size];}// 哈希函数private int hash(String key) {return key.hashCode() % size;}// 插入元素public void put(String key) {int index = hash(key);while (table[index] != null) {index = (index + 1) % size; // 线性探查}table[index] = key; // 当前位置为空,插入}// 获取元素public String get(String key) {int index = hash(key);int initialIndex = index;while (table[index] != null) {if (table[index].equals(key)) {return table[index];}index = (index + 1) % size; // 继续探查if (index == initialIndex) break; // 回到起始位置,表已循环}return null; // 未找到}
}
  1. 双重哈希 (Double Hashing)
    • 描述:这是开放寻址法的扩展,使用第二个哈希函数来计算步长,以确定下一个探查位置。即在第一次冲突后,使用第二个哈希函数计算步长。
    • 公式:设 h1(k)h2(k) 是两个哈希函数,则探查序列为 h(k, i) = (h1(k) + i * h2(k)) % m,其中 m 是哈希表的大小,i 是探查次数。
    • 优点
      • 能有效减少聚集现象,提高性能,特别是在负载因子较高时。
      • 可以使用多个哈希函数对同一个键进行查找。
    • 缺点
      • 实现相对复杂,需要管理两个哈希函数。
      • 如果选择的哈希函数不好,依然可能导致性能下降。
class HashTableDoubleHashing {private String[] table;private int size;// 初始化哈希表public HashTableDoubleHashing(int size) {this.size = size;table = new String[size];}// 第一个哈希函数private int hash1(String key) {return key.hashCode() % size;}// 第二个哈希函数private int hash2(String key) {return 1 + (key.hashCode() % (size - 1)); // 确保不为零}// 插入元素public void put(String key) {int index = hash1(key);int stepSize = hash2(key);while (table[index] != null) {index = (index + stepSize) % size; // 双重哈希}table[index] = key; // 当前位置为空,插入}// 获取元素public String get(String key) {int index = hash1(key);int stepSize = hash2(key);int initialIndex = index;while (table[index] != null) {if (table[index].equals(key)) {return table[index];}index = (index + stepSize) % size; // 继续探查if (index == initialIndex) break; // 回到起始位置,表已循环}return null; // 未找到}
}

性能分析

  • 时间复杂度:

    • 最优情况下(无碰撞):O(1)(插入、查找、删除)
    • 最坏情况下(所有元素都散列到同一索引):O(n),其中 n 是元素数量。
  • 空间复杂度: O(n),用于存储 n 个元素。

数学过程

  1. 选择或设计哈希函数:根据键的特性选择或设计合适的哈希函数。
    例如,对于整数键,可以直接使用键值作为哈希值;
    对于字符串键,可以使用某种算法(如除留余数法、折叠法、乘法哈希等)来计算哈希值。

  2. 计算哈希值:对于给定的键,使用哈希函数计算其哈希值。例如,对于字符串键 “Kimi”,简单的哈希函数可以是:
    H ( K ) = ∑ i = 1 n K i × p i − 1 m o d m H(K) = \sum_{i=1}^{n} K_i \times p^{i-1} \mod m H(K)=i=1nKi×pi1modm
    其中, K i K_i Ki 是字符串中的第 i i i个字符的 ASCII 值, p p p 是一个质数(如 31), m m m 是哈希表的大小。

设计哈希函数

1. 留余数法(Modulus Method)

定义: 留余数法是最简单的哈希函数之一,直接利用模运算将输入映射到数组索引。

公式:
index = key m o d array_size \text{index} = \text{key} \mod \text{array\_size} index=keymodarray_size

特点:

  • 简单易懂: 实现起来非常简单,计算速度快。
  • 适用性: 适用于整数类型的键。

示例代码:

int hashFunction(int key, int arraySize) {return key % arraySize;
}

优点:

  • 实现简单。
  • 可以快速得出索引。

缺点:

  • 散列性能依赖于 array_size 的选择。如果大小不是质数,可能会导致较多的碰撞。
2. 折叠法(Folding Method)

定义: 折叠法将键划分为几个部分,然后通过对这些部分进行求和(或其他操作)来生成哈希值。这种方法适合较长的键(如字符串或复合数据)。

实现步骤:

  1. 将键分成若干个部分(常常是相等长度的子串)。
  2. 将这些部分转化为整数值。
  3. 将所有整数值相加(或其他组合),然后取模得到数组索引。

示例:
假设有一个字符串键 “12345678”:

  • 分为 “123”、“456” 和 “78”。
  • 将其各部分转化为整数并求和。
  • 最后对数组大小取模得到索引。

优点:

  • 可以有效处理长键。
  • 更均匀地分布哈希值,减少碰撞。

缺点:

  • 实现较复杂,可能需要处理更多的字符串操作。
3. 乘法哈希(Multiplicative Hashing)

定义: 乘法哈希利用乘法运算来生成哈希值,这种方法也可以适用于任何类型的键。它涉及将键乘以一个常数,然后取哈希值的特定部分。

实现步骤:

  1. 选择一个常数 ( A )(通常是大于 0 但小于 1 的正数)。
  2. 计算哈希值:
    h ( k ) = ⌊ m ⋅ ( k ⋅ A m o d 1 ) ⌋ h(k) = \lfloor \text{m} \cdot (k \cdot A \mod 1) \rfloor h(k)=m(kAmod1)⌋
    其中 m \text{m} m 是哈希表的大小。

示例代码:

int multiplicativeHash(int key, int arraySize) {double A = (Math.sqrt(5) - 1) / 2;  // 常数return (int) (arraySize * ((key * A) % 1));
}

优点:

  • 更加均匀地分布哈希值,从而减少碰撞。
  • 适用于整数和字符串等不同类型的键。

缺点:

  • 相对实现更加复杂。
  • 需要选择合适的常数 A A A
总结
  • 留余数法:简单易用,适合整数,但对数组大小选择敏感。
  • 折叠法:适合处理长键,具有较好的分布性,但实现较复杂。
  • 乘法哈希:能够生成更均匀的哈希值,适用性广,但实现相对复杂。

哈希表动态调整

为什么需要动态调整

在使用哈希表时,随着数据的插入或删除,哈希表的负载因子(即表中元素数量与数组长度的比例)可能会变化。

当负载因子过高时,表会过于拥挤,导致冲突的增加,进而影响查找、插入和删除操作的效率。为了保持哈希表的性能,通常会在以下情况下进行动态调整:

  • 扩展(Resize Up):当负载因子超过某个阈值时(如 0.7),为了减少冲突,哈希表会扩展到一个更大的数组。
  • 缩减(Resize Down):当负载因子低于某个阈值时(如 0.2),为了节省空间,哈希表会缩减到一个更小的数组。

动态调整的过程

  1. 确定新大小:计算新哈希表的大小,通常是当前大小的两倍或减半。
  2. 创建新数组:初始化一个新的数组,大小为新计算的大小。
  3. 重新哈希:将原数组中的每个元素重新映射到新数组中,使用新的哈希函数(通常是相同的哈希函数,因为元素的数量变化了)。
    • 遍历原数组中的每一个元素,计算其新的哈希值,并插入到新数组中。由于数组大小改变,可能会导致哈希值的计算方式改变,因此每个元素需要重新计算其位置。
  4. 替换旧数组:将旧的哈希表数组替换为新的数组,并更新相应的控制变量(如当前大小、负载因子等)。

动态调整的复杂度

  • 扩展和缩减操作:在哈希表动态调整时,重新哈希的时间复杂度为 O(n),其中 n 是当前哈希表中的元素数量,因为每个元素都需要重新计算其位置。
  • 插入/删除操作:在平均情况下,插入和删除操作的时间复杂度为 O(1),但在动态调整时,会变为 O(n),所以动态调整的代价可能会影响到性能。

示例

import java.util.Arrays;public class HashTable {private int size; // 哈希表的大小private int count; // 当前存储的键值对数量private Entry[] table; // 存储键值对的数组// 内部类表示一个键值对private static class Entry {String key; // 存储的键Object value; // 存储的值Entry(String key, Object value) {this.key = key;this.value = value;}}// 构造函数,初始化哈希表,指定初始大小public HashTable(int initialSize) {this.size = initialSize; // 设置初始大小this.count = 0; // 初始化计数为0this.table = new Entry[size]; // 创建存储数组}// 默认构造函数,设置默认大小为8public HashTable() {this(8);}// 计算哈希值private int hash(String key) {return Math.abs(key.hashCode() % size); // 使用 hashCode 的绝对值与当前大小求余}// 插入键值对public void insert(String key, Object value) {// 检查负载因子(load factor),如果超出0.7则扩展哈希表if ((double) count / size > 0.7) {resize(size * 2); // 将哈希表大小扩大至两倍}int index = hash(key); // 计算哈希值得到索引while (table[index] != null) { // 线性探测冲突index = (index + 1) % size; // 在当前位置已被占用,检查下一个位置}table[index] = new Entry(key, value); // 将键值对插入count++; // 增加当前计数}// 扩展哈希表大小,并重新插入项private void resize(int newSize) {Entry[] oldTable = table; // 保存旧的哈希表size = newSize; // 设置新的大小table = new Entry[newSize]; // 创建新的哈希表count = 0; // 重置计数// 将旧哈希表中的所有项重新插入新表for (Entry entry : oldTable) {if (entry != null) {insert(entry.key, entry.value); // 重新插入}}}// 根据键获取值public Object get(String key) {int index = hash(key); // 计算哈希值while (table[index] != null) { // 线性探测查找if (table[index].key.equals(key)) { // 找到对应的键return table[index].value; // 返回值}index = (index + 1) % size; // 检查下一个位置}return null; // 未找到}// 主方法,测试哈希表功能public static void main(String[] args) {HashTable hashTable = new HashTable(); // 创建哈希表实例hashTable.insert("key1", "value1"); // 插入键值对hashTable.insert("key2", "value2"); // 插入键值对// 测试获取功能System.out.println("Get key1: " + hashTable.get("key1")); // 输出: value1System.out.println("Get key2: " + hashTable.get("key2")); // 输出: value2System.out.println("Get key3: " + hashTable.get("key3")); // 输出: null}
}

使用场景

哈希表适合以下情况:

  • 需要快速查找数据的场合,如缓存、字典。
  • 处理大量唯一数据,如用户标识、商品编号。
  • 需要快速统计频次,如单词频率统计。

实际应用案例:任务管理器

在开发一个任务管理器应用时,哈希表(如 HashMap)可以用于高效管理任务。在该应用中,用户可以添加、更新和删除任务,并能够快速查找特定任务。任务信息包括任务ID、名称、优先级、截止日期等。

具体实现

  1. 数据结构选择:使用 HashMap 存储任务。键为任务ID,值为任务对象,包含所有相关信息。

  2. 操作示例

    • 添加任务
      HashMap<Integer, Task> taskMap = new HashMap<>();
      Task newTask = new Task(1, "完成报告", "高", "2024-11-30");
      taskMap.put(newTask.getId(), newTask);
      
    • 查找任务
      Task taskToFind = taskMap.get(1); // O(1) 时间复杂度
      if (taskToFind != null) {System.out.println("找到任务: " + taskToFind.getName());
      }
      
    • 更新任务
      if (taskMap.containsKey(1)) {Task taskToUpdate = taskMap.get(1);taskToUpdate.setPriority("中");
      }
      
  3. 优势分析

    • 快速查找:使用哈希表后,查找响应速度显著提升,平均响应时间从几十毫秒降低到几毫秒,用户体验大幅改善。
    • 简洁的代码:清晰的代码结构使得任务的增删改查逻辑更加易于维护。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

云服务器部署WebSocket项目

WebSocket是一种在单个TCP连接上进行全双工通信的协议&#xff0c;其设计的目的是在Web浏览器和Web服务器之间进行实时通信&#xff08;实时Web&#xff09; WebSocket协议的优点包括&#xff1a; 1. 更高效的网络利用率&#xff1a;与HTTP相比&#xff0c;WebSocket的握手只…

PDF内容提取,MinerU使用

准备环境 # python 3.10 python3 -m pip install huggingface_hub python3 -m pip install modelscope python3 -m pip install -U magic-pdf[full] --extra-index-url https://wheels.myhloli.com下载需要的模型 import json import osimport requests from huggingface_hub…

掌握 Spring 事务管理:深入理解 @Transactional 注解

在业务方法上使用Transactional开启声明式事务时&#xff0c;很有可能由于使用方式有误&#xff0c;导致事务没有生效。 环境准备 表结构 CREATE TABLE admin (id bigint(20) unsigned NOT NULL AUTO_INCREMENT,username varchar(255) DEFAULT NULL,password varchar(255) …

设计模式之 观察者模式

观察者模式&#xff08;Observer Pattern&#xff09;是一种行为型设计模式&#xff0c;它定义了一种一对多的依赖关系&#xff0c;让多个观察者对象同时监听一个主题对象&#xff08;Subject&#xff09;。当主题对象的状态发生变化时&#xff0c;所有依赖于它的观察者都会得到…

【python】将word文档内容转换为excel表格

在日常工作中&#xff0c;我们经常需要将Word文档中的内容提取并转换为Excel表格&#xff0c;以便进行数据分析和处理。本文将介绍如何使用Python编写一个简单的程序&#xff0c;将Word文档中的内容转换为Excel表格。 一.实例 使用以下word文档作为例子&#xff1a; 工具界面如…

Linux|进程程序替换

目录 什么是进程替换 替换原理 exec函数 exec* 函数的共性 什么是进程替换 进程程序替换是指将一个进程中正在运行的程序替换为另一个全新的程序的过程&#xff0c;但替换不是创建新进程&#xff0c;只是将对应程序的代码和数据进行替换。具体来说&#xff0c;这个替换过程涉…

大数运算(加减乘除和输入、输出模块)

为什么会有大数呢&#xff1f;因为long long通常为64位范围约为 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807&#xff0c;最多也就19位&#xff0c;那么超过19位的如何计算呢&#xff1f;这就引申出来大数了。 本博客适合思考过这道题&#xff0c;但是没做出来或…

IntelliJ+SpringBoot项目实战(四)--快速上手数据库开发

对于新手学习SpringBoot开发&#xff0c;可能最急迫的事情就是尽快掌握数据库的开发。目前数据库开发主要流行使用Mybatis和Mybatis Plus,不过这2个框架对于新手而言需要一定的时间掌握&#xff0c;如果快速上手数据库开发&#xff0c;可以先按照本文介绍的方式使用JdbcTemplat…

flex布局 昵图网【案例】

效果展示 只是个大概&#xff0c;可自己完善。 昵图网 代码展示 <body><!-- https://static.ntimg.cn/original/images/soso.png --><div class"container"><div class"header"><!-- <div class"logo"><i…

[第五空间 2021]pklovecloud 详细题解

知识点: 构造POP链 PHP类的作用域 NULL强比较 目录穿越 源码如下: <?php include flag.php; class pkshow { function echo_name() { return "Pk very safe^.^"; } } class acp { protected $cinder; public $neutron;public $n…

dockerfile构建Nginx镜像练习二(5-2)

环境准备&#xff1a; (1)保证拥有centos基础镜像 docker images | grep centos (2)服务器保证可以连接外网 1.创建工作目录 mkdir nginx cd nginx 2.在工作目录中创建并编写Dockerfile文件 vim dockerfile #定义基础镜像 FROM centos:7#维护者信息(可缺省) MAINTAINER d…

Android Surfaceflinger显示图层合成方式

Android SurfaceFlinger是Android系统中负责窗口管理和图像合成的核心组件。它接收来自不同应用的图层数据&#xff0c;并将这些图层合并成一个单一的图像&#xff0c;然后输出到显示设备上。SurfaceFlinger的合成方式主要涉及两种&#xff1a;Client合成和Device合成。 adb s…

wsl安装

一. wsl简介 1. wsl和wsl2的区别 wsl需要把linux命令翻译为windows命令&#xff0c;性能差一些。 wsl2直接使用linux内核&#xff0c;不需要翻译&#xff0c;性能好&#xff0c;但开销相对大一点&#xff0c;因为需要多运行一个hyper-v虚拟机 (并非完整的虚拟机&#xff0c;是…

任务通知的本质(任务通知车辆运行) 软件定时器的本质(增加游戏音效)

任务通知的本质 没有任务通知 所谓"任务通知"&#xff0c;你可以反过来读"通知任务"。 我们使用队列、信号量、事件组等等方法时&#xff0c;并不知道对方是谁。使用任务通知时&#xff0c;可 以明确指定&#xff1a;通知哪个任务。 使用队列、信号量、…

Kubernetes的pod控制器

文章目录 一&#xff0c;什么是pod控制器二&#xff0c;pod控制器类型&#xff08;重点&#xff09;1.ReplicaSet2.Deployment3.DaemonSet4.StatefulSet5.Job6.Cronjob 三&#xff0c;pod与控制器的关系1.Deployment2.SatefulSet2.1StatefulSet组成2.2headless的由来2.3有状态服…

【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录

背景 Jetbrain IDE 支持生成 Test 类&#xff0c;其中选择JUnit5 和 JUnit&#xff0c;但是感觉这不是标准的单元测试&#xff0c;因为接口命名吧。 差异对比 两者生成的单测API名称同原API&#xff0c;没加test前缀的。使用差异主要表现在&#xff1a; setUp &#xff06; …

知识中台在多语言客户中的应用

在全球化的商业环境中&#xff0c;企业面临着多语言客户服务的挑战。HelpLook知识中台作为一种智能化解决方案&#xff0c;为企业提供了一个强大的工具&#xff0c;以实现多语言客户服务的自动化和优化。 一、多语言客户服务的重要性 多语言客户服务对于跨国企业至关重要&…

使用 Elastic AI Assistant for Search 和 Azure OpenAI 实现从 0 到 60 的转变

作者&#xff1a;来自 Elastic Greg Crist Elasticsearch 推出了一项新功能&#xff1a;Elastic AI Assistant for Search。你可以将其视为 Elasticsearch 和 Kibana 开发人员的内置指南&#xff0c;旨在回答问题、引导你了解功能并让你的生活更轻松。在 Microsoft AI Services…

【K8S问题系列 |18 】如何解决 imagePullSecrets配置正确,但docker pull仍然失败问题

如果 imagePullSecrets 配置正确&#xff0c;但在执行 docker pull 命令时仍然失败&#xff0c;可能存在以下几种原因。以下是详细的排查步骤和解决方案。 1. 检查 Docker 登录凭证 确保你使用的是与 imagePullSecrets 中相同的凭证进行 Docker 登录&#xff1a; 1.1 直接登录…

Redis的特性ubuntu进行安装

文章目录 1.六大特性1.1内存存储数据1.2可编程1.3可扩展1.4持久化1.5集群1.6高可用1.7速度快 2.具体应用场景&#xff08;了解&#xff09;3.Ubuntu安装Redis3.1安装指令3.2查看状态3.3查找配置文件3.4修改文件内容3.5重启服务器生效3.6安装客户端并进行检查 4.Redis客户端介绍…