分块查找
- 导读
- 一、基本思想
-
- [1.1 分块过程](#1.1 分块过程)
-
- [1.1.1 逻辑分配](#1.1.1 逻辑分配)
- [1.1.2 物理分配](#1.1.2 物理分配)
- 二、查找过程
-
- [2.1 索引表顺序查找](#2.1 索引表顺序查找)
- [2.2 索引表折半查找](#2.2 索引表折半查找)
- [2.3 分块顺序查找](#2.3 分块顺序查找)
- 三、平均查找长度
- 结语

导读
大家好,很高兴又和大家见面啦!!!
在前面的学习中,我们系统学习了两种基础而重要的查找算法:
- 顺序查找作为最直观的查找方式,虽然实现简单但对数据无任何要求,其O(n)的时间复杂度在数据量大时效率较低。
- 折半查找则通过"二分法"思想将查找效率提升至O(log₂n),但要求数据必须有序且通常需要顺序存储。
在实际应用中,我们常常面临这样的困境:数据动态变化导致难以维持有序性,但又希望获得较高的查找效率。正是为了解决这一矛盾,分块查找应运而生!
本篇博客将深入探讨分块查找算法,它巧妙地将顺序查找和折半查找的优点相结合:
- 继承顺序查找的优点:对数据有序性要求低,块内元素可无序排列
- 吸收折半查找的优点:通过索引表的有序性实现快速定位
- 创新性地引入分块思想:通过"块内无序、块间有序"的设计平衡了动态性与效率
文章将从分块查找的基本思想入手,详细讲解两种分块方式(逻辑分配与物理分配)的实现细节,通过具体实例演示查找过程,并深入分析算法的平均查找长度。无论你是正在学习数据结构的学生,还是希望优化实际项目中查找效率的开发者,这篇文章都将为你提供宝贵的 insights。
让我们一起探索这种兼具灵活性与效率的查找算法吧!
一、基本思想
分块查找 也称索引顺序查找,它吸取了顺序查找和折半查找各自的优点,即有动态结构,有适合快速查找。
其基本思想为:
- 将查找表分为若干个子块
- 各自块内部元素可以无序,但是子块与子块之间必须有序
- 建立一个索引表,索引表中的每个元素包含各块的最大关键字与各块中的第一个元素地址
- 索引表按关键字有序排列
下面我们还是通过实例来理解其基本思想:
1.1 分块过程
例如在关键码集合:{88, 24, 72, 61, 21, 6, 32, 11, 8, 31, 22, 83, 78, 54}
中,我们按照某种规则对该集合进行分块,如以跨度 30 30 30 为单位进行分块,我们就可以将该集合分为三块:
- 第一块: ( 0 , 29 ) (0, 29) (0,29)
- 第二块: ( 30 , 59 ) (30, 59) (30,59)
- 第三块: ( 60 , 89 ) (60, 89) (60,89)
接下来我们可以根据该分块从小到大的顺序创建一个索引表
0, 29 30, 59 60, 89
之后,我们只需要将各元素依次分配到各个分块中即可,这里有两种方式:
- 逻辑分配:我们通过在各分块中记录各元素的数组下标,完成逻辑上的分配
- 物理分配:我们通过对原数组进行物理重排,将各元素分配到各自的分块中
两种方式各自的优缺点分别为:
- 逻辑分块:
- 优点:
- 原数组保持完全不变,仅需额外存储索引信息。
- 适合动态数据(插入/删除时只需更新索引表)。
- 缺点:
- 需要额外空间存储索引(如块指针、范围等)。
- 优点:
- 物理分块:
- 优点:
- 无需额外索引结构,内存效率高。
- 缺点:
- 原数组顺序被破坏,可能影响其他依赖原始顺序的操作。
- 插入/删除元素需重新分块,效率较低。
- 优点:
下面我们分别来说明一下这两种分配方式的具体过程:
1.1.1 逻辑分配
在逻辑分配中,我们需要完成两件事:
- 找到各分块中的元素最大值
- 记录各分块中的各元素下标
对于数组:{88, 24, 72, 61, 21, 6, 32, 11, 8, 31, 22, 83, 78, 54}
各元素下标分别为:
88
0 24
1 72
2 61
3 21
4 6
5 32
6 11
7 8
8 31
9 22
10 83
11 78
12 54
13
接下来,我们可以通过对数组进行遍历,并将各元素及其下标记录到各分块中:
0, 29 30, 59 60, 89 88
0
max 24
1
max 72
2 61
3 21
4 6
5 32
6 11
7 8
8 31
9 22
10 83
11 78
12 54
13
max
按照上图的分配,对应的索引表需要更改为:最大值 + 元素下标的形式
24
1, 4, 5, 7, 8, 10 54
6, 8, 13 88
0, 2, 3, 11, 12
现在我们就可以在不改变原有数组的情况下,通过索引表来查找各个元素了;
1.1.2 物理分配
在物理分配中,我们需要完成三件事:
- 找到各分块中的元素最大值
- 将各分块中的各元素动态分配到其对应分块中
- 记录各分块首元素的地址
与逻辑分配相同的是,我们同样需要通过数组遍历来查找各分块的元素,查找的结果前面有介绍,这里我就不再赘述。
接下来我们就来看一下完成物理分配后得到的新数组:
24
0
max
begin 21
1 6
2 11
3 8
4 22
5 32
6
begin 31
7 54
8
max 88
9
max
begin 72
10 61
11 83
12 78
13
按照上图的分配,对应的索引表需要更改为:最大值 + 分块起始元素下标的形式:
24
0 54
6 88
9
可以看到,不管是逻辑分配,还是物理分配,每一个分块内的元素均为无序:
- 逻辑分配中,各分块中的元素下标所对应的元素之间无序排列
- 物理分配中,各分块中的元素之间无序排列
但是分块之间有序:
- 逻辑分配中,其分块索引表中的分块索引按升序排列
- 物理分配中,其分块索引表中的分块索引按升序排列
二、查找过程
在分块查找中,分为两部分:
- 查找分块索引表
- 由于索引表是有序排列,因此,其查找过程既可以采用顺序查找 也可以采用折半查找;
- 查找索引值对应分块
- 由于分块内的元素无序排列,因此,其查找过程只能采用顺序查找
下面我们以物理分配为例来说明查找过程:
24
0
max
begin 21
1 6
2 11
3 8
4 22
5 32
6
begin 31
7 54
8
max 88
9
max
begin 72
10 61
11 83
12 78
13
上图所示数组对应的分块索引表为:
24
0 54
6 88
9
下面我们分别以顺序查找和折半查找为例,来说明查找元素33与21的整个过程;
2.1 索引表顺序查找
在索引表中,我们按照从左到右的顺序完成顺序查找:
- 查找元素33:
- 第一次关键字比较: 33 > 24 33 > 24 33>24 ,元素不在该分区内
- 第二次关键字比较: 33 < 54 33 < 54 33<54 ,元素位于该分区内
- 查找元素21:
- 第一次关键字比较: 21 < 24 21 < 24 21<24 ,元素位于该分区内
2.2 索引表折半查找
在索引表中,我们通过指针 low
指向索引表最左侧下标,即 low = 0
,指针 high
指向索引表最右侧下标,即 high = 2
,之后完成折半查找:
- 查找元素33
- 指针
mid = (high - low) / 2 + low = (2 - 0) / 2 + 0 = 1
- 第一次关键字比较: 54 > 33 54 > 33 54>33 说明目标值位于小值区,我们需要更改指针
high
的右边界指向 - 更改指针
high
:high = mid - 1 = 1 - 1 = 0
- 指针
mid = (higih - low) / 2 + low = (0 - 0) / 2 + 0 = 0
- 第二次关键字比较: 24 < 33 24 < 33 24<33 说明目标值位于大值区,我们需要更改指针
low
的左边界指向 - 更改指针
low
:low = mid + 1 = 0 + 1 = 1
- 由于
low > high
,说明此时元素 33 33 33 位于分区2 中
- 指针
- 查找元素21
- 指针
mid = (high - low) / 2 + low = (2 - 0) / 2 + 0 = 1
- 第一次关键字比较: 54 > 21 54 > 21 54>21 说明目标值位于小值区,我们需要更改指针
high
的右边界指向 - 更改指针
high
:high = mid - 1 = 1 - 1 = 0
- 指针
mid = (higih - low) / 2 + low = (0 - 0) / 2 + 0 = 0
- 第二次关键字比较: 24 > 21 24 > 21 24>21 说明目标值位于小值区,我们需要更改指针
high
的右边界指向 - 更改指针
high
:high = mid - 1 = 0 - 1 = -1
- 由于
low > high
,说明此时元素 21 21 21 位于分区1 中
- 指针
在确定了分区后,接下来我们需要对确定好的分区进行顺序查找!!!
2.3 分块顺序查找
从索引表中我们可以确定元素 33 33 33 位于分区2中:
32 31 54
通过顺序查找,经过 3 次关键字比较后,我们并不能在分区中找到该元素,这说明原数组中并不存在该元素,因此本次查找失败;
从索引表中我们可以确定元素 21 21 21 位于分区1中:
24 21 6 11 8 22
通过顺序查找,经过 2 次关键字比较,我们便找到了该元素,这时需要返回该元素的存储位置;
三、平均查找长度
在分块查找中,其平均查找长度由索引表和分区共同组成,即:
A S L = L l + L s ASL = L_l + L_s ASL=Ll+Ls
其中: L l L_l Ll 表示索引表内的查找长度, L s L_s Ls 为分区块内的查找长度
若有一个长度为 n n n 的查找表,我们将其均匀地分为 b b b 块,每块中有 s s s 个记录,在等概率情况下,其平均查找长度为:
- 索引表与块内均采用顺序查找:
A S L 成功 = L l + L s = b + 1 2 + s + 1 2 = b + s + 2 2 = n s + s + 2 2 = s 2 + 2 ∗ s + n 2 ∗ s A S L 失败 1 = L l + L s = b + 1 2 + s = n s + 1 2 + s = 2 ∗ s 2 + s + n 2 ∗ s A S L 失败 2 = L l = b + 1 2 = n s + 1 2 = n + s 2 ∗ s \begin{align*} ASL_{成功} &= L_l + L_s \\ &= \frac{b + 1}{2} + \frac{s + 1}{2} \\ &= \frac{b + s + 2}{2} \\ &= \frac{\frac{n}{s} + s + 2}{2} \\ &= \frac{s^2 + 2 * s + n}{2 * s} \\ ASL_{失败1} &= L_l + L_s \\ &= \frac{b + 1}{2} + s \\ &= \frac{\frac{n}{s} + 1}{2} + s \\ &= \frac{2 * s ^ 2 + s + n}{2*s} \\ ASL_{失败2} &= L_l \\ &= \frac{b + 1}{2} \\ &= \frac{\frac{n}{s} + 1}{2} \\ &= \frac{n + s}{2 * s} \end{align*} ASL成功ASL失败1ASL失败2=Ll+Ls=2b+1+2s+1=2b+s+2=2sn+s+2=2∗ss2+2∗s+n=Ll+Ls=2b+1+s=2sn+1+s=2∗s2∗s2+s+n=Ll=2b+1=2sn+1=2∗sn+s
注:这里计算的失败1指的是索引表查找成功,分块查找失败;失败2指的是索引表查找失败
- 索引表采用折半查找,块内采用顺序查找,假设索引表对应的判定树为一棵满二叉树:
A S L 成功 = L l + L s = ∑ i = 1 b P i C i + ∑ j = 1 s P j C j = 1 ∗ 1 ∗ 1 b + 2 ∗ 2 ∗ 1 b + ⋯ + 2 h − 1 ∗ h ∗ 1 b + s + 1 2 = 1 ∗ 1 + 2 ∗ 2 + ⋯ + 2 h − 1 ∗ h b + s + 1 2 = ( h − 1 ) ∗ 2 h + 1 b + s + 1 2 = ( log 2 ( b + 1 ) − 1 ) ∗ 2 log 2 ( b + 1 ) + 1 b + s + 1 2 = ( b + 1 ) ∗ log 2 ( b + 1 ) − b − 1 + 1 b + s + 1 2 = ( n s + 1 ) ∗ log 2 ( n s + 1 ) − n s n s + s + 1 2 = ( n + s ) ∗ log 2 ( n + s s ) − n s n s + s + 1 2 = ( n + s ) ∗ log 2 ( n + s s ) − n n + s + 1 2 A S L 失败 = L l + L s = ∑ i = 1 b + 1 P i C i + s = 1 b + 1 ∗ ( h + 1 ) + 1 b + 1 ∗ ( h + 1 ) + ⋯ + 1 b + 1 ∗ ( h + 1 ) + s = h + 1 + s = log 2 ( b + 1 ) + 1 + s = log 2 ( n s + 1 ) + 1 + s \begin{align*} ASL_{成功} &= L_l + L_s \\ &= \sum\limits^b_{i = 1}P_iC_i + \sum\limits^s_{j = 1}P_jC_j \\ &= 1 * 1 * \frac{1}{b} + 2 * 2 * \frac{1}{b} + \cdots + 2^{h - 1} * h * \frac{1}{b} + \frac{s + 1}{2} \\ &= \frac{1 * 1 + 2 * 2 + \cdots + 2 ^{h - 1} * h}{b} + \frac{s + 1}{2}\\ &= \frac{(h - 1) * 2 ^ h + 1}{b} + \frac{s + 1}{2} \\ &= \frac{(\log_{2}{(b + 1)} - 1) * 2 ^ {\log_{2}{(b + 1)}}+1}{b} + \frac{s + 1}{2} \\ &= \frac{(b + 1) * \log_{2}{(b + 1)} - b - 1 + 1}{b} + \frac{s + 1}{2} \\ &= \frac{(\frac{n}{s} + 1) * \log_{2}{(\frac{n}{s} + 1)} - \frac{n}{s}}{\frac{n}{s}} + \frac{s + 1}{2} \\ &= \frac{\frac{(n + s)* \log_{2}{(\frac{n + s}{s})} - n}{s}}{\frac{n}{s}} + \frac{s + 1}{2} \\ &= \frac{(n + s)* \log_{2}{(\frac{n + s}{s})} - n}{n} + \frac{s + 1}{2} \\ ASL_{失败} &= L_l + L_s \\ &= \sum\limits^{b+1}{i = 1}P_iC_i + s \\ &= \frac{1}{b + 1} * (h + 1) + \frac{1}{b + 1} * (h + 1) + \cdots + \frac{1}{b + 1} * (h + 1) + s \\ &= h + 1 + s \\ &= \log{2}{(b + 1)} + 1 + s \\ &= \log_{2}{(\frac{n}{s} + 1)} + 1 + s \end{align*} ASL成功ASL失败=Ll+Ls=i=1∑bPiCi+j=1∑sPjCj=1∗1∗b1+2∗2∗b1+⋯+2h−1∗h∗b1+2s+1=b1∗1+2∗2+⋯+2h−1∗h+2s+1=b(h−1)∗2h+1+2s+1=b(log2(b+1)−1)∗2log2(b+1)+1+2s+1=b(b+1)∗log2(b+1)−b−1+1+2s+1=sn(sn+1)∗log2(sn+1)−sn+2s+1=sns(n+s)∗log2(sn+s)−n+2s+1=n(n+s)∗log2(sn+s)−n+2s+1=Ll+Ls=i=1∑b+1PiCi+s=b+11∗(h+1)+b+11∗(h+1)+⋯+b+11∗(h+1)+s=h+1+s=log2(b+1)+1+s=log2(sn+1)+1+s
现在我们以顺序查找成功为例,其平均比较长度为:
A S L 成功 = s 2 + 2 ∗ s + n 2 ∗ s ASL_{成功} = \frac{s^2 + 2 * s + n}{2 * s} ASL成功=2∗ss2+2∗s+n
通过对该函数求导可得,当 s = n s = \sqrt{n} s=n 时,其平均查找长度取最小值: n + 1 \sqrt{n} + 1 n +1
在分块查找中,虽然索引表占用了额外的存储空间,索引查找也增加了一定的系统开销,但由于其分块结构,使得在块内查找时的范围较小,与顺序查找相比,分块查找的总体效率提升了不少。
从上述的平均查找长度分析来看,分块查找的整体平均查找长度分析还是比较复杂的。
这主要体现在当我们对索引表采用折半查找时,它并不能像我们直接对有序查找表使用折半查找一样,能够直接确定查找表中是否存在该元素。
我们只能够通过索引表确定查找值的所在分区,而具体的查找结果还需要通过对分块内的元素进行更进一步的顺序查找。
结语
通过本文的学习,相信大家对分块查找这一高效的查找算法有了全面的理解。让我们回顾一下本文的核心要点:
📚 核心知识点回顾
分块查找的精髓在于巧妙结合了顺序查找和折半查找的优点:
-
"块内无序、块间有序"的设计理念
-
通过索引表实现快速定位,通过块内顺序查找保证灵活性
-
两种实现方式:逻辑分配(保持原数组)和物理分配(重排数组)
关键收获:
-
当块大小s=√n时,平均查找长度达到最优值√n+1
-
索引表可选用顺序查找或折半查找,适应不同场景需求
-
在动态数据环境下,分块查找展现出独特的优势
💡 实际应用价值
分块查找不仅在理论学习中具有重要意义,在实际开发中也有着广泛应用:
-
数据库索引的底层原理
-
文件系统的目录结构设计
-
大规模数据的分片处理
-
实时系统中的快速检索
🤝 期待与您互动
如果本文对您有帮助,欢迎:
-
点赞 👍 - 您的认可是我持续创作的最大动力
-
收藏 ⭐ - 方便日后随时查阅复习
-
关注 ➕ - 获取更多数据结构与算法的精彩内容
-
转发 🔄 - 分享给更多需要的小伙伴
-
评论 💬 - 留下您的宝贵意见和学习心得
您还希望了解哪些查找算法或数据结构的深度解析?在评论区告诉我,我会根据大家的需求安排后续的创作计划!
学习之路,你我同行。 让我们在算法的世界里继续探索,下期再见!✨