Python高性能编程3版:列表与元组的高效探索

Python高性能编程3版:列表与元组的高效探究

3 列表与元组

主要内容

  • 列表和元组的功用是什么?
  • 在列表或元组中进行查找的复杂度情况怎样?
  • 如何达成这种复杂度?
  • 列表和元组有哪些差异?
  • 怎样对列表进行追加操作?
  • 何时该选用列表或元组?

高效编写程序的关键之一在于明晰所使用数据结构所具备的特性。高效编程很大程度上是要明确你对数据提出何种问题,并选取能快速回应这些问题的数据结构。在本章里,我们会探讨列表和元组能够快速回应哪些类型的问题,以及它们是怎样做到的。

列表和元组属于数组这类数据结构。数组是有着内在排序的扁平数据列表。通常在这类数据结构中,元素的相对顺序和元素自身同等重要!并且,这种对顺序的先验认知极为宝贵:凭借知晓数组中的数据处于特定位置,我们能够在O(1)的时间内获取这些数据!实现数组的方式有诸多,每种方案都有其有用的特性和保障。这就是为何在Python中有两种数组类型:列表和元组。列表是动态数组,允许我们修改并调整存储数据的大小,而元组是静态数组,其内容固定不变。

来解读一下上述表述。计算机的系统内存可看作是一系列编号的桶,每个桶能存储一个数字。Python通过引用来将数据存储在这些桶中,这意味着数字本身只是指向我们真正关心的数据的引用。所以,这些桶能够存储我们想要的任何类型的数据(而numpy数组不同,它有静态类型,只能存储该类型的数据)。

当我们要创建数组(以及列表或元组)时,首先得分配一块系统内存(这块内存的每一部分将用作实际数据的整数大小指针)。这需要调用系统内核,请求使用N个连续的内存桶。下图展示了大小为6的数组(本例中为列表)的系统内存布局示例。

注意:在Python中,列表也存储它们的大小,所以在分配的六个块中,只有五个是可用的——第三个元素是长度。

若要查找列表中的任意特定元素,我们只需知道想要的元素,并记住数据从哪个存储桶起始。由于所有数据占用相同空间(一个“桶”,更确切地说,是一个指向实际数据的整数大小指针),所以无需知晓存储数据的类型来进行计算。

若已知由N个元素组成的列表在内存中的起始位置,该如何在列表中查找任意元素呢?比如,若我们要获取数组中的第零个元素,只需转到序列中的第一个存储桶M,然后读出其中的值。反之,若要获取数组中的第五个元素,就转到位于M + 5位置的桶,再读取其中内容。所以,通过将数据存储在连续的桶中,并知晓数据的顺序,无论数组大小如何,我们都能通过一步(即O(1))就确定要查看哪个桶来定位数据(例3-1)。

例3-1. 不同大小列表中的查找计时

>>> %%timeit l = list(range(10))
...: l[5]
...:
14 ns ± 0.182 ns per loop (mean ± std. dev. of 7 runs, 100,000,000 loops each)

>>> %%timeit l = list(range(10_000_000))
...: l[100_000]
...:
13.9 ns ± 0.123 ns per loop (mean ± std. dev. of 7 runs, 100,000,000 loops each)

要是给我们一个顺序未知的数组,想要检索某个特定元素,该怎么办呢?若顺序已知,只需查找该特定值即可。但这种情况下,就得进行搜索操作。最基本的解决办法是线性搜索,我们遍历数组中的每个元素,检查是否是想要的值,如例3-2所示。

例3-2. 对列表进行线性搜索

import timeit


def linear_search(needle, array):
    for i, item in enumerate(array):
        if item == needle:
            return i
    return -1


