散列表的构建是散列查找的前提,说起查找,我第一时间想起二分查找,可是二分查找要求序列是有序排列的并且元素的存储地址是连续的。对于数据查找还好,但是如果做插入或删除操作的话,就要移动大量的元素,当数据量很大时,这样明显不划算。二分查找不行(要求序列为有序序列),那么可以用二叉搜索树啊!二叉搜索树的查找、插入和删除时间复杂度可以从O(logN)到O(N)。N为二叉搜索树的高度。
既然二叉搜索树已经很快了,可是又有一种更快的查找方式,就是散列查找。散列查找的时间复杂度几乎可以达到O(1)(不过事实上很难很难做到- -、),因为散列查找的查找方法是根据散列函数直接算出元素在序列中的位置。要实现这样的查找,就要先把所有元素按照散列函数构造出一张散列表,(有点像加密和解密- -)。
首先把每一个元素按照散列函数计算出在散列表中的存储位置并插入(这个位置是一个整数值)。如果这个散列函数设计的好,每个元素都有唯一对应位置,即每个位置只对应一个元素,那么在之后做散列查找时,时间复杂度就可以达到O(1)。不过这在实际情况中大量数据下,是很难实现的,往往会出现多个元素经过散列函数计算后得到同一个位置,这就是冲突。
构建散列函数表,首先要设计一个散列函数,这里我们先用求余做散列函数,即拿到一个元素,我们把它和所有元素的个数总和做求余运算来获得散列位置,例如我们构建散列表,散列表最大表长为21,插入元素“9”时,9对21求余得到9本身,所有9被插入到散列表中地址等于9的位置。往后我们继续做插入,假设遇到元素“30”,30对21求余也是得到9,可是这时候散列表上9这个位置已经有元素了,这就发生了冲突,接着我们就要解决这个冲突。
解决冲突的方法有两种,(现在我只写出来一种,因为下面我的散列表都是用一维数组构建的- -、)。第一种是“开放地址法”,遇到冲突后,就按照某种规则,寻找下一个位置,直到找到空位置为止;第二种是把冲突的对象都链接在一起,用一个链表链接(就像桶排序里面的桶那样)。
开放地址法也分为线性探测、平方探测和双散列,这里我先说最简单的线性探测- -、。线性探测就是在发生冲突后,在原来的地址上往下一个地址找,一个一个的往下探测,直到找到空位置为止。
思路大概就是上面那样了,下面来看看代码部分怎么实现:
/*
* 散列表 - 线性探测
* date:2018/12/20
*author:justin
*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <stdbool.h>
#define MaxTableSize 20 /*散列表的最大长度*/
typedef int ElemType; /*关键词的数据类型为整型*/
typedef int Index; /*散列表的地址类型为整型*/
typedef int Position; /*数据所在的位置类型与散列表的地址类型一致*/
/*散列表中单元格的状态类型,Legitimate为有合法元素;Empty为空单元格;Deleted为有已删除元素*/
typedef enum {
Legitimate, Empty, Deleted
} EntryType;
/*散列表单元格的存储结构*/
typedef struct HashEntry Cell;
struct HashEntry {
ElemType Data; /*单元格存放的元素*/
EntryType Info; /*单元格的状态*/
};
/*散列表的存储结构*/
typedef struct TableNode *HashTable;
struct TableNode {
Cell *Cells; /*存放散列表数据的数组*/
int TableSize; /*散列表的最大长度*/
};
/*函数声明*/
int NextPrime(int N); /*返回大于N且不超过MaxTableSize的最小素数*/
HashTable CreateTable(int TableSize); /*创建并初始化散列表*/
void BuildHashTable(HashTable H); /*构建散列表*/
void PrintHashTable(HashTable H); /*输出散列表*/
int Hash(ElemType Data, int TableSize); /*散列函数*/
Position LinearFind(HashTable H, ElemType Data); /*线性探测查找*/
bool Insert(HashTable H, ElemType Data); /*散列表的插入操作*/
void Find(HashTable H); /*用户使用线性探测查找*/
第18行用一个联合表示散列表中每个单元格的状态,分别有已存在合法元素、空单元格和有已删除元素的单元格。第23行散列表中的每一个单元格中包含数据元素和单元格的状态标识符。至于散列表的存储结构在第30行用一个一维数组来表示。
/*散列函数*/
int Hash(ElemType Data, int TableSize)
{
return (int)(Data%TableSize);
}
/*线性探测查找*/
Position LinearFind(HashTable H, ElemType Data)
{
Position currentPos, newPos; /*一个指示初始插入位置,一个指示新位置*/
int collisionNum=0; /*记录发生冲突的次数*/
if (Data!=-1) {
/*根据散列函数找到该元素应该放的初始位置*/
newPos=currentPos=Hash(Data, H->TableSize);
/*如果该位置的单元格非空,并且不是要找的元素时,即发生了冲突*/
while (H->Cells[newPos].Info!=Empty && H->Cells[newPos].Data!=Data) {
/*冲突次数+1*/
collisionNum++;
/*线性探测,增量+1*/
newPos=currentPos+collisionNum; /*!!!!*/
/*特殊情况判断看位置下标是否越界,是就调整成正确位置*/
if (newPos>=H->TableSize) {
newPos=newPos%H->TableSize;
}
}
printf("元素%d插入操作:\t散列地址=%d\t实际地址=%d\t冲突次数=%d\n", Data,currentPos, newPos, collisionNum);
}
return newPos; /*此时的newPos位置是Data的位置,或者是一个空单元格的位置*/
}
接下来先要设计一个散列函数,这里简单起见就用求余方法(第144行)。然后按照这个散列函数构建散列表,即做插入操作。因为越往后插入的元素很大概率会出现冲突现象,所以其实插入操作我们也是用到线性探测的方法。第154行通过散列函数获得元素的初始插入位置后,第156行判断这个位置的单元格状态是否为空,不是就让冲突次数++,并且往下一个位置探测,直到找到空的单元格后,就会跳出while循环。找到插入位置后,返回出去给插入操作的函数,这个方法同样也就是查找方法了。
/*散列表的插入操作*/
bool Insert(HashTable H, ElemType Data)
{
Position Pos=LinearFind(H, Data); /*探测Data是否已存在散列表中*/
if (Data!=-1 && H->Cells[Pos].Info!=Legitimate) {
/*如果这个单元格没有被占用,说明Data可以插入在这个Pos位置*/
/*插入*/
H->Cells[Pos].Data=Data;
H->Cells[Pos].Info=Legitimate;
return true;
} else if (Data==-1) {
return;
} else {
printf("该元素已存在于散列表中.\n");
return false;
}
}
插入操作就很简单了,记得插入完后更新单元格的状态(第181行)。
上面是存放整数构建的散列表,字符串也可以,实现代码就是用到字符串操作函数:
/*散列函数*/
int Hash(ElemType Data[], int TableSize)
{
int Pos=Data[0]-'a'; //字符串首字母转换成数字
return (int)(Pos%TableSize);
}
/*线性探测查找*/
Position LinearFind(HashTable H, ElemType Data[])
{
Position currentPos, newPos; /*一个指示初始插入位置,一个指示新位置*/
int collisionNum=0; /*记录发生冲突的次数*/
if (strcmp(Data, "-1")!=0) {
/*根据散列函数找到该元素应该放的初始位置*/
newPos=currentPos=Hash(Data, H->TableSize);
/*如果该位置的单元格非空,并且不是要找的元素时,即发生了冲突*/
while (H->Cells[newPos].Info!=Empty && strcmp(H->Cells[newPos].Data, Data)!=0) {
/*冲突次数+1*/
collisionNum++;
/*线性探测,增量+1*/
newPos=currentPos+collisionNum; /*!!!!*/
/*特殊情况判断看位置下标是否越界,是就调整成正确位置*/
if (newPos>=H->TableSize) {
newPos=newPos%H->TableSize;
}
}
//puts(Data);
printf("元素插入操作:\t散列地址=%d\t实际地址=%d\t冲突次数=%d,\n", currentPos, newPos, collisionNum);
}
return newPos; /*此时的newPos位置是Data的位置,或者是一个空单元格的位置*/
}
/*散列表的插入操作*/
bool Insert(HashTable H, ElemType Data[])
{
Position Pos=LinearFind(H, Data); /*探测Data是否已存在散列表中*/
if (strcmp(Data, "finish")!=0 && H->Cells[Pos].Info!=Legitimate) {
/*如果这个单元格没有被占用,说明Data可以插入在这个Pos位置*/
/*插入*/
strcpy(H->Cells[Pos].Data, Data); /*字符串复制进去*/
H->Cells[Pos].Info=Legitimate;
return true;
} else if (strcmp(Data, "finish")==0) {
return;
} else {
printf("该元素已存在于散列表中.\n");
return false;
}
}
例如第162行判断字符串相等就用strcmp函数。
最后附上运行结果:
两种实现的完整代码已上传GitHub:
https://github.com/justinzengtm/Hashing/blob/master/integer_hashtable_linear%20probing.c
https://github.com/justinzengtm/Hashing/blob/master/string_hashtable_linear%20probing.c