散列表

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情

简介

散列表(Hash Table)的实现常常叫做散列(Hash)。散列是一种用于常数时间进行插入、删除和查找的技术。但是,那些需要元素箭任何排序信息的操作将不会得到有效的支持。因此,FindMin、FindMax以及以线性时间按排序将整个表进行打印的操作是散列表不支持的。

理想的散列表数据结构只不过是一个包含有关键字的具有固定大小的数组。典型情况下,一个关键字就是带有相关值的字符串。把表的大小记为:TableSize。每个关键字被映射到从0到TableSize - 1这个范围中的某个数,并且被放到适当的单元内。这个映射就叫做散列函数(hash function)。理想情况下,它应该运算简单并且保证任何两个不同的关键字映射到不同的单元。不过,这是不可能的,因为单元的数目是有限的,而关键字实际上却是无穷的。因此,寻找散列函数时,应该尽量在单元之间均匀的 分配关键字。剩下的问题就是要选择一个函数,决定当两个关键字散列到同一个值的时候(即发生冲突)应该做些什么以及如何确定散列表的大小。

散列函数

如果输入的关键字是整数,那么一般合理的方法就是直接返回“Key mod TableSize”的结果,同时,表的大小最好选择为素数。但是,通常,关键字是字符串。这种情形下,散列函数需要仔细的选择。

一种选择方法是把字符串中字符的ASCII码值加起来。这种方法的缺点很明显,如果表很大,则函数将不会很好的分配关键字。例如,假定TableSize = 10007(素数),并设所有的关键字至多8个字符长,由于char型变量的值最多是127,因此散列函数只能取值在0到1016(127 * 8)之间,这显然不是一种均匀分配。

另一种散列函数假设Key至少有两个字符外加NULL结束符,值27表示英文字符个数外加空格,而729 = 27 * 27,该函数只考察前三个字符,但是,假如他们是随机的,而表的大小还是10007,那么就会得到一个合理的均匀分配。但是,英文不是随机的,虽然3个字符(忽略空格)有26*26*26=17576中可能的组合,但是,实际上联机词典显示,3个字母的不同组合数实际上只有2851,即使这些组合没有冲突,也只用掉不到28%的空间。所以这种方法也不是很好。所以,对于关键字为字符串时,一定要仔细考虑选择的散列函数。上两种散列函数代码如下:

typedef unsigned int Index;
Index Hash(int key, int TableSize)
{
	return key % TableSize;
}


Index Hash(const char *key, int TableSize)
{
	unsigned int HashVal = 0;
	while (*key != '\0'){
		HashVal += *key++;
	}
}

Index Hash(const char *key, int TableSize)
{
	return (key[0] + 27 * key[1] + 729 * key[2]);
}

冲突解决

目前,解决冲突的办法一般有三个,分离链接法,开放地址法,再散列,其中,开放地址法包括:一次线性探测,二次线性探测,双散列。

分离链接

其解决冲突的主要做法是将散列到同一个值的所有元素保留到一个表中。为方便起见,这些表都有表头,因此,表的实现与链表其实是一样的,如果空间紧凑,则可以去掉表头。如右图所示:

为了执行Find,需要使用散列函数来确定究竟考察哪个表,此时可以用通常的方式遍历该表并返回所找到的被查找项所在的位置。为了执行Insert,同样遍历一个相应的表以检查该元素是否已经处在适当的位置(如果要插入重复的元素,那么通常要留出一个额外的域,这个域当重复元出现时增1)。如果这个元素是个新的元素,那么它或者被插到表的尾端,或者前端,哪个容易就执行哪个。删除元素则是链表中的删除操作的直接实现,此处不再赘述。

装填因子

定义散列表的装填因子lamda为散列表中的元素个数与散列表大小的比值。表的平均长度为lamda。执行一次查找所需要的工作是计算散列函数值所需要的常数时间加上遍历表所用的时间。在一次不成功的查找中,遍历的链接数平均为lamda。成功的查找则需要遍历大约1+0.5*lamda。它保证必然会遍历一个链接,而我们也期望沿着一个表的中途就能找到匹配的元素。这就指出,表的大小实际并不重要,而装填因子才是重要的。分离链接散列的一般法则是使得表的大小尽量与预料的元素个数差不多,换句话说,即lamda尽可能为1。

代码如下:

头文件:hashsep.h

typedef int ElementType;
typedef unsigned int Index;

#ifndef _HashSep_H_

struct ListNode;
typedef struct ListNode *Position;
struct HashTbl;
typedef struct HashTbl *HashTable;