if __name__ == "__main__":
    setup = "from __main__ import (linear_search, haystack, needle)"
    iterations = 1000

    for haystack_size in (10000, 100000, 1000000):
        haystack = range(haystack_size)
        for needle in (1, 6000, 9000, 1000000):
            index = linear_search(needle, haystack)
            t = timeit.timeit(
                stmt="linear_search(needle, haystack)", setup=setup, number=iterations
            )
            print(
                f"Value {needle: <8} found in haystack of "
                f"size {len(haystack): <8} at index "
                f"{index: <8} in {t/iterations:.5e} seconds"
            )

执行结果:

# python linear_search.py
Value 1        found in haystack of size 10000    at index 1        in 1.32893e-07 seconds
Value 6000     found in haystack of size 10000    at index 6000     in 2.02747e-04 seconds
Value 9000     found in haystack of size 10000    at index 9000     in 3.04364e-04 seconds
Value 1000000  found in haystack of size 10000    at index -1       in 3.49609e-04 seconds
Value 1        found in haystack of size 100000   at index 1        in 1.31818e-07 seconds
Value 6000     found in haystack of size 100000   at index 6000     in 2.06552e-04 seconds
Value 9000     found in haystack of size 100000   at index 9000     in 3.20001e-04 seconds
Value 1000000  found in haystack of size 100000   at index -1       in 3.51215e-03 seconds
Value 1        found in haystack of size 1000000  at index 1        in 1.31243e-07 seconds
Value 6000     found in haystack of size 1000000  at index 6000     in 1.98086e-04 seconds
Value 9000     found in haystack of size 1000000  at index 9000     in 3.07604e-04 seconds
Value 1000000  found in haystack of size 1000000  at index -1       in 3.37868e-02 seconds

这种算法的最差性能是O(n)。当搜索不在数组中的元素时,就会出现这种情况。要知道要搜索的元素不在数组中,就得先将它与其他元素核对。最终会执行到最后的return -1语句。实际上,这种算法正是list.index()所采用的算法。

要提升速度,唯一的办法是了解数据在内存中的放置方式,或者我们所拥有的数据桶的排列方式。例如,哈希表(“字典和集合是如何工作的?”)是字典和集合的基础数据结构,它通过在插入/检索时增加额外开销并强制对项目进行严格且特殊的排序,以O(1)的速度解决了这个问题。另外,如果对数据进行排序,使每个条目都比左边(或右边)的相邻条目大(或小),那么就可以使用专门的搜索算法,将查找时间降低到O(log n)。不过,有时使用这些搜索算法是最佳解决方案(尤其是搜索算法非常灵活,允许以创造性的方式定义搜索)。

练习

给定如下数据,编写算法来查找值61的索引:[9, 18, 18, 19, 29, 42, 56, 61, 88, 95]
已知数据是有序的,怎样能更快完成呢?
提示:若将数组一分为二,就能知晓左边所有值都小于右边集合中最小的元素。可利用此方法!

3.1 更高效的搜索

如前所述,若先对数据进行排序,使某一项左边的所有元素都比该项小(或大),就能获得更好的搜索性能。比较是通过对象的eqlt魔法函数完成的,若使用自定义对象,也可由用户自定义。

若没有eqlt方法,自定义对象将仅与相同类型的对象比较,比较会使用实例在内存中的位置。定义了这两个魔法函数后,可使用标准库中的functools.total_ordering装饰器自动定义所有其他排序函数,尽管性能会有少许损失。

必要的两个要素是排序算法和搜索算法。Python列表有内置的使用Tim排序的排序算法。在最佳情况下,Tim排序能以O(n)的速度对列表排序(在最差情况下,排序速度为O(n log n))。它通过结合多种类型的排序算法,并运用启发式方法猜测在给定数据下哪种算法性能最佳来实现这一性能(更确切地说,它混合了插入和合并排序算法)。

对列表排序后,可使用二进制搜索(例3-3)找到所需元素,其平均复杂度为O(log n)。它首先查看列表的中点,然后将该值与所需值比较。若这个中点的值小于我们期望的值,就考虑列表的右半部分,如此继续将列表减半,直到找到该值,或确定该值不在排序列表中。所以,无需像线性搜索那样读取列表中的所有值,只需读取其中一部分。

