散列介绍
散列的英文名是Hash,其音译为哈希,这是一种将键映射到存储位置的存储技术。对于一个散列表来说,只要知道了某个值的键就很快速访问到表中的这个值,其时间复杂度为O(1)。所以可以把散列表看成由一个个键值对组成。值是我们需要存储的数据,键是我们用来标记值的记号。
可以把散列表看成一个数组,散列的值就是数组的元素,散列的键就是数组的下标。但是一般情况下散列的键不太可能会是简单的数组下标,比如现在有5个键分别是1001,1002,1003,1004,1005;如果让键对应数组的下标的话,那岂不是要给数组开辟一千多个空间来仅仅只存储这5个键值对,这样还是太得不偿失了,所以就引入了散列函数这一操作。
散列函数
散列函数的目的就是以一种函数关系将键尽可能平均的映射到散列表的合适位置上,对于上面的例子,我们可以把散列函数写成y=x-1001,这样一来,1001,...,1005就会被映射成0,1,2,3,4。这刚好就可以很方便当成下标存入数组中去了。
这里介绍一种简易的散列函数:hash(key)=key%TableSize,对键以散列表的大小取模来做值的存储索引,这种函数简单常用,其分布也还算比较均匀。不过显而易见还是会出现多个key映射到同一个索引上,比如12和22对10取模得到的都是2。面对这种冲突情况,这里先介绍用分离链接法解决。
分离链接法实现散列
类型声明
cpp
typedef int ElementType;
struct ListNode;
typedef struct ListNode *Position;
struct HashTbl;
typedef struct HashTbl *HashTable;
struct ListNode {
ElementType Element;
Position Next;
};
typedef Position List;
struct HashTbl {
int TableSize;
List *TheLists;
};
static int Hash(ElementType Key,int TableSize);
HashTable InitializeTable(int TableSize);
void DestroyTable(HashTable H);
Position Find(ElementType Key,HashTable H);
void Insert(ElementType Key, HashTable H);
ElementType Retrieve(Position P);
散列表的数据类型为指向结构体HashTbl的指针,其包括表示表大小的TableSize和用于存储数据的链表数组,这个数组的每个位置对于一个链表,当同一个位置有多个元素时,就让这多个元素组成一个链表挂在数组的这个位置上,这样就处理了冲突的情况。其结构可以理解成这样:
哈希函数
cpp
static int Hash(ElementType Key,int TableSize) {
return Key % TableSize;
}
采用简单的取模函数。
散列的初始化
cpp
HashTable InitializeTable(int TableSize) {
HashTable H;
int i;
H = (HashTable) malloc(sizeof(struct HashTbl));
if (H == NULL) {
printf("Memory allocation failed in InitializeTable()\n");
exit(1);
}
H->TableSize=TableSize;
H->TheLists=(List*) malloc(sizeof(List)*TableSize);
if (H->TheLists == NULL) {
printf("Memory allocation failed in InitializeTable()\n");
exit(1);
}
for (i = 0; i < TableSize; i++) {
H->TheLists[i]=(Position) malloc(sizeof(struct ListNode));
if (H->TheLists[i] == NULL) {
printf("Memory allocation failed in InitializeTable()\n");
exit(1);
}
H->TheLists[i]->Next=NULL;
}
return H;
}
常规地进行散列表的初始化,初始化连续空间为TableSize的链表数组,再在数组的每个位置初始化一个链表。
散列的销毁
cpp
void DestroyTable(HashTable H) {
int i;
Position P,Temp;
for (i = 0; i < H->TableSize; i++) {
P = H->TheLists[i];
while (P != NULL) {
Temp = P;
P = P->Next;
free(Temp);
}
}
free(H->TheLists);
free(H);
}
先挨个销毁链表数组中的每个链表,再销毁链表,最后销毁散列表。
元素查找
cpp
Position Find(ElementType Key,HashTable H) {
Position P;
List L;
L=H->TheLists[Hash(Key,H->TableSize)];
P=L->Next;
while (P != NULL&&P->Element != Key) {
P=P->Next;
}
return P;
}
先通过散列函数将Key转换成散列索引,通过遍历索引处的链表得到其key值在散列表中的位置。
元素插入
cpp
void Insert(ElementType Key, HashTable H) {
Position P,NewNode;
List L;
P=Find(Key,H);
if (P==NULL) {
NewNode=(Position) malloc(sizeof(struct ListNode));
if (NewNode==NULL) {
printf("Memory allocation failed in Insert()\n");
exit(1);
}
L=H->TheLists[Hash(Key,H->TableSize)];
NewNode->Next = L->Next;
NewNode->Element = Key;
L->Next = NewNode;
}
}
先通过Find方法找一找表中有没有这个元素,如果有的话那就不进行插入操作,如果没有的话那就通过散列函数找到其索引,通过在链表数组所在索引处的位置的链表以及头插的方式在散列中添加该元素,这里的该元素就是Key值本身。
元素查看
cpp
ElementType Retrieve(Position P) {
if (P!=NULL) {
return P->Element;
}
printf("Node is NULL!\n");
exit(1);
}
已知Position,所以直接访问即可。
实例
cpp
int main() {
HashTable H = InitializeTable(10);
Insert(5,H);
Insert(15,H);
Insert(3,H);
Insert(13,H);
Insert(7,H);
ElementType key =15;
Position P = Find(key,H);
if (P!=NULL) {
printf("Element found at position %d\n",P->Element);
}else {
printf("Element not found\n");
}
key=20;
P = Find(key,H);
if (P!=NULL) {
printf("Element found at position %d\n",P->Element);
}else {
printf("Element not found\n");
}
DestroyTable(H);
return 0;
}
结果如下: