详解Python查找算法的实现(线性,二分,分块,插值)

方予
方予
发布时间:2026-10-12| 2人浏览过
原创

本文深入解析Python中四种常见查找算法的实现与性能。涵盖线性查找及其前哨站优化、二分查找的递归与迭代实现、插值查找对中间位置的改进,以及分块查找的索引构建。详细分析各算法的时间复杂度、适用场景及优缺点,并提供完整代码示例与优化建议。

查找算法旨在从序列数据(即数据集)中检索特定数据(即关键字)是否存在。常见的查找算法包括以下几种:

  • 线性查找:亦称顺序查找,适用于无序数列的查找场景。
  • 二分查找:亦称折半查找,专门用于有序数列的查找。
  • 插值查找:作为二分查找算法的改进版本。
  • 分块查找:又称索引顺序查找,是对线性查找的优化改进。
  • 树表查找:包含二叉查找树和平衡二叉树查找等类型。
  • 哈希查找:通过关键字直接定位所需数据。

鉴于树表查找和哈希查找的内容篇幅较长,本文暂不展开。本文将重点详解除这两类之外的其他查找算法,逐一分析其优缺点,并探讨相应的优化方案。

1. 线性查找

线性查找,又称顺序查找,是一种基础、穷举式的暴力查找算法。其逻辑简单,易于理解和编码实现。然而,当数据量庞大时,由于其朴素穷举的思想且缺乏优化设计,算法性能往往较低。

线性查找的核心思想如下:

  • 从列表头部开始,逐一扫描原始列表中的每个数据元素,并将其与给定的关键字进行比较。
  • 若发现两者相等,则判定查找成功。
  • 若扫描完整个列表仍未找到与关键字相等的数据,则判定查找失败。

基于上述算法描述,可以很容易地编写代码实现:

'''
线性查找算法
参数:
    nums: 序列
    key:关键字
返回值:
    关键字在序列中的位置
    如果没有,则返回 -1
'''
def line_find(nums, key):
    for i in range(len(nums)):
        if nums[i] == key:
            return i
    return -1
'''
测试线性算法
'''
if __name__ == "__main__":
    nums = [4, 1, 8, 10, 3, 5]
    key = int(input("请输入要查找的关键字:"))
    pos = line_find(nums, key)
    print("关键字 {0} 在数列的第 {1} 位置".format(key, pos))
'''
输出结果:
请输入要查找的关键字:3
关键字 3 在数列的 4 位置
'''

接下来分析线性查找算法的平均时间复杂度。

1. 最佳情况:若目标关键字恰好位于数列的第 1 个位置,则仅需查找 1 次即可成功。

例如,在数列=[4,1,8,10,3,5]中查找关键字 4。

此时仅需查找 1 次。

2. 最坏情况:直到扫描到数列尾部时才找到关键字。

例如,在数列=[4,1,8,10,3,5]中查找是否存在关键字 5。

此时查找次数等于数列长度,本例中为 6 次。

3. 一般情况:若关键字位于数列中间某位置,其被查找到的概率为 1/n 。

其中 n 代表数列的长度。

线性查找的平均查找次数计算公式为 (1+n)/2。转换为大 O 表示法,其时间复杂度为 O(n) 。

在大 O 表示法中,常数项通常被忽略。

线性查找的最坏情况是:扫描完整整个数列后,仍未找到目标关键字。

例如,在数列=[4,1,8,10,3,5]中查找是否存在关键字 12。

经过 6 次扫描后,查找失败。

改良线性查找算法

可以对线性查找进行优化,例如引入“前哨站”机制。所谓“前哨站”,即在查找开始前,将待查找的关键字插入到数列的尾部。

def line_find_(nums, key):
    i = 0
    while nums[i] != key:
        i += 1
    return -1 if i == len(nums)-1 else i

'''
测试线性算法
'''
if __name__ == "__main__":
    nums = [4, 1, 8, 10, 3, 5]
    key = int(input("请输入要查找的关键字:"))
    # 查找之前,先把关键字存储到列到的尾部
    nums.append(key)
    pos = line_find_(nums, key)
    print("关键字 {0} 在数列的第 {1} 位置".format(key, pos))

