顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应关系
在查找一个元素时,必须要经过key的多次比较。
顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN).
有没有这样一种方法 不经过任何比较 直接从表中得到要查找的元素。
大佬神作: 构造一种存储结构,通过某种函数使元素的存储位置与key之间建立一一映射的关系,在查找时通过该函数找到该元素.
插入元素:
将待插入元素的key,以某个函数[哈希函数/散列函数]计算出该元素的存储位置并按此位置存放,构造出一个结构[哈希表/散列表]
搜索元素:
对元素的key进行同样的计算,求得的函数值即为元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
设计一个合理的哈希函数
哈希函数设计原则:
常见哈希函数
当发生哈希冲突时,如果哈希表未被装满,把key存放到冲突位置的==“下一个” ==空位置
寻找空位置:
插入:
删除:
负载因子
哈希表的负载因子定义为: α = 表中的元素个数[ _n ] / 哈希表长[ _tables.size() ]
负载因子a: 哈希表装满程度的标志因子
表长是定值,α与_n成正比
α越大 填入表中的元素越多 哈希冲突可能性越大
α越小 填入表中的元素越少 哈希冲突可能性越小
哈希表的平均查找长度是负载因子α的函数 处理冲突方法不同函数不同
负载因子越大,冲突的概率越高,查找效率越低,空间利用率越高
负载因子越小,冲突的概率越低,查找效率越高,空间利用率越低
容量问题: 1.size是实际能够访问数据的范围 2.capacity是存储数据的空间大小
优点:
实现简单
缺点;
x占据y的位置 y就得放到y+1的位置
冲突累计 产生数据堆积
本意是要减缓哈希冲突
虽然使得有相同hashi的不同数据有位置存放
但是数据堆积时 会使得寻找某关键码的位置需要许多次比较
导致搜索效率降低。
对关键码用哈希函数计算哈希地址,具有相同地址的不同关键码归于同一子集合,每一个子集合称为一个桶各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
桶的个数有限[即哈希表的表长有限]
随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容
某种情况下 每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,我们假定在元素个数刚好等于桶的个数时,进行扩容 即
需要了解到是 我们最然控制α为1时进行扩容 并不代表此时哈希桶都挂了一个结点 更为普遍的情况是 一些桶为空 一些桶有许多结点 只不过结点总个数为哈希表长度大小
在一些函数中 我们遇到了一个问题:
//哈希函数计算的下标
size_t hashi = key % _tables.size();
当key是字符串类型时 无法进行取模操作
改进
不传显示函数 默认调用缺省函数
调用显示传参函数
在解决字符串取模问题时 我们首先想到了搞一个仿函数 由key传参 获得整型 但是这仍然无法解决数据是字符串类型 于是我们想到了 缺省值这一语法 Fun类模板默认接收ConvertFunc 当用户想要使用string类型时 显示传StringToInt 在实现StringToInt时 我们右遇到了另一个问题 当数据对象不同但是ASCII值加和相同 此时又造成了不同数据会哈希冲突的局面 怎么办呢? 这个问题实际上是一个很经典的问题 对于这个问题 业界很多大佬都对其发表了自己的见解 我们采取一种即可 有想对其进行更多了解的 点击链接查阅即可 这里不再赘述
字符串Hash函数对比
有人说当在哈希表中大量数据都插入到了一个桶中 此时最坏的T(N)=O(N)
但实际上 由于存在负载因子α 每进行一次扩容这样的情况就会消失 所以
哈希表的T(N) = O(1) 由下面的性能测试代码可以证明
闭散列需要开大量空间以确保搜索效率 在这些空间中 有许多空闲空间
开散列虽然增设链表结点和指针 但是与闭散列相比 更节省空间
//闭散列/开放地址法
namespace OpenAddressing
{
//状态枚举
enum State
{
EMPTY,
EXIST,
DELETE
};
//哈希数据元素
template<class K, class V>
struct HashData
{
pair<K, V> _pair;
State _state = EMPTY;
};
//哈希表
template<class K, class V>
class HashTable
{
public:
//插入函数
bool Insert(const pair<K, V>& pair)
{
//值已存在 插入错误
if (Find(pair.first))
return false;
//负载因子/荷载系数 -- Load_Factor = _n / _tables.size();
//(double)_n / (double)_tables.size() >= 0.7
//_n * 10 / _tables.size() >= 7
//使得扩容发生的条件: size为0 负载因子达到阈值
if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
{
/ 低级代码 /
/*
//先更新size 由size作为参数扩容 解决只改容量 不更新访问范围的问题
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
//调用vector的有参构造[有参构造里调用reserve] 创建一个新表
vector> newtables(newsize);
//遍历旧表 由哈希函数更新数据位置
for (auto& e : _tables)
{
if (e._state == EXIST)
{
//哈希函数计算出的下标
size_t hashi = pair.first % newtables.size();
//重新线性探测
size_t index = hashi;//index代替hashi进行操作 保留原始hashi的值不变
for (size_t i = 1; newtables[index]._state == EXIST; ++i)
{
index = hashi + i; //从原始下标不断 +1 +2 +3 ...
index %= newtables.size();//防止越界 只在表内定位index
}
//将数据放入合适位置
newtables[index]._pair = e._pair;
newtables[index]._state = EXIST;
}
}
//新表的数据才是我们想要的数据 交换后 newtables中存放的变为旧数据
//newtables是个局部变量 让其"自生自灭"
_tables.swap(newtables);
*/
// 高级代码 //
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
HashTable<K, V> other;
other._tables.resize(newsize);
for (auto& e : _tables)
{
if (e._state == EXIST)
other.Insert(e._pair);
}
_tables.swap(other._tables);
/
//以上高级代码实际是对下面的线性探测进行了复用
}
//哈希函数计算出的下标
size_t hashi = pair.first % _tables.size();
// 线性探测
size_t index = hashi;//index代替hashi进行操作 保留原始hashi的值不变
for (size_t i = 1; _tables[index]._state == EXIST; ++i)
{
index = hashi + i; //从原始下标不断 +1 +2 +3 ...
index %= _tables.size();//防止越界 只在表内定位index
}
//将数据放入合适位置
_tables[index]._pair = pair;
_tables[index]._state = EXIST;
_n++; //数据个数++
return true;
}
//查找函数
HashData<K, V>* Find(const K& key)
{
//哈希表为空 返回nullptr
if (_tables.size() == 0)
return nullptr;
//哈希函数计算出的下标
size_t hashi = key % _tables.size();
// 线性探测
size_t index = hashi;
for (size_t i = 1; _tables[index]._state != EMPTY; ++i)
{
//obj是key的前提是obj存在
if (_tables[index]._state == EXIST
&& _tables[index]._pair.first == key)
{
return &_tables[index];
}
index = hashi + i;
index %= _tables.size();
//当表中元素状态非exist即delete时
//for循环判断条件一直为真 死循环
//解决: 当找了一圈还未找到
//表中无此key 返回false
if (index == hashi)
break;
}
return nullptr;
}
//删除函数
bool Erase(const K& key)
{
HashData<K, V>* pos = Find(key);
if (pos)
{
pos->_state = DELETE;
--_n; //虽然已标记删除 仍然要使数据个数减减 防止有用数据未达到阈值就执行扩容
return true;
}
else
return false;
}
private:
vector<HashData<K, V>> _tables;
size_t _n = 0;//存储的数据个数
};
// 测试函数 ///
void TestHashTable()
{
int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
HashTable<int, int> ht;
//插入
for (auto& e : a)
ht.Insert(make_pair(e, e));
//插入第8个数据 达到阈值 测试扩容
ht.Insert(make_pair(15, 15));
//查找 + 删除
int tmp = 12;
if (ht.Find(tmp))
cout << tmp << "在" << endl;
else
cout << tmp << "不在" << endl;
ht.Erase(tmp);
if (ht.Find(tmp))
cout << tmp << "在" << endl;
else
cout << tmp << "不在" << endl;
}
}
//开散列/链地址法
namespace ChainAddressing
{
/*STL库中unordered_map/unordered_set的定义
template
<
class Key,
class T,
class Hash = hash,
class Pred = equal_to,
class Alloc = allocator< pair >
>
class unordered_map;
class unordered_set;
*/
//结点类
template<class K, class V>
struct HashNode
{
HashNode<K, V>* _next;
pair<K, V> _pair;
HashNode(const pair<K, V>& pair)
:_next(nullptr)
, _pair(pair)
{
}
};
//转换函数==类模板特化应用场景==[int->int char->int + 特化:string->int]
template<class K>
struct hash
{
size_t operator()(const K& key)
{
return key;
}
};
template<>
struct hash<string>
{
size_t operator()(const string& s)
{
size_t value = 0;
for (auto ch : s)
{
value = value * 131 + ch;
}
return value;
}
};
//字符串转换函数
/*
struct StringToInt
{
size_t operator()(const string& s)
{
//问题代码:
//return s[0];
//1.如果传空串 违背使用者意愿
//2.不同单词相同首字母均会哈希冲突 不太合适
//代码改进
size_t value = 0;
for (auto& ch : s)
{
value = value * 131 + ch;
}
return value;
}
};
*/
//哈希表类
template< class K, class V,class Hash = hash<K> >
class HashTable
{
typedef HashNode<K, V> Node;
public:
//析构函数
~HashTable()
{
for (auto& ptr : _tables)
{
while (ptr)
{
//记录下一个结点
Node* next = ptr->_next;
//释放当前结点
delete ptr;
//更新ptr
ptr = next;
}
ptr = nullptr;
}
}
//查找函数
Node* Find(const K& key)
{
//为空不查找 返回nullptr
if (_tables.size() == 0)
return nullptr;
Hash hash;
//哈希函数计算的下标
size_t hashi = hash(key) % _tables.size();
//首先得到表里的指针 即相当于每一个桶的头指针
//[实际上 每一个桶就是一个链表 表中的ptr是每一个链表的哨兵指针]
Node* ptr = _tables[hashi];
while (ptr)
{
if (ptr->_pair.first == key)
return ptr;
ptr = ptr->_next;
}
return nullptr;
}
//删除函数
bool Erase(const K& key)
{
Hash hash;
//哈希函数计算的下标
size_t hashi = hash(key) % _tables.size();
//首先得到表里的指针 即相当于每一个桶的头指针
//[实际上 每一个桶就是一个链表 表中的ptr是每一个链表的哨兵指针]
Node* ptr = _tables[hashi];
Node* prv = nullptr;
while (ptr)
{
//当前值为目标值 执行删除操作
if (ptr->_pair.first == key)
{
if (prv == nullptr)
_tables[hashi] = ptr->_next;
else
prv->_next = ptr->_next;
delete ptr;
return true;
}
//当前值不为目标值 继续向下遍历
else
{
prv = ptr;
ptr = ptr->_next;
}
}
return false;
}
//C++SGI版本STL库:获得下一个素数
//在SGI下 设定哈希表的容量为素数
//[可能效率更高 有兴趣的可以自行查阅]
/*
size_t GetNextPrime(size_t index)
{
static const int num = 28;
static const unsigned long prime[num] =
{
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
};
size_t i = 0;
for (i = 0; i < num; ++i)
{
if (prime[i] > index)
return prime[i];
}
return prime[i];
}
*/
//插入函数
bool Insert(const pair<K, V>& pair)
{
//表中已有 返回false
if (Find(pair.first))
return false;
Hash hash;
//负载因子/荷载系数 -- Load_Factor = _n / _tables.size();
//负载因子 == 1时扩容
if (_n == _tables.size())
{
/// 高级代码1.0 /
/*
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
HashTable newht;
newht.resize(newsize);
for (auto& ptr : _tables)
{
while (ptr)
{
newht.Insert(ptr->_pair);
ptr = ptr->_next;
}
}
_tables.swap(newht._tables);
*/
//初代扩容
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
//引进stl库素数思想
//size_t newsize = GetNextPrime(_tables.size());
vector<Node*> newtables(newsize, nullptr);
//遍历旧表 取出旧表里每一个指针
for (auto& ptr : _tables)
{
//ptr是旧表里存储的每一个指针
//它指向当前哈希桶的首结点 即他存储的是首结点的地址
while (ptr)
{
//算出 当前结点根据新哈希函数计算的新下标
size_t hashi = hash(ptr->_pair.first) % newtables.size();
//ptr是首结点的地址 ptr->_next为第二个结点的地址
Node* next = ptr->_next;
// 头插到新表
ptr->_next = newtables[hashi];
newtables[hashi] = ptr;
//更新ptr 即向下遍历
ptr = next;
}
}
_tables.swap(newtables);
}
//哈希函数计算出的下标
size_t hashi = hash(pair.first) % _tables.size();
//链表头插
Node* newnode = new Node(pair);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return true;
}
//最大桶数据个数
size_t MaxBucketSize()
{
size_t max = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
auto ptr = _tables[i];
size_t size = 0;
while (ptr)
{
++size;
ptr = ptr->_next;
}
//每个桶数据个数
//printf("[%d] -> %d\n", i, size);
if (size > max)
max = size;
}
return max;
}
private:
vector<Node*> _tables; // 指针数组
size_t _n = 0; // 存储有效数据个数
};
测试函数 //
//插入测试
void TestHashTable1()
{
int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
HashTable<int, int> ht;
for (auto e : a)
{
ht.Insert(make_pair(e, e));
}
ht.Insert(make_pair(15, 15));
ht.Insert(make_pair(25, 25));
ht.Insert(make_pair(35, 35));
ht.Insert(make_pair(45, 45));
}
//插入和删除
void TestHashTable2()
{
int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
HashTable<int, int> ht;
for (auto e : a)
{
ht.Insert(make_pair(e, e));
}
ht.Erase(12);
ht.Erase(3);
ht.Erase(33);
}
//字符串问题
void TestHashTable3()
{
//不实现类模板特化 显示传StringToInt
//HashTable ht;
//使用类模板特化 不需要显示传 更符合大佬设计的底层哈希
HashTable<string, string> ht;
ht.Insert(make_pair("Kevin", "凯文"));
ht.Insert(make_pair("Eddie", "彭于晏"));
ht.Insert(make_pair("Tom", "汤姆"));
ht.Insert(make_pair("Jerry", "杰瑞"));
ht.Insert(make_pair("", "null_string"));
}
//性能测试
void TestHashTable4()
{
size_t N = 900000;
HashTable<int, int> ht;
srand(time(0));
for (size_t i = 0; i < N; ++i)
{
size_t x = rand() + i;
ht.Insert(make_pair(x, x));
}
cout << "最大桶数据个数" << ht.MaxBucketSize() << endl;
}
}