您的位置:首页 > 其它

散列表(拉链式和线性探测)

2016-01-05 19:04 417 查看
无论是顺序查找还是二分查找(包括二叉树),查找或插入的时间复杂度总会与数据的总数N有关,而散列表可以将查找和插入操作降低到常数级别。

最简单的一种常数级别的符号表,就是直接将数据的键值作为数组的索引,通过键值就可以立即访问到数据,时间复杂度为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;


测试结果:



可以看到拉链式的几个链表长度都差不多,线性探测的数据分布也还可以,都没有太长的连续数据。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息