C++ 程序实现插值搜索算法

2025年2月8日 | 17 分钟阅读

插值查找 是一种算法,用于在已排序的数组中有效地搜索目标值。与始终检查搜索区间中位元素的二分查找不同,插值查找根据区间端点的值更明智地估计目标的位置。当数组中的值分布不均匀时,此方法非常有效。

插值查找的基本原理是通过数学方程预测目标在数组中可能的位置。该公式考虑了搜索区间端点的值,并假设数组索引与相应值之间存在线性关系。其目的是比总是选择中间元素的二分查找更快地缩小搜索空间,特别是当值的分布不对称时。

然后,算法检查估计的位置是否是键的实际位置。如果键在此位置找到,则搜索成功。否则,算法通过比较键与估计位置处的值来更新搜索区间,方法是更改低或高端点。

插值查找的基本思想是利用目标元素的值以及数组中值的范围来查找其近似位置

此处,

  • low 和 high 是表征当前搜索区间的指示符。
  • arr[low] 和 arr[high] 是这些索引处的值。
  • key 是要搜索的元素。
  • pos 是键在数组中的近似位置。

其思想是,如果数组元素分布均匀,则此公式可以更准确地估计键的位置。但是,如果数组分布不均匀,插值查找在最坏的情况下可能退化为二分查找。

算法然后比较估计位置 (arr[pos]) 和键。

如果 arr[pos] = key, 则元素位于位置 pos。

如果 arr[pos] > key, 则键可能在左半部分,因此搜索区间更新为前半部分 (high = pos-1)。

否则,如果 arr[pos] < key, 则通过将搜索区间更新为右半部分 (low = pos+1) 来将搜索转移到右半部分。

此搜索将一直进行,直到找到键或搜索区间变为空。当数组中的几乎所有元素都均匀分布时,插值搜索的效率会提高。

插值查找 的关键特性是它根据数组中值的排列方式改变其行为。当值等距分布时,插值公式暗示线性关系,搜索将简化为对数搜索二分查找。然而,如果值分布不均匀,公式会使过程趋于一致,从而更快地收敛到所需值。

方法-1:独立函数

在 C++ 中实现插值查找算法的独立函数方法需要创建一个位于任何类之外的独立函数,该函数执行符合插值公式的操作。这种方法直接且易于理解,适用于较小的程序,其中“搜索”是相对独立的任务。

程序

输出

Entеr thе kеy to sеarch: 14
Kеy found at indеx 6

解释

该算法用于在已排序的数组中快速找到给定的键。interpolationSearch() 函数封装了搜索过程的逻辑。

  • 独立函数 - 插值查找

代码的核心部分是插值搜索函数。该函数使用插值搜索算法,该算法根据键的值和数组元素的分布,在已排序的数组中搜索键的可能位置。

  • 初始化搜索区间

该方法以两个参数开头,low 和 high,它们代表当前搜索区间的参数。最初,考虑整个数组;因此,low 设置为 0,high 设置为 n - 1,其中 n 是数组的大小。

  • 插值公式

在 while 循环中,函数使用插值公式确定键的近似位置。这些公式根据键在数组中的关系改变位置,并使用low 和 high索引的值。

  • 比较和调整

然后函数将那里 (arr[pos]) 的值与键进行比较。如果两个值相等,则函数返回 pos 的索引值;键已被找到。因为 arr[pos] 的值在此处大于键,所以键很可能在数组的左侧部分。

在这种情况下,搜索区间相应地移动(high = pos - 1)。相反,如果值小于键,则键最有可能在右半部分,并且搜索区间调整到右侧(low = pos + 1)。

  • 循环终止

while 循环执行,直到区间变为空 (low > high) 或直到找到键。如果数组中不存在键,则函数返回 -1。

  • 主函数 - 用户输入和函数调用

主函数展示了插值搜索函数的使用。它初始化了一个示例数组,并获取用户输入以搜索键。然后使用数组、其大小和键作为参数调用 interpolationSearch 函数。

  • 结果

插值搜索 的输出保存在 result 变量中。因此,主操作返回键是否找到以及找到的位置。如果未找到键,则显示键不在数组中的消息。

复杂度分析

时间复杂度

插值查找算法的时间复杂度根据输入数据的类型和数组元素的结构而变化。理想情况下,如果数据分布均匀,该算法的平均时间复杂度将是O(log n)。但是,在最坏的情况下,当分布不均匀并接近线性搜索时,时间复杂度可能退化到O(n)。

最佳情况

理想情况下,插值搜索的功能类似于二分查找,复杂度为O(log n)。当数组元素均匀分布且插值公式始终缩小搜索空间时,就会发生这种情况。

最坏情况

在最坏的情况下,插值搜索可能退化为O(n)时间性能,接近线性搜索。这是由于数组元素的分配不均,导致插值公式产生的键位置估计不准确。

空间复杂度分析

