ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

hello-algo 空间复杂度精讲:用“暂存空间 + 输出空间”口径量化算法内存开销

hello-algo 空间复杂度精讲:用“暂存空间 + 输出空间”口径量化算法内存开销 hello-algo 空间复杂度精讲用“暂存空间 输出空间”口径量化算法内存开销【免费下载链接】hello-algo《Hello 算法》动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo本文以《Hello 算法》hello-algo计算复杂度章节中的空间复杂度主题为主线系统讲解空间复杂度的统计口径、推算规则与五种常见阶数常数、对数、线性、平方、指数并结合仓库中codes/python/chapter_computational_complexity/space_complexity.py的六个可运行函数与codes/pythontutor/chapter_computational_complexity/space_complexity.md的分步动画注释给出从原理到实测的完整分析路径。读完后你将能够独立判断一段 Python 代码在循环、递归、动态分配等场景下各自占用的内存阶数并能在真实工程中做出“以空间换时间”或“以时间换空间”的权衡决策。什么是空间复杂度以及“统计什么”空间复杂度space complexity用于衡量算法占用内存空间随着数据量变大时的增长趋势。这个概念与时间复杂度非常类似只需将“运行时间”替换为“占用内存空间”。算法在运行过程中使用的内存空间主要包括三种对应完整多语言版讲解见 docs/chapter_computational_complexity/space_complexity.md输入空间用于存储算法的输入数据。暂存空间用于存储算法在运行过程中的变量、对象、函数上下文等数据。输出空间用于存储算法的输出数据。一般情况下空间复杂度的统计范围是**“暂存空间”加上“输出空间”**输入空间往往由调用方决定通常不计入。暂存空间可进一步细分为三部分暂存数据保存算法运行过程中的各种常量、变量、对象等栈帧空间保存调用函数的上下文数据。系统每次调用函数都会在栈顶部创建一个栈帧函数返回后栈帧空间会被释放指令空间保存编译后的程序指令实际统计中通常忽略不计。因此分析一段程序的空间复杂度时通常统计暂存数据、栈帧空间和输出数据三部分。仓库中的 Python 版本示例与 docs/chapter_computational_complexity/space_complexity.md 中 Python 代码块一致的概念示意class Node: 类 def __init__(self, x: int): self.val: int x # 节点值 self.next: Node | None None # 指向下一节点的引用 def function() - int: 函数 # 执行某些操作... return 0 def algorithm(n) - int: # 输入数据 A 0 # 暂存数据常量一般用大写字母表示 b 0 # 暂存数据变量 node Node(0) # 暂存数据对象 c function() # 栈帧空间调用函数 return A b c # 输出数据仓库中的 Python 实现与运行方式本章的可运行代码位于 codes/python/chapter_computational_complexity/space_complexity.py文件头部通过修改sys.path引入仓库自带的模块包再从中取用节点类与打印工具import sys from pathlib import Path sys.path.append(str(Path(__file__).parent.parent)) from modules import ListNode, TreeNode, print_tree从源码结构看modules包的入口 codes/python/modules/__init__.py 统一再导出ListNode、TreeNode、print_tree等符号其定义分别位于 codes/python/modules/list_node.pyvalnext两个字段与 codes/python/modules/tree_node.pyval、height、left、right字段。运行环境方面需要注意版本前提源文件使用了dict[int, str]PEP 585 泛型和TreeNode | NonePEP 604 联合类型这类新式类型标注因此至少需要 Python 3.10。仓库的 codes/Dockerfile 中 Python 环境正是以apt-get install -y python3.10安装的与此吻合。运行方式很简单在项目根目录下执行python3 codes/python/chapter_computational_complexity/space_complexity.py文件的 Driver CodeL78-L90以n 5为输入依次调用六个阶数示例函数constant(n)、linear(n)、linear_recur(n)、quadratic(n)、quadratic_recur(n)、build_tree(n)最后用print_tree打印建出的满二叉树。由于n很小直接运行观察输出即可要观察“内存如何随 n 增长”则应借助下一节的 pythontutor 分步动画。pythontutor 动画注释逐指令观察内存生命周期codes/pythontutor目录是为在线 Python 教学演示器准备的一批注释文件codes/pythontutor/chapter_computational_complexity/space_complexity.md 中按函数顺序存放了 6 个 HTML 注释块标记格式为!-- [file]{space_complexity}-[class]{}-[func]{constant} --注释后紧跟一条经 URL 编码的演示器 render 链接其中内嵌了完整的 Python 源码文档站构建时会把该标记替换为可逐步执行的交互动画从而在可视化面板中观察每一行代码执行时的堆/栈状态。值得注意的实现细节由于演示器沙箱无法导入仓库的modules包内嵌代码是自包含的——constant演示在文件顶部内联定义了一个ListNode类字段为val与nextbuild_tree演示同样内联定义了TreeNodeval、left、right。此外演示版对数组规模做了收缩以便动画可读例如constant中写的是nums [0] * 10而仓库源码 L24 中为nums [0] * 10000——两者对空间阶数的结论完全一致因为固定长度数组都是 O(1)。六个演示对应的函数与源文件中的实现一一对应演示标记源文件函数空间阶数观察重点[func]{constant}constant()O(1)循环内变量与函数调用的空间不累积[func]{linear}linear()O(n)长度 n 的列表与哈希表[func]{linear_recur}linear_recur()O(n)递归深度 n 个栈帧并存[func]{quadratic}quadratic()O(n²)n×n 二维列表[func]{quadratic_recur}quadratic_recur()O(n²)递归中逐层分配递减数组[func]{build_tree}build_tree()O(2ⁿ)满二叉树节点数 2ⁿ−1推算方法只关注最差空间复杂度且以峰值内存为准空间复杂度的推算方法与时间复杂度大致相同只需将统计对象从“操作数量”转为“使用空间大小”。而与时间复杂度不同的是通常只关注最差空间复杂度。原因是内存空间是一项硬性要求必须确保在所有输入数据下都有足够的内存空间预留。“最差”有两层含义对应 docs 主文档中的示例def algorithm(n: int): a 0 # O(1) b [0] * 10000 # O(1) if n 10: nums [0] * n # O(n)以最差输入数据为准当n 10时空间复杂度为 O(1)但当n 10时初始化的数组nums占用 O(n) 空间因此最差空间复杂度为 O(n)以算法运行中的峰值内存为准程序执行最后一行之前只占 O(1) 空间但初始化数组nums时峰值为 O(n)因此最差空间复杂度仍按 O(n) 计。在递归函数中需要特别注意统计栈帧空间。docs 主文档给出了一组经典对照Python 版def function() - int: # 执行某些操作 return 0 def loop(n: int): 循环的空间复杂度为 O(1) for _ in range(n): function() def recur(n: int): 递归的空间复杂度为 O(n) if n 1: return return recur(n - 1)loop()与recur()的时间复杂度都是 O(n)但空间复杂度不同loop()在循环中调用了 n 次function()每轮调用都立即返回并释放栈帧因此空间复杂度仍为 O(1)。这正对应constant()函数中“循环中的函数占用 O(1) 空间”那段代码L29-L31recur()运行过程中会同时存在 n 个尚未返回的recur()调用层层压栈占用 O(n) 栈帧空间。五种常见空间复杂度阶数与代码证据设输入数据大小为 n常见空间复杂度类型从低到高为O(1) O(log n) O(n) O(n²) O(2ⁿ)即常数阶 对数阶 线性阶 平方阶 指数阶。常数阶 O(1)常数阶常见于数量与输入数据大小 n 无关的常量、变量、对象。仓库实现L20-L31def constant(n: int): 常数阶 # 常量、变量、对象占用 O(1) 空间 a 0 nums [0] * 10000 node ListNode(0) # 循环中的变量占用 O(1) 空间 for _ in range(n): c 0 # 循环中的函数占用 O(1) 空间 for _ in range(n): function()易错点nums [0] * 10000虽然元素很多但长度与 n 无关仍是 O(1)两个for循环中的变量c与function()调用每轮结束就释放不会累积整体仍是 O(1)。线性阶 O(n)线性阶常见于元素数量与 n 成正比的数组、链表、栈、队列等L34-L41def linear(n: int): 线性阶 # 长度为 n 的列表占用 O(n) 空间 nums [0] * n # 长度为 n 的哈希表占用 O(n) 空间 hmap dict[int, str]() for i in range(n): hmap[i] str(i)递归产生的线性阶同样由“栈帧深度”决定L44-L49def linear_recur(n: int): 线性阶递归实现 print(递归 n , n) if n 1: return linear_recur(n - 1)该函数递归深度为 n即同时存在 n 个未返回的linear_recur()调用使用 O(n) 大小的栈帧空间——这一点可以在 pythontutor 动画中逐指令看到调用栈随n递减逐层加深、再逐层弹出的过程。平方阶 O(n²)平方阶常见于矩阵和图元素数量与 n 成平方关系L52-L55def quadratic(n: int): 平方阶 # 二维列表占用 O(n^2) 空间 num_matrix [[0] * n for _ in range(n)]递归版本更能体现“空间随递归层层叠加”的机制L58-L64def quadratic_recur(n: int) - int: 平方阶递归实现 if n 0: return 0 # 数组 nums 长度为 n, n-1, ..., 2, 1 nums [0] * n return quadratic_recur(n - 1)递归深度为 n每层各自持有一个未释放的数组长度分别为 n、n−1、…、2、1总元素个数为等差数列求和 n(n1)/2故总体占用 O(n²) 空间。注意各层的nums因栈帧未返回而无法被回收这是它与“循环中分配定长数组O(1)”的本质区别。指数阶 O(2ⁿ)指数阶常见于二叉树L67-L74def build_tree(n: int) - TreeNode | None: 指数阶建立满二叉树 if n 0: return None root TreeNode(0) root.left build_tree(n - 1) root.right build_tree(n - 1) return root层数为 n 的满二叉树节点总数为 2ⁿ − 1每层节点数是上一层的 2 倍因此占用 O(2ⁿ) 空间。这里的空间增长来自堆上持续存在的节点对象TreeNode实例在树构建完成后仍全部存活直到返回给调用方。Driver Code 中的print_tree(root)会将其打印出来n5 时即 2⁵−1 31 个节点。对数阶 O(log n)docs 主文档指出对数阶常见于分治算法。例如归并排序输入长度为 n 的数组每轮递归将数组从中点划分为两半形成高度为 log n 的递归树使用 O(log n) 栈帧空间。再例如将数字转化为字符串输入正整数 n它的位数为 ⌊log₁₀ n⌋ 1对应字符串长度同为 ⌊log₁₀ n⌋ 1因此空间复杂度为 O(log₁₀ n 1) O(log n)。权衡时间与空间以空间换时间还是以时间换空间理想情况下我们希望算法的时间复杂度和空间复杂度都最优但实际情况中同时优化两者通常非常困难。降低时间复杂度通常以提升空间复杂度为代价反之亦然。牺牲内存空间来提升运行速度的思路称为“以空间换时间”反之称为“以时间换空间”。本章的示例本身就是一个小对照linear()中维护一个 n 项哈希表用 O(n) 额外空间换取后续按 key 的 O(1) 查找是典型的“以空间换时间”而linear_recur()不额外开辟数据容器却因递归调用链付出了 O(n) 栈帧空间——它提醒我们即使代码里没有显式数据结构递归深度本身也会消耗内存。选择哪种思路取决于更看重哪个方面。大多数情况下时间比空间更宝贵“以空间换时间”是更常用的策略但在数据量很大、内存受限时控制空间复杂度同样关键例如用遍历代替递归、用滚动数组代替完整二维 DP 表。小结空间复杂度的统计口径 暂存数据 栈帧空间 输出数据输入空间与指令空间通常不计。只算最差情况与峰值内存if n 10: nums [0] * n的最差空间复杂度为 O(n)。循环调用的空间不累积O(1)递归调用的栈帧会累积O(深度)这是 loop/recur 对照示例 的核心结论。五种常见阶数各有标志性来源定长容器 → O(1)正比容器 → O(n)矩阵 → O(n²)满二叉树 → O(2ⁿ)分治递归树 → O(log n)。仓库内可直接运行 codes/python/chapter_computational_complexity/space_complexity.py 验证行为并通过 codes/pythontutor/chapter_computational_complexity/space_complexity.md 的 6 段动画标记逐指令观察内存生命周期。【免费下载链接】hello-algo《Hello 算法》动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表