26考研——查找_散列(Hash)表(7)

408答疑



五、散列(Hash)表

散列表的基本概念

核心定义

  • 散列表(哈希表):通过哈希函数直接对关键字进行映射访问的表。
  • 散列函数(哈希函数)​:将关键字映射到存储地址的函数,记为 H a s h M a p ( k e y ) = A d d r HashMap(key)=Addr HashMap(key)=Addr(地址可为数组下标、内存地址等)。
  • 冲突:不同关键字被映射到同一地址的现象(如 k e y 1 ≠ k e y 2 key_1 \neq key_2 key1=key2 H a s h M a p ( k e y 1 ) = H a s h M a p ( k e y 2 ) HashMap(key_1)=HashMap(key_2) HashMap(key1)=HashMap(key2))。
  • 理想时间复杂度:查找操作的时间复杂度为 O ( 1 ) O(1) O(1)(与数据量无关)。

查找及性能分析

平均查找长度(ASL)
  • 定义:衡量散列表查找效率的核心指标,需分别计算搜索成功和失败的情况。
  • 冲突的影响:即使通过散列函数建立了直接映射,冲突仍导致查找过程需进行关键字比较。
搜索成功的平均搜索长度
  • 定义:找到表中已有元素所需的平均探查次数。
  • 计算方式:所有已有元素探查次数的平均值(例如:若表中元素探查次数分别为 k 1 , k 2 , … , k n k_1, k_2, \dots, k_n k1,k2,,kn,则 A S L 成功 = 1 n ∑ i = 1 n k i ASL_{成功} = \frac{1}{n}\sum_{i=1}^n k_i ASL成功=n1i=1nki)。
搜索失败的平均搜索长度
  • 定义:在表中未找到目标元素时,定位到插入位置所需的平均探查次数。
  • 计算方式:所有可能散列地址上插入新元素时探查空桶次数的平均值(需覆盖散列函数所有可能结果)。
查找效率影响因素
  1. 散列函数设计:应尽量减少冲突(如均匀分布关键字)。
  2. 冲突处理方法:包括开放定址法、链地址法等。
  3. 装载因子:定义为 α = 表中元素数 散列表长度 \alpha = \frac{\text{表中元素数}}{\text{散列表长度}} α=散列表长度表中元素数 α \alpha α 越大冲突概率越高。

散列表特性

  • 关键字与存储地址的直接映射关系区别于线性表、树表(需逐次比较)。
  • 实际应用中需权衡 α \alpha α 值以平衡空间和时间效率。

散列函数的构造方法

  • 在构造散列函数时,必须注意以下几点:

    1. 定义域和值域:散列函数的定义域必须包含全部关键字,而值域的范围则依赖于散列表的大小。

    2. 均匀分布:散列函数计算出的地址应尽可能均匀地分布在整个地址空间,尽可能地减少冲突。

    3. 计算效率:散列函数应尽量简单,能在较短的时间内计算出任意一个关键字对应的散列地址。

  • 散列函数的目标:采用何种构造散列函数的方法取决于关键字集合的情况,但最终的目标都是尽量降低产生冲突的可能性。

直接定址法

  • 核心思想:直接取关键字的某个线性函数值为散列地址。
  • 散列函数
    H ( k e y ) = k e y H(key) = key H(key)=key H ( k e y ) = a × k e y + b H(key) = a \times key + b H(key)=a×key+b a a a b b b 为常数)
  • 特点
    • 计算简单且不会产生冲突
    • 若关键字分布不连续,会导致存储空间浪费。
  • 适用场景:关键字分布基本连续的情况(如顺序编号的身份证号、学号)。

除留余数法

  • 核心思想:通过取模运算将关键字映射到散列地址。
  • 散列函数
    H ( k e y ) = k e y m o d    p H(key) = key \mod p H(key)=keymodp p p p 为不大于散列表长度 m m m 且最接近 m m m质数
  • 特点
    • 最简单、最常用的冲突控制方法。
    • p p p 的选择直接影响冲突概率(需使映射结果均匀分布)。
  • 适用场景:表长可动态调整的开放定址法场景。