HashTable InitializeTable( int TableSize);
void DestroyTable( HashTable H );
Position Find( ElementType Key, HashTable H );
void Insert( ElementType Key, HashTable H );
ElementType Retrieve( Position P );

#endif

实现文件:hashsep.c

#include "hashsep.h"
#include <stdlib.h>
#include <stdio.h>

#define  MinTableSize (10)

struct ListNode
{
	ElementType Element;
	Position Next;
};

typedef Position List;

struct HashTbl
{
	int TableSize;
	List *TheLists;			//注意,TheList实际上是一个指向ListNode结构的指针的指针
};

static Index Hash(ElementType key, int TableSize)
{
	return key % TableSize;
}

static int NextPrime( int N )	//取大于N的下一个素数
{
	int i, flag = 0;
	if (N % 2 == 0)
		N++;
	for (;;N+=2)
	{
		flag = 0;
                for (i = 2; i * i < N; ++i)
		{
			if (N % i == 0)
			{
				flag = 1;
				break;
			}	
		}
		if (!flag)
			return N;
	}
}

HashTable InitializeTable(int TableSize)
{
	HashTable H;
	int i;
	if (TableSize < MinTableSize)
	{
		printf("Table size too small!");
		return NULL;
	}
	H = (HashTable)malloc(sizeof(struct HashTbl));	//初始化Hash表
	if (H == NULL)
	{
		printf("Malloc space for HashTable failed!");
		return NULL;
	}
	H->TableSize = NextPrime(TableSize);
	H->TheLists = malloc( sizeof( List ) * H->TableSize );	//为链表分配空间
	if (H->TheLists == NULL)
	{
		printf("Malloc space for list failed!");
		return NULL;
	}
	for (i = 0; i < H->TableSize; ++i)
	{
		H->TheLists[ i ] = malloc( sizeof( struct ListNode ) );	//为每个表头分配空间
		if( H->TheLists[ i ] == NULL )
		{
			printf("Malloc space for list failed!");
			return NULL;
		}
		else
			H->TheLists[ i ]->Next = NULL;
	}
	return H;
}


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;
}

void Insert( ElementType Key, HashTable H )
{
	Position Pos, NewCell;
	List L;
	Pos = Find(Key, H);
	if ( Pos == NULL )
	{
		NewCell = (Position)malloc(sizeof(struct ListNode));
		if (NewCell == NULL)
		{
			printf("Insert::Out of space!");
			return;
		}
		L = H->TheLists[Hash(Key, H->TableSize)];
		NewCell->Element = Key;
		NewCell->Next = L->Next;
		L->Next = NewCell;
	}
}


ElementType Retrieve( Position P )
{
	return P->Element;
}

void DestroyTable( HashTable H )
{
	int i;
	for (i = 0; i < H->TableSize; ++i)
	{
		Position P = H->TheLists[i];
		Position Tmp;
		while (P != NULL)
		{
			Tmp = P->Next;
			free(P);
			P = Tmp;
		}
	}
	free(H->TheLists);
	free(H);
}

开放地址法

分离链接散列算法的缺点是需要指针,由于给新单元分配地址需要时间,因此不可避免的会影响算法的速度。而开放地址法是另一种不需要链表解决冲突的办法。这种办法的原理是:当有冲突发生时,那么就要尝试选择另外的单元,直到找出空的单元为止。更一般的,h0(x),h1(x),h2(x),等等相继被试选,其中,hi(x)=(Hash(X) + F(i)) mod TableSize,且F(0) = 0。函数F是解决冲突的办法。因为所有的数据都要放入表内,所以开放地址散列法所需要的表要比分离链接散列更大。一般来说,对于开放地址法,装填因子lamda应该低于0.5。

线性探测法

即解决冲突函数F为:F(i)= i。即从冲突位置开始,依次查看该冲突位置后的每个位置,看是否已经放入元素。这种方法,只要表足够大,总会找到空的单元,将新元素插入到散列表中。但是,如此花费的时间是相当大的。即使表相对较空,已经插入的元素会开始形成一些区块,其结果形成一次聚集,于是,散列到区块中的任何关键字都需要多次试选单元才能解决冲突。

平方探测法

平方探测是消除线性探测中一次聚集问题的冲突解决方法。平方探测就是冲突函数为二次函数的探测方法,流行的选择是:F(i)=i*i。对于线性探测,让元素几乎填满散列表并不是一个好主意,因为此时表的性能会降低。对于平方探测情况甚至更糟,一旦表被填满超过一半,当表的大小不是素数时甚至在表被填满一半之前,就不能保证一次找到一个空单元了。虽然平方探测排除了一次聚集,但是散列到同一位置上的那些元素将探测相同的备选单元,这叫做二次聚集

