引子
先来看一个问题:
SCP基金会建立了一个收容点,为每一个收容的SCP都编号为“SCP-xxxx-xx”。编号的每一个SCP都有一个专门的文件描述其收容注意事项。请你设计一个程序,输入编号就可以找到对应的文件并且输出。
这个问题的核心在于,如何通过编号迅速找到对应的文件。如果我们使用fstream
对象来代表文件的话,一个可能的思路是这样的:
struct SCP{
fstream document;//文件
string codeName;// 编号
};
int main(){
vector<SCP> data;
...
for(auto ix = data.begin(); ix != data.end(); ++ix){
if(ix->codeName == key){
cout << ix->document;
break;
}
}
}
用一个数组存储打包的数据,然后通过搜索算法得到结果。这样做可行,但会花费O(n)的时间。有没有更快捷的方法,可以输入编号之后马上就得到对应的文件呢?
哈希表就是为此而生。
哈希表
哈希表(Hash Map)维护很多对数据。每一对数据都由键和值组成。其特性是只要输入键,就可以在O(1)时间内找到对应的值。如果使用一般未排序的线性表的话,搜索过程会花掉O(n)时间。刚才例子里的codeName就是键,document就是值。
因为这一种特性很像是查字典的时候,可以根据字典索引迅速找到对应的字词。所以Python提供的哈希表数据结构就叫做dictionary(字典)。
哈希函数
哈希表的特性来源于哈希函数,哈希函数具有把输入值在O(1)时间内转化为一个索引的能力。
不过并非所有类型数据都可以作为输入的键值。Python把这样的数据类型形容为Unhashable。
哈希函数使用的这种算法就称作哈希算法。哈希算法还有许多其他特性,比如不能用输出的索引值逆推出输入值。但在这里我们只关心这一个特点。
这个能力对我们非常有用。我们只需要把编号输入给哈希函数,然后得到一个索引,只要把document存储到数组上索引指示的位置,之后我们就可以输入编号到哈希函数得到索引 ⇒ \Rightarrow ⇒按照索引找到数组内对应的文件。全程只消耗O(1)时间,基本上可以认为做到了完美。
哈希函数の碰撞
但事与愿违,鸽巢原理指出,不论多高明的哈希算法,都无法保证每个键值都获得独一无二的索引。如果两个输入的键值获得了相同的索引,在数组上存储时就会发生冲突。不仅如此,这还会打破键与值之间一一对应的特性(因为好几个键获得了同一个索引),从而给查找也带来麻烦。
首先,我们需要在索引相同的情况下找到正确的值。所以把key和data绑定起来:
class HashNode {
public:
int key;
string data;// 演示只使用了string作为data类型,其实key和data的类型可以自己选择。
HashNode() : key(0), data() {}
HashNode(const int key, const string &data) : key(key), data(data) {}
};
其次,我们把HashNode储存在一个list内,再把list储存在一个vector内,这样就形成了一个二维结构,索引重复的就放入链表内。
图源《我的第一本算法书》,侵删。
简单的代码实现
因为本文核心是了解哈希表的结构,所以我并不会花大量时间去实现底层结构,点到为止即可。
所以我会使用STL容器来帮助我实现这样的数组-链表结构。
class HashNode {
public:
int key;
string data;
HashNode() : key(0), data() {}
HashNode(const int key, const string &data) : key(key), data(data) {}
};
// Seemed a little useless...but it may become nessasary when expanding the system...I guess...
class HashList : public list<HashNode> {
typedef unsigned size_t;
public:
size_t index;
HashList() : list<HashNode>(), index(0) {}
HashList(const HashList &rhs) : index(rhs.index), list<HashNode>(rhs) {}
};
class HashTable {
typedef unsigned size_t;
private:
// Using list to tackle Hash collision.
vector<HashList> _table;
public:
// A Hash method, but not a good method.
static size_t Hash(int key) {
return (key*key*key+key*key+2*key+7)%50;
}
HashTable() : _table(50) {}
HashTable(const HashTable &rhs) : _table(rhs._table) {}
~HashTable() = default;
void insert(int key, const string &data) {
_table[Hash(key)].push_back(HashNode(key, data));
}
void remove(int key) {
auto ix(Hash(key));
if (_table[ix].empty()) {
return;
}
HashList::iterator it{_table[ix].begin()};
while (it != _table[ix].end()) {
if (it->key == key) {
_table[ix].erase(it);
}
++it;
}
}
bool isEmpty() const {
bool empty{true};
for (auto &i : _table) {
empty = i.empty();
}
return empty;
}
const string &operator[](int key) const {
auto ix{Hash(key)};
for (auto &i : _table[ix]) {
if (i.key == key) {
return i.data;
}
}
throw "No that value";
}
string &operator[](int key) {
auto ix{Hash(key)};
for (auto &i : _table[ix]) {
if (i.key == key) {
return i.data;
}
}
throw "No that value";
}
};
由于以上代码是作者在极度疲倦状态下写成的,所以质量不佳,见谅。