C++:哈希表

目录

哈希的概念

直接定址法

哈希冲突

负载因子 

哈希函数

除法散列法/除留余数法

乘法散列法

处理哈希冲突

开放定址法

线性探测

二次探测

双重散列

链地址法

哈希表的实现

哈希表的结构

闭散列(开放定址法)

结构

插入

查找

删除

开散列(链地址法)

结构

插入

查找

删除

完整代码


哈希的概念

哈希又称散列,是一种组织数据的方式

它的本质是通过哈希函数把关键字key跟存储位置建立一个映射关系,查找时再通过这个哈希函数计算出key存储的位置,进行快速查找

所以它的查找时间复杂度能达到恐怖的O(1)

直接定址法

直接定址法可以是一个26个大小的数组arr[26]来代表26个英文字母,这时候这26个空间就和这26个英文字母建立了映射关系

 . - 力扣(LeetCode)

class Solution {
public:int firstUniqChar(string s) {int hash[26];for (auto str : s)hash[str - 'a']++;for (int i = 0; i < s.size(); i++)if (hash[s[i] - 'a'] == 1)return i;return -1;}
};

哈希冲突

当我们使用直接定址法的时候,当数据映射到同一个位置的时候就把它叫做哈希冲突或者哈希碰撞

哈希冲突是不可避免的 

为了减少哈希冲突,我们可以使用一个比较好的哈希函数来减少哈希冲突

负载因子 

若哈希表的大小为M,已经映射存储的数据个数为N,那么 负载因子 = N / M

负载因子越大,哈希冲突的概率越高,空间利用率越高

负载因子越小,哈希冲突的概率越低,空间利用率越低

哈希函数

除法散列法/除留余数法

假设哈希表的大小为M

那么通过key除以M的余数作为映射位置的下标

哈希函数为:hashi = key % M 

乘法散列法

乘法散列法对哈希表的大小M没有要求

第一步:用关键字key乘上常数A(0 < A < 1),并抽取key*A的小数部分

第二部:后再用M乘以key*A的小数部分,再向下取整

哈希函数为:hashi = floor(M * ((A * key) % 1.0))

这里最重要的是A的值如何设定

Knuth认为A = 0.6180339887......(黄金分割点)比较好

处理哈希冲突

主要有两种方法,开放定址法和链地址法

开放定址法

线性探测

从发生冲突的位置开始,依次线性向后探测,直到寻找到下一个没有存储数据的位置为止

如果走到哈希表尾,则回绕到哈希表头的位置

h(key) = hash0 = key % M,若hash0冲突,则线性探测公式为

hc(key, i) = hashi = (hash0 + i) % M, i = {1, 2, 3..., M - 1}(负载因子小于1,最多探测M-1次,一定能找到一个位置存储)

二次探测

从发生冲突的位置开始,依次左右按二次方跳跃式探测,直到寻找到下一个没有存储数据的位置为止,如果从右走到哈希表尾,则回绕到哈希表头的位置,如果往左走到哈希表头,则回绕到哈希表尾的位置

h(key) = hash0 = key % M,若hash0冲突,则二次线性探测公式为

hc(key, i) = hashi = (hash0 +/- i^2) % M, i = {1, 2, 3, ....., M / 2}

当hashi < 0时,需要hash += M

双重散列

当第一个哈希函数计算出的值发生冲突,使用第二个哈希函数计算出一个跟key相关的偏移量值,不断往后探测,直到寻找到下一个没有数据的位置为止

h1(key) = hash0 = key % M,hash0位置冲突了,则双重探测公式为

hc(key, i) = hashi = (hash0 + i * h2(key)) % M,i = {1, 2, 3, ....., M}

链地址法

开放定址法中所有的元素都放到哈希表里,链地址法中所有的数据不再直接存储在哈希表中

哈希表里只需要存储一个指针,当没有数据映射这个位置时,指针为空,当有多个数据映射这个位置时,我们把这些冲突的数据链接成一个链表,挂在哈希表当前位置的下面

链地址法也叫做拉链法或者哈希桶

哈希表的实现

哈希表的结构

enum State
{EXIST,EMPTY,DELETE
};template<class K, class V>
struct HashData
{pair<K, V> _kv;State _state = EMPTY;
};

因为我们删除一个值后无法判断这个值是存在还是删除,所以我们可以用一个State状态来标记当前位置的是一个什么状态,所以我们需要枚举出三个状态区分

