数据结构(14)——哈希表(1)

2024-09-06 02:36
文章标签 数据结构 哈希 14

本文主要是介绍数据结构(14)——哈希表(1),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

欢迎来到博主的专栏:数据结构
博主ID:代码小豪

文章目录

    • 哈希表的思想
    • 映射方法(哈希函数)
      • 除留余数法
    • 哈希表
      • insert
      • 闭散列
      • 负载因子
      • 扩容
      • find和erase

哈希表的思想

在以往的线性表中,查找速度取决于线性表是否有序,如果是无序的线性表,我们就要从表头开始匹配key值是否相同,因此时间复杂度取决于线性表的元素个数,为O(N)。

如果线性表有序,则可以通过二分查找法大大的减少查找时间,时间复杂度为O(logN),这个时间复杂度看起来让人满意,但是我们要考虑到一点,那就是保持线性表的有序性是要付出代价的,使用排序算法也有O(N*logN)的时间复杂度。

即使是使用关联式容器,它的查找速度也是O(logN),但是好处在于插入和删除元素并不会破坏查找速度。但是查找的速度就局限于此了吗?

已现实为例,如果我们想要在城里找到大舅妈,肯定不会遍历整个城的街道,从街道口走到街道尾去找大舅妈的家,我们会先知道大舅妈的住址,直接去这个住址就能找到大舅妈。

这就是哈希表的查找的方式了,将key值放在映射的地址处,这样查找key值就不用从头开始遍历了。举个例子,假设现在有一个能容纳十个元素的哈希表。我们规定key值的个位数就是映射的地址值,即让1位于1号,2位于2号。以此类推,而10则放在0号。

在这里插入图片描述

假设我们要查找13,那么去3号处就能找到对应值,查找速度为O(1)。这就是哈希表查找的优势。

映射方法(哈希函数)

哈希表中的映射方法叫做哈希函数,以上表为例,其哈希函数为F(key)=key%10。于是F(13)=3,F(25)=5。一个好的哈希函数很重要。这将决定哈希表的查找,插入,删除的速率(最主要还是查找)。哈希函数需要能将key值的数据转换成整形的能力。

但是并非所有类型都可以和整形转换。比如string就不能通过F(key)=key%10的方式获得映射值,此时我们就需要设计一个使用于string的哈希函数,比如可以设计让string的所有元素相加为映射值的哈希函数,即F(string)={a1+a2+……+an}。实际上字符串的哈希函数设计绝没这么简单,感兴趣可以在网上搜索。

那么自定义类型当然也要依靠自定义的哈希函数才能获得其映射值,通常哈希函数的设计需要考虑一下几点:
(1)选取的key值的独特性,比如我们在数据库中查找一个人,如果选择“姓名”作为映射值,那么肯定效率不佳,因为全国同名同性的人是在太多了,如果我们选择“生日+姓名”作为映射值就会好很多。

(2)哈希函数的结果要尽可能的分散,假设现在有N个元素要插入到哈希表,那么它们的映射值越分散在哈希表就越好,如果N个元素经过哈希函数的运算结果的映射值都是1,那么肯定是毫无效率可言的。

(3)哈希函数的计算结果要包含在哈希表的域中,如果一个哈希表能容纳100个元素(即可容纳映射值0-99的元素),那么如果一个key值计算出映射值为101,那么肯定是没法插入的。

在c++标准库中的unordered_map,就是用哈希表为底层的容器,其允许我们传入自定义的哈希函数,已适配那些自定义的类型的映射值计算。

template < class Key,                                    // unordered_map::key_typeclass T,                                      // unordered_map::mapped_typeclass Hash = hash<Key>, //可以传入自定义的哈希函数// unordered_map::hasherclass Pred = equal_to<Key>,                   // unordered_map::key_equalclass Alloc = allocator< pair<const Key,T> >  // unordered_map::allocator_type> class unordered_map;

除留余数法

除留余数法是一个比较通用,而且简单的哈希函数。其主要方法为:

设哈希表可容纳最大地址数为m,取一个不超过m的值p作为除数,其哈希函数为:hash(key)=key%p。

比如当前哈希表的最大地址数为50,待插入的key的哈希值为313,则其映射值为313%50=13。

在后续哈希表的底层设计中,博主将采用这个方法。

哈希表

hash表的底层可以用序列式容器vector,或者deque,因为哈希表的映射值与下标很适配。

template<class key,class value>
class hash_tab
{
public:hash_tab(){_tab.resize(10);_n = 0;}typedef pair<key, value> value_type;private:vector<hash_data<value_type>> _tab;size_t _n;//当前有效数据
};

而哈希表每个元素都要存储两个数据,分别是data,以及状态state。由于vector一次性开了十个元素的空间,因此有些空间是没有有效数据的。这些没有有效数据的元素的状态记为空(empty),如果元素具有有效数据则记为有(exist),如果元素的有效数据被删除,则记为删除(delete)。

