了解过搜索二叉树与红黑树后,它们的结构特点主要是为了进行快速查找, O ( l o g 2 N ) O(log_2N) O(log2N)的时间复杂度,通常在几次关键码的比较后就能找到目标元素。但是最最理想的搜索,是能够像数组那样,知道元素的下标,直接就可以访问,时间复杂度达到 O ( 1 ) O(1) O(1)。
其实哈希结构就是与数组类似的,通过元素的关键码计算出下标(哈希映射)。通过这一步计算,可以将元素存储到对于位置,同样也可以在对应位置访问该元素。
对于两个数据元素i和j,其中关键码 k i ≠ k j k_i \ne k_j ki=kj,但是hash( k i k_i ki) = hash( k j k_j kj)
即:不同关键字通过相同哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
引起哈希冲突的一个原因:哈希函数不够合理
哈希函数:能将任意长度的关键码映射成固定长度的数据(下标)
常见的哈希函数(了解):
取关键字的某个线性函数为散列地址:$Hash(Key)= A*Key + B $
散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数
H a s h ( k e y ) = k e y m o d p ( p < = m ) Hash(key) = key \bmod p (p <= m) Hash(key)=keymodp(p<=m)
选择质数作为除数:由于质数本身不存在多余的因子,所以与其他数做取模运算的结果更加分散
例如:关键字为1234,平方为1522756,选取中间3位:277作为哈希地址
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
几部分叠加求和,并按散列表表长,取后几位作为散列地址
H a s h ( k e y ) = r a n d o m ( k e y ) Hash(key) = random(key) Hash(key)=random(key)
解决哈希冲突两种常见的方法是:闭散列和 开散列
闭散列也叫做开放定址法
当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中下一个空位置中
当发生哈希冲突时,从发生冲突的位置开始,依次向后探测,直到找到下一个空位置为止
int index = Hash(key);
while(hashTable[index].state == EXITS)
{
++index;//向后遍历
}
线性探测的缺陷是产生冲突的数据会堆积在一起 ,寻找某个关键码可能需要多次比较,导致效率降低。
寻找下一个空位置时,不再挨着逐个去找,选择逐步跳跃式的去找,避免冲突堆积
int index = Hash(key);
int start = index, i = 0;
while(hashTable[index].state == EXITS)
{
index = start + i * i;
++i;
}
随着插入数据的增多, 插入的数据产生冲突的概率也增加了。冲突的增加,在查找时的效率也会降低。
装载因子(load factor) = 表中有效数据个数 / 空间大小
通常来说,当哈希表的装载因子超过0.7时就需要考虑进行扩容,否则可能会导致插入/查找操作的效率大幅下降。
enum State
{
EMPTY, //空状态
EXITS, //使用状态
DELETE, //删除状态
};
template
struct HashDate
{
pair _kv; // 存储数据的键值对
State _state; // 标记状态
};
template>
class HashTable
{
private:
vector> _table; // 哈希数组
size_t _n = 0; // 有效数据个数,来计算装载因子
};
将k类型的关键码转换为整型
template
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
// 特化 20:12继续
template<>
struct HashFunc
{
// BKDR哈希算法
size_t operator()(const string& key)
{
size_t val = 0;
for (auto ch : key)
{
val *= 131;
val += ch;
}
return val;
}
};
BKDR哈希算法是由徐盛忠教授于1992年提出的一种哈希函数,它是通过将字符串视为整数来计算哈希值的。其基本思想是使用一个较小的质数seed(比如31、131、1313、13131等),将字符串中各个字符的ASCII码转化为整数后,与seed相乘并求和得到一个哈希值
HashDate* find(const K& key)
{
if (_table.size() == 0)//没有元素
{
return nullptr;
}
HashFunc hf; //通过仿函数将关键码转换为整型
size_t index = hf(key) % _table.size();//除留余数法
// 线性探测
while (_table[index]._state != EMPTY)
{
// 找到待查找元素,并返回它的地址
if (_table[index]._state == EXITS &&
_table[index]._kv.first == key)
{
return &_table[index];
}
++index;
index %= _table.size();
}
return nullptr;// 不存在此元素
}
bool insert(const pair& kv)
{
if (find(kv.first))
{
return false; // 元素已存在
}
if (_table.size() == 0)
{
_table.resize(10); //设置哈希表初始空间大小
}
// 装载因子超过阈值0.7就进行扩容
else if( _n * 1.0 /_table.size() > 0.7)
{
//创建一个新的哈希表,大小为原哈希表的2倍
HashTable newHT;
newHT._table.resize(2 * _table.size());
//将原数据插入到新哈希表
for (auto& e : _table)
{
if (e._state == EXIST)
{
newHT.Insert(e._kv);
}
}
//交换两个哈希表
_table.swap(newHT._table);
}
// 用哈希函数算出哈希表中的映射位置
HashFunc hf;
size_t index = hf(kv.first) % _table.size();
// 线性探测
while (_table[index]._state == EXITS)
{
++index;
index %= _table.size(); // 防止下标越界
}
//插入元素
_table[index]._kv = kv;
_table[index]._state = EXITS;
++_n;
return true;
}
bool erase(const K& key)
{
//查找该元素
HashDate* ret = find(key);
if (ret == nullptr)
return false; //不存在
else // 找到
{
ret->_state = DELETE;//删除
--_n;
return true;
}
}
开散列也叫做链地址法
首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
template
struct HashNode
{
pair _kv;
HashNode* _next;
HashNode(const pair& kv)
:_kv(kv)
, _next(nullptr)
{}
};
template>
class HashTable
{
typedef HashNode Node;
private:
vector _tables;//哈希表
size_t _size = 0; // 存储有效数据个数
};
Node* find(const K& key)
{
if (_table.size() == 0)
return nullptr; //哈希表为空
//哈希映射
hash hash;
size_t index = hash(key) % _table.size();
Node* cur = _table[index];
while (cur != nullptr)
{
if (cur->_kv.first == key)
return cur;
else cur = cur->_next;
}
return nullptr;
}
bool insert(const pair& kv)
{
// 去重
if (Find(kv.first)) return false;
if (_table.size() == 0)
{
_table.resize(53); //设置哈希表初始空间大小
}
// 装载因子超过阈值0.7就进行扩容
else if (_n * 1.0 / _table.size() > 0.7)
{
//创建一个新的哈希表,大小为原哈希表的2倍
vector newTables;
newTables.resize(__stl_next_prime(_tables.size()), nullptr);
// 旧表中节点移动映射新表
Hash hash;
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];//首节点
while (cur != nullptr)
{
Node* next = cur->_next;
size_t hashi = hash(cur->_kv.first) % newTables.size();
//头插
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
//交换两个哈希表
_tables.swap(newTables);
}
Hash hash;
size_t hashi = hash(kv.first) % _tables.size();
// 头插
Node* newnode = new Node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_size;
return true;
}
//返回质数
size_t __stl_next_prime(size_t n)
{
static const size_t __stl_num_primes = 28;
static const size_t __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
};
for (size_t i = 0; i < __stl_num_primes; ++i)
{
if (__stl_prime_list[i] > n)
{
return __stl_prime_list[i];
}
}
return -1;
}
使用质数作为除数
bool erase(const K& key)
{
//哈希表为空
if (_tables.size() == 0) return nullptr;
//find
Hash hash;
size_t hashi = hash(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;
--_size;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
~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;
}
}
观看~~