分离链接法是不同于前面两种线性探测和平方探测,线性探测和平方探测是,遇到冲突就往“下一个”散列地址寻找空位,这样散列表中的没空位后就不能插入元素了,但是分离链接法不同,它把在同一散列地址上的元素链接在一起,也就是把冲突元素用一个链表链接起来,这样发生每次计算出来的散列地址就一定是该元素在散列表中的散列地址,只不过在找到散列地址后,还要遍历一遍这个地址里的链表来查找要找到那个元素。这样看来,分离链接法的存储结构和桶排序的桶存储结构是一样的,也是一个一维数组里面存放每一个链表的头结点。分离链接法的好处在于,发生冲突后不用去重新计算新的散列地址,而是直接在初始散列地址中插入到链表里即可。
来看看分离链接法的散列表存储结构:
#define MaxTableSize 20 /*散列表的最大长度*/
#define KeyLength 20 /*定义关键词字符串的最大长度*/
//typedef char ElemType[KeyLength+1]; /*关键词类型用字符串形式*/
typedef int Index; /*散列地址的类型*/
/*单链表的存储结构*/
typedef struct ListNode *PtrlNode;
struct ListNode {
char Data[KeyLength+1]; /*链表中的数据结点*/
PtrlNode Next; /*指向下一个结点的指针*/
};
typedef PtrlNode Position; /*散列地址类型*/
typedef PtrlNode List; /*链表*/
/*散列表的存储结构*/
typedef struct TableNode *HashTable; /*散列表的类型*/
struct TableNode {
int TableSize; /*散列表的最大长度*/
List Heads; /*存放链表头结点的数组*/
};
既然数组的每一个单元格是一个链表的头结点,那么肯定要用到单链表了,所以第11到16行定义一个单链表的存储结构。第20到25行,散列表的存储结构除了一个记录散列表大小的TableSize外还有一个存放链表头结点的数组,这里存放的是一个结点但是在初始化时我们会这样做
/*散列表的初始化函数*/
HashTable CreateTable(int TableSize)
{
HashTable H;
int i=0;
H=(HashTable)malloc(sizeof(struct TableNode));
/*使散列表的最大长度是素数*/
H->TableSize=NextPrime(TableSize);
/*初始化数组中每个单元格一个单链表的头结点*/
H->Heads=(List)malloc(H->TableSize*sizeof(struct ListNode));
/*初始化每一个数组中头结点*/
for (i=0; i<H->TableSize; i++) {
H->Heads[i].Data[0]='\0';
H->Heads[i].Next=NULL;
}
return H;
}
第97行我们会申请TableSize个这样的结点为一个数组。
接着看查找函数:
/*分离链接法的查找函数*/
Position ScFind(HashTable H, char Key[])
{
Position P;
Index Pos; /*散列地址*/
Pos=Hash(Key, H->TableSize); /*获得初始散列地址*/
//printf("元素插入操作:\t初始散列地址=%d\n", Pos);
P=H->Heads[Pos].Next; /*从计算出的散列地址的链表头结点的Next开始查找*/
/*遍历*/
while (P && strcmp(P->Data, Key)) {
P=P->Next;
}
return P; /*此时P指向找到的结点*/
}
第一步我们依然是根据查找函数找出出事的散列地址,然后根据这个散列地址获得这个地址上的链表的头结点(第163行)。接下来就是对这个单向链表的遍历,遍历的条件是P不为NULL同时不是我们要查找的元素,这样的遍历最后返回的P结点要么是NULL,要么是要查找的元素的结点。
接下来看插入:
/*分离链接法的插入函数*/
bool ScInsert(HashTable H, char Key[])
{
Position P, NewCell;
Index Pos;
P=ScFind(H, Key); /*在散列表中查找要插入的元素*/
/*如果这个元素在散列表中没有*/
if (!P) {
NewCell=(Position)malloc(sizeof(struct ListNode)); /*插入该结点*/
strcpy(NewCell->Data, Key); /*数据复制进结点*/
Pos=Hash(Key, H->TableSize); /*获得该插入元素的插入地址*/
/*将NewCell插入到H->Heads[Pos]链表的第一个结点*/
NewCell->Next=H->Heads[Pos].Next;
H->Heads[Pos].Next=NewCell;
return true;
} else {
/*如果这个元素已经存在*/
printf("该关键词已存在于散列表中.\n");
return false;
}
}
插入函数同样的道理,第178行先是在散列表中查找这个要插入的元素,如果没有就做插入操作,第181行初始化一个结点来存放要插入的元素,然后把结点插入到链表里(第185-186行)。
最后到删除,由于是链表方式的散列表,所以我们不用“懒惰删除”啦!直接像对链表做删除操作一样即可:
/*分离链表法的删除函数*/
bool ScDelete(HashTable H)
{
Position P, Q;
Index Pos;
char Key[KeyLength+1];
printf("\n请输入要插删除的元素:");
gets(Key);
Pos=Hash(Key, H->TableSize); /*获得要删除的元素初始散列地址*/
P=H->Heads[Pos].Next; /*从计算出的散列地址的链表头结点的Next开始查找*/
/*因为删除操作的原因,先看特殊情况如果要删除的元素在头结点*/
if (P && strcmp(P->Data,Key)==0) {
H->Heads[Pos].Next=P->Next;
free(P);
printf("删除成功!\n");
PrintHashTable(H); /*删除完后再输出一次链表*/
return true;
} else {
/*遍历*/
while (P->Next && strcmp(P->Next->Data, Key)) {
P=P->Next;
}
/*如果散列表中没有这个元素*/
if (!P->Next) {
printf("要删除的元素不存在于散列表中.\n");
return false;
} else {
Q=P->Next; /*此时Q是要删除的元素的结点*/
P->Next=Q->Next;
free(Q);
printf("删除成功!\n");
PrintHashTable(H); /*删除完后再输出一次链表*/
return true;
}
}
}
对要删除的函数获取散列地址后,注意删除头结点和其他结点的操作是有点不同的,因为我们知道对链表的删除操作是找到要删除的结点的上一个结点,然后让它的Next指向要删除结点的Next,最后free掉要删除的结点。所以程序第213行如果我们要删除的结点是链表的头结点,就让散列表该单元格头结点的Next指向头结点的Next,然后free掉头结点即可(第214-215行)。如果要删除的结点不是链表的头结点,第219行开始就遍历这个链表,找到要删除的结点再做删除操作。
最后的最后,释放掉这个散列表:
/*释放散列表*/
void DestroyTable(HashTable H)
{
int i;
Position P, Tmp;
/*释放散列表中的每一个单元格里的链表*/
for (i=0; i<H->TableSize; i++) {
P=H->Heads[i].Next;
while (P) {
Tmp=P->Next;
free(P);
P=Tmp;
}
}
free(H->Heads); /*释放存放头结点的数组*/
free(H); /*释放散列表*/
return;
}
完整代码已上传GitHub:
https://github.com/justinzengtm/Hashing/blob/master/Separate%20Chaining.c