enum state
{EXIST,//存在映射DELETE,//删除映射EMPTY//空的映射
};template<class T>
struct hash_data
{hash_data(){_state = EMPTY;}hash_data(const T& data,state state){_data = data;_state = state;}const hash_data& operator=(const hash_data& hashdata){_data = hashdata._data;_state = hashdata._state;return *this;}T _data;state _state;
};

insert

insert的方式如下:
(1)先通过hash函数计算出key值的映射地址处。博主采用除留余数法。
(2)将元素插入到映射值对应的地方。

比如现在插入的元素key值为60,由于当前哈希表的最大空间为10(默认构造函数),因此除留余数法为hash(60)=60%10=0。插入在0下标处。
在这里插入图片描述
ok,现在我们来面临第一个问题,如果我们现在插入30,那么它该插入在什么位置呢?根据哈希函数hash(30)=30%10=0。那么它应该插入在映射值为0的下标处,但是0下标处已经存在60这个数据了,那么该如何处理呢?

这种情况被称为“哈希冲突”,即不同的key值(30与60),但是经过哈希函数计算后,得到了相同的映射值(0)。如果想要减少哈希冲突,优化哈希函数是一个不错的选择,但是这并不能解决哈希冲突,通常我们会用两种方法解决哈希冲突,闭散列和开散列。这篇文章我们先来了解闭散列的哈希表,在下一篇中博主再谈谈开散列的哈希表如何实现。

闭散列

闭散列:也称开放地址法,当发生哈希冲突时。探测哈希表的空位置,将key值插入在冲突位置的下一个空位置处。探测方法也分为多种

方法(1)线性探测

线性探测:从冲突位置开始,依次往后探测,直到找到下一个新位置为止。以上例为例,由于30插入的位置与60发生了哈希冲突,那么30从0下标开始线性探测后续位置,直到遇到第一个空位置(EMPTY),DELETE也算作空位置。
在这里插入图片描述
如果此时在插入一个20,与60依然发生了哈希冲突,根据线性探测的方法,20应该插入在下标2的位置。
在这里插入图片描述
假如这个哈希表满了,那么再次插入一个key值会发生什么事呢?
在这里插入图片描述
根据线性探测的规则,插入2的位置会陷入一个死循环,因为在这个哈希表中已经不存在空位置了,在这个哈希表中无论探测多少次都找不到位置。

实际上,当哈希表中的元素占比整个空间越来越多时,哈希冲突发生的几率会越来越高,而每次发生哈希冲突时,都会带来额外的时间开销(线性探测)。因此,最好的解决方法是控制元素与空间之间的占比。

负载因子

我们将哈希表中元素与空间之间的占比成为负载因子。即:

负载因子=元素个数÷哈希表的总容量

负载因子与元素个数成正比,与哈希表的总容量成反比,当负载因子小时,发生哈希冲突的几率低,插入效率高,但是空间利用率也低。当负载因子大时,发生哈希冲突的几率大,插入效率低,但是空间利用率高。因此控制负载因子在一定的数值内也是很重要的。

通常来说,采取线性探测的哈希表,其负载因子应该控制到0.7~0.8。如果负载因子超过这个区间,就要对哈希表进行扩容。以降低哈希表的负载因子。

扩容

哈希表的扩容策略是异地扩容,即先构造一个新的哈希表,这个新的哈希表是原哈希表的二倍,然后再讲原哈希表的元素重新插入在新哈希表当中。最后交换新旧两个哈希表。那么为什么要这样做呢?与其说异地扩容的好处,不如讲讲原地扩容的坏处。

假如我们将上例中的哈希表扩容两倍,即新哈希表的容量为20.
在这里插入图片描述
由于哈希表的最大容量发生了变化,那么哈希函数也会随之变化,因为我们设定的哈希函数为

hash(key)=key%size