例3-3. 高效搜索排序列表--二进制搜索

import timeit


def binary_search(needle, haystack):
    # imin和imax存储当前考虑的haystack的边界。初始为haystack的边界,逐渐收敛到包围needle。
    imin, imax = 0, len(haystack)
    while True:
        if imin >= imax:
            return -1
        midpoint = (imin + imax) // 2
        if haystack[midpoint] > needle:
            imax = midpoint
        elif haystack[midpoint] < needle:
            imin = midpoint + 1
        else:
            return midpoint


if __name__ == "__main__":
    setup = "from __main__ import (binary_search, haystack, needle)"
    iterations = 10000

    for haystack_size in (10000, 100000, 1000000):
        haystack = range(haystack_size)
        for needle in (1, 6000, 9000, 1000000):
            index = binary_search(needle, haystack)
            t = timeit.timeit(
                stmt="binary_search(needle, haystack)", setup=setup, number=iterations
            )
            print(
                f"Value {needle: <8} found in haystack of "
                f"size {len(haystack): <8} at index "
                f"{index: <8} in {t/iterations:.5e} seconds"
            )

执行结果:

# python binary_search.py
Value 1        found in haystack of size 10000    at index 1        in 9.15618e-07 seconds
Value 6000     found in haystack of size 10000    at index 6000     in 1.24657e-06 seconds
Value 9000     found in haystack of size 10000    at index 9000     in 1.21479e-06 seconds
Value 1000000  found in haystack of size 10000    at index -1       in 1.66498e-06 seconds
Value 1        found in haystack of size 100000   at index 1        in 1.15999e-06 seconds
Value 6000     found in haystack of size 100000   at index 6000     in 1.76719e-06 seconds
Value 9000     found in haystack of size 100000   at index 9000     in 1.57295e-06 seconds
Value 1000000  found in haystack of size 100000   at index -1       in 2.03157e-06 seconds
Value 1        found in haystack of size 1000000  at index 1        in 1.36514e-06 seconds
Value 6000     found in haystack of size 1000000  at index 6000     in 1.74420e-06 seconds
Value 9000     found in haystack of size 1000000  at index 9000     in 1.96218e-06 seconds
Value 1000000  found in haystack of size 1000000  at index -1       in 2.39626e-06 seconds

通过这种方法,无需使用字典这种可能较重的解决方案就能查找列表中的元素。尤其是当操作的数据列表本质上是有序的时候。在列表中进行二进制搜索查找对象比先将数据转换为字典再进行单次查找更高效。虽然字典查找只需O(1),但将数据转换为字典需要O(n)(而且字典不限制重复键可能不可取)。而二进制搜索需要O(log n)。

此外,Python标准库中的bisect模块简化了这一过程,除了使用高度优化的二进制搜索查找元素外,还提供了在保持列表有序的同时向列表中添加元素的简便方法。它通过提供替代函数,将元素添加到正确的有序位置。由于列表始终保持有序,很容易找到要找的元素(相关示例可在bisect模块的文档中找到)。此外,还可以使用bisect快速找到最接近的元素(例3-4)。这对比较两个相似但不完全相同的数据集很有用。

例3-4. 使用bisect模块查找列表中的近似值

import bisect
import random


def find_closest(haystack, needle):
    # bisect.bisect_left将返回haystack中第一个大于needle的值
    i = bisect.bisect_left(haystack, needle)
    if i == len(haystack):
        return i - 1
    elif haystack[i] == needle:
        return i
    elif i > 0:
        j = i - 1
        # 由于知道值大于needle(反之亦然对于j处的值),此处无需使用绝对值
        if haystack[i] - needle > needle - haystack[j]:
            return j
    return i