template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};template<>
struct HashFunc<string>
{size_t operator()(const string& s){// BKDRsize_t hash = 0;for (auto ch : s){hash += ch;hash *= 131;}return hash;}
};

因为我们映射的key不能确定是什么类型,若是char、int这类整型,可以强转成size_t当作key

但是如果是string这类无法转换成数字的类型,我们就需要自己写一个仿函数来拿出一个整数key

这里用的是BKDR算法来进行转换的key

inline unsigned long __stl_next_prime(unsigned long n)
{// Note: assumes long is at least 32 bits.static const int __stl_num_primes = 28;static const unsigned long __stl_prime_list[__stl_num_primes] = {53, 97, 193, 389, 769,1543, 3079, 6151, 12289, 24593,49157, 98317, 196613, 393241, 786433,1572869, 3145739, 6291469, 12582917, 25165843,50331653, 100663319, 201326611, 402653189, 805306457,1610612741, 3221225473, 4294967291};const unsigned long* first = __stl_prime_list;const unsigned long* last = __stl_prime_list + __stl_num_primes;const unsigned long* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;
}

这是扩容的逻辑,因为哈希表为了减少哈希冲突,哈希表的容量需要尽可能的是素数,所以这里写了一个函数列了一个素数表来拿到下一个接近2倍并且是素数的值

lower_bound函数前两个参数是一个迭代器区间,第三个参数是一个值value

该函数会在给定区间内给出一个不小于value的值

这样在扩容的时候就能按接近2倍扩容并且容量还是素数,减少了哈希冲突的可能性

闭散列(开放定址法)

结构

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:HashTable():_tables(__stl_next_prime(0)), _n(0){}
private:vector<HashData<K, V>> _tables;size_t _n;
};

第一个模板参数表示key,第二个模板参数表示value,第三个模板参数是将key转换成整数的类,里面会有仿函数

 _tables表的容量默认是素数表中大于0的数也就是第一个53

_n表示当前表的大小为0

插入

bool Insert(const pair<K, V>& kv)
{if (Find(kv.first))return false;// 负载因子大于0.7则扩容if (_n * 10 / _tables.size() >= 7){HashTable<K, V> newht;newht._tables.resize(__stl_next_prime(_tables.size() + 1));for (auto& data : _tables){if (data._state == EXIST){newht.Insert(data._kv);}}_tables.swap(newht._tables);}Hash hash;size_t hash0 = hash(kv.first) % _tables.size();size_t hashi = hash0;size_t i = 1;while (_tables[hashi]._state == EXIST){hashi = (hash0 + i) % _tables.size();i++;}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;_n++;return true;
}

首先用Find函数判断该值的key是否存在哈希表内(该哈希表不存在值冗余)

Find函数下面实现 

负载因子若是太小会浪费空间,若是太大容易造成哈希冲突,所以这里规定若是负载因子>=0.7则扩容

首先创建一个新的哈希表,并且提前将它的容量扩容到当前表大小的后一个素数表中的值

然后再遍历旧表将每个EXIST存在的值插入到新表中,最后两个表交换即可

首先用Hash类创建出hash对象,从而能够取出key中代表的那个的整数

用这个整数模上表大小即可得到映射关系的位置

若当前位置已经存在值,则表示哈希冲突,那么可以使用线性探测来一个个走

最后插入当前位置即可

查找

HashData<K, V>* Find(const K& key)
{Hash hash;size_t hash0 = hash(key) % _tables.size();size_t hashi = hash0;size_t i = 1;while (_tables[hashi]._state != EMPTY){if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key){return &_tables[hashi];}hashi = (hash0 + i) % _tables.size();i++;}return nullptr;
}

先用hash对象取出key对应的整数,求出映射关系,若当前位置有值则根据上面的线性探测规则一个个往后找即可

删除

bool Erase(const K& key)
{HashData<K, V>* ret = Find(key);if (ret){ret->_state = DELETE;return true;}else{return false;}
}

 删除只需要找到位置之后把状态state置为DELETE即可

开散列(链地址法)

结构

template<class K, class V>
struct HashNode
{pair<K, V> _kv;HashNode<K, V>* _next;HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}
};template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{typedef HashNode<K, V> Node;
public:HashTable():_tables(__stl_next_prime(0)), _n(0){}
private:vector<Node*> _tables; // 指针数组size_t _n = 0;
};