数字分析法

  • 核心思想:分析关键字各位分布规律,选取分布均匀的位作为散列地址。
  • 操作步骤
    1. 将关键字转换为 r r r 进制数(如十六进制)。
    2. 统计各数位上不同数码出现的频率。
    3. 选择分布最均匀的若干位组合成散列地址。
  • 特点:高度依赖具体关键字集合,更换关键字需重新设计函数。
  • 适用场景:已知关键字集合且分布规律可预先分析的场景(如固定格式的电话号码)。

平方取中法

  • 核心思想:取关键字平方值的中间几位作为散列地址。
  • 操作步骤
    1. 计算关键字的平方值(如 k e y 2 key^2 key2)。
    2. 根据散列表长度截取平方值的中间若干位(如取中间 4 位)。
  • 特点
    • 散列地址与关键字的每一位相关,分布更均匀。
    • 需根据实际需求调整截取位数。
  • 适用场景:关键字各位取值不均匀或位数不足(如短字符串、小范围整数)。

方法对比与选择原则

方法优势局限性典型应用场景
直接定址法无冲突、计算快空间利用率低连续分布的关键字
除留余数法灵活、适用性强依赖质数选择通用场景
数字分析法针对性强、效率高需预先分析关键字分布固定格式数据
平方取中法分布均匀、适应性强计算复杂度略高非均匀分布的关键字

处理冲突的方法

冲突的不可避免性

应该注意到,任何设计出来的散列函数都不可能绝对地避免冲突。为此,必须考虑在发生冲突时应该如何处理,即为产生冲突的关键字寻找下一个“空”的Hash地址。用 H i H_i Hi 表示处理冲突中第 i i i 次探测得到的散列地址,假设得到的另一个散列地址 H 1 H_1 H1 仍然发生冲突,只得继续求下一个地址 H 2 H_2 H2。以此类推,直到 H k H_k Hk 不发生冲突为止,则 H k H_k Hk 为关键字在表中的地址。

闭散列(开放定址法)

  • 闭散列,也称为开放定址法,是指表中可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。其数学递推公式为:
    H i = ( H ( k e y ) + d i ) % m H_i = (H(key) + d_i) \% m Hi=(H(key)+di)%m
    式中, H ( key ) H(\text{key}) H(key) 为散列函数; i = 1 , 2 , … , k i = 1, 2, \ldots, k i=1,2,,k k ≤ m − 1 k \leq m - 1 km1); m m m 表示散列表表长; d i d_i di 为增量序列。所有的数据都是在封闭空间内进行定位存储,不增加新的存储空间。
  • 取定某一增量序列后,对应的处理方法就是确定的。通常有以下 4 种取法:
线性探测法
概述

线性探测法,又称线性探测再散列法。 d i = 1 , 2 , … , m − 1 d_i = 1, 2, \ldots, m - 1 di=1,2,,m1。它的特点是:冲突发生时,顺序查看表中下一个单元(探测到表尾地址 m − 1 m-1 m1 时,下一个探测地址是表首地址 0),直到找出一个空闲单元(当表未填满时一定能找到一个空闲单元)或查遍全表。

特点

线性探测法可能使第 i i i 个散列地址的同义词存入第 i + 1 i+1 i+1 个散列地址,这样本应存入第 i + 1 i+1 i+1 个散列地址的元素就争夺第 i + 2 i+2 i+2 个散列地址的元素的地址……从而造成大量元素在相邻的散列地址上聚集(或堆积)起来,大大降低了查找效率。

示例分析

