简单说明
hashtable适用于需要频繁插入、删除、查找的场合、在这些场合中hashtable都可以常数平均时间完成、然而之所以hashtable的效率这么高、是因为在以上这些操作时都是通过hash function直接定位元素在表中的位置然后直接操作。不可避免的有一些部分性质或全部性质相同的元素被定位到同一个位置上、这时新的问题产生了:解决冲突。实际上解决方法有很多:像线性探测、二次探测、开链。
线性探测和二次探测实现方法非常相似、线性探测:如果定位的位置已经被占据、那么就 pos+ 1 向下一个位置查看、直到找到一个空位置为止(在表中循环查找)。二次探测:如果位置pos已经被使用、那么下一个查看位置就是pos + 1^2、pos + 2^2、pos + 3^2。
开链:每个表格维护着一个list、hashfuction定位到某个list然后、我们在list上进行插入删除操作。虽然针对list而进行的查找是一种线性操作、但是如果list足够短、效率还是很高的。
使用二次探测、如果假设表格大小为质数(为了减小冲突使用质数、开链法不需要)、而且保证元素个数不得超过桶个数的一半(如果超过就重新配置表格)、那么就可以确定每插入一个新元素所需的探测次数不超过2次。
虽然hashtable在许多操作上效率都很高、但是这也取决于hash函数、如果开链的hashtable、所有元素都在同一个链表上、那么查找效率就成了o(n)。
代码实现
下面是使用hashtable开链法实现的set
#ifndef HASHTABLE_H #define HASHTABLE_H #include <cstdio> #include <cstdlib> #include <vector> #include <functional> using namespace std; //开链式 HashTable class HashTable final { private: static const int PRIME_COUNT = 28; static const unsigned int primes[PRIME_COUNT]; //28个质数、开链式hashtable其实不需要桶数为质数这里就是以STL中的实现为例了 struct List { int val; List* next; List(int v = 0):val(v),next(0){} }; typedef function<void(int)> FF; //函数类型提供给foreach使用 vector<List*> table; //存放每个“桶”的头指针 unsigned bucket_count; //桶数量 unsigned element_count; //目前表中元素数量 public: HashTable(unsigned n):table(prime(n),NULL),bucket_count(0), element_count(0) { bucket_count = table.size(); } ~HashTable(){ release(); } private: unsigned prime(unsigned n) //根据提供需要的“桶”个数、转换为稍大一些的质数 { for(int i = 0; i < PRIME_COUNT; i++){ if(primes[i] >= n){ return primes[i]; } } return primes[PRIME_COUNT - 1]; } unsigned bucket_num(int val,unsigned num) //找到当前元素val应该放在桶数为num时的哪一个桶中 { if(val < 0) val = -val; return val % num; } void resize(unsigned element_num) //重新调整表的长度(桶的数量) { const auto size = table.size(); if(element_num > size) {//需要重新配置空间:为了维护hashtable的插入和查找的效率、当元素个数大于桶数时就要重新配置 const auto new_bucket_num = prime(element_num); vector<List*> t(new_bucket_num,NULL); //处理旧桶中的数据 for(unsigned i = 0; i < size; i++) { List* node = table[i]; while(node) { const auto index = bucket_num(node->val,new_bucket_num); table[i] = node->next; node->next = t[index]; t[index] = node; //重新指向这个桶中的下一个位置 node = table[i]; } } table.swap(t); bucket_count = new_bucket_num; } } public: bool insert(int val); //插入元素 bool erase(int val); //删除元素 void foreach(FF f); //用于测试的foreach void release(); //释放空间 unsigned qbuckets(); //得到桶数目 unsigned qelements(); //得到元素数目 }; const unsigned int HashTable::primes[PRIME_COUNT] = {//28个质数的数组 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 }; bool HashTable::insert(int val) { resize(element_count + 1); const auto index = bucket_num(val,bucket_count); //头添加 List* node = new List(val); node->next = table[index]; table[index] = node; ++element_count; return true; } bool HashTable::erase(int val) { const auto index = bucket_num(val,bucket_count); if(table[index] && table[index]->val == val){ auto obj = table[index]; table[index] = obj->next; delete obj; obj = NULL; --element_count; return true; } for(List* obj = table[index];obj && obj->next; obj = obj->next){ if(obj->next->val == val){ auto del = obj->next; obj->next = del->next; delete del; del = NULL; --element_count; return true; } } return false; } void HashTable::foreach(FF f) { List* obj = NULL; for(unsigned i = 0; i < bucket_count; i++) { obj = table[i]; for(;obj;obj = obj->next) { f(obj->val); } } } void HashTable::release() { if(element_count > 0){ List* obj = NULL; List* del = NULL; for(unsigned i = 0; i < bucket_count; i++) { obj = table[i]; for(;obj;) { del = obj; obj = obj->next; delete del; del = NULL; } table[i] = NULL; } element_count = 0; } } unsigned HashTable::qbuckets() { return bucket_count; } unsigned HashTable::qelements() { return element_count; } #endif rb_tree和hashtable对比:
rb_tree保证在插入、删除、查找的时间复杂度都是log(n)、而hashtable操作的时间复杂度是常数级的、但是hash function的耗时也要考虑。
rb_tree是有序的、hashtable是无序的。
一般情况hashtable所消耗内存空间较大。