目录
一、哈希概念
顺序结构以及平衡树中,元素key与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过key的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即,搜索的效率取决于搜索过程中元素的比较次数。
效率最高的搜索方法:不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的key之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
1.插入和查找
向该结构中插入元素和查找元素时:
插入元素:将元素key存放到用hashFunc计算出的元素key的位置。
查找元素:对元素的key进行计算,把用hashFunc计算的函数值当做元素的存储位置,在哈希结构中按此位置取元素比较,若key相等,则查找成功。
2.哈希表
哈希方法中使用的转换函数称为哈希函数(也叫散列函数),来建立映射关系,构造出来的结构称为哈希表 (Hash Table)(也叫散列表)。
如有数据集合{ 5,2,6,8,9,7},假如哈希函数设置为:
hash(key) = key % capacity
其中capacity为存储元素底层空间总大小
按照这种方法查找不用拿key多次比较,因此查找的速度比较快。
不同关键字通过 相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突。当再插入别的元素时,有可能发生哈希冲突,比如插入22,hashFunc(22) = 22%10 = 2,2的位置已经存了数据2了,那么22该如何存储呢?
引起哈希冲突的原因:哈希函数设计不合理。哈希函数设计原则包括:
(1)哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
(2)哈希函数计算出来的地址能均匀分布在整个空间中
(3)哈希函数应比较简单
3.常见的哈希函数
(1)直接定址法
取关键字的某个线性函数为散列地址:Hash(key)= A*key + B
优点:简单,速度快,节省空间,查找key O(1)的时间复杂度
缺点:当数据范围大时会浪费空间,不能处理浮点数,字符串数据
使用场景:适用于整数,数据范围比较集中
例如计数排序,统计字符串中出现的用26个英文字符统计,给数组分配26个空间,遍历到的字符是谁,就把相应的元素值++
(2)除留余数法
把数据映射到有限的空间里面。设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将key转换成哈希地址。如第2节哈希表的例子。
哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。
解决哈希冲突最常用的方法是闭散列和开散列。
二、用闭散列解决哈希冲突
闭散列也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。 下一个位置怎样找呢?有以下两种常见方式:
1.线性探测法介绍
如下场景,要插入22,通过哈希函数hashfunc(22) = 22%10=2计算出的地址为2,2的位置已经有数据2了,现在发生了冲突:
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
①插入:通过哈希函数获取待插入元素在哈希表中的位置。如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。
②删除:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,否则会影响其他元素的搜索。比如删除元素2,如果直接删除掉,22查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素,即给每个位置一个标记,用空、存在、删除3种状态来区分。
负载因子 = 存储的有效数据个数/空间的大小
负载因子越大,冲突的概率越高,增删查改效率越低
负载因子越小,冲突的概率越低,增删查改的效率越高,但是空间利用率低,浪费多。
负载因子 <1,就能保证发生哈希冲突时一定能找到空位置
2.线性探测的实现
(1)状态
区分哈希表的一个位置有没有数据,如果用两种状态表示,在(1)或不在(0),那么就会带来两个问题:
①0表示不在,那么如何存数据0呢?
②如果数据发生冲突,当前位置和后面位置都存放的是冲突数据,加入当前位置的数据被删除了,那么查找key时发现当前位置状态为不在,那么就不会再向后查找了。
因此要用3个状态位分别表示空、已占用、已删除,用枚举表示状态位:
#pragma once
#include<vector>
#include<iostream>
using namespace std;
namespace CloseHash
{
//当前位置的状态有3种:空、已存在、已删除
enum State
{
EMPTY,
EXIST,
DELETE,
};
}
(2)定义HashData
哈希数据应包含两个成员:数据和状态
template<class K, class V>
struct HashData
{
pair<K, V> _kv;//数据
State _state = CloseHash::State::EMPTY;//状态
};
(3)哈希表
哈希表包含两个成员:哈希数据、存储的有效数据的个数
模板有3个参数K、V、HashFunc。
①由于不知道key是K还是pair,所以需要定义两个模板参数K、V来包含key是K或pair的两种情况
②由于不知道key的数据类型是in