【数据结构】哈希表的理论与实现 - 教程

news/2025/11/19 23:16:21/文章来源:https://www.cnblogs.com/tlnshuju/p/19244377

【数据结构】哈希表的理论与实现 - 教程

2025-11-19 23:11  tlnshuju  阅读(0)  评论(0)    收藏  举报

哈希表的理论

哈希表是一种基于哈希函数实现高效查找的数据结构,其核心思想是通过哈希函数将关键字映射到存储位置。

哈希表示例分析

12 18 21 24 33 45 67 72

214524181233

哈希函数:除数留余法

数值哈希计算 (mod 7)结果位置冲突情况
1212 % 75与33冲突
3333 % 75与12冲突
1818 % 74-
4545 % 73与24冲突
2121 % 70-
2424 % 73与45冲突

哈希冲突/哈希碰撞了。
解决办法:1、线性探测法 2、链地址法

哈希表的搜索操作: O(1)

18 % 7 = 4 arr[4] 18

45 % 7 = 3 arr[3]

45 % 7 = 3 arr[3] ≠ 45 产生哈希冲突了,线性探测法继续找 O(1) → O(n)

哈希冲突是不可避免的,怎么减少哈希冲突?(下面讲的是线性探测法)

1、哈希函数。 如,除留余数法 让哈希表(桶)的长度:素数

2、哈希表的装载因子。 loadfactor = 已占用的桶的个数/桶的总个数 > 阈值(0.75)——→ 哈希表就需要扩容了。相当于数组的扩容(对于线性探测哈希表来说),原来哈希表中的元素,需要在新的哈希表中重新哈希。—— O(n)

均摊时间复杂度 O(1)

线性探测哈希表

增加元素:

通过哈希函数计算数据存放的位置

该位置空闲,直接存储元素,完成

该位置被占用,从当前位置向后找空闲的位置,存放该元素

查询元素:

通过哈希函数计算数据存放的位置,从该位置取值(判断状态 STATE_USEING)

该值==要查询的元素值,找到了!

该值 ≠ 要查询的元素值(之前往这个位置放元素时,发生哈希冲突了),继续遍历往后找该元素

【补充】往后遍历到什么时候结束呢?

位置是空的有两种情况:1、这个位置是空的,没放过元素 (不需要继续往后搜索) 2、这个位置是空的,以前放过元素,后来被删除了(需要继续往后搜索)

会发现桶里面只放元素是不行的,还要放桶的状态

struct Node{

int val;

State state; //当前位置的状态

}

enumState{

STATE_USEING, //正在使用

STATE_UNUSE, //从来没用过

STATE_DEL //当前位置的元素被删除

}

删除元素:

通过哈希函数计算数据存放的位置,从该位置取值,判断状态STATE_USING

该值==要删除的值,直接修改当前位置的状态就可以 STATE_DEL

该值 ≠ 要删除的值,继续往后遍历,找到该元素,修改状态,如果遇到 STATE_UNUSE,结束

实现:

#include <iostream>using namespace std;enum State{STATE_UNUSE,    //从未使用过的桶STATE_USING,    //正在使用的桶STATE_DEL,      //元素被删除了的桶};//桶的类型struct Bucket{Bucket(int key = 0, State state = STATE_UNUSE): key_(key), state_(state){}int key_;     //存储的数据State state_;     //桶的当前状态};//线性探测哈希表类型class HashTable{public:HashTable(int size = primes_[0], double loadFactor = 0.75): useBucketNum_(0), loadFactor_(loadFactor), primeIdex_(0){//把用户传入的size调整到最近的比较大的素数上if(size != primes_[0]){for(; primeIdex_ < PRIME_SIZE; primeIdex_++){if(primes_[primeIdex_] > size)break;}//用户传入的size过大,已经超过最后一个素数,调整到最会一个素数if(primeIdex_ == PRIME_SIZE){primeIdex_--;}}tableSize_ = primes_[primeIdex_];table_ = new Bucket[tableSize_];}~HashTable(){delete[]table_;table_ = nullptr;}public://插入元素bool insert(int key){//考虑扩容double factor = useBucketNum_*1.0 / tableSize_;cout << "factor:" << factor << endl;if(factor > loadFactor_){//哈希表开始扩容expand();}int idx = key % tableSize_;int i = idx;do{if(table_[i].state_ != STATE_USING){table_[i].state_ = STATE_USING;table_[i].key_ = key;useBucketNum_++;return true;}i = (i+1)%tableSize_;}while(i != idx);return false;}//删除元素bool erase(int key){int idx = key % tableSize_;int i = idx;do{if(table_[i].state_ == STATE_USING && table_[i].key_ == key){table_[i].state_ = STATE_DEL;useBucketNum_--;}i = (i+1) % tableSize_;}while(table_[i].state_ != STATE_UNUSE && i != idx);return true;}//查询bool find(int key){int idx = key % tableSize_;int i = idx;do{if(table_[i].state_ == STATE_USING && table_[i].key_ == key){return true;}i = (i+1) % tableSize_;}while(table_[i].state_ != STATE_UNUSE && i != idx);return false;}private:void expand(){++primeIdex_;if(primeIdex_ ==  PRIME_SIZE){throw "HashTable is too large! can not expand anymore!";}Bucket* newTable = new Bucket[primes_[primeIdex_]];for(int i = 0; i < tableSize_; i++){if(table_[i].state_ == STATE_USING){   //旧表有效数据放到新表int idx = table_[i].key_ % primes_[primeIdex_];int k = idx;do{if(newTable[k].state_ != STATE_USING){newTable[k].state_ = STATE_USING;newTable[k].key_ = table_[i].key_;break;}k = (k+1) % primes_[primeIdex_];}while(k != idx);}}delete[]table_;table_ = newTable;tableSize_ = primes_[primeIdex_];}private:Bucket* table_;      //指向动态开辟的哈希表int tableSize_;      //哈希表当前的长度int useBucketNum_;   //已经使用的桶的个数double loadFactor_;  //哈希表的装载因子static const int PRIME_SIZE = 10;   //素数表的大小static int primes_[PRIME_SIZE];    //素数表int primeIdex_;                   //当前使用的素数下标};int HashTable::primes_[PRIME_SIZE] = {3, 7, 23, 47, 97, 251, 443, 911, 1471, 42773};int main(){HashTable htable;htable.insert(21);htable.insert(32);htable.insert(14);htable.insert(15);htable.insert(22);cout << htable.find(14) << endl;htable.erase(14);cout << htable.find(14) << endl;return 0;}

