闭散列表
可以根据散列表解决散列冲突的方法把散列表分成开散列表和闭散列表:
当发生散列冲突时,使用开放寻址法把冲突的数据项放入别的空的单元中,称为闭散列表,其中寻址方法有线性探测法,平方探测法等。
当发生散列冲突时,使用拉链法,把映射到同一地址的数据元素组织成链表,称为开散列表
闭散列表的装载因子不能大于1,且一般当装载因子大于0.75时可认为将会产生大量散列冲突导致散列表性能下降。因此闭散列表在装在因子大于0.75时需要启动扩容机制。
闭散列表扩容的方法有两种,第一种为一次性扩容,即申请一个两倍于原散列表大小的新表,把所有数据搬移至新表中。但是这种扩容机制会让个别几次插入操作的时间特别长(即刚好需要扩容的那几次),导致性能不够稳定。第二种方法为动态扩容,即同时维护两个表,每次插入操作都只把原表中的一项数据移动到新表,动态扩容的好处是几乎每次插入操作的时间都差不多。
开散列表的装载因子可以大于1,但是当一个单元中的元素过多时遍历链表也会增加操作的时间复杂度。一般的做法是当一个单元中的链表长度大于8时把链表转换成红黑树以降低查询的时间复杂度。
以线性探测法解决散列冲突的闭散列表代码如下(采用的是一次性扩容机制)
#include <iostream> using namespace std; // 键值对 template<class KEY, class VALUE> struct PAIR{ KEY key; VALUE value; PAIR(){} PAIR(KEY k, VALUE v):key(k), value(v){} }; // 动态查找表抽象类 template<class KEY, class VALUE> class dynamicSearchTable{ public: virtual PAIR<KEY, VALUE>* find(const KEY &x) const = 0; // 根据键在哈希表中查找键值对 virtual bool insert(const PAIR<KEY, VALUE> &x) = 0; // 往哈希表中插入一个键值对 virtual bool remove(const KEY &x) = 0; // 根据键在哈希表中删除键值对 virtual ~dynamicSearchTable(){}; }; // 基于线性探测法的闭散列表的实现 template<class KEY, class VALUE> class closeHashTable:public dynamicSearchTable<KEY, VALUE>{ private: struct node{ // 哈希表中用到的节点 PAIR<KEY, VALUE> data; int state; // 节点状态 0:空, 1:占用, 2:被删除 node(){state = 0;} }; node* array; // 哈希表储存 int MaxSize; // 哈希表的储存上限 int CurrentSize; // 当前哈希表已储存的节点数 bool expansion(); // 扩容函数,这里使用的是一次性扩容,实际上还可以使用均摊法动态扩容 int (*Hash)(const KEY &x); // 指向哈希函数的指针,该指针可以指向所有参数表为const KEY & x返回值为int的函数 static int defaultHash(const KEY &x){return int(x);} // 默认哈希函数 public: closeHashTable(int size = 101, int (*f)(const KEY & x) = defaultHash); ~closeHashTable(){delete []array;} PAIR<KEY, VALUE>* find(const KEY &x) const; bool insert(const PAIR<KEY, VALUE> &x); bool remove(const KEY &x); }; template<class KEY, class VALUE> bool closeHashTable<KEY, VALUE>::expansion(){ // 一次性扩容 int newMaxSize = MaxSize * 2; node* newarray; try{newarray = new node[newMaxSize];} catch(bad_alloc &memExp){return false;} for(int i = 0; i < MaxSize; ++i){ if(array[i].state == 1){ int pos = Hash(array[i].data.key) % newMaxSize; while(newarray[pos].state == 1) pos = (pos + 1) % newMaxSize; newarray[pos].data = array[i].data; newarray[pos].state = 1; } } MaxSize = newMaxSize; delete []array; array = newarray; return true; } template<class KEY, class VALUE> closeHashTable<KEY, VALUE>::closeHashTable(int size, int (*f)(const KEY &x)){ MaxSize = size; CurrentSize = 0; array = new node[MaxSize]; Hash = f; } template<class KEY, class VALUE> bool closeHashTable<KEY, VALUE>::insert(const PAIR<KEY, VALUE> &x){ if(CurrentSize / MaxSize > 0.75 && !expansion()) return false; int initPos, Pos; initPos = Pos = Hash(x.key) % MaxSize; do{ if(array[Pos].state == 1) Pos = (Pos + 1) % MaxSize; else break; }while(Pos != initPos); array[Pos].data = x; array[Pos].state = 1; ++ CurrentSize; return true; } template<class KEY, class VALUE> PAIR<KEY, VALUE>* closeHashTable<KEY, VALUE>::find(const KEY &x) const{ int initPos, Pos; initPos = Pos = Hash(x) % MaxSize; do{ if(array[Pos].state == 0) break; if(array[Pos].state == 1 && array[Pos].data.key == x) return (PAIR<KEY, VALUE>*) &array[Pos]; Pos = (Pos + 1) % MaxSize; }while(Pos != initPos); return NULL; } template<class KEY, class VALUE> bool closeHashTable<KEY, VALUE>::remove(const KEY &x){ // 哈希表的删除都是迟删除,而不能是真正的删除 int initPos, Pos; initPos = Pos = Hash(x) % MaxSize; do{ if(array[Pos].state == 0) break; if(array[Pos].state == 1 && array[Pos].data.key == x){ array[Pos].state = 2; --CurrentSize; return true; } }while(initPos != Pos); return false; }
可以利用LeetCode上的第一题两数之和测试上面实现的哈希表,代码如下(题目详见leetcode)
int H(const int & x){ //自定义哈希函数 return abs(int(x*13.33261)); } class Solution { public: vector<int> twoSum(vector<int>& nums, int target) { closeHashTable<int, int> a(101, H); // 创建空哈希表 for(int i = 0; i < nums.size(); ++i) // 把vector中的数据都插入哈希表 a.insert(PAIR<int, int>(nums[i], i)); for(int i = 0; i < nums.size(); ++i){ a.remove(nums[i]); //先在哈希表中删除第一个当前检测的数据以免重复取 PAIR<int, int>* p = a.find(target - nums[i]); if(p != NULL) return vector<int>{i, p->value}; a.insert(PAIR<int, int>(nums[i], i)); //再把删除的重新插入 } return vector<int>{0}; } };
这里的自定义哈希函数乘了一个随意的系数之后,运行时间从760ms下降到12ms,可见碰撞问题多影响哈希表的效率。