散列表是什么,你真的懂了吗?

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位。
实际工作中需视不同的情况采用不同的哈希函数,通常考虑的因素有:
· 计算哈希函数所需时间
· 关键字的长度
· 哈希表的大小
· 关键字的分布情况
· 记录的查找频率
1.直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a·key + b,其中a和b为常数(这种散列函数叫做自身函数)。若其中H(key)中已经有值了,就往下一个找,直到H(key)中没有值了,就放进去。
2. 数字分析法:分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
3. 平方取中法:当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。
例:我们把英文字母在字母表中的位置序号作为该英文字母的内部编码。例如K的内部编码为11,E的内部编码为05,Y的内部编码为25,A的内部编码为01, B的内部编码为02。由此组成关键字“KEYA”的内部代码为11052501,同理我们可以得到关键字“KYAB”、“AKEY”、“BKEY”的内部编码。之后对关键字进行平方运算后,取出第7到第9位作为该关键字哈希地址,如下图所示
在这里插入图片描述
4. 折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。数位叠加可以有移位叠加和间界叠加两种方法。移位叠加是将分割后的每一部分的最低位对齐,然后相加;间界叠加是从一端向另一端沿分割界来回折叠,然后对齐相加。
5. 随机数法:选择一随机函数,取关键字的随机值作为散列地址,即H(key)=random(key)其中random为随机函数,通常用于关键字长度不等的场合。
6. 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p,p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。

对于散列表的性能分析有以下判断:
首先有个指标即平均查找长度ASL用来度量散列表查找效率:成功查找、不成功查找。
而且我们要对关键词的查找的同时,这还要取决于产生冲突的多少。
而影响产生冲突的多少的因素有:
(1)散列函数是够均匀:你构造的散列表不均匀,那么冲突就会变多,然后性能就会变差
(2)处理冲突的方法
(3)散列表的装填因子α

因此我们需要分析不同的冲突处理方法和装填因子对效率的影响。
1.线性探测法的查找性能
有证明可得,线性探测法的期望探测发满足下列公式:
在这里插入图片描述在这里插入图片描述
线性探测法的实现代码如下:

#include<stdio.h>
#include<stdlib.h>
#include<math.h>
typedef enum
{
    Legitimate, // 合法
    Empty,      // 空
    Deleted     // 已经删除
} EntryType;
#define MAXN 100000
typedef struct HashTbl Cell;
struct HashTbl
{
	int data;
	int lnfo;//单元状态
};
typedef struct TblNode *HashTable;
struct TblNode
{
	int TableSize;//哈希表的大小
	Cell *Cells;//存放数据的数组
};
HashTable CreateHashTable(int Tablesize);
int NextPrime(int N);
void Insert(HashTable H,int x);
int Find(HashTable H,int key);
void PrintHashTable(HashTable H);
int main()
{
	HashTable  H;
	int N;
	printf("请输入给定哈希表的大小:\n");
	scanf("%d",&N);
	H=CreateHashTable(N);
	int x;
	for(int i=0;i<5;i++)
	{
		printf("请输入元素:\n");
		scanf("%d",&x);
		Insert(H,x);//往哈希表中放入元素
	}
	PrintHashTable(H);//打印哈希表中的元素
	return 0;
}
HashTable CreateHashTable(int Tablesize)
{
	HashTable H;
	H=(HashTable)malloc(sizeof(struct TblNode));
	H->TableSize=NextPrime(Tablesize);
	H->Cells=(Cell *)malloc(sizeof(Cell)*H->TableSize);
	for(int i=0;i<H->TableSize;i++)
		H->Cells[i].lnfo=Empty;
	return H;
}
int NextPrime(int N)//返回素数
{
	int p=(N%2)?N+2:N+1;
	while(p<=MAXN)
	{
		int i;
		for((int)sqrt(p);i>2;i--)
		{
			if(p%i)
				break;
		}
		if(i==2)
			break;
		else
			p+=2;

	}
	return p;
}
void Insert(HashTable H,int x)
{
	int pos=Find(H,x);
	if(H->Cells[pos].lnfo!=Legitimate)
	{
		H->Cells[pos].lnfo=Legitimate;
		H->Cells[pos].data=x;
		printf("插入成功\n");
	}
	else
	{
		printf("键值已存在");
	}
}
int Find(HashTable H,int key)
{
	int CurPos=key%H->TableSize;
	int CollisionNum=0;
	while (H->Cells[CurPos].lnfo != Empty&&
		H->Cells[CurPos].data != key)
	{
		CurPos += 2 * (++CollisionNum) - 1;
		//如果新的定位越过数组,那么可以通过减去TableSize把它拉回数组范围内
		if (CurPos >= H->TableSize)
			CurPos -= H->TableSize;
	}
	return CurPos;
}
void PrintHashTable(HashTable H)
{
    for (int i = 0; i < H->TableSize; i++)
    {
		if (H->Cells[i].lnfo == Legitimate)
			printf("%3d",H->Cells[i].data);
    }
   printf("\n");
}