链式哈希表

线性探测哈希表的缺陷:

1、发生哈希冲突时,靠近O(n)的时间复杂度,存储变慢

2、多线程环境中,线性探测所用到的基于数组实现的哈希表,只能给全局的表用互斥锁来保证哈希表的原子操作,保证线程安全!

链式哈希表可以用:分段的锁!既保证了线程安全,又有一定的并发量,提高了效率!

例如:12 18 21 24 33 45 67 72 哈希函数采用除留余数法,哈希表长度7
在这里插入图片描述

哈希表O(1) 无线趋近于O(1)—→哈希冲突的存在

每个桶的链表比较长,链表搜索花费的时间就大

优化一:当链表长度大于某个阈值时,把桶里面的这个链表转化成红黑树(搜索时间复杂度O(logn))

优化二:链式哈希表每个桶都可以创建自己的互斥锁,不同桶中的链表操作,可以互斥起来

实现:

#include <iostream>#include <vector>#include <list>#include <algorithm>using namespace std;//链式哈希表class HashTable{public:HashTable(int size = primes_[0], double loadFactor = 0.75): useBucketNum_(0), loadFactor_(loadFactor), primeIdex_(0){if(size != primes_[0]){for(; primeIdex_ < PRIME_SIZE; primeIdex_++){if(primes_[primeIdex_] >= size){break;}}if(primeIdex_ == PRIME_SIZE)primeIdex_--;}table_.resize(primes_[primeIdex_]);}public://增加元素  不能重复插入keyvoid insert(int key){//判断扩容double factor = useBucketNum_*1.0/table_.size();cout << "factor:" << factor << endl;if(factor > loadFactor_){expand();}int idx = key % table_.size();if(table_[idx].empty()){useBucketNum_++;table_[idx].emplace_front(key);}else{//使用全局的::find泛型算法,而不是调用自己的成员方法auto it = ::find(table_[idx].begin(), table_[idx].end(), key);if(it == table_[idx].end()){//key不存在table_[idx].emplace_front(key);}}}//删除元素void erase(int key){int idx = key % table_.size();auto it = ::find(table_[idx].begin(), table_[idx].end(), key);if(it != table_[idx].end()){table_[idx].erase(it);if(table_[idx].empty()){useBucketNum_--;}}}//搜索元素bool find(int key){int idx = key % table_.size();auto it = ::find(table_[idx].begin(), table_[idx].end(), key);return it != table_[idx].end();}private://扩容函数void expand(){if(primeIdex_ + 1 ==  PRIME_SIZE){throw "HashTable is too large! can not expand anymore!";}primeIdex_++;useBucketNum_ = 0;vector<list<int>> oldTable;//swap交换两个容器的成员变量(两个容器Allocator一样时),不涉及数据拷贝,效率很高 table_.swap(oldTable);   //table_ 与 oldTable交换后,table_变为空table_.resize(primes_[primeIdex_]);for(auto list : oldTable){for(auto key : list){int idx = key % table_.size();if(table_[idx].empty()){useBucketNum_++;}table_[idx].emplace_front(key);}}}private:vector<list<int>> table_;int useBucketNum_;      //记录使用的桶的个数double loadFactor_;     //记录哈希表的装载因子static const int PRIME_SIZE = 10;   //素数表的大小static int primes_[PRIME_SIZE];    //素数表int primeIdex_;                 //当前使用的素数下标};int HashTable::primes_[PRIME_SIZE] = {3, 7, 23, 47, 97, 251, 443, 911, 1471, 42773};int main(){HashTable htable;htable.insert(21);htable.insert(32);htable.insert(14);htable.insert(15);htable.insert(22);htable.insert(23);cout << htable.find(15) << endl;htable.erase(15);cout << htable.find(15) << endl;return 0;}