使用“前哨站”优化后的线性查找算法,其时间复杂度并未改变,仍为 O(n)。从 2 版本的代码来看,改动也不大。

但从实际运行角度看,第 2 种方案减少了 if 判断指令的执行次数,从而减少了编译后的指令数量,进而降低了 CPU 执行指令的总次数。这种优化属于微优化,而非算法本质上的复杂度优化。

使用计算机编程语言编写的代码属于伪指令代码。

经过编译处理后生成的指令代码称为 CPU 指令集。

一种优化思路即在于减少编译后生成的指令集大小。

2. 二分查找

二分查找属于有序查找算法,意味着被查找的数列必须是有序的。例如,在数列=[4,1,8,10,3,5,12]中查找关键字 4,由于数列无序,无法直接使用二分查找;若需使用,必须先对数列进行排序。

二分查找运用了二分(折半)算法思想,其算法执行过程中需随时获取两个关键信息:

  • 一是数列的中间位置 mid_pos。
  • 二是数列中间位置对应的值 mid_val。

现在通过数列 nums=[1,3,4,5,8,10,12] 中查找关键字 8 来演示二分查找的流程。

在开始二分查找前,先定义两个位置(指针)变量:

  • 左指针 l_idx 初始指向数列的最左侧元素。
  • 右指针 r_idx 初始指向数列的最右侧元素。

第 1 步:通过左、右指针的当前位置计算出数列的中间位置 mid_pos=3,并根据 mid_pos 的值找出数列中间位置所对应的值 mid_val=nums[mid_pos],即 5。

二分查找的核心在于确定数列中间位置的值。

第 2 步:将数列中间位置的值与给定关键字进行比较。此处关键字为 8,中间位置值为 5。显然 8 大于 5,由于数列有序,无需再与数列中 5 之前的数字比较,只需专注于与 5 之后的数字比较。

经过一次比较后,待查找的数列范围缩小了一半,这也是二分算法名称的由来。

第 3 步:根据比较结果调整数列范围。这里的调整并非物理结构上的改变,而是逻辑上的调整,原数列保持不变。即通过修改左指针或右指针的位置,在逻辑上缩小数列范围。调整后的数列如图所示。

二分查找中,数列的范围由左指针到右指针的长度决定。

第 4 步:重复上述步骤,直到找到目标或确定不存在为止。

编码实现二分查找算法

'''
二分查找算法
'''
def binary_find(nums, key):
    # 初始左指针
    l_idx = 0
    # 初始在指针
    r_ldx = len(nums) - 1
    while l_idx <= r_ldx:
        # 计算出中间位置
        mid_pos = (r_ldx + l_idx) // 2
        # 计算中间位置的值
        mid_val = nums[mid_pos]
        # 与关键字比较
        if mid_val == key:
            # 出口一:比较相等,有此关键字,返回关键字所在位置
            return mid_pos
        elif mid_val > key:
            # 说明查找范围应该缩少在原数的左边
            r_ldx = mid_pos - 1
        else:
            l_idx = mid_pos + 1
    # 出口二:没有查找到给定关键字
    return -1

'''
测试二分查找
'''
if __name__ == "__main__":
    nums = [1, 3, 4, 5, 8, 10, 12]
    key = 3
    pos = binary_find(nums, key)
    print(pos)

基于前述二分算法流程分析,可知二分查找的子问题与原始问题逻辑一致,因此可以使用递归方式实现:

'''
递归实现二分查找
'''
def binary_find_dg(nums, key, l_idx, r_ldx):
    if l_idx > r_ldx:
        # 出口一:没有查找到给定关键字
        return -1
    # 计算出中间位置
    mid_pos = (r_ldx + l_idx) // 2
    # 计算中间位置的值
    mid_val = nums[mid_pos]
    # 与关键字比较
    if mid_val == key:
        # 出口二:比较相等,有此关键字,返回关键字所在位置
        return mid_pos
    elif mid_val > key:
        # 说明查找范围应该缩少在原数的左边
        r_ldx = mid_pos - 1
    else:
        l_idx = mid_pos + 1
    return binary_find_dg(nums, key, l_idx, r_ldx)
'''
测试二分查找
'''
if __name__ == "__main__":
    nums = [1, 3, 4, 5, 8, 10, 12]
    key = 8
    pos = binary_find_dg(nums, key,0,len(nums)-1)
    print(pos)

