散列(Hash):
其实看到名字大概就明白是个什么东西了,至少哈希函数有听说过吧。实际上就是将关键字尽可能随机的映射到一张表上的一种函数。这种数据结构并不提供其他作用,数据也不存在排列的顺序,毕竟是相对随机的。
但是,很显然,他终归只是一张表,是有极限的,而关键字可能有非常非常多,那么就容易出现一种意外,也就是哈希碰撞(两个不同的关键字却被映射到了同一个位置),这是有可能的,单可能性大小取决于你如何实现这个函数。
比方说,如果我的哈希函数是:hash=Key%11,假设Key并没有限制,那就有很多个关键字会有同样的hash值了。(假设现在Key是整形的数字)
所以为了解决这种问题,一种方法便是分离链接法。
举一个比较形象的例子吧。
假设散列表是一个平面,他有X轴和Y轴,两个轴的坐标都必须是整数(整数只是为了好理解一些罢了)。
比方说(1,1)。现在有两个关键字都被映射到了这个点上,那如何解决?为它增加一个Z轴。
那么关键字便能够这样储存:(1,1,key1)和(1,1,key2)
就像是在这个点下面挂上了两个关键字一样。它没有解决碰撞的问题,但是容许了碰撞的出现。因为即便hash值相同,关键字也同样能够被顺利储存下来。
在C++中,这种结构是实现方法便是链表。所谓的Z轴就相当于在每一个节点下面挂上一条链表。
注:个人是跟着书上学的,所以代码大致和黑皮书相同。
必要的声明:(因为各种声明有些绕,所以加了些许方便理解的注释和一些没必要的名词。)
struct Listnode;//表节点
typedef struct Listnode* Position;//指向表节点的“位置指针”
struct HashTbl;//哈希表
typedef struct HashTbl* HashTable;//指向哈希表的“表指针”
typedef Position List;//“位置指针”也将作为“列表指针”
struct Listnode {
int info;//关键字
Position Next;//指向下一个表节点的“位置指针”
};
struct HashTbl {
int TableSize;//表尺寸
List* TheLists;//指向“位置指针”的“列表指针”
}
建立表:
HashTable InitializeTable(int TableSize)
{
HashTable H;
H = new HashTbl;
//H->TableSize = NextPrime(TableSize);
H->TableSize = TableSize;//这要求Tablesize是大于表大小的素数
H->TheLists = new List[TableSize];
for (int i = 0; i < TableSize; i++)
{
H->TheLists[i] = new Listnode;
H->TheLists[i]->Next = NULL;
}
return H;
}
流程说明:
该函数将会返回一个“表指针”,这个指针指向我们刚刚建立的哈希表。
首先,新建一个表指针,并为其开辟一个哈希表空间。现在这个表指针H已经指向了刚开辟的空间。
将我们输入的“表尺寸”作为这张新表的尺寸,并根据这个尺寸,在表中开辟一个数组,这个数组的元素是“列表指针”。现在,新表中的列表指针TheLists成为了刚开辟好的数组的第一个元素——一个新的列表指针。注意,这个数组的大小会和设定好的“表尺寸一样大”。
接下来为每一个“表指针”开辟一个新节点,并将节点的Next指针指向NULL。
现在,TheLists中的每一个列表指针指向新节点。并且,节点中的关键字都还没有初始化。
Find函数:该表将会返回找到的关键字的“位置指针”
Position Find(HashTable H,int Key)
{
Position P;
List L;
L = H->TheLists[Hash(Key, H->TableSize)];
P = L->Next;
while (P != NULL && P->info != Key)//strcmp strcpy
P = P->Next;
return P;
}
流程说明:
新建一个“位置指针”P和“列表指针”L
让“列表指针”临时成为关键字本该出现的那一列。(我总觉得这种说辞有些不太简洁。)
Hash(Key,H->TableSize)其实就是将关键字进行映射,假设结果被映射到了5,那么“列表指针”将临时成为数组中的第六个。
“位置指针”临时成为L->Next
之所以是临时,目的是不改变哈希表本来的结构,所以引入临时变量来操作数据。
接下来开始遍历这一列上的每一个节点,直到找到了相同的关键字或者已经枚举尽了。
注:因为关键字不一定都是整数,像是字符串之类的关键字,则必须用strcmp和strcpy这样的函数来比较。
插入函数:
void Insert(int Key,HashTable H)
{
Position tmpPos, Newcell;
List L;
tmpPos = Find(H, Key);
if (tmpPos == NULL)
{
Newcell = new Listnode;
L = H->TheLists[Hash(Key, H->TableSize)];
Newcell->Next = L->Next;
Newcell->info = Key;
L->Next = Newcell;
}
}
流程说明:
输入关键字和哈希表地址。
首先,新建一个“临时位置指针”tmpPos和Newcell,以及一个“列表指针”L。
查找这张表中是否已经存在这个关键字了,如果存在就直接跳过,否则才进行添加。
假设本不存在这个关键字。
为Newcell新建一个节点。
让L指针临时成为指向相应坐标的位置指针。
那么现在L将指向某个节点,比方说TheLists[5]。
令Newcell节点中的Next指针成为L指针指向的节点的Next指针。
关键字赋予。
将L指向的节点中Next指针指向这个新节点。
(这个新节点将被挂在最靠近“轴”的那一侧。)
最后是删除函数:
void Deletenode(int Key, HashTable H)
{
Position tmpPos,tmpP2;
List L;
tmpPos = Find(H,Key);
L = H->TheLists[Hash(Key, H->TableSize)];
if (tmpPos != NULL&&L->info!=Key)
{
tmpP2 = tmpPos->Next;
while(L->Next!=NULL)
{
if (L->Next->info == Key)
{
L->Next = tmpP2;
delete tmpPos;
}
else
L = L->Next;
}
}
else if (L->info = Key)
{
int K;
L->info = K;
}
}
书上并没有给出删除数据的函数,所以这个函数是我自己现写的,不太确定是否完全正确。
但思路很简单,就是找到关键字那一排,然后把节点删掉,然后再把链表重新拼起来。如果关键字存在头节点,那就只替换掉关键字就行。
(所以最好是不要往头节点放东西,将表制成头节点不包含数据的样式最佳)