插值搜索算法的空间复杂度由用于索引和存储输入数据的两个变量决定。在此特定的 C++ 实现中,主要复杂度组成部分是输入数组 (arr)、在 interpolationSearch 函数中使用的变量(low, high, pos, key)以及主函数中使用的任何其他变量 (n 和 result)。

输入数组

空间复杂度由输入数组的大小决定,该大小与数组中的元素数量 (n) 成正比。因此,空间复杂度为O(n)。

局部变量

搜索过程由插值搜索函数中使用的 low、high、pos 和 key 变量控制。这些变量具有恒定的空间复杂度,它们对空间的影响被认为是O(1)。

附加变量

主函数中的 n 变量表示数组的大小,它需要 O(1) 空间。result 变量存储插值搜索的输出,也需要O(1) 空间。

方法 2:类实现

类实现方法将插值搜索算法的搜索逻辑封装在一个类或结构体中。组织代码符合面向对象原则,提高了封装性,并实现了潜在的代码重用。该类 InterpolationSearch封装了插值搜索功能,在一个静态方法 search 中。

这种设计鼓励更具模块化和结构化的代码库,使搜索算法易于集成到更大的系统中或在程序的各个部分中重用。基于类的结构还为搜索功能提供了清晰定义的命名空间,减少了更广泛的代码库中的命名冲突,并提高了代码的可维护性。

程序

输出

Entеr thе kеy to sеarch: 18
Kеy found at indеx 8

解释

  • 插值搜索算法的类实现方法围绕一个名为 InterpolationSearch 的类来封装和功能。这种设计与面向对象编程原则一致,增强了封装性和代码重用的可能性。
  • 实现的核心是 InterpolationSearch 类中的 search 方法。通过将此方法声明为静态,无需创建类的实例即可访问它。此静态方法封装了插值搜索逻辑,并提供了一个连贯且模块化的用户界面。
  • search 方法定义了两个变量,low 和 high,它们代表当前搜索区间的边界,就像独立函数方法一样。它在 while 循环中使用插值公式来估计键的可能位置。循环内的内部搜索逻辑与独立函数方法相对一致,包括比较和调整以缩小搜索空间。
  • 这种基于类的结构有几个优点。首先,它鼓励封装,因为它将搜索逻辑分组到一个类中,并防止直接访问内部变量和方法。这提高了代码结构并减少了与其他程序部分的干扰风险。
  • 其次,类实现为搜索函数提供了一个额外的命名空间。这种分离最大程度地减少了大型代码库中的命名冲突,在这些代码库中,许多函数或类可能共享相似的名称。因此,它有助于提高代码的可维护性和可读性。
  • 此外,基于类的方法促进了代码的重用。由于其封装的搜索方法,InterpolationSearch 类可以轻松地集成到程序的各个部分,甚至可以在完全不同的程序中重用。这些模块化特性和可重用性是开发可扩展和可维护的软件系统的关键。

复杂度分析

时间复杂度分析

使用类实现方法实现的插值搜索算法的时间复杂度与该方法没有区别。在最佳情况下,当数据分布均匀时,平均时间复杂度为O(log n)。

然而,在最坏的情况下,当非均匀分布接近线性搜索时,时间复杂度变为O(n)。由于插值搜索算法在数据分布均匀时表现良好,在数据倾斜时可能会下降。

空间复杂度分析

在空间复杂度方面,主要贡献者是输入数组 (arr[])、在 InterpolationSearch: search 方法 (low, high, pos, key) 中使用的变量以及主函数中的其他变量 (n 和 result)。由于输入数组,空间复杂度为 O(n),局部变量和参数所需的额外空间为O(1),使总空间复杂度为O(n)。基于类的方法增加了相对较小的额外空间复杂度,保持了插值搜索算法的效率。

方法 3:递归实现

插值搜索算法的递归实现使用一个函数,该函数反复将搜索区间分成两半,直到找到键或区间无效。

递归函数检查搜索区间的有效性,根据插值公式估计键的位置,并根据比较进行递归调用。该策略非常有用,因为它简洁地表达了搜索逻辑,类似于问题固有的分而治之性质。

程序

输出

Entеr thе kеy to sеarch: 13
Kеy not found in thе array.

解释

提供的 C++ 代码说明了插值搜索算法的递归版本。

  • 递归函数 - interpolationSearchRecursive

代码的核心在于 interpolationSearchRecursive 函数。此递归函数执行插值搜索算法。它接受四个参数:已排序的数组 arr[]、当前搜索区间的下限 low、当前搜索区间的上限 high 以及要搜索的键。

  • 基本情况

递归函数以基本情况开始,该情况检查搜索区间是否有效 (low <= high) 并且键是否位于当前搜索区间内 (key >= arr[low] && key <= arr[high])。如果此条件失败,则递归结束,但函数返回 -1,表示数组中不存在键。

  • 插值公式

函数中使用插值公式来计算数组中键的可能位置 (pos)。该公式还根据键在数组中的相对位置进行调整,方式与迭代方法相同。

  • 比较和递归调用

然后函数将估计位置 (arr[pos]) 的值与键进行比较。如果它们相同,则函数返回位置,表示已找到键。arr[pos] 的值大于键,这表明键可能在数组的左半部分。