线性探测法是当hash一个关键字时,发现没有冲突,就保存关键字,如果出现冲突,就探测冲突地址下一个地址,依次按照线性查找,直到发现有空地址为止,从而解决冲突。

  • 关键词:37, 25, 14, 36, 49, 68, 57, 11
  • 散列表:HT[12],表的大小 m = 12 m = 12 m=12
  • 散列函数 Hash ( x ) = x % 11 \text{Hash}(x) = x \% 11 Hash(x)=x%11

在这里插入图片描述

  • 平均搜索长度(ASL)计算
    A S L s u c c = 1 8 ∑ i = 1 8 C i = 1 8 ( 1 + 1 + 3 + 4 + 3 + 1 + 7 + 1 ) = 21 8 ASL_{succ} = \frac{1}{8} \sum_{i=1}^{8} C_i = \frac{1}{8} (1 + 1 + 3 + 4 + 3 + 1 + 7 + 1) = \frac{21}{8} ASLsucc=81i=18Ci=81(1+1+3+4+3+1+7+1)=821
    A S L u n s u c c = 2 + 1 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 + 1 11 = 40 11 ASL_{unsucc} = \frac{2 + 1 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 + 1}{11} = \frac{40}{11} ASLunsucc=112+1+8+7+6+5+4+3+2+1+1=1140
平方探测法
概述

平方探测法,又称二次探测法。 d i = 1 2 , − 1 2 , 2 2 , − 2 2 , … , k 2 , − k 2 d_i = 1^2, -1^2, 2^2, -2^2, \ldots, k^2, -k^2 di=12,12,22,22,,k2,k2,其中 k ≤ m / 2 k \leq m/2 km/2,散列表长度 m m m 必须是一个可以表示成 4 k + 3 4k + 3 4k+3 的素数。

特点

平方探测法是一种处理冲突的较好方法,可以避免出现“堆积”问题,它的缺点是不能探测到散列表上的所有单元,但至少能探测到一半单元。

示例分析

二次探测法指采用前后跳跃式探测的方法,发生冲突时,向后 1 2 1^2 12 位探测,向前 1 2 1^2 12 位探测,向后 2 2 2^2 22 位探测,向前 2 2 2^2 22 位探测……以跳跃式探测,避免堆积。

  • 关键词:37, 25, 14, 36, 49, 68, 57, 11
  • 散列表:HT[19],表的大小 m = 19 m = 19 m=19
  • 散列函数 Hash ( x ) = x % 19 \text{Hash}(x) = x \% 19 Hash(x)=x%19

在这里插入图片描述

  • 平均搜索长度(ASL)计算
    A S L s u c c = 1 8 ( 1 + 1 + 1 + 1 + 1 + 2 + 1 + 3 ) = 11 8 ASL_{succ} = \frac{1}{8} (1 + 1 + 1 + 1 + 1 + 2 + 1 + 3) = \frac{11}{8} ASLsucc=81(1+1+1+1+1+2+1+3)=811
    A S L u n s u c c = 1 19 ( 2 + 1 + 1 + 1 + 1 + 1 + 2 + 1 + 1 + 1 + 3 + 4 + 2 + 1 + 2 + 1 + 1 + 3 + 4 ) = 33 19 ASL_{unsucc} = \frac{1}{19} (2 + 1 + 1 + 1 + 1 + 1 + 2 + 1 + 1 + 1 + 3 + 4 + 2 + 1 + 2 + 1 + 1 + 3 + 4) = \frac{33}{19} ASLunsucc=191(2+1+1+1+1+1+2+1+1+1+3+4+2+1+2+1+1+3+4)=1933
双散列法
概述