if __name__ == "__main__":
    important_numbers = []
    for i in range(10):
        new_number = random.randint(0, 1000)
        bisect.insort(important_numbers, new_number)

    # important_numbers由于使用bisect.insort插入新元素,已经有序
    print(important_numbers)
    # > [14, 265, 496, 661, 683, 734, 881, 892, 973, 992]

    closest_index = find_closest(important_numbers, -250)
    print(f"Closest value to -250: {important_numbers[closest_index]}")
    # > Closest value to -250: 14

    closest_index = find_closest(important_numbers, 500)
    print(f"Closest value to 500: {important_numbers[closest_index]}")
    # > Closest value to 500: 496

    closest_index = find_closest(important_numbers, 1100)
    print(f"Closest value to 1100: {important_numbers[closest_index]}")
    # > Closest value to 1100: 992

执行结果

[26, 69, 153, 159, 172, 221, 336, 426, 772, 859]
Closest value to -250: 26
Closest value to 500: 426
Closest value to 1100: 859

一般而言,这涉及编写高效代码的基本规则:选择正确的数据结构并坚持使用!虽然

文章整理自互联网,只做测试使用。发布者:Lomu,转转请注明出处:https://www.it1024doc.com/12620.html

(0)
LomuLomu
上一篇 8小时前
下一篇 2025 年 5 月 15 日

相关推荐

  • 促销系统:促销活动、优惠券、优惠规则概念模型设计

    大家好,我是汤师爷~ 概念模型设计是促销系统开发的关键环节,我们需要基于之前的功能分析,将复杂的促销业务拆解成清晰的领域概念,这些概念之间的关系界定和边界划分,将直接决定系统的可维护性和扩展性。 促销系统核心概念模型 通过对促销业务的分析,我们可以抽象出促销系统的关键概念模型。 1、促销活动模型 促销活动模型对活动的各个要素和规则进行抽象,包含活动名称、描述…

    2025 年 1 月 11 日
    19300
  • 【深度学习】Java DL4J基于 CNN 构建农作物病虫害检测模型

    🧑 博主简介:CSDN博客专家 ,历代文学网 (PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可微信小程序搜索“历代文学 ”)总架构师,15年工作经验,精通Java编程,高并发设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理…

    未分类 2025 年 1 月 10 日
    34400
  • 2025年最新DataGrip永久破解教程(附激活码/注册码)🔥

    🚀 本教程适用于JetBrains全家桶,包括IDEA、PyCharm、DataGrip、GoLand等开发工具! 先给大家看看最新版本的破解成果,直接激活到2099年,简直不要太爽!✨ 下面我将用详细的图文教程,手把手教你如何永久激活DataGrip。这个方法同样适用于旧版本哦! 💡 无论你用的是Windows、Mac还是Linux系统,都能完美破解! 第…

    DataGrip激活码 6天前
    5100
  • 🚀 2025最新PyCharm永久激活教程|破解到2099年|附最新激活码和注册码

    🔥 本教程适用于Jetbrains全家桶,包括IDEA、PyCharm、DataGrip、Goland等所有产品! 先给大家看看最新PyCharm版本成功破解的截图,有效期直接到2099年,简直不要太爽!😎 下面我就用详细的图文教程,手把手教你如何激活PyCharm到2099年。这个方法同样适用于旧版本哦! ✨ 支持所有系统和版本:- 无论是Windows、…

    2025 年 6 月 14 日
    14200
  • 2024 IDEA最新激活码,IDEA永久免费激活码2025-01-19 更新

    IDEA 2024最新激活码 以下是最新的IDEA激活码,更新时间:2025-01-19 🔑 激活码使用说明 1️⃣ 复制下方激活码 2️⃣ 打开 IDEA 软件 3️⃣ 在菜单栏中选择 Help -> Register 4️⃣ 选择 Activation Code 5️⃣ 粘贴激活码,点击 Activate ⚠️ 必看!必看! 🔥 获取最新激活码: 实时更…

    2025 年 1 月 19 日
    63300

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信