双散列

顾名思义,即两个散列函数,对于双散列,一种流行的选择是:F(i)=i*hash_2(X)。这个公式是说,将第二个散列函数用到X并在距离hash_2(X),2hash_2(X),3hash_2(X)等处探测。一定要注意hash_2(X)的选择,选择不好是灾难性的。

在开放地址散列表中,标准的删除操作不能进行。因为相应的单元可能已经引起过冲突,元素绕过它存在了别处。因此,开放地址散列表需要懒惰删除。

下面给出平方探测的代码:
头文件hashquad.h:

typedef int ElemtypeType;
#ifndef _HashQuad_H
struct HashTbl;
typedef struct HashTbl *HashTable;

HashTable InitializeTable( int TableSize );
void DestroyTable( HashTable H );
Position Find (ElemtypeType Key, HashTable H );
void Insert( ElemtypeType Key, HashTable H );
ElemtypeType Retrieve( Position P, HashTable H );
HashTable Rehash( HashTable H );

#endif
实现文件:hashquad.c

#include "hashquad.h"
#include <stdio.h>
#include <stdlib.h>

typedef unsigned int Index;
typedef Index Position;

#define MinTableSize (10)

enum KindOfEntry {Legitimate, Empty, Deleted};

struct HashEntry
{
	ElemtypeType Element;
	enum KindOfEntry Info;
};

typedef struct HashEntry Cell;

struct HashTbl
{
	int TableSize;
	Cell *TheCells;
};

static int NextPrime( int N )
{
	if (N % 2 == 0)
		++N;
	int i = 0;
	int flag = 0;
	for (;;N += 2)
	{
		flag = 0;
		for (i = 2; i * i < N; ++i)
		{
			if (N % i == 0){
				flag = 1;
				break;
			}
		}
		if (flag == 0)
			return N;
	}
}

Index Hash( ElementType Key, int TableSize )
{
	return Key % TableSize;
}

HashTable InitializeTable( int TableSize )
{
	HashTable H;
	int i;
	if (TableSize < MinTableSize)
	{
		printf("Tabsize is too small!");
		return NULL;
	}

	H = malloc(sizeof(struct HashTbl));
	if (H == NULL)
	{
		printf("Initial hashtable failed, out of space!");
		return NULL;
	}
	H->TableSize = NextPrime(TableSize);
	H->TheCells = malloc(sizeof(Cell) * H->TableSize);
	if (H->TheCells == NULL)
	{
		printf("Initial elements failed, out of space!");
		return NULL;
	}
	for (i = 0; i < H->TableSize; ++i)
	{
		H->TheCells[i].Info = Empty;
	}
	return H;
}

Position Find(ElementType Key, HashTable H)
{
	Position CurrentPos;
	int CollisionNum = 0;
	CurrentPos = Hash(Key, H->TableSize);
	while (H->TheCells[CurrentPos].Info != Empty && H->TheCells[CurrentPos].Element != Key)
	{
		CurrentPos += 2 * ++CollisionNum - 1;   //因为x*x = (x-1)(x-1)+2*x-1
		if (CurrentPos > H->TableSize)
			CurrentPos -= H->TableSize;
	}
	return CurrentPos;
}


void Insert( ElemtypeType Key, HashTable H )
{
	Position Pos;
	Pos = Find(Key, H);
	if (H->TheCells[Pos].Info != Legitimate)
	{
		H->TheCells[Pos].Info = Legitimate;
		H->TheCells[Pos].Element = Key;
	}
}

ElemtypeType Retrieve( Position P, HashTable H )
{
	return H->TheCells[P].Element;
}

void DestroyTable( HashTable H )
{
	free(H->TheCells);
	free(H);
}

再散列

对于使用平方探测的发放地址散列法,如果表的元素填的太满,那么操作的运行时间开始消耗过长,且Insert操作可能失败。这可能发生于有太多的移动和插入混合的场合。此时,一种解决方法是建立另一个大约两倍大的表(而且使用一个相关的新散列函数),扫描整个原始表,计算每个未删除元素的新散列值并将其插入到新表中。

代码如下:

HashTable Rehash( HashTable H )
{
	int i, OldSize;
	Cell *OldCells;
	OldCells = H->TheCells;
	OldSize = H->TableSize;

	H = InitializeTable( 2 * OldSize);
	for (i = 0; i < OldSize; ++i)
		if (OldCells[i].Info == Legitimate)
			Insert(OldCells[i].Element, H);
	free(OldCells);
	return H;
}











