个人主页:Lei宝啊
愿所有美好如期而遇
目录
前言
C语言中,有这样一道题目,给定一串字符串,统计字母的数量,(字母全为小写),我们是怎么做的?
不管是开123个空间(z的ASCII值为122),还是27个空间(26个字母),本质上都是哈希表,以他们的值为key值去存value值,在C中,只不过是以下标充作key值,数组元素充作value值。
在C++中,我们有了unordered_set和unordered_map,前者是存<key,key>,后者是存<key,value>,底层都是哈希表。
接下来我们将会介绍哈希表。
哈希表
哈希冲突
不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
哈希函数
我们这里只介绍两种比较常用的哈希函数:直接定址法,除留余数法。
直接定址法
我们前言中所说到的方法就是直接定址法,取关键字的某个线性函数为散列地址:
Hash(key) = A * key + B,我们前言中A = 1, B = 0。
例如:
char s[] = "abcdef"; 他们的ASCII码值分别为97,98,99,100,101,102,那么他们的key值我们可以直接取,或者减去97,以数组下标0作为起始,也就是Hash(key) = 1 * key - 97。
- 优点:简单、均匀
- 缺点:需要事先知道关键字的分布情况
- 使用场景:适合查找比较小且连续的情况
比如:
1,2,3,99,999,4900990。这样的一串数字,需要开多少空间? 这种方法就不合适了。
除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数, 按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
一般来说,我们求余的那个p的值为数组的空间大小(C中),在C++中,我们使用vector时,就不能求余空间大小,而是求余有效元素个数(resize)。
举个例子:
但是这种情况该怎么办?
他们求余后的key值是相同的,这样就导致了哈希冲突,而解决哈希冲突的两种常见办法就是:闭散列和开散列。
闭散列
什么是闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有 空位置,那么可以把key存放到冲突位置中的“下一个”空位置中去。
那如何寻找下一个空位置呢?
我们采用线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
元素的插入
Hash(key) = key % p p = 10
元素的删除
我们删除元素时不可以直接删除,比如我们寻找14这个值,但是在这之前删除了4,4的位置变成了空,14这个值我们计算它的Hash(key),得到4,而此时这个位置是空的,这代表表中没有14吗?不是这样的,所以我们不能直接删除,而是以状态来标定。
我们设置三个状态:存在,删除,空。当我们删除一个数时,将他的状态修改为删除,将来查找时遇到删除继续向后查找,直到某个位置的状态为空。
这个时候就会有一个问题:如果我的表是满的,而且我要查找的元素不在表中,岂不是会发生死循环?是的,所以我们就不让他满,当表填充百分之70,我们扩容,这不但保证了不会死循环,而且还提高了整表的效率,在插入和查找时不会有更多的冲突,也就是说,其实填充率越小,那么冲突越少,事实也确实是这样的。
代码实现与解析
//状态标定
enum State
{
EMPTY,
EXIST,
DELETE
};
接下来我们建一个HashTable类,这个类的成员函数是一个vector,以及有效数据个数,那么vector要存的元素类型应该是什么?所以我们还需要模板,但是一个位置不能同时存值和状态,所以我们就应该清楚,vector的元素类型应该是自定义类型了,我们将定义一个HashNode结构体。
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
State state = EMPTY;
};
接着我们开始实现哈希表类,我们先要明确这个类的成员函数:
private:
vector<Node> _table;
//实际存的数据个数
size_t _size = 0;
第二点就是他的模板参数列表:
template<class K, class V, class Hash = HashFunc<K>>
那么第三个模板参数是什么意思呢?
就是我们计算Hash(key)的方法,如果我们要插入的值是string类型,还能直接求余取值吗?所以我们需要一个这样的仿函数来进行计算:
//默认为整型值
template<class K>
struct HashFunc
{
size_t operator()(const K& num)
{
return num;
}
};
template<>
struct HashFunc<string>
{
size_t operator()(const string& s)
{
size_t num = 0;
for (auto e : s)
{
num += e;
num *= 31;
}
return num;
}
};
插入方法:(代码有详细注释)
bool
insert(const pair<K, V>& kv)
{
//查找节点是否存在,存在则返回false
if (Find(kv.first))
return false;
/* 限定值给定0.7
判断空间是否已达到限定,达到则扩容并重新定位
但是扩容并不是个好选择,我们重新开一个空间,最后
使用vector的swap更好
*/
if (_size / _table.size() * 10 >= 0.7)
{
HashTable<K, V> newtable;
newtable._table.resize(_table.size() * 2);
for (auto &e : _table)
{
//复用insert,只有扩容时才会进入这个if语句
if (e.state == EXIST)
newtable.insert(e._kv);
}
_table.swap(newtable._table);
}
/* 新节点位置安排
如果求余得到的位置处不为存在,则插入
否则向后寻找,直到某处为空或者删除
*/
Hash hs;
int pos = hs(kv.first) % _table.size();
while (_table[pos].state == EXIST)
{
pos++;
pos %= _table.size();
}
_table[pos]._kv = kv;
_table[pos].state = EXIST;
_size++;
return true;
}
我们上面代码涉及到了另一个方法Find:
/*
* 查找函数,返回值为Node*类型,即HashNode<K, V>,(_kv, state)
* 如果存在且key值相同,则返回位置。
*/
Node*
Find(const K& key)
{
Hash hs;
int pos = hs(key) % _table.size();
while (_table[pos].state != EMPTY)
{
if (_table[pos].state == EXIST
&& _table[pos]._kv.first == key)
return &_table[pos];
pos++;
pos %= _table.size();
}
return nullptr;
}
最后就是我们的删除函数:
bool Erase(const K& key)
{
Node* cur = Find(key);
if (!cur)
return false;
cur->state = DELETE;
_size--;
return true;
}
整体代码
enum State
{
EMPTY,
EXIST,
DELETE
};
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
State state = EMPTY;
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
typedef HashNode<K, V> Node;
/*
@param 设定元素个数,默认为10
构造函数
*/
HashTable(size_t size = 10)
{
//resize是重新设定元素个数
_table.resize(size);
}
/*
* 查找函数,返回值为Node*类型,即HashNode<K, V>,(_kv, state)
* 如果存在且key值相同,则返回位置。
*/
Node*
Find(const K& key)
{
Hash hs;
int pos = hs(key) % _table.size();
while (_table[pos].state != EMPTY)
{
if (_table[pos].state == EXIST && _table[pos]._kv.first == key)
return &_table[pos];
pos++;
pos %= _table.size();
}
return nullptr;
}
/*
@param 插入的元素为pair<K, V>类型
插入函数
*/
bool
insert(const pair<K, V>& kv)
{
//查找节点是否存在,存在则返回false
if (Find(kv.first))
return false;
/* 限定值给定0.7
判断空间是否已达到限定,达到则扩容并重新定位
但是扩容并不是个好选择,我们重新开一个空间,最后
使用vector的swap更好
*/
if (_size / _table.size() * 10 >= 0.7)
{
HashTable<K, V> newtable;
newtable._table.resize(_table.size() * 2);
for (auto &e : _table)
{
//复用insert,只有扩容时才会进入这个if语句
if (e.state == EXIST)
newtable.insert(e._kv);
}
_table.swap(newtable._table);
}
/* 新节点位置安排
如果求余得到的位置处不为存在,则插入
否则向后寻找,直到某处为空或者删除
*/
Hash hs;
int pos = hs(kv.first) % _table.size();
while (_table[pos].state == EXIST)
{
pos++;
pos %= _table.size();
}
_table[pos]._kv = kv;
_table[pos].state = EXIST;
_size++;
return true;
}
bool Erase(const K& key)
{
Node* cur = Find(key);
if (!cur)
return false;
cur->state = DELETE;
_size--;
return true;
}
private:
vector<Node> _table;
//实际存的数据个数
size_t _size = 0;
};
}
开散列
什么是开散列
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地 址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链 接起来,各链表的头结点存储在哈希表中。
图示:
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
代码实现与解析
我们仍然先创建HashTable类,他的成员变量我们从上图可以看出,仍然是vector和size,但是vecotr的元素的结构应该改为链式结构,也就是这样:
template<class K, class V>
struct HashNode
{
HashNode<K, V>* next;
pair<K, V> _kv;
HashNode(const pair<K, V>& kv)
:next(nullptr)
,_kv(kv)
{}
};
类的模板参数列表:
template<class K, class V, class Hash = HashFunc<K>>
HashFunc和闭散列的计算方法是一样的。
//默认为整型值
template<class K>
struct HashFunc
{
size_t operator()(const K& num)
{
return num;
}
};
template<>
struct HashFunc<string>
{
size_t operator()(const string& s)
{
size_t num = 0;
for (auto e : s)
{
num += e;
num *= 31;
}
return num;
}
};
构造函数:
HashTable()
{
_table.resize(10);
}
插入方法:
我们这里浅谈一下Hash这个仿函数,我们创建一个Hash对象,然后使用这个对象里的operator()重载,计算key值。
这里我们是当实际存的有效元素的个数等于表的有效元素个数时进行扩容,但是如果说真的要再开一个新表,重新去创建这些新节点,开销还是很大的,所以我们直接遍历旧表,将旧表的节点全部接到新表中去,当然我们不是新建一个对象,而是新建一个vector,最后使旧表的vector调用成员函数swap与新的vector交换就可以。(如果说我们新建一个表,并且节点全部重新new,那么不仅仅有重新new节点的开销,我们还需要对他进行析构)
bool insert(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
Hash hs;
if (_table.size() == _size)
{
vector<Node*> newtable;
newtable.resize(_table.size() * 2);
for (auto e : _table)
{
Node* cur = e;
Node* next;
while (cur)
{
int pos = hs(cur->_kv.first) % newtable.size();
next = cur->next;
cur->next = newtable[pos];
newtable[pos] = cur;
cur = next;
}
}
_table.swap(newtable);
}
int pos = hs(kv.first) % _table.size();
Node* cur = _table[pos];
Node* newnode = new Node(kv);
newnode->next = _table[pos];
_table[pos] = newnode;
_size++;
return true;
}
查找方法:
Node* Find(const K& key)
{
Hash hs;
int pos = hs(key) % _table.size();
Node* cur = _table[pos];
while (cur)
{
if (cur->_kv.first == key)
return cur;
cur = cur->next;
}
return nullptr;
}
删除方法:
这里我们要解释一下,先根据key值找到要删除节点的位置,然后根据这个位置循环找到这个节点,如果这个节点不是vector当前位置的唯一节点,我们删除当前节点并完成后续连接操作,如果是唯一节点,那么删除这个节点,vector这个位置直接置空,--有效元素个数。(比较巧妙的就是这个prev指针,不是博主想出来的(滑稽),巧在如果说直接找到了,但是prev为空,那么就说明找到的这个节点是当前位置唯一的节点,如果说prev不为空,那么就说明找到的这个节点是有前驱节点或后继节点的)
bool Erase(const K& key)
{
Hash hs;
int pos = hs(key) % _table.size();
Node* prev = nullptr;
Node* cur = _table[pos];
while (cur)
{
if (cur->_kv.first == key)
{
if (prev)
{
prev->next = cur->next;
}
else
{
_table[pos] = nullptr;
}
delete cur;
--_size;
return true;
}
prev = cur;
cur = cur->next;
}
return false;
}
闭散列我们新增迭代器:
那么开散列我们为什么不使用迭代器呢?emmm,倒也不是不行,只是那里懒得写,其实原理都差不多,主要就是operator++的重载,开散列那里,operator++就是寻找当前节点的下一个节点,也就是pos++,找到一个状态为存在的元素就能返回(就是个遍历)。
而我们这里operator++,寻找当前节点的下一个节点就不是pos++了,而是如果这个节点当前位置仅仅只有自己这个节点或者说没有下一个节点,还需要向后找vector不为空的位置的第一个节点,如果当前位置有下一个节点,那么返回下一个节点。
我们的迭代器类不仅仅需要当前节点的地址,还需要HashTable对象的地址,否则当我们的节点在vector中的位置很尴尬时,是找不到下一个位置的,并且由于HashTable和HashIterator是相互调用,必然是一前一后,所以还需要对其中一个类在前面做声明(注意参数的默认值写在声明就可以了,声明和定义不能同时出现,同时声明时需要将模板的参数列表也要写出),而且我们的HashIterator需要使用HashTable中的成员变量,但是这个成员变量是私有的,所以我们需要让HashIterator类变成HashTable类的友元,在声明友元时,同样需要带上模板参数。
template<class K, class V, class Hash = HashFunc<K>>
class HashIterator
{
typedef HashNode<K, V> Node;
typedef HashTable<K, V, Hash> hstable;
typedef HashIterator<K, V, Hash> iterator;
public:
HashIterator(Node* n, hstable* h)
:node(n)
,hst(h)
{}
iterator& operator++()
{
if (node->next)
{
node = node->next;
}
else
{
Hash hs;
int pos = hs(node->_kv.first) % hst->_table.size();
pos++;
while (pos < hst->_table.size())
{
if (hst->_table[pos])
{
node = hst->_table[pos];
break;
}
pos++;
}
if (pos == hst->_table.size())
node = nullptr;
}
return *this;
}
pair<K, V>& operator*()
{
return node->_kv;
}
bool operator!=(const iterator& s)
{
return node != s.node;
}
private:
Node* node;
hstable* hst;
};
这里我们就需要析构函数了,因为vector每个位置挂了一串我们自己new出来的节点。
~HashTable()
{
for (auto& e : _table)
{
Node* cur = e;
while (cur)
{
Node* next = cur->next;
delete cur;
cur = next;
}
e = nullptr;
}
}
整体代码
//开散列哈希表
namespace Open
{
template<class K, class V>
struct HashNode
{
HashNode<K, V>* next;
pair<K, V> _kv;
HashNode(const pair<K, V>& kv)
:next(nullptr)
,_kv(kv)
{}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable;
template<class K, class V, class Hash = HashFunc<K>>
class HashIterator
{
typedef HashNode<K, V> Node;
typedef HashTable<K, V, Hash> hstable;
typedef HashIterator<K, V, Hash> iterator;
public:
HashIterator(Node* n, hstable* h)
:node(n)
,hst(h)
{}
iterator& operator++()
{
if (node->next)
{
node = node->next;
}
else
{
Hash hs;
int pos = hs(node->_kv.first) % hst->_table.size();
pos++;
while (pos < hst->_table.size())
{
if (hst->_table[pos])
{
node = hst->_table[pos];
break;
}
pos++;
}
if (pos == hst->_table.size())
node = nullptr;
}
return *this;
}
pair<K, V>& operator*()
{
return node->_kv;
}
bool operator!=(const iterator& s)
{
return node != s.node;
}
private:
Node* node;
hstable* hst;
};
template<class K, class V, class Hash>
class HashTable
{
template<class K, class V, class Hash>
friend class HashIterator;
public:
typedef HashIterator<K, V, Hash> iterator;
typedef HashNode<K, V> Node;
iterator begin()
{
for (auto& e : _table)
{
if (e)
{
return iterator(e, this);
}
}
return end();
}
iterator end()
{
return iterator(nullptr, this);
}
HashTable()
{
_table.resize(10);
}
Node* Find(const K& key)
{
Hash hs;
int pos = hs(key) % _table.size();
Node* cur = _table[pos];
while (cur)
{
if (cur->_kv.first == key)
return cur;
cur = cur->next;
}
return nullptr;
}
~HashTable()
{
for (auto& e : _table)
{
Node* cur = e;
while (cur)
{
Node* next = cur->next;
delete cur;
cur = next;
}
e = nullptr;
}
}
bool insert(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
Hash hs;
if (_table.size() == _size)
{
vector<Node*> newtable;
newtable.resize(_table.size() * 2);
for (auto e : _table)
{
Node* cur = e;
Node* next;
while (cur)
{
int pos = hs(cur->_kv.first) % newtable.size();
next = cur->next;
cur->next = newtable[pos];
newtable[pos] = cur;
cur = next;
}
}
_table.swap(newtable);
}
int pos = hs(kv.first) % _table.size();
Node* cur = _table[pos];
Node* newnode = new Node(kv);
newnode->next = _table[pos];
_table[pos] = newnode;
_size++;
return true;
}
bool Erase(const K& key)
{
Hash hs;
int pos = hs(key) % _table.size();
Node* prev = nullptr;
Node* cur = _table[pos];
while (cur)
{
if (cur->_kv.first == key)
{
if (prev)
{
prev->next = cur->next;
}
else
{
_table[pos] = nullptr;
}
delete cur;
--_size;
return true;
}
prev = cur;
cur = cur->next;
}
return false;
}
private:
vector<Node*> _table;
size_t _size = 0;
};
}