哈希表总结

哈希表的核心定义:存储位置=f(关键字) 一个关键字通过散列函数进行映射,得到其存储位置。这种技术称为散列技术。f称为哈希函数或者散列函数。采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为哈希表。

优势:快速查找,时间复杂度O(1)

缺点:链式哈希表每一个节点既要存数据,又要存地址,内存空间占用了比较大。空间换时间。

散列函数:

设计特点: 计算简单(复杂度会降低查找的时间)、散列地址分布均匀(减少哈希冲突)

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

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

相关文章

在阿里云上部署Redis

首先确定一个下载目录:cd /user #进入usr文件夹 mkdir redis #创建redis文件包下载redis安装包:wget http://download.redis.io.release.redis-6.0.1.tar.gz #获取redis安装包解压安装包tar -xzvf redis-6.0.1.tar.gz …

pip安装第三方包

https://pypi.tuna.tsinghua.edu.cn/simple

李克特量表(Likert scale)

李克特量表(Likert scale)是社会科学和市场研究中最常用的一种量表形式,用于衡量态度、意见或感知等心理变量。 核心特点对称与平衡: 量表通常是对称且平衡的,即积极和消极的选项数量相等,中间有一个中性点。 等…

java---maven

java及maven: 前提是已经安装了java:下载maven:https://maven.apache.org/然后下载:https://maven.apache.org/download.cgi找到下载:下载安装,然后放到一个没有中文路径的文件夹里面,然后配置环境变量: 搜索【…

新来的外包,在大群分享了它的限流算法的实现

1. 令牌桶按用户维度限流 前文golang/x/time/rate演示了基于整体请求速率的令牌桶限流; 那基于用户id、ip、apikey请求速率的限流(更贴近生产的需求), 阁下又该如何应对? 那这个问题就从全局速率变成了按照用户维度…

用 Rust 与 Tesseract 进行英文数字验证码识别

Rust 是一门注重性能与安全的系统编程语言,越来越多用于图像处理和自动化领域。本文将介绍如何使用 Rust 调用 Tesseract 引擎,实现简单的英文数字验证码识别。 更多内容访问ttocr.com或联系1436423940 一、开发准备…

详细介绍:开源AI大模型、AI智能名片与S2B2C商城系统:个体IP打造与价值赋能的新范式

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

ThreadLocal 源码解析

TreadLocal ThreadLocal解决的是线程内部变量的问题,并不是为了解决并发与共享变量的问题。 堆中有两个引用指向ThreadLocal,一个是ThreadLocal本身(强引用),一个ThreadLocalMap中Entry的key(弱引用)。 ThreadL…

黑马程序员SpringCloud微服务开发与实战- Docker项目部署-03

黑马程序员SpringCloud微服务开发与实战- Docker项目部署-03Posted on 2025-11-19 22:59 心默默言 阅读(0) 评论(0) 收藏 举报1

C# 和 Tesseract 实现英文数字验证码识别

验证码识别是一项常见的图像处理任务,尤其在自动化测试和数据采集场景中非常实用。本文将介绍如何在 C# 环境下,结合 Tesseract OCR 完成英文数字验证码的自动识别。 一、开发环境准备安装 Visual Studio 更多内容访…

linux ftp自动

您提到的“Linux FTP自动”可能有多种含义,以下是几种常见情况的解释和对应的Linux命令或脚本示例:1. 自动 FTP 上传文件(使用 ftp 命令) 如果您想在 Linux 系统中自动上传文件到 FTP 服务器,可以使用 ftp 命令结…

linux ftp脚本

在 Linux 系统中,ftp 命令行工具是用于与 FTP 服务器进行交互的常用工具。如果你想要编写一个 FTP 脚本,可以利用 ftp 命令结合 bash 脚本来实现自动化操作。 以下是一个简单的 FTP 脚本示例,用于上传文件到 FTP 服…

实用指南:【案例实战】鸿蒙分布式智能办公应用的架构设计与性能优化

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

Yanhua Mini ACDP-2 BMW ECU Package: EUC Clone License with Modules 3/8/27 Bench Interface Board

Navigating BMW ECU Challenges: The Yanhua Mini ACDP-2 Package as Your Solution For European and American BMW owners and mechanics, ECU-related repairs and diagnostics can feel like a puzzle with missin…

根据图片路径将文件下载到本地

import java.io.*; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption;public class FileDownloader {public static void main(String[] args…

2025雅思一对一提分攻略:5家靠谱机构适配不同基础学员

2025雅思一对一提分攻略:5家靠谱机构适配不同基础学员在雅思备考中,一对一课程因极强的针对性成为提分关键选择。尤其是基础薄弱、存在单科瓶颈或短期冲刺高分的学员,优质的一对一教学能精准击破问题。结合2025年最…

redis-RDB/AOF-主从复制整理 - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

A few basic changes in PyQt6 and PySide6 regarding shader-based OpenGL graphics

https://gamedev.net/blogs/entry/2273817-a-few-basic-changes-in-pyqt6-and-pyside6-regarding-shader-based-opengl-graphics/