双散列法。 d i = i × Hash 2 ( key ) d_i = i \times \text{Hash}_2(\text{key}) di=i×Hash2(key)。需要使用两个散列函数,当通过第一个散列函数 H ( key ) H(\text{key}) H(key) 得到的地址发生冲突时,则利用第二个散列函数 Hash 2 ( key ) \text{Hash}_2(\text{key}) Hash2(key) 计算该关键字的地址增量。它的具体散列函数形式如下:
H i = ( H ( k e y ) + i × H a s h 2 ( k e y ) ) % m H_i = (H(key) + i \times Hash_2(key)) \% m Hi=(H(key)+i×Hash2(key))%m
初始探测位置 H 0 = H ( key ) % m H_0 = H(\text{key}) \% m H0=H(key)%m i i i 是冲突的次数,初始为 0。

示例分析

数据冲突时,解决冲突将使用第二个散列函数,因此得名双散列法。

  • 关键词:22, 41, 53, 46, 30, 13, 01, 67
  • 散列表:HT[11], m = 11 m = 11 m=11
  • 散列函数 Hash ( x ) = ( 3 x ) % 11 \text{Hash}(x) = (3x) \% 11 Hash(x)=(3x)%11
  • 再散列函数 ReHash ( x ) = ( 7 x ) % 10 + 1 \text{ReHash}(x) = (7x) \% 10 + 1 ReHash(x)=(7x)%10+1

在这里插入图片描述

伪随机序列法
概述

伪随机序列法。 d i = d_i = di= 伪随机数序列。

开放定址法对比
方法核心机制优点缺点
线性探测法顺序线性探测实现简单同义词堆积严重
平方探测法平方偏移量跳跃探测减少堆积探测范围受限(50%空间)
双散列法双散列函数协同定位冲突概率低函数设计复杂度高
伪随机序列法伪随机数引导探测路径分布均匀性佳随机数质量依赖性强
注意事项

采用开放定址法时,不能随便物理删除表中已有元素,否则会截断其他同义词元素的查找路径。因此,要删除一个元素时,可以做一个删除标记,进行逻辑删除。但这样做的副作用是:执行多次删除后,表面上看起来散列表很满,实际上有许多位置未利用。

拉链法(链接法,Chaining)

开散列,也称为拉链法,是一种处理散列冲突的方法。当不同的关键字通过散列函数映射到同一地址时,为了避免非同义词发生冲突,可以将所有的同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识。

特点
  • 散列地址为 i i i 的同义词链表的头指针存放在散列表的第 i i i 个单元中。
  • 查找、插入和删除操作主要在同义词链中进行。
  • 拉链法适用于经常进行插入和删除的情况。
示例分析

拉链法又叫链地址法,拉链法就是把具有相同散列地址的关键字(同义词)值放在同一个单链表中,简单来说拉链法就是数组加链表的组合结构。

  • 关键词:37, 25, 14, 36, 49, 68, 57, 11
  • 散列表:HT[11],表的大小 m = 11 m = 11 m=11
  • 散列函数 Hash ( x ) = x % 11 \text{Hash}(x) = x \% 11 Hash(x)=x%11

在这里插入图片描述

  • 平均搜索长度(ASL)计算
    A S L s u c c = 1 8 ( 1 + 1 + 2 + 1 + 2 + 3 + 1 + 1 ) = 12 8 = 1.5 ASL_{succ} = \frac{1}{8}(1+1+2+1+2+3+1+1) = \frac{12}{8} = 1.5 ASLsucc=81(1+1+2+1+2+3+1+1)=812=1.5
    A S L u n s u c c = 1 11 ( 2 + 1 + 3 + 4 + 2 + 2 + 1 + 1 + 1 + 1 + 1 ) = 19 11 ≈ 1.73 ASL_{unsucc} = \frac{1}{11}(2+1+3+4+2+2+1+1+1+1+1) = \frac{19}{11} \approx 1.73 ASLunsucc=111(2+1+3+4+2+2+1+1+1+1+1)=11191.73

散列查找及性能分析的应用

