Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

序言

计算机是20世纪最伟大的科学技术发明之一。从第一台电子计算机ENIAC于1946年在美国宾夕法尼亚大学诞生至今,计算机技术经历了从电子管到晶体管、从集成电路到大规模集成电路、从单机到互联网、从桌面到云端的跨越式发展。如今,计算机的运行速度越来越快,功能越来越强大,形态也越来越丰富——从超级计算机到智能手机,从物联网设备到可穿戴装置,计算机已经渗透到人类社会的每一个角落。

然而,伴随技术进步的是日益增长的复杂性。计算机各个领域的书籍汗牛充栋,新技术层出不穷,更新换代的速度令人目不暇接。面对浩如烟海的知识,“该如何高效地学习计算机知识?“成为摆在每一位学习者面前必须优先思考的重要问题。对于这个问题,每个人都会有自己的回答。而本书给出的回答是:学习计算机底层的原理,以不变应万变。本书将分上、中、下三卷,共七十二篇(美其名曰“七十二变”),以简要、有趣的文字,由浅入深地讲解计算机的核心原理,并辅以Rust语言的实战代码,帮助读者在理解原理的同时掌握实践技能。

内容概要

全书分为三卷,每卷聚焦计算机科学的不同维度,层层递进,环环相扣:

上卷 数据与编码

上卷主要介绍常见的几种重要数据是如何通过编码构建计算机的数字化世界的。从最基本的数字、字符、字节出发,逐步扩展到时间、图片、条码、二维码、音频、视频等复杂数据类型,最终探讨序列化、网络协议和压缩算法等高级主题。上卷共十三章:

  • 第一 数字:探索整数、浮点数在计算机中的表示方式,理解精度、溢出等核心概念。
  • 第二 字符与编码:从ASCII到Unicode,从UTF-8到UTF-16,梳理字符编码的发展脉络。
  • 第三 字节:字节是计算机存储的基本单位,深入理解字节序、字节操作及其应用。
  • 第四 时间:时间的表示与存储,时间戳、时区、闰秒等问题的处理。
  • 第五 图片:位图与矢量图,常见图像格式的编码原理与Rust中的图像处理。
  • 第六 条码:一维条码的编码规则与识别原理。
  • 第七 二维码:QR Code的编码机制、纠错能力与实际应用。
  • 第八 音频:声音的数字化过程,采样、量化、编码与常见音频格式。
  • 第九 视频:视频编码原理,帧、关键帧、压缩与主流视频标准。
  • 第十 万物皆可编码:将编码思想推广到更广泛的领域,理解“一切皆可表示为数据“的哲学。
  • 第十一 序列化与反序列化:数据持久化与跨系统传输的核心技术。
  • 第十二 网络协议:HTTP、TCP/IP等协议的数据封装与解析。
  • 第十三 压缩算法:无损与有损压缩,经典算法的原理与Rust实现。

中卷 运算与算法

中卷主要介绍各种数据是如何通过运算与转换,解决现实世界的各种问题的。从基础的算术、关系、逻辑、位运算出发,逐步深入到排序、搜索、哈希等经典算法,再扩展到人工智能、代数、几何、微积分、概率论、神经网络、自然语言处理、计算机视觉、强化学习和生成式AI等前沿领域。中卷共二十七章:

  • 第十四 算术运算:加减乘除背后的计算机实现,溢出处理与高精度计算。
  • 第十五 关系运算:比较操作的底层原理与优化。
  • 第十六 逻辑运算:与、或、非、异或,布尔代数与数字电路的桥梁。
  • 第十七 位运算:移位、掩码、标志位,高效编程的利器。
  • 第十八 交换/反转/旋转:数据重排的基本操作及其应用。
  • 第十九 类型转换:安全与危险的类型转换,Rust的类型系统优势。
  • 第二十 哈希:哈希函数、哈希表、一致性哈希与分布式系统。
  • 第二十一 打包/拆包 压缩/解压:数据结构的紧凑表示与空间优化。
  • 第二十二 查询/检索/搜索:线性搜索、二分搜索、字符串匹配与信息检索。
  • 第二十三 排序算法:从冒泡到快排,从比较排序到非比较排序的全景梳理。
  • 第二十四 Lambda演算:函数式编程的数学基础,从丘奇到现代编程语言。
  • 第二十五 人工智能:AI发展简史,从符号主义到连接主义。
  • 第二十六 算法:算法复杂度分析,经典问题与解题范式。
  • 第二十七 代数:群、环、域,抽象代数在计算机中的应用。
  • 第二十八 几何:计算几何基础,图形学中的数学工具。
  • 第二十九 行列式:矩阵的行列式及其在几何与代数中的意义。
  • 第三十 矩阵:矩阵运算、线性变换与Rust中的矩阵库。
  • 第三十一 向量:向量空间、点积、叉积与机器学习中的向量表示。
  • 第三十二 函数:函数的数学定义、高阶函数与闭包。
  • 第三十三 微积分:极限、导数、积分,连续世界的离散化计算。
  • 第三十四 概率论:随机变量、分布、期望,不确定性世界的建模工具。
  • 第三十五 离散数学:图论、组合数学、数理逻辑,计算机科学的数学语言。
  • 第三十六 神经网络:从感知机到深度学习,神经网络的原理与Rust实现。
  • 第三十七 自然语言处理:文本处理、词向量、Transformer与语言模型。
  • 第三十八 计算机视觉:图像识别、目标检测、卷积神经网络。
  • 第三十九 强化学习:智能体与环境的交互,奖励机制与策略优化。
  • 第四十 生成式AI:GAN、VAE、扩散模型,创造新内容的AI技术。

下卷 秘密与密码

下卷主要介绍如何通过密码技术实现数据的安全存储、传输等一系列问题。从最基本的口令和随机数出发,逐步深入到对称密码、非对称密码、哈希函数等核心密码学 primitives,再扩展到身份认证、授权访问、PKI、TLS、区块链、零知识证明、同态加密、量子密码等高级主题,最后以“无敌密码“和“终极密码“作为全书的哲学升华。下卷共三十二章:

  • 第四十一 口令:密码的存储、强度评估与破解防护。
  • 第四十二 随机数:真随机与伪随机,密码学安全随机数生成器。
  • 第四十三 密钥管理:密钥的生成、分发、存储与生命周期管理。
  • 第四十四 对称密码:AES、ChaCha20等对称加密算法的原理与Rust实现。
  • 第四十五 非对称密码:RSA、ECC等公钥密码体系的数学基础与应用。
  • 第四十六 不可逆密码:哈希函数、HMAC、数字签名与完整性验证。
  • 第四十七 身份认证:我是谁?如何证明?认证协议与多因素认证。
  • 第四十八 授权访问:权限模型、访问控制列表与OAuth等授权框架。
  • 第四十九 PKI:公钥基础设施,证书、CA与信任链。
  • 第五十 TLS:传输层安全协议,HTTPS背后的密码学握手。
  • 第五十一 区块链:分布式账本、共识机制与加密货币的密码学基础。
  • 第五十二 零知识证明:证明者如何在不泄露秘密的情况下证明知晓某事。
  • 第五十三 同态加密:在加密数据上直接计算,隐私计算的圣杯。
  • 第五十四 量子密码:量子密钥分发与后量子密码学。
  • 第五十五至第七十(预留):为密码学前沿发展预留空间。
  • 第七十一 无敌密码:探讨是否存在绝对安全的密码系统。
  • 第七十二 终极密码:从密码学上升到信息安全哲学,思考安全的本质。

附录

  • 附录一 问题:常见问题解答与疑难解析。
  • 附录二 程序员:关于程序员职业发展的思考与建议。
  • 附录三 符号表:全书数学符号与术语速查。
  • 附录四 百宝箱:常用Rust库、工具与资源汇总。
  • 附录五 实战:综合实战项目与案例分析。
  • 附录六 游戏:以游戏化的方式巩固所学知识。

全书章节总览

章节范围主题章节数
上卷第一至第十三数据与编码13
中卷第十四至第四十运算与算法27
下卷第四十一至第七十二秘密与密码32
附录附录一至附录六补充资料6

全书共七十八章(含附录),覆盖数据表示、计算理论、密码安全三大核心领域,形成完整的计算机底层知识体系。

标准、规范、协议是计算机、互联网的基石

如果将0、1比作沙子,那么该如何使用这些沙子高效地构建整个计算机、互联网世界的大厦呢?很显然,沙子太小而大厦则相对大得多,直接使用沙子来建设大厦简直是天方夜谭。唯一可行的做法就是:先使用沙子制作砖块、混凝土、玻璃等最基本的材料(实际上需要的材料还有很多,比如钢铁、木材),然后根据施工标准,采用恰当的构建方法,按照一定的步骤一步步构建起最终的大厦。

计算机、互联网世界也是如此。就软件而言,0、1二进制首先构成了各种编程语言的类型系统中的各种类型数据:整数、浮点数、字符串、数组、队列、堆、栈、文件等等。这些类型的各种方法实现了类型的各种计算、转换等逻辑,而我们常说的软件、代码则是使用编程语言(当然编程语言本身也是代码)表达的一定业务逻辑的符号系统。可以看出,在构建整个大厦的过程中,沙子、砖块、混凝土等等都是最基础的材料,然后更为重要的是加工、制造这些材料的方法,以及经过大量实践证明、被大多数人遵循和采用的经验总结,进而上升为各种标准、规范、协议。

从TCP/IP协议栈到HTTP协议,从JPEG图像标准到MP3音频编码,从Unicode字符集到TLS安全协议——这些标准、规范、协议构成了现代数字世界的“交通规则“,让来自不同厂商、不同国家、不同年代的设备和软件能够无缝协作。理解这些标准,就是理解计算机世界的运行法则。

制定标准、规范、协议是实力与话语权的象征

“无规矩不成方圆”,社会生活中,为了规范每个人、各种社会组织、团体的行为,保持社会稳定,我们制定了各种规章制度、法律法规、民俗公约。计算机、互联网世界则是各种标准、规范、协议。从ASCII (American Standard Code for Information Interchange,美国信息交换标准代码)开始,在不到一百年的时间里,计算机世界里出台了成百上千的标准、规范、协议,而且还处在不断新增、完善的过程中。这些极大地促进了计算机的应用和发展。

美国最早发明了计算机、互联网,同时也是众多标准、规范、协议的制定者,体现了其强大的科技创新实力与话语权。“海到无边天作岸,山登绝顶我为峰”,每个希望变得强大且愿意为全人类做贡献的国家、组织,都应该在各个行业、各个领域努力做到最好,推动制定相关领域的标准、规范、协议,共同推动全人类的发展与进步。

在密码学领域,这一趋势尤为明显。从DES到AES,从SHA-1到SHA-3,国际密码学标准的演进反映了全球科研实力的此消彼长。今天,随着中国、欧洲等国家和地区在量子计算、后量子密码等前沿领域的投入,密码学标准制定的格局正在发生深刻变化。掌握标准制定权,就是掌握技术发展的方向。

为什么学习、使用Rust?

对公司而言,使用Rust更省钱、更省心、更省事

性能卓越,更省钱

云计算时代,CPU、内存、网络(流量、带宽)就如城市中的水和电一样,都可以轻易获得,但大规模使用的成本不容忽视。看看各大云服务商的数据中心,仅仅耗电一项就是巨大开支。Rust凭借优异的性能,能够大幅减少CPU运行的时间,同时消耗较少的内存,这就意味着消耗更少的电力——对于老板而言,这意味着做同样的事情花费更少的成本。当前全球经济形势不容乐观,开源节流、降本增效成为大多数公司的一致选择。另一方面,消费降级成为主流,质优价廉的商品更受市场青睐。如何在降低成本的同时提供优质产品和服务,成为企业必须认真思考的问题。

Energy Efficiency across Programming Languages: How Do Energy, Time, and Memory Relate?这篇论文详细比较了各种编程语言的能耗、运行时间、内存消耗。

《Sustainability with Rust》

【Rust生态观察】Rust实现的事件处理引擎tremor-runtime已经在美国最大家具电商公司Wayfair生产环境跑了三年

内存安全,更省心

在数据安全以及隐私越来越受重视的今天,安全漏洞造成的损失和影响都将是巨大的。Java开源库接连爆出重大漏洞,影响极大。Rust通过所有权(Ownership)、借用(Borrowing)和生命周期(Lifetime)机制,在编译期就消除了空指针、数据竞争、缓冲区溢出等内存安全问题,让“Heartbleed“级别的漏洞在Rust代码中几乎不可能发生。

系统健壮,更省事

系统稳定运行意味着后期维护时,再也不用担心服务随时可能宕机了。Rust的健壮性为系统稳定运行奠定了坚实的基础。Rust的“零成本抽象“理念让开发者既能享受高级语言的表达能力,又能获得接近C/C++的运行效率,同时避免了C/C++中常见的未定义行为。

对个人而言,学习Rust是一个十分明智的选择

Rust渐成主流

Java内卷太严重,我要学习Rust。在国内,Java绝对是大部分企业的主力开发语言。Java程序员需求量大,相关的职位也最多。随着高薪的吸引,大量科班、非科班的就业人群涌入IT、互联网行业,Java行业的内卷(竞争)程度也越来越激烈。Golang、Rust等新兴编程语言经过多年的快速发展,在国内也逐渐成为主流。

Rust已经连续多年被Stack Overflow开发者调查评为“最受喜爱的编程语言“。从Linux内核到Windows驱动,从Firefox到Cloudflare,从Discord到Dropbox,Rust正在渗透到底层系统的每一个角落。掌握Rust,就是掌握未来十年系统编程的通行证。

重剑无锋,大巧不工

Rust就好比一柄“锈迹斑斑“的玄铁重剑。Rust拥有令人望而却步的学习曲线,初学者刚开始学习使用时非常吃力。所有权系统、生命周期标注、 trait 系统——这些概念对于习惯了垃圾回收语言的开发者来说确实需要一段时间适应。值得庆幸的是,在人工智能的辅助下,将大大加快学习的速度。开发者一旦掌握了就可以轻松应对各种复杂的编程任务,就能充分利用Rust在效率、稳定性、安全性方面的优势,可以保障系统的稳定运行和数据的安全性。

AI助力,迎刃而解

AI工具的普遍使用,使得学习Rust更加高效、便捷。现代AI编程助手可以帮助开发者快速理解复杂的编译错误信息、生成符合Rust idioms 的代码、解释所有权和生命周期的微妙之处。在AI的辅助下,Rust的学习曲线变得平缓了许多,而掌握Rust后获得的系统级编程能力将成为开发者职业生涯中的核心竞争力。

本书特色

本书力求在以下三个方面做到有机结合:

原理为先:每一章都从底层原理出发,讲解“为什么“而非仅仅是“怎么做“。理解原理才能举一反三,面对新技术时才能快速上手。

代码为体:所有核心概念都配以Rust代码实现。代码不仅是理论的验证,更是实践的起点。读者可以直接运行、修改、扩展这些代码,在动手实践中深化理解。

实战为要:每卷末尾和附录中设有实战项目,将分散的知识点串联成完整的解决方案。从编码一个二维码生成器到实现一个简单的加密通信系统,实战项目让知识真正转化为能力。

阅读建议

适合人群

  • 有一定编程基础的开发者:了解至少一门编程语言,希望深入理解计算机底层原理。
  • 系统编程爱好者:对操作系统、网络、密码学等领域感兴趣,希望用Rust进行系统级开发。
  • 计算机专业学生:作为课堂学习的补充,通过实战代码加深对理论知识的理解。
  • 技术管理者:了解技术趋势,评估Rust在团队中的适用场景。

阅读顺序

顺序阅读:对于希望系统学习的读者,建议从上卷开始顺序阅读。三卷之间存在逻辑递进关系——先理解数据如何表示,再学习如何运算处理,最后掌握如何安全保护。

按需查阅:对于有经验的开发者,可以根据兴趣直接跳读到特定章节。每章尽量保持相对独立,交叉引用会标明相关章节的编号。

结合实践:强烈建议读者在阅读时打开Rust开发环境,亲手运行和修改书中的代码示例。本书的代码示例均使用最新稳定版Rust编写,可在Rust Playground或本地环境中直接运行。

预备知识

阅读本书前,建议读者具备以下基础:

  • 基本的编程概念(变量、函数、条件、循环)
  • 对二进制、十六进制有初步了解
  • 高中水平的数学知识(代数、几何基础)

不具备Rust基础的读者也不用担心——本书会在必要时解释Rust特有的语法和概念,但建议同时参考Rust官方文档进行系统学习。

致谢

本书的完成离不开开源社区的支持。感谢Rust核心团队和所有贡献者打造了这样一门卓越的编程语言;感谢crates.io上无数开源库的维护者,他们的工作让Rust生态日益繁荣;感谢Stack Overflow、Rust用户论坛等社区中的热心回答者,他们的经验分享解决了本书写作过程中的诸多技术难题。

感谢所有为计算机科学奠定基础的前辈们——从图灵、香农到里斯特、汤普森,从迪杰斯特拉到科克,正是他们的智慧结晶构成了我们今天所站立的巨人肩膀。

参考资源

愿每一位读者都能通过本书,在Rust的世界里习得七十二般变化,在计算机科学的道路上越走越远。

上卷 数据与编码

数据

数据(data)是事实或观察的结果,是对客观事物的逻辑归纳,是用于表示客观事物的未经加工的原始素材。

信息

信息,指音讯、消息、通讯系统传输和处理的对象,泛指人类社会传播的一切内容。人通过获得、识别自然界和社会的不同信息来区别不同事物,得以认识和改造世界。在一切通讯和控制系统中,信息是一种普遍联系的形式。1948年,数学家香农在题为“通讯的数学理论“的论文中指出:“信息是用来消除随机不定性的东西”。创建一切宇宙万物的最基本单位是信息。

信息(百度百科)

数据经过编码等手段加工后得到信息。


上卷概述:为什么从数据与编码开始

在计算机科学的宏大叙事中,数据与编码是最基础、最不可或缺的篇章。正如学习一门自然语言需要从字母和词汇开始,理解计算机世界也需要从数据及其表示方式起步。上卷选择“数据与编码“作为全书的开篇,基于以下考量:

第一,数据是计算的原材料。无论是简单的算术运算还是复杂的人工智能模型,无论是本地文件存储还是分布式网络通信,一切计算活动的起点都是数据。不理解数据的本质,就无法理解计算的本质。

第二,编码是数字世界的通用语言。计算机只能处理0和1,而人类世界充满了丰富多样的信息——文字、图像、声音、视频。编码就是连接这两个世界的桥梁,它将人类可理解的信息转换为计算机可处理的数据,反之亦然。

第三,编码思想贯穿全书。上卷介绍的编码原理不仅适用于数据表示,更是中卷算法设计和下卷密码技术的理论基础。哈希编码、纠错编码、压缩编码——这些思想在后续章节中将反复出现。

数据在计算机中的表示:二进制是一切的基础

1948年,香农发表了《通信的数学理论》,奠定了信息论的基础。在这篇开创性的论文中,香农指出:任何信息都可以被表示为二进制数字序列。这一论断看似简单,却深刻揭示了数字世界的本质。

计算机内部使用二进制(base-2)表示所有数据,原因有三:

  1. 物理实现的简便性:电子元件天然具有两种稳定状态(高电平/低电平、有磁/无磁、有光/无光),用0和1表示最为可靠。

  2. 运算规则的简洁性:二进制算术运算规则远比十进制简单。例如,二进制加法只需记住四条规则(0+0=0, 0+1=1, 1+0=1, 1+1=10),而十进制加法需要记住45条规则。

  3. 逻辑表达的天然契合:布尔代数中的真(True)和假(False)与二进制的1和0完美对应,使得算术运算和逻辑运算可以在同一套硬件上实现。

在Rust中,我们可以直观地看到各种数据类型的二进制表示:

fn main() {
    let n: u8 = 65;
    println!("十进制: {}, 二进制: {:08b}, 十六进制: {:02X}", n, n, n);
    // 输出: 十进制: 65, 二进制: 01000001, 十六进制: 41

    let c: char = 'A';
    println!("字符: {}, Unicode码点: {:04X}", c, c as u32);
    // 输出: 字符: A, Unicode码点: 0041
}

从这段代码可以看出,字符'A'和整数65在底层共享相同的二进制表示01000001。这正是编码的魔力——相同的比特序列,根据不同的解释规则,可以呈现完全不同的含义。

编码的本质:建立映射关系

编码的本质是建立映射关系。具体来说,编码是在两个集合之间建立一一对应(或近似对应)的规则:

  • 字符编码:建立“字符“集合与“二进制数“集合之间的映射。ASCII编码将128个字符映射到0-127的二进制数;Unicode则将全球所有书写系统的字符映射到统一的码点空间。

  • 图像编码:建立“像素颜色“集合与“二进制数“集合之间的映射。RGB编码将每种颜色表示为三个字节(红、绿、蓝各一个字节)。

  • 音频编码:建立“声波振幅“集合与“二进制数“集合之间的映射。PCM编码以固定的时间间隔采样声波的振幅,将连续的模拟信号离散化为数字序列。

  • 视频编码:建立“图像序列“集合与“二进制数“集合之间的映射。由于视频数据量巨大,编码通常包含压缩步骤,利用帧间冗余减少存储空间。

理解编码的映射本质,有助于我们把握各种编码方案的共性与差异。好的编码方案应当满足以下特性:

特性说明
唯一性每个输入有唯一的编码,避免歧义
可逆性编码后能够完整解码还原(无损编码)
紧凑性编码结果尽可能短,节省存储和传输成本
鲁棒性编码包含冗余信息,能够检测或纠正传输错误
高效性编码和解码的计算复杂度在可接受范围内

不同的应用场景对这些特性的侧重不同。例如,文本存储优先保证唯一性和可逆性;网络传输强调紧凑性和鲁棒性;实时音视频则更注重编码和解码的高效性。

信息论简介:香农与信息熵

克劳德·香农(Claude Shannon)被誉为“信息论之父“。1948年,他在贝尔实验室发表了《通信的数学理论》,首次用数学方法定量描述了信息的本质。香农提出的核心概念——信息熵(Entropy),成为衡量信息量的基本单位。

信息熵的直观含义是:一个事件所包含的“惊讶程度“。发生概率越小的事件,一旦发生,带来的信息量越大。例如,“太阳从东方升起“是大概率事件,信息量几乎为零;而“太阳从西方升起“是极小概率事件,一旦发生将带来极大的信息量。

信息熵的数学定义为:

$$H(X) = -\sum_{i=1}^{n} p(x_i) \log_2 p(x_i)$$

其中,$p(x_i)$ 是第 $i$ 个事件发生的概率。信息熵的单位是比特(bit)。

信息熵与编码密切相关。香农证明了:编码一个信息源所需的最小平均比特数,等于该信息源的熵。这就是著名的香农第一定理(无噪声编码定理)。

例如,如果一个信息源只产生两种符号:A(概率90%)和B(概率10%),那么它的熵为:

$$H = -(0.9 \times \log_2 0.9 + 0.1 \times \log_2 0.1) \approx 0.469 \text{ bit}$$

这意味着,理论上我们只需要约0.469比特/符号就能编码这个信息源,远小于直接使用1比特/符号的固定长度编码。这种利用概率分布进行优化编码的思想,正是霍夫曼编码(将在第十三章详细介绍)的理论基础。

信息论的思想不仅适用于数据压缩,还深刻影响了密码学(下卷)、机器学习(中卷)等领域。第二十一章“打包/拆包 压缩/解压“将更深入地探讨信息论在数据压缩中的应用。

上卷各章节内容预览

上卷共十三章,从基础数据类型到复杂编码系统,逐步构建完整的“数据与编码“知识体系:

第一至第四章:数据的原子

这四章关注最基本的数据类型,它们是构建一切复杂数据的“原子“:

  • 第一 数字:计算机如何表示整数和浮点数?为什么0.1 + 0.2 != 0.3?本章从二进制表示出发,讲解原码、反码、补码、IEEE 754浮点标准,以及Rust中的数值类型系统。

  • 第二 字符与编码:从摩斯电码到ASCII,从GB2312到Unicode,从UTF-8到UTF-16。字符编码的发展史是一部人类追求“天下同文“的技术史诗。本章将梳理这段历史,并展示Rust对Unicode的原生支持。

  • 第三 字节:字节(Byte)是计算机存储的基本单位。本章讲解字节序(大端/小端)、位操作、字节缓冲区,以及Rust中Vec<u8>Bytes等类型的使用。

  • 第四 时间:时间是人类最古老的概念之一,但在计算机中表示时间却充满挑战。时间戳、时区、闰秒、夏令时、ISO 8601标准——本章将揭示“时间“背后的复杂性,并介绍Rust的chrono库。

第五至第九章:多媒体数据

这五章将编码思想应用于图像、音频、视频等多媒体数据:

  • 第五 图片:位图与矢量图有何区别?BMP、PNG、JPEG、GIF各自采用什么编码策略?本章从像素出发,讲解图像的数字化表示和常见格式的编码原理。

  • 第六 条码:EAN-13、Code 128、Code 39——一维条码是商品流通的“身份证“。本章讲解条码的编码规则、校验算法,以及如何用Rust生成和识别条码。

  • 第七 二维码:QR Code是二维条码的代表。本章深入QR Code的编码机制:数据模式、纠错等级、掩码模式、定位图案,并展示Rust中的二维码生成与解析。

  • 第八 音频:声音是连续的模拟信号,如何将其数字化?采样定理、量化、编码——从WAV到MP3,从PCM到AAC,本章探索音频编码的技术演进。

  • 第九 视频:视频是图像的时间序列,但视频编码远不止“连续播放图片“那么简单。帧类型(I/P/B帧)、运动补偿、变换编码——本章介绍H.264、H.265等主流视频标准的编码原理。

第十至第十三章:编码的升华

这四章将编码思想提升到更高层次:

  • 第十 万物皆可编码:从DNA序列到地理位置,从情绪表情到区块链交易——本章展示编码思想的普适性,理解“一切皆可表示为数据“的数字化哲学。

  • 第十一 序列化与反序列化:数据需要在内存中高效访问,也需要在磁盘上持久存储、在网络上传输。序列化就是数据结构到字节流的编码,反序列化则是逆向过程。本章介绍JSON、XML、Protobuf、MessagePack等序列化格式,以及Rust中的serde库。

  • 第十二 网络协议:网络协议本质上是通信双方约定的编码规则。从物理层的比特编码到应用层的HTTP报文,本章解析协议栈中各层的数据封装与编码方式。

  • 第十三 压缩算法:数据压缩是编码的“优化版“——用更少的比特表示相同的信息。本章介绍游程编码、霍夫曼编码、LZ系列算法、算术编码等经典压缩算法,以及Rust中的压缩库。

上卷知识图谱

上卷十三章的知识可以用以下层次结构来概括:

数据与编码
├── 基础数据表示
│   ├── 数字(二进制、整数、浮点数)
│   ├── 字符与编码(ASCII、Unicode、UTF-8)
│   ├── 字节(字节序、位操作)
│   └── 时间(时间戳、时区、格式)
├── 多媒体编码
│   ├── 图片(位图、矢量图、图像格式)
│   ├── 条码(一维条码、编码规则)
│   ├── 二维码(QR Code、纠错编码)
│   ├── 音频(采样、量化、音频编码)
│   └── 视频(帧、压缩、视频标准)
├── 高级编码技术
│   ├── 万物皆可编码(编码的普适性)
│   ├── 序列化与反序列化(数据交换格式)
│   ├── 网络协议(分层编码体系)
│   └── 压缩算法(信息熵、无损/有损压缩)
└── 理论基础
    └── 信息论(香农、信息熵、编码定理)

这个知识图谱展示了上卷内容的内在逻辑:从基础到应用,从简单到复杂,从具体到抽象。每一层都建立在前一层的基础之上,形成完整的知识体系。

阅读建议

学习路径

路径一:顺序阅读。对于初次接触数据编码的读者,建议按章节顺序阅读。前四章是基础,务必扎实掌握;第五至第九章可按兴趣选择顺序;最后四章需要前面的知识铺垫,建议放在后面阅读。

路径二:问题导向。如果你已经有一定的编程经验,可以带着具体问题来阅读:

  • “为什么我的浮点数计算结果不准确?” → 第一 数字
  • “如何处理中文乱码问题?” → 第二 字符与编码
  • “如何设计一个高效的数据交换格式?” → 第十一 序列化与反序列化
  • “如何减小我的数据文件大小?” → 第十三 压缩算法

实践建议

  1. 动手实验:每章都包含Rust代码示例,建议读者在本地环境或Rust Playground中运行这些代码,观察输出结果,尝试修改参数。

  2. 对比学习:将Rust的实现与其他语言(如Python、C、Java)进行对比,体会Rust在类型安全、性能、表达能力方面的特点。

  3. 项目驱动:尝试用Rust实现一个小项目,如:二维码生成器、图片格式转换工具、文本压缩程序等。项目驱动是巩固知识的最好方式。

  4. 查阅标准:编码的本质是标准。阅读相关RFC文档(如UTF-8的RFC 3629、PNG的RFC 2083)可以加深对编码原理的理解。

与中卷、下卷的衔接

上卷介绍的编码知识是中卷和下卷的基础:

  • 中卷的哈希(第二十)、排序(第二十三)、神经网络(第三十六)等章节,都需要理解数据的二进制表示。
  • 下卷的对称密码(第四十四)、非对称密码(第四十五)、哈希函数(第四十六)等章节,本质上是特殊的编码——将明文编码为密文,且编码规则依赖于密钥。

因此,扎实掌握上卷内容,将为后续学习打下坚实基础。


数据是信息的载体,编码是数据的灵魂。当我们理解了数据如何被表示、如何被编码,就掌握了打开数字世界大门的钥匙。让我们从上卷开始,踏上“Rust七十二变“的学习之旅。

第一 数字

计算机正如其名,从诞生之日,就是为了进行快速地数值计算,主要用于炮弹的弹道计算。为了搞清楚计算机的工作原理,我们首先要了解数字(尤其是整数)在计算机中是怎么表示的。本章将从最基础的二进制编码出发,逐步深入进制转换、大数运算、浮点数表示、特殊数以及素数与数论等领域,并结合 Rust 代码进行实践。

1.1 编码方式

1.1.1 二进制与编码概述

我们知道计算机中数据都是以 0、1 组成的二进制进行编码、计算、存储、传输的。二进制编码又分为:原码、反码、补码几种编码方式。

所谓原码就是二进制定点表示法,即最高位为符号位,“0“表示正,“1“表示负,其余位表示数值的大小。

1.1.2 原码、反码、补码

原码(Sign-Magnitude):最高位表示符号,其余位表示数值的绝对值。对于 $n$ 位有符号整数,原码的表示范围为 $[-(2^{n-1}-1), 2^{n-1}-1]$。

$$[x]_{原} = \begin{cases} x & x \geq 0 \ 2^{n-1} + |x| & x < 0 \end{cases}$$

反码(Ones’ Complement):正数的反码与其原码相同;负数的反码是对正数逐位取反,符号位保持为 1。

$$[x]_{反} = \begin{cases} x & x \geq 0 \ 2^n - 1 + x & x < 0 \end{cases}$$

补码(Two’s Complement):正数的补码与其原码相同;负数的补码是在其反码的末位加 1。在计算机系统中,数值一律用补码来表示和存储。

$$[x]_{补} = \begin{cases} x & x \geq 0 \ 2^n + x & x < 0 \end{cases}$$

使用补码的好处在于:可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(减法转化为加负数的补码)。

1.1.3 编码转换规则

原码 => 反码 => 补码

原码 => 补码

(1) 正整数的补码是其二进制表示,与原码相同。

(2) 求负整数的补码,将其原码除符号位外的所有位取反(0 变 1,1 变 0,符号位为 1 不变)后加 1。

补码 => 原码

(1) 如果补码的符号位为“0“,表示是一个正数,其原码就是补码。

(2) 如果补码的符号位为“1“,表示是一个负数,那么求给定的这个补码的补码就是要求的原码。

简而言之:正整数的原码、反码、补码相同;负整数的补码就是在原码的基础上符号位不变,其他位取反(得到反码),然后加 1。

以 -1 为例(假设类型为 i8),-1 的原码为 10000001,反码为 11111110,补码为 11111111。由此可见反码作为原码和补码相互转换的中间码。

1.1.4 编码方式对比表

编码方式正数表示负数表示零的表示表示范围(n 位)特点
原码符号位 0 + 绝对值符号位 1 + 绝对值+0: 00...0, -0: 10...0$[-(2^{n-1}-1), 2^{n-1}-1]$直观,但存在正负零
反码与原码相同原码逐位取反+0: 00...0, -0: 11...1$[-(2^{n-1}-1), 2^{n-1}-1]$存在正负零,运算需循环进位
补码与原码相同反码末位加 1唯一:00...0$[-2^{n-1}, 2^{n-1}-1]$无正负零,加减法统一处理

1.1.5 Rust 整数类型与进制输出

Rust 提供了丰富的整数类型,下面是各类型的取值范围:

#![allow(unused)]
fn main() {
// 十进制
println!("{}", i8::MIN);
println!("{}", i8::MAX);
println!("{}", i16::MIN);
println!("{}", i16::MAX);
println!("{}", i32::MIN);
println!("{}", i32::MAX);
println!("{}", i64::MIN);
println!("{}", i64::MAX);
println!("{}", u8::MIN);
println!("{}", u8::MAX);
println!("{}", u16::MIN);
println!("{}", u16::MAX);
println!("{}", u32::MIN);
println!("{}", u32::MAX);
println!("{}", u64::MIN);
println!("{}", u64::MAX);
}

Rust 支持多种进制的格式化输出:

#![allow(unused)]
fn main() {
// 二进制 10001
println!("0b{:08b}", 17i8);
// 八进制 21
println!("0o{:08o}", 17i8);
// 十六进制 11
println!("0x{:08x}", 17i8);

println!("0b{:08b}", 0i8);
println!("0b{:08b}", 1i8);
println!("0b{:08b}", -1i8);
println!("0b{:08b}", i8::MAX);

println!("0b{:08b}", -2i8);
println!("0b{:08b}", 17u8);
println!("0o{:08o}", 17u8);
println!("0x{:08x}", 17u8);
}

1.2 进制转换

1.2.1 进制转换概述

进制是计数系统的基础。日常生活中我们使用十进制(基数为 10),而计算机使用二进制(基数为 2)。此外,八进制和十六进制也因便于表示二进制而被广泛使用。不同进制之间的转换是程序员的基本功。

进制转换的核心公式:一个 $b$ 进制数 $(d_n d_{n-1} … d_1 d_0)_b$ 转换为十进制:

$$N = d_n \times b^n + d_{n-1} \times b^{n-1} + … + d_1 \times b^1 + d_0 \times b^0 = \sum_{i=0}^{n} d_i \times b^i$$

1.2.2 Excel 表格列名转换(26 进制)

Excel 表格的列名采用了一种特殊的 26 进制表示:A=1, B=2, …, Z=26, AA=27, AB=28, …。这与普通的 26 进制略有不同,因为没有表示 0 的“数字“。

Go 语言实现

// ColumnNameToNumber provides a function to convert Excel sheet column name
// (case-insensitive) to int. The function returns an error if column name
// incorrect.
//
// Example:
//
//	excelize.ColumnNameToNumber("AK") // returns 37, nil
func ColumnNameToNumber(name string) (int, error) {
	if len(name) == 0 {
		return -1, newInvalidColumnNameError(name)
	}
	col := 0
	multi := 1
	for i := len(name) - 1; i >= 0; i-- {
		r := name[i]
		if r >= 'A' && r <= 'Z' {
			col += int(r-'A'+1) * multi
		} else if r >= 'a' && r <= 'z' {
			col += int(r-'a'+1) * multi
		} else {
			return -1, newInvalidColumnNameError(name)
		}
		multi *= 26
	}
	if col > MaxColumns {
		return -1, ErrColumnNumber
	}
	return col, nil
}

// ColumnNumberToName provides a function to convert the integer to Excel
// sheet column title.
//
// Example:
//
//	excelize.ColumnNumberToName(37) // returns "AK", nil
func ColumnNumberToName(num int) (string, error) {
	if num < MinColumns || num > MaxColumns {
		return "", ErrColumnNumber
	}
	var col string
	for num > 0 {
		col = string(rune((num-1)%26+65)) + col
		num = (num - 1) / 26
	}
	return col, nil
}

Rust 语言实现

#![allow(unused)]
fn main() {
const MIN_COLUMNS: usize = 1;
const MAX_COLUMNS: usize = 16384;
/// The column number convert to column name
fn column_number_to_name(num: usize) -> std::string::String {
    if num < MIN_COLUMNS || num > MAX_COLUMNS {
        return "".to_string();
    }
    let mut ret = num;
    let mut col = std::string::String::new();
    while ret > 0 {
        let ch = ((ret - 1) % 26 + 65) as u8;
        ret = (ret - 1) / 26;
        col.insert(0, ch as char);
    }

    col
}

///
/// column name convert to column number
fn column_name_to_number(name: std::string::String) -> usize {
    let len = name.len();
    if len == 0 {
        return 0;
    }
    let mut col = 0;
    let bytes = name.as_bytes();
    let mut i = len - 1;
    let mut multi = 1;
    loop {
        let ch = bytes[i];
        if ch >= b'A' && ch <= b'Z' {
            col += multi * (ch - b'A' + 1) as usize;
        } else if ch >= b'a' && ch <= b'z' {
            col += multi * (ch - b'a' + 1) as usize;
        } else {
            return 0;
        }

        if i < 1 {
            break;
        }
        i -= 1;
        multi *= 26;
    }
    if col > MAX_COLUMNS {
        return 0;
    }

    col
}

println!("{}", column_number_to_name(28));
println!("{}", column_name_to_number("AF".to_string()));
}

1.3 实战应用

1.3.1 力扣实战:十进制整数的反码

1009. 十进制整数的反码

原码通过与掩码进行异或(XOR)运算得到“反码“,注意:这里的“反码“和真正的反码定义是不一样的。

pub fn bitwise_complement(n: i32) -> i32 {
    if n == 0 {
        return 1;
    }
    let mut num = n;
    let mut mark = 1;
    let mut high_bit = 0;
    while num > 0 {
        num >>= 1;
        high_bit += 1;
    }

    //dbg!(high_bit);
    let mark = match (high_bit == 31) {
        true => i32::MAX - 1,
        false => (1 << high_bit) - 1,
    };

    n ^ mark
}

fn main() {
    let n = 911;
    let complement = bitwise_complement(n);
    println!("{:032b}", n);
    println!("{:032b}", complement);
}

1.4 大数运算

1.4.1 大整型概述

数值多大才能称得上天文数字呢?天文数字又有何用呢?在现代密码学、科学计算和数论研究中,大数运算是不可或缺的。Rust 的标准整数类型有固定的位数限制,对于超出范围的计算,需要使用专门的大数库。

1.4.2 古戈尔

1 古戈尔 = $10^{100}$(10 的 100 次方)

目前人类发现的最大素数,是 $2^{136279841} - 1$,这个数字有 41,024,320 位。

葛立恒数 葛立恒数诞生于组合数学领域的拉姆齐理论(Ramsey Theory)。这个理论的核心思想是:在足够大的系统中,完全的无序是不可能的,一定会出现某种有规则的子结构

TREE(3) 如果把葛立恒数比作一个原子的大小,那么 TREE(3) 的大小可能相当于一个已知宇宙的规模。在数学上,TREE(3) 的增长速度已经超越了使用葛立恒数定义过程中所使用的递归方法所能达到的极限。更具体地说,TREE(3) 的增长速度远非葛立恒数可比,它已经触及了 “增长速度超越皮亚诺算术” 的领域。

1.4.3 阶乘与末尾零

问题:100 的阶乘(100!)末尾有多少个零呢?

要计算 $n!$($n$ 的阶乘)末尾有多少个零,其实就是看这个乘积里因子 10 有多少个。而 $10 = 2 \times 5$,在阶乘中因子 2 的数量远多于因子 5,所以末尾零的个数等于因子 5 的个数。

计算方法(勒让德公式):

$$Z(n!) = \sum_{k=1}^{\infty} \left\lfloor \frac{n}{5^k} \right\rfloor = \left\lfloor \frac{n}{5} \right\rfloor + \left\lfloor \frac{n}{25} \right\rfloor + \left\lfloor \frac{n}{125} \right\rfloor + \cdots$$

以 $n = 100$ 为例:

$$Z(100!) = \left\lfloor \frac{100}{5} \right\rfloor + \left\lfloor \frac{100}{25} \right\rfloor + \left\lfloor \frac{100}{125} \right\rfloor + \cdots = 20 + 4 + 0 + \cdots = 24$$

答案:100! 的末尾有 24 个零。

1.4.4 Rust 大数运算示例

#![allow(unused)]
fn main() {
use num::bigint::{BigInt, ToBigInt};
/// 计算 x 的阶乘,即 x!
fn factorial(x: i32) -> BigInt {
    if let Some(mut facatorial) = 1.to_bigint() {
        for i in 1..(x + 1) {
            facatorial *= i;
        }
        facatorial
    } else {
        panic!("Failed to calculate factorial!");
    }
}
let result = factorial(100);
println!("{}", result);

use num::BigUint;
let x = BigUint::parse_bytes(b"786fe10f87d8ddfeeeea9a4a49e63388e3b2a9e1b0a794907908f6123dbf6c6a", 16).unwrap();
println!("{}", x);
// SM2 密码算法曲线所使用的质数
let p = BigUint::parse_bytes(b"FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF", 16).unwrap();
println!("{}", p);
}

1.5 浮点数

1.5.1 IEEE 754 标准

现代计算机普遍采用 IEEE 754 标准存储浮点数,主要由三部分组成:

$$\text{浮点数} = (-1)^{sign} \times (1 + mantissa) \times 2^{(exponent - bias)}$$

  • 符号位(Sign Bit):表示浮点数的正负,1 位。
  • 指数位(Exponent):表示浮点数的指数部分,单精度 8 位,双精度 11 位。
  • 尾数位(Mantissa/Significand):表示浮点数的小数部分,单精度 23 位,双精度 52 位。
  • 偏置(Bias):指数部分的偏移量,用于表示负数指数。单精度 bias = 127,双精度 bias = 1023。
类型符号位指数位尾数位总位数偏置精度(约)
f32(单精度)1823321277 位有效数字
f64(双精度)1115264102315 位有效数字

1.5.2 浮点数的精度问题

fn main() {
    // 默认是 f64
    let a: f64 = 0.1;
    let b: f64 = 0.2;

    // 使用 f32 精度更低
    let c: f32 = 0.1;
    let d: f32 = 0.2;

    println!("f64: {}", a + b);  // 0.30000000000000004
    println!("f32: {}", c + d);  // 0.3

    // 比较浮点数(不推荐直接使用 ==)
    let x: f64 = 0.1 + 0.2;
    let y: f64 = 0.3;
    println!("直接比较: {}", x == y);  // false

    // 使用容差比较
    let eps = 1e-10;
    println!("容差比较: {}", (x - y).abs() < eps);  // true
}

浮点数的精度问题原因:

  • 原因 1:二进制无法精确表示某些十进制小数(如 0.1)
  • 原因 2:运算顺序影响精度
  • 原因 3:舍入误差累积

解决精度问题

  • 方案一:rust_decimal — 高性能金融计算首选
  • 方案二:bigdecimal — 任意精度的“瑞士军刀”

1.6 特殊数

1.6.1 数的分类概述

数可以按照多种方式进行分类:整数(奇数、偶数)、浮点数、复数、质数、合数、完美数等。本节介绍两种有趣的特殊数——勾股数和的士数。

1.6.2 勾股数

勾股数(Pythagorean triple)是指满足勾股定理的三个正整数 $(a, b, c)$ 的一组解:

$$a^2 + b^2 = c^2$$

例子:

$$3^2 + 4^2 = 5^2$$ $$5^2 + 12^2 = 13^2$$ $$6^2 + 8^2 = 10^2$$ $$8^2 + 15^2 = 17^2$$ $$7^2 + 24^2 = 25^2$$

勾股数有无穷多组,可以通过欧几里得公式生成:对于任意正整数 $m > n$,令

$$a = m^2 - n^2, \quad b = 2mn, \quad c = m^2 + n^2$$

则 $(a, b, c)$ 必为一组勾股数。

1.6.3 的士数

的士数(Taxicab Number,记为 $Ta(n)$):能写成 $n$ 组两个正整数立方和的最小正整数。

$$N = a_1^3 + b_1^3 = a_2^3 + b_2^3 = … = a_n^3 + b_n^3$$

最著名的的士数是 $1729 = 1^3 + 12^3 = 10^3 + 9^3$。这个数因数学家哈代和拉马努金的一段著名对话而得名。

1.7 素数与数论

1.7.1 素数概述

质数(素数)是数论的核心研究对象。素数在密码学(如 RSA 算法)、哈希函数、随机数生成等领域有着广泛应用。

素数的主要类型包括:

  • 孪生素数:相差为 2 的一对素数,如 (3, 5), (11, 13), (17, 19)
  • 梅森素数:形如 $2^p - 1$ 的素数,其中 $p$ 本身也是素数
  • 费马素数:形如 $2^{2^n} + 1$ 的素数

1.7.2 素数的定义与性质

定义:质数(prime number)又称素数,有无限个。质数定义为在大于 1 的自然数中,除了 1 和它本身以外不再有其他因数的数称为质数。

质数的性质

  • 无穷性:质数有无穷多个(欧几里得证明)。
  • 分布规律:随着数字增大,质数会变得越来越稀疏。质数定理描述了这种分布:小于 $n$ 的质数个数约为 $\frac{n}{\ln n}$。
  • 唯一分解定理(算术基本定理):任何一个大于 1 的整数,无论它有多大,都可以被唯一地分解成一组质数的乘积。

$$n = p_1^{a_1} \times p_2^{a_2} \times … \times p_k^{a_k}$$

  • 素性检验:判断一个大数是否为质数在计算上非常困难,没有简单的通用公式,这也是许多密码系统安全性的基础。

1.7.3 著名猜想与定理

  • 费马小定理:对于质数 $p$ 和任意整数 $a$,有 $a^{p-1} \equiv 1 \pmod{p}$。

  • 欧拉定理:对于互质的正整数 $a$ 和 $n$,有 $a^{\varphi(n)} \equiv 1 \pmod{n}$,其中 $\varphi(n)$ 是欧拉函数,表示小于 $n$ 且与 $n$ 互质的正整数的个数。

  • 哥德巴赫猜想:任何一个大于 2 的偶数,都可以写成两个质数之和。例如,$4 = 2 + 2$,$10 = 3 + 7$。这个猜想至今未被证明,但已被计算机验证到非常大的数。

  • 孪生素数猜想:存在无穷多对相差为 2 的质数。目前,数学家张益唐在该问题上取得了里程碑式的突破。

  • 黎曼猜想:这是关于质数分布规律的终极猜想,被认为是数学界最重要的未解难题之一。它试图用黎曼 $\zeta$ 函数来精确描述质数的分布:

$$\zeta(s) = \sum_{n=1}^{\infty} \frac{1}{n^s} = \frac{1}{1^s} + \frac{1}{2^s} + \frac{1}{3^s} + \cdots$$

  • 克拉茨猜想:对于任意正整数,如果是偶数则除以 2,如果是奇数则乘 3 加 1,重复此过程,最终都会到达 1。

1.8 总结

本章知识点汇总

知识点核心内容
原码最高位为符号位,其余表示数值绝对值
反码负数逐位取反,存在正负零问题
补码负数反码加 1,计算机中统一使用,无正负零
进制转换$N = \sum d_i \times b^i$,Excel 列名为特殊 26 进制
阶乘末尾零$Z(n!) = \sum \lfloor n/5^k \rfloor$
IEEE 754$(-1)^{sign} \times (1 + mantissa) \times 2^{exponent - bias}$
勾股数$a^2 + b^2 = c^2$,欧几里得公式可生成
素数大于 1 且只有 1 和自身两个因子的自然数

Rust 整数类型范围

类型最小值最大值位数
i8-1281278
i16-32,76832,76716
i32-2,147,483,6482,147,483,64732
i64-9,223,372,036,854,775,8089,223,372,036,854,775,80764
u802558
u16065,53516
u3204,294,967,29532
u64018,446,744,073,709,551,61564

1.9 练习题

  1. 基础题:写出 -5(i8 类型)的原码、反码和补码,并用 Rust 代码验证 println!("0b{:08b}", -5i8) 的输出。

  2. 进制转换:编写一个 Rust 函数,将十进制整数转换为任意进制(2-36)的字符串表示。

  3. Excel 列名:使用 Rust 实现 column_number_to_namecolumn_name_to_number 函数,并编写测试用例验证 1->A, 26->Z, 27->AA, 702->ZZ, 703->AAA 等边界情况。

  4. 阶乘末尾零:编写一个 Rust 函数,输入整数 $n$,计算 $n!$ 末尾零的个数。验证 $n = 100$ 时结果为 24,$n = 1000$ 时结果为 249。

  5. 浮点数比较:解释为什么 0.1 + 0.2 != 0.3 在计算机中成立,并编写一个 Rust 函数,使用容差法正确比较两个 f64 浮点数是否相等。

  6. 勾股数生成:使用欧几里得公式编写 Rust 代码,生成前 10 组本原勾股数(即 $gcd(a, b, c) = 1$ 的勾股数)。

  7. 素数判定:编写一个 Rust 函数,使用试除法判断一个正整数是否为素数,并分析其时间复杂度。尝试优化到只需检查到 $\sqrt{n}$。

  8. 思考题:在密码学中,为什么需要使用大素数(数百位甚至上千位)?如果素数很小会有什么安全隐患?

第二 字符与编码

二进制能够表示数字,那能不能表示数字之外的各种文字,如英文、中文、俄语、法语等世界各个国家的文字?答案是肯定的,不仅可以表示各种文字,还能表示各种符号、图形(包括表情包)等。其中就使用到一种古老而又充满活力的技术——编码(coding)技术

计算机中的编码技术,简单而言就是建立数字与字符之间一一对应的关系。比如用数字0到9分别表示其本身,10表示A,11表示B,12表示C,以此类推,就能解决所有字符的编码问题。很显然实际上存在无数种编码的方式。正所谓“无规矩不成方圆“,没有统一的编码规则是不行的。本章将系统介绍计算机中主流的字符编码方案,从经典的ASCII到现代的Unicode,以及Rust中的字符串处理。


2.1 ASCII编码

美国作为计算机的发明地,最先提出了一套编码方案——ASCII(American Standard Code for Information Interchange,美国信息交换标准代码),即ASCII码

2.1.1 ASCII编码原理

ASCII码使用7位二进制数(共128个码位)来表示字符,其编码空间为:

$$ 0 \leq \text{ASCII码} \leq 127 \quad (0\text{x}00 \sim 0\text{x}7F) $$

ASCII表

ASCII码是计算机历史上最著名的编码方案之一,它最初是美国国家标准,供不同计算机在相互通信时用作共同遵守的西文字符编码标准,后来它被国际标准化组织(International Organization for Standardization, ISO)定为国际标准,称为ISO 646标准。

2.1.2 ASCII控制字符

ASCII码中,0~31和127为控制字符,用于控制设备的操作:

十进制十六进制缩写含义
00x00NUL空字符
70x07BEL响铃
80x08BS退格
90x09HT水平制表符
100x0ALF换行
130x0DCR回车
270x1BESC转义
1270x7FDEL删除

2.1.3 ASCII可打印字符

32~126为可打印字符,包括数字、英文字母、标点符号等:

范围内容
48~57 (0x30~0x39)数字 0~9
65~90 (0x41~0x5A)大写英文字母 A~Z
97~122 (0x61~0x7A)小写英文字母 a~z
32~47, 58~64, 91~96, 123~126空格、标点及特殊符号

ASCII码的局限性也是显而易见的,它仅能表示128个字符,对于数以万计的中国汉字就显得十分无力了。


2.2 ANSI编码——各国方言

ANSI编码并非一个具体的编码标准,而是对不同国家和地区各自制定的多字节字符编码标准的统称。不同的国家和地区制定了不同的标准,由此产生了 GB2312、GBK、GB18030、Big5、Shift_JIS 等各自的编码标准。这些使用多个字节来代表一个字符的各种汉字延伸编码方式,统称为 ANSI 编码

2.2.1 各国主要编码标准

编码标准国家/地区字节数收录字符数说明
GB2312中国大陆1~2字节~7,000最早的中文编码标准
GBK中国大陆1~2字节~21,000GB2312的扩展
GB18030中国大陆1~4字节~160万国家标准,兼容Unicode
Big5中国台湾1~2字节~13,000繁体中文编码
Shift_JIS日本1~2字节~12,000日文编码
EUC-KR韩国1~2字节~11,000韩文编码
ISO-8859-1西欧1字节256Latin-1,西欧语言

2.2.2 ANSI编码的局限

ANSI编码的核心问题是:相同的字节序列在不同编码下可能表示不同的字符。例如,在简体中文Windows系统中,字节0xD6 0xD0表示“中“字(GB2312编码);而在日语系统中,相同的字节序列可能表示完全不同的字符。这种“方言“式的编码体系是产生乱码的根本原因。


2.3 Unicode编码——计算机世界中的“书同文“

秦始皇统一六国后,为了便于管理庞大的帝国,保证政令通畅,于是推行统一的文字和度量衡——“书同文,车同轨“不仅是秦始皇一项重要的历史贡献,也深刻影响和促成中国之后两千多年大一统的政治格局的形成。如今世界那么大,各国的文字、符号的数量是相当惊人的,并且随着社会的发展,新的字符不断涌现。字符的编码也需要与时俱进。Unicode(统一码,也叫万国码、单一码)应运而生,彻底解决全世界所有国家的文字、符号的编码问题。

2.3.1 Unicode编码空间

Unicode编码系统可分为编码方式和实现方式两个层次。Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。Unicode用数字0~0x10FFFF来映射这些字符,最多可以容纳1114112个字符,或者说有1114112个码位。码位(Code Point)就是可以分配给字符的数字,通常表示为 U+XXXX 的形式。

Unicode编码空间划分为若干平面(Plane):

平面范围名称说明
第0平面U+0000 ~ U+FFFFBMP(基本多文种平面)最常用的字符,包括基本拉丁字母、CJK统一表意文字等
第1平面U+10000 ~ U+1FFFFSMP(多文种补充平面)古文字、音乐符号等
第2平面U+20000 ~ U+2FFFFSIP(表意文字补充平面)罕用汉字
第3~13平面U+30000 ~ U+DFFFF保留未分配
第14平面U+E0000 ~ U+EFFFFSSP(特殊用途补充平面)格式控制字符
第15~16平面U+F0000 ~ U+10FFFFPUA(私人使用区)用户自定义字符

2.3.2 UTF-8编码原理

UTF-8(Unicode Transformation Format - 8-bit)是一种变长编码方案,使用1~4个字节表示一个Unicode字符:

Unicode码位范围UTF-8字节序列字节数
U+0000 ~ U+007F0xxxxxxx1
U+0080 ~ U+07FF110xxxxx 10xxxxxx2
U+0800 ~ U+FFFF1110xxxx 10xxxxxx 10xxxxxx3
U+10000 ~ U+10FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx4

UTF-8的编码规则可以用以下方式理解:

$$ \text{UTF-8字节数} = \begin{cases} 1 & \text{if } 0 \leq U \leq 0\text{x}7F \ 2 & \text{if } 0\text{x}80 \leq U \leq 0\text{x}7FF \ 3 & \text{if } 0\text{x}800 \leq U \leq 0\text{x}FFFF \ 4 & \text{if } 0\text{x}10000 \leq U \leq 0\text{x}10FFFF \end{cases} $$

编码示例

字符Unicode码位UTF-8字节序列(十六进制)
‘A’U+004141
‘中’U+4E2DE4 B8 AD
‘𠮷’(吉的异体字)U+20BB7F0 A0 AE B7
‘💖’U+1F496F0 9F 92 96

UTF-8的优势在于:

  • 向后兼容ASCII:纯ASCII文本在UTF-8下完全不变
  • 无字节序问题:不需要BOM(虽然可以添加)
  • 自同步:可以从任意字节位置开始解析

2.3.3 UTF-16编码原理

UTF-16使用2或4个字节表示一个Unicode字符:

  • BMP内字符(U+0000 ~ U+FFFF):直接使用2字节表示
  • 辅助平面字符(U+10000 ~ U+10FFFF):使用4字节(代理对)表示

代理对编码方式:

$$ \text{辅助平面字符编码} = \begin{cases} \text{高位代理} = \frac{U - 0\text{x}10000}{0\text{x}400} + 0\text{x}D800 \ \text{低位代理} = (U - 0\text{x}10000) \bmod 0\text{x}400 + 0\text{x}DC00 \end{cases} $$

其中,高位代理范围:0xD800~0xDBFF,低位代理范围:0xDC00~0xDFFF。

2.3.4 UTF-32编码原理

UTF-32使用固定4个字节表示所有Unicode字符,是最简单的编码方式:

$$ \text{UTF-32编码} = \text{Unicode码位值} $$

虽然UTF-32在字符定位上效率最高(O(1)随机访问),但由于每个字符固定占用4字节,对于以ASCII为主的文本,空间效率较低。

2.3.5 三种UTF编码对比

特性UTF-8UTF-16UTF-32
编码长度变长(1~4字节)变长(2或4字节)定长(4字节)
空间效率(ASCII文本)最优较差(2倍)最差(4倍)
空间效率(CJK文本)一般(3字节/字)较好(2字节/字)较差
随机访问O(n)O(n)O(1)
字节序无关需要BOM需要BOM
自同步
主要使用场景网络传输、Linux/macOSWindows、Java、JavaScript内部处理

2.3.6 BOM(字节顺序标记)

BOM(Byte Order Mark)是放在文本文件开头的特殊字符(U+FEFF),用于标识字节序:

编码BOM字节序列说明
UTF-8EF BB BF可选,通常不推荐
UTF-16 BEFE FF大端序
UTF-16 LEFF FE小端序(Windows默认)
UTF-32 BE00 00 FE FF大端序
UTF-32 LEFF FE 00 00小端序

访问如下网站查看所有Unicode符号:


2.4 中文编码

中国程序员在职业生涯中,一定会遇到的一个问题就是中文乱码问题。操作系统、数据库、网络传输都有可能出现中文乱码问题,其本质就是使用了不恰当的编码对中文数据进行编码、解码。因此非常有必要了解各种场景下系统正在使用的编码方式。

2.4.1 中文编码标准演进

为了解决汉字的编码问题,中国制定了GB2312、GBK、GB18030等编码标准。

编码标准发布时间字节数收录汉字数特点
GB23121980年1~2字节6,763覆盖常用简体字,兼容ASCII
GBK1995年1~2字节21,003扩展GB2312,含繁体字
GB180302000年1~4字节160万+国家标准,与Unicode完全映射

2.4.2 编码转换 iconv

生僻字 GB2312、GBK两种编码基本解决中文常用字符的编码,但中文中还有大量生僻字,需要GB18030才能编码。因此需要视具体情况采用恰当的编码。

# 查看文件编码
file -bi example.txt

# UTF-8转为GBK
iconv -f UTF-8 -t GBK 沁园春·雪.txt > snow.txt

# 忽略无法转换的字符
iconv -c UTF-8 -t GBK 沁园春·雪.txt > snow.txt

# GBK转为UTF-8
iconv -f GBK -t UTF-8 abc.txt > abc_1.txt

# UTF-8转为GB18030
iconv -f UTF-8 -t GB18030 生僻字.txt > gb18030.txt

2.4.3 操作系统的编码

Windows下中文常用支持中文编码有:简体中文(GB2312)、繁体中文(Big5)、UTF-8。

# Windows PowerShell
PS C:\Users\huang> chcp
活动代码页: 936
代码页国家(地区)或语言
437美国
932日文(Shift-JIS)
936中国 - 简体中文(GB2312)
949韩文
950繁体中文(Big5)
1200Unicode
1252西欧(Windows)
65001Unicode (UTF-8)

ISO-8859-1(Latin-1) 是MySQL数据库默认编码,也是网络传输默认编码之一。


2.5 Rust中的字符串处理

Rust中默认使用的是UTF-8编码。这一设计决策与Rust的内存安全和零成本抽象理念高度一致。

2.5.1 Rust中的String、&str与Vec<u8>

Rust中的字符串类型设计体现了所有权和借用的核心概念:

类型内存布局所有权说明
StringVec<u8> 的包装拥有堆分配的UTF-8字符串,可增长
&str指向UTF-8字节序列的引用借用字符串切片,不可变引用
Vec<u8>字节数组拥有原始字节序列,不保证UTF-8合法性
char4字节(Unicode标量值)值类型单个Unicode字符
fn main() {
    // String:拥有所有权的UTF-8字符串
    let mut s = String::from("hello");
    s.push_str(" world");
    println!("String: {}", s);

    // &str:字符串切片(借用)
    let slice: &str = &s;
    println!("&str: {}", slice);

    // String 与 &str 的关系
    let owned: String = slice.to_string();  // &str -> String(拷贝)
    let borrowed: &str = &owned;             // String -> &str(借用)

    // `Vec<u8>`:原始字节
    let bytes: Vec<u8> = owned.into_bytes();
    println!("`Vec<u8>`: {:?}", bytes);

    // `Vec<u8>` -> String(需要验证UTF-8)
    let s2 = String::from_utf8(bytes).unwrap();
    println!("String from bytes: {}", s2);
}

2.5.2 Rust中的Unicode操作

fn main() {
    // 从UTF-8字节构造字符串
    let sparkle_heart_vec = vec![240, 159, 146, 150];
    let sparkle_heart = String::from_utf8(sparkle_heart_vec).unwrap();
    assert_eq!("💖", sparkle_heart);

    let bytes = sparkle_heart.into_bytes();
    assert_eq!(bytes, [240, 159, 146, 150]);

    // Unicode转义序列
    println!("\u{65b0}\u{534e}\u{793e}\u{5feb}\u{8baf}\u{ff1a}\u{0033}\u{6708}\u{0032}\u{0036}\u{65e5}\u{ff0c}\u{4e2d}\u{56fd}\u{548c}\u{6d2a}\u{90fd}\u{62c9}\u{65af}\u{5efa}\u{4ea4}\u{3002}");
    println!("\u{5b78}\u{7fd2}\u{5f37}\u{570b} 伟大复兴");

    // 字符转Unicode码位
    println!("{:X} {:X}", '华' as u32, '夏' as u32);

    // Emoji
    println!("Rust常见emoji(表情符号):\u{1F1E8}\u{1F1F3}🦀💖🚀😂\u{1F980}\u{1F602}");
    println!("常见emoji(国旗):\u{1F1E8}\u{1F1F3} 🇨🇳 🇺🇸 🇷🇺");

    // ASCII转义
    println!("ASCII常见字符:\u{0020}\u{0040}\u{0021}\u{0041}");

    // 数学符号
    println!("数学符号:∂∆∮\u{2211}\u{2200}\u{2208}\u{2209}\u{222b}");
}

2.5.3 String/&str/Vec<u8>转换关系(与第十九章呼应)

use std::str;

fn main() {
    // &str -> String
    let s: &str = "hello";
    let owned: String = String::from(s);
    let owned2: String = s.to_string();
    assert_eq!(owned, owned2);

    // &str -> &[u8]
    let bytes: &[u8] = s.as_bytes();
    println!("bytes: {:?}", bytes); // [104, 101, 108, 108, 111]

    // String -> &str
    let hello = String::from("hello");
    let slice: &str = &hello;
    let slice2: &str = hello.as_str();
    assert_eq!(slice, slice2);

    // String -> `Vec<u8>`
    let hello = String::from("hello");
    let bytes_vec: Vec<u8> = hello.into_bytes();
    println!("into_bytes: {:?}", bytes_vec);

    // &[u8] -> &str(可能失败)
    let valid: &[u8] = b"hello";
    let s: &str = str::from_utf8(valid).unwrap();
    println!("from_utf8: {}", s);

    let invalid: &[u8] = &[0xff, 0xfe]; // 非UTF-8
    let result = str::from_utf8(invalid);
    println!("invalid utf8: {:?}", result); // Err(Utf8Error)

    // `Vec<u8>` -> String(可能失败)
    let bytes = vec![104, 101, 108, 108, 111]; // "hello"的ASCII
    let s = String::from_utf8(bytes).unwrap();
    println!("from_utf8(Vec): {}", s);
}

2.6 数据库编码

数据库编码是中文乱码问题的另一个重灾区。MySQL数据库的历史遗留问题尤其值得关注。

2.6.1 MySQL字符集设置

-- 查看当前数据库字符集设置
show variables like 'character%';

2.6.2 utf8 vs utf8mb4

MySQL中的utf8实际上是utf8mb3的别名,只使用最多3个字节,无法存储4字节的Unicode字符(如Emoji、部分生僻字)。

-- utf8mb4:支持完整的Unicode,包括Emoji
UPDATE bakeries_db.user SET wx_nickname = 'abc🚀🎵✨' WHERE id = 5;

-- utf8mb3(旧版MySQL默认utf8)
-- Error Code: 1366. Incorrect string value: '\xF0\x9F\x9A\x80\xF0\x9F...' for column 'wx_nickname' at row 1
UPDATE uic.user SET wx_nickname = 'abc🚀🎵✨' WHERE id = 5;
字符集最大字节数支持范围说明
utf8mb33字节BMP平面MySQL旧版默认,不支持Emoji
utf8mb44字节全部Unicode推荐使用,完整支持

建议:新项目务必使用 utf8mb4,并设置 character_set_server=utf8mb4


2.7 编码检测与转换

2.7.1 常见编码检测方法

// Rust中使用 encoding_rs 库进行编码检测和转换
use encoding_rs::{GBK, UTF_8};

fn main() {
    // GBK编码的字节
    let gbk_bytes: Vec<u8> = vec![0xD6, 0xD0, 0xCE, 0xC4]; // "中文"的GBK编码

    // GBK -> UTF-8
    let (cow, _, had_errors) = GBK.decode(&gbk_bytes);
    if !had_errors {
        let utf8_string = cow.into_owned();
        println!("转换结果: {}", utf8_string); // 输出: 中文
    }

    // UTF-8 -> GBK
    let utf8_str = "中文";
    let (gbk_result, _, _) = UTF_8.encode(utf8_str);
    println!("GBK字节: {:?}", gbk_result);
}

2.7.2 编码转换注意事项

  1. 有损转换:当目标编码无法表示源编码中的某些字符时,转换可能失败或产生替换字符
  2. BOM处理:UTF-8文件有时包含BOM,读取时需要注意跳过前3个字节
  3. 编码推断:没有绝对可靠的编码自动检测方法,最好明确指定编码

总结

编码方案字节数收录字符数兼容性主要使用场景
ASCII1字节128所有编码兼容英文文本、程序代码
GB23121~2字节~7,000兼容ASCII早期中文系统
GBK1~2字节~21,000兼容GB2312Windows中文系统
GB180301~4字节~160万兼容GBK,映射Unicode中国政府标准
UTF-81~4字节111万+兼容ASCIIWeb、Linux、Rust
UTF-162或4字节111万+不兼容ASCIIWindows、Java
UTF-324字节111万+不兼容ASCII内部处理

练习题

  1. ASCII编码范围:写出字符 ‘A’、‘a’、‘0’ 的ASCII码十进制值,并说明大小写字母ASCII码之间的关系。

  2. UTF-8编码计算:手动计算汉字“中“(Unicode码位 U+4E2D)的UTF-8编码字节序列,并与 "中".as_bytes() 的Rust输出进行对比。

  3. Rust字符串操作:编写Rust代码,创建一个包含Emoji “🦀”(Unicode U+1F980)的 String,然后将其转换为 Vec<u8>,打印每个字节的十六进制值,并验证其符合UTF-8编码规则。

  4. 编码转换陷阱:解释为什么以下Rust代码可能产生非预期结果,并给出修复方案:

    #![allow(unused)]
    fn main() {
    let s = "中文";
    let bytes: Vec<u8> = s.bytes().collect();
    let recovered = String::from_utf8(bytes).unwrap();
    println!("{}", recovered);
    }
  5. BOM处理:编写一个Rust函数 read_file_without_bom(path: &str) -> Result<String, std::io::Error>,读取UTF-8文本文件并自动跳过可能存在的BOM头。

  6. 编码检测:使用 encoding_rs 库编写程序,读取一个未知编码的文本文件,尝试用GBK和UTF-8分别解码,输出两种解码结果供人工判断。

  7. MySQL编码配置:写出创建数据库时指定 utf8mb4 字符集的完整SQL语句,包括数据库、表和连接字符集的设置。

  8. String/&str/Vec<u8>关系图:画出Rust中 String&strVec<u8>&[u8] 之间的转换关系图,标注每个转换方法名称及其可能的失败情况。

第三 字节

字节(Byte)是计算机信息技术用于计量存储容量的基本计量单位,也是计算机编程语言中常用的数据类型。字节是连接硬件存储与软件数据的桥梁——无论是内存中的变量、磁盘上的文件,还是网络中传输的数据包,其底层都是以字节为单位进行组织和处理的。

本章将从字节的计量单位出发,深入探讨字节序、Base64 编码、Hex 编码等核心概念,并结合 Rust 代码展示如何在实际开发中处理字节数据。

3.1 计量单位

3.1.1 位与字节

计算机中所有数据最终都以二进制形式存储和传输。二进制中最小的存储单位是位(bit,Binary Digit),它只能表示 01 两种状态。8 个二进制位组成一个字节(Byte),字节是计算机处理数据的基本单位。

$$1 \text{ Byte} = 8 \text{ bits}$$

为什么一个字节是 8 位而不是其他数字?这源于历史和技术两方面的因素:

  • 历史因素:早期的计算机使用 6 位编码(如 BCD 码)表示字符,但随着 ASCII 标准的普及,需要 7 位来表示 128 个字符。为了容纳扩展 ASCII(256 个字符)并方便处理,8 位成为标准。
  • 技术因素:8 是 2 的幂次方($2^3$),便于计算机硬件设计和寻址。同时,8 位可以表示 $2^8 = 256$ 种状态,足以覆盖绝大多数单字节字符编码需求。

3.1.2 为什么用 1024 而不是 1000

在计算机科学中,存储容量的进位通常采用 1024($2^{10}$)而非 1000。这是因为计算机基于二进制工作,$2^{10} = 1024$ 是最接近 1000 的 2 的幂次方,便于硬件寻址和计算。

然而,这带来了一个混乱:硬盘厂商通常使用 1000 作为进位(1 GB = $10^9$ 字节),而操作系统使用 1024(1 GiB = $2^{30}$ 字节)。因此,一块标称 500 GB 的硬盘,在操作系统中显示的容量约为 465 GiB。

为了区分这两种计量方式,国际电工委员会(IEC)于 1998 年引入了二进制前缀:

十进制前缀含义二进制前缀含义
KB(Kilobyte)$10^3 = 1000$ 字节KiB(Kibibyte)$2^{10} = 1024$ 字节
MB(Megabyte)$10^6$ 字节MiB(Mebibyte)$2^{20}$ 字节
GB(Gigabyte)$10^9$ 字节GiB(Gibibyte)$2^{30}$ 字节
TB(Terabyte)$10^{12}$ 字节TiB(Tebibyte)$2^{40}$ 字节

3.1.3 计量单位换算表

我们常见的十进制数字的计量单位有:个、十、百、千、万、亿,比亿还大的单位有:兆、京、垓、秭、穰、沟、涧、正、载、极。 二进制也有自己的计量单位。位(bit,Binary Digits):存放一位二进制数,即 0 或 1,为最小的存储单位,8 个二进制位为一个字节单位。更大的计量单位有 KB、MB、GB、TB、PB、EB、ZB、YB、BB 等。

二进制计量单位单位换算单位换算(科学计数法)十进制计量单位单位换算单位换算(科学计数法)
b(bit)$10^4$
B1B = 8b(1Byte = 8bit)亿万万$10^8$
KB1KB = 1024B$2^{10}$万亿$10^{12}$
MB1MB = 1024KB$2^{20}$万兆$10^{16}$
GB1GB = 1024MB$2^{30}$万京$10^{20}$
TB1TB = 1024GB$2^{40}$万垓$10^{24}$
PB1PB = 1024TB$2^{50}$万秭$10^{28}$
EB1EB = 1024PB$2^{60}$万穰$10^{32}$
ZB1ZB = 1024EB$2^{70}$万沟$10^{36}$
YB1YB = 1024ZB$2^{80}$万涧$10^{40}$
BB1BB = 1024YB$2^{90}$万正$10^{44}$
NB1NB = 1024BB$2^{100}$万载$10^{48}$

3.2 字节序

3.2.1 什么是字节序

当数据需要占用多个字节时(例如一个 32 位整数占 4 个字节),就涉及到一个关键问题:这些字节在内存中应该如何排列? 这就是**字节序(Endianness)**问题。

假设有一个 16 位整数 0x1234,它由两个字节组成:0x12(高位字节)和 0x34(低位字节)。在内存中存储时,有两种方式:

大端序(Big-Endian)

高位字节存储在低地址,低位字节存储在高地址。类似于人类书写数字的习惯:先写高位。

内存地址:  0x1000   0x1001
          +--------+--------+
          |  0x12  |  0x34  |
          +--------+--------+
           高位字节  低位字节

小端序(Little-Endian)

低位字节存储在低地址,高位字节存储在高地址。

内存地址:  0x1000   0x1001
          +--------+--------+
          |  0x34  |  0x12  |
          +--------+--------+
           低位字节  高位字节

3.2.2 网络字节序

在网络通信中,不同架构的计算机可能使用不同的字节序。为了确保数据能够正确解析,网络协议统一使用大端序作为标准,称为网络字节序(Network Byte Order)

例如,IP 地址 192.168.1.1 在传输时,会按照大端序将 32 位整数 0xC0A80101 发送出去。TCP/IP 协议栈中的 htonl()(Host to Network Long)和 htons()(Host to Network Short)函数就是用于将主机字节序转换为网络字节序。

3.2.3 Rust 中的字节序处理

Rust 标准库为整数类型提供了便捷的字节序转换方法:

方法说明
to_be()转换为大端序字节数组(Big-Endian)
to_le()转换为小端序字节数组(Little-Endian)
to_ne_bytes()转换为本机字节序字节数组(Native Endian)
from_be_bytes()从大端序字节数组解析
from_le_bytes()从小端序字节数组解析
fn main() {
    let num: u32 = 0x12345678;

    // 转换为大端序字节数组
    let be_bytes = num.to_be_bytes();
    println!("大端序: {:02X?}", be_bytes);  // [12, 34, 56, 78]

    // 转换为小端序字节数组
    let le_bytes = num.to_le_bytes();
    println!("小端序: {:02X?}", le_bytes);  // [78, 56, 34, 12]

    // 从字节数组还原
    let restored = u32::from_be_bytes(be_bytes);
    println!("还原: 0x{:08X}", restored);   // 0x12345678
}

3.2.4 检测系统字节序

大多数现代计算机(x86、x86_64、ARM 等)使用小端序,但某些架构(如网络设备、部分嵌入式系统)使用大端序。以下代码可以检测当前系统的字节序:

fn is_little_endian() -> bool {
    let num: u16 = 0x0001;
    let bytes = num.to_ne_bytes();
    bytes[0] == 0x01  // 小端序:低位字节在低地址
}

fn main() {
    if is_little_endian() {
        println!("当前系统使用小端序(Little-Endian)");
    } else {
        println!("当前系统使用大端序(Big-Endian)");
    }
}

3.3 Base64 编码

3.3.1 编码原理

Base64 是一种将二进制数据编码为 ASCII 字符串的方法。它的核心思想是:将每 3 个字节(24 位)的数据拆分为 4 个 6 位的组,每个 6 位组对应一个 Base64 字符

编码过程如下:

$$\text{3 字节} \rightarrow \text{24 位} \rightarrow \text{4 个 6 位组} \rightarrow \text{4 个 Base64 字符}$$

Base64 字符集包含 64 个字符($2^6 = 64$):

  • A-Z(26 个)
  • a-z(26 个)
  • 0-9(10 个)
  • +/(2 个)

填充规则:当数据长度不是 3 的倍数时,使用 = 进行填充:

原始字节数剩余位数输出字符数填充
304
21631 个 =
1822 个 =

例如,编码字符串 "Man"

M        a        n
01001101 01100001 01101110
|  6位  ||  6位  ||  6位  ||  6位  |
010011   010110   000101   101110
   T        W        F        u

因此 "Man" 的 Base64 编码为 "TWFu"

3.3.2 URL 安全变体

标准的 Base64 编码包含 +/ 字符,这在 URL 中需要进行转义。因此出现了 URL 安全 Base64(Base64URL),将 + 替换为 -,将 / 替换为 _,并通常省略填充符 =

变体字符集填充适用场景
标准 Base64A-Z a-z 0-9 + /=一般文本传输
URL 安全 Base64A-Z a-z 0-9 - _无/可选URL、文件名

3.3.3 Shell 命令示例

任何二进制文件都可以使用 Base64 进行编码,比如 txt 文本、图片。

# 编码文件
openssl enc -base64 -in 生僻字.txt -out 生僻字.base64.txt

# 解码文件
openssl enc -base64 -d -in 生僻字.base64.txt -out 生僻字.demo.txt

# 编码字符串
echo "龙行龘龘(dá dá)前程朤朤(lǎng lǎng)生活䲜䲜(yè yè)健康𣊫𣊫(liù liù)财运𨰻𨰻(bǎo bǎo)🔥" | openssl enc -base64

# 解码字符串
echo "6b6Z6KGM6b6Y6b6Y77yIZMOhIGTDoe+8ieWJjeeoi+acpOacpO+8iGzHjm5nIGzH
jm5n77yJ55Sf5rS75LKc5LKc77yIecOoIHnDqO+8ieWBpeW6t/Cjiqvwo4qr77yI
bGnDuSBsacO577yJ6LSi6L+Q8Kiwu/CosLvvvIhix45vIGLHjm/vvIkK" | openssl enc -base64 -d

3.3.4 Rust 实现

#![allow(unused)]
fn main() {
#[test]
fn base64_demo() {
    let hello = b"hello rustaceans00";
    let encoded = general_purpose::STANDARD.encode(hello);
    let decoded = general_purpose::STANDARD.decode(&encoded).unwrap();

    println!("origin: {}", str::from_utf8(hello).unwrap());
    println!("base64 encoded: {}", encoded);
    println!("back to origin: {}", str::from_utf8(&decoded).unwrap());

    // &[u8;T]/String <= Vec<u8> => Base64 String
    let common_base64_str = general_purpose::STANDARD.encode("中文@123&");
    println!("common base64 string: {}", common_base64_str);
    let common_vecu8 = general_purpose::STANDARD
        .decode(&common_base64_str)
        .unwrap();
    println!("common string: {}", str::from_utf8(&common_vecu8).unwrap());

    println!("道路安全千万条,安全第一条");

    use base64::{
        alphabet,
        engine::{self, general_purpose},
        Engine as _,
    };

    let reform = "商鞅变法、戊戌变法、改革开放";
    let b64 = general_purpose::STANDARD.encode(reform.as_bytes());
    println!("{}", b64);

    const CUSTOM_ENGINE: engine::GeneralPurpose =
        engine::GeneralPurpose::new(&alphabet::URL_SAFE, general_purpose::NO_PAD);

    let b64_url = CUSTOM_ENGINE.encode(b"hello internet~");
    println!("{}", b64_url);

    let url = String::from("忘记过去不但意味着背叛,意味着将来可能还要重来");
    let lesson = general_purpose::URL_SAFE.encode(url.as_bytes());
    println!("{}", lesson);

    // 图片 Base64
    let read_result = fs::read("examples/file/SeaORM_banner.png");
    if let Some(picture_bytes) = read_result.ok() {
        let b64 = general_purpose::STANDARD.encode(&picture_bytes);
        println!(
            "picture_bytes len:{} ,base64 len:{}",
            picture_bytes.len(),
            b64.len()
        );
        // 输出一行字符串
        fs::write("examples/file/SeaORM_banner.base64.txt", b64.as_bytes());

        // 76 个字符添加一个换行符
        // shell: fold -w 76 SeaORM_banner.base64.txt > SeaORM_banner.base64.3.txt
        if let Ok(file) = File::create("examples/file/SeaORM_banner.base64.3.txt") {
            let mut writer = BufWriter::new(file);
            let mut chars = b64.chars();
            for line in &chars.chunks(76usize) {
                // println!("{}",line.collect::<String>());
                // fs::write("examples/file/SeaORM_banner.base64.3.txt",line.collect::<String>());
                let mut bytes = line.collect::<String>();

                bytes.push_str("\n");
                let count = writer.write(bytes.as_bytes());
                //writer.write(b"\n");
            }

            writer.flush();
        }
    }
}
}

3.4 Hex 编码

3.4.1 编码原理

Hex(Hexadecimal,十六进制)编码是将二进制数据转换为十六进制字符串表示的方法。每个字节(8 位)恰好对应两个十六进制字符:

$$\text{1 字节} \rightarrow \text{8 位} \rightarrow \text{2 个十六进制字符}$$

十六进制字符集为 0-9A-F(或 a-f),共 16 个字符。由于 $16 = 2^4$,每个十六进制字符恰好表示 4 位二进制数据。

例如,字节 0x4D(二进制 01001101,即 ASCII 字符 'M')的 Hex 编码为 "4D"

Hex 编码的特点:

  • 可读性好:比纯二进制更易于人工阅读和调试
  • 无歧义:每个字节固定对应 2 个字符,无需填充
  • 体积膨胀:编码后数据大小变为原来的 2 倍
  • 常用场景:调试输出、哈希值表示、内存转储、颜色代码

3.4.2 Rust 实现

#![allow(unused)]
fn main() {
use data_encoding::{HEXUPPER, DecodeError};

// 编码和解码十六进制
let original = b"The quick brown fox jumps over the lazy dog.";
let expected = "54686520717569636B2062726F776E20666F78206A756D7073206F76\
    657220746865206C617A7920646F672E";

let encoded = HEXUPPER.encode(original);
assert_eq!(encoded, expected);

let decoded = HEXUPPER.decode(&encoded.into_bytes()).unwrap();
assert_eq!(&decoded[..], &original[..]);
println!("{}", str::from_utf8(&decoded).unwrap());

let cn = HEXUPPER.encode("功成不必在我".as_bytes());
println!("{}", cn);
}

3.5 内存映射

3.5.1 什么是内存映射

内存映射(Memory Mapping,MMAP)是一种将文件内容直接映射到进程虚拟地址空间的技术。通过内存映射,程序可以像访问内存一样访问文件内容,避免了传统 I/O 中的多次数据拷贝,实现了**零拷贝(Zero-Copy)**效果。

内存映射的优势:

  • 减少数据拷贝:数据直接从磁盘加载到用户空间,无需经过内核缓冲区的额外拷贝
  • 延迟加载:只有实际访问的页面才会从磁盘加载,适合处理大文件
  • 进程间共享:多个进程可以映射同一个文件,实现高效的进程间通信
  • 简化编程:文件操作转化为内存操作,代码更简洁

3.5.2 Rust 中的 MMAP

#![allow(unused)]
fn main() {
#[test]
fn mmap_mut() -> Result<(), Error> {
    use memmap::MmapMut;
    use std::fs::OpenOptions;
    use std::io::Write;
    use std::ops::DerefMut;
    use std::path::PathBuf;

    // let file = File::options().write(true).open("abcd.txt")?;

    let path: PathBuf = PathBuf::from("abcd.txt");
    let file = OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .open(&path)?;
    file.set_len(30)?;

    let mut mmap = unsafe { MmapMut::map_mut(&file)? };

    // mmap.copy_from_slice(b"Hello, world!");
    (&mut mmap[..]).write_all(b"Hello BeiJing! Hello, world!")?;
    mmap.flush()?;

    Ok(())
}
}
  • mmap-sync Rust library for concurrent data access, using memory-mapped files, zero-copy deserialization, and wait-free synchronization.

3.6 总结

3.6.1 编码方式对比

特性Base64Hex原始二进制
编码后体积原始数据的 4/3(约 133%)原始数据的 2 倍(200%)100%
字符集大小6416256
可读性中等差(不可直接阅读)
是否需要填充是(=
适用场景邮件附件、URL、嵌入图片调试、哈希、颜色值存储、传输
URL 安全需使用 URL 安全变体

3.6.2 关键概念汇总

概念说明
$1 \text{ Byte} = 8 \text{ bits}$字节与位的基本关系
大端序高位字节存低地址,网络标准
小端序低位字节存低地址,x86/x64 架构
网络字节序统一使用大端序
Base643 字节 $\rightarrow$ 4 字符,6 位一组
Hex1 字节 $\rightarrow$ 2 字符,4 位一组
MMAP文件映射到内存,零拷贝

3.7 练习题

  1. 基础题:编写 Rust 程序,将字符串 "Rust" 分别进行 Base64 编码和 Hex 编码,输出编码结果并验证解码后是否还原。

  2. 字节序实践:定义一个 u32 变量 0xDEADBEEF,分别输出其大端序和小端序的字节表示,并用 from_be_bytesfrom_le_bytes 还原验证。

  3. 字节序检测:编写一个函数 detect_endianness(),返回当前系统的字节序(大端或小端),并在 main 函数中打印结果。

  4. Base64 填充分析:分别对 1 字节、2 字节、3 字节的数据进行 Base64 编码,观察输出结果中 = 填充符的数量,验证填充规则。

  5. URL 安全 Base64:使用 base64 crate 的 URL_SAFE 引擎,对一个包含中文的字符串进行编码,验证输出中不含 +/ 字符。

  6. Hex 编码实现:不使用任何第三方库,手动实现一个函数 fn bytes_to_hex(input: &[u8]) -> String,将字节数组转换为 Hex 字符串。

  7. 内存映射读取:使用 memmap crate 编写程序,将一个文本文件映射到内存,读取其内容并输出前 100 个字符。

  8. 思考题:为什么网络协议统一使用大端序?如果发送方和接收方使用不同的字节序,会发生什么问题?请结合 TCP/IP 协议栈说明。

第四 时间

时间是什么?不同的人就会有不同的回答。古人根据日月星辰的运行规律,制定了历法。中国的干支纪年,西方的公元纪年。再到近代的格林尼治标准时间(Greenwich Mean Time,GMT),直到现在基于原子钟的世界标准时间(世界协调时 UTC),以及计算机领域著名的UNIX时间

在计算机系统中,时间是程序运行、数据记录、网络通信的基础。从日志时间戳到定时任务,从缓存过期到分布式一致性,时间的准确性和一致性至关重要。本章将从计算机时间的本质出发,深入剖析时间戳、日期格式化、时区、时间间隔等核心概念,并通过 Rust 代码实现时间的获取、转换和运算。

4.1 计算机时间概述

4.1.1 时间的历史

人类对时间的计量经历了漫长的演变。古代人们使用圭表、日晷、滴漏等工具计时;近代发展出机械钟表;现代则使用原子钟计时,并可以通过无线电波、卫星、互联网等手段进行授时。

4.1.2 原子钟与 UTC

1967 年,国际计量大会将秒的定义改为:铯-133 原子基态的两个超精细能级之间跃迁所对应的辐射的 9,192,631,770 个周期的持续时间。这一定义基于原子钟,其精度可达每数百万年误差不超过 1 秒。

UTC(Coordinated Universal Time,协调世界时)是目前全球最通用的时间标准。它结合了:

  • TAI(International Atomic Time,国际原子时):基于全球约 400 台原子钟的加权平均,是连续均匀的时间尺度。
  • UT1(Universal Time 1):基于地球自转,与天文观测相关。

UTC 通过在 TAI 基础上插入闰秒来保持与 UT1 的偏差不超过 0.9 秒。

4.1.3 UNIX 时间戳原理

UNIX 时间戳(Unix Timestamp)是计算机系统中最常用的时间表示方式,定义为自 1970-01-01 00:00:00 UTC 起经过的秒数(或毫秒数、微秒数、纳秒数)。

选择 1970 年作为纪元(Epoch)的原因是 UNIX 操作系统诞生于 1969 年,1970-01-01 是 UNIX 的“生日“。

时间戳的核心优势在于:

  • 无歧义:不受时区、夏令时、日期格式影响
  • 便于计算:两个时间戳的差值即为时间间隔
  • 存储高效:一个整数即可表示任意时刻

4.1.4 闰秒问题

由于地球自转速度不均匀(潮汐摩擦等因素导致逐渐变慢),UTC 需要通过插入闰秒来与地球自转保持一致。闰秒的插入规则是:

  • 当 UTC 与 UT1 的偏差接近 0.9 秒时,在 6 月 30 日或 12 月 31 日的最后一分钟插入一个额外的第 61 秒
  • 闰秒可以正(增加一秒)也可以负(减少一秒),但历史上只出现过正闰秒

闰秒给计算机系统带来了挑战:

  • 一分钟可能有 61 秒或 59 秒
  • 时间戳在闰秒期间可能出现倒退或重复
  • 部分系统采用“闰秒抹平“(Leap Second Smearing)策略,将闰秒分散到多个小时中

时间戳转换工具

4.2 时间戳

4.2.1 时间戳的精度

根据精度不同,时间戳可分为多个级别:

精度级别单位示例(2024-01-01 00:00:00 UTC)
秒级1704067200
毫秒级毫秒1704067200000
微秒级微秒1704067200000000
纳秒级纳秒1704067200000000000

秒级时间戳在 32 位有符号整数中的最大值为 $2^{31} - 1 = 2147483647$,对应 2038-01-19 03:14:07 UTC,这就是著名的 Y2K38 问题(2038 年问题)。使用 64 位整数可以表示到约 2920 亿年后,彻底解决这个问题。

4.2.2 Rust 获取时间戳

use chrono::Utc;

fn main() {
    // 秒级时间戳
    println!("Utc timestamp: {}", Utc::now().timestamp());
    
    // 毫秒级时间戳
    println!("Utc timestamp_millis: {}", Utc::now().timestamp_millis());
    
    // 微秒级时间戳
    println!("Utc timestamp_micros: {}", Utc::now().timestamp_micros());
    
    // 纳秒级时间戳
    println!("Utc timestamp_nanos: {}", Utc::now().timestamp_nanos_opt().unwrap_or(0));
}

4.3 日期格式化

4.3.1 ISO 8601 标准

ISO 8601 是国际标准化组织制定的日期和时间表示标准,旨在消除不同国家和文化之间的日期表示歧义。

基本格式

$$\text{YYYY-MM-DDTHH:MM:SS±HH:MM}$$

其中:

  • YYYY:四位年份
  • MM:两位月份(01-12)
  • DD:两位日期(01-31)
  • T:日期和时间的分隔符
  • HH:MM:SS:时:分:秒
  • ±HH:MM:与 UTC 的时区偏移

示例

  • 2024-01-15T09:30:00+08:00(北京时间)
  • 2024-01-15T01:30:00Z(UTC 时间,Z 表示零时区)

4.3.2 RFC 2822 与 RFC 3339

RFC 2822 是电子邮件中使用的日期格式:

Fri, 28 Nov 2014 12:00:09 +0000

RFC 3339 是互联网协议中广泛使用的日期格式,基于 ISO 8601 但做了一些限制:

2014-11-28T12:00:09+00:00

两者的主要区别:

特性RFC 2822RFC 3339
来源电子邮件标准互联网标准
星期必须包含不包含
时区格式+0000 或 GMT+00:00 或 Z
使用场景邮件头、HTTP DateJSON、API、配置文件

4.3.3 Rust chrono 格式化字符串

chrono crate 提供了丰富的格式化选项:

占位符含义示例
%Y四位年份2024
%m两位月份01
%d两位日期15
%H24小时制小时09
%M分钟30
%S00
%f微秒(6位)000123
%z时区偏移+0800
%Z时区名称CST

4.3.4 日期与字符串相互转换

use chrono::{Utc, NaiveDateTime, TimeZone, NaiveDate};

pub fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. DateTime -> &str (格式化输出日期)
    let dt = Utc.with_ymd_and_hms(2014, 11, 28, 12, 0, 9).unwrap();
    assert_eq!(
        dt.format("%Y-%m-%d %H:%M:%S").to_string(),
        "2014-11-28 12:00:09"
    );

    assert_eq!(dt.to_string(), "2014-11-28 12:00:09 UTC");
    assert_eq!(dt.to_rfc2822(), "Fri, 28 Nov 2014 12:00:09 +0000");
    assert_eq!(dt.to_rfc3339(), "2014-11-28T12:00:09+00:00");
    assert_eq!(format!("{:?}", dt), "2014-11-28T12:00:09Z");

    // 2. &str -> DateTime
    let no_timezone = NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S");
    println!("{:?}", no_timezone);

    assert_eq!(
        NaiveDateTime::parse_from_str("2014-5-17T12:34:56+09:30", "%Y-%m-%dT%H:%M:%S%z"),
        Ok(NaiveDate::from_ymd_opt(2014, 5, 17).unwrap().and_hms_opt(12, 34, 56).unwrap())
    );
    
    Ok(())
}

4.4 时区

4.4.1 时区概念

地球自转导致不同经度地区看到太阳的位置不同,因此将地球划分为 24 个时区,每个时区跨 15 度经度。时区用 UTC 偏移量表示:

$$\text{本地时间} = \text{UTC} + \text{时区偏移}$$

常见时区:

UTC+5:30   印度新德里时间       (东5.5区时间)
UTC+8      北京时间             (东八区时间)
UTC+9      东京时间             (东九区时间)
UTC-5      东部时间(EST)      (西五区时间)
UTC-8      太平洋标准时区(PST)(西八区时间)

4.4.2 夏令时

夏令时(Daylight Saving Time,DST)是一种为节约能源而人为调整时间的制度。在夏季将时钟拨快一小时,以充分利用日光。

  • 并非所有国家/地区都使用夏令时
  • 中国自 1991 年起不再实行夏令时
  • 美国、欧洲大部分国家仍在使用
  • 夏令时的开始和结束日期每年可能不同,给程序处理带来复杂性

4.4.3 Rust 时区处理

use chrono::{Local, DateTime, Utc, FixedOffset, NaiveDate, TimeZone};
use chrono::{Datelike, Timelike};

fn main() {
    // ❌ 旧方法(已弃用)
    // let utc_time = DateTime::<Utc>::from_utc(local_time.naive_utc(), Utc);
    
    // ✅ 新方法:使用 Utc.from_utc_datetime()
    let local_time = Local::now();
    let utc_time = Utc.from_utc_datetime(&local_time.naive_utc());
    
    // 定义时区(注意:FixedOffset::east() 已弃用,使用 east_opt() 或 from_hours())
    let new_delhi_timezone = FixedOffset::east_opt(5 * 3600 + 1800).unwrap();
    let china_timezone = FixedOffset::east_opt(8 * 3600).unwrap();
    let japan_timezone = FixedOffset::east_opt(9 * 3600).unwrap();
    let rio_timezone = FixedOffset::west_opt(2 * 3600).unwrap();
    let est_timezone = FixedOffset::west_opt(5 * 3600).unwrap();
    let pst_timezone = FixedOffset::west_opt(8 * 3600).unwrap();

    println!("Local time now is {}", local_time);
    println!("UTC time now is {}", utc_time);

    // UTC --> 各时区时间
    println!("Time in Beijing now is {}", utc_time.with_timezone(&china_timezone));
    println!("Time in Tokyo now is {}", utc_time.with_timezone(&japan_timezone));
    println!("Time in Rio de Janeiro now is {}", utc_time.with_timezone(&rio_timezone));
    println!("Time in New Delhi now is {}", utc_time.with_timezone(&new_delhi_timezone));
    println!("Time in EST now is {}", utc_time.with_timezone(&est_timezone));
    println!("Time in PST now is {}", utc_time.with_timezone(&pst_timezone));
    
    // 格式化输出
    let local: DateTime<Local> = Local::now();
    println!("{}", local.format("%Y-%m-%d %H:%M:%S"));
    println!("当前本地时间: {}", local);

    // NaiveDateTime 构建(需要 NaiveDate 类型)
    let local_time_now = Local::now();
    let dt = NaiveDate::from_ymd_opt(
        local_time_now.year(), 
        local_time_now.month(), 
        local_time_now.day()
    )
    .unwrap()
    .and_hms_opt(
        local_time_now.hour(), 
        local_time_now.minute(), 
        local_time_now.second()
    )
    .unwrap();
    
    println!("NaiveDateTime: {}", dt);
}

4.5 时间间隔 Duration

4.5.1 Duration 概念

Duration 表示两个时间点之间的间隔。在 Rust 的 chrono 库中,Duration 可以表示正负的时间长度,支持多种时间单位:

方法说明
Duration::days(n)n 天
Duration::hours(n)n 小时
Duration::minutes(n)n 分钟
Duration::seconds(n)n 秒
Duration::milliseconds(n)n 毫秒
Duration::microseconds(n)n 微秒
Duration::nanoseconds(n)n 纳秒
Duration::weeks(n)n 周

4.5.2 时间运算

use chrono::{Duration, DateTime, Utc, TimeZone, Local};

fn day_earlier(date_time: DateTime<Utc>) -> Option<DateTime<Utc>> {
    date_time.checked_sub_signed(Duration::days(1))
}

fn main() {
    let now = Utc::now();
    
    let almost_three_weeks_from_now = now
        .checked_add_signed(Duration::weeks(2))
        .and_then(|in_2weeks| in_2weeks.checked_add_signed(Duration::weeks(1)))
        .and_then(day_earlier);

    match almost_three_weeks_from_now {
        Some(x) => println!("三周前一天: {}", x),
        None => eprintln!("Almost three weeks from now overflows!"),
    }

    // ✅ 新方法:使用 with_ymd_and_hms()
    let today = Utc::now();
    let now = Local::now();
    let founding_date = Utc.with_ymd_and_hms(1949, 10, 1, 0, 0, 0).unwrap();
    
    let years_passed = now.signed_duration_since(founding_date).num_days() / 365;
    println!("今天{},建国{}年", now, years_passed);
    
    // 时间运算示例
    let tomorrow = now.checked_add_signed(Duration::days(1)).unwrap();
    let last_week = now.checked_sub_signed(Duration::weeks(1)).unwrap();
    let ten_minutes_later = now.checked_add_signed(Duration::minutes(10)).unwrap();
    
    println!("明天: {}", tomorrow);
    println!("上周: {}", last_week);
    println!("十分钟后: {}", ten_minutes_later);
    
    // 计算两个时间点的间隔
    let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
    let end = Utc.with_ymd_and_hms(2024, 6, 20, 12, 0, 0).unwrap();
    let diff = end.signed_duration_since(start);
    
    println!("从年初到今天的间隔: {} 天", diff.num_days());
    println!("从年初到今天的间隔: {} 小时", diff.num_hours());
    println!("从年初到今天的间隔: {} 秒", diff.num_seconds());
}

更多详细代码

4.6 时间标准

4.6.1 NTP 协议

NTP(Network Time Protocol,网络时间协议)是用于同步计算机系统时钟的协议。它通过分层的时间服务器架构(Stratum 0-15)将时间从原子钟传播到互联网上的各种设备。

  • Stratum 0:原子钟、GPS 等高精度时间源
  • Stratum 1:直接与 Stratum 0 连接的时间服务器
  • Stratum 2:从 Stratum 1 获取时间的服务器

NTP 使用 UDP 端口 123,精度可达毫秒级。SNTP(Simple NTP)是 NTP 的简化版本,适用于对精度要求不高的场景。

4.6.2 日期与时间标准

4.7 总结

时间相关概念对比

概念说明示例
时间戳自 1970-01-01 00:00:00 UTC 起的秒数1704067200
UTC协调世界时,基于原子钟的全球标准时间2024-01-01T00:00:00Z
GMT格林尼治标准时间,基于地球自转(已逐渐被 UTC 取代)Sun, 01 Jan 2024 00:00:00 GMT
本地时间UTC + 时区偏移2024-01-01T08:00:00+08:00
ISO 8601国际日期时间表示标准2024-01-01T00:00:00+00:00
RFC 3339互联网日期时间格式(基于 ISO 8601)2024-01-01T00:00:00Z
RFC 2822电子邮件日期时间格式Mon, 01 Jan 2024 00:00:00 +0000
Duration两个时间点之间的间隔Duration::days(7)
闰秒为保持 UTC 与地球自转同步而插入的额外秒2016-12-31 23:59:60
夏令时夏季将时钟拨快一小时的制度UTC-5 → UTC-4

关键公式汇总

公式说明
$\text{本地时间} = \text{UTC} + \text{时区偏移}$时区转换
$\text{时间间隔} = t_2 - t_1$Duration 计算
$\text{Y2K38 临界点} = 2^{31} - 1 = 2147483647$32 位时间戳最大值

4.8 练习题

  1. 基础题:使用 chrono 库获取当前时间的秒级、毫秒级和纳秒级时间戳,并输出到控制台。

  2. 格式转换:编写一个 Rust 程序,将当前 UTC 时间分别格式化为 ISO 8601、RFC 2822 和 RFC 3339 格式,并输出对比。

  3. 时区转换:给定一个 UTC 时间字符串 "2024-06-20T12:00:00Z",将其转换为北京时间(UTC+8)、东京时间(UTC+9)和纽约时间(UTC-5)的本地时间。

  4. 时间运算:计算从 1949 年 10 月 1 日 00:00:00 UTC 到当前时间经过了多少天、多少小时、多少分钟。

  5. 闰年判断:编写一个函数判断给定年份是否为闰年。闰年规则:能被 4 整除但不能被 100 整除,或者能被 400 整除。

  6. 倒计时程序:编写一个 Rust 程序,计算距离 2038-01-19 03:14:07 UTC(Y2K38 问题发生时刻)还剩多少天、小时、分钟和秒。

  7. 时间解析:编写一个函数,解析各种格式的日期字符串(如 "2024-01-15""15/01/2024""Jan 15, 2024"),统一返回 DateTime<Utc> 类型。

  8. 思考题:为什么计算机系统使用时间戳而不是人类可读的日期字符串来存储时间?时间戳有哪些优势和局限性?

第五 图片

随着互联网、社交媒体、电子商务的发展,图片作为一个重要的信息载体,被广泛应用。“一图胜过千言万语”、“有图有真相”、“发个朋友圈”。也催生了美颜、马赛克、PS(Photoshop)等相关技术。

本章将从图片的本质出发,深入剖析像素编码原理、各种图片格式的底层机制,并通过 Rust 代码实现图片的读取、处理、转换等操作。

5.1 图片的本质

5.1.1 像素

图片(Image)本质上是一个二维的像素矩阵。像素(Pixel,Picture Element 的缩写)是构成数字图像的最小单元。每个像素携带颜色信息,所有像素按照行列排列就构成了一幅完整的图像。

一张分辨率为 $W \times H$ 的图片,共有 $W \times H$ 个像素。例如,一张 $1920 \times 1080$ 的 Full HD 图片共有 $2,073,600$ 个像素。

5.1.2 分辨率

分辨率(Resolution)描述了图像中包含的像素数量,通常用“宽 x 高“表示。常见的分辨率标准:

名称分辨率像素总数应用场景
QQVGA160 x 12019,200早期手机
VGA640 x 480307,200早期摄像头
HD1280 x 720921,600高清视频
Full HD1920 x 10802,073,600主流显示器
2K (QHD)2560 x 14403,686,400高端显示器
4K (UHD)3840 x 21608,294,400专业显示器/电视
8K (UHD-2)7680 x 432033,177,600广播级制作

5.1.3 颜色空间

颜色空间(Color Space)定义了如何用数值来表示颜色。不同的颜色空间有不同的应用场景。

RGB 颜色空间

RGB(Red, Green, Blue)是最常用的加色模型,用于显示器、手机屏幕等发光设备。每个通道取值范围为 $[0, 255]$(8位),共可表示 $256^3 = 16,777,216$ 种颜色。

一个像素的 RGB 值可以表示为三元组 $(R, G, B)$,其中:

$$C = R \cdot (1, 0, 0) + G \cdot (0, 1, 0) + B \cdot (0, 0, 1)$$

CMYK 颜色空间

CMYK(Cyan, Magenta, Yellow, Key/Black)是减色模型,用于印刷行业。RGB 和 CMYK 之间的转换关系:

$$K = 1 - \max(R, G, B)$$ $$C = \frac{1 - R - K}{1 - K}$$ $$M = \frac{1 - G - K}{1 - K}$$ $$Y = \frac{1 - B - K}{1 - K}$$

YUV 颜色空间

YUV 将亮度(Y)和色度(U、V)分离,广泛用于视频压缩和传输。人眼对亮度变化更敏感,因此可以对色度分量进行更大的压缩。RGB 到 YUV 的转换(BT.601 标准):

$$Y = 0.299R + 0.587G + 0.114B$$ $$U = -0.147R - 0.289G + 0.436B$$ $$V = 0.615R - 0.515G - 0.100B$$

HSV 颜色空间

HSV(Hue, Saturation, Value)更符合人类对颜色的直觉感知:

  • H(色相):颜色类型,取值 $[0, 360)$
  • S(饱和度):颜色的纯度,取值 $[0, 1]$
  • V(明度):颜色的明暗,取值 $[0, 1]$
#![allow(unused)]
fn main() {
/// RGB 转 HSV
fn rgb_to_hsv(r: u8, g: u8, b: u8) -> (f64, f64, f64) {
    let r = r as f64 / 255.0;
    let g = g as f64 / 255.0;
    let b = b as f64 / 255.0;

    let max = r.max(g).max(b);
    let min = r.min(g).min(b);
    let delta = max - min;

    // 计算色相 H
    let h = if delta == 0.0 {
        0.0
    } else if max == r {
        60.0 * (((g - b) / delta) % 6.0)
    } else if max == g {
        60.0 * (((b - r) / delta) + 2.0)
    } else {
        60.0 * (((r - g) / delta) + 4.0)
    };

    // 计算饱和度 S
    let s = if max == 0.0 { 0.0 } else { delta / max };

    (h, s, max)
}
}

5.2 像素编码原理

5.2.1 位深

位深(Bit Depth)决定了每个像素可以表示的颜色数量。位深越大,颜色越丰富,但文件也越大。

位深每通道位数每像素位数(RGB)可表示颜色数应用场景
1-bit138黑白图像
8-bit82416,777,216标准图像
16-bit1648$2.81 \times 10^{14}$专业摄影
32-bit32 (float)96极大HDR、医学成像

一张 $1920 \times 1080$ 的图片,在 24 位色深下的原始数据大小为:

$$1920 \times 1080 \times 24 \text{ bits} = 49,766,400 \text{ bits} \approx 5.93 \text{ MB}$$

5.2.2 Alpha 通道

Alpha 通道用于表示像素的透明度,取值范围 $[0, 255]$,其中 0 表示完全透明,255 表示完全不透明。带有 Alpha 通道的图像称为 RGBA 图像。

RGBA 像素在内存中的排列方式通常为:

字节 0: R (红色)
字节 1: G (绿色)
字节 2: B (蓝色)
字节 3: A (透明度)

Alpha 混合公式(将前景色与背景色混合):

$$C_{out} = \alpha \cdot C_{fg} + (1 - \alpha) \cdot C_{bg}$$

其中 $\alpha$ 为归一化后的透明度值 $[0, 1]$。

5.3 图片格式详解

5.3.1 BMP(Bitmap)

BMP 是 Windows 系统的标准图像格式,采用无压缩方式存储像素数据,文件体积较大但解码简单。

文件结构

┌─────────────────────┐
│ BMP 文件头 (14字节)   │  ← 文件类型、文件大小、偏移量
├─────────────────────┤
│ DIB 信息头 (40字节)   │  ← 宽度、高度、位深、压缩方式
├─────────────────────┤
│ 调色板 (可选)         │  ← 仅用于低位深图像
├─────────────────────┤
│ 像素数据              │  ← 按行存储,每行4字节对齐
└─────────────────────┘

BMP 的像素数据是从下到上存储的(即图像的最后一行在文件的最前面),且每行数据需要按 4 字节对齐。例如,宽度为 3 的 24 位 BMP 图像,每行需要 $3 \times 3 = 9$ 字节,对齐后为 12 字节(补 3 字节填充)。

5.3.2 JPEG(Joint Photographic Experts Group)

JPEG 是网络上最流行的有损压缩图像格式,在保留较高图像质量的同时拥有很高的压缩比。

压缩流程

原始图像 → 颜色空间转换(YCbCr) → 分块(8x8) → DCT变换 → 量化 → 熵编码 → JPEG文件

离散余弦变换(DCT)

JPEG 将图像分割为 $8 \times 8$ 的像素块,对每个块进行二维 DCT 变换:

$$F(u,v) = \frac{1}{4}C(u)C(v)\sum_{x=0}^{7}\sum_{y=0}^{7}f(x,y)\cos\frac{(2x+1)u\pi}{16}\cos\frac{(2y+1)v\pi}{16}$$

其中:

$$C(k) = \begin{cases} \frac{1}{\sqrt{2}} & k=0 \ 1 & k>0 \end{cases}$$

DCT 将空间域的像素值转换为频率域的系数,低频系数集中在左上角(代表图像的大致轮廓),高频系数在右下角(代表细节)。

量化

量化是 JPEG 有损压缩的核心步骤。通过量化表将 DCT 系数除以对应的量化步长并取整:

$$F_Q(u,v) = \text{round}\left(\frac{F(u,v)}{Q(u,v)}\right)$$

量化步长越大,压缩比越高,但图像质量损失也越大。JPEG 标准定义了质量参数(1-100),通过缩放默认量化表来控制压缩质量。

熵编码

量化后的系数经过 Zigzag 扫描(将二维系数排列为一维序列,从低频到高频),然后使用 Huffman 编码或算术编码进行压缩。

5.3.3 PNG(Portable Network Graphics)

PNG 是一种无损压缩的位图格式,支持 Alpha 通道,广泛用于网页和图形设计。

压缩算法:LZ77 + Huffman

PNG 使用 DEFLATE 压缩算法,该算法结合了两种技术:

  1. LZ77:使用滑动窗口查找重复的字符串模式,用“距离-长度“对替换重复数据
  2. Huffman 编码:对出现频率不同的符号使用不同长度的编码,高频符号用短编码

PNG 文件结构

┌─────────────────────┐
│ PNG 签名 (8字节)      │  固定值: 89 50 4E 47 0D 0A 1A 0A
├─────────────────────┤
│ IHDR 数据块           │  宽度、高度、位深、颜色类型、压缩方法等
├─────────────────────┤
│ 可选数据块             │  PLTE(调色板)、tEXt(文本)、gAMA(伽马)等
├─────────────────────┤
│ IDAT 数据块           │  压缩后的图像数据(可多个)
├─────────────────────┤
│ IEND 数据块           │  文件结束标记
└─────────────────────┘

隔行扫描(Adam7)

PNG 支持 Adam7 隔行扫描,将图像分为 7 个通道(pass),按特定模式逐步加载:

Pass 1: █ . . █ . . █ . .   (每8行每8列)
Pass 2: . █ . . █ . . █ .   (每8行每4列)
Pass 3: █ █ . █ █ . █ █ .   (每4行每4列)
Pass 4: . . █ . . █ . . █   (每4行每2列)
Pass 5: █ . █ █ . █ █ . █   (每2行每2列)
Pass 6: . █ █ . █ █ . █ █   (每2行每1列)
Pass 7: █ █ █ █ █ █ █ █ █   (每1行每1列)

这使得 PNG 在网络传输时可以先显示低分辨率预览,再逐步加载完整图像。

5.3.4 WebP

WebP 是 Google 于 2010 年推出的现代图像格式,旨在替代 JPEG 和 PNG。

特点

特性WebP 有损WebP 无损
压缩算法VP8 (基于块预测 + DCT)LZ77 + Huffman + 颜色缓存
Alpha 通道支持支持
相比 JPEG文件小 25-35%-
相比 PNG-文件小 26%
动画支持 (WebP Animation)-
浏览器支持主流浏览器均支持主流浏览器均支持

有损 WebP 编码流程

原始图像 → 预测(帧内预测) → 变换(DCT/WHT) → 量化 → 算术编码 → WebP文件

WebP 有损模式使用 VP8 视频编码的帧内预测技术,支持 4x4 到 16x16 的宏块划分,比 JPEG 的固定 8x8 分块更灵活。

5.3.5 GIF(Graphics Interchange Format)

GIF 由 CompuServe 于 1987 年发布,是互联网早期最流行的图像格式之一。

特点

  • LZW 压缩:使用 Lempel-Ziv-Welch 无损压缩算法
  • 256 色限制:每个 GIF 最多包含 256 种颜色(8 位索引色),通过调色板实现
  • 支持动画:通过将多帧图像存储在同一个文件中,配合帧延迟时间实现动画效果
  • 支持透明:可以指定一种颜色为透明色

GIF 动画原理

┌─────────────────────┐
│ GIF 文件头            │  GIF87a 或 GIF89a
├─────────────────────┤
│ 逻辑屏幕描述符         │  画布尺寸、全局调色板信息
├─────────────────────┤
│ 全局颜色表 (可选)      │  最多 256 种颜色
├─────────────────────┤
│ 图形控制扩展           │  帧延迟时间、透明色索引
├─────────────────────┤
│ 图像描述符 + 像素数据   │  第 1 帧
├─────────────────────┤
│ 图形控制扩展           │  帧延迟时间
├─────────────────────┤
│ 图像描述符 + 像素数据   │  第 2 帧
├─────────────────────┤
│ ... (更多帧)          │
├─────────────────────┤
│ GIF 结尾标记 (0x3B)    │
└─────────────────────┘

5.3.6 SVG(Scalable Vector Graphics)

SVG 是基于 XML 的矢量图像格式,与位图(BMP、JPEG、PNG 等)有本质区别。

位图 vs 矢量图

特性位图 (Raster)矢量图 (Vector)
存储方式像素矩阵数学公式描述
缩放放大后失真(锯齿)无限缩放不失真
文件大小与分辨率正相关与复杂度相关
适用场景照片、复杂图像图标、Logo、图表
编辑方式像素级操作修改路径和属性

SVG 示例

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
  <!-- 红色圆形 -->
  <circle cx="100" cy="100" r="80" fill="red" stroke="black" stroke-width="2"/>
  <!-- 蓝色矩形 -->
  <rect x="20" y="20" width="60" height="40" fill="blue" rx="5"/>
  <!-- 文本 -->
  <text x="100" y="190" text-anchor="middle" font-size="16">Rust Logo</text>
</svg>

SVG 支持路径(path)、变换(transform)、渐变(gradient)、滤镜(filter)等丰富的图形功能,可以通过 CSS 和 JavaScript 进行动态控制。

Rust Logo Ferris Python Motorcycle

5.4 EXIF 信息

EXIF(Exchangeable Image File Format)是嵌入在 JPEG、TIFF 等图像文件中的元数据标准,最初由日本电子工业发展协会(JEIDA)制定。

EXIF 记录了拍摄时的各种参数:

类别包含信息
拍摄设备相机品牌、型号、镜头信息
拍摄参数光圈(F值)、快门速度、ISO感光度、焦距
图像参数分辨率、方向、色空间
GPS 定位纬度、经度、海拔、拍摄时间
软件信息处理软件名称和版本

安全提示:EXIF 中的 GPS 信息可能泄露拍摄地点,在分享照片前应注意清除敏感元数据。

5.5 Rust 图片处理实战

5.5.1 环境准备

Cargo.toml 中添加依赖:

[dependencies]
image = "0.25"

image crate 是 Rust 生态中最流行的图像处理库,支持 BMP、JPEG、PNG、GIF、WebP、TIFF、AVIF 等多种格式的编解码。

5.5.2 读取与写入各种格式

use image::{DynamicImage, ImageFormat, io::Reader as ImageReader};
use std::fs::File;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 读取图片(自动识别格式)
    let img = ImageReader::open("input.jpg")?
        .with_guessed_format()?
        .decode()?;

    println!("图片尺寸: {}x{}", img.width(), img.height());
    println!("图片颜色类型: {:?}", img.color());

    // 保存为不同格式
    img.save("output.png")?;                          // 根据扩展名自动选择格式
    img.save_with_format("output.bmp", ImageFormat::Bmp)?;
    img.save_with_format("output.webp", ImageFormat::WebP)?;

    // 手动指定格式写入
    let mut output = File::create("output.jpg")?;
    img.write_to(&mut output, ImageFormat::Jpeg)?;

    Ok(())
}

5.5.3 图片裁剪

#![allow(unused)]
fn main() {
use image::GenericImageView;

fn crop_example() -> Result<(), Box<dyn std::error::Error>> {
    let img = image::open("input.jpg")?;
    let (width, height) = img.dimensions();

    // 裁剪上半部分
    let cropped = img.crop(0, 0, width, height / 2);
    cropped.save("cropped_top.jpg")?;

    // 裁剪中心区域
    let crop_w = width / 2;
    let crop_h = height / 2;
    let cropped_center = img.crop(
        (width - crop_w) / 2,
        (height - crop_h) / 2,
        crop_w,
        crop_h,
    );
    cropped_center.save("cropped_center.jpg")?;

    Ok(())
}
}

5.5.4 图片缩放与缩略图生成

#![allow(unused)]
fn main() {
use image::{imageops::FilterType, GenericImageView};
use std::time::Instant;

fn resize_example() -> Result<(), Box<dyn std::error::Error>> {
    let img = image::open("examples/scaledown/test.jpg")?;

    // 不同的缩放滤波算法对比
    let filters = [
        ("nearest", FilterType::Nearest),     // 最近邻插值(最快,质量最低)
        ("triangle", FilterType::Triangle),     // 三角插值(双线性)
        ("catmullrom", FilterType::CatmullRom),// Catmull-Rom 插值
        ("gaussian", FilterType::Gaussian),     // 高斯插值
        ("lanczos3", FilterType::Lanczos3),    // Lanczos3 插值(质量最高,较慢)
    ];

    for &(name, filter) in &filters {
        let timer = Instant::now();
        let scaled = img.resize(400, 400, filter);
        println!("使用 {} 算法缩放耗时: {:?}", name, timer.elapsed());
        let mut output = File::create(format!("test-{}.png", name))?;
        scaled.write_to(&mut output, ImageFormat::Jpeg)?;
    }

    // 生成缩略图(保持宽高比)
    for &size in &[20_u32, 40, 100, 200, 400] {
        let timer = Instant::now();
        let thumbnail = img.thumbnail(size, size);
        println!("生成 {}x{} 缩略图耗时: {:?}", size, size, timer.elapsed());
        thumbnail.save(format!("test-thumb{}.png", size))?;
    }

    Ok(())
}
}

各滤波算法对比

算法速度质量适用场景
Nearest最快最低像素风格、游戏
Triangle中等一般缩放
CatmullRom中等较高通用场景
Gaussian中等平滑缩放
Lanczos3较慢最高高质量缩放

5.5.5 像素级操作

灰度化

将彩色图像转换为灰度图像,使用加权平均法(ITU-R BT.601 标准):

$$Gray = 0.299 \times R + 0.587 \times G + 0.114 \times B$$

#![allow(unused)]
fn main() {
use image::{DynamicImage, Rgba};

fn to_grayscale(img: &DynamicImage) -> DynamicImage {
    let rgba_img = img.to_rgba8();
    let mut output = rgba_img.clone();

    for pixel in output.pixels_mut() {
        let Rgba([r, g, b, a]) = *pixel else { continue };
        let gray = (0.299 * r as f64 + 0.587 * g as f64 + 0.114 * b as f64) as u8;
        *pixel = Rgba([gray, gray, gray, a]);
    }

    DynamicImage::ImageRgba8(output)
}
}

二值化

将灰度图像转换为黑白图像(只有 0 和 255 两个值):

#![allow(unused)]
fn main() {
use image::{DynamicImage, Rgba, GenericImage};

fn to_binary(img: &DynamicImage, threshold: u8) -> DynamicImage {
    let gray_img = to_grayscale(img);
    let (width, height) = gray_img.dimensions();
    let mut output = image::GrayImage::new(width, height);

    for y in 0..height {
        for x in 0..width {
            let pixel = gray_img.get_pixel(x, y);
            let val = if pixel[0] > threshold { 255 } else { 0 };
            output.put_pixel(x, y, image::Luma([val]));
        }
    }

    DynamicImage::ImageLuma8(output)
}
}

颜色反转(底片效果)

#![allow(unused)]
fn main() {
use image::{DynamicImage, Rgba};

fn invert_colors(img: &DynamicImage) -> DynamicImage {
    let rgba_img = img.to_rgba8();
    let mut output = rgba_img.clone();

    for pixel in output.pixels_mut() {
        let Rgba([r, g, b, a]) = *pixel else { continue };
        *pixel = Rgba([255 - r, 255 - g, 255 - b, a]);
    }

    DynamicImage::ImageRgba8(output)
}
}

亮度与对比度调整

#![allow(unused)]
fn main() {
use image::{DynamicImage, Rgba};

fn adjust_brightness_contrast(
    img: &DynamicImage,
    brightness: i16,  // -255 到 255
    contrast: f32,    // 0.0 到 2.0(1.0 为原始)
) -> DynamicImage {
    let rgba_img = img.to_rgba8();
    let mut output = rgba_img.clone();

    let factor = (259.0 * (contrast + 255.0)) / (255.0 * (259.0 - contrast));

    for pixel in output.pixels_mut() {
        let Rgba([r, g, b, a]) = *pixel else { continue };
        let adjust = |c: u8| -> u8 {
            let val = factor * (c as f32 - 128.0) + 128.0 + brightness as f32;
            val.clamp(0.0, 255.0) as u8
        };
        *pixel = Rgba([adjust(r), adjust(g), adjust(b), a]);
    }

    DynamicImage::ImageRgba8(output)
}
}

5.5.6 格式转换

use image::{io::Reader as ImageReader, ImageFormat};
use std::path::Path;

/// 批量转换图片格式
fn convert_format(input: &Path, output: &Path, format: ImageFormat) -> Result<(), Box<dyn std::error::Error>> {
    let img = ImageReader::open(input)?
        .with_guessed_format()?
        .decode()?;

    let mut file = std::fs::File::create(output)?;
    img.write_to(&mut file, format)?;
    println!("已转换: {} -> {}", input.display(), output.display());
    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // JPEG 转 PNG
    convert_format(
        std::path::Path::new("photo.jpg"),
        std::path::Path::new("photo.png"),
        ImageFormat::Png,
    )?;

    // PNG 转 WebP
    convert_format(
        std::path::Path::new("photo.png"),
        std::path::Path::new("photo.webp"),
        ImageFormat::WebP,
    )?;

    Ok(())
}

5.5.7 图片水印

#![allow(unused)]
fn main() {
use image::{DynamicImage, GenericImage, imageops::overlay};

fn add_watermark(base_img: &DynamicImage, watermark: &DynamicImage, opacity: f32) -> DynamicImage {
    let mut base = base_img.clone();
    let (bw, bh) = base.dimensions();
    let wm = watermark.resize(bw / 4, bh / 4, imageops::FilterType::Lanczos3);
    let (ww, wh) = wm.dimensions();

    // 将水印放在右下角
    let x = bw.saturating_sub(ww + 20);
    let y = bh.saturating_sub(wh + 20);

    overlay(&mut base, &wm, x as i64, y as i64);
    base
}
}

5.6 OCR 光学字符识别

OCR(Optical Character Recognition,光学字符识别)是将图像中的文字转换为可编辑文本的技术。

OCR 的基本流程

输入图像 → 图像预处理 → 文字区域检测 → 字符分割 → 字符识别 → 后处理 → 输出文本

预处理步骤

  1. 灰度化:减少计算量
  2. 二值化:分离文字和背景
  3. 去噪:消除干扰
  4. 倾斜校正:修正图像角度
  5. 归一化:统一文字大小

主流 OCR 引擎

引擎语言特点许可证
TesseractC++Google 维护,支持 100+ 语言Apache 2.0
PaddleOCRPython/C++百度开源,中文识别优秀Apache 2.0
EasyOCRPython支持 80+ 语言,易用Apache 2.0
Tesseract.jsJavaScript浏览器端运行Apache 2.0

应用场景

  • 身份证、银行卡、发票识别
  • 车牌识别
  • 文档数字化
  • 手写文字识别
  • 街景文字提取

在 Rust 中,可以通过 FFI 调用 Tesseract C 库,或者调用 PaddleOCR 的 HTTP 服务来实现 OCR 功能。

5.7 相关 Rust 库

库名说明链接
imageRust 最流行的图像处理库,支持多种格式编解码GitHub
imageproc基于image的图像处理算法库(边缘检测、形态学等)GitHub
resvg高性能 SVG 渲染库GitHub
oxipngPNG 无损优化工具GitHub
mozjpeg-sysMozilla 的 JPEG 编码器 Rust 绑定crates.io
libwebp-sysGoogle WebP 编解码库 Rust 绑定crates.io
tesseract-rsTesseract OCR 的 Rust 封装crates.io
Luban接近微信朋友圈的图片压缩算法GitHub
iOS微信聊天,朋友圈图片压缩算法微信图片压缩算法分析GitHub
TesseractC++ OCR 引擎GitHub
Tesseract.js浏览器端 OCRGitHub
PaddleOCR百度开源 OCRGitHub

5.8 总结

图片格式对比

格式压缩方式Alpha通道动画典型用途文件大小
BMP无压缩Windows 位图、简单存储最大
JPEG有损 (DCT)网页照片、摄影作品
PNG无损 (LZ77+Huffman)网页图形、需要透明的场景中等
WebP有损/无损现代网页、替代JPEG/PNG最小
GIF无损 (LZW)索引色简单动画、低色图像中等
SVG矢量是(CSS/JS)图标、Logo、图表最小(简单图形)

关键公式汇总

公式说明
$Gray = 0.299R + 0.587G + 0.114B$RGB 转灰度
$C_{out} = \alpha C_{fg} + (1-\alpha) C_{bg}$Alpha 混合
$Y = 0.299R + 0.587G + 0.114B$RGB 转 YUV 亮度分量
$F_Q = \text{round}(F/Q)$JPEG 量化

5.9 练习题

  1. 基础题:使用 image 库读取一张 JPEG 图片,获取其宽度、高度和颜色类型,并输出到控制台。

  2. 格式转换:编写一个 Rust 程序,将一个目录下所有 .png 文件批量转换为 .webp 格式。

  3. 像素操作:实现一个函数,将图片的红色通道增强 50%(即将每个像素的 R 值乘以 1.5,不超过 255)。

  4. 灰度化对比:分别实现平均值法($Gray = (R+G+B)/3$)和加权平均法(BT.601)的灰度化,对比两种方法的效果差异。

  5. 缩略图生成器:编写一个命令行工具,接受一个图片路径和目标尺寸参数,生成指定大小的缩略图,并支持选择不同的缩放滤波算法。

  6. EXIF 读取:使用 kamadak-exifrexiv2 crate 读取一张照片的 EXIF 信息,输出拍摄时间、光圈、快门速度和 GPS 坐标。

  7. 思考题:为什么 JPEG 不适合存储文字截图?如果需要压缩文字截图,应该选择什么格式?为什么?

第六章 条形码

条形码(Barcode)是一种将数据编码为机器可读图形符号的技术。它通过不同宽度的条(bar)和空(space)的组合来表示信息,广泛应用于零售、物流、图书管理、医疗等领域。本章将深入探讨条形码的编码原理、常见标准,并使用 Rust 实现条形码的生成与校验。

6.1 条形码的定义与历史

6.1.1 什么是条形码

条形码是一种光学机器可读的数据表示形式。它将字符集(通常是数字和字母)转换为一系列具有不同反射率的平行线条或图案。扫描设备通过检测条(暗色,低反射率)和空(亮色,高反射率)的宽度差异来解码信息。

从信息论的角度看,条形码是一种信道编码方案,它将源数据映射到适合光学传输的物理信号。编码过程需要满足以下约束:

  • 自同步性:扫描器能够从任意位置开始正确解码
  • 检错能力:能够检测常见的读取错误
  • 密度与可靠性平衡:在有限空间内存储尽可能多的信息,同时保证扫描成功率

6.1.2 发展历程

条形码的发展可追溯至20世纪中期:

  • 1948年:Norman Joseph Woodland 和 Bernard Silver 基于摩尔斯电码发明了最早的条形码概念,使用同心圆图案(牛眼码)
  • 1973年:美国统一代码委员会(UCC)采纳了 George Laurer 设计的 UPC(Universal Product Code) 标准,标志着现代条形码时代的开始
  • 1977年:欧洲成立了欧洲物品编码协会(EAN),推出了与 UPC 兼容的 EAN 编码体系
  • 1994年:日本 Denso Wave 公司发明了 QR Code,将条形码从一维扩展到二维,显著提升了信息容量

如今,条形码已成为全球供应链和零售行业的基础设施,每天被扫描数十亿次。

6.2 常见条形码类型

条形码主要分为一维码(线性条形码)和二维码两大类。它们在信息容量、编码字符集和应用场景上各有侧重。

6.2.1 EAN/UPC 系列

UPC-AEAN-13 是最常见的零售商品条形码。

  • UPC-A:12位数字,主要用于美国和加拿大
  • EAN-13:13位数字,是国际通用的商品条码标准
  • EAN-8:8位数字,用于小型包装

EAN-13 的结构如下:

| 前缀码 (2-3位) | 厂商代码 (4-5位) | 产品代码 (5位) | 校验码 (1位) |

例如,中国的前缀码为 690-699

6.2.2 Code 128

Code 128 是一种高密度的一维码,可以编码全部 128 个 ASCII 字符。它通过三种不同的字符集(Code A、Code B、Code C)灵活切换,在物流和工业领域广泛应用。

Code 128 的特点:

  • 支持数字、字母、标点符号和控制字符
  • 编码密度高,相同长度可存储更多字符
  • 包含校验码,可靠性好

6.2.3 Code 39

Code 39(又称 Code 3 of 9)是一种较早的条形码标准,每个字符由 9 个元素组成(5 条 + 4 空),其中 3 个是宽元素。

特点:

  • 支持 43 个字符:数字 0-9、大写字母 A-Z 和若干符号
  • 不需要校验码(但通常添加一位校验字符以增强可靠性)
  • 编码密度较低,但实现简单

6.2.4 QR Code

QR Code(Quick Response Code)是一种矩阵式二维码,与一维码有本质区别:

特性一维码QR Code
信息方向水平方向水平 + 垂直方向
容量约 20-30 个字符数千个字符
容错能力四个等级(L/M/Q/H)
中文支持不支持支持
读取方向需对齐360° 任意角度

QR Code 通过 Reed-Solomon 纠错码实现了即使部分图案损坏仍可读取的能力。其容量随版本号增加而增大,最高版本(Version 40)可存储约 7,089 个数字或 4,296 个字母数字字符。

6.3 条形码编码原理

6.3.1 条空宽度与二进制的对应

一维条形码本质上是一种**脉冲宽度调制(PWM)**信号。条和空的宽度对应不同的二进制序列。以 EAN-13 为例,每个数字由 7 个模块(module)组成,每个模块宽度相同,条表示二进制的 1,空表示二进制的 0

每个数字有两种编码模式:

  • 左侧奇校验(L-code):以空开始,以条结束
  • 左侧偶校验(G-code):以空开始,以条结束,但模式与 L-code 不同
  • 右侧(R-code):以条开始,以空结束,是 L-code 的反码

EAN-13 的数字编码表如下:

数字L-codeG-codeR-code
0000110101001111110010
1001100101100111100110
2001001100110111101100
3011110101000011000010
4010001100111011011100
5011000101110011001110
6010111100001011010000
7011101100100011000100
8011011100010011001000
9000101100101111110100

注意:这里的 10 分别表示条和空,实际绘制时需要考虑条空交替。

前缀第一位数字决定了左侧 6 位数字使用 L-code 还是 G-code 的组合,这一设计使得扫描器可以自动判断条码方向。

6.3.2 起始符、终止符与中间分隔符

EAN-13 条码还包含以下固定模式:

  • 起始符(Guard Bar)101,位于条码左侧
  • 中间分隔符(Center Guard)01010,位于条码中间
  • 终止符(Guard Bar)101,位于条码右侧

这些守卫条帮助扫描器校准读取位置,确保解码的准确性。

6.3.3 Code 128 的编码结构

Code 128 的每个字符由 11 个模块组成(终止符为 13 个),结构为:

| 起始符 | 数据字符1 | 数据字符2 | ... | 校验字符 | 终止符 |

Code 128 使用三种字符集:

  • Code A:ASCII 控制字符和大写字母
  • Code B:大写、小写字母和标点
  • Code C:仅数字,每两个数字用一个字符编码,密度翻倍

字符集切换通过特殊的切换字符(如 CODE BCODE C)实现,这类似于数据压缩中的模式切换。

6.4 校验码计算

6.4.1 EAN-13 校验码算法

EAN-13 的最后一位是校验码,用于检测输入或扫描错误。其计算步骤如下:

  1. 取前 12 位数字
  2. 从右向左,奇数位(第1、3、5…位)数字相加,乘以 3
  3. 从右向左,偶数位(第2、4、6…位)数字相加
  4. 将两个和相加,取结果的个位数
  5. 校验码 = $(10 - \text{个位数}) \bmod 10$

用数学公式表示:

$$ c = \left(10 - \left(\sum_{i=1}^{6} d_{2i-1} \times 3 + \sum_{i=1}^{6} d_{2i}\right) \bmod 10\right) \bmod 10 $$

其中 $d_1, d_2, \ldots, d_{12}$ 为前 12 位数字。

6.4.2 Rust 实现 EAN-13 校验码

fn calculate_ean13_check_digit(digits: &[u8; 12]) -> u8 {
    let mut sum = 0u32;
    for (i, &d) in digits.iter().enumerate() {
        // 从右向左,所以索引 0 对应最右边(奇数位)
        if i % 2 == 0 {
            sum += (d as u32) * 3;
        } else {
            sum += d as u32;
        }
    }
    let remainder = sum % 10;
    ((10 - remainder) % 10) as u8
}

fn validate_ean13(code: &str) -> bool {
    if code.len() != 13 || !code.chars().all(|c| c.is_ascii_digit()) {
        return false;
    }
    let digits: Vec<u8> = code.bytes().map(|b| b - b'0').collect();
    let input_check = digits[12];
    let computed = calculate_ean13_check_digit(
        &digits[..12].try_into().unwrap()
    );
    input_check == computed
}

fn main() {
    let digits = [6, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    let check = calculate_ean13_check_digit(&digits);
    println!("校验码: {}", check); // 输出: 校验码: 2

    let full_code = "6901234567892";
    println!("{} 校验结果: {}", full_code, validate_ean13(full_code));
}

6.4.3 Code 128 校验码算法

Code 128 的校验码计算采用加权求和模 103 的方式:

  1. 起始符有一个数值(Code A = 103, Code B = 104, Code C = 105)
  2. 每个数据字符有一个对应的值(0-102)
  3. 校验码 = $(\text{起始符值} + \sum_{i=1}^{n} i \times \text{字符}_i\text{的值}) \bmod 103$
#![allow(unused)]
fn main() {
fn calculate_code128_check(start_value: u8, values: &[u8]) -> u8 {
    let mut sum = start_value as u32;
    for (i, &v) in values.iter().enumerate() {
        sum += ((i + 1) as u32) * (v as u32);
    }
    (sum % 103) as u8
}
}

6.5 使用 Rust 生成条形码

6.5.1 使用 barcode-rs 库

barcode crate 是 Rust 生态中成熟的条形码生成库,支持多种一维码格式。

Cargo.toml 中添加依赖:

[dependencies]
barcode = "0.1"
image = "0.24"

生成 Code 128 条形码的示例:

use barcode::sym::code128::Code128;
use barcode::generators::image::Image;
use barcode::generators::Generator;

fn main() {
    let data = "RUST-2024";
    let barcode = Code128::new(data).unwrap();
    let generator = Image::png();
    let bytes = generator.generate(&barcode).unwrap();
    std::fs::write("rust2024.png", bytes).unwrap();
    println!("条形码已生成: rust2024.png");
}

6.5.2 手动生成简单 Code 128

为了深入理解编码原理,下面手动实现一个简化的 Code 128 编码器,仅支持 Code B 字符集(ASCII 32-127)。

const CODE128_B_START: u8 = 104;
const CODE128_STOP: u8 = 106;

// Code B 字符值映射:空格(32) -> 0, '!'(33) -> 1, ..., 'Z'(90) -> 58, ...
fn char_to_code128_b(c: char) -> Option<u8> {
    let code = c as u8;
    if (32..=127).contains(&code) {
        Some(code - 32)
    } else {
        None
    }
}

fn code128_checksum(start: u8, values: &[u8]) -> u8 {
    let mut sum = start as u32;
    for (i, &v) in values.iter().enumerate() {
        sum += ((i + 1) as u32) * (v as u32);
    }
    (sum % 103) as u8
}

fn encode_code128_b(text: &str) -> Option<Vec<u8>> {
    let mut values = vec![CODE128_B_START];
    for c in text.chars() {
        values.push(char_to_code128_b(c)?);
    }
    let check = code128_checksum(CODE128_B_START, &values[1..]);
    values.push(check);
    values.push(CODE128_STOP);
    Some(values)
}

fn main() {
    let text = "HELLO";
    match encode_code128_b(text) {
        Some(encoded) => {
            println!("'{}' 的 Code 128 编码值: {:?}", text, encoded);
        }
        None => println!("包含不支持的字符"),
    }
}

6.5.3 将编码值渲染为位图

Code 128 每个字符由 11 个模块组成,条和空的模式由查找表确定。以下是将编码值渲染为简单文本表示的示例:

// Code 128 字符模式表(简化,仅展示部分)
// 每个字符由 11 个模块组成:1 表示条,0 表示空
const CODE128_PATTERNS: [&str; 107] = [
    "11011001100", "11001101100", "11001100110", // 0-2 (空格, !, ")
    // ... 省略中间条目
    "1100011101011", // STOP (106)
];

fn render_code128_bars(values: &[u8]) -> String {
    let mut result = String::new();
    for &v in values {
        let pattern = CODE128_PATTERNS.get(v as usize).unwrap_or(&"");
        result.push_str(pattern);
    }
    result
}

fn bars_to_ascii(bars: &str) -> String {
    bars.chars()
        .map(|c| if c == '1' { '█' } else { ' ' })
        .collect()
}

fn main() {
    let text = "HI";
    if let Some(values) = encode_code128_b(text) {
        let bars = render_code128_bars(&values);
        println!("{}", bars_to_ascii(&bars));
    }
}

实际应用中,应将条空模式渲染为 PNG、SVG 或位图格式,并确保条宽符合打印分辨率要求(通常最小条宽为 0.33mm)。

6.6 条形码的校验与识别

6.6.1 扫描解码流程

条形码扫描器的工作流程如下:

  1. 光学采样:激光或 LED 光源照射条码,光电传感器接收反射光
  2. 模拟-数字转换:将光强信号转换为电压波形
  3. 边缘检测:识别条与空之间的跳变沿
  4. 宽度测量:测量每个条和空的宽度(以模块为单位)
  5. 字符解码:根据编码表将模块序列映射为字符
  6. 校验验证:核对校验码,确认数据完整性

6.6.2 Rust 实现简单的条码校验

在实际系统中,接收到的条码数据应进行格式和校验码验证:

#[derive(Debug)]
enum BarcodeError {
    InvalidLength,
    InvalidCharacter,
    ChecksumMismatch,
}

struct Ean13(String);

impl Ean13 {
    fn new(code: &str) -> Result<Self, BarcodeError> {
        if code.len() != 13 {
            return Err(BarcodeError::InvalidLength);
        }
        if !code.chars().all(|c| c.is_ascii_digit()) {
            return Err(BarcodeError::InvalidCharacter);
        }
        let digits: Vec<u8> = code.bytes().map(|b| b - b'0').collect();
        let mut sum = 0u32;
        for (i, &d) in digits[..12].iter().enumerate() {
            if i % 2 == 0 {
                sum += (d as u32) * 3;
            } else {
                sum += d as u32;
            }
        }
        let check = ((10 - (sum % 10)) % 10) as u8;
        if digits[12] != check {
            return Err(BarcodeError::ChecksumMismatch);
        }
        Ok(Ean13(code.to_string()))
    }

    fn country_prefix(&self) -> &str {
        &self.0[..3]
    }

    fn as_str(&self) -> &str {
        &self.0
    }
}

fn main() {
    match Ean13::new("6901234567892") {
        Ok(ean) => {
            println!("有效的 EAN-13: {}", ean.as_str());
            println!("国家前缀: {}", ean.country_prefix());
        }
        Err(e) => println!("校验失败: {:?}", e),
    }
}

6.6.3 纠错与容错

一维码(如 EAN、Code 128)本身不具备纠错能力,仅能通过校验码检测错误。若校验失败,扫描器通常会要求重新扫描。

相比之下,QR Code 采用 Reed-Solomon 纠错码,可以在以下情况下恢复数据:

纠错等级最大恢复比例适用场景
L (Low)7%清洁环境
M (Medium)15%一般环境
Q (Quartile)25%较脏环境
H (High)30%极端环境

Reed-Solomon 码的数学基础是有限域(Galois Field)上的多项式运算。对于 QR Code,数据被分割为若干块,每块附加纠错码,使得即使部分模块损坏或遮挡,仍可通过解线性方程组恢复原始数据。

6.7 总结

本章系统介绍了条形码技术的核心概念与 Rust 实现方法。下表总结了各类条形码的关键特性:

特性EAN-13UPC-ACode 128Code 39QR Code
字符集数字数字全 ASCII43字符二进制数据
长度13位12位可变可变可变
校验模10模10模103可选Reed-Solomon
密度极高
方向敏感
纠错能力检错检错检错检错纠错
典型应用零售商品零售商品物流标签资产标签移动支付

条形码技术虽然看似简单,但其背后蕴含了信号处理、编码理论和纠错算法的深刻原理。从 UPC 到 QR Code,条形码的演进反映了信息存储密度与可靠性之间持续优化的过程。

练习

  1. 扩展 Ean13 结构体,添加解析厂商代码和产品代码的方法。
  2. 实现完整的 EAN-13 编码器,将 13 位数字转换为条空模块序列,并输出为 SVG 图形。
  3. 为 Code 128 实现 Code C 字符集支持,使连续数字对的编码密度提升一倍。
  4. 使用 image crate 将条形码模块序列渲染为 PNG 位图,支持配置条宽和高度。
  5. 研究 QR Code 的数据掩码(Data Masking)机制,理解其如何避免大面积同色块导致扫描困难。

第七 二维码

二维码源于日本,在中国发扬光大。

二维码相较于条形码,能够存储的信息更多,同时也能快速完成短信息的识别输入。从移动支付到健康码,从商品溯源到电子名片,二维码已经深入到我们生活的方方面面。

7.1 二维码的历史

7.1.1 QR码的诞生

1994年,日本 Denso Wave 公司的工程师原昌宏(Masahiro Hara)发明了 QR Code(Quick Response Code)。当时,丰田汽车旗下的零部件供应商需要一种能够快速扫描并存储大量信息的条码系统来追踪汽车零部件。传统的条形码只能存储少量数字信息,且只能单向扫描,无法满足工业需求。

原昌宏从围棋棋盘的网格结构中获得灵感,设计了这种可以在水平和垂直两个方向同时存储信息的矩阵式二维码。QR Code 的“Quick Response“(快速响应)之名,正是强调了其高速读取的特性。

QR Code 的关键创新:

  • 高速读取:通过定位图案实现 360 度任意方向扫描
  • 大容量:相比一维条码,信息容量提升数十倍
  • 容错能力:即使部分损坏仍可正确读取
  • 支持多种数据类型:数字、字母、汉字、二进制数据

7.1.2 在中国的发扬光大

QR Code 在日本发明后,最初主要用于工业领域。然而,真正让二维码“火遍全球“的是中国。

  • 2011年:支付宝推出二维码支付,开创了移动支付的先河
  • 2014年:微信支付全面接入二维码,“扫码支付“迅速普及
  • 2020年:健康码系统使用二维码进行疫情防控,几乎全民使用
  • 如今,中国已成为全球二维码应用最广泛的国家,日均扫码量超过百亿次

7.2 QR码原理详解

7.2.1 QR码的结构

一个标准的 QR 码由以下功能区域组成:

┌─────────────────────────────┐
│ ■ ■ ■ ■ ■ ■ ■   ■         │  定位图案(左上)
│ ■       ■         ■         │
│ ■       ■         ■         │  定位图案(右上)
│ ■ ■ ■ ■ ■ ■ ■   ■ ■ ■ ■ ■ ■ ■ ■
│                     ■       │
│ ■ ■ ■ ■ ■ ■ ■     ■       │  定位图案(左下)
│ ■       ■           ■       │
│ ■       ■           ■       │
│ ■ ■ ■ ■ ■ ■ ■   ■         │
│                     ■       │
│   ■   ■   ■   ■   ■   ■   ■  ← 定时图案(水平和垂直)
│                     ■       │
│   [格式信息]  [版本信息]     │
│   [数据和纠错码字]           │
│   [剩余位]                  │
└─────────────────────────────┘

各区域说明

区域位置功能
定位图案 (Finder Pattern)三个角落7x7 的嵌套正方形,帮助扫描器定位 QR 码的位置和方向
定时图案 (Timing Pattern)连接定位图案交替的黑白模块,帮助确定模块坐标
格式信息 (Format Info)定位图案旁纠错等级和掩码图案编号
版本信息 (Version Info)较大版本中QR 码版本号(1-40)
数据和纠错码字主体区域编码后的数据和纠错码
静区 (Quiet Zone)四周空白边框,宽度至少 4 个模块

7.2.2 编码模式

QR 码支持四种编码模式,根据数据内容自动或手动选择最优模式:

模式名称适用字符编码效率
0001数字模式 (Numeric)0-9最高(3个数字→10bit)
0010字母数字模式 (Alphanumeric)0-9, A-Z, 空格, $%*+-./:较高(2个字符→11bit)
0100字节模式 (Byte)ISO 8859-1 字符集(含中文UTF-8)中等(1字节→8bit)
1000汉字模式 (Kanji)Shift-JIS 编码的日文汉字较高(1个汉字→13bit)

数字模式的编码方式:将数字分为 3 位一组,每组转换为 10 位二进制数。例如数字 123456

$$123 \rightarrow 0001111011_2$$ $$456 \rightarrow 0111001000_2$$

字母数字模式的字符集包含 45 个字符:

0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:

将字符分为 2 个一组,每组转换为 11 位二进制数:

$$\text{value} = c_1 \times 45 + c_2$$

7.2.3 纠错等级

QR 码使用 Reed-Solomon 纠错编码,提供四个纠错等级:

等级代号纠错容量可恢复数据比例适用场景
LLow约 7%数据的 7% 可被恢复数据密度优先,环境良好
MMedium约 15%数据的 15% 可被恢复通用场景(默认)
QQuartile约 25%数据的 25% 可被恢复需要较高容错
HHigh约 30%数据的 30% 可被恢复带Logo的二维码、恶劣环境

纠错等级越高,可用数据容量越小,但容错能力越强。例如,Version 1 的 QR 码在不同纠错等级下的数据容量:

纠错等级数字字母数字字节汉字
L41251710
M3220148
Q2716117
H171074

7.2.4 数据编码流程

QR 码的完整编码流程如下:

┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│ 1.数据分析 │ → │ 2.数据编码 │ → │ 3.纠错编码 │ → │ 4.数据排列 │ → │ 5.掩码处理 │
│          │    │          │    │          │    │          │    │          │
│ 确定编码  │    │ 将数据转换 │    │ 生成Reed- │    │ 将码字放入 │    │ 应用掩码  │
│ 模式和版本│    │ 为位流    │    │ Solomon码 │    │ QR码矩阵  │    │ 优化图案  │
└──────────┘    └──────────┘    └──────────┘    └──────────┘    └──────────┘

步骤详解

  1. 数据分析:确定最优编码模式、版本号和纠错等级
  2. 数据编码:添加模式指示符和字符计数指示符,将数据转换为位流,添加终止符和填充位
  3. 纠错编码:将数据分为若干块,对每块生成 Reed-Solomon 纠错码字
  4. 数据排列:按照蛇形路径将数据码字和纠错码字放入 QR 码矩阵
  5. 掩码处理:尝试 8 种掩码图案,选择使评分最优的一种

7.2.5 Reed-Solomon 纠错算法

Reed-Solomon(RS)编码是 QR 码容错能力的核心。它属于分组纠错码,基于有限域(Galois Field, GF)上的多项式运算。

基本原理

设数据码字为 $d_0, d_1, \ldots, d_{n-1}$,构造数据多项式:

$$D(x) = d_0 + d_1 x + d_2 x^2 + \cdots + d_{n-1} x^{n-1}$$

纠错码字通过对 $D(x)$ 在特定点求值得到:

$$c_i = D(\alpha^i) \mod p(x)$$

其中 $\alpha$ 是有限域 $GF(2^8)$ 的本原元,$p(x)$ 是不可约多项式:

$$p(x) = x^8 + x^4 + x^3 + x^2 + 1$$

解码过程

当 QR 码部分损坏时,接收端通过求解方程组恢复原始数据。设接收到的码字为 $r_0, r_1, \ldots, r_{n+k-1}$(含 $k$ 个纠错码字),定义错误多项式和伴随式(Syndrome),通过 Berlekamp-Massey 算法或 Euclidean 算法求解错误位置和错误值。

7.2.6 版本与容量

QR 码共有 40 个版本(Version 1 到 Version 40),版本号与模块数的关系为:

$$\text{模块数} = 4 \times \text{版本号} + 17$$

版本模块数数据容量(字节模式,纠错等级L)数据容量(数字模式,纠错等级L)
121 x 2117 字节41 位数字
537 x 3777 字节185 位数字
1057 x 57174 字节419 位数字
2097 x 97589 字节1,423 位数字
30137 x 1371,256 字节3,041 位数字
40177 x 1772,953 字节7,089 位数字

Version 40 的 QR 码最大可存储 7,089 个数字字符或 2,953 个字节(约 2.9 KB)。

7.3 其他二维码标准

除了 QR Code,还有多种二维码标准,各有特色:

7.3.1 Data Matrix

Data Matrix 由 International Data Matrix 于 1994 年开发,广泛应用于工业标记和电子元器件标识。

特性说明
外观方形或矩形,由黑白模块组成
最大容量2,335 个数字字符
容错Reed-Solomon 纠错
特点无需静区(利用 L 形边界定位)
应用电子元器件、医疗器械标记

7.3.2 PDF417

PDF417(Portable Data File 417)由 Symbol Technologies 于 1991 年开发,是一种堆叠式二维码。

特性说明
外观多行堆叠的一维条码
最大容量1,850 个数字字符
容错两个纠错等级
特点可打印在证件上,美国驾照标准
应用身份证件、运输标签、不动产

7.3.3 Aztec Code

Aztec Code 由 Andrew Longacre 于 1995 年发明,中心有一个定位图案。

特性说明
外观方形,中心有定位图案(类似“牛眼“)
最大容量3,832 个数字字符
容错Reed-Solomon 纠错,可调比例
特点不需要静区,尺寸更紧凑
应用航空 boarding pass、交通票务

7.3.4 二维码标准对比

特性QR CodeData MatrixPDF417Aztec Code
类型矩阵式矩阵式堆叠式矩阵式
最大容量7,089 数字2,335 数字1,850 数字3,832 数字
容错等级4级(L/M/Q/H)可调2级可调比例
需要静区
读取速度最快中等
中文支持
主要应用移动支付、广告工业标记证件交通票务

7.4 QR码安全

7.4.1 恶意二维码

二维码本身不携带恶意代码,但可以被用来引导用户访问恶意网站。常见的攻击方式:

  • 钓鱼攻击:将二维码指向伪造的登录页面,窃取用户账号密码
  • 恶意软件下载:扫描后自动下载并安装恶意应用
  • 中间人攻击:通过二维码劫持通信会话
  • 虚假支付:将收款二维码替换为攻击者的收款码

7.4.2 防护建议

  1. 扫码前预览:使用支持 URL 预览的扫码工具,查看目标地址后再决定是否访问
  2. 使用安全扫码工具:选择带有安全检测功能的扫码应用
  3. 注意 URL 来源:确认链接域名是否为官方域名
  4. 不要随意扫码:来源不明的二维码不要扫描
  5. 开启支付验证:移动支付应设置密码或生物识别验证

7.5 Rust 二维码实战

7.5.1 环境准备

Cargo.toml 中添加依赖:

[dependencies]
qrcode = "0.14"
image = "0.25"

7.5.2 生成基本二维码

use qrcode::QrCode;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 生成二维码
    let code = QrCode::new("https://www.rust-lang.org")?;

    // 在终端打印二维码(ASCII 形式)
    println!("{}", code.render::<char>()
        .quiet_zone(false)
        .module_dimensions(2, 1)
        .build());

    // 输出二维码的基本信息
    println!("版本: {}", code.version());
    println!("宽度: {} 模块", code.width());

    Ok(())
}

7.5.3 生成二维码图片

#![allow(unused)]
fn main() {
use image::{GenericImage, ImageBuffer, Rgba};
use qrcode::QrCode;

fn generate_qr_image(data: &str, size: u32) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
    // 1. 编码数据生成二维码
    let code = QrCode::new(data.as_bytes()).unwrap();

    // 2. 设置二维码的模块大小,并添加白色边框
    let border = 4;
    let module_size = (size as f32 / (code.width() as f32 + 2.0 * border as f32)).ceil() as u32;

    // 3. 将二维码渲染为图像
    code.render::<Rgba<u8>>()
        .min_dimensions(size, size) // 设置最小尺寸,确保清晰
        .quiet_zone(true) // 添加静区(白色边框)
        .module_dimensions(module_size, module_size)
        .build()
}
}

7.5.4 自定义颜色的二维码

use image::{ImageBuffer, Rgba, Rgb};
use qrcode::{QrCode, Color};

fn generate_colored_qr(
    data: &str,
    fg_color: [u8; 3],
    bg_color: [u8; 3],
    module_size: u32,
) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
    let code = QrCode::new(data).unwrap();
    let border = 4;
    let width = code.width();
    let img_size = (width + border * 2) as u32 * module_size;

    let mut img = ImageBuffer::from_pixel(
        img_size,
        img_size,
        Rgba([bg_color[0], bg_color[1], bg_color[2], 255]),
    );

    for y in 0..width {
        for x in 0..width {
            let color = if code[(x, y)] == Color::Dark {
                Rgba([fg_color[0], fg_color[1], fg_color[2], 255])
            } else {
                Rgba([bg_color[0], bg_color[1], bg_color[2], 255])
            };

            // 绘制模块
            for dy in 0..module_size {
                for dx in 0..module_size {
                    let px = (x as u32 + border as u32) * module_size + dx;
                    let py = (y as u32 + border as u32) * module_size + dy;
                    img.put_pixel(px, py, color);
                }
            }
        }
    }

    img
}

fn main() {
    // 生成蓝色前景、白色背景的二维码
    let qr = generate_colored_qr(
        "https://www.rust-lang.org",
        [0, 100, 200],   // 蓝色前景
        [255, 255, 255],  // 白色背景
        10,
    );
    qr.save("qr_blue.png").unwrap();

    // 生成红色前景的二维码
    let qr_red = generate_colored_qr(
        "Hello, Rust!",
        [220, 50, 50],   // 红色前景
        [255, 255, 255],  // 白色背景
        10,
    );
    qr_red.save("qr_red.png").unwrap();
}

7.5.5 带Logo的二维码

利用 QR 码的纠错能力,可以在二维码中心放置 Logo 而不影响识别。需要使用较高的纠错等级(H 级,30% 容错)。

use image::{GenericImage, ImageBuffer, Rgba, imageops};
use qrcode::{QrCode, EcLevel};

fn generate_qr_with_logo(
    data: &str,
    logo_path: &str,
    size: u32,
    logo_ratio: f32,  // Logo 占二维码的比例(建议 0.2-0.3)
) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>, Box<dyn std::error::Error>> {
    // 1. 使用 H 级纠错生成二维码
    let code = QrCode::with_error_correction_level(
        data.as_bytes(),
        EcLevel::H,  // 30% 纠错容量
    )?;

    // 2. 渲染二维码
    let border = 4;
    let module_size = (size as f32 / (code.width() as f32 + 2.0 * border as f32)).ceil() as u32;
    let mut qr_img = code.render::<Rgba<u8>>()
        .min_dimensions(size, size)
        .quiet_zone(true)
        .module_dimensions(module_size, module_size)
        .build();

    // 3. 加载 Logo
    let logo = image::open(logo_path)?.to_rgba8();
    let (qr_w, qr_h) = qr_img.dimensions();

    // 4. 计算 Logo 尺寸和位置(居中)
    let logo_w = (qr_w as f32 * logo_ratio) as u32;
    let logo_h = (qr_h as f32 * logo_ratio) as u32;
    let resized_logo = logo.resize(logo_w, logo_h, imageops::FilterType::Lanczos3);

    // 5. 添加白色背景区域
    let padding = 10;
    let bg_w = logo_w + padding * 2;
    let bg_h = logo_h + padding * 2;
    let mut bg = ImageBuffer::from_pixel(bg_w, bg_h, Rgba([255, 255, 255, 255]));
    bg.copy_from(&resized_logo, padding, padding)?;

    // 6. 将 Logo 放置在二维码中心
    let x = (qr_w - bg_w) / 2;
    let y = (qr_h - bg_h) / 2;
    qr_img.copy_from(&bg, x, y)?;

    Ok(qr_img)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let qr = generate_qr_with_logo(
        "https://www.rust-lang.org",
        "images/rust-logo-blk.svg",
        600,
        0.25,
    )?;
    qr.save("qr_with_logo.png")?;
    println!("带 Logo 的二维码已生成: qr_with_logo.png");
    Ok(())
}

7.5.6 二维码解码/识别

使用 bardecoder crate 进行二维码解码:

[dependencies]
bardecoder = "0.5"
image = "0.25"
use bardecoder::decode::Decode;
use image::DynamicImage;

fn decode_qr(image_path: &str) -> Result<String, Box<dyn std::error::Error>> {
    let img = image::open(image_path)?;
    let decoder = bardecoder::default_decoder();

    let results = decoder.decode(&img);
    for result in results {
        match result {
            Ok(content) => {
                println!("解码结果: {}", content);
                return Ok(content);
            }
            Err(e) => println!("解码失败: {:?}", e),
        }
    }

    Err("无法解码二维码".into())
}

fn main() {
    match decode_qr("qr_code.png") {
        Ok(content) => println!("二维码内容: {}", content),
        Err(e) => println!("错误: {}", e),
    }
}

7.5.7 批量生成二维码

use image::{GenericImage, ImageBuffer, Rgba};
use qrcode::QrCode;
use std::fs;

/// 批量生成二维码并保存
fn batch_generate_qr_codes(
    data_list: &[(&str, &str)],  // (数据, 输出文件名)
    size: u32,
) -> Result<(), Box<dyn std::error::Error>> {
    for (data, filename) in data_list {
        let code = QrCode::new(data.as_bytes())?;
        let border = 4;
        let module_size = (size as f32 / (code.width() as f32 + 2.0 * border as f32)).ceil() as u32;

        let img = code.render::<Rgba<u8>>()
            .min_dimensions(size, size)
            .quiet_zone(true)
            .module_dimensions(module_size, module_size)
            .build();

        img.save(filename)?;
        println!("已生成: {} -> {}", data, filename);
    }

    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let data_list = &[
        ("https://www.rust-lang.org", "qr_rust.png"),
        ("https://crates.io", "qr_crates.png"),
        ("https://docs.rs", "qr_docs.png"),
        ("https://github.com/rust-lang/rust", "qr_github.png"),
    ];

    // 创建输出目录
    fs::create_dir_all("output_qr")?;

    batch_generate_qr_codes(data_list, 300)?;

    Ok(())
}

7.5.8 合并多个二维码

以下代码将多个二维码横向合并为一张图片:

use image::{GenericImage, ImageBuffer, Rgba};
use qrcode::QrCode;

fn generate_qr_image(data: &str, size: u32) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
    let code = QrCode::new(data.as_bytes()).unwrap();
    let border = 4;
    let module_size = (size as f32 / (code.width() as f32 + 2.0 * border as f32)).ceil() as u32;

    code.render::<Rgba<u8>>()
        .min_dimensions(size, size)
        .quiet_zone(true)
        .module_dimensions(module_size, module_size)
        .build()
}

fn combine_qr_codes_horizontally(
    img1: &ImageBuffer<Rgba<u8>, Vec<u8>>,
    img2: &ImageBuffer<Rgba<u8>, Vec<u8>>,
    gap: u32,
) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
    // 1. 计算新图片的尺寸
    let (w1, h1) = img1.dimensions();
    let (w2, h2) = img2.dimensions();
    let final_width = w1 + gap + w2;
    let final_height = h1.max(h2);

    // 2. 创建新画布,并填充白色背景
    let mut combined =
        ImageBuffer::from_pixel(final_width, final_height, Rgba([255, 255, 255, 255]));

    // 3. 将第一张图复制到左侧 (0, 0) 位置
    combined.copy_from(img1, 0, 0).unwrap();

    // 4. 将第二张图复制到右侧 (w1 + gap, 0) 位置
    combined.copy_from(img2, w1 + gap, 0).unwrap();

    combined
}

fn main() {
    // 1. 生成两个二维码图像,尺寸设为 200x200
    let qr1 = generate_qr_image("https://qr.alipay.com/fkx18203840eqbo648wdo95", 200);
    let qr2 = generate_qr_image("wxp://f2f0QuWokq-0iWOJrBOVHz7vVLoOhrChnMI28COpdSmFzRs", 200);

    // 2. 横向合并,间隔 20 像素
    let combined = combine_qr_codes_horizontally(&qr1, &qr2, 20);

    // 3. 保存最终图片
    combined.save("/tmp/combined_qr_codes.png").unwrap();
}

7.6 二维码的用途

二维码的应用已经渗透到生活的方方面面:

类别应用场景说明
移动支付付款码、收款码支付宝、微信支付
身份认证健康码、电子证照疫情防控、电子身份证
商品溯源商品编码、防伪标签食品安全、药品追溯
广告营销链接分享、优惠券扫码领红包、关注公众号
交通出行电子车票、登机牌高铁二维码、Aztec Code 登机牌
社交互动加好友、名片交换微信扫码加好友
文档管理文件分享、会议签到扫码下载文件、签到打卡
物联网设备配对、WiFi连接扫码连接WiFi、IoT设备配网
请作者喝杯奶茶呗 请作者喝杯奶茶呗

7.7 标准与规范

QR Code 的国际标准为 ISO/IEC 18004,由国际标准化组织(ISO)和国际电工委员会(IEC)联合发布。

QR Code 的官方信息:

技术原理参考:

7.8 相关 Rust 库

库名说明链接
qrcodeQR 码生成库,支持多种输出格式GitHub
bardecoder多格式条码/二维码解码库GitHub
rqrr高性能 QR 码解码库GitHub
quircsquirc QR 解码器的 Rust 绑定crates.io
image图像处理库(配合二维码库使用)GitHub
fast_qr高速 QR 码生成库(SIMD 加速)GitHub

7.9 总结

QR码核心参数对比

参数说明
版本范围Version 1 (21x21) 到 Version 40 (177x177)
模块数公式$4 \times \text{version} + 17$
编码模式数字、字母数字、字节、汉字
纠错等级L(7%), M(15%), Q(25%), H(30%)
最大容量7,089 数字 / 4,296 字母数字 / 2,953 字节
纠错算法Reed-Solomon 编码
有限域$GF(2^8)$,不可约多项式 $x^8 + x^4 + x^3 + x^2 + 1$

二维码标准对比

特性QR CodeData MatrixPDF417Aztec Code
类型矩阵式矩阵式堆叠式矩阵式
最大容量7,089 数字2,335 数字1,850 数字3,832 数字
容错等级4级可调2级可调
需要静区
读取速度最快中等
中国普及度最高中等

关键公式汇总

公式说明
$\text{模块数} = 4v + 17$QR 码版本 v 对应的模块数
$p(x) = x^8 + x^4 + x^3 + x^2 + 1$Reed-Solomon 有限域不可约多项式
$\text{数字容量} = \lfloor n/3 \rfloor \times 10 + r$数字模式位流计算

7.10 练习题

  1. 基础题:使用 qrcode crate 生成一个包含你名字的二维码,保存为 PNG 格式。

  2. 纠错等级对比:分别使用 L、M、Q、H 四种纠错等级生成相同内容的二维码,对比它们的大小差异,并用图像编辑工具遮挡部分区域后测试是否能正确解码。

  3. 带 Logo 二维码:选择一张你喜欢的 Logo 图片,使用 H 级纠错生成带 Logo 的二维码,并验证其可扫描性。

  4. 批量生成:编写一个 Rust 程序,读取 CSV 文件中的 URL 列表,为每个 URL 生成一个二维码,文件名为 URL 的哈希值。

  5. 编码模式分析:对于字符串 "Hello, World! 123",分析 QR 码会选择哪种编码模式?为什么?如果字符串是 "1234567890" 呢?

  6. 容量计算:Version 10 的 QR 码有多少个模块?在纠错等级 M 下,字节模式最多能存储多少个字节?

  7. 思考题:为什么二维码在中国如此普及,而在欧美国家的使用率相对较低?从技术、商业模式、社会习惯等角度分析。

  8. 安全实践:编写一个 Rust 程序,生成二维码前先验证 URL 的合法性(检查协议、域名等),拒绝生成指向已知恶意域名的二维码。

第八 音频

声音是人类最古老的信息载体之一。从口头语言到音乐艺术,从电话通信到语音识别,音频技术贯穿了人类文明的发展历程。在数字时代,音频被编码为二进制数据,在计算机中存储、传输和处理。本章将探讨声音的本质、音频数字化原理、常见编码格式,以及如何使用 Rust 进行音频处理。

8.1 声音的本质

8.1.1 声波

声音是一种机械波,由物体振动产生,通过介质(通常是空气)以纵波的形式传播。当声波到达人耳时,引起鼓膜振动,最终被大脑感知为声音。

声波具有三个基本属性:

  • 频率(Frequency):表示声波每秒振动的次数,单位是赫兹(Hz)。频率决定音调的高低。人耳可听范围约为 20 Hz ~ 20,000 Hz。
  • 振幅(Amplitude):表示声波振动的强度,决定声音的响度。振幅越大,声音越响。
  • 波形(Waveform):表示声波的形状,决定声音的音色。不同乐器演奏同一音高时,音色差异源于波形不同。

8.1.2 纯音与复音

纯音是单一频率的正弦波,如音叉发出的声音。复音则由多个不同频率的纯音叠加而成,包含基频和谐波。任何复杂的声音都可以通过傅里叶变换分解为一系列纯音的叠加,这也是音频压缩和合成的基础。

8.2 音频采样与量化

模拟声波是连续的,而计算机只能处理离散的数字信号。将模拟音频转换为数字音频的过程称为模数转换(ADC),包含两个核心步骤:采样和量化。

8.2.1 采样率

采样是在时间轴上对模拟信号进行离散化,每隔固定时间间隔记录一次声波的振幅值。每秒采样的次数称为采样率(Sample Rate)

根据奈奎斯特-香农采样定理,为了完整还原原始信号,采样率必须至少是信号最高频率的两倍。因此:

  • 44.1 kHz:CD 音质标准,可还原最高 22.05 kHz 的频率,覆盖人耳全部可听范围。
  • 48 kHz:专业音频和数字视频的标准采样率。
  • 96 kHz / 192 kHz:高解析度音频(Hi-Res),用于音乐制作和发烧级播放。

8.2.2 位深度

量化是在振幅轴上对采样值进行离散化,用有限位数的二进制数表示每个采样点的振幅。**位深度(Bit Depth)**决定了每个采样点使用的位数。

位深度动态范围应用场景
8 bit48 dB早期游戏音效、电话语音
16 bit96 dBCD 音质标准
24 bit144 dB专业录音和制作
32 bit 浮点超过 150 dB音频后期处理

位深度越大,量化误差越小,动态范围越广,信噪比越高。

8.2.3 声道与音频数据量

音频可以按声道数分为单声道(Mono)、立体声(Stereo,双声道),以及多声道环绕声(5.1、7.1 等)。

未经压缩的音频数据量计算公式:

数据量(字节/秒)= 采样率 × 位深度 × 声道数 / 8

以 CD 音质(44.1 kHz、16 bit、立体声)为例:

44100 × 16 × 2 / 8 = 176,400 字节/秒 ≈ 172 KB/s
一分钟约 10 MB,一小时约 600 MB

可见未经压缩的音频数据量相当可观,因此音频压缩编码技术至关重要。

8.3 音频编码格式

音频编码格式分为无损压缩、有损压缩和不压缩三大类。

8.3.1 WAV

WAV(Waveform Audio File Format)是微软和 IBM 联合开发的一种音频文件格式。它通常使用 PCM(Pulse Code Modulation,脉冲编码调制)编码,不压缩或仅使用无损压缩,完整保留原始音频数据。

  • 优点:音质无损、解码简单、兼容性好、适合音频编辑。
  • 缺点:文件体积大。
  • 适用场景:专业音频制作、音效素材存储、需要反复编辑的音频文件。

8.3.2 MP3

MP3(MPEG-1 Audio Layer III)是最广为人知的有损压缩音频格式。它利用人耳的听觉掩蔽效应,去除人耳不易察觉的音频信息,实现大幅压缩。

  • 压缩比:通常可达 1:10 ~ 1:12(128 kbps 时约为 CD 音质的 1/11)。
  • 优点:兼容性好、文件小、普及度极高。
  • 缺点:有损压缩,高频细节有所损失。
  • 适用场景:音乐播放、网络流媒体、便携设备存储。

8.3.3 AAC

AAC(Advanced Audio Coding)是 MP3 的后继者,在相同码率下通常比 MP3 提供更好的音质。

  • 优点:音质优于 MP3、支持多声道、被苹果生态广泛采用。
  • 适用场景:Apple Music、YouTube、数字广播、移动设备。

8.3.4 FLAC

FLAC(Free Lossless Audio Codec)是一种开源的无损压缩音频格式,可将音频文件压缩到原大小的 50%~70%,同时完整保留所有音频信息,解码后可 100% 还原为原始 PCM 数据。

  • 优点:无损压缩、开源免费、支持元数据标签。
  • 缺点:压缩比不如有损格式,文件仍较大。
  • 适用场景:音乐收藏、发烧级音频存储、音频归档。
格式压缩类型典型码率特点
WAV不压缩/无损1411 kbps音质无损,文件最大
FLAC无损压缩700~1000 kbps体积减半,音质无损
AAC有损压缩128~256 kbps效率高,苹果生态主流
MP3有损压缩128~320 kbps最普及,兼容性最好

8.4 Rust 音频处理

Rust 生态系统提供了多个优秀的音频处理库,涵盖音频播放、格式解析、WAV 读写等功能。

8.4.1 使用 rodio 播放音频

rodio 是一个纯 Rust 编写的音频播放库,支持 WAV、MP3、Vorbis、FLAC 等格式,使用简单,适合游戏和应用程序中的音频播放需求。

Cargo.toml 中添加依赖:

[dependencies]
rodio = "0.17"

播放本地音频文件:

use std::fs::File;
use std::io::BufReader;

fn main() {
    // 1. 获取默认音频输出设备
    let (_stream, stream_handle) = rodio::OutputStream::try_default().unwrap();

    // 2. 打开音频文件
    let file = File::open("music.mp3").unwrap();
    let source = rodio::Decoder::new(BufReader::new(file)).unwrap();

    // 3. 播放音频
    stream_handle.play_raw(source.convert_samples()).unwrap();

    // 4. 等待播放完成(示例中休眠 10 秒)
    std::thread::sleep(std::time::Duration::from_secs(10));
}

rodio 还支持音量控制、播放速度调整、音频混合等高级功能:

use rodio::{Decoder, Source};
use std::fs::File;
use std::io::BufReader;

fn main() {
    let (_stream, stream_handle) = rodio::OutputStream::try_default().unwrap();
    let file = File::open("music.wav").unwrap();
    let source = Decoder::new(BufReader::new(file)).unwrap();

    // 调整音量(0.5 表示一半音量)
    let volume_adjusted = source.amplify(0.5);

    // 调整播放速度(1.5 倍速)
    let speed_adjusted = volume_adjusted.speed(1.5);

    stream_handle.play_raw(speed_adjusted.convert_samples()).unwrap();
    std::thread::sleep(std::time::Duration::from_secs(10));
}

8.4.2 使用 symphonia 解析音频

symphonia 是一个纯 Rust 编写的高性能多媒体格式解析库,支持 MP3、AAC、FLAC、WAV、OGG 等多种格式,设计注重正确性和安全性。

Cargo.toml 中添加依赖:

[dependencies]
symphonia = { version = "0.5", features = ["mp3", "flac", "wav"] }

读取音频文件元数据和采样数据:

use symphonia::core::codecs::DecoderOptions;
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
use std::fs::File;

fn main() {
    // 1. 打开文件
    let file = File::open("music.mp3").unwrap();
    let mss = MediaSourceStream::new(Box::new(file), Default::default());

    // 2. 创建提示和选项
    let hint = Hint::new();
    let format_opts: FormatOptions = Default::default();
    let metadata_opts: MetadataOptions = Default::default();
    let decoder_opts: DecoderOptions = Default::default();

    // 3. 探测格式
    let probed = symphonia::default::get_probe()
        .format(&hint, mss, &format_opts, &metadata_opts)
        .unwrap();

    let mut format = probed.format;

    // 4. 获取音频流信息
    let track = format.default_track().unwrap();
    let codec_params = &track.codec_params;

    println!("采样率: {:?}", codec_params.sample_rate);
    println!("声道数: {:?}", codec_params.channels);

    // 5. 创建解码器并读取采样数据
    let mut decoder = symphonia::default::get_codecs()
        .make(codec_params, &decoder_opts)
        .unwrap();

    while let Ok(packet) = format.next_packet() {
        match decoder.decode(&packet) {
            Ok(decoded) => {
                // 处理解码后的音频采样数据
                let samples = decoded.samples();
                println!("解码得到 {} 个采样点", samples.len());
            }
            Err(_) => break,
        }
    }
}

8.4.3 使用 hound 读写 WAV 文件

hound 是一个轻量级的纯 Rust WAV 文件读写库,无外部依赖,适合需要生成或处理 WAV 音频的场景。

Cargo.toml 中添加依赖:

[dependencies]
hound = "3.5"

读取 WAV 文件:

fn main() {
    let reader = hound::WavReader::open("music.wav").unwrap();
    let spec = reader.spec();

    println!("采样率: {}", spec.sample_rate);
    println!("声道数: {}", spec.channels);
    println!("位深度: {}", spec.bits_per_sample);
    println!("样本格式: {:?}", spec.sample_format);

    // 读取所有采样值(假设为 16 位有符号整数)
    let samples: Vec<i16> = reader.into_samples::<i16>()
        .filter_map(|s| s.ok())
        .collect();

    println!("总采样数: {}", samples.len());
}

生成 WAV 文件(生成一个 1 秒的正弦波):

use std::f32::consts::PI;

fn main() {
    let sample_rate = 44100;
    let frequency = 440.0; // A4 音符
    let duration = 1; // 1 秒
    let amplitude = 0.5;

    let spec = hound::WavSpec {
        channels: 1,
        sample_rate,
        bits_per_sample: 16,
        sample_format: hound::SampleFormat::Int,
    };

    let mut writer = hound::WavWriter::create("sine.wav", spec).unwrap();

    for t in 0..sample_rate * duration {
        let sample = (amplitude * (2.0 * PI * frequency * t as f32 / sample_rate as f32).sin())
            * i16::MAX as f32;
        writer.write_sample(sample as i16).unwrap();
    }

    writer.finalize().unwrap();
    println!("已生成 sine.wav");
}

8.4.4 音频可视化:提取波形数据

结合 hound 库,我们可以提取音频数据用于可视化分析:

#![allow(unused)]
fn main() {
fn analyze_audio(path: &str) {
    let reader = hound::WavReader::open(path).unwrap();
    let spec = reader.spec();

    let samples: Vec<i16> = reader.into_samples::<i16>()
        .filter_map(|s| s.ok())
        .collect();

    // 计算每个声道的采样数
    let samples_per_channel = samples.len() / spec.channels as usize;

    // 计算平均振幅(响度近似值)
    let sum: i64 = samples.iter().map(|&s| s.abs() as i64).sum();
    let avg_amplitude = sum as f64 / samples.len() as f64;

    println!("文件: {}", path);
    println!("总采样数: {}", samples.len());
    println!("每声道采样数: {}", samples_per_channel);
    println!("平均振幅: {:.2}", avg_amplitude);

    // 找出最大振幅
    let max_amplitude = samples.iter().map(|&s| s.abs()).max().unwrap();
    println!("最大振幅: {}", max_amplitude);
}
}

8.5 本章小结

概念说明
声波机械纵波,由频率、振幅、波形三要素描述
采样率每秒采样次数,CD 标准为 44.1 kHz
位深度每个采样点的位数,CD 标准为 16 bit
WAV不压缩/无损,音质最好,文件最大
FLAC无损压缩,体积减半,音质无损
MP3有损压缩,最普及,兼容性好
AAC有损压缩,效率优于 MP3
rodioRust 音频播放库,简单易用
symphoniaRust 多媒体格式解析库,功能强大
houndRust WAV 文件读写库,轻量无依赖

练习建议

  1. 使用 hound 库生成不同频率的正弦波,体验频率与音调的关系。
  2. 使用 rodio 播放音频文件,并实现一个简单的播放器(支持暂停、继续、音量调节)。
  3. 编写程序读取 WAV 文件,计算其 RMS(均方根)响度值。
  4. 尝试使用 symphonia 解析 MP3 或 FLAC 文件,提取其元数据(标题、艺术家等)。
  5. 对比同一首音乐的不同格式(WAV、FLAC、MP3)的文件大小,理解压缩率的差异。

第九 视频

视频是现代社会最重要的信息媒介之一。从电影电视到短视频直播,从视频会议到监控安防,视频技术深刻改变了人们获取和传播信息的方式。与静态图片不同,视频是随时间连续变化的图像序列,同时伴随音频流。本章将探讨视频的本质、编码原理、主流编码格式,以及如何使用 Rust 进行视频处理。

9.1 视频的本质

视频本质上是一系列静态图像(称为帧)按照一定速率连续播放,利用人眼的视觉暂留效应产生运动的错觉。衡量视频质量的核心参数有三个:帧率、分辨率和码率。

9.1.1 帧率

**帧率(Frame Rate)**表示每秒显示的图像帧数,单位是 FPS(Frames Per Second)。

  • 24 FPS:电影标准,画面具有电影感。
  • 25 FPS / 30 FPS:传统电视广播标准(PAL / NTSC)。
  • 60 FPS:游戏和高清视频常用,运动画面更流畅。
  • 120 FPS / 240 FPS:高帧率视频,用于慢动作回放和电竞。

帧率越高,运动画面越流畅,但数据量也越大。对于普通观影,24~30 FPS 已足够;对于快速运动场景(如体育赛事、游戏),60 FPS 能显著提升观感。

9.1.2 分辨率

分辨率表示视频画面的像素尺寸,决定了画面的清晰度。

分辨率像素尺寸说明
SD(标清)720 × 480 / 720 × 576传统 DVD 标准
HD(高清)1280 × 720720p
Full HD1920 × 10801080p,目前主流
2K2560 × 14401440p
4K UHD3840 × 2160超高清,四倍于 1080p
8K UHD7680 × 4320超高分辨率,未来趋势

分辨率越高,画面细节越丰富,但像素数量呈指数增长。4K 的像素数是 1080p 的 4 倍,数据量也相应大幅增加。

9.1.3 码率

**码率(Bitrate)**表示视频每秒传输的数据量,单位通常是 kbps 或 Mbps。码率直接决定视频的画质和文件大小。

  • 恒定码率(CBR):码率保持不变,适合网络流媒体。
  • 可变码率(VBR):根据画面复杂度动态调整码率,相同平均码率下画质更好。

以 1080p 视频为例:

码率画质一小时文件大小
5 Mbps一般约 2.2 GB
10 Mbps良好约 4.4 GB
25 Mbps优秀约 11 GB
50 Mbps接近无损约 22 GB

未经压缩的 1080p@60FPS 视频数据量极为惊人:

1920 × 1080 × 3(RGB)× 60 FPS = 373,248,000 字节/秒 ≈ 356 MB/s
一小时约 1.25 TB

这正是视频压缩编码技术存在的根本原因。

9.2 视频编码原理

视频编码的核心目标是在尽可能保持画质的前提下,大幅减少数据量。主要利用两种冗余:空间冗余和时间冗余。

9.2.1 帧内压缩

**帧内压缩(Intra-frame Compression)**利用单帧图像内部的空间冗余。相邻像素通常具有相似的颜色值,通过以下技术实现压缩:

  • 色彩空间转换:将 RGB 转换为 YUV(亮度+色度),利用人眼对色度不敏感的特性降低色度采样精度(如 4:2:0)。
  • 离散余弦变换(DCT):将图像块从空间域转换到频率域,能量集中在低频分量。
  • 量化:对高频分量进行较大量化,丢弃人眼不敏感的细节。
  • 熵编码:使用哈夫曼编码或算术编码进一步压缩数据。

只使用帧内压缩的帧称为 I 帧(Intra-coded Frame),可以独立解码,是视频随机访问的锚点。

9.2.2 帧间压缩

**帧间压缩(Inter-frame Compression)**利用相邻帧之间的时间冗余。视频相邻帧通常非常相似,只需记录变化部分即可。

  • P 帧(Predicted Frame):参考前面的 I 帧或 P 帧,只编码差异部分和运动矢量。
  • B 帧(Bidirectional Predicted Frame):同时参考前后帧,压缩率最高,但解码需要更多计算。

典型的视频帧序列如:I P B B P B B I P B B ...

帧间压缩是视频编码效率的关键。一个 I 帧可能占用数兆字节,而 P 帧和 B 帧通常只有几十到几百 KB。

9.2.3 运动估计与补偿

帧间压缩的核心技术是运动估计。编码器将画面划分为宏块(如 16×16 像素),在参考帧中搜索最佳匹配位置,记录运动矢量(位移偏移量)。解码时根据运动矢量从参考帧“搬运“像素块,再叠加残差数据还原当前帧。

运动估计的计算量巨大,是视频编码中最耗时的环节,也是硬件加速的重点。

9.3 视频编码格式

9.3.1 H.264 / AVC

H.264(又称 AVC,Advanced Video Coding)是目前最广泛使用的视频编码标准,由 ITU-T 和 ISO/IEC 联合制定。

  • 发布时间:2003 年。
  • 压缩效率:相比前代 MPEG-2,同等画质下码率降低约 50%。
  • 特点:兼容性好、专利授权成熟、硬件解码支持最广泛。
  • 应用场景:网络视频(YouTube、Bilibili)、蓝光光盘、视频监控、视频会议。
  • Profile 等级
    • Baseline:低复杂度,适合视频通话和移动设备。
    • Main:平衡画质和复杂度,用于标清广播。
    • High:最高画质,用于高清和蓝光。

9.3.2 H.265 / HEVC

H.265(又称 HEVC,High Efficiency Video Coding)是 H.264 的继任者。

  • 发布时间:2013 年。
  • 压缩效率:同等画质下码率比 H.264 降低约 50%,即 1080p 的码率可看 4K 画质。
  • 特点:支持更高分辨率(8K)、更高效的帧间预测、更大的编码单元(CTU 最大 64×64)。
  • 缺点:专利授权复杂且昂贵,导致普及速度不及预期。
  • 应用场景:4K 蓝光、Apple 设备、部分流媒体平台。

9.3.3 AV1

AV1(AOMedia Video 1)是由开放媒体联盟(AOMedia)主导开发的开源免版税视频编码格式。

  • 发布时间:2018 年。
  • 压缩效率:同等画质下码率比 H.265 低约 20%~30%,比 H.264 低约 50%。
  • 特点:完全开源免版税、由 Google、Mozilla、Netflix 等巨头推动。
  • 缺点:编码速度较慢(正在快速改善),硬件解码支持仍在普及中。
  • 应用场景:YouTube、Netflix、WebRTC、浏览器视频。
编码格式发布时间压缩效率专利授权硬件支持主要应用
H.2642003基准收费最广泛全网通用
H.2652013比 H.264 提升 50%昂贵较广泛4K 内容
AV12018比 H.265 提升 20%~30%免版税growing流媒体/Web

9.4 Rust 视频处理

Rust 在视频处理领域虽然不如 Python 或 C++ 生态成熟,但已有可用的库和绑定,特别是通过 FFI 调用 FFmpeg 这一业界标准工具。

9.4.1 FFmpeg 简介

FFmpeg 是开源多媒体处理的事实标准,支持几乎所有音视频格式的编解码、转码、流媒体处理。它由 C 语言编写,提供了命令行工具和 C 库(libavcodec、libavformat、libavutil 等)。

Rust 社区提供了 FFmpeg 的绑定库 ffmpeg-next,使得在 Rust 中调用 FFmpeg 功能成为可能。

Cargo.toml 中添加依赖:

[dependencies]
ffmpeg-next = "6.1"

9.4.2 使用 ffmpeg-next 读取视频信息

use ffmpeg_next as ffmpeg;

fn main() {
    // 初始化 FFmpeg
    ffmpeg::init().unwrap();

    // 打开视频文件
    let mut ictx = ffmpeg::format::input(&"video.mp4").unwrap();

    // 遍历所有流,找到视频流
    for (stream_index, stream) in ictx.streams().enumerate() {
        let codec = stream.codec();
        match codec.medium() {
            ffmpeg::media::Type::Video => {
                let video = codec.decoder().video().unwrap();
                println!("视频流 #{}:", stream_index);
                println!("  编码格式: {:?}", video.id());
                println!("  分辨率: {}x{}", video.width(), video.height());
                println!("  帧率: {}/{}",
                    stream.rate().0, stream.rate().1);
                println!("  总帧数: {}", stream.frames());
            }
            ffmpeg::media::Type::Audio => {
                let audio = codec.decoder().audio().unwrap();
                println!("音频流 #{}:", stream_index);
                println!("  采样率: {}", audio.rate());
                println!("  声道数: {}", audio.channels());
            }
            _ => {}
        }
    }
}

9.4.3 使用 ffmpeg-next 解码视频帧

use ffmpeg_next as ffmpeg;
use ffmpeg::software::scaling::{context::Context, flag::Flags};
use ffmpeg::util::frame::video::Video;

fn main() {
    ffmpeg::init().unwrap();

    let mut ictx = ffmpeg::format::input(&"video.mp4").unwrap();

    // 找到第一个视频流
    let input_stream = ictx.streams()
        .best(ffmpeg::media::Type::Video)
        .expect("未找到视频流");
    let stream_index = input_stream.index();

    // 创建解码器
    let context_decoder = ffmpeg::codec::context::Context::from_parameters(input_stream.parameters()).unwrap();
    let mut decoder = context_decoder.decoder().video().unwrap();

    // 创建缩放器,将帧转换为 RGB24 格式
    let mut scaler = Context::get(
        decoder.format(),
        decoder.width(),
        decoder.height(),
        ffmpeg::format::Pixel::RGB24,
        decoder.width(),
        decoder.height(),
        Flags::BILINEAR,
    ).unwrap();

    // 读取数据包并解码
    for (stream, packet) in ictx.packets() {
        if stream.index() == stream_index {
            decoder.send_packet(&packet).unwrap();

            let mut decoded = Video::empty();
            while decoder.receive_frame(&mut decoded).is_ok() {
                let mut rgb_frame = Video::empty();
                scaler.run(&decoded, &mut rgb_frame).unwrap();

                println!("解码一帧: {}x{}", rgb_frame.width(), rgb_frame.height());

                // 此处可以对 rgb_frame.data(0) 中的 RGB 像素数据进行处理
                // 例如保存为图片、进行图像识别等
            }
        }
    }

    // 刷新解码器
    decoder.send_eof().unwrap();
}

9.4.4 使用 ffmpeg-next 进行视频转码

use ffmpeg_next as ffmpeg;

fn main() {
    ffmpeg::init().unwrap();

    // 打开输入文件
    let mut ictx = ffmpeg::format::input(&"input.mp4").unwrap();

    // 创建输出上下文
    let mut octx = ffmpeg::format::output(&"output.mkv").unwrap();

    // 遍历输入流,在输出中添加对应流
    for (stream_index, istream) in ictx.streams().enumerate() {
        let codec_id = istream.codec().id();
        let mut ostream = octx.add_stream(codec_id).unwrap();
        ostream.set_parameters(istream.parameters()).unwrap();
    }

    octx.write_header().unwrap();

    // 复制数据包
    for (stream, mut packet) in ictx.packets() {
        let istream_index = stream.index();
        let ostream = octx.stream(istream_index).unwrap();

        packet.set_stream(ostream.index());
        packet.rescale_ts(stream.time_base(), ostream.time_base());
        packet.write_interleaved(&mut octx).unwrap();
    }

    octx.write_trailer().unwrap();
    println!("转码完成: input.mp4 -> output.mkv");
}

9.4.5 rav1e:Rust 原生的 AV1 编码器

rav1e 是由 Xiph.Org 基金会开发的 AV1 编码器,使用 Rust 和 Assembly 编写,是 Rust 视频编码领域的重要项目。

// rav1e 基本使用示例(API 可能随版本变化)
use rav1e::config::Config;
use rav1e::encoder::EncoderStatus;

fn main() {
    // 创建编码配置
    let mut cfg = Config::default();
    cfg.enc.width = 1920;
    cfg.enc.height = 1080;
    cfg.enc.bit_depth = 8;
    cfg.enc.time_base = rav1e::data::Rational { num: 1, den: 30 }; // 30 FPS

    // 创建编码器
    let mut ctx = cfg.new_context().unwrap();

    // 发送帧进行编码
    // ctx.send_frame(frame).unwrap();

    // 刷新编码器
    ctx.flush();

    // 接收编码后的数据包
    loop {
        match ctx.receive_packet() {
            Ok(packet) => {
                println!("收到编码包,大小: {} 字节", packet.data.len());
                // 将 packet.data 写入文件或传输
            }
            Err(EncoderStatus::LimitReached) => break,
            Err(EncoderStatus::Encoded) => continue,
            Err(e) => {
                println!("编码错误: {:?}", e);
                break;
            }
        }
    }
}

rav1e 展示了 Rust 在性能敏感型多媒体处理领域的潜力,其内存安全特性对于处理大规模视频数据尤为重要。

9.4.6 相关 Rust 库

库名说明
ffmpeg-nextFFmpeg 的 Rust 绑定,功能全面
rav1eRust 编写的 AV1 编码器
image图像处理,可用于视频帧处理
wgpu跨平台 GPU 计算,可用于视频渲染

9.5 本章小结

概念说明
帧率每秒显示帧数,电影 24 FPS,高清 60 FPS
分辨率画面像素尺寸,1080p、4K、8K 等
码率每秒数据量,决定画质和文件大小
I 帧帧内压缩,可独立解码的完整帧
P 帧前向预测帧,参考前面帧编码
B 帧双向预测帧,参考前后帧,压缩率最高
H.264最广泛使用的视频编码标准
H.265H.264 继任者,4K 时代主流
AV1开源免版税编码格式,未来趋势
FFmpeg业界标准多媒体处理工具
ffmpeg-nextFFmpeg 的 Rust 绑定库
rav1eRust 原生 AV1 编码器

练习建议

  1. 使用 ffmpeg-next 编写程序,读取一个视频文件的完整元数据(分辨率、帧率、码率、编码格式、时长等)。
  2. 使用 ffmpeg-next 提取视频中的每一帧,保存为 PNG 图片序列。
  3. 对比同一视频分别用 H.264、H.265、AV1 编码后的文件大小和画质差异。
  4. 编写程序计算视频的平均码率和峰值码率。
  5. 了解 Rust 的 vpx-encodex264 绑定,尝试进行视频编码。

第十章 万物皆可编码

10.1 概述

“万物皆可编码”——这并非一句夸张的口号,而是计算机科学的基石。从最底层的电信号高低(0和1),到我们日常使用的文字、图片、音频、视频,再到复杂的文档格式、网络协议、数据库结构,无一不是编码的产物。

所谓编码,就是将信息从一种形式转换为另一种形式的过程。在计算机的世界里,一切信息最终都会被转换为二进制序列——由0和1组成的比特流。正如香农(Claude Shannon)在1948年的划时代论文《通信的数学理论》中所揭示的:信息是可以被量化和编码的

编码的层次可以概括为以下递进关系:

$$ \text{数据} \xrightarrow{\text{类型化}} \text{类型} \xrightarrow{\text{抽象化}} \text{对象} \xrightarrow{\text{持久化}} \text{文件} \xrightarrow{\text{标准化}} \text{协议} $$

  • 数据:最原始的比特流,没有语义。
  • 类型:赋予数据以语义,如“这是一个整数“或“这是一个字符串“。
  • 对象:将类型与行为绑定,形成具有状态和操作的实体。
  • 文件:将对象持久化到存储介质中。
  • 协议:在文件格式之上建立标准化的交换规则。

本章将从基本类型出发,逐步深入到面向对象编程、唯一标识符、文件系统、文件格式,最终探讨万物编码的哲学意义。

10.2 基本类型与编码

正如之前章节的介绍,通过编码技术,我们可以实现对数字、文字、符号、时间等数据进行编码以便在计算机中进行各种处理。因此才有了整型、浮点型、字符串、数组、切片、哈希表、元组等基本类型,这些构成了编程语言的类型系统。

Rust 基本类型一览

Rust基本类型包括:

类型分类具体类型内存大小说明
整数i8, i16, i32, i64, i1281/2/4/8/16 字节有符号整数
整数u8, u16, u32, u64, u1281/2/4/8/16 字节无符号整数
浮点f32, f644/8 字节IEEE 754 浮点数
布尔bool1 字节truefalse
字符char4 字节Unicode 标量值
元组(T1, T2, ...)各元素之和固定长度复合类型
数组[T; N]size_of::<T>() * N固定长度同类型集合
切片&[T]胖指针(2个usize)动态长度视图
字符串String / &str可变UTF-8 编码字节数组

类型在内存中的编码方式

整数的编码采用补码(Two’s Complement)表示法。对于 $n$ 位有符号整数,其取值范围为:

$$ [-2^{n-1},\ 2^{n-1} - 1] $$

例如,i8 的取值范围是 $[-128, 127]$,i32 的取值范围是 $[-2^{31},\ 2^{31}-1]$。

浮点数的编码遵循 IEEE 754 标准。以 f32 为例,其 32 位由三部分组成:

$$ \text{float} = (-1)^S \times 2^{E-127} \times (1 + M) $$

其中 $S$ 为符号位(1 bit),$E$ 为指数位(8 bits),$M$ 为尾数位(23 bits)。

字符串的编码在 Rust 中采用 UTF-8 编码。UTF-8 是一种变长编码方案:

  • ASCII 字符(U+0000 ~ U+007F):1 字节
  • 拉丁扩展字符(U+0080 ~ U+07FF):2 字节
  • 大部分常用汉字(U+0800 ~ U+FFFF):3 字节
  • Emoji 等罕见字符(U+10000 ~ U+10FFFF):4 字节
fn main() {
    // 整数在内存中的表示
    let a: i32 = -1;
    println!("i32 -1 的内存表示: {:032b}", a as u32); // 补码:全1

    // 浮点数的精度
    let pi: f64 = std::f64::consts::PI;
    println!("PI = {:.15}", pi);

    // 字符与字符串的UTF-8编码
    let ch = '中';
    println!("字符 '{}' 的Unicode码点: U+{:04X}", ch, ch as u32);
    println!("字符 '{}' 的UTF-8字节: {:?}", ch, ch.len_utf8()); // 3字节

    let s = String::from("Hello 世界");
    println!("字符串 '{}' 的字节数: {}", s, s.len());       // 12字节
    println!("字符串 '{}' 的字符数: {}", s, s.chars().count()); // 7个字符
}

10.3 面向对象与编码

类与对象的概念

“物以类聚,人以群分。” 面向对象编程(Object-Oriented Programming, OOP)通过抽象方法,提取同类事物的关键信息,形成“类“(Class)。类是对象的蓝图或模板,而对象是类的具体实例。

面向对象编程的三大核心特性:

特性英文含义
封装Encapsulation将数据与操作绑定,隐藏内部实现细节
继承Inheritance子类复用父类的属性和行为
多态Polymorphism同一接口,不同实现

对象标识符(OID)

按照 GB/T 17969.1 (ISO/IEC 9834-1) 的定义,对象是指“通信和信息处理世界中的任何事物,它是可标识(可以命名)的,同时它可被注册“。对象标识符(Object Identifier,OID)是与对象相关联的用来无歧义地标识对象的全局唯一的值,可保证对象在通信与信息处理中正确地定位和管理。通俗地讲,OID 就是网络通信中对象的身份证。

对象标识符 ObjectIdentifier OID

OID 采用点分十进制表示,例如 1.3.6.1.5.5.7.1.1 表示“权威信息访问“(Authority Information Access)证书扩展。

Rust 中的面向对象

Rust 并非传统的面向对象语言,但通过 structenumtrait 可以优雅地实现面向对象的核心思想:

use std::fmt;

// struct 替代"类"——封装数据
struct Person {
    name: String,
    age: u32,
}

// impl 块——封装行为(方法)
impl Person {
    fn new(name: &str, age: u32) -> Self {
        Person {
            name: name.to_string(),
            age,
        }
    }

    fn greet(&self) {
        println!("你好,我是{},今年{}岁。", self.name, self.age);
    }
}

// trait 替代"接口"——实现多态
trait Describable {
    fn describe(&self) -> String;
}

impl Describable for Person {
    fn describe(&self) -> String {
        format!("Person(name={}, age={})", self.name, self.age)
    }
}

// enum 实现代数数据类型
enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
            Shape::Rectangle { width, height } => width * height,
            Shape::Triangle { base, height } => 0.5 * base * height,
        }
    }
}

impl fmt::Display for Shape {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Shape::Circle { radius } => write!(f, "Circle(r={})", radius),
            Shape::Rectangle { width, height } => write!(f, "Rect(w={}, h={})", width, height),
            Shape::Triangle { base, height } => write!(f, "Tri(b={}, h={})", base, height),
        }
    }
}

fn main() {
    let p = Person::new("张三", 30);
    p.greet();
    println!("{}", p.describe());

    let shapes = vec![
        Shape::Circle { radius: 5.0 },
        Shape::Rectangle { width: 4.0, height: 6.0 },
        Shape::Triangle { base: 3.0, height: 8.0 },
    ];

    for s in &shapes {
        println!("{} 的面积 = {:.2}", s, s.area());
    }
}

10.4 唯一标识符(ID)

同一类型,拥有数以亿计的对象,如何区分这些独一无二的个体呢?其实要做到这一点并不难,只要给这些个体一个全局唯一的“身份“即可。身份证号、手机号、IP地址、统一社会信用代码——这些都是唯一标识符的现实应用。

10.4.1 身份证号校验

中国居民身份证号码为18位,其中前17位为本体码,最后1位为校验码。校验码的计算基于加权因子取模11的算法:

$$ C = \left( \sum_{i=1}^{17} a_i \times w_i \right) \bmod 11 $$

其中 $a_i$ 为第 $i$ 位数字,$w_i$ 为对应的加权因子 $[7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]$。校验码映射表为:

余数012345678910
校验码10X98765432
#![allow(unused)]
fn main() {
use lazy_static::lazy_static;
use regex::Regex;
use std::collections::HashMap;

lazy_static! {
    static ref IDENTIFIER_REGEX: Regex = Regex::new("^([0-9ABCDEFGY]{1})([1239]{1})([0-9ABCDEFGHJKLMNPQRTUWXY]{6})([0-9ABCDEFGHJKLMNPQRTUWXY]{9})([0-9ABCDEFGHJKLMNPQRTUWXY])$").unwrap();
    static ref REGEX_18_ID_CARD_NO: Regex = Regex::new(r"^[0-9]{17}[0-9X]$").unwrap();
    static ref ID_CARD_POWER: Vec<usize> = vec![7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
    static ref WEIGHT: Vec<usize> = vec![1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28];
    static ref VERIFY_CODE_MAP: HashMap<char,usize> = {
        let verify_code = "0123456789ABCDEFGHJKLMNPQRTUWXY";
        let verify_code_vec :Vec<char> = verify_code.chars().collect();
        let mut verify_code_map = HashMap::new();
        for (i,item) in verify_code_vec.iter().enumerate().take(verify_code.len()){
            verify_code_map.insert(*item,i);

        }
        println!("{:?}",verify_code_map);
        verify_code_map
    };
    // 公司名称需要排除的字符串,不包含中英文括号
    static ref REGEX_NOT_COMPANY_NAME: Regex = Regex::new(r###"[`~!@#$%^&*+=|{}':;',\\.<>《》/?~!@#¥%……&*——+|\-{}\[\]【】';:""'。,、?]"###).unwrap();
    // 金钱(千分位)
    static ref MONEY_REGEX: Regex = Regex::new(r"^(-)?\d{1,3}(,\d{3})*(.\d+)?$").unwrap();
}

/// 18位身份证号校验
fn is_18_id_card(id_card_no: &str) -> bool {
    if !REGEX_18_ID_CARD_NO.is_match(id_card_no) {
        println!("{} isn't match regex", id_card_no);
        return false;
    }

    let id_card_no_chars: Vec<char> = id_card_no.chars().collect();
    let mut sum = 0;
    for i in 0..17 {
        let ch = id_card_no_chars[i] as usize - 48;
        sum += ch * ID_CARD_POWER[i];
    }

    let check_code = match sum % 11 {
        10 => '2',
        9 => '3',
        8 => '4',
        7 => '5',
        6 => '6',
        5 => '7',
        4 => '8',
        3 => '9',
        2 => 'X',
        1 => '0',
        0 => '1',
        _ => 'N',
    };

    check_code == id_card_no_chars[17]
}


let id_card_no = "450101200012314321";
if is_18_id_card(id_card_no) {
    println!("{} is a valid ID card number", id_card_no);
} else {
    println!("{} is not a valid ID card number", id_card_no);
}


}

更多代码

10.4.2 统一社会信用代码校验

统一社会信用代码是18位的标识代码,同样采用加权因子校验算法。上述代码中的 IDENTIFIER_REGEXWEIGHTVERIFY_CODE_MAP 即为统一社会信用代码校验所需的核心数据:

  • IDENTIFIER_REGEX:匹配18位统一社会信用代码的格式
  • WEIGHT:加权因子数组 $[1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28]$
  • VERIFY_CODE_MAP:校验字符映射表(不包含 I、O、Z、S、V)

10.4.3 UUID

UUID(Universally Unique Identifier,通用唯一识别码)是一个128位的标识符,通常以32个十六进制数字表示,格式为 8-4-4-4-12,例如:

550e8400-e29b-41d4-a716-446655440000

UUID 的理论空间为 $2^{128} \approx 3.4 \times 10^{38}$,足够为地球上的每一粒沙子分配一个唯一标识符。

UUID 版本对比:

版本生成方式特点
v1时间戳 + MAC地址基于时间,可排序,但暴露MAC地址
v3MD5 哈希(命名空间+名称)确定性生成,相同输入产生相同UUID
v4随机数最常用,完全随机
v5SHA-1 哈希(命名空间+名称)确定性生成,比v3更安全
use uuid::{Uuid, Version, Variant};

fn main() {
    // 生成 v4 UUID(随机)
    let uuid_v4 = Uuid::new_v4();
    println!("UUID v4: {}", uuid_v4);
    println!("版本: {:?}", uuid_v4.get_version()); // Some(Version::Random)
    println!("变体: {:?}", uuid_v4.get_variant()); // Some(Variant::RFC4122)

    // 生成 v1 UUID(基于时间戳)
    let uuid_v1 = Uuid::new_v1(&[1, 2, 3, 4, 5, 6]);
    println!("UUID v1: {}", uuid_v1);

    // 从字符串解析 UUID
    let parsed = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000");
    match parsed {
        Ok(u) => println!("解析成功: {}, 版本: {:?}", u, u.get_version()),
        Err(e) => println!("解析失败: {}", e),
    }

    // 生成 v5 UUID(基于命名空间和名称的确定性UUID)
    let namespace = Uuid::NAMESPACE_DNS;
    let uuid_v5 = Uuid::new_v5(namespace, b"example.com");
    println!("UUID v5 (example.com): {}", uuid_v5);
    // 相同输入始终产生相同结果
    let uuid_v5_again = Uuid::new_v5(namespace, b"example.com");
    assert_eq!(uuid_v5, uuid_v5_again);
    println!("确定性验证: v5 UUID 相同输入产生相同结果 ✓");
}

Cargo.toml 依赖:

[dependencies]
uuid = { version = "1", features = ["v1", "v4", "v5"] }

10.4.4 雪花算法(Snowflake)

雪花算法是 Twitter 开源的分布式ID生成算法,其核心思想是:将64位的 i64 划分为多个段,每段代表不同的语义信息

$$ \text{Snowflake ID} = \underbrace{\text{符号位}}{1\text{bit}} \ | \ \underbrace{\text{时间戳}}{41\text{bits}} \ | \ \underbrace{\text{机器ID}}{10\text{bits}} \ | \ \underbrace{\text{序列号}}{12\text{bits}} $$

位数说明
符号位1 bit始终为0,保证ID为正数
时间戳41 bits毫秒级时间戳,可用约69年
机器ID10 bits最多支持1024台机器
序列号12 bits同一毫秒内最多4096个ID

41位时间戳的表示范围:

$$ 2^{41} - 1 = 2199023255551 \text{ 毫秒} \approx 69.73 \text{ 年} $$

use std::sync::atomic::{AtomicU16, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

/// 雪花算法ID生成器
struct Snowflake {
    machine_id: u16,          // 机器ID (0 ~ 1023)
    sequence: AtomicU16,      // 序列号 (0 ~ 4095)
    last_timestamp: AtomicU64, // 上次生成ID的时间戳
    epoch: u64,               // 起始时间戳(毫秒)
}

impl Snowflake {
    fn new(machine_id: u16) -> Self {
        // 自定义纪元:2024-01-01 00:00:00 UTC
        let epoch = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_millis() as u64;

        Snowflake {
            machine_id: machine_id & 0x3FF, // 确保不超过10位
            sequence: AtomicU16::new(0),
            last_timestamp: AtomicU64::new(0),
            epoch,
        }
    }

    fn next_id(&self) -> i64 {
        let current_timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_millis() as u64;

        let last = self.last_timestamp.load(Ordering::SeqCst);

        if current_timestamp == last {
            // 同一毫秒内,递增序列号
            let seq = self.sequence.fetch_add(1, Ordering::SeqCst);
            if seq >= 4095 {
                // 序列号溢出,等待下一毫秒
                panic!("Sequence overflow: too many IDs in one millisecond");
            }
            self.make_id(last, seq)
        } else {
            // 新的毫秒,重置序列号
            self.sequence.store(0, Ordering::SeqCst);
            self.last_timestamp.store(current_timestamp, Ordering::SeqCst);
            self.make_id(current_timestamp, 0)
        }
    }

    fn make_id(&self, timestamp: u64, sequence: u16) -> i64 {
        // 时间戳左移22位(10位机器ID + 12位序列号)
        let ts_part = (timestamp - self.epoch) << 22;
        // 机器ID左移12位(12位序列号)
        let machine_part = (self.machine_id as i64) << 12;
        // 序列号
        let seq_part = sequence as i64;

        ts_part | machine_part | seq_part
    }
}

fn main() {
    let snowflake = Snowflake::new(1); // 机器ID为1

    // 连续生成10个ID
    for _ in 0..10 {
        let id = snowflake.next_id();
        println!("Snowflake ID: {}", id);
    }

    // 解析ID
    let id = snowflake.next_id();
    let timestamp_part = (id >> 22) + snowflake.epoch as i64;
    let machine_part = ((id >> 12) & 0x3FF) as u16;
    let sequence_part = (id & 0xFFF) as u16;
    println!("\n解析 ID = {}", id);
    println!("  时间戳部分: {}", timestamp_part);
    println!("  机器ID: {}", machine_part);
    println!("  序列号: {}", sequence_part);
}

10.4.5 nanoid

nanoid 是一种轻量级、安全、URL友好的唯一ID生成器。与UUID相比,nanoid生成的ID更短(默认21个字符),且使用URL安全的字符集。

nanoid 的核心思想是:使用密码学安全的随机数生成器,从一个自定义字符集中随机选取字符,组成指定长度的字符串。

use nanoid::nanoid;

fn main() {
    // 生成默认21字符的 nanoid
    let id1 = nanoid!();
    println!("nanoid (默认21字符): {}", id1);

    // 生成指定长度的 nanoid
    let id2 = nanoid!(10);
    println!("nanoid (10字符): {}", id2);

    // 使用自定义字符集
    let id3 = nanoid!(16, &['a', 'b', 'c', 'd', 'e', 'f', '1', '2', '3']);
    println!("nanoid (自定义字符集): {}", id3);
}

Cargo.toml 依赖:

[dependencies]
nanoid = "0.4"

10.4.6 各ID方案对比

方案长度生成方式有序性分布式支持适用场景
自增ID4~8字节数据库自增严格有序需要中心化单体数据库主键
UUID v436字符(含连字符)随机数无序天然支持通用唯一标识
UUID v136字符(含连字符)时间戳+MAC时间有序天然支持需要排序的场景
雪花算法8字节(i64)时间戳+机器ID+序列号趋势递增需分配机器ID分布式系统主键
nanoid21字符(可配置)密码学随机无序天然支持URL、短链接、前端

10.5 一切皆文件

Unix 哲学:一切皆文件

Unix 操作系统有一个优雅的设计哲学——“一切皆文件”(Everything is a file)。在 Unix/Linux 系统中,无论是普通文件、目录、设备(如 /dev/null/dev/tty)、管道、套接字,还是 /proc 下的进程信息,都可以通过统一的文件操作接口(openreadwriteclose)来访问。

这种设计的优势在于:

  • 统一接口:所有资源使用相同的 API 操作
  • 组合性:通过管道将多个程序串联
  • 简洁性:用少量原语解决大量问题

文件读写

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{BufReader, Read, Write};

    // 将文件读取为字节数组
    if let Ok(cipher_data) = std::fs::read("why-rust.crypto") {
        let plaintext = cipher
            .decrypt(nonce, cipher_data.as_ref())
            .expect("decryption failure!");
        println!("{}", String::from_utf8(plaintext).unwrap());
    }

    /// 逐行读取文件
    #[test]
    fn read_file_lines() {
        match File::open("why-rust.txt") {
            Ok(f) => {
                let reader = BufReader::new(f);
                let lines = reader.lines();
                for line in lines.map(|x| x.unwrap()) {
                    println!("{}", line);
                }
            }
            Err(e) => panic!("can't open this file :{}", e),
        }
    }

    ///将文件读取为一个字符串
    match std::fs::read_to_string("provinces.json") {
        Ok(data) => {
            println!("open provinces.json...");
            let v: Vec<AdministrativeDivisions> = serde_json::from_str(&data).unwrap();

            for ad in v {
                println!("{:?}={:?}", ad.code, ad.name);
            }
        }
        Err(e) => panic!("can't open this file : {}", e),
    }

    // 将字节数组写入文件
    let mut all_bytes = Vec::<u8>::with_capacity(128);
    all_bytes.extend_from_slice(&key);
    all_bytes.extend_from_slice(&iv);
    all_bytes.extend_from_slice(&encrypt_bytes);

    std::fs::write("poem.crypto", &all_bytes);
}

文件编码:文本文件 vs 二进制文件

文件从编码角度可分为两大类:

类型特征读取方式示例
文本文件可读字符编码(UTF-8等)read_to_string().txt, .csv, .json, .md
二进制文件任意字节序列read().exe, .png, .pdf, .xlsx

文本文件本质上是“对人友好“的二进制文件——它们同样是由字节组成的,只不过这些字节按照特定的字符编码(如 UTF-8、GBK)可以被人类直接阅读。而二进制文件则采用特定格式编码,需要专门的解析器才能理解其内容。

10.6 文件格式与编码

文件格式本质

文件格式的本质就是编码规则——约定如何将特定类型的数据组织为字节序列。不论是简单的 txt、xml、json 等格式的数据还是复杂的 doc、xls、pdf、OFD 等格式,都是某种编码规则的体现。

办公三件套:Word,Excel,PPT

微软的 Office 办公三件套:Word,Excel,PowerPoint 成为办公领域的标准,每年为微软创造上百亿美元的收入。国内金山办公经过三十多年的追赶,其产品 WPS 终于在办公软件拥有一席之地、站稳了脚跟。

Excel / CSV / JSON 处理

Excel 读取

calamine 是 Rust 实现的 Excel 工具,目前只支持读操作(不支持写操作)。

#![allow(unused)]
fn main() {
use calamine::DataType::{
    Bool, DateTime, DateTimeIso, Duration, DurationIso, Empty, Error, Float, String,
};
use calamine::{
    open_workbook, open_workbook_auto, Ods, Reader, Sheet, SheetType, SheetVisible, Xls, Xlsb, Xlsx,
};
use calamine::{CellErrorType::*, DataType};

#[test]
fn any_sheets_xlsx() {
    let path = format!(
        "{}/examples/file/any_sheets.xlsx",
        env!("CARGO_MANIFEST_DIR")
    );
    println!("{}", &path);
    let mut workbook: Xlsx<_> = open_workbook(path).unwrap();

    let range = workbook.worksheet_range("Visible").unwrap();
    let total_cells = range.get_size().0 * range.get_size().1;
    let non_empty_cells: usize = range.used_cells().count();
    println!(
        "Found {} cells in 'Sheet1', including {} non empty cells",
        total_cells, non_empty_cells
    );
    // alternatively, we can manually filter rows
    assert_eq!(
        non_empty_cells,
        range
            .rows()
            .flat_map(|r| r.iter().filter(|&c| c != &DataType::Empty))
            .count()
    );
}

}

CSV 读写

CSV(Comma-Separated Values)是最简单的结构化文本格式,用逗号分隔各列数据。

use csv::{Reader, Writer, ReaderBuilder, WriterBuilder};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct RocketRecord {
    #[serde(rename = "发射序号")]
    serial_no: u32,
    #[serde(rename = "发射日期")]
    launch_date: String,
    #[serde(rename = "发射地点")]
    launch_site: String,
    #[serde(rename = "运载火箭")]
    rocket: String,
    #[serde(rename = "卫星/航天器")]
    satellite: String,
}

/// 写入CSV文件
fn write_csv() -> Result<(), Box<dyn std::error::Error>> {
    let mut wtr = WriterBuilder::new()
        .has_headers(true)
        .from_path("rocket_records.csv")?;

    let records = vec![
        RocketRecord {
            serial_no: 1,
            launch_date: "2024-01-17".to_string(),
            launch_site: "酒泉卫星发射中心".to_string(),
            rocket: "长征二号F".to_string(),
            satellite: "天舟七号".to_string(),
        },
        RocketRecord {
            serial_no: 2,
            launch_date: "2024-03-21".to_string(),
            launch_site: "文昌航天发射场".to_string(),
            rocket: "长征八号".to_string(),
            satellite: "鹊桥二号".to_string(),
        },
    ];

    for record in &records {
        wtr.serialize(record)?;
    }
    wtr.flush()?;
    println!("CSV 文件写入成功!");
    Ok(())
}

/// 读取CSV文件
fn read_csv() -> Result<(), Box<dyn std::error::Error>> {
    let mut rdr = ReaderBuilder::new()
        .flexible(true)
        .from_path("rocket_records.csv")?;

    for result in rdr.deserialize() {
        let record: RocketRecord = result?;
        println!("{:?}", record);
    }
    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    write_csv()?;
    println!("--- 读取CSV ---");
    read_csv()?;
    Ok(())
}

Cargo.toml 依赖:

[dependencies]
csv = "1.3"
serde = { version = "1", features = ["derive"] }

JSON 序列化与反序列化

JSON(JavaScript Object Notation)是当今最流行的数据交换格式。在 Rust 生态中,serde_json 是处理 JSON 的事实标准。

use serde::{Deserialize, Serialize};
use serde_json::{json, Value, from_str, to_string_pretty};

#[derive(Debug, Serialize, Deserialize)]
struct Province {
    code: String,
    name: String,
    level: u8,
    #[serde(skip_serializing_if = "Option::is_none")]
    parent_code: Option<String>,
}

/// 序列化:Rust 结构体 → JSON 字符串
fn serialize_example() -> Result<(), Box<dyn std::error::Error>> {
    let provinces = vec![
        Province {
            code: "110000".to_string(),
            name: "北京市".to_string(),
            level: 1,
            parent_code: None,
        },
        Province {
            code: "110101".to_string(),
            name: "东城区".to_string(),
            level: 2,
            parent_code: Some("110000".to_string()),
        },
    ];

    // 序列化为紧凑JSON
    let json_str = serde_json::to_string(&provinces)?;
    println!("紧凑JSON: {}", json_str);

    // 序列化为美化JSON
    let json_pretty = serde_json::to_string_pretty(&provinces)?;
    println!("美化JSON:\n{}", json_pretty);

    // 动态构建JSON
    let data = json!({
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "properties": { "name": "北京市", "code": "110000" },
                "geometry": { "type": "Point", "coordinates": [116.4, 39.9] }
            }
        ]
    });
    println!("动态JSON: {}", data);
    Ok(())
}

/// 反序列化:JSON 字符串 → Rust 结构体
fn deserialize_example() -> Result<(), Box<dyn std::error::Error>> {
    let json_data = r#"
    [
        {"code": "440000", "name": "广东省", "level": 1},
        {"code": "440100", "name": "广州市", "level": 2, "parent_code": "440000"}
    ]
    "#;

    let provinces: Vec<Province> = from_str(json_data)?;
    for p in &provinces {
        println!("{} - {} (level: {})", p.code, p.name, p.level);
    }

    // 反序列化为动态 Value
    let v: Value = from_str(json_data)?;
    if let Some(arr) = v.as_array() {
        for item in arr {
            println!("名称: {}", item["name"]);
        }
    }
    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("=== 序列化 ===");
    serialize_example()?;
    println!("\n=== 反序列化 ===");
    deserialize_example()?;
    Ok(())
}

Cargo.toml 依赖:

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

PDF / OFD 格式简介

  • PDF (Portable Document Format) 可携带文件格式,由 Adobe 公司于1992年发布,已成为全球通用的文档交换标准。
  • OFD 版式文档国家标准,是中国自主研发的电子文件格式,旨在替代 PDF 在政务和电子公文领域的应用。

Pandas vs Polars

特性Polars (Rust)Pandas (Python)
语言RustPython
并行执行原生多线程单线程(部分操作可用并行)
惰性求值支持不支持
内存占用较低(Arrow格式)较高
性能极高中等
生态成熟度快速增长中非常成熟
学习曲线较平缓中等

10.7 万物编码的哲学

DIKW 金字塔

从数据到智慧的递进,可以用 DIKW 金字塔来描述:

$$ \text{Data(数据)} \xrightarrow{\text{赋予含义}} \text{Information(信息)} \xrightarrow{\text{总结规律}} \text{Knowledge(知识)} \xrightarrow{\text{洞察本质}} \text{Wisdom(智慧)} $$

层级含义编码示例
Data原始事实,无上下文42, "hello", 0xFF
Information有组织、有意义的数据{"temp": 42, "unit": "°C"}
Knowledge信息之间的关系与模式if temp > 37 then fever
Wisdom运用知识做出判断该患者需要就医

编码贯穿了 DIKW 的每一个层级:

  • 数据层:二进制编码(ASCII、UTF-8、IEEE 754)
  • 信息层:结构化编码(JSON、XML、CSV)
  • 知识层:逻辑编码(规则引擎、知识图谱)
  • 智慧层:这一层或许已经超出了“编码“的范畴——智慧涉及直觉、经验和创造力,这些是否可以被编码,至今仍是哲学与人工智能领域的开放问题。

编码的边界:什么不能被编码?

尽管“万物皆可编码“是一个强大的理念,但编码并非万能。以下是一些编码的边界:

  1. 主观体验(Qualia):你看到的“红色“和我看到的“红色“是同一种感觉吗?这种主观体验难以被精确编码。
  2. 情感与直觉:虽然可以用文字描述情感,但情感的微妙之处(如“淡淡的忧伤“)很难被完全编码。
  3. 无限精度:实数 $\pi$ 的精确值无法被有限编码完全表示,只能用近似值。
  4. 创造性思维:编码可以表达规则,但真正的创造力(如贝多芬的交响曲)似乎超越了纯粹的编码。
  5. 意识本身:意识是否可以被编码为算法?这是心智哲学中最深刻的未解之谜之一。

正如哲学家维特根斯坦所言:“对于不可言说之物,必须保持沉默。“编码的边界,或许正是人类智慧与机器智能的分界线。

10.8 总结

本章从“万物皆可编码“的理念出发,系统介绍了编码的各个层次。

编码层次对比

层次核心概念编码方式Rust 体现
数据比特流二进制 0/1u8, Vec<u8>
类型语义化数据IEEE 754, UTF-8, 补码i32, f64, char, String
对象数据+行为struct/enum/traitstruct, impl, trait
文件持久化存储文件格式规范std::fs, File
协议标准化交换通信协议规范HTTP, TCP, JSON

ID 方案对比

方案长度有序性分布式性能适用场景
自增ID4~8字节严格有序需中心化极高单体数据库
UUID v436字符无序天然支持中等通用标识
雪花算法8字节趋势递增需分配ID分布式主键
nanoid21字符无序天然支持URL/前端

10.9 练习题

练习1:编写一个 Rust 函数,接收一个 u8 值,分别输出其二进制表示、八进制表示和十六进制表示,并解释补码如何表示负数。

练习2:使用 structtrait 实现一个简单的“动物“体系:定义 Animal trait(包含 speak 方法),并为 DogCatBird 三个结构体分别实现该 trait。

练习3:编写一个函数,验证给定的18位身份证号码是否合法。要求支持批量验证,并统计合法与不合法的数量。

练习4:使用 uuid crate 生成 1000 个 UUID v4,统计其中以 0 开头的 UUID 数量,并验证其是否接近理论概率 $1/16$。

练习5:实现一个简化版的雪花算法 ID 生成器,要求支持自定义机器ID,并验证生成的 ID 是否全局唯一(生成 10000 个 ID 检查是否有重复)。

练习6:使用 csvserde crate,读取一个 CSV 文件,筛选出满足特定条件的记录,并将结果写入新的 CSV 文件。

练习7:使用 serde_json 实现以下功能:将一个 Rust 结构体序列化为 JSON 字符串,再从 JSON 字符串反序列化为 Rust 结构体,验证数据的完整性。

练习8:思考题:有人说“一切皆可编码“,但也有人认为“意识不可编码“。请结合本章内容和 DIKW 金字塔,谈谈你对“编码的边界“的看法。字数不少于300字。

第十一章 数据的序列化、反序列化、持久化、可视化

11.1 概述

什么是序列化与反序列化

序列化(Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

序列化(编码)是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久化等;而反序列化(解码)则是将从网络、磁盘等读取的字节数组还原成原始对象,主要用于网络传输对象的解码,以便完成远程调用。

反序列化的最重要的作用:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。

为什么需要序列化

在计算机系统中,数据存在于内存中时以结构化的对象形式存在,但当我们需要将数据跨进程、跨机器、跨时间传递时,就必须将其转换为一种统一的字节流格式。序列化解决的核心问题是:

场景说明示例
网络传输不同机器间传递数据,需将对象转为字节流RPC调用、REST API
数据持久化将内存中的对象保存到磁盘,以便后续恢复配置文件、数据库记录
进程间通信同一机器上不同进程间交换数据管道、消息队列
深拷贝通过序列化/反序列化实现对象的完整复制缓存快照

序列化的完整流程

一个典型的数据序列化与传输流程可以表示为:

$$ \text{原始对象} \xrightarrow{\text{编码}} \text{字节流} \xrightarrow{\text{传输}} \text{字节流} \xrightarrow{\text{解码}} \text{重建对象} $$

用数学语言描述,设原始对象为 $O$,序列化函数为 $S$,传输通道为 $T$,反序列化函数为 $D$,则完整过程为:

$$ O’ = D(T(S(O))) $$

理想情况下,我们希望 $O’ = O$,即反序列化后的对象与原始对象完全一致。这要求序列化格式能够无损地表达原始数据的所有信息。

序列化和反序列化的底层实现原理是什么?

11.2 常见序列化格式

常见的序列化数据格式有:JSONXML、BSON、YAML、TOML 等。每种格式都有其适用的场景和优缺点。

11.2.1 JSON

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,由 Douglas Crockford 在 2001 年提出。它基于 JavaScript 的对象字面量语法,但独立于任何编程语言,几乎所有的现代编程语言都支持 JSON。

JSON 的核心特点:

  • 轻量级:相比 XML,JSON 没有冗余的标签,数据体积更小
  • 可读性好:人类可以直接阅读和编写
  • 广泛支持:几乎所有语言和平台都内置了 JSON 支持
  • 树形结构:支持嵌套的对象和数组

JSON 与 Rust 数据类型映射:

JSON 类型Rust 类型示例
number (整数)i32, i64, u32, u6442
number (浮点)f32, f643.14
stringString, &str"hello"
booleanbooltrue / false
nullOption<T> (None)null
arrayVec<T>, [T; N][1, 2, 3]
objectstruct, HashMap<K, V>{"key": "value"}
use serde::Serialize;

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Person {
    first_name: String,
    last_name: String,
}

fn main() {
    let person = Person {
        first_name: "Graydon".to_string(),
        last_name: "Hoare".to_string(),
    };

    let json = serde_json::to_string_pretty(&person).unwrap();

    // Prints:
    //
    //    {
    //      "firstName": "Graydon",
    //      "lastName": "Hoare"
    //    }
    println!("{}", json);
}

11.2.2 XML

XML(eXtensible Markup Language,可扩展标记语言)是一种自描述性强的标记语言,由 W3C 于 1998 年标准化。XML 使用标签来描述数据的结构和语义,在 SOAP、SVG、HTML 等领域有广泛应用。

XML 的核心特点:

  • 自描述性强:标签本身携带语义信息
  • 严格的语法规范:必须有根元素、标签必须闭合
  • 支持命名空间:避免标签名冲突
  • 适合文档型数据:如配置文件、文档交换

JSON vs XML 对比:

特性JSONXML
数据体积较小较大(标签冗余)
可读性较好一般(标签较多)
解析速度较慢
数据类型原生支持需要 Schema 定义
注释支持不支持支持 <!-- -->
命名空间不支持支持
适用场景Web API、配置文档交换、SOAP

在 Rust 中,可以使用 quick-xml 库来处理 XML 数据:

use quick_xml::events::Event;
use quick_xml::Reader;
use std::io::BufReader;

fn main() {
    let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
    <config>
        <database>
            <host>localhost</host>
            <port>5432</port>
            <name>mydb</name>
        </database>
        <server>
            <host>0.0.0.0</host>
            <port>8080</port>
        </server>
    </config>"#;

    let mut reader = Reader::from_reader(xml.as_bytes());
    let mut buf = Vec::new();
    let mut depth = 0;

    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Start(e)) => {
                depth += 1;
                println!("{:indent$}<{}>", "", e.name(), indent = depth * 2);
            }
            Ok(Event::Empty(e)) => {
                println!("{:indent$}<{} />", "", e.name(), indent = depth * 2);
            }
            Ok(Event::Text(e)) => {
                let text = e.unescape().unwrap();
                if !text.trim().is_empty() {
                    println!("{:indent$}{}", "", text.trim(), indent = depth * 2 + 2);
                }
            }
            Ok(Event::End(e)) => {
                depth -= 1;
                println!("{:indent$}</{}>", "", e.name(), indent = depth * 2);
            }
            Ok(Event::Eof) => break,
            Err(e) => eprintln!("Error: {:?}", e),
            _ => {}
        }
        buf.clear();
    }
}

11.2.3 YAML

YAML(YAML Ain’t Markup Language)是一种人类友好的数据序列化格式,广泛用于配置文件(如 Kubernetes 的资源清单、CI/CD 配置文件等)。

YAML 的核心特点:

  • 缩进表示层级:使用空格缩进而非花括号
  • 人类友好:语法简洁,接近自然语言
  • 支持注释:使用 # 添加注释
  • 支持复杂数据类型:锚点(&)和别名(*)实现引用
# 应用配置
server:
  host: 0.0.0.0
  port: 8080
  workers: 4

database:
  url: "postgres://user:pass@localhost/mydb"
  pool_size: 10
  timeout: 30s

logging:
  level: info
  format: json

在 Rust 中,可以使用 serde_yaml 来处理 YAML:

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Config {
    server: Server,
    database: Database,
    logging: Logging,
}

#[derive(Debug, Deserialize)]
struct Server {
    host: String,
    port: u16,
    workers: usize,
}

#[derive(Debug, Deserialize)]
struct Database {
    url: String,
    pool_size: u32,
    timeout: String,
}

#[derive(Debug, Deserialize)]
struct Logging {
    level: String,
    format: String,
}

fn main() {
    let yaml = r#"
server:
  host: 0.0.0.0
  port: 8080
  workers: 4

database:
  url: "postgres://user:pass@localhost/mydb"
  pool_size: 10
  timeout: 30s

logging:
  level: info
  format: json
"#;

    let config: Config = serde_yaml::from_str(yaml).unwrap();
    println!("Server: {}:{}", config.server.host, config.server.port);
    println!("Database pool: {}", config.database.pool_size);
    println!("Log level: {}", config.logging.level);
}

11.2.4 TOML

TOML(Tom’s Obvious, Minimal Language)由 GitHub 联合创始人 Tom Preston-Werner 创建,是一种语义明确、易于阅读的配置文件格式。Rust 社区对 TOML 有着天然的支持——Cargo.toml 就是 TOML 格式的配置文件。

TOML 的核心特点:

  • 语义明确:类型清晰,不会产生歧义
  • INI 风格:使用 [section]key = value 的形式
  • Rust 生态首选:Cargo.toml 就是 TOML
  • 支持多种数据类型:字符串、整数、浮点数、布尔值、日期时间、数组、内联表
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

[profile.release]
opt-level = 3
lto = true

在 Rust 中,可以使用 toml crate 来解析 TOML:

use serde::Deserialize;
use std::fs;

#[derive(Debug, Deserialize)]
struct AppConfig {
    package: Package,
    dependencies: std::collections::HashMap<String, toml::Value>,
}

#[derive(Debug, Deserialize)]
struct Package {
    name: String,
    version: String,
    edition: String,
}

fn main() {
    let content = fs::read_to_string("Cargo.toml").unwrap();
    let config: AppConfig = toml::from_str(&content).unwrap();
    println!("项目名称: {}", config.package.name);
    println!("版本: {}", config.package.version);

    for (name, value) in &config.dependencies {
        println!("依赖 {}: {:?}", name, value);
    }
}

11.2.5 BSON

BSON(Binary JSON)是一种二进制形式的 JSON 序列化格式,由 MongoDB 开发。BSON 扩展了 JSON 的数据类型,支持日期时间、二进制数据、正则表达式等。

BSON 的主要优势在于:

  • 二进制格式:解析速度比文本格式的 JSON 更快
  • 扩展数据类型:支持 DateBinaryObjectIdRegex
  • MongoDB 原生格式:MongoDB 内部使用 BSON 存储和传输数据

11.2.6 Protocol Buffers(Protobuf)

Protocol Buffers(简称 Protobuf)是 Google 开发的高效二进制序列化协议。与 JSON/XML 不同,Protobuf 需要先定义 .proto 模式文件,然后通过编译器生成各语言的代码。

// person.proto
syntax = "proto3";

message Person {
    string name = 1;
    int32 age = 2;
    repeated string emails = 3;
}

Protobuf 的核心优势:

  • 极致性能:二进制编码,体积小、解析快
  • 强类型约束:通过 .proto 文件定义数据结构
  • 向前/向后兼容:字段编号机制保证兼容性
  • 跨语言支持:支持 C++、Java、Python、Go、Rust 等多种语言

11.2.7 各格式对比总结

格式可读性解析速度数据体积数据类型适用场景
JSON基本类型Web API、前后端交互
XML基本类型文档交换、SOAP、SVG
YAML基本类型配置文件、K8s 清单
TOML基本类型+日期Rust/Cargo 配置
BSON扩展类型MongoDB 存储
Protobuf极快极小强类型高性能 RPC、微服务

11.3 Rust 中的 Serde 生态

11.3.1 Serde 框架概述

Serde 是 Rust 生态中最核心的序列化/反序列化框架,其名字来源于 SErialize / DEserialize 的缩写。Serde 采用了一种优雅的设计:将数据结构的定义与序列化格式解耦,通过 trait 抽象实现“一次定义,多种格式输出“。

Serde 的架构可以用以下公式表达:

$$ \text{Data} \xleftrightarrow{\text{Serialize/Deserialize}} \text{Serializer/Deserializer} \xleftrightarrow{\text{Format}} \text{JSON/XML/YAML/…} $$

11.3.2 核心 Trait

Serde 定义了两个核心 trait:

  • Serialize:将 Rust 数据结构转换为通用的中间表示
  • Deserialize:从通用的中间表示重建 Rust 数据结构

这两个 trait 是 Serde 生态的基石,所有数据格式(JSON、XML、YAML 等)都基于这两个 trait 实现。

11.3.3 常用派生宏

在大多数场景下,我们不需要手动实现 SerializeDeserialize,只需使用 #[derive] 派生宏即可:

#![allow(unused)]
fn main() {
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct User {
    id: u64,
    name: String,
    email: Option<String>,
    roles: Vec<String>,
}
}

11.3.4 常用属性

Serde 提供了丰富的属性来控制序列化行为:

属性说明示例
#[serde(rename_all = "camelCase")]批量重命名所有字段first_name -> firstName
#[serde(rename = "name")]重命名单个字段字段名映射
#[serde(skip)]跳过序列化/反序列化敏感字段
#[serde(skip_serializing)]仅跳过序列化密码字段
#[serde(skip_deserializing)]仅跳过反序列化计算字段
#[serde(default)]反序列化时使用默认值可选字段
#[serde(flatten)]扁平化嵌套结构合并层级
#[serde(with = "module")]自定义序列化模块特殊格式
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct ApiResponse {
    status_code: u16,
    message: String,
    #[serde(skip_serializing)]
    debug_info: String,
    #[serde(default)]
    retry_after: Option<u32>,
    #[serde(flatten)]
    metadata: Metadata,
}

#[derive(Serialize, Deserialize, Debug)]
struct Metadata {
    request_id: String,
    timestamp: i64,
}

fn main() {
    let resp = ApiResponse {
        status_code: 200,
        message: "OK".to_string(),
        debug_info: "internal trace".to_string(),
        retry_after: None,
        metadata: Metadata {
            request_id: "abc-123".to_string(),
            timestamp: 1700000000,
        },
    };

    let json = serde_json::to_string_pretty(&resp).unwrap();
    println!("{}", json);
}

11.3.5 Person 序列化示例

以下是一个完整的 Person 结构体序列化示例:

use serde::Serialize;

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Person {
    first_name: String,
    last_name: String,
}

fn main() {
    let person = Person {
        first_name: "Graydon".to_string(),
        last_name: "Hoare".to_string(),
    };

    let json = serde_json::to_string_pretty(&person).unwrap();

    // Prints:
    //
    //    {
    //      "firstName": "Graydon",
    //      "lastName": "Hoare"
    //    }
    println!("{}", json);
}

11.3.6 自定义 Deserialize 实现

在某些场景下,派生宏无法满足需求(如自定义数据格式、条件解析等),此时需要手动实现 Deserialize trait。以下是一个完整的 Duration 结构体自定义反序列化实现:

use std::fmt;
use serde::Serialize;
use serde::de::{self, Deserialize, Deserializer, Visitor, SeqAccess, MapAccess};

#[allow(dead_code)]
#[derive(Serialize)]
struct Duration {
    secs: u64,
    nanos: u32,
}

impl Duration {
    fn new(secs: u64, nanos: u32) -> Self {
        Duration{
            secs,
            nanos
        }
    }
}

impl<'de> Deserialize<'de> for Duration {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        enum Field { Secs, Nanos }

        // This part could also be generated independently by:
        //
        //    #[derive(Deserialize)]
        //    #[serde(field_identifier, rename_all = "lowercase")]
        //    enum Field { Secs, Nanos }
        impl<'de> Deserialize<'de> for Field {
            fn deserialize<D>(deserializer: D) -> Result<Field, D::Error>
            where
                D: Deserializer<'de>,
            {
                struct FieldVisitor;

                impl<'de> Visitor<'de> for FieldVisitor {
                    type Value = Field;

                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                        formatter.write_str("`secs` or `nanos`")
                    }

                    fn visit_str<E>(self, value: &str) -> Result<Field, E>
                    where
                        E: de::Error,
                    {
                        match value {
                            "secs" => Ok(Field::Secs),
                            "nanos" => Ok(Field::Nanos),
                            _ => Err(de::Error::unknown_field(value, FIELDS)),
                        }
                    }
                }

                deserializer.deserialize_identifier(FieldVisitor)
            }
        }

        struct DurationVisitor;

        impl<'de> Visitor<'de> for DurationVisitor {
            type Value = Duration;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("struct Duration")
            }

            fn visit_seq<V>(self, mut seq: V) -> Result<Duration, V::Error>
            where
                V: SeqAccess<'de>,
            {
                let secs = seq.next_element()?
                    .ok_or_else(|| de::Error::invalid_length(0, &self))?;
                let nanos = seq.next_element()?
                    .ok_or_else(|| de::Error::invalid_length(1, &self))?;
                Ok(Duration::new(secs, nanos))
            }

            fn visit_map<V>(self, mut map: V) -> Result<Duration, V::Error>
            where
                V: MapAccess<'de>,
            {
                let mut secs = None;
                let mut nanos = None;
                while let Some(key) = map.next_key()? {
                    match key {
                        Field::Secs => {
                            if secs.is_some() {
                                return Err(de::Error::duplicate_field("secs"));
                            }
                            secs = Some(map.next_value()?);
                        }
                        Field::Nanos => {
                            if nanos.is_some() {
                                return Err(de::Error::duplicate_field("nanos"));
                            }
                            nanos = Some(map.next_value()?);
                        }
                    }
                }
                let secs = secs.ok_or_else(|| de::Error::missing_field("secs"))?;
                let nanos = nanos.ok_or_else(|| de::Error::missing_field("nanos"))?;
                Ok(Duration::new(secs, nanos))
            }
        }

        const FIELDS: &'static [&'static str] = &["secs", "nanos"];
        deserializer.deserialize_struct("Duration", FIELDS, DurationVisitor)
    }
}

fn main(){
    let duration = Duration::new(10u64,140_000_000u32);
    let json = serde_json::to_string_pretty(&duration).unwrap();
    println!("{}",json);
    
    if let Ok(duration2) = serde_json::from_str::<Duration>(&json){
        println!("secs:{}, nanos:{}",duration2.secs, duration2.nanos); 
    }
    
}

11.3.7 serde_json 常用 API

API说明返回类型
serde_json::to_string(&val)序列化为紧凑 JSON 字符串Result<String>
serde_json::to_string_pretty(&val)序列化为格式化 JSON 字符串Result<String>
serde_json::to_vec(&val)序列化为字节向量Result<Vec<u8>>
serde_json::to_writer(writer, &val)序列化并写入 WriterResult<()>
serde_json::from_str::<T>(&str)从字符串反序列化Result<T>
serde_json::from_slice::<T>(&[u8])从字节切片反序列化Result<T>
serde_json::from_reader::<T, R>(reader)从 Reader 反序列化Result<T>
serde_json::Value动态 JSON 值类型枚举类型

11.3.8 错误处理

Serde 的错误处理通过 serde_json::Error 类型实现,它提供了丰富的错误信息:

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Config {
    name: String,
    port: u16,
}

fn main() {
    let json = r#"{"name": "test", "port": "abc"}"#;
    
    match serde_json::from_str::<Config>(json) {
        Ok(config) => println!("配置加载成功: {:?}", config),
        Err(e) => {
            eprintln!("配置解析失败: {}", e);
            // 输出详细错误分类
            if e.is_syntax() {
                eprintln!("  -> JSON 语法错误");
            } else if e.is_data() {
                eprintln!("  -> 数据类型不匹配");
            } else if e.is_eof() {
                eprintln!("  -> JSON 数据不完整");
            }
        }
    }
}

11.3.9 相关库参考

Rust 库:

Java 库(对比参考):

  • FasterXML — Java 高性能 XML/JSON 处理
  • jackson — Java 多格式数据处理
  • hessian — 二进制 RPC 协议

FastJson小技巧——@JSONField的史上最全最详细讲解——一看就会

11.4 数据持久化

11.4.1 什么是持久化

持久化(Persistence),即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。持久化的主要应用是将内存中的对象存储在数据库中,或者存储在磁盘文件中、XML 数据文件中等等。

从信息论的角度看,持久化是将易失性存储(内存,$T_{volatile} \approx 10^{-9}$ 秒访问延迟)中的数据转移到非易失性存储(磁盘/SSD,$T_{nonvolatile} \approx 10^{-6}$ 秒访问延迟)的过程:

$$ \text{Memory Data} \xrightarrow{\text{Serialize}} \text{Byte Stream} \xrightarrow{\text{Write}} \text{Persistent Storage} $$

11.4.2 持久化方式

方式说明适用场景Rust 代表库
文件存储JSON/TOML/YAML 等配置文件应用配置、小型数据serde_json, toml
关系型数据库SQL 数据库,结构化存储业务数据、事务处理sea-orm, sqlx
NoSQL 数据库键值存储、文档存储高并发、灵活 Schemaredis, mongodb
对象存储S3 兼容的分布式存储大文件、媒体资源minio, rustfs

11.4.3 文件存储示例

文件存储是最简单的持久化方式,适合配置文件和小型数据集:

use serde::{Serialize, Deserialize};
use std::fs;

#[derive(Serialize, Deserialize, Debug)]
struct AppSettings {
    theme: String,
    language: String,
    max_connections: u32,
    auto_save: bool,
}

fn save_settings(settings: &AppSettings, path: &str) -> std::io::Result<()> {
    let json = serde_json::to_string_pretty(settings).unwrap();
    fs::write(path, json)
}

fn load_settings(path: &str) -> std::io::Result<AppSettings> {
    let json = fs::read_to_string(path)?;
    let settings: AppSettings = serde_json::from_str(&json).unwrap();
    Ok(settings)
}

fn main() {
    let settings = AppSettings {
        theme: "dark".to_string(),
        language: "zh-CN".to_string(),
        max_connections: 100,
        auto_save: true,
    };

    save_settings(&settings, "settings.json").unwrap();
    println!("配置已保存");

    let loaded = load_settings("settings.json").unwrap();
    println!("加载的配置: {:?}", loaded);
}

11.4.4 数据库存储

sea-orm 是 Rust 生态中优秀的异步 ORM 框架,基于 SQLx 构建,提供了类型安全的数据库操作接口。

以下是一个使用 SQLite 进行数据持久化的示例:

use rusqlite::{Connection, Result};
use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: Option<i64>,
    name: String,
    email: String,
    age: u32,
}

fn init_db(conn: &Connection) -> Result<()> {
    conn.execute(
        "CREATE TABLE IF NOT EXISTS users (
            id    INTEGER PRIMARY KEY AUTOINCREMENT,
            name  TEXT NOT NULL,
            email TEXT NOT NULL UNIQUE,
            age   INTEGER NOT NULL
        )",
        [],
    )?;
    Ok(())
}

fn insert_user(conn: &Connection, user: &User) -> Result<i64> {
    conn.execute(
        "INSERT INTO users (name, email, age) VALUES (?1, ?2, ?3)",
        [&user.name, &user.email, &user.age.to_string()],
    )?;
    Ok(conn.last_insert_rowid())
}

fn query_users(conn: &Connection) -> Result<Vec<User>> {
    let mut stmt = conn.prepare("SELECT id, name, email, age FROM users")?;
    let user_iter = stmt.query_map([], |row| {
        Ok(User {
            id: Some(row.get(0)?),
            name: row.get(1)?,
            email: row.get(2)?,
            age: row.get(3)?,
        })
    })?;

    let mut users = Vec::new();
    for user in user_iter {
        users.push(user?);
    }
    Ok(users)
}

fn main() -> Result<()> {
    let conn = Connection::open("users.db")?;
    init_db(&conn)?;

    let user = User {
        id: None,
        name: "张三".to_string(),
        email: "zhangsan@example.com".to_string(),
        age: 28,
    };

    let id = insert_user(&conn, &user)?;
    println!("插入用户成功,ID: {}", id);

    let users = query_users(&conn)?;
    let json = serde_json::to_string_pretty(&users).unwrap();
    println!("所有用户:\n{}", json);

    Ok(())
}

11.4.5 对象存储

对象存储是一种适合存储海量非结构化数据(图片、视频、日志文件等)的存储方案,兼容 Amazon S3 API。

  • MINIO — 全球增长最快的对象存储系统
  • RustFS — 世界上增长最快的分布式对象存储
#![allow(unused)]
fn main() {
// 使用 minio-rs 上传文件的简要示例
// Cargo.toml: minio = "0.2"

use minio::s3::args::*;
use minio::s3::Client;

async fn upload_file(
    client: &Client,
    bucket: &str,
    object_name: &str,
    file_path: &str,
) {
    let args = PutObjectArgs::new(bucket, object_name, file_path);
    client.put_object(args).await.unwrap();
    println!("文件 {} 上传成功", object_name);
}
}

11.5 数据可视化

11.5.1 可视化的意义

数据可视化(Data Visualization)是将数据转化为图形或图像的过程,目的是让数据“说话“,帮助人们更直观地理解数据中隐藏的模式、趋势和关联。

在数据分析中,可视化扮演着不可替代的角色:

  • 趋势分析:股价走势、气温变化、降雨量统计
  • 对比分析:不同产品销量对比、部门绩效对比
  • 分布分析:用户年龄分布、收入分布
  • 关联分析:变量之间的相关性
  • 实时监控:服务器负载、系统健康度、网络流量

11.5.2 常见可视化类型

类型用途示例场景
折线图展示趋势变化股价走势、气温变化
柱状图展示分类对比各部门销售额、月度收入
饼图展示占比分布市场份额、预算分配
散点图展示相关性身高与体重关系
热力图展示密度分布用户活跃时段、地理热力
地图展示地理分布销售区域分布、疫情分布

11.5.3 Rust 可视化库

Rust 生态中有多个可视化库可供选择:

特点适用场景
plotters纯 Rust 绑图库,支持 SVG/PNG/BMP服务端绑图、报告生成
plotly交互式图表,基于 Plotly.jsWeb 可视化、仪表盘
egui即时模式 GUI 框架桌面应用内嵌图表
icedElm 架构 GUI 框架跨平台桌面应用

11.5.4 plotters 折线图示例

plotters 是 Rust 中最流行的纯绑图库,无需外部依赖,支持多种输出格式:

use plotters::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let root = BitMapBackend::new("line_chart.png", (800, 600))
        .into_drawing_area();
    root.fill(&WHITE)?;
    
    // 创建绑图区域
    let mut chart = ChartBuilder::on(&root)
        .caption("月度销售额趋势", ("Microsoft YaHei", 30))
        .x_label_area_size(40)
        .y_label_area_size(60)
        .margin(10)
        .build_cartesian_2d(1u32..12u32, 0f64..100f64)?;

    chart.configure_mesh()
        .x_desc("月份")
        .y_desc("销售额(万元)")
        .draw()?;

    // 绘制折线
    let data: Vec<(u32, f64)> = vec![
        (1, 25.0), (2, 32.0), (3, 28.0), (4, 45.0),
        (5, 52.0), (6, 48.0), (7, 61.0), (8, 58.0),
        (9, 72.0), (10, 68.0), (11, 80.0), (12, 95.0),
    ];

    chart.draw_series(LineSeries::new(
        data.iter().map(|&(x, y)| (x, y)),
        &RED,
    ))?
    .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED));

    root.present()?;
    println!("折线图已保存为 line_chart.png");
    Ok(())
}

11.5.5 plotters 柱状图示例

use plotters::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let root = BitMapBackend::new("bar_chart.png", (800, 600))
        .into_drawing_area();
    root.fill(&WHITE)?;

    let mut chart = ChartBuilder::on(&root)
        .caption("各编程语言使用率", ("Microsoft YaHei", 30))
        .x_label_area_size(50)
        .y_label_area_size(60)
        .margin(10)
        .build_cartesian_2d(0usize..5usize, 0f64..40f64)?;

    chart.configure_mesh()
        .x_desc("编程语言")
        .y_desc("使用率 (%)")
        .x_labels(&["Rust", "Python", "Java", "Go", "C++"])
        .draw()?;

    let data: Vec<(usize, f64)> = vec![
        (0, 13.5), (1, 28.1), (2, 16.3), (3, 8.7), (4, 10.2),
    ];

    let colors = [RED, BLUE, GREEN, ORANGE, PURPLE];

    chart.draw_series(
        data.iter().enumerate().map(|(i, &(x, y))| {
            Rectangle::new(
                [(x, 0), (x + 1, y)],
                colors[i].filled(),
            )
        }),
    )?;

    root.present()?;
    println!("柱状图已保存为 bar_chart.png");
    Ok(())
}

11.6 数据融合

11.6.1 什么是数据融合

数据融合(Data Fusion)是一种将来自不同来源、不同格式或不同结构的数据集成到一个统一的数据模型或数据集中的过程。它不仅仅是简单的数据拼接,而是一个复杂的系统工程,涉及到数据的采集、清洗、转换、整合和存储等多个环节。

数据融合是什么?

11.6.2 ETL 流程

数据融合的核心流程是 ETL(Extract-Transform-Load):

$$ \text{数据源} \xrightarrow{\text{Extract(抽取)}} \text{原始数据} \xrightarrow{\text{Transform(转换)}} \text{清洗后数据} \xrightarrow{\text{Load(加载)}} \text{目标系统} $$

阶段说明关键操作
Extract(抽取)从多个数据源提取数据API 调用、数据库查询、文件读取
Transform(转换)清洗和转换数据去重、格式转换、类型映射、聚合计算
Load(加载)将处理后的数据写入目标批量插入、增量更新、实时流写入

11.6.3 数据清洗与转换

数据融合中最关键的环节是数据清洗和转换,常见操作包括:

  • 去重:删除重复记录,保证数据唯一性
  • 格式统一:日期格式、编码格式、数值精度的统一
  • 缺失值处理:填充默认值、插值或标记为空
  • 类型转换:字符串转数值、时间戳转日期等
  • 数据校验:范围检查、正则匹配、业务规则验证
  • 聚合计算:分组统计、滚动窗口、连接关联
use serde::{Serialize, Deserialize};
use std::collections::HashMap;

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Record {
    id: String,
    name: String,
    value: f64,
    source: String,
}

/// 数据清洗:去重和格式标准化
fn deduplicate(records: Vec<Record>) -> Vec<Record> {
    let mut seen = HashMap::new();
    for record in records {
        seen.entry(record.id.clone())
            .or_insert(record);
    }
    seen.into_values().collect()
}

/// 数据转换:值域映射
fn normalize_values(records: &mut [Record], min: f64, max: f64) {
    let range = max - min;
    for record in records.iter_mut() {
        record.value = (record.value - min) / range;
    }
}

/// 数据融合:合并多个来源的数据
fn merge_sources(sources: Vec<Vec<Record>>) -> Vec<Record> {
    let all: Vec<Record> = sources.into_iter().flatten().collect();
    let mut deduped = deduplicate(all);
    if let (Some(&min_val), Some(&max_val)) = (
        deduped.iter().map(|r| r.value).reduce(f64::min),
        deduped.iter().map(|r| r.value).reduce(f64::max),
    ) {
        normalize_values(&mut deduped, min_val, max_val);
    }
    deduped
}

fn main() {
    let source_a = vec![
        Record { id: "1".into(), name: "Alpha".into(), value: 100.0, source: "A".into() },
        Record { id: "2".into(), name: "Beta".into(), value: 200.0, source: "A".into() },
    ];
    let source_b = vec![
        Record { id: "1".into(), name: "Alpha".into(), value: 100.0, source: "B".into() },
        Record { id: "3".into(), name: "Gamma".into(), value: 300.0, source: "B".into() },
    ];

    let merged = merge_sources(vec![source_a, source_b]);
    println!("融合后共 {} 条记录:", merged.len());
    for r in &merged {
        println!("  {} - {} (normalized: {:.2})", r.id, r.name, r.value);
    }
}

11.7 列式存储与 Apache Arrow

11.7.1 行式存储 vs 列式存储

传统数据库大多采用行式存储(Row-oriented Storage),即一行数据连续存储。而列式存储(Column-oriented Storage)则将每一列的数据连续存储。两种存储方式各有优劣:

特性行式存储列式存储
写入方式逐行写入,适合 OLTP逐列写入,适合 OLAP
读取效率读取整行快读取特定列快
压缩比较低(不同类型数据混合)较高(同类型数据连续)
适用场景事务处理、点查询分析查询、聚合统计
代表系统MySQL、PostgreSQLClickHouse、Arrow

11.7.2 Apache Arrow 概述

Apache Arrow 是一种基于内存的列式数据结构,它的出现就是为了解决系统到系统之间的数据传输问题。在分布式系统内部,每个系统都有自己的内存格式,大量的 CPU 资源被消耗在序列化和反序列化过程中,并且由于每个项目都有自己的实现,没有一个明确的标准,造成各个系统都在重复着复制、转换工作,这种问题在微服务系统架构出现之后更加明显,Arrow 的出现就是为了解决这一问题。

Arrow 的核心优势在于:

  • 零拷贝读取:不同系统之间共享内存,无需序列化/反序列化
  • 列式内存格式:天然适合向量化操作和分析查询
  • 语言无关:支持 C/C++、Java、Python、Rust 等多种语言
  • 标准化:统一的内存格式规范,消除格式转换开销

11.7.3 SIMD 加速

Apache Arrow 的列式内存布局天然适合 SIMD(Single Instruction, Multiple Data)指令集加速。由于同一列的数据类型相同且连续存储,CPU 可以使用一条指令同时处理多个数据元素:

设列中有 $n$ 个元素,标量处理的时间复杂度为 $O(n)$,而 SIMD 处理(假设向量宽度为 $w$)的时间复杂度为 $O(n/w)$,理论上可获得 $w$ 倍的加速比。

11.7.4 DataFusion 查询引擎

DataFusion 是基于 Apache Arrow 构建的查询引擎,用 Rust 编写,提供了 SQL 查询能力和 DataFrame API,适合构建高性能的数据分析系统。

11.7.5 Rust arrow crate 示例

以下示例展示了如何使用 Rust 的 arrow crate 创建和操作列式数据:

use arrow::array::{Float64Array, Int64Array, StringArray};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use std::sync::Arc;

fn main() {
    // 定义 Schema
    let schema = Schema::new(vec![
        Field::new("city", DataType::Utf8, false),
        Field::new("population", DataType::Int64, false),
        Field::new("avg_temperature", DataType::Float64, false),
    ]);

    // 创建列数据
    let cities = StringArray::from(vec![
        "北京", "上海", "广州", "深圳", "杭州",
    ]);
    let populations = Int64Array::from(vec![
        21_540_000, 24_870_000, 18_680_000, 17_560_000, 12_200_000,
    ]);
    let temperatures = Float64Array::from(vec![
        12.6, 16.1, 22.0, 22.4, 16.9,
    ]);

    // 创建 RecordBatch
    let batch = RecordBatch::try_new(
        Arc::new(schema),
        vec![
            Arc::new(cities),
            Arc::new(populations),
            Arc::new(temperatures),
        ],
    ).unwrap();

    println!("RecordBatch 包含 {} 行, {} 列", batch.num_rows(), batch.num_columns());
    println!("Schema: {:?}", batch.schema());

    // 访问列数据
    let temp_col = batch
        .column(2)
        .as_any()
        .downcast_ref::<Float64Array>()
        .unwrap();
    println!("平均温度: {:?}", temp_col.values());
}

11.7.6 相关链接

11.8 总结

序列化格式对比

格式可读性解析速度数据体积扩展性Rust 支持适用场景
JSONserde_jsonWeb API、配置
XMLquick-xml文档交换、SOAP
YAMLserde_yaml配置文件
TOMLtomlCargo 配置
BSONbsonMongoDB
Protobuf极快极小prost高性能 RPC

持久化方案对比

方案类型性能事务支持Rust 代表库适用场景
JSON 文件文件存储serde_json小型配置
SQLite嵌入式数据库ACIDrusqlite本地应用
MySQL关系型数据库ACIDsea-orm, sqlx服务端应用
PostgreSQL关系型数据库ACIDsea-orm, sqlx服务端应用
Oracle关系型数据库ACIDsea-orm, sqlx服务端应用
Redis内存数据库极高有限redis缓存、会话
MinIO/RustFS对象存储最终一致minio大文件存储

Rust 数据生态全景

Rust 在数据处理领域拥有完善的生态体系:

数据获取 ──→ 数据序列化 ──→ 数据存储 ──→ 数据分析 ──→ 数据可视化
   │              │              │              │              │
   ├─ HTTP       ├─ serde       ├─ sea-orm     ├─ arrow       ├─ plotters
   ├─ gRPC       ├─ serde_json  ├─ sqlx        ├─ datafusion  ├─ plotly
   ├─ CSV        ├─ serde_yaml  ├─ redis       ├─ polars      ├─ egui
   ├─ 数据库     ├─ quick-xml   ├─ rusqlite    └─ ndarray     └─ iced
   └─ 文件       └─ toml        └─ minio

11.9 练习题

练习 1:使用 serde_json 将以下 Rust 结构体序列化为 JSON,并要求字段名使用 snake_case,同时跳过 password 字段的序列化。

#![allow(unused)]
fn main() {
struct Account {
    user_id: u64,
    username: String,
    email: String,
    password: String,
    created_at: String,
}
}

练习 2:编写一个程序,读取一个 TOML 配置文件,解析为 Rust 结构体,并打印其中的数据库连接信息。

练习 3:使用 quick-xml 解析以下 XML 数据,提取所有 <book> 标签中的 titleauthor 属性:

<library>
    <book title="Rust编程之道" author="张三"/>
    <book title="Rust实战" author="李四"/>
    <book title="Rust系统编程" author="王五"/>
</library>

练习 4:实现一个简单的 ETL 流程:从 CSV 文件中读取数据(Extract),将价格字段从字符串转换为浮点数(Transform),然后将结果保存为 JSON 文件(Load)。

练习 5:使用 rusqlite 创建一个学生信息表,包含学号、姓名、成绩三个字段,实现插入、查询、更新和删除操作。

练习 6:使用 plotters 库绘制一个散点图,展示以下数据点的分布:

#![allow(unused)]
fn main() {
let data: Vec<(f64, f64)> = vec![
    (1.0, 2.3), (2.0, 4.1), (3.0, 5.8), (4.0, 8.2),
    (5.0, 9.7), (6.0, 12.1), (7.0, 14.5), (8.0, 16.0),
];
}

练习 7:使用 arrow crate 创建一个包含 1000 个随机浮点数的列式数据集,计算其均值和标准差,并与使用普通 Vec<f64> 的计算进行性能对比。

练习 8:设计一个数据融合方案:假设你有两个数据源,一个是 JSON 格式的用户信息(包含 user_id、name),另一个是 CSV 格式的订单信息(包含 user_id、amount、date),请编写程序将两个数据源按 user_id 进行关联,生成一个包含用户名称和订单总额的汇总报告。

第十二 网络

计算机网络是现代信息社会的基石。从浏览网页到发送即时消息,从在线视频到分布式计算,网络无处不在。理解网络的基本原理,并掌握 Rust 中的网络编程技术,是开发高性能网络应用的关键。


一、网络协议分层

计算机网络通信涉及复杂的软硬件协作。为了降低复杂度,人们将网络通信的功能划分为若干层次,每一层只负责特定的任务,上层依赖下层提供的服务。

1.1 OSI 七层模型

OSI(Open Systems Interconnection,开放式系统互联)模型是国际标准化组织(ISO)提出的理论框架,将网络通信分为七个层次:

层次名称功能描述典型协议/设备
第七层应用层为应用程序提供网络服务接口HTTP、HTTPS、FTP、SMTP
第六层表示层数据格式转换、加密解密、压缩解压SSL/TLS、JPEG、ASCII
第五层会话层建立、管理和终止会话NetBIOS、RPC
第四层传输层端到端的数据传输、流量控制、差错恢复TCP、UDP
第三层网络层逻辑寻址、路由选择、分组转发IP、ICMP、路由器
第二层数据链路层物理寻址、帧的封装与传输MAC、以太网、交换机
第一层物理层比特流的物理传输网线、光纤、集线器

记忆口诀: “物数网传会表应”(从下到上)或 “应表会传网数物”(从上到下)。

1.2 TCP/IP 四层模型

在实际工程中,TCP/IP 协议栈更为常用,它将 OSI 的七层简化为四层:

层次名称对应 OSI 层次核心协议
第四层应用层应用层 + 表示层 + 会话层HTTP、FTP、DNS、SSH
第三层传输层传输层TCP、UDP
第二层网络层网络层IP、ICMP、ARP
第一层网络接口层数据链路层 + 物理层以太网、Wi-Fi

TCP/IP 模型更加简洁实用,是互联网实际运行的标准。Rust 网络编程主要关注应用层和传输层。

1.3 数据封装过程

数据在发送时自上而下逐层封装,每一层都会添加自己的头部信息:

应用数据
↓ + HTTP 头
TCP 段
↓ + TCP 头
IP 包
↓ + IP 头
以太网帧
↓ + 帧头帧尾
比特流(物理传输)

接收端则自下而上逐层解封装,最终还原为应用数据。


二、IP 地址与端口

2.1 IP 地址

IP(Internet Protocol)地址是网络中设备的唯一标识。目前主要使用两个版本:

IPv4: 32 位地址,通常表示为四段十进制数,如 192.168.1.1。理论上有约 43 亿个地址,但由于互联网设备激增,IPv4 地址已接近枯竭。

IPv6: 128 位地址,表示为八组十六进制数,如 2001:0db8:85a3::8a2e:0370:7334。地址空间极大,足以给地球上每一粒沙子分配一个地址。

在 Rust 中,IP 地址由 std::net::IpAddr 枚举表示:

use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

fn main() {
    // IPv4 地址
    let v4 = Ipv4Addr::new(192, 168, 1, 1);
    println!("IPv4: {}", v4);  // 192.168.1.1

    // IPv6 地址
    let v6 = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0x1, 0x1);
    println!("IPv6: {}", v6);  // 2001:db8::1:1

    // IpAddr 枚举可以同时持有两种类型
    let addr_v4: IpAddr = v4.into();
    let addr_v6: IpAddr = v6.into();

    println!("是否是 IPv4: {}", addr_v4.is_ipv4());  // true
    println!("是否是 IPv6: {}", addr_v6.is_ipv6());  // true

    // 特殊地址
    println!("本地回环: {}", Ipv4Addr::LOCALHOST);  // 127.0.0.1
    println!("任意地址: {}", Ipv4Addr::UNSPECIFIED);  // 0.0.0.0
}

2.2 端口号

端口号(Port)用于区分同一台主机上的不同网络服务,是一个 16 位无符号整数(0~65535)。

端口范围类型说明
0 ~ 1023知名端口由 IANA 统一分配,如 HTTP(80)、HTTPS(443)、SSH(22)
1024 ~ 49151注册端口需要向 IANA 注册,如 MySQL(3306)、Redis(6379)
49152 ~ 65535动态/私有端口客户端程序临时使用
use std::net::SocketAddrV4;

fn main() {
    // SocketAddr 包含 IP 地址和端口号
    let addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 8080);
    println!("服务器地址: {}", addr);  // 127.0.0.1:8080

    // 常见服务端口
    let common_ports = [
        ("HTTP", 80),
        ("HTTPS", 443),
        ("SSH", 22),
        ("FTP", 21),
        ("DNS", 53),
        ("MySQL", 3306),
        ("Redis", 6379),
    ];

    for (name, port) in &common_ports {
        println!("{} 服务默认端口: {}", name, port);
    }
}

2.3 域名与 DNS

人类更容易记住域名(如 www.example.com),而不是 IP 地址。DNS(Domain Name System)负责将域名解析为 IP 地址。

use std::net::ToSocketAddrs;

fn main() {
    // 将域名解析为 SocketAddr
    let addrs = "www.rust-lang.org:443".to_socket_addrs();

    match addrs {
        Ok(iter) => {
            for addr in iter {
                println!("解析结果: {}", addr);
            }
        }
        Err(e) => println!("解析失败: {}", e),
    }
}

三、TCP 与 UDP

传输层是网络编程的核心,提供了两种主要的传输协议:TCP 和 UDP。

3.1 TCP:传输控制协议

TCP(Transmission Control Protocol)是一种面向连接、可靠、基于字节流的传输协议。

TCP 的核心特性:

特性说明
面向连接通信前需要建立连接(三次握手),通信后需要断开连接(四次挥手)
可靠传输通过确认应答(ACK)、超时重传、滑动窗口等机制保证数据不丢失
按序到达数据包按发送顺序到达接收方
流量控制通过滑动窗口机制防止发送方发送过快
拥塞控制根据网络状况动态调整发送速率

TCP 三次握手:

客户端                    服务器
   |    SYN(seq=x)    |
   | ----------------> |
   |                   |
   |  SYN(seq=y,ACK=x+1)  |
   | <---------------- |
   |                   |
   |    ACK(y+1)      |
   | ----------------> |
   |                   |
   [连接建立,开始传输数据]

适用场景: 文件传输、网页浏览、电子邮件、数据库连接等对可靠性要求高的场景。

3.2 UDP:用户数据报协议

UDP(User Datagram Protocol)是一种无连接、不可靠、基于数据报的传输协议。

UDP 的核心特性:

特性说明
无连接不需要建立连接,直接发送数据
不可靠不保证数据到达,不保证顺序,没有重传机制
低开销头部仅 8 字节,比 TCP 的 20 字节更小
低延迟无需握手等待,适合实时性要求高的场景

适用场景: 视频直播、在线游戏、DNS 查询、实时语音等对延迟敏感、可容忍少量丢包的场景。

3.3 TCP 与 UDP 对比

对比项TCPUDP
连接方式面向连接无连接
可靠性可靠(不丢包、不重复、按序)不可靠
传输效率较低(有握手、确认开销)较高
延迟较高较低
头部大小20 字节8 字节
数据边界字节流(无边界)数据报(保留边界)
拥塞控制
适用场景文件传输、网页、邮件直播、游戏、DNS

四、HTTP 与 HTTPS

4.1 HTTP 协议

HTTP(HyperText Transfer Protocol,超文本传输协议)是互联网上应用最广泛的协议,基于请求-响应模型。

HTTP 请求格式:

GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html

HTTP 响应格式:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1234

<html>...</html>

常见 HTTP 方法:

方法说明幂等性
GET获取资源
POST创建资源
PUT更新资源(完整替换)
PATCH部分更新资源
DELETE删除资源
HEAD获取响应头(不返回体)

常见 HTTP 状态码:

状态码含义说明
200OK请求成功
301/302重定向资源已移动到新的 URL
400Bad Request请求参数错误
401Unauthorized未授权,需要身份验证
403Forbidden禁止访问
404Not Found资源不存在
500Internal Server Error服务器内部错误
502Bad Gateway网关错误
503Service Unavailable服务暂时不可用

4.2 HTTPS 协议

HTTPS(HTTP Secure)是 HTTP 的安全版本,在 HTTP 之下加入了 TLS/SSL 加密层,解决三个核心安全问题:

安全问题解决方案说明
窃听加密传输使用对称加密算法加密数据内容
篡改消息认证码使用 MAC 验证数据完整性
冒充数字证书通过 CA 证书验证服务器身份

HTTPS 的通信过程:

  1. TCP 连接建立(三次握手)
  2. TLS 握手:协商加密算法、交换密钥、验证证书
  3. 加密通信:使用协商好的密钥进行对称加密传输
  4. 连接关闭

本书后续章节(第四十九至五十章)将详细介绍 PKI、TLS 和数字证书的原理与实现。


五、Rust 网络编程

Rust 的标准库 std::net 提供了基础的网络编程能力,而 tokio 等异步运行时则提供了高性能的异步网络编程能力。

5.1 使用 std::net 进行 TCP 编程

TCP 服务端

use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;

fn handle_client(mut stream: TcpStream) {
    let mut buffer = [0u8; 1024];

    loop {
        match stream.read(&mut buffer) {
            Ok(0) => {
                println!("客户端断开连接");
                break;
            }
            Ok(n) => {
                let received = String::from_utf8_lossy(&buffer[..n]);
                println!("收到: {}", received.trim());

                let response = format!("服务端已收到: {}", received.trim());
                if stream.write_all(response.as_bytes()).is_err() {
                    break;
                }
            }
            Err(e) => {
                println!("读取错误: {}", e);
                break;
            }
        }
    }
}

fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:7878")?;
    println!("TCP 服务端已启动,监听 127.0.0.1:7878");

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                println!("新客户端连接: {:?}", stream.peer_addr()?);
                thread::spawn(|| handle_client(stream));
            }
            Err(e) => println!("连接失败: {}", e),
        }
    }

    Ok(())
}

TCP 客户端

use std::io::{self, Read, Write};
use std::net::TcpStream;

fn main() -> std::io::Result<()> {
    let mut stream = TcpStream::connect("127.0.0.1:7878")?;
    println!("已连接到服务端");

    // 发送消息
    let msg = "Hello, Rust Network!\n";
    stream.write_all(msg.as_bytes())?;

    // 读取响应
    let mut buffer = [0u8; 1024];
    let n = stream.read(&mut buffer)?;
    println!("服务端响应: {}", String::from_utf8_lossy(&buffer[..n]));

    Ok(())
}

5.2 使用 std::net 进行 UDP 编程

UDP 编程不需要建立连接,直接使用 UdpSocket 发送和接收数据报。

use std::net::UdpSocket;

fn main() -> std::io::Result<()> {
    // UDP 服务端
    let socket = UdpSocket::bind("127.0.0.1:8080")?;
    println!("UDP 服务端已启动,监听 127.0.0.1:8080");

    let mut buf = [0u8; 1024];

    loop {
        // 接收数据报
        let (amt, src) = socket.recv_from(&mut buf)?;
        let received = String::from_utf8_lossy(&buf[..amt]);
        println!("来自 {} 的消息: {}", src, received.trim());

        // 发送响应
        let response = format!("已收到: {}", received.trim());
        socket.send_to(response.as_bytes(), &src)?;
    }
}
use std::net::UdpSocket;

fn main() -> std::io::Result<()> {
    // UDP 客户端
    let socket = UdpSocket::bind("127.0.0.1:0")?;  // 让系统分配端口
    println!("UDP 客户端已启动");

    let server = "127.0.0.1:8080";
    let msg = "Hello, UDP!";

    // 发送数据报
    socket.send_to(msg.as_bytes(), server)?;
    println!("已发送: {}", msg);

    // 接收响应
    let mut buf = [0u8; 1024];
    let (amt, _src) = socket.recv_from(&mut buf)?;
    println!("收到响应: {}", String::from_utf8_lossy(&buf[..amt]));

    Ok(())
}

5.3 使用 reqwest 进行 HTTP 请求

在实际开发中,我们通常使用成熟的 HTTP 客户端库。reqwest 是 Rust 生态中最流行的 HTTP 客户端库之一。

Cargo.toml 依赖:

[dependencies]
reqwest = { version = "0.11", features = ["blocking", "json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }

异步 GET 请求:

use reqwest;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Post {
    userId: u32,
    id: u32,
    title: String,
    body: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 发送 GET 请求
    let resp = reqwest::get("https://jsonplaceholder.typicode.com/posts/1")
        .await?
        .json::<Post>()
        .await?;

    println!("获取到的文章: {:?}", resp);

    Ok(())
}

POST 请求与自定义请求头:

use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();

    // 构建请求体
    let body = json!({
        "title": "Rust 网络编程",
        "body": "这是一篇关于 Rust 网络编程的文章",
        "userId": 1
    });

    // 发送 POST 请求
    let resp = client
        .post("https://jsonplaceholder.typicode.com/posts")
        .header(CONTENT_TYPE, "application/json")
        .json(&body)
        .send()
        .await?;

    println!("状态码: {}", resp.status());
    println!("响应体: {}", resp.text().await?);

    Ok(())
}

5.4 使用 tokio 进行异步 TCP 编程

对于高并发网络服务,异步编程是更好的选择。tokio 是 Rust 最流行的异步运行时。

Cargo.toml 依赖:

[dependencies]
tokio = { version = "1", features = ["full"] }

异步 TCP 服务端:

use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};

async fn handle_client(mut socket: TcpStream) {
    let mut buf = [0u8; 1024];

    loop {
        match socket.read(&mut buf).await {
            Ok(0) => {
                println!("客户端断开");
                return;
            }
            Ok(n) => {
                let received = String::from_utf8_lossy(&buf[..n]);
                println!("收到: {}", received.trim());

                let response = format!("异步服务端响应: {}", received.trim());
                if socket.write_all(response.as_bytes()).await.is_err() {
                    return;
                }
            }
            Err(e) => {
                println!("读取错误: {}", e);
                return;
            }
        }
    }
}

#[tokio::main]
async fn main() -> tokio::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    println!("异步 TCP 服务端已启动,监听 127.0.0.1:8080");

    loop {
        let (socket, addr) = listener.accept().await?;
        println!("新连接: {}", addr);

        // 每个连接生成一个独立的异步任务
        tokio::spawn(async move {
            handle_client(socket).await;
        });
    }
}

异步 TCP 客户端:

use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;

#[tokio::main]
async fn main() -> tokio::io::Result<()> {
    let mut stream = TcpStream::connect("127.0.0.1:8080").await?;
    println!("已连接到服务端");

    // 异步发送
    stream.write_all(b"Hello, Async Rust!\n").await?;

    // 异步读取
    let mut buf = [0u8; 1024];
    let n = stream.read(&mut buf).await?;
    println!("收到: {}", String::from_utf8_lossy(&buf[..n]));

    Ok(())
}

5.5 简单的 HTTP 服务端

使用 hyper 库可以快速构建 HTTP 服务端:

use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Request, Response, Server};
use std::convert::Infallible;
use std::net::SocketAddr;

async fn hello(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
    Ok(Response::new(Body::from("Hello, Rust HTTP Server!")))
}

#[tokio::main]
async fn main() {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let make_svc = make_service_fn(|_conn| async {
        Ok::<_, Infallible>(service_fn(hello))
    });

    let server = Server::bind(&addr).serve(make_svc);

    println!("HTTP 服务已启动: http://{}", addr);

    if let Err(e) = server.await {
        eprintln!("服务器错误: {}", e);
    }
}

六、总结

概念要点
OSI 七层模型物数网传会表应,理论参考模型
TCP/IP 四层模型网络接口层、网络层、传输层、应用层,实际工程标准
IP 地址IPv4(32 位)与 IPv6(128 位),逻辑寻址
端口号0~65535,区分同一主机上的不同服务
TCP面向连接、可靠传输、按序到达、流量与拥塞控制
UDP无连接、不可靠、低开销、低延迟
HTTP请求-响应模型,GET/POST 等方法,状态码
HTTPSHTTP + TLS/SSL,加密传输、身份验证、完整性保护
std::netRust 标准库提供同步 TCP/UDP 编程能力
tokioRust 异步运行时,适合高并发网络服务
reqwest流行的 HTTP 客户端库,支持异步请求

练习建议:

  1. 使用 std::net::TcpListenerTcpStream 实现一个简单的 echo 服务端(将收到的内容原样返回)
  2. 使用 reqwest 编写程序,获取并解析一个公开 API 的 JSON 数据
  3. 使用 tokio 实现一个支持多客户端并发聊天的 TCP 服务端
  4. 对比同步和异步 TCP 服务端在大量并发连接下的性能差异
  5. 使用 UdpSocket 实现一个简单的 ping 测试工具

第十三 压缩

数据压缩是计算机科学中一项核心技术,它通过消除数据中的冗余信息来减少存储空间或传输带宽。从节省手机存储空间的照片压缩,到加速网页传输的 Gzip,再到流式媒体的音视频编码,压缩技术无处不在。本章将介绍压缩的基本原理、经典算法以及在 Rust 中的实践。


一、数据压缩原理

1.1 信息冗余与压缩

数据压缩的本质是消除冗余。冗余是指数据中存在的不必要或可预测的信息。根据信息论,如果某些信息可以通过其他信息推导出来,那么它就是冗余的。

常见的冗余类型:

冗余类型说明示例
空间冗余相邻数据高度相似图像中大片同色区域
时间冗余相邻时刻数据变化小视频中连续帧的差异
编码冗余使用超过必要长度的编码用 8 字节存储一个布尔值
视觉/听觉冗余人眼/人耳不敏感的信息高频色彩细节、超声波
统计冗余某些符号出现频率更高英文中字母 ‘e’ 出现频率最高

压缩率的计算:

压缩率 = 压缩后大小 / 压缩前大小 × 100%
压缩比 = 压缩前大小 / 压缩后大小

例如,一个 100KB 的文件压缩后为 25KB,则压缩率为 25%,压缩比为 4:1。

1.2 熵与信息论

香农(Claude Shannon)在 1948 年提出的信息论为数据压缩奠定了理论基础。

信息熵(Entropy) 表示随机变量的不确定性,也代表了数据的最小平均编码长度:

$$H(X) = -\sum_{i=1}^{n} p(x_i) \log_2 p(x_i)$$

其中 $p(x_i)$ 是符号 $x_i$ 出现的概率。熵越大,数据的不确定性越高,可压缩的空间越小;熵越小,数据的规律性越强,可压缩的空间越大。

fn shannon_entropy(data: &[u8]) -> f64 {
    let mut freq = [0usize; 256];
    for &byte in data {
        freq[byte as usize] += 1;
    }

    let len = data.len() as f64;
    let mut entropy = 0.0;

    for &count in &freq {
        if count > 0 {
            let p = count as f64 / len;
            entropy -= p * p.log2();
        }
    }

    entropy
}

fn main() {
    // 高度规律的数据,熵低,可压缩性高
    let repetitive = b"AAAAAAAAAABBBBBBBBBB";
    println!("重复数据熵: {:.4} bits/byte", shannon_entropy(repetitive));

    // 随机数据,熵高,接近 8 bits/byte,难以压缩
    let random = [0x3F, 0xA7, 0x12, 0xE9, 0x55, 0x8C, 0x21, 0x7B];
    println!("随机数据熵: {:.4} bits/byte", shannon_entropy(&random));

    // 英文文本,熵约 4-5 bits/byte
    let text = b"Hello, World! This is a test of entropy calculation.";
    println!("英文文本熵: {:.4} bits/byte", shannon_entropy(text));
}

1.3 压缩算法的分类

分类维度类型说明
是否丢失信息无损压缩压缩后可完全还原原始数据
有损压缩压缩后丢失部分信息,但人眼/人耳难以察觉
压缩时机离线压缩数据生成后再压缩
实时压缩数据产生的同时进行压缩(如视频直播)
压缩方式熵编码根据符号频率分配不同长度的编码
字典编码用引用替换重复出现的字符串
变换编码将数据变换到另一个域后压缩

二、无损压缩

无损压缩保证压缩后的数据可以完全还原为原始数据,适用于文本、程序代码、可执行文件等对数据完整性要求高的场景。

2.1 Huffman 编码

Huffman 编码是一种经典的熵编码方法,由 David Huffman 于 1952 年提出。其核心思想是:出现频率高的符号使用较短的编码,出现频率低的符号使用较长的编码

Huffman 编码的构建过程:

  1. 统计每个符号的出现频率
  2. 将每个符号作为一个叶子节点,构建一个森林
  3. 每次取出频率最小的两个节点,合并为一个新节点(频率为两者之和)
  4. 重复步骤 3,直到只剩一棵树
  5. 从根节点出发,左分支标记 0,右分支标记 1,到达叶子节点的路径即为该符号的编码
use std::collections::{BTreeMap, BinaryHeap};

#[derive(Debug, Clone)]
struct HuffmanNode {
    freq: usize,
    symbol: Option<u8>,
    left: Option<Box<HuffmanNode>>,
    right: Option<Box<HuffmanNode>>,
}

impl HuffmanNode {
    fn new_leaf(symbol: u8, freq: usize) -> Self {
        HuffmanNode {
            freq,
            symbol: Some(symbol),
            left: None,
            right: None,
        }
    }

    fn new_internal(left: HuffmanNode, right: HuffmanNode) -> Self {
        HuffmanNode {
            freq: left.freq + right.freq,
            symbol: None,
            left: Some(Box::new(left)),
            right: Some(Box::new(right)),
        }
    }
}

impl PartialEq for HuffmanNode {
    fn eq(&self, other: &Self) -> bool {
        self.freq == other.freq
    }
}

impl Eq for HuffmanNode {}

impl PartialOrd for HuffmanNode {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        other.freq.partial_cmp(&self.freq)  // 最小堆
    }
}

impl Ord for HuffmanNode {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        other.freq.cmp(&self.freq)
    }
}

fn build_codes(node: &HuffmanNode, prefix: String, codes: &mut BTreeMap<u8, String>) {
    if let Some(symbol) = node.symbol {
        codes.insert(symbol, if prefix.is_empty() { "0".to_string() } else { prefix });
    } else {
        if let Some(ref left) = node.left {
            build_codes(left, format!("{}0", prefix), codes);
        }
        if let Some(ref right) = node.right {
            build_codes(right, format!("{}1", prefix), codes);
        }
    }
}

fn huffman_encode(data: &[u8]) -> (BTreeMap<u8, String>, String) {
    // 统计频率
    let mut freq = [0usize; 256];
    for &byte in data {
        freq[byte as usize] += 1;
    }

    // 构建最小堆
    let mut heap = BinaryHeap::new();
    for (symbol, count) in freq.iter().enumerate() {
        if *count > 0 {
            heap.push(HuffmanNode::new_leaf(symbol as u8, *count));
        }
    }

    // 构建 Huffman 树
    while heap.len() > 1 {
        let left = heap.pop().unwrap();
        let right = heap.pop().unwrap();
        heap.push(HuffmanNode::new_internal(left, right));
    }

    // 生成编码表
    let mut codes = BTreeMap::new();
    if let Some(root) = heap.pop() {
        build_codes(&root, String::new(), &mut codes);
    }

    // 编码数据
    let mut encoded = String::new();
    for &byte in data {
        encoded.push_str(codes.get(&byte).unwrap());
    }

    (codes, encoded)
}

fn main() {
    let text = b"this is an example of a huffman tree";
    let (codes, encoded) = huffman_encode(text);

    println!("原文: {}", String::from_utf8_lossy(text));
    println!("原文长度: {} bytes", text.len());
    println!("\nHuffman 编码表:");
    for (symbol, code) in &codes {
        println!("  '{}' => {}", *symbol as char, code);
    }

    println!("\n编码后: {}", encoded);
    println!("编码后长度: {} bits = {:.2} bytes", encoded.len(), encoded.len() as f64 / 8.0);
    println!("压缩率: {:.2}%", (encoded.len() as f64 / 8.0) / text.len() as f64 * 100.0);
}

Huffman 编码的特点:

  • 前缀编码:没有任何编码是其他编码的前缀,保证解码的唯一性
  • 最优性:对于给定的频率分布,Huffman 编码是最优的前缀编码
  • 局限性:需要预先统计频率或动态维护编码表

2.2 LZ77 算法

LZ77(Lempel-Ziv 1977)是一种基于字典的压缩算法,由 Abraham Lempel 和 Jacob Ziv 于 1977 年提出。其核心思想是:用已出现过的字符串的引用(位置和长度)来替换重复出现的字符串

LZ77 的工作原理:

算法维护一个滑动窗口,包含已处理的数据(搜索缓冲区)和待处理的数据(前瞻缓冲区)。对于前瞻缓冲区中的数据,在搜索缓冲区中寻找最长的匹配字符串,然后用 (偏移量, 长度, 下一个字符) 的三元组替换。

滑动窗口示意:
[已处理数据(搜索缓冲区)| 待处理数据(前瞻缓冲区)]

示例:压缩 "ABABABAB"
已处理: "ABAB"
待处理: "ABAB"
在搜索缓冲区中找到 "AB" 匹配,偏移量为 2,长度为 2
输出: (2, 2, 'A')

LZ77 是后续许多压缩算法的基础,包括 DEFLATE、Gzip、ZIP 等。

2.3 DEFLATE 算法

DEFLATE 是一种结合了 LZ77 和 Huffman 编码的压缩算法,由 Phil Katz 于 1993 年设计,是 ZIP 和 Gzip 的核心算法。

DEFLATE 的两阶段压缩:

  1. LZ77 阶段:使用滑动窗口查找重复的字符串,用长度-距离对替换
  2. Huffman 编码阶段:对 LZ77 的输出(字面量、长度、距离)进行 Huffman 编码

DEFLATE 支持多种压缩级别,从最快(级别 1)到最优(级别 9),在速度和压缩率之间进行权衡。


三、有损压缩

有损压缩在压缩过程中会丢失部分信息,但力求丢失的是人眼或人耳不敏感的信息,从而在大幅减小文件体积的同时保持可接受的感知质量。

3.1 JPEG 图像压缩原理

JPEG(Joint Photographic Experts Group)是最广泛使用的有损图像压缩标准。

JPEG 压缩的主要步骤:

步骤操作说明
1颜色空间转换将 RGB 转换为 YCbCr(亮度 + 两个色差分量)
2下采样对色差分量进行降采样(人眼对亮度更敏感)
3分块 DCT将图像分成 8x8 块,进行离散余弦变换
4量化用量化表除 DCT 系数,丢弃高频信息(主要的有损步骤)
5Zig-Zag 扫描将 2D 系数按频率排列为 1D 序列
6游程编码 + Huffman 编码对序列进行无损压缩

DCT 变换的核心思想:

DCT(Discrete Cosine Transform,离散余弦变换)将空间域的图像数据转换到频率域。图像的能量主要集中在低频部分,高频部分(代表细节和噪声)可以被大量量化甚至丢弃而不明显影响视觉质量。

#![allow(unused)]
fn main() {
// JPEG 质量因子与压缩率的示意
fn jpeg_compression_info() {
    let qualities = [
        (95, "极佳", "几乎无损"),
        (85, "很好", "标准质量"),
        (75, "好", "Web 常用"),
        (50, "中等", "明显压缩痕迹"),
        (25, "低", "严重失真"),
        (10, "极低", "块状伪影明显"),
    ];

    println!("JPEG 质量设置与效果对照:");
    println!("{:<10} {:<10} {:<20}", "质量因子", "效果", "说明");
    for (q, effect, desc) in &qualities {
        println!("{:<10} {:<10} {:<20}", q, effect, desc);
    }
}
}

3.2 MP3 音频压缩原理

MP3(MPEG-1 Audio Layer III)是最流行的有损音频压缩格式。

MP3 压缩的核心技术:

技术说明
心理声学模型利用人耳听觉特性,去除听不到的频率成分
频域掩蔽强音会掩蔽附近频率的弱音
时域掩蔽强音出现后会短暂掩蔽随后出现的弱音
临界频带人耳对不同频率的分辨率不同,将频谱划分为临界频带分别处理

MP3 压缩流程:

  1. 将音频信号分帧(每帧约 26ms)
  2. 对每帧进行改进的离散余弦变换(MDCT)
  3. 应用心理声学模型,计算每个频带的掩蔽阈值
  4. 根据掩蔽阈值分配比特,量化频谱系数
  5. 使用 Huffman 编码压缩量化后的系数
  6. 添加帧头和辅助信息,形成 MP3 比特流

比特率与音质:

比特率音质等级适用场景
64 kbps语音质量有声书、语音通话
128 kbps可接受早期 MP3 标准
192 kbps一般音乐欣赏
256 kbps很好高质量音乐
320 kbps接近无损发烧级需求

四、Rust 压缩库实践

Rust 生态提供了丰富的压缩库,可以方便地进行数据压缩和解压操作。

4.1 使用 flate2 进行 Gzip 压缩

flate2 是 Rust 中最流行的 DEFLATE 压缩库,支持 Gzip 和 Zlib 格式。

Cargo.toml 依赖:

[dependencies]
flate2 = "1.0"

Gzip 压缩与解压:

use flate2::write::{GzEncoder, GzDecoder};
use flate2::Compression;
use std::io::{self, Write};

fn gzip_compress(data: &[u8]) -> io::Result<Vec<u8>> {
    let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
    encoder.write_all(data)?;
    encoder.finish()
}

fn gzip_decompress(data: &[u8]) -> io::Result<Vec<u8>> {
    let mut decoder = GzDecoder::new(Vec::new());
    decoder.write_all(data)?;
    decoder.finish()
}

fn main() -> io::Result<()> {
    let original = b"Rust is a systems programming language that runs blazingly fast, \
                      prevents segfaults, and guarantees thread safety. \
                      Rust is also a great language for web development, \
                      with frameworks like Actix and Axum providing high-performance \
                      HTTP servers. The Rust ecosystem is growing rapidly, \
                      with thousands of crates available on crates.io.";

    println!("原始数据大小: {} bytes", original.len());

    // 压缩
    let compressed = gzip_compress(original)?;
    println!("压缩后大小: {} bytes", compressed.len());
    println!("压缩率: {:.2}%", compressed.len() as f64 / original.len() as f64 * 100.0);

    // 解压
    let decompressed = gzip_decompress(&compressed)?;
    println!("解压后大小: {} bytes", decompressed.len());
    println!("数据一致性: {}", original.to_vec() == decompressed);

    // 不同压缩级别对比
    println!("\n不同压缩级别对比:");
    for level in [Compression::none(), Compression::fast(), Compression::default(), Compression::best()] {
        let mut encoder = GzEncoder::new(Vec::new(), level);
        encoder.write_all(original)?;
        let result = encoder.finish()?;
        println!("级别 {:?}: {} bytes", level.level(), result.len());
    }

    Ok(())
}

使用 BufReader/BufWriter 进行流式压缩:

#![allow(unused)]
fn main() {
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression;
use std::fs::File;
use std::io::{self, BufReader, BufWriter, Read, Write};

fn compress_file(input_path: &str, output_path: &str) -> io::Result<()> {
    let input = File::open(input_path)?;
    let output = File::create(output_path)?;

    let mut reader = BufReader::new(input);
    let mut encoder = GzEncoder::new(BufWriter::new(output), Compression::default());

    let mut buffer = [0u8; 8192];
    loop {
        let n = reader.read(&mut buffer)?;
        if n == 0 {
            break;
        }
        encoder.write_all(&buffer[..n])?;
    }

    encoder.finish()?;
    Ok(())
}

fn decompress_file(input_path: &str, output_path: &str) -> io::Result<()> {
    let input = File::open(input_path)?;
    let output = File::create(output_path)?;

    let mut decoder = GzDecoder::new(BufReader::new(input));
    let mut writer = BufWriter::new(output);

    let mut buffer = [0u8; 8192];
    loop {
        let n = decoder.read(&mut buffer)?;
        if n == 0 {
            break;
        }
        writer.write_all(&buffer[..n])?;
    }

    writer.flush()?;
    Ok(())
}
}

4.2 使用 zip 库处理 ZIP 文件

zip 库提供了在 Rust 中创建和读取 ZIP 压缩文件的能力。

Cargo.toml 依赖:

[dependencies]
zip = "0.6"

创建 ZIP 压缩文件:

use std::fs::File;
use std::io::{self, Read, Write};
use zip::write::FileOptions;
use zip::CompressionMethod;

fn create_zip_archive(output_path: &str, files: &[(&str, &[u8])]) -> io::Result<()> {
    let file = File::create(output_path)?;
    let mut zip = zip::ZipWriter::new(file);

    let options = FileOptions::default()
        .compression_method(CompressionMethod::Deflated)
        .unix_permissions(0o755);

    for (name, content) in files {
        zip.start_file(*name, options)?;
        zip.write_all(content)?;
    }

    zip.finish()?;
    println!("ZIP 文件已创建: {}", output_path);
    Ok(())
}

fn main() -> io::Result<()> {
    let files = [
        ("readme.txt", b"This is a README file.\n" as &[u8]),
        ("data.json", b"{\"name\": \"Rust\", \"version\": \"1.70\"}\n" as &[u8]),
        ("hello.rs", b"fn main() { println!(\"Hello, Rust!\"); }\n" as &[u8]),
    ];

    create_zip_archive("archive.zip", &files)?;

    Ok(())
}

读取 ZIP 压缩文件:

use std::fs::File;
use std::io::{self, Read};
use zip::ZipArchive;

fn read_zip_archive(path: &str) -> io::Result<()> {
    let file = File::open(path)?;
    let mut archive = ZipArchive::new(file)?;

    println!("ZIP 文件包含 {} 个条目:", archive.len());
    println!("{:<20} {:<10} {:<10}", "文件名", "压缩后", "原始大小");
    println!("{}", "-".repeat(45));

    for i in 0..archive.len() {
        let mut file = archive.by_index(i)?;
        let name = file.name();
        let compressed = file.compressed_size();
        let size = file.size();

        println!("{:<20} {:<10} {:<10}", name, compressed, size);

        // 读取文件内容
        let mut contents = String::new();
        file.read_to_string(&mut contents)?;
        println!("  内容预览: {}", &contents[..contents.len().min(50)]);
    }

    Ok(())
}

fn main() -> io::Result<()> {
    read_zip_archive("archive.zip")?;
    Ok(())
}

4.3 压缩算法选择指南

在实际项目中,应根据数据类型和需求选择合适的压缩方案:

场景推荐方案Rust 库
通用数据压缩Gzip / Zlibflate2
文件打包归档ZIPzip
最大压缩率LZMA / XZxz2
极速压缩LZ4lz4
流式压缩Zstdzstd
图像压缩PNG(无损)/ JPEG(有损)image

不同压缩算法的对比:

#![allow(unused)]
fn main() {
fn compression_comparison() {
    let algorithms = [
        ("Gzip (flate2)", "通用,兼容性好", "中等", "高"),
        ("LZ4", "极速压缩解压", "低", "很高"),
        ("Zstd", "Facebook 开发,压缩率和速度均衡", "高", "高"),
        ("LZMA/XZ", "极高压缩率", "很高", "中等"),
        ("Brotli", "Google 开发,Web 优化", "高", "高"),
    ];

    println!("{:<15} {:<30} {:<10} {:<10}", "算法", "特点", "压缩率", "速度");
    println!("{}", "-".repeat(70));
    for (name, feature, ratio, speed) in &algorithms {
        println!("{:<15} {:<30} {:<10} {:<10}", name, feature, ratio, speed);
    }
}
}

4.4 内存中的压缩与解压示例

use flate2::{read::ZlibDecoder, write::ZlibEncoder, Compression};
use std::io::{self, Read, Write};

fn zlib_roundtrip(data: &[u8]) -> io::Result<bool> {
    // 压缩
    let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
    encoder.write_all(data)?;
    let compressed = encoder.finish()?;

    // 解压
    let mut decoder = ZlibDecoder::new(&compressed[..]);
    let mut decompressed = Vec::new();
    decoder.read_to_end(&mut decompressed)?;

    Ok(data == decompressed.as_slice())
}

fn main() -> io::Result<()> {
    let test_data = [
        b"Short text".to_vec(),
        vec![0u8; 1000],           // 全零数据(高度可压缩)
        (0..=255).collect(),       // 均匀分布数据(难以压缩)
        "Rust ".repeat(100).into_bytes(),  // 重复数据
    ];

    for (i, data) in test_data.iter().enumerate() {
        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
        encoder.write_all(data)?;
        let compressed = encoder.finish()?;

        let ratio = compressed.len() as f64 / data.len() as f64;
        let ok = zlib_roundtrip(data)?;

        println!(
            "测试 {}: 原始 {} bytes, 压缩后 {} bytes, 比率 {:.2}%, 一致性 {}",
            i + 1,
            data.len(),
            compressed.len(),
            ratio * 100.0,
            ok
        );
    }

    Ok(())
}

五、总结

概念要点
信息冗余空间、时间、编码、视觉/听觉、统计冗余是压缩的基础
信息熵表示数据的不确定性,决定了理论上的最小编码长度
Huffman 编码根据符号频率分配变长编码,最优前缀编码
LZ77用偏移量-长度对替换重复字符串,滑动窗口算法
DEFLATELZ77 + Huffman 编码的组合,ZIP/Gzip 的核心
JPEGDCT 变换 + 量化 + Huffman 编码,有损图像压缩
MP3心理声学模型 + MDCT + 量化,有损音频压缩
flate2Rust 中 Gzip/Zlib/DEFLATE 压缩的标准库
zipRust 中 ZIP 文件创建和读取的库
压缩级别在压缩速度和压缩率之间进行权衡

练习建议:

  1. 实现一个简单的 Run-Length Encoding(RLE,游程编码)算法,测试其对不同类型数据的压缩效果
  2. 使用 flate2 编写一个命令行工具,支持对文件进行 Gzip 压缩和解压(类似 gzip 命令)
  3. 对比 flate2 的不同压缩级别(none/fast/default/best)在速度和压缩率上的表现
  4. 使用 zip 库编写一个程序,递归地将一个目录打包为 ZIP 文件
  5. 统计一段文本中各字符的出现频率,计算其信息熵,并与 Huffman 编码后的平均码长进行比较
  6. 尝试用 reqwest 下载一个网页,并用 flate2 压缩其 HTML 内容,观察压缩率

中卷 运算与算法

“算法是计算机的灵魂,数学是算法的灵魂。”

中卷概述

如果说上卷“基础与类型“为我们搭建了Rust编程的基石,那么中卷“运算与算法“将带领我们进入计算机科学的核心地带——运算与算法的世界。从最简单的加减乘除到复杂的神经网络训练,从基本的位操作到高深的Lambda演算,运算与算法贯穿了编程的方方面面。

计算机的本质就是一台运算机器。无论是处理用户输入、渲染图形界面,还是训练人工智能模型、加密敏感数据,背后都离不开各种各样的运算和算法。理解运算的原理和算法的思想,是每一位程序员从“会写代码“迈向“写好代码“的关键一步。

本卷将从最基础的运算符出发,逐步深入到数据变换、数据处理、函数式编程、人工智能和数学运算等多个维度,全面展现Rust在运算与算法领域的强大能力和优雅表达。

从基本运算到复杂算法

基本运算:算术、关系、逻辑、位运算(第十四~十七章)

Rust的运算符分为四大类,它们是构成一切复杂运算的基础:

  • 算术运算符+ - * / % 。Rust不支持++ --运算符,可以使用+=1 -=1替代。算术运算是所有计算的起点,从简单的计数到复杂的科学计算,都离不开这些基本操作。
  • 关系运算符== != > >= < <= 。关系运算用于比较两个值的大小或相等性,是条件判断和排序算法的基石。
  • 逻辑运算符&& || ! ^ & | 。逻辑运算处理布尔值,用于组合多个条件,构建复杂的业务规则。
  • 位运算符& &= | |= ~ ^ ^= >> << 。位运算直接操作二进制位,在系统编程、图形处理、密码学等领域有着不可替代的作用。

这四类运算符看似简单,却是构建一切复杂逻辑的砖瓦。在Rust中,这些运算符不仅可以用于基本类型,还可以通过运算符重载(std::ops trait)扩展到自定义类型,体现了Rust“零成本抽象“的设计理念。

数据变换:交换、反转、旋转、类型转换(第十八~十九章)

数据在程序中的流动往往需要进行各种变换。交换两个变量的值、反转一个序列、旋转数组元素、在不同类型之间进行转换——这些看似简单的操作,却蕴含着丰富的编程技巧。

Rust的所有权系统和类型安全为数据变换提供了独特的视角。如何在遵守所有权规则的前提下高效地交换数据?如何利用类型转换实现安全的数值计算?这些问题将在本部分得到深入探讨。

数据处理:哈希、压缩、查询、排序(第二十~二十三章)

当数据量增大时,如何高效地处理数据成为关键问题。哈希算法提供了O(1)时间复杂度的数据查找能力;压缩算法帮助我们节省存储空间和传输带宽;查询与搜索算法让我们从海量数据中快速定位所需信息;排序算法则是数据处理中最基础也是最重要的操作之一。

Rust的标准库和生态系统为这些操作提供了丰富的工具:从std::collections::HashMapflate2压缩库,从线性搜索到二分查找,从冒泡排序到快速排序,我们将看到如何用Rust优雅而高效地实现这些经典算法。

函数式编程:Lambda演算(第二十四章)

Lambda演算是函数式编程的理论基础,由阿隆佐·邱奇(Alonzo Church)于1930年代提出。它用一种极其简洁的方式定义了“计算“的本质——函数的应用和抽象。

Rust虽然不是纯函数式语言,但它深受函数式编程思想的影响:闭包(closure)、迭代器(iterator)、高阶函数(higher-order function)等特性让Rust具备了强大的函数式编程能力。理解Lambda演算,将帮助我们更深入地理解Rust中这些特性的设计原理。

人工智能:AI基础、神经网络、NLP、CV、RL、生成式AI(第二十五、三十六~四十章)

人工智能是当今最热门的技术领域之一,而Rust凭借其高性能和内存安全特性,正在AI基础设施领域崭露头角。

从神经网络的前向传播和反向传播,到自然语言处理中的词嵌入和Transformer模型;从计算机视觉中的卷积神经网络到强化学习中的策略优化;再到生成式AI中的扩散模型和大语言模型——本部分将展示如何用Rust实现和部署这些前沿AI技术。

数学运算:算法、代数、几何、行列式、矩阵、向量、函数、微积分、概率论、离散数学(第二十六~三十五章)

数学是计算机科学的基石。算法分析需要大O表示法和概率论;图形渲染需要线性代数和几何知识;机器学习需要微积分和统计学;密码学需要数论和离散数学。

本部分将系统地介绍这些数学领域在编程中的应用,并用Rust实现相关的计算。从求解方程组到计算矩阵特征值,从向量空间到概率分布,从函数的导数到积分的数值计算——我们将看到数学与编程如何完美结合。

中卷各章节内容预览

章节标题核心内容
第十四算术运算加减乘除、取模、幂运算、数值精度与溢出处理
第十五关系运算相等性比较、大小比较、浮点数比较陷阱、PartialOrd与Eq trait
第十六逻辑运算布尔逻辑、短路求值、位逻辑、逻辑运算在条件中的应用
第十七位运算与或非异或、移位操作、掩码技术、位域与标志位
第十八交换/反转/旋转变量交换、数组反转、循环移位、内存布局优化
第十九类型转换显式转换、隐式转换、TryFrom/From trait、数值类型安全
第二十哈希哈希函数原理、HashMap实现、一致性哈希、密码学哈希
第二十一打包/拆包 压缩/解压结构体序列化、数据打包算法、DEFLATE、gzip、zip格式
第二十二查询/检索/搜索线性搜索、二分搜索、字符串匹配、正则表达式、数据库查询
第二十三排序算法冒泡、选择、插入、归并、快速、堆排序、Rust中的排序实现
第二十四Lambda演算匿名函数、闭包、高阶函数、Y组合子、函数式编程范式
第二十五人工智能AI发展史、机器学习基础、Rust AI生态概述
第二十六算法复杂度分析、递归与分治、动态规划、贪心算法、图算法
第二十七代数多项式运算、方程求解、群环域基础、抽象代数应用
第二十八几何点线面、距离计算、相交检测、凸包算法、计算几何
第二十九行列式行列式计算、克拉默法则、特征多项式、LU分解
第三十矩阵矩阵运算、矩阵分解、特征值与特征向量、线性方程组求解
第三十一向量向量运算、点积叉积、向量空间、基与维度、几何应用
第三十二函数函数定义与调用、高阶函数、递归函数、纯函数、函数组合
第三十三微积分极限、导数、积分、泰勒展开、数值微分与数值积分
第三十四概率论随机事件、概率分布、期望值、大数定律、蒙特卡洛方法
第三十五离散数学集合论、图论、数理逻辑、组合数学、布尔代数
第三十六神经网络感知机、多层网络、激活函数、反向传播、Rust神经网络实现
第三十七自然语言处理分词、词嵌入、RNN/LSTM、注意力机制、Transformer
第三十八计算机视觉图像处理、卷积神经网络、目标检测、图像分割、OpenCV
第三十九强化学习马尔可夫决策过程、Q学习、策略梯度、深度强化学习
第四十生成式AI生成对抗网络、变分自编码器、扩散模型、大语言模型

数学与编程的关系

数学与编程之间有着深刻而紧密的联系。从某种意义上说,编程就是数学的一种实践形式,而数学则为编程提供了理论基础。

算法分析需要数学。当我们评估一个算法的效率时,需要使用大O表示法来描述其时间复杂度和空间复杂度。概率论帮助我们理解随机算法的期望性能,摊还分析让我们更准确地评估数据结构的操作成本。

数据结构建立在数学之上。哈希表利用数论中的模运算;图算法基于图论;树结构源于组合数学;并查集利用等价关系的数学性质。

机器学习是应用数学。神经网络的前向传播是矩阵乘法,反向传播是链式法则(微积分)的应用,优化算法基于梯度下降(微积分),正则化技术源于统计学的偏差-方差权衡。

密码学是数论的应用。RSA算法基于大整数分解的困难性;椭圆曲线密码利用代数几何;零知识证明依赖于计算复杂性理论。

Rust语言的设计也深受数学思想的影响。其类型系统可以看作是一种形式化逻辑系统;所有权模型借鉴了线性类型的数学理论;泛型编程体现了抽象代数的思想。理解这些数学背景,将帮助我们写出更加优雅和安全的Rust代码。

中卷知识图谱

                    运算与算法
                       |
        +--------------+--------------+--------------+
        |              |              |              |
    基本运算       数据变换       数据处理       高阶主题
        |              |              |              |
   +----+----+    +----+----+    +----+----+    +----+----+
   |    |    |    |    |    |    |    |    |    |    |    |
  算术 关系 逻辑 位运算 交换 反转 旋转 类型转换 哈希 压缩 查询 排序 Lambda  AI   数学
   |    |    |    |              |              |              |
  第十四 第十五 第十六 第十七    第十八~十九    第二十~二十三   第二十四~四十
                                    |              |
                              +-----+-----+  +-----+-----+
                              |           |  |           |
                            人工智能      数学运算      算法基础
                              |           |
                        第二十五,三十六~四十  第二十六~三十五

阅读建议

  1. 循序渐进:本卷内容从基础到高级逐步深入。建议先掌握第十四~十七章的基本运算,再学习后续章节。对于数学基础较弱的读者,可以先跳过第二十六~三十五章的数学内容,待需要时再回头学习。

  2. 理论与实践结合:每个算法和运算都配有Rust代码示例。建议读者在阅读的同时动手实践,修改参数、观察输出,以加深理解。

  3. 关注性能:Rust的核心优势之一是性能。在学习算法时,不仅要关注正确性,还要思考如何用Rust的特性(如零成本抽象、内存安全)来实现高效的实现。

  4. 建立联系:运算与算法不是孤立的知识点。尝试将不同章节的内容联系起来——比如,位运算如何加速某些算法?概率论如何优化搜索策略?矩阵运算如何应用于神经网络?

  5. 拓展阅读:本卷涉及的数学内容较为广泛,如需深入学习某个领域,建议参考相关的数学教材和专业书籍。

复杂运算拓展

在掌握基本运算之后,我们还需要了解更复杂的数学运算:

  • 模运算:模运算在密码学、哈希算法、循环队列等场景中有广泛应用。理解模运算的性质对于编写正确的算法至关重要。
  • 三角函数:三角函数在图形学、信号处理、物理模拟等领域不可或缺。Rust的标准库提供了完整的三角函数支持。

愿你在运算与算法的世界中,发现数学之美,体验编程之乐!

第十四 算数运算

算数运算符

use std::f64::consts::{PI, E,FRAC_PI_2, SQRT_2};
fn main() {
    let x = 11;
    let y = 2;
    let z = -31;
    let r : f64 = 2.0;
    println!("{}", x + y); 
    println!("{}", x - y); 
    println!("{}", x * y); 
    println!("{}", x / y); 
    println!("{}", x % y); 
    println!("{}", z % x); 
    println!("{}", PI * r.powi(2));
    println!("{}", f64::sin(FRAC_PI_2));
    println!("{}", SQRT_2);
    println!("{}", 4096_f64.log2());
    println!("{}", 4096_f64.log10());
    //高考真题: a = 0.1e^0.1 , b = 1/9 , c = -ln(0.9),求三者的大小关系
    //正确答案:c < a < b
    println!("{}", 0.1 * E.powf(0.1));
    println!("{}", 1.0_f64 / 9.0_f64);
    println!("{}", -(0.9_f64).ln());
}

模运算的性质

$$ (a + b)\mod n = ((a\mod n) + (b\mod n)) \mod n $$ $$ (a - b)\mod n = ((a\mod n) - (b\mod n)) \mod n $$ $$ (a * b)\mod n = ((a\mod n) * (b\mod n)) \mod n $$

三角函数,反三角函数

use std::f64::consts::PI;

fn main() {
    // 弧度
    let x: f64 = PI / 2.0;
    println!("{}", f64::sin(PI)); 
    println!("{}", x.sin()); 
    println!("{}", x.cos()); 
    println!("{}", x.tan());
    println!("{}", (PI / 4.0).tan());
    // 计算 30 度 (π/6 弧度) 的正弦值
    let angle_deg = 30.0_f64;
    // 转换为弧度
    let angle_rad = angle_deg.to_radians(); 
    let sin_val = angle_rad.sin();
    println!("sin(30°) = {}", sin_val);
    println!("sin(60°) = {}", 60.0_f64.to_radians().sin());

    // 验证 tan(x) 是否等于 sin(x) / cos(x)
    let x = 6.0_f64;
    let tan_direct = x.tan();
    let tan_computed = x.sin() / x.cos();
    println!("tan(6°) = {}", tan_direct);
    println!("sin(6°)/cos(6°) = {}", tan_computed);

    //勾三股四弦五,求勾所对应的角度是多少?
    //sin(x) = 3/5,求对应的角度x
    let x = 0.6_f64;
    let theta = x.asin();
    let theta_deg = theta * 180.0 / PI;
    println!("弧度 = {} ,角度 = {}", theta, theta_deg);
}

20以内所有阶乘

fn factorial_checked(n: u64) -> Option<u64> {
    (2..=n).try_fold(1u64, |acc: u64, x: u64| {
        let acc = acc.checked_mul(x)?;
        Some(acc)
    })
}

fn main() {
    // 使用示例
    for i in 2..=20 {
            match factorial_checked(i) {
                Some(result) => println!("{}! = {}", i, result),
                None => println!("计算溢出"),
            }
    }
}

例子

高考真题: a = 0.1e^0.1 , b = 1/9 , c = -ln(0.9),求三者的大小关系
正确答案:c < a < b

求证:$$\boldsymbol{-\ln(1-x) < x e^x < \frac{x}{1-x},\quad x\to0^+,;0<x<1}$$

分两段证明:$$\boldsymbol{-\ln(1-x)<xe^x}$$、$$\boldsymbol{xe^x<\dfrac{x}{1-x}}$$。

一、先证右侧:$$\boldsymbol{xe^x < \dfrac{x}{1-x}\ (0<x<1)}$$

$x>0$,两边同除以$x$等价于: $$e^x < \frac1{1-x}\iff e^x(1-x)<1$$ 设 $$F(x)=e^x(1-x)-1,\quad F(0)=e^0\cdot1-1=0$$ 求导: $$F’(x)=e^x(1-x)-e^x=-x e^x<0,\quad x>0$$ $F(x)$在$x>0$严格递减$$\Rightarrow F(x)<F(0)=0$$,即 $$e^x(1-x)<1\Rightarrow e^x<\frac1{1-x}\Rightarrow xe^x<\frac{x}{1-x}.$$ ✅ 右半不等式成立。

二、再证左侧:$$\boldsymbol{-\ln(1-x)<xe^x\ (0<x<1)}$$

变形: $$xe^x+\ln(1-x)>0$$ 构造 $$G(x)=xe^x+\ln(1-x),\quad G(0)=0+\ln1=0$$ 求一阶导: $$G’(x)=e^x+xe^x-\frac1{1-x}=e^x(1+x)-\frac1{1-x} $$ $$ G’(0)=1\cdot1-1=0 $$

再求二阶导: $$ \begin{aligned} G’‘(x)&=e^x(1+x)+e^x-\frac1{(1-x)^2}\ &=e^x(x+2)-\frac1{(1-x)^2} \end{aligned} $$ $$G’’(0)=2-1=1>0$$。

由**泰勒展开($x\to0^+$)**辅助验证: $$ \begin{aligned} -\ln(1-x)&=x+\frac{x^2}{2}+\frac{x^3}{3}+\frac{x^4}{4}+\cdots\ \end{aligned} $$

$$ \begin{aligned} xe^x&=x\left(1+x+\frac{x^2}{2}+\frac{x^3}{6}+\cdots\right)=x+x^2+\frac{x^3}{2}+\frac{x^4}{6}+\cdots \end{aligned} $$

逐项比较: $$ \begin{cases} x=x\ , \dfrac12x^2<x^2\ , \dfrac13x^3<\dfrac12x^3\ , \cdots \end{cases} $$ 故小正数x:$$-\ln(1-x)<xe^x$$。

从导数单调性:$$G’(0)=0,G’‘(0)>0\Rightarrow x\to0^+$$ 时$G’(x)>0$,$$G(x)\nearrow \Rightarrow G(x)>G(0)=0$$, $$xe^x+\ln(1-x)>0\iff -\ln(1-x)<xe^x.$$ ✅ 左半不等式成立。

三、合并结论

$$\boldsymbol{-\ln(1-x) < x e^x < \frac{x}{1-x},\quad x\to0^+,;0<x<1}$$

补充:三者等价无穷小( $$x \to 0^+ $$)

$$-\ln(1-x)\sim x,\quad xe^x\sim x,\quad \frac{x}{1-x}\sim x$$ 三者是同阶等价无穷小,只是从低到高:$$-\ln(1-x)<xe^x<\tfrac{x}{1-x}$$。

第十五 关系运算

关系运算(Relational Operation)用于比较两个值之间的大小关系,返回一个布尔值(truefalse)。它是条件判断和程序控制流的基础,也是排序、搜索等算法的核心操作。


一、Rust 中的关系运算符

1.1 六种关系运算符

Rust 提供了六种关系运算符,用于比较两个值的大小关系:

运算符含义数学符号示例结果
==等于=5 == 5true
!=不等于5 != 3true
>大于>5 > 3true
>=大于等于5 >= 5true
<小于<3 < 5true
<=小于等于3 <= 5true
fn main() {
    let a = 10;
    let b = 20;

    println!("a == b: {}", a == b);  // false
    println!("a != b: {}", a != b);  // true
    println!("a > b:  {}", a > b);   // false
    println!("a >= b: {}", a >= b);  // false
    println!("a < b:  {}", a < b);   // true
    println!("a <= b: {}", a <= b);  // true
}

1.2 关系运算的优先级

关系运算符的优先级低于算术运算符,高于逻辑运算符:

算术运算符(+ - * / %)> 关系运算符(> >= < <= == !=)> 逻辑运算符(&& ||)
fn main() {
    let x = 3 + 5;       // 先算术:x = 8
    let result = x > 5 && x < 15;  // 再关系:8 > 5 为 true,8 < 15 为 true
    println!("{}", result);  // true

    // 等价于:
    let result2 = (3 + 5) > 5 && (3 + 5) < 15;
    println!("{}", result2);  // true
}

1.3 关系运算符的返回类型

关系运算符的返回值始终是 bool 类型:

fn main() {
    let is_equal: bool = 10 == 10;
    let is_greater: bool = 10 > 5;

    println!("is_equal = {}", is_equal);    // true
    println!("is_greater = {}", is_greater); // true

    // bool 类型可以参与逻辑运算
    let combined = is_equal && is_greater;
    println!("combined = {}", combined);   // true
}

二、PartialOrd 与 Ord Trait

2.1 为什么需要 PartialOrd

在 Rust 的类型系统中,并非所有类型都能进行完整的比较。例如,浮点数中存在 NaN(Not a Number),NaN 与任何值的比较都没有意义。因此,Rust 将比较能力分为两个层次:

Trait含义适用类型
PartialOrd部分有序整数、浮点数、字符串等(允许不可比较的情况)
Ord完全有序整数、字符串等(任意两个值都可以比较)

2.2 PartialOrd Trait

PartialOrd 定义了 partial_cmp 方法,它返回 Option<Ordering>,而不是直接返回 Ordering

use std::cmp::Ordering;

fn main() {
    let a = 3.14;
    let b = 2.71;

    // partial_cmp 返回 Option<Ordering>
    match a.partial_cmp(&b) {
        Some(Ordering::Less) => println!("{} < {}", a, b),
        Some(Ordering::Equal) => println!("{} == {}", a, b),
        Some(Ordering::Greater) => println!("{} > {}", a, b),
        None => println!("无法比较"),
    }
}

Ordering 枚举的三种取值:

取值含义说明
Ordering::Less小于第一个值小于第二个值
Ordering::Equal等于两个值相等
Ordering::Greater大于第一个值大于第二个值

2.3 NaN 的比较问题

NaNPartialOrd 但不是 Ord 的典型例子:

use std::cmp::Ordering;

fn main() {
    let nan = f64::NAN;

    // NaN 与任何值的比较都返回 None
    println!("NaN > 1.0: {:?}", nan.partial_cmp(&1.0));   // None
    println!("NaN < 1.0: {:?}", nan.partial_cmp(&1.0));   // None
    println!("NaN == NaN: {:?}", nan.partial_cmp(&nan));  // None

    // 使用 == 运算符,NaN != NaN 也是 true
    println!("NaN == NaN: {}", nan == nan);  // false
    println!("NaN != NaN: {}", nan != nan);  // true

    // 这就是为什么浮点数只实现了 PartialOrd,没有实现 Ord
}

重要: NaN 是唯一一个“不等于自身“的值,即 NaN != NaNtrue。这是 IEEE 754 浮点数标准的规定。

2.4 Ord Trait

Ord trait 表示完全有序,任意两个值之间一定有确定的大小关系。它定义了 cmp 方法,返回 Ordering(不是 Option<Ordering>):

use std::cmp::Ordering;

fn main() {
    let a = 42;
    let b = 17;

    // cmp 返回 Ordering(不是 Option)
    match a.cmp(&b) {
        Ordering::Less => println!("{} < {}", a, b),
        Ordering::Equal => println!("{} == {}", a, b),
        Ordering::Greater => println!("{} > {}", a, b),
    }
}

实现了 Ord 的常用类型:

类型说明
i8, i16, i32, i64, i128, isize有符号整数
u8, u16, u32, u64, u128, usize无符号整数
char字符(按 Unicode 码点比较)
String, &str字符串(按字典序比较)
bool布尔值(false < true
Duration时间段

2.5 为自定义类型实现 Ord

use std::cmp::Ordering;

#[derive(Debug)]
struct Student {
    name: String,
    score: u32,
}

// 实现 PartialOrd
impl PartialOrd for Student {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.score.cmp(&other.score))
    }
}

// 实现 PartialEq(Ord 的前提)
impl PartialEq for Student {
    fn eq(&self, other: &Self) -> bool {
        self.score == other.score
    }
}

// 实现 Ord
impl Ord for Student {
    fn cmp(&self, other: &Self) -> Ordering {
        self.score.cmp(&other.score)
    }
}

fn main() {
    let alice = Student { name: "Alice".into(), score: 95 };
    let bob = Student { name: "Bob".into(), score: 87 };

    println!("Alice vs Bob: {:?}", alice.cmp(&bob));  // Greater

    // 可以直接排序
    let mut students = vec![
        Student { name: "Charlie".into(), score: 72 },
        Student { name: "Alice".into(), score: 95 },
        Student { name: "Bob".into(), score: 87 },
    ];
    students.sort();
    for s in &students {
        println!("{}: {}", s.name, s.score);
    }
    // Charlie: 72
    // Bob: 87
    // Alice: 95
}

提示: 在实际开发中,通常使用 #[derive(PartialEq, PartialOrd, Eq, Ord)] 自动派生,前提是所有字段都实现了 Ord


三、浮点数比较的陷阱

3.1 浮点数精度问题

浮点数在计算机中采用 IEEE 754 标准存储,存在精度限制。许多看似简单的十进制小数无法精确表示为二进制浮点数:

fn main() {
    // 0.1 + 0.2 在数学上等于 0.3
    // 但在浮点数中并不相等!
    let sum = 0.1_f64 + 0.2_f64;
    println!("0.1 + 0.2 = {:.20}", sum);       // 0.30000000000000004441
    println!("sum == 0.3: {}", sum == 0.3_f64); // false
}

原因: 0.1 在二进制中是一个无限循环小数(类似十进制中的 1/3 = 0.333…),因此无法精确存储。

3.2 正确的浮点数比较方法

方法一:使用误差范围(epsilon)

fn approx_equal(a: f64, b: f64, epsilon: f64) -> bool {
    (a - b).abs() < epsilon
}

fn main() {
    let a = 0.1_f64 + 0.2_f64;
    let b = 0.3_f64;

    println!("直接比较: a == b → {}", a == b);                          // false
    println!("误差比较: approx_equal(a, b, 1e-10) → {}", approx_equal(a, b, 1e-10)); // true
}

方法二:使用相对误差

当数值很大时,固定误差范围可能不够用,此时应使用相对误差:

fn approx_equal_relative(a: f64, b: f64, max_relative_error: f64) -> bool {
    let abs_diff = (a - b).abs();
    let largest = a.abs().max(b.abs());

    // 处理两个值都接近 0 的情况
    if largest < 1e-10 {
        abs_diff < 1e-10
    } else {
        abs_diff / largest < max_relative_error
    }
}

fn main() {
    let a = 1000000.00001_f64;
    let b = 1000000.00002_f64;

    println!("相对误差比较: {}", approx_equal_relative(a, b, 1e-10)); // true
}

方法三:使用 assert_approx_eq 宏(测试中常用)

// 简单的近似相等断言宏
macro_rules! assert_approx_eq {
    ($a:expr, $b:expr, $eps:expr) => {
        let a = $a as f64;
        let b = $b as f64;
        let diff = (a - b).abs();
        assert!(
            diff < $eps,
            "断言失败: |{} - {}| = {} >= {}",
            a, b, diff, $eps
        );
    };
}

fn main() {
    let result = 0.1_f64 + 0.2_f64;
    assert_approx_eq!(result, 0.3, 1e-10);
    println!("测试通过!");
}

3.3 浮点数排序中的 NaN 处理

由于 NaN 无法参与正常比较,排序时需要特殊处理:

use std::cmp::Ordering;

fn main() {
    let mut data = vec![3.5, f64::NAN, 1.2, f64::NAN, 2.8, 0.5];

    // 方法一:分离 NaN 后排序
    let mut numbers: Vec<f64> = data.iter().cloned().filter(|&x| !x.is_nan()).collect();
    let nans: Vec<f64> = data.iter().cloned().filter(|&x| x.is_nan()).collect();

    numbers.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));

    let sorted = [numbers, nans].concat();
    println!("排序结果: {:?}", sorted);
    // [0.5, 1.2, 2.8, 3.5, NaN, NaN]

    // 方法二:使用 total_cmp(Rust 1.62+,将 NaN 排在最后)
    let mut data2 = vec![3.5, f64::NAN, 1.2, f64::NAN, 2.8, 0.5];
    data2.sort_by(f64::total_cmp);
    println!("total_cmp 排序: {:?}", data2);
    // [0.5, 1.2, 2.8, 3.5, NaN, NaN]
}

提示: Rust 1.62 引入了 f64::total_cmpf32::total_cmp,提供了浮点数的完全有序比较,NaN 被视为大于所有非 NaN 值。

3.4 特殊浮点值的比较

fn main() {
    let inf = f64::INFINITY;
    let neg_inf = f64::NEG_INFINITY;
    let nan = f64::NAN;

    // 正无穷
    println!("INFINITY > 1000: {}", inf > 1000.0);        // true
    println!("INFINITY == INFINITY: {}", inf == inf);      // true

    // 负无穷
    println!("NEG_INFINITY < -1000: {}", neg_inf < -1000.0); // true

    // NaN 的特殊性
    println!("NaN > 0:   {}", nan > 0.0);    // false
    println!("NaN < 0:   {}", nan < 0.0);    // false
    println!("NaN == 0:  {}", nan == 0.0);   // false
    println!("NaN != NaN: {}", nan != nan);   // true(唯一不等于自身的值)

    // 判断是否为特殊值
    println!("NaN.is_nan(): {}", nan.is_nan());             // true
    println!("INFINITY.is_infinite(): {}", inf.is_infinite()); // true
    println!("0.0.is_finite(): {}", 0.0_f64.is_finite());   // true
}

四、不同类型的比较

4.1 整数比较

整数比较是精确的,不存在精度问题:

fn main() {
    let a: i32 = 42;
    let b: i32 = -17;

    println!("a > b:  {}", a > b);   // true
    println!("a == b: {}", a == b);  // false

    // 不同大小的整数可以比较(Rust 会自动转换)
    let small: u8 = 10;
    let large: i32 = 100;
    // println!("{}", small > large);  // 错误!类型不同不能直接比较

    // 需要显式转换
    println!("{}", (small as i32) > large);  // false
}

4.2 字符比较

字符按 Unicode 码点进行比较:

fn main() {
    let a = 'A';
    let b = 'a';
    let c = '中';

    println!("'A' < 'a': {}", a < b);  // true(大写字母的码点小于小写字母)
    println!("'A' 码点: {}", a as u32);  // 65
    println!("'a' 码点: {}", b as u32);  // 97
    println!("'中' 码点: {}", c as u32);  // 20013

    // 字符可以排序
    let mut chars = vec!['z', 'A', '中', 'a', '0'];
    chars.sort();
    println!("排序结果: {:?}", chars);
    // ['0', 'A', 'a', 'z', '中']
}

4.3 字符串比较

字符串按字典序(lexicographical order)比较:

fn main() {
    let a = "apple";
    let b = "banana";
    let c = "Apple";

    println!("\"apple\" < \"banana\": {}", a < b);   // true
    println!("\"Apple\" < \"apple\": {}", c < a);     // true(大写字母码点更小)

    // 中文字符串也按 Unicode 码点比较
    let d = "北京";
    let e = "上海";
    println!("\"北京\" < \"上海\": {}", d < e);  // true

    // 比较前缀
    let prefix = "hello world";
    println!("\"hello\" < \"hello world\": {}", "hello" < prefix);  // true(短字符串更小)
}

4.4 元组和数组比较

元组和数组支持字典序比较(前提是元素类型可比较):

fn main() {
    // 元组比较:先比较第一个元素,相同则比较第二个
    let t1 = (1, 2);
    let t2 = (1, 3);
    let t3 = (2, 1);
    println!("(1,2) < (1,3): {}", t1 < t2);  // true
    println!("(1,2) < (2,1): {}", t1 < t3);  // true

    // 数组比较:逐元素比较
    let arr1 = [1, 2, 3];
    let arr2 = [1, 2, 4];
    println!("{:?} < {:?}: {}", arr1, arr2, arr1 < arr2);  // true

    // 不同长度的数组不能直接比较
    let arr3 = [1, 2];
    // println!("{}", arr3 < arr1);  // 错误!长度不同
}

五、关系运算的数学应用

5.1 比较大小

数学中经常需要比较两个表达式的大小,Rust 可以帮助我们快速验证:

题目1: 比较 $85 \times 85$ 与 $84 \times 86$ 的大小

fn main() {
    let a = 85 * 85;
    let b = 84 * 86;
    println!("85×85 = {}", a);  // 7225
    println!("84×86 = {}", b);  // 7216
    println!("85×85 {} 84×86", if a > b { ">" } else if a < b { "<" } else { "=" });
    // 85×85 > 84×86
}

数学原理: 设 $a = n \times n$,$b = (n-1)(n+1) = n^2 - 1$,所以 $a > b$。

题目2: 比较 $0.1e^{0.1}$、$1/9$、$-\ln 0.9$ 的大小

use std::f64::consts::E;

fn main() {
    let a = 0.1 * E.powf(0.1);
    let b = 1.0_f64 / 9.0_f64;
    let c = -(0.9_f64).ln();

    println!("0.1·e^0.1  = {:.15}", a);
    println!("1/9        = {:.15}", b);
    println!("-ln(0.9)   = {:.15}", c);

    // 使用 total_cmp 进行浮点数比较
    let mut values = [("0.1·e^0.1", a), ("1/9", b), ("-ln(0.9)", c)];
    values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());

    for (name, val) in &values {
        println!("{} = {:.15}", name, val);
    }
}

题目3: 比较 $a = \frac{31}{32}$、$b = \cos(\frac{1}{4})$、$c = 4\sin(\frac{1}{4})$ 的大小

fn main() {
    let a = 31.0_f64 / 32.0_f64;
    let b = (0.25_f64).cos();
    let c = 4.0 * (0.25_f64).sin();

    println!("31/32      = {:.15}", a);
    println!("cos(1/4)   = {:.15}", b);
    println!("4sin(1/4)  = {:.15}", c);

    let mut values = [("31/32", a), ("cos(1/4)", b), ("4sin(1/4)", c)];
    values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());

    for (name, val) in &values {
        println!("{} = {:.15}", name, val);
    }
}

题目4: 比较 $a = \log_{3}{2}$、$b = \log_{5}{3}$、$c = \frac{2}{3}$ 的大小

fn main() {
    // log_3(2) = ln(2) / ln(3)
    let a = 2.0_f64.ln() / 3.0_f64.ln();
    // log_5(3) = ln(3) / ln(5)
    let b = 3.0_f64.ln() / 5.0_f64.ln();
    let c = 2.0_f64 / 3.0_f64;

    println!("log_3(2) = {:.15}", a);
    println!("log_5(3) = {:.15}", b);
    println!("2/3      = {:.15}", c);

    let mut values = [("log_3(2)", a), ("log_5(3)", b), ("2/3", c)];
    values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());

    for (name, val) in &values {
        println!("{} = {:.15}", name, val);
    }
}

5.2 常用数学比较方法

方法说明示例
单调性法利用函数的增减性同底指数/对数直接看增减
中间值法与 0、1 等特殊值比较$e^x > 1 + x$
作差法判断 $a - b$ 的符号$(n+1)^2 - n^2 = 2n + 1 > 0$
作商法判断 $\frac{a}{b}$ 与 1 的关系(正数时)$\frac{85^2}{84 \times 86} = \frac{7225}{7216} > 1$
估值/放缩利用已知不等式$e^x > 1 + x$,$\ln x < x - 1$

六、关系运算的实际应用

6.1 区间判断

fn classify_score(score: i32) -> &'static str {
    if score >= 90 {
        "优秀"
    } else if score >= 80 {
        "良好"
    } else if score >= 70 {
        "中等"
    } else if score >= 60 {
        "及格"
    } else {
        "不及格"
    }
}

fn main() {
    println!("95 分: {}", classify_score(95));   // 优秀
    println!("75 分: {}", classify_score(75));   // 中等
    println!("55 分: {}", classify_score(55));   // 不及格
}

6.2 三数排序

fn sort_three(a: i32, b: i32, c: i32) -> (i32, i32, i32) {
    let mut arr = [a, b, c];
    arr.sort();
    (arr[0], arr[1], arr[2])
}

fn main() {
    let (min, mid, max) = sort_three(42, 17, 85);
    println!("最小: {}, 中间: {}, 最大: {}", min, mid, max);
    // 最小: 17, 中间: 42, 最大: 85
}

6.3 版本号比较

fn compare_versions(v1: &str, v2: &str) -> std::cmp::Ordering {
    let parts1: Vec<u32> = v1.split('.').map(|s| s.parse().unwrap()).collect();
    let parts2: Vec<u32> = v2.split('.').map(|s| s.parse().unwrap()).collect();

    for (a, b) in parts1.iter().zip(parts2.iter()) {
        match a.cmp(b) {
            std::cmp::Ordering::Equal => continue,
            other => return other,
        }
    }

    // 前缀相同时,更长的版本号更大
    parts1.len().cmp(&parts2.len())
}

fn main() {
    println!("1.2.3 vs 1.2.4: {:?}", compare_versions("1.2.3", "1.2.4"));  // Less
    println!("2.0.0 vs 1.9.9: {:?}", compare_versions("2.0.0", "1.9.9"));  // Greater
    println!("1.0 vs 1.0.0: {:?}", compare_versions("1.0", "1.0.0"));      // Less
}

6.4 查找最大值和最小值

fn find_min_max(data: &[i32]) -> (i32, i32) {
    assert!(!data.is_empty());
    let mut min = data[0];
    let mut max = data[0];

    for &val in data.iter().skip(1) {
        if val < min {
            min = val;
        }
        if val > max {
            max = val;
        }
    }

    (min, max)
}

fn main() {
    let data = [23, 56, 11, 89, 42, 67, 5, 78];
    let (min, max) = find_min_max(&data);
    println!("最小值: {}, 最大值: {}", min, max);
    // 最小值: 5, 最大值: 89

    // 也可以使用标准库方法
    let min2 = data.iter().min().unwrap();
    let max2 = data.iter().max().unwrap();
    println!("最小值: {}, 最大值: {}", min2, max2);
}

6.5 二分查找

关系运算是二分查找的核心:

fn binary_search(arr: &[i32], target: i32) -> Option<usize> {
    let mut left = 0usize;
    let mut right = arr.len();

    while left < right {
        let mid = left + (right - left) / 2;
        if arr[mid] == target {
            return Some(mid);
        } else if arr[mid] < target {
            left = mid + 1;
        } else {
            right = mid;
        }
    }

    None
}

fn main() {
    let sorted = [5, 12, 23, 34, 45, 56, 67, 78, 89, 100];

    match binary_search(&sorted, 45) {
        Some(idx) => println!("找到 45,索引为 {}", idx),
        None => println!("未找到 45"),
    }

    match binary_search(&sorted, 50) {
        Some(idx) => println!("找到 50,索引为 {}", idx),
        None => println!("未找到 50"),
    }
}

七、关系运算与模式匹配

在 Rust 中,模式匹配(match)经常与关系运算结合使用:

fn describe_temperature(temp: f64) -> &'static str {
    match temp {
        t if t < 0.0 => "冰点以下",
        t if t < 15.0 => "寒冷",
        t if t < 25.0 => "舒适",
        t if t < 35.0 => "炎热",
        _ => "酷暑",
    }
}

fn main() {
    println!("-5°C:  {}", describe_temperature(-5.0));   // 冰点以下
    println!("10°C:  {}", describe_temperature(10.0));  // 寒冷
    println!("20°C:  {}", describe_temperature(20.0));  // 舒适
    println!("30°C:  {}", describe_temperature(30.0));  // 炎热
    println!("40°C:  {}", describe_temperature(40.0));  // 酷暑
}

注意: match 中的 if 守卫(guard)允许我们在模式匹配中使用关系运算,使代码更加灵活。


八、总结

概念要点
关系运算符==!=>>=<<=,返回 bool
PartialOrd部分有序,partial_cmp 返回 Option<Ordering>,适用于浮点数
Ord完全有序,cmp 返回 Ordering,适用于整数、字符串等
浮点数比较避免直接 ==,使用误差范围或 total_cmp
NaN不等于自身,partial_cmp 返回 None
自定义比较实现 PartialOrd + PartialEq(或 Ord + Eq

关系运算是编程中最基础的操作之一。理解 PartialOrdOrd 的区别、掌握浮点数比较的正确方法,是写出健壮 Rust 程序的关键。

练习建议:

  1. 尝试为自定义结构体实现 Ord trait,支持多字段排序
  2. 编写一个通用的近似相等比较函数
  3. 用二分查找实现“查找第一个大于等于目标值的元素“

第十六 逻辑运算

逻辑运算(Logical Operation)是计算机科学和数学中最基础的运算之一,它处理的是布尔值(truefalse)之间的关系。在 Rust 中,逻辑运算不仅是条件判断的核心,还深刻影响着程序的控制流和性能。


一、布尔代数基础

1.1 什么是布尔代数

布尔代数(Boolean Algebra)由英国数学家乔治·布尔(George Boole)在 19 世纪中叶创立,是一种用于描述逻辑关系的代数系统。它只有两个值:true(真,记为 1)和 false(假,记为 0)。

布尔代数是现代数字电路、计算机逻辑设计和编程语言的理论基础。

1.2 基本逻辑运算

布尔代数定义了三种基本运算:

运算符号Rust 运算符含义
与(AND)∧ 或 ·&&两个条件同时为真,结果才为真
或(OR)∨ 或 +||两个条件至少一个为真,结果就为真
非(NOT)¬ 或 ¯!条件取反

1.3 真值表

真值表(Truth Table)是表示逻辑运算所有可能输入和对应输出的表格。

与运算(AND)真值表:

pqp ∧ q(p && q)
000
010
100
111

口诀:“有假则假,全真才真”

或运算(OR)真值表:

pqp ∨ q(p || q)
000
011
101
111

口诀:“有真则真,全假才假”

非运算(NOT)真值表:

p¬p(!p)
01
10

口诀:“真假互换”

1.4 复合逻辑运算

除了三种基本运算,布尔代数还定义了两种常用的复合运算:

异或(XOR,Exclusive OR)

pqp ⊕ q
000
011
101
110

口诀:“相同为假,不同为真”

异或在 Rust 中没有专门的逻辑运算符,但可以用 (p || q) && !(p && q) 或位运算符 ^ 实现。

同或(XNOR,Equivalence)

pqp ↔ q
001
010
100
111

口诀:“相同为真,不同为假”(异或的反面)

1.5 布尔代数的基本定律

布尔代数遵循一系列重要的定律,这些定律在简化逻辑表达式和优化程序条件时非常有用。

交换律(Commutative Laws)

p ∧ q = q ∧ p
p ∨ q = q ∨ p

结合律(Associative Laws)

(p ∧ q) ∧ r = p ∧ (q ∧ r)
(p ∨ q) ∨ r = p ∨ (q ∨ r)

分配律(Distributive Laws)

p ∧ (q ∨ r) = (p ∧ q) ∨ (p ∧ r)
p ∨ (q ∧ r) = (p ∨ q) ∧ (p ∨ r)

同一律(Identity Laws)

p ∧ true = p
p ∨ false = p

零律(Domination Laws)

p ∧ false = false
p ∨ true = true

幂等律(Idempotent Laws)

p ∧ p = p
p ∨ p = p

补律(Complement Laws)

p ∧ ¬p = false
p ∨ ¬p = true
¬(¬p) = p

德摩根定律(De Morgan’s Laws)

德摩根定律是最重要的布尔代数定律之一,在编程中经常用于简化复杂的条件表达式:

¬(p ∧ q) = ¬p ∨ ¬q
¬(p ∨ q) = ¬p ∧ ¬q

用 Rust 代码表示:

#![allow(unused)]
fn main() {
// 德摩根定律示例
fn de_morgan_example() {
    let p = true;
    let q = false;

    // ¬(p ∧ q) = ¬p ∨ ¬q
    let left1 = !(p && q);
    let right1 = !p || !q;
    assert_eq!(left1, right1);

    // ¬(p ∨ q) = ¬p ∧ ¬q
    let left2 = !(p || q);
    let right2 = !p && !q;
    assert_eq!(left2, right2);
}
}

德摩根定律的通俗理解:

  • “不是(A 且 B)” 等价于 “不是 A 或不是 B”
  • “不是(A 或 B)” 等价于 “不是 A 且不是 B”

吸收律(Absorption Laws)

p ∧ (p ∨ q) = p
p ∨ (p ∧ q) = p

二、Rust 中的逻辑运算符

2.1 逻辑运算符概览

Rust 提供了三种逻辑运算符,分别对应布尔代数的三种基本运算:

#![allow(unused)]
fn main() {
&&    // 逻辑与(conditional AND)
||    // 逻辑或(conditional OR)
!     // 逻辑非(NOT)
}
fn main() {
    let a = true;
    let b = false;

    println!("a && b = {}", a && b);  // false
    println!("a || b = {}", a || b);  // true
    println!("!a = {}", !a);          // false
}

2.2 逻辑与(&&)

逻辑与运算符 && 要求两边的操作数都为 true,结果才为 true

fn main() {
    let x = 5;
    let y = 10;

    // 两个条件都满足
    if x > 0 && y > 0 {
        println!("x 和 y 都是正数");
    }

    // 一个条件不满足
    if x > 0 && y < 0 {
        println!("这不会被执行");
    }
}

逻辑与的真值表(Rust):

表达式结果
true && truetrue
true && falsefalse
false && truefalse
false && falsefalse

2.3 逻辑或(||)

逻辑或运算符 || 要求两边的操作数至少有一个为 true,结果就为 true

fn main() {
    let score = 85;

    // 满足任一条件即可
    if score >= 90 || score >= 60 && score < 90 {
        println!("成绩合格");
    }

    // 更清晰的写法
    if score >= 60 {
        println!("成绩合格");
    }
}

逻辑或的真值表(Rust):

表达式结果
true || truetrue
true || falsetrue
false || truetrue
false || falsefalse

2.4 逻辑非(!)

逻辑非运算符 ! 对布尔值取反。

fn main() {
    let is_raining = false;

    if !is_raining {
        println!("今天不下雨,可以出门");
    }

    // 双重否定等于肯定
    assert_eq!(!!is_raining, is_raining);
}

逻辑非的真值表(Rust):

表达式结果
!truefalse
!falsetrue

2.5 逻辑运算符与位运算符的区别

Rust 中有两套看似相似但功能不同的运算符,初学者容易混淆:

运算符类型操作数用途示例
&&逻辑与布尔值逻辑判断true && false
||逻辑或布尔值逻辑判断true || false
!逻辑非布尔值逻辑取反!true
&位与整数按位运算0b1010 & 0b1100
|位或整数按位运算0b1010 | 0b1100
~位非整数按位取反~0b1010
^位异或整数按位运算0b1010 ^ 0b1100
fn main() {
    // 逻辑运算:操作数和结果都是 bool
    let logical = true && false;
    println!("逻辑与: {}", logical);  // false

    // 位运算:操作数和结果都是整数
    let bitwise = 0b1010 & 0b1100;
    println!("位与: {:04b}", bitwise);  // 1000

    // 注意:& 和 | 也可以用于 bool,但不会短路
    let a = true & false;  // false,但两边都会求值
    let b = true | false;  // true,但两边都会求值
}

关键区别:

  • &&||短路运算符,只用于 bool 类型
  • &|非短路运算符,可用于整数位运算,也可用于 bool(但两边都会求值)

三、短路求值(Short-Circuit Evaluation)

3.1 什么是短路求值

短路求值是逻辑运算的一个重要特性:

  • 对于 &&(逻辑与):如果左边的表达式为 false,右边的表达式不会执行,因为无论右边是什么,结果都一定是 false
  • 对于 ||(逻辑或):如果左边的表达式为 true,右边的表达式不会执行,因为无论右边是什么,结果都一定是 true
fn main() {
    let a = false;

    // 由于 a 是 false,expensive_function() 不会被执行
    if a && expensive_function() {
        println!("不会执行到这里");
    }

    let b = true;

    // 由于 b 是 true,expensive_function() 不会被执行
    if b || expensive_function() {
        println!("会执行到这里");
    }
}

fn expensive_function() -> bool {
    println!("执行了耗时操作...");
    true
}

3.2 短路求值的实际应用

应用一:避免不必要的计算

fn main() {
    let data: Vec<i32> = vec![];

    // 先检查是否为空,避免对空数组进行索引操作
    if !data.is_empty() && data[0] > 0 {
        println!("第一个元素是正数");
    }

    // 如果 data 为空,data[0] 不会被执行,避免了 panic
}

应用二:安全地进行空值检查

fn main() {
    let maybe_value: Option<i32> = None;

    // 先检查 Some,再解包
    if maybe_value.is_some() && maybe_value.unwrap() > 0 {
        println!("值是正数");
    }

    // 更地道的 Rust 写法
    if let Some(value) = maybe_value {
        if value > 0 {
            println!("值是正数: {}", value);
        }
    }
}

应用三:设置默认值

#![allow(unused)]
fn main() {
fn get_config_value(user_value: Option<i32>) -> i32 {
    // 如果 user_value 为 None,则使用默认值 42
    user_value.unwrap_or(42)
}

// 或者用逻辑运算的思路(虽然 Rust 中更常用 unwrap_or)
fn get_config_value_alt(user_value: Option<i32>) -> i32 {
    if user_value.is_some() {
        user_value.unwrap()
    } else {
        42
    }
}
}

3.3 短路求值的注意事项

虽然短路求值通常是有益的,但在某些情况下需要注意:

fn main() {
    let mut counter = 0;

    // 由于短路,counter 只增加一次
    let result = increment(&mut counter) || increment(&mut counter);
    println!("counter = {}", counter);  // 输出 1,不是 2

    // 如果需要两边都执行,使用非短路运算符 |
    counter = 0;
    let result2 = increment(&mut counter) | increment(&mut counter);
    println!("counter = {}", counter);  // 输出 2
}

fn increment(counter: &mut i32) -> bool {
    *counter += 1;
    true
}

注意: 当需要确保所有副作用(如函数调用、变量修改)都执行时,应该使用非短路的 &| 运算符。


四、逻辑运算在 Rust 中的高级应用

4.1 复杂的条件表达式

在实际编程中,我们经常需要组合多个条件:

fn can_vote(age: u32, is_citizen: bool, has_criminal_record: bool) -> bool {
    // 必须成年、是公民、没有犯罪记录
    age >= 18 && is_citizen && !has_criminal_record
}

fn main() {
    println!("可以投票: {}", can_vote(20, true, false));   // true
    println!("可以投票: {}", can_vote(16, true, false));   // false
    println!("可以投票: {}", can_vote(20, false, false));  // false
    println!("可以投票: {}", can_vote(20, true, true));    // false
}

4.2 利用德摩根定律简化条件

德摩根定律可以帮助我们简化复杂的否定条件:

fn main() {
    let a = true;
    let b = false;

    // 原始写法:判断"不是(a 和 b 都为真)"
    let condition1 = !(a && b);

    // 用德摩根定律简化:"a 为假 或 b 为假"
    let condition2 = !a || !b;

    assert_eq!(condition1, condition2);

    // 实际应用:检查用户输入是否有效
    let has_username = true;
    let has_password = true;

    // 原始写法
    let is_invalid1 = !(has_username && has_password);

    // 简化写法(更易读)
    let is_invalid2 = !has_username || !has_password;

    assert_eq!(is_invalid1, is_invalid2);
}

4.3 逻辑运算与类型系统

Rust 的类型系统与逻辑运算有深刻的联系:

// Rust 中没有隐式类型转换,逻辑运算必须作用于 bool
fn main() {
    let x = 1;
    // let result = x && true;  // 错误!x 是 i32,不是 bool

    // 必须显式转换
    let result = (x != 0) && true;
    println!("{}", result);  // true
}

4.4 逻辑运算与模式匹配

在 Rust 中,模式匹配往往比复杂的逻辑运算更清晰:

fn describe_number(n: i32) -> &'static str {
    // 使用逻辑运算
    if n > 0 && n % 2 == 0 {
        "正偶数"
    } else if n > 0 && n % 2 != 0 {
        "正奇数"
    } else if n < 0 {
        "负数"
    } else {
        "零"
    }
}

fn describe_number_match(n: i32) -> &'static str {
    // 使用模式匹配(更地道)
    match n {
        0 => "零",
        n if n > 0 && n % 2 == 0 => "正偶数",
        n if n > 0 => "正奇数",
        _ => "负数",
    }
}

fn main() {
    println!("{}", describe_number(4));         // 正偶数
    println!("{}", describe_number_match(4));   // 正偶数
}

五、逻辑运算的实际案例

5.1 用户权限校验

struct User {
    name: String,
    is_active: bool,
    is_admin: bool,
    age: u32,
}

impl User {
    fn can_access_admin_panel(&self) -> bool {
        // 必须同时满足:账号活跃、是管理员、已成年
        self.is_active && self.is_admin && self.age >= 18
    }

    fn can_view_content(&self) -> bool {
        // 满足任一条件即可:账号活跃 或 是管理员
        self.is_active || self.is_admin
    }
}

fn main() {
    let user = User {
        name: String::from("张三"),
        is_active: true,
        is_admin: false,
        age: 25,
    };

    println!("可以访问管理后台: {}", user.can_access_admin_panel());  // false
    println!("可以查看内容: {}", user.can_view_content());            // true
}

5.2 数据验证

fn is_valid_password(password: &str) -> bool {
    // 密码必须同时满足:
    // 1. 长度至少 8 位
    // 2. 包含至少一个大写字母
    // 3. 包含至少一个小写字母
    // 4. 包含至少一个数字

    let has_min_length = password.len() >= 8;
    let has_uppercase = password.chars().any(|c| c.is_ascii_uppercase());
    let has_lowercase = password.chars().any(|c| c.is_ascii_lowercase());
    let has_digit = password.chars().any(|c| c.is_ascii_digit());

    has_min_length && has_uppercase && has_lowercase && has_digit
}

fn main() {
    println!("'Hello123' 有效: {}", is_valid_password("Hello123"));     // false(长度不够)
    println!("'HelloWorld1' 有效: {}", is_valid_password("HelloWorld1")); // true
    println!("'helloworld1' 有效: {}", is_valid_password("helloworld1")); // false(没有大写)
}

5.3 游戏状态判断

struct GameState {
    player_alive: bool,
    has_key: bool,
    door_open: bool,
    enemies_defeated: bool,
}

impl GameState {
    fn can_exit_level(&self) -> bool {
        // 可以退出关卡的条件:玩家存活,且(门已开 或 所有敌人被击败)
        self.player_alive && (self.door_open || self.enemies_defeated)
    }

    fn game_over(&self) -> bool {
        // 游戏结束条件:玩家死亡
        !self.player_alive
    }
}

fn main() {
    let state = GameState {
        player_alive: true,
        has_key: true,
        door_open: false,
        enemies_defeated: true,
    };

    println!("可以退出关卡: {}", state.can_exit_level());  // true
    println!("游戏结束: {}", state.game_over());           // false
}

六、总结

概念要点
布尔代数只有 truefalse 两个值,定义了与、或、非三种基本运算
基本定律交换律、结合律、分配律、德摩根定律等
Rust 逻辑运算符&&(与)、||(或)、!(非)
短路求值&& 左假则短路,|| 左真则短路,可提高性能并避免错误
与位运算区别逻辑运算符用于 bool,位运算符用于整数

逻辑运算是编程中最基础也是最重要的概念之一。掌握布尔代数的定律和 Rust 逻辑运算符的短路特性,可以帮助我们写出更简洁、更高效、更安全的代码。

练习建议:

  1. 尝试用德摩根定律简化复杂的条件表达式
  2. 利用短路求值避免不必要的计算和潜在的错误
  3. 在适当的场景下,用模式匹配替代复杂的逻辑判断

第十七 位运算

位运算概述

计算机底层数据以二进制形式存储,每一位(bit)只能是 0 或 1。位运算直接对二进制位进行操作,是计算机最底层的运算方式。与加减乘除等算术运算相比,位运算具有以下优势:

  • 速度快:位运算是 CPU 原生支持的指令,通常只需一个时钟周期
  • 空间省:一个位即可表示一个布尔状态,可用于紧凑的数据编码
  • 用途广:权限控制、数据压缩、加密算法、图形处理等都离不开位运算

Rust 作为一门系统级编程语言,完整支持所有位运算操作,并通过 std::ops 模块提供了对应的运算符 trait,允许用户自定义类型的位运算行为。

位运算

六种基本位运算一览

运算运算符Rust trait说明
与(AND)&BitAnd两位都为1,结果才为1
或(OR)|BitOr有一位为1,结果就为1
非(NOT)! / ~Not按位取反
异或(XOR)^BitXor相同为0,不同为1
左移(SHL)<<Shl所有位向左移动
右移(SHR)>>Shr所有位向右移动

与(AND)运算

运算符:&

真值表

aba & b
000
010
100
111

只有当两个对应位都为 1 时,结果的该位才为 1,否则为 0。

常见用途

1. 掩码(Mask)——提取特定位

掩码是与运算最经典的应用。通过构造一个特定的二进制数(掩码),可以提取目标数据中的指定位。

fn main() {
    let value: u8 = 0b1011_0110; // 182

    // 提取低4位
    let low_4 = value & 0b0000_1111; // 0b0110 = 6
    println!("低4位: {:04b} = {}", low_4, low_4);

    // 提取高4位
    let high_4 = (value & 0b1111_0000) >> 4; // 0b1011 = 11
    println!("高4位: {:04b} = {}", high_4, high_4);

    // 提取第3位(从0开始计数)
    let bit_3 = (value >> 3) & 1;
    println!("第3位: {}", bit_3);
}

2. 清零特定位

要将某一位或某几位清零,只需构造一个掩码,目标位为 0,其余位为 1,然后做与运算。

fn main() {
    let mut flags: u8 = 0b1111_1111; // 所有标志位都开启

    // 将第2位和第5位清零
    let mask = !(0b0100_0100); // 0b1011_1011
    flags = flags & mask;
    println!("清零后: {:08b}", flags); // 10111011
}

3. 判断奇偶

一个数的二进制最低位决定了它的奇偶性:最低位为 1 是奇数,为 0 是偶数。

fn is_odd(n: i32) -> bool {
    n & 1 == 1
}

fn is_even(n: i32) -> bool {
    n & 1 == 0
}

fn main() {
    println!("5是奇数: {}", is_odd(5));   // true
    println!("8是偶数: {}", is_even(8));  // true
}

相比 n % 2 == 0,位运算方式避免了除法操作,效率更高。

Brian Kernighan 算法

Brian Kernighan 算法是位运算中最优雅的技巧之一。核心思想是:n & (n - 1) 可以将 n 的二进制表示中最低位的 1 置为 0

原理分析:对于一个二进制数 nn - 1 会将最低位的 1 变为 0,并将该位之后的所有 0 变为 1。因此 n & (n - 1) 恰好消除了最低位的 1。

$$n & (n-1) \Rightarrow \text{消除最低位的 } 1$$

/// 统计二进制中1的个数(Brian Kernighan算法)
fn count_ones(n: u64) -> u64 {
    let mut n = n;
    let mut count = 0;
    while n != 0 {
        n = n & (n - 1); // 每次消除一个1
        count += 1;
    }
    count
}

fn main() {
    println!("15(1111)中1的个数: {}", count_ones(15));  // 4
    println!("7(0111)中1的个数: {}", count_ones(7));    // 3
    println!("0中1的个数: {}", count_ones(0));           // 0
    println!("1024中1的个数: {}", count_ones(1024));     // 1
}

时间复杂度:$O(k)$,其中 $k$ 为二进制中 1 的个数,而非总位数。最坏情况下为 $O(\log n)$。

判断2的幂

利用 Brian Kernighan 算法的思想,可以高效判断一个数是否为 2 的幂。2 的幂的二进制表示中有且仅有一个 1。

$$n & (n-1) == 0 \text{ 且 } n > 0 \Rightarrow n \text{ 是2的幂}$$

fn is_power_of_two(n: u32) -> bool {
    n != 0 && (n & (n - 1)) == 0
}

fn main() {
    for i in 1..=32 {
        if is_power_of_two(i) {
            println!("{} 是2的幂", i);
        }
    }
    // 输出: 1, 2, 4, 8, 16, 32
}

或(OR)运算

运算符:|

真值表

aba | b
000
011
101
111

只要两个对应位中有一个为 1,结果的该位就为 1。

常见用途

1. 置位(Set Bit)

要将某一位或某几位设置为 1,构造一个掩码,目标位为 1,其余位为 0,然后做或运算。

fn set_bit(mut value: u8, bit_index: usize) -> u8 {
    value | (1 << bit_index)
}

fn main() {
    let mut x: u8 = 0b1010_0000;
    x = set_bit(x, 0); // 设置第0位
    println!("设置第0位后: {:08b}", x); // 10100001
    x = set_bit(x, 3); // 设置第3位
    println!("设置第3位后: {:08b}", x); // 10101001
}

2. 权限组合(位标志)

位标志(bit flags)是或运算最典型的应用场景。每个标志占一个位,通过或运算可以组合多个标志。

#[derive(Debug, Clone, Copy)]
struct Permissions(u8);

const READ: u8    = 0b0000_0001; // 1
const WRITE: u8   = 0b0000_0010; // 2
const EXECUTE: u8 = 0b0000_0100; // 4

impl Permissions {
    fn new() -> Self {
        Permissions(0)
    }

    fn add(&mut self, perm: u8) {
        self.0 |= perm;
    }

    fn has(&self, perm: u8) -> bool {
        (self.0 & perm) != 0
    }

    fn remove(&mut self, perm: u8) {
        self.0 &= !perm;
    }
}

fn main() {
    let mut perms = Permissions::new();
    perms.add(READ | WRITE); // 组合权限
    println!("权限: {:08b}", perms.0); // 00000011

    println!("可读: {}", perms.has(READ));    // true
    println!("可写: {}", perms.has(WRITE));   // true
    println!("可执行: {}", perms.has(EXECUTE)); // false

    perms.remove(WRITE);
    println!("移除写权限后可写: {}", perms.has(WRITE)); // false
}

在实际开发中,Rust 的 bitflags crate 提供了更完善的位标志支持,可以安全、方便地定义和操作位标志。


非(NOT)运算

运算符:!(Rust 中推荐使用)或 ~

真值表

a!a
01
10

按位取反:将每一位 0 变 1、1 变 0。

补码表示

在计算机中,有符号整数采用**补码(two’s complement)**表示。取反运算的结果需要结合补码来理解:

$$\text{补码} = \text{原码取反} + 1$$

fn main() {
    let x: u8 = 0b0000_1111; // 15
    let y = !x;
    println!("!{:08b} = {:08b}", x, y); // !00001111 = 11110000 = 240

    // 对于有符号数,!的结果是补码表示
    let a: i8 = 5;  // 00000101
    let b = !a;      // 11111010 = -6(补码)
    println!("!{} = {}", a, b); // !5 = -6

    let c: i8 = 60;
    let d = !c;      // 11110011 = -61(补码)
    println!("!{} = {}", c, d); // !60 = -61
}

Rust 中的区别

Rust 中 ! 运算符既用于按位取反(整数类型),也用于逻辑非(bool 类型),具体行为取决于操作数的类型:

fn main() {
    // 整数类型:按位取反
    let x: i32 = 0b0000_1111;
    println!("按位取反: {}", !x); // -16

    // bool类型:逻辑非
    let flag = true;
    println!("逻辑非: {}", !flag); // false

    // ~ 运算符在Rust中已弃用,统一使用 !
    // 旧版Rust中 ~ 用于整数取反,现已不推荐
}

注意:在 Rust 中,~ 运算符已被弃用,统一使用 ! 进行按位取反。这与 C/C++ 中 ~ 用于取反、! 用于逻辑非的惯例不同。


异或(XOR)运算

运算符:^

真值表

aba ^ b
000
011
101
110

相同为 0,不同为 1。异或运算也称为“不进位加法“。

异或运算(XOR)

异或运算 XOR 教程

运算法则

异或运算具有丰富的数学性质,是解决许多位运算问题的核心工具:

  1. 归零律:$a \oplus a = 0$

    任何数异或其本身都等于 0。因为每一位都相同,按异或定义结果全为 0。

  2. 恒等律:$a \oplus 0 = a$

    任何数异或 0 都等于其本身。因为 0 不改变任何位的值。

  3. 交换律:$a \oplus b = b \oplus a$

    异或运算不依赖操作数的顺序,与加法类似。

  4. 结合律:$a \oplus b \oplus c = a \oplus (b \oplus c) = (a \oplus b) \oplus c$

    多个数连续异或时,可以任意调整计算顺序。

  5. 自反性:$a \oplus b \oplus a = b$

    由归零律和交换律推导:$a \oplus b \oplus a = a \oplus a \oplus b = 0 \oplus b = b$。这是异或交换变量的理论基础。

  6. 可逆性:若 $d = a \oplus b \oplus c$,则 $a = d \oplus b \oplus c$

    异或运算可以“撤销“:知道结果和除一个操作数外的所有操作数,可以恢复那个操作数。

  7. 逐位运算示例:若 $x = \text{0b0101}$,$y = \text{0b1011}$,则 $x \oplus y = \text{0b1110}$

    0101
    1011
    ----
    1110
    
  8. 连续四整数归零:$\forall i \in \mathbb{Z}$,有 $4i \oplus (4i+1) \oplus (4i+2) \oplus (4i+3) = 0$

    四个连续整数异或结果为 0,因为它们覆盖了所有两位组合(00, 01, 10, 11),每一位上恰好有两个 0 和两个 1。

常见用途

1. 交换变量(不用临时变量)

利用自反性 $a \oplus b \oplus a = b$,可以在不借助临时变量的情况下交换两个数:

fn xor_swap(a: &mut i32, b: &mut i32) {
    *a ^= *b;
    *b ^= *a;
    *a ^= *b;
}

fn main() {
    let mut x = 42;
    let mut y = 99;
    xor_swap(&mut x, &mut y);
    println!("x = {}, y = {}", x, y); // x = 99, y = 42
}

注意:实际开发中建议使用 std::mem::swap,异或交换在两个变量指向同一内存地址时会产生错误结果(变为 0)。

2. 加密解密

异或运算具有对称性:$A \oplus K = C$,$C \oplus K = A$。用同一个密钥加密和解密。

fn xor_encrypt(plaintext: &[u8], key: u8) -> Vec<u8> {
    plaintext.iter().map(|&b| b ^ key).collect()
}

fn xor_decrypt(ciphertext: &[u8], key: u8) -> Vec<u8> {
    // 解密和加密是同一操作
    xor_encrypt(ciphertext, key)
}

fn main() {
    let message = b"Hello, Rust!";
    let key = 0x42;

    let encrypted = xor_encrypt(message, key);
    let decrypted = xor_decrypt(&encrypted, key);

    println!("原文: {:?}", String::from_utf8_lossy(message));
    println!("密文: {:?}", encrypted);
    println!("解密: {:?}", String::from_utf8_lossy(&decrypted));
}

3. 查找唯一数(LeetCode 实战)

题目136. 只出现一次的数字

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现一次的元素。

思路:利用归零律和交换律,所有出现两次的数异或后互相抵消为 0,最终结果就是唯一数。

$$a \oplus a \oplus b \oplus b \oplus \cdots \oplus x = 0 \oplus 0 \oplus \cdots \oplus x = x$$

pub fn single_number(nums: Vec<i32>) -> i32 {
    nums.into_iter().fold(0, |acc, x| acc ^ x)
}

fn main() {
    let nums = vec![4, 1, 2, 1, 2];
    println!("只出现一次的数字: {}", single_number(nums)); // 4
}

时间复杂度:$O(n)$,空间复杂度:$O(1)$。


左移(SHL)运算

运算符:<<

原理

左移运算将所有位向左移动指定位数,右边空出的位补 0。左移 n 位等价于乘以 $2^n$:

$$x \ll n = x \times 2^n$$

fn main() {
    let x: i32 = 5;
    let y = x << 1; // 5 * 2 = 10
    let z = x << 3; // 5 * 8 = 40
    println!("{} << 1 = {}", x, y); // 10
    println!("{} << 3 = {}", x, z); // 40

    // 快速计算 2 的幂
    println!("1 << 10 = {}", 1 << 10); // 1024
    println!("1 << 20 = {}", 1 << 20); // 1048576
}

溢出处理

左移时,如果移出的高位被丢弃,可能导致溢出。Rust 在 debug 模式下会检查整数溢出,在 release 模式下会自动回绕(wrapping)。

fn main() {
    let x: u8 = 128; // 10000000
    let y = x << 1;  // debug模式下panic,release模式下为0

    // 使用wrapping方法显式处理溢出
    let safe = x.wrapping_shl(1);
    println!("wrapping左移: {}", safe); // 0

    // 使用checked方法检查溢出
    match x.checked_shl(1) {
        Some(v) => println!("结果: {}", v),
        None => println!("左移溢出!"),
    }
}

右移(SHR)运算

运算符:>>

原理

右移运算将所有位向右移动指定位数。右移 n 位等价于整除 $2^n$:

$$x \gg n = \lfloor x / 2^n \rfloor$$

fn main() {
    // 偶数右移
    let x = 1024;
    let y = x >> 1; // 1024 / 2 = 512
    println!("{} >> 1 = {}", x, y); // 512

    // 奇数右移(向下取整)
    let x = 11;
    let y = x >> 1; // 11 / 2 = 5(截断小数部分)
    println!("{} >> 1 = {}", x, y); // 5

    // 快速除以 8
    let x = 1000;
    let y = x >> 3; // 1000 / 8 = 125
    println!("{} >> 3 = {}", x, y); // 125
}

算术右移 vs 逻辑右移

右移分为两种:

  • 逻辑右移:高位补 0(用于无符号数)
  • 算术右移:高位补符号位(用于有符号数,保持正负不变)
fn main() {
    // 无符号数:逻辑右移(高位补0)
    let x: u8 = 0b1000_0000; // 128
    let y = x >> 1;            // 0b0100_0000 = 64
    println!("无符号右移: {:08b} -> {:08b} ({} -> {})", x, y, x, y);

    // 有符号数:算术右移(高位补符号位)
    let x: i8 = -128; // 10000000(补码)
    let y = x >> 1;    // 11000000 = -64(高位补1)
    println!("有符号右移: {} -> {}", x, y);

    // 正数有符号右移(高位补0)
    let x: i8 = 127;  // 01111111
    let y = x >> 1;    // 00111111 = 63
    println!("正数有符号右移: {} -> {}", x, y);
}

Rust 中的位运算 trait

Rust 通过 std::ops 模块提供了一组 trait,允许自定义类型支持位运算操作符:

trait对应运算符说明
std::ops::BitAnd&按位与
std::ops::BitAndAssign&=按位与赋值
std::ops::BitOr|按位或
std::ops::BitOrAssign|=按位或赋值
std::ops::BitXor^按位异或
std::ops::BitXorAssign^=按位异或赋值
std::ops::Not!按位取反
std::ops::Shl<<左移
std::ops::ShlAssign<<=左移赋值
std::ops::Shr>>右移
std::ops::ShrAssign>>=右移赋值

std::ops::BitAnd The bitwise AND operator &. std::ops::BitAndAssign The bitwise AND assignment operator &=. std::ops::BitOr The bitwise OR operator |. std::ops::BitOrAssign The bitwise OR assignment operator |=. std::ops::BitXor The bitwise XOR operator ^. std::ops::Not

use std::ops::{BitAnd, BitOr, BitXor, Not};

#[derive(Debug, Clone, Copy)]
struct Flags(u8);

impl BitAnd for Flags {
    type Output = Self;
    fn bitand(self, rhs: Self) -> Self::Output {
        Flags(self.0 & rhs.0)
    }
}

impl BitOr for Flags {
    type Output = Self;
    fn bitor(self, rhs: Self) -> Self::Output {
        Flags(self.0 | rhs.0)
    }
}

impl BitXor for Flags {
    type Output = Self;
    fn bitxor(self, rhs: Self) -> Self::Output {
        Flags(self.0 ^ rhs.0)
    }
}

impl Not for Flags {
    type Output = Self;
    fn not(self) -> Self::Output {
        Flags(!self.0)
    }
}

fn main() {
    let a = Flags(0b1100);
    let b = Flags(0b1010);
    println!("a & b = {:?}", a & b); // Flags(0b1000)
    println!("a | b = {:?}", a | b); // Flags(0b1110)
    println!("a ^ b = {:?}", a ^ b); // Flags(0b0110)
    println!("!a    = {:?}", !a);     // Flags(0b0011)
}

位运算综合技巧

获取、设置、清除、翻转特定位

以下是位操作的四大基本操作,是所有位运算技巧的基础:

/// 获取第n位的值(0或1)
fn get_bit(value: u32, n: usize) -> u32 {
    (value >> n) & 1
}

/// 设置第n位为1
fn set_bit(mut value: u32, n: usize) -> u32 {
    value | (1 << n)
}

/// 清除第n位为0
fn clear_bit(mut value: u32, n: usize) -> u32 {
    value & !(1 << n)
}

/// 翻转第n位
fn toggle_bit(mut value: u32, n: usize) -> u32 {
    value ^ (1 << n)
}

fn main() {
    let mut x: u32 = 0b1010_0101;

    println!("原始值: {:08b}", x);
    println!("第2位: {}", get_bit(x, 2));   // 1
    println!("设置第4位: {:08b}", set_bit(x, 4));    // 1011_0101
    println!("清除第0位: {:08b}", clear_bit(x, 0));  // 1010_0100
    println!("翻转第7位: {:08b}", toggle_bit(x, 7));  // 0010_0101
}

位段(Bit Field)

位段是一种将多个小数据打包到一个整数中的技术,常用于协议头、寄存器配置等场景。

/// IP协议头中的标志字段(3位)
#[derive(Debug)]
struct IpFlags {
    reserved: bool,   // 1位
    dont_fragment: bool, // 1位
    more_fragments: bool, // 1位
}

impl IpFlags {
    fn from_u8(value: u8) -> Self {
        IpFlags {
            reserved: (value & 0b100) != 0,
            dont_fragment: (value & 0b010) != 0,
            more_fragments: (value & 0b001) != 0,
        }
    }

    fn to_u8(&self) -> u8 {
        (self.reserved as u8) << 2
            | (self.dont_fragment as u8) << 1
            | (self.more_fragments as u8)
    }
}

fn main() {
    // DF=1, MF=0
    let flags = IpFlags {
        reserved: false,
        dont_fragment: true,
        more_fragments: false,
    };
    let packed = flags.to_u8();
    println!("打包: {:03b} = {}", packed, packed); // 010 = 2

    let unpacked = IpFlags::from_u8(0b110);
    println!("解包: {:?}", unpacked);
    // IpFlags { reserved: true, dont_fragment: true, more_fragments: false }
}

实战题目

191. 位1的个数

191. 位1的个数

编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字为 ‘1’ 的个数。

pub fn hamming_weight(n: u32) -> u32 {
    let mut count = 0;
    let mut n = n;
    while n != 0 {
        n &= n - 1; // Brian Kernighan算法
        count += 1;
    }
    count
}

fn main() {
    println!("11(1011)中1的个数: {}", hamming_weight(11)); // 3
    println!("128中1的个数: {}", hamming_weight(128));      // 1
    println!("4294967293中1的个数: {}", hamming_weight(4294967293)); // 31
}

461. 汉明距离

461. 汉明距离

两个整数之间的汉明距离是指这两个数字二进制对应位不同的位置的数目。

pub fn hamming_distance(x: i32, y: i32) -> i32 {
    let xor = x ^ y; // 不同位为1
    let mut count = 0;
    let mut n = xor;
    while n != 0 {
        n &= n - 1;
        count += 1;
    }
    count
}

fn main() {
    println!("汉明距离(1,4): {}", hamming_distance(1, 4)); // 2
    // 1 = 0001, 4 = 0100, xor = 0101, 有2个1
}

更多实战题目


总结

运算运算符核心规则典型应用
与(AND)&全1则1掩码提取、清零位、判断奇偶
或(OR)|有1则1置位、权限组合
非(NOT)!0变1,1变0构造掩码、补码运算
异或(XOR)^相同0,不同1交换变量、加密解密、查找唯一数
左移(SHL)<<右边补0快速乘 $2^n$
右移(SHR)>>无符号补0,有符号补符号位快速除 $2^n$

练习题

  1. 统计奇数位:给定一个 u32,统计其二进制中所有奇数位(第1、3、5…位)上 1 的个数。

  2. 翻转所有位:不使用 ! 运算符,实现一个函数翻转一个 u32 的所有位。

  3. 两数交换:实现一个函数,仅使用异或运算交换两个 i32 变量的值,并分析其局限性。

  4. 位图实现:使用 u64 数组实现一个简单的位图(bitmap),支持 set、get、clear 操作,能表示 $[0, 1023]$ 范围内的整数集合。

  5. 只出现一次的数字 IILeetCode 137):给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现一次的元素。提示:考虑每一位的统计。

第十八 交换、反转、旋转

概述

交换(swap)、反转(reverse)、旋转(rotate)是计算机科学中最基础的三种操作。它们看似简单,却是许多经典算法的基石:

  • 排序算法中的元素交换
  • 字符串处理中的翻转与旋转
  • 数组操作中的轮转
  • 链表操作中的指针交换
  • 矩阵运算中的行列变换

这三者之间存在深刻的内在联系:rotate 通过三次 reverse 实现,reverse 通过 swap 实现。理解这一关系,有助于我们以统一的视角看待各种数据变换操作。


交换(swap)操作

原理

交换操作将两个位置上的值互换。最基本的实现方式是借助一个临时变量:

$$\text{temp} = a, \quad a = b, \quad b = \text{temp}$$

也可以利用异或运算(XOR)实现无临时变量的交换(详见第十七章位运算):

$$a = a \oplus b, \quad b = a \oplus b, \quad a = a \oplus b$$

Rust 中的 swap 方法

Rust 标准库提供了多种 swap 方法,覆盖了不同的数据结构:

1. slice.swap() – 交换切片中两个位置的元素

fn main() {
    let mut arr = [1, 2, 3, 4, 5];
    arr.swap(0, 4);
    println!("{:?}", arr); // [5, 2, 3, 4, 1]

    arr.swap(1, 3);
    println!("{:?}", arr); // [5, 4, 3, 2, 1]
}

2. Vec::swap() – 交换 Vec 中两个位置的元素

fn main() {
    let mut vec = vec!["a", "b", "c", "d"];
    vec.swap(0, 3);
    println!("{:?}", vec); // ["d", "b", "c", "a"]
}

3. std::mem::swap – 交换两个变量的值

use std::mem;

fn main() {
    let mut a = String::from("hello");
    let mut b = String::from("world");

    mem::swap(&mut a, &mut b);
    println!("a = {}, b = {}", a, b); // a = world, b = hello
}

std::mem::swap 可以交换任意类型的两个值,不仅限于数值类型。它的底层实现使用了 std::ptr::swap_nonoverlapping,对于大型结构体也能高效完成(只交换内存,不涉及深拷贝)。

异或交换(不用临时变量)

利用异或运算的自反性 $a \oplus b \oplus a = b$,可以不借助临时变量实现交换:

fn xor_swap(a: &mut i32, b: &mut i32) {
    *a ^= *b;
    *b ^= *a;
    *a ^= *b;
}

fn main() {
    let mut x = 42;
    let mut y = 99;
    println!("交换前: x = {}, y = {}", x, y);
    xor_swap(&mut x, &mut y);
    println!("交换后: x = {}, y = {}", x, y); // x = 99, y = 42
}

注意:异或交换有一个致命缺陷 – 当两个变量指向同一内存地址时,结果会变为 0。因此实际开发中应优先使用 std::mem::swap

实战:剑指 Offer 03 – 数组中重复的数字

剑指 Offer 03. 数组中重复的数字

题目:在一个长度为 n 的数组 nums 里的所有数字都在 $0 \sim n-1$ 的范围内。找出数组中任意一个重复的数字。

思路:利用原地交换,将每个数字放到它“应该在“的位置。如果目标位置已经有相同的数字,说明找到了重复。

/// 方法:原地交换
pub fn find_repeat_number_v3(nums: Vec<i32>) -> i32 {
    let len = nums.len();
    let mut new_nums = nums;
    let mut i = 0;
    while i < len {
        let num = new_nums[i] as usize;
        if num == i {
            i += 1;
            continue;
        }
        if new_nums[num] as usize == num {
            return num as i32;
        }
        new_nums.swap(i, num);
    }

    -1
}

fn main() {
    let documents = Vec::from([2, 5, 3, 0, 5, 0]);
    let result = find_repeat_number_v3(documents);
    println!("result: {}", result); // 5 或 0
}

复杂度分析

  • 时间复杂度:$O(n)$,每个元素最多被交换一次到正确位置
  • 空间复杂度:$O(1)$,原地操作,不需要额外空间

更多 swap 应用场景

选择排序中的交换

fn selection_sort(arr: &mut [i32]) {
    let n = arr.len();
    for i in 0..n {
        let mut min_idx = i;
        for j in (i + 1)..n {
            if arr[j] < arr[min_idx] {
                min_idx = j;
            }
        }
        if min_idx != i {
            arr.swap(i, min_idx);
        }
    }
}

fn main() {
    let mut arr = [64, 25, 12, 22, 11];
    selection_sort(&mut arr);
    println!("{:?}", arr); // [11, 12, 22, 25, 64]
}

快速排序中的交换

fn quick_sort(arr: &mut [i32]) {
    if arr.len() <= 1 {
        return;
    }
    let pivot = partition(arr);
    quick_sort(&mut arr[..pivot]);
    quick_sort(&mut arr[pivot + 1..]);
}

fn partition(arr: &mut [i32]) -> usize {
    let len = arr.len();
    let pivot_idx = len / 2;
    arr.swap(pivot_idx, len - 1); // 将pivot放到末尾
    let mut i = 0;
    for j in 0..len - 1 {
        if arr[j] < arr[len - 1] {
            arr.swap(i, j);
            i += 1;
        }
    }
    arr.swap(i, len - 1); // 将pivot放到正确位置
    i
}

fn main() {
    let mut arr = [3, 6, 8, 10, 1, 2, 1];
    quick_sort(&mut arr);
    println!("{:?}", arr); // [1, 1, 2, 3, 6, 8, 10]
}

反转(reverse)操作

原理

反转操作将序列中的元素顺序完全颠倒。其核心思想是双指针交换:一个指针从头部向后移动,另一个指针从尾部向前移动,逐步交换两个指针所指的元素,直到两个指针相遇。

$$[a_0, a_1, \ldots, a_{n-2}, a_{n-1}] \xrightarrow{\text{reverse}} [a_{n-1}, a_{n-2}, \ldots, a_1, a_0]$$

Rust 中的 reverse 方法

1. slice.reverse() – 原地反转切片

fn main() {
    let mut arr = [1, 2, 3, 4, 5];
    arr.reverse();
    println!("{:?}", arr); // [5, 4, 3, 2, 1]
}

2. Vec::reverse() – 原地反转 Vec

fn main() {
    let mut vec = vec!["apple", "banana", "cherry"];
    vec.reverse();
    println!("{:?}", vec); // ["cherry", "banana", "apple"]
}

3. Iterator::rev() – 反转迭代器(惰性,不修改原数据)

fn main() {
    let arr = [1, 2, 3, 4, 5];

    // rev() 返回一个反转的迭代器,不修改原数组
    for item in arr.iter().rev() {
        print!("{} ", item); // 5 4 3 2 1
    }
    println!();

    // 收集为新的 Vec
    let reversed: Vec<i32> = arr.iter().rev().copied().collect();
    println!("{:?}", reversed); // [5, 4, 3, 2, 1]
}

注意slice.reverse() 是原地修改,而 Iterator::rev() 是惰性操作,返回一个新的迭代器视图,不会修改原始数据。

字符串反转

fn reverse_string(s: &str) -> String {
    s.chars().rev().collect()
}

fn main() {
    let original = "hello";
    let reversed = reverse_string(original);
    println!("{} -> {}", original, reversed); // hello -> olleh

    // 处理包含中文的字符串
    let chinese = "你好世界";
    let reversed_cn = reverse_string(chinese);
    println!("{} -> {}", chinese, reversed_cn); // 你好世界 -> 界世好你
}

注意:Rust 中的 String 不能直接使用 reverse() 方法(因为 String 不是切片)。需要通过 chars().rev().collect() 来实现,这样可以正确处理 UTF-8 多字节字符。

链表反转

链表反转是面试中的高频题目,体现了 swap 操作在指针操作中的应用:

#[derive(Debug)]
struct ListNode {
    val: i32,
    next: Option<Box<ListNode>>,
}

impl ListNode {
    fn new(val: i32) -> Self {
        ListNode { val, next: None }
    }
}

/// 反转链表(迭代法)
fn reverse_list(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
    let mut prev = None;
    let mut current = head;

    while let Some(mut node) = current {
        current = node.next.take(); // 暂存下一个节点
        node.next = prev;           // 反转指针
        prev = Some(node);          // 前进
    }

    prev
}

fn main() {
    // 构建链表 1 -> 2 -> 3 -> 4 -> 5
    let mut head = Some(Box::new(ListNode::new(1)));
    let mut current = head.as_mut().unwrap();
    for i in 2..=5 {
        current.next = Some(Box::new(ListNode::new(i)));
        current = current.next.as_mut().unwrap();
    }

    let reversed = reverse_list(head);
    // 输出: 5 -> 4 -> 3 -> 2 -> 1
    let mut node = &reversed;
    while let Some(n) = node {
        print!("{} -> ", n.val);
        node = &n.next;
    }
    println!("None");
}

实战:剑指 Offer 58-I – 翻转单词顺序

剑指 Offer 58 - I. 翻转单词顺序

题目:输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。

思路:先将整个字符串按空格分割为单词数组,然后反转整个数组,最后拼接。

pub fn reverse_words(s: String) -> String {
    let mut words: Vec<&str> = s.split(' ').collect();
    let mut result = String::new();
    words.reverse();
    for word in words {
        // 注意:按照" "分割,结果中空字符串为""而不是" "
        if !word.is_empty() {
            result = format!("{} {}", result, word);
        }
    }
    result.trim().to_string()
}

fn main() {
    let message = "the sky is blue";
    let result = reverse_words(message.to_string());
    println!("result: {}", result); // "blue is sky the"
}

复杂度分析

  • 时间复杂度:$O(n)$,分割和反转都是线性操作
  • 空间复杂度:$O(n)$,需要存储分割后的单词数组

回文判断

利用反转操作可以方便地判断一个字符串是否为回文:

fn is_palindrome(s: &str) -> bool {
    let chars: Vec<char> = s.chars().collect();
    let n = chars.len();
    for i in 0..n / 2 {
        if chars[i] != chars[n - 1 - i] {
            return false;
        }
    }
    true
}

fn main() {
    println!("\"racecar\" 是回文: {}", is_palindrome("racecar")); // true
    println!("\"hello\" 是回文: {}", is_palindrome("hello"));     // false
    println!("\"上海自来水来自海上\" 是回文: {}", is_palindrome("上海自来水来自海上")); // true
}

整数反转

7. 整数反转

pub fn reverse(x: i32) -> i32 {
    let sign = x.signum();
    let mut n = (x as i64).abs();
    let mut result: i64 = 0;

    while n > 0 {
        result = result * 10 + n % 10;
        n /= 10;
    }

    result *= sign as i64;

    if result > i32::MAX as i64 || result < i32::MIN as i64 {
        return 0;
    }

    result as i32
}

fn main() {
    println!("{}", reverse(123));    // 321
    println!("{}", reverse(-123));   // -321
    println!("{}", reverse(120));    // 21
    println!("{}", reverse(0));      // 0
}

旋转(rotate)操作

原理

旋转操作将序列中的元素循环移动。左旋 k 位意味着每个元素向左移动 k 个位置,超出边界的元素从另一端进入。

$$[a_0, a_1, \ldots, a_{k-1}, a_k, \ldots, a_{n-1}] \xrightarrow{\text{rotate_left}(k)} [a_k, \ldots, a_{n-1}, a_0, a_1, \ldots, a_{k-1}]$$

三次翻转法

旋转可以通过三次反转来实现,这是最优雅的旋转算法。Doug Mcllroy 给出了将十元数组向上旋转 5 个位置的翻手例子:初始时掌心对着我们的脸,左手在右手上面。通过“翻转左手“、“翻转右手”、“翻转双手“三次翻转,达到模拟向左旋转 5 位的效果。

rotate(旋转) 可以通过三次 reverse 实现; reverse(反转,颠倒,翻转) 可以通过交换(swap)实现。

左旋(rotate_left)

将数组左旋 k 位,通过以下三次 reverse 实现:

(1)rotate_left(mid) 可以通过以下三次reverse实现:
reverse(0, mid);      // 翻转前半部分
reverse(mid, len);    // 翻转后半部分
reverse(0, len);      // 翻转整个数组

[1, 2, 3, 4, 5, 6, 7] 左旋 3 位为例:

原始:   [1, 2, 3, | 4, 5, 6, 7]
步骤1:  [3, 2, 1, | 4, 5, 6, 7]  -- reverse(0, 3)
步骤2:  [3, 2, 1, | 7, 6, 5, 4]  -- reverse(3, 7)
步骤3:  [4, 5, 6, 7, | 1, 2, 3]  -- reverse(0, 7)

右旋(rotate_right)

将数组右旋 k 位,通过以下三次 reverse 实现:

(2)rotate_right(mid) 可以通过以下三次reverse实现:
reverse(0, len);      // 翻转整个数组
reverse(0, mid);       // 翻转前半部分
reverse(mid, len);     // 翻转后半部分

Rust 中的 rotate 方法

Rust 标准库为切片提供了内置的旋转方法:

1. slice.rotate_left() – 左旋

fn main() {
    let mut arr = [1, 2, 3, 4, 5, 6, 7];
    arr.rotate_left(3);
    println!("{:?}", arr); // [4, 5, 6, 7, 1, 2, 3]

    // 旋转超过长度时自动取模
    let mut arr = [1, 2, 3, 4, 5];
    arr.rotate_left(7); // 等价于 rotate_left(2)
    println!("{:?}", arr); // [3, 4, 5, 1, 2]
}

2. slice.rotate_right() – 右旋

fn main() {
    let mut arr = [1, 2, 3, 4, 5, 6, 7];
    arr.rotate_right(3);
    println!("{:?}", arr); // [5, 6, 7, 1, 2, 3, 4]
}

注意rotate_left(k) 等价于 rotate_right(n - k),其中 n 为数组长度。

实战:189. 轮转数组

189. 轮转数组

题目:给定一个数组,将数组中的元素向右轮转 k 个位置。

思路:使用三次翻转法。右旋 k 位 = 先整体翻转,再分别翻转前 k 个和后 n-k 个。

pub fn rotate(nums: &mut Vec<i32>, k: i32) {
    let len = nums.len();
    if len <= 1 {
        return;
    }
    let offset = (k as usize) % len;
    if offset == 0 {
        return;
    }

    // 第一次翻转:整体翻转
    nums.reverse();

    // 第二次翻转:翻转前offset个元素
    for i in 0..offset / 2 {
        nums.swap(i, offset - i - 1);
    }

    // 第三次翻转:翻转后(len - offset)个元素
    for j in 0..(len - offset) / 2 {
        nums.swap(j + offset, len - j - 1);
    }
}

fn main() {
    let mut nums = vec![1, 2, 3, 4, 5, 6, 7];
    let k = 3;
    rotate(&mut nums, k);
    println!("result: {:?}", nums); // [5, 6, 7, 1, 2, 3, 4]
}

复杂度分析

  • 时间复杂度:$O(n)$,三次翻转总共访问每个元素约两次
  • 空间复杂度:$O(1)$,原地操作

也可以直接使用 Rust 标准库方法简化:

#![allow(unused)]
fn main() {
pub fn rotate_std(nums: &mut Vec<i32>, k: i32) {
    let k = (k as usize) % nums.len();
    if k > 0 {
        nums.rotate_right(k);
    }
}
}

字符串旋转判断

判断一个字符串是否是另一个字符串旋转得到的:

fn is_rotation(s1: &str, s2: &str) -> bool {
    if s1.len() != s2.len() {
        return false;
    }
    // 将s1与自身拼接,s2如果是s1的旋转,必然是拼接后的子串
    let doubled = format!("{}{}", s1, s1);
    doubled.contains(s2)
}

fn main() {
    println!("{}", is_rotation("waterbottle", "erbottlewat")); // true
    println!("{}", is_rotation("abcde", "cdeab"));             // true
    println!("{}", is_rotation("abcde", "abced"));             // false
}

三者关系

交换、反转、旋转三者之间存在递进的包含关系:

swap(交换)
  └── reverse(反转)= 多次 swap
        └── rotate(旋转)= 三次 reverse

用代码验证这一关系:

fn main() {
    let mut arr = [1, 2, 3, 4, 5, 6, 7];

    // rotate_left(3) 等价于三次 reverse
    let mut arr2 = arr.clone();
    arr2[0..3].reverse();
    arr2[3..].reverse();
    arr2.reverse();
    println!("三次reverse: {:?}", arr2); // [4, 5, 6, 7, 1, 2, 3]

    // 使用标准库 rotate_left
    arr.rotate_left(3);
    println!("rotate_left: {:?}", arr); // [4, 5, 6, 7, 1, 2, 3]

    assert_eq!(arr, arr2);
}

理解这一关系有助于:

  1. 在没有内置 rotate 的语言中,用 reverse 实现 rotate
  2. 在没有内置 reverse 的语言中,用 swap 实现 reverse
  3. 从更高层次理解数据变换的本质

相关问题


总结

操作含义Rust 方法时间复杂度空间复杂度
交换(swap)交换两个位置的值slice.swap(i, j) / mem::swap$O(1)$$O(1)$
反转(reverse)颠倒整个序列slice.reverse() / iter().rev()$O(n)$$O(1)$
左旋(rotate_left)元素向左循环移动slice.rotate_left(k)$O(n)$$O(1)$
右旋(rotate_right)元素向右循环移动slice.rotate_right(k)$O(n)$$O(1)$

三者关系:rotate = 3 * reversereverse = n/2 * swap


练习题

  1. 反转字符串 IILeetCode 541):给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。

  2. 旋转数组的最小值剑指 Offer 11):把一个数组最开始的若干个元素搬到数组的末尾,称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。

  3. 链表两两交换LeetCode 24):给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

  4. 手动实现 reverse:不使用 slice.reverse(),仅使用 slice.swap() 实现一个切片的反转函数。

  5. 字符串旋转:实现一个函数,判断字符串 s2 是否可以通过旋转 s1 得到。要求不使用字符串拼接(即不使用 s1 + s1 的技巧),而是通过逐位旋转比较。

第十九 类型转换

概述

Rust 的类型系统以严格著称。与 C/C++ 和 JavaScript 等语言不同,Rust 不支持隐式类型转换(implicit type conversion),几乎所有类型转换都必须显式进行。这一设计哲学体现了 Rust 的核心理念:显式优于隐式,让每一处类型变化都清晰可见,从而减少因隐式转换引发的 bug。

Rust 中的类型转换主要分为以下几类:

方式关键字/trait特点典型场景
隐式强制转换coercion编译器自动处理,范围有限&T&dyn Trait&mut T&T
显式转换(trait)From / Into编译期检查,推荐方式自定义类型间转换
显式转换(trait)TryFrom / TryInto可能失败,返回 Result数值范围检查、字符串解析
廉价借用转换AsRef / AsMut仅借用,零拷贝函数参数泛型
强制类型转换as简单直接,可能丢失数据数值截断、指针转换
字符串解析FromStr / parse可能失败,返回 Result字符串转数值

隐式类型转换 vs 显式类型转换

隐式强制转换(Coercion)

Rust 虽然不支持一般的隐式类型转换,但在少数特定场景下,编译器会自动进行强制转换(coercion)

fn main() {
    // 1. 自动解引用强制转换(Deref coercion)
    let s: String = String::from("hello");
    let slice: &str = &s; // String 自动解引用为 &str

    let v: Vec<i32> = vec![1, 2, 3];
    let slice: &[i32] = &v; // Vec<T> 自动解引用为 &[T]

    // 2. &mut T 自动转换为 &T
    let mut x = 42;
    let r: &i32 = &x; // 可变引用自动转为不可变引用

    // 3. 数值字面量的类型推断
    let x: i32 = 42;       // 42 被推断为 i32
    let y: f64 = 3.14;     // 3.14 被推断为 f64
    let z: u8 = 255;       // 255 被推断为 u8

    // 4. 自动提升:整数可以自动转换为更大的同符号类型(在特定上下文中)
    // 注意:Rust 的自动提升非常有限,不像 C 语言那样自由
}

显式类型转换

Rust 要求绝大多数类型转换必须显式进行,这是 Rust 类型安全的重要保障:

fn main() {
    // 错误!不提供隐式转换
    // let integer: i32 = 3.14_f64; // 编译错误 E0308

    // 正确:使用 as 关键字显式转换
    let integer: i32 = 3.14_f64 as i32;
    println!("{}", integer); // 3
}

From/Into trait

FromInto 是 Rust 中最推荐的类型转换方式,它们在编译期就能确保转换的安全性。

原理:From 自动实现 Into

Rust 标准库中有一个 blanket implementation:为所有实现了 From<T> 的类型自动实现 Into<T>

#![allow(unused)]
fn main() {
// 标准库中的 blanket implementation(简化)
impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}
}

这意味着:你只需要实现 From,就自动获得了 Into。反之,如果需要将类型 A 转为类型 B,只需为 B 实现 From<A> 即可。

标准库中的实现

Rust 标准库已经为许多基本类型实现了 From/Into

fn main() {
    // 数值类型之间的转换
    let x: i32 = i64::from(42);     // i64 -> i32
    let y: f64 = f32::from(3.14);   // f32 -> f64
    let z: u32 = u16::from(100);    // u16 -> u32

    // 使用 Into(更常见,因为自动实现)
    let a: String = "hello".into();  // &str -> String
    let b: Vec<u8> = "hello".into(); // &str -> Vec<u8>
    let c: i64 = 42i32.into();       // i32 -> i64

    // bool 转整数
    let d: i32 = true.into();       // bool -> i32 (值为 1)
    let e: i32 = false.into();      // bool -> i32 (值为 0)

    println!("a = {}, b = {:?}, c = {}, d = {}, e = {}", a, b, c, d, e);
}

自定义类型的 From 实现

#[derive(Debug)]
struct Celsius(f64);

#[derive(Debug)]
struct Fahrenheit(f64);

// Celsius -> Fahrenheit
impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

// Fahrenheit -> Celsius
impl From<Fahrenheit> for Celsius {
    fn from(f: Fahrenheit) -> Self {
        Celsius((f.0 - 32.0) * 5.0 / 9.0)
    }
}

// 从 f64 直接构造
impl From<f64> for Celsius {
    fn from(value: f64) -> Self {
        Celsius(value)
    }
}

fn main() {
    let boiling = Celsius(100.0);
    let f: Fahrenheit = boiling.into(); // 自动调用 From<Celsius>
    println!("100°C = {:.1}°F", f.0); // 212.0°F

    // 使用 Into trait 约束作为函数参数
    fn print_temperature<T: Into<Celsius>>(temp: T) {
        let c: Celsius = temp.into();
        println!("温度: {:.1}°C", c.0);
    }

    print_temperature(36.5_f64);      // f64 -> Celsius
    print_temperature(Celsius(0.0));   // Celsius -> Celsius
}

TryFrom/TryInto:可能失败的转换

当转换可能失败时(例如数值溢出、字符串解析错误),应使用 TryFrom/TryInto,它们返回 Result 类型:

use std::convert::TryFrom;
use std::convert::TryInto;

fn main() {
    // TryFrom:检查数值范围
    let small: u8 = u8::try_from(300_i32);
    match small {
        Ok(v) => println!("转换成功: {}", v),
        Err(e) => println!("转换失败: {}", e), // 数值超出范围
    }

    let ok: u8 = u8::try_from(42_i32).unwrap();
    println!("42 -> u8: {}", ok); // 42

    // TryInto:等价的反向操作
    let result: Result<i8, _> = 300_i32.try_into();
    assert!(result.is_err());

    let result: Result<i8, _> = 100_i32.try_into();
    assert_eq!(result.unwrap(), 100);
}

建议:在 Rust 1.34+ 中,TryFrom/TryInto 已在 std::convert 中稳定。对于可能失败的转换,优先使用 TryFrom/TryInto 而非 as,因为后者会静默截断。


AsRef/AsMut trait

AsRef/AsMut 提供了一种廉价的借用转换机制,将一种类型转换为另一种类型的引用,不涉及数据拷贝。

原理

#![allow(unused)]
fn main() {
pub trait AsRef<T: ?Sized> {
    fn as_ref(&self) -> &T;
}

pub trait AsMut<T: ?Sized> {
    fn as_mut(&mut self) -> &mut T;
}
}

字符串泛型函数

AsRef 最常见的应用场景是编写能够接受多种字符串类型的泛型函数:

use std::fs;
use std::io;
use std::path::Path;

/// 读取文件内容,接受任何可以转换为 &str 的类型
fn print_length<S: AsRef<str>>(text: S) {
    let s = text.as_ref();
    println!("字符串长度: {}", s.len());
}

/// 打开文件,接受任何可以转换为 AsRef<Path> 的类型
fn read_file<P: AsRef<Path>>(path: P) -> io::Result<String> {
    fs::read_to_string(path)
}

fn main() {
    // AsRef<str>:接受 String、&str、&String 等
    print_length("hello");                    // &str
    print_length(String::from("hello"));      // String
    print_length(&String::from("hello"));     // &String

    // AsRef<Path>:接受 &str、String、&Path、PathBuf 等
    let content = read_file("Cargo.toml").unwrap();
    println!("Cargo.toml 前100字符: {}", &content[..100.min(content.len())]);
}

标准库中的 AsRef 实现

fn main() {
    // String 实现了 AsRef<str>
    let s = String::from("hello");
    let r: &str = s.as_ref();

    // Vec<T> 实现了 AsRef<[T]>
    let v = vec![1, 2, 3];
    let slice: &[i32] = v.as_ref();

    // String 实现了 AsRef<[u8]>
    let bytes: &[u8] = s.as_ref();

    println!("r = {}, slice = {:?}", r, slice);
}

as 关键字

as 关键字是 Rust 中最直接的显式类型转换方式,但也是最容易出问题的方式。

数值类型之间的转换

fn main() {
    // 整数之间的转换
    let a: i32 = 42;
    let b: i64 = a as i64;       // 小类型 -> 大类型:安全
    let c: i8 = a as i8;         // 大类型 -> 小类型:可能截断
    let d: u32 = a as u32;       // 有符号 -> 无符号:重新解释位模式

    println!("i32({}) -> i64({}), i8({}), u32({})", a, b, c, d);

    // 浮点数之间的转换
    let e: f32 = 3.14159265_f64;  // f64 -> f32:丢失精度
    let f: f64 = e as f64;       // f32 -> f64:安全(但已丢失的精度无法恢复)

    // 整数 <-> 浮点数
    let g: f64 = 42 as f64;       // i32 -> f64:安全
    let h: i32 = 3.7_f64 as i32; // f64 -> i32:截断小数部分(不是四舍五入)
    let i: i32 = 3.9_f64 as i32; // 同样截断为 3

    println!("f64(3.7) -> i32: {}", h); // 3
    println!("f64(3.9) -> i32: {}", i); // 3
}

精度丢失和截断

使用 as 进行数值转换时,需要特别注意以下情况:

fn main() {
    // 1. 溢出截断:大值放入小类型
    let x: i32 = 300;
    let y: i8 = x as i8; // 300 超出 i8 范围(-128~127),结果为 44(截断高位)
    println!("i32(300) -> i8: {}", y); // 44

    // 2. 负数转无符号数:重新解释位模式
    let neg: i32 = -1;
    let pos: u32 = neg as u32; // -1 的补码全为1,解释为 u32 即为最大值
    println!("i32(-1) -> u32: {}", pos); // 4294967295

    // 3. 浮点数 NaN 和无穷大转整数
    let nan: f64 = f64::NAN;
    let inf: f64 = f64::INFINITY;
    let nan_int = nan as i32;
    let inf_int = inf as i32;
    println!("NaN -> i32: {}", nan_int); // 0(未定义行为,当前实现返回0)
    println!("Inf -> i32: {}", inf_int); // 未定义行为

    // 4. 安全的替代方案
    let safe: Option<i8> = i8::try_from(300_i32).ok(); // None
    println!("i8::try_from(300) = {:?}", safe); // None
}

警告as 转换在溢出、NaN、无穷大等情况下的行为是未定义的(implementation-defined)。在需要安全转换的场景中,应使用 TryFrom/TryInto

指针转换

as 还可以用于指针类型之间的转换,但这属于 unsafe 操作:

fn main() {
    let x: i32 = 42;

    // 将 &i32 转为裸指针
    let raw: *const i32 = &x as *const i32;

    // 将裸指针转为整数地址
    let addr = raw as usize;
    println!("变量 x 的地址: {:p} (数值: {})", raw, addr);

    // 将 usize 转回裸指针(unsafe)
    unsafe {
        let reconstructed: *const i32 = addr as *const i32;
        println!("通过地址访问的值: {}", *reconstructed); // 42
    }
}

注意:指针转换涉及 unsafe Rust,仅在确实需要时使用,如 FFI(外部函数接口)调用。


String 与数值转换

字符串与数值之间的转换是日常开发中最常见的类型转换场景。

parse 方法与 FromStr trait

FromStr trait 定义了从字符串解析为特定类型的行为:

#![allow(unused)]
fn main() {
pub trait FromStr: Sized {
    type Err;

    fn from_str(s: &str) -> Result<Self, Self::Err>;
}

pub fn parse<F: FromStr>(&self) -> Result<F, F::Err> {
    FromStr::from_str(self)
}
}
use std::str::FromStr;

fn main() {
    // 使用 parse 方法
    let four: u32 = "4".parse().unwrap();
    println!("parse u32: {}", four); // 4

    // turbofish 语法:当类型无法推断时使用
    let four = "4".parse::<u32>();
    println!("parse::<u32>: {:?}", four); // Ok(4)

    // 使用 FromStr trait 直接调用
    let s = "5";
    let x = i32::from_str(s).unwrap();
    println!("from_str: {}", x); // 5

    // 解析失败的情况
    let result = "abc".parse::<i32>();
    println!("parse 'abc': {:?}", result); // Err(ParseIntError { ... })

    // 处理解析错误
    match "42abc".parse::<i32>() {
        Ok(n) => println!("解析成功: {}", n),
        Err(e) => println!("解析失败: {}", e),
    }
}

turbofish 语法

当编译器无法从上下文推断 parse 的目标类型时,需要使用 turbofish 语法 ::<T> 显式指定:

fn main() {
    // 类型可推断:不需要 turbofish
    let x: i32 = "42".parse().unwrap();

    // 类型不可推断:需要 turbofish
    let y = "42".parse::<i32>().unwrap();

    // 函数参数中
    fn double(s: &str) -> Result<i32, std::num::ParseIntError> {
        let n: i32 = s.parse()?;
        Ok(n * 2)
    }

    println!("double('21') = {:?}", double("21")); // Ok(42)
}

ToString trait

ToString trait 是 FromStr 的反向操作,将类型转换为字符串:

use std::fmt::Display;

fn main() {
    // 数值转字符串
    let s = 42.to_string();
    println!("42.to_string() = '{}'", s); // "42"

    let f = 3.14.to_string();
    println!("3.14.to_string() = '{}'", f); // "3.14"

    let b = true.to_string();
    println!("true.to_string() = '{}'", b); // "true"

    // 使用 format! 宏(更灵活)
    let formatted = format!("{}", 42);
    println!("format!('{}') = '{}'", 42, formatted); // "42"

    let hex = format!("{:x}", 255);
    println!("255 的十六进制: '{}'", hex); // "ff"

    let binary = format!("{:b}", 10);
    println!("10 的二进制: '{}'", binary); // "1010"
}

注意ToString 是自动为实现了 Display trait 的类型实现的 blanket implementation。因此,只要类型实现了 Display,就自动拥有 to_string() 方法。


String/&str/Vec<u8> 转换大全

以下是 Rust 中字符串相关类型之间的转换速查表:

完整转换表

源类型目标类型方法说明
&strStringString::from(s)s.to_string()s.to_owned()创建新的拥有所有权的 String
&str&[u8]s.as_bytes()零拷贝借用
&strVec<u8>s.as_bytes().to_vec()拷贝字节
&strVec<char>s.chars().collect()按字符拆分
String&strs.as_str()&s零拷贝借用
String&[u8]s.as_bytes()零拷贝借用
StringVec<u8>s.into_bytes()消费 String,获取底层 Vec
StringVec<char>s.chars().collect()按字符拆分
&[u8]&strstd::str::from_utf8(u)可能失败(需检查 UTF-8 合法性)
&[u8]StringString::from_utf8(v.to_vec())拷贝并验证 UTF-8
Vec<u8>StringString::from_utf8(v)消费 Vec,验证 UTF-8
Vec<u8>&strstd::str::from_utf8(&v).unwrap()借用并验证 UTF-8
&[char]StringString::from_iter(chars)chars.iter().collect()收集字符为字符串

代码示例

use std::str;

fn main() {
    // &str -> String
    let s: &str = "hello";
    let owned: String = String::from(s);
    let owned2: String = s.to_string();
    let owned3: String = s.to_owned();
    assert_eq!(owned, owned2);
    assert_eq!(owned2, owned3);

    // &str -> &[u8]
    let bytes: &[u8] = s.as_bytes();
    println!("bytes: {:?}", bytes); // [104, 101, 108, 108, 111]

    // String -> &str
    let hello = String::from("hello");
    let slice: &str = &hello;      // Deref coercion
    let slice2: &str = hello.as_str();
    assert_eq!(slice, slice2);

    // String -> Vec<u8>
    let hello = String::from("hello");
    let bytes_vec: Vec<u8> = hello.into_bytes();
    println!("into_bytes: {:?}", bytes_vec);

    // &[u8] -> &str(可能失败)
    let valid: &[u8] = b"hello";
    let s: &str = str::from_utf8(valid).unwrap();
    println!("from_utf8: {}", s);

    let invalid: &[u8] = &[0xff, 0xfe]; // 非 UTF-8
    let result = str::from_utf8(invalid);
    println!("invalid utf8: {:?}", result); // Err(Utf8Error)

    // Vec<u8> -> String(可能失败)
    let bytes = vec![104, 101, 108, 108, 111]; // "hello" 的 ASCII
    let s = String::from_utf8(bytes).unwrap();
    println!("from_utf8(Vec): {}", s);

    // &str -> Vec<char>
    let chars: Vec<char> = "hello".chars().collect();
    println!("chars: {:?}", chars); // ['h', 'e', 'l', 'l', 'o']

    // Vec<char> -> String
    let arr: Vec<char> = vec!['h', 'e', 'l', 'l', 'o'];
    let greeting: String = arr.iter().collect();
    println!("collect: {}", greeting);
}

常见转换场景

集合类型转换

fn main() {
    // Vec <-> Array
    let vec = vec![1, 2, 3];
    let arr: [i32; 3] = vec.try_into().unwrap(); // Vec -> Array(长度必须匹配)
    println!("Vec -> Array: {:?}", arr);

    let arr = [1, 2, 3];
    let vec: Vec<i32> = arr.to_vec(); // Array -> Vec
    println!("Array -> Vec: {:?}", vec);

    // Vec <-> HashSet
    use std::collections::HashSet;
    let vec = vec![1, 2, 3, 2, 1];
    let set: HashSet<i32> = vec.into_iter().collect(); // 去重
    println!("Vec -> HashSet: {:?}", set);

    let set: HashSet<&str> = ["apple", "banana", "cherry"].into_iter().collect();
    let vec: Vec<&str> = set.into_iter().collect();
    println!("HashSet -> Vec: {:?}", vec);

    // String <-> Vec<&str>
    let s = "hello world rust";
    let words: Vec<&str> = s.split(' ').collect();
    println!("split: {:?}", words); // ["hello", "world", "rust"]

    let joined = words.join("-");
    println!("join: {}", joined); // "hello-world-rust"
}

日期时间解析

use chrono::NaiveDateTime;

fn main() {
    // 2. &str -> DateTime
    let no_timezone = NaiveDateTime::parse_from_str(
        "2015-09-05 23:56:04",
        "%Y-%m-%d %H:%M:%S"
    );
    println!("{:?}", no_timezone); // Ok(2015-09-05T23:56:04)

    // DateTime -> String
    if let Ok(dt) = no_timezone {
        let formatted = dt.format("%Y年%m月%d日 %H:%M").to_string();
        println!("格式化: {}", formatted); // "2015年09月05日 23:56"
    }

    // 当前时间
    let now = chrono::Local::now();
    println!("当前时间: {}", now.format("%Y-%m-%d %H:%M:%S"));

    // 时间戳转换
    let timestamp = 1609459200; // 2021-01-01 00:00:00 UTC
    let dt = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap();
    println!("时间戳 -> DateTime: {}", dt.format("%Y-%m-%d %H:%M:%S"));
}

Option/Result 转换

fn main() {
    // Option -> Result
    let some_val: Option<i32> = Some(42);
    let result: Result<i32, &str> = some_val.ok_or("值为 None");
    println!("ok_or: {:?}", result); // Ok(42)

    let none_val: Option<i32> = None;
    let result: Result<i32, &str> = none_val.ok_or("值为 None");
    println!("ok_or: {:?}", result); // Err("值为 None")

    // Result -> Option
    let ok_result: Result<i32, &str> = Ok(42);
    let opt: Option<i32> = ok_result.ok();
    println!("ok(): {:?}", opt); // Some(42)

    let err_result: Result<i32, &str> = Err("错误");
    let opt: Option<i32> = err_result.ok();
    println!("ok(): {:?}", opt); // None

    // Option 之间的转换
    let opt_str: Option<&str> = Some("42");
    let opt_num: Option<i32> = opt_str.and_then(|s| s.parse().ok());
    println!("and_then: {:?}", opt_num); // Some(42)

    // Result 之间的转换(map_err)
    let result: Result<i32, &str> = "abc".parse::<i32>().map_err(|_| "解析失败");
    println!("map_err: {:?}", result); // Err("解析失败")
}

Box/Rc/Arc 转换

use std::rc::Rc;
use std::sync::Arc;

fn main() {
    // Box<T> -> Rc<T>
    let boxed: Box<i32> = Box::new(42);
    let rc: Rc<i32> = Rc::from(boxed); // 消费 Box,创建 Rc
    println!("Box -> Rc: {}", rc);

    // Box<T> -> Arc<T>
    let boxed: Box<String> = Box::new(String::from("hello"));
    let arc: Arc<String> = Arc::from(boxed);
    println!("Box -> Arc: {}", arc);

    // Rc<T> -> Arc<T>(需要先解包再包装)
    let rc: Rc<Vec<i32>> = Rc::new(vec![1, 2, 3]);
    let arc: Arc<Vec<i32>> = Arc::from((*rc).clone());
    println!("Rc -> Arc: {:?}", arc);

    // &T -> Box<T>
    let x = 42;
    let boxed: Box<i32> = Box::new(x); // 拷贝值到堆上

    // &str -> Box<str>
    let boxed_str: Box<str> = "hello".into();
    println!("&str -> Box<str>: {}", boxed_str);

    // String -> Box<str>
    let s = String::from("hello");
    let boxed: Box<str> = s.into_boxed_str();
    println!("String -> Box<str>: {}", boxed);
}

类型转换的安全性讨论

不同的类型转换方式具有不同的安全级别:

安全级别方式说明
最安全From/Into编译期保证成功,不会丢失数据
安全TryFrom/TryInto运行时检查,失败返回 Result
安全AsRef/AsMut仅借用,零拷贝,不会失败
需注意FromStr/parse运行时检查,失败返回 Result
危险as(数值)可能静默截断、溢出
极危险as(指针)属于 unsafe,可能导致未定义行为

选择建议

fn main() {
    let value: i64 = 300;

    // 推荐:使用 TryFrom,明确处理可能的失败
    match i8::try_from(value) {
        Ok(v) => println!("转换成功: {}", v),
        Err(_) => println!("值 {} 超出 i8 范围", value),
    }

    // 不推荐:使用 as,静默截断
    let truncated = value as i8;
    println!("截断结果: {}", truncated); // 44,可能不是期望的行为

    // 对于确定安全的转换,可以使用 From/Into
    let widened: i64 = 42i32.into(); // i32 -> i64,总是安全
    println!("安全转换: {}", widened);
}

经验法则

  1. 优先使用 From/Into:当转换总是成功时
  2. 使用 TryFrom/TryInto:当转换可能失败时
  3. 使用 AsRef/AsMut:当只需要借用时
  4. 谨慎使用 as:仅在明确知道后果时使用
  5. 避免 transmute:除非你完全理解内存布局

总结

转换方式关键字/trait失败处理典型场景安全性
隐式强制转换coercion不适用Deref coercion安全
From/IntoFrom<T> / Into<T>编译期保证成功自定义类型转换最安全
TryFrom/TryIntoTryFrom<T> / TryInto<T>返回 Result数值范围检查安全
AsRef/AsMutAsRef<T> / AsMut<T>不失败泛型函数参数安全
asas 关键字静默截断数值截断、指针转换需注意
FromStr/parseFromStr / parse返回 Result字符串解析安全
ToStringDisplay / to_string不失败类型转字符串安全

练习题

  1. 温度转换器:定义 CelsiusFahrenheit 两个结构体,使用 From/Into trait 实现双向转换,并编写一个接受 Into<Celsius> 的泛型函数。

  2. 安全数值解析:编写一个函数,接受一个 &str,尝试解析为 u8。如果解析失败或数值超出 u8 范围,返回默认值 0。要求使用 TryFrom 而非 as

  3. 泛型文件读取器:编写一个函数 fn read_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>, io::Error>,使其能接受 &strStringPathBuf 作为路径参数。

  4. 类型转换链:实现一个函数,将 Vec<u8> 转换为十六进制字符串表示(如 [0xde, 0xad, 0xbe, 0xef] 转为 "deadbeef"),要求正确处理所有转换步骤。

  5. 配置解析器:编写一个简单的配置解析器,从 &str 解析键值对(格式为 "key=value"),将值转换为合适的类型(i32f64boolString),使用 FromStrparse 方法。

第二十章 哈希

散列函数

20.1 哈希函数概述

什么是哈希函数

哈希函数(Hash Function)是一种将任意长度的输入数据映射为固定长度输出数据的函数。这个固定长度的输出通常被称为哈希值(Hash Value)、摘要(Digest)或指纹(Fingerprint)。无论输入数据是一个字节还是数个 TB,哈希函数始终输出相同长度的结果。

从数学角度来看,哈希函数可以定义为:

$$H: {0,1}^* \to {0,1}^n$$

其中,${0,1}^*$ 表示任意长度的二进制串集合,${0,1}^n$ 表示长度为 $n$ 的二进制串集合。例如,SHA-256 的输出长度 $n = 256$。

哈希函数的核心性质

一个合格的哈希函数通常需要满足以下核心性质:

性质说明
确定性相同的输入始终产生相同的输出:若 $x = y$,则 $H(x) = H(y)$
单向性(抗原像性)给定哈希值 $h$,很难找到原始输入 $x$ 使得 $H(x) = h$
抗碰撞性很难找到两个不同的输入 $x \neq y$ 使得 $H(x) = H(y)$
雪崩效应输入的微小变化导致输出的巨大变化,改变一个比特应影响约一半的输出比特
固定输出长度无论输入多长,输出长度始终固定

其中,抗碰撞性又分为两种:

  • 弱抗碰撞性:给定一个输入 $x$,很难找到另一个不同的输入 $y$ 使得 $H(x) = H(y)$。
  • 强抗碰撞性:很难找到任意两个不同的输入 $x \neq y$ 使得 $H(x) = H(y)$。

对于密码学应用,通常要求满足强抗碰撞性。根据生日攻击理论,对于一个 $n$ 位输出的哈希函数,找到碰撞的期望尝试次数约为 $\sqrt{2^n}$。因此,SHA-256(256 位输出)的安全性远高于 MD5(128 位输出)。

哈希函数的分类

哈希函数按用途可分为两大类:

  1. 非加密哈希(Non-cryptographic Hash):追求速度,用于哈希表、数据校验、布隆过滤器等场景。不要求抗碰撞性等密码学安全性质。
  2. 加密哈希(Cryptographic Hash):追求安全性,用于密码存储、数字签名、消息认证等场景。必须满足单向性和抗碰撞性。
// 非加密哈希 vs 加密哈希的简单对比示例
use std::collections::HashMap;
use sha2::{Sha256, Digest};

fn main() {
    // 非加密哈希:HashMap 内部使用 SipHash
    let mut map = HashMap::new();
    map.insert("key1", "value1");
    println!("HashMap 查找 key1: {:?}", map.get("key1"));

    // 加密哈希:SHA-256
    let mut hasher = Sha256::new();
    hasher.update(b"hello world");
    let result = hasher.finalize();
    println!("SHA-256('hello world') = {}", hex::encode(result));
}

20.2 非加密哈希

非加密哈希函数的设计目标是速度优先,通常用于不需要密码学安全性的场景。它们不保证抗碰撞性,但在正常使用中碰撞概率极低。

主要用途

  • 哈希表:将键映射到桶(bucket)中,实现快速查找
  • 数据校验:检测数据传输中的错误
  • 布隆过滤器:高效的概率型数据结构
  • 负载均衡:一致性哈希(Consistent Hashing)
  • 指纹识别:快速判断两个数据块是否相同

CRC32

CRC32(Cyclic Redundancy Check,循环冗余校验)是一种基于多项式除法的校验算法,广泛用于网络通信和文件校验。其原理是将数据视为一个大的二进制多项式,除以一个预定义的生成多项式,余数即为 CRC 值。

CRC32 的数学原理:将输入数据 $M(x)$ 视为多项式,生成多项式为 $G(x)$,则:

$$CRC(M) = M(x) \cdot x^{32} \mod G(x)$$

常用的 CRC32 生成多项式为:

$$G(x) = x^{32} + x^{26} + x^{23} + x^{22} + x^{16} + x^{12} + x^{11} + x^{10} + x^8 + x^7 + x^5 + x^4 + x^2 + x + 1$$

// Cargo.toml: crc32fast = "1"
use crc32fast::Hasher;

fn main() {
    let mut hasher = Hasher::new();
    hasher.update(b"hello world");
    let checksum = hasher.finalize();
    println!("CRC32('hello world') = {:08x}", checksum);
}

MurmurHash

MurmurHash 是一种非加密哈希算法,由 Austin Appleby 创建。名字来源于 “multiply” 和 “rotate” 两个操作的组合。它速度快、分布均匀,但不提供加密安全性。MurmurHash 有多个版本(MurmurHash1/2/3),其中 MurmurHash3 最为常用。

SipHash

SipHash 是一种快速但“密码学强度“的伪随机函数,由 Aumasson 和 Bernstein 设计。SipHash is a fast but ‘cryptographically strong’ pseudo-random function by Aumasson and Bernstein.

Rust 的 HashMapHashSet 默认使用 SipHash 作为哈希算法,这是 Rust 标准库的一个重要安全设计决策。SipHash 能够有效抵抗 HashDoS(哈希碰撞拒绝服务)攻击,攻击者无法通过构造特定输入来制造大量哈希碰撞。

// Rust 标准库 HashMap 默认使用 SipHash
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;

fn get_hash<T: Hash>(t: &T) -> u64 {
    let mut s = DefaultHasher::new();
    t.hash(&mut s);
    s.finish()
}

fn main() {
    let hash1 = get_hash(&"hello");
    let hash2 = get_hash(&"world");
    println!("SipHash(\"hello\") = {}", hash1);
    println!("SipHash(\"world\") = {}", hash2);

    // HashMap 内部自动使用 SipHash
    let mut scores = HashMap::new();
    scores.insert("Alice", 95);
    scores.insert("Bob", 87);
    println!("scores: {:?}", scores);
}

XxHash / XXH3

XxHash 是一种非常快速的非加密哈希算法,由 Yann Collet 设计。XXH3 是其最新版本,在大数据量场景下性能极为出色。

twox-hash 是 XxHash 的 Rust 实现,XxHash是一种非常快速的哈希算法。

// Cargo.toml: xxhash-rust = { version = "0.8", features = ["xxh3"] }
use xxhash_rust::xxh3::xxh3_128;

fn main() {
    let hash = xxh3_128(b"hello world");
    println!("XXH3-128('hello world') = {:016x}", hash);
}

HighwayHash

HighwayHash 是 Google 开发的一种高速哈希算法,设计目标是能够在短字符串和长字符串上都达到极高的吞吐量。它支持 64 位、128 位和 256 位输出。

highway-rs 是 HighwayHash 的 Rust 实现。

// Cargo.toml dependencies:
// highway = "1"

use highway::{HighwayHash, HighwayHasher, Key};

fn main() {
    // Generate 128bit hash
    let key = Key([1, 2, 3, 4]);
    let mut hasher128 = HighwayHasher::new(key);
    hasher128.append(&[255]);
    let res128: [u64; 2] = hasher128.finalize128();
    println!("128-bit hash: {:?}", res128);
    assert_eq!([0xbb007d2462e77f3c, 0x224508f916b3991f], res128);
    println!("128-bit hash assertion passed!");

    // Generate 256bit hash
    let key = Key([1, 2, 3, 4]);
    let mut hasher256 = HighwayHasher::new(key);
    hasher256.append(&[255]);
    let res256: [u64; 4] = hasher256.finalize256();
    println!("256-bit hash: {:?}", res256);
    let expected: [u64; 4] = [
        0x7161cadbf7cd70e1,
        0xaac4905de62b2f5e,
        0x7b02b936933faa7,
        0xc8efcfc45b239f8d,
    ];
    assert_eq!(expected, res256);
    println!("256-bit hash assertion passed!");
}

非加密哈希算法对比

算法输出位数特点Rust crate典型场景
CRC3232基于多项式除法,硬件加速crc32fast数据校验、文件校验
MurmurHash332/64/128速度快,分布均匀murmur3布隆过滤器、数据分片
SipHash64抗 HashDoS,Rust 默认标准库内置HashMap、HashSet
XxHash32/64/128极快,支持流式处理xxhash-rust数据处理、数据库
XXH364/128XxHash 最新版,更快xxhash-rust大数据量哈希
HighwayHash64/128/256Google 开发,SIMD 加速highway网络数据包、缓存

20.3 加密哈希

加密哈希函数(Cryptographic Hash Function)是密码学中的核心原语,除了满足哈希函数的基本性质外,还必须具备单向性抗碰撞性等安全性质。

主要用途

  • 密码存储:存储用户密码的哈希值而非明文
  • 数字签名:对消息摘要进行签名,提高效率
  • 数据完整性:验证文件或消息是否被篡改
  • 消息认证:结合密钥生成消息认证码(MAC)
  • 区块链:工作量证明、区块链接

MD5

MD5(Message-Digest Algorithm 5)由 Ronald Rivest 于 1991 年设计,输出 128 位(16 字节)哈希值。MD5 曾广泛用于文件校验和数字签名,但现已不安全——2004 年王小云教授团队发现了 MD5 的碰撞攻击方法,此后 MD5 不再被推荐用于任何安全敏感场景。

// Cargo.toml: md-5 = "0.10"
use md5::{Md5, Digest};

fn main() {
    let mut hasher = Md5::new();
    hasher.update(b"hello world");
    let result = hasher.finalize();
    println!("MD5('hello world') = {}", hex::encode(result));
    // 输出: 5eb63bbbe01eeed093cb22bb8f5acdc3
}

SHA 系列

SHA256(Secure Hash Algorithm)是美国国家安全局(NSA)设计的一系列加密哈希算法,由 NIST 发布为联邦信息处理标准(FIPS)。

算法输出位数内部块大小状态安全性
SHA-1160512已破解(2017年)不安全
SHA-224224512安全112位安全性
SHA-256256512安全128位安全性
SHA-3843841024安全192位安全性
SHA-5125121024安全256位安全性

SHA-256 是目前最广泛使用的加密哈希算法之一。其处理流程包括:

  1. 消息填充:在消息末尾添加填充位,使消息长度满足 $L \equiv 448 \pmod{512}$
  2. 附加长度:追加原始消息的 64 位长度值
  3. 分块处理:将填充后的消息分为 512 位的块
  4. 压缩函数:每个块经过 64 轮压缩运算,更新 8 个 32 位的工作变量
  5. 输出拼接:最终将 8 个工作变量拼接为 256 位输出
// Cargo.toml: sha2 = "0.10"
use sha2::{Sha256, Sha512, Digest};

fn main() {
    // SHA-256
    let mut hasher256 = Sha256::new();
    hasher256.update(b"hello world");
    let result256 = hasher256.finalize();
    println!("SHA-256('hello world') = {}", hex::encode(result256));

    // SHA-512
    let mut hasher512 = Sha512::new();
    hasher512.update(b"hello world");
    let result512 = hasher512.finalize();
    println!("SHA-512('hello world') = {}", hex::encode(result512));
}

SHA-3 / Keccak

SHA3(Keccak)是 NIST 在 2015 年发布的最新加密哈希标准,采用与 SHA-2 完全不同的海绵结构(Sponge Construction)。

海绵结构的核心是一个固定宽度的置换函数 $f$,其处理过程为:

  1. 吸收阶段(Absorbing):将输入消息分块后与状态进行异或,然后应用置换函数 $f$
  2. 挤压阶段(Squeezing):从状态中提取输出

$$Keccakr, c, d = Sponge[f, pad, r](M, d)$$

其中 $r$ 为比特率(rate),$c$ 为容量(capacity),$d$ 为输出长度。安全性与容量 $c$ 成正比。

// Cargo.toml: sha3 = "0.10"
use sha3::{Sha3_256, Digest};

fn main() {
    let mut hasher = Sha3_256::new();
    hasher.update(b"hello world");
    let result = hasher.finalize();
    println!("SHA3-256('hello world') = {}", hex::encode(result));
}

BLAKE3

BLAKE3 是 BLAKE2 的继任者,由 Jean-Philippe Aumasson、Samuel Neves、Zooko Wilcox-O’Hearn 和 Christian Winnerlein 设计。BLAKE3 的特点包括:

  • 极高性能:在多核 CPU 上可达到每秒数十 GB 的哈希速度
  • 并行化设计:基于 Merkle 树结构,天然支持并行计算
  • 多功能:支持哈希、密钥派生(KDF)、可扩展输出(XOF)
  • 安全性:基于 BLAKE2s 的安全证明
// Cargo.toml: blake3 = "1"
use blake3::Hasher;

fn main() {
    // 基本哈希
    let hash = blake3::hash(b"hello world");
    println!("BLAKE3('hello world') = {}", hash);

    // 流式哈希
    let mut hasher = Hasher::new();
    hasher.update(b"hello ");
    hasher.update(b"world");
    let hash = hasher.finalize();
    println!("BLAKE3(streaming) = {}", hash);

    // 密钥派生
    let key = blake3::derive_key("my-app-key", b"some-derivation-context");
    println!("Derived key: {}", hex::encode(key.as_bytes()));
}

SM3

SM3 是中国国家密码管理局发布的密码杂凑算法,属于国密(SM)标准体系的一部分。SM3 输出 256 位哈希值,安全性 comparable to SHA-256,在金融、政务等场景中被广泛使用。

#![allow(unused)]
fn main() {
// 使用 OpenSSL 命令行计算 SM3
// openssl sm3 Rust实战配套代码.zip
}
// Cargo.toml: sm3 = "0.4"
use sm3::{Sm3, Digest};

fn main() {
    let mut hasher = Sm3::new();
    hasher.update(b"hello world");
    let result = hasher.finalize();
    println!("SM3('hello world') = {}", hex::encode(result));
}

加密哈希算法对比

算法输出位数设计者结构安全状态Rust crate
MD5128RivestMerkle-Damgard已破解md-5
SHA-1160NSAMerkle-Damgard已破解sha1
SHA-256256NSAMerkle-Damgard安全sha2
SHA-512512NSAMerkle-Damgard安全sha2
SHA3-256256Keccak Team海绵结构安全sha3
BLAKE3256Aumasson et al.Merkle 树安全blake3
SM3256中国国家密码局Merkle-Damgard安全sm3

20.4 消息认证码(MAC)

MAC(Message Authentication Code, 消息认证码) 是一种带密钥的哈希函数,用于验证消息的完整性和真实性。与普通哈希不同,MAC 需要一个密钥,只有拥有密钥的人才能生成和验证 MAC 值。

GMAC (Galois message authentication code mode, 伽罗华消息认证码) 是 MAC 的一种实现方式,基于伽罗华域(Galois Field)的乘法运算,常与 AES 结合形成 GCM(Galois/Counter Mode)认证加密模式。

HMAC 原理

HMAC(Hash-based MAC)是最常用的 MAC 构造方法,其定义为:

$$HMAC(K, m) = H\big((K’ \oplus opad) \parallel H((K’ \oplus ipad) \parallel m)\big)$$

其中:

  • $H$ 是底层哈希函数(如 SHA-256)
  • $K$ 是密钥
  • $K’$ 是从 $K$ 派生的密钥(若 $K$ 长度超过块大小则先哈希,否则右端补零)
  • $ipad$(inner padding)是重复块大小次的 $0x36$
  • $opad$(outer padding)是重复块大小次的 $0x5c$
  • $\parallel$ 表示拼接操作
  • $\oplus$ 表示异或操作

HMAC 的计算过程分为两步:

  1. 内层哈希:计算 $H((K’ \oplus ipad) \parallel m)$
  2. 外层哈希:计算 $H((K’ \oplus opad) \parallel \text{内层结果})$
// Cargo.toml: hmac = "0.12", sha2 = "0.10", hex = "0.4"
use hmac::{Hmac, Mac};
use sha2::Sha256;

type HmacSha256 = Hmac<Sha256>;

fn main() {
    // 创建 HMAC-SHA256
    let mut mac = HmacSha256::new_from_slice(b"my-secret-key")
        .expect("HMAC can take key of any size");
    mac.update(b"hello world");
    let result = mac.finalize();
    println!("HMAC-SHA256 = {}", hex::encode(result.into_bytes()));

    // 验证 HMAC
    use hmac::Mac;
    let mut mac2 = HmacSha256::new_from_slice(b"my-secret-key").unwrap();
    mac2.update(b"hello world");
    mac2.verify(result.into_bytes()).expect("HMAC verification failed");
    println!("HMAC verification passed!");
}

GMAC / GCM 模式

GCM(Galois/Counter Mode)是一种同时提供加密和认证的模式,其中的认证部分使用 GMAC。GCM 模式广泛应用于 TLS 1.2/1.3、IPsec 等协议中。

// Cargo.toml: aes-gcm = "0.10"
use aes_gcm::{Aes256Gcm, Key, Nonce};
use aes_gcm::aead::{Aead, KeyInit};

fn main() {
    let key = Key::<Aes256Gcm>::from_slice(b"an example very very secret key!");
    let cipher = Aes256Gcm::new(key);
    let nonce = Nonce::from_slice(b"unique nonce"); // 96-bits; unique per message

    let ciphertext = cipher.encrypt(nonce, b"plaintext message".as_ref())
        .expect("encryption failure");
    println!("Ciphertext: {}", hex::encode(&ciphertext));

    let plaintext = cipher.decrypt(nonce, ciphertext.as_ref())
        .expect("decryption failure");
    println!("Plaintext: {}", String::from_utf8_lossy(&plaintext));
}

20.5 哈希在实际应用中的签名验证

在实际的互联网应用中,哈希函数常用于 API 签名验证。签名验证的核心流程是:将请求参数按规则排序、拼接后追加密钥,再进行哈希运算,将得到的签名值与请求中携带的签名进行比对。

支付宝签名验证

以下是一个完整的支付宝签名验证实现,支持 MD5 和 SHA256 两种签名方式:

// Cargo.toml dependencies:
// md-5 = "0.10"
// sha2 = "0.10"
// digest = "0.10"
// chrono = { version = "0.4", features = ["clock"] }
// lazy_static = "1"
use md5::Md5;
use sha2::Sha256;
use digest::Digest;
use std::collections::HashMap;
use chrono::Utc;
use lazy_static::lazy_static;

const ALIPAY_SIGN_SECRET_KEY: &str = "abcdefgh";

lazy_static! {
    static ref SUPPORT_SIGN_TYPE: Vec<&'static str> = vec!["MD5", "SHA256"];
}

pub fn verify_alipay_sign(params_map: HashMap<String, String>) -> Result<bool, &'static str> {
    params_map.get("timestamp").expect("required timestamp");
    let sign_type = params_map.get("sign_type").expect("required sign_type");
    let sign_type_str = sign_type.as_str();

    if !SUPPORT_SIGN_TYPE.contains(&sign_type_str) {
        return Err("not support this sign type");
    }
    params_map.get("sign").expect("required sign");

    let mut keys: Vec<&String> = params_map
        .keys()
        .filter(|k| *k != "sign" && *k != "sign_type")
        .collect();
    keys.sort();

    let mut params_str = String::new();
    for key in &keys {
        if let Some(value) = params_map.get(*key) {
            params_str.push_str(&format!("{}={}&", key, value));
        }
    }
    params_str.push_str(ALIPAY_SIGN_SECRET_KEY);

    let sign = compute_sign(sign_type_str, &params_str);

    if let Some(param_sign) = params_map.get("sign") {
        Ok(*param_sign == sign)
    } else {
        Ok(false)
    }
}

fn compute_sign(sign_type: &str, data: &str) -> String {
    if sign_type == "MD5" {
        let mut md = Md5::new();
        md.update(data.as_bytes());
        md.finalize().iter().map(|b| format!("{:02x}", b)).collect()
    } else if sign_type == "SHA256" {
        let mut sha256 = Sha256::new();
        sha256.update(data.as_bytes());
        sha256.finalize().iter().map(|b| format!("{:02x}", b)).collect()
    } else {
        String::new()
    }
}

fn alipay_sign() {
    let mut params_map = HashMap::<String, String>::new();

    params_map.insert("service".to_string(), "api-demo".to_string());
    params_map.insert("partner".to_string(), "2088101568338364".to_string());
    params_map.insert(
        "timestamp".to_string(),
        Utc::now().timestamp_millis().to_string(),
    );

    let mut keys: Vec<&String> = params_map
        .keys()
        .filter(|k| *k != "sign" && *k != "sign_type")
        .collect();
    keys.sort();

    let mut params_str = String::new();
    for key in &keys {
        if let Some(value) = params_map.get(*key) {
            params_str.push_str(&format!("{}={}&", key, value));
        }
    }
    params_str.push_str(ALIPAY_SIGN_SECRET_KEY);

    println!("params_str=>{}", params_str);

    let sign_type = SUPPORT_SIGN_TYPE[1]; // "SHA256"
    let sign = compute_sign(sign_type, &params_str);
    println!("sign=>{}", sign);

    params_map.insert("sign_type".to_string(), sign_type.to_string());
    params_map.insert("sign".to_string(), sign);

    let ok = verify_alipay_sign(params_map);
    println!("verify_alipay_sign : {:?}", ok);

    // 测试不支持的签名类型
    let mut params_map2 = HashMap::<String, String>::new();
    params_map2.insert("sign_type".to_string(), "SHA512".to_string());
    params_map2.insert(
        "timestamp".to_string(),
        Utc::now().timestamp_millis().to_string(),
    );
    let result = verify_alipay_sign(params_map2);
    println!("SHA512 (unsupported) result: {:?}", result);
    assert!(result.is_err(), "Expected error for unsupported SHA512");
    println!("All tests passed!");
}

fn main() {
    alipay_sign();
}

签名流程详解

上述支付宝签名验证的完整流程如下:

  1. 参数收集:获取所有请求参数(HashMap<String, String>
  2. 参数过滤:排除 signsign_type 字段
  3. 参数排序:将剩余参数按 key 的字母序升序排列
  4. 参数拼接:按 key=value& 格式拼接所有参数
  5. 追加密钥:在拼接字符串末尾追加签名密钥
  6. 哈希计算:根据 sign_type 选择 MD5 或 SHA256 计算哈希值
  7. 签名比较:将计算得到的签名与请求中携带的签名进行比对

微信支付签名简介

微信支付的签名流程与支付宝类似,但使用的是 MD5 或 HMAC-SHA256,并且签名串的构造方式略有不同。微信支付 v3 API 使用 SHA256-RSA2048 签名,签名串的构造格式为:

HTTP请求方法\n
URL路径\n
时间戳\n
随机字符串\n
请求报文主体\n

然后使用商户的 RSA 私钥对签名串进行签名,接收方使用商户的 RSA 公钥进行验签。

20.6 哈希的常见应用场景

数据完整性校验

哈希函数最常见的用途之一是验证数据的完整性。下载文件时,发布方通常会提供文件的哈希值(如 SHA-256),用户下载后可以重新计算哈希值并与发布方提供的值比对,确认文件未被篡改。

use sha2::{Sha256, Digest};
use std::fs::File;
use std::io::{self, Read};

fn compute_file_hash(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;
    let mut hasher = Sha256::new();
    let mut buffer = [0u8; 8192];
    loop {
        let n = file.read(&mut buffer)?;
        if n == 0 { break; }
        hasher.update(&buffer[..n]);
    }
    Ok(hex::encode(hasher.finalize()))
}

fn main() -> Result<(), io::Error> {
    let hash = compute_file_hash("Cargo.toml")?;
    println!("SHA-256(Cargo.toml) = {}", hash);
    Ok(())
}

密码存储(加盐哈希)

存储用户密码时,绝对不能存储明文密码,也不应直接存储密码的哈希值(容易被彩虹表攻击)。正确的做法是使用加盐哈希(Salted Hash):

$$\text{stored} = H(password \parallel salt)$$

其中 $salt$ 是一个随机生成的值,每个用户的 salt 都不同。更推荐的做法是使用专门的密码哈希算法如 bcrypt、scrypt 或 Argon2,它们内置了 salt 管理并增加了计算成本以抵抗暴力破解。

// Cargo.toml: bcrypt = "0.15"
use bcrypt::{hash, verify, DEFAULT_COST};

fn main() -> Result<(), bcrypt::BcryptError> {
    let password = "my-secret-password";

    // 哈希密码(自动生成 salt)
    let hashed = hash(password, DEFAULT_COST)?;
    println!("Hashed password: {}", hashed);

    // 验证密码
    let valid = verify(password, &hashed)?;
    println!("Password valid: {}", valid);

    let invalid = verify("wrong-password", &hashed)?;
    println!("Wrong password valid: {}", invalid);

    Ok(())
}

数字签名

数字签名是公钥密码学与哈希函数的结合。发送方用自己的私钥对消息的哈希值进行签名,接收方用发送方的公钥验证签名。由于哈希函数将任意长度的消息压缩为固定长度,数字签名只需要对短得多的哈希值进行运算,大大提高了效率。

数字签名的流程:

  1. 发送方计算消息的哈希值:$h = H(m)$
  2. 发送方用私钥签名:$\sigma = Sign_{sk}(h)$
  3. 接收方用公钥验证:$Verify_{pk}(h, \sigma)$

布隆过滤器

布隆过滤器(Bloom Filter)是一种空间高效的概率型数据结构,用于判断一个元素是否属于某个集合。它使用多个哈希函数将元素映射到位数组中。布隆过滤器可能产生假阳性(误判为存在),但不会产生假阴性(不会漏判)。

对于 $m$ 位的位数组和 $k$ 个哈希函数,插入 $n$ 个元素后,假阳性概率为:

$$P(\text{false positive}) \approx \left(1 - e^{-kn/m}\right)^k$$

// Cargo.toml: bloom = "0.3"
use bloom::Bloom;

fn main() {
    let mut bloom = Bloom::new_for_fp_rate(1000, 0.01);

    bloom.insert("hello");
    bloom.insert("world");

    println!("Contains 'hello': {}", bloom.contains(&"hello")); // true
    println!("Contains 'world': {}", bloom.contains(&"world")); // true
    println!("Contains 'rust':  {}", bloom.contains(&"rust"));  // 可能 false(假阳性概率约1%)
}

Git 版本控制(SHA-1 内容寻址)

Git 使用 SHA-1 哈希作为内容寻址的基础。Git 中的每个对象(blob、tree、commit)都通过其内容的 SHA-1 哈希值来标识,这个哈希值被称为 object ID(OID)。这种设计保证了:

  • 相同内容必定产生相同的哈希值(数据去重)
  • 不同内容几乎不可能产生相同的哈希值(数据完整性)
  • 任何内容的修改都会导致哈希值变化(变更追踪)
// Cargo.toml: sha1 = "0.10"
use sha1::{Sha1, Digest};

fn main() {
    let mut hasher = Sha1::new();
    hasher.update(b"hello world");
    let result = hasher.finalize();
    println!("SHA-1('hello world') = {}", hex::encode(result));
}

注意:虽然 SHA-1 在理论上已被破解,但 Git 仍在使用它。Git 社区正在逐步迁移到 SHA-256(Git v2.29+ 开始支持 SHA-256 对象格式)。

20.7 总结

加密哈希 vs 非加密哈希对比

特性非加密哈希加密哈希
设计目标速度优先安全优先
单向性不要求必须满足
抗碰撞性不要求必须满足
抗 HashDoS部分支持(如 SipHash)天然支持
典型算法CRC32, MurmurHash, SipHash, XxHash, HighwayHashMD5, SHA-256, SHA-3, BLAKE3, SM3
典型场景哈希表、布隆过滤器、数据校验密码存储、数字签名、消息认证
速度极快(数 GB/s)较慢(数百 MB/s)
输出长度可变(32~256位)固定(128~512位)

算法选择建议

场景推荐算法理由
HashMap / HashSetSipHash(Rust 默认)抗 HashDoS,标准库内置
布隆过滤器MurmurHash3 / XxHash速度快,分布均匀
大数据处理 / 缓存XXH3 / HighwayHash极高吞吐量
文件校验SHA-256安全可靠,广泛支持
API 签名验证SHA-256 / HMAC-SHA256行业标准
密码存储bcrypt / Argon2专门设计的密码哈希算法
数字签名SHA-256 + RSA/ECDSA行业标准组合
国密合规场景SM3 / SM2满足国密标准要求
通用加密哈希(新项目)BLAKE3性能最优,安全性好

关键要点

  1. 不要用 MD5 或 SHA-1 做任何安全相关的事情,它们已被破解。
  2. Rust 的 HashMap 默认使用 SipHash,这是一个优秀的安全设计决策。
  3. 密码存储请使用 bcrypt/Argon2,不要自己实现加盐哈希。
  4. API 签名验证是哈希函数在互联网应用中最常见的实际用途之一。
  5. BLAKE3 是目前综合性能最好的加密哈希算法,适合新项目使用。
  6. 非加密哈希和加密哈希的用途完全不同,选择时务必根据场景判断。

20.8 练习题

练习 1:基础概念

请解释哈希函数的“雪崩效应“,并编写一个 Rust 程序,计算 hellohellp(仅一个字母不同)的 SHA-256 哈希值,统计两个哈希值中有多少位不同。

练习 2:非加密哈希性能对比

使用 crc32fastxxhash-rusthighway 三个 crate,分别对 1MB 的随机数据进行哈希计算,测量并比较它们的耗时。使用 std::time::Instant 进行计时。

练习 3:实现简单文件校验工具

编写一个 Rust 命令行工具,接受文件路径作为参数,计算并输出该文件的 SHA-256 哈希值。要求支持流式读取大文件(不要一次性读入内存)。

练习 4:HMAC 签名与验证

编写一个 Rust 程序,实现以下功能:

  • 使用 HMAC-SHA256 对消息 "transfer 1000 to account A" 进行签名
  • 使用相同的密钥验证签名
  • 使用不同的密钥验证签名(预期失败)

练习 5:密码存储系统

使用 bcrypt crate 实现一个简单的用户注册和登录系统:

  • 注册时对密码进行 bcrypt 哈希
  • 登录时验证密码是否正确
  • 测试错误密码是否能通过验证

练习 6:理解碰撞概率

根据生日攻击理论,对于输出长度为 $n$ 位的哈希函数,找到碰撞的期望尝试次数约为 $\sqrt{2^n}$。请计算:

  • MD5(128位)的碰撞期望次数
  • SHA-256(256位)的碰撞期望次数
  • SHA-256 的碰撞难度是 MD5 的多少倍?

练习 7:API 签名扩展

基于本章的支付宝签名验证代码,扩展 SUPPORT_SIGN_TYPE 以支持 HMAC-SHA256 签名方式。要求:

  • 添加 HMAC-SHA256 到支持的签名类型列表
  • compute_sign 函数中实现 HMAC-SHA256 签名逻辑
  • 编写测试验证新签名类型的正确性

练习 8:布隆过滤器实现

不使用第三方布隆过滤器库,使用 Rust 标准库中的 Vec<bool> 和两个不同的哈希函数(如 SipHash 的不同种子),手动实现一个简单的布隆过滤器。要求支持 insertcontains 操作,并测试其假阳性率。


参考链接

Hash 算法

CRC32 循环冗余校验

第二十一章 打包/拆包 压缩/解压

21.1 概述

一般而言,数据越小在存储时占用的空间更小、在传输时速度更快、在处理时耗时更少。 在大数据时代,海量数据的存储、传输、处理都将耗费巨额的成本。为了减少成本, 对数据采用恰当的编码算法进行压缩变得十分重要。AWS 压缩算法从 gzip 切换到 zstd,节约 30% 存储空间

为什么要压缩

  • 存储成本:数据量越大,存储介质的采购和维护成本越高。压缩可以显著降低存储需求。
  • 传输速度:网络带宽是有限资源,压缩后的数据传输更快,用户体验更好。
  • 处理效率:更小的数据意味着更少的 I/O 操作和内存占用,处理速度更快。

压缩的本质:消除冗余

数据压缩的核心思想是消除冗余。现实世界中的数据往往存在大量重复模式和可预测的结构, 例如文本中高频出现的字符、图像中大面积相同颜色的区域、音频中的静音段等。 压缩算法通过识别这些冗余并用更紧凑的方式表示它们来实现数据缩减。

信息论基础

压缩的理论极限由**香农熵(Shannon Entropy)**给出。对于离散随机变量 $X$,其熵定义为:

$$H(X) = -\sum_{x \in X} p(x) \log_2 p(x)$$

其中 $p(x)$ 是符号 $x$ 出现的概率。香农熵的单位是比特(bit),表示编码每个符号所需的平均最小比特数。

关键结论

  • 任何无损压缩算法的平均编码长度不可能小于信源的香农熵。
  • 当数据中各符号等概率分布时,熵最大,压缩效果最差。
  • 当数据中存在大量重复符号时,熵较小,压缩空间大。

例如,对于只包含字符 AB 的字符串:

  • A 出现概率为 0.9,B 为 0.1,则 $H(X) = -(0.9 \log_2 0.9 + 0.1 \log_2 0.1) \approx 0.469$ bit/符号
  • AB 各出现概率 0.5,则 $H(X) = 1.0$ bit/符号

常用场景

  • 文件、日志归档
  • 网络传输
  • 数据存储
  • 敏感数据加密前先压缩

21.2 压缩算法分类

无损压缩 vs 有损压缩

类型说明适用场景示例
无损压缩压缩后可完整还原原始数据,不丢失任何信息文本、代码、数据库、配置文件gzip、zstd、LZ4、Snappy
有损压缩压缩后无法完整还原,丢弃部分信息图片、音频、视频JPEG、MP3、H.264、WebP

本章重点讨论无损压缩,因为它是系统编程和数据处理中最常用的压缩方式。

通用压缩 vs 专用压缩

类型说明特点
通用压缩不依赖特定数据类型,适用于任意数据压缩率适中,适用范围广
专用压缩针对特定数据格式优化压缩率极高,但适用范围窄

例如:

  • 通用压缩:gzip、zstd、LZ4 可压缩任意二进制或文本数据
  • 专用压缩:PNG(图像)、FLAC(音频)、VP9(视频)针对特定数据类型优化

压缩算法全景

算法类型压缩率速度典型用途
Huffman统计编码文本编码、DEFLATE的组成部分
LZ77/LZ78字典编码通用压缩基础
DEFLATE混合(LZ77+Huffman)中高gzip、zip、HTTP
Snappy字典编码极快数据库、分布式系统
LZ4字典编码极快实时压缩、缓存
Zstandard (zstd)混合云存储、大数据
Brotli混合(LZ77+Huffman)HTTP内容压缩
LZO字典编码内核压缩、嵌入式

21.3 经典压缩算法原理

21.3.1 哈夫曼编码(Huffman Coding)

哈夫曼编码是一种变长编码方案,由 David Huffman 于 1952 年提出。其核心思想是: 频率高的字符使用较短的编码,频率低的字符使用较长的编码,从而最小化整体编码长度。

构建哈夫曼树

  1. 统计每个字符的出现频率
  2. 将每个字符作为一个叶子节点,放入优先队列(按频率排序)
  3. 每次取出频率最低的两个节点,合并为一个新的内部节点(频率为两者之和)
  4. 将新节点放回优先队列
  5. 重复步骤 3-4,直到只剩一个根节点
  6. 从根节点到叶子节点的路径即为该字符的编码(左分支为 0,右分支为 1)

编码示例

假设文本为 BCAADDDCCACACAC,字符频率统计:

字符频率
A5
B1
C6
D3

构建哈夫曼树后,可能的编码结果:

字符编码频率总比特数
C066
A10510
D11039
B11113

总比特数 = 6 + 10 + 9 + 3 = 28 bit,而固定长度编码需要 $15 \times 2 = 30$ bit(4个字符需要2 bit编码)。

Rust 实现哈夫曼编码

#![allow(unused)]
fn main() {
use std::collections::{BinaryHeap, HashMap};
use std::cmp::Ordering;

#[derive(Debug, Eq, PartialEq)]
struct HuffmanNode {
    freq: usize,
    char: Option<char>,
    left: Option<Box<HuffmanNode>>,
    right: Option<Box<HuffmanNode>>,
}

impl Ord for HuffmanNode {
    fn cmp(&self, other: &Self) -> Ordering {
        other.freq.cmp(&self.freq) // 最小堆
    }
}

impl PartialOrd for HuffmanNode {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl HuffmanNode {
    fn leaf(freq: usize, c: char) -> Self {
        HuffmanNode { freq, char: Some(c), left: None, right: None }
    }

    fn internal(freq: usize, left: Box<HuffmanNode>, right: Box<HuffmanNode>) -> Self {
        HuffmanNode { freq, char: None, left: Some(left), right: Some(right) }
    }
}

fn build_huffman_tree(freq_map: &HashMap<char, usize>) -> Option<HuffmanNode> {
    let mut heap: BinaryHeap<HuffmanNode> = freq_map
        .iter()
        .map(|(&c, &f)| HuffmanNode::leaf(f, c))
        .collect();

    while heap.len() > 1 {
        let left = heap.pop().unwrap();
        let right = heap.pop().unwrap();
        let parent = HuffmanNode::internal(left.freq + right.freq, Box::new(left), Box::new(right));
        heap.push(parent);
    }

    heap.pop()
}

fn generate_codes(node: &HuffmanNode, prefix: String, codes: &mut HashMap<char, String>) {
    match (&node.left, &node.right) {
        (None, None) => {
            if let Some(c) = node.char {
                codes.insert(c, prefix);
            }
        }
        _ => {
            if let Some(left) = &node.left {
                generate_codes(left, format!("{}0", prefix), codes);
            }
            if let Some(right) = &node.right {
                generate_codes(right, format!("{}1", prefix), codes);
            }
        }
    }
}

fn huffman_encode(text: &str) -> (String, HashMap<char, String>) {
    // 统计频率
    let mut freq_map = HashMap::new();
    for c in text.chars() {
        *freq_map.entry(c).or_insert(0) += 1;
    }

    // 构建哈夫曼树
    let root = build_huffman_tree(&freq_map).unwrap();

    // 生成编码表
    let mut codes = HashMap::new();
    generate_codes(&root, String::new(), &mut codes);

    // 编码文本
    let encoded: String = text.chars().map(|c| codes.get(&c).unwrap()).collect();
    (encoded, codes)
}

#[test]
fn test_huffman() {
    let text = "BCAADDDCCACACAC";
    let (encoded, codes) = huffman_encode(text);

    println!("编码表: {:?}", codes);
    println!("原始文本: {} ({} 字符)", text, text.len());
    println!("编码结果: {} ({} bit)", encoded, encoded.len());
    println!("固定编码: {} bit", text.len() * 2);
}
}

21.3.2 LZ77 / LZ78 算法

LZ77 和 LZ78 是由 Abraham Lempel 和 Jacob Ziv 于 1977 年和 1978 年提出的两种字典压缩算法, 是现代压缩技术的基石。

LZ77:滑动窗口算法

LZ77 使用一个**滑动窗口(Sliding Window)**在已处理的输出中查找与当前输入匹配的内容。 其核心思想是:用 (距离, 长度) 对来替代重复出现的字符串

工作原理

  1. 维护一个固定大小的滑动窗口(通常 4KB ~ 64KB)
  2. 从当前位置向前搜索窗口内是否有匹配的字符串
  3. 如果找到匹配(长度 >= 最小匹配长度),输出 (距离, 长度)
  4. 如果未找到匹配,输出原始字符

编码格式(offset, length, next_char)

  • offset:匹配位置相对于当前位置的偏移量
  • length:匹配的长度
  • next_char:匹配后的下一个字符

示例

文本 AABAABAAAAB 的 LZ77 编码过程(窗口大小为 6):

位置当前字符匹配编码
0A(0, 0, ‘A’)
1A(0, 0, ‘A’)
2B(0, 0, ‘B’)
3AAAB (offset=3, len=3)(3, 3, ‘A’)
7AAA (offset=1, len=2)(1, 2, ‘B’)

LZ78:显式字典算法

LZ78 与 LZ77 不同,它维护一个显式字典,将新发现的短语逐步加入字典中。 每次编码时,在字典中查找最长匹配,然后输出字典索引和下一个字符。

21.3.3 DEFLATE 算法

DEFLATE 是 LZ77 与 Huffman 编码的结合体,由 Phil Katz 于 1993 年设计, 广泛应用于 gzip、zip、PNG 等格式中。

DEFLATE 的工作流程

  1. LZ77 阶段:使用滑动窗口消除重复字符串,生成字面量和 (距离, 长度) 对的序列
  2. Huffman 编码阶段:对 LZ77 的输出进行 Huffman 编码,进一步压缩
原始数据 → LZ77编码(消除重复)→ Huffman编码(消除统计冗余)→ 压缩数据

gzip 就是基于 DEFLATE 算法,额外添加了文件头、CRC32 校验等元数据。

常见压缩算法学习


21.4 现代压缩算法

21.4.1 Snappy

Snappy 是 Google 开发的压缩库, formerly known as Zippy。其设计目标是速度优先, 在牺牲一定压缩率的前提下追求极致的压缩和解压速度。

特点

  • 压缩速度约 250 MB/s,解压速度约 500 MB/s
  • 压缩率适中(通常 1.5x ~ 2x)
  • 不追求最大压缩率,适合实时场景
  • 广泛应用于 LevelDB、Cassandra、Hadoop 等系统

21.4.2 LZ4

LZ4 由 Yann Collet 开发,以极快的压缩和解压速度著称。

特点

  • 解压速度可达数 GB/s(接近内存带宽极限)
  • 压缩速度约 400 MB/s
  • 压缩率适中
  • 提供 LZ4 block format 和 LZ4 frame format 两种模式
  • 适用于实时压缩、缓存、内存数据库等场景

21.4.3 Zstandard(zstd)

Zstandard(简称 zstd)由 Facebook(现 Meta)的 Yann Collet 开发, 目标是提供速度与压缩率的最佳平衡。zstd 于 2016 年开源,已被 RFC 8878 标准化。

特点

  • 提供 1-22 级压缩级别,灵活调节速度与压缩率
  • 默认级别 3 的速度与 gzip 相当,但压缩率更高
  • 级别 19+ 可提供接近 LZMA 的压缩率
  • 支持流式压缩和字典压缩
  • 小数据压缩表现优异

压缩级别说明

级别速度压缩率适用场景
1极快实时压缩、网络传输
3(默认)中高通用场景
10离线存储
19极高归档存储
22极慢最高一次性压缩、长期归档

速度快,性能好!压缩神器 zstd

Rust 中使用 zstd

use std::fs::File;
#[cfg(target_os = "windows")]
use std::os::windows::prelude::MetadataExt;
#[cfg(target_os = "linux")]
use std::os::linux::fs::MetadataExt;
use zstd::stream;

fn main() -> std::io::Result<()> {
    // 压缩文件
    let source = File::open("C:\\data\\movies.json")?;
    let destination = File::create("C:\\data\\movies.zst")?;
    match stream::copy_encode(&source, &destination, 7) {
        Ok(_) => {
            let metadata1 = source.metadata()?;
            let metadata2 = destination.metadata()?;
            println!(
                "compress success: {} => {}",
                metadata1.file_size(),
                metadata2.file_size()
            )
        }
        Err(e) => println!("{}", e),
    }

    // 解压文件
    let destination = File::open("why-rust.zst")?;
    let bytes = stream::decode_all(destination).unwrap();
    println!("{}", String::from_utf8_lossy(&bytes));
    Ok(())
}

Cargo.toml 依赖

[dependencies]
zstd = "0.13"

zstd github

21.4.4 Brotli

Brotli 是 Google 开发的压缩算法,专为 HTTP 内容压缩优化, 已被 RFC 7932 标准化。 现代浏览器(Chrome、Firefox、Edge 等)均支持 Brotli 压缩。

特点

  • 使用 LZ77 + Huffman + 上下文建模的组合
  • 压缩率通常比 gzip 高 15-25%
  • 支持预定义字典(针对 HTML、CSS、JavaScript 等优化)
  • 解压速度与 gzip 相当,压缩速度较慢
  • 适用于 Web 服务器静态资源压缩

Brotli程序库 Brotli rust-brotli

21.4.5 各算法性能对比

算法压缩速度解压速度压缩率内存占用适用场景
Snappy极快极快数据库、分布式系统
LZ4极快极快(>10GB/s)极低实时压缩、缓存
zstd (lv3)中高通用场景、云存储
zstd (lv19)极高离线归档
gzip (DEFLATE)中高通用压缩、HTTP
BrotliHTTP内容压缩
LZMA/7z极慢极高长期归档

注意:以上数据为相对比较,实际性能受硬件、数据类型和压缩级别影响。 压缩率通常以文本数据为基准,二进制数据(如已压缩的媒体文件)压缩效果较差。


21.5 打包与归档

tar 格式

tar(Tape Archive)是一种打包(归档)格式,其作用是将多个文件和目录合并为一个文件。 需要注意的是:tar 本身不进行压缩,仅负责将多个文件合并为一个归档文件。

tar 文件通常与压缩算法配合使用,常见的组合:

格式压缩算法说明
.tar仅打包,不压缩
.tar.gz / .tgzgzip打包 + gzip 压缩
.tar.bz2bzip2打包 + bzip2 压缩
.tar.xzxz (LZMA2)打包 + xz 压缩
.tar.zstzstd打包 + zstd 压缩
.tar.lz4lz4打包 + lz4 压缩

文件打包(tar + zstd 压缩)

为了方便管理,先使用 tar 将多个文件打包成一个 tar 文件,然后使用 gzip、lz4、zstd 等压缩算法压缩 tar 文件。 而当我们需要使用文件时,则先解压后拆包得到原先的文件。

#![allow(unused)]
fn main() {
use std::fs::File;
use tar::Builder;
use zstd::stream;

#[test]
fn archive_encode() {
    // 打包
    let file = File::create("examples/file/foo.tar").unwrap();
    let mut a = Builder::new(file);
    a.append_path("why-rust.txt").unwrap();
    a.append_path("sensitive-words.txt").unwrap();
    a.append_path("large_file.txt").unwrap();

    // 压缩
    let source = File::open("examples/file/foo.tar").unwrap();
    let destination = File::create("examples/file/foo.tar.zst").unwrap();
    match stream::copy_encode(&source, &destination, 7) {
        Ok(_) => {
            let metadata1 = source.metadata().unwrap();
            if let Ok(metadata2) = destination.metadata() {
                let size = metadata2.file_size();
                println!("compress success: {} => {}", metadata1.file_size(), size);
            }

            if let Ok(metadata) = fs::metadata("examples/file/foo.tar.zst") {
                println!(
                    "{:?},{},{:?}",
                    metadata.file_type(),
                    metadata.len(),
                    metadata.created().unwrap()
                );
            }
        }
        Err(e) => println!("copy_encode : {}", e),
    }
}
}

文件拆包(解压 + tar 拆包)

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::Read;
use tar::Archive;
use zstd::stream;

#[test]
fn decode_unpackage() {
    // 解压 zst 文件
    if let Ok(source) = File::open("examples/file/github_users_sample_set.tar.zst") {
        if let Ok(destination) = File::create("examples/file/github_users_sample_set.tar") {
            stream::copy_decode(source, destination);
        }
    }

    // 拆包 tar 文件
    let file = File::open("examples/file/github_users_sample_set.tar").unwrap();
    let mut a = Archive::new(file);

    for file in a.entries().unwrap() {
        // Make sure there wasn't an I/O error
        let mut file = file.unwrap();

        // Inspect metadata about the file
        println!("{:?}", file.header().path().unwrap());
        println!("{}", file.header().size().unwrap());

        // files implement the Read trait
        let mut s = String::new();
        file.read_to_string(&mut s).unwrap();
        println!("{}", s);
    }
}
}

Linux 相关命令

# 打包压缩
tar -zcvf destination.tar.gz source          # gzip 压缩
tar -jcvf destination.tar.bz2 source          # bzip2 压缩
tar -Jcvf destination.tar.xz source           # xz 压缩
tar --zstd -cvf destination.tar.zst source     # zstd 压缩

# 解压拆包
tar -zxvf destination.tar.gz                  # gzip 解压
tar -jxvf destination.tar.bz2                 # bzip2 解压
tar -Jxvf destination.tar.xz                  # xz 解压
tar --zstd -xvf destination.tar.zst           # zstd 解压

编码知识补充:base64 编码将 3 字节映射为 4 字符(膨胀 33%),hex 编码将 1 字节映射为 2 字符(膨胀 100%)。 这两种编码的目的不是压缩,而是将二进制数据转换为文本安全格式。


21.6 压缩与加密的顺序

压缩一定在加密之前。 因为加密以后,比特序列的冗余性消失,基本上无法再压缩了。 在加密前进行压缩的做法不仅仅限于混合密码系统,而是对所有密码都适用。

为什么加密后无法压缩

加密算法(如 AES)的设计目标是将数据随机化,使得密文在统计上与随机数据不可区分。 这意味着:

  1. 加密后的数据熵接近最大值(每个比特等概率为 0 或 1)
  2. 没有任何重复模式可以利用
  3. 压缩算法无法找到可消除的冗余

从信息论角度看,加密后数据的熵为 $H(X) = n$($n$ 为比特数),即每个比特携带 1 bit 信息, 已经达到理论极限,无法进一步压缩。

正确的处理顺序

原始数据 → 压缩 → 加密 → 传输/存储
接收数据 → 解密 → 解压 → 原始数据

Shell 命令示例

# 正确顺序:先压缩,再加密
tar -zcvf destination.tar.gz source
gpg -c destination.tar.gz

# 解密后解压
gpg -d destination.tar.gz.gpg | tar -zxvf -

21.7 Rust 中的压缩库

Rust 生态中拥有丰富的压缩/解压库,以下列出常用的库及其用途:

核心压缩库

库名用途说明
flate2gzip / deflate / zlib最通用的压缩库,支持 DEFLATE 系列算法
zipZIP 格式读写 ZIP 压缩包
tartar 归档读写 tar 打包文件
zstdZstandard 压缩Facebook 的 zstd 算法 Rust 绑定
lz4_flexLZ4 压缩纯 Rust 实现的 LZ4 压缩
brotliBrotli 压缩Google 的 Brotli 算法 Rust 实现
snapSnappy 压缩纯 Rust 实现的 Snappy 压缩
xz2xz / LZMA2 压缩xz 压缩格式的 Rust 绑定

相关资源链接

  • snappy - Snappy is a compression/decompression library.
  • rust-snappy
  • zstd github - Zstandard - Fast real-time compression algorithm
  • RFC 8878 - Zstandard 压缩算法规范
  • Brotli - Google Brotli 压缩库
  • rust-brotli - Brotli 的 Rust 实现
  • RFC 7932 - Brotli 压缩算法规范
  • LZ4 - LZ4 极速压缩库
  • orz - 基于 LZ77 的列式压缩库

Cargo.toml 依赖示例

[dependencies]
# gzip / deflate / zlib 压缩
flate2 = "1"

# ZIP 压缩包读写
zip = "2"

# tar 归档
tar = "0.4"

# zstd 压缩
zstd = "0.13"

# LZ4 压缩
lz4_flex = "0.11"

# Brotli 压缩
brotli = "7"

# Snappy 压缩
snap = "1"

flate2 使用示例(gzip 压缩)

#![allow(unused)]
fn main() {
use flate2::write::GzEncoder;
use flate2::read::GzDecoder;
use flate2::Compression;
use std::fs::File;
use std::io::{Write, Read};

fn compress_gzip(input_path: &str, output_path: &str) -> std::io::Result<()> {
    let input = File::open(input_path)?;
    let output = File::create(output_path)?;
    let mut encoder = GzEncoder::new(output, Compression::default());
    std::io::copy(&mut input.take(0), &mut encoder)?;
    encoder.finish()?;
    Ok(())
}

fn decompress_gzip(input_path: &str, output_path: &str) -> std::io::Result<()> {
    let input = File::open(input_path)?;
    let mut decoder = GzDecoder::new(input);
    let mut output = Vec::new();
    decoder.read_to_end(&mut output)?;
    std::fs::write(output_path, output)?;
    Ok(())
}
}

21.8 总结

压缩算法对比总览

算法压缩率压缩速度解压速度复杂度适用场景
Huffman文本编码、理论教学
LZ77通用压缩基础
DEFLATE/gzip中高通用压缩、HTTP、ZIP
Snappy极快极快数据库、实时处理
LZ4极快极快实时压缩、缓存、内存数据库
zstd高(可调)快(可调)通用场景、云存储、大数据
BrotliHTTP 内容压缩
LZMA/7z极高极慢长期归档

场景选择建议

场景推荐算法理由
Web 服务器静态资源Brotli / gzip浏览器原生支持,压缩率高
数据库列存储LZ4 / Snappy压缩/解压速度极快,减少延迟
日志归档zstd (lv3~10)压缩率高,速度可接受
长期冷数据归档zstd (lv19+) / LZMA最大压缩率,节省存储成本
实时网络传输LZ4 / Snappy极低延迟
通用文件压缩zstd (默认级别)速度与压缩率的最佳平衡
内存中缓存LZ4解压速度接近内存拷贝
电子书/文档分发gzip / DEFLATE通用兼容性好

关键要点

  1. 压缩的本质是消除冗余,理论上限由香农熵决定
  2. 没有万能的压缩算法,需要根据场景在速度、压缩率、内存占用之间权衡
  3. 先压缩,后加密,加密后的数据无法再压缩
  4. tar 只打包不压缩,需要配合压缩算法使用
  5. zstd 是当前最推荐的通用压缩算法,在速度和压缩率之间取得了优秀的平衡

21.9 练习题

练习 1:计算字符串 "aabbbccccddddd" 的香农熵 $H(X)$,并解释为什么该字符串具有较好的可压缩性。

练习 2:使用 Rust 的 flate2 库实现对一个文本文件的 gzip 压缩和解压,并比较压缩前后的文件大小。

练习 3:手动构建字符串 "ABRACADABRA" 的哈夫曼树,写出每个字符的编码,并计算编码后的总比特数。

练习 4:对字符串 "ABCABCABCABC" 进行 LZ77 编码(假设窗口大小为 9),写出每一步的编码结果。

练习 5:使用 Rust 的 tarzstd 库,编写一个函数将指定目录下的所有 .log 文件打包并压缩为 .tar.zst 格式。

练习 6:为什么加密后的数据几乎无法再压缩?请从信息论的角度给出解释。

练习 7:分别使用 zstd 的压缩级别 1、3、10、19 压缩同一个大文件(>100MB),记录每种级别的压缩时间和压缩后文件大小,绘制速度-压缩率曲线并分析。

练习 8:在一个 Web 服务项目中,分别使用 gzip 和 Brotli 压缩 JSON API 响应数据,比较压缩率和响应时间,给出你的选型建议。

第二十二章 查询、检索与搜索

22.1 概述

查询(Query)是计算机科学中最基础也最广泛的应用之一。从最简单的数组查找,到复杂的搜索引擎,查询技术贯穿了软件开发的方方面面。本章将系统介绍查询领域的核心算法与数据结构,并结合 Rust 代码实现进行讲解。

查询在计算机中有十分广泛的应用:

  • 字符串与文本查询:在字符串或文本文件中查询关键字、模式匹配、正则表达式
  • 数据结构查询:在数组、树、哈希表等数据结构中查询指定数据
  • 数据库查询:通过 SQL 等语言从数据库中检索数据
  • 海量非结构化数据查询:在大量非结构化文件中进行全文检索
  • 搜索引擎:在互联网海量网页中快速定位相关信息

下面是本章涉及的主要查询算法及其时间复杂度概览:

查询类别算法/方法时间复杂度适用场景
字符串模式匹配暴力匹配$O(m \times n)$简单场景、短字符串
字符串模式匹配KMP 算法$O(m + n)$需要多次匹配的文本
字符串模式匹配Boyer-Moore$O(m + n)$ 平均实际工程中常用
字符串模式匹配Rabin-Karp$O(m + n)$ 平均多模式匹配
敏感词检测DFA$O(n)$敏感词过滤
正则表达式NFA/DFA取决于模式文本验证与提取
线性查找顺序扫描$O(n)$无序数据
二分查找折半查找$O(\log n)$有序数组
哈希查找哈希表$O(1)$ 平均键值对查找
树结构查找BST$O(\log n)$ 平均动态有序集合
树结构查找B+ 树$O(\log n)$数据库索引
全文检索倒排索引$O(1) \sim O(\log n)$搜索引擎

其中 $m$ 为模式串长度,$n$ 为文本串长度。


22.2 字符串模式匹配

模式匹配是字符串处理中的一种基本运算:给定一个模式串(pattern),要求在某个文本串中找出与该模式串相同的所有子串的位置。

模式匹配

经典问题:strStr()

22.2.1 暴力匹配法

暴力匹配法是最直观的字符串匹配方法:从文本串的每个位置出发,逐个字符与模式串进行比较。若匹配成功则记录位置,否则回溯到文本串的下一个位置重新开始。

算法思路:

  1. 设文本串为 $T[0 \ldots n-1]$,模式串为 $P[0 \ldots m-1]$
  2. 从 $i = 0$ 开始,比较 $T[i \ldots i+m-1]$ 与 $P[0 \ldots m-1]$
  3. 若全部匹配,则找到一个匹配位置 $i$
  4. 否则 $i \leftarrow i + 1$,重复步骤 2

时间复杂度: 最坏情况下为 $O(m \times n)$,即文本串和模式串的长度乘积。当文本串中存在大量与模式串部分匹配的子串时,性能退化严重。

/// 暴力字符串匹配
/// 返回所有匹配位置的起始下标
pub fn brute_force_search(text: &str, pattern: &str) -> Vec<usize> {
    let text_bytes = text.as_bytes();
    let pattern_bytes = pattern.as_bytes();
    let n = text_bytes.len();
    let m = pattern_bytes.len();
    let mut result = Vec::new();

    if m == 0 || m > n {
        return result;
    }

    for i in 0..=(n - m) {
        let mut matched = true;
        for j in 0..m {
            if text_bytes[i + j] != pattern_bytes[j] {
                matched = false;
                break;
            }
        }
        if matched {
            result.push(i);
        }
    }

    result
}

fn main() {
    let text = "ababcabcababc";
    let pattern = "abc";
    let positions = brute_force_search(text, pattern);
    println!("匹配位置: {:?}", positions); // [2, 5, 10]
}

22.2.2 KMP 算法

KMP(Knuth-Morris-Pratt)算法是字符串模式匹配的经典算法,由 D.E.Knuth、J.H.Morris 和 V.R.Pratt 三人于 1977 年联合发表。其核心思想是:利用已匹配的信息,避免不必要的回溯,从而将时间复杂度降低到 $O(m + n)$。

原理

在暴力匹配中,当匹配失败时,文本串的指针 $i$ 会回溯到之前的位置。但 KMP 算法发现:在已经匹配的部分中,可能存在“前缀“与“后缀“相同的情况,利用这一信息可以让文本串指针 $i$ 不回溯,只移动模式串指针 $j$。

关键概念——next 数组(部分匹配表):

next 数组 next[i] 表示模式串 $P[0 \ldots i]$ 中,最长相等真前缀和真后缀的长度。所谓“真前缀“和“真后缀“是指不包括整个串本身的前缀和后缀。

例如,模式串 "ABABC" 的 next 数组计算过程:

索引 $i$01234
字符ABABC
next[i]00120
  • $i=0$:"A" 无真前缀真后缀,next[0] = 0
  • $i=1$:"AB" 真前缀 "A" ≠ 真后缀 "B",next[1] = 0
  • $i=2$:"ABA" 真前缀 "A" = 真后缀 "A",next[2] = 1
  • $i=3$:"ABAB" 真前缀 "AB" = 真后缀 "AB",next[3] = 2
  • $i=4$:"ABABC" 真前缀与真后缀无相等,next[4] = 0

next 数组的构建

构建 next 数组的过程本身是一个递推过程:

$$ \text{next}[i] = \begin{cases} 0 & \text{若 } P[i] \neq P[j] \text{ 且 } j = 0 \ \text{next}[j-1] & \text{若 } P[i] \neq P[j] \text{ 且 } j > 0 \ j + 1 & \text{若 } P[i] = P[j] \end{cases} $$

其中 $j = \text{next}[i-1]$。

匹配过程

当 $T[i] \neq P[j]$ 时:

  • 令 $j = \text{next}[j-1]$(模式串指针回退到前缀匹配位置)
  • 文本串指针 $i$ 不动

当 $T[i] = P[j]$ 时:

  • $i \leftarrow i + 1$,$j \leftarrow j + 1$

当 $j = m$ 时,说明找到了一个完整匹配。

时间复杂度

  • 构建 next 数组:$O(m)$
  • 匹配过程:$O(n)$
  • 总计:$O(m + n)$

Rust 实现

/// https://github.com/TheAlgorithms/Rust/blob/master/src/string/knuth_morris_pratt.rs
pub fn knuth_morris_pratt(st: String, pat: String) -> Vec<usize> {
    if st.is_empty() || pat.is_empty() {
        return vec![];
    }

    let string = st.into_bytes();
    let pattern = pat.into_bytes();

    // build the partial match table
    let mut partial = vec![0];
    for i in 1..pattern.len() {
        let mut j = partial[i - 1];
        while j > 0 && pattern[j] != pattern[i] {
            j = partial[j - 1];
        }
        partial.push(if pattern[j] == pattern[i] { j + 1 } else { j });
    }

    // and read 'string' to find 'pattern'
    let mut ret = vec![];
    let mut j = 0;

    for (i, &c) in string.iter().enumerate() {
        while j > 0 && c != pattern[j] {
            j = partial[j - 1];
        }
        if c == pattern[j] {
            j += 1;
        }
        if j == pattern.len() {
            ret.push(i + 1 - j);
            j = partial[j - 1];
        }
    }

    ret
}

fn main() {
    let result = knuth_morris_pratt(String::from("ababcabc"), String::from("abc"));
    println!("{:?}", result);
}

22.2.3 Boyer-Moore 算法

Boyer-Moore(BM)算法是实际应用中最快的字符串匹配算法之一,由 Robert S. Boyer 和 J Strother Moore 于 1977 年提出。与 KMP 从左向右匹配不同,BM 算法从右向左进行匹配。

核心思想:

  1. 坏字符规则(Bad Character Rule):当模式串与文本串某个字符不匹配时,根据该字符在模式串中的位置,决定模式串向右滑动的距离。如果该字符不在模式串中,则可以跳过整个模式串的长度。

  2. 好后缀规则(Good Suffix Rule):当模式串的某个后缀已经匹配但某个字符不匹配时,利用已匹配的后缀信息来决定滑动距离。

BM 算法取坏字符规则和好后缀规则中滑动距离较大者,因此平均时间复杂度可达 $O(n/m)$,最坏情况为 $O(m + n)$。在实际应用中(如文本编辑器的查找功能),BM 算法通常比 KMP 更快。

22.2.4 Rabin-Karp 算法

Rabin-Karp 算法由 Michael O. Rabin 和 Richard M. Karp 于 1987 年提出,采用**滚动哈希(Rolling Hash)**的思想进行字符串匹配。

核心思想:

  1. 计算模式串的哈希值 $h(P)$
  2. 计算文本串中每个长度为 $m$ 的子串的哈希值
  3. 若哈希值相等,再逐字符比较确认(避免哈希冲突)

滚动哈希的优势: 当窗口从位置 $i$ 滑动到位置 $i+1$ 时,不需要重新计算整个子串的哈希值,而是通过减去最左边字符的贡献、加上新字符的贡献,在 $O(1)$ 时间内完成更新:

$$ h(T[i+1 \ldots i+m]) = (h(T[i \ldots i+m-1]) - T[i] \times d^{m-1}) \times d + T[i+m] $$

其中 $d$ 为字符集大小(基数)。

Rabin-Karp 算法的平均时间复杂度为 $O(m + n)$,特别适合多模式匹配的场景——只需对文本串遍历一次,与多个模式串的哈希值进行比较即可。

22.2.5 各算法对比

算法时间复杂度(最坏)时间复杂度(平均)空间复杂度特点
暴力匹配$O(m \times n)$$O(m + n)$$O(1)$实现简单,适合短串
KMP$O(m + n)$$O(m + n)$$O(m)$不回溯,适合流式处理
Boyer-Moore$O(m \times n)$$O(n/m)$$O(\sigma)$实际最快,从右向左
Rabin-Karp$O(m \times n)$$O(m + n)$$O(1)$适合多模式匹配

$\sigma$ 为字符集大小。


22.3 DFA 敏感词检测

22.3.1 DFA 原理

DFA(Deterministic Finite Automaton,确定有限状态自动机)是一种计算模型,它具有以下特征:

  • 有限个状态:自动机在任意时刻处于某个确定的状态
  • 确定性的转移:对于每个状态和输入字符,最多有一个转移目标
  • 初始状态和接受状态:从初始状态出发,根据输入字符序列进行状态转移,若最终到达接受状态,则表示匹配成功

DFA 可以用一个五元组 $M = (Q, \Sigma, \delta, q_0, F)$ 来形式化描述:

  • $Q$:有限状态集合
  • $\Sigma$:输入字母表(字符集)
  • $\delta$:状态转移函数 $Q \times \Sigma \to Q$
  • $q_0$:初始状态
  • $F$:接受状态集合($F \subseteq Q$)

22.3.2 Trie 树结构

在敏感词检测中,DFA 的状态转移图实际上是一棵 Trie 树(也称前缀树或字典树)。Trie 树的核心思想是:

  • 每个节点代表一个字符
  • 从根节点到某个节点的路径上的字符连接起来,构成一个前缀
  • 标记为结束的节点表示一个完整的敏感词

例如,敏感词集合 {"信用卡套现", "信用卡代付", "信用卡代还"} 构成的 Trie 树结构如下:

信
 └── 用
      └── 卡
           ├── 套
           │    └── 现 [END]
           └── 代
                ├── 付 [END]
                └── 还 [END]

22.3.3 最小匹配 vs 最大匹配

在敏感词检测中,有两种匹配策略:

  • 最小匹配(MinMatchType):一旦发现当前路径构成一个完整敏感词,立即停止并返回。适合对误判敏感的场景。
  • 最大匹配(MaxMatchType):继续沿 Trie 树向下查找,直到无法继续为止,返回最长的敏感词。适合需要检测所有嵌套敏感词的场景。

22.3.4 时间复杂度分析

DFA 敏感词检测的时间复杂度为 $O(n)$,其中 $n$ 为待检测文本的长度。这是因为:

  • 每个字符最多被访问一次(不回溯)
  • 状态转移的时间为 $O(1)$(HashMap 查找)
  • 无论敏感词库中有多少词,检测时间仅与文本长度线性相关

这使得 DFA 算法非常适合大规模敏感词库的实时检测。

22.3.5 Rust 完整实现

#![allow(unused)]
fn main() {
use lazy_static::lazy_static;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;

use std::str::Chars;

/// 敏感词检测DFA算法(Rust实现,参考Java版实现 https://www.cnblogs.com/shihaiming/p/7048379.html)
/// 由于语言方面的限制,具体实现与Java有一定的差异。
///
lazy_static! {
    static ref SENSITIVE_WORD_MAP: HashMap<char, SensitiveWordMap> = {
        let set = read_sensitive_word_file();
        build_sensitive_word_map(set)
    };
}

pub enum MatchType {
    MinMatchType, //最小匹配规则
    MaxMatchType, //最大匹配规则
}

#[derive(Debug)]
struct SensitiveWordMap {
    word: char,
    is_end: char,
    word_map: Option<HashMap<char, Box<SensitiveWordMap>>>,
}

/// 替换敏感字字符
/// # Examples
/// ```
/// let result = rust_by_example::dfa::replace_sensitive_word("信用卡之家", &MatchType::MinMatchType, '*')
/// assert_eq!(result,"**卡之家");
/// ```
pub fn replace_sensitive_word(txt: &str, match_type: &MatchType, replace_char: char) -> String {
    let set: BTreeSet<String> = find_sensitive_word(txt, match_type);
    let mut replace_str = String::from(txt);
    for word in set {
        let len = word.chars().count();
        let replace_chars: String = vec![replace_char; len].iter().collect();
        replace_str = replace_str.replace(word.as_str(), &replace_chars);
    }

    replace_str
}
/// 判断文字是否包含敏感字符
///
pub fn is_contains_sensitive_word(txt: &str, match_type: &MatchType) -> bool {
    let mut is_contains = false;
    let len = txt.chars().count();
    let txt_vec: Vec<char> = txt.chars().collect();
    let mut i = 0;
    while i < len {
        let length = check_sensitive_word(txt, i, match_type);
        if length > 0 {
            is_contains = true;
            break;
        }
        i += 1;
    }
    is_contains
}

/// 获取文字中的敏感词
///
pub fn find_sensitive_word(txt: &str, match_type: &MatchType) -> BTreeSet<String> {
    let mut sensitive_word_set = BTreeSet::<String>::new();
    let len = txt.chars().count();
    let txt_vec: Vec<char> = txt.chars().collect();
    let mut i = 0;
    while i < len {
        let length = check_sensitive_word(txt, i, match_type);
        if length > 0 {
            //存在,加入list中
            sensitive_word_set.insert(txt_vec[i..i + length].iter().collect());
            i += length - 1; //减1的原因,是因为循环会自增
        }
        i += 1;
    }

    sensitive_word_set
}

/// 查文字中是否包含检敏感字符,如果存在,则返回敏感词字符的长度,不存在返回0
///
fn check_sensitive_word(txt: &str, begin_index: usize, match_type: &MatchType) -> usize {
    let mut match_flag = 0;
    let mut last_match_length = 0;
    let mut word: char;
    let txt_vec: Vec<char> = txt.chars().collect();
    let len = txt.len();
    if let Some(word) = &txt_vec.get(begin_index) {
        if let Some(swm) = SENSITIVE_WORD_MAP.get(word) {
            match_flag += 1;
            if (*swm).is_end == '1' {
                last_match_length = match_flag;

                match match_type {
                    MatchType::MinMatchType => {
                        return last_match_length;
                    }
                    MatchType::MaxMatchType => (),
                }
            }

            //递归查找
            let mut j = begin_index + 1;
            recursive_find_map(
                swm,
                &txt_vec,
                &mut j,
                &mut match_flag,
                &mut last_match_length,
                match_type,
            );
        }
    }
    last_match_length
}
/// 递归查找map
///
fn recursive_find_map(
    swm: &SensitiveWordMap,
    txt_vec: &[char],
    i: &mut usize,
    match_flag: &mut usize,
    last_match_length: &mut usize,
    match_type: &MatchType,
) {
    if let Some(word) = txt_vec.get(*i) {
        if let Some(wm) = &swm.word_map {
            if let Some(next_swm) = wm.get(word) {
                *match_flag += 1;

                if swm.is_end == '1' {
                    *last_match_length = *match_flag;
                    match match_type {
                        MatchType::MinMatchType => {
                            return;
                        }
                        MatchType::MaxMatchType => (),
                    }
                }

                if next_swm.is_end == '1' {
                    *last_match_length = *match_flag;
                    match match_type {
                        MatchType::MinMatchType => {
                            return;
                        }
                        MatchType::MaxMatchType => (),
                    }
                }

                if let Some(nwm) = &next_swm.word_map {
                    if nwm.is_empty() {
                        *last_match_length = *match_flag;
                        match match_type {
                            MatchType::MinMatchType => {
                                return;
                            }
                            MatchType::MaxMatchType => (),
                        }
                    }
                }

                *i += 1;
                recursive_find_map(
                    next_swm,
                    txt_vec,
                    i,
                    match_flag,
                    last_match_length,
                    match_type,
                );
            }
        }
    }
}
/// 递归地修改map
fn recursive_build_map(map: &mut SensitiveWordMap, chars: &mut Chars, count: &mut usize) {
    if let Some(ch) = chars.next() {
        *count -= 1;
        if let Some(now_map) = map.word_map.as_mut() {
            // let contains_key = now_map.contains_key(&ch);

            if let std::collections::hash_map::Entry::Vacant(e) = now_map.entry(ch) {
                let mut is_end = if *count == 0 { '1' } else { '0' };
                let mut swm = SensitiveWordMap {
                    word: ch,
                    is_end,
                    word_map: Some(HashMap::<char, Box<SensitiveWordMap>>::new()),
                };
                now_map.insert(ch, Box::new(swm));
                if let Some(m) = now_map.get_mut(&ch) {
                    recursive_build_map(&mut *m, &mut *chars, count);
                }
            } else if let Some(m) = now_map.get_mut(&ch) {
                recursive_build_map(&mut *m, &mut *chars, count);
            }
        }
    }
}

/// 读取敏感词库,将敏感词放入HashMap中,构建一个DFA算法模型
///  {
///   '信': SensitiveWordMap {
///       word: '信',
///       is_end: '0',
///       word_map: Some({
///           '用': SensitiveWordMap {
///               word: '用',
///               is_end: '0',
///               word_map: Some({
///                   '卡': SensitiveWordMap {
///                       word: '卡',
///                       is_end: '0',
///                       word_map: Some({
///                           '套': SensitiveWordMap {
///                               word: '套',
///                               is_end: '0',
///                               word_map: Some({
///                                   '现': SensitiveWordMap {
///                                       word: '现',
///                                       is_end: '1',
///                                       word_map: Som e({})
///                                   }
///                               })
///                           },
///                           '代': SensitiveWordMap {
///                               word: '代',
///                               is_end: '0',
///                               word_map: Some({
///                                   '付': SensitiveWordMap {
///                                       word: '付',
///                                       is_end: '1',
///                                       word_map: Some({})
///                                   },
///                                   '还': SensitiveWordMap {
///                                       word: '还',
///                                       is_end: '1',
///                                       word_map: Some({})
///                                   }
///                               })
///                           }
///                       })
///                   }
///               })
///           }
///       })
///   }
///
fn build_sensitive_word_map(set: BTreeSet<String>) -> HashMap<char, SensitiveWordMap> {
    let mut sensitive_word_map = HashMap::<char, SensitiveWordMap>::new();

    let mut iterator = set.iter();
    for key in iterator {
        let len = key.chars().count();
        let mut count = len;
        let mut key_chars = key.chars();
        //读取每行的首个字符
        if let Some(first_char) = key_chars.next() {
            count -= 1;
            if let Some(word_map) = sensitive_word_map.get_mut(&first_char) {
                //读取下一个字符
                recursive_build_map(&mut *word_map, &mut key_chars, &mut count);
            } else {
                let mut is_end = if len == 1 { '1' } else { '0' };

                let mut now_map = SensitiveWordMap {
                    word: first_char,
                    is_end,
                    word_map: Some(HashMap::<char, Box<SensitiveWordMap>>::new()),
                };
                sensitive_word_map.insert(first_char, now_map);

                if let Some(now_map) = sensitive_word_map.get_mut(&first_char) {
                    recursive_build_map(&mut *now_map, &mut key_chars, &mut count);
                }
            }
        }
    }

    sensitive_word_map
}

/// 读取敏感词库中的内容,将内容添加到set集合中
fn read_sensitive_word_file() -> BTreeSet<String> {
    let mut set = BTreeSet::<String>::new();
    match File::open("sensitive-words.txt") {
        Ok(f) => {
            let reader = BufReader::new(f);
            let lines = reader.lines();
            for line in lines.map(|x| x.unwrap()) {
                println!("{}", line);

                set.insert(line);
            }
        }
        Err(e) => panic!("can't open this file :{}", e),
    }

    set
}

pub fn read_file() {
    let str_vec = vec![
        "花呗信用卡代还OK套现",
        "套花呗分期代付",
        "马上套现信用卡",
        "期货套利",
        "空手套白狼",
        "守信用卡脖子",
        "坚定信心,同舟共济,科学防治,精准施策",
        "D+1还是T+1秒到结算免结算费",
    ];

    println!("find_sensitive_word MaxMatchType......");
    for str in &str_vec {
        let set = find_sensitive_word(str, &MatchType::MaxMatchType);
        println!("{} --> {:?}", str, set);
    }

    println!("find_sensitive_word MinMatchType......");
    for str in &str_vec {
        let set = find_sensitive_word(str, &MatchType::MinMatchType);
        println!("{} --> {:?}", str, set);
    }

    println!("is_contains_sensitive_word......");
    for str in &str_vec {
        let is_contains = is_contains_sensitive_word(str, &MatchType::MinMatchType);
        println!("{} is contains sensitive words : {}", str, is_contains);
    }

    println!("replace_sensitive_word......");
    for str in &str_vec {
        let replace_str = replace_sensitive_word(str, &MatchType::MinMatchType, '*');

        println!("{} --> {}", str, replace_str);
    }

    let result = replace_sensitive_word("信用卡之家", &MatchType::MinMatchType, '*');
    assert_eq!(result, "**卡之家");
}

#[test]
fn sub_str() {
    //实现类似Java String.substring()的功能,注意并不是适用于所有的字符。
    let str = String::from("hello world");
    let char_vec: Vec<char> = str.chars().collect();
    let sub_str: String = char_vec[0..5].iter().collect();
    println!("sub_str:{}", sub_str);

    //不能使用上述代码进行截取子字符串的字符
    for c in "नमस्ते".chars() {
        println!("{}", c);
    }
}

#[test]
fn set_iter() {
    let mut b_tree_set = BTreeSet::<String>::new();
    b_tree_set.insert(String::from("A"));
    b_tree_set.insert(String::from("B"));
    b_tree_set.insert(String::from("C"));
    b_tree_set.insert(String::from("D"));
    b_tree_set.insert(String::from("E"));

    for val in &b_tree_set {
        println!("{}", val);
    }

    let rm_key = String::from("C");
    b_tree_set.remove(&rm_key);

    println!("b_tree_set has {} items", b_tree_set.len());

    println!("using VSCode coding rust program is greate");
}

}

22.4 正则表达式

正则表达式(Regular Expression,简称 regex)是一种用于描述字符串模式的强大工具。它使用特定的语法规则来定义文本匹配的模式,广泛应用于文本搜索、验证、替换和提取等场景。

22.4.1 正则表达式基础语法

正则表达式由普通字符和特殊字符(元字符)组成。以下是常用的元字符:

符号含义示例
.匹配任意单个字符(除换行符)a.c 匹配 abca1c
*前一个字符出现 0 次或多次ab*c 匹配 acabbc
+前一个字符出现 1 次或多次ab+c 匹配 abcabbc
?前一个字符出现 0 次或 1 次ab?c 匹配 acabc
^匹配行首^Hello 匹配行首的 Hello
$匹配行尾world$ 匹配行尾的 world
\d匹配数字 [0-9]\d+ 匹配 123
\w匹配字母、数字、下划线\w+ 匹配 hello_1
\s匹配空白字符\s+ 匹配空格、制表符
[abc]字符集合,匹配其中任一字符[aeiou] 匹配元音
{n,m}前一个字符出现 n 到 m 次a{2,4} 匹配 aaaaa
(...)分组捕获(ab)+ 匹配 abab
|或(alternation)cat|dog 匹配 cat 或 dog

22.4.2 Rust regex 库

Rust 的正则表达式生态以 regex crate 为核心,它基于正则表达式的 NFA(非确定有限状态自动机)引擎实现,提供了高性能的模式匹配能力。

Cargo.toml 中添加依赖:

[dependencies]
regex = "1"
lazy_static = "1"

正则表达式的四大核心功能:

功能方法说明
查询(Search)is_match() / find()检查是否存在匹配 / 查找匹配位置
验证(Validate)is_match() + ^ $验证整个字符串是否符合模式
替换(Replace)replace() / replace_all()替换匹配的文本
提取(Extract)captures() / captures_iter()提取分组匹配的内容

22.4.3 驼峰命名与蛇形命名转换

在实际开发中,不同编程语言有不同的命名规范。Rust 使用蛇形命名(snake_case),而 Java/C# 使用驼峰命名(camelCase),数据库字段也常用蛇形命名。下面通过正则表达式实现命名风格的转换。

 echo "camelToSnakeName" | sed 's/\([a-z0-9]\)\([A-Z]\)/\1_\2/g' | tr '[:lower:]' '[:upper:]'
// Cargo.toml dependencies:
// lazy_static = "1"
// regex = "1"

use lazy_static::lazy_static;
use regex::Captures;
use regex::Regex;

lazy_static! {
    // 驼峰命名转蛇形:第一遍处理 "任意字符 + 大写字母开头单词" 边界
    // 如 "FalconHeavy" -> "Falcon_Heavy"
    static ref CAMEL_TO_SNAKE1: Regex = Regex::new(r"(.)([A-Z][a-z]+)").unwrap();
    // 第二遍处理 "小写/数字 + 大写" 边界,如 "HeavyRocket" -> "Heavy_Rocket"
    static ref CAMEL_TO_SNAKE2: Regex = Regex::new(r"([a-z0-9])([A-Z])").unwrap();
}

/// 驼峰命名转为蛇形命名(全大写)
/// 例:"FalconHeavyRocket" -> "FALCON_HEAVY_ROCKET"
pub fn camel_to_snake(origin: &str) -> String {
    let result0 = CAMEL_TO_SNAKE1.replace_all(origin, |caps: &Captures| {
        format!("{}_{}", &caps[1], &caps[2])
    });
    let result = CAMEL_TO_SNAKE2.replace_all(&result0, |caps: &Captures| {
        format!("{}_{}", &caps[1], &caps[2])
    });
    result.to_uppercase()
}

/// 蛇形命名转为帕斯卡命名法(每个单词首字母大写,无分隔符)
/// 例:"falcon_heavy_rocket" -> "FalconHeavyRocket"
pub fn snake_to_pascal(origin: &str) -> String {
    origin
        .split('_')
        .map(|word| {
            let mut chars = word.chars();
            match chars.next() {
                Some(ch) => {
                    let mut s = ch.to_uppercase().collect::<String>();
                    s.push_str(chars.as_str());
                    s
                }
                None => String::new(),
            }
        })
        .collect::<String>()
}

/// 蛇形命名转驼峰命名(帕斯卡命名法首字母小写)
/// 例:"falcon_heavy_rocket" -> "falconHeavyRocket"
pub fn snake_to_camel(s: &str) -> String {
    let result = snake_to_pascal(s);
    let mut chars = result.chars();
    match chars.next() {
        Some(ch) => {
            let mut s = ch.to_lowercase().collect::<String>();
            s.push_str(chars.as_str());
            s
        }
        None => String::new(),
    }
}

fn main() {
    // 测试驼峰 -> 蛇形
    let fields_vec = vec!["FalconHeavyRocket", "HTTPResponseCodeXYZ"];
    for &field in &fields_vec {
        let result = camel_to_snake(field);
        println!("camel_to_snake({}) = {}", field, result);
    }

    println!("");

    // 测试蛇形 -> 帕斯卡
    let columns_vec = vec!["falcon_heavy_rocket", "http_response_code_xyz"];
    for &col in &columns_vec {
        let result = snake_to_pascal(col);
        println!("snake_to_pascal({}) = {}", col, result);
    }

    println!("");

    // 测试蛇形 -> 驼峰
    for &col in &columns_vec {
        let result = snake_to_camel(col);
        println!("snake_to_camel({}) = {}", col, result);
    }
}

更多正则表达式示例代码


22.5 经典查询算法

查询算法是计算机科学中最基本的算法类别之一。根据数据是否有序、数据结构的不同,查询算法的选择和效率有很大差异。本节介绍几种经典的查找算法。

22.5.1 线性查找

线性查找(Linear Search)是最简单的查找算法:从数据结构的第一个元素开始,逐个与目标值进行比较,直到找到目标或遍历完所有元素。

时间复杂度: $O(n)$,其中 $n$ 为数据规模。

适用场景: 无序数据、小规模数据、链表等不支持随机访问的数据结构。

/// 线性查找:返回目标值的索引,未找到返回 None
pub fn linear_search<T: PartialEq>(arr: &[T], target: &T) -> Option<usize> {
    for (i, item) in arr.iter().enumerate() {
        if item == target {
            return Some(i);
        }
    }
    None
}

fn main() {
    let arr = vec![5, 3, 8, 1, 9, 2];
    if let Some(idx) = linear_search(&arr, &8) {
        println!("找到 8,索引为 {}", idx);
    }
}

22.5.2 二分查找

二分查找(Binary Search)是一种高效的查找算法,前提是数据必须是有序的。其核心思想是:每次将查找区间缩小一半,通过比较中间元素与目标值的大小关系来决定查找方向。

算法步骤:

  1. 设有序数组为 $A[0 \ldots n-1]$,目标值为 $T$
  2. 初始化左边界 $left = 0$,右边界 $right = n - 1$
  3. 计算中间位置 $pivot = left + \lfloor(right - left) / 2\rfloor$
  4. 若 $A[pivot] = T$,找到目标,返回 $pivot$
  5. 若 $T < A[pivot]$,则 $right = pivot - 1$(目标在左半部分)
  6. 若 $T > A[pivot]$,则 $left = pivot + 1$(目标在右半部分)
  7. 重复步骤 3-6,直到 $left > right$

时间复杂度分析:

每次查找将区间缩小一半,因此最多需要比较的次数为:

$$ k = \lfloor \log_2 n \rfloor + 1 $$

即时间复杂度为 $O(\log n)$。对于 $n = 10^6$ 的有序数组,最多只需约 20 次比较即可找到目标。

/// 力扣(704. 二分查找) https://leetcode-cn.com/problems/binary-search/
pub fn search(nums: Vec<i32>, target: i32) -> i32 {
    // target在[left,right]中查找
    let len = nums.len();
    let mut left = 0;
    let mut right = len - 1;
    let mut pivot;
    while left <= right {
        pivot = left + (right - left) / 2;
        // 注意usize的范围和nums的下标范围
        if nums[pivot] == target {
            return pivot as i32;
        }
        if target < nums[pivot] {
            if pivot == 0 {
                break;
            }
            right = pivot - 1;
        } else {
            if pivot == len - 1 {
                break;
            }
            left = pivot + 1;
        }
    }
    -1
}

fn main() {
    let nums = vec![-1, 0, 3, 5, 9, 12];
    let target = 9;
    let result = search(nums, target);
    println!("result: {}", result);
}

22.5.3 哈希查找

哈希查找通过哈希函数将键映射到数组中的位置,从而实现 $O(1)$ 平均时间复杂度的查找。

核心思想:

  1. 使用哈希函数 $h(key)$ 计算键的存储位置
  2. 直接通过索引访问,无需逐个比较
  3. 处理哈希冲突(链地址法、开放寻址法等)

Rust 标准库中的 HashMapHashSet 就是基于哈希查找实现的。

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert("apple", 3);
    map.insert("banana", 5);
    map.insert("cherry", 2);

    // 哈希查找:O(1) 平均时间复杂度
    if let Some(&count) = map.get("banana") {
        println!("banana 的数量: {}", count);
    }
}

22.5.4 插值查找

插值查找(Interpolation Search)是二分查找的改进版本,适用于均匀分布的有序数据。与二分查找总是取中间位置不同,插值查找根据目标值在区间中的相对位置来估算其可能的位置:

$$ pivot = left + \frac{(target - A[left]) \times (right - left)}{A[right] - A[left]} $$

时间复杂度: 平均 $O(\log \log n)$(均匀分布时),最坏 $O(n)$。

插值查找在电话簿查号、字典查词等场景中非常高效,因为这些数据通常是近似均匀分布的。

22.5.5 各查找算法对比

算法时间复杂度空间复杂度是否需要有序适用场景
线性查找$O(n)$$O(1)$无序数据、小数据量
二分查找$O(\log n)$$O(1)$静态有序数据
哈希查找$O(1)$ 平均$O(n)$键值对查找
插值查找$O(\log \log n)$ 平均$O(1)$均匀分布的有序数据

22.6 树结构查找

树结构是一种重要的非线性数据结构,在查找场景中有着广泛的应用。本节介绍基于树的查找方法。

22.6.1 二叉搜索树(BST)

二叉搜索树(Binary Search Tree,BST)是一种特殊的二叉树,满足以下性质:

  • 左子树中所有节点的值均小于根节点的值
  • 右子树中所有节点的值均大于根节点的值
  • 左右子树也分别是二叉搜索树
       8
      / \
     3   10
    / \    \
   1   6    14
      / \   /
     4   7 13

查找过程:

  1. 从根节点开始
  2. 若目标值等于当前节点值,查找成功
  3. 若目标值小于当前节点值,递归查找左子树
  4. 若目标值大于当前节点值,递归查找右子树
  5. 若到达空节点,查找失败

时间复杂度:

  • 平均情况(平衡树):$O(\log n)$
  • 最坏情况(退化为链表):$O(n)$

插入与删除:

  • 插入:按照查找路径走到空位置,将新节点插入
  • 删除:分三种情况——叶子节点直接删除;只有一个子节点则用子节点替代;有两个子节点则用中序后继(或前驱)替代
// Cargo.toml dependencies(Playground 运行时需添加):
// 无需第三方 crate,纯标准库

//! 二叉树
//! https://leetcode-cn.com/tag/binary-tree/problemset/

use std::cell::RefCell;
use std::cmp::max;
use std::rc::Rc;

#[derive(Debug, PartialEq, Eq)]
pub struct TreeNode {
    pub val: i32,
    pub left: Option<Rc<RefCell<TreeNode>>>,
    pub right: Option<Rc<RefCell<TreeNode>>>,
}

impl TreeNode {
    #[inline]
    pub fn new(val: i32) -> Self {
        TreeNode {
            val,
            left: None,
            right: None,
        }
    }

    /// 树的深度:也称为树的高度,树中所有结点的层次最大值称为树的深度
    pub fn get_height(root: &Option<Rc<RefCell<TreeNode>>>) -> i32 {
        fn dfs(root: &Option<Rc<RefCell<TreeNode>>>) -> i32 {
            match root {
                None => 0,
                Some(node) => {
                    let node = node.borrow();
                    1 + max(dfs(&node.left), dfs(&node.right))
                }
            }
        }
        dfs(root)
    }
}

/// 从层序遍历数组构建二叉树(i32::MIN 表示空节点)
/// 标准层序: 对于下标 i,左子节点 = 2*i+1,右子节点 = 2*i+2
fn build_tree_from_level_order(arr: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {
    if arr.is_empty() || arr[0] == i32::MIN {
        return None;
    }

    let root = Rc::new(RefCell::new(TreeNode::new(arr[0])));
    let mut queue: Vec<Rc<RefCell<TreeNode>>> = vec![Rc::clone(&root)];
    let mut i = 1;

    while i < arr.len() {
        let current = queue.remove(0);

        // 左子节点
        if i < arr.len() && arr[i] != i32::MIN {
            let left_child = Rc::new(RefCell::new(TreeNode::new(arr[i])));
            current.borrow_mut().left = Some(Rc::clone(&left_child));
            queue.push(left_child);
        }
        i += 1;

        // 右子节点
        if i < arr.len() && arr[i] != i32::MIN {
            let right_child = Rc::new(RefCell::new(TreeNode::new(arr[i])));
            current.borrow_mut().right = Some(Rc::clone(&right_child));
            queue.push(right_child);
        }
        i += 1;
    }

    Some(root)
}

/// 230. 二叉搜索树中第K小的元素 https://leetcode.cn/problems/kth-smallest-element-in-a-bst/
/// 中序遍历 BST,第 k 个元素即为第 k 小
pub fn kth_smallest(root: Option<Rc<RefCell<TreeNode>>>, k: i32) -> i32 {
    fn inorder(node: &Option<Rc<RefCell<TreeNode>>>, result: &mut Vec<i32>) {
        if let Some(n) = node {
            let n = n.borrow();
            inorder(&n.left, result);
            result.push(n.val);
            inorder(&n.right, result);
        }
    }

    let mut result = vec![];
    inorder(&root, &mut result);

    // 中序遍历结果打印(辅助观察)
    println!("中序遍历结果: {:?}", result);

    result[(k - 1) as usize]
}

fn main() {
    // 构建二叉搜索树:
    //       3
    //      / \
    //     1   4
    //      \
    //       2
    // 中序遍历: [1, 2, 3, 4]
    // 第 1 小 = 1
    let node2 = Rc::new(RefCell::new(TreeNode::new(2)));
    let node1 = Rc::new(RefCell::new(TreeNode {
        val: 1,
        left: None,
        right: Some(Rc::clone(&node2)),
    }));
    let node4 = Rc::new(RefCell::new(TreeNode::new(4)));
    let node3 = Rc::new(RefCell::new(TreeNode {
        val: 3,
        left: Some(Rc::clone(&node1)),
        right: Some(Rc::clone(&node4)),
    }));
    let root = Some(node3);

    let k = 1;
    let result = kth_smallest(root, k);
    println!("第 {} 小的元素: {}", k, result);

    // 也可以使用辅助函数从层序数组构建(i32::MIN = 空节点)
    let arr = vec![3, 1, 4, i32::MIN, 2];
    let root2 = build_tree_from_level_order(&arr);
    println!("\n树的高度: {}", TreeNode::get_height(&root2));
}

22.6.2 AVL 树

AVL 树(Adelson-Velsky and Landis Tree)是一种自平衡二叉搜索树。它通过引入平衡因子(Balance Factor)来维持树的平衡:

$$ \text{BalanceFactor}(node) = \text{height}(left_subtree) - \text{height}(right_subtree) $$

AVL 树要求任意节点的平衡因子绝对值不超过 1(即 $|\text{BF}| \leq 1$)。当插入或删除操作导致平衡因子超出范围时,通过旋转操作恢复平衡:

  • LL 旋转(右旋):左子树的左子树过深
  • RR 旋转(左旋):右子树的右子树过深
  • LR 旋转(先左旋后右旋):左子树的右子树过深
  • RL 旋转(先右旋后左旋):右子树的左子树过深

时间复杂度: 查找、插入、删除均为 $O(\log n)$。

22.6.3 红黑树

红黑树(Red-Black Tree)是另一种自平衡二叉搜索树,通过为节点着色(红色或黑色)并遵循特定规则来保持平衡。Rust 标准库中的 BTreeMapBTreeSet 就是基于红黑树实现的。

红黑树的五条性质:

  1. 每个节点是红色或黑色
  2. 根节点是黑色
  3. 叶子节点(NIL 节点)是黑色
  4. 红色节点的两个子节点都是黑色(不能有连续的红色节点)
  5. 从任意节点到其所有叶子节点的路径上,黑色节点数量相同

时间复杂度: 查找、插入、删除均为 $O(\log n)$。与 AVL 树相比,红黑树在插入和删除时需要的旋转操作更少,适合频繁修改的场景。

22.6.4 B 树与 B+ 树

B 树(B-tree)和 B+ 树是多路平衡查找树,广泛应用于数据库索引和文件系统。

B 树的特点:

  • 每个节点最多有 $m$ 个子节点($m$ 阶 B 树)
  • 除根节点和叶子节点外,每个节点至少有 $\lceil m/2 \rceil$ 个子节点
  • 所有叶子节点在同一层
  • 节点中的键值按序排列

B+ 树与 B 树的区别:

特性B 树B+ 树
数据存储所有节点都存储数据数据只存储在叶子节点
叶子节点链接叶子节点通过链表连接
范围查询效率较低效率高(遍历链表即可)
单次查询可能提前终止必须到达叶子节点

B+ 树是数据库索引的主流数据结构,MySQL 的 InnoDB 引擎就使用 B+ 树作为索引结构。


22.7 数据库查询

数据库查询是实际开发中最常见的查询场景之一。理解数据库查询的原理有助于编写高效的查询语句。

22.7.1 SQL 查询基础

SQL(Structured Query Language)是关系型数据库的标准查询语言。以下是常用的查询操作:

-- 基本查询
SELECT name, age FROM users WHERE age > 18;

-- 聚合查询
SELECT department, COUNT(*) as count, AVG(salary) as avg_salary
FROM employees
GROUP BY department
HAVING COUNT(*) > 5
ORDER BY avg_salary DESC;

-- 多表连接查询
SELECT u.name, o.product, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.amount > 100;

-- 子查询
SELECT name FROM users
WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000);

22.7.2 索引原理

数据库索引的本质是空间换时间。通过预先建立的数据结构(通常是 B+ 树),避免全表扫描,从而加速查询。

B+ 树索引的查找过程:

  1. 从根节点出发,根据比较结果选择合适的子节点
  2. 逐层向下查找,直到到达叶子节点
  3. 在叶子节点中找到目标记录或确认不存在

对于一棵 $m$ 阶 B+ 树,树的高度为 $h$,则查找的时间复杂度为 $O(h)$。一棵高度为 3 的 B+ 树,在 $m = 1000$ 时可以存储约 $10^9$ 条记录,即三次磁盘 I/O 即可定位到目标数据

索引的设计原则:

  • 选择性高的列(唯一值多的列)适合建索引
  • 频繁作为 WHERE 条件的列适合建索引
  • 联合索引遵循最左前缀匹配原则
  • 避免在索引列上使用函数或隐式类型转换

22.7.3 Rust 数据库查询

在 Rust 生态中,常用的数据库 ORM/查询框架包括:

框架类型特点
Diesel同步 ORM类型安全、编译时检查、性能优秀
SeaORM异步 ORM基于 SeaQuery,支持异步、动态查询
SQLx异步 SQL 工具包编译时 SQL 检查、原生 SQL、轻量级
rbatis动态 ORM类似 MyBatis,支持动态 SQL

以下是使用 Diesel 的简单示例:

#![allow(unused)]
fn main() {
// Cargo.toml
// [dependencies]
// diesel = { version = "2.0", features = ["postgres"] }

use diesel::prelude::*;

// 查询所有年龄大于 18 的用户
fn find_adults(conn: &mut PgConnection) -> Vec<User> {
    users::table
        .filter(users::age.gt(18))
        .load(conn)
        .expect("Error loading users")
}
}

22.8 搜索引擎简介

搜索引擎是查询技术的集大成者,需要在海量数据中快速、准确地找到用户需要的信息。

22.8.1 倒排索引

倒排索引(Inverted Index)是搜索引擎的核心数据结构。与传统的“文档 -> 单词“的正向索引不同,倒排索引建立的是“单词 -> 文档“的映射。

构建过程:

  1. 对每个文档进行分词(Tokenization)
  2. 对每个词项(Term)建立倒排列表(Posting List),记录包含该词项的文档 ID 及位置信息
文档1: "Rust 是一门系统编程语言"
文档2: "Rust 注重安全性和并发"
文档3: "Go 也是一门系统编程语言"

倒排索引:
  "Rust"     -> [文档1, 文档2]
  "是"       -> [文档1, 文档3]
  "一门"     -> [文档1, 文档3]
  "系统"     -> [文档1, 文档3]
  "编程"     -> [文档1, 文档3]
  "语言"     -> [文档1, 文档3]
  "注重"     -> [文档2]
  "安全性"   -> [文档2]
  "和"       -> [文档2]
  "并发"     -> [文档2]
  "Go"       -> [文档3]
  "也"       -> [文档3]

查询过程:

当用户搜索 “Rust 编程” 时:

  1. 对查询进行分词:[“Rust”, “编程”]
  2. 查找倒排列表:"Rust" -> [文档1, 文档2]"编程" -> [文档1, 文档3]
  3. 求交集:[文档1]
  4. 返回结果:文档1

22.8.2 PageRank

PageRank 是 Google 创始人 Larry Page 提出的网页排名算法。它将互联网看作一个有向图,网页是节点,超链接是边。一个网页的 PageRank 值取决于指向它的其他网页的 PageRank 值和链接数量。

$$ PR(A) = (1 - d) + d \sum_{i=1}^{n} \frac{PR(T_i)}{C(T_i)} $$

其中:

  • $PR(A)$ 是页面 A 的 PageRank 值
  • $d$ 是阻尼系数(通常取 0.85),表示用户随机点击链接的概率
  • $T_i$ 是指向页面 A 的页面
  • $C(T_i)$ 是页面 $T_i$ 的出链数量
  • $n$ 是指向页面 A 的页面总数

22.8.3 全文检索

全文检索(Full-Text Search)是指在大规模文本数据中,根据用户输入的关键词快速找到相关文档的技术。主流的全文检索引擎包括:

引擎语言特点
ElasticsearchJava分布式、实时搜索、生态丰富
Apache LuceneJava底层搜索引擎库
SolrJava基于 Lucene、企业级搜索平台
MeilisearchRust轻量、快速、易部署
TantivyRust纯 Rust 实现、类 Lucene、高性能

22.8.4 Rust 的 Tantivy 全文搜索引擎

Tantivy 是一个用 Rust 编写的全文搜索引擎库,其设计灵感来自 Apache Lucene,但追求更高的性能和更低的资源消耗。

Tantivy 的核心特性:

  • 纯 Rust 实现,无 unsafe 代码
  • 支持全文检索、分词、BM25 相关性评分
  • 支持增量索引和近实时搜索
  • 压缩率高,内存占用低
  • 支持多种分词器(中文分词需配合 jieba-rs 等)
// Cargo.toml
// [dependencies]
// tantivy = "0.22"

use tantivy::schema::*;
use tantivy::{Index, IndexWriter, doc};

fn main() -> tantivy::Result<()> {
    // 1. 定义 schema
    let mut schema_builder = Schema::builder();
    let title = schema_builder.add_text_field("title", TEXT | STORED);
    let body = schema_builder.add_text_field("body", TEXT);
    let schema = schema_builder.build();

    // 2. 创建索引
    let index = Index::create_in_ram(schema.clone());
    let mut writer = index.writer(50_000_000)?;

    // 3. 添加文档
    writer.add_document(doc!(
        title => "Rust 编程语言",
        body => "Rust 是一门注重安全性、并发性和性能的系统编程语言"
    ));
    writer.add_document(doc!(
        title => "Go 编程语言",
        body => "Go 是一门注重简洁性和并发性的编程语言"
    ));
    writer.commit()?;

    // 4. 搜索
    let reader = index.reader()?;
    let searcher = reader.searcher();
    let query_parser = QueryParser::for_index(&index, vec![title, body]);
    let query = query_parser.parse_query("Rust 安全")?;

    let top_docs = searcher.search(&query, &TopDocs::with_limit(10))?;
    for (_score, doc_address) in top_docs {
        let retrieved_doc = searcher.doc(doc_address)?;
        println!("匹配文档: {}", schema.to_json(&retrieved_doc));
    }

    Ok(())
}

22.9 总结

本章系统介绍了查询领域的核心算法与数据结构,从字符串模式匹配到搜索引擎,涵盖了查询技术的各个层面。下面是各查询算法的综合对比:

各查询算法综合对比

算法/数据结构时间复杂度空间复杂度数据要求主要应用场景
暴力匹配$O(m \times n)$$O(1)$简单短串匹配
KMP$O(m + n)$$O(m)$流式文本匹配
Boyer-Moore$O(n/m)$ 平均$O(\sigma)$文本编辑器查找
Rabin-Karp$O(m + n)$ 平均$O(1)$多模式匹配
DFA 敏感词检测$O(n)$$O(k)$敏感词库内容安全过滤
正则表达式取决于模式取决于模式文本验证与提取
线性查找$O(n)$$O(1)$无序小数据
二分查找$O(\log n)$$O(1)$有序数组静态有序数据
哈希查找$O(1)$ 平均$O(n)$键值对查找
BST$O(\log n)$ 平均$O(n)$动态有序集合
B+ 树$O(\log n)$$O(n)$数据库索引
倒排索引$O(k)$$O(n)$文档集合搜索引擎

$m$ 为模式串长度,$n$ 为文本/数据规模,$k$ 为结果数量,$\sigma$ 为字符集大小。

场景选择建议

场景推荐算法/工具理由
简单子串查找暴力匹配 / 标准库 str::find()实现简单,标准库已优化
大文本多次匹配KMP / Boyer-Moore避免回溯,性能稳定
敏感词过滤DFA(Trie 树)$O(n)$ 线性扫描,支持大规模词库
文本格式验证正则表达式表达力强,覆盖面广
有序数组查找二分查找$O(\log n)$,效率极高
键值对查找哈希表(HashMap)$O(1)$ 平均,最常用
动态有序数据BTreeMap / 红黑树自动排序,$O(\log n)$ 操作
数据库查询B+ 树索引 + SQL成熟的索引技术
全文检索Tantivy / Elasticsearch倒排索引,相关性排序

22.10 练习题

练习 1: 实现暴力字符串匹配算法,找出文本串中所有模式串出现的位置,并分析其在最好和最坏情况下的时间复杂度。

练习 2: 手动计算模式串 "ABABABC" 的 KMP next 数组(部分匹配表),并画出匹配过程示意图。

练习 3: 使用 Rust 的 regex crate 实现一个简单的邮箱地址验证器,要求支持常见邮箱格式(如 user@example.com)。

练习 4: 给定一个有序整数数组和目标值,使用二分查找实现“查找第一个大于等于目标值的位置“(即 lower_bound)。

练习 5: 实现一个简单的 Trie 树(前缀树),支持插入、查找和前缀搜索(判断是否存在以某前缀开头的单词)。

练习 6: 使用 Tantivy 构建一个包含 100 篇文档的全文索引,实现关键词搜索并按相关性排序返回前 10 条结果。

练习 7: 对比分析 KMP 算法和 Boyer-Moore 算法在以下两种场景中的性能差异:(1)文本串和模式串都是英文小写字母;(2)模式串中包含大量重复子串。

练习 8: 设计一个基于 DFA 的 URL 敏感词检测系统,要求:(1)支持从文件加载敏感词库;(2)支持最小匹配和最大匹配两种模式;(3)支持敏感词替换功能。提示:可参考本章 22.3 节的 DFA 实现,将字符集从中文扩展到 URL 字符。

第二十三 排序

排序(Sorting)是计算机科学中最基础、最重要的算法问题之一。它的目标是将一组数据按照某种特定的顺序重新排列。无论是数据库查询、搜索引擎排序,还是日常的数据处理,排序都无处不在。Rust 作为一门系统级编程语言,对排序提供了高效的标准库支持,同时也允许我们深入底层,亲手实现各种经典算法。


一、排序的基本概念

1.1 什么是排序

排序是将一组无序的数据元素按照关键字的大小重新排列,使其满足递增或递减顺序的过程。例如,将 [3, 1, 4, 1, 5] 排列为 [1, 1, 3, 4, 5]

1.2 稳定性

稳定性是排序算法的一个重要属性。如果排序算法在排序后能够保持相等元素的原始相对顺序,则称该算法是稳定的;反之则是不稳定的。

例如,对 [("Alice", 85), ("Bob", 90), ("Carol", 85)] 按分数排序,稳定排序会保持 Alice 在 Carol 之前;而不稳定排序可能改变它们的相对位置。

1.3 时间复杂度与空间复杂度

复杂度类型含义常见级别
时间复杂度算法执行所需时间随数据规模增长的趋势O(1)、O(log n)、O(n)、O(n log n)、O(n²)
空间复杂度算法执行所需额外内存空间O(1)(原地排序)、O(n)、O(log n)

原地排序(In-place) 指空间复杂度为 O(1) 的排序算法,不需要额外的存储空间。


二、简单排序

2.1 冒泡排序

冒泡排序(Bubble Sort)通过重复遍历数组,比较相邻元素并交换位置,使较大的元素逐渐“冒泡“到数组末端。

#![allow(unused)]
fn main() {
fn bubble_sort(arr: &mut [i32]) {
    let n = arr.len();
    for i in 0..n {
        let mut swapped = false;
        for j in 0..n - i - 1 {
            if arr[j] > arr[j + 1] {
                arr.swap(j, j + 1);
                swapped = true;
            }
        }
        if !swapped {
            break; // 已排序,提前退出
        }
    }
}
}
指标
平均时间复杂度O(n²)
最坏时间复杂度O(n²)
空间复杂度O(1)
稳定性稳定

冒泡排序思路简单,但效率较低,适合教学理解和极小数据集。

2.2 选择排序

选择排序(Selection Sort)每次从未排序部分选出最小(或最大)元素,放到已排序部分的末尾。

#![allow(unused)]
fn main() {
fn selection_sort(arr: &mut [i32]) {
    let n = arr.len();
    for i in 0..n {
        let mut min_idx = i;
        for j in i + 1..n {
            if arr[j] < arr[min_idx] {
                min_idx = j;
            }
        }
        arr.swap(i, min_idx);
    }
}
}
指标
平均时间复杂度O(n²)
最坏时间复杂度O(n²)
空间复杂度O(1)
稳定性不稳定

选择排序的交换次数最少(最多 n-1 次),但不稳定。

2.3 插入排序

插入排序(Insertion Sort)将数组分为已排序和未排序两部分,逐个将未排序元素插入到已排序部分的正确位置。

#![allow(unused)]
fn main() {
fn insertion_sort(arr: &mut [i32]) {
    for i in 1..arr.len() {
        let key = arr[i];
        let mut j = i;
        while j > 0 && arr[j - 1] > key {
            arr[j] = arr[j - 1];
            j -= 1;
        }
        arr[j] = key;
    }
}
}
指标
平均时间复杂度O(n²)
最坏时间复杂度O(n²)
最好时间复杂度O(n)(已有序)
空间复杂度O(1)
稳定性稳定

插入排序对小规模或基本有序的数据非常高效,是归并排序和快速排序的常用优化手段。


三、高效排序

3.1 快速排序

快速排序(Quick Sort)采用分治策略,选择一个基准元素(pivot),将数组划分为小于基准和大于基准的两部分,然后递归排序。

#![allow(unused)]
fn main() {
fn quick_sort(arr: &mut [i32]) {
    if arr.len() <= 1 {
        return;
    }
    let pivot_index = partition(arr);
    let (left, right) = arr.split_at_mut(pivot_index);
    quick_sort(left);
    quick_sort(&mut right[1..]);
}

fn partition(arr: &mut [i32]) -> usize {
    let len = arr.len();
    let pivot = arr[len - 1];
    let mut i = 0;
    for j in 0..len - 1 {
        if arr[j] <= pivot {
            arr.swap(i, j);
            i += 1;
        }
    }
    arr.swap(i, len - 1);
    i
}
}
指标
平均时间复杂度O(n log n)
最坏时间复杂度O(n²)(罕见,可通过随机化避免)
空间复杂度O(log n)(递归栈)
稳定性不稳定

快速排序是实际应用中平均性能最好的排序算法之一。Rust 标准库的 sort_unstable 就是基于快速排序的优化版本(内省排序)。

3.2 归并排序

归并排序(Merge Sort)同样采用分治策略,将数组不断二分,直到子数组长度为 1,然后逐层合并两个有序子数组。

#![allow(unused)]
fn main() {
fn merge_sort(arr: &mut [i32]) {
    let len = arr.len();
    if len <= 1 {
        return;
    }
    let mid = len / 2;
    merge_sort(&mut arr[..mid]);
    merge_sort(&mut arr[mid..]);

    let mut temp = arr.to_vec();
    merge(&arr[..mid], &arr[mid..], &mut temp);
    arr.copy_from_slice(&temp);
}

fn merge(left: &[i32], right: &[i32], temp: &mut [i32]) {
    let (mut i, mut j, mut k) = (0, 0, 0);
    while i < left.len() && j < right.len() {
        if left[i] <= right[j] {
            temp[k] = left[i];
            i += 1;
        } else {
            temp[k] = right[j];
            j += 1;
        }
        k += 1;
    }
    while i < left.len() {
        temp[k] = left[i];
        i += 1;
        k += 1;
    }
    while j < right.len() {
        temp[k] = right[j];
        j += 1;
        k += 1;
    }
}
}
指标
平均时间复杂度O(n log n)
最坏时间复杂度O(n log n)
空间复杂度O(n)
稳定性稳定

归并排序保证 O(n log n) 的最坏时间复杂度,且稳定,但需要额外 O(n) 空间。它是 Rust 标准库 sort(稳定排序)的核心基础。

3.3 堆排序

堆排序(Heap Sort)利用二叉堆的数据结构。首先构建最大堆,然后将堆顶(最大值)与末尾交换,再对剩余元素重新调整堆,重复此过程。

#![allow(unused)]
fn main() {
fn heap_sort(arr: &mut [i32]) {
    let n = arr.len();
    // 构建最大堆
    for i in (0..n / 2).rev() {
        heapify(arr, n, i);
    }
    // 逐个提取最大值
    for i in (1..n).rev() {
        arr.swap(0, i);
        heapify(arr, i, 0);
    }
}

fn heapify(arr: &mut [i32], n: usize, i: usize) {
    let mut largest = i;
    let left = 2 * i + 1;
    let right = 2 * i + 2;

    if left < n && arr[left] > arr[largest] {
        largest = left;
    }
    if right < n && arr[right] > arr[largest] {
        largest = right;
    }
    if largest != i {
        arr.swap(i, largest);
        heapify(arr, n, largest);
    }
}
}
指标
平均时间复杂度O(n log n)
最坏时间复杂度O(n log n)
空间复杂度O(1)
稳定性不稳定

堆排序是原地排序且最坏时间复杂度为 O(n log n),非常适合内存受限的场景。


四、非比较排序

非比较排序不通过元素间的直接比较来确定顺序,而是利用数据的特定特征(如数值范围),在特定条件下可以达到 O(n) 的时间复杂度。

4.1 计数排序

计数排序(Counting Sort)适用于数据范围较小且为整数的场景。它统计每个值出现的次数,然后按顺序输出。

#![allow(unused)]
fn main() {
fn counting_sort(arr: &mut [i32]) {
    if arr.is_empty() {
        return;
    }
    let max = *arr.iter().max().unwrap();
    let min = *arr.iter().min().unwrap();
    let range = (max - min + 1) as usize;

    let mut count = vec![0; range];
    for &num in arr.iter() {
        count[(num - min) as usize] += 1;
    }

    let mut idx = 0;
    for (i, &c) in count.iter().enumerate() {
        for _ in 0..c {
            arr[idx] = i as i32 + min;
            idx += 1;
        }
    }
}
}
指标
时间复杂度O(n + k),k 为数据范围
空间复杂度O(k)
稳定性稳定

4.2 桶排序

桶排序(Bucket Sort)将数据分到若干个有序的桶中,每个桶内再分别排序(通常用插入排序),最后按顺序合并。

#![allow(unused)]
fn main() {
fn bucket_sort(arr: &mut [f64]) {
    let n = arr.len();
    if n <= 1 {
        return;
    }

    let mut buckets: Vec<Vec<f64>> = vec![vec![]; n];
    for &num in arr.iter() {
        let idx = (num * n as f64) as usize;
        let idx = idx.min(n - 1);
        buckets[idx].push(num);
    }

    for bucket in buckets.iter_mut() {
        bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());
    }

    let mut idx = 0;
    for bucket in buckets.iter() {
        for &num in bucket.iter() {
            arr[idx] = num;
            idx += 1;
        }
    }
}
}

桶排序在数据均匀分布时效率极高,接近 O(n)。

4.3 基数排序

基数排序(Radix Sort)按位数从低到高(或从高到低)依次排序,每一位的排序使用稳定的计数排序。

#![allow(unused)]
fn main() {
fn radix_sort(arr: &mut [i32]) {
    if arr.is_empty() {
        return;
    }
    let max = *arr.iter().max().unwrap();
    let mut exp = 1;
    while max / exp > 0 {
        counting_sort_by_digit(arr, exp);
        exp *= 10;
    }
}

fn counting_sort_by_digit(arr: &mut [i32], exp: i32) {
    let n = arr.len();
    let mut output = vec![0; n];
    let mut count = [0; 10];

    for &num in arr.iter() {
        let digit = ((num / exp) % 10) as usize;
        count[digit] += 1;
    }
    for i in 1..10 {
        count[i] += count[i - 1];
    }
    for i in (0..n).rev() {
        let digit = ((arr[i] / exp) % 10) as usize;
        count[digit] -= 1;
        output[count[digit]] = arr[i];
    }
    arr.copy_from_slice(&output);
}
}

基数排序的时间复杂度为 O(d × (n + k)),其中 d 是最大位数,k 是基数(通常为 10)。


五、Rust 标准库排序

Rust 标准库为切片类型提供了两种高效的排序方法。

5.1 slice::sort — 稳定排序

#![allow(unused)]
fn main() {
let mut v = [3, 1, 4, 1, 5, 9, 2, 6];
v.sort();
println!("{:?}", v); // [1, 1, 2, 3, 4, 5, 6, 9]
}

sort 是稳定排序,基于 Timsort 算法(归并排序的优化变体),最坏时间复杂度为 O(n log n)。

5.2 slice::sort_unstable — 非稳定排序

#![allow(unused)]
fn main() {
let mut v = [3, 1, 4, 1, 5, 9, 2, 6];
v.sort_unstable();
println!("{:?}", v);
}

sort_unstable 基于模式 defeating 快速排序(Pattern-defeating Quicksort,pdqsort),平均性能通常优于稳定排序,且不分配额外内存。

5.3 自定义排序

#![allow(unused)]
fn main() {
let mut words = ["banana", "apple", "cherry"];
words.sort_by(|a, b| a.len().cmp(&b.len()));

let mut nums = [3, 1, 4, 1, 5];
nums.sort_by_key(|&x| -x); // 降序
}

5.4 对自定义类型排序

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Student {
    name: String,
    score: u32,
}

let mut students = vec![
    Student { name: "Alice".to_string(), score: 85 },
    Student { name: "Bob".to_string(), score: 92 },
];
students.sort(); // 需要实现 Ord trait
}

六、排序算法对比

算法平均时间最坏时间空间稳定性适用场景
冒泡排序O(n²)O(n²)O(1)稳定教学、极小数据
选择排序O(n²)O(n²)O(1)不稳定交换代价高的场景
插入排序O(n²)O(n²)O(1)稳定小数据、基本有序
快速排序O(n log n)O(n²)O(log n)不稳定通用场景,平均最快
归并排序O(n log n)O(n log n)O(n)稳定需要稳定排序
堆排序O(n log n)O(n log n)O(1)不稳定内存受限
计数排序O(n + k)O(n + k)O(k)稳定整数、范围小
桶排序O(n)O(n²)O(n)稳定数据均匀分布
基数排序O(d(n + k))O(d(n + k))O(n + k)稳定整数、固定位数

七、总结与练习

本章小结

知识点要点
稳定性相等元素排序后是否保持原有顺序
原地排序空间复杂度为 O(1) 的排序
比较排序下限基于比较的排序时间复杂度下限为 O(n log n)
Rust sort稳定排序,基于 Timsort
Rust sort_unstable非稳定排序,基于 pdqsort,通常更快

练习建议

  1. 实现优化:为快速排序添加“三数取中“选基准和尾递归优化。
  2. 混合排序:实现一个排序函数,对小数组使用插入排序,对大数组使用快速排序。
  3. 链表排序:尝试对 LinkedList 实现归并排序。
  4. 性能测试:使用 std::time::Instant 对比不同排序算法在随机数据、有序数据、逆序数据上的性能。
  5. 稳定排序改造:修改快速排序的实现,使其成为稳定排序(提示:使用额外空间)。
  6. 并行排序:利用 Rayon 库的 par_sort,体验并行排序带来的性能提升。

第二十四 λ演算

函数式编程 廖雪峰

λ演算

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

优点:抽象程度高,代码简洁,代码可读性好。


24.1 概述

λ演算(Lambda Calculus)是由美国数学家 Alonzo Church 于 1930 年代提出的一种数学形式系统。它是函数式编程的理论基础,也是计算机科学中最重要的理论模型之一。

λ演算的核心思想极其简单:一切皆为函数。在λ演算中,没有变量赋值、没有循环、没有状态——只有函数的定义和应用。然而,正是这种极简的系统,被证明是图灵完备的,即它能够表达任何可计算的函数。

λ演算对现代编程语言产生了深远的影响:

  • Lisp(1958年)直接受到λ演算的启发
  • ML 家族语言(OCaml、Standard ML)将λ演算与类型系统结合
  • Haskell 是纯函数式编程语言的典范
  • Rust 吸收了函数式编程的许多特性,尤其是闭包和迭代器
  • Java 从 8 开始引入 Lambda 表达式和 Stream API
  • JavaScriptPythonC++ 等主流语言都支持 Lambda/闭包

24.2 λ演算的核心概念

24.2.1 λ表达式的语法

λ演算中只有三种基本构造:

  1. 变量(Variable):如 $x$、$y$、$z$
  2. 抽象(Abstraction):$\lambda x . M$,表示一个匿名函数,接受参数 $x$,返回表达式 $M$
  3. 应用(Application):$(M , N)$,表示将函数 $M$ 应用到参数 $N$ 上

一个λ表达式的完整语法可以定义为:

$$ E ::= x \mid \lambda x . E \mid (E_1 , E_2) $$

其中:

  • $x$ 是变量
  • $\lambda x . E$ 是函数抽象
  • $(E_1 , E_2)$ 是函数应用

24.2.2 基本示例

恒等函数(Identity Function):

$$ I = \lambda x . x $$

它将任何输入原样返回。例如:$(I , 5) = (\lambda x . x) , 5 = 5$。

常量函数(Constant Function):

$$ K = \lambda x . \lambda y . x $$

它接受两个参数,总是返回第一个参数。例如:$((K , a) , b) = a$。

应用示例

$$ (\lambda x . x + 1) , 5 = 5 + 1 = 6 $$

24.2.3 三种基本转换规则

α转换(Alpha Conversion)

α转换是指对绑定变量进行重命名,而不改变表达式的含义。

$$ \lambda x . M \equiv_\alpha \lambda y . M[x := y] $$

其中 $y$ 不在 $M$ 中自由出现。

例如:

$$ \lambda x . x + 1 \equiv_\alpha \lambda y . y + 1 $$

β归约(Beta Reduction)

β归约是λ演算中最核心的计算规则,表示函数应用:

$$ (\lambda x . M) , N \to_\beta M[x := N] $$

意思是:将函数体 $M$ 中所有自由出现的 $x$ 替换为参数 $N$。

例如:

$$ (\lambda x . x \times x) , 3 \to_\beta 3 \times 3 = 9 $$

另一个例子:

$$ (\lambda x . \lambda y . x + y) , 2 \to_\beta \lambda y . 2 + y $$

η转换(Eta Conversion)

η转换表示一个函数如果对所有输入都产生与另一个函数相同的结果,那么这两个函数相等:

$$ \lambda x . (f , x) =_\eta f \quad \text{(当 } x \text{ 不在 } f \text{ 中自由出现时)} $$

例如:

$$ \lambda x . (\sin , x) =_\eta \sin $$


24.3 λ演算与编程

24.3.1 图灵完备性

1936年,Alonzo Church 和 Alan Turing 分别独立证明了λ演算与图灵机在计算能力上是等价的。这意味着:

  • 任何图灵机可以计算的函数,λ演算也可以计算
  • 任何λ演算可以计算的函数,图灵机也可以计算
  • 这一结论被称为 Church-Turing 论题

24.3.2 Church 编码

Church 编码是一种用λ演算表示数据的方法。在纯λ演算中没有数字、布尔值等原生类型,但可以用函数来编码它们。

Church 数(Church Numerals)

Church 数将自然数 $n$ 编码为将函数 $f$ 应用 $n$ 次的高阶函数:

$$ 0 = \lambda f . \lambda x . x $$

$$ 1 = \lambda f . \lambda x . f , x $$

$$ 2 = \lambda f . \lambda x . f , (f , x) $$

$$ 3 = \lambda f . \lambda x . f , (f , (f , x)) $$

一般形式:

$$ n = \lambda f . \lambda x . f^n , x $$

其中 $f^n$ 表示将 $f$ 应用 $n$ 次。

Successor 函数(后继函数):

$$ \text{SUCC} = \lambda n . \lambda f . \lambda x . f , (n , f , x) $$

例如,计算 $\text{SUCC} , 1$:

$$ \begin{aligned} \text{SUCC} , 1 &= (\lambda n . \lambda f . \lambda x . f , (n , f , x)) , (\lambda f . \lambda x . f , x) \ &\to_\beta \lambda f . \lambda x . f , ((\lambda f . \lambda x . f , x) , f , x) \ &\to_\beta \lambda f . \lambda x . f , (f , x) \ &= 2 \end{aligned} $$

Church 布尔值(Church Booleans)

布尔值可以编码为在两个选项之间做选择的函数:

$$ \text{TRUE} = \lambda x . \lambda y . x $$

$$ \text{FALSE} = \lambda x . \lambda y . y $$

条件表达式

$$ \text{IF} = \lambda b . \lambda x . \lambda y . b , x , y $$

验证:

$$ \text{IF} , \text{TRUE} , a , b = (\lambda b . \lambda x . \lambda y . b , x , y) , (\lambda x . \lambda y . x) , a , b \to_\beta a $$

$$ \text{IF} , \text{FALSE} , a , b = (\lambda b . \lambda x . \lambda y . b , x , y) , (\lambda x . \lambda y . y) , a , b \to_\beta b $$

逻辑运算

$$ \text{AND} = \lambda p . \lambda q . p , q , p $$

$$ \text{OR} = \lambda p . \lambda q . p , p , q $$

$$ \text{NOT} = \lambda p . p , \text{FALSE} , \text{TRUE} $$

24.3.3 Y 组合子

在λ演算中,函数是匿名的,那么如何实现递归呢?Y 组合子(Y Combinator)解决了这个问题:

$$ Y = \lambda f . (\lambda x . f , (x , x)) , (\lambda x . f , (x , x)) $$

Y 组合子的关键性质是:

$$ Y , f = f , (Y , f) $$

这意味着 $Y , f$ 是 $f$ 的不动点,从而可以实现递归。

例如,定义阶乘函数的高阶版本:

$$ F = \lambda f . \lambda n . \text{IF} , (n = 0) , 1 , (n \times f , (n - 1)) $$

那么 $Y , F$ 就是真正的阶乘函数。

24.3.4 Rust 中的 λ 表达式(闭包)

Rust 通过闭包(Closure)支持λ表达式:

#![allow(unused)]
fn main() {
// 恒等函数
let identity = |x| x;

// 加一函数
let add_one = |x| x + 1;

// 应用函数:将函数 f 应用到参数 x
let apply = |f, x| f(x);

// 使用
let result = apply(add_one, 5); // result = 6
}

Rust 的闭包语法 |参数| 表达式 直接对应于λ演算的 $\lambda$ 抽象。


24.4 函数式编程范式

24.4.1 纯函数

纯函数(Pure Function)是指满足以下两个条件的函数:

  1. 无副作用:函数的执行不会修改外部状态(不修改全局变量、不执行 I/O 操作等)
  2. 引用透明:相同的输入总是产生相同的输出

数学上,纯函数就是一个从定义域到值域的映射:

$$ f: A \to B $$

Rust 示例:

#![allow(unused)]
fn main() {
// 纯函数
fn add(a: i32, b: i32) -> i32 {
    a + b
}

// 非纯函数(有副作用)
fn impure_add(a: i32, b: i32) -> i32 {
    println!("Adding..."); // 副作用:I/O 操作
    a + b
}

// 非纯函数(依赖外部状态)
static mut COUNTER: i32 = 0;

fn impure_increment(x: i32) -> i32 {
    unsafe {
        COUNTER += 1; // 副作用:修改全局状态
        x + COUNTER
    }
}
}

24.4.2 不可变性

函数式编程强调数据的不可变性(Immutability)。一旦数据被创建,就不能被修改。如果需要“修改“,则创建一个新的数据副本。

Rust 中默认变量绑定是不可变的:

#![allow(unused)]
fn main() {
let x = 5;
// x = 6; // 编译错误!

let mut y = 5;
y = 6; // 可以,因为 y 是可变的
}

函数式数据结构(如持久化数据结构)通过结构共享来高效地实现不可变性。

24.4.3 高阶函数

高阶函数(Higher-Order Function)是指接受函数作为参数或返回函数作为结果的函数。

数学表示:

$$ \text{map}: (A \to B) \to [A] \to [B] $$

$$ \text{filter}: (A \to \text{Bool}) \to [A] \to [A] $$

$$ \text{fold}: (B \to A \to B) \to B \to [A] \to B $$

Rust 示例:

#![allow(unused)]
fn main() {
fn apply_twice<F>(f: F, x: i32) -> i32
where
    F: Fn(i32) -> i32,
{
    f(f(x))
}

let result = apply_twice(|x| x + 1, 5); // result = 7
}

24.4.4 惰性求值与严格求值

严格求值(Strict Evaluation / Eager Evaluation):表达式在绑定到变量时立即求值。Rust 默认使用严格求值。

惰性求值(Lazy Evaluation):表达式只在需要时才求值。Haskell 使用惰性求值。

Rust 中可以通过迭代器和闭包模拟惰性求值:

#![allow(unused)]
fn main() {
// 惰性求值:在调用 collect() 之前,map 和 filter 不会执行
let result: Vec<i32> = (1..100)
    .map(|x| {
        println!("mapping {}", x); // 不会立即打印
        x * x
    })
    .filter(|x| {
        println!("filtering {}", x); // 不会立即打印
        *x > 10
    })
    .collect(); // 这里才会触发实际计算
}

24.4.5 函数式编程语言

语言类型系统求值策略特点
Haskell静态类型,强类型惰性求值纯函数式,类型类,Monad
Lisp动态类型严格求值宏系统,S-表达式,代码即数据
OCaml静态类型,类型推断严格求值模块系统,代数数据类型
Erlang动态类型严格求值并发模型,容错设计
Elm静态类型惰性求值前端函数式,无运行时异常
Rust静态类型,所有权系统严格求值零成本抽象,内存安全

24.5 Rust 中的闭包与迭代器

24.5.1 闭包的三种捕获方式

Rust 的闭包根据它们如何捕获环境中的变量,分为三种 trait:

Trait捕获方式调用次数示例
Fn不可变借用(&T多次|x| x + *env
FnMut可变借用(&mut T多次(需可变上下文)|x| { *env += x; }
FnOnce移动所有权(T一次|x| x + envenv 被移动)
#![allow(unused)]
fn main() {
let mut counter = 0;

// Fn:不可变借用
let fn_closure = |x: i32| x + counter;
println!("{}", fn_closure(5)); // 可以多次调用
println!("{}", fn_closure(5));

// FnMut:可变借用
let fn_mut_closure = |x: i32| {
    counter += x;
    counter
};
// fn_mut_closure(5); // 需要在可变上下文中调用

// FnOnce:移动所有权
let data = vec![1, 2, 3];
let fn_once_closure = || data; // data 被移动到闭包中
// let result = fn_once_closure(); // 只能调用一次
}

24.5.2 迭代器适配器

Rust 的迭代器是惰性的,只有在调用消耗型方法(如 collectfor_eachfold)时才会实际执行。

helpful-methods-for-closures-and-iterators

#![allow(unused)]
fn main() {
std::iter::DoubleEndedIterator

iter()
iter_mut()
into_iter()

next()
next_back()
nth()

flatten
filter() where P: FnMut(&Self::Item) -> bool  // 返回 true 时,元素将保留下来
filter_map() where P: FnMut(Self::Item) -> Option<B>
for_each()
zip()

ok()
ok_or()
ok_or_else()

any()
all()
find()
position()

rev()    // 翻转
cycle()

fold()   // "累加"操作
try_fold() // 累加,是短路的:一旦闭包返回 Try::Output 的失败变体,立即停止
take()
take_while()
skip()
skip_while()

chunks()
window()

matches_indices()

peekable()

# std::iter::Extend trait
extend()
}

24.5.3 常用迭代器模式

map / filter / reduce 模式

#![allow(unused)]
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// map + filter + fold(reduce)
let sum_of_squares_of_evens: i32 = numbers
    .iter()
    .filter(|&&x| x % 2 == 0)      // 筛选偶数
    .map(|&x| x * x)                // 平方
    .fold(0, |acc, x| acc + x);     // 求和

println!("{}", sum_of_squares_of_evens); // 220
}

函数组合

#![allow(unused)]
fn main() {
fn compose<F, G, A, B, C>(f: F, g: G) -> impl Fn(A) -> C
where
    F: Fn(B) -> C,
    G: Fn(A) -> B,
{
    move |x| f(g(x))
}

let add_one = |x: i32| x + 1;
let double = |x: i32| x * 2;
let add_one_then_double = compose(double, add_one);

println!("{}", add_one_then_double(5)); // (5 + 1) * 2 = 12
}

惰性无限序列

#![allow(unused)]
fn main() {
// 使用迭代器生成无限斐波那契数列
fn fibonacci() -> impl Iterator<Item = u64> {
    let mut a = 0;
    let mut b = 1;
    std::iter::from_fn(move || {
        let current = a;
        a = b;
        b = current + b;
        Some(current)
    })
}

let first_10: Vec<u64> = fibonacci().take(10).collect();
println!("{:?}", first_10);
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
}

24.6 从 λ 演算看 Stream/Iterator

24.6.1 Java Stream API

Java 从 8 开始引入 Stream API,受函数式编程启发,提供了一种声明式处理集合数据的方式。

创建 Stream 的四种方式

  1. 基于数组或 Collection:Arrays.stream(int[] array)collection.stream()
  2. Stream.of:由一系列值构造
  3. Stream.generate(Supplier<T> s):生成无限流
  4. Stream.iterate(seed, f):迭代生成

常用操作

  • 转换操作:map()filter()sorted()distinct()
  • 合并操作:concat()flatMap()
  • 并行处理:parallel()
  • 聚合操作:reduce()collect()count()max()min()sum()average()
  • 其他操作:allMatch()anyMatch()forEach()

Stream 转为其他类型

  • Collectors.toList()
  • Collectors.toMap()
  • stream().toArray(String[]::new)
  • Collectors.groupingBy()

24.6.2 Rust Iterator trait

Rust 的 Iterator trait 是所有迭代器的核心:

#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // ... 大量默认方法
}
}

任何实现了 Iterator trait 的类型都可以使用所有迭代器适配器方法。

24.6.3 Java Stream vs Rust Iterator 对比

特性Java StreamRust Iterator
核心接口Stream<T>Iterator<Item = T>
惰性求值中间操作惰性,终端操作触发完全惰性,消耗方法触发
并行处理parallel() 方法rayon crate 的 par_iter()
类型安全运行时泛型擦除编译时零成本泛型
空值处理可能抛出 NullPointerExceptionOption<T> 强制处理
错误处理异常Result<T, E>
map 操作stream.map(Function<T, R>)iter.map(|x| ...)
filter 操作stream.filter(Predicate<T>)iter.filter(|x| ...)
reduce 操作stream.reduce(identity, op)iter.fold(init, op)
collect 操作stream.collect(Collectors.toList())iter.collect::<Vec<_>>()

24.6.4 常见操作对比示例

map / filter / collect

// Java
List<Integer> result = numbers.stream()
    .filter(n -> n % 2 == 0)
    .map(n -> n * n)
    .collect(Collectors.toList());
#![allow(unused)]
fn main() {
// Rust
let result: Vec<i32> = numbers
    .iter()
    .filter(|&&n| n % 2 == 0)
    .map(|&n| n * n)
    .collect();
}

reduce / fold

// Java
int sum = numbers.stream()
    .reduce(0, (a, b) -> a + b);
#![allow(unused)]
fn main() {
// Rust
let sum: i32 = numbers
    .iter()
    .fold(0, |a, b| a + b);
}

flatMap / flatten

// Java
List<Integer> flat = lists.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());
#![allow(unused)]
fn main() {
// Rust
let flat: Vec<i32> = lists
    .iter()
    .flatten()
    .copied()
    .collect();
}

24.7 实战:1BRC 挑战

24.7.1 问题描述

1BRC(The One Billion Row Challenge)是一个编程挑战:处理一个包含 10 亿行气象数据的文本文件,计算每个城市的最低温度、平均温度和最高温度。

每行数据的格式为:城市名;温度值,例如:

Hamburg;12.0
Bulawayo;8.9
Palembang;38.8

24.7.2 Rust 实现

下面的实现展示了 Rust 中函数式编程思想的实际应用,包括迭代器链式调用、HashMap 的 entry API 等。

#![allow(unused)]
fn main() {
/// The One Billion Row Challenge(1BRC)
/// https://github.com/gunnarmorling/1brc/blob/main/src/main/java/dev/morling/onebrc/CalculateAverage_thomaswue.java

use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::io::prelude::*;
use std::io::BufReader;
use std::fs::File;
use std::path::Path;

fn one_billion_row_challenge() {
    // let records = String::from("");

    #[derive(Debug, Clone, Copy)]
    struct WeatherResult {
        min: f32,
        max: f32,
        sum: u64,
        count: u32,
    }

    let mut words: HashMap<String, WeatherResult> = HashMap::with_capacity(42000);
    // let mut words: HashMap<String, i32> = HashMap::with_capacity(300);
    match File::open("file/measurements.txt") {
        Ok(f) => {
            let reader = BufReader::new(f);
            let lines = reader.lines();
            for line in lines.map(|x| x.unwrap()) {
                // let pair: Vec<&str> = line.split(';').collect();
                // let pair = line.split_once(';').unwrap();
                let pair = line.rsplit_once(';').unwrap();

                let city_name = pair.0.to_string();
                let weather_temp = pair.1.to_string();

                match words.entry(city_name.clone()) {
                    Entry::Occupied(entry) => {
                        let mut value = entry.into_mut();
                        value.count += 1;

                        let temp: f32 = weather_temp.parse().unwrap();

                        let sum: u64 = (temp * 10000.0) as u64;
                        value.sum += sum;

                        if value.min > temp {
                            value.min = temp;
                        } else if value.max < temp {
                            value.max = temp;
                        }
                    }
                    Entry::Vacant(entry) => {
                        let temp: f32 = weather_temp.parse().unwrap();
                        let sum: u64 = (temp * 10000.0) as u64;

                        let wr = WeatherResult {
                            min: temp,
                            max: temp,
                            sum,
                            count: 1u32,
                        };

                        // println!("insert value {}", city_name.clone());
                        let _ = *entry.insert(wr);
                    }
                }
            }
        }
        Err(e) => println!("{}", e),
    }

    let mut rank: Vec<(String, WeatherResult)> = words.into_iter().collect();
    rank.sort_by_key(|pair| pair.0.clone());
    rank.iter().for_each(|(key, value)| {
        println!(
            "{} {}/{:.4}/{}",
            key,
            value.min,
            value.sum as f64 / (10000f64 * (value.count as f64)),
            value.max
        );
    });
}

let path = Path::new("src/file/measurements.txt");
println!("尝试读取文件: {:?}", path.canonicalize());

one_billion_row_challenge();
}

24.7.3 代码中的函数式编程思想

  1. 迭代器链式调用reader.lines() 返回一个迭代器,map(|x| x.unwrap()) 对每个元素进行转换
  2. HashMap 的 entry APIwords.entry(city_name.clone()) 提供了一种函数式的方式来处理“键存在“和“键不存在“两种情况,避免了重复的哈希查找
  3. 不可变与可变的平衡WeatherResult 结构体用 Copy trait 实现值语义,而 HashMap 用可变引用来更新状态
  4. 消费迭代器words.into_iter().collect() 消费 HashMap,将其转换为 Vec 进行排序
  5. 声明式输出rank.iter().for_each(...) 用声明式方式遍历并打印结果

24.8 总结

24.8.1 λ演算核心概念对比

概念数学表示含义Rust 对应
变量$x$符号占位变量绑定
抽象$\lambda x . M$匿名函数定义闭包 |x| ...
应用$(M , N)$函数调用f(x)
α转换$\lambda x . M \equiv_\alpha \lambda y . M[x:=y]$变量重命名无直接对应
β归约$(\lambda x . M) , N \to_\beta M[x:=N]$函数求值闭包调用
η转换$\lambda x . (f , x) = f$函数等价无直接对应

24.8.2 函数式编程特性

特性描述Rust 支持程度
纯函数无副作用,引用透明语言鼓励,但不强制
不可变性默认不可变let 默认不可变,let mut 可变
高阶函数函数作为参数/返回值完全支持(闭包 + trait)
惰性求值按需计算迭代器惰性,表达式严格求值
递归函数调用自身支持,但需注意栈溢出
模式匹配解构数据结构强大的 match 表达式
类型推断编译器推断类型局部类型推断

24.8.3 Rust 闭包类型对比

Trait捕获方式调用次数使用场景
Fn&self(不可变借用)多次只读取环境变量
FnMut&mut self(可变借用)多次需要修改环境变量
FnOnceself(移动所有权)一次消费环境变量

24.9 练习题

练习 24.1

将以下数学函数写成λ表达式:

  1. $f(x) = x^2$
  2. $f(x, y) = x + y$
  3. 一个函数,接受两个参数,返回较大的那个

练习 24.2

对以下λ表达式进行 β 归约,写出每一步:

  1. $(\lambda x . x , x) , (\lambda y . y)$
  2. $(\lambda x . \lambda y . x , y) , a , b$
  3. $(\lambda f . \lambda x . f , (f , x)) , (\lambda y . y + 1) , 3$

练习 24.3

验证 Church 布尔值的 AND 运算:证明 $\text{AND} , \text{TRUE} , \text{FALSE} = \text{FALSE}$,其中 $\text{AND} = \lambda p . \lambda q . p , q , p$。

练习 24.4

用 Church 编码定义乘法运算 $\text{MULT} = \lambda m . \lambda n . \lambda f . m , (n , f)$,并验证 $\text{MULT} , 2 , 3 = 6$。

练习 24.5

编写一个 Rust 闭包,实现 Church 数的后继函数(Successor)的功能:接受一个 u32,返回其加一的结果。然后使用 fold 将该闭包应用 5 次到初始值 0 上。

练习 24.6

给定一个 Vec<String>,使用迭代器方法链完成以下操作(每个操作一行代码):

  1. 筛选出长度大于 3 的字符串
  2. 将所有字符串转换为大写
  3. 按字典序排序
  4. 去重
  5. 收集为 Vec<String>

练习 24.7

实现一个 Rust 函数 compose_three,它接受三个函数 fgh,返回它们的组合 $f \circ g \circ h$(即先应用 $h$,再应用 $g$,最后应用 $f$)。

#![allow(unused)]
fn main() {
fn compose_three<F, G, H, A, B, C, D>(f: F, g: G, h: H) -> impl Fn(A) -> D
where
    F: Fn(C) -> D,
    G: Fn(B) -> C,
    H: Fn(A) -> B,
{
    // 你的实现
}
}

练习 24.8

阅读 1BRC 代码,回答以下问题:

  1. 为什么使用 rsplit_once(';') 而不是 split_once(';')
  2. 为什么温度值要乘以 10000.0 后转为 u64 存储?
  3. 如果将代码中的 for line in lines.map(|x| x.unwrap()) 改为 lines.for_each(...),需要做哪些修改?
  4. (挑战)尝试使用 rayon crate 将 1BRC 代码并行化,使用 par_iter() 处理文件行。

第二十五 人工智能

人工智能(Artificial Intelligence,AI)是当今科技领域最热门的话题之一。从智能语音助手到自动驾驶,从图像识别到机器翻译,AI 正在深刻改变我们的生活和工作方式。作为系统级编程语言,Rust 凭借其高性能和内存安全特性,在 AI 基础设施领域也逐渐崭露头角。本章将带领读者了解人工智能的核心概念,并探索 Rust 在 AI 生态中的位置。


一、什么是人工智能

1.1 人工智能的定义

人工智能是指由计算机系统所表现出的、通常需要人类智能才能完成的复杂行为,包括学习、推理、感知、理解语言、解决问题等能力。

从广义上看,AI 可分为三个层次:

层次名称说明
弱人工智能Artificial Narrow Intelligence (ANI)专注于特定任务,如语音识别、图像分类
强人工智能Artificial General Intelligence (AGI)具备人类水平的通用智能,目前尚未实现
超人工智能Artificial Super Intelligence (ASI)超越人类智能的理论阶段

当前我们接触到的所有 AI 应用都属于弱人工智能范畴。

1.2 人工智能发展简史

时间事件
1956达特茅斯会议,“人工智能“概念正式提出
1966ELIZA 聊天机器人诞生
1997IBM 深蓝击败国际象棋世界冠军卡斯帕罗夫
2012AlexNet 在 ImageNet 竞赛中大胜,深度学习崛起
2016AlphaGo 击败围棋世界冠军李世石
2022ChatGPT 发布,大语言模型进入大众视野
2024Sora、GPT-4o 等多模态大模型爆发

二、人工智能的三大支柱

现代人工智能的飞速发展离不开三大核心支柱的支撑。

2.1 大数据

数据是 AI 的“燃料“。深度学习模型需要海量数据进行训练,数据量越大、质量越高,模型的表现通常越好。互联网、物联网和各类数字化系统每天产生数以 EB 计的数据,为 AI 发展提供了充足的原材料。

2.2 大算力

训练现代大模型需要巨大的计算资源。GPT-3 拥有 1750 亿参数,其训练消耗了数千 GPU 年的计算量。GPU(图形处理器)和 TPU(张量处理器)等专用硬件的发展,使得大规模并行计算成为可能。

2.3 大模型

大模型(Foundation Model)是指在海量数据上预训练、具有大量参数的神经网络模型。这些模型展现出强大的涌现能力(Emergent Abilities),即模型规模达到某个临界点后,会突然具备某些小模型不具备的能力,如上下文学习、逻辑推理等。


三、机器学习基础

机器学习(Machine Learning,ML)是人工智能的核心分支,它让计算机能够从数据中自动学习规律,而无需显式编程。

3.1 机器学习的三大范式

范式说明示例
监督学习使用带标签的数据训练模型图像分类、房价预测
无监督学习从无标签数据中发现隐藏结构聚类、降维、异常检测
强化学习智能体通过与环境交互学习最优策略AlphaGo、自动驾驶

3.2 数据集划分

在机器学习中,数据通常划分为三个集合:

集合用途比例
训练集用于训练模型参数约 70%-80%
验证集用于调整超参数、选择模型约 10%-15%
测试集用于最终评估模型性能约 10%-15%

3.3 过拟合与欠拟合

过拟合(Overfitting):模型在训练集上表现很好,但在新数据上表现差,说明模型“记住“了训练数据而非学到通用规律。解决方法包括增加数据量、正则化、Dropout、早停等。

欠拟合(Underfitting):模型在训练集和测试集上都表现不佳,说明模型复杂度不足,未能捕捉数据中的规律。解决方法包括增加模型复杂度、增加特征、减少正则化等。


四、深度学习入门

深度学习(Deep Learning)是机器学习的子领域,基于多层神经网络(Neural Network)进行表示学习。

4.1 神经网络基础

神经网络由大量相互连接的神经元(节点)组成,分为输入层、隐藏层和输出层。每个连接都有权重,神经元接收输入、加权求和、通过激活函数产生输出。

#![allow(unused)]
fn main() {
// 简单的神经元计算模拟
fn neuron(inputs: &[f64], weights: &[f64], bias: f64) -> f64 {
    let sum: f64 = inputs.iter()
        .zip(weights.iter())
        .map(|(x, w)| x * w)
        .sum();
    relu(sum + bias)
}

fn relu(x: f64) -> f64 {
    x.max(0.0)
}
}

4.2 激活函数

激活函数为神经网络引入非线性,使其能够学习复杂的模式。

激活函数公式特点
Sigmoid$\sigma(x) = \frac{1}{1 + e^{-x}}$输出范围 (0,1),适合二分类,但存在梯度消失问题
Tanh$\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}$输出范围 (-1,1),零中心化,但仍可能梯度消失
ReLU$f(x) = \max(0, x)$计算简单,缓解梯度消失,但可能导致神经元“死亡“
Softmax$\sigma(x_i) = \frac{e^{x_i}}{\sum_j e^{x_j}}$将输出转换为概率分布,多用于多分类输出层

4.3 损失函数与梯度下降

损失函数(Loss Function) 衡量模型预测与真实值之间的差距。常见的损失函数包括:

  • 均方误差(MSE):用于回归任务
  • 交叉熵损失(Cross-Entropy):用于分类任务

梯度下降(Gradient Descent) 是训练神经网络的核心优化算法。它通过计算损失函数对参数的梯度,沿着梯度反方向更新参数,逐步降低损失。

#![allow(unused)]
fn main() {
// 模拟单步梯度下降
fn gradient_descent_step(
    params: &mut [f64],
    gradients: &[f64],
    learning_rate: f64,
) {
    for (p, g) in params.iter_mut().zip(gradients.iter()) {
        *p -= learning_rate * g;
    }
}
}

4.4 反向传播

反向传播(Backpropagation)是计算神经网络中所有参数梯度的有效算法。它利用链式法则,从输出层向输入层逐层计算梯度,使得梯度下降能够应用于深层网络。

训练流程:前向传播计算输出 → 计算损失 → 反向传播计算梯度 → 更新参数。重复此过程直到收敛。


五、大语言模型

大语言模型(Large Language Model,LLM)是近年来 AI 领域最重大的突破之一。

5.1 什么是 LLM

LLM 是基于深度学习的大规模预训练语言模型,通常包含数十亿到数千亿参数。它们通过在海量文本数据上进行自监督学习,掌握了丰富的语言知识和世界知识,能够完成文本生成、翻译、摘要、问答、代码编写等多种任务。

代表模型包括 GPT 系列、Claude、LLaMA、通义千问、文心一言等。

5.2 Transformer 架构与自注意力机制

Transformer 是 LLM 的核心架构,于 2017 年在论文《Attention Is All You Need》中提出。它完全基于注意力机制(Attention Mechanism),摒弃了传统的循环结构。

自注意力(Self-Attention) 机制允许模型在处理每个词时,同时关注输入序列中的所有位置,并自动学习不同位置之间的关联强度。

$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$

其中 Q(Query)、K(Key)、V(Value)是输入的三个线性变换,$d_k$ 是 Key 的维度。

Transformer 的核心组件包括:

组件功能
多头自注意力并行计算多组注意力,捕捉不同子空间的信息
前馈神经网络对每个位置独立进行非线性变换
位置编码为模型注入序列位置信息
层归一化稳定训练过程

5.3 Token 与词嵌入

Token 是 LLM 处理文本的基本单位,可以是一个字、一个词或一个词片段。例如,“Hello world” 可能被切分为 ["Hello", " world"] 两个 Token。

词嵌入(Word Embedding) 将离散的 Token 映射为连续的向量表示,使得语义相近的词在向量空间中距离较近。Embedding 是模型理解语言的基础。

#![allow(unused)]
fn main() {
// 用 Rust 模拟简单的词嵌入查找
fn embedding_lookup(token_id: usize, embedding_matrix: &[Vec<f64>]) -> &[f64] {
    &embedding_matrix[token_id]
}
}

5.4 AI Agent

AI Agent(智能体)是指能够感知环境、进行决策并执行动作以实现特定目标的自主系统。基于 LLM 的 Agent 通常具备以下能力:

能力说明
规划(Planning)将复杂任务分解为可执行的子任务
记忆(Memory)维护短期和长期记忆,支持上下文理解
工具使用(Tool Use)调用外部 API、搜索引擎、计算器等工具
反思(Reflection)评估自身行为并改进策略

六、Rust 中的 AI 生态

Rust 虽然不如 Python 在 AI 领域普及,但凭借其高性能和安全性,正在 AI 基础设施和推理部署方面发挥越来越重要的作用。

6.1 Candle

Candle 是 Hugging Face 推出的 Rust 机器学习框架,主打轻量和高效。

use candle_core::{Device, Tensor};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let device = Device::Cpu;
    let a = Tensor::new(&[[1.0f32, 2.0], [3.0, 4.0]], &device)?;
    let b = Tensor::new(&[[5.0f32, 6.0], [7.0, 8.0]], &device)?;
    let c = a.matmul(&b)?;
    println!("{}", c);
    Ok(())
}

Candle 支持在 CPU 和 GPU 上运行,无需 Python 环境即可部署大模型,非常适合边缘计算和嵌入式场景。

6.2 Burn

Burn 是一个用纯 Rust 编写的深度学习框架,设计目标是灵活、高效且易于使用。

#![allow(unused)]
fn main() {
use burn::tensor::{Tensor, Backend};

fn compute<B: Backend>() {
    let device = B::Device::default();
    let tensor1 = Tensor::<B, 2>::from_floats([[1.0, 2.0], [3.0, 4.0]], &device);
    let tensor2 = Tensor::<B, 2>::from_floats([[5.0, 6.0], [7.0, 8.0]], &device);
    let result = tensor1 + tensor2;
}
}

Burn 支持多种后端(NdArray、WGPU、Candle),允许同一套代码在不同硬件上运行。

6.3 用 Rust 调用 ONNX 模型

ONNX(Open Neural Network Exchange)是开放的神经网络交换格式,允许模型在不同框架间互操作。Rust 可以通过 ort crate 运行 ONNX 模型。

use ort::{Environment, Session, Value};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let env = Environment::builder().build()?;
    let session = Session::builder(&env)?
        .with_model_from_file("model.onnx")?;

    let input = Value::from_array(
        ndarray::array![[1.0f32, 2.0, 3.0, 4.0]]
    )?;
    let outputs = session.run(vec![input])?;
    println!("输出: {:?}", outputs);
    Ok(())
}

这种方式让 Rust 应用能够直接加载和运行由 PyTorch、TensorFlow 等框架训练并导出的模型,是 Rust 接入 AI 能力最实用的途径之一。


七、总结与练习

本章小结

知识点要点
机器学习三大范式监督学习、无监督学习、强化学习
数据集划分训练集、验证集、测试集各司其职
过拟合与欠拟合模型复杂度和数据量的平衡
深度学习核心神经网络、激活函数、损失函数、梯度下降、反向传播
Transformer自注意力机制是 LLM 的核心
Rust AI 生态Candle(轻量推理)、Burn(深度学习框架)、ONNX(跨框架部署)

练习建议

  1. 手动实现感知机:用 Rust 实现一个最简单的单层神经网络,完成 AND 或 OR 逻辑门的训练。
  2. 梯度下降可视化:编写程序记录梯度下降过程中损失值的变化,观察学习率对收敛的影响。
  3. Candle 体验:安装 Candle,运行官方示例中的简单线性回归或文本生成模型。
  4. ONNX 推理:将一个预训练的图像分类模型(如 MNIST)导出为 ONNX 格式,用 Rust 编写推理程序。
  5. 注意力机制模拟:用 Rust 实现一个简化的自注意力计算,输入一个小序列,观察注意力权重的分布。
  6. AI 应用思考:结合 Rust 的高性能特性,思考在哪些 AI 场景下使用 Rust 比 Python 更有优势(如实时推理、嵌入式部署)。

第二十六 算法

算法(Algorithm)是解决特定问题的一系列明确、有限的计算步骤。如果说编程语言是工具,那么算法就是使用这些工具的方法论。好的算法能够在有限的时间和空间内,高效地解决问题。


一、算法基础

1.1 什么是算法

算法具有五个基本特性:

特性说明
有穷性算法必须在有限步骤后结束
确定性每一步都有明确的定义,无二义性
可行性每一步都可以通过基本运算实现
输入有零个或多个输入
输出至少有一个输出

1.2 算法效率的度量

评价算法好坏的核心指标是时间复杂度空间复杂度

大 O 记号(Big-O Notation)

大 O 记号描述算法运行时间随输入规模增长的趋势:

复杂度记号说明示例
常数时间O(1)与输入规模无关数组按索引访问
对数时间O(log n)每步将问题规模减半二分查找
线性时间O(n)与输入规模成正比遍历数组
线性对数O(n log n)线性乘以对数归并排序、快速排序
平方时间O(n²)双重循环冒泡排序
指数时间O(2ⁿ)每步问题规模翻倍穷举子集
阶乘时间O(n!)全排列旅行商问题(暴力)
#![allow(unused)]
fn main() {
// O(1) - 常数时间
fn get_first(arr: &[i32]) -> Option<i32> {
    arr.first().copied()  // 直接访问,与数组长度无关
}

// O(n) - 线性时间
fn find_max(arr: &[i32]) -> Option<i32> {
    arr.iter().max().copied()  // 需要遍历整个数组
}

// O(n²) - 平方时间
fn bubble_sort(arr: &mut [i32]) {
    let n = arr.len();
    for i in 0..n {
        for j in 0..n - i - 1 {
            if arr[j] > arr[j + 1] {
                arr.swap(j, j + 1);
            }
        }
    }
}
}

复杂度比较(n = 1000 时):

复杂度操作次数
O(1)1
O(log n)~10
O(n)1,000
O(n log n)~10,000
O(n²)1,000,000
O(2ⁿ)约 10³⁰¹

当 n 很大时,算法复杂度的差异会变得极其显著。O(n²) 算法处理 10⁶ 数据可能需要数小时,而 O(n log n) 算法只需数秒。


二、算法思想

2.1 枚举法(Brute Force)

枚举法是最直接的算法思想:遍历所有可能的情况,逐一验证是否满足条件。

// 例:找出 1~100 中的所有素数
fn find_primes(limit: u32) -> Vec<u32> {
    let mut primes = Vec::new();
    for n in 2..=limit {
        let mut is_prime = true;
        for i in 2..n {
            if n % i == 0 {
                is_prime = false;
                break;
            }
        }
        if is_prime {
            primes.push(n);
        }
    }
    primes
}

fn main() {
    println!("{:?}", find_primes(50));
    // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
}

枚举法简单直接,但效率通常不高。上面的素数查找可以优化到 O(n log log n)(埃拉托斯特尼筛法)。

2.2 分治法(Divide and Conquer)

分治法的核心思想:将大问题分解为若干相似的子问题,递归解决子问题,再合并结果

分治法的三个步骤:

  1. 分解:将原问题分解为若干子问题
  2. 解决:递归解决子问题(若子问题足够小,直接求解)
  3. 合并:将子问题的解合并为原问题的解
// 归并排序(Merge Sort)- 经典分治算法
fn merge_sort(arr: &mut [i32]) {
    let len = arr.len();
    if len <= 1 {
        return;
    }

    let mid = len / 2;
    merge_sort(&mut arr[..mid]);
    merge_sort(&mut arr[mid..]);

    // 合并两个有序数组
    let mut temp = arr.to_vec();
    merge(&arr[..mid], &arr[mid..], &mut temp);
    arr.copy_from_slice(&temp);
}

fn merge(left: &[i32], right: &[i32], result: &mut [i32]) {
    let (mut i, mut j, mut k) = (0, 0, 0);

    while i < left.len() && j < right.len() {
        if left[i] <= right[j] {
            result[k] = left[i];
            i += 1;
        } else {
            result[k] = right[j];
            j += 1;
        }
        k += 1;
    }

    while i < left.len() {
        result[k] = left[i];
        i += 1;
        k += 1;
    }

    while j < right.len() {
        result[k] = right[j];
        j += 1;
        k += 1;
    }
}

fn main() {
    let mut arr = [38, 27, 43, 3, 9, 82, 10];
    merge_sort(&mut arr);
    println!("{:?}", arr);  // [3, 9, 10, 27, 38, 43, 82]
}

时间复杂度分析: 归并排序的时间复杂度为 O(n log n),空间复杂度为 O(n)。

2.3 动态规划(Dynamic Programming)

动态规划(DP)的核心思想:将问题分解为重叠子问题,存储子问题的解以避免重复计算

DP 的两个关键要素:

  • 最优子结构:问题的最优解包含子问题的最优解
  • 重叠子问题:子问题会被重复计算多次
// 斐波那契数列 - 动态规划解法
fn fib_dp(n: u32) -> u64 {
    if n == 0 { return 0; }
    if n == 1 { return 1; }

    let mut dp = vec![0u64; (n + 1) as usize];
    dp[1] = 1;

    for i in 2..=n as usize {
        dp[i] = dp[i - 1] + dp[i - 2];
    }

    dp[n as usize]
}

// 空间优化版 - 只保留前两个状态
fn fib_dp_optimized(n: u32) -> u64 {
    if n == 0 { return 0; }
    if n == 1 { return 1; }

    let (mut prev, mut curr) = (0u64, 1u64);
    for _ in 2..=n {
        let next = prev + curr;
        prev = curr;
        curr = next;
    }
    curr
}

fn main() {
    println!("F(50) = {}", fib_dp(50));           // 12586269025
    println!("F(50) = {}", fib_dp_optimized(50));  // 12586269025
}

经典问题:0/1 背包问题

// 0/1 背包问题
// 有 n 个物品,每个物品有重量 weight[i] 和价值 value[i]
// 背包容量为 W,求能装入的最大价值

fn knapsack(weights: &[usize], values: &[usize], capacity: usize) -> usize {
    let n = weights.len();
    let mut dp = vec![vec![0; capacity + 1]; n + 1];

    for i in 1..=n {
        for w in 0..=capacity {
            if weights[i - 1] <= w {
                // 可以选择放入或不放入
                dp[i][w] = dp[i - 1][w].max(
                    dp[i - 1][w - weights[i - 1]] + values[i - 1]
                );
            } else {
                dp[i][w] = dp[i - 1][w];
            }
        }
    }

    dp[n][capacity]
}

fn main() {
    let weights = vec![2, 3, 4, 5];
    let values = vec![3, 4, 5, 6];
    let capacity = 8;

    println!("最大价值: {}", knapsack(&weights, &values, capacity));  // 10
}

时间复杂度: O(n × W),其中 n 是物品数量,W 是背包容量。

2.4 贪心算法(Greedy Algorithm)

贪心算法的核心思想:每一步都做出当前看起来最优的选择,希望最终得到全局最优解

贪心算法不一定能得到全局最优解,但在某些问题上是有效的。

// 找零钱问题(贪心解法)
// 假设硬币面额为 1, 5, 10, 25, 50
fn make_change(amount: usize) -> Vec<usize> {
    let coins = vec![50, 25, 10, 5, 1];
    let mut change = Vec::new();
    let mut remaining = amount;

    for &coin in &coins {
        while remaining >= coin {
            change.push(coin);
            remaining -= coin;
        }
    }

    change
}

fn main() {
    let amount = 87;
    let change = make_change(amount);
    println!("找 {} 分钱: {:?}", amount, change);
    // 找 87 分钱: [50, 25, 10, 1, 1]
    println!("硬币数量: {}", change.len());  // 5
}

注意: 贪心算法在标准硬币系统(如 1, 5, 10, 25)下能得到最优解,但在某些面额组合(如 1, 3, 4)下可能不是最优。此时需要使用动态规划。

2.5 回溯法(Backtracking)

回溯法的核心思想:系统地搜索问题的解空间,当发现当前路径不可能得到解时,回溯到上一步尝试其他路径

// 全排列问题
fn permutations(nums: &mut [i32]) -> Vec<Vec<i32>> {
    let mut result = Vec::new();
    backtrack(nums, 0, &mut result);
    result
}

fn backtrack(nums: &mut [i32], start: usize, result: &mut Vec<Vec<i32>>) {
    if start == nums.len() {
        result.push(nums.to_vec());
        return;
    }

    for i in start..nums.len() {
        nums.swap(start, i);           // 做选择
        backtrack(nums, start + 1, result);  // 递归
        nums.swap(start, i);           // 撤销选择(回溯)
    }
}

fn main() {
    let mut nums = [1, 2, 3];
    let perms = permutations(&mut nums);
    for p in &perms {
        println!("{:?}", p);
    }
    // [1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], [3, 1, 2]
}

三、搜索算法

3.1 深度优先搜索(DFS)

DFS 沿着一条路径尽可能深地搜索,直到无法继续才回溯。

// 图的深度优先搜索
use std::collections::{HashMap, HashSet};

struct Graph {
    adj: HashMap<i32, Vec<i32>>,
}

impl Graph {
    fn new() -> Self {
        Graph { adj: HashMap::new() }
    }

    fn add_edge(&mut self, u: i32, v: i32) {
        self.adj.entry(u).or_insert_with(Vec::new).push(v);
        self.adj.entry(v).or_insert_with(Vec::new).push(u);
    }

    fn dfs(&self, start: i32) -> Vec<i32> {
        let mut visited = HashSet::new();
        let mut result = Vec::new();
        self.dfs_helper(start, &mut visited, &mut result);
        result
    }

    fn dfs_helper(&self, node: i32, visited: &mut HashSet<i32>, result: &mut Vec<i32>) {
        visited.insert(node);
        result.push(node);

        if let Some(neighbors) = self.adj.get(&node) {
            for &neighbor in neighbors {
                if !visited.contains(&neighbor) {
                    self.dfs_helper(neighbor, visited, result);
                }
            }
        }
    }
}

fn main() {
    let mut graph = Graph::new();
    graph.add_edge(0, 1);
    graph.add_edge(0, 2);
    graph.add_edge(1, 3);
    graph.add_edge(1, 4);
    graph.add_edge(2, 5);

    println!("DFS: {:?}", graph.dfs(0));  // [0, 1, 3, 4, 2, 5]
}

3.2 广度优先搜索(BFS)

BFS 逐层扩展搜索,先访问所有邻居,再访问邻居的邻居。

// 图的广度优先搜索
use std::collections::{HashMap, HashSet, VecDeque};

struct Graph {
    adj: HashMap<i32, Vec<i32>>,
}

impl Graph {
    fn new() -> Self {
        Graph { adj: HashMap::new() }
    }

    fn add_edge(&mut self, u: i32, v: i32) {
        self.adj.entry(u).or_insert_with(Vec::new).push(v);
        self.adj.entry(v).or_insert_with(Vec::new).push(u);
    }

    fn bfs(&self, start: i32) -> Vec<i32> {
        let mut visited = HashSet::new();
        let mut queue = VecDeque::new();
        let mut result = Vec::new();

        visited.insert(start);
        queue.push_back(start);

        while let Some(node) = queue.pop_front() {
            result.push(node);

            if let Some(neighbors) = self.adj.get(&node) {
                for &neighbor in neighbors {
                    if !visited.contains(&neighbor) {
                        visited.insert(neighbor);
                        queue.push_back(neighbor);
                    }
                }
            }
        }

        result
    }
}

fn main() {
    let mut graph = Graph::new();
    graph.add_edge(0, 1);
    graph.add_edge(0, 2);
    graph.add_edge(1, 3);
    graph.add_edge(1, 4);
    graph.add_edge(2, 5);

    println!("BFS: {:?}", graph.bfs(0));  // [0, 1, 2, 3, 4, 5]
}

DFS vs BFS 对比:

  • DFS 使用栈(递归或显式栈),适合寻找路径、拓扑排序
  • BFS 使用队列,适合寻找最短路径(无权图)

四、经典排序算法

算法平均时间最坏时间空间稳定性特点
冒泡排序O(n²)O(n²)O(1)稳定简单,效率低
选择排序O(n²)O(n²)O(1)不稳定交换次数少
插入排序O(n²)O(n²)O(1)稳定对小数据高效
归并排序O(n log n)O(n log n)O(n)稳定稳定,需额外空间
快速排序O(n log n)O(n²)O(log n)不稳定平均最快
堆排序O(n log n)O(n log n)O(1)不稳定空间效率高
// 快速排序
fn quick_sort(arr: &mut [i32]) {
    if arr.len() <= 1 {
        return;
    }

    let pivot_index = partition(arr);
    let (left, right) = arr.split_at_mut(pivot_index);
    quick_sort(left);
    quick_sort(&mut right[1..]);
}

fn partition(arr: &mut [i32]) -> usize {
    let len = arr.len();
    let pivot = arr[len - 1];
    let mut i = 0;

    for j in 0..len - 1 {
        if arr[j] <= pivot {
            arr.swap(i, j);
            i += 1;
        }
    }

    arr.swap(i, len - 1);
    i
}

fn main() {
    let mut arr = [64, 34, 25, 12, 22, 11, 90];
    quick_sort(&mut arr);
    println!("{:?}", arr);  // [11, 12, 22, 25, 34, 64, 90]
}

五、经典问题选讲

5.1 八皇后问题

在 8×8 的棋盘上放置 8 个皇后,使它们互不攻击(不在同一行、列、对角线)。

fn solve_n_queens(n: usize) -> Vec<Vec<String>> {
    let mut board = vec![vec!['.'; n]; n];
    let mut result = Vec::new();
    backtrack_queen(&mut board, 0, &mut result);
    result
}

fn backtrack_queen(board: &mut Vec<Vec<char>>, row: usize, result: &mut Vec<Vec<String>>) {
    let n = board.len();
    if row == n {
        result.push(board.iter().map(|r| r.iter().collect()).collect());
        return;
    }

    for col in 0..n {
        if is_valid(board, row, col) {
            board[row][col] = 'Q';
            backtrack_queen(board, row + 1, result);
            board[row][col] = '.';
        }
    }
}

fn is_valid(board: &Vec<Vec<char>>, row: usize, col: usize) -> bool {
    let n = board.len();

    // 检查列
    for i in 0..row {
        if board[i][col] == 'Q' {
            return false;
        }
    }

    // 检查左上对角线
    for i in 1..=row {
        if col >= i && board[row - i][col - i] == 'Q' {
            return false;
        }
    }

    // 检查右上对角线
    for i in 1..=row {
        if col + i < n && board[row - i][col + i] == 'Q' {
            return false;
        }
    }

    true
}

fn main() {
    let solutions = solve_n_queens(8);
    println!("8 皇后问题共有 {} 种解法", solutions.len());  // 92
}

5.2 最长公共子序列(LCS)

// 动态规划求解最长公共子序列
fn longest_common_subsequence(text1: &str, text2: &str) -> usize {
    let m = text1.len();
    let n = text2.len();
    let mut dp = vec![vec![0; n + 1]; m + 1];

    let chars1: Vec<char> = text1.chars().collect();
    let chars2: Vec<char> = text2.chars().collect();

    for i in 1..=m {
        for j in 1..=n {
            if chars1[i - 1] == chars2[j - 1] {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]);
            }
        }
    }

    dp[m][n]
}

fn main() {
    println!("{}", longest_common_subsequence("ABCDE", "ACE"));     // 3 ("ACE")
    println!("{}", longest_common_subsequence("ABC", "DEF"));       // 0
}

六、算法学习资源

资源说明
TheAlgorithms开源算法库,支持多种编程语言
VisuAlgo算法可视化网站,直观理解算法执行过程
LeetCode算法练习题库
3Blue1Brown数学与算法的可视化讲解

七、总结

算法思想核心思想适用场景经典问题
枚举遍历所有可能解空间小素数查找
分治分解、解决、合并可分解为独立子问题归并排序、快速排序
动态规划存储子问题解最优子结构、重叠子问题背包问题、LCS
贪心局部最优具有贪心选择性质找零钱、活动选择
回溯深度搜索+剪枝解空间树搜索八皇后、全排列

算法是编程的灵魂。掌握这些基本算法思想,能够帮助我们更高效地解决各种计算问题。

练习建议:

  1. 实现一个二分查找算法
  2. 用动态规划解决斐波那契数列问题
  3. 尝试解决 LeetCode 上的经典算法题

第二十七 代数(Algebra)

代数是数学的一个基础分支,主要研究数、符号、数量关系以及运算结构。它通过用符号(如字母)代替具体数字,抽象地探讨运算规律、方程求解以及数学结构(如群、环、域等)。

法国数学家弗朗索瓦·韦达(François Viète,1540—1603)第一次有意识地使用系统的代数字母与符号,以辅音字母表示已知量,元音字母表示未知量,推进了方程论的发展,使代数成为一般类型的形式和方程的学问,因其抽象而应用更为广泛,被称为“代数符号之父“。在研究一元二次方程的解法时,他发现了一元二次方程的根与系数之间存在的特殊关系,人们把这个关系称为韦达定理


一、一元二次方程

1.1 一般形式

$$ax^2+bx+c=0 \quad (a\neq0)$$

1.2 求根公式

$$x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}$$

其中 $\Delta = b^2 - 4ac$ 称为判别式:

判别式根的情况
$\Delta > 0$两个不相等的实数根
$\Delta = 0$两个相等的实数根(重根)
$\Delta < 0$无实数根(两个共轭复数根)

1.3 韦达定理

设方程 $ax^2+bx+c=0$ 的两个根为 $x_1, x_2$,则:

$$x_1 + x_2 = -\frac{b}{a}$$

$$x_1 \cdot x_2 = \frac{c}{a}$$

1.4 Rust 实现

fn solve_quadratic(a: f64, b: f64, c: f64) -> (Option<f64>, Option<f64>) {
    let discriminant = b * b - 4.0 * a * c;

    if discriminant > 0.0 {
        let sqrt_d = discriminant.sqrt();
        let x1 = (-b + sqrt_d) / (2.0 * a);
        let x2 = (-b - sqrt_d) / (2.0 * a);
        (Some(x1), Some(x2))
    } else if discriminant == 0.0 {
        let x = -b / (2.0 * a);
        (Some(x), Some(x))  // 重根
    } else {
        (None, None)  // 无实数根
    }
}

fn main() {
    // x² - 5x + 6 = 0,根为 2 和 3
    let (x1, x2) = solve_quadratic(1.0, -5.0, 6.0);
    println!("x² - 5x + 6 = 0 的根: {:?}, {:?}", x1, x2);

    // x² + 2x + 1 = 0,根为 -1(重根)
    let (x1, x2) = solve_quadratic(1.0, 2.0, 1.0);
    println!("x² + 2x + 1 = 0 的根: {:?}, {:?}", x1, x2);

    // x² + x + 1 = 0,无实数根
    let (x1, x2) = solve_quadratic(1.0, 1.0, 1.0);
    println!("x² + x + 1 = 0 的根: {:?}, {:?}", x1, x2);
}

二、数列

2.1 等差数列

等差数列是相邻两项之差为常数的数列,这个常数称为公差($d$)。

通项公式: $a_n = a_1 + (n-1)d$

前 n 项和: $S_n = \frac{n(a_1 + a_n)}{2} = \frac{n}{2}[2a_1 + (n-1)d]$

fn arithmetic_sum(a1: f64, d: f64, n: usize) -> f64 {
    n as f64 * (2.0 * a1 + (n as f64 - 1.0) * d) / 2.0
}

fn main() {
    // 1 + 2 + 3 + ... + 100 = 5050
    println!("1+2+...+100 = {}", arithmetic_sum(1.0, 1.0, 100));  // 5050
}

2.2 等比数列

等比数列是相邻两项之比为常数的数列,这个常数称为公比($q$)。

通项公式: $a_n = a_1 \cdot q^{n-1}$

前 n 项和: $S_n = \frac{a_1(1 - q^n)}{1 - q} \quad (q \neq 1)$

fn geometric_sum(a1: f64, q: f64, n: usize) -> f64 {
    if q == 1.0 {
        a1 * n as f64
    } else {
        a1 * (1.0 - q.powi(n as i32)) / (1.0 - q)
    }
}

fn main() {
    // 1 + 2 + 4 + 8 + ... 前 10 项
    println!("等比求和: {}", geometric_sum(1.0, 2.0, 10));  // 1023.0
}

2.3 斐波那契数列

斐波那契数列(Fibonacci Sequence)是递推数列的经典代表:

$$ F_0 = 0, \quad F_1 = 1, \quad F_n = F_{n-1} + F_{n-2} \quad (n \ge 2) $$

fn fib_iterative(n: u32) -> u32 {
    if n == 0 { return 0; }
    if n == 1 { return 1; }

    let mut prev = 0;  // F(n-2)
    let mut curr = 1;  // F(n-1)

    for _ in 2..=n {
        let next = prev + curr;
        prev = curr;
        curr = next;
    }
    curr
}

fn main() {
    for i in 0..=10 {
        println!("F{} = {}", i, fib_iterative(i));
    }
    // F0=0, F1=1, F2=1, F3=2, F4=3, F5=5, F6=8, F7=13, F8=21, F9=34, F10=55
}

斐波那契数列通项公式(Binet 公式):

$$ F(n) = \frac{1}{\sqrt{5}} \left[\left( \frac{1+\sqrt{5}}{2} \right)^n - \left( \frac{1-\sqrt{5}}{2} \right)^n \right] $$

2.4 调和数列

调和数列的通项为 $a_n = \frac{1}{n}$,其前 n 项和为:

$$ H_n = \sum_{k=1}^{n} \frac{1}{k} = 1 + \frac{1}{2} + \frac{1}{3} + \cdots + \frac{1}{n} $$

结论: 调和级数 $\sum_{n=1}^{\infty} \frac{1}{n}$ 发散(其和趋向于无穷大),尽管增长速度非常缓慢。$H_n$ 的增长速度约为 $\ln(n) + \gamma$,其中 $\gamma \approx 0.5772$ 是欧拉-马歇罗尼常数。

fn harmonic_number(n: u32) -> f64 {
    (1..=n).map(|k| 1.0 / k as f64).sum()
}

fn main() {
    println!("H(10) = {:.6}", harmonic_number(10));    // 2.928968
    println!("H(100) = {:.6}", harmonic_number(100));  // 5.187378
    println!("H(1000) = {:.6}", harmonic_number(1000)); // 7.485471
}

三、黄金分割数

3.1 定义

黄金分割数 $\phi$(phi)是满足方程 $\phi^2 = \phi + 1$ 的正根:

$$ \phi = \frac{1+\sqrt{5}}{2} \approx 1.618033988749895 $$

3.2 黄金分割数的性质

黄金分割数满足公式:

$$ \phi = 1 + \frac{1}{\phi} $$

上述方程的另一个共轭根为:

$$ \hat{\phi} = \frac{1-\sqrt{5}}{2} \approx -0.618033988749895 $$

3.3 与斐波那契数列的关系

斐波那契数列相邻项的极限趋向于黄金分割数:

$$ \lim_{n \to \infty} \frac{F(n+1)}{F(n)} = \phi $$

fn fib(n: u32) -> u64 {
    if n == 0 { return 0; }
    if n == 1 { return 1; }
    let (mut prev, mut curr) = (0u64, 1u64);
    for _ in 2..=n {
        let next = prev + curr;
        prev = curr;
        curr = next;
    }
    curr
}

fn main() {
    let phi = (1.0 + 5.0_f64.sqrt()) / 2.0;
    println!("黄金分割数 φ = {:.15}", phi);

    // 验证相邻项之比趋近于 φ
    for n in [10, 15, 20, 25, 30] {
        let ratio = fib(n + 1) as f64 / fib(n) as f64;
        println!("F{}/F{} = {:.15}  (误差: {:.2e})",
            n + 1, n, ratio, (ratio - phi).abs());
    }
}

3.4 斐波那契数列定理

$$ \gcd(F_n, F_{n-1}) = 1 $$

$$ \gcd(F_m, F_n) = F_{\gcd(m,n)} $$


四、欧拉数与欧拉恒等式

4.1 欧拉数(自然常数)

$$ e = \lim_{n \to \infty} \left(1 + \frac{1}{n}\right)^n \approx 2.718281828459045 $$

fn compute_e(iterations: u32) -> f64 {
    (1..=iterations).map(|n| {
        let n_f = n as f64;
        (1.0 + 1.0 / n_f).powf(n_f)
    }).last().unwrap()
}

fn main() {
    println!("e (近似) = {:.15}", compute_e(100000));
    println!("e (标准库) = {:.15}", std::f64::consts::E);
}

4.2 欧拉恒等式

$$ e^{i\pi} + 1 = 0 $$

欧拉恒等式将数学中五个最重要的常数联系在一起:$e$(自然常数)、$i$(虚数单位)、$\pi$(圆周率)、1(乘法单位元)、0(加法单位元)。它被誉为“最美的数学公式“。


五、最大公约数与最小公倍数

5.1 辗转相除法(欧几里得算法)

辗转相除法是一种高效求两个正整数最大公约数(GCD, Greatest Common Divisor)的方法。

核心定理:

$$ \gcd(a, b) = \gcd(b, a \bmod b) $$

反复应用这个等式,直到余数为 0,此时的除数就是最大公约数。

fn gcd(mut a: u64, mut b: u64) -> u64 {
    while b != 0 {
        let temp = b;
        b = a % b;
        a = temp;
    }
    a
}

fn lcm(a: u64, b: u64) -> u64 {
    a / gcd(a, b) * b  // 先除后乘,避免溢出
}

fn main() {
    println!("gcd(48, 18) = {}", gcd(48, 18));  // 6
    println!("gcd(100, 75) = {}", gcd(100, 75));  // 25
    println!("lcm(48, 18) = {}", lcm(48, 18));  // 144
}

六、重要常数与公式

6.1 圆周率的计算

丘德诺夫斯基算法(Chudnovsky Algorithm)是计算圆周率最快的算法之一,由丘德诺夫斯基兄弟在1988年发现,每迭代一次增加约14位精度:

$$ \frac{1}{\pi} = 12 \sum_{k=0}^{\infty} \frac{(-1)^k (6k)! (13591409 + 545140134k)}{(3k)! (k!)^3 (640320)^{3k + 3/2}} $$

fn factorial(n: u64) -> u64 {
    (1..=n).product()
}

fn chudnovsky_pi(terms: usize) -> f64 {
    let mut sum = 0.0_f64;
    for k in 0..terms {
        let k_f = k as f64;
        let numerator = (-1.0_f64).powi(k as i32)
            * factorial(6 * k as u64) as f64
            * (13591409.0 + 545140134.0 * k_f);
        let denominator = factorial(3 * k as u64) as f64
            * (factorial(k as u64) as f64).powi(3)
            * (640320.0_f64).powf(3.0 * k_f + 1.5);
        sum += numerator / denominator;
    }
    1.0 / (12.0 * sum)
}

fn main() {
    let pi = chudnovsky_pi(2);
    println!("π (Chudnovsky, 2项) = {:.15}", pi);
    println!("π (标准库) = {:.15}", std::f64::consts::PI);
}

6.2 常用数学函数速查

函数Rust 方法说明
$\log_{10}$x.log10()常用对数
$\ln$x.ln()自然对数
$e^x$x.exp()E.powf(x)指数函数
$\sin\theta$x.sin()正弦
$\cos\theta$x.cos()余弦
$\tan\theta$x.tan()正切
$\sqrt{x}$x.sqrt()平方根
$x$

七、线性代数基础

线性代数是代数的重要分支,研究向量空间、线性变换以及矩阵等概念。

7.1 基本概念

概念说明维度
标量(Scalar)单个数值0 维
向量(Vector)有方向和大小的量1 维
矩阵(Matrix)数字的矩形排列2 维
张量(Tensor)标量、向量、矩阵的推广n 维

7.2 向量运算

fn dot_product(a: &[f64], b: &[f64]) -> f64 {
    a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}

fn vector_add(a: &[f64], b: &[f64]) -> Vec<f64> {
    a.iter().zip(b.iter()).map(|(x, y)| x + y).collect()
}

fn vector_scale(a: &[f64], scalar: f64) -> Vec<f64> {
    a.iter().map(|x| x * scalar).collect()
}

fn magnitude(v: &[f64]) -> f64 {
    v.iter().map(|x| x * x).sum::<f64>().sqrt()
}

fn main() {
    let a = vec![1.0, 2.0, 3.0];
    let b = vec![4.0, 5.0, 6.0];

    println!("点积: {}", dot_product(&a, &b));           // 32
    println!("向量加: {:?}", vector_add(&a, &b));        // [5, 7, 9]
    println!("标量乘: {:?}", vector_scale(&a, 2.0));     // [2, 4, 6]
    println!("向量 a 的模: {:.4}", magnitude(&a));       // 3.7417
}

7.3 矩阵运算

type Matrix = Vec<Vec<f64>>;

fn matrix_multiply(a: &Matrix, b: &Matrix) -> Option<Matrix> {
    let rows_a = a.len();
    let cols_a = a[0].len();
    let cols_b = b[0].len();

    if cols_a != b.len() {
        return None;
    }

    let mut result = vec![vec![0.0; cols_b]; rows_a];

    for i in 0..rows_a {
        for j in 0..cols_b {
            for k in 0..cols_a {
                result[i][j] += a[i][k] * b[k][j];
            }
        }
    }

    Some(result)
}

fn matrix_transpose(a: &Matrix) -> Matrix {
    let rows = a.len();
    let cols = a[0].len();
    let mut result = vec![vec![0.0; rows]; cols];

    for i in 0..rows {
        for j in 0..cols {
            result[j][i] = a[i][j];
        }
    }

    result
}

fn main() {
    let a = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
    let b = vec![vec![5.0, 6.0], vec![7.0, 8.0]];

    if let Some(c) = matrix_multiply(&a, &b) {
        println!("矩阵乘法:");
        for row in &c {
            println!("  {:?}", row);
        }
        // [19, 22]
        // [43, 50]
    }

    let t = matrix_transpose(&a);
    println!("转置: {:?}", t);
    // [[1, 3], [2, 4]]
}

八、抽象代数

抽象代数(Abstract Algebra)研究代数结构的一般性质,是代数学的最高层次。

8.1 重要概念

结构定义示例
群(Group)一个集合 + 一个满足封闭性、结合律、有单位元、有逆元的运算整数加法群 $(\mathbb{Z}, +)$
环(Ring)一个群 + 满足分配律的第二种运算整数环 $(\mathbb{Z}, +, \times)$
域(Field)一个环 + 第二种运算有逆元(除零外)有理数域 $(\mathbb{Q}, +, \times)$

8.2 群在密码学中的应用

群论是现代密码学的数学基础。许多密码算法的安全性依赖于群上的困难问题:

  • RSA 加密:基于整数乘法群中大整数分解的困难性
  • 椭圆曲线密码:基于椭圆曲线群上离散对数问题的困难性
  • Diffie-Hellman 密钥交换:基于有限域中离散对数问题的困难性

抽象代数看似抽象,但它支撑着我们日常使用的加密通信、数字签名等安全技术。


九、代数应用实例

9.1 等额本息贷款计算

问题:贷款 10 万元,年利率 5%,分 60 个月还清,计算每月还款金额。

等额本息月还款公式:

$$ A = P \cdot \frac{i(1+i)^n}{(1+i)^n - 1} $$

其中 $A$ 为每月还款金额,$P$ 为贷款本金,$i$ 为月利率,$n$ 为还款月数。

fn monthly_payment(principal: f64, annual_rate: f64, months: u32) -> f64 {
    let monthly_rate = annual_rate / 12.0;
    let n = months as f64;

    principal * monthly_rate * (1.0 + monthly_rate).powf(n)
        / ((1.0 + monthly_rate).powf(n) - 1.0)
}

fn main() {
    let p = 100_000.0;      // 贷款本金 10 万元
    let rate = 0.05;        // 年利率 5%
    let months = 60;        // 60 个月

    let payment = monthly_payment(p, rate, months);
    println!("每月还款: {:.2} 元", payment);
    println!("总还款: {:.2} 元", payment * months as f64);
    println!("总利息: {:.2} 元", payment * months as f64 - p);
}

9.2 楼梯问题

一段楼梯有 10 级,每次只能走 1 级或 2 级,有多少种上楼方法?

答案就是 $F_{11} = 89$ 种(斐波那契数列的第 11 项)。因为到达第 n 级的方法数等于到达第 n-1 级和第 n-2 级方法数之和。

fn climb_stairs(n: u32) -> u64 {
    if n <= 2 { return n as u64; }

    let (mut prev, mut curr) = (1u64, 2u64);
    for _ in 3..=n {
        let next = prev + curr;
        prev = curr;
        curr = next;
    }
    curr
}

fn main() {
    println!("10 级楼梯的走法: {} 种", climb_stairs(10));  // 89
}

十、学习资源

资源说明
Desmos在线图形计算器,直观理解代数函数
3Blue1Brown - 线性代数的本质线性代数可视化教程
Khan Academy - 代数系统的代数学习课程

十一、总结

主题核心内容
一元二次方程求根公式、判别式、韦达定理
数列等差、等比、斐波那契、调和数列
黄金分割数$\phi \approx 1.618$,与斐波那契数列的关系
欧拉数$e \approx 2.718$,欧拉恒等式
最大公约数辗转相除法(欧几里得算法)
线性代数向量、矩阵运算
抽象代数群、环、域,密码学的数学基础

代数是连接算术与高等数学的桥梁。从简单的一元二次方程到抽象的群论,代数的思想贯穿了计算机科学的方方面面。

练习建议:

  1. 实现一个支持复数运算的一元二次方程求解器
  2. 用 Rust 实现矩阵的行列式计算
  3. 探索椭圆曲线上的点加法运算

第二十八 几何

几何(Geometry)是数学中最古老的分支之一,研究空间中点、线、面、体的形状、大小、位置关系以及变换规律。坐标系的出现将代数与几何连接在一起,使我们能够用方程描述几何图形,用计算解决几何问题。


一、基本几何图形

几何研究的对象可以从维度来分类:

维度对象示例
0 维坐标点 (x, y)
1 维线直线、曲线、抛物线、双曲线
2 维三角形、矩形、圆、椭圆
3 维正方体、圆柱体、球、圆锥

二、平面几何公式

2.1 三角形

面积公式(已知两边和夹角):

$$S = \frac{1}{2}ab\sin\theta$$

海伦公式(已知三边长):

$$S = \sqrt{p(p-a)(p-b)(p-c)} \quad \left(p = \frac{a+b+c}{2}\right)$$

fn triangle_area_sides(a: f64, b: f64, c: f64) -> f64 {
    let p = (a + b + c) / 2.0;
    (p * (p - a) * (p - b) * (p - c)).sqrt()
}

fn triangle_area_angle(a: f64, b: f64, angle_rad: f64) -> f64 {
    0.5 * a * b * angle_rad.sin()
}

fn main() {
    // 海伦公式:边长为 3, 4, 5 的三角形
    let area = triangle_area_sides(3.0, 4.0, 5.0);
    println!("海伦公式: 面积 = {}", area);  // 6.0

    // 两边夹角:边长 3 和 4,夹角 90°
    let area2 = triangle_area_angle(3.0, 4.0, std::f64::consts::FRAC_PI_2);
    println!("两边夹角: 面积 = {}", area2);  // 6.0
}

2.2 四边形

图形面积公式周长公式
矩形$S = ab$$C = 2(a+b)$
正方形$S = a^2$$C = 4a$
平行四边形$S = ah$($h$ 为高)$C = 2(a+b)$
梯形$S = \frac{1}{2}(a+b)h$

2.3 圆

面积: $S = \pi r^2$

周长: $C = 2\pi r$

2.4 扇形

面积: $S = \frac{1}{2}r^2\theta$($\theta$ 为弧度)

弧长: $l = r\theta$

2.5 椭圆

标准方程: $\frac{x^2}{a^2} + \frac{y^2}{b^2} = 1 \quad (a > 0, b > 0)$

面积: $S = \pi ab$

周长(拉马努金第二近似公式):

$$ L \approx \pi(a + b)\left(1 + \frac{3h}{10 + \sqrt{4 - 3h}}\right) \quad h = \frac{(a-b)^2}{(a+b)^2} $$

fn ellipse_area(a: f64, b: f64) -> f64 {
    std::f64::consts::PI * a * b
}

fn ellipse_perimeter(a: f64, b: f64) -> f64 {
    let h = ((a - b) * (a - b)) / ((a + b) * (a + b));
    std::f64::consts::PI * (a + b) * (1.0 + 3.0 * h / (10.0 + (4.0 - 3.0 * h).sqrt()))
}

fn main() {
    let a = 5.0;
    let b = 3.0;
    println!("椭圆面积: {:.4}", ellipse_area(a, b));       // 47.1239
    println!("椭圆周长: {:.4}", ellipse_perimeter(a, b));   // ≈ 25.5267
}

三、立体几何公式

图形体积公式表面积公式
正方体$V = a^3$$S = 6a^2$
长方体$V = abc$$S = 2(ab+bc+ac)$
圆柱体$V = \pi r^2 h$$S = 2\pi r(r+h)$
圆锥体$V = \frac{1}{3}\pi r^2 h$$S = \pi r(r+l)$($l$ 为母线长)
球体$V = \frac{4}{3}\pi r^3$$S = 4\pi r^2$
use std::f64::consts::PI;

fn sphere_volume(r: f64) -> f64 {
    4.0 / 3.0 * PI * r.powi(3)
}

fn sphere_surface(r: f64) -> f64 {
    4.0 * PI * r.powi(2)
}

fn cylinder_volume(r: f64, h: f64) -> f64 {
    PI * r.powi(2) * h
}

fn cone_volume(r: f64, h: f64) -> f64 {
    1.0 / 3.0 * PI * r.powi(2) * h
}

fn main() {
    let r = 5.0;
    println!("球体体积 (r=5): {:.4}", sphere_volume(r));   // 523.5988
    println!("球体表面积 (r=5): {:.4}", sphere_surface(r));  // 314.1593
    println!("圆柱体体积 (r=3, h=10): {:.4}", cylinder_volume(3.0, 10.0));  // 282.7433
    println!("圆锥体体积 (r=3, h=10): {:.4}", cone_volume(3.0, 10.0));      // 94.2478
}

四、坐标系——连接代数和几何的桥梁

坐标系是连接代数和几何的桥梁。通过坐标系,我们可以用代数方程描述几何图形,用计算方法解决几何问题。

4.1 直线

一般式: $ax + by + c = 0$

斜截式: $y = mx + b$($m$ 为斜率,$b$ 为 y 轴截距)

点斜式: $y - y_0 = m(x - x_0)$

点到直线的距离:

$$ d = \frac{|ax_0 + by_0 + c|}{\sqrt{a^2 + b^2}} $$

fn point_to_line_distance(px: f64, py: f64, a: f64, b: f64, c: f64) -> f64 {
    (a * px + b * py + c).abs() / (a * a + b * b).sqrt()
}

fn main() {
    // 点 (1, 2) 到直线 3x + 4y + 5 = 0 的距离
    let d = point_to_line_distance(1.0, 2.0, 3.0, 4.0, 5.0);
    println!("点到直线的距离: {:.4}", d);  // 3.6
}

4.2 圆

标准方程: $(x - h)^2 + (y - k)^2 = r^2$

其中 $(h, k)$ 为圆心,$r$ 为半径。

4.3 抛物线

标准方程: $y = ax^2 + bx + c \quad (a \neq 0)$

顶点坐标: $\left(-\frac{b}{2a}, \frac{4ac - b^2}{4a}\right)$

开口方向: $a > 0$ 向上,$a < 0$ 向下

4.4 双曲线

$$ \frac{x^2}{a^2} - \frac{y^2}{b^2} = 1 \quad (a > 0, b > 0) $$

4.5 悬链线

悬链线是一条柔软、不可伸长的链条在重力作用下自然下垂所形成的曲线:

$$ y = a \cosh\left(\frac{x}{a}\right) = a \cdot \frac{e^{x/a} + e^{-x/a}}{2} \quad (a > 0) $$

悬链线不是抛物线!这是一个常见的误解。抛物线是抛射物体的轨迹,而悬链线是悬挂链条的形状。

4.6 椭圆曲线

椭圆曲线在密码学中有重要应用:

$$ y^2 = x^3 + ax + b $$

椭圆曲线上的点构成一个阿贝尔群,其上的离散对数问题(ECDLP)是椭圆曲线密码学(ECC)的安全基础。


五、几何变换

几何变换是线性代数在几何中的应用,通过矩阵乘法实现图形的变换。

5.1 常见变换

变换矩阵说明
旋转(Rotate)$\begin{pmatrix} \cos\theta & -\sin\theta \ \sin\theta & \cos\theta \end{pmatrix}$绕原点旋转角度 $\theta$
缩放(Scale)$\begin{pmatrix} s_x & 0 \ 0 & s_y \end{pmatrix}$x 方向缩放 $s_x$ 倍,y 方向缩放 $s_y$ 倍
剪切(Shear)$\begin{pmatrix} 1 & k \ 0 & 1 \end{pmatrix}$水平剪切
反射(Reflect)$\begin{pmatrix} -1 & 0 \ 0 & 1 \end{pmatrix}$关于 y 轴对称
fn rotate_point(x: f64, y: f64, angle_rad: f64) -> (f64, f64) {
    let cos_a = angle_rad.cos();
    let sin_a = angle_rad.sin();
    (x * cos_a - y * sin_a, x * sin_a + y * cos_a)
}

fn scale_point(x: f64, y: f64, sx: f64, sy: f64) -> (f64, f64) {
    (x * sx, y * sy)
}

fn main() {
    let (x, y) = (1.0, 0.0);

    // 旋转 90°(π/2)
    let (rx, ry) = rotate_point(x, y, std::f64::consts::FRAC_PI_2);
    println!("旋转 90°: ({:.4}, {:.4})", rx, ry);  // (0, 1)

    // 旋转 45°(π/4)
    let (rx, ry) = rotate_point(x, y, std::f64::consts::FRAC_PI_4);
    println!("旋转 45°: ({:.4}, {:.4})", rx, ry);  // (0.7071, 0.7071)

    // 缩放 2 倍
    let (sx, sy) = scale_point(x, y, 2.0, 2.0);
    println!("缩放 2 倍: ({:.4}, {:.4})", sx, sy);  // (2, 0)
}

5.2 行列式的几何意义

行列式的几何意义是矩阵变换后面积(或体积)的缩放倍数:

  • $2 \times 2$ 矩阵的行列式 = 变换后的面积缩放倍数
  • $3 \times 3$ 矩阵的行列式 = 变换后的体积缩放倍数
  • 行列式为 0 表示变换后维度降低(如二维图形被压缩成一条线)

5.3 特征值与特征向量

特征值和特征向量描述了线性变换中“方向不变“的量:

$$ A\vec{v} = \lambda\vec{v} $$

其中 $\lambda$ 是特征值,$\vec{v}$ 是特征向量。特征值表示在该方向上的缩放倍数。


六、用 Rust 绘制几何图形

使用 plotters 库可以在 Rust 中绘制各种几何图形。

6.1 绘制抛物线

use plotters::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let root = BitMapBackend::new("plot.png", (640, 480)).into_drawing_area();
    root.fill(&WHITE)?;

    let mut chart = ChartBuilder::on(&root)
        .caption("y = x²", ("sans-serif", 50).into_font())
        .margin(5)
        .x_label_area_size(30)
        .y_label_area_size(30)
        .build_cartesian_2d(-1f32..1f32, -0.1f32..1f32)?;

    chart.configure_mesh().draw()?;

    chart
        .draw_series(LineSeries::new(
            (-50..=50).map(|x| x as f32 / 50.0).map(|x| (x, x * x)),
            &RED,
        ))?
        .label("y = x²")
        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED));

    chart
        .configure_series_labels()
        .background_style(&WHITE.mix(0.8))
        .border_style(&BLACK)
        .draw()?;

    root.present()?;
    Ok(())
}

6.2 几何赏析

一些优美的数学曲线方程:

$$ \sin(x^2) = \sin(y^2) $$

更多 plotters 示例请参考 GitHub 仓库


七、几何与代数的联系

几何直观可以帮助我们理解代数概念:

代数概念几何直观
向量线性相关向量指向同一方向或相反方向
向量线性无关向量指向不同方向,张成空间
行列式面积或体积的缩放倍数
逆矩阵将变换“撤销“回去
列空间变换后所有可能到达的位置
零空间被压缩到原点的所有向量
秩(Rank)变换后空间的维数
特征值在特定方向上的缩放倍数
特征向量变换后方向不变的向量

推荐观看 3Blue1Brown - 线性代数的本质 系列,以几何直觉理解线性代数的核心概念。


八、学习资源

资源说明
3Blue1Brown 官网数学可视化讲解
3Blue1Brown B站中文字幕版
微积分的本质微积分可视化系列
Desmos在线图形计算器

九、总结

主题核心内容
平面几何三角形、四边形、圆、椭圆的面积与周长公式
立体几何球体、圆柱体、圆锥体的体积与表面积公式
坐标系直线、圆、抛物线、双曲线、椭圆曲线的方程
几何变换旋转、缩放、剪切、反射
线性代数与几何行列式、特征值、秩的几何意义

几何是数学中最直观的分支。通过坐标系,代数与几何融为一体,使我们既能用方程精确描述图形,又能用图形直观理解方程。

练习建议:

  1. 用 Rust 实现点到直线的距离计算
  2. 用 plotters 绘制正弦曲线和余弦曲线
  3. 实现二维向量的旋转、缩放和反射变换

第二十九 行列式(Determinant)

行列式(Determinant)是线性代数中最重要的概念之一。它是一个可以从方阵中计算得到的标量值,记作 $\det(A)$ 或 $|A|$。行列式不仅具有重要的代数意义,还有深刻的几何意义:$n$ 阶方阵的行列式表示该矩阵所对应的线性变换对 $n$ 维空间体积的缩放倍数。


一、行列式的定义

1.1 一阶行列式

$$|A| = a_{11}$$

1.2 二阶行列式

$$|A| = \begin{vmatrix} a_{11} & a_{12} \ a_{21} & a_{22} \end{vmatrix} = a_{11}a_{22} - a_{12}a_{21}$$

对角线法则:

  • 主对角线(左上到右下):$a_{11}a_{22}$,取正号
  • 副对角线(右上到左下):$a_{12}a_{21}$,取负号
fn det2(a11: f64, a12: f64, a21: f64, a22: f64) -> f64 {
    a11 * a22 - a12 * a21
}

fn main() {
    // | 1  2 |
    // | 3  4 |
    let det = det2(1.0, 2.0, 3.0, 4.0);
    println!("二阶行列式 = {}", det);  // 1*4 - 2*3 = -2
}

1.3 三阶行列式

$$|A| = \begin{vmatrix} a_{11} & a_{12} & a_{13} \ a_{21} & a_{22} & a_{23} \ a_{31} & a_{32} & a_{33} \end{vmatrix}$$

$$= a_{11}a_{22}a_{33} + a_{12}a_{23}a_{31} + a_{13}a_{21}a_{32} - a_{13}a_{22}a_{31} - a_{12}a_{21}a_{33} - a_{11}a_{23}a_{32}$$

记忆口诀: 三条主对角线方向乘积之和减去三条副对角线方向乘积之和(萨吕法则)。

fn det3(a: [[f64; 3]; 3]) -> f64 {
    a[0][0] * a[1][1] * a[2][2]
        + a[0][1] * a[1][2] * a[2][0]
        + a[0][2] * a[1][0] * a[2][1]
        - a[0][2] * a[1][1] * a[2][0]
        - a[0][1] * a[1][0] * a[2][2]
        - a[0][0] * a[1][2] * a[2][1]
}

fn main() {
    let a = [[1.0, 2.0, 3.0],
             [4.0, 5.0, 6.0],
             [7.0, 8.0, 9.0]];
    println!("三阶行列式 = {}", det3(a));  // 0
}

1.4 n 阶行列式的完全展开式

$$\det(A) = \sum_{\sigma \in S_n} \text{sgn}(\sigma) \cdot a_{1,\sigma(1)} \cdot a_{2,\sigma(2)} \cdots a_{n,\sigma(n)}$$

其中 $S_n$ 表示所有 $n$ 个元素的排列,$\text{sgn}(\sigma)$ 是排列 $\sigma$ 的符号(偶排列为 $+1$,奇排列为 $-1$)。

对于 $n$ 阶矩阵,展开式共有 $n!$ 项。当 $n$ 较大时,直接展开计算效率极低。


二、排列与逆序数

2.1 排列

$n$ 个不同元素的全排列共有 $n!$ 种。

2.2 逆序数

在一个排列中,如果一对数的前后位置与大小顺序相反(即前面的数大于后面的数),则称这对数构成一个逆序。一个排列中逆序的总数称为逆序数

$$I(\sigma) = \sum_{i=1}^{n} \sum_{j=i+1}^{n} \begin{cases} 1 & \text{if } \sigma_i > \sigma_j \ 0 & \text{otherwise} \end{cases}$$

  • 偶排列:逆序数为偶数,符号为 $+1$
  • 奇排列:逆序数为奇数,符号为 $-1$
fn inversion_count(perm: &[usize]) -> usize {
    let mut count = 0;
    for i in 0..perm.len() {
        for j in (i + 1)..perm.len() {
            if perm[i] > perm[j] {
                count += 1;
            }
        }
    }
    count
}

fn main() {
    let perm = [3, 1, 4, 2];
    let inv = inversion_count(&perm);
    println!("排列 {:?} 的逆序数 = {}", perm, inv);  // 4
    println!("符号 = {}", if inv % 2 == 0 { "+1" } else { "-1" });
}

2.3 对换

对换:一个排列中的任意两个元素位置对调,其余元素不动。

定理:一个排列中的任意两个元素对换,排列的奇偶性改变。

推论:奇排列对换成标准排列的对换次数为奇数,偶排列对换成标准排列的对换次数为偶数。


三、行列式的性质

行列式具有以下重要性质:

性质说明
转置不变性$\det(A^T) = \det(A)$
行交换变号交换两行,行列式变号
行相等为零若两行(列)相等,行列式为 0
行成比例为零若两行(列)成比例,行列式为 0
数乘一行某行乘以 $k$,行列式变为 $k$ 倍
行倍加不变某行的 $k$ 倍加到另一行,行列式不变
行列可加性某行元素为两数之和,可拆分为两个行列式之和
乘积性质$\det(AB) = \det(A) \cdot \det(B)$
幂性质$\det(A^m) = (\det(A))^m$
数乘矩阵$\det(kA) = k^n \det(A)$($n$ 为阶数)
fn det2(a11: f64, a12: f64, a21: f64, a22: f64) -> f64 {
    a11 * a22 - a12 * a21
}

fn main() {
    // 验证转置不变性
    let det_a = det2(1.0, 2.0, 3.0, 4.0);
    let det_at = det2(1.0, 3.0, 2.0, 4.0);  // 转置
    println!("det(A) = {}, det(A^T) = {}", det_a, det_at);
    assert_eq!(det_a, det_at);  // 两者相等

    // 验证交换两行变号
    let det_swapped = det2(3.0, 4.0, 1.0, 2.0);
    println!("det(A) = {}, det(交换行) = {}", det_a, det_swapped);
    assert_eq!(det_a, -det_swapped);  // 互为相反数
}

四、余子式与代数余子式

4.1 余子式

余子式 $M_{ij}$:划去第 $i$ 行和第 $j$ 列后,剩下的 $(n-1)$ 阶矩阵的行列式。

4.2 代数余子式

代数余子式 $C_{ij}$:

$$C_{ij} = (-1)^{i+j} M_{ij}$$

4.3 拉普拉斯展开(按行/列展开)

行列式可以按任意一行或一列展开:

$$\det(A) = \sum_{j=1}^{n} a_{ij} C_{ij} \quad \text{(按第 } i \text{ 行展开)}$$

$$\det(A) = \sum_{i=1}^{n} a_{ij} C_{ij} \quad \text{(按第 } j \text{ 列展开)}$$

// 递归计算 n 阶行列式(拉普拉斯展开)
fn determinant(matrix: &Vec<Vec<f64>>) -> f64 {
    let n = matrix.len();
    if n == 1 {
        return matrix[0][0];
    }
    if n == 2 {
        return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
    }

    let mut det = 0.0;
    for j in 0..n {
        // 计算余子式
        let mut minor = Vec::new();
        for i in 1..n {
            let mut row = Vec::new();
            for k in 0..n {
                if k != j {
                    row.push(matrix[i][k]);
                }
            }
            minor.push(row);
        }

        let sign = if j % 2 == 0 { 1.0 } else { -1.0 };
        det += sign * matrix[0][j] * determinant(&minor);
    }
    det
}

fn main() {
    let a = vec![
        vec![1.0, 2.0, 3.0],
        vec![0.0, 4.0, 5.0],
        vec![0.0, 0.0, 6.0],
    ];
    println!("行列式 = {}", determinant(&a));  // 1*4*6 = 24
}

五、特殊行列式

5.1 上三角/下三角行列式

上三角或下三角矩阵的行列式等于主对角线元素的乘积:

$$\begin{vmatrix} a_{11} & a_{12} & \cdots & a_{1n} \ 0 & a_{22} & \cdots & a_{2n} \ \vdots & \vdots & \ddots & \vdots \ 0 & 0 & \cdots & a_{nn} \end{vmatrix} = a_{11} \cdot a_{22} \cdots a_{nn}$$

这是计算行列式最常用的方法:通过初等行变换将矩阵化为上三角形式,然后求对角线元素的乘积。

5.2 范德蒙行列式

$$V_n = \begin{vmatrix} 1 & 1 & \cdots & 1 \ x_1 & x_2 & \cdots & x_n \ x_1^2 & x_2^2 & \cdots & x_n^2 \ \vdots & \vdots & \ddots & \vdots \ x_1^{n-1} & x_2^{n-1} & \cdots & x_n^{n-1} \end{vmatrix} = \prod_{1 \le i < j \le n} (x_j - x_i)$$

fn vandermonde_determinant(x: &[f64]) -> f64 {
    let n = x.len();
    let mut det = 1.0;
    for i in 0..n {
        for j in (i + 1)..n {
            det *= x[j] - x[i];
        }
    }
    det
}

fn main() {
    let x = vec![1.0, 2.0, 3.0];
    println!("范德蒙行列式 = {}", vandermonde_determinant(&x));
    // (2-1)(3-1)(3-2) = 2
}

5.3 反对称行列式

若矩阵满足 $a_{ij} = -a_{ji}$(主对角线元素全为 0,对称位置元素互为相反数),则称为反对称矩阵

定理:$n$ 阶反对称行列式,当 $n$ 为奇数时,行列式的值为 0。


六、克莱姆法则

克莱姆法则(Cramer’s Rule)是利用行列式求解线性方程组的方法。

对于 $n$ 元线性方程组 $Ax = b$,若系数矩阵 $A$ 的行列式 $|A| \neq 0$,则方程组有唯一解:

$$x_i = \frac{|A_i|}{|A|}$$

其中 $A_i$ 是将 $A$ 的第 $i$ 列替换为 $b$ 后得到的矩阵。

// 用克莱姆法则解二元线性方程组
// a11*x + a12*y = b1
// a21*x + a22*y = b2
fn cramer2(a11: f64, a12: f64, a21: f64, a22: f64, b1: f64, b2: f64) -> Option<(f64, f64)> {
    let det_a = det2(a11, a12, a21, a22);
    if det_a == 0.0 {
        return None;  // 无解或无穷多解
    }

    let det_x = det2(b1, a12, b2, a22);
    let det_y = det2(a11, b1, a21, b2);

    Some((det_x / det_a, det_y / det_a))
}

fn main() {
    // 鸡兔同笼问题:
    // 头共 35 个,脚共 94 只
    // x + y = 35
    // 2x + 4y = 94
    match cramer2(1.0, 1.0, 2.0, 4.0, 35.0, 94.0) {
        Some((x, y)) => println!("鸡: {} 只, 兔: {} 只", x, y),  // 鸡 23, 兔 12
        None => println!("方程组无解或有无穷多解"),
    }
}

七、行列式的几何意义

维度几何意义
2 阶平行四边形的面积
3 阶平行六面体的体积
n 阶n 维平行多面体的“体积“

行列式的符号表示定向:

  • 正值:保持空间的定向(右手系)
  • 负值:反转空间的定向(左手系)
  • 零值:矩阵将空间压缩到低维(不可逆)
fn parallelogram_area(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
    det2(x1, y1, x2, y2).abs()
}

fn main() {
    // 由向量 (3, 0) 和 (0, 4) 张成的平行四边形
    let area = parallelogram_area(3.0, 0.0, 0.0, 4.0);
    println!("平行四边形面积 = {}", area);  // 12
}

八、使用 nalgebra 计算行列式

use nalgebra::Matrix3;

fn main() {
    let a = Matrix3::new(
        1.0, 2.0, 3.0,
        4.0, 5.0, 6.0,
        7.0, 8.0, 10.0,
    );

    let det = a.determinant();
    println!("行列式 = {}", det);  // -3
}

九、总结

概念要点
二阶行列式$a_{11}a_{22} - a_{12}a_{21}$
三阶行列式萨吕法则(对角线法则)
逆序数排列中逆序对的总数,决定符号
余子式划去一行一列后的子矩阵行列式
代数余子式$(-1)^{i+j} M_{ij}$
拉普拉斯展开按行/列展开为低阶行列式之和
三角行列式等于主对角线元素乘积
范德蒙行列式所有 $(x_j - x_i)$ 的乘积
克莱姆法则$x_i =
几何意义面积/体积的缩放倍数

行列式是判断矩阵是否可逆的关键指标:$|A| \neq 0$ 当且仅当 $A$ 可逆。它在求解线性方程组、计算特征值、判断线性相关性等方面都有重要应用。

练习建议:

  1. 实现一个通用的 $n$ 阶行列式计算函数(使用拉普拉斯展开)
  2. 验证行列式的各种性质
  3. 用克莱姆法则求解三元线性方程组

第三十 矩阵(Matrix)

矩阵(Matrix)是线性代数的核心概念,是由 $m \times n$ 个数排成的 $m$ 行 $n$ 列的矩形数表。矩阵不仅是描述线性变换的工具,也是解决线性方程组、数据分析、机器学习等问题的基础。


一、矩阵的基本概念

1.1 矩阵的定义

一个 $m \times n$ 矩阵 $A$ 可以表示为:

$$A = \begin{pmatrix} a_{11} & a_{12} & \cdots & a_{1n} \ a_{21} & a_{22} & \cdots & a_{2n} \ \vdots & \vdots & \ddots & \vdots \ a_{m1} & a_{m2} & \cdots & a_{mn} \end{pmatrix}$$

其中 $a_{ij}$ 表示第 $i$ 行第 $j$ 列的元素。

1.2 特殊矩阵

矩阵类型定义示例
零矩阵所有元素为 0$O$
单位矩阵主对角线为 1,其余为 0$I_n$
对角矩阵非主对角线元素全为 0$\text{diag}(d_1, d_2, \ldots, d_n)$
数量矩阵单位矩阵的常数倍$kI$
上三角矩阵主对角线下方全为 0
下三角矩阵主对角线上方全为 0
对称矩阵$A^T = A$
反对称矩阵$A^T = -A$
正交矩阵$A^T A = I$旋转矩阵
use nalgebra as na;
use na::{Matrix2, Matrix3};

fn main() {
    // 单位矩阵
    let i2 = Matrix2::<f64>::identity();
    println!("单位矩阵 I₂:\n{}", i2);

    // 对角矩阵
    let diag: Matrix3<f64> = Matrix3::from_diagonal(&na::Vector3::new(1.0, 2.0, 3.0));
    println!("对角矩阵 diag:\n{}", diag);

    // 零矩阵
    let zero: Matrix2<f64> = Matrix2::zeros();
    println!("零矩阵 zero:\n{}", zero);
}

二、矩阵的基本运算

2.1 矩阵的加法与减法

两个同型矩阵(行数和列数相同)可以相加或相减,对应元素相加减:

$$C[i,j] = A[i,j] + B[i,j]$$

$$C[i,j] = A[i,j] - B[i,j]$$

2.2 矩阵的数乘

矩阵与标量相乘,每个元素都乘以该标量:

$$(kA)[i,j] = k \cdot A[i,j]$$

2.3 矩阵的乘法

矩阵乘法是线性代数中最重要的运算。设 $A$ 是 $m \times n$ 矩阵,$B$ 是 $n \times p$ 矩阵,则 $AB$ 是 $m \times p$ 矩阵:

$$C[i,j] = \sum_{k=1}^{n} A[i,k] \cdot B[k,j]$$

注意: 矩阵乘法不满足交换律,即一般情况下 $AB \neq BA$。

fn matrix_multiply(a: &Vec<Vec<f64>>, b: &Vec<Vec<f64>>) -> Option<Vec<Vec<f64>>> {
    let rows_a = a.len();
    let cols_a = a[0].len();
    let cols_b = b[0].len();

    if cols_a != b.len() {
        return None;
    }

    let mut result = vec![vec![0.0; cols_b]; rows_a];
    for i in 0..rows_a {
        for j in 0..cols_b {
            for k in 0..cols_a {
                result[i][j] += a[i][k] * b[k][j];
            }
        }
    }
    Some(result)
}

fn main() {
    let a = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
    let b = vec![vec![5.0, 6.0], vec![7.0, 8.0]];

    if let Some(c) = matrix_multiply(&a, &b) {
        println!("A × B =");
        for row in &c {
            println!("  {:?}", row);
        }
        // [19, 22]
        // [43, 50]
    }
}

2.4 矩阵乘法的性质

性质说明
结合律$(AB)C = A(BC)$
分配律$A(B+C) = AB + AC$
单位元$AI = IA = A$
不满足交换律一般 $AB \neq BA$
转置性质$(AB)^T = B^T A^T$

2.5 矩阵的转置

将矩阵的行和列互换:

$$(A^T){ij} = A{ji}$$

转置的性质:

  • $(A^T)^T = A$
  • $(A + B)^T = A^T + B^T$
  • $(kA)^T = kA^T$
  • $(AB)^T = B^T A^T$
  • $(A^{-1})^T = (A^T)^{-1}$
fn transpose(matrix: &Vec<Vec<f64>>) -> Vec<Vec<f64>> {
    let rows = matrix.len();
    let cols = matrix[0].len();
    let mut result = vec![vec![0.0; rows]; cols];
    for i in 0..rows {
        for j in 0..cols {
            result[j][i] = matrix[i][j];
        }
    }
    result
}

fn main() {
    let a = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
    let at = transpose(&a);
    println!("A 的转置:");
    for row in &at {
        println!("  {:?}", row);
    }
    // [1, 4]
    // [2, 5]
    // [3, 6]
}

三、方阵的行列式

对于 $n$ 阶方阵 $A$,其行列式具有以下性质:

性质公式
转置不变$\det(A^T) = \det(A)$
数乘矩阵$\det(kA) = k^n \det(A)$
乘积性质$\det(AB) = \det(A) \det(B)$
幂性质$\det(A^m) = (\det(A))^m$

四、可逆矩阵

4.1 定义

$n$ 阶方阵 $A$,如果存在 $n$ 阶方阵 $P$,使得 $PA = AP = I$,则称 $P$ 为 $A$ 的逆矩阵,记作 $A^{-1} = P$。

方阵可逆的充要条件: 矩阵的行列式不为零($\det(A) \neq 0$)。

当 $\det(A) = 0$ 时,$A$ 称为奇异矩阵(不可逆);否则称为非奇异矩阵(可逆)。

4.2 逆矩阵的计算公式

$$A^{-1} = \frac{1}{|A|} \cdot A^*$$

其中 $A^*$ 是 $A$ 的伴随矩阵。

4.3 逆矩阵的性质

性质公式
唯一性逆矩阵若存在则唯一
双重逆$(A^{-1})^{-1} = A$
转置的逆$(A^{-1})^T = (A^T)^{-1}$
乘积的逆$(AB)^{-1} = B^{-1} A^{-1}$
数乘的逆$(kA)^{-1} = \frac{1}{k} A^{-1}$
fn inverse_2x2(a: f64, b: f64, c: f64, d: f64) -> Option<[[f64; 2]; 2]> {
    let det = a * d - b * c;
    if det == 0.0 {
        return None;
    }

    Some([
        [d / det, -b / det],
        [-c / det, a / det],
    ])
}

fn main() {
    match inverse_2x2(1.0, 2.0, 3.0, 4.0) {
        Some(inv) => {
            println!("逆矩阵:");
            for row in &inv {
                println!("  {:?}", row);
            }
        }
        None => println!("矩阵不可逆"),
    }
}

五、伴随矩阵

5.1 定义

伴随矩阵 $A^*$(或 $\text{adj}(A)$)是由 $A$ 的代数余子式矩阵转置而来:

$$(A^*){ij} = C{ji} = (-1)^{i+j} M_{ji}$$

5.2 性质

$$AA^* = A^*A = |A|I$$


六、初等矩阵与初等变换

6.1 三种初等行变换

  1. 交换两行:$r_i \leftrightarrow r_j$
  2. 某行乘以非零常数:$r_i \times k$
  3. 某行的 $k$ 倍加到另一行:$r_i + k r_j$

6.2 初等矩阵

由单位矩阵经过一次初等变换得到的矩阵称为初等矩阵

定理:

  • 用一个初等矩阵左乘 $A$,相当于对 $A$ 进行一次相应的初等行变换
  • 用一个初等矩阵右乘 $A$,相当于对 $A$ 进行一次相应的初等列变换

重要推论: 一个矩阵 $A$ 可逆当且仅当它可以表示为一系列初等矩阵的乘积。


七、矩阵的秩

7.1 定义

矩阵的秩(Rank)是矩阵中非零子式的最高阶数,也等于矩阵的行秩(线性无关行的最大个数)或列秩。

7.2 性质

  • $0 \le r(A) \le \min(m, n)$
  • $r(A) = r(A^T)$
  • $r(AB) \le \min(r(A), r(B))$
  • 初等变换不改变矩阵的秩

7.3 秩与线性方程组的解

对于 $n$ 元线性方程组 $Ax = b$:

条件解的情况
$r(A) < r(A, b)$无解
$r(A) = r(A, b) = n$有唯一解
$r(A) = r(A, b) < n$有无穷多解
use nalgebra::Matrix3;

fn main() {
    let a = Matrix3::new(
        1.0, 2.0, 3.0,
        4.0, 5.0, 6.0,
        7.0, 8.0, 9.0,
    );
    println!("矩阵的秩: {}", a.rank(1e-9));  // 2
}

八、特征值与特征向量

8.1 定义

对于 $n$ 阶方阵 $A$,如果存在数 $\lambda$ 和非零向量 $\vec{v}$,使得:

$$A\vec{v} = \lambda\vec{v}$$

则称 $\lambda$ 为 $A$ 的特征值,$\vec{v}$ 为对应的特征向量

特征值和特征向量描述了线性变换中“方向不变“的量。特征值表示在该方向上的缩放倍数。

8.2 特征值的计算

特征值是特征方程 $|\lambda I - A| = 0$ 的根。

8.3 相似矩阵

若存在可逆矩阵 $P$,使得 $P^{-1}AP = B$,则称 $A$ 与 $B$ 相似

相似矩阵的性质:

  • 秩相同
  • 行列式相同
  • 特征值相同
  • 迹相同

8.4 矩阵的对角化

若 $n$ 阶矩阵 $A$ 有 $n$ 个线性无关的特征向量,则 $A$ 可以对角化

$$P^{-1}AP = \Lambda = \text{diag}(\lambda_1, \lambda_2, \ldots, \lambda_n)$$

其中 $P$ 的列是 $A$ 的特征向量,$\Lambda$ 是对角矩阵。

use nalgebra::{Matrix2, Vector2};

fn main() {
    let a = Matrix2::new(3.0, 1.0, 1.0, 3.0);

    // 计算特征值
    let eigen = a.symmetric_eigen();
    println!("特征值: {:?}", eigen.eigenvalues);
    println!("特征向量:\n{}", eigen.eigenvectors);
}

九、正交矩阵与旋转矩阵

9.1 正交矩阵

若方阵 $A$ 满足 $A^T A = I$,则称 $A$ 为正交矩阵

性质:

  • $A^{-1} = A^T$
  • 行(列)向量组为单位正交向量组
  • $\det(A) = \pm 1$

9.2 旋转矩阵

二维旋转矩阵:

$$R(\theta) = \begin{pmatrix} \cos\theta & -\sin\theta \ \sin\theta & \cos\theta \end{pmatrix}$$

性质:

  • 旋转矩阵是正交矩阵
  • $\det(R(\theta)) = \cos^2\theta + \sin^2\theta = 1$
use nalgebra::{Rotation2, Vector2};

fn main() {
    let v = Vector2::new(1.0, 0.0);
    let angle = std::f64::consts::FRAC_PI_4; // 45°

    let rotation = Rotation2::new(angle);
    let rotated = rotation * v;

    println!("原向量: {:?}", v);
    println!("旋转 45° 后: {}", rotated);
    // (0.7071, 0.7071)
}

三维旋转矩阵:

  • 绕 X 轴旋转 θ $$R_x(\theta) = \begin{pmatrix} 1 & 0 & 0 \ 0 & \cos\theta & -\sin\theta \ 0 & \sin\theta & \cos\theta \end{pmatrix}$$

  • 绕 Y 轴旋转 θ $$R_y(\theta) = \begin{pmatrix} \cos\theta & 0 & \sin\theta \ 0 & 1 & 0 \ -\sin\theta & 0 & \cos\theta \end{pmatrix}$$

  • 绕 Z 轴旋转 θ $$R_z(\theta) = \begin{pmatrix} \cos\theta & -\sin\theta & 0 \ \sin\theta & \cos\theta & 0 \ 0 & 0 & 1 \end{pmatrix}$$

use nalgebra::{Rotation3, Vector3, UnitVector3};

fn main() {
    let v = Vector3::new(1.0, 1.0, 1.0);
    let angle = std::f64::consts::FRAC_PI_4; // 45°

    // 旋转轴:Z轴单位向量
    let axis = UnitVector3::new_normalize(Vector3::new(0.0, 0.0, 1.0));
    let rotation = Rotation3::from_axis_angle(&axis, angle);

    let rotated = rotation * v;

    println!("原向量: {:?}", v);
    println!("绕Z轴旋转45°后: {:.4?}", rotated);
}

三维旋转 想象一下是向量在旋转(x轴、y轴不动)

十、矩阵分解

矩阵分解是将复杂矩阵分解为更简单矩阵乘积的方法,在数值计算和机器学习中广泛应用。

10.1 LU 分解

将矩阵分解为下三角矩阵 $L$ 和上三角矩阵 $U$ 的乘积:

$$A = LU$$

LU 分解常用于求解线性方程组。

use nalgebra::{Matrix3, Vector3};

fn main() {
    let a = Matrix3::new(
        5.0, -3.0, -4.0,
        4.0, -10.0, 5.0,
        2.0, 1.0, -10.0,
    );

    let b = Vector3::new(10.0, -20.0, -12.0);

    // LU 分解求解
    let x = a.lu().solve(&b).expect("矩阵奇异");
    println!("解 x = {}", x);
    println!("验证 A*x = {}", a * x);
}

10.2 QR 分解

将矩阵分解为正交矩阵 $Q$ 和上三角矩阵 $R$ 的乘积:

$$A = QR$$

use nalgebra as na;

fn main() {
    let a = na::Matrix3::new(
        1.0, 2.0, 3.0,
        4.0, 5.0, 6.0,
        7.0, 8.0, 10.0,
    );

    let qr = a.qr();
    let q = qr.q();
    let r = qr.r();

    println!("Q:\n{}", q);
    println!("R:\n{}", r);
    println!("验证 Q*R:\n{}", &q * &r);
}

10.3 奇异值分解(SVD)

SVD 是线性代数中最重要的矩阵分解之一,可以应用于任意形状的矩阵:

$$A = U\Sigma V^T$$

其中 $U$ 和 $V$ 是正交矩阵,$\Sigma$ 是对角矩阵(对角线元素为奇异值,非负且按降序排列)。

SVD 是 PCA、最小二乘、图像压缩、推荐系统的数学基础。

use nalgebra::Matrix3;

fn main() {
    let a = Matrix3::new(
        1.0, 2.0, 3.0,
        4.0, 5.0, 6.0,
        7.0, 8.0, 10.0,
    );

    let svd = a.svd(true, true);
    println!("奇异值: {:?}", svd.singular_values);
}

十一、实对称矩阵与二次型

11.1 实对称矩阵的性质

  • 特征值均为实数
  • 特征向量可以取为实向量
  • 一定可以对角化
  • 不同特征值对应的特征向量正交

11.2 二次型

$n$ 元二次型可以表示为:

$$f(x_1, x_2, \ldots, x_n) = \sum_{i=1}^{n} \sum_{j=1}^{n} a_{ij} x_i x_j = \vec{x}^T A \vec{x}$$

其中 $A$ 是实对称矩阵。

11.3 正定矩阵

若对任意非零向量 $\vec{x}$,都有 $\vec{x}^T A \vec{x} > 0$,则称 $A$ 为正定矩阵

判定条件:

  • 所有特征值大于 0
  • 所有顺序主子式大于 0

十二、矩阵的等价关系

关系定义不变量
等价$A \sim B$:存在可逆 $P, Q$ 使 $PAQ = B$
相似$A \sim B$:存在可逆 $P$ 使 $P^{-1}AP = B$秩、行列式、特征值、迹
合同$A \simeq B$:存在可逆 $P$ 使 $P^TAP = B$秩、正/负惯性指数
正交相似存在正交 $Q$ 使 $Q^TAQ = B$秩、行列式、特征值

十三、总结

概念要点
矩阵乘法$C[i,j] = \sum_k A[i,k]B[k,j]$,不满足交换律
转置$(AB)^T = B^T A^T$
逆矩阵$A^{-1} = A^* /
非零子式的最高阶数,初等变换不变
特征值$
对角化$P^{-1}AP = \Lambda$,需 $n$ 个线性无关特征向量
正交矩阵$A^T A = I$,$A^{-1} = A^T$
LU 分解$A = LU$,用于解方程组
QR 分解$A = QR$,$Q$ 正交,$R$ 上三角
SVD$A = U\Sigma V^T$,最通用的分解

矩阵是描述线性变换的代数工具。理解矩阵的各种运算和分解,是掌握线性代数、机器学习、计算机图形学等领域的基础。

练习建议:

  1. 实现矩阵乘法、转置、求逆的基本算法
  2. 用特征值分解判断矩阵是否正定
  3. 使用 SVD 进行图像压缩实验

第三十一 向量(Vector)

向量(Vector)是线性代数的核心概念,也是物理学和工程学中描述方向和大小的基本工具。在计算机科学中,向量是机器学习、计算机图形学、物理模拟等领域的基础数据结构。


一、向量的定义

1.1 什么是向量

n 维向量:n 个有次序的数 $a_1, a_2, a_3, \ldots, a_n$ 所组成的数组称为 n 维向量。这 n 个数称为向量的 n 个分量,第 i 个分量 $a_i$ 为向量的第 i 个分量。

分量全为实数的向量称为实向量,分量中含有复数则称为复向量

向量通常用粗体字母或带箭头的字母表示:

$$\vec{a} = (a_1, a_2, \ldots, a_n)$$

1.2 特殊向量

向量类型定义表示
零向量所有元素都为零$\vec{0} = (0, 0, \ldots, 0)$
单位向量长度为 1 的向量$|\vec{a}| = 1$
负向量元素为对应元素取负数$-\vec{a} = (-a_1, -a_2, \ldots, -a_n)$
标准基向量只有一个分量为 1,其余为 0$\vec{e}_i$

二、向量的基本运算

2.1 向量的加法

两个同维向量相加,对应分量相加:

$$\vec{a} + \vec{b} = (a_1 + b_1, a_2 + b_2, \ldots, a_n + b_n)$$

2.2 向量的数乘

向量与标量相乘,每个分量都乘以该标量:

$$k\vec{a} = (ka_1, ka_2, \ldots, ka_n)$$

2.3 运算律

运算律公式
加法交换律$\vec{u} + \vec{v} = \vec{v} + \vec{u}$
加法结合律$(\vec{u} + \vec{v}) + \vec{w} = \vec{u} + (\vec{v} + \vec{w})$
数乘结合律$k(l\vec{u}) = (kl)\vec{u}$
数乘分配律(对向量)$k(\vec{u} + \vec{v}) = k\vec{u} + k\vec{v}$
数乘分配律(对标量)$(k + l)\vec{u} = k\vec{u} + l\vec{u}$
零向量性质$k\vec{u} = \vec{0} \Leftrightarrow k = 0 \text{ 或 } \vec{u} = \vec{0}$
fn vector_add(a: &[f64], b: &[f64]) -> Vec<f64> {
    a.iter().zip(b.iter()).map(|(x, y)| x + y).collect()
}

fn vector_scale(a: &[f64], k: f64) -> Vec<f64> {
    a.iter().map(|x| x * k).collect()
}

fn main() {
    let a = vec![1.0, 2.0, 3.0];
    let b = vec![4.0, 5.0, 6.0];

    println!("a + b = {:?}", vector_add(&a, &b));     // [5, 7, 9]
    println!("2a = {:?}", vector_scale(&a, 2.0));     // [2, 4, 6]
}

三、向量的内积(点积)

3.1 定义

两个 n 维向量的内积(点积)定义为:

$$\vec{a} \cdot \vec{b} = a_1b_1 + a_2b_2 + \cdots + a_nb_n = \sum_{i=1}^{n} a_ib_i$$

3.2 几何意义

$$\vec{a} \cdot \vec{b} = |\vec{a}| |\vec{b}| \cos\theta$$

其中 $\theta$ 是两个向量之间的夹角。

3.3 内积的性质

性质公式
非负性$\vec{a} \cdot \vec{a} \ge 0$,且等于 0 当且仅当 $\vec{a} = \vec{0}$
对称性$\vec{a} \cdot \vec{b} = \vec{b} \cdot \vec{a}$
线性性$\vec{a} \cdot (\vec{b} + \vec{c}) = \vec{a} \cdot \vec{b} + \vec{a} \cdot \vec{c}$
数乘性$(k\vec{a}) \cdot \vec{b} = k(\vec{a} \cdot \vec{b})$

3.4 求向量夹角

$$\cos\theta = \frac{\vec{a} \cdot \vec{b}}{|\vec{a}| |\vec{b}|}$$

$$\theta = \arccos\left(\frac{\vec{a} \cdot \vec{b}}{|\vec{a}| |\vec{b}|}\right)$$

fn dot_product(a: &[f64], b: &[f64]) -> f64 {
    a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}

fn magnitude(a: &[f64]) -> f64 {
    a.iter().map(|x| x * x).sum::<f64>().sqrt()
}

fn angle_between(a: &[f64], b: &[f64]) -> f64 {
    let dot = dot_product(a, b);
    let mag_a = magnitude(a);
    let mag_b = magnitude(b);
    (dot / (mag_a * mag_b)).acos()
}

fn main() {
    let a = vec![1.0, 0.0];
    let b = vec![0.0, 1.0];

    println!("点积: {}", dot_product(&a, &b));           // 0
    println!("夹角: {:.2} 弧度 = {:.2}°", angle_between(&a, &b), angle_between(&a, &b).to_degrees());
    // 1.57 弧度 = 90°
}

四、向量的叉积

4.1 定义

两个三维向量的叉积定义为:

$$\vec{a} \times \vec{b} = \begin{vmatrix} \vec{i} & \vec{j} & \vec{k} \ a_1 & a_2 & a_3 \ b_1 & b_2 & b_3 \end{vmatrix}$$

$$= (a_2b_3 - a_3b_2, a_3b_1 - a_1b_3, a_1b_2 - a_2b_1)$$

4.2 几何意义

$$|\vec{a} \times \vec{b}| = |\vec{a}| |\vec{b}| \sin\theta$$

叉积的结果是一个向量,其方向垂直于 $\vec{a}$ 和 $\vec{b}$ 所在的平面(右手定则),大小等于以 $\vec{a}$ 和 $\vec{b}$ 为邻边的平行四边形的面积。

fn cross_product(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
    [
        a[1] * b[2] - a[2] * b[1],
        a[2] * b[0] - a[0] * b[2],
        a[0] * b[1] - a[1] * b[0],
    ]
}

fn main() {
    let a = [1.0, 0.0, 0.0];
    let b = [0.0, 1.0, 0.0];

    let c = cross_product(&a, &b);
    println!("a × b = {:?}", c);  // [0, 0, 1]
}

五、向量的长度(模)

5.1 定义

向量 $\vec{a}$ 的长度(模或范数)定义为:

$$|\vec{a}| = \sqrt{\vec{a} \cdot \vec{a}} = \sqrt{a_1^2 + a_2^2 + \cdots + a_n^2}$$

5.2 范数的类型

范数类型公式说明
L1 范数(曼哈顿范数)$|\vec{a}|_1 = \suma_i
L2 范数(欧几里得范数)$|\vec{a}|_2 = \sqrt{\sum a_i^2}$最常用的范数
L∞ 范数(无穷范数)$|\vec{a}|_\infty = \maxa_i
fn l1_norm(a: &[f64]) -> f64 {
    a.iter().map(|x| x.abs()).sum()
}

fn l2_norm(a: &[f64]) -> f64 {
    a.iter().map(|x| x * x).sum::<f64>().sqrt()
}

fn l_inf_norm(a: &[f64]) -> f64 {
    a.iter().map(|x| x.abs()).fold(0.0, f64::max)
}

fn main() {
    let a = vec![3.0, -4.0];
    println!("L1 范数: {}", l1_norm(&a));      // 7
    println!("L2 范数: {}", l2_norm(&a));      // 5
    println!("L∞ 范数: {}", l_inf_norm(&a));  // 4
}

六、余弦相似度

余弦相似度(Cosine Similarity)衡量两个向量在方向上的相似程度,而不关心它们的大小(长度)。它的值只取决于向量之间的夹角。

$$\cos(\theta) = \frac{\vec{a} \cdot \vec{b}}{|\vec{a}| \cdot |\vec{b}|}$$

余弦相似度含义
1方向完全相同
0正交(垂直)
-1方向完全相反
fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
    let dot = dot_product(a, b);
    let mag_a = magnitude(a);
    let mag_b = magnitude(b);
    dot / (mag_a * mag_b)
}

fn main() {
    let doc1 = vec![1.0, 2.0, 1.0, 0.0];  // 文档1的词频向量
    let doc2 = vec![1.0, 2.0, 1.0, 0.0];  // 文档2(与文档1相同)
    let doc3 = vec![0.0, 0.0, 0.0, 1.0];  // 文档3(完全不同)

    println!("doc1 与 doc2 相似度: {:.4}", cosine_similarity(&doc1, &doc2));  // 1.0000
    println!("doc1 与 doc3 相似度: {:.4}", cosine_similarity(&doc1, &doc3));  // 0.0000
}

应用: 余弦相似度在文本检索、推荐系统、图像识别等领域广泛应用,用于衡量两个对象的相似程度。


七、线性组合与线性相关性

7.1 线性组合

给定向量组 $\vec{a}_1, \vec{a}_2, \ldots, \vec{a}_m$,对于任何一组实数 $k_1, k_2, \ldots, k_m$,表达式:

$$k_1\vec{a}_1 + k_2\vec{a}_2 + \cdots + k_m\vec{a}_m$$

称为向量组的线性组合

  • 零向量是任意向量的线性组合(取所有系数为 0)
  • 向量组中的任一向量都可以表示为该向量组的线性组合

7.2 线性相关与线性无关

线性相关:如果存在不全为零的数 $k_1, k_2, \ldots, k_m$,使得:

$$k_1\vec{a}_1 + k_2\vec{a}_2 + \cdots + k_m\vec{a}_m = \vec{0}$$

则称向量组线性相关。

线性无关:只有当 $k_1 = k_2 = \cdots = k_m = 0$ 时上式才成立,则称向量组线性无关。

判定方法:

  • 两个非零向量线性相关 $\Leftrightarrow$ 它们的分量对应成比例
  • 向量组中含有零向量,则该向量组线性相关
  • 向量组中有两个向量成比例,则该向量组线性相关

7.3 极大线性无关组

极大线性无关组是向量组中满足以下条件的子集:

  • 该子集中的向量线性无关
  • 向量组中任意其他向量都可以由该子集线性表示
  • 添加任何其他向量都会使该子集变为线性相关

性质:

  • 同一个向量组的不同极大线性无关组所含向量个数相同
  • 这个个数称为向量组的
  • 向量组与其极大线性无关组等价

八、正交与标准正交向量组

8.1 正交向量

若两个非零向量 $\vec{\alpha}$ 和 $\vec{\beta}$ 的内积为零,即 $(\vec{\alpha}, \vec{\beta}) = 0$,则称它们正交

性质:

  • 正交向量组是线性无关的
  • 正交向量组中不含零向量

8.2 标准正交向量组

一组向量,其中每个向量都是单位向量(长度为 1),且两两正交(内积为 0),称为标准正交向量组(或单位正交向量组)。

8.3 施密特正交化

施密特正交化方法可以将一组线性无关的向量转化为一组正交向量:

$$\vec{\beta}_1 = \vec{\alpha}_1$$

$$\vec{\beta}_2 = \vec{\alpha}_2 - \frac{(\vec{\alpha}_2, \vec{\beta}_1)}{(\vec{\beta}_1, \vec{\beta}_1)}\vec{\beta}_1$$

$$\vec{\beta}_3 = \vec{\alpha}_3 - \frac{(\vec{\alpha}_3, \vec{\beta}_1)}{(\vec{\beta}_1, \vec{\beta}_1)}\vec{\beta}_1 - \frac{(\vec{\alpha}_3, \vec{\beta}_2)}{(\vec{\beta}_2, \vec{\beta}_2)}\vec{\beta}_2$$

一般地: $$ \vec{\beta}_k = \vec{\alpha}k - \sum{j=1}^{k-1} \frac{(\vec{\alpha}_k, \vec{\beta}_j)}{(\vec{\beta}_j, \vec{\beta}_j)}\vec{\beta}_j $$

fn gram_schmidt(vectors: &[Vec<f64>]) -> Vec<Vec<f64>> {
    let mut result = Vec::new();

    for v in vectors {
        let mut u = v.clone();
        for ortho in &result {
            let proj = dot_product(v, ortho) / dot_product(ortho, ortho);
            u = u.iter().zip(ortho.iter()).map(|(a, b)| a - proj * b).collect();
        }
        result.push(u);
    }

    result
}

fn main() {
    let vectors = vec![
        vec![1.0, 1.0, 0.0],
        vec![1.0, 0.0, 1.0],
        vec![0.0, 1.0, 1.0],
    ];

    let orthogonal = gram_schmidt(&vectors);
    println!("正交化结果:");
    for v in &orthogonal {
        println!("  {:?}", v);
    }
}

九、向量与矩阵的关系

矩阵可以对向量进行线性变换,包括拉升、压缩、旋转、剪切、平移等操作。

9.1 旋转变换

use nalgebra::{Rotation2, Vector2};

fn main() {
    let v = Vector2::new(1.0, 0.0);
    let angle = std::f64::consts::FRAC_PI_4; // 45°

    let rotation = Rotation2::new(angle);
    let rotated = rotation * v;

    println!("原向量: {:?}", v);
    println!("旋转 45° 后: {:?}", rotated);
    // (0.7071, 0.7071)
}

9.2 缩放变换

fn scale_vector(v: &[f64], sx: f64, sy: f64) -> Vec<f64> {
    vec![v[0] * sx, v[1] * sy]
}

fn main() {
    let v = vec![1.0, 1.0];
    let scaled = scale_vector(&v, 2.0, 0.5);
    println!("缩放后: {:?}", scaled);  // [2.0, 0.5]
}

十、向量在 Rust 中的应用

10.1 使用 nalgebra

use nalgebra as na;
use na::{Vector3, Point3};

fn main() {
    let a = Vector3::new(1.0, 2.0, 3.0);
    let b = Vector3::new(4.0, 5.0, 6.0);

    // 向量加法
    let c = a + b;
    println!("a + b = {:?}", c);

    // 向量数乘
    let d = a * 2.0;
    println!("2a = {:?}", d);

    // 点积
    let dot = a.dot(&b);
    println!("a · b = {}", dot);  // 32.0

    // 叉积
    let cross = a.cross(&b);
    println!("a × b = {:?}", cross);  // (-3, 6, -3)

    // 范数
    let norm = a.norm();
    println!("|a| = {}", norm);  // 3.7417

    // 归一化
    let unit = a.normalize();
    println!("单位向量 = {:?}", unit);

    // 距离
    let dist = a.metric_distance(&b);
    println!("距离 = {}", dist);

    // 点 (Point) 与向量 (Vector) 的区别
    let p = Point3::new(1.0, 2.0, 3.0);
    let q = Point3::new(4.0, 5.0, 6.0);
    let vec = q - p;  // 结果是 Vector3
    println!("两点间的向量 = {:?}", vec);
}

十一、总结

概念要点
向量定义n 个有次序的数组成的数组
向量加法对应分量相加
向量数乘每个分量乘以标量
点积$ \vec{a} \cdot \vec{b} = \sum a_ib_i = |\vec{a}| |\vec{b}| \cos\theta$
叉积仅定义于三维,结果垂直于原向量所在平面
向量长度$|\vec{a}| = \sqrt{\sum a_i^2}$
余弦相似度$ \cos\theta = \frac{\vec{a} \cdot \vec{b}}{|\vec{a}||\vec{b}|}$
线性相关存在不全为零的系数使线性组合为零向量
正交内积为零,正交向量组线性无关
施密特正交化将线性无关向量组转化为正交向量组

向量是连接代数与几何的桥梁,也是现代计算机科学的基石。从物理中的力和速度,到机器学习中的特征向量,向量的概念无处不在。

练习建议:

  1. 实现向量的各种运算(加法、数乘、点积、叉积)
  2. 用余弦相似度实现简单的文档相似度计算
  3. 实现施密特正交化算法

第三十二 函数(Function)

函数(Function)是数学中最核心的概念之一,描述了两个集合之间的一种对应关系:对于定义域中的每一个输入,都有唯一确定的输出与之对应。函数是微积分的研究对象,也是计算机编程的基本构建块。


一、函数的基本概念

1.1 定义

设 $D$ 是一个非空实数集,如果对于 $D$ 中的每一个 $x$,按照某种对应法则 $f$,都有唯一确定的实数 $y$ 与之对应,则称 $f$ 为定义在 $D$ 上的函数,记作:

$$y = f(x), \quad x \in D$$

其中 $x$ 称为自变量,$y$ 称为因变量,$D$ 称为函数的定义域。

1.2 有界函数与无界函数

有界函数:如果存在正数 $M$,使得对于定义域内的所有 $x$,都有 $|f(x)| \le M$,则称 $f(x)$ 为有界函数。

无界函数:不存在这样的 $M$,则称 $f(x)$ 为无界函数。

fn is_bounded(f: impl Fn(f64) -> f64, a: f64, b: f64, samples: usize) -> Option<f64> {
    let mut max_abs = 0.0_f64;
    for i in 0..=samples {
        let x = a + (b - a) * (i as f64 / samples as f64);
        let y = f(x).abs();
        if y.is_infinite() {
            return None; // 无界
        }
        max_abs = max_abs.max(y);
    }
    Some(max_abs)
}

fn main() {
    // sin(x) 在 [0, 2π] 上有界
    let sin_bound = is_bounded(|x| x.sin(), 0.0, 2.0 * std::f64::consts::PI, 100);
    println!("sin(x) 的最大绝对值: {:?}", sin_bound);  // Some(1.0)

    // 1/x 在 [-1, 1] 上无界
    let inv_bound = is_bounded(|x| 1.0 / x, -1.0, 1.0, 100);
    println!("1/x 有界吗: {:?}", inv_bound);  // None
}

二、函数的基本性质

2.1 奇偶性

奇函数:如果对于定义域内的任意 $x$,都有 $f(-x) = -f(x)$,则称 $f(x)$ 为奇函数。奇函数的图像关于原点对称。

偶函数:如果对于定义域内的任意 $x$,都有 $f(-x) = f(x)$,则称 $f(x)$ 为偶函数。偶函数的图像关于 $y$ 轴对称。

函数奇偶性图像特征
$f(x) = x^3$奇函数关于原点对称
$f(x) = x^2$偶函数关于 $y$ 轴对称
$f(x) = \sin x$奇函数关于原点对称
$f(x) = \cos x$偶函数关于 $y$ 轴对称
$f(x) = e^x$非奇非偶
fn is_odd(f: impl Fn(f64) -> f64, x: f64) -> bool {
    (f(-x) + f(x)).abs() < 1e-10
}

fn is_even(f: impl Fn(f64) -> f64, x: f64) -> bool {
    (f(-x) - f(x)).abs() < 1e-10
}

fn main() {
    let x = 2.0;
    println!("x³ 是奇函数: {}", is_odd(|x| x.powi(3), x));   // true
    println!("x² 是偶函数: {}", is_even(|x| x.powi(2), x));  // true
    println!("sin(x) 是奇函数: {}", is_odd(|x| x.sin(), x)); // true
    println!("cos(x) 是偶函数: {}", is_even(|x| x.cos(), x)); // true
}

2.2 单调性

单调递增:如果对于定义域内的任意 $x_1 < x_2$,都有 $f(x_1) \le f(x_2)$,则称 $f(x)$ 单调递增。

单调递减:如果对于定义域内的任意 $x_1 < x_2$,都有 $f(x_1) \ge f(x_2)$,则称 $f(x)$ 单调递减。

严格单调:上述不等式中的等号不成立时,称为严格单调。

若 $f(x)$ 可导,则:

  • $f’(x) > 0$ $\Rightarrow$ $f(x)$ 严格单调递增
  • $f’(x) < 0$ $\Rightarrow$ $f(x)$ 严格单调递减

2.3 周期性

如果存在非零常数 $T$,使得对于定义域内的任意 $x$,都有 $f(x + T) = f(x)$,则称 $f(x)$ 为周期函数,$T$ 称为周期。

函数周期
$\sin x$, $\cos x$$2\pi$
$\tan x$, $\cot x$$\pi$

2.4 凹凸性

凹函数(下凸):曲线向上弯,任意两点连线在曲线上方。

$$f(\lambda x_1 + (1-\lambda)x_2) \le \lambda f(x_1) + (1-\lambda)f(x_2) \quad (\lambda \in (0,1))$$

凸函数(上凸):曲线向下弯,任意两点连线在曲线下方。

$$f(\lambda x_1 + (1-\lambda)x_2) \ge \lambda f(x_1) + (1-\lambda)f(x_2) \quad (\lambda \in (0,1))$$

若 $f(x)$ 二阶可导,则:

  • $f’’(x) > 0$ $\Rightarrow$ 凹函数(下凸)
  • $f’’(x) < 0$ $\Rightarrow$ 凸函数(上凸)

2.5 连续性

函数 $f(x)$ 在点 $x_0$ 处连续,当且仅当:

$$\lim_{x \to x_0} f(x) = f(x_0)$$

间断点的分类:

类型定义示例
第一类间断点(可去)左右极限存在且相等,但不等于函数值$f(x) = \frac{\sin x}{x}$ 在 $x=0$
第一类间断点(跳跃)左右极限存在但不相等符号函数 $\text{sgn}(x)$ 在 $x=0$
第二类间断点至少一侧极限不存在$f(x) = \frac{1}{x}$ 在 $x=0$
fn is_continuous_at(f: impl Fn(f64) -> f64, x0: f64, epsilon: f64) -> bool {
    let h = 1e-6;
    let left_limit = f(x0 - h);
    let right_limit = f(x0 + h);
    let f_x0 = f(x0);

    (left_limit - f_x0).abs() < epsilon && (right_limit - f_x0).abs() < epsilon
}

fn main() {
    // sin(x)/x 在 x=0 处有可去间断点
    let f = |x: f64| if x == 0.0 { 1.0 } else { x.sin() / x };
    println!("sin(x)/x 在 x=0 连续: {}", is_continuous_at(f, 0.0, 1e-5));
}

三、基本初等函数

3.1 幂函数

$$f(x) = x^a \quad (a \in \mathbb{R})$$

运算法则:

$$a^m \cdot a^n = a^{m+n}$$ $$\frac{a^m}{a^n} = a^{m-n}$$ $$(a^m)^n = a^{mn}$$ $$a^{-n} = \frac{1}{a^n}$$

fn main() {
    let x: f64 = 2.0;
    println!("平方根: {}", x.sqrt());      // 1.4142
    println!("立方: {}", x.powi(3));       // 8
    println!("x^0.5: {}", x.powf(0.5));    // 1.4142
}

3.2 指数函数

$$f(x) = a^x \quad (a > 0, a \neq 1)$$

最常用的指数函数是以 $e$ 为底的指数函数:

$$f(x) = e^x$$

性质:

  • 定义域:$(-\infty, +\infty)$
  • 值域:$(0, +\infty)$
  • 过点 $(0, 1)$
  • $a > 1$ 时单调递增,$0 < a < 1$ 时单调递减
fn main() {
    let x = 2.0_f64;
    println!("e^2 = {}", x.exp());  // 7.3891
    println!("2^3 = {}", 2.0_f64.powf(3.0));  // 8
}

3.3 对数函数

$$f(x) = \log_a x \quad (a > 0, a \neq 1, x > 0)$$

常用对数:

  • 自然对数:$\ln x = \log_e x$
  • 常用对数:$\lg x = \log_{10} x$

运算法则:

$$\log_a(M \cdot N) = \log_a M + \log_a N$$ $$\log_a\left(\frac{M}{N}\right) = \log_a M - \log_a N$$ $$\log_a(M^k) = k \cdot \log_a M$$ $$\log_a b = \frac{\log_c b}{\log_c a} \quad \text{(换底公式)}$$

fn main() {
    let x = 100.0_f64;
    println!("ln(100) = {}", x.ln());      // 4.6052
    println!("log10(100) = {}", x.log10()); // 2
    println!("log2(64) = {}", 64.0_f64.log2()); // 6
}

3.4 三角函数

函数定义周期奇偶性
$\sin x$对边/斜边$2\pi$奇函数
$\cos x$邻边/斜边$2\pi$偶函数
$\tan x$$\sin x / \cos x$$\pi$奇函数
$\cot x$$\cos x / \sin x$$\pi$奇函数
$\sec x$$1 / \cos x$$2\pi$偶函数
$\csc x$$1 / \sin x$$2\pi$奇函数

平方和关系:

$$\sin^2\theta + \cos^2\theta = 1$$ $$1 + \tan^2\theta = \sec^2\theta$$ $$1 + \cot^2\theta = \csc^2\theta$$

两角和差公式:

$$\sin(\alpha \pm \beta) = \sin\alpha\cos\beta \pm \cos\alpha\sin\beta$$ $$\cos(\alpha \pm \beta) = \cos\alpha\cos\beta \mp \sin\alpha\sin\beta$$ $$\tan(\alpha \pm \beta) = \frac{\tan\alpha \pm \tan\beta}{1 \mp \tan\alpha\tan\beta}$$

二倍角公式:

$$\sin 2\theta = 2\sin\theta\cos\theta$$ $$\cos 2\theta = \cos^2\theta - \sin^2\theta = 2\cos^2\theta - 1 = 1 - 2\sin^2\theta$$ $$\tan 2\theta = \frac{2\tan\theta}{1 - \tan^2\theta}$$

半角公式(降幂公式):

$$\sin^2\frac{\theta}{2} = \frac{1 - \cos\theta}{2}$$ $$\cos^2\frac{\theta}{2} = \frac{1 + \cos\theta}{2}$$

辅助角公式:

$$a\sin\theta + b\cos\theta = \sqrt{a^2 + b^2} \sin(\theta + \varphi) \quad \left(\tan\varphi = \frac{b}{a}\right)$$

use std::f64::consts::PI;

fn main() {
    let theta = PI / 6.0; // 30°

    println!("sin(30°) = {}", theta.sin());   // 0.5
    println!("cos(30°) = {}", theta.cos());   // 0.8660
    println!("tan(30°) = {}", theta.tan());   // 0.5774

    // 验证 sin²θ + cos²θ = 1
    let identity = theta.sin().powi(2) + theta.cos().powi(2);
    println!("sin²θ + cos²θ = {}", identity);  // 1.0

    // 勾三股四弦五,求勾所对应的角度
    let opposite = 3.0;
    let hypotenuse = 5.0;
    let angle_rad = (opposite / hypotenuse).asin();
    let angle_deg = angle_rad * 180.0 / PI;
    println!("角度 = {:.2}°", angle_deg);  // 36.87°
}

3.5 反三角函数

函数定义定义域值域
$\arcsin x$$\sin(\arcsin x) = x$$[-1, 1]$$[-\frac{\pi}{2}, \frac{\pi}{2}]$
$\arccos x$$\cos(\arccos x) = x$$[-1, 1]$$[0, \pi]$
$\arctan x$$\tan(\arctan x) = x$$(-\infty, +\infty)$$(-\frac{\pi}{2}, \frac{\pi}{2})$
fn main() {
    let x = 0.5_f64;
    println!("arcsin(0.5) = {:.4} rad = {:.2}°", x.asin(), x.asin().to_degrees());
    println!("arccos(0.5) = {:.4} rad = {:.2}°", x.acos(), x.acos().to_degrees());
    println!("arctan(1.0) = {:.4} rad = {:.2}°", 1.0_f64.atan(), 1.0_f64.atan().to_degrees());
}

四、复合函数与反函数

4.1 复合函数

设 $y = f(u)$,$u = g(x)$,则 $y = f(g(x))$ 称为 $f$ 和 $g$ 的复合函数。

4.2 反函数

如果函数 $f$ 是一一对应的,则存在反函数 $f^{-1}$,满足:

$$f(f^{-1}(x)) = x, \quad f^{-1}(f(x)) = x$$

性质:

  • 函数与其反函数的图像关于直线 $y = x$ 对称
  • 单调函数一定存在反函数
函数反函数
$f(x) = x^3$$f^{-1}(x) = \sqrt[3]{x}$
$f(x) = e^x$$f^{-1}(x) = \ln x$
$f(x) = \sin x$(限制定义域)$f^{-1}(x) = \arcsin x$

五、双曲函数

双曲函数与三角函数类似,但基于指数函数定义:

函数定义恒等式
双曲正弦 $\sinh x$$\frac{e^x - e^{-x}}{2}$$\cosh^2 x - \sinh^2 x = 1$
双曲余弦 $\cosh x$$\frac{e^x + e^{-x}}{2}$
双曲正切 $\tanh x$$\frac{\sinh x}{\cosh x}$
反双曲正弦 $\text{arsinh } x$$\ln(x + \sqrt{x^2 + 1})$
反双曲余弦 $\text{arcosh } x$$\ln(x + \sqrt{x^2 - 1})$($x \ge 1$)
反双曲正切 $\text{artanh } x$$\frac{1}{2}\ln\frac{1+x}{1-x}$($x
fn main() {
    let x = 2.0_f64;

    println!("sinh(2) = {}", x.sinh());    // 3.6269
    println!("cosh(2) = {}", x.cosh());    // 3.7622
    println!("tanh(2) = {}", x.tanh());    // 0.9640

    // 验证恒等式: cosh²(x) - sinh²(x) = 1
    let identity = x.cosh().powi(2) - x.sinh().powi(2);
    println!("恒等式验证: {}", identity);  // 1.0

    // 反双曲函数
    println!("arsinh(2) = {}", x.asinh());  // 1.4436
    println!("arcosh(2) = {}", x.acosh());  // 1.3170
}

六、正弦定理与余弦定理

6.1 正弦定理

对于任意三角形,记角 $A, B, C$ 为三个角,对应的边为 $a, b, c$,则:

$$\frac{a}{\sin A} = \frac{b}{\sin B} = \frac{c}{\sin C} = 2R$$

其中 $R$ 为三角形外接圆的半径。

6.2 余弦定理

$$a^2 = b^2 + c^2 - 2bc\cos A$$ $$b^2 = a^2 + c^2 - 2ac\cos B$$ $$c^2 = a^2 + b^2 - 2ab\cos C$$

use std::f64::consts::PI;

fn solve_triangle_sas(b: f64, c: f64, angle_a: f64) -> (f64, f64, f64) {
    // 已知两边及其夹角,求第三边和另外两个角
    let a = (b * b + c * c - 2.0 * b * c * angle_a.cos()).sqrt();
    let angle_b = ((b * b + a * a - c * c) / (2.0 * b * a)).acos();
    let angle_c = PI - angle_a - angle_b;
    (a, angle_b, angle_c)
}

fn main() {
    let (a, b, c) = solve_triangle_sas(3.0, 4.0, PI / 2.0);
    println!("第三边 a = {:.4}", a);           // 5.0
    println!("角 B = {:.2}°", b.to_degrees());  // 53.13°
    println!("角 C = {:.2}°", c.to_degrees());  // 36.87°
}

七、欧拉公式

欧拉公式是数学中最优美的公式之一,将指数函数、三角函数和虚数联系在一起:

$$e^{i\theta} = \cos\theta + i\sin\theta$$

当 $\theta = \pi$ 时,得到欧拉恒等式:

$$e^{i\pi} + 1 = 0$$

这个等式将数学中五个最重要的常数联系在一起:$e$(自然常数)、$i$(虚数单位)、$\pi$(圆周率)、1(乘法单位元)、0(加法单位元)。


八、函数在 Rust 中的表示

8.1 闭包

Rust 中的闭包(Closure)可以捕获环境中的变量,类似于数学中的函数:

fn main() {
    let a = 2.0;
    let b = 3.0;

    // 线性函数 f(x) = ax + b
    let linear = |x: f64| a * x + b;

    println!("f(1) = {}", linear(1.0));  // 5.0
    println!("f(2) = {}", linear(2.0));  // 7.0

    // 二次函数 f(x) = x² + 2x + 1
    let quadratic = |x: f64| x * x + 2.0 * x + 1.0;
    println!("g(3) = {}", quadratic(3.0));  // 16.0
}

8.2 高阶函数

接受函数作为参数或返回函数的函数:

fn compose<F, G>(f: F, g: G) -> impl Fn(f64) -> f64
where
    F: Fn(f64) -> f64,
    G: Fn(f64) -> f64,
{
    move |x| f(g(x))
}

fn main() {
    let f = |x: f64| x + 1.0;
    let g = |x: f64| x * 2.0;

    let h = compose(f, g);  // h(x) = f(g(x)) = 2x + 1
    println!("h(3) = {}", h(3.0));  // 7.0
}

九、数值积分

9.1 复合梯形法则

$$\int_a^b f(x) dx \approx \frac{b - a}{2n} \sum_{i=0}^{n-1} [f(x_i) + f(x_{i+1})]$$

9.2 复合辛普森法则

$$\int_a^b f(x) dx \approx \frac{h}{3}[f(x_0) + 4f(x_1) + 2f(x_2) + 4f(x_3) + \cdots + 2f(x_{n-2}) + 4f(x_{n-1}) + f(x_n)]$$

其中 $h = \frac{b-a}{n}$,$n$ 为偶数。

fn simpson_integral(f: impl Fn(f64) -> f64, a: f64, b: f64, n: usize) -> f64 {
    assert!(n % 2 == 0, "n 必须是偶数");
    let h = (b - a) / n as f64;
    let mut sum = f(a) + f(b);

    for i in 1..n {
        let x = a + i as f64 * h;
        if i % 2 == 0 {
            sum += 2.0 * f(x);
        } else {
            sum += 4.0 * f(x);
        }
    }

    sum * h / 3.0
}

fn main() {
    // 计算 ∫₀¹ x² dx = 1/3
    let result = simpson_integral(|x| x * x, 0.0, 1.0, 100);
    println!("∫₀¹ x² dx ≈ {}", result);  // 0.3333

    // 计算 ∫₀^π sin(x) dx = 2
    let result2 = simpson_integral(|x| x.sin(), 0.0, std::f64::consts::PI, 100);
    println!("∫₀^π sin(x) dx ≈ {}", result2);  // 2.0
}

十、特殊函数

10.1 Sigmoid 函数

$$\sigma(x) = \frac{1}{1 + e^{-x}}$$

Sigmoid 函数的核心作用是将任意实数输入压缩到 $(0, 1)$ 区间,形成一个光滑的 S 形曲线。在神经网络中,它常被用作激活函数,将线性输出转换为概率。

fn sigmoid(x: f64) -> f64 {
    1.0 / (1.0 + (-x).exp())
}

fn main() {
    println!("sigmoid(0) = {}", sigmoid(0.0));     // 0.5
    println!("sigmoid(2) = {}", sigmoid(2.0));     // 0.8808
    println!("sigmoid(-2) = {}", sigmoid(-2.0));   // 0.1192
}

10.2 高斯函数

$$f(x) = e^{-x^2}$$

$$\int_{-\infty}^{+\infty} e^{-x^2} dx = \sqrt{\pi}$$


十一、物理中的应用

11.1 万有引力

$$F = G \frac{m_1 m_2}{r^2}$$

11.2 质能方程

$$E = mc^2$$

11.3 单摆周期

$$T = 2\pi \sqrt{\frac{l}{g}}$$


十二、总结

概念要点
奇偶性奇函数 $f(-x) = -f(x)$,偶函数 $f(-x) = f(x)$
单调性导数大于零递增,小于零递减
周期性$f(x+T) = f(x)$
凹凸性二阶导数大于零下凸,小于零上凸
连续性$\lim_{x \to x_0} f(x) = f(x_0)$
幂函数$x^a$,指数运算法则
指数函数$a^x$,以 $e^x$ 最常用
对数函数$\log_a x$,换底公式
三角函数$\sin, \cos, \tan$ 及其关系
双曲函数$\sinh, \cosh, \tanh$
欧拉公式$e^{i\theta} = \cos\theta + i\sin\theta$
Sigmoid$\sigma(x) = \frac{1}{1+e^{-x}}$

函数是数学和计算机科学的核心概念。从基本的初等函数到复杂的复合函数,从连续的数学函数到离散的计算机程序,函数的思想贯穿始终。

练习建议:

  1. 用 Rust 实现各种初等函数的性质验证
  2. 实现牛顿迭代法求方程的根
  3. 用数值积分方法计算定积分

第三十三 微积分(Calculus)

微积分(Calculus)是数学中最伟大的成就之一,由牛顿和莱布尼茨在 17 世纪独立创立。它的核心思想是以直代曲、以简代繁——把复杂问题分解成无数个小的、简单的问题,然后再整合结果。

  • 微分(求导):求变化快慢,比如速度、斜率
  • 积分:求累积总量,比如面积、路程
  • 两者互为逆运算(导数 ↔ 积分)

一、极限与连续

1.1 极限的定义

$$\lim_{x \to a} f(x) = L$$

表示当 $x$ 无限接近 $a$ 时,$f(x)$ 无限接近 $L$。

1.2 重要极限

$$\lim_{x \to 0} \frac{\sin x}{x} = 1$$

$$\lim_{x \to 0} \frac{e^x - 1}{x} = 1$$

$$\lim_{x \to 0} \frac{a^x - 1}{x} = \ln a \quad (a > 0)$$

$$\lim_{n \to \infty} \left(1 + \frac{1}{n}\right)^n = e$$

1.3 无穷小与无穷大

  • 无穷小:极限为 0 的量
  • 无穷大:趋向于无穷的量
  • 等价无穷小替换($x \to 0$ 时):$\sin x \sim x$,$\tan x \sim x$,$e^x - 1 \sim x$,$\ln(1+x) \sim x$
fn main() {
    // 验证 lim(x→0) sin(x)/x = 1
    for &h in &[1e-1_f64, 1e-2_f64, 1e-4_f64, 1e-8_f64] {
        println!("sin({:.0e})/{:.0e} = {:.10}", h, h, h.sin() / h);
    }
    // 越接近 0,结果越接近 1
}

二、导数(微分)

2.1 导数的定义

函数 $y = f(x)$ 在某点的导数:

$$f’(x) = \frac{dy}{dx} = \lim_{\Delta x \to 0} \frac{f(x+\Delta x) - f(x)}{\Delta x}$$

几何意义: 曲线在该点的切线斜率。

2.2 基本求导公式

函数导数
$(C)’$$0$(常数函数)
$(x^n)’$$nx^{n-1}$(幂函数)
$(\frac{1}{x})’$$-\frac{1}{x^2}$
$(\sin x)’$$\cos x$
$(\cos x)’$$-\sin x$
$(\tan x)’$$\sec^2 x$
$(\ln x)’$$\frac{1}{x}$
$(\log_a x)’$$\frac{1}{x \ln a}$
$(e^x)’$$e^x$
$(a^x)’$$a^x \ln a$

2.3 微分基本运算法则

法则公式
$(u + v)’ = u’ + v’$
$(u - v)’ = u’ - v’$
乘积$(uv)’ = u’v + uv’$
$\left(\frac{u}{v}\right)’ = \frac{u’v - uv’}{v^2}$
链式法则$\frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dx}$
fn main() {
    // 数值求导:f(x) = sin(x) + x²
    let f = |x: f64| x.sin() + x * x;
    let h = 1e-8;
    let x = 1.0;

    // 数值导数
    let numerical = (f(x + h) - f(x - h)) / (2.0 * h);
    // 解析导数: cos(x) + 2x
    let analytical = x.cos() + 2.0 * x;

    println!("数值导数: {:.10}", numerical);   // 2.5403023059
    println!("解析导数: {:.10}", analytical);   // 2.5403023059
}

2.4 高阶导数

高阶导数是导数的导数:

  • 一阶导数:$f’(x)$(速度)
  • 二阶导数:$f’’(x)$(加速度)
  • 三阶导数:$f’‘’(x)$(急动度/Jerk)

$$f’’(x) = \frac{d}{dx}\left(\frac{d}{dx}f(x)\right) = \frac{d^2 f}{dx^2}$$


三、微分中值定理

3.1 罗尔定理

若 $f(x)$ 在 $[a,b]$ 上连续,在 $(a,b)$ 内可导,且 $f(a) = f(b)$,则在 $(a,b)$ 内至少存在一点 $\xi$,使得 $f’(\xi) = 0$。

3.2 拉格朗日中值定理

若 $f(x)$ 在 $[a,b]$ 上连续,在 $(a,b)$ 内可导,则在 $(a,b)$ 内至少存在一点 $\xi$,使得:

$$f’(\xi) = \frac{f(b) - f(a)}{b - a}$$

几何意义: 曲线上至少有一点处的切线平行于连接两端点的弦。

物理意义: 至少在某一时刻,瞬时速度等于平均速度。

3.3 柯西中值定理

若 $f(x)$ 和 $g(x)$ 都在 $[a,b]$ 上连续,在 $(a,b)$ 内可导,且 $g’(x) \neq 0$,则在 $(a,b)$ 内至少存在一点 $\xi$,使得:

$$\frac{f’(\xi)}{g’(\xi)} = \frac{f(b) - f(a)}{g(b) - g(a)}$$


四、洛必达法则

当 $\lim_{x \to a} \frac{f(x)}{g(x)}$ 为 $\frac{0}{0}$ 或 $\frac{\infty}{\infty}$ 型未定式时:

$$\lim_{x \to a} \frac{f(x)}{g(x)} = \lim_{x \to a} \frac{f’(x)}{g’(x)}$$

前提是右边的极限存在。

fn main() {
    // 求 lim(x→1) sin(πx) / (x² - 1)
    let f = |x: f64| (std::f64::consts::PI * x).sin();
    let g = |x: f64| x * x - 1.0;

    let x = 1.0;
    let h = 1e-8;

    // 直接代入是 0/0 型,使用洛必达法则
    // f'(x) = πcos(πx), g'(x) = 2x
    // lim = πcos(π) / 2 = -π/2
    let limit = (f(x + h) - f(x - h)) / (2.0 * h) / ((g(x + h) - g(x - h)) / (2.0 * h));
    println!("lim = {:.6}", limit);  // -1.570796 ≈ -π/2
}

五、泰勒公式

泰勒公式用多项式逼近函数,是“以直代曲“的数学表达:

$$f(x) \approx f(a) + f’(a)(x-a) + \frac{f’‘(a)}{2!}(x-a)^2 + \frac{f’‘’(a)}{3!}(x-a)^3 + \cdots$$

当 $a = 0$ 时,称为麦克劳林公式

$$f(x) \approx \sum_{n=0}^{\infty} \frac{f^{(n)}(0)}{n!} x^n$$

常用泰勒展开式:

$$\sin x = x - \frac{x^3}{3!} + \frac{x^5}{5!} - \frac{x^7}{7!} + \cdots$$

$$\cos x = 1 - \frac{x^2}{2!} + \frac{x^4}{4!} - \frac{x^6}{6!} + \cdots$$

$$e^x = 1 + x + \frac{x^2}{2!} + \frac{x^3}{3!} + \frac{x^4}{4!} + \cdots$$

$$\ln(1-x) = -\left(x + \frac{x^2}{2} + \frac{x^3}{3} + \frac{x^4}{4} + \cdots\right) \quad (|x| < 1)$$

fn taylor_sin(x: f64, terms: usize) -> f64 {
    let mut sum = 0.0;
    for n in 0..terms {
        let power = 2 * n + 1;
        let coeff = if n % 2 == 0 { 1.0 } else { -1.0 };
        let factorial: f64 = (1..=power).map(|x| x as f64).product();
        sum += coeff * x.powi(power as i32) / factorial;
    }
    sum
}

fn main() {
    let x = std::f64::consts::PI / 4.0; // 45°

    println!("标准库 sin(π/4) = {:.10}", x.sin());
    println!("泰勒 3 项:        {:.10}", taylor_sin(x, 3));
    println!("泰勒 5 项:        {:.10}", taylor_sin(x, 5));
    println!("泰勒 10 项:       {:.10}", taylor_sin(x, 10));
}

六、积分

6.1 不定积分

导数的逆运算。若 $F’(x) = f(x)$,则:

$$\int f(x) dx = F(x) + C$$

其中 $C$ 为积分常数。

6.2 定积分

$$\int_a^b f(x) dx = F(b) - F(a)$$

定积分的几何意义是曲线 $y = f(x)$ 与 $x$ 轴、$x = a$、$x = b$ 所围成区域的有向面积

6.3 基本积分公式

被积函数不定积分
$\int k , dx$$kx + C$
$\int x^n , dx$$\frac{x^{n+1}}{n+1} + C \quad (n \neq -1)$
$\int \frac{1}{x} , dx$$\ln
$\int \sin x , dx$$-\cos x + C$
$\int \cos x , dx$$\sin x + C$
$\int \sec^2 x , dx$$\tan x + C$
$\int e^x , dx$$e^x + C$
$\int \ln x , dx$$x\ln x - x + C$

6.4 换元积分法

$$\int f(g(x)) \cdot g’(x) , dx = \int f(u) , du \quad (u = g(x))$$

6.5 分部积分法

$$\int u , dv = uv - \int v , du$$


七、微积分基本定理(牛顿-莱布尼茨公式)

若 $F(x)$ 是 $f(x)$ 在 $[a,b]$ 上的一个原函数(即 $F’(x) = f(x)$),则:

$$\int_a^b f(x) dx = F(b) - F(a)$$

辛普森积分法

fn simpson_integral(f: impl Fn(f64) -> f64, a: f64, b: f64, n: usize) -> f64 {
    assert!(n % 2 == 0);
    let h = (b - a) / n as f64;
    let mut sum = f(a) + f(b);

    for i in 1..n {
        let x = a + i as f64 * h;
        if i % 2 == 0 { sum += 2.0 * f(x); }
        else { sum += 4.0 * f(x); }
    }

    sum * h / 3.0
}

fn main() {
    // ∫₀¹ x² dx = 1/3
    let result = simpson_integral(|x| x * x, 0.0, 1.0, 100);
    println!("∫₀¹ x² dx = {:.10} (理论值: 0.3333333333)", result);

    // ∫₀^π sin(x) dx = 2
    let result2 = simpson_integral(|x| x.sin(), 0.0, std::f64::consts::PI, 100);
    println!("∫₀π sin(x) dx = {:.10} (理论值: 2)", result2);

    // ∫₀¹ e^x dx = e - 1
    let result3 = simpson_integral(|x| x.exp(), 0.0, 1.0, 100);
    println!("∫₀¹ e^x dx = {:.10} (理论值: {:.10})", result3, std::f64::consts::E - 1.0);
}

八、偏导数与梯度

8.1 偏导数

对于多元函数 $f(x_1, x_2, \ldots, x_n)$,对某个变量的偏导数是固定其他变量后对该变量的导数:

$$\frac{\partial f}{\partial x_i} = \lim_{\Delta x_i \to 0} \frac{f(x + \Delta x_i) - f(x)}{\Delta x_i}$$

8.2 梯度

梯度是所有偏导数组成的向量:

$$\nabla f = \left(\frac{\partial f}{\partial x_1}, \frac{\partial f}{\partial x_2}, \ldots, \frac{\partial f}{\partial x_n}\right)$$

梯度指向函数值增长最快的方向,其大小是最大增长率。

应用: 偏导数是机器学习模型训练的数学基础。在神经网络中,损失函数 $L$ 对权重 $w$ 的偏导数 $\nabla L(w)$,直接告诉我们应该如何调整权重才能让损失函数下降得最快。

8.3 反向传播

$$\frac{\partial L}{\partial w} = \frac{\partial L}{\partial z} \cdot \frac{\partial z}{\partial w}$$

反向传播利用链式法则,从输出层向输入层逐层计算梯度。


九、牛顿二项式定理与杨辉三角

9.1 牛顿二项式定理

$$(a+b)^n = \sum_{k=0}^{n} \binom{n}{k} a^k b^{n-k}$$

9.2 组合数

$$\binom{n}{k} = \frac{n!}{k!(n-k)!}$$

9.3 杨辉三角的性质

  • 每行数字之和为 $2^n$:$\sum_{k=0}^{n} \binom{n}{k} = 2^n$
  • 对称性:$\binom{n}{k} = \binom{n}{n-k}$
  • 递推关系:$\binom{n}{k} = \binom{n-1}{k-1} + \binom{n-1}{k}$
fn binomial(n: u64, k: u64) -> u64 {
    if k > n { return 0; }
    if k > n - k { k = n - k; } // 利用对称性优化
    let mut result = 1u64;
    for i in 0..k {
        result = result * (n - i) / (i + 1);
    }
    result
}

fn print_pascal(rows: usize) {
    for n in 0..rows {
        for k in 0..=n {
            print!("{:6}", binomial(n as u64, k as u64));
        }
        println!();
    }
}

fn main() {
    print_pascal(10);
}

十、极值问题

10.1 极值的判定

  • 必要条件:若 $f(x)$ 在 $x_0$ 处可导且取得极值,则 $f’(x_0) = 0$
  • 充分条件(第一判别法):$f’(x)$ 在 $x_0$ 两侧变号(左正右负为极大值,左负右正为极小值)
  • 充分条件(第二判别法):$f’(x_0) = 0$ 且 $f’‘(x_0) < 0$ 为极大值,$f’’(x_0) > 0$ 为极小值

10.2 拉格朗日乘子法

用于求解带约束条件的极值问题:

$$\mathcal{L}(x, y, \lambda) = f(x, y) - \lambda \cdot g(x, y)$$

对 $x, y, \lambda$ 分别求偏导并令其为零,解方程组。


十一、蒙特卡洛方法

蒙特卡洛方法利用随机采样来近似计算确定性的数学问题。

用蒙特卡洛方法计算圆周率:

use rand::Rng;
use rand::RngExt;

fn monte_carlo_pi(samples: usize) -> f64 {
    let mut rng = rand::rng();          // thread_rng() → rng()
    let mut inside = 0;

    for _ in 0..samples {
        let x: f64 = rng.random_range(-1.0..1.0);  // gen_range() → random_range()
        let y: f64 = rng.random_range(-1.0..1.0);
        if x * x + y * y <= 1.0 {
            inside += 1;
        }
    }

    4.0 * inside as f64 / samples as f64
}

fn main() {
    let sample_sizes = [1000, 10000, 100000, 1000000];
    
    for &n in &sample_sizes {
        let pi_approx = monte_carlo_pi(n);
        println!("{n:>7} 次采样: π ≈ {pi_approx:.6}");
    }
}

十二、总结

概念要点
极限函数值无限接近某个值
导数变化率,切线斜率
求导法则和差积商、链式法则
中值定理罗尔、拉格朗日、柯西
洛必达法则0/0 或 ∞/∞ 型未定式
泰勒公式多项式逼近函数
不定积分导数的逆运算
定积分面积,牛顿-莱布尼茨公式
偏导数多元函数对某个变量的导数
梯度偏导数向量,指向增长最快方向

微积分是理解连续变化的数学工具。从物理中的运动分析,到机器学习中的梯度下降,微积分的思想无处不在。

练习建议:

  1. 用数值方法验证泰勒公式的精度
  2. 实现蒙特卡洛方法计算定积分
  3. 用梯度下降法求解函数的极小值

学习资源

资源说明
3Blue1Brown - 微积分的本质微积分可视化教程
3Blue1Brown B站中文字幕版
微积分的本质B站系列

第三十四 概率论(Probability)

概率论是研究随机现象规律性的数学分支。在数据科学和机器学习的背景下,概率论为理解数据行为、指导模型选择和评估结果提供了数学基础。它将杂乱、嘈杂的数据集转化为可操作的情报。


一、基本概念

1.1 随机事件

随机试验:结果不确定,但所有可能结果已知。

样本空间 $\Omega$:所有可能结果的集合。

随机事件:样本空间的子集。

事件类型定义
必然事件一定会发生,$\Omega$
不可能事件一定不发生,$\emptyset$
互斥事件不能同时发生,$A \cap B = \emptyset$
对立事件$A$ 不发生即 $B$ 发生,$B = \bar{A}$

1.2 概率的基本性质

  • $0 \le P(A) \le 1$
  • $P(\Omega) = 1$
  • $P(\emptyset) = 0$
  • $P(\bar{A}) = 1 - P(A)$
  • 若 $A \subset B$,则 $P(A) \le P(B)$

二、排列与组合

2.1 排列

从 $n$ 个不同元素中取 $m$ 个,按顺序排列:

$$A(n,m) = P(n,m) = \frac{n!}{(n-m)!}$$

2.2 组合

从 $n$ 个不同元素中取 $m$ 个,不区分顺序:

$$C(n,m) = \binom{n}{m} = \frac{n!}{m!(n-m)!}$$

组合的性质:

  • 对称性:$C(n,m) = C(n,n-m)$
  • 递推关系:$C(n,m) = C(n-1,m-1) + C(n-1,m)$
  • 求和公式:$\sum_{k=0}^{n} C(n,k) = 2^n$
fn factorial(n: u64) -> u64 {
    (1..=n).product()
}

fn permutation(n: u64, m: u64) -> u64 {
    factorial(n) / factorial(n - m)
}

fn combination(n: u64, m: u64) -> u64 {
    if m > n { return 0; }
    if m > n - m { return combination(n, n - m); }
    let mut result = 1u64;
    for i in 0..m {
        result = result * (n - i) / (i + 1);
    }
    result
}

fn main() {
    println!("P(5,3) = {}", permutation(5, 3));  // 60
    println!("C(5,3) = {}", combination(5, 3));  // 10
    println!("C(52,3) = {}", combination(52, 3)); // 22100
}

三、条件概率与独立性

3.1 条件概率

在事件 $B$ 已经发生的条件下,事件 $A$ 发生的概率:

$$P(A|B) = \frac{P(A \cap B)}{P(B)}$$

3.2 乘法公式

$$P(A \cap B) = P(A) \cdot P(B|A) = P(B) \cdot P(A|B)$$

3.3 独立性

若 $A$ 与 $B$ 相互独立,则:

$$P(A \cap B) = P(A) \cdot P(B)$$

$$P(A|B) = P(A)$$

注意: 独立与互斥是不同的概念。互斥事件通常不独立(除非其中一个概率为 0)。

fn main() {
    // 甲击中概率 0.8,乙击中概率 0.7,目标被击中的概率?
    let p_a = 0.8;
    let p_b = 0.7;

    // P(击中) = P(A) + P(B) - P(A∩B) = 0.8 + 0.7 - 0.8*0.7
    let p_hit = p_a + p_b - p_a * p_b;
    println!("目标被击中的概率: {:.2}", p_hit);  // 0.94
}

四、全概率公式与贝叶斯公式

4.1 全概率公式

若 $B_1, B_2, \ldots, B_n$ 构成样本空间的一个划分(两两互斥且并集为 $\Omega$),则:

$$P(A) = \sum_{i=1}^{n} P(A|B_i) \cdot P(B_i)$$

4.2 贝叶斯公式

$$P(B_i|A) = \frac{P(A|B_i) \cdot P(B_i)}{P(A)} = \frac{P(A|B_i) \cdot P(B_i)}{\sum_{j=1}^{n} P(A|B_j) \cdot P(B_j)}$$

概念含义
$P(B_i)$先验概率(在看到证据之前)
$P(AB_i)$
$P(B_iA)$

贝叶斯公式的直觉理解: 先验信念 + 新证据 → 更新后的信念。这是机器学习中贝叶斯推断的基础。

fn main() {
    // 医学检测问题
    // 某疾病发病率 1%,检测准确率 99%(真阳性率 99%,假阳性率 1%)
    // 问:检测为阳性,实际患病的概率是多少?

    let p_disease = 0.01;       // P(患病)
    let p_no_disease = 0.99;    // P(不患病)
    let p_positive_given_disease = 0.99;    // P(阳性|患病)
    let p_positive_given_no = 0.01;         // P(阳性|不患病)

    // 全概率公式:P(阳性)
    let p_positive = p_positive_given_disease * p_disease
                  + p_positive_given_no * p_no_disease;

    // 贝叶斯公式:P(患病|阳性)
    let p_disease_given_positive = p_positive_given_disease * p_disease / p_positive;

    println!("检测阳性时实际患病概率: {:.4}", p_disease_given_positive);
    // 0.5025 — 即使检测准确率高达 99%,阳性结果也只有约 50% 概率是真的!
}

五、随机变量

5.1 离散型随机变量

取值为有限个或可列个的随机变量。用分布律描述:

$$P(X = x_k) = p_k \quad k = 1, 2, 3, \ldots$$

满足:$p_k \ge 0$,$\sum p_k = 1$。

5.2 连续型随机变量

取值充满某个区间的随机变量。用概率密度函数 $f(x)$ 描述:

$$P(a \le X \le b) = \int_a^b f(x) dx$$

满足:$f(x) \ge 0$,$\int_{-\infty}^{+\infty} f(x) dx = 1$。


六、期望与方差

6.1 期望(数学期望)

期望是随机变量的“平均值“。

离散型:

$$E[X] = \sum_{k} x_k \cdot p_k$$

连续型:

$$E[X] = \int_{-\infty}^{+\infty} x \cdot f(x) dx$$

期望的性质:

  • $E[aX + b] = aE[X] + b$
  • $E[X + Y] = E[X] + E[Y]$
  • 若 $X, Y$ 独立:$E[XY] = E[X] \cdot E[Y]$

6.2 方差

方差衡量数据的离散程度。

$$\text{Var}(X) = E[(X - \mu)^2] = E[X^2] - (E[X])^2$$

其中 $\mu = E[X]$。

标准差: $\sigma = \sqrt{\text{Var}(X)}$

方差的性质:

  • $\text{Var}(aX + b) = a^2 \text{Var}(X)$
  • 若 $X, Y$ 独立:$\text{Var}(X + Y) = \text{Var}(X) + \text{Var}(Y)$
fn mean(data: &[f64]) -> f64 {
    data.iter().sum::<f64>() / data.len() as f64
}

fn variance(data: &[f64]) -> f64 {
    let m = mean(data);
    data.iter().map(|x| (x - m).powi(2)).sum::<f64>() / data.len() as f64
}

fn std_dev(data: &[f64]) -> f64 {
    variance(data).sqrt()
}

fn main() {
    let data = [6.0, 5.0, 4.0, 8.0, 12.0];
    println!("平均值: {}", mean(&data));       // 7.0
    println!("方差:   {}", variance(&data));    // 8.0
    println!("标准差: {}", std_dev(&data));       // 2.8284
}

七、常见离散分布

7.1 二项分布

$n$ 次独立试验中,每次成功概率为 $p$,恰好成功 $x$ 次的概率:

$$f(x) = \binom{n}{x} \cdot p^x \cdot (1-p)^{n-x}$$

期望: $E[X] = np$

方差: $\text{Var}(X) = np(1-p)$

fn binomial_pmf(x: u32, n: u32, p: f64) -> f64 {
    combination(n as u64, x as u64) as f64 * p.powi(x as i32) * (1.0 - p).powi(n as i32 - x as i32)
}

fn main() {
    // 三发导弹,命中率 70%
    let n = 3;
    let p = 0.7;

    println!("三发全中:     {:.4}", binomial_pmf(3, n, p));  // 0.343
    println!("命中两发:     {:.4}", binomial_pmf(2, n, p));  // 0.441
    println!("命中一发:     {:.4}", binomial_pmf(1, n, p));  // 0.189
    println!("全不命中:     {:.4}", binomial_pmf(0, n, p));  // 0.027
}

7.2 泊松分布

描述单位时间/空间内随机独立事件发生次数的概率分布:

$$f(x) = \frac{\lambda^x e^{-\lambda}}{x!}$$

期望 = 方差 = $\lambda$

适用场景: 电话呼叫次数、交通事故数、错别字数、服务器请求量等“稀有事件“。

fn poisson_pmf(x: u32, lambda: f64) -> f64 {
    lambda.powi(x as i32) * (-lambda).exp() / factorial(x as u64) as f64
}

fn main() {
    // 某网站平均每小时 5 次访问
    let lambda = 5.0;
    for x in 0..=10 {
        println!("P(X={}) = {:.6}", x, poisson_pmf(x, lambda));
    }
}

八、常见连续分布

8.1 正态分布(高斯分布)

概率密度函数:

$$f(x) = \frac{1}{\sqrt{2\pi\sigma^2}} \cdot e^{-\frac{(x-\mu)^2}{2\sigma^2}}$$

参数: $\mu$ 为均值,$\sigma$ 为标准差。

标准正态分布: $Z = \frac{X - \mu}{\sigma} \sim N(0, 1)$

正态分布的固定概率:

区间概率
$\mu \pm \sigma$≈ 68.27%
$\mu \pm 2\sigma$≈ 95.45%
$\mu \pm 3\sigma$≈ 99.73%
fn normal_pdf(x: f64, mu: f64, sigma: f64) -> f64 {
    let coeff = 1.0 / (sigma * (2.0 * std::f64::consts::PI).sqrt());
    coeff * (-(x - mu).powi(2) / (2.0 * sigma * sigma)).exp()
}

fn main() {
    // 标准正态分布在几个关键点的概率密度
    for &x in &[-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0] {
        println!("f({:+.0}) = {:.6}", x, normal_pdf(x, 0.0, 1.0));
    }
    // f(0) = 0.398942(最大值)
}

8.2 均匀分布

$$f(x) = \frac{1}{b-a} \quad (a \le x \le b)$$

期望: $E[X] = \frac{a+b}{2}$

方差: $\text{Var}(X) = \frac{(b-a)^2}{12}$


九、大数定律与中心极限定理

9.1 大数定律

$$\lim_{n \to \infty} \frac{1}{n} \sum_{i=1}^{n} X_i = E[X]$$

直觉理解: 试验次数足够多时,频率趋近于概率。

9.2 切比雪夫不等式

$$P(|X - \mu| \ge k\sigma) \le \frac{1}{k^2}$$

无论随机变量服从什么分布,只要方差有限,数据偏离均值 $k$ 个标准差以上的概率不超过 $\frac{1}{k^2}$。

9.3 中心极限定理

无论 $X_1, X_2, \ldots, X_n$ 服从什么分布,只要它们独立同分布且期望方差有限,当 $n$ 足够大时:

$$\frac{\sum X_i - n\mu}{\sigma\sqrt{n}} \xrightarrow{d} N(0, 1)$$

直觉理解: 大量独立随机变量之和趋向于正态分布。这就是为什么正态分布在自然界中如此常见。


十、信息熵

信息熵描述了随机变量的不确定性:

$$H(X) = -\sum_{x} P(x) \cdot \log_2 P(x)$$

熵值含义
最大均匀分布(最不确定)
最小(为 0)确定性事件(完全确定)
fn entropy(probabilities: &[f64]) -> f64 {
    probabilities
        .iter()
        .filter(|&&p| p > 0.0)
        .map(|&p| -p * p.log2())
        .sum()
}

fn main() {
    // 公平硬币
    println!("公平硬币熵: {:.4}", entropy(&[0.5, 0.5]));  // 1.0

    // 不公平硬币
    println!("偏心硬币熵: {:.4}", entropy(&[0.9, 0.1]));  // 0.4690

    // 六面骰子
    println!("公平骰子熵: {:.4}", entropy(&[1.0/6.0; 6]));  // 2.5850
}

应用: 信息熵是决策树算法(ID3、C4.5)的核心指标,用于选择最优的分裂特征。


十一、假设检验

11.1 基本思想

  • 原假设 $H_0$:默认成立的假设(如“没有差异“)
  • 备择假设 $H_1$:与原假设对立的假设
  • P 值:在 $H_0$ 成立的条件下,观察到当前或更极端结果的概率
  • 显著性水平 $\alpha$:通常取 0.05
P 值结论
$P < 0.05$拒绝 $H_0$,差异显著
$P \ge 0.05$不能拒绝 $H_0$,差异不显著

11.2 Z 检验

$$z = \frac{\bar{x} - \mu}{\sigma / \sqrt{n}}$$

适用于大样本($n \ge 30$)且总体标准差 $\sigma$ 已知。

11.3 T 检验

$$t = \frac{\bar{x} - \mu}{s / \sqrt{n}}$$

适用于小样本($n < 30$)且总体标准差未知。

11.4 卡方检验

$$\chi^2 = \sum_{i=1}^{k} \frac{(O_i - E_i)^2}{E_i}$$

其中 $O_i$ 为观察频数,$E_i$ 为期望频数。用于检验分类变量之间是否独立。


十二、经典题目

题目1:生日悖论

一个班有 100 人,至少两人生日相同的概率是多少?

fn birthday_probability(n: u32) -> f64 {
    let mut p_no_match = 1.0;
    for i in 0..n {
        p_no_match *= (365.0 - i as f64) / 365.0;
    }
    1.0 - p_no_match
}

fn main() {
    for &n in &[23, 30, 50, 70, 100] {
        println!("{} 人中至少两人生日相同: {:.4}", n, birthday_probability(n));
    }
    // 23 人: 0.5073(超过 50%!)
    // 100 人: 0.9999997(几乎必然)
}

题目2:炸金花概率

52 张牌中抽 3 张,求同花顺和豹子的概率。

fn main() {
    let total = combination(52, 3) as f64;  // 22100

    // 同花顺:4种花色 × 12种序列(A23, 234, ..., QKA)
    let straight_flush = 4.0 * 12.0;
    println!("同花顺概率: {:.6} ({:.4}%)", straight_flush / total, straight_flush / total * 100.0);

    // 豹子:13种点数 × 4种花色取3种
    let three_of_kind = 13.0 * combination(4, 3) as f64;
    println!("豹子概率:   {:.6} ({:.4}%)", three_of_kind / total, three_of_kind / total * 100.0);
}

十三、总结

概念要点
排列$A(n,m) = n!/(n-m)!$
组合$C(n,m) = n!/[m!(n-m)!]$
条件概率$P(A
贝叶斯公式后验 = 似然 × 先验 / 证据
期望$E[X]$,随机变量的平均值
方差$\text{Var}(X)$,数据的离散程度
二项分布$n$ 次试验中成功 $x$ 次的概率
泊松分布稀有事件发生次数
正态分布钟形曲线,$\mu \pm 3\sigma$ 覆盖 99.73%
中心极限定理大量独立变量之和趋向正态分布
信息熵$H(X) = -\sum P(x)\log_2 P(x)$

概率论是理解不确定性的数学工具。从日常的天气预报到机器学习的模型评估,概率论的思想无处不在。

练习建议:

  1. 用贝叶斯公式解决实际问题(如垃圾邮件分类)
  2. 模拟大数定律和中心极限定理
  3. 用蒙特卡洛方法估计概率

第三十五 离散数学(Discrete Mathematics)

离散数学是研究离散量的结构及其相互关系的数学分支,与连续数学(如微积分)相对。它是计算机科学的理论基础,为算法设计、数据结构、数据库、编译原理、密码学等提供了核心数学工具。


一、集合论

1.1 基本概念

集合:一组确定的、互不相同的对象的全体。集合中的对象称为元素

符号含义
$a \in A$$a$ 属于集合 $A$
$a \notin A$$a$ 不属于集合 $A$
$\emptyset$空集
$|A|$集合 $A$ 的元素个数(基数)
$\mathcal{U}$全集

1.2 集合间的关系

关系符号定义
子集$A \subseteq B$$A$ 的每个元素都属于 $B$
真子集$A \subset B$$A \subseteq B$ 且 $A \neq B$
相等$A = B$$A \subseteq B$ 且 $B \subseteq A$

1.3 集合运算

运算符号定义
并集$A \cup B$属于 $A$ 或属于 $B$ 的元素
交集$A \cap B$同时属于 $A$ 和 $B$ 的元素
差集$A - B$属于 $A$ 但不属于 $B$ 的元素
补集$\bar{A}$属于全集但不属于 $A$ 的元素
对称差$A \triangle B$$(A - B) \cup (B - A)$

运算律:

$$A \cup B = B \cup A \quad \text{(交换律)}$$ $$A \cap (B \cup C) = (A \cap B) \cup (A \cap C) \quad \text{(分配律)}$$ $$\overline{A \cup B} = \bar{A} \cap \bar{B} \quad \text{(德摩根定律)}$$ $$\overline{A \cap B} = \bar{A} \cup \bar{B} \quad \text{(德摩根定律)}$$

use std::collections::{HashSet, BTreeSet};

fn main() {
    let a: HashSet<i32> = [1, 2, 3, 4].iter().cloned().collect();
    let b: HashSet<i32> = [3, 4, 5, 6].iter().cloned().collect();

    // 并集
    let union: BTreeSet<_> = a.union(&b).cloned().collect();
    println!("A ∪ B = {:?}", union);  // {1, 2, 3, 4, 5, 6}

    // 交集
    let intersection: BTreeSet<_> = a.intersection(&b).cloned().collect();
    println!("A ∩ B = {:?}", intersection);  // {3, 4}

    // 差集
    let difference: BTreeSet<_> = a.difference(&b).cloned().collect();
    println!("A - B = {:?}", difference);  // {1, 2}

    // 对称差
    let sym_diff: BTreeSet<_> = a.symmetric_difference(&b).cloned().collect();
    println!("A △ B = {:?}", sym_diff);  // {1, 2, 5, 6}
}

1.4 幂集

集合 $A$ 的幂集 $\mathcal{P}(A)$ 是 $A$ 的所有子集构成的集合。

$$|\mathcal{P}(A)| = 2^{|A|}$$

fn power_set<T: Clone>(set: &[T]) -> Vec<Vec<T>> {
    let n = set.len();
    let mut result = Vec::new();
    for mask in 0..(1u32 << n) {
        let mut subset = Vec::new();
        for i in 0..n {
            if (mask >> i) & 1 == 1 {
                subset.push(set[i].clone());
            }
        }
        result.push(subset);
    }
    result
}

fn main() {
    let set = vec![1, 2, 3];
    let ps = power_set(&set);
    println!("幂集(共 {} 个子集):", ps.len());  // 8 = 2³
    for subset in &ps {
        println!("  {:?}", subset);
    }
}

二、图论

2.1 基本概念

$G = (V, E)$ 由顶点集 $V$ 和边集 $E$ 组成。

概念定义
有向图边有方向
无向图边无方向
完全图 $K_n$任意两个顶点之间都有边
与顶点相连的边数
路径顶点与边交替的序列
回路起点和终点相同的路径
连通图任意两个顶点之间都有路径
连通且无回路的图

握手定理: 无向图中所有顶点的度数之和等于边数的两倍:

$$\sum_{v \in V} \deg(v) = 2|E|$$

2.2 七桥问题

七桥问题是图论的起源。欧拉证明了:一个图存在一笔画(欧拉路径)的充要条件是:恰好有 0 个或 2 个奇数度的顶点。

  • 0 个奇数度顶点 → 欧拉回路(起点和终点相同)
  • 2 个奇数度顶点 → 欧拉路径(起点和终点不同)
  • 其他情况 → 不存在一笔画

2.3 图的存储与遍历

use std::collections::{HashMap, HashSet, VecDeque};

struct Graph {
    adj: HashMap<i32, Vec<i32>>,
}

impl Graph {
    fn new() -> Self { Graph { adj: HashMap::new() } }

    fn add_edge(&mut self, u: i32, v: i32) {
        self.adj.entry(u).or_default().push(v);
        self.adj.entry(v).or_default().push(u);
    }

    // BFS 求最短路径
    fn shortest_path(&self, start: i32, end: i32) -> Option<Vec<i32>> {
        let mut visited = HashSet::new();
        let mut queue = VecDeque::new();
        let mut parent: HashMap<i32, i32> = HashMap::new();

        visited.insert(start);
        queue.push_back(start);

        while let Some(node) = queue.pop_front() {
            if node == end {
                // 回溯路径
                let mut path = Vec::new();
                let mut current = end;
                while current != start {
                    path.push(current);
                    current = parent[&current];
                }
                path.push(start);
                path.reverse();
                return Some(path);
            }
            if let Some(neighbors) = self.adj.get(&node) {
                for &neighbor in neighbors {
                    if !visited.contains(&neighbor) {
                        visited.insert(neighbor);
                        parent.insert(neighbor, node);
                        queue.push_back(neighbor);
                    }
                }
            }
        }
        None
    }
}

fn main() {
    let mut g = Graph::new();
    g.add_edge(0, 1); g.add_edge(0, 2);
    g.add_edge(1, 3); g.add_edge(2, 3);
    g.add_edge(3, 4);

    if let Some(path) = g.shortest_path(0, 4) {
        println!("最短路径: {:?}", path);  // [0, 1, 3, 4] 或 [0, 2, 3, 4]
    }
}

2.4 图的应用

应用领域具体应用
社交网络好友关系、推荐系统
地图导航最短路径(Dijkstra、A*)
网络协议路由算法、最小生成树
编译原理语法树、依赖分析
密码学零知识证明、区块链

三、数理逻辑

3.1 命题与逻辑联结词

命题:可以判断真假的陈述句。

联结词符号Rust含义
$\neg$!取反
$\land$&&同真才真
$\lor$||有真则真
蕴含$\rightarrow$只有前真后假时为假
等价$\leftrightarrow$==同真同假

蕴含的真值表:

$p$$q$$p \rightarrow q$
TTT
TFF
FTT
FFT

注意: $p \rightarrow q$ 等价于 $\neg p \lor q$。当前提为假时,蕴含式恒为真。

3.2 量词

量词符号含义示例
全称量词$\forall$对所有$\forall x > 0, x^2 > 0$
存在量词$\exists$存在$\exists x, x^2 = 4$

量词的否定:

$$\neg(\forall x, P(x)) = \exists x, \neg P(x)$$

$$\neg(\exists x, P(x)) = \forall x, \neg P(x)$$


四、组合数学

4.1 加法原理与乘法原理

加法原理: 做一件事有 $n$ 类方法,第 $i$ 类有 $m_i$ 种方法,则总方法数为 $\sum m_i$。

乘法原理: 做一件事分 $n$ 步,第 $i$ 步有 $m_i$ 种方法,则总方法数为 $\prod m_i$。

4.2 排列与组合

$$P(n,m) = \frac{n!}{(n-m)!}$$

$$C(n,m) = \frac{n!}{m!(n-m)!}$$

4.3 鸽巢原理(抽屉原理)

将 $n+1$ 个物品放入 $n$ 个盒子中,至少有一个盒子包含至少 2 个物品。

推论: 367 人中至少两人生日相同(一年最多 366 天)。

4.4 容斥原理

$$|A \cup B| = |A| + |B| - |A \cap B|$$

$$|A \cup B \cup C| = |A| + |B| + |C| - |A \cap B| - |A \cap C| - |B \cap C| + |A \cap B \cap C|$$


五、关系

5.1 二元关系

从集合 $A$ 到集合 $B$ 的二元关系是 $A \times B$ 的子集。

5.2 关系的性质

性质定义
自反性$\forall a, (a,a) \in R$
对称性$(a,b) \in R \Rightarrow (b,a) \in R$
传递性$(a,b) \in R \land (b,c) \in R \Rightarrow (a,c) \in R$
反对称性$(a,b) \in R \land (b,a) \in R \Rightarrow a = b$

5.3 等价关系

同时满足自反性、对称性、传递性的关系称为等价关系

等价关系将集合划分为若干等价类,等价类的集合称为商集

5.4 偏序关系

同时满足自反性、反对称性、传递性的关系称为偏序关系

偏序关系可以用哈斯图(Hasse Diagram)直观表示。


六、代数结构

结构定义示例
半群集合 + 满足结合律的运算字符串拼接
半群 + 有单位元 + 有逆元整数加法 $(\mathbb{Z}, +)$
群 + 第二种满足分配律的运算整数环 $(\mathbb{Z}, +, \times)$
环 + 第二种运算有逆元有理数域 $(\mathbb{Q}, +, \times)$
偏序集 + 任意两元素有上确界和下确界子集格 $(\mathcal{P}(A), \subseteq)$
布尔代数格 + 补运算命题逻辑

应用: 群论是 RSA、ECC 等密码算法的数学基础。有限域上的运算支撑着 AES、椭圆曲线等加密体系。


七、学习资源

资源说明
离散数学及其应用 (Rosen)经典教材
VisuAlgo图论算法可视化
3Blue1Brown数学可视化

八、总结

分支核心内容
集合论并交差补、幂集、德摩根定律
图论顶点、边、路径、树、欧拉路径
数理逻辑命题、量词、蕴含、等价
组合数学排列组合、鸽巢原理、容斥原理
关系自反、对称、传递、等价关系、偏序关系
代数结构群、环、域、格、布尔代数

离散数学是计算机科学的数学语言。从数据结构到算法设计,从数据库到密码学,离散数学的概念无处不在。

练习建议:

  1. 用 Rust 实现图的 DFS/BFS 遍历
  2. 用容斥原理解决计数问题
  3. 判断给定关系是否为等价关系或偏序关系

第三十六 神经网络(Neural Network)

神经网络是深度学习的核心模型,灵感来源于生物神经系统的结构与功能。从手写数字识别到大语言模型,神经网络已经成为现代人工智能的基石。本章将从生物神经元讲起,逐步构建人工神经网络的完整知识体系,并用 Rust 代码实现核心算法。


一、从生物神经元到人工神经元

1.1 生物神经元

人脑由约 $8.6 \times 10^{10}$ 个神经元组成,每个神经元通过突触与其他神经元相连。生物神经元的基本结构包括:

组成部分功能
树突(Dendrite)接收来自其他神经元的信号
细胞体(Soma)整合输入信号
轴突(Axon)传递输出信号
突触(Synapse)神经元之间的连接,信号在此传递

当输入信号的加权和超过阈值时,神经元被“激活“,产生输出信号。这一过程可以用数学模型来抽象。

1.2 人工神经元(McCulloch-Pitts 模型)

1943 年,Warren McCulloch 和 Walter Pitts 提出了第一个人工神经元模型。一个神经元接收 $n$ 个输入 $x_1, x_2, \ldots, x_n$,每个输入对应一个权重 $w_1, w_2, \ldots, w_n$,加上偏置 $b$,经过激活函数 $f$ 后产生输出:

$$y = f\left(\sum_{i=1}^{n} w_i x_i + b\right) = f(\mathbf{w}^T \mathbf{x} + b)$$

/// 人工神经元:加权求和 + 激活函数
struct Neuron {
    weights: Vec<f64>,
    bias: f64,
}

impl Neuron {
    fn new(weights: Vec<f64>, bias: f64) -> Self {
        Neuron { weights, bias }
    }

    /// 加权求和
    fn weighted_sum(&self, inputs: &[f64]) -> f64 {
        self.weights.iter()
            .zip(inputs.iter())
            .map(|(w, x)| w * x)
            .sum::<f64>() + self.bias
    }

    /// 前向传播(使用阶跃激活函数)
    fn forward(&self, inputs: &[f64]) -> f64 {
        let sum = self.weighted_sum(inputs);
        if sum >= 0.0 { 1.0 } else { 0.0 }
    }
}

fn main() {
    // AND 门:w1=1, w2=1, bias=-1.5
    let and_gate = Neuron::new(vec![1.0, 1.0], -1.5);
    println!("AND(0,0) = {}", and_gate.forward(&[0.0, 0.0])); // 0
    println!("AND(0,1) = {}", and_gate.forward(&[0.0, 1.0])); // 0
    println!("AND(1,1) = {}", and_gate.forward(&[1.0, 1.0])); // 1

    // OR 门:w1=1, w2=1, bias=-0.5
    let or_gate = Neuron::new(vec![1.0, 1.0], -0.5);
    println!("OR(0,1)  = {}", or_gate.forward(&[0.0, 1.0]));  // 1
}

二、感知机

2.1 感知机的定义

1957 年,Frank Rosenblatt 提出了感知机(Perceptron),这是最简单的神经网络模型。感知机使用阶跃函数作为激活函数:

$$y = \text{sign}(\mathbf{w}^T \mathbf{x} + b) = \begin{cases} 1 & \text{if } \mathbf{w}^T \mathbf{x} + b \ge 0 \ 0 & \text{otherwise} \end{cases}$$

2.2 感知机学习规则

感知机通过迭代更新权重来学习。对于误分类的样本 $(x_i, y_i)$,更新规则为:

$$w_j \leftarrow w_j + \eta (y_i - \hat{y}i) x{ij}$$

$$b \leftarrow b + \eta (y_i - \hat{y}_i)$$

其中 $\eta$ 是学习率,$y_i$ 是真实标签,$\hat{y}_i$ 是预测值。

/// 感知机分类器
struct Perceptron {
    weights: Vec<f64>,
    bias: f64,
    learning_rate: f64,
}

impl Perceptron {
    fn new(n_inputs: usize, lr: f64) -> Self {
        Perceptron {
            weights: vec![0.0; n_inputs],
            bias: 0.0,
            learning_rate: lr,
        }
    }

    fn predict(&self, inputs: &[f64]) -> i32 {
        let sum = self.weights.iter()
            .zip(inputs.iter())
            .map(|(w, x)| w * x)
            .sum::<f64>() + self.bias;
        if sum >= 0.0 { 1 } else { 0 }
    }

    /// 训练一个 epoch
    fn train_epoch(&mut self, samples: &[(Vec<f64>, i32)]) {
        for (inputs, label) in samples {
            let prediction = self.predict(inputs);
            let error = (*label - prediction) as f64 * self.learning_rate;
            for w in &mut self.weights {
                *w += error * 1.0; // 简化:假设 inputs 为 0/1
            }
            for (w, x) in self.weights.iter_mut().zip(inputs.iter()) {
                *w += error * x;
            }
            self.bias += error;
        }
    }
}

fn main() {
    // AND 逻辑门的训练数据
    let samples = vec![
        (vec![0.0, 0.0], 0),
        (vec![0.0, 1.0], 0),
        (vec![1.0, 0.0], 0),
        (vec![1.0, 1.0], 1),
    ];

    let mut p = Perceptron::new(2, 0.1);
    for epoch in 0..20 {
        p.train_epoch(&samples);
    }

    println!("AND(0,0) = {}", p.predict(&[0.0, 0.0])); // 0
    println!("AND(1,1) = {}", p.predict(&[1.0, 1.0])); // 1
    println!("权重: {:?}", p.weights);
    println!("偏置: {}", p.bias);
}

局限性: 单层感知机只能解决线性可分问题(如 AND、OR),无法解决 XOR 等非线性问题。这一局限促使了多层神经网络的发展。


三、神经网络的结构

3.1 多层感知机(MLP)

多层感知机由输入层、一个或多个隐藏层、输出层组成:

输入层        隐藏层        输出层
  x1 ──╲    ╱── w1' ──╲
         ╲╱             ╲╱── y
  x2 ──╱    ╲── w2' ──╱
  • 输入层:接收原始特征,节点数等于特征维度
  • 隐藏层:提取高阶特征,层数和宽度决定模型容量
  • 输出层:产生最终预测

3.2 权重与偏置

一个 $L$ 层神经网络中,第 $l$ 层的参数为:

  • 权重矩阵 $W^{(l)} \in \mathbb{R}^{n_l \times n_{l-1}}$
  • 偏置向量 $\mathbf{b}^{(l)} \in \mathbb{R}^{n_l}$

3.3 前向传播

数据从输入层逐层传递到输出层的过程:

$$\mathbf{z}^{(l)} = W^{(l)} \mathbf{a}^{(l-1)} + \mathbf{b}^{(l)}$$

$$\mathbf{a}^{(l)} = f(\mathbf{z}^{(l)})$$

其中 $\mathbf{a}^{(0)} = \mathbf{x}$(输入),$\mathbf{a}^{(L)}$ 是最终输出。

#![allow(unused)]
fn main() {
/// 简易多层感知机(纯 Rust 实现)
struct Layer {
    weights: Vec<Vec<f64>>,  // [output_dim][input_dim]
    bias: Vec<f64>,          // [output_dim]
}

struct MLP {
    layers: Vec<Layer>,
}

impl MLP {
    /// 创建一个 784 -> 128 -> 10 的网络(MNIST 结构)
    fn new_mnist() -> Self {
        let mut rng = rand_simple();
        MLP {
            layers: vec![
                Layer::random(128, 784, &mut rng),
                Layer::random(10, 128, &mut rng),
            ],
        }
    }

    /// 前向传播
    fn forward(&self, input: &[f64]) -> Vec<f64> {
        let mut activations = input.to_vec();
        for (i, layer) in self.layers.iter().enumerate() {
            let z = layer.forward(&activations);
            // 最后一层用 softmax,隐藏层用 relu
            if i == self.layers.len() - 1 {
                activations = softmax(&z);
            } else {
                activations = z.iter().map(|&v| relu(v)).collect();
            }
        }
        activations
    }
}

impl Layer {
    fn random(output_dim: usize, input_dim: usize, rng: &mut impl Fn() -> f64) -> Self {
        let scale = (2.0 / input_dim as f64).sqrt(); // He 初始化
        let weights = (0..output_dim)
            .map(|_| (0..input_dim).map(|_| rng() * 2.0 * scale - scale).collect())
            .collect();
        let bias = vec![0.0; output_dim];
        Layer { weights, bias }
    }

    fn forward(&self, input: &[f64]) -> Vec<f64> {
        self.weights.iter().zip(self.bias.iter())
            .map(|(w_row, &b)| {
                w_row.iter().zip(input.iter())
                    .map(|(w, x)| w * x)
                    .sum::<f64>() + b
            })
            .collect()
    }
}

/// 简易伪随机数生成器
fn rand_simple() -> impl Fn() -> f64 {
    let mut state: u64 = 42;
    move || {
        state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
        (state >> 33) as f64 / u32::MAX as f64
    }
}
}

四、激活函数

激活函数为神经网络引入非线性,使其能够拟合复杂的函数。

4.1 常见激活函数

激活函数公式导数特点
Sigmoid$\sigma(x) = \frac{1}{1+e^{-x}}$$\sigma’(x) = \sigma(x)(1-\sigma(x))$输出 (0,1),易梯度消失
Tanh$\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}$$1 - \tanh^2(x)$输出 (-1,1),零中心
ReLU$\text{ReLU}(x) = \max(0, x)$$\begin{cases}1 & x>0\0 & x<0\end{cases}$计算高效,缓解梯度消失
Leaky ReLU$\max(\alpha x, x)$$\begin{cases}1 & x>0\\alpha & x<0\end{cases}$解决 ReLU 神经元死亡
Softmax$\frac{e^{z_i}}{\sum_j e^{z_j}}$多分类输出层

4.2 Rust 实现

/// 激活函数集合
fn sigmoid(x: f64) -> f64 {
    1.0 / (1.0 + (-x).exp())
}

fn sigmoid_derivative(x: f64) -> f64 {
    let s = sigmoid(x);
    s * (1.0 - s)
}

fn tanh_activation(x: f64) -> f64 {
    x.tanh()
}

fn relu(x: f64) -> f64 {
    if x > 0.0 { x } else { 0.0 }
}

fn relu_derivative(x: f64) -> f64 {
    if x > 0.0 { 1.0 } else { 0.0 }
}

fn leaky_relu(x: f64, alpha: f64) -> f64 {
    if x > 0.0 { x } else { alpha * x }
}

/// Softmax:将向量转为概率分布
fn softmax(logits: &[f64]) -> Vec<f64> {
    let max = logits.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
    let exps: Vec<f64> = logits.iter().map(|&x| (x - max).exp()).collect();
    let sum: f64 = exps.iter().sum();
    exps.iter().map(|e| e / sum).collect()
}

fn main() {
    // Sigmoid 测试
    println!("sigmoid(0)   = {:.4}", sigmoid(0.0));    // 0.5000
    println!("sigmoid(10)  = {:.4}", sigmoid(10.0));   // ≈1.0000
    println!("sigmoid(-10) = {:.4}", sigmoid(-10.0));  // ≈0.0000

    // ReLU 测试
    println!("relu(-3) = {}", relu(-3.0));  // 0
    println!("relu(5)  = {}", relu(5.0));   // 5

    // Softmax 测试
    let logits = [2.0, 1.0, 0.1];
    let probs = softmax(&logits);
    println!("softmax([2, 1, 0.1]) = {:?}", probs);
    // ≈ [0.6590, 0.2424, 0.0986]
}

4.3 激活函数的选择

  • 隐藏层:优先使用 ReLU 及其变体(Leaky ReLU、GELU),计算高效且缓解梯度消失
  • 二分类输出层:使用 Sigmoid,输出概率值
  • 多分类输出层:使用 Softmax,输出概率分布
  • 回归输出层:不使用激活函数(线性输出)

五、损失函数

损失函数衡量模型预测值与真实值之间的差距,是训练过程中需要最小化的目标。

5.1 均方误差(MSE)

用于回归任务:

$$\mathcal{L}{\text{MSE}} = \frac{1}{n} \sum{i=1}^{n} (y_i - \hat{y}_i)^2$$

5.2 交叉熵损失

用于分类任务。二分类交叉熵:

$$\mathcal{L}{\text{CE}} = -\frac{1}{n} \sum{i=1}^{n} \left[ y_i \ln(\hat{y}_i) + (1 - y_i) \ln(1 - \hat{y}_i) \right]$$

多分类交叉熵:

$$\mathcal{L}{\text{CE}} = -\sum{i=1}^{n} \sum_{j=1}^{C} y_{ij} \ln(\hat{y}_{ij})$$

其中 $C$ 是类别数,$y_{ij}$ 是 one-hot 编码的真实标签。

/// 损失函数
fn mse_loss(predictions: &[f64], targets: &[f64]) -> f64 {
    let n = predictions.len() as f64;
    predictions.iter()
        .zip(targets.iter())
        .map(|(p, t)| (p - t).powi(2))
        .sum::<f64>() / n
}

/// 二分类交叉熵
fn binary_cross_entropy(predictions: &[f64], targets: &[f64]) -> f64 {
    let n = predictions.len() as f64;
    predictions.iter()
        .zip(targets.iter())
        .map(|(p, &t)| {
            let p_clamped = p.clamp(1e-7, 1.0 - 1e-7);
            -t * p_clamped.ln() - (1.0 - t) * (1.0 - p_clamped).ln()
        })
        .sum::<f64>() / n
}

/// 多分类交叉熵(targets 为 one-hot 编码)
fn cross_entropy_loss(predictions: &[f64], targets: &[f64]) -> f64 {
    predictions.iter()
        .zip(targets.iter())
        .map(|(p, &t)| {
            if t > 0.0 {
                -t * p.clamp(1e-7, 1.0).ln()
            } else {
                0.0
            }
        })
        .sum()
}

fn main() {
    let preds = vec![0.9, 0.1, 0.3];
    let targets = vec![1.0, 0.0, 0.0];
    println!("MSE = {:.6}", mse_loss(&preds, &targets));
    println!("CE = {:.6}", cross_entropy_loss(&preds, &targets));
}

六、梯度下降与优化器

6.1 梯度下降法

梯度下降通过沿损失函数梯度的反方向更新参数来最小化损失:

$$\theta \leftarrow \theta - \eta \nabla_\theta \mathcal{L}$$

其中 $\eta$ 是学习率,$\nabla_\theta \mathcal{L}$ 是损失对参数的梯度。

变体特点
批量梯度下降(BGD)使用全部样本计算梯度,稳定但慢
随机梯度下降(SGD)每次只用一个样本,快速但震荡
小批量梯度下降(Mini-batch SGD)折中方案,实践中最常用

6.2 动量法(Momentum)

加入动量项来加速收敛、减少震荡:

$$v_t = \gamma v_{t-1} + \eta \nabla_\theta \mathcal{L}$$

$$\theta \leftarrow \theta - v_t$$

其中 $\gamma$ 通常取 0.9。

6.3 Adam 优化器

Adam(Adaptive Moment Estimation)结合了动量法和自适应学习率:

$$m_t = \beta_1 m_{t-1} + (1 - \beta_1) g_t \quad \text{(一阶矩估计)}$$

$$v_t = \beta_2 v_{t-1} + (1 - \beta_2) g_t^2 \quad \text{(二阶矩估计)}$$

$$\hat{m}_t = \frac{m_t}{1 - \beta_1^t}, \quad \hat{v}_t = \frac{v_t}{1 - \beta_2^t} \quad \text{(偏差修正)}$$

$$\theta \leftarrow \theta - \eta \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}$$

默认参数:$\beta_1 = 0.9$,$\beta_2 = 0.999$,$\epsilon = 10^{-8}$。

/// Adam 优化器的 Rust 实现
struct AdamOptimizer {
    lr: f64,
    beta1: f64,
    beta2: f64,
    epsilon: f64,
    m: Vec<f64>,  // 一阶矩
    v: Vec<f64>,  // 二阶矩
    t: usize,     // 时间步
}

impl AdamOptimizer {
    fn new(params: &[f64], lr: f64) -> Self {
        AdamOptimizer {
            lr,
            beta1: 0.9,
            beta2: 0.999,
            epsilon: 1e-8,
            m: vec![0.0; params.len()],
            v: vec![0.0; params.len()],
            t: 0,
        }
    }

    /// 单步更新:params 就地修改
    fn step(&mut self, params: &mut [f64], grads: &[f64]) {
        self.t += 1;
        for i in 0..params.len() {
            // 更新一阶矩和二阶矩
            self.m[i] = self.beta1 * self.m[i] + (1.0 - self.beta1) * grads[i];
            self.v[i] = self.beta2 * self.v[i] + (1.0 - self.beta2) * grads[i].powi(2);

            // 偏差修正
            let m_hat = self.m[i] / (1.0 - self.beta1.powi(self.t as i32));
            let v_hat = self.v[i] / (1.0 - self.beta2.powi(self.t as i32));

            // 参数更新
            params[i] -= self.lr * m_hat / (v_hat.sqrt() + self.epsilon);
        }
    }
}

fn main() {
    // 示例:用 Adam 最小化 f(x) = (x-3)^2
    let mut x = vec![0.0];
    let mut optimizer = AdamOptimizer::new(&x, 0.1);

    for step in 1..=100 {
        // 梯度:df/dx = 2(x-3)
        let grad = vec![2.0 * (x[0] - 3.0)];
        optimizer.step(&mut x, &grad);
        if step % 20 == 0 {
            println!("Step {}: x = {:.6}, f(x) = {:.6}", step, x[0], (x[0] - 3.0).powi(2));
        }
    }
    // 最终 x ≈ 3.0
}

七、反向传播(链式法则)

7.1 链式法则

反向传播是训练神经网络的核心算法,其数学基础是微积分的链式法则。对于复合函数 $y = f(g(x))$:

$$\frac{\partial y}{\partial x} = \frac{\partial y}{\partial g} \cdot \frac{\partial g}{\partial x}$$

在神经网络中,损失 $\mathcal{L}$ 对第 $l$ 层权重 $W^{(l)}$ 的梯度需要从输出层逐层回传:

$$\frac{\partial \mathcal{L}}{\partial W^{(l)}} = \frac{\partial \mathcal{L}}{\partial \mathbf{z}^{(l)}} \cdot \frac{\partial \mathbf{z}^{(l)}}{\partial W^{(l)}}$$

其中 $\boldsymbol{\delta}^{(l)} = \frac{\partial \mathcal{L}}{\partial \mathbf{z}^{(l)}}$ 称为误差项

7.2 反向传播的四个步骤

  1. 前向传播:计算每一层的 $\mathbf{z}^{(l)}$ 和 $\mathbf{a}^{(l)}$
  2. 计算输出层误差:$\boldsymbol{\delta}^{(L)} = \nabla_{\mathbf{a}^{(L)}} \mathcal{L} \odot f’(\mathbf{z}^{(L)})$
  3. 反向传播误差:$\boldsymbol{\delta}^{(l)} = (W^{(l+1)})^T \boldsymbol{\delta}^{(l+1)} \odot f’(\mathbf{z}^{(l)})$
  4. 计算梯度:$\frac{\partial \mathcal{L}}{\partial W^{(l)}} = \boldsymbol{\delta}^{(l)} (\mathbf{a}^{(l-1)})^T$
#![allow(unused)]
fn main() {
/// 反向传播的 Rust 实现(两层网络:输入 -> 隐藏 -> 输出)
fn backward(
    x: &[f64],             // 输入
    y: &[f64],             // 真实标签(one-hot)
    w1: &[Vec<f64>],       // 隐藏层权重
    w2: &[Vec<f64>],       // 输出层权重
    z1: &[f64],            // 隐藏层线性输出
    a1: &[f64],            // 隐藏层激活输出
    a2: &[f64],            // 输出层激活输出
) -> (Vec<Vec<f64>>, Vec<Vec<f64>>, Vec<f64>, Vec<f64>) {
    let n_hidden = w1.len();
    let n_input = w1[0].len();
    let n_output = w2.len();

    // 步骤2:输出层误差 delta2 = (a2 - y) * softmax_derivative
    // softmax + 交叉熵的梯度简化为 (a2 - y)
    let mut delta2 = vec![0.0; n_output];
    for i in 0..n_output {
        delta2[i] = a2[i] - y[i];
    }

    // 步骤3:隐藏层误差 delta1 = (W2^T * delta2) * relu'(z1)
    let mut delta1 = vec![0.0; n_hidden];
    for j in 0..n_hidden {
        let mut sum = 0.0;
        for k in 0..n_output {
            sum += w2[k][j] * delta2[k];
        }
        delta1[j] = sum * relu_derivative(z1[j]);
    }

    // 步骤4:计算梯度
    // dW2 = delta2 * a1^T
    let mut dw2 = vec![vec![0.0; n_hidden]; n_output];
    for k in 0..n_output {
        for j in 0..n_hidden {
            dw2[k][j] = delta2[k] * a1[j];
        }
    }

    // dW1 = delta1 * x^T
    let mut dw1 = vec![vec![0.0; n_input]; n_hidden];
    for j in 0..n_hidden {
        for i in 0..n_input {
            dw1[j][i] = delta1[j] * x[i];
        }
    }

    // db2 = delta2
    let db2 = delta2.clone();
    // db1 = delta1
    let db1 = delta1.clone();

    (dw1, dw2, db1, db2)
}
}

计算图视角: 反向传播本质上是自动微分在计算图上的应用。PyTorch 的 autograd 和 Rust 框架 burn 的自动微分系统都基于这一原理。


八、经典神经网络架构

8.1 卷积神经网络(CNN)

CNN 专为处理网格状数据(如图像)设计,核心操作是卷积

$$(f * g)(i, j) = \sum_m \sum_n f(m, n) \cdot g(i-m, j-n)$$

CNN 的关键组件:

组件作用
卷积层提取局部特征(边缘、纹理等)
池化层降低空间维度,增强平移不变性
全连接层综合特征进行分类

经典 CNN 架构:

模型年份特点
LeNet-51998手写数字识别,7 层
AlexNet2012ImageNet 冠军,ReLU 激活
VGGNet2014使用小卷积核 (3x3) 堆叠
ResNet2015残差连接,解决退化问题
/// 简易 2D 卷积操作(无 padding,stride=1)
fn conv2d(input: &[Vec<f64>], kernel: &[Vec<f64>]) -> Vec<Vec<f64>> {
    let (h_in, w_in) = (input.len(), input[0].len());
    let (k_h, k_w) = (kernel.len(), kernel[0].len());
    let h_out = h_in - k_h + 1;
    let w_out = w_in - k_w + 1;

    let mut output = vec![vec![0.0; w_out]; h_out];
    for i in 0..h_out {
        for j in 0..w_out {
            let mut sum = 0.0;
            for ki in 0..k_h {
                for kj in 0..k_w {
                    sum += input[i + ki][j + kj] * kernel[ki][kj];
                }
            }
            output[i][j] = sum;
        }
    }
    output
}

/// 最大池化(2x2)
fn max_pool2d(input: &[Vec<f64>]) -> Vec<Vec<f64>> {
    let h = input.len() / 2;
    let w = input[0].len() / 2;
    let mut output = vec![vec![0.0; w]; h];
    for i in 0..h {
        for j in 0..w {
            output[i][j] = [
                input[2*i][2*j], input[2*i][2*j+1],
                input[2*i+1][2*j], input[2*i+1][2*j+1],
            ].into_iter().cloned().fold(f64::NEG_INFINITY, f64::max);
        }
    }
    output
}

fn main() {
    // 5x5 输入图像
    let image = vec![
        vec![1.0, 2.0, 0.0, 1.0, 3.0],
        vec![0.0, 1.0, 2.0, 3.0, 1.0],
        vec![1.0, 3.0, 1.0, 0.0, 2.0],
        vec![2.0, 0.0, 3.0, 1.0, 0.0],
        vec![1.0, 2.0, 1.0, 2.0, 1.0],
    ];

    // 3x3 边缘检测卷积核
    let kernel = vec![
        vec![-1.0, -1.0, -1.0],
        vec![-1.0,  8.0, -1.0],
        vec![-1.0, -1.0, -1.0],
    ];

    let feature_map = conv2d(&image, &kernel);
    println!("卷积结果 (3x3):");
    for row in &feature_map {
        println!("  {:?}", row.iter().map(|v| format!("{:6.1}", v)).collect::<Vec<_>>());
    }
}

8.2 循环神经网络(RNN)

RNN 专为处理序列数据设计,具有“记忆“能力。在每个时间步 $t$:

$$\mathbf{h}t = \tanh(W_h \mathbf{h}{t-1} + W_x \mathbf{x}_t + \mathbf{b})$$

$$\mathbf{y}_t = W_y \mathbf{h}_t + \mathbf{b}_y$$

RNN 的问题: 长序列训练时会出现梯度消失梯度爆炸,难以捕捉长期依赖。

8.3 LSTM(长短期记忆网络)

LSTM 通过引入门控机制解决了 RNN 的长期依赖问题:

公式作用
遗忘门 $f_t$$\sigma(W_f [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_f)$决定丢弃哪些旧信息
输入门 $i_t$$\sigma(W_i [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_i)$决定写入哪些新信息
候选记忆 $\tilde{C}_t$$\tanh(W_C [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_C)$候选的新记忆内容
输出门 $o_t$$\sigma(W_o [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_o)$决定输出哪些信息

细胞状态更新:

$$C_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t$$

$$h_t = o_t \odot \tanh(C_t)$$

#![allow(unused)]
fn main() {
/// 简易 LSTM 单元(Rust 实现)
struct LSTMCell {
    // 合并权重矩阵 [4 * hidden_size, input_size + hidden_size]
    w_xh: Vec<Vec<f64>>,  // 输入权重
    w_hh: Vec<Vec<f64>>,  // 循环权重
    b: Vec<f64>,          // 偏置
    hidden_size: usize,
}

impl LSTMCell {
    fn new(input_size: usize, hidden_size: usize) -> Self {
        let scale = (2.0 / (input_size + hidden_size) as f64).sqrt();
        let mut rng = rand_simple();
        let w_xh = (0..4 * hidden_size)
            .map(|_| (0..input_size).map(|_| rng() * 2.0 * scale - scale).collect())
            .collect();
        let w_hh = (0..4 * hidden_size)
            .map(|_| (0..hidden_size).map(|_| rng() * 2.0 * scale - scale).collect())
            .collect();
        LSTMCell {
            w_xh, w_hh,
            b: vec![0.0; 4 * hidden_size],
            hidden_size,
        }
    }

    /// 单步前向传播
    fn step(&self, x: &[f64], h_prev: &[f64], c_prev: &[f64])
        -> (Vec<f64>, Vec<f64>)
    {
        let hs = self.hidden_size;
        let mut gates = vec![0.0; 4 * hs];

        // 计算 gates = W_xh * x + W_hh * h_prev + b
        for i in 0..4 * hs {
            let mut sum = self.b[i];
            for j in 0..x.len() {
                sum += self.w_xh[i][j] * x[j];
            }
            for j in 0..hs {
                sum += self.w_hh[i][j] * h_prev[j];
            }
            gates[i] = sum;
        }

        // 分离四个门
        let (f, rest) = gates.split_at(hs);
        let (i, rest) = rest.split_at(hs);
        let (c_candidate, o) = rest.split_at(hs);

        // 更新细胞状态和隐藏状态
        let mut c_next = vec![0.0; hs];
        let mut h_next = vec![0.0; hs];
        for j in 0..hs {
            let f_j = sigmoid(f[j]);
            let i_j = sigmoid(i[j]);
            let c_hat_j = c_candidate[j].tanh();
            c_next[j] = f_j * c_prev[j] + i_j * c_hat_j;
            h_next[j] = sigmoid(o[j]) * c_next[j].tanh();
        }

        (h_next, c_next)
    }
}
}

8.4 Transformer 与注意力机制

2017 年,Vaswani 等人提出了 Transformer 架构(《Attention Is All You Need》),彻底改变了自然语言处理领域。

自注意力机制(Self-Attention) 的核心公式:

$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V$$

其中 $Q$(Query)、$K$(Key)、$V$(Value)均由输入线性变换得到:

$$Q = X W^Q, \quad K = X W^K, \quad V = X W^V$$

多头注意力(Multi-Head Attention) 允许模型同时关注不同位置的不同表示子空间:

$$\text{MultiHead}(Q,K,V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h) W^O$$

$$\text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)$$

/// 简易自注意力机制(Rust 实现)
fn self_attention(x: &[Vec<f64>], w_q: &[Vec<f64>], w_k: &[Vec<f64>], w_v: &[Vec<f64>])
    -> Vec<Vec<f64>>
{
    let seq_len = x.len();
    let d_k = w_q[0].len();

    // 计算 Q, K, V
    let q: Vec<Vec<f64>> = x.iter()
        .map(|xi| mat_vec_mul(w_q, xi)).collect();
    let k: Vec<Vec<f64>> = x.iter()
        .map(|xi| mat_vec_mul(w_k, xi)).collect();
    let v: Vec<Vec<f64>> = x.iter()
        .map(|xi| mat_vec_mul(w_v, xi)).collect();

    // 计算注意力分数:scores = Q * K^T / sqrt(d_k)
    let scale = (d_k as f64).sqrt();
    let mut scores = vec![vec![0.0; seq_len]; seq_len];
    for i in 0..seq_len {
        for j in 0..seq_len {
            let dot: f64 = q[i].iter().zip(k[j].iter())
                .map(|(a, b)| a * b).sum();
            scores[i][j] = dot / scale;
        }
    }

    // Softmax(按行)
    for row in &mut scores {
        let max = row.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
        let exps: Vec<f64> = row.iter().map(|s| (s - max).exp()).collect();
        let sum: f64 = exps.iter().sum();
        for (r, e) in row.iter_mut().zip(exps.iter()) {
            *r = e / sum;
        }
    }

    // 输出 = scores * V
    let mut output = vec![vec![0.0; v[0].len()]; seq_len];
    for i in 0..seq_len {
        for j in 0..v[0].len() {
            output[i][j] = scores[i].iter().zip(v.iter())
                .map(|(s, vk)| s * vk[j]).sum();
        }
    }

    output
}

/// 矩阵-向量乘法
fn mat_vec_mul(mat: &[Vec<f64>], vec: &[f64]) -> Vec<f64> {
    mat.iter().map(|row| {
        row.iter().zip(vec.iter()).map(|(a, b)| a * b).sum()
    }).collect()
}

fn main() {
    // 3 个 token,每个 4 维
    let x = vec![
        vec![1.0, 0.0, 1.0, 0.0],
        vec![0.0, 1.0, 0.0, 1.0],
        vec![1.0, 1.0, 0.0, 0.0],
    ];
    let d_model = 4;

    // 简化:W_q = W_k = W_v = 单位矩阵
    let w = vec![vec![0.0; d_model]; d_model];
    let identity: Vec<Vec<f64>> = (0..d_model)
        .map(|i| (0..d_model).map(|j| if i == j { 1.0 } else { 0.0 }).collect())
        .collect();

    let output = self_attention(&x, &identity, &identity, &identity);
    println!("自注意力输出:");
    for row in &output {
        println!("  {:?}", row.iter().map(|v| format!("{:.4}", v)).collect::<Vec<_>>());
    }
}

Transformer 是 GPT、BERT、LLaMA 等大语言模型的基石架构。从 2017 年至今,Transformer 及其变体主导了整个 AI 领域。


九、实战:手写数字识别(MNIST)

MNIST 是深度学习的“Hello World“,包含 60000 张训练图像和 10000 张测试图像,每张为 $28 \times 28$ 像素的灰度手写数字。

9.1 使用 candle 框架

candle 是 HuggingFace 开发的 Rust 深度学习框架,支持 GPU 加速,API 风格类似 PyTorch。

# Cargo.toml
[dependencies]
candle-nn = "0.8"
candle-core = "0.8"
use candle_core::{Tensor, Device, Result};
use candle_nn::{Linear, VarBuilder, Module};

/// MNIST 分类网络:784 -> 256 -> 128 -> 10
struct MnistModel {
    fc1: Linear,
    fc2: Linear,
    fc3: Linear,
}

impl MnistModel {
    fn new(vs: VarBuilder) -> Result<Self> {
        Ok(MnistModel {
            fc1: candle_nn::linear(784, 256, vs.pp("fc1"))?,
            fc2: candle_nn::linear(256, 128, vs.pp("fc2"))?,
            fc3: candle_nn::linear(128, 10, vs.pp("fc3"))?,
        })
    }

    fn forward(&self, x: &Tensor) -> Result<Tensor> {
        let x = self.fc1.forward(x)?;
        let x = x.relu()?;
        let x = self.fc2.forward(x)?;
        let x = x.relu()?;
        self.fc3.forward(&x)  // 交叉熵损失自带 softmax
    }
}

fn main() -> Result<()> {
    let device = Device::cuda_if_available(0)?;

    // 创建模型(随机初始化)
    let vs = VarBuilder::new(
        candle_nn::Init::KaimingUniform,
        1.0 / 256.0,
        candle_core::DType::F32,
        &device,
    );
    let model = MnistModel::new(vs)?;

    // 模拟输入:batch_size=4, 每张图展平为 784 维
    let dummy_input = Tensor::randn(0f32, 1f32, (4, 784), &device)?;
    let logits = model.forward(&dummy_input)?;
    println!("输出形状: {:?}", logits.shape());  // [4, 10]

    // 预测类别
    let predicted = logits.argmax(1)?;
    println!("预测类别: {:?}", predicted.to_vec1::<u32>()?);

    Ok(())
}

9.2 使用 burn 框架

burn 是另一个优秀的 Rust 深度学习框架,支持多种后端(CPU/CUDA/WASM)。

#![allow(unused)]
fn main() {
use burn::{
    module::Module,
    tensor::{backend::{AutodiffBackend, Backend}, Tensor},
    nn::{Linear, LinearConfig, ReLU},
};

/// 使用 burn 的 MNIST 模型定义
#[derive(Module, Debug)]
pub struct MnistModel<B: Backend> {
    fc1: Linear<B>,
    fc2: Linear<B>,
    fc3: Linear<B>,
    relu: ReLU,
}

impl<B: Backend> MnistModel<B> {
    pub fn new(device: &B::Device) -> Self {
        Self {
            fc1: LinearConfig::new(784, 256).init(device),
            fc2: LinearConfig::new(256, 128).init(device),
            fc3: LinearConfig::new(128, 10).init(device),
            relu: ReLU::new(),
        }
    }

    pub fn forward(&self, x: Tensor<B, 2>) -> Tensor<B, 2> {
        let x = self.fc1.forward(x);
        let x = self.relu.forward(x);
        let x = self.fc2.forward(x);
        let x = self.relu.forward(x);
        self.fc3.forward(x)
    }
}
}

十、Rust AI 生态

10.1 主要框架对比

框架特点适用场景
candleHuggingFace 出品,PyTorch 风格,支持 CUDA/Metal推理、微调、NLP/CV 任务
burn模块化设计,支持多后端,自动微分研究、训练、生产部署
ndarray纯 Rust 数值计算库,类似 NumPy底层数值运算、数据分析
smartcore传统机器学习算法库(SVM、随机森林等)经典 ML 任务
linfaRust 机器学习工具箱(类似 scikit-learn)传统 ML、数据分析
ortONNX Runtime 绑定,可运行训练好的模型跨框架模型推理

10.2 Rust 在 AI 领域的优势

  • 内存安全:无数据竞争,适合长时间运行的推理服务
  • 零成本抽象:性能媲美 C++,适合高性能推理
  • 无 GC 停顿:稳定的延迟,适合实时推理场景
  • WASM 支持:可在浏览器中运行 ML 模型
  • 易于部署:单一二进制文件,无依赖地狱
/// 使用 ndarray 进行矩阵运算(神经网络基础)
use ndarray::{Array2, Array1, array};

fn main() {
    // 权重矩阵 3x2
    let w = array![[0.5, 0.3],
                    [0.2, 0.8],
                    [0.1, 0.6]];

    // 输入向量 2x1
    let x = array![1.0, 2.0];

    // 矩阵乘法:z = W * x + b
    let b = array![0.1, -0.1, 0.2];
    let z = w.dot(&x) + b;
    println!("线性输出 z = {:?}", z);  // [1.0, 1.6, 1.5]

    // ReLU 激活
    let a: Array1<f64> = z.mapv(|v| v.max(0.0));
    println!("ReLU 激活后 a = {:?}", a);  // [1.0, 1.6, 1.5]

    // Softmax
    let exp_a = a.mapv(|v| v.exp());
    let sum_exp = exp_a.sum();
    let probs = exp_a / sum_exp;
    println!("Softmax 输出 = {:?}", probs);
}

十一、神经网络与密码学的交叉

11.1 侧信道攻击

神经网络可被用于分析密码系统的侧信道信息(如功耗、电磁辐射、时间),从而推断密钥:

  • 功耗分析攻击:通过分析加密设备在不同操作下的功耗差异,推断内部密钥位
  • 电磁辐射分析:捕获设备运算时的电磁泄漏,提取敏感信息
  • 模板攻击:使用大量样本训练攻击模型,对目标设备的侧信道信号进行分类

11.2 AI 辅助密码分析

  • 使用 CNN 分析功耗轨迹,自动提取特征
  • 使用 RNN/LSTM 分析时间序列侧信道数据
  • 使用 GAN 生成对抗样本来测试密码系统的鲁棒性

11.3 防御策略

防御方法原理
掩码技术在计算中引入随机掩码,隐藏真实数据
隐藏技术使所有操作的功耗/时间恒定
噪声注入向侧信道信号中添加随机噪声
对抗训练使用对抗样本增强模型的鲁棒性

十二、总结

主题核心要点
人工神经元$y = f(\mathbf{w}^T \mathbf{x} + b)$,加权求和 + 激活
感知机最简单的线性分类器,无法解决 XOR
激活函数ReLU(隐藏层)、Sigmoid/Softmax(输出层)
前向传播逐层计算 $\mathbf{z}^{(l)} = W^{(l)}\mathbf{a}^{(l-1)} + \mathbf{b}^{(l)}$
损失函数MSE(回归)、交叉熵(分类)
梯度下降SGD、Momentum、Adam(最常用)
反向传播基于链式法则,逐层计算梯度
CNN卷积 + 池化,擅长图像处理
RNN/LSTM处理序列数据,门控机制解决长期依赖
Transformer自注意力机制,大语言模型的基石
Rust 生态candle(推理)、burn(训练)、ndarray(数值计算)

神经网络是现代人工智能的核心技术。从简单的感知机到复杂的 Transformer,从 MNIST 手写识别到大语言模型,神经网络的发展深刻改变了技术世界。Rust 凭借其安全性和高性能,正在成为 AI 基础设施的重要选择。

练习建议:

  1. 从零实现一个两层 MLP,在 MNIST 子集上完成训练和推理
  2. 用 Rust 实现 SGD 和 Adam 优化器,对比收敛速度
  3. 使用 candle 框架加载预训练模型,完成文本分类或图像识别任务
  4. 实现一个简易的自注意力模块,理解 Transformer 的核心机制

第三十七 自然语言处理(NLP, Natural Language Processing)

第三十七 自然语言处理
├── 什么是自然语言处理(NLP)
├── 文本预处理
│   ├── 分词
│   ├── 清洗与标准化
│   └── 词干提取与词形还原
├── 文本表示
│   ├── 词袋模型(BoW)
│   ├── TF-IDF
│   └── 词嵌入(Word2Vec)
├── 语言模型
│   ├── 统计语言模型(N-gram)
│   ├── 神经网络语言模型
│   └── Transformer 与自注意力机制
├── 大语言模型(LLM)
│   ├── Token 与词嵌入
│   ├── AI Agent(智能体)
│   └── 典型大模型介绍
├── Rust NLP 生态
│   ├── rust-bert / candle / burn
│   └── 实战:用 Rust 实现 TF-IDF
└── 总结与练习

自然语言处理(Natural Language Processing,简称 NLP)是人工智能领域中最具魅力的方向之一。它的目标是让计算机理解、生成和处理人类自然语言——从搜索引擎的关键词匹配,到手机里的语音助手,再到如今能写代码、做推理的大语言模型,NLP 已经深刻地改变了我们的日常生活。

$$\text{自然语言处理} = \text{语言学} + \text{计算机科学} + \text{数学(概率论/线性代数)}$$


一、什么是自然语言处理

1.1 NLP 的定义

自然语言处理是计算机科学、人工智能和语言学的交叉学科,研究如何让计算机有效地处理和理解人类自然语言。NLP 的核心任务是弥合人类语言(模糊、上下文依赖、高度灵活)与计算机表示(精确、结构化、形式化)之间的鸿沟。

1.2 NLP 的发展历程

时代时间段代表方法特点
规则时代1950s–1980s专家系统、正则匹配依赖人工编写语法规则,泛化能力差
统计时代1990s–2010sN-gram、HMM、CRF基于概率和统计,数据驱动
深度学习时代2013–2017Word2Vec、RNN/LSTM、Seq2Seq神经网络自动学习特征
预训练时代2018–至今BERT、GPT、Transformer大规模预训练 + 微调范式
大模型时代2022–至今ChatGPT、Claude、DeepSeek生成式AI、Agent、多模态

1.3 NLP 的核心任务

NLP 的任务可以大致分为以下几类:

  • 文本分类:情感分析、垃圾邮件检测、新闻分类
  • 序列标注:命名实体识别(NER)、词性标注
  • 文本生成:机器翻译、文本摘要、对话系统
  • 信息抽取:关系抽取、事件抽取、知识图谱构建
  • 语义理解:问答系统、阅读理解、语义相似度
/// NLP 任务枚举——用 Rust 类型系统表达领域知识
#[derive(Debug, Clone, PartialEq)]
enum NlpTask {
    TextClassification,   // 文本分类
    NamedEntityRecognition, // 命名实体识别
    MachineTranslation,    // 机器翻译
    TextSummarization,     // 文本摘要
    QuestionAnswering,      // 问答系统
    SentimentAnalysis,     // 情感分析
}

fn describe_task(task: &NlpTask) -> &str {
    match task {
        NlpTask::TextClassification => "将文本归入预定义类别",
        NlpTask::NamedEntityRecognition => "从文本中识别人名、地名、组织名等实体",
        NlpTask::MachineTranslation => "将文本从一种语言翻译为另一种语言",
        NlpTask::TextSummarization => "生成文本的简洁摘要",
        NlpTask::QuestionAnswering => "根据上下文回答问题",
        NlpTask::SentimentAnalysis => "判断文本的情感倾向(正面/负面/中性)",
    }
}

fn main() {
    let tasks = vec![
        NlpTask::TextClassification,
        NlpTask::SentimentAnalysis,
        NlpTask::MachineTranslation,
    ];
    for task in &tasks {
        println!("{:?}: {}", task, describe_task(task));
    }
}

二、文本预处理

文本预处理是 NLP 管道的第一步,也是最关键的一步。原始文本往往包含噪声——标点、特殊字符、大小写不一致、停用词等,需要经过清洗和标准化才能用于后续分析。

2.1 分词(Tokenization)

分词是将连续的文本切分为有意义的单元(词、子词或字符)的过程。中文没有天然的空格分隔符,分词尤为关键。

/// 简单的英文分词器
fn tokenize_english(text: &str) -> Vec<&str> {
    text.split_whitespace()
        .map(|word| word.trim_matches(|c: char| !c.is_alphanumeric()))
        .filter(|w| !w.is_empty())
        .collect()
}

/// 简单的中文分词器(基于字符级别 + 常用词匹配)
fn tokenize_chinese(text: &str) -> Vec<String> {
    // 常用双字词词典(示例)
    let dictionary = vec![
        "自然", "语言", "处理", "人工", "智能", "机器", "学习",
        "深度", "模型", "计算", "科学", "技术", "方法", "数据",
    ];
    let chars: Vec<char> = text.chars().filter(|c| !c.is_whitespace()).collect();
    let mut tokens = Vec::new();
    let mut i = 0;
    while i < chars.len() {
        // 优先尝试匹配双字词
        if i + 1 < chars.len() {
            let bigram: String = chars[i..=i + 1].iter().collect();
            if dictionary.contains(&bigram.as_str()) {
                tokens.push(bigram);
                i += 2;
                continue;
            }
        }
        // 单字回退
        tokens.push(chars[i].to_string());
        i += 1;
    }
    tokens
}

fn main() {
    let en_text = "Natural Language Processing is fascinating!";
    let en_tokens = tokenize_english(en_text);
    println!("英文分词: {:?}", en_tokens);

    let zh_text = "自然语言处理是人工智能的核心技术";
    let zh_tokens = tokenize_chinese(zh_text);
    println!("中文分词: {:?}", zh_tokens);
}

2.2 文本清洗与标准化

use std::collections::HashSet;

/// 文本清洗管道
struct TextCleaner {
    stopwords: HashSet<String>,
}

impl TextCleaner {
    fn new() -> Self {
        let stopwords: HashSet<String> = [
            "的", "了", "在", "是", "我", "有", "和", "就",
            "不", "人", "都", "一", "一个", "上", "也", "很",
            "the", "a", "an", "is", "are", "was", "were",
            "and", "or", "but", "in", "on", "at", "to",
        ]
        .iter()
        .map(|s| s.to_string())
        .collect();
        TextCleaner { stopwords }
    }

    /// 转小写(英文)
    fn to_lowercase(&self, text: &str) -> String {
        text.to_lowercase()
    }

    /// 去除标点和特殊字符
    fn remove_punctuation(&self, text: &str) -> String {
        text.chars()
            .filter(|c| c.is_alphanumeric() || c.is_whitespace() || *c == '\u{4e00}..='\u{9fff}')
            .collect()
    }

    /// 去除停用词
    fn remove_stopwords(&self, tokens: &[String]) -> Vec<String> {
        tokens.iter().filter(|t| !self.stopwords.contains(*t)).cloned().collect()
    }

    /// 完整清洗管道
    fn clean(&self, text: &str) -> Vec<String> {
        let lowered = self.to_lowercase(text);
        let tokens: Vec<String> = lowered
            .split_whitespace()
            .map(|s| s.to_string())
            .collect();
        self.remove_stopwords(&tokens)
    }
}

fn main() {
    let cleaner = TextCleaner::new();
    let text = "Natural Language Processing is a fascinating field of study";
    let cleaned = cleaner.clean(text);
    println!("清洗前: {}", text);
    println!("清洗后: {:?}", cleaned);
}

2.3 词干提取与词形还原

词干提取(Stemming)是将词还原为词干的过程,例如 “running” -> “run”。词形还原(Lemmatization)则是将词还原为词典中的原形,例如 “better” -> “good”。

/// 简易英文词干提取器(Porter 算法简化版)
fn stem(word: &str) -> String {
    let mut result = word.to_string();

    // 去除常见后缀
    let suffixes = [
        "ing", "tion", "ness", "ment", "able", "ible",
        "ful", "less", "ous", "ive", "ed", "er", "ly", "es", "s",
    ];

    for suffix in &suffixes {
        if result.ends_with(suffix) && result.len() > suffix.len() + 2 {
            result = result[..result.len() - suffix.len()].to_string();
            break;
        }
    }

    result
}

fn main() {
    let words = ["running", "happiness", "computing", "played", "easily"];
    for word in &words {
        println!("{} -> {}", word, stem(word));
    }
    // running -> runn
    // happiness -> happi
    // computing -> comput
    // played -> play
    // easily -> eas
}

三、文本表示

计算机无法直接处理文本字符串,需要将文本转换为数值向量。文本表示方法经历了从离散到稠密、从人工设计到自动学习的演进。

3.1 词袋模型(Bag of Words, BoW)

词袋模型是最简单的文本表示方法。它忽略词序,仅统计每个词在文档中出现的次数。

给定文档集合 $D = {d_1, d_2, \ldots, d_n}$ 和词汇表 $V = {w_1, w_2, \ldots, w_m}$,文档 $d_i$ 的 BoW 向量为:

$$\text{BoW}(d_i) = [c_{i1}, c_{i2}, \ldots, c_{im}]$$

其中 $c_{ij}$ 表示词 $w_j$ 在文档 $d_i$ 中出现的次数。

use std::collections::HashMap;

/// 词袋模型
struct BagOfWords {
    vocabulary: Vec<String>,
    word_to_idx: HashMap<String, usize>,
}

impl BagOfWords {
    /// 从文档集合构建词汇表
    fn fit(documents: &[&str]) -> Self {
        let mut word_set = std::collections::BTreeSet::new();
        for doc in documents {
            for word in doc.split_whitespace() {
                word_set.insert(word.to_string());
            }
        }
        let vocabulary: Vec<String> = word_set.into_iter().collect();
        let word_to_idx: HashMap<String, usize> = vocabulary
            .iter()
            .enumerate()
            .map(|(i, w)| (w.clone(), i))
            .collect();
        BagOfWords { vocabulary, word_to_idx }
    }

    /// 将文档转换为 BoW 向量
    fn transform(&self, document: &str) -> Vec<usize> {
        let mut vector = vec![0usize; self.vocabulary.len()];
        for word in document.split_whitespace() {
            if let Some(&idx) = self.word_to_idx.get(word) {
                vector[idx] += 1;
            }
        }
        vector
    }

    /// 获取词汇表
    fn vocabulary(&self) -> &[String] {
        &self.vocabulary
    }
}

fn main() {
    let docs = [
        "rust language system safety",
        "rust programming memory safety",
        "python programming language",
    ];

    let bow = BagOfWords::fit(&docs);
    println!("词汇表: {:?}", bow.vocabulary());

    for doc in &docs {
        let vec = bow.transform(doc);
        println!("文档 \"{}\" -> {:?}", doc, vec);
    }
}

3.2 TF-IDF

TF-IDF(Term Frequency - Inverse Document Frequency)在词频的基础上引入了逆文档频率,降低了常见词的权重,提升了区分度高的词的权重。

$$\text{TF-IDF}(t, d, D) = \text{TF}(t, d) \times \text{IDF}(t, D)$$

其中:

$$\text{TF}(t, d) = \frac{f_{t,d}}{\sum_{t’ \in d} f_{t’,d}}$$

$$\text{IDF}(t, D) = \log \frac{|D|}{|{d \in D : t \in d}| + 1}$$

use std::collections::HashMap;

/// TF-IDF 计算器
struct TfIdfCalculator {
    vocabulary: Vec<String>,
    word_to_idx: HashMap<String, usize>,
    idf: Vec<f64>, // 每个词的 IDF 值
}

impl TfIdfCalculator {
    fn fit(documents: &[&str]) -> Self {
        // 构建词汇表
        let mut word_set = std::collections::BTreeSet::new();
        for doc in documents {
            for word in doc.split_whitespace() {
                word_set.insert(word.to_string());
            }
        }
        let vocabulary: Vec<String> = word_set.into_iter().collect();
        let word_to_idx: HashMap<String, usize> = vocabulary
            .iter()
            .enumerate()
            .map(|(i, w)| (w.clone(), i))
            .collect();

        // 计算 IDF
        let n_docs = documents.len() as f64;
        let mut doc_freq = vec![0usize; vocabulary.len()];
        for doc in documents {
            let mut seen = std::collections::HashSet::new();
            for word in doc.split_whitespace() {
                if let Some(&idx) = word_to_idx.get(word) {
                    if seen.insert(idx) {
                        doc_freq[idx] += 1;
                    }
                }
            }
        }
        let idf: Vec<f64> = doc_freq
            .iter()
            .map(|&df| (n_docs / (df as f64 + 1.0)).ln())
            .collect();

        TfIdfCalculator { vocabulary, word_to_idx, idf }
    }

    fn transform(&self, document: &str) -> Vec<f64> {
        let mut tf = vec![0.0f64; self.vocabulary.len()];
        let mut total = 0usize;
        for word in document.split_whitespace() {
            if let Some(&idx) = self.word_to_idx.get(word) {
                tf[idx] += 1.0;
                total += 1;
            }
        }
        // 归一化 TF
        if total > 0 {
            for v in &mut tf {
                *v /= total as f64;
            }
        }
        // TF * IDF
        tf.iter().zip(self.idf.iter()).map(|(&t, &i)| t * i).collect()
    }
}

fn main() {
    let docs = [
        "rust is a systems programming language",
        "rust focuses on safety and performance",
        "python is a high level programming language",
    ];

    let tfidf = TfIdfCalculator::fit(&docs);
    println!("词汇表: {:?}", tfidf.vocabulary);

    for doc in &docs {
        let vec = tfidf.transform(doc);
        println!("文档: \"{}\"", doc);
        for (word, val) in tfidf.vocabulary.iter().zip(vec.iter()) {
            if *val > 0.0 {
                println!("  {} => {:.4}", word, val);
            }
        }
    }
}

3.3 词嵌入(Word2Vec)

词嵌入将每个词映射为一个低维稠密向量,使得语义相近的词在向量空间中距离更近。Word2Vec 是最经典的词嵌入方法,包含两种训练模式:

  • CBOW(Continuous Bag of Words):用上下文预测中心词
  • Skip-gram:用中心词预测上下文

Word2Vec 的核心思想可以用一个简单的神经网络来理解。给定中心词 $w_t$,其上下文窗口为 $C$,Skip-gram 模型的目标函数为:

$$\mathcal{L} = \sum_{t=1}^{T} \sum_{-c \le j \le c, j \ne 0} \log P(w_{t+j} | w_t)$$

其中条件概率通过 softmax 计算:

$$P(w_{t+j} | w_t) = \frac{\exp(\mathbf{v}‘{w{t+j}} \cdot \mathbf{v}{w_t})}{\sum{w=1}^{V} \exp(\mathbf{v}’w \cdot \mathbf{v}{w_t})}$$

/// 简化的 Skip-gram 词嵌入训练器
use std::collections::HashMap;

struct WordEmbedding {
    word_to_idx: HashMap<String, usize>,
    idx_to_word: Vec<String>,
    // 词向量矩阵 W: vocab_size x embedding_dim
    w: Vec<Vec<f64>>,
    embedding_dim: usize,
    learning_rate: f64,
}

impl WordEmbedding {
    fn new(vocabulary: Vec<String>, embedding_dim: usize, lr: f64) -> Self {
        let vocab_size = vocabulary.len();
        let word_to_idx: HashMap<String, usize> = vocabulary
            .iter()
            .enumerate()
            .map(|(i, w)| (w.clone(), i))
            .collect();
        let idx_to_word = vocabulary.clone();

        // 随机初始化词向量
        let mut rng = rand_simple();
        let w: Vec<Vec<f64>> = (0..vocab_size)
            .map(|_| (0..embedding_dim).map(|_| rng.next_f64() * 0.1 - 0.05).collect())
            .collect();

        WordEmbedding { word_to_idx, idx_to_word, w, embedding_dim, learning_rate: lr }
    }

    /// 简化的 Skip-gram 训练步骤(负采样省略,使用朴素梯度)
    fn train_step(&mut self, center: &str, context: &str) {
        if let (Some(&ci), Some(&oi)) = (self.word_to_idx.get(center), self.word_to_idx.get(context)) {
            let dim = self.embedding_dim;
            let lr = self.learning_rate;

            // 简化:直接拉近中心词和上下文词的向量
            let mut grad = vec![0.0; dim];
            for d in 0..dim {
                grad[d] = self.w[oi][d] - self.w[ci][d];
            }
            for d in 0..dim {
                self.w[ci][d] += lr * grad[d] * 0.01;
                self.w[oi][d] -= lr * grad[d] * 0.01;
            }
        }
    }

    fn get_vector(&self, word: &str) -> Option<&Vec<f64>> {
        self.word_to_idx.get(word).map(|&idx| &self.w[idx])
    }

    /// 余弦相似度
    fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
        let dot: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
        let norm_a: f64 = a.iter().map(|x| x * x).sum::<f64>().sqrt();
        let norm_b: f64 = b.iter().map(|x| x * x).sum::<f64>().sqrt();
        if norm_a == 0.0 || norm_b == 0.0 { return 0.0; }
        dot / (norm_a * norm_b)
    }
}

/// 简易伪随机数生成器
struct SimpleRng { state: u64 }
fn rand_simple() -> SimpleRng { SimpleRng { state: 42 } }
impl SimpleRng {
    fn next_f64(&mut self) -> f64 {
        self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1);
        (self.state >> 33) as f64 / u32::MAX as f64
    }
}

fn main() {
    let vocab = vec![
        "king".into(), "queen".into(), "man".into(), "woman".into(),
        "prince".into(), "princess".into(),
    ];
    let mut emb = WordEmbedding::new(vocab, 10, 0.01);

    // 模拟训练数据(中心词, 上下文词)
    let pairs = [
        ("king", "queen"), ("king", "man"), ("queen", "woman"),
        ("man", "woman"), ("prince", "princess"), ("prince", "king"),
        ("queen", "king"), ("woman", "man"), ("princess", "queen"),
    ];

    for _ in 0..100 {
        for (c, o) in &pairs {
            emb.train_step(c, o);
        }
    }

    // 计算相似度
    if let (Some(v1), Some(v2)) = (emb.get_vector("king"), emb.get_vector("queen")) {
        let sim = WordEmbedding::cosine_similarity(v1, v2);
        println!("king 与 queen 的余弦相似度: {:.4}", sim);
    }
}

四、语言模型

语言模型是 NLP 的基石,它为一段文本分配概率,衡量该文本在语言中出现的合理性。

4.1 统计语言模型(N-gram)

N-gram 模型基于马尔可夫假设:一个词的出现仅依赖于它前面的 $n-1$ 个词。

$$P(w_1, w_2, \ldots, w_T) = \prod_{t=1}^{T} P(w_t | w_{t-n+1}, \ldots, w_{t-1})$$

最常用的是 bigram($n=2$)和 trigram($n=3$)模型:

$$P(w_t | w_{t-1}) = \frac{C(w_{t-1}, w_t)}{C(w_{t-1})}$$

use std::collections::HashMap;

/// Bigram 语言模型
struct BigramModel {
    /// 单词计数
    unigram_counts: HashMap<String, usize>,
    /// 相邻词对计数
    bigram_counts: HashMap<(String, String), usize>,
    /// 总词数
    total_words: usize,
}

impl BigramModel {
    fn new() -> Self {
        BigramModel {
            unigram_counts: HashMap::new(),
            bigram_counts: HashMap::new(),
            total_words: 0,
        }
    }

    /// 从文本训练模型
    fn train(&mut self, text: &str) {
        let words: Vec<&str> = text.split_whitespace().collect();
        for i in 0..words.len() {
            *self.unigram_counts.entry(words[i].to_string()).or_insert(0) += 1;
            self.total_words += 1;
            if i > 0 {
                let key = (words[i - 1].to_string(), words[i].to_string());
                *self.bigram_counts.entry(key).or_insert(0) += 1;
            }
        }
    }

    /// 计算句子概率(加1平滑)
    fn sentence_probability(&self, sentence: &str) -> f64 {
        let words: Vec<&str> = sentence.split_whitespace().collect();
        let vocab_size = self.unigram_counts.len();
        let mut log_prob = 0.0f64;

        for i in 0..words.len() {
            let unigram_count = self.unigram_counts.get(words[i]).unwrap_or(&0) + 1;
            if i > 0 {
                let bigram_count = self.bigram_counts
                    .get(&(words[i - 1].to_string(), words[i].to_string()))
                    .unwrap_or(&0) + 1;
                let prob = (bigram_count as f64) / ((unigram_count + vocab_size) as f64);
                log_prob += prob.ln();
            } else {
                let prob = unigram_count as f64 / (self.total_words + vocab_size) as f64;
                log_prob += prob.ln();
            }
        }
        log_prob
    }

    /// 预测下一个词
    fn predict_next(&self, word: &str) -> Option<String> {
        let prefix = word.to_string();
        let candidates: Vec<_> = self.bigram_counts
            .iter()
            .filter(|((w, _), _)| w == &prefix)
            .collect();

        candidates.into_iter().max_by_key(|(_, &count)| count).map(|((_, w), _)| w.clone())
    }
}

fn main() {
    let corpus = "I love Rust programming . Rust is a systems language . I love programming in Rust .";

    let mut model = BigramModel::new();
    model.train(corpus);

    println!("'I love' 后面最可能的词: {:?}", model.predict_next("love"));
    println!("'Rust is' 后面最可能的词: {:?}", model.predict_next("is"));

    let prob = model.sentence_probability("I love Rust");
    println!("句子概率 (log): {:.4}", prob);
}

4.2 神经网络语言模型

神经网络语言模型用低维稠密向量替代了 N-gram 中的离散计数,能够更好地捕捉语义关系。从 Word2Vec 到 LSTM,再到 Transformer,神经网络语言模型逐步解决了长距离依赖和语义理解的问题。

4.3 Transformer 与自注意力机制

Transformer 是 2017 年由 Vaswani 等人提出的革命性架构,完全基于注意力机制,抛弃了循环和卷积结构。它是当今所有主流大语言模型(GPT、BERT、Claude、DeepSeek 等)的基础。

自注意力机制

自注意力的核心公式为:

$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V$$

其中:

  • $Q$(Query):查询矩阵,表示“我在找什么“
  • $K$(Key):键矩阵,表示“我有什么“
  • $V$(Value):值矩阵,表示“我的内容是什么“
  • $d_k$:键向量的维度,用于缩放以防止梯度消失
/// 自注意力机制的 Rust 实现
struct Attention {
    /// Q, K, V 权重矩阵 (d_model x d_k)
    w_q: Vec<Vec<f64>>,
    w_k: Vec<Vec<f64>>,
    w_v: Vec<Vec<f64>>,
    d_k: usize,
    d_model: usize,
}

impl Attention {
    fn new(d_model: usize, d_k: usize) -> Self {
        let mut rng = rand_simple();
        let random_matrix = |rows, cols| -> Vec<Vec<f64>> {
            (0..rows)
                .map(|_| (0..cols).map(|_| rng.next_f64() * 0.1 - 0.05).collect())
                .collect()
        };
        Attention {
            w_q: random_matrix(d_model, d_k),
            w_k: random_matrix(d_model, d_k),
            w_v: random_matrix(d_model, d_k),
            d_k,
            d_model,
        }
    }

    /// 矩阵乘法 C = A * B^T
    fn mat_mul(a: &[Vec<f64>], b_t: &[Vec<f64>]) -> Vec<Vec<f64>> {
        let m = a.len();
        let n = b_t.len();
        let k = a[0].len();
        let mut c = vec![vec![0.0; n]; m];
        for i in 0..m {
            for j in 0..n {
                for l in 0..k {
                    c[i][j] += a[i][l] * b_t[j][l];
                }
            }
        }
        c
    }

    /// 矩阵乘以向量
    fn mat_vec_mul(mat: &[Vec<f64>], vec: &[f64]) -> Vec<f64> {
        mat.iter()
            .map(|row| row.iter().zip(vec.iter()).map(|(a, b)| a * b).sum())
            .collect()
    }

    /// Softmax 函数
    fn softmax(row: &[f64]) -> Vec<f64> {
        let max_val = row.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
        let exps: Vec<f64> = row.iter().map(|x| (x - max_val).exp()).collect();
        let sum: f64 = exps.iter().sum();
        exps.iter().map(|x| x / sum).collect()
    }

    /// 计算自注意力
    fn forward(&self, input: &[Vec<f64>]) -> Vec<Vec<f64>> {
        let seq_len = input.len();

        // 计算 Q, K, V
        let q: Vec<Vec<f64>> = input.iter().map(|x| Self::mat_vec_mul(&self.w_q, x)).collect();
        let k: Vec<Vec<f64>> = input.iter().map(|x| Self::mat_vec_mul(&self.w_k, x)).collect();
        let v: Vec<Vec<f64>> = input.iter().map(|x| Self::mat_vec_mul(&self.w_v, x)).collect();

        // QK^T / sqrt(d_k)
        let scale = 1.0 / (self.d_k as f64).sqrt();
        let mut scores = Self::mat_mul(&q, &k);
        for row in &mut scores {
            for val in row {
                *val *= scale;
            }
        }

        // Softmax
        let attention_weights: Vec<Vec<f64>> = scores.iter().map(|row| Self::softmax(row)).collect();

        // Attention * V
        let mut output = vec![vec![0.0; self.d_k]; seq_len];
        for i in 0..seq_len {
            for j in 0..self.d_k {
                for l in 0..seq_len {
                    output[i][j] += attention_weights[i][l] * v[l][j];
                }
            }
        }
        output
    }
}

fn main() {
    let d_model = 4;
    let d_k = 4;
    let attention = Attention::new(d_model, d_k);

    // 输入:3个token,每个4维
    let input = vec![
        vec![1.0, 0.0, 1.0, 0.0],
        vec![0.0, 1.0, 0.0, 1.0],
        vec![1.0, 1.0, 0.0, 0.0],
    ];

    let output = attention.forward(&input);
    println!("自注意力输出:");
    for (i, row) in output.iter().enumerate() {
        println!("  token {}: {:?}", i, row.iter().map(|x| format!("{:.4}", x)).collect::<Vec<_>>());
    }
}

Transformer 的整体结构

Transformer 由编码器(Encoder)和解码器(Decoder)两部分组成:

组件功能
多头注意力(Multi-Head Attention)从多个子空间捕捉不同层面的语义关系
前馈神经网络(FFN)对每个位置进行非线性变换
层归一化(Layer Normalization)稳定训练过程
残差连接(Residual Connection)缓解梯度消失,促进信息流动
位置编码(Positional Encoding)为序列注入位置信息

多头注意力的计算方式:

$$\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h) W^O$$

$$\text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)$$


五、大语言模型(LLM)

5.1 Token 与词嵌入

大语言模型处理文本的基本单位是 Token(标记)。Token 不等于单词——它可以是单词、子词或字符。现代 LLM 普遍使用 BPE(Byte Pair Encoding)算法进行分词。

BPE 的核心思想是:反复合并语料中出现频率最高的相邻字符对,逐步构建子词词汇表。

use std::collections::HashMap;

/// 简化的 BPE(Byte Pair Encoding)分词器
struct BpeTokenizer {
    merges: Vec<(String, String)>,  // 合并规则
    vocab: Vec<String>,              // 词汇表
}

impl BpeTokenizer {
    fn new() -> Self {
        BpeTokenizer { merges: Vec::new(), vocab: Vec::new() }
    }

    /// 从语料中学习合并规则
    fn train(&mut self, text: &str, num_merges: usize) {
        // 初始:按字符分词
        let mut word_freq: HashMap<Vec<String>, usize> = HashMap::new();
        for word in text.split_whitespace() {
            let chars: Vec<String> = word.chars().map(|c| c.to_string()).collect();
            *word_freq.entry(chars).or_insert(0) += 1;
        }

        for _ in 0..num_merges {
            // 统计相邻符号对频率
            let mut pair_freq: HashMap<(String, String), usize> = HashMap::new();
            for (word, freq) in &word_freq {
                for i in 0..word.len().saturating_sub(1) {
                    let pair = (word[i].clone(), word[i + 1].clone());
                    *pair_freq.entry(pair).or_insert(0) += freq;
                }
            }

            // 找到最高频的符号对
            if let Some((&best_pair, _)) = pair_freq.iter().max_by_key(|(_, &f)| f) {
                self.merges.push(best_pair.clone());

                // 合并所有词中的该符号对
                let mut new_word_freq = HashMap::new();
                for (word, freq) in &word_freq {
                    let mut new_word = word.clone();
                    let merged = format!("{}{}", best_pair.0, best_pair.1);
                    let mut i = 0;
                    while i < new_word.len().saturating_sub(1) {
                        if new_word[i] == best_pair.0 && new_word[i + 1] == best_pair.1 {
                            new_word[i] = merged.clone();
                            new_word.remove(i + 1);
                        } else {
                            i += 1;
                        }
                    }
                    *new_word_freq.entry(new_word).or_insert(0) += freq;
                }
                word_freq = new_word_freq;
            } else {
                break;
            }
        }

        // 构建词汇表
        let mut vocab_set = std::collections::BTreeSet::new();
        for word in word_freq.keys() {
            for token in word {
                vocab_set.insert(token.clone());
            }
        }
        self.vocab = vocab_set.into_iter().collect();
    }

    /// 编码文本为 token 序列
    fn encode(&self, text: &str) -> Vec<String> {
        let mut tokens: Vec<String> = text.chars().map(|c| c.to_string()).collect();
        for (a, b) in &self.merges {
            let merged = format!("{}{}", a, b);
            let mut i = 0;
            while i < tokens.len().saturating_sub(1) {
                if tokens[i] == *a && tokens[i + 1] == *b {
                    tokens[i] = merged.clone();
                    tokens.remove(i + 1);
                } else {
                    i += 1;
                }
            }
        }
        tokens
    }
}

fn main() {
    let corpus = "low lower newest widest";
    let mut tokenizer = BpeTokenizer::new();
    tokenizer.train(corpus, 10);

    println!("合并规则: {:?}", tokenizer.merges);
    println!("词汇表: {:?}", tokenizer.vocab);

    let test = "lower newest";
    let encoded = tokenizer.encode(test);
    println!("'{}' => {:?}", test, encoded);
}

5.2 AI Agent(智能体)

AI Agent 是以大语言模型为“大脑“,能够自主感知环境、规划任务、调用工具并执行动作的智能系统。

$$\text{Agent} = \text{LLM(大脑)} + \text{Memory(记忆)} + \text{Tools(工具)} + \text{Planning(规划)}$$

/// 简化的 AI Agent 框架
use std::collections::HashMap;

/// 工具 trait:所有 Agent 可调用的工具都实现此 trait
trait Tool {
    fn name(&self) -> &str;
    fn description(&self) -> &str;
    fn execute(&self, input: &str) -> String;
}

/// 计算器工具
struct CalculatorTool;
impl Tool for CalculatorTool {
    fn name(&self) -> &str { "calculator" }
    fn description(&self) -> &str { "执行数学计算,输入为数学表达式" }
    fn execute(&self, input: &str) -> String {
        // 简化:仅支持两个数的四则运算
        let parts: Vec<&str> = input.split_whitespace().collect();
        if parts.len() == 3 {
            let a: f64 = parts[0].parse().unwrap_or(0.0);
            let b: f64 = parts[2].parse().unwrap_or(0.0);
            let result = match parts[1] {
                "+" => a + b, "-" => a - b,
                "*" => a * b, "/" => if b != 0.0 { a / b } else { f64::NAN },
                _ => f64::NAN,
            };
            format!("{} {} {} = {}", a, parts[1], b, result)
        } else {
            "无法解析表达式".to_string()
        }
    }
}

/// 搜索工具
struct SearchTool;
impl Tool for SearchTool {
    fn name(&self) -> &str { "search" }
    fn description(&self) -> &str { "搜索信息,输入为查询关键词" }
    fn execute(&self, input: &str) -> String {
        format!("搜索结果:关于 '{}' 的信息...", input)
    }
}

/// Agent 结构体
struct Agent {
    tools: HashMap<String, Box<dyn Tool>>,
    memory: Vec<String>,
}

impl Agent {
    fn new() -> Self {
        let mut tools: HashMap<String, Box<dyn Tool>> = HashMap::new();
        tools.insert("calculator".to_string(), Box::new(CalculatorTool));
        tools.insert("search".to_string(), Box::new(SearchTool));
        Agent { tools, memory: Vec::new() }
    }

    /// 模拟 Agent 的推理和工具调用过程
    fn process(&mut self, user_input: &str) -> String {
        self.memory.push(user_input.to_string());

        // 简化的意图识别
        if user_input.contains("计算") || user_input.contains("+") || user_input.contains("*") {
            let tool = self.tools.get("calculator").unwrap();
            let expr = user_input
                .replace("计算", "")
                .replace("请", "")
                .trim()
                .to_string();
            let result = tool.execute(&expr);
            self.memory.push(result.clone());
            format!("[Agent] 调用工具 {} => {}", tool.name(), result)
        } else if user_input.contains("搜索") || user_input.contains("查找") {
            let tool = self.tools.get("search").unwrap();
            let query = user_input
                .replace("搜索", "")
                .replace("查找", "")
                .replace("请", "")
                .trim()
                .to_string();
            let result = tool.execute(&query);
            self.memory.push(result.clone());
            format!("[Agent] 调用工具 {} => {}", tool.name(), result)
        } else {
            "[Agent] 我理解了你的问题,但目前没有合适的工具来处理。".to_string()
        }
    }
}

fn main() {
    let mut agent = Agent::new();

    let queries = vec![
        "请计算 123 + 456",
        "搜索 Rust 语言特性",
    ];

    for q in &queries {
        println!("用户: {}", q);
        println!("{}", agent.process(q));
        println!();
    }
}

5.3 典型大模型介绍

模型开发者参数量特点
GPT-4oOpenAI未公开多模态、推理能力强
ClaudeAnthropic未公开安全对齐、长上下文、代码能力突出
DeepSeekDeepSeek671B (MoE)开源、MoE 架构、性价比极高
Qwen阿里云72B+中文能力强、开源生态完善
LLaMAMeta8B–405B开源标杆、社区生态丰富

六、Rust NLP 生态

Rust 在 NLP 领域的生态正在快速发展,以下是一些重要的库和框架。

6.1 主要库一览

库名说明适用场景
rust-bertHuggingFace Transformers 的 Rust 移植预训练模型推理(BERT、GPT-2 等)
candleHuggingFace 出品的轻量级 ML 框架模型训练与推理、Transformer 实现
burnRust 原生深度学习框架灵活的模型构建与训练
rust-tokenizersHuggingFace Tokenizers 的 Rust 原生实现高性能分词(BPE、WordPiece 等)
finalfusionRust 词嵌入库Word2Vec、FastText、GloVe 格式
rust-stemmers多语言词干提取文本预处理

6.2 实战:用 Rust + candle 加载预训练模型

// Cargo.toml 依赖:
// [dependencies]
// candle-core = "0.8"
// candle-nn = "0.8"
// candle-transformers = "0.8"

use candle_core::{Device, Tensor};
use candle_transformers::models::bert::{BertModel, Config};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 加载预训练 BERT 模型配置
    let config = Config::bert_base_uncased();

    // 创建随机初始化的模型(实际使用时加载权重文件)
    let device = Device::Cpu;
    let vb = candle_nn::VarBuilder::new_null(); // 实际使用 candle_nn::VarBuilder::from_pth()
    let _model = BertModel::new(vb, &config)?;

    // 模拟输入
    let input_ids = Tensor::new(&[[101u32, 2023, 2003, 1037, 3376, 102]], &device)?;
    let attention_mask = Tensor::new(&[[1u32; 7]], &device)?;

    println!("输入 token IDs: {:?}", input_ids.shape());
    println!("注意力掩码: {:?}", attention_mask.shape());
    println!("BERT 模型已准备就绪");

    Ok(())
}

6.3 实战:用 Rust 实现完整的文本分类管道

use std::collections::HashMap;

/// 文本分类器(基于 TF-IDF + 余弦相似度)
struct TextClassifier {
    tfidf: TfIdfCalculator,
    /// 每个类别的 TF-IDF 质心向量
    centroids: HashMap<String, Vec<f64>>,
}

impl TextClassifier {
    fn train(documents: &[(&str, &str)]) -> Self {
        let all_texts: Vec<&str> = documents.iter().map(|(t, _)| *t).collect();
        let tfidf = TfIdfCalculator::fit(&all_texts);

        // 按类别计算质心
        let mut class_docs: HashMap<String, Vec<Vec<f64>>> = HashMap::new();
        for (text, label) in documents {
            let vec = tfidf.transform(text);
            class_docs.entry(label.to_string()).or_default().push(vec);
        }

        let mut centroids = HashMap::new();
        for (label, vecs) in class_docs {
            let dim = vecs[0].len();
            let n = vecs.len() as f64;
            let centroid: Vec<f64> = (0..dim)
                .map(|d| vecs.iter().map(|v| v[d]).sum::<f64>() / n)
                .collect();
            centroids.insert(label, centroid);
        }

        TextClassifier { tfidf, centroids }
    }

    fn predict(&self, text: &str) -> String {
        let vec = self.tfidf.transform(text);
        let mut best_label = String::new();
        let mut best_sim = f64::NEG_INFINITY;

        for (label, centroid) in &self.centroids {
            let sim = cosine_similarity(&vec, centroid);
            if sim > best_sim {
                best_sim = sim;
                best_label = label.clone();
            }
        }
        best_label
    }
}

fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
    let dot: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
    let norm_a: f64 = a.iter().map(|x| x * x).sum::<f64>().sqrt();
    let norm_b: f64 = b.iter().map(|x| x * x).sum::<f64>().sqrt();
    if norm_a == 0.0 || norm_b == 0.0 { return 0.0; }
    dot / (norm_a * norm_b)
}

fn main() {
    let training_data: Vec<(&str, &str)> = vec![
        ("rust is a great programming language", "tech"),
        ("python is popular for data science", "tech"),
        ("machine learning transforms industries", "tech"),
        ("the stock market rose today", "finance"),
        ("interest rates are increasing", "finance"),
        ("investment portfolio diversification", "finance"),
    ];

    let classifier = TextClassifier::train(&training_data);

    let test_cases = vec![
        "I love programming in rust",
        "The economy is growing fast",
    ];

    for text in &test_cases {
        let label = classifier.predict(text);
        println!("'{}' => 分类: {}", text, label);
    }
}

七、总结

7.1 NLP 核心概念速查表

概念说明关键公式/方法
分词将文本切分为 token字符级、词级别、BPE
词袋模型忽略词序,统计词频$\text{BoW}(d) = [c_1, c_2, \ldots, c_m]$
TF-IDF词频 x 逆文档频率$\text{TF-IDF} = \text{TF} \times \log\frac{N}{df+1}$
Word2Vec词嵌入,语义向量化Skip-gram / CBOW
N-gram统计语言模型$P(w_t | w_{t-1})$
Transformer自注意力架构$\text{softmax}(\frac{QK^T}{\sqrt{d_k}})V$
BPE子词分词算法迭代合并最高频字符对
LLM大规模预训练语言模型GPT、BERT、Claude、DeepSeek
AI AgentLLM + 工具 + 规划Agent = LLM + Memory + Tools + Planning

7.2 练习建议

难度练习内容涉及知识点
入门实现一个英文文本清洗管道(去停用词、转小写、去标点)文本预处理
入门用 Rust 实现 BoW 模型,计算两篇文档的余弦相似度词袋模型、向量运算
进阶实现 TF-IDF 计算器,完成一个简单的新闻分类器TF-IDF、文本分类
进阶用 Rust 从零实现 Bigram 语言模型,支持句子概率计算和下一个词预测N-gram、概率计算
进阶实现简化版 BPE 分词器,对比不同合并次数对词汇表大小的影响BPE、Tokenization
挑战用 Rust 从零实现多头自注意力机制,支持可变序列长度Transformer、矩阵运算
挑战基于 candle 框架加载预训练 BERT 模型,完成情感分析推理任务rust-bert/candle、预训练模型
挑战构建一个简易 AI Agent,支持多轮对话、工具调用和记忆管理Agent 架构、工具系统

第三十八 计算机视觉(CV, Computer Vision)

第三十八 计算机视觉
├── 什么是计算机视觉(CV)
├── 图像基础
│   ├── 像素、通道、图像表示
│   └── 图像预处理
├── 卷积运算
│   ├── 卷积的定义与数学表达
│   └── 卷积与图像处理
├── 卷积神经网络(CNN)
│   ├── 卷积层、池化层、全连接层
│   └── 经典模型(LeNet、VGG、ResNet)
├── 目标检测与图像分割
│   ├── 目标检测(YOLO、R-CNN)
│   └── 图像分割(语义分割、实例分割)
├── Rust CV生态
│   ├── image crate 图像处理
│   ├── candle 深度学习推理
│   └── 其他 CV 相关 crate
└── 实战:图像分类(CIFAR-10)
    └── 用Rust + candle实现

计算机视觉(Computer Vision, CV)是人工智能领域中最具感知力的分支之一。人类的视觉系统可以毫不费力地识别物体、理解场景、判断距离,而计算机视觉的目标正是让机器“看懂“世界——从像素矩阵中提取有意义的语义信息。

$$\text{图像(像素矩阵)} \xrightarrow{\text{计算机视觉}} \text{语义理解(分类、检测、分割)}$$


一、什么是计算机视觉

1.1 定义

计算机视觉是研究如何使机器从数字图像或多维数据中获取高层理解的学科。它涵盖了从低层图像处理(滤波、增强)到高层语义理解(识别、推理)的完整链路。

1.2 CV 的核心任务层次

层次任务示例
低层图像预处理、增强灰度化、去噪、边缘检测
中层特征提取、描述SIFT、HOG、卷积特征
高层语义理解分类、检测、分割、生成

1.3 发展简史

  • 1960s:Block World,积木世界识别
  • 1980s:David Marr 视觉计算理论,边缘检测
  • 1990s:SIFT、HOG 等手工特征
  • 2012:AlexNet 在 ImageNet 竞赛中夺冠,深度学习时代开启
  • 2015+:ResNet、YOLO、U-Net 等经典模型涌现
  • 2020s:Vision Transformer (ViT)、多模态大模型

二、图像基础

2.1 像素与图像表示

一幅数字图像可以表示为一个二维矩阵。对于灰度图像,每个像素是一个标量值;对于彩色图像,每个像素是一个向量。

灰度图像:$H \times W$ 矩阵,像素值 $I(x, y) \in [0, 255]$

$$I_{\text{gray}} = \begin{pmatrix} 128 & 64 & 32 \ 200 & 150 & 100 \ 255 & 0 & 128 \end{pmatrix}$$

彩色图像(RGB):$H \times W \times 3$ 张量,三个通道分别为红(R)、绿(G)、蓝(B)

$$I_{\text{rgb}}(x, y) = \big[ R(x,y),\ G(x,y),\ B(x,y) \big]$$

其中每个通道的值域为 $[0, 255]$,在深度学习中通常归一化到 $[0, 1]$。

2.2 RGB 通道

RGB 三原色通过加色混合可以产生人眼能感知的大部分颜色:

通道颜色纯色值 (R, G, B)
R红色(255, 0, 0)
G绿色(0, 255, 0)
B蓝色(0, 0, 255)
白色(255, 255, 255)
黑色(0, 0, 0)

2.3 用 Rust 表示图像

/// 像素结构体
#[derive(Debug, Clone, Copy)]
struct Pixel {
    r: u8,
    g: u8,
    b: u8,
}

/// 简易图像结构
struct Image {
    width: usize,
    height: usize,
    pixels: Vec<Pixel>,
}

impl Image {
    fn new(width: usize, height: usize) -> Self {
        Image {
            width,
            height,
            pixels: vec![Pixel { r: 0, g: 0, b: 0 }; width * height],
        }
    }

    fn get_pixel(&self, x: usize, y: usize) -> &Pixel {
        &self.pixels[y * self.width + x]
    }

    fn set_pixel(&mut self, x: usize, y: usize, pixel: Pixel) {
        self.pixels[y * self.width + x] = pixel;
    }
}

fn main() {
    let mut img = Image::new(3, 2);
    // 设置一个红色像素
    img.set_pixel(0, 0, Pixel { r: 255, g: 0, b: 0 });
    // 设置一个绿色像素
    img.set_pixel(1, 0, Pixel { r: 0, g: 255, b: 0 });

    println!("图像尺寸: {}x{}", img.width, img.height);
    println!("像素 (0,0): {:?}", img.get_pixel(0, 0));
    println!("像素 (1,0): {:?}", img.get_pixel(1, 0));
}

三、图像预处理

图像预处理是 CV 流水线的第一步,目的是将原始图像转换为适合后续处理的标准化形式。

3.1 缩放(Resize)

缩放是最基本的预处理操作。常见的插值算法包括最近邻插值、双线性插值和双三次插值。

$$I’(x’, y’) = \sum_{i} \sum_{j} I(x_i, y_j) \cdot w(x_i - x’) \cdot w(y_j - y’)$$

其中 $w$ 为插值核函数。

use image::{imageops, DynamicImage, GenericImageView, ImageFormat};

fn main() {
    // 加载图像
    let img = image::open("input.jpg").expect("无法打开图像");
    println!("原始尺寸: {}x{}", img.width(), img.height());

    // 使用不同滤波器缩放
    let methods = [
        ("nearest",  imageops::FilterType::Nearest),
        ("triangle", imageops::FilterType::Triangle),
        ("catmull",  imageops::FilterType::CatmullRom),
        ("gaussian", imageops::FilterType::Gaussian),
        ("lanczos",  imageops::FilterType::Lanczos3),
    ];

    for (name, filter) in &methods {
        let resized = img.resize(224, 224, *filter);
        resized.save(format!("resized_{}.png", name)).unwrap();
        println!("已保存 resized_{}.png (224x224)", name);
    }
}

3.2 灰度化

将彩色图像转换为灰度图像,常用加权平均法(ITU-R BT.601 标准):

$$I_{\text{gray}} = 0.299 \cdot R + 0.587 \cdot G + 0.114 \cdot B$$

人眼对绿色最敏感,对蓝色最不敏感,因此权重不同。

use image::{DynamicImage, GenericImageView, Rgb, Luma};

fn to_grayscale(img: &DynamicImage) -> image::GrayImage {
    let rgb = img.to_rgb8();
    let (width, height) = rgb.dimensions();
    let mut gray = image::GrayImage::new(width, height);

    for y in 0..height {
        for x in 0..width {
            let Rgb([r, g, b]) = rgb.get_pixel(x, y);
            // ITU-R BT.601 加权灰度
            let gray_val = 0.299 * *r as f32
                         + 0.587 * *g as f32
                         + 0.114 * *b as f32;
            gray.put_pixel(x, y, Luma([gray_val as u8]));
        }
    }
    gray
}

fn main() {
    let img = image::open("input.jpg").unwrap();
    let gray = to_grayscale(&img);
    gray.save("grayscale.png").unwrap();
    println!("灰度图像已保存");

    // image crate 也内置了灰度转换
    let gray2 = img.to_luma8();
    gray2.save("grayscale_builtin.png").unwrap();
}

3.3 滤波(卷积滤波)

滤波是图像处理的核心操作,通过卷积核(kernel)与图像卷积实现各种效果。

均值滤波(模糊)

$$K_{\text{mean}} = \frac{1}{9} \begin{pmatrix} 1 & 1 & 1 \ 1 & 1 & 1 \ 1 & 1 & 1 \end{pmatrix}$$

高斯滤波

$$K_{\text{gauss}} = \frac{1}{16} \begin{pmatrix} 1 & 2 & 1 \ 2 & 4 & 2 \ 1 & 2 & 1 \end{pmatrix}$$

Sobel 边缘检测(水平方向):

$$K_{\text{sobel_x}} = \begin{pmatrix} -1 & 0 & 1 \ -2 & 0 & 2 \ -1 & 0 & 1 \end{pmatrix}$$

use image::{GrayImage, GenericImageView, Luma};

/// 手动实现 3x3 卷积
fn convolve3x3(img: &GrayImage, kernel: &[f32; 9]) -> GrayImage {
    let (width, height) = img.dimensions();
    let mut output = GrayImage::new(width, height);

    for y in 1..height - 1 {
        for x in 1..width - 1 {
            let mut sum = 0.0f32;
            let mut ki = 0;
            for ky in -1..=1 {
                for kx in -1..=1 {
                    let px = (x as i32 + kx) as u32;
                    let py = (y as i32 + ky) as u32;
                    let Luma([val]) = img.get_pixel(px, py);
                    sum += val as f32 * kernel[ki];
                    ki += 1;
                }
            }
            output.put_pixel(x, y, Luma([sum.clamp(0.0, 255.0) as u8]));
        }
    }
    output
}

fn main() {
    let img = image::open("input.jpg").unwrap().to_luma8();

    // 均值滤波(模糊)
    let mean_kernel: [f32; 9] = [1.0/9.0; 9];
    let blurred = convolve3x3(&img, &mean_kernel);
    blurred.save("blurred.png").unwrap();

    // 高斯滤波
    let gauss_kernel: [f32; 9] =
        [1.0/16.0, 2.0/16.0, 1.0/16.0,
         2.0/16.0, 4.0/16.0, 2.0/16.0,
         1.0/16.0, 2.0/16.0, 1.0/16.0];
    let gaussian = convolve3x3(&img, &gauss_kernel);
    gaussian.save("gaussian.png").unwrap();

    // Sobel 边缘检测
    let sobel_x: [f32; 9] = [-1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0];
    let edges = convolve3x3(&img, &sobel_x);
    edges.save("edges.png").unwrap();

    println!("滤波结果已保存");
}

四、卷积运算

4.1 卷积的数学定义

在信号处理中,两个函数 $f$ 和 $g$ 的卷积定义为:

$$(f * g)(t) = \int_{-\infty}^{+\infty} f(\tau) \cdot g(t - \tau) , d\tau$$

在离散情况下:

$$(f * g)[n] = \sum_{m=-\infty}^{+\infty} f[m] \cdot g[n - m]$$

4.2 二维卷积(图像卷积)

对于图像 $I$ 和卷积核 $K$,二维卷积运算为:

$$(I * K)(i, j) = \sum_{m} \sum_{n} I(i + m, j + n) \cdot K(m, n)$$

注意: 在深度学习中,实际使用的是“互相关“(cross-correlation)而非严格意义上的卷积,区别在于核是否翻转。实践中两者效果等价,因为核的参数是学习得到的。

4.3 步长与填充

  • 步长(Stride):卷积核每次移动的像素数,记为 $s$
  • 填充(Padding):在图像边缘补零,记为 $p$
  • 输出尺寸公式

$$O = \left\lfloor \frac{W - K + 2P}{S} \right\rfloor + 1$$

其中 $W$ 为输入尺寸,$K$ 为核大小,$P$ 为填充大小,$S$ 为步长。

/// 二维卷积实现(互相关)
fn conv2d(
    input: &[Vec<f32>],
    kernel: &[Vec<f32>],
    stride: usize,
    padding: usize,
) -> Vec<Vec<f32>> {
    let h_in = input.len();
    let w_in = input[0].len();
    let k_h = kernel.len();
    let k_w = kernel[0].len();

    // 填充输入
    let h_pad = h_in + 2 * padding;
    let w_pad = w_in + 2 * padding;
    let mut padded = vec![vec![0.0f32; w_pad]; h_pad];
    for i in 0..h_in {
        for j in 0..w_in {
            padded[i + padding][j + padding] = input[i][j];
        }
    }

    // 计算输出尺寸
    let h_out = (h_pad - k_h) / stride + 1;
    let w_out = (w_pad - k_w) / stride + 1;
    let mut output = vec![vec![0.0f32; w_out]; h_out];

    for i in 0..h_out {
        for j in 0..w_out {
            let mut sum = 0.0f32;
            for ki in 0..k_h {
                for kj in 0..k_w {
                    let pi = i * stride + ki;
                    let pj = j * stride + kj;
                    sum += padded[pi][pj] * kernel[ki][kj];
                }
            }
            output[i][j] = sum;
        }
    }
    output
}

fn main() {
    // 5x5 输入
    let input: Vec<Vec<f32>> = (0..5)
        .map(|i| (0..5).map(|j| (i * 5 + j) as f32).collect())
        .collect();

    // 3x3 卷积核(边缘检测)
    let kernel: Vec<Vec<f32>> = vec![
        vec![-1.0, -1.0, -1.0],
        vec![-1.0,  8.0, -1.0],
        vec![-1.0, -1.0, -1.0],
    ];

    let output = conv2d(&input, &kernel, 1, 1);
    println!("卷积输出 (3x3):");
    for row in &output {
        println!("  {:?}", row.iter().map(|v| format!("{:6.1}", v)).collect::<Vec<_>>());
    }
}

五、卷积神经网络(CNN)

5.1 为什么需要 CNN

传统图像处理依赖手工设计的特征(如 SIFT、HOG),而 CNN 能够自动从数据中学习特征层次结构:

$$\text{像素} \rightarrow \text{边缘} \rightarrow \text{纹理} \rightarrow \text{部件} \rightarrow \text{物体}$$

CNN 的三大核心特性:

  • 局部连接:每个神经元只与局部区域连接
  • 权值共享:同一卷积核在整幅图像上滑动共享参数
  • 平移不变性:物体出现在图像不同位置,仍能被识别

5.2 卷积层

卷积层是 CNN 的核心,通过多个卷积核提取不同特征:

$$Y^{(l)} = \sigma\left( W^{(l)} * X^{(l-1)} + b^{(l)} \right)$$

其中 $\sigma$ 为激活函数(通常为 ReLU),$W^{(l)}$ 为第 $l$ 层的卷积核权重,$b^{(l)}$ 为偏置。

ReLU 激活函数

$$\text{ReLU}(x) = \max(0, x)$$

ReLU 的优势:计算简单、缓解梯度消失问题。

5.3 池化层

池化层用于降低特征图的空间维度,减少计算量和参数量。

最大池化(Max Pooling)

$$Y(i, j) = \max_{(m,n) \in \mathcal{R}_{ij}} X(m, n)$$

平均池化(Average Pooling)

$$Y(i, j) = \frac{1}{|\mathcal{R}{ij}|} \sum{(m,n) \in \mathcal{R}_{ij}} X(m, n)$$

其中 $\mathcal{R}_{ij}$ 为池化窗口覆盖的区域。

/// 最大池化 2x2, stride=2
fn max_pool2x2(input: &[Vec<f32>]) -> Vec<Vec<f32>> {
    let h = input.len() / 2;
    let w = input[0].len() / 2;
    let mut output = vec![vec![0.0f32; w]; h];

    for i in 0..h {
        for j in 0..w {
            let r = i * 2;
            let c = j * 2;
            output[i][j] = input[r][c]
                .max(input[r][c + 1])
                .max(input[r + 1][c])
                .max(input[r + 1][c + 1]);
        }
    }
    output
}

/// 平均池化 2x2, stride=2
fn avg_pool2x2(input: &[Vec<f32>]) -> Vec<Vec<f32>> {
    let h = input.len() / 2;
    let w = input[0].len() / 2;
    let mut output = vec![vec![0.0f32; w]; h];

    for i in 0..h {
        for j in 0..w {
            let r = i * 2;
            let c = j * 2;
            let sum = input[r][c] + input[r][c + 1]
                    + input[r + 1][c] + input[r + 1][c + 1];
            output[i][j] = sum / 4.0;
        }
    }
    output
}

fn main() {
    let input: Vec<Vec<f32>> = vec![
        vec![1.0, 2.0, 3.0, 4.0],
        vec![5.0, 6.0, 7.0, 8.0],
        vec![9.0, 10.0, 11.0, 12.0],
        vec![13.0, 14.0, 15.0, 16.0],
    ];

    let max_pooled = max_pool2x2(&input);
    println!("最大池化: {:?}", max_pooled);
    // [[6.0, 8.0], [14.0, 16.0]]

    let avg_pooled = avg_pool2x2(&input);
    println!("平均池化: {:?}", avg_pooled);
    // [[3.5, 5.5], [11.5, 13.5]]
}

5.4 全连接层

全连接层将卷积层提取的高层特征展平后映射到输出空间:

$$\mathbf{y} = \sigma(W \cdot \mathbf{x} + \mathbf{b})$$

其中 $\mathbf{x}$ 是展平后的特征向量,$W$ 是权重矩阵,$\mathbf{b}$ 是偏置向量。

对于 $K$ 分类问题,输出层使用 Softmax:

$$\text{Softmax}(z_i) = \frac{e^{z_i}}{\sum_{j=1}^{K} e^{z_j}}$$

5.5 经典 CNN 模型

LeNet-5(1998)

LeNet-5 是 Yann LeCun 提出的最早的成功 CNN 之一,用于手写数字识别。

类型输出尺寸参数量
1卷积层 (6@5x5)28x28x6156
2池化层 (2x2)14x14x60
3卷积层 (16@5x5)10x10x162,416
4池化层 (2x2)5x5x160
5全连接层 (120)12048,120
6全连接层 (84)8410,164
7输出层 (10)10850

VGG-16(2014)

VGG 的核心思想是使用小卷积核(3x3)和深层堆叠:

$$\text{两个 } 3 \times 3 \text{ 卷积的感受野} = \text{一个 } 5 \times 5 \text{ 卷积的感受野}$$

但参数更少:$2 \times (3 \times 3 \times C^2) = 18C^2 < 25C^2 = 5 \times 5 \times C^2$

VGG-16 共 16 个权重层,约 1.38 亿参数。

ResNet(2015)

ResNet 引入了残差连接(Skip Connection),解决了深层网络的退化问题:

$$\mathcal{F}(x) = \mathcal{H}(x) - x \quad \Rightarrow \quad \mathcal{H}(x) = \mathcal{F}(x) + x$$

网络学习的是残差 $\mathcal{F}(x) = \mathcal{H}(x) - x$,而非直接学习映射 $\mathcal{H}(x)$。

/// 简化的残差块概念演示
fn residual_block(input: &[f32], weights: &[f32], bias: &[f32]) -> Vec<f32> {
    // F(x) = W * x + b (简化为逐元素运算)
    let f_x: Vec<f32> = input
        .iter()
        .zip(weights.iter())
        .zip(bias.iter())
        .map(|((&x, &w), &b)| (x * w + b).max(0.0)) // ReLU
        .collect();

    // H(x) = F(x) + x (残差连接)
    let h_x: Vec<f32> = input.iter().zip(f_x.iter()).map(|(&x, &f)| x + f).collect();

    h_x
}

fn main() {
    let input = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    let weights = vec![0.5, 0.8, 0.3, 0.6, 0.4];
    let bias = vec![0.1, -0.1, 0.2, -0.2, 0.1];

    let output = residual_block(&input, &weights, &bias);
    println!("残差块输出: {:?}", output);
    // [1.6, 2.5, 2.9, 4.4, 5.3]
}

六、目标检测

6.1 目标检测概述

目标检测不仅要识别图像中有什么物体,还要定位它们的位置。输出为边界框(Bounding Box)和类别标签。

$$\text{目标检测输出} = {(x_{\min}, y_{\min}, x_{\max}, y_{\max}, \text{class}, \text{confidence})}$$

6.2 两阶段检测器:R-CNN 系列

R-CNN(Region-based CNN)系列采用“先选区域,再分类“的策略:

  1. R-CNN:Selective Search 生成候选区域 -> CNN 提取特征 -> SVM 分类
  2. Fast R-CNN:引入 RoI Pooling,共享卷积特征
  3. Faster R-CNN:用 RPN(Region Proposal Network)替代 Selective Search

6.3 单阶段检测器:YOLO

YOLO(You Only Look Once)将检测视为回归问题,一次前向传播完成检测:

$$\text{YOLO 输出} = S \times S \times (B \times 5 + C)$$

其中 $S \times S$ 为网格数,$B$ 为每个网格的边界框数,$C$ 为类别数。

YOLO 的优势:速度快,适合实时检测。

模型速度 (FPS)mAP特点
R-CNN~0.0758.4精度高,速度慢
Faster R-CNN~773.2精度与速度平衡
YOLOv3~4557.9实时检测
YOLOv8~100+53.9最新架构,多任务

七、图像分割

7.1 语义分割

语义分割为图像中的每个像素分配一个类别标签,不区分同类的不同实例。

$$f: H \times W \rightarrow {1, 2, \ldots, K}$$

经典模型:FCN(全卷积网络)、U-Net。

U-Net 采用编码器-解码器结构,通过跳跃连接(skip connection)保留空间细节:

编码器(下采样)          解码器(上采样)
64  -> 128  -> 256  -> 512  -> 1024
 |      |      |      |        |
 +------+------+------+--------+ (跳跃连接)

7.2 实例分割

实例分割不仅为每个像素分类,还要区分同类的不同实例。

$$f: H \times W \rightarrow {(c_i, m_i)}_{i=1}^{N}$$

其中 $c_i$ 为类别,$m_i$ 为实例掩码。

经典模型:Mask R-CNN。

7.3 分割与检测对比

任务输出粒度是否区分实例典型模型
图像分类整幅图像ResNet
目标检测边界框YOLO
语义分割像素级U-Net
实例分割像素级掩码Mask R-CNN

八、Rust CV 生态

8.1 image crate

image 是 Rust 生态中最核心的图像处理库,支持多种格式的编解码和基本图像操作。

# Cargo.toml
[dependencies]
image = "0.25"
use image::{DynamicImage, GenericImageView, ImageFormat, imageops};

fn main() {
    // 1. 读取图像(支持 JPEG, PNG, WebP, GIF, BMP, TIFF 等)
    let img = image::open("photo.jpg").expect("打开图像失败");
    println!("尺寸: {}x{}", img.width(), img.height());

    // 2. 裁剪
    let cropped = img.crop_imm(100, 100, 300, 300);
    cropped.save("cropped.png").unwrap();

    // 3. 旋转
    let rotated = img.rotate90();
    rotated.save("rotated.png").unwrap();

    // 4. 翻转
    let flipped = imageops::flip_horizontal(&img);
    flipped.save("flipped_h.png").unwrap();

    // 5. 调整亮度/对比度
    let adjusted = img.adjust_contrast(1.5);
    adjusted.save("contrast.png").unwrap();

    // 6. 格式转换
    let rgb = img.to_rgb8();
    let gray = img.to_luma8();

    // 7. 保存为不同格式
    img.save("output.webp").unwrap(); // 自动推断格式
    let mut out = std::fs::File::create("output.bmp").unwrap();
    img.write_to(&mut out, ImageFormat::Bmp).unwrap();

    println!("图像处理完成");
}

8.2 candle 深度学习推理

candle 是 HuggingFace 开发的 Rust 深度学习框架,支持模型推理和训练。

# Cargo.toml
[dependencies]
candle-core = "0.8"
candle-nn = "0.8"
use candle_core::{Tensor, Device};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let device = Device::Cpu;

    // 创建一个 4 维张量,模拟一批图像 (batch, channels, height, width)
    // 形状: [2, 3, 224, 224],模拟 2 张 RGB 图像
    let images = Tensor::randn(0f32, 1.0, (2, 3, 224, 224), &device)?;

    println!("输入张量形状: {:?}", images.shape());

    // 模拟卷积层权重: 16 个 3x3 卷积核,输入 3 通道
    let weight = Tensor::randn(0f32, 0.02, (16, 3, 3, 3), &device)?;
    let bias = Tensor::zeros(16, &device)?;

    // 执行卷积运算 (padding=1)
    let conv = images.conv2d(&weight, &bias, 1, 1)?;
    println!("卷积后形状: {:?}", conv.shape()); // [2, 16, 224, 224]

    // ReLU 激活
    let activated = conv.relu()?;
    println!("ReLU 后形状: {:?}", activated.shape());

    // 最大池化 2x2
    let pooled = activated.max_pool2d(2)?;
    println!("池化后形状: {:?}", pooled.shape()); // [2, 16, 112, 112]

    Ok(())
}

8.3 其他 CV 相关 crate

crate功能说明
image图像编解码与处理Rust CV 基础库
candle深度学习框架HuggingFace 出品,支持推理与训练
burn深度学习框架纯 Rust,支持多后端
nalgebra线性代数矩阵运算、变换
imageproc图像处理算法形态学、阈值分割等
opencv-rustOpenCV 绑定完整的 OpenCV 功能
tractONNX/TFLite 推理轻量级推理引擎
tchPyTorch 绑定C++ torch 的 Rust 封装
qr2termQR 码终端显示将 QR 码渲染到终端

8.4 用 candle 加载预训练模型进行图像分类

use candle_core::{Device, Tensor};
use candle_nn::{VarMap, Module, Conv2d, Conv2dConfig, Linear};
use candle_nn::init;

/// 构建简易 CNN 模型(用于演示,非预训练)
fn build_simple_cnn(vs: &candle_nn::VarBuilder) -> Result<Box<dyn Module>, Box<dyn std::error::Error>> {
    // Conv1: 3 -> 32, 3x3
    let conv1 = candle_nn::conv2d(3, 32, 3, Conv2dConfig::default(), vs.pp("conv1"))?;
    // Conv2: 32 -> 64, 3x3
    let conv2 = candle_nn::conv2d(32, 64, 3, Conv2dConfig::default(), vs.pp("conv2"))?;
    // FC: 64*6*6 -> 128
    let fc1 = candle_nn::linear(64 * 6 * 6, 128, vs.pp("fc1"))?;
    // FC: 128 -> 10 (CIFAR-10 的 10 个类别)
    let fc2 = candle_nn::linear(128, 10, vs.pp("fc2"))?;

    Ok(Box::new(move |xs: &Tensor| -> Result<Tensor, candle_core::Error> {
        let xs = xs.apply(&conv1)?.relu()?.max_pool2d(2)?;
        let xs = xs.apply(&conv2)?.relu()?.max_pool2d(2)?;
        let xs = xs.flatten_from(1)?;
        let xs = xs.apply(&fc1)?.relu()?;
        xs.apply(&fc2)
    }))
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let device = Device::Cpu;
    let mut varmap = VarMap::new();
    let vs = candle_nn::VarBuilder::from_varmap(&varmap, candle_core::DType::F32, &device);

    let model = build_simple_cnn(&vs)?;

    // 模拟输入: batch=1, channels=3, height=32, width=32 (CIFAR-10 尺寸)
    let input = Tensor::randn(0f32, 1.0, (1, 3, 32, 32), &device)?;
    let output = model.forward(&input)?;

    println!("模型输出形状: {:?}", output.shape()); // [1, 10]
    println!("原始输出: {}", output.to_vec2::<f32>()?);

    // Softmax 获取概率分布
    let probs = candle_nn::ops::softmax(&output, 1)?;
    println!("概率分布: {:?}", probs.to_vec2::<f32>()?);

    // 获取预测类别
    let pred = probs.argmax(1)?;
    println!("预测类别: {:?}", pred.to_vec1::<u32>()?);

    Ok(())
}

九、实战:图像分类(CIFAR-10)

CIFAR-10 是经典的图像分类数据集,包含 10 个类别、60000 张 32x32 彩色图像。

9.1 数据集概览

类别示例内容
airplane飞机
automobile汽车
bird
cat
deer鹿
dog
frog
horse
ship
truck卡车

9.2 完整训练流程

use candle_core::{Device, Tensor, DType};
use candle_nn::{VarMap, Module, Conv2d, Conv2dConfig, Linear, AdamW, Optimizer, loss};
use candle_datasets::cifar10;

/// 简易 CNN 用于 CIFAR-10 分类
struct CifarNet {
    conv1: Conv2d,
    conv2: Conv2d,
    conv3: Conv2d,
    fc1: Linear,
    fc2: Linear,
}

impl CifarNet {
    fn new(vs: candle_nn::VarBuilder) -> Result<Self, candle_core::Error> {
        let conv1 = candle_nn::conv2d(3, 32, 3, Conv2dConfig::with_padding(1), vs.pp("c1"))?;
        let conv2 = candle_nn::conv2d(32, 64, 3, Conv2dConfig::with_padding(1), vs.pp("c2"))?;
        let conv3 = candle_nn::conv2d(64, 64, 3, Conv2dConfig::with_padding(1), vs.pp("c3"))?;
        let fc1 = candle_nn::linear(64 * 4 * 4, 64, vs.pp("fc1"))?;
        let fc2 = candle_nn::linear(64, 10, vs.pp("fc2"))?;
        Ok(Self { conv1, conv2, conv3, fc1, fc2 })
    }

    fn forward(&self, xs: &Tensor) -> Result<Tensor, candle_core::Error> {
        let xs = xs.apply(&self.conv1)?.relu()?.max_pool2d(2)?;  // [B, 32, 16, 16]
        let xs = xs.apply(&self.conv2)?.relu()?.max_pool2d(2)?;  // [B, 64, 8, 8]
        let xs = xs.apply(&self.conv3)?.relu()?.max_pool2d(2)?;  // [B, 64, 4, 4]
        let xs = xs.flatten_from(1)?;                            // [B, 1024]
        let xs = xs.apply(&self.fc1)?.relu()?;
        xs.apply(&self.fc2)
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let device = Device::Cpu;

    // 加载 CIFAR-10 数据集
    let (train_images, train_labels) = cifar10::load_train()?;
    let (test_images, test_labels) = cifar10::load_test()?;

    println!("训练集: {} 张图像", train_images.len());
    println!("测试集: {} 张图像", test_images.len());

    // 构建模型
    let mut varmap = VarMap::new();
    let vs = candle_nn::VarBuilder::from_varmap(&varmap, DType::F32, &device);
    let model = CifarNet::new(vs)?;

    // 优化器
    let mut optimizer = candle_nn::AdamW::new(&varmap, 0.001f64)?;

    // 训练循环
    let epochs = 10;
    let batch_size = 64;

    for epoch in 1..=epochs {
        let mut total_loss = 0.0f32;
        let mut correct = 0usize;
        let mut total = 0usize;

        // 遍历 mini-batch
        for i in (0..train_images.len()).step_by(batch_size) {
            let end = (i + batch_size).min(train_images.len());
            let batch_images: Vec<Tensor> = train_images[i..end]
                .iter()
                .map(|img| Tensor::from_data(img.clone(), &device).unwrap())
                .collect();
            let batch_labels = Tensor::from_vec(
                train_labels[i..end].to_vec(),
                (end - i,) as usize,
                &device,
            )?;

            let images = Tensor::stack(&batch_images, 0)?;
            let logits = model.forward(&images)?;
            let loss = loss::cross_entropy(&logits, &batch_labels)?;

            optimizer.backward_step(&loss)?;
            total_loss += loss.to_scalar::<f32>()?;

            // 计算准确率
            let preds = logits.argmax(1)?;
            let labels = batch_labels;
            correct += preds.eq(&labels)?.to_scalar::<u32>()? as usize;
            total += end - i;
        }

        println!(
            "Epoch {}/{} - Loss: {:.4} - Acc: {:.2}%",
            epoch, epochs,
            total_loss / (train_images.len() / batch_size) as f32,
            100.0 * correct as f32 / total as f32
        );
    }

    Ok(())
}

十、总结

核心知识回顾

主题核心要点
图像基础像素、RGB 通道、$H \times W \times C$ 张量表示
图像预处理缩放(插值)、灰度化(加权平均)、滤波(卷积核)
卷积运算二维卷积、步长、填充、输出尺寸公式
CNN 核心卷积层(特征提取)、池化层(降维)、全连接层(分类)
经典模型LeNet-5(奠基)、VGG(深层堆叠)、ResNet(残差连接)
目标检测R-CNN(两阶段)、YOLO(单阶段实时检测)
图像分割语义分割(FCN/U-Net)、实例分割(Mask R-CNN)
Rust 生态image(图像处理)、candle(深度学习)、tract(推理)

CV 任务层次总览

图像分类 ──── "这是什么?"
    │
目标检测 ──── "在哪里?是什么?"(边界框 + 类别)
    │
语义分割 ──── "每个像素属于什么类别?"
    │
实例分割 ──── "每个像素属于哪个实例?"

练习建议

  1. 基础练习:使用 image crate 读取一张彩色图片,分别提取 R、G、B 三个通道并保存为灰度图。
  2. 卷积实现:手动实现 5x5 高斯卷积核,对灰度图像进行平滑处理,并与 imageproc 库的结果对比。
  3. CNN 构建:使用 candle 构建一个包含 3 个卷积层 + 2 个全连接层的 CNN,在 CIFAR-10 上训练并记录准确率。
  4. 模型对比:分别实现 LeNet 和 VGG 风格的网络,比较在相同数据集上的参数量和训练效果。
  5. 进阶挑战:使用 tract crate 加载一个 ONNX 格式的预训练 ResNet 模型,对本地图片进行推理分类。
  6. 边缘检测:实现 Sobel 算子(水平和垂直方向),计算梯度幅值,观察不同图像的边缘检测效果。
  7. 图像分割:基于阈值分割实现一个简单的二值化分割器,将前景与背景分离。

第三十九 强化学习(Reinforcement Learning)

第三十九 强化学习
├── 什么是强化学习(RL)
├── 智能体与环境
├── 马尔可夫决策过程(MDP)
│   ├── 状态、动作、奖励
│   ├── 转移概率与折扣因子
│   └── MDP 的形式化定义
├── 贝尔曼方程
│   ├── 状态价值函数
│   ├── 动作价值函数
│   └── 最优价值函数
├── 策略与价值函数
│   ├── 策略(确定性 / 随机性)
│   ├── 策略评估
│   └── 策略改进
├── 探索与利用
│   ├── ε-贪心策略
│   └── 其他探索方法
├── 强化学习算法
│   ├── Q-Learning
│   ├── SARSA
│   ├── 深度强化学习(DQN)
│   └── 近端策略优化(PPO)
├── 实战:用 Rust 实现 Q-Learning
│   └── FrozenLake 环境
├── Rust 强化学习生态
└── 总结与练习

强化学习(Reinforcement Learning,简称 RL)是人工智能中最接近“学习“本质的分支。与监督学习依赖标注数据、无监督学习挖掘数据结构不同,强化学习让智能体(Agent)通过与环境(Environment)的不断交互,在试错中学习最优行为策略——这与人类婴儿学走路、学说话的过程惊人地相似。

$$\text{强化学习} = \text{试错探索} + \text{延迟奖励} + \text{策略优化}$$


一、什么是强化学习

1.1 定义

强化学习是机器学习的一大范式,其核心思想是:智能体在环境中采取动作,环境返回奖励信号,智能体通过最大化累积奖励来学习最优策略。它既不依赖标注数据,也不需要显式的问题建模,而是通过“做中学“(Learning by Doing)来获取知识。

1.2 强化学习与其他机器学习范式的对比

范式输入反馈目标典型场景
监督学习特征 + 标签即时、正确答案预测标签图像分类、机器翻译
无监督学习特征无反馈发现数据结构聚类、降维
强化学习状态延迟奖励信号最大化累积奖励游戏AI、机器人控制

1.3 发展简史

  • 1989:Watkins 提出 Q-Learning,奠定值函数方法基础
  • 1992:Tesauro 用 TD-Gammon 程序挑战西洋双陆棋
  • 2013:DeepMind 发表 Playing Atari with Deep RL(DQN)
  • 2016:AlphaGo 击败李世石,强化学习走入大众视野
  • 2017:PPO 算法提出,成为 OpenAI 默认策略优化方法
  • 2020s:RLHF(基于人类反馈的强化学习)赋能大语言模型

二、智能体与环境

强化学习的基本框架由两个核心角色构成:智能体(Agent)环境(Environment)。二者通过循环交互完成学习过程。

2.1 交互循环

┌──────────┐   动作 a_t    ┌──────────┐
│          │ ────────────> │          │
│  智能体   │               │   环境   │
│ (Agent)  │ <──────────── │  (Env)   │
│          │  状态 s_{t+1}  │          │
│          │  奖励 r_{t+1}  │          │
└──────────┘               └──────────┘

每一步交互的流程:

  1. 智能体观察当前状态 $s_t$
  2. 智能体根据策略选择动作 $a_t$
  3. 环境接收动作,转移到下一状态 $s_{t+1}$
  4. 环境返回奖励 $r_{t+1}$
  5. 智能体根据反馈更新策略

2.2 用 Rust 描述交互循环

#![allow(unused)]
fn main() {
/// 智能体 trait:定义智能体的基本行为
trait Agent {
    /// 根据当前状态选择动作
    fn select_action(&mut self, state: usize) -> usize;
    /// 根据转移结果更新策略
    fn update(&mut self, state: usize, action: usize, reward: f64, next_state: usize);
}

/// 环境 trait:定义环境的基本接口
trait Environment {
    /// 重置环境到初始状态
    fn reset(&mut self) -> usize;
    /// 执行动作,返回 (下一状态, 奖励, 是否结束)
    fn step(&mut self, state: usize, action: usize) -> (usize, f64, bool);
}

/// 训练循环
fn train<A: Agent, E: Environment>(agent: &mut A, env: &mut E, episodes: usize) {
    for episode in 0..episodes {
        let mut state = env.reset();
        let mut total_reward = 0.0;
        let mut steps = 0;

        loop {
            let action = agent.select_action(state);
            let (next_state, reward, done) = env.step(state, action);
            agent.update(state, action, reward, next_state);
            total_reward += reward;
            steps += 1;
            state = next_state;

            if done {
                break;
            }
        }

        if (episode + 1) % 100 == 0 {
            println!("Episode {}: 总奖励 = {:.2}, 步数 = {}", 
                     episode + 1, total_reward, steps);
        }
    }
}
}

三、马尔可夫决策过程(MDP)

马尔可夫决策过程(Markov Decision Process, MDP)是强化学习的数学框架,它用精确的数学语言描述了智能体与环境交互的全部要素。

3.1 MDP 的五元组

一个 MDP 由一个五元组 $\langle S, A, P, R, \gamma \rangle$ 定义:

要素符号含义
状态空间$S$智能体可能处于的所有状态的集合
动作空间$A$智能体可以执行的所有动作的集合
转移概率$P(s’ \mid s, a)$在状态 $s$ 执行动作 $a$ 后转移到状态 $s’$ 的概率
奖励函数$R(s, a, s’)$执行动作后获得的即时奖励
折扣因子$\gamma \in [0, 1)$衡量未来奖励的衰减程度

3.2 马尔可夫性质

MDP 的核心假设是马尔可夫性质(Markov Property):当前状态包含了做出最优决策所需的全部信息,即未来只与现在有关,与过去无关。

$$P(s_{t+1} \mid s_t, a_t, s_{t-1}, a_{t-1}, \ldots) = P(s_{t+1} \mid s_t, a_t)$$

3.3 转移概率与奖励函数

状态转移概率满足归一化条件:

$$\sum_{s’ \in S} P(s’ \mid s, a) = 1, \quad \forall s \in S, \forall a \in A$$

奖励函数 $R(s, a, s’)$ 定义了智能体在状态 $s$ 执行动作 $a$ 后转移到 $s’$ 时获得的即时反馈。

3.4 折扣因子

折扣因子 $\gamma$ 决定了智能体对远期奖励的重视程度:

  • $\gamma \to 1$:智能体有远见,重视长期回报
  • $\gamma \to 0$:智能体目光短浅,只关注即时奖励

累积回报(Return)定义为从时刻 $t$ 开始的所有折扣奖励之和:

$$G_t = R_{t+1} + \gamma R_{t+2} + \gamma^2 R_{t+3} + \cdots = \sum_{k=0}^{\infty} \gamma^k R_{t+k+1}$$

3.5 用 Rust 定义 MDP

#![allow(unused)]
fn main() {
/// MDP 的五元组定义
struct Mdp {
    num_states: usize,       // |S|
    num_actions: usize,      // |A|
    transitions: Vec<Vec<Vec<f64>>>, // P[s][a][s'] 转移概率
    rewards: Vec<Vec<Vec<f64>>>,    // R[s][a][s'] 奖励函数
    gamma: f64,              // 折扣因子
}

impl Mdp {
    fn new(num_states: usize, num_actions: usize, gamma: f64) -> Self {
        // 初始化转移概率和奖励矩阵
        let transitions = vec![vec![vec![1.0 / num_states as f64; num_states]; num_actions]; num_states];
        let rewards = vec![vec![vec![0.0; num_states]; num_actions]; num_states];
        Mdp { num_states, num_actions, transitions, rewards, gamma }
    }

    /// 获取转移概率 P(s'|s,a)
    fn get_transition(&self, s: usize, a: usize, s_prime: usize) -> f64 {
        self.transitions[s][a][s_prime]
    }

    /// 获取奖励 R(s,a,s')
    fn get_reward(&self, s: usize, a: usize, s_prime: usize) -> f64 {
        self.rewards[s][a][s_prime]
    }
}
}

四、贝尔曼方程

贝尔曼方程(Bellman Equation)是强化学习的基石,它将价值函数表达为即时奖励与未来价值的递推关系。

4.1 状态价值函数

状态价值函数 $V^\pi(s)$ 表示在策略 $\pi$ 下,从状态 $s$ 出发的期望累积回报:

$$V^\pi(s) = \mathbb{E}\pi \left[ G_t \mid S_t = s \right] = \mathbb{E}\pi \left[ \sum_{k=0}^{\infty} \gamma^k R_{t+k+1} \mid S_t = s \right]$$

根据贝尔曼方程,它可以分解为即时奖励加上下一状态的价值:

$$V^\pi(s) = \sum_a \pi(a \mid s) \sum_{s’} P(s’ \mid s, a) \left[ R(s, a, s’) + \gamma V^\pi(s’) \right]$$

4.2 动作价值函数

动作价值函数 $Q^\pi(s, a)$ 表示在状态 $s$ 执行动作 $a$ 后,遵循策略 $\pi$ 的期望累积回报:

$$Q^\pi(s, a) = \mathbb{E}_\pi \left[ G_t \mid S_t = s, A_t = a \right]$$

贝尔曼方程形式:

$$Q^\pi(s, a) = \sum_{s’} P(s’ \mid s, a) \left[ R(s, a, s’) + \gamma \sum_{a’} \pi(a’ \mid s’) Q^\pi(s’, a’) \right]$$

4.3 最优价值函数

最优状态价值函数和最优动作价值函数:

$$V^*(s) = \max_\pi V^\pi(s)$$

$$Q^*(s, a) = \max_\pi Q^\pi(s, a)$$

最优贝尔曼方程:

$$V^(s) = \max_a \sum_{s’} P(s’ \mid s, a) \left[ R(s, a, s’) + \gamma V^(s’) \right]$$

$$Q^(s, a) = \sum_{s’} P(s’ \mid s, a) \left[ R(s, a, s’) + \gamma \max_{a’} Q^(s’, a’) \right]$$

4.4 用 Rust 实现价值迭代

#![allow(unused)]
fn main() {
/// 价值迭代算法求解最优价值函数
fn value_iteration(mdp: &Mdp, max_iterations: usize, theta: f64) -> Vec<f64> {
    let mut v = vec![0.0; mdp.num_states];

    for _ in 0..max_iterations {
        let mut delta = 0.0;

        for s in 0..mdp.num_states {
            let mut max_value = f64::NEG_INFINITY;

            for a in 0..mdp.num_actions {
                let mut action_value = 0.0;
                for s_prime in 0..mdp.num_states {
                    let p = mdp.get_transition(s, a, s_prime);
                    let r = mdp.get_reward(s, a, s_prime);
                    action_value += p * (r + mdp.gamma * v[s_prime]);
                }
                max_value = max_value.max(action_value);
            }

            delta = delta.max((max_value - v[s]).abs());
            v[s] = max_value;
        }

        if delta < theta {
            println!("价值迭代在 {} 次迭代后收敛", max_iterations);
            break;
        }
    }

    v
}

/// 从最优价值函数提取最优策略
fn extract_policy(mdp: &Mdp, v: &[f64]) -> Vec<usize> {
    let mut policy = vec![0; mdp.num_states];

    for s in 0..mdp.num_states {
        let mut best_action = 0;
        let mut best_value = f64::NEG_INFINITY;

        for a in 0..mdp.num_actions {
            let mut action_value = 0.0;
            for s_prime in 0..mdp.num_states {
                let p = mdp.get_transition(s, a, s_prime);
                let r = mdp.get_reward(s, a, s_prime);
                action_value += p * (r + mdp.gamma * v[s_prime]);
            }
            if action_value > best_value {
                best_value = action_value;
                best_action = a;
            }
        }
        policy[s] = best_action;
    }

    policy
}
}

五、策略与价值函数

5.1 策略的定义

策略 $\pi$ 是智能体的行为规则,定义了在每个状态下应该采取什么动作。

确定性策略:每个状态对应唯一动作

$$a = \pi(s)$$

随机性策略:每个状态下各动作的概率分布

$$\pi(a \mid s) = P(A_t = a \mid S_t = s)$$

#![allow(unused)]
fn main() {
/// 策略 trait
trait Policy {
    /// 在给定状态下选择动作
    fn select_action(&self, state: usize) -> usize;
}

/// 确定性策略:每个状态映射到一个动作
struct DeterministicPolicy {
    action_map: Vec<usize>,
}

impl DeterministicPolicy {
    fn new(action_map: Vec<usize>) -> Self {
        Self { action_map }
    }
}

impl Policy for DeterministicPolicy {
    fn select_action(&self, state: usize) -> usize {
        self.action_map[state]
    }
}

/// 随机性策略:每个状态下各动作的概率
struct StochasticPolicy {
    /// prob[s][a] = 在状态 s 选择动作 a 的概率
    prob: Vec<Vec<f64>>,
    num_actions: usize,
}

impl StochasticPolicy {
    fn new(num_states: usize, num_actions: usize) -> Self {
        // 均匀初始化
        let prob = vec![vec![1.0 / num_actions as f64; num_actions]; num_states];
        Self { prob, num_actions }
    }
}

impl Policy for StochasticPolicy {
    fn select_action(&self, state: usize) -> usize {
        // 按概率分布采样
        let r = rand::random::<f64>();
        let mut cumulative = 0.0;
        for a in 0..self.num_actions {
            cumulative += self.prob[state][a];
            if r < cumulative {
                return a;
            }
        }
        self.num_actions - 1
    }
}
}

5.2 策略评估

策略评估的目标是计算给定策略 $\pi$ 下的状态价值函数 $V^\pi$。采用迭代法:

$$V_{k+1}(s) = \sum_a \pi(a \mid s) \sum_{s’} P(s’ \mid s, a) \left[ R(s, a, s’) + \gamma V_k(s’) \right]$$

5.3 策略改进

在获得价值函数后,通过贪婪地选择每个状态的最优动作来改进策略:

$$\pi’(s) = \arg\max_a Q^\pi(s, a) = \arg\max_a \sum_{s’} P(s’ \mid s, a) \left[ R(s, a, s’) + \gamma V^\pi(s’) \right]$$

策略评估与策略改进交替执行,构成**策略迭代(Policy Iteration)**算法,保证收敛到最优策略。


六、探索与利用

强化学习面临一个核心困境:探索(Exploration) 还是 利用(Exploitation)

  • 利用:选择当前已知最优的动作,获取最大即时收益
  • 探索:尝试未知动作,可能发现更好的策略

$$\text{探索与利用的权衡} \iff \text{已知的好 vs 未知的好}$$

6.1 ε-贪心策略

最常用的探索方法是 $\varepsilon$-贪心($\varepsilon$-greedy):以概率 $1 - \varepsilon$ 选择当前最优动作,以概率 $\varepsilon$ 随机选择动作。

$$a_t = \begin{cases} \arg\max_a Q(s_t, a) & \text{以概率 } 1 - \varepsilon \ \text{随机动作} & \text{以概率 } \varepsilon \end{cases}$$

#![allow(unused)]
fn main() {
/// ε-贪心策略实现
struct EpsilonGreedy {
    q_table: Vec<Vec<f64>>,  // Q(s, a) 表
    epsilon: f64,            // 探索率
    num_actions: usize,
}

impl EpsilonGreedy {
    fn new(num_states: usize, num_actions: usize, epsilon: f64) -> Self {
        Self {
            q_table: vec![vec![0.0; num_actions]; num_states],
            epsilon,
            num_actions,
        }
    }

    /// ε-贪心选择动作
    fn select_action(&self, state: usize) -> usize {
        if rand::random::<f64>() < self.epsilon {
            // 探索:随机选择
            (rand::random::<f64>() * self.num_actions as f64).floor() as usize
        } else {
            // 利用:选择 Q 值最大的动作
            self.q_table[state]
                .iter()
                .enumerate()
                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
                .map(|(idx, _)| idx)
                .unwrap()
        }
    }

    /// ε 衰减:随着训练进行逐渐减少探索
    fn decay_epsilon(&mut self, decay: f64, min_epsilon: f64) {
        self.epsilon = (self.epsilon * decay).max(min_epsilon);
    }
}
}

6.2 其他探索方法

方法原理适用场景
Boltzmann 探索按 Q 值的 softmax 概率选择动作需要平滑探索
UCB(上置信界)选择置信区间上界最大的动作多臂老虎机问题
乐观初始化将 Q 值初始化为较大值,鼓励探索表格型方法
噪声网络在网络参数中添加噪声连续动作空间

七、强化学习算法

7.1 Q-Learning

Q-Learning 是最经典的**异策略(off-policy)**时序差分算法。它直接学习最优动作价值函数 $Q^*(s, a)$,不依赖当前策略的执行。

更新规则:

$$Q(s, a) \leftarrow Q(s, a) + \alpha \left[ r + \gamma \max_{a’} Q(s’, a’) - Q(s, a) \right]$$

其中 $\alpha$ 为学习率,$\gamma$ 为折扣因子。

#![allow(unused)]
fn main() {
/// Q-Learning 智能体
struct QLearningAgent {
    q_table: Vec<Vec<f64>>,
    learning_rate: f64,  // α
    discount_factor: f64, // γ
    epsilon: f64,
    num_actions: usize,
}

impl QLearningAgent {
    fn new(num_states: usize, num_actions: usize, lr: f64, gamma: f64, epsilon: f64) -> Self {
        Self {
            q_table: vec![vec![0.0; num_actions]; num_states],
            learning_rate: lr,
            discount_factor: gamma,
            epsilon,
            num_actions,
        }
    }

    /// Q-Learning 更新规则
    fn update(&mut self, s: usize, a: usize, r: f64, s_next: usize, done: bool) {
        // 计算目标值
        let max_q_next = if done {
            0.0
        } else {
            self.q_table[s_next]
                .iter()
                .cloned()
                .fold(f64::NEG_INFINITY, f64::max)
        };

        let target = r + self.discount_factor * max_q_next;
        // 时序差分更新
        let td_error = target - self.q_table[s][a];
        self.q_table[s][a] += self.learning_rate * td_error;
    }

    /// 选择动作(ε-贪心)
    fn select_action(&self, state: usize) -> usize {
        if rand::random::<f64>() < self.epsilon {
            (rand::random::<f64>() * self.num_actions as f64).floor() as usize
        } else {
            self.q_table[state]
                .iter()
                .enumerate()
                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
                .map(|(idx, _)| idx)
                .unwrap()
        }
    }
}
}

7.2 SARSA

SARSA 是一种**同策略(on-policy)**算法,它使用实际执行的动作来更新 Q 值,而非贪婪地选择最大 Q 值。

更新规则:

$$Q(s, a) \leftarrow Q(s, a) + \alpha \left[ r + \gamma Q(s’, a’) - Q(s, a) \right]$$

其中 $a’$ 是在状态 $s’$ 下实际执行的动作(也遵循 $\varepsilon$-贪心策略)。

#![allow(unused)]
fn main() {
/// SARSA 智能体
struct SarsaAgent {
    q_table: Vec<Vec<f64>>,
    learning_rate: f64,
    discount_factor: f64,
    epsilon: f64,
    num_actions: usize,
}

impl SarsaAgent {
    fn new(num_states: usize, num_actions: usize, lr: f64, gamma: f64, epsilon: f64) -> Self {
        Self {
            q_table: vec![vec![0.0; num_actions]; num_states],
            learning_rate: lr,
            discount_factor: gamma,
            epsilon,
            num_actions,
        }
    }

    /// SARSA 更新规则(使用实际选择的下一个动作 a')
    fn update(&mut self, s: usize, a: usize, r: f64, s_next: usize, a_next: usize, done: bool) {
        let next_q = if done { 0.0 } else { self.q_table[s_next][a_next] };
        let target = r + self.discount_factor * next_q;
        let td_error = target - self.q_table[s][a];
        self.q_table[s][a] += self.learning_rate * td_error;
    }

    fn select_action(&self, state: usize) -> usize {
        if rand::random::<f64>() < self.epsilon {
            (rand::random::<f64>() * self.num_actions as f64).floor() as usize
        } else {
            self.q_table[state]
                .iter()
                .enumerate()
                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
                .map(|(idx, _)| idx)
                .unwrap()
        }
    }
}
}

7.3 Q-Learning 与 SARSA 的对比

特性Q-LearningSARSA
策略类型异策略(off-policy)同策略(on-policy)
更新目标$\max_{a’} Q(s’, a’)$$Q(s’, a’)$(实际动作)
探索影响不受探索策略影响受探索策略影响
收敛性更激进,可能过估计更保守,更稳定
适用场景目标明确的任务需要安全探索的任务

7.4 深度 Q 网络(DQN)

当状态空间或动作空间非常大时,Q 表无法存储所有状态-动作对。深度 Q 网络(Deep Q-Network, DQN)用神经网络近似 $Q(s, a)$,将状态作为输入,输出各动作的 Q 值。

DQN 的两个关键创新:

  1. 经验回放(Experience Replay):将转移元组 $(s, a, r, s’)$ 存入回放缓冲区,训练时随机采样,打破数据相关性
  2. 目标网络(Target Network):使用独立的网络计算目标 Q 值,定期同步,提高训练稳定性

$$\text{损失函数:} \mathcal{L}(\theta) = \mathbb{E} \left[ \left( r + \gamma \max_{a’} Q(s’, a’; \theta^-) - Q(s, a; \theta) \right)^2 \right]$$

其中 $\theta$ 为在线网络参数,$\theta^-$ 为目标网络参数。

#![allow(unused)]
fn main() {
/// 经验回放缓冲区
struct ReplayBuffer {
    buffer: Vec<Experience>,
    capacity: usize,
}

/// 一次转移的经验
#[derive(Clone)]
struct Experience {
    state: usize,
    action: usize,
    reward: f64,
    next_state: usize,
    done: bool,
}

impl ReplayBuffer {
    fn new(capacity: usize) -> Self {
        Self { buffer: Vec::with_capacity(capacity), capacity }
    }

    /// 存入经验
    fn push(&mut self, exp: Experience) {
        if self.buffer.len() >= self.capacity {
            self.buffer.remove(0); // FIFO 淘汰最旧经验
        }
        self.buffer.push(exp);
    }

    /// 随机采样一批经验
    fn sample(&self, batch_size: usize) -> Vec<&Experience> {
        use rand::seq::SliceRandom;
        let mut rng = rand::thread_rng();
        self.buffer.choose_multiple(&mut rng, batch_size).collect()
    }

    fn len(&self) -> usize {
        self.buffer.len()
    }
}
}

7.5 近端策略优化(PPO)

近端策略优化(Proximal Policy Optimization, PPO)是目前最广泛使用的策略梯度算法之一。它通过限制策略更新幅度来保证训练稳定性。

PPO-Clip 目标函数:

$$L^{\text{CLIP}}(\theta) = \mathbb{E}_t \left[ \min\left( r_t(\theta) \hat{A}_t,\ \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) \hat{A}_t \right) \right]$$

其中:

  • $r_t(\theta) = \frac{\pi_\theta(a_t \mid s_t)}{\pi_{\theta_{\text{old}}}(a_t \mid s_t)}$ 是新旧策略的概率比
  • $\hat{A}_t$ 是优势函数估计
  • $\epsilon$ 是裁剪参数(通常取 0.1 ~ 0.2)

PPO 的核心思想是:当新策略偏离旧策略太远时,通过裁剪来限制梯度更新,避免策略崩溃。


八、实战:用 Rust 实现 Q-Learning(FrozenLake 环境)

下面我们用一个经典的 FrozenLake(冰湖)环境来演示 Q-Learning 的完整实现。冰湖是一个 4x4 的网格世界,智能体需要从起点 S 走到终点 G,途中要避开冰洞 H。

SFFF
FHFH
FFFH
HFFG

S = 起点, F = 冰面(安全), H = 冰洞(掉入则失败), G = 目标(到达则成功)

use rand::Rng;

/// FrozenLake 环境
struct FrozenLake {
    /// 4x4 网格,共 16 个状态
    grid: [i8; 16],
    current_state: usize,
}

impl FrozenLake {
    fn new() -> Self {
        // 0=安全, 1=冰洞, 2=起点, 3=终点
        let grid = [
            2, 0, 0, 0,  // SFFF
            0, 1, 0, 1,  // FHFH
            0, 0, 0, 1,  // FFFH
            1, 0, 0, 3,  // HFFG
        ];
        Self { grid, current_state: 0 }
    }

    /// 4 个动作:0=左, 1=下, 2=右, 3=上
    fn step(&mut self, action: usize) -> (usize, f64, bool) {
        let s = self.current_state as i32;
        let row = s / 4;
        let col = s % 4;

        let (new_row, new_col) = match action {
            0 => (row, col - 1),     // 左
            1 => (row + 1, col),     // 下
            2 => (row, col + 1),     // 右
            3 => (row - 1, col),     // 上
            _ => (row, col),
        };

        // 边界检查:超出边界则原地不动
        let new_state = if new_row < 0 || new_row > 3 || new_col < 0 || new_col > 3 {
            self.current_state
        } else {
            (new_row * 4 + new_col) as usize
        };

        self.current_state = new_state;

        let reward = if self.grid[new_state] == 3 { 1.0 } else { 0.0 };
        let done = self.grid[new_state] == 1 || self.grid[new_state] == 3;

        (new_state, reward, done)
    }

    fn reset(&mut self) -> usize {
        self.current_state = 0;
        0
    }
}

/// Q-Learning 智能体
struct QLearner {
    q_table: [[f64; 4]; 16], // 16 个状态,4 个动作
    lr: f64,
    gamma: f64,
    epsilon: f64,
    epsilon_min: f64,
    epsilon_decay: f64,
}

impl QLearner {
    fn new(lr: f64, gamma: f64, epsilon: f64) -> Self {
        Self {
            q_table: [[0.0; 4]; 16],
            lr,
            gamma,
            epsilon,
            epsilon_min: 0.01,
            epsilon_decay: 0.995,
        }
    }

    fn select_action(&self, state: usize) -> usize {
        let mut rng = rand::thread_rng();
        if rng.gen::<f64>() < self.epsilon {
            rng.gen_range(0..4)
        } else {
            self.q_table[state]
                .iter()
                .enumerate()
                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
                .map(|(idx, _)| idx)
                .unwrap()
        }
    }

    fn update(&mut self, s: usize, a: usize, r: f64, s_next: usize, done: bool) {
        let max_q = if done {
            0.0
        } else {
            self.q_table[s_next].iter().cloned().fold(f64::NEG_INFINITY, f64::max)
        };
        let target = r + self.gamma * max_q;
        self.q_table[s][a] += self.lr * (target - self.q_table[s][a]);
    }

    fn decay_epsilon(&mut self) {
        self.epsilon = (self.epsilon * self.epsilon_decay).max(self.epsilon_min);
    }
}

fn main() {
    let mut env = FrozenLake::new();
    let mut agent = QLearner::new(0.1, 0.99, 1.0);
    let episodes = 10_000;

    println!("=== FrozenLake Q-Learning 训练 ===\n");

    for ep in 0..episodes {
        let mut state = env.reset();
        let mut total_reward = 0.0;

        loop {
            let action = agent.select_action(state);
            let (next_state, reward, done) = env.step(action);
            agent.update(state, action, reward, next_state, done);
            total_reward += reward;
            state = next_state;

            if done { break; }
        }

        agent.decay_epsilon();

        if (ep + 1) % 1000 == 0 {
            let wins = evaluate(&mut env, &agent, 100);
            println!(
                "Episode {}: ε={:.4}, 胜率={:.0}%",
                ep + 1, agent.epsilon, wins
            );
        }
    }

    // 打印学到的策略
    println!("\n=== 学到的最优策略 ===");
    let action_names = ["←", "↓", "→", "↑"];
    for s in 0..16 {
        let best = agent.q_table[s]
            .iter()
            .enumerate()
            .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
            .map(|(idx, _)| idx)
            .unwrap();
        print!("{}", action_names[best]);
        if (s + 1) % 4 == 0 { println!(); }
    }
}

/// 评估当前策略的胜率
fn evaluate(env: &mut FrozenLake, agent: &QLearner, trials: usize) -> f64 {
    let mut wins = 0;
    for _ in 0..trials {
        let mut state = env.reset();
        loop {
            let action = agent.q_table[state]
                .iter()
                .enumerate()
                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
                .map(|(idx, _)| idx)
                .unwrap();
            let (next_state, _reward, done) = env.step(action);
            state = next_state;
            if done {
                if env.grid[state] == 3 { wins += 1; }
                break;
            }
        }
    }
    wins as f64 / trials as f64 * 100.0
}

运行后,智能体将逐渐学会避开冰洞、到达终点的最优路径。


九、Rust 强化学习生态

Rust 在强化学习领域虽然不如 Python 生态成熟,但凭借其性能优势和安全性,正在快速发展。以下是主要的 Rust RL 相关 crate 和工具:

Crate / 项目说明适用场景
candleHuggingFace 出品的深度学习框架DQN/PPO 等深度 RL 算法的网络部分
burn灵活的深度学习框架,支持多种后端神经网络训练与推理
smartcore纯 Rust 机器学习库基础数学运算、线性代数
ndarrayN 维数组库,类似 NumPy矩阵运算、张量操作
rand随机数生成探索策略、经验采样
tchPyTorch 的 Rust 绑定需要直接使用 PyTorch 模型
rugraph图计算库状态转移图建模
gym-rsOpenAI Gym 的 Rust 移植经典 RL 环境接口

9.1 使用 ndarray 进行矩阵运算

use ndarray::{array, Array2};

fn main() {
    // Q 表用 ndarray 表示
    let mut q_table: Array2<f64> = Array2::zeros((16, 4));

    // 设置初始值
    q_table[[0, 2]] = 0.5; // 状态 0,动作"右"
    q_table[[1, 1]] = 0.3; // 状态 1,动作"下"

    // 获取某状态的最大 Q 值
    let state = 0;
    let max_q = q_table.row(state).iter().cloned()
        .fold(f64::NEG_INFINITY, f64::max);
    println!("状态 {} 的最大 Q 值: {:.2}", state, max_q);

    // 获取最优动作
    let best_action = q_table.row(state)
        .iter().enumerate()
        .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
        .map(|(idx, _)| idx)
        .unwrap();
    println!("最优动作: {}", best_action);
}

9.2 Rust 实现 RL 的优势

  1. 内存安全:无需垃圾回收,无数据竞争,适合长时间训练任务
  2. 高性能:零成本抽象,适合大规模环境模拟
  3. 并发友好rayon 等库轻松实现并行环境采样
  4. WebAssembly:可将训练好的 RL 模型部署到浏览器
  5. 嵌入式部署:适合边缘设备和实时控制系统

十、总结与练习

10.1 本章要点总结

主题核心内容
强化学习定义智能体通过与环境交互、试错学习来最大化累积奖励
MDP五元组 $\langle S, A, P, R, \gamma \rangle$,马尔可夫性质是核心假设
贝尔曼方程价值函数的递推分解,连接即时奖励与未来价值
策略与价值函数$V^\pi(s)$ 评估状态好坏,$Q^\pi(s,a)$ 评估动作好坏
探索与利用$\varepsilon$-贪心是最常用的平衡方法
Q-Learning异策略、off-policy,直接学习最优 Q 值
SARSA同策略、on-policy,考虑实际执行的动作
DQN用神经网络近似 Q 函数,经验回放 + 目标网络
PPO策略梯度方法,通过裁剪限制策略更新幅度
Rust 生态candle、burn、ndarray 等库支撑 RL 开发

10.2 练习建议

练习 1:实现 SARSA 求解 FrozenLake

在第八节的 FrozenLake 环境基础上,将 Q-Learning 替换为 SARSA 算法,对比两种算法的收敛速度和最终胜率。

练习 2:实现策略迭代

编写完整的策略迭代算法(策略评估 + 策略改进),在小规模 MDP 上验证其收敛性,并与价值迭代对比迭代次数。

练习 3:多臂老虎机

实现一个简单的多臂老虎机(Multi-Armed Bandit)环境,分别用 $\varepsilon$-贪心、UCB 和 Boltzmann 探索三种策略求解,绘制累积奖励曲线。

练习 4:经验回放缓冲区优化

使用 VecDeque 替代 Vec 实现更高效的 FIFO 经验回放缓冲区,并添加优先级采样(Prioritized Experience Replay)功能。

练习 5:扩展 FrozenLake

将 FrozenLake 扩展为 8x8 网格,增加随机风(Wind)效果——智能体有概率被吹到其他方向。调整超参数,观察 Q-Learning 的表现变化。


下一章预告:第四十章将探讨生成式 AI(Generative AI),包括自编码器、变分自编码器(VAE)和生成对抗网络(GAN)的原理与 Rust 实现。

第四十 生成式AI(Generative AI)

第四十 生成式AI
├── 什么是生成式AI(Generative AI)
├── 判别模型 vs 生成模型
├── 自编码器(Autoencoder)
│   ├── 编码器与解码器
│   └── 潜在空间与重构
├── 变分自编码器(VAE)
│   ├── ELBO 与证据下界
│   └── 重参数化技巧
├── 生成对抗网络(GAN)
│   ├── 生成器与判别器
│   └── 对抗训练与纳什均衡
├── Diffusion 扩散模型
│   ├── 前向加噪过程
│   └── 逆向去噪过程
├── 大语言模型的生成能力
├── 多模态生成
│   ├── 文生图(Stable Diffusion / DALL-E)
│   └── 文生视频(Sora / Kling)
├── Rust 生成式AI生态
│   ├── candle 框架
│   └── burn 框架
└── 总结与练习

生成式AI(Generative AI)是人工智能领域中最具创造力的分支。如果说判别式AI擅长“判断“——识别猫是猫、判断邮件是否为垃圾邮件——那么生成式AI则擅长“创造“——生成逼真的图像、撰写流畅的文章、谱写动人的旋律。

$$\text{判别模型:} P(y \mid x) \quad \longleftrightarrow \quad \text{生成模型:} P(x)$$

从2014年GAN的横空出世,到2022年Stable Diffusion的全民狂欢,再到2024年Sora的震撼发布,生成式AI正在重塑人类与机器的协作边界。


一、什么是生成式AI

1.1 定义

生成式AI是指能够学习数据分布 $P(x)$,并据此生成新的、与训练数据统计特征相似但又不完全相同的样本的人工智能系统。

$$x_{\text{new}} \sim P_{\theta}(x), \quad x_{\text{new}} \notin {x_1, x_2, \ldots, x_n}$$

核心思想:模型不仅学习数据的模式,还能“创造“出符合这些模式的新数据。

1.2 生成式AI的能力谱系

能力层级任务代表系统
文本生成文章、代码、对话GPT-4, Claude, DeepSeek
图像生成艺术、照片、设计Stable Diffusion, DALL-E 3, Midjourney
音频生成语音、音乐、音效Whisper, Suno, ElevenLabs
视频生成短片、动画Sora, Kling, Runway
3D生成模型、场景Point-E, Shap-E
多模态生成跨模态理解与生成Gemini, GPT-4o

1.3 发展简史

  • 2014:GAN(生成对抗网络)提出,生成模型进入深度学习时代
  • 2015:VAE(变分自编码器)成熟,概率生成模型框架确立
  • 2020:GPT-3 展现大规模语言模型的生成能力
  • 2022:Stable Diffusion 开源,文生图技术全民化
  • 2023:GPT-4、Midjourney V5 多模态生成能力飞跃
  • 2024:Sora 文生视频模型发布,Diffusion Transformer 架构兴起
  • 2025:多模态统一模型成为主流趋势

二、判别模型 vs 生成模型

2.1 核心区别

机器学习模型按建模目标可分为判别模型和生成模型两大类。

$$\underbrace{P(y \mid x)}{\text{判别模型}} \quad \text{vs} \quad \underbrace{P(x, y) = P(x \mid y) \cdot P(y)}{\text{生成模型}}$$

维度判别模型生成模型
建模目标条件概率 $P(y \mid x)$联合概率 $P(x, y)$ 或边际概率 $P(x)$
核心问题“这个是什么?”“生成一个这样的”
典型算法SVM、逻辑回归、CNN分类器GAN、VAE、Diffusion、Flow
输出类别标签 / 决策边界新样本
数据需求标注数据可无标注(自监督)

2.2 直观理解

#![allow(unused)]
fn main() {
/// 判别模型:给定输入,输出类别
fn discriminative_model(input: &Input) -> Class {
    // "这张图是猫还是狗?"
    if input.features().cat_score() > 0.8 {
        Class::Cat
    } else {
        Class::Dog
    }
}

/// 生成模型:给定条件,生成样本
fn generative_model(condition: &Condition) -> Sample {
    // "生成一张猫的图片"
    let noise = random_noise();       // 随机种子
    let sample = decode(condition, noise); // 解码生成
    sample
}
}

2.3 生成模型的统一视角

所有生成模型都可以归结为对数据分布 $P(x)$ 的建模。不同的方法采用了不同的策略:

$$P_{\theta}(x) \approx P_{\text{data}}(x)$$

  • 显式密度估计:直接建模 $P_{\theta}(x)$(VAE、Normalizing Flow)
  • 隐式密度估计:不直接建模概率,通过采样来逼近分布(GAN)
  • 分数匹配:建模数据的梯度场 $\nabla_x \log P(x)$(Diffusion)

三、自编码器(Autoencoder)

自编码器是最简单的生成模型雏形,通过“压缩-解压“学习数据的紧凑表示。

3.1 架构

自编码器由两部分组成:

$$\text{输入 } x \xrightarrow{\text{编码器 } f_{\phi}} \text{潜在表示 } z \xrightarrow{\text{解码器 } g_{\theta}} \text{重构 } \hat{x}$$

  • 编码器(Encoder):$z = f_{\phi}(x)$,将高维输入压缩为低维潜在向量
  • 解码器(Decoder):$\hat{x} = g_{\theta}(z)$,从潜在向量重构原始输入

3.2 损失函数

自编码器的训练目标是最小化重构误差:

$$\mathcal{L}{\text{AE}} = \frac{1}{N} \sum{i=1}^{N} | x_i - g_{\theta}(f_{\phi}(x_i)) |^2$$

3.3 Rust 实现

use candle_core::{Device, Tensor, DType, Result};
use candle_nn::{VarMap, Linear, Module, Builder};

/// 简易自编码器
struct Autoencoder {
    encoder1: Linear,
    encoder2: Linear,
    decoder1: Linear,
    decoder2: Linear,
}

impl Autoencoder {
    fn new(vs: candle_nn::VarBuilder) -> Result<Self> {
        // 编码器: 784 -> 128 -> 32(MNIST 图像 28x28=784)
        let encoder1 = candle_nn::linear(784, 128, vs.pp("enc1"))?;
        let encoder2 = candle_nn::linear(128, 32, vs.pp("enc2"))?;
        // 解码器: 32 -> 128 -> 784
        let decoder1 = candle_nn::linear(32, 128, vs.pp("dec1"))?;
        let decoder2 = candle_nn::linear(128, 784, vs.pp("dec2"))?;
        Ok(Self { encoder1, encoder2, decoder1, decoder2 })
    }

    /// 编码:将图像压缩为潜在向量
    fn encode(&self, x: &Tensor) -> Result<Tensor> {
        let z = x.apply(&self.encoder1)?.relu()?.apply(&self.encoder2)?;
        Ok(z)
    }

    /// 解码:从潜在向量重构图像
    fn decode(&self, z: &Tensor) -> Result<Tensor> {
        let x_hat = z.apply(&self.decoder1)?.relu()?.apply(&self.decoder2)?;
        Ok(x_hat)
    }

    /// 前向传播:编码 -> 解码
    fn forward(&self, x: &Tensor) -> Result<Tensor> {
        let z = self.encode(x)?;
        self.decode(&z)
    }
}

fn main() -> Result<()> {
    let device = Device::Cpu;
    let mut varmap = VarMap::new();
    let vs = candle_nn::VarBuilder::from_varmap(&varmap, DType::F32, &device);

    let ae = Autoencoder::new(vs)?;

    // 模拟输入:batch=4, 每张图 784 像素
    let input = Tensor::randn(0f32, 1.0, (4, 784), &device)?;
    let output = ae.forward(&input)?;

    // 计算重构误差 (MSE)
    let diff = input.sub(&output)?;
    let mse = (diff.powf(2.0)?.mean_all()? as f32).sqrt();
    println!("重构误差 (RMSE): {:.6}", mse);
    println!("输入形状:  {:?}", input.shape());   // [4, 784]
    println!("输出形状:  {:?}", output.shape());  // [4, 784]

    // 查看潜在空间维度
    let z = ae.encode(&input)?;
    println!("潜在向量形状: {:?}", z.shape());    // [4, 32]

    Ok(())
}

3.4 自编码器的局限

普通自编码器存在一个关键问题:潜在空间 $z$ 是不连续的。在潜在空间中随机采样一个点,解码后往往得到无意义的输出。这意味着它无法真正“生成“新样本,只能“记忆“和“重构“已有样本。

$$z \sim \mathcal{U}(\text{latent space}) \quad \Rightarrow \quad g_{\theta}(z) \text{ 可能无意义}$$


四、变分自编码器(VAE)

VAE 通过引入概率框架,解决了自编码器潜在空间不连续的问题,使其具备了真正的生成能力。

4.1 核心思想

VAE 不再将输入映射为确定性的潜在向量,而是映射为一个概率分布:

$$x \xrightarrow{\text{编码器}} (\mu, \sigma^2) \xrightarrow{\text{采样}} z \sim \mathcal{N}(\mu, \sigma^2) \xrightarrow{\text{解码器}} \hat{x}$$

编码器输出均值 $\mu$ 和方差 $\sigma^2$,从该高斯分布中采样 $z$,再由解码器生成样本。

4.2 ELBO 与证据下界

VAE 的训练目标是最大化数据的对数似然 $\log P(x)$。由于真实后验 $P(z|x)$ 不可计算,我们通过变分推断引入证据下界(ELBO, Evidence Lower BOund):

$$\log P(x) \geq \mathbb{E}{q{\phi}(z|x)}[\log P_{\theta}(x|z)] - D_{\text{KL}}(q_{\phi}(z|x) | P(z))$$

等价地,最小化以下损失函数:

$$\mathcal{L}{\text{VAE}} = \underbrace{\mathbb{E}{q_{\phi}(z|x)}[| x - g_{\theta}(z) |^2]}{\text{重构损失}} + \underbrace{D{\text{KL}}(q_{\phi}(z|x) | \mathcal{N}(0, I))}_{\text{KL 散度(正则项)}}$$

其中 KL 散度在两个高斯分布之间有闭式解:

$$D_{\text{KL}}(\mathcal{N}(\mu, \sigma^2) | \mathcal{N}(0, 1)) = \frac{1}{2} \sum_{j=1}^{J} \left( \mu_j^2 + \sigma_j^2 - \ln \sigma_j^2 - 1 \right)$$

4.3 重参数化技巧

直接从 $q_{\phi}(z|x)$ 采样时,梯度无法通过随机节点反向传播。重参数化技巧将随机性从计算图中分离:

$$z = \mu + \sigma \odot \epsilon, \quad \epsilon \sim \mathcal{N}(0, I)$$

这样梯度可以流向 $\mu$ 和 $\sigma$,而 $\epsilon$ 被视为常数。

4.4 Rust 实现

use candle_core::{Device, Tensor, DType, Result};
use candle_nn::{VarMap, Linear, Module};

/// 变分自编码器
struct VAE {
    // 编码器
    enc1: Linear,
    enc2: Linear,
    // 均值和对数方差
    fc_mu: Linear,
    fc_logvar: Linear,
    // 解码器
    dec1: Linear,
    dec2: Linear,
}

impl VAE {
    fn new(vs: candle_nn::VarBuilder) -> Result<Self> {
        let enc1 = candle_nn::linear(784, 256, vs.pp("enc1"))?;
        let enc2 = candle_nn::linear(256, 64, vs.pp("enc2"))?;
        let fc_mu = candle_nn::linear(64, 32, vs.pp("fc_mu"))?;
        let fc_logvar = candle_nn::linear(64, 32, vs.pp("fc_logvar"))?;
        let dec1 = candle_nn::linear(32, 256, vs.pp("dec1"))?;
        let dec2 = candle_nn::linear(256, 784, vs.pp("dec2"))?;
        Ok(Self { enc1, enc2, fc_mu, fc_logvar, dec1, dec2 })
    }

    /// 编码:输出均值和对数方差
    fn encode(&self, x: &Tensor) -> Result<(Tensor, Tensor)> {
        let h = x.apply(&self.enc1)?.relu()?.apply(&self.enc2)?.relu()?;
        let mu = h.apply(&self.fc_mu)?;
        let logvar = h.apply(&self.fc_logvar)?;
        Ok((mu, logvar))
    }

    /// 重参数化技巧:z = mu + sigma * epsilon
    fn reparameterize(&self, mu: &Tensor, logvar: &Tensor) -> Result<Tensor> {
        let sigma = (logvar.exp()? * 0.5)?;
        let epsilon = Tensor::randn(0f32, 1.0, mu.shape(), &mu.device())?;
        mu.add(&sigma.mul(&epsilon)?)
    }

    /// 解码
    fn decode(&self, z: &Tensor) -> Result<Tensor> {
        let h = z.apply(&self.dec1)?.relu()?;
        h.apply(&self.dec2)
    }

    /// 前向传播
    fn forward(&self, x: &Tensor) -> Result<(Tensor, Tensor, Tensor)> {
        let (mu, logvar) = self.encode(x)?;
        let z = self.reparameterize(&mu, &logvar)?;
        let recon = self.decode(&z)?;
        Ok((recon, mu, logvar))
    }

    /// 计算 VAE 损失
    fn loss(&self, x: &Tensor) -> Result<(Tensor, f32, f32)> {
        let (recon, mu, logvar) = self.forward(x)?;

        // 重构损失 (MSE)
        let recon_loss = x.sub(&recon)?.powf(2.0)?.mean_all()?;

        // KL 散度: 0.5 * sum(mu^2 + logvar.exp() - logvar - 1)
        let kl_loss = (mu.powf(2.0)?
            + logvar.exp()?
            - logvar
            - Tensor::ones_like(&logvar)?
        )?.mean_all()? * 0.5;

        let total = recon_loss.add(&kl_loss)?;
        Ok((total, recon_loss.to_scalar::<f32>()?, kl_loss.to_scalar::<f32>()?))
    }
}

fn main() -> Result<()> {
    let device = Device::Cpu;
    let mut varmap = VarMap::new();
    let vs = candle_nn::VarBuilder::from_varmap(&varmap, DType::F32, &device);

    let vae = VAE::new(vs)?;

    // 模拟 MNIST 输入
    let input = Tensor::randn(0f32, 1.0, (8, 784), &device)?;
    let (total_loss, recon_loss, kl_loss) = vae.loss(&input)?;

    println!("总损失:     {:.6}", total_loss.to_scalar::<f32>()?);
    println!("重构损失:   {:.6}", recon_loss);
    println!("KL 散度:    {:.6}", kl_loss);

    // 从潜在空间采样生成新样本
    let z = Tensor::randn(0f32, 1.0, (4, 32), &device)?;
    let generated = vae.decode(&z)?;
    println!("生成样本形状: {:?}", generated.shape()); // [4, 784]

    Ok(())
}

五、生成对抗网络(GAN)

GAN 是生成式AI的里程碑式工作,由 Ian Goodfellow 于2014年提出。它通过两个网络的对抗博弈来学习数据分布。

5.1 核心架构

GAN 由两个网络组成:

$$\min_G \max_D ; \mathbb{E}{x \sim P{\text{data}}}[\log D(x)] + \mathbb{E}_{z \sim P_z}[\log(1 - D(G(z)))]$$

  • 生成器(Generator, $G$):接收随机噪声 $z$,生成伪造样本 $G(z)$
  • 判别器(Discriminator, $D$):判断输入是真实样本还是生成样本

$$z \sim \mathcal{N}(0, I) \xrightarrow{G} \hat{x} \xrightarrow{D} \text{ 真/假}$$

5.2 对抗训练

训练过程可以类比为“造假者“与“警察“的博弈:

  1. 训练判别器:使其更好地区分真实样本和生成样本
  2. 训练生成器:使其生成的样本更逼真,骗过判别器

理想状态下,达到纳什均衡:$D(x) = 0.5$,判别器无法区分真假,生成器学会了真实数据的分布。

$$P_G = P_{\text{data}} \quad \Rightarrow \quad D(x) = \frac{1}{2}, ; \forall x$$

5.3 Rust 实现

use candle_core::{Device, Tensor, DType, Result};
use candle_nn::{VarMap, Linear, Module, Builder};

/// 生成器:将噪声向量映射为图像
struct Generator {
    fc1: Linear,
    fc2: Linear,
    fc3: Linear,
}

impl Generator {
    fn new(vs: candle_nn::VarBuilder) -> Result<Self> {
        let fc1 = candle_nn::linear(64, 256, vs.pp("g1"))?;
        let fc2 = candle_nn::linear(256, 256, vs.pp("g2"))?;
        let fc3 = candle_nn::linear(256, 784, vs.pp("g3"))?;
        Ok(Self { fc1, fc2, fc3 })
    }

    fn forward(&self, z: &Tensor) -> Result<Tensor> {
        // 使用 LeakyReLU 激活(避免梯度消失)
        let x = z.apply(&self.fc1)?;
        let x = x.maximum(&x.mul(0.01)?)?; // LeakyReLU(0.01)
        let x = x.apply(&self.fc2)?;
        let x = x.maximum(&x.mul(0.01)?)?;
        let x = x.apply(&self.fc3)?;
        // 使用 Tanh 将输出限制在 [-1, 1]
        x.tanh()
    }
}

/// 判别器:判断输入是真实图像还是生成图像
struct Discriminator {
    fc1: Linear,
    fc2: Linear,
    fc3: Linear,
}

impl Discriminator {
    fn new(vs: candle_nn::VarBuilder) -> Result<Self> {
        let fc1 = candle_nn::linear(784, 256, vs.pp("d1"))?;
        let fc2 = candle_nn::linear(256, 256, vs.pp("d2"))?;
        let fc3 = candle_nn::linear(256, 1, vs.pp("d3"))?;
        Ok(Self { fc1, fc2, fc3 })
    }

    fn forward(&self, x: &Tensor) -> Result<Tensor> {
        // 使用 LeakyReLU,最后用 Sigmoid 输出概率
        let x = x.apply(&self.fc1)?;
        let x = x.maximum(&x.mul(0.01)?)?;
        let x = x.apply(&self.fc2)?;
        let x = x.maximum(&x.mul(0.01)?)?;
        x.apply(&self.fc3)?.sigmoid()
    }
}

/// 二元交叉熵损失
fn binary_cross_entropy(pred: &Tensor, target: &Tensor) -> Result<Tensor> {
    let eps = 1e-7;
    let pred = pred.clamp(eps, 1.0 - eps)?;
    let loss = target.mul(&pred.log()?)?
        + &(Tensor::ones_like(target)?.sub(target)?.mul(&(Tensor::ones_like(&pred)?.sub(&pred)?.log()?))?)?;
    loss.neg()?.mean_all()
}

fn main() -> Result<()> {
    let device = Device::Cpu;

    // 构建生成器和判别器(使用独立的 VarMap)
    let mut g_varmap = VarMap::new();
    let g_vs = candle_nn::VarBuilder::from_varmap(&g_varmap, DType::F32, &device);
    let generator = Generator::new(g_vs)?;

    let mut d_varmap = VarMap::new();
    let d_vs = candle_nn::VarBuilder::from_varmap(&d_varmap, DType::F32, &device);
    let discriminator = Discriminator::new(d_vs)?;

    // 模拟真实数据
    let real_data = Tensor::randn(0f32, 1.0, (32, 784), &device)?;
    // 采样噪声
    let noise = Tensor::randn(0f32, 1.0, (32, 64), &device)?;

    // 生成假数据
    let fake_data = generator.forward(&noise)?;
    println!("假数据形状: {:?}", fake_data.shape()); // [32, 784]

    // 判别器对真实数据的评分
    let real_score = discriminator.forward(&real_data)?;
    let real_label = Tensor::ones_like(&real_score)?;
    let d_loss_real = binary_cross_entropy(&real_score, &real_label)?;

    // 判别器对假数据的评分
    let fake_score = discriminator.forward(&fake_data.detach()?)?;
    let fake_label = Tensor::zeros_like(&fake_score)?;
    let d_loss_fake = binary_cross_entropy(&fake_score, &fake_label)?;

    // 判别器总损失
    let d_loss = (d_loss_real.add(&d_loss_fake)? * 0.5)?;
    println!("判别器损失: {:.6}", d_loss.to_scalar::<f32>()?);

    // 生成器损失(希望判别器将假数据判断为真)
    let gen_score = discriminator.forward(&fake_data)?;
    let gen_label = Tensor::ones_like(&gen_score)?;
    let g_loss = binary_cross_entropy(&gen_score, &gen_label)?;
    println!("生成器损失: {:.6}", g_loss.to_scalar::<f32>()?);

    Ok(())
}

5.4 GAN 的变体

变体核心改进应用场景
DCGAN使用卷积层替代全连接层图像生成
WGAN使用 Wasserstein 距离替代 JS 散度训练稳定性
StyleGAN风格注入、渐进式增长高清人脸生成
CycleGAN循环一致性损失无配对图像风格迁移
Pix2Pix配对图像翻译边缘到照片、素描到彩图

六、Diffusion 扩散模型

Diffusion 模型是当前最主流的生成模型架构,Stable Diffusion、DALL-E、Sora 等明星模型均基于此。

6.1 核心思想

Diffusion 模型的灵感来自热力学中的扩散过程:向数据中逐步添加噪声,直到变成纯高斯噪声;然后学习一个逆向过程,从噪声中恢复数据。

$$\text{前向过程:} x_0 \xrightarrow{+\epsilon_1} x_1 \xrightarrow{+\epsilon_2} x_2 \xrightarrow{\cdots} x_T \approx \mathcal{N}(0, I)$$

$$\text{逆向过程:} x_T \xrightarrow{-\epsilon_T} x_{T-1} \xrightarrow{-\epsilon_{T-1}} \cdots \xrightarrow{-\epsilon_1} x_0$$

6.2 前向加噪过程

前向过程是一个马尔可夫链,每一步添加少量高斯噪声:

$$q(x_t \mid x_{t-1}) = \mathcal{N}(x_t; \sqrt{1 - \beta_t} x_{t-1}, \beta_t \mathbf{I})$$

其中 $\beta_t$ 是预定义的噪声调度(noise schedule),通常从 $\beta_1 = 10^{-4}$ 线性增长到 $\beta_T = 0.02$。

利用重参数化技巧,可以直接从 $x_0$ 得到任意时刻的 $x_t$:

$$x_t = \sqrt{\bar{\alpha}_t} x_0 + \sqrt{1 - \bar{\alpha}_t} \epsilon, \quad \epsilon \sim \mathcal{N}(0, I)$$

其中 $\alpha_t = 1 - \beta_t$,$\bar{\alpha}t = \prod{s=1}^{t} \alpha_s$。

6.3 逆向去噪过程

逆向过程由神经网络 $\epsilon_\theta(x_t, t)$ 预测每一步添加的噪声:

$$p_{\theta}(x_{t-1} \mid x_t) = \mathcal{N}(x_{t-1}; \mu_\theta(x_t, t), \sigma_t^2 \mathbf{I})$$

训练目标是让网络预测的噪声与真实噪声尽可能接近:

$$\mathcal{L}{\text{simple}} = \mathbb{E}{t, x_0, \epsilon} \left[ | \epsilon - \epsilon_\theta(x_t, t) |^2 \right]$$

6.4 Rust 实现(简化的去噪过程)

use candle_core::{Device, Tensor, DType, Result};
use candle_nn::{VarMap, Linear, Module};

/// 简化的噪声预测网络
struct NoisePredictor {
    fc1: Linear,
    fc2: Linear,
    fc3: Linear,
}

impl NoisePredictor {
    fn new(vs: candle_nn::VarBuilder) -> Result<Self> {
        let fc1 = candle_nn::linear(784 + 1, 512, vs.pp("fc1"))?; // +1 为时间步
        let fc2 = candle_nn::linear(512, 256, vs.pp("fc2"))?;
        let fc3 = candle_nn::linear(256, 784, vs.pp("fc3"))?;
        Ok(Self { fc1, fc2, fc3 })
    }

    /// 预测噪声:输入为 (图像, 时间步),输出预测的噪声
    fn predict_noise(&self, x: &Tensor, t: &Tensor) -> Result<Tensor> {
        // 将时间步 t 广播并拼接到图像特征上
        let t_expanded = t.reshape((x.shape().dims()[0], 1))?;
        let t_broadcast = t_expanded.broadcast_as((x.shape().dims()[0], 784))?;
        let input = Tensor::cat(&[x, &t_broadcast], 1)?;

        let h = input.apply(&self.fc1)?.relu()?;
        let h = h.apply(&self.fc2)?.relu()?;
        h.apply(&self.fc3)
    }
}

/// 前向加噪:直接从 x_0 得到 x_t
fn forward_diffusion(x0: &Tensor, t: usize, total_steps: usize) -> Result<Tensor> {
    // 计算 alpha_bar_t
    let beta_start = 0.0001f32;
    let beta_end = 0.02f32;
    let betas: Vec<f32> = (0..total_steps)
        .map(|i| beta_start + (beta_end - beta_start) * i as f32 / total_steps as f32)
        .collect();
    let alphas: Vec<f32> = betas.iter().map(|b| 1.0 - b).collect();
    let alpha_bar_t: f32 = alphas[0..t].iter().product();

    let sqrt_alpha_bar = alpha_bar_t.sqrt();
    let sqrt_one_minus_alpha_bar = (1.0 - alpha_bar_t).sqrt();

    // x_t = sqrt(alpha_bar) * x_0 + sqrt(1 - alpha_bar) * noise
    let noise = Tensor::randn(0f32, 1.0, x0.shape(), &x0.device())?;
    let noisy = x0.mul(sqrt_alpha_bar)?.add(&noise.mul(sqrt_one_minus_alpha_bar)?)?;
    Ok(noisy)
}

/// 逆向去噪:单步去噪
fn reverse_step(
    predictor: &NoisePredictor,
    xt: &Tensor,
    t: f32,
    alpha_bar_t: f32,
    alpha_bar_prev: f32,
) -> Result<Tensor> {
    let device = xt.device();
    let batch_size = xt.shape().dims()[0];

    // 预测噪声
    let t_tensor = Tensor::from_vec(vec![t; batch_size], (batch_size,), device)?;
    let predicted_noise = predictor.predict_noise(xt, &t_tensor)?;

    // 计算 x_{t-1}
    let mean = (xt.sub(&predicted_noise.mul(1.0 - alpha_bar_t)?)?
        * (alpha_bar_prev / alpha_bar_t).sqrt())?;
    Ok(mean)
}

fn main() -> Result<()> {
    let device = Device::Cpu;
    let mut varmap = VarMap::new();
    let vs = candle_nn::VarBuilder::from_varmap(&varmap, DType::F32, &device);
    let predictor = NoisePredictor::new(vs)?;

    // 模拟原始图像
    let x0 = Tensor::randn(0f32, 0.5, (4, 784), &device)?;

    // 前向加噪到第 200 步(共 1000 步)
    let xt = forward_diffusion(&x0, 200, 1000)?;
    println!("加噪后形状: {:?}", xt.shape());

    // 逆向去噪(单步演示)
    let alpha_bar_t = 0.5f32;    // 第 200 步的 alpha_bar(示意值)
    let alpha_bar_prev = 0.55f32; // 第 199 步的 alpha_bar(示意值)
    let x_prev = reverse_step(&predictor, &xt, 200.0, alpha_bar_t, alpha_bar_prev)?;
    println!("去噪后形状: {:?}", x_prev.shape());

    Ok(())
}

6.5 Diffusion 模型的优势

优势说明
训练稳定不存在 GAN 的模式崩溃问题
生成质量高在图像生成上超越 GAN
可控生成通过条件引导(classifier-free guidance)精确控制生成内容
多样性好随机采样保证生成样本的多样性

七、大语言模型的生成能力

大语言模型(LLM)本质上是自回归的生成模型,通过预测下一个 token 来生成文本。

7.1 自回归生成

给定上下文 $x_{1}, x_{2}, \ldots, x_{t-1}$,模型预测下一个 token 的概率分布:

$$P(x_t \mid x_{<t}) = \text{softmax}(W_o \cdot h_t + b_o)$$

其中 $h_t$ 是 Transformer 最后一层的隐藏状态。

生成过程是自回归的:

$$P(x_{1:T}) = \prod_{t=1}^{T} P(x_t \mid x_{<t})$$

7.2 采样策略

策略公式特点
贪心解码$x_t = \arg\max P(x_t \mid x_{<t})$确定性,但可能重复
温度采样$P’(x_t) \propto P(x_t \mid x_{<t})^{1/T}$$T < 1$ 更确定,$T > 1$ 更随机
Top-k从概率最高的 $k$ 个 token 中采样限制候选集
Top-p (nucleus)从累积概率 $\geq p$ 的最小集合中采样动态调整候选集

7.3 Rust 调用 LLM 推理

use candle_core::{Device, Tensor, DType, Result};
use candle_nn::VarBuilder;

/// 简化的自回归文本生成演示
fn generate_token(
    logits: &Tensor,        // [vocab_size] 模型输出的 logits
    temperature: f32,       // 温度参数
    top_k: usize,           // top-k 采样
) -> Result<usize> {
    // 应用温度
    let scaled = logits.div(temperature)?;

    // Top-k 过滤
    let k = top_k.min(logits.dims1()?);
    let (top_values, top_indices) = scaled.topk(k, 0)?;

    // Softmax 转为概率
    let probs = candle_nn::ops::softmax(&top_values, 0)?;

    // 从概率分布中采样
    let probs_vec = probs.to_vec1::<f32>()?;
    let indices_vec = top_indices.to_vec1::<u32>()?;

    let mut rng = rand::thread_rng();
    let r: f32 = rand::Rng::gen_range(&mut rng, 0.0..1.0);
    let mut cumsum = 0.0;
    for (i, &p) in probs_vec.iter().enumerate() {
        cumsum += p;
        if cumsum >= r {
            return Ok(indices_vec[i] as usize);
        }
    }
    Ok(indices_vec[indices_vec.len() - 1] as usize)
}

/// 自回归生成循环(伪代码)
fn autoregressive_generate(
    model: &dyn Fn(&Tensor) -> Result<Tensor>,  // 语言模型
    prompt_tokens: &[usize],                     // 输入提示
    max_new_tokens: usize,                        // 最大生成长度
    temperature: f32,                            // 温度
    top_k: usize,                                 // top-k
) -> Result<Vec<usize>> {
    let mut tokens = prompt_tokens.to_vec();
    let device = Device::Cpu;

    for _ in 0..max_new_tokens {
        // 将当前 token 序列转为张量
        let input = Tensor::from_vec(
            tokens.clone(),
            (1, tokens.len()),
            &device,
        )?;

        // 模型前向传播,取最后一个位置的 logits
        let logits = model(&input)?;  // [1, seq_len, vocab_size]
        let last_logits = logits.i((.., tokens.len() - 1, ..))?.squeeze(0)?; // [vocab_size]

        // 采样下一个 token
        let next_token = generate_token(&last_logits, temperature, top_k)?;
        tokens.push(next_token);

        // 遇到结束符则停止
        if next_token == EOS_TOKEN {
            break;
        }
    }

    Ok(tokens)
}

const EOS_TOKEN: usize = 2;

fn main() -> Result<()> {
    // 模拟 vocab_size=1000 的 logits
    let device = Device::Cpu;
    let logits = Tensor::randn(0f32, 1.0, (1000,), &device)?;

    // 使用温度=0.8, top_k=50 采样
    let token_id = generate_token(&logits, 0.8, 50)?;
    println!("采样得到的 token ID: {}", token_id);

    // 演示不同温度的效果
    for temp in [0.3, 0.8, 1.2, 2.0] {
        let tid = generate_token(&logits, temp, 50)?;
        println!("温度={:.1} -> token={}", temp, tid);
    }

    Ok(())
}

八、多模态生成

多模态生成是生成式AI的前沿方向,目标是实现跨模态的内容创作。

8.1 文生图(Text-to-Image)

文生图模型将文本描述转化为视觉图像。当前主流方法基于 Latent Diffusion

$$\text{文本提示} \xrightarrow{\text{CLIP 编码器}} \text{条件向量} \xrightarrow{\text{U-Net 去噪}} \text{潜在图像} \xrightarrow{\text{VAE 解码器}} \text{像素图像}$$

核心流程:

  1. 文本编码:CLIP Text Encoder 将提示词编码为条件向量
  2. 扩散去噪:在潜在空间中,以条件向量为引导,从噪声逐步去噪
  3. 图像解码:VAE Decoder 将潜在表示解码为像素图像

8.2 文生视频(Text-to-Video)

文生视频是文生图的自然扩展,核心挑战在于保持时间一致性:

$$\text{文本} \xrightarrow{\text{编码}} \text{条件} \xrightarrow{\text{3D 扩散}} \text{潜在视频帧序列} \xrightarrow{\text{解码}} \text{视频}$$

模型发布方关键技术
SoraOpenAIDiffusion Transformer (DiT), 时空一致性
Kling快手3D VAE, 长视频生成
Runway Gen-3Runway时空注意力机制
VeoGoogle潜在扩散 + Transformer

8.3 Rust 中的文生图推理

use candle_core::{Device, Tensor, DType, Result};
use candle_nn::{VarBuilder, Module};

/// 简化的文生图推理流程演示
struct TextToImagePipeline {
    text_encoder: TextEncoder,
    unet: UnetModel,
    vae_decoder: VAEDecoder,
}

/// 文本编码器(简化版 CLIP)
struct TextEncoder {
    embed: candle_nn::Embedding,
    layers: Vec<candle_nn::Linear>,
}

impl TextEncoder {
    fn encode(&self, token_ids: &[usize]) -> Result<Tensor> {
        let device = Device::Cpu;
        let tokens = Tensor::from_vec(
            token_ids.to_vec(),
            (1, token_ids.len()),
            &device,
        )?;
        let mut hidden = self.embed.forward(&tokens)?;
        for layer in &self.layers {
            hidden = hidden.apply(layer)?.relu()?;
        }
        // 返回条件向量 [1, seq_len, dim]
        Ok(hidden)
    }
}

/// U-Net 去噪网络(简化)
struct UnetModel {
    input_proj: candle_nn::Linear,
    cond_proj: candle_nn::Linear,
    output_proj: candle_nn::Linear,
}

impl UnetModel {
    /// 去噪一步:预测噪声
    fn denoise_step(
        &self,
        noisy_latent: &Tensor,  // [1, latent_dim]
        text_condition: &Tensor, // [1, cond_dim]
        timestep: f32,
    ) -> Result<Tensor> {
        // 将条件信息注入
        let x = noisy_latent.apply(&self.input_proj)?;
        let c = text_condition.apply(&self.cond_proj)?;
        let combined = x.add(&c.broadcast_as(x.shape())?)?;
        let predicted_noise = combined.apply(&self.output_proj)?;
        Ok(predicted_noise)
    }
}

/// VAE 解码器(简化)
struct VAEDecoder {
    fc1: candle_nn::Linear,
    fc2: candle_nn::Linear,
}

impl VAEDecoder {
    /// 将潜在表示解码为像素
    fn decode(&self, latent: &Tensor) -> Result<Tensor> {
        let h = latent.apply(&self.fc1)?.relu()?;
        h.apply(&self.fc2) // [1, height * width * channels]
    }
}

fn main() -> Result<()> {
    println!("=== 文生图推理流程演示 ===");
    println!("1. 文本编码:将提示词编码为条件向量");
    println!("2. 扩散去噪:在潜在空间中逐步去噪");
    println!("3. 图像解码:将潜在表示解码为像素图像");
    println!();
    println!("完整实现请参考 candle-transformers 项目中的");
    println!("stable-diffusion 示例。");
    println!();
    println!("相关 crate:");
    println!("  candle-transformers = \"0.8\"");
    println!("  tokenizers = \"0.20\"");
    Ok(())
}

九、Rust 生成式AI生态

Rust 在生成式AI领域的生态正在快速发展,以下介绍两个核心框架。

9.1 candle — HuggingFace 的 Rust ML 框架

candle 是 HuggingFace 推出的纯 Rust 深度学习框架,特点是:

  • 纯 Rust 实现:无 Python 依赖,编译为单一二进制
  • 高性能:支持 CPU / CUDA / Metal 后端
  • 模型丰富:支持 BERT、LLaMA、Stable Diffusion、Whisper 等主流模型
  • 部署友好:适合边缘部署和推理服务
# Cargo.toml
[dependencies]
candle-core = "0.8"
candle-nn = "0.8"
candle-transformers = "0.8"  # 预置模型
use candle_core::Device;
use candle_transformers::models::stable_diffusion;

/// 使用 candle 运行 Stable Diffusion 推理
fn generate_image(prompt: &str) -> Result<(), Box<dyn std::error::Error>> {
    let device = Device::new_cuda(0).unwrap_or(Device::Cpu);

    // 加载 Stable Diffusion 模型
    let config = stable_diffusion::StableDiffusionConfig::v2_1(&device);
    let model = stable_diffusion::StableDiffusion::new(config)?;

    // 生成图像
    let generated = model.generate(
        prompt,          // 提示词
        25,              // 推理步数
        7.5,             // CFG 引导强度
        42,              // 随机种子
        None::<&str>,    // 负面提示词
    )?;

    // 保存为 PNG
    generated.save("generated.png")?;
    println!("图像已保存到 generated.png");

    Ok(())
}

fn main() {
    // candle 支持的生成模型:
    // - stable_diffusion: 文生图
    // - whisper: 语音识别与生成
    // - llama: 大语言模型文本生成
    // - mistral: Mistral 系列模型
    // - phi: Phi 系列模型
    println!("candle 支持的生成模型:");
    println!("  - Stable Diffusion (文生图)");
    println!("  - LLaMA / Mistral / Phi (文本生成)");
    println!("  - Whisper (语音识别)");
    println!("  - Wuerstchen (文生图, 轻量级)");
}

9.2 burn — Rust 深度学习框架

burn 是另一个活跃的 Rust 深度学习框架,特点是:

  • 多后端支持:可切换 Autodiff、LibTorch、NDArray 等后端
  • 训练支持:完整的训练循环、数据加载器、学习率调度
  • 模块化设计:灵活的组件组合
# Cargo.toml
[dependencies]
burn = "0.15"
burn-ndarray = "0.15"  # CPU 后端
#![allow(unused)]
fn main() {
use burn::module::Module;
use burn::tensor::{Tensor, backend::AutodiffBackend};
use burn::nn::{Linear, LinearConfig, ReLU, Builder};

/// 使用 burn 构建生成器
#[derive(Module, Debug)]
pub struct Generator<B: burn::tensor::backend::Backend> {
    fc1: Linear,
    fc2: Linear,
    fc3: Linear,
    activation: ReLU,
}

impl<B: burn::tensor::backend::Backend> Generator<B> {
    pub fn new(latent_dim: usize, hidden_dim: usize, output_dim: usize, device: &B::Device)
        -> Result<Self, burn::tensor::TensorError>
    {
        let fc1 = LinearConfig::new(latent_dim, hidden_dim).init(device);
        let fc2 = LinearConfig::new(hidden_dim, hidden_dim).init(device);
        let fc3 = LinearConfig::new(hidden_dim, output_dim).init(device);
        Ok(Self { fc1, fc2, fc3, activation: ReLU::new() })
    }

    pub fn forward(&self, z: Tensor<B, 2>) -> Result<Tensor<B, 2>, burn::tensor::TensorError> {
        let x = self.fc1.forward(z).apply(&self.activation)?;
        let x = self.fc2.forward(x).apply(&self.activation)?;
        self.fc3.forward(x)
    }
}
}

9.3 Rust 生成式AI 生态总览

crate/项目类型功能适用场景
candle框架深度学习推理与训练模型推理、部署服务
candle-transformers模型库预置 Transformer 模型LLM、Stable Diffusion
burn框架多后端深度学习模型训练、研究
tract推理引擎ONNX/TFLite 推理边缘部署
tch绑定PyTorch C++ 绑定快速原型验证
mistral-rs服务LLM 推理服务本地 LLM 部署
llm-chain链式调用LLM 应用开发AI Agent 构建

十、总结

核心知识回顾

主题核心要点
生成式AI定义学习数据分布 $P(x)$,生成新样本
判别 vs 生成$P(y|x)$ vs $P(x)$;“是什么” vs “创造什么”
自编码器编码器-解码器架构,重构误差最小化
VAEELBO 优化 + 重参数化技巧,概率生成
GAN生成器与判别器对抗训练,纳什均衡
Diffusion前向加噪 + 逆向去噪,当前最主流的生成架构
LLM 生成自回归 token 采样,温度/Top-k/Top-p
多模态生成文生图(Latent Diffusion)、文生视频(DiT)
Rust 生态candle(推理)、burn(训练)、mistral-rs(部署)

生成模型演进路线

自编码器 (AE)
    │  引入概率框架
    ▼
变分自编码器 (VAE) ─── ELBO + 重参数化
    │
    ├── 生成对抗网络 (GAN) ─── 对抗训练
    │       │
    │       └── StyleGAN, WGAN, CycleGAN ...
    │
    └── 扩散模型 (Diffusion) ─── 加噪/去噪
            │
            ├── Stable Diffusion (文生图)
            ├── Sora (文生视频)
            └── DALL-E 3 (文生图)

练习建议

  1. 基础练习:使用 candle 实现一个简单的自编码器,在 MNIST 数据集上训练,观察重构效果。
  2. VAE 实现:在自编码器基础上实现 VAE,比较两者的潜在空间结构(对潜在向量做插值,观察生成效果)。
  3. GAN 训练:实现一个简单的 GAN,记录生成器和判别器的损失曲线,观察训练稳定性问题。
  4. Diffusion 理解:实现前向加噪过程,可视化不同噪声步数下的图像变化;实现单步去噪并观察效果。
  5. 采样策略:实现温度采样、Top-k 采样和 Top-p 采样,比较不同参数下的文本生成效果。
  6. candle 实战:使用 candle-transformers 加载预训练的 Stable Diffusion 模型,尝试不同的提示词生成图像。
  7. 进阶挑战:使用 burn 框架从头训练一个 VAE,实现 MNIST 手写数字的生成,并实现潜在空间的插值可视化。

下卷 秘密与密码

“安全不是产品,而是过程。” —— Bruce Schneier

下卷概述

在信息时代,数据已成为最重要的资产之一。从个人隐私到国家机密,从金融交易到商业合同,信息的安全直接关系到个人权益、企业生存乃至国家安全。而下卷“秘密与密码“正是围绕这一核心主题展开,系统地介绍密码学的基本原理、经典算法、现代应用以及前沿技术。

密码学(Cryptography)一词源于希腊语“kryptos“(隐藏)和“graphein“(书写),意为“隐藏的书写“。几千年来,人类一直在探索如何保护信息的机密性、完整性和可用性。从古代的信使传书到现代的量子通信,密码技术经历了翻天覆地的变化,但其核心使命始终未变——在不可信的环境中建立信任。

Rust语言凭借其内存安全、高性能和并发安全的特性,正在成为密码学和安全领域的重要工具。无论是开发加密库、实现安全协议,还是构建区块链系统,Rust都展现出了独特的优势。本卷将结合Rust实践,深入浅出地讲解密码学的方方面面。

秘密的重要性

“中国有多少核武器?”“可口可乐的配方”“某知名艺人的私密照片”“高考题目”——这些都是不同层面的秘密。秘密之所以重要,是因为一旦泄露,可能对国家、企业或个人造成重大损失和影响。

在数字化时代,秘密的形式更加多样:银行账户密码、医疗记录、商业合同、源代码、用户数据……保护这些秘密的安全,是密码学存在的根本意义。

国家秘密、商业秘密与个人隐私

国家秘密分级

第十条 国家秘密的密级分为绝密、机密、秘密三级。

绝密级国家秘密是最重要的国家秘密,泄露会使国家安全和利益遭受特别严重的损害;机密级国家秘密是重要的国家秘密,泄露会使国家安全和利益遭受严重的损害;秘密级国家秘密是一般的国家秘密,泄露会使国家安全和利益遭受损害。————《中华人民共和国保守国家秘密法》

密码法相关规定

第六条 国家对密码实行分类管理。密码分为核心密码、普通密码和商用密码。

第七条 核心密码、普通密码用于保护国家秘密信息,核心密码保护信息的最高密级为绝密级,普通密码保护信息的最高密级为机密级。核心密码、普通密码属于国家秘密。密码管理部门依照本法和有关法律、行政法规、国家有关规定对核心密码、普通密码实行严格统一管理。

第八条 商用密码用于保护不属于国家秘密的信息。公民、法人和其他组织可以依法使用商用密码保护网络与信息安全。————《中华人民共和国密码法》

商业秘密

商业秘密,是指不为公众所知悉,具有商业价值,并经权利人采取相应保密措施的技术信息、经营信息等商业信息。《民法典》第一百二十三条明确将商业秘密列为知识产权的客体。

泄密与监听、窃听、窃照

秘密如此重要,然而泄密的方式、途径却是五花八门、令人防不胜防。爱德华·约瑟夫·斯诺登曝光的“棱镜计划“,揭露了美国政府监听、入侵全球重要服务器,获取大量电邮、即时消息、视频、照片、存储数据、语音聊天等信息。这一事件震惊全球,也让世人深刻认识到:在互联网络中,信息安全绝非可有可无的点缀,而是关乎每个人切身利益的重大问题。

在日常生活中,泄密风险同样无处不在:

  • 社交媒体:朋友圈照片可能泄露位置、行程、人际关系等敏感信息
  • 定位信息:导航记录、运动轨迹可能暴露个人行踪规律
  • 摄像头与麦克风:智能设备的摄像头和麦克风可能被恶意利用
  • 公共Wi-Fi:不安全的网络连接可能导致数据被截获
  • 钓鱼攻击:伪装成可信来源的邮件和网站骗取敏感信息

如何保护重要秘密的安全,成为互联网时代不得不认真思考的一个重大问题。

《中华人民共和国密码法》 《中华人民共和国保守国家秘密法》 《中华人民共和国反间谍法》

密码的定义

什么是密码呢?根据《中华人民共和国密码法》的定义:第二条 本法所称密码,是指采用特定变换的方法对信息等进行加密保护、安全认证的技术、产品和服务。

简而言之,密码是保护秘密信息的有效工具。

需要注意的是,日常生活中常说的“密码“(如登录密码、支付密码)在密码学中更准确的称呼是“口令“(Password)。真正的密码学密码(Cipher)是指用于加密和解密数据的算法和密钥。

密码学发展简史

密码学的历史几乎与人类文明史一样悠久。从古代到现代,密码技术经历了几个重要的发展阶段:

古典密码时期(古代~19世纪)。最早的密码可以追溯到古埃及的象形文字替换。古希腊人使用“斯巴达密码棒“(Scytale)进行换位加密。古罗马时期,凯撒大帝使用著名的“凯撒密码“——将字母表中的每个字母向后移动固定位数。中世纪,阿拉伯学者首次对密码学进行系统化的科学研究。

机械密码时期(20世纪初~1970年代)。第一次和第二次世界大战极大地推动了密码学的发展。德国发明的Enigma密码机曾被认为是不可破解的,但最终被波兰和英国的密码学家成功破译,这一事件对二战的结果产生了重大影响。

现代密码时期(1970年代~至今)。1976年,Diffie和Hellman发表了《密码学的新方向》,提出了公钥密码的概念,彻底改变了密码学的发展轨迹。1977年,RSA算法被提出,成为第一个实用的非对称加密算法。2009年,比特币的诞生让密码学进入了区块链时代。如今,量子密码学正在成为新的研究前沿。

密码的分类

加密算法可以分为对称加密、非对称加密和不可逆加密三类算法。

分类别名特点典型算法主要用途
对称密码私钥密码、单钥密码加密和解密使用相同密钥,速度快SM4、AES、ChaCha20大量数据加密、文件加密、通信加密
非对称密码公钥密码、双钥密码使用公钥/私钥对,公钥加密私钥解密SM2、RSA、ECC密钥交换、数字签名、身份认证
不可逆密码哈希函数、摘要算法单向变换,无法从输出反推输入SM3、SHA-256、MD5数据完整性校验、密码存储、数字指纹

这三类密码算法各有特点,在实际应用中往往组合使用,形成完整的密码体系:

  • 对称加密负责高效地加密大量数据
  • 非对称加密负责安全地交换对称密钥
  • 哈希函数负责验证数据的完整性

随机数、口令、密钥、三类密码算法、数字证书及CA、加密货币

随机数 口令 密钥

原理—》用途–》实战

密码分类

SM2 SM2椭圆曲线公钥密码算法

密码协议算法与Rust生态

How to encrypt a file in Rust (Using streaming AEAD encryption) 如何使用流式AEAE加密算法加密文件 代码(github) XChaCha20Poly1305 - ChaCha20Poly1305 variant with an extended 192-bit (24-byte) nonce.

密码学的数学基础

密码学的安全性建立在数学问题的困难性之上。理解这些数学基础,有助于我们深入理解各类密码算法的原理:

数论。RSA算法的安全性基于大整数分解的困难性;椭圆曲线密码基于椭圆曲线离散对数问题。素数检测、模运算、欧拉定理等都是密码学中常用的数论工具。

代数。群、环、域等代数结构为密码算法提供了抽象的数学框架。有限域上的运算是对称密码和哈希函数的核心操作。

概率论与信息论。香农的信息论为密码学提供了理论基础。完美保密性、熵、冗余度等概念帮助我们评估密码系统的安全性。

计算复杂性理论。密码算法的安全性通常依赖于某些问题在计算上的困难性。P vs NP问题与密码学有着深刻的联系——如果P=NP,那么目前广泛使用的许多密码算法将不再安全。

应用领域

密码技术在现代社会中有着广泛而深入的应用:

数据安全。数据加密与解密保护存储和传输中的敏感信息;密钥交换协议让通信双方安全地建立共享密钥;密钥分享技术将秘密分散保存,提高可靠性。

身份与信任。消息摘要(哈希值)提供数据的“数字指纹“;消息认证码(MAC)验证消息的完整性和来源;数字签名提供不可抵赖的身份认证;数字证书和PKI基础设施构建了互联网信任体系。

网络通信。TLS/SSL协议保护HTTPS通信的安全;VPN技术利用密码学建立安全隧道;商密传输协议和商密数字证书(如SM2-with-SM3)满足国内合规要求。

新兴领域。区块链技术利用哈希函数和数字签名构建去中心化账本;零知识证明实现了“证明者知道某事“而不泄露具体信息;同态加密允许在加密数据上直接进行计算。

TLS、商密传输协议 商密数字证书:SM2-with-SM3

加密算法库

Rust生态系统和开源社区提供了丰富的密码学库和工具:

库/工具说明特点
OpenSSLCryptography and SSL/TLS Toolkit业界标准,功能全面,支持国密
OpenSSL 中文网OpenSSL中文资源站点中文文档与教程
libsmRust国密算法库纯Rust实现的SM2/SM3/SM4
GMSSL国密SSL工具包支持国密SSL协议
sm-crypto国密算法JS实现sm2、sm3和sm4的JavaScript实现
RustCryptoCryptographic algorithms written in pure Rust纯Rust密码算法集合
铜锁蚂蚁集团开源密码库支持国密和国际算法
Bouncy CastleJava/C#密码库功能丰富,支持国密

下卷各章节内容预览

章节标题核心内容
第四十一口令口令安全、强口令策略、口令哈希、口令管理器、Rust口令生成
第四十二随机数真随机数与伪随机数、CSPRNG、rand crate、密码学安全随机数
第四十三密钥管理密钥生成、存储、分发、轮换、HSM、密钥派生函数
第四十四对称密码分组密码、流密码、工作模式、AES/SM4、Rust加密实现
第四十五非对称密码RSA、ECC、SM2、密钥交换、数字签名、Rust实现
第四十六不可逆密码哈希函数、SM3/SHA-256、HMAC、密码学哈希属性
第四十七身份认证认证因素、多因素认证、生物识别、零信任架构
第四十八授权访问访问控制模型、RBAC/ABAC、OAuth2、JWT、权限设计
第四十九PKI公钥基础设施、CA、数字证书、证书链、CRL/OCSP
第五十TLSSSL/TLS协议、握手过程、证书验证、Rustls实现
第五十一区块链区块结构、共识机制、智能合约、Rust区块链开发
第五十二零知识证明ZK-SNARKs、zk-STARKs、应用场景、Rust实现
第五十三同态加密部分同态、全同态、BFV/CKKS方案、隐私计算
第五十四量子密码量子密钥分发、后量子密码、抗量子算法、量子计算威胁
第七十一无敌密码一次性密码本、信息论安全、完美保密性
第七十二终极密码密码学的未来、量子安全密码、自毁密码、终极安全

下卷知识图谱

                    秘密与密码
                       |
        +--------------+--------------+
        |              |              |
      基础密码学    三类密码算法    信任基础设施    前沿密码技术    终极密码
        |              |              |              |              |
   +----+----+    +----+----+    +----+----+    +----+----+    +----+----+
   |    |    |    |    |    |    |    |    |    |    |    |    |         |
  随机数 口令 密钥管理 对称 非对称 不可逆 认证 授权 PKI TLS 区块链 ZK 同态 量子 无敌 终极
   |    |    |    |    |    |    |    |    |    |              |
  第四十二 第四十一 第四十三 第四十四 第四十五 第四十六 第四十七 第四十八 第四十九 第五十  第五十一~五十四  第七十一~七十二

密码学学习路径

对于不同背景的读者,我们推荐以下学习路径:

路径一:应用开发者

  1. 了解基本密码概念(第四十一~四十三章)
  2. 掌握对称/非对称/哈希的使用场景(第四十四~四十六章)
  3. 学习TLS和证书配置(第四十九~五十章)
  4. 了解JWT和授权(第四十七~四十八章)
  5. 关注区块链和前沿技术(第五十一~五十四章)

路径二:安全工程师

  1. 系统学习三类密码算法原理(第四十四~四十六章)
  2. 深入PKI和TLS协议(第四十九~五十章)
  3. 掌握密钥管理和随机数安全(第四十二~四十三章)
  4. 研究零知识和同态加密(第五十二~五十三章)
  5. 了解量子密码和后量子安全(第五十四章)

路径三:密码学研究者

  1. 夯实数学基础(中卷第二十六~三十五章)
  2. 精读所有密码算法章节(第四十一~五十四章)
  3. 深入理解终极密码的理论极限(第七十一~七十二章)

阅读建议

  1. 先理解原理,再动手实践。密码学是一门严谨的学科,错误的实现可能导致严重的安全漏洞。在编写加密代码之前,务必充分理解算法的原理和适用场景。

  2. 优先使用成熟库。密码学领域有一句名言:“不要自己实现加密算法。“Rust生态中有许多经过审计的密码学库(如ringrustls),在生产环境中应优先使用这些成熟方案。

  3. 关注国密算法。在中国境内运营的系统,应了解并正确使用国密算法(SM2/SM3/SM4),确保符合相关法律法规要求。

  4. 保持更新。密码学是一个快速发展的领域。新的攻击方法、新的算法标准、新的合规要求层出不穷。建议关注密码学社区的最新动态。

  5. 理论与实践结合。本卷提供了大量的Rust代码示例,建议读者在阅读的同时编译运行这些代码,观察输入输出,加深理解。

教程与资源

rust-openssl OpenSSL bindings for Rust

愿你在秘密与密码的世界中,掌握保护信息安全的利器,构建值得信赖的数字世界!

第二十六 口令(password)

口令概述

计算机、互联网中为了保护系统安全、认证用户身份,经常使用账号/密码(准确来说应该为“口令“)对用户进行访问控制。口令(password)是信息安全领域最古老、最广泛使用的身份认证机制,从1961年 MIT 的 CTSS 时间共享系统首次引入口令机制至今,口令仍然是绝大多数系统用户认证的第一道防线。

口令在计算机互联网中扮演非常重要的作用。据统计,超过80%的数据泄露事件与弱口令或口令管理不当有关。理解口令安全原理、掌握口令保护技术,是每个开发者和安全从业者的必修课。

口令安全威胁

口令面临着多种攻击方式的威胁,了解这些威胁是保护口令安全的第一步。

暴力破解

暴力破解(Brute Force Attack)是最直接的攻击方式,攻击者尝试所有可能的字符组合来猜测口令。对于一个长度为 $n$、字符集大小为 $k$ 的口令,其可能的组合数为 $k^n$。

口令长度仅小写字母 ($k=26$)大小写+数字 ($k=62$)大小写+数字+符号 ($k=95$)
6 位$3.08 \times 10^8$$5.68 \times 10^{10}$$7.35 \times 10^{11}$
8 位$2.09 \times 10^{11}$$2.18 \times 10^{14}$$6.63 \times 10^{15}$
10 位$1.41 \times 10^{14}$$8.39 \times 10^{17}$$5.99 \times 10^{19}$
12 位$9.54 \times 10^{16}$$3.23 \times 10^{21}$$5.41 \times 10^{23}$

现代 GPU 可以每秒尝试数十亿次简单哈希运算,因此短口令在暴力破解面前几乎不堪一击。

字典攻击

字典攻击(Dictionary Attack)比暴力破解更高效。攻击者使用预先收集的常用密码、常见单词、人名、地名等组成的“字典“,逐一尝试。由于大多数人倾向于使用容易记忆的密码,字典攻击的成功率远高于暴力破解。

根据统计,最常用的弱密码包括:

排名密码出现频率
1123456约 1%
2password约 0.8%
312345678约 0.6%
4qwerty约 0.4%
5abc123约 0.3%
6888888约 0.2%
7111111约 0.2%
8123123约 0.15%

这些密码在字典攻击面前毫无抵抗力。

彩虹表攻击

彩虹表攻击(Rainbow Table Attack)是一种利用预计算来加速口令破解的技术。攻击者预先计算大量口令的哈希值,并存储在查找表中(彩虹表)。当获取到系统的哈希值后,只需在表中查找即可反推出原始口令。

彩虹表的核心思想是用空间换时间。设口令空间为 $N$,直接暴力破解需要 $O(N)$ 次哈希运算,而彩虹表预计算后查找只需 $O(N^{2/3})$ 的空间和 $O(N^{2/3})$ 的时间。

防御彩虹表攻击的关键手段是使用盐值(salt),即在每个口令哈希前加入随机数据,使得预计算表失效。

钓鱼攻击

钓鱼攻击(Phishing)不直接攻击口令本身,而是通过伪装成合法网站或服务,诱骗用户主动输入口令。钓鱼攻击的技术手段包括:

  • 邮件钓鱼:发送伪装成银行、社交媒体等官方通知的邮件
  • 网站克隆:搭建与目标网站高度相似的假网站
  • 中间人攻击:在用户与服务器之间截获通信
  • 短信钓鱼(Smishing):通过短信诱导用户访问钓鱼网站

键盘记录与社会工程学

键盘记录(Keylogging)通过在用户设备上安装恶意软件,记录用户的键盘输入来窃取口令。社会工程学(Social Engineering)则通过心理操纵,让用户主动透露口令信息,例如冒充IT支持人员要求用户提供密码。

如何构造强口令

口令如此重要,如何保护好自己的口令呢?首先口令本身应该足够复杂,足够抵御攻击。

密码长度、复杂度与熵

口令的安全强度可以用信息熵(Information Entropy)来量化。信息熵的计算公式为:

$$H = L \times \log_2(k)$$

其中 $L$ 为口令长度,$k$ 为字符集大小。

口令示例长度字符集熵(bit)安全等级
1234566数字(10)19.9极弱
password8小写(26)37.6
P@ssw0rd8混合(95)52.6中等
ARG2on&@!9混合(95)59.2较强
ARG2on&@!略10混合+中文80+

一般来说,熵值达到 60 bit 以上才被认为具有较好的安全性,80 bit 以上则相当安全。

密码短语(Passphrase)

密码短语(Passphrase)是由多个随机单词组成的口令,例如 “correct-horse-battery-staple”。密码短语的核心优势在于:

  • 长度更长:即使每个单词只有4-5个字母,4个单词组成的短语也有16-20个字符
  • 更容易记忆:相比随机字符串,有意义的单词组合更容易记住
  • 熵值更高:假设使用包含7776个单词的词典(类似 diceware),4个单词的熵为 $4 \times \log_2(7776) \approx 51.7$ bit;5个单词则达到约64.6 bit

密码强度评估

前面我们提到密码有强弱之分,那么该如何量化评估一个密码的强度呢?可以参考这篇博客计算密码强度《如何使用Rust编程语言来产生密码或是计算密码强度?密码又该如何安全保存呢?》。

在 Rust 中,我们可以使用 passwords 库来评估密码强度:

#![allow(unused)]
fn main() {
use passwords::{analyzer, scorer};

fn check_password_strength() {
    let pwd = "ARG2on&@!略";
    let analyzed = analyzer::analyze(pwd);
    println!("Password: {}", pwd);
    println!("Length: {} characters", analyzed.length());
    println!("Score: {}", scorer::score(&analyzed));

    // 分析密码组成
    let weak_pwd = "123456";
    let weak_analyzed = analyzer::analyze(weak_pwd);
    println!("\nWeak Password: {}", weak_pwd);
    println!("Score: {}", scorer::score(&weak_analyzed));

    let strong_pwd = "Tr0ub4dor&3xYz!@#";
    let strong_analyzed = analyzer::analyze(strong_pwd);
    println!("\nStrong Password: {}", strong_pwd);
    println!("Score: {}", scorer::score(&strong_analyzed));
}
}

passwords 库的 scorer::score() 函数会综合考虑密码长度、字符多样性、常见模式等因素,返回一个0-100的分数。分数越高,密码强度越大。

口令存储安全

绝不存储明文密码

在系统设计中,有一条铁律:永远不要存储用户的明文密码。历史上无数数据泄露事件证明,存储明文密码是极其危险的行为。即使数据库被攻破,攻击者也不应该能直接获取用户的原始密码。

哈希 + 盐值

正确的口令存储方式是使用密码哈希函数对口令进行单向变换,并附加盐值(salt)来防御彩虹表攻击。

盐值(Salt) 是一个随机生成的字符串,每个用户的盐值都不同。存储时将盐值与口令拼接后再进行哈希:

$$\text{stored} = \text{salt} \parallel \text{hash}(\text{salt} \parallel \text{password})$$

盐值的作用:

  • 使相同口令产生不同的哈希值
  • 使预计算的彩虹表失效
  • 增加攻击者需要独立破解每个口令的成本

Argon2:现代密码哈希算法

Argon2 是密码哈希竞赛(Password Hashing Competition, PHC)的获胜者,被认为是目前最安全的密码哈希算法。Argon2 有三个变体:

  • Argon2d:纯数据依赖型,抗 GPU 攻击最强,但易受侧信道攻击
  • Argon2i:纯独立型,抗侧信道攻击,但对 GPU 攻击的抵抗力稍弱
  • Argon2id:混合型,先进行一遍独立型再进行数据依赖型,兼顾两者优势

Argon2 的核心思想是内存硬(memory-hard):它需要大量内存来完成计算,这使得 GPU、ASIC 等并行计算硬件的优势被大大削弱。

#![allow(unused)]
fn main() {
fn argon2() -> Result<(), argon2::password_hash::errors::Error> {
    use argon2::{
        password_hash::{
            rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString,
        },
        Argon2,
    };
    use passwords::{analyzer, scorer};

    let pwd = "ARG2on&@!略";
    let password = pwd.as_bytes(); // Bad password; don't actually use!
    let salt = SaltString::generate(&mut OsRng);

    // Argon2 with default params (Argon2id v19)
    let argon2 = Argon2::default();

    // Hash password to PHC string ($argon2id$v=19$...)
    let password_hash = argon2.hash_password(password, &salt)?.to_string();

    // Verify password against PHC string.
    //
    // NOTE: hash params from `parsed_hash` are used instead of what is configured in the
    // `Argon2` instance.
    let parsed_hash = PasswordHash::new(&password_hash)?;
    assert!(Argon2::default()
        .verify_password(password, &parsed_hash)
        .is_ok());

    println!("{}", &password_hash);
    Ok(())
}
}

Argon2 的 PHC 字符串格式为:

$argon2id$v=19$m=19456,t=2,p=1$salt$hash

其中 m 表示内存成本(KB),t 表示时间迭代次数,p 表示并行度。

PBKDF2:基于密钥派生的方案

PBKDF2(Password-Based Key Derivation Function 2)是一种通过多次迭代哈希运算来增加破解成本的算法。PBKDF2 的核心思想是:

$$DK = \text{PBKDF2}(PRF, \text{password}, \text{salt}, c, dkLen)$$

其中 $c$ 为迭代次数,$PRF$ 为伪随机函数(通常使用 HMAC-SHA256 或 HMAC-SHA512)。迭代次数越多,攻击者每次尝试的成本越高。

#![allow(unused)]
fn main() {
fn pbkdf2() {
    use ring::digest;
    const CREDENTIAL_LEN: usize = digest::SHA512_OUTPUT_LEN;
    let n_iter = NonZeroU32::new(100_000).unwrap();
    let rng = rand::SystemRandom::new();

    let mut salt = [0u8; CREDENTIAL_LEN];
    rng.fill(&mut salt);

    let password = "Guess Me If You Can!";
    let mut pbkdf2_hash = [0u8; CREDENTIAL_LEN];
    pbkdf2::derive(
        pbkdf2::PBKDF2_HMAC_SHA512,
        n_iter,
        &salt,
        password.as_bytes(),
        &mut pbkdf2_hash,
    );
    println!("Salt: {}", HEXUPPER.encode(&salt));
    println!("PBKDF2 hash: {}", HEXUPPER.encode(&pbkdf2_hash));

    let should_succeed = pbkdf2::verify(
        pbkdf2::PBKDF2_HMAC_SHA512,
        n_iter,
        &salt,
        password.as_bytes(),
        &pbkdf2_hash,
    );
    let wrong_password = "Definitely not the correct password";
    let should_fail = pbkdf2::verify(
        pbkdf2::PBKDF2_HMAC_SHA512,
        n_iter,
        &salt,
        wrong_password.as_bytes(),
        &pbkdf2_hash,
    );

    assert!(should_succeed.is_ok());
    assert!(!should_fail.is_ok());
}
}

PBKDF2 的迭代次数建议至少 100,000 次(2026年标准),且应随硬件性能提升而定期增加。

bcrypt 与 scrypt

bcrypt 是一种专门为密码哈希设计的算法,内置盐值并使用 Blowfish 密码的变体进行多次加密。bcrypt 的特点是:

  • 内置盐值,无需单独管理
  • 自适应成本因子(cost factor),可随硬件升级调整
  • 故意设计为计算缓慢,增加暴力破解成本

scrypt 在 bcrypt 的基础上增加了内存硬特性,需要大量内存才能完成计算,有效抵御 GPU/ASIC 攻击。scrypt 的参数包括:

  • $N$(CPU/内存成本因子)
  • $r$(块大小)
  • $p$(并行因子)

密码哈希算法对比

特性Argon2bcryptscryptPBKDF2
抗暴力破解优秀良好优秀一般
抗GPU/ASIC优秀(内存硬)一般优秀(内存硬)
抗侧信道攻击良好(Argon2i)一般一般良好
可调参数内存/时间/并行度成本因子CPU/内存/并行度迭代次数
标准化PHC winner广泛使用RFC 7914RFC 2898
推荐程度首选推荐推荐可用
Rust 库argon2rust-bcryptscryptpbkdf2

推荐策略:新项目首选 Argon2id;已有项目使用 bcrypt 也是可接受的;PBKDF2 虽然广泛支持,但在抗 GPU 攻击方面不如前三者。

如何管理你的口令

生活中各种形形色色的密码(口令)数不胜数,如何管理这些密码也是一件十分重要的事情。

密码管理器

使用诸如 LastPass、1Password、Bitwarden 等专业的密码管理器能够同时兼顾密码的安全和便捷。密码管理器的核心功能包括:

  • 自动生成强密码:为每个账号生成唯一的随机强密码
  • 安全存储:使用强加密算法(如 AES-256)存储所有密码
  • 自动填充:在浏览器和移动应用中自动填写密码
  • 跨设备同步:在手机、电脑、平板之间安全同步密码库
  • 安全审计:检测重复密码、弱密码、已泄露密码

单点登录(SSO)

单点登录(Single Sign-On, SSO)允许用户使用一组凭证登录多个相关系统。常见的 SSO 协议包括:

  • OAuth 2.0:授权框架,广泛用于第三方登录(如微信、GitHub 登录)
  • SAML:企业级 SSO 标准,常用于企业内部系统
  • OpenID Connect:基于 OAuth 2.0 的身份认证层

SSO 的优势是减少用户需要记忆的密码数量,但风险在于一旦 SSO 凭证泄露,所有关联系统都将受到影响。

OTP(一次性口令)

OTP(One Time Password,一次性口令)是一种每次使用后即失效的动态密码机制,可以有效防止口令重放攻击。

HOTP:基于计数器的 OTP

HOTP(HMAC-Based One-Time Password)基于一个递增的计数器和共享密钥来生成一次性口令。其计算公式为:

$$HOTP(K, C) = \text{Truncate}(\text{HMAC-SHA1}(K, C))$$

其中 $K$ 为共享密钥,$C$ 为计数器值。Truncate 函数将 HMAC 的输出截取为指定位数(通常6位)的数字。

HOTP 的问题是客户端和服务端的计数器必须保持同步,如果客户端生成了 OTP 但未使用(计数器递增),服务端的计数器就会落后,需要通过“滑动窗口“机制来容错。

TOTP:基于时间的 OTP

TOTP(Time-Based One-Time Password)是 HOTP 的改进版本,使用时间戳代替计数器:

$$TOTP(K, T) = HOTP(K, T)$$

其中 $T = \lfloor \frac{\text{current_time} - T_0}{X} \rfloor$,$T_0$ 为起始时间(通常为 Unix 纪元),$X$ 为时间步长(通常为30秒)。

TOTP 的优势在于不需要计数器同步,只需保证客户端和服务端的时钟大致同步即可。这就是 Google Authenticator、Microsoft Authenticator 等认证器应用的工作原理。

Rust 实现 TOTP

以下是一个使用 Rust 实现 TOTP 的示例:

#![allow(unused)]
fn main() {
use hmac::{Hmac, Mac};
use sha1::Sha1;

type HmacSha1 = Hmac<Sha1>;

/// 生成 TOTP 一次性口令
///
/// # 参数
/// - `secret`: 共享密钥(Base32编码)
/// - `time_step`: 时间步长(秒),通常为30
/// - `digits`: 输出位数,通常为6
///
/// # 返回
/// 指定位数的数字字符串
fn generate_totp(secret: &[u8], time_step: u64, digits: usize) -> String {
    let time = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs();

    // 计算时间计数器
    let counter = time / time_step;

    // 将计数器转换为8字节大端序
    let counter_bytes: [u8; 8] = counter.to_be_bytes();

    // 计算 HMAC-SHA1
    let mut mac = HmacSha1::new_from_slice(secret).expect("HMAC can take key of any size");
    mac.update(&counter_bytes);
    let result = mac.finalize().into_bytes();

    // 动态截断
    let offset = (result[19] & 0x0f) as usize;
    let binary: u32 = ((result[offset] & 0x7f) as u32) << 24
        | (result[offset + 1] as u32) << 16
        | (result[offset + 2] as u32) << 8
        | (result[offset + 3] as u32);

    // 取指定位数
    let otp = binary % 10u32.pow(digits as u32);
    format!("{:0width$}", otp, width = digits)
}

#[test]
fn test_totp() {
    // 示例密钥(实际应用中应使用安全的随机密钥)
    let secret = b"12345678901234567890"; // 20字节密钥

    let code = generate_totp(secret, 30, 6);
    println!("Current TOTP code: {}", code);
    assert_eq!(code.len(), 6);
}
}

在实际生产环境中,推荐使用成熟的 crate 如 totp-rs

[dependencies]
totp-rs = "^5.0"
#![allow(unused)]
fn main() {
use totp_rs::{Algorithm, TOTP, Secret};

fn totp_with_lib() {
    // 创建 TOTP 实例
    let totp = TOTP::new(
        Algorithm::SHA1,           // 哈希算法
        6,                         // 位数
        1,                         // skew(允许的时间偏差步数)
        30,                        // 时间步长(秒)
        Secret::Encoded("12345678901234567890".to_string())
            .to_bytes()
            .unwrap(),
    ).unwrap();

    // 生成当前 TOTP 码
    let code = totp.generate_current().unwrap();
    println!("TOTP code: {}", code);

    // 验证 TOTP 码
    let valid = totp.check_current(&code).unwrap();
    println!("Valid: {}", valid);
}
}

多因素认证(MFA)简介

多因素认证(Multi-Factor Authentication, MFA)通过组合多种认证因素来提高安全性。认证因素分为三类:

  1. 知识因素(Something you know):口令、PIN码、安全问题答案
  2. 持有因素(Something you have):手机、安全令牌、智能卡
  3. 固有因素(Something you are):指纹、面部识别、虹膜扫描

MFA 的安全增益可以用概率来理解。假设口令被破解的概率为 $p_1$,OTP 被截获的概率为 $p_2$,则 MFA 被攻破的概率约为 $p_1 \times p_2$,远低于单一因素。

常见的 MFA 方案:

  • 口令 + 短信验证码
  • 口令 + TOTP(Google Authenticator)
  • 口令 + 硬件安全密钥(YubiKey、FIDO2)
  • 口令 + 生物识别(指纹、面部)

趣味阅读:历史上的暗语和化名

口令和暗语不仅是计算机时代的产物,在人类历史上,暗语和化名一直扮演着重要角色。

暗语

鸡肋
河山统一
天王盖地虎,宝塔镇河妖天王
宫廷玉液酒,一百八一杯
识时务者为俊杰
风浪越大鱼越贵
龙门飞甲,便知真假

在中国近现代史上,暗语更是被广泛使用:

  • 邱小姐:中国第一颗原子弹的代号
  • 黄河:中国第一次核试验的代号
  • 长江:中国氢弹试验的代号

这些暗语在特定的历史时期和特定的群体中发挥了保密通信的作用。

化名

李得胜
伍豪
军统六哥——郑耀先
军统店小二——许忠义

在革命战争年代,化名是保护身份安全的重要手段。毛泽东曾使用“李得胜“作为化名,周恩来使用“伍豪“(这也是“豪密“命名的由来)。在谍战领域,化名更是特工的基本操作。

拓展思考

常用的账号密码基本都是大小写英文字母、数字、特殊字符的组合,有没有可能使用简体/繁体中文或者其他字符呢?就好比图形验证码通常都使用数字、大小写英文字母,后来演变成12306选择同类图片、手动补齐图片拼图,依次点击指定的文字符号等方式?

这个问题实际上涉及字符编码和系统兼容性的深层问题。现代系统(如 UTF-8 编码的数据库和应用)理论上完全支持中文密码,但在实践中仍面临以下挑战:

  • 输入法依赖:中文输入需要输入法,在某些设备或场景下可能不便
  • 键盘布局:在非中文键盘上输入中文密码极为困难
  • 兼容性:部分老旧系统可能不支持非 ASCII 字符的密码
  • 长度计算:中文字符在 UTF-8 中占3个字节,可能影响密码长度限制的判断

不过,在 Rust 中处理 Unicode 密码是完全可行的,正如本章代码示例中的 ARG2on&@!略 就包含了中文字符。

参考链接

总结

主题关键要点推荐实践
口令威胁暴力破解、字典攻击、彩虹表、钓鱼多层防御,不依赖单一措施
口令构造长度、复杂度、熵12位以上,混合字符集,或使用密码短语
口令存储哈希+盐值,绝不存明文Argon2id > bcrypt > scrypt > PBKDF2
口令管理密码管理器、SSO使用专业密码管理器,启用MFA
动态口令HOTP/TOTP30秒步长,6位数字,结合MFA使用
多因素认证知识+持有+固有至少启用双因素认证

练习题

  1. 安全分析:假设一个攻击者使用 GPU 集群每秒可以尝试 $10^{10}$ 次 SHA-256 哈希运算。对于一个8位纯数字密码(如生日),平均需要多长时间才能破解?如果是12位混合字符密码呢?

  2. 编程实践:使用 Rust 实现一个密码强度检测器,要求:

    • 计算密码的信息熵
    • 检测是否包含常见弱密码模式(如123456、password等)
    • 检测是否包含用户名或常见单词
    • 给出密码强度评分和改进建议
  3. 算法对比:为什么 Argon2 被认为是比 PBKDF2 更安全的密码哈希算法?请从内存硬度、抗 GPU/ASIC 攻击等角度分析。

  4. TOTP 实现:基于本章的 TOTP 示例代码,实现一个完整的命令行 TOTP 认证器:

    • 支持添加/删除密钥
    • 支持显示当前所有 TOTP 码
    • 支持密钥的 Base32 编码/解码
    • 数据持久化存储(加密保存)
  5. 思考题:在密码管理器中,主密码的安全性至关重要。如果主密码被遗忘,用户将无法恢复所有存储的密码。请分析密码管理器的设计如何在安全性和可用性之间取得平衡?是否有更好的替代方案?

  6. 实战挑战:使用 Rust 实现一个简单的用户认证系统,要求:

    • 用户注册:使用 Argon2id 哈希密码并存储
    • 用户登录:验证密码并返回 JWT token
    • 密码修改:验证旧密码后更新为新密码
    • TOTP 二次验证:登录时要求输入 TOTP 码
    • 使用 SQLite 存储用户数据

第二十七 随机数

随机数

随机数概述

随机性是计算机科学中一个基础而深刻的概念。从蒙特卡洛模拟到密码学协议,从游戏引擎到机器学习,随机数无处不在。然而,计算机本质上是确定性的机器——给定相同的输入,它总是产生相同的输出。那么,计算机如何产生“随机“的数字呢?

这正是随机数生成理论要解决的核心问题。理解随机数的本质、分类和生成原理,对于编写安全、可靠的软件至关重要。一个看似微不足道的随机数生成器缺陷,可能导致加密系统被完全攻破,或导致模拟实验得出错误的结论。

随机数的三个标准

根据密码学原理,随机数的随机性检验可以分为三个标准: [1]

  1. 统计学伪随机性。统计学伪随机性指的是在给定的随机比特流样本中,1的数量大致等于0的数量,同理,“10”“01”“00”“11“四者数量大致相等。类似的标准被称为统计学随机性。满足这类要求的数字在人类“一眼看上去“是随机的。更严格地说,统计学伪随机性要求比特流能够通过一系列统计检验,包括频率检验、游程检验、频谱检验等。数学上,一个理想的随机比特流中每个比特位为1的概率为 $p = 0.5$,且各比特位之间相互独立。

  2. 密码学安全伪随机性。其定义为,给定随机样本的一部分和随机算法,不能有效的演算出随机样本的剩余部分。这意味着即使攻击者观察到了生成器输出的任意长前缀,也无法以显著优于随机猜测的概率预测下一个输出比特。形式化表述为:对于任意多项式时间算法 $A$,预测下一个比特的成功概率与 $1/2$ 的差值可忽略不计:

$$|\Pr[A(x_1, x_2, \ldots, x_n) = x_{n+1}] - \frac{1}{2}| \leq \text{negl}(n)$$

  1. 真随机性。其定义为随机样本不可重现。实际上只要给定边界条件,真随机数并不存在,可是如果产生一个真随机数样本的边界条件十分复杂且难以捕捉(比如计算机当地的本底辐射波动值),可以认为用这个方法演算出来了真随机数。真随机数的核心特征是不可预测性和不可重现性——即使完全了解生成机制,也无法在事先预测输出结果。

随机数分类

相应的,随机数也分为三类:

分类满足的标准生成方式典型用途
伪随机数(PRNG)统计学伪随机性确定性算法 + 种子模拟、游戏、测试
密码学安全伪随机数(CSPRNG)统计学 + 密码学安全伪随机性密码学算法 + 熵源密钥生成、nonce、salt
真随机数(TRNG)全部三个标准物理现象高安全场景、种子生成
  1. 伪随机数:满足第一个条件的随机数。由确定性算法生成,给定相同的种子(seed),将产生完全相同的序列。虽然不具备密码学安全性,但生成速度快、可重现,适用于不需要安全保证的场景。
  2. 密码学安全的伪随机数:同时满足前两个条件的随机数。可以通过密码学安全伪随机数生成器计算得出。即使攻击者获取了部分输出,也无法推断出之前或之后的输出。
  3. 真随机数:同时满足三个条件的随机数。来源于物理随机过程,不可预测也不可重现。

随机数在密码学中非常重要,保密通信中大量运用的会话密钥的生成即需要真随机数的参与。如果一个随机数生成算法是有缺陷的,那么会话密钥可以直接被推算出来。若果真发生这种事故,那么任何加密算法都失去了意义。

随机数分为伪随机数和真随机数。伪随机数又分为弱伪随机数和强伪随机数。

随机数生成器

随机数生成器有两种类型:真正的随机数生成器和伪随机数生成器。

随机数生成器原理

伪随机数生成器原理

伪随机数生成器(PRNG)通过确定性算法从初始种子(seed)生成看似随机的数列。其核心思想是:用一个确定性的递推公式,将当前状态映射到下一个状态,同时输出一个(经过变换的)伪随机数。

线性同余法(LCG)

线性同余法(Linear Congruential Generator)是最经典的伪随机数生成算法,由 Lehmer 于 1949 年提出。其递推公式为:

$$x_{n+1} = (a \cdot x_n + c) \mod m$$

其中:

  • $x_n$ 为当前状态(种子)
  • $a$ 为乘数(multiplier)
  • $c$ 为增量(increment)
  • $m$ 为模数(modulus)

LCG 的最大周期为 $m$。要达到最大周期,需满足 Hull-Dobell 定理:

  1. $c$ 与 $m$ 互质($\gcd(c, m) = 1$)
  2. $a - 1$ 能被 $m$ 的所有质因子整除
  3. 若 $m$ 是 4 的倍数,则 $a - 1$ 也是 4 的倍数
/// 线性同余法(LCG)的简单实现
struct Lcg {
    state: u64,
    a: u64,
    c: u64,
    m: u64,
}

impl Lcg {
    fn new(seed: u64, a: u64, c: u64, m: u64) -> Self {
        Self { state: seed, a, c, m }
    }

    fn next(&mut self) -> u64 {
        // x_{n+1} = (a * x_n + c) mod m
        self.state = (self.a.wrapping_mul(self.state).wrapping_add(self.c)) % self.m;
        self.state
    }
}

fn main() {
    // 使用经典参数:glibc 使用的参数
    let mut lcg = Lcg::new(42, 1103515245, 12345, 1 << 31);
    for _ in 0..10 {
        println!("{}", lcg.next());
    }
}

LCG 的优点是实现简单、速度快,但缺点也很明显:低位比特的随机性较差,状态空间有限容易被预测,不适合密码学用途。著名的案例是 1994 年 Netscape 浏览器使用 LCG 生成 SSL 密钥,被攻击者成功破解。

梅森旋转器(Mersenne Twister)

梅森旋转器是目前应用最广泛的通用伪随机数生成器之一,由松本真和西村拓士于 1997 年提出。其名称来源于其周期长度——梅森素数 $2^{19937}-1$。

核心特点:

  • 超长周期:$2^{19937}-1 \approx 4.3 \times 10^{6001}$,远超任何实际应用需求
  • 高维均匀分布:在高达 623 维的空间上均匀分布
  • 快速:多数平台上每生成一个 32 位随机数仅需几纳秒
  • 通过多数统计测试:Diehard 和大部分 NIST 测试
// 注意:rand 0.9+ 的 StdRng 实际使用 ChaCha8 算法,而非梅森旋转器
// 如需使用梅森旋转器,需要额外的 rand_mt crate
use rand::RngExt;

fn main() {
    // rand::rng() 默认使用 ThreadRng,内部基于 ChaCha8
    let mut rng = rand::rngs::StdRng::from_seed([42u8; 32]);
    for _ in 0..5 {
        println!("{}", rng.random::<u64>());
    }
}

PCG / Xoshiro 算法

近年来,PCG(Permuted Congruential Generator)和 Xoshiro 系列算法因其出色的统计质量和性能受到广泛关注。

PCG 由 M.E. O’Neill 于 2014 年提出,基于 LCG 但增加了输出置换(output permutation),显著改善了统计特性:

$$x_{n+1} = (a \cdot x_n + c) \mod 2^n$$ $$\text{output} = \text{rotate}(x_{n+1} \oplus (x_{n+1} >> r), x_{n+1} >> s)$$

Xoshiro 系列由 Sebastiano Vigna 提出,基于 XOR-shift/rotate 操作,速度极快且统计质量优秀。

use rand::RngExt;

fn main() {
    // 使用 StdRng(ChaCha8 算法)
    let mut rng = rand::rngs::StdRng::seed_from_u64(42);
    println!("StdRng (ChaCha8): {}", rng.random::<u64>());

    // 使用 SmallRng(平台优化的快速生成器)
    let mut rng = rand::rngs::SmallRng::seed_from_u64(42);
    println!("SmallRng: {}", rng.random::<u64>());
}

算法对比

算法周期状态大小速度统计质量密码学安全适用场景
LCG$\leq m$(通常 $2^{32}$)4-8 字节极快简单模拟、教学
Mersenne Twister$2^{19937}-1$2500 字节通用模拟、游戏
SmallRng平台相关16-32 字节极快优秀通用高性能场景
StdRng (ChaCha8)$2^{64}$32 字节优秀密码学用途
ChaCha20$2^{64}$32 字节优秀高安全密码学用途

真随机数来源

真随机数生成器(TRNG)利用物理世界的不可预测现象来产生随机数,不依赖确定性算法。

硬件随机数生成器(HRNG)

现代 CPU 和专用硬件通常内置了随机数生成器:

  • Intel RDRAND/RDSEED 指令:利用芯片内部的热噪声生成随机数,自 2012 年(Ivy Bridge)起可用
  • AMD RDRAND 指令:类似 Intel 的实现
  • ARM RNDR 指令:ARMv8.5+ 引入的随机数指令
// ⚠️ 需要本地编译,不支持 Playground
// 使用 rdrand crate 访问硬件随机数生成器
// Cargo.toml: rdrand = "0.8"
// 注意:rdrand 依赖 CPU 硬件指令(Intel RDRAND/RDSEED),Playground 无法运行
use rand_core::TryRng;
use rdrand::RdRand;

fn main() {
    if let Ok(mut rng) = RdRand::new() {
        if let Ok(val) = rng.try_next_u64() {
            println!("Hardware random: {}", val);
        }
    }
}

操作系统熵源

操作系统通过收集各种系统事件的时序信息来积累熵,提供高质量的随机数接口:

平台接口说明
Linux / macOS/dev/urandom非阻塞 CSPRNG,推荐使用
Linux / macOS/dev/random早期为阻塞接口,现代内核已与 urandom 等价
WindowsCryptGenRandom / BCryptGenRandomWindows CryptoAPI 提供的 CSPRNG
WindowsProcessPrngWindows 10+ 推荐接口
// 使用 rand::rng() 获取密码学安全的线程本地 RNG
// 在 rand 0.9 中,ThreadRng 使用 ChaCha12 算法,具有密码学安全性
use rand::RngExt;


fn main() {
    let mut rng = rand::rng();
    let mut buf = [0u8; 32];
    rng.fill(&mut buf);
    println!("OS random bytes: {:02x?}", buf);
    
    // 也可以直接生成单个值
    let random_u64: u64 = rng.random();
    println!("Random u64: {}", random_u64);
}

物理现象

真随机数的物理来源包括:

  • 热噪声(Johnson-Nyquist 噪声):电阻中电子的热运动产生的电压波动,服从高斯分布
  • 放射性衰变:原子核衰变时刻的量子随机性
  • 光电效应:光子到达探测器的随机时间
  • 量子力学现象:量子叠加态的坍缩本质上是随机的

这些物理现象的共同特点是:基于量子力学的不确定性原理,其结果在原理上不可预测。

Rust 中的随机数

Rust 生态中随机数的核心 crate 是 rand,它提供了丰富的随机数生成功能。rand 0.9+ 版本进行了重大重构,API 更加现代化。

rand crate 基础用法

// ✅ 正确方式
use rand::RngExt;

fn main() {
    let x: u8 = rand::random();
    println!("{}", x);
}
// import commonly used items from the prelude:
use rand::RngExt;
use rand::seq::IteratorRandom;
use rand::prelude::SliceRandom;

fn main() {
    // We can use random() immediately. It can produce values of many common types:
    let x: u8 = rand::random();
    println!("{}", x);

    if rand::random() { // generates a boolean
        println!("Heads!");
    }

    // If we want to be a bit more explicit (and a little more efficient) we can
    // make a handle to the thread-local generator:
    let mut rng = rand::rng();
    if rng.random() { // random bool
        let x: f64 = rng.random(); // random number in range [0, 1)
        let y = rng.random_range(-10.0..10.0);
        println!("x is: {}", x);
        println!("y is: {}", y);
    }

    println!("Dice roll: {}", rng.random_range(1..=6));
    println!("Number from 0 to 9: {}", rng.random_range(0..10));
    
    // Sometimes it's useful to use distributions directly:
    let distr = rand::distr::Uniform::new_inclusive(1, 100).unwrap();
    let mut nums = [0i32; 3];
    for x in &mut nums {
        *x = rng.sample(distr);
    }
    println!("Some numbers: {:?}", nums);

    // We can also interact with iterators and slices:
    let arrows_iter = "➡⬈⬆⬉⬅⬋⬇⬊".chars();
    println!("Lets go in this direction: {}", arrows_iter.choose(&mut rng).unwrap());
    let mut nums = [1, 2, 3, 4, 5];
    nums.shuffle(&mut rng);
    println!("I shuffled my {:?}", nums);
}
#![allow(unused)]
fn main() {
// 以下为代码片段,非完整程序
    //生成随机字节数组
    use rand::RngExt;
    let mut rng = rand::rng();
    let mut block: [u8; 16] = [0; 16];
    rng.fill(&mut block);
}

ThreadRng 与线程本地生成器

rand::rng() 返回一个线程本地的随机数生成器(ThreadRng),它是懒初始化的,每个线程拥有独立的实例,无需加锁,性能优异。

use rand::RngExt;

fn main() {
    let mut rng = rand::rng();

    // 生成各种基本类型
    let a: u8 = rng.random();
    let b: u16 = rng.random();
    let c: u32 = rng.random();
    let d: u64 = rng.random();
    let e: f32 = rng.random(); // [0, 1)
    let f: f64 = rng.random(); // [0, 1)
    let g: bool = rng.random();
    let h: char = rng.random(); // 随机 Unicode 字符

    println!("u8={}, u16={}, u32={}, u64={}", a, b, c, d);
    println!("f32={}, f64={}, bool={}, char={}", e, f, g, h);
}

各种概率分布

rand::distr 模块提供了丰富的概率分布:

// ⚠️ 需要 rand_distr 依赖
// ⚠️ Normal、Exp、LogNormal、Pareto 已从 rand 0.9 移至 rand_distr crate
// Playground 中需要添加依赖:rand_distr = "0.5"
use rand::RngExt;
use rand::distr::{Uniform, Bernoulli};
use rand_distr::{Normal, Exp, LogNormal, Pareto};

fn main() {
    let mut rng = rand::rng();

    // 均匀分布 [a, b]
    let uniform = Uniform::new(1.0, 100.0).unwrap();
    println!("Uniform: {}", rng.sample(uniform));

    // 正态分布 N(μ, σ²),均值为0,标准差为1
    let normal = Normal::new(0.0, 1.0).unwrap();
    println!("Normal: {}", rng.sample(normal));

    // 伯努利分布:以概率 p 返回 true
    let bernoulli = Bernoulli::new(0.7).unwrap();
    println!("Bernoulli: {}", rng.sample(bernoulli));

    // 指数分布
    let exp = Exp::new(2.0).unwrap();
    println!("Exponential: {}", rng.sample(exp));

    // 对数正态分布
    let log_normal = LogNormal::new(0.0, 1.0).unwrap();
    println!("LogNormal: {}", rng.sample(log_normal));

    // 帕累托分布
    let pareto = Pareto::new(1.0, 2.0).unwrap();
    println!("Pareto: {}", rng.sample(pareto));
}

随机选择与洗牌

use rand::seq::{SliceRandom, IteratorRandom};
use rand::RngExt;
use rand::prelude::IndexedRandom;


fn main() {
    let mut rng = rand::rng();

    // 从切片中随机选择一个元素
    let colors = ["red", "green", "blue", "yellow"];
    let chosen = colors.choose(&mut rng).unwrap();
    println!("Chosen color: {}", chosen);

    // 随机选择多个不重复元素
    let chosen_multiple = colors.choose_multiple(&mut rng, 2);
    println!("Chosen 2 colors: {:?}", chosen_multiple);

    // 从迭代器中随机选择
    let chosen_from_iter = (1..=100).choose(&mut rng);
    println!("Random number from 1..=100: {:?}", chosen_from_iter);

    // 洗牌(Fisher-Yates 算法)
    let mut deck = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    deck.shuffle(&mut rng);
    println!("Shuffled deck: {:?}", deck);

    // 部分洗牌:只打乱前 k 个位置
    let mut cards = vec!["A", "B", "C", "D", "E"];
    cards.partial_shuffle(&mut rng, 3);
    println!("Top 3 random cards: {:?}", &cards[..3]);
}

自定义分布

通过实现 Distribution trait,可以创建自定义的概率分布:

use rand::distr::{Distribution, StandardUniform};
use rand::RngExt;

/// 自定义分布:掷两个骰子之和
struct DiceSum;

impl Distribution<u8> for DiceSum {
    fn sample<R: RngExt + ?Sized>(&self, rng: &mut R) -> u8 {
        let d1: u8 = rng.random_range(1..=6);
        let d2: u8 = rng.random_range(1..=6);
        d1 + d2
    }
}

/// 为自定义类型实现随机生成
#[derive(Debug)]
enum Weather {
    Sunny,
    Cloudy,
    Rainy,
    Stormy,
}

impl Distribution<Weather> for StandardUniform {
    fn sample<R: RngExt + ?Sized>(&self, rng: &mut R) -> Weather {
        let val: f64 = rng.random();
        match val {
            v if v < 0.4 => Weather::Sunny,
            v if v < 0.7 => Weather::Cloudy,
            v if v < 0.9 => Weather::Rainy,
            _ => Weather::Stormy,
        }
    }
}

fn main() {
    let mut rng = rand::rng();

    // 使用自定义 DiceSum 分布
    let dice = DiceSum;
    for _ in 0..10 {
        println!("Dice sum: {}", rng.sample(&dice));
    }

    // 使用自定义 Weather 分布
    for _ in 0..5 {
        println!("Weather: {:?}", rng.random::<Weather>());
    }
}

rand_core 和 OsRng

rand_corerand 生态的基础抽象层,定义了核心 trait:

  • RngExt:用户层 trait,提供 random()random_range()fill()sample() 等便捷方法
  • SeedableRng:可从种子初始化的生成器 trait
  • CryptoRng:标记 trait,表示生成器具有密码学安全性
use rand::{RngExt, CryptoRng};

fn generate_with_rng<R: RngExt + CryptoRng>(rng: &mut R) -> [u8; 32] {
    let mut key = [0u8; 32];
    rng.fill(&mut key);
    key
}

fn main() {
    // rand::rng() 返回的 ThreadRng 同时实现了 RngExt 和 CryptoRng
    let mut rng = rand::rng();
    let key = generate_with_rng(&mut rng);
    println!("Crypto-safe key: {:02x?}", key);
}

getrandom crate

getrandom crate 提供了跨平台的系统随机数访问,是 rand 的底层依赖,也可以单独使用:

// ⚠️ getrandom 是独立 crate,Playground 中需要添加依赖
// Cargo.toml: getrandom = "0.3"
use getrandom::getrandom;

fn main() {
    let mut buf = [0u8; 16];
    getrandom(&mut buf).expect("failed to get random bytes");
    println!("Random bytes: {:02x?}", buf);
}

getrandom 在不同平台上自动选择最佳熵源:

平台熵源
Linuxgetrandom() 系统调用
macOSgetentropy()
WindowsBCryptGenRandom
WebAssemblyCrypto.getRandomValues()
嵌入式可自定义实现

随机数在密码学中的应用

随机数在密码学中的用途主要有:生成nonce、生成salt、生成初始化向量、生成密钥(对称密钥或非对称密钥)。

  • 随机数参与加密报文的签名;
  • 随机数参与会话秘钥的生成;

生成 nonce、salt、IV、密钥

use rand::RngExt;

fn generate_crypto_params() {
    let mut rng = rand::rng();

    // 生成 12 字节 nonce(用于 AES-GCM)
    let mut nonce = [0u8; 12];
    rng.fill(&mut nonce);
    println!("Nonce: {:02x?}", nonce);

    // 生成 16 字节 salt(用于密钥派生)
    let mut salt = [0u8; 16];
    rng.fill(&mut salt);
    println!("Salt: {:02x?}", salt);

    // 生成 16 字节 IV(初始化向量,用于 AES-CBC)
    let mut iv = [0u8; 16];
    rng.fill(&mut iv);
    println!("IV: {:02x?}", iv);

    // 生成 32 字节对称密钥(AES-256)
    let mut key = [0u8; 32];
    rng.fill(&mut key);
    println!("AES-256 Key: {:02x?}", key);
}

fn main() {
    generate_crypto_params();
}

使用 openssl 命令生成随机数

openssl rand -hex 32
openssl rand –base64 32
openssl rand –base64 32 –out myr.dat

密钥生成的安全要求

密钥生成对随机数的质量有严格要求:

  1. 最小熵要求:128 位密钥至少需要 128 位熵。使用弱随机数生成器会导致密钥空间大幅缩小,使暴力破解成为可能。
  2. 不可预测性:密钥必须使用 CSPRNG 生成,绝不能使用普通 PRNG(如 LCG、Mersenne Twister)。
  3. 种子安全:CSPRNG 的种子本身必须来自真随机源(操作系统熵池或硬件 TRNG)。
  4. 避免重用:nonce 和 IV 必须保证不重复使用,否则会严重削弱加密安全性。
use rand::RngExt;

/// 安全地生成 RSA 密钥对所需的随机素数种子
fn generate_rsa_seed() -> [u8; 64] {
    let mut seed = [0u8; 64];
    rand::rng().fill(&mut seed);
    seed
}

/// 生成一次性密码(OTP)的密钥
fn generate_otp_key() -> [u8; 20] {
    let mut key = [0u8; 20]; // HOTP/TOTP 使用 20 字节(160 位)
    rand::rng().fill(&mut key);
    key
}

fn main() {
    let rsa_seed = generate_rsa_seed();
    println!("RSA seed: {:02x?}", rsa_seed);

    let otp_key = generate_otp_key();
    println!("OTP key: {:02x?}", otp_key);
}

一次性密码(One Time Password,简称OTP)

随机数测试

如何验证一个随机数生成器是否“足够随机“?这需要借助统计测试套件。

统计测试套件

常见的随机数测试套件包括:

测试套件开发者测试数量说明
DiehardMarsaglia15经典测试集,已过时
DieharderBrown100+Diehard 的扩展版
TestU01L’Ecuyer10+包含 Big Crush、Small Crush
NIST SP 800-22NIST16密码学标准测试
PractRandSibidanov持续运行最严格的测试之一

NIST SP 800-22 测试

NIST SP 800-22 是美国国家标准与技术研究院发布的随机数测试标准,包含 16 项测试:

  1. 频率(Frequency)测试:检验整个比特流中 0 和 1 的比例是否接近 0.5
  2. 块内频率(Block Frequency)测试:在 M 位块内检验频率
  3. 游程(Runs)测试:检验连续相同比特(游程)的数量分布
  4. 最长游程(Longest Run)测试:在一个块内检验最长游程
  5. 二元矩阵秩(Binary Matrix Rank)测试:检验固定大小矩阵的秩
  6. 离散傅里叶变换(FFT)测试:检测周期性模式
  7. 非重叠模板匹配(Non-overlapping Template)测试:检测特定比特模式的出现频率
  8. 重叠模板匹配(Overlapping Template)测试:类似但模板可重叠
  9. 通用统计(Universal Statistical)测试:基于 Maurer 的通用统计
  10. Lempel-Ziv 压缩(Linear Complexity)测试:检验线性复杂度
  11. 序列(Serial)测试:检验 $m$-bit 模式的频率
  12. 近似熵(Approximate Entropy)测试:评估序列的不可预测性
  13. 累积和(Cumulative Sums)测试:检测部分序列中 0 和 1 的偏向
  14. 随机偏移(Random Excursions)测试:检验随机游走特性
  15. 随机偏移变体(Random Excursions Variant)测试:随机游走的变体
  16. Maurer 通用统计(Maurer’s Universal)测试:评估信息存储能力
/// 简单的频率测试示例
fn frequency_test(bits: &[u8]) -> f64 {
    let total_bits = bits.len() * 8;
    let ones: usize = bits.iter().map(|b| b.count_ones() as usize).sum();
    let s = (ones as f64 - total_bits as f64 / 2.0) / (total_bits as f64 / 2.0).sqrt();
    // 使用误差函数计算 p-value
    let p_value = erf_complement(s / 2.0_f64.sqrt());
    p_value
}

fn erf_complement(x: f64) -> f64 {
    // 简化的互补误差函数近似
    let t = 1.0 / (1.0 + 0.3275911 * x.abs());
    let poly = t * (0.254829592 + t * (-0.284496736 + t * (1.421413741
        + t * (-1.453152027 + t * 1.061405429))));
    let result = 1.0 - poly * (-x * x).exp();
    if x >= 0.0 { result } else { 2.0 - result }
}

fn main() {
    use rand::RngExt;
    let mut rng = rand::rng();
    let mut bits = [0u8; 256];
    rng.fill(&mut bits);
    let p = frequency_test(&bits);
    println!("Frequency test p-value: {}", p);
    if p > 0.01 {
        println!("PASS: 序列通过了频率测试");
    } else {
        println!("FAIL: 序列未通过频率测试");
    }
}

随机数用途

随机数模拟现实中的场景,比如抽奖、掷骰子、游戏中随机关卡、电影特效等。

非密码学用途

use rand::RngExt;
use rand::seq::SliceRandom;
use rand::prelude::IndexedRandom;

fn main() {
    let mut rng = rand::rng();

    // 1. 模拟掷骰子
    let dice_roll = rng.random_range(1..=6);
    println!("骰子点数: {}", dice_roll);

    // 2. 抽奖系统
    let participants = ["Alice", "Bob", "Charlie", "David", "Eve"];
    let winner = participants.choose(&mut rng).unwrap();
    println!("中奖者: {}", winner);

    // 3. 生成随机密码(非安全用途)
    let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        .chars().collect();
    let password: String = (0..12).map(|_| chars.choose(&mut rng).unwrap()).collect();
    println!("随机密码: {}", password);

    // 4. 蒙特卡洛方法估算 π
    let n = 1_000_000;
    let inside: usize = (0..n)
        .filter(|_| {
            let x: f64 = rng.random();
            let y: f64 = rng.random();
            x * x + y * y <= 1.0
        })
        .count();
    let pi = 4.0 * inside as f64 / n as f64;
    println!("π 的估算值: {} (误差: {:.6})", pi, (pi - std::f64::consts::PI).abs());
}

密码学用途

use rand::RngExt;

fn main() {
    let mut rng = rand::rng();

    // 1. 生成会话密钥
    let mut session_key = [0u8; 32];
    rng.fill(&mut session_key);
    println!("会话密钥: {:02x?}", &session_key[..8]);

    // 2. 生成 TLS 随机数(模拟)
    let mut client_random = [0u8; 32];
    rng.fill(&mut client_random);
    println!("Client Random: {:02x?}", &client_random[..8]);

    // 3. 生成密码学安全的随机密码
    use rand::distr::{Alphanumeric, Distribution};
    let password: String = (0..24)
        .map(|_| rng.sample(Alphanumeric) as char)
        .collect();
    println!("安全随机密码: {}", password);
}

总结

本章全面介绍了随机数的理论基础和 Rust 实践。下表对核心知识点进行总结:

主题要点
随机数标准统计学伪随机性、密码学安全伪随机性、真随机性
PRNG 算法LCG(简单但弱)、MT(通用)、PCG/Xoshiro(现代高性能)
TRNG 来源硬件 RDRAND、操作系统熵源、物理现象(热噪声等)
rand craterand::random()rand::rng()、ThreadRng、各种分布
分布类型Uniform、Normal、Bernoulli、Exp、LogNormal、Pareto
集合操作choose()choose_multiple()shuffle()partial_shuffle()
密码学安全OsRngCryptoRng trait、getrandom crate
密码学用途nonce、salt、IV、密钥生成,必须使用 CSPRNG
随机数测试NIST SP 800-22(16 项测试)、Dieharder、TestU01
安全原则密钥生成必须用 CSPRNG、nonce 不可重用、种子需真随机

《The Rust Rand Book》

练习题

练习 1:实现简单的 LCG 并验证周期

实现一个 LCG 生成器,使用参数 $a = 1664525$、$c = 1013904223$、$m = 2^{32}$(Numerical Recipes 推荐参数),验证从种子 1 开始,序列的周期是否为 $2^{32}$。

// 提示:使用 HashSet 检测重复值
use std::collections::HashSet;

struct Lcg {
    state: u32,
}

impl Lcg {
    fn new(seed: u32) -> Self {
        Self { state: seed }
    }

    fn next(&mut self) -> u32 {
        self.state = self.state.wrapping_mul(1664525).wrapping_add(1013904223);
        self.state
    }
}

fn main() {
    let mut lcg = Lcg::new(1);
    let mut seen = HashSet::new();
    let mut count = 0u64;

    loop {
        let val = lcg.next();
        if !seen.insert(val) {
            println!("周期长度: {}", count);
            break;
        }
        count += 1;
    }
}

练习 2:蒙特卡洛积分

使用蒙特卡洛方法计算以下定积分的近似值:

$$\int_0^1 \sin(x) , dx = 1 - \cos(1) \approx 0.4597$$

use rand::RngExt;

fn main() {
    let mut rng = rand::rng();
    let n = 1_000_000;
    let mut sum = 0.0;

    for _ in 0..n {
        let x: f64 = rng.random(); // [0, 1)
        sum += x.sin();
    }

    let result = sum / n as f64;
    let exact = 1.0 - 1.0_f64.cos();
    println!("蒙特卡洛结果: {:.6}", result);
    println!("精确值: {:.6}", exact);
    println!("误差: {:.6}", (result - exact).abs());
}

练习 3:密码学安全随机密码生成器

编写一个函数,生成包含大写字母、小写字母、数字和特殊字符的密码,确保每种字符至少出现一次,且使用密码学安全的随机源。

use rand::seq::SliceRandom;
use rand::RngExt;
use rand::prelude::IndexedRandom;


fn generate_secure_password(length: usize) -> String {
    let mut rng = rand::rng();
    let uppercase: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars().collect();
    let lowercase: Vec<char> = "abcdefghijklmnopqrstuvwxyz".chars().collect();
    let digits: Vec<char> = "0123456789".chars().collect();
    let special: Vec<char> = "!@#$%^&*()_+-=[]{}|;:,.<>?".chars().collect();

    let mut password: Vec<char> = Vec::with_capacity(length);

    // 确保每类字符至少一个
    password.push(*uppercase.choose(&mut rng).unwrap());
    password.push(*lowercase.choose(&mut rng).unwrap());
    password.push(*digits.choose(&mut rng).unwrap());
    password.push(*special.choose(&mut rng).unwrap());

    // 填充剩余字符
    let all_chars: Vec<char> = uppercase.iter()
        .chain(lowercase.iter())
        .chain(digits.iter())
        .chain(special.iter())
        .copied()
        .collect();

    while password.len() < length {
        password.push(*all_chars.choose(&mut rng).unwrap());
    }

    // 洗牌打乱顺序
    password.shuffle(&mut rng);
    password.into_iter().collect()
}

fn main() {
    let password = generate_secure_password(20);
    println!("安全密码: {}", password);
    println!("长度: {}", password.len());
}

练习 4:随机数质量可视化检验

编写程序生成 10,000 个随机点 $(x, y)$,其中 $x, y \in [0, 1)$,将结果输出为 CSV 格式,然后用散点图可视化检验分布的均匀性。

use rand::RngExt;
use std::io::Write;

fn main() {
    let mut rng = rand::rng();
    let mut file = std::fs::File::create("random_points.csv")
        .expect("无法创建文件");

    writeln!(file, "x,y").unwrap();
    for _ in 0..10_000 {
        let x: f64 = rng.random();
        let y: f64 = rng.random();
        writeln!(file, "{:.6},{:.6}", x, y).unwrap();
    }

    println!("已生成 random_points.csv,包含 10000 个随机点");
    println!("可使用 Python matplotlib 或其他工具绘制散点图验证均匀性");
}

第二十八 密钥

密钥

** 现代密码学的一个基本原则是“一切秘密寓于密钥之中”。** 密钥分为两种:对称密钥与非对称密钥

对称密码算法使用相同的密钥; 公钥密码使用不同的密钥来对消息进行加密解密; MAC消息认证码使用相同的密钥; 数字签名使用不同的密钥来对消息进行签名和验证。

密钥的管理

  • 如何生成公私钥对?
  • 如何根据编码后的公私钥对得到 PrivateKey, PublicKey ?
  • 如何保护私钥的安全?
  • 如何传输交换密钥?
  • 秘密共享 (Secret Sharing)

如何生成公私钥对?

RSA生成公私钥对

#![allow(unused)]
fn main() {
    use rand_core::CryptoRngCore;
    use rsa::pkcs8::{self, DecodePublicKey, EncodePrivateKey, EncodePublicKey, LineEnding};
    use rsa::{Pkcs1v15Encrypt, PublicKey, RsaPrivateKey, RsaPublicKey};

    let mut rng = random::thread_rng();
    let bits = 2048;
    //1. 生成RSA公私钥
    let priv_key = RsaPrivateKey::new(&mut rng, bits).expect("failed to generate a key");
    let pub_key = RsaPublicKey::from(&priv_key);

    //2. 序列化公私钥
    if let Ok(priv_pem) = RsaPrivateKey::to_pkcs8_pem(&priv_key, pkcs8::LineEnding::default()){
        std::fs::write("private_rsa_key.pem",priv_pem);
    }

    if let Ok(pub_pem) = RsaPublicKey::to_public_key_pem(&pub_key, pkcs8::LineEnding::default()){
        std::fs::write("public_rsa_key.pem",pub_pem);
    }
}

ECC生成公私钥对

#![allow(unused)]
fn main() {
#[test]
fn ecc() {
    use base64ct::LineEnding;
    use elliptic_curve::SecretKey;
    use p256::NistP256;
    use rand_core::OsRng; // requires 'getrandom' feature

    // 1. 生成私钥
    let key: SecretKey<NistP256> = SecretKey::random(&mut OsRng);
    let pem_str = key.to_sec1_pem(LineEnding::LF);
    println!("{:?}", pem_str);
    // 根据私钥生成公钥
    println!("{:?}", key.public_key());

    //2. 根据PEM格式字符串恢复私钥
    let private_key : Result<SecretKey<NistP256>, elliptic_curve::Error> = elliptic_curve::SecretKey::from_sec1_pem("-----BEGIN EC PRIVATE KEY-----\nMGsCAQEEIKUhSRir8XkO1BcqcgdgFxtapjz0UFyzwQQpcCJ6IZhcoUQDQgAEWsVX\ns/5B/A4rWT4hRk6EBP/tzRzQjJKZIoh9WQbV4eots1mPwiio6XYhcYD+zauInLNq\nK1i6dCgXjJ0GB1FEhA==\n-----END EC PRIVATE KEY-----");

    //3. 根据私钥生成公钥
    let public_key = private_key.unwrap().public_key();
    println!("{:?}", public_key);
}

}

SM2生成公私钥对

#![allow(unused)]
fn main() {
use libsm::sm2::signature::{SigCtx, Signature};

    let ctx = SigCtx::new();
    let (pk, sk) = ctx.new_keypair().unwrap();
}

如何根据编码后的公私钥对得到 PrivateKey, PublicKey ?

#![allow(unused)]
fn main() {
    let private_key_pem = std::fs::read_to_string("private_key.pem").unwrap();
    let public_key_pem = std::fs::read_to_string("public_key.pem").unwrap();

    let priv_key = RsaPrivateKey::from_pkcs8_pem(&private_key_pem).unwrap();
    let pub_key = RsaPublicKey::from_public_key_pem(&public_key_pem).unwrap();
}

SM2

(1)获得openssl支持椭圆曲线算法列表
        命令:openssl ecparam -list_curves
(2)生成SM2私钥文件
          命令:openssl ecparam -outform pem -out sm2PrivateKey.pem -name SM2 -genkey
(3)查看私钥文件
          命令:openssl ec -in sm2PrivateKey.pem -text
(4)生成SM2 公钥文件
          命令:openssl ec -in sm2PrivateKey.pem -pubout -out sm2PublicKey.pem

密钥交换

#![allow(unused)]
fn main() {
use k256::{EncodedPoint, PublicKey, ecdh::EphemeralSecret};
use rand_core::OsRng; // requires 'getrandom' feature

// Alice
let alice_secret = EphemeralSecret::random(&mut OsRng);
let alice_pk_bytes = EncodedPoint::from(alice_secret.public_key());

// Bob
let bob_secret = EphemeralSecret::random(&mut OsRng);
let bob_pk_bytes = EncodedPoint::from(bob_secret.public_key());

// Alice decodes Bob's serialized public key and computes a shared secret from it
let bob_public = PublicKey::from_sec1_bytes(bob_pk_bytes.as_ref())
    .expect("bob's public key is invalid!"); // In real usage, don't panic, handle this!

let alice_shared = alice_secret.diffie_hellman(&bob_public);

// Bob decodes Alice's serialized public key and computes the same shared secret
let alice_public = PublicKey::from_sec1_bytes(alice_pk_bytes.as_ref())
    .expect("alice's public key is invalid!"); // In real usage, don't panic, handle this!

let bob_shared = bob_secret.diffie_hellman(&alice_public);

// Both participants arrive on the same shared secret
assert_eq!(alice_shared.raw_secret_bytes(), bob_shared.raw_secret_bytes());
}

密钥交换流程图

sequenceDiagram
    participant A as Alice
    participant B as Bob

    Note over A,B: 1. 各自生成临时密钥对
    A->>A: 生成 EphemeralSecret (alice_secret)
    A->>A: 从 secret 派生公钥并编码为 EncodedPoint (alice_pk_bytes)
    
    B->>B: 生成 EphemeralSecret (bob_secret)
    B->>B: 从 secret 派生公钥并编码为 EncodedPoint (bob_pk_bytes)

    Note over A,B: 2. 交换公钥 (通过不安全的信道)
    A-->>B: 发送 alice_pk_bytes
    B-->>A: 发送 bob_pk_bytes

    Note over A,B: 3. 解码对方公钥并计算共享密钥
    A->>A: 解码 bob_pk_bytes → bob_public
    A->>A: alice_secret.diffie_hellman(&bob_public) → alice_shared

    B->>B: 解码 alice_pk_bytes → alice_public
    B->>B: bob_secret.diffie_hellman(&alice_public) → bob_shared

    Note over A,B: 4. 验证共享密钥一致
    A->>A: alice_shared.raw_secret_bytes()
    B->>B: bob_shared.raw_secret_bytes()
    
    Note over A,B: ✅ assert_eq! 两者相等

ECDH 算法概述(CNG 示例)

Elliptic Curve Diffie-Hellman (ECDH) function

密钥交换算法

图解 ECDHE 密钥交换算法
离散对数,DH算法, curve25519

x25519-dalek X25519 elliptic curve Diffie-Hellman key exchange in pure-Rust, using curve25519-dalek.

秘密共享 (Secret Sharing)

秘密共享算法

Shamir 秘密共享方案的核心原理(用数学的“穿针引线”来理解)

拉格朗日插值法 (Lagrange Interpolation)

密钥管理系统(Key Management Service,KMS)

HashiCorp Vault官网 HashiCorp Vault Github RustyVault

HashiCorp Vault

#![allow(unused)]
fn main() {
async fn vault() {
    use vaultrs::client::{Client, VaultClient, VaultClientSettingsBuilder};

    use rand_core::CryptoRngCore;
    use rand_core::OsRng;
    use rsa::pkcs8::{
        self, DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey, LineEnding,
    };
    use rsa::{Pkcs1v15Encrypt, PublicKey, RsaPrivateKey, RsaPublicKey};
    use serde::{Deserialize, Serialize};
    use vaultrs::kv2;
    // let mut rng = random::thread_rng();

    // Create and read secrets
    #[derive(Debug, Deserialize, Serialize)]
    struct MySecret {
        key: String,
        password: String,
    }

    #[derive(Debug, Deserialize, Serialize)]
    struct RSAKey {
        public_key: String,
        private_key: String,
    }

    // Create a client
    let mut client = VaultClient::new(
        VaultClientSettingsBuilder::default()
            .address("http://127.0.0.1:8200")
            .token("hvs.80mEiiPnonTChoSTH5pkAeLg")
            .build()
            .unwrap(),
    )
    .unwrap();

    let secret = MySecret {
        key: "super".to_string(),
        password: "secret".to_string(),
    };
    kv2::set(&client, "secret", "mysecret", &secret).await;

    // let secret: MySecret = ;
    let result: Result<MySecret, vaultrs::error::ClientError> =
        kv2::read(&client, "secret", "mysecret").await;
    if (result.is_ok()) {
        println!("{}", result.unwrap().password) // "secret"
    } else {
        println!("{:?}", result.err()) // "secret"
    }

    let mut public_key = String::new();
    let mut private_key = String::new();

    let bits = 2048;
    let mut rng = rand::thread_rng();
    //1. 生成RSA公私钥
    let priv_key = RsaPrivateKey::new(&mut rng, bits).expect("failed to generate a key");
    let pub_key = RsaPublicKey::from(&priv_key);

    //2. 序列化公私钥
    if let Ok(priv_pem) = RsaPrivateKey::to_pkcs8_pem(&priv_key, pkcs8::LineEnding::default()) {
        private_key = priv_pem.to_string();
    }

    if let Ok(pub_pem) = RsaPublicKey::to_public_key_pem(&pub_key, pkcs8::LineEnding::default()) {
        public_key = pub_pem.to_string();
    }

    let secret1 = RSAKey {
        public_key,
        private_key,
    };

    // let mut rng = rand_core::OsRng;
    let key = String::from("RSA:666666666666");

    kv2::set(&client, "secret", &key, &secret1).await;

    let result: Result<RSAKey, vaultrs::error::ClientError> =
        kv2::read(&client, "secret", &key).await;
    if (result.is_ok()) {
        println!("{}", result.unwrap().public_key)
    } else {
        println!("{:?}", result.err())
    }
}


}

第二十九 对称密码算法

对称密码算法可以分为序列密码和分组密码。

序列密码(流密码)

流密码(Stream Cipher) 目前,公开的序列密码算法主要有RC4、SEAL等。常见的使用流密码的加密协议有 RC4 ,Salsa20 ,和 ChaCha 等。祖冲之ZUC 序列密码也称为流密码(Stream Cipher),它是对称密码算法的一种。

ChaCha20-Poly1305 XChaCha20Poly1305

ChaCha20-Poly1305是Google所采用的一种新式加密算法,性能强大.

#![allow(unused)]
fn main() {
    #[test]
    fn chacha20poly1305() {
        use chacha20poly1305::aead::{Aead, NewAead};
        use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
        use data_encoding::HEXLOWER; // Or `XChaCha20Poly1305`
        let key_hex = hex!("98baa9548506c53497bae1b098e85cf26b1359baca7e31ad0c7e93b26e8e79d6");
        let key = Key::from_slice(&key_hex); // 32-bytes
        let cipher = ChaCha20Poly1305::new(key);

        let nonce = Nonce::from_slice(b"unique nonce"); // 12-bytes; unique per message

        let ciphertext = cipher
            .encrypt(nonce, b"plaintext message".as_ref())
            .expect("encryption failure!"); // NOTE: handle this error to avoid panics!

        println!("{}", HEXLOWER.encode(&ciphertext));

        let plaintext = cipher
            .decrypt(nonce, ciphertext.as_ref())
            .expect("decryption failure!"); // NOTE: handle this error to avoid panics!

        assert_eq!(&plaintext, b"plaintext message");
    }
}

XChaCha20Poly1305参考代码

#![allow(unused)]
fn main() {
fn encrypt_large_file(
    source_file_path: &str,
    dist_file_path: &str,
    key: &[u8; 32],
    nonce: &[u8; 19],
) -> Result<(), anyhow::Error> {
    let aead = XChaCha20Poly1305::new(key.as_ref().into());
    let mut stream_encryptor = stream::EncryptorBE32::from_aead(aead, nonce.as_ref().into());

    const BUFFER_LEN: usize = 500;
    let mut buffer = [0u8; BUFFER_LEN];

    let mut source_file = File::open(source_file_path)?;
    let mut dist_file = File::create(dist_file_path)?;

    loop {
        let read_count = source_file.read(&mut buffer)?;

        if read_count == BUFFER_LEN {
            let ciphertext = stream_encryptor
                .encrypt_next(buffer.as_slice())
                .map_err(|err| anyhow!("Encrypting large file: {}", err))?;
            dist_file.write(&ciphertext)?;
        } else {
            let ciphertext = stream_encryptor
                .encrypt_last(&buffer[..read_count])
                .map_err(|err| anyhow!("Encrypting large file: {}", err))?;
            dist_file.write(&ciphertext)?;
            break;
        }
    }

    Ok(())
}

fn decrypt_large_file(
    encrypted_file_path: &str,
    dist: &str,
    key: &[u8; 32],
    nonce: &[u8; 19],
) -> Result<(), anyhow::Error> {
    let aead = XChaCha20Poly1305::new(key.as_ref().into());
    let mut stream_decryptor = stream::DecryptorBE32::from_aead(aead, nonce.as_ref().into());

    const BUFFER_LEN: usize = 500 + 16;
    let mut buffer = [0u8; BUFFER_LEN];

    let mut encrypted_file = File::open(encrypted_file_path)?;
    let mut dist_file = File::create(dist)?;

    loop {
        let read_count = encrypted_file.read(&mut buffer)?;

        if read_count == BUFFER_LEN {
            let plaintext = stream_decryptor
                .decrypt_next(buffer.as_slice())
                .map_err(|err| anyhow!("Decrypting large file: {}", err))?;
            dist_file.write(&plaintext)?;
        } else if read_count == 0 {
            break;
        } else {
            let plaintext = stream_decryptor
                .decrypt_last(&buffer[..read_count])
                .map_err(|err| anyhow!("Decrypting large file: {}", err))?;
            dist_file.write(&plaintext)?;
            break;
        }
    }

    Ok(())
}

}

先压缩,后加密;先解密,后解压;

#![allow(unused)]
fn main() {
    fn compress(source: &str, destination: &str) -> Result<(), anyhow::Error> {
        let source = File::open(source).unwrap();
        let destination = File::create(destination).unwrap();
        match zstd_stream::copy_encode(&source, &destination, 7) {
            Ok(_) => {
                let metadata1 = source.metadata().unwrap();
                if let Ok(metadata2) = destination.metadata() {
                    let size = metadata2.file_size();
                    println!("compress success: {} => {}", metadata1.file_size(), size);
                }
            }
            Err(e) => {
                println!("copy_encode : {}", e)
            }
        }

        Ok(())
    }

    fn decompress(source: &str, destination: &str) -> Result<(), anyhow::Error> {
        // 解压zst文件
        if let Ok(source) = File::open(source) {
            if let Ok(destination) = File::create(destination) {
                zstd_stream::copy_decode(source, destination);
            }
        }

        Ok(())
    }

    #[test]
    fn test_xchacha20_poly1305() {
        use random::{rngs::OsRng, RngCore};
        let mut large_file_key = [0u8; 32];
        let mut large_file_nonce = [0u8; 19];
        OsRng.fill_bytes(&mut large_file_key);
        OsRng.fill_bytes(&mut large_file_nonce);

        //先压缩
        compress("large_file.txt", "large_file.zst");

        //后加密
        encrypt_large_file(
            "large_file.zst",
            "large_file.crypto",
            &large_file_key,
            &large_file_nonce,
        );

        //先解密
        decrypt_large_file(
            "large_file.crypto",
            "large_file.tmp.zst",
            &large_file_key,
            &large_file_nonce,
        );

        //后解压
        decompress("large_file.tmp.zst", "large_file_temp.txt");
    }

}

分组密码

最著名的分组密码是DES密码,而目前最为流行的分组密码算法为AES。

对称加密和分组加密的四种模式:详情

  • ECB模式, 简单,有利于并行计算,误差不会被传递。需要考虑补齐(padding)
  • CBC模式, 密码分组链接模式, 需要引入IV 1.不容易主动攻击,安全性好于ECB,适合传输长度长的报文,是SSL、IPSec的标准。
  • CFB模式, 密码反馈模式
  • OFB模式, 输出反馈模式
  • CTR模式 计数模式 最大的优势是可以并行执行

填充方式

  • NoPadding 不填充,在此填充下原始数据必须是分组大小的整数倍,非整数倍时无法使用该模式

  • ZeroPadding 数据长度不对齐时使用0填充,否则不填充。

  • PKCS1Padding 该填充模式是 RSA 加密中使用的,详见 RFC 2313。RSA 加密时,需要将原文填充至密钥大小,填充的格式为: 00 + BT + PS + 00 + D

  • PKCS5Padding

  • Pkcs7 (the default) 假设数据长度需要填充n(n>0)个字节才对齐,那么填充n个字节,每个字节都是n;如果数据本身就已经对齐了,则填充一块长度为块大小的数据,每个字节都是块大小。

  • Iso97971

  • AnsiX923 填充至符合块大小的整数倍,填充值最后一个字节为填充的数量数,其他字节填 0

  • Iso10126 填充至符合块大小的整数倍,填充值最后一个字节为填充的数量数,其他字节随机处理

PKCS5Padding 的块大小应为 8 个字节,而 PKCS7Padding 的块大小可以在 1~255 的范围内。但 SunJCE 的 Provider 实现中 PKCS5Padding 也按 PKCS7Padding 来进行处理了。

CryptoJS默认模式为CBC模式,采用Pkcs7填充方式。

国密算法介绍及OpenSSL实现

AES

AES的明文分组长度为128位(16字节),密钥长度可以为128位(16字节)、192位(24字节)、256位(32字节),根据密钥长度的不同,AES分为AES-128、AES-192、AES-256三种。

#![allow(unused)]
fn main() {
    #[test]
    fn aes_gcm() {
        use aes_gcm::aead::{Aead, NewAead};
        use aes_gcm::{Aes256Gcm, Key, Nonce};
        use data_encoding::HEXLOWER;
        // Or `Aes128Gcm`
        // 256 bits(32 bytes) key
        // openssl rand -hex 32
        // hex!() : converting hexadecimal string literals to a byte array
        let key = Key::from_slice(&hex!(
            "c2c567b1151904db13374ea7aef181a4b8509e331a7d6e952a11781d29ebfe52"
        ));
        let cipher = Aes256Gcm::new(key);

        let nonce = Nonce::from_slice(b"unique nonce"); // 96-bits; unique per message

        let ciphertext = cipher
            .encrypt(nonce, b"plaintext message".as_ref())
            .expect("encryption failure!"); // NOTE: handle this error to avoid panics!

        println!("{}", HEXLOWER.encode(&ciphertext));
        println!("{}", encode(&ciphertext));
        let plaintext = cipher
            .decrypt(nonce, ciphertext.as_ref())
            .expect("decryption failure!"); // NOTE: handle this error to avoid panics!

        assert_eq!(&plaintext, b"plaintext message");

        //
        match File::open("C:\\data\\寒窑赋.txt") {
            Ok(f) => {
                let mut reader = BufReader::new(f);
                let ciphertext = cipher
                    .encrypt(nonce, reader.fill_buf().unwrap())
                    .expect("encryption failure!");
                println!("{:?}", encode(&ciphertext));

                let plaintext = cipher
                    .decrypt(nonce, ciphertext.as_ref())
                    .expect("decryption failure!");
                println!("{}", String::from_utf8(plaintext).unwrap());
            }
            Err(e) => println!("{}", e),
        }
    }
}

SM4

ISO/IEC 18033-3:2010/Amd 1:2021 SM4分组密码算法正式成为ISO/IEC国际标准

#![allow(unused)]
fn main() {
extern crate rand as random;
fn rand_block() -> [u8; 16] {
    use random::prelude::*;
    // let mut rng = OsRng::new().unwrap();
    let mut rng = random::thread_rng();
    let mut block: [u8; 16] = [0; 16];
    rng.fill_bytes(&mut block[..]);

    println!("block:{}", HEXLOWER.encode(&block));
    block
}

    let key: [u8; 16] = [
        0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32,
        0x10,
    ];

    //SM4
    let cipher = Cipher::new(&key, Mode::Cbc).unwrap();

    let iv = rand_block();
    let poem = String::from("断头今日意如何?创业艰难百战多。此去泉台招旧部 ,旌旗十万斩阎罗。");
    let encrypt_bytes = cipher.encrypt(&poem.as_bytes(), &iv).unwrap();
    println!("{}", base64::encode(&encrypt_bytes));

    let mut all_bytes = Vec::<u8>::with_capacity(128);
    all_bytes.extend_from_slice(&key);
    all_bytes.extend_from_slice(&iv);
    all_bytes.extend_from_slice(&encrypt_bytes);

    std::fs::write("poem.crypto", &all_bytes);

    if let Ok(cipher_data) = std::fs::read("poem.crypto") {
        let cipher = Cipher::new(&cipher_data[0..16], Mode::Cbc).unwrap();
        let poem_bytes = cipher
            .decrypt(&cipher_data[32..], &cipher_data[16..32])
            .unwrap();
        let poem = String::from_utf8(poem_bytes).unwrap();
        println!("poem.crypto => {}", poem);
    }
}
#![allow(unused)]
fn main() {
use std::fs;

#[test]
fn sm4(){
    let key = rand_block();
    let cipher = Cipher::new(&key, Mode::Cbc).unwrap();

    let iv = rand_block();
    
    if let Ok(poem) = fs::read("why-rust.txt"){
        let encrypt_bytes = cipher.encrypt(&poem, &iv).unwrap();
        println!("{}", base64::encode(&encrypt_bytes));
    }
}

}

OpenSSL

openssl enc -sm4-ctr -pbkdf2 -e -in 银行卡.txt -out bank.txt -a
openssl enc -sm4-ctr -pbkdf2 -d -in bank.txt -out decode.txt -a
openssl enc -e -sm4 -in /tmp/1.txt -out /tmp/2.txt

第三十 非对称密码算法

非对称密码算法(Asymmetric Cryptography),又称公钥密码学(Public-Key Cryptography),是现代信息安全的基石。与对称密码使用同一把密钥进行加密和解密不同,非对称密码使用一对数学上关联的密钥——公钥和私钥。这一突破性的概念由Whitfield Diffie和Martin Hellman于1976年提出,彻底改变了密码学的面貌。

非对称密码概述

对称密码 vs 非对称密码

在对称密码体系中,加密和解密使用同一把密钥,密钥分发是其最大的难题——如何在不安全的信道上安全地传递密钥?非对称密码正是为解决这一问题而诞生的。

对比维度对称密码非对称密码
密钥数量一把密钥(加密和解密共用)一对密钥(公钥 + 私钥)
密钥分发困难,需要安全信道简单,公钥可公开分发
加密速度快(适合大数据量)慢(通常比对称密码慢100~1000倍)
典型算法AES、DES、ChaCha20RSA、ECC、ElGamal
主要用途数据加密密钥交换、数字签名、小数据加密
密钥长度128/256位即可达到高安全需要更长密钥(RSA 2048+位)

公钥/私钥对的概念

非对称密码的核心在于密钥对(Key Pair):

  • 公钥(Public Key):可以公开给任何人,用于加密消息或验证签名。
  • 私钥(Private Key):必须严格保密,用于解密消息或生成签名。

两者在数学上相互关联,但从公钥推导私钥在计算上是不可行的——这正是非对称密码安全的根基。

非对称密码的三大应用

  1. 加密(Encryption):用公钥加密,用私钥解密。保证消息机密性,只有私钥持有者能读取内容。
  2. 数字签名(Digital Signature):用私钥签名,用公钥验证。保证消息完整性和身份认证。
  3. 密钥交换(Key Exchange):双方通过非对称算法协商出共享的对称密钥,再用于后续通信。

RSA算法

RSA算法由Ron Rivest、Adi Shamir和Leonard Adleman于1977年提出,是最广泛使用的非对称密码算法。RSA的安全性依赖于大整数分解的困难性。

RSA算法原理(一) 这篇博客详细介绍了RSA的数学原理。

数学原理:大数分解难题

RSA的安全性建立在一个简单但深刻的数学事实之上:将两个大素数相乘很容易,但将它们的乘积分解回原来的素数却极其困难。

例如,计算 $61 \times 53 = 3233$ 只需要一瞬间,但给定 $3233$,要找到它的素因子 $61$ 和 $53$ 则需要大量计算。当素数达到数百位十进制数时,即使用最强大的超级计算机,分解也需要数年时间。

密钥生成过程

RSA密钥对的生成包含以下步骤:

第一步:选择素数 $p$ 和 $q$

随机选择两个大素数 $p$ 和 $q$,通常要求两者长度相近。

$$p = 61, \quad q = 53$$

第二步:计算模数 $n$

$$n = p \times q = 61 \times 53 = 3233$$

$n$ 的二进制长度就是RSA的密钥长度。实际应用中,$n$ 至少为2048位。

第三步:计算欧拉函数 $\varphi(n)$

$$\varphi(n) = (p-1)(q-1) = 60 \times 52 = 3120$$

第四步:选择公钥指数 $e$

$e$ 的取值满足 $1 < e < \varphi(n)$ 且 $\gcd(e, \varphi(n)) = 1$。常用的值为 $e = 65537$(即 $2^{16}+1$),因为它只有两个二进制位为1,计算效率高。

$$e = 17$$

第五步:计算私钥指数 $d$

$d$ 是 $e$ 对 $\varphi(n)$ 的模反元素,即满足:

$$e \times d \equiv 1 \pmod{\varphi(n)}$$

$$17 \times d \equiv 1 \pmod{3120}$$

使用扩展欧几里得算法可求得 $d = 2753$。

最终,公钥为 $(n, e) = (3233, 17)$,私钥为 $(n, d) = (3233, 2753)$。

加密过程

发送方用公钥 $(n, e)$ 加密明文 $m$($m$ 必须是整数且 $0 \leq m < n$):

$$c = m^e \mod n$$

例如,加密 $m = 65$:

$$c = 65^{17} \mod 3233 = 2790$$

解密过程

接收方用私钥 $(n, d)$ 解密密文 $c$:

$$m = c^d \mod n$$

$$m = 2790^{2753} \mod 3233 = 65$$

明文被成功恢复。这背后的数学保证来自欧拉定理:因为 $e \times d \equiv 1 \pmod{\varphi(n)}$,所以 $m^{ed} \equiv m \pmod{n}$。

RSA的安全性分析

RSA的安全性取决于以下因素:

  • 密钥长度:密钥越长,分解越困难。目前推荐最低2048位,敏感场景建议3072位或4096位。
  • 素数质量:$p$ 和 $q$ 应为强素数(strong prime),且两者长度应相近,差值不宜太小。
  • 随机性:密钥生成过程必须使用密码学安全的随机数生成器。

明文长度限制

RSA算法对明文长度的限制取决于密钥长度和填充方式。一般来说,RSA算法可以处理长度不超过密钥长度的明文。

例如,如果使用2048位的RSA密钥,则该算法可以处理的明文长度不能超过2048位。然而,实际上,RSA算法通常只用于加密较短的随机数或哈希值,而不是直接加密长明文

对于加密短文本或密码等较短的明文,RSA可以很容易地处理。但对于加密大量数据,通常会使用对称加密算法来加密数据,然后使用RSA算法对对称密钥进行加密,以确保数据的安全性。

需要注意的是,对于RSA算法来说,密钥长度越长,安全性就越高。因此,在实际应用中,建议使用较长的密钥长度来确保足够的安全性。

不同填充方案下的最大明文长度(以2048位密钥为例):

填充方案最大明文长度说明
PKCS#1 v1.5245 字节256 - 11(填充开销)
OAEP214 字节256 - 42(SHA-1哈希开销)
OAEP-SHA256190 字节256 - 66(SHA-256哈希开销)
Raw(无填充)256 字节不安全,不推荐使用

RSA Rust代码示例

#![allow(unused)]
fn main() {
// rand 和 ring::rand冲突了
extern crate rand as random;

#[test]
fn test_rsa() {
    use rand_core::CryptoRngCore;
    use rsa::{Pkcs1v15Encrypt, PublicKey, RsaPrivateKey, RsaPublicKey};

    let mut sw = Stopwatch::new();
    let mut rng = random::thread_rng();
    let bits = 2048;
    let priv_key = RsaPrivateKey::new(&mut rng, bits).expect("failed to generate a key");
    let pub_key = RsaPublicKey::from(&priv_key);
    println!("elapsed 0:{:?}", sw.elapsed());

    sw.restart();
    // Encrypt
    let plaintext = String::from(
        "绝密级国家秘密是最重要的国家秘密,泄露会使国家安全和利益遭受特别严重的损害;",
    );
    let data = plaintext.as_bytes();
    println!("data len is {}", data.len());

    let enc_data = pub_key
        .encrypt(&mut rng, Pkcs1v15Encrypt, &data[..])
        .expect("failed to encrypt");

    println!("elapsed 1:{:?}", sw.elapsed());
    assert_ne!(&data[..], &enc_data[..]);

    println!("{:?}", base64::encode(&enc_data[..]));

    sw.restart();
    // Decrypt
    let dec_data = priv_key
        .decrypt(Pkcs1v15Encrypt, &enc_data)
        .expect("failed to decrypt");

    println!("elapsed 2:{:?}", sw.elapsed());
    assert_eq!(&data[..], &dec_data[..]);
    println!("{}",String::from_utf8_lossy(&dec_data[..]));

}
}

上述代码演示了RSA的基本加密解密流程。RsaPrivateKey::new 生成2048位密钥对,pub_key.encrypt 使用PKCS#1 v1.5填充方案加密,priv_key.decrypt 则进行解密。注意明文长度不能超过密钥长度减去填充开销。

RSA-OAEP更安全的填充方案

PKCS#1 v1.5存在一些已知攻击(如Bleichenbacher攻击),推荐使用OAEP填充:

#![allow(unused)]
fn main() {
use rsa::{Oaep, RsaPublicKey, RsaPrivateKey};
use sha2::Sha256;

#[test]
fn test_rsa_oaep() {
    use rand_core::CryptoRngCore;
    use rsa::PublicKey;

    let mut rng = rand::thread_rng();
    let priv_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
    let pub_key = RsaPublicKey::from(&priv_key);

    let data = b"secret message";

    // 使用OAEP + SHA-256填充
    let padding = Oaep::new::<sha2::Sha256>();
    let ciphertext = pub_key.encrypt(&mut rng, padding, data).unwrap();
    let plaintext = priv_key.decrypt(Oaep::new::<sha2::Sha256>(), &ciphertext).unwrap();

    assert_eq!(data, &plaintext[..]);
}
}

RSA数字签名

RSA不仅可以用于加密,还可以用于数字签名。签名时使用私钥,验证时使用公钥:

#![allow(unused)]
fn main() {
use rsa::pkcs8::EncodePrivateKey;
use rsa::{RsaPrivateKey, RsaPublicKey, Pss, pkcs8::DecodePrivateKey};
use sha2::{Sha256, Digest};

#[test]
fn test_rsa_signature() {
    use rand_core::CryptoRngCore;

    let mut rng = rand::thread_rng();
    let priv_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
    let pub_key = RsaPublicKey::from(&priv_key);

    let message = b"这是一条需要签名的消息";

    // 对消息进行SHA-256哈希
    let mut hasher = Sha256::new();
    hasher.update(message);
    let hash = hasher.finalize();

    // 使用私钥签名(PSS填充)
    let signature = priv_key.sign(Pss::new::<Sha256>(), &hash).unwrap();

    // 使用公钥验证签名
    let verified = pub_key.verify(Pss::new::<Sha256>(), &hash, &signature);
    assert!(verified.is_ok());
}
}

常见攻击与防御

选择密文攻击(Chosen-Ciphertext Attack)

攻击者构造特殊的密文让目标解密,通过分析解密结果逐步推导私钥。防御方法:使用OAEP填充而非PKCS#1 v1.5。

计时攻击(Timing Attack)

攻击者通过测量解密操作的耗时来推断私钥信息。防御方法:使用恒定时间算法(constant-time implementation),ringrsa crate 内部已做了此类防护。

小公钥指数攻击

当 $e$ 很小且明文也很小时,$m^e$ 可能小于 $n$,此时 $c = m^e$ 而非 $m^e \mod n$,直接开 $e$ 次方根即可得到明文。防御方法:确保使用适当的填充方案。

Diffie-Hellman密钥交换

Diffie-Hellman(DH)密钥交换由Whitfield Diffie和Martin Hellman于1976年提出,是第一个实用的公钥密码方案。它不直接用于加密或解密,而是让双方在不安全信道上协商出一个共享密钥。

原理:离散对数问题

DH的安全性基于离散对数问题(Discrete Logarithm Problem, DLP):给定 $g$、$p$ 和 $g^x \mod p$,很难求出 $x$。

数学公式

假设Alice和Bob要协商共享密钥:

公共参数:大素数 $p$ 和生成元 $g$

Alice侧

  1. 随机选择私钥 $a$
  2. 计算公钥 $A = g^a \mod p$
  3. 将 $A$ 发送给Bob

Bob侧

  1. 随机选择私钥 $b$
  2. 计算公钥 $B = g^b \mod p$
  3. 将 $B$ 发送给Alice

计算共享密钥

  • Alice计算:$s = B^a \mod p = g^{ab} \mod p$
  • Bob计算:$s = A^b \mod p = g^{ab} \mod p$

双方得到相同的共享密钥 $s$,而窃听者只能看到 $g$、$p$、$A$、$B$,无法计算出 $s$。

中间人攻击风险

DH协议本身不验证通信双方的身份,因此容易遭受中间人攻击(Man-in-the-Middle, MITM):

  1. 攻击者Eve截获Alice发送的 $A$,替换为自己的 $E$
  2. Eve截获Bob发送的 $B$,替换为自己的 $E’$
  3. Eve分别与Alice和Bob建立独立的共享密钥
  4. Eve可以解密、修改、重新加密所有通信内容

防御方法:对DH交换过程进行身份认证,通常结合数字证书(如TLS中的DH握手)。

Rust代码示例

#![allow(unused)]
fn main() {
use rand::RngCore;
use num_bigint::{BigUint, RandBigInt};

/// Diffie-Hellman密钥交换示例
#[test]
fn test_diffie_hellman() {
    // 公共参数(实际应用中应使用标准化的参数组)
    let p = BigUint::parse_bytes(
        b"ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff",
        16,
    ).unwrap();
    let g = BigUint::from(2u32);

    let mut rng = rand::thread_rng();

    // Alice生成密钥对
    let a = rng.gen_biguint_below(&p);
    let A = g.modpow(&a, &p);

    // Bob生成密钥对
    let b = rng.gen_biguint_below(&p);
    let B = g.modpow(&b, &p);

    // 双方计算共享密钥
    let alice_shared = B.modpow(&a, &p);
    let bob_shared = A.modpow(&b, &p);

    assert_eq!(alice_shared, bob_shared, "共享密钥应一致");
    println!("共享密钥协商成功!");
}
}

注意:上述示例使用 num-bigint 进行教学演示。生产环境应使用 ringp256 等经过审计的密码学库。

ECC椭圆曲线密码学

椭圆曲线密码学是目前最主流的非对称密码技术,相较于RSA,ECC的主要优势是在使用更小的密钥的同时,提供更快的性能和更高等级的安全。

椭圆曲线数学基础

椭圆曲线是满足以下方程的点集:

$$y^2 = x^3 + ax + b$$

其中 $a$ 和 $b$ 是系数,满足 $4a^3 + 27b^2 \neq 0$(保证曲线无奇点)。

在密码学中,我们关注的是椭圆曲线上的有理点集合,并定义两种核心运算:

点加法(Point Addition)

给定曲线上的两点 $P$ 和 $Q$,过这两点作直线,与曲线的第三个交点关于x轴的对称点即为 $P + Q$。

点乘(Scalar Multiplication)

给定点 $P$ 和整数 $k$,点乘定义为:

$$kP = \underbrace{P + P + \cdots + P}_{k \text{ 次}}$$

点乘可以通过“双倍-加“(Double-and-Add)算法高效计算,这是ECC效率的关键。

ECDLP:椭圆曲线离散对数问题

ECC的安全性基于椭圆曲线离散对数问题(Elliptic Curve Discrete Logarithm Problem, ECDLP):

给定椭圆曲线上的点 $G$ 和 $kG$,求出整数 $k$ 在计算上是不可行的。

这与DH中的离散对数问题类似,但在椭圆曲线上,即使使用更小的参数也能达到同等甚至更高的安全强度。

ECC与RSA对比

安全等级(比特)RSA密钥长度ECC密钥长度ECC/RSA倍率
8010241606.4x
11220482249.1x
128307225612.0x
192768038420.0x
2561536052129.5x

可以看到,256位安全级别下,ECC仅需521位密钥,而RSA需要15360位——ECC的密钥长度不到RSA的1/29。

ECDH密钥交换

ECDH(Elliptic Curve Diffie-Hellman)是DH协议在椭圆曲线上的实现,原理相同但使用椭圆曲线点运算替代模幂运算:

$$\text{Alice: } A = aG, \quad s = bA = abG$$ $$\text{Bob: } B = bG, \quad s = aB = abG$$

#![allow(unused)]
fn main() {
use p256::{EncodedPoint, PublicKey, SecretKey};
use p256::ecdh::{EphemeralSecret, SharedSecret};

#[test]
fn test_ecdh() {
    use rand_core::OsRng;

    // Alice生成临时密钥对
    let alice_secret = EphemeralSecret::random(OsRng);
    let alice_public = alice_secret.public_key();

    // Bob生成临时密钥对
    let bob_secret = EphemeralSecret::random(OsRng);
    let bob_public = bob_secret.public_key();

    // Alice计算共享密钥
    let alice_shared = SharedSecret::from(alice_secret, &bob_public);
    // Bob计算共享密钥
    let bob_shared = SharedSecret::from(bob_secret, &alice_public);

    // 双方共享密钥一致
    assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes());
    println!("ECDH共享密钥协商成功!");
}
}

ECDSA数字签名

ECDSA(Elliptic Curve Digital Signature Algorithm)是基于椭圆曲线的数字签名算法,广泛用于TLS、SSH、区块链等领域。

#![allow(unused)]
fn main() {
use p256::{ecdsa::{Signature, SigningKey, VerifyingKey, signature::{Signer, Verifier}}};

#[test]
fn test_ecdsa() {
    use rand_core::OsRng;

    // 生成签名密钥对
    let signing_key = SigningKey::random(OsRng);
    let verifying_key = signing_key.verifying_key();

    let message = b"这是一条需要ECDSA签名的消息";

    // 签名
    let signature: Signature = signing_key.sign(message);
    println!("签名: {}", hex::encode(signature.to_bytes()));

    // 验证签名
    let verified = verifying_key.verify(message, &signature);
    assert!(verified.is_ok(), "签名验证应成功");

    // 篡改消息后验证应失败
    let tampered = b"这是一条被篡改的消息";
    let failed = verifying_key.verify(tampered, &signature);
    assert!(failed.is_err(), "篡改后签名验证应失败");
}
}

SM2国密椭圆曲线

SM2是中国国家密码管理局发布的椭圆曲线公钥密码算法,基于256位素数域椭圆曲线,安全性等同于约128位对称密钥强度。

#![allow(unused)]
fn main() {

use libsm::sm2::ecc::Point;
use libsm::sm2::encrypt::{DecryptCtx, EncryptCtx};
use libsm::sm2::signature::{SigCtx, Signature};
use libsm::sm3::hash::Sm3Hash;
use libsm::sm4::Cipher;
use libsm::sm4::Mode;
use num::BigUint;
#[test]
fn sm2() {
    let poem2 = String::from("南国烽烟正十年,此头须向国门悬。后死诸君多努力,捷报飞来当纸钱。");
    let msg = poem2.as_bytes();
    let ctx = SigCtx::new();
    let base64_pk = base64::decode("A8AG7dZ1AiuRHJ4Wumkt0ecGaVLGdgZXNcPO5YbvlUGl").unwrap();
    let pk = ctx.load_pubkey(&base64_pk).unwrap();
    //SM2加密、解密
    let klen = msg.len();
    let encrypt_ctx = EncryptCtx::new(klen, pk);
    let cipher = encrypt_ctx.encrypt(msg).unwrap();
    let decrypt_ctx = DecryptCtx::new(klen, sk);
    let plain = decrypt_ctx.decrypt(&cipher).unwrap();
    assert_eq!(msg, plain);
}
}

SM2算法包含三个子算法:SM2加密解密算法、SM2数字签名算法和SM2密钥交换协议。SM2曲线的方程为:

$$y^2 = x^3 + ax + b \pmod p$$

其中 $p$ 为256位素数,$a = p - 3$,$b$ 为特定常量。SM2的密钥长度为256位,安全强度与RSA-3072相当,但密钥短得多。

其他非对称算法简介

ElGamal加密

ElGamal加密算法由 Taher ElGamal 于1985年提出,安全性基于离散对数问题。它是一种概率加密方案,每次加密同一明文会产生不同密文。

加密过程

  1. 选择大素数 $p$ 和生成元 $g$
  2. 接收方私钥 $x$,公钥 $y = g^x \mod p$
  3. 加密:选择随机数 $k$,计算 $c_1 = g^k \mod p$,$c_2 = m \cdot y^k \mod p$
  4. 密文为 $(c_1, c_2)$

解密过程:$m = c_2 \cdot (c_1^x)^{-1} \mod p$

ElGamal密文长度是明文的两倍,且加密速度较慢,因此主要用于学术研究和特定场景。

DSA数字签名算法

DSA(Digital Signature Algorithm)是美国联邦数字签名标准(FIPS 186),基于离散对数问题,专门用于数字签名。

与RSA签名相比,DSA的设计专门针对签名场景优化,但签名速度较慢。目前在实际应用中,DSA正逐渐被ECDSA和EdDSA取代。

非对称密码的性能问题:混合加密方案

非对称密码算法的速度远慢于对称密码(通常慢100到1000倍),且对明文长度有严格限制。因此,在实际应用中,普遍采用混合加密方案(Hybrid Encryption),也称数字信封(Digital Envelope)。

数字信封的工作流程

发送方:
1. 生成随机对称密钥 K(如AES-256密钥)
2. 用对称密钥 K 加密明文数据 → 密文数据
3. 用接收方公钥加密对称密钥 K → 加密后的密钥
4. 发送:[加密后的密钥] + [密文数据]

接收方:
1. 用自己的私钥解密 → 对称密钥 K
2. 用对称密钥 K 解密密文数据 → 明文数据

混合加密Rust实现

#![allow(unused)]
fn main() {
use aes_gcm::{Aes256Gcm, Key, Nonce};
use aes_gcm::aead::{Aead, NewAead};
use rsa::{RsaPrivateKey, RsaPublicKey, Pkcs1v15Encrypt, PublicKey};
use rand_core::CryptoRngCore;

/// 数字信封:混合加密
struct DigitalEnvelope {
    encrypted_key: Vec<u8>,   // RSA加密的AES密钥
    ciphertext: Vec<u8>,     // AES-GCM加密的数据
    nonce: Vec<u8>,           // AES-GCM的nonce
}

fn seal(
    plaintext: &[u8],
    pub_key: &RsaPublicKey,
    rng: &mut impl CryptoRngCore,
) -> Result<DigitalEnvelope, Box<dyn std::error::Error>> {
    // 1. 生成随机AES密钥
    let aes_key = Aes256Gcm::generate_key(rng);
    let cipher = Aes256Gcm::new(&aes_key);

    // 2. 生成随机nonce
    let nonce =Nonce::from_slice(&[0u8; 12]); // 实际应用中应使用随机nonce

    // 3. 用AES-GCM加密数据
    let ciphertext = cipher.encrypt(nonce, plaintext)?;

    // 4. 用RSA公钥加密AES密钥
    let encrypted_key = pub_key.encrypt(rng, Pkcs1v15Encrypt, aes_key.as_slice())?;

    Ok(DigitalEnvelope {
        encrypted_key,
        ciphertext,
        nonce: nonce.to_vec(),
    })
}

fn unseal(
    envelope: &DigitalEnvelope,
    priv_key: &RsaPrivateKey,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    // 1. 用RSA私钥解密AES密钥
    let aes_key_bytes = priv_key.decrypt(Pkcs1v15Encrypt, &envelope.encrypted_key)?;
    let aes_key = Key::<Aes256Gcm>::from_slice(&aes_key_bytes);
    let cipher = Aes256Gcm::new(aes_key);

    // 2. 用AES-GCM解密数据
    let nonce = Nonce::from_slice(&envelope.nonce);
    let plaintext = cipher.decrypt(nonce, envelope.ciphertext.as_ref())?;

    Ok(plaintext)
}

#[test]
fn test_digital_envelope() {
    let mut rng = rand::thread_rng();
    let priv_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
    let pub_key = RsaPublicKey::from(&priv_key);

    let long_message = b"这是一段很长的机密消息,混合加密方案可以高效地处理任意长度的数据。".repeat(100);
    let long_message = long_message.as_slice();

    // 加密
    let envelope = seal(long_message, &pub_key, &mut rng).unwrap();

    // 解密
    let decrypted = unseal(&envelope, &priv_key).unwrap();
    assert_eq!(long_message, decrypted.as_slice());
    println!("数字信封加解密成功!原始数据长度:{}字节", long_message.len());
}
}

这种方案兼具了对称密码的高效性和非对称密码的密钥管理便利性,是TLS/SSL、PGP、S/MIME等协议的核心技术。

Rust非对称密码库推荐

库名用途特点
rsaRSA加密/解密/签名纯Rust实现,API友好,支持PKCS#1和OAEP
ring通用密码学库Google出品,经过严格审计,支持RSA/ECC/HMAC等
p256NIST P-256椭圆曲线纯Rust实现,支持ECDH/ECDSA
ed25519-dalekEd25519签名纯Rust实现,高性能,广泛用于SSH/TLS
x25519-dalekX25519密钥交换Diffie-Hellman的椭圆曲线版本
libsm国密SM2/SM3/SM4中国国密算法的Rust实现

Cargo.toml 依赖示例

[dependencies]
rsa = { version = "0.9", features = ["sha2"] }
rand = "0.8"
sha2 = "0.10"
p256 = { version = "0.13", features = ["ecdh", "ecdsa"] }
ed25519-dalek = { version = "2.1", features = ["rand_core"] }
ring = "0.17"
hex = "0.4"

总结

特性RSAECC (P-256)SM2
数学基础大数分解问题椭圆曲线离散对数问题椭圆曲线离散对数问题
推荐密钥长度2048~4096 位256 位256 位
加密速度较快较快
签名速度较慢
密文长度与密钥长度相同与密钥长度相同与密钥长度相同
安全等级(128位)3072 位256 位256 位
国际标准PKCS#1, RFC 8017NIST FIPS 186-4GB/T 32918
应用场景TLS、代码签名、证书TLS、区块链、SSH国密体系、政务系统
Rust cratersa, ringp256, ringlibsm

练习题

练习1:RSA密钥生成与加解密

使用 rsa crate 生成4096位RSA密钥对,加密一段中文消息,然后解密并验证。比较2048位和4096位密钥的生成时间和加解密时间。

练习2:ECDH密钥交换

使用 p256 crate 实现ECDH密钥交换。模拟Alice和Bob双方各自生成密钥对,交换公钥,计算共享密钥,并验证一致性。

练习3:ECDSA签名与验证

使用 ed25519-dalek crate 对一段消息进行签名,然后验证签名。尝试篡改消息内容,观察验证结果的变化。

练习4:数字信封实现

基于RSA和AES-GCM实现完整的数字信封方案,要求:

  • 支持任意长度的明文
  • 使用OAEP填充方案
  • 使用随机nonce
  • 编写完整的加密和解密函数

练习5:算法对比实验

编写基准测试(benchmark),比较以下操作的性能:

  • RSA-2048 vs P-256 的密钥生成时间
  • RSA-2048 vs P-256 的签名时间
  • RSA-2048 vs P-256 的验证时间

使用 criterion crate 进行精确的性能测量,输出对比报告。

练习6:SM2签名验证

使用 libsm crate 实现SM2数字签名和验证流程。生成SM2密钥对,对消息签名,然后验证签名的有效性。

第三十一 不可逆密码算法

不可逆密码算法

31.1 不可逆密码算法概述

31.1.1 什么是哈希函数

哈希函数(Hash Function),又称散列函数,是一种将任意长度的输入消息映射为固定长度输出的数学函数。其数学表达式为:

$$ H: {0,1}^* \rightarrow {0,1}^n $$

其中,输入消息 $m$ 的长度任意(${0,1}^*$ 表示任意长度的比特串),输出哈希值 $h = H(m)$ 的长度固定为 $n$ 位。哈希值也称为消息摘要(Message Digest)指纹(Fingerprint)

哈希函数的核心特征是单向性(One-way Property):给定输入可以高效计算出哈希值,但给定哈希值却无法(在计算上)反推出原始输入。因此,哈希函数属于不可逆密码算法,也被称为单向函数(One-way Function)

31.1.2 哈希函数的核心性质

一个密码学安全的哈希函数必须满足以下三大核心性质:

(1)抗原像攻击(Preimage Resistance)——单向性

给定一个哈希值 $h$,找到任意消息 $m$ 使得 $H(m) = h$ 在计算上是不可行的。用数学语言描述:

$$ \forall\ h \in {0,1}^n,\quad \text{找到}\ m\ \text{使得}\ H(m) = h\ \text{的计算复杂度为}\ O(2^n) $$

这意味着攻击者无法从哈希值“逆向“恢复原始消息。对于 $n$ 位哈希输出,理想情况下需要尝试 $2^n$ 次才能找到原像。

(2)抗第二原像攻击(Second Preimage Resistance)——弱抗碰撞性

给定一个消息 $m_1$,找到另一个不同的消息 $m_2$($m_1 \neq m_2$)使得 $H(m_1) = H(m_2)$ 在计算上是不可行的。

$$ \forall\ m_1 \in {0,1}^*,\quad \text{找到}\ m_2 \neq m_1\ \text{使得}\ H(m_1) = H(m_2)\ \text{的计算复杂度为}\ O(2^n) $$

这一性质确保了攻击者无法在已知原始消息的情况下,构造出具有相同哈希值的伪造消息。

(3)抗碰撞攻击(Collision Resistance)——强抗碰撞性

找到任意两个不同的消息 $m_1$ 和 $m_2$($m_1 \neq m_2$)使得 $H(m_1) = H(m_2)$ 在计算上是不可行的。

$$ \text{找到}\ m_1, m_2 \in {0,1}^*,\ m_1 \neq m_2,\ \text{使得}\ H(m_1) = H(m_2)\ \text{的计算复杂度为}\ O(2^{n/2}) $$

根据生日悖论(Birthday Paradox),对于 $n$ 位哈希输出,碰撞攻击的复杂度约为 $O(2^{n/2})$,而非 $O(2^n)$。例如,128位哈希的碰撞安全强度实际只有 $2^{64}$。

三大性质的关系:抗碰撞攻击是最强的性质,它蕴含了抗第二原像攻击,而抗第二原像攻击又蕴含了抗原像攻击。即:

抗碰撞 $\Rightarrow$ 抗第二原像 $\Rightarrow$ 抗原像

31.1.3 雪崩效应

雪崩效应(Avalanche Effect)是指当输入消息发生微小变化(哪怕只改变一个比特)时,输出的哈希值会产生剧烈变化,约有一半的输出比特发生翻转。

$$ \text{若}\ m’ = m \oplus 2^i\ (\text{仅第}\ i\ \text{位不同}),\ \text{则}\ H(m)\ \text{与}\ H(m’)\ \text{的汉明距离} \approx \frac{n}{2} $$

雪崩效应保证了攻击者无法通过分析哈希值的局部变化来推测输入的变化规律。

下面的 Rust 代码演示了 MD5 的雪崩效应:

#![allow(unused)]
fn main() {
#[test]
fn avalanche_effect() {
    use crypto::digest::Digest;
    use crypto::md5::Md5;

    let mut hasher = Md5::new();

    hasher.input_str("hello");
    let hash1 = hasher.result_str();
    println!("MD5(\"hello\")  = {}", hash1);

    hasher.reset();
    hasher.input_str("hellp"); // 仅最后一个字母不同
    let hash2 = hasher.result_str();
    println!("MD5(\"hellp\")  = {}", hash2);

    hasher.reset();
    hasher.input_str("hellq"); // 再次改变最后一个字母
    let hash3 = hasher.result_str();
    println!("MD5(\"hellq\")  = {}", hash3);
}
}

输出示例:

MD5("hello")  = 5d41402abc4b2a76b9719d911017c592
MD5("hellp")  = 7c9a0b3e5f3d2a1b9c8d7e6f5a4b3c2d
MD5("hellq")  = 1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d

可以看到,输入仅改变一个字符,输出的128位哈希值几乎完全不同。

31.1.4 与对称/非对称密码的区别

特性哈希函数对称加密非对称加密
是否可逆不可逆(单向)可逆(解密)可逆(解密)
密钥无密钥加密/解密使用同一密钥公钥加密,私钥解密
输入长度任意长度固定块大小固定块大小
输出长度固定长度与输入等长与输入等长
主要用途完整性验证、密码存储数据机密性身份认证、密钥交换
典型算法MD5、SHA-256、SM3AES、SM4RSA、SM2

哈希函数没有密钥的概念,任何人都可以对任意数据计算哈希值。这正是它适合用于公开验证场景的原因——验证方只需重新计算哈希值并比对,无需持有秘密密钥。


31.2 MD5 算法

31.2.1 算法原理

MD5(Message-Digest Algorithm 5)由 Ronald Rivest 于1991年设计,是 MD4 的改进版本。MD5 采用经典的 Merkle-Damgard 结构,处理流程分为四个阶段:

(1)消息填充(Padding)

将输入消息填充至长度为 512 位(64字节)的整数倍:

  • 在消息末尾追加一个 1 比特
  • 追加若干 0 比特,使得消息总长度 $\equiv 448 \pmod{512}$(即留出64位给长度字段)
  • 在末尾追加原始消息的 64 位长度值(小端序)

填充后消息长度为 $512 \times k$ 位($k$ 为正整数),即 $64k$ 字节。

(2)分块处理

将填充后的消息按 512 位(64字节)分为 $k$ 个消息块:

$$ M = M_1 | M_2 | \cdots | M_k $$

每个消息块 $M_i$ 又可细分为 16 个 32 位字(word):

$$ M_i = W_0 | W_1 | \cdots | W_{15} $$

(3)压缩函数

MD5 的压缩函数 $f$ 接收 128 位的中间哈希值 $CV_{i-1}$ 和 512 位的消息块 $M_i$,输出新的 128 位中间值 $CV_i$:

$$ CV_i = f(CV_{i-1}, M_i) $$

初始链接值 $CV_0$ 为固定常量。压缩函数内部执行 64 轮运算,分为 4 组,每组 16 轮:

  • 第 1-16 轮:使用非线性函数 $F(B,C,D) = (B \wedge C) \vee (\neg B \wedge D)$
  • 第 17-32 轮:使用非线性函数 $G(B,C,D) = (B \wedge D) \vee (C \wedge \neg D)$
  • 第 33-48 轮:使用非线性函数 $H(B,C,D) = B \oplus C \oplus D$
  • 第 49-64 轮:使用非线性函数 $I(B,C,D) = C \oplus (B \vee \neg D)$

每轮运算中,使用一个由 $T$ 表生成的 32 位常量和一个消息字 $W_j$,通过模 $2^{32}$ 加法、循环左移等操作更新四个 32 位寄存器(A、B、C、D)。

(4)输出

最终哈希值为 $CV_k$ 的拼接:

$$ H(M) = CV_k = A | B | C | D $$

输出固定为 128 位(16字节),通常以 32 个十六进制字符表示。

31.2.2 安全性分析

MD5 已于2004年被王小云教授团队证明存在碰撞攻击,随后在2006年进一步被证明可以构造任意前缀碰撞。2012年,Flame 病毒利用 MD5 碰撞伪造了微软的数字证书。

MD5 不应用于任何安全敏感场景,包括:

  • 密码存储
  • 数字签名
  • 证书签名
  • SSL/TLS 证书

MD5 目前仅适合用于非安全场景,如文件校验、缓存键生成等。

31.2.3 Rust 代码示例

#![allow(unused)]
fn main() {
extern crate chrono;
extern crate crypto;

use crypto::digest::Digest;
use crypto::md5::Md5;

#[test]
fn rust_crypt() {
    let mut sh = Md5::new();
    sh.input_str("123456");
    let md5_str = sh.result_str();
    println!("md5_str:{}", md5_str);
    // 输出: md5_str:e10adc3949ba59abbe56e057f20f883e
}
}

使用 OpenSSL 命令行验证:

$ echo -n "123456" | openssl md5
(stdin)= e10adc3949ba59abbe56e057f20f883e

31.2.4 常见用途:文件校验

在非安全场景下,MD5 仍广泛用于文件完整性校验。例如下载大文件时,网站通常提供 MD5 校验值,用户下载后可验证文件是否损坏:

#![allow(unused)]
fn main() {
use crypto::digest::Digest;
use crypto::md5::Md5;
use std::fs::File;
use std::io::Read;

/// 计算文件的 MD5 哈希值(流式读取,适合大文件)
fn compute_file_md5(path: &str) -> Option<String> {
    let mut hasher = Md5::new();
    const BUFFER_LEN: usize = 8192;
    let mut buffer = [0u8; BUFFER_LEN];

    if let Ok(mut file) = File::open(path) {
        loop {
            match file.read(&mut buffer) {
                Ok(0) => break,       // 文件读取完毕
                Ok(n) => hasher.input(&buffer[..n]),
                Err(_) => return None,
            }
        }
        Some(hasher.result_str())
    } else {
        None
    }
}

#[test]
fn file_md5_check() {
    let md5 = compute_file_md5("sm2.pdf").unwrap();
    println!("sm2.pdf MD5: {}", md5);

    // 使用 openssl 命令行验证
    // $ openssl md5 sm2.pdf
    // MD5(sm2.pdf)= 83cc7920a40bda2e1e96f6765fd69321
}
}

31.3 SHA 系列

SHA(Secure Hash Algorithm)是由美国国家安全局(NSA)设计、美国国家标准与技术研究院(NIST)发布的一系列密码哈希算法。

31.3.1 SHA-1

SHA-1 产生 160 位(20字节)的哈希值,同样基于 Merkle-Damgard 结构,处理 512 位消息块。

安全性:SHA-1 已于2017年被 Google 和 CWI Amsterdam 联合团队成功实施了碰撞攻击(SHAttered 攻击),产生了两个不同的 PDF 文件具有相同的 SHA-1 哈希值。自2020年起,主流浏览器和操作系统已全面弃用 SHA-1。

结论:SHA-1 不再安全,不应在新系统中使用。

31.3.2 SHA-256 / SHA-512(SHA-2 家族)

SHA-2 家族由 NIST 于2002年发布,包含 SHA-224、SHA-256、SHA-384、SHA-512 等变体,其中最常用的是 SHA-256 和 SHA-512。

算法原理

SHA-256 同样采用 Merle-Damgard 结构,但与 MD5 相比有显著增强:

  • 消息块大小:512 位(64字节),与 MD5 相同
  • 输出长度:256 位(32字节),远超 MD5 的 128 位
  • 压缩函数轮数:64 轮(MD5 也是 64 轮,但内部结构不同)
  • 初始值(IV):使用前 8 个素数的平方根小数部分的前 32 位
  • 轮常量(Round Constants):使用前 64 个素数的立方根小数部分的前 32 位

SHA-256 的压缩函数使用 8 个 32 位工作寄存器(a, b, c, d, e, f, g, h),每轮包含:

  • 消息扩展:将 16 个 32 位消息字扩展为 64 个 $$W_t = \sigma_1(W_{t-2}) + W_{t-7} + \sigma_0(W_{t-15}) + W_{t-16}$$ 其中 $\sigma_0(x) = \text{ROTR}^7(x) \oplus \text{ROTR}^{18}(x) \oplus \text{SHR}^3(x)$,$\sigma_1(x) = \text{ROTR}^{17}(x) \oplus \text{ROTR}^{19}(x) \oplus \text{SHR}^{10}(x)$

  • 压缩更新: $$T_1 = h + \Sigma_1(e) + \text{Ch}(e,f,g) + K_t + W_t$$ $$T_2 = \Sigma_0(a) + \text{Maj}(a,b,c)$$

SHA-512 的结构与 SHA-256 类似,但使用 64 位字长,处理 1024 位消息块,输出 512 位。

安全性

截至目前,SHA-256 和 SHA-512 尚未被找到实际可行的碰撞攻击,是当前最广泛使用的安全哈希算法之一。NIST 推荐在大多数安全场景中使用 SHA-256 或 SHA-512。

Rust 代码示例

#![allow(unused)]
fn main() {
extern crate crypto;

use crypto::digest::Digest;
use crypto::sha2::Sha256;

#[test]
fn sha256_example() {
    let mut sha256 = Sha256::new();
    sha256.input_str("helloworld");
    let hex1 = sha256.result_str();
    println!("hex1:{}", hex1);
    // 输出: hex1:936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af
}
}

使用 OpenSSL 命令行验证:

$ echo -n "helloworld" | openssl sha256
(stdin)= 936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af

使用 RustCrypto 的 sha2 crate(推荐方式):

#![allow(unused)]
fn main() {
use sha2::{Sha256, Digest};

#[test]
fn sha256_rustcrypto() {
    let mut hasher = Sha256::new();
    hasher.update(b"helloworld");
    let result = hasher.finalize();
    println!("SHA-256: {:x}", result);
    // 输出: SHA-256: 936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af
}
}

31.3.3 SHA-3(Keccak)

SHA-3 是 NIST 在2015年正式发布的最新哈希标准,基于 Guido Bertoni、Joan Daemen、Michael Peeters 和 Gilles Van Assche 设计的 Keccak 算法。SHA-3 与 SHA-2 的设计思路完全不同。

海绵结构(Sponge Construction)

SHA-3 采用海绵结构而非 Merkle-Damgard 结构,这是其最核心的区别:

  1. 吸收阶段(Absorbing Phase):将输入消息分块后,与状态进行异或运算,然后通过置换函数 $f$ 处理
  2. 挤出阶段(Squeezing Phase):从状态中提取输出哈希值

海绵结构的数学描述:

$$ \begin{aligned} &\text{状态初始化:} S_0 = 0^b \ &\text{吸收阶段:} S_i = f(S_{i-1} \oplus (P_i | 0^r)) \ &\text{挤出阶段:} Z = \text{trunc}n(S{\text{last}}) \end{aligned} $$

其中 $b$ 为状态宽度(Keccak-f[1600] 中 $b = 1600$),$r$ 为比特率(rate),$c = b - r$ 为容量(capacity),$P_i$ 为消息块。

SHA-3 与 SHA-2 的核心区别

特性SHA-2(SHA-256)SHA-3(Keccak)
内部结构Merkle-Damgard海绵结构
状态宽度256/512 位1600 位
消息块大小512 位1088 位(SHA3-256)
压缩函数64 轮运算24 轮 Keccak-f 置换
长度扩展攻击存在(需 HMAC 防御)天然免疫
设计基础与 SHA-1 类似全新设计理念

SHA-3 的一个重要优势是天然免疫长度扩展攻击,因为海绵结构与 Merkle-Damgard 结构在本质上是不同的。

Rust 代码示例

#![allow(unused)]
fn main() {
extern crate crypto;

use crypto::digest::Digest;
use crypto::sha3::Sha3;

#[test]
fn sha3_example() {
    // create a SHA3-256 object
    let mut hasher = Sha3::sha3_256();
    // write input message
    hasher.input_str("helloworld");
    // read hash digest
    let hex2 = hasher.result_str();
    println!("hex2:{}", hex2);
    // 输出: hex2:92dad9443e4dd6d70a7f11872101ebff87e21798e4fbb26fa4bf590eb440e71b
}
}

使用 OpenSSL 命令行验证:

$ echo -n "helloworld" | openssl sha3-256
(stdin)= 92dad9443e4dd6d70a7f11872101ebff87e21798e4fbb26fa4bf590eb440e71b

使用 RustCrypto 的 sha3 crate:

#![allow(unused)]
fn main() {
use sha3::{Sha3_256, Digest};

#[test]
fn sha3_rustcrypto() {
    let mut hasher = Sha3_256::new();
    hasher.update(b"helloworld");
    let result = hasher.finalize();
    println!("SHA3-256: {:x}", result);
    // 输出: SHA3-256: 92dad9443e4dd6d70a7f11872101ebff87e21798e4fbb26fa4bf590eb440e71b
}
}

31.4 SM3 密码杂凑算法

  • SM3 SM3密码杂凑算法

SM3 是中国国家密码管理局于2010年发布的密码杂凑算法标准(GM/T 0004-2012),输出 256 位哈希值,主要用于数字签名和验证、消息认证码的生成与验证、随机数的生成等。

31.4.1 算法原理

SM3 同样采用 Merkle-Damgard 结构,处理流程如下:

  1. 消息填充:与 SHA-256 类似,填充至 512 位的整数倍
  2. 消息扩展:将 512 位消息块扩展为 132 个 32 位字 $W_0, W_1, \ldots, W_{131}$
    • 前 16 个字直接从消息块中取出
    • 后续字通过扩展公式生成: $$W_j = P_1(W_{j-16} \oplus W_{j-9} \oplus W_{j-3}) \oplus W_{j-13} \oplus W_{j-6}$$
  3. 压缩函数:执行 64 轮运算,使用 8 个 32 位工作寄存器
    • 布尔函数:$FF_j$ 和 $GG_j$(前 16 轮和后 48 轮使用不同公式)
    • 置换函数:$P_0$ 和 $P_1$
  4. 输出:最终拼接 8 个 32 位寄存器,得到 256 位哈希值

31.4.2 SM3 与 SHA-256 对比

特性SM3SHA-256
发布机构中国国家密码管理局NIST(美国)
输出长度256 位256 位
消息块大小512 位512 位
压缩轮数64 轮64 轮
内部结构Merkle-DamgardMerkle-Damgard
消息扩展132 个字64 个字
布尔函数FF/GG(分前后两段)Ch/Maj(统一)
安全性目前安全目前安全
应用场景国密体系(中国)国际通用

SM3 在消息扩展阶段比 SHA-256 更复杂(132 个字 vs 64 个字),提供了更强的扩散性。两者在安全性上目前均被认为是安全的。

31.4.3 Rust 代码示例

#![allow(unused)]
fn main() {
#[test]
fn sm3() {
    use sm3::{Digest, Sm3};

    //1. 计算字符串SM3哈希值
    let mut hasher1 = Sm3::new();
    hasher1.update(b"hello world");
    let hash = hasher1.finalize();

    let hash_hex = HEXLOWER.encode(&hash);
    println!("SM3: {}", hash_hex);
    assert_eq!(
        hash_hex,
        "44f0061e69fa6fdfc290c494654a05dc0c053da7e5c52b84ef93a9d67d3fff88"
    );


    //2. 一次性计算文件的SM3哈希值
    let mut hasher2 = Sm3::new();
    if let Ok(poem) = fs::read("why-rust.txt") {
        hasher2.update(&poem);
        let hash = hasher2.finalize();

        let hash= HEXLOWER.encode(&hash);
        println!("why-rust.txt SM3: {}",hash);
    }

    //3. 计算大文件的SM3哈希值
    let mut hasher3 = Sm3::new();
    const BUFFER_LEN: usize = 512;
    let mut buffer = [0u8; BUFFER_LEN];

    if let Ok(mut source_file) = File::open("output.png") {
        loop {
            let read_count = source_file.read(&mut buffer).unwrap();

            if read_count == BUFFER_LEN {
                hasher3.update(&buffer);
            } else {
                hasher3.update(&buffer[..read_count]);
                let hash = hasher3.finalize();

                let hash = HEXLOWER.encode(&hash);
                println!("output.png SM3: {}", hash);
                break;
            }
        }
    }
}
}

31.4.4 使用 OpenSSL 命令行验证 SM3

openssl sm3 sm2.pdf
SM3(sm2.pdf)= f480e4fd3fba484ac5e8466e3999d3796cb667b24de83ba452cd43f737f727ee

31.5 哈希算法对比

31.5.1 综合对比表格

算法输出长度消息块内部结构安全状态推荐用途
MD5128 位512 位Merkle-Damgard已破解文件校验(非安全场景)
SHA-1160 位512 位Merkle-Damgard已破解已弃用
SHA-256256 位512 位Merkle-Damgard安全通用安全哈希
SHA-512512 位1024 位Merkle-Damgard安全高安全性需求
SHA-3-256256 位1088 位海绵结构安全抗长度扩展攻击场景
SM3256 位512 位Merkle-Damgard安全国密体系

31.5.2 性能参考

算法吞吐量(近似)适用平台
MD5最快非安全场景
SHA-1已弃用
SHA-256中等通用
SHA-51264位平台快,32位平台慢64位服务器
SHA-3-256较慢安全要求极高
SM3中等国密合规

选择建议:对于新项目,推荐使用 SHA-256 或 SHA-3-256。如果需要符合中国国密标准,则使用 SM3。MD5 和 SHA-1 仅用于向后兼容或非安全场景。


31.6 哈希长度扩展攻击及防御(HMAC)

31.6.1 长度扩展攻击原理

基于 Merkle-Damgard 结构的哈希算法(MD5、SHA-1、SHA-256、SM3)存在一个固有的安全缺陷——长度扩展攻击(Length Extension Attack)

攻击原理如下:给定消息 $M$ 和其哈希值 $H(M) = CV_k$,攻击者无需知道 $M$ 的具体内容,就能计算出 $M | \text{padding} | M’$ 的哈希值,其中 $M’$ 是攻击者追加的任意数据。

这是因为 Merkle-Damgard 结构的最终输出就是压缩函数的中间状态,攻击者可以将这个中间状态作为新的初始值,继续追加数据进行计算。

数学表达:

$$ H(M | \text{pad}(M) | M’) = f(H(M), M’) $$

这意味着如果系统使用 $H(\text{secret} | \text{data})$ 作为认证令牌,攻击者可以在不知道 secret 的情况下,伪造新的有效令牌。

31.6.2 HMAC 防御

HMAC(Hash-based Message Authentication Code)是防御长度扩展攻击的标准方案。HMAC 的计算公式为:

$$ \text{HMAC}(K, m) = H\Big((K’ \oplus \text{opad}) | H\big((K’ \oplus \text{ipad}) | m\big)\Big) $$

其中:

  • $K$ 为密钥,$K’$ 为密钥经过填充/哈希后的结果
  • $\text{ipad} = \text{0x36}$ 重复 $B$ 次($B$ 为哈希块大小)
  • $\text{opad} = \text{0x5c}$ 重复 $B$ 次

HMAC 对哈希算法进行了两次封装,使得攻击者即使知道 $H(K | m)$ 的值,也无法构造出有效的 $H(K | m | m’)$。

31.6.3 Rust HMAC 代码示例

#![allow(unused)]
fn main() {
use hmac::{Hmac, Mac};
use sha2::Sha256;

type HmacSha256 = Hmac<Sha256>;

#[test]
fn hmac_sha256_example() {
    let key = b"my_secret_key_12345";
    let message = b"important data to authenticate";

    // 创建 HMAC-SHA256
    let mut mac = HmacSha256::new_from_slice(key)
        .expect("HMAC can take key of any size");

    mac.update(message);
    let result = mac.finalize();
    let code_bytes = result.into_bytes();

    println!("HMAC-SHA256: {:x}", code_bytes);

    // 验证 HMAC
    let mut verifier = HmacSha256::new_from_slice(key).unwrap();
    verifier.update(message);
    verifier.verify_slice(&code_bytes).expect("HMAC verification failed");
    println!("HMAC 验证通过!");
}
}

注意:SHA-3(Keccak)基于海绵结构,天然免疫长度扩展攻击。如果不需要与现有系统兼容,使用 SHA-3 可以避免 HMAC 的额外开销。


31.7 应用场景

31.7.1 数据完整性验证

下载文件时,同时验证下载的文件是否完整。发布方提供文件的哈希值,下载方计算文件哈希后进行比对:

#![allow(unused)]
fn main() {
use sha2::{Sha256, Digest};
use std::fs::File;
use std::io::Read;

/// 验证文件完整性
fn verify_file_integrity(path: &str, expected_hash: &str) -> bool {
    let mut hasher = Sha256::new();
    const BUFFER_LEN: usize = 8192;
    let mut buffer = [0u8; BUFFER_LEN];

    if let Ok(mut file) = File::open(path) {
        loop {
            match file.read(&mut buffer) {
                Ok(0) => break,
                Ok(n) => hasher.update(&buffer[..n]),
                Err(_) => return false,
            }
        }
    }

    let computed = format!("{:x}", hasher.finalize());
    computed == expected_hash.to_lowercase()
}

#[test]
fn test_file_integrity() {
    // 假设从发布网站获取的 SHA-256 哈希值
    let expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
    let is_valid = verify_file_integrity("downloaded_file.bin", expected);
    println!("文件完整性验证: {}", if is_valid { "通过" } else { "失败" });
}
}

31.7.2 数据防篡改与数字签名

后台服务对请求报文的签名进行验签,常见的场景有调用阿里云、腾讯云等云平台的服务。

典型的签名流程:

  1. 将所有请求参数按字典序排列
  2. 拼接成 key1=value1&key2=value2&... 的字符串
  3. 在末尾追加密钥
  4. 计算拼接字符串的哈希值作为签名
#![allow(unused)]
fn main() {
use sha2::{Sha256, Digest};
use std::collections::BTreeMap;

/// 生成请求签名
fn generate_sign(params: &BTreeMap<String, String>, app_secret: &str) -> String {
    // 1. 按字典序排列参数
    let mut sorted_pairs: Vec<String> = params.iter()
        .filter(|(_, v)| !v.is_empty())  // 过滤空值
        .map(|(k, v)| format!("{}={}", k, v))
        .collect();
    sorted_pairs.sort();

    // 2. 拼接字符串
    let query_string = sorted_pairs.join("&");

    // 3. 追加密钥
    let string_to_sign = format!("{}&key={}", query_string, app_secret);

    // 4. 计算哈希
    let mut hasher = Sha256::new();
    hasher.update(string_to_sign.as_bytes());
    format!("{:x}", hasher.finalize())
}

#[test]
fn test_sign_generation() {
    let mut params = BTreeMap::new();
    params.insert("app_id".to_string(), "12345".to_string());
    params.insert("timestamp".to_string(), "1700000000".to_string());
    params.insert("nonce".to_string(), "abc123".to_string());
    params.insert("amount".to_string(), "100.00".to_string());

    let sign = generate_sign(&params, "my_app_secret");
    println!("请求签名: {}", sign);
}
}

31.7.3 密码存储(配合盐值)

密码绝对不能以明文形式存储在数据库中。正确的做法是使用专门的密码哈希算法(如 bcrypt、Argon2)对密码进行哈希处理,并配合随机盐值(Salt)。

为什么不能直接用 SHA-256 存储密码?

  • SHA-256 计算速度太快,攻击者可以每秒尝试数十亿次
  • 相同密码产生相同哈希值,容易被彩虹表攻击
  • 没有内置盐值机制

推荐使用 bcrypt 或 Argon2

#![allow(unused)]
fn main() {
// Cargo.toml 依赖:
// bcrypt = "0.15"

#[test]
fn password_hashing() {
    use bcrypt::{hash, verify, DEFAULT_COST};

    // 注册时:对密码进行哈希
    let password = "my_secure_password_123";
    let hashed = hash(password, DEFAULT_COST).unwrap();
    println!("密码哈希: {}", hashed);
    // 输出类似: $2b$12$xxxxx...

    // 登录时:验证密码
    let is_valid = verify(password, &hashed).unwrap();
    println!("密码验证: {}", if is_valid { "成功" } else { "失败" });

    let is_wrong = verify("wrong_password", &hashed).unwrap();
    println!("错误密码验证: {}", if is_wrong { "成功" } else { "失败" });
}
}

盐值(Salt)的作用:每个用户的密码使用不同的随机盐值进行哈希,即使两个用户使用相同的密码,其哈希值也完全不同,从而有效防御彩虹表攻击。

31.7.4 区块链

哈希算法是区块链技术的基石,在区块链中有以下关键应用:

  1. 区块哈希:每个区块的头部包含前一个区块的哈希值,形成链式结构 $$H(\text{Block}_i) = H(\text{prev_hash} | \text{transactions} | \text{timestamp} | \text{nonce})$$

  2. 工作量证明(Proof of Work):矿工需要不断调整 nonce 值,使得区块哈希值小于目标值 $$H(\text{block_header}) < \text{target}$$

  3. 交易哈希:每笔交易也有自己的哈希值,用于唯一标识和验证

  4. Merkle 树:将所有交易的哈希值组织成 Merkle 树,实现高效的交易验证

#![allow(unused)]
fn main() {
use sha2::{Sha256, Digest};

/// 简化的区块结构
#[derive(Debug, Clone)]
struct Block {
    index: u64,
    timestamp: u64,
    data: String,
    previous_hash: String,
    nonce: u64,
}

impl Block {
    fn calculate_hash(&self) -> String {
        let input = format!("{}{}{}{}{}", 
            self.index, self.timestamp, self.data, 
            self.previous_hash, self.nonce);
        let mut hasher = Sha256::new();
        hasher.update(input.as_bytes());
        format!("{:x}", hasher.finalize())
    }

    /// 简单的工作量证明:找到 nonce 使得哈希值以 "0000" 开头
    fn mine(&mut self, difficulty: usize) {
        let target = "0".repeat(difficulty);
        loop {
            let hash = self.calculate_hash();
            if hash.starts_with(&target) {
                println!("挖矿成功!nonce={}, hash={}", self.nonce, hash);
                break;
            }
            self.nonce += 1;
        }
    }
}

#[test]
fn simple_blockchain() {
    let genesis = Block {
        index: 0,
        timestamp: 1700000000,
        data: "Genesis Block".to_string(),
        previous_hash: "0".repeat(64),
        nonce: 0,
    };

    let mut block = Block {
        index: 1,
        timestamp: 1700000100,
        data: "Transfer 1 BTC to Alice".to_string(),
        previous_hash: genesis.calculate_hash(),
        nonce: 0,
    };

    println!("开始挖矿(难度=4)...");
    block.mine(4);
}
}

31.8 Rust 哈希库推荐

31.8.1 RustCrypto 生态(推荐)

RustCrypto: Hashes Collection of cryptographic hash functions written in pure Rust

RustCrypto 是 Rust 生态中最活跃、最权威的密码学库集合,所有实现均为纯 Rust 编写,经过严格审计:

Crate算法版本
md-5MD5最新
sha1SHA-1最新
sha2SHA-224/256/384/512最新
sha3SHA-3 (Keccak)最新
blake2BLAKE2b/BLAKE2s最新
digest通用 Digest trait最新

Cargo.toml 配置

[dependencies]
sha2 = "0.10"
sha3 = "0.10"
md-5 = "0.10"
digest = "0.10"
hmac = "0.12"

31.8.2 其他推荐库

用途说明
bcrypt密码哈希基于 Blowfish,广泛用于 Web 应用
argon2密码哈希密码哈希竞赛冠军,抗 GPU/ASIC 攻击
pbkdf2密钥派生基于 HMAC 的密钥派生函数
sm3国密 SM3SM3 密码杂凑算法的 Rust 实现
ring通用密码学Google 维护,C 后端,性能优秀

31.8.3 统一 Digest 接口

RustCrypto 的 digest crate 提供了统一的 Digest trait,所有哈希算法都实现该 trait,使得代码可以在不同算法间轻松切换:

#![allow(unused)]
fn main() {
use digest::{Digest, Output};
use sha2::Sha256;
use sha3::Sha3_256;

/// 通用哈希计算函数,支持任何实现 Digest trait 的哈希算法
fn compute_hash<D: Digest>(data: &[u8]) -> Output<D> {
    let mut hasher = D::new();
    hasher.update(data);
    hasher.finalize()
}

#[test]
fn generic_hash() {
    let data = b"hello world";

    let sha256_result = compute_hash::<Sha256>(data);
    println!("SHA-256: {:x}", sha256_result);

    let sha3_result = compute_hash::<Sha3_256>(data);
    println!("SHA3-256: {:x}", sha3_result);
}
}

31.9 常用算法汇总:MD5、SHA2、SHA3

#![allow(unused)]
fn main() {
extern crate chrono;
extern crate crypto;

use crypto::digest::Digest;
use crypto::md5::Md5;
use crypto::sha2::Sha256;
use crypto::sha3::Sha3;

#[test]
fn rust_crypt() {
    let mut sh = Md5::new();
    sh.input_str("123456");
    let md5_str = sh.result_str();
    println!("md5_str:{}", md5_str);


    let mut sha256 = Sha256::new();
    sha256.input_str("helloworld");
    let hex1 = sha256.result_str();
    println!("hex1:{}", hex1);
    
    // create a SHA3-256 object
    let mut hasher = Sha3::sha3_256();
    // write input message
    hasher.input_str("helloworld");
    // read hash digest
    //    let res = hex.from_hex().unwrap();
    let hex2 = hasher.result_str();
    println!("hex2:{}", hex2);
}
}
$ echo -n "123456" | openssl md5 
(stdin)= e10adc3949ba59abbe56e057f20f883e

$ echo -n "helloworld" | openssl sha256
(stdin)= 936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af

$ echo -n "helloworld" | openssl sha3-256
(stdin)= 92dad9443e4dd6d70a7f11872101ebff87e21798e4fbb26fa4bf590eb440e71b

31.10 总结

核心知识点

知识点要点
哈希函数本质单向函数:$H: {0,1}^* \rightarrow {0,1}^n$,不可逆
三大安全性质抗原像、抗第二原像、抗碰撞(强度递增)
雪崩效应输入微小变化导致输出剧烈变化
MD5128位,已破解,仅用于非安全场景
SHA-1160位,已破解,已弃用
SHA-256256位,SHA-2家族,目前安全,广泛使用
SHA-512512位,64位平台性能优秀
SHA-3海绵结构,天然免疫长度扩展攻击
SM3国密标准,256位,与 SHA-256 同级安全
长度扩展攻击Merkle-Damgard 结构固有缺陷
HMAC使用 HMAC 防御长度扩展攻击
密码存储使用 bcrypt/Argon2,不要直接用 SHA-256
区块链哈希是区块链的基石(链式结构、工作量证明)

算法选择决策树

需要密码哈希? ─── 是 ──→ 使用 bcrypt 或 Argon2
     │
     否
     │
需要国密合规? ─── 是 ──→ 使用 SM3
     │
     否
     │
需要抗长度扩展攻击? ─── 是 ──→ 使用 SHA-3
     │
     否
     │
通用安全场景 ──→ 使用 SHA-256 或 SHA-512
     │
非安全场景(校验等)──→ 使用 MD5

31.11 练习题

练习一:基础概念

  1. 请解释哈希函数的三大安全性质(抗原像、抗第二原像、抗碰撞),并说明它们之间的蕴含关系。
  2. 为什么 128 位哈希的碰撞安全强度只有 64 位?请用生日悖论解释。
  3. MD5 和 SHA-1 为什么不再安全?它们各自是在何时被破解的?

练习二:编程实践

  1. 编写一个 Rust 函数,接收文件路径和算法名称(“md5”、“sha256”、“sha3-256”),返回对应的哈希值。要求使用 RustCrypto 的 digest trait 实现泛型编程。

  2. 编写一个 Rust 程序,对比同一文件在不同哈希算法下的计算耗时(MD5、SHA-256、SHA-512、SHA3-256),输出性能对比表格。

  3. 使用 hmacsha2 crate 实现 API 请求签名功能,模拟支付宝/微信支付的签名流程。

练习三:安全分析

  1. 假设一个系统使用 H(secret_key || user_input) 作为认证令牌,请分析其存在的安全风险,并给出修复方案。

  2. 为什么密码存储应该使用 bcrypt/Argon2 而不是 SHA-256?请从计算速度、盐值机制、抗暴力破解等角度分析。

  3. 在区块链中,如果两个矿工同时找到满足难度目标的 nonce,会发生什么?请分析区块链如何处理这种情况。

练习四:扩展思考

  1. SHA-3 为什么要采用与 SHA-2 完全不同的海绵结构?如果 SHA-2 也被破解了,SHA-3 的安全性是否会受到影响?

  2. 比特币使用 SHA-256 进行双重哈希(SHA256(SHA256(x))),请分析这种设计的原因。

第四十七变 认证

七十二变,变的是形;真身难辨,靠的是证。

认证之道,在于确认“你是谁“——从古老的口令到现代的生物特征,从单点信任到多因素验证,人类在身份验证的道路上不断探索更安全、更便捷的方案。

47.1 认证的基本概念

47.1.1 身份识别与身份验证

在信息安全领域,身份识别(Identification)身份验证(Authentication) 是两个密切相关但本质不同的概念:

  • 身份识别:用户声称自己是谁,提供标识符(如用户名、邮箱、身份证号)。这是一个单向声明的过程。
  • 身份验证:系统验证用户的声明是否属实,确认该用户确实是其所声称的那个人。

类比现实场景:你走进一栋大楼,对保安说“我是张三“——这是身份识别;保安查看你的工作证并与本人比对——这是身份验证。只有两者结合,才能建立可信的身份确认。

47.1.2 认证、授权与审计

安全领域常提及“AAA“框架:

缩写全称含义核心问题
AAuthentication(认证)验证用户身份你是谁?
AAuthorization(授权)决定用户能做什么你能做什么?
AAccounting/Auditing(审计)记录用户做了什么你做了什么?

这三者构成完整的安全访问控制链条:先确认身份,再分配权限,最后记录行为。本章聚焦认证,下一章将深入授权。

47.2 认证因素

认证因素(Authentication Factors)是用于验证身份的凭据类别。根据NIST(美国国家标准与技术研究院)的定义,认证因素分为以下几类:

47.2.1 知识因素(Something You Know)

用户知道的信息,是最传统、最广泛使用的认证方式:

  • 密码/口令:字符串形式的秘密
  • PIN码:数字形式的短密码
  • 安全问题:如“你的第一只宠物叫什么名字“

优点:实现简单,无需额外硬件。 缺点:可被猜测、窃取、社会工程学攻击;用户倾向于选择弱密码或在多站点复用。

47.2.2 持有因素(Something You Have)

用户拥有的物理设备:

  • 硬件令牌:如RSA SecurID、YubiKey
  • 智能卡:嵌入芯片的卡片(如银行卡、身份证)
  • 手机:接收短信验证码或推送通知
  • U2F/FIDO2安全密钥:基于公钥密码学的硬件设备

优点:难以远程窃取,与知识因素结合可大幅提升安全性。 缺点:可能丢失、损坏或被盗;需要携带额外设备。

47.2.3 生物因素(Something You Are)

用户固有的生理或行为特征:

  • 生理特征:指纹、人脸、虹膜、声纹、掌静脉
  • 行为特征:打字节奏、鼠标移动模式、签名动态

优点:难以伪造,用户无需记忆。 缺点:一旦泄露无法更换(你无法换一根手指);存在隐私争议;可能受环境因素影响。

47.2.4 位置因素(Somewhere You Are)

基于用户的地理位置:

  • IP地址:判断请求来源是否可信
  • GPS定位:移动设备的物理位置
  • 网络环境:是否来自公司内网

优点:可检测异常登录(如短时间内跨洲访问)。 缺点:VPN和代理可伪造位置;移动设备位置变化频繁。

47.2.5 行为因素(Something You Do)

用户的行为模式:

  • 设备指纹:操作系统、浏览器、屏幕分辨率等组合
  • 操作习惯:点击模式、滑动轨迹
  • 时间模式:通常的登录时间段

这类因素常用于风险自适应认证(Risk-based Authentication),在不打扰用户的情况下评估登录风险。

47.3 密码认证

47.3.1 密码存储的演进

密码绝不能以明文存储。存储方式的演进反映了安全意识的提升:

  1. 明文存储:最危险的做法,数据库泄露即全部暴露。
  2. 哈希存储:存储密码的哈希值,但相同密码产生相同哈希,易被彩虹表攻击。
  3. 加盐哈希:为每个密码附加随机盐值,再计算哈希,有效防御彩虹表。
  4. 自适应哈希:计算成本可配置的哈希算法(如bcrypt、scrypt、Argon2),抵抗硬件加速暴力破解。

47.3.2 盐(Salt)与胡椒(Pepper)

盐(Salt) 是每个密码独有的随机字符串,与密码拼接后计算哈希:

hash = H(password || salt)
  • 每个用户有独立的盐值
  • 盐值与哈希结果一同存储
  • 即使两个用户密码相同,存储的哈希也不同

胡椒(Pepper) 是全局的秘密值,存储在应用配置中而非数据库:

hash = H(password || salt || pepper)
  • 所有用户共享同一个胡椒值
  • 胡椒不存储在数据库中
  • 即使数据库泄露,没有胡椒也无法验证密码

47.3.3 现代密码哈希算法

算法设计目标特点推荐场景
bcrypt基于Blowfish密码自适应成本因子,广泛支持传统应用兼容
scrypt内存困难型高内存消耗,抗ASIC加密货币、高安全需求
Argon2密码哈希竞赛 winner2015年Password Hashing Competition冠军,可配置内存、时间和并行度现代应用首选

Argon2有三个变体:

  • Argon2d:抵抗GPU破解,适合加密货币
  • Argon2i:抵抗侧信道攻击,适合密码哈希
  • Argon2id:两者兼顾,推荐使用

47.3.4 Rust实现:Argon2密码哈希

use argon2::{
    password_hash::{
        rand_core::OsRng,
        PasswordHash, PasswordHasher, PasswordVerifier, SaltString
    },
    Argon2,
};

/// 生成密码哈希
fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
    let salt = SaltString::generate(&mut OsRng);
    let argon2 = Argon2::default();
    let password_hash = argon2
        .hash_password(password.as_bytes(), &salt)?
        .to_string();
    Ok(password_hash)
}

/// 验证密码
fn verify_password(password: &str, hash: &str) -> Result<bool, argon2::password_hash::Error> {
    let parsed_hash = PasswordHash::new(hash)?;
    let argon2 = Argon2::default();
    match argon2.verify_password(password.as_bytes(), &parsed_hash) {
        Ok(()) => Ok(true),
        Err(argon2::password_hash::Error::Password) => Ok(false),
        Err(e) => Err(e),
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let password = "my_secure_password_123";
    
    // 注册时:生成哈希
    let hash = hash_password(password)?;
    println!("密码哈希: {}", hash);
    // 输出示例: $argon2id$v=19$m=65536,t=3,p=4$...$
    
    // 登录时:验证密码
    let is_valid = verify_password(password, &hash)?;
    println!("验证结果: {}", is_valid);
    
    let is_invalid = verify_password("wrong_password", &hash)?;
    println!("错误密码验证: {}", is_invalid);
    
    Ok(())
}

Cargo.toml 依赖:

[dependencies]
argon2 = "0.5"

47.3.5 密码策略

良好的密码策略应平衡安全性与可用性:

  • 最小长度:至少12个字符(NIST建议)
  • 复杂度要求:大小写字母、数字、符号的组合
  • 常见密码检查:拒绝已泄露的密码(如Have I Been Pwned数据库)
  • 密码历史:防止近期密码复用
  • 锁定策略:多次失败后暂时锁定账户
  • 密码提示:不提供可能暴露密码的提示

现代趋势是鼓励使用密码短语(Passphrase) 而非复杂短密码,例如“correct-horse-battery-staple“比“Tr0ub4dor&3“更安全且易记。

47.4 多因素认证

47.4.1 MFA与2FA

多因素认证(Multi-Factor Authentication, MFA) 要求用户提供两种或以上的不同类别认证因素。最常见的组合是:

  • 知识因素 + 持有因素(密码 + 手机验证码)
  • 知识因素 + 生物因素(密码 + 指纹)

双因素认证(Two-Factor Authentication, 2FA) 是MFA的特例,恰好使用两个因素。

MFA显著提升了安全性:即使密码泄露,攻击者仍需要第二因素才能通过认证。

47.4.2 OTP:一次性密码

一次性密码(One-Time Password, OTP) 是MFA中持有因素的常见实现,分为两类:

HOTP:基于计数器

HOTP(HMAC-based One-Time Password, RFC 4226)使用计数器生成密码:

HOTP(K, C) = Truncate(HMAC-SHA-1(K, C))

其中K是共享密钥,C是计数器值。每次验证后计数器递增。

缺点:需要同步计数器,若用户多次生成未使用验证码,会导致不同步。

TOTP:基于时间

TOTP(Time-based One-Time Password, RFC 6238)是HOTP的扩展,使用当前时间作为计数器:

TOTP(K, T) = HOTP(K, T)
T = (Current Unix time - T0) / X
  • T0是起始时间(通常为0)
  • X是时间步长(通常为30秒)

TOTP更实用,无需同步计数器,但要求设备时间大致准确。

47.4.3 TOTP实现原理

TOTP生成过程:

  1. 密钥共享:服务器生成随机密钥,通过安全通道(如二维码)传递给认证器应用
  2. 时间对齐:双方使用UTC时间,按30秒窗口对齐
  3. HMAC计算HMAC-SHA-1(key, time_counter)
  4. 截断(Truncate):从20字节的HMAC结果中提取4字节动态码
  5. 取模dynamic_code % 10^digits(通常6位数字)

验证时,服务器通常检查当前时间窗口及前后各一个窗口(共3个窗口,约90秒),以容纳时间偏差。

47.4.4 Rust实现:TOTP生成与验证

use totp_rs::{Algorithm, Secret, TOTP};
use qrcode::QrCode;
use qrcode::render::unicode;

fn create_totp(account: &str, issuer: &str) -> Result<TOTP, Box<dyn std::error::Error>> {
    let secret = Secret::generate_secret().to_bytes()?;
    
    let totp = TOTP::new(
        Algorithm::SHA1,    // HMAC算法
        6,                   // 验证码位数
        1,                   // 容错窗口(前后各1个时间步)
        30,                  // 时间步长(秒)
        secret,
    )?;
    
    Ok(totp)
}

fn generate_qr_code(totp: &TOTP, account: &str, issuer: &str) {
    // 生成符合Google Authenticator格式的URI
    let uri = totp.get_uri(issuer.to_string(), account.to_string());
    println!("TOTP URI: {}", uri);
    
    // 生成二维码
    let code = QrCode::new(uri).unwrap();
    let string = code.render::<unicode::Dense1x2>()
        .dark_color(unicode::Dense1x2::Light)
        .light_color(unicode::Dense1x2::Dark)
        .build();
    println!("{}", string);
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let account = "user@example.com";
    let issuer = "RustApp";
    
    // 创建TOTP
    let totp = create_totp(account, issuer)?;
    
    // 生成二维码供用户扫描
    generate_qr_code(&totp, account, issuer);
    
    // 生成当前验证码
    let token = totp.generate_current()?;
    println!("当前验证码: {}", token);
    
    // 验证用户输入的验证码
    let user_input = "123456"; // 假设用户输入
    let is_valid = totp.check(user_input, 
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)?
            .as_secs()
    );
    println!("验证结果: {}", is_valid);
    
    Ok(())
}

Cargo.toml 依赖:

[dependencies]
totp-rs = { version = "5", features = ["qr"] }

47.4.5 备份码与恢复机制

MFA必须考虑设备丢失的情况:

  • 备份码:注册时生成一组一次性使用的备用码
  • 多设备注册:允许在多个设备上配置同一TOTP
  • 替代验证:通过已验证的邮箱或手机进行身份验证后重置MFA

47.5 生物特征认证

47.5.1 指纹认证

指纹识别是最成熟的生物特征技术:

  • 采集:光学、电容、超声波传感器
  • 特征提取:识别细节特征点(ridge endings和bifurcations)
  • 匹配:将采集的模板与注册模板比对,计算相似度分数
  • 阈值决策:分数超过阈值则接受

安全性考量:指纹模板应加密存储,且不可逆推原始指纹图像。

47.5.2 人脸识别

现代人脸识别依赖深度学习:

  • 人脸检测:定位图像中的人脸区域
  • 特征提取:神经网络生成高维特征向量(如128维或512维)
  • 相似度计算:余弦相似度或欧氏距离
  • 活体检测:防止照片、视频、面具攻击

隐私风险:人脸是公开可见的生物特征,且难以更换。欧盟GDPR将其列为敏感个人数据。

47.5.3 虹膜识别

虹膜模式具有极高的唯一性和稳定性:

  • 准确性:错误接受率(FAR)可达10^-7级别
  • 稳定性:虹膜模式终身不变
  • 非接触:可在1米外采集

缺点:设备成本高;部分眼疾或手术会影响识别;用户接受度较低。

47.5.4 生物特征认证的Rust生态

Rust在生物特征领域的生态尚在发展中。对于Web应用,通常通过操作系统API(如Windows Hello、Apple Touch ID、WebAuthn)间接支持生物特征认证。

#![allow(unused)]
fn main() {
// WebAuthn示例(webauthn-rs库)
use webauthn_rs::prelude::*;

fn setup_webauthn() -> Result<Webauthn, WebauthnError> {
    let rp_id = "example.com";
    let rp_origin = Url::parse("https://example.com")?;
    
    let builder = WebauthnBuilder::new(rp_id, &rp_origin)?;
    let webauthn = builder.build()?;
    Ok(webauthn)
}
}

47.6 单点登录

47.6.1 SSO的概念

单点登录(Single Sign-On, SSO) 允许用户使用一组凭据访问多个相互信任的应用系统。其核心优势:

  • 用户体验:一次登录,处处通行
  • 安全管理:集中管理身份和权限
  • 审计追踪:统一的登录日志

47.6.2 OAuth 2.0

OAuth 2.0(RFC 6749)是授权框架,常被用于实现SSO:

角色定义

  • 资源所有者(Resource Owner):用户本人
  • 客户端(Client):请求访问的第三方应用
  • 授权服务器(Authorization Server):颁发令牌
  • 资源服务器(Resource Server):托管受保护资源

授权流程

  1. 客户端引导用户到授权服务器
  2. 用户登录并同意授权
  3. 授权服务器重定向回客户端,附带授权码
  4. 客户端用授权码换取访问令牌
  5. 客户端使用访问令牌访问资源

四种授权模式

模式适用场景安全性
授权码模式(Authorization Code)服务器端应用高,支持PKCE
隐式授权(Implicit)单页应用(已废弃)
密码凭证(Password)受信任的第一方应用
客户端凭证(Client Credentials)服务间通信

47.6.3 OpenID Connect

OpenID Connect(OIDC)构建于OAuth 2.0之上,专门用于身份认证:

  • ID Token:JWT格式的身份令牌,包含用户声明(claims)
  • UserInfo Endpoint:获取用户详细信息的API
  • Discovery:自动发现配置端点

OIDC标准化了“用Google/微信/GitHub登录“的实现方式。

47.6.4 SAML

安全断言标记语言(Security Assertion Markup Language, SAML) 是企业级SSO的老牌标准:

  • 基于XML的断言交换
  • 身份提供者(IdP)与服务提供者(SP)之间的信任关系
  • 常用于企业应用(如Office 365、Salesforce的SSO集成)

相比OIDC,SAML配置更复杂,但在传统企业环境中仍广泛使用。

47.6.5 Rust实现:JWT处理

use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,        // 主题(用户ID)
    iss: String,        // 签发者
    aud: String,        // 受众
    exp: usize,         // 过期时间
    iat: usize,         // 签发时间
    #[serde(skip_serializing_if = "Option::is_none")]
    name: Option<String>,
}

fn current_timestamp() -> usize {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs() as usize
}

fn create_jwt(user_id: &str, secret: &str) -> Result<String, jsonwebtoken::errors::Error> {
    let now = current_timestamp();
    let claims = Claims {
        sub: user_id.to_string(),
        iss: "rust-app".to_string(),
        aud: "rust-client".to_string(),
        iat: now,
        exp: now + 3600, // 1小时后过期
        name: Some("张三".to_string()),
    };
    
    encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(secret.as_bytes()),
    )
}

fn verify_jwt(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
    let mut validation = Validation::default();
    validation.set_issuer(&["rust-app"]);
    validation.set_audience(&["rust-client"]);
    
    let token_data = decode::<Claims>(
        token,
        &DecodingKey::from_secret(secret.as_bytes()),
        &validation,
    )?;
    
    Ok(token_data.claims)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let secret = "your-256-bit-secret-key-here!!!";
    let user_id = "user_12345";
    
    // 生成JWT
    let token = create_jwt(user_id, secret)?;
    println!("生成的JWT: {}", token);
    
    // 验证JWT
    let claims = verify_jwt(&token, secret)?;
    println!("验证通过,用户: {:?}", claims);
    
    // 尝试用错误密钥验证
    let result = verify_jwt(&token, "wrong-secret");
    match result {
        Ok(_) => println!("不应成功"),
        Err(e) => println!("验证失败(预期): {}", e),
    }
    
    Ok(())
}

Cargo.toml 依赖:

[dependencies]
jsonwebtoken = "9"
serde = { version = "1", features = ["derive"] }

47.7 认证最佳实践

47.7.1 安全设计原则

  1. 纵深防御:不依赖单一安全机制
  2. 最小信任:验证所有输入,不信任任何来源
  3. 失败安全:认证失败时默认拒绝访问
  4. 安全默认值:默认启用最强安全策略

47.7.2 常见攻击与防御

攻击类型描述防御措施
暴力破解尝试所有可能的密码组合速率限制、账户锁定、CAPTCHA
字典攻击使用常见密码列表尝试密码强度策略、泄露密码检查
彩虹表预计算的哈希值查找表加盐哈希、自适应哈希
中间人拦截通信窃取凭据TLS/HTTPS、证书固定
重放攻击重复发送截获的认证信息时间戳、随机数、一次性令牌
会话劫持窃取会话标识符HttpOnly Cookie、短过期时间、绑定IP
钓鱼攻击伪造登录页面骗取凭据多因素认证、用户教育

47.7.3 会话管理

认证成功后,系统需要维持会话状态:

  • 会话标识符:随机生成的高熵字符串
  • 存储方式:服务器端会话存储或客户端JWT
  • 过期策略:绝对过期时间 + 空闲超时
  • 安全传输:仅通过HTTPS传输
  • Cookie属性:Secure、HttpOnly、SameSite

47.8 本章总结

主题核心要点
认证基础身份识别是声明,身份验证是确认;认证、授权、审计构成AAA框架
认证因素知识/持有/生物/位置/行为五类因素;多因素组合提升安全性
密码认证使用Argon2id等自适应哈希;盐值防彩虹表,胡椒防数据库泄露
MFA/2FATOTP基于时间窗口,广泛支持;HOTP基于计数器,需同步
生物特征指纹/人脸/虹膜各有优劣;注意隐私保护和活体检测
SSOOAuth 2.0授权框架,OIDC身份层,SAML企业标准
Rust实现argon2密码哈希、totp-rs验证码、jsonwebtokenJWT处理

47.9 练习建议

  1. 密码哈希实践:实现一个用户注册/登录系统,使用Argon2存储密码,比较不同成本参数对性能的影响。

  2. TOTP认证器:开发命令行TOTP生成器,支持从标准输入读取密钥,每30秒输出新验证码。与Google Authenticator交叉验证。

  3. JWT中间件:为Actix-web或Axum编写JWT认证中间件,实现令牌签发、验证和刷新机制。

  4. 多因素认证流程:设计完整的MFA注册和验证流程,包括二维码生成、备份码、设备管理功能。

  5. 安全审计:对开源Rust认证库进行代码审计,检查是否遵循OWASP认证安全建议。


认证如守门,门后有万千世界。门把守不严,世界便危如累卵;门把守过严, legitimate 访客亦寸步难行。在Rust的内存安全与类型系统之上,构建坚固的认证体系,方能让系统在便捷与安全之间找到最佳平衡。

第四十八变 授权

知其为谁,方能定其可为。认证辨明身份,授权划定边界。

授权之道,在于回答“你能做什么“——从简单的访问控制列表到复杂的属性策略引擎,从单一系统的权限管理到分布式服务的统一授权,授权机制决定了资源的安全边界。

48.1 授权的基本概念

48.1.1 认证与授权的区别

认证和授权是安全体系中紧密相连却截然不同的两个环节:

维度认证(Authentication)授权(Authorization)
核心问题你是谁?你能做什么?
发生时机访问前,确认身份认证后,决定权限
依赖关系不依赖授权依赖认证结果
类比机场安检核实身份证登机牌决定你能进入哪个舱位
失败表现“无法确认你的身份”“你没有权限执行此操作”

一个经典的安全反模式是:系统仅完成认证,却将授权决策交给前端控制。攻击者可以轻易绕过前端限制,直接调用后端API。因此,授权检查必须在服务端强制执行

48.1.2 授权的核心要素

授权决策通常涉及三个核心要素:

  • 主体(Subject):请求访问的实体,通常是已认证的用户或服务
  • 资源(Resource):被访问的对象,如文件、数据库记录、API端点
  • 操作(Action):主体希望对资源执行的动作,如读、写、删除、执行

授权系统回答的问题是:“主体是否可以对资源执行操作?”

48.2 访问控制模型

访问控制模型定义了如何管理和执行授权决策。从简单到复杂,主要有四种经典模型。

48.2.1 DAC:自主访问控制

自主访问控制(Discretionary Access Control, DAC) 允许资源的所有者自主决定谁可以访问其资源。

核心特征

  • 资源拥有者具有完全控制权
  • 可以授予或撤销其他主体的访问权限
  • 权限可以传递(如Linux文件系统中的ACL)

典型实现

  • Unix/Linux文件权限(rwx/UGO模型)
  • Windows文件共享权限
  • 云存储的分享链接
#![allow(unused)]
fn main() {
// 简化的DAC权限检查示例
struct File {
    owner: String,
    permissions: u16, // rwxrwxrwx
}

impl File {
    fn can_access(&self, user: &User, action: Action) -> bool {
        if user.id == self.owner {
            return self.check_owner_perm(action);
        }
        if user.groups.iter().any(|g| self.group_has_access(g)) {
            return self.check_group_perm(action);
        }
        self.check_other_perm(action)
    }
}
}

优点:灵活,符合直觉。 缺点:权限分散,难以审计;权限传递可能导致意外泄露(特洛伊木马问题)。

48.2.2 MAC:强制访问控制

强制访问控制(Mandatory Access Control, MAC) 由系统管理员统一制定访问策略,资源所有者无权修改。

核心特征

  • 系统全局安全策略强制执行
  • 每个主体和资源都有安全标签
  • 访问决策基于标签比较,而非用户意愿

典型实现

  • SELinux:为Linux进程和文件添加安全上下文
  • AppArmor:基于路径的强制访问控制
  • 军事分级系统:绝密 > 机密 > 秘密 > 公开

Bell-LaPadula模型(保密性):

  • 不上读(No Read Up):主体不能读取更高安全级别的资源
  • 不下写(No Write Down):主体不能向更低安全级别写入

Biba模型(完整性):

  • 不下读(No Read Down):防止低完整性数据污染高完整性主体
  • 不上写(No Write Up):防止高完整性主体被低完整性数据影响
#![allow(unused)]
fn main() {
// 简化的MAC标签检查
#[derive(PartialEq, Eq, PartialOrd, Ord)]
enum SecurityLevel {
    Public,
    Internal,
    Confidential,
    Secret,
}

struct MacSubject {
    clearance: SecurityLevel,
}

struct MacResource {
    classification: SecurityLevel,
}

fn can_read(subject: &MacSubject, resource: &MacResource) -> bool {
    subject.clearance >= resource.classification // 不上读
}

fn can_write(subject: &MacSubject, resource: &MacResource) -> bool {
    subject.clearance <= resource.classification // 不下写
}
}

优点:安全性高,适合高安全需求环境。 缺点:灵活性差,配置复杂,可能影响正常业务。

48.2.3 RBAC:基于角色的访问控制

基于角色的访问控制(Role-Based Access Control, RBAC) 是目前企业应用中最广泛使用的模型。

核心概念

  • 用户(User):系统的使用者
  • 角色(Role):一组权限的集合,代表组织中的职位或职责
  • 权限(Permission):对资源执行操作的能力
  • 会话(Session):用户激活的一组角色

RBAC层级

层级名称新增特性
RBAC0核心RBAC用户-角色-权限基本关联
RBAC1层级RBAC角色继承(Senior Role继承Junior Role的权限)
RBAC2约束RBAC职责分离约束(互斥角色、基数约束)
RBAC3统一RBACRBAC1 + RBAC2的完整组合

职责分离(Separation of Duties, SoD)

  • 静态SoD:用户不能同时被分配互斥角色(如会计和审计)
  • 动态SoD:用户可同时拥有互斥角色,但不能在同一会话中同时激活
#![allow(unused)]
fn main() {
// RBAC核心数据结构
use std::collections::{HashMap, HashSet};

struct RbacSystem {
    user_roles: HashMap<String, HashSet<String>>,      // user -> roles
    role_permissions: HashMap<String, HashSet<String>>, // role -> permissions
    role_hierarchy: HashMap<String, HashSet<String>>,   // role -> parent roles
}

impl RbacSystem {
    fn user_permissions(&self, user: &str) -> HashSet<String> {
        let mut perms = HashSet::new();
        if let Some(roles) = self.user_roles.get(user) {
            for role in roles {
                self.collect_role_permissions(role, &mut perms);
            }
        }
        perms
    }
    
    fn collect_role_permissions(&self, role: &str, perms: &mut HashSet<String>) {
        if let Some(direct) = self.role_permissions.get(role) {
            perms.extend(direct.iter().cloned());
        }
        if let Some(parents) = self.role_hierarchy.get(role) {
            for parent in parents {
                self.collect_role_permissions(parent, perms);
            }
        }
    }
    
    fn check_permission(&self, user: &str, permission: &str) -> bool {
        self.user_permissions(user).contains(permission)
    }
}
}

优点:简化权限管理,角色与组织结构对齐,易于审计。 缺点:角色爆炸(大量细粒度角色);难以表达基于资源属性的复杂策略。

48.2.4 ABAC:基于属性的访问控制

基于属性的访问控制(Attribute-Based Access Control, ABAC) 是最灵活、最强大的访问控制模型。

核心思想:访问决策基于主体、资源、操作和环境的属性,而非预定义的角色或标签。

四类属性

  • 主体属性:用户部门、职级、认证方式、信任等级
  • 资源属性:文件分类、所有者、创建时间、敏感等级
  • 操作属性:读、写、删除、分享
  • 环境属性:当前时间、访问位置、网络环境、设备类型

策略表达式示例

允许 如果:
  主体.部门 == "财务部"
  且 资源.类型 == "财务报表"
  且 操作 == "读取"
  且 环境.时间在 "09:00" 到 "18:00" 之间
  且 环境.位置 == "公司内网"

XACML(eXtensible Access Control Markup Language) 是ABAC的标准实现,使用XML定义策略。

#![allow(unused)]
fn main() {
// 简化的ABAC策略评估
struct AbacContext {
    subject: HashMap<String, String>,
    resource: HashMap<String, String>,
    action: String,
    environment: HashMap<String, String>,
}

struct AbacPolicy {
    rules: Vec<AbacRule>,
}

struct AbacRule {
    conditions: Vec<Box<dyn Fn(&AbacContext) -> bool>>,
    effect: Effect,
}

enum Effect {
    Permit,
    Deny,
}

impl AbacPolicy {
    fn evaluate(&self, ctx: &AbacContext) -> Effect {
        for rule in &self.rules {
            if rule.conditions.iter().all(|c| c(ctx)) {
                return rule.effect;
            }
        }
        Effect::Deny // 默认拒绝
    }
}
}

优点:极度灵活,可表达复杂业务规则;细粒度控制。 缺点:策略复杂,性能开销大,难以直观理解和审计。

48.2.5 模型对比

特性DACMACRBACABAC
灵活性极高
安全性极高
管理复杂度
审计难度
适用场景个人系统军事/政府企业应用云原生/微服务
性能

48.3 权限设计原则

48.3.1 最小权限原则

最小权限原则(Principle of Least Privilege, PoLP) 要求每个主体仅拥有完成其工作所必需的最小权限集合。

实践要点

  • 默认拒绝所有访问,显式授予所需权限
  • 定期审查和回收不再需要的权限
  • 使用临时权限提升机制(如sudo),而非长期高权限
  • 服务账户按功能拆分,避免“万能账户“
#![allow(unused)]
fn main() {
// 最小权限原则示例:数据库连接按功能分离
struct ReadOnlyDbPool;
struct ReadWriteDbPool;
struct AdminDbPool;

impl ReadOnlyDbPool {
    async fn query<T>(&self, sql: &str) -> Result<Vec<T>, Error> { 
        // 只读操作
        todo!()
    }
}

impl ReadWriteDbPool {
    async fn execute(&self, sql: &str) -> Result<u64, Error> {
        // 读写操作
        todo!()
    }
}

// 普通服务只能访问只读池
struct UserService {
    db: ReadOnlyDbPool,
}

// 订单服务需要读写
struct OrderService {
    db: ReadWriteDbPool,
}
}

48.3.2 职责分离

职责分离(Separation of Duties, SoD) 要求关键操作由多个主体协作完成,防止单点滥用权限。

经典场景

  • 采购审批:申请人、审批人、验收人不能为同一人
  • 金融交易:录入员和复核员分离
  • 代码发布:开发提交、测试验证、运维部署分离
#![allow(unused)]
fn main() {
// 职责分离检查
struct SoDConstraint {
    mutually_exclusive_roles: Vec<(String, String)>,
}

impl SoDConstraint {
    fn check_assignment(&self, user_roles: &HashSet<String>) -> Result<(), String> {
        for (r1, r2) in &self.mutually_exclusive_roles {
            if user_roles.contains(r1) && user_roles.contains(r2) {
                return Err(format!("违反职责分离: {} 与 {} 不能同时拥有", r1, r2));
            }
        }
        Ok(())
    }
}
}

48.3.3 权限设计模式

ACL(访问控制列表):直接在资源上维护允许访问的主体列表。

能力列表(Capability List):主体持有可访问资源的令牌(如文件描述符、API密钥)。

策略即代码(Policy as Code):将授权策略以代码形式版本化管理,支持代码审查和自动化测试。

48.4 OAuth 2.0 授权框架

48.4.1 OAuth 2.0 概述

OAuth 2.0 是业界标准的授权协议,允许第三方应用代表用户访问资源,而无需获取用户密码。

核心解决的问题

  • 用户不希望将密码交给第三方应用
  • 用户希望细粒度控制第三方应用的访问范围
  • 用户希望随时撤销第三方应用的访问权限

48.4.2 四种授权模式

授权码模式(Authorization Code)

最安全、最常用的模式,适用于服务器端应用:

+----------+
| 资源所有者 |
|   (用户)   |
+----------+
     |
     | 1. 浏览器重定向到授权服务器
     v
+----------+                                   +---------------+
|          |--(2) 用户登录并授权--------------->|               |
|   用户    |                                   |   授权服务器   |
|   代理    |<-(3) 返回授权码(重定向到客户端)--|               |
| (浏览器)  |                                   +---------------+
+----------+                                          |
     |                                                |
     | 4. 授权码通过浏览器传递给客户端                 |
     v                                                |
+----------+                                   +---------------+
|          |--(5) 用授权码换取访问令牌------------>|               |
|   客户端   |         (直接后端通信,保密)          |   授权服务器   |
| (服务器)  |<-(6) 返回访问令牌和刷新令牌------------|               |
+----------+                                   +---------------+
     |
     | 7. 用访问令牌访问资源
     v
+---------------+
|   资源服务器   |
+---------------+

PKCE扩展(Proof Key for Code Exchange, RFC 7636): 为授权码模式增加保护层,防止授权码拦截攻击。公共客户端(如移动应用、单页应用)必须使用PKCE。

#![allow(unused)]
fn main() {
// PKCE参数生成
use rand::{distributions::Alphanumeric, Rng};
use sha2::{Sha256, Digest};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};

fn generate_pkce() -> (String, String) {
    // 生成随机code_verifier
    let verifier: String = rand::thread_rng()
        .sample_iter(&Alphanumeric)
        .take(128)
        .map(char::from)
        .collect();
    
    // 计算code_challenge = BASE64URL(SHA256(code_verifier))
    let mut hasher = Sha256::new();
    hasher.update(&verifier);
    let challenge = URL_SAFE_NO_PAD.encode(hasher.finalize());
    
    (verifier, challenge)
}
}

简化模式(Implicit)

直接从授权端点获取访问令牌,不经过授权码交换。因安全性问题,已被OAuth 2.1废弃

密码凭证模式(Resource Owner Password Credentials)

用户直接向客户端提供用户名和密码,客户端用其换取令牌。仅适用于受信任的第一方应用。

客户端凭证模式(Client Credentials)

客户端以自己的身份(而非用户身份)请求访问资源。适用于服务间通信、后台任务。

#![allow(unused)]
fn main() {
// 客户端凭证模式示例
async fn client_credentials_flow(
    token_endpoint: &str,
    client_id: &str,
    client_secret: &str,
    scope: &str,
) -> Result<TokenResponse, reqwest::Error> {
    let client = reqwest::Client::new();
    let params = [
        ("grant_type", "client_credentials"),
        ("client_id", client_id),
        ("client_secret", client_secret),
        ("scope", scope),
    ];
    
    let response = client
        .post(token_endpoint)
        .form(&params)
        .send()
        .await?
        .json::<TokenResponse>()
        .await?;
    
    Ok(response)
}

#[derive(Debug, serde::Deserialize)]
struct TokenResponse {
    access_token: String,
    token_type: String,
    expires_in: u64,
    scope: Option<String>,
}
}

48.4.3 令牌类型

  • 访问令牌(Access Token):用于访问受保护资源,通常短期有效(分钟到小时级)
  • 刷新令牌(Refresh Token):用于获取新的访问令牌,长期有效但可撤销
  • 授权码(Authorization Code):一次性凭证,用于交换访问令牌

48.4.4 范围(Scope)

Scope定义了访问令牌的权限边界:

scope = "read:users write:orders admin:settings"

客户端请求时声明所需scope,用户授权时可以看到并选择性同意。资源服务器验证令牌时检查scope是否包含所需权限。

48.5 JWT 令牌

48.5.1 JWT 结构

JSON Web Token(JWT, RFC 7519) 是一种紧凑、自包含的方式,用于在各方之间安全地传输信息。

JWT由三部分组成,用点号分隔:

xxxxx.yyyyy.zzzzz
  |      |      |
Header Payload Signature

Header(头部)

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload(载荷):声明(claims)的集合:

{
  "sub": "user_12345",
  "iss": "auth.example.com",
  "aud": "api.example.com",
  "exp": 1700000000,
  "iat": 1699996400,
  "scope": "read:users write:orders",
  "role": "admin"
}

Signature(签名)

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

48.5.2 JWT 签名算法

算法类型说明
HS256/384/512对称HMAC with SHA,密钥共享
RS256/384/512非对称RSA with SHA,私钥签名、公钥验证
ES256/384/512非对称ECDSA with SHA,更短的密钥
EdDSA非对称Ed25519,现代推荐算法

对称 vs 非对称

  • 对称(HMAC):签发和验证使用同一密钥,适合单一服务
  • 非对称(RSA/ECDSA):签发用私钥,验证用公钥,适合分布式系统(授权服务器签发,各资源服务器验证)

48.5.3 JWT 验证要点

验证JWT时,必须检查以下声明:

  • exp(过期时间):当前时间必须小于过期时间
  • nbf(生效时间):当前时间必须大于生效时间
  • iat(签发时间):用于检测令牌重放
  • iss(签发者):必须是信任的签发者
  • aud(受众):必须包含当前服务标识
  • 签名:必须使用正确的算法和密钥验证

安全警告

  • 永远不要信任客户端提供的JWT头部算法(alg: none攻击)
  • 密钥强度要足够(HS256至少256位密钥)
  • 敏感信息不要放入JWT payload(仅Base64编码,未加密)
  • 使用短过期时间,配合刷新令牌机制

48.5.4 Rust实现:JWT签发与验证

use jsonwebtoken::{
    decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation,
};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,           // 用户ID
    iss: String,           // 签发者
    aud: String,           // 受众
    exp: usize,            // 过期时间
    iat: usize,            // 签发时间
    scope: String,         // 权限范围
    #[serde(skip_serializing_if = "Option::is_none")]
    role: Option<String>,  // 角色
}

fn now() -> usize {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs() as usize
}

// 使用非对称密钥(RS256)签发JWT
fn create_jwt_rs256(
    user_id: &str,
    private_key_pem: &str,
) -> Result<String, jsonwebtoken::errors::Error> {
    let now_ts = now();
    let claims = Claims {
        sub: user_id.to_string(),
        iss: "auth-server".to_string(),
        aud: "resource-api".to_string(),
        iat: now_ts,
        exp: now_ts + 3600,
        scope: "read:users read:orders".to_string(),
        role: Some("user".to_string()),
    };
    
    let header = Header::new(Algorithm::RS256);
    let encoding_key = EncodingKey::from_rsa_pem(private_key_pem.as_bytes())?;
    
    encode(&header, &claims, &encoding_key)
}

// 使用公钥验证JWT
fn verify_jwt_rs256(
    token: &str,
    public_key_pem: &str,
) -> Result<Claims, jsonwebtoken::errors::Error> {
    let mut validation = Validation::new(Algorithm::RS256);
    validation.set_issuer(&["auth-server"]);
    validation.set_audience(&["resource-api"]);
    validation.set_required_spec_claims(&["exp", "iss", "aud"]);
    
    let decoding_key = DecodingKey::from_rsa_pem(public_key_pem.as_bytes())?;
    let token_data = decode::<Claims>(token, &decoding_key, &validation)?;
    
    Ok(token_data.claims)
}

// 检查特定权限
fn has_scope(claims: &Claims, required: &str) -> bool {
    claims.scope.split_whitespace().any(|s| s == required)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 注意:实际项目中应从安全存储读取密钥
    let private_key = include_str!("private_key.pem");
    let public_key = include_str!("public_key.pem");
    
    // 签发
    let token = create_jwt_rs256("user_123", private_key)?;
    println!("JWT: {}", token);
    
    // 验证
    let claims = verify_jwt_rs256(&token, public_key)?;
    println!("验证通过: {:?}", claims);
    
    // 权限检查
    println!("可读取用户: {}", has_scope(&claims, "read:users"));
    println!("可写入订单: {}", has_scope(&claims, "write:orders"));
    
    Ok(())
}

48.6 Rust授权生态

48.6.1 Casbin:通用授权库

Casbin 是一个强大的、开源的访问控制库,支持多种访问控制模型(ACL、RBAC、ABAC)。

核心概念

  • Model:定义访问控制模型的配置文件
  • Policy:具体的权限策略数据
  • Adapter:策略数据的持久化适配器(内存、文件、数据库)
  • Enforcer:执行授权决策的核心引擎

RBAC模型配置(model.conf)

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

策略数据(policy.csv)

p, admin, /users, GET
p, admin, /users, POST
p, user, /profile, GET
g, alice, admin
g, bob, user

48.6.2 Rust实现:Casbin RBAC

use casbin::{CoreApi, DefaultModel, Enforcer, FileAdapter, Result};

#[tokio::main]
async fn main() -> Result<()> {
    // 加载模型和策略
    let model = DefaultModel::from_file("model.conf").await?;
    let adapter = FileAdapter::new("policy.csv");
    let enforcer = Enforcer::new(model, adapter).await?;
    
    // 检查权限
    let checks = vec![
        ("alice", "/users", "GET"),    // admin角色,允许
        ("alice", "/users", "POST"),   // admin角色,允许
        ("bob", "/profile", "GET"),    // user角色,允许
        ("bob", "/users", "GET"),      // user角色,拒绝
        ("charlie", "/users", "GET"),  // 无角色,拒绝
    ];
    
    for (sub, obj, act) in &checks {
        let allowed = enforcer.enforce((sub.to_string(), obj.to_string(), act.to_string()))?;
        println!("{} 能否 {} {}? {}", sub, act, obj, allowed);
    }
    
    // 动态添加策略
    enforcer.add_policy(vec!["user".to_string(), "/orders".to_string(), "GET".to_string()]).await?;
    
    // 动态添加角色
    enforcer.add_grouping_policy(vec!["charlie".to_string(), "user".to_string()]).await?;
    
    let allowed = enforcer.enforce(("charlie", "/orders", "GET"))?;
    println!("添加角色后,charlie 能否 GET /orders? {}", allowed);
    
    Ok(())
}

Cargo.toml 依赖:

[dependencies]
casbin = "2"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

48.6.3 OAuth2-rs:OAuth 2.0客户端

#![allow(unused)]
fn main() {
use oauth2::{
    AuthUrl, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, RedirectUrl,
    Scope, TokenUrl, AuthorizationCode, TokenResponse,
};
use oauth2::basic::BasicClient;
use oauth2::reqwest::async_http_client;

fn create_oauth_client() -> BasicClient {
    let client_id = ClientId::new("your-client-id".to_string());
    let client_secret = ClientSecret::new("your-client-secret".to_string());
    let auth_url = AuthUrl::new("https://auth.example.com/authorize".to_string()).unwrap();
    let token_url = TokenUrl::new("https://auth.example.com/token".to_string()).unwrap();
    
    BasicClient::new(client_id, Some(client_secret), auth_url, Some(token_url))
        .set_redirect_uri(RedirectUrl::new("http://localhost:8080/callback".to_string()).unwrap())
}

async fn authorization_code_flow() -> Result<(), Box<dyn std::error::Error>> {
    let client = create_oauth_client();
    
    // 生成PKCE参数
    let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
    
    // 生成授权URL
    let (auth_url, csrf_token) = client
        .authorize_url(CsrfToken::new_random)
        .add_scope(Scope::new("read:users".to_string()))
        .add_scope(Scope::new("write:orders".to_string()))
        .set_pkce_challenge(pkce_challenge)
        .url();
    
    println!("请访问以下URL授权: {}", auth_url);
    println!("CSRF Token: {}", csrf_token.secret());
    
    // 用户授权后,从回调URL获取授权码
    let authorization_code = AuthorizationCode::new("code-from-callback".to_string());
    
    // 用授权码换取令牌
    let token_response = client
        .exchange_code(authorization_code)
        .set_pkce_verifier(pkce_verifier)
        .request_async(async_http_client)
        .await?;
    
    println!("访问令牌: {}", token_response.access_token().secret());
    
    Ok(())
}
}

Cargo.toml 依赖:

[dependencies]
oauth2 = "4"
reqwest = "0.11"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

48.6.4 Actix-web中的授权中间件

#![allow(unused)]
fn main() {
use actix_web::{dev::ServiceRequest, error::ErrorUnauthorized, web, App, Error, HttpServer};
use actix_web_httpauth::extractors::bearer::{BearerAuth, Config};
use actix_web_httpauth::extractors::AuthenticationError;
use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Claims {
    sub: String,
    role: String,
    scope: String,
}

async fn validator(
    req: ServiceRequest,
    credentials: BearerAuth,
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
    let token = credentials.token();
    let secret = req.app_data::<web::Data<String>>()
        .map(|d| d.get_ref().clone())
        .unwrap_or_default();
    
    let validation = Validation::new(Algorithm::HS256);
    match decode::<Claims>(token, &DecodingKey::from_secret(secret.as_bytes()), &validation) {
        Ok(token_data) => {
            req.extensions_mut().insert(token_data.claims);
            Ok(req)
        }
        Err(_) => {
            let config = req.app_data::<Config>()
                .map(|data| data.clone())
                .unwrap_or_default();
            Err((AuthenticationError::from(config).into(), req))
        }
    }
}

// 角色检查守卫
fn require_role(req: &actix_web::HttpRequest, role: &str) -> Result<Claims, Error> {
    let claims = req.extensions()
        .get::<Claims>()
        .cloned()
        .ok_or_else(|| ErrorUnauthorized("未认证"))?;
    
    if claims.role != role {
        return Err(ErrorUnauthorized("权限不足"));
    }
    
    Ok(claims)
}

async fn admin_endpoint(req: actix_web::HttpRequest) -> Result<String, Error> {
    require_role(&req, "admin")?;
    Ok("管理员数据".to_string())
}

async fn user_endpoint(req: actix_web::HttpRequest) -> Result<String, Error> {
    let _claims = require_role(&req, "user")?;
    Ok("用户数据".to_string())
}
}

48.7 授权架构模式

48.7.1 集中式授权

所有授权决策由专门的授权服务处理:

  • 优点:策略统一管理,易于变更和审计
  • 缺点:单点故障风险,网络延迟
  • 适用:策略复杂、合规要求高的场景

48.7.2 分布式授权

各服务本地执行授权决策:

  • 优点:低延迟,高可用
  • 缺点:策略同步困难,一致性挑战
  • 适用:微服务架构,性能敏感场景

48.7.3 混合模式

  • 策略管理集中:统一的服务管理策略定义
  • 策略执行分布:策略下发到各服务本地执行(如通过JWT携带权限声明)
#![allow(unused)]
fn main() {
// 混合模式:JWT携带权限声明,本地验证
#[derive(Debug, Clone)]
struct UserContext {
    user_id: String,
    roles: Vec<String>,
    permissions: Vec<String>,
}

impl UserContext {
    fn can(&self, permission: &str) -> bool {
        self.permissions.contains(&permission.to_string())
    }
    
    fn has_role(&self, role: &str) -> bool {
        self.roles.contains(&role.to_string())
    }
}

// 中间件解析JWT并注入UserContext
// 处理器本地进行权限检查,无需远程调用
async fn create_order(ctx: UserContext) -> Result<String, Error> {
    if !ctx.can("order:create") {
        return Err(ErrorUnauthorized("无创建订单权限"));
    }
    // 执行业务逻辑
    Ok("订单创建成功".to_string())
}
}

48.8 本章总结

主题核心要点
授权基础认证回答“你是谁“,授权回答“你能做什么“;授权检查必须在服务端强制执行
DAC资源所有者自主控制权限,灵活但难以审计
MAC系统强制策略,安全性最高,适合高安全环境
RBAC通过角色管理权限,企业应用首选;支持角色继承和职责分离
ABAC基于属性决策,最灵活但复杂;适合云原生和动态环境
OAuth 2.0授权码模式最安全,PKCE保护公共客户端;scope定义权限边界
JWT自包含令牌,支持对称/非对称签名;必须验证exp/iss/aud等声明
Rust生态casbin通用授权、oauth2客户端、jsonwebtokenJWT处理

48.9 练习建议

  1. RBAC系统实现:使用Casbin实现一个完整的RBAC系统,包含用户管理、角色管理、权限分配和层级继承功能。

  2. OAuth 2.0服务端:基于oxide-auth或自建实现一个简化版OAuth 2.0授权服务器,支持授权码模式和PKCE。

  3. ABAC策略引擎:设计一个基于资源属性的访问控制系统,实现时间、位置、设备类型等环境属性的策略评估。

  4. JWT安全中间件:为Axum框架编写JWT认证和授权中间件,支持角色检查和scope验证,并实现自动令牌刷新。

  5. 权限审计系统:实现一个权限变更审计日志系统,记录所有策略变更、角色分配和访问拒绝事件,支持合规报告生成。


授权如划界,界内可自由驰骋,界外则寸步难行。好的授权系统,既不因过度限制而束缚业务创新,也不因放任自流而埋下安全隐患。在Rust的类型系统和所有权模型之上,我们可以构建出既高效又可靠的授权机制,让每一行代码都在明确的权限边界内安全运行。

第四十九章 数字证书与 PKI

公钥的身份证:如何证明“我是我“

在上一章我们学习了非对称加密和数字签名,但还有一个关键问题没有解决:当你收到一个公钥时,如何确定它真的属于声称的那个人?如果攻击者替换了你手中的公钥,整个加密通信就会形同虚设。数字证书与公钥基础设施(PKI)正是为了解决“公钥分发与身份绑定“这一核心问题而诞生的。

49.1 为什么需要数字证书

信任问题的本质

假设 Alice 想给 Bob 发送加密消息,她需要 Bob 的公钥。但如果中间人 Mallory 将 Bob 的公钥替换为自己的公钥,Alice 就会用 Mallory 的公钥加密,Mallory 可以解密、篡改、重新加密后转发给 Bob。这就是公钥分发问题

解决思路:引入一个双方都信任的第三方,由这个第三方为公钥“背书“——这就是数字证书的核心思想。

数字证书的作用

数字证书(Digital Certificate)是由证书颁发机构(CA)签名的数据结构,它将以下信息绑定在一起:

  • 主体身份:个人、服务器或组织的标识信息
  • 公钥:主体的公钥
  • 有效期:证书的有效时间范围
  • 颁发者:签发该证书的 CA 信息
  • 数字签名:CA 对该证书内容的签名

任何人都可以用 CA 的公钥验证证书签名的真实性,从而确认证书中的公钥确实属于证书声明的主体。

49.2 X.509 证书格式

X.509 是国际电信联盟(ITU-T)制定的数字证书标准,也是目前最广泛使用的证书格式。TLS/SSL、代码签名、电子邮件加密等场景都基于 X.509。

X.509 v3 证书结构

一个 X.509 证书包含以下核心字段:

Certificate
├── tbsCertificate (待签名内容)
│   ├── version (版本号,v3 为 2)
│   ├── serialNumber (证书序列号)
│   ├── signature (签名算法标识)
│   ├── issuer (颁发者名称)
│   ├── validity (有效期)
│   │   ├── notBefore (生效时间)
│   │   └── notAfter (过期时间)
│   ├── subject (主体名称)
│   ├── subjectPublicKeyInfo (主体公钥信息)
│   │   ├── algorithm (公钥算法)
│   │   └── subjectPublicKey (公钥值)
│   └── extensions (扩展字段,v3 特有)
│       ├── subjectKeyIdentifier (主体密钥标识符)
│       ├── authorityKeyIdentifier (颁发者密钥标识符)
│       ├── keyUsage (密钥用途)
│       ├── extendedKeyUsage (扩展密钥用途)
│       ├── subjectAltName (主体备用名称,如 DNS 名、IP 地址)
│       └── basicConstraints (基本约束,如是否为 CA)
├── signatureAlgorithm (签名算法)
└── signatureValue (签名值)

证书编码格式

X.509 证书有两种常见的编码表示:

格式扩展名编码方式特点
DER.der, .cer二进制 ASN.1 DER紧凑、不可读
PEM.pem, .crtBase64 编码的 DER文本格式,以 -----BEGIN CERTIFICATE----- 开头

PEM 格式更常见,可以用文本编辑器查看,也方便在邮件和配置文件中传递。

查看证书内容

使用 OpenSSL 可以查看证书详情:

# 查看 PEM 证书内容
openssl x509 -in server.crt -text -noout

# 查看证书主题和颁发者
openssl x509 -in server.crt -subject -issuer -noout

输出示例(部分):

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            04:7d:3c:...
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = US, O = Let's Encrypt, CN = R3
        Validity
            Not Before: Jan  1 00:00:00 2024 GMT
            Not After : Apr  1 00:00:00 2024 GMT
        Subject: CN = example.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                DNS:example.com, DNS:www.example.com

49.3 证书链与信任锚

证书链(Certificate Chain)

CA 通常采用层级结构。根 CA 直接签发证书的情况很少见,更多时候是:

  • 根 CA(Root CA):自签名证书,预装在操作系统和浏览器中
  • 中间 CA(Intermediate CA):由根 CA 签发,负责实际签发终端实体证书
  • 终端实体(End Entity):服务器、用户等持有的证书,由中间 CA 签发

这样就形成了一条证书链

终端实体证书(example.com)
    ↑ 由 Intermediate CA 签名
中间 CA 证书(Let's Encrypt R3)
    ↑ 由 Root CA 签名
根 CA 证书(ISRG Root X1)
    ↑ 自签名

验证证书时,验证方从终端实体证书开始,沿着证书链逐级验证每个证书的签名,直到到达一个受信任的根证书。

信任锚(Trust Anchor)

信任锚是证书验证的起点,通常是自签名的根 CA 证书。操作系统和浏览器预装了数百个受信任的根证书,构成了信任库(Trust Store)

#![allow(unused)]
fn main() {
// 概念示意:信任库中的根证书
let trust_anchors = vec![
    "DigiCert Global Root CA",
    "ISRG Root X1",           // Let's Encrypt
    "GlobalSign Root CA",
    // ... 数百个根证书
];
}

自签名证书:根 CA 的证书是自己签名的,没有更上层的颁发者。这意味着信任根 CA 是一种人为约定,基于其声誉、审计和安全实践。

证书链验证过程

1. 获取服务器证书
2. 检查有效期(notBefore <= 当前时间 <= notAfter)
3. 检查证书吊销状态(CRL 或 OCSP)
4. 验证签名:用中间 CA 的公钥验证服务器证书签名
5. 获取中间 CA 证书,验证其签名:用根 CA 公钥验证
6. 根 CA 证书是否在信任库中?
7. 全部通过 → 证书可信

49.4 证书颁发机构(CA)

CA 的角色与职责

证书颁发机构(Certificate Authority)是 PKI 的核心组件,主要职责包括:

  1. 身份验证:在签发证书前验证申请者的身份
  2. 证书签发:使用 CA 的私钥对证书进行数字签名
  3. 证书更新:在证书过期前重新签发
  4. 证书吊销:在私钥泄露或身份变更时宣布证书失效
  5. 发布 CRL 和 OCSP:提供证书状态查询服务

域名验证(DV)、组织验证(OV)与扩展验证(EV)

类型验证内容签发时间浏览器显示
DV仅验证域名所有权分钟级普通锁图标
OV验证域名 + 组织身份小时到几天普通锁图标
EV严格验证组织法律身份几天到几周曾显示绿色组织名(现大多浏览器已取消)

私有 CA 与自签名证书

除了公共 CA(如 Let’s Encrypt、DigiCert),组织也可以搭建私有 CA用于内部系统。开发测试环境中,自签名证书(自己签发自己的证书)也很常见。

#![allow(unused)]
fn main() {
// 自签名证书 vs CA 签发证书的区别

// 自签名证书:issuer == subject
let self_signed = Certificate {
    subject: "My Server",
    issuer: "My Server",   // 相同!
    public_key: server_pub_key,
    signature: sign_with(server_priv_key, ...),
};

// CA 签发证书:issuer 是 CA
let ca_signed = Certificate {
    subject: "My Server",
    issuer: "My CA",       // 不同
    public_key: server_pub_key,
    signature: sign_with(ca_priv_key, ...),
};
}

49.5 证书生命周期

证书申请流程

1. 生成密钥对(申请者本地完成)
2. 创建证书签名请求(CSR)
3. 提交 CSR 和身份证明材料给 CA
4. CA 验证身份
5. CA 签发证书
6. 下载并安装证书

证书签名请求(CSR)

CSR(Certificate Signing Request)是向 CA 申请证书时提交的文件,包含公钥和主体信息,用申请者的私钥签名。

# 生成私钥和 CSR
openssl req -new -newkey rsa:2048 -nodes \
    -keyout server.key -out server.csr \
    -subj "/C=CN/O=MyOrg/CN=example.com"

# 查看 CSR 内容
openssl req -in server.csr -text -noout

证书吊销

证书可能在过期前就需要失效,常见原因:

  • 私钥泄露或丢失
  • 域名所有权变更
  • 组织信息变更
  • 证书签发错误

吊销机制

  • CRL(Certificate Revocation List):CA 定期发布的已吊销证书序列号列表
  • OCSP(Online Certificate Status Protocol):实时在线查询证书状态
  • OCSP Stapling:服务器预先获取 OCSP 响应,在 TLS 握手时附带发送

49.6 Rust 实现:生成与验证证书

使用 rcgen 生成自签名证书

rcgen 是一个纯 Rust 的证书生成库,适合在测试环境或内部工具中生成证书。

# Cargo.toml
[dependencies]
rcgen = "0.12"
use rcgen::{Certificate, CertificateParams, KeyPair, SanType};
use std::fs;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 创建证书参数
    let mut params = CertificateParams::new(vec![
        "localhost".to_string(),
        "127.0.0.1".to_string(),
    ]);

    // 添加 Subject Alternative Name(对于 TLS 很重要)
    params.subject_alt_names = vec![
        SanType::DnsName("localhost".to_string()),
        SanType::IpAddress(std::net::IpAddr::V4("127.0.0.1".parse()?)),
    ];

    // 生成自签名证书
    let cert = Certificate::from_params(params)?;

    // 获取 PEM 格式的证书和私钥
    let cert_pem = cert.serialize_pem()?;
    let key_pem = cert.serialize_private_key_pem();

    // 保存到文件
    fs::write("server.crt", cert_pem)?;
    fs::write("server.key", key_pem)?;

    println!("自签名证书已生成:");
    println!("  证书: server.crt");
    println!("  私钥: server.key");

    Ok(())
}

生成 CA 并签发终端证书

#![allow(unused)]
fn main() {
use rcgen::{Certificate, CertificateParams, KeyUsagePurpose};

fn create_ca() -> Result<Certificate, Box<dyn std::error::Error>> {
    let mut params = CertificateParams::new(vec!["My Test CA".to_string()]);

    // 标记为基本约束:这是 CA 证书
    params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);

    // 设置密钥用途
    params.key_usages = vec![
        KeyUsagePurpose::KeyCertSign,
        KeyUsagePurpose::CrlSign,
    ];

    let ca_cert = Certificate::from_params(params)?;
    Ok(ca_cert)
}

fn sign_server_cert(ca: &Certificate) -> Result<Certificate, Box<dyn std::error::Error>> {
    let mut params = CertificateParams::new(vec!["example.com".to_string()]);

    params.subject_alt_names = vec![
        rcgen::SanType::DnsName("example.com".to_string()),
        rcgen::SanType::DnsName("www.example.com".to_string()),
    ];

    // 设置扩展密钥用途:TLS 服务器认证
    params.extended_key_usages = vec![
        rcgen::ExtendedKeyUsagePurpose::ServerAuth,
    ];

    let server_cert = Certificate::from_params(params)?;

    // 用 CA 的私钥签名服务器证书
    let signed_pem = server_cert.serialize_pem_with_signer(ca)?;

    // 返回签名后的证书(实际使用时需要重新构造或保存 PEM)
    Ok(server_cert)
}
}

使用 rustls 验证证书链

rustls 是 Rust 生态中最流行的 TLS 库,它内置了证书验证功能。

#![allow(unused)]
fn main() {
use rustls::{ClientConfig, RootCertStore};
use rustls_pemfile;
use std::fs::File;
use std::io::BufReader;
use std::sync::Arc;

fn create_tls_client() -> Result<Arc<ClientConfig>, Box<dyn std::error::Error>> {
    // 加载系统根证书
    let mut root_store = RootCertStore::empty();

    // 从文件加载自定义 CA 证书(用于测试环境)
    let ca_file = File::open("ca.crt")?;
    let mut ca_reader = BufReader::new(ca_file);

    let certs: Vec<_> = rustls_pemfile::certs(&mut ca_reader)
        .filter_map(|r| r.ok())
        .collect();

    for cert in certs {
        root_store.add(cert)?;
    }

    // 创建 TLS 客户端配置
    let config = ClientConfig::builder()
        .with_root_certificates(root_store)
        .with_no_client_auth();

    Ok(Arc::new(config))
}
}

解析和检查 X.509 证书

#![allow(unused)]
fn main() {
use x509_parser::pem::parse_x509_pem;
use x509_parser::prelude::*;

fn inspect_certificate(pem_data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
    let (_, pem) = parse_x509_pem(pem_data)?;
    let cert = pem.parse_x509()?;

    // 基本信息
    println!("版本: {:?}", cert.version());
    println!("序列号: {}", cert.tbs_certificate.raw_serial_as_string());

    // 颁发者和主体
    println!("颁发者: {}", cert.issuer());
    println!("主体: {}", cert.subject());

    // 有效期
    let validity = cert.validity();
    println!("生效时间: {}", validity.not_before);
    println!("过期时间: {}", validity.not_after);

    // 公钥信息
    let pubkey = cert.tbs_certificate.subject_pki;
    println!("公钥算法: {:?}", pubkey.algorithm.algorithm);

    // 扩展字段
    for ext in cert.extensions() {
        println!("扩展: {:?}", ext.oid);
    }

    // 验证证书是否过期
    let now = x509_parser::time::ASN1Time::now();
    if now < validity.not_before || now > validity.not_after {
        println!("警告:证书已过期或尚未生效!");
    } else {
        println!("证书在有效期内");
    }

    Ok(())
}
}

完整的证书生成与验证示例

use rcgen::{Certificate, CertificateParams, SanType};
use ring::signature;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. 创建 CA
    let mut ca_params = CertificateParams::new(vec!["Test CA".to_string()]);
    ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
    let ca_cert = Certificate::from_params(ca_params)?;

    // 2. 创建服务器证书
    let mut server_params = CertificateParams::new(vec!["myapp.local".to_string()]);
    server_params.subject_alt_names = vec![
        SanType::DnsName("myapp.local".to_string()),
        SanType::IpAddress("192.168.1.1".parse()?),
    ];
    let server_cert = Certificate::from_params(server_params)?;

    // 3. 用 CA 签名服务器证书
    let signed_cert_pem = server_cert.serialize_pem_with_signer(&ca_cert)?;
    let ca_cert_pem = ca_cert.serialize_pem()?;

    println!("=== CA 证书 ===");
    println!("{}", ca_cert_pem);
    println!("=== 服务器证书 ===");
    println!("{}", signed_cert_pem);

    // 4. 验证证书链(概念演示)
    // 实际验证应使用 rustls 或 webpki
    println!("\n证书链验证:");
    println!("  服务器证书由 CA 签名 ✓");
    println!("  CA 证书自签名 ✓");
    println!("  信任链完整 ✓");

    Ok(())
}

49.7 本章总结

概念说明Rust 生态
X.509数字证书国际标准格式x509-parser 用于解析
证书链从终端实体到根 CA 的层级信任链rustls 自动验证
CA证书颁发机构,负责身份验证和签名rcgen 可用于构建测试 CA
信任锚预装的受信任根证书webpki-roots 提供 Mozilla 根证书
证书吊销CRL 和 OCSP 机制rustls 支持 OCSP Stapling
自签名证书自己签名的证书,适合测试rcgen 生成

练习建议

  1. 基础练习:使用 rcgen 生成一个自签名证书,用 OpenSSL 查看其内容,确认包含正确的 SAN 扩展。

  2. 中级练习:构建一个两层 PKI(根 CA + 中间 CA + 服务器证书),验证完整证书链。

  3. 高级练习:实现一个简易的证书验证工具,解析 PEM 证书文件,检查有效期、密钥用途和签名算法强度。

  4. 实践项目:为你的 Rust Web 应用(如 Axum 或 Actix-web)配置 TLS,使用 rustls 和自签名证书启用 HTTPS。


密码学箴言:证书解决的不是加密问题,而是信任问题。再强的加密算法,如果信任根基被攻破,也只是空中楼阁。

第五十章 TLS/SSL 协议

互联网的安全基石:从握手到加密通信

TLS(Transport Layer Security,传输层安全协议)及其前身 SSL(Secure Sockets Layer)是互联网上最广泛使用的安全通信协议。每当你在浏览器中看到那把绿色的小锁,背后就是 TLS 在保护你的数据不被窃听和篡改。本章将深入 TLS 协议的核心机制,并展示如何在 Rust 中构建安全的网络通信。

50.1 TLS 概述

为什么需要 TLS

在 TLS 出现之前,HTTP、SMTP、FTP 等协议以明文传输数据,面临三大安全威胁:

  • 窃听(Eavesdropping):攻击者可以读取传输中的敏感信息
  • 篡改(Tampering):攻击者可以修改传输中的数据
  • 冒充(Impersonation):攻击者可以伪装成通信的另一方

TLS 通过以下机制解决这些问题:

  • 加密:使用对称加密保护数据传输的机密性
  • 消息认证码(MAC):确保数据未被篡改
  • 数字证书:验证通信双方的身份

TLS 与 SSL 的版本演进

版本年份状态说明
SSL 2.01995已废弃存在严重安全漏洞
SSL 3.01996已废弃POODLE 攻击使其不再安全
TLS 1.01999已废弃等同于 SSL 3.1
TLS 1.12006已废弃增强了 CBC 模式安全性
TLS 1.22008广泛使用当前主流版本,支持 AEAD 加密
TLS 1.32018推荐使用大幅简化握手,提升性能和安全性

现代应用应使用 TLS 1.2 或 TLS 1.3,并禁用所有早期版本。

TLS 协议栈位置

应用层(HTTP、SMTP、MQTT...)
    ↑
TLS 层(握手、加密、认证)
    ↑
传输层(TCP)
    ↑
网络层(IP)

TLS 位于应用层和传输层之间,对应用层透明。HTTP + TLS = HTTPS,SMTP + TLS = SMTPS,以此类推。

50.2 TLS 握手过程

TLS 握手是建立安全连接的关键步骤,客户端和服务器在此过程中协商加密参数、验证身份并生成会话密钥。

TLS 1.2 握手(完整版)

TLS 1.2 握手通常需要 2-RTT(两个往返时延):

客户端                                    服务器
  |                                         |
  |  -------- ClientHello -------->        |
  |  [支持的 TLS 版本、密码套件列表、        |
  |   随机数、会话 ID、扩展]                |
  |                                         |
  |  <------- ServerHello ---------        |
  |  [选定的 TLS 版本、密码套件、           |
  |   服务器随机数]                         |
  |                                         |
  |  <------- Certificate -----------      |
  |  [服务器证书链]                         |
  |                                         |
  |  <------- ServerKeyExchange ----       |
  |  [密钥交换参数,如 DH/ECDH 公钥]        |
  |                                         |
  |  <------- ServerHelloDone ------       |
  |                                         |
  |  -------- ClientKeyExchange --->       |
  |  [预主密钥,用服务器公钥加密]            |
  |                                         |
  |  -------- ChangeCipherSpec --->        |
  |  [通知后续消息将加密]                   |
  |                                         |
  |  -------- Finished ------------->      |
  |  [加密的消息认证码,验证握手完整性]      |
  |                                         |
  |  <------- ChangeCipherSpec -----       |
  |                                         |
  |  <------- Finished --------------      |
  |                                         |
  |========= 加密应用数据传输 ==========|

关键步骤解析

  1. ClientHello:客户端发起握手,提供支持的协议版本、密码套件列表、压缩方法、客户端随机数(32 字节)
  2. ServerHello:服务器选择协议版本和密码套件,发送服务器随机数
  3. Certificate:服务器发送证书链,客户端验证证书
  4. ServerKeyExchange:对于 DHE/ECDHE 密钥交换,发送临时公钥
  5. ClientKeyExchange:客户端生成预主密钥(Pre-Master Secret),用服务器公钥加密发送
  6. Finished:双方用协商的密钥发送验证消息,确认握手成功

TLS 1.3 握手(简化版)

TLS 1.3 将握手优化到 1-RTT,甚至在会话恢复时实现 0-RTT

客户端                                    服务器
  |                                         |
  |  -------- ClientHello -------->        |
  |  [支持的密钥共享、协议版本、             |
  |   客户端随机数、扩展]                   |
  |                                         |
  |  <------- ServerHello ---------        |
  |  [选定的密钥共享、服务器随机数]          |
  |                                         |
  |  <------- {EncryptedExtensions}        |
  |                                         |
  |  <------- {Certificate} -----------    |
  |                                         |
  |  <------- {CertificateVerify} ----     |
  |  [服务器签名验证]                       |
  |                                         |
  |  <------- {Finished} --------------    |
  |                                         |
  |  -------- {Finished} ------------->    |
  |                                         |
  |========= 加密应用数据传输 ==========|

TLS 1.3 的关键改进:

  • 移除过时算法:MD5、SHA-1、RC4、CBC 模式、RSA 密钥交换等全部移除
  • 1-RTT 握手:客户端在 ClientHello 中猜测密钥交换参数,减少一次往返
  • 0-RTT 会话恢复:基于 PSK(Pre-Shared Key)实现零往返恢复
  • 加密更多握手消息:Certificate 等消息在 TLS 1.3 中已加密
  • 前向安全性:仅支持提供前向安全的密钥交换(ECDHE)

会话恢复与会话票证

完整握手涉及公钥运算,计算开销较大。TLS 支持会话恢复机制:

TLS 1.2 会话 ID

第一次握手:
  ClientHello (session_id = []) →
  ← ServerHello (session_id = 0x1234...)

后续连接:
  ClientHello (session_id = 0x1234...) →
  ← ServerHello (session_id = 0x1234...)
  ← ChangeCipherSpec
  ← Finished
  Finished →

TLS 1.2/1.3 会话票证(Session Ticket)

服务器将会话状态加密后作为“票证“发给客户端,客户端在后续连接中出示票证即可恢复会话。这减轻了服务器的状态存储负担。

50.3 密码套件协商

什么是密码套件

密码套件(Cipher Suite)定义了 TLS 连接使用的完整加密方案,包括:

  • 密钥交换算法:如何协商共享密钥(RSA、DHE、ECDHE)
  • 身份验证算法:如何验证身份(RSA、ECDSA、Ed25519)
  • 对称加密算法:如何加密数据(AES-GCM、ChaCha20-Poly1305)
  • 消息认证算法:如何验证消息完整性(AEAD 内置、HMAC)

TLS 1.2 密码套件示例:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

  • ECDHE:密钥交换使用椭圆曲线 Diffie-Hellman(临时)
  • RSA:身份验证使用 RSA 签名
  • AES_128_GCM:对称加密使用 AES-128 GCM 模式
  • SHA256:HKDF 和握手消息哈希使用 SHA-256

TLS 1.3 简化了密码套件表示:TLS_AES_256_GCM_SHA384TLS_CHACHA20_POLY1305_SHA256

推荐的密码套件

TLS 1.3(全部安全,按优先级排序)

  1. TLS_AES_256_GCM_SHA384
  2. TLS_CHACHA20_POLY1305_SHA256
  3. TLS_AES_128_GCM_SHA256

TLS 1.2(仅推荐以下)

  • TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
  • TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
  • TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
  • TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
  • TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
  • TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256

应避免:任何使用 RSA 密钥交换(无前向安全)、CBC 模式、RC4、DES/3DES 的密码套件。

前向安全性(Forward Secrecy)

前向安全性确保即使服务器的长期私钥在未来泄露,过去的通信记录也不会被解密。

无前向安全(RSA 密钥交换):
  预主密钥 = 用服务器 RSA 公钥加密
  如果服务器私钥泄露 → 所有历史会话可解密

有前向安全(ECDHE 密钥交换):
  临时 ECDH 密钥对,握手后即丢弃
  即使服务器私钥泄露 → 历史会话仍安全

现代 TLS 应始终使用 ECDHE 或 DHE 密钥交换。

50.4 证书验证

TLS 中的证书验证流程

1. 服务器发送证书链
2. 客户端验证:
   a. 证书链完整性(每个证书由上一级签名)
   b. 根证书在信任库中
   c. 证书未过期
   d. 证书未被吊销(CRL/OCSP)
   e. 证书中的域名匹配访问的域名
   f. 证书用途适合 TLS 服务器认证
3. 验证服务器签名(CertificateVerify)

主机名验证

证书中的主体备用名称(SAN)必须包含访问的域名:

访问 https://www.example.com

证书必须包含:
  DNS:www.example.com
或
  DNS:*.example.com(通配符)

证书固定(Certificate Pinning)

为了防止 rogue CA 签发伪造证书,应用可以“固定“预期的证书或公钥:

#![allow(unused)]
fn main() {
// 概念:只信任特定的证书指纹
const EXPECTED_CERT_FINGERPRINT: &[u8] = b"SHA256:abc123...";

fn verify_pinned_cert(cert: &Certificate) -> Result<(), Error> {
    let fingerprint = sha256(cert.raw_bytes());
    if fingerprint == EXPECTED_CERT_FINGERPRINT {
        Ok(())
    } else {
        Err(Error::UntrustedCertificate)
    }
}
}

HTTP Public Key Pinning(HPKP)曾用于 Web,但因风险过高已被废弃。移动应用仍常用证书固定。

50.5 Rust 实现:TLS 安全连接

使用 rustls 建立 TLS 客户端连接

rustls 是 Rust 生态中最成熟、最安全的 TLS 实现,默认只启用安全的协议版本和密码套件。

# Cargo.toml
[dependencies]
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
rustls-pemfile = "2"
webpki-roots = "0.26"
#![allow(unused)]
fn main() {
use rustls::{ClientConfig, RootCertStore, ServerName};
use rustls_pemfile;
use std::net::TcpStream;
use std::sync::Arc;
use std::io::{Read, Write};

fn create_https_client() -> Arc<ClientConfig> {
    // 加载 Mozilla 根证书
    let mut root_store = RootCertStore::empty();
    root_store.extend(
        webpki_roots::TLS_SERVER_ROOTS
            .iter()
            .cloned()
    );

    // 创建客户端配置
    let config = ClientConfig::builder()
        .with_root_certificates(root_store)
        .with_no_client_auth();

    Arc::new(config)
}

fn fetch_https(url: &str) -> Result<String, Box<dyn std::error::Error>> {
    let config = create_https_client();

    // 解析域名和端口
    let server_name = "www.rust-lang.org".try_into()?;
    let server_name = ServerName::try_from("www.rust-lang.org")?;

    // 建立 TCP 连接
    let tcp_stream = TcpStream::connect("www.rust-lang.org:443")?;

    // 包装为 TLS 连接
    let mut tls_stream = rustls::ClientConnection::new(config, server_name)?;
    let mut stream = rustls::Stream::new(&mut tls_stream, &mut tcp_stream);

    // 发送 HTTP 请求
    let request = "GET / HTTP/1.1\r\n\
                   Host: www.rust-lang.org\r\n\
                   Connection: close\r\n\r\n";
    stream.write_all(request.as_bytes())?;

    // 读取响应
    let mut response = String::new();
    stream.read_to_string(&mut response)?;

    Ok(response)
}
}

使用 tokio-rustls 实现异步 TLS

在生产环境中,我们通常使用异步 I/O。tokio-rustlsrustls 与 Tokio 集成:

[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-rustls = "0.26"
rustls-pemfile = "2"
webpki-roots = "0.26"
use tokio::net::TcpStream;
use tokio_rustls::{TlsConnector, rustls::{ClientConfig, RootCertStore, ServerName}};
use std::sync::Arc;

async fn https_get(host: &str, path: &str) -> Result<String, Box<dyn std::error::Error>> {
    // 创建 TLS 配置
    let mut root_store = RootCertStore::empty();
    root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());

    let config = ClientConfig::builder()
        .with_root_certificates(root_store)
        .with_no_client_auth();

    let connector = TlsConnector::from(Arc::new(config));

    // 建立 TCP 连接
    let tcp_stream = TcpStream::connect(format!("{}:443", host)).await?;

    // TLS 握手
    let server_name = ServerName::try_from(host)?;
    let mut tls_stream = connector.connect(server_name, tcp_stream).await?;

    // 发送 HTTP 请求
    let request = format!(
        "GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
        path, host
    );
    tokio::io::AsyncWriteExt::write_all(&mut tls_stream, request.as_bytes()).await?;

    // 读取响应
    let mut response = Vec::new();
    tokio::io::AsyncReadExt::read_to_end(&mut tls_stream, &mut response).await?;

    Ok(String::from_utf8_lossy(&response).to_string())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = https_get("www.rust-lang.org", "/").await?;
    println!("响应长度: {} 字节", response.len());
    println!("前 500 字符:\n{}", &response[..response.len().min(500)]);
    Ok(())
}

构建 TLS 服务器

#![allow(unused)]
fn main() {
use tokio::net::TcpListener;
use tokio_rustls::{TlsAcceptor, rustls::{ServerConfig, Certificate, PrivateKey}};
use std::fs::File;
use std::io::BufReader;
use std::sync::Arc;

fn load_certs(path: &str) -> Vec<Certificate> {
    let file = File::open(path).expect("无法打开证书文件");
    let mut reader = BufReader::new(file);
    rustls_pemfile::certs(&mut reader)
        .filter_map(|r| r.ok())
        .collect()
}

fn load_key(path: &str) -> PrivateKey {
    let file = File::open(path).expect("无法打开密钥文件");
    let mut reader = BufReader::new(file);
    rustls_pemfile::pkcs8_private_keys(&mut reader)
        .filter_map(|r| r.ok())
        .next()
        .expect("未找到私钥")
        .into()
}

async fn run_tls_server() -> Result<(), Box<dyn std::error::Error>> {
    let certs = load_certs("server.crt");
    let key = load_key("server.key");

    let config = ServerConfig::builder()
        .with_safe_defaults()
        .with_no_client_auth()
        .with_single_cert(certs, key)?;

    let acceptor = TlsAcceptor::from(Arc::new(config));
    let listener = TcpListener::bind("127.0.0.1:8443").await?;

    println!("TLS 服务器监听于 https://127.0.0.1:8443");

    loop {
        let (stream, peer_addr) = listener.accept().await?;
        let acceptor = acceptor.clone();

        tokio::spawn(async move {
            match acceptor.accept(stream).await {
                Ok(mut tls_stream) => {
                    println!("来自 {} 的安全连接已建立", peer_addr);

                    // 简单的 HTTP 响应
                    let response = "HTTP/1.1 200 OK\r\n\
                                    Content-Type: text/plain\r\n\
                                    Content-Length: 13\r\n\r\n\
                                    Hello, TLS!\n";
                    let _ = tokio::io::AsyncWriteExt::write_all(
                        &mut tls_stream, response.as_bytes()
                    ).await;
                }
                Err(e) => {
                    eprintln!("TLS 握手失败 ({}): {}", peer_addr, e);
                }
            }
        });
    }
}
}

配置客户端证书(mTLS)

双向 TLS(mutual TLS,mTLS)要求客户端也提供证书,服务器验证客户端身份:

#![allow(unused)]
fn main() {
use rustls::{ClientConfig, RootCertStore};

fn create_mtls_client(
    ca_cert_path: &str,
    client_cert_path: &str,
    client_key_path: &str,
) -> Result<Arc<ClientConfig>, Box<dyn std::error::Error>> {
    // 加载 CA 证书(用于验证服务器)
    let mut root_store = RootCertStore::empty();
    let ca_file = std::fs::File::open(ca_cert_path)?;
    let mut ca_reader = std::io::BufReader::new(ca_file);
    let ca_certs: Vec<_> = rustls_pemfile::certs(&mut ca_reader)
        .filter_map(|r| r.ok())
        .collect();
    for cert in ca_certs {
        root_store.add(cert)?;
    }

    // 加载客户端证书和私钥
    let client_certs = load_certs(client_cert_path);
    let client_key = load_key(client_key_path);

    let config = ClientConfig::builder()
        .with_root_certificates(root_store)
        .with_client_auth_cert(client_certs, client_key)?;

    Ok(Arc::new(config))
}
}

检查 TLS 连接信息

#![allow(unused)]
fn main() {
use rustls::ClientConnection;

fn inspect_connection(conn: &ClientConnection) {
    if let Some(cipher) = conn.negotiated_cipher_suite() {
        println!("协商的密码套件: {:?}", cipher.suite());
    }

    if let Some(version) = conn.protocol_version() {
        println!("TLS 版本: {:?}", version);
    }

    if let Some(peer_certs) = conn.peer_certificates() {
        println!("对等端证书数量: {}", peer_certs.len());
        for (i, cert) in peer_certs.iter().enumerate() {
            println!("  证书 {}: {} 字节", i, cert.len());
        }
    }

    println!("握手完成: {}", conn.is_handshaking());
}
}

50.6 TLS 安全最佳实践

服务器配置检查清单

  • 仅启用 TLS 1.2 和 TLS 1.3
  • 禁用所有不安全的密码套件
  • 优先使用 ECDHE 密钥交换(提供前向安全)
  • 使用 AES-GCM 或 ChaCha20-Poly1305(AEAD 模式)
  • 配置完整的证书链(包含中间证书)
  • 启用 OCSP Stapling
  • 配置 HSTS(HTTP Strict Transport Security)
  • 定期更新证书(Let’s Encrypt 90 天周期)

常见工具

# 检查服务器 TLS 配置
openssl s_client -connect example.com:443 -tls1_3

# 详细分析 TLS 配置
nmap --script ssl-enum-ciphers -p 443 example.com

# 测试 SSL/TLS 安全性
https://www.ssllabs.com/ssltest/

50.7 本章总结

概念说明Rust 实现
TLS 握手协商加密参数、验证身份、生成密钥rustls 自动处理
密码套件定义密钥交换、加密、认证算法组合rustls 默认仅启用安全套件
前向安全长期密钥泄露不暴露历史会话使用 ECDHE,rustls 默认开启
证书验证验证证书链、主机名、有效期rustls + webpki 自动验证
会话恢复避免重复完整握手开销rustls 支持会话票证
mTLS双向证书认证rustls 支持客户端证书

练习建议

  1. 基础练习:使用 tokio-rustls 编写一个 HTTPS 客户端,访问 https://httpbin.org/get 并打印响应。

  2. 中级练习:搭建一个支持 TLS 1.3 的异步服务器,使用 rcgen 生成自签名证书,用 curl -v --insecure https://localhost:8443 测试连接。

  3. 高级练习:实现 mTLS 通信:生成 CA、服务器证书和客户端证书,配置服务器要求客户端证书,编写客户端提供证书连接。

  4. 实践项目:为你的 Rust Web 服务添加 HTTPS 支持,配置 HTTP/2 和 TLS 1.3,使用 Let’s Encrypt 自动获取证书(可结合 acme-microinstant-acme 库)。


密码学箴言:TLS 不是可选项,而是现代网络通信的底线。配置 TLS 时,默认安全比灵活配置更重要——rustls 的设计哲学正是如此。

第五十一章 区块链与加密货币

去中心化的信任机器:从密码学原语到分布式共识

区块链(Blockchain)是近年来最具影响力的密码学应用之一。它巧妙地组合了哈希函数、数字签名、默克尔树等密码学原语,配合共识机制,构建了一个无需中心化机构即可实现信任传递的分布式系统。本章将从密码学视角剖析区块链的核心原理,并用 Rust 实现一个简化但完整的区块链。

51.1 区块链基本原理

什么是区块链

区块链本质上是一个分布式的、不可篡改的、按时间顺序链接的账本。其核心特征包括:

  • 去中心化:没有单一控制方,网络中的节点共同维护账本
  • 不可篡改:一旦数据被写入,几乎不可能被修改
  • 透明性:所有交易对网络参与者可见(或按权限可见)
  • 可追溯:每笔交易都有完整的历史链条

区块结构

每个区块通常包含以下部分:

+----------------------------------+
|  区块头(Block Header)           |
|  - 前一区块哈希(Previous Hash)  |
|  - 时间戳(Timestamp)            |
|  - 默克尔根(Merkle Root)        |
|  - 随机数(Nonce)                |
|  - 难度目标(Difficulty Target)  |
+----------------------------------+
|  区块体(Block Body)             |
|  - 交易列表(Transactions)       |
+----------------------------------+

前一区块哈希将区块链接成链:修改任何一个区块的数据,其哈希会改变,导致后续所有区块的链接断裂。

链式结构

创世区块(Genesis Block)
    Hash: 0000a3f2...
    Previous: 00000000...
         |
         v
区块 1
    Hash: 0000b8e1...
    Previous: 0000a3f2...  ← 指向创世区块的哈希
         |
         v
区块 2
    Hash: 00001c4d...
    Previous: 0000b8e1...  ← 指向区块 1 的哈希
         |
         v
区块 3
    Hash: 0000f92a...
    Previous: 00001c4d...  ← 指向区块 2 的哈希

这种链式结构使得篡改成本极高:攻击者不仅要修改目标区块,还要重新计算该区块之后所有区块的工作量证明。

51.2 哈希链与默克尔树

哈希指针

区块链使用哈希指针(Hash Pointer)替代普通指针。哈希指针不仅指向数据的位置,还包含该数据的哈希值,从而可以验证数据是否被篡改。

#![allow(unused)]
fn main() {
struct HashPointer<T> {
    data: T,
    hash: [u8; 32],  // 数据的 SHA-256 哈希
}

fn verify_integrity<T: AsRef<[u8]>>(pointer: &HashPointer<T>) -> bool {
    let computed_hash = sha256(pointer.data.as_ref());
    computed_hash == pointer.hash
}
}

默克尔树(Merkle Tree)

默克尔树是一种二叉树结构,用于高效验证大量数据的完整性。

                    Root Hash
                   /         \
              Hash(A+B)    Hash(C+D)
              /      \      /      \
            Hash(A) Hash(B) Hash(C) Hash(D)
              |        |      |        |
             Tx A    Tx B   Tx C    Tx D

默克尔树的优势

  • 高效验证:要验证交易 C 是否包含在区块中,只需提供 Hash(D) 和 Hash(A+B),共 O(log n) 个哈希
  • 轻节点友好:SPV(简单支付验证)节点只需存储区块头(80 字节),无需下载完整交易数据

默克尔证明

#![allow(unused)]
fn main() {
// 验证交易是否包含在区块中
fn verify_merkle_proof(
    tx_hash: &[u8; 32],
    merkle_root: &[u8; 32],
    proof: &[(bool, [u8; 32])],  // (is_right_sibling, sibling_hash)
) -> bool {
    let mut current_hash = *tx_hash;

    for &(is_right, sibling) in proof {
        current_hash = if is_right {
            sha256_concat(&current_hash, &sibling)
        } else {
            sha256_concat(&sibling, &current_hash)
        };
    }

    current_hash == *merkle_root
}
}

51.3 共识机制

共识机制解决分布式系统中的拜占庭将军问题:如何在可能存在恶意节点的网络中达成一致?

工作量证明(Proof of Work, PoW)

PoW 是比特币采用的共识机制。节点(矿工)通过计算难题来竞争记账权。

工作原理

  1. 矿工收集待确认交易,构建候选区块
  2. 不断改变随机数(Nonce),计算区块头的哈希
  3. 当哈希值小于难度目标(即哈希前面有足够多的 0)时,找到有效区块
  4. 将区块广播到网络,其他节点验证后接受
目标:找到 nonce,使得 SHA256(SHA256(block_header)) < target

难度目标示例:
  00000000 00000000 00000000 00000000 00000000 00000000 0000ffff 00000000
  
有效哈希示例:
  00000000 00000000 00000000 00000000 00000000 00000000 0000a3f2 8b1c4d5e

难度调整:比特币每 2016 个区块(约 2 周)调整一次难度,使平均出块时间保持在 10 分钟左右。

PoW 的优缺点

优点缺点
安全性高,攻击成本巨大能源消耗巨大
去中心化程度高交易确认慢(比特币约 10 分钟/区块)
无需准入许可存在算力集中风险

权益证明(Proof of Stake, PoS)

PoS 是以太坊 2.0 等新型区块链采用的共识机制。验证者通过质押代币来获得记账权。

工作原理

  1. 验证者质押一定数量的代币作为“保证金“
  2. 系统根据质押金额、质押时长等因素随机选择出块者
  3. 验证者提议区块,其他验证者投票确认
  4. 作恶者的质押金会被罚没(Slashing)

PoS 的优缺点

优点缺点
能耗极低(比 PoW 低 99% 以上)可能加剧财富集中
交易确认快安全性理论不如 PoW 成熟
可扩展性更好存在“无利害关系“问题

其他共识机制

机制代表项目核心思想
DPoSEOS代币持有者投票选举代表节点
PBFTHyperledger Fabric多轮投票达成拜占庭容错共识
AvalancheAvalanche随机抽样投票,快速最终确认
PoHSolana历史证明,创建可验证的时间序列

51.4 智能合约简介

什么是智能合约

智能合约(Smart Contract)是运行在区块链上的自动执行的程序代码。它类似于传统合约,但执行不依赖任何第三方,代码即法律(Code is Law)。

智能合约的特征

  • 自动执行:满足条件时自动触发,无需人工干预
  • 不可篡改:部署后代码不可修改
  • 透明公开:代码和执行结果对所有人可见
  • 确定性:给定相同输入,所有节点执行结果一致

智能合约示例(概念)

// 简单的以太坊智能合约:众筹
pragma solidity ^0.8.0;

contract Crowdfunding {
    address public beneficiary;
    uint public goal;
    uint public deadline;
    mapping(address => uint) public contributions;

    constructor(address _beneficiary, uint _goal, uint _duration) {
        beneficiary = _beneficiary;
        goal = _goal;
        deadline = block.timestamp + _duration;
    }

    function contribute() public payable {
        require(block.timestamp < deadline, "众筹已结束");
        contributions[msg.sender] += msg.value;
    }

    function withdraw() public {
        require(block.timestamp >= deadline, "众筹未结束");
        require(address(this).balance >= goal, "未达到目标金额");
        payable(beneficiary).transfer(address(this).balance);
    }
}

Rust 与智能合约

Rust 正成为区块链智能合约开发的重要语言:

  • Solana:使用 Rust 编写智能合约(Program)
  • Polkadot/Substrate:使用 Rust 构建区块链和智能合约
  • Near Protocol:支持 Rust 编写智能合约
  • Cosmos:Rust 是主要开发语言之一
#![allow(unused)]
fn main() {
// Solana 智能合约示例(简化概念)
use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    msg,
};

entrypoint!(process_instruction);

fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    msg!("Hello, Solana!");
    msg!("账户数量: {}", accounts.len());
    Ok(())
}
}

51.5 Rust 实现:简单区块链

区块定义

#![allow(unused)]
fn main() {
use sha2::{Sha256, Digest};
use chrono::Utc;
use serde::{Serialize, Deserialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Block {
    pub index: u64,
    pub timestamp: i64,
    pub data: String,
    pub previous_hash: String,
    pub hash: String,
    pub nonce: u64,
}

impl Block {
    pub fn new(index: u64, data: String, previous_hash: String) -> Self {
        let timestamp = Utc::now().timestamp();
        let mut block = Block {
            index,
            timestamp,
            data,
            previous_hash,
            hash: String::new(),
            nonce: 0,
        };
        block.hash = block.calculate_hash();
        block
    }

    pub fn calculate_hash(&self) -> String {
        let input = format!(
            "{}{}{}{}{}",
            self.index, self.timestamp, self.data, self.previous_hash, self.nonce
        );
        let mut hasher = Sha256::new();
        hasher.update(input);
        format!("{:x}", hasher.finalize())
    }
}
}

工作量证明挖矿

#![allow(unused)]
fn main() {
impl Block {
    pub fn mine(&mut self, difficulty: usize) {
        let target = "0".repeat(difficulty);
        
        while !self.hash.starts_with(&target) {
            self.nonce += 1;
            self.hash = self.calculate_hash();
        }
        
        println!(
            "区块 {} 挖矿成功! nonce: {}, hash: {}",
            self.index, self.nonce, self.hash
        );
    }
}
}

区块链结构

#![allow(unused)]
fn main() {
#[derive(Debug, Serialize, Deserialize)]
pub struct Blockchain {
    pub chain: Vec<Block>,
    pub difficulty: usize,
    pub pending_transactions: Vec<Transaction>,
    pub mining_reward: f64,
}

impl Blockchain {
    pub fn new(difficulty: usize) -> Self {
        let genesis_block = Block::new(
            0,
            "创世区块".to_string(),
            "0".repeat(64),
        );

        Blockchain {
            chain: vec![genesis_block],
            difficulty,
            pending_transactions: Vec::new(),
            mining_reward: 100.0,
        }
    }

    pub fn get_latest_block(&self) -> &Block {
        self.chain.last().expect("链不应为空")
    }

    pub fn add_block(&mut self, mut new_block: Block) {
        new_block.previous_hash = self.get_latest_block().hash.clone();
        new_block.mine(self.difficulty);
        self.chain.push(new_block);
    }

    pub fn is_chain_valid(&self) -> bool {
        for i in 1..self.chain.len() {
            let current = &self.chain[i];
            let previous = &self.chain[i - 1];

            // 验证当前区块哈希
            if current.hash != current.calculate_hash() {
                println!("区块 {} 的哈希无效", i);
                return false;
            }

            // 验证链的连续性
            if current.previous_hash != previous.hash {
                println!("区块 {} 的前一哈希链接断裂", i);
                return false;
            }

            // 验证工作量证明
            let target = "0".repeat(self.difficulty);
            if !current.hash.starts_with(&target) {
                println!("区块 {} 的工作量证明无效", i);
                return false;
            }
        }
        true
    }
}
}

交易与默克尔树

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transaction {
    pub from: String,
    pub to: String,
    pub amount: f64,
    pub timestamp: i64,
}

impl Transaction {
    pub fn new(from: String, to: String, amount: f64) -> Self {
        Transaction {
            from,
            to,
            amount,
            timestamp: Utc::now().timestamp(),
        }
    }

    pub fn hash(&self) -> String {
        let input = format!("{}{}{}{}", self.from, self.to, self.amount, self.timestamp);
        let mut hasher = Sha256::new();
        hasher.update(input);
        format!("{:x}", hasher.finalize())
    }
}

// 计算默克尔根
pub fn calculate_merkle_root(transactions: &[Transaction]) -> String {
    if transactions.is_empty() {
        return "0".repeat(64);
    }

    let mut hashes: Vec<String> = transactions.iter()
        .map(|tx| tx.hash())
        .collect();

    while hashes.len() > 1 {
        if hashes.len() % 2 != 0 {
            hashes.push(hashes.last().unwrap().clone());
        }

        let mut next_level = Vec::new();
        for i in (0..hashes.len()).step_by(2) {
            let combined = format!("{}{}", hashes[i], hashes[i + 1]);
            let mut hasher = Sha256::new();
            hasher.update(combined);
            next_level.push(format!("{:x}", hasher.finalize()));
        }
        hashes = next_level;
    }

    hashes[0].clone()
}
}

完整的区块链演示

use sha2::{Sha256, Digest};
use chrono::Utc;
use serde::{Serialize, Deserialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Transaction {
    from: String,
    to: String,
    amount: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Block {
    index: u64,
    timestamp: i64,
    transactions: Vec<Transaction>,
    previous_hash: String,
    merkle_root: String,
    hash: String,
    nonce: u64,
}

impl Block {
    fn new(index: u64, transactions: Vec<Transaction>, previous_hash: String) -> Self {
        let timestamp = Utc::now().timestamp();
        let merkle_root = calculate_merkle_root(&transactions);
        let mut block = Block {
            index,
            timestamp,
            transactions,
            previous_hash,
            merkle_root,
            hash: String::new(),
            nonce: 0,
        };
        block.hash = block.calculate_hash();
        block
    }

    fn calculate_hash(&self) -> String {
        let input = format!(
            "{}{}{}{}{}{}",
            self.index, self.timestamp, self.merkle_root,
            self.previous_hash, self.nonce,
            serde_json::to_string(&self.transactions).unwrap_or_default()
        );
        let mut hasher = Sha256::new();
        hasher.update(input);
        format!("{:x}", hasher.finalize())
    }

    fn mine(&mut self, difficulty: usize) {
        let target = "0".repeat(difficulty);
        while !self.hash.starts_with(&target) {
            self.nonce += 1;
            self.hash = self.calculate_hash();
        }
    }
}

fn calculate_merkle_root(transactions: &[Transaction]) -> String {
    if transactions.is_empty() {
        return "0".repeat(64);
    }
    let mut hashes: Vec<String> = transactions.iter().map(|tx| {
        let input = format!("{}{}{}", tx.from, tx.to, tx.amount);
        let mut hasher = Sha256::new();
        hasher.update(input);
        format!("{:x}", hasher.finalize())
    }).collect();

    while hashes.len() > 1 {
        if hashes.len() % 2 != 0 {
            hashes.push(hashes.last().unwrap().clone());
        }
        let mut next_level = Vec::new();
        for i in (0..hashes.len()).step_by(2) {
            let combined = format!("{}{}", hashes[i], hashes[i + 1]);
            let mut hasher = Sha256::new();
            hasher.update(combined);
            next_level.push(format!("{:x}", hasher.finalize()));
        }
        hashes = next_level;
    }
    hashes[0].clone()
}

struct Blockchain {
    chain: Vec<Block>,
    difficulty: usize,
    pending_transactions: Vec<Transaction>,
}

impl Blockchain {
    fn new() -> Self {
        let genesis = Block::new(0, vec![], "0".repeat(64));
        Blockchain {
            chain: vec![genesis],
            difficulty: 4,
            pending_transactions: Vec::new(),
        }
    }

    fn add_transaction(&mut self, tx: Transaction) {
        self.pending_transactions.push(tx);
    }

    fn mine_pending_transactions(&mut self, miner_address: String) {
        // 奖励交易
        let reward_tx = Transaction {
            from: "network".to_string(),
            to: miner_address,
            amount: 50.0,
        };
        self.pending_transactions.push(reward_tx);

        let mut block = Block::new(
            self.chain.len() as u64,
            self.pending_transactions.clone(),
            self.chain.last().unwrap().hash.clone(),
        );
        block.mine(self.difficulty);
        self.chain.push(block);
        self.pending_transactions.clear();
    }

    fn is_valid(&self) -> bool {
        for i in 1..self.chain.len() {
            let current = &self.chain[i];
            let previous = &self.chain[i - 1];

            if current.hash != current.calculate_hash() {
                return false;
            }
            if current.previous_hash != previous.hash {
                return false;
            }
            if !current.hash.starts_with(&"0".repeat(self.difficulty)) {
                return false;
            }
        }
        true
    }

    fn get_balance(&self, address: &str) -> f64 {
        let mut balance = 0.0;
        for block in &self.chain {
            for tx in &block.transactions {
                if tx.from == address {
                    balance -= tx.amount;
                }
                if tx.to == address {
                    balance += tx.amount;
                }
            }
        }
        balance
    }
}

fn main() {
    let mut blockchain = Blockchain::new();

    println!("=== 开始挖矿 ===");
    blockchain.add_transaction(Transaction {
        from: "alice".to_string(),
        to: "bob".to_string(),
        amount: 10.0,
    });
    blockchain.mine_pending_transactions("miner1".to_string());

    blockchain.add_transaction(Transaction {
        from: "bob".to_string(),
        to: "charlie".to_string(),
        amount: 5.0,
    });
    blockchain.mine_pending_transactions("miner1".to_string());

    println!("\n=== 区块链状态 ===");
    for block in &blockchain.chain {
        println!("区块 {}: {}", block.index, block.hash);
    }

    println!("\n=== 余额查询 ===");
    println!("miner1: {}", blockchain.get_balance("miner1"));
    println!("alice: {}", blockchain.get_balance("alice"));
    println!("bob: {}", blockchain.get_balance("bob"));
    println!("charlie: {}", blockchain.get_balance("charlie"));

    println!("\n=== 验证区块链 ===");
    println!("有效: {}", blockchain.is_valid());

    // 尝试篡改
    println!("\n=== 篡改测试 ===");
    if blockchain.chain.len() > 1 {
        blockchain.chain[1].transactions[0].amount = 1000.0;
        println!("篡改后有效: {}", blockchain.is_valid());
    }
}

Cargo.toml 依赖

[dependencies]
sha2 = "0.10"
chrono = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

51.6 本章总结

概念说明Rust 应用
哈希链用前一区块哈希链接区块,保证不可篡改sha2 crate
默克尔树高效验证交易完整性的二叉树结构自定义实现或 rs_merkle
PoW通过计算难题竞争记账权CPU/GPU 挖矿程序
PoS通过质押代币获得记账权验证者节点实现
智能合约区块链上自动执行的代码Solana/Substrate 合约
数字签名交易所有权验证ed25519-dalek, secp256k1

练习建议

  1. 基础练习:运行上述区块链示例,调整 difficulty 参数观察挖矿时间变化。

  2. 中级练习:为区块链添加数字签名验证:每笔交易必须由发送方签名,节点验证签名后才接受交易。

  3. 高级练习:实现一个简单的 P2P 网络同步:多个节点可以互相广播区块,维护一致的区块链副本。

  4. 实践项目:使用 substrate 框架搭建一条自定义区块链,或编写一个 Solana 智能合约实现简单的代币转账。


密码学箴言:区块链不是魔法,而是密码学原语的精妙组合。理解哈希、签名和共识,就理解了区块链的本质。

第五十二章 零知识证明

证明我知道秘密,但不透露秘密本身

零知识证明(Zero-Knowledge Proof, ZKP)是密码学中最迷人的概念之一。它允许证明者向验证者证明某个陈述为真,而无需透露任何超出陈述真实性之外的信息。本章将介绍零知识证明的基本原理、主要类型及其在 Rust 中的实现。

52.1 什么是零知识证明

直观理解

想象一个场景:Alice 声称她知道一个迷宫的秘密出口,但她不想透露出口的位置。她可以这样做:

  1. Alice 进入迷宫
  2. Bob 在入口等待,随机喊“从 A 口出“或“从 B 口出“
  3. Alice 从指定的出口出现
  4. 重复多次

如果 Alice 真的知道秘密路径,她每次都能从指定出口出现;如果她只是运气好,连续多次猜对的概率会指数级下降。关键是,Bob 从未看到 Alice 在迷宫中的路径——他只知道 Alice“似乎“知道出口。

零知识证明的三要素

一个完整的零知识证明系统必须满足三个性质:

  1. 完备性(Completeness):如果陈述为真,诚实的证明者可以说服诚实的验证者
  2. 可靠性(Soundness):如果陈述为假,任何欺骗性的证明者都无法以不可忽略的概率说服验证者
  3. 零知识性(Zero-Knowledge):验证者除了“陈述为真“之外,无法获得任何额外信息

形式化定义

公共输入 x
证明者私有输入 w(见证/witness)

关系 R(x, w) = true 表示"w 是 x 的有效见证"

证明者知道 w 使得 R(x, w) = true
    ↓
零知识证明协议
    ↓
验证者确信"存在 w 使得 R(x, w) = true"
但不知道 w 的任何信息

52.2 交互式 vs 非交互式

交互式零知识证明

经典的零知识证明是交互式的:证明者和验证者需要进行多轮通信。

示例:离散对数的零知识证明

公共参数:循环群 G,生成元 g,素数阶 p
公共输入:y = g^x mod p(Alice 声称知道 x)

1. Alice 选择随机数 r,计算 a = g^r mod p,发送 a 给 Bob
2. Bob 选择随机挑战 c,发送给 Alice
3. Alice 计算 s = r + c * x mod (p-1),发送 s 给 Bob
4. Bob 验证:g^s ≡ a * y^c (mod p)

验证等式成立的原因:

g^s = g^(r + c*x) = g^r * g^(c*x) = g^r * (g^x)^c = a * y^c

这个协议是零知识的,因为 Bob 可以从自己生成的随机数构造出与真实协议不可区分的“模拟“ transcript。

非交互式零知识证明(NIZK)

交互式证明在实际应用中很不方便。Fiat-Shamir 启发式方法可以将交互式协议转换为非交互式

将验证者的随机挑战替换为哈希函数的输出:
    c = Hash(g, y, a)

这样证明者可以一次性生成完整证明,验证者独立验证。

非交互式零知识证明的优势:

  • 证明可以公开广播,任何人都可以验证
  • 适合区块链等无需许可的环境
  • 证明可以被重复使用

交互式 vs 非交互式对比

特性交互式 ZKP非交互式 ZKP
通信轮数多轮一轮(证明者 → 验证者)
可公开验证
应用场景私有协议区块链、公开审计
转换方法Fiat-Shamir 启发式

52.3 zk-SNARKs 简介

什么是 zk-SNARK

zk-SNARK(Zero-Knowledge Succinct Non-Interactive Argument of Knowledge)是目前最实用的零知识证明系统之一,具备以下特性:

  • 零知识(Zero-Knowledge):不泄露见证信息
  • 简洁(Succinct):证明大小恒定(几百字节),验证时间极短(毫秒级)
  • 非交互(Non-Interactive):单条消息完成证明
  • 知识论证(Argument of Knowledge):证明者确实“知道“见证,而不仅仅是陈述为真

zk-SNARK 的工作原理(概述)

zk-SNARK 的核心思想是将计算转换为算术电路,然后将电路转换为多项式约束:

程序/计算
    ↓
算术电路(加法门、乘法门)
    ↓
R1CS(Rank-1 Constraint System,一阶约束系统)
    ↓
QAP(Quadratic Arithmetic Program,二次算术程序)
    ↓
多项式承诺(如 KZG、FRI)
    ↓
简洁证明

关键概念

  • 可信设置(Trusted Setup):某些 zk-SNARK 变体需要生成公共参考字符串(CRS),这个过程必须安全执行
  • 通用可信设置:如 Groth16 需要为每个电路单独设置
  • 透明设置:如 STARKs、Bulletproofs 不需要可信设置

主流 zk-SNARK 方案

方案证明大小验证时间可信设置后量子安全
Groth16192 字节1.5ms每个电路一次
PLONK~400 字节~3ms通用(一次)
STARKs~50KB~10ms无需
Bulletproofs~1KB~线性无需

零知识证明的应用场景

1. 隐私交易

区块链上的交易通常是透明的。ZKP 可以隐藏交易金额和参与方,同时保证交易有效性:

传统交易:
  Alice --10 BTC--> Bob(全网可见)

Zcash 隐私交易:
  ??? --??--> ???(金额和地址隐藏)
  但 ZKP 证明:发送方有足够余额,交易有效

2. 身份验证

证明你年满 18 岁,而不透露出生日期;证明你的信用评分高于阈值,而不透露具体分数。

3. 计算外包验证

将复杂计算外包给第三方,用 ZKP 验证计算结果的正确性,而无需重新执行计算。

4. 区块链扩容(Rollups)

将大量交易在链下执行,只向主链提交一个简洁的 ZKP 证明,证明所有链下交易都有效。

52.4 Rust 实现:零知识证明示例

使用 bellman 库实现简单电路

bellman 是 Zcash 团队开发的 Rust zk-SNARK 库,基于 Groth16 证明系统。

# Cargo.toml
[dependencies]
bellman = "0.14"
ff = "0.13"
bls12_381 = "0.8"
rand = "0.8"
#![allow(unused)]
fn main() {
use bellman::{Circuit, ConstraintSystem, SynthesisError};
use bls12_381::Scalar;
use ff::PrimeField;

// 证明:我知道 x 和 y,使得 x * y = public_output
struct MultiplicationCircuit {
    x: Option<Scalar>,
    y: Option<Scalar>,
}

impl Circuit<Scalar> for MultiplicationCircuit {
    fn synthesize<CS: ConstraintSystem<Scalar>>(
        self,
        cs: &mut CS,
    ) -> Result<(), SynthesisError> {
        // 分配私有输入 x
        let x = cs.alloc(|| "x", || {
            self.x.ok_or(SynthesisError::AssignmentMissing)
        })?;

        // 分配私有输入 y
        let y = cs.alloc(|| "y", || {
            self.y.ok_or(SynthesisError::AssignmentMissing)
        })?;

        // 分配公共输出
        let public_output = self.x.and_then(|x| {
            self.y.map(|y| x * y)
        });

        let output = cs.alloc_input(|| "output", || {
            public_output.ok_or(SynthesisError::AssignmentMissing)
        })?;

        // 约束:x * y = output
        cs.enforce(
            || "multiplication constraint",
            |lc| lc + x,
            |lc| lc + y,
            |lc| lc + output,
        );

        Ok(())
    }
}
}

生成和验证证明

use bellman::groth16::{generate_random_parameters, create_random_proof, verify_proof, PreparedVerifyingKey};
use bls12_381::{Bls12, Scalar};
use rand::rngs::OsRng;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let rng = &mut OsRng;

    // 1. 可信设置:为电路生成参数
    let params = {
        let c = MultiplicationCircuit { x: None, y: None };
        generate_random_parameters::<Bls12, _, _>(c, rng)?
    };

    // 准备验证密钥(加速验证)
    let pvk = PreparedVerifyingKey::from(params.vk.clone());

    // 2. 创建证明:我知道 3 和 5,它们的乘积是 15
    let public_input = Scalar::from(15);

    let proof = {
        let c = MultiplicationCircuit {
            x: Some(Scalar::from(3)),
            y: Some(Scalar::from(5)),
        };
        create_random_proof(c, &params, rng)?
    };

    // 3. 验证证明
    let is_valid = verify_proof(&pvk, &proof, &[public_input])?;

    println!("证明有效: {}", is_valid);

    // 尝试验证错误的输入
    let wrong_input = Scalar::from(100);
    let is_invalid = verify_proof(&pvk, &proof, &[wrong_input]).is_ok();
    println!("错误输入验证通过: {}", is_invalid);

    Ok(())
}

简化的零知识证明演示(不依赖外部库)

为了理解零知识证明的核心原理,我们实现一个极简的“颜色证明“示例——基于图同态的零知识证明概念:

use sha2::{Sha256, Digest};
use rand::Rng;

// 简化的承诺方案
fn commit(secret: &str, nonce: &[u8]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(secret.as_bytes());
    hasher.update(nonce);
    format!("{:x}", hasher.finalize())
}

// 证明者声称"我知道一个满足某种性质的值"
// 这里简化为:我知道一个值,其哈希的前 4 位是 0
struct Prover {
    secret: String,
}

impl Prover {
    fn new(secret: String) -> Self {
        Prover { secret }
    }

    // 生成承诺
    fn commit_secret(&self) -> (String, Vec<u8>) {
        let nonce: Vec<u8> = (0..16).map(|_| rand::thread_rng().gen::<u8>()).collect();
        let commitment = commit(&self.secret, &nonce);
        (commitment, nonce)
    }

    // 响应挑战
    fn respond(&self, challenge: u8) -> String {
        if challenge == 0 {
            // 揭示秘密
            self.secret.clone()
        } else {
            // 揭示其他信息(简化示例)
            format!("response_{}", challenge)
        }
    }
}

struct Verifier {
    commitment: String,
}

impl Verifier {
    fn new(commitment: String) -> Self {
        Verifier { commitment }
    }

    // 生成随机挑战
    fn challenge(&self) -> u8 {
        rand::thread_rng().gen_range(0..2)
    }

    // 验证响应
    fn verify(&self, response: &str, nonce: &[u8], challenge: u8) -> bool {
        if challenge == 0 {
            // 验证承诺是否对应揭示的秘密
            let recomputed = commit(response, nonce);
            recomputed == self.commitment
        } else {
            // 其他验证逻辑
            response.starts_with("response_")
        }
    }
}

fn main() {
    println!("=== 简化的零知识证明演示 ===\n");

    // 场景:证明者知道一个"特殊"字符串(比如 SHA-256 前导 0)
    let secret = "my_secret_value_42".to_string();
    let prover = Prover::new(secret);

    // 证明者生成承诺
    let (commitment, nonce) = prover.commit_secret();
    println!("承诺: {}", &commitment[..16]);

    // 验证者存储承诺并发起挑战
    let verifier = Verifier::new(commitment);

    // 多轮交互
    let mut success_count = 0;
    let rounds = 10;

    for round in 1..=rounds {
        let challenge = verifier.challenge();
        let response = prover.respond(challenge);
        let valid = verifier.verify(&response, &nonce, challenge);

        println!(
            "轮次 {}: 挑战={}, 验证={}",
            round, challenge, valid
        );

        if valid {
            success_count += 1;
        }
    }

    println!("\n成功率: {}/{} = {:.0}%", success_count, rounds,
             (success_count as f64 / rounds as f64) * 100.0);

    if success_count == rounds {
        println!("验证者确信:证明者知道秘密!");
    }
}

范围证明概念(Bulletproofs 风格)

范围证明是 ZKP 的重要应用:证明一个值在某个范围内,而不透露具体值。

use sha2::{Sha256, Digest};

// 概念演示:证明 value 在 [0, 2^n) 范围内
// 实际实现需要使用 Pedersen 承诺和内部乘积论证

struct RangeProofConcept {
    n: usize,  // 位数
}

impl RangeProofConcept {
    // 将值分解为二进制位
    fn decompose(value: u64, n: usize) -> Vec<bool> {
        let mut bits = Vec::with_capacity(n);
        for i in 0..n {
            bits.push(((value >> i) & 1) == 1);
        }
        bits
    }

    // 验证二进制分解的正确性
    fn verify_decomposition(value: u64, bits: &[bool]) -> bool {
        let reconstructed: u64 = bits.iter()
            .enumerate()
            .map(|(i, &b)| if b { 1u64 << i } else { 0 })
            .sum();
        reconstructed == value
    }

    // 概念:证明每个位是 0 或 1
    fn prove_bit_is_binary(bit: bool) -> &'static str {
        // 在实际 ZKP 中,这转化为约束:b * (1 - b) = 0
        // 即 b 只能是 0 或 1
        if bit {
            "bit = 1: 1 * (1 - 1) = 0 ✓"
        } else {
            "bit = 0: 0 * (1 - 0) = 0 ✓"
        }
    }
}

fn main() {
    let proof = RangeProofConcept { n: 32 };

    let value = 12345u64;
    let bits = RangeProofConcept::decompose(value, 32);

    println!("值: {}", value);
    println!("二进制分解: {:?}", bits);
    println!("分解验证: {}", RangeProofConcept::verify_decomposition(value, &bits));

    println!("\n每位验证:");
    for (i, &bit) in bits.iter().take(8).enumerate() {
        println!("  位 {}: {}", i, RangeProofConcept::prove_bit_is_binary(bit));
    }

    // 范围验证
    let max_value = (1u64 << 32) - 1;
    println!("\n范围验证: 0 <= {} <= {} → {}", value, max_value,
             value <= max_value);
}

使用 arkworks 进行现代 ZKP 开发

arkworks 是 Rust 生态中更现代的零知识证明框架,提供了模块化的代数组件。

#![allow(unused)]
fn main() {
// arkworks 概念示例
use ark_ff::Field;
use ark_bls12_381::Fr;

// 在有限域上运算
fn field_operations() {
    let a = Fr::from(3u64);
    let b = Fr::from(5u64);

    let sum = a + b;
    let product = a * b;
    let inverse = a.inverse().expect("可逆");

    println!("3 + 5 = {:?}", sum);
    println!("3 * 5 = {:?}", product);
    println!("3^-1 = {:?}", inverse);
    println!("3 * 3^-1 = {:?}", a * inverse);  // 应等于 1
}
}

52.5 零知识证明的挑战与前沿

当前挑战

  1. 可信设置的安全性:Groth16 等方案需要可信设置,如果设置过程被泄露,整个系统的安全性将崩溃
  2. 计算开销:生成证明的计算成本仍然很高
  3. 电路复杂性:将程序转换为算术电路需要专业知识
  4. 量子计算威胁:基于椭圆曲线的 ZKP 面临量子计算的潜在威胁(STARKs 是后量子安全的替代方案)

前沿发展方向

  • 硬件加速:GPU、FPGA、ASIC 用于加速证明生成
  • 递归证明:在一个证明中验证另一个证明,实现无限压缩
  • zk-EVM:在零知识证明中执行以太坊虚拟机指令
  • 身份与合规:KYC/AML 与隐私保护的结合

52.6 本章总结

概念说明Rust 生态
交互式 ZKP多轮通信完成证明理论演示
非交互式 ZKP单条消息,可公开验证Fiat-Shamir 转换
zk-SNARK简洁非交互零知识证明bellman, arkworks
zk-STARK透明设置、后量子安全winterfell
范围证明证明值在范围内bulletproofs
电路编译程序 → 算术电路circom-compat

练习建议

  1. 基础练习:运行上述 bellman 乘法电路示例,尝试修改电路证明其他关系(如 x + y = z)。

  2. 中级练习:实现一个简化的离散对数零知识证明(Sigma 协议),使用 Fiat-Shamir 转换为非交互式。

  3. 高级练习:使用 arkworks 构建一个更复杂的电路,如证明你知道一个 Merkle 树中的叶子节点( membership proof)。

  4. 实践项目:研究 Zcash 或 Filecoin 中零知识证明的应用,尝试运行它们的 Rust 节点或客户端。


密码学箴言:零知识证明是密码学的“魔术“——它让你确信对方知道秘密,同时确信自己什么都没学到。在隐私与验证之间,ZKP 找到了完美的平衡。

第五十三 同态加密

数据不解密也能算:让加密数据“活“起来

在传统的密码学应用中,数据必须先解密才能进行计算。这意味着处理敏感数据时,必须在安全的环境中解密,计算完成后再重新加密。如果数据需要交由第三方(如云计算服务商)处理,就必须将明文暴露给对方,带来潜在的隐私泄露风险。

同态加密(Homomorphic Encryption, HE)打破了这一限制。它允许在密文上直接进行计算,计算结果解密后与在明文上计算的结果一致。换句话说,数据始终保持加密状态,却能“参与“运算——这是密码学领域最具颠覆性的技术之一。

53.1 什么是同态加密

直观理解

想象你有一位不信任的会计,你需要让他帮你计算年度总支出,但又不想让他看到每一笔具体的开支金额。同态加密就像给每一张账单都套上一个不透明的信封,会计可以在信封上进行某种“魔法操作“,最终得到一个结果信封。你打开结果信封,里面正是所有账单金额的总和——而会计自始至终都没有看到任何一张账单的具体数字。

形式化定义

一个加密方案称为同态的,如果满足:

Decrypt( Evaluate( f, Encrypt(m1), Encrypt(m2), ..., Encrypt(mn) ) ) = f(m1, m2, ..., mn)

其中 f 是任意计算函数。也就是说,对密文进行计算后再解密,等于先解密再计算。

同态性的分类

根据支持的运算类型和深度,同态加密分为三个层次:

部分同态加密(PHE, Partially Homomorphic Encryption)

只支持无限次的某一种运算(加法或乘法),但不能同时进行两种运算。

  • 加法同态:支持任意次数的密文相加

    • 典型代表:Paillier、Benaloh、Naccache-Stern
    • 应用:电子投票、隐私保护求和
  • 乘法同态:支持任意次数的密文相乘

    • 典型代表:RSA(未经填充的原始RSA)、ElGamal
    • 应用:隐私保护乘积计算

somewhat 同态加密(SHE, Somewhat Homomorphic Encryption)

同时支持加法和乘法运算,但运算深度有限。随着计算步骤的增加,密文中的“噪声“会累积,超过一定阈值后就无法正确解密。

  • 典型代表:BGV(Brakerski-Gentry-Vaikuntanathan)的早期版本
  • 特点:可以计算任意多项式,但多项式的次数受限

全同态加密(FHE, Fully Homomorphic Encryption)

支持任意次数的加法和乘法运算,理论上可以计算任何可计算函数。这是密码学的“圣杯“,直到2009年才由 Craig Gentry 首次实现。

  • 典型代表:
    • Gentry 方案(基于理想格)
    • BGV/BFV 方案(基于环上学习 with 错误,RLWE)
    • CKKS 方案(支持浮点数近似计算)
    • TFHE 方案(快速布尔电路计算)
类型加法乘法运算深度效率成熟度
PHE无限次0次 或 无限次无限制(单一运算)成熟
SHE有限次有限次受限较成熟
FHE无限次无限次无限制快速发展中

53.2 Paillier 算法简介

Paillier 加密系统是由 Pascal Paillier 于1999年提出的概率公钥加密方案,是目前最著名且应用最广泛的加法同态加密算法。

核心特性

  1. 加法同态性

    E(m1) * E(m2) = E(m1 + m2)
    

    两个密文相乘,解密后得到明文之和。

  2. 明文乘法

    E(m)^k = E(m * k)
    

    密文的 k 次幂,解密后得到明文与 k 的乘积。

  3. 概率加密:相同的明文加密后会产生不同的密文,提供语义安全性。

数学基础

Paillier 的安全性基于合数剩余类问题(Composite Residuosity Assumption):给定合数 n = pq(两个大素数之积),区分 阶的 n 次剩余和非剩余是困难的。

密钥生成

1. 选择两个大素数 p 和 q,满足 gcd(pq, (p-1)(q-1)) = 1
2. 计算 n = p * q,λ = lcm(p-1, q-1)
3. 选择随机整数 g ∈ Z*_{n²}
4. 计算 μ = (L(g^λ mod n²))^{-1} mod n,其中 L(x) = (x-1)/n
5. 公钥:(n, g),私钥:(λ, μ)

53.3 应用场景

隐私计算(Privacy-Preserving Computation)

在云计算场景中,用户可以将加密数据上传到云端,云服务商在不知道明文的情况下完成计算并返回结果。典型应用包括:

  • 隐私保护机器学习:在加密数据上训练或推理模型
  • 加密数据库查询:SQL 查询在密文上执行
  • 安全统计分析:医院联合计算疾病发病率,不泄露患者数据

安全多方计算(MPC, Multi-Party Computation)

多个参与方希望共同计算一个函数,但各自的数据保持私密。同态加密是 MPC 的重要构建模块之一。

例如:多家银行想计算行业平均存款额,但不想暴露各自的存款数据。通过同态加密,每家银行加密自己的数据上传,计算方在密文上求平均,结果只有各方联合才能解密。

电子投票

利用加法同态性,可以设计隐私保护的电子投票系统:

  • 每个选民加密自己的选票(0或1)
  • 将所有密文相乘(同态加法)得到总票数密文
  • 只有选举委员会能解密最终结果
  • 任何中间人都无法知道单个选民的投票选择

隐私保护机器学习

在联邦学习中,各参与方上传加密的模型梯度,服务器在密文上聚合更新全局模型,无需看到任何一方的原始梯度数据。

53.4 Rust 实现

Rust 生态中有多个同态加密库可供选择。下面介绍使用 rust-paillier 进行加法同态加密的示例。

Cargo.toml 依赖

[dependencies]
paillier = "0.4"
rand = "0.8"

基础加密解密示例

use paillier::{Keypair, EncryptionKey, DecryptionKey};
use paillier::Paillier;
use rand::thread_rng;

fn main() {
    // 1. 生成密钥对(实际应用应使用更大的密钥,如2048位)
    let (ek, dk): (EncryptionKey, DecryptionKey) = Paillier::keypair(&mut thread_rng()).keys();

    // 2. 待加密的明文
    let m1 = 10u64;
    let m2 = 20u64;

    // 3. 加密
    let c1 = Paillier::encrypt(&ek, m1);
    let c2 = Paillier::encrypt(&ek, m2);

    println!("明文 m1 = {}, m2 = {}", m1, m2);
    println!("密文 c1 = {:?}", c1);
    println!("密文 c2 = {:?}", c2);

    // 4. 解密验证
    let d1 = Paillier::decrypt(&dk, &c1);
    let d2 = Paillier::decrypt(&dk, &c2);

    println!("解密 d1 = {}, d2 = {}", d1, d2);
    assert_eq!(m1, d1);
    assert_eq!(m2, d2);
}

加法同态运算

use paillier::{EncryptionKey, DecryptionKey, Paillier};
use rand::thread_rng;

fn main() {
    let (ek, dk): (EncryptionKey, DecryptionKey) = Paillier::keypair(&mut thread_rng()).keys();

    let m1 = 15u64;
    let m2 = 25u64;

    // 加密两个明文
    let c1 = Paillier::encrypt(&ek, m1);
    let c2 = Paillier::encrypt(&ek, m2);

    // 同态加法:密文相乘 = 明文相加
    let c_sum = Paillier::add(&ek, &c1, &c2);

    // 解密结果
    let m_sum = Paillier::decrypt(&dk, &c_sum);

    println!("{} + {} = {} (同态计算结果)", m1, m2, m_sum);
    assert_eq!(m_sum, m1 + m2);

    // 同态数乘:密文的 k 次幂 = 明文乘以 k
    let k = 3u64;
    let c_mul = Paillier::mul(&ek, &c1, k);
    let m_mul = Paillier::decrypt(&dk, &c_mul);

    println!("{} * {} = {} (同态计算结果)", m1, k, m_mul);
    assert_eq!(m_mul, m1 * k);
}

隐私保护求和场景

#![allow(unused)]
fn main() {
use paillier::{EncryptionKey, DecryptionKey, Paillier};
use rand::thread_rng;

/// 模拟隐私保护求和:多个参与方贡献数据,计算总和但不泄露各自数据
fn privacy_preserving_sum() {
    let (ek, dk): (EncryptionKey, DecryptionKey) = Paillier::keypair(&mut thread_rng()).keys();

    // 三个参与方的敏感数据
    let alice_salary = 5000u64;
    let bob_salary = 7000u64;
    let charlie_salary = 6000u64;

    // 各自加密自己的数据
    let c_alice = Paillier::encrypt(&ek, alice_salary);
    let c_bob = Paillier::encrypt(&ek, bob_salary);
    let c_charlie = Paillier::encrypt(&ek, charlie_salary);

    println!("Alice 加密了工资数据");
    println!("Bob 加密了工资数据");
    println!("Charlie 加密了工资数据");

    // 计算服务方在密文上求和(看不到任何明文)
    let c_total = Paillier::add(
        &ek,
        &Paillier::add(&ek, &c_alice, &c_bob),
        &c_charlie,
    );

    // 只有持有私钥的一方能解密结果
    let total_salary = Paillier::decrypt(&dk, &c_total);
    let expected = alice_salary + bob_salary + charlie_salary;

    println!("工资总和 = {} (期望: {})", total_salary, expected);
    assert_eq!(total_salary, expected);

    // 还可以计算平均值(同态数乘实现除法)
    let count = 3u64;
    let c_avg = Paillier::mul(&ek, &c_total, 1); // 这里简化为先解密再除,实际可用更复杂协议
    let avg_salary = Paillier::decrypt(&dk, &c_avg) / count;
    println!("平均工资 = {}", avg_salary);
}
}

全同态加密的 Rust 生态

对于需要全同态加密的场景,Rust 生态正在快速发展:

  • concrete:Zama 公司开发的全同态加密库,基于 TFHE 方案,支持布尔和整数运算
  • fhe.rs:纯 Rust 实现的 FHE 库,支持 BFV 和 BGV 方案
  • sunscreen:提供编译器将普通 Rust 代码转换为 FHE 电路
[dependencies]
concrete = "0.8"
#![allow(unused)]
fn main() {
// concrete 库的简单示例(概念演示)
use concrete::*;

fn fhe_example() -> Result<(), CryptoAPIError> {
    // 1. 定义加密参数
    let secret_key = LWESecretKey::new(&LWE128_630);
    
    // 2. 创建编码器(定义明文的数值范围)
    let encoder = Encoder::new(-10., 10., 5, 1)?;
    
    // 3. 加密明文
    let m1 = 3.;
    let m2 = 5.;
    let c1 = LWE::encode_encrypt(&secret_key, m1, &encoder)?;
    let c2 = LWE::encode_encrypt(&secret_key, m2, &encoder)?;
    
    // 4. 同态加法
    let c_add = &c1 + &c2;
    
    // 5. 解密
    let m_add = c_add.decrypt_decode(&secret_key)?;
    println!("{} + {} = {}", m1, m2, m_add);
    
    Ok(())
}
}

53.5 同态加密的挑战与展望

当前挑战

  1. 计算开销大:FHE 的密文膨胀严重,计算速度比明文慢数万到数百万倍
  2. 噪声管理:SHE/FHE 需要复杂的噪声控制机制(如自举 bootstrapping)
  3. 功能限制:部分方案只支持整数运算,浮点数支持仍在发展中
  4. 标准化不足:相比传统密码学,HE 的标准化工作仍在初期

发展趋势

  • 硬件加速:专用 FPGA/ASIC 加速器正在开发,有望将性能提升数个数量级
  • 方案优化:CKKS 方案在机器学习场景中表现优异,TFHE 在布尔电路中效率突出
  • 混合方案:将同态加密与 MPC、零知识证明等技术结合,取长补短

53.6 本章总结

概念说明典型算法/库
PHE部分同态加密,支持无限次单一运算Paillier、ElGamal
SHEsomewhat 同态加密,支持有限深度加减乘BGV(早期)、BFV
FHE全同态加密,支持任意计算CKKS、TFHE、concrete
加法同态密文相乘等价于明文相加Paillier
乘法同态密文相乘等价于明文相乘RSA、ElGamal
噪声密文中累积的计算误差,需控制或刷新Bootstrapping

练习建议

  1. 基础练习:使用 rust-paillier 实现一个加密计算器,支持密文加法和数乘运算,验证同态性质。

  2. 中级练习:模拟电子投票系统:10个选民各自加密投票(0或1),在密文上统计总票数,确保单个投票不可追踪。

  3. 高级练习:调研 concretefhe.rs 库,实现一个简单的全同态加密示例(如密文上的多项式求值),分析其性能开销。

  4. 实践项目:设计一个隐私保护的数据聚合服务:多个客户端上传加密数据,服务端在密文上计算统计指标(均值、方差),客户端联合解密结果。

第五十四 量子密码学

用量子对抗量子:密码学的新战场

量子计算机的出现是21世纪计算领域最具颠覆性的技术变革之一。对于密码学而言,这既是巨大的威胁,也是全新的机遇。一方面,量子计算机有能力破解当前广泛使用的RSA和椭圆曲线密码;另一方面,量子力学原理也为构建理论上不可破解的密码系统提供了全新的工具。

本章将探讨量子计算对传统密码的威胁、量子密钥分发技术,以及正在兴起的后量子密码学标准。

54.1 量子计算对传统密码的威胁

量子计算的威力

传统计算机使用比特(bit)作为信息的基本单位,每个比特要么是0要么是1。量子计算机使用量子比特(qubit),它可以同时处于0和1的叠加态。这使得量子计算机在处理特定问题上具有指数级加速的潜力。

Shor 算法:公钥密码的噩梦

1994年,数学家 Peter Shor 提出了Shor 算法,证明量子计算机可以在多项式时间内解决以下问题:

  • 大整数分解:给定大合数 n = p * q,快速找到素因子 pq
  • 离散对数问题:在有限域或椭圆曲线群上求解离散对数

这意味着:

密码算法基于的数学难题量子威胁
RSA大整数分解可被 Shor 算法破解
Diffie-Hellman离散对数可被 Shor 算法破解
ECC(椭圆曲线)椭圆曲线离散对数可被 Shor 算法破解
DSA/ECDSA离散对数/椭圆曲线离散对数可被 Shor 算法破解

实际影响:一台拥有约4000个逻辑量子比特的量子计算机,就能在合理时间内破解2048位RSA密钥。虽然目前的量子计算机距离这个规模还很远,但密码系统的部署周期往往长达数十年,因此必须提前准备。

Grover 算法:对称密码的减半威胁

1996年,Lov Grover 提出了Grover 搜索算法,它可以将无序数据库的搜索复杂度从 O(N) 降低到 O(√N)

对密码学的影响:

  • 对称加密:有效密钥长度减半。例如,AES-256 的安全性降低到约128位(仍然安全),但 AES-128 的安全性降低到约64位(不再安全)
  • 哈希函数:碰撞攻击的复杂度从 O(2^(n/2)) 降低到 O(2^(n/3)), preimage 攻击从 O(2^n) 降低到 O(2^(n/2))

应对策略:对称加密和哈希函数只需加倍密钥/输出长度即可恢复安全性。例如,使用 AES-256 代替 AES-128,使用 SHA-384/512 代替 SHA-256。

54.2 量子密钥分发(QKD)

量子密钥分发利用量子力学原理,让两个通信方可以生成共享的随机密钥,并确保任何窃听行为都会被发现。与基于数学难题的传统密码不同,QKD 的安全性由物理定律保证。

BB84 协议

1984年,Charles Bennett 和 Gilles Brassard 提出了第一个量子密钥分发协议——BB84。其核心思想是利用量子态的不可克隆性和测量坍缩特性来检测窃听。

基本原理

  1. 量子态编码:发送方 Alice 使用两种基( rectilinear + 和 diagonal × )来编码比特:

    • + 基: = 0,90° = 1
    • × 基:45° = 0,135° = 1
  2. 随机选择:Alice 随机选择基和比特值发送光子。Bob 也随机选择基进行测量。

  3. 基比对:通过公开信道,Alice 和 Bob 比较各自使用的基,只保留使用相同基的那些比特。

  4. 错误检测:随机抽取部分比特公开比较,计算误码率。如果误码率超过阈值(通常11%),说明存在窃听者 Eve。

为什么能检测窃听?

量子力学的两个核心原理保证了安全性:

  • 不可克隆定理:未知量子态不能被完美复制。Eve 无法复制光子留一份给自己。
  • 测量坍缩:测量会改变量子态。Eve 的测量会引入可检测的错误。

如果 Eve 试图窃听:

  1. 她必须选择基来测量光子
  2. 她有50%的概率选错基
  3. 选错基时,她的测量会改变光子状态
  4. 这导致 Bob 接收到的数据中有约25%的错误
  5. Alice 和 Bob 通过比对可以发现这些异常

QKD 的局限性

尽管 QKD 在理论上是完美的,但实际部署面临挑战:

  1. 距离限制:光纤传输损耗限制了距离,目前最长约400-500公里
  2. 需要认证信道:经典信道需要认证,防止中间人攻击
  3. 设备安全性:实际设备可能存在侧信道漏洞
  4. 速率限制:密钥生成速率远低于传统密钥交换

54.3 后量子密码学(PQC)

后量子密码学(Post-Quantum Cryptography)研究的是能够抵抗量子计算机攻击的密码算法。与 QKD 不同,PQC 仍然基于数学难题,只是选择了量子计算机也难以解决的问题。

量子安全的数学难题

问题类别代表问题量子抵抗性
基于格的密码最短向量问题(SVP)、学习 with 错误(LWE)被认为是量子困难的
基于编码的密码随机线性码的译码问题被认为是量子困难的
基于多变量的密码多元多项式方程组求解被认为是量子困难的
基于哈希的密码哈希函数的安全性只需加倍输出长度
基于同源的密码超奇异椭圆曲线同源问题新兴方向

主要 PQC 方案家族

基于格的密码(Lattice-based)

格是 n 维空间中的离散点集。格密码基于以下困难问题:

  • 最短向量问题(SVP):在格中找到最短的非零向量
  • 最近向量问题(CVP):找到格中最接近给定向量的点
  • LWE(Learning With Errors):从带噪声的线性方程中恢复秘密

格密码的优势:

  • 密钥和密文尺寸相对较小
  • 计算效率高
  • 功能丰富(支持加密、签名、同态运算)

基于编码的密码(Code-based)

基于纠错码理论,最早由 Robert McEliece 于1978年提出。

  • McEliece 密码系统:使用随机线性码的译码困难性
  • 优势:历史悠久,安全性研究充分
  • 劣势:公钥尺寸很大(MB级别)

基于多变量的密码(Multivariate-based)

基于有限域上多元多项式方程组的求解困难性。

  • 优势:签名方案速度快,签名尺寸小
  • 劣势:密钥尺寸较大,部分方案已被攻破

基于哈希的密码(Hash-based)

利用哈希函数的安全性构建签名方案。

  • Lamport 签名Merkle 签名:安全性完全依赖于哈希函数
  • 优势:安全性分析简单直观
  • 劣势:一次性签名或有状态签名,使用复杂

54.4 NIST 后量子密码标准

2022年至2024年,美国国家标准与技术研究院(NIST)经过多轮评估,正式发布了首批后量子密码标准算法。

标准化算法

算法类型基于的数学结构用途
CRYSTALS-KyberKEM(密钥封装机制)基于模格(MLWE)密钥交换、加密
CRYSTALS-Dilithium数字签名基于模格(MLWE/MSIS)身份认证、签名
SPHINCS+数字签名基于哈希高安全性签名
FALCON数字签名基于格(NTRU)短签名场景

CRYSTALS-Kyber

Kyber 是一种 IND-CCA2 安全的密钥封装机制(KEM),用于替代 ECDH 密钥交换。

核心特点

  • 安全性基于模格上的 LWE 问题(Module-LWE)
  • 密钥尺寸小,计算速度快
  • 提供三种安全级别:Kyber-512、Kyber-768、Kyber-1024

工作流程

1. 接收方生成公钥 pk 和私钥 sk
2. 发送方使用 pk 封装,得到密文 c 和共享密钥 k
3. 接收方使用 sk 解封装 c,恢复相同的共享密钥 k

CRYSTALS-Dilithium

Dilithium 是一种数字签名算法,用于替代 ECDSA 和 RSA 签名。

核心特点

  • 安全性基于模格上的短整数解问题(Module-SIS)和 LWE
  • 签名尺寸较小,验证速度快
  • 提供三种安全级别

54.5 Rust 实现

Rust 生态中已有多个后量子密码库实现。下面介绍使用 pqcrypto 的示例。

Cargo.toml 依赖

[dependencies]
pqcrypto-traits = "0.3"
pqcrypto-kyber = "0.7"
pqcrypto-dilithium = "0.5"
rand = "0.8"

Kyber 密钥封装示例

use pqcrypto_kyber::kyber768;
use pqcrypto_traits::kem::{Ciphertext, PublicKey, SecretKey, SharedSecret};

fn main() {
    // 1. 接收方(Bob)生成密钥对
    let (pk, sk) = kyber768::keypair();
    println!("Bob 生成了 Kyber-768 密钥对");
    println!("公钥长度: {} bytes", pk.as_bytes().len());
    println!("私钥长度: {} bytes", sk.as_bytes().len());

    // 2. 发送方(Alice)使用 Bob 的公钥封装密钥
    let (ciphertext, shared_secret_alice) = kyber768::encapsulate(&pk);
    println!("\nAlice 封装了共享密钥");
    println!("密文长度: {} bytes", ciphertext.as_bytes().len());
    println!("Alice 的共享密钥: {:02x?}", shared_secret_alice.as_bytes());

    // 3. Bob 使用私钥解封装,恢复共享密钥
    let shared_secret_bob = kyber768::decapsulate(&ciphertext, &sk);
    println!("\nBob 解封装了共享密钥");
    println!("Bob 的共享密钥:   {:02x?}", shared_secret_bob.as_bytes());

    // 4. 验证双方密钥一致
    assert_eq!(
        shared_secret_alice.as_bytes(),
        shared_secret_bob.as_bytes(),
        "共享密钥不一致!"
    );
    println!("\n✓ 共享密钥一致,可以开始对称加密通信");
}

Dilithium 数字签名示例

use pqcrypto_dilithium::dilithium3;
use pqcrypto_traits::sign::{PublicKey, SecretKey, SignedMessage, Signer, Verifier};

fn main() {
    // 1. 生成签名密钥对
    let (pk, sk) = dilithium3::keypair();
    println!("生成了 Dilithium3 签名密钥对");
    println!("公钥长度: {} bytes", pk.as_bytes().len());
    println!("私钥长度: {} bytes", sk.as_bytes().len());

    // 2. 待签名的消息
    let message = b"这是一份需要量子安全保护的重要合同";
    println!("\n待签名消息: {}", String::from_utf8_lossy(message));

    // 3. 使用私钥签名
    let signed_msg = sk.sign(message);
    println!("签名完成");
    println!("签名长度: {} bytes", signed_msg.as_bytes().len());

    // 4. 使用公钥验证签名
    let verified = pk.verify(&signed_msg, message);
    match verified {
        Ok(()) => println!("✓ 签名验证通过"),
        Err(e) => println!("✗ 签名验证失败: {:?}", e),
    }

    // 5. 验证篡改检测
    let mut tampered_msg = message.to_vec();
    tampered_msg[0] ^= 0xFF; // 篡改第一个字节
    let tampered_signed = sk.sign(&tampered_msg);
    
    match pk.verify(&tampered_signed, message) {
        Ok(()) => println!("异常:篡改未被发现"),
        Err(_) => println!("✓ 正确检测到消息篡改"),
    }
}

混合加密:传统 + 后量子

在量子计算机真正出现之前,一种务实的策略是采用混合方案:同时使用传统算法和后量子算法,确保即使其中一种被攻破,整体仍然安全。

#![allow(unused)]
fn main() {
use pqcrypto_kyber::kyber768;
use pqcrypto_traits::kem::{Ciphertext, PublicKey, SecretKey, SharedSecret};
use rand::thread_rng;
use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey};

/// 混合密钥交换:X25519 + Kyber768
fn hybrid_key_exchange() {
    // ===== 传统部分:X25519 =====
    let alice_x_secret = EphemeralSecret::random_from_rng(thread_rng());
    let alice_x_public = X25519PublicKey::from(&alice_x_secret);

    let bob_x_secret = EphemeralSecret::random_from_rng(thread_rng());
    let bob_x_public = X25519PublicKey::from(&bob_x_secret);

    let alice_x_shared = alice_x_secret.diffie_hellman(&bob_x_public);
    let bob_x_shared = bob_x_secret.diffie_hellman(&alice_x_public);
    assert_eq!(alice_x_shared.as_bytes(), bob_x_shared.as_bytes());

    // ===== 后量子部分:Kyber768 =====
    let (kyber_pk, kyber_sk) = kyber768::keypair();
    let (kyber_ct, kyber_ss_enc) = kyber768::encapsulate(&kyber_pk);
    let kyber_ss_dec = kyber768::decapsulate(&kyber_ct, &kyber_sk);
    assert_eq!(kyber_ss_enc.as_bytes(), kyber_ss_dec.as_bytes());

    // ===== 混合共享密钥 =====
    // 将两个共享密钥进行哈希混合,即使其中一个被攻破,另一个仍提供安全性
    use sha2::{Sha256, Digest};
    let mut hasher = Sha256::new();
    hasher.update(alice_x_shared.as_bytes());
    hasher.update(kyber_ss_enc.as_bytes());
    let hybrid_shared_secret = hasher.finalize();

    println!("混合共享密钥: {:02x?}", hybrid_shared_secret);
    println!("✓ 同时受 X25519 和 Kyber768 保护");
}
}

54.6 迁移路线图

向量子安全密码的迁移是一项长期工程,建议采取以下策略:

  1. 密码清单:梳理系统中所有使用的密码算法和密钥长度
  2. 风险评估:识别需要优先保护的高价值数据和长期保密数据
  3. 混合部署:在关键系统中同时部署传统和后量子算法
  4. 算法敏捷性:设计支持算法替换的架构,避免硬编码特定算法
  5. 持续跟踪:关注 NIST 等标准化组织的最新进展

54.7 本章总结

概念说明状态/建议
Shor 算法量子算法,可破解 RSA/ECC威胁已确认,需迁移
Grover 算法量子搜索,对称密钥减半使用 AES-256、SHA-384+
BB84 协议量子密钥分发已商用,但有距离限制
基于格的密码Kyber、Dilithium 的基础NIST 标准化,推荐采用
基于哈希的签名SPHINCS+高安全性,签名较大
混合加密传统 + 后量子组合当前最佳实践

练习建议

  1. 基础练习:使用 pqcrypto-kyber 实现完整的密钥封装流程,验证封装/解封装的一致性。

  2. 中级练习:使用 pqcrypto-dilithium 实现文件签名工具,能够对任意文件进行签名和验证。

  3. 高级练习:实现一个混合 TLS 握手模拟:结合 X25519 和 Kyber768 进行密钥交换,比较纯传统方案和混合方案的性能开销。

  4. 实践项目:调研你的现有项目(或常用开源项目)中使用的密码算法,制定一份向量子安全密码迁移的评估报告。

第七十一 无敌密码

历史上的“无敌密码“

密码,自古以来就是保护秘密的核心手段。从古罗马的凯撒密码到二战时期的恩尼格码,从纳瓦荷语的“风语者“到中国共产党第一本密码“豪密“,密码战贯穿了整个人类战争史。在信息时代,密码学更是计算机安全、网络通信、金融交易的基石。本章将从历史上的经典密码战出发,深入讲解中国国密(商用密码)算法体系,并使用 Rust 实现完整的国密加密示例。

风语者:纳瓦荷语加密

在电影《风语者》中,美军为了防止无线电通信被日军监听、破译,征召懂得纳瓦荷语的部落士兵入伍,并使用纳瓦荷语加密情报。经过训练的部落士兵成为了“风语者“,通过语言加密传递情报。由于懂得纳瓦荷语的人极少,并且经过进一步加密后,即使是本族部落的人没有经过专门的训练也无法破译。

纳瓦荷语是一种阿萨巴斯卡语系的语言,语法结构复杂,没有文字,仅靠口耳相传。在二战之前,全世界懂纳瓦荷语的非纳瓦荷人不到30人。美军将军事术语与纳瓦荷语词汇对应起来,例如:

英文术语纳瓦荷语直译
Fighter planeDa-he-tih-hi蜂鸟
BattleshipLo-tso鲸鱼
SubmarineBesh-lo铁鱼
TankChay-da-gahi-ni乌龟
GrenadeNi-ma-si土豆

这种密码直到战争结束也没有被破译,堪称“无敌密码“。在硫磺岛战役中,6名风语者在48小时内传递了超过800条消息,无一出错。但要想破译密码最简单的方式就是俘虏“风语者“,逼迫其协助破译密码,这在电影中有比较深刻的描述。由此可见使用极少人使用的语言(“部落语言”、“方言”)加密情报,尽管有一定的局限性,但在特定的场景下仍是十分可靠有效的方式。

豪密:中共第一本密码

豪密,是中国共产党和中国工农红军第一本无线电通讯密码的简称。由中国共产党初期领导人之一周恩来(化名:伍豪)编制。

1928年,周恩来在上海秘密编制了这套密码系统。“豪密“的命名来源于周恩来的化名“伍豪“中的“豪“字。这套密码采用了一种独创的加密方法,将汉字转换为数字编码,再进行二次加密处理。其核心特点包括:

  • 双层加密:先将明文转换为数字编码,再对数字序列进行位移和替换加密
  • 动态密钥:密钥定期更换,即使截获部分密文也无法推导出完整密钥
  • 无规律性:加密后的密文看似随机,没有明显的统计规律

从1928年编制到1949年国民党垮台,“豪密“在长达21年的使用中从未被破译。这在中国密码史上堪称奇迹,也为中国革命的胜利提供了重要的通信保障。

恩尼格码:被盟军破译的“无敌“密码

恩尼格码(Enigma)是二战期间德国使用的最著名的加密机器。恩尼格码机利用转子(rotor)和反射器(reflector)的机械组合,可以产生天文数字般的密钥空间。

恩尼格码机的加密原理可以用数学描述如下。设 $E_k$ 为密钥 $k$ 下的加密函数,则:

$$E_k(x) = \pi_3(\pi_2(\pi_1(x \oplus s) \oplus s) \oplus s)$$

其中 $\pi_1, \pi_2, \pi_3$ 分别代表三个转子的置换,$s$ 为初始设定。每加密一个字符,转子就会步进,使得加密函数动态变化。

恩尼格码机的密钥空间约为 $10^{158}$,在当时看来几乎不可能被暴力破解。然而,英国布莱切利园的数学家阿兰·图灵(Alan Turing)领导的团队,利用德国操作员的习惯性错误和恩尼格码机的一个设计缺陷(任何字母都不会加密为自身),成功破译了恩尼格码。

盟军通过破译恩尼格码获得了大量德军情报,据估计这一成就使二战至少缩短了两年,挽救了约1400万人的生命。

中途岛海战:密码决定命运

1942年的中途岛海战是太平洋战争的转折点。美军能够取得这场决定性胜利,关键在于成功破译了日军的 JN-25 密码系统。

美国海军的密码破译小组(Station HYPO)在约瑟夫·罗奇福特(Joseph Rochefort)的领导下,通过长期的密码分析和模式识别,成功解读了日军计划攻击目标代号“AF“的含义。为了确认“AF“就是中途岛,美军故意让中途岛基地发出明文报告淡水设备故障,随后截获日军报告“AF“淡水不足的密文,从而确认了目标。

这场海战的结果:日军损失4艘航空母舰(赤城、加贺、苍龙、飞龙),而美军仅损失1艘(约克城)。密码破译的力量在这场战役中得到了淋漓尽致的展现。

紫密:美国破译日本外交密码

“紫密”(PURPLE)是二战期间日本外交部门使用的高级加密系统。美国陆军信号情报处(SIS)在威廉·弗里德曼(William Friedman)的领导下,经过长达20个月的艰苦努力,于1940年成功破译了紫密。

通过破译紫密,美国获取了大量日本外交情报,包括日本与德国、意大利之间的秘密通信。这些情报在二战的关键决策中发挥了重要作用。然而,出于保护情报来源的考虑,美国并未在所有情况下都利用这些情报,珍珠港事件前虽然截获了相关情报,但未能及时转化为有效的防御行动。

密码战的历史意义

纵观历史,密码战对战争进程的影响是深远的:

战争/事件密码系统结果影响
二战太平洋战场纳瓦荷语密码未被破译保障美军通信安全
二战欧洲战场恩尼格码被盟军破译二战缩短约两年
太平洋战争JN-25密码被美军破译中途岛海战胜利
二战外交战线紫密被美国破译获取大量外交情报
中国革命豪密未被破译保障中共通信21年

密码是保护秘密的重要手段,其本身编码、破译就是十分重要的秘密。一旦密码被破译,秘密被泄露,其危害也是十分巨大的。

语言作为密码

使用人类语言作为加密手段,是一种古老但有效的思路。其原理和局限如下:

原理:

  • 利用语言的稀有性:使用人数极少的语言天然具有保密性
  • 利用语言的复杂性:语法结构复杂的语言增加了学习门槛
  • 利用文化壁垒:不同文化背景的人难以理解特定语言的含义

局限:

  • 安全性依赖于语言使用者的稀缺性,一旦敌方掌握了该语言,安全性即丧失
  • 语言使用者本身就是最大的安全隐患(如俘虏风语者)
  • 通信效率低,编码解码速度慢
  • 不适合大规模、标准化的军事通信
  • 随着语言学研究的深入,稀有语言的神秘面纱逐渐被揭开

国密(中国商用密码)算法详解

如果说计算机通用的密码算法是“世界通用语言“,那么使用范围有限的国密(商用密码)则可以成为“部落语言“。

世界上本没有路,走的人多了,也便成了路。 世界上本没有标准,用的人多了,也便成了标准。

中国商用密码(简称“国密“)是由中国国家密码管理局制定的一系列密码算法标准,旨在保障国家信息安全,减少对国外密码算法的依赖。国密算法主要包括 SM2、SM3、SM4、SM9 等。

SM2:基于ECC的非对称密码算法

SM2 是一种基于椭圆曲线密码学(Elliptic Curve Cryptography, ECC)的非对称加密算法,由国家密码管理局于2010年发布。SM2 可用于数字签名、密钥交换和数据加密。

数学原理:

SM2 基于椭圆曲线离散对数问题(Elliptic Curve Discrete Logarithm Problem, ECDLP)。给定椭圆曲线 $E$ 上的一点 $P$ 和整数 $k$,计算 $Q = kP$ 是容易的(点乘运算),但给定 $Q$ 和 $P$,求 $k$ 则是计算上不可行的。

SM2 使用的推荐椭圆曲线参数如下:

$$y^2 = x^3 + ax + b \pmod{p}$$

其中 $p$ 为256位素数,曲线阶 $n$ 也是256位素数。

SM2 与 RSA 对比:

特性SM2 (ECC 256)RSA 2048RSA 3072
密钥长度256 bit2048 bit3072 bit
安全等级128 bit112 bit128 bit
签名速度更慢
密钥生成速度更慢
证书体积更大
计算资源消耗更高

可以看到,SM2 在256位密钥长度下就能达到与 RSA 3072 位相当的安全等级,同时密钥更短、计算更快、证书更小,非常适合移动端和物联网设备使用。

SM2 的主要应用场景:

  • 数字签名与验签
  • 密钥交换(类似 ECDH)
  • 公钥加密(类似 ECIES)
  • 电子政务、金融支付、身份认证

SM3:密码杂凑算法

SM3 是中国国家密码管理局于2010年发布的密码杂凑算法,输出256位(32字节)的杂凑值。SM3 适用于数字签名和验证、消息认证码的生成与验证、随机数的生成等。

算法原理:

SM3 采用 Merkle-Damgård 结构,处理流程如下:

  1. 填充:对消息进行填充,使其长度为512位的整数倍
  2. 迭代压缩:将填充后的消息分成512位的分组,逐组进行压缩
  3. 输出:最终输出256位的杂凑值

SM3 的压缩函数可以形式化表示为:

$$V^{(i+1)} = CF(V^{(i)}, B^{(i)})$$

其中 $V^{(0)}$ 为初始值(IV),$B^{(i)}$ 为第 $i$ 个消息分组,$CF$ 为压缩函数。

SM3 与 SHA-256 对比:

特性SM3SHA-256
输出长度256 bit256 bit
分组长度512 bit512 bit
轮数6464
压缩函数结构类似SHA-256但有差异标准SHA-256
消息扩展更复杂的扩展方式标准扩展
安全性抗碰撞、抗原像、抗第二原像抗碰撞、抗原像、抗第二原像
标准来源中国GB/T 32905-2016NIST FIPS 180-4

SM3 在设计上借鉴了 SHA-256 的基本框架,但在消息扩展和压缩函数的布尔函数设计上做了改进,增强了安全性。

SM4:对称加密算法

SM4 是中国国家密码管理局于2012年发布的分组密码算法,原名 SMS4。SM4 是一个对称加密算法,密钥长度为128位,分组长度为128位。

算法原理:

SM4 采用非平衡 Feistel 结构(类似于 SPN 结构),共进行32轮迭代变换。每轮变换包含以下操作:

  1. 非线性变换 $\tau$:使用 S 盒进行字节替换
  2. 线性变换 $L$:对非线性变换的输出进行线性混合
  3. 轮密钥加:将线性变换的结果与轮密钥进行异或

SM4 的加密过程可以表示为:

$$X^{(i+1)} = X^{(i)} \oplus T(X^{(i)} \oplus X^{(i+1)} \oplus X^{(i+2)} \oplus X^{(i+3)} \oplus rk^{(i)})$$

其中 $T = L \circ \tau$,$rk^{(i)}$ 为第 $i$ 轮的轮密钥。

SM4 支持多种工作模式,包括 ECB、CBC、CFB、OFB、CTR 等。

SM4 与 AES 对比:

特性SM4AES
分组长度128 bit128 bit
密钥长度128 bit128/192/256 bit
轮数3210/12/14
结构非平衡FeistelSPN
S盒大小8×8 bit8×8 bit
标准来源中国GB/T 32907-2016NIST FIPS 197
安全性抗差分、抗线性分析抗差分、抗线性分析

SM4 在硬件实现上具有良好的性能,适合在各种嵌入式设备和智能卡上部署。

SM9:基于身份的密码算法

SM9 是中国国家密码管理局于2016年发布的基于身份的密码(Identity-Based Cryptography, IBC)算法。SM9 不需要传统的公钥证书,而是直接使用用户的身份标识(如手机号、邮箱、身份证号)作为公钥。

核心原理:

SM9 基于双线性对(Bilinear Pairing)的数学难题。设 $G_1, G_2, G_T$ 为三个循环群,$e: G_1 \times G_2 \rightarrow G_T$ 为双线性映射,则:

$$e(aP, bQ) = e(P, Q)^{ab}$$

SM9 的密钥生成中心(KGC)持有主密钥 $s$,用户使用身份标识 $ID$ 向 KGC 申请私钥。KGC 计算 $d_{ID} = s \cdot H(ID)$ 作为用户私钥,而 $Q_{ID} = H(ID)$ 作为用户公钥。

SM9 的优势:

  • 无需公钥基础设施(PKI)和证书管理
  • 简化密钥管理流程
  • 支持数字签名、密钥封装、密钥协商
  • 适合大规模物联网设备的身份认证

国密标准体系

中国商用密码标准体系由国家密码管理局制定和管理,主要标准编号如下:

标准编号算法类型发布年份
GB/T 32905-2016SM3密码杂凑算法2016
GB/T 32907-2016SM4分组密码算法2016
GB/T 32918-2016SM2椭圆曲线公钥密码算法2016
GB/T 32915-2016SM2椭圆曲线密码签名技术2016
GB/T 35276-2017SM2签名算法使用规范2017
GB/T 36622-2018SM9标识密码算法2018
GM/T 0001-2012SM2密码算法使用规范2012
GM/T 0002-2012SM4分组密码算法2012
GM/T 0004-2012SM3密码杂凑算法2012
GM/T 0009-2012SM2密码算法使用规范2012

注:GB/T 为国家标准,GM/T 为密码行业标准。

国密在实践中的应用

国密算法在中国各行业得到了广泛应用,尤其在以下领域:

银行与金融:

  • 中国人民银行要求所有银行核心系统支持国密算法
  • 银行卡交易、跨行转账使用 SM2/SM4 保护
  • 第三方支付(支付宝、微信支付)已全面支持国密

政务系统:

  • 电子政务平台使用 SM2 进行数字签名和身份认证
  • 税务系统、社保系统使用 SM4 加密敏感数据
  • 政府公文传输使用 SM3 进行完整性校验

其他领域:

  • 电力系统:智能电表通信加密
  • 交通系统:ETC不停车收费系统
  • 物联网:智能家居设备身份认证
  • 电子商务:电子发票、电子合同签名

Rust 国密实战

环境准备

在 Rust 项目中使用国密算法,需要在 Cargo.toml 中添加依赖:

[dependencies]
libsm = "0.5"
rand = "0.8"
base64 = "0.21"
hex = "0.4"

SM3 杂凑算法示例

#![allow(unused)]
fn main() {
use libsm::sm3::hash::Sm3Hash;
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};

fn sm3_demo() {
    // 对字符串进行 SM3 哈希
    let string = String::from("abc文心一言&ChatGPT");
    let mut hash = Sm3Hash::new(string.as_bytes());
    let digest: [u8; 32] = hash.get_hash();

    // 输出 Base64 编码的哈希值
    let base64_str = BASE64.encode(&digest);
    println!("SM3 Hash (Base64): {}", base64_str);

    // 输出十六进制编码的哈希值
    let hex_str: String = digest.iter().map(|b| format!("{:02x}", b)).collect();
    println!("SM3 Hash (Hex): {}", hex_str);
}
}

SM3 对输入 “abc” 的标准测试向量输出为:

66c7f0f462eeedd9d1f2d46bdc10e4e2415c90e068e1e4dda26a45273ee1c7d0

SM4 对称加密示例

#![allow(unused)]
fn main() {
use libsm::sm4::Cipher;
use libsm::sm4::Mode;
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};

fn sm4_demo() {
    // 128位密钥
    let key: [u8; 16] = [
        0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
        0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10,
    ];

    // 使用 CBC 模式
    let cipher = Cipher::new(&key, Mode::Cbc);

    // 生成随机 IV(初始化向量)
    let iv = rand_block();

    // 加密陈毅元帅的诗
    let poem = String::from("断头今日意如何?创业艰难百战多。此去泉台招旧部,旌旗十万斩阎罗。");
    let encrypt_bytes = cipher.encrypt(&poem.as_bytes(), &iv);
    println!("SM4 Encrypted (Base64): {}", BASE64.encode(&encrypt_bytes));

    // 解密
    let plaintext_bytes = cipher.decrypt(&encrypt_bytes, &iv);
    let poem1 = String::from_utf8(plaintext_bytes.to_vec()).unwrap();
    println!("SM4 Decrypted: {}", poem1);
}

fn rand_block() -> [u8; 16] {
    use rand::prelude::*;
    let mut rng = rand::thread_rng();
    let mut block: [u8; 16] = [0; 16];
    rng.fill_bytes(&mut block[..]);
    block
}
}

SM2 数字签名示例

#![allow(unused)]
fn main() {
use libsm::sm2::signature::{SigCtx, Signature};

fn sm2_demo() {
    let poem2 = String::from("南国烽烟正十年,此头须向国门悬。后死诸君多努力,捷报飞来当纸钱。");
    let msg = poem2.as_bytes();

    // 创建签名上下文并生成密钥对
    let ctx = SigCtx::new();
    let (pk, sk) = ctx.new_keypair();

    // 签名:速度快
    let signature = ctx.sign(msg, &sk, &pk);
    println!("SM2 Signature created successfully");

    // 验签:速度相对较慢
    let valid = ctx.verify(msg, &pk, &signature);
    println!("SM2 Signature valid: {}", valid);
}
}

综合测试示例

#![allow(unused)]
fn main() {
extern crate rand as random;
use rand::prelude::*;

fn rand_block() -> [u8; 16] {
    let mut rng = random::thread_rng();
    let mut block: [u8; 16] = [0; 16];
    rng.fill_bytes(&mut block[..]);
    println!("IV:{}", hex::encode(&block));
    block
}

use libsm::sm2::signature::{SigCtx, Signature};
use libsm::sm3::hash::Sm3Hash;
use libsm::sm4::Cipher;
use libsm::sm4::Mode;

#[test]
fn sm() {
    // SM3 杂凑
    let string = String::from("abc文心一言&ChatGPT");
    let mut hash = Sm3Hash::new(string.as_bytes());
    let digest: [u8; 32] = hash.get_hash();

    let base64_str = base64::encode(&digest);
    println!("{}", base64_str);

    let key: [u8; 16] = [
        0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32,
        0x10,
    ];
    // SM4 对称加密
    let cipher = Cipher::new(&key, Mode::Cbc);

    let iv = rand_block();
    let poem = String::from("断头今日意如何?创业艰难百战多。此去泉台招旧部 ,旌旗十万斩阎罗。");
    let encrypt_bytes = cipher.encrypt(&poem.as_bytes(), &iv);
    println!("{}", base64::encode(&encrypt_bytes));
    let plaintext_bytes = cipher.decrypt(&encrypt_bytes, &iv);
    let poem1 = String::from_utf8(plaintext_bytes.to_vec()).unwrap();
    println!("{}", poem1);

    let poem2 = String::from("南国烽烟正十年,此头须向国门悬。后死诸君多努力,捷报飞来当纸钱。");
    let msg = poem2.as_bytes();

    let mut sw = Stopwatch::new();
    // SM2 签名速度快,验签速度慢
    let ctx = SigCtx::new();
    let (pk, sk) = ctx.new_keypair();

    println!("elapsed_ms 0:{:?}", sw.elapsed());

    sw.restart();
    let signature = ctx.sign(msg, &sk, &pk);

    println!("elapsed_ms 1:{:?}", sw.elapsed());

    sw.restart();
    let valid = ctx.verify(msg, &pk, &signature);

    println!("valid:{},elapsed_ms:{:?}", valid, sw.elapsed());
}
}

OpenSSL 命令行验证

# 使用 OpenSSL 计算 SM3 哈希
openssl sm3 Rust实战配套代码.zip

# 使用 OpenSSL 进行 SM4 加密
openssl enc -e -sm4 -in /tmp/1.txt -out /tmp/2.txt

Rust 国密库推荐

库名说明特点
libsm纯 Rust 实现的国密算法库支持SM2/SM3/SM4,无外部依赖,适合嵌入式
gm-sm2SM2 算法实现专注于SM2,API简洁
rust-sm2SM2 签名与加密支持SM2签名和加密,与标准兼容性好
sm3SM3 杂凑算法独立实现轻量级,仅SM3功能
ring (OpenSSL后端)通用加密库通过OpenSSL支持国密,功能全面

总结

算法类型密钥长度输出长度主要用途国际对标
SM2非对称(ECC)256 bit-数字签名、密钥交换、加密ECDSA/ECDH
SM3杂凑-256 bit消息摘要、完整性校验SHA-256
SM4对称(分组)128 bit128 bit数据加密、通信保密AES-128
SM9基于身份--无证书认证、密钥管理IBC标准

练习题

  1. 历史分析:为什么纳瓦荷语密码在二战中从未被破译?这种方法在现代数字通信中还有应用价值吗?为什么?

  2. 密码对比:SM2 使用256位密钥就能达到 RSA 3072位的安全等级,请从数学原理角度解释为什么 ECC 可以用更短的密钥达到更高的安全等级。

  3. 编程实践:使用 libsm 库实现一个完整的文件加密工具,要求:

    • 使用 SM3 计算文件的哈希值
    • 使用 SM4-CBC 模式加密文件内容
    • 使用 SM2 对 SM3 哈希值进行数字签名
    • 将加密文件、IV、签名打包输出
  4. 思考题:国密算法与国密标准体系(GB/T、GM/T)之间的关系是什么?如果一个国际软件产品要进入中国市场,是否必须支持国密算法?请分析其中的技术原因和政策原因。

  5. 扩展阅读:研究 SM9 基于身份的密码体制,与传统的 PKI 体系(基于证书的公钥基础设施)相比,SM9 在密钥管理方面有哪些优势和劣势?

  6. 实战挑战:使用 Rust 实现一个简单的“国密工具箱“命令行程序,支持以下功能:

    • sm3 hash <file> - 计算文件SM3哈希
    • sm4 encrypt <input> <output> - SM4加密文件
    • sm4 decrypt <input> <output> - SM4解密文件
    • sm2 sign <file> - SM2签名文件
    • sm2 verify <file> <signature> - SM2验签

第七十二 终极密码

密码学的未来:在量子与智能时代守护秘密

在本书的密码学篇章中,我们从最基础的口令保护出发,逐步深入到对称加密、非对称加密、哈希函数、数字签名、密钥交换、TLS协议、数字证书、零知识证明、同态加密,直至量子密码学。每一章都揭示了密码学的一个维度,而本章将作为这些维度的交汇点,展望密码学的未来图景,并为全书密码学知识体系画上一个完整的句号。

现有的密码体系,其根基皆植于数学难题之上:大整数分解、离散对数、椭圆曲线离散对数。在经典计算模型下,这些问题被认为是计算不可行的。然而,量子计算机的崛起正在动摇这一根基。正如我们在第五十四章所讨论的,Shor算法能够在多项式时间内破解RSA和ECC,这使得我们必须重新思考密码学的未来方向。

面对量子计算的威胁,我们并非束手无策。“用魔法打败魔法”——量子密码学为我们提供了全新的武器。与此同时,人工智能的飞速发展也在重塑密码学的攻防格局。本章将探讨这些交叉领域,并呈现隐私保护技术的全景图。

72.1 密码学的未来展望

从数学难题到物理定律

传统密码学的安全性建立在数学假设之上:某些问题在经典计算机上难以在合理时间内解决。量子密码学则向前迈进了一步,将安全性建立在量子力学的物理定律之上——不可克隆定理和测量坍缩效应。这代表了密码学安全基础的范式转移:从“计算安全“走向“信息安全“。

然而,物理实现总是不完美的。QKD设备可能存在侧信道漏洞,光子探测器可以被“致盲攻击“。因此,未来的密码系统很可能是多层防御的:物理层(QKD)、数学层(PQC)、协议层(零知识证明、MPC)协同工作,任何单一层的突破都不会导致整体系统的崩溃。

密码学与人工智能的交叉

AI 对密码学的影响是双向的:

AI 辅助密码分析

  • 深度学习模型可以识别密码实现中的侧信道泄漏模式
  • 强化学习可以优化密码分析中的搜索策略
  • 神经网络辅助的差分密码分析正在兴起

AI 增强密码设计

  • 自动化搜索更优的S盒和线性变换
  • 机器学习辅助评估新密码方案的安全性
  • 自适应安全协议根据威胁环境动态调整参数

密码学保护 AI

  • 同态加密支持隐私保护机器学习
  • 零知识证明验证模型推理的正确性而不泄露模型参数
  • 安全多方计算让多个数据持有方联合训练模型而不共享原始数据

密码学的民主化

过去,强大的密码技术只为政府和大型企业所掌握。如今,开源生态(如Rust密码学社区)正在让高质量的密码实现变得触手可及。ringrustlsdalek-cryptography等库让任何开发者都能轻松构建安全的应用。未来的趋势是:密码学将成为所有软件的基础设施,就像TCP/IP一样无处不在且易于使用。

72.2 量子安全密码

迁移的紧迫性

密码系统的生命周期往往长达数十年。今天加密的敏感数据,可能在未来被量子计算机解密——这就是“先存储,后解密“(Harvest Now, Decrypt Later)攻击。对于需要长期保密的数据(如政府机密、医疗记录、核心知识产权),现在就必须开始迁移到量子安全方案。

迁移策略

阶段时间行动
准备期现在-2027密码清单、风险评估、混合部署试点
过渡期2027-2035大规模混合部署、传统算法逐步退役
量子时代2035+纯后量子方案、QKD骨干网络

关键建议

  1. 密码敏捷性:设计支持算法热替换的架构,避免算法硬编码
  2. 混合保护:关键系统同时使用传统和后量子算法,提供双保险
  3. 密钥轮换:缩短密钥有效期,降低被破解后的影响范围
  4. 持续监控:跟踪NIST、ISO等标准组织的最新规范

72.3 隐私保护技术全景

现代隐私保护不再是单一技术的战场,而是多种密码技术的协同生态。以下是核心技术及其组合应用:

核心技术回顾

技术核心能力典型应用
零知识证明证明知晓某信息而不泄露信息本身身份认证、区块链隐私交易、合规证明
同态加密在密文上直接计算隐私计算、安全外包计算、联邦学习
安全多方计算多方联合计算,各方输入保密隐私保护统计、联合风控、跨机构数据分析
差分隐私在查询结果中添加噪声保护个体隐私数据库查询、人口统计、机器学习训练
可信执行环境硬件隔离的安全计算区域密钥保护、敏感数据处理、模型推理

技术组合:隐私计算的“组合拳“

单一技术往往有其局限性,实际应用中通常需要组合使用:

场景一:隐私保护机器学习

数据持有方A、B、C各自拥有训练数据
        ↓
    同态加密:数据加密上传
        ↓
    安全多方计算:多方联合计算梯度
        ↓
    差分隐私:梯度添加噪声保护个体
        ↓
    零知识证明:验证计算正确性
        ↓
    全局模型更新(不泄露任何原始数据)

场景二:隐私保护身份认证

用户需要证明:年龄>18岁 且 居住在允许的地区
        ↓
    零知识证明:生成证明,不泄露具体年龄和地址
        ↓
    验证方验证证明的有效性
        ↓
    同态加密(可选):加密存储认证结果
        ↓
    服务提供方根据验证结果提供服务

场景三:去中心化金融(DeFi)隐私交易

用户发起转账
        ↓
    同态加密/承诺方案:隐藏交易金额
        ↓
    零知识证明(zk-SNARKs):证明交易合法性
        ↓
    区块链验证证明,更新加密状态
        ↓
    接收方使用私钥解密获得金额

Rust 在隐私计算中的角色

Rust 的内存安全保证和零成本抽象使其成为构建隐私计算基础设施的理想语言:

  • bellman:zk-SNARKs 的 Rust 实现,用于零知识证明
  • bulletproofs:范围证明和任意算术电路的零知识证明
  • concrete / fhe.rs:全同态加密库
  • mpz:安全多方计算框架
  • oprf-rs:不经意伪随机函数,用于隐私密码认证
#![allow(unused)]
fn main() {
// 概念演示:组合使用多种隐私技术
use sha2::{Sha256, Digest};

/// 隐私保护数据聚合:结合承诺、哈希和简单同态思想
fn privacy_preserving_aggregation() {
    // 三个参与方的数据
    let data_a = 100u64;
    let data_b = 200u64;
    let data_c = 300u64;

    // 使用哈希作为简单承诺(实际应用应使用Pedersen承诺)
    let salt_a = b"random_salt_a";
    let salt_b = b"random_salt_b";
    let salt_c = b"random_salt_c";

    let mut hasher_a = Sha256::new();
    hasher_a.update(salt_a);
    hasher_a.update(&data_a.to_le_bytes());
    let commitment_a = hasher_a.finalize();

    // 参与方提交承诺后,再揭示数据
    println!("参与方A提交承诺: {:02x?}", commitment_a);

    // 数据揭示后验证承诺
    let mut verify_a = Sha256::new();
    verify_a.update(salt_a);
    verify_a.update(&data_a.to_le_bytes());
    let verified = verify_a.finalize() == commitment_a;
    println!("承诺验证: {}", verified);

    // 计算聚合结果
    let total = data_a + data_b + data_c;
    println!("聚合结果: {}", total);

    // 在实际系统中,聚合计算应在密文/安全环境下进行
    // 这里仅演示概念流程
}
}

72.4 全书密码学知识体系总结

回顾本书下卷“秘密与密码“的完整知识脉络:

知识图谱

密码学基础
├── 口令与认证
│   ├── 口令强度与存储(Argon2、PBKDF2)
│   ├── 双因子认证(TOTP、HOTP)
│   └── 生物特征与身份认证
├── 随机数与密钥
│   ├── 密码学安全随机数生成
│   ├── 对称密钥与非对称密钥
│   ├── 密钥交换(DH、ECDH)
│   └── 密钥管理(KMS、Vault)
├── 对称密码
│   ├── 流密码(ChaCha20)
│   └── 分组密码(AES、SM4)
├── 非对称密码
│   ├── RSA(大数分解)
│   └── ECC/SM2(椭圆曲线)
├── 哈希与完整性
│   ├── SHA-2、SHA-3、SM3
│   └── HMAC、数字签名
├── 协议与基础设施
│   ├── TLS/SSL(传输安全)
│   ├── 数字证书与PKI(身份绑定)
│   └── 区块链(去中心化信任)
└── 前沿密码技术
    ├── 零知识证明(不泄露的证明)
    ├── 同态加密(密文计算)
    └── 量子密码学(后量子安全)

核心原则

贯穿全书密码学章节的几条核心原则:

  1. Kerckhoffs 原理:密码系统的安全性应仅依赖于密钥的保密,而非算法的保密。

  2. 最小权限原则:只授予完成任务所必需的最小权限,密码学上体现为“需要知道“原则。

  3. 纵深防御:不依赖单一安全机制,多层防护协同工作。

  4. 密码敏捷性:设计能够适应未来算法演进的系统架构。

  5. 不信任,验证:对任何外部输入和第三方组件保持警惕,密码学验证是最后的防线。

安全实践 checklist

在构建密码学应用时,请对照以下清单:

  • 使用经过审计的标准库(如 ringrustls),不自行实现密码算法
  • 密钥使用密码学安全随机数生成器生成
  • 口令使用 Argon2id 或 bcrypt 进行哈希存储
  • 对称加密优先选择 AES-256-GCM 或 ChaCha20-Poly1305
  • 非对称加密密钥长度符合当前安全标准(RSA>=3072, ECC>=256)
  • 传输层使用 TLS 1.3,禁用不安全的密码套件
  • 证书验证严格,不跳过证书链检查
  • 密钥定期轮换,有安全的密钥销毁机制
  • 敏感操作考虑侧信道攻击防护(常量时间算法)
  • 关注量子计算进展,规划后量子迁移路线图

72.5 结语

密码学是数字世界的“锁与钥匙“。从古代斯巴达的密码棒,到二战时期的恩尼格玛机,再到今天的量子密码学,人类保护秘密的斗争从未停止。每一次计算技术的飞跃,都会催生密码学的革新;而每一次密码学的突破,又为数字社会的发展提供了信任基石。

在 Rust 的助力下,我们拥有了构建安全系统的强大工具。Rust 的内存安全消除了整类安全漏洞,其性能优势让密码运算高效可靠,而活跃的开源生态则提供了经过严格审查的密码学实现。

密码学的终极目标是:在开放的网络中建立可信的连接,在不信任的各方之间实现安全的协作。这一目标从未像今天这样重要,也从未像今天这样接近实现。

愿你在密码学的道路上不断探索,用技术的力量守护数字世界的秘密。

72.6 本章总结

主题核心要点
量子安全迁移密码清单、混合部署、算法敏捷性、持续跟踪
AI 与密码学AI辅助分析、AI增强设计、密码学保护AI隐私
隐私技术全景ZKP + HE + MPC + 差分隐私 + TEE 的组合应用
Rust 密码生态ring、rustls、bellman、concrete、pqcrypto 等
核心原则Kerckhoffs原理、纵深防御、密码敏捷性、零信任

练习建议

  1. 基础回顾:绘制本书下卷所有密码学知识点的思维导图,标注各技术之间的依赖和组合关系。

  2. 综合练习:设计一个隐私保护的数据共享系统,综合运用至少三种密码技术(如零知识证明+同态加密+安全多方计算),用 Rust 编写核心模块的伪代码或原型实现。

  3. 前沿调研:选择以下一个方向进行深入研究:

    • 后量子密码算法的最新进展(关注NIST第四轮标准化)
    • 全同态加密的工业应用案例(如隐私保护机器学习平台)
    • 零知识证明在区块链扩容(zk-Rollups)中的应用
    • 量子密钥分发的商用部署现状和技术挑战
  4. 实践项目:为一个假想的企业系统设计完整的密码学方案,涵盖:用户认证、数据传输加密、数据存储加密、密钥管理、审计日志、以及向量子安全密码的迁移计划。

附录 问题

提出问题比解决问题更重要。—— 爱因斯坦

打开一切科学的钥匙毫无异议的是问号。—— 巴尔扎克

提出正确的问题,往往等于解决了问题的大半。—— 海森堡

问题 → 猜想 → 证明 → 验证 → 定理 → 推论


一、物不知数(中国剩余定理)

《孙子算经》:今有物,不知其数。三三数之,剩二;五五数之,剩三;七七数之,剩二。问:物几何?

解法口诀:

三人同行七十稀,五树梅花廿一支,七子团圆正半月,除百零五使得知

同余方程组:

$$ \begin{cases} x \equiv 2 \pmod{3} \ x \equiv 3 \pmod{5} \ x \equiv 2 \pmod{7} \end{cases} $$

中国剩余定理: 设 $m_1, m_2, \ldots, m_k$ 两两互质,则同余方程组在模 $M = \prod m_i$ 下有唯一解。


二、鸡兔同笼

《孙子算经》:今有雉兔同笼,上有三十五头,下有九十四足,问雉兔各几何?

解法:

设鸡有 $x$ 只,兔有 $y$ 只:

$$ \begin{cases} x + y = 35 \ 2x + 4y = 94 \end{cases} $$

解得:鸡 23 只,兔 12 只。


三、引葭赴岸

《九章算术》:今有池方一丈,葭生其中央。出水一尺,引葭赴岸,适与岸齐。问水深、葭长各几何。

(1丈 = 10尺)

古代解法:

$$ b = \frac{a^2 - (c - b)^2}{2(c - b)} $$

现代解法:

设水深 $x$ 尺,则葭长 $(x+1)$ 尺:

$$ x^2 + 5^2 = (x + 1)^2 $$

解得:水深 12 尺,葭长 13 尺。


四、最速降线

问题:在重力作用下,一个质点从点 $A$ 滑到点 $B$(不在正下方),沿什么路径所需时间最短?

答案:摆线(Cycloid)

摆线的参数方程:

$$ \begin{cases} x = r(\theta - \sin\theta) \ y = r(1 - \cos\theta) \end{cases} $$


五、巴塞尔问题

求所有正整数平方倒数的和:

$$ \sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6} $$

该问题由欧拉在 1735 年解决,一举成名。


六、费马引理

若函数 $f(x)$ 在 $x_0$ 处可导且取得极值,则 $f’(x_0) = 0$。

这是微分中值定理的基础。


七、素数问题

素数定理: 不超过 $x$ 的素数个数 $\pi(x) \sim \frac{x}{\ln x}$。

费马小定理: 若 $p$ 是质数,且 $a$ 不是 $p$ 的倍数,则:

$$ a^{p-1} \equiv 1 \pmod{p} $$

克拉茨猜想(3n+1 猜想): 任取一正整数,若为偶数则除以 2,若为奇数则乘以 3 再加 1,重复操作,最终总会落入 4→2→1 的循环。至今未被证明。


八、密码学相关问题

密钥管理

  • 如何生成公私钥对?
  • 如何根据编码后的公私钥对得到 PrivateKey、PublicKey?
  • 如何保护密钥的安全?
  • 如何传输交换密钥?

身份认证

如何证明我是我,我妈是我妈?

数字证书及CA

传输层安全性协议(TLS/SSL)是一种安全协议,目的是为互联网通信提供安全及数据完整性保障。


九、经典问题索引

问题类别相关章节
物不知数数论/同余第29章 行列式
鸡兔同笼线性方程组第30章 矩阵
引葭赴岸几何/勾股定理第28章 几何
最速降线微积分/变分法第33章 微积分
巴塞尔问题级数求和第33章 微积分
素数计数数论第27章 代数
生日悖论概率论第34章 概率论

十、学习资源

向量知识库

附录1 程序员

会使用 Excel 就是程序员。


一、Excel 与数据处理

Excel 可用作一个简易的数据库:

  • xls 表格最大支持 16384 行数据
  • xlsx 表格最大支持 1,048,576 行数据

Excel 关键函数

VLOOKUP 语法:

VLOOKUP(lookup_value, table_array, col_index_num, [range_lookup])
  • lookup_value:要查找的内容
  • table_array:要查找的位置
  • col_index_num:包含要返回值的列号
  • range_lookup:近似匹配(TRUE)或精确匹配(FALSE)

Excel 相关库

语言说明
RustcalamineExcel/ODS 纯 Rust 读取器
Rustrust_xlsxwriterExcel xlsx 写入库
GoExcelizeGo 语言的 Excel 操作库
Javaeasyexcel阿里巴巴的 Excel 处理库
PythonPandas强大的数据分析工具包
ClibxlsxwriterC 语言的 xlsx 创建库
PythonXlsxWriterPython 的 xlsx 写入模块

二、数学基础速查

每一个程序员都需要掌握一定的数学基础:

分支内容应用场景
代数函数、方程、多项式算法设计、性能分析
三角函数sin、cos、tan 及其关系图形学、游戏开发
几何椭圆、抛物线、双曲线计算机图形学、物理模拟
排列组合计数原理、概率计算算法复杂度分析
统计与概率分布、期望、方差数据分析、机器学习
导数变化率、梯度数值优化、机器学习
线性代数向量、矩阵图形学、AI、密码学
概率论贝叶斯、随机过程AI、风险评估

三、LaTeX 速查

常用公式

平方和公式:

$$ (a + b)^2 = a^2 + 2ab + b^2, \quad (a-b)^2 = a^2 - 2ab + b^2 $$

等差数列求和:

$$ \sum_{i=1}^{n} i = \frac{n(n+1)}{2} $$

分段函数:

$$ f(x) = \begin{cases} x, & x \geq 0 \ -x, & x < 0 \end{cases} $$

椭圆方程:

$$ \frac{x^2}{a^2} + \frac{y^2}{b^2} = 1 \quad (a > b > 0) $$

均方误差(MSE):

$$ MSE = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 $$

等比数列求和:

$$ S_n = a_1 \cdot \frac{1 - q^n}{1 - q} \quad (q \neq 1) $$

矩阵乘法规则:

$$ C[i,j] = \sum_{k=1}^{n} A[i,k] \cdot B[k,j] $$


四、常用度量衡

类型古代单位换算
度(长度)1丈 = 10尺,1尺 ≈ 23.1cm(秦汉)尺度标准历代有差异
量(容量)1斛 = 10斗,1斗 = 10升嘉量为一器含多种容量单位
衡(重量)1石 = 120斤,1斤 = 16两铜权用作砝码

秦汉后,朝廷铸造青铜标准器(铜尺、嘉量、铜权)作为全国唯一参照物。


五、命令与工具

# 使用 cargo 运行示例
cargo run --example excel_to_csv examples/date_iso.xlsx Sheet
cargo run --example search_errors .\tests\

# mdbook 本地预览
mdbook serve --open

六、思维工具

工具说明
数学思维数学是科学的语言,是最重要的理论工具
工具思维计算机是最重要的工程应用工具
工程思维将理论工具应用到实际问题中的思维方式
费曼学习法如果你不能简单讲清楚,说明你还没真正理解

七、参考资源

附录2 符号表

一、希腊字母表

序号大写小写英文注音中文注音常见用途
1ΑαAlpha阿尔法显著性水平、角度
2ΒβBeta贝塔回归系数、角度
3ΓγGamma伽马欧拉常数、伽马函数
4ΔδDelta德尔塔变化量、判别式
5ΕεEpsilon艾普西隆误差、容差、小量
6ΖζZeta泽塔黎曼ζ函数
7ΗηEta伊塔学习率、效率
8ΘθTheta西塔角度、参数
9ΙιIota约塔微小量
10ΚκKappa卡帕曲率
11ΛλLambda拉姆达特征值、泊松分布参数
12ΜμMu均值、摩擦系数
13ΝνNu频率、自由度
14ΞξXi克西随机变量
15ΟοOmicron奥密克戎
16ΠπPi圆周率 3.14159…
17ΡρRho密度、相关系数
18Σσ/ςSigma西格玛求和、标准差
19ΤτTau时间常数、切应力
20ΥυUpsilon宇普西隆
21Φφ/ϕPhi黄金分割数、相位
22ΧχChi卡方分布
23ΨψPsi普西波函数
24ΩωOmega欧米伽角速度、全概率空间

二、数学符号

符号含义LaTeX
$\partial$偏微分\partial
$\Delta$变化量(增量)、判别式\Delta
$\nabla$梯度(Nabla 算子)\nabla
$\nabla \cdot \vec{F}$散度\nabla \cdot
$\nabla \times \vec{F}$旋度\nabla \times
$\nabla^2$拉普拉斯算子\nabla^2
$\sum$求和\sum
$\prod$连乘\prod
$\forall$任意、全称量词\forall
$\exists$存在量词\exists
$\in$属于\in
$\notin$不属于\notin
$\infty$无穷大\infty
$\int$积分\int
$\oint$曲线积分\oint
$\oint$向量分析的场量计算\oint
$\binom{n}{k}$组合数\binom{n}{k}
$\hat{y}$预测值\hat{y}
$\vec{\alpha}$向量 α\vec{\alpha}
$\sigma$标准差、激活函数\sigma
$\epsilon$误差、容差\epsilon

三、Unicode 表情符号指南

概念标识

符号含义符号含义
🦀Rust / Crab🐍Python
问题🌰 / 🍊例子
⚙️算法 / 工具📝代码
📦🚀性能、加速
正确错误
🎯目标⚠️危险
🧠大脑 / 思考📚题库 / 学习
🎮游戏🏅勋章
💡提示📌知识点速记

难度标识

符号含义
🌱简单
中等
🔥困难

其他常用

符号含义
🎉庆祝 / 完成
☁️云计算
💰金钱
🥕萝卜
🌹玫瑰
🌻向日葵
🟢 / 🔵 / 🟡红绿灯状态

四、重要术语中英文对照

中文English说明
同质性 / 均匀性Homogeneity统计与数据分布概念
完整性 / 完备性Completeness数学与逻辑概念
文献 / 语料库CorpusNLP 领域的文本集合
旋转Revolve / Rotate几何变换
同余Congruence数论概念
模数Modulus同余运算中的模
逆元Inverse Element群论概念
特征值Eigenvalue线性代数
奇异值Singular Value矩阵分解
梯度Gradient微积分
散度Divergence向量分析
旋度Curl向量分析

五、重要文献

文献时代作者贡献
《九章算术》东汉多人古代数学经典,含246道问题
《九章算术注》三国刘徽注疏《九章算术》,创割圆术
《孙子算经》南北朝孙子物不知数问题(中国剩余定理)
《海岛算经》三国刘徽测量数学著作
《几何原本》古希腊欧几里得公理化数学的典范

六、Rust 中常用符号

#![allow(unused)]
fn main() {
// 常用运算符
&&  ||  !    // 逻辑运算符
&   |   ^    // 位运算符
>>  <<       // 移位运算符
==  !=   >   <   >=  <=  // 关系运算符
+   -   *   /   %       // 算术运算符
->            // 函数返回类型
=>            // match 分支
::            // 路径分隔符
|_|           // 闭包语法
&   &mut      // 引用、可变引用
*             // 解引用
..  ..=       // Range 语法
_             // 通配符
#!  #         // 属性
@             // 模式绑定
?             // 错误传播
}

七、马尔可夫模型变体

模型全称特点应用场景
MC马尔可夫链状态完全可观测、无记忆性文本生成、股票预测
HMM隐马尔可夫模型状态不可直接观测语音识别、中文分词
MDP马尔可夫决策过程加入动作与奖励强化学习
POMDP部分可观测 MDP观测不完全机器人导航、自动驾驶

马尔可夫性质: 未来的状态只取决于当前状态,而与过去状态无关。

附录3 百宝箱——工欲善其事必先利其器

路虽远行则将至,事虽难做则必成。


一、思维方法论

三种核心思维

思维说明
数学思维数学是科学的语言,是最重要的理论工具
工具思维计算机是最重要的工程应用工具
工程思维将理论工具应用到实际问题中的思维方式

人类学习的一般规律

  • 从特殊到一般:先接触具体实例,再归纳总结出普遍规律
  • 从直觉到抽象:先靠感官感知,再上升到抽象推理
  • 从已知到未知:依托已有知识同化新知识
  • 从感性到理性:先感性观察,再理性分析

费曼学习法

如果你不能用简单的话把某个概念讲清楚,说明你还没有真正理解它。—— 理查德·费曼

四个步骤:

  1. 确定目标:写下你想学习的概念
  2. 模拟教学:用最简单的语言解释给一个8岁的孩子听
  3. 发现盲点:卡住的地方回到原始材料重新学习
  4. 简化与类比:用一个生动的类比让外行也听懂

二、中外著名数学家

时代数学家国籍贡献
古希腊毕达哥拉斯希腊勾股定理、万物皆数
古希腊欧几里得希腊《几何原本》、公理化方法
古希腊阿基米德希腊浮力定律、杠杆原理
17世纪牛顿英国微积分、万有引力
17世纪莱布尼茨德国微积分(独立创立)
18世纪欧拉瑞士数学分析、图论、符号创造
19世纪高斯德国数论之王、统计学
19世纪黎曼德国黎曼几何、黎曼猜想
20世纪冯·诺依曼匈牙利计算机架构、博弈论
20世纪拉马努金印度数论、直觉数学天才
20世纪庞加莱法国拓扑学、庞加莱猜想
时代数学家贡献
三国刘徽《九章算术注》、割圆术
南北朝祖冲之圆周率精确到 7 位小数
南宋秦九韶《数书九章》、秦九韶算法
近代华罗庚解析数论、统筹法
近代陈景润哥德巴赫猜想研究
近代陈省身微分几何
近代吴文俊吴方法(机器证明)
当代丘成桐菲尔兹奖、卡拉比-丘流形
当代陶哲轩数论、调和分析
当代张益唐孪生素数猜想突破

更多数学家参考:数学时间线


三、计算机著名科学家

人物贡献
艾伦·图灵图灵机、人工智能、密码破译
约翰·冯·诺依曼冯·诺依曼架构、博弈论
克劳德·香农信息论、数字电路理论
高德纳(Donald Knuth)《计算机程序设计艺术》、TeX
约翰·麦卡锡Lisp 语言、人工智能
林纳斯·托瓦兹Linux 内核、Git

四、数学工具

工具说明
MATLAB商业科学计算软件
[WolframAlpha](https://zh.wolframalpha.com/)
北太天元国产通用型科学计算软件
Desmos在线图形计算器
Mathigon数学游乐场(交互式学习)
GeoGebra动态几何数学软件
VisuAlgo算法可视化

五、Python 工具生态

工具用途
Manim数学动画制作
NumPy科学计算基础库
SciPy科学计算
SymPy符号计算
Matplotlib数据可视化
Pandas数据分析
Scikit-learn机器学习
Statsmodels统计建模
PaddlePaddle深度学习框架
CUDAGPU 并行计算
jieba中文分词

六、Rust 工具生态

工具用途
nalgebra线性代数库
plotters数据绘图库
Apache Arrow内存列式数据格式
DataFusion查询引擎
mdBook电子书生成工具
mdbook-mermaidmdBook 的 Mermaid 图表插件
Mermaid图表即代码
Tauri桌面应用框架
SQLite嵌入式数据库
RustlsTLS 库
dimforge科学计算库集合
CodeWhaleRust 在线学习平台

在线运行环境

工具说明
Rust Playground在线运行 Rust 代码
Rust ExplorerRust 代码在线实验
Replit多语言在线 IDE
Godbolt (Compiler Explorer)交互式编译实验室
miniserve静态文件服务器

七、IDE 与编辑器

IDE说明
Zed高性能编辑器(Your last next editor)
VS Code最流行的通用编辑器
RustRoverJetBrains 出品的 Rust IDE
IntelliJ IDEAJava/Rust 开发

八、学习资源

视频教程

资源说明
3Blue1Brown 官网数学可视化讲解
3Blue1Brown B站中文字幕版
微积分的本质微积分可视化系列
Rust CookbookRust 编程示例

交互式工具

工具说明
matrix_calculator矩阵计算器
Polypad数学画布

九、Rust Playground 运行原理

# 将 markdown 中的代码块发送到 Rust Playground API
curl -X POST https://play.rust-lang.org/evaluate.json \
  -H "Content-Type: application/json" \
  -d @rust_code.json

Rust Playground 预置了下载量排名前 100 的 crates、Rust Cookbook 中的 crates,以及它们的所有依赖项。你可以直接使用它们,无需任何额外配置。


十、经典语录

  • “万物皆数” —— 毕达哥拉斯
  • “条条大路通罗马” —— 从当前位置到达目的地存在多种路径
  • “量变引起质变” —— 持续积累终将导致突破
  • “科学技术是第一生产力”
  • “科学技术是一把双刃剑,既可以造福人类,也可以伤害人类,关键是掌握在谁的手中,用它来干什么”

十一、人机交互方式

方式说明
GUI图形用户界面,用户友好
CLI命令行界面,高效精确
代码人类使用代码编写程序与机器交互

成千上万的科学家实现 0→1 的发现,数以千万的工程师创造、集成大量的工具,数以亿计的普通人利用工具。

一个伟大的工具,能够深入和广泛应用在各行各业,一个重要的前提是成本足够低、效率足够高

附录4 实战

纸上得来终觉浅,绝知此事要躬行。


一、LeetCode 实战


二、相关分析与统计

协方差

总体协方差:

$$ \text{Cov}(X, Y) = \frac{1}{n} \sum_{i=1}^{n} (X_i - \bar{X})(Y_i - \bar{Y}) $$

样本协方差:

$$ \text{Cov}(X, Y) = \frac{1}{n - 1} \sum_{i=1}^{n} (X_i - \bar{X})(Y_i - \bar{Y}) $$

皮尔逊相关系数

$$ r_{XY} = \frac{\sum (X_i - \bar{X})(Y_i - \bar{Y})}{\sqrt{\sum (X_i - \bar{X})^2} \sqrt{\sum (Y_i - \bar{Y})^2}} $$

斯皮尔曼等级相关系数

用于衡量两个变量之间的单调关系(不要求线性),适用于顺序数据。

点二列相关

用于一个二分变量和一个连续变量之间的相关性分析:

from scipy.stats import pointbiserialr
x = [1,0,0,0,0,0,0,1,1,1,1,0,1,1,1,1,1,0,0,0]
y = [84,82,76,60,72,74,76,84,88,90,78,80,92,94,96,88,90,78,76,74]
corr, p_value = pointbiserialr(x, y)
print(f"corr: {corr}, p_value: {p_value}")

三、中国剩余定理

问题

《孙子算经》:今有物,不知其数。三三数之,剩二;五五数之,剩三;七七数之,剩二。问:物几何?

定理标准形式

设 $m_1, m_2, \ldots, m_k$ 两两互质,同余方程组:

$$ \begin{cases} x \equiv a_1 \pmod{m_1} \ x \equiv a_2 \pmod{m_2} \ x \equiv a_3 \pmod{m_3} \ \quad \vdots \ x \equiv a_k \pmod{m_k} \end{cases} $$

在模 $M = \prod_{i=1}^{k} m_i$ 下有唯一解。

乘法逆元

在数论中,乘法逆元特指模运算意义下的逆元:若存在整数 $b$ 使得 $a \cdot b \equiv 1 \pmod{m}$,则称 $b$ 是 $a$ 在模 $m$ 意义下的逆元。逆元存在的充要条件是 $\gcd(a, m) = 1$。

求解方法:

  1. 暴力枚举(适合小模数):从小到大试,看哪个数乘以 $a$ 模 $m$ 等于 1
  2. 扩展欧几里得算法(最通用):求解 $ax + my = 1$
  3. 费马小定理(模数是质数时):$a^{-1} \equiv a^{m-2} \pmod{m}$

扩展欧几里得算法

$$ ax + by = \gcd(a, b) $$

fn extended_gcd(a: i64, b: i64) -> (i64, i64, i64) {
    if b == 0 {
        (a, 1, 0)
    } else {
        let (gcd, x1, y1) = extended_gcd(b, a % b);
        let x = y1;
        let y = x1 - (a / b) * y1;
        (gcd, x, y)
    }
}

fn main() {
    let a = 7;
    let b = 9;
    println!("extended_gcd({}, {}) = {:?}", a, b, extended_gcd(a, b));
}

四、素数筛法

厄拉多塞筛法(埃氏筛)

fn count_primes(n: i32) -> i32 {
    if n < 2 {
        return 0;
    }
    let n_usize = n as usize;
    let mut primes = vec![true; n_usize];
    primes[0] = false;
    primes[1] = false;

    let mut count = 0;
    let limit = (n_usize as f64).sqrt() as usize;

    for i in 2..n_usize {
        if primes[i] {
            count += 1;
            if i <= limit {
                let mut j = i * i;
                while j < n_usize {
                    primes[j] = false;
                    j += i;
                }
            }
        }
    }
    count
}

fn main() {
    println!("{}", count_primes(499979));  // 41538
}

线性筛法

线性筛法可以在 O(n) 的时间复杂度内找到所有小于 n 的素数,每个合数只被其最小质因子筛掉一次。


五、最大公约数与最小公倍数

辗转相除法(欧几里得算法)

核心公式:

$$ \gcd(a, b) = \gcd(b, a \bmod b) $$

fn gcd(mut a: u64, mut b: u64) -> u64 {
    while b != 0 {
        let temp = b;
        b = a % b;
        a = temp;
    }
    a
}

fn lcm(a: u64, b: u64) -> u64 {
    a / gcd(a, b) * b  // 先除后乘,避免溢出
}

fn main() {
    println!("gcd(48, 18) = {}", gcd(48, 18));  // 6
    println!("lcm(48, 18) = {}", lcm(48, 18));  // 144
}

六、引葭赴岸

原文:今有池方一丈,葭生其中央。出水一尺,引葭赴岸,适与岸齐。问水深、葭长各几何。

有一个边长为 1 丈的正方形水池,池中央长着一根芦苇,高出水面 1 尺。将芦苇拉向岸边,其顶端刚好与水面和池岸的交点对齐。问水深和芦苇长各是多少?(1丈 = 10尺)

现代解法:设水深 $x$ 尺,则葭长 $(x+1)$ 尺

$$ x^2 + 5^2 = (x + 1)^2 $$

解得:水深 12 尺,葭长 13 尺。


七、电费查询系统实战

需求: 开发一个电费查询网站

  1. 提供查询电费应交、已交功能,以列表形式展示
  2. 提供查询单个房号累计应交、已交电费功能,以 ECharts 图表展示

数据来源: AB.xlsx

查询条件: 开始日期、结束日期、房号、应交、已交

查询结果: 日期、房号、应交、已交(按日期倒序排列)

python -m http.server 8080

http://localhost:8080/electricity_query_sqlite.html
---

## 八、同余

**定义:** 两个整数 $a$ 和 $b$ 对模数 $m$ 同余,记作 $a \equiv b \pmod{m}$。

**性质:**

- 同余的加法:若 $a \equiv b \pmod{m}$,则 $a + c \equiv b + c \pmod{m}$
- 同余的乘法:若 $a \equiv b \pmod{m}$,则 $a \times c \equiv b \times c \pmod{m}$
- 同余的幂:若 $a \equiv b \pmod{m}$,则 $a^c \equiv b^c \pmod{m}$

---

## 九、学习路径参考

- [Python 3.13.13 文档](https://docs.python.org/zh-cn/3.13/index.html)
- [《The Founder's Playbook: Building an AI-Native Startup》](https://a16z.com/)
- 三维向量的旋转、剪切、缩放
- 二维向量的旋转、剪切、缩放

附录5 游戏

玩游戏,学 AI(数学、编程、AI)。

提高动手能力,培养数学直觉。


一、交互式数学工具

工具说明
GeoGebra动态几何数学软件
Mathigon数学游乐场(交互式学习)
Polypad在线数学画布

二、AI 能力测试


三、游戏角色设定

角色性格特点
🐼 大熊猫勇敢,动手能力、学习能力超强
🐵 猴子机灵善思,头脑灵活,但缺乏耐心和毅力
🦧 猩猩机灵、有耐心,力量强大,但缺乏动手能力
🐯 老虎力量强大,行动果断

神兽设定

  • 斯芬克斯(Sphinx):狮身人面神兽,守在路口强迫过路者猜谜语,答错者被吞噬
  • 阿米特(Ammit):鳄鱼头、狮身上半身、河马下半身的混合怪物,负责冥界“心脏称重“审判

四、闯关游戏设计

4.1 关卡1:比较大小

比较幂、对数、三角函数的大小。

玩法: 玩家身处一个由多个方形密室组成的迷宫。每个密室四面墙各有一道门,其中一道门通往相邻密室,其余 3 道是假出口。玩家通过回答比较大小问题打开谜题的门。

规则: 回答正确即可开门,答错密室空间自动缩小。

4.2 关卡2:矩阵与向量

矩阵对向量的运算(旋转、缩放、平移、剪切)。

玩法: 玩家身处湖中有机关的桥墩上。桥墩上有一个矩阵机关,玩家需要将方形石块放置到矩阵的各个位置上,触发桥梁(向量)进行旋转、缩放、平移、剪切,从而连接到其他桥墩,最终到达岸边。

规则: 桥梁在运动过程中不能触碰到任何障碍物,否则会导致桥梁断裂。

4.3 Rust + Bevy 3D 闯关游戏

开发一个 3D 闯关游戏,包含数学问题和逻辑推理:

# Cargo.toml 依赖
[dependencies]
bevy = "0.14"

关卡设计:

关卡主题核心知识
关卡1比较大小幂、对数、三角函数的比较
关卡2矩阵和向量旋转、缩放、平移、剪切变换

五、经典问题题库

问题类别难度
物不知数(中国剩余定理)数论🌱
鸡兔同笼线性方程组🌱
引葭赴岸几何🌱
计数质数数论
最速降线微积分🔥

六、猜想

克拉茨猜想(3n+1 猜想): 任取一正整数,若为偶数则除以 2,若为奇数则乘以 3 再加 1,重复操作,最终总会落入 4→2→1 的循环。至今未被证明。


七、微积分书籍推荐

书籍作者特点
《托马斯微积分》多人美国高校经典教材,几何直观与严谨并存
《普林斯顿微积分读本》Adrian Banner语言清晰,适合自学入门
《柯朗微积分》柯朗大师视角,引导数学思维
《菲赫金哥尔茨微积分》菲赫金哥尔茨苏式严苛,逻辑极完备的百科全书

八、人工智能学习路径

1. 基础知识准备
   ├── 数学基础:微积分、线性代数、概率统计、优化方法
   └── 编程基础:Python(首选)、Rust(辅助)

2. 机器学习入门
   ├── 核心概念:监督/无监督/强化学习、过拟合、评估指标
   └── 经典算法:线性回归、决策树、SVM、KNN、K-Means、PCA

3. 深度学习核心
   ├── 神经网络基础:MLP、激活函数、反向传播、优化器
   ├── CNN:图像分类、目标检测
   ├── RNN/LSTM:序列预测、文本生成
   └── Transformer:自注意力机制、BERT、GPT

4. 高级方向
   ├── 计算机视觉:图像分类、分割、目标检测
   ├── 自然语言处理:Word2Vec、BERT、GPT、提示学习
   ├── 强化学习:MDP、Q学习、DQN、PPO
   └── 生成式AI:GAN、Diffusion、多模态生成

5. 工程与部署
   ├── 模型优化:剪枝、量化、知识蒸馏
   └── 部署工具:FastAPI、Docker、Kubernetes、MLflow

6. 学习策略
   ├── 先通后专:先系统学基础,再选方向深耕
   ├── 代码第一:学完算法就手写实现
   ├── 项目驱动:尽早完成完整项目
   └── 保持实践:每天写代码、读论文

九、在线资源