以下均为个人的学习笔记。写出来是为了在写的过程中看看自己的想法有没有什么毛病。如果您找到了什么问题,请务必告知我,谢谢!
学习过程完全参照了黑皮书《数据结构与算法分析》,所以实现代码大致与书中一样(也有些许改动,因为我的黑皮书是C语言实现,而我实际实现所用却是C++,多少有一些不必要的不同啦),或许现在已经有更好的写法了,所以仅供参考吧。
先贴出完全的代码,再进行逐步分析:
#include<iostream>
using namespace std;
//----------------------------//
struct HashTbl;
typedef struct HashTbl* HashTable;//表指针HashTable
struct HashEntry;
typedef unsigned int Index;
typedef Index Position;
typedef struct HashEntry Cell;
enum KindOfEntry {Legitimate,Empty,Deleted};//用以标记单元格当前状态
#define MinTableSize 2//最低表尺寸
struct HashEntry
{
int Key;//关键字
enum KindOfEntry Info;//状态栏
};
struct HashTbl
{
int TableSize;//表尺寸
Cell* TheCells;//表单元指针TheCells
};
Position Find(int key, HashTable H);
void Insert(int key, HashTable H);
HashTable InitializeTable(int TableSize);
//----------------------------//
HashTable InitializeTable(int TableSize)
{
HashTable H;
if (TableSize < MinTableSize)
{
return NULL;
}
else
{
H = new HashTbl;
H->TableSize = TableSize;//注意,请保证这里是TableSize是一个素数
H->TheCells = new HashEntry[H->TableSize];
for (int i = 0; i < H->TableSize; i++)
{
H->TheCells[i].Info = Empty;
}
return H;
}
}
Position Find(int key, HashTable H)
{
Position CurrentPos;
int CollisionNum;
CollisionNum = 0;
CurrentPos = Hash(key, H->TableSize);
while (H->TheCells[CurrentPos].Info != Empty && H->TheCells[CurrentPos].Key != key)
{
CollisionNum++;
CurrentPos += 2 * CollisionNum - 1;
if (CurrentPos >= H->TableSize)
CurrentPos -= H->TableSize;
}
return CurrentPos;
}
void Insert(int key,HashTable H)
{
Position Pos;
Pos = Find(key, H);
if (H->TheCells[Pos].Info != Legitimate)
{
H->TheCells[Pos].Info = Legitimate;
H->TheCells[Pos].Key = key;
}
}
//-------------------------------------------------------------------------------------//
继上一篇链接法之后,这次遇到了开放寻址。原理也并不复杂。其实就是当发生了碰撞(不同的关键字却拥有一样的哈希值)时,为后到的关键字再找另外一个地方存放。当然,这个存放也不能是瞎存放,它是有一定规则的,常见的有两种,分别是“线性”和“平方”。
线性探测:
当发生碰撞的时候,就往下一个单元格去存放(如果还碰撞就继续往下,碰到底了就绕回表头继续往下)。相对的朴素的一种方法,只要表还没装满,那就一定能给关键字找到合适的位置。当然,这也同时很不效率,因为它必须一个个去匹配判断,一次次去绕,怎么想都不是很效率,所以还有另外一种“平方”。
平方探测:
因为上一种不太效率,所以平方探测可能更平常一些。简单来说,当发送了碰撞,就去找当前单元格下的 i^2 格,以此类推(其中,i是一个从1开始的常数,每次判断都会+1。所以偏移量是按照1,4,9,16的顺序来增加的)。
注:还有另外一种平方探测,和上面的相近,只是变成了 (-1)^i(i)^2 而已,并且 常量i 是每两回合+1(-1,+1,-4,+4像这样)。
![](/qrcode.jpg)
这种方式能很好的防止过多次数的匹配,因为插入数据的单元都很分散,但也同样有些毛病。比方说,必须要保证哈希表足够大。试想一下,这种匹配方式,是不是有可能导致某个数据来回匹配无数次都没办法放进空的单元格里?解决这种方式就需要让哈希表足够大。显然,这可能会浪费不少空间,但速度无疑提升了。
//-------------------------------------------//
首先是创建表函数:
HashTable InitializeTable(int TableSize)
{
HashTable H;
if (TableSize < MinTableSize)
return NULL;
else
{
H = new HashTbl;
H->TableSize = TableSize;
H->TheCells = new HashEntry[H->TableSize];
for (int i = 0; i < H->TableSize; i++)
H->TheCells[i].Info = Empty;
return H;
}
}
并不复杂,连代码都每几行。
首先,建立一个“表指针H”,然后根据输入的“表尺寸TableSize”建表。
①先为H开辟一个“表空间”
②尺寸赋予
③为该空间中的“表单元指针”开辟出一个数组,以存放每个表单元的数据(我自己偶尔会绕进去,所以姑且打个注释吧。指针在定义的时候就存在了,而开辟指针空间实际上是为指针所指向的结构体开辟一块空间,这一操作只是让这些被声明好的指针有地方能指,而不是把指针放进去......(我自己也觉得这样特地去说好像有点蠢......))
④将这个每个“表单元”初始化为Empty
⑤返回这个表的地址
//-----------------------------------------------------//
查找关键字函数:
Position Find(int key, HashTable H)
{
Position CurrentPos;
int CollisionNum=0;
CurrentPos = Hash(key, H->TableSize);//Hash函数,用以计算该关键字最初的哈希值
while (H->TheCells[CurrentPos].Info != Empty && H->TheCells[CurrentPos].Key != key)
{
CollisionNum++;
CurrentPos += 2 * CollisionNum - 1;
if (CurrentPos >= H->TableSize)
CurrentPos -= H->TableSize;
}
return CurrentPos;
}
很有意思的函数,我最初并没有看懂为什么CurrentPos是那样计算的,但从结论来说,作者说的对。
①声明位置指针CurrentPos
②定义碰撞次数CollisionNum=0
③得出其本该对应的哈希值
④遍历表。如果“该单元状态非空,且关键字不相同”,碰撞次数增加,CurrentPos指针偏移一个 CollisionNum^2(并判断是否超出了表的范围,若超出就把它来回来)
最妙的就是偏移量计算了。它避免了一些看似需要的乘法和除法。书上还特别叮嘱,不要改变While的判断条件的先后顺序(这是很有必要,也很有趣的方法。当你发现该单元格内数据为Empty,则直接返回这个地址了,而不是返回NULL。这会为下一个Insert函数提供很多便利。并且,也是非常有必要的是,对于没有初始化的单元格内的Key,它无疑是有一个确确实实的值的,这样做能省下一点时间。)
F(i)=F(i-1)+2i-1 这是书中给出的算法,在计算机中并不难实现。
//-----------------------------------------------------------//
插入函数:
void Insert(int key,HashTable H)
{
Position Pos;
Pos = Find(key, H);
if (H->TheCells[Pos].Info != Legitimate)
{
H->TheCells[Pos].Info = Legitimate;
H->TheCells[Pos].Key = key;
}
}
①声明,并找出该关键字对应的Pos位置
②如果这个关键字已经存在了,就什么都不做;如果不存在,那就往里面放新的关键字。
不妨假设现在的表中是不存在Deleted状态的单元格。那么Find函数只会返回Legitimate或者Empty。
返回Legitimate状态的条件,只有一种:①单元格不为空,关键字相同。此时,Find函数会马上返回Legitimate
返回Empty状态的条件,也只有一种:①单元格为空,直接返回。
这样的说明分明是没必要的,但我写出来才理顺了。
//--------------------------------------------------------//
删除函数并没有写,我觉得那没太大必要,就做些简单的说明吧。
本例中,“删除”操作并不是指把数据真的删除,只是把单元格状态标记为Deleted罢了。数据仍然会被保存。
如果真的想写,和上面的Insert差不多。直接用Find找出来,判断是否为Empty就行了。