开散列需要Hash节点,该节点需要有值value和一个next指针,这样就能构成一个链表

下面的结构和上面的闭散列一致

开散列需要写拷贝构造、赋值重载、析构函数,这些节点的值需要我们手动释放

插入

bool Insert(const pair<K, V>& kv)
{if (Find(kv.first))return false;Hash hash;// 负载因子为1时扩容if (_n == _tables.size()){vector<Node*> newTable(__stl_next_prime(_tables.size() + 1));for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;// 头插到新表size_t hashi = hash(cur->_kv.first) % newTable.size();cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTable);}size_t hashi = hash(kv.first) % _tables.size();Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;_n++;return true;
}

这里的负载因子只需要到1再扩容即可,因为这里处理哈希冲突是用链表接在下面的,不会计入负载因子的分子中

这里的扩容逻辑几乎和上面闭散列的一致,区别就是插入数据的时候这里是需要用链表的方式头插

下面的插入逻辑也是将新值头插到映射关系的位置上即可

查找

Node* Find(const K& key)
{Hash hash;size_t hashi = hash(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;
}

先找到映射关系的位置,再沿着链表一个个查找即可 

删除

bool Erase(const K& key)
{size_t hashi = key % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){if (prev == nullptr){// 头结点_tables[hashi] = cur->_next;}else{// 中间节点prev->_next = cur->_next;}delete cur;--_n;return true;}else{prev = cur;cur = cur->_next;}}return false;
}

先找到映射关系的位置,再沿着链表一个个查找

若找到删除的值,则可以根据prev是否为空来判断删除的节点是否是头节点

完整代码

#pragma once
#include <iostream>
#include <vector>
#include <string>
using namespace std;enum State
{EXIST,EMPTY,DELETE
};template<class K, class V>
struct HashData
{pair<K, V> _kv;State _state = EMPTY;
};template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};template<>
struct HashFunc<string>
{size_t operator()(const string& s){// BKDRsize_t hash = 0;for (auto ch : s){hash += ch;hash *= 131;}return hash;}
};inline unsigned long __stl_next_prime(unsigned long n)
{// Note: assumes long is at least 32 bits.static const int __stl_num_primes = 28;static const unsigned long __stl_prime_list[__stl_num_primes] = {53, 97, 193, 389, 769,1543, 3079, 6151, 12289, 24593,49157, 98317, 196613, 393241, 786433,1572869, 3145739, 6291469, 12582917, 25165843,50331653, 100663319, 201326611, 402653189, 805306457,1610612741, 3221225473, 4294967291};const unsigned long* first = __stl_prime_list;const unsigned long* last = __stl_prime_list + __stl_num_primes;const unsigned long* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;
}namespace open_address
{template<class K, class V, class Hash = HashFunc<K>>class HashTable{public:HashTable():_tables(__stl_next_prime(0)), _n(0){}bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;// 负载因子大于0.7则扩容if (_n * 10 / _tables.size() >= 7){HashTable<K, V> newht;newht._tables.resize(__stl_next_prime(_tables.size() + 1));for (auto& data : _tables){if (data._state == EXIST){newht.Insert(data._kv);}}_tables.swap(newht._tables);}Hash hash;size_t hash0 = hash(kv.first) % _tables.size();size_t hashi = hash0;size_t i = 1;while (_tables[hashi]._state == EXIST){hashi = (hash0 + i) % _tables.size();i++;}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;_n++;return true;}HashData<K, V>* Find(const K& key){Hash hash;size_t hash0 = hash(key) % _tables.size();size_t hashi = hash0;size_t i = 1;while (_tables[hashi]._state != EMPTY){if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key){return &_tables[hashi];}hashi = (hash0 + i) % _tables.size();i++;}return nullptr;}bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret){ret->_state = DELETE;return true;}else{return false;}}private:vector<HashData<K, V>> _tables;size_t _n;};
}namespace hash_bucket
{template<class K, class V>struct HashNode{pair<K, V> _kv;HashNode<K, V>* _next;HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}};template<class K, class V, class Hash = HashFunc<K>>class HashTable{typedef HashNode<K, V> Node;public:HashTable():_tables(__stl_next_prime(0)), _n(0){}~HashTable(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;Hash hash;// 负载因子为1时扩容if (_n == _tables.size()){vector<Node*> newTable(__stl_next_prime(_tables.size() + 1));for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;// 头插到新表size_t hashi = hash(cur->_kv.first) % newTable.size();cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTable);}size_t hashi = hash(kv.first) % _tables.size();Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;_n++;return true;}Node* Find(const K& key){Hash hash;size_t hashi = hash(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;}bool Erase(const K& key){size_t hashi = key % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){if (prev == nullptr){// 头结点_tables[hashi] = cur->_next;}else{// 中间节点prev->_next = cur->_next;}delete cur;--_n;return true;}else{prev = cur;cur = cur->_next;}}return false;}private:vector<Node*> _tables; // 指针数组size_t _n = 0;};
}

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

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

相关文章

Java系统学习笔记

计算机知识 CMD 环境变量 想要在任意目录下都可以打开指定的软件&#xff0c;就可以把软件的路径配置到环境变量中。 JDK JDK安装目录中 javac 是JDK提供的一个工具&#xff0c;可以通过这个工具&#xff0c;把java文件编译成class文件 java 也是JDK提供的一个工具&#xf…

C语言部分输入输出(printf函数与scanf函数,getchar与putchar详解,使用Linux ubuntu)

1.输入输出 1.1.按格式输入输出 printf 可以在man手册中查看 int printf(const char *format, ...); printf:函数名(参数)int:函数的返回值 功能&#xff1a;按格式在终端输出 参数&#xff1a;多参 返回值&#xff1a;输出字符个数 格式&#xff1a; %d int %c char…

WPF+MVVM案例实战(十五)- 实现一个下拉式菜单(上)

文章目录 1 案例效果2、图标资源下载3、功能实现1.文件创建2、菜单原理分析3、一级菜单两种样式实现1、一级菜单无子项样式实现2、一级菜单有子项样式实现 4、总结 1 案例效果 提示 2、图标资源下载 从阿里矢量素材官网下载需要的菜单图片&#xff0c;如下所示&#xff1a; …

Python 从入门到实战43(Pandas数据结构)

我们的目标是&#xff1a;通过这一套资料学习下来&#xff0c;可以熟练掌握python基础&#xff0c;然后结合经典实例、实践相结合&#xff0c;使我们完全掌握python&#xff0c;并做到独立完成项目开发的能力。 上篇文章我们学习了NumPy数组操作的相关基础知识。今天学习一下pa…

电商行业的流程革命:创建SOP的最佳实践

在电商行业&#xff0c;随着业务规模的扩大和市场环境的快速变化&#xff0c;创建和遵循标准化操作程序&#xff08;SOP&#xff09;变得尤为重要。SOP不仅能够确保业务流程的一致性和高效性&#xff0c;还能帮助企业快速适应市场变化&#xff0c;提高服务质量。本文将探讨电商…

服务器宝塔安装哪吒监控

哪吒文档地址&#xff1a;https://nezha.wiki/guide/dashboard.html 一、准备工作 OAuth : 我使用的gitee&#xff0c;github偶尔无法访问&#xff0c;不是很方便。第一次用了极狐GitLab&#xff0c;没注意&#xff0c;结果是使用90天&#xff0c;90天后gg了&#xff0c;无法登…

计算机网络(Ⅶ)Web and HTTP

一些术语&#xff1a; Web页&#xff1a;由一些对象组成 对象可以是HTML文件&#xff0c;JPEG图像&#xff0c;Java小程序&#xff0c;声音剪辑文件等 Web页含有一个基本的HTML文件&#xff0c;该基本HTML文件又包含若干对象的引用&#xff08;链接&#xff09; 通过URL对每个对…

【ChatGPT】搜索趋势分析

【ChatGPT】搜索趋势分析 为了分析 ChatGPT 在过去一年的流行趋势&#xff0c;我们可以查看 Google Trends 的数据 安装依赖pytrends pip install pytrends运行以下 Python 脚本 import pandas as pd import matplotlib.pyplot as plt from pytrends.request import TrendR…

「MinIO快速入门」

官网&#xff1a;MinIO | S3 Compatible Storage for AI 中文&#xff1a;http://minio.org.cn/ MinIO 开始 MinIO 是一款高性能、开源的对象存储服务器。 一、主要特点 高性能 MinIO 采用了分布式架构&#xff0c;能够高效地处理大量数据的存储和访问请求。它针对多核处理…

C++ | Leetcode C++题解之第516题最长回文子序列

题目&#xff1a; 题解&#xff1a; class Solution { public:int longestPalindromeSubseq(string s) {int n s.length();vector<vector<int>> dp(n, vector<int>(n));for (int i n - 1; i > 0; i--) {dp[i][i] 1;char c1 s[i];for (int j i 1; j…

2024年,Rust开发语言,现在怎么样了?

Rust开发语言有着一些其他语言明显的优势&#xff0c;但也充满着争议&#xff0c;难上手、学习陡峭等。 Rust 是由 Mozilla 主导开发的通用、编译型编程语言&#xff0c;2010年首次公开。 在 Stack Overflow 的年度开发者调查报告中&#xff0c;Rust 连续多年被评为“最受喜爱…

【C/C++】模拟实现strlen

学习目标&#xff1a; 使用代码模拟实现strlen。 逻辑&#xff1a; strlen 需要输入一个字符串数组类型的变量&#xff0c;并且返回一个整型类型的数据。strlen 需要计算字符串数组有多少个元素。 代码1&#xff1a;使用计数器 #define _CRT_SECURE_NO_WARNINGS 1 #include&…

LLM | 论文精读 | 地学视觉语言大模型:Towards Vision-Language Geo-Foundation Model: A Survey

论文标题&#xff1a;Towards Vision-Language Geo-Foundation Model: A Survey 作者&#xff1a;Yue Zhou, Litong Feng, Yiping Ke, Xue Jiang, Junchi Yan, Xue Yang, Wayne Zhang 期刊&#xff1a;未提供 DOI&#xff1a;https://arxiv.org/abs/2406.09385 email&#x…

LC:贪心题解

文章目录 376. 摆动序列 376. 摆动序列 题目链接&#xff1a;https://leetcode.cn/problems/wiggle-subsequence/description/ 这个题目自己首先想到的是动态规划解题&#xff0c;贪心解法真的非常妙&#xff0c;参考下面题解&#xff1a;https://leetcode.cn/problems/wiggle…

淘宝商品评价API的获取与应用

在当今数字化时代&#xff0c;电商平台如淘宝已成为消费者购物的重要渠道。对于商家和开发者而言&#xff0c;如何高效地获取并利用商品评价数据&#xff0c;成为了提升产品竞争力和优化用户体验的关键。本文将详细介绍如何使用淘宝开放平台提供的商品评论API来获取这些宝贵的数…

类和对象(中)—— 类的六个默认成员函数

目录 1.类中的默认成员函数 2.构造函数 为什么要有构造函数 什么是构造函数 构造函数做了什么 默认生成的构造函数功能的分析 C11的补救 什么时候自己写构造函数 3.析构函数 为什么要有析构函数 什么是析构函数 析构函数做了什么 默认生成的析构函数功能的分析 什…

Java Executor ScheduledExecutorService 源码

前言 相关系列 《Java & Executor & 目录》《Java & Executor & ScheduledExecutorService & 源码》《Java & Executor & ScheduledExecutorService & 总结》《Java & Executor & ScheduledExecutorService & 问题》 涉及内容 …

[C++]C++知识点总结(Xmind思维导图)

目录 一、C入门基础 二、类和对象 三、C/C内存管理 四、模板 五、C的IO流 六、继承 七、多态 八、C11 九、智能指针 十、类型转换 由于C知识体系过于庞大&#xff0c;分为下面几个部分列出逻辑图&#xff08;有些不完整&#xff0c;后续更新中...&#xff09; 一、C入…

Elasticsearch分词器基础安装

简介 Elasticsearch (ES) 是一个基于 Lucene 的搜索引擎&#xff0c;分词器是其核心组件之一&#xff0c;负责对文本数据进行分析和处理。 1. 文本分析 分词器将输入的文本拆分成一个个单独的词&#xff08;tokens&#xff09;&#xff0c;以便后续的索引和搜索。例如&#x…

EfficientNet-B6模型实现ISIC皮肤镜图像数据集分类

项目源码获取方式见文章末尾&#xff01; 回复暗号&#xff1a;13&#xff0c;免费获取600多个深度学习项目资料&#xff0c;快来加入社群一起学习吧。 《------往期经典推荐------》 项目名称 1.【基于opencv答题卡识别判卷】 2.【卫星图像道路检测DeepLabV3Plus模型】 3.【G…