1、建立散列表
散列搜索
散列搜索不需要遍历很多次,他是尽量根据每个元素的特点(散列关系函数),构建散列表;然后搜索的时候只需要根据特点搜索,不需要遍历,这样比遍历查找效率要高很多,适用于大量数据,并且可用于字符串。
构建散列表
以关键字key为自变量,通过一个确定的函数 h(散列函数),计算出对应的函数值h(key),作为数据对象的存储地址;
可能不同的关键字key会映射到同一个散列地址上,称为“冲突(Collision)”。
处理冲突的方法
- 开放地址法:冲突对象换个位置保存;
- 链地址法:冲突对象用链表保存。搜索时,遍历链表,需要控制链表长度;
- 再散列法:即在产生地址冲突时,用另一个哈希函数计算地址,直到冲突不再发生;
- 建立公共冲突区:一旦发生冲突,都填入公共冲突区表。
散列搜索的关键问题
- 如何解决冲突
- 如何构造散列函数尽量避免冲突
2、 构造散列函数
构造散列函数的目标:
- 使散列地址能够尽可能均匀地分布在散列空间上;
- 使计算尽可能简单。
通常考虑的因素有:
- 计算哈希函数所需时间
- 关键字长度
- 哈希表的大小
- 关键字的分布情况
- 记录的查找频率
常用的构造哈希函数的方法有:
- 直接定址法
- 数字分析法
- 平方取中法
- 折叠法
- 除留余数法
- 随机数法
2.1 直接定址法
取关键字或关键字的某个线性函数值为哈希地址。
H(key)=key或H(key)=a*key+b
其中a和b为常数,这种哈希函数叫做自身函数。
这种函数对于不同的关键字不会发生冲突。
2.2 数字分析法
取关键字中某些取值较分散的若干数字位组成哈希地址。
适合于所有关键字已知,且对每一位的取值做出了一定分析。
2.3 平方取中法
取关键字平方后的中间几位作为哈希地址。
因为一个数平方后的中间几位数和数的每一位都相关,使得散列地址具有较好的分散性,具体取的位数由表长决定。
平方取中法适用于关键字中的每一位取值都不够分散或者较分散的位数小于散列地址所需要的位数的情况。
2.4 折叠法
将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列希地址,这种方法称为折叠法(folding)。
数位叠加时可以有移位叠加和间界叠加(就是把相邻的数镜像叠加)两种方法。
折叠法适用于关键字的位数较多,而且关键字中每一位上数字分布大致均匀的情况。
2.5 除留余数法
取关键字被某个不大于哈希表表长m的数p除后所得的余数为哈希地址。
H(key)= k%p
p必须小于m,并且应该取1.1m~1.7m之间的一个素数。
这是一种最常用的构造哈希函数的方法。
2.6 随机数法
选择一个随机函数,取关键字的随机函数值作为它的哈希地址。
H(key)=random(key)
其中random为随机函数。
通常,当关键字长度不等时采用此法。
3、散列搜索
散列表的搜索过程和建立过程基本一致。给定key值,根据建立表时设定的哈希函数求得哈希地址,若表中此位置上没有记录,则查找不成功;若有记录,则比较关键字,若和给定值相同,则查找成功;若关键字不同,则根据造表时设定的处理冲突的方法找“下一地址”,直至哈希表中某个位置为“空”或者表中所填记录的关键字等于给定值时为止。
4、测试代码
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <stdlib.h>
#define HASH_TAB_SIZE 17
typedef struct
{
int iKey;
char pszName[24];
int iAge;
}UserData;
typedef struct
{
int iCount;
UserData *pUserData[HASH_TAB_SIZE];
}HashTab;
int InitHashTab(HashTab *pHash)
{
pHash->iCount = HASH_TAB_SIZE;
for(int i=0; i<HASH_TAB_SIZE; ++i)
{
pHash->pUserData[i] = NULL;
}
return 0;
}
/* 散列函数:除留余数法 */
int GetHash(int iKey)
{
return abs(iKey) % HASH_TAB_SIZE;
}
/* 冲突处理:开放定址法 */
void InsertHash(HashTab *pHash, UserData *pstData)
{
int iCursor;
iCursor = GetHash(pstData->iKey);
while (NULL != pHash->pUserData[iCursor])
{
iCursor = (2 * iCursor + 1) % HASH_TAB_SIZE;
}
pHash->pUserData[iCursor] = (UserData *)malloc(sizeof(UserData));
memcpy(pHash->pUserData[iCursor], pstData, sizeof(UserData));
}
bool SearchHash(HashTab *pHash, int iKey, UserData **pstData)
{
int iCursor = GetHash(iKey);
if (NULL == pHash->pUserData[iCursor])
{
return false;
}
while (iKey != pHash->pUserData[iCursor]->iKey)
{
printf("Collision >>> %d\n", iCursor);
iCursor = (2 * iCursor + 1) % HASH_TAB_SIZE;
if((NULL == pHash->pUserData[iCursor]) ||
(iCursor == GetHash(iKey)))
{
return false;
}
}
*pstData = pHash->pUserData[iCursor];
return true;
}
int main(void)
{
int aData[10] = {9,10,17,6,34,1,2,3,4,8};
int iLen = sizeof(aData)/sizeof(aData[0]);
HashTab stHash;
bool bFind;
UserData stData;
UserData *pstData;
InitHashTab(&stHash);
for(int i=0; i<iLen; ++i)
{
stData.iKey = aData[i];
stData.iAge = 10 * aData[i];
strcpy(stData.pszName, "Test");
InsertHash(&stHash, &stData);
}
printf("\n======== Hash\n");
for(int i=0; i<HASH_TAB_SIZE; ++i)
{
pstData = stHash.pUserData[i];
if(NULL != pstData)
{
printf(" i:%d age:%d\n", i, pstData->iAge);
}
else
{
printf(" i:%d null\n", i);
}
}
printf("\n======== Search 34\n");
bFind = SearchHash(&stHash, 34, &pstData);
if(true == bFind)
{
printf("Key: %d\n", pstData->iKey);
printf("Age: %d\n", pstData->iAge);
printf("Name: %s\n", pstData->pszName);
}
else
{
printf("Find 8 failure.\n");
}
printf("\n======== Search 35\n");
bFind = SearchHash(&stHash, 35, &pstData);
if(true == bFind)
{
printf("Key: %d\n", pstData->iKey);
printf("Age: %d\n", pstData->iAge);
printf("Name: %s\n", pstData->pszName);
}
else
{
printf("Find 35 failure.\n");
}
return 0;
}
4、测试log
======== Hash
i:0 age:170
i:1 age:340
i:2 age:20
i:3 age:10
i:4 age:40
i:5 null
i:6 age:60
i:7 age:30
i:8 age:80
i:9 age:90
i:10 age:100
i:11 null
i:12 null
i:13 null
i:14 null
i:15 null
i:16 null
======== Search 34
Collision >>> 0
Key: 34
Age: 340
Name: Test
======== Search 35
Collision >>> 1
Collision >>> 3
Collision >>> 7
Find 35 failure.
5、算法分析
- 时间效率为O(1),搜索很快;
- 创建散列表需要时间和空间开销 ;
- 散列函数选择有难度,会直接影响搜索效率,和空间开销。