2.平方探测法和双散列探测法的查找性能
有证明可得,平方探测法和双散列探测法的查找性能满足下列公式
在这里插入图片描述
在这里插入图片描述
我们可以发现
理论值和具体值会存在一些差异,理论值是根据一般情况下的散列表,而具体值根据具体的散列表,可能这个表的元素分布比较不一样。

然后我们可以得出相应的图,看一下装填因子和ASL的关系
在这里插入图片描述
合理的装填因子应该不超过0.85

代码实现如下:

#include<stdio.h>
#include<stdlib.h>
#include<math.h>
typedef enum
{
    Legitimate, // 合法
    Empty,      // 空
    Deleted     // 已经删除
} EntryType;
#define MAXN 100000
typedef struct HashTbl Cell;
struct HashTbl
{
	int data;
	int lnfo;//单元状态
};
typedef struct TblNode *HashTable;
struct TblNode
{
	int TableSize;//哈希表的大小
	Cell *Cells;//存放数据的数组
};
HashTable CreateHashTable(int Tablesize);
int NextPrime(int N);
void Insert(HashTable H,int x);
int Find(HashTable H,int key);
void PrintHashTable(HashTable H);
int main()
{
	HashTable  H;
	int N;
	printf("请输入给定哈希表的大小:\n");
	scanf("%d",&N);
	H=CreateHashTable(N);
	int x;
	for(int i=0;i<5;i++)
	{
		printf("请输入插入的元素:\n");
		scanf("%d",&x);
		Insert(H,x);//往哈希表中放入元素
	}
	PrintHashTable(H);//打印哈希表中的元素
	return 0;
}
HashTable CreateHashTable(int Tablesize)
{
	HashTable H;
	H=(HashTable)malloc(sizeof(struct TblNode));
	H->TableSize=NextPrime(Tablesize);
	H->Cells=(Cell *)malloc(sizeof(Cell)*H->TableSize);
	for(int i=0;i<H->TableSize;i++)
		H->Cells[i].lnfo=Empty;
	return H;
}
int NextPrime(int N)//返回素数
{
	int p=(N%2)?N+2:N+1;
	while(p<=MAXN)
	{
		int i;
		for((int)sqrt(p);i>2;i--)
		{
			if(p%i)
				break;
		}
		if(i==2)
			break;
		else
			p+=2;

	}
	return p;
}
void Insert(HashTable H,int x)
{
	int pos=Find(H,x);
	if(H->Cells[pos].lnfo!=Legitimate)
	{
		H->Cells[pos].lnfo=Legitimate;
		H->Cells[pos].data=x;
		printf("插入成功\n");
	}
	else
	{
		printf("键值已存在");
	}
}
int Find(HashTable H,int key)
{
	int Currentpos, NewPos;
    int CNum = 0; // 记录冲突次数
    // NewPos = Currentpos = Hash(Key, H->TableSize); // 初始散列的位置
    int p = H->TableSize;         
    NewPos = Currentpos = key % p; // 除留余数法

    // 为空的话就是找不到
	while (H->Cells[NewPos].lnfo != Empty && H->Cells[NewPos].data != key)
    {
        // 奇数次冲突 加i的平方
        if (++CNum % 2)
        {
            // CNum相当于i的映射
            // NewPos是增加后的位置
            NewPos = Currentpos + ((CNum + 1) / 2) * ((CNum + 1) / 2);
            // 如果加超了
            // 调增成合法的地址
            if (NewPos >= H->TableSize)
                NewPos = NewPos % H->TableSize;
        }
        else
        {
            NewPos = Currentpos - (CNum / 2) * (CNum / 2);
            while (NewPos < 0)
                NewPos += H->TableSize;
        }
    }
	return NewPos;
}
void PrintHashTable(HashTable H)
{
    for (int i = 0; i < H->TableSize; i++)
    {
		if (H->Cells[i].lnfo == Legitimate)
			printf("%3d",H->Cells[i].data);
    }
   printf("\n");
}

