算法设计高效指南-全-

算法设计高效指南(全)

原文:annas-archive.org/md5/5f0ba566b86db0052b379e58b4789df4

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:前言

在本书的第二章中,我们探讨了“循环不变式”的概念,这指的是在一个变化的过程中保持不变的属性。 如果我们回顾自工业革命以来的人类进步,尽管有波动和不断变化,我们依然能够识别出一些不变的属性。 其中之一是我们推动自动化的动力,另一个是我们解决问题的系统化方法,这一方法随着数字计算机的发明而走向了正式化。 这就是我们现在 所称的算法。

本书是对算法领域的又一贡献。 它并不意味着取代那些我在阅读和教学中一直享受的现有杰出作品。 相反,它通过将理论概念与实际应用相结合,提供了一种学习算法的替代方法。 它还代表了我在研究、工业和学术界,特别是在计算机科学和人工智能领域,30 年经验的积累,以及 12 年持续教授算法 给学生的经验。

如果可能的话,我会把我的学生作为合著者,因为我从他们提出的挑战性问题中学到的东西,远超过我能教给他们的。 他们的提问推动我去发现每个算法方面背后的更深层次的解释和逻辑。 是的,我的确是指每一个方面。 算法的目的是使人类制造的机器正确运行,并提供我们预期的结果。 在这里,只有逻辑过程和行为 是可以接受的。

本书并不声称涵盖算法领域的每一个方面或主题。 相反,它将重点放在算法的设计与分析上,旨在解决我们在评估算法时必须始终考虑的核心问题。 首先,最重要的是,我们能否证明算法能正确运行并按预期执行? 确定算法能够可靠地为所有 可能的输入生成预期结果,这是至关重要的。

接下来,我们必须问自己,如何确保算法始终按照预期的方式运行。 这需要我们仔细审视其逻辑,理解其步骤,并确认每个部分都能为整体目标做出贡献。 最后,我们需要考虑算法是否能够在可接受的时间内终止。 无论算法设计得多么优雅,如果它不能高效地完成任务,那么它在现实世界中的应用就变得不切实际。 通过集中关注这些基本问题,本书旨在引导你设计出健壮、可靠且 高效的算法。

本书适合谁阅读

本书的主要读者群体是中级软件工程师、开发人员、信息技术专业人士以及研究科学家。 它作为一本实用手册,帮助读者在各种 专业领域有效理解和应用算法。

虽然本书最初并非作为教科书编写,但考虑到其缺乏习题——这些习题可能会在未来版本中加入——它提供了许多示例,能够增强学习效果。 我们认为它可以作为计算机科学、软件工程以及其他相关工程专业的学生,在“算法设计与分析”课程中的主要或辅助教材。 作者在教授这些课程已有十多年,写这本书是为了应对在讲解复杂的 算法概念时经常遇到的挑战。

虽然本书主要面向对算法有所了解并且有数学或工程背景的读者,但某些章节具有更广泛的吸引力。 第一章 第五章,以及 第八章、第九章 第十四章讨论了有趣的现实决策场景,使这些章节对于技术领域之外的更广泛受众也具有可读性和吸引力。

本书内容

第一章**, 算法分析导论, 提供了关于算法的基础性理解,将其视为有结构和系统化的解决问题工具。 它介绍了算法设计的基本概念,并探讨了计算机系统的双重特性,区分硬件和软件。 本章还强调了算法分析的重要性,聚焦于正确性和效率,为后续章节更深入的算法技术探索奠定了基础。 随后的章节将进一步展开这一主题。

第二章**, 算法正确性的数学归纳法与循环不变式, 讨论了证明算法正确性所需的数学基础。 它将数学归纳法作为一种基本技巧,并通过循环不变式扩展这一概念,以确保迭代过程的可靠性。 本章提供了实际的示例,以说明这些原理,为验证算法提供了坚实的基础,确保它们在 所有场景下产生正确的结果。

第三章**, 复杂度分析中的增长率, 探讨了算法效率的概念以及运行时间如何随着输入规模的变化而变化。 本章介绍了各种增长率,从常数时间到阶乘时间,并解释了如何使用渐近符号(如大 O 符号、Ω符号和Θ符号)来表示和比较它们。 本章为理解和预测算法性能奠定了基础,帮助你在为 不同任务设计或选择算法时做出明智的决策。

第四章**, 递归与递归函数, 介绍了递归函数的概念,这是分析递归算法复杂度的关键。 本章探讨了两种主要类型的递归函数:减法型和分治型,详细讲解了它们如何影响算法的效率。 通过像归并排序和二分查找这样的实际例子,展示了递归关系在 现实场景中的应用。

第五章**, 递归函数求解, 详细讨论了分析递归算法性能的技巧。 本章介绍了关键方法,如代入法、主定理和递归树,用于求解在分治法和其他递归算法中出现的递归关系。 本章解释了如何应用这些方法来确定算法的渐近复杂度,并突出了 每种方法的优缺点。

第六章**,排序算法, 探讨了将数据按特定顺序排列的基本技术,全面概述了迭代和递归排序方法。 本章从详细分析基于比较的算法开始,重点介绍其操作原理和时间复杂度。 接着,进一步探讨了高级排序技术,如归并排序,解释了分治法及其对稳定性和效率的影响。 此外,本章介绍了非基于比较的 排序算法。

第七章**,搜索算法, 首先考察了不同类型的搜索算法,包括线性和亚线性方法,突出它们的特点和性能。 接着,本章介绍了哈希的概念,解释了哈希函数如何实现常数时间的 搜索操作。

第八章**,排序与搜索的共生关系, 概述了排序和搜索算法之间的相互关系。 本章考察了排序如何显著提高搜索操作的效率,并探讨了排序在特定情况下的优势。 本章通过真实世界的例子,全面分析了排序成本与更快搜索所带来的好处之间的权衡,突出了 这些动态关系。

第九章**,随机化算法, 讨论了在算法设计中使用随机性,解决确定性方法可能难以高效解决的问题。 通过多种案例研究,包括招聘问题和生日悖论,本章展示了随机化算法如何有效应对不确定性并增强 决策过程。

第十章**,动态规划, 从动态规划与其他方法(如分治法和贪心算法)之间的基本区别开始讲解。 本章通过详细的实例介绍了动态规划。 本章深入理解了动态规划的优势与局限,为在各种 问题解决场景中的实际应用做好准备。

第十一章**, 数据结构的概貌, 探讨了不同类型的数据结构,如线性和非线性、静态和动态,以及支持顺序访问和随机访问的结构,这些结构显著影响算法性能。 这些基础知识为进一步深入探讨特定数据结构及其算法应用铺平了道路。 随后的章节。

第十二章**, 线性数据结构, 深入探讨了数组、链表、栈、队列和双端队列等基本结构。 本章将引导您了解每种数据结构的基本操作和特性,突显它们的独特优势和权衡。 还介绍了跳表等高级概念。

第十三章**, 非线性数据结构, 讨论非线性数据结构。 本章首先探讨了区分非线性结构与线性结构的一般特性。 涵盖了两大类别——图和树——分析它们的类型、特性和应用。 本章最后介绍了堆,一种特殊形式的二叉树,展示了它们在排序算法和 优先队列中的重要性。

第十四章**, 明日算法, 探讨正在定义计算未来的前沿趋势。 本章将这些新兴趋势分为三个关键领域:可扩展性、上下文感知和道德责任。 探讨了算法如何演变以应对大规模数据处理的需求、适应动态环境,并 以道德方式运作。

为了从本书中获取最大收益

为了有效使用本书中的代码,你需要安装最新版本的 Python 和适合的集成开发环境 (IDE) 来开发和测试代码。 我们推荐使用 Anaconda Python 环境,因为它易于使用,并且具有全面的包管理功能。 Anaconda 附带了许多预安装的库和工具,这些库和工具对于算法开发和分析非常有用。 你可以从官方 网站下载并安装 Anaconda:https://www.anaconda.com/download

安装完成后,你可以使用 Anaconda 的 IDE(Spyder)或 Jupyter Notebook 来运行本书中提供的代码示例。 这两个工具都提供了交互式环境,非常适合学习和实验算法。 此外,你还可以尝试其他 IDE,如 PyCharm 或 Visual Studio Code,它们提供了强大的功能用于 Python 开发。

如果你使用的是本书的数字版,我们建议你亲自输入代码,或者从本书的 GitHub 仓库中获取代码(链接将在下一节提供)。 这样做可以帮助你避免复制粘贴 代码时可能出现的错误。

下载示例代码文件

你可以从 GitHub 上下载本书的示例代码文件,地址是 https://github.com/PacktPublishing/Efficient-Algorithm-Design。如果代码有更新,将会在 GitHub 仓库中更新。

我们还有其他的代码包,来自我们丰富的图书和视频目录,可以在 https://github.com/PacktPublishing/找到。 快去看看吧!

使用的约定

本书中使用了若干文本约定。

<st c="11761">文中的代码</st>:表示文本中的代码字、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter/X 账号。 例如:“在上面的函数中,运行时间取决于 <st c="12029">for i in range(1, n + 1):</st> 循环 执行的次数。”

一段代码块的格式如下:

 def dp_fib(n, memo={}):
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    memo[n] = dp_fib(n-1, memo) + dp_fib(n-2, memo)
    return memo[n]

任何命令行输入或输出格式如下:

 pip install networkx matplotlib

粗体:表示新术语、重要词汇或屏幕上出现的词语。 例如,菜单或对话框中的词语会显示为 粗体。例如:“在 第十三章中,我们将讨论基于一种特定数据结构的搜索算法,称为 二分查找 (BSTs)。”

提示或重要说明

呈现如下。

联系我们

我们非常欢迎读者的反馈。 您的反馈对我们非常重要。

一般反馈:如果你对本书的任何方面有疑问,请通过电子邮件联系我们,邮箱地址为 customercare@packtpub.com 并在邮件主题中注明书名。

勘误:虽然我们已尽力确保内容的准确性,但仍可能出现错误。 如果你在本书中发现错误,请向我们报告。 请访问 www.packtpub.com/support/errata 并填写 表单。

盗版:如果你在互联网上发现我们作品的任何非法复制品,请提供该位置地址或网站名称,我们将不胜感激。 请通过 copyright@packt.com 与我们联系,并提供该材料的链接。

如果你有兴趣成为作者:如果你在某个领域拥有专长,并且有兴趣撰写或参与撰写一本书,请 访问 authors.packtpub.com

分享你的想法

一旦你阅读了 高效算法设计,我们很希望听到你的想法! 点击这里直接访问 Amazon 评论页面 并分享 你的反馈。

您的评价对我们以及技术社区至关重要,能够帮助我们确保提供优质的 内容。

下载本书的免费 PDF 副本

感谢购买 本书!

你喜欢在路上阅读但无法随身携带印刷版的 书籍吗?

你购买的电子书与你选择的设备不兼容吗?

别担心,现在每本 Packt 的书都可以免费获得不带 DRM 的 PDF 版本。

随时随地,在任何设备上阅读。 直接从你喜爱的技术书籍中搜索、复制和粘贴代码到 你的应用程序。

福利不止于此,你还可以在你的 收件箱每天获取独家折扣、新闻简报和优质免费内容

按照以下简单步骤获取 这些福利:

  1. 扫描二维码或访问下面的 链接

https://packt.link/free-ebook/9781835886823

  1. 提交你的购买 凭证

  2. 就这样! 我们将把免费的 PDF 和其他福利直接发送到你的 电子邮箱

第二章:第一部分:算法分析基础

本部分为理解和分析算法奠定基础。 它从基本概念开始,重点讲解如何证明算法的正确性和衡量算法的效率。 你将学习到重要的技术,如数学归纳法、循环不变式和复杂度分析,为更 深入的主题做铺垫。

本部分包括以下章节:

  • 第一章 算法分析导论

  • 第二章 算法正确性中的数学归纳法和循环不变式

  • 第三章 复杂度分析中的增长速率

  • 第四章 递归与递推函数

  • 第五章 递推函数求解

第三章:1

算法分析简介

本书的目标是揭开算法的神秘面纱,使其对希望增强对算法设计、分析和在各个技术领域应用的理解的读者更加易于理解和操作。 尽管本书是为软件工程师、计算机科学家及其他熟悉算法并渴望提升技能的专业人士设计的,但它也具备足够的深度和资源,可以通过更多的实践和努力为早期职业人士提供一个良好的起点。 通过探索理论基础和实际应用,本书旨在弥合学术研究与现实世界 技术应用之间的差距。

在本章开篇中,我们探讨了算法的本质,将其定义为计算和其他领域中解决问题的重要、结构化的工具。 我们通过详细分析硬件与软件的二分法以及它们的独特特性,探讨了算法在学术研究和实际应用中的重要性,尤其是在硬件日益廉价的快速发展的技术环境中,算法分析变得尤为重要。 本章为你提供了一个基础的路线图,帮助你通过复杂的算法概念,逐步实现全面的理解,为更高阶的主题和实际应用做好准备。 这段介绍标志着深入学习算法分析及 其应用的旅程的开始。

在本章中,我们将讨论以下 主要主题:

  • 算法 和问题解决

  • 算法分析的理论依据

  • 算法分析的双重维度——效率 和正确性

理解算法和问题解决

法国哲学家和数学家勒内·笛卡尔(1596-1650)以其心身二元论理论闻名。 他提出,心灵和身体是两种根本不同的物质:心灵是一个非扩展的、思考的实体,而身体是一个扩展的、非思考的实体。 笛卡尔认为这两种物质相互作用,同时保持它们的独立性和存在。 这种二元论观点强调了心灵(思想)和身体(物质)两个方面的分离,认为它们在本质上是不同的。 他的理论对关于意识及心身关系的哲学讨论产生了重要影响。 并且对心灵与身体之间的关系提出了深刻的思考。

但为什么我们 在讨论算法时要从笛卡尔的 二元论理论开始,尽管它受到了哲学家的严重批评? 答案在于该模型能够帮助我们理解算法的本质及其在人类所有发明中的独特性。

计算机的历史叙述,主要被视为通过数学表示来自动化问题解决的设备,始于硬件和软件之间的 明显分离。硬件是执行软件代码并产生预期结果的有形物理组件。 相对而言,软件是通过一种称为计算机程序的正式语言表达的系统化解决方案,从高层语言如 Python 和 C++到低层语言如汇编语言和 机器代码。

然而,硬件 和软件本质上是不同的,这一点在计算机系统中尤为显著。 主要的区别在于它们各自所依赖的学科。 计算机硬件受物理法则支配,决定了物理组件如何操作和相互作用。 相反,计算机软件操作在数学领域内,受数学法则支配,决定了软件能够执行的逻辑、算法和功能。 这种二分法使计算机系统与其他人类制造的技术区分开来。 例如,一辆车完全受物理法则的支配,无论是在微观还是宏观层面。 硬件和软件的一个显著区别是硬件是有生命的。 它容易腐蚀、出现缺陷和过时。 相反,软件是永恒的,不受折旧、老化、缺陷或过期的影响。 这一概念与笛卡尔的 二元论理论*相吻合。

软件的本质体现了一个抽象的概念,称为 算法。算法代表了一组抽象的规则或程序,这些规则或程序可以通过多种编程语言以不同方式实现和表达。 尽管表示方法多种多样,但这些不同的算法实现都旨在产生统一、一致的输出。 算法的这一特性,即抽象而又在实现上具有多样性,使它们成为软件开发和设计的基本要素。 在现实世界中,与算法最相似的概念是食谱和音乐符号。 这两者都代表了一个 计划 的逐步实现,用以创造食物 或音乐。

一个好食谱的标志,除了能做出美味的食物外,还体现在它以定量和抽象的形式表达,使得任何厨师,无论经验如何,都能执行它。 然而,这通常并非完全可行,因为食谱通常是用自然语言写成的,容易引发各种解读。 另一个好食谱的理想特征是它不依赖于特定的厨房设备,尽管这一点也并非总是现实的。 食谱,像算法一样,旨在实现其应用的普遍性,但语言的细微差别和特定情境会影响它们的可复制性 和结果。

音乐符号的情况稍微更为有利。 由于这些符号类似于形式化语言,它们提供了一种更清晰、更标准化的教学方法。 然而,实际演奏的音乐仍然受到演奏者的诠释、乐器特性以及众多声学因素的影响。 虽然音乐符号比食谱的自然语言提供了更精确的指导,但表演的可变性和环境条件意味着结果仍然可能会有显著差异。 这突显了将抽象、形式化的指令转化为一致的现实世界结果的挑战,类似于在不同的 计算环境中执行算法。

这两个 例子帮助我们推测出算法的关键特性: 算法的特性:

  • 程序员独立性:理想情况下,算法的最终产品应当在很大程度上独立于谁来实现它。 这意味着,不管程序员是谁,算法都应始终产生相同的正确结果,且计算成本或资源使用在 不同的实现中应该大致相似。

  • 硬件独立性:一个有效的算法应尽可能独立于运行它的硬件。 它应能够在各种硬件平台上产生一致的结果,而无需重大修改或依赖于特定的 硬件特性。

  • 抽象性与清晰性:算法应当是抽象且明确的,不留任何解释的余地。 这种清晰性确保了算法能够被理解 并且始终如一地实施,无论程序员的主观理解 或方法如何。

  • 可量化的正确性与成本:算法的正确性——其产生预期结果的能力——和其成本,包括时间和内存等计算资源,必须是可量化的。 这使得可以根据算法的效率 和有效性对不同算法进行客观评估和比较。

算法是 按步骤、程序化、并且通常是迭代的方法,用于解决问题。 然而,至关重要的是要理解,它们只被设计用来解决 可计算 的问题。 这意味着,为了使一个问题能够通过算法解决,它必须是一个能够通过一系列逻辑和数学步骤解决的问题。 本质上,一个可计算的问题是一个可以通过算法系统地解决的问题,算法提供了一种清晰和有组织的方法来找到 解决方案。

一个经典的 例子是 不可计算问题 停机问题。由 艾伦·图灵提出,这个问题询问是否有可能创建一个算法,能够判断任何给定的程序及其输入是否会停机(停止运行)或会无限运行下去。 图灵证明了没有这样的算法存在;对于一个通用算法来说,预测所有可能的程序-输入组合的行为,并确定它们是否会停机,是不可能的。 这是因为该算法必须考虑无限多种可能的程序行为,而这是 不可行的。

可计算问题的经典例子是排序一个数字列表。 例如,给定一个数字列表,如 <st c="8840">[3, 1, 4, 1, 5, 9, 2]</st>,一个排序算法可以将这些数字重新排列成特定的顺序,如升序: <st c="8951">[1, 1, 2, 3, 4, 5, 9]</st>。排序问题是可计算的,因为它们有一个明确定义的过程或步骤,可以遵循这些步骤来实现排序列表,而且这个过程适用于任何有限的数字列表。 数字。

在建立了可以使用算法解决的基本问题类型的理解后,我们现在准备探索算法设计中采用的主要问题解决方法。 然而,首先需要解决一个常见的误解。 一些教科书将启发式方法和算法方法呈现为对立的,但它们之间的关系更偏向于互补 而非矛盾。

启发式方法,正如我们将在 第十章中讨论的那样,提供了实用且常常更快的解决方案,这些方案根植于经验、直觉或常识性规则。 它们的主要优势在于速度和实用性,但这也伴随着一定的权衡:启发式方法并不总是能够保证最优或正确的解决方案。 另一方面,算法方法则是遵循一组定义明确、结构化步骤的方法,通常能够为手头的问题提供最优解。 基于数学和逻辑程序,算法方法提供了可预测性、可重复性和保证的结果,在精度至关重要的情况下尤为可靠。 正如我们将在 第十章中详细探讨的那样,启发式方法和算法方法之间存在一种共生关系。 尽管各自具有优势和劣势,但它们往往可以相互补充,有效地解决各种问题。 理解何时以及如何使用每种方法是算法设计艺术中的关键技能。

算法分析的理由

在过去的二十年中,我们见证了先进计算系统的非凡进展,特别是在人工智能(AI)、机器学习、深度学习、机器人技术和计算机视觉等领域。 这一进步更多地归因于科技界和信息社会中两次重大革命,而不是算法设计的改进。 第一次革命是 1991 年互联网公开发布后大量数据的可获得性。 第二次是硬件的巨大进步,包括 图形处理单元(GPU)等强大且价格适中的处理器的开发,以及超高容量的存储解决方案。

尽管计算资源在这些显著改进中取得了突出进展,但算法的研究和分析对于 几个原因仍然至关重要:

  • 算法正确性:无论计算资源多么丰富,必须在所有场景下数学证明算法的正确性。 通过实例展示有效性并不足够;需要数学框架来进行严格的证明。 这确保了算法在各种条件 和输入下的可靠性。

  • 算法效率:算法分析是理解不同算法效率的关键。 通过检查时间和空间复杂度,我们可以选择或设计不仅更快而且更具资源效率的算法。这在资源有限的环境中,或处理大数据集时尤为重要。 此外,当多种算法可用于同一任务时,分析有助于做出明智的决策,选择哪种算法 来使用。

  • 更好的解决问题能力:深入了解算法的功能及如何分析它们,能够提高解决问题的能力。 这包括有条理地将问题分解为更小的部分,并设计最优解决方案,这项技能在计算以外的许多领域同样具有价值

  • 理解局限性:理解算法的局限性与找到最有效的解决方案同样重要。 算法分析帮助识别算法能够或不能有效解决的问题,这在处理时间、内存或特定数据 结构限制时至关重要。

  • 为未来挑战做准备:技术和数据的格局持续演变,规模和复杂性不断增长。 在算法分析方面的坚实基础,使我们能够有效地应对并解决这些新兴的 计算挑战。

因此,我们可以说,尽管技术进步显著提高了计算能力,但算法分析的重要性依然存在,确保我们继续开发不仅快速,而且正确、高效、适应现代问题不断变化复杂性的解决方案。

让我们进行一个思想实验。假设在计算资源无限的情况下,拥有极其快速的处理器和几乎免费的内存。考虑一个包含大量元素的数组,A,其中包含非常多的元素,n。我们的目标是将这些元素按升序排列。为了简化算法设计和分析的复杂性,我们可能会选择生成所有可能的* A *排列,并检查每一个排列是否已经排序。这种暴力破解方法需要生成 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo!</mml:mo></mml:mrow></mml:mfenced></mml:math> 种排列。 然而,即使是最基础的排序算法,例如冒泡排序,其复杂度也为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>,而更先进的排序算法可以提供更高的效率。这个例子说明了深入理解算法分析如何显著改变我们解决问题的方式,强调即便在计算资源丰富的世界里,设计高效算法的重要性。

在这本书中,我们探讨了四种主要的算法问题解决方法,每种方法都有其独特的优点、局限性以及在不同类型问题中的适用性:

  • 顺序或直接方法:这种方法是最基本的解决问题的形式。它涉及一系列线性指令,通常包括循环和决策点。虽然顺序算法相对容易测试和调试,但它们可能会计算开销较大,从而在处理复杂任务时效率较低。

  • 分治法:这种方法通过将问题分解为更小、更易处理的子问题来解决问题。 每个子问题都独立求解,然后它们的解决方案被组合起来解决原始的更大问题。 然而,将顺序算法转化为分治策略并不总是有利的。 一个典型的例子是阶乘问题,其中顺序方法更为简单(参见 第四章)。

  • 动态规划:动态规划通过将问题分解为较小的子问题来解决它,递归地解决每一个子问题。 动态规划的一个关键要求是子问题必须有重叠,这样方法才能有效地重用先前计算的解决方案。 这种方法的局限性在于它需要具有重叠子问题的必要性,这种情况并非总是存在(参见 第十章)。

  • 贪婪算法:贪婪算法专注于从一组可能解决方案中找到最优解。 它们在每一步都做出最佳选择,旨在达到全局最优。 贪婪算法的挑战在于,它们不一定始终导致最佳的整体解决方案,因为在每一步做出局部最优选择并不一定会导致全局最优解(参见 第十章)。

在本书的后续章节中,我们将详细探讨每种问题解决方法的优点、局限性和实际应用。 我们的探索将集中于比较这些算法的一个关键标准: 计算成本。

既然我们已经从这本书中确立了我们的期望,主要问题仍然是:我们从算法分析中寻求什么? 虽然“分析”可以涵盖广泛的概念,在这个语境下,它特指设计高效算法的关键目标。 首先,我们的目标是开发绝对正确的算法,即它们始终产生预期结果。 其次,我们努力设计尽可能成本效益的算法。 因此,在算法分析过程中,我们集中于两个关键维度:正确性 和成本。

算法分析的双重维度——效率与正确性

算法分析的目标确实是双重的。 首先,它旨在确定算法的正确性。 一个算法被认为是正确的,如果它始终解决预期的问题,并为所有有效输入产生正确的输出。 这种正确性取决于两个 关键标准:

  • 终止性:一个算法必须在有限的步骤后得以结束。 它不应陷入无限循环或无休止地运行,无论提供什么样的输入。

  • 有效性:算法必须对每个可能的输入产生预期的结果或有效的解决方案。 它需要精确地遵循问题的规范和 要求。

有趣的是,仅仅通过使用大量正面示例来测试算法的正确性,是无法得出最终结论的。 虽然许多成功的测试用例可能暗示算法是正确的, 但只需要一个反例就能推翻它。 这种方法被称为间接证明,它在算法分析中扮演着至关重要的角色。

算法分析的第二种方法涉及 直接证明方法,例如归纳推理或数学归纳法。 这种方法需要确认算法在基本案例(通常是最简单的输入)上正确运行,并证明如果它对一个任意案例有效,则它会继续对随后的案例有效。 另一个证明正确性的关键方法是循环不变式条件,它在算法的每次循环迭代中建立了某些条件。 这通常是证明算法正确性的主要方法。

第二章中,我们将深入探讨算法正确性的概念。 我们将详细探讨这些证明方法,提供一个全面的理解,了解如何建立和验证算法的正确性。

算法分析的第二个关键方面是对算法效率的评估,这在确定算法在不同条件下的表现如何有效地执行时至关重要。 算法的效率主要通过两个主要标准来衡量:时间和空间。 时间效率指的是算法完成执行所需的计算时间。 空间效率涉及算法完成任务所需的内存量:

  • 时间效率或计算复杂度:这与算法解决问题所需的时间有关,特别是当输入数据的大小增长时。 了解一个算法的时间复杂度至关重要,以便确定它如何高效地处理日益增长的 大数据集。

  • 空间效率:这指的是一个算法在执行过程中所需要的内存量。 估算内存使用量至关重要,尤其是在数据密集型任务或内存资源有限的环境中。 内存资源。

为了分析这些方面,我们采用算法理论,也称为 渐近分析。渐近 表示法,如大 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Θ</mml:mi></mml:math>,和 Ω为描述算法时间和空间复杂度相对于输入大小的行为提供了框架。 这些表示法提供了一种方式,表达算法资源需求的上下限,使我们能够在 不同条件下对其效率进行理论评估。

第三章中,我们将详细探讨算法理论和渐近表示法。 这将使我们具备评估和比较不同 算法效率的工具,这是根据算法的 性能特征选择合适算法的重要技能。

需要注意的是,在评估算法效率时,确实还存在其他维度需要考虑,尤其是在现代计算和多样化应用环境的背景下:

  • 电池和能耗:在移动应用中,算法的效率可以显著影响电池寿命。 要求较少处理能力的算法有助于节省电池,这是移动计算中一个至关重要的因素。

  • 数据传输和网络访问:对于需要数据传输和网络连接的算法,传输的数据量和网络访问的频率成为关键的效率因素。 这在网络带宽有限或成本高昂的应用中尤为重要

  • 基于云的服务:在算法严重依赖云服务的场景中,必须考虑与这些服务相关的成本。 这不仅包括计算成本,还包括云环境中的数据存储和传输成本。

  • 人工标注在人工智能和机器学习中的作用:某些算法,特别是在人工智能和机器学习中,可能需要人工标注或干预。 这一过程所涉及的时间和精力也可能成为整体效率和实用性的关键因素。 这些算法的效率。

尽管这些方面在不同的背景下确实重要,但本书的主要焦点是从时间和空间角度讨论算法的效率。 这一重点使得对这两个基本且普遍适用的标准进行更深入的探索成为可能,为理解和评估各种应用中算法的性能提供了坚实的基础。

总结

本章介绍了算法的基本性质,强调了它们作为结构化和系统化工具在各个领域中有效解决问题的作用。 本章首先给出了全面的定义,并探讨了算法的不同方面,包括它们对硬件和软件双重性质的依赖,分别受物理法则和数学原理的影响。 本章重点讨论了算法分析的重要性,揭示了评估算法的必要性,尤其是在硬件变得更易获得且成本更低的情况下。 本章为你提供了通过引导和结构化的学习路径来探索算法的复杂性和应用的基础。 这一引言章节为掌握算法分析和应用的艺术奠定了基础。 下一章将建立每个软件从业者在有效的 算法分析中所需的基本数学基础。

第四章:2

数学归纳法与算法正确性中的循环不变式

笛卡尔曾著名地说:“对我来说,一切都归结为数学,”强调了数学与各种智力追求之间的深刻联系。 这一观点在计算机科学领域尤为相关,因为算法的设计、效率和验证深深植根于数学原理之中。 在本章及下一章中,我们将探讨支撑算法分析的数学基础。 我们将讨论这些数学概念如何不仅指导算法解决方案的开发,而且确保它们的有效性和准确性。 本讨论旨在提供对数学在算法设计与分析艺术和科学中不可或缺的理解 和分析。

在本章中,我们探讨了数学归纳法的概念及其在建立循环不变式中的关键作用,循环不变式是评估算法正确性的框架,构成了证明算法正确性的基础。 通过一系列详细的例子,我们解释了循环不变式的概念,展示了它们的功能及其重要性。 本讨论为后续章节中应用这些概念奠定了基础,增强了你严谨验证算法正确性技巧的能力。 这一探索不仅为理论框架提供了支持,也为实践提供了洞见,赋予了你设计和分析算法的基本工具。 在本章中,以下主题将 被探讨:

  • 数学归纳法

  • 算法正确性的循环不变式 的数学归纳法

数学归纳法

寻找 反例是质疑某些命题或算法正确性时的一种策略。 这种方法在传统证明正确性的方法难以应用时特别有用。 反例本质上是一个特定的实例或案例,用来展示一个命题的虚假性或算法的错误性。 当一个证明显得复杂或算法背后的逻辑错综复杂时,识别反例可以提供一种更直接的方式 来驳斥该命题。 这种方法也称为 间接证明

通常,反例可以很容易地观察到。 在这种情况下,它是快速有效地否定假设的一种方式,避免了冗长复杂的证明。 然而,也有一些情况下,找到反例是一项挑战。 如果经过认真寻找后,仍然难以找到反例,这可能表明通过直接证明正确性来解决问题更为合适。 然而,需要注意的是,无法找到反例并不能本质上证明算法的正确性。 仅仅因为反例难以找到或不明显,并不意味着算法是有效的。 这突显了在算法开发和评估过程中严格分析和测试的重要性,确保正确性的声明不会在没有经过彻底检查的情况下被轻易接受。 现在,让我们来看一下 以下示例。

示例 2.1:

证明或反驳: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mo∀</mml:mo>mml:mia</mml:mi>mml:mo></mml:mo>mml:mn1</mml:mn><mml:mi mathvariant="normal"> </mml:mi>mml:mo∀</mml:mo>mml:mib</mml:mi>mml:mo></mml:mo>mml:mn1</mml:mn>mml:mo:</mml:mo><mml:mi mathvariant="normal"> </mml:mi><mml:mfenced separators="|">mml:mrowmml:mia</mml:mi>mml:mo+</mml:mo>mml:mib</mml:mi></mml:mrow></mml:mfenced>mml:mo!</mml:mo><mml:mi mathvariant="normal"> </mml:mi>mml:mo=</mml:mo><mml:mi mathvariant="normal"> </mml:mi>mml:mia</mml:mi>mml:mo!</mml:mo><mml:mi mathvariant="normal"> </mml:mi>mml:mib</mml:mi>mml:mo!</mml:mo><mml:mi mathvariant="normal"> </mml:mi></mml:math>

a=2 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi>mml:mo=</mml:mo>mml:mn3</mml:mn></mml:math>。然后,我们必须证明 以下内容:

2+3!=2!*3!

这是错误的。

如前所述,间接证明无法保证关系或算法的有效性。 这突显了在算法分析中 使用 直接证明技术 的必要性。 与间接证明不同,间接证明通过寻找反例来推翻假设,而直接证明则是通过积极证明一个关系或算法在所有 可能条件下都成立。

例如,在 间接证明中,找到一个反例就足以使该命题无效。 然而,在直接证明中,特别是当涉及到自然数时,必须考虑从 n=0 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mi∞</mml:mi></mml:math>的所有自然数。这种穷举法需要对范围和 n的类型有明确的定义。在算法分析的上下文中, n 通常代表我们在证明过程中所检查的案例数。 由于案例数不能是分数,必须始终是一个整数, n 被定义为自然数。 此外, n 的范围从 0 扩展到无穷大,以确保考虑到所有可能的情景,验证不存在可以推翻算法正确性的情况。 这一严格的方法确保了算法在所有可想象的情况下的可靠性得到了全面验证。 这正是 数学归纳法的最终目标。

回溯

概念 数学归纳法 有着超过 3000 年的丰富历史。 数学归纳法背后的原理是,如果一个假设在某个范围的边界上成立,并且在该范围内继续成立,那么这个假设可以被认为在整个范围内有效。 这种方法对于证明数学序列特别有效,并且与 计算机算法直接相关。

在计算机算法的背景下,数学归纳可以被看作是验证算法正确性的强大工具。

算法可以被视为一个复杂的序列,其中输入,<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>,表示案例编号或循环索引,序列体现了算法的功能期望。

在数学归纳的过程中,证明从一个边界开始,通常是当<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo=</mml:mo>mml:mn0</mml:mn></mml:math>时。从那里开始,任务是证明如果假设对某个任意自然数<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo=</mml:mo>mml:mik</mml:mi></mml:math>有效,则对<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo=</mml:mo>mml:mik</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:math>也是真实的。一旦对k(这可以是任何自然数)建立了这一点,它有效地证明了所有自然数<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi><mml:mi mathvariant="double-struck"> </mml:mi>mml:mo∈</mml:mo><mml:mi mathvariant="double-struck">N</mml:mi></mml:math>的假设。这种一步一步的方法,从一个自然数到下一个自然数的进展,强调了数学归纳在纯数学和算法设计中的可靠性和适用性。

  1. 基础案例(初始步骤):这 包括评估并证明假设对于最小的<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mi </mml:mi>mml:min</mml:mi></mml:math>值成立,通常是 n=0或者 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math>,具体取决于问题的要求。 在算法设计的上下文中,基础案例通常是 n=1,因为测试算法通常从第一个可能的、非空的实例开始。 此步骤确立了假设在 序列起始点的有效性。

  2. 归纳步骤:这一 步骤要求证明,如果假设对于某个任意的案例编号有效 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>,那么它对于<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mi </mml:mi>mml:min</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:math>依然有效。这个过程,通常称为假设或归纳假设,涉及通过证明某个案例的假设成立,从而逻辑地推断出下一个案例的假设也成立。 通过成功地展示这一点,我们可以确定假设对于所有后续案例成立,直到 无穷大。

在示例 2.2中,我们逐步演示了数学归纳法的过程。

示例 2.2

使用数学归纳法证明 以下命题。

前几个自然数的和由以下公式给出:

1+2+3+…+n=nn+12

证明:

  1. 基础情况: 对于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mi </mml:mi>mml:mo=</mml:mo>mml:mi </mml:mn>1</mml:math>,序列只包含一个元素,即 1\。 根据 公式:

    <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mn1</mml:mn><mml:mi mathvariant="normal"> </mml:mi>mml:mo=</mml:mo>mml:mfracmml:mrowmml:mn1</mml:mn><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac>mml:mo=</mml:mo>mml:mfracmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac>mml:mo=</mml:mo><mml:mi mathvariant="normal"> </mml:mi>mml:mn1</mml:mn></mml:math>

    基础情况成立,因为方程的两边 相等。

  2. 归纳步骤: 假设命题对于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mi </mml:mi>mml:mo=</mml:mo>mml:mi </mml:mi>mml:mik</mml:mi></mml:math>;即假设 满足 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn1</mml:mn>mml:mo+</mml:mo>mml:mn2</mml:mn>mml:mo+</mml:mo>mml:mn3</mml:mn>mml:mo+</mml:mo>mml:mo…</mml:mo>mml:mi </mml:mi>mml:mo+</mml:mo>mml:mik</mml:mi>mml:mi </mml:mi>mml:mo=</mml:mo>mml:mi </mml:mi>mml:mfracmml:mrowmml:mik</mml:mi>mml:mi </mml:mi><mml:mfenced separators="|">mml:mrowmml:mik</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:math>

现在,证明它适用于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mi </mml:mi>mml:mo=</mml:mo>mml:mi </mml:mi>mml:mik</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:math>。因此,考虑 和:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mn1</mml:mn>mml:mi </mml:mi>mml:mo+</mml:mo>mml:mi </mml:mi>mml:mn2</mml:mn>mml:mi </mml:mi>mml:mo+</mml:mo>mml:mi </mml:mi>mml:mn3</mml:mn>mml:mi </mml:mi>mml:mo+</mml:mo>mml:mi </mml:mi>mml:mo…</mml:mo>mml:mo+</mml:mo>mml:mi </mml:mi>mml:mik</mml:mi>mml:mi </mml:mi>mml:mo+</mml:mo>mml:mi </mml:mi><mml:mfenced separators="|">mml:mrowmml:mik</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>

根据归纳假设,前面几个数的和是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mi </mml:mi>mml:mik</mml:mi>mml:mi </mml:mi></mml:math>数字之和是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mik</mml:mi><mml:mfenced separators="|">mml:mrowmml:mik</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:math>

加上 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn>mml:mi </mml:mi></mml:math>到两边,得到 以下结果:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mn1</mml:mn>mml:mo+</mml:mo>mml:mn2</mml:mn>mml:mo+</mml:mo>mml:mn3</mml:mn>mml:mo+</mml:mo>mml:mo…</mml:mo>mml:mo+</mml:mo>mml:mik</mml:mi>mml:mi </mml:mi>mml:mo+</mml:mo><mml:mfenced separators="|">mml:mrowmml:mik</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mi </mml:mi>mml:mo=</mml:mo>mml:mi </mml:mi>mml:mfracmml:mrowmml:mik</mml:mi>mml:mi </mml:mi>mml:mo×</mml:mo><mml:mfenced separators="|">mml:mrowmml:mik</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac>mml:mo+</mml:mo>mml:mi </mml:mi><mml:mfenced separators="|">mml:mrowmml:mik</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math> =k×k+1+2k+12=k+1k+2</mml:mrow>

这表明 如果命题在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo=</mml:mo>mml:mik</mml:mi></mml:math>时为真,那么它对于 也为真 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mi </mml:mi>mml:mo=</mml:mo>mml:mi </mml:mi>mml:mik</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:math>时也成立。

你可能会好奇数学归纳法与算法正确性之间的联系。 为了阐明这一点,我们首先需要审视算法的架构。 从根本上讲,算法是一种“序列”——就像烹饪食谱,它是一系列旨在解决问题的指令。 每个算法都包含不同类型的指令,通常可以广泛分类为 以下几类:

  • 简单命令:这些是 不依赖于输入大小的指令。 例如,基本的赋值操作,如 x = 0。尽管可能涉及复杂的算术或逻辑操作,这些命令依然不依赖于输入大小。 我们将其称为 规模无关命令。通常,这些命令的正确性是直观的,且常被认为是理所当然的,因为 它们的简单性。

  • 复合命令:该 类别包括其他命令的块,这些命令可以是简单命令或其他复合命令。 复合命令可能被封装在一个函数调用内,或者由一系列更简单的命令组成。 然而,最显著的复合命令形式是 选择块 迭代

    • 选择:这些 算法中的命令修改了控制流或程序中的逻辑方向。 最常用的选择命令是 if-then 指令块。 这些块的正确性至关重要,取决于算法的逻辑结构,因此需要仔细分析以确保它们按预期运行。

    • 迭代:如果 我们将算法类比为一辆车辆,那么简单命令代表了车辆的车身和所有固定部件,而选择组件则类似于转向机制。 然而,车辆的运动依赖于 发动机,类似地,算法的运行也依赖于其迭代组件。 虽然所有其他组件可能是规模无关的,但循环定义了算法的规模——换句话说,它们的成本和复杂性。 此外,算法的正确性在很大程度上依赖于这些 迭代组件。

本质上,数学归纳法与算法正确性之间的关系在考虑迭代组件时变得显而易见。 数学归纳法类似于证明:如果一个车辆(算法)在给定的距离内(特定的迭代步骤数)能够正确运行,那么它也将在接下来的距离增量(迭代的下一步)上正确运行。 这种证明方法特别适合验证算法中循环和递归过程的行为,确保它们在所有 可能的场景下都符合规范。

以下是实现迭代式二分查找算法的 Python 代码,旨在查找目标值在排序数组中的位置。 在以下代码中,我们将详细说明代码的执行过程,并突出显示简单命令、迭代块和选择块的具体作用。 最后,我们将演示如何测试 该函数:

 def binary_search(ar,x):
    low = 0
    high = len(ar) - 1
    mid = 0
    while low <= high:
        mid = (high + low) // 2
        if ar[mid] < x:
            low = mid + 1
        elif ar[mid] > x:
            high = mid - 1
        else:
            return mid

我们来分析一下二分查找算法中的每个命令块:

  • 简单命令

    • low = 0:初始化 搜索区域的下边界 边界

    • high = len(ar) - 1:根据 数组的长度 设置上边界

    • mid = 0:初始化 中点变量

    这些命令设置了搜索的初始条件,且不直接依赖于输入的大小。 它们会在 循环开始之前执行一次。

  • 迭代块:当 while 循环(while low <= high:)将持续进行,只要数组中还有部分 剩余元素需要考虑,定义了算法的复杂度,并且通过确保搜索边界内的每个元素 都被考虑,影响算法的正确性。

  • 选择块:在 循环内部,根据 ar[mid] x的比较, low high 边界被调整,从而缩小搜索空间。 其中, if elif 条件调整搜索边界(low 和 high),而 else 条件则处理目标被找到的情况,直接影响算法中的流向和决策过程。

使用二分查找示例来演示为什么算法的正确性(以及其成本,下一章将讨论)通常是基于算法的迭代部分来评估的。 考虑到这一点,当应用数学归纳法验证算法的正确性时,出现了一个重要的挑战:确保算法最终终止,而不是无限运行。 这一点就是我们为什么引入了一个专门为计算机算法量身定制的概念,称为循环不变量。 这种数学归纳法的调整有助于确保尽管算法是迭代性的,它仍然能够始终产生正确的输出,并按预期终止。 如预期一样。

算法正确性的循环不变量

循环不变量是一个扩展了数学归纳法的概念,专门用于分析算法。 它调整了数学归纳法的传统步骤,更好地适应计算机算法中迭代过程的结构,如在 表 2.1中所述。

数学归纳法 循环不变量中的步骤 **
基本情况 初始化:对应于数学归纳法中的基本情况。 在这里,验证在迭代 循环开始之前,感兴趣的关系是否成立。
归纳步骤 维护:类似于数学归纳法中的归纳步骤。 此步骤确保如果在迭代开始时关系成立,那么在该迭代结束时关系也必须成立。
在数学归纳法中,我们证明一个无限多个案例的假设,并且它不涉及 终止步骤。 终止:一个通常不属于数学归纳法的额外步骤。 这个步骤确认,在循环终止后,所关心的关系依然成立。 循环终止时,关系依然有效。

表 2.1:数学归纳法和循环不变式步骤比较

这三个步骤—— 初始化 维护,和 终止 ——构成了循环不变式理论的核心,对于证明涉及循环的算法的正确性至关重要。 这种方法确保算法不仅在开始时正确,而且在执行过程中始终保持正确性,并最终以预期的结果结束,从而验证其功能和 操作的完整性。

为了更好地理解循环不变式的概念,我们将使用一个实际示例,描述一辆校车从起始站到学校的行程,在每个站点接学生,但没有学生下车。 我们将通过这个场景展示,在循环过程中,某些元素(如站点)发生变化,而其他元素(如乘客数量)保持不变, 以及这如何与算法循环中的循环不变式相关。

示例 2.3

校车从起点开始,所有座位为空。 在每个站点,它停下来接学生,但没有学生下车。 这个过程会在每个站点重复,直到公交车到达 学校。

循环不变式分析:在这种情况下,我们将乘客数量作为循环不变式进行分析。 我们正在测试的不变式是: “从开始到结束,公交车上的乘客数量只会增加。” 该不变式始终成立。”

现在,讓我们来看看在校车示例中循环不变式的每个步骤。

  • 初始化:在初始站点(起始站),公交车是空的,车上没有学生。 这为我们的循环不变式设定了初始条件,即公交车上的乘客(学生)最初为零。 乘客数量最初为零。

  • 维护:当公交车从一个车站到下一个车站时(从站点 i-1 到站点 i),学生只会上车,没有人下车。 因此,车上的学生人数要么增加,要么保持不变,但绝不会减少。 这维护了循环不变式,即车上乘客人数只能增加。

  • 终止:当到达学校时,接送学生的过程停止。 循环不变式成立,因为在整个旅程中,学生人数只会增加或保持不变。 在最后这一点,循环不变式确认没有学生被送下车,且每个停靠站只能增加学生。 站点。

这个例子说明了循环不变式——即公交车上的学生人数从未减少——在整个旅程中始终成立。 它被正确初始化,并通过每次迭代(每次车站停靠)得到维护,且当循环在学校终止时仍然成立。 这个类比有助于阐明如何应用循环不变式,以确保某些条件在算法循环执行过程中保持不变,从而提供了一种验证过程正确性的方法。

一个需要澄清的重要方面是循环条件与循环不变式之间的区别。 为了更好地说明这一点,我们重新回顾一下前一节中的二分查找算法示例,专门聚焦于算法的循环部分:

 while low <= high:
        mid = (high + low)//2
        if ar[mid] < x:
            low = mid + 1
        elif ar[mid] > x:
            high = mid - 1
        else:
            return mid

循环条件是一个逻辑语句,用于判断循环是否继续执行或终止。 它在每次循环迭代之前进行评估。 如果条件评估为真,循环继续执行;如果条件评估为假,循环终止。 循环条件确保循环执行正确次数,并且对于控制算法的流程至关重要。 例如,在二分查找算法中,循环条件可能是 <st c="17767">low <= high</st>,这确保搜索在还有元素需要考虑时继续进行。 的过程中。

另一方面,循环不变量 是一种在每次循环迭代前后都成立的属性或条件。 它用于证明算法的正确性。 通过证明不变量在循环开始时以及循环过程中始终成立,我们可以确保算法保持其预期的属性,并最终产生正确的结果。 维持循环不变量的过程包括三个步骤:初始化(证明不变量在循环开始前成立)、维护(证明如果不变量在一次迭代前成立,它在迭代后仍然成立),以及终止(证明当循环终止时,不变量和循环条件共同暗示算法的正确性)

总的来说,循环条件控制循环的执行,而循环不变量通过在循环执行过程中保持一致的属性,确保算法的正确性。

在二分查找算法中,循环条件是 <st c="18863">low <= high</st>。这个条件决定了循环的执行,确保当搜索空间有效时,循环继续。 本质上,它检查指定范围内(<st c="19078">low</st> <st c="19086">high</st>)是否仍然存在潜在元素,目标可能就在其中。 当这个条件不成立时(即 <st c="19164">low</st> 超过 <st c="19176">high</st>),表明搜索空间已被耗尽而未找到目标,循环(也就是搜索)终止。 因此,循环条件对于定义循环的操作边界至关重要。 问题是,为什么循环条件不是一个循环不变量呢? 循环条件不是循环不变量的原因有几个关键因素:

  • 不同阶段的有效性:循环不变量必须在循环开始前、每次迭代过程中以及循环退出时始终保持为真。 相比之下,循环条件仅在循环过程中需要为真,以允许循环继续进行。 一旦循环退出(当 low 超过 high时), low <= high 条件为假,这对于停止循环是必要的,但与 循环不变量的要求不符。

  • 条件的作用:循环条件的主要作用是根据当前的状态检查是否可以继续循环,基于 low high。它是一种控制机制,而不是关于算法有效性或过程完整性的正确性声明或保证。 另一方面,循环不变量涉及保持某种属性或条件,这些属性或条件在整个执行过程中验证算法逻辑的正确性。 它的执行。

现在,让我们来看一下一个 循环不变量。

与循环条件相比,循环不变量必须在每次循环迭代的开始和结束时都保持为真,才能确保算法的正确性。 对于二分查找,一个合适的循环不变量可以是: “在每次迭代开始时,如果目标存在于数组中,它必须位于由 low 和 high 限制的子数组内。” 这个不变量确保了在每次迭代后,搜索空间通过根据目标与当前搜索范围中间元素的比较调整 <st c="20969">low</st> <st c="20976">high</st> 索引,正确地缩小了搜索空间。 范围(<st c="21078">ar[mid]</st>)。

在这种情况下,循环不变量对于确保算法的正确性至关重要,确保目标元素不会被遗漏,并且搜索空间有效地缩小。 相反,循环条件控制搜索过程的继续。 理解它们各自的不同作用,对于有效分析和确定算法的正确性至关重要,特别是像二分查找这样的算法,在其中精确地维护和缩小搜索边界是关键。 在接下来的章节中,随着新算法的引入,我们将持续回顾循环不变量,以评估 每个算法的正确性。

总结

本章讨论了数学归纳法和循环不变式作为验证算法正确性的基础工具。 本章首先解释了数学归纳法,描述了其两个主要步骤:基础情况,它确立了命题的初始有效性;以及归纳步骤,展示了命题在连续迭代中的维持。 接着介绍了循环不变式,通过增加终止步骤来扩展数学归纳法,以确保算法的属性从开始到结束始终有效。 通过详细的例子,如二分查找算法,本章展示了如何应用这些原理来证明算法的正确性和高效性,强调了它们在开发可靠软件中的重要性。 本章旨在为你提供方法论,帮助你严格评估各种算法的完整性和操作正确性。

如前所述,算法分析涉及两个关键方面:证明算法的正确性和分析其复杂性。 下一章将讨论第二个方面,重点介绍算法的复杂性以及如何估算 这一复杂性。

参考文献和进一步阅读

  • 算法导论. 作者:托马斯·H. 科尔曼、查尔斯·E. 莱瑟森、罗纳德·L. 里维斯特和克利福德·斯坦恩。 第四版。 麻省理工学院出版社 2022 年。

  • 算法设计. 作者:乔恩·克莱因伯格和埃娃·塔尔多斯。 第一版。 皮尔森 2005 年。

  • 计算机程序设计的艺术,第 1 卷:基础算法。 唐纳德·E. 克努斯。 第三版。 阿迪森-韦斯利专业出版 1997 年。

  • 算法解锁. 作者:托马斯·H. 科尔曼。 麻省理工学院出版社 2013 年。

  • 离散数学及其应用. 作者:肯尼斯·H. 罗森。 麦格劳-希尔科学/工程/数学出版 第十二版。 麦格劳-希尔 2012 年。

  • 《具体数学:计算机科学基础》. 作者:罗纳德·L. 格雷厄姆,唐纳德·E. 克努斯,欧仁·帕塔什尼克。 第二版。 艾迪生-韦斯利出版社。 1994 年。

  • 《如何证明:结构化方法》. 作者:丹尼尔·J. 韦尔曼。 第三版。 剑桥大学出版社。 2019 年。

第五章:3

复杂度分析的增长速率

在算法设计与分析领域,理解算法运行时间如何随着输入数据的大小增长是至关重要的。 这一概念被称为 增长速率,它使我们能够预测和比较不同算法的性能,确保我们选择最有效的解决方案来应对计算问题。 随着输入数据规模的扩展,算法的效率变得越来越关键,特别是在数据处理、机器学习和人工智能等领域,这些领域通常需要处理大量数据。 数据集已成为常态。

增长速率通过渐进符号来描述,这些符号为我们提供了一个框架,用于根据输入数据的大小对算法进行分类,依据其运行时间或空间需求。 这些符号包括大 O、 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Ω</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Θ</mml:mi></mml:math>,它们帮助我们通过分别表示算法的上界、下界和紧界,来规范化算法的效率。 通过这一形式化方法,我们可以更好地理解算法的可扩展性,并据此做出关于算法在特定任务和环境中适用性的明智决策。 以及环境。

在本章中,我们将探讨这些渐进符号的复杂性,提供详细的解释和实际示例来说明它们的应用。 我们将探讨常见的增长速率,如常数、对数、线性、对数线性、多项式、指数和阶乘,重点讲解它们对算法性能的影响。 在本章结束时,你将能够牢固掌握如何分析和解释算法的复杂度,从而获得设计更高效、更有效的计算解决方案的知识。 计算解决方案。

以下主题将在 本章中进行讲解:

  • 算法增长速率解析 中的算法

  • 渐进符号

  • 应对无法解决的问题 – 非确定性多项式时间(NP)-困难问题

算法增长速率解析

让我们重新回顾一下 关于学习算法目标的初步讨论。 其中一个主要目标是分析算法,以预测其行为和性能。 这种分析对于以下两个 主要原因至关重要:

  • 做出明智的 设计决策

  • 理解如何设计高效且 低成本的算法

一个算法的成本取决于其生命周期中的许多参数。 然而,我们在这里关注的是运行算法的成本。 尽管设计、维护、测试和淘汰算法相关的成本同样重要,但它们超出了我们 当前讨论的范围。

运行算法的成本或其复杂度通常在两个维度上进行衡量或估算: 时间 空间 时间复杂度 指的是生成预期输出所需的时间。 这假设算法最终会终止,尽管有些情况下,算法可能因设计缺陷或其他错误而永远不会终止。 空间复杂度,另一方面,指的是运行该 算法所需的内存。 时间和空间复杂度都是至关重要的考虑因素,因为我们的计算资源有固有的限制。 如果假设我们可以访问即时计算,能够在恒定时间内运行任何算法并处理任何数量的数据,且拥有无限的内存,那么计算复杂度的研究将变得不必要。 然而,考虑到当前的技术状况,这些限制使得计算复杂度的研究成为设计 高效算法的必要条件。

时间和空间复杂度是作为输入数据大小的函数进行衡量的,通常用 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo∈</mml:mo><mml:mfenced open="{" close="}" separators="|">mml:mrowmml:mn0,1</mml:mn>mml:mo,</mml:mo>mml:mn2,3</mml:mn>mml:mo,</mml:mo>mml:mo…</mml:mo></mml:mrow></mml:mfenced>mml:mo.</mml:mo></mml:math> 这是直接的,因为每个算法的目标是通过处理输入数据来生成预期的输出。 无论输入数据是数值、文本、图像还是视频,我们都关心随着输入数据量的增加,复杂度如何变化。 这个概念被称为增长率,用大 O 表示,它描述了相对于输入大小,时间复杂度的上界或最坏情况。 算法的复杂度,用 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>来表示,然后通过大 O 分析来估算。 我们将详细讨论计算 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:r></mml:mfenced></mml:math>的技术,尤其是递归算法,这些算法更加复杂。 这可能看起来微不足道,但值得一提的是,较高或更快的增长率通常意味着算法需要更多的时间来生成 预期结果。

增长率 是数据大小的函数,且可以采取多种数学形式。 然而,我们关注一些知名的模式,这些模式有助于比较算法。 通过比较执行相同任务的算法,我们旨在识别在时间和空间方面最有效的算法。 虽然主要关注时间复杂度,但这些概念也可以推广到空间和 内存复杂度。

常数增长

所有计算复杂度挑战的根源在于计算的迭代性质。 正如在上一章中讨论的,迭代组件,我们将其比作算法的引擎,可以通过 循环 循环 递归来实现。 无论具体实现方式如何——尽管选择循环和递归的方式会显著影响算法的复杂度——最终决定算法复杂度的是迭代的次数。 理解这些迭代如何影响整体复杂度对于设计 高效算法至关重要。

当算法的复杂度与输入数据的大小无关时,增长率最慢。 这一类增长函数称为 常数 常数,表示为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>。如果一个算法的时间复杂度是常数时间,表示为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>,则表示 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miC</mml:mi></mml:math>,这意味着它受到一个不依赖于输入数据大小的常数的限制。 问题是,我们在哪些情况下可能会遇到常数增长率,并且输入数据量在增加呢? 答案是这种情况非常罕见。 以下是一些常数增长率的情况示例: 增长率:

  • 报告输入的第一个数字:这与输入的数字 数量无关

  • 访问数组元素:通过索引检索元素是一个常数 时间操作

  • 在链表的开头插入一个元素:这个操作涉及更新几个指针,所需的时间是 恒定的

在这些例子中,我们没有使用循环来实现算法。 然而,值得注意的是,任何循环的存在通常会使得复杂度变为非恒定。 如果迭代是由输入数据的大小控制的, n,那么算法不可能是恒定的。 关键在于,对于一个算法来说,要具备恒定的增长率,它执行的操作数量不能依赖于 输入数据的大小。

亚线性增长

除了恒定增长函数,所有 其他的增长率都依赖于输入数据的大小, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>。在恒定增长和线性增长(线性时间算法的复杂度)之间,存在一系列亚线性函数。 亚线性时间算法 那些时间复杂度增长速度慢于输入数据大小的算法,通常表示为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>。换句话说,这些算法不需要检查输入中的每一个元素就能产生输出。 这些算法的目标是通过执行比输入中元素总数少的操作来实现高效性。 输入的元素数量。

根据经验法则,每当一个问题需要检查每个输入元素时,例如在顺序查找算法中,复杂度至少是线性时间, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>。在这些情况下,我们无法实现更好的时间复杂度,因为每个元素都必须被检查以确保正确性。 子线性时间算法通常具有如下时间复杂度特征: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced></mml:math>。以下是一些子线性 时间算法的例子:

  • 二分查找 (<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>): 二分查找 是一种高效的算法,用于在 排序数组中通过反复将搜索区间对半分割来查找元素。 该算法特别适用于大型的排序数据集。 以下是二分查找算法的一个简单 Python 实现。 迭代部分使用 while 循环 实现:

     def binary_search(a, x):
        low = 0
        high = len(a) - 1
        while low <= high:
            mid = (high + low) // 2
            if a[mid] < x:
                low = mid + 1
            elif a[mid] > x:
                high = mid - 1
            else:
                return mid
        return -1 
    print(binary_search([0,1,3,5,8,10],10))
    

    二分查找的最坏情况时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mi </mml:mi>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math> 当被查找的项位于二叉树的底部时。 在最优情况下,它的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>。对数增长率, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>,是多对数增长的特例,表示为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrow<mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrowmml:msupmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi>mml:mo></mml:mo>mml:mn0</mml:mn></mml:math> 且常数。 在对数增长中,我们 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miK</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math>

  • 跳跃搜索(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced></mml:math>:跳跃搜索(或称为块搜索)是一种算法,旨在通过按固定步长跳跃前进,再在识别的块内执行线性搜索,从而高效地在已排序的数组中查找元素。 这种方法特别适用于大型已排序数组,在这些数组中,线性搜索会过于缓慢。 这里展示的是跳跃搜索算法的 Python 代码实现:

     import math
    def jump_search(a, x):
        n = len(a)
        step = int(math.sqrt(n))
        prev = 0
        while a[min(step, n)-1] < x:
            prev = step
            step += int(math.sqrt(n))
            if prev >= n:
                return -1
        for i in range(prev, min(step, n)):
            if a[i] == x:
                return i
        return -1
    
  • 插值搜索(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>):一个具有复杂度的算法示例 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math> 是插值搜索 算法。 插值搜索是对二分搜索的改进,适用于有序数组中值分布均匀的情况。 当数据均匀分布时,搜索可以在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math> 的时间复杂度下完成。 这适用于数据均匀分布的情况。

    插值搜索的工作原理是估计目标值在有序数组中的位置。 它根据数组的范围和目标值,使用公式计算目标的可能位置。 如果数据的分布均匀,位置估算非常准确,从而大大减少了比较的次数。 以下是插值搜索的 Python 实现代码,供你参考:

     def interpolation_search(a, x):
        low = 0
        high = len(a) - 1
        while low <= high and x >= a[low] and x <= a[high]:
            if low == high:
                if a[low] == x:
                    return low
                return -1
            pos = low + ((high - low) // (a[high] - a[low]) * (x - a[low]))
            if a[pos] == x:
                return pos
            if a[pos] < x:
                low = pos + 1
            else:
                high = pos - 1
        return -1
    ```</st></st>
    
    

跳跃搜索和插值搜索都表现出亚线性增长率,相较于线性搜索,提供了潜在的效率提升。 在接下来的小节中,我们将讨论线性增长模式的概念,通过 相关示例来说明它们的特征。

线性增长

算法分析中最直接的增长速率函数是线性时间,表示为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 线性增长 作为一个重要的分界线,区分了快速算法,如常数时间(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>)和亚线性时间(例如, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math> On),以及较慢或极慢的算法,如多项式时间(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>)和指数时间(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>),这些都被视为非线性。

当算法的运行时间与输入数据的大小成正比时,该算法被认为具有线性时间复杂度。 换句话说,线性时间算法的运行时间是 Tn=cn,其中 c是常数。 线性时间算法对于解决必须处理每个输入元素的问题至关重要。 在这种情况下,操作的数量与输入数据的大小成正比增长。 这意味着如果输入大小翻倍,处理输入所需的时间也会翻倍。 线性时间算法的例子包括顺序搜索、查找数组中的最小值或最大值,以及对数组中的元素进行求和。

一个显著的线性时间算法是计算非负整数𝑛的阶乘(记作 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo!</mml:mo></mml:math>)。 以下是 Python 中阶乘算法的简单迭代实现:

 def factorial(n):
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result

由于问题的性质,每个从 1 到 n 的数字都必须被访问,递归实现无法实现比 线性时间更好的复杂度:

 def factorial_recursive(n):
    if n == 0 or n == 1:
        return 1
    else:
        return n * factorial_recursive(n - 1)

线性时间 算法至关重要,因为它们在效率和全面性之间提供了平衡。 它们确保每个元素都被考虑到,因此适用于那些跳过元素可能导致不正确或不完整结果的问题。 然而,需要认识到,尽管线性时间算法对于中等规模的输入非常高效,但对于非常大的数据集,它们可能变得不切实际。 在这种情况下,可能需要更复杂的具有亚线性时间复杂度的算法来实现 可接受的性能。

非线性增长

非线性时间算法族涵盖了各个领域中一系列著名的复杂问题,包括数据处理、优化、机器学习和人工智能。这些算法的增长速度比线性时间复杂度更为复杂,复杂度范围从相对高效的算法,如O(n log n),到极为昂贵的算法,具有非多项式增长的复杂度,例如指数级的O(2^n)和阶乘级的O(n!)时间。

以下是一些非线性算法的例子:

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math> 算法:如 归并排序 快速排序,以及 堆排序 属于这一类。 这些是经典的排序算法,它们的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>。它们对于大数据集非常高效,并且由于其相对可控的增长率,在实际应用中经常使用。 在接下来的章节中,我们将详细讨论这一类算法。 由于其高效性和广泛适用于各种 排序任务,这些算法在计算机科学中是基础性的。

  • 对数线性时间:对数线性时间算法,也 称为 准线性时间算法,其时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mi </mml:mi>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:msupmml:mrow<mml:mi mathvariant="normal">l</mml:mi><mml:mi mathvariant="normal">o</mml:mi><mml:mi mathvariant="normal">g</mml:mi></mml:mrow>mml:mrow<mml:mi mathvariant="normal">k</mml:mi></mml:mrow></mml:msup>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>,其中 k是一个正的常数。 许多著名的算法属于这一类,并且在高效处理大数据集时非常重要。 对数线性时间算法的示例如下:

    • 归并排序:一种 经典的 分治排序算法,它将输入数组分割成更小的子数组,对它们进行排序,然后将它们合并回一起。 它的时间复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>

    • 堆排序:该 排序算法从输入数据构建堆数据结构,然后反复提取最大元素来构建排序后的数组。 它的时间复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>

    • 快速排序:另一种 分治排序算法,它选择一个 枢轴元素并围绕枢轴将数组分区。 在平均情况下,它的时间复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>

  • 多项式时间算法:多项式时间算法的复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mia</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>,其中 a 是一个小常数。 这些算法通常被认为在中等大小的输入下高效,但随着输入规模的增加,其性能可能会显著下降。 以下是 一些例子:

    • 矩阵乘法:标准的矩阵乘法算法时间复杂度为On3。更先进的算法,如斯特拉森算法,可以将此复杂度降低到大约 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2.81</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>,但它仍然是多项式复杂度。

    • 弗洛伊德-沃舍尔最短路径:该 算法找到从源节点到图中所有其他节点的最短路径,前提是图中的边权为非负数。 其时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:miV</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>,其中 𝑉 是图中的顶点数。

    多项式时间算法 由于其相对可预测的性能,对于许多实际应用至关重要。 然而,随着输入规模的增长,它们的效率可能会成为问题,使得它们不太适用于非常大的数据集。 尽管如此,它们仍然是算法设计的基石,为计算机科学 和工程中的广泛问题提供了可行的解决方案。

  • 指数时间算法:这些 算法的特点是极其缓慢且计算开销巨大。 它们的复杂度一般形式为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>,其中 𝑎 是一个正的常数。 这些算法的一个常见特殊情况是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>。一个著名的指数时间算法示例是递归解决方案 用于解决 汉诺塔问题

    汉诺塔问题涉及将 𝑛 个圆盘从一个柱子移动到另一个柱子,使用第三个柱子作为辅助,遵循 以下规则:

    • 每次只能移动一个圆盘

    • 一个圆盘只能放在一个 更大的圆盘上

    汉诺塔问题的递归解法的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>,因为每一步都涉及递归地解决两个大小为 𝑛−1 的子问题。 汉诺塔问题的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>,将在后续章节中详细探讨,当我们讨论递归函数时。 此问题的递归函数为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>,将通过替代法和主方法等技术进行分析,以推导出复杂度。 通过这些方法,可以证明 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>。要全面了解,请参阅 第四章 第五章

    这是一个关于汉诺塔问题的递归 Python 实现:

     def tower_of_hanoi(n, source, target, auxiliary):
        if n == 1:
            print(f"Move disk 1 from {source} to {target}")
            return
        tower_of_hanoi(n - 1, source, auxiliary, target)
        print(f"Move disk {n} from {source} to {target}")
        tower_of_hanoi(n - 1, auxiliary, target, source)
    

    指数时间算法 通常在大规模输入时不切实际,因为随着输入规模的增长,其运行时间会急剧增加。 这些算法通常用于没有已知有效解的问题,它们作为一种基准,用于比较更 复杂算法的性能。

  • 阶乘时间算法:这些 算法会生成一个集合的所有排列,因此它们在计算上非常昂贵。 一个典型的例子是暴力破解方法解决 旅行商问题(TSP),它的时间复杂度是阶乘级别 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo!</mml:mo></mml:mrow></mml:mfenced></mml:math>

    旅行商问题(TSP)是一个经典的优化问题,在这个问题中,销售员必须找到一条最短的路线,访问一组城市并且每个城市只能访问一次,最后回到起始城市。 给定一组城市以及每对城市之间的距离,目标是确定最有效的旅行路线,最小化总的 旅行距离。

暴力破解法解决 TSP 问题的方式是生成所有可能的城市排列,并计算每个排列的总距离,以找到最短的那一个。 这会导致时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo!</mml:mo></mml:mrow></mml:mfenced></mml:math>。排列的数量是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 个城市的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo!</mml:mo></mml:math> (n 阶乘),其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 表示城市的数量。 以下是 TSP 问题的简单实现:

 from itertools import permutations
def calculate_distance(permutation, distance_matrix):
    distance = 0
    for i in range(len(permutation) - 1):
        distance += distance_matrix[permutation[i]][permutation[i + 1]]
    distance += distance_matrix[permutation[-1]][permutation[0]]  # Return to the start
    return distance
def tsp_brute_force(distance_matrix):
    n = len(distance_matrix)
    cities = list(range(n))
    min_distance = float('inf')
    best_permutation = None
    for permutation in permutations(cities):
        current_distance = calculate_distance(permutation, distance_matrix)
        if current_distance < min_distance:
            min_distance = current_distance
            best_permutation = permutation
    return best_permutation, min_distance

阶乘时间算法 对于除最小输入规模之外的所有情况,都是高度不切实际的,因为运行时间增长极快。 随着城市(或元素)数量的增加,排列组合的数量以及解决问题所需的计算量以阶乘的速度增加。 由于其阶乘时间复杂度,这些算法对于更大的数据集变得不可行,突显了在 实际应用中对更高效的启发式或近似算法的需求。

总之,非线性时间算法在 解决各个领域复杂且计算密集型的问题中起着基础作用。 虽然它们提供了强大的解决方案,但它们的性能可能会根据输入规模和算法的具体增长速率而有显著变化。 理解这些复杂性有助于选择合适的算法,并为 特定应用优化性能。

表 3.1 提供了不同输入数据大小的实际增长率示例。 它清楚地表明, 非线性算法,特别是那些具有指数和阶乘增长率的算法,随着输入大小的增加,计算复杂度会迅速增长。 这种资源需求的快速增加使得这种算法对于除了最小数据集外的其他情况,在计算上变得不切实际。

输入数据的大小 (n)
增长率 16
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math> C
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math> 2
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math> 4
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 16
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math> 64
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 265
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 4,096
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mn65</mml:mn>mml:mo×</mml:mo>mml:msupmml:mrowmml:mn10</mml:mn></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:math>
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo!</mml:mo></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mn20.92</mml:mn>mml:mo×</mml:mo>mml:msupmml:mrowmml:mn10</mml:mn></mml:mrow>mml:mrowmml:mn12</mml:mn></mml:mrow></mml:msup></mml:math>
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mn18.44</mml:mn>mml:mo×</mml:mo>mml:msupmml:mrowmml:mn10</mml:mn></mml:mrow>mml:mrowmml:mn18</mml:mn></mml:mrow></mml:msup></mml:math>

表 3.1:不同输入规模的实际增长率示例(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">C</mml:mi></mml:math>:一个常数且在计算上可以忽略不计; <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">E</mml:mi></mml:math>:极大数且在计算上不可行)

图 3**.1 (a) (b) 展示了不同函数的增长速率图,这些函数的输入大小范围从 n=1 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo=</mml:mo>mml:mn1000</mml:mn></mml:math>。这些图表提供了不同时间复杂度的可视化比较,展示了它们随着输入大小的增加如何扩展。 这些图形整体展示了不同时间复杂度对算法性能的影响,强调了为 大规模问题选择高效算法的重要性。

(a)

(b)

图 3.1 (a) 和 (b):展示了 n=1 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">n</mml:mi>mml:mo=</mml:mo>mml:mn1000</mml:mn></mml:math>

本节 讨论了理解算法复杂度如何随输入数据的大小变化的重要性,通常用 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>表示。它强调了时间和空间复杂度是评估算法性能的关键指标。 本节解释了增长率可能会有很大的差异,从常数时间 O1到阶乘时间 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo!</mml:mo></mml:mrow></mml:mfenced></mml:math>,并包括中间复杂度,如对数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>、线性 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>和对数线性 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>。增长速率对于做出明智的设计决策和确保高效的算法实现至关重要,尤其是在处理大型数据集时。 通过比较不同的增长率,可以更好地理解算法效率与计算资源需求之间的权衡。 现在,是时候引入一个数学框架来研究算法的行为,这与 算法的增长速率直接相关。

渐近符号

渐近符号 是一个数学 框架,用于描述算法在输入规模趋向无穷大时其时间和空间复杂度的行为。 它根据算法的增长速率对算法进行分类,从而使得不同算法的效率比较不受硬件或实现细节的影响。 渐近符号源自数学分析领域,用来描述数学函数的极限行为。 顾名思义,渐近符号并不提供具体的解决方案,而是对函数在 规模扩展时的行为的形式化表示。

假设我们有一个类似如下的函数: 如下所示:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mi </mml:mi>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:mi </mml:mi>mml:mn1000</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:mi </mml:mi>mml:mn6</mml:mn>mml:min</mml:mi>mml:mi </mml:mi>mml:mo+</mml:mo>mml:mi </mml:mi>mml:mn5</mml:mn>mml:mi </mml:mi>mml:mo×</mml:mo>mml:msupmml:mrowmml:mn10</mml:mn></mml:mrow>mml:mrowmml:mn5</mml:mn></mml:mrow></mml:msup></mml:math>

我们想要预测 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo→</mml:mo>mml:mi∞</mml:mi></mml:math>时的行为。 虽然项 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn1000</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math> 的系数非常大,并且有一个很大的常数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn5</mml:mn>mml:mo×</mml:mo>mml:msupmml:mrowmml:mn10</mml:mn></mml:mrow>mml:mrowmml:mn5</mml:mn></mml:mrow></mml:msup></mml:math>,除了项 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:math> 外的所有项 n n 非常大的时候都会变得微不足道。 在数学分析中,我们用符号表示fn∼n3 并读作“<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 渐近地 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:math>。”

在算法分析中, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 被替换为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mi </mml:mi>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>,表示正在研究的算法运行时间的函数复杂性。 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mi </mml:mi>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 可以通过分析算法在使用迭代(非递归)方法实现时直接估算,或者可以通过递归算法的特定递归关系描述。 一旦我们得到 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mi </mml:mi>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>,我们可以预测算法行为随着 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> (数据元素数量或问题大小)增长到非常大时,使用渐近符号。

在描述算法分析中的 渐近符号之前,我们需要解释在使用 渐近符号时处理方程和不等式的简化指导原则。

在渐近符号中的简化规则

渐近符号 提供了一种描述函数行为随着输入大小趋向无穷大的方法。 在使用渐近符号时,简化表达式以专注于最重要的项非常重要。 以下是关键的 简化规则:

  • 去除常数:忽略常数系数,因为它们不影响 增长率。

    示例: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mi </mml:mi>mml:mn5</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:mi </mml:mi>mml:mn3</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:mi </mml:mi>mml:mn2</mml:mn>mml:min</mml:mi>mml:mi </mml:mi>mml:mo+</mml:mo>mml:mi </mml:mi>mml:mn7</mml:mn></mml:math> 简化为<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mi </mml:mi>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>.

  • 保留最重要的项:保留增长率最高的项,因为它在n变大时主导了该函数。

    示例: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mi </mml:mi>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:mi </mml:mi>mml:mn1000</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:mi </mml:mi>mml:mn50</mml:mn>mml:min</mml:mi></mml:math> 简化为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>.

  • 合并相似项:加法时,保留增长率最高的项。

    示例: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mi </mml:mi>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mi </mml:mi>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 简化为 ![+ 项的乘积:当乘以函数时,乘上它们的 增长率。 示例: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo×</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math> 简化为 ![+ 嵌套函数:对于嵌套函数,外层函数的增长 率占主导。 示例 Tn=O2On 简化为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

让我们在以下示例中练习简化 规则:

  • 示例 3.1

    简化 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mi </mml:mi>mml:mn3</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:mi </mml:mi>mml:mn2</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:mi </mml:mi>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>

    解答:去掉常数项和低阶项: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mi </mml:mi>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

  • 示例 3.2

    简化 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn100</mml:mn>mml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow>mml:mo+</mml:mo>mml:mn50</mml:mn>mml:min</mml:mi></mml:math>.

    解答:舍去低阶 项: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>.

  • 例子 3.3

    简化 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>.

    解答:保留增长最快的 项:<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mi </mml:mi>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mi </mml:mi>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>.

  • 例子 3.4

    简化 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>

    解答:乘以增长 率: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>

  • 示例 3.5

    简化 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>

    解答:首先简化内层 函数:

    <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mrow></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn2</mml:mn>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>.

在建立了 渐近符号和简化技术的基础之后,我们现在将注意力转向本章的一个关键方面:由 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miΘ</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miΩ</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>.

渐近界限

分析算法复杂度的关键 在于研究其运行时间。 那么,我们应该考虑哪种运行时间呢? 算法的表现可能会根据输入数据的性质及其分布的不同而有所不同。 通常,我们希望了解最坏情况,因为它能提供关于任何输入的算法运行时间上限的保证。 然而,我们也可能对平均情况感兴趣,这能给出算法在典型输入集上的性能更实际的预期。 最终,我们的目标是得出关于算法行为的一般结论,这些结论独立于特定的输入数据及其分布。 让我们详细讨论这些不同的情况:

  • 最坏情况:最坏情况考虑了在给定最具挑战性的输入数据的情况下,算法完成所需的最大时间。 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>。这种分析提供了一个保证,确保算法不会超过此时间,这对于那些对性能要求严格的应用尤为重要。 描述最坏情况的渐近符号是 O符号(大 O 符号)。 这被称为 渐近 上界

  • 平均情况:平均情况评估算法在所有可能输入(大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>)下的期望运行时间,假设输入的概率分布为某种特定形式。 这种分析提供了算法在典型使用场景下性能的更现实的衡量标准,揭示了其在正常操作条件下的效率。 用于描述平均情况的渐近表示法通常是<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mi </mml:mi><mml:mi mathvariant="normal">Θ</mml:mi></mml:math> 表示法(大 Theta 表示法)。 这被称为 渐近 紧界限 界限

  • 最优情况:最优情况检查算法在最有利的输入(大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>)下完成所需的最短时间。虽然在性能分析中不常用,但它可以突出算法在最佳条件下的效率。 用于描述最优情况的渐近表示法,称为 渐近下界,通常是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Ω</mml:mi></mml:math>-表示法(大 Omega 表示法)。

在探讨了三种时间复杂度场景(最优、平均和最差情况)之后,我们将介绍三种基本的渐近界限:上界、紧界限和 下界。

渐近上界(O 表示法)

算法被分类为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mig</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>) 如果存在正的常数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>,使得算法的时间复杂度或运行时间 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 满足不等式 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:mic</mml:mi>mml:mo⋅</mml:mo>mml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 对于所有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo≥</mml:mo>mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>。从形式上讲, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math> 表示一组具有相似渐近上界的函数或运行时间:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mfenced open="{" close="}" separators="|">mml:mrowmml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo:</mml:mo>mml:mi </mml:mi>mml:mo∃</mml:mo>mml:mi </mml:mi>mml:mic</mml:mi>mml:mo></mml:mo>mml:mn0</mml:mn>mml:mo,</mml:mo>mml:mi </mml:mi>mml:mo∃</mml:mo>mml:mi </mml:mi>mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub>mml:mo></mml:mo>mml:mn0</mml:mn>mml:mi </mml:mi>mml:mis</mml:mi>mml:miu</mml:mi>mml:mic</mml:mi>mml:mih</mml:mi>mml:mi </mml:mi>mml:mit</mml:mi>mml:mih</mml:mi>mml:mia</mml:mi>mml:mit</mml:mi>mml:mi </mml:mi>mml:mo∀</mml:mo>mml:mi </mml:mi>mml:min</mml:mi>mml:mi </mml:mi>mml:mo≥</mml:mo>mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:mi </mml:mi>mml:mn0</mml:mn>mml:mo≤</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:mic</mml:mi>mml:mo⋅</mml:mo>mml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math>

图 3**.2 展示了关于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>的假设图。渐进上界,表示为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math>,提供了一种描述算法运行时间在输入大小增加时最坏情况增长率的方式,确保 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 的增长速度不超过 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi>mml:mo.</mml:mo>mml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>。以下示例展示了如何确定一个函数的上渐进界以及用于 证明它的方法:

示例 3.6

证明 4n2=On3通过确定常数 c <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>

解法: 根据 O符号的正式定义,我们需要证明如下: 以下内容:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn4</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo≤</mml:mo>mml:mic</mml:mi>mml:mo.</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:math> 对于 所有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mi </mml:mi>mml:mo≥</mml:mo>mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>

为了简化,我们将两边同时除以<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>: 4≤c.n

这个不等式有两个未知数, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mi </mml:mi>mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>,这给出了多个可能的解。 例如,如果我们选择 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math>,这将简化为 4≤n。所以, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo>mml:mn4</mml:mn></mml:math> 或任何更大的值。 因此,对于 c=1 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo>mml:mn4</mml:mn></mml:math>,不等式成立。 还有其他可能的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math> 对使不等式成立,但 这是一个 直接的解。

 图 3.2:展示 O 表示法的概念。运行时间 T(n)​​是渐近地被 c ⋅ g(n)​​所界限,其中 c 是一个正的常数。这意味着对于足够大的输入大小 n,特别是对于所有 n≥n0,T(n)的值不会超过 c⋅g(n)​​。

图 3.2:演示 O符号的概念。 运行时间 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">T</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:mfenced></mml:math> 渐近地被 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">c</mml:mi>mml:mo.</mml:mo><mml:mi mathvariant="bold-italic">g</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:mfenced></mml:math>所界定, c是一个正的常数。 这意味着,对于足够大的输入大小 n,具体来说,对于所有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic"> </mml:mi><mml:mi mathvariant="bold-italic">n</mml:mi><mml:mi mathvariant="bold-italic"> </mml:mi>mml:mo≥</mml:mo>mml:msubmml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">T</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:mfenced></mml:math> 的值将不会超过 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">c</mml:mi>mml:mo⋅</mml:mo><mml:mi mathvariant="bold-italic">g</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:mfenced></mml:math>

示例 3.7

展示属于<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>的成员。

如前所述,O表示一组函数。 表 3.2 提供了属于<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>的函数及其对应的常数c<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mic</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:min</mml:mi></mml:math> 1 0
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mn60</mml:mn>mml:min</mml:mi>mml:mo+</mml:mo>mml:mn4</mml:mn></mml:math> 60 2
nlogn 0.5 2
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math> 1 0
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:min</mml:mi></mml:math> 2 1
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:mn5000</mml:mn>mml:min</mml:mi></mml:math> 5001 1

表 3.2:展示 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">O</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

例 3.8

证明 O(n!) 增长得比 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

我们需要证明 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo!</mml:mo></mml:mrow></mml:mfenced>mml:mo></mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>。为此,我们使用 斯特林近似

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:min</mml:mi>mml:mo!</mml:mo>mml:mo∼</mml:mo>mml:msqrtmml:mn2</mml:mn>mml:miπ</mml:mi>mml:min</mml:mi></mml:msqrt>mml:msupmml:mrow<mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mie</mml:mi></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:math>

使用这个近似公式来表示 n!,我们需要证明以下内容:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:mn2</mml:mn>mml:miπ</mml:mi>mml:min</mml:mi></mml:msqrt>mml:msupmml:mrow<mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mie</mml:mi></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:mo></mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

对两边取对数,我们得到以下结果:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrow<mml:mfenced separators="|">mml:mrowmml:msqrtmml:mn2</mml:mn>mml:miπ</mml:mi>mml:min</mml:mi></mml:msqrt>mml:msupmml:mrow<mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mie</mml:mi></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:mrow></mml:mrow>mml:mo></mml:mo>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrow<mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:mrow></mml:mrow></mml:math>

这简化为以下形式:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mn0.5</mml:mn><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mrow>mml:mo+</mml:mo>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:miπ</mml:mi></mml:mrow></mml:mrow>mml:mo+</mml:mo>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrow<mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mie</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mrow>mml:mo></mml:mo>mml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mrow></mml:math>

进一步简化两边,我们得到 以下结果:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow>mml:mo></mml:mo>mml:min</mml:mi></mml:math>

随着 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 增长, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:math> 项占主导地位。 因此, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo!</mml:mo></mml:mrow></mml:mfenced></mml:math> 增长速度超过 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>,这完成了 证明。

我们鼓励 读者采用相同的方法来证明 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> > <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo!</mml:mo></mml:mrow></mml:mfenced></mml:math>

在这一小节中,我们介绍了上渐近界限的概念,解释了如何描述和证明特定增长函数。 在接下来的小节中,我们将探讨下渐近界限。

渐近下界(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold">Ω</mml:mi></mml:math>-符号)

一个算法被认为是 Ω ,当存在正的常数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Ω</mml:mi>mml:mo(</mml:mo>mml:mig</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>) 以及 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math> 使得算法的时间复杂度或其运行时间 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 满足以下条件 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo≥</mml:mo>mml:mic</mml:mi>mml:mo⋅</mml:mo>mml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 对所有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo≥</mml:mo>mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo≥</mml:mo>mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>。形式上, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Ω</mml:mi><mml:mfenced separators="|">mml:mrowmml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math> 表示一组共享相似渐近 下界的函数或运行时间:

Ωgn=Tn:∃c>0,∃n0>0suchthat∀n≥n0,0≤c.g(n)≤Tn

渐近下界,用 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Ω</mml:mi><mml:mfenced separators="|">mml:mrowmml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math>表示,提供了一种描述算法运行时间在输入大小增加时的最优增长率的方法,确保 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 增长速度不低于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi>mml:mo.</mml:mo>mml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 图 3**.3 展示了 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>的假设图形。

图 3.3: 演示 ​ ​符号表示法的概念。运行时间​T​(n)​​渐近地由 ​c . g​(n)​​界定,其中 ​c​ 是一个正常数。这意味着对于足够大的输入规模 ​n​,特别是对于所有 ​n ≥ ​n​ 0​​​,T​(n)​​的值将不会低于 ​c . g​(n)​​。

图 3.3:演示 Ω符号的概念。 运行时间 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">T</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:mfenced></mml:math> 是渐进界限由 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">c</mml:mi>mml:mo.</mml:mo><mml:mi mathvariant="bold-italic">g</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:mfenced></mml:math>约束,其中 c是一个正的常数。 这意味着对于足够大的输入规模 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">n</mml:mi></mml:math>,特别是对于所有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">n</mml:mi>mml:mo≥</mml:mo>mml:msubmml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">T</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:mfenced></mml:math>的值将不会低于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">c</mml:mi>mml:mo.</mml:mo><mml:mi mathvariant="bold-italic">g</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:mfenced></mml:math>

示例 3.9

证明 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msqrtmml:min</mml:mi></mml:msqrt>mml:mo=</mml:mo><mml:mi mathvariant="normal">Ω</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math> 通过确定常数 c <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>

解答:根据 Ω的正式定义,我们需要证明以下内容:Ω:对于所有<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msqrtmml:min</mml:mi></mml:msqrt>mml:mo≥</mml:mo>mml:mic</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:math> ,满足<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mi </mml:mi>mml:mo≥</mml:mo>mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>。我们将不等式的两边都除以<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:math><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow>mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfrac>mml:mo≥</mml:mo>mml:mic</mml:mi></mml:math>,然后我们找到合适的c<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>。为了确定<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>的适当值,我们需要找到使得不等式对所有<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo≥</mml:mo>mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>时,不等式成立的值。让我们从一个特定的值开始c=1:</st

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mfracmml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow>mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfrac>mml:mo≥</mml:mo>mml:mn1</mml:mn></mml:math>

这简化为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msqrtmml:min</mml:mi></mml:msqrt>mml:mo≥</mml:mo>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:math>. 我们需要找到 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math> 使得这个不等式对所有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo≥</mml:mo>mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>. 让我们对不等式的两边同时平方,以便更容易 进行比较:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:min</mml:mi>mml:mo≥</mml:mo>mml:msupmml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:min</mml:mi></mml:math>

对于较大的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>,该项log2n 增长的速度远低于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>。因此,会有一个点 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math> ,超过这个点不等式n≥log2n 成立。 为了粗略估计 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>,我们可以解这个 方程: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo≥</mml:mo>mml:msupmml:mrowmml:milmml:miomml:mig</mml:mrow>mml:mrowmml:mn2</mml:mrow>mml:min</mml:mi></mml:math><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo≥</mml:mo>mml:msupmml:mrowmml:milmml:miomml:mig</mml:mrow>mml:mrowmml:mn2</mml:mrow>mml:min</mml:mi></mml:math>

这是一个超越方程,解析求解它是复杂的。 然而,我们可以采用数值方法来找到一个近似解。 从实际计算中, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math> 大约为 16\。 因此,我们可以 选择 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo>mml:mn16</mml:mn></mml:math>

示例 3.10

示例成员 属于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Ω</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

如前所述,Ω 符号表示一组函数。 表 3.3 提供了属于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Ω</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 的函数示例及其对应的常数 c <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mic</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:math> 1 0
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow>mml:mo+</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:mn4</mml:mn></mml:math> 5 1
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:math> 1 4
n10 1 0
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:msup></mml:math> 1 0
nn 1 9

表 3.3:展示一些 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold">Ω</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

渐近紧界(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">θ</mml:mi></mml:math>-符号)

一个 算法被称为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math> 如果存在正的常数 c1, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math>, 和 n0使得算法的运行时间 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 满足 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo.</mml:mo>mml:mig</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo>mml:mo≤</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mo.</mml:mo>mml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 对于所有 n≥n0. 从形式上讲, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math> 表示一组共享相似渐进 下界的函数或运行时间:

θgn=Tn:∃c1>0,∃c2>0,∃n0>0suchthat∀n≥n0,0≤c1.g(n)≤Tn≤c2⋅gn

图 3**.4 展示了假设图表 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>.

图 3.4:展示了​ ​符号的概念。运行时间​T​(n)​​在​c​ 1​​ . g​(n)​​和​c​ 2​​ . g​(n)​​之间渐近有界,其中​c​ 1​​, ​c​ 2​​ > 0​。这意味着对于足够大的输入大小​n​,特别是对于所有​n ≥ ​n​ 0​​​,​T​(n)​​的值不会低于​c​ 1​​ . g​(n)​​,并且不会超过​c​ 2​​ . g​(n)​​。

图 3.4:展示 θ符号的概念。 运行时间 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">T</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:mfenced></mml:math> 在渐近上被限制在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrow<mml:mi mathvariant="bold-italic">c</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo.</mml:mo><mml:mi mathvariant="bold-italic">g</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrow<mml:mi mathvariant="bold-italic">c</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mo.</mml:mo><mml:mi mathvariant="bold-italic">g</mml:mi>mml:mo(</mml:mo><mml:mi mathvariant="bold-italic">n</mml:mi>mml:mo)</mml:mo></mml:math>之间, 其中 c1,c2>0。这意味着,对于足够大的输入大小n,特别是对于所有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">n</mml:mi>mml:mo≥</mml:mo>mml:msubmml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">T</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:mfenced></mml:math> 的值将不会低于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrow<mml:mi mathvariant="bold-italic">c</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo.</mml:mo><mml:mi mathvariant="bold-italic">g</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:mfenced></mml:math> 并且不会超过 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrow<mml:mi mathvariant="bold-italic">c</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mo.</mml:mo><mml:mi mathvariant="bold-italic">g</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:mfenced></mml:math>

例 3.11:

证明 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo-</mml:mo>mml:mn4</mml:mn>mml:min</mml:mi>mml:mo=</mml:mo>mml:miθ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 通过确定 常数 c1,c2, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>.

解答: 从 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miθ</mml:mi></mml:math> 符号的正式定义出发,我们需要 证明

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo.</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo≤</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo-</mml:mo>mml:mn4</mml:mn>mml:min</mml:mi>mml:mo≤</mml:mo>mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mo.</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math> 对所有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mi </mml:mi>mml:mo≥</mml:mo>mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>. 我们将两个不等式的两边都除以 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo≤</mml:mo>mml:mn1</mml:mn>mml:mo-</mml:mo>mml:mfracmml:mrowmml:mn4</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:mfrac>mml:mo≤</mml:mo>mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math>

对于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo>mml:mn8</mml:mn></mml:math>,我们将得到 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo>mml:mfracmml:mrowmml:mn1</mml:mn></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:math> ,并且对于大 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo>mml:mfracmml:mrowmml:mn1</mml:mn></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:math>.

示例 3.12:

证明 下列公式 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn6</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup>mml:mo≠</mml:mo><mml:mi mathvariant="normal">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>.

解答:首先,我们 尝试证明 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn6</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup>mml:mo=</mml:mo><mml:mi mathvariant="normal">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:mo.</mml:mo></mml:math> 为此,我们需要证明存在正的常数c1 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math> 使得以下式子成立: 如下所示:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo≤</mml:mo>mml:mn6</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup>mml:mo≤</mml:mo>mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>

考虑第二个不等式并将两边 除以 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mn6</mml:mn>mml:min</mml:mi>mml:mo≤</mml:mo>mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math>

对于较大的!<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>,这个不等式不成立,因为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math> 是一个常数。 因此,不可能找到一个常数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math> ,使得这个不等式对所有较大的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>成立。这否定了 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn6</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:math>的紧界限。 因此,我们得出结论 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn6</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup>mml:mo≠</mml:mo>mml:miθ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

如前所述,<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miθ</mml:mi></mml:math> 符号表示一组函数。表 3.4 提供了属于<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 的函数示例,以及它们对应的常数c1, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math>,和<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math>
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:math> 1 1 0
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mn2</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:mn4</mml:mn></mml:math> 1 2 0
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:math> 1 2 1
<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mn10</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:math> 1 10 0

表 3.4:展示一些 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

示例 3.13

说明 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math>.

解答:这两个渐近界限的主要区别在于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math> 描述了适用于所有输入的上界,而 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math> 表示紧密的界限,这意味着它提供了 上下界,因此并不适用于每个输入。 表 3.5 通过插入排序算法作为例子来说明这一差异。

O(g(n)) <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block"><mml:mi mathvariant="bold">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math>
最坏情况(****无序输入) <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>
有序输入 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:min</mml:mi></mml:math>

表 3.5:说明 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">θ</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">g</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">O</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">g</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math> 在插入排序算法中的区别

在本节中,我们探讨了时间复杂度的各种情境,然后过渡到渐进界限。 我们提供了渐进界限的正式定义,并包含了几个示例,以演示如何解决这些问题并估算 它们的参数。

面对无法解决的 NP-hard 问题

在前面的章节中,我们探讨了不同的增长速率,并学习了如何使用正式的表示方法描述它们,特别是渐进符号。 在研究这些增长速率时,我们遇到了一些看似非常具有挑战性的问题,但我们并未讨论它们是否能够解决。 在本节中,我们将重点讨论那些超出我们计算能力范围的问题,讨论 NP-hard 问题,这些问题本质上是困难的,甚至是无法高效解决的。

在计算机科学中,有一类问题挑战了我们寻找高效解决方案的能力。 这些问题被称为 NP-hard 问题 ,代表了计算机科学中一些最复杂和最具挑战性的问题。 在探索 NP-hard 问题之前,了解为什么有些问题无法高效解决,或者在某些情况下根本无法解决是至关重要的: 这就是原因所在:

  • 固有复杂性:某些问题本质上具有复杂性,随着输入大小的增加,解决它们所需的步骤数呈指数级增长。 这种复杂性通常使得在合理的时间内找到解决方案变得不切实际,甚至是不可能的。 例如,涉及检查所有可能配置或组合的问题,如旅行商问题(TSP),可能有阶乘或指数级数量的 可能解决方案。

  • 资源约束:即使在现代计算能力下,解决一些问题所需的资源(时间、内存或两者)也可能超出可行的限制。 那些需要更多计算资源的问题,超过可用资源时,变得 实际上无法解决。

  • 非确定性特性:一些问题在给出解决方案的情况下可以快速验证,但找到解决方案本身需要探索一个指数级大的搜索空间。 这些问题属于 NP 类,其中解决方案可以在多项式时间内检查,但用 当前的算法找到它们是不可行的。

  • 不可判定性:某些问题是不可判定的,这意味着没有算法可以为所有可能的输入确定解决方案。 最著名的例子是 停机问题,它询问给定的计算机程序是否最终会停止或永远运行。 艾伦·图灵证明了 没有算法能够为所有可能的程序 和输入解决这个问题。

NP,NP 完全,和 NP 困难的复杂度

在计算复杂性理论中,类 NP NP 完全,和 NP 困难 用于根据计算难度和解决这些问题所需的资源对问题进行分类。 这些分类与渐近界限有重要的联系,有助于理解算法性能的理论极限。 以下是这些问题的简短描述: 这些问题包括:

  • NP:NP 是 一类决策问题,对于这些问题,给定的解决方案可以在多项式时间内通过确定性图灵机验证其正确性或错误性。 如果一个问题属于 NP,那么就存在一个算法,可以在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 时间内验证某个 常数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math>的解决方案。

  • NP-complete: NP-complete 问题 是 NP 问题的一个子集,既在 NP 中又与 NP 中的任何问题一样困难。 如果一个问题是 NP-complete,那么每个 NP 中的问题都可以通过多项式时间归约到它。 有效地(在多项式时间内)解决一个 NP-complete 问题意味着每个 NP 中的问题都可以在多项式时间内解决,实质上是将复杂性类 P 和 NP 折叠在一起的可能性。 解决任何 NP-complete 问题的多项式时间算法的存在仍然是计算机科学中最重要的未解问题之一。

  • NP-hard: NP-hard 问题 代表了计算问题中至少与 NP 复杂度类中最困难的问题一样具有挑战性的类别。 与 NP-complete 问题相比,NP-hard 问题不局限于决策问题(是/否问题),也不一定能在 NP 内部解决。 NP-hard 问题通常以指数时间复杂度为特征。 如果一个问题是 NP-hard,目前没有已知的多项式时间算法来解决它,其渐近界限通常用超多项式函数表示,如 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo!</mml:mo>mml:mo)</mml:mo></mml:math>

NP-hard 问题是计算理论中最具挑战性和复杂性的问题之一。 它们被定义为任何 NP 问题可以在多项式时间内归约的问题。 然而,与 NP 完全问题不同,NP-hard 问题不需要是决策问题,也不必属于 NP 类。 有效地解决一个 NP-hard 问题意味着能有效地解决所有 NP 问题,而这目前是超出我们能力范围的。

研究 NP-困难问题至关重要,因为它帮助我们理解计算能力的极限以及可以实际解决的边界。 通过面对这些无法解决或不可处理的问题,研究人员努力开发更好的启发式方法、近似方法和理论见解,从而为各种应用领域提供更高效的算法。 应用。

NP、NP-完全和 NP-困难的分类为理解问题的计算难度提供了框架。 这些类别与渐近界限紧密相关,它们有助于识别算法效率的极限以及寻找多项式时间解决方案的潜力。 理解这些复杂性类别对于确定在实际时间限制内解决问题的可行性,以及指导高效算法的开发至关重要。 高效的算法。

总结

第三章中,我们研究了算法复杂度中增长速率的概念,强调了它在理解算法运行时间如何随着输入大小增加而变化方面的重要性。 这种理解对于预测算法行为和做出有根据的设计决策至关重要。 我们涵盖了多种增长速率,从常数时间 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math> 到阶乘时间 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo!</mml:mo></mml:mrow></mml:mfenced></mml:math>,并讨论了这些增长速率如何影响算法的效率和实用性,尤其是在处理 大数据集时。

我们还介绍了各种渐进符号,例如大 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi></mml:math>, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miΩ</mml:mi></mml:math>, 和 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miϴ</mml:mi></mml:math>,用于正式描述算法运行时间的上界、下界和紧界。 通过实例和比较,我们展示了不同增长率如何影响计算资源和性能。 本章为识别和分析算法的复杂性奠定了基础,提供了评估和比较算法在实际应用中效率所需的工具。

在下一章中,我们将深入探讨递归算法,并讨论如何通过递归函数来建模运行时间,然后通过估算方法研究算法的复杂性行为。

在下一章中,我们将深入分析递归算法,探索如何通过递归关系来建模它们的运行时间。 递归关系为表达递归过程的时间复杂性提供了强有力的工具,它通过将问题分解为更小的子问题并建立它们之间的关系。 我们将讨论解决这些递归关系的各种方法,例如主定理和代入法。 这些方法将帮助我们准确估算递归算法的复杂性,并理解随着输入规模的增长,它们的行为。 对递归算法的更深入理解将进一步增强我们设计高效有效解决复杂计算问题的能力。

参考文献与进一步阅读

  • 算法导论. 作者:Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest 和 Clifford Stein。 第四版。 MIT 出版社。 2022 年。

  • 算法设计. 作者:Jon Kleinberg 和 Éva Tardos。 第一版。 Pearson 出版社。 2005 年。

  • 计算机程序的艺术,第 1 卷:基础算法. 作者:Donald E. Knuth。 第三版。 Addison-Wesley Professional。 1997 年。

  • 算法概要:实践指南. 作者:George Heineman, Gary Pollice, Stanley Selkow。 第二版。 O’Reilly Media。 2016 年。

  • 算法. 作者:Robert Sedgewick 和 Kevin Wayne。 第四版。 Addison-Wesley Professional。 2011 年。

  • C++中的数据结构与算法分析. 作者:Mark Allen Weiss。 第四版。 Pearson。 2013 年。

  • 离散数学及其应用. 作者:Kenneth H. Rosen。 McGraw-Hill Science/Engineering/Math。 第十二版。 McGraw-Hill。 2012 年。

  • 具体数学:计算机科学的基础. 作者:Ronald L. Graham, Donald E. Knuth 和 Oren Patashnik。 第二版。 Addison-Wesley。 1994 年。

第六章:4

递归与递推函数

估算迭代算法的复杂度或运行时间相对简单,因为它们具有线性和可预测的性质。 然而,递归算法涉及在执行过程中函数自我调用一次或多次,在复杂度估算中提出了独特的挑战。 这些自我引用的结构通常导致复杂且非直观的运行时间,无法通过简单的观察或传统的 迭代分析轻易辨别。

为了应对这一挑战,我们引入了 递推函数的概念。 递推函数是描述递归算法运行时间的数学模型,它以输入大小为基础。 通过将运行时间表示为递归自身的函数,我们可以系统地分析并解决这些递推,以获得算法复杂度的准确估算。

本章探讨了递推函数的各个方面,包括它们的公式化、组成部分以及解决这些函数的技术。 我们将探讨这些函数如何捕捉递归调用的本质,以及它们对算法整体计算成本的影响。 理解递推函数对于准确预测递归算法的性能并优化 它们的实现至关重要。

本章结构 如下:

  • 递归算法

  • 递推函数

  • 展开 递推函数

递归算法

想象一个传统的俄罗斯套娃,它通常在俄罗斯出现。 当你打开最外层的娃娃时,你会发现里面藏着一个更小的娃娃。 打开这个更小的娃娃后,里面还有一个更小的娃娃,这个过程持续进行,直到你找到最小的、无法再分割的娃娃。 这个迷人的嵌套结构是理解递归的完美类比 在算法中的应用。

就像俄罗斯套娃一样,递归算法 通过将问题分解为同一问题的更小实例来解决问题。 每个实例都比上一个更简单,直到达到基本情况,可以直接解决,无需进一步递归。 这种自我引用的方法是计算机科学中的一个基本概念,用于以直接和 优雅的方式解决复杂问题。

在本节中,我们将探讨递归算法的原理,从其基本定义和性质开始。 我们将研究递归的工作原理,其优点和潜在风险,以及它特别适用的问题类型。 通过理解递归的核心概念,您将能够有效地设计和实现递归解决方案,利用其能力简化和解决 复杂问题。

递归的基础知识

计算机科学的发展 以深刻理解如何通过计算来模拟智能和问题解决。 在其核心,计算涉及将复杂任务分解为更简单、可重复的步骤。 这一基础概念催生了驱动我们 现代计算机的技术。

在计算机中实现重复的一个基本方法是 通过 循环 或迭代过程。 循环 是一种重复执行一系列指令直到满足特定条件的结构。 它们易于理解,其复杂度易于估计,并且相对简单调试。 然而,虽然循环对于许多任务非常有效,但有时可能会在计算上消耗大量资源,并且在某些类型的问题上不太直观。

管理重复的另一种方法是通过 递归。与循环不同,循环迭代一系列步骤, 递归 是一个函数调用自身来解决原始问题的较小实例,这称为子问题。 这种方法根植于 分治 **策略,其中问题被划分为更小的子问题,每个子问题都递归解决,直到达到基本情况。 这种方法通常可以导致更优雅和简单的解决方案,特别是对于自然适合递归结构的问题。

以下两个 Python 代码示例展示了如何使用两种不同方法实现阶乘(n)算法:迭代方法(使用循环)和 递归方法:

 def factorial_iterative(n):
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result

在迭代实现中,重复是线性且直接的。 迭代的非递归实现的一个优点是,它们的复杂度和运行时间容易估算。 在上面的函数中,运行时间取决于 <st c="4703">for i in range(1, n + 1):</st> 循环的执行次数。 考虑到循环的范围,可以清楚地看出时间复杂度是 Tn=n,简化为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

现在,让我们来看看递归实现 阶乘(Factorial(n))的实现:

 def factorial_recursive(n):
    # Base case
    if n == 0:
        return 1
    # Recursive case
    else:
        return n * factorial_recursive(n - 1)

在递归实现中,第一个显著的区别是没有使用循环。 相反,重复通过函数的嵌套自调用来实现。 这使得追踪递归算法的流程变得更加复杂。 此外,估算递归算法的运行时间不像非递归算法那样直接。 非递归算法的估算则更加简单。

为了估算运行时间,我们需要使用一种计算函数来建模递归过程,这个函数叫做递归函数,下一节将详细讨论它。 对于递归的阶乘(Factorial(n))函数,递归函数为 Tn=Tn−1+c,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 表示计算阶乘 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>的运行时间,而 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi></mml:math> 是一个常数,表示非递归操作的时间。 在下一章中,我们将看到,解决这个递归函数可以得出递归阶乘(Factorial(n))的计算复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>,这比 迭代实现的效果还差。

从阶乘(Factorial(n))的递归算法中,我们可以识别出所有递归算法中共有的三个组件:

  • 递归调用:一种在其定义中调用自身的函数,例如 factorial_recursive(n - 1)。每次调用都会处理原始问题的一个更小或更简单的版本。

  • 基准情况:递归调用停止的条件。 它防止了无限递归,并为问题的最简单版本提供了直接的解决方案。 在递归阶乘中,这个部分是 如下所示:

     if n == 0:
    return 1
    
  • 递归情况:函数中发生递归的部分。 它将问题分解为更小的子问题,并通过调用函数自身来解决这些子问题。 在递归阶乘中,这部分是 return n * factorial_recursive(n - 1)

<st c="6909">factorial_recursive(n)</st>不同,递归实现通常比非递归实现具有更好的运行时间。 让我们考虑以下使用递归方法计算一个数的幂的例子,该方法利用这一技巧来实现 提高效率:

 def power_recursive(base, exponent):
    # Base case
    if exponent == 0:
        return 1
    # Recursive case
    elif exponent % 2 == 0:
        half_power = power_recursive(base, exponent // 2)
        return half_power * half_power
    else:
        return base * power_recursive(base, exponent - 1)

让我们解释一下 这段代码:

  • 基本情况:如果 指数为 0,结果是 1,因为任何数的零次方 都是 1。

  • <st c="7947">base</st> 在递归调用之后。

  • 递归调用:该函数在 recursive(base, exponent // 2) power_recursive(base, exponent - 1)中递归调用自身。

这种方法确保递归调用的数量相对于指数以对数方式增长,从而实现 O(logn)的时间复杂度。这比迭代方法的线性时间复杂度 要显著提高 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

递归的类型

递归 可以大致分为两种类型:直接递归和间接递归。 理解这些类型有助于识别递归调用的性质及其对性能 和复杂度的潜在影响。

直接递归

直接递归发生在一个函数直接调用自身时。这是最常见的递归形式,函数是自引用的。本书迄今为止所有的递归算法示例都是直接递归类型。一些直接递归的主要应用场景如下:

  • 简化了那些可以自然分解为相同子问题的问题

  • 相较于间接递归,直接递归更易于理解和调试

  • 常用于诸如阶乘计算斐波那契数列树遍历等问题中

直接递归有几种类型,每种类型适用于不同的计算问题和算法策略。这里是直接递归的主要类型:

  • 尾递归:这是递归的一种特殊情况,其中递归调用是函数返回前的最后一步操作。这意味着不会对递归调用的结果执行进一步的操作。尾递归的优势在于它可以通过编译器进行优化,从而避免栈溢出。编译器可以复用现有的栈帧,而不是为每个递归调用创建新的栈帧,有效地将递归转化为循环。一个说明性示例是factorial_tail函数,它通过直接调用自身作为最后一步来计算一个数的阶乘:

     def factorial_tail(n, accumulator=1):
        if n == 0:
            return accumulator
        else:
            return factorial_tail(n - 1, n * accumulator)
    

    尾递归的一种应用是管理链表。尽管读者可能已经熟悉链表数据结构,我们将在第十二章中详细探讨它们。简而言之,链表是一种基本数据结构,由节点组成,每个节点包含一个值和一个指向下一个节点的引用(或链接)。链表的固有递归结构——每个节点可以看作是整个链表的一个小版本——使得递归成为执行诸如遍历、插入和删除等操作的理想方法。

    这是一个使用尾递归进行链表递归遍历的示例。 链表的遍历通常通过递归方式进行,处理当前节点后,通过递归调用移动到下一个节点。 在尾递归方法中,处理下一个节点的调用是函数中的最后一个操作。 以下是一个简单的 Python 代码:

    def traverse_linked_list(node):
        if node is None:
            return
        print(node.value)
        traverse_linked_list(node.next)
    
  • 头递归:这是指一个函数的初始操作是对自身的 递归调用。 这意味着函数中的所有其他操作都要等到递归调用完成后才会执行。 虽然头递归不如尾递归常见,但它确实有其用途。 然而,由于需要维护一个待处理操作的栈,直到递归调用展开,因此它通常被认为效率较低。 以下是一个头递归 的示例:

     def head_recursive(n):
        if n == 0:
            return
        else:
            head_recursive(n - 1)
            print(n)
    
  • 线性递归:在这种类型的 递归中,一个函数在每次调用时最多进行一次递归调用。 这导致了一个简单的递归调用链,类似一条直线。 以下是一个简单的 线性递归示例:

     def linear_recursive(n):
        if n == 0:
            return 0
        else:
            return n + linear_recursive(n - 1)
    
  • 树形递归:与线性递归不同,在树形递归中,一个函数在一次调用中会多次调用自身。 这导致了递归调用的分支结构,类似一棵树。 示例包括斐波那契数列计算、树的遍历和快速排序算法:

     def fun(n):
        if (n > 0):
            print(n, end=" ")
            fun(n - 1)
            fun(n - 1)
    
  • 二叉递归:这是指 一个函数在一次调用中对自身进行两次递归调用的模式。 这种方法常用于分治算法,它将一个问题分解成两个较小的子问题,并递归地解决它们。 这些子问题的解决方案随后被组合起来,以得到 原始问题的 解决方案:

     def fibonacci_binary_recursive(n):
        if n <= 1:
            return n
        else:
            return fibonacci_binary_recursive(n - 1) +        fibonacci_binary_recursive(n - 2)
    
  • 多重递归:这是一种递归形式,其中一个函数在一次调用中对自身进行 超过两个递归调用。 这种模式比线性递归或二叉递归更为少见,但对于那些固有地分解成多个子问题的问题来说,它可能非常有用。 这些子问题每一个都会递归求解,然后将它们的解决方案结合起来以获得最终结果。 下一个 示例中实现了一个简单的多重递归(multiple_recursive):

     def multiple_recursive(n):
        if n <= 1:
            return 1
        else:
            return multiple_recursive(n - 1) + multiple_recursive(n - 2) + multiple_recursive(n - 3)
    

    一个更复杂的例子是 如下:

    def ternary_search(arr, target, start, end):
        if start > end:
            return -1  # Target not found
        else:
            mid1 = start + (end - start) // 3
            mid2 = start + 2 * (end - start) // 3
            if arr[mid1] == target:
                return mid1
            elif arr[mid2] == target:
                return mid2
            elif arr[mid1] > target:
                return ternary_search(arr, target, start, mid1 - 1)  # First recursive call
            elif arr[mid2] < target:
                return ternary_search(arr, target, mid2 + 1, end)  # Second recursive call
            else:
                return ternary_search(arr, target, mid1 + 1, mid2 - 1)  # Third recursive call
    

    在这个例子中, <st c="14153">ternary_search</st> 函数在一个已排序的数组上执行三分查找。 它将数组分成三个大致相等的部分,并进行三次递归调用 来搜索每个部分,演示了多重递归的概念。

  • 嵌套递归:这是递归的一种更复杂的形式,其中一个函数的 递归调用不仅仅传递一个修改过的参数,而是将另一个递归调用包含在参数中。 这意味着递归的深度可能会迅速增加,使得这种递归类型比线性递归或二叉递归更难分析 和理解:

     def nested_recursive(n):
        if n > 100:
            return n - 10
        else:
            return nested_recursive_function(nested_recursive(n + 11))
    

间接递归

<st c="15199">functionA</st> 调用 <st c="15215">functionB</st>,然后 <st c="15230">functionB</st> 调用 <st c="15246">functionA</st>,形成了一个 间接递归:

 def functionA(n):
    if n <= 0:
        return "End"
    else:
        return functionB(n - 1)
def functionB(n):
    if n <= 0:
        return "End"
    else:
        return functionA(n - 2)

在间接递归的示例中, <st c="15470">functionA</st> 调用 <st c="15486">functionB</st>,然后 <st c="15514">functionA</st> 再次被调用。 这个循环会持续,直到任一函数中的基本条件满足(<st c="15604">n <= 0</st>),从而终止递归。 这种递归形式可以涉及两个以上的函数,从而形成一个复杂的调用链

间接递归的一些主要应用场景如下:

  • 对于 需要多个阶段转化或处理的问题非常有用

  • 由于涉及多个函数,因此调试和追踪可能会更加困难。

  • 通常 出现在互相递归的算法中 以及某些 状态机中

理解直接递归和间接递归之间的区别,有助于更好地选择适合给定问题的递归方法。 直接递归直接且广泛使用,而间接递归尽管更复杂,但在某些特定场景下,特别是在需要相互依赖的 函数调用时,间接递归非常强大。

递归问题解决

递归问题解决 通常 采用 分解、征服和合并 策略。 这一框架在将复杂问题分解为简单的子问题、独立解决这些子问题,然后合并其结果形成最终解决方案方面非常有效。 让我们详细探讨这一框架

  • 分解:在 分解步骤中,将问题拆解为较小的子问题,这些子问题更容易解决。 此步骤涉及确定如何将原始问题划分为更小的部分。 关键是要确保子问题与原始问题具有相同的性质,但在大小上更简单或更小。 尺寸上。

  • 征服:在 征服步骤中,子问题通过递归方式解决。 如果子问题仍然太大,则使用相同的分解、征服和合并方法进一步划分。 这个过程将继续,直到子问题达到基本情况,可以直接解决,而不需要 进一步递归。

  • 合并:在 合并步骤中,将子问题的解决方案合并,以形成原始问题的解决方案。 此步骤涉及将递归调用的结果整合,以获得 最终答案。

值得一提的是,这一框架中的关键要素是识别 子问题。一个 子问题 与原始问题相似,但在规模上更小。 本质上,子问题是原始问题的较小实例。

这一框架将在接下来的章节中详细探讨,特别是在像归并排序这样的排序算法中。 为了让读者了解这一框架,考虑以下归并排序算法,它将在 第六章中讨论:

 def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left_half = arr[:mid]
    right_half = arr[mid:]
    sorted_left = merge_sort(left_half)
    sorted_right = merge_sort(right_half)
    return merge(sorted_left, sorted_right)
def merge(left, right):
    sorted_array = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] < right[j]:
            sorted_array.append(left[i])
            i += 1
        else:
            sorted_array.append(right[j])
            j += 1
    # Append any remaining elements
    sorted_array.extend(left[i:])
    sorted_array.extend(right[j:])
    return sorted_array

让我们简要说明在归并排序算法中如何应用分解、征服和 合并框架:

  • 分治: 归并排序 函数将数组分成两部分, 左半部分 右半部分。这是通过使用数组的中点来完成的。

  • 征服: 归并排序 函数在两个子数组上递归调用。 每次递归调用都会进一步划分数组,直到达到基准情况,即数组长度为 0 1 (已排序)。

  • 合并: 归并 函数用于合并已排序的两部分。 它遍历两部分,比较元素并将较小的元素添加到排序后的数组中。 任何剩余的元素都会被 添加到数组中。

在接下来的章节中,我们将说明识别这三个组成部分对于递归算法设计和分析的重要性。

递归的优点与挑战

递归是计算机科学和算法设计中的一个基本概念。 虽然它提供了若干优点,但也带来了一些挑战。 理解这两者有助于你决定何时以及如何有效地使用 递归。

以下是递归解法的三个主要优点:

  • 简洁性和清晰度: 递归通常能为具有重复或自相似结构的问题提供更直接且直观的解决方案,如树的遍历、阶乘计算和斐波那契数列。 与迭代解法相比,递归解法通常更加简洁、易读。 这使得代码更易维护和理解。

  • 复杂问题的简化: 递归简化了将复杂问题分解为更简单子问题的过程。 这在分治算法中尤其有用,如归并排序和快速排序。 递归函数还可以产生优雅且简洁的代码,特别是当问题本身具有递归特性时,如动态规划和 组合问题。

  • 隐式状态管理:递归调用通过调用栈本身来管理状态,在许多情况下不需要显式的状态管理。 这可以简化逻辑,并减少与 状态 管理相关的错误几率。

另一方面,递归算法也带来了一些挑战。 以下是一些主要的挑战 和缺点:

  • 性能问题:每次 递归调用都会向调用栈添加一个新帧,相较于迭代解决方案,这可能会导致显著的开销,尤其是在递归深度较大时。 深度递归或无限递归可能会导致栈溢出错误,特别是当递归深度超过最大栈大小时。 这是在栈内存有限的语言中常见的问题。

  • 调试复杂性:调试递归函数可能会很具挑战性,因为它涉及跟踪多个层次的函数调用。 理解执行流程和每层递归中的状态可能会很困难。 不正确地定义基准情况也可能导致无限递归或错误结果。 确保所有基准情况都被正确处理是保证算法正确性的关键。

  • 空间复杂度:递归算法可能会因为调用栈所需的额外内存而具有较高的空间复杂度。 对于输入规模较大或递归深度较深的问题,这可能会成为一个问题。 递归函数通常需要为每次递归调用额外分配内存,这可能会导致与其 迭代 对应方法相比,增加辅助空间的使用。

此外,在设计递归算法时,我们需要注意以下实际问题。 其中一些将在 后续章节中详细讨论:

  • 尾递归:某些 语言和编译器通过将尾递归函数优化为迭代形式,从而减少了调用栈的开销。 在可能的情况下,设计递归函数时应考虑使其成为尾递归。

  • 备忘录法:使用备忘录法 存储昂贵递归调用的结果,避免重复计算。 这种技术在动态规划中尤为有用。 欲了解更多信息,请参见 第十章

  • 迭代替代方案:当递归导致性能或内存问题时,考虑使用迭代解决方案。 迭代算法通常能更高效地实现相同的结果。 迭代算法通常能以更高效的方式实现相同的结果。 更高效。

本节介绍了递归的概念,突出了其在计算机科学和算法设计中的重要性。 递归是一种方法,其中一个函数调用自身来解决相同问题的较小实例,示例包括阶乘计算和斐波那契数列。 本节通过示例(如阶乘计算和斐波那契数列)解释了递归,并强调了分治法框架,展示了如何将问题分解为更简单的子问题,独立求解后再合并以形成最终解决方案,以归并排序为 关键示例。

各种类型的递归,包括直接递归和间接递归,进行了分析,并提供了各自的示例。 讨论了递归的优点,如简洁性、清晰性和有效的问题分解,同时也探讨了其挑战,如函数调用开销、潜在的栈溢出问题以及调试复杂性。 提供了实际的考虑因素,包括尾递归、记忆化和迭代替代方案,以优化 递归函数。

总之,本节提供了关于递归的全面概述,详细阐述了其原理、优点和挑战。 为理解和应用递归问题解决技巧在算法设计中的应用奠定了坚实的基础。 下一节将讨论递归算法的运行时间模型,即 递推函数,这是 复杂度 分析的关键。

递归函数

表示 增量(非递归)算法运行时间的函数可以由于这些算法线性、顺序的特点而直接确定。 例如,考虑计算阶乘的增量实现 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 表 4.1 展示了该算法以及其在 第二列中的相关计算成本。

描述递归算法运行时间的函数并不像增量算法那样直观。 为了分析递归算法的运行时间,我们使用 递归函数 递归关系。这些概念源自 数学。

在数学中, 递归函数 是定义 n项序列中第 n 项的方程,该方程以其前几项为基础。 通常,方程只涉及序列的前 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 项,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 是一个不依赖于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>的参数。 这个参数 k被称为递归函数的阶数。 一旦知道了前 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 项的值,剩余的项就可以通过反复应用 递归函数来确定。

指令 成本
<st c="25397">def factorial_incremental(n):</st> -
<st c="25429">result = 1</st> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mic</mml:mi></mml:math>
<st c="25441">for i in range(1, n +</st> <st c="25463">1):</st> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:min</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:math>
<st c="25471">result *= i</st> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:min</mml:mi></mml:math>
<st c="25532">返回结果</st> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math/Math" display="block">mml:mic</mml:mi></mml:math>
<st c="25547">运行时间</st> <st c="25560">函数 T(n)</st> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math/Math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:min</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn>mml:mo+</mml:mo>mml:mn2</mml:mn>mml:mic</mml:mi></mml:math>
<st c="25590">复杂度</st> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math/Math" display="block">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

表 4.1:阶乘递增实现及其运行时间

例如,斐波那契序列可以通过递归函数定义,其中每一项是前两项的和。 通过知道前两项,整个序列可以生成(见 示例 4.2)。 类似地,在算法分析中,递归函数通过表达问题的时间复杂度以 更小的子问题解来帮助我们模拟递归算法的运行时间。

在递归算法的背景下, 递归函数捕捉了算法如何将问题分解为子问题、递归求解并组合它们解决方案的本质。 这种方法使我们能够系统分析和预测递归算法的性能,即使它们的执行路径和计算成本不如 增量算法那样立即显现。

两种主要的递归函数类型 减法递归 分治递归 函数。 减法和分治 递归是定义问题递归方式的两种方法,其中较大问题的解决方案用较小子问题的解决方案表达。 然而,它们在问题分解和子问题解组合方式上有所不同。 让我们详细探讨这两种递归 类型。

减法递归函数

减法递归 函数(也称为 减法与征服 递减与征服)是一类 线性递归函数,其中序列中的下一个项是前几项的线性组合。 这些 函数在数学和计算机科学中至关重要,用于建模具有 递归结构 的问题。

线性递归函数是一个方程,它通过线性系数将序列中的一项与其前面的项联系起来。 一个阶数为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 的线性递归函数的一般形式是:

an=c1an−1+c2an−2+⋯+ckan−k

这里, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:mo…</mml:mo>mml:mo,</mml:mo>mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msub></mml:math> 是常数, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 是递归函数的阶数。

减法递归函数的一般形式是: 如下所示:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:msubmml:mrowmml:mik</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:msubmml:mrowmml:mik</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mo…</mml:mo>mml:mo+</mml:mo>mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

以下是 各个组件的详细解析:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mik</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:math>: 这是一个正整数,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn0</mml:mn>mml:mo<</mml:mo>mml:msubmml:mrowmml:mik</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:mo<</mml:mo>mml:min</mml:mi></mml:math>。它表示每次递归调用中问题规模减少的步长。 如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mik</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo>mml:msubmml:mrowmml:mik</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo>mml:mo…</mml:mo>mml:mo=</mml:mo>mml:mik</mml:mi></mml:math>,线性递推函数可以写作 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:mia</mml:mi>mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mik</mml:mi>mml:mo)</mml:mo>mml:mo+</mml:mo>mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

这里, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi></mml:math> 是待解决的子问题的数量。

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:msubmml:mrowmml:mik</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:mrow></mml:mfenced></mml:math>:这是递归函数的递归部分。 它表示函数通过减少 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mik</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:math>的大小来调用自身。 这个项捕捉了递归的本质,显示当前问题如何依赖于一个 更小的子问题的解决方案。

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>:这表示算法非递归部分的运行时间。 它包括函数中执行的所有操作所花费的时间,不包括递归调用所花费的时间。 这可能包括初始化、结果组合或任何其他发生在 递归调用之外的处理。

以下是减法递归函数的主要 特性: 递归函数的主要特性:

  • 减法方法通过从原始问题大小中减去一个常数值(例如 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math>)来分解问题。 它通过减小问题大小来解决问题。

  • 较小问题的解决方案随后被用来解决原始问题,通常不需要完全解决原始问题的其余部分。

  • 这种方法不像分治法那样常见,但对于某些特定问题,能够从稍微 更小的子问题 获得解答,它是有效的。

让我们来研究这个类型的递推函数,在两个著名的阶乘和 斐波那契算法中。

示例 4.1

实现 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 使用线性递推:一个数的阶乘 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 可以使用线性递推函数来描述。 在这种情况下,问题的规模每次递归都减少 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math> ,使其成为一个简单的 线性递推。

阶乘的 递推 阶乘函数 可以 写成 如下形式:

T(n)=T(n−1)+O(1)

以下是 详细分析:

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math> 是每次递归中子问题的数量 ,它的值为 1。

  • k=1 表示在每次递归中问题的规模减少 1。 递归步骤。

  • T(n−1) 是递归部分。 它表明阶乘函数调用自身 ,并且 n−1

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 是非递归部分。 它表示在每一步执行的常量时间操作,如乘法和函数 调用开销。

以下代码为使用 线性递归的 Python 实现阶乘函数:

 def factorial_recursive(n):
    # Base case: if n is 0, the factorial is 1
    if n == 0:
        return 1
    # Recursive case: multiply n by the factorial of (n - 1)
    else:
        return n * factorial_recursive(n - 1)

这个递归函数表明时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>,因为该函数会 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 次递归调用,并且每次调用都进行常量量的工作。

阶乘算法的运行时间 对于增量法和递归法而言,其性能不可能优于θ(n)。这一限制源自问题本身的性质。 要计算 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>的阶乘,必须处理从 2 到 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>的每一个数字。 没有任何捷径或优化能将计算成本降低到低于这种线性 时间复杂度的水平。

例 4.2

斐波那契数列 线性递归函数最著名的例子之一。 它由以下递归函数定义:ºu| 递归函数:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miF</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:miF</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo>mml:mo+</mml:mo>mml:miF</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn>mml:mo)</mml:mo></mml:math>

它也被定义为 初始条件:

F(0)=0

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miF</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math>

这意味着 斐波那契数列中的每一项都是前两项之和。 斐波那契数列可以通过递归算法实现 如下所示:

 def fibonacci_recursive(n):
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)

斐波那契数列的线性递归算法的递推公式是 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo>mml:mo+</mml:mo>mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn>mml:mo)</mml:mo>mml:mo+</mml:mi>O</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>

以下是 分解:

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 表示计算第 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mo(</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>个斐波那契数的时间复杂度。 该函数以 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 作为参数递归调用自身。

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn>mml:mo)</mml:mo></mml:math> 表示计算第 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mo(</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn>mml:mo)</mml:mo></mml:math>个斐波那契数的时间复杂度。 该函数以 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn>mml:mo)</mml:mo></mml:math> 作为参数递归调用自身。

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 表示每次递归调用中执行的常数时间操作,如加法操作以及其他 常数时间操作。

斐波那契序列的递归 实现具有指数级时间复杂度, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>。这种低效源于相同的 计算被 多次重复 进行。

线性递归函数,如斐波那契序列,不仅是理论构造;它们在许多领域中都有实际应用,除了计算机科学中的算法设计外,还包括预测股价、分析投资策略以及研究生态学 和流行病学中的人口增长。

分治法递归函数

在分治法 的递归函数中, 问题规模被分解成较小的子问题,独立解决后再合并结果。 这些通常表示为: 如下:

T(n)=aT(nb)+f(n)

在这里, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi></mml:math> 是子问题的数量, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi></mml:math> 是将问题规模划分的因子, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 是划分问题和合并结果的时间复杂度。

需要注意的是,我们通常对 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi></mml:math>有一些约束,这些将在下一章介绍求解递归函数的主定理时讨论。 简单来说,对 参数的约束是:

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi>mml:mo≥</mml:mo>mml:mn1</mml:mn></mml:math> 意味着子问题的数量不能小于 1

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi>mml:mo></mml:mo>mml:mn1</mml:mn></mml:math> 意味着每个子问题应该比原始问题小,从而确保算法 最终终止

此外, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi></mml:math> 都应该是常数,并且独立于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>

以下是分治法递归函数的主要 特性:

  • 分治法将问题分解为两个或更多大小大致相等的子问题

  • 这些子问题的解决方案然后被组合起来,以得到 原始问题的解决方案

  • 这种方法通常用于那些可以自然地分解成 独立子问题

让我们以归并排序的 递归函数为例,来展示分治法的应用。

例 4.3

一个例子是 在诸如 归并排序(参见前一节)等算法中看到的递归函数,其中问题在每一步被减半,且子问题通过递归解决后再合并。

归并排序算法的递归函数如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo/</mml:mo>mml:mn2</mml:mn>mml:mo)</mml:mo>mml:mo+</mml:mo>mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

这里是 具体的拆解:

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi>mml:mo=</mml:mo>mml:mn2</mml:mn></mml:math> 是每次递归中子问题的数量。

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo/</mml:mo>mml:mn2</mml:mn>mml:mo)</mml:mo></mml:math> 表示问题被划分为两个子问题(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi>mml:mo=</mml:mo>mml:mn2</mml:mn></mml:math>),每个子问题的大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo/</mml:mo>mml:mn2</mml:mn>mml:mo)</mml:mo></mml:math>。解决每个子问题的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo/</mml:mo>mml:mn2</mml:mn>mml:mo)</mml:mo></mml:math>

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 表示合并已排序子数组所需的时间,这与数组的大小成线性关系。

在前面的例子中,展示了一个典型的分治递归,并介绍了它的关键参数。 在下一个例子中,我们将研究二分查找的递归函数,这也是一个分治方法,类似于 归并排序。

例 4.4

二分查找 是一个 经典 的分治算法示例。 二分查找通过反复将查找区间 对半分割并检查目标值位于左半部分还是右半部分来工作。 这是使用递归方法实现的二分查找的 Python 代码:

 def binary_search(arr, target, low, high):
    if low > high:
        return -1  # Target is not in the array
    mid = (low + high) // 2
    if arr[mid] == target:
        return mid
    elif arr[mid] < target:
        return binary_search(arr, target, mid + 1, high)
    else:
        return binary_search(arr, target, low, mid - 1)

二分查找算法在每次递归调用时将问题规模减半。 这可以通过以下 递归函数来表达:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:miT</mml:mi>mml:mo(</mml:mo>mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac>mml:mo)</mml:mo>mml:mo+</mml:mo>mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>

这里是 分解:

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac>mml:mo)</mml:mo></mml:math> 表示在数组的一半内进行递归调用查找。 从递归函数中我们知道 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math> 并且 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi>mml:mo=</mml:mo>mml:mn2</mml:mn></mml:math>

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 表示常数时间操作,例如将目标与中间元素进行比较并确定下一步搜索的半部分。

在本节中,我们探讨了递归函数,重点讨论了减法法和分治法两种方法。 我们学习了如何识别每种类型的递归函数并确定其组成部分的参数。 通过这一分析,我们获得了准确分类递归函数的能力。 函数的分类。

此外,我们将对递归函数的理解应用于一些著名的算法,例如搜索算法和排序算法。 我们研究了具体的例子,如归并排序和二分查找,了解递归函数如何在实践中操作,使我们能够观察到这些概念对算法效率和复杂度的直接影响。 这一综合性的研究为有效分析和设计递归算法提供了坚实的基础。 有效。

展开递归函数

到目前为止,我们已经讨论了算法设计中的递归结构,并介绍了不同类型的递归。 接着我们重点讨论了两种类型的递归函数:减法递归和分治递归。 表 4.2 总结了这两种递归函数的性质。 正如表格所示,分治递归通常能提供更高效的解决方案,尽管效率高度依赖于具体解决的问题。 问题。

在本节中,我们将揭示递归函数的奥秘。 这种理解将在下一章中发挥重要作用,我们将解决递归函数并估算其计算复杂度,换句话说,估算它们的增长速度。 通过展开这些函数,我们可以深入了解递归算法如何运作,以及它们的性能如何随着输入规模的变化而变化。 这些知识将使我们能够更有效地分析和优化算法。

无论递归函数的类型如何,它们都由两个主要元素组成:递归组件和非递归组件。 其中,递归组件 通过减法或除法减少问题的规模。 另一方面,非递归组件,也称为 驱动函数,是作为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>的函数形式表达,表示问题的大小。 问题。

递归组件 定义了如何将问题分解为更小的子问题。 对于减法递归函数,这种减少通常是常量量,例如 T(n)=T(n−k)。对于分治递归函数,问题通过一个因子来分解,例如 T(n)=aT(nb)。由于除法比减法更快速地减少问题规模,分治递归通常会导致更高效的算法。 这类似于乘法比加法增长得更快;同样,分割问题的规模通常比减去常量更迅速。 因此,基于分治策略的算法通常比使用简单 减法递归的算法更高效。

特征 减法递归 分治递归
问题分解 常量 减少。 分解成大致相等的 子问题。
子问题组合 来自一个子问题的解决方案;其他的可以 被忽略。 将多个解决方案组合以找到 整体解决方案。
子问题的数量 子问题的数量 一个(在 大多数情况下) 两个 或更多。
适用性 可以通过稍微简化的问题推导出解决方案的特定问题。 可以从更小的问题中推导解决方案。 天然可分解为 独立子问题的问题。
效率 对于那些将问题划分成子问题后能更快解决的问题,可能效率较低。 这是因为在 减法递归中,问题的规模减少得较慢。 通常,对于那些可以分解为多个子问题的问题,更有效率。 这是因为它可以利用并行处理,并且通常导致对数时间复杂度或线性对数时间复杂度。 然而,划分和合并子问题的开销,有时会让它在处理非常小的 问题规模时,效率低于减法递归。
示例算法 阶乘、二分查找、 和斐波那契。 归并排序、快速排序和斯特拉森的 矩阵 乘法算法。

表 4.2:总结减法递归与分治递归函数的主要区别

非递归部分,或称驱动函数, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>,表示递归调用外部完成的工作。 它包含了划分问题和合并子问题结果的所有操作。 这个函数在确定算法的整体时间复杂度中起着至关重要的作用。 算法的时间复杂度由它决定。

让我们将递归函数想象成一辆机械车辆。 递归组件充当引擎的角色。 值越大, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi></mml:math>,引擎运行得越慢,这意味着递归算法需要解决更多的子问题。 另一方面, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi></mml:math> 充当齿轮的角色。 值越大, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi></mml:math>,引擎工作得越快。 这意味着每一步(或递归)都会让问题的规模更快地缩小成 更小的子问题。

现在,你可能会想知道 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>的作用是什么。驱动函数,尽管这个名字可能不太直观,代表了车辆的负载。 较大的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 意味着在增长速度上,递归算法完成任务所需的工作量会更多。 这意味着,如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 增长得很快,算法每一步都需要付出更多的努力,就像车辆承载更 重的负载一样。

递归算法为许多问题提供了优雅的解决方案,但它们的时间复杂度可能很难分析。 一个有用的指南是,驱动函数,即递归调用外部的工作,设定了总体复杂度的下限。 换句话说,递归算法不能比执行 非递归操作所需的时间更快。

然而,驱动函数并不是决定最终复杂度的唯一因素。 每个递归步骤中创建的子问题数量(通常用 a表示)和问题规模减少的系数(用 b表示)起着至关重要的作用。 这些参数与驱动函数相互作用,导致不同的时间复杂度。 例如,如果子问题的数量较少,且每一步问题规模显著减少,那么驱动函数的影响可能会被掩盖。 另一方面,如果有许多子问题或者问题规模减少得很慢,驱动函数可能成为 主导因素。

为了系统地分析所有这些因素的相互作用,并准确估算递归算法的时间复杂度,我们使用了一种强大的工具—— 主定理。该定理为解决递归函数提供了一个框架,这些递归函数是数学表达式,定义了递归算法的运行时间,基于 它们的子问题。

总结

第四章中,我们探讨了递归函数的复杂性及其在分析递归算法复杂度中的关键作用。 我们首先研究了递归算法的结构,区分了减法型递归函数和分治法递归函数。 这些概念通过各种例子进行了说明,突出了不同类型的递归函数如何影响算法的整体效率。

接着,我们解释了递归函数的组成部分,强调了递归部分和非递归(驱动)部分的重要性。 本章介绍了主定理作为解决递归函数的强大工具。 通过应用该定理,我们展示了如何估算递归算法的计算复杂度,考虑子问题的数量、缩减比例和驱动函数。 详细的分析和示例为我们提供了全面的理解,帮助我们如何处理和解决递归函数,为更高级的话题,如算法设计和 复杂度分析奠定了基础。

在下一章,我们将探讨如何解决递归函数。 我们将讨论几种分析递归算法的方法,并估算它们的运行时间,包括主定理。 此外,我们还将研究这些方法在各种递归算法中的应用,深入了解它们的 时间复杂度。

参考文献及进一步阅读

  • 《算法导论》. 由 Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest 和 Clifford Stein 编著。 第四版。 MIT 出版社。 2022 年:

    • 第四章 分治法, Divide-and-Conquer

    • 第三十四章, 高级话题

  • 算法设计. 作者:乔恩·克莱因伯格和埃娃·塔尔多斯。 第一版。 皮尔逊 2005 年:

    • 第五章, 分治法 与征服

    • 第六章, 递归关系与 主定理

  • 算法. 作者:罗伯特·塞奇威克和凯文·韦恩。 第四版。 亚迪生-韦斯利 专业出版。 2011 年:

    • 第二章, 算法分析原理 算法分析原理

    • 第四章, 分治算法

  • 计算机程序设计艺术,第 1 卷:基本算法. 作者:唐纳德·E·克努斯。 第三版。 亚迪生-韦斯利 专业出版。 1997 年:

    • 第一章, 基本概念

    • 第二章, 信息结构

  • 算法设计与分析导论. 作者:阿纳尼·列维廷。 第三版。 皮尔逊 2011 年:

    • 第五章 分治法

第七章:5

解决递归函数

在上一章中,我们讨论了分析递归算法的挑战,特别是在估算其计算复杂度时。 在本章中,我们将探索三种解决递归函数的主要方法:代入法、主定理以及使用 递归树的可视化技术。

代入法通过构造严谨的证明来解决递归函数。 该方法虽然有时较为复杂,但具有很高的通用性,能够处理多种类型的递归函数。 在代入法中,我们使用了多种技术,包括数学归纳法,来验证 我们的解法。

主定理,也称为主方法,为确定递归算法的复杂度提供了一种系统化的方式,依据递归函数的参数。 该定理提供了一套简明的规则,成为分析许多常见 递归函数的强大工具。

最后,递归树通过将递归分解为树状结构,帮助可视化问题的复杂度。 虽然递归树并未提供直接的证明,但它们提供了有价值的洞察和直观理解,可以引导我们找到 正式的解决方案。

在本章中,我们将彻底审视每种方法的局限性,并展示一整套实际应用的综合示例。 到最后,读者将更加深入地理解如何有效地使用 这些技术解决递归函数。

我们将在 本章中讨论以下主题:

  • 代入法 方法

  • 递归树作为一种 可视化技术

  • 主定理 方法

  • 超越主定理—— Akra-Bazzi 方法

代入法

替换法包括一系列技术,包括归纳法,用于为递归函数提供证明。通常情况下,我们需要在进行变量替换时富有创新性,将递归函数转化为我们已知解的形式。这种方法的一个关键特征是其灵活性 - 解决同一递归可能有多种方法。虽然它并非为所有问题提供统一解决方案,但替换法是一个强大的工具。事实上,即使是主定理(在下一节中讨论)也是通过这种方法证明的。

通过采用替换法,我们可以处理各种递归函数。这个过程通常涉及假设一个解,将其代入原始递归,然后使用归纳法证明假设是正确的。这种方法允许创造性和适应性,使其成为算法分析中宝贵的技术。虽然替换法可能需要深思熟虑和机智,但它为处理复杂递归提供了一个稳健的框架。这种方法的多功能性和强大性突显了它在算法设计和分析中的重要性。

探索替代法的最好方式,也许是通过实践应用多个例子。 通过处理各种递归函数,我们可以更好地理解这一方法中的细节和技巧。 这种实践方式不仅能展示替代法的多样性和强大功能,还能证明它如何有效地应用于解决不同类型的问题。 让我们通过一些例子来观察这种方法的实际应用,从而更深刻地理解它在算法分析中的价值。 读者应注意,在本章以及本书的其他章节中,当我们提到 解决递归函数时,我们指的是估算复杂度或增长速率。 这通常涉及到确定 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miθ</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>,在少数 情况下, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Ω</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

迭代法或展开递归

在接下来的例子中,替代法通过使用迭代法应用,也称为 展开递归函数。这一技术涉及逐步展开递归,揭示出一种模式,有助于推导出闭式解。 通过将递归逐步代入原始递归,我们可以系统地识别项是如何发展的,并 积累,进而更清楚地理解函数的增长行为。 这种方法对于解决和简化复杂的 递归函数尤其有用。

示例 5.1

求解以下减法递归函数: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:min</mml:mi></mml:math>.

解答:为了求解递归函数,我们可以使用迭代法找到模式,并推导出闭式解。 我们从扩展递归步骤开始 逐步进行:

T(n)=T(n−2)+n

T(n−2)=T(n−4)+(n−2)

T(n−4)=T(n−6)+(n−4)

...

将以下内容代入 原始递归中:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:min</mml:mi></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn4</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:min</mml:mi></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn6</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn4</mml:mn></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced>mml:mo+</mml:mo><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:min</mml:mi></mml:math>

...

继续这个模式,我们得到 如下结果:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn>mml:mik</mml:mi></mml:mrow></mml:mfenced>mml:mo+</mml:mo><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo+</mml:mo><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn4</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mo⋯</mml:mo>mml:mo+</mml:mo><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn><mml:mfenced separators="|">mml:mrowmml:mik</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math>

我们在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn>mml:mik</mml:mi></mml:math> 到达基准情况时停止,我们假设基准情况是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:mn0</mml:mn>mml:mo)</mml:mo></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>。为了简化,假设 T(0)=0。所以,模式变为 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn0</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mrowmml:munderover<mml:mo stretchy="false">∑</mml:mo>mml:mrowmml:mii</mml:mi>mml:mo=</mml:mo>mml:mn0</mml:mn></mml:mrow>mml:mrowmml:mik</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:munderover>mml:mrow<mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn>mml:mii</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mrow></mml:math>

由于 T(0)=0,我们得到 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mrowmml:munderover<mml:mo stretchy="false">∑</mml:mo>mml:mrowmml:mii</mml:mi>mml:mo=</mml:mo>mml:mn0</mml:mn></mml:mrow>mml:mrowmml:mik</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:munderover>mml:mrow<mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn>mml:mii</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mrow></mml:math>

为了求和的项数,我们解方程 n−2k=0 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi>mml:mo=</mml:mo>mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:math>

因此,和为 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mrowmml:munderover<mml:mo stretchy="false">∑</mml:mo>mml:mrowmml:mii</mml:mi>mml:mo=</mml:mo>mml:mn0</mml:mn></mml:mrow>mml:mrow<mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo/</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:munderover>mml:mrow<mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn>mml:mii</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mrow></mml:math>

这是一个 等差数列,其中第一项 a=n 和最后一项 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mil</mml:mi>mml:mo=</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn><mml:mfenced separators="|">mml:mrow<mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo/</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn></mml:math>。项数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:math>

一个等差数列的和由以下公式给出: 如下所示:

S=项数的总和2×第一项的和+最后一项的和

所以,以下是情况:所示:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mfracmml:mrowmml:min</mml:mi>mml:mo/</mml:mo>mml:mn2</mml:mn></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac>mml:mo×</mml:mo><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo+</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced></mml:math>

因此,递推函数的闭式解为如下:所示:

Tn=n24+n2

这为我们提供了渐近复杂度的分析Tn=θn2

在前面的例子中,我们演示了如何使用替代法证明一个递减递推函数。替代法。

例 5.2

解以下递减递推函数:!<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>

解法:这个递归函数描述了一个算法,其中大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 的问题被减少到大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn></mml:math>的子问题。递归调用外部的工作量是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>。我们可以使用迭代或展开法通过替换法来解决这个递归。

让我们展开这个递归 几次:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn4</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:msupmml:mrow<mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn6</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:msupmml:mrow<mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn4</mml:mn></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:msupmml:mrow<mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo+</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>

...

如果我们继续 这个模式,我们最终会得到基本情况 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:mn0</mml:mn>mml:mo)</mml:mo></mml:math> (取决于基本情况是如何定义的)。 请注意,我们得到一个 平方和:

Tn=Tbasecase+12+22+…+n−22+n2

从 1 到 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 的平方和是一个 著名的公式:

12+22+…+n2=nn+12n+16

因此,以下是 情况:

Tn=Tbasecase+nn+12n+16

精确解依赖于 T(basecase),它是常数时间(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>)。 然后,从渐近意义上讲,我们可以说 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

在这个例子中,为了实现替代方法,我们对递归进行迭代或展开,以识别增长模式。 递归函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math> 的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>。这表明算法的运行时间随着输入规模的增加而立方增长。

示例 5.3

解决以下减法递归 函数: T(n)=T(logn)+n

解答:我们可以通过展开递归并找到 一个模式来解决它。

让我们解释递归函数。 这个递归函数涉及子问题规模的对数递减。 递归函数 T(n)=T(logn)+n 描述了一个算法,其中以下内容为 情况:

  • 问题的规模 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 被缩减为一个子问题,规模为 大小为 logn

  • 递归调用外的工作 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>

让我们通过展开递归来使用迭代方法,以理解 该函数的行为:

T(n)=T(logn)+n

T(n)=T(loglogn)+logn+n

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mo+</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mo+</mml:mo>mml:min</mml:mi></mml:math>

...

注意,每次 迭代都会将对数函数应用于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi></mml:math>的参数。达到基准情况(例如 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>)之前的迭代次数大约是我们可以对 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 进行对数运算的次数。 这个次数大约是 log*n(迭代对数)。

所有迭代中的总工作量大约是 如下所示:

n+logn+loglogn+logloglogn+…log*nterms

前述和式中的主导项是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>。由于重复应用对数运算,后续每个项的增长速度远慢于前一个项。 因此,我们可以说 如下所示:

T(n)=O(n)

在前面的例子中,算法的运行时间主要由递归调用之外的工作决定(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>)。 递归调用对总体运行时间的贡献不大,因为每次递归时问题规模会非常快速地减少(对数级别)。 迭代对数(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi></mml:mrow>mml:mrowmml:mi*</mml:mi></mml:mrow></mml:msup>mml:min</mml:mi></mml:math>) 是一个增长非常缓慢的函数。 在所有实际应用中,它可以视为常数。 这就是为什么我们可以说时间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 尽管在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi></mml:mrow>mml:mrowmml:mi*</mml:mi></mml:mrow></mml:msup>mml:min</mml:mi></mml:math> 求和中有 项。

猜测和归纳法

示例 5.3 可以通过猜测和归纳法求解。 我们通过猜测并使用替代法解决 T(n)=T(logn)+n 假设 T(n)=O(n) 并通过归纳法证明它。 示例 5.4中,我们展示了如何使用猜测和 归纳法求解递归函数。

示例 5.4

求解 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:miT</mml:mi>mml:mo(</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mo)</mml:mo>mml:mo+</mml:mo>mml:min</mml:mi></mml:math> 通过猜测 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

我们通过猜测来解决 T(n)=T(logn)+n 使用替代法,假设 T(n)=O(n) 然后通过归纳法证明它 来完成。

这是 逐步解决方案:

  1. 猜测 形式:

    我们假设 T(n)=O(n)。具体地,假设 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:mic</mml:mi>mml:min</mml:mi></mml:math> 对于某个 常数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi>mml:mo></mml:mo>mml:mn0</mml:mn></mml:math>

  2. 基础情况:

    我们需要建立一个基例。 对于较小的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> (例如, n=1),递推函数会简化。 假设 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 是一个常数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math> 由于我们关心的是渐进行为,我们专注于较大的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>

  3. 归纳假设

    假设 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mik</mml:mi></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:mic</mml:mi>mml:mik</mml:mi></mml:math> 对于所有 k≤n成立。 我们需要证明 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:mic</mml:mi>mml:min</mml:mi></mml:math> 也成立。

  4. 归纳步骤

    使用递推函数,我们 得出 T(n)=T(logn)+n

    根据归纳假设,<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:mic</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:math>。将其代入递归函数中,我们得到<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:mic</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mo+</mml:mo>mml:min</mml:mi></mml:math>

    对于我们的假设<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:mic</mml:mi>mml:min</mml:mi></mml:math>,我们需要clogn+n≤cn

  5. 简化不等式:

    为了满足这个不等式,我们需要<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mic</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:mfrac>mml:mo≤</mml:mo>mml:mic</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:math>

    随着<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 增大,<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:mfrac></mml:math> 接近<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn0</mml:mn></mml:math>。因此,存在一个充分大的<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>,使得:

  6. 选择 常数

    我们可以选择<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi></mml:math> 使得前述条件在充分大的<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>下成立。例如,对于c=2,有:

这个 不等式在足够大的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>时成立。因此,我们的假设 T(n)=O(n) 是有效的。 因此,我们已经证明了该递归的解是 Tn=Tlogn+nisTn=On

通过这种猜测和归纳的方法,我们确认了最初的猜测是正确的,并且该递归的复杂度是线性的 关于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>

猜测与归纳法中的猜测步骤是求解递归函数的关键部分。它涉及根据递归函数的结构对解的形式做出有根据的假设。 以下是猜测步骤的 常规操作:

  • 理解递归函数:我们首先仔细检查递归函数。 我们查看其中的各个组成部分,例如在每次递归调用中问题规模的变化以及非递归的成本函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>。然后,我们将递归函数与熟悉的模式进行比较,比如常见算法中的模式(例如,归并排序或 二分查找)

  • 分析增长率:我们考虑递归函数中的项,以推测它们如何贡献于整体复杂度。 例如,如果递归涉及如下项 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:math>,我们可能会猜测解中包含一个对数项,因为每一步都将问题规模减少一个倍数。 如果递归包括一个加法线性项,例如 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>,这表明解可能涉及线性或非线性的 增长 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:math>

  • 利用经验和模式:我们使用对常见递推函数及其解的了解。 例如,如果递推式看起来像 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mi </mml:mi>mml:mn2</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mi </mml:mi>mml:min</mml:mi></mml:math>,我们可能猜测 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mi </mml:mi>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math> ,因为这个形式对于分治算法来说是典型的。 对于递推式 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mi </mml:mi>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mi </mml:mi>mml:mn1</mml:mn></mml:math>,我们可能猜测 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mi </mml:mi>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> ,因为每一步都会将问题规模减少 1,且具有 常数成本。

  • 做出有根据的猜测:基于我们的分析,我们假设一个可能的形式为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>。这可能是 ![<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:r></mml:mfenced>mml:mo=</mml:mo>mml:mi </mml:mi>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced>+ 细化猜测:有时,在通过归纳法验证初步猜测后,我们可能需要对猜测进行细化。 例如,如果我们的猜测 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mi </mml:mi>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 未满足归纳步骤,我们可能需要考虑一个更高阶的项,如 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow>mml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mo>⁡mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>

通过遵循 这些步骤,我们系统地得出了一个合理的解决方案,可以通过严格的验证。 猜测步骤结合了直觉、经验和分析,提出一个解决方案,然后你可以通过 归纳法进行验证。

变量变换方法

存在 一些不规则的递归函数,传统方法,如 主定理**定理 部分,甚至 递归树作为可视化技术 部分,无法解决。 在这些情况下,替代法提供了一种替代解决方案,还有一些先进的、广义的方法,比如 Akra-Bazzi 方法 (见 超越主定理——Akra-Bazzi 方法 部分)。

替代法 方法 涉及通过变量变换将原始递归转化为更易处理的形式。 这种方法可以揭示模式并简化分析,使得找到闭式解变得更加容易。 通过仔细选择新变量,我们可以将复杂的递归函数转换为更直接 易于求解的形式。

在下一个示例中,我们演示了如何使用变量替换方法有效地解决不规则的递推函数。 这个例子将展示替换法在处理其他方法无法应对的复杂递推函数时的威力与灵活性。

例 5.5

求解 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:math>

在前一章中,我们将递推函数分为两类:减法(递减)递推函数和分治法(递分)递推函数。 虽然函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:math> 可以被视为分治递推函数的一个子类,但它更准确地属于 替换法 递推函数的一类。

为了解决这个递推函数,我们可以使用变量替换的方法来简化递推,从而更容易进行分析。 这将使分析变得更加简便。

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo=</mml:mo>mml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mim</mml:mi></mml:mrow></mml:msup></mml:math>

然后, logn=m

将递归关系重新表达为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math>,我们得到 以下结果:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mim</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mim</mml:mi>mml:mo/</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mim</mml:mi></mml:math>

为了简化符号,设 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mim</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>。那么我们得到 以下结果:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mim</mml:mi></mml:math>

这是一个常见的递归函数形式,适用于像归并排序算法(参见 第六章),其递归关系为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:min</mml:mi></mml:math> ,其渐近界为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>。因此, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:mim</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:mim</mml:mi></mml:mrow></mml:mfenced></mml:math>。通过将 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math> 替换为 logn,我们得到 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>。然而,为了彻底性,我们假设我们不知道这个结果,并详细求解。

现在,我们解这个新的递推函数!<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miS</mml:mi>mml:mo(</mml:mo>mml:mim</mml:mi>mml:mo)</mml:mo></mml:math>。让我们展开递推几步,找出其中的模式:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mim</mml:mi>mml:mo/</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:math>

替换为<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miS</mml:mi>mml:mo(</mml:mo>mml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac>mml:mo)</mml:mo></mml:math>进入第一个方程:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mn2</mml:mn>mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mim</mml:mi></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn4</mml:mn>mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mn2</mml:mn>mml:mim</mml:mi></mml:math>

再展开一步:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn8</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:math>

替换 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo:</mml:mo></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn4</mml:mn><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mn2</mml:mn>mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn8</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mn2</mml:mn>mml:mim</mml:mi></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn8</mml:mn>mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn8</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mn3</mml:mn>mml:mim</mml:mi></mml:math>

通过展开这个模式,我们可以看到在每个层级中,S的系数<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miS</mml:mi></mml:math> 呈指数性下降,而 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math> 的系数则呈线性增加。

我们可以在经过<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 步后概括这个模式:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup>mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mik</mml:mi>mml:mim</mml:mi></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfrac></mml:math> 变为 1 时,我们得到 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi>mml:mo=</mml:mo>mml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:math> ,这意味着 k=logm 代入

k=logm

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:mim</mml:mi></mml:mrow></mml:msup>mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo><mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mim</mml:mi></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mim</mml:mi>mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mim</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:mim</mml:mi></mml:math>

已知 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>,为常数,我们表示为 c <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi></mml:math>

因此,我们得到 如下结果:

S(m)=mc+mlogm

S(m)=m(c+logm)

代入 返回 m=logn

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mim</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi><mml:mfenced separators="|">mml:mrowmml:mic</mml:mi>mml:mo+</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

因此,递推函数的解 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:math> 如下所示:

Tn=Θlognloglogn

在这一部分,我们解释了一种强大的技术,通过假设一个解并通过数学归纳法证明其正确性来解决递推函数。 该方法包括将猜测的解代回递推函数中,以验证它是否满足原方程。 这种方法允许在处理各种递推形式时具有灵活性和创造性,特别是对于那些其他方法(如主定理或递归树)无法轻松处理的递推形式。 通过反复精炼猜测并使用归纳法,替代方法提供了一种有结构的方式来推导闭式解,并理解 递归算法的增长行为。

递归树作为一种可视化技术

递归树方法 是一种强大的技术,用于解决和可视化递归函数,尤其是在分析分治算法时非常有效。 它通过将递归过程可视化为一棵树来实现,每个节点代表一个子问题,而边表示递归调用。 通过求和树中每一层的成本,我们可以确定该算法的整体复杂度。

这是递归树方法的逐步 解释:

  1. 构建 递归树

    • 首先写下原始的 递归函数

    • 树中的每个节点代表一次对 递归的调用

    • 树的根节点对应于原始的 问题 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

    • 一个节点的子节点表示由 递归调用 生成的子问题

  2. 识别 成本

    • 确定每个节点的成本。 此成本通常对应于该步骤中的非递归工作,通常表示为 f(n) <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

    • 写下根节点的成本并将其传播到 树中。

  3. 展开

    • 通过根据 递归函数 将每个子问题拆解成其组成部分,继续展开树。

    • 此过程会持续进行,直到子问题达到 递归的基本情况

  4. 计算每一层的总成本 每一层

    • 求出 每一层中所有节点的 成本

    • 确定每一层的节点数量和每个节点的成本 每个节点

  5. 求和各层的成本 所有层级的成本

    • 将树中所有层的成本加起来,得到 总成本

    • 分析求和以确定整体渐进复杂度

递归树方法通过将递归问题分解为较小的子问题并以树形结构进行可视化,来分析其复杂度。从根节点开始,方法包括绘制每一层递归,其中每个节点代表一个子问题,而边表示递归调用。 在每一层,计算并汇总解决所有子问题的成本。 通过展开树直到达到基准情况,并汇总所有层级的成本,可以确定算法的总复杂度。 该方法提供了一种清晰且系统化的方式来理解成本的分布及递归算法的整体行为,通常能够识别出递归的渐进复杂度。 让我们在 下一个示例中进一步探索该方法。

示例 5.6

求解 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn4</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:min</mml:mi></mml:math> 使用递归树方法。

这是解决方案:

  1. 构造递归树

    • 树的根是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

    • 这将分解为 4 个子问题,每个子问题的大小 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:math>

    ...

    第 0 层:<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

    / | | </st>

    第 1 级: 4*T(n/2)

    / / / / | | | | \ \ \ </st>

    第 2 级: 4*T(n/4) (16 个子问题)

    ...

  2. 识别 成本

    • 根节点(第 0 级)处的成本 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>

    • 在第 1 级,每个 4 个子问题 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:math>的成本是 ! n2

    • 在第 2 级,每个 16 个子问题 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:math>的成本是 ! n4

    ...

  3. 展开

    • 继续展开,直到子问题达到基本情况(例如, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>)

    • 树的层数是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:math> 因为每一层 问题大小会减少一半

  4. 计算每一层的总成本 每一层

    • 第 0 层: 成本 = <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>

    • 第 1 层: 成本 = <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn4</mml:mn>mml:mo⋅</mml:mo><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:min</mml:mi></mml:math>

    • 第 2 层: 成本 = <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn16</mml:mn>mml:mo⋅</mml:mo><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn4</mml:mn>mml:min</mml:mi></mml:math>

    • 一般层级 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math>: 成本 = <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:mn4</mml:mn></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msup>mml:mo⋅</mml:mo><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msup>mml:min</mml:mi></mml:math>

  5. 总计各层级的成本 所有层级

    • 总成本 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 是所有层级的成本之和: 所有层级:

      • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:min</mml:mi>mml:mo+</mml:mo>mml:mn2</mml:mn>mml:min</mml:mi>mml:mo+</mml:mo>mml:mn4</mml:mn>mml:min</mml:mi>mml:mo+</mml:mo>mml:mn8</mml:mn>mml:min</mml:mi>mml:mo+</mml:mo>mml:mo…</mml:mo>mml:mo+</mml:mo>mml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:msup>mml:min</mml:mi></mml:math>
    • 这是一个等比数列,其首项为 a=n 和公比为 比例 r=2

      • S=ark+1−1r−1
    • 在我们的例子中, a=n r=2 并且 k=logn

      • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:min</mml:mi>mml:mfracmml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrow<mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:msup>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow>mml:mrowmml:mn2</mml:mn>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfrac>mml:mo=</mml:mo>mml:min</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn2</mml:mn>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>
    • 简化后,我们得到以下结果: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo-</mml:mo>mml:min</mml:mi></mml:math>

    • 渐近地,主导项是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn2</mml:mn>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>,因此我们得到以下结果: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

递归树方法 提供了递归算法如何将一个问题分解成子问题的清晰视觉表示。 通过对树中每一层的成本求和,我们可以确定算法的总复杂度。 这种方法特别有助于理解和解决复杂的递归函数,能够为 分治算法 提供洞察。

主定理

算法分析中, 主定理 在求解分治算法的递归方程中起着关键作用。 主定理于 1980 年提出,现已成为估算各种递归函数复杂度的主流方法。 主定理提供了一个简洁的框架,用于确定以下形式递归的渐近行为:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mia</mml:mi>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

这里, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi>mml:mo≥</mml:mo>mml:mn1</mml:mn></mml:math> 并且 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi>mml:mo></mml:mo>mml:mn1</mml:mn></mml:math> 是常数,并且 f(n),驱动函数,是一个渐近正函数,由多项式函数界定。 这意味着存在两个多项式函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mig</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mih</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 使得以下等式成立: 情况如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:mih</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

主定理的重要性在于它能够简化许多常见算法(如归并排序、快速排序和二分查找等)的复杂度分析。 通过基于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mrowmml:mrowmml:msubmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mia</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:msup></mml:math>之间的关系来对递归行为进行分类,主定理使得复杂度估计变得快速且准确,而无需详细的逐个分析。

虽然我们在这里没有提供主定理的证明,但读者可以参考本章末尾的文献,获取详细的证明和进一步的阅读资料。 理解主定理及其应用对于任何学习算法设计与分析的人来说都是至关重要的,因为它为评估许多 递归算法的效率奠定了基础。

让我们来探索主定理的关键概念,这些概念对于理解和描述这种方法的不同情况是必不可少的:

  • 临界指数(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miw</mml:mi></mml:math>:这个值, w=logba,代表了一个阈值,用于比较驱动函数 的增长速率 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 与递归部分的 递归函数的增长速率。

  • 分水岭函数(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:miw</mml:mi></mml:mrow></mml:msup></mml:math>:这个函数, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:miw</mml:mi></mml:mrow></mml:msup>mml:mo=</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mrowmml:mrowmml:msubmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mia</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:msup></mml:math>,作为 分界线 区分了 主定理的不同情况。 它告诉我们递归的增长速率,假如驱动函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 被忽略时。

案例 1 – 递归调用的主导作用或叶子重的递归树

案例 1中, 递归调用之外的工作量(驱动函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>)被递归调用中的工作量所主导。 驱动函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 以多项式的方式增长,但增长速度比分水岭函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:miw</mml:mi></mml:mrow></mml:msup></mml:math>)慢。 更正式的表达是:

fn=Onc其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi>mml:mo<</mml:mo>mml:miw</mml:mi></mml:math>

或者,等效地,这是 如下所示:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:miw</mml:mi>mml:mo-</mml:mo>mml:miε</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 对于某些 ε>0

这意味着 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 的上界是一个多项式,其指数小于 分水岭。

为什么 情况 1 是叶节点重的? 为了 理解为什么 情况 1 被称为 叶节点重,请想象一个递归树来表示算法的执行过程。 每个节点代表一次递归调用,其子节点代表子问题。 在这种情况下,树中每一层所做的工作(由 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>)与递归调用的次数相比相对较小。 因此,大部分的工作都在递归树的叶节点上完成,因此称为 术语 叶节点重

当递归调用占主导时,算法的整体时间复杂度由递归树中的叶子节点数量决定。 这个数量随着树的深度呈指数增长。 由于树的深度与输入大小的对数成正比(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub>mml:min</mml:mi></mml:math>),因此,整体时间复杂度变为: 以下形式:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:miw</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub>mml:mia</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

换句话说,时间复杂度主要由递归将问题拆分成更小的子问题来主导。 如果递归调用外的工作量(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>)相比于递归内部的工作量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mo(</mml:mo>mml:mia</mml:mi>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo)</mml:mo></mml:math>时,绝大部分时间都花费在递归调用中,从而使递归树呈现出叶子结点较重的特点。 在这种情况下,总的运行时间主要由在达到基本情况之前,可以将问题分割多少次来决定。 这一点由指数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miw</mml:mi>mml:mo=</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub>mml:mia</mml:mi></mml:math>来表示。

示例 5.7

考虑这个递归: T(n)=2T(n2)+n

在这里, a=2 b=2,和 f(n)=n。我们有 w=log22=1。由于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:min</mml:mi>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 情况 1 适用,时间复杂度如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

情况 2 – 平衡增长或平衡递归树

情况 2 中的 主定理处理的场景是,递归调用外部的工作量(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>)以与递归调用内部的工作量大致相同的速率增长。 这导致了一个平衡的递归树,其中每一层贡献相同量的工作。 情况 2中,驱动函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 以与水分界函数相同的速率多项式增长,可能还带有附加的对数因子。 更正式地,这表示如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:miw</mml:mi></mml:mrow></mml:msup>mml:mil</mml:mi>mml:mio</mml:mi>mml:msupmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 对于 某些 k≥0

意味着 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 被一个与 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:miw</mml:mi></mml:mrow></mml:msup></mml:math> 成比例的函数(上下界)紧密界定。 并乘以一个 对数因子。

为什么 Case 2 是平衡的?在递归树中,每个级别的工作量大致与该级别的节点数成比例。 Case 2。随着深度增加,每个级别的节点数呈指数增长,因此每个级别的工作量也呈指数增长。 然而, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 中的对数因子减缓了这种增长,导致工作在递归树中更 均衡 地分布。

当工作在递归树的各级别间平衡时,整体时间复杂度由所有级别的总工作量决定。 这可以通过计算每个级别的工作量之和来计算,其结果如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:miw</mml:mi></mml:mrow></mml:msup>mml:mil</mml:mi>mml:mio</mml:mi>mml:msupmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:msup>mml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub>mml:mia</mml:mi></mml:mrow></mml:msup>mml:mil</mml:mi>mml:mio</mml:mi>mml:msupmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:msup>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

换句话说,时间复杂度是分水岭函数的乘积,再加上一个考虑每个级别工作量的额外对数因子。 每个级别。

如果递归调用外的工作(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>)与递归内部的工作(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi>mml:miT</mml:mi>mml:mo(</mml:mo>mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:mfrac>mml:mo)</mml:mo>mml:mo)</mml:mo></mml:math>)大致以相同的速度增长,那么递归树是平衡的。 每一层对总体运行时间的贡献都很大。 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 中的对数因子代表了由于递归的平衡性质,在每一层上所做的额外工作。

示例 5.8

考虑这个 递推关系: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrow<mml:mi mathvariant="normal">n</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:math>

在这里, a=2 b=2,以及 f(n)=nlogn。我们有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miw</mml:mi>mml:mo=</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mn2</mml:mn>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math>。由于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msup>mml:mil</mml:mi>mml:mio</mml:mi>mml:msupmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msup>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 情况 2 适用于 k = 1,时间复杂度如下: 如下所示:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msup>mml:mil</mml:mi>mml:mio</mml:mi>mml:msupmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:msupmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

情况 3 – 非递归工作或根重递归树的主导作用

情况 3 主定理处理的是递归调用外部的工作(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>)明显主导了递归调用中的工作量。 这导致了一个“根重”的递归树,其中大部分工作集中在 树的顶层。

情况 3中,驱动函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 以比临界点函数更快的速度多项式增长,可能还带有额外的对数因子。 更正式地说,这是 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΩ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:miw</mml:mi>mml:mo+</mml:mo>mml:miε</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 对于某些 ε>0

这意味着 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 被一个比 临界点 的多项式下界所限制,该多项式的指数更大。

除了增长率条件外,案例 3 还需要满足一个规则性条件: afnb≤cfn 对于某个常数 c<1 和足够 大的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>

此条件确保递归树每一层的工作量不会随着我们向下遍历树而增长得过快。

为什么 案例 3 是根部集中型的? 再想象一下递归树。 案例 3中,树根的工作量(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>)远大于子问题中的工作量。 随着我们向下遍历树,每一层的工作量显著减少。 这种工作量在树顶层集中的现象就是为什么 案例 3 被称为 根部集中型

当非递归工作占主导时,算法的整体时间复杂度主要由递归树根部的工作量决定,即 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>。因此,时间复杂度变为 以下形式:

T(n)=Θ(f(n))

如果递归调用外的工作量(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>)远大于 递归内部的工作量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mo(</mml:mo>mml:mia</mml:mi>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:math>),那么大部分时间都花费在开始时,使得递归树呈现“根重”形态。在这种情况下,整体运行时间主要由在开始拆分问题之前完成的工作量决定 进入子问题的处理。

示例 5.9

考虑以下递归: 以下递归:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>

这里, a=2,b=2,和 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>。我们有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miw</mml:mi>mml:mo=</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mn2</mml:mn>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math>。由于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo=</mml:mo>mml:miΩ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 并且如果正则条件成立, 情况 3 适用,时间复杂度为 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

为了评估 正则性条件,我们检查是否 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn2</mml:mn>mml:msupmml:mrow<mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo≤</mml:mo>mml:mic</mml:mi>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math> 其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi>mml:mo≤</mml:mo>mml:mn0.5</mml:mn></mml:math>. 正则性条件成立,且 情况 3 适用于 该问题。

正则性条件对于 情况 3至关重要。如果没有它,定理的结论可能不成立。 一些函数,如 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfrac></mml:math>,可能看起来最初符合 情况 3 ,但无法满足 正则性条件。

修改后的主定理用于解决递减递归函数

主定理 可以扩展以处理以下形式的递减递归函数的特例:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mia</mml:mi>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mib</mml:mi></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

这里, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 其中 k≥0. 让我们进一步分析:

  • a>0 表示减法递归函数中的子问题数量

  • b>0 是每个子问题的规模缩小程度,其中 n−b 表示 每个子问题的大小

在这类问题中,我们有三种 不同的情况:

情况 1:如果 a<1 那么Tn=θnk

当子问题的数量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi></mml:math> 小于 1 时,递推式的主导项是驱动函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>。因此,整体复杂度直接由 给出 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

案例 2:如果 a=1 Tn=θnk+1

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi></mml:math> 等于 1 时,每个子问题对整体复杂度的贡献是相同的,从而导致最终解中的附加因子 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 。因此,复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi>mml:mo+mml:mn1</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

情况 3:如果 a>1 那么Tn=θan/b.fn

当子问题的数量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi></mml:math> 大于 1 时,递归函数呈指数增长。 总体复杂度受指数增长因子 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi>mml:mo/</mml:mo>mml:mib</mml:mi></mml:mrow></mml:msup></mml:math> 和驱动函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>的组合影响 导致 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi>mml:mo/</mml:mo>mml:mib</mml:mi></mml:mrow></mml:msup>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math>

例子 5.10

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn0.5</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn3</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:min</mml:mi></mml:math>

解决方案:让我们确定 关键参数:

  • a=0.5 是每次递归时子问题数量减少的因子 每次递归

  • b=3 是每次递归时问题规模减少的量 每次递归

  • k=1 是多项式 项的指数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>

由于 a<1,我们可以应用 案例 1

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

在我们的例子中,已知 a=0.5 b=3,以及 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:min</mml:mi>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>。由于所有条件都已满足, 案例 1 告诉我们 以下内容:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

这意味着由递归函数描述的算法的时间复杂度是线性的(即,它与输入大小成正比增长) <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>)。 由于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn0.5</mml:mn></mml:math>的因素,递归调用的成本迅速降低,而线性项 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 最终主导了 运行时间。

例 5.11

解: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>

解答:让我们识别关键参数: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi>mml:mo=</mml:mo>mml:mn2</mml:mn></mml:math> 以及 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi>mml:mo=</mml:mo>mml:mn2</mml:mn></mml:math>

由于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math>,我们可以应用 情况 2

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

示例 5.12

解答 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:mn4</mml:mn>mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo>mml:mo+</mml:mo>mml:min</mml:mi></mml:math>.

解法:让我们确定关键参数: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi>mml:mo=</mml:mo>mml:mn4</mml:mn></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math>

在我们的 问题中,我们有 a=4,它大于 1\。 因此,我们属于 情况 3 修改后的主定理。 代入我们的值,我们得到 如下结果:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn4</mml:mn></mml:mrow>mml:mrow<mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo/</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:mrow></mml:msup><mml:mi mathvariant="normal">*</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn4</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup><mml:mi mathvariant="normal">*</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

该算法的时间复杂度是指数级的,具体为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn4</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup>mml:mi*</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>。这意味着随着输入大小的增加,运行时间增长得非常快 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 指数项 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:mn4</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:math> 主导着增长,而线性项 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 随着 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 增大而变得可以忽略不计。

主定理的限制

主定理是分析分治算法的强大工具。 然而,它的有效性仅限于特定类型的递归函数。 认识到这些限制对于选择分析更复杂算法时合适的方法至关重要:

  • 驱动函数的限制 函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">f</mml:mi>mml:mo(</mml:mo><mml:mi mathvariant="bold-italic">n</mml:mi>mml:mo)</mml:mo></mml:math>

    • 非多项式函数:主定理假设递归调用之外的工作(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>)是一个多项式函数(例如, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math> nlogn如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 是指数函数(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:math>),对数函数(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:math>),阶乘函数(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo!</mml:mo></mml:math>),或其他非多项式形式,主定理不能 直接应用。

    • 非正函数:主定理要求 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 对于所有相关的输入大小,必须严格为正。 如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi></mml:math>) 变为负数或零,定理的假设 将被违反。

    • 非光滑函数:主定理假设 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>具有一定的光滑性。具有突变、间断或分段定义的函数可能不符合该定理的框架。

    • 不规则函数:该定理的有效性取决于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 与函数的递归部分相比,具有规则的增长模式。 具有振荡行为或可变指数的函数可能会挑战该定理的适用性。

  • 递归参数的约束(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">a</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">b</mml:mi></mml:math>****)

    • 非恒定参数:主定理假设子问题的数量(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi></mml:math>)以及输入规模减少的因子(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi></mml:math>)是常数。 如果这些中的任何一个依赖于输入规模 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>,则主定理不再适用。

    • 非整数除数:该定理假设输入被均匀地划分为子问题(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo/</mml:mo>mml:mib</mml:mi></mml:math>)。 如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi></mml:math> 是分数或无理数, 则可能会导致定理无法处理的复杂情况。

  • 递推函数的限制 函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">T</mml:mi>mml:mo(</mml:mo><mml:mi mathvariant="bold-italic">n</mml:mi>mml:mo)</mml:mo></mml:math>

    • 非单调函数:主定理假设时间复杂度函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 是单调递增的,这意味着随着输入大小的增加,算法的运行时间不会减少。 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:mic</mml:mi>mml:mio</mml:mi>mml:mis</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 等函数 违反了这一假设。

替代方法

当主定理的条件不满足时,可以使用替代方法来分析 递推函数:

  • Akra-Bazzi 方法:该 方法推广了主定理,并能够处理更广泛的递推函数,包括那些具有非多项式差异的函数。 它提供了一种系统化的方式来计算 这些递推的渐近复杂度。

  • 代入法:该 方法涉及猜测递推的解,并通过 数学归纳法 来证明其正确性。

  • 迭代法:这涉及到反复展开递推函数,揭示出一种模式,从而得到一个 闭式解。

  • 递归树方法:这种 可视化方法帮助理解递归调用的结构,并估计每一层的整体工作量。

在下节中,我们将介绍 Akra-Bazzi 方法,这是对主定理的推广。 该方法扩展了我们解决更广泛递推函数的能力,克服了主定理中的许多局限。

我们来看几个例子,这些例子展示了主定理的局限性。

例子 5.13

求解 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:math>

解法:主定理在这里不适用,因为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi>mml:mo=</mml:mo>mml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:math> 不是常数。

为了求解这个 递推关系,我们需要探索其他方法,如代入法、迭代法或 Akra-Bazzi 方法,这些方法能够处理 a以及复杂的 形式 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:math>

例子 5.14

求解 (Tn=2Tn2+nlogn

解答

在分析每种情况之前,我们先复习一下 递归函数的组件:

a=2

b=2

临界指数是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:miw</mml:mi></mml:mrow></mml:msup></mml:math> 其中 0<w<log22=1

f(n)=nlogn

现在,让我们评估 主定理中的每种情况:

情况 1

检查是否 nlogn=On1−ε

这不成立,因为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfrac></mml:math> 增长速度比 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn>mml:mo-</mml:mo>mml:miε</mml:mi></mml:mrow></mml:msup></mml:math>更快。

案例 2:

检查是否Check if nlogn=θnlogkn。我们需要找到一个 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 使得以下条件 成立:

nlogkn≤nlogn≤nlogk+1n

不存在这样一个 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 能够夹住 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfrac></mml:math> 在这些函数之间。

案例 3:

检查是否 nlogn=Ωn1+ε. 这可能适用,因为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfrac></mml:math> 增长得比 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrow<mml:mi mathvariant="normal">n</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn>mml:mo+</mml:mo>mml:miε</mml:mi></mml:mrow></mml:msup></mml:math>要快 然而,我们还必须评估 规律性条件:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mia</mml:mi>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:mic</mml:mi>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

代入后,我们得到 以下结果:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn2</mml:mn>mml:mfracmml:mrowmml:min</mml:mi>mml:mo/</mml:mo>mml:mn2</mml:mn></mml:mrow>mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="italic">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrow<mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo/</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced></mml:mrow></mml:mrow></mml:mrow></mml:mfrac>mml:mo≤</mml:mo>mml:mic</mml:mi>mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="italic">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfrac></mml:math> 对于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi>mml:mo<</mml:mo>mml:mn1</mml:mn></mml:math>

1logn−1≤clogn

对于较大的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>,不存在一个常数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi>mml:mo<</mml:mo>mml:mn1</mml:mn></mml:math> 能够满足此条件。 因此, 案例 3 不适用。

前面例子中的递归函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo/</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="italic">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfrac></mml:math> 不符合主定理的任何情况。 这展示了主定理的一个局限性:它要求 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 被多项式函数有界,并且与临界指数之间有多项式差异 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mrowmml:mrowmml:msubmml:mrow<mml:mi mathvariant="italic">log</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mia</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:msup></mml:math>。在这里, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 不符合这些界限,这突显了需要使用替代方法,如代入法、迭代法或 Akra-Bazzi 方法来求解 这个递归。

示例 5.15

求解 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn0.5</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mfracmml:mrowmml:mn1</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:mfrac></mml:math>

解答:主定理不适用 因为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi>mml:mo<</mml:mo>mml:mn1</mml:mn></mml:math>

例 5.16

求解 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn64</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn8</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo-</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:math>

解答:主定理不适用 因为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo>mml:mo<</mml:mo>mml:mn1</mml:mn></mml:math>

例 5.17

求解 T(n)=T(n2)+n(2−cosn)

解答

该递归函数描述了一个分治算法,其中以下情况成立:

  • 大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 的问题被分解为一个大小为 的子问题 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo/</mml:mo>mml:mn2</mml:mn></mml:math>

  • 递归调用外的工作 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn2</mml:mn>mml:mo-</mml:mo>mml:mic</mml:mi>mml:mio</mml:mi>mml:mis</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

为什么主定理 不能直接应用?

主定理最适用于具有简单多项式增长的函数。 在这种情况下,项 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn2</mml:mn>mml:mo-</mml:mo>mml:mic</mml:mi>mml:mio</mml:mi>mml:mis</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 由于存在余弦函数,它不是一个简单的多项式。

一种替代方法是简化递归函数中的非递归部分。 我们可以利用余弦函数在-1 和 1 之间震荡的事实:

-1≤cosn≤1

乘以-1 并加上 2,我们得到 如下:

1≤2−cosn≤3

因此,以下是 这种情况:

n≤n(2−cosn)≤3n

这告诉我们 n(2−cosn) 上下有线性函数的界限。 现在,我们将主定理应用于带界限的函数。 我们可以基于非递归项的上下界创建两个新的递归函数:

  • 下界 :T⁻(n)=T⁻(n2)+n

  • 上界 : <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:miT</mml:mi></mml:mrow>mml:mrowmml:mo+</mml:mo></mml:mrow></mml:msup><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:msupmml:mrowmml:miT</mml:mi></mml:mrow>mml:mrowmml:mo+</mml:mo></mml:mrow></mml:msup><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mn3</mml:mn>mml:min</mml:mi></mml:math>

现在,这两个递归都符合 主定理的形式:

  • a=1 (子问题的数量)

  • b=2 (输入大小的减少因子)

  • f(n)=n (用于下界) 和 f(n)=3n (用于 上界)

在这两种情况下,我们都有<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrow<mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub>mml:mia</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:msup>mml:mo=</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrow<mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:mrow></mml:msup>mml:mo=</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msup>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math>

应用主定理的案例 2,我们对<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo⁻</mml:mo>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo⁺</mml:mo>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>,得到如下结果:

T⁻(n)=Θ(nlogn)

T⁺(n)=Θ(nlogn)

由于T⁻(n)≤T(n)≤T⁺(n)T(n)被夹在两个其他函数之间,并且这两个<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo⁻</mml:mo>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo⁺</mml:mo>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>Θ(nlogn),因此我们可以得出以下结论:

T(n)=Θ(nlogn)

你应该注意到,前一个例子中余弦函数的振荡特性并不会显著影响递归函数的整体增长率。长期来看,线性项<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>主导了行为,而算法的时间复杂度主要由递归拆分和每步中执行的线性工作决定。

例 5.18

求解T(n)=4T(n2)+nlogn

解法:乍一看,主定理似乎是正确的工具。 我们有 以下内容:

  • a=4 (子问题的数量)

  • b=2 (输入大小减少的因子)

  • f(n)=nlogn (递归调用外的工作量)

这给我们带来了 一个 分界点 函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrow<mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub>mml:mia</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:msup>mml:mo=</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrow<mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mn4</mml:mn></mml:mrow></mml:mfenced></mml:mrow></mml:msup>mml:mo=</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>

现在,我们比较 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfrac></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>进行比较。

示例 5.17相似,函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfrac></mml:math> 无法整齐地适配 主定理的三个情况之一:

  • 案例 1: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrow<mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub>mml:mia</mml:mi>mml:mo-</mml:mo>mml:miε</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 但是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfrac></mml:math> 增长速度比 分水岭函数

  • 案例 2: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrow<mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub>mml:mia</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:msup>mml:mil</mml:mi>mml:mio</mml:mi>mml:msupmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 但是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfrac></mml:math> 不符合 该形式

  • 案例 3****<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mo:</mml:mo>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΩ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrow<mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub>mml:mia</mml:mi>mml:mo+</mml:mo>mml:miε</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 并且 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfrac></mml:math> 增长速度慢于 分水岭函数

尽管案例 3 看起来是一个潜在的适配,但是正则性条件不成立。 该条件要求afnb≤cfn 对于某个常数 c<1 以及足够大的 n。 在这种情况下,它变为 以下内容:

4(n2)/log(n2)≤c(nlogn)

简化后,我们得到 如下结果:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mn2</mml:mn>mml:min</mml:mi>mml:mo/</mml:mo><mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mo-</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:mn2</mml:mn></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:mic</mml:mi>mml:min</mml:mi>mml:mo/</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>的值较大时,我们无法找到一个常数 c<1 来满足这个不等式。 因此, 主定理的情况 3 不成立。

本节介绍了解决在分析分治算法中常见的递归关系的系统方法。 主方法提供了一组简单的规则,用于确定递归关系的渐近行为。 它根据 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mrowmml:mrowmml:msubmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub></mml:mrow>mml:mo⁡</mml:mo>mml:mrow/</mml:mrow>mml:mia</mml:mi></mml:mrow></mml:msup></mml:math>的相对增长速度将递归关系分类为三种情况,从而可以快速准确地估算复杂度。 该方法的实用性在于它能够处理广泛的问题,而无需详细的逐案例分析,使其成为算法设计和分析中的宝贵工具。

超越主定理 – Akra-Bazzi 方法

在上一节中,我们 讨论了主定理在解决递归函数时的局限性。 尽管主定理能够处理广泛的问题,并且是最常见的方法,但它确实有一些局限性。 例如,它仅适用于特定类型的分治法 递归函数:

T(n)=aT(n/b)+f(n)

在这里,子问题规模是相等的(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo/</mml:mo>mml:mib</mml:mi></mml:math>)。 正如我们在上一节所展示的,我们可以使用主定理解决减法递归函数的特殊情况。 它对函数形式 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>也有局限。 具体来说,主定理在处理以下问题时会遇到困难:

  • 不等的子问题规模:递归拆分成大小差异显著的子问题 不同的规模

  • 更复杂的拆分:递归中有超过两个子问题 的情况

  • 非多项式工作量:函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 表示递归调用外部工作量的函数,并不能轻易归类为多项式 或对数形式

对于主定理无法解决的情况, Akra-Bazzi 方法 为更广泛的递归函数提供了解决方案。 该方法扩展了我们解决更复杂递归问题的能力,是算法分析中的一项有价值工具。 Akra-Bazzi 方法能够处理以下更 一般形式的递归:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mrowmml:munderover<mml:mo stretchy="false">∑</mml:mo>mml:mrowmml:mii</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:munderover>mml:mrowmml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:miT</mml:mi>mml:mo(</mml:mo>mml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:min</mml:mi>mml:mo+</mml:mo>mml:msubmml:mrowmml:mih</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo>mml:mo)</mml:mo></mml:mrow></mml:mrow></mml:math>

在这里,以下是 的情况:

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 是子问题的数量(可以大于 2)

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo></mml:mo>mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math> 并且 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn0</mml:mn></mml:mrow></mml:msub></mml:math> 一个常数

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:mo></mml:mo>mml:mn0</mml:mn></mml:math>,一个对所有的常数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math>,表示第 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math>个子问题出现的次数

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn0</mml:mn>mml:mo<</mml:mo>mml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:mo<</mml:mo>mml:mn1</mml:mn></mml:math>,对于所有的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:mrow></mml:mfrac></mml:math>,是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math>的子问题大小(不同的子问题可以有 不同的大小)

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mfenced open="|" close="|" separators="|">mml:mrowmml:mif</mml:mi>mml:mi’</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced>mml:mo∈</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mic</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:mo,</mml:mo></mml:math> 其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi></mml:math> 一个常数

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mfenced open="|" close="|" separators="|">mml:mrowmml:msubmml:mrowmml:mih</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:mrow></mml:mfenced>mml:mo∈</mml:mo>mml:miO</mml:mi>mml:mo(</mml:mo>mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:msupmml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:min</mml:mi></mml:mrow></mml:mfrac>mml:mo)</mml:mo></mml:math>,对于 所有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math>

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 是递归的非递归部分或驱动函数,表示递归调用外的工作,并且具有更广泛的 可允许形式

以下 是使用 Akra-Bazzi 方法解递归函数的步骤:

  1. <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mip</mml:mi></mml:math>:解以下方程:

    <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mrowmml:munderover<mml:mo stretchy="false">∑</mml:mo>mml:mrowmml:mii</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:munderover>mml:mrowmml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:msubsupmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow>mml:mrowmml:mip</mml:mi></mml:mrow></mml:msubsup>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mrow></mml:math>

    这个 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mip</mml:mi></mml:math> 是至关重要的;它代表了递归的“平衡点”。

  2. 计算积分:求以下积分的值:

    <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miI</mml:mi>mml:mo=</mml:mo>mml:mrowmml:munderover<mml:mo stretchy="false">∫</mml:mo>mml:mrowmml:mn1</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:munderover>mml:mrow<mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mfracmml:mrowmml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:miu</mml:mi></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:msupmml:mrowmml:miu</mml:mi></mml:mrow>mml:mrowmml:mip</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mid</mml:mi>mml:miu</mml:mi></mml:mrow></mml:mrow></mml:math>

  3. 渐近界:T(n)的渐近复杂度如下: 如下:

    <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo∈</mml:mo>mml:miθ</mml:mi>mml:mo(</mml:mo>mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mip</mml:mi></mml:mrow></mml:msup>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo+</mml:mo>mml:mrowmml:msubsup<mml:mo stretchy="false">∫</mml:mo>mml:mrowmml:mn1</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msubsup>mml:mrowmml:mfracmml:mrowmml:mig</mml:mi>mml:mo(</mml:mo>mml:miu</mml:mi>mml:mo)</mml:mo></mml:mrow>mml:mrowmml:msupmml:mrowmml:miu</mml:mi></mml:mrow>mml:mrowmml:mip</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfrac>mml:mid</mml:mi>mml:miu</mml:mi></mml:mrow></mml:mrow>mml:mo)</mml:mo>mml:mo)</mml:mo></mml:math>

请注意以下关于 Akra-Bazzi 方法的事项:

  • Akra-Bazzi 方法是 主定理 的一种推广。

  • 它处理具有不等子问题大小、更复杂分割以及更广泛工作函数类型的递归关系。

  • 该方法涉及寻找一个平衡指数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mip</mml:mi></mml:math> 并对工作函数进行积分,从而确定 渐近复杂度

为什么它有效? Akra-Bazzi 背后的直觉

将递归函数看作一棵树。 每个节点代表一个递归调用,其子节点则是子问题。 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mip</mml:mi></mml:math> 起到平衡树的不同分支贡献的权重作用。 积分然后汇总了树的每一层的工作量,考虑了 这种平衡。

示例 5.19

让我们考虑以下递归:

T(n)=2T(n/3)+T(n/4)+n

求解 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mip</mml:mi></mml:math> 2×3p+1×4p=1. 求解此方程(数值方法)得到 我们得到 p≈1.207

计算 该积分

∫1nuu1+1.207du≈0.828n0.207

渐进界限:

Tn=Θn1.2071+0.828n0.207=Θn1.207

示例 5.20

求解 T(n)=4T(n2)+nlogn

解答:在上一节中,我们看到这个递归函数无法使用主定理求解。 另一方面,Akra-Bazzi 方法是主定理的推广,能够处理更广泛的递归类型。 让我们在这里应用 它:

求解 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mip</mml:mi></mml:math>:解方程: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mn4</mml:mn></mml:mrow>mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mip</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfrac>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math>. 这给我们带来 结果 p=2

计算 积分

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mrowmml:msubsup<mml:mo stretchy="false">∫</mml:mo>mml:mrowmml:mn1</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msubsup>mml:mrowmml:mfracmml:mrowmml:miu</mml:mi>mml:mo/</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:miu</mml:mi></mml:mrow>mml:mrowmml:msupmml:mrowmml:miu</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfrac></mml:mrow></mml:mrow>mml:mid</mml:mi>mml:miu</mml:mi>mml:mo=</mml:mo>mml:mrowmml:msubsup<mml:mo stretchy="false">∫</mml:mo>mml:mrowmml:mn1</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msubsup>mml:mrowmml:mfracmml:mrowmml:mn1</mml:mn></mml:mrow>mml:mrowmml:msupmml:mrowmml:miu</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:miu</mml:mi></mml:mrow></mml:mfrac>mml:mid</mml:mi>mml:miu</mml:mi></mml:mrow></mml:mrow></mml:math>

这个积分有点复杂,但它的近似值是 loglogn

渐近界限

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mip</mml:mi></mml:mrow></mml:msup><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn>mml:mo+</mml:mo>mml:mrowmml:munderover<mml:mo stretchy="false">∫</mml:mo>mml:mrowmml:mn1</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:munderover>mml:mrow<mml:mfenced open="[" close="]" separators="|">mml:mrowmml:miu</mml:mi>mml:mo/</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:miu</mml:mi></mml:mrow></mml:mfenced>mml:mo/</mml:mo></mml:mrow></mml:mrow>mml:msupmml:mrowmml:miu</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msup>mml:mid</mml:mi>mml:miu</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn>mml:mo+</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

本节详细介绍了一种 用于解决无法通过主定理处理的更复杂递归函数的先进且通用的方法。 该方法特别适用于子问题规模不统一的递归,或者在处理更复杂形式的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>时。Akra-Bazzi 方法通过提供一个框架,扩展了传统递归求解技术的适用性,能够处理更广泛的函数和分裂比。 通过利用此方法,能够为复杂递归推导出精确的渐近界限,使其成为分析复杂递归算法性能的强大工具。 本节包括实际示例,展示了该方法在解决超出 简化方法范围的递归问题中的应用和效果。

总结

第五章中,讨论了在算法分析背景下解决递归函数的方法。 本章概述了三种主要技术:替代法、主定理和递归树。 替代法通过变量替代和归纳法构造证明,来解决递归函数。 主定理提供了一种系统的方法,基于递归函数来确定递归算法的复杂性。 递归树有助于将问题分解为子问题,从而在没有直接证明的情况下,提供对复杂性的洞察。 证明。

替代法被详细阐述,突出了它在处理各种递归函数时的灵活性和强大功能。 该方法包括假设一个解,将其代入递归公式,并通过归纳法证明其正确性。 实践例子展示了迭代方法,展示了如何通过展开递归来揭示模式并得出封闭形式的解。 通过多个例子强调了该方法的多功能性,展示了它在解决其他方法可能无法有效处理的复杂递归问题中的应用。 处理效果。

本章还探讨了主定理的局限性,特别是在处理非多项式、非正、非平滑或不规则的驱动函数时,以及当 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi></mml:math> 不是常数时。 Akra-Bazzi 方法被介绍为主定理的推广,能够处理更广泛的递归函数,包括那些具有不等子问题大小或更复杂分裂的递归函数。 提供了示例,展示了这些方法的应用,强调了当主定理的条件不满足时需要采用替代方法。 本章总结了这些高级方法的应用,确保了对递归函数求解的全面理解,特别是在 算法分析中的应用。

在下一章,我们将讨论算法中最基础的几种类型:排序。 我们在本章中学到的技巧,特别是在分析递归函数时,将被用来分析排序算法并估算 它们的复杂度。

参考文献与进一步阅读资料

  • 算法导论。作者:Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, 和 Clifford Stein。 第四版。 MIT Press。 2022 年:

    • 第四章, 递归
  • 算法。作者:R. Sedgewick, K. Wayne。 第四版。 Addison-Wesley。 2011 年:

    • 第二章,算法分析原理 算法分析

    • 第五章,排序

  • 算法设计手册。作者:S. S. Skiena。 第二版。 Springer。 2008 年:

    • 第三章,数据结构 与递归

    • 第五章, 图算法

  • 算法设计。作者:Jon Kleinberg 和 Éva Tardos。 第一版。 Pearson。 2005 年:

    • 第五章,分治法 与征服

    • 第七章,递归

  • 线性递归方程的解法。Mohamad Akra, Louay Bazzi。 计算优化与应用。第 10 卷,第 2 期, 第 195–210 页。 1998 年。

第八章:第二部分:算法深度剖析

在这一部分,我们将详细探讨特定类型的算法,从基础的排序和搜索技术到更高级的主题,如动态规划和随机化算法。 这一部分加深了你对算法设计的理解,展示了不同方法如何高效地解决复杂 问题。

这一部分包括以下章节: 以下章节:

  • 第六章**, 排序算法

  • 第七章**, 搜索算法

  • 第八章**, 排序与搜索之间的共生关系

  • 第九章**, 随机化算法

  • 第十章**, 动态规划

第九章:6

排序算法

全球计算能力的惊人 25%用于数据排序,突显了其在现代计算过程中的关键角色。 这一重大资源分配凸显了高效排序算法在信息检索、数据库管理、科学模拟和机器学习等各种应用中的重要性。 本章系统地探讨了最重要的排序算法,从基本的冒泡排序到高级的快速排序,无论是迭代还是递归方法。 每个算法都进行了正确性和复杂性评估。 本章以关于线性时间排序的讨论结束,提供了有关 数据假设的背景。

排序算法使用不同的方法实现。 每个算法都具有独特的特性,如稳定性、原地排序或适应性,这些特性决定了它们在不同任务中的适用性。 本章探讨了这些特性,阐明了每种排序机制的优缺点。 通过了解这些细微差别,您将能够选择最佳的排序方法来处理特定的用例 和数据集。

在建立这一基础概述的基础上,本章深入探讨了迭代和递归排序算法的复杂性。 它从更简单、更直观的迭代方法开始,例如选择排序和插入排序,逐渐过渡到更高效的递归方法,例如归并排序和快速排序。 此外,还探讨了在特定条件下实现线性时间复杂度的排序算法,突出了对输入数据假设的重要性。 这种全面的探索确保您对各种排序技术有深入的理解,使您能够高效处理各种 计算挑战。

本章涵盖了 以下主题:

  • 排序算法的 分类

  • 迭代 排序算法

  • 递归 排序算法

  • 非比较性 排序算法

排序算法的分类

首先,让我们检查区分不同排序算法的关键特性,为理解它们的独特特征和实际应用提供一个全面的框架。 我们将探讨六个关键特性:比较、递归、适应性、逆序、内存使用和稳定性。 比较决定了一个算法是否依赖于元素间的成对比较来排序数据,进而影响其通用性和时间复杂度的界限。 递归涉及将排序过程分解为更小、更易处理的子问题,通常导致优雅的分治法解决方案。 适应性衡量算法有效处理部分已排序数据的能力,从而提高在实际场景中的表现。 逆序计算出无序的元素对数,作为评估算法在不同上下文中效率的度量。 内存使用检查算法所需的额外空间,区分就地排序和非就地排序方法。 最后,稳定性确保相等的元素保持原始的相对顺序,这对于多级排序任务至关重要。 通过理解这些特性,我们可以更好地理解每种排序算法的优缺点,并做出有关 它们应用的明智决策。

比较

比较是排序和查找算法中的基本操作。 在排序中,比较通过成对评估确定元素的相对顺序。 我们可以根据排序算法是否使用比较来建立顺序对其进行分类。 排序的顺序。

基于比较的排序算法,如归并排序和快速排序,非常灵活。 我们可以将它们应用于任何具有定义比较函数的数据类型。 这些算法通常可以实现 O(nlogn) 的时间复杂度。 另一方面,一些算法并不依赖于比较。 线性时间排序算法的例子包括计数排序和基数排序。 它们采用了替代排序技术,通常能够在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>)的特定条件下实现线性时间复杂度。

基于比较的排序

比较在许多排序算法中起着核心作用。 在基于比较的排序算法中,确定元素顺序的主要操作是对元素对进行比较。 该算法根据这些比较的结果做出关于元素位置的决策,这些比较通常涉及如 <st c="4660"><</st>, <st c="4663">></st>, <st c="4669">==</st>

这些算法基于对元素的比较来决定它们的顺序。 每次比较操作都会决定一个元素是否应排在另一个元素之前或之后。 基于比较的排序算法可以应用于任何定义了比较函数的数据类型。 这使得它们具有广泛的适用性, 并且应用广泛。

我们可以证明存在一个下界为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 用于比较基础排序算法的时间复杂度。 这是由于比较的决策树模型。 这意味着,平均而言,至少需要 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 比较操作才能对 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 个元素进行排序。 我们来解释为什么任何基于比较的排序算法的时间复杂度不能优于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

这些算法是通过比较每一对元素来实现的。 我们可以将这些算法做出的决策表示为一个二叉决策树。 以下是该 决策树的元素:

  • 每个内部(非终端)节点表示两个元素之间的比较(例如,A<B?)

  • 每个分支表示该比较的结果(是 或否)

  • 每个叶(终端)节点表示输入数组的一个可能的最终排序顺序。

让我们通过 一个例子来解释比较排序的决策树表示。

示例 6.1

让我们考虑一个包含三个随机数的数组:A=[3, 1, 4]。 为了通过比较来排序这个数组,我们可能从决策树开始,如 图 6**.1所示。

图 6.1:实现基于比较的三元素数组排序的决策树(白色框为内部节点;左分支为“是”,右分支为“否”)

图 6.1:实现基于比较的三元素数组排序的决策树(白色框为内部节点;左分支为“是”,右分支为“否”)

这棵树 将继续分支,表示所有可能的比较结果,并指向表示输入数组不同排序顺序的叶节点。 让我们估计使用此 决策树进行排序的下界时间复杂度:

  • 对于一个大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>的数组,存在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo!</mml:mo></mml:math> 种可能的排列(不同的顺序)。 这些排列中的每一个都有可能是正确的 排序顺序。

  • 在决策树中,每个叶节点代表这些可能排列中的一个。 因此,树必须至少有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo!</mml:mo></mml:math> 个叶子节点来覆盖所有可能性。 在我们的例子中,我们将有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn3</mml:mn>mml:mo!</mml:mo>mml:mo=</mml:mo>mml:mn6</mml:mn></mml:math> 种可能的排列或 叶节点。

  • 一个具有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miL</mml:mi></mml:math> 叶子节点的二叉树,其最小高度为 logL。由于我们的决策树至少需要 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo!</mml:mo></mml:math> 个叶子节点,其最小高度为 <mml:math xmlns=mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo!</mml:mo>mml:mo)</mml:mo></mml:math>。使用斯特林近似,我们知道 log(n!) 大约等于 nlogn (见 示例 3.8 来自 第三章)。

因此,所有基于比较的排序算法的时间复杂度下界为 Ω(nlogn)。这意味着没有任何算法能够 consistently 排序数组的速度比 O(nlogn)更快。

以下是一些知名的基于比较的 排序算法:

  • 冒泡排序:数组中的相邻元素反复比较,如果它们的顺序错误,则交换它们的位置。 每一次遍历,较大的元素逐渐被移动到数组的末尾。

  • 插入排序:该算法通过反复比较并将元素插入到已排序部分的适当位置,逐步构造一个已排序的数组。 排序部分逐步增大。

  • 快速排序:在 快速排序算法中,使用比较将数组围绕一个主元素进行分区,然后递归地排序 结果分区。

  • 归并排序:在 这种排序算法中,数组被分成两部分,几乎相等。 每一部分会递归排序,然后通过比较将已排序的部分合并

  • 堆排序:在这种 排序算法中,从原始数组构建一个最大堆(或最小堆)树,然后反复提取最大(或最小)元素。 在整个过程中,使用比较来维持堆的性质(见 第十三章)。

非比较排序

基于比较的方法不同,在 非比较算法中,我们并不直接比较元素来确定它们的顺序。 相反,我们使用诸如计数频率、应用哈希函数或利用数据的特定属性等替代技术。 这些方法利用对输入数据的假设 来实现高效排序,通常可以达到线性时间复杂度, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>,在某些条件下。 非比较排序算法的主要 特性包括 以下几点:

  • 数据特定技术:这些算法经常利用数据的特定属性,如范围(特别是针对数值型或整数数据)或位数,来执行 排序过程

  • 线性时间复杂度:非比较排序算法可以实现优于  O(nlogn) 时间复杂度,通常为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>,通过避免比较并使用更直接的方法来 对元素进行排序

  • 内存使用:尽管这些算法可以实现线性时间复杂度,但它们通常需要临时内存,这些内存通常与输入大小或数据值范围成正比。 数据值

在本章中,我们 介绍了三种著名的非比较排序算法。 第一种是计数排序,它通过计算每个不同元素的出现次数,将其放置到正确的位置上,适用于已知范围内的整数。 接下来是基数排序,它按特定顺序(例如,从最低有效位开始)处理元素的每个数字或字符,并使用稳定的子程序(如计数排序)按每个数字对元素进行排序。 最后,桶排序将输入列表的元素根据其值分配到多个桶或容器中。 每个桶或容器会单独进行排序(通常使用简单的比较排序),所有排序好的桶会被连接在一起,形成最终的 排序列表。

递归

递归 在许多排序算法中发挥着重要作用,使得 它们能够通过分治策略高效地分解并解决排序问题。 递归排序算法提供了简洁直观的实现,但也带来了栈开销和潜在的性能波动。 另一方面,非递归排序算法避免了递归和栈管理的复杂性,提供了内存效率和简洁性,尽管它们在处理 大数据集时可能表现不如递归算法。

递归排序算法

递归是 排序算法中常用的技术,它可以将复杂问题分解成更简单的子问题。 在排序的上下文中,递归使得算法能够将输入数组分割成更小的片段,并独立地对这些 片段进行排序。 最终排序的数组是通过合并已排序的片段生成的。 递归排序算法利用分治策略,这有助于以可管理的 代码复杂度实现高效排序。 递归排序算法具有以下特性:

  • 分治策略:顾名思义,这种策略包含三个步骤:首先,将问题(数组)拆分成更小的子问题(子数组)。 其次,递归地解决(排序)每个子问题(子数组)。 第三,将各个子问题的解合并,解决 原始问题。

  • 基本情况与递归情况:每个递归算法都有一个基本情况,当子问题足够小(例如,只有一个元素或一个空数组)时终止递归。 递归情况继续分解问题并解决 子问题。

  • 栈的使用:递归调用消耗栈空间,这可能导致较高的内存使用,特别是在深度递归时。 然而,尾递归优化和迭代方法在 某些情况下可以缓解这一问题。

表 6.1 提供了一些递归 排序算法的示例:

排序 算法 过程 时间与 空间复杂度
归并排序
  1. 分割:如果数组有多个元素,将其分割成两半,尽量 均匀。

  2. 分治:递归地对每一半应用归并排序。 直到每个子数组只有一个元素(一个 trivially 已排序的数组)。

  3. 合并:通过比较每一半的元素,将两个已排序的半部分合并成一个排序后的数组,先取较小的元素,放入 新数组中。

  4. 重复此过程,直到两个半部分的所有元素都已 合并。

时间: O(nlogn) 在所有情况下(最佳、平均 和最坏情况下)空间: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 用于归并组件的临时内存。
快速排序
  1. 选择枢轴:从数组中选择一个元素作为枢轴。 常见的选择有第一个元素、最后一个元素或随机 选择的元素。

  2. 分区:将数组划分为两个子数组。 第一个子数组(左边)的元素小于枢轴,第二个子数组(右边)的元素大于枢轴。 枢轴现在处于最终 排序位置。

  3. 递归排序:递归地对左子数组应用快速排序。 递归地对右子数组应用快速排序。

  4. 继续这个过程,直到每个子数组要么为空,要么只包含一个元素(一个 trivially 已排序的数组)。

时间复杂度: O(nlogn) 平均情况下, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 最坏情况下。空间复杂度: O(logn) 递归栈的空间, 在平均情况下。

| 堆排序 | 构建最大堆:从最后一个非叶子节点开始,按反向层次顺序递归堆化每个节点。 这会从输入数组构建一个最大堆。提取并重建:重复以下步骤,直到堆 为空:

  • 交换根节点(最大元素)与堆的最后一个元素。

  • 将堆大小 减少 1。

  • 对根节点应用递归堆化,恢复 最大堆属性。

(堆排序将在 第十三章中详细讨论) | 时间: O(nlogn) 所有情况下。空间: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 对于迭代版本;递归堆化过程使用  O(logn) 栈空间。 |

表 6.1:递归排序算法的示例

非递归排序算法

非递归排序算法 不使用递归来实现排序。 相反,它们依赖于迭代技术,通过 循环来管理排序过程。 这些算法通常具有更简单的空间复杂度,因为它们避免了递归策略中的栈开销。 非递归排序算法使用迭代方法(通过循环实现),并且通常具有较高的内存效率。 从技术角度讲,非递归排序算法具有以下 特征:

  • 迭代方法:非递归排序算法使用循环(例如, for 循环或 while 循环)对数组进行排序,从而避免了 递归调用

  • 内存效率:这些算法通常更加节省内存,因为它们避免了递归调用所需的额外栈空间

  • 更简化的栈管理:通过使用迭代方法,非递归算法避免了管理递归栈深度的复杂性,这在处理 大型数据集 时可能会导致栈溢出

表 6.2 列出了 一些著名的非递归 排序算法:

排序 算法 过程 时间和 空间复杂度

| 插入 排序 | 从第二个元素开始。 假设第一个元素已经 排序好了。遍历未排序部分:

  • 将其与已排序部分的元素进行比较(与其左边的元素)。

  • 将已排序部分的元素向右移动,直到当前元素处于 正确位置。

  • 将当前元素插入 该位置。

继续,直到整个数组排序完成。 对未排序部分的每个元素重复步骤 2。 | 时间复杂度: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 在平均和最坏情况下, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 在最佳情况下。空间复杂度: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 因为它是 原地排序。 |

选择 排序 找到最小的元素(初始时为 整个数组)。交换:将最小的元素与未排序部分最左侧的元素交换。 现在,最左侧的元素已处于其最终的 排序位置。重复:考虑剩余的未排序部分(排除已经排序的元素)。 重复步骤 1 和 2,直到整个数组 排序完成。 时间复杂度: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 所有情况下。空间复杂度: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 因为它是 原地排序。

| 冒泡排序 | 比较 并交换:

  • 比较每两个元素。 如果它们的顺序不正确, 交换它们。

  • 继续这个过程,直到数组的最后一个元素 被访问。

重复:

  • 经过一轮遍历,最大的元素将“冒泡”到 数组的末尾。

  • 重复步骤 1,但这次,停止在距离数组末尾一位的位置(因为最后一个元素已经在其 正确位置)。

  • 继续重复步骤 1,每次减少比较范围一个元素,直到整个数组 排序完成。

时间: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 对于平均情况和最坏情况, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 最佳情况下。空间: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 因为它是 原地排序。

表 6.2:非递归排序算法示例

递归是一个关键因素, 它将排序算法区分开来。 在接下来的子节中,我们将探讨其他方面, 例如适应性。

适应性

在排序中,适应性指的是算法利用数据中已有顺序来提高效率的能力。 一种适应性排序算法在输入数据部分已排序或具有某种内在结构时表现尤为出色,这样就需要更少的操作来完成完全排序。 本质上,输入数据越有序,算法完成任务的速度就越快。

一种 适应性排序算法 在数据部分有序时,其平均运行时间将比输入完全随机时要快。 此外,当数据接近已排序状态时,这些排序算法进行的比较和交换更少,从而减少了计算量。 适应性排序算法在实际应用中更加高效,因为许多实际数据集并非完全随机,通常具有某种已存在的顺序(例如,时间序列数据或周期性更新的列表)。 适应性排序算法能够更高效地处理此类数据集

让我们考虑 两种自适应排序算法。 首先是插入排序,它具有很高的自适应性。 在最优情况下,当输入数据已经排序时,它的运行时间为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>,因为它只需要进行 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:math> 次比较。 对于小规模或几乎已排序的数据,插入排序是高效的,因为它需要进行较少的交换和比较。 下一个自适应排序算法是冒泡排序,它在某种程度上是自适应的。 如果输入数据已经排序,冒泡排序可以在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 时间内完成排序,尽管这需要进行优化,以便在没有交换时提前停止 一次遍历。

另一方面,让我们 考察两种非自适应排序算法:快速排序和堆排序。 快速排序的时间复杂度受枢轴选择和元素初始排列的影响较大。 然而,它本身并不会因为部分排序的数据而得到改善。 堆排序是另一种非自适应算法。 正如我们将在 第十三章中看到的,预排序的数据在 此过程中没有任何优势。

逆序

序列中的反转 是一种情况,其中一对元素的顺序错乱。 形式上,给定一个数组 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi></mml:math>,反转被定义为一对索引 ,i i<j A[i]>A[j]。简单来说,反转发生在数组中较大的元素在较小的元素之前的情况下。 反转在排序中很重要,因为它们衡量数组内部无序的程度。 总反转数提供了数组离排序完的程度的见解。 我们可以 考虑 两种情况:

  • 零反转:对于所有 i<j,我们有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mii</mml:mi></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:miA</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mij</mml:mi></mml:mrow></mml:mfenced></mml:math>;换句话说,数组 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi></mml:math> 完全排序

  • 最大逆序数:对于所有 i<j,我们有 A[i]>A[j];换句话说,数组 A是按 逆序排列的

逆序的概念 对于理解排序算法及其分析至关重要。 逆序指的是数组中元素的顺序不正确的元素对。 例如,在冒泡排序中,算法不断交换相邻的元素,直到它们按正确的顺序排列。 冒泡排序过程中执行的交换次数直接对应于数组中逆序对的数量。 数组中的逆序数越多,排序的难度越大。

在归并排序中,逆序数 可以在归并过程中进行计数。 具体来说,当来自数组右半部分的元素在左半部分的元素之前被插入时,这表示逆序数等于左半部分剩余元素的个数。 这个洞察有助于我们量化数组在排序过程中存在的无序性。 这一过程帮助我们在数组排序时准确把握其中的逆序情况。

逆序数还会影响排序算法的复杂度。 数组中的逆序数可以用来分析这些算法的效率。 例如,具有 O(nlogn) 复杂度的算法,如归并排序和快速排序,可以高效地处理大量逆序。 相比之下,具有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 复杂度的算法,如冒泡排序和插入排序,随着逆序数的增加可能变得越来越低效。 当逆序数增多时,效率会显著下降。

内存使用

内存使用 在排序中是通过算法处理和排列输入数据所需的额外存储量来衡量的。 这与排序算法是否被归类为原地排序 密切相关。

原地排序

原地排序算法 直接在输入数组 内操作。 它们只需要固定数量的额外存储空间。 这些算法通过使用固定的额外空间重新排列给定数组中的元素,通常是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 额外空间。

原地排序算法具有较高的内存效率,所需的额外空间最小。 在内存资源有限或处理大型数据集的环境中,这一点尤为重要。 这些算法通常通过交换输入数组中的元素来实现排序。 常见的技术包括分区和将元素逐步放入正确的位置。 插入排序、快速排序和堆排序是原地排序算法的例子。 快速排序通过其原地分区方案,通常在不需要额外与输入大小成比例的存储空间的情况下对数组进行排序。 然而,它使用 O(logn) 额外空间来支持递归栈。 插入排序将元素插入到数组中的正确位置,不需要除了几个用于循环和交换的变量之外的额外空间。 最后,堆排序将数组转换为堆结构,然后在原地进行排序。 这通过反复移除最大元素(或最小元素,取决于所需顺序),然后恢复堆的性质来保持 堆结构。

非原地排序

排序算法 如果操作不直接在输入数据上进行并且需要额外的内存空间,无论是线性还是非线性地与输入大小成比例 ,则称为非原地排序算法。 这些算法在排序过程中使用额外的空间来存储数据的副本或中间结果。 非原地排序算法消耗更多内存,因为它们需要额外的存储空间来保存数据的临时副本或中间结构。 另一方面,这些算法可能更容易实现和理解,而且它们通常比 原地算法更容易保持排序的稳定性。

非原地排序的例子包括归并排序、计数排序和基数排序。 归并 排序使用 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 额外的内存来保存输入的两个分区。 每次递归步骤都需要额外的空间来存储中间结果,从而导致更高的整体内存使用。 计数排序使用与输入值范围成比例的额外内存,通常为 O(n+k),其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 是输入数据的范围。 基数排序与计数排序类似,需要额外的空间来保存每个正在处理的数字或字符的临时数据,因此导致 O(n+k) 的内存使用。

在选择原地排序和非原地排序算法之间时,有几个权衡需要考虑:

  • 在内存受限的环境中,原地排序算法由于其最小的 内存占用,通常是首选。

  • 非原地算法可能更容易实现和理解,特别是对于复杂的 排序任务。

  • 在某些情况下,非原地算法由于能够高效处理大规模或复杂的数据集,尽管它们的 内存使用量较高,仍然能够提供更好的性能。

  • 非原地算法通常更容易保持相等元素的相对顺序,因此它们是稳定的。 确保原地算法的稳定性可能会 更具挑战性。

内存使用量 是选择排序算法时的重要考虑因素。 原地排序方法在内存消耗方面非常高效,因为它们直接操作输入数组,几乎不需要额外的空间。 然而,非原地方法,如归并排序和计数排序,虽然可以提供在简单性和稳定性方面的优势,但它们需要与输入大小成比例的额外内存。 选择原地排序和非原地排序取决于具体问题。 例如,如果内存有限,应考虑使用原地排序。

稳定性

排序中的稳定性指的是保持具有相同值的元素的原始相对顺序。 如果排序算法确保具有相同值的元素在排序后保持它们初始的相对顺序,则该排序算法被视为稳定的。 稳定算法包括插入排序、归并排序和冒泡排序。 相反,快速排序、堆排序和选择排序是 不稳定算法的例子。

稳定排序算法和不稳定排序算法的区别在于它们能否保持相等元素的原始顺序。 稳定算法保证这种顺序的保持,确保相等元素的相对位置在排序过程中不变。 相反,不稳定算法可能会改变相等元素的相对位置。

考虑以下元组列表,其中每个元组包含一个字母和一个数字:[(‘A’, 3), (‘B’, 1), (‘C’, 3), (‘D’, 2), (‘E’, 1)]

如果我们使用稳定的排序算法按数字(每个元组的第二个元素)升序排序这个列表,结果可能是 如下:

[(‘B’, 1), (‘E’, 1), (‘D’, 2), (‘A’, 3), (‘C’, 3)]

注意,对于具有相同数字的元组(如(‘A’,3)和(‘C’,3)),它们在排序后会保持原有的顺序。 换句话说,在排序后的列表中,‘A’(3)排在‘C’(3)前面,就像它在原始列表中的位置一样。 如果使用不稳定的排序算法,结果顺序可能是 如下:

[(‘E’, 1), (‘B’, 1), (‘D’, 2), (‘C’, 3), (‘A’, 3)]

在这里,‘A’(3)和‘C’(3)的相对顺序 未能保持。

稳定性 在以下场景中特别重要:

  • 多关键字排序:当 执行多级排序(例如,首先按一个属性排序,然后按另一个属性排序)时,稳定性确保先前排序的顺序得到保持。 例如,如果你首先根据员工的雇佣状态排序,然后再根据员工编号排序,稳定排序会保持雇佣状态的顺序,同时按 id 排序。 在此过程中,雇佣状态的顺序得以保留。

  • 保持输入顺序:在某些应用中,输入元素的顺序除了其排序后的位置外,还具有其他意义。 稳定性确保这种意义 得以保留。

稳定性在需要多级排序或输入顺序携带额外信息的应用中至关重要。 在这种情况下,稳定的排序算法是首选,以确保数据的完整性 和正确性。

迭代排序算法

迭代排序算法 因其简单性而闻名,使得它们易于理解、实现和调试。 迭代排序方法的主要优点是它们的空间效率;它们通常需要最少的额外内存,这在内存受限的环境中是一个重要的优势。 然而,这些算法在时间复杂度方面通常表现不佳,尤其是在处理大型数据集时。 在数据规模和处理时间至关重要的场景中,这种局限性可能会特别成为问题。 本节介绍了三种常见的迭代排序算法:冒泡排序、选择排序和 插入排序。

冒泡排序

冒泡排序是一个 简单的基于比较的算法 ,它反复扫描数组,比较并交换相邻元素,如果它们的顺序错误。 这个过程会一直重复,直到整个数组被排序。 “冒泡”的类比来自于较小的元素逐渐浮升到顶端,而较大的元素则下降到底部。

尽管 它简单,冒泡排序通常 效率低下,排序数据集时表现出二次方的运行时间, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo²</mml:mo>mml:mo)</mml:mo>mml:mo,</mml:mo></mml:math> 无论是在平均情况还是最坏情况下。 然而,它易于理解和实现,使其成为一个宝贵的教育工具,并且是排序小型数据集的可行选项。 正如我们在上一节中所看到的,冒泡排序是一个稳定的原地算法,并且它的空间复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>

示例 6.2

表 6.3 展示了冒泡排序的逐步过程,对于数组 [<st c="29655">5, 3, 8, 4, 2, 7,</st> <st c="29674">1, 6</st>]:

轮次 操作描述 的内容 数组 的内容
1 5 与 3, 交换它们5 与 8, 不交换8 与 4, 交换它们8 与 2, 交换它们8 与 7, 交换它们8 与 1, 交换它们8 与 6, 交换它们 [3, 5, 8, 4, 2, 7, 1, 6][3, 5, 8, 4, 2, 7, 1, 6][3, 5, 4, 8, 2, 7, 1, 6][3, 5, 4, 2, 8, 7, 1, 6][3, 5, 4, 2, 7, 8, 1, 6][3, 5, 4, 2, 7, 1, 8, 6][3, 5, 4, 2, 7, 1, 6, 8]
2 3 对 5, 无需交换5 对 4, 交换它们5 对 2, 交换它们5 对 7, 无需交换7 对 1, 交换它们7 对 6, 交换它们8 现在已处于它的 正确位置 [3, 5, 4, 2, 7, 1, 6, 8][3, 4, 5, 2, 7, 1, 6, 8][3, 4, 2, 5, 7, 1, 6, 8][3, 4, 2, 5, 7, 1, 6, 8][3, 4, 2, 5, 1, 7, 6, 8][3, 4, 2, 5, 1, 6, 7, 8]
3 3 对 4, 无需交换4 对 2, 交换它们4 对 5, 无需交换5 对 1, 交换它们5 对 6, 无需交换7 和 8 现在已处于它们的 正确位置 [3, 4, 2, 5, 1, 6, 7, 8][3, 2, 4, 5, 1, 6, 7, 8][3, 2, 4, 5, 1, 6, 7, 8][3, 2, 4, 1, 5, 6, 7, 8][3, 2, 4, 1, 5, 6, 7, 8]
4 3 对 2, 交换它们3 对 4, 无需交换4 对 1, 交换它们4 对 5, 无需交换6, 7 和 8 现在已处于它们的 正确位置 [2, 3, 4, 1, 5, 6, 7, 8][2, 3, 4, 1, 5, 6, 7, 8][2, 3, 1, 4, 5, 6, 7, 8][2, 3, 1, 4, 5, 6, 7, 8]
5 2 对 3, 无需交换3 对 1, 交换它们3 对 4, 无需交换5, 6, 7 和 8 现在已处于它们的 正确位置 [2, 3, 1, 4, 5, 6, 7, 8][2, 1, 3, 4, 5, 6, 7, 8][2, 1, 3, 4, 5, 6, 7, 8]
6 2 对 1, 交换它们2 对 3, 无需交换4, 5, 6, 7 和 8 现在已处于它们的 正确位置现在数组 已排序。 [1, 2, 3, 4, 5, 6, 7, 8][1, 2, 3, 4, 5, 6, 7, 8]

表 6.3:演示冒泡排序的示例

以下是 冒泡排序算法的 Python 实现:

 def bubble_sort_iterative(a):
    n = len(a)
    for i in range(n):
        elements_swapped = False
        for j in range(0, n - i - 1):
            if a[j] > a[j + 1]:
                a[j], a[j + 1] = a[j + 1], a[j]
                elements_swapped = True
        if not elements_swapped:
            break
    return a

冒泡排序算法非常简单。 它由两个嵌套循环组成;外循环总是 执行 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 次,而内循环如果数组已经排序好或接近排序好(表示无需交换 )则可以提前终止。

现在,我们来分析冒泡排序的正确性并评估其 时间复杂度。

正确性证明

第二章中讨论的那样,我们利用循环不变式的概念来证明算法的正确性。 循环不变式是指在每次循环迭代前后都保持为真的条件。

冒泡排序的循环不变式说明,在每次外循环迭代开始时(<st c="32130">for i in range(n):</st>),最后的 <st c="32162">i</st> 个元素已排序,并且处于它们的最终位置。 为了证明算法的正确性,我们需要评估 三个条件:

  • 初始化:在第一次迭代之前(i = 0),没有任何元素被处理。 由于空子数组 已经是排序的,所以不变式显然成立。

  • 维护:假设不变式在 i 迭代之前成立。 在第 i 迭代过程中,若相邻元素的顺序不对,则会交换它们。 在第 i 遍历结束时,最大的未排序元素会被“冒泡”到数组中的正确位置,从而确保最后的 i 个元素已排序。 因此,不变式 得以维持。

  • 终止条件:该算法在外部循环执行完n次迭代后终止,其中n是输入数据的大小。在此时,恒等式保证整个数组已排序,因为最后n个元素(即整个数组)已处于正确的位置。

复杂度分析

要理解时间复杂度,我们可以在以下几种场景中进行分析:

为了确定 空间复杂度,我们需要评估排序过程中使用的辅助或临时内存。 冒泡排序被认为是一种原地算法,意味着它直接在输入数组上操作,只需要一个常量数量的额外空间来进行 元素交换。

选择排序

选择排序 是一种基于比较的方法,它 将列表分为两部分:一个已排序部分,从空开始并逐渐增加,一个未排序部分,从包含所有元素开始并逐渐减少。 在每一步中,算法找到未排序部分中最小(或最大)的元素,并将其移到已排序部分的末尾。 这一过程持续进行,直到整个列表 被排序完成。

选择排序以其简单性为特点,始终表现出二次方的运行时间, O(n²),无论在最好、平均还是最坏情况下。 虽然对于大数据集效率低下,但其简单的特性使得它容易理解和实现。 这个算法不是自适应的,意味着它不会利用数据中的任何现有顺序。 然而,选择排序直接在输入数据中操作,只需要少量的常量额外内存(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>)。 需要注意的是,选择排序是不稳定的,它不保证保持相同元素的原始顺序。 尽管有这些缺点,选择排序对于排序小数据集仍然很有用,并且易于 实现。

示例 6.3

表 6.4 展示了选择排序在数组[<st c="35244">29, 10, 14, 37,</st> <st c="35261">13, 5</st>]中的逐步过程:

轮次 操作描述 数组内容
1 在[29, 10, 14, 37, 13, 5]中找到最小值(即 5),并将最小值与 第一个元素 交换 [5, 10, 14, 37, 13, 29]
2 在[10, 14, 37, 13, 29]中找到最小值(即 10),并将最小值与第二个元素交换(无需交换,因为它已经 就位 [5, 10, 14, 37, 13, 29]
3 在[14, 37, 13, 29]中找到最小值(即 13),并将最小值与 第三个元素 交换 [5, 10, 13, 37, 14, 29]
4 在[37, 14, 29]中找到最小值(即 14),并将最小值与 第四个元素 交换 [5, 10, 13, 14, 29, 37]
5 在[37, 29]中找到最小值(即 29),并将最小值与 第五个元素 交换 [5, 10, 13, 14, 37, 29]
现在数组 已经排序完毕。 [5, 10, 13, 14, 29, 37]

表 6.4:演示选择排序的示例

以下是选择排序算法的 Python 实现:

 def selection_sort_iterative(a):
    n = len(a)
    for i in range(n):
        min_id = i
        for j in range(i + 1, n):
            if a[j] < a[min_id]:
                min_id = j
        a[i], a[min_id] = a[min_id], a[i]
    return a

选择排序算法非常简单。 外层循环, <st c="36399">for i in range(n):</st>,遍历数组, <st c="36440">a</st>,内层循环, <st c="36463">for j in range(i + 1, n):</st>,找到最小值的元素。 然后,将最小值与子数组顶部的元素交换: <st c="36607">a[i], a[min_id] =</st> <st c="36625">a[min_id], a[i]</st>

让我们检查算法的正确性并评估 其复杂度。

正确性证明

选择排序中的 循环不变量表示在每次外层循环的开始(<st c="36849">for i in range(n):</st>),子数组 <st c="36887">a[0:i]</st>由原始数组中最小的 <st c="36920">i</st> 个元素组成,且这些元素按升序排列。 我们需要评估三个条件来验证 算法的正确性:

  • 初始化:在第一次迭代之前(i = 0),子数组 a[0:0]是空的。 由于空子数组 已经是有序的,因此不变量显然成立。

  • 维护:假设在第 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:mii</mml:mi></mml:mrow>mml:mrowmml:mit</mml:mi>mml:mih</mml:mi></mml:mrow></mml:msup></mml:math> 次迭代之前,不变量成立。 在这一迭代过程中,算法在子数组 a[i:n]中找出最小的元素,并将其与 a[i]交换。 这样, a[i] 就成了 a[i:n]中的最小元素,子数组 a[0:i+1]也变得有序。 这样可以确保不变量 得以维持。

  • 终止:算法在外层循环执行完 n 次迭代后终止。 此时,不变量保证整个数组 a[0:n]已排序,因为所有元素都已 处理完毕。

复杂度分析

为了 评估选择排序的时间复杂度,我们需要考虑两个嵌套循环。 外部循环运行 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 次,每次迭代,内部循环运行 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:math> 次,总共进行 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:math> 次比较。 由于此操作主导了算法的运行时间,因此最佳、平均和最坏情况下的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

对于空间复杂度,我们需要确定执行选择排序时使用的辅助或临时内存。 由于它采用原地排序方法,因此只需要一个常量量的额外空间用于交换元素,从而使其空间 复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>

插入排序

插入排序 是一种简单直观的算法,它通过逐步构建最终的有序列表,每次插入一个元素。 插入排序 通过将数组分为有序部分和无序部分来操作。 每一步,算法从无序部分取出下一个元素,并将其插入到有序部分的适当位置。 这个过程类似于排序扑克牌,每张牌都放在相对于已经排序好的牌的位置。 已经排序的部分。

插入排序的时间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo²</mml:mo>mml:mo)</mml:mo></mml:math> 在平均情况和最坏情况中均为此复杂度。 然而,在处理接近排序好的或 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 小型数据集时,它展示了卓越的效率。

插入排序的一个主要优点是它作为排序方法的稳定性。 此外,它是一个就地排序算法,仅需常量量的额外内存。 它的简单性和对小型数据集的高效性使插入排序在特定情况下非常有价值,同时也是理解基本 排序概念的优秀教育工具。

示例 6.4

表 6.5 展示了 插入排序对数组 [<st c="39515">8, 3, 1, 7,</st> <st c="39528">0, 10</st>] 的逐步处理过程:

通过 操作的描述 数组的内容
1 关键字 = 3,将 3 与 8 比较,并将 8 移到 右边。将 3 插入到 正确的位置。 [3, 8, 1, 7, 0, 10]
2 关键字 = 1,将 1 与 8 比较,并将 8 移到 右边。将 1 与 3 比较,并将 3 移到 右边。将 1 插入到 正确的位置。 [1, 3, 8, 7, 0, 10]
3 关键字 = 7,将 7 与 8 比较,并将 8 移到 右边。将 7 插入到 正确的位置。 [1, 3, 7, 8, 0, 10]
4 键值 = 0,将 0 与 8 进行比较,并将 8 移到右边。将 0 与 7 进行比较,并将 7 移到右边。将 0 与 3 进行比较,并将 3 移到右边。将 0 与 1 进行比较,并将 1 移到右边。将 0 插入到正确的位置。 [0, 1, 3, 7, 8, 10]
5 键值 = 10,没有元素需要移动,将 10 插入正确的位置。 现在数组 已经排序。 [0, 1, 3, 7, 8, 10]

表 6.5:演示插入排序的示例

以下是插入排序的简单 Python 实现:

 def insertion_sort_iterative(a):
    n = len(a)
    for i in range(1, n):
        pointer = a[i]
        j = i - 1
        while j >= 0 and pointer < a[j]:
            a[j + 1] = a[j]
            j -= 1
        a[j + 1] = pointer
    return a

插入排序算法的核心在于其内部的 <st c="40659">while</st> 循环(<st c="40671">while j >= 0 and key < a[j]:</st>)。 <st c="40725">while</st> 循环的条件确保如果数据是预排序的(或几乎已排序),则算法在时间复杂度上表现为线性。 接下来我们将详细讨论算法的正确性及其复杂度。

正确性证明

插入排序的 循环不变量指出,在外层循环的每次迭代开始时(<st c="41053">for i in range(1, n):</st>), <st c="41095">a[0:i]</st> 中的元素与 <st c="41152">a[0:i]</st>中的元素相同,但现在它们是按顺序排列的。 为了验证算法的正确性,我们需要评估以下 三个条件:

  • 初始化:在第一次迭代之前(i = 1),子数组 a[0:1]仅包含数组中的第一个元素,该元素自然处于正确的位置(已排序)。

  • 维护:让我们假设循环不变量在第 i迭代之前成立。 该算法将 a[i] 插入到已排序的子数组 a[0:i]中,方法是将大于 a[i] 的元素向右移动一个位置。 这个插入操作确保了 a[0:i+1] 是已排序的。 因此,循环不变量 得以保持。

  • 终止条件:该算法在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 次外循环迭代后终止,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 是数组的长度。 此时,不变式保证整个数组 a[0:n]已经排序,因为所有元素都已 处理完毕。

复杂度分析

要理解 时间复杂度,我们可以在以下情况下进行分析:

  • 最佳情况 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> – 如前所示, while 循环的条件确保当输入已经排序时, while 循环不会执行,从而保证了线性 时间复杂度。

  • 平均情况 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> – 算法平均执行 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:math> 次比较和 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:math> 次交换操作。 这是算法的平均表现。

  • 最坏情况 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> - 当输入数组是逆序排列时,发生此情况。 在这种情况下, while 循环的条件使得它在每次迭代中执行 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:math> 次。 考虑到外部循环执行 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 次,最大比较次数和交换次数将 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:math>

要评估 空间复杂度,我们需要评估运行插入排序所需的辅助或临时空间。 鉴于插入排序是一个原地算法,它只需要常量量的额外空间用于元素交换。 因此,它的空间复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>

在介绍了主要的迭代排序算法后,我们现在将重点讨论递归 排序方法。

递归排序算法

那些 迭代的非递归排序算法,虽然通常在空间效率上表现良好,但在时间复杂度方面往往存在不足,特别是在处理大数据集时。 这种限制在涉及大量数据的场景中变得尤为关键,因为这时需要更高效的排序机制。 表 6.1中,我们讨论了递归排序算法的一般特点,突出了它们克服 迭代方法所面临的时间复杂度问题的潜力。

在这一节中,我们分析了两种主要的递归排序算法:归并排序和快速排序。 这两种算法都利用分治策略,与迭代排序算法相比,性能更为优越。 归并排序具有稳定的 O(nlogn) 时间复杂度,提供了稳定且可预测的排序效率。 快速排序以其平均情况的 O(nlogn) 性能著称,并且将简洁性 与高效性相结合。

还值得一提的是,另一个重要的递归排序算法——堆排序,将在 第十三章 中介绍,当时我们会讨论堆结构。 堆排序像归并排序和快速排序一样,采用分治方法,但通过使用二叉堆数据结构来实现,从而提供了一种高效且节省空间的 排序解决方案。

归并排序

归并排序 是一种基于比较的排序 算法,采用分治策略。 其操作过程是通过递归地将未排序的数组分割成两个大致相等的部分,直到每个子数组只包含一个元素(这自然是排序好的)。 然后,算法将这些子数组合并 ,生成新的排序子数组,并持续这个过程,直到只剩下一个排序好的数组,这个数组就是原始数组的排序结果。 在合并步骤中,比较每个子数组的最小元素,然后将两个子数组 合并在一起。

示例 6.5

表 6.6 展示了合并排序对于数组[<st c="45113">38, 27, 43, 3,</st> <st c="45129">9, 82</st>]的逐步过程:

拆分/合并 数组(子数组)的内容
第一次拆分 将[38, 27, 43, 3, 9, 82]拆分为[38, 27, 43]和[3, 9, 82]。
第二次拆分(左半部分) 将[38, 27, 43]拆分为[38]和[27, 43]。将[27, 43]拆分为[27] 和[43]。
合并(左半部分) 将[27]和[43]合并得到[27, 43]。将[38]和[27, 43]合并得到[27, 38, 43]。
第二次拆分(右半部分) 将[3, 9, 82]拆分为[3]和[9, 82]。将[9, 82]拆分为[9] 和[82]。
合并(右半部分) 将[9]和[82]合并得到[9, 82]。将[3]和[9, 82]合并得到[3, 9, 82]。
最终合并 将[27, 38, 43]和[3, 9, 82]合并得到[3, 9, 27, 38, 43, 82]。现在数组已排序:[3, 9, 27, 38, 43, 82]。

表 6.6:演示合并排序的示例

以下是合并排序算法的 Python 实现: 排序算法:

 import numpy as np
def merge(A,p,q,r):
    n1=q-p+1
    n2=r-q
    n11=n1+1
    n22=n2+1
    left = [0 for i in range(n11)]
    right = [0 for i in range(n22)]
    for i in range(n1):
        left[i]=A[p+i-1]
    for j in range(n2):
        right[j]=A[q+j]
    left[n11-1]=1000  #very large number
    right[n22-1]=1000 #very large number
    i=0
    j=0
    for k in range(p-1,r):
        if left[i]<=right[j]:
            A[k]=left[i]
            i=i+1
        else:
            A[k]=right[j]
            j=j+1
    return(A)
def mergeSort(A,p,r):
    if p<r:
        q=int(np.floor((p+r)/2))
        mergeSort(A,p,q)
        mergeSort(A,q+1,r)
        merge(A,p,q,r)
    return(A)

尽管 有许多不同的合并排序实现 ,我们展示的版本非常直接。 该算法的关键组件是一个名为 <st c="46561">merge</st> 的函数,用于合并已经 递归排序的两个分区。

正确性证明

合并排序中的循环不变量定义指出,在每次合并过程的开始, <st c="46771">左分区</st> <st c="46790">右分区</st> 子数组已经是排序好的。 我们需要评估三个条件以证明 算法的正确性:

  • 初始化:在第一次合并操作之前,子数组包含单个元素,这些元素 本身就是排序好的。

  • 维护:假设在合并两个子数组之前,不变量是成立的。 在合并过程中,从任一子数组中选择最小的剩余元素并将其添加到合并数组中。 这确保了合并数组的排序顺序。 不变量成立的原因是每一步选择都确保合并后的数组 保持有序。

  • 终止条件:当所有子数组都合并成一个排序数组时,算法终止。 此时,不变量保证整个数组 是有序的。

复杂度分析

归并排序是一种递归排序算法,其操作遵循递归函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>。在每一步递归中,归并排序将问题分解为两个几乎相等的子问题,反映了典型的分治法关系 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mia</mml:mi>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 其中 a=2 b=2。数组的分割和已排序子数组的合并都是线性时间操作, 因此 f(n)=O(n)

为了评估归并排序的运行时间复杂度,我们需要通过主定理等方法来求解这个递推函数。 主定理提供了一种简洁的方法来解决形式为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mia</mml:mi>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 通过将 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mrowmml:mrowmml:msubmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mia</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:msup></mml:math>进行比较

在归并排序的情况下, a=2 b=2 并且 f(n)=O(n)

我们计算 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub>mml:mia</mml:mi>mml:mo=</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mn2</mml:mn>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math>。根据以下的 主定理:

  • 如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mic</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi>mml:mo<</mml:mo>mml:mrowmml:mrowmml:msubmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mia</mml:mi></mml:mrow></mml:mrow></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mrowmml:mrowmml:msubmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mia</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

  • 如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi>mml:mih</mml:mi>mml:mie</mml:mi>mml:mit</mml:mi>mml:mia</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mrowmml:mrowmml:msubmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mia</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 那么 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mrowmml:mrowmml:msubmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mia</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:msup>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>

  • 如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">Ω</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mic</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi>mml:mo></mml:mo>mml:mrowmml:mrowmml:msubmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mia</mml:mi></mml:mrow></mml:mrow></mml:math>,并且如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:mfrac></mml:rrow></mml:mfenced>mml:mo≤</mml:mo>mml:mic</mml:mi>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 对于某些 c<1 且当 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>时, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math>

在我们的例子中, f(n)=O(n) 符合主定理的第二种情况,因为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> <mml:math xmlns=mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mrowmml:mrowmml:msubmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mrow></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>。因此,适用第二种情况,得出以下结论:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

为了评估归并排序的时间复杂度,我们可以在以下几种情况进行分析:

  • 最佳情况: O(nlogn) – 合并排序始终以对数步长分割数组并合并子数组。

  • 平均情况: O(nlogn) – 由于其 分治法,算法在所有情况下都能保持一致的表现。

  • 最坏情况: O(nlogn) – 归并排序不是自适应的,这意味着无论初始元素的顺序如何,它的表现始终保持一致。 元素

要确定空间复杂度,我们需要考虑执行归并排序所需的辅助或临时空间。 在归并排序算法中,使用了两个数组, <st c="49363">left_partition</st> <st c="49382">right_partition</st>,作为合并操作期间的临时存储。 这两个数组的总大小等于输入大小,因此空间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

总之,归并排序 是一种高效且稳定的排序算法,其时间复杂度为OnlognOnlogn 在最佳、平均和最坏情况下均如此。 它采用分治法,将数组分成更小的子数组,递归排序后再按顺序合并。 尽管它需要O(n) 额外的空间,但它稳定的表现和一致性使它成为排序大型数据集的优选,特别是在需要稳定性时。 归并排序的递归特性和可靠性能使其成为计算机科学中的基础算法。

快速排序

快速排序是一种 高效的分治排序算法,像归并排序一样,依赖于比较。 它通过递归从数组中选择一个枢轴元素,并根据其他元素是否小于或大于该枢轴,将剩余元素划分为两个子数组。 然后,这些子数组会递归排序。 这个过程会一直重复,直到达到基准情况:空数组或只有一个元素的子数组,这本身就已经是排序好的。 此过程将一直进行,直到所有子数组为空或只有一个元素为止。 快速排序的效果在很大程度上取决于枢轴的选择和划分方法。 划分方法的选择对于最终性能有很大影响。

快速排序算法由三个 关键组成部分构成:

  • 枢轴选择:这一步骤涉及从数组中选择一个元素作为枢轴。 常见的选择有第一个、最后一个、中间元素,或随机选择一个元素。 具体的枢轴选择策略会影响算法的整体性能。 在概率快速排序中,枢轴是随机选择的(见 第九章)。

  • 划分:在这一步中,数组被分成两个较小的子数组。 所有小于枢轴的元素放入一个子数组,而大于枢轴的元素放入另一个子数组。 经过这一步,枢轴元素已经处于其最终排序位置。 在数组中。

  • 递归:划分步骤会递归地重复,直到所有子数组为空或只包含一个元素,最终得到一个完全 排序好的数组。

示例 6.6

我们用六个随机数的数组来说明操作——[<st c="51665">35, 12, 99, 42,</st> <st c="51682">5, 8</st>]:

  1. 选择 一个枢轴(例如, 8)并重新排列数组,使得小于枢轴的元素位于其前面,大于枢轴的元素位于其后。

  2. 递归排序左 子数组 <st c="51864">[5]</st> 已经 排序完毕。

  3. 递归排序右子数组 <st c="51960">35</st>)并重新排列: <st c="51980">[5, 8, 12, 35,</st> <st c="51995">42, 99]</st>

  4. 递归排序子数组 [12] [****42, 99]

    • [12] 已经排序。

    • 选择一个枢轴(例如, 42)并重新排列 [42, 99] [5, 8, 12, 35, 42, 99]

  5. 现在数组已经排序: [5, 8, 12, 35, 42, 99]

以下是快速排序算法的 Python 实现: 排序算法:

 def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    else:
        pivot = arr[len(arr) // 2]
        left = [x for x in arr if x < pivot]
        middle = [x for x in arr if x == pivot]
        right = [x for x in arr if x > pivot]
        return quick_sort(left) + middle + quick_sort(right)

如所示,快速排序算法的核心概念是选择一个枢轴元素进行分区。 这个过程会递归地重复。

正确性证明

归并排序中的循环不变式 定义保证在每次分区过程开始时,所有 <st c="52827">left_partition</st> 中的元素都不大于 <st c="52867">pivot</st>,并且所有 <st c="52894">right_partition</st> 中的元素都不小于 <st c="52932">pivot</st>。我们需要评估以下三个条件来验证 算法的正确性:

  • 初始化:在第一次分区操作之前,子数组是整个数组,由于尚未处理任何元素,循环不变式显然成立。

  • 维护:在分区过程中,元素会与枢轴进行比较,并根据需要交换位置。 这确保了两个子数组, left_partition right_partition,按照定义被维护; left_partition 包含小于 pivot的元素, right_partition 包含大于 pivot**的元素。

  • 终止:当子数组的长度为 0 或 1 时,算法终止,因为这些子数组本身已经排序。 循环不变式保证在每一步中元素都被正确分区,从而确保整个数组在完成时已经排序。

复杂度分析

快速排序的特点是 以下递归函数: 递归函数:

T(n)=T(k)+T(n−k−1)+O(n)

这里, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 是小于主元的元素个数。 为了简化分析,我们假设一个理想情况,其中主元总是将列表分成两个相等的部分,得出以下 递推函数:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

这可以 表示为一个通用的分治函数形式:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mia</mml:mi>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> a=2 b=2 以及 f(n)=O(n)

为了确定时间复杂度,我们应用主定理。 主定理帮助求解形式为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mia</mml:mi>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 通过比较 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub>mml:mia</mml:mi></mml:mrow></mml:msup></mml:math>进行比较。 在快速排序的情况下, a=2 b=2 并且 f(n)=O(n)

在我们的例子中, f(n)=O(n) 匹配了主定理的第二种情况,因为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mrowmml:mrowmml:msubmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mrow></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>。因此,适用第二种情况,得出以下结论:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

这表明,快速排序的平均时间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>。由于这种高效性,快速排序对于大数据集非常有效。 然而,在最坏的情况下,当枢轴选择不当,导致持续的不平衡分割(例如,总是选择最小或最大的元素作为枢轴)时,递推关系变为 以下形式:

T(n)=T(n−1)+T(0)+O(n)

这简化为 如下结果:

T(n)=T(n−1)+O(n)

解决这个递归关系得到 如下结果:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

因此,快速排序的最坏情况时间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>。然而,通过采用随机基准选择或三数中值法等策略,可以大大减少遇到最坏情况的几率,从而帮助保持算法的平均情况表现。 以下是快速排序的 时间复杂度总结:

  • 最佳情况<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mi </mml:mi>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math> – 这种情况发生在基准值始终将数组划分为两个几乎相等的部分时

  • 平均情况: O(nlogn) – 该算法在随机 枢轴选择下表现良好

  • 最坏情况: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> – 当枢轴选择始终导致最不平衡的划分时发生(例如,最小或 最大元素)

快速排序是一种 递归算法,其特征是递归关系 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>。在每次递归步骤中,快速排序将问题分成两个几乎相等的子问题,从而得到关系 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mia</mml:mi>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>,其中 a=b=2。分割和合并操作在 O(n) 时间内完成,表示为 f(n)=O(n)。要理解时间复杂度,我们可以在以下场景中进行分析。

对于空间复杂度,我们需要考虑执行快速排序所需的辅助或临时空间。 快速排序需要额外的空间来存储递归栈。 在平均情况下,栈的深度是 O(logn)。然而,在最坏的情况下(例如,不平衡的分区),栈的深度可能是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

总的来说,快速排序的性能受选取枢轴和划分方法的显著影响。 不当的枢轴选择可能导致最坏情况下的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo²</mml:mo>mml:mo)</mml:mo></mml:math>,而平均情况下的性能为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>。诸如随机枢轴选择或三数取中法等技术可以帮助减少枢轴选择不当的风险。 尽管快速排序不是稳定排序算法,但它是就地排序,并且在平均情况下仅需要 O(logn) 的辅助空间来存储递归栈。 其高效性和简洁性使得快速排序在各种应用中成为处理大数据集时的热门选择。

到目前为止,我们讨论的所有排序算法,无论是递归的还是非递归的,都是基于比较的。 现在,我们将讨论排序算法中的另一个重要主题——非比较排序方法,它可以实现 线性时间

非比较排序

我们已经注意到 所有基于比较的排序算法具有下界时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Ω</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>,这意味着没有任何基于比较的算法能够达到比 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:math> 的性能。 然而,也有一些排序算法不依赖于比较。 这些非比较基排序算法利用了关于数据的某些假设或信息,从而实现了 更好的性能。

基于比较的算法不同,非比较基排序算法可以像线性时间一样高效地实现下界。 这种显著的效率使得它们有时被称为线性时间排序算法。 通过利用数据的特定属性,如有限的整数值范围或数字分布,这些算法能够绕过比较限制,并在合适的条件下更快地排序。 在本节中,我们将探讨三种著名的非比较基排序算法:计数排序、基数排序和 桶排序。

计数排序

计数排序 是我们在本节讨论的第一个非比较排序算法。 它通过计数输入数组中每个不同元素的频率来排序数组中的元素。 然后,这个频率计数被用来确定每个元素在排序输出中的正确位置。 因此,在这个排序过程中没有涉及任何比较操作。 计数排序特别适用于排序在特定范围内的整数,并以其线性时间复杂度而著称,在合适的条件下非常高效。 与其他排序算法不同,计数排序不进行元素之间的比较,从而避开了 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Ω</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 基于比较的排序的下界。 让我们通过 一个示例逐步演示计数排序算法。

计数排序是一种高效的算法,时间复杂度为 O(n+k)。它也是一种稳定的排序方法,附加内存使用量最小。 尽管它需要额外的空间来存储输入值的范围,但它保持线性空间复杂度。 由于其线性时间复杂度,计数排序广泛应用于排序元素范围有限的大型数据集。 它在合适条件下的简洁性和高效性,使它成为 算法设计师的重要工具。 接下来,我们通过 以下示例来解释计数排序算法。

示例 6.7

我们通过一个包含六个随机数字的数组来说明计数排序的操作—[<st c="59948">4, 2, 2, 8,</st> <st c="59961">3, 3</st>]:

确定 范围:

  • 找到 最大元素(8)和最小 元素(2

  • 创建一个大小为 max−min+1=8−2+1=7的计数数组,初始化为 0

计数出现次数:

  • 统计每个元素的出现次数并将其存储在 计数数组

  • 计数数组: [2, 2, 1, 0, 0, 0, 1]

  • 这对应于元素 2, 3, 4, 5, 6, 7, 8

累计计数:

  • 通过将前一个元素的计数加到每个元素中来修改计数数组。 这有助于确定元素的位置

  • 累计计数数组: [2, 4, 5, 5, 5, 5, 6]

构建 输出数组:

  • 根据累计计数将每个元素放入输出数组的正确位置。 我们从最后一个元素开始,直到 第一个元素。

表格 6.7 展示了数组[<st c="60725">4, 2, 2, 8,</st> <st c="60738">3, 3</st>]进行计数排序的逐步过程:

数组中的元素 在数组中的位置 累计计数数组 输出数组
3 2, 3, 5, 5, 5, 5, 6 0, 0, 0, 3, 0, 0
3 2, 2, 5, 5, 5, 5, 6 0, 0, 3, 3, 0, 0
8 2, 2, 5, 5, 5, 5, 5 0, 0, 3, 3, 0, 8
2 1, 2, 5, 5, 5, 5, 5 0, 2, 3, 3, 0, 8
2 0, 2, 5, 5, 5, 5, 5 2, 2, 3, 3, 0, 8
4 0, 2, 4, 5, 5, 5, 5 2, 2, 3, 3, 4, 8

表格 6.7:演示计数排序的示例

以下是 一个简单的 Python 实现的计数排序算法:

 def counting_sort(arr):
    if not arr:
        return arr
    max_val = max(arr)
    min_val = min(arr)
    range_of_elements = max_val - min_val + 1
    # Create a count array to store count of individual elements and initialize it to 0
    count = [0] * range_of_elements
    output = [0] * len(arr)
    # Store the count of each element
    for num in arr:
        count[num - min_val] += 1
    # Change count[i] so that count[i] contains the actual position of this element in the output array
    for i in range(1, len(count)):
        count[i] += count[i - 1]
    # Build the output array
    for num in reversed(arr):
        output[count[num - min_val] - 1] = num
        count[num - min_val] -= 1
    # Copy the output array to arr, so that arr contains sorted numbers
    for i in range(len(arr)):
        arr[i] = output[i]
    return arr

<st c="61916">计数排序</st> 算法 相当简单。 前两个循环(<st c="61978">for i in range(1, len(count)):</st> <st c="62014">for i in range(1, len(count)):</st>)构建了一个累积计数数组。 然后,这个数组在第三个循环(<st c="62122">for k in reversed(a):</st>)中用于构建 排序数组。

正确性证明

计数排序中的循环不变量指出,在处理每个元素之后,累积 <st c="62293">计数</st> 数组正确反映数组中小于或等于每个值的元素数量,且输出数组包含已排序的元素,直到当前索引。 我们需要评估算法正确性的三个条件:

  • 初始化:在处理任何元素之前, 计数 数组初始化为 0,输出数组为空。 此时,不变量 显然成立。

  • 维护:在处理每个元素时,累积 计数 数组会更新,以反映元素的正确计数。 在构建输出数组的过程中,元素根据累积计数放入正确的位置,从而确保排序顺序 得以保持。

  • 终止:算法在所有元素处理并放入输出数组后终止。 此时,不变量保证输出数组已排序,且累积 计数 数组准确反映了元素的位置。

复杂度分析

计数排序 具有线性时间复杂度,受数组大小(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>)和输入值的范围 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> (其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi>mml:mo=</mml:mo>mml:mtextmax</mml:mtext>mml:mo-</mml:mo>mml:mtextmin</mml:mtext>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:math>)。 让我们在以下三种情况下分析计数排序的时间复杂度:

  • 最优情况:在最优情况下,计数排序遍历输入数组以计算每个元素的频率(计数),然后遍历 计数 数组以计算累计计数,最后再次遍历输入数组,将元素放置到输出数组的正确位置。 这个过程需要 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 时间来计算元素, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mik</mml:mi>mml:mo)</mml:mo></mml:math> 时间来计算累计计数,最后 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 时间来构建输出数组,最终总时间复杂度为 O(n+k)

  • 平均情况:计数排序的平均时间复杂度为 O(n+k) 因为涉及的步骤(计数元素、计算累计计数、构建输出数组)无论初始顺序或元素分布如何都保持一致。 输入数组中的每个元素都会被处理固定次数

  • 最坏情况:在最坏情况下,计数排序的时间复杂度仍为 O(n+k) 最坏情况发生在输入值的范围相较于输入数组的大小较大时。 尽管如此,计数、计算累计计数以及将元素放入输出数组的操作相对于元素数量和范围 的值是在线性时间内完成的。

现在,我们需要估算计数排序的空间复杂度。 计数排序需要额外的空间来存储 <st c="64871">count</st> 数组和输出数组(<st c="64905">temp</st>),因此其辅助空间复杂度为 O(n+k)。让我们来看看计数排序所需的辅助空间。 首先是 count 数组,它用于存储在 min,max范围内每个元素的频率。 count 数组的大小与输入值的范围成正比,即 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 第二个辅助内存空间是输出数组。 与输入数组大小相同的输出数组(<st c="65293">temp</st>)用于存储排序后的元素。 这需要 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 额外的空间。 然后,计数排序所需的总辅助空间是 count 数组和输出数组空间的总和,结果为 O(n+k)

计数排序是一个高效的线性时间排序算法,但它有一个主要的限制:其效率 高度依赖于待排序数组中元素的范围。 此外,计数排序仅限于整数数值数据。 在下一个小节中,我们将讨论基数排序,它可以解决计数排序固有的一些限制。 计数排序。

基数排序

基数排序,另一种 非比较型 排序技术,通过顺序处理单独的数字或字符来排序元素。 它利用排序可以一次按位置进行,从最不重要的位开始,逐步向最重要的位推进的概念。 通过为每个位置使用稳定的排序算法,如计数排序,基数排序保持元素在共享相同数字或字符时的原始顺序。 这种方法使得基数排序在处理由数字或固定长度字符串组成的数据集时非常高效。

基数排序特别适用于排序大量固定数字或字符长度的整数或字符串。 该算法利用计数排序保持稳定性,并对每一位实现线性时间复杂度。 虽然基数排序需要额外的空间来存储输出和计数数组,但其整体效率使其成为特定类型数据的绝佳选择,尤其是当数字或字符的数量已知且有限时。 理解基数排序及其机制可以在 适当的场景中提供显著的性能优势。

除了线性时间复杂度和稳定性外,基数排序算法还提供了若干其他优点,包括 以下几点:

  • 适用于大数据:基数排序对于排序大数据集特别有效,尤其是当输入值的范围并不比元素的数量大很多时。 它能够很好地处理大量数据,特别是当键是整数或长度固定的字符串时。

  • 可预测的性能:基数排序始终表现出色,没有最坏情况的退化,正如快速排序中所见。 它的时间复杂度是可预测的,不依赖于输入数据的 初始顺序。

  • 可扩展性:基数排序可以很容易地适应排序除整数以外的数据类型,例如字符串 或其他序列,方法是使用不同的基数或将每个字符视为 “数字”。

让我们通过一个例子逐步演示基数排序算法。

例 6.8

让我们举例说明 基数排序的操作,使用六个随机数的数组——[<st c="68084">170, 45, 75, 90,</st> <st c="68102">802, 24</st>]:

  1. 按最不重要的数字排序原始数组(个位数):

    对最不重要的数字应用计数排序:[170, 90, 802, 24, 45, 75]

  2. 按照第二个最不重要的数字(10 位)排序:

    对第二个最不重要的数字应用计数排序:[802, 24, 45, 75, 170, 90]

  3. 按照最重要数字(100 位)排序:

    对最重要数字应用计数排序:[24, 45, 75, 90, 170, 802]

现在数组已排序:[24, 45, 75, 90, 170, 802]

这是基数排序算法的 Python 实现:

 def count_sort(a, e):
    size = len(a)
    result = [0] * size
    count = [0] * 10  # For digits 0-9
    for i in range(size):
        digit = (a[i] // e) % 10
        count[digit] += 1
    for i in range(1, 10):
        count[i] += count[i - 1]
    for i in range(size - 1, -1, -1):
        digit = (a[i] // e) % 10
        result[count[digit] - 1] = a[i]
        count[digit] -= 1
    for i in range(size):
        a[i] = result[i]
def radix_sort(a):
    maxv = max(a)
    e = 1
    while maxv // e > 0:
        count_sort(a, e)
        e *= 10

在上述基数排序算法实现中,radix_sort(a)函数接受输入数组(a),并从最不重要的数字开始应用计数排序(count_sort(a, e))。它根据每个数字在适当的位数上排序数据(e)。

正确性证明

在基数排序中,循环不变式定义为:每次按每个数字(从最不重要到最重要的顺序)排序后,数组会相对于该数字部分排序,同时保持具有相同数字的元素的相对顺序。

  • 初始化:在处理任何数字之前,数组是未排序的。由于没有部分排序,循环不变式显然成立。

  • 维护:在每次迭代中,应用计数排序基于当前数字对元素进行排序。由于计数排序是稳定的,它保持具有相同数字的元素的相对顺序。这保证了每次经过排序后,数组相对于该数字是部分排序的。

  • 终止:算法在按最重要数字排序后终止。此时,循环不变式保证数组已完全排序,因为所有数字都按重要性顺序处理过。

复杂度分析

基数排序 的时间复杂度依赖于最大数字的位数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mid</mml:mi></mml:math>、元素的数量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>以及位值的范围 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math>。让我们分析基数排序在 三种情况下的时间复杂度:

  • 最佳情况:在最佳情况下,基数排序处理数组中每个数字的每一位。 对于每一位,它使用计数排序,计数排序的时间复杂度为 O(n+k) 时间。 由于有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mid</mml:mi></mml:math> 位数,总的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mid</mml:mi>mml:mo⋅</mml:mo><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo+</mml:mo>mml:mik</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math>

  • 平均情况:基数排序的平均时间复杂度保持为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mid</mml:mi>mml:mo⋅</mml:mo><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo+</mml:mo>mml:mik</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math> 因为涉及的步骤(每位使用计数排序排序)对于每一位都一致执行,无论输入值的分布如何。

  • 最坏情况:在最坏的情况下,基数排序仍然表现为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mid</mml:mi>mml:mo⋅</mml:mo><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo+</mml:mo>mml:mik</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math> 的时间复杂度。 这是因为每一位数字都通过计数排序在线性时间内处理,并且该过程会重复 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mid</mml:mi></mml:math> 次,处理 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mid</mml:mi></mml:math> 个数字。

关于基数排序的空间复杂度,它需要额外的空间来存储计数排序使用的计数数组和输出数组,因此产生了一个辅助空间复杂度 O(n+k)

当数组元素分布在一个广泛的范围内时,基数排序的效率较高,这意味着值是稀疏的。 如果元素在范围内密集分布,则可以采用桶排序这一替代方法,具体内容将在 下一小节讨论。

桶排序

我们将要讨论的 最终非比较排序 算法是桶排序。 该方法涉及将数组元素分配到多个容器中,这些容器称为“桶”,每个桶与一特定范围的值相关联。 然后,每个桶中的元素会被独立排序,通常使用另一种排序算法,随后将这些桶合并,形成完整的排序数组。 当输入数据均匀分布在已知范围内时,桶排序表现出卓越的效率。 它利用了分治策略,通过将排序任务分解为更小、更 易管理的子问题来简化排序过程。

桶排序 对于排序大规模数据集,尤其是具有均匀分布值的数据集,非常有效,在这些场景下,相比基于比较的排序算法,提供了显著的性能优势。 理解桶排序及其机制可以帮助优化适用应用中的排序性能。 让我们通过一个例子,逐步演示桶排序算法。

示例 6.9

让我们用一个包含六个随机数的数组来演示桶排序的操作— [<st c="72904">0.78, 0.17, 0.39, 0.26,</st> <st c="72929">0.72, 0.94</st>]:

  1. 将元素 分配到桶中:

    创建一个空的桶列表。

    根据每个元素的值,将其分配到适当的桶中。

    桶: <st c="73091">[[0.17, 0.26], [0.39], [0.72,</st> <st c="73121">0.78], [0.94]]</st>

  2. 排序 每个桶:

    将每个桶内的元素排序。 这可以通过使用另一个排序算法,如 插入排序来实现。

    已排序的桶: <st c="73282">[[0.17, 0.26], [0.39], [0.72,</st> <st c="73312">0.78], [0.94]]</st>

  3. 连接 已排序的桶:

    将所有已排序的桶合并,形成最终的 已排序数组:

    <st c="73422">[0.17, 0.26, 0.39, 0.72,</st> <st c="73448">0.78, 0.94]</st>

数组现在已经排序完毕。

以下是一个简单的 Python 实现的桶排序算法:

 def bucket_sort(a):
    number_of_buckts = len(a)
    buckts = [[] for _ in range(number_of_buckts)]
    for i in a:
        idx = int(i * number_of_buckts)
        buckts[idx].append(i)
    sorted_a = []
    for b in buckts:
        sorted_a.extend(insertion_sort(b))
    return sorted_a

算法非常简单。 首先,设置桶,然后将数组的元素 <st c="73904">a</st>分配到这些桶中(<st c="73943">for I in a:</st>)。 然后,对每个桶进行插入排序并进行连接(<st c="74025">for b in buckts:</st>)。 最后,将已排序的桶进行 合并。

正确性证明

桶排序中的 循环不变式定义为在每次迭代开始时,每个桶内的元素相对于其在桶内的位置是部分排序的。 我们需要评估以下三种条件,以验证 算法的正确性:

  • 初始化:在处理任何元素之前,桶是空的。 由于没有进行部分排序,不变量显然成立。

  • 维护:在每次迭代过程中,元素会根据它们的值被分配到不同的桶中。 逐个对每个桶进行排序确保桶内的元素是有序的。 这保持了 不变量。

  • 终止:当所有的桶都排序完成并连接后,算法终止。 此时, 不变量保证整个数组是有序的,因为每个桶内的元素已排序,且桶被按顺序连接。 按顺序。

复杂度分析

桶排序 的时间复杂度依赖于元素的数量, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>和桶的数量, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math>。元素在桶中的分布也在决定总体时间复杂度中起着关键作用。 我们来分析基数排序在以下 三种情况中的时间复杂度:

  • 最优情况:在最优情况下,元素均匀分布在各个桶中,并且每个桶包含大致相等数量的元素。 如果在桶内使用有效的排序算法,如插入排序,单独排序每个桶的时间是常数时间。 因此,总体时间复杂度 O(n+k)

  • 平均情况:平均来说,桶排序的时间复杂度保持在 O(n+k) 只要元素分布均匀,且桶的数量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math>与元素的数量成正比, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>。每个元素都会被放入一个桶中,时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> ,每个桶根据其包含的元素数量在线性时间内进行排序。

  • 最坏情况:在最坏的情况下,如果所有元素都被放入一个桶中,算法的时间复杂度会退化为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>。这是因为所有元素都需要在一个桶内进行排序,当使用像插入排序这样的排序算法时,时间复杂度会达到平方级别, 从而导致最坏情况。

对于桶排序的空间复杂度,我们知道它需要额外的空间来存储桶和输出数组,从而导致辅助空间复杂度为 O(n+k)。辅助空间有两个组成部分:第一个是桶。 桶是 用来存储特定值范围内的元素。 桶的数量, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math>,通常是根据输入值的范围和分布来选择的。 第二个辅助空间是输出数组。 一个与输入数组大小相同的输出数组用于暂时存储已排序的元素。 这需要 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 额外的空间。 然后,桶排序所需的总辅助空间是桶和输出数组所需空间的总和,结果为 O(n+k)

这就是我们关于非比较排序算法的讨论。 这些算法相比于基于比较的排序算法提供了更好的时间复杂度,但也有一些局限性。 它们受到特定数据类型的限制,并依赖于对数据的假设,这可能会缩小它们的应用范围。

总结

本章全面探讨了各种排序算法,重点介绍了它们的基本原理、效率以及实际应用。 本章首先讨论了区分排序算法的基本属性,例如基于比较与非基于比较的方法、稳定性、适应性和内存使用情况。 这些属性对于理解为什么某些算法更适合特定类型的数据和应用至关重要。 本章强调了时间复杂度的重要性,特别指出基于比较的算法具有一个下限复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Ω</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>,而非基于比较的算法在 合适的条件下可以实现线性时间复杂度。

接下来,本章讨论了具体的排序算法,从迭代方法开始:冒泡排序、选择排序和插入排序。 尽管这些算法实现简单且易于理解,但由于其 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 时间复杂度,已被证明对于大数据集效率低下。 解释过程中包括了详细的逐步操作、代码实现以及使用循环不变量证明其正确性。 每种算法的独特特性,如稳定性和空间效率,都进行了讨论,提供了清晰的理解,说明了它们的优点 和局限性。

最后,本章探讨了更高级的递归排序算法,包括归并排序和快速排序。 这些算法利用分治策略,通过平均时间复杂度为 O(nlogn)的时间复杂度实现了更高效的排序。 归并排序的一致性和稳定性与快速排序可能出现的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 最坏情况下的时间复杂度进行了对比,这可以通过良好的枢轴选择策略来减轻。 此外,本章还涉及了非比较排序算法,如计数排序、基数排序和桶排序,解释了它们如何通过利用特定的数据特性实现线性时间复杂度。 本章总结时强调了根据数据集的特点和应用的具体要求选择合适排序算法的重要性。 应用程序的需求。

在下一章中,我们将重点讨论算法设计中的另一个核心问题——搜索。 然后, 第八章 探讨了排序与搜索之间的关系,揭示了其中的有趣模式 和联系。

参考文献及进一步阅读

  • 算法导论. 作者:Thomas H. Cormen,Charles E. Leiserson,Ronald L. Rivest 和 Clifford Stein第四版。 MIT 出版社。 2022 年

    • 第二章, 入门

    • 第四章,分治法(包括 归并排序)

    • 第七章,快速排序

    • 第八章,线性时间排序 线性时间排序

    • 第九章,中位数与 顺序统计量

  • 算法. 由 R. Sedgewick 和 K. Wayne第四版Addison-Wesley2011 年

    • 第二章: 基本排序
  • 计算机程序设计艺术,第 3 卷:排序与查找,作者:唐纳德· E. 克努斯

    • 第五章 排序
  • C++中的数据结构与算法分析,作者:马克· 艾伦·魏斯

    • 第七章 排序
  • 算法设计手册,作者:史蒂文· S. 斯基纳

    • 第四章 排序与查找

第十章:7

搜索算法

在数据与信息处理领域,搜索和信息检索扮演着至关重要的角色。 搜索算法的效率和准确性直接影响到各种应用的效果,从数据库管理系统到搜索引擎。 本章讨论了搜索算法的关键重要性,通过一系列示例阐明它们的基本特性。 的例子。

搜索算法的设计旨在优化检索过程,使其更加快速高效。 这个优化的关键之一是排序,它通过将数据组织成有利于快速搜索的方式来加速检索过程。 排序和搜索之间的相互作用在许多应用中都很明显,排序后的数据可以支持更复杂、更快速的搜索技术。 我们将在下一章探讨排序和搜索之间的这一重要关系。 然而,当我们将搜索的概念与排序分开时,通常会留下基本的顺序搜索。 顺序搜索以线性时间运行,逐个扫描每个元素,直到找到所需的结果。 这种方法虽然简单,但并不总是最有效的,尤其是在处理 大数据集时。

为了克服顺序搜索的局限性,我们可以利用数据假设来设计更先进的技术,如哈希。 哈希将数据转换为固定大小的值或哈希码(哈希值),在理想条件下,可以实现常数时间的搜索操作。 本章探讨了这些先进技术,说明了数据假设如何显著提升搜索性能。 通过使用哈希函数,我们可以实现常数时间复杂度,大幅提高搜索操作的效率, 在许多应用中。

在本章中,搜索算法被分为三类:线性时间搜索算法、亚线性(例如对数时间)搜索算法,以及利用哈希的常数时间搜索算法。 每一类都将详细讨论,重点关注其特性、应用场景和性能特征。 本章的结构 如下:

  • 搜索算法的特性 搜索算法

  • 线性时间和对数时间 搜索算法

  • 哈希

搜索算法的特性

在深入了解搜索算法及其特性之前,首先要区分计算机科学中两种搜索类型: 算法搜索 人工智能中的搜索 (AI)。 虽然这两种搜索类型有一些相似之处,但它们在目标和方法上存在明显的差异

在算法和 人工智能 中的 搜索 在目标、方法论和应用上可能存在显著差异。 下面是对比两者关键差异的总结。

算法搜索指的是在数据结构中找到特定元素或元素集的过程,例如数组、列表或树。 其主要目标是尽可能快速地定位到目标元素,通常通过时间复杂度来衡量(例如, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> O(logn))。 另一方面,算法搜索的正确性是确保算法正确地识别目标元素的存在与否

我们通过以下方法之一来实现算法搜索:

  • 线性查找:逐个遍历每个元素,直到找到目标或到达结构的末尾

  • 二分查找:通过反复将查找区间 对半分割,高效地定位排序数组中的元素

  • 哈希:利用哈希函数将元素映射到特定位置,以实现 快速访问

  • 树遍历:在树结构中查找,例如二叉搜索树、AVL 树和 红黑树

算法搜索的主要应用是在数据检索中,广泛用于数据库、文件系统和一般的数据处理任务。

另一方面,我们有 AI 中的搜索算法,简称 AI 搜索。 AI 搜索涉及在问题空间中,从初始状态到目标状态,找到一系列动作或路径。 它通常处理更复杂且动态的环境,并且目标与算法搜索不同。 AI 搜索的主要目标是解决问题——找到一个复杂问题的解,该问题可能需要穿越一个庞大的状态空间。 此外,AI 搜索的目标是从众多可能的选项中找到最佳或最有效的解决方案,并且必须能够处理动态和不确定的环境,在这些环境中,条件和目标 可能会发生变化。

AI 搜索算法 根据问题类型和我们对目标状态的了解,使用不同的 策略来实现。 虽然这些方法可以通过多种方式进行分类,但最著名的 AI 搜索方法是未加信息搜索和加信息搜索。 未加信息搜索技术,如 例如 广度优先搜索 (BFS) 和 深度优先搜索 (DFS),在没有关于目标的特定知识的情况下,探索搜索空间。 相比之下,加信息搜索技术,如 A*搜索 贪心搜索,使用启发式方法来引导搜索 过程,使得向目标的搜索更加高效。 启发式方法提供到达目标的估计成本,帮助优先选择那些看起来 更有前景的路径。

AI 搜索有许多应用。 例如,在机器人技术中,搜索帮助代理在动态环境中进行导航和执行任务。 在游戏中,AI 代理搜索最佳的棋步,在象棋、围棋和 视频游戏等游戏中。

总之,算法搜索专注于使用明确定义的程序和数据结构,在结构化数据中高效地查找特定元素。 相比之下,AI 搜索涉及探索大型且通常是非结构化的问题空间,以找到复杂问题的最优或可行解,采用未加信息和加信息的技术,通常结合启发式方法和 学习方法。

本质上,尽管这两种类型的搜索都旨在寻找解决方案,但算法搜索通常更关注在定义明确的约束条件下进行数据检索和操作,而人工智能搜索则处理更广泛和复杂的问题解决场景,通常需要适应性 和学习。

在本节中,我们介绍了几种属性,用于评估和比较不同的算法搜索算法(在本章中,我们将 搜索 用于指代算法搜索)。 这些属性,除了时间和空间复杂度外,还提供了一个基准,帮助我们理解搜索算法的行为和效率。 通过考虑这些属性,我们可以 为特定场景选择最合适的算法:

  • 数据结构要求:不同的搜索算法可能需要特定的数据结构才能高效地运行。 例如,二分查找要求数据是有序的,才能正确执行,而顺序查找可以在任何线性数据结构中工作,如数组或 链表。

  • 适应性:一些搜索算法具有根据输入数据特征进行自我调整的能力,从而提高其性能。 例如,插值搜索可以根据数据分布进行调整,在均匀分布的数据集上,比二分查找表现得要好得多。 分布的数据集。

  • 实现复杂性:算法实现的复杂性是一个实际考虑因素,尤其在时间紧迫的情况下。 例如,顺序搜索等简单算法容易实现和理解,而更复杂的算法,如平衡搜索树(参见 第十三章)或哈希算法,则需要对数据结构和 算法设计有更深入的理解。

  • 预处理要求:某些搜索算法要求在应用之前对数据进行预处理。 例如,二分查找要求数据已排序,这增加了整体时间复杂度。 预处理步骤有时可能会抵消更快搜索时间的好处,特别是当数据频繁变化并需要 不断重新排序时。

  • 最优性:一些算法基于其时间复杂度和性能特征,在特定场景下被认为是最优的。 例如,二分查找在排序数组中进行查找时是 最优的,因为它具有对数时间复杂度。 然而,最优性可能根据上下文和应用的具体要求而变化。 在一个场景中最优的算法,在另一个场景中可能不是最佳选择,特别是当假设条件或 环境变化时。

通过考察这些关键特性——数据结构需求、适应性、实现复杂度、预处理需求和最优性——我们可以在不同上下文中做出明智的决策,选择合适的搜索算法。 这种全面的理解确保我们为数据处理需求选择最有效和高效的搜索技术。

搜索算法的特性中,时间复杂度是最关键的因素。 搜索算法的效率主要由其时间复杂度决定,这决定了算法在数据集中定位元素的速度。 因此,搜索算法通常根据时间复杂度进行分类,以便清晰地理解其在 不同条件下的表现。

在接下来的章节中,我们将探索按时间复杂度分类的搜索算法。 这些类别提供了一种结构化的方法来分析和比较各种 搜索技术:

  • 线性时间搜索算法:这些 算法,如顺序查找,的运行时间是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> ,这意味着找到一个元素所需的时间随着数据集大小的增加而线性增长。 我们将探索线性时间算法适用的场景以及它们的 实现细节。

  • 子线性时间搜索算法:此类算法包括二分查找, 其操作时间为 O(logn) 时间。 二分查找对于排序后的数据集特别高效,利用分治策略迅速缩小搜索空间。 第十三章中,我们将讨论一种基于特定数据结构的搜索算法 ,该数据结构被称为 二叉搜索树 (BSTs)。 二叉搜索树保持元素的有序排列,从而允许高效的搜索、插入和 删除操作。

  • 常数时间搜索算法:这些 算法旨在实现 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 时间复杂度,其中查找元素所需的时间无论数据集大小如何都保持不变。 哈希是实现常数时间搜索操作的主要技术。 我们将研究哈希函数是如何工作的,它们的实现方式,以及在什么条件下它们能提供 最佳性能。

通过理解并根据时间复杂度对搜索算法进行分类,我们可以更好地理解它们的优缺点。 这种结构化的方法使我们能够为给定的应用选择最合适的算法,从而确保高效且有效的 数据检索。

线性时间和对数时间搜索算法

搜索算法的研究中,理解线性搜索和子线性搜索方法对于选择最 高效的方法来解决特定问题至关重要。 线性搜索是最简单的方法,它通过顺序检查数据集中的每个元素,直到找到目标元素或达到数据集的末尾。 虽然这种方法简单且对小型或未排序的数据集有效,但其 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 时间复杂度使其在处理大数据集时变得不切实际。 相比之下,子线性搜索算法,如二分搜索和跳跃搜索,提供了更高效的解决方案,时间复杂度优于 O(n),通常利用已排序数据的属性显著减少所需的比较次数。 通过比较这两类算法,我们可以欣赏搜索技术的进步及其在优化数据 检索过程中的应用。

线性或顺序搜索

一种 通用的搜索算法,无论数据是否有任何假设(是否排序),其渐近上界为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>。这是因为,在最坏的情况下,我们可能需要访问并评估数据集中的每个元素,以确定目标元素是否存在。 因此,线性搜索算法的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>。我们来分别分析线性搜索算法的递归和迭代(非递归)实现。

线性搜索的迭代(非递归)实现通过逐个检查数组中的每个元素,判断其是否与目标元素匹配。 以下是一个简单的 Python 实现 线性搜索:

 def iterative_linear_search(a, target):
    for index in range(len(a)):
        if a[index] == target:
            return index
    return -1

<st c="12693">估算</st> <st c="12704">迭代线性搜索的时间复杂度非常简单。</st> <st c="12772">该算法包含一个循环,循环内部的所有命令都会执行</st> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> <st c="12842"><st c="12891">次,其中</st> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> <st c="12904"><st c="12953">是数组中的元素个数。</st> <st c="12993">此外,循环结束后的最后一条指令只执行一次。</st> <st c="13055">这导致了运行时间的上界为</st> <st c="13107">O</st> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math><st c="13110"><st c="13111">。</st></st></st></st>

<st c="13112">线性搜索的递归实现涉及检查当前元素,如果未找到目标元素,则递归调用以检查下一个元素。</st> <st c="13279">以下是一个用 Python 实现递归</st> <st c="13333">顺序搜索的代码:</st>

 def recursive_linear_search(a, target, index=0):
    if index >= len(a):
        return -1
    if a[index] == target:
        return index
    return recursive_linear_search(a, target, index + 1)

<st c="13519">The</st> <st c="13524">递归线性搜索</st> <st c="13547">函数接受三个参数:</st> <st c="13581">a</st> <st c="13582">(待搜索的数组),</st> <st c="13606">target</st> <st c="13612">(要查找的元素),以及</st> <st c="13646">index</st> <st c="13651">(数组中的当前位置,默认为</st> <st c="13699">0</st> <st c="13700">)。</st> <st c="13703">如果</st> <st c="13706">index</st> <st c="13711">大于或等于数组的长度,则意味着我们已经到达数组的末尾,目标元素未找到。</st> <st c="13840">函数返回</st> <st c="13861">-1</st> <st c="13863">。如果当前元素在</st> <st c="13891">a[index]</st> <st c="13899">与</st> <st c="13908">target</st> <st c="13914">匹配,函数返回当前的</st> <st c="13949">index</st> <st c="13954">。如果当前元素与目标不匹配,函数会递归调用自己,移动到下一个索引(</st> <st c="14071">index + 1</st> <st c="14081">)。</st> <st c="14085">该算法可以使用减法递归函数进行描述,具体如下:</st>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo>mml:mo+</mml:mo>mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>.

这个递归关系表明,在每次递归中,问题的规模会减少 1(即,需要根据搜索标准评估的数据量减少 1)。 这符合以下 一般形式:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mia</mml:mi>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mib</mml:mi></mml:mrow></mml:mfenced>mml:mo+</mml:mo><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>.

使用 用于递减递归的主定理,我们可以识别出适用的 情况:

  • 情况 1:如果 a<1 那么 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

  • 案例 2:如果 a=1 那么 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

  • 案例 3:如果 a>1 那么 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi>mml:mo/</mml:mo>mml:mib</mml:mi></mml:mrow></mml:msup>mml:mo.</mml:mo>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math>,以及 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi>mml:mo=</mml:mo>mml:mn0</mml:mn></mml:math> 在减法递归函数中的参数时, 情况 1 适用。 因此,时间复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>,这证明了算法的运行时间 是线性的。

两种实现都表现出了 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 的时间复杂度,这意味着在最坏情况下,必须检查数组中的每个元素才能找到目标元素。 然而,两个实现都有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 或常数空间复杂度,这是一个重要的优势。 线性搜索简单易懂,不需要特定的数据格式、预处理或 先前排序。

线性搜索适用于优先考虑简单性和易于实现的场景,尤其是在数据集相对较小或未排序的情况下。 它特别适用于以下情况:

  • 无序或非结构化数据:当数据没有排序或没有存储在一种特定结构中,无法促进更快的搜索方法时,线性搜索是一种直接且 可行的选项

  • 小型数据集:对于小型数据集,更复杂的搜索算法可能不值得投入额外开销,从而使线性搜索成为一个 高效的选择

  • 首次出现搜索:当你需要在数组或列表中找到某个元素的第一次出现时,线性搜索是 适用的

  • 单次或少量搜索:如果你只需要执行一次或几次搜索,线性搜索的简单性可以超过那些需要预处理(例如排序)的复杂算法的优点

线性搜索用于各种应用,其中适用前述条件。 一些常见的应用 包括 以下内容:

  • 扫描器和解析器:线性搜索常用于词法扫描器和解析器中,寻找序列中字符的标记或特定模式 或数据

  • 无序列表中的查找操作:在处理无序列表或数组时,线性搜索被用来找到特定的元素 或值

  • 验证和核实:线性搜索用于验证输入或核实列表中是否存在某个元素,例如检查用户输入的值是否存在于数据库 或列表中

  • 实时系统:在数据不断变化且无法进行排序的实时系统中,线性搜索提供了一种快速查找元素的方法,无需 预处理

  • 嵌入式系统:在资源有限的嵌入式系统中,线性搜索的常数空间复杂度使其成为搜索操作的合适选择

虽然线性搜索可能不是大数据集或已排序数据集的最有效算法,但其简单性、常数空间复杂度和灵活性使其在各种应用中成为一项有价值的工具,尤其是当处理无序或 小型数据集时。

子线性搜索

在算法设计中,子线性查找算法代表了一类比线性时间更高效的查找技术。 这些算法特别强大,因为它们可以在不需要检查数据集中的每一项的情况下定位元素。 通过利用排序数据的特性和先进的分区策略,子线性查找算法减少了所需比较的次数,从而加速了查找过程。 这种效率使得它们在处理大型数据集时显得尤为重要,而线性查找方法在这种情况下会显得 过于缓慢。

子线性查找算法,如二分查找、跳跃查找和斐波那契查找,利用不同的 策略迅速减少查找空间。 例如,二分查找通过不断将数组一分为二,而跳跃查找则将数据划分为块并在这些较小的块中执行线性查找。 斐波那契查找利用斐波那契数列来确定查找空间的范围,优化适合内存块的数据。 这些算法各有独特的优势,适用于特定类型的问题,突出了子线性查找方法在计算效率方面的多样性和强大功能。 让我们深入探讨最著名的子线性 查找算法。

二分查找

二分查找 是一种 在排序数组中定位元素的有效方法。 该算法通过不断地 将查找空间一分为二 来工作。 如果中间元素与目标值匹配,则查找完成。 如果不匹配,查找将继续在目标值可能出现的数组一半中进行。 二分查找通常采用递归方式实现。 以下是二分查找的递归实现:

 def recursive_binary_search(a, target, left, right):
    if right >= left:
        mid = left + (right - left) // 2
        if a[mid] == target:
            return mid
        elif a[mid] > target:
            return recursive_binary_search(a, target, left, mid - 1)
        else:
            return recursive_binary_search(a, target, mid + 1, right)
    return -1

二分 查找 通过反复将查找区间一分为二来工作。 二分查找的时间复杂度是 O(logn)。让我们详细分析并证明这一时间复杂度。

给定一个排序数组 <st c="19618">a</st>,以及一个目标值 <st c="19641">target</st>,二分查找遵循以下步骤:

  1. 初始化:设置两个指针, left right,分别指向数组的开始和结束位置。

  2. 中间元素:计算中间索引, mid = left + (right - left) // 2

  3. 比较

    • 如果 a[mid] == target,目标已找到,返回 mid 索引

    • 如果 a[mid] < target,更新 left mid + 1 并重复 该过程

    • 如果 a[mid] > target,更新 right mid - 1 并重复 该过程

  4. 终止:该过程继续直到 left > right。如果没有找到目标, 返回 -1

初始搜索空间是整个大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>的数组。在每一步中,算法将目标与中间元素进行比较。 根据比较结果,搜索空间被二分;要么丢弃左半部分,要么丢弃右半部分:

  • 第一步之后,搜索空间 n2

  • 第二步之后,搜索空间 n4

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:mik</mml:mi></mml:mrow>mml:mrowmml:mit</mml:mi>mml:mih</mml:mi></mml:mrow></mml:msup></mml:math> 步后,搜索空间 变为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:msupmml:mrowmml:mn2</mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfrac></mml:math>

当搜索空间缩小到 1 个元素时,算法停止,即 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfrac>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math>。现在我们求解 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> n2k=1⟹n=2k⟹k=logn。因此,二分查找算法最多执行 logn 次比较。

我们也 可以 通过递归函数来证明时间复杂度。 我们定义 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 为二分查找在大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>的数组上的时间复杂度。 如果数组大小为 1 (n=1),那么时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> ,因为只需要进行一次比较: T(1)=O(1)

对于大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>的数组,我们进行一次比较以检查中间元素,然后递归地在左或右半部分进行查找,每一部分的大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:math>。然后,递归二分查找的递推函数为: 如下所示:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>.

使用分治法递归关系的主定理, 案例 2 适用。 因此, 以下适用:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

与线性查找不同,二分查找效率非常高,适用于大规模数据集。 该算法实现简单且易于理解。 迭代版本使用常数空间, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>,即使是递归版本,空间开销也相对较低, O(logn)。二分查找的主要用途是查找已排序数组或列表中的元素。 二分查找还用于字典操作,如在排序列表中查找单词。 的条目。

另一方面,二分查找仅适用于已排序的数组。 如果数据未排序,则必须先进行排序,这会增加额外的开销。 二分查找最适用于静态数组,其中数据不会频繁变化。 频繁的插入和删除操作会导致数组重新排序,从而增加排序开销。 此外,二分查找在链表或其他非连续内存结构上的效率较低,因为它依赖于数组提供的高效随机访问。 由数组提供的高效访问。

将数据集按中点划分的概念可以扩展到将数据集划分为多个区间。 三分查找就是这种方法的一个例子。 它是一种分治查找算法,作用于排序数组,通过将数组划分为三个部分,并确定目标元素所在的部分,从而将查找空间缩小到三分之一。 这一过程会一直重复,直到找到目标元素或查找空间耗尽。 因此,三分查找的时间复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:msubmml:mrowmml:mig</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msub>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

插值查找

插值查找 是在排序数组中查找目标值的二分查找算法的改进版。 而二分查找总是探测中间元素,插值查找 则根据数组边界的值和目标值本身,做出一个关于目标值可能位置的合理猜测。

以下是 插值查找的递归实现:

 def recursive_interpolation_search(a, target, low, high):
    if low <= high and target >= a[low] and target <= a[high]:
        pos = low + ((high - low) // (a[high] - a[low]) * (target - a[low]))
        if a[pos] == target:
            return pos
        if a[pos] < target:
            return recursive_interpolation_search(a, target, pos + 1, high)
        return recursive_interpolation_search(a, target, low, pos - 1)
    return -1  # Target not found

比较插值查找与二分查找的代码时,主要的区别在于每个算法计算中点(或位置)的方法。 这个差异反映了每个算法用来定位目标元素的不同方法。 在二分查找中,中点是左值和右值的平均值:

<st c="24192">mid = left + (right - left) // 2</st>

二分查找假设元素均匀分布,并将查找区间一分为二,不考虑元素的实际值。 而在插值查找中,中点(由 <st c="24427">pos</st>表示)是根据目标元素相对于当前低值和高值的关系来估算的:

<st c="24546">pos = low + ((high - low) // (arr[high] - a[low]) * (target -</st> <st c="24609">a[low]))</st>

插值查找尝试 通过考虑元素的分布来改进中点估算。 这使得它在数据均匀分布的情况下更高效,因为它通过跳跃更接近目标元素,从而潜在地减少了比较次数。

让我们来探讨一下 插值查找背后的直觉。 想象一下数组是一个数轴。 插值查找基于目标值在数组中的最小值和最大值之间的相对位置来估算目标值的位置。 当值均匀分布时,这个估算通常相当准确,导致与 二分查找相比更快的收敛速度。

我们知道,二分查找的时间复杂度是 O(logn)。现在的问题是:插值查找的时间复杂度是多少? 首先,我们需要找到描述插值查找算法行为的递归函数。 插值查找通过估算目标值在数组中的位置,然后递归或迭代地精细化这个估算值。 这个过程的效率在很大程度上依赖于数组中值的分布情况。 对于均匀分布的数据,估算的位置接近目标的实际位置,从而导致比较次数减少。 位置估算由以下公式给出: 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mip</mml:mi>mml:mio</mml:mi>mml:mis</mml:mi>mml:mo=</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:miw</mml:mi>mml:mo+</mml:mo><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrow<mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">h</mml:mi><mml:mi mathvariant="bold-italic">i</mml:mi><mml:mi mathvariant="bold-italic">g</mml:mi><mml:mi mathvariant="bold-italic">h</mml:mi>mml:mo-</mml:mo><mml:mi mathvariant="bold-italic">l</mml:mi><mml:mi mathvariant="bold-italic">o</mml:mi><mml:mi mathvariant="bold-italic">w</mml:mi></mml:mrow></mml:mfenced></mml:mrow>mml:mrow<mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">a</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrow<mml:mi mathvariant="bold-italic">h</mml:mi><mml:mi mathvariant="bold-italic">i</mml:mi><mml:mi mathvariant="bold-italic">g</mml:mi><mml:mi mathvariant="bold-italic">h</mml:mi></mml:mrow></mml:mfenced>mml:mo-</mml:mo><mml:mi mathvariant="bold-italic">a</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrow<mml:mi mathvariant="bold-italic">l</mml:mi><mml:mi mathvariant="bold-italic">o</mml:mi><mml:mi mathvariant="bold-italic">w</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo×</mml:mo><mml:mfenced separators="|">mml:mrowmml:mit</mml:mi>mml:mia</mml:mi>mml:mir</mml:mi>mml:mig</mml:mi>mml:mie</mml:mi>mml:mit</mml:mi>mml:mo-</mml:mo>mml:mia</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:miw</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math>

在每次迭代中,搜索区间的大小会根据估计位置按比例缩小。 平均而言,插值查找比二分查找更显著地减少了搜索空间。 然而,估计位置在很大程度上依赖于数据的分布。 在最坏的情况下,当数据分布高度偏斜时,时间复杂度为 T(n)=O(n),使得插值查找的效率不比 线性查找更高。

在一个更现实的平均情况中,当数据分布接近均匀时,插值查找表现得更好。 为了分析这一点,我们需要确定此情况下的递推函数。 对于均匀分布的数据,我们假设每次递归时问题规模都会减少为原问题规模的平方根。 这为插值查找在 平均情况中的递推关系提供了以下表达式:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrt<mml:mi mathvariant="bold-italic">n</mml:mi></mml:msqrt></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>

由于 递推函数不符合主定理的标准形式,我们需要使用替代方法,例如代入法,来 解决它。

我们进行一个变量变化: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo=</mml:mo>mml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mim</mml:mi></mml:mrow></mml:msup></mml:math>。然后我们可以将递推函数重写为 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mim</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:msup></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>

接下来,进行另一个变量的变换: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mim</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>. 现在递归关系变为 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miS</mml:mi><mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mim</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>

这是一个熟悉的递归函数(类似于二分查找),我们可以得出 如下结论:

S(m)=O(logm)

替换 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math> 并用 logn替代,我们 得到如下结果:

Sm=T2m=T2logn=Tn=Ologlogn

在最坏情况下,如果元素的分布高度倾斜或不均匀,位置估计可能不准确,导致线性搜索行为。 这导致最坏情况下的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>。另一方面,空间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 因为插值搜索只需在内存中保留 <st c="27836">pos</st> 变量,而这并不依赖于数据的大小。

插值搜索对于均匀分布的数据非常高效,但对于非均匀分布可能会退化为线性搜索性能。 这一分析表明,虽然插值搜索具有潜在的优势,但其效率高度依赖于 数据分布。

指数搜索

指数搜索,又称 跳跃搜索 倍增搜索,是 用于查找排序数组中可能包含目标值的区间的算法。 它通过 最初检查第一个元素,然后重复加倍间隔大小直到找到可能包含目标的区间。 一旦找到这个范围,就在其中使用二分搜索来定位 目标值。

指数搜索的时间复杂度是 O(logn) 在最坏情况和平均情况下均如此。 这种效率是由算法快速增加区间大小然后在 确定的范围内使用二分搜索所致。

指数搜索首先通过不断翻倍索引来找到目标元素可能所在的范围。 这一过程需要 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 时间,因为翻倍过程实际上对索引执行了二分查找。 一旦找到范围,在该范围内进行的二分查找再需要 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 时间。 然而,由于范围查找步骤已经显著减少了问题规模,整体时间复杂度 保持 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

指数搜索的空间复杂度取决于实现方法。 在迭代方法中,空间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>。以下是指数搜索的一个简单迭代实现: exponential search:

 def iterative_exponential_search(a, target):
    if a[0] == target:
        return 0
    n = len(a)
    i = 1
    while i < n and a[i] <= target:
        i = i * 2
    return binary_search(a, i // 2, min(i, n - 1), target)

让我们简要解释一下代码。 代码的关键部分是这一行 <st c="29784">while i < n and a[i] <= target:</st>,其中发生了指数增长: <st c="29854">i = i * 2</st>。在这一步骤中,索引翻倍(<st c="29898">1</st> <st c="29902">2</st> <st c="29905">4</st> <st c="29908">8</st>,以此类推),直到找到大于或等于目标值的元素。 一旦确定了这个范围, <st c="30012">binary_search</st> 将在该范围内执行,以定位 目标元素。

迭代实现使用恒定的额外空间,无论输入数组的大小如何。 唯一额外的内存需求是用于跟踪索引和 目标元素的几个变量。

与迭代方法不同,在递归方法中,空间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>。我们来看一下 递归实现:

 def recursive_exponential_search(a, target, i=1):
    n = len(a)
    if a[0] == target:
        return 0
    if i < n and a[i] <= target:
        return recursive_exponential_search(a, target, i * 2)
    return binary_search(a, i // 2, min(i, n - 1), target)

递归实现使用额外的空间来处理每次递归调用的调用栈。 由于递归的深度与 logn成比例,空间复杂度 O(logn)

指数搜索是一种有效的算法,用于快速缩小大规模排序数组中的搜索范围。 通过结合指数搜索和二分搜索的优点,它既高效又灵活。 其主要优势在于能够高效处理大数据集,尽管它要求数据必须排序,并且在其双阶段 搜索方法上引入了一定的复杂性。

跳跃搜索

跳跃搜索 是一种用于在排序数组中查找元素的算法。它的工作原理是将数组分成固定大小的块,通过块大小跳跃到前面,然后在可能包含目标元素的块内执行线性搜索。跳跃的最优步长通常是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msqrtmml:min</mml:mi></mml:msqrt></mml:math>,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 是数组中的元素数量。该方法的目的是通过最初跳过数组中的大部分部分来减少比较的次数。我们将证明跳跃搜索的时间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced></mml:math>。跳跃搜索的迭代实现如下:

 import math
def jump_search(a, target):
    n = len(a)
    step = int(math.sqrt(n))
    prev = 0
    while a[min(step, n) - 1] < target:
        prev = step
        step += int(math.sqrt(n))
        if prev >= n:
            return -1
    while a[prev] < target:
        prev += 1
        if prev == min(step, n):
            return -1
    if a[prev] == target:
        return prev
    return -1

由于算法的特性,跳跃搜索的递归实现不太常见。尽管如此,它仍可以按照以下方式实现:

 import math
def recursive_jump_search(a, target, prev=0, step=None):
    n = len(a)
    if step is None:
        step = int(math.sqrt(n))  # Block size to jump
    if prev >= n:
        return -1
    if a[min(step, n) - 1] < target:
        return recursive_jump_search(a, target, step, step + int(math.sqrt(n)))
    while prev < min(step, n) and a[prev] < target:
        prev += 1
    if prev < n and a[prev] == target:
        return prev
    return -1

我们来分析一下算法,然后估算跳跃搜索的时间复杂度。该算法分为三个步骤:

  1. 初始化

    • 设置块大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msqrtmml:min</mml:mi></mml:msqrt></mml:math>

    • 初始化 prev0step<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msqrtmml:min</mml:mi></mml:msqrt></mml:math>

  2. 跳跃阶段:按块大小跳跃,直到当前值大于或等于目标值或达到数组的末尾。

  3. 线性搜索阶段:在 识别的块内执行线性搜索。

现在,让我们分析时间复杂度。 我们知道每个块的大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msqrtmml:min</mml:mi></mml:msqrt></mml:math>。在最坏的情况下,我们可能需要遍历整个数组来找到包含目标元素的块。 到达目标块所需的跳跃次数为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mo(</mml:mo><mml:mfenced open="⌈" close="⌉" separators="|">mml:mrowmml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced>mml:mo=</mml:mo><mml:mfenced open="⌈" close="⌉" separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced>mml:mo)</mml:mo></mml:math>。每次跳跃都会进行一次比较,因此跳跃阶段的比较次数为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msqrtmml:min</mml:mi></mml:msqrt></mml:math>

在这一阶段, 我们准备开始线性搜索阶段。 在识别潜在块后,将在该块内执行线性搜索。 该块内的 最大搜索元素数为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msqrtmml:min</mml:mi></mml:msqrt></mml:math>。因此,线性搜索阶段的比较次数最多为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msqrtmml:min</mml:mi></mml:msqrt></mml:math>

我们有两个 后续阶段:

  • 跳跃阶段 比较: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced></mml:math>

  • 线性搜索阶段 比较: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced></mml:math>

我们将两个阶段加在一起;比较的总数为 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced></mml:math>

跳跃搜索算法的总时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:r></mml:mfenced></mml:math>。这是因为该算法在跳跃阶段最多执行 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msqrtmml:min</mml:mi></mml:msqrt></mml:math> 次比较,并在每个区块内的线性搜索阶段最多执行 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msqrtmml:min</mml:mi></mml:msqrt></mml:math> 次比较。 这些阶段的组合导致了总的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced></mml:math>。该分析表明,跳跃搜索的效率低于二分搜索, O(log n),但在实现简单性和常数空间复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 有优势的情况下,仍然具有一定的实用性。

跳跃搜索比线性搜索在大数组中更高效,因为它通过跳过一部分元素来减少比较次数。 跳跃搜索算法相对简单易懂,且与二分搜索不同,跳跃搜索除了确保数据已排序外,不需要任何数据预处理。

然而,跳跃搜索也有一些局限性。 像二分搜索一样,跳跃搜索只适用于排序数组。 此外,跳跃搜索的效率依赖于选择一个最优的块大小,通常是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msqrtmml:min</mml:mi></mml:msqrt></mml:math>,但在实际应用中,这可能并非总是最有效的。 对于非常小的数组,计算块大小并执行跳跃的开销可能使得跳跃搜索不如像 线性搜索 等更简单的算法高效。

跳跃搜索在大规模排序数组中非常有用,当二分搜索可能不太直观或数据是顺序访问时。 在数据库中,跳跃搜索可以用于高效地索引和查询排序数据。 跳跃搜索的另一个使用场景是在内存受限的环境中。 考虑到其 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 空间复杂度,跳跃搜索适合于内存受限的环境,在这些环境中,无法提供额外的空间来存储数据结构 ,如二叉搜索树或哈希表。

总之,跳跃搜索 是一种在排序数组中有效的搜索算法,既保持了线性搜索的简单性,又兼具了二分搜索的高效性。 它的主要优势在于其 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced></mml:math> 时间复杂度,使其适用于需要快速搜索排序数据的某些应用。

总结

所有子线性搜索算法都依赖于排序数据,这意味着如果数据尚未排序,它们必须先对数据进行排序。 当数据是动态变化时,这就构成了一个显著的限制。 我们将在 下一章详细讨论这个问题。

大多数亚线性搜索算法都是二分搜索的改进或扩展,跳跃查找是一个例外,它基于将数据分段并在每个段内进行查找。 二分搜索及其变种的主要思想是找到数组中最优的中位位置估计。 这将二分搜索与插值搜索和指数搜索区分开来。 另一方面,每种搜索算法的目标都是减少我们寻找目标的搜索空间或范围,并尽量减少遗漏目标的风险。 在所有二分搜索的变种中,都是通过一个或多个中位点来实现这一点,而在跳跃查找中,目标是通过均匀地划分 搜索空间来实现的。

如果我们将搜索空间(数据集)划分为多个分区,例如, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msqrtmml:min</mml:mi></mml:msqrt></mml:math>,我们指的是跳跃查找。 虽然它看起来与二分查找相似,后者只有一个中位点,但跳跃查找将数据集划分为多个分区,并使用多个中位点(分区数 = 中位点数 + 1)。 这一区别导致跳跃查找具有不同的时间复杂度模式,即 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced></mml:math>,而相比之下,二分查找及其变种的复杂度为 O(logn) 的复杂度。 及其变种。

一种有趣的方法是使用斐波那契数列来划分搜索空间。 这引出了斐波那契搜索。 斐波那契搜索是一种高效的搜索算法,适用于已排序的数组。 它利用斐波那契数的性质将数组划分为更小的部分,使其类似于二分查找和跳跃查找。 斐波那契搜索的主要优势是其在处理适合于 内存块的数组时的高效性。

哈希

在前两部分中,我们探讨了两组搜索算法:具有线性时间复杂度和更高效的次线性时间复杂度的算法。 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 线性搜索算法,例如简单的顺序搜索,具有 的时间复杂度,这使它们简单直接,但对于大数据集不够高效。 另一方面,次线性搜索算法,如二分搜索和跳跃搜索,提供显著更好的时间复杂度,通常为 O(logn) <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msqrtmml:min</mml:mi></mml:msqrt></mml:mrow></mml:mfenced></mml:math>通过利用排序数据的特性来最小化所需的比较次数。

然而,实现这种改进的时间复杂度并非没有代价:对数据进行排序需要时间。 排序是次线性搜索算法效率的先决条件。 没有经过排序的数据,无法实现次线性时间复杂度的理论优势。 排序过程本身可能会耗费时间,通常 Onlogn 尤其是对于像快速排序或归并排序这样的高效算法。 因此,虽然次线性搜索算法提供了更快的搜索时间,但仅当数据能够高效排序或数据相对静态时,排序步骤才能在多次搜索中分摊。 在设计和应用搜索算法时,排序时间与搜索效率之间的这种权衡是一个关键考虑因素。

现在的问题是:我们是否可以比亚线性搜索算法获得更好的性能? 具体来说,是否可以在常数时间内执行搜索,或者 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>?要回答这个问题,我们需要重新审视搜索操作的目标。 任何搜索算法的目标都是高效地找到关键字在数据结构中的索引或地址。 实现常数时间搜索的一种方法是通过一种称为哈希的技术。

在哈希中,我们使用一个哈希函数,该函数将目标关键字作为输入,并计算一个直接对应于数据结构中关键字位置的值。 这个值被称为哈希值(或哈希码),然后用于索引哈希表,允许对 数据进行常数时间访问。

在深入讨论哈希主题之前,让我们定义一些 基本术语:

  • 关键字:在搜索算法和数据结构的背景下,关键字是用于搜索、访问或管理集合中元素的唯一标识符,如数组、列表或数据库。 关键字对于高效的数据检索和操作至关重要。 例如,在字典中,关键字可以是一个单词,而相关联的值可以是该单词的定义。 关键字被用于各种数据结构,例如哈希表,其中它们被输入到哈希函数中以生成 一个索引。

  • 索引:在数据结构(如数组或列表)中,索引是位置的数值表示。 它指示了特定元素在结构中存储的位置。 例如,在数组 [10, 20, 30, 40]中,元素 30 的索引是 2。索引对于在支持随机访问的数据结构(如数组 和列表)中直接访问元素至关重要。

  • 地址:地址 是指存储数据元素的内存中特定位置。 在搜索和数据结构的上下文中,地址通常是指对应某个索引或键的实际内存位置。 在低级编程中,例如 C 或 C++,地址可能是像 0x7ffee44b8b60这样的值,表示变量的确切内存位置。 地址用于直接访问和操作存储在内存中的数据。 在高级编程中,地址通常会被抽象化,但理解地址对于优化性能和理解 内存管理至关重要。

在哈希表中,一个键通过哈希函数传递以生成一个索引。 然后,使用该索引来定位哈希表中对应的数据。 另一方面,在数组中,索引直接对应存储数据元素的内存地址。

哈希函数

哈希函数 是一个 数学函数,能够将输入的键转换为一个数值,称为 哈希值 。该哈希值随后映射到哈希表中的一个索引。 一个好的哈希函数能够将键均匀分布在哈希表中,以最小化碰撞,即两个或更多键哈希到同一个索引。 一个有效的哈希函数有几个 关键属性:

  • 确定性:哈希函数必须对相同的输入始终产生相同的输出(哈希值)。 这确保了数据检索 和验证等应用的可预测性和可靠性。

  • 固定输出大小:无论输入数据的大小如何,输出的哈希值应该具有固定长度。 这使得哈希值容易存储和比较,提升了 各种算法中的效率。

  • 效率:哈希函数应该是计算上快速的,即使对于大输入也能够快速生成哈希值。 这对于实时应用和依赖哈希的算法至关重要 以保证性能。

  • 均匀性:一个好的哈希函数会将其输出值均匀地分布在输出空间中。 即使输入发生微小变化,也应该产生显著不同的哈希值,避免出现模式,并使反向工程 输入变得困难。

  • 碰撞抗性:应该在计算上不可行找到两个不同的输入,产生相同的哈希值(即碰撞)。 碰撞抗性对于密码存储和 数字签名等安全应用至关重要。

还有一些额外的属性,对于加密哈希函数尤其重要(超出了本书的范围):

  • 预图像抗性:给定一个哈希值,应该很难找到产生它的原始输入。 这个属性可以防止试图从 哈希值中恢复原始数据的攻击。

  • 第二预图像抗性:给定一个输入及其哈希值,应该很难找到第二个输入,产生相同的哈希值。

  • 无关性:输入的不同部分与结果哈希值之间不应存在任何关联。

通过 对输入数据应用哈希函数,我们构建了一个哈希表。 哈希表是一种存储键值对的数据结构。 每个键都会通过哈希函数处理,生成一个哈希值,该值决定了存储相应值的索引。 哈希表的主要优点是,它使得查找、插入和删除操作可以在平均情况下以 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 时间复杂度进行。

哈希表的效率取决于哈希函数如何将键分布在表格中。 理想情况下,一个好的哈希函数会最小化空单元格的数量(使表格不那么稀疏),并减少碰撞的数量(即多个键哈希到相同的索引)。 设计一个好的哈希函数的艺术在于在这些因素之间找到平衡,以确保 最佳性能。

同时,必须注意,在为特定算法选择哈希函数时,考虑应用场景非常重要。 不同的应用可能需要不同的特性。 例如,密码学应用需要强大的碰撞抗性,而数据结构可能更注重速度。

常数时间查找通过哈希技术

哈希的主要目标是 通过使用哈希函数将键直接映射到哈希表中的位置,从而实现常数时间查找。 最直接的哈希函数是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mih</mml:mi>mml:mo(</mml:mo>mml:mik</mml:mi>mml:mie</mml:mi>mml:miy</mml:mi>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:mik</mml:mi>mml:mie</mml:mi>mml:miy</mml:mi></mml:math>,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mih</mml:mi></mml:math> 是哈希函数。 这种方法被称为直接寻址,得到的哈希表被称为 直接寻址表。

虽然直接寻址简单易懂,但它也有若干 显著的局限性:

  • 稀疏哈希表:直接寻址通常会创建一个非常稀疏的哈希表,这意味着哈希表的大小必须与可能的输入键的范围一样大。 例如,如果输入键的范围是从 1 到 1,000,000,那么哈希表必须有 1,000,000 个槽位,即使实际上只使用了几个键。 这会导致内存的低效使用。

  • 高碰撞概率:在直接寻址中,如果两个不同的键映射到相同的位置(碰撞),可能会导致数据检索和插入问题。 尽管直接寻址假设每个键是唯一的,但在实际应用中,碰撞是 常常不可避免的。

  • 仅限于数值数据:直接寻址仅对数值型整数数据有效。 它不适用于其他数据类型,如字符串或复合对象,因此限制了其在许多 实际场景中的应用。

使用哈希实现常数时间搜索涉及两个关键步骤。 首先,必须设计一个高效的哈希函数,尽可能满足许多期望的属性。 尽管努力创建一个好的哈希函数,但碰撞是不可避免的。 因此,第二步是在数据结构操作中有效地处理碰撞,包括搜索 和检索。

搜索中使用的哈希函数类型

哈希函数在决定基于哈希的搜索算法效率中至关重要。 让我们来探讨一些常用的哈希函数,并附上解释 和示例。

除法余数(模)方法

除法余数(模)方法 是一种直观且常用的生成哈希值的技术。 在该方法中,哈希值是通过将键值除以哈希表大小后的余数来获得的。 所使用的公式如下: 公式如下:

h(key)=keymodm

这里, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mih</mml:mi></mml:math> 是哈希函数, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi>mml:mie</mml:mi>mml:miy</mml:mi></mml:math> 是输入数据,而 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math> 是哈希表的大小。 我们来看一个 示例,演示这种方法如何工作。

示例 7.1

使用除法余数哈希方法,我们为示例键值 987654321 计算哈希值,哈希表的大小为 100

让我们一步一步地确定哈希值: 逐步进行:

  1. 应用模运算:通过对键取模哈希表大小来计算哈希值:

    h(987654321)=987654321mod100

  2. 计算余数:执行除法并找到 余数:

    987654321mod100=21

因此,键的哈希值 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn987654321</mml:mn></mml:math> 在哈希表大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn100</mml:mn></mml:math> 时为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn21</mml:mn></mml:math>

模运算简单易行,使得此方法既简单又高效。 它在计算上也非常高效,因为模运算的速度相对较快。 此外,相同的输入键始终会产生相同的哈希值,确保了 哈希表的一致性。

然而, 模运算方法也有其局限性。 如果哈希表的大小选择不当(例如,选择了 2 的幂),则哈希值可能不会均匀分布,导致聚集现象。 为了解决这个问题,通常建议选择质数作为表的大小。 此外,如果输入的键具有某种模式或共同的因子,使用此方法可能会导致碰撞和聚集,从而降低哈希表的效率。 像所有哈希函数一样,除法余数法也可能会发生碰撞。 因此,必须采取有效的碰撞处理策略,例如链式法或开放定址法,来 保持性能。

除法余数(模)方法是一种简单且高效的哈希函数,广泛应用于各种场景。 它通过将键与哈希表大小取余来生成哈希值。 尽管它提供了简单性和计算效率,但选择合适的表大小(最好是素数)对于确保哈希值的均匀分布非常重要。 此外,处理冲突的机制对于解决此方法的固有局限性至关重要。

乘法方法

乘法哈希法 是一种通过将键与常数分数相乘,并提取结果的适当部分来生成哈希值的技术。 此方法旨在将键更均匀地分布到哈希表中。 我们一步步地解释这个方法:

  1. 将键与常数相乘, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi></mml:math> 其中 0<A<1

  2. 提取 乘积的小数部分。

  3. 将小数部分与哈希表的 大小 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math>相乘。

  4. 取结果的下限值来获得 哈希值。

哈希函数的公式如下: 所示:

h(key)=⌊m(key⋅Amod1)⌋

这里, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi>mml:mo=</mml:mo>mml:mn0.618033</mml:mn></mml:math> (黄金比例的近似值)。 让我们通过 一个例子来看看这个方法。

例子 7.2

使用 乘法 哈希法,我们确定了示例键的哈希值, 123456,以及哈希表大小 100

让我们一步步确定哈希值:

  1. 将键与 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi></mml:math>相乘: 123456×0.618033=76293.192648

  2. 提取小数部分: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn0.192648</mml:mn></mml:math>

  3. 通过表大小相乘: 0.192648×100=19.2648

  4. 地板: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mfenced open="⌊" close="⌋" separators="|">mml:mrowmml:mn19.2648</mml:mn></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn19</mml:mn></mml:math>

因此,键 123456 在哈希表大小为 100 时的哈希值为 19

乘法法则倾向于将键更均匀地分布到哈希表中,从而减少聚集。 此外,与除法法则不同,乘法法则的有效性不那么依赖于表大小是素数。 而且,乘法和取模运算是 计算上高效的。

然而,乘法方法有一些局限性。 它的有效性很大程度上依赖于常数的选择 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi></mml:math>。虽然黄金比例通常被使用,但其他值可能需要进行测试,以获得最佳性能。 对于非常大的密钥或高度精确的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi></mml:math>,浮点运算中的精度问题可能会影响哈希值。 最后,处理浮点运算的需求可能使得该方法的实现比 除法方法 稍显复杂。

中平方方法

中平方哈希函数 是一种通过平方密钥然后从结果中提取适当数量的中间数字或位来生成哈希值的技术。 该方法旨在通过利用平方的性质将密钥更均匀地分布到哈希表中。 让我们通过一个示例来说明中平方 哈希函数。

示例 7.3

使用中平方 哈希方法,我们为示例密钥 456 计算哈希值,哈希表大小为 100*

  1. 平方密钥:平方密钥以获得一个较大的 数字: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:mn456</mml:mn></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mo=</mml:mo>mml:mn207936</mml:mn></mml:math>

  2. 提取中间数字:从平方值中提取适当数量的中间数字。 提取的数字数量可以根据哈希表的大小而有所不同。 为了简单起见,我们提取两个 中间数字:

    • 平方值: 207936

    • 中间数字: 07 (来自平方数的中间部分)

  3. 使用中间数字作为哈希值:使用这些中间数字来确定哈希表中的索引。 因此,密钥 456 被映射到哈希表中的索引 07

中平方哈希函数的 关键特性如下:

  • 均匀分布:通过对密钥进行平方并提取中间的数字,这种方法倾向于产生更均匀的密钥分布,因为平方有助于将 数值分散开来。

  • 简洁性:中平方方法实现起来十分简单。 它涉及对密钥进行平方,然后提取结果的中间部分。

  • 独立于密钥大小:该方法相对独立于密钥的大小,适用于各种 密钥长度。

另一方面,中平方方法存在一些限制:

  • 依赖于中间数字:该方法的效率依赖于平方值的中间数字。 如果中间数字分布不均,可能导致 聚集。

  • 选择数字:决定提取多少中间数字可能是一个挑战,并且可能需要实验来优化 特定应用。

  • 有限的密钥范围:对于非常小的密钥,平方后的值可能无法提供足够的数字来提取,从而降低该方法的有效性。

  • 计算成本:对非常大的密钥进行平方计算可能在计算上开销较大,特别是在处理能力有限的环境中。

中平方哈希函数 是一种有效的生成哈希码的方法,通过对密钥进行平方并提取中间的数字。 该技术利用平方的特性来分散数值,并在哈希表中实现更均匀的密钥分布。 尽管它简单且与密钥大小无关,但该方法的效率取决于中间数字的分布,并且对于大密钥可能涉及一定的计算成本。 总体而言,中平方哈希函数仍然是设计哈希函数的有用工具,适用于 各种应用。

折叠方法

折叠哈希函数是一种通过将键拆分成多个部分,将这些部分相加,然后对表大小取模生成哈希值的技术。 这种方法对于大键(例如电话号码或身份证号码)特别有用,旨在将键更均匀地分布到哈希表中。 我们可以考虑一个实际的例子来说明折叠 哈希函数。

例子 7.4

使用折叠 哈希方法,我们可以为示例键 987654321 计算哈希值,哈希表大小为 100

  1. 拆分键:将键分成相等的部分。 为了简单起见,我们将其拆分为每组三位数: 987,654,321

  2. 将部分相加:将各部分相加: 987+654+321=1962

  3. 取模:将总和对表的大小取模得到哈希值 代码: 1962mod100=621962mod100=62

因此,键 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn987654321</mml:mn></mml:math> 映射到哈希表的索引 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn62</mml:mn></mml:math> 位置。

折叠 哈希方法具有以下特点:

  • 均匀分布:折叠方法旨在通过确保密钥的所有部分都对哈希值做出贡献,从而产生更均匀的密钥分布。 这有助于减少聚集现象,并提高哈希表的整体性能。

  • 简洁性:该算法实现和理解都非常直接。 它仅涉及拆分、求和和取模运算。

  • 灵活性:它可以通过调整键的拆分方式,处理各种大小的键。

另一方面,折叠 哈希方法也有 其局限性:

  • 依赖于键结构:折叠哈希函数的效率取决于键的结构。 如果键的各部分具有相似的模式或值,可能无法均匀分布这些 键值。

  • 不适合小键值:对于小键值,拆分和求和的开销可能比简单的哈希函数(如除法余数法)提供的益处要小。

  • 处理不同长度:如果键值长度不同,可能很难决定如何均匀拆分它们,这可能导致 分布不均。

  • 求和溢出:对于非常大的键值,部分的总和可能超过典型的整数范围,导致溢出问题。 不过,这可以通过在每一步使用模运算来缓解。

折叠哈希函数是一个高效且直接的方法,用于哈希大键值。 通过 将键拆分成多个部分,对这些部分求和,然后取模运算,可以产生相对均匀的哈希值分布,从而提高哈希表的性能。 然而,其效率可能会受到键的结构、需要处理不同键长度以及潜在溢出问题的影响。 尽管存在这些局限性,折叠方法依然是设计哈希函数的重要工具,广泛应用于 各种场景。

通用哈希

通用哈希方法是一种旨在 最小化哈希表碰撞概率的技术。 它通过使用一组哈希函数,并随机选择其中一个进行哈希计算。 该方法提供了一种概率性保证,确保碰撞次数较低,因此在安全性和性能要求较高的应用中尤其有用。 以下是通用方法的逐步实现: 通用方法的步骤:

  • 定义一组哈希函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">H</mml:mi></mml:math> 从中选择一个特定的函数 h∈H 来使用。 每个哈希函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">h</mml:mi></mml:math> 应该能够在哈希表中均匀地映射键。

  • 随机选择一个哈希函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">h</mml:mi></mml:math> 从这组函数中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">H</mml:mi></mml:math> 选择一个用于哈希 键。

  • 使用选定的哈希函数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">h</mml:mi></mml:math> 计算 键的哈希值。

示例 7.5

使用折叠 哈希方法,计算示例键 123456 的哈希值,哈希表大小为 100

让我们 计算 ha,bx=ax+bmodpmodm.

这里,p 是一个比任何可能的键都大的质数,a <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi></mml:math> 是随机选取的整数,使得 1≤a<p 0≤b<p,且m 哈希表的大小。

现在,我们在 这个例子中使用以下参数:

  • 键: 123456

  • 表格大小 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math> 100

  • 质数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mip</mml:mi></mml:math> 101

  • 随机选择的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mia</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi></mml:math>:假设 a=34 b=7

  • 我们计算 哈希值:

    h34,7123456=34×123456+7mod101mod100

  • 首先,我们计算 中间值:

    34×123456+7=4197503

    97503mod101=90

  • 然后,我们计算最终的哈希 值: 90mod100=90 因此,选定参数下,键 123456 的哈希值为 90

通用哈希 显著降低了 碰撞的概率,提供了强大的概率保障,确保不同的键值会映射到不同的哈希值。 通过从哈希函数族中随机选择哈希函数,它还具有抵抗敌对攻击的能力,使其在密码学应用中非常有用。 此外,这种方法通过选择合适的哈希函数族,可以适应不同的键分布。

但是,通用哈希方法存在一些局限性。 定义和实现一组哈希函数比简单的方法如除法-余数或乘法更复杂。 此外,需要随机选择哈希函数并计算可能更复杂的哈希值,可能会增加额外的计算开销。 最后,通用哈希方法的有效性依赖于所选择的哈希函数的真实随机性,在某些环境中可能难以实现。 某些环境中。

接下来的两种方法是为了对字符串数据进行哈希处理,其中字符串中的字符会影响哈希码。 哈希码。

多项式哈希用于字符串

多项式哈希 (也被称为 拉宾-卡普滚动哈希) 是 一种 高效 计算 大字符串内子串哈希值的技术。 它将字符串的字符视为多项式的系数,其中每个字符的 ASCII(或 Unicode)值被一个与其在字符串中位置相对应的素数幂所乘。 在多项式哈希方法中,字符串中的每个字符都视为多项式的系数。 哈希码是在某个值处计算的多项式值。 对于字符串 abcd 和选择的基数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn31</mml:mn></mml:math>,哈希码可以计算如下:

habcd=a×313+b×312+c×31+dmodm

多项式哈希的实现步骤如下:

  1. 初始化:选择一个素数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mip</mml:mi></mml:math> (通常是 11 或 31) 和一个模数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math> (通常是一个大素数,以最小化哈希冲突,并且与 哈希表的大小相对应).

  2. 哈希计算

    1. 初始化哈希值 0

    2. 遍历字符串中的每个字符。

    3. 对于每个字符,执行 以下操作:

    • 将当前哈希值 乘以 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mip</mml:mi></mml:math>

    • 加上 字符的 ASCII 值。

    • 对结果应用模运算 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math> ,确保哈希值保持在一个 可管理的范围内。

  3. 滚动哈希:要计算子字符串的哈希值,我们可以减去已经不在子字符串中的字符的哈希值,并加上新包含的字符的哈希值。 这种“滚动”更新非常高效,可以快速比较 不同的子字符串。

以下是 一个使用 多项式哈希的 Python 示例:

 def polynomial_hash(string, p=11, m=2**31):
  hash_value = 0
  for char in string:
    hash_value = (hash_value * p + ord(char)) % m
  return hash_value
# Example usage
string = "Hello"
hash_value = polynomial_hash(string)
print(f"The polynomial hash value of '{string}' is: {hash_value}")

让我们来解释 这个示例 Python 代码:

  • polynomial_hash 函数接受一个字符串作为输入,并可以带有可选参数, p (素数) 和 m (模数)

  • 它将 hash_value 初始化为 0

  • 它遍历每个字符(char)在 字符串中

  • 对于每个字符,执行哈希 更新计算:

    • hash_value * p 有效地将前一个字符在 多项式中向左移动一个位置

    • ord(char) 获取 字符 的 ASCII 值

    • 结果通过取模 m 来防止溢出,并确保 哈希值 的范围一致

    • 最后,它返回计算得到的 哈希值

    • 字符串 "Hello" 的多项式哈希值是 99162322

多项式哈希在 比较大字符串中的子字符串时尤其高效。 它避免了为每个子字符串重新计算整个哈希值,使其适用于抄袭检测或模式匹配等应用。 冲突仍然是可能的,特别是当模数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math> 没有仔细选择时。 使用一个大质数作为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math> 可以帮助减少冲突的频率。 质数的选择 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mip</mml:mi></mml:math> 和模数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math> 会影响性能和冲突概率。 可能需要进行实验以找到适用于 特定应用的最佳值。

DJB2 哈希函数用于字符串

DJB2 哈希函数 是一种 简单且有效的算法 用于从字符串生成哈希值(数值表示)。 它以其速度和良好的哈希值分布而闻名,使其适用于各种应用,如哈希表。 以下是此 哈希函数的四个步骤:

  1. 初始化:哈希值的初始值设为 5381。这个初始值的选择有些任意,但在实际应用中证明效果良好。 实践中已被证明有效。

  2. 迭代:该函数遍历输入字符串中的每个字符。

  3. 哈希更新:对于每个字符,当前的哈希值会被乘以 33(左移 5 位后再加到自身)。 该字符的 ASCII 值会被加到 哈希值中。

  4. 最终化:在处理完所有字符后,哈希值通常会进行掩码处理,以确保其适合 32 位无符号 整数范围。

以下是该 哈希函数的 Python 示例:

 def djb2(string):
  hash = 5381
  for char in string:
    hash = ((hash << 5) + hash) + ord(char)
  return hash & 0xFFFFFFFF

<st c="64094">djb2</st> 函数以字符串作为输入。 它将哈希变量初始化为 <st c="64169">5381</st>。它遍历字符串中的每个字符(<st c="64208">char</st>)。 对于每个字符,它执行哈希更新计算: <st c="64292">(hash << 5) + hash</st> 等价于将 <st c="64340">hash</st> 乘以 <st c="64348">33</st>

<st c="64351">ord(char)</st> 获取 字符的 ASCII 值。 最后,函数返回掩码后的哈希值,该值会被处理为 32 位无符号整数。 例如,如果 <st c="64499">string = "Hello"</st>,则 DJB2 哈希函数的输出为 <st c="64550">99162322</st>

DJB2 是 相对容易理解和实现的。 它计算速度较快,这在性能关键的场景中具有优势。 虽然 DJB2 通常表现良好,但并不完美,碰撞仍然可能发生,特别是在处理非常 大的数据集时。

碰撞处理

使用哈希函数的主要挑战之一是处理冲突。 冲突 发生在两个不同的 键值产生相同的哈希值时,从而导致数据检索和存储上的潜在冲突。 有效处理冲突对于保持哈希表的性能和可靠性至关重要。 有多种策略可用于解决此问题,如链式法和开放地址法,但每种方法都有其自身的权衡和复杂性。 理解和缓解冲突问题对于设计健壮且高效的哈希数据结构至关重要。 以下是主要的冲突处理技术,每种技术都有示例、局限性 和属性。

链式法

在链式法中,哈希表中的每个位置都指向一个链表(或链) ,该链表包含哈希到相同索引的元素。 当发生冲突时,新元素将简单地添加到 链表的末尾。

示例 7.6

在这里,我们正在 使用链式法处理哈希表的冲突 ,哈希表的大小为 10 ,键值为 12 22 32 (所有这些都生成哈希值 2 ,其公式为 h(k)=kmod10)。

索引 哈希表
0
1
2 12 à 22à 32
3
4
5
6
7
8
9

表 7.1 – 使用链式法处理冲突的示例

链式结构 实现起来相对简单,并且支持动态调整大小,允许列表在添加更多元素时增长。 然而,这种方法需要额外的内存来存储链表。 此外,如果发生许多冲突,链条可能会变长,从而导致 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 最坏情况下的时间复杂度。 在平均情况下,插入、删除和查找操作的运行时间是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 如果链条较短的话。 为了更精确的分析,必须引入 负载因子。

负载因子,通常表示为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">λ</mml:mi></mml:math>,是哈希表中一个重要的概念,包括那些使用链式处理冲突的哈希表。 它衡量了哈希表的填充程度,定义为哈希表中元素的数量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 与表中槽位数量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math> 之比:

λ=nm

负载因子有助于理解哈希表的性能。 低负载因子表示表中有很多空槽,导致较短的链条和更快的平均查找时间。 而高负载因子则表示链条较长,可能导致更慢的 查找时间。

重新审视基本操作(插入、删除和搜索)的运行时间,使用 链式法来处理碰撞时,平均运行时间为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn>mml:mo+</mml:mo><mml:mi mathvariant="normal">λ</mml:mi></mml:mrow></mml:mfenced></mml:math> 这包括访问哈希表槽位的常数时间和 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">λ</mml:mi></mml:math> 扫描链表的时间,链表的平均大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">λ</mml:mi></mml:math>,假设数据均匀分布在槽位中,形成大小均匀的链表。

在许多实现中,负载因子被用作判断何时调整哈希表大小的阈值。 当负载因子超过某个值时,哈希表会进行调整(通常是加倍大小),以保持 高效的操作。

示例 7.7

考虑一个有 10 个槽位(大小为 m=10)的哈希表,和以下的键值: 12, 22, 32, 42, 和 52。我们使用一个简单的 哈希函数:

h(key)=keymod10:

哈希值为 2, 2, 2, 2, 和 2。所有键值都存储在 索引 2的链表中:

索引 哈希表
0
1
2 12 à 22à 32à42à 52
3
4
5
6
7
8
9

表 7.2 – 使用简单链式法处理碰撞的示例

  • 元素数量: n=5

  • 槽位数: m=10

  • 负载因子: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">λ</mml:mi>mml:mo=</mml:mo>mml:mfracmml:mrowmml:mn5</mml:mn></mml:mrow>mml:mrowmml:mn10</mml:mn></mml:mrow></mml:mfrac>mml:mo=</mml:mo>mml:mn0.5</mml:mn></mml:math>

负载因子的 特性 如下:

  • 较低的负载因子通常意味着更高的效率,因为链条较短,搜索时间 更快

  • 较高的负载因子表示更好的内存利用率,因为更多的槽位被使用,但也可能由于 更长的链条而导致性能下降

  • 维持一个最优的负载因子(通常低于 0.75)通常需要动态调整哈希表的大小,以平衡性能和 内存利用率

然而,负载因子 有其限制:

  • 随着负载因子的增加,每个索引处的链条变得更长,从而导致搜索、插入和 删除时间增加

  • 较高的负载因子增加了碰撞的可能性,这可能会降低哈希表的性能

  • 频繁调整大小以维持最优负载因子可能会带来开销,并影响调整大小操作期间的性能

许多哈希表实现会设置一个阈值负载因子(例如, 0.75)。 当负载因子超过该阈值时,表会被重新调整大小。 在调整大小时,一个常见的策略是将哈希表的大小加倍,并将所有现有元素重新哈希到 新表中。

开放寻址

开放地址法 通过使用系统化的探测序列直接处理哈希表中的冲突,以为每个键找到一个空槽。 当发生冲突时,算法根据特定策略探测表格,直到找到一个空槽。 在这里,我们介绍三种常见的开放地址法——线性探测法、二次探测法和 双重哈希法:

  • 线性探测法:当 发生冲突时,线性探测法检查表中的下一个槽,继续这一过程,直到找到一个空槽。

    示例 7.8

    为了 使用线性探测法处理冲突,针对一个 大小为 10 的哈希表和键 12, 13, 22, 和 32, 我们使用哈希函数 h(k)=kmod10。这些键的哈希值分别为 2, 3, 2, 和 2

  1. 计算 哈希值:

    • 对于键 12 12mod10= 2

    • 对于键 13 13mod10=3

    • 对于键 22 22mod10=2

    • 对于键 32 32mod10=2

  2. 使用 线性探测处理冲突:

    • 12 哈希到索引 2,因此它被放置在 索引 2

    • 13 哈希到索引 3,因此它被放置在 索引 3

    • 22 哈希到索引 2,此位置已被 12占用。使用线性探测,我们检查下一个槽(索引 3),该槽已被 13占用。然后我们检查下一个槽(索引 4),该槽为空,因此 22 被放置在 索引 4

    • 32 哈希到索引 2,此位置已被 12占用。使用线性探测,我们检查下一个槽(索引 3,该槽已被 13占用),以及下一个槽(索引 4,该槽已被 22占用)。 下一个空槽位位于索引 5,因此 32 被放置在 索引 5

  3. 这是结果的 哈希表:

索引 哈希表
0
1
2 12
3 13
4 22
5 32
6
7
8
9

表 7.3 – 使用线性探测处理冲突的示例

这个例子 展示了线性探测如何通过顺序检查下一个可用槽来解决哈希冲突 的过程。

线性探测 简单易行,易于实现。 它访问连续的内存位置,使得它对缓存友好。 然而,它可能导致主要集群,其中填充的槽位形成群组,导致较长的探测序列。 在最坏情况下,其运行时间会恶化为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 随着表 填充。

  • 二次探测:这种方法类似于线性探测,但使用二次函数来确定下一个槽位。 探测序列是 hk=hk+i2modm,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math> 是探测次数。

    例子 7.9

    为了处理哈希表大小为 10 和关键字 12 13 22 32的碰撞,我们使用哈希函数 h(k)=kmod10。这些关键字的哈希值分别为 2 3 2 2

  1. 计算 哈希值:

    • 对于关键字 12: 12mod10=2

    • 对于键 13: 13mod10=3

    • 对于键 22: 22mod10=2

    • 对于键 32: 32mod10=2

  2. 处理冲突 使用 二次探测法:

    • 键 12 哈希到索引 2,因此它被放置在 索引 2

    • 键 13 哈希到索引 3,因此它被放置在 索引 3

    • 键 22 哈希到索引 2,该位置已经被 12 占用。 使用二次探测法,我们检查下一个槽位 如下:

      h22,1=2+12mod10=3,该位置被 13 占用

      h22,2=2+22mod10=6,该位置为空,因此 22 被放置在 索引 6

    • 键 32 哈希到索引 2,该位置被 12 占用。 使用二次探测法,我们检查下一个槽位 如下:

      h32,1=2+12mod10=3,该位置被 13 占用

      h32,2=2+22mod10=6,该位置被 22

      h32,3=2+32mod10=1,该位置为空,因此 32 被放置在 索引 1

  3. 这是得到的 哈希表:

索引 哈希表
0
1 32
2 12
3 13
4
5
6 22
7
8
9

表 7.4 – 使用二次探测处理碰撞的示例

这个例子 演示了二次探测如何通过检查逐渐远离的槽位来解决碰撞,使用二次函数来确定 探测序列。

二次探测中,主聚集的可能性相比线性探测有所降低。 然而,它的访问模式较不利于缓存。 虽然二次探测缓解了主聚集,但仍可能出现次聚集,若多个元素哈希到相同的初始索引,且按照相同的探测序列进行探测。 此外,相比于 线性探测,二次探测实现起来更加复杂。

  • 双重哈希:这个方法 使用一个辅助哈希函数来确定探测序列,从而进一步减少聚集。 探测序列如下:

    hkey,i=h1key+i⋅h2keymodm

    例 7.10

    要使用双重哈希处理一个大小为 10 的哈希表,并且键为 12、13、22 和 32,我们使用 主哈希函数 h1key=keymod10。这些键的哈希值分别是 2、3、2 和 2。 我们还使用了一个次级哈希函数, h2(key) = 1 + (key mod 9),用于确定 探测序列:

  1. 计算主 哈希值:

    • 对于键 12: 12mod10=2

    • 对于键 13: 13mod10=3

    • 对于键 22: 22mod10=2

    • 对于键 32: 32mod10=2

  2. 使用 双重哈希处理冲突:

    • 键 12 哈希到索引 2,因此它被放置在 索引 2。

    • 键 13 哈希到索引 3,因此它被放置在 索引 3。

    • 键值 22 哈希到索引 2,已经被 12 占用。 使用二次哈希函数,我们计算出 探测序列:

      二次哈希 对于 22:

      h_2(22)=1+(22mod9)=1+4=5

      探测序列:

      h22,i=h122+i⋅h222mod10

      第一次探测: 2+1×5mod10=7,该位置为空,因此 22 被放置在 索引 7

    • 键值 32 哈希到索引 2,已经被 12 占用。 使用二次哈希函数,我们计算出 探测序列:

      二次哈希 对于 32:

      h_2(32)=1+(32mod9)=1+5=6

      探测序列:

      h32,i=h132+i⋅h232mod10

      第一次探测: 2+1×6mod10=8,该位置为空,因此 32 被放置在 索引 8

  3. 这是 结果 哈希表:

索引 哈希表
0
1
2 12
3 13
4
5
6
7 22
8 32
9

表格 7.5 – 使用双重哈希处理碰撞的示例

这个示例 演示了双重哈希如何通过使用次级哈希函数来确定探测序列,从而解决碰撞问题,减少聚集的可能性

双重哈希的主要 优势是它显著减少了主聚集和次聚集。 它保持 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 插入、删除和搜索操作的平均时间复杂度。 然而,由于需要使用两个哈希函数,双重哈希的实现更为复杂。 此外,由于非连续的 内存访问,它可能效率较低,特别是在缓存方面。

布谷鸟哈希 – 吸取鸟类碰撞解决方法的灵感

布谷鸟哈希 是一种 独特的开放寻址方案 用于处理哈希表中的碰撞,灵感来源于 布谷鸟的行为。 就像布谷鸟的雏鸟将其他鸟蛋推出巢外,布谷鸟哈希允许一个新键“踢出”一个已存在的键,为自己腾出空间。 让我们一步一步来解释它是如何工作的: 接下来:

  1. 两个哈希函数:布谷鸟哈希采用两个独立的哈希函数(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mih</mml:mi>mml:mn1</mml:mn></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mih</mml:mi>mml:mn2</mml:mn></mml:math>),它们将键映射到哈希表中的槽位。 这使得每个键有两个潜在的位置可以 存储。

  2. 插入:当插入一个键时,算法首先尝试将其放置在由 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mih</mml:mi>mml:mn1</mml:mn>mml:mo(</mml:mo>mml:mik</mml:mi>mml:mie</mml:mi>mml:miy</mml:mi>mml:mo)</mml:mo></mml:math>:确定的槽位

    • 如果槽位为空,键 将被插入。

    • 如果该槽位已被占用,现有的键将被“踢出”,新键将取而代之。

    被“踢出的”键然后会尝试插入到其备用位置,该位置由 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mih</mml:mi>mml:mn2</mml:mn>mml:mo(</mml:mo>mml:mik</mml:mi>mml:mie</mml:mi>mml:miy</mml:mi>mml:mo)</mml:mo></mml:math>确定。 该过程会持续进行,可能会继续踢出更多的键,直到找到一个空槽或达到最大踢出次数 为止。

  3. 查找:为了查找一个键,算法会检查两个可能的位置(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mih</mml:mi>mml:mn1</mml:mn>mml:mo(</mml:mo>mml:mik</mml:mi>mml:mie</mml:mi>mml:miy</mml:mi>mml:mo)</mml:mo></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mih</mml:mi>mml:mn2</mml:mn>mml:mo(</mml:mo>mml:mik</mml:mi>mml:mie</mml:mi>mml:miy</mml:mi>mml:mo)</mml:mo></mml:math>)。 如果在任一位置找到该键,查找 就成功了。

  4. 删除: 删除一个键是直接的;只需从找到它的槽中移除即可。

让我们在下面的例子中说明这种哈希方法。

示例 7.11

我们来处理碰撞 ,使用布谷鸟哈希算法来处理大小为 10 的哈希表,包含键 12、13、22 和 32:

  1. 我们使用以下 哈希函数:

    • h1key=keymod10

    • h2key=key/10mod10 (‘/’ 是 整数除法)

  2. 插入步骤:

    • 键 12:

      哈希 值: h112=12mod10=2

      将 12 放入表格 1 的 索引 2 处。

    • 键 13:

      哈希 值: h113=13mod10=3

      将 13 放入表格 1 的 索引 3 处。

    • 键 22:

      哈希 值: h122=22mod10=2.

      表格 1 中的索引 2 已被 12 占用。 将 12 踢出并将 22 放入 表格 1

    • 索引 2。

      重新插入 12,使用 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mih</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math> h212=12/10mod10=1.

      将 12 放入表 2 的 索引 1 处。

    • 键 32:

      哈希 值: h132=32mod10=2.

      表 1 中的索引 2 被 22 占据。 踢出 22 并将 32 放入表 1 中的

    • 索引 2。

      重新插入 22,使用 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mih</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math> h222=22/10mod10=2.

      表 2 中的索引 2 为空,因此将 22 放入表 2 的 索引 2 处。

  3. 以下是 结果 哈希表:

索引 哈希 表(1) 索引 哈希 表(2)
0 0
1 1 12
2 32 2 22
3 13 3
4 4
5 5
6 6
7 7
8 8
9 9

表 7.6 – 使用布谷鸟哈希处理碰撞的示例

布谷鸟哈希 提供了恒定的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 最坏情况查找时间,这相比于其他碰撞解决技术(如线性探测或链式哈希)是一个显著的优势。 它还通过确保每个键只能位于两个可能的位置之一来消除聚集现象。 然而,布谷鸟哈希需要相对较低的负载因子才能有效运行,这意味着相比于其他方法,表格可能需要更大。 此外,如果表格过于满或检测到循环,需要进行重新哈希操作,这可能会 带来高昂的成本。

布谷鸟哈希是当需要在高负载下进行极其快速的查找和删除时的绝佳选择。 然而,重要的是要意识到关于负载因子的限制以及重新哈希的潜在开销。

总结

哈希具有多个优点,最显著的是它的速度。 哈希提供了平均情况的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 查找、插入和删除操作的时间复杂度,使其非常快速。 哈希表能够高效地处理大量条目,前提是负载因子(条目与表格大小的比率)得到了良好的管理。 此外,哈希表具有多功能性,可用于各种应用,如实现字典、缓存 和集合。

然而,哈希也有其局限性。 虽然碰撞被最小化,但仍然可能发生,并且必须高效管理以保持性能。 哈希表可能需要比其他数据结构更多的内存,尤其是在表格稀疏填充时。 在最坏情况下,当发生许多碰撞时,时间复杂度可能会退化为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

尽管存在这些挑战,哈希依然是一种强大的方法,能够实现常数时间的搜索操作。 哈希表广泛应用于数据库索引(在此它们使快速记录检索成为可能)和缓存中,帮助快速定位缓存数据。 它们在编译器和解释器中也起着至关重要的作用,用于存储标识符的信息,并在许多编程语言中用于实现集合和关联数组(映射)。 哈希的效率和多功能性使其成为计算机科学中不可或缺的技术,用于快速且高效的 数据检索。

总结

本章我们探讨了搜索和搜索算法的基本概念,从线性搜索和子线性搜索方法的概述开始。 我们分析了线性搜索,尽管其简单且易于实现,但在效率上存在局限性,特别是在处理大数据集时。 接着,我们讨论了子线性搜索算法,如二分查找、跳跃查找和插值查找,突出了它们改善的时间复杂度,并讨论了它们在何种条件下 表现最佳。

最后,我们介绍了哈希的概念及其在实现搜索、插入和删除操作常数时间复杂度中的关键作用。 我们涵盖了不同的哈希方法,包括除法余数法、乘法法、中平方法和通用哈希,解释了每种方法的工作原理及其各自的优势 和不足。

我们还讨论了处理冲突的各种技术,例如线性探测、二次探测、双重哈希和布谷鸟哈希。 分析了每种方法的优缺点,展示了它们如何解决不同场景下搜索操作的挑战。 这些技术展示了如何通过有效管理搜索空间并 最小化冲突来优化搜索操作,从而实现更复杂的策略。

本章最后强调了哈希在各种应用中的重要性,如数据库索引、缓存、符号表以及在编程语言中实现集合和映射。 尽管存在诸如冲突处理和内存开销等挑战,但哈希被证明是计算机科学中一种多功能且高效的技术,能够实现快速 数据检索。

在下一章中,我们将探讨排序与搜索在计算系统和算法设计中的关系。 我们将研究如何在这两个过程之间找到平衡,以最小化 计算成本。

参考文献与进一步阅读

  • 《算法导论》. 作者:Thomas H. 科门,Charles E. 莱瑟森,Ronald L. 里维斯特,和 Clifford Stein。 第四版。 MIT 出版社。 2022 年:

    • 第十一章, 哈希表
  • 《计算机程序设计的艺术》. 作者:D. E. 克努斯。 第 3 卷:排序与查找(第二版)。 Addison-Wesley。 1998 年:

    • 第 6.1 节 *, 查找

    • 第 6.2 节, 二分查找

    • 第 6.4 节 *, 哈希

  • 《C++中的数据结构与算法分析》. 作者:M. A. 威斯。 (第四版)。 Pearson。 2012 年:

    • 第五章 *, 哈希

    • 第七章, 查找树

  • 算法. 作者:R. 塞奇威克,K. 韦恩。 第四版。 Addison-Wesley。 2011 年:

    • 第 3.4 节, 哈希表

第十一章:8

排序与搜索的共生关系

一些估计表明,排序消耗了全球计算能力的相当一部分,本章将探讨排序和搜索之间的共生关系。 我们将提供这些基本操作的概述,并探索它们如何相互作用并互相补充。 讨论将重点介绍现实世界的例子,说明如何在 数据处理中平衡这些关键任务。

本章作为一本实用指南,帮助你运用分析技术解决复杂的现实问题。 你将比较排序和搜索的复杂性,理解如何在它们的应用中实现平衡。 通过分析现实世界的场景,你将获得有关如何在决策中有效利用算法分析的见解。 通过这些例子,本章旨在突出选择适合特定情况的方法的重要性,优化排序和搜索过程,从而提高整体效率 和性能。

我们将在 本章中探讨以下主题:

  • 在排序 和搜索之间找到合适的平衡

  • 效率困境——组织 还是不组织?

在排序和搜索之间找到合适的平衡

让我们花一点时间思考我们与各种信息系统的互动,从本地数据库到搜索引擎和图书馆。 这些互动中有多少涉及与搜索相关的任务? 考虑决策过程。 我们能否假设,在大多数情况下,做决策都涉及寻找最佳选项? 同样,我们能否推断出,大多数问题解决任务都涉及某种形式的 搜索?

在前一章中,我们探讨了算法搜索和人工智能驱动搜索之间的区别。 虽然本章重点讨论的是算法搜索,但我们提出的一些问题也与人工智能搜索 相关。

我们之前提出的问题的答案是,确实,我们与计算机的大多数交互都涉及 直接的搜索任务,或者可以被表述为搜索问题,例如 优化 约束满足问题。搜索的概念不仅限于计算机 系统。 在我们的日常生活中,我们经常解决搜索问题。 无论是寒冷冬日里在杂乱的衣橱中寻找合适的衬衫,还是在一堆重要文件、传单和杂物中寻找电费单,我们都在不断地进行搜索活动。 这证明了搜索操作在计算机领域以及我们 日常生活中的基础性和普遍性。

这突显了在搜索任务中投入的巨大计算和人力成本。 在算法设计与分析中,这些努力通常通过时间和空间复杂度来衡量。 虽然还涉及其他成本因素,但这些超出了我们 当前讨论的范围。

排序与搜索的共生关系g

本节的主要目标是 从实践角度分析搜索操作的成本,重点关注其时间和空间复杂度。 理解这些成本对于优化搜索算法和提高整体 系统效率至关重要。

在前一章中,我们展示了线性搜索在没有任何预处理(如排序)的情况下,其时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 在最坏的情况下。 这意味着我们可能需要进行最多 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 次比较才能找到目标键。 此外,如果我们搜索所有具有相同键的项,比较的次数仍然是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 并且无法 减少。

然而,我们可以通过在进行任何查找操作之前投入一些计算资源对数据进行排序,从而实现比 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 更好的性能。 如在 第六章中讨论的那样,归并排序是最有效的排序算法之一,其运行时间为 O(nlogn)。一旦数据被排序,我们可以使用更高效的查找算法,例如二分查找类算法。 例如,标准的 二分查找 其运行时间为 O(logn)。此外,对于均匀分布的数据,我们还可以采用更高效的算法,例如 插值查找,其平均情况下的运行时间为 O(loglogn)

为了讨论的目的,假设我们选择标准的二分查找算法,其 O(logn) 运行时间。 通过将排序与高效的查找算法相结合,我们可以显著降低与在未排序数据上使用线性查找相比的整体计算成本。 如我们所见,排序与查找之间呈现出一种 共生关系。

我们 借用了“共生”这一术语,源自生物系统。 在生物学中,共生关系指的是两种不同的有机体、物种或实体之间的互利互动。 在这种关系中,双方通常都能获得某种形式的好处,其性质可以有所不同。 共生有三种主要类型

  • 共生关系:两种生物 都从这种关系中受益。 例如,蜜蜂和花朵之间有共生关系;蜜蜂从花朵中获取花蜜,而花朵通过蜜蜂的传粉得到授粉。

  • 共栖关系:一种生物受益,而另一种生物既不受到帮助也不受到伤害。 例如,螺旋藻附着在鲸鱼身上,借助被鲸鱼带到不同的觅食场所而受益,但不影响 鲸鱼。

  • 寄生关系:一种生物 以另一种生物为代价获得好处。 例如,蜱虫吸食哺乳动物的血液,使蜱虫受益,但可能对宿主造成伤害。

从广义上讲,“共生”可以描述各种背景下的关系,在这些关系中,合作和互惠互利是关键。 然而,排序和搜索可以被视为一种具有独特形式的共生关系,类似于寄生关系。 在这种关系中,排序对搜索有利,因为已排序的数据可以显著提高搜索操作的效率。 例如,二分查找要求数据已排序才能正常工作,并且能够在对数时间内快速定位元素,这比在未排序数据中进行线性查找要快得多。 此外,某些搜索算法,如数据库或特定应用中的搜索算法,固有地涉及排序作为预处理步骤,以优化 搜索过程。

然而,反过来并非如此。 高效的搜索算法并不会提高排序算法的效率。 在基于比较的排序算法中,排序算法的性能不受搜索算法效率的影响。 排序有其固有的复杂性,而与之相关的成本并不会因搜索技术的进步而减少。 因此,虽然排序为搜索提供了显著的优势,但这一好处是单方面的。 这里的共生关系更像是寄生关系,搜索在利用已排序的数据获得好处时,没有回报排序过程的任何 优势。

以下是搜索和 排序如何相互作用:

  • 搜索操作的效率:二分查找算法要求数据是已排序的,才能正确工作。 当数据已排序时,二分查找可以在 O(logn) 时间内快速定位元素,这比 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 线性查找在 无序数据中的时间复杂度要快得多。

  • 排序作为预处理步骤:许多与搜索相关的问题从排序作为预处理步骤中受益。 例如,当处理范围查询或搜索多个元素时,首先对数据进行排序可以带来更 高效的算法。

  • 复杂算法:一些复杂算法结合了排序和搜索,以更高效地解决问题。 例如,寻找中位数、众数或其他统计量的算法通常会从 排序数据开始。

  • 数据结构:如平衡二叉查找树(例如,AVL 树和红黑树)等数据结构本身维持有序顺序,从而支持高效的搜索、插入和删除操作。 类似地,数据库中使用的 B 树也维持有序数据,以优化 搜索操作。

排序和搜索并非计算机科学中唯一具有共生关系的概念。 还有其他概念对,它们也表现出类似的互惠关系。 让我们来看看其中的一些: 它们:

  • AI 搜索中的探索与利用

    • 探索:由像 广度优先搜索 (BFS)等算法实现,探索涉及 广泛地扫描搜索空间。 我们在探索上投入的越多,就越不需要依赖于利用,而后者可能 是有风险的。

    • 利用:由如 深度优先搜索 (DFS)等算法实现,利用 专注于深入探索一条路径,然后再尝试其他路径。 平衡探索与利用对于避免搜索失败的风险和确保 高效解决问题至关重要。

  • 成本与人工智能中的启发式搜索策略 策略

    • 在人工智能的搜索策略中,搜索成本与启发式方法之间存在共生关系。 在搜索过程的早期,投入精心选择的启发式方法可以显著降低整体搜索成本,帮助搜索朝着更有成效的方向发展。 这种平衡有助于更高效地解决问题。 一个典型的例子是 A*搜索算法,它将启发式方法和路径成本相结合,最优地找到最 高效的解决方案。

这些例子 展示了不同概念之间的共生关系是计算机科学中的一个常见主题。 平衡和优化这些关系是开发高效算法和解决复杂 计算问题的关键。

让我们通过一个示例来结束这一部分。 假设有一种假设的货币,用于支付任务的计算复杂度,这种货币被称为“比较”。当从未排序的数据库中搜索和检索一个项目时,我们需要支付 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 次比较,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 是数据集的大小。 然而,如果我们提前使用高效的排序算法对数据进行排序,搜索的成本可以降低到 logn 次比较。

但是,搜索成本的降低伴随着前期成本的增加。 高效地排序数据大约需要 n.logn 次比较。 因此,先排序再搜索的总成本是 n.logn (排序的成本)加上 logn (搜索的成本),这简化为大约 n.logn 次比较,因为 logn 相较于 而言, n.logn较小。

考虑到这一点,你可能会疑惑,为什么要投资于排序。 排序的好处在于,当需要多个搜索操作时,排序的优势变得显而易见。 如果我们先排序一次,然后进行多次搜索,那么最初的排序成本会通过大量搜索操作摊销,使得每个单独的搜索操作显著更快、更高效。 因此,在需要频繁搜索的场景下,排序的前期投入是值得的,这突显了排序优化搜索任务整体效率的共生关系。 搜索任务。

然而,在这种情境下有一个强大的潜在假设,这个假设在许多实际应用中可能不成立——即数据是静态的,不会发生变化。 实际上,数据集通常是动态的,除了需要搜索操作,还需要频繁的插入 和删除。

处理未排序数据时,插入新项目很简单,不会产生比较成本,因为新项目只是添加到集合的末尾。 然而,删除项目需要搜索它,这可能需要最多 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 比较,然后进行 删除。

相反,对于已排序的数据,插入新项目需要维护顺序,这通常意味着重新排序数据或找到新项目的适当位置,增加了额外的比较成本。 从已排序数据集中删除项目涉及在对数时间内搜索它,这得益于顺序,但不需要在移除后重新排序。

总之,在 排序数据前,当搜索频繁时可以显著减少搜索操作的成本,但这种好处在动态数据集中会减弱。 每次插入可能需要额外的排序,增加了总体成本。 因此,在数据经常修改的环境中,需要权衡为了高效搜索而预先排序的优势与维护排序顺序的持续成本。 这一考虑挑战了在 动态场景中通过排序提高搜索效率的影响。

在接下来的章节中,我们将通过现实世界的例子展示如何在排序和搜索之间实现平衡,以设计高效的算法。 我们将通过信息系统内外的实例来探索这种平衡,帮助做出更有效和 高效的决策。

效率困境——组织还是不组织?

我们注意到 许多专业人士和大学教授办公室乱七八糟,到处堆满了文件、书籍和账单。 他们常常苦于找不到需要的物品,在这个过程中花费大量时间。 担心可能会丢失重要物品,他们倾向于几乎保留一切。 相比之下,一些人非常重视组织(即排序)。 他们花费大量时间来整理和分类自己的物品。 然而,当需要检索物品时,他们投入的组织时间是值得的。

一个根本性的问题是,哪种方法更高效——有序还是无序? 在这一部分,我旨在探讨过度重视组织可能被高估的观点,并从排序与搜索之间的共生关系中获得启示

办公环境凌乱的专业人士往往在一个需要立即检索特定物品却充满挑战的环境中工作。 这种无序的方法可能导致低效,因为大量时间都浪费在寻找所需的文件或工具上。 尽管表面上看似混乱,但往往存在某种对个人有意义的内在系统,虽然这种系统对他人来说可能并不明显

相反,优先考虑组织的人会提前花时间对物品进行分类和整理。 这种有条理的方法起初可能显得费时,但当他们需要迅速取回某些东西时,这种方法却大有裨益。 检索效率的提升通常足以弥补整理所花费的时间。 通过拥有一个结构良好的系统,他们减少了搜索时间,使工作流程更加顺畅和 可预测。

组织与无序哪个更高效的核心问题取决于具体的背景和个人的需求。 对于某些人来说,凌乱的工作环境可能激发创造力和灵活性,而对于另一些人来说,井然有序的环境提供了清晰 和效率。

从排序和搜索算法中得到的启示可以看出,每种方法的有效性会有所不同。 在计算机术语中,一个有序的系统(例如排序数组)允许更快的搜索时间(使用二分查找等算法),而一个无序的系统(例如未排序的数组)可能需要更为全面的搜索方法(例如线性查找)。 然而,初始的排序过程本身有一定成本,是否进行排序取决于搜索操作的频率和紧迫性。

总之,组织与无序的价值可以类比于算法中排序与搜索的权衡。 两种方法各有优缺点,最佳策略通常是在适应具体需求和限制的情况下找到平衡。 通过理解排序与搜索之间的共生关系,我们可以做出更为明智的决策, 更有效地管理我们的资源和 时间。

像计算机科学家一样思考

让我们考虑以下场景——Janet 是一名数学教授,她收到一堆科研论文;假设她平均每天收到 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mip</mml:mi></mml:math> 篇论文。 她每天需要检索几篇论文用于研究。 她挑选的论文数量不定,但平均来说,她每天检索 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mis</mml:mi></mml:math> 篇论文。 其中有几篇是她的最爱,经常引用,而大部分论文是 很少访问的。

利用我们对搜索、排序及其计算成本的理解,我们旨在帮助 Janet 做出高效的决策。 首先,Janet 需要决定是否应该对她桌上的文献进行排序,如果需要,排序的频率应是多少。 这个决策将取决于排序成本与检索效率之间的平衡。 其次,她希望计算在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 天内进行操作的总运行时间,考虑到排序和检索的时间。

以下是 我们的假设:

  • 平均每天收到的新论文数量 :p

  • 平均每天访问的科研论文数量 :s

  • 总天数 :k

如果文档未排序,访问每篇文档将需要 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 的时间,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 是她桌面上的文档总数。 经过 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mid</mml:mi></mml:math> 天后,文档的总数大约是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mip</mml:mi>mml:mo.</mml:mo>mml:mid</mml:mi></mml:math>。如果文档已排序,访问每篇文档将需要 O(logn) 的时间,因为要进行二分查找。 排序 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 篇文档需要 O(nlogn) 的时间。

让我们估算一下在未排序的情况下的总运行时间。 一天的总访问时间是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mis</mml:mi>mml:mo.</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>。在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 天内,运行时间为:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mrowmml:munderover<mml:mo stretchy="false">∑</mml:mo>mml:mrowmml:mid</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:munderover>mml:mrowmml:mis</mml:mi></mml:mrow>mml:mo.</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mip</mml:mi>mml:mid</mml:mi></mml:mrow></mml:mfenced>mml:mo≈</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mis</mml:mi>mml:mip</mml:mi>mml:msupmml:mrowmml:mik</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

如果珍妮特每天整理她的文件,那么一天的整理时间是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrow<mml:mfenced separators="|">mml:mrowmml:mip</mml:mi>mml:mid</mml:mi></mml:mrow></mml:mfenced>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrow<mml:mfenced separators="|">mml:mrowmml:mip</mml:mi>mml:mid</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>。一天的访问时间可以 估算为:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mis</mml:mi>mml:mo.</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrow<mml:mfenced separators="|">mml:mrowmml:mip</mml:mi>mml:mid</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>,并且超过 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 天:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mrowmml:munderover<mml:mo stretchy="false">∑</mml:mo>mml:mrowmml:mid</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:munderover>mml:mrow<mml:mfenced separators="|">mml:mrowmml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mip</mml:mi>mml:mid</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:mip</mml:mi>mml:mid</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mis</mml:mi>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:mip</mml:mi>mml:mid</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:mrow></mml:mrow>mml:mo≈</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mip</mml:mi>mml:msupmml:mrowmml:mik</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi><mml:mfenced separators="|">mml:mrowmml:mip</mml:mi>mml:msupmml:mrowmml:mik</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math>

频繁地整理文件 如果 Janet 访问的文件数量相比她收到的文件数量很大,那么频繁整理文件是有益的。 更精确地说,若 s>log(n),其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 是文件的总数量,那么整理文件是有利的。 例如,假设 Janet 的档案中有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn10,000</mml:mn></mml:math> 个文件,并且她一天内取回超过 100 个文件(因为 log(10,000)=100),那么整理文件将变得有利。 然而,这个数字会每天增加,这并不是 非常现实的。

尽管如此,我们对 Janet 的情况做出了若干隐含的假设:

  • 排序能力:我们假设 Janet 能够使用高效的算法(例如归并排序)对她的文件进行排序。 然而,通常情况下,人类排序的效率不及简单的算法(如冒泡排序),后者的效率要低得多。

  • 平等访问概率:我们假设所有文件被访问的可能性相同。 但实际上,一些文件被频繁引用,而其他文件则很少甚至从未被检索,可能遵循幂律分布。

  • 将文件返回顶部:在无排序的情况下,我们假设 Janet 通常会把用过的文件放回堆顶。 这意味着她不必重新翻找整个堆来找到该文件。 此外,尽管人类在系统排序方面表现不佳,但他们通常具有很强的能力在随机杂乱的物品中定位物品,这与心理学上的 哈希机制类似。

考虑到这些因素,最初支持频繁排序的分析可能并不成立。 人类天然具备定位常用物品的能力,而且在动态环境中维持排序系统并不实际,这些都表明对于 Janet 而言,排序可能并非最有效的方式。

需要注意的是,所提供的示例是一个计算分析,当然可以为决策提供参考,但它并不是唯一需要考虑的因素。 换句话说,当涉及到人类或人类参与的过程时,计算效率并不一定能转化为生产力。 保持一个整洁有序的环境能够在心理上改善我们的情绪,帮助我们集中注意力,并促进 深度工作。

毫不奇怪 ,即使是在涉及机器人起义的假设场景中,机器人本身也可能表现出无序和缺乏组织!

尽管我们的分析表明,从纯粹的计算角度来看,排序可能并非最有效的方式,但人类因素,如对整洁工作空间的需求以及组织的心理益处,仍不应被忽视。 这些因素能够显著提升整体生产力和幸福感,因此将文件进行排序和组织的决策比单纯的计算效率所建议的更为复杂。

总结

在本章中,我们详细探讨了排序和搜索之间的共生关系。 结果表明,尽管排序可以显著降低搜索操作的时间复杂度,但从排序中获得的整体效率必须仔细权衡,在动态环境中,其中数据经常更新。 通过各种场景和例子,我们突出了在决定是否以及如何频繁地对数据进行排序时所涉及的权衡。 我们还指出,尽管排序的计算优势不一定会转化为在考虑人因素和心理影响时的实际效率。 我们的分析表明,人类发现物品的自然能力和有组织工作空间的心理优势增加了决策过程的复杂性。

本章结束时,我们认识到尽管计算分析提供了宝贵的洞见,但在涉及人类时并不是生产力的唯一决定因素。 排序的假设性好处必须与实际考虑和个体的固有优势和偏好取得平衡。 在我们继续前进时,下一章将介绍随机算法,为处理计算问题提供新的视角。 这些算法利用随机性来实现高效的解决方案,并提供了与迄今为止讨论的确定性方法的激动人心的对比。

参考资料和进一步阅读

  • 《活出算法:人类决策的计算机科学》。作者:布莱恩·克里斯汀和汤姆·格里菲斯。 出版社:亨利霍尔特和 公司。 2016 年

  • 《深度工作:专注成功的规则在分心世界中》。作者:卡尔·纽波特,大中央 出版社,2016 年

第十二章:9

随机算法

在前几章讨论的确定性算法的基础上,我们现在转向那些不确定性和随机性是关键因素的情况。 在本章中,我们将探讨如何设计算法,即使在存在不可预测因素的情况下,也能做出最佳决策。 随机算法将机会元素引入其逻辑,提供创新的方式来解决那些可能用纯粹确定性方法解决起来具有挑战性或效率低下的问题。 这些算法通常能够简化解决方案、提高性能,并为问题解决提供新的视角。 在本章中,我们将审视利用随机性实现期望结果的各种策略和技术。 通过理解随机算法背后的原理,我们可以在不确定的环境中开发出强大的解决方案。 我们的讨论将涵盖理论基础、实际应用以及一些示例,展示将随机性融入 算法设计中的力量与多样性。

请注意,虽然本章中使用了一些概率论的初步内容,但要全面理解本章,仍然需要具备足够的概率论基础。 如果你对概率论不够熟悉,我们建议在继续之前,使用可信的参考书籍和教材来刷新你的知识。

在本章中,我们将涵盖以下内容: 主题:

  • 概率算法综述 的回顾

  • 随机算法分析 的分析

  • 案例研究

概率算法综述

首先,让我们探索是否可以将到目前为止讨论的概念应用于解决以下 假设问题:

  • 一个新的在线约会应用程序 Matcher已经开发出来,旨在帮助用户找到潜在的伴侣。 该应用程序像一个游戏,旨在将用户与他们最合适的 约会对象配对:

    • 该应用程序中有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miN</mml:mi></mml:math> 潜在的匹配项可供选择(匹配总数对用户是未知的)。 让我们考虑一个名为 Tom 的用户。

    • 当 Tom 打开Matcher应用时,系统会一次展示一个潜在匹配对象,随机选择。Tom 可以选择喜欢(向右滑动)或不喜欢(向左滑动)每个个人资料。

    • Tom 的决定是不可逆的;一旦他在个人资料上向右滑动或向左滑动,他就无法改变主意。所有的决定都是最终的。

    • Tom 最多可以喜欢n个个人资料(其中n远小于N)。一旦 Tom 喜欢了n个个人资料,应用程序将不再向他展示更多个人资料。

    • 互动结束的条件是:要么 Tom 已经喜欢了n个个人资料,要么没有更多的个人资料可以显示。

    • 需要注意的是,喜欢一个个人资料并不保证匹配;还需要对方喜欢 Tom 的个人资料。

    • 对 Tom 来说,挑战在于如何最有效地利用他的n个喜欢,选择最合适的匹配对象。他需要决定何时停止浏览,开始喜欢个人资料,以最大化选择最佳可用选项的机会。

  • 芳被邀请去她朋友家,朋友家位于一条长长的单行道的中间。 街道的一个侧面允许停车。 街道上交通繁忙,找到停车位非常具有挑战性。 根据经验,约有 10%的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 停车位通常在任何时刻可用。 芳每次经过时只能看到一个停车位,无法看到前方的停车位。 目标是确定她何时应决定停车并占用一个可用的 停车位。

上述两种问题有一个根本的相似性:它们都是搜索问题。 在每种情境中,目标是从一系列的可能性中识别出最佳选项: 选择最优解:

  • 汤姆正在寻找一个最佳匹配的 约会应用程序

  • 芳正在尝试找到一个最靠近她 朋友家的停车位

然而,这些问题与传统的搜索设置有所不同。 与传统搜索问题不同,在这些问题中,所有数据项并非一开始就能进行比较,而是数据项会按顺序和不可预测地出现。 数据的顺序到达意味着我们无法预测下一个数据项,从而使得基于比较的标准搜索 算法变得无效。

在这些情境中,挑战不在于通过比较识别最佳数据项,而是在于决定何时停止搜索并 做出选择。 决策过程涉及对每个选项进行评估,并确定是否接受它,或继续搜索可能更好的选项。 以下是这些问题的关键特征: 这些问题的特点:

  • 数据顺序到达:数据项逐一到达,必须立即做出决策,而无法预知 未来的数据。

  • 不可逆决策:一旦做出接受或拒绝某个选项的决策,就无法撤回。 这增加了复杂性,因为你不能重新审视 之前的选择。

  • 最佳停顿规则:这些问题的核心在于找到最佳的停顿时刻。 这涉及到决定在何时停止搜索,并接受当前选项作为 最佳可用选项。

最优停止理论提供了解决这类问题的框架。 该理论有助于确定在何时继续寻找的成本超过找到更好选项的概率时,停止寻找的最佳时机。 例如,我们可以考虑 以下情况:

  • 汤姆可以使用一种策略,他最初会查看一定数量的资料,而不做任何决定(以收集匹配质量的信息),然后选择一个比他已经查看过的资料更好的资料。 到目前为止。

  • 方可以开车经过前几个停车位(以评估可用性和接近度),然后停在一个比她已看到的更近的停车位。 已经看到的停车位。

这些问题展示了最优停止理论在实际生活中的应用,特别是在数据按顺序到达并且必须实时做出决策的场景中。 通过理解和应用这一理论,人们可以在不确定性和信息不完全的情况下,做出何时停止寻找并接受选项的明智决策。 这一方法将焦点从寻找最佳数据项转向确定做出决策的最佳时刻。 这一方法将重点从寻找最佳数据项转向确定做出决策的最佳时机。 决策的最佳时机。

最优停止问题是随机算法的一个典型示例。 在我们详细探讨随机算法之前,让我们回顾一下算法的基本概念。 算法是一个将一组输入数据转化为一组输出数据的系统。 图 9**.1 提供了一个简单的框图来说明 这一概念。

图 9.1:一个框图,展示了通过算法将输入数据映射到输出数据的过程

图 9.1:一个框图,展示了通过算法将输入数据映射到输出数据的过程。

让我们通过将输入映射到输出的视角重新审视算法的概念。 假设我们有一个输入数据集表示为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi>mml:mo=</mml:mo>mml:mo<</mml:mo>mml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:mo…</mml:mo>mml:mo,</mml:mo>mml:msubmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msub>mml:mo></mml:mo></mml:math>。一个由一组过程指令组成的算法,会将 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi></mml:math> 转化为 一个 输出 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miB</mml:mi>mml:mo=</mml:mo>mml:mo<</mml:mo>mml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:mo…</mml:mo>mml:mo,</mml:mo>mml:msubmml:mrowmml:mib</mml:mi></mml:mrow>mml:mrowmml:mim</mml:mi></mml:mrow></mml:msub>mml:mo></mml:mo></mml:math>

在确定性算法的上下文中,这一转化过程是一致且可重复的。 给定相同的输入 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi></mml:math>,一个确定性算法总是会产生相同的输出 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miB</mml:mi></mml:math>。这种可预测性是确定性算法的一个标志,确保它们的行为完全由输入决定 它们接收到的输入。

例如,考虑我们在前几章介绍的排序和搜索算法。 这些算法是确定性的,因为它们保证每次使用相同的输入运行时,都会产生相同的结果。 相同的输入。

确定性算法通过确保相同的输入总是产生相同的输出,提供了可预测性和可靠性。 这一特性对计算机科学中许多算法至关重要,尤其是那些需要一致且可重复结果的任务,如排序和搜索。 理解这一概念为探索引入随机元素的算法奠定了基础,后者通过引入随机性来实现不同的目标,并以独特的方式处理不确定性。

非确定性算法

非确定性算法 是主要用于计算复杂度研究的理论构造。 它们假设存在一种“非确定性”机器,如非确定性图灵机,可以做出任意选择,从而同时探索不同的计算路径。 非确定性算法常用于定义问题的类别,如 非确定性多项式时间 (NP),包括那些其解可以通过确定性算法在多项式时间内验证的问题。 非确定性算法无法在标准的确定性机器上实际实现。 它们作为理解并行计算潜力的一种方式,并用来分类 问题的复杂度。

非确定性算法无法在标准的确定性机器上实际实现。 另一方面,随机算法是可以实际实现的算法,它们使用随机数来影响决策过程。 这些算法可以在标准计算机上实现,并在计算机科学 和工程的许多领域中有应用。

图 9**.2 提供了一个简单的框图,以说明随机算法的概念。 随机算法包含一个随机性元素,作为一个额外的隐藏输入,我们无法控制它。 这一随机性元素引入了不确定性,实质上作为算法的第二个输入。 正是这种固有的随机性导致算法即使在显式输入保持不变的情况下,仍然表现出不同的行为和/或性能。

考虑一下 随机快速排序 算法。 与传统的快速排序不同,后者通常选择一个固定的基准(如第一个、最后一个或中间的元素),随机快速排序则随机选择基准。 这种随机选择可能导致每次运行算法时产生不同的比较和交换序列,即使是在相同的输入序列上。 例如,给定输入序列  A=[3,1,4,1,5,9,2,6,5],传统的快速排序算法可能总是选择中间元素作为基准。 相比之下,随机快速排序可能选择任何元素作为基准,从而在每次运行中产生不同的步骤顺序和分区。 因此,中间步骤可能会有所不同,可能会导致即使使用相同的数据,整体运行时间也会有所不同。 同样的数据。

图 9.2:一个方框图,展示了通过非确定性算法将输入数据映射到输出数据的过程

图 9.2:一个方框图,展示了通过非确定性算法将输入数据映射到输出数据的过程

任何输入的随机性都会引入算法性能和行为的变动。 尽管随机快速排序的平均时间复杂度保持在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>,但由于基准选择是随机的,特定实例的实际运行时间可能会有所不同。 在最坏情况下,如果基准选择反复很差,性能可能会下降,尽管这种情况在统计上是不太可能发生的。 统计上不太可能。

通过引入随机性,这些 算法可以避免确定性算法可能遇到的特殊情况。 例如,随机化快速排序避免了在某些输入下传统快速排序的最坏情况 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 出现的情况。 随机化算法通常更加健壮和适应性强。 它们在各种输入下表现良好,使其成为实践中的多用途工具。 在下一节中,我们将探讨分析 随机化算法的框架。

为了有效地学习随机化算法,我们需要探讨几个 关键主题:

  • 随机化算法的分析:理解算法在概率和期望结果方面的表现至关重要。 这涉及到分析平均情况的表现,而不仅仅是关注最坏情况。 这一主题将在接下来的名为 随机化算法的分析的部分中详细讲解。

  • 随机化数据结构:设计包含随机性的 数据结构可以带来更高效的操作。 例如,跳表和哈希表就是其中的典型。 尤其是跳表,我们将在 第十一章中深入探讨。

  • 案例研究:为了应用所学的概念,我们将分析一些在不确定性下的特定问题,比如本节开始时提到的问题。 详细的解决方案和讨论将在名为 案例研究的部分中呈现。

通过探索这些主题,我们将全面了解随机化算法的运作方式,以及如何在 各种应用中有效利用它们。

随机化算法的分析

在分析 随机算法时,我们常常使用 概率分析。与其专注于最坏情况,我们更倾向于检查 期望 算法在所有可能随机选择下的表现。 这个期望值提供了一个更现实的算法典型行为的图像。 以下是一些 关键原则:

  • 我们计算关键性能指标的期望值,如运行时间或空间使用。 这涉及对所有可能输入的性能进行平均,并按 它们的概率加权。

  • 我们研究算法性能指标的分布和方差,以了解它们偏离期望值的程度。 这有助于评估算法的可靠性和一致性。 算法。

  • 重点是分析算法在典型或随机选择的输入下的行为,而不是最坏情况下的输入。 这提供了一个更现实的算法 实际性能衡量标准。

  • 我们使用反映算法预期使用场景的现实输入模型。 例如,在排序中,假设输入是随机排列的,而不是总是已排序或 倒序排序的。

  • 我们建立了具有高概率的性能界限。 例如,一个算法可能会在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mil</mml:mi>mml:mio</mml:mi>mml:mig</mml:mi>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 时间内高概率运行,即使它偶尔会 运行较慢。

  • 使用随机变量来模拟算法内部的随机性。 我们分析这些变量如何影响算法的行为和性能。 我们还考虑随机变量之间的独立性或相关性,以简化分析或得出更准确的 性能估计。

除了前面讨论的原则之外,随机算法的分析通常还涉及具体的技术和模型。 随机抽样是一种基本技术,用于高效地做出决策或估计大数据集的属性。 蒙特卡洛算法和拉斯维加斯算法以不同的方式利用随机性。 蒙特卡洛算法提供概率性保证,并在固定时间内运行,具有一定的错误概率,而拉斯维加斯算法保证正确性,但运行时间是可变的。 马尔可夫链和随机游走是分析更高级随机算法的重要模型。 马尔可夫链有助于理解具有状态转移的系统行为,而随机游走被应用于各种网络算法和 优化问题。

分析方法可以是自适应的或对抗性的。 自适应分析研究算法如何应对变化的输入条件或环境变化。 自适应分析研究算法如何响应变化的输入条件或环境变化。 另一方面,对抗性分析考虑的是输入被设计为挑战算法性能至极限的情形。 需要注意的是,尽管这些原则和技术对于全面理解随机算法至关重要,但它们超出了本书的范围。 因此,我们不会在这里详细讨论所有这些原则。

通过几个例子来说明随机算法的分析往往最为有效,逐步展示详细的过程。 第一个例子是著名的 蒙提·霍尔问题。第二个例子是 生日悖论 ,最后,我们讨论著名的 招聘 秘书问题

蒙提·霍尔问题

蒙提·霍尔问题 是一个经典的脑筋急转弯,展示了概率推理的力量以及有时反直觉的特性,而概率推理是分析 随机算法的核心概念。

问题如下:你面前有三扇门(分别标为 A、B 和 C)。 其中一扇门后有一辆车,另外两扇门后是山羊。 你选择了一扇门(假设是 A 门)。 主持人知道每扇门后面有什么,打开了另一扇门(假设是 B 门)露出了一只山羊。 主持人然后给你选择,是否换到剩下的封闭门(C 门)。 你应该坚持最初的选择,还是换门以最大化赢得汽车的机会呢? 该怎么做?

反直觉的答案是:是的,你应该毫不犹豫地换门。 换门能使你赢得汽车的机会加倍。 乍一看,似乎在揭示了一扇门之后,剩下两扇门的胜率是 50/50。 然而,这是一种常见的误解。 正确的策略是始终换门,以下是原因:

  • 汽车在 A 门后(你最初选择的门)的概率: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mn1</mml:mn></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:mfrac></mml:math>

  • 汽车在 B 门或 C 门后面的概率: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:mfrac></mml:math>

  • 主持人打开 B 门的动作(它总是会露出一只山羊)并不会改变最初的概率。 相反,它提供了额外的信息,影响了这些概率的分布。 这些概率的分布情况。

让我们来分析一下概率,并考虑汽车可能位置的两种情况:

  • 汽车在你最初选择的门后(门 A)的概率: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mn1</mml:mn></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:mfrac></mml:math>

    • 如果你坚持选择 A 门,你有概率赢得汽车,概率为<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mn1</mml:mn></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:mfrac></mml:math>

    • 如果你切换到 C 门,你有概率输掉,概率为<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mn1</mml:mn></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:mfrac></mml:math>

  • 汽车可能在其他门(B 门或 C 门)后的概率为: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:mfrac></mml:math>

    • 由于主持人已经在 B 门后揭示了山羊,如果汽车不在 A 门后,那么它一定在 C 门后。

    • 如果你切换到 C 门,你有概率赢得汽车,概率为<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:mfrac></mml:math>

然后,通过换门,你实际上是在押注你最初选择的门是错误的概率,这个概率是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:mfrac></mml:math>. 因此,换门会将你赢得汽车的机会从 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mn1</mml:mn></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:mfrac></mml:math> 提高到 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:mfrac></mml:math>. 这一违反直觉的结果是一个经典例子,展示了在人类直觉常常在 概率情境中产生误导时的情况。 蒙提霍尔问题凸显了理解概率和基于数学分析做出决策的重要性,而不是 依赖直觉。

生日悖论

生日悖论,也 被称为生日问题,是一个著名的概率问题,展示了一个违反直觉的结果。 它涉及到在一群人中,至少有两个人共享相同生日的可能性。 令人惊讶的是,这个概率在群体很小的时候就能相对较高。

问题可以这样表述:在一群 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 人中,至少有两个人共享相同的生日的概率是多少? 假设一年有 365 天,每个人的生日在这些天中任意一天出现的概率相同。

为了理解生日悖论,计算互补概率更为简单——即没有两个人共享相同生日的概率——然后从 1 中减去这个概率。 我们可以按如下方式分解生日问题的互补概率的计算: 如下所示:

  • 第一个人拥有独特生日的概率是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mn365</mml:mn></mml:mrow>mml:mrowmml:mn365</mml:mn></mml:mrow></mml:mfrac></mml:math> (因为还没有选择其他人)

  • 第二个人和第一个人生日不同的概率 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mn364</mml:mn></mml:mrow>mml:mrowmml:mn365</mml:mn></mml:mrow></mml:mfrac></mml:math>

  • 第三个人和前两个人生日不同的概率 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:mn363</mml:mn></mml:mrow>mml:mrowmml:mn365</mml:mn></mml:mrow></mml:mfrac></mml:math>

  • ….

  • 对于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 个人,所有生日都独特的概率 P独特生日 如下所示: 如下:

    P独特生日=365365×364365×363365×…×365−n−1365

    或者,它是 如下所示:

    P独特生日=∏i=0n−11−i365

然后,至少两个人共享同一生日的概率 Psharedbirthday 如下所示: 如下所示:

Psharedbirthday=1−Puniquebirthday

结果表明, 共享生日的概率随着群体大小的增加而迅速增加。 以下是一个简单的 Python 核心代码,用于模拟 生日悖论:

 import random
import matplotlib.pyplot as plt
def simulate_birthday_paradox(trials, n):
    shared_birthday_count = 0
    for _ in range(trials):
        birthdays = []
        for person in range(n):
            birthday = random.randint(1, 365)
            if birthday in birthdays:
                shared_birthday_count += 1
                break
            birthdays.append(birthday)
    return shared_birthday_count / trials
def main():
    trials = 10000
    results = []
    group_sizes = range(2, 367)
    for n in group_sizes:
        probability = simulate_birthday_paradox(trials, n)
        results.append(probability)
        print(f"Group size: {n}, Probability of shared birthday: {probability:.4f}")
    plt.figure(figsize=(10, 6))
    plt.plot(group_sizes, results, marker='o')
    plt.title('Birthday Paradox Simulation')
    plt.xlabel('Group Size')
    plt.ylabel('Probability of Shared Birthday')
    plt.grid(True)
    plt.show()
if __name__ == "__main__":
    main()

该函数运行 给定群体大小的生日悖论模拟 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 和一定次数的试验,默认是 10,000 次。 它计算至少有两个人共享相同生日的试验次数。 对于每次试验,它为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 个人生成随机生日,并检查是否有重复。 图 9**.3 展示了模拟的结果。

图 9.3:模拟演示了随着群体大小的增加,共享生日的概率如何迅速增加

图 9.3:模拟演示了随着群体大小的增加,共享生日的概率如何迅速增加

共享生日的概率急剧增加,违反了直觉,因为我们的本能可能认为需要一个更大的群体,才能获得更高的共享生日的概率。 这一点可以在 图 9**.3中观察到。

生日悖论 说明了在人类直觉上,如何常常错误地判断涉及组合和大数的概率。 它作为概率论中的一堂宝贵课程,展示了数学计算在理解看似简单的问题中的重要性。 这个悖论还有实际应用,比如在密码学中,哈希碰撞的概念类似于 生日问题。

雇佣秘书问题

雇佣秘书问题,也被称为 秘书问题 最佳选择问题,是最优停止理论中的一个著名问题。 它描述了一个场景,雇主希望从 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 逐一面试的申请者中挑选出最合适的秘书。 雇主必须决定是否在面试后立即聘用某个候选人,而不能回头考虑之前的候选人。 目标是最大化选择 最佳候选人的概率。

以下是在 这个问题中给出的内容:

  • 顺序面试 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 候选人 以随机顺序逐一进行面试。

  • 立即决策:在 每次面试后,雇主必须决定是否聘用该候选人。 如果拒绝,该候选人将 无法重新考虑。

  • 目标:目标是最大化选择 最佳候选人的概率。

这个问题的最优策略出人意料地优雅且违背直觉。 它涉及 两个阶段:

  • 观察阶段:直接拒绝前 第一个 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 候选人。 此阶段纯粹是为了观察,以便了解候选人的质量。 第一个 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 候选人也被称为 训练样本。

  • 选择阶段:从 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrow<mml:mfenced separators="|">mml:mrowmml:mik</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mit</mml:mi>mml:mih</mml:mi></mml:mrow></mml:msup></mml:math> 候选人开始,聘用第一个比所有之前 面试过的候选人都更优秀的人。

可能会出现一个问题:如果第一个 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 之后的候选人都不比那些最初被拒绝的更优秀,怎么办? 在这种不幸的情况下,唯一的选择是聘用最后一个候选人。 这种结果增加了面试的整体成本,并加大了聘用不合适候选人的风险。 不合适的候选人。

值为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 最大化选择最佳候选者的概率可以近似为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mie</mml:mi></mml:mrow></mml:mfrac></mml:math>,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mie</mml:mi></mml:math> 是自然对数的底数(大约为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn2.718</mml:mn></mml:math>)。 对于较大的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>,这大约简化为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn37</mml:mn>mml:mi%</mml:mi></mml:math> 总候选人数的 百分比。

使用最优停顿规则选择最佳候选者的概率由以下公式给出:

Pnk=k−1n∑j=kn1j−1

我们知道 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miP</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msub><mml:mfenced separators="|">mml:mrowmml:mik</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mfracmml:mrowmml:mn1</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:mfrac></mml:math>

以下 Python 代码估算 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miP</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msub></mml:math> 对于不同的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 值的估算,并且还估算了最大化 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi>mml:mo/</mml:mo>mml:min</mml:mi></mml:math> 的比率,这个比率最大化了 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miP</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msub></mml:math> 的概率。

 import numpy as np
import matplotlib.pyplot as plt
def calculate_p_n(n, k):
    if k == 1:
        return 1 / n
    sum_term = sum(1 / (j - 1) for j in range(k, n + 1))
    return (k - 1) / n * sum_term
def find_optimal_k(n):
    probabilities = [calculate_p_n(n, k) for k in range(1, n + 1)]
    optimal_k = np.argmax(probabilities) + 1
    return optimal_k, probabilities
n_values = np.arange(10, 501, 1)  # Smoother plot with more points
optimal_k_ratios = []
for n in n_values:
    optimal_k, probabilities = find_optimal_k(n)
    optimal_k_ratios.append(optimal_k / n)
plt.figure(figsize=(10, 6))
plt.plot(n_values, optimal_k_ratios, marker='o', linestyle='-', markersize=4, label='Optimal k/n Ratio')
plt.axhline(1/np.e, color='r', linestyle='--', label='1/e (approximately 0.3679)')
plt.title('Optimal k/n Ratio for Different Values of n')
plt.xlabel('n')
plt.ylabel('Optimal k/n Ratio')
plt.legend()
plt.grid(True)
plt.show()

让我们简要解释一下代码。 <st c="25373">calculate_p_n</st> 函数计算给定条件下的概率 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miP</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msub><mml:mfenced separators="|">mml:mrowmml:mik</mml:mi></mml:mrow></mml:mfenced></mml:math> 对于给定的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 使用提供的公式计算。 <st c="25528">find_optimal_k</st> 函数计算 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miP</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msub><mml:mfenced separators="|">mml:mrowmml:mik</mml:mi></mml:mrow></mml:mfenced></mml:math> 对于所有的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 从 1 到 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:mrow></mml:math> 并找出能够最大化 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miP</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msub></mml:math> 值。 参数如下:

  • n_values:用于分析的不同 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 值。

  • optimal_k_ratios:此项存储比率 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi>mml:mo/</mml:mo>mml:min</mml:mi></mml:math> 最大化 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miP</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msub></mml:math> 每个 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>的值。

  • 第一个图(图 9.4)显示了<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miP</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msub><mml:mfenced separators="|">mml:mrowmml:mik</mml:mi></mml:mrow></mml:mfenced></mml:math><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math>的关系,针对不同的<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>值。第二个图(图 9.5)显示了最大化<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi>mml:mo/</mml:mo>mml:min</mml:mi></mml:math>的比例,针对不同的<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miP</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msub></mml:math>值,且包含了一条参考水平线,表示<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn1</mml:mn>mml:mo/</mml:mo>mml:mie</mml:mi></mml:math>

图 9**.4 展示了概率如何 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miP</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msub><mml:mfenced separators="|">mml:mrowmml:mik</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 的变化 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 以及 图 9**.5 展示了最大化该概率的比率 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi>mml:mo/</mml:mo>mml:min</mml:mi></mml:math> 趋向于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn1</mml:mn>mml:mo/</mml:mo>mml:mie</mml:mi>mml:mo=</mml:mo>mml:mn0.37</mml:mn></mml:math> 或 37% 的候选人总数,这验证了 理论近似。

这一策略背后的逻辑 基于在收集足够的信息以做出明智决策和等待过久而错过最佳候选人的风险之间进行权衡。 通过拒绝前 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 个候选人,雇主为后续候选人的评估设定了一个基准。 使用这种策略选择最佳候选人的概率大约是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn1</mml:mn>mml:mo/</mml:mo>mml:mie</mml:mi></mml:math>,或者大约是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn37</mml:mn>mml:mi%</mml:mi></mml:math> (见 图 9**.4)。 这意味着,平均来说,雇主按照此 最优策略选择最佳候选人的概率为 37%。

示例 9.1

使用最优停留理论,提出一个在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 个候选人中选择一个的策略。

假设有 n=10 个候选人。 使用最优 停留规则:

  • 观察阶段:拒绝前 10/e≈3.7≈4 个候选人。

  • 选择阶段:从第五个候选人开始,聘用第一个比所有 前面候选人都优秀的人。

如果最佳候选人位于前四名之内,将会被错过。 如果最佳候选人位于第五名或之后,选择到他们的机会很大,因为他们可能比处于 观察阶段的候选人更优秀。

图 9.4:不同候选人数(n)下的概率 P​ n​​​(k) 对 k 的关系

图 9.4:概率 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrow<mml:mi mathvariant="bold-italic">P</mml:mi></mml:mrow>mml:mrow<mml:mi mathvariant="bold-italic">n</mml:mi></mml:mrow></mml:msub><mml:mfenced separators="|">mml:mrow<mml:mi mathvariant="bold-italic">k</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">k</mml:mi></mml:math> 在不同候选人数 (<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">n</mml:mi></mml:math>)

图 9.5:不同候选人数 (n) 的最优 k/n 比例。随着 n 的增加,k/n 比例趋向黄金比率 1/e = 0.37 或 37% 的候选人总数。

图 9.5:不同候选人数 (n) 的最优 k/n 比例。 随着 n 的增加, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">k</mml:mi>mml:mo/</mml:mo><mml:mi mathvariant="bold-italic">n</mml:mi></mml:math> 比率趋向黄金比率 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn1</mml:mn>mml:mo/</mml:mo><mml:mi mathvariant="bold-italic">e</mml:mi>mml:mo=</mml:mo>mml:mn0.37</mml:mn></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn37</mml:mn><mml:mi mathvariant="bold-italic">%</mml:mi></mml:math> 的候选人总数。

上述 招聘问题是最优停止理论的经典示例,展示了在实时场景中应用概率决策的过程。 它有几个有趣的意义 和扩展:

  • 未知候选人数量:如果候选人数量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 未知,可以开发自适应策略 来应对。

  • 多重选择:可以选择多个候选人的变体,相应调整 策略

这个理论有许多实际应用。 尽管该模型经过简化,但它为招聘和其他顺序选择场景中的决策过程提供了深入的见解(见下一节)。 类似的策略可以应用于在线拍卖,竞标者必须根据观察到的价格决定何时停止竞标。 该理论也有一些行为学上的见解。 该问题突显了最优策略如何常常违背直觉,需要严谨的数学方法来识别最佳行动路线。 行动方向。

招聘 秘书问题优雅地结合了概率论、决策理论和最优停止理论的元素。 它提供了一个清晰的例子,展示了在不确定性和连续选择的情境中,结构化策略如何显著改善决策。 通过理解这个问题背后的原理,人们可以获得关于最优停止理论及其应用的宝贵见解。

案例研究

在本章开始时,我们介绍了汤姆和方面临的三个问题。 这些问题涉及随机算法和概率推理,可以通过最优停止理论来解决。 从本质上讲,这些问题集中在确定何时停止搜索,而不是搜索什么。 作为案例研究,我们将详细分析并解决这些问题,应用我们在 本章中学到的概念。

在线约会应用中的最佳选择

一个新的 在线约会应用, Matcher,已 被设计来帮助用户找到最佳配对。 该应用程序类似于游戏,用户每次只能看到一个潜在的匹配对象,随机从一个 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miN</mml:mi></mml:math> 潜在匹配对象中选出(匹配总数对用户是未知的)。 Tom,我们的用户,最多可以使用 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 个喜欢(其中n≪N)。 他的目标是最大化利用有限的喜欢数,找到最佳匹配。

当 Tom 打开 Matcher 应用时,他可以选择 喜欢 (右滑)或 不喜欢 (左滑)每个资料。 一旦做出决定,就无法更改。 Tom 需要仔细决定何时停止浏览,并开始使用他的喜欢,以最大化选择最佳匹配的机会。 我们应该注意 以下几点:

  • Tom 必须实时决定是 喜欢 还是 不喜欢 每个呈现的资料 按顺序展示

  • 一旦 Tom 选择了 喜欢 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 个资料后,应用程序将不再向他展示任何 其他资料

  • 目标是在他的限制条件下,最大化选择最佳匹配的概率 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 喜欢

存在一些限制条件:

  • Tom 无法重新访问 之前的资料

  • 喜欢 一个资料并不保证匹配;还需要对方也 喜欢 Tom 的资料

最优停止 理论提供了一种策略,旨在最大化从一系列选择中选择最佳选项的机会。 该策略包括两个阶段:观察阶段和 选择阶段:

  • 观察阶段:Tom 应该观察并拒绝前 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 个资料,以收集关于候选池的信息。 在经典的秘书问题中,最优选择的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 大约是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mie</mml:mi></mml:mrow></mml:mfrac></mml:math>。然而,由于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miN</mml:mi></mml:math> 是未知的,Tom 可以使用一种自适应策略来 估计 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math>

  • 选择阶段:在观察阶段之后,Tom 应该开始选择下一个比他在观察阶段看到的所有资料更好的资料。 如果 Tom 没有找到更好的资料,他将选择他遇到的最后几个资料,确保他使用所有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 选择。

如果不知道总共有多少资料(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miN</mml:mi></mml:math>),一种实用的方法是假设 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miN</mml:mi></mml:math> 是一个很大的数字,例如 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn1000</mml:mn></mml:math>。然后 Tom 可以采用基于最优停留理论的策略。 然而,在 现实情况下,使用此方法存在显著的缺点。

Tom 假设 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miN</mml:mi></mml:math> 是很大的,例如 1000 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn1000</mml:mn></mml:math>

  • 观察阶段: 排除第一个 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn37</mml:mn>mml:mi%</mml:mi></mml:math> 的资料。 例如对于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miN</mml:mi>mml:mo=</mml:mo>mml:mn1000</mml:mn></mml:math>,这意味着排除前 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn370</mml:mn></mml:math> 个资料。

  • 选择阶段: 从 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:mn371</mml:mn></mml:mrow>mml:mrowmml:mis</mml:mi>mml:mit</mml:mi></mml:mrow></mml:msup></mml:math> 开始喜欢资料,仅当它们比观察阶段看到的所有资料更好时,才可以选择。

这些是该策略的缺点 ,具体有两个方面:

  • 人类记忆的局限性:对于汤姆来说,记住并比较 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn370</mml:mn></mml:math> 每个他看到的新资料是不现实的。 这会带来巨大的认知负担,并且对 大多数人来说并不可行。

  • 按顺序排列的资料潜在偏差:如果资料是按某种方式排序的(例如按他们收到的点赞数排序),则此策略可能会失败。 如果最佳资料出现在开始或结束位置,这种策略将无法有效工作,因为它依赖于 资料排序的随机性。

考虑到前述策略的不可行性,我们可以采用一种更实际的方法来缓解这些问题。 我们不再使用固定的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn37</mml:mn>mml:mi%</mml:mi></mml:math> 的一个大假设 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miN</mml:mi></mml:math>,而是将观察阶段缩减为基于汤姆总喜欢数的一个可管理的资料数(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>)。 例如,可以使用一个较小的分数,如 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:math> 作为观察阶段的参考。 这种实际方法的另一个好处是,它采用了一种相对比较策略,在这种策略下,汤姆只需记住目前为止看到的最佳资料,而不是 所有资料。

让我们探索提出的实际方法。 汤姆可以选择观察前 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 个档案,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi>mml:mo≈</mml:mo>mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:math>。这是一种在缺乏信息的情况下平衡探索和开发的启发式方法。 关于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miN</mml:mi></mml:math>

假设汤姆有 10 个可用的喜欢。 首先,我们 选择 k:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mik</mml:mi>mml:mo=</mml:mo>mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac>mml:mo=</mml:mo>mml:mfracmml:mrowmml:mn10</mml:mn></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac>mml:mo=</mml:mo>mml:mn5</mml:mn></mml:math>

汤姆观察并拒绝前五个档案。 从第六个档案开始,汤姆会喜欢比他观察到的前五个档案更好的下一个档案。 如果后续没有更好的档案,他会把喜欢用在他看到的最后几个档案上。 如果没有更好的后续档案,他会在最后的档案上使用他的喜欢。

通过使用这种策略,汤姆最大化了在他的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 喜欢限制内找到高质量匹配的机会。 这种启发式方法平衡了收集信息和及时决策的需求,而不知道候选人总数。

对于汤姆在 Matcher 应用上的修订最优停止策略 包括将部分“喜欢”用于观察阶段,然后应用收集到的信息,在选择阶段做出决策。 这种方法为不确定条件下的顺序决策提供了实际的解决方案,确保汤姆能有效使用“喜欢”,最大化找到 最佳匹配的机会。

寻找最靠近的停车位

方正在驾车前往她朋友的 住所,沿着一条繁忙的单行道,路上有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 停车位,停车仅限于街道的一侧。 根据过去的经验,约有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn10</mml:mn>mml:mi%</mml:mi></mml:math> 的停车位通常在任何给定时刻是空的。 方每次经过时只能看到一个空位,无法看到前方的空位。 目标是确定方在何时才是停车的最佳时机,使用最优停止理论。 图 9**.6 展示了这个问题。 图中,街道的左侧显示了可以停车的地方,并突出显示了某些空位。 街道的右侧是 禁止停车区。

最优停止理论帮助方决定何时停止寻找并停车,通过平衡对停车位的探索与利用已找到的最佳空位

图 9.6:方停车问题的示意图

图 9.6:方停车问题的示意图

在招聘和约会应用的问题中,存在隐性和主观的标准来排名候选人。 相比之下,停车问题有一个明确的标准:停车位到方朋友家距离。 因此,我们的策略高度依赖于目的地的位置 我们可以识别出 三种情景:

  • 街道起点的目的地:这是最简单的情况,因为方应该选择她遇到的第一个可用停车位。 在这里无需应用最优停止理论,因为这代表的是 最理想的情况。

  • 街道尽头的目的地:这是最坏的情况。 方需要找到离目的地最近的可用停车位,因此她应该尽可能多地拒绝停车位,以最大化找到接近街道尽头的停车位的机会。 最优停止理论在 这种情况下特别有用。

  • 街道中间的目的地:这代表了一个平均情况,假设目的地正好位于街道的中间。 在这里,最优停止理论同样适用,帮助方决定何时停止并选择一个可用的停车位。 我们为 这个情况解决问题。

平均情况可以分成 两部分:

  • 到达目的地之前:这一部分与最坏情况类似。 方将尽量使用最优停止理论拒绝尽可能多的停车位。 她应该拒绝前 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 个停车位,然后选择第 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrow<mml:mfenced separators="|">mml:mrowmml:mik</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:mrow>mml:mrowmml:mit</mml:mi>mml:mih</mml:mi></mml:mrow></mml:msup></mml:math> 个可用的停车位。

  • 经过目的地后:如果方在到达目的地之前找不到任何可用停车位,策略就会发生变化。 此时,问题转变为最理想的情况。 方应当在经过目的地后,停车于她遇到的第一个可用停车位。 目的地之后。

我们可以一步步解决这个问题,适用于平均情况(以下简称,目的地称为 中点):

  • 确定 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math>: 对于最优停车策略,我们计算 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi>mml:mo≈</mml:mo>mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mie</mml:mi></mml:mrow></mml:mfrac></mml:math>。然而,考虑到通常只有约 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn10</mml:mn>mml:mi%</mml:mi></mml:math> 的停车位在任何时候是可用的,我们需要相应地调整我们的计算。 方可以通过考虑街道的长度除以普通轿车的平均长度来估算街道上可以停放的车辆总数。 由于她的目的地位于街道中间,我们将可用性乘以 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn0.5</mml:mn></mml:math>。如果她的目的地在街道的前三分之一,那么这个系数将是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn0.33</mml:mn></mml:math>。因此,我们按以下方式调整 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math>

    k≈5n100×e=n54.3

以下表示方在开始认真考虑 停车之前将经过的停车位数量:

  • 中点之前:方应当开车经过第一个 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 停车位而不停车。 经过 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 停车位后,方将选择下一个比她之前见过的所有停车位都更好的空位停车。 到目前为止。

  • 中点之后:如果方在到达中点时尚未找到合适的停车位,她将停在遇到的第一个空闲停车位。 她遇到的第一个停车位。

这一方法将问题分解为可管理的部分,并在每部分内有效地应用最优停止理论。 在中点之前,方通过拒绝前 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 个停车位,收集足够的信息,以便根据停车位的质量做出明智的决策。 这一策略最大化了她找到一个更好停车位的机会,尤其是在她接近目的地时。 然而,在中点之后,我们转向最佳情况方法,确保如果方在中点之前没有找到停车位,她能够尽量减少步行到朋友家时的距离。 中点之前。

总结

在这一章中,我们探讨了使用最优停止定理和随机算法在不确定性条件下做出最优决策的各种问题。 我们考察了诸如招聘问题、 匹配器 约会应用程序,以及方的停车问题等场景,每个问题都要求在收集信息与及时决策之间找到战略性的平衡。 通过这些示例,我们展示了最优停止定理如何通过设置适当的观察阶段和选择标准,提供一种结构化的方法来最大化选择最佳选项的机会。 本章展示了概率推理和最优停止规则在实际决策情境中的强大作用。 在下一章中,我们将探讨动态规划,这是一种通过将复杂问题分解为 更简单的子问题来解决问题的强大技术。

参考文献与进一步阅读

  • 算法导论。作者:Thomas H. Cormen,Charles E. Leiserson,Ronald L. Rivest 和 Clifford Stein。 第四版。 MIT 出版社。 2022 年:

    • 第五章 概率分析与 随机算法
  • 算法与人生:人类决策的计算机科学。作者:Brian Christian 和 Tom Griffiths。 亨利·霍尔特出版社 公司 2016 年。

  • 谁解答了秘书问题。作者:T. S. Ferguson。 统计学科学*。4(3):282–89。

  • 蒙提·霍尔 问题 https://en.wikipedia.org/wiki/Monty_Hall_problem

第十三章:10

动态规划

动态规划是一种 强大的技术,能够显著降低许多复杂算法的计算成本,尽管它也有一些权衡。 本章介绍了动态规划,并包括对 贪心算法的回顾。

我们将通过回顾分治法的原则,来与动态规划进行对比。 动态规划在算法设计中脱颖而出,因为它能够解决涉及重叠子问题和最优子结构的问题。 通过存储这些子问题的结果,动态规划避免了冗余计算,从而显著提高了 效率。

通过各种示例,我们将探索如何应用动态规划来解决经典问题,如背包问题、最长公共子序列问题和旅行推销员问题。 每个示例将展示如何逐步分解问题,定义状态空间,并制定递推关系,进而得出动态规划解法。

需要注意的是,动态规划是一个广泛的主题,不能仅通过一章内容完全覆盖。 在这一章中,我们将重点讨论这一方法的关键方面,包括其主要元素、应用和示例。 此外,我们还将讨论如何估算动态规划解法的复杂度,并探讨这种方法的优缺点。 这一方法的优缺点。

具体来说,我们将在以下 主要部分中进行详细阐述:

  • 动态规划 与分治法

  • 探索 动态规划

  • 贪心算法 – 简介

动态规划与分治法

第四章 第五章中,我们 探讨了递归算法及其复杂度分析方法。 我们还探讨了分治策略。 分治法背后的基本思想是将问题分解为更小的子问题,优化解决这些子问题,然后将它们的解合并形成最终解。 这一过程通常是递归进行的,也就是说,问题会不断被分解为子问题,直到达到一个子问题足够小,可以通过直觉或简单的方法解决。 这个最小且最简单的问题被称为 基本情况

动态规划遵循与分治法相似的策略。 它将一个问题分解为多个子问题,明确假设任何子问题的最优解将有助于最终最优解的形成。 这一特性被称为 最优子结构。虽然 这一特性在分治算法中也存在,但通常是隐式假设,而非 明确声明。

然而,动态规划在一个关键方面超越了分治方法,这也是它的独特之处。 在动态规划中,子问题通常会共享公共的子子问题,也就是说,子问题之间会有重叠。 这一特性被称为 重叠子问题。并非所有问题都会表现出这种行为。 当一个问题没有重叠子问题时,使用简单的分治方法更为合适,因为在 这种情况下,动态规划不会提供额外的好处。

但为什么重叠子问题如此重要呢? 算法设计和分析的主要目标之一,正是我们始终关注的,是减少算法的计算成本或复杂度。 重叠子问题的重要性在于它们能够显著降低计算复杂度。 在子问题重叠的情况下,分治算法可能会冗余地多次求解这些重叠的子子问题。 相比之下,动态规划会存储这些子子问题的解,并在需要时重复使用它们。 这种对先前计算结果的重用,可以大大减少计算复杂度。 然而,这种高效性是有代价的:需要额外的空间来存储这些 子子问题的解。

现在我们理解了分治法与动态规划之间的联系,让我们来讨论一下动态规划本身。 首先,需要明确的是,动态规划这一术语并不涉及编写代码或计算机编程。 相反,它指的是一种数学优化方法。 动态规划最初被提出作为解决优化问题的一种技术,这也是为什么它在数学优化和计算机科学中都有研究的原因。 在本章中,我们将重点讨论动态规划在 计算机科学领域中的应用。

正如我们 之前讨论的,动态规划建立在两个基本概念之上:最优子结构的假设和重叠子问题的存在。 最优子结构意味着问题的最优解可以通过其子问题的最优解来构建。 重叠子问题是指在递归过程中,同一子问题被多次求解。 在接下来的子节中,我们将更详细地探讨这些概念,研究它们如何成为动态规划的基础,以及它们如何促进高效算法的发展,以应对 复杂问题。

最优子结构

最优子结构 是算法设计中的一个核心概念,意味着一个给定问题的最优解可以通过有效利用其较小子问题的最优解来构建。 这一特性表明,问题可以被分解为更简单、相互重叠的子问题,每个子问题都可以独立解决。 一旦子问题得到最优解,它们的解就可以被组合起来,形成原始更大问题的解。 大的问题。

最优子结构的存在至关重要,因为它使我们能够将复杂问题分解为更易处理的组件。 这种分解不仅简化了问题解决过程,而且使得可以应用像动态规划和分治这样的算法策略。 在动态规划中,最优子结构特性确保一旦我们获得了所有子问题的最优解,就能利用它们系统地构建整个问题的最优解。 通过将子问题的结果存储在表中,避免了重新计算,从而提高了 算法的效率。

在分治算法中,尽管方式不同,最优子结构同样至关重要。 这些算法将问题分解为不重叠的子问题,独立解决每个子问题,然后合并它们的解。 动态规划和分治方法的成功依赖于问题具备最优子结构,因为它保证了通过最优解决较小部分问题将导致整体问题的最优解。 整体问题。

最优子结构不仅是一个理论概念,也是算法设计中的一个实用工具。 它帮助我们识别何时可以通过动态规划或其他依赖于从较小组件逐步构建解决方案的策略高效地解决问题。 理解并识别不同问题中的最优子结构是开发高效且有效算法的关键技能。 接下来,我们将通过一些 示例问题来探讨这一概念。

第一个例子是最短路径问题。 在图上的最短路径问题中,目标是找到从源节点(顶点)到目标节点的最短路径。 这个问题展示了最优子结构特性,因为两个节点之间的最短路径可以分解为经过中间节点的更小的最短路径。 假设 S 是源节点, G 是目标节点。 我们的目标是找到这两个节点之间的最短路径。 假设 A 是从 S G 的最优(最短)路径上的一个节点。 最优子结构特性表明,因为A 是最优路径的一部分,从 S G 的整个最优路径必须包含从 S A 的最优路径,接着是从 A G 的最优路径。 最优子结构也意味着,任何不是最优的子路径不能成为整体最优解的一部分。 这意味着,如果从 S 到 A 或从 A G 的子路径不是最短的,那么从 S G 的整个路径就不可能是最短的(见 图 10.1)。

图 10.1:展示最短路径问题中的最优子结构。

图 10.1:展示最短路径问题中的最优子结构。

第二个例子是寻找 最长公共子序列 (LCS)。 这个问题涉及 识别可以在一组序列中找到的最长子序列。 需要理解的是,子序列不同于子串。 在子序列中,与子串不同,元素不需要在序列中连续出现。 这种区别在计算机科学的许多应用中至关重要,例如数据压缩、序列比对和 文件比较。

LCS 问题中的最优子结构 意味着找到两个序列的 LCS 的解决方案可以通过较小子问题的解决方案构建。 具体来说,两个序列 X 和 Y 的 LCS 可以通过考虑这些序列的前缀的 LCS 来构建。

假设我们有两个序列 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miX</mml:mi></mml:math> 的长度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miY</mml:mi></mml:math> 的长度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>。这两个序列的最长公共子序列(LCS)表示为 LCS<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mo(</mml:mo>mml:miX</mml:mi>mml:mo,</mml:mo>mml:miY</mml:mi>mml:mo)</mml:mo></mml:math>,可以通过以下方法确定:如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miX</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miY</mml:mi></mml:math> 是空序列,那么 LCS 也是空序列。 这是我们递归的基本情况。 如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miX</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miY</mml:mi></mml:math> 的最后一个字符相同 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mfenced separators="|">mml:mrowmml:mis</mml:mi>mml:mia</mml:mi>mml:miy</mml:mi>mml:miX</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miY</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:math>,那么这些字符必须是 LCS 的一部分。 因此,问题转化为查找前缀的 LCS <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miX</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mn1</mml:mn>mml:mo…</mml:mo>mml:mim</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miY</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mn1</mml:mn>mml:mo…</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>,然后将匹配的字符附加到 这个 LCS 中。

然而,如果最后的字符在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miX</mml:mi></mml:math><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miY</mml:mi></mml:math> 不匹配(即,<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miX</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mim</mml:mi></mml:mrow></mml:mfenced>mml:mo≠</mml:mo>mml:miY</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>),那么最长公共子序列(LCS)是通过以下两种方法中得到的较长的一个:

  • 排除 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miX</mml:mi></mml:math> 的最后一个字符,并考虑 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miX</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mn1</mml:mn>mml:mo…</mml:mo>mml:mim</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miY</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mn1</mml:mn>mml:mo…</mml:mo>mml:min</mml:mi></mml:mrow></mml:mfenced></mml:math> 的最长公共子序列。

  • 排除最后一个字符 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miY</mml:mi></mml:math> 并考虑 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miX</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mn1</mml:mn>mml:mo…</mml:mo>mml:mim</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miY</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mn1</mml:mn>mml:mo…</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>

L(i,j) 表示前缀的 LCS <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miX</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mn1</mml:mn>mml:mo…</mml:mo>mml:mii</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miY</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mn1</mml:mn>mml:mo…</mml:mo>mml:mij</mml:mi></mml:mrow></mml:mfenced></mml:math>。然后,最优子结构性质可以表示为 如下:

示例 10.1:

考虑以下序列:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miX</mml:mi>mml:mo=</mml:mo>mml:mtextabbbsbbsh</mml:mtext></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miY</mml:mi>mml:mo=</mml:mo>mml:mtextbbsdhjsh</mml:mtext></mml:math>

如果两个序列的最后一个字符相同(这里,两者都以 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mih</mml:mi></mml:math>) 结尾,那么 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mih</mml:mi></mml:math> 是最长公共子序列(LCS)的一部分,问题简化为寻找 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miX</mml:mi>mml:mo=</mml:mo>mml:mtextabbbsbbs</mml:mtext></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miY</mml:mi>mml:mo=</mml:mo>mml:mtextbbsdhjs</mml:mtext>mml:mo.</mml:mo></mml:math> 如果最后一个字符不同,我们必须考虑较小序列的 LCS <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miX</mml:mi>mml:mo=</mml:mo>mml:mtextabbbsbbsh</mml:mtext></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miY</mml:mi>mml:mo=</mml:mo>mml:mtextbbsdhjs</mml:mtext></mml:math>,或者 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miX</mml:mi>mml:mo=</mml:mo>mml:mtextabbbsbbs</mml:mtext></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miY</mml:mi>mml:mo=</mml:mo>mml:mtextbbsdhjsh</mml:mtext></mml:math>,然后取 较长的那个。

LCS 问题中的最优子结构确保了可以通过较小子问题的 LCS 来构建整体 LCS。 这一性质对动态规划方法至关重要,在动态规划中,我们系统地解决并存储这些子问题的结果,以高效地构建最终的 解决方案。

并非所有问题 都具有最优子结构性质。 最长路径问题就是一个例子。 特别是在可能存在环的图中,最长路径问题被认为不具有最优子结构性质。 要理解这一点,我们首先回顾一下最优子结构的含义:一个问题如果具有最优子结构性质,那么该问题的最优解可以通过其子问题的最优解来构造。 其子问题的最优解。

在最长路径问题中,目标是找到图中两个节点之间的最长简单路径(即不重复节点的路径)。 当图中包含环时,这个问题特别具有挑战性,因为环的存在可能使得识别最长路径变得复杂。 在最长路径问题中,最优子结构性质不成立,原因如下:

  • 后续路径的依赖性:在许多情况下,一个看似是从一个节点到另一个节点的最长路径的一部分的子路径,扩展到其他节点时可能并不会导致整体的最长路径。 这是因为,选择一个看起来很长的子路径,可能会迫使你在后续选择较短的路径,从而减少整体的 路径长度。

  • 环的参与:如果图中包含环,那么最长路径可能涉及以某种方式穿越图的部分,使得它无法简单地分解为独立贡献于整体最长路径的子问题。 是否包含或排除某些边缘的决定,可能会显著改变最终的 路径长度。

  • 非加性特性:在具有最优子结构的问题中,通常可以独立地解决子问题,然后将它们组合起来得到最优解。 然而,在最长路径问题中,优化地解决一个子问题(即,从一个节点到另一个节点找到最长路径)并不能保证这个子路径将成为整个图中最优解的一部分。 例如,如果你已经找到了从顶点 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miv</mml:mi></mml:mrow>mml:mrowmml:mia</mml:mi></mml:mrow></mml:msub></mml:math> 到顶点 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miv</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub></mml:math> ,然后从顶点 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miv</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub></mml:math> 到顶点 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miv</mml:mi></mml:mrow>mml:mrowmml:mic</mml:mi></mml:mrow></mml:msub></mml:math>,这些路径的组合可能并不会产生从 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miv</mml:mi></mml:mrow>mml:mrowmml:mia</mml:mi></mml:mrow></mml:msub></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miv</mml:mi></mml:mrow>mml:mrowmml:mic</mml:mi></mml:mrow></mml:msub></mml:math>的最长路径。 可能全局最优解会绕过 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miv</mml:mi></mml:mrow>mml:mrowmml:mib</mml:mi></mml:mrow></mml:msub></mml:math> ,如果有一条更长的替代路径 可用的话。

示例 10.2:

考虑一个加权图,其中节点为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miB</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miC</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miD</mml:mi></mml:math>,且存在边 A→B B→C C→D A→D。从 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miD</mml:mi></mml:math> 的最远路径最初可能看起来会经过 B 和 C。 然而,如果直接边 A→D 比路径的总和更长 A→B→C→D,那么最优解并不涉及从 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miB</mml:mi></mml:math> <mml:math xmlns=mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miB</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miC</mml:mi></mml:math> ,以及 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miC</mml:mi></mml:math> <mml:math xmlns=mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miD</mml:mi></mml:math>的最长路径。

显然,这个图是有向的且带权重的。 如果这个图是无向图且无权重的,那么这个场景是不可行的,因为它违反了三角不等式,三角不等式指出,两个点之间的直接距离应该小于或等于经过中间点的距离之和。 然而,即使在这样一个有向的无权重图中,结果也可能 让人感到反直觉。

假设这些节点代表 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miB</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miC</mml:mi></mml:math> D分别代表四个城市:多伦多、芝加哥、丹佛和洛杉矶。 边表示直飞航班的票价。 例如, A→B 表示从多伦多到芝加哥的直飞航班票价。

如在 图 10**.2中所示,从多伦多到洛杉矶的最远路径是通过一班直飞航班(由边 Toronto→LosAngeles ,权重为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mi$</mml:mi>mml:mn957</mml:mn></mml:math>)且不包含任何在芝加哥或丹佛的停留。 这个结果可能显得反直觉,因为人们通常认为,较长的路径会涉及更多的停留,但在这种情况下,直飞航班最贵,因此在成本方面形成了最长路径。

值得一提的是,在图中没有循环的情况下,正如我们简单的示例所示,解决最长路径问题的一个简单算法是将所有权重取负,然后将其作为最短路径问题来解决。 这个转换之所以有效,是因为在一个负权图中寻找最短路径,相当于在原始图中寻找最长路径。

图 10.2:最长路径问题的一个示例

图 10.2:最长路径问题的一个示例

最长路径问题缺乏最优子结构,这意味着动态规划以及类似的依赖于将问题分解为子问题的技术并不奏效。 这是因为,最长路径问题需要图的全局知识,不能仅仅依赖从较小、独立解决的子问题逐步构建。 因此,最长路径问题通常更具挑战性,尤其是在包含循环的图中,并且不适合依赖最优子结构的动态规划等方法。

重叠子问题

动态规划的第二个特点,区分它与分治算法的地方,是重叠子问题的概念。 在一个设计良好的分治策略中,每一步递归通常解决一个新的、独特的子问题。 一个很好的例子是归并排序,在每一步划分中,划分之间不会重叠,并且每个子问题都是相互独立的。

然而,也有一些问题,其中某些子问题会重叠,意味着在递归过程中同一个子问题会被解决多次。 这正是动态规划的优势所在,它通过避免冗余计算来提升效率。 为了说明这一点,我们可以考虑斐波那契数列作为一个例子。

斐波那契数列的定义如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miF</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn0</mml:mn></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn0</mml:mn></mml:math>

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miF</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:math>

Fn=Fn−1+Fn−2forn≥2

使用分治法计算斐波那契数时,相同的子问题会被反复求解。 例如,为了计算 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miF</mml:mi>mml:mo(</mml:mo>mml:mn5</mml:mn>mml:mo)</mml:mo></mml:math>,算法需要计算 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miF</mml:mi>mml:mo(</mml:mo>mml:mn4</mml:mn>mml:mo)</mml:mo></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miF</mml:mi>mml:mo(</mml:mo>mml:mn3</mml:mn>mml:mo)</mml:mo></mml:math>。但是为了计算 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miF</mml:mi>mml:mo(</mml:mo>mml:mn4</mml:mn>mml:mo)</mml:mo></mml:math>,算法又会计算 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miF</mml:mi>mml:mo(</mml:mo>mml:mn3</mml:mn>mml:mo)</mml:mo></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miF</mml:mi>mml:mo(</mml:mo>mml:mn2</mml:mn>mml:mo)</mml:mo></mml:math>,这意味着 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miF</mml:mi>mml:mo(</mml:mo>mml:mn3</mml:mn>mml:mo)</mml:mo></mml:math> 会被计算多次。 避免这种冗余的方法之一是将之前计算的子问题结果存储在具有近乎常数访问时间的数据结构中,例如 哈希表。

这种冗余导致了指数级的时间复杂度,因为递归调用的数量随着 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>增长呈指数级。这就是动态规划提供显著优势的地方。 通过避免重复解决相同的子问题,动态规划存储这些子问题的结果,并在需要时重用它们,从而大大减少了计算次数。

通过利用重叠子问题,动态规划将斐波那契数列的递归解法从指数级时间复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 转换为线性时间复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>。这种效率提升是动态规划在存在重叠子问题的情况下优于分治法的原因。 在下一节中,我们将通过几个示例更详细地探讨动态规划。 接下来,我们将深入分析这些例子。

探索动态规划

一个问题 当我们识别出重叠子问题时会出现:如何存储这些解以避免冗余的计算呢? 这就是动态规划引入“记忆化”概念的地方。 记忆化技术 包括将子问题的结果存储在数据结构中,如数组或字典中,这样当相同的子问题再次出现时,可以立即使用已存储的结果,避免重新计算。 从而省去了重新计算的必要。

在我们开始之前,重要的是澄清一下,“记忆化”(memoization)并不是“记忆”(memorization)的拼写错误。这两个术语有着不同的含义。 记忆化来源于拉丁语单词“memo”,意思是“要被记住”。它指的是计算机科学中的一种技术,其中昂贵的函数调用结果被存储并重用,以避免冗余的计算。 相反,记忆是指通过学习和重复将信息记住的过程。 通常是通过重复进行的。

记忆化是一种在动态规划中使用的技术,通过存储代价较高的函数调用的结果,并在相同输入再次出现时重用它们,从而优化递归算法。 记忆化避免了多次重新计算相同的结果,在第一次解决子问题时保存结果,然后每当再次遇到相同的子问题时返回存储的结果。 这显著降低了具有 重叠子问题 的算法的时间复杂度。

让我们探讨记忆化是如何工作的。 在典型的递归算法中,一个函数会不断地用更小的输入调用自己,直到达到基准情况。 没有记忆化时,函数可能会用相同的输入被多次调用,从而导致冗余计算。 这种低效在使用分治策略的直接递归算法中非常常见。 通过存储这些重复的子问题的结果,记忆化消除了这些冗余计算,使算法变得 更加高效。

使用记忆化时,当一个函数被调用时,算法首先检查给定输入的结果是否已经被存储。 如果已经存储,则立即返回存储的结果,避免冗余计算。 子问题的结果被存储在一个数据结构中,通常是字典或数组,充当查找表。 这个表通常被称为 一个 记忆表

例子 10.3:

让我们重新回顾斐波那契数列的例子,通过它来说明记忆化,并与不使用记忆化的方法进行比较。

斐波那契数列定义为 如下:

F(0)=0

F(1)=1

Fn=Fn−1+Fn−2forn≥2

没有记忆化时

以下是 使用朴素递归方法实现斐波那契数列的一个简单 Python 实现:

 def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)
n = 10
print(f"Fibonacci number F({n}) is: {fib(n)}")

这种方法具有指数时间复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math> 因为它会重新计算相同的斐波那契数 多次。

首先,让我们分析递归斐波那契数列算法的复杂性。 该算法将问题分解为两个子问题,子问题的规模分别减少了一个和两个。 递归部分有两个组件,而驱动函数用于计算前两个斐波那契数的和,并在常数 时间内操作(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mic</mml:mi></mml:math>)。

这个算法的递归关系可以表示为 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>

这个递归关系并不完全符合分治法或减法递归函数的标准形式。 因此,我们将其简化为 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mic</mml:mi>mml:mo≈</mml:mo>mml:mn2</mml:mn><mml:mfenced separators="|">mml:mrowmml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mic</mml:mi></mml:mrow></mml:mfenced></mml:math>

通过这种 简化,递归函数呈现出一种可以使用主定理分析的形式(见 第五章)。 根据主定理,由于 a=2 且 b=1,递归斐波那契数列算法的时间复杂度为:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mia</mml:mi></mml:mrow>mml:mrowmml:min</mml:mi>mml:mo/</mml:mo>mml:mib</mml:mi></mml:mrow></mml:msup>mml:mo⋅</mml:mo>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miΘ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

这个结果显示,递归的斐波那契算法具有指数时间复杂度,具体为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="normal">Θ</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>,使得它在处理大值时效率低下 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>。这种低效是为什么备忘录技术或动态规划常用于优化斐波那契数的计算。

图 10.3:展示计算斐波那契(5)的非备忘录方法的树形图

图 10.3:展示计算斐波那契(5)的非备忘录方法的树形图

图 10**.3 展示了一个树形结构的例子,表示用于计算斐波那契(5)的分治(非备忘录)实现。 如图所示,叶节点重复包含斐波那契(1)和斐波那契(0)。 此外,许多斐波那契数在整个过程中被重复计算多次。

使用备忘录技术

通过应用备忘录技术,该算法避免了冗余的计算。 这是一个简单的 Python 实现斐波那契数列的动态规划实现,它使用备忘录技术来优化 计算:

 def dp_fib(n, memo={}):
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    memo[n] = dp_fib(n-1, memo) + dp_fib(n-2, memo)
    return memo[n]
n = 10
print(f"Fibonacci number F({n}) is: {dp_fib(n)}")

在这个版本中,时间复杂度被降低为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>,因为每个斐波那契数仅计算一次,并存储以供 以后参考。

图 10.4:展示计算斐波那契(8)的备忘录方法的图表

图 10.4:演示计算斐波那契(8)的备忘录方法的图表

图 10**.4 展示了使用备忘化的动态规划方法来计算 Fibonacci(8) 的图示。 在这种方法中,没有任何斐波那契数被重复计算。 相反,之前解决过的子问题会被重用,这在从节点延伸出的边中得以体现。 这种对子问题的重用是我们使用树形结构可视化分治算法的原因(图 10**.3),而动态规划更适合用图形来表示(图 10**.4)。

备忘化在动态规划中至关重要,尤其是对于有重叠子问题的问题。 通过存储这些子问题的结果,备忘化最小化了解决整体问题所需的计算次数,从而显著提高了时间复杂度。 然而,这一好处是以额外的 存储空间为代价的。 这种权衡通常在时间效率更为关键的场景中是有利的。 需要注意的是,备忘化通常用于自顶向下的动态规划方法中,在这种方法中,问题是递归地解决的,结果在计算时就会被存储。 在接下来的子章节中,我们将简要讨论自顶向下和自底向上 动态规划。

自顶向下与自底向上动态规划方法

实现动态规划有 两种主要方法:自顶向下和自底向上。 这两种方法都旨在通过避免冗余计算来降低计算复杂度,但它们的实现方式 不同。

自顶向下的动态规划,也叫做备忘化,是一种递归方法。 它从顶部开始解决问题,逐步将其分解为更小的子问题。 当算法解决这些子问题时,它会将结果存储在一个数据结构中(通常是字典或数组),这样如果相同的子问题需要再次解决时,就可以直接使用已存储的结果,而不是 重新计算它。

自顶向下的过程从原始问题开始,递归地将其分解为更小的子问题。 每次解决一个子问题时,结果会被存储(备忘化)。 如果再次遇到该子问题,算法会检索已存储的结果,而不是 重新计算它。

自顶向下方法相对较容易实现,特别是从分治递归解决方案适应而来。 在自顶向下动态规划中,只处理解决原问题必要的子问题,这在某些子问题不需要时可以是有利的。 然而,这种方法也有其缺点。 由于需要在调用堆栈上存储递归函数调用,可能需要更多的内存。 例子 10.3 说明了斐波那契数列计算的备忘录版本,是自顶向下 动态规划的经典示例。

自顶向下方法的替代方案是动态规划的自底向上实现。 自底向上动态规划,也称为表格法,是一种迭代方法。 它从解决最小的子问题开始,然后利用这些解来构建原问题的解。 在这种方法中,创建一个表(通常是一个数组),每个条目代表一个子问题的解。 该过程从处理基本情况开始,并首先解决最小的子问题。 解决方案存储在表中,然后迭代使用它们来解决更大的子问题,最终导致 最终解。

底向上方法通常比自顶向下方法更节省空间,因为它避免了递归函数调用的开销。 此外,通过使用迭代过程而不是递归,它消除了堆栈溢出的风险。 这种方法通常提供了解决方案构建方式的更清晰视角,使其更易于管理和理解。 然而,自底向上方法有两个显著的缺点。 首先,它需要解决所有子问题,即使对最终解也可能不必要。 此外,在初始实现时可能更复杂,特别是从 递归方法过渡时。

以下是斐波那契数列的自底向上 Python 实现:

 def bottom_up_fib(n):
    if n <= 1:
        return n
    fib = [0] * (n+1)
    fib[1] = 1
    for i in range(2, n+1):
        fib[i] = fib[i-1] + fib[i-2]
    return fib[n]
n = 10
print(f"Fibonacci number F({n}) is: {bottom_up_fib(n)}")

表格 10.1 总结了动态规划中自顶向下和自底向上方法的特点。

特点 自顶向下(记忆化) 自底向上(表格法)
方法 递归,从顶部(主要问题)开始解决问题,并将其拆分为 子问题。 迭代,从底部(基本情况)开始解决问题,并逐步构建到 主要问题。
空间复杂度 可能由于调用栈和 记忆化存储使用更多内存。 通常使用较少的内存,因为它主要依赖于一个表 或数组。
时间复杂度 通过存储子问题的结果来避免重新计算, 通常是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 也避免了重新计算, 通常是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>
实现难度 实现难度 如果从朴素的 递归解法过渡,通常更容易实现。 最初可能会更难实现,尤其是在转换自 递归方法时。
栈溢出风险 由于 深度递归,风险较高。 没有风险,因为它完全避免了 递归。

表 10.1:自上而下与自下而上的方法对比

自上而下 和自下而上的动态规划方法实现相同的目标:通过避免冗余计算来优化计算过程。 两者之间的选择通常取决于特定问题、程序员对递归或迭代的熟悉程度以及任务的内存和性能约束。 表 10.1 展示了动态规划中自上而下和自下而上的方法对比。

让我们花点时间讨论一下如何计算动态规划实现的时间复杂度。 正如我们所指出的,动态规划算法更适合用有向图而非树来表示。 第五章中,我们探讨了使用递归树来估算递归算法的时间复杂度。 在这里,我们将不再使用树,而是利用图形来可视化动态规划算法。

在动态规划图中,节点表示子问题,从这些节点发出的边表示我们在每个子问题中遇到的选择。 我们可以从边的目标节点的角度来解读这一点,因为图是有向的。 每个指向节点的入边代表一个函数调用——这意味着子问题是被回忆而不是重新计算的,就像在典型的 分治法中那样。

现在,让我们假设总共有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math> 个子问题(或函数),每个子问题被回忆 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mir</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:math> 次,其中 1≤i≤m。这个动态规划算法的时间复杂度可以表示如下: 如下所示:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:munderover<mml:mo stretchy="false">∑</mml:mo>mml:mrowmml:mii</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:mrow>mml:mrowmml:mim</mml:mi></mml:mrow></mml:munderover>mml:mrowmml:mii</mml:mi></mml:mrow></mml:mrow>mml:mo⋅</mml:mo>mml:msubmml:mrowmml:mir</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:mrow></mml:mfenced>mml:mo,</mml:mo>mml:mn1</mml:mn>mml:mo≤</mml:mo>mml:mii</mml:mi>mml:mo≤</mml:mo>mml:mim</mml:mi></mml:math>

这种方法通常会导致线性时间复杂度,特别是在子问题的数量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mim</mml:mi></mml:math> 与输入的大小成正比的情况下 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>。在最坏的情况下,其中 m=n,每个子问题都直接对应输入的唯一状态,算法必须精确地处理每个状态 一次。

当这种情况发生时,动态规划算法会高效地在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 时间内计算出解。 这是因为算法通过存储并重用重叠的子问题的结果,而不是重新计算它们,从而避免了指数时间复杂度的产生。 因此,总操作次数与输入的大小成线性关系,避免了在朴素 递归方法中出现的指数时间复杂度。

这种线性时间复杂度是动态规划的一个关键优势。 它使得算法能够有效地随着输入规模的变化进行扩展,从而使得解决大规模和复杂问题变得可行,而这些问题如果使用暴力法或 分治策略将是计算上不可行的。

让我们应用前面讨论的方法来分析动态规划实现斐波那契数列的时间复杂度,如 示例 10.3所示。通过考虑 图 10**.4,该图表示计算斐波那契数列第 8 个数的图(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miF</mml:mi>mml:mo(</mml:mo>mml:mn8</mml:mn>mml:mo)</mml:mo></mml:math>),我们可以推导出 时间复杂度。

在图中,每个节点对应一个子问题——具体来说,就是需要计算的斐波那契数之一。 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miF</mml:mi>mml:mo(</mml:mo>mml:mn8</mml:mn>mml:mo)</mml:mo></mml:math> 包括 8 个节点,每个节点代表一个重叠的子问题。 此外,这个图中的每个节点都有两种选择:一种对应于前一个斐波那契数的函数调用 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mo(</mml:mo>mml:miF</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo>mml:mo)</mml:mo></mml:math> 和另一个对应于再前一个斐波那契数的函数调用 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mo(</mml:mo>mml:miF</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo-</mml:mo>mml:mn2</mml:mn>mml:mo)</mml:mo>mml:mo)</mml:mo></mml:math>。这些选择被描绘为每个节点的出边,或者等效地,作为每个节点的两条入边。 在图中。

为了计算 时间复杂度,我们观察到图结构导致每个节点(或子问题)只被处理一次,每个节点涉及两个操作:分别对应每个出边。 这导致总共 8×2 操作 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miF</mml:mi>mml:mo(</mml:mo>mml:mn8</mml:mn>mml:mo)</mml:mo></mml:math>

将这一推广到 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miF</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>,我们观察到对于任意斐波那契数 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>,操作的总数可以表达为: 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miT</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn2</mml:mn>mml:mo×</mml:mo>mml:min</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>

该公式表明,动态规划计算斐波那契数列的方法随着输入规模呈线性增长 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>。这一效率是通过动态规划避免冗余计算,存储和重用子问题的结果(备忘录法)实现的,从而显著降低了时间复杂度,相较于朴素递归方法,其时间复杂度是指数级的 时间复杂度。

让我们来看另一个经典的动态规划例子,那就是 背包问题

使用动态规划解决 0/1 背包问题

背包问题是一个经典的 优化问题,目标是确定通过选择物品放入背包,在满足重量限制的前提下能够获得的最大价值。 每个物品都有特定的重量和价值,挑战在于选择最优的物品组合,以最大化总价值,同时不超过背包的重量限制。 假设我们有一组 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 物品,每个物品都有一个重量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miw</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:math> 和一个价值 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miv</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:math> 和一个最大重量容量为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miW</mml:mi></mml:math>的背包。目标是最大化背包中物品的总价值,使得 以下条件成立:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miV</mml:mi>mml:mo=</mml:mo>mml:mrowmml:mrow<mml:mi mathvariant="normal">max</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:mrowmml:munderover<mml:mo stretchy="false">∑</mml:mo>mml:mrowmml:mii</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:munderover>mml:mrowmml:msubmml:mrowmml:miv</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:mrow></mml:mrow></mml:mrow></mml:mrow>mml:mo×</mml:mo>mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:math>

受限于:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mrowmml:munderover<mml:mo stretchy="false">∑</mml:mo>mml:mrowmml:mii</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:munderover>mml:mrowmml:msubmml:mrowmml:miw</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:mrow></mml:mrow>mml:mo×</mml:mo>mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:mo≤</mml:mo>mml:miW</mml:mi></mml:math>

根据我们如何定义 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:math>,我们会遇到以下几种类型的 背包问题:

  • 如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:mo∈</mml:mo>mml:mo{</mml:mo>mml:mn0,1</mml:mn>mml:mo}</mml:mo></mml:math>,这个问题被称为 0/1 背包问题。 这是我们将在 本章集中解决的问题。

  • 如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:mo∈</mml:mo>mml:mo{</mml:mo>mml:mn0,1</mml:mn>mml:mo,</mml:mo>mml:mn2</mml:mn>mml:mo,</mml:mo>mml:mo…</mml:mo>mml:mo,</mml:mo>mml:mic</mml:mi>mml:mo}</mml:mo></mml:math>,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mic</mml:mi></mml:math> 是一个常数, 这个问题被称为 有界背包问题 (BKP)

  • 如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:math> 可以是任何非负整数,那么 该问题称为 无限背包问题 问题 (UKP).

首先,让我们 通过一种朴素的方法来分析 0/1 背包问题的复杂度。 给定 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 个物品,一种直接的解决方案是生成这些 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 物品的所有可能子集,计算每个子集的对应重量和价值,然后确定最优解。 这种方法的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>,使得该问题成为非多项式问题,对于较大的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>值来说,计算上是不可行的。

目标是使用动态规划方法来解决 0/1 背包问题。 背包问题具有最优子结构和重叠子问题的特性,使其成为动态 规划解决方案的理想选择。

最优子结构意味着问题的最优解可以看作一个背包问题,包含 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 项和容量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miW</mml:mi></mml:math>。假设我们已经有了该问题的最优解。 如果我们从这个最优解中去除最后一项,那么剩下的项必须形成一个背包问题的最优解,包含 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:math> 项和容量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miW</mml:mi></mml:math> 减去被去除项的重量。

换句话说,较大问题(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 项,容量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miW</mml:mi></mml:math>) 的最优解由一个较小子问题(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:math> 项,减少后的容量) 的最优解加上是否包括第 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mit</mml:mi>mml:mih</mml:mi></mml:mrow></mml:msup></mml:math> 项的决策组成。 这一性质使得我们可以将问题分解为更小的子问题,并递归地求解它们,这是动态 规划方法的基础。

例如,如果我们 已经得到了一个包含 5 个物品和容量为 12 的背包问题的最优解,并且我们去除第五个物品,那么剩下的 4 个物品必须形成一个包含 4 个物品且容量减少了第五个物品重量的最优解。

重叠的 子问题在这个问题中指的是在寻找最优解的过程中,同样的子问题被多次解决的情况。 在使用递归方法解决 0/1 背包问题时,我们常常会发现相同的子问题被重复解决。 具体来说,当决定是否将某个物品放入背包时,我们实际上是在解决相同的问题,只是背包容量减少了或 物品数量减少了。

考虑一个包含三个物品和背包容量为 W=5的简单例子。每个物品都有一个重量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miw</mml:mi>mml:mo_</mml:mo>mml:mii</mml:mi></mml:math> 和一个 价值 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miv</mml:mi>mml:mo_</mml:mo>mml:mii</mml:mi></mml:math>

  • 物品 1: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miw</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo>mml:mn2</mml:mn>mml:mo,</mml:mo>mml:msubmml:mrowmml:miv</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo>mml:mn3</mml:mn></mml:math>

  • 物品 2: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miw</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo>mml:mn3</mml:mn>mml:mo,</mml:mo>mml:msubmml:mrowmml:miv</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo>mml:mn4</mml:mn></mml:math>

  • 项目 3:<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miw</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo>mml:mn4</mml:mn>mml:mo,</mml:mo>mml:msubmml:mrowmml:miv</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo>mml:mn5</mml:mn></mml:math>

递归解决这个问题时,我们可能会遇到以下子问题:

  • 包括项目 1:这会导致解决剩余容量为的子问题 W=3 和剩余的项目(项目 2 和 项目 3)

  • 不包括项目 1:这会导致解决带有完全容量的子问题 W=5 和剩余的项目(项目 2 和 项目 3)

然而,当我们考虑项目 2 时,你将再次遇到 相同的子问题:

  • 包括项目 2:解决容量为的问题 W=2 (如果包含项目 1) 或 W=3 (如果不包含项目 1) 和剩余项目(项目 3)

  • 不包括项目 2:解决当前容量的问题,剩余只有项目 3 剩下

正如我们所见,一些子问题(例如,带有的子问题) W=3 和项目 3)会多次出现。 如果没有动态规划,我们将反复解决这些子问题,导致 不必要的计算。

现在我们已经清楚理解了 0/1 背包问题中动态规划的两个基本要素,让我们一步步地走过 解决方案:

  1. 创建 一个二维表格 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mid</mml:mi></mml:math> ,其中有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:math> 行和 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miW</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:math> 列,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 表示物品数量, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miW</mml:mi></mml:math> 表示背包的总容量。 d(i,w) 表示使用前 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math> 个物品,且背包容量为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miw</mml:mi></mml:math>时所能获得的最大值。例如, d(4,5) 表示使用前 4 个物品,假设背包最大容量为 5 时能达到的最大值,尽管实际容量可能 更大。

  2. 初始化第一行 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mo(</mml:mo>mml:mii</mml:mi>mml:mo=</mml:mo>mml:mn0</mml:mn>mml:mo)</mml:mo></mml:math> 和第一列 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mo(</mml:mo>mml:miw</mml:mi>mml:mo=</mml:mo>mml:mn0</mml:mn>mml:mo)</mml:mo></mml:math>,这意味着 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mid</mml:mi>mml:mo(</mml:mo>mml:mii</mml:mi>mml:mo,</mml:mo>mml:mn0</mml:mn>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:mn0</mml:mn></mml:math> 对于所有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mid</mml:mi>mml:mo(</mml:mo>mml:mn0</mml:mn>mml:mo,</mml:mo>mml:miw</mml:mi>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:mn0</mml:mn></mml:math> 对于所有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miw</mml:mi></mml:math>. 假设我们有三个物品,其重量和价值如下: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miw</mml:mi>mml:mo=</mml:mo>mml:mo{</mml:mo>mml:mn1,3</mml:mn>mml:mo,</mml:mo>mml:mn4</mml:mn>mml:mo}</mml:mo></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miv</mml:mi>mml:mo=</mml:mo>mml:mo{</mml:mo>mml:mn15,20,30</mml:mn>mml:mo}</mml:mo></mml:math> 并且背包容量是 6。 (见 表 10.2)。

    <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miw</mml:mi></mml:math>
    <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mii</mml:mi></mml:math>
    0 0
    1 0
    2 0

    | 3 | 0 | | | | | | |

表格 10.2:初始化表格 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">D</mml:mi></mml:math>

  1. 通过以下规则填充表格: 以下规则:

    对于每个项 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math> 重量 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miw</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:math>

    • 如果新项的重量超过当前重量限制,我们将排除新项: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mid</mml:mi>mml:mo(</mml:mo>mml:mii</mml:mi>mml:mo,</mml:mo>mml:miw</mml:mi>mml:mo)</mml:mo>mml:mo=</mml:mo>mml:mid</mml:mi>mml:mo(</mml:mo>mml:mii</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn>mml:mo,</mml:mo>mml:miw</mml:mi></mml:math>) 如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miw</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:mo></mml:mo>mml:miw</mml:mi></mml:math>

    • 否则,我们有 两种选择:

      • 包含当前 项: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mid</mml:mi><mml:mfenced separators="|">mml:mrowmml:mii</mml:mi>mml:mo,</mml:mo>mml:miw</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:msubmml:mrowmml:miv</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:mo+</mml:mo>mml:mid</mml:mi><mml:mfenced separators="|">mml:mrowmml:mii</mml:mi>mml:mo-</mml:mo>mml:mn1mml:mo,</mml:mo>mml:miw</mml:mi>mml:mo-</mml:mo>mml:msubmml:mrowmml:miw</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:mrow></mml:mfenced></mml:math>

      • 排除当前 项: d(i,w)=d(i−1,w)

    选择这两个值中的最大值:

    <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mid</mml:mi><mml:mfenced separators="|">mml:mrowmml:mii</mml:mi>mml:mo,</mml:mo>mml:miw</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mrowmml:mrow<mml:mi mathvariant="italic">max</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrow<mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miv</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:mo+</mml:mo>mml:mid</mml:mi><mml:mfenced separators="|">mml:mrowmml:mii</mml:mi>mml:mo-</mml:mo>mml:mn1mml:mo,</mml:mo>mml:miw</mml:mi>mml:mo-</mml:mo>mml:msubmml:mrowmml:miw</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:mrow></mml:mfenced>mml:mo,</mml:mo>mml:mid</mml:mi><mml:mfenced separators="|">mml:mrowmml:mii</mml:mi>mml:mo-</mml:mo>mml:mn1mml:mo,</mml:mo>mml:miw</mml:mi></mml:mrow></mml:mfenced></mml:mrow></mml:mfenced></mml:mrow></mml:mrow></mml:math> 如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miw</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:mo≤</mml:mo>mml:miw</mml:mi></mml:math>

  2. 生成 解。 该解位于 d(n,W),表示通过整个物品集合和完整背包容量可以达到的最大值。 表 10.3 显示了完整的表格, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">D</mml:mi></mml:math>,最终的解在右下角被突出显示。 最大值为 45,表示仅选择物品 1 和物品 3。

    <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miw</mml:mi></mml:math>
    <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mii</mml:mi></mml:math>
    0 0
    1 0
    2 0

    | 3 | 0 | 15 | 15 | 20 | 30 | 30 | 45 |

表 10.3:完成的表格 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">D</mml:mi></mml:math>。最终结果已突出显示

以下是使用动态规划解决 0/1 背包问题的 Python 实现:

 def dp_knapsack(weights, values, W):
  n = len(weights)
  d = [[0 for _ in range(W + 1)] for _ in range(n + 1)]
  for i in range(n + 1):
    for w in range(W + 1):
      if i == 0 or w == 0:
        d[i][w] = 0
      elif weights[i - 1] <= w:
        dp[i][w] = max(values[i - 1] + d[i - 1][w - weights[i - 1]], d[i - 1][w])
      else:
        d[i][w] = d[i - 1][w]
  return d[n][W]
weights = [2, 3, 4]
values = [3, 4, 5]
W = 5
result = knapsack(weights, values, W)
print("Maximum value:", result)

正如我们 之前所展示的,解决 0/1 背包问题的朴素分治法导致指数级的 时间复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:msupmml:mrowmml:mn2</mml:mn></mml:mrow>mml:mrowmml:min</mml:mi></mml:mrow></mml:msup></mml:math>),因为它会探索每一种可能的物品组合,使得对于大数据集来说不切实际。 另一方面,动态规划方法只解决每个子问题一次,并将结果存储在表格中,从而导致时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:miW</mml:mi>mml:mo)</mml:mo></mml:math>。空间复杂度也是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:miW</mml:mi>mml:mo)</mml:mo></mml:math> ,因为需要存储 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><mml:mi mathvariant="bold-italic">D</mml:mi></mml:math> 表格。

动态规划的局限性

在前面的章节中,我们通过两个例子和复杂度分析演示了动态规划的诸多优点。 以下是 这些优点的总结:

  • 最优解:动态规划保证在存在重叠子问题和最优子结构的问题中找到最优解。 通过系统地解决并存储子问题的解,动态规划确保最终解是 最佳可能解。

  • 效率:通过避免重计算重叠子问题,动态规划将许多问题的时间复杂度从指数级降低到多项式级,使得解决大规模问题成为可能,这些问题使用 其他方法是不可行的。

  • 多功能性:动态规划可应用于广泛的问题领域,包括但不限于优化问题如背包问题、最短路径问题和序列比对。 它是解决各种组合、概率和 确定性问题的强大工具。

  • 空间与时间的权衡:动态规划通常允许在空间和 时间复杂度之间进行权衡。 例如,通过存储中间结果,可以降低时间复杂度,但以增加空间使用为代价。 在某些情况下,还可以应用空间优化技术以减少 空间需求。

然而,这些优势伴随着某些缺点 和局限性:

  • 高空间复杂度:动态规划的主要缺点之一是可能具有高空间复杂度。 存储所有子问题的解可能需要大量内存,尤其是对于输入规模或维度较大的问题,在 内存受限的环境中可能是不可行的。

  • 复杂的实现:与贪婪算法或分治法等简单方法相比,动态规划的实现可能更为复杂。 正确定义子问题、识别递归结构以及管理动态规划表格的需要,可能使实现变得具有挑战性,特别是对于 复杂的问题。

  • 问题特定:动态规划并非通用解决方案,只适用于展示重叠子问题和最优子结构的问题。 对于不符合这些条件的问题,动态规划可能不提供任何优势,甚至 可能效率低下。

  • 难以识别子问题:在某些情况下,确定适当的子问题并构建动态规划解的递推关系可能并不容易。 这需要对问题有深刻的理解,这可能是有效应用动态 规划的障碍。

  • 表格管理的开销:特别是在具有多个维度或状态的复杂问题中,管理动态规划表格可能会增加额外的开销和复杂性。 如果不 有效管理,这也可能导致增加的计算开销。

动态规划 是一种强大的技术,能够显著降低时间复杂度,并确保广泛问题的最优解。 然而,它也有权衡,特别是在空间复杂度和实现复杂度方面。 理解何时以及如何应用动态规划是充分利用其优势,同时减少其局限性的关键。 在本章的下一节也是最后一节中,我们将介绍贪心算法。 虽然它们可能无法完全解决动态规划的所有局限性,但它们在解决更广泛问题时提供了更大的灵活性。

贪心算法——简介

在本章的开始 我们强调了分治算法和动态规划之间的一个关键区别:尽管两者都利用最优子结构,分治算法通常不涉及重叠子问题。 动态规划在存在重叠子问题时特别有效,因为它通过存储和重用这些子问题的解决方案避免了冗余计算。

但如果我们无法为当前问题定义最优子结构,会发生什么呢? 在这种情况下,我们转向另一类算法,即贪心算法。 贪心算法 采用了与问题求解的根本不同的方法。 与动态规划通过最优地解决子问题逐步构建解决方案不同,贪心算法基于每一步看似最佳的选择做出一系列决策,期望这些局部最优的决策最终会导致全局最优解。

贪心算法的关键特征 如下:

  • 局部最优选择:贪心算法通过选择每一步看似最好的选项来做出决策,而不考虑该选择的全局后果。

  • 没有重叠子问题:贪心算法不需要重叠的子问题。 相反,它们最适用于每个选择互不依赖的问题。

  • 简单实现:由于贪心算法通常涉及直接、顺序的决策,因此相比于 动态规划,它们更容易实现,且在时间复杂度上更高效。

让我们解释一个经典的优化问题中的贪心算法。

旅行商问题

旅行商问题 (TSP)是计算机科学和运筹学中的经典挑战。 给定一组位置和它们之间的距离,目标是确定一条最短的路径,经过每个位置一次并返回起点。 TSP 以其计算复杂性著称,因为找到确切的最优解需要检查所有可能的城市访问排列,对于大量城市来说,这是不可行的。 然而,贪心算法提供了一种更简单的解决方法,尽管它不一定是最优的。

贪心算法背后的核心概念是使用启发式方法。 在下一节中,我们将讨论启发式方法的详细信息。 现在,让我们暂停一下,专注于使用贪心方法解决 TSP。

解决 TSP 的常见贪心启发式算法之一是最近邻算法。 步骤如下:

  1. 从一个随机城市开始:选择一个任意城市作为起点。

  2. 访问最近的未访问城市:从当前城市出发,访问尚未访问的最近城市。 这一决策是根据当前城市与潜在下一个城市之间的最短距离做出的。

  3. 重复直到所有城市都被访问:继续移动到最近的未访问城市,直到所有城市都已被访问。

  4. 返回起始城市:当所有城市都被访问过后,返回起始城市以完成旅行。

示例 10.4:

让我们考虑一个简化的例子,包含四个城市:A、B、C 和 D。 这些是城市之间的距离:

  • A 到 B = 10

  • A 到 C = 15

  • A 到 D = 20

  • B 到 C = 35

  • B 到 D = 25

  • C 到 D = 30

最近邻算法的步骤如下:

  1. 城市 A 开始。

  2. 访问最近的城市:从 A 出发,最近的城市是 B(距离 = 10)。

  3. 移动到 B 城市:从 B 出发,最近的未访问城市是 D(距离 = 25)。

  4. 移动到 D 城市:从 D 出发,最近的未访问城市是 C(距离 = 30)。

  5. 移动到 C 城市:现在所有城市都已访问。 最后,返回起始城市 A(距离 = 15)。

  6. 使用贪心算法得到的最终旅游路径是 A → B → D → C → A,总距离是 10 + 25 + 30 + 15 = 80 单位。

启发式方法及其在贪心算法中的作用

术语 启发式方法 源自拉丁词 heuristicus该词本身源自希腊词 heuriskein,意思是 发现启发式方法被广泛应用于各个领域。 在这里,我们将重点讨论它们在问题解决和 人工智能中的应用。

启发式方法是解决问题的策略或技巧旨在快速高效地产生解决方案,尽管不一定是最优的。 启发式方法在复杂问题中尤其有用,在这些问题中,找到准确的解决方案可能需要过多的时间或计算能力。 与其穷举所有可能的解决方案,启发式方法采用经验法则、教育性猜测或直觉策略来生成一个在合理时间内的解决方案, 足够好 而且通常是可接受的。

贪心算法是一类常常依赖启发式方法做决策的算法。 在贪心算法中,策略是基于当前信息在每一步做出最佳选择,期望这将导致一个最优或近似最优的解决方案。 每一步的 最佳选择 是由 启发式方法决定的。

让我们讨论贪婪算法中启发式方法的工作原理。 在贪婪算法中,启发式方法帮助算法决定在每个步骤中选择哪个选项。 这个决定基于局部信息,意味着算法不考虑整个问题,而是专注于当前步骤。 在使用最近邻居算法的 TSP 中,启发式方法是 选择最近的未访问城市。这个决定基于当前城市到其他城市的距离,而不考虑 整体路径。

启发式方法 通过提供一条简单快速的规则来指导算法。 这个规则基于特定问题的特征设计,旨在导致一个好的解决方案。 让我们在找零问题中详细探讨一下。 找零问题是一个经典的优化问题,目标是使用给定的硬币面额确定达到特定金额所需的最少硬币数量。 例如,如果我们有面额为 1、5 和 10 的硬币,需要凑出 12 元,挑战在于找到最小化使用硬币总数的组合。 这个问题可以使用各种方法来解决,包括贪婪算法、动态规划或递归,具体取决于硬币的具体面额。 对于这个问题,贪婪启发式方法可能会 总是先选择最大面额的硬币。这条简单规则易于遵循,并且通常能够快速找到 解决方案。

启发式方法还 使得贪心算法能够快速找到解,即使是在复杂的问题中。 然而,这种速度是以可能错过最优解为代价的。 启发式方法在快速找到解和找到最佳解之间提供了平衡。 让我们讨论一下霍夫曼编码算法中的这种行为。 霍夫曼编码是一种用于无损数据压缩的贪心算法。 它根据输入数据中字符的频率为字符分配可变长度的编码,频率较高的字符分配较短的编码。 该算法构建了一棵二叉树,称为霍夫曼树,其中每个叶子节点代表一个字符及其频率。 通过遍历这棵树,为每个字符生成唯一的二进制编码,从而最小化编码数据的总长度。 霍夫曼编码广泛应用于文件压缩和编码等领域。 在霍夫曼编码算法中,贪心方法采用启发式策略,总是首先合并两个最不常见的符号,这样可以得到一个高效的编码,尽管不一定是 最优的编码。

启发式方法为贪心算法提供了显著的优势。 它们通过使决策无需探索所有可能的选项,来加速这些算法。 此外,基于启发式的决策通常易于理解和实施,使得贪心算法变得直观。 在许多实际场景中,特别是在人工智能中,快速找到一个近似解往往比需要过多计算时间的精确解更有价值。

然而,启发式方法有其自身的局限性。 因为启发式方法依赖于局部信息,有时会导致次优解;在短期内看似最佳的选择,长期来看可能并非最优。 此外,启发式方法通常是 问题特定的,这意味着对于某个问题有效的启发式方法可能对另一个问题无效。 由启发式方法指导的贪心算法,也可能会陷入局部最优解——即比邻近的替代方案更好的解,但并非全局最佳解。 这可能导致启发式方法错过全局 最优解。

贪心算法 并不总能得到最优解。 在前面讨论的旅行商问题(TSP)示例中,贪心策略可能忽略了需要更多战略决策的较短路径,而不仅仅是每一步选择最近的邻居。 由于贪心算法是启发式的,它提供了一个快速的近似解,但无法保证解是最优的。 此外,贪心策略可能会陷入局部最优解,其中某个看似最好的决策实际上会妨碍实现 全局最优解。

贪心算法最适用于以下场景:

  • 最优子结构不存在。 如果问题没有明确的最优子结构,动态规划可能不适用。 在这种情况下,贪心算法可以提供更直接的解决方案。 例如。

  • 贪心算法适用于特定类型的问题,例如调度、最短路径或资源分配问题,在这些问题中,每一步的局部最优选择能够产生整体 最优解。

  • 可以接受快速的近似解,数据量较小,且计算资源 有限。

  • 问题的背景允许在解的质量与 计算效率之间进行潜在的权衡。

虽然贪心算法提供了一个比动态规划更简单且通常更高效的替代方案,但它并不是万能的。 由于缺乏重叠的子问题和依赖局部优化,贪心算法只能应用于某些类型的问题。 理解何时使用贪心算法与动态规划是算法设计中有效问题求解的关键。

表 10.4 展示了分治法、动态规划与 贪心算法的比较。

特征 分治法 动态规划 贪心算法
问题求解 策略 将问题分解为独立的子问题,递归求解, 合并解决方案 将问题分解为重叠的子问题,一次性求解, 存储结果 通过局部 最优选择逐步构建解决方案
最优 子结构 隐式使用最优 子结构 高度依赖 最优子结构 假设局部最优能导出 全局最优
重叠 子问题 子问题通常是独立的;解决一个子问题不会影响 另一个子问题的解 通过存储和重用结果来处理具有重叠子问题的问题,以避免 冗余计算 通常不涉及重叠子问题。 每个决策是基于当前状态做出的,与 之前的决策无关
使用场景 排序、查找、 数值问题 优化问题、 复杂问题 最小生成树、最短 路径、调度
计算 效率 取决于问题性质, 递归开销 更多占用空间,避免 冗余计算 通常更快,占用更少空间,但可能并不总是 最优的
示例 问题 归并排序、快速排序、 二分查找 背包问题、最长公共子序列、 Floyd-Warshall Kruskal 算法、Dijkstra 最短路径、 活动选择

表 10.4:分治法、动态规划和贪心算法的比较

总结

在本章中,我们探讨了这些算法策略的关键概念和差异,重点讲解了每种方法如何利用最优子结构解决问题。 我们讨论了分治算法如何将问题分解为较小且不重叠的子问题,以及动态规划如何通过存储和重用子问题的解来高效地处理重叠子问题。 本章还涵盖了贪心算法,强调了它们依赖启发式方法在每一步做出局部最优选择,尽管这并不总是能导致全局 最优解。

在本章中,我们提供了像 0/1 背包问题和旅行商问题(TSP)这样的例子,以说明每种方法的优缺点。 我们还探讨了启发式在贪心算法中的作用,指出它们如何实现快速的近似解,但有时可能导致次优解。 在总结讨论时,我们承认根据问题的实际情况选择合适的算法策略的重要性。 在处理当前问题时,算法策略的选择至关重要。

在下一章中,我们将介绍数据结构,它是支持高效算法设计和实现的基础元素。 数据结构在算法设计中的作用不容小觑。

参考书目与进一步阅读

  • 算法导论。 作者:Thomas H. Cormen,Charles E. Leiserson,Ronald L. Rivest,Clifford Stein。 第四版。 MIT 出版社。 2022 年。

    • 第十五章, 动态规划

    • 第十六章, 贪心算法

    • 第三十四章,NP 完全性(用于算法复杂度的比较) 算法复杂度比较

  • 算法设计。 作者:J. Kleinberg 和 É. Tardos。 Pearson 出版。 2006 年。

    • 第四章, 贪心算法

    • 第五章, 分治法

    • 第六章, 动态规划

  • 算法。 作者:S. Dasgupta,C. H. Papadimitriou 和 U. V. Vazirani。 McGraw-Hill 出版。 2008 年

    • 第二章, 分治法

    • 第五章, 贪心算法

    • 第六章, 动态规划

第十四章:第三部分:基础数据结构

本部分探讨了支撑算法性能的核心数据结构。 我们将研究线性和非线性结构,探讨它们的操作、优势和权衡。 理解这些数据结构对于设计高效算法和 优化解决方案至关重要。

本部分包括以下章节:

  • 第十一章**, 数据结构的全景

  • 第十二章**, 线性数据结构

  • 第十三章**, 非线性数据结构

第十五章:11

数据结构的概貌

算法与数据之间的关系是设计高效软件程序的基础。 数据结构的选择直接影响算法的性能,因为基础的数据结构可能需要资源密集型的操作,如搜索、插入和删除,从而可能导致软件执行效率低下。 相比之下,更高级的数据结构可以简化这些操作,降低复杂性,并显著提升整体算法性能。 理解这种关系是优化软件程序速度和资源使用的关键。 软件程序。

在本章中,我们将探索数据结构的各个方面,包括它们作为线性和非线性类型的分类,以及静态和动态数据分配之间的区别。 我们还将讨论数据结构所支持的基本操作,例如搜索、插入和删除,并探讨不同数据结构下这些操作的效率差异。 通过研究这些特性,本章旨在提供一个全面的理解,帮助选择和实现最符合特定算法需求的数据结构,从而实现更高效、更有效的 软件设计。

我们将在 本章中涵盖以下主要内容:

  • 数据结构的分类 数据结构

  • 抽象 数据类型

  • 字典

数据结构的分类

理解 并设计算法而不考虑它们操作的数据是一个不完整的过程。 第一章中,我们探讨了计算硬件与算法之间的独特关系。 同样,算法与数据之间也存在着重要的联系。 一个高效的算法在很大程度上依赖于使用合适且高效的数据结构。 因此,在本章及接下来的两章中,我们将从算法设计的角度,专注于数据结构。 虽然数据结构是一个广泛且复杂的领域,值得详细研究,但我们在这里以及接下来的两章中的重点将是它们在 算法效率中的关键作用。

首先,理解评估和衡量数据结构效率与特征的关键要素至关重要。 了解这些要素很重要,因为选择合适的数据结构直接影响算法的性能和有效性。 当设计一个算法时,选择合适的数据结构不仅仅是个人喜好问题,而是算法 特定需求所决定的必要条件。

让我们首先定义一下,在算法的背景下什么是数据结构。 数据结构是一种系统化的方式,用于组织、管理和存储计算机中的数据,以便高效地访问和修改。 数据的组织并非随意的;它是专门设计来支持特定类型的操作,这些操作对算法和 计算机程序的性能至关重要。

在算法领域,数据结构是算法运行的基础。 它决定了数据如何存储、如何检索,以及如何在程序执行过程中进行操作。 算法的效率通常取决于底层数据结构的有效性。 无论任务涉及搜索、排序、插入还是删除数据,数据结构的选择都可能极大地影响算法的速度和资源消耗。

本质上,数据结构不仅仅是数据组织的一种方法;它是影响算法效率、可扩展性和整体性能的关键组成部分,尤其是在 计算机科学中。

现在的关键问题是,哪种数据结构最适合特定的计算任务和算法? 本节将致力于解答这个问题。 在本节中,我们将介绍定义数据结构效率的标准。 这些标准将指导我们探索各种数据结构及其对不同类型算法的适用性。 接下来,讨论将转向全面考察数据结构如何分类。 这种数据结构的分类法将帮助我们理解不同类型的数据结构及其适用场景,为选择最合适的数据结构提供结构化的思路。 从而应对特定的算法挑战。

物理数据结构与逻辑数据结构

在数据 结构中,区分物理数据结构和逻辑数据结构非常重要。 物理数据结构 指的是数据在计算机内存中的实际组织和排列。 在这种情况下,数据项在内存中的物理位置决定了它们之间的物理关系。 当算法执行时,它处理的数据以反映其 物理结构的方式存储在计算机内存中。

物理数据结构直接关系到数据在内存中的布局方式,包括内存分配、指针以及硬件级别上数据的检索或修改方式。 数据的物理组织方式可以显著影响算法的性能,特别是在速度和 内存效率方面。

相反,逻辑数据结构 指的是算法或程序员所感知的数据的抽象组织。 这是一个概念模型,定义了数据如何相关,而不考虑其物理存储方式。 逻辑数据结构的例子包括数组、链表、树和图。 这些结构由可以在其上执行的操作和数据项之间的关系来定义,而不是它们在内存中的物理位置。

当算法执行时,它操作这些逻辑数据结构。 然而,为了使算法能够正常运行,必须在逻辑结构和存储数据的物理内存之间建立映射。 这种映射由系统程序和内存管理子系统的组合管理,这些是操作系统的重要组成部分。 这些系统确保算法使用的逻辑数据结构能够有效地转换为适当的物理数据结构 在内存中。

随着计算机算法和模型抽象层次的提高,对数据物理表示的依赖逐渐减少。 在这些高级层次上,关注点从数据如何在内存中物理存储转移到如何在逻辑上结构化和操作数据以达到 期望的结果。

本质上,高级算法的设计是为了处理数据,而无需关心底层的物理数据表示。 这种抽象使得算法设计具有更大的灵活性和通用性,因为相同的逻辑结构可以在不同的物理系统上实现,而无需改变 算法本身。

在本书中,我们专注于在逻辑层面研究数据结构。 这种方法使我们能够集中于数据的抽象组织和可以对其执行的操作,而不依赖于物理存储的具体细节。 通过这种方式理解数据结构,我们可以开发出更具通用性和鲁棒性的算法,这些算法适用于不同的计算环境,无论底层硬件或 内存架构如何。

总之,物理数据结构关注的是数据在内存中的实际存储,而逻辑数据结构则提供了一种抽象的方式来组织和操作数据。 算法的效率往往取决于这两个方面的管理 和协调。

原始数据结构与复合数据结构

讨论数据结构时,区分原始数据结构和复合数据结构至关重要。 原始数据结构 最基本的数据表示形式。 它们包括基本类型,如整数、浮点数、字符和指针。 这些结构通常由计算机硬件或编程语言直接支持,并且它们是构建更复杂数据结构的基石。 原始数据结构在实现和操作上都很简单,例如赋值、执行算术运算和基本的 输入/输出操作。

相反, 复合数据结构 更为复杂,且由多个原始数据元素组成。 它们被设计用来处理更复杂的数据组织和操作。 复合数据结构的例子包括数组、链表、树和图。 这些结构允许将多个原始数据类型组合成一个更强大的实体,从而实现更复杂的操作,如搜索、排序和 层次化组织。

复合数据结构对于实现需要处理更复杂数据关系的算法至关重要。 例如,树结构可以表示数据项之间的层次关系,而图结构可以表示更复杂的关系网络。 与原始数据结构不同,复合数据结构需要精心设计和管理,以确保数据处理的高效性。 在本书中(第十二章 第十三章),我们的重点是从 逻辑角度理解和利用复合数据结构。

线性数据结构与非线性数据结构

一种重要的 逻辑数据结构分类方法是根据它们的线性性质进行区分。 逻辑数据结构可以分为线性或非线性两种类型,每种类型都有不同的用途,并支持不同类型的 操作。

线性数据结构 是指数据元素按照顺序排列的结构,其中 每个元素与其前后元素相连接,形成一条直线。 线性数据结构的例子包括数组、链表、栈和队列。 在一个线性数据结构中,诸如遍历、插入和删除等操作通常遵循简单的顺序模式。 这种有序的特性使得线性数据结构非常适用于需要按特定顺序处理数据的任务,例如管理队列或执行 简单搜索。

在内存方面,线性数据结构具有以下 优势:

  • 连续内存分配:线性数据结构,如数组,使用连续的内存块,使得由于引用局部性,内存访问更快、更可预测。 这使得缓存内存的利用更加高效,减少了访问 元素的开销。

  • 内存管理简便:因为线性数据结构通常涉及固定大小(如数组)或顺序指针(如链表),所以内存管理较为简单。 与更复杂的结构相比,内存分配和释放更容易实现。

  • 低内存开销:对于如数组这样的结构,由于不需要额外的指针或链接来连接元素,因此内存开销很小,与非线性结构如树 或图相比,内存使用较低。

相反,线性数据结构存在 在内存方面的 局限性:

  • 内存浪费(固定大小限制):在如数组等结构中,内存在创建时为固定数量的元素分配。 如果元素数量少于分配的大小,就会有未使用的内存空间,从而导致 效率低下。

  • 内存重新分配:扩展如数组这样的线性数据结构需要重新分配内存并将元素复制到新的、更大的内存块中,这在时间和空间上都代价高昂。 这种重新分配可能导致内存碎片化 和低效。

  • 链表中的指针开销:在链表中,每个元素存储一个额外的指针指向下一个元素,这增加了总体的内存使用,特别是在处理大量元素时。 这种开销可能抵消通过 动态大小调整获得的一些内存优势。

  • 顺序内存分配用于连续结构:线性结构如数组需要连续的内存块。 如果没有足够的连续空间可用,内存分配可能会失败或变得低效,从而导致潜在的 性能瓶颈。

线性数据结构的 简洁性通常导致实现上的简便性和性能的可预测性。 然而,它们可能并不总是最有效的选择来表示数据元素之间更复杂的关系,因为它们的顺序性质在 某些场景中可能限制灵活性。

非线性数据结构相反, 它们并不以线性顺序组织数据。 相反,数据元素以更复杂的方式连接,形成如树、图和堆等结构。 在非线性数据结构中,每个元素可能与多个元素连接,从而能够表示数据点之间的层次或网络关系。

非线性数据结构特别适合表示具有固有分层结构的数据,例如家谱或文件目录,或者建模网络,例如社交连接或交通路线。 它们为需要管理和查询数据项之间复杂关系的任务提供了更大的灵活性和效率。 然而,它们的复杂性也需要更复杂的 遍历、搜索 和操作算法。

在本书中,我们将从逻辑角度讨论线性(第十二章)和非线性数据结构(第十三章)的使用,重点是它们如何支持不同类型 的算法。

静态与动态内存分配

另一种 重要的分类基于内存分配数据的方式。 这种分类将数据结构分为静态数据分配和动态数据分配。

静态数据结构 在创建时大小固定的数据结构。 这意味着为数据结构分配的内存量在其生命周期内保持不变。 静态数据结构的一个例子是数组。 当我们声明一个数组时,我们指定它可以容纳的元素数量,这个大小在运行时不能更改。 静态数据结构通常更容易实现和访问,因为它们的内存布局是预先确定的,当数据大小已知时可以实现有效的内存使用

然而,静态数据结构的主要缺点是其缺乏灵活性。 如果实际数据大小超过预定义容量,没有办法在不重新分配内存且潜在移动数据的情况下扩展结构,这可能是低效 和繁琐的。

动态数据结构则允许在运行时调整内存分配。 这意味着数据结构的大小可以根据需要增长或缩小,具体取决于正在执行的操作。 动态数据结构的例子包括链表、树和图。 在动态数据结构中,随着元素的增加,内存会被分配,随着元素的删除,内存会被回收,从而在处理大小不确定或随时间变化的数据时,提供了更大的灵活性和内存使用的高效性。

动态数据结构在无法事先确定数据量或需要高效处理变动数据量的场景中尤其有用。 然而,这种灵活性也意味着在内存分配和指针管理上需要额外的开销,这可能导致实现更加复杂,并且与静态数据结构相比,访问速度可能较慢。 第十二章 第十三章中,我们将探讨静态与动态数据结构,重点分析它们的特点、优势 及其权衡。

顺序访问与随机访问

数据结构的最终分类基于数据元素的访问和检索方式。 这种分类将数据结构分为顺序访问和 随机访问两类。

顺序访问数据结构 是指那些数据元素按特定线性顺序访问的数据结构。 要检索特定元素,必须首先遍历前面的元素。 顺序访问数据结构的例子包括链表和队列。 在这些结构中,访问特定元素通常需要迭代一系列节点或元素,这可能导致检索时间变长,特别是当所需元素位于结构较深的位置时

顺序访问通常用于数据元素顺序重要或数据需要按线性方式处理的场景,例如在流应用程序中,或当数据需要逐个元素处理时。 然而,主要的限制是无法直接访问特定元素,必须遍历整个序列,这对于 大数据集来说可能效率低下。

随机访问数据结构则允许 直接访问任何数据元素,而无需遍历其他元素。 这意味着如果你知道元素的索引或键值,就可以立即检索或修改任何元素。 随机访问数据结构的例子包括数组和哈希表。 例如,在数组中,你可以通过索引直接访问任何元素,从而使得检索和更新等操作 非常快速。

当需要快速访问单个元素时,随机访问特别有益,尤其是在速度至关重要的应用中,如数据库或实时系统。 然而,这种便利性伴随着一些权衡,比如数组需要连续的内存分配,如果数据结构较为稀疏,可能导致内存使用效率低下。

第十二章中,我们将探讨顺序访问和随机访问数据结构,重点关注它们的使用场景、优势和局限性。 理解不同数据结构的访问模式对设计既高效又适合其处理数据类型的算法至关重要。 在选择顺序访问和随机访问时,这一决定可能会显著影响算法的性能,特别是在速度和 资源利用率方面。

抽象数据类型

一个 抽象数据类型 (ADT)是 一种数据类型的数学模型,数据类型由其行为(操作)定义,而非其实现方式。 ADT 封装了数据和可以对该数据执行的操作,抽象化了实现细节。 换句话说,ADT 指定了哪些操作是可能的,以及这些操作的行为,但并不说明这些操作是如何 实现的。

抽象数据类型(ADT)是计算机科学中的基础概念,因为它们允许开发人员以更灵活和模块化的方式操作数据。 通过定义操作而不具体说明实现细节,ADT 使得代码更易于维护,并能适应 不同的应用场景。

ADT 可以根据它们支持的操作和所解决的使用场景分为几种类型。 以下是一些 常见的类型:

  • 列表:列表是 一种表示有序元素集合的抽象数据类型(ADT),其中每个元素在序列中都有一个特定的位置。 列表的主要操作包括插入、删除、访问和遍历元素。 常见的列表实现方式有数组和链表。 数组是一种直接的实现方式,其中元素存储在连续的内存位置中,能够通过索引快速访问。 与此不同,链表是一种更灵活的实现方式,每个元素(或节点)指向下一个元素,允许动态调整大小,并且更容易进行元素的插入或删除。 列表常用于管理有序的项集合,例如学生名单、待办事项列表或歌曲播放列表。 数组和链表将会在 第十二章中讨论。

  • :栈是 一种基于 后进先出 (LIFO)原则的抽象数据类型(ADT),其中 最近添加的元素是第一个被移除的。 栈的主要操作包括 压栈 (将元素添加到栈顶)和 弹栈 (移除栈顶元素)。 栈的一个常见示例是调用栈,编程语言通过调用栈来跟踪函数的调用与返回。 另一个常见的使用案例是文本编辑器中的撤销机制,其中栈用于回退到先前的状态。 栈在编译器中用于解析表达式、实现回溯算法(例如解决迷宫或谜题),以及管理递归中的嵌套函数调用等方面也至关重要。 栈将在 第十二章中进行回顾。

  • 队列(Queue):一个 队列是一个抽象数据类型(ADT),它遵循 先进先出( FIFO )原则,意味着最先加入的元素是最先被移除的。 队列的主要操作是 入队 (将元素添加到队列中)和 出队 (从队列中移除元素)。 队列在软件编程和计算机系统中有广泛的应用,比如在打印队列中管理打印任务,文档按照接收的顺序进行处理,以及操作系统中的任务调度,进程按队列顺序执行。 第十二章中,我们将回顾队列的主要特性。

  • 双端队列(Deque):一个 双端队列是一个抽象数据类型(ADT),允许元素从序列的两端插入和删除,实际上是对栈和队列的通用化。 例如, 标准模板库(STL) 中 C++的双端队列实现提供了一个灵活的序列容器,可以在两端动态增长和缩小。 另一个例子是循环缓冲区,它是一个常用于数据周期性添加和删除的双端队列实现。 双端队列在实现诸如文本编辑器中的撤销/重做功能等软件应用中特别有用。 它们在更复杂的应用中也发挥着重要作用,例如在算法中管理滑动窗口问题。 双端队列将在 第十二章中详细讨论。

  • 集合:集合 是一个抽象数据类型(ADT),表示一组唯一元素,其中元素的顺序不重要。 集合的主要操作包括插入、删除、成员检查以及集合运算,如并集、交集和差集。 集合实现的例子包括 HashSet ,它在 Java 中确保集合中没有重复元素,以及 集合 ,它在 Python 中支持多种数学运算,如并集和交集。 集合广泛应用于诸如管理唯一项目集合(例如,某课程注册学生名单)、实现要求唯一性的操作(例如,从列表中删除重复项)以及进行数学 集合运算等场景。

  • 字典:字典,也 称为映射或关联数组,是一种将数据存储为键值对的抽象数据类型(ADT),其中每个键都是唯一的,并与特定的值相关联。 主要操作包括插入新的键值对、删除键值对、根据键查找值,有时还包括遍历键或值。 字典实现的例子包括 HashMap ,它在 Java 中使用唯一键快速检索值,以及 字典 ,它在 Python 中提供了一种将键映射到值的多功能方式。 字典广泛应用于各种场景,例如实现查找表、管理配置设置(每个设置通过唯一键标识)以及通过键存储和检索数据,例如通过用户 ID 索引的用户个人资料。 关于字典的详细讨论请参见 下一节。

  • : 图是 一种抽象数据类型(ADT),表示由节点(称为顶点)组成的集合,节点之间通过边连接。 图可以是有向图,其中边具有特定方向,或者是无向图,其中边没有方向。 图也可以包含环。 图的常见操作包括添加顶点、添加边和遍历结构。 图通常使用邻接表实现,其中每个节点都有一个邻居节点的列表,或者使用邻接矩阵,这是一种二维数组,表示节点之间是否存在边。 图是 广泛应用于各种科学和工程领域,包括建模社交网络、通信网络和交通系统等网络。 图将在 第十三章中进一步讨论。

  • : 树 是一种层次结构的抽象数据类型(ADT),其中元素以节点的形式排列,从一个根节点开始,根节点向外分支成子节点,形成子树。 树可以有多种类型,例如二叉树,其中每个节点最多有两个子节点,或者更复杂的结构,如 B 树。 示例包括 二叉搜索树 (BST) 和堆。 在二叉搜索树中,每个节点最多有两个子节点,左子节点小于父节点,右子节点大于父节点。 堆是一种专门的树结构,遵循 属性,通常用于实现优先队列。 我们将在 第十三章中讨论树。

抽象数据类型(ADT)使程序员能够专注于可以对数据执行哪些操作,而不是如何实现这些操作。 这种抽象化导致了更加灵活、可维护和可重用的代码。 像列表、栈、队列、集合、映射、图和树等 ADT 是计算机科学的基础,每种 ADT 根据数据的性质和所需操作的不同,服务于特定的用例。

现在我们已经了解了数据结构分类和抽象数据类型,下一部分将重点介绍可以对数据结构执行的主要操作。 我们将从介绍一种抽象数据结构开始,称为 字典

字典

字典 是一种抽象数据结构,旨在以键值对的形式存储数据。 在字典中,每个元素由一个唯一的键组成,该键用于访问关联的值。 键值对使得查找变得快速,这是使用字典的主要优势之一。 字典也常被称为映射、关联数组或 符号表。

尽管我们的重点 主要放在逻辑数据结构上,但值得注意的是,字典也可以通过所谓的关联 存储器,或者 内容寻址 存储器 (CAM)直接在硬件中实现。

关联存储器用于超高速搜索操作。 正如你所料,使用关联存储器时,任何搜索操作都可以在恒定时间内执行, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>。尽管使用关联存储器可能在通用算法应用中显得昂贵且不切实际,但在软件中更可行的做法是使用哈希。 哈希,在 第七章中有所讨论,提供了一种实现内容寻址数据结构的算法方法,允许高效的查找和 其他操作。

字典的主要 特性包括 以下几点:

  • 在大多数实现中,字典不会维护键值对的特定顺序。 元素是根据内部使用的 哈希 函数以任意顺序存储的。

  • 字典中的每个键都是唯一的。 如果你试图插入一个新键值对,且其键在字典中已存在,现有的值通常会被 新值覆盖。

  • 字典的主要操作——如插入、删除和访问元素——通常在平均情况下以 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math> 时间完成,这得益于底层哈希表的实现。 这使得字典在需要快速查找的大型数据集上非常高效。

问题是如何 实现和维护字典。 幸运的是,许多编程语言都提供了对字典的内置支持。 这是一个简单的例子,展示了字典如何在 Python 中使用:

 # Creating a dictionary to store information about a student
student_info = {
    "name": "John Doe",
    "age": 21,
    "major": "Computer Science",
    "GPA": 3.8
}
# Accessing values using keys
print("Name:", student_info["name"])  # Output: Name: John Doe
print("Age:", student_info["age"])  # Output: Age: 21
# Adding a new key-value pair
student_info["graduation_year"] = 2024
print("Graduation Year:", student_info["graduation_year"])  # Output: Graduation Year: 2024
# Updating an existing value
student_info["GPA"] = 3.9
print("Updated GPA:", student_info["GPA"])  # Output: Updated GPA: 3.9
# Deleting a key-value pair
del student_info["major"]
print("After deletion:", student_info)  # Output: {'name': 'John Doe', 'age': 21, 'GPA': 3.9, 'graduation_year': 2024}
# Iterating over the dictionary
for key, value in student_info.items():
    print(f"{key}: {value}")

这个例子 展示了字典在 Python 中的基本操作,展示了如何轻松创建、操作和访问数据,充分发挥这种强大数据结构的优势。 让我们简要解释一下 代码:

  • 创建字典:我们定义一个字典 student_info,其中包含表示学生各种属性的键值对。

  • 访问值:我们使用 name age 键,通过方括号访问对应的值。

  • 添加键值对:我们添加一个新的键 graduation_year,并为其指定相应的值。

  • 更新值:我们更新与 GPA 键相关联的值。

  • 删除键值对:我们使用 del 语句删除 主要 键及其值。

  • 遍历字典:我们使用 for 循环遍历字典,打印每个 键值对。

正如我们在前面的例子中所展示的,字典不仅仅是键值对的集合;它们还支持一系列基本操作,允许高效的数据管理。 这些基本操作包括插入、查找、更新(或编辑)和删除。 让我们通过 Python 示例详细探索每个操作的实现。

插入

插入 涉及 将一个新的键值对添加到字典中。 如果键已经存在,则更新该键所关联的值;否则,创建一个新的条目。 以下是一个简单字典的插入函数, 用 Python 编写:

 # Creating an empty dictionary
student_grades = {}
# Inserting key-value pairs into the dictionary
student_grades["Alice"] = 85
student_grades["Bob"] = 90
print(student_grades)  # Output: {'Alice': 85, 'Bob': 90}

在此示例中,我们创建一个空字典,名为 <st c="30656">student_grades</st> 并插入两个键值对 – <st c="30704">Alice: 85</st> <st c="30718">Bob: 90</st>。字典现在包含这些值,新的 条目可以通过类似方式 添加。

搜索

搜索 从字典中检索与特定键相关联的值的操作。 这是字典中最常见且高效的操作之一,通常在常数时间内执行。 让我们看一个简单的搜索函数, 它用于字典中:

 # Searching for a value by its key
alice_grade = student_grades.get("Alice")
print("Alice's grade:", alice_grade)  # Output: Alice's grade: 85
# Searching for a non-existent key
charlie_grade = student_grades.get("Charlie", "Not Found")
print("Charlie's grade:", charlie_grade)  # Output: Charlie's grade: Not Found

在这里, <st c="31382">get</st> 方法被用来搜索与 <st c="31445">Alice</st> 键关联的值。 如果键存在,则返回对应的值。 如果未找到键,例如 <st c="31555">Charlie</st>,可以返回一个默认值(<st c="31581">"未找到"</st>)。 替代返回。

更新

更新 涉及 更改字典中现有键所关联的值。 如果键存在,则其值会被修改;如果键不存在,则会添加一个新的键值对。 以下是一个更新的例子: 例如:

 # Updating an existing key-value pair
student_grades["Alice"] = 88
print(student_grades)  # Output: {'Alice': 88, 'Bob': 90}
# Adding a new key-value pair through update
student_grades["Charlie"] = 92
print(student_grades)  # Output: {'Alice': 88, 'Bob': 90, 'Charlie': 92}

在此示例中, <st c="32175">Alice</st> 键相关联的值从 85 更新为 88。 此外,一个新的键值对, <st c="32247">Charlie: 92</st>,被添加到 字典中。

删除

删除 移除 字典中的一个键值对。 一旦移除,键及其关联的值将不再存在于字典中。 看看下面的 例子:

 # Deleting a key-value pair by key
del student_grades["Bob"]
print(student_grades)  # Output: {'Alice': 88, 'Charlie': 92}
# Attempting to delete a non-existent key (optional approach)
removed_grade = student_grades.pop("David", "Key not found")
print(removed_grade)  # Output: Key not found

在这个例子中, <st c="32783">del</st> 语句用于从字典中删除 <st c="32819">"Bob"</st> 键及其相关的值。 此外, <st c="32893">pop</st> 方法用于尝试删除 <st c="32937">David</st> 键,而该键并不存在于字典中,返回默认消息: <st c="33017">键</st> <st c="33021">未找到</st>

这些基本操作——插入、查找、更新和删除——对于在任何编程语言中使用字典至关重要。 它们允许高效地管理数据,并使你能够创建动态且适应性强的程序。 无论是添加新数据、检索已有信息、修改值,还是删除条目,这些功能都使字典成为算法设计和日常编程任务中一个多功能且强大的工具。 程序任务。

第十二章 第十三章中,我们将介绍各种数据结构,并评估它们在这些基本操作中的表现。 此外,某些数据结构包括扩展功能和特定操作,如在二叉搜索树(BST)中查找后继和前驱。

总结

本章探讨了数据结构的基础概念,重点讨论了它们的分类及可执行的基本操作。 本章讨论了物理数据结构和逻辑数据结构之间的区别,以及原始数据结构和复合数据结构之间的区别。 它还涵盖了线性数据结构与非线性数据结构之间的差异,以及静态与动态数据分配、顺序访问与随机访问的影响。 通过这些讨论,强调了为特定算法任务选择合适数据结构的重要性。

本章还提供了关于字典的工作原理的见解,重点介绍了它们的关键操作,如插入、查找、更新和删除,并通过示例演示它们的实际应用。 讨论强调了字典作为一个多功能且高效的工具,在 算法设计中的作用。

随着我们继续前进,下一章将深入探讨线性数据结构,包括数组和链表,并详细分析它们的属性和应用场景。

参考资料与进一步阅读

  • 算法导论 作者:Thomas H. Cormen,Charles E. Leiserson,Ronald L. Rivest,和 Clifford Stein。 第四版。 MIT 出版社。 2022:

    • 第十章 基础 数据结构
  • 算法 作者:R. Sedgewick,K. Wayne。 第四版。 Addison-Wesley。 2011:

    • 第一章 基础知识
  • C++中的数据结构与算法分析 作者:Mark A. Weiss。 第四版。 Pearson。 2012:

    • 第三章 列表、栈, 队列

第十六章:12

线性数据结构

在本章中,我们将探索 线性数据结构的基础概念,这些概念在计算机科学和算法设计中发挥着 至关重要的作用。 我们将从理解数组和链表的基本知识开始,学习这些结构如何存储和管理数据。 本章将引导你了解这些结构上的关键操作,如插入、删除和查找,并通过分析它们的时间复杂度来理解它们的效率。 通过比较数组和链表,你将更好地理解在特定应用中选择合适数据结构时的权衡。 应用。

随着我们的深入学习,我们将发现更多高级的线性数据结构,如栈、队列和 双端队列 (双向队列)。 我们 将学习这些结构如何扩展基本列表的功能,以及它们如何应用于实际场景,如任务调度和资源管理。 此外,本章还将介绍 跳表,一种 概率数据结构,在数组的高效性和链表的灵活性之间提供了平衡。 通过本章的学习,你将掌握有效实现和使用这些线性数据结构所需的知识。 结构。

本章将涵盖以下主题:

  • 列表

  • 跳表

  • 队列

  • 双端队列

列表

A 列表 是一个有序的元素集合,可以容纳相同或不同类型的元素,其中每个元素都有索引并在列表中占据特定位置。 列表通常用于存储可以轻松访问、插入或删除的数据序列。 它们可以包含不同类型的元素,尽管在一些编程语言中,列表通常是同质的,意味着所有元素都属于 相同类型。

列表通常通过数组或链式结构来实现,这两种方式在性能和内存使用上有明显不同。 当列表中的元素存储在连续的内存位置时,该列表被称为 数组。在 这种情况下,通过索引访问元素非常高效,通常需要常数时间(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>),因为可以直接计算任何元素的内存位置。 然而,数组一旦创建后大小固定,如果元素的数量频繁变化,就可能导致效率低下,需要创建新的数组并复制 数据。

另一方面,如果列表是通过链式结构实现的,则称为 链表。在链表中,每个元素,称为 一个 节点,包含一个指向序列中下一个节点的引用(或链接)。 这种结构使得列表可以在添加或删除元素时动态增长或缩小,而无需占用大块的连续内存。 然而,由于节点分散在内存中,通过索引访问元素需要从头遍历列表,这可能会很耗时(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 在最坏情况下)。

在选择使用数组还是链表时,取决于应用程序的具体需求,比如插入和删除的频率与快速访问元素的需求之间的权衡。 当快速访问和可预测的内存使用至关重要时,通常优先选择数组,而链表则更适用于需要动态调整大小和频繁修改的场景。 在接下来的小节中,我们将探讨数组的关键特性,特别是在算法效率和可以执行的各种操作方面。

数组

一个 数组 是一个基本的数据结构,由一组元素组成,每个元素通过 至少一个数组索引或键来标识。 数组是计算机科学中最简单且最广泛使用的数据结构之一。 它们通常用于存储相同类型的固定大小的元素序列。 每个元素的位置由其索引定义,通常从零开始。

数组具有以下 定义特征,影响它们的行为 和性能:

  • 固定大小:一旦创建了数组,它的大小就被设置好,无法更改。 这意味着数组能够容纳的元素数量在创建时就已预定。 例如,在大多数编程语言中,我们必须在声明数组时指定数组的大小,例如 int[] a = new int[10]; 在 Java 中,这会创建一个能够容纳 10 个整数的数组。 以下是一个简单的数组声明 在 Python 中的例子:

     # Define an array (list) of integers
    a = [10, 20, 30, 40, 50]
    # Print the array
    print(a)  # Outputs: [10, 20, 30, 40, 50]
    
  • 连续内存分配:数组的元素存储在连续的内存位置中。 这使得通过简单的数学公式计算内存地址,从而高效地访问任何元素成为可能。 例如,在一个一维数组 a ,其大小为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>,元素的地址 a[i] 可以通过以下公式计算: base+i*length,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mib</mml:mi>mml:mia</mml:mi>mml:mis</mml:mi>mml:mie</mml:mi></mml:math> 是基地址, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math> 是数组中元素的索引 a,而 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mil</mml:mi>mml:mie</mml:mi>mml:min</mml:mi>mml:mig</mml:mi>mml:mit</mml:mi>mml:mih</mml:mi></mml:math> 是数组中每个元素的大小。 例如,对于 1 字节的元素,大小为 1;对于 16 位或字的元素,大小为 2,等等。 表 12.1 展示了一个简单的数组示例。

内存地址 FF01 FF02 FF03 FF04 FF05 FF06
内容 23 123 54 67 34 87
索引 0 1 2 3 4 5

表 12.1:一个数组示例

  • 同质元素:数组中的所有元素必须具有相同的数据类型,确保数组是一个统一的集合。 例如,整数数组 int[] 只能存储整数值,而字符串数组 String[] 只能存储 字符串值。

  • 索引访问:数组 允许通过索引直接访问任何元素,提供常数时间的访问,这是这种数据结构的主要优势之一。 访问数组中的第三个元素 a 就像是 a [2]一样简单。

数组 支持多种操作,每种操作都有其独特的性能影响。 以下是 常见的数组操作及其 时间复杂度:

  • 插入:这是指向数组中添加新元素。 例如,考虑 a = [1, 2, 3, 4]。如果有空间,将 5 插入数组的末尾是非常直接的。 然而,若要将 5 插入到索引 1 ,则需要将索引 1 到右边的所有元素移动。 数组插入操作的时间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 在最佳情况下,时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 在最坏情况下,时间复杂度为 最佳情况是在部分填充的数组末尾插入元素。 如果是在数组的开头或中间插入,则需要移动元素,属于最坏情况。 以下是一个 Python 示例:

     a = [1, 2, 3, 4]
    a.insert(1, 5)  # a becomes [1, 5, 2, 3, 4]
    print(a)
    

    由于 Python 使用零索引,因此 <st c="6725">a.insert(1, 5)</st> 操作会将值插入数组的第二个位置。

  • 删除: 删除 是指从数组中移除一个元素。 假设有 a = [1, 2, 3, 4],删除索引 2 处的元素 1 需要将索引 1 之后的所有元素向左移动,以填补空缺。 在最好的情况下,删除最后一个元素的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>。然而,如果我们删除数组开头或中间的元素,则需要将随后的元素移动,导致最坏情况下的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

     a = [1, 2, 3, 4]
    a.pop(1)  # a becomes [1, 3, 4]
    ```</st>
    
    
  • 编辑或更新: 编辑是 修改数组中现有元素的操作。 假设有 a = [1, 2, 3, 4],将索引 2 处的元素 3 改为 5 是一个直接的 操作。 我们可以通过索引直接访问该元素并更新它,因此其时间复杂度为 !<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>:

     a = [1, 2, 3, 4]
    a[2] = 5  # a becomes [1, 2, 5, 4]
    ```</st>
    
    
  • 搜索: 搜索 是指在数组中查找特定元素。 这一主题在 第七章中进行了广泛讨论,讨论的多数搜索算法是基于数组作为底层数据结构的。 第十三章中,我们将探讨如何在非线性数据结构上进行搜索,如树形结构。

  • 访问:访问 指的是在数组中特定索引处检索元素的值。 数组的一个关键优势之一是其访问时间是常数时间(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>),允许直接通过其索引检索任何元素,无需遍历。

数组可以在更复杂的结构中实现。 一个例子是 <st c="8292">列表</st> 在 Python 中)。 这些 数组可以在元素添加到初始容量之外时调整大小。 然而,其基本原理保持不变,数组提供了高效的访问和遍历。 另一个 例子是 多维数组。这些数组可以扩展到多个维度,如 2D 数组(矩阵)或 3D 数组,在图像处理、科学模拟和游戏开发等应用中特别有用。 以下是 Python 中的一个 2D 数组示例

 matrix = [
      [1, 2, 3],
      [4, 5, 6],
      [7, 8, 9]
]
print(matrix[1][2])  # Outputs: 6 (element at second row, third column)

数组 一种基本且高效的数据结构,用于存储和管理元素集合,特别是当集合的大小已知且保持恒定时。 它们的连续内存分配使得访问快速,并且各种操作的实现直接明了。 然而,它们的固定大小和插入、删除可能存在的低效性使得它们相比于其他数据结构如链表而言不够灵活。 了解数组的权衡和适当的使用案例对于有效的算法设计和实现至关重要。

链表

链表 一种 线性 数据结构,其中元素(称为节点)按顺序排列。 与数组不同,链表不在连续的内存位置存储其元素。 相反,链表中的每个节点至少包含两个部分:数据和指向序列中下一个节点的引用(或指针)。 这种结构允许链表在元素添加或删除时动态调整大小,轻松增长或缩小。

链表有几个 关键特点,使其与其他数据结构(如数组)区分开来:

  • 动态大小:链表的大小可以动态增长或缩小,因为节点可以根据需要添加或删除,而无需重新分配或重新组织整个数据结构。 例如,我们可以继续向链表中添加节点,而不必担心预先定义的大小。

  • 非连续内存分配:与数组不同,链表不要求连续的内存位置。 每个节点都独立存储在内存中,并通过指针连接在一起。 例如,在单向链表中,每个节点包含指向下一个节点的指针,这样元素就可以分散存储在内存中。 通过内存分布。

  • 顺序访问:链表必须从头开始顺序访问,因为没有直接通过索引访问特定元素的方法。 例如,要访问链表中的第三个元素,我们必须先遍历前两个节点。

  • 变种:链表 有不同的形式,包括单向链表(每个节点指向下一个节点)、双向链表(每个节点指向下一个节点和上一个节点)以及循环链表(最后一个节点指向第一个节点)。 例如,在双向链表中,由于每个节点都有指向前后节点的指针,因此可以在两个方向上进行遍历。 下一个节点。

链表支持各种操作,每个操作具有特定的性能特征。 让我们回顾一下主要操作及其时间复杂度 和示例。

链表插入

考虑 这个链表: <st c="11577">24</st> <st c="11582">3</st> <st c="11586">12</st> <st c="11591">17</st>。如果我们想将值 <st c="11626">8</st> 插入 <st c="11636">3</st> <st c="11642">12</st>之间,过程包括创建一个新节点,节点值为 <st c="11702">8</st> 并相应地更新指针。 下面是逐步操作: 步骤:

  1. 创建新节点:首先,我们创建一个包含值 8的新节点。初始时,这个新节点的指针被设置为 null,因为它尚未指向任何内容。

  2. 更新新节点的指针:接下来,将新节点的指针设置为指向下一个节点, 3,即包含 12的节点。现在,新节点 3 已连接到 节点 4

  3. 更新前一个节点的指针:最后,更新包含 3 的节点的指针,使其指向新节点 8。这完成了插入,结果是链表 24 3 8 12 17

图 12.1 展示了在 链表中插入新节点的过程。

图 12.1:在三步中向链表添加新节点的过程

图 12.1:在三步中向链表添加新节点的过程

图 12.1中, Link 表示指向 <st c="12710">24</st>的指针。 最后一个节点,称为 <st c="12776">17</st>),指向 <st c="12792">null</st>,表示链表的结束。 我们用 <st c="12856">null</st> 表示一个指向 <st c="12870">/</st> 符号的指针。 新创建的节点的地址标记为 <st c="12932">New</st>,初始时,它指向 <st c="12965">null</st>

这是一个简单的 Python 实现,展示了 上述过程:

 class Node:
    def __init__(self, data):
        self.data = data  # Store data
        self.next = None  # Initialize next as null (None in Python)
class LinkedList:
    def __init__(self):
        self.head = None  # Initialize the head of the list as None
    def insert_after(self, prev_node, new_data):
        if prev_node is None:
            print("The given previous node must be in the LinkedList.")
            return
        new_node = Node(new_data)  # Create a new node with the provided data
        new_node.next = prev_node.next  # Point the new node to the next node (e.g., 4)
        prev_node.next = new_node  # Point the previous node (e.g., 2) to the new node (e.g., 3)
    def print_list(self):
        temp = self.head
        while temp:
            print(temp.data, end=" -> ")
            temp = temp.next
        print("None")

为了测试 Node 类和 LinkedList 类的功能,我们可以使用以下示例:

 if __name__ == "__main__":
    llist = LinkedList()
    # Creating the initial linked list 1 -> 2 -> 4
    llist.head = Node(1)
    second = Node(2)
    third = Node(4)
    llist.head.next = second
    second.next = third
    # Insert 3 between 2 and 4
    llist.insert_after(second, 3)
    # Print the updated linked list
    llist.print_list()

让我们简要解释一下 这段代码:

  • Node:每个 Node 对象存储一个 数据 值和一个 指向下一个节点的 指针,在 链表中。

  • LinkedListLinkedList 类管理链表,包括 插入操作。

  • insert_after:该方法在给定节点(prev_node)后插入新节点。新节点的数据为 new_data ,并且更新指针以正确插入到 链表中。

  • print_list:该方法遍历链表并打印每个节点的数据。

链表中插入操作的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>,最佳情况下,例如在链表开头或结尾插入时,如果位置已知。 最坏情况下,如果需要通过链表遍历特定位置进行插入,时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>

链表中的删除操作

链表中的删除操作涉及删除特定节点。 例如,给定链表 <st c="15054">24</st> <st c="15059">3</st> <st c="15063">12</st> <st c="15068">17</st> ,删除值为 <st c="15117">3</st> 的节点需要通过更新前一个节点(<st c="15187">2</st>)的指针来绕过它(<st c="15218">4</st>)。 与插入类似,链表中删除操作的时间复杂度取决于要删除节点的位置。 最坏情况下,需要遍历查找节点,时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 图 12**.2 展示了示例链表中的删除过程。

图 12.2:链表中删除节点的过程

图 12.2:链表中删除节点的过程

图 12**.3中,顶部部分显示了删除值为 <st c="15707">12</st>的节点之前的链表,底部部分则显示了删除后的链表。 以下是一个 删除链表中节点的示例 Python 代码:

 def delete_node(self, key):
    temp = self.head
    if (temp is not None):
        if (temp.data == key):
            self.head = temp.next
            temp = None
            return
    while(temp is not None):
        if temp.data == key:
            break
        prev = temp
        temp = temp.next
    if(temp == None):
        return
    prev.next = temp.next
    temp = None
# Example usage:
llist.delete_node(3)  # Deletes the node with value 3
llist.print_list()

<st c="16218">delete_node</st> 函数应当添加到 <st c="16262">LinkedList</st> 类中,参见前一节。

在链表中编辑

编辑操作涉及 修改链表中现有节点的数据。 例如,如果我们想将第二个节点的值从 <st c="16472">2</st> 改为 <st c="16477">5</st> ,那么在链表 <st c="16486">1</st> <st c="16490">2</st> <st c="16494">3</st> <st c="16498">4</st> 中,我们需要相应地更新该节点的数据。 这个操作的时间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 因为我们可能需要遍历链表来找到要更新的节点。 以下是一个进行 编辑/更新操作的示例 Python 代码:

 def update_node(self, old_data, new_data):
    temp = self.head
    while temp is not None:
        if temp.data == old_data:
            temp.data = new_data
            return
        temp = temp.next
# Example usage:
llist.update_node(2, 5)  # Updates node with value 2 to 5

<st c="16979">update_node</st> 函数应当添加到 <st c="17023">LinkedList</st> 类中,参见前一节。

在链表中查找

<st c="17090">In</st> 第七章,我们详细探讨了多种搜索算法,这些算法都基于数组作为底层数据结构。 例如,在二分查找中,我们可以直接访问数组中特定索引处的元素,例如数组的中间元素。 然而,当使用链表时,由于数据结构的顺序特性,找到具有特定值的节点变得更加困难。 这意味着无论使用何种搜索算法,当应用于单链表时,搜索本质上变成了顺序的线性查找。 因此,链表中的搜索时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>.

<st c="17717">Here is the Python code example using a singly</st> <st c="17765">linked list:</st>

 def search_node(self, key):
    temp = self.head
    while temp is not None:
        if temp.data == key:
            return True
        temp = temp.next
    return False
# Example usage:
found = llist.search(3)  # Returns True if 3 is found

<st c="17979">The</st> <st c="17984">search_node</st> <st c="17995">function</st> <st c="18005">should be added to the</st> <st c="18028">LinkedList</st> <st c="18038">class in the</st> <st c="18052">previous section.</st>

<st c="18090">This</st> <st c="18096">operation involves retrieving the value of a node at a specific position in the linked list.</st> 例如,访问 <st c="18235">17</st> <st c="18240">12</st> <st c="18245">3</st> <st c="18249">24</st> <st c="18254">6</st> 链表中的第四个节点时,需要顺序遍历直到到达目标节点。 此操作的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>。以下是一个实现链表访问的简单 Python 代码:

 def get_nth(self, index):
    temp = self.head
    count = 0
    while (temp):
        if (count == index):
            return temp.data
    count += 1
    temp = temp.next
    return None
# Example usage:
value = llist.get_nth(2)  # Returns the value of the third node

<st c="18670">The</st> <st c="18675">get_nth</st> <st c="18682">function should be added to the</st> <st c="18715">LinkedList</st> <st c="18725">class in the</st> <st c="18739">previous section.</st>

在实际应用中,链表 通常作为实现栈和队列的基础数据结构,因为它们具有动态调整大小的能力。 它们也被操作系统用于内存分配管理,在这种情况下,空闲内存块被连接在一个链表中。 链表的另一个重要应用是在图或树中表示邻接表,每个顶点指向一个包含 邻接顶点的链表。

链表提供了一个灵活且动态的替代数组的方案,特别是在数据结构需要频繁改变大小的场景中。 它们的非连续内存分配允许高效的插入和删除操作,无需像数组那样移动元素。 然而,这种灵活性也带来了顺序访问时间的问题,使得链表不太适合需要频繁随机访问的应用。 理解链表与其他数据结构(如数组)之间的权衡,对于选择最合适的数据结构来解决特定问题至关重要。 在下一节中,我们将简要探讨其他类型的 链表。

双向和循环链表

一个 双向链表 一种链表类型,其中每个节点 包含两个指针:一个指向下一个节点,另一个指向前一个节点(参见 图 12**.3)。 这种双向结构允许在前向和反向方向上遍历链表,使得某些操作更加高效。 尽管双向链表允许双向访问,但它们的顺序性质意味着主要操作的时间复杂度与单向链表类似。

图 12.3:一个示例双向链表

图 12.3:一个示例双向链表

另一种链表的变体是 循环链表 ,其中最后一个节点指向第一个节点,形成一个循环结构(见 图 12**.4)。 这可以应用于单向链表和双向链表。 由于最后一个节点连接回第一个节点,我们可以在不遇到空引用的情况下循环遍历链表。 这一特性非常适用于需要持续循环遍历数据的应用场景,如轮询调度或缓冲区管理。 双向链表类似,循环链表在 时间复杂度上并没有比非循环链表提供任何改进。 链表。

图 12.4:循环链表

图 12.4:循环链表

双向链表和循环链表在不同的使用场景中各自具有独特的优势。 双向链表在导航上提供了更大的灵活性,且节点移除更为容易,而循环链表则能够高效地进行数据的循环遍历。 在性能方面,这两种结构的时间复杂度与单向链表相似,但在特定操作中具有额外的优势。 在下一节中,我们将探讨一种结合了链表和数组优势的数据结构。 和数组。

跳表

正如我们在上一章讨论的那样,在学习数据结构时,评估其在插入、删除和查找三项关键操作中的性能非常重要。 数组在查找方面表现优异,特别是在数据已经排序的情况下,因为它们具备直接(或随机)访问能力,允许实现亚线性时间复杂度。 然而,由于数组是静态的,当涉及到插入新数据或删除现有数据时,它们可能会带来挑战。 另一方面,链表表现出相反的行为。 它们的动态分配使得插入和删除变得容易,但由于缺乏直接访问,甚至在排序数据中,查找也变成了一个顺序过程,具有线性 时间复杂度。

这个问题由此产生:是否有可能结合数组和链表的优点? 换句话说,是否能像数组一样实现比顺序访问更快的访问,同时享受链表的动态内存分配优势? 答案就在于 跳表。跳表是一种概率数据结构,通过在基本链表的基础上增加多个链表层级,从而实现更快速的搜索、插入和删除操作。 跳表的每一层包含来自下层的部分元素,最底层包含所有元素,形成一个简单的链表。 更高的层级充当“快速通道”,一次跳过多个元素,这就是 跳表的名字来源。 图 12**.6 展示了一个跳表的示例。 第 1 层作为标准链表工作,而所有上层则充当 快速通道。

让我们通过类比来解释 跳表的概念。 假设我们正在前往一个特定车站见朋友,车站位于公交系统中的某一站。 公交车会在每个车站停靠,每次停靠都需要耗费相当的时间,所以我们希望在前往朋友车站的途中尽量减少停靠站点的数量。 一共有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 个车站,而我们知道朋友所在车站的具体编号。 该公交系统有两种 线路类型:

  • 常规公交车会在每个 车站停靠。

  • 快速线路仅停靠四个车站,具体为:<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:math><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:mfrac></mml:math><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn3</mml:mn>mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:math><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math>

乘坐常规公交是最安全的选择,因为它确保我们能够到达目的地,但它需要在每个站点停靠,这可能会低效,总共需要停靠的次数为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 站点。 另外,我们可以选择快线公交,它只停靠四个站点。 如果我们朋友的站点位于这些快线停靠站之间的某个区间内(例如,在 1 和 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac></mml:math>之间,或者在 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:fraction></mml:math>之间),我们可以在离朋友的站点最近的快线停靠站下车,然后换乘常规公交,直达朋友的站点。 通过这样做,我们将停靠站的数量从最多的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 减少到大约 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mn4</mml:mn></mml:mrow></mml:mfrac>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:math>,使得我们的旅程更加高效。 跳表的结构与这种 公交系统类似。

跳表具有几个定义特征, 这些特征会影响其行为 和性能:

  • 多个层级:跳表由多个层级组成,每个层级包含前一个层级的一个子集。 最底层是一个标准的链表,包含所有元素。 在跳表中,第一层可能包含所有元素,第二层可能包含一半元素,第三层可能包含四分之一元素, 以此类推。

  • 概率平衡:跳表使用随机化来确定每个元素出现的层级。 这使得跳表的平均时间复杂度与平衡二叉搜索树类似,而无需复杂的平衡算法。 例如,在插入一个元素时,会为该元素随机选择一个(最多的)层级。

  • 高效的查找:跳表通过使用更高层级跳过列表中的大部分部分,从而提高查找速度,减少所需比较次数。 例如,在跳表中查找一个元素时,可以通过跳过多个节点来迅速找到该元素。 通过上层级,可以加速查找。

  • 动态调整大小:跳表可以在添加或删除元素时动态调整其结构,保持高效的操作而无需重建整个结构。 例如,随着新元素的插入,可能会根据 随机化过程将它们添加到多个层级。

跳表支持各种操作,每个操作都有特定的性能特征。 以下部分概述了主要操作及其时间复杂度 和示例。

跳表中的插入

向跳表中插入 数字需要几个步骤。 在这个例子中,我们将插入数字 <st c="26463">3</st>, <st c="26466">4</st>, <st c="26469">5</st>, <st c="26472">7</st>, <st c="26475">8</st>, <st c="26478">9</st> <st c="26485">10</st> 到一个有四个层级的跳表中。 每个数字将一个接一个地插入,层级将根据随机化过程进行分配。 以下是这个 过程的详细逐步描述:

  • 步骤 1: 初始化跳表:从一个空的跳表开始,该跳表有四个层级: 等级 4 (最上层), 等级 3 等级 2,以及 等级 1 (最底层)。 最初,列表的每个层级只有一个头节点,指向 null

  • 步骤 2: 插入 3

    • 确定 3 的层级:随机确定 3 应该出现在的层级。 假设它出现在 所有层级。

    • 插入:在 等级 1 4,没有其他节点;所以, 3 只是被插入,且 等级 1 4 的头节点现在指向 3

这里是 跳表:

  • 等级 4: 3 --> null

  • 等级 3: 3 --> null

  • 等级 2: 3 --> null

  • 等级 1: 3 --> null

  • 步骤 3: 插入 4

    • 确定 4 的层级:随机确定 4 应该出现在的层级。 假设它只出现在 等级 1。较高层级保持不变,因为 3 不出现在那里。

    • 插入:从头到适当位置遍历 等级 1 。应该将 4 插入到 3之后。 更新指针,使得 3 现在指向 4 等级 1

这里是 跳表:

  • 等级 4: 3 --> null

  • 等级 3: 3 --> null

  • 等级 2: 3 --> null

  • 等级 1: 3 --> 4 --> null

  • <st c="27808">5</st> 被随机分配到 等级 1 2 3

    • 插入:在 级别 2 3,元素 5 是下一个元素, 3。然后,这些级别的 3 的指针会更新,指向 5。在 级别 1,将 5 插入到 4 之后,并相应更新指针。

这是 跳表:

  • 级别 4:3 --> null

  • 级别 3:3 --> 5 --> null

  • 级别 2:3 --> 5 --> null

  • 级别 1:3 --> 4 -->5 --> null

  • 步骤 5: 插入 7

    • 确定 7 的级别:假设 7 被随机分配给 级别 1。所有上级 保持不变。

    • 插入:在 级别 1 7 插入到 5之后。

这是 跳表:

  • 级别 4:3 --> null

  • 级别 3:3 --> 5 -->null

  • 级别 2:3 --> 5 -->null

  • 级别 1:3 --> 4 --> 5 -->7 --> null

  • 步骤 6: 插入 8

    • 确定 8 的级别:假设 8 被随机分配给 级别 1 2

    • 插入:在 级别 2,将 8 插入到 5 之后,通过更新指针。 级别 1,将 8 插入到 7之后。

这是 跳表:

  • 级别 4:3 --> null

  • 级别 3:3 --> 5 --> null

  • 级别 2:3 --> 5 --> 8 --> null

  • 级别 1:3 --> 4 --> 5 --> 7 --> 8 --> null

  • 对于 步骤 9 步骤 10,我们会像处理元素 7一样进行。插入所有元素后的最终跳表在 图 12**.5中进行了展示。

图 12.5:一个具有四个层级的跳表示例

图 12.5:一个具有四个层级的跳表示例

跳表中的插入 过程涉及确定新元素将出现在何层,并更新每个相关层级的指针以维持顺序。 层级的随机分配确保跳表保持平衡,从而提供高效的查找、插入和删除操作。 上述步骤展示了每个数字是如何被插入到跳表中的,利用了 更高层级创建的快速通道。

以下是插入到跳表中的 Python 代码实现:

 import random
class Node:
    def __init__(self, value, level):
        self.value = value
        self.forward = [None] * (level + 1)
class SkipList:
    def __init__(self, max_level):
        self.max_level = max_level
        self.head = Node(-1, max_level)  # Head node with value -1 (acts as a sentinel)
        self.level = 0
    def random_level(self):
        level = 0
        while random.random() < 0.5 and level < self.max_level:
            level += 1
        return level
    def insert(self, value):
        update = [None] * (self.max_level + 1)
        current = self.head
        for i in range(self.level, -1, -1):
            while current.forward[i] and current.forward[i].value < value:
                current = current.forward[i]
            update[i] = current
        level = self.random_level()
        if level > self.level:
            for i in range(self.level + 1, level + 1):
                update[i] = self.head
            self.level = level
        new_node = Node(value, level)
        for i in range(level + 1):
            new_node.forward[i] = update[i].forward[i]
            update[i].forward[i] = new_node
    def print_skiplist(self):
        print("Skip List:")
        for i in range(self.level, -1, -1):
            print(f"Level {i}: ", end="")
            node = self.head.forward[i]
            while node:
                print(node.value, end=" -> ")
                node = node.forward[i]
            print("None")
# Example usage
if __name__ == "__main__":
    skiplist = SkipList(3)
    # Insert elements into the skip list
    skiplist.insert(3)
    skiplist.insert(4)
    skiplist.insert(5)
    skiplist.insert(7)
    skiplist.insert(8)
    skiplist.insert(9)
    skiplist.insert(10)
    # Print the skip list
    skiplist.print_skiplist()

让我们来看一下 代码,并解释该算法的主要类:

  • 节点:每个节点对象存储一个 和一个指向 前向指针的列表,在第 1 层有 4 个指针,第 2 层有 5 个,第 3 层有 5 个,第 4 层为 null

  • 跳表:包含以下功能:

    • random_level():此方法根据 概率模型 为每个新节点生成一个随机层级

    • insert():此方法将在跳表的 适当层级插入一个新值

    • print_skiplist():此方法打印跳表,显示从最高层到 最低层的每一层节点

作为示例,我们 创建一个最多包含三层的跳表。 插入的值为 <st c="31690">3</st> <st c="31693">4</st> <st c="31696">5</st> <st c="31699">7</st> <st c="31702">8</st> <st c="31705">9</st>,和 <st c="31712">10</st> 被插入到跳表中。 最后,我们打印跳表,显示节点在各个层级中的组织方式,如下所示:

 Skip List:
Level 2: 7 --> 10 --> None
Level 1: 5 --> 7 --> 8 --> 9 --> 10 --> None
Level 0: 3 --> 4 --> 5 --> 7 --> 8 --> 9 --> 10 --> None

插入后,我们来讨论跳表在 查找操作中的表现。

在跳表中搜索

在跳表中搜索一个项,通常是从最高层开始,沿着列表向前移动,直到找到目标或者需要下移一层。 以下是一步步的解释,如何在我们之前构建的跳表中搜索数字 8

  • 步骤 1: 从最高层开始(Level 4):

    • Level 3的头节点开始。

    • 3 (第一个值在 Level 3) 小于 7吗?是的;下一个节点 null

  • 步骤 2: 移动到 Level 3

    • 现在,下降到节点 3 Level 3 ,下一个节点 5

    • 5 小于 9吗?是的。 移动到节点 5。下一个节点 为空。

  • 步骤 3: 移动到 Level 2

    • 现在,下降到节点 5 Level 3 ,下一个节点 8

    • 8 小于 9吗?不。

  • 步骤 4: 移动到 Level 1

    • 下一个节点是 7。目标 已找到。

总比较次数为三次,显示出相较于标准链表中 Level 1 顺序查找的明显改进。 跳表在效率上明显优于链表,尤其是在处理 大数据集时。

以下是必须添加到之前描述的 <st c="33213">search</st> 方法,它将添加到 <st c="33274">SkipList</st> 类中:

 def search(self, value):
        current = self.head
        for i in range(self.level, -1, -1):
            while current.forward[i] and current.forward[i].value < value:
                current = current.forward[i]
        current = current.forward[0]
        if current and current.value == value:
            return True
        return False

以下是 <st c="33593">search</st> <st c="33605">SkipList</st> 类中的示例用法:

 if __name__ == "__main__":
    skiplist = SkipList(3)
    skiplist.insert(3)
    skiplist.insert(4)
    skiplist.insert(5)
    skiplist.insert(7)
    skiplist.insert(8)
    skiplist.insert(9)
    skiplist.insert(10)
    skiplist.print_skiplist()
    value_to_search = 7
    found = skiplist.search(value_to_search)
    print(f"\nSearch for {value_to_search}: {'Found' if found else 'Not Found'}")
    value_to_search = 6
    found = skiplist.search(value_to_search)
    print(f"Search for {value_to_search}: {'Found' if found else 'Not Found'}")

通过插入 示例数据,得到以下结果:

 Skip List:
Level 1: 3 --> 8 --> 9 --> 10 --> None
Level 0: 3 --> 4 --> 5 --> 7 --> 8 --> 9 --> 10 --> None
Search for 7: Found
Search for 6: Not Found

为了总结我们关于跳表的讨论,我们将跳过删除的详细解释。 跳表中的删除操作首先进行查找,然后更新 必要的指针。

跳表为搜索、删除和插入操作提供了高效的平均时间复杂度,通常为 O(logn)。这种效率是通过使用多级链表实现的,使得操作可以跳过大量数据,类似于二叉搜索树。 在最优情况下,当目标元素靠近起始点时,操作的时间复杂度可以接近 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 然而,由于跳表的概率性质,结构有可能退化,导致最坏情况下的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 尽管如此,跳表在平均情况下的性能仍然稳健,使其成为动态数据结构的实用选择,特别是在平衡搜索、插入和删除时间 至关重要的场景中。

一个 是一个 线性数据结构,遵循后进先出(LIFO)原则,这意味着最后添加到栈中的元素会最先被移除。 可以将其想象为一堆盘子:我们将新的盘子放在顶部,当需要盘子时,我们首先取走最上面的那个。 栈广泛用于各种应用,包括表达式求值、函数调用管理和软件中的撤销机制。

栈有几个 决定其行为和性能的特征:

  • LIFO 顺序: 最后插入的元素是最先被移除的。 例如,如果我们将数字 1, 2 3 推入栈中,它们将以相反的顺序被弹出: 3, 2, 1

  • 一端操作: 所有的插入(push)和删除(pop)操作都在栈的顶部进行。 无法直接访问栈中间或底部的元素。 如果我们将多个元素推入栈中,我们只能直接访问最近添加的元素。 这也意味着栈不支持随机访问。 与数组不同,我们不能直接访问栈中某个特定索引的元素。 访问仅限于栈顶 元素。

  • 动态大小: 栈可以随着元素的推入或弹出动态增大或缩小。 当我们将更多元素推入栈时,栈的大小会增加;而当我们弹出元素时,栈的大小会减小。

栈支持几个基本操作,每个操作具有特定的性能特点。 以下是这些主要操作及其时间复杂度 和示例:

  • push: 它将一个新元素添加到栈顶。 它的时间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>,因为此操作仅涉及将元素添加到栈顶。 以下是一个简单的 Python 示例:

     stack = []
    stack.append(3)  # Stack is now [3]
    stack.append(5)  # Stack is now [3, 5]
    print(stack)
    ```</st>
    
    
  • pop: 这会移除栈顶元素。 它在常数时间内执行移除操作 (<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>). 以下是一个 Python 示例:

     top_element = stack.pop()
    print(stack)
    ```</st>
    
    
  • peek: 它用于获取栈顶元素的值,但不将其从栈中移除。 对于栈 [1, 2, 3],peek 操作将返回 3 ,并且不会改变栈的状态。 时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>,因为它只涉及访问栈顶元素。 一个简单的 Python 指令如下:

     top_element = stack[-1]
    ```</st>
    
    
  • search: 通常用于在栈中查找一个元素。 例如,如果我们在栈 [1, 2, 3]中查找元素 2,我们会在栈顶往下数第 1 位找到它。 显然,栈中 search 操作的时间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>,因为它可能需要从栈顶到栈底逐个扫描。 让我们来看一个简单的 Python 示例:

     element_to_find = 2
    position = stack.index(element_to_find)  # Finds the position of 2 in the stack
    ```</st>
    
    
  • edit: 该操作修改栈顶元素的值。 如果栈顶元素是 [1, 2, 3] ,其值为 3 ,我们希望将其改为 5,则栈会变为 [1, 2, 5]。此操作的时间复杂度为常数时间(<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>),因为该操作只影响栈顶元素。 一个 Python 代码示例如下:

     stack[-1] = 5  # Changes the top element to 5; Stack becomes [3, 5]
    ```</st>
    
    

一些编程语言还支持额外的操作,例如 <st c="38511">isFull</st>,用于检查栈是否已满,以及 <st c="38563">isEmpty</st>,用于判断栈是否为空。

栈在计算机编程中有着广泛的应用,涉及多个关键场景。 以下是一些重要的 使用案例:

  • 栈用于存储函数调用,将每个调用压入栈中,并在 函数完成时弹出

  • 它们用于转换和评估表达式,特别是将中缀表达式转换为后缀或 前缀表示法

  • 栈在软件中实现撤销功能,每个操作都会压入栈中,可以通过弹出操作撤销

栈是许多算法和应用中使用的基础且多功能的数据结构。 它们提供高效的 <st c="39219">push</st> <st c="39228">pop</st> 操作,时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> ,特别适用于需要先入后出的场景。 然而,它们缺乏随机访问和 LIFO 特性,可能不适用于需要按不同顺序访问元素的情况。 了解栈的权衡和适用场景对于有效的算法设计和实现至关重要。 在下一节中,我们将探讨与栈相反操作的队列。 队列。

队列

一个 队列 按 FIFO 原则操作,这意味着它的行为与栈相反:第一个添加的元素是第一个被移除的。 这种结构类似于排队等待服务的人群:排队的第一个人是第一个被服务的。 队列用于多种场景,包括任务调度、缓冲和计算机系统中的资源管理。 计算机系统中。

队列有几个 影响其行为和性能的特点: 和性能:

  • FIFO 顺序:插入到队列中的第一个元素是第一个被 移除的。

  • 两端操作:元素在队列的后端(末端)添加,并从队列的前端(开始)移除。

  • 动态大小:队列 可以在元素入队或出队时动态增大或减小。 当我们入队更多元素时,队列的大小会增加;当我们出队元素时, 队列的大小会减少。

  • 没有随机访问:像栈一样,队列与数组不同,不能直接通过特定的索引访问元素。 访问仅限于队列的前端 元素。

队列支持一些基本操作,每个操作都有特定的性能特点。 以下是主要操作:

  • 入队:这涉及将一个新元素添加到队列的末尾。 假设队列当前包含元素 1 2。将 3 加入队列时,它会被放置在队列的末尾,结果是队列变为 [1, 2, 3]。此操作在常数时间内执行,或 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>,因为该操作仅涉及将元素添加到队列的末尾。 一个简单的 Python 示例如下: 如下:

     queue = []
    queue.append(1)  # Queue is now [1]
    queue.append(2)  # Queue is now [1, 2]
    queue.append(3)  # Queue is now [1, 2, 3]
    print(queue)
    ```</st>
    
    
  • 出队:此操作用于移除队列的前端元素。 如果我们出队队列 [1, 2, 3],前端元素 1 会被移除,剩下队列 [2, 3]。与 入队相似,此操作的时间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math>,因为它仅涉及移除前端元素。 这里是一个简单的 Python 指令:

     front_element = queue.pop(0)
    ```</st>
    
    
  • 查看队首:此操作用于检索队列前端元素的值,而不将其从队列中移除,且此操作在常数时间内执行。 对于队列 [1, 2, 3],查看队首会返回 1 ,并且队列不发生变化。 查看队首的 Python 指令可以如下: 如下:

     front_element = queue[0]
    
  • 查找:队列的查找操作是线性时间的,因为它可能需要从队列的前端扫描到队列的末尾。 以下是实现队列查找的 Python 代码: 如下:

     target = 2
    position = queue.index(target)
    

一些编程语言还支持额外的操作,如 <st c="42369">isFull</st>,用于检查队列是否已满,以及 <st c="42421">isNull</st>,用于确定队列 是否为空。

类似于堆栈,队列 在计算机编程中有着广泛的应用。 一个著名的例子是 任务调度,在操作系统中,队列被用来管理进程。 当进程准备好运行时,它们会被加入队列,执行完毕后则会从队列中移除。 另一个例子是缓冲,在这种情况下,队列临时存储数据,直到数据被处理,比如在打印队列或流媒体服务中。 最后,队列在资源管理中起着至关重要的作用,确保对共享资源的访问是按照请求的顺序进行的。 队列是一个基本的数据结构,广泛用于许多算法和应用,特别是在需要以 FIFO 方式管理任务或资源的场景中。

队列提供高效的入队和出队操作,时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> 但不允许对元素进行随机访问。 理解队列的适当使用场景对于有效的算法设计和实现至关重要。 在本章的最后一节中,我们通过引入一种具有两个端点的结构,扩展了队列的概念,从而有效地结合了堆栈和 队列的功能。

双端队列

双端队列(Deque) 是一种 多功能的数据结构,允许从序列的两个端点插入和删除元素,使其成为堆栈和队列的广义形式。 双端队列可以同时作为堆栈和队列使用,提供了更大的灵活性来处理元素的添加 或移除。

让我们重点介绍一下 双端队列(Deque)的 特性:

  • 在两个端点进行插入和删除:元素可以从双端队列的前端或后端进行添加或移除。 例如,我们可以将元素推入双端队列的前端或后端,并从任一端弹出它们。 同样也可以进行操作。

  • 无固定方向:双端队列不强制执行严格的 LIFO 或 FIFO 顺序;相反,它们允许在两端进行操作。 例如,我们可以将双端队列视为栈,只使用一端,或视为队列,使用两端。

  • 动态大小:双端队列可以随着元素的添加或删除动态增长或收缩。

  • 无随机访问:像栈和队列一样,双端队列不允许直接访问指定索引处的元素。

双端队列支持多种操作,每种操作具有特定的性能特点。 以下是主要操作及其时间复杂度 和示例:

  • 从前端添加(在前端插入):此操作将一个新元素添加到双端队列的前端。 例如,如果当前双端队列为 [2, 3],将 1 添加到前端,将变成 [1, 2, 3]。此操作在常数时间内完成,因为它只涉及将元素添加到前端。 下面是一个简单的 Python 代码:

     from collections import deque
    d = deque([2, 3])
    d.appendleft(1)  # Deque is now [1, 2, 3]
    print(d)
    
  • 从后端添加(在后端插入):此操作将一个新元素添加到双端队列的后端。 如果当前双端队列为 [1, 2],将 3 添加到后端,将变成 [1, 2, 3]。与 从前端添加类似,时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>。下面是一个简单的 Python 指令来执行 此操作:

     d.append(3)  # Deque is now [1, 2, 3]
    ```</st>
    
    
  • 从前端删除(从前端删除):此操作以常数时间从双端队列的前端删除元素。 如果我们从双端队列 [1, 2, 3]删除前端元素,它将变为 [2, 3]。执行 从前端删除 操作的简单 Python 指令如下:

     front_element = d.popleft
    
  • 从后端删除(从后端删除):与前一个操作类似,只是从双端队列的后端删除元素。 执行此操作的 Python 代码如下:

     rear_element = d.pop()
    
  • Peek(访问前端或后端元素):在常数时间内检索前端或后端元素的值,而不将其从双端队列中移除。 对于双端队列 [1, 2, 3],查看前端将返回 1,查看后端将返回 3**

     front_element = d[0]  #Returns the front element w/o removing it; Deque remains [1, 2]
    rear_element = d[-1]  #Returns the rear element w/o removing it; Deque remains [1, 2]
    

需要注意的是,许多用于双端队列的函数可能会或不会被不同的编程语言支持。 幸运的是,Python 提供了对 这些函数的内置支持。

双端队列在计算机编程中有几个重要的应用场景。 一个关键的例子是任务调度,在任务调度算法中,双端队列被用于根据需要从队列的两端添加或移除任务。 另一个应用场景是在滑动窗口算法中,双端队列被用来高效管理元素,随着窗口在数据集上滑动,元素被添加或移除。 最后,双端队列也被用于实现应用程序中的撤销/重做功能,允许从队列的任意一端添加或移除操作。

双端队列是一种灵活的 数据结构,它综合了栈和队列的特点,允许在两端进行插入和删除操作。 它们提供高效的操作,时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math> ,用于在任意一端添加或移除元素。 双端队列在需要从两端管理元素的场景中特别有用,如任务调度、滑动窗口算法以及撤销/重做功能。 理解双端队列的适用场景对于有效的算法设计 和实现至关重要。

总结

在这一章中,我们探讨了各种线性数据结构,重点关注它们的定义、特性和操作。 我们首先深入研究了数组和链表,分析了它们如何处理基本操作,如插入、删除和查找。 我们讨论了这些结构之间的权衡,指出数组在访问元素方面的高效性,以及链表在动态内存分配方面的灵活性。 本章还介绍了更高级的线性结构,如栈、队列和双端队列,展示了它们在计算机编程中的实际应用,从任务调度到表达式求值和 资源管理。

接着,我们介绍了跳表,一种概率性数据结构,它结合了数组和链表的优点,提供了高效的查找、插入和删除操作。 通过详细示例,我们展示了跳表如何改进传统链表的局限性。 在本章的结尾,我们强调了理解每种数据结构的适用场景的重要性,以优化算法设计。 现在,你已经对线性数据结构有了透彻的了解,这些数据结构在算法设计中起着至关重要的作用。 在下一章中,我们将转向非线性数据结构,探讨它们独特的特性 和应用。

参考文献及进一步阅读

  • 算法导论. 作者:Thomas H. Cormen,Charles E. Leiserson,Ronald L. Rivest,和 Clifford Stein. 第四版. MIT Press. 2022 年:

    • 第十章, 基本 数据结构
  • 算法. 作者:R. Sedgewick,K. Wayne. 第四版. Addison-Wesley. 2011 年:

    • 第一章 , 基础知识
  • C++中的数据结构与算法分析. 作者:Mark A. Weiss. 第四版. Pearson. 2012 年:

    • 第三章, 列表、栈 和队列

第十七章:13

非线性数据结构

非线性数据结构 构成 一个关键的数据结构类别,广泛应用于设计高效算法。 与线性数据结构(如数组和链表)不同,非线性结构允许数据元素以更复杂、层次化的方式存储和访问。 这些结构使得高效处理关系、依赖性和层次数据成为可能,因而在解决各种计算问题时至关重要。 计算问题。

在本章中,我们首先探索定义非线性数据结构的一般属性和特征。 接着,我们讨论两大类:图和树。 用于模拟对象之间关系的多功能结构,而 则表示 层次关系,以更结构化的形式展现。 最后,我们 研究一种特殊的二叉树,称为 ,它在实现高效算法(如堆排序)中至关重要。 学习非线性数据结构对于算法设计至关重要,因为它们在许多算法中扮演着关键角色,包括排序和搜索算法。 此外,图、树以及其他形式的非线性数据结构在人工智能、机器学习和优化等先进领域得到广泛应用,这些领域对高效的数据管理和处理要求极高。 在这些领域中,数据管理和处理的效率至关重要。

本章涵盖以下主题:

  • 非线性 数据结构简介

非线性数据结构简介

第十一章中,我们 介绍了 抽象数据类型 (ADTs),并将其分为两大类:线性和非线性。 随后,我们在 第十二章中深入讨论了线性数据结构,分析了它们与我们核心目标——设计和分析高效算法——之间的关系。 虽然我们触及了线性数据结构的许多关键方面,但值得注意的是,这一领域非常广泛,完全可以单独进行更深入的探讨。 对于那些有兴趣深入探索数据结构的人,我们在 第十一章 第十二章的末尾已列出了相关参考资料。

在本章中,我们的重点转向非线性数据结构。 与前一章类似,我们将从它与高效算法设计的关系出发,来探讨这一话题。 我们的目标不仅仅是介绍各种非线性数据结构,更是要突出它们在提高算法性能方面的作用和应用。

让我们从简要讨论非线性数据结构的定义、其主要特征以及一些常用类型开始。

与线性数据结构不同,线性数据结构中的元素按顺序排列(例如,数组、链表),非线性数据结构以层级或互联的方式组织数据。 在非线性结构中,每个元素可能与多个其他元素相连接,形成复杂的关系,从而在 某些操作中实现更高效的数据处理。

以下是非线性 数据结构的主要特点:

  • 层级关系:元素的结构呈现出反映层次关系的方式,这意味着 某些元素可能作为 父元素 而其他元素则是 子元素。这种情况在像 树形结构 图形等结构中尤为明显。

  • 复杂的遍历模式:与线性结构不同,线性结构的遍历相对 简单,遍历非线性数据结构需要更复杂的技术,这些技术通常是特定于所使用的结构的。

  • 变量访问时间:搜索、插入或删除元素所需的时间可以根据结构和实现方式的不同而大相径庭。 在许多情况下,非线性数据结构相比 于其 线性结构,能够实现更高效的操作。

非线性数据结构由 若干关键元素组成,这些元素定义了其结构和功能。 最重要的组成部分包括节点、边、父节点、子节点、根节点、叶子节点和子树。 让我们详细了解这些组成部分:

  • 节点:节点或顶点是大多数非线性数据结构的基本构建块。 每个节点通常包含数据,并且根据结构的类型,可能还会与其他顶点连接。 例如,在树结构中,顶点表示层级中的独立元素。 例如,在社交网络图中,每个节点代表一个用户,节点中存储的数据可能是该用户的 个人资料信息。

  • :边或箭头是连接两个节点的链接。 在非线性结构中,边在定义节点之间的关系中起着至关重要的作用。 在二叉树中,边定义了父节点与子节点之间的关系。 例如,如果父节点代表经理,那么边将连接到表示其员工的子节点。 在图中,边表示两个实体之间的关系。 例如,在交通网络中,一条边可能表示两座城市之间的直飞航班。 在运输网络中,一条边可能代表 两座城市之间的航班。

  • 父母和子女:在树等层级非线性数据结构中,节点按层级组织,父节点直接连接到其下方的子节点。 父子关系是树结构的基本概念:

    • 父节点:一个具有一个或多个直接 子节点的节点

    • 子节点:一个直接连接到其上方节点的节点(即父节点)

    例如,在企业层级树中,经理是父节点,子下属是 子节点。

  • :根是 树结构中最顶部的节点,作为遍历树的起点。 一棵树只能有一个根,所有其他节点都是该根的后代。 如果一个节点没有父节点,它被认为是根。 在文件系统中,根目录是最顶层的文件夹,所有其他目录或文件都从 它那里分支出来。

  • 叶子:叶子是没有子节点的节点。 叶子表示树结构的端点,在这些地方不会再发生分支。 在许多算法中,叶子非常关键,因为它们通常标志着遍历 或搜索的完成点。

  • 子树:子树是树的一部分,包括一个节点及其所有子孙节点。 子树使得树可以递归处理,其中每个节点及其子节点可以被视为一个独立的树。 在决策树中,每个节点及其分支构成一个子树,代表一部分 可能的决策。

非线性数据结构有多种类型,每种类型都适用于不同的算法问题。 最常见的非线性数据结构包括图、树和堆。 在接下来的章节中,我们将详细探讨这三种基本的非线性数据结构。 每种结构都有其独特的特点和应用,理解它们对于设计高效的算法至关重要。 我们将探讨它们的属性、实现方式以及它们在解决各种 计算问题中的作用。

最具多功能性且广泛使用的非线性数据结构之一。 它们用于表示实体之间的关系,其中实体表示为节点(也称为 顶点), 关系则表示为边。 图可以建模各种现实世界的问题,从社交网络到交通系统和 通信协议。

图有多种类型,每种类型具有特定的属性,使其适合不同的任务。 以下是最常见的图类型:

  • 无向图:在 无向图中,边没有方向。 两个节点之间的关系是双向的(参见 图 13**.1)。 如果节点 A 和节点 B 之间有一条边,你可以从 A 遍历到 B,也可以从 B 遍历到 A,且没有任何限制。 无向图的一个例子是 Facebook 好友的社交网络,其中的连接是相互的。 这意味着如果 A 是 B 的朋友,那么 B 也是 A 的朋友,体现了关系的双向性质。

图 13.1:无向图

图 13.1:无向图

  • 有向图(有向图):在有向图中,边具有方向。 节点之间的关系是单向的,这意味着 如果存在从节点 A 指向节点 B 的有向边,你只能从 A 遍历到 B,而不能反过来。例如,网站上的页面有指向其他页面的链接,形成了一个有向图。 图 13**.2 展示了有向图的一个例子。 如图所示,图形不必是 完全连接的。

图 13.2:有向图

图 13.2:有向图

  • 带权图:在带权图中,每条边都被赋予一个数值或 权重。这个权重通常表示与节点之间连接相关的成本、距离或时间。 带权图的一个例子是道路网络,其中每条边的权重代表城市之间的 距离或旅行时间。 图 13.3 描绘了一个带权图,其中权重被分配给了 各个边。

图 13.3:带权图

图 13.3:带权图

  • 无权图:在无权图中,所有边具有相同的重要性,这意味着 在节点之间的旅行没有特定的成本或距离。 图 13**.1中,图是无权图。

  • 有向图与无向图:有向图包含至少一个循环,即 你可以从一个节点出发,遍历 边,最后回到 同一个节点。 无向图没有这样的循环,因此在任务调度等应用中至关重要。 图中的 *图 13.1**包含循环。

  • 符号图:在符号图中,边上标有 正号或负号,通常代表有利或不利的关系。 这种图在关系可以有极性时非常有用,例如在社交网络中,边可以表示 友谊(正向)或冲突(负向)。 其中,正边代表友谊,负边代表竞争关系的社交网络就是符号图的一个例子。

  • 超图:超图通过允许边(称为 超边)一次连接多个节点,从而推广了图的概念。 这种类型的图在表示复杂关系时尤为有用,尤其是当单一连接可能涉及多个实体时。 例如,在研究合作网络中,一个超边可能代表三位或更多研究人员共同撰写的论文,同时连接所有研究人员。

图 13.4:超图

图 13.4:超图

图 13**.4 展示了一个超图的示例,包含以下组件:顶点集合为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miV</mml:mi>mml:mo=</mml:mo><mml:mfenced open="{" close="}" separators="|">mml:mrowmml:mia</mml:mi>mml:mo,</mml:mo>mml:mib</mml:mi>mml:mo,</mml:mo>mml:mic</mml:mi>mml:mo,</mml:mo>mml:mid</mml:mi>mml:mo,</mml:mo>mml:mie</mml:mi>mml:mo,</mml:mo>mml:mif</mml:mi>mml:mo,</mml:mo>mml:mig</mml:mi>mml:mo,</mml:mo>mml:mih</mml:mi>mml:mo,</mml:mo>mml:mik</mml:mi></mml:mrow></mml:mfenced></mml:math>,超边集合为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miE</mml:mi>mml:mo=</mml:mo><mml:mfenced open="{" close="}" separators="|">mml:mrowmml:msubmml:mrowmml:mie</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:msubmml:mrowmml:mie</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:msubmml:mrowmml:mie</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msub></mml:mrow></mml:mfenced></mml:math>。每个超边连接多个顶点,如下所示: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mie</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo><mml:mfenced open="{" close="}" separators="|">mml:mrowmml:mia</mml:mi>mml:mo,</mml:mo>mml:mib</mml:mi>mml:mo,</mml:mo>mml:mic</mml:mi>mml:mo,</mml:mo>mml:mid</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mie</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo><mml:mfenced open="{" close="}" separators="|">mml:mrowmml:mid</mml:mi>mml:mo,</mml:mo>mml:mie</mml:mi>mml:mo,</mml:mo>mml:mif</mml:mi>mml:mo,</mml:mo>mml:mig</mml:mi></mml:mrow></mml:mfenced></mml:math> 以及 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mie</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msub>mml:mo=</mml:mo><mml:mfenced open="{" close="}" separators="|">mml:mrowmml:mic</mml:mi>mml:mo,</mml:mo>mml:mih</mml:mi>mml:mo,</mml:mo>mml:mik</mml:mi></mml:mrow></mml:mfenced></mml:math>

在接下来的 章节中,我们将探讨如何表示算法中最常用的几种图,并讨论与每种表示方法相关的复杂性。

图的表示

图可以以 多种方式表示,每种方法都适用于特定的使用场景,取决于图的结构和所需的操作类型。 在接下来的小节中,我们将探讨三种常见的图表示方法及其关键特性。 在评估每种方法时,我们将重点关注它们性能的三个关键方面:

  • 空间复杂度:使用所选表示方法存储图所需的内存量

  • 访问节点所有邻居的时间复杂度:检索所有直接连接(相邻)到某一给定节点的节点的效率

  • 检查边是否存在的时间复杂度:确定两个特定节点之间是否存在边所需的时间

通过分析这些复杂性,我们可以更好地理解每种图形表示的优缺点,以及它们如何应用于不同的 算法任务。

邻接矩阵

一个 邻接矩阵 图形 表示为 V×V 矩阵,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miV</mml:mi></mml:math> 是图中的节点或顶点数。 矩阵中的每个单元格在位置 (i,j) 指示节点 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math> 和节点 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mij</mml:mi></mml:math>之间边的存在(可能带有权重)。 在无向图中,邻接矩阵 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miD</mml:mi></mml:math> 对称,这意味着 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miD</mml:mi>mml:mo=</mml:mo>mml:miD</mml:mi></mml:math>’,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miD</mml:mi></mml:math> 表示矩阵 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miD</mml:mi></mml:math>的转置。这种对称性是因为无向图中的边没有方向,所以 如果节点 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math> 和节点 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mij</mml:mi></mml:math>之间有边,关系是互相的。 因此, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miD</mml:mi><mml:mfenced separators="|">mml:mrowmml:mii</mml:mi>mml:mo,</mml:mo>mml:mij</mml:mi></mml:mrow></mml:mfenced></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miD</mml:mi>mml:mo(</mml:mo>mml:mij</mml:mi>mml:mo,</mml:mo>mml:mii</mml:mi>mml:mo)</mml:mo></mml:math> 将具有相同的值。</st

对于 无权重、无符号图,邻接矩阵是一个二进制矩阵,其中每个元素的值为 0 或 1。 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miD</mml:mi>mml:mo(</mml:mo>mml:mii</mml:mi>mml:mo,</mml:mo>mml:mij</mml:mi>mml:mo)</mml:mo></mml:math> 的值为 1 表示顶点 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math> <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mij</mml:mi></mml:math>之间有一条边,值为 0 表示它们之间没有边。

示例 13.1

这里是一个表示带权有向图的矩阵

A --> B (2)

B --> C (3)

A --> C (4)

表示这个简单图的邻接矩阵是一个 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn3</mml:mn>mml:mo×</mml:mo>mml:mn3</mml:mn></mml:math> 矩阵 如下所示:

ABC

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mtablemml:mtrmml:mtdmml:miA</mml:mi></mml:mtd></mml:mtr>mml:mtrmml:mtdmml:miB</mml:mi></mml:mtd></mml:mtr>mml:mtrmml:mtdmml:miC</mml:mi></mml:mtd></mml:mtr></mml:mtable><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mtablemml:mtrmml:mtdmml:mn0</mml:mn></mml:mtd>mml:mtdmml:mn2</mml:mn></mml:mtd>mml:mtdmml:mn4</mml:mn></mml:mtd></mml:mtr>mml:mtrmml:mtdmml:mn0</mml:mn></mml:mtd>mml:mtdmml:mn0</mml:mn></mml:mtd>mml:mtdmml:mn3</mml:mn></mml:mtd></mml:mtr>mml:mtrmml:mtdmml:mn0</mml:mn></mml:mtd>mml:mtdmml:mn0</mml:mn></mml:mtd>mml:mtdmml:mn0</mml:mn></mml:mtd></mml:mtr></mml:mtable></mml:mrow></mml:mfenced></mml:math>

显然,该表示的空间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:miV</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>。无论边的数量如何,邻接矩阵都需要与顶点数的平方成比例的空间,因为它必须考虑所有顶点对之间的每条可能边。 这对于大型图可能效率低下,特别是如果图是稀疏的(即边的数量相对于可能边的数量较少)。

在图中,访问节点的所有邻居是图中的一个操作。使用邻接矩阵表示,时间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:miV</mml:mi></mml:mrow></mml:mfenced></mml:math>。要找到给定节点的所有邻居,需要检查矩阵的相应行(或列)中的所有条目。 对于节点 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math>,您扫描 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:mii</mml:mi></mml:mrow>mml:mrowmml:mit</mml:mi>mml:mih</mml:mi></mml:mrow></mml:msup></mml:math> 行以检查哪些顶点从 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math>具有直接边缘,这让我们想起了线性搜索算法,其中我们要报告所有非零条目。 这一操作需要检查 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miV</mml:mi></mml:math> 条目,导致时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:miV</mml:mi>mml:mo)</mml:mo></mml:math>,无论节点实际有多少邻居。 相比之下,检查邻接矩阵表示中是否存在边的时间复杂度为 O(1),因为我们可以直接在常数时间内访问任何边。 由于邻接矩阵 是一个二维数组,检查节点 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math> 节点 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mij</mml:mi></mml:math> 之间是否存在边是对位置 (i,j)的直接访问操作。只需检查 (i,j) 处的值是否非零(对于加权图)或 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn1</mml:mn></mml:math> (对于 无权图)。

我们什么时候应该使用邻接矩阵?

当图是稠密图(即边的数量接近 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:miV</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>),此时高空间需求变得不那么重要。 此外,如果我们的算法需要常量时间的边存在性检查,邻接矩阵则具有明显的优势。 最后,邻接矩阵实现简单,对于某些注重易用性的算法来说,它是一个实用的选择。

邻接表

一个 邻接表中,每个节点存储其邻居节点的列表 (或与之相连的节点)。 这是一种更节省空间的表示方法,尤其适用于稀疏图。 示例 13.1 的邻接表 如以下所示 如下:

A: B(2), C(4)

B: C(3)

C: -

邻接表表示使用 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:miV</mml:mi>mml:mo+</mml:mo>mml:miE</mml:mi>mml:mo)</mml:mo></mml:math> 空间,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miE</mml:mi></mml:math> 是边的数量。 这种表示方式通常对于 稀疏图更加高效。

访问一个节点的邻居(即相邻的顶点)需要耗费时间 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mik</mml:mi>mml:mo)</mml:mo></mml:math>,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 是该节点的度(即与该节点相连的边的数量)。 在最坏的情况下,这个时间复杂度可能是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:miV</mml:mi>mml:mo)</mml:mo></mml:math>,但是对于大多数实际应用来说,它要小得多。 在上述邻接表中,冒号后列出的节点表示该节点左侧的邻居或相邻节点。

要检查 两个特定顶点之间是否存在一条边,你需要遍历相邻顶点的列表。 这需要 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mik</mml:mi>mml:mo)</mml:mo></mml:math> 的时间,因为我们可能需要扫描源节点的所有邻居。 在最坏的情况下,这需要 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:miV</mml:mi>mml:mo)</mml:mo></mml:math>,但对于稀疏图来说,通常要小得多。

何时使用邻接表

邻接表在图较为稀疏时最为高效——也就是说,当边的数量远小于 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msupmml:mrowmml:miV</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>。由于空间复杂度是 O(V+E),因此对于边较少的图,邻接表更加节省内存。 此外,如果我们的算法经常需要访问某个节点的所有邻居,邻接表提供高效的 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mik</mml:mi>mml:mo)</mml:mo></mml:math> 访问。 它对于动态图也非常有益,在这种图中,节点和边经常被添加或删除,因为更新邻接表直接且在 内存方面的开销较小。

边列表

一个 边列表 明确地 存储图中的所有边 以及它们的权重(如果有的话)。 当图较为稀疏并且你主要需要处理边时,边列表非常有用。 该边列表在 示例 13.1 中的图是 如下:

(A, B, 2)

(A, C, 4)

(B, C, 3)

边列表表示法使用<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:miE</mml:mi>mml:mo)</mml:mo></mml:math> 空间。 对于非常稀疏的图,这种表示方式非常高效。 要访问节点的所有邻居,边列表并没有直接存储它们,因此查找所有邻居需要扫描整个边列表,最坏情况下的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:miE</mml:mi>mml:mo)</mml:mo></mml:math> 同样,检查两个顶点之间是否存在边也需要扫描整个边列表,导致时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:miE</mml:mi>mml:mo)</mml:mo></mml:math>

当使用边列表

边列表是非常适合稀疏图,在这种图中,我们主要需要处理边本身,而不是频繁访问邻居或检查边的存在。 它使用<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:miE</mml:mi>mml:mo)</mml:mo></mml:math> 空间,使其在边较少的图中具有很高的内存效率。 然而,它的主要限制在于,它对于某些操作并不高效,例如查找节点的所有邻居或检查特定边是否存在,这两者都需要扫描整个列表。 因此,边列表最适合用于那些直接处理边的算法,例如一些以边为中心的算法,如 Kruskal 最小生成树算法。

图遍历

遍历 是图算法中的一种基础操作,其目标是按照特定顺序访问所有节点。 两种最广泛使用的图遍历技术是 深度优先搜索 DFS)和 广度优先搜索 BFS)。 在接下来的章节中,我们将详细探讨这两种方法 ,重点介绍它们的过程、应用场景 和复杂度。

DFS 图遍历

DFS 是一种 图遍历技术,它会在回溯之前尽可能深入地探索每一条分支或路径。 它通常通过递归或栈实现,特别适合用于探索深层结构或在图中发现特定路径。 在图中运作良好。

DFS 的核心思想是从一个任意节点开始(通常称为 根节点),然后尽可能深入地探索图的每一条分支,再转向下一个分支。 DFS 按照以下 基本步骤进行:

  1. 访问起始节点。

  2. 对于当前节点的每个未访问的邻居,执行一次对该邻居的 DFS。

  3. 重复此过程,直到所有从起始节点可达的节点都被访问。

让我们 考虑 图中的 图 13**.5 ,它包含六个节点。

图 13.5:一个示例图

图 13.5:一个示例图

节点 A开始进行 DFS,遍历顺序如下:A --> B --> D --> E --> F --> C。 这个遍历完全探索了一条路径(分支),然后才转向下一个。

这里是一个使用递归实现的 Python 深度优先搜索(DFS)。 首先,我们定义一个 简单的图:

 # You will need first: pip install networkx matplotlib
import networkx as nx
import matplotlib.pyplot as plt
# Define the graph as an adjacency list
graph = {
    'A': ['B', 'C'],
    'B': ['D', 'E'],
    'C': ['B' , 'F'],
    'D': ['E'],
    'E': ['F'],
    'F': ['A']
}

下面的 代码可视化了 示例图:

 # Visualize the graph
visualize_graph(G)
# Create a directed graph using NetworkX
G = nx.DiGraph()
# Add edges to the graph
for node, neighbors in graph.items():
    for neighbor in neighbors:
        G.add_edge(node, neighbor)
# Visualize the graph using NetworkX and Matplotlib
def visualize_graph(G):
    pos = nx.spring_layout(G)  # Positions for all nodes
    nx.draw(G, pos, with_labels=True, node_color='lightblue', node_size=2000, font_size=10, font_weight='bold')
    plt.title("Graph Visualization")
    plt.show()

现在,我们 实现 DFS 遍历算法:

 # DFS function (optional, same as before)
visited = set()
def dfs(node):
    if node not in visited:
        print(node)
        visited.add(node)
        for neighbor in graph[node]:
            dfs(neighbor)
# Start DFS at node 'A'
dfs('A')

让我们来看一下 代码:

  • 该图表示为一个 邻接表

  • 我们使用一个 访问 集合来确保节点不被重复访问

  • dfs 函数打印当前节点,标记其为已访问,并递归地对所有 未访问的邻居节点 进行调用。

对于给定的图,输出将如下所示:

<st c="20564" class="calibre11">A, B, D, E, F, C</st>

与广度优先搜索(BFS)相比,深度优先搜索(DFS)在内存使用上更为高效,特别是在处理深度图时。 这是因为 DFS 只需要跟踪当前路径和回溯信息,而 BFS 必须在每一层存储所有节点。 DFS 在路径寻找方面也非常有用,尤其是在需要探索所有可能路径的场景下,比如迷宫求解算法。 此外,DFS 经常用于拓扑排序 有向无环图 (DAGs),这一技术在任务调度和 解决依赖关系时非常有用。

然而,DFS 也有一些 局限性。 一个主要的缺点是,DFS 在无权图中可能无法找到最短路径,因为它可能在找到解决方案之前就探索了一个较深的路径,而这条路径可能不是最优的。 此外,在非常大或无限的图中,DFS 可能会陷入探索长路径或循环的困境,除非采取如循环检测等预防措施。 在诸如 Python 这样的语言中,递归深度有限,如果图特别深,使用递归的 DFS 可能会导致栈溢出错误。 为了避免这种情况,可以通过使用显式栈来迭代实现 DFS,而不依赖 递归。

DFS 的 时间复杂度是 O(V+E),其中 V。 这是因为 DFS 会访问图中的每个节点和每条边一次。 在一个稀疏图中,其中 E≈V,时间复杂度接近 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:miV</mml:mi>mml:mo)</mml:mo></mml:math>。另一方面,在一个稠密图中,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miE</mml:mi>mml:mo≈</mml:mo>mml:msupmml:mrowmml:miV</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:math>,时间复杂度 接近 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:msupmml:mrowmml:miV</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>

DFS 的空间复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:miV</mml:mi>mml:mo)</mml:mo></mml:math> 在最坏的情况下,由于递归栈的深度或迭代版本中使用的显式栈。 在最坏的情况下,如果图是一个长的线性链,栈可能会保存所有 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miV</mml:mi></mml:math> 节点。

DFS 有两种常见的变种: 先序 DFS,即在探索邻居之前先访问节点(如前面的示例所示),以及 后序 DFS,即只在访问完所有邻居后才访问节点。 同一图中(图 13**.5),如果我们执行后序 DFS,遍历的顺序将是:D --> F --> E --> B --> C --> A。 这些变种在不同的场景中很有用,比如树的遍历和需要特定顺序处理节点的算法。

DFS 在算法设计中有很多应用,包括在 AI 搜索算法中的使用。 例如,DFS 用于需要探索节点之间所有可能路径的问题,如解决谜题或在迷宫中寻找路径。 它对于检测有向图和无向图中的环路非常有效,帮助识别图结构中的循环。 此外,DFS 还可以用于查找图中的所有连通分量,特别是在无向图中。 在有向无环图(DAG)中,DFS 在拓扑排序中起着关键作用,这对于调度和依赖解析等任务至关重要。 这些多样化的应用突显了 DFS 在各种 计算问题中的重要性。

总之,DFS 是一种强大且高效的图遍历技术,特别适用于路径寻找和解决需要探索所有可能性的问题。 尽管它不能保证找到最短路径,但在内存效率至关重要或处理深度结构时,它表现得非常出色。 其主要的权衡是,对于大型图可能需要较长的搜索时间,并且在深度递归时可能会发生栈溢出。 在下一节中,我们将探讨广度优先搜索(BFS) 遍历方法。

广度优先搜索(BFS)图遍历

广度优先搜索(BFS) 是另一种 图遍历算法,它从给定的源节点开始,逐层探索节点。 与深度优先搜索(DFS)不同,DFS 会尽可能深入一个分支,直到回溯,而 BFS 会先探索一个节点的所有邻居,然后再移动到下一级的邻居。 这使得 BFS 在寻找 无权图中的最短路径时特别有效。

BFS 算法从根节点(或任意起始节点)开始,首先探索所有邻居节点。 在访问完当前层级的所有邻居后,算法会继续到下一层,访问那些邻居的邻居,以此类推。 遍历会持续进行,直到所有从起始节点可达的节点都被访问过。

BFS 的核心依赖于使用队列数据结构,这确保了节点按照正确的顺序被探索(先进先出 (FIFO))。 让我们考虑 图 13**.6 中包含六个节点的图。 节点 A开始 BFS,遍历顺序将是 A --> B --> C --> D --> E --> F --> C。 这种遍历方式会先完全探索一条路径(分支),然后再移动到下一条。

图 13.6:BFS 遍历示例图

图 13.6:BFS 遍历示例图

这是一个 Python 实现 BFS 的代码,使用了 队列:

 from collections import deque
# Graph represented as an adjacency list
graph = {
    'A': ['B', 'C'],
    'B': ['D', 'E'],
    'C': ['F'],
    'D': ['A'],
    'E': ['B','D'],
    'F': ['E','D']
}
# BFS function
def bfs(start_node):
    visited = set()           # Set to track visited nodes
    queue = deque([start_node])  # Initialize the queue with the starting node
    while queue:
        node = queue.popleft()  # Dequeue a node
        if node not in visited:
            print(node)
            visited.add(node)  # Mark it as visited
            queue.extend(graph[node])  # Enqueue all unvisited neighbors
# Start BFS at node 'A'
bfs('A')

这就是 代码的工作原理。 首先,我们使用邻接表表示图。 接下来,我们实现队列数据结构,这是 BFS 算法的核心。 最后,我们实现 BFS 遍历 算法本身:

  • 图的表示:图使用 邻接表 来表示。

  • 队列 deque 用于高效地处理队列操作(入队和 出队节点)

  • bfs:节点按照出队顺序被处理,它们的邻居被加入队列以供 进一步探索

对于给定的图,输出将会是: 如下所示:

 A, B, C, D, E, F

BFS 相对于 DFS 有几个优点 在无权图中,BFS 保证了第一次到达一个节点时,是通过从源节点出发的最短路径,这使得它在路径查找问题中非常理想。 此外,BFS 会在转向下一层之前,先探索当前层的所有节点,这在需要首先访问所有直接邻居的情况下尤为有用,例如在交通系统中寻找最短路径。 另外,BFS 在识别无向图中所有连通分量方面非常有效,因为它会系统地探索所有 可达的节点。

然而,BFS 也有其缺点。 它需要将当前层的所有节点存储在内存中,这可能导致显著的内存消耗,尤其是在分支因子较大的图中。 此外,BFS 对于深层结构的图可能效率较低,因为它是逐层探索的。 在图的深度较大而宽度不宽的情况下,DFS 可能是一个更高效的替代方案,因为它在探索时更侧重深度而非广度。

BFS 的时间复杂度是 O(V+E),因为在遍历过程中每个顶点和边都只会被处理一次。 相比之下,BFS 的空间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:miV</mml:mi>mml:mo)</mml:mo></mml:math>,因为在最坏的情况下,某一层的所有节点可能会同时被存储在队列中。 在分支因子大或层宽较大的图中,这一点尤其重要,因为内存需求可能 会大幅增加。

BFS 有两个显著的变种: 双向 BFS 多源 BFS。双向 BFS 用于 找到两个节点之间的最短路径。 它同时进行两个 BFS 遍历——一个从源节点开始, 一个从目标节点开始——直到两次搜索在中间相遇。 这种方法显著减少了搜索空间,使得在源节点和目标节点相距较远的情况下,比传统 BFS 更快。 多源 BFS 涉及多个起始点。 BFS 从所有源节点同时启动,允许从多个起点同时进行探索。 这种变种在需要从多个位置探索路径的场景中非常有用,例如在图中找到从多个源到目标的最短距离。

BFS 有着广泛的 应用范围,包括它在人工智能搜索策略中的使用。 其一个关键优势是能够在无权图中找到最短路径。 由于 BFS 会在深入之前先探索同一层级的所有节点,因此它可以保证节点首次被访问时,必定是通过最短路径。 这使得 BFS 非常适合用于路径寻找算法,例如导航系统或解决所有动作成本相等的谜题。

让我们修改 BFS 算法来找到 两个节点之间的最短路径:

 def bfs_shortest_path(start_node, target_node):
    visited = set()
    queue = deque([[start_node]])  # Queue stores paths
    while queue:
        path = queue.popleft()  # Dequeue the first path
        node = path[-1]  # Get the last node from the path
        if node == target_node:
            return path  # Return the path when target is reached
        if node not in visited:
            visited.add(node)
            for neighbor in graph[node]:
                new_path = list(path)
                new_path.append(neighbor)
                queue.append(new_path)  # Enqueue the new path
    return None  # Return None if there is no path
# Find the shortest path between 'A' and 'F'
print(bfs_shortest_path('A', 'F'))

对于给定的图, 节点 <st c="29782">A</st> <st c="29788">F</st> 之间的最短路径 如下所示:

 ['A', 'C', 'F']

另一个 BFS 的重要应用是在无向图中。 BFS 尤其适用于识别从给定起点可达的所有节点。 通过这种方式,BFS 可以高效地检测并标记连通分量,这在网络分析和社交 网络映射中至关重要。

此外,BFS 广泛用于树的层次遍历。 在这种情况下,BFS 会先访问每个深度级别的所有节点,然后才会继续到下一个深度层级,这使得它在节点的层级或等级很重要时成为理想的算法,例如在组织结构图、文件系统结构或 层次聚类中。

除了这些核心应用,BFS 还广泛应用于人工智能搜索算法中,与 DFS 一起成为两种主要的搜索技术。此外,BFS 还被用于在各种图相关问题中寻找最小生成树和最短路径:

  • BFS 在人工智能中的应用:BFS 作为许多人工智能搜索策略的基础,特别是那些需要逐层探索所有可能状态的策略,如游戏树或谜题求解

  • 寻找最小生成树:在无权图中,BFS 可以作为构建块,用来通过确保所有节点按最短路径顺序从源节点访问来找到最小生成树。

  • 网络广播:在计算机网络中,BFS 用于模拟广播路由,在这种情况下,信息必须在最短时间内发送到所有节点,这使得它在网络发现协议中至关重要,如开放最短路径优先OSPF)。

这些多样化的应用突出展示了 BFS 的多功能性,使其成为算法设计和实践应用中的基本工具,广泛应用于各个领域。

总结来说,BFS 是一种基本的图遍历技术,特别适用于当目标是逐层探索所有节点或在无权图中寻找最短路径时。虽然它在时间复杂度上效率较高,但由于其较高的内存需求,对于具有较大分支因子的图而言,可能会成为一个缺点。BFS 保证的最短路径特性以及其在各种算法任务中的多功能性使其成为许多现实世界应用中的强大工具。

在继续讨论下一个非线性数据结构之前,让我们通过总结图的意义和在算法设计中的应用来结束对图的讨论。图是极其多功能的结构,它使我们能够在网络、社会分析和人工智能等领域建模和解决各种问题。图可以表示实体之间的复杂关系,通过 BFS 和 DFS 等算法,我们可以高效地遍历、搜索和处理图数据。图在路径寻找、网络路由、环检测甚至层级问题解决等关键应用中发挥着核心作用。

它们在算法设计中的重要性不言而喻,因为它们为解决涉及连通性、优化和搜索策略的问题奠定了基础,无论是在理论领域还是实践领域。

是一种 层次化的非线性数据结构,由节点和连接这些节点的边构成。 树在各种应用中被广泛使用,如数据组织、数据库、网络结构等。 树有一个根节点,所有其他节点都通过父子关系连接。 树的结构确保没有循环,每个子节点恰好有 一个父节点。

在本节中,我们将探索不同类型的树及其特性,以及如何表示树和 讨论 两种重要类型: 二叉搜索树 (BSTs) 和 红黑树

不同类型的树及其特性

树木有许多类型,每种类型都有适合不同应用的独特特性。 以下是一些最 常见的类型:

  • 通用树:通用树是一种树,其中任何节点可以 拥有任意数量的子节点。 这种树类型可用于表示层次化数据,如文件系统或组织结构图。 图 13**.7 展示了一个通用树的示例。

图 13.7:一个通用树的示例

图 13.7:一个通用树的示例

  • 二叉树:二叉树是一种每个节点最多有两个子节点的树,子节点分别称为 左子节点 右子节点。它是计算机科学中最常用的树结构之一。 图 13**.7中,所有以 节点 1 节点 5 节点 6 为根的子树是二叉树的示例。

  • 完全二叉树:如果每个节点有零个或两个子节点,则称为完全二叉树。 没有节点只有一个子节点。 图 13**.7中,以 节点 5 为根的子树是一个完全 二叉树。

  • 完全二叉树:一个 完全二叉树是一个 所有层级都已完全填充,除非是最后一层,该层必须从左到右填充。 一个著名的完全二叉树是堆结构,我们将在本章末尾讨论它。 本章。

  • 平衡二叉树:如果 任何节点的左右子树的高度差不超过一,则该树被认为是平衡的。 平衡树更受欢迎,因为它们可以确保搜索、插入和删除操作的最佳性能。 图 13**.8中,二叉树是 完全平衡的。

图 13.8:一个完全平衡的二叉树

图 13.8:一个完全平衡的二叉树

图 13**.9 展示了一个非平衡二叉树的反例。 在这棵树中,子树的高度差异很大,违反了平衡二叉树的特性,即任何节点的左右子树高度差不应超过一。 这种不平衡会导致搜索、插入和删除等操作效率低下,因为树的结构开始类似于 线性链表。

图 13.9:一个非平衡二叉树的反例

图 13.9:一个非平衡二叉树的反例

  • AVL 树:一种 AVL 树(以发明者 Adelson-Velsky 和 Landis 命名)是一种自平衡的二叉查找树(BST)。 为每个节点维护一个平衡因子(即左右子树高度的差值),确保在插入 和删除操作后,树保持平衡。

  • B 树:B 树 是一种自平衡的树形数据结构,用于维护 排序的数据,并允许在对数时间内进行搜索、顺序访问、插入和删除操作。 B 树通常用于数据库 和文件系统中。

  • 红黑树:一种 红黑树是另一种类型的自平衡二叉查找树。 树中的每个节点都会被分配一个颜色(红色或黑色),以确保树保持平衡,这样可以保证插入 和删除操作的最坏时间复杂度更好。

接下来,我们将讨论两种常见的树表示方法: 链式表示法 数组表示法。每种方法都有不同的优点,适用于特定类型的操作和树结构。 通过了解这两种方法,我们可以为特定应用选择最有效的表示法。

树的表示法

树可以 以多种方式表示,具体选择取决于使用场景和操作的复杂度。 接下来我们将详细探讨这一点。

链式表示法

二叉树的链式表示法中,每个节点包含数据和指向其左子节点和右子节点的指针(或引用)。 与数组表示法相比,这种表示法更灵活,因为它不要求树必须是完全二叉树。 相反,每个节点直接引用其子节点,从而允许 不规则结构的存在。

这种 表示法通常用于二叉树和二叉搜索树(BST),其中节点可以有任意的排列。 树中的每个节点被定义为一个类或结构体,包含一个值和指向其左子节点和右子节点的两个指针。 子节点。

以下代码是一个使用 链式表示法在 Python 中实现节点的示例:

 # Definition of a TreeNode class in Python
class TreeNode:
    def __init__(self, key):
        self.key = key       # Node value
        self.left = None     # Pointer to left child
        self.right = None    # Pointer to right child
# Creating nodes and linking them to form a binary tree
root = TreeNode(10)          # Root node
root.left = TreeNode(5)      # Left child of root
root.right = TreeNode(20)    # Right child of root
# Adding more nodes to the tree
root.left.left = TreeNode(3) # Left child of node with value 5
root.left.right = TreeNode(7) # Right child of node with value 5
root.right.left = TreeNode(15) # Left child of node with value 20
root.right.right = TreeNode(25) # Right child of node with value 20
# Function to perform an in-order traversal of the tree
def inorder_traversal(node):
    if node:
        inorder_traversal(node.left)
        print(node.key, end=' ')
        inorder_traversal(node.right)
# In-order traversal of the tree
print("In-order Traversal:")
inorder_traversal(root)

接下来 我们来解释一下 代码的各个组件:

  • TreeNode:每个节点包含一个值(key)和两个指针(left right),分别引用左子节点和右子节点。

  • 创建节点:我们创建节点并将它们链接在一起,形成一个 二叉树

  • 中序遍历inorder_traversal 函数递归地访问左子树、根节点,然后是右子树,对于 二叉搜索树(BST) 以排序顺序打印节点。

链式表示法适用于任何类型的二叉树,无论是完全二叉树、平衡二叉树还是不规则二叉树。 对于稀疏树,这种方法特别节省内存,因为只有实际存在的节点才会分配内存。 这样就不需要为不存在的节点连续分配内存,这与 数组表示法 不同。

此外, 链式表示法对于动态操作,如插入、删除和遍历,更加灵活。 由于每个节点直接引用其子节点,修改树结构非常直接,不需要像数组表示中那样重新排序或移动元素。 这使得它非常适用于频繁增长或变化的树,例如 在二叉搜索树(BST)中。

数组表示

这种 方法常用于表示 完全二叉树,例如堆结构。 在这种方法中,二叉树作为数组或列表存储,其中根节点位于索引 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mn0</mml:mn></mml:math>。对于索引 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math>上的任何节点,其子节点的位置如下:

  • 左子节点位于 索引 2i+1

  • 右子节点位于 索引 2i+2

这种基于数组的表示对于完全二叉树非常高效,因为它避免了需要指针来追踪父子关系。 它还允许通过计算索引快速访问子节点或父节点。 以下是一个简单的 Python 示例,演示了一个小的完全 二叉树的数组表示:

 # Array representation of a complete binary tree
binary_tree = [10, 5, 20, 3, 7, 15, 25]
# Accessing elements
root = binary_tree[0]
left_child_of_root = binary_tree[2 * 0 + 1]  # index 1
right_child_of_root = binary_tree[2 * 0 + 2]  # index 2
# Display the values
print(f"Root: {root}")
print(f"Left Child of Root: {left_child_of_root}")
print(f"Right Child of Root: {right_child_of_root}")

这种 基于数组的结构非常适合堆,其中 插入和删除操作需要高效的重新排序以保持堆属性。 基于索引的父子关系的简洁性使得这种表示方法在完全 二叉树中既快速又节省内存。

父数组表示

另一种 表示树的方法是通过将每个节点的父节点存储在一个数组中。 在这种方法中,数组的每个索引对应一个节点,索引处的值表示该节点的父节点。 根节点被赋予一个特殊的值(通常是 <st c="41295">-1</st>),以表示它没有父节点。

这种表示法在我们需要从父子关系重建树时特别有用,或者当树以某种方式存储,使得不需要直接访问子节点时 尤其适用。

这里是一个简单的 Python 实现,用于通过父节点数组表示树。 第一部分是一个根据父节点数组构建树的函数:

 def build_tree(parent_array):
    n = len(parent_array)
    nodes = [None] * n
    root = None
    # Create tree nodes for each index
    for i in range(n):
        nodes[i] = TreeNode(i)
    # Assign parents to each node
    for i in range(n):
        if parent_array[i] == -1:
            root = nodes[i]  # This is the root node
        else:
            parent_node = nodes[parent_array[i]]
            if parent_node.left is None:
                parent_node.left = nodes[i]
            else:
                parent_node.right = nodes[i]
    return root

接下来,我们 定义 TreeNode 类:

 class TreeNode:
    def __init__(self, key):
        self.key = key
        self.left = None
        self.right = None

最后,我们构建一个示例父节点数组,其中 <st c="42291">-1</st> 表示根节点:

 parent_array = [-1, 0, 0, 1, 1, 2, 2]
# Build the tree from the parent array
root = build_tree(parent_array)
# Function to perform an in-order traversal of the tree
def inorder_traversal(node):
    if node:
        inorder_traversal(node.left)
        print(node.key, end=' ')
        inorder_traversal(node.right)
# In-order traversal of the tree
print("In-order Traversal:")
inorder_traversal(root)

让我们 解释一下代码的各个组成部分:

  • 父节点数组:该数组表示每个节点的父节点。 例如,如果 parent_array[3] = 1,则意味着 节点 3 的父节点是 节点 1 。根节点的值为 -1,表示它没有父节点。

  • 构建树:我们首先创建一个节点数组,然后通过父节点数组将每个节点链接到它的父节点。 这些节点根据可用性,作为左子节点或右子节点相连接。

  • 中序遍历:我们进行树的中序遍历,以按照它们的 排序顺序访问节点。

父节点数组表示法有几个优点。 其中一个显著的好处是它的空间效率。 由于这种方法仅存储父子关系,它省去了额外指向左子节点和右子节点的指针,从而使其成为一种紧凑的结构。 这一特点使得它特别适用于内存 有限的环境。

另一个优点是它在从给定的父子关系中重建树时非常有用,比如在文件系统、组织结构图或其他层次结构中。 这种方法使得树的重建变得简单而高效。

此外,在需要直接访问父节点的应用中,它也非常高效。 由于数组中的每个索引都对应一个特定的节点并存储其父节点,检索任何节点的父节点可以在 常数时间内完成。

最后, 父节点数组表示法非常适合存储在外部存储器中的树结构。 它需要最小的数据存储空间,这使得它在数据库或大型系统中尤其有用,在这些系统中,树结构需要按需重建,而不占用过多空间。

这种表示方式在处理静态树结构时非常有用,尤其是在只需要存储父节点关系即可进行预期操作的情况下。

二叉搜索树

A 二叉搜索树(BST) 是一种二叉树,其中的节点按照特定方式组织:每个节点的左子树只包含值小于该节点的节点,而右子树只包含值大于该节点的节点。 这一特性使得二叉搜索树在搜索操作中非常高效。 图 13**.10 展示了一个简单的二叉搜索树。 以下是 二叉搜索树的 一些属性:

  • 左子节点包含的值小于 父节点的值

  • 右子节点包含的值大于 父节点的值

  • 二叉搜索树的中序遍历会生成一个 有序序列

图 13.10:一个示例二叉搜索树

图 13.10:一个示例二叉搜索树

在探讨二叉搜索树的操作之前,首先了解如何遍历二叉搜索树非常重要。 在二叉搜索树中,遍历类似于图的遍历,指的是访问并处理树中每个节点的过程。 二叉搜索树有三种常见的遍历方法(或树走访方式):中序遍历、前序遍历和后序遍历。 每种遍历方法都有特定的节点访问顺序,且它们的时间复杂度都是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 因为每个节点只会被访问一次。

中序遍历

中序遍历 访问 节点的顺序为:左子树,根节点,右子树。 在二叉搜索树(BST)中,中序遍历将按升序访问节点。 中序遍历的步骤如下: 如下所示:

  1. 遍历 左子树。

  2. 访问 根节点。

  3. 遍历 右子树。

对于图示的二叉搜索树(BST) 图 13**.11,我们希望对树执行中序遍历。

图 13.11:一个示例二叉搜索树(BST)

图 13.11:一个示例二叉搜索树(BST)

以下是用于 中序遍历的 Python 代码:

 # Definition of TreeNode class
class TreeNode:
    def __init__(self, key):
        self.key = key
        self.left = None
        self.right = None
# Function for in-order traversal
def inorder_traversal(node):
    if node:
        inorder_traversal(node.left)
        print(node.key, end=' ')
        inorder_traversal(node.right)

首先,我们使用以下 Python 代码构建 二叉搜索树(BST):

 # Example: Build the BST
root = TreeNode(22)
root.left = TreeNode(35)
root.right = TreeNode(30)
root.left.left = TreeNode(5)
root.left.right = TreeNode(15)
root.right.left = TreeNode(25)
root.right.right = TreeNode(35)

然后,我们调用 <st c="46688">inorder_traversal</st> 来执行中序 二叉搜索树(BST)遍历:

 # Perform in-order traversal
print("In-Order Traversal:")
inorder_traversal(root)

中序遍历 生成 如下:

<st c="46866" class="calibre11">5 10 15 20 25 30 35</st>

先序遍历

先序遍历 访问 节点的顺序为:根节点,左子树,右子树。 这种方法对于创建树的副本或打印树结构非常有用。 先序遍历的步骤如下: 如下所示:

  1. 访问 根节点。

  2. 遍历 左子树。

  3. 遍历 右子树。

先序遍历将按照以下顺序访问二叉搜索树(BST)中的节点: 图 13**.11 中的顺序: <st c="47307">20, 10, 5, 15, 30,</st> <st c="47326">25, 35</st>

以下是用于 先序遍历的 Python 代码:

 # Function for pre-order traversal
def preorder_traversal(node):
    if node:
        print(node.key, end=' ')
        preorder_traversal(node.left)
        preorder_traversal(node.right)
# Perform pre-order traversal
print("Pre-Order Traversal:")
preorder_traversal(root)

后序遍历

后序遍历 访问 节点的顺序为:左子树,右子树,根节点。 这种遍历通常用于树的删除操作中,因为在删除父节点之前需要先删除子节点。 后序遍历的步骤如下: 如下所示:

  1. 遍历 左子树。

  2. 遍历 右子树。

  3. 访问 根节点。

图 13**.11中,后序遍历将按照以下顺序访问节点: <st c="48117">5, 15, 10, 25, 35,</st> <st c="48136">30, 20</st>

以下是 后序遍历的 Python 代码:

 # Function for post-order traversal
def postorder_traversal(node):
    if node:
        postorder_traversal(node.left)
        postorder_traversal(node.right)
        print(node.key, end=' ')
# Perform post-order traversal
print("Post-Order Traversal:")
postorder_traversal(root)

总之,先序遍历按顺序访问节点(左、根、右),通常用于 从二叉搜索树中提取排序数据。 先序遍历按顺序访问节点(根、左、右),适用于复制树结构或打印树。 后序遍历按顺序访问节点(左、右、根),有助于像树删除这样的任务,其中需要先处理子节点再处理父节点。

在二叉搜索树中,主要操作——插入、删除和搜索——依赖于树的结构和属性。 这些操作的效率很大程度上取决于树是否平衡。 接下来,让我们详细探讨这些操作。

二叉搜索树中的搜索操作

在二叉搜索树中搜索 利用了左子树的值小于当前节点,右子树的值大于当前节点的特性。 这使得我们可以在每一步有效地将搜索空间减半,类似于 二分搜索:

  • 平均情况(平衡树):在平衡的二叉搜索树(如 图 13**.6所示),搜索 操作需要 O(logn) 时间。 这是因为树的高度相对于节点数量是对数级别的,我们在 每一层都缩小了搜索空间。

  • 最坏情况(不平衡树):如果二叉搜索树(BST)不平衡,搜索的时间复杂度 可能接近 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>,此时树形结构类似于链表。 在这种情况下,树的高度会随着节点数量的增加而线性增长,导致 搜索效率低下。

对于极端情况,如 图 13**.12中偏斜的树,搜索复杂度达到 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>,在这种情况下,每个节点只有一个子节点,树退化成一个 线性结构。

图 13.12:极端不平衡二叉搜索树的示例

图 13.12:极端不平衡二叉搜索树的示例

以下是一个在 BST 中搜索的 Python 实现。 第一部分是对 BST 中 <st c="50283">TreeNode</st> 类的定义:

 class TreeNode:
    def __init__(self, key):
        self.key = key
        self.left = None
        self.right = None

为了构建二叉搜索树(BST),我们使用 <st c="50467">insert</st> 函数实现插入操作 ,代码如下:

 Function to insert a node in the BST
def insert(node, key):
    # If the tree is empty, return a new node
    if node is None:
        return TreeNode(key)
    # Otherwise, recur down the tree
    if key < node.key:
        node.left = insert(node.left, key)
    else:
        node.right = insert(node.right, key)
    return node

以下是 <st c="50781">search</st> 操作的实现代码:

 # Function to search a key in the BST
def search(node, key):
    # Base case: the node is None (key not found) or the key matches the current node's key
    if node is None or node.key == key:
        return node
    # If the key is smaller than the node's key, search the left subtree
    if key < node.key:
        return search(node.left, key)
    # Otherwise, search the right subtree
    return search(node.right, key)

让我们创建根节点并将元素插入 二叉搜索树(BST):

 root = None
keys = [20, 10, 30, 5, 15, 25, 35]
for key in keys:
    root = insert(root, key)

下面展示了如何在 BST 中搜索一个键: 在 BST 中搜索:

 search_key = 25
found_node = search(root, search_key)
# Output the result
if found_node:
    print(f"Key {search_key} found in the BST.")
else:
    print(f"Key {search_key} not found in the BST.")

让我们解释一下 算法中的重要部分:

  • TreeNode:每个 BST 中的节点包含一个键(节点的值),以及指向其左子节点和 右子节点的引用。

  • insert:insert 函数将值插入到 BST 中。 它递归遍历树并根据 BST 的属性将新节点插入到正确的位置。

  • search:search 函数递归地查找给定的键。 如果当前节点的键与正在搜索的键匹配,它会返回该节点。 否则,根据键是小于还是大于当前节点的键,它会继续在左子树或右子树中进行搜索。

在二叉搜索树中,查找操作的时间复杂度取决于树的平衡程度。 在最坏的情况下,时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:miL</mml:mi></mml:mrow></mml:mfenced></mml:math>,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miL</mml:mi></mml:math> 是二叉搜索树的深度。 在极度不平衡的情况下,如果二叉搜索树实际上形成了线性结构,则时间复杂度变为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>。然而,在完全平衡的二叉搜索树中,时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>,从而确保更高效的查找。

二叉搜索树中的插入操作

<st c="52886">null</st>),新节点被 插入到该位置。

让我们简要讨论一下在二叉搜索树(BST)中插入操作的时间复杂度:与搜索类似,插入操作的时间复杂度取决于树的平衡性。 在最坏的情况下,当树非常不平衡并且类似于线性结构时,时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 表示 节点的数量。 相比之下,在平衡的二叉搜索树中,插入操作的时间复杂度为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrowmml:min</mml:mi></mml:mrow></mml:mrow></mml:mrow></mml:mfenced></mml:math>,因为插入操作涉及到遍历 树的高度:

  • 平均情况(平衡树):在平衡的二叉搜索树中,插入操作平均需要 O(logn) 的时间,因为我们本质上是在执行搜索,找到合适的位置来插入 新节点

  • 最坏情况(不平衡树):与搜索类似,如果二叉搜索树不平衡,插入时间可能退化为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:min</mml:mi></mml:mrow></mml:mfenced></mml:math>,尤其是当插入的值使树变得 倾斜

在提供的 Python 搜索算法代码中,<st c="53802">insert</st> 函数负责将节点插入到二叉搜索树中,同时保持树的属性。 它通过递归方式工作,找到插入 新节点的正确位置。

让我们来看看 <st c="53988">insert</st> 函数是如何工作的。 如果树为空(即当前节点是 <st c="54059">None</st>),函数会创建一个新的节点,并返回它,从而使其成为根节点或叶子节点。 如果要插入的键小于当前节点的键,函数会递归地向左子树移动,以找到合适的位置。 如果要插入的键大于当前节点的键,函数会递归地向右子树移动。 一旦找到合适的位置,新的节点会作为左子节点或右子节点添加。

这是一个简单的 Python 代码实现,用于在 二叉搜索树(BST)中插入:

 # Function to insert a node in the BST
def insert(node, key):
    # If the tree is empty, return a new node
    if node is None:
        return TreeNode(key)
    # Otherwise, recur down the tree
    if key < node.key:
        node.left = insert(node.left, key)
    else:
        node.right = insert(node.right, key)
    return node

例如,如果我们插入值 <st c="54915">20</st>, <st c="54919">10</st>, <st c="54923">30</st>, <st c="54927">5</st>, <st c="54930">15</st>, <st c="54934">25</st>, 和 <st c="54942">35</st> 使用这个 <st c="54956">insert</st> 函数,它将创建如 图 13**.13所示的二叉搜索树(BST)。

图 13.13:表示[20, 10, 30, 5, 15, 25, 35]的最终二叉搜索树(BST)

图 13.13:表示[20, 10, 30, 5, 15, 25, 35]的最终二叉搜索树(BST)

35 的插入过程在树中被高亮显示。 插入 <st c="55165">35</st>时,首先将其与根节点 <st c="55211">20</st>进行比较。由于 <st c="55221">35</st> 大于 <st c="55240">20</st>,我们移动到右子树。 接下来, <st c="55280">35</st> 与节点 <st c="55309">30</st>进行比较。由于 <st c="55319">35</st> 大于 <st c="55338">30</st>,它将被插入为节点 <st c="55389">30</st>的右子节点。 关键字比较过程确保了 <st c="55433">35</st> 根据二叉搜索树的属性正确地放置,其中右子树中的值大于 父节点。

总结来说, <st c="55584">insert</st> 函数通过将每个新值与现有节点值进行比较,确保每个新值都被放置在树中的正确位置。 它保持了二叉搜索树的属性,即左子树中的所有值都小于父节点,而右子树中的所有值 都大于父节点。

二叉搜索树(BST)中的删除操作

在二叉搜索树(BST)中, 删除操作比插入操作更复杂,因为我们需要在删除节点后维护二叉搜索树的特性。 删除节点时,需要考虑三种可能的情况: 删除一个节点:

  • 删除叶子节点:没有子节点的节点可以 直接删除。

  • 删除具有一个子节点的节点:该节点将被 其子节点替代。

  • 删除具有两个子节点的节点:该节点将被其中序前驱节点(左子树中的最大节点)或中序后继节点(右子树中的最小节点)替换。 替换后,替换源节点也必须 被删除。

以下是实现二叉搜索树(BST)中删除操作的 Python 代码。 首先,我们必须定义 <st c="56642">TreeNode</st> 类(参见前面的例子)。 接下来,我们使用前面讨论过的 <st c="56733">insert</st> 函数来构建树。 接下来,我们实现 <st c="56800">min_value_node</st> 函数,以便在删除节点时找到后继节点:

 # Function to find the minimum value node in the right subtree (in-order successor)
def min_value_node(node):
    current = node
    while current.left is not None:
        current = current.left
    return current

最后,我们实现 <st c="57081">delete_node</st> 函数来处理删除操作的三种情况 在二叉搜索树(BST)中的实现:

 # Function to delete a node from the BST
def delete_node(root, key):
    # Base case: the tree is empty
    if root is None:
        return root
    # If the key to be deleted is smaller than the root's key, go to the left subtree
    if key < root.key:
        root.left = delete_node(root.left, key)
    # If the key to be deleted is greater than the root's key, go to the right subtree
    elif key > root.key:
        root.right = delete_node(root.right, key)
    # If key is equal to the root's key, this is the node to be deleted
    else:
        # Case 1: Node with only one child or no child
        if root.left is None:
            return root.right
        elif root.right is None:
            return root.left
        # Case 2: Node with two children
        # Get the in-order successor (smallest in the right subtree)
        temp = min_value_node(root.right)
        # Replace the current node's key with the in-order successor's key
        root.key = temp.key
        # Delete the in-order successor
        root.right = delete_node(root.right, temp.key)
    return root

以下是一个使用 <st c="58142">insert</st> 函数构建二叉搜索树(BST)并删除值为 <st c="58194">30</st>的节点的例子:

 # Create a BST and insert values into it
root = None
keys = [20, 10, 30, 5, 15, 25, 35]
for key in keys:
    root = insert(root, key)
# Delete a node from the BST
delete_key = 30
root = delete_node(root, delete_key)
# Function to perform in-order traversal
def inorder_traversal(node):
    if node:
        inorder_traversal(node.left)
        print(node.key, end=' ')
        inorder_traversal(node.right)
# Perform in-order traversal after deletion
print("In-order Traversal after Deletion:")
inorder_traversal(root)

让我们解释一下 这个例子。 我们首先比较 节点 30 和根节点 节点 20。由于 30 大于 20,我们移动到右子树。 我们发现 节点 30 并注意到它有两个子节点(节点 25 35)。 我们用它的中序后继节点 节点 30 替换该节点, 节点 35,然后从原位置删除 节点 35 图 13**.14 展示了移除 节点 35后的二叉搜索树(BST)。

图 13.14:移除 35 后的二叉搜索树(见图 13.9)

图 13.14:移除 35 后的二叉搜索树(见图 13.9)

在二叉搜索树(BST)中,删除操作的时间复杂度是 O(logn) 平均情况下,当树是平衡时,因为我们只需遍历树的高度来定位并删除节点。 然而,在最坏的情况下,时间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math>,特别是在树不平衡并且呈现为 链表的情况下。

在下一节中,我们将探讨堆结构,它在排序算法和其他需要高效 数据管理的应用中发挥着至关重要的作用。

A 是一种特殊类型的二叉树,满足堆的性质。 在堆中,父节点总是与其子节点之间遵循特定的顺序关系。 堆常用于各种算法,特别是在排序和优先队列中,因为它们能够高效地访问最小或最大元素。

根据它们遵循的顺序性质,堆主要有两种类型:

  • 最大堆:在最大堆中,每个节点的值都大于或等于其子节点的值,最大的元素位于根节点。 最大堆通常用于需要高效访问最大元素的算法中,比如堆排序和优先队列的实现。 在最大堆中,堆的性质如下: 对于每个节点 Typeequationhere. * Ai≥A2i+1(左子节点)

    • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mii</mml:mi></mml:mrow></mml:mfenced>mml:mo≥</mml:mo>mml:miA</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mn2</mml:mn>mml:mii</mml:mi>mml:mo+</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced></mml:math> (右子节点),其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi></mml:math> 堆的数组表示
  • 最小堆:在最小堆中, 每个节点的值都小于或等于其子节点的值。 最小元素始终位于根节点。 最小堆通常用于像 Dijkstra 最短路径算法和 Prim 最小生成树这样的算法中。 在这种情况下,堆的性质如下:对于每个节点 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math>,这是 成立的:

    • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mii</mml:mi></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:miA</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mn2mml:mii</mml:mi>mml:mo+mml:mn1</mml:mn></mml:mrow></mml:mfenced></mml:math> (左子节点)

    • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miA</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mii</mml:mi></mml:mrow></mml:mfenced>mml:mo≤</mml:mo>mml:miA</mml:mi><mml:mfenced open="[" close="]" separators="|">mml:mrowmml:mn2mml:mii</mml:mi>mml:mo+mml:mn2</mml:mrow></mml:mfenced></mml:math> (右子节点)

堆通常表示为存储在数组中的完全二叉树。 在完全二叉树中,情况是这样的: 如下所示:

  • 根节点位于 索引 0

  • 对于索引 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math>处的节点,左子节点位于索引 2i+1,右子节点位于索引 2i+2

  • 位于索引 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mii</mml:mi></mml:math> 的节点的父节点位于 索引 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mtextfloor</mml:mtext><mml:mfenced separators="|">mml:mrow<mml:mfenced separators="|">mml:mrowmml:mii</mml:mi>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow></mml:mfenced>mml:mo/</mml:mo>mml:mn2</mml:mn></mml:mrow></mml:mfenced></mml:math>

这种数组表示法使得堆能够高效地存储在内存中,而无需使用指针来表示子节点(见 图 13**.15)。

图 13.15:一个示例最大堆

图 13.15:一个示例最大堆

索引 1 2 3 4 5 6 7 8 9 10 11 12 13
节点 98 81 86 63 21 68 18 10 51 4 14 1 50

图 13.16:图 13.15 中最大堆的数组表示

现在我们已经了解了堆的属性和堆的表示方法,接下来让我们探讨堆上的操作。

堆操作

堆的 主要操作包括 插入 删除 堆化 (用于维护堆的属性)。 这些操作都依赖于堆的属性,以确保结构保持有效的 堆。

堆中的插入

要将一个元素插入堆中,我们首先将元素添加到数组的最后一个位置(堆的末尾)。 然后,执行 <st c="61991">堆化向上</st> 操作,涉及将插入的元素与其父元素进行比较。 如果堆的属性被破坏,我们交换这两个元素。 此过程会一直重复,直到恢复堆的属性。

以下是插入操作的 Python 代码,适用于 最大堆:

 def heapify_up(heap, index):
    parent = (index - 1) // 2
    if index > 0 and heap[parent] < heap[index]:
        # Swap the parent and current node
        heap[parent], heap[index] = heap[index], heap[parent]
        # Recursively heapify the parent node
        heapify_up(heap, parent)
def insert_max_heap(heap, element):
    heap.append(element)
    heapify_up(heap, len(heap) - 1)

以下是构建 最大堆的例子:

 # Example usage
heap = []
insert_max_heap(heap, 20)
insert_max_heap(heap, 15)
insert_max_heap(heap, 30)
insert_max_heap(heap, 5)
insert_max_heap(heap, 40)
print("Heap after insertions:", heap)

插入操作的时间复杂度是 O(logn),因为我们可能需要交换树中的元素以恢复 堆的属性。

堆中的删除

在堆中,删除 通常涉及移除根元素(在最大堆中是最大值,在最小堆中是最小值)。 删除过程分为 三步:

  1. 用数组中的最后一个元素替换根元素。

  2. 数组中移除最后一个元素。

  3. 堆化根元素的过程涉及将其与子节点进行比较。在堆属性被破坏的情况下,会在最大堆中将根与最大的子节点交换,或者在最小堆中将根与最小的子节点交换。此过程会持续直到堆属性恢复

这里是一个用于在最大堆中删除元素的简单 Python 代码

 def heapify_down(heap, index):
    largest = index
    left = 2 * index + 1
    right = 2 * index + 2
    if left < len(heap) and heap[left] > heap[largest]:
        largest = left
    if right < len(heap) and heap[right] > heap[largest]:
        largest = right
    if largest != index:
        heap[index], heap[largest] = heap[largest], heap[index]
        heapify_down(heap, largest)
def delete_max_heap(heap):
    if len(heap) == 0:
        return None
    if len(heap) == 1:
        return heap.pop()
    root = heap[0]
    heap[0] = heap.pop()  # Move last element to the root
    heapify_down(heap, 0)  # Restore heap property
    return root

让我们用一个示例最大堆并使用delete_max_heap删除根元素:

 heap = [40, 30, 20, 5, 15]
deleted = delete_max_heap(heap)
print("Heap after deletion of max element:", heap)

删除最大元素后的堆是[30, 15, 20, 5]

删除操作的时间复杂度是O(logn),因为我们可能需要交换树中的元素以恢复堆的堆属性

堆化(构建堆)

为了从任意数组构建堆,我们使用堆化过程。从第一个非叶子节点开始,向上遍历至根节点,确保堆属性得以保持。考虑以下heapify的 Python 实现:

 def heapify(heap, n, i):
    largest = i
    left = 2 * i + 1
    right = 2 * i + 2
    if left < n and heap[left] > heap[largest]:
        largest = left
    if right < n and heap[right] > heap[largest]:
        largest = right
    if largest != i:
        heap[i], heap[largest] = heap[largest], heap[i]
        heapify(heap, n, largest)

如我们所见,heapify函数递归调用自身,以确保整个树的堆属性得以保持。如果在任何节点发现堆属性被破坏,heapify将继续向下遍历树,通过比较和交换节点来修复结构,直到堆属性完全恢复

以下是一个使用heapify递归算法构建最大堆的 Python 代码:

 def build_max_heap(a):
    n = len(a)
    # Start from the first non-leaf node and heapify each node
    for i in range(n // 2 - 1, -1, -1):
        heapify(a, n, i)

以下是一个简单的build_max_heap用法示例:

 arr = [5, 15, 20, 30, 40]
build_max_heap(a)
print("Array after building max-heap:", a)

建立最大堆后,表示最大堆的数组[40, 30, 20, 5, 15]

构建堆的时间复杂度是 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mi>mml:mo)</mml:mo></mml:math> 因为每个节点最多需要 O(logn) 次交换,但大多数节点位于树的底部,需要 较少的交换。

让我们探索堆的一个主要应用 :堆排序。

堆排序

堆排序 是一种 高效的基于比较的排序算法,利用堆数据结构(尤其是最大堆)对元素进行排序,其时间复杂度为 O(nlogn) 它的工作方式首先将输入数组转换为最大堆,然后重复提取最大元素(堆的根),以构建排序好的输出。 这个过程确保元素按升序排列。 以下概述 执行堆排序所涉及的步骤:

  1. 从输入数组构建一个最大堆。

  2. 交换根(最大元素)与 最后一个元素。

  3. 减小堆的大小并 堆化 根元素。

  4. 重复这个过程直到堆 为空。

这是堆排序的 Python 实现:

 def heapsort(arr):
    n = len(arr)
    build_max_heap(arr)  # Step 1: Build a max-heap
    for i in range(n - 1, 0, -1):
        arr[0], arr[i] = arr[i], arr[0]  # Step 2: Swap root with last element
        heapify(arr, i, 0)  # Step 3: Heapify the reduced heap

让我们通过一个例子来详细讨论堆排序算法:

 a = [5, 15, 20, 30, 40]
heapsort(arr)
print("Sorted array:", arr)

输出将会 如下所示:

<st c="67187" class="calibre11">Sorted array: [5, 15, 20, 30, 40]</st>

堆排序的时间 复杂度是 O(nlogn),因为构建堆需要 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:min</mml:mo>mml:mo)</mml:mo></mml:math>,而每次提取最大值(在最大堆中)或最小值(在最小堆中)需要 O(logn) ,总共需要 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 个元素。 正如在 第六章中讨论并展示的那样,堆排序的时间复杂度无法优于 O(nlogn) ,因为它是一种基于比较的排序算法,这对此类算法的时间复杂度设定了下界。 堆排序是一种就地排序算法,其空间复杂度 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi>mml:mo(</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:math>

通过堆结构,我们结束了关于非线性数据结构的讨论。 本章提供了这一重要数据结构类的关键要点,但并不打算作为一本专门讲解数据结构的书籍的全面替代。 它提供了一个概览,强调了它们在算法设计中的作用,更多的细节可以在 专业书籍中找到。

总结

在本章中,我们探讨了非线性数据结构的关键概念和应用,这些结构对于设计高效的算法至关重要。 我们首先讨论了非线性结构的一般特性,强调了它们在组织和访问模式方面与线性数据结构的不同。 我们详细介绍了两个主要类别:图和树。 图被介绍为建模关系的多功能结构,而树则提供了数据的更层次化的组织方式。 我们考察了不同类型的树,如二叉搜索树,讨论了它们的特性、操作和在 算法设计中的应用。

本章以堆作为重点,堆是二叉树的一种特殊形式,广泛应用于优先队列和排序算法(如堆排序)中。 我们介绍了堆的构建过程,如何通过插入、删除和堆化操作维护堆的性质,以及堆在排序中的作用。 总体而言,本章为非线性数据结构提供了基础理解,并强调了它们在高效处理和操作复杂数据关系中的重要性。 本章结束时,我们的算法讨论暂时告一段落。 然而,在下一章中,我们将探讨算法发展中的新兴趋势和未来方向。

参考文献及进一步阅读

  • 《算法导论》. 作者:Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, 和 Clifford Stein. 第四版. MIT 出版社. 2022 年:

    • 第六章 6, 堆排序

    • 第十二章, 二叉 查找树

    • 第二十二章, 基础 图算法

  • 《C++中的数据结构与算法分析》 . 作者:Mark A. Weiss. 第四版. Pearson. 2012 年:

    • 第四章 4,

    • 第五章, 二叉 查找树

    • 第六章 6,

    • 第九章, 图算法

  • 算法. 作者:R. Sedgewick,K. Wayne。 第四版。 Addison-Wesley 出版社。 2011 年。

    • 第三章, 查找(二叉 搜索树)

    • 第四章, 排序(堆排序)

    • 第五章 ,

第十八章:第四部分:下一步

这一部分展望了算法的未来,探索了新兴趋势和计算领域的发展变化。 它讨论了算法扩展、构建上下文感知系统,以及确保伦理决策在 算法设计中的挑战与机遇。

这一部分包括了以下章节:

  • 第十四章 明日的算法

第十九章:14

明日算法

本章探讨了在算法设计中塑造计算未来的先进和新兴主题。 随着技术以前所未有的速度发展,算法处于这一转型的最前沿,推动了量子计算、机器学习和大数据处理等领域的创新。 我们概述了多种影响可扩展、上下文感知和伦理意识算法发展的趋势。 这些趋势包括处理大规模数据的新方法、生物启发式技术的整合、内存高效且环保可持续的算法日益重要,以及在算法部署中对伦理考虑的迫切需求。 本章旨在提供这些前沿领域的概述,洞察它们如何重新定义计算机科学和 软件工程的格局。

本章将涵盖以下主题: 本章内容包括:

  • 过去汲取经验

  • 可扩展性

  • 上下文感知

  • 道德责任

  • 总结

从过去汲取经验

自计算机问世以来,出现了许多突破性的思想、概念和技术进展,极大地改变了我们与机器的互动方式以及信息处理的方式。 以下是计算技术演变的关键阶段的简要概述: 计算技术的发展历程:

  • 早期计算机:这一旅程始于 20 世纪中期早期计算机的开发,例如 ENIAC 和 UNIVAC。 这些机器是大型的、占据整个房间的设备,使用真空管和打孔卡片进行基本的计算。 它们标志着自动化计算的开始,为 未来的创新奠定了基础。

  • 第一波人工智能(AI):人工智能的概念与早期计算机同时出现,研究人员探索了机器能够模仿人类思维的可能性。 早期的 AI 集中于符号推理和问题解决,为这个快速发展的领域奠定了基础。 这一领域迅速演变。

  • 大型主机计算机:随着 技术的进步,大型主机计算机逐渐崭露头角。 这些强大的机器主要被大型组织用于复杂的数据处理任务。 主机计算机引入了集中式计算的概念,允许多个用户访问单一的强大 计算机系统。

  • 专家系统与第二波人工智能:第二波人工智能带来了 专家系统的发展,这些系统旨在模仿人类 专家在特定领域中的决策能力。 这些系统依赖于基于规则的逻辑,并被用于医学诊断和 金融分析等领域。

  • 个人计算机(PC):1970 年代和 1980 年代个人计算机的发明标志着计算机领域的重大转变。 个人计算机使计算变得更加普及,将其从大型机构的工具转变为家庭必备品。 这个时代见证了用户友好界面、软件应用程序的兴起,以及计算能力的民主化。

  • 互联网与网络:1990 年代互联网 和万维网的出现彻底改变了我们获取和分享信息的方式。 它将全球的计算机连接起来,实现了即时通讯、数据交换,以及在线服务和数字经济的兴起。 这一时期还见证了电子商务、社交媒体和 云计算的崛起。

  • 移动计算:2000 年代初期带来了移动计算时代,智能手机和 平板电脑变得无处不在。 这些设备将计算能力与便携性相结合,允许用户随时随地访问信息、进行沟通,并执行各种任务。 移动应用程序和无线技术进一步扩展了 这些设备的功能。

  • 第三波及当前的人工智能:我们现在正处于第三波人工智能的浪潮中,特点是机器学习、深度学习和自然语言处理的进展。 这一波的重点是构建能够从大量数据中学习、识别模式并做出自主决策的系统。 人工智能正在融入日常生活的各个方面,从虚拟助手和自动驾驶汽车到医学诊断和 金融服务。

这些阶段中的每一个都代表了技术能力的跃升,以及我们与计算机交互和利用计算机的方式的转变,塑造了现代的 数字化格局。

尽管每一次技术革命都带来了快速且深刻的变化,但有一点始终未变:算法的基本需求。 无论硬件和软件能力如何飞跃,算法始终是每一次进步的核心,推动着每项 新发展的效率和功能。

就像数学归纳法中一个原则在每个步骤中都成立一样,我们可以推断出,设计、分析和优化算法的必要性将在未来继续至关重要。 事实上,随着技术的日益复杂以及我们处理的数据量呈指数增长,强大且高效的算法的重要性只会更加 突出。

算法 是使系统能够处理信息、解决问题和做出决策的基础框架。 随着我们进一步进入如人工智能、量子计算和大数据等领域,挑战变得越来越复杂。 高效的算法将是充分发挥这些技术潜力的关键,确保它们能够在规模化、实时运行以及最优 资源利用方面表现出色。

此外,随着我们继续将技术融入社会的关键领域,如医疗、金融和基础设施,对不仅高效而且安全、公平、可解释的算法的需求变得更加迫切。 未来可能会要求算法能够适应动态环境、管理不确定性并自主运行,同时保持道德标准并 保护隐私。

总之,我们可以说,尽管我们使用的工具和平台可能会不断发展,但算法在塑造未来技术中的基础性作用将始终存在。 它们的设计、分析和优化将始终是进步的基石,随着我们面临更复杂和 严峻的挑战,它们的重要性只会日益增加。

众多新兴的算法趋势正受到当前人工智能革命和其他技术进步的影响。 本章的目的不是预测未来,而是通过三个关键方面来提供理解这些趋势的框架:可扩展性、上下文感知和 道德责任。

通过探讨这些维度,我们旨在启动一场关于算法设计不断演变的对话及其更广泛影响。 目标是鼓励读者积极参与这些话题,思考这些趋势如何可能影响技术 和社会的未来。

在接下来的部分,我们将更详细地探讨这些方面,讨论它们如何塑造当前的趋势以及算法的未来。 本讨论并非旨在穷尽所有内容,而是作为深入探索的起点。 我们邀请读者加入这一持续的对话,反思这些趋势如何影响他们的工作以及算法设计在更广泛社会层面的影响。

可扩展性

数据爆炸和现代系统日益复杂的需求,迫使算法 必须在前所未有的规模上有效运作。 无论是处理海量数据集、管理大规模分布式系统,还是为数百万用户优化流程,可扩展性已成为核心问题。 算法不仅要高效,还必须能够适应日益增长的需求和多样化的 应用场景。

当前的人工智能革命,不仅仅是由算法的进步推动的,还受到三大 重要发展推动:

  • 处理能力的指数级增长:处理能力的持续提升,已经达到即使是摩尔定律(即每两年微芯片上晶体管数量翻倍的预测)可能不再成立的程度,这使得更加复杂和资源密集型的算法得以执行(参见图 14**.1)。 专用硬件的出现,如 图形处理单元GPU)和张量处理单元TPU),进一步加速了处理大规模计算的能力,尤其是那些机器学习模型所需的计算。GPUs 最初是为了加速计算机图形中的图像和视频渲染而设计的。 它们已经发展成为处理深度学习、科学模拟和数据分析等任务所需的复杂数学计算。 由于其高度并行的结构,GPU 非常适合同时处理大量数据块,使其在训练和运行机器学习模型时尤其高效。

    TPU 是一种由 Google 专门为机器学习任务设计的硬件加速器,特别是那些涉及神经网络和深度学习模型的任务。 TPU 经过优化,能够高效地运行大规模计算,提供高性能以支持机器学习应用中的训练和推理。 它们专门设计用于处理 AI 算法的计算需求,尤其是使用 TensorFlow 这一 Google 开源机器学习框架的算法(参见图 14**.1)。

图 14.1:过去 20 年最快计算机的计算能力(对数尺度)

图 14.1:过去 20 年最快计算机的计算能力(对数尺度)

  • 存储成本下降和容量增加:虽然存储和内存成本大幅下降,但其容量大幅增长(见 图 14**.2)。 意味着现在可以廉价地存储大量数据,从而实现对庞大数据集的保留和处理。 这些趋势使得数据驱动的算法得以蓬勃发展,因为曾经限制模型大小或可用数据量的存储约束已经不复存在(见 图 14**.2)。

图 14.2:不同类型的 1 TB 存储器的历史价格(对数刻度)

图 14.2:不同类型的 1 TB 存储器的历史价格(对数刻度)

  • 互联网推动的数据爆炸:自 1991 年互联网诞生以来, 可用数据量呈现爆炸式增长。 网络的自由和 开放性释放了一股信息潮流,从用户生成的内容到交易数据,为算法提供了无与伦比的数据集。 社交网络、在线平台和物联网设备进一步推动了这一日益增长的数据海洋。 数据海洋。

这三个因素改变了算法,尤其是那些用于人工智能和机器学习的算法。 大量数据的可用性使得算法可以更加依赖统计学习和大规模的模式识别。 因此,像神经网络这样的算法已经演变成了极其庞大且复杂的模型,如具有数十亿参数的深度学习架构。 参数数量。

反过来,这些模型推动了对更大处理能力和存储的需求,强化了不断扩大的规模循环。 并行计算和并行性已成为处理训练这些巨大模型需求的关键策略。 分布式计算、基于云的机器学习和并行处理框架(如 MapReduce)等技术已经被开发出来,以应对这一 不断增长的需求。

因此,今天的人工智能革命不仅仅是关于扩展基础设施,也在于设计更智能的算法。 如果没有处理能力、存储和数据的丰盈发展,许多人工智能的突破是不可能实现的。 这种对规模的关注持续推动着能够从庞大数据集中学习、处理并采取行动的算法的发展。 庞大数据集。

设计和分析算法并行进行时,考虑规模问题以及算法如何利用和支持新兴硬件技术是至关重要的。 随着数据和计算需求的持续增长,可扩展的算法变得愈加重要,以确保在大规模系统中高效地进行处理和问题解决。 以下是一些支持可扩展算法发展的新兴趋势: 可扩展算法:

  • 量子算法:量子 计算有望彻底改变我们解决大规模 问题的方式。 传统计算机在某些类型的计算中表现不佳,如大数因式分解或量子系统模拟。 量子算法,如 Shor 算法 用于 因式分解,以及 Grover 算法 用于搜索,可以提供指数级的速度提升,使得 它们成为处理当前对经典计算机来说无法解决的大规模问题的理想选择。 经典计算机。

  • 近似算法:在许多 大规模问题中,找到 一个精确的解决方案在计算上是昂贵的,甚至是不可能的。 近似算法提供了一种方法,可以在合理的时间内获得接近最优的解。 这些算法在那些完美解并非必要,但一个足够好的解却很有价值并且必须迅速找到的场景中至关重要,如路由、调度和 优化问题。

  • 参数化复杂度:这是 计算复杂度理论中的一个框架,通过关注输入数据中的特定方面或参数,而不是整体输入大小,从而为分析问题的复杂度提供了更细致的途径。 仅仅依赖于总体输入大小。 在传统的复杂度分析中,问题是根据其运行时间随着输入大小的增长进行分类的。 然而,对于许多现实世界中的问题,输入的某些参数可能比数据的总大小对算法的性能有更大的影响。 通过识别和隔离这些关键参数,参数化复杂度使得即使问题在一般情况下仍然是计算上困难的,也能为实际使用场景设计出更高效的算法。 一般情况下。

    参数化复杂性中的主要目标是开发算法,其运行时间不一定对于问题的每个实例都是最优的,但在某些参数保持较小的情况下是可管理的,即使总体输入规模很大。 中心概念 固定参数可解性 FPT),其中问题被认为是固定参数可解的,如果它可以在时间内解决 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:miO</mml:mi><mml:mfenced separators="|">mml:mrowmml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:mik</mml:mi></mml:mrow></mml:mfenced>mml:mi*</mml:mi>mml:min</mml:mi>mml:msupmml:mrowmml:mic</mml:mi></mml:mrow>mml:mrowmml:mi</mml:mi></mml:mrow></mml:msup></mml:mrow></mml:mfenced></mml:math>,其中 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 是输入规模, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 是参数,并且 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:mik</mml:mi></mml:mrow></mml:mfenced></mml:math> 是一个仅依赖于参数而不是总体输入规模的函数。 如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mik</mml:mi></mml:math> 很小,即使 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:min</mml:mi></mml:math> 很大,算法仍然可以有效运行。 参数化复杂性在生物信息学、调度问题和 数据库系统中的实际应用。

  • 亚线性算法:随着 数据集的规模不断增大,甚至读取整个输入都变得不切实际时,亚线性算法变得至关重要。 这些算法通过仅检查输入的一小部分来产生有用的结果,特别适用于属性测试等应用,在这些应用中,我们希望在不完全处理数据的情况下确定数据集的属性。

  • 流数据算法:在数据以连续流的方式到达的场景中,例如网络流量监控或金融行情数据,算法必须在有限的内存下即时处理信息。 流数据算法旨在在这些约束条件下工作,通过对数据进行单次或有限次数的遍历,提取有意义的洞察或维持摘要,即使数据量不断增长。

  • 并行和分布式算法:为了充分利用现代硬件的强大能力,算法必须设计成能够高效地在多核处理器和 分布式系统上运行。 并行和分布式算法 将大型问题分解为可以并行解决的小子问题,从而减少计算时间,并使得大规模数据能够实时或接近实时地处理。

  • 平滑分析:传统的 算法分析通常侧重于最坏情况或平均情况,这可能无法准确反映实际的性能。 平滑分析通过分析算法在输入数据轻微随机扰动下的表现,提供了一个更为细致的视角。 这种方法为实际的大规模设置中的算法性能提供了更为真实的度量,弥合了理论分析 经验性能 之间的差距。

  • 大规模数据的算法:大数据应用需要能够高效处理和分析海量数据集的算法。 这些算法旨在处理 跨不同存储系统分布的高维数据,同时确保可扩展性和性能。 它们包括数据挖掘、聚类和机器学习方法,能够在海量数据上有效运作。

  • 图神经网络(GNNs):在许多现实世界的场景中,数据自然地以图的形式呈现,如社交网络、生物系统和通信网络。 GNNs 是 一类新兴的算法,能够直接在图结构数据上操作,使其能够随着数据中关系的复杂性扩展。 GNNs 在处理涉及大规模互联数据集的任务时尤其强大,使它们成为从网络分析到 分子化学等应用领域中非常有价值的工具。

这些趋势从不同角度应对扩展性挑战,从利用量子计算等新型计算范式到优化算法如何与大型数据集交互。 尤其是生物启发的算法和内存高效算法,通过利用自然过程并最大限度地利用有限资源,提供了创新的问题解决方法。 这些进展共同构成了在数据日益增长和计算需求不断增加的时代中,算法设计的关键基础。 计算需求。

虽然计算能力至关重要,但仅有计算能力不足以在波动、混乱和动态的环境中做出有效决策。 除了单纯的处理能力外,我们还必须投资于能够适应多变甚至敌对环境的算法。 这要求使算法具备更强的上下文意识,使其能够智能地应对周围环境的复杂性。 它们能够响应复杂的外部环境。

上下文意识

现代算法越来越多地需要在多样、动态且常常不可预测的环境中运行。 这些环境从实时数据流到嵌入式系统,要求快速处理和决策,而嵌入式系统的计算资源则受到严格限制。 此外,算法还必须在复杂的生态系统中运行,这些生态系统包括云基础设施、边缘计算环境,以及 物联网(IoT) 设备的整合。 在这些多变的环境中,算法必须具备韧性、适应性,并能够在不同的条件和约束下执行。 以下是支持上下文意识算法发展的新兴趋势:

  • 生物启发的算法:自然为开发能够适应变化环境的算法提供了丰富的灵感来源。 生物启发的算法,如遗传算法、蚁群优化和 神经网络启发的计算,利用自然过程中的机制。 例如,遗传算法模拟自然选择过程,通过连续几代的演化优化复杂问题。 蚁群优化受到蚂蚁寻找资源最优路径行为的启发,提供了有效的路由和调度解决方案。 这些算法在传统方法可能力不从心的情况下表现出色,尤其是在需要适应性和鲁棒性的复杂、大规模环境中。 并且具有很强的适应性。

  • 内存高效的算法:在内存资源有限的环境中,如 嵌入式系统和物联网设备,最小化内存使用的能力至关重要。 内存高效的算法旨在这些约束条件下有效运行,使用数据压缩、就地计算和空间高效的数据结构等技术。 通过减少内存占用,这些算法使得在存储和处理能力有限的设备上能够进行复杂的计算,扩展了可从先进算法解决方案中受益的应用范围。 这在需要设备本地处理数据,且由于延迟、带宽或 隐私问题无法依赖云资源的情境中尤其重要。

  • 在线算法:这些 算法 旨在通过不完整的信息实时做出决策。 在许多现实应用中,数据是连续到达的,决策必须在没有提前知道完整输入的情况下即时做出。 在线算法在这种动态环境中表现出色,使其适用于股票交易、网络 路由以及 负载均衡等需要即时响应的应用场景。 这些算法可以在这些领域中发挥重要作用。

  • 自适应与动态算法:为了在变化的环境中有效运行,一些 算法被设计为自适应,能够根据新的 输入数据或变化的条件进行调整。 自适应算法可以根据观察到的数据修改其行为,随着时间的推移不断学习和改进。 动态算法则能够处理问题结构本身的变化,如图形更新或调度问题的变动。 这些能力对于自动化系统等应用至关重要,这些系统必须持续适应其环境以 有效运作。

  • 机器学习算法:机器学习算法天生具有一定的上下文感知能力 因为它们能够从数据中学习。 这些算法能够适应各种环境,无论是处理静态数据集还是 处理实时数据流。 强化学习等技术使算法能够通过与环境的互动学习最优行为,使它们适合在动态环境中进行复杂决策任务。 此外,机器学习模型可以部署在边缘设备等环境中,在这些设备上本地处理和分析数据,从而减少对持续 云连接的需求。

这些新兴趋势表明,算法不仅需要高效,还需要具有上下文感知能力。在当今快速发展的技术环境中,算法必须能够适应新数据、在资源限制下运行并做出实时决策。 无论是从自然过程中汲取灵感、为有限的内存优化,还是从数据中学习,上下文感知算法对于解决现代 计算环境中的复杂性至关重要。

尽管超高速计算机、智能算法和先进设备看起来像是通向乌托邦未来的路径,但仅凭它们无法实现这一结果。 过去一个世纪的痛苦教训——无休止的战争、环境破坏以及个人隐私的侵蚀——已向我们展示,未经控制的技术进步推动可能带来更多危害而非好处。 这就是为什么在处理技术时,保持道德警觉至关重要,确保创新受到伦理考量的引导,并服务于 更大的利益。

道德责任

算法已深深 嵌入到我们生活的几乎每个方面,影响着从医疗和金融到我们消费的媒体和购买的产品等决策。 虽然它们极大地提升了许多人的生活质量,提供了效率、便利和新功能,但算法的兴起也带来了重大伦理挑战 和责任。

其中一个紧迫的关注点是算法误用的风险以及其可能产生的无意负面副作用。 大公司设计算法以最大化用户参与度和利润,通常通过引导用户浏览广告或内容来增加股东价值。 尽管这些做法合法,属于商业模型的范围内,但它们引发了伦理问题。 例如,优先推送吸引眼球内容的算法可能无意中推动了耸人听闻、虚假信息或极化内容,这对社会产生有害影响。 一个现实世界的例子是社交媒体算法在政治选举期间放大分裂性内容的作用。 例如,2016 年,Facebook 的算法因推动高度情绪化和极化的政治内容而受到批评,这在美国 总统选举期间加剧了虚假信息的传播,并加深了社会分裂。

除了这些合法但在伦理上有疑问的做法,还有一些明显不道德和非法的算法使用。 这些包括传播虚假信息、针对易受害群体发布有害或剥削性内容,以及加剧偏见,比如种族或性别歧视。 如果算法设计和监控不当,可能会加剧刻板印象和系统性不平等。 例如,偏见的训练数据可能会导致在招聘、贷款批准和 执法等领域产生不公平的结果。

另一个关键问题是算法的 环境影响。 训练大型语言模型所需的计算能力支持大规模数据中心并开采加密货币,这需要巨大的能量。 这些活动消耗大量电力,导致显著的碳足迹。 算法的环境成本不仅仅包括能源消耗,还包括技术的整个生命周期,其中包括电子设备的生产和处理,涉及原材料的开采和 电子垃圾的产生。

此外,算法驱动系统的社会影响深远。 全球向自动化和 AI 的转变导致了就业模式的变化,某些工作岗位消失,而其他岗位则需要越来越专业化的技能。 这一转型可能导致经济上的 displacement 和不平等,特别是在缺乏支持再培训和适应新工作市场的基础设施的地区。 此外,人才流失现象——即来自发展中国家的人才迁移到发达国家从事先进算法的工作——可能加剧全球不平等,使某些地区缺乏推动本地创新所需的专业知识。

考虑到这些复杂性,算法设计和部署所涉及的道德责任是巨大的。 这需要一种超越技术考量的深思熟虑的方法,纳入伦理、社会和环境因素。 开发者、企业和政策制定者必须共同努力,制定促进算法负责任使用的指南和框架。 这些指南和框架可以围绕以下考虑因素建立:

  • 伦理设计与实施:确保算法的设计考虑公平性、透明性和问责制。 这包括使用多样化的数据集,实施偏见检测和缓解策略,并提供清晰的解释,说明算法是如何做出决策的。

  • 监管与监督:政府和监管机构需要建立并执行标准,以防止算法的滥用,保护用户隐私,并确保 AI 系统在伦理边界内运作。

  • 环境考虑:开发和采用节能的算法和实践,最大限度地减少对环境的影响。 这包括优化数据中心,投资可再生能源,并考虑技术基础设施的全生命周期。

  • 公众意识与教育:提高公众对算法如何影响日常生活的理解,并赋予个人做出明智选择的能力。 这还包括教育未来的开发者了解他们工作中的伦理影响。

  • 算法风险管理:算法可能引入各种类型的风险,其中最突出的是算法偏差,即基于性别、种族或社会阶层等因素而使某些用户群体受到偏好或劣势化。 这些偏差通常源于用于训练机器学习模型的数据存在偏见,导致不公平的结果。 例如,在招聘、贷款批准或刑事司法系统中使用的算法在未经适当审计或监控时已被证明会持续甚至放大社会偏见。 责任在于确保算法基于多样化、代表性数据集进行训练,并定期测试其公平性 和准确性。

    另一类 风险涉及代码的误用或滥用,特别是在嵌入式系统中。 嵌入式系统被集成到无数设备中,从家用电器到医疗设备和工业控制系统,使它们容易受到恶意利用。 人们常说,如果任何设备中的任何代码遭到破坏,这可能成为一种责任。 这在安全性和安全性至关重要的系统中尤其令人担忧,如自动驾驶车辆、医疗设备和国防技术。 如果这些系统中的嵌入式代码被破坏,后果可能是灾难性的,从人员伤亡到大规模的财务和 基础设施损失。

尽管前述考虑涉及各种利益相关者,作为工程师和计算机科学家,我们有责任捍卫道德原则,特别是与人权和公共利益相关的原则。 除了依赖监管机构,我们必须培养自己的伦理指南,由深刻的道德意识引导。 这需要在我们的工作中积极维护伦理标准,确保我们的贡献以公平和 负责任的方式造福社会。

软件实践中的道德意识

在道德、环境和社会领域已被认可的责任之外,软件从业者必须培养更高层次的伦理意识,或者我们所说的 道德意识。这不仅仅是遵守伦理指南或法律要求。 它涉及对其创新的更广泛影响和潜在间接用途的深入主动考虑,即使这些创新是出于最好的意图和 伦理应用。

具有道德意识的从业者意识到,算法一旦开发出来,可能会独立于原始创造者的意图或预期,被以不同的方式使用。 例如,一个旨在提升社交媒体平台用户参与度的算法,可能无意中促成了错误信息的传播或成瘾行为。 类似地,一个为安全目的开发的面部识别算法,可能会被重新用于监视,从而侵犯隐私或 公民自由。

为了降低这些风险,从业者可以采取 几种策略:

  • 知识产权和许可:控制算法使用的一种方式是通过知识产权保护它。 通过申请专利或采用特定的许可协议,开发者可以对其作品的使用方式施加限制,确保其符合伦理标准。 然而,这种方法并非万无一失,因为执行知识产权可能具有挑战性,特别是在全球范围内,不同国家有不同的 法律框架。

  • 伦理保障和影响评估:从业者应在开发过程中进行全面的 伦理影响评估。 这不仅包括分析算法的直接应用,还要考虑其潜在的次要效应和意外后果。 通过考虑算法可能被误用的各种方式,开发者可以实施保障措施,如使用限制或内置监控系统,以防止 不道德的应用。

  • 双重用途考虑:在发布算法之前,必须考虑其双重用途潜力——它如何可能合法且有益地使用,同时也要考虑它如何可能被用于非法或有害的目的。 如果一个算法被误用的风险较高,从业者需要仔细考虑是否应将其公开发布,或者是否需要额外的保障措施来 限制访问。

  • 透明度与合作:与利益相关者(包括伦理学家、法律专家和更广泛的社区)进行交流,可以为算法潜在的影响提供宝贵的见解。 在开发实践中保持透明,并与伦理相关的考虑进行公开对话,有助于做出更明智的决策,并在问题变得严重之前识别潜在风险。

  • 持续的伦理教育:了解不断变化的伦理环境,并对重新审视自己工作的道德影响保持开放态度,对从业人员至关重要。 这包括对新兴伦理问题的持续教育,以及反思社会规范和价值观如何随着时间的推移而变化。

道德意识 是指在算法的技术创建和部署之外,保持警觉和责任。 它要求我们不仅要时刻质疑算法的直接收益,还要考虑它的更广泛社会影响、间接效应和滥用的潜力。 通过采取这种有责任心的方式,软件从业者可以为优先考虑伦理问题并致力于追求 更大利益的技术环境做出贡献。

总之,虽然算法具有带来重大积极变化的潜力,但它们也承担着不可忽视的道德责任。 解决与算法使用相关的伦理、社会和环境挑战,对于确保它们对社会的影响既积极又可持续至关重要。

最后的话

在我们结束本书时,我们的目标是为计算机科学学生和软件工程师提供一份全面的概述,涵盖他们在学术旅程和实际应用中会遇到的算法设计与分析的最重要话题。 我们努力在理论基础和实践问题解决能力之间取得平衡,这两者对构建高效且 有效的算法至关重要。

在本书中,我们力求在呈现复杂概念时做到准确和清晰。 然而,鉴于该领域的广度和深度,错误和遗漏是不可避免的。 我们鼓励读者在遇到任何问题、错误或模糊之处时提出反馈,以便在未来的版本中得到解决和改进。 您的反馈对完善内容并确保其作为学习者和 从业者的可靠资源具有不可估量的价值。

我们故意在本章中没有讨论的话题是生成性 AI 在软件工程和算法设计中的角色。 这个省略的原因很简单:生成性 AI 对算法领域的影响和发展方向仍在展开。 我们正处于一场技术演变之中,很难准确预测生成性 AI 将如何重塑 算法设计的格局。

然而,有一点是肯定的: 大型语言模型 (LLMs)和生成式人工智能正在迅速成为推动 对更高效算法需求的核心力量。 它们的复杂性要求进行精密的分析和评估。 在所有新兴的应用场景中,LLMs 和生成式 AI 尤为突出,成为了可扩展性、上下文感知和道德 责任交汇的领域。

对可扩展 AI 算法的压力已经显现。 这些模型的规模以及与之互动的用户数量正以惊人的速度增长。 例如,OpenAI 的 ChatGPT 在技术普及方面打破了纪录,在短时间内就达到了每周 2 亿用户。 这种前所未有的规模要求有高度高效、可扩展的算法来支撑如此 庞大的基础设施。

此外,对话式 AI 和 LLMs 正迅速从突破性创新转变为日常产品中的标准功能。 这种转变意味着 AI 系统必须在高度多样化的情境下有效运行,往往是在不确定性和波动性中操作,同时涉及到人类互动。 设计能够适应这些挑战性环境的算法对其 长期成功至关重要。

或许关于生成式 AI 和 LLMs 最紧迫的问题是它们对社会和环境的影响。 训练像 GPT-4 这样的巨大模型的碳足迹已经相当可观,除非我们开发更高效的算法并采纳更严格的行业规范,否则这一影响只会加剧。 此外,道德风险同样令人担忧。 这些系统可能在大范围内传播错误信息,扩大社会经济差距,并有可能通过操纵 公众舆论来破坏民主进程。

展望 AI 和 LLMs 在算法设计中的角色,我坚信,尽管该领域无疑将被生成式 AI 所改变,但解决问题的核心原则将始终不变。 我们可能会获得新的工具和方法,但在算法设计和分析中对人类洞察力的需求将持续存在。 生成式 AI 可以自动化开发过程的某些方面,加速周期并扩展算法能力。 然而,人类在过程中所带来的创造性和批判性思维将始终不可或缺,特别是在那些需要伦理判断、情境敏感性以及 深思熟虑决策的领域。

最后,我希望这本书能成为那些进入算法世界的人们的宝贵指南和伴侣。 在这个领域学习和创新的旅程仍在继续,我鼓励读者继续探索、质疑和为不断发展的计算机科学和 软件工程领域做出贡献。

参考文献和进一步阅读

  • 摩尔定律是什么? 作者 Max Roser, Hannah Ritchie, 和 Edouard Mathieu (2023). 在线出版 OurWorldinData.org.

  • 量子计算与量子信息. 作者 M. A. Nielsen 和 I. L. Chuang. 剑桥大学 出版社. 2010.

  • 近似算法. 作者 V. V. Vazirani. Springer. 2001.

  • 参数化算法. 作者 M. Cygan, F. V. Fomin, Ł. Kowalik, D. Lokshtanov, D. Marx, M. Pilipczuk 和 M., A. Saurabh. Springer. 2016.

  • 亚线性时间算法. 在数据结构与应用手册中. 作者 A. Czumaj 和 C. Sohler. Chapman & Hall/CRC. 2004.

  • 数据流: 算法与应用. 作者 S. Muthukrishnan. 现在的基础和 趋势. 2005.

  • 并行计算简介. 作者 A. Grama, A. Gupta, G. Karypis, 和 V. Kumar. Pearson Education. 2003.

  • 分布式系统: 原理与范例. 作者 A. A. S. Tanenbaum 和 M. V. Steen. Pearson Education. 2007.

  • 算法的平滑分析: 为什么单纯形算法通常需要多项式时间. 作者 D. A. Spielman 和 S.-H. Teng. ACM 期刊, 51(3), 385-463. 2004.

  • 大数据集挖掘. 作者:J. Leskovec, A. Rajaraman 和 J. D. Ullman。 剑桥大学 出版社。 2014 年。

  • 图神经网络:方法与应用综述。 作者: J. Zhou, G. Cui, S. Hu, Z. Zhang, C, Yang, Z. Liu 和 M. Sun。 AI Open, 1, 57-81。 2020 年。

  • 进化计算导论. 作者:A. E. Eiben 和 J. E. Smith。 Springer 出版社。 2003 年。

  • 蚁群算法优化. 作者:M. Dorigo 和 T. Stützle. MIT 出版社。 2004 年。

  • 在线计算与竞争分析. 作者:A. Borodin 和 R. El-Yaniv。 剑桥大学 出版社。 2005 年。

  • 算法伦理:辩论的框架. 作者:B. D. Mittelstadt, P. Allo, M. Taddeo, S. Wachter 和 L. Floridi。 Big Data & Society, 3(2)。 2016 年。

  • 数据中心电力使用增长 2005 至 2010. 作者:J. G. Koomey. Analytics 出版社。 2011 年。

posted @ 2025-07-19 15:47  绝不原创的飞龙  阅读(34)  评论(0)    收藏  举报