7. 数据结构-查找

11,336 阅读26分钟

考纲要求 💕

知识点(考纲要求)

1、顺序表的查找

2、树表的查找

3、哈希表及其查找

考核要求

1、掌握各种查找的特性以及它们之间的差异,知道使用各种查找方法的条件。

2、重点掌握顺序查找、二分查找和分块查找的基本算法。

3、重点掌握构造哈希函数的方法和哈希冲突解决方法。能够按照给定条件构造哈希表。


▶️ 1. 查找的基本概念、平均查找长度


image.png


1.1 ✨ 查找的基本概念

❗ 1.1.1 查找表和关键字

查找(Searching):在数据集合中寻找满⾜某种条件的数据元素的过程称为查找

查找表(Search Table):是由同一类型的数据元素(记录)构成的集合

关键字(Key):数据元素中唯⼀标识该元素的某个数据项的值,使⽤基于关键字的查找,查找结果应该是唯⼀的。

  • 主关键字(Primary Key):  可以唯一地标识一个记录(比如身份证号码)
  • 次关键字(Secondary Key):  可以识别多个数据元素的关键字(比如微信昵称)

举例:

image.png

image.png


❗ 1.1.2 静态查找表和动态查找表

静态查找表(Static Search Table):只作查找操作的查找表,它的主要操作有:

  • 查找某个特定的数据元素是否在查找表中
  • 检索某个特定的数据元素和各种属性

动态查找表( Dynamic Search Table):在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素

  • 查找时插入数据元素
  • 查找时删除数据元素

image.png


❗ 1.1.3 平均查找长度

平均查找长度(ASL,Average Search Length):所有查找过程中进⾏关键字的⽐较次数的平均值

公式如下:

image.png

  • n:数据元素个数
  • Pi​:查找第i个元素的概率(通常认为查找任何一个元素概率相同
  • Ci​:查找第i个元素的查找长度

❗ 1.1.4 查找算法的评价指标

查找⻓度——在查找运算中,需要对⽐关键字的次数称为查找⻓度

平均查找⻓度(ASL, Average Search Length)—— 所有查找过程中进⾏关键字的⽐较次数的平均值

(ASL 的数量级反应了查找算法时间复杂度)

回忆下以前学过的二叉排序树的asl:

image.png

可以asl反应了时间复杂度,越短越好。

评价⼀个查找算法的效率时, 通常考虑查找成功/查找失败 两种情况的 ASL


▶️ 2. 顺序查找

image.png


2.1 ✨ 顺序查找基本思想

顺序查找(Sequential Search):又叫做线性查找。从表中第一个或最后一个记录开始,逐个进行比较。若某个记录的关键字和给定值相等则查找成功;如果查找到最后一个元素时,关键字和给定值还是不相等,则表示查找不成功

查找过程如下:

动画.gif


2.2 ✨ 顺序查找实现

❗ 2.2.1 基本顺序表实现

如果采用顺序表实现顺序查找,那么代码如下

typedef struct SSTable   //查找表的数据结构(顺序表)
{
	int* arr;              //动态数组基址 malloc申请空间
	int len;               //表的长度
}SSTable;

int Search_Seq(SSTable ST,int key)
{
	int i;
	for(i=1;i<ST.len&&ST.arr[i]!=key;i++)
	//查找成功,则返回元素下标;查找失败,则返回-1
        return  i==ST.len? -1:i;
}

或者这样写容易看:

typedef struct Sequence_table
{
	int* arr;
	int len;
}Sequence_table;

int Sequential_Search(int* a,int n,int key) //n是顺序表长度 a是arr
{
	int i;
	for(i=1;i<n;i++)
	{
		if(a[i]==key)
			return i;//查找成功,返回数组下标i
	}
	return 0; //查找失败
}

  • 查找成功举例:

image.png

  • 查找失败举例:

image.png


上面的代码中每次循环时都要对循环变量是否越界进行判断,所以我们可以设置一个“哨兵”,如果返回0则表示失败。这种放置哨兵的方式不需要在每次比较后都要判断查找位置是否越界


❗ 2.2.2 哨兵实现

代码增加一个哨兵:

typedef struct SSTable   //查找表的数据结构(顺序表)
{
	int* arr;              //动态数组基址 malloc申请空间
	int len;               //表的长度
}SSTable;

int Search_Seq(SSTable ST,int key)
{
        ST.arr[0]=key;        // “哨兵”
	int i;
	for(i=ST.len;ST.arr[i]!=key;--i) //从后往前找
        return i; //查找成功,则返回元素下标;查找失败,则返回0
}

举例:查找16关键字

  • 先把关键字放入0号位置当做哨兵

image.png

  • 从后往前扫描 i--,找到元素返回i

image.png

  • 查找失败,比如查找66,没有,最后到哨兵跳出循环,返回0表示查找失败

image.png


2.3 ✨ 效率分析

借用ASL公式:

image.png

  • n:数据元素个数
  • Pi​:查找第i个元素的概率(通常认为查找任何一个元素概率相同
  • Ci​:查找第i个元素的查找长度

查找成功  对于一个长度为n的查找表,查找任何一个元素概率均为1/n,因此关键字的比较次数呈现出的是一个等差数列也即{1,2,3,…,n},所以

image.png

  • 时间复杂度为O(n)

查找失败  对于一个长度为n的查找表,如果查找失败就表示该查找表内没有目标元素,这也就意味着所有元素都对比了一遍,也就是到len+1;

image.png

  • 时间复杂度为O(n)

2.4 ✨ 顺序查找优化(针对查找表为有序表)

当查找表为有序表时,若采用顺序查找,那么当被查找元素的值大于(或小于)了查找表中间元素的值时,查找其实就已经可以判定为失败了

image.png

其实这时候就可以判断查找失败了


2.5 ✨ ⽤查找判定树分析ASL


image.png


下图为上面顺序查找的查找判定树

  • 蓝色结点表示成功情况,有n个
  • 粉色结点通常会被当做空指针,表示失败情况,有n+1个

image.png


查找成功:  计算时,根节点是1,从根节点开始,数到每个节点的位置是几,那么这个节点的比较次数就是几,全部加起来然后除以结点个数

image.png

查找失败:  计算时,从根节点开始向每个结点处数,直到数到空指针(空指针不算),然后加起来除以空指针个数

image.png


2.6 ✨ 顺序查找优化(针对查找概率不相等)

如果查找概率不相等,可以把概率较高的结点放置在前面

image.png

这样可以使平均查找长度更小。效率更高。


▶️ 3. 二分查找(折半查找)


image.png


3.1 ✨ 二分查找基本思想

二分查找法(Binary Search):又称之为折半查找,针对有序顺序表
具体来讲:在有序表中,每次取中间记录作为比较对象,有以下三种情况

  • 若给定值与中间记录的关键字相等,则查找成功
  • 若给定值小于与中间记录的关键字,则在中间记录左半区继续查找
  • 若给定值大于与中间记录的关键字,则在中间记录右半区继续查找

不断重复上述过程,直至成功;或无此记录,查找失败


3.2 ✨ 二分查找思想流程

举例:算法的执行过程

  • 查找33,先找数组中间mid

image.png

  • 33>mid,只可能在右边区域,low=mid+1

image.png

  • 再取mid,比较查找

image.png

  • 33<mid, 只可能在左边区域. high=mid-1

image.png

  • mid=(low+high)/2 , 比较

image.png

  • 33>mid,只可能在右边区域,low=mid+1

image.png

  • 此时low=high,数值相同,查找成功

image.png

image.png


下面看看失败的样子:查找12

直接到这一步:

image.png

  • 12<mid, 只可能在左边区域. high=mid-1

image.png

  • mid=(low+high)/2 , 此时mid=1,比较12>mid,只可能在右边区域,low=mid+1;

image.png

  • 此时low,mid,high相同

image.png

  • 12>10,所以应该在右边区域,low=mid+1;但是此时low竟然大于high了,说明查找失败

image.png


3.3 ✨ 二分查找法代码

代码如下:

typedef struct SSTable   //查找表的数据结构(顺序表)
{
	int* elem;              //动态数组基址 malloc申请空间
	int len;               //表的长度
}SSTable;

//二分查找
int Binary_Search(SSTable L,int key)
	{
		int low=0,high=L.len-1,mid; //初始low=0,high等于长度-1,最后一个
		while(low <= high)
		{
			mid=(low+high)/2;//二分
			if(L.elem[mid]==key)
                        return mid;//查找成功返回所在位置
                        else  if(L.elem[mid]>key) //key<当前位置值,high=mid-1;
                        high=mid-1;//从前半部继续查找
                        else 
                        low=mid+1;//从后半部继续查找
                }
                
                return -1;//查找失败        
}

或者这样,使low=1:

	int Binary_Search(int* a,int n,int key)
	{
		int low,high,mid;
		low=1;
		hgih=n;//low和high用于界定区间
		while(low <= high)
		{
			mid=(low+high)/2;//二分
			if(key < a[mid])
				high=mid-1;
			else if(key > a[mid])
				low=mid+1;
			else
				return mid;
		}
		return 0;
	}
}

这样的好处使编号对上了,不用+1得到真正的第几个数。

  • 举例:查找key=62

1:程序开始运行,在第3~5行进行初始化,此时low=1high=10(high=n数组长度)

image.png

2:第6~15行是二分查找循环主体

3:执行第8行后,mid=5,由于a[5]=47<key,意味着目标元素很可能在右半区。所以执行第12行,也即low=5+1=6

image.png

4:再次循环,此时mid=(6+10)/2=8,又a[9]=73>key,意味着目标元素很可能在左半区,于是high=8-1=7

image.png

5:再次循环,此时mid=(6+7)/2=6,又a[6]=59<key,意味着目标元素很可能在右半区,于是low=6+1=7

image.png

6:再次循环,此时mid=(7+7)/ 2=7,此时a[7]=62=key,查找成功

也就是第七个元素。不用+1求编号。


其实上面二种算法代码都可以,只是修改一小点东西而已。


3.4 ✨ 二分查找法效率分析

对于上述查找过程,我们可以绘制出一棵二叉树,称之为二分查找的判定树

image.png

  • 绿色:查找成功
  • 紫色:查找失败

查找成功:  每个结点的比较次数等于该结点所在层数

image.png

查找失败:  失败对应的就是空指针,也就是上面的粉色结点。

image.png

对于第四层的紫色结点,每次要先查找3次,所以总共查找次数使3*4,然后求出总查找次数,每个的概率相同,除以12.


3.5 ✨ 二分查找判定树的构造

❗ 3.5.1 二分查找树规律

如果当前low和high之间有奇数个元素,则mid分开后,左右两部分元素个数相等

image.png

如果当前low和high之间有偶数个元素,则mid分开后,左半部分比右半部分少一个元素

image.png

得到结论:

折半查找的判定树中,若mid = ⌊(low + high)/2⌋ ,则对于任何⼀个结点,必有:
右⼦树结点数-左⼦树结点数=0或1


它用在哪里呢? 用在二分查找树的构造上面,比如:

image.png

最多只能多一个或者0个元素,不能多2个元素,所以3必须在左边。

image.png

而4也只能在右边继续。


❗ 3.5.2 二分查找树构造

接着上面开始,mid = ⌊(low + high)/2⌋

流程如下:

动画.gif


❗ 3.5.3 二分查找树特点

不难发现,二分查找判定树有如下特点

  • 它一定是一个 AVL(平衡二叉树)树

  • 只有最下面一层是不满的元素为n时,树高h=log2(n+1)log_2(n+1). (计算方法同"完全二叉树")

  • 判定树结点关键字:左<中<右,满⾜⼆叉排序树的定义

  • 失败结点为n+1个(等于成功结点的空链域数量)


❗ 3.5.4 二分查找效率(时间复杂度)

  • 树高h=log2(n+1)log_2(n+1) ,该树⾼不包查找失败的

  • 查找成功的ASL ≤ h ,查找失败的ASL ≤ h

  • 因此查找的次数也就是树高量级的,也就是时间复杂度=O(log2nlog_2n)


▶️ 4. 分块查找


image.png


4.1 ✨ 分块查找基本思想

分块查找:我们可以对数据集进行分块,使其分块有序,然后再对每一块建立一个索引项

分块有序具体是指:

  • 块内无序:  也即块内的记录不要求有序
  • 块间有序:  要求第n+1块中所有记录的关键字均大于第n块中所有记录的关键字,也就是递增的

其中,每一块将对应一个索引表,它保存了每个分块的最大关键字和存储区间

image.png

  • 块内无序:下面蓝色的分块的记录数据没有递增递减,无序的。
  • 块间有序:上面索引表中每个记录它记录的最大关键字是递增的。

4.2 ✨ 分块查找结构定义

定义如下:

typedef struct
{
	DataType maxValue;
	int low,high;
}Index;//索引表

ElemType  List[100];//存储实际元素的数组

4.3 ✨ 分块查找算法流程

查找时分两步进行

  • 首先在索引表中查找待查找关键字所在的块。由于索引表是块间有序的,因此很容易利用其它查找算法(如二分查找)得到结果
  • 接着在对应的块中查找目标值,由于块中大部分情况下是无序的,所以只能采用顺序查找

特别注意,即便在索引表上直接找到了元素,也仍然要进入块内查找,只有在块内找到才是真的找到

  • 比如举例查找22

image.png

  • 先使用二分查找索引表,在30这个最大关键字的区间,然后[6,8]块间,从起始位置使用顺序查找查找块内,也就是查找数组,从6开始,找到7位置成功。

  • 再来看个查找失败的流程:查找29

image.png


4.4 ✨ 分块查找用二分查找注意!

在块间查找时,如果采用二分查找,其lowhigh指针要注意变化过程,举例如下

  • 查找目标19,最终low>high查找失败

image.png

若索引表中不包含⽬标关键字,则折半查找索引表最终停在 low>high,要在low所指分块中查找


image.png

原因:

low=high时候,mid可以等于

  • mid<key low+1; low所指的关键字一定大于mid所指的元素
  • mid>key high-1; low 所指的关键字一定大于key

索引表中存的是最大的关键字的,所以我们需要到比key大的分块中。

不管发生哪种情况,都是low指向的关键字更大一点,所以选择low分块中进行查找。


  • 查找19成功

image.png

  • 查找54失败,超出索引表范围了。

image.png

low所指的范围超过索引表为空,查找失败。


4.5 ✨ 分块查找效率分析(ASL)

先看上面这个图的例子的查找效率分析:

image.png


来总结下规律:

假设,⻓度为n的查找表被均匀地分为b块,每块s个元素

image.png

设索引查找和块内查找的平均查找⻓度分别为LI、LS,则分块查找的平均查找⻓度为

ASL=LI + LS

image.png

  • 这里n=sb。带入ASL=b+1/2 + s+1/2 中,然后求导数为0,求极值。

image.png


▶️ 5. B树


image.png


5.1 ✨ B树的基本概念

❗ 5.1.1 B树

前面我们学过了二叉查找树,如何变为m叉查找树。

B树(B-tree):是一种平衡的多路查找树,结点最大的分支数目称之为B树的(order)。和二叉排序树一样,每个结点把查找范围分为了两个区间,小于它的在左侧,大于它的在右侧

比如5叉查找树:比如下面47,48,50,56那一块最大有5个分叉,最大的分支数目。

image.png

结构定义如下:

struct Node
{
	DataType keys[4];//最多4个关键字
	struct Node* child[5];//最多5个孩子
	int num;//结点中有几个关键字
}

5.2 ✨ B树操作

❗ 5.2.1 查找

动画.gif


❗ ❗ 查找效率分析-分叉数

二叉查找树的效率取决树的高度,如果树太高,要查更多层结点,效率低。

  • 策略:m叉查找树中,规定除了根节点外,任何结点⾄少有⌈m/2⌉个分叉,即⾄少含有⌈m/2⌉ − 1 个关键字

  • eg: 对于5叉排序树,规定 除了根节点外,任何结点都⾄少有[2/5]=[2.5]=3个分叉,2个关键字

image.png


那么为什么要除了根节点,根结点也保证3个分叉不好嘛

实际上做不到。

image.png

无法保证元素个数,无法保证根结点分叉数。


❗ ❗ 查找效率分析-不平衡

一个树左子树很多,不平衡,要查很多层结点。

  • 策略:m叉查找树中,规定对于任何⼀个结点,其所有⼦树的⾼度都要相同

image.png

这就是一个好的B树:

image.png


❗ 5.2.2 插入

举例:5阶B树-关键字节点数

  • 首先确定根节点,结点关键字个数 [m/2]-1≤n≤m-1 即:2≤n≤4 (注:此处省略失败结点)

image.png

  • 在插⼊key后,若导致原结点关键字数超过上限,则从中间位置([m/2] )将其中的关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置([m/2])的结点插⼊原结点的⽗结点。

动画.gif

  • 新元素⼀定是插⼊到最底层“终端节点”,⽤“查找”来确定插⼊位置

比如插入90:

image.png

  • 错误示范:插入上面层。注意:B树的失败结点只能出现在最下⾯⼀层

image.png


  • 插入88,90,99。中间88提上,90,99是88右边关键字

image.png

  • 插入60,83,87,这时候80需要提上。但是上一层49,88。需要分裂,给80腾出位置。

动画.gif

  • 继续类似这样,插入

image.png

image.png

image.png


插入总结:

image.png


❗ 5.2.3 删除

举例:5阶B树-关键字节点数

1. 删除节点在终端节点

  • 若被删除关键字在终端节点,则直接删除该关键字(要注意节点关键字个数是否低于下限 ⌈m/2⌉ − 1)

也就是删除后那个快的关键字个数还是要在([m/2]-1<=n<=m-1).

比如删除60关键字:

图片.png

2.删除节点不在终端节点

  • 但是如果删除的不是终端节点怎么办?

  • 若被删除关键字在非终端节点,则用直接前驱或直接后继来替代被删除的关键字

  • 直接前驱:当前关键字左侧指针所指子树中“最右下”的元素

  • 直接后继:当前关键字右侧指针所指子树中“最左下”的元素

举例:删除根结点80,找到它的直接后继,也就是最右下元素77来替代

图片.png

  • 替代后:

图片.png


  • 删除77,找到它的直接后继,也就是最左下元素,删除替代。

图片.png

替换后:

图片.png


3. 删除后低于关键字下限

如果删除后低于关键字下限。

比如删除38,这样第一个块,只剩一个25了,低于2个关键字下限。需要借位。

  • 兄弟够借。若被删除关键字所在结点删除前的关键字个数低于下限,且与此结点右(或左)兄弟结点的关键字个数还很宽裕,则需要调整该结点、右(或左)兄弟结点及其双亲结点(父子换位法)

比如现在先试试右边兄弟的79节点,但是70节点比49大。

图片.png

不符合<49块,所以可以再看看父亲节点,把49拉下来,70拉上去。

图片.png

  • 说白了,当右兄弟很宽裕时,用当前结点的后继、后继的后继来填补空缺

  • 再看一个左孩子的。左兄弟很宽裕时,用当前结点的前驱、前驱的前驱来填补空缺

动画.gif


4. 兄弟不够借,合并

如果左边、右边的也不够,咋办?

需要合并。

图片.png

  • 兄弟不够借。若被删除关键字所在结点删除前的关键字个数低于下限,且此时与该结点相邻的左、右兄弟结点的关键字个数均=[m/2]-1 ,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进行合并。

动画.gif

但是这时候,父节点的关键就一个73不合法,需要合并。

动画.gif

最后:

图片.png


总结

无论如何都要保证B树的性质:关键字的个数,和子树0<关键字1<子树1<关键字2<子树2<...


通过上面的可以总结出B树的特点和效率


5.3 ✨ B树(假设m阶)特点及效率

image.png

image.png


压缩后核心特性:

image.png


5.4 ✨ B树的高度

注:⼤部分学校算B树的⾼度不包括叶⼦结点(失败结点)

  • 求最小高度

image.png


  • 求最大高度

课本上的方法是这样的: 从叶子结点的下限最少

image.png

如果从关键字的总数出发:

image.png


5.5 ✨ B树回顾总结

image.png


▶️ 6. B+树


图片.png


6.1 ✨ B+树基本概念

  • 以下是4阶B+树:4阶是最多4个子树

图片.png

一棵m阶B+树需满足下列条件:

  • 每个分支结点最多有m棵子树(孩子结点)。几阶

  • 非叶根结点至少有两棵子树,其他每个分支结点至少有[m/2]棵子树

  • 结点的子树个数与关键字个数相等

  • 所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来

  • 所有分支结点中仅包含它的各个子结点中关键字的最大值及指向其子结点的指针。


解释:

  • 非叶根结点至少有两棵子树,其他每个分支结点至少有[m/2]棵子树

图片.png


  • 结点的子树个数与关键字个数相等

图片.png


  • 所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来

图片.png


  • 所有分支结点中仅包含它的各个子结点中关键字的最大值及指向其子结点的指针。

图片.png


6.2 ✨ B+树的查找

举例查找目标成功:9

动画.gif

举例查找目标失败:7

动画.gif

到这一步时候从大到小排序的,已经到8了,说明没有7,查找失败


除了从上往下遍历查找外,也可以叶子结点顺序查找

动画.gif


B+树中,无论查找成功与否,最终一定都要走到最下面一层结点


6.3 ✨ B+树 vs B树

6.3.1 关键字与分叉

m阶B+树m阶B树
n个关键字对应n个分叉n个关键字对应n+1个分叉(子树)
MergedImages.png

6.3.2 结点关键字数

m阶B+树m阶B树
根节点的关键字数n∈[1, m]。其他结点的关键字数n∈[[m/2] , m]根节点的关键字数n∈[1, m-1]。 其他结点的关键字数n∈[[m/2] -1, m-1]

MergedImages(1).png

6.3.3 关键字重复

m阶B+树m阶B树
在B+树中,叶结点包含全部关键字,非叶结点中出现过的关键字也会出现在叶结点中在B树中,各结点中包含的关键字是不重复的

MergedImages(2).png

6.3.4 结点包含信息否

m阶B+树m阶B树
在B+树中,叶结点包含信息,所有非叶结点仅起索引作用,非叶结点中的每个索引项只含有对应子 树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。B树的结点中都包含了关键字对应的记录的存储地址

MergedImages(3).png


典型应用:关系型数据库的“索引”(如MySQL)

图片.png


总结对比

图片.png


▶️ 7. 散列查找(哈希表)


image.png

image.png


7.1 ✨ 散列表(哈希表)基本概念

❗ 7.1.1 基本概念


image.png


散列表(Hash Table),又称哈希表。是一种数据结构,特点是:数据元素的关键字与其
存储地址直接相关

image.png

举例:

例:有⼀堆数据元素,关键字分别为 {19, 14, 23, 1, 68, 20, 84, 27, 55, 11, 10, 79},散列函数 H(key)=key%13(关键字对13取余)

image.png

❗ 7.1.2 同义词和冲突

然后下面的1%13=1,重复占位了。

image.png

1和14是同义词。散列函数已经确定的位置存放了元素,冲突。

  • 若不同的关键字通过散列函数映射到同⼀个值,则称它们为“同义词”
  • 通过散列函数确定的位置已经存放了其他元素,则称这种情况为“冲突

❗ 7.1.3 拉链法处理冲突

  • 拉链法(⼜称链接法、链地址法)处理“冲突”:把所有“同义词”存储在⼀个链表中

image.png


7.2 ✨ 散列查找

查找⻓度——在查找运算中,需要对比关键字的次数称为查找⻓度


举例: 求查找长度, 查找27

  • 通过散列函数计算⽬标元素存储地址:Addr=H(Key), 27%13=1, 27的查找⻓度=3

image.png

  • 查找失败举例 查找21

image.png

  • 查找失败举例: 查找66

图片.png


这里需要注意不同的学校不同,也就是说有些地方认为这是一次查找,所以长度为1,所以要具体分别对待。


7.3 ✨ 求查找ASL

❗ 7.3.1 查找成功ASL

图片.png

求查找成功:

  • 按层数查,第一层,第二层。。。。

图片.png

  • 按查找次数总和/12(总结点)

图片.png

冲突越多,比如这上面的2,3,4.查找效率越低。所以要尽可能平衡(同义词)。

图片.png

最理想情况:散列查找时间复杂度可到达O(1)


❗ 7.3.2 查找失败ASL(装填因子)

怎样计算查找失败下的ASL:

查找失败映射到每个结点概率相同。

图片.png

这里引入装填因子概念。

上面分子部分本质就是数据元素的个数,因此装填因子α=表中记录数/散列表⻓度,也就是元素记录数/13=0.92

装填因子会直接影响 散列表的查找效率


7.4 ✨ 设计常见散列(哈希)函数

❗ 7.4.1 除留余数法

除留余数法 —— H(key) = key % p

p取值:散列表表⻓为m,取一个不大于m但最接近或等于m的质数p

举例:

图片.png

设:可能出现的关键字={1,2,3,4,5,6,7,8,9,10......}

图片.png

这里为啥右边取质数冲突更大?

因为关键字连续,会产生哈希冲突——不同的关键字映射到了相同的地址


设:可能出现的关键字={2,4,6,8,10,12......}

图片.png


Tips:散列函数的设计要结合实际的关键字分布特点来考虑,不要教条化。适用于一定规律的


❗ 7.4.2 直接定址法

直接定址法 —— H(key) = key 或 H(key) = a*key + b

其中,a和b是常数。这种方法计算最简单,且不会产生冲突。适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费。

举例:

图片.png


❗ 7.4.3 直接定址法

数字分析法 —— 选取数码分布较为均匀的若干位作为散列地址

设关键字是r进制数(如十进制数),r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等;而在某些位上分布不均匀,只有某几种数码经常出现,此时可选取数码分布较为均匀的若干位作为散列地址

这种方法适合于已知的关键字集合, 若更换了关键字,则需要重新构造新的散列函数

例:以“手机号码”作为关键字设计散列函数

图片.png


❗ 7.4.4 平方取中法

平方取中法——取关键字的平方值的中间几位作为散列地址。

具体取多少位要视实际情况而定。这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀,适用于关键字的每位取值都不够均匀或均小于散列地址所需的位数

举例:

图片.png

比如1210x1210,这里面跟12有关的是绿色圈起来部分,所以只用考虑中间部分,也就是641。

实际举例: 身份证

图片.png

这里看出生年月日,一般都是19或者20,每位取值不够均匀。

假设学生不超过十万人,可身份证号平方取中间5位

图片.png


例:要存储整个学校的学生信息,以“身份证号”作为关键字设计散列函数

图片.png

若散列表的⻓度为1000000000000000000(别数了,有18个0)
则可以直接用身份证号作为散列地址,且不可能有冲突,查找时间复杂度为O(1)
也就是直接定址。也就是不会重复。


总结: 散列查找是典型的“用空间换时间”的算法,只要散列函数设计的合理,则散列表越
⻓,冲突的概率越低。


7.5 ✨ 解决hash哈希冲突

❗ 7.5.1 拉链法

拉链法(又称链接法、链地址法)处理“冲突”:把所有“同义词”存储在一个链表中

图片.png


❗ 7.5.2 开放定址法

当产生哈希冲突时,如果哈希表没有被装满,那么哈希表中必然还会有空的位置,所以开放定址法就会不断寻找空的位置,找到空的位置将关键字塞入进去

其数学递推公式为:

图片.png

按照找的方式不用可以分为三种方法:

图片.png

7.5.2.1 线性探测法

从没有发生冲突的位置开始,依次向后探测,直到找到下一个空的位置为止

线性探测法—— di = 0, 1, 2, 3, ..., m-1;即发生冲突时,每次往后探测相邻的下一个单元是否为空。

举例:

图片.png

  • 19,14,23,无冲突,插入1,冲突

运用公式:

图片.png

  • 第一次发生冲突: 1%13=1; H0=(1+d0)%16=(1+0)%16=1;

  • 发生第一次冲突后,计算下一个H1,哈希地址。 H1=(1+d1)%16=(1+1)%16=2;

  • 也就是检查冲突的下一个位置,看是否空闲。

图片.png

1放入冲突的下一个位置。

其余流程如下:

动画.gif

  • 注意:这里使用的%是13,其实线性探测(冲突函数)应该使用的是表长16

比如举例:继续插入25

图片.png

图片.png


7.5.2.2 查找操作

举例:查找成功27

图片.png

直接散列函数求值H(key)=27%13=1 ; 冲突了使用线性探测法

图片.png

总共探测四个位置找到27;27的查找长度=4

图片.png


  • 查找失败21

图片.png

越早遇到空位置,就可以越早确定查找失败


7.5.2.3 删除操作

注意:采用“开放定址法”时,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填入散列表的同义词结点的查找路径,可以做一个“删除标记”,进行逻辑删除

主要防止查找时候出错,提前遇到了空白的位置,结束路径。

动画.gif


7.5.2.4 查找效率分析ASL

还是上面:

图片.png

图片.png


  • 查找失败的情况:初次探测的地址 H0 只 有可能在[0,12],所以有13种情况。

0 的位置本身是空的。查找1次就行了。
%取余后映射到1位置,最多查找到13,查找13次。 依次往后。
最后一个映射到12位置,最多查找到13,查找2次。

图片.png


查找效率不高的原因:

线性探测法很容易造成同义词、非同义词的“聚集(堆积)”现象,严重影响查找效率

产生原因——冲突后再探测一定是放在某个连续的位置


接下来学习第二种方法:开放定址法

7.5.2.5 平方探测法

定义:

图片.png


例:散列函数 H(key)=key%13,采用平方探测法处理冲突

图片.png

第一个元素6已经插入,第二个元素19,19%13=6,发生冲突。
采用开放定址法,H1=(6+1)%27(表长)=7. 19放入7位置。

第三个元素32%13=6,冲突,采用开放定址法,H2=(6-1)%27(表长)=5. 32放入5位置。

d是增量,相当于正值向右边移动d,负值向左边移动d。

后续如下:

动画.gif


查找

查找过程如下:

动画1.gif


7.5.2.6 m=4j + 3的素数

散列表⻓度m必须是一个可以表示成4j + 3的素数,才能探测到所有位置

比如m=7和m=8:

图片.png


❗ 7.5.3 伪随机序列法

伪随机序列法: di 是一个伪随机序列,如 di= 0, 5, 24, 11, ..

定义:

图片.png

举例:

动画1.gif


❗❗ 总结

处理冲突的方法——开放定址法

图片.png


再散列法(再哈希法)

再散列法(再哈希法):除了原始的散列函数 H(key) 之外,多准备几个散列函数, 当散列函数冲突时,用下一个散列函数计算一个新地址,直到不冲突为止:

  • 公式:Hi = RHi (Key) i=1,2,3....,k

王道上的解释如下:

图片.png