哈希表算法(闭散列表操作,开散列表操作)创建,查找,插入,删除 #include <stdio.h> #include <malloc.h> #include "stdlib.h" #include <math.h> typedef int Status; typedef int KEYTYPE; typedef struct { KEYTYPE key; }ElemType; typedef struct { ... 阅读详情

相关推荐

哈希表与散列表的原理及C++实现

哈希表(Hash Table)是一种高效的数据结构,用于存储键值对(Key-Value Pairs)。它通过哈希函数(Hash Function)将键(Key)映射到一个固定大小的数组(称为散列表)中的某个位置,从而实现快速的插入、查找和删除操作。哈希表的核心思想是通过哈希函数将键转换为数组的索引,从而直接访问对应的值。理想情况下,哈希表的插入、查找和删除操作的时间复杂度都是O(1)。哈希表是一种高效的数据结构,通过哈希函数将键映射到数组索引,从而实现快速的插入、查找和删除操作。

Lostgreen的博客 1540

哈希表的查找、删除等相关算法

哈希表中线性探查法解决冲突,查找,删除、插入关键字等操作

什么是散列表(哈希表)?

本文文末有福利,不要错过奥! 前言 假设你们班级100个同学每个人的学号是由院系-年级-班级和编号组成,例如学号为01100168表示是1系,10级1班的68号。为了快速查找到68号的成绩信息,可以建立一张表,但是不能用学号作为下标,学号的数值实在太大。因此将学号除以1100100取余,即得到编号作为该表的下标,那么,要查找学号为01100168的成绩的时候,只要直接访问表下标为68的数据即可...

守望的博客-编程珠玑 3557

算法设计与分析——散列表/哈希表(Hash Table)(三):散列表原理

直接寻址技术的缺点是非常明显的:如果全域U很大,则在一台标准的计算机可用内存容量中,要存储大小为∣U∣|U|∣U∣的一张表TTT也许不太实际,甚至是不可能的。还有,实际存储的关键字集合KKK相对UUU来说可能很小,使得分配给TTT的大部分空间都将浪费掉。 当存储在字典中的关键字集合KKK比所有可能的关键字的全域UUU要小许多时,散列表需要的存储空间要比直接寻址表少得多。特别地,我们能将散列表的存储需求降至Θ(∣K∣)\Theta(|K|)Θ(∣K∣),同时散列表中查找一个元素的优势仍得到保持,只需要O(1)

冯·诺依曼 1万+

解决散列表冲突方法

1 解决冲突方法1.1 链接法又叫拉链法,将具有同一散列地址的记录存储在一条线性链表中1.2 开放定址法通过对原hash函数进行修改,添加探查函数,当出现冲突时,往下一个地址写数据图中p(i)为探查函数探查函数分为以下四种:        线性探查法:(p(i)为1,2,...,...n)        二次探查法        随机探查法:(p(i)为随机数)        双散列函数法使用开放...

LishcCai的博客 1万+

散列表(线性、平方、双散列、分离链表解决冲突)

前言 相信看到这篇博客的小伙伴应该都是在学数据结构, 用线性探测的方法解决冲突

沉在海里的鱼的博客 5859

散列表基本操作(c++)

但是,散列表的缺点是需要消耗大量的内存空间,因为需要预留足够的空间来存储散列表。此外,散列表的性能取决于哈希函数的设计,如果哈希函数不好,会导致散列表的性能下降。如果哈希函数设计得好,可以使得散列表的插入、删除和查找操作的时间复杂度都为O(1)。具体实现过程中,使用了哈希函数将关键字映射到散列表中的位置,然后使用链表来解决哈希冲突的问题。

m0_65690223的博客 532

数据结构小记【Python/C++版】——散列表

数据结构基础

程序员与背包客_CoderZ 1615

哈希表(散列表)C/C++代码实现

哈希表(散列表): 1,构造散列函数 最常用的方法是除留余数法: H(key) = key%p 这个方法的关键是选取适当的p, 一般情况下,可以选p为小于表长的最大质数。例如表长m=100, 可取p=97。 2,处理冲突 选择一个 “好” 的散列函数可以在一定程度上减少冲突,但在实际应用中,很难完全避免发生冲突 1,开放地址法 (1)线性勘测法 (2)二次探测法 (3)伪随机探测法 线性探测法的优点是:只要散列表未填满,总能 找到一个不发生冲突的地址。缺点是:会产生 ”二次聚集“ 现象。而二次探测法和伪随机