线性探测法查找过程

  • 初始化:计算地址 A d d r = Hash ( key ) Addr = \text{Hash}(\text{key}) Addr=Hash(key)
  • 查找步骤
    1. 检测查找表中地址为 A d d r Addr Addr 的位置上是否有记录,若无记录,返回查找失败;若有记录,比较它与 key \text{key} key 的值,若相等,则返回查找成功标志,否则执行步骤 2。
    2. 用给定的处理冲突方法计算“下一个散列地址”,并把 A d d r Addr Addr 置为此地址,转入步骤 1。
示例

例如,关键字序列 {19, 14, 23, 01, 68, 20, 84, 27, 55, 11, 10, 79} 按散列函数 H(key)=key%13 和线性探测处理冲突构造所得的散列表 L 如下图所示。
在这里插入图片描述

  • 给定值 84 的查找过程为:首先求得散列地址 H ( 84 ) = 6 H(84) = 6 H(84)=6,因 L [ 6 ] L[6] L[6] 不空且 L [ 6 ] ≠ 84 L[6] \neq 84 L[6]=84,则找第一次冲突处理后的地址 H 1 = ( 6 + 1 ) % 16 = 7 H_1 = (6 + 1) \% 16 = 7 H1=(6+1)%16=7,而 L [ 7 ] L[7] L[7] 不空且 L [ 7 ] ≠ 84 L[7] \neq 84 L[7]=84,则找第二次冲突处理后的地址 H 2 = ( 6 + 2 ) % 16 = 8 H_2 = (6 + 2) \% 16 = 8 H2=(6+2)%16=8 L [ 8 ] L[8] L[8] 不空且 L [ 8 ] = 84 L[8] = 84 L[8]=84,查找成功,返回记录在表中的序号 8。
  • 给定值 38 的查找过程为:首先求得散列地址 H ( 38 ) = 12 H(38) = 12 H(38)=12 L [ 12 ] L[12] L[12] 不空且 L [ 12 ] ≠ 38 L[12] \neq 38 L[12]=38,则找下一地址 H 1 = ( 12 + 1 ) % 16 = 13 H_1 = (12 + 1) \% 16 = 13 H1=(12+1)%16=13,由于 L [ 13 ] L[13] L[13] 是空记录,所以表中不存在关键字为 38 的记录。
  • 查找各关键字的比较次数如下表所示:
关键字140168275519208479231110
比较次数121431139113
  • 平均查找长度(ASL)
    A S L s u c c = 1 n ∑ i = 1 n C i = 1 × 6 + 2 + 3 × 3 + 4 + 9 12 = 30 12 = 2.5 ASL_{succ} = \frac{1}{n}\sum_{i=1}^n C_i= \frac{1×6 + 2 + 3×3 + 4 + 9}{12} = \frac{30}{12} = 2.5 ASLsucc=n1i=1nCi=121×6+2+3×3+4+9=1230=2.5
    C i C_i Ci为第 i i i个关键字的比较次数)
    • 分子30:总比较次数(6个关键字1次,1个关键字2次,3个关键字3次等)
    • 分母12:关键字总数

散列表的冲突处理与查找效率

不同冲突处理方法的影响

对同一组关键字,若设定相同的散列函数,但采用不同的处理冲突方法(如拉链法、开放寻址法等),生成的散列表结构会不同,其平均查找长度(ASL)也会不同。例如,拉链法与线性探测法的平均查找长度存在差异。

散列表查找效率的影响因素

散列表的查找效率主要由以下三个因素决定:

  1. 散列函数:直接影响关键字的分布均匀性。
  2. 处理冲突的方法:不同方法(如拉链法、二次探测法)对冲突的解决策略不同,影响查找路径长度。
  3. 装填因子:定义为表中记录数 n n n 与散列表长度 m m m 的比值:
    α = n m \alpha = \frac{n}{m} α=mn
    平均查找长度依赖于 α \alpha α,而非直接依赖于 n n n m m m α \alpha α 越大,表越“满”,冲突概率越高,查找效率越低;反之冲突概率越低。
查找过程的本质