3.分离链接法的查找性能
所有地址链表的平均查找长度定义成装填因子α
α可能超过1
有证明可得,期望探测次数为
在这里插入图片描述
在这里插入图片描述
分离链表的实现过程为:
利用除数留余法来获得相关的数组下标,如果产生冲突,则放到这个数组指向的链表之后。
在这里插入图片描述
分离链表的实现代码如下;

#include<stdio.h>
#include<stdlib.h>
#define MIN 2
typedef struct ListNode *position;
typedef position List;
struct ListNode
{
	int element;
	position next;
};

struct HashTbl
{
	int TableSize;
	List *ThisList;
};
typedef struct HashTbl *HashTable;

HashTable CreateHashTable(int N);
int NextPrime(int N);
int IsPrime(int N);
void Insert(HashTable H,int x);
position find(HashTable H,int x);

int main()
{
	HashTable H;
	int N,i,x;
	printf("请输入你要构造的散列表的大小:\n");
	scanf("%d",&N);
	H=CreateHashTable(N);
	for(i=0;i<N;i++)
	{
		printf("请输入要插入的元素:\n");
		scanf("%d",&x);
		Insert(H,x);
	}
	position p;
	printf("请输入你要查找的元素:\n");
	scanf("%d",&x);
	p=find(H,x);
	if(p)
	{
		printf("查找成功:\n");
		printf("%d",p->element);
	}
	else
	{
		printf("查找失败!");
	}
	return 0;

}
HashTable CreateHashTable(int N)
{
	HashTable H;
	int i;
	if(N<MIN)
	{
		printf("散列表太小!,无法构成散列表");
		return NULL;
	}
	H=(HashTable)malloc(sizeof(struct HashTbl));
	if(H==NULL)
	{
		printf("空间溢出!");
		return NULL;
	}
	H->TableSize=NextPrime(N);//得到大小为素数的散列表
	H->ThisList=(List *)malloc(sizeof(List)*H->TableSize);

	if(H->ThisList==NULL)
	{	
		printf("空间溢出");
	return NULL;
	}
	for(i=0;i<H->TableSize;i++)
	{
		H->ThisList[i]=(List)malloc(sizeof(struct ListNode));
		if(H->ThisList[i]==NULL)
		{
			printf("空间溢出");
			return NULL;
		}
		else
			H->ThisList[i]->next=NULL;
	}
	return H;
}
int NextPrime(int N)//判断是够是素数
{
	while(1)
	{
		if(IsPrime(N))
			return N;
		else
			N++;
	}
}
int IsPrime(int N)
{
	int i;
	for(i=2;i*i<=N;i++)
	{
		if(N%i==0)
			return 0;
	}
	return 1;
}
void Insert(HashTable H,int x)
{
	position pos,Newcell;
	List L;
	pos=find(H,x);
	if(pos==NULL)//判断条件
	{
		Newcell=(List)malloc(sizeof(struct ListNode));
		if(Newcell==NULL)
		{
			printf("溢出空间");
			return ;
		}
		else
		{
			L=H->ThisList[x%H->TableSize];//找到相应的位置并放入那个链表之后
			Newcell->next=L->next;
			Newcell->element=x;
			L->next=Newcell;
		}
	}
}
position find(HashTable H,int x)/*查找函数,作用是查找位置是否为空,为空返回NULL,方便我们进行插入结点。
								不为空就是我们要查找的那个数*/
{
	position p;
	List L;
	L=H->ThisList[x%H->TableSize];
	p=L->next;
	while(p)
	{
		if(p->element==x)
			return p;
		p=p->next;
	}
	return NULL;
}

