散列表(拉链式和线性探测)
2016-01-05 19:04
417 查看
无论是顺序查找还是二分查找(包括二叉树),查找或插入的时间复杂度总会与数据的总数N有关,而散列表可以将查找和插入操作降低到常数级别。
最简单的一种常数级别的符号表,就是直接将数据的键值作为数组的索引,通过键值就可以立即访问到数据,时间复杂度为1.但是这种方法有一个缺点就是键值的分布并不是平均的,而且跨度可能会很大,这样就会导致数组中很多空间被浪费了。
而散列表就是解决了这个缺点,将键值映射到均匀分布的一定范围内的索引值。而在映射的过程中,由于键值的不均匀分布,必然会出现不同键值映射到同一索引的情况(即碰撞),所以还要进行碰撞处理,常用的有拉链式和线性探测等。
1.拉链式
顾名思义,就是把产生碰撞的数据按顺序排成链表,先通过映射找到所在链表,然后再在链表中查找数据。
除了映射处理,主要的时间都是花在链表查找上,所以链表越短查找越快,但是空间占用也越大,所以根据实际情况作平衡处理。另一方面,链表的平均长度=数据量N/链表数量M,因此应当尽量保证每条链表的长度差不多,那么就要使用更加均匀的映射函数。
我这里使用的是C++自带的hash函数,再按链表数M取余。即:
拉链式散列表的声明:
具体实现:
2.线性探测
这种方法是申请一段比数据总数N更大(至少为2N)的空间,那么当所有数据都存进去之后数据之间必然总会存在一些间隔,而这些间隔就是用来存放映射重叠的数据。因此他的思想就是每添加一个数据,先对其键值进行映射,然后根据映射结果找到相应位置进行存放,如果映射结果所在的位置已经被填充,就放到下一个位置(即所谓线性探测),直到找到合适的位置为止。而查找也是先进行映射,然后从映射结果所在位置开始向后查找,直到空格为止(由于空间比数据多,总会有空格存在)。显而易见,这种方法的查找效率与分配的存储空间有关,空间越大,连续存放的数据越少,则查找速度越快。
示意图:
类声明:
实现代码:
其中要注意的是空间是连续且循环的,所以最后一个格子的下一个格子是第一个格子,所以要用
另外,删除某个数据之后要将后面到空格之间的所有数据重新插入表中,否则由于删除后所在位置变成空格,会导致后面的某些元素无法被查找。
测试代码:
测试结果:
可以看到拉链式的几个链表长度都差不多,线性探测的数据分布也还可以,都没有太长的连续数据。
最简单的一种常数级别的符号表,就是直接将数据的键值作为数组的索引,通过键值就可以立即访问到数据,时间复杂度为1.但是这种方法有一个缺点就是键值的分布并不是平均的,而且跨度可能会很大,这样就会导致数组中很多空间被浪费了。
而散列表就是解决了这个缺点,将键值映射到均匀分布的一定范围内的索引值。而在映射的过程中,由于键值的不均匀分布,必然会出现不同键值映射到同一索引的情况(即碰撞),所以还要进行碰撞处理,常用的有拉链式和线性探测等。
1.拉链式
顾名思义,就是把产生碰撞的数据按顺序排成链表,先通过映射找到所在链表,然后再在链表中查找数据。
除了映射处理,主要的时间都是花在链表查找上,所以链表越短查找越快,但是空间占用也越大,所以根据实际情况作平衡处理。另一方面,链表的平均长度=数据量N/链表数量M,因此应当尽量保证每条链表的长度差不多,那么就要使用更加均匀的映射函数。
我这里使用的是C++自带的hash函数,再按链表数M取余。即:
std::hash<int> h; int h_val=h(key)%_chainNum;
拉链式散列表的声明:
class SeparateChainingHashST { public: SeparateChainingHashST(int chainNum);//指定链表数目 ~SeparateChainingHashST(); void put(int key, int val); ChainNode* get(int key); void showChains();//所有链表打印出来 private: void deleteAllNodes(ChainNode* head);//删除链表头为head的所有节点 inline unsigned int hash(int key);//计算哈希值(小于链表数) private: int _chainNum; NodePoniter *_chainHeads; //存放链表头的数组 std::hash<int> _h;//用来计算hash码 };
具体实现:
SeparateChainingHashST::SeparateChainingHashST(int chainNum) { _chainNum=chainNum; _chainHeads=new NodePoniter[chainNum];//新建一个数组来存放链表的头 for(int i=0;i<chainNum;i++)//将所有链表头初始化为空指针 _chainHeads[i]=nullptr; } unsigned int SeparateChainingHashST::hash(int key) { // std::hash<int> h; return _h(key)%_chainNum; } void SeparateChainingHashST::put(int key, int val) { int index=hash(key); ChainNode* chain=_chainHeads[index];//找到对应的链表头 if(chain==nullptr) //如果链表为空,新建头指针 { _chainHeads[index]=new ChainNode(key, val); return ; } while(1) { if(chain->_key==key)//如果key已存在,更新val { chain->_val=val; return; } if(chain->_next==nullptr) break; chain=chain->_next; } chain->_next=new ChainNode(key, val);//加到链表尾部 } ChainNode* SeparateChainingHashST::get(int key) { int index=hash(key); ChainNode* chain=_chainHeads[index];//找到对应的链表头 while(chain!=nullptr) { if(chain->_key==key) { return chain;//如果找到对应key,则返回node } chain=chain->_next; } return nullptr; } void SeparateChainingHashST::showChains() { ChainNode* N; for(int i=0; i<_chainNum; i++) { cout<<"Chain"<<i<<endl; N=_chainHeads[i]; while (N) { cout<<N->_key<<" "; N=N->_next; } cout<<endl; } } SeparateChainingHashST::~SeparateChainingHashST() { for(int i=0; i<_chainNum; i++) { deleteAllNodes(_chainHeads[i]);//删除所有链表的节点 } delete[] _chainHeads;//删除数组 } void SeparateChainingHashST::deleteAllNodes(ChainNode* head)//删除链表头为head的链表 { ChainNode* N; while (head) { N=head; head=head->_next; delete N; } }
2.线性探测
这种方法是申请一段比数据总数N更大(至少为2N)的空间,那么当所有数据都存进去之后数据之间必然总会存在一些间隔,而这些间隔就是用来存放映射重叠的数据。因此他的思想就是每添加一个数据,先对其键值进行映射,然后根据映射结果找到相应位置进行存放,如果映射结果所在的位置已经被填充,就放到下一个位置(即所谓线性探测),直到找到合适的位置为止。而查找也是先进行映射,然后从映射结果所在位置开始向后查找,直到空格为止(由于空间比数据多,总会有空格存在)。显而易见,这种方法的查找效率与分配的存储空间有关,空间越大,连续存放的数据越少,则查找速度越快。
示意图:
类声明:
class LinearProbingHashST { public: LinearProbingHashST(); LinearProbingHashST(int keyNumEstimate); ~LinearProbingHashST(); void put(int key, int val); ChainNode* get(int key); bool deleteNode(int key); void AdjustSpace();//调整空间,如果保证空间利用率保持在1/8~1/2 void show(); private: inline unsigned int hash(int key); void resize(int cap);//设置格子数目为cap void put(ChainNode* N); private: int _keyNum;//已添加元素数目 int _blockNum;//格子数目 NodePoniter* _blocks;//格子数组的首指针 std::hash<int> _h; };
实现代码:
LinearProbingHashST::LinearProbingHashST() { _keyNum=0; _blockNum=4;//默认空格为4个 _blocks=new ChainNode*[_blockNum]; for(int i=0;i<_blockNum;i++) _blocks[i]=nullptr; } LinearProbingHashST::LinearProbingHashST(int keyNumEstimate) { _keyNum=0; _blockNum=2* keyNumEstimate;//保证空间是将要加入元素个数的2倍 _blocks=new ChainNode*[_blockNum]; for(int i=0;i<_blockNum;i++) _blocks[i]=nullptr; } LinearProbingHashST::~LinearProbingHashST() { for(int i=0;i<_blockNum;i++) if(_blocks[i]) delete _blocks[i]; delete[] _blocks; } unsigned int LinearProbingHashST::hash(int key) { return _h(key)%_blockNum; } void LinearProbingHashST::resize(int cap) { auto blocksOld=_blocks;//原数组指针 _blocks=new ChainNode*[cap];//新建数组 for(int i=0;i<cap;i++) _blocks[i]=nullptr; int blockNumOld=_blockNum; _keyNum=0; _blockNum=cap;//这里必须先设置正确的_blockNum才能开始调用put for(int i=0;i<blockNumOld;i++)//将原数组的数组(node指针)全部插入到新数组中 { if(blocksOld[i]) put(blocksOld[i]); } delete[] blocksOld; } void LinearProbingHashST::put(int key, int val) { if(_keyNum>=_blockNum/2) resize(_blockNum*2); int index=hash(key); ChainNode* N=_blocks[index]; while (N) { if(N->_key == key) { N->_val=val; return ; } index=(++index)%_blockNum; N=_blocks[index];//如果不为空则查找下一个,到达最后一格回到第一格 } //将新元素添加到空格中 _blocks[index]=new ChainNode(key, val); _keyNum++; } void LinearProbingHashST::put(ChainNode* insertNode) { if(_keyNum>=_blockNum/2) resize(_blockNum*2); if(insertNode==nullptr) return; int index=hash(insertNode->_key); ChainNode* N=_blocks[index]; while (N) { if(N->_key == insertNode->_key) { N->_val=insertNode->_val; return ; } index=(++index)%_blockNum; N=_blocks[index];//如果不为空则查找下一个,到达最后一格回到第一格 } //将新元素添加到空格中 _blocks[index]=insertNode; _keyNum++; } ChainNode* LinearProbingHashST::get(int key) { int index=hash(key); ChainNode* N=_blocks[index]; while (N) { if(N->_key == key) { return N; } index=(++index)%_blockNum; N=_blocks[index];//如果key不同则查找下一个,直到找到空格 } return nullptr; } bool LinearProbingHashST::deleteNode(int key) { if(_keyNum==0) return false; int index=hash(key); ChainNode* N=_blocks[index]; while (N) { if(N->_key == key) {//删除对应元素 _blocks[index]=nullptr; delete N; //将被删除元素后面的元素(到空格为止)重新插入 do { _keyNum--; index=(++index)%_blockNum; N=_blocks[index]; _blocks[index]=nullptr; put(N); }while (N); return true; } index=(++index)%_blockNum; N=_blocks[index];//如果key不同则查找下一个,直到找到空格 } return false; } void LinearProbingHashST::AdjustSpace() { if(_keyNum<=_blockNum/8) resize(_blockNum/2); if(_keyNum>=_blockNum/2) resize(_blockNum*2); } void LinearProbingHashST::show() {//输出所有的格子里的数据,空的输出0 cout<<"the data of all blocks:"<<endl; for(int i=0;i<_blockNum;i++) { if(_blocks[i]) cout<<_blocks[i]->_key<<" "; else cout<<"0 "; } cout<<endl; }
其中要注意的是空间是连续且循环的,所以最后一个格子的下一个格子是第一个格子,所以要用
index=(++index)%_blockNum;
另外,删除某个数据之后要将后面到空格之间的所有数据重新插入表中,否则由于删除后所在位置变成空格,会导致后面的某些元素无法被查找。
测试代码:
//////////////////////////////////// cout<<"SeparateChainingHashST test"<<endl; SeparateChainingHashST mySC(5); for(int i=0;i<myLen;i++) { mySC.put(origNum2[i], i); } mySC.showChains(); cout<<endl<<"get test"<<endl; for(int i=0;i<myLen;i++) { cout<<mySC.get(origNum2[i])->_val<<" "; } cout<<endl; //////////////////////////////////// cout<<"LinearProbingHashST test"<<endl; LinearProbingHashST myLP; for(int i=0;i<myLen;i++) myLP.put(origNum2[i], i); myLP.show(); cout<<endl<<"get test"<<endl; for(int i=0;i<myLen;i++) cout<<myLP.get(origNum2[i])->_val<<" "; cout<<endl; for(int i=0;i<8;i++) myLP.deleteNode(origNum2[i]); cout<<"deleteNode test"<<endl; cout<<"the data have been deleted:"<<endl; ChainNode* N=nullptr; for(int i=0;i<myLen;i++) { N=myLP.get(origNum2[i]); if(N==nullptr) cout<<origNum2[i]<<" "; } cout<<endl; cout<<"end of the page!"<<endl;
测试结果:
可以看到拉链式的几个链表长度都差不多,线性探测的数据分布也还可以,都没有太长的连续数据。
相关文章推荐
- c语言实现hashmap(转载)
- Ruby中的数组和散列表的使用详解
- Ruby中Hash的11个问题解答
- Ruby简明教程之数组和Hash介绍
- 在C#中生成与PHP一样的MD5 Hash Code的方法
- js中hash和ico的关联分析
- Javascript SHA-1:Secure Hash Algorithm
- 理解php Hash函数,增强密码安全
- 详解散列表算法与其相关的C语言实现
- PHP利用hash冲突漏洞进行DDoS攻击的方法分析
- PowerShell中定义哈希散列(Hash)和调用例子
- Redis String 类型和 Hash 类型学习笔记与总结
- php操作redis中的hash和zset类型数据的方法和代码例子
- Perl 哈希Hash用法之入门教程
- perl哈希hash的常见用法介绍
- php自定义hash函数实例
- php对文件进行hash运算的方法
- php常用hash加密函数
- PHP Hash算法:Times33算法代码实例
- php的hash算法介绍