尽管散列表通过关键字与存储位置的直接映射加速查找,但冲突的存在使得查找过程仍需要比较操作。因此,平均查找长度是衡量散列表查找效率的核心指标。

散列表的代码实操

哈希表

结点的定义
  • 定义一个链地址法的哈希表结点,包含数据域和指向下一个结点的指针。
typedef struct HashNode {
    ElemType data;               // 数据域
    struct HashNode *next;       // 指针域
} HashNode;
初始化哈希表
  • 初始化哈希表,将每个桶置为空。
void initHash(hashTable h) {
    for (int i = 0; i < P; ++i)
        h[i] = NULL;
}
除留余数法
  • 使用除留余数法计算哈希地址。
int Hash(ElemType key) {
    return key % P;
}
插入数据
  • 计算哈希地址,创建新结点,并插入到对应桶的链表头。
void insertHash(hashTable h, ElemType x) {
    int idx = Hash(x);
    HashNode *s = (HashNode*)malloc(sizeof(HashNode));
    s->data = x;
    s->next = h[idx];
    h[idx] = s;
}
查找数据
  • 计算哈希地址,遍历链表查找关键字。
HashNode* findHash(hashTable h, ElemType key) {
    int idx = Hash(key);
    HashNode *p = h[idx];
    while (p != NULL && p->data != key)
        p = p->next;
    return p;
}
删除数据
  • 查找关键字,找到后从链表中删除并释放内存。
void deleteHash(hashTable h, ElemType key) {
    HashNode *p = findHash(h, key);
    if (p == NULL)
        return; // 删除失败

    int idx = Hash(key);
    HashNode *pre = h[idx];
    if (pre == p) // 第一个结点
        h[idx] = p->next;
    else {
        while (pre->next != p)
            pre = pre->next;
        pre->next = p->next;
    }
    free(p);
}
打印哈希表
  • 遍历哈希表,打印每个桶中的元素。
void printHash(hashTable h) {
    for (int i = 0; i < P; ++i) {
        printf("%d : ", i);
        HashNode *p = h[i];
        while (p != NULL) {
            printf("%d->", p->data);
            p = p->next;
        }
        printf("Nil.\n");
    }
}

桶结点

结点的定义
  • 定义一个桶结点,包含三个数据域和一个指向下一个桶的指针。
typedef struct bucket_node {
    int data[3];
    struct bucket_node *next;
} bucket_node;
初始化
  • 初始化桶,将每个桶的三个数据域置为 NULL_DATA,指针域置为空。
void init_bucket_node() {
    for (int i = 0; i < P; ++i) {
        for (int j = 0; j < 3; ++j)
            hash_table[i].data[j] = NULL_DATA;
        hash_table[i].next = NULL;
    }
}
插入数据
  • 计算桶地址,尝试在桶内插入数据,若桶满则创建新桶并链接。
int insert_new_element(int new_element) {
    int idx = new_element % P;
    for (int i = 0; i < 3; ++i) {
        if (hash_table[idx].data[i] == NULL_DATA) {
            hash_table[idx].data[i] = new_element;
            return 0; // 插入成功
        }
    }

    bucket_node *p = &hash_table[idx];
    while (p->next != NULL) {
        p = p->next;
        for (int i = 0; i < 3; ++i) {
            if (p->data[i] == NULL_DATA) {
                p->data[i] = new_element;
                return 0; // 插入成功
            }
        }
    }

    // 创建溢出桶
    bucket_node *s = (bucket_node*)malloc(sizeof(bucket_node));
    for (int i = 0; i < 3; ++i)
        s->data[i] = NULL_DATA;
    s->next = NULL;

    // 链接结点
    p->next = s;
    s->data[0] = new_element;
    return 0;
}

六、参考资料

鲍鱼科技课件

b站免费王道课后题讲解:
在这里插入图片描述

网课全程班:
在这里插入图片描述

26王道考研书

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

408答疑+v:18675660929

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值