导读
大家好,很高兴又和大家见面啦!!!
在前面的学习中,我们掌握了两种基础的查找算法:顺序查找和折半查找。
-
顺序查找 简单直接,对数据的有序性没有要求,但效率较低,平均时间复杂度为 O(n),当数据量巨大时,性能瓶颈明显。
-
折半查找 效率很高,时间复杂度为 O(log n),但前提是数据必须有序,并且对于频繁进行插入、删除操作的动态数据集,维护有序性的成本很高。
那么,是否存在一种折中的方案,既能适应动态变化,又能获得比顺序查找更好的性能呢?
答案是肯定的,这就是本篇博客将要深入探讨的 分块查找(或称索引顺序查找)。它巧妙地吸取了顺序查找和折半查找各自的优点,其核心思想可以概括为 “块内无序,块间有序”。
这篇博客将带你彻底搞懂分块查找:
- 核心思想剖析:通过生动的实例和图解,详细讲解如何对数据集进行分块,以及如何构建索引表。
- 两种实现方式对比:深入探讨 逻辑分块 和 物理分块 两种实现策略,清晰列出它们各自的优缺点和适用场景,帮助你根据实际需求做出选择。
- 查找过程全演示:一步步追踪查找关键字的具体过程,包括在索引表中如何使用顺序查找或折半查找,以及在块内如何进行顺序查找。
- 性能定量分析:博客不仅定性地说明其效率,还提供了严谨的平均查找长度(ASL) 公式推导,并论证了当每块元素数为 √n 时,性能达到最优。
如果你对如何在动态数据集中实现高效查找感兴趣,或者想深入理解如何将简单算法组合以解决复杂问题,那么这篇关于分块查找的详细解析将是你的绝佳学习资料。
一、基本思想
分块查找也称索引顺序查找,它吸取了顺序查找和折半查找各自的优点,即有动态结构,有适合快速查找。
其基本思想为:
- 将查找表分为若干个子块
- 各自块内部元素可以无序,但是子块与子块之间必须有序
- 建立一个索引表,索引表中的每个元素包含各块的最大关键字与各块中的第一个元素地址
- 索引表按关键字有序排列
下面我们还是通过实例来理解其基本思想:
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)
接下来我们可以根据该分块从小到大的顺序创建一个索引表
之后,我们只需要将各元素依次分配到各个分块中即可,这里有两种方式:
- 逻辑分配:我们通过在各分块中记录各元素的数组下标,完成逻辑上的分配
- 物理分配:我们通过对原数组进行物理重排,将各元素分配到各自的分块中
两种方式各自的优缺点分别为:
- 逻辑分块:
- 优点:
- 原数组保持完全不变,仅需额外存储索引信息。
- 适合动态数据(插入/删除时只需更新索引表)。
- 缺点:
- 需要额外空间存储索引(如块指针、范围等)。
- 优点:
- 物理分块:
- 优点:
- 无需额外索引结构,内存效率高。
- 缺点:
- 原数组顺序被破坏,可能影响其他依赖原始顺序的操作。
- 插入/删除元素需重新分块,效率较低。
- 优点:
下面我们分别来说明一下这两种分配方式的具体过程:
1.1.1 逻辑分配
在逻辑分配中,我们需要完成两件事:
- 找到各分块中的元素最大值
- 记录各分块中的各元素下标
对于数组:{88, 24, 72, 61, 21, 6, 32, 11, 8, 31, 22, 83, 78, 54}
各元素下标分别为:
接下来,我们可以通过对数组进行遍历,并将各元素及其下标记录到各分块中:
按照上图的分配,对应的索引表需要更改为:最大值 + 元素下标的形式
现在我们就可以在不改变原有数组的情况下,通过索引表来查找各个元素了;
1.1.2 物理分配
在物理分配中,我们需要完成三件事:
- 找到各分块中的元素最大值
- 将各分块中的各元素动态分配到其对应分块中
- 记录各分块首元素的地址
与逻辑分配相同的是,我们同样需要通过数组遍历来查找各分块的元素,查找的结果前面有介绍,这里我就不再赘述。
接下来我们就来看一下完成物理分配后得到的新数组:
按照上图的分配,对应的索引表需要更改为:最大值 + 分块起始元素下标的形式:
可以看到,不管是逻辑分配,还是物理分配,每一个分块内的元素均为无序:
- 逻辑分配中,各分块中的元素下标所对应的元素之间无序排列
- 物理分配中,各分块中的元素之间无序排列
但是分块之间有序:
- 逻辑分配中,其分块索引表中的分块索引按升序排列
- 物理分配中,其分块索引表中的分块索引按升序排列
二、查找过程
在分块查找中,分为两部分:
- 查找分块索引表
- 由于索引表是有序排列,因此,其查找过程既可以采用顺序查找也可以采用折半查找;
- 查找索引值对应分块
- 由于分块内的元素无序排列,因此,其查找过程只能采用顺序查找
下面我们以物理分配为例来说明查找过程:
上图所示数组对应的分块索引表为:
下面我们分别以顺序查找和折半查找为例,来说明查找元素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中:
通过顺序查找,经过 3 次关键字比较后,我们并不能在分区中找到该元素,这说明原数组中并不存在该元素,因此本次查找失败;
从索引表中我们可以确定元素 21 21 21 位于分区1中:
通过顺序查找,经过 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
结语
到这里,我们今天关于分块查找的深入探讨就要告一段落了。让我们一起来回顾一下本篇博客的核心要点:
- 核心思想:我们深入理解了分块查找“块内无序、块间有序”的核心设计思想。它作为一种折中方案,巧妙地结合了顺序查找和折半查找的优点,非常适合处理动态变化的数据集。
- 两种实现方式:我们详细对比了逻辑分块和物理分块两种实现策略。逻辑分块保持了原数据顺序,适合频繁增删的场景;而物理分块则提升了内存访问效率,但会改变原始数据。理解它们的优劣有助于我们在实际应用中做出正确选择。
- 完整查找过程:我们通过具体的例子,一步步演示了在索引表(可使用顺序或折半查找)和块内(使用顺序查找)的完整查找流程,让抽象的算法变得清晰直观。
- 性能定量分析:我们不仅定性讨论了效率,还通过推导平均查找长度(ASL) 的公式,从数学上证明了当每块包含 √n 个元素时,分块查找的性能达到最优。
希望这篇内容详实的博客能帮助你彻底掌握分块查找算法,并将其加入你的算法工具箱。
如果觉得本篇博客对你有帮助,欢迎点赞、收藏支持! 你的肯定是我持续创作的最大动力。
对于分块查找的实现细节或应用场景,欢迎在评论区留言交流,我们一起探讨。也非常欢迎你将本文转发给更多正在学习数据结构与算法的朋友,知识因分享而更有价值。
最后,感谢各位朋友的耐心阅读与支持!在下一篇内容中,我们将一起探索另一种重要的查找算法,大家记得关注哦!咱们下一篇再见!