在这种情况下,该函数将在更新的搜索区间上递归调用左侧。同样,如果值小于键,该函数将在右侧以更新的搜索区间递归调用自身。

  • 主函数

在主函数中,创建了一个示例数组(必须对该数组进行排序才能进行插值搜索)。用户输入用于搜索键。然后使用初始搜索区间和键调用递归函数 interpolationSearchRecursive。结果根据键的存在打印出来。

  • 递归性质

递归实现隐藏了函数调用的递归性质,以管理子区间并高效地搜索空间。该函数创建数组的较小子部分,直到找到键或搜索区间无效。

复杂度分析

时间复杂度

递归插值搜索算法的时间复杂度取决于输入数据的分布。在最佳情况下,当数据分布均匀时,平均情况复杂度为O(log n)。然而,在最坏的情况下,当分布极不均匀并接近线性搜索时,时间复杂度为O(n)。

最佳情况

在最佳情况下,递归插值搜索与其迭代对应项执行得相当,平均时间复杂度达到O(log n)。这是由于数组元素的均匀分布,插值公式能够高效地持续收缩搜索空间。

最坏情况

在最坏的情况下,时间复杂度可能下降到O(n)。这种情况发生在数组元素分布不均,并且插值公式提供了对键的位置的不准确估计时,从而导致次优的搜索行为。

空间复杂度

递归插值搜索算法的空间复杂度取决于函数调用堆栈和每个函数调用中使用的附加变量。主要来源是输入数组 (arr[]),递归函数的参数 (low, high, key) 以及每个函数调用中的局部变量。

输入数组

数组大小 arr[] 驱动空间复杂度。数组所需的空间与元素数量 (n) 成正比。因此,与输入数组相关的空间复杂度为O(n)。

函数调用堆栈

算法的递归性质导致了函数调用堆栈的空间复杂度。在最佳情况下,当递归有效地减小搜索空间时,调用堆栈的最大深度为O(log n)。然而,在最坏的情况下,如果递归的行为类似于线性搜索,最大深度可能为 O(n)。

局部变量

每个函数调用中的局部变量,如 low、high、pos 和 key,每个调用都需要恒定的空间。因此,与局部变量相关的空间复杂度为每个函数调用O(1)。

结论

插值搜索算法的递归实现提供了迭代方法的替代实现。它以更简洁的方式递归地描述搜索逻辑,并通过调用函数来处理子区间。该代码旨在提高可读性,并且递归插值搜索算法的每一步都通过注释进行了解释。当递归技术受欢迎时,这种实现是一种有价值的方法。

方法 4:模板实现

模板实现中,使用 C++ 编写了模板插值搜索算法的通用模板版本。使用模板,您可以构建一个灵活且与类型无关的实现,适用于不同的数据类型。在此配置中,代码的可重用性和灵活性得到了增强。

程序

输出

Intеgеr Kеy found at indеx 6
Float Kеy found at indеx 6

解释

提供的 C++ 代码说明了插值搜索算法的模板实现方法,该方法是通用的且与类型无关的。

  • 函数模板 - interpolationSearch

模板实现是在 interpolationSearch 函数中找到的,该函数是一个固定模板。函数模板的参数类型名 T 允许它使用不同的数据类型工作。此模板设计旨在促进灵活性和代码的可重用性。

  • 初始化搜索区间

该函数模板初始化了两个变量low 和 high,代表当前搜索范围的边界。此初始化对于插值搜索算法至关重要,它允许缩小搜索空间。

  • 插值公式和搜索逻辑

在 while 循环中,插值公式估计键 (pos) 的可能位置。循环中的搜索逻辑与其他实现一致,包括比较和调整以缩小搜索空间。

  • 主函数中的模板实例化

在主函数中,使用特定的数据类型实例化了模板。第一个示例显示了在整数数组 (intArr) 中搜索,而第二个示例则详细介绍了在浮点数数组 (floatArr) 中搜索。这种方法的快速性确保了相同的模板函数可以与不同的数组和键顺利配合。

复杂度分析

时间复杂度

插值搜索算法的模板实现的时间复杂度与其它实现保持一致。它受到输入分布的影响,并且分析仅适用于模板化非模板化版本。

最佳情况

在最佳情况下,当数据分布均匀时,平均复杂度时间为O(log n)。插值搜索加速了搜索空间,获得了对数时间复杂度。

最坏情况

然而,在最坏的情况下,当分布不均匀且接近线性搜索时,时间复杂度变为O(n)。然而,这种情况发生在插值公式提供了键位置的不正确估计时,从而导致次优的搜索行为。

空间复杂度分析

由于插值搜索算法的模板实现的空间复杂度,使用输入数组 (arr[]),它是 O(n)。其他空间需求由函数调用堆栈产生,在最佳情况下最大递归深度为O(log n),在最坏情况下为O(n)。每个函数调用中的局部变量贡献O(1)空间,当考虑递归深度时,总空间复杂度为O(n)。