接下来分析二分查找的性能。

二分查找的过程用树形结构描述更为直观,搜索结束后绘制的树结构是一棵二叉树。

1. 如上述代码执行过程,先找到数列中的中间数字 5,随后以 5 为根节点构建唯一结点树。

2. 5 与关键字 8 比较后,再在以数字 5 为分界线的右侧数列中找到中间数字 10,树形结构变为下图所示。

3. 10 与关键字 8 比较后,再在 10 的左侧继续查找。

查找到 8 后,意味着二分查找已找到结果,仅需 3 次即可查找到最终结果。

从二叉树结构可直观得出结论:二分查找关键字的比较次数由关键字在二叉树结构中的深度决定。

4. 上述示例查找的是给定数字 8。为了能够查找数列中的任意数字,最终完整的树结构应如下图所示。

显然,该树结构是标准的二叉树。从树结构可以看出,无论查找任何数字,最少需要 1 次,如查找数字 5;最多也只需 3 次,这比线性查找快得多。

根据二叉树特性,结点数为 n 的树的深度为 h=log2(n+1),因此二分查找算法的大 O 表示时间复杂度为 O(logn),属于对数级别的时间复杂度。

当对长度为 1000 的数列进行二分查找时,所需次数最多只要 10 次,二分查找算法的效率显然高效。

然而,二分查找要求数列提前排序,前述时间复杂度未考虑排序时间。因此,二分查找通常适用于数据变化稳定的有序数列。

3. 插值查找

插值查找本质上是二分查找,其对二分查找算法中计算中间位置的逻辑进行了改进。

原生二分查找算法中计算中间位置的逻辑为:中间位置等于左指针位置加上右指针位置然后除以 2。

    # 计算中间位置
    mid_pos = (r_ldx + l_idx) // 2

插值算法计算中间位置的逻辑如下所示:

其中 key 代表要查找的关键字。

# 插值算法中计算中间位置
mid_pos = l_idx + (key - nums[l_idx]) // (nums[r_idx] - nums[l_idx]) * (r_idx - l_idx)

以下是插值查找的编码实现:

# 插值查找基于二分法,只是mid计算方法不同
def binary_search(nums, key):
    l_idx = 0
    r_idx = len(nums) - 1
    old_mid = -1
    mid_pos = None
    while l_idx < r_idx and nums[0] <= key and nums[r_idx] >= key and old_mid != mid_pos:
        # 中间位置计算
        mid_pos = l_idx + (key - nums[l_idx]) // (nums[r_idx] - nums[l_idx]) * (r_idx - l_idx)
        old_mid = mid_pos
        if nums[mid_pos] == key:
            return "index is {}, target value is {}".format(mid_pos, nums[mid_pos])
            # 此时目标值在中间值右边,更新左边界位置
        elif nums[mid_pos] < key:
            l_idx = mid_pos + 1
        # 此时目标值在中间值左边,更新右边界位置
        elif nums[mid_pos] > key:
            r_idx = mid_pos - 1
    return "Not find"

li =[1, 3, 4, 5, 8, 10, 12]
print(binary_search(li, 6))

在插值算法计算中间位置时,若多次计算结果相同,则可视为查找失败。

插值算法的性能介于线性查找和二分查找之间。

当数列中数字较多且分布较为均匀时,插值查找的平均性能比折半查找好得多。但如果数列中数据分布非常不均匀,此时插值算法并非最佳选择。

4. 分块查找

分块查找类似于数据库中的索引查询,因此也称为索引查找。其算法核心仍基于线性查找。

现有原始数列 nums=[5,1,9,11,23,16,12,18,24,32,29,25],需要查找关键字 11 是否存在。

第 1 步:使用分块查找前,需先将原始数列按区域分成多个块。分块数量可根据实际情况自行定义。分块时有一个要求:前一个块中的最大值必须小于后一个块的最小值。

块内部可以无序,但需保持整个数列按块有序。

分块查找要求原始数列从整体上具有升序或降序趋势。如果数列分布不具有趋向性,仍想使用分块查找,则需进行分块有序调整。