散列表不像搜索树那样,在散列表中查找,我们只需要相应的对应,就可以实现查找效率为常数级别的。
在这里插入图片描述
而且对于字符串这样比较难比较的东西,散列表中就可以定义关键字来进行相应的查找。
在这里插入图片描述分离链接法的相应代码如下:

4.开放地址法
在这里插入图片描述
开放地址法的代码如下:

#include<stdio.h>
#include<stdlib.h>
#include<math.h>
typedef enum
{
    Legitimate, // 合法
    Empty,      // 空
    Deleted     // 已经删除
} EntryType;
#define MAXN 100000
typedef struct HashTbl Cell;
struct HashTbl
{
	int data;
	int lnfo;//单元状态
};
typedef struct TblNode *HashTable;
struct TblNode
{
	int TableSize;//哈希表的大小
	Cell *Cells;//存放数据的数组
};
HashTable CreateHashTable(int Tablesize);
int NextPrime(int N);
void Insert(HashTable H,int x);
int Find(HashTable H,int key);
void PrintHashTable(HashTable H);
int main()
{
	HashTable  H;
	int N;
	printf("请输入给定哈希表的大小:\n");
	scanf("%d",&N);
	H=CreateHashTable(N);
	int x;
	for(int i=0;i<5;i++)
	{
		printf("请输入插入的元素:\n");
		scanf("%d",&x);
		Insert(H,x);//往哈希表中放入元素
	}
	PrintHashTable(H);//打印哈希表中的元素
	return 0;
}
HashTable CreateHashTable(int Tablesize)
{
	HashTable H;
	H=(HashTable)malloc(sizeof(struct TblNode));
	H->TableSize=NextPrime(Tablesize);
	H->Cells=(Cell *)malloc(sizeof(Cell)*H->TableSize);
	for(int i=0;i<H->TableSize;i++)
		H->Cells[i].lnfo=Empty;
	return H;
}
int NextPrime(int N)//返回素数
{
	int p=(N%2)?N+2:N+1;
	while(p<=MAXN)
	{
		int i;
		for((int)sqrt(p);i>2;i--)
		{
			if(p%i)
				break;
		}
		if(i==2)
			break;
		else
			p+=2;

	}
	return p;
}
void Insert(HashTable H,int x)
{
	int pos=Find(H,x);
	if(H->Cells[pos].lnfo!=Legitimate)
	{
		H->Cells[pos].lnfo=Legitimate;
		H->Cells[pos].data=x;
		printf("插入成功\n");
	}
	else
	{
		printf("键值已存在");
	}
}
int Find(HashTable H,int key)
{
	int Currentpos, NewPos;
    int CNum = 0; // 记录冲突次数
    // NewPos = Currentpos = Hash(Key, H->TableSize); // 初始散列的位置
    int p = H->TableSize;          
    NewPos = Currentpos = key % p; // 除留余数法

    // 为空的话就是找不到
 while (H->Cells[NewPos].lnfo != Empty && H->Cells[NewPos].data != key)
    {
        NewPos = (Currentpos + (++CNum)) % H->TableSize;
    }
	return NewPos;
}
void PrintHashTable(HashTable H)
{
    for (int i = 0; i < H->TableSize; i++)
    {
		if (H->Cells[i].lnfo == Legitimate)
			printf("%3d",H->Cells[i].data);
    }
   printf("\n");
}

一般建议不使用开放地址法。
而对于分离链接法的效率来说。在这里插入图片描述

希望大家能够好好了解相关的思想和代码实现,成功get散列表。
一定要多思考,尝试自己手动敲写代码,这样才能获得收获!

  • 11
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值