由于size从10变成了20,那么哈希函数就变成了key%20。那么哈希表中17的映射值为
hash(17)=17%20=17,这个插入位置就不对。因此更好的方式是新建一个新哈希表。将旧表的内容插入至新表当中。
在这里插入图片描述

	bool insert(const value_type& kv){//负载因子过多,需要扩容 负载因子等于size%_nif (double(_n) / _tab.size() >= 0.7){hash_tab new_tab;new_tab._tab.resize(_tab.size() * 2);//建立新表for (auto& e : _tab){if (e._state == EXIST)//只插入有效值{new_tab.insert(e._data);//将旧表的值插入到新表当中}}_tab.swap(new_tab._tab);//交换新旧两表}size_t hashnum = kv.first % _tab.size();//哈希函数——除留余数法while (_tab[hashnum]._state ==EXIST )//线性探测{hashnum++;hashnum %= _tab.size();}_tab[hashnum] = hash_data<value_type>(kv, EXIST);//插入新值_n++;return true;}

find和erase

find函数的方法如下:

通过哈希函数计算出映射值,找到映射的位置,然后线性检测找到正确的key值,然后返回节点,如果线性检测到空节点(DELETE不算空节点,仅EMPTY)。就说明哈希表中不存在对应key值,返回nullptr。

在这里插入图片描述

	hash_data<value_type>* find(const key& key){size_t hashnum = key % _tab.size();while (_tab[hashnum]._state != EMPTY){if (key == _tab[hashnum]._data.first&&_tab[hashnum]._state!=DELETE){return &_tab[hashnum];}hashnum++;hashnum %= _tab.size();}return nullptr;}

erase的实现就更简单,我们先通过find查找到待删除的元素,然后将该元素的状态调整成DELETE就可以了,因为对这个元素的data进行修改不是一个明智的选择。这会影响到映射关系。


bool erase(const key& key)
{size_t hashnum = key % _tab.size();hash_data<value_type>* ptr=find(key);if (ptr == nullptr)return false;ptr->_state = DELETE;_n--;return true;
}

这篇关于数据结构(14)——哈希表(1)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1140755

相关文章

哈希leetcode-1

目录 1前言 2.例题  2.1两数之和 2.2判断是否互为字符重排 2.3存在重复元素1 2.4存在重复元素2 2.5字母异位词分组 1前言 哈希表主要是适合于快速查找某个元素(O(1)) 当我们要频繁的查找某个元素,第一哈希表O(1),第二,二分O(log n) 一般可以分为语言自带的容器哈希和用数组模拟的简易哈希。 最简单的比如数组模拟字符存储,只要开26个c

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

usaco 1.3 Prime Cryptarithm(简单哈希表暴搜剪枝)

思路: 1. 用一个 hash[ ] 数组存放输入的数字,令 hash[ tmp ]=1 。 2. 一个自定义函数 check( ) ,检查各位是否为输入的数字。 3. 暴搜。第一行数从 100到999,第二行数从 10到99。 4. 剪枝。 代码: /*ID: who jayLANG: C++TASK: crypt1*/#include<stdio.h>bool h

业务中14个需要进行A/B测试的时刻[信息图]

在本指南中,我们将全面了解有关 A/B测试 的所有内容。 我们将介绍不同类型的A/B测试,如何有效地规划和启动测试,如何评估测试是否成功,您应该关注哪些指标,多年来我们发现的常见错误等等。 什么是A/B测试? A/B测试(有时称为“分割测试”)是一种实验类型,其中您创建两种或多种内容变体——如登录页面、电子邮件或广告——并将它们显示给不同的受众群体,以查看哪一种效果最好。 本质上,A/B测

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

《数据结构(C语言版)第二版》第八章-排序(8.3-交换排序、8.4-选择排序)

8.3 交换排序 8.3.1 冒泡排序 【算法特点】 (1) 稳定排序。 (2) 可用于链式存储结构。 (3) 移动记录次数较多,算法平均时间性能比直接插入排序差。当初始记录无序,n较大时, 此算法不宜采用。 #include <stdio.h>#include <stdlib.h>#define MAXSIZE 26typedef int KeyType;typedef char In

哈希表的底层实现(1)---C++版

目录 哈希表的基本原理 哈希表的优点 哈希表的缺点 应用场景 闭散列法 开散列法 开放定值法Open Addressing——线性探测的模拟实现 超大重点部分评析 链地址法Separate Chaining——哈希桶的模拟实现 哈希表(Hash Table)是一种数据结构,它通过将键(Key)映射到值(Value)的方式来实现快速的数据存储与查找。哈希表的核心概念是哈希

哈希表的封装和位图

文章目录 2 封装2.1 基础框架2.2 迭代器(1)2.3 迭代器(2) 3. 位图3.1 问题引入3.2 左移和右移?3.3 位图的实现3.4 位图的题目3.5 位图的应用 2 封装 2.1 基础框架 文章 有了前面map和set封装的经验,容易写出下面的代码 // UnorderedSet.h#pragma once#include "HashTable.h"

【408数据结构】散列 (哈希)知识点集合复习考点题目

苏泽  “弃工从研”的路上很孤独,于是我记下了些许笔记相伴,希望能够帮助到大家    知识点 1. 散列查找 散列查找是一种高效的查找方法,它通过散列函数将关键字映射到数组的一个位置,从而实现快速查找。这种方法的时间复杂度平均为(

PHP: 深入了解一致性哈希

前言 随着memcache、redis以及其它一些内存K/V数据库的流行,一致性哈希也越来越被开发者所了解。因为这些内存K/V数据库大多不提供分布式支持(本文以redis为例),所以如果要提供多台redis server来提供服务的话,就需要解决如何将数据分散到redis server,并且在增减redis server时如何最大化的不令数据重新分布,这将是本文讨论的范畴。 取模算法 取模运