Ycy的博客 1万+

数据结构——哈希表(散列表

文章目录一,哈希表(散列表)概念二,哈希(散列)函数的构造哈希(散列)函数的构造原则构造方法平方取中法折叠法保留余数法三,冲突处理散列冲突的方法开放定址法1.线性探测法2.二次探测法3.随机探测法再散列函数法公共溢出区法链地址法四,代码实现1.哈希函数2.链表和哈希表的创建3.哈希表初始化3.从哈希表中根据key查找元素4.哈希表插入元素5.元素删除6.哈希表销毁 一,哈希表(散列表)概念 大话数据结构里面是这样介绍的: 散列表,又称为哈希表(Hash table),采用散列技术将记录存储在一块连续的存储空

m0_61705102的博客 1万+

哈希表(散列表)详解

深入理解哈希表,哈希函数的构造和解决哈希冲突的方法

不能再留遗憾了的博客 2214

[一篇详解哈希表]数据结构之哈希表(散列表)

✅作者简介:大家好,我是Philosophy7?让我们一起共同进步吧!🏆 📃个人主页:Philosophy7的csdn博客 🔥系列专栏: 👑哲学语录: 承认自己的无知,乃是开启智慧的大门 💖如果觉得博主的文章还不错的话,请点赞👍+收藏⭐️+留言📝支持一下博>主哦🤞 文章目录散列表简介:为什么需要散列表?哈希函数的实现哈希表的代码 散列表 简介: 散列表又可以叫做哈希表,那什么是散列表呢? 现在就让我们深入研究一下吧 是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把.

Philosophy7的博客 2364

散列表(哈希表)知识详解

哈希表知识总结

m0_57105290的博客 6495

数据结构 | 散列表(哈希表)

散列表(Hash Table),也称为哈希表,是一种根据关键码值(Key value)而直接进行访问的数据结构。它通过把关键码值映射到表中一个位置来访问记录,从而加快查找速度。这个映射函数称为散列函数,存放记录的数组称为散列表

ROBOT_a的博客 2625

散列表(哈希表)

散列表(hash table,又称哈希表),是一种数据集,其中数据项的存储方式尤其有利于将来快速的查找定位。散列表的基本思想是,首先在关键字key和存储位置p之间建立一个对应关系H,使得p = H(key),H称为哈希函数,p称为散列地址。当创建哈希表时,把关键字key的记录直接存入地址为H(key)的地址单元中;以后查找关键字为key的元素时,再利用哈希函数p = H(key)计算出该元素的散列地址p,从而达到直接存取记录的目的。

2302_77530416的博客 952

C++数据结构——散列表(哈希表)

一:散列表的概念 定义:散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希函数。 基本概念: 若关键字为k,则其值存放在f(k)的存储位置上。由此,不需比较便可直

eyes++的博客 2762

散列表 分布式散列表(DHT)哈希表 是什么

  (如果你自认为是一个熟练的程序员,请直接略过“预备知识”这个章节,看下一章节)   (注:在本文中,凡是提及“散列”或“哈希”或“hash”,均表示相同含义)   关于 hash 的概念,俺曾经写过一篇相关的扫盲教程《扫盲文件完整性校验——关于散列值和数字签名》,不了解此概念的同学,可以先看看。   老实说,如果你还没有搞明白 hash 的概念,就不要浪费时间看本文的后续部分啦。   “散列表/哈希表”是用来存储“键值对”的一种容器。“键值对”洋文称之为“key/value pairs”,简称“

软件工程小施同学 的专栏 2533

哈希表(散列表

除留余数法用关键字除以p得到的余数作为关键字的存储位置.(p是一个整数) 那么关键是怎么取到合适的除数p?技巧:设表长为m,取p<=m且为质数例如{15,23,27,38,53,61,70},散列函数Hash(key)=key%7,那么哈希表为:查找的时候同样用这个哈希函数,比如我们要查找70.用10除以7余数为0,直接去0号位置查找.以上就是我们常用的散列表的构造方法:直接定址法和除留余数法.非同义词不会冲突,无”聚集”现象;链表上节点空间动态申请,更适合于表长不确定的情况.

weixin_75188368的博客 1324

面试八股之集合篇6.1——散列表——散列表(哈希表)HashTable概述

🔥本篇概览:详细讲解了散列表(哈希表)的性质

做一个善良的人,做好自己的事,如果愿意,再发一份光。 955
上一篇: AVL树C语言实现
下一篇: 优先队列之二叉堆与d-堆
红桃Ace
博客等级 码龄15年 5粉丝 16原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值