第 2 步:根据分块信息建立索引表。索引表至少应包含 2 个字段,包括每一块中的最大值数字以及每一块的起始地址。显然,索引表中的数字是有序的。

第 3 步:查找给定关键字时,先查询索引表,确定关键字可能位于哪个块。例如查询关键字 29,可知其应在第三块中。随后根据索引表提供的第三块地址信息,进入第三块数列,按线性匹配算法查找 29 的具体位置。

以下是分块查找的编码实现:

首先编码实现根据分块数量创建索引表,此处使用二维列表保存储索引表中的信息。

'''
分块:建立索引表
参数:
    nums 原始数列
    blocks 块大小
'''
def create_index_table(nums, blocks):
    # 索引表使用列表保存
    index_table = []
    # 每一块的数量
    n = len(nums) // blocks
    for i in range(0, len(nums), n):
        # 索引表中的每一行记录
        tmp_lst = []
        # 最大值
        tmp_lst.append(max(nums[i:i + n-1]))
        # 起始地址
        tmp_lst.append(i)
        # 终止地址
        tmp_lst.append(i + n - 1)
        # 添加到索引表中
        index_table.append(tmp_lst)
    return index_table
'''
测试分块
'''
nums = [5, 1, 9, 11, 23, 16, 12, 18, 24, 32, 29, 25]
it = create_index_table(nums, 3)
print(it)
'''
输出结果:
[[11, 0, 3], [23, 4, 7], [32, 8, 11]]
'''

代码执行后,输出结果与分析结果一致。

以上代码仅针对整体趋势有序的数列进行分块。如果整体并非趋向有序,则需提供相应的块排序方案,有兴趣者可自行完成。

如上代码仅用于说明分块查找算法原理。

分块查找的完整代码如下:

'''
分块:建立索引表
参数:
    nums 原始数列
    blocks 块大小
'''
def create_index_table(nums, blocks):
    # 索引表使用列表保存
    index_table = []
    # 每一块的数量
    n = len(nums) // blocks
    for i in range(0, len(nums), n):
        tmp_lst = []
        tmp_lst.append(max(nums[i:i + n - 1]))
        tmp_lst.append(i)
        tmp_lst.append(i + n - 1)
        index_table.append(tmp_lst)
    return index_table

'''
使用线性查找算法在对应的块中查找
'''
def lind_find(nums, start, end):
    for i in range(start, end):
        if key == nums[i]:
            return i
            break
    return -1

'''
测试分块
'''
nums = [5, 1, 9, 11, 23, 16, 12, 18, 24, 32, 29, 25]
key = 16
# 索引表
it = create_index_table(nums, 3)
# 索引表的记录编号
pos = -1
# 在索引表中查询
for n in range(len(it) - 1):
    # 是不是在第一块中
    if key <= it[0][0]:
        pos = 0
    # 其它块中
    if it[n][0] < key <= it[n + 1][0]:
        pos = n + 1
        break
if pos == -1:
    print("{0} 在 {1} 数列中不存在".format(key, nums))
else:
    idx = lind_find(nums, it[pos][1], it[pos][2] + 1)
    if idx != -1:
        print("{0} 在 {1} 数列的 {2} 位置".format(key, nums, idx))
    else:
        print("{0} 在 {1} 数列中不存在".format(key, nums))
'''
输出结果
16 在 [5, 1, 9, 11, 23, 16, 12, 18, 24, 32, 29, 25] 数列的第 5 位置
'''

分块查找对于整体趋向有序的数列,其查找性能较好。但如果原始数列整体无序,则需提供块排序算法,其时间复杂度不如二分查找算法。

分块查找需要建立索引表,这需要额外的存储空间,因此空间复杂度较高。其优于二分查找之处在于只需对原始数列进行部分排序。本质上,分块查找仍以线性查找为主。

5. 总结

本文讲解了线性、二分、插值、分块查找算法。此外,还有树表查找、哈希查找等其他算法。

分块算法可视为对线性查找算法的优化。

插值查找可视为在二分算法基础上的一个变化。

算法没有固定模式,如果学会了二分查找算法,相当于掌握了一招,需要学会领悟,然后在这一招上演变出更多变化。

以上就是详解Python查找算法的实现(线性,二分,分块,插值)的详细内容,更多关于Python查找算法的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。