Types and Programming Language

这篇博客是Pierce, Benjamin C.在2002年出版的《Types and Programming Languages》的阅读笔记。 无类型系统 无类型数学表达式 书中首先给出一个语言的语法说明。语法说明的目标是给出“terms”集合$\mathcal{T}$的描述。文中采用了如下3种等价的描述: 归纳地描述:给出一系列类似“如果$t_1,\dots\in\mathcal{T}$,那么$\dots\in\mathcal{T}$”的规则,并且强调$\mathcal{T}$是满足这些规则中最小的。$t$之类的变量被称为元变量 推理规则:类似归纳,只是每个规则采用“一条横线,上方是若干前提,下方是结论”来描述,“最小的”这个要求通常隐含。没有前提的规则称为公理,推理规则则是公理和真规则(有至少一个前提)。公理有时候不需要横线。有些推理规则实际上是规则模式,因为它可能包含元变量,对应了一系列甚至无穷个具体规则 具体地描述:定义集合序列,并指出这一无穷集合的并就是$\mathcal{T}$ 由上面的规则可以看出,对于一个term,它也肯定是推理规则中的某个给出。因而: 我们可以定义term上的函数,并按情况讨论递归地给出函数定义 我们可以按情况讨论term,进而归纳地给出其性质的证明 可以通过下面的3种方式定义语义: 操作语义:定义一个抽象的机器,并给出一个状态转移函数。机器从初始状态(可以是一个term),不断地进行状态转移(化简term),直至停机。有时可以给出一个语言的多个操作语义,从而便于理解或实现 指称语义:定义一个解释函数,从term映射到语义域(数学中的对象,如数或函数) 公理化语义:首先定义term的含义(而非先给出程序的行为)。相较更容易挖掘不变性 早期研究(70年代及之前)认为操作语义劣于其他。之后操作语义由于其简洁更受欢迎。本书主要使用操作语义。于是本书依照推理规则的形式给出了求值规则。注意:对于嵌套的term,必须从最外层开始求值(不应对内层嵌套term求值)。求值规则可以分为两类: 计算规则:进行实际的计算 一致性规则:决定那一子部分先计算 一个规则的实例是指将规则中的每个元变量一致地替换为某term的结果(包括结论和前提)。一个关系满足一个(求值)规则是指对于每个规则的实例,要么结论在关系中,要么存在一个前提不在关系中(这里的关系是指化简前后的两个term)。书最后定义了单步求值关系为满足所有求值规则的最小关系,记作$*\rightarrow *$。多步求值关系是单步求值关系的自反、传递闭包,记作$*\xrightarrow{*}*$。 将多个规则实例彼此组合在一起,形成树状结构,其中上方的叶子节点由公理实例组成,内部节点由真规则实例组成,这种结构被称为推导树。 通过按推导归纳可以证明求值的诸多性质,如: 确定性:如果$t\rightarrow t’,t\rightarrow t’’$,那么$t’=t’'$ 每个值都是标准形式:标准形式是指不存在$t\rightarrow t’$的$t$,一般语言会遵守这个 标准形式都是值:不一定满足,这对于运行时错误很重要 标准形式的唯一性:由确定性可得 求值的终止性:复杂语言不一定满足。证明方法一般是找到一个状态上的函数,随着求值严格递减,且递减序列不可能无穷长 一个term如果是标准形式但不是值,则称为卡住,这时候程序来到了没有意义的状态 。 数学表达式的ML实现 简单类型 子类型 递归类型 多态 高阶系统

2022/5/6
articleCard.readMore

编程语言入坑指南

本文先按照理论和应用两方面介绍了编程语言方向的相关概念,而后介绍了清华软院静态分析组研究的内容。最后本文介绍了该方向的科研就业现状,并给学弟学妹们给出一些建议。 作者介绍 我叫孙子平,是: 软院五字班本科毕业生 软院零字班工学硕士在读 主攻静态分析方向。我的微信ID同GitHub ID,欢迎大家通过微信、邮件私戳,相关联系方式见页面下方名信片。这篇文章大多是原创的个人观点,仅供参考,也欢迎来评论区指正。 编程语言方向简介 早在高中时,我就萌发了一个梦想,希望能参与到知名编程语言的设计中。当时,我认为那是编程领域的圣杯,是一个值得毕生奋斗的目标。进入大三后,我依旧怀揣着这个梦想,只是这个梦想具化为了一个领域,编程语言领域。大家通常简称它为PL(Programming Language)。当时,我找到了Paul Zhu学长。他告诉我软院与PL最相关的方向可能是: 周旻老师的静态分析方向; 贺飞老师的形式化验证方向。 恰巧当时我很喜欢函数式编程课,讲师理论上是周老师。于是,我联系了周老师,成功入坑静态分析。这门课程后来被取消掉了,也算是很遗憾。 PL主要研究编程语言的设计和实现。它的一方面很理论,包括形式语言与自动机、类型系统;另一方面则偏应用,包括编译器和静态分析。接下来,我讲按这两部分介绍一下PL,希望能够激起有兴趣的人对此的热爱。 PL理论篇 编程语言相关的理论有很多,这里我选取两个较为典型的理论体系:形式语言与自动机,和类型系统。我会更偏重于后者,因为前者是软院已有的课程。 形式语言与自动机 形式语言与自动机是软院大三上的课程。这一系列的理论试图将现实世界中的计算,抽象为一个可以用数学描述的行为,从而进一步描述计算能力和计算复杂度。这些理论发展得非常成熟。到了现代,容易摘得的科研果实基本已被摘取,只剩下一些非常难啃的硬骨头。在这些硬骨头中,最著名的应当是P/NP问题,其难度不言自喻。有趣的是,形式语言与自动机理论也能用于实现编译器的前端。 类型系统 相较而言,类型系统则更能激发我的兴趣。编程语言借助类型检查,可以限制所能进行的操作。根据这种检查发生于编译期还是运行期,类型系统可以分为静态的和动态的。而根据其对隐式类型转换的允许程度,可以将类型系统划分为强类型和弱类型。 类型之间的关系则更为有趣精妙。以下部分大多是个人观点。多态,是指不同类型在同一代码下拥有不同的行为。其目的是提高代码复用能力。面向对象最具吸引力的特性正是多态。相较之下,封装很容易实现,即使实现了也只是“基于对象“,谈不上“面向对象”;而继承的目的则要么是组合(Mixin),要么就是多态。面向对象强调的是动态多态,即在运行时才决定何种行为。类似地,也有静态多态,即在编译期就决定何种行为。例如,二元加法运算符作用于数值类型时表现为加法,作用于字符串类型时表现为拼接,这就是静态多态;函数重载也是静态多态;而类似于C++的模板泛型编程,则是更为强大的静态多态。更多安全的、符合直觉的多态意味着更高效地复用。举个例子,考虑下面的几种操作: 拼接字符串数组 求整数数组的最小值 求二叉搜索树上所有浮点数的积 这些操作都是针对一个可以按某种顺序遍历的容器,将其元素按照某个有单位元且符合结合律的二元操作收集起来。在C语言的时代,人们很难想象这些操作能用统一的代码完成。而借助多态,这种程度的代码复用就成为了现实。例如C++中,你可以用std::accumulate做到上面的所有操作。 方法 容器 元素类型 单位元 二元操作 拼接字符串数组 数组 字符串 空字符串 拼接 求整数数组的最小值 数组 整数 最大的整数 最小 求二叉搜索树上所有浮点数的积 二叉树 浮点数 1.0 乘 #include <iostream> #include <vector> #include <string> #include <numeric> #include <set> #include <limits> using namespace std; int main() { vector<string> strings { "hello", " ", "world" }; vector<int> doubles { 3, 1, 2 }; set<double> ints { 2.0, 3.0, 0.1 }; cout << accumulate(strings.begin(), strings.end(), string(), plus()) << endl; cout << accumulate(doubles.begin(), doubles.end(), numeric_limits<int>::infinity(), [](auto a, auto b) { return min(a, b); }) << endl; cout << accumulate(ints.begin(), ints.end(), 1.0, multiplies()) << endl; return 0; } 而有些函数式语言,则将多态的技能树点满,并配之以简洁的语法,构造出令人惊奇的代码。 import Data.Foldable import Data.Monoid import Data.Semigroup import qualified Data.Set as S main :: IO () main = do print . fold $ ["hello", " ", "world"] print . getMin . foldMap Min $ [3 :: Int, 1, 2] print . getProduct . foldMap Product $ S.fromList [2.0 :: Double, 3.0, 0.1] 从上面的描述中,可以看出类型系统不仅能确保安全。它更是编程语言实现代码复用的利器。它是编程语言千篇一律的语法词法下,有趣的灵魂。然而类型系统不止于次。Curry-Howard同构指出,命题与类型有着对应关系。而命题的证明则对应于类型实例的构造。自此,类型论成为了集合论的竞争者,并成为了计算机辅助证明系统主流实现的基石。举个例子,下面是一些常见的命题和类型的对应关系。 命题 类型 Python类型注解 $P\land Q$ P类型和Q类型构成的二元组类型 Tuple[P, Q] $P\rightarrow Q$ 一元函数类型,其参数为P类型,其返回值为Q类型 Callable[[P], Q] $\neg P$ 等价于$P\rightarrow \bot$,而$\bot$类型没有任何实例 因而,为了证明下面的逻辑命题 $$(P\rightarrow Q) \rightarrow (\neg Q \rightarrow \neg P)$$ 即 $$(P\rightarrow Q) \rightarrow ((Q \rightarrow \bot) \rightarrow (P \rightarrow \bot))$$ 在类型论中,可以构造下面的solution函数: from typing import Callable, TypeVar, NoReturn P = TypeVar('P') Q = TypeVar('Q') class Never: def __init__(self) -> None: assert False def solution(f: Callable[[P], Q]) -> Callable[[Callable[[Q], Never]], Callable[[P], Never]]: def foo(g: Callable[[Q], Never]) -> Callable[[P], Never]: def bar(x: P) -> Never: return g(f(x)) return bar return foo 你可以把这段程序丢给Python的类型检查工具mypy --strict,检查一下所有的类型注解是否正确。mypy通过类型检查就验证了上面的证明是正确的。这使得基于类型论的辅助证明系统能够永不会失误地验证证明的正确性。想象一下在未来,某个数学家给出了黎曼猜想的证明。那是一份长长的代码。全球的数学家们把代码输入进计算机辅助证明系统,就可以立刻判断这个证明的正确性。 当然啦,没多少人用Python做计算机辅助证明。下面是使用Coq定理证明器证明上述定理的代码 Theorem example : forall P Q : Prop, (P -> Q) -> (~Q -> ~P). Proof. intros P Q f g. exact (fun (x: P) => g (f x)). Qed. 总结一下,类型系统对编程语言影响深远。类型的约束能过确保程序安全;类型系统的表达能力则关系着语言的抽象、复用能力。而类型论则能使编程语言成为数学定理证明的工具。 PL实战篇 编译器是指将源代码翻译为可执行程序的软件。静态分析则是指在不运行程序的情况下寻找代码的漏洞。像JetBrains全家桶和VS Code插件们的一大卖点就是快速且强大的静态分析能力。编译器与静态分析对编程语言的实现而言都至关重要。 编译器 编译器的书大多会花很多篇幅讲如何实现编译器的前端,也就是如何解析源代码。其实我个人认为这有点舍本逐末。如果你要设计的语言是上下文无关文法,那么Yacc、ANTLR之类的工具就非常合适。然而不少语言无法简单地用这类工具解析。它们大都上下文相关。C++里 int main() { vector<string> array; } 那对尖括号看上去像是模板参数列表,但实际上,搭配下面的上下文,就成了大于小于号。 #include <iostream> struct { int operator < (int other) { std::cout << "hello, world!" << std::endl; return 0; } } vector; int string = 0, array = 0; C语言的A * b;也是经典的二义性语句。根据A为类型还是变量,它可能是指针定义语句,也可能是乘法的表达式语句。针对这些非常复杂的上下文,手写递归下降是我最推荐的解决方案。实际上主流的编译器大多是这样解析源代码的。 编译器相对更重要的部分是设计中间表示,并在之上优化。这部分的知识会非常琐碎,就不多介绍了。对于要做编译原理大作业的同学,可以参考计算机系的MiniDecaf 编译器结构。值得一提的是,来自苹果的LLVM已经成为了现代编译型编程语言实现的首选基础设施。 静态分析 在我眼里,静态分析是很现实的。由于图灵机糟糕的性质,静态分析试图解决的所有问题都几乎是不可判定的,包括被解引用的指针是否为空、数组是否越界、除数是否为零。这就会导致静态分析是个工程问题。 我们组在静态分析上的研究内容主要有两部分: C语言上基于LLVM的值流分析 Java上的污点传播分析 前者是用值流图建模程序。图的节点是值,而边则从值的定义指向值的使用。类似于空指针解引用等的缺陷挖掘,也就被转化为图上的路径搜索。后者主要用于查找SQL注入之类的漏洞。它将Web请求的输入视作被污染的数据。如果被污染的数据未经清洗(例如转义)就传播到了重要的调用(例如数据库命令),就被视为一个漏洞。 现状与建议 科研 编程语言各个分支的研究都较为成熟。很多时候,我都在阅读上世纪的文献。这使得新的发现变得更加困难。所以对于真的想发论文的小伙伴,这个方向是比较硬核的。 就业 编程语言的应用方向一直不温不火。伴随着AI芯片的火热,这方面也确实有编译器人才缺口。但总的而言,国内需要编译器人才的公司非常少,主要是华为等一些带有民族色彩的企业。外企的话,像微软、Intel都一直有这方面的人才需求。如果你很喜欢这个领域,那这里的就业也不会让你很失望。 结语 我个人认为编程语言领域是非常有趣的,它的很多方面都吸引着我继续探索。然而,这一方向的研究有一定难度,主要是其本身发展较为成熟。就业方面,编译器的就业需求和供给都是较为稳定的,不会出现太多问题。

2022/4/29
articleCard.readMore

友情链接

这里是通往通往小伙伴们站点的传送门~~ 施脚的站点 一个热爱买VPS的、整天折腾代码的、读了审计的、单身的、试图和我讨论哲学的高中同学。 吸猫 SJH.MOE - 致十年后的我 Yamatu的站点 同样爱折腾VPS、喜欢计算机的高中生。是通过玩MC认识的。 yamatu的博客 刘神 卡吧基佬。交大计算机博士生。研究方向是计算机体系结构、编译器系统。又高又帅又有钱,人缘又好。高中同学。 Zihan (Altair) Liu, Subject No.i 新宇 清软6字班同学。研究方向是共识算法、分布式存储和时序数据库。 谭新宇的博客 凯超 6字班大神级人物,特奖得主,也是0字班辅导员。又聪明又敬业。研究方向主要是深度学习。 Kaichao You Eren Zhao 计算机系0字班学弟。 这不完美的童话 | La vida sola viviras

2022/4/24
articleCard.readMore

Tarjan的支配树算法

这篇文章介绍了Tarjan的快速支配算法1。这个算法很有洞察力地利用了”半支配“的性质。 流图$G=(V,E,r)$上进行深度优先搜索后,节点按照发现顺序编号(论文从1编号)。搜索产生了以$r$为根的搜索树$T$,编号正是前序遍历序号。本文假设所有节点以此序号标识。 如果$G$上的两节点$v,w$,满足$v\leq w$,那么任何$v$到$w$的路径一定经过$v,w$在$T$上的共同祖先。 半支配者$\mathrm{sdom}(w)$定义为: $$\mathrm{sdom}(w)=\min\{v\mid\text{存在路径}v=v_0,v_1,\dots,v_k=w~\text{满足}\forall i(1\leq i\leq k-1\rightarrow v_i>w)\}$$ 本文中的$x\xrightarrow{*}y$是指在$T$上,$x$是$y$的祖先。$x\xrightarrow{+}y$是指在$T$上,$x$是$y$的祖先且$x\neq y$。 对任何$w\neq r$,$\mathrm{idom}(w)\xrightarrow{+}w$。 对任何$w\neq r$,$\mathrm{sdom}(w)\xrightarrow{+}w$。 由于$G$上存在路径$\mathrm{parent}(w)\rightarrow w$,由知$\mathrm{sdom}(w)\leq\mathrm{parent}(w)<w$。由,$\mathrm{sdom}(w)$到$w$的路径一定经过$\mathrm{sdom}(w),w$的共同祖先。共同祖先$\leq\mathrm{sdom}(w)$,故而共同祖先只能是$\mathrm{sdom}(w)$。 对任何$w\neq r$,$\mathrm{idom}(w)\xrightarrow{*}\mathrm{sdom}(w)$。 $\mathrm{sdom}(w)$取到的路径上,除了$\mathrm{sdom}(w),w$没有$w$的祖先,故而都不是$\mathrm{idom}(w)$而路径$r\xrightarrow{*}\mathrm{sdom}(w)$拼接上$\mathrm{sdom}(w)$取到的路径组成的路径上一定有$\mathrm{idom}(w)$,故而$\mathrm{idom}(w)\xrightarrow{*}\mathrm{sdom}(w)$ 节点$v,w$满足$v\xrightarrow{*}w$,那么$v\xrightarrow{*}\mathrm{idom}(w)$或$\mathrm{idom}(w)\xrightarrow{*}\mathrm{idom}(v)$。 其实这条定理是说$\mathrm{idom}(w)$不可能位于$\mathrm{idom}(v)$与$v$之间。反之,$\mathrm{idom}(v)$到$v$存在不经过$\mathrm{sdom}(w)$的路径,将这个路径拼接上$r\xrightarrow{*}\mathrm{idom}(v)$和$v\xrightarrow{*}(w)$,就得到了不经过$\mathrm{sdom}(w)$到达$w$的路径,矛盾。 对于$w\neq r$,如果对于所有满足$\mathrm{sdom}(w)\xrightarrow{+}u\xrightarrow{*}w$的$u$,都有$\mathrm{sdom}(u)\geq\mathrm{sdom}(w)$,那么$\mathrm{idom}(w)=\mathrm{sdom}(w)$。 只需要证明$\mathrm{sdom}(w)$支配$w$。任取从$r$到$w$的路径,现在证明路径上一定有$\mathrm{sdom}(w)$。令$x$是这个路径上最后一个满足$x<\mathrm{sdom}(w)$的节点,如果不存在,那么$\mathrm{sdom}(w)=r$,原命题成立。令$y$是这个路径上第一个满足$\mathrm{sdom}(w)\xrightarrow{*}y\xrightarrow{*}w$的节点。对于路径上$x$与$y$之间的节点$v$,一定有$v>y$。否则的话路径上有$v,y$且$\neq y$的共同祖先,且该祖先$\geq\mathrm{sdom}(w)$,那么这与$y$的选取矛盾。故而$\mathrm{sdom}(y)\leq x<\mathrm{sdom}(w)$,所以只可能$y=\mathrm{sdom}(w)$,即路径一定包括$\mathrm{sdom}(w)$。 对于$w\neq r$,令$u$是满足$\mathrm{sdom}(w)\xrightarrow{+}u\xrightarrow{*}w$的节点中$\mathrm{sdom}(u)$最小的那个,那么$\mathrm{sdom}(u)\leq\mathrm{sdom}(w)$且$\mathrm{idom}(u)=\mathrm{idom}(w)$。 对于$\mathrm{sdom}(w)\rightarrow z\xrightarrow{*}w$,$\mathrm{sdom}(z)\leq\mathrm{sdom}(w)$,所以一定有$\mathrm{sdom}(u)\leq\mathrm{sdom}(w)$。接下来证明$\mathrm{idom}(u)=\mathrm{idom}(w)$。由,一定有$\mathrm{idom}(w)\xrightarrow{*}\mathrm{idom}(u)$,接下来证明$\mathrm{idom}(u)$支配$w$。任取从$r$到$w$的路径,现在证明路径上一定有$\mathrm{idom}(u)$。令$x$是这个路径上最后一个满足$x<\mathrm{idom}(u)$的节点,如果不存在,那么$\mathrm{idom}(u)=r$,原命题成立。令$y$是这个路径上第一个满足$\mathrm{idom}(u)\xrightarrow{*}y\xrightarrow{*}w$的节点。对于路径上$x$与$y$之间的节点$v$,一定有$v>y$,证明同的相关证明。故而$\mathrm{sdom}(y)\leq x<\mathrm{idom}(u)\leq\mathrm{sdom}(u)$。然而$u$是满足$\mathrm{sdom}(w)\xrightarrow{+}u\xrightarrow{*}w$,$\mathrm{sdom}(u)$最小的,故$\mathrm{idom}(u)\xrightarrow{*}y\xrightarrow{*}\mathrm{sdom}(w)\xrightarrow{+}u$。然而$y$不可能在$\mathrm{idom}(u)$和$u$的中间。否则$r\xrightarrow{*}\mathrm{sdom}(y)$拼接上$\mathrm{sdom}(y)$取的路径再拼接上$y\xrightarrow{+}u$形成的路径,避开了$\mathrm{idom}(u)$。所以只可能$y=\mathrm{idom}(u)$,即路径一定包括$\mathrm{idom}(u)$。 对于$w\neq r$,令$u$是满足$\mathrm{sdom}(w)\xrightarrow{+}u\xrightarrow{*}w$的节点中$\mathrm{sdom}(u)$最小的那个,那么: $$\mathrm{idom}(w)=\begin{cases}\mathrm{sdom}(w)&\text{if}~\mathrm{sdom}(w)=\mathrm{sdom}(u)\\\mathrm{idom}(u)&\text{otherwise}\end{cases}$$ 对于$w\neq r$: $$\mathrm{sdom}(w)=\min(\{v\mid(v,w)\in E\land v<w\}\cup\{\mathrm{sdom}(u)\mid u>w\land(v,w)\in E\land u\xrightarrow{*}v\})$$ 令$x$是上式右侧。先证明$\mathrm{sdom}(w)\leq x$。如果$x$满足$(x,w)\in E\land x<w$,那么由,$\mathrm{sdom}(w)\leq x$。如果$x$满足$x=\mathrm{sdom}(u)\land u>w\land(v,w)\in E\land u\xrightarrow{*}v$,那么$\mathrm{sdom}(u)$选的路径中间的点$>u>w$,$u\xrightarrow{*}v$上的点$\geq u>w$,最后这两条路径拼接上$(v,w)$的到的路径满足$\mathrm{sdom}(w)$的候选路径,故而$\mathrm{sdom}(w)\leq x$。 接着证明$\mathrm{sdom}(w)\geq x$。如果$\mathrm{sdom}(w)$所选的路径$\mathrm{sdom}(w)=v_0,v_1,\dots,v_k=w$长度为1,那么$(\mathrm{sdom}(w),w)\in E\land\mathrm{sdom}(w)<w$,故而$\mathrm{sdom}(w)\geq x$。如果$\mathrm{sdom}(w)$所选的路径长度大于1。令$j\geq 1$是$v_j\xrightarrow{*}v_{k-1}$最小的,那么对于$1\leq i\leq j-1$,一定有$v_i>v_j$。否则,取$i$满足$1\leq i\leq j-1$且$v_i$最小。那么有$v_i$到$v_j$的路径经过了共同祖先,那么$v_i$由于最小,一定是共同祖先,那么$v_i\xrightarrow{+}v_j$,与$j$的选取矛盾。此时就有$\mathrm{sdom}(w)\geq\mathrm{sdom}(v_j)\geq x$。 其实是说,$\mathrm{sdom}(w)$所选的路径,一定是: 树边、前向边开始 接着若干($n\geq 0$): 若干树边($n\geq 0$) 回边或者交叉边,到比上一步小的节点 Lengauer, Thomas, and Robert Endre Tarjan. “A fast algorithm for finding dominators in a flowgraph.” ACM Transactions on Programming Languages and Systems (TOPLAS) 1.1 (1979): 121-141. ↩︎

2021/10/22
articleCard.readMore

Stanford CS231n

Image Classification Distance Metric to compare images L1 distance: $d_1(I_1,I_2)=\sum_p|I_1^p-I_2^p|$ In Nearest Neighbor classifier, we just memorize all training image, and then find the nearest training image for each testing image, which is the label of testing image. Therefore, with N examples, training need O(1) and predicting need O(N), which is not what we want. K-nearest neighbors: instead of copying label from nearest neighbor, take majority vote from K closest points. Hyperparameters:choices about the algorithm that we set rather than learn. they are problem-dependent. Try them all and see what works better. The best way of setting hyperparameters is splitting data into folds, try each fold as validation and average the results. But it is best for small datasets. linear classification SVM $$f(x, W) = Wx + b$$ Example: 3 training examples,3 classes. With some W the scores $f(x,W) = Wx$ are: Multiclass SVM loss: Given an example $(x_i,y_i)$ where $x_i$ is the image and where $y_i$ is the (integer) label, and using the shorthand for the scores vector: $s = f(x_i,W)$ The SVM loss has the form: $$L_i=\sum_{j \neq y_i}\max(0,s_j-s_{y_i} + 1)$$ In this example, $L_1 = max(0, 5.1-3.2 + 1) + max(0,-1.7 - 3.2 + 1) = max(0, 2.9) + max(0, -3.9) = 2.9 + 0 = 2.9$ We can also know that $L_2 = 0,L_3 = 12.9$, so the average $L = 5.27$ Q1: What happens to loss if car scores change a bit? A1: The loss will not change. Q2: What is the min/max possible loss? A2: The min is 0, and the max is infinity. Q3: At initialization W is small so all s \approx 0. What is the loss? A3;-1 Q4: What if the sum was over all classes?(including $j = y_i$) A4: The loss increased by 1 Q5: What if we used mean instead of sum? A5: The answer does not change. Q6: What if we used $L_i = \sum_{j\neq y_i}max(0,s_j-s_{y_i} + 1)^2$ ? A6: It is a different classification. Q7: Suppose that we found a W such that L = 0. Is this W unique? A7: No Softmax Softmax Classifier is a multinomial logistic regression. scores = unnormalized log probabilities of the classes. $$P(Y = k|X = x_i) = \frac{e^{s_k}}{\sum_je^{s_j}},~\text{where}~ s = f(x_i;W)$$ Want to maximize the log likelihood, or (for a loss function) to minimize the negative log likelihood of the correct class: $$L_i = -\log P(Y = y_i|X = x_i)$$ Optimization Random check is a silly way to find the ‘bottom of valley’, In practice, always use analytic gradient, but check implementation with numerical gradient. This is called a gradient check. SGD(Stochastic Gradient Descent) Full sum expensive when N is large. Approximate sum using a minibatch of examples 32/64/128 common Backpropagation and Neural Networks

2021/10/17
articleCard.readMore

DeepLab (WIP)

本篇笔记来自多篇paper的整合,分别是: DeepLab v1:Semantic Image Segmentation With Deep Convolutional Nets and Fully Connected CRFs DeepLab v2: Semantic Image Segmentation with Deep Convolutional Nets, Atrous Convolution, and Fully Connected CRFs v1 作者发现DCNNs的最后一层输出并不能有效地对物体分割,这是由于DCNNs的不变性使它更适用于high-level任务。在该文中,将DCNN最后一层输出和全连接的CRF结合起来可以解决上述问题。 扫盲 不变性 invariance:对一个函数,在其输入中施加的变换不会影响其输出。CNN中的池化有近似不变性。 high-level任务:分类、检测、分割等。一般处理的都是高品质图像,处理降质图像时,性能就会有下降。 hole algorithm 在Caffe 框架中加入一个im2col函数(可以将多通道特征图转化成向量块)稀疏地抽样。这个方法可以在任何子抽样率下有效计算密集CNN特征图而不用引进任何近似值。 CRF(Conditional Random Field) 有多层最大池化的深度模型最适合分类任务。但增加的不变性和更大的感受野会使输出时根据分数推断位置更具挑战性。 目前有两个办法。首先是多收集信息来估算物体的边界;其次是表示为超像素,用low-level分类方法处理位置任务。 本文利用DCNNs的识别能力和全连接CRFs的精细位置估算获得了成功,在物体边界上的处理更精准。 一般会用short range CRFs处理平滑噪声分割,CNN输出已经很平滑,再用short range CRF会使模型变得糟糕。所以采用全连接CRFs,可以使模型中的每个像素连接到其他像素。 总结 我目前没想明白这篇文章在干嘛。我的大致理解是,CNN可以做语义分割,但是在每个物体的边界处处理很模糊,并不够精确。这是因为CNN中的池化具有不变性,层数越深,这种不变性就会增加,感受野也会变大,想要找到物体在图像中的位置就会更难。因此本文在CNN的最后一层运用了全连接CRF,可以在处理边界时更加精确。但是全连接CRF是如何处理图像中物体的边界的,涉及到一些数学公式的理解,还有待学习。 v2 三个主要贡献 强化了卷积在密集预测任务中的作用,空洞卷积使得计算特征响应时可以有效控制结果,也可以扩大卷积的视场以获得更多上下文信息而不用增加额外的参数或计算量。 提出了ASPP(Atrous Spatial Pyramid Pooling)可以稳健地分割多尺度物体。ASPP在多抽样率和有效的视场检测了输入的卷积特征层,从而获取多尺度物体和图像上下文。 结合DCNNs和概率图模型可以改进对象边界的定位。结合最大池化和DCNNs中的downsampling实现不变性,需要牺牲位置精确度,为了解决这个问题,我们将DCNN最后一层的输出和全连接CRF结合,可以在定性和定量上确保定位的精确度。 DCNN在语义分割中的挑战及方法 降低了特征的空间分辨率:用反卷积的上采样替代最后几层最大池化。 存在多个尺度的对象:传统方法是向DCNN中输入同一图像的不同缩放版本,然后整合特征或者得分图,但这样会增加计算量。本文在空间金字塔池化的方法上提出了ASPP,不需要重采样特征,而是采用不同采样率的多个并行多孔卷积层有效地实现此映射。 由于DCNN的不变性而降低了定位精度:跳跃结构可以解决此问题,而全连接的CRF可以提高模型捕获精细细节的能力。 v3 主要贡献 v3+

2021/10/13
articleCard.readMore

FCN for Semantic Segmentation(wip)

本篇笔记来自FCN for semantic segmentation1一文。 介绍 摘要:Fully Convolutional networks中可以输入不同大小的照片,并产生相应大小的输出。 FCN实现了端到端,像素到像素的技术。Fully convolutional现有网络从大小不一的输入中预测输出。学习和推断通过反向计算会在整张图片上同时进行。网络中的上采样层可以做出像素级预测,并在子采样池中学习。 语义分割在语义和位置上面临一个固有的关系:整体信息解决对象是什么,而局部信息让我们知道对象的位置。深度特征分层将位置和语义编码成一个局部到整体的金字塔。我们把结合了了深度、粗糙的语义信息和浅层、精细的表层信息的特征谱定义为一个skip框架。 在下文中,我们将回顾深度分类器网、FCNs和用convnet的语义分割的最新工作。接下来的内容解释FCN的设计和预测,介绍网络中上采样的架构和多层结合,再介绍一下我们的实验框架。最后,证明一下PASCAL VOC 2011-2、NYUDv2、和SIFT Flow。 相关工作 FCN convnet里的每一层数据都是一个大小为hwd三维行列式,h和w是空间维度,d是特征或者channel维度。第一层是图片,像素大小为h*w,有d种颜色。 Adapting classifiers for dense prediction shift-and-stitch is filter rarefaction upsampling 逐块训练是损失采样 分割架构 从分类器到密集FCN 结合对象和位置 框架 结果 结论 Long, Jonathan, Evan Shelhamer, and Trevor Darrell. “Fully convolutional networks for semantic segmentation.” Proceedings of the IEEE conference on computer vision and pattern recognition. 2015. ↩︎

2021/10/11
articleCard.readMore

深度学习在语义分割中的应用(未完待续)

本篇笔记来自深度学习在语义分割中的应用的评论1一文。 介绍 本文剩余部分包括: 第二部分介绍了语义分割的问题、符号及一些背景 第三部分介绍了数据集、比赛和性能测试 第四部分评论现有模型 第五部分讨论上述模型的结果 最后总结全文 术语和背景介绍 通用深度网络架构 下面介绍当前最流行的几种架构: AlexNet AlextNet是深度CNN先驱,曾凭借84.6%的TOP5测试准确率获得ILSVRC-2012。它由五个卷积层、最大池化、ReLUs(Rectified Linear Units)、三个FC(fully-connected) layers以及dropout。 VGG(Visual Geometry Group) 该模型,也叫VGG16,因为其有16加权层。凭借92.7%的TOP5测试准确率获得ILSVRC-2013。VGG16和以前的模型的主要区别在于运用一堆卷积层和小的感受野(receptive fields),这使得参数变小,更加非线性,分辨能力更强,模型更加容易训练。 GoogLeNet GoogLeNet以93.3%的TOP5测试准确率获得ILSVRC-2014,其特点是复杂、包含了22层、以及有一个最新介绍的inception模组。这种方法证明了CNN层不止能按照以前的方式排列,还可以通过别的方式堆积在一起。这些模组由一个NiN(Network in Network)层、一个池操作、一个大和一个小的卷积层组成。这些部分平行计算,然后通过卷积计算降维。因为这些模组的存在,网络可以减少参数数量和操作步骤。 ResNet 微软的ResNet以96.4%准确率赢得了ILSVRC-2016。其拥有152层和残差块(residual block)。在残差块中,每一层都可以将输入复制到下一层。这个方法基于,下一层总是学新的东西并且不同于已经编码的输入。此外,这种连接可以解决梯度消失问题。 ReNet 在ReNet中,他们一直用的序列RNNs,而不是多维RNNs。通过这种方式,RNNs的数量在每一层根据输入照片的维数d线性缩减,这样,每一个卷积层都会被四个垂直以及平行方向的RNNs代替。 迁移学习 不能直接应用迁移学习。一方面,必须要有预训练网络,通常会采用已有的网络架构;另一方面,使用微调(fine-tuning)而不是从头训练会有所不同。选择哪一层fine-tuning很重要,通常是网络的最上层————因为最下层会包含更多常见特征,也需要选择合适的学习率,通常会选择更小的,因为我们希望预训练的权重可以恰到好处,之后就可以不再大幅度改变。 数据预处理和增强 数据增强一般是将一系列转换应用于数据或者特征空间,或者两者皆有。常见的是应用于数据空间,将已有数据进行转换会生成新的样本。有很多转换可用:翻译、回转、扭曲、缩减、色彩空间变化、裁剪等。 数据集和比赛 2D数据集 PASCAL Visual Object Classes (VOC):该比赛中的数据集包含ground-truth,并包含五种竞赛:分类、检测、分割、动作分类、人物布局。 PASCAL Context:所有的照片包含像素点标签。包含540个类别。 PASCAL Part:对象的每个部分都有像素点级别的分割。 Semantic Boundaries Dataset(SBD):是PASCAL VOC的扩充,提供语义分割的ground-truth。 Microsoft Common Object in Context(COCO):关于图片识别、分割和描述的大型数据集。 SYNTHetic Collection of Imagery and Annotations:是一个关于虚拟城市的大型数据集。 et al 2.5D 数据集 NYUDv2:包含1449张室内RGB-D照片。 SUN3D:大型RGB-D视频数据集,有8个标注序列。 SUNRGBD:包含10000张RGB-D照片,适用于场景理解。 et al 3D 数据集 ShapeNet Part:ShapeNet的子集,包含细粒度的3D对象分割。原始数据集的16个类别中的31693个网格样本。 Stanford 2D-3D-S:是一个多模型、大型室内空间数据集,有语义标注。 et al 方法 解码变量(decoder variants) 解码或者map低分辨率的图片为分割做像素级处理是难题,这部分叫做decoder,也是FCN架构的divergence point。 整合上下文(Itegrating Context Knowledge) 语义分割既需要整合多个空间尺度的信息,又要平衡局部和整体信息。 有条件的随机区域(CRF):将低水平的照片信息和产生每个像素的类别分数的多类别推断系统结合起来,这种结合有助于抓取那些CNNs也没注意到的长期依赖并完善局部细节。 扩张的卷积:支持指数级扩张的感受野而不牺牲分辨率。 多尺度预测:运用有多种不同尺度的网络然后预测一个单一输出。 特征融合:将上层提取的整体特征和下层的局部特征合并。 循环神经网络:得益于特殊结构,循环神经网络可以同时适用长短序列。但问题是照片没有自然序列结构,标准vanilla RNNs集中在一维输入。 实例分割 区分同一类别的不同对象。 RGB-D数据 如果图片是RGB,深度数据就需要在每个像素用三种channels编码。RGB-D图片会抓取每个观点到FCN网络,然后得到关于每个图片的每个像素的40个类别的概率。 3D数据 视频序列 讨论 总结 Garcia-Garcia, Alberto, et al. “A review on deep learning techniques applied to semantic segmentation.” arXiv preprint arXiv:1704.06857 (2017). ↩︎

2021/10/10
articleCard.readMore

构建数据依赖的实现

这篇文章总结了DFST、支配、归约的各种性质和算法,并最终给出路径摘要的方案。上一篇文章的链接在这里:从CFG直接构建GSA的算法。 注意:未经许可,禁止转载。通过OpenTimestamps,我已经获得了该文最早的区块链时间戳。有且只有我(me@szp.io)持有其证明。因此如果你能伪造区块链,请你投计算机顶会;如果不能,不要未经我的许可,转载文章内容,否则我保留追究权利。 这篇文章尚在编写中,因而还没给出完整的引用。如果你很迫切想知道前人的工作,可以看上一篇文章“从CFG直接构建GSA的算法”的底部。 数据依赖的定义 数据依赖是定义在合并基本块(入边数目大于等于2的基本块)的入边上的,它表示的是: 如果该边是循环回边:以该边结束,从该基本块到其本身的简单回路成立的条件; 如果该边不是循环回边:以该边结束,从该基本块的立即支配者到其本身的简单路径成立的条件。 本文中的控制流图(CFG)是指带入口节点$Entry$、可以有重边、可以有自环的有向图;此外还需要满足可达性,即存在从$Entry$到任意节点的路径。(所有算法应当能容忍$Entry$有入边的情况,虽然这是次要的) 我们用$N$表示CFG的节点集合,$E$表示CFG的边集合。 示例CFG 各个合并节点入边的数据依赖见。 边的ID 数据依赖条件 6 $(P\land\neg R)\lor(\neg P\land Q\land\neg R)$ 7 $R$ 9 $\neg P\land Q$ 10 $\neg P\land\neg Q\land\neg T$ 13 $\neg P\land\neg Q\land T$ 17 $P$ 中CFG的所有数据依赖条件 接下来,我将讲解可归约CFG上路径摘要算法的原理和实现。路径摘要算法可以用于快速地求解数据依赖条件,也可以用于求解某些前向数据流分析。 路径摘要算法的步骤 路径摘要算法是多步骤的。具体各个步骤的依赖关系见。 数据依赖算法步骤 各步骤的输出及目的见。 编号 名称 输出 1 深度优先生成树 以入口基本块为根的生成树,识别出非循环边和回边 2 支配树 每个基本块的立即支配者 3 识别循环 每个基本块所属的最近循环头 4 归约序列 找到一个序列,序列的每个节点可以归约到后面的节点 5 路径摘要 按照归约序列,计算从立即支配者出发的路径条件 算法的各个步骤 深度优先生成树(DFST) DFST边的类别 一个有向图的边可以按照其某个生成树分类: 树边:终点是起点的父亲 前向边:终点是起点的子孙,且不是自环边或树边 回边:终点是起点的祖父,且不是自环边 自环边:起始与终止节点相同 交叉边:终点既不是起点的子孙,也不是起点的祖父 注:本文中的祖父是指包含节点自己的集合:节点、节点的父亲、节点的父亲的父亲等等组成的集合;子孙的概念类似。如果需要强调不包含自己,我会使用严格祖先、严格子孙。 形象的分类见。特别地,对于有向图按DFST分类的重要性质被标注在了图片的右侧。 DFST边的分类及性质 例子见,节点上的数字为逆后序遍历序号,要指出的一点是,DFST的前序/后序/逆后序遍历特指一种遍历方式,这是相对于生成树的发现顺序的。 示例DFST,标注了逆后序遍历序号及边的类型 自环边和回边我们统称为循环边,而树边、前向边和交叉边统称为非循环边。 DFST边的性质 的第1个性质(自环边与DFST无关)显然。这里给出第2个性质(非循环边组成DAG)的证明。 令DFST上节点$x$的先序遍历序号为$\mathrm{PreOrder}(x)$,那么原图的边$(u,v)$按照生成树分类,一定满足: 树边:$\mathrm{PreOrder}(u)<\mathrm{PreOrder}(v)$(指向还未搜索过的节点,构成生成树) 自环:$\mathrm{PreOrder}(u)=\mathrm{PreOrder}(v)$ 其他边:$\mathrm{PreOrder}(u)>\mathrm{PreOrder}(v)$(指向已经搜索过的节点,回溯) 逆命题同样成立:任何一棵生成树,如果有$\mathrm{PreOrder}$满足了上面的三个性质,它就可以是以$\mathrm{PreOrder}$为发现顺序的深度优先生成树。 可以有两个重要的推论,如下: $$\mathrm{RevPostOrder}(u)>\mathrm{RevPostOrder}(v)$$ 进一步,非循环边组成的子图是一个DAG,其拓扑排序的一种结果是$\mathrm{RevPostOrder}$。 按树的高度进行归纳。 最后,我们很关心的一个问题是:回边集合和非循环边集合会不会随DFST变化。遗憾的是,确实可能会发生变化。最小的例子见。 展示了回边集合和非循环边集合会随DFST变化 但如果CFG可归约,那么DFST对应的回边集合和非循环边集合是不会变的。这也就是的第3个性质,具体细节会在归约章节讨论。 支配树 支配的概念 CFG上,节点$x$支配节点$y$,是指从$Entry$到$y$的每条路径都经过了$x$。由于具有自反性(任何节点都支配自己)、反对称性(否则到$x$需要经过另一个节点$y$且到$y$需要经过$x$,结果到达$x$或$y$没有有穷的路径)、传递性,这是个偏序关系(由此支配关系可以对应到一个DAG)。为了方便,我们有时会说此时$x$小于$y$。节点$x$严格支配节点$y$就是$x$支配$y$且$x\neq y$。 非入口节点$x$的立即支配者$y$就是所有严格支配者中极大的(进一步是“最大的”,看下文“唯一”),它是存在的(至少$Entry$是其严格支配者),且唯一的(如果不同的$y$和$z$同时出现在所有$Entry$到$x$的路径上,那么一定有$y$严格支配$z$或$z$严格支配$y$。否则就会出现两条路径,一条$y$出现在了$z$之前,另一条$z$出现在了$y$之前,那么拼接一下就可以得到不经过$y$/不经过$z$的路径)。 非入口节点的立即支配者存在且唯一,和支配的偏序关系是支配的两条独立性质。通过这两条性质,就能知道支配关系组成了一棵带根树,根即为$Entry$。这颗树我们称为支配树。 支配算法 严格支配关系满足: $$\begin{cases}\mathrm{StrictDoms}(x)=\varnothing,&\text{if}~x=Entry\\\mathrm{StrictDoms}(x)=\bigcap_{y\in\mathrm{Pred}(x)}y\cup\mathrm{StrictDoms}(y),&\text{if}~x\neq Entry\end{cases}$$ (集合交的单位元是全集,因而两个式子不能合并) 说明了严格支配关系的必要条件,但对于一般的CFG,这不是充分条件,详见。 DAG CFG的直接支配算法 是否可以递归?对于DAG CFG,我们发现这个定义是个结构递归(可以找到一种顺序,在求$\mathrm{StrictDoms}(x)$时,对任意$y\in\mathrm{Pred}(x)$,$\mathrm{StrictDoms}(y)$已经求出),因而对于任意基本块$x$,满足上式的$\mathrm{StrictDoms}(x)$是唯一确定的,并且按照拓扑排序即可在确定的时间内完成计算。在稍后的章节中,我们将看到这个递归算法在忽略回边的情况下,同样适用于可归约图。 实际计算中,如果用bit vector作为集合,其集合的交并运算非常复杂,并且没有完全利用支配的“树”的性质。注意到: $$\mathrm{StrictDoms}(x)=\{\mathrm{idom}(x),\mathrm{idom}(\mathrm{idom}(x)),\dots\}$$ 且该集合是全序的,立即支配者为最大元素: $$\mathrm{idom}(x)=\max(\mathrm{StrictDoms}(x))$$ 因此就可以得到一个时间、空间上更有效的计算方法: $$\begin{align*} \mathrm{idom}(x)&=\max(\mathrm{StrictDoms}(x))\\&=\max(\bigcap_{y\in\mathrm{Pred}(x)}y\cup\mathrm{StrictDoms}(y))\\&=\max(\bigcap_{y\in\mathrm{Pred}(x)}\{y,\mathrm{idom}(y),\mathrm{idom}(\mathrm{idom}(y)),\dots\})\\&=\mathrm{LCA}(\mathrm{Pred}(x)),~~~~\text{if}~x\neq Entry\end{align*}$$ 这里$\mathrm{LCA}$是指支配者树上的最低公共祖先。总结一下,就得到了下面的算法: DAG形式CFG的支配树算法。 将$Entry$节点构成一颗单节点树$DomTree$(初始化) 循环:如果还存在一个基本块$x$,满足$x\notin DomTree\land\mathrm{Pred}(x)\subseteq DomTree$: 以$\mathrm{LCA}(\mathrm{Pred}(x))$为父亲,将$x$插入到$DomTree$上 实际实现中,按拓扑序进行插入即可避免中循环的判断。而逆后序遍历恰好是DAG的一种拓扑排序。 $\mathrm{LCA}$的计算 未加说明的话,以下讨论是对于一般的CFG,不仅限于DAG形式的。 多元素集合上的$\mathrm{LCA}(S)$可以归结为两个变量的$\mathrm{LCA}(a, b)$(因为$\mathrm{LCA}$有结合律)。单元素集合上的$\mathrm{LCA}(\{x\})=x$。空集上的$\mathrm{LCA}(\varnothing)$是ill-defined(因为$\mathrm{LCA}$无单位元)。在所有基本块可达的情况下,如果$x\neq Entry$,则$\mathrm{Pred}(x)\neq\varnothing$,所以定义良好。接下来就考虑如何快速地求解两个变量的$\mathrm{LCA}(a, b)$。 支配树上的祖父子孙关系,在DFST上得到了保留。精确地来说: $$\begin{cases}\mathrm{Ancestors}_{DomTree}(x)\equiv\mathrm{Doms}(x)\subseteq\mathrm{Ancestors}_{DFST}(x)\\\mathrm{Descendants}_{DomTree}(x)\equiv\mathrm{Doms}^{-1}(x)\subseteq\mathrm{Descendants}_{DFST}(x)\end{cases}$$ 是更一般的情况。 从支配的定义得到。 类似地,我们也有: $$\mathrm{StrictAncestors}_{DomTree}(x)\equiv\mathrm{StrictDoms}(x)\subseteq\mathrm{StrictAncestors}_{DFST}(x)$$ 注意:其实许多符号既有图论版本的表述,又有控制流版本的表述,它们是等价的,例如: $$\begin{align*}\mathrm{Ancestors}_{DomTree}&\equiv\mathrm{Doms}\\\mathrm{StrictAncestors}_{DomTree}&\equiv\mathrm{StrictDoms}\\\mathrm{Parent}_{DomTree}&\equiv\mathrm{idom}\end{align*}$$ 形象地来说,支配者树比DFST更加扁。一个等价的描述是:如果将树的自上而下视作一个偏序关系,那么支配树的偏序关系是DFST偏序关系的子集;另一个更有趣的描述是:支配树的偏序关系是所有DFST偏序关系的交。个人最喜欢的描述是:支配树是DFST中的某些支干重新接到了祖先上形成的新树。而这个描述将很好地体现在。是一个例子。 DFST与支配树之间的关系 基于此,可以下面的定理: 若DFST上对基本块$x$的逆后序遍历编号$\mathrm{RevPostOrder}(x)$,则支配树上,其子孙的$\mathrm{RevPostOrder}$大于等于$\mathrm{RevPostOrder}(x)$ $$\forall y(y\in\mathrm{Descendants}_{DomTree}(x)\rightarrow\mathrm{RevPostOrder}(y)\geq\mathrm{RevPostOrder}(x))$$ 从和逆后序遍历的性质得到。 有些时候我们会想能不能得到更强的定理:支配树上也一定存在一种先序遍历(等价表述“逆后序遍历”)的结果和DFST的逆后序遍历一样?答案是否定的,见。 基于就有了下面的算法。 支配树上$\mathrm{LCA}(a, b)$的算法。 依据DFST上的逆后序遍历对基本块进行编号,记这个编号为$\mathrm{RevPostOrder}(x)$ 循环:如果$a\neq b$: 如果$\mathrm{RevPostOrder}(a)<\mathrm{RevPostOrder}(b)$: $b\leftarrow\mathrm{Parent}_{DomTree}(b)$(分支1) 否则:(一定有$\mathrm{RevPostOrder}(a)>\mathrm{RevPostOrder}(b)$) $a\leftarrow\mathrm{Parent}_{DomTree}(a)$(分支2) 返回$a$ 这个算法不仅简单,而且性能不错,因为能使用连续的数组提高缓存命中率。注意,分支1和分支2可能会交替执行,这其实是前文中“不能得到更强的定理”导致的:例如中,$N_0,\dots,N_4$插入到支配树后,计算$\mathrm{LCA}(\mathrm{Pred}(N_5))$即$\mathrm{LCA}(N_4,N_3)$时,会: 找到$N_4$的父亲$N_1$(分支2) 找到$N_3$的父亲$N_0$(分支1) 找到$N_1$的父亲$N_0$(分支2) 得出:$\mathrm{LCA}(N_4,N_3) = N_0$ 一般CFG的迭代支配算法 对于一般的CFG,不是结构递归,甚至单单这个式子,作为方程都无法确定唯一解,是个最小的例子。 一般CFG无唯一解示例 中的例子提醒我们,如果采用迭代算法,算法的最终不动点和初值有关,且某些不动点是错误的解(不完整的解)。那么什么解是正确的呢?其实观察上面的两个解,你就可以猜到是那个偏序关系最丰富,树最高的那个解。这里给出其形式化描述以及证明。 对一般的CFG,求解下面的式子: $$\begin{cases}\mathrm{X}(x)=\varnothing,&\text{if}~x=Entry\\\mathrm{X}(x)=\bigcap_{y\in\mathrm{Pred}(x)}y\cup\mathrm{X}(y),&\text{if}~x\neq Entry\end{cases}$$ 会得到若干解,那么对于任意基本块$x$,每个解一定满足: $$\mathrm{X}(x)\subseteq\mathrm{StrictDoms}(x)$$ 注意:证明非结构递归时,不能采用数学归纳法,因为那很可能是循环论证。反证法,假设存在$y$,满足$y\in\mathrm{X}(x)\land y\neq\mathrm{StrictDoms}(x)$。由于$y\neq\mathrm{StrictDoms}(x)$,存在路径$Entry=w\\_0\rightarrow w\\_1\rightarrow\cdots\rightarrow w\\_k=x$,且除终点外路径不经过$y$,即$y\notin\{w\\_i\mid 0\leq i\leq k-1\}$。通过有限次(强调!)的交换律和结合律,可以得到下面的式子: $$\mathrm{X}(x)=(w_{k-1}\cup((w_{k-2}\cup((\cdots)\cap\cdots))\cap\cdots$$ 因此,我们可以得到: $$\mathrm{X}(x)\subseteq\{w_i\mid 0\leq i\leq k-1\}$$ 故$y\notin\mathrm{X}(x)$,与假设矛盾。 基于,我们可以给出一个迭代算法的思路。先用一个偏大的集合初始化所有$\mathrm{StrictDoms}$。而后不断迭代,不断缩小$\mathrm{StrictDoms}$,剔除其中肯定错误的元素(所有更小的合法解都不包含的元素)。那么,这个偏大的集合可以是什么呢?所有CFG节点自然是一个很粗暴且正确的想法。 考虑:支配树的偏序关系是DFST的子集。用DFST作为初始的支配树是完全可行的。于是我们就有了下面的算法: 一般CFG的迭代支配树算法。 用某个DFST初始化$DomTree$(初始化) 循环:如果树上有一个节点$x$,满足$\mathrm{Parent}_{DomTree}(x)\neq\mathrm{LCA}(\mathrm{Pred}(x))$: 更新$\mathrm{Parent}_{DomTree}(x)$为$\mathrm{LCA}(\mathrm{Pred}(x))$(移动了一整棵子树到某个祖先上) 实现上,由于循环体可能会触发很多节点重新计算,一般是多次逆后序遍历,直到一次更改也不发生。这个算法正是K. Cooper的快速支配树算法。 一般CFG的增量支配算法 已知某CFG的支配树,能否快速求出新增节点或边后新CFG的支配树吗? 先考虑新增一个节点$x$。为了保证CFG的可达性,在新增节点的同时还需要新增一条边$(s, x)$。此时,以$s$为父亲,插入$x$到$DomTree$上即可。 再考虑新增一条边的情况。需要下面的定理来导出算法。 若原CFG的支配树为$DomTree$,添加若干边后的支配树为$DomTree'$,那么一定有: $$\begin{cases}\mathrm{Ancestors}_{DomTree’}(x)\subseteq\mathrm{Ancestors}_{DomTree}(x)\\\mathrm{Descendants}_{DomTree’}(x)\subseteq\mathrm{Descendants}_{DomTree}(x)\end{cases}$$ 是原CFG为树的一个特例。 依据,修改的“初始化”为“用原CFG的支配树初始化$DomTree$”,就可以得到增量支配算法。但的循环如果还采用遍历所有节点、计算所有前驱的方式,就显得太盲目了。所以,我们有两个待解决问题: 新增一条边后,如何快速地找出需要更新的节点?见。 以什么顺序添加节点和边能较快地增量构造支配树?见。 为了缩减篇幅,下文的$\mathrm{Ancestors}_{DomTree}$、$\mathrm{Parent}_{DomTree}$和$\mathrm{StrictAncestors}_{DomTree}$不再加下标。 支配树上最小范围的更新传播 新增一条边后$(s, x)$,只有$x$的前驱集可能发生了变化,即: $$\mathrm{Pred}’(x)=\mathrm{Pred}(x)\cup\{s\}$$ 所以我们只需要更新$x$的父亲为: $$\begin{align*}\mathrm{Parent}’(x)&=\mathrm{LCA}(\mathrm{Pred}’(x))\\&=\mathrm{LCA}(\mathrm{Pred}(x)\cup\{s\})\\&=\mathrm{LCA}(\mathrm{LCA}(\mathrm{Pred}(x)),s)\\&=\mathrm{LCA}(\mathrm{Parent}(x),s)\end{align*}$$ 注意:使用了先前的计算结果,极大地减少了计算量。通过可以立刻知道什么情况下,不用更新: $$\mathrm{Parent}(x)\in\mathrm{Ancestors}(s)\Leftrightarrow\mathrm{Parent}’(x)=\mathrm{Parent}(x)$$ 实际实现时,不是很有用,因为$\mathrm{Ancestors}$也需要沿着支配树上行,与$\mathrm{LCA}$计算量差不多。但其实揭示了一个很重要的支配树性质,之后会被经常用于加速计算。 CFG的所有边$(s,x)$在支配树上有: $$\mathrm{Parent}(x)\in\mathrm{Ancestors}(s)$$ 等价的控制流表述为: $$\mathrm{idom}(x)\in\mathrm{Doms}(s)$$ 这里给出两个证明思路: 向CFG添加重边不会使支配树发生变化,由即证; 反证法:假设$\mathrm{idom}(x)\notin\mathrm{Doms}(s)$,那么存在不经过$\mathrm{\mathrm{idom}(x)}$的路径$Entry\xrightarrow{*}s\rightarrow x$,矛盾。 形象地来说,指出,所有CFG上的边在支配树上,会指向起始节点的祖先或起始节点祖先的孩子。 随着$x$的父亲变更,CFG上所有从$x$出发可达的节点都可能需要更新父亲。这部分节点是哪些?它们的父亲又应当更新为哪个节点呢? (不完全的)支配树上,若节点集合$\mathcal{X}$外的所有节点$y$都满足$\mathrm{LCA}(\mathrm{Pred}(y))=\mathrm{Parent}(y)$。现将$\mathcal{X}$中所有节点$x$的父亲从节点$p_x$更新为节点$q$,且$q\in\mathrm{StrictAncestors}(p_x)$。那么对于$\mathcal{X}$外的$y$,满足$\mathrm{LCA}'(\mathrm{Pred}(y))\neq\mathrm{Parent}(y)$的集合恰好是(带单引号的符号是更新父亲后的;不带单引号的符号是更新父亲前的): $$\mathrm{DomUpdate}(\mathcal{X},q):=\{y\mid{}\begin{aligned}[t]&~q\in\mathrm{StrictAncestors}(\mathrm{Parent}(y))\\\land&~\exists x(\begin{aligned}[t]&~x\in\mathcal{X}\\\land&~x\notin\mathrm{Ancestors}(y)\\\land&~\exists z(z\in\mathrm{Pred}(y)\land x\in\mathrm{Ancestors}(z)))\}\end{aligned}\end{aligned}$$ 并且对于属于$\mathrm{DomUpdate}(\mathcal{X},q)$且不属于$\mathcal{X}$的$y$有: $$\mathrm{LCA’}(\mathrm{Pred}(y))=q$$ 先证明属于$\mathrm{DomUpdate}(\mathcal{X},q)$且不属于$\mathcal{X}$的$y$满足$\mathrm{LCA’}(\mathrm{Pred}(y))=q$。$y$有前导在$\mathcal{X}$的某个元素$x$的子树中,而由$x\notin\mathrm{Ancestors}(y)$可以知道$y$也有前导不在$x$的子树中,这样$\mathrm{LCA}’(\mathrm{Pred}(y))=\mathrm{LCA}(\mathrm{Parent}(y),q)$。又由于$q\in\mathrm{StrictAncestors}(\mathrm{Parent}(y))$,可得$\mathrm{LCA}(\mathrm{Parent}(y),q)=q$。 再证明不在$\mathcal{X}$且不在$\mathrm{DomUpdate}(\mathcal{X},q)$中的$y$满足$\mathrm{LCA}’(\mathrm{Pred}(y))\neq\mathrm{Parent}(y)$: $q\notin\mathrm{StrictAncestors}(\mathrm{Parent}(y))$:对于$\mathcal{X}$中的所有$x$,一定有$\mathrm{Pred}(y)$不全在$x$的子树上(否则$\mathrm{LCA}(\mathrm{Pred}(y))$也在$x$的子树上,则$x$支配$y$,由于更新前$p_x$严格支配$x$且$q$严格支配$p_x$,故$q\in\mathrm{StrictAncestors}(\mathrm{Parent}(y))$,矛盾)。 若$\forall x\in\mathcal{X}$,$\mathrm{Pred}(y)$都不在$x$的子树上:$\mathrm{LCA}(\mathrm{Pred})(y)$不变。 若$\exists x\in\mathcal{X}$,存在$u\in\mathrm{Pred}(y)$在$x$的子树上:一定有$\mathrm{LCA}(\mathrm{Pred}(y))\in\mathrm{Ancestors}(q)$(既有前驱在$x$的子树上,又有前驱不在$x$的子树上,因此$\mathrm{LCA}(\mathrm{Pred}(y))\in\mathrm{Ancestors}(p_x)$,由$q\in\mathrm{StrictAncestors}(p_x)$,所以要么$\mathrm{LCA}(\mathrm{Pred}(y))\in\mathrm{Ancestors}(q)$,要么$q\in\mathrm{StrictAncestors}(\mathrm{LCA}(\mathrm{Pred}(y)))$,后者与假设矛盾),由此更新后,$\mathrm{LCA}(\mathrm{Pred}(y))$是$\mathrm{Pred}(y)$的公共祖先。现要证明更新后,$\mathrm{LCA}(\mathrm{Pred}(y))$的孩子中,有至少两个是$\mathrm{Pred}(y)$某元素的祖先。 若$\mathrm{LCA}(\mathrm{Pred}(y))=q$:取$\mathrm{Pred}(y)$中在$x$子树中的元素$u$和不在$x$子树中的元素$v$,更新后$u$到达$q$经过$x$,$v$到达$q$则不经过$x$。 若$\mathrm{LCA}(\mathrm{Pred}(y))\in\mathrm{StrictAncestors}(q)$:所有$\mathrm{Pred}(y)$到达$\mathrm{LCA}(\mathrm{Pred}(y))$经过的孩子是不变的($\mathrm{Pred}(y)$中那些在$\mathcal{X}$某元素子树中的本来就有祖先$q$;不在任何$\mathcal{X}$元素子树中的祖先集合就没变)。 $\forall x(x\in\mathcal{X}\rightarrow x\in\mathrm{Ancestors}(y)\lor\forall z(z\in\mathrm{Pred}(y)\rightarrow x\notin\mathrm{Ancestors}(z)))$: 若$\forall x\in\mathcal{X}$,$\mathrm{Pred}(y)$都不在$x$的子树上:$\mathrm{LCA}(\mathrm{Pred})(y)$不变。 若有至少一个$\mathcal{X}$的元素是$y$的祖先:令它们的$\max$为$x$,则$\mathrm{Pred}(y)$都在$x$的子树上,子树不变,所以$\mathrm{LCA}(\mathrm{Pred}(y))$不变。 如何使用呢?首先,新的边$(s,x)$被添加进了CFG(因而$\mathrm{Pred}$都是考虑了新边),而后,依据我们移动$x$的父亲。若移动真的发生,我们依据定理计算还要更新的集合(的式子其实更新前和更新后计算都是一样的结果)并更新,再计算影响不断迭代。迭代一定终止,因为每次迭代后$\{y\mid\mathrm{Parent}(y)\neq q\}$的个数都在减少。一个不带优化的算法就出来了: 在计算完原CFG的支配树$DomTree$后,新增一条边$(s, x)$到CFG上,未优化的求新CFG的支配树算法。 将$(s, x)$添加到CFG上 $q\leftarrow\mathrm{LCA}(\mathrm{Parent}(x),s)$ 如果$\mathrm{Parent}(x)\neq q$ $\mathrm{Parent}(x)\leftarrow q$ $\mathcal{X}\leftarrow\{x\}$ 循环:$\mathcal{X}\leftarrow\mathrm{DomUpdate}(\mathcal{X}, q)$,如果$\mathcal{X}\neq\varnothing$: 循环:对于$y\in\mathcal{X}$: $\mathrm{Parent}(y)\leftarrow q$ 首先,我们会发现: $$\mathrm{DomUpdate}(\mathcal{X}, q)=\bigcup_{x\in\mathcal{X}}\mathrm{DomUpdate}(\{x\}, q)$$ 这告诉我们,挨个算的结果也一样。进一步,由于并的结合律和交换率,只要避免$\mathrm{DomUpdate}$重复计算,可以FIFO,可以LIFO,都能有一样的结果。那么如何避免重复计算?先应用更改即$\mathrm{Parent}(y)\leftarrow q$,再enqueue即可。这样就无需任何散列表之类的数据结构。 接着,我们来考虑优化$\mathrm{DomUpdate}(\{x\})$的计算。 判断$x\notin\mathrm{Ancestors}(y)\land\exists z(z\in\mathrm{Pred}(y)\land x\in\mathrm{Ancestors}(z)))$:可以搜索$x$的子孙,找到流出其控制区域的出边(这个概念几乎就是支配边界)。这就有两个问题:如何搜索子孙?如何判断边$(w,y)$流出控制区域?前者稍后说,先考虑后者。借助,知$\mathrm{Parent}(y)$支配$w$。由于$x$也支配$w$,故而要么$\mathrm{Parent}(y)$严格支配$x$,要么$x$支配$\mathrm{Parent}(y)$。前者说明是流出控制区域,只需要用即可快速分辨。 判断$q\in\mathrm{StrictAncestors}(\mathrm{Parent}(y))$:先前的检测确保了$\mathrm{Parent}(y)$严格支配$x$,由于$q$也严格支配$x$,故而要么$q$严格支配$\mathrm{Parent}(y)$,要么$\mathrm{Parent}(y)$支配$q$,前者就是我们想要的,同样只需要用即可快速分辨。 如何所搜$x$的子孙?构建支配树时我们会维护一个动态的$\mathrm{Parent}$数组,如果还要维护动态的$\mathrm{Children}$数组太慢了。可否顺着CFG遍历?答案是可行的,因为支配子树的节点可以从子树的根出发,不经过子树外的节点遍历完。然而这需要散列表之类的数据结构标记是否遍历过。有没有更好的?答案就是顺着DFST遍历。(这是支配树和DFST关系的第二个独立性质,第一个是) 支配树上,以$s$为根的子树节点集合为$\mathcal{S}$,则在DFST(有向图)上,从$s$出发,只经过$\mathcal{S}$中的节点,可以到达$\mathcal{S}$中的每个节点。 这个性质是否是Tarjan Lemma 5的等价表述? 由可知从$s$出发可以到达$\mathcal{S}$中的每个节点。反证法,如果存在$s$到某个节点$x\in\mathcal{S}$,经过了不被$s$支配的节点$y$,就可以构造绕过$s$的路径$Entry\xrightarrow{*}y\xrightarrow{+}x$,矛盾。 进一步,遍历子树可以和$\mathrm{DomUpdate}$的迭代共享一个工作队列,最后优化的算法如下: 在计算完原CFG的支配树$DomTree$后,新增一条边$(s, x)$到CFG上,优化后的求新CFG的支配树算法。 将$(s, x)$添加到CFG上,并初始化$S$为工作队列 $q\leftarrow\mathrm{LCA}(\mathrm{Parent}(x),s)$ 如果:$\mathrm{Parent}(x)\neq q$ $S.\mathrm{push}((x, \mathrm{Parent}(x)))$ $\mathrm{Parent}(x)\leftarrow q$ 循环:如果$S$非空: $(t, p)\leftarrow S.\mathrm{pop}()$ 循环:对于$t$的每个出边$e=(t, y)$: $p_y\leftarrow\mathrm{Parent}(y)$ 如果:$\mathrm{RevPostOrder}(p_y)\leq\mathrm{RevPostOrder}(p)$ 如果:$\mathrm{RevPostOrder}(p_y)>\mathrm{RevPostOrder}(q)$: $S.\mathrm{push}((y, p_y))$ $\mathrm{Parent}(y)\leftarrow q$ 否则如果:$e$是DFST的树边: $S.\mathrm{push}((y, p))$ 快速CFG增量顺序 算法比较 支配边界算法 节点$x$的支配边界是那些有前驱被支配,但本身不被严格支配的节点,记为$\mathrm{DF}(x)$: $$\mathrm{DF}(x)=\{y|x\notin\mathrm{StrictDoms}(y)\land\exists p(p\in\mathrm{Pred(y)}\land x\in\mathrm{Doms}(p))\}$$ 按定义,有可能$x\in\mathrm{DF}(x)$。直观理解支配边界就是支配从有到无的界线。接着对于节点$x$,我们引入序列: $$\begin{cases}\mathrm{DF}^{(i)}(x)\equiv\mathrm{DF}(x),&i=0\\\mathrm{DF}^{(i)}(x)=\mathrm{DF}^{(i-1)}(x)\cup\bigcup_{y\in\mathrm{DF}^{(i-1)}(x)}\mathrm{DF}(y),&i=1,2,\dots\end{cases}$$ 注意到: 序列单调递增:$\mathrm{DF}^{(i)}(x)\subseteq\mathrm{DF}^{(i+1)}(x),i=0,1,\dots$ 序列有上界,且上界为有限集:$\mathrm{DF}^{(i)}(x)\subseteq N,i=0,1,\dots$ 故存在最小上界,称作$x$的支配边界闭包,记作$\mathrm{DF}^+(x)$。

2021/9/28
articleCard.readMore

语义分割论文概览

这篇文章是顺着一篇语义分割的综述1开始阅读相关论文。 介绍 早期的Object Detection多数采用尺度不变特征转换(SIFT)。深度学习应用到Object Detection,使得模型相较于传统算法能进行更复杂、精细地识别。这使得大家的研究方向着重于Generic Object Detection(而不是针对某个类别设计特殊的算法)。 目标 递进地来说,Object Detection的目标可以分为以下几种: 图像级别的分类:将图片标记为一个或多个类别 用边框围出事物并标类(边框有时会被叫做Axis-Aligned,因为它们是与坐标轴平齐的矩形) 语义分割:将图片的每个像素标类 事物实例分割:语义分割的基础上,对于同一类别的不同实例加以区分 挑战 准确度相关的挑战 类别内变化大 内在因素:比如各种各样的椅子 图像条件:背景、光照等 类别数目多 性能相关的挑战 SIFT2 ⚠️警告: SIFT这个算法真的是复杂哭了。这里描述的是原论文里的方法。结合GitHub Repo rmislam/PythonSIFT的源码阅读可以极大地帮助理解原论文。当然这个源码似乎是原论文的改进版,里面用到了很多可怕的数学知识。 本文中的部分图片来源于这个Repo的教程。 该文提出了一种局部的图像特征,它不随缩放、平移、旋转的影响,并能部分地容忍光照、仿射、投影。它有生物学基础,即使是在杂乱、遮挡的情况下也能健壮地识别。 特征采用了分阶段过滤方案:首先寻找关键点。再对每个关键点会生成特征。然后匹配多张图的关键点,就可以寻找待识别的物体。 关键点定位 关键点定位:主要思想 (非人话版)通过高斯差分金字塔,从而尺度不变地选取关键点。其输入是一张图片,目标是选出图片中最有特点的点,类似下图(其中关键点用红点标出): 高斯差分金字塔:高斯差是将一张图片通过两个标准差不同的高斯模糊后,相互作差得到的一张新的图片。这张新的图片会包含某一层次的细节。通过采用标准差不断递增的高斯模糊图片,而后紧挨着的两个图片作差,就可以得到一系列变化由细致到粗糙的图片。这一系列的差值图片我们成为高斯差分金字塔。类似下图: 极值点即关键点:差分金字塔某层上的极值点,通常是原图中某一尺度变化丰富的点。通过求解每层共享的极值点,就能得到在各个尺度下都变化显著的点,这些点就是我们想要寻找的关键点。 关键点定位:具体算法 我们用第$n$步指代算法的多个阶段,用第$n$步步骤$i$指代某个阶段的具体操作。 第一步:得到高斯差分金字塔。 对输入图片扩大到2倍(面积就4倍哦)使用双线性差值 选取$\sigma=\sqrt{2}$的高斯模糊处理先前的图片,得到图片A 再次使用$\sigma=\sqrt{2}$的高斯模糊处理图片A,得到图片B(此时相当于对步骤2输入图片直接做$\sigma=2$的高斯模糊) 产生一层高斯差分结果$A-B$ 将图片B的大小是缩小为原来的$1/1.5$倍,采用双线性插值。如果得到的图片太小或完成一定次数,算法完成。否则将该图片作为步骤2的输入,执行步骤2 第二步:找到关键点。 从高斯差分金字的第1层开始,比较每个像素与其相邻的8个像素的大小,如果都大于或小于,则认为是极值点 接着计算这些极值点对应下一层的像素点(考虑第一步步骤5中的缩放),判断他们是否还为极值点,如果不是则筛去它们 重复第2步,直到遍历完所有层 由于极值点数目减少很快,所以第二步比第一步快很多。 Repo中的算法更加复杂,会对原图进行多次缩小,构成多个octave,然后对每个octave构建固定层的高斯差分金字塔。对于关键点选取,Repo是将某一像素所在的当前层连同前后层附近的点全挖去出来,形成了3x3x3的Cube,然后确保其中间点是其中最大的,从而粗略地定位斑点所在的位置和纵深(整数),作为下一步精确寻找的起始点。为了更精确地定位,Repo会用牛顿迭代法进一步寻找极值点的位置和纵深(实数),之后通过斑点的足够明/暗(响应的大小)和圆润与否进一步(Hessian矩阵特征值比值)筛选。 确认关键点的朝向 确认关键点的朝向:主要思想 (非人话版)通过寻找关键点的主要朝向,之后的特征提取可以在消去这个主要朝向后进行,从而使得特征提取不依赖于旋转方向。 像素$A_{ij}$梯度的模$M_{ij}$和朝向$R_{ij}$:对于第一步步骤2的图片A中的每个像素,我们定义其梯度的大小(模)和朝向: $$M_{ij}=\sqrt{(A_{ij}-A_{i+1,j})^2+(A_{ij}-A_{i,j+1})^2}$$ $$R_{ij}=\mathrm{atan2}{(A_{ij}-A_{i+1,j},A_{i,j+1}-A_{ij})}$$ 关键点的位置修正:之前得到关键点的位置是个整数,这里原论文没有细说怎么修正到sub-pixel,但可以想见是使用二次函数拟合局部点,然后找到极值位置。 对光照环境的健壮:设了个阈值,只考虑大于0.1倍最大可能光照的值。(嗯,我没看懂他在说啥) 关键点的主要朝向计算:挖取关键点周围的像素,计算它们的梯度方向,然后投放到36个桶里(每个桶宽度是$10^\circ$)。注意每个像素对桶的贡献是加权的,权重符合以关键点为中心,标准差为当前层$\sigma$ 3倍的高斯分布。之后,这个直方图会被平滑一下,然后选择最高点。关于“周围”的划分,由于是高斯分布,所以3倍标准差以外的点贡献很小,就可以不考虑算作“周围”。 确认关键点的朝向:具体算法 第三步:关键点的主要朝向计算。 初始化36个桶,以及一个标准差为关键点所在层$\sigma$ 3倍的高斯核 对于关键点所在层的图片$A$(而非$B$),挖取周围的像素(论文没说,我觉得再上面标准差基础上乘3倍就可以了,也就是所在层$\sigma$ 9倍),计算其梯度朝向,找到其对应的桶,往桶中加上该像素对应的高斯核值 用一个平滑函数卷积一下36个桶(Repo里用的卷积核是$[1,4,6,4,1]/16$) 找到数额最大的桶作为方向,这里当然也要使用包括最大桶及其左右的两个桶(共3个点),用二次函数插值找最大点 Repo中为了算法更稳定,它会寻找多个极值点,在极值点是最大值的0.8倍以上时,会创建多个位置相同,方向不同的关键点。 关键点特征提取 关键点特征提取:主要思想 文章扯了一大堆生物学。反正就是把关键点周围的像素,旋转一下,抵消掉主要朝向(这样图片旋转了,特征也不变)。然后统计不同位置(分成4x4个大致位置),不同朝向(分成8个大致方向)的像素点的梯度大小。 不过吧,论文里做的更加优雅点,有种双线性插值的感觉,然后有些数字对不太上,读得很困惑。 关键点特征提取:具体算法 第四步: 创建4x4x8的特征张量,前两个维度表示坐标(这里的每个桶代表4倍关键点大小的像素间距),后一个维度表示方向(这里每个桶代表梯度向量$45^\circ$夹角) 将关键点周围的点旋转抵消掉主要朝向,然后计算对应的特征张量的索引(这里索引是3个实数)即位置和方向,以及要存入的值即梯度大小 将这个梯度大小按索引小数部分对应的权重分配到相邻的8个整数索引上 特征张量归一(除以L2范数) 匹配图像 两幅图的关键点在特征的距离上彼此做个最近邻匹配,然后匹配的节点的坐标送进一个大的表示仿射变换的齐次线性方程组,最后来个最小二乘法。 深度学习模型分类 总的来说模型可以分为两类: 两阶段模型:先预处理选出候选区域 一阶段模型:无预处理阶段 两阶段模型 RCNN(Regions with CNN Features)3 与类别无关的候选区域选取,方案为Selective Search4 从图像中裁剪出相同大小的候选区域用于CNN模型预训练 用SVM对CNN模型中提取的特征进行分类 用边框对各类别的特征进行回归 SPPNet(Spatial Pyramid Pooling Net) 之前说到,在RCNN过程中,需要从一张图片中裁剪出的众多区域里提取CNN特征,导致RCNN的训练较慢。而SPP中可以输入不同大小的区域,将其应用于卷积层中,可以使RCNN显著提速。 Fast RCNN Fast RCNN同时在softmax分类器和边框回归训练,还在CONV和FC layer之间添加了RoI(Region of Interest)池给每一个候选区域提取一个已修正长度的特征。相比RCNN和SPPNet,Fast RCNN更加高效。 Faster RCNN Faster RCNN用更高效和精确的RPN(Region Proposal Network)来生成候选区域,RPN和Fast RCNN共享CONV特征,然后将其输入分类器或边框中训练,使得计算更高效。 RFCN(Region based Convolutional Network) 在RFCN中,没有隐藏的FC layer。RFCN和Faster RCNN的区别仅在于RoI,在Faster RCNN中,RoI之后的计算不能被分享;而在RFCN中,将一些特殊的CONV层作为FCN的输出来构造一套对位置敏感的地图,这些地图上的RoI不同于标准的,其精确度和Faster RCNN差不多,但速度更快。 Mask RCNN Mask RCNN处理轴对象实例分割,分为两个阶段,第一个阶段是RPN,第二阶段则是在分类和边框训练的同时在CNN特征地图上进行FCN。 Light Head RCNN 为了提高RFCN的速度,尽可能减少RoI的计算,Li运用了一种大的核分裂卷积来产生更小的特征。 Selective Search 基于图(数据结构)的分割算法5 Selective Search的第一步是使用基于图(数据结构)的分割算法。具体来说,对于图像中的每个通道,我们将其看作由边和节点组成的带权无向图$G=(V,E)$,其中每个像素$p_i$对应于一个节点$v_i\in V$。而边,则是连接相邻的8个像素(论文指出可以采用其他的方法)。边的权重表示的是像素的不相似程度(越大越不相似),这里直接使用像素强度$I(p_i)$的差异作为权重: $$w(v_i,v_j)=|I(p_i)-I(p_j)|$$ 通常在进行这步之前,会使用$\sigma=0.8$的高斯模糊(模糊半径1.6)处理一下图片。 接下来,我们定义该图连通分量$C$的内部差异$Int(C)$。它被定义为其最小生成树$MSE$(Prim算法可以得到)最大的边权。 $$Int(C)=\max_{e\in MST(C)} w(e)$$ 然后,我们定义两个连通分量$C_1,C_2$的最小内部差异$MInt(C_1,C_2)$: $$MInt(C_1,C_2)=\min(Int(C_1)+\tau(C_1),Int(C_2)+\tau(C_2))$$ 其中函数$\tau$可以调控差异,它越大象征着内部差异越大,相对地更能容忍连通区域之间的大差异,从而促使更大的连通区域形成。可能的设计包括使得细条形的$\tau$比较大,从而使得最终分割不太包含细条形的区域。这里我们使用: $$\tau(C)=k/|C|$$ 其中$|C|$表示连通分量节点的个数。这使得小的连通区域不太可能形成。$k$越大,算法更易形成越大的区域。对于128x128的图片,$k=150$是不错的选择;对于320x240及更大的图片,$k=300$是不错的选择。 算法的结果是一个分割$S$,它被定义为互不交且之间无空隙的连通分量的集合。 最后算法如下: 对边集$E$按权重递增排序$\pi=(o_1,\dots,o_m)$($m=|E|$) 初始化分割$S$,其中每个节点各自成一个连通分量。 对于$\pi$中的每个边$o_q=(v_i,v_j)$: 如果$v_i$和$v_j$分属于$S$中不同的两个连通分量$C_i$和$C_j$,且$w(o_q)\leq MInt(C_i,C_j)$: 合并$S$中的连通分量$C_i$和$C_j$ 对于彩色的图片,这个算法在每个通道上运行,最终两个像素在同一个连通分量里当且仅当它们在各个通道上都在同一个连通分量里。 Selective Search剩余算法 一阶段模型 DetectorNet DetectorNet将边框作为分类问题,给定一个图片,他们用一个网络预测一个大致的格子,然后用四个其余的网络预测出对象的上、下、左、右。Detector必须对每张图片采取重要的样本,然后用网络对每个样本的各部分训练。 OverFeat OverFeat产生一组特征向量,每个都代表一个输入的图片中的位置以及可以预测物体的出现。一旦一个物体被识别,相同的特征就会被用来预测一个边框分类器。除此以外,OverFeat能通过处理原始图片六倍大小得到多规模的特征来提高整体表现,并将其整合到一起,得到一个最后的特征向量。OverFeat比RCNN速度更快,但精确度不足。 YOLO(You Only Look Once) 不同于其他基于区域的方法,YOLO从图片整体获得特征。YOLO将一张图片分为SS个格子,每个格子预测C类概率,B个边框和置信分数,这些预测都被编译成SS*(5B+C)张量。 YOLO容易忽略一些小的对象,因为格子分割是很粗糙的,而每一个格子又只能包含一个对象。 YOLOv2 and YOLO9000 YOLOv2是YOLO的增强版,用DarkNet19取代了GoogLeNet,优化了现有工作。 YOLO9000用联合优化的方法同时训练了ImageNet和带WordTree的COCO,可以观测9000多个对象类别。 SSD(Single Shot Detector) SSD比YOLO更快,并与基于区域检测的检测器,如Faster RCNN有差不多的精确度。SSD高效地结合了Faster RCNN中的RPN、YOLO以及多规模CONV特征实现了更快更精确的检测。和YOLO类似,SSD检测一些修正过的边框,然后从中给对象打分,最后由NMS产生最后的检测结果。 基本子问题 基于DCNN的对象表示 特征表示已经转移成为了架构的设计问题。 提高对象表示的方法 检测对象通常需要处理大量数据,一个经典的策略是运行能处理大量图片的检测器,但是会受制于时间和内存;另一种是CNN可以一层一层地计算特征层次,子抽样的层会得到一个如金字塔般固定的规模。 三种多规模对象检测: 检测多CNN层的联合特征 检测多CNN层 上面两种方法的结合 上下文建模 上下文可以分为三类: 语义上下文:不同场景中对象被发现的可能性不一样 空间上下文:在不同位置发现对象的可能性不同 尺度上下文:对象有一部分限制的大小和其他场景中的规模相关 Global context 根据图片或场景,global context可以作为检测对象的线索。 Local context Local context考虑到了对象和其周围的区域。一般来说,在对一些对象之间的关系建模时要求不同类、位置、规模的边框是有逻辑的。在深度学习领域,相关模型的研究很有限,具有代表性的有SMN(Spatial Memory Network)、Object Relation Network、SIN(Structure Inference Network)。 检测方法 一个好的检测建议应该有以下几种特点: 高召回,即只需一点建议就可以实现 建议尽可能精确地匹配对象 高效 边框建议法 对象分割建议法 Liu, Li, et al. “Deep learning for generic object detection: A survey.” International journal of computer vision 128.2 (2020): 261-318. ↩︎ Lowe, David G. “Object recognition from local scale-invariant features.” Proceedings of the seventh IEEE international conference on computer vision. Vol. 2. Ieee, 1999. ↩︎ Girshick, Ross, et al. “Rich feature hierarchies for accurate object detection and semantic segmentation.” Proceedings of the IEEE conference on computer vision and pattern recognition. 2014. ↩︎ Uijlings, Jasper RR, et al. “Selective search for object recognition.” International journal of computer vision 104.2 (2013): 154-171. ↩︎ Felzenszwalb, Pedro F., and Daniel P. Huttenlocher. “Efficient graph-based image segmentation.” International journal of computer vision 59.2 (2004): 167-181. ↩︎

2021/9/26
articleCard.readMore

Coq学习笔记(未完待续)

这篇文章来自《Coq in a Hurry》1的总结。 表达式和逻辑公式 编写正确的公式 使用Check命令能查看公式的类型: > Check True. True : Prop > Check 3. 3 : nat a: A可以表示a的类型是A,或者a是A的证明。它可以位于表达式中,显式指明表达式类型。 常见的类型有Prop表示命题,nat表示自然数。复杂的类型可以通过逻辑联结词(/\、\/)和诸如加、乘、对(a, b)(其类型为A * B)等等创建。使用fun能创建λ表达式(其类型为A -> B,右结合)、forall和exists创建量词: > Check (fun x:nat => x = 3). fun x : nat => x = 3 : nat -> Prop > Check (forall x:nat, x < 3 \/ (exists y: nat, x = y + 3)). forall x : nat, x < 3 \/ (exists y : nat, x = y + 3) : Prop 使用let .. in可以给表达式一个临时的名字,注意到函数应用不需要括号。 > Check (let f := fun x => (x * 3,x) in f 3). (let f := fun x : nat => (x * 3, x) in f 3 : nat * nat 有些符号被重载了,比如*既表示数字的乘法,又表示笛卡尔积类型。使用Locate命令可以找到符号背后的函数。 > Locate "_ <= _". 项是合法的需要遵守: 语法正确 类型正确 而Check命令不仅进行了上面的检查,还给出了表达式的类型。 对表达式求值 使用Compute可以对表达式求值。 用Coq编程 Coq中的程序用函数表示,简单的程序可以被Coq执行,复杂的可以用Extraction命令转换为其他编程语言的。 定义新的常量 使用Definition关键字可以定义常量: > Definition example1 := fun x : nat => x*x+2*x+1. 上面的形式等价于: > Definition example1 (x : nat) := x*x+2*x+1. 定义完后,就可以在诸如Check和Compute的命令中使用。使用Reset后面跟一个名字可以清除该名字。可以使用Print命令查看一个对象的定义。 布尔条件表达式 Coq内置了布尔(bool)值true和false。为了使用它,你需要导入Bool库。 > Require Import Bool. 布尔值可以使用if ... then ... else ...。后者接受一个模式返回符合的;而后者接受一系列的模式,返回都符合的。 使用自然数计算 需要使用Arith库进行自然数的计算。自然数有两种形式,一种是0,另一种是S p,其中p是一个自然数。可以使用match ... with ... end进行模式匹配。第一个子句的|可省略。 Definition is_zero (n:nat) := match n with 0 => true | S p => false end. 如果要使用递归,可以使用Fixpoint关键字。Fixpoint必须遵守一个约束叫structural recursion,即递归只能作用在初始参数(上面的p)上。这个约束确保计算是可终止的。递归函数可以有多个参数,这时候structural recursion必须在一个参数上是成立的。 Coq还支持深模式匹配,Coq会检查是否所有的模式都匹配了: Fixpoint evenb n := match n with 0 => true | 1 => false | S (S p) => evenb p end. 使用列表计算 需要导入List库。如果要将几个元素放入列表中,需要使用::,它将左边的元素添加到右边列表的头,用nil表示空列表: Check 1::2::3::nil. 注意nil需要上下文确定其具体类型。可以使用类型注解表示其类型。 Check (nil : list nat) 有一些预定义的列表函数: app,别名++:链接列表。 map:对列表的每个元素做映射。 列表也可以模式匹配为nil和a::tl(a是一个元素,tl是剩余的列表)。同样也可以递归,递归只能作用在剩余的列表上。 命题和证明 找到已有的证明 Search后面跟标识符可以搜索已有的证明。SearchPattern后面跟一个模式表达式(使用_表示一个不完整的子表达式)。SearchRewrite与SearchPattern类似,只是寻找的一定是个相等的谓词。SearchAbout搜索所有的与某个符号相关的证明。 构建新的证明 构建证明的通常方式是“目标导向的证明”: 使用Theorem或者Lemma开始一个定理。 Coq显示需要证明的命题,已经可以用于该证明的上下文(上下文在=====上方,目标在下方)。 用户输入命令,分解目标。 Coq显示还需要证明的命题。 回到3。 步骤3称为tactics。某些tactics减少了目标的数量。当所有目标都解决,证明就完成了,这时候可以输入Qed.命令,该命令保存了证明。一个简单的证明如下: Lemma example2 : forall a b: Prop, a /\ b -> b /\ a. Proof. intros a b H. split. destruct H as [H1 H2]. exact H2. intuition. Qed. 上下文中的内容一般是a: type或者H: formula的形式,我们称为假设,它表示我们现在有的事实。destruct H as [H1 H2]在H为两个命题的合取时可以使用。它的作用是创建两个新的假设,名字为H1和H2。下表包含了常见的tactics。 Hypothesis H conclusion $\Rightarrow$ apply H intros H $\forall$ apply H intros H $\land$ elim H、case H、destruct H as [H1 H2] intros H $\neg$ elim H、case H intros H $\exists$ elim H、case H、destruct H as [x H] exists v $\lor$ elim H、case H、destruct H as [H1 | H2] left或者right $=$ rewrite H、rewrite <- H reflexivity、ring False elim H、case H 当你在处理假设时,可以用Hypothesis那一列;处理结论时用conclusion那列。 我们用exact H表示我们要证明的结论就在上下文中,assumption可以做到类似的效果。intuition可以用于自动证明。 当使用elim或者case,会将事实放在结论处,其目标会成为蕴含的前件。这些前件稍后可被intros引入。因此有destruct可以同时完成这两件事。 所有的以假设作为参数的tactics都可以以定理作为参数。 用基本tactics的另一个例子 (未完待续) Yves Bertot. Coq in a Hurry. 3rd cycle. Types Summer School, also used at the University of Goteborg, Nice,Ecole Jeunes Chercheurs en Programmation,Universite de Nice, France. 2016, pp.49. inria-00001173v6 ↩︎

2021/8/29
articleCard.readMore

Packrat记忆化Parser与Recoverable Parser

已经废弃了,因为算法感觉很复杂不可靠。主要介绍Tilly的前端所用到的技术。包括记忆化的Packrat Parser(包括它们带来的增量和解决左递归的方法)、Parser Combinator和基于PEG的Recoverable Parser。 Packrat Parser用于解决左递归1 算法 全局变量: $Pos: \mathrm{P{\scriptsize OSITION}}$ $\mathrm{H{\scriptsize EADS}}: \mathrm{P{\scriptsize OSITION}}\to\mathrm{H{\scriptsize EAD}}$ $LRStack:\mathrm{LR}~\text{or}~\mathrm{\scriptsize NIL}$ (链表) $\mathrm{M{\scriptsize EMO}}:(\mathrm{R{\scriptsize ULE}},\mathrm{P{\scriptsize OSITION}})\rightarrow\mathrm{M{\scriptsize EMO}E{\scriptsize NTRY}}$ 其中: $\mathrm{LR}=(seed:\mathrm{AST},rule:\mathrm{R{\scriptsize ULE}},head:\mathrm{H{\scriptsize EAD}}~\text{or}~\mathrm{\scriptsize NIL},next:\mathrm{lR})$ $\mathrm{H{\scriptsize EAD}}=(rule: \mathrm{R{\scriptsize ULE}},involvedSet:\mathrm{S{\scriptsize ET}}~\text{of}~\mathrm{R{\scriptsize ULE}},evalSet:\mathrm{S{\scriptsize ET}}~\text{of}~\mathrm{R{\scriptsize ULE}})$ $\mathrm{M{\scriptsize EMO}E{\scriptsize NTRY}}=(ans: \mathrm{AST}~\text{or}~\mathrm{LR},pos:\mathrm{P{\scriptsize OSITION}})$ $\mathrm{G{\scriptsize ROW}\text{-}LR}(R, P, M, H)$ 参数: $R: \mathrm{R{\scriptsize ULE}}$ $P: \mathrm{P{\scriptsize OSITION}}$ $M: \mathrm{M{\scriptsize EMO}E{\scriptsize NTRY}}$ $H: \mathrm{H{\scriptsize EAD}}$ 返回值: $\mathrm{AST}$ 过程: $\mathrm{H{\scriptsize EADS}}(P)\leftarrow H$ $\mathbf{while}~\mathrm{\scriptsize TRUE}$ $Pos\leftarrow P$ $H.evalSet\leftarrow\mathrm{C{\scriptsize OPY}}(H.involvedSet)$ $\mathbf{let}~ans=\mathrm{E{\scriptsize VAL}}(R.body)$ $\mathbf{if}~ans=\mathrm{\scriptsize FAIL}~\text{or}~Pos\leq M.pos$ $\mathbf{break}$ $M.ans\leftarrow ans$ $M.pos\leftarrow Pos$ $\mathrm{H{\scriptsize EADS}}(P)\leftarrow\mathrm{\scriptsize NIL}$ $Pos\leftarrow M.pos$ $\mathbf{return}~M.ans$ $\mathrm{A{\scriptsize PPLY}\text{-}R{\scriptsize LUE}}(R, P)$ 参数: $R: \mathrm{R{\scriptsize ULE}}$ $P: \mathrm{P{\scriptsize OSITION}}$ 返回值: $\mathrm{AST}$ 过程: $\mathbf{let}~m=\mathrm{R{\scriptsize ECALL}}(R, P)$ $\mathbf{if}~m=\mathrm{\scriptsize NIL}$ $\triangleright$创建一个新的$\mathrm{LR}$,并入栈 $\mathbf{let}~lr=\mathbf{new}~\mathrm{LR}(\mathrm{\scriptsize FAIL},R,\mathrm{\scriptsize NIL},LRStack)$ $LRStack\leftarrow lr$ $\triangleright$记忆化$lr$,并解析$R$ $m\leftarrow\mathbf{new}~\mathrm{M{\scriptsize EMO}E{\scriptsize NTRY}}(lr,P)$ $\mathrm{M{\scriptsize EMO}}(R,P)\leftarrow m$ $\mathbf{let}~ans=\mathrm{E{\scriptsize VAL}}(R.body)$ $\triangleright lr$出栈 $LRStack\leftarrow LRStack.next$ $m.pos\leftarrow Pos$ $\mathbf{if}~lr.head\neq\mathrm{\scriptsize NIL}$ $lr.seed\leftarrow ans$ $\mathbf{return}~\mathrm{LR}\text{-}\mathrm{A{\scriptsize NSWER}}(R,P,m)$ $\mathbf{else}$ $m.ans\leftarrow ans$ $\mathbf{return}~ans$ $\mathbf{else}$ $Pos\leftarrow m.pos$ $\mathbf{if}~m.ans~\text{is}~\mathrm{LR}$ $\mathrm{S{\scriptsize ETUP}}\text{-}\mathrm{LR}(R,m.ans)$ $\mathbf{return}~m.ans.seed$ $\mathbf{else}$ $\mathbf{return}~m.ans$ $\mathrm{S{\scriptsize ETUP}\text{-}LR}(R, L)$ 参数: $R: \mathrm{R{\scriptsize ULE}}$ $L: \mathrm{LR}$ 过程: $\mathbf{if}~L.head=\mathrm{\scriptsize NIL}$ $L.head\leftarrow\mathbf{new}~\mathrm{H\scriptsize{EAD}}(R,\{\},\{\})$ $\mathbf{let}~s=LRStack$ $\mathbf{while}~s.head\neq L.head$ $s.head\leftarrow L.head$ $L.head.involvedSet\leftarrow L.head.involvedSet\cup\{s.rule\}$ $s\leftarrow s.next$ $\mathrm{LR\text{-}A{\scriptsize NSWER}}(R, P, M)$ 参数: $R: \mathrm{R{\scriptsize ULE}}$ $P: \mathrm{P{\scriptsize OSITION}}$ $M: \mathrm{M{\scriptsize EMO}E{\scriptsize NTRY}}$,其中$M.ans~\mathbf{is}~\mathrm{LR1}$ 返回值: $\mathrm{AST}$ 过程: $\mathbf{let}~h=M.ans.head$ $\textbf{if}~h.rule\neq R$ $\textbf{return}~M.ans.seed$ $\textbf{else}$ $M.ans\leftarrow M.ans.seed$ $\textbf{if}~M.ans=\mathrm{\scriptsize FAIL}$ $\textbf{return}~\mathrm{\scriptsize FAIL}$ $\textbf{else}$ $\textbf{return}~\mathrm{G{\scriptsize ROW}\text{-}LR}(R, P, M, h)$ $\mathrm{R{\scriptsize ECALL}}(R, P)$ 参数: $R: \mathrm{R{\scriptsize ULE}}$ $P: \mathrm{P{\scriptsize OSITION}}$ 返回值: $\mathrm{M{\scriptsize EMO}E{\scriptsize NTRY}}~\text{or}~\mathrm{\scriptsize NIL}$ 过程: $\textbf{let}~m=\mathrm{M{\scriptsize EMO}}(R, P)$ $\textbf{let}~h=\mathrm{H{\scriptsize EADS}}(P)$ $\triangleright$如果不是在扩展种子的阶段,直接返回存储的值 $\textbf{if}~h=\mathrm{\scriptsize NIL}$ $\mathbf{return}~m$ $\triangleright$不要计算那些这次左递归没涉及的规则 $\textbf{if}~m=\mathrm{\scriptsize NIL}~\text{and}~R\notin\{h.read\}\cup h.involvedSet$ $\mathbf{return}~\mathbf{new}~\mathrm{M{\scriptsize EMO}E{\scriptsize NTRY}}(\mathrm{\scriptsize FAIL},P)$ $\triangleright$只允许涉及的规则被计算一次 $\textbf{if}~R\in h.evalSet$ $h.evalSet\leftarrow h.evalSet\setminus\{R\}$ $\textbf{let}~ans=\mathrm{E{\scriptsize VAL}}(R.body)$ $m.ans\leftarrow ans$ $m.pos\leftarrow pos$ $\textbf{return}~m$ 合并后的算法 参数: $R: \mathrm{R{\scriptsize ULE}}$ $P: \mathrm{P{\scriptsize OSITION}}$ 返回值: $\mathrm{AST}$ 过程: $\textbf{let}~m=\mathrm{M{\scriptsize EMO}}(R, P)$ $\textbf{let}~h=\mathrm{H{\scriptsize EADS}}(P)$ $\triangleright$如果不是在扩展种子的阶段,直接返回存储的值 $\textbf{if}~h\neq\mathrm{\scriptsize NIL}$ $\triangleright$不要计算那些这次左递归没涉及的规则 $\textbf{if}~m=\mathrm{\scriptsize NIL}~\text{and}~R\notin\{h.read\}\cup h.involvedSet$ $m\leftarrow~\mathbf{new}~\mathrm{M{\scriptsize EMO}E{\scriptsize NTRY}}(\mathrm{\scriptsize FAIL},P)$ $\triangleright$只允许涉及的规则被计算一次 $\textbf{elif}~R\in h.evalSet$ $h.evalSet\leftarrow h.evalSet\setminus\{R\}$ $\textbf{let}~ans=\mathrm{E{\scriptsize VAL}}(R.body)$ $m.ans\leftarrow ans$ $m.pos\leftarrow pos$ $\mathbf{if}~m=\mathrm{\scriptsize NIL}$ $\triangleright$创建一个新的$\mathrm{LR}$,并入栈 $\mathbf{let}~lr=\mathbf{new}~\mathrm{LR}(\mathrm{\scriptsize FAIL},R,\mathrm{\scriptsize NIL},LRStack)$ $LRStack\leftarrow lr$ $\triangleright$记忆化$lr$,并解析$R$ $m\leftarrow\mathbf{new}~\mathrm{M{\scriptsize EMO}E{\scriptsize NTRY}}(lr,P)$ $\mathrm{M{\scriptsize EMO}}(R,P)\leftarrow m$ $\mathbf{let}~ans=\mathrm{E{\scriptsize VAL}}(R.body)$ $\triangleright lr$出栈 $LRStack\leftarrow LRStack.next$ $m.pos\leftarrow Pos$ $\mathbf{if}~lr.head\neq\mathrm{\scriptsize NIL}$ $lr.seed\leftarrow ans$ $\mathbf{let}~h=m.ans.head$ $\textbf{if}~h.rule\neq R$ $\textbf{return}~m.ans.seed$ $\textbf{else}$ $m.ans\leftarrow m.ans.seed$ $\textbf{if}~m.ans=\mathrm{\scriptsize FAIL}$ $\textbf{return}~\mathrm{\scriptsize FAIL}$ $\textbf{else}$ $\mathrm{H{\scriptsize EADS}}(P)\leftarrow h$ $\mathbf{while}~\mathrm{\scriptsize TRUE}$ $Pos\leftarrow P$ $h.evalSet\leftarrow\mathrm{C{\scriptsize OPY}}(h.involvedSet)$ $\mathbf{let}~ans=\mathrm{E{\scriptsize VAL}}(R.body)$ $\mathbf{if}~ans=\mathrm{\scriptsize FAIL}~\text{or}~Pos\leq m.pos$ $\mathbf{break}$ $m.ans\leftarrow ans$ $m.pos\leftarrow Pos$ $\mathrm{H{\scriptsize EADS}}(P)\leftarrow\mathrm{\scriptsize NIL}$ $Pos\leftarrow m.pos$ $\mathbf{return}~m.ans$ $\mathbf{else}$ $m.ans\leftarrow ans$ $\mathbf{return}~ans$ $\mathbf{else}$ $Pos\leftarrow m.pos$ $\mathbf{if}~m.ans~\text{is}~\mathrm{LR}$ $\mathbf{let}~L=m.ans$ $\mathbf{if}~L.head=\mathrm{\scriptsize NIL}$ $L.head\leftarrow\mathbf{new}~\mathrm{H\scriptsize{EAD}}(R,\{\},\{\})$ $\mathbf{let}~s=LRStack$ $\mathbf{while}~s.head\neq L.head$ $s.head\leftarrow L.head$ $L.head.involvedSet\leftarrow L.head.involvedSet\cup\{s.rule\}$ $s\leftarrow s.next$ $\mathbf{return}~L.seed$ $\mathbf{else}$ $\mathbf{return}~m.ans$ Warth, Alessandro, James R. Douglass, and Todd Millstein. “Packrat parsers can support left recursion.” Proceedings of the 2008 ACM SIGPLAN symposium on Partial evaluation and semantics-based program manipulation. 2008. ↩︎

2021/6/29
articleCard.readMore

区间分析

这篇文章的内容来自Allen的Control Flow Analysis1。 基础概念 有向图$G=(B,E)$有节点(块)集合$\{b_1,b_2,\dots,b_n\}$和边集合$\{(b_i,b_j),(b_k,b_l),\dots\}$组成。每个有向边是一个节点有序对$(b_i,b_j)$(不一定互异),代表有向边从$b_i$出发到$b_j$。立即前驱函数$\Gamma^1_G:B\rightarrow P(B)$($P(B)$是$B$的幂集)被定义为$\Gamma^1_G(b_i)=\{b_j\mid(b_i,b_j)\in E\}$ ,可空。类似的可以定理立即后继函数$\Gamma^{-1}_G:B\rightarrow P(B)$被定义为$\Gamma^{-1}_G(b_j)=\{b_i\mid(b_i,b_j)\in E\}$,同样可空。 一个图是连通的,当且仅当任何一个节点可以通过不断使用$\Gamma^1_G$和/或$\Gamma^{-1}_G$得到。注意这里的连通是不讲求方向的。本文讨论的图是有向且连通的(这里的连通应该是指从$e_0$出发能够到达)。 基本块是有唯一入口和唯一出口的指令序列。它可能有多个立即前驱和立即后继,甚至立即前驱、立即后继是自己。控制流图是节点为基本块,边表示控制流路径的图。 图$G=(B,E)$的子图是$G’=(B’,E’)$,其中$B’\subseteq B,E’\subseteq E$,此外$E’\subseteq B’\times B’$。 一个路径$P$是可以一个节点序列$(b_1,b_2,\cdots,b_n)$,其中$b_{i+1}\in\Gamma^1_G(b_i)$(这里假定不存在重边)。边是隐含的$(b_i,b_{i+1})\in E$。节点和边都可以不是唯一的。 节点$q$是节点$p$的后继当且仅当存在路径$P=(p,\dots,q)$。类似可以定义前驱。一个节点可能既是另一个的前驱又是其后继。 一个闭路径,或者环路是一个$b_1=b_n$的路径$P$。如果除了$b_1$和$b_n$,这个路径上的节点没有重复,那称之为简单环路;否则称之为复合环路。 路径$P$的长度$\delta(P)$是路径中边的数目。基于此可以定义两节点之间的最短路:$\delta_\text{min}(p,q)=\min\{\delta(P_i)\mid P_i=(p,\dots,q)\}$。 有向图的一个强连通分量是一个有向子图,其中从任意节点到任意节点。因而每个节点一定在一个闭路径上,且每个节点都是其本身的前驱和后继。闭环路所代表的子图是一种强连通分量的特例。我们可以观察到强连通分量之间有3种关系: 分离:没有共享的节点; 包含:一个强连通分量是另一个的子集; 其他:有部分节点共享,这时候可以合并成一个更大的强连通分量。 如果对于$G$中的任何一个强连通分量$R$,其他的(不是其子图的)强连通分量$R’$与其不共享节点,那么称这个强连通分量$R$为最大的。一个我认为没有太多用的基于强连通分量建立偏序关系的方案就省略了。 支配关系 在一个有向图内,一个没有后继的节点成为出口节点。类似的定义不太适合可能存在循环入边的入口节点,因而入口节点被认为是控制流图中包含程序入口的节点,它是唯一的。然后给这个有向图添加一个没有前驱的初始节点$e_0$作为入口节点的唯一前驱。 如果一个节点$b_i$出现在了每条从$e_0$到$b_k$的路径上,那么称$b_i$是前支配(back dominate;predominate)$b_k$。令$\mathcal{P}=\{P\mid P=(e_0,\dots,b_k)\}$,则其前支配者集合$BD(b_k)$为: $$BD(b_k)=\{b_i\mid b_i\neq b_k\land b_i\in\cap\mathcal{P}\}$$ 译注:这里的支配者把本身排除了,但实际上一般认为任何一个节点支配其本身 。$b_k$的立即前支配者$b_i$是前支配者集合中离$b_k$最近的。 $$b_i=\underset{b_j\in BD(b_k)}{\text{arg\,min}}\delta_\text{min}(b_j,b_k)$$ 立即前支配者存在且唯一。由于$e_0$是所有节点的前支配者,所以一定存在。如果同时存在两个不同的立即前支配者,它们有相同的距离,必然在不同的路径上;但又由于立即前支配者必然出现在了每条路径上,因而矛盾,一定唯一。 一个有趣的发现是$BD(b_k)$可以通过$\delta_\text{min}$建立严格偏序关系。 $$BD(b_k)=(b_1,b_2,\dots,b_j)~~\text{where}~b_1=e_0$$ 在上式中对任意$1\leq m<n\leq j$,有$\delta_\text{min}(b_m)>\delta_\text{min}(b_n)$。不仅如此,对任意的$1\leq m<j$,有$b_m$是$b_{m+1}$的立即前支配者;对任意的$1\leq m<n\leq j$,$b_m$是$b_n$的前支配者。 类似地我们可以定义节点$b_i$后支配节点$b_k$当且仅当$b_i$出现在了每条$b_k$前往任意出口的路径上。同样地,我们可以引入一个节点$x_0$作为所有出口节点的后继。后支配的相关概念和前支配类似,这里不重复表述了。 一个关节节点出现在了每条从入口到出口的路径上。对一有唯一入口$e_0$的图,关节节点就是$e_0$以及$e_0$的后支配者集合。 区间 给定一个节点$h$,区间$I(h)$是指最大的,以$h$为唯一入口的子图,并且该子图中的所有环路都包含$h$。$h$被称为区间头或者头节点。区间可以用其包含的节点表示,区间的边则可以被隐含地得到。 通过选取适当的节点作头节点集合的元素,一个图可以被唯一地划分为区间。以下是一个划分图的算法。 算法用到以下的数据结构: $H$:存放处理过的和待处理的节点; $I(h)$:$Map\langle B, Set\langle B\rangle\rangle$,以$h$作为区间头的节点集合。 过程A: 添加$e_0$到$H$中并标记为待处理。 如果$H$有待处理的$h$: 标记$h$为已处理。 $I(h)\leftarrow\{h\}$ 对所有的$b\in G$且$b\notin I(h)$且$\Gamma^{-1}_G(b)\subseteq I(h)$:(这一步不断重复,直到无法继续,实际实现需要一个工作队列) $I(h)\leftarrow I(h)\cup\{b\}$ 对所有的$b\in G$且$b\notin H$且$b\notin I(h)$且$\Gamma^{-1}_G(b)\cap I(h)\neq\varnothing$: 添加$b$到$H$中并标记为待处理。 区间分析的例子 上图包含以下区间: 区间 节点集合 $I(1)$ $1$ $I(2)$ $2$ $I(3)$ $3,4,5,6$ $I(7)$ $7,8$ 首先我们证明所有的$I(h)$都是最大的、单入口的,且所有$I(h)$的环路都包含$h$,稍后我们会证明这是个划分。 论断1-3论证算法的正确性。 论断 1 $I(h)$只包含一个入口节点,$h$。 证明 假设还存在另一个节点$b\in I(h),b\neq h$是入口节点,那么$b$的立即前导节点中一定存在节点不在$I(h)$中,这与算法中$b$添加到$I(h)$的条件即步骤2.3矛盾。此外可以注意到,$h\neq e_0$有至少一个立即前导节点在$I(h)$外,这可以有步骤$2.3$推导出。 论断 2 $I(h)$中所有的闭环都包含$h$。 证明 假设存在一条环路$P=(b_1,b_2,\dots,b_n,b_1)$不包含$h$。我们知道$b_{i-1}$是$b_i$的立即前驱节点。因此只有$b_{i-1}$成为$I(h)$的成员后,$b_i$才能成为;此外,只有$b_n$成为了$I(h)$的成员后之后,$b_1$才能成为。结果就是没有一个能成为$I(h)$的成员。矛盾。 论断 3 $I(h)$是最大的。 证明 由步骤2.3可以得到,即不断执行知道无法有更多的节点添加的步骤。 区间具有一些性质,这里在下方给出,这些限制不限于过程A给出的区间,也不需要“最大”这个性质。 论断4-7寻找区间与支配的关系。 论断 4 区间头支配区间内的所有节点。 证明 论断1的推论。 对于区间$I(h)$内的任意一个节点$b_i$可以定义一个更加严格的局部后继函数$L^1_I$,将后继限制在区间内的非头节点中: $$L^1_I(b_i)=\{b_j\mid b_j\in\Gamma^1_I(b_i)\land b_j\neq h\}$$ 借助这个局部后继函数,可以定义局部前导函数$L^{-1}_I$。特别地我们有$L^{-1}_I(h)=\varnothing$。 使用局部后继函数,可以在区间内定义一种特殊的路径:前向路径$F=(b_1,b_2,\dots,b_n)$,其中$b_{i+1}\in L^1_I(b_i)$。可以注意到从$h$到$I(h)$内某节点的所有路径上的节点都在$I(h)$内(这是支配决定的)。 论断 5 可以通过局部后继函数在区间内的节点定义偏序关系。也就是对于给定的区间,可以表示为节点的序列$I(h)=(b_1=h,b_2,\dots,b_n)$,并且对于所有的$i<j$,要么$b_i$在某个前向路径上是$b_j$的前驱,要么$b_i$和$b_j$不同时出现在任何一个前向路径上。 证明 由过程A可以看出一个非$h$的节点在变成$I(h)$成员前,其立即前驱节点必须已经成为$I(h)$的成员。 其实这个区间序列就是过程A中添加到$I(h)$的先后顺序。 论断 6 前支配列表中序列的相对顺序和区间序列是一致的。 证明 在一个区间中,如果$b_i$是$b_j$的前支配者,那么$b_i$会出现在$h$到$b_j$的每个前向路径上。 这个定理是我非常关注的。它揭示了区间(归约)和支配的关系。我现在只想知道从区间序列中能否得到支配者树。 论断 7 对于区间成员$b_k$,它的前支配者序列为$BD(b_k)=(b_1=e_0,b_2,\dots,b_j)$。那么一定有一个$b_i\in BD(b_k)$满足$b_i=h$,且对于所有$b_i$之后的节点$b_l,i<l\leq j$,$b_l$一定是区间的成员。 证明 $b_l$出现在所有的从$h$到$b_k$的路径上,因而$b_l$一定是区间成员。 这个定理给出了一种基于区间的求解前支配者集合的思路。 论断8-9寻找区间和强连通分量的关系。 论断 8 区间中的任何强连通分量一定包含区间头。因此一个区间不可能包含分离的前连通分量。 证明 由任何闭路径都包含$h$可以得到。 论断 9 如果区间内部存在一个强连通分量,那么一定存在从这个强连通分量内的任意节点到区间内的任意节点的路径。 证明 一定存在从这个强连通分量内的任意节点到区间头的路径,也一定存在从区间头到区间内的任意节点的路径。 定义区间出口节点为没有立即后继(全图的出口节点)或者存在至少一个立即后继不在区间中的节点。论断10-11寻找求解区间关节节点的方案。 论断 10 对于区间而言,区间头是一个关节节点。 证明 真的很显然。 论断 11 所有区间头的区间内后支配者,以及区间头,就是区间的关节节点。 两终端子图有时也会是研究者感兴趣的。两终端子图是只有一个出口节点的区间。 接下来我们给出寻找区间内强连通分量、区间的关节节点以及区间内每个节点的前支配者的算法。这些算法可以嵌入过程A中,使得整个算法一步完成。 在具体实现上,为了快速地计算,这些算法可以使用位图以加速集合运算。 过程B: 计算出区间内每个节点的前支配者。 $BD(h)\leftarrow\varnothing$ 按照区间序列的顺序遍历节点$b_j$: $BD(b_j)\leftarrow\bigcap\limits_{b_i\in L^{-1}_I(b_j)}(b_i\cup BD(b_i))$ 过程C: 求出区间的关节节点。 $A\leftarrow\bigcap\limits_{b_i\in\text{IntervalExits}}(b_i\cup BD(b_i))$ 过程D: 找到节点$b_i$的所有局部前驱$LP(b_i)$。 $LP(h)\leftarrow\varnothing$ 按照区间序列的顺序遍历节点$b_j$: $LP(b_j)\leftarrow\bigcup\limits_{b_i\in L^{-1}_I(b_j)}(b_i\cup LP(b_i))$ 一个锁节点是区间中以区间头作为立即后继的节点。一个不存在锁节点的区间也就不存在强连通分量。 分 过程E: 求出包含锁节点的区间的强连通分量。 $SCR\leftarrow\bigcup\limits_{b_i\in\text{IntervalLatchings}}(b_i\cup LP(b_i))$ 另一种等价的算法是从锁节点出发,不断添加所有的立即前驱直到到达区间头。这种算法不需要求解$LP$。 使用区间划分图 接下来,我们证明过程A产生出来的区间集合$\Phi=\{I(h_1),I(h_2),\dots\}$组成了唯一的$G$的划分。所以我们要证明$\Phi$覆盖了$G$,任意两个区间的交为空,以及$\Phi$是唯一的。 论断 12 $\Phi$覆盖了$G$。 证明 如果$b\in G$且$b$不属于任何区间。由于$G$是个连通的图,那么$b$要么是$e_0$要么至少有一个立即前驱。如果$b=e_0$,那么$e_0\in I(e_0)$。如果它有至少一个立即前驱,那么它要么和立即前驱在同一区间内,要么它形成了新的区间头,要么立即前驱也不属于任何区间。对于最后一个情况不断递归就会得到$e_0$不属于任何区间,因而最后一个情况不成立。(其实这应该用数学归纳法) 论断 13 $\Phi$中的区间是分离的,也就是对于不同的$I(h)$和$I(h’)$,有$I(h)\cap I(h’)=\varnothing$。 证明 如下: 区间头一定是互异的,即$h\neq h’$,因为过程A中,每个节点成为循环头后,就会被标记为已处理,从而不可能再次成为循环头。 区间头不可能成为另一个区间的成员。假设区间头$h$是区间$I(h’)$的成员。故$h$的所有立即前驱也在$I(h’)$中。$h$要成为区间头,必然有一个但不是全部的前驱还在某个别的区间$I(h’’)$中(这里不能假设前驱只归属于一个区间)。我们将要证明$h’’$也是区间$I(h’)$的成员。 假设立即前驱$b$既在区间$I(h’)$中,又在区间$I(h’’)$中,我们有$b$同时被$h’$和$h’’$前支配。由于前支配集合是严格偏序的,所以要么$h’$前支配$h’’$要么反过来。由于$h$有不在区间$I(h’’)$的前驱$c$,故而是$h’$前支配$h’’$(否则由区间最大会得到$h’$和$c$都在$I(h’’)$中),进而由论断7得$h’’$在$I(h’)$中。 这样不断递归下去,只能得到没有区间头位在$I(h’)$中。 不同区间的交为空。假设存在$b\in I(h)\cap I(h’)$,由上面的讨论我们知道$b$不是任何区间头。如果$b$要同时出现在两个区间中,那么它的立即前驱也必须出现在两个区间中。不断递归下去就会得到两个区间头出现在两个区间中,从而矛盾。 论断 14 $\Phi=\{I(h),I(h’),\dots\}$是唯一的。 证明 实际上由于区间总归是最大的,所以当区间头确定完后,区间就确定了。由于区间之间是不会重叠的,所以区间的先后顺序是不会影响区间头的选取,只是区间头的顺序发生了变化。 接下来我们扩充区间的定义。先前的区间被称为一阶区间,区间所在的原图被称为一阶图。我们会构造高阶图和高阶区间,所以我们用上标表示阶。 二阶图是将每个一阶区间合并成一个节点之后形成的图。二阶图节点的立即前驱是原图中区间头的非区间内立即前驱;立即后继则是原图中区间出口节点的非区间立即后继。二阶区间是二阶图的区间。 不断如此就能构建高阶图和高阶区间。最后的高阶图要么只有一个节点组成,称原图为可归约图;否则,称原图为不可归约图(不再包含节点数不少于两个的区间)。 对于不可归约图,可以采用分裂节点的方式化为可归约图。假设$G^1$最终归约成了一个节点,可以观察到一下有趣的性质: $G^1$中的每个节点出现在有且仅有一个区间$I^1(h)\in\Phi^1$,而后成为了$G^2$的节点,并且出现在有且仅有一个区间$I^2(h)\in\Phi^2$,以此类推。 因而对于每个基本块而言,它们有唯一的成员关系链:$b_i\in I^1(h_1)\in I^2(h_2)\in\cdots\in I^n(h_n)$。 由于区间内的节点使用局部后继函数建立了偏序关系,所以整个图的节点都建立了偏序关系。 这里给出一般的适用于许多分析的框架(其实思路就是自底向上,在自上而下): 过程F: 处理程序中的每个基本块,收集感兴趣的基本块信息保存至基本块的入口和出口。令$k=1$ 对于每个$k$阶区间: 按照区间序列处理成员,首先更新成员的状态作为对前导的反应,然后将修改传播至后继 如果锁节点传播给区间头的信息发生了更新,那就重新上一步 令$k=k+1$,重做上一步,直到归约成$n$阶图即一个节点 令$k=n-2$ 将$k+1$阶图的信息关联到$k$阶图的区间头上 将区间头的信息传递给区间内的每个节点 令$k=k-1$,重做上两步,直到得到一阶图 总结 本文介绍的区间有很多性质,可以服务于全局分析。区间内节点间的偏序关系提供了一个自然的顺序;对图划分并组成层级的区间可以帮助我们快速地传递信息;图中的支配关系可以用区间挖掘;图中嵌套的强连通分量也可以用区间检测。 Allen, Frances E. “Control flow analysis.” ACM Sigplan Notices 5.7 (1970): 1-19. ↩︎

2021/5/20
articleCard.readMore

检测流图的可归约性

这篇文章的主要内容来自于Tarjan的Testing Flow Graph Reducibility1。研究这篇文章的目的是为了得到求解归约序列的算法。依照规约序列分析可以很好的降低分析的复杂度,比如“从CFG直接构建GSA的算法”中的算法。 许多程序优化和分析都涉及到interval分析。而适合这类分析的流图是可归约的。本篇文章使用深度优先搜索和并查集测试流图的可规约性。本算法的复杂度为$O(E\log^* E)$,其中$\log^*x=\min\{i\mid\log^{(i)}x\leq 1\}$。 基础概念 引理 1 如果$ND(v)$是树$T$某节点$v$的子孙个数。如果$T$有$V$个节点,并且通过前序标号为$1$到$V$。那么树上有路径$v\xrightarrow{*}w$,当且仅当$v\leq w<v+ND(v)$。 这个定理给出了在前序遍历完毕后,$O(1)$复杂度判断两节点是否有祖先-子孙关系的方法。 令$(G,s)$为一流图(流图有一特殊的入口节点,且该入口节点可以到达所有节点),$T$是$G$的生成树且以$s$为根,并通过前序标号。$T$被称为深度优先生成树(DFST),如果$G$中不在$T$的边可以分为3个集合: 对于$G$的边$(v,w)$在$T$中存在路径$w\xrightarrow{*}v$,称为循环边; 对于$G$的边$(v,w)$在$T$中存在路径$v\xrightarrow{*}w$,称为前向边; 对于$G$的边$(v,w)$在$T$中即不存在路径$v\xrightarrow{*}w$也不存在路径$w\xrightarrow{*}v$,且此时$w<v$,称为交叉边; 一个DFST之所以这么称呼,是因为它可以通过从$s$出发在$G$上进行深度优先遍历得到。因而可以在$O(V+E)$的时间复杂度里生成DFST、计算每个节点的序号和$ND(v)$、计算出循环边、前向边、交叉边的集合。 流图和可归约性 令$v$和$w\neq s$是流图$(G,s)$中的两个节点。定义一种操作“将$w$合并进$v$”,即删除$w$及其所有边,在不构成自环和重边的情况下,将$w$的边接到$v$上,构成新图$G’$。$G’$同样是流图。 如果$G’$是$G$通过几次合并操作形成的图,那么$G’$的每个节点$v’$都对应$G$中节点的集合。此外$G’$的每个边也会对应$G$的边,但反过来不一定有对应的。考虑下面的变换: $T_2$:如果$(v,w)$是$w$唯一的入边,且$w\neq s$,将$w$合并进$v$。 一个流图如果是可归约的当且仅当不断使用$T_2$可以将流图转化为只包含$s$的图(这个定义中假定流图不包含自环)。如果$G’$由$G$通过几次$T_2$变换得到,则称$G’$是$G$的归约。一个唯一的图可以通过不断使用$T_2$变换得到,因此变换的顺序并不影响可归约性。 令$T$是流图$(G,s)$的一个DFST。 定理 2 (Hecht & Ullman) 如果$G$是可归约的当且仅当相对于$T$对于每个循环边$(v,w)$,$w$支配$v$。 对于$G$中的任何节点$w$。令$C(w)=\{v\mid (v,w)\text{是循环边}\}$,令$P(w)=\{v\mid\exists z\in C(w)\text{使得存在不经过$w$从$v$到$z$的路径}\}$。如果没有循环边$(v,w)$,那么$C(w)=\varnothing$、$P(w)=\varnothing$。形象地说,$C(w)$是循环头$w$对应的循环尾集合,$P(w)$是循环头$w$除去它本身外对应的循环体集合。对于有多个循环入口的情况下,即循环边终点不支配循环边起点的情况下,$s\in P(w)$。 引理 3 $G$是可归约的当且仅当对于所有的$w$和所有的$v\in P(w)$,$w\xrightarrow{*}v$在$T$中。 证明 “$\Rightarrow$”,反证法,如果存在$v\in P(w)$且$w\xrightarrow{*}v$不再$T$中,即$v$不是$w$的子孙。因而存在不经过$w$的$s$到$v$再到$C(w)$中某节点的一条路径,即$w$不支配$v$,也不支配$C(w)$中某节点,由定理$2$知$G$不可归约。“$\Leftarrow$”,反证法,$G$是不可归约的,由定理$2$,存在一个循环边$(v,w)$,满足$w$不支配$v$。因而存在$s$到$v$的不经过$w$的路径 ,故$s\in P(w)$,但$w\xrightarrow{*} s$不再$T$中。 令$w$是$G$中有循环边作为入边的节点中,编号最大的那个。并且假设对于所有的$v\in P(w)$,$w\xrightarrow{*}v$在$T$中。令$G’$是$G$在将所有$P(w)$中的节点合并进$w$后形成的图。 引理 4 $G’$中每个边$(v’,w’)$都对应于$G$中的某个边$(v,w’)$,且$v’\rightarrow v$在$T$中。 证明 令$(v,w’)$是$G$中的某条边。如果$w’\in P(w)$,那么$v\in P(w)\cup\{w\}$(因为$w’\in P(w)$等价于存在不经过$w$的从$w’$到$C(w)$某节点路径,将$(v,w’)$连街上这条路径,如果$v\neq w$就可以得到不经过$w$的$v$到$C(w)$某节点路径)。如果$v\in P(w)$,那么$w\xrightarrow{*}v$(题设)。因此,有以下情况: $w’\in P(w)\land(v\in P(w)\lor v=w)$:$(v,w’)$在$G’$中无对应的边; $v\notin P(w)\land v\neq w$:此时一定有$w’\notin P(w)$,$(v,w’)$在$G’$中对应于$(v,w’)$; $w’\notin P(w)\land(v\in P(w)\lor v=w)$:此时$v$一定被合进$w$(或者$v$就是$w$),由题设知道有$v’=w\xrightarrow{*}v$,$(v,w’)$在$G’$中对应于$(v’,w’)$。 令$T’$是$G’$的子图,其中的边由那些对应于原DFST $T$的边组成。 引理 5 $T’$,在和$T$有相同的编号下,是$G’$的DFST。$G’$的循环边对应于$G$的循环边,$G’$的前向边对应于$G$的前向边或交叉边,$G’$的交叉边对应于$G$的交叉边。 引理 6 对于任何节点$x<w$,令$P’(x)$和$C’(x)$根据$T’$定义在$G’$上,同时$P(x)$和$C(x)$根据$T$定义在$G$上。如果对于所有$y\in P’(x)$在$T’$中有$x\xrightarrow{*}y$当且仅当对于所有$y\in P(x)$在$T$中有$x\xrightarrow{*}y$ 证明 “$\Leftarrow$”,反证法,如果存在$y\in P’(x)$且$T’$中没有$x\xrightarrow{*}y$,即$T’$中$y$不是$x$的子孙。那么同样可以得到在$T$中$y$不是$x$的子孙,需要证明的是$y\in P(x)$。进一步,在$G’$中存在不经过$x$的从$y$到某个点$z’$的路径$p’$,其中$(z’,x)$是$G’$的循环边。由引理5,可以知道$(z’,x)$对应于$G$中的某个循环边$(z,x)$。由于有$P(w)\cup\{w\}$导出的$G$的子图是强连通的,因而在$G$中存在一个不仅过$x$的从$y$到$z$的路径$p$,路径上的边由$p’$对应的边或者$P(w)\cup\{w\}$中节点之间的边组成。因而$y\in P(x)$。 反向的,“$\Rightarrow$”,反证法,如果存在$y\in P(x)$且在$T$中$y$不是$x$的子孙。如果$y\notin P(w)$,那么$y\in P’(x)$且在$T’$中$y$不是$x$的子孙。如果$y\in P(w)$,那么$T’$中$w$不是$x$的子孙,且$w\in P’(x)$。 引理3-6可以导出一个高效地测试图可归约性的算法。令$T$是流图$(G,s)$的一个DFST,并且令$w_1>w_2>\cdots>w_n$是$G$中有入边是循环边的节点。我们计算$P(w_1)$。如果$P(w_1)$中有非$w_1$的子孙节点,我们就停止;否则的话。我们将$P(w_1)$合并进$w_1$组成一个图$G’$,然后计算$G’$中的$P’(w_2)$。如果$P’(w_2)$中有非$w_2$的子孙节点,我们就停止;否则的话。我们将$P‘(w_2)$合并进$w_2$组成一个图$G’’$。不断这样直到我们知道$G$不可归约或者所有的循环边都处理完了,后者意味着$G$可归约。 合并节点从而组成$G’$和$G’’$可以使用并查集。初始的时候,有$V$个单元素集合,每个集合的代表元素都是那唯一的元素。我们定义以下操作: $\text{FIND}(x)$,找到$x$所属集合的代表元; $\text{UNION}(A,B,C)$,合并集合$A$和$B$(摧毁它们),并且给一个新的名字$C$。 下面给出归约算法。除了测试可归约性,这个算法还能给出每个节点首先合并进了哪个$w_i$。这个值被定义为$\text{HIGHPT}(x)$,如果$x$没被合并,那么定义为0。$\text{HIGHPT}(x)$被用于构建一个归约序列。 算法$\text{REDUCE}(G,s)$: 使用深度优先搜索构建$G$的DFST,节点通过前序遍历标号$1$到$V$,对每个节点$v$计算$ND(v)$ 对于$v=1\dots V$: 构建进入$v$的循环边、前向边、交叉边 构建包含$v$的单元素并查集 $\text{HIGHTPT}(v):=0$ 对于$w=V\dots 1$: $P:=\varnothing$ 对于每个进入$w$的循环边$(v,w)$: $P:=P\cup\{\text{FIND}(v)\}$ $Q:=P$($Q$是一个待处理的工作队列) 当$Q\neq\varnothing$时不断循环: $x:=pop(Q)$ 如果$w>x\lor w+ND(w)\leq x$: 终止,$G$不可归约 如果$\text{HIGHPT(x)}=0$: $\text{HIGHPT(x)}:=w$ 对于每个进入$x$前向边、树边、交叉边$(y,x)$: $y’:=\text{FIND}(y)$ 如果$y’\notin P$且$y’\neq w$: $P:=P\cup\{y’\}$ $push(Q,y’)$ 注释:现在$P=P(w)$ 对于每个$x\in P$: $\text{UNION}(x,w,w)$ $G$可归约 标鲜红色的代码被我从内层循环移了出来。原作者的伪代码似乎会漏给循环尾赋$\text{HIGHPT}$。要查看算法差异可以见页底引用的原论文。 归约一个可归约图 这个算法本身不是构造性的,但我们可以通过$\text{HIGHPT}(v)$构造归约序列。我们给每个$G$上的节点赋予一个数$\text{SNUMBER}$,使得对于树边$(v,w)$,$\text{SNUMBER}(v)<\text{SNUMBER}(w)$;对于交叉边$(v,w)$,$\text{SNUMBER}(v)<\text{SNUMBER}(w)$。这步可以通过深度优先搜索时,先遍历序号大的实现。再运行完算法后,我们就得到了一个二元组$(\text{HIGHPT}(v),\text{SNUMBER}(v))$,而后我们对节点按二元组字典序排序,其中二元组第一个元素降序,第二个元素升序。这个排序可以在$O(V)$的时间内通过两轮基数排序完成。 这里实际上就是对DAG进行了拓扑排序。 流图示例,节点标签为$dfs (highpt, snumber)$ 引理 7 如果$G$是可归约的,那么我们可以通过归约顺序合并$G$的节点。 证明 通过合并节点的数目进行归纳。假设$v$前面的节点都被合并了,这就创建了一个图$G$的归约$G’$。如果$v$不是根节点,那么$v$有一个树边作为入边。我们要证明的是,除了这条树边,$v$没有其他额外的边,这使得$T_2$变换能够继续进行。 假设$G$包含循环边$(u,v)$,那么所有的$x\in P(v)$已经被合并了。因为从代码中,我们可以知道$\text{HIGHTPT}(x)\geq v$,而另一方面由于$\text{HIGHTPT}(v)<v$(对于所有节点无条件成立)。故$\text{HIGHTPT}(x)>\text{HIGHTPT}(v)$。 假设$G$包含前向边$(u,v)$,那么所有的$u\xrightarrow{*}v$上路径除了$v$的节点$x$不晚于$v$被合并了。因为$\text{HIGHTPT}(x)\geq\text{HIGHTPT}(v)$(如何证明?,感觉上当$\text{HIGHTPT}(v)$被赋值时,所有逆着前向边、树边、交叉边能遍历到且没标上$\text{HIGHTPT}$的节点都会被标上同样的$\text{HIGHTPT}$)且$\text{SNUMBER}(x)\leq\text{SNUMBER}(v)$。 假设$G$包含交叉边$(u,v)$,那么令$w$是$u$和$v$的最近公共祖先。我们要证明$w\xrightarrow{*}u$和$w\xrightarrow{*}v$上路径除了$v$的节点$x$不晚于$v$被合并了。证明思路同前向边,这里用到了$\text{SNUMBER}$对于交叉边的性质。 Tarjan, Robert. “Testing flow graph reducibility.” Proceedings of the fifth annual ACM symposium on Theory of computing. 1973. ↩︎

2021/4/27
articleCard.readMore

从CFG直接构建GSA的算法

本文来源于这篇Efficient building and placing of gating functions论文。该论文提供了一种算法,能够直接从CFG(控制流图)构建GSA(Gated单一赋值)形式。而之前的方法需要先插入phi节点即转换成SSA(静态单一赋值)形式,再进行构建。其核心思想是借助了他们提出的gating path这一概念。 内容主要来自这篇1论文。 背景及相关知识 支配 摘自“如何构建SSA形式的CFG”。 支配:$x$支配$y \Leftrightarrow$ 从起始节点到$y$的每条路径都经过了$x$,记为$x\underline{\gg}y$;从定义来说$\forall x, x$支配$x$;这是一个偏序关系(满足自反、传递)。 严格支配:$x$严格支配$y \Leftrightarrow x$支配$y \land x \neq y$,记为$x\gg y$;如果$x$不严格支配$y$,则记为$x\rlap{\hspace{.5em}/}\gg y$。 支配边界:$y \in x$的支配边界$\Leftrightarrow x$支配了$y$的前驱节点,但$d$没有严格支配$y$;从定义来说$x$的支配边界可能包含$x$自己;直观理解支配边界就是支配从有到无的界线。记$DF(X)$为节点$X$的支配边界。$DF$也可以定义在集合$\mathcal{X}$上,$DF(\mathcal{X})=\bigcup_{x\in \mathcal{X}}DF(X)$。 $$DF(X) = {Y|\exists P\in Pred(Y)(X\underline{\gg}P\land X\rlap{\hspace{.6em}|}\gg Y)}$$ 立即支配者:$x$是$y$的立即支配者$\Leftrightarrow x$严格支配$y$且$\forall z$严格支配$y$,$x$不严格支配$z$;我们会用idom来表示立即支配者;直观理解$y$的idom就是离$y$最接近的严格支配$y$的节点;一个节点的idom是唯一的。 支配者树:每个节点的立即支配者组成了一棵树(支配的偏序确保是有向无环的,idom的唯一进而确保是棵树)。 注意支配的概念是对于一个有起始节点的有向图的。在CFG中,支配者树的根是Entry。 可归约性与循环 可归约性 首先介绍两种变换,称为“归约”: T1变换:移除一个节点的自环; T2变换:对于一个有唯一前继$p$的节点$n$(可能有重边),移除$n$并让其后继成为$p$的后继。 可归约有很多等价的定义,现在给出其中的两种: CFG中的边可以分为两个不重叠的集合:前向边和回边,并满足: 前向边组成了DAG,且都能从入口节点到达; 回边的终止节点支配了起始节点。 反复使用T1和T2变换能将CFG转换为单一节点。 不可归约图主要是存在强连通分量有多个入口的情况。在结构化编程、不使用goto语句的情况下,创造出的CFG都是可归约图。 循环特指那些只有单一入口的强连通分量。在可归约图中识别循环将变得很简单,其切入点是回边检测:那些从被支配节点到支配节点的边就是回边,对应的支配节点是循环头,被支配节点是循环尾。下一小节会有更多关于循环的定义。 可归约图除了在识别循环上有帮助外,还能极大地简化程序分析复杂性。 对于不可归约图,可以通过复制分裂节点,转换成可归约图。 可归约图的逆图不一定是可归约图。 循环 以下是关于循环的一些概念,注意循环通常是可归约图的概念: 循环:CFG上有唯一入口节点(循环头)的强连通子图; 循环入边:起点在循环外,终点在循环内; 循环出边:起点在循环内,终点在循环外; 循环头:循环入边的终点。支配循环内所有节点; 回边:终点是循环头,起点在循环内; 某回边的自然循环:被循环头支配,并能到达该回边的节点集合; 循环尾:回边的起点; 循环出口:循环出边的终点; 嵌套循环:循环头在另一循环内的的循环。 对于两个不同的自然循环,它们可能有3种关系: 互相分离:没有任何节点共享; 互相嵌套:一个的循环头严格支配另一个循环头; 共享循环头:循环头是共享的。 对于第3种情况,可以添加公共的循环尾合并为一个循环。进而循环只剩下两种关系:分离,嵌套。 SSA(静态单一赋值)形式 通过对CFG进行转化,确保每个变量只有一次定义(即一处赋值),可以极大地简化程序分析,这种转化的结果就是SSA形式。顺序执行的代码只需要给变量添加版本号,就能转化为SSA形式。而在CFG的交汇节点处,如果流入同一变量的不同版本,就需要插入一个选择函数来确保单一赋值。这种函数称为$\phi$函数,形如$x_{n+1}=\phi(x_1,x_2,\dots,x_n)$。它位于CFG节点的起始处(如果有多个变量的话,可能有多个$\phi$函数)。它的参数是那些流入的不同版本。语义上,根据控制流是从哪条边流入,$\phi$函数会返回对应的变量版本。 Cytron2给出了一个高效的转化SSA形式的算法。为了理解这篇论文的理论基础,首先定义支配边界闭包的$DF^+(\mathcal{X})$为下列序列的极限: $$\begin{cases} DF_1=DF(\mathcal{X})\newline DF_{i+1}=DF(\mathcal{X}\cup DF_i) \end{cases}$$ 也就是说$DF^+(\mathcal{X})$是$\mathcal{X}$的支配边界,并上支配边界的支配边界,等等。然后这篇论文给出了最重要的定理:令$\mathcal{X}$为某个变量所有赋值语句出现过的CFG节点集合,$DF^+(\mathcal{X})$是赋值语句交汇的点,也就是最少的需要为该变量插入$\phi$函数的地方。依据这个定理,Cytron给出了两步算法,先是放置$\phi$函数,再重命名变量(给变量加上版本号)。 GSA(Gated单一赋值)形式 虽然SSA形式的CFG更清楚的显示了def-use链,但它仍属于控制流的一种表示。Ottenstein3提出了GSA,它是一种更加易于分析的值流表示。在此工作基础上,Havlak4进一步简化了GSA为TGSA(T是Thinned,意为简化 )。在这篇论文里,他们给出的TGSA构造方法是以SSA结果为输入的。而本文则试图给出一个不依赖于SSA结果的算法。这里给出TGSA向传统SSA添加并用以替代$\phi$函数的3种新gating函数。 $\gamma$函数,含有谓词的合并 V1 := ... if (P) then V2 := ... endif V3 := phi(V1, V2) $$\Rightarrow$$ V1 = ... if (P) then V2 := ... endif V3 := gamma(P, V2, V1) 在SSA的基础上,用$\gamma$函数替换$\phi$函数 $\gamma$试图表示条件分支语句的合并,它用于替换除循环头起始处的$\phi$函数。$\gamma$函数有如下的形式$V_3 := \gamma(P,V_1,V_2)$。含义是,如果$P$(谓词输入)为真,$V$的取值为$V_2$否则为$V_1$($V_1$、$V_2$统称为值输入)。注意到,$\phi$函数是依据流入控制流选取多个值,而$\gamma$则是根据条件选取多个值,后者便于值流分析。$\gamma$函数很容易被扩展为多分支用于switch,类似于$\gamma(P,V_1,V_2,\dots,V_n)$。 $\gamma$函数可能嵌套,构成DAG(嵌套表达式在合并公共子表达式后,就构成了DAG)。该合并节点CFG上的立即支配者为DAG的根$\gamma$函数提供谓词输入。而从DAG的根到末端经过的$\gamma$函数的顺序,应当与从合并节点CFG上的立即支配者到合并节点的前向CFG路径(除去循环回边的CFG)经过的分支顺序一致。 某些情况下,若某个分支条件不可能发生,用$\top$表示(译注:这里这个符号很困惑,它明明是底类型$\bot$)。这个记号和后文中$\varnothing$具有相同的意义,只是来源于不同的文章。 一个嵌套$\gamma$函数,并包含不可能发生条件$\top$的示例 考虑上图中的例子,在$A_3$处,将$\phi$函数替换为$\gamma$函数,结果应该为$\gamma(P,\gamma(Q,A_1,\top),A_2)$。 $\mu$函数,循环合并 $\mu$函数被插入在循环头起始处,并有如下的形式$V’:=\mu(V_{init},V_{iter})$。其中$V_{init}$是从循环外获得的初始输入,$V_{iter}$是从循环内回边获得的迭代输入。如果有多条循环外入边或有多条回边,相应的$V_{init}$和$V_{iter}$可能是一个$\gamma$ DAG。语义上,$\mu$函数产生了一个无穷的序列。而这个序列中的符合条件的第一个元素会被循环出口处的$\eta$函数选取。 一个循环头可能会被若干自然循环共享,即嵌套,或者除循环头外不共享节点。后者可以通过一个post-body节点合并多个回边化为一个自然循环。无论何种情况,$V_{iter}$应该是考虑了所有循环路径的结果。 这里使用了TGSA而不是GSA论文中的定义。 不可归约的CFG可能会有多个循环头。本文假定CFG都是可归约的,许多算法都是建立在这个假设上的。 TODO: 如何处理不可归约图?可能需要参照Tarjan的path sequence。 $\eta$函数,循环值选取 $\eta$函数被插入在循环出口的尾部,用于从$\mu$函数产生出来的序列中,选取符合某一条件的第一个值,其形式为$V’:=\eta(P,V_{final})$。其中$V_{final}$是一个由$\mu$函数产生的序列。而$P$则是断言,如果$P$依赖于循环内的变量,那么它也是序列。语义上,当$P$为真时,$V_{final}$的值被选取并赋值给$V’$。与$P$和$V_{final}$不同$V’$是序列中的一个值。 值得注意的是,与$\gamma$和$\mu$不同,$\eta$不表示合并。因此$\eta$的$P$和$V_{final}$一般不是$\gamma$ DAG。当然,如果从循环头到基本块有多个包含赋值的路径的话,那么$\eta$所处基本块的开始处会有$\gamma$节点。其插入的位置也有所不同。$\gamma$和$\mu$与$\phi$类似,都位于基本块的开头,而$\eta$插入在基本块的尾部。 此外,即使是在可归约图中,一个循环也可能存在多个循环出口。循环出口可能与循环头是同一个基本块(while),也有可能不同(do-while)。 A = 0; do { if (P) A = A + 1; else A = A + 2; } while (Q); write(A); $$\Rightarrow$$ 一个循环头与出口不位于同一基本块的循环插入$\mu$和$\eta$函数 讨论: 如何构建$\eta$函数? 以下内容是个人不成熟的想法,仅供参考。 上述的描述与论文中一笔带过的$\eta$函数有较大的差别。论文给出的算法无法处理循环出口与循环头不是同一个基本块的情况。论文中的$\eta$函数被放置在了循环头。如何构建$\eta$函数还需要进一步讨论。但进过一些思考,放置在循环出口更加合理。现在问题的难点在于: 循环出口:快速地从归约图和/或支配着树中找到循环出口,思路是识别出回边之后,反向遍历直到遇到循环头收集节点。而后找到这些节点集合不是到达本身集合中的出边。 重命名:论文并没有给出重命名这步的细节。重命名总是假设使用的变量来自最近一条该变量的定义语句。在循环出口末插入$\eta$节点会影响循环体内变量的重命名。解决方案是添加post-exit基本块在循环体出口和该出口的循环出边之间,并在其中放置$\eta$节点。 嵌套循环:一个节点可能是多个嵌套自然循环的出口,这时候我们可以插入多个包含$\eta$的基本块。这几个基本块串行。这种设计具有一致性,比如每个$\eta$都有唯一的$\mu$对应;每从$\mu$的$V_{init}$进入循环就$+1$,经过$\eta$就$-1$,就能记述循环深度。 最后,举个例子,$P\rightarrow{Clause_1,\dots,Clause_n}$表示$P$满足分支条件的析取;而$\gamma$函数采用了扩展表示,$\gamma(P,A_1,A_2,A_{default})$,表示$P$满足对应分支条件后,返回对应的值。 A = 0; l1: A = A + 1; l2: A = A + 2; switch (P) { case 1: goto l1; case 2: goto l2; default: // pass through } write(A); $$\Rightarrow$$ $$\Rightarrow$$ 一个包含`switch`和我所提出的$\eta$函数的例子。 算法思路 计算$\gamma$和$\mu$算法可以分为两部分,首先先放置$\gamma$,$\mu$函数并构造表达式,其中的变量没有标上版本号。之后再使用与SSA一样的重命名算法,给所有使用和被使用处的变量标上版本号。第2步算法在论文中并没有被提及,但可以参照Cytron2的论文。 $\gamma$和$\mu$就是原来$\phi$函数的变种,因而它们可以使用Cytron关于SSA的论断,即所有的$DF^+(\mathcal{X})$就是需要插入的位置($\mathcal{X}$为所有赋值语句的基本块)。但论文中并没有采取这个方法,他们找到了一种等价于$DF^+$的计算方法。相比于Cytron先计算$DF$,在用working list计算出$DF^+$的方法,这篇论文的方法更为直接。我们将在这一章讲解这篇论文提出的方法的理论基础。 但是$\gamma$和$\mu$表达式的内容就比较复杂了。它需要收集从某一个节点的支配者出发,到该节点所有路径组成的$\gamma$ DAG。论文采用了Tarjan5提出的用于解决图路径问题的通用算法“路径表达式”,我们将在这一节给出这个通用方案,以及这个方案是如何被运用到可归约图上的。 至于$\eta$函数,目前还需要探索。 路径表达式 核心思想是:使用正则表达式来表达一组路径具有同一起点和终点的路径,即路径表达式。路径表达式通过从最基本的元素(不可达、空路径和一个边)外加$\cup$(合并)、$\cdot$(拼接)和$*$(重复)构造出复杂的路径表达式,从而能表示一组复杂的路径。对于实际问题而言,通过赋予路径表达式不同的含义,并重新定义合并、拼接和重复运算,就能有通用的解法。如对于最短路问题,$x\cup y$相当于路径$\min\{x, y\}$,$x\cdot y$相当于$x + y$,$x^*$相当于$\begin{cases}0,&x\geq 0\newline-\infty,&x<0\end{cases}$。因而,绝大多数能划归为图论的问题,如最短路,线性方程组求解和控制流分析都能用路径表达式求解。本文所关注的$\gamma$和$\mu$函数参数的$\gamma$ DAG求解问题也能用这个通用的方案求解。问题的难点就在于如何快速、准确地构造出路径表达式。 路径表达式问题主要有两类: 单源路径表达式问题:求解从一个节点出发到其他所有节点的路径表达式; 任意两点路径表达式问题:求解任意节点到任意节点的路径表达式。 在接下来的文章中,我们主要关注可归约图的单源路径表达式问题。相比接下来所述的Tarjan的论文中给出了更多有趣的算法。他提出了路径序列作为快速计算路径表达式的方法,并给出了计算一般图、强连通分量分解图(特例DAG)、可归约图的路径序列方案。这些算法我还没来得及仔细看。下文只是一些相对简单,不涉及路径序列的概念和算法。 正则表达式 字母表$\Sigma$是一个既不包含$\Lambda$也不包含$\varnothing$的集合。$\Sigma$上的正则表达式是由以下规则组成的语言: “$\Lambda$"(空串)和”$\varnothing$"(空集合)以及对任意的$a\in\Sigma$,"$a$“都是原子正则表达式; 如果$R_1$和$R_2$是正则表达式,$(R_1\cup R_2)$(并)、$(R_1\cdot R_2)$(连接)以及$(R_1)^*$(重复)都是复合正则表达式。 我们使用$\sigma(R)$表示$R$对应的语言。具体计算规则可以递归定义,这里不再给出。 如果$\sigma(R_1)=\sigma(R_2)$,则正则表达式$R_1$和$R_2$被称为等价的; 如果$R=\varnothing$或者$R$不包含$\varnothing$,则正则表达式为简单的; 任何正则表达式都可以转化为简单的。可以重复使用以下的规则: 替换子表达式$\varnothing\cdot R_1$和$R_1\cdot\varnothing$为$\varnothing$; 替换子表达式$\varnothing\cup R_1$和$R_1\cup\varnothing$为$R_1$; 替换子表达式$\varnothing^*$为$\Lambda$。 如果正则表达式$R$能唯一地表示$\sigma(R)$中的每个字符串,则称$R$是不冗余的。也可以准确地递归定义不冗余,这里不再给出。 路径表达式(续) 令$G=(V,E)$为有向图。其中$G$上的任何路径都可以被认为是$E$上的字符串。一个类型为$(v,w)$的路径表达式$P$是$E$上的一个简单正则表达式,其中$\sigma(P)$中的所有路径都是从$v$到$w$的。 路径表达式$P$的类型也可以通过递归定义完成。这里注意到: $\cup$:要求左右子表达式有相同的类型; $\cdot$:要求左表达式的类型终点与右表达式的类型起点一样; $*$:要求子表达式类型终点起点一样。 路径表达式有什么用? 你可能会发现,路径表达式的存储也需要用到树状的结构。在路径表达式上计算最短路等问题并没有降低复杂度,所以路径表达式有什么用? 其实,如果我们找到计算路径表达式的方法,再重新定义原子表达式和复合运算符,就能完成通用的图论问题求解算法。具体例子见本节首。 可归约控制流图的路径表达式计算 回顾可归约图的一种定义:通过下面两种归约操作能转化为单一节点: T1:移除自环; T2:单一前继的节点合并进前继。 随着归约的进行,归约图上的节点会代表原图的一个子图,称为区域。归约图的边是原图的边,归约图的区域是原图节点的划分。对于每个区域,都有唯一的头,它是原图上所有进入区域的边的终点。你也可以认为头就是没有被合并进其他节点里的节点。 对图中的节点进行排序,构建一个归约序列$v_1,v_2,\dots,v_n$。使得依据这个序列可以通过下面的算法归约成单一节点: 对于$i=1\dots n-1$ 对$v_i$来回使用T1变换,去除自环; 使用T2变换将$v_i$合并进$v_j,j>i$。 给出下面定义,其中$r$是可归约图的入口: $header(v_i),v_i\neq r$:步骤1.2中$header(v_i)=v_j$,此外可以定义$\begin{cases}header^0(v)=v\newline header^1(v)=header(v)\newline header^i(v)=(header\circ header^{i-1})(v)\end{cases}$; $cycle(v_i),v_i\neq r$:步骤1.1中$v_i$消去的自环; $noncycle(v_i),v_i\neq r$:步骤1.2中删除的边。 有以下引理: 如果$v\neq r$,那么$header(v)>v$; 要么$h(e)=header(t(e))$(前向边),要么$h(e)\leq t(e)$(回边或自环),这里$h(e)$是边$e$的起始,$t(e)$是边$e$的终点; 如果$e\in cycle(t(e))$,那么存在$i\geq 0$满足$header^i(h(e))=t(e)$; 如果$e\in noncycle(t(e))$,那么对任意$i\geq 0$满足$header^i(h(e))\neq t(e)$,但存在$i\geq 0$满足$header^i(h(e))=header(t(e))$。 注意到可归约图的节点依据偏序关系最终形成了一棵树(一个节点只有一个$header$)。而支配者树也是一颗树。这两种树之间有怎么样的关联和差异? 暂时没有看到相关文献。但个人认为: 归约树不一定是支配者树。 可归约图的支配者树一定是一种归约树。如果想要将支配者树转换成排序好的序列,需要树上节点的直接孩子进行排序,方法是在原图上去除回边后对区域内节点进行拓扑排序。 不可归约图没有归约树(只有森林),但一定有支配者树。 其中第2点是后面算法的基础之一。 接下来,我们介绍可归约图构建单源路径表达式的方法。算法的输入是依据归约关系排序好的节点、每个节点的$header$、$cycle$和$noncycle$。 这个算法一边计算路径表达式,一边归约图。为了表示归约图,算法使用了一个森林,森林中每个节点$v$的父亲是$header(v)$。这样森林的每一个树都代表了区域,而区域的头则是树的根。森林中每个节点$v$都关联了一个不冗余的路径表达式$R(v)$,代表从$header(v)$到$v$的路径表达式。 算法通过以下四个函数操作树: 函数 描述 $INITIALIZE(v)$ 将$v$组成单一节点的一颗树,并另$R(v):=\Lambda$ $UPDATE(v,R)$ 如果$v$是根,那么$R(v):=R$ $LINK(v,w)$ 如果$v$和$w$是根,那么通过将$w$的父亲设为$v$合并树 $EVAL(v)$ 假设从$v$所属树的根$r$到$v$的树上路径为$r=v_0\rightarrow v_1\rightarrow\cdots\rightarrow v_k=v$,返回$R(v_0)\cdot R(v_1)\cdot\ldots\cdot R(v_k)$ 算法如下: 对于所有的节点$v$ $INITIALIZE(v)$ 对于$v:=1\dots n-1$: $P:=\varnothing$;$Q:=\varnothing$ 对于$e\in noncycle(v)$: $P:=[P\cup [EVAL(h(e))\cdot e]]$ 对于$e\in cycle(v)$: $Q:=[Q\cup [EVAL(h(e))\cdot e]]$ $UPDATE(v,[P\cdot[Q^*]])$ $LINK(header(v),v)$ $P(r,r):=\varnothing$* 对于$e\in cycle(r)$: $P(r,r):=[P(r,r)\cup[EVAL(h(e))\cdot e]]$ $P(r,r):=[P(r,r)^*]$ 对于$v:=1\dots n-1$ $P(r,v):=[P(r,r)\cdot EVAL(v)]$ 算法完成后,$P$就存放了从$r$到所有点的路径表达式。这里$[~\cdot~]$表示化简。 Gating Path 符号表 符号 含义 备注 $d\xrightarrow{+}v$ 从$d$到$v$的至少包含一条边的路径 可以是自环 $d\xrightarrow{*}v$ 从$d$到$v$的可空的路径 可以$d$就是$v$,路径无边 $d\rightarrow v$ 从$d$到$v$的有且只有一条边的路径   Gating Path的定义 定义 1 CFG中的节点$v$的gating path被定义为,从$idom(v)$到$v$的一条CFG路径,且该路径上的每个节点都被$idom(v)$支配。 用于演示gating path的CFG图 用于演示gating path的支配者树 gating path的示例,展示了某些节点的gating path 节点 gating path $b$ $a\rightarrow b$ $c$ $a\rightarrow c$ $d$ $a\rightarrow b\rightarrow d,~~a\rightarrow c\rightarrow d$ $e$ $d\rightarrow e$ Gating Path的存在性?存在 引理 1 对于任何CFG的路径$d\xrightarrow{+}v$,如果$d\gg v$且$d$在路径中出现了一次,那么$d$支配路径上的所有节点。 上述表述也就是路径除了起始节点,路径没有经过$d$。 引理 1 证明 反证法。如果路径上存在一个非$d$的节点$u$,满足$d\rlap{\hspace{.5em}/}\gg u$,那么路径$\text{Entry}\xrightarrow{*}u\xrightarrow{*}v$就没经过$d$,$v$不被$d$支配,矛盾。 推论 1 对任何节点$v$都存在gating path。 推论 1 证明 永远可以找到$idom(v)\xrightarrow{+}v$,移除这条路径上所有$idom(v)\xrightarrow{*}idom(v)$的环,就得到了符合引理1的路径。 支配边界闭包与支配树的兄弟子树的关系 接下来引理2和引理3将要论证:如果$v\in DF^+(X)$,那么$idom(v)\gg X$。从支配树的角度来说,$X$为$v$的兄弟子树上的某一个节点。 引理2和3演示用的CFG图 引理2和3演示用的支配者树 用于演示引理2和引理3的示例 节点 支配者树推断的可能支配闭包 实际支配闭包 2 7,exit 7,exit 3 7,exit 7,exit 4 5,6,7,exit 6,7,exit 6 4,5,7,exit 7,exit 注:这个论断可以用于寻找支配闭包包含$v$的那些节点$X$,也能寻找$X$的支配闭包可能包含的节点$v$。 接下来证明这个论断: 引理 2 如果$v\in DF(X)$,那么$idom(v)\gg X$。 引理 2 证明 反证法。假设$idom(v)\rlap{\hspace{.5em}/}\gg X$(不严格支配),由于$idom(v)$不是$X$,所以$idom(v)$不支配$X$。故存在路径$Entry\xrightarrow{+}X$避开了$idom(v)$。由于$v\in DF(X)$,所以存在$w\in Pred(v)$,使得$X\underline{\gg} w$。取路径$X\xrightarrow{*}w\rightarrow v$,移除路径中$X\xrightarrow{*}X$的环(使得$X$只出现在该路径的头)。此时由引理1,$X$支配$X\xrightarrow{*}v$上的所有节点,故$idom(X)$不在这条路径中(否则$X$将支配$idom(v)$和$v$)。将这条路径与$Entry\xrightarrow{+}X$,就得到了一条从$Entry$到$v$的避免$idom(v)$的路径,这与题设矛盾。 引理 3 如果$v\in DF^+(X)$,那么$idom(v)\gg X$。 引理 3 证明 数学归纳法。(初始情况)对于$DF^1(X)$,有引理2成立。(归纳)如果$v\in DF^{i-1}(X)$,那么$idom(v)\gg X$。任取$v\in DF(u),u\in DF^{i-1}(X)$。由归纳假设知$idom(u)\gg X$;由引理1,$idom(v)\gg u$,即$idom(v)\underline{\gg}idom(u)$。由传递性得$idom(v)\gg X$。 Gating Path与支配边界闭包的关系 首先,引理4和5论证支配边界(闭包)关系中可以得到一条特殊的gating path。 引理 4 如果$v\in DF(X)$,那么存在从$idom(v)$经过$X$到$v$的gating path。 引理 4 证明 由$DF$定义知道存在一个$w\in Pred(v)$,满足$X\underline{\gg}w$。又由推论1知,可以找到由gating path拼接的路径$X\xrightarrow{*}w$,满足路径上的每个节点都被$X$控制。由引理2知道$idom(v)\gg X$,类似地,又由推论1知,可以找到由gating path拼接的路径$idom(v)\gg X$,满足路径上的每个节点都被$idom(v)$控制。将这两条路径拼接成$idom(v)\xrightarrow{+}X\xrightarrow{*}w\rightarrow v$,注意到路径上的每个节点都被$idom(v)$控制,所以这是$v$的gating path。 引理 5 如果$v\in DF^+(X)$,那么存在从$idom(v)$经过$X$到$v$的gating path。 引理 5 证明 数学归纳法。 首先,引理6反过来,论证从一条特殊的gating path,可以得到支配边界闭包关系。 引理 6 如果存在一个$idom(v)$经过$X$到$v$的gating path,且$idom(v)\neq X$,那么$v\in DF^+(X)$。 $v$ $idom(v)$ $X$ 6 3 4,5 7 1 2,3,4,5,6(注意,7不是4、5的直接支配边界,而是闭包) 引理 6 证明 依据gating path的子路径$X\xrightarrow{*}w\rightarrow v$上的合并节点(入边数目大于1)个数进行数学归纳法。$v$一定是个合并节点,否则$w$将成为$idom(v)$且由题设$idom(v)\neq X$知$idom(v)\neq w$矛盾。令$X\xrightarrow{+}v$上合并节点的个数为$n$。 如果$n=1$,那么$v$是唯一的合并节点,路径$X\xrightarrow{*}w\rightarrow v$上的每个中间节点只有一个前导,故$X\underline{\gg}w$,$v\in DF(X)$。 假设命题对于$n<i$成立,当$n=i$时,令$u$是$X\xrightarrow{*}w\rightarrow v$上倒数第2个合并节点,令$P_u$为$idom(u)\xrightarrow{+}u$是该gating path的一个子路径(译注:由于$idom(v)$支配$u$,故$idom(u)$也在该gating path上,这里$P_u$应该是移除了环的路径,因而是gating path)。如果$X$在$P_u$中且$X\neq idom(u)$,那么由归纳假设,$u\in DF^{k<i}(X)$;同时,由于$v\in DF(u)$,故$v\in DF^{k+1}(X)$。如果$X=idom(u)$或者$X$不在$P_u$上,那么$X\underline{\gg}idom(u)\gg u\underline{\gg} w$(这步证明存疑,$X$似乎不一定支配$idom(u)$),因而$v\in DF(X)$。 使用引理5和6,我们得到了下面的定理: 定理 1 给定初始的CFG节点集合$\mathcal{X}$,对于CFG上的任意节点$v$,$v\in DF^+(\mathcal{X})$当且仅当存在一个gating path满足$idom(v)\xrightarrow{+}X\xrightarrow{+}v,X\in\mathcal{X}$。 这个证明就是将引理5和6整合了,并且将$DF^+$应用到了集合上。 引理6红线部分证明的瑕疵 用于演示引理6证明瑕疵的CFG 用于演示引理6证明瑕疵的支配者树 用于演示引理6证明瑕疵的示例 红色路径为gating path,此时$n=3$,且$X$不位于$P_u$上。 GSA构建算法 用Gating函数代表路径表达式 我们使用路径表达式来表达两节点之间路径集合,最终目的是找到立即支配者到合并节点的路径集合,后者就是要插入$\gamma$和$\mu$的地方。重新定义路径表达式的原子表达式和运算规则,使之运算的结果就是表示路径成立的$\gamma$函数。这样路径表达式计算完毕,就自然得到了$\gamma$函数。 原子路径表达式 首先,我们使用$\Lambda$表示路径可达,$\varnothing$表示路径不可达。 然后,我们考虑对于一条边,它的路径表达式: 如果是if (B)的then分支出边,该边的路径表达式为:$\gamma(B,\Lambda,\varnothing)$; 如果是if (B)的else分支出边,该边的路径表达式为:$\gamma(B,\varnothing,\Lambda)$; 如果是唯一出边,该边的路径表达式为:$\Lambda$; 这里,$\gamma(B,\Lambda,\varnothing)$可以被理解为,当$B$为真时,路径可达;当$B$为假时,路径不可达。else分支类似理解。更多分支可以通过定义$\gamma(B,e_1,e_2,\dots,e_n)$来表示,其中$n$是分支数。 复合路径表达式 接下来,我们就需要定义路径表达式的$\cup$,$~\cdot~$运算。$*$运算不定义,因为之后用不到,但其实它也无法定义,这是被$\gamma$函数无法表达循环这件事本身所限制住的(主要是因为$B$可能会变)。 并集 对于路径表达式$R_1$和$R_2$,定义它们的并: $$R_1\cup R_2:=\begin{cases} R_2, &\text{if}~R_1=\varnothing \newline R_1, &\text{if}~R_2=\varnothing \newline \gamma(B,(R_{1_t}\cup R_{2_t}),(R_{1_f}\cup R_{2_f})), &\text{if}~\begin{aligned}R_1=\gamma(B,R_{1_t},R_{1_f}) \newline R_2=\gamma(B,R_{2_t},R_{2_f})\end{aligned} \newline \end{cases}$$ 前两个条件很容易理解,如果两个路径集合取并集,其中一个是不可达,留下另一个就可以了。关于最后一个情况,有三个疑问。 最后一条情况一定被满足么?是的,可以归纳证明类型相同的路径表达式,如果包含$\gamma$,其条件一定是相同的。 语义上如何理解?$B$为真的时候,两种情况中真的路径求并就可以了;else分支类似。 类型一致么?是的,可以归纳证明$R_1$、$R_{1_t}$、$R_{1_f}$以及$R_2$等6个路径表达式是同一类型,所以子表达式中的$\cup$没问题。 拼接 对于路径表达式$R_1$和$R_2$,定义它们的拼接: $$R_1\cdot R_2:=\begin{cases} \varnothing, &\text{if}~R_1=\varnothing~\text{or}~R_2=\varnothing \newline R_2, &\text{if}~R_1=\Lambda \newline R_1, &\text{if}~R_2=\Lambda \newline \gamma(B,(R_{1_t}\cdot R_2),(R_{1_f}\cdot R_2)), &\text{if}~R_1=\gamma(B,R_{1_t},R_{1_f}) \newline \end{cases}$$ 这里就讲解语义上的理解:前三条很直白,最后一条也就是$R_2$分配到then和else分支。 从拼接的构造过程,我们就能得到$\gamma$函数,含有谓词的合并一节中的结论:(不考虑循环回边)在合并节点处的路径表达式的根$\gamma$函数的谓词输入就是合并节点的立即支配者的条件;其值输入则是(如果有)嵌套的$\gamma$函数,从外到里则与立即支配者到合并节点的路径对应。 需要注意到的是,反复运用合并和拼接后,路径表达式只有嵌套的$\gamma$、$\Lambda$和$\varnothing$组成(不包含$~\cdot~$和$\cup$)。 例子 1 2 3 4 5 if (P) then A := 1 else A := 2 endif 这里,我们把endif视作一个基本块,并用行号来表示CFG节点。我们的目标是求出立即支配者1(if)到被支配者5(endif)的路径表达式。 首先给出原子路径表达式: 边 原子路径表达式 $1\rightarrow 2$ $\gamma(P,\Lambda,\varnothing)$ $1\rightarrow 4$ $\gamma(P,\varnothing,\Lambda)$ $2\rightarrow 5$ $\Lambda$ $4\rightarrow 5$ $\Lambda$ 再给出复合路径表达式: 路径 路径表达式 $1\rightarrow 2\rightarrow 5$ $\gamma(P,\Lambda,\varnothing)\cdot\Lambda=\gamma(P,\Lambda,\varnothing)$ $1\rightarrow 4\rightarrow 5$ $\gamma(P,\varnothing,\Lambda)\cdot\Lambda=\gamma(P,\varnothing,\Lambda)$ $\bigcup 1\xrightarrow{*}5$ $\gamma(P,\Lambda,\varnothing)\cup\gamma(P,\varnothing,\Lambda)=\gamma(P,\Lambda,\Lambda)$ 那么对于复杂的可归约图,求解路径表达式就需要按照一个顺序。其计算方法就应当参见先前说的Tarjan的算法。 有些人就有疑问了,对于绝大多数情况,路径表达式的值输入都是$\Lambda$,没有什么意义。其实,这里还缺最后一步,就是在合并节点处,依据流入边的顺序对$\Lambda$进行标号。这一步有点类似$\phi$函数的标号,方便之后重命名标上版本号(注意标号与版本号的区别)。 为什么给$\Lambda$标号?因为在消除死代码后,合并节点的每个$\Lambda$都代表着一种可能流入的值(假定entry包含对所有变量的默认初始化)。 标号 标号是很简单的操作。在合并节点求路径表达式时,会把所有流入的路径取个并。在取并之前,依据流入路径的顺序给$\Lambda$标号即可。 例如上例中,计算$\bigcup 1\xrightarrow{*}5$时,会合并两个路径,给它们标上号: $*\xrightarrow{*}2\rightarrow 5$:$\gamma(P,\Lambda^1,\varnothing)$; $*\xrightarrow{*}4\rightarrow 5$:$\gamma(P,\varnothing,\Lambda^2)$; 合并之后,就有最终的计算结果:$\gamma(P,\Lambda^1,\Lambda^2)$。经过重命名的步骤后,就能得到想要的gating函数了。再次强调这里的上标不是数据流上的版本号,而是控制流的入边,切勿搞混。 有时候,类似$\gamma(P,\Lambda,\Lambda)$并没有出现在我们感兴趣的合并节点上,这时候可直接改为$\Lambda$。对于不存在死循环的CFG,另合并节点为$u$,一般发生在$u=idom_{post}(idom_{pre}(u))$。这里通过下标区别前向立即支配者和反向立即支配者。 算法 这个算法输出以下3个数据结构: $\Phi::Set\langle CFGNode\rangle$:需要放置$\gamma$或$\mu$函数的CFG节点; $GP::Map\langle CFGNode,PathExpression\rangle$:从立即支配者到该节点的路径表达式(不考虑循环,即所有gating path); $G^*::Map\langle CFGNode,PathExpression\rangle$:对于循环头,从该节点出发沿循环体回到该节点的路径表达式(如果有共享循环头的多个自然循环,这些自然循环全都会被考虑);其他情况为$\varnothing$。 算法的输出是:对任何$v\in\Phi$,如果$G^*[v]=\varnothing$,那么就在$v$处插入$\gamma$函数,其值即为$GP[v]$;否则插入$\mu$函数,其值为$\mu(GP[v],G^*[v])$。 算法还会用到临时变量: $ListP::Array\langle(e = (w,v)::CFGEdge,p::PathExpression)\rangle$:数组的元素是二元组,其中$e$是边,以$w$为起点,$v$为终点。我们用$subroot(w)$代表一个CFG节点,在支配树上它即是$w$的祖先,又是$v$本身或$v$的兄弟节点。$p$是路径$subroot(w)\xrightarrow{*}w\rightarrow v$的路径表达式。之所以在这里引入一个临时数组,是为了分两次遍历,以确保遍历的顺序。 接下来,我给出算法的伪代码,算法的输入是包含某变量赋值节点的集合$\mathcal{X}::Set\langle CFGNode\rangle$: 初始化:对每个$v\in N$($N$是CFG节点的集合): $\Phi[v]\leftarrow false$ $GP[v]\leftarrow\varnothing$ $G^*[v]\leftarrow\varnothing$ 对每个$u\in N$,以支配者树后序遍历(前序遍历的逆也行)的顺序遍历: $ListP = [~]$ 对于$v\in children(u)$: 对于每个$e=(w,v)\in E$($E$是CFG边的集合) 如果$w=u$,那么: $GP[v]\leftarrow GP[v]\cup(e)$ 否则: $(\phi,p)\leftarrow EVAL(e)$ 如果$\phi$为真,$\Phi\leftarrow\Phi\cup\{v\}$ $ListP\leftarrow[\dots ListP,(e,,p)]$ 对$children(u)$拓扑排序 对于$v\in children(u)$,以拓扑顺序遍历: 对于每个$(e=(w,v),p)\in ListP$: 如果$subroot(w)=v$: $G^*[v]\leftarrow G^*[v]\cup p$ 否则: $GP[v]\leftarrow GP[v]\cup (GP[subroot(w)]\cdot p)$ 如果$subroot(w)\in\Phi$为真,$\Phi\leftarrow\Phi\cup\{v\}$ $UPDATE(v,GP[v])$ 红色的部分原论文似乎是错放到了内层循环中,这里已经更改。 接下来我们给出这里用到的3个函数,注意$EVAL()$的含义是有变化的。$UPDATE()$函数额外包含了一个简化的过程。由于支配者树是一种特殊的归约树,我们不必引入$LINK()$。 函数 描述 $EVAL(e)$ 令$e=(w,b)$,假设从$subroot(w)$到$v$的树上路径为$r=w_0=subroot(w)\rightarrow w_1\rightarrow\cdots\rightarrow w_k=w$,返回值的$p$为$(R(r)\cdot R(w_1)\cdot\ldots\cdot R(w)\cdot e)$,返回值的$\phi$在$\vee_{i=0}^k(w_i\in\Phi\lor w_i\in\mathcal{X})$时为真,否则为假 $UPDATE(v,R)$ $R(v):=R$,这里$R$中$\gamma$函数的值输入全部为$\Lambda$时,$\gamma$函数会被替换成$\Lambda$,因为之后的标号将给值输入同样的标号,没必要使用$\gamma$函数 算法解释 以后序遍历或前序遍历的逆的顺序遍历支配者树,可以确保孩子在父亲节点遍历之前被遍历到。对于$e=(w,v)\in E$,可以分为以下几种情况。 $e$来自$v$的立即支配者(即$w=u=idom(v)$),那么这条边就是$v$的gating path,将其直接存入$GP[v]$; 其他情况下一定会有$u\gg w$,也就是$w$在$u$支配子树中处在非根的位置,这时有两种情况: $e$来自$v$支配树上的兄弟节点的子树($subroot(w)\neq v$),即$e\in cycle(v)$。此时路径$u\xrightarrow{+}subroot(w)\xrightarrow{*} w\rightarrow v$是$v$的gating path,应当被并到$GP[v]$中。 $e$来自$v$支配子树($subroot(w)=v$),即$e\in cycle(v)$。此时路径$v=subroot(w)\xrightarrow{*} w\rightarrow v$是循环,应当被并到$G^*[v]$中。 注意到2.1情形,$u\xrightarrow{+}subroot(w)$这个子路径需要引用到正在计算中的其他$GP[v],v\in children(u)$。所以这就需要引入拓扑排序。而$subroot(w)\xrightarrow{*} w\rightarrow v$部分的路径则是使用Tarjan的算法由$EVAL()$给出的。 拓扑排序细节 拓扑排序是在以$children(u)$为节点,以这些节点对应的子图之间的边为边的图上进行的。解释一下这样做的原因。在把所有的$v\in children(u)$对应的子图归约成一个点后,可能存在3类边: $v\rightarrow u$这就是未来的$cycle(u)$,这种边是不会影响当前处理的先后顺序的 $v$离开$u$控制区域的边,未来某些节点的$noncycle(\dots)$。同样这种边是不会影响顺序的 $v$到其他$children(u)$子图的边,这类边的前驱需要先算其$GP$,而后继计算的时候就需要引用前驱的$GP$,这类边决定了运算的顺序。 这个拓扑排序的顺序,其实就是前面的归约序列顺序。 标号细节 其实所有$GP[v]\leftarrow GP[v]\cup(\dots)$的地方,就需要对$\dots$进行标号,也就是在算法2.2.1.1.1和2.4.1.2.2都需要标号。 算法的拆解 这个算法其实是可以被拆解的成3部分: 归约序列的计算:这部分采用了在支配者树上进行拓扑排序的算法来做的。这部分可以被替换。 路径表达式的计算:计算$GP$和$G^*$,这两个都是不依赖于输入$\mathcal{X}$的,是图的内在性质,因而对于多个变量,这部分是不需要重复计算的。 $\gamma$和$\mu$插入位置的计算:这部分是原论文gating path性质的实践,可以在计算路径表达式的同时计算出来。实际上这部分算法可以被替换成Cytron的算法。对于多个变量,这部分是需要重复计算的,之后的重命名算法也需要重复计算。 完整的例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 read(A) if (P) then goto 5 if (Q) then A := 5 while (R) do A := A + 1 enddo else if (T) then A := A * 3 else A := A + 6 endif endif write(A) 用于演示的CFG 用于演示的支配者树,那些在CFG中有但支配者树没的边,用虚线添加了进来 算法的完整示例 loop: $u=10$ derive:$v$ edge:$(w,v)$ 影响 $11$ $(10,11)$ $GP(11)=\gamma(T,\Lambda,\varnothing)$ $12$ $(10,12)$ $GP(12)=\gamma(T,\varnothing,\Lambda)$ $13$ $(11,13)$ $subroot(w)=11,~~p(subroot(w),v)=\Lambda^1,~~\phi=\texttt{true},~~\Phi(13)=\texttt{true},~~ListP(13)={(11,13,\Lambda^1)}$ ^ $(12,13)$ $subroot(w)=12,~~p(subroot(w),v)=\Lambda^2,~~\phi=\texttt{true},~~\Phi(13)=\texttt{true},~~ListP(13)={(11,13,\Lambda^1),(12,13,\Lambda^2)}$ sequence: $11\rightarrow 13,12\rightarrow 13\Rightarrow11,12,13$ merge:$v$ $subroot(w),v,p(subroot(w),v)$ 影响 $13$ $11,13,\Lambda^1$ $GP(13)=\gamma(T,\Lambda^1,\varnothing)$ ^ $12,13,\Lambda^2$ $GP(13)=\gamma(T,\Lambda^1,\Lambda^2)$ 此时树的样子为: $u=10$时构建的树。 loop: $u=8$ derive:$v$ edge:$(w,v)$ 影响 $9$ $(8,9)$ $GP(9)=\gamma(Q,\Lambda,\varnothing)$ $10$ $(8,10)$ $GP(10)=\gamma(Q,\varnothing,\Lambda)$ $u=8$时构建的树。 loop: $u=6$ derive:$v$ edge:$(w,v)$ 影响 $7$ $(6,7)$ $GP(7)=\Lambda$ $u=6$时构建的树。 loop: $u=4$ derive:$v$ edge:$(w,v)$ 影响 $5$ $(4,5)$ $GP(5)=\Lambda$ $u=4$时构建的树。 loop: $u=3$ derive:$v$ edge:$(w,v)$ 影响 $4$ $(3,4)$ $GP(4)=\gamma(R,\Lambda,\varnothing)$ $u=3$时构建的树。 loop: $u=2$ derive:$v$ edge:$(w,v)$ 影响 $3$ $(2,3)$ $GP(3)=\gamma(P,\Lambda^1,\varnothing)$ ^ $(5,3)$ $subroot(w)=3,~~p(subroot(w),v)=\gamma(R,\Lambda^2,\varnothing),~~\phi=\texttt{true},~~\Phi(3)=\texttt{true},~~ListP(3)={(3,3,\gamma(R,\Lambda^2,\varnothing))}$ ^ $(9,3)$ $subroot(w)=8,~~p(subroot(w),v)=\gamma(Q,\Lambda^3,\varnothing),~~\phi=\texttt{true},~~\Phi(3)=\texttt{true},~~ListP(3)={(3,3,\gamma(R,\Lambda^2,\varnothing)),(8,3,\gamma(Q,\Lambda^3,\varnothing))}$ $6$ $(3,6)$ $subroot(w)=3,~~p(subroot(w),v)=\gamma(R,\varnothing,\Lambda^1),~~\phi=\texttt{true},~~\Phi(6)=\texttt{true},~~ListP(6)={(3,6,\gamma(R,\varnothing,\Lambda^1))}$ ^ $(13,6)$ $subroot(w)=8,~~p(subroot(w),v)=\gamma(Q,\varnothing,\Lambda^2),~~\phi=\texttt{true},~~\Phi(6)=\texttt{true},~~ListP(6)={(3,6,\gamma(R,\varnothing,\Lambda^1)),(8,6,\gamma(Q,\varnothing,\Lambda^2))}$ $8$ $(2,8)$ $GP(2)=\gamma(P,\varnothing,\Lambda)$ sequence: $8\rightarrow 6,8\rightarrow 3,3\rightarrow 6\Rightarrow 8,3,6$ merge:$v$ $subroot(w),v,p(subroot(w),v)$ 影响 $3$ $3,3,\gamma(R,\Lambda^2,\varnothing)$ $G^*(3)=\gamma(R,\Lambda^2,\varnothing)$ ^ $8,3,\gamma(Q,\Lambda^3,\varnothing)$ $GP(3)=\gamma(P,\Lambda^1,\gamma(Q,\Lambda^3,\varnothing))$ $6$ $3,6,\gamma(R,\varnothing,\Lambda^1)$ $GP(6)=\gamma(P,\gamma(R,\varnothing,\Lambda^1),\gamma(Q,\gamma(R,\varnothing,\Lambda^1),\varnothing)),\Phi(6)=\texttt{true}$ ^ $8,6,\gamma(Q,\varnothing,\Lambda^2)$ $GP(6)=\gamma(P,\gamma(R,\varnothing,\Lambda^1),\gamma(Q,\gamma(R,\varnothing,\Lambda^1),\Lambda^2))$ 原论文给的$GP(6)$似乎求错了。此时树的样子为: $u=2$时构建的树。 loop: $u=1$ derive:$v$ edge:$(w,v)$ 影响 $1$ $(1,2)$ $GP(2)=\Lambda$ $u=1$时构建的树。 .subfigures-centered { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; } .subfigures { display: flex; align-items: flex-end; justify-content: space-around; flex-wrap: wrap; } .subfigures-centered > figure, .subfigures > figure { margin: 0.5em; flex: 1 1 0; } P. Tu and D. Padua, “Efficient building and placing of gating functions,” in Proceedings of the ACM SIGPLAN 1995 conference on Programming language design and implementation - PLDI ’95, 1995. ↩︎ R. Cytron, J. Ferrante, B. K. Rosen, M. N. Wegman, and F. K. Zadeck, “Efficiently computing static single assignment form and the control dependence graph,” ACM Trans. Program. Lang. Syst., vol. 13, no. 4, pp. 451–490, 1991. ↩︎ ↩︎ K. J. Ottenstein, R. A. Ballance, and A. B. MacCabe, “The program dependence web: a representation supporting control-, data-, and demand-driven interpretation of imperative languages,” in Proceedings of the ACM SIGPLAN 1990 conference on Programming language design and implementation - PLDI ’90, 1990. ↩︎ P. Havlak, “Construction of thinned gated single-assignment form,” in Languages and Compilers for Parallel Computing, Berlin, Heidelberg: Springer Berlin Heidelberg, 1994, pp. 477–499. ↩︎ R. E. Tarjan, “Fast algorithms for solving path problems,” J. ACM, vol. 28, no. 3, pp. 594–614, 1981. ↩︎

2021/4/27
articleCard.readMore

如何构建SSA形式的CFG

这篇文章是关于如何从非SSA(静态单一赋值)形式的CFG(控制流图)构造出SSA形式的控制流图。这主要涉及到图论中的Dominator理论。难点在于phi函数的插入。 简介 SSA中的每个变量仅被定义一次。SSA形式的代码极大地降低了定义使用链的可能数目。在传统的非SSA形式的代码中,如果有$D$处定义和$U$处使用,就可能有$D\times U$种可能的组合。因而SSA形式的代码有利于程序的优化和分析。 顺序执行的代码SSA形式较为简单。但程序会有分支和合并,通过在合并处插入$\phi$函数,就能解决带分支代码的SSA形式。$\phi$函数表示从进来的分支中选取某一个值作为新的值。如下面的代码: if (p) v = 1; else v = 2; return v; 就会被转化成: if (p) v1 = 1; else v2 = 2; v3 = phi(v1, v2); return v3; 使用SSA形式中的一个分析例子是常量传播分析。常量传播分析是指分析哪些变量是常量,对于非SSA形式的分析,这较为困难。对于SSA形式,我们可以将那些使用常量定义的变量,将其所有出现的地方替换成常量,不断迭代直到到达不动点即可。 何处安放$\phi$函数 假设$V$在程序中只有一处赋值。那么$V$的值要么是程序开始处的$V_0$,要么是被赋值后的$V_1$(注:这里可能在原作者1眼中所有的变量都是在程序入口处有定义的,见控制流图(CFG))。假设$X$是给$V$赋值的基本块,那么对于$X$严格支配的基本块$Y$,它见到的值一定是$V_1$。如果控制流跑到了$Z$,而$Z$不被$X$严格支配,且$Z$是这个路径中的第一个,那么$Z$即可能从$X$看到$V_1$又可能从程序开始处看到$V_2$。$Z$被称为$X$的支配边界(dominance frontier),需要添加$\phi$函数。因此总的来说,我们可以寻找到给$V$赋值的基本块的所有支配边界,它们就是需要插入$\phi$函数的地方。 使用支配边界进行SSA计算的这个想法也适用于计算控制依赖。控制依赖可以确定语句执行的条件。 控制流图(CFG) 程序的语句可以被组织成基本块,控制流从基本块的第一个语句进入,到最后一条语句流出。CFG是一个有向图,其节点除了基本块外,还有Entry和Exit节点。Entry到程序的任何入口基本块会有一条边,程序的任何出口到基本块会有一条边。此外还有一条从Entry到Exit的边,原因之后解释。其他的边代表执行流的跳转。一个拥有多个后继的节点称为分支,一个拥有多个前驱的节点称为合并。每个节点在Entry节点都会有一个赋值,代表程序进入时它的值,这个赋值与其他赋值同等对待。 我们使用$p:X_0\xrightarrow{*}X_J$代表一般的路径(可空,长度$J$的路径包含$J+1$个节点和$J$个边),使用$p:X_0\xrightarrow{+}X_J$代表非空路径。 对于两个非空路径$p:X_0\xrightarrow{+}X_J$和$q:Y_0\xrightarrow{+}Y_K$,我们说它们交汇于节点$Z$如果: $$\begin{cases} X_0\neq Y_0 \newline X_J = Z = Y_K \newline (X_j = Y_k) \Rightarrow (j = J \lor k = K) \newline \end{cases}$$ 直觉来说,就是$p$和$q$从不同的节点出发,然后没有在中间交于相同的节点,只是在最后交于$Z$,然后其中有的边可能包含循环$Z\xrightarrow{+}Z$的路径。 静态单一赋值形式(SSA) 一个赋值语句$A$形如$LHS(A)\leftarrow RHS(A)$。其中$LHS$是一个互异的目标变量元组,而$RHS$是一个表达式元组,两个元组长度相等。语义上,$RHS$中的每一个表达式都赋值给了对应的$LHS$的目标变量。 将程序转换成SSA形式分为两步,首先,一些平凡的$\phi$函数被插入,形如$V\leftarrow\phi(V,V,\dots)$。第二步则是替换所有的$V$为新的变量$V_i$,这里被替换的$V$包括分支语句中出现的和赋值中出现的。因而,本文中,一个赋值可能是个普通赋值或者$\phi$赋值。 先前提到的$\phi$赋值有如下的形式$V\leftarrow\phi(R,S,\dots)$,其中$V,R,S,\dots$是变量,$\phi$赋值位于基本块的开始。右手边变量的个数应当与进入基本块的前驱数目相等,这里要求基本块的前驱以某种形式排序。如果控制流从第$j$个前驱流入,那么$V$的取值就是右手边第$j$个变量。 SSA形式可以被看作一个程序的性质,或者一个从不具备该性质的程序到具备的变换。作为变换,它要求新程序满足以下的性质,对于每一个原始程序的变量$V$: 如果$X\xrightarrow{+}Z$和$Y\xrightarrow{+}Z$交汇于节点$Z$,且$X$和$Y$包含了对$V$的赋值(原始程序中的),$\phi$赋值应当被插入到$Z$中(新程序); 每一个对$V$的使用(包括$\phi$函数)都被替换为$V_i$,使程序成为静态单一赋值; 沿着任何控制流路径,源程序中的$V$的取值于新程序中的$V_i$的取值必须一样。 最小SSA形式是指插入的$\phi$函数尽可能少。一些没有必要的$\phi$函数可能会影响程序的优化。另一种是修剪的SSA形式,它是指如果变量没有在交汇点$Z$中及之后使用,就删掉$\phi$语句。不过有时我们会需要在所有交汇的地方放置$\phi$函数,但本文的算法经过微小的改动就可以得到修剪的SSA形式。 其他的程序结构 对于数组,将数组元素视为变量会很不方便,因而可以引入两个函数$Access(A, i)$表示访问数组$A$的第$i$个元素,其返回值就是$A$的第$i$个元素的值,$Update(A, j, V)$表示修改数组$A$的第$j$个元素,将其值置为$V$,并返回新的数组$A$。所以对$A$某个元素的赋值相当于对整个数组$A$赋值。 结构体大体上可以看成是数组。 除此之外,可能存在到变量的隐式引用,比如全局变量被子过程的使用和改变、变量别名、解引用指针等等。对于语句$S$,3中类型的引用影响到了到SSA形式的转换: $MustMod(S)$:一定被$S$修改的变量集合; $MayMod(S)$:可能被$S$修改的变量集合; $MayUse(s)$:在$S$执行之前的值可能被$S$用到的变量集合。 将$S$转化为赋值语句$A$时,$MayMod(S)$中的所有变量应当出现在$LHS(A)$中,$MayUse(S)\cup(MayMod(S)-MustMod(S))$的所有变量出现在$RHS(A)$中(这部分我不太理解)。 对于堆内存的访问,将堆内存视为一整个变量对于大多优化算法足够。如果我们不能获取到函数体,那就要假定所有的全局变量及参数引用的对象会被改变,而调用者的局部变量不应假定为会改变。当然更细致的堆内存模型和别名分析是很有帮助的。更细致的分析可以减少副作用以及减少$LHS$和$RHS$元组的长度。 SSA算法概览 从CFG中构造出支配边界映射; 使用支配边界插入$\phi$函数; 重命名变量。 支配 支配者树 图论中的相关概念: 支配:$x$支配$y \Leftrightarrow$ 从起始节点到$y$的每条路径都经过了$x$,记为$x\underline{\gg}y$;从定义来说$\forall x, x$支配$x$;这是一个偏序关系(满足自反、传递)。 严格支配:$x$严格支配$y \Leftrightarrow x$支配$y \land x \neq y$,记为$x\gg y$;如果$x$不严格支配$y$,则记为$x\rlap{\hspace{.6em}|}\gg y$。 支配边界:$y \in x$的支配边界$\Leftrightarrow x$支配了$y$的前驱节点,但$d$没有严格支配$y$;从定义来说$x$的支配边界可能包含$x$自己;直观理解支配边界就是支配从有到无的界线。 立即支配者:$x$是$y$的立即支配者$\Leftrightarrow x$严格支配$y$且$\forall z$严格支配$y$,$x$不严格支配$z$;我们会用idom来表示立即支配者;直观理解$y$的idom就是离$y$最接近的严格支配$y$的节点;一个节点的idom是唯一的。 支配者树:每个节点的立即支配者组成了一棵树(支配的偏序确保是有向无环的,idom的唯一进而确保是棵树)。 注意支配的概念是对于一个有起始节点的有向图的。 在CFG中,支配者树的根是Entry,除了Entry外,其他节点都有idom。支配者树可以在$O(E\alpha(E,N))$的时间内给出,甚至可以用更复杂的算法在$O(E)$时间内给出。由于$\alpha(E,N)$很小,我们假定支配者树是线性时间内求解的。 考虑下面的图: 其支配者树如下,其中节点$X$的标签为: $$\begin{gather*}(DF_{up}(X))\newline X[DF_{local}(X)|DF(X)]\end{gather*}$$ 下文中,前驱$Pred$、后继$Succ$和路径这些名词是CFG上的,而父亲$Parent$、孩子$Children$、祖先、子孙这些名词是指支配者树的。关于支配者树的计算我将在稍后给出。 支配边界 首先我们给出支配边界$DF(X)$的形式化定义: $$DF(X) = \{Y|\exists P\in Pred(Y)(X\underline{\gg}P\land X\rlap{\hspace{.6em}|}\gg Y)\}$$ 直接依据定义计算支配边界会具有很高的复杂度(二次复杂度)。为了以线性于$\sum_X |DF(X)|$的速度计算支配边界,我们对每个节点定义两个中间的集合$DF_{local}$和$DF_{up}$,使得: $$\begin{equation}DF(X)=DF_{local}(X)\cup\bigcup_{Z\in Children(X)}DF_{up}(Z)\end{equation}$$ 对于任意节点$X$,一些$X$的后继可能会对$DF(X)$有贡献,这种贡献$DF_{local}(X)$被定义为: $$\begin{equation}DF_{local}(X)\stackrel{\text{def}}{=}\{Y\in Succ(X)|X\rlap{\hspace{.6em}|}\gg Y\}\end{equation}$$ 对于任意非Entry的节点$Z$,$DF(Z)$中的一些节点或许会对$DF(idom(Z))$,这种贡献$DF_{up}=(Z)$被定义为: $$\begin{equation}DF_{up}(Z)\stackrel{\text{def}}{=}\{Y\in DF(Z)|idom(Z)\rlap{\hspace{.6em}|}\gg Y\}\end{equation}$$ 引理1: 公式1是正确的。 引理1证明: 由于支配关系是自反的,所以公式2中,$X$支配自己,故而$DF_{local}(X)\subseteq DF(X)$。由于支配关系是传递的,所以公式3中的$Z$严格支配$Y$的前驱而$X=idom(Z)$,故而$DF_{up}(X)\subseteq DF(X)$。类似的,我们可以证明$X$的支配边界在其前驱为$X$的情况下在$DF_{local}(X)$中,否则在$DF_{up}(X)$中。 引理2: 对于任意节点$X$, $$DF_{local}(X)=\{Y\in Succ(X)|idom(Y)\neq X\}$$ 引理3: 对于任意节点$X$和它的任意孩子$Z$(支配树上的), $$\begin{equation}DF_{up}(Z)=\{Y\in DF(Z)|idom(Y)\neq X\}\end{equation}$$ 引理3证明: 推导公式4可以推导出公式3较为复杂。使用反证法。 于是就有了下方计算$DF(X)$的算法: 自底向上遍历支配者树上的每个节点$X$: $DF(X)\leftarrow\emptyset$ 对于每个$Y\in Succ(X)$: 如果$idom(Y)\neq X$,则$DF(X)\leftarrow DF(X)\cup\{Y\}$(计算$DF_{local}(X)$) 对于每个$Z\in Children(X)$: 对于每个$Y\in DF(Z)$: 如果$idom(Y)\neq X$,则$DF(X)\leftarrow DF(X)\cup\{Y\}$(计算$DF_{up}(X)$) $DF_{local}(X)$总的计算时间为$O(|E|)$($E$为CFG的边集),$DF_{up}(X)$总的计算时间正比于所有$DF$的大小和,最坏情况为$O(|N|^2)$($N$为CFG的顶点集),但通常而言,$DF_{up}(X)$的计算时间是线性的。 支配边界与合并的关系 对于CFG上的节点集合$S$,$J(S)$是它们的合并节点$Z$,也就是存在两个非空的CFG路径,从$S$中不同的两点出发,交汇在$Z$。而$J^+(S)$被定义为下列序列的极限(其实是闭包): $$\begin{cases} J_1=J(S)\newline J_{i+1}=J(S\cup J_i) \end{cases}$$ 特别的,如果$S$是某变量$V$的赋值节点集合,$J^+(S)$是$V$的$\phi$函数节点集合。 同时,我们定义节点集合上的$DF$: $$DF(S)=\bigcup_{x\in S}DF(X)$$ 同样就可以定义$DF^+(S)$为下列序列的极限: $$\begin{cases} DF_1=DF(S)\newline DF_{i+1}=DF(S\cup DF_i) \end{cases}$$ 这里只是给出一个定义,并不是最快的计算方法。 如果$S$是某变量$V$的赋值节点集合,我们会证明(这个定理依赖于$V$的定义在Entry): $$J^+(S)=DF^+(S)$$ 引理4: 对任意CFG中非空路径$p:X\xrightarrow{+}Z$,存在路径上的一个节点$X’\in\{X\}\cup DF^+(\{X\})$支配$Z$。除非$X$支配$p$的每个节点,$X’\in DF^{+}({X})$。 引理5: 对CFG中两个不同的节点$X,Y$,若有非空路径$p:X\xrightarrow{+}Z$和$q:Y\xrightarrow{+}Z$交汇于$Z$。那么$Z\in DF^+(\{X\})\cup DF^+(\{Y\})$。 引理5证明: 假设$X’$和$Y’$分别是引理4中$p$和$q$存在的节点。$X’$在$q$上时,即$X’=Z$,只需要考虑$Z=X$的情况,此时$Z\in DF(X)\subseteq DF^+(X)$。同理$Y’$在$p$上也成立。如果$X’$不在$q$上且$Y’$不在$p$上,则可以推导出$X’$和$Y’$支配$Z$,进而推导出$X’$支配$Y’$或$Y’$支配$X’$,与交汇定义矛盾(存在中间交点)。 引理6: 对于任意CFG节点集合$S$,$J(S)\subseteq DF^+(S)$。 引理7: 对于任意包含$Entry$的CFG节点集合$S$,$DF(S)\subseteq J(S)$。 定理: 对于任意包含$Entry$的CFG节点集合$S$,$DF^+(S)=J^+(S)$。 构造最小SSA形式 使用支配边界寻找ϕ函数需要的地方 接下来给出放置平凡$\phi$函数的算法,它需要用到以下3个数据结构: $Worklist: Queue\langle CFGNode\rangle$ $Visited: Map\langle CFGNode,bool\rangle$ $Placed: Map\langle CFGNode,bool\rangle$ 算法如下: 对于每个变量$V$: $Worklist\leftarrow V$的所有赋值节点 $Visited\leftarrow$全$false$ $Placed\leftarrow$全$false$ 如果$Worklist.empty()$为假 $X\leftarrow Worklist.pop()$ 对每个$Y\in DF(X)$,如果$Placed[Y]$为假 $Placed[Y]\leftarrow true$ 在$Y$处放置$\langle V\leftarrow\phi(V,\dots,V)\rangle$ 如果$Visited[Y]$为假 $Visited[Y]\leftarrow true$ $Worklist.push(Y)$ 这个算法的复杂度为$O(\sum_X(A_{tot}(X)\times |DF(X)|))$。这里$A_{tot}$是总的赋值数目(包括$\phi$),一般情况下,这个算法线性于$A_{tot}(X)$。 重命名 我们给出一个递归函数$Search(X: CFGNode)$,它有唯一的参数$X$是一个CFG节点。此外还有以下的“全局变量”作为上下文: $Stacks: Map\langle Variable, Stack\langle Integer\rangle\rangle$ $Counters: Map\langle Variable, Integer\rangle$ 首先: $Stacks\leftarrow$全为空栈 $Counters\leftarrow$全为0 调用$Search(Entry)$ $Search(X: CFGNode)$实现如下: 对于每个$A: Statement\in X$: 如果$A$是个普通赋值: 对每个$V: Variable\in RHS(A)$ 使用$V_i$替换$V$,其中$i$是$Stacks[V].top()$ 对每个$V: Variable\in LHS(A)$: 使用$V_i$替换$V$,其中$i$是$Counters[V]$ $Stacks[V].push(Counters[V])$ $Counters[V]+=1$ 对于每个$Y: CFGNode \in Succ(X)$: $j\leftarrow$基本块$X$到$Y$的边的序号 对每个$F:\phi$函数$\in Y$: 使用$V_i$替换$RHS(F)$的第$j$个操作数,其中$i$是$Stacks[V].top()$ 对于每个$Y: CFGNode \in Children(X)$: 调用$Search(Y)$ 对于每个$A: Statement in X$ 对每个$V: Variable\in oldLHS(A)$: $Stacks[V].pop()$ 控制依赖的构建 控制依赖是反向控制流图的支配边界。类似地我们定义反向支配和立即反向支配者等概念。 一个CFG节点$Y$被认为是控制依赖于$X$,如果满足下面两条: 存在一个非空路径$p:X\xrightarrow{+}Y$,使得$Y$反向支配$X$之后的$p$上所有节点。 $Y$没有严格反向支配$X$。 这等价于$X$的某条出边使得$Y$一定被执行,但也存在一些从$X$出发的路径$Y$不被执行。 $X,Y$是CFG节点,那么$Y$控制依赖于$X$,当且仅当在RCFG中$X\in DF(Y)$。因而计算$CD: MultiMap\langle CFGNode, CFGNode\rangle$控制依赖算法如下: 对每个CFG节点$Y$: 对每个$X\in RDF(Y)$: $CD.put(X, Y)$ 通过添加$Entry\rightarrow Exit$的边,控制依赖的根将成为$Entry$。 Cytron, R., Ferrante, J., Rosen, B. K., Wegman, M. N., & Zadeck, F. K. (1991). Efficiently computing static single assignment form and the control dependence graph. ACM Transactions on Programming Languages and Systems (TOPLAS), 13(4), 451-490. ↩︎

2021/4/27
articleCard.readMore

数值分析基础第1章:引论

本文的内容主要来自关治的《数值分析基础(第3版)》第1章《引论》和老师的PPT。 数值分析的研究对象 (略) 数值计算的误差 误差的来源与分类 误差大致分为4类: 输入数据的误差; 舍入误差:来源于计算机的数字是有限的; 截断误差:用有限的过程代替无限的过程,或用简化的问题代替不易计算的原问题; 误差在计算过程中的传播。 绝对误差和相对误差、有效数字 定义2.1 设$x$是精确值,$x_A$是一个近似值。$e(x)=|x-x_A|$称为$x_A$的绝对误差,而$e_r(x)=\frac{|x-x_A|}{x}$称为$x_A$的相对误差。 定义2.2 因为$x$通常是未知的,一般只能给出$|x-x_A|$的上界$\epsilon_A$。$|x-x_A|\leq\epsilon_A$称为$x_A$的绝对误差界,而$\epsilon_r(x)=\frac{\epsilon_A}{x}$称为$x_A$的相对误差界。 对于一个实数,取有限位数作为近似值,采用四舍五入的方法。这样得到的近似值,绝对误差界为最后数位的半个单位。因此引入了有效数字概念。 定义2.3 设$x_A$是$x$的一个近似值,$x_A=\pm0.a_1a_2\cdots a_i\cdots\times 10^k$,其中$k$为整数,$a_i\in{0,1,2,\cdots,9}$,$a_1\neq 0$。如果有$|x-x_A|\leq\frac{1}{2}\times 10^{k-n}$,则称$x_A$具有$n$位有效数字:$a_1,a_2,\cdots,a_n$。 有效数字就是寻找误差在$5$以内(包括$5$)的那位,计数之前数字的个数。 设$x$的近似值$x_A=\pm0.a_1a_2\cdots a_i\cdots\times 10^k$,则: 如果$x_A$有$n$位有效数字,则: $$\frac{|x-x_A|}{x_A}\leq\frac{1}{2a_1}\times 10^{1-n}$$ 如果: $$\frac{|x-x_A|}{x_A}\leq\frac{1}{2(a_1+1)}\times 10^{1-n}$$ 则$x_A$至少有$n$位有效数字。 1的证明比较简单,这里证明2,由$|x_A|=0.a_1a_2\cdots\times 10^k\leq0.(a_1+1)\times 10^k=(a_1+1)\times 10^{k-1}$可以证明。 求函数值和算术运算的误差估计 向前误差分析 设$x$的浮点数表示为$x^*$。则: $$\begin{aligned} x^*&=x(1+\delta)\\ (x^*)^2&=x^2(1+2\delta) \end{aligned}$$ 因而,每次乘法相对误差加倍。这里,$\delta^2$由于超出浮点数表示能力而被忽略。 向后误差分析 把计算结果的误差归结为原始数据经扰动之后的精确的计算结果的误差分析叫做后向误差分析。 设$y=f(x_1,x_2,\cdots,x_n)$,如果参量$x_i,1\leq i\leq n$有误差,则$y$也会有误差。若$x_i,1\leq i\leq n$的近似值为$\tilde{x}_i,1\leq i\leq n$,相应的解为$\tilde{y}$,则$\tilde{y}$的绝对误差和相对误差分别为: $$\begin{aligned} |y-\tilde{y}|&\leq\sum\limits_{i=1}^n\left|\frac{\partial f}{\partial x_i}\right||x_i-\tilde{x_i}|\\ \frac{|y-\tilde{y}|}{|y|}&\leq\sum\limits_{i=1}^n\left|\frac{x_i}{y}\frac{\partial f}{\partial x_i}\right|\frac{|x_i-\tilde{x_i}|}{|x_i|} \end{aligned}$$ 把两个数$a,b$的$+,-,\times,\div$看成二元函数,则有: $$\begin{aligned} e(a\pm b)&\leq e(a)+e(b)\\ e(ab)&\leq|b|e(a)+|a|e(b)\\ e_r(a\pm b)&\leq\frac{|a|e_r(a)+|b|e_r(b)}{|a\pm b|}\\ e_r(ab)&\leq e_r(a)+e_r(b)\\ e(\frac{a}{b})&\leq \frac{e(a)}{|b|}+\left|\frac{a}{b^2}\right|e(b)\\ e_r(\frac{a}{b})&\leq e_r(a)+e_r(b) \end{aligned}$$ 计算机的浮点数表示和舍入误差 在计算机上,实数系统$\mathbb{R}$是用浮点数系统$\mathbb{F}$来近似的。实数$x$在$\beta$进制浮点数系统下的表示为: $$x=(-1)^s\cdot(0.a_1a_2\cdots a_t)\cdot\beta^e=(-1)^s\cdot m\cdot \beta^{e-t}$$ 其中: $s$:符号位,$1$为负数,$0$为正数; $m=a_1a_2\cdots a_t,0\leq a_i\leq\beta-1$:尾数,$t$称为字长; $e,L\leq e\leq U$:指数。 二进制浮点数系统$\mathbf{F}$为: $$\mathbb{F}={\pm 0.d_1d_2\cdots d_t\times 2^k}\cup{0}$$ 其中$d_1=1$。 浮点数的近似$\mathrm{fl}(x)$,有两种: 截断; 舍入:IEEE属于这种。 定义$\epsilon_{mach}$为机器精度: $$\begin{aligned} \left|\frac{x-\mathrm{fl}(x)}{x}\right|&=\left|\frac{0.1d_2\cdots d_td_{t+1}\cdots\times 2^k-0.1d_2\cdots d_t\times 2^k}{0.1d_2\cdots\times 2^k}\right|\\ &\leq\left|\frac{0.d_{t+1}d_{t+2}\cdots\times 2^k}{0.1d_2\cdots\times 2^k}\right|\times 2^{-t} \leq 2^{-t}=\epsilon_{mach} \end{aligned}$$ 病态问题、数值稳定性与避免误差危害 病态问题与条件数 敏度分析是研究$x$有微小变化$\delta x$时,函数值$f$会发生多大的变化: $$\frac{|f(x+\delta x)-f(x)|}{f(x)}\leq\kappa(x)\frac{|\delta x|}{|x|}$$ 称$\kappa(x)$为$f$在$x$的条件数。 当$\kappa(x)$很大时,自变量的微小变化就可能引起函数值的巨大变化,则称$f$在$x$点是病态的;否则称$f$在$x$点是良态的。 一个计算问题是否病态是问题本身的固有属性。 $$\begin{aligned} \kappa(x)&=\lim_{x_A\to x}\frac{\left|\frac{f(x)-f(x_A)}{f(x)}\right|}{\left|\frac{x-x_A}{x}\right|}\\ &=\left|\frac{x}{f(x)}\right|\lim_{x_A\to x}\left|\frac{f(x)-f(x_A)}{x-x_A}\right|\\ &=\left|\frac{xf’(x)}{f(x)}\right| \end{aligned}$$ 例如:Wilkinson多项式$\prod\limits_{i=1}^n(x-i)$求根就属于病态问题,用特征值的定义求求解特征值同样是病态问题。 数值方法的稳定性 一个数值方法,如果初始数或计算过程某一不有微小的改变,由此引发的计算结果也只是微小的变化,则称该方法是数值稳定的,否则称为数值不稳定的。 一般来说,如果具有初始误差$\epsilon_0>0$,计算$n$步后的误差为$\epsilon_n$,若方法是数值稳定的,则存在与$n$无关的常数$C$使得$\epsilon_n\leq C\epsilon_0$。 $|\epsilon|\approx Cn\epsilon_0$,线性型的误差增长; $|\epsilon|\approx C^n\epsilon_0$,指数型的误差增长。$|C|\leq 1$稳定,$|C|>1$不稳定。 TODO: 给个例子。 避免误差危害 避免相近的数相减; 避免和绝对值大的数相乘; 避免和绝对值小的除数相除。 数学上等价$\neq$计算上等价:例如对于$ax^2+2bx+c=0$,采用$x_1=\frac{-b-\mathrm{sgn}(b)\sqrt{b^2-ac}}{a}, x_2=\frac{c}{ax_1}$,可以解决$b^2\gg|ac|$情况下的问题;使用$\frac{x^2}{\sqrt{1+x^2}+1}$替代$\sqrt{1+x^2}-1$可以避免$|x|\ll 1$时精度的损失。同时对于求解$n$次多项式,可以利用分配律合并多项式,达到$n$次乘法和$n$次加法。 选用公式也很重要,对于无穷级数毕竟求解的情况,通常选用同号数列求和极限比交替数列求和极限收敛速度更快。 线性代数的一些基本概念 矩阵乘法的定义 定义$\mathbb{F}^{m\times p}\times\mathbb{F}^{p\times n}=\mathbb{F}^{m \times n}$的一个运算,称为矩阵乘法,$\mathbf{A}\mathbf{B}=\mathbf{C}$,其中$c_{ij}=\sum\limits_{k=1}^p a_{ik}b_{kj}$。 矩阵迹的定义 设$\mathbf{A}=(a_{ij})_{i,j=1}^n\in\mathbf{F}^{n\times n}$,矩阵$\mathbf{A}$的迹是其所有对角元素之和。$\mathrm{tr}(\mathbf{A})=\sum\limits_{i=1}^na_{ii}$。 定理 若$\mathbf{A}\in\mathbb{F}^{n\times m},\mathbf{B}\in\mathbb{F}^{m\times n}$,则$\mathrm{tr}(\mathbf{A}\mathbf{B})=\mathrm{tr}(\mathbf{B}\mathbf{A})$。 证明: $$\mathrm{tr}(\mathbf{A}\mathbf{B})=\sum_{i=1}^n(\mathbf{A}\mathbf{B})_{ii}=\sum_{i=1}^n\sum_{j=1}^m\mathbf{A}_{ij}\mathbf{B}_{ji}=\sum_{j=1}^m\sum_{i=1}^n\mathbf{B}_{ji}\mathbf{A}_{ij}=\sum_{j=1}^m(\mathbf{A}\mathbf{B})_{jj}=\mathrm{tr}(\mathbf{B}\mathbf{A})$$ 非奇异矩阵的定义 矩阵$\mathbf{A}\in\mathbb{R}^{n\times n}$称为非奇异的或可逆的,若存在一个矩阵$\mathbf{B}\in\mathbb{R}^{n\times n}$,使得$\mathbf{A}\mathbf{B}=\mathbf{B}\mathbf{A}=\mathbf{I}$。其中$\mathbf{B}$称为$\mathbf{A}$的逆矩阵,如果$\mathbf{A}$的逆矩阵不存在,则称其是奇异的。 注:对于有结合律且有左右逆的代数系统中,左逆一定等于右逆。此外,$(\mathbf{A}_1\mathbf{A}_2\cdots\mathbf{A}_m)^{-1}=\mathbf{A}_m^{-1}\cdots\mathbf{A}_2^{-1}\mathbf{A}_1^{-1}$。 线性方程组的可解性定理 $\mathbf{A}\in\mathbb{R}^{n\times n}$可逆,则方程组$\mathbf{A}\vec{x}=\vec{b}$有唯一解$\vec{x}=\mathbf{A}^{-1}\vec{b}$。 $\mathbf{A}\in\mathbb{R}^{n\times n}$非奇异$\Leftrightarrow$齐次方程组$\mathbf{A}\vec{x}=\vec{0}$只有$\vec{0}$解(平凡解)$\Leftrightarrow$ $\det\mathbf{A}\neq 0$。 矩阵转置的定义 设$\mathbf{A}\in\mathbb{R}^{m\times n}$,它的转置$\mathbf{A}^T$可通过交换$\mathbf{A}$的行和列得到,即$(\mathbf{A})_{ij}=a_ij\Rightarrow(\mathbf{A}^T)_{ij}=a_{ji}$ 注:$(\mathbf{A}^T)^T=\mathbf{A}$,$(\mathbf{A}\mathbf{B})^T=\mathbf{B}^T\mathbf{A}^T$,$(\mathbf{A}^T)^{-1}=(\mathbf{A}^{-1})^T$。 对称矩阵的定义,若$\mathbf{A}^T=\mathbf{A}$,则称$\mathbf{A}$为对称矩阵。 注:$\mathbf{A}^T\mathbf{A}$一定是对称矩阵。 矩阵的特征值问题、相似变换化标准形 矩阵的特征值问题 对于$\mathbf{A}\in\mathbb{C}^{n\times n}$,若有$\lambda\in\mathbb{C}$和非零向量$\vec{x}\in\mathbb{C}^n$,使得: $$\mathbf{A}\vec{x}=\lambda\vec{x}$$ 则称$\vec{x}$为特征值$\lambda$对应的特征向量。 特征值的求解 $\mathbf{A}\in\mathbb{C}^{n\times n}$,多项式$p(\lambda)=\det(\lambda\mathbf{I}-\mathbf{A})$称为$\mathbf{A}$的特征多项式,方程$p(\lambda)=0$称为特征方程,若$\lambda$是$p$的根,则$\lambda$是$\mathbf{A}$的特征值。 注:由于实际上,特征多项式是一个高阶多项式,所以直接使用特征多项式求解特征值是个病态问题。 谱的定义 任意$\mathbf{A}\in\mathbb{C}^{n\times n}$,一定有$n$个特征值。全体特征值的集合称为$\mathbf{A}$的谱,记作$\sigma(\mathbf{A})$: $$\sigma(\mathbf{A})\stackrel{\text{def}}{=}{\lambda_1,\cdots,\lambda_n}$$ 还定义谱半径: $$\rho(\mathbf{A})\stackrel{\text{def}}{=}\max_{\lambda\in\sigma(\mathbf{A})}|\lambda|$$ 有: $\mathrm{tr}(\mathbf{A})=\sum\limits_{i\leq i\leq n}\lambda_i$; $\det\mathbf{A}=\prod\limits_{i\leq i\leq n}\lambda_i$。 注:展开特征多项式就能得到这个结论。 可对角化的定义 设$\mathbf{A}\in\mathbb{F}^{n\times n}$,若存在非奇异矩阵$\mathbf{P}\in\mathbb{F}^{n\times n}$,使得: $$\mathbf{D}=\mathbf{P}^{-1}\mathbf{A}\mathbf{P}$$ 为对角阵,则称$\mathbf{A}$可对角化。$\mathbf{A}$可对角化的充要条件是它有$n$个线性无关的特征向量。 实对称矩阵的特征值问题 若$\mathbf{A}$是实对称矩阵,则$\mathbf{A}$的特征值都是实数,可以对角化。且不同特征值的特征向量相互正交。 可对角化的充分条件 矩阵$\mathbf{A}\in\mathbb{R}^{n\times n}$的不同特征值对应的特征向量是线性无关的。若$\mathbf{A}$有$n$个不同的特征值,则$\mathbf{A}$可对角化。 代数重数的定义 若$\mathbf{A}$具有重特征值,即特征方程有重根,则: $$\det(\lambda\mathbf{I}-\mathbf{A})=(\lambda-\lambda_1)^{n_1}(\lambda-\lambda_2)^{n_2}\cdots(\lambda-\lambda_s)^{n_s},\lambda_i\neq\lambda_j,\text{if}\;i\neq j$$ 亦即$\lambda_i$是特征方程的$n_i$重根,$n_i$称为特征值$\lambda_i$的代数重数。 $$n_1+n_2+\cdots+n_s=n,n_i\geq 1,1\leq i\leq s$$ 几何重数的定义 设$\lambda_i$对应的最大线性无关特征向量的个数为$m_i$,则$m_i$就是其次方程组$(\lambda_i\mathbf{I}-\mathbf{A})\vec{x}=\vec{0}$的基础解系所包含最大线性无关解的个数,亦即特征值对应的特征子空间的维数,$m_i$称为特征值$\lambda_i$的几何重数。 几何重数小于代数重数 $m_i\leq n_i, i=1,2,\cdots,s$。 可对角化的充要条件 设$\mathbf{A}$具有重特征值,则$\mathbf{A}$可对角化的重要条件是每个特征值的几何重数和代数重数相等。 Jordan标准型 任意复方阵可以通过相似变换化为Jordan标准型J: $$\mathbf{P}^{-1}\mathbf{A}\mathbf{P}=\mathbf{J}=\mathrm{diag}(\mathbf{J}_1,\mathbf{J}_2,\cdots,\mathbf{J}_p)$$ 每个$\mathbf{J}_i$对应一个特征值$\lambda_i$,它是$r_i$个小块组成的块对角阵: $$\mathbf{J}_i=\mathrm{diag}(\mathbf{J}_{i1},\mathbf{J}_{i2},\cdots,\mathbf{J}_{ir_1}),\mathbf{J}_{ik}=\begin{bmatrix}\lambda_i&1&&\\&\ddots&\ddots&\\&&\lambda_i&1\\&&&\lambda_i\end{bmatrix}$$ 线性空间与内积空间 线性空间 线性空间的定义 设$\mathbb{F}$是一个数域,$V$是一个非空集合,在$V$上定义两种运算: 加法:$\forall u,v\in V$,有唯一$u+v\in V$(封闭性),且: $u+v=v+u, \forall u,v\in V$(加法交换律); $u+(v+w)=(u+v)+w, \forall u,v\in V$(加法结合律); 有唯一零元素$\theta\in V$,使得$u+\theta=u, \forall u\in V$; 对每个$u\in V$,有唯一的负元素$-u\in\theta$,使得$u+(-u)=\theta$。 数乘:$\forall\alpha\in\mathbb{F},u\in V$,有唯一$\alpha u\in V$(封闭性),且: $1u=u,\forall u\in V$(单位元); $\alpha(\beta u)=(\alpha\beta)u,\forall\alpha,\beta\in\mathbb{F}, u\in V$(数乘的交换律); $\alpha(u+v)=\alpha u+\alpha v,\forall\alpha\in\mathbb{F},u,v\in V$(数乘对数的结合律); $(\alpha+\beta)u=\alpha u+\beta u,\forall\alpha,\beta\in\mathbb{F},u\in V$(数乘对向量的结合律)。 称$V$为数域$\mathbb{F}$上的线性空间(或数域$F$上的向量空间)。 线性子空间的定义 若线性空间$V$的一个子集$W$按照$V$的加法和数乘也是一个线性空间,则称$W$是$V$的线性子空间。 例子:实向量$\mathbb{R}^n$、复向量$\mathbb{C}^n$、$[a,b]$闭区间上的连续函数$C[a,b]$、实矩阵$\mathbb{R}^{m\times n}$、复矩阵$\mathbb{C}^{m\times n}$和$[a,b]$闭区间上最高次数为$n$的多项式$\mathcal{P}_N[a,b]$都能组成线性空间。 线性无关的定义 设$(\mathbb{V},\mathbb{F})$是一个线性空间,$x_i\in V,i=1,2,\cdots,n$,若存在不是全零的$a_i\in\mathbb{F}, i=1,2,\cdots,n$,使得: $$\sum_{i=1}^na_ix_i=0$$ 则称${x_i}_{i=1}^n$是线性相关的,反之,则称它是线性无关的。 例子:在$\mathcal{P}_N[a,b]$中,${1,x,\cdots,x^N}$是线性无关的,在$C[-\pi,\pi]$中,${1,\cos x,\sin x,\cdots,\cos nx, \sin nx}$是线性无关的。 基和维数的定义 若${x_i}_{i=1}^n$,且$\forall x\in V$,有$x=\sum\limits_{i=1}^na_ix_i$,则${x_i}_{i=1}^n$构成$V$的一组基,空间的维数为$n$。 例子:$\mathcal{P}_N$中${1,x,\cdots,x^N}$是一组基,$\dim\mathcal{P}_N=N+1$;$C[a,b]$中$\forall N,{1,x,\cdots,x^N}$是线性无关的,$\dim C[a,b]=+\infty$。 内积空间 内积的定义 设$V$是数域$\mathbb{F}$上的线性空间,内积$(\cdot,\cdot):V\times V\to\mathbb{F}$,且有: $(u+v,w)=(u,w)+(v,w),\forall u,v,w\in V$; $(\alpha u,v)=\alpha(u,v),\forall u,v\in V,\alpha\in\mathbb{F}$; $(u,v)=\overline{(v,u)},\forall u,v\in V$; $(u,u)\geq 0,\forall u\in V$,且$(u,u)=0\Leftrightarrow u=\theta$。 则称$(u,v)$是$u$和$v$的内积,定义了内积的空间称为内积空间。 欧氏空间上的一种内积 $\vec{x},\vec{y}\in\mathbb{R}^n$,则内积定义为: $$(\vec{x},\vec{y})=\vec{y}^T\vec{x}$$ 酉空间上的一种内积 $\vec{x},\vec{y}\in\mathbb{C}^n$,则内积定义为: $$(\vec{x},\vec{y})=\overline{\vec{y}}^T\vec{x}$$ 此外也可以定义一种带权内积:$(\vec{x},\vec{y})_\omega=\sum\limits_{i=1}^n\omega_ix_iy_i$。 正交的定义 若向量$\vec{x},\vec{y}\in\mathbb{R}^n$满足$(\vec{x},\vec{y})=0$,则称它们是正交的。两个向量的集合$X$和$Y$,如果每个$\vec{x}\in X$和每个$\vec{y}\in Y$正交,则称$X$与$Y$正交。 正交集合的定义 $S$是非零向量的集合,若$\forall\vec{x},\vec{y}\in S(\vec{x}\neq\vec{y}\rightarrow(\vec{x},\vec{y})=0)$,则称其为正交集合。 定理正交是线性无关的充分条件 正交集合$S$中的向量是线性无关的。 证:假设$S={\vec{x}_1,\vec{x}_2,\cdots,\vec{x}_n}$,假设有$\sum\limits_{i=1}^nk_i\vec{x}_i=\vec{0}$,两边同时点乘$x_j$,有$\sum\limits_{i=1}^nk_i\vec{x}_i^T\vec{x}_j=k_j\vec{x}_j^T\vec{x}_j=0$,由$\vec{x}_j\neq\vec{0}$和正定性,我们知道$\vec{x}_j^T\vec{x}_j\neq 0$,故$k_j=0$,同理$k_i,i=1,2,\cdots,n=0$,集合$S$中的向量线性无关。 正交基的推论 若正交集合$S\subseteq\mathbb{R}^n$有$n$个向量,则它是$\mathbb{R}^n$的一组基。 连续函数内积的定义 设$f(x),g(x)\in C[a,b]$,则它们的$L^2$内积为: $$(f,g)=\int_a^bf(x)g(x)dx$$ 权函数的定义 若定在$[a,b]$上的可积函数$\rho(x)$满足: $\rho(x)\geq 0,\forall x\in[a,b]$; 在$[a,b]$的任一子区间上$\rho(x)$不恒为零。 称$\rho(x)$为$[a,b]$上的一个权函数。 利用权函数可以定义带权$L^2$内积: $$(f,g)_\rho=\int_a^b\rho(x)f(x)g(x)dx$$ Cauchy-Schwarz不等式 设$V$是一个内积空间,对任一的$u,v$有: $$|(u,v)^2|\leq(u,u)(v,v)$$ 等号成立当且仅当$u,v$线性相关。 证:这里仅给出实数域上的证明,对任意$u,v\in V,t\in\mathbb{R}$,考虑内积$(tu+v,tu+v)=(u,u)t^2+2(u,v)t+(v,v)\geq 0$,故判别式$\Delta=(2(u,v))^2-4(u,u)(v,v)<0$,得证。 Gram-Schmidt正交化方法 若${u_1,u_2,\cdots,u_n}$是内积空间$V$中的一个线性无关元素系列,则: $$\begin{cases} v_1=u_1\\ v_i=u_i-\sum\limits_{k=1}^{i-1}\frac{(u_i,v_k)}{(v_k,v_k)}v_k,i=2,3,\cdots \end{cases}$$ 生成$V$中一个正交序列${v_1,v_2,\cdots,v_n}$是$\mathrm{span}{u_1,u_2,\cdots,u_n}$的一组基。 若要求归一化,$q_i=\frac{1}{\sqrt{(v_i,v_i)}}v_i$。则可得到QR分解。 经典的Gram-Schmidt正交化: $j=1,\cdots, n$: 计算$r_{ij}:=(\vec{u}_j,\vec{q}_i), i=1,2,\cdots,j-1$; 计算$\vec{q}’=\vec{u}_j-\sum\limits_{i=1}^{j-1}r_{ij}\vec{q}_i$; 计算$r_{jj}=|\vec{q}’|_2$,如果$r_{jj}=0$,停止,否则$\vec{q}_j:=\frac{\vec{q}’}{r_{jj}}$; 改进的Gram-Schmidt正交化: $j=1,\cdots, n$: 令$\vec{q}’:=\vec{u}_j$; $i=1,\cdots, j-1$: 计算$r_{ij}:=(\vec{q}’,q_i), i=1,2,\cdots,j-1$; 计算$\vec{q}’=\vec{q}’-r_{ij}\vec{q}_i$; 计算$r_{jj}=|\vec{q}’|_2$,如果$r_{jj}=0$,停止,否则$\vec{q}_j:=\frac{\vec{q}’}{r_{jj}}$; 运行结果: 正确 错误,{{ evaluation && evaluation[1] }} 异常,抛出 {{ exception }} 运行时间: {{ (Math.round(time * 1000) / 1000).toFixed(3) }} ms 输入: 返回值: 检验: 范数、赋范线性空间 范数的定义 设$V$是数域$\mathbb{F}$上的线性空间,定义$|\cdot|:V\to\mathbb{R}$,满足: 正定性:$|u|\geq 0,\forall u\in V$,且$|u|=0\Leftrightarrow u=\theta$; 齐次性:$|\alpha u|=|\alpha||u|,\forall u\in V,\alpha\in\mathbb{R}$; 三角不等式:$|u+v|\leq|u|+|v|,\forall u,v\in V$。 称$|\cdot|$为$V$的范数(模),定义了范数的线性空间称为赋范线性空间。 常见的范数 对于$\vec{x}=[x_1,x_2,\cdots,x_n]^T$可定义范数结构: $\infty$-范数:$|\vec{x}|_{\infty}=\max_{1\leq i\leq n}|x_i|$; $1$-范数:$|\vec{x}|_1=\sum\limits_{i=1}^n|x_i|$; $2$-范数:$|\vec{x}|_2=(\sum\limits_{i=1}^n|x_i|^2)^{1/2}$; $p$-范数:$|\vec{x}|_p=(\sum\limits_{i=1}^n|x_i|^p)^{1/p}$。 内积与2-范数的关系 通过内积可定义$2$-范数: $$|x|_2=\sqrt{(x, x)}$$ 设$\vec{x},\vec{y}\in\mathbb{R}^n$,其夹角$\alpha$被定义为: $$\cos\alpha=\frac{(\vec{x},\vec{y})}{|\vec{x}|_2|\vec{y}|_2}$$ 平行时,即$\alpha=0$或$\alpha=\pi$时,Cauchy-Schwarz不等式取等号。 距离的定义 设$X$是任一非空集合,$X$中的任意两点$x,y$有$d(x,y)\in\mathbb{R}$与之一一对应,且满足: 非负性:$d(x,y)\geq 0$,且$d(x,y)=0\Leftrightarrow x=y$; 对称性:$d(x,y)=d(y,x)$; 三角不等式:$d(x,y)\leq d(x,z)+d(z,y)$。 称$d(x,y)$是$X$上的一个距离,定义了距离的集合$X$称为一个距离空间。 范数与距离的关系 设$V$是赋范空间,则$u,v\in V$的距离可定义为: $$d(u,v)=|u-v|$$ 常见距离相似度: Hamming距离:序列最小替换以相同的树木; 欧氏距离:$|\vec{x}-\vec{y}|_2$; 曼哈顿距离:$|\vec{x}-\vec{y}|_1$; 切比雪夫距离:$|\vec{x}-\vec{y}|_\infty$; 余弦距离(实际上是相似度):$\cos\theta=\frac{(\vec{x},\vec{y})}{|\vec{x}|_2|\vec{y}|_2}$; Jaccard相似系数:$J(A,B)=\frac{|A\cap B|}{|A\cup B|}$; 相关系数$\rho_{XY}=\frac{\mathrm{Cov}(X,Y)}{\sigma_X\sigma_Y}=\frac{E((X-E(X))(Y-E(Y)))}{\sigma_X\sigma_Y}$。 K近邻(KNN)算法 监督学习,选取最近的$K$个训练数据点,预测数据分类为这$K$个数据点的多数类别。 k-means聚类算法 无监督学习,确定有$k$个分类后,选择$k$个距离较远的初始数据点作为质心,而后每个数据点离哪个质心近,就归类为质心所属的集合,再重新计算每个集合的质心,循环往复直到质心位置变化量小。 范数等价 设$|\cdot|_\alpha$和$|\cdot|_\beta$是线性空间$V$的两个范数,若存在正的常数$C_1$和$C_2$,使得: $$C_1|u|_\alpha\leq|u|_\beta\leq C_2|u|_\alpha,\forall u\in V$$ 则称范数$|\cdot|_\alpha$和范数$|\cdot|_\beta$等价。 有限维空间中的任意两个范数等价。 几种常见矩阵的性质 正交矩阵和酉矩阵 正交矩阵的定义 设$\mathbf{Q}\in\mathbb{R}^{n\times n}$,若满足: $$\mathbf{Q}^T\mathbf{Q}=\mathbf{I}$$ 则称$\mathbf{Q}$为正交矩阵。 正交矩阵的性质 正交矩阵有如下的性质: $\mathbf{Q}$不同的列向量相互正交,且各列向量的2-范数为1; $\mathbf{Q}^{-1}=\mathbf{Q}^T$,且$\mathbf{Q}^T$也是正交矩阵; $|\det\mathbf{Q}|=1$; 若$\mathbf{A}$和$\mathbf{B}$是同阶的正交矩阵,则$\mathbf{A}\mathbf{B}$和$\mathbf{B}\mathbf{A}$都是正交矩阵。 对称矩阵和对称正定矩阵 对称阵的定义 如果$\mathbf{A}\in\mathbb{R}^{n\times n}$,有$A^T=A$,则称$\mathbf{A}$为对称阵。 对称阵的性质: $\mathbf{A}$的特征值均为实数,且有$n$个线性无关的特征向量; $\mathbf{A}$对应于不同特征值的特征向量必正交; 存在正交阵$\mathbf{Q}$使得$\mathbf{Q}^{-1}\mathbf{A}\mathbf{Q}$为对角阵。 性质1的证明: $$\begin{aligned} &\mathbf{A}\vec{x}=\lambda\vec{x}\\ \Rightarrow&\overline{\mathbf{A}\vec{x}}=\overline{\lambda\vec{x}}\\ \Rightarrow&\mathbf{A}\overline{\vec{x}}=\overline{\lambda}\overline{\vec{x}}\\ \Rightarrow&\vec{x}^T\mathbf{A}\overline{\vec{x}}=\vec{x}^T\overline{\lambda\vec{x}}\\ \Rightarrow&(\mathbf{A}\vec{x})^T\overline{\vec{x}}=\overline{\lambda}\vec{x}^T\overline{\vec{x}}\\ \Rightarrow&\lambda\vec{x}^T\overline{\vec{x}}=\overline{\lambda}\vec{x}^T\overline{\vec{x}}\\ \Rightarrow&\lambda-\overline{\lambda}=0 \end{aligned}$$ 初等矩阵 可约矩阵 对角占优矩阵 x.slice()); const r = Array(u[0].length).fill().map(x => Array(u[0].length).fill(0)); for (let j = 0; j x.slice()); const r = Array(u[0].length).fill().map(x => Array(u[0].length).fill(0)); for (let j = 0; j typeof x === 'number')) { return false } } return true } function matrixMultiply (a, b) { const result = Array(a.length).fill().map(() => Array(b[0].length).fill(0)) for (let i = 0; i Array(n).fill(0)) for (let i = 0; i Array(a.length)) for (let i = 0; i ({ qrDecompositionPredefined: [ { name: '经典', code: qrDecompositionOrigin }, { name: '改进', code: qrDecompositionModified } ], qrDecompositionTestCases }), methods: { qrDecompositionCorrect (test, result) { if (!Array.isArray(result)) { return [false, '应返回Q,R组成的元组'] } const u = test[0][0] const [q, r] = result if (!isMatrix(q, u.length, u[0].length)) { return [false, 'Q不是合法的矩阵'] } if (!isMatrix(r, u[0].length, u[0].length)) { return [false, 'R不是合法的矩阵', true] } const qr = matrixMultiply(q, r) const qr\_mse = matrixMSE(u, qr) const qq = matrixMultiply(matrixTranspose(q), q) const qq\_mse = matrixMSE(matrixEye(u[0].length), qq) if (isNaN(qr\_mse) || qr\_mse > epsilon) { return [false, `QR!=U, MSE=${qr\_mse}`, true, true, qr, qr\_mse, qq, qq\_mse] } if (isNaN(qq\_mse) || qq\_mse > epsilon) { return [false, `Q^TQ!=I, MSE=${qq\_mse}`, true, true, qr, qr\_mse, qq, qq\_mse] } return [true, '', true, true, qr, qr\_mse, qq, qq\_mse] }, convertNumber (number) { if (Math.abs(number) row.map(this.convertNumber).join('&')).join('\\\\\\\\')}\\\\end{bmatrix}` } } }

2020/9/28
articleCard.readMore

最优化理论与算法第3章:单纯形方法

本文的内容主要来自陈宝林的《最优化理论与算法(第2版)》第3章《单纯形方法》和老师的PPT。 单纯形方法原理 基本可行解的转换 考虑问题: $$\begin{aligned} \min \;\;\;\;&f\stackrel{\text{def}}{=}\vec{c}^T\vec{x} \\ \text{s. t.} \;\;\;\;&\mathbf{A}\vec{x}=\vec{b} \\ \;\;\;\;&\vec{x}\geq\vec{0} \end{aligned}$$ 同时记: $$\begin{aligned} &\mathbf{A}=[\vec{p}_1,\vec{p}_2,\cdots,\vec{p}_n]=[\mathbf{B}\mid\mathbf{N}]\\ &\vec{x}^{(0)}=\begin{bmatrix} \mathbf{B}^{-1}\vec{b}\\ \vec{0} \end{bmatrix} \end{aligned}$$ 其中$\mathbf{B}$和$\mathbf{N}$分别是基本可行解$\vec{x}^{(0)}$的基矩阵和非基矩阵。则$\vec{x}^{(0)}$处的目标函数值为: $$f_0=\vec{c}^T\vec{x}^{(0)}=\vec{c}_B^T\mathbf{B}^{-1}\vec{b}$$ 我们的目标是从这个基本可行解出发,得到另一个目标函数值更小的。考虑任意可行解: $$\vec{x}=\begin{bmatrix} \vec{x}_B\\ \vec{x}_N \end{bmatrix}$$ 注意,这里的$\vec{x}_B$和$\vec{x}_N$的基矩阵和非基矩阵是相对$\vec{x}^{(0)}$说的。这时候目标函数值为: $$\begin{aligned} f&=\vec{c}_B^T(\mathbf{B}^{-1}\vec{b}-\mathbf{B}^{-1}\mathbf{N}\vec{x}_N)+\vec{c}_N^T\vec{x}_N\\ &=f_0-(\vec{c}_B^T\mathbf{B}^{-1}\mathbf{N}-\vec{c}_N^T)\vec{x}_N\\ &=f_0-\sum_{j=m+1}^n(\vec{c}_B^T\mathbf{B}^{-1}\vec{p}_j-c_j)x_j\\ &=f_0-\sum_{j=m+1}^n(z_j-c_j)x_j\;\;\;\;\text{其中}z_j=\vec{c}_B^T\mathbf{B}^{-1}\vec{p}_j \end{aligned}$$ 注意到$\vec{x}\geq 0$,所以此时若$z_j-c_j\leq 0, j=m+1,\cdots,n$,则达到最优解条件。若此时存在$z_j-c_j>0$,为了让$f$下降得更快,所以我们会添加一个基$x_k, m<k\leq n$,这个基对应的$z_j-c_j$应当是最最大的。所以有: $$z_k-c_k=\max_{m<j\leq n} z_j-c_j$$ 则此时: $$\begin{aligned} \vec{x}_B&=\mathbf{B}^{-1}\vec{b}-\mathbf{B}^{-1}\vec{p}_kx_k\\ &=\vec{x}_B^{(0)}-\vec{y}_kx_k\\ &\text{其中}\vec{x}_B^{(0)}=\mathbf{B}^{-1}\vec{b},\vec{y}_k=\mathbf{B}^{-1}\vec{p}_k \end{aligned}$$ 为了保证新的解是可行解,我们需要确保$\vec{x}_B\geq 0$,即$\vec{x}_B^{(0)}-\vec{y}_kx_k\geq 0$,所以我们应当取: $$x_k=\min\left\{\frac{x_{Bi}^{(0)}}{y_{ki}}\mid y_{ki}>0\right\}=\frac{x_{Br}^{(0)}}{y_{kr}}$$ 若$\vec{y}_k\leq 0$,则问题无界。如果找到了,这时候$x_r=0$,得到了一个新的可行解,接下来我们证明这是基本可行解,方法是证明$\vec{p}_1,\cdots,\vec{p}_{r-1},\vec{p}_k,\vec{p}_{r+1},\vec{p}_m$线性无关。注意到$\vec{p}_1,\cdots,\vec{p}_{r-1},\vec{p}_r,\vec{p}_{r+1},\vec{p}_m$是线性无关的,而$\vec{p}_k=\mathbf{B}\vec{y}_k=\sum\limits_{i=1}^m y_{ki}\vec{p}_i$,而$y_{kr}\neq 0$,故$\vec{p}_r$可被$\vec{p}_1,\cdots,\vec{p}_{r-1},\vec{p}_k,\vec{p}_{r+1},\vec{p}_m$线性标出,故$\vec{p}_1,\cdots,\vec{p}_{r-1},\vec{p}_k,\vec{p}_{r+1},\vec{p}_m$线性无关。故: $$\vec{x}=[x_0^{(0)},\cdots,x_{r-1}^{(0)},0,x_{r+1}^{(0)},\cdots,x_m^{(0)},0,\cdots,0,x_k,0,\cdots,0]^T$$ 是新的更优的基本可行解。 定理 3.1.1 若在极小化问题中,对于某个基本可行解,所有$z_j-c_j\leq 0$,则这个基本可行解是最优解,这里: $$z_j-c_j=\vec{c}_B^T\mathbf{B}^{-1}\vec{p}_j-c_j, j=1,\cdots,n$$ 称为判别数或检验数。注:如果$j$是基的下标,则判别数为$0$。 单纯形方法计算步骤 首先要给定一个初始的基本可行解,假设初始基矩阵为$\mathbf{B}$,执行以下步骤: 求$\vec{x}_B=\mathbf{B}^{-1}\vec{b}$; 求单纯形子$\vec{w}^T=\vec{c}_B\mathbf{B}^{-1}$,对于所有非基变量,计算判别数$z_j-c_j=\vec{w}^T\vec{p}_j-c_j$,令: $$z_k-c_k=\max_{j\in\text{非基变量下标集}} z_j-c_j$$ 若$z_k-c_k\leq 0$,则找到最优解,退出。否则进入下一步; 求$\vec{y}_k=\mathbf{B}^{-1}\vec{p}_k$,若$\vec{y}_k\leq 0$,则不存在有限最优解,退出,否则进入下一步; 确定下标$r$,使: $$\frac{\vec{x}_{Br}}{y_{kr}}=\min\left\{\frac{\vec{x}_{Bi}}{y_{ki}}\mid y_{ki}>0\right\}$$ $x_r$为离基变量,$x_k$为进基变量,用$\vec{p}_k$替换$\vec{p}_r$得到新的基矩阵,返回步骤1。 对于最大化问题,步骤2的$\max$改$\min$即可。 收敛性 所有迭代会出现下列情况: $z_k-c_k\leq 0$,当前解为最优解; $z_k-c_k>0$且$\vec{y}_k\leq 0$,问题无界; $z_k-c_k>0$且$\vec{y}_k\nleq 0$,找到新基本可行解,非退化情况下目标函数下降。 定理 3.1.2 对于非退化问题,单纯形方法经过有限次迭代要么达到最优基本可行解,要么无界。 使用表格形式的单纯形方法 首先,对标准形式作变换可以得到: $$\begin{aligned} \min \;\;\;\;&f \\ \text{s. t.} \;\;\;\;&f-\vec{c}_B^T\vec{x}_B-\vec{c}_N^T\vec{x}_N=0 \\ \;\;\;\;&\mathbf{B}\vec{x}_B+\mathbf{N}\vec{x}_N=\vec{b} \\ \;\;\;\;&\vec{x}_B\geq\vec{0},\vec{x}_N\geq\vec{0} \end{aligned}$$ 消去第一个等式中的$\vec{x}_B$,得到: $$\begin{aligned} \min \;\;\;\;&f \\ \text{s. t.} \;\;\;\;&\mathbf{B}\vec{x}_B+\mathbf{N}\vec{x}_N=\vec{b} \\ \;\;\;\;&f+(\vec{c}_B^T\mathbf{B}^{-1}\mathbf{N}-\vec{c}_N^T)\vec{x}_N=0 \\ \;\;\;\;&\vec{x}_B\geq\vec{0},\vec{x}_N\geq\vec{0} \end{aligned}$$ 将上式写在表格中就有了单纯形表: $f$ $\vec{x}_B$ $\vec{x}_N$ 右端 $\vec{x}_B$ $\vec{0}$ $\mathbf{I}_m$ $\mathbf{B}^{-1}\mathbf{N}$ $\mathbf{B}^{-1}\vec{b}$ $f$ $1$ $0$ $\vec{c}_B\mathbf{B}^{-1}\mathbf{N}-\vec{c}_N$ $\vec{c}_B\mathbf{B}^{-1}\vec{b}$ 其中,$\vec{c}_B\mathbf{B}^{-1}\mathbf{N}-\vec{c}_N$的每一个分量即为判别数$z_j-c_j$,$\mathbf{B}^{-1}\mathbf{N}$的列分量就是$\vec{y}_k$。第一列通常省略。 初始的时候。我们已经有了一个可行解$\vec{x}_N=\vec{0}$。 若$\vec{c}_B\mathbf{B}^{-1}\mathbf{N}-\vec{c}_N\leq 0$,则找到了最优解。 若$\vec{c}_B\mathbf{B}^{-1}\mathbf{N}-\vec{c}_N\nleq 0$,则需要用主元消去法。在表的最后一行中,寻找除右端外的最大值,它所在的一行称主列,从而确定了离基变量。这时候寻找主列上除最后一列外大于0的元素,且右端除以该元素值最小的,找到的该元素称为主元,它所在的行称为主行,再用主行加上初等行变换,消去主元所在列,并使主元为$1$。循环此操作。 .simplex-table tr { border: none !important; } .simplex-table tr th { border: none !important; } .simplex-table tr:nth-child(2n) { background-color: transparent !important; } .simplex-table tr:nth-child(2n) td:not(:first-child) { background-color: #f6f8fa !important; } .simplex-table tr td:first-child { border: none !important; }

2020/9/27
articleCard.readMore

最优化理论与算法第2章:线性规划的基本性质

本文的内容主要来自陈宝林的《最优化理论与算法(第2版)》第2章《线性规划的基本性质》。 标准形式及图解法 标准形式 一般线性规划问题,可以写成下列标准形式: $$\begin{aligned} \min \;\;\;\;&\vec{c}^T\vec{x} \\ \text{s. t.} \;\;\;\;& \mathbf{A}\vec{x}=\vec{b} \\ \;\;\;\;& \vec{x}\geq\vec{0} \end{aligned}$$ 其中$\vec{c},\vec{x}\in\mathbb{R}^n$,$\mathbf{A}\in\mathbb{R}^{m\times n}$,$\vec{b}\in\mathbb{R}^m$。同时一般假定$\vec{b}\geq\vec{0}$。 极大化转换:若优化目标为$\max$,则另$\vec{c}’=-\vec{c}$。 约束方程常量为负:若$b_i<0$,另$b_i=-b_i’,a_{ij}=-a_{ij}’$。 决策变量无非负限制:若$x_j$无非负限制,可以另$x_j=x_j’-x_j’’$,其中$x_j’,x_j’’\geq 0$。 决策变量有上下界:若$x_j\geq l_j$,另$x_j’=x_j-l_j$;若$x_j\leq u_j$,另$x_j’=u_j-x_j$。注:此方法与约束方程为不等式类似,如果同时存在上下界,就引入松弛变量。 约束方程为不等式:若给定的问题为: $$\begin{aligned} \min \;\;\;\;&\vec{c}^T\vec{x} \\ \text{s. t.} \;\;\;\;& \mathbf{A}\vec{x}\leq\vec{b} \\ \;\;\;\;& \vec{x}\geq\vec{0} \end{aligned}$$ 可以引入松弛变量$\vec{x}’\in\mathbb{R}^m$,化为下列标准式: $$\begin{aligned} \min \;\;\;\;&\vec{c}^T\vec{x} \\ \text{s. t.} \;\;\;\;& \mathbf{A}\vec{x}+\vec{x}’=\vec{b} \\ \;\;\;\;& \begin{bmatrix}\vec{x}\\\vec{x}’\end{bmatrix}\geq\vec{0} \end{aligned}$$ 类似地,对于$\mathbf{A}\vec{x}\geq\vec{b}$,可以引入剩余变量。 含有绝对值:若给定的问题为(这里使用$|\cdot|$表示逐个元素的绝对值): $$\begin{aligned} \min \;\;\;\;&\vec{c}^T|\vec{x}| \\ \text{s. t.} \;\;\;\;& \mathbf{A}\vec{x}=\vec{b} \end{aligned}$$ 可以引入$\vec{u}=\frac{|\vec{x}|+\vec{x}}{2},\vec{v}=\frac{|\vec{x}|-\vec{x}}{2}$,于是我们有$\vec{u},\vec{v}>\vec{0},\vec{x}=\vec{u}-\vec{v},|\vec{x}|=\vec{u}+\vec{v}$,化为下列标准式: $$\begin{aligned} \min \;\;\;\;&\vec{c}^T(\vec{u}+\vec{v}) \\ \text{s. t.} \;\;\;\;& \mathbf{A}(\vec{u}-\vec{v})=\vec{b} \\ \;\;\;\;& \begin{bmatrix}\vec{u}\\\vec{v}\end{bmatrix}\geq\vec{0} \end{aligned}$$ TODO: 写个程序演示一下? 图解法 线性不等式的几何意义是半平面,因此对于两变量线性规划,可以使用图解法: 作出线性规划问题的可行域; 作出目标函数的等值线; 移动等值线到可行域边界得到最优点。 注意,目标函数的梯度指向目标函数增大的方向。 线性规划可能存在无穷多个最优解,这些解组成了可行域的一条边;线性规划可行域为空集,则线性规划无解。若可行域无界,则该线性规划可能无界,即不存在有限最优值(这种情况也是无最优解)。 $$\text{线性规划问题} \begin{cases} \text{有可行解} \begin{cases} \text{有最优解} \begin{cases} \text{唯一解} \\ \text{无穷多解} \end{cases} \\ \text{无界(必要条件是可行域无界)} \end{cases} \\ \text{有可行解} \end{cases}$$ 基本性质 可行域 定理2.2.1 约束条件为线性等式或者不等式的线性规划的可行域是凸集。 注:由凸集的有限交是凸集即可得证。 最优极点 考虑标准形式的可行域,其极点为$\vec{x}_1,\cdots,\vec{x}_k$,极方向为$\vec{d}_1,\cdots,\vec{d}_l$,由表示定理可以知道,可行点$\vec{x}$可以表示为: $$\begin{aligned} &\vec{x}=\sum_{j=1}^k\lambda_j\vec{x}_{j}+\sum_{j=1}^l\mu_j\vec{d}_j\\ &\sum_{j=1}^k\lambda_j=1,\\ &\lambda_j\geq 0, j=1,\cdots,k,\\ &\mu_j\geq 0, j=1,\cdots,l. \end{aligned}$$ 代入标准形式: $$\begin{aligned} \min \;\;\;\;&\sum_{j=1}^k\lambda_j\vec{c}^T\vec{x}_j+\sum_{j=1}^l\mu_j\vec{c}^T\vec{d}_j \\ \text{s. t.} \;\;\;\;&\sum_{j=1}^k\lambda_j=1, \\ \;\;\;\;&\lambda_j\geq 0, j=1,\cdots,k,\\ \;\;\;\;&\mu_j\geq 0, j=1,\cdots,l. \end{aligned}$$ 若某个$\vec{c}^T\vec{d}_j<0$,则$\mu_j$可以取很大,问题成为了无界。所以有界的充要条件是所有$\vec{c}^T\vec{d}_j\geq 0$。有界的最优解一定有$\mu_j=0$。所以对于有界的问题,简化成了: $$\begin{aligned} \min \;\;\;\;&\sum_{j=1}^k\lambda_j\vec{c}^T\vec{x}_j \\ \text{s. t.} \;\;\;\;&\sum_{j=1}^k\lambda_j=1, \\ \;\;\;\;&\lambda_j\geq 0, j=1,\cdots. \end{aligned}$$ 这时,令: $$\vec{x}_p=\underset{\vec{x}_j,j=1,\cdots,k}{\operatorname{arg,min}}\;\vec{c}^T\vec{x}_j$$ 则: $$\min\sum_{j=1}^k\lambda_j\vec{c}^T\vec{x}_j=\vec{c}^T\vec{x}_p\;\;\;\;\text{此时}\lambda _j=\begin{cases} 1, &j=p \\ 0, &j\neq p \end{cases}$$ 定理2.2.2 设标准形式的线性规划可行域非空,则有下列结论: 存在最优解(这里的存在是指有限的)的充要条件是$\vec{c}\vec{d}_j\geq 0$,其中$\vec{d}_j$为可行域极方向; 若存在最优解,则最优解可在某个极点上达到。 最优基本可行解 对标准形式的线性规划,若$r(\mathbf{A})=m$,可将$\mathbf{A}$列调换后,得到矩阵$\mathbf{A}=[\mathbf{B},\mathbf{N}]$,其中$\mathbf{B}\in\mathbb{R}^{m\times m}$为满秩方阵。$\vec{x}$进行对应的行变换,可以得到$\vec{x}=\begin{bmatrix}\vec{x}_B\\\vec{x}_N\end{bmatrix}$,于是约束条件可以写为: $$\begin{aligned} & [\mathbf{B},\mathbf{N}]\begin{bmatrix}\vec{x}_B\\\vec{x}_N\end{bmatrix}=\vec{b} \\ \Rightarrow&\mathbf{B}\vec{x}_B+\mathbf{N}\vec{x}_N=\vec{b}\\ \Rightarrow&\vec{x}_B=\mathbf{B}^{-1}\vec{b}-\mathbf{B}^{-1}\mathbf{N}\vec{x}_N \end{aligned}$$ 其中$\vec{x}_N$的分量是自由分量,令$\vec{x}_N=\vec{0}$,可得到一个解: $$\vec{x}=\begin{bmatrix}\vec{x}_B\\\vec{x}_N\end{bmatrix}=\begin{bmatrix}\mathbf{B}^{-1}\vec{b}\\\vec{0}\end{bmatrix}$$ 定义2.2.1 对于: $$\vec{x}=\begin{bmatrix}\vec{x}_B\\\vec{x}_N\end{bmatrix}=\begin{bmatrix}\mathbf{B}^{-1}\vec{b}\\\vec{0}\end{bmatrix}$$ 称为方程$\mathbf{A}\vec{x}=\vec{b}$的一个基本解,$\mathbf{B}$称为基矩阵,简称为基。$\vec{x}_B$的各分量称为基变量,基变量全体称为一组基,$\vec{x}_N$的各分量称为非基变量。若$\mathbf{B}^{-1}\vec{b}\geq\vec{0}$,则$\vec{x}$称为基本可行解,$\mathbf{B}$称为可行基矩阵,基变量全体称为一组可行基。若$\mathbf{B}^{-1}\vec{b}>\vec{0}$称基本可行解是非退化的,否则称为退化的。 由于基矩阵只有有限个($\leq\begin{pmatrix}n\\m\end{pmatrix}=\frac{n!}{m!(n-m)!}$),基本解也只有有限个,基本可行解也为有限个。 定理2.2.3 极点的代数含义 令$K={\vec{x}\mid\mathbf{A}\vec{x}=\vec{b}, \vec{x}\geq\vec{0}}$的极点集与$\mathbf{A}\vec{x}=\vec{b}, \vec{x}\geq\vec{0}$基本可行解集等价。 注:其实线性规划的标准形式可以看作超平面与第一象限(闭集)的交,所有变量的个数$n$就是整个空间的维度,而非基变量的个数$n-m$是超平面的维度。与坐标轴、坐标平面之类的东西(这些东西的维度为$m$)交点就是那些非基变量为$0$的点,这些交点如果分量都大于$0$,那么就位于第一象限内,也就是极点。退化的基本可行解是指基本可行解本来交于类似坐标平面的高维事物,却交于类似于坐标轴的低维事物,即同时交于两个坐标平面,这种情况会出现重复的基本解。 引理 基本可行解的代数含义 对于可行解$\vec{x}^{(0)}$,$\vec{x}^{(0)}$是基本可行解$\Leftrightarrow$ $\vec{x}^{(0)}$的非零分量对应的$\mathbf{A}$的列向量线性无关。 证:“$\Rightarrow$”,由于$\vec{x}^{(0)}$是基本可行解,它取正值的分量必为基变量,对应的列向量是基矩阵的一部分,基矩阵可逆,所以对应的列向量必线性无关。“$\Leftarrow$”,这里只需要证明它是基变量,由于$\mathbf{A}\in\mathbb{R}^{m\times n}$行满秩,$\vec{x}^{(0)}$非零分量对应的列向量,一定可以扩充成$m$个线性无关的列向量,由定义知$\vec{x}^{(0)}$为基本解。 复述定理:令$S={\vec{x}\mid\mathbf{A}\vec{x}=\vec{b}, \vec{x}\geq\vec{0}}\land \vec{x}^{(0)}\in S$,$\vec{x}^{(0)}$是极点$\Leftrightarrow$ $\vec{x}^{(0)}$是基本可行解。 定理2.2.3 证:不妨假设$\vec{x}^{(0)}$的前$k$个分量大于$0$,余下的$n-k$个分量为$0$,即$\vec{x}^{(0)}=[x_1^{(0)},\cdots,x_k^{(0)},0,\cdots,0]^T$ “$\Rightarrow$”。由反证法,接下来证明,若$\vec{x}^{(0)}$是极点,且$\vec{x}^{(0)}$不是基本可行解,即$\vec{x}^{(0)}$前$k$个正分量$\vec{x}_j^{(0)}$对应的$\mathbf{A}$的列向量$\vec{p}_j$线性相关,会推导出矛盾。 存在不全为零的$y_1,y_2,\cdots,y_k\in\mathbb{R}$,满足$\sum\limits_{j=1}^k y_j\vec{p}_j=\vec{0}$,取$\lambda$满足$0<\lambda<\min{|\frac{\vec{x}_i^{(0)}}{y_i}|\mid y_i\neq 0,0\leq i\leq k}$,令: $$\vec{x}_j^{(1)}=\begin{cases} x_j^{(0)}+\lambda y_j,&1\leq j\leq k\\ 0,&k+1\leq j\leq n \end{cases} \;\;\;\; \vec{x}_j^{(2)}=\begin{cases} x_j^{(0)}-\lambda y_j,&1\leq j\leq k\\ 0,&k+1\leq j\leq n \end{cases}$$ 则我们有$\vec{x}^{(1)},\vec{x}^{(2)}\geq 0$,且由$\vec{x}^{(0)}\in S$有$\vec{x}^{(1)},\vec{x}^{(2)}\in S$。但$\vec{x}^{(0)}=\frac{1}{2}\vec{x}^{(1)}+\frac{1}{2}\vec{x}^{(2)}$,且$\vec{x}^{(1)}\neq\vec{x}^{(2)}$与极点定义矛盾。 “$\Leftarrow$”。已知$\vec{x}^{(0)}=\begin{bmatrix}\mathbf{B}^{-1}\vec{b}\\\vec{0}\end{bmatrix}$,其中$\mathbf{B}$为$\vec{x}^{(0)}$对应的基矩阵。假设存在$\vec{x}^{(1)},\vec{x}^{(2)}\in S\land\lambda\in(0, 1)$,满足$\vec{x}^{(0)}=\lambda\vec{x}^{(1)}+(1-\lambda)\vec{x}^{(2)}$。我们将$\vec{x}^{(1)},\vec{x}^{(2)}$也按前$k$个分量和后$n-k$个分量拆分,即$\vec{x}^{(1)}=\begin{bmatrix}\vec{x}_B^{(1)}\\\vec{x}_N^{(1)}\end{bmatrix},\vec{x}^{(2)}=\begin{bmatrix}\vec{x}_B^{(2)}\\\vec{x}_N^{(2)}\end{bmatrix}$。 $$\begin{bmatrix} \mathbf{B}^{-1}\vec{b}\\ \vec{0} \end{bmatrix}=\begin{bmatrix} \lambda\vec{x}_B^{(1)}+(1-\lambda)\vec{x}_N^{(1)}\\ \lambda\vec{x}_B^{(2)}+(1-\lambda)\vec{x}_N^{(2)} \end{bmatrix}$$ 由于$\lambda,1-\lambda>0$同时$\vec{x}_N^{(1)},\vec{x}_N^{(2)}\geq\vec{0}$,所以$\vec{x}_N^{(1)}=\vec{x}_N^{(2)}=\vec{0}$。又由于$\mathbf{B}\vec{x}_B^{(1)}+\mathbf{N}\vec{x}_N^{(1)}=\vec{b}$且$\mathbf{B}\vec{x}_B^{(2)}+\mathbf{N}\vec{x}_N^{(2)}=\vec{b}$,所以我们有$\vec{x}_B^{(1)}=\vec{x}_B^{(2)}=\mathbf{B}^{-1}\vec{b}$。故$\vec{x}^{(1)}=\vec{x}^{(2)}=\vec{x}^{(0)}$,$\vec{x}^{(0)}$为极点。 定理 极方向的代数含义 设$S={\vec{x}\mid\mathbf{A}\vec{x}=\vec{b},\vec{x}\geq 0}$的极方向$\vec{d}$有$k$个非零分量, $\vec{d}$是$S$的极方向$\Leftrightarrow$ $\vec{d}$的非零分量对应的$\mathbf{A}$的列向量组的秩为$k-1$。 证: “$\Leftarrow$”。$\mathbf{A}$为行满秩矩阵,故$k\leq m+1$。不妨假设$\vec{d}$的$k$个非零分量分别是$d_1,d_2,\cdots,d_{k-1}$和$d_{m+1}$,并且$\vec{d}$对应的列向量的一个极大线性无关组是$\vec{p}_1,\vec{p}_2,\cdots,\vec{p}_{k-1}$。这$k-1$个线性无关向量可以在$A$的列向量中扩充成$m$个线性无关的向量,$\mathbf{B}=[\vec{p}_1,\vec{p}_2,\cdots,\vec{p}_{m}]$。 假设存在$\vec{d}^{(1)}$和$\vec{d}^{(2)}$是$S$的方向(即$\mathbf{A}\vec{d}^{(1)}=\vec{0}$且$\mathbf{A}\vec{d}^{(2)}=\vec{0}$且$\vec{d}^{(1)},\vec{d}^{(2)}\geq\vec{0}$)且存在$\lambda_1,\lambda_2>0$,满足$\vec{d}=\lambda_1\vec{d}^{(1)}+\lambda_2\vec{d}^{(2)}$。则$d_{m+2}^{(1)},\cdots,d_n^{(1)}$与$d_{m+2}^{(2)},\cdots,d_n^{(2)}$均为$0$。因而$\vec{d}^{(1)},\vec{d}^{(2)}$可以记为: $$\vec{d}^{(1)}=\begin{bmatrix} \vec{d}_B^{(1)}\\ d^{(1)}_{m+1}\\ \vec{0} \end{bmatrix} \;\;\;\; \vec{d}^{(2)}=\begin{bmatrix} \vec{d}_B^{(2)}\\ d^{(2)}_{m+1}\\ \vec{0} \end{bmatrix} $$ 故: $$\begin{cases} \vec{d}_B^{(1)}=-d_{m+1}^{(1)}\mathbf{B}^{-1}\vec{p}_{m+1}\\ \vec{d}_B^{(2)}=-d_{m+1}^{(2)}\mathbf{B}^{-1}\vec{p}_{m+1} \end{cases}$$ 若$d_{m+1}^{(1)}$为$0$,则$\vec{d}_B^{(1)}=\vec{0}$不是方向,故$d_{m+1}^{(1)}\neq 0$,同理$d_{m+1}^{(2)}\neq 0$,所以有$\vec{d}^{(1)}=\frac{d_{m+1}^{(1)}}{d_{m+1}^{(2)}}\vec{d}^{(2)}$,故$\vec{d}$是极方向。 “$\Rightarrow$”。不妨设$d_{m+1}>0$。若$k=1$,则$d_{m+1}\vec{p}_{m+1}=0$,$\vec{p}_{m+1}=\vec{0}$,结论成立。若$k>1$,设$d_1,d_2,\cdots,d_{k-1},d_{m+1}>0$,可以知道$\vec{p}_1,\vec{p}_2,\dots,\vec{p}_{k-1},\vec{p}_{m+1}$线性相关,故对应的列向量组秩$<k$。假设$\vec{p}_1,\vec{p}_2,\dots,\vec{p}_{k-1}$线性相关,则存在不全为零的$y_1,y_2,\cdots,y_{k-1}\in\mathbb{R}$,满足$\sum\limits_{j=1}^{k-1}y_j\vec{p}_j=\vec{0}$,取$\lambda$满足$0<\lambda<\min{|\frac{\vec{x}_i^{(0)}}{y_i}|\mid y_i\neq 0,0\leq i\leq k-1}$,令: $$\vec{d}_j^{(1)}=\begin{cases} d_j^{(0)}+\lambda y_j,&1\leq j\leq k-1\\ d_{m+1},&j=m+1\\ 0,&k\leq j\leq m\lor j\geq m+2 \end{cases} \;\;\;\; \vec{d}_j^{(2)}=\begin{cases} d_j^{(0)}-\lambda y_j,&1\leq j\leq k-1\\ d_{m+1},&j=m+1\\ 0,&k\leq j\leq m\lor j\geq m+2 \end{cases}$$ 则我们有$\mathbf{A}\vec{d}^{(1)}=\mathbf{A}\vec{d}^{(2)}=\vec{0}$,且$\vec{d}^{(1)},\vec{d}^{(2)}\geq \vec{0}$,且$\vec{d}^{(1)}\neq\vec{0}\land\vec{d}^{(2)}\neq\vec{0}$。由于$\vec{d}^{(1)}\neq\vec{d}^{(2)}$,且$\vec{d}=\frac{1}{2}\vec{d}^{(1)}+\frac{1}{2}\vec{d}^{(2)}$,所以$\vec{d}$不是极方向,故对应的列向量组秩$=k-1$。 基本可行解的存在问题 定理2.2.4 如果$\mathbf{A}\vec{x}=\vec{b}, \vec{x}\geq\vec{0}$有可行解,则一定存在基本可行解。 证:证明思路是不断构造更多$0$分量的可行解,直到可行解对应的向量组线性无关。设$A=[\vec{p}_1,\vec{p}_2,\cdots,\vec{p}_n]$,$\vec{x}=[x_1,\cdots,x_s,0,\cdots,0]^T$是一个可行解,且$x_j>0,j=1,\cdots,s$。若$\vec{p}_1,\vec{p}_2,\cdots,\vec{p}_s$线性无关,则由引理,$\vec{x}$为基本可行解。若$\vec{p}_1,\vec{p}_2,\cdots,\vec{p}_s$线性相关,则存在不全为$0$,且至少有一个正数的$y_1,\cdots,y_s$(如果都是负的,等式同时取相反数即可),满足: $$\sum_{j=1}^sy_j\vec{p}_j=\vec{0}$$ 取$\lambda=\min{\frac{x_j}{y_j}\mid y_j>0}=\frac{x_k}{y_k}$,定义$x_j’=\begin{cases}x_j-\lambda y_j,&j=1,2,\cdots,s\\0,&j=1,\cdots,n\end{cases}$。则我们有$x_j’\geq 0, (j=1,\cdots,n)$且$x_k’ = 0$。$\mathbf{A}\vec{x}’=\vec{b}$,故$\vec{x}’$是可行解,且正分量减少了,不断重复这个步骤,就可以获得基本可行解。 定理 若LP有最优解,则存在一个基本可行解是最优解。定理2.2.2与定理2.2.4的推论。 定理 若LP问题有最优解,则要么最优解唯一,要么有无 穷多最优解。 证明思路是如果存在两个不同的最优解,它们的正线性组合也是最优解。

2020/9/20
articleCard.readMore

最优化理论与算法第1章:引言

本文的内容主要来自陈宝林的《最优化理论与算法(第2版)》第1章《引言》。 学科简述 (略) 线性与非线性规划问题 目标函数和约束函数都是线性的,称为线性规划问题,若含有非线性函数,称为非线性规划问题。 满足约束条件的点称为可行点,全体可行点组成的集合称为可行集或可行域。如果可行域是整个空间称为无约束问题。 定义1.2.1 $f:\mathbb{R}^n\rightarrow\mathbb{R}$为目标函数,$S$为可行域,$\vec{x}’\in S$,若$\forall \vec{x}(\vec{x}\in S\rightarrow f(\vec{x})\geq f(\vec{x}’))$,则称$\vec{x}’$为$f$在$S$上的全局极小点。 定义1.2.2 $f:\mathbb{R}^n\rightarrow\mathbb{R}$为目标函数,$S$为可行域,$\vec{x}’\in S$,若$\exists\epsilon(\epsilon\in\mathbb{R}^+\land\forall \vec{x}(\vec{x}\in N(\vec{x}’,\epsilon)\rightarrow f(\vec{x})\geq f(\vec{x}’)))$,则称$\vec{x}’$为$f$在$S$上的局部极小点。其中$N(\vec{x}’,\epsilon)={\vec{x}\mid|\vec{x}-\vec{x}’|<\epsilon}$为邻域。 注:全局极小点不需要用到距离和范数,它和函数的最值定义几乎是一样的,只是定义域成了可行域。而局部极小点用到了邻域,需要距离也就是范数,它和函数的极小值定义也几乎是一样的。 几个数学概念 向量范数和矩阵范数 定义1.3.1 实值函数$|\cdot|:\mathbb{R}^n\rightarrow\mathbb{R}$称为向量范数,若满足$\forall\alpha,\vec{x},\vec{y}(\alpha\in\mathbf{R}\land \vec{x},\vec{y}\in\mathbf{R}^n)$: 严格正定性:$|\vec{x}|\geq 0\land(|\vec{x}|=0\leftrightarrow\vec{x}=\vec{0})$; 齐次性(线性形):$|\alpha\vec{x}|=|\alpha|\cdot|\vec{x}|$ 三角不等式:$|\vec{x}+\vec{y}|\leq|\vec{x}|+|\vec{y}|$ 常见的向量范数有: $L_1$范数:$|\vec{x}|_1=\sum\limits_{j=1}^n |x_j|$ $L_2$范数:$|\vec{x}|_2=(\sum\limits_{j=1}^n x_j^2)^\frac{1}{2}$ $L_\infty$范数:$|\vec{x}|_\infty=\max\limits_j |x_j|$ 注:这里的范数都称为$L_p$-范数,来源于$|\vec{x}|_p=(\sum\limits_{j=1}^n |x_j|^p)^\frac{1}{p}$。其中$|\vec{x}|_\infty=\lim\limits_{p\rightarrow+\infty}(\sum\limits_{j=1}^n |x_j|^p)^\frac{1}{p}$。 定义1.3.2 $|\cdot|_\alpha,|\cdot|_\beta$为$\mathbb{R}^n$上的范数,若$\exists c_1, c_2(c_1, c_2\in\mathbb{R}^+\land\forall\vec{x}(\vec{x}\in\mathbb{R}^n\rightarrow c_1|\vec{x}|_\alpha\leq |\vec{x}|_\beta\leq c_2|\vec{x}|_\alpha))$,则称这两个范数等价。 $\mathbb{R}^n$中任何两个范数等价(这里必须是有限维,这个定理的证明我尚不能掌握,需要参考泛函分析)。 定义1.3.3 $\mathbf{A}\in\mathbb{R}^{m\times n}$为矩阵,$|\cdot|_\alpha$为$\mathbb{R}^m$上的向量范数,$|\cdot|_\beta$为$\mathbb{R}^n$上的向量范数,定义矩阵范数$|\mathbf{A}|=\max\limits_{|\vec{x}|_\beta=1}|\mathbf{A}\vec{x}|_\alpha$。 注:这里可以想像成矩阵的范数,是其对应的线性映射,能够将一个向量拉长的最大倍数。 定理1.3.1 矩阵范数的性质: $|\mathbf{A}\vec{x}|_\alpha\leq|\mathbf{A}|\cdot|\vec{x}|_\beta$; $|\lambda\mathbf{A}|=|\lambda|\cdot|\mathbf{A}|$; $|\mathbf{A}+\mathbf{B}|\leq|\mathbf{A}|+|\mathbf{B}|$; $|\mathbf{A}\mathbf{D}|\leq|\mathbf{A}|\cdot|\mathbf{D}|$。 定理1.3.1 (1) 证: $$ \begin{aligned} &|\mathbf{A}|=\max\limits_{|\vec{x}|_\beta=1}|\mathbf{A}\vec{x}|_\alpha \\ \Rightarrow &|\mathbf{A}|=\max\limits_{|\frac{\vec{x}}{|\vec{x}|}_\beta|_\beta=1}|\mathbf{A}\frac{\vec{x}}{|\vec{x}|}_\beta|_\alpha;(\text{变量代换}) \\ \Rightarrow &|\mathbf{A}|=\max\frac{|\mathbf{A}\vec{x}|_\alpha}{|\vec{x}|_\beta};(\text{利用齐次性,并去掉了恒成立的max条}) \\ \Rightarrow &|\mathbf{A}|\geq\frac{|\mathbf{A}\vec{x}|_\alpha}{|\vec{x}|_\beta} \end{aligned} $$ 定理1.3.1 (3) 证: $$ \begin{aligned} |\mathbf{A}+\mathbf{B}|&=\max\limits_{|\vec{x}|_\beta=1}|(\mathbf{A}+\mathbf{B})\vec{x}|_\alpha \\ &\leq\max\limits_{|\vec{x}|_\beta=1}|\mathbf{A}\vec{x}|_\alpha + |\mathbf{B}\vec{x}|_\alpha;(\text{三角不等式}) \\ &=|\mathbf{A}|+|\mathbf{B}| \end{aligned} $$ 定理1.3.1 (4) 证: $$ \begin{aligned} |\mathbf{A}\mathbf{D}|&=\max\limits_{|\vec{x}|_\beta=1}|\mathbf{A}\mathbf{D}\vec{x}|_\alpha \\ &\leq\max\limits_{|\vec{x}|_\beta=1}|\mathbf{A}|\cdot|\mathbf{D}\vec{x}|_\gamma;(\text{性质1}) \\ &=|\mathbf{A}|\cdot|\mathbf{D}| \end{aligned} $$ 常见的矩阵范数有: $|\mathbf{A}|_1=\max_j\sum\limits_{i=1}^m |a_{ij}|$; $|\mathbf{A}|_2=\sqrt[]{\lambda_{\mathbf{A}^T\mathbf{A}}}$,($\lambda$是最大特征值,这被称为谱范数); $|\mathbf{A}|_\infty=\max_i\sum\limits_{j=1}^n |a_{ij}|$。 注:这里$p$矩阵范数就是$L_p$向量范数的组合,我采用形象化的方式来解释这3种范数。先考虑第2个范数,它将球拉伸成椭球,长径拉伸最大值也就是其最大的奇异值;再考虑第1个范数,它将一个立方体拉伸成长方体,其中立方体的顶点是$[0,\dots,0,1,0,\dots,0]^T$,映射完后,相当于挨个取出列向量,求他们的$L_1$范数(求和),再取出最大的;最后考虑第3个范数,它同样将一个立方体拉伸成长方体,其中立方体的顶点是$[1,\dots,1]^T$,映射完后,相当于将行向量求和,最后取出最大的。 序列的极限 定义1.3.4 ${\vec{x}^{(k)}}$是$\mathbb{R}^n$中的向量序列,$\vec{x}’\in\mathbb{R}^n$,若: $$\forall\epsilon(\epsilon\in\mathbb{R}^+\rightarrow\exists N(N\in\mathbb{N}^+\land\forall n(n\in\mathbb{N}^+\land n>N\rightarrow |\vec{x}^{(n)}-\vec{x}’|<\epsilon))$$ 则称序列收敛到$\vec{x}’$,或序列以$\vec{x}’$为极限,记作$\lim\limits_{k\to\infty}\vec{x}^{(k)}=\vec{x}’$。 序列若存在极限,则任何子序列有相同的极限(选取适当的$N$即可证明),序列的极限是唯一的。 序列的极限是唯一的 证: 反证法,若存在两极限,则$\lim\limits_{k\to\infty}\vec{x}^{(k)}=\vec{a}$同时$\lim\limits_{k\to\infty}\vec{x}^{(k)}=\vec{b}$,且$\vec{a}\neq\vec{b}$。取$\epsilon_0=\frac{|\vec{b}-\vec{a}|}{2}$,由假设知道: $$ \begin{aligned} &\begin{aligned} &\exists N_1(N_1\in\mathbf{N}^+\land\forall n(n\in\mathbb{N}^+\land n>N_1\rightarrow |\vec{x}^{(n)}-\vec{a}|<\epsilon_0))) \\ \land &\exists N_2(N_2\in\mathbf{N}^+\land\forall n(n\in\mathbb{N}^+\land n>N_2\rightarrow |\vec{x}^{(n)}-\vec{b}|<\epsilon_0))) \end{aligned} \\ \Rightarrow & \exists N_1,N_2(N_1,N_2\in\mathbf{N}^+\land \forall n (n\in\mathbb{N}^+\land n>N_1\land n>N_2\rightarrow |\vec{x}^{(n)}-\vec{a}|<\epsilon_0 \land |\vec{x}^{(n)}-\vec{b}|<\epsilon_0)) \\ \Rightarrow & \exists N_1,N_2(N_1,N_2\in\mathbf{N}^+\land \forall n (n\in\mathbb{N}^+\land n>\max(N_1,N_2)\rightarrow |\vec{x}^{(n)}-\vec{a}| + |\vec{x}^{(n)}-\vec{b}|<|\vec{b}-\vec{a}|)) \\ \Rightarrow & \bot ;(\text{违反三角不等式}) \end{aligned} $$ 定义1.3.5 ${\vec{x}^{(k)}}$是$\mathbb{R}^n$中的向量序列,若存在子序列${\vec{x}^{(k_j)}}$,$\lim\limits_{k_j\to\infty}\vec{x}^{(k_j)}=\hat{\vec{x}}$,则称$\hat{\vec{x}}$是${\vec{x}^{(k)}}$的一个聚点。 Bolzano–Weierstrass定理:$\mathbb{R}^n$中有界序列必有聚点,即有界序列必有收敛子列,证明详见波尔查诺-魏尔斯特拉斯定理 - 维基百科,自由的百科全书。这只在有限维实向量赋范空间成立。 定义1.3.6 ${\vec{x}^{(k)}}$是$\mathbb{R}^n$中的向量序列,若: $$\forall\epsilon(\epsilon\in\mathbb{R}^+\rightarrow\exists N(N\in\mathbb{N}^+\land\forall n_1,n_2(n_1,n_2\in\mathbb{N}^+\land n_1,n_2>N\rightarrow|\vec{x}^{n_1}-\vec{x}^{n_2}|<\epsilon)))$$ 则称${\vec{x}^{(k)}}$为柯西序列。$\mathbb{R}^n$中,柯西序列和收敛数学互为充要条件。 注:柯西序列的定义没有涉及到极限值,这便于完成一些对收敛性的证明。那些所有柯西序列收敛到空间中某点的空间,称为完备空间,比如实数是完备的,有理数是不完备的。完备空间中,柯西序列和收敛序列互为充要条件。不完备空间中,柯西序列是收敛序列的必要条件。 定理1.3.2 ${\vec{x}^{(k)}}$是$\mathbb{R}^n$中的柯西序列,则其聚点为极限点。 注:$\mathbb{R}^n$中,收敛序列(即柯西序列)必有唯一聚点,且该聚点为极限点。一个序列可能有多个聚点。 设$S\subseteq\mathbb{R}^n$: 若$S$中每个收敛序列的极限均在$S$,则称$S$为闭集; 若$\forall\hat{\vec{x}}(\hat{\vec{x}}\in S\rightarrow\exists\epsilon(\epsilon\in\mathbb{R}^+\land N(\hat{\vec{x}},\epsilon)\subseteq S))$,则称$S$为开集,这样的$\hat{\vec{x}}$称为内点; 若$S$是有界闭集,则称$S$为紧集。 闭集的另一个定义是:一个补集是开集的集合称为闭集。这里集合$S$的补集定义为$S^C={x\mid x\notin S,x\in\mathbb{N}}$。 紧集的一个等价定义是:集合中的序列都有收敛子序列。由Bolzano–Weierstrass定理可证$S\subseteq\mathbb{R}^n$紧致,当且仅当,$S$为有界闭集。 梯度、海森矩阵、泰勒展开式 设$f:S\to\mathbb{R}$,其中$S\subseteq\mathbb{R}^n$。如果$f$在$S$中的每一点连续,则称$f$在$S$上连续,记作$f\in C(S)$。若$f$在每一点$\vec{x}\in S$,一阶导数$\frac{\partial f(\vec{x})}{\partial x_i}$存在且连续,则称$f$在$S$上连续可微,记作$f\in C^1(S)$。若$f$在每一点$\vec{x}\in S$,二阶导数$\frac{\partial^2 f(\vec{x})}{\partial x_i\partial x_j}$存在且连续,则称$f$在$S$上二次连续可微,记作$f\in C^2(S)$。 函数$f\in C^1(S)$在$\vec{x}$的梯度为: $$\nabla f(\vec{x})=\begin{bmatrix} \frac{\partial f(\vec{x})}{\partial x_1},\frac{\partial f(\vec{x})}{\partial x_2},\cdots,\frac{\partial f(\vec{x})}{\partial x_n} \end{bmatrix}^T$$ 函数$f\in C^2(S)$在$\vec{x}$的海森矩阵为对称矩阵,如下: $$[\nabla^2 f(\vec{x})]_{ij}=\frac{\partial^2 f(\vec{x})}{\partial x_i\partial x_j}$$ 对于二次函数$f(\vec{x})=\frac{1}{2}\vec{x}^T\mathbf{A}\vec{x}+\vec{b}^T\vec{x}+c$: 其梯度:$\nabla f(\vec{x})=\mathbf{A}\vec{x} + \vec{b}$; 其海森矩阵:$\nabla^2 f(\vec{x})=\mathbf{A}$。 对$S\in\mathbb{R}^n$上的函数$f:S\to\mathbb{R}$: 若$f\in C^1(S)$,则对任意$\vec{x}_0\in\mathbb{R}^n$,有一阶泰勒展式: $$f(\vec{x})=f(\vec{x_0})+\nabla f(\vec{x}_0)^T(\vec{x}-\vec{x}_0)+o(|\vec{x}-\vec{x}_0|)$$ 若$f\in C^2(S)$,则对任意$\vec{x}_0\in\mathbb{R}^n$,有二阶泰勒展式: $$f(\vec{x})=f(\vec{x_0})+\nabla f(\vec{x}_0)^T(\vec{x}-\vec{x}_0)+\frac{1}{2}(\vec{x}-\vec{x}_0)^T\nabla^2 f(\vec{x}_0)(\vec{x}-\vec{x}_0)+o(|\vec{x}-\vec{x}_0|^2)$$ 雅可比矩阵、链式法则和隐函数存在定理 雅可比矩阵 考虑向量值函数$\vec{h}:\mathbb{R}^n\to\mathbb{R}^m$: $$\vec{h}(\vec{x})=(h_1(\vec{x}),h_2(\vec{x}),\cdots,h_m(\vec{x}))^T$$ 若对任意$i,j$,$\frac{\partial h_i(\vec{x})}{\partial x_j}$存在,则其雅可比矩阵为: $$[\vec{h}’(\vec{x})]_{ij}=\frac{\partial h_i(\vec{x})}{\partial x_j}$$ 也可记作$\nabla\vec{h}(\vec{x})^T$。 链式法则 对于实数空间上的复合向量值函数$\vec{h}(\vec{x})=\vec{f}(\vec{g}(\vec{x}))$,若$f,g$均可微,则有: $$\vec{h}’(\vec{x})=\vec{h}’(\vec{g}(\vec{x}))\vec{g}’(\vec{x})$$ 隐函数定理 定理 1.3.3 对于$\vec{h}:\mathbb{R}^m\times\mathbb{R}^n\to\mathbb{R}^m$,定点向量$\vec{a}^{(0)}\in\mathbb{R}^m,\vec{b}^{(0)}\in\mathbb{R}^n$,令$\vec{x}^{(0)}=[\vec{a}^{(0)},|,\vec{b}^{(0)}]$,满足: $\vec{h}(\vec{x}^{(0)})=\vec{0}$; 在$\vec{x}^{(0)}$的某一个邻域中,$h_i\in C^1 (i=1,\cdots,m)$; $\begin{vmatrix}\begin{bmatrix}\frac{\partial h_i}{\partial a_j}(\vec{x})\end{bmatrix}_{m\times m}\end{vmatrix}\neq 0$ 则存在$\vec{b}^{(0)}\in\mathbb{R}^n$的一个邻域,使得对于邻域中的点$\vec{b}\in\mathbb{R}^n$,唯一存在函数$\vec{\phi}:\mathbb{R}^n\to\mathbb{R}^m$,满足: $\phi_i\in C^1;(i=1,\cdots,m)$; $\vec{a}^{(0)}=\phi(\vec{b}^{(0)})$; $\vec{h}([\vec{\phi}(\vec{b}^{(0)}),|,\vec{b}^{(0)}])=\vec{0}$。 二次型的正定性与半正定性 正定二次型的定义 对实二次型$f(\vec{x})=\vec{x}^T\mathbf{A}\vec{x}$,若: $$\forall\vec{x}(\vec{x}\in\mathbb{R}^n\land\vec{x}\neq\vec{0}\rightarrow f(\vec{x})\geq 0)$$ 则称$f(x)$为正定二次型,$\mathbf{A}$为正定矩阵。 正定二次型的判断 对于$\mathbf{A}\in\mathbb{R}^{n\times n}$,下列命题等价: $\vec{x}^T\mathbf{A}\vec{x}$是正定二次型; $\mathbf{A}$是正定矩阵; $\mathbf{A}$的$n$个顺序主子式都大于零; $\mathbf{A}$的$n$个特征值都大于零; 存在可逆矩阵$\mathbf{P}$,使得$A=P^TP$。 半正定二次型的定义 对实二次型$f(\vec{x})=\vec{x}^T\mathbf{A}\vec{x}$,若: $$\forall\vec{x}(\vec{x}\in\mathbb{R}^n\land\vec{x}\neq\vec{0}\rightarrow f(\vec{x})\geq 0)\land\exists\vec{x}(\vec{x}\in\mathbb{R}^n\land\vec{x}\neq\vec{0}\land f(\vec{x})=0)$$ 则称$f(x)$为半正定二次型,$\mathbf{A}$为半正定矩阵。 半正定二次型的判断 对于$\mathbf{A}\in\mathbb{R}^{n\times n}$,下列命题等价: $\vec{x}^T\mathbf{A}\vec{x}$是半正定二次型; $\mathbf{A}$是半正定矩阵; $\mathbf{A}$的所有主子式都大于等于零,且至少有一个等于零; $\mathbf{A}$的$n$个特征值都大于等于零,且至少有一个等于零。 凸集和凸函数 凸集 定义1.4.1 设$S\subseteq\mathbb{R}^n$,若$\forall\vec{x}_1,\vec{x}_2,\lambda(x_1,x_2\in S\land\lambda\in[0,1]\rightarrow\lambda\vec{x}_1+(1-\lambda)\vec{x}_2\in S)$,则称$S$为凸集。其中$\lambda\vec{x}_1+(1-\lambda)\vec{x}_2$称为凸组合。 常见的凸集有超平面${\vec{x}\mid\vec{p}^T\vec{x}=\alpha}$、半空间${\vec{x}\mid\vec{p}^T\vec{x}\leq\alpha}$、射线${\vec{x}\mid\vec{x}_0+\lambda\vec{d},\lambda\geq 0}$(其中$\vec{x}_0$为顶点,$\vec{d}$为方向向量)。 设$S_1,S_2\subseteq\mathbb{R}^n$为凸集,$\beta\in\mathbb{R}$,则: $\beta S_1={\beta\vec{x}\mid\vec{x}\in S_1}$为凸集; $S_1\cap S_2$为凸集; $S_1+S_2={\vec{x}_1+\vec{x}_2\mid\vec{x}_1\in S_1,\vec{x}_2\in S_2}$为凸集; $S_1-S_2={\vec{x}_1-\vec{x}_2\mid\vec{x}_1\in S_1,\vec{x}_2\in S_2}$为凸集。 凸集的线性组合也是凸集,但凸集的并不是凸集。 定义1.4.2 设$C\subseteq\mathbb{R}^n$,若$\forall\vec{x},\lambda(\vec{x}\in C\land\lambda\in\mathbb{R}^*\rightarrow\lambda\vec{x}\in C)$,则称$C$为锥。若$C$又为凸集,则称$C$为凸锥。 向量集${\vec{x}_1,\vec{x}_2,\cdots,\vec{x}_n}$的非负线性组合${\sum\limits_{i=1}^n\lambda_i\vec{x}_i\mid\lambda_i\geq 0,i=1,\cdots,n}$。 定义1.4.3 有限个半空间的交${\vec{x}\mid\mathbf{A}\vec{x}\leq\vec{b}}$称为多面集,若$\vec{b}=\vec{0}$,则多面集成为凸锥。 定义1.4.4 设$S$为非空凸集,$\vec{x}\in S$,若$\forall\vec{x}_1,\vec{x}_2,\lambda(\vec{x}_1,\vec{x}_2\in S\land\lambda\in(0,1)\land\vec{x}=\lambda\vec{x}_1+(1-\lambda)\vec{x}_2\rightarrow\vec{x}=\vec{x}_1=\vec{x}_2)$,则称$\vec{x}$为凸集$S$的极点。 紧凸集中的点可以表示为极点的线性组合,但对无界集并不成立。 定义1.4.5 设$S\subseteq\mathbb{R}^n$为闭凸集,$\vec{d}\in\mathbb{R}^n$非零,若$\forall\vec{x}(\vec{x}\in S\rightarrow{\vec{x}+\lambda\vec{d}\mid\lambda\in\mathbb{R}^+}\subseteq S)$,则称$\vec{d}$为$S$的方向。又设$\vec{d}_1,\vec{d}_2$是$S$的两个方向,若$\forall\lambda(\lambda\in\mathbb{R}^+\rightarrow\vec{d}_1\neq\lambda\vec{d}_2)$,则称$\vec{d}_1,\vec{d}_2$是不同的方向。若$S$的方向$\vec{d}$不能表示成两个不同方向的正的线性组合,则称$\vec{d}$是$S$的极方向。 有界集不存在方向和极方向。 考虑平面直角坐标系中的半平面$x\geq \alpha$,可以发现有3个极方向,分别是$(0, \lambda_+)^T,(0, \lambda_-)^T,(\lambda_+, \lambda)^T$,且不存在极点。 特别地,对于$S={x\mid\mathbf{A}\vec{x}=\vec{b},\vec{x}\geq\vec{0}}$,$\vec{d}$为$S$的方向等价于$\vec{d}\geq\vec{0}\land\mathbf{A}\vec{d}=\vec{0}$。 定理1.4.1 表示定理:设$S={x\mid\mathbf{A}\vec{x}=\vec{b},\vec{x}\geq\vec{0}}$为非空多面集(它一定是多面集),则: 极点集非空且为有限个$\vec{x}_1,\cdots,\vec{x}_k$; 极方向为空当且仅当$S$有界,$S$无界,则有有限个极方向$\vec{d}_1,\cdots,\vec{d}_l$; $\vec{x}\in S$当且仅当: $$\begin{aligned}&\vec{x}=\sum_{j=1}^k\lambda_j\vec{x}_{j}+\sum_{j=1}^l\mu_j\vec{d}_j,\\ &\sum_{j=1}^k\lambda_j=1,\\ &\lambda_j\geq 0, j=1,\cdots,k,\\ &\mu_j\geq 0, j=1,\cdots,l. \end{aligned}$$ 凸集分离定理 (暂不收录) 凸函数 (暂不收录) 凸函数的判别 (暂不收录) 凸规划 (暂不收录)

2020/9/18
articleCard.readMore

全自动清华刷课脚本

清华研究生的选课如今(2020年)还没有采用Waiting List机制,这就使得刷课脚本成为可能,然而验证码一直以来是困扰刷课脚本的最大难题。为了解决验证码,我独立提出了一个模型(我觉得肯定被提出过了,只是没有查阅文献),在朋友的帮助下完成了数据集的采集和标注,并达到了90%以上的准确率,实现了全自动清华刷课。项目在这里。 本人设计的刷课脚本仅供学习用途,对使用时造成的损失不负任何责任。项目以GNU GPLv3协议发布,这意味着基于此项目的项目必须以同样的协议发布并开源。项目真的很不容易,写了1k多行的代码,还标注了4000个样本,这占用了我一定的精力。希望大家能用star的方式支持我的项目。 如何使用 首先你需要安装Python3。然后依次执行下面的命令,准备仓库。项目没有在Windows上测试过,但应该可以运行,欢迎PR来补充Windows的文档,Windows用户请使用第二条pip install指令。 #### Clone Repo git clone https://github.com/sunziping2016/THUCourseSpider.git # git clone git@github.com:sunziping2016/THUCourseSpider.git cd THUCourseSpider #### Create venv (Optional) # python -m venv venv # source venv/bin/activate #### Install dependecies pip install -r requirements.txt # pip install -r requirements-win.txt 接下来的步骤可以分为3步: 搜集并标注数据(可使用repo里已标注好的captcha.tgz); 训练模型(可使用我训练好的模型,已经对外开放),在我的RTX 2070 Super上需要约20分钟; 运行刷课脚本。 对于想使用训练好的模型用户,可以从看使用训练好的模型和运行刷课脚本。如果需要自己训练模型,可以看使用已标注好数据、生成用于训练数据集、训练模型和运行刷课脚本。对于实在是想知道标数据是什么样的体验的用户,通读整个第1章即可。 本项目的程序大多有命令行参数可供调整,加上--help参数即可查看帮助。 搜集并标注数据 原始数据集存放的目录是captcha。验证码被下下来并确认标记答案正确后,会存放到captcha/<md5hash>.<code>.jpeg。其中<md5hash>是图片的md5散列值,<code>是正确答案。 我们需要对一部分原始图片标记位置,以便更好地对分类网络进行预训练。没有预训练整个网络基本无法工作,所以标记位置是很重要的。位置信息会被存储在segmentation.txt。其内容大致如下: 000a5b757fe41963b3a3d503d3de4640.W2FP.jpeg:70,101,121,142 0019fa074e8d50b0727a570407c9f411.4QFT9.jpeg:57,77,101,123,148 00253474dbe8159a9a05720654d4a6e3.XWF8.jpeg:67,92,119,139 00362bf4a054de99b4f4db7393ea33c1.GXXP8.jpeg:55,76,100,126,150 006b4ce5e73d0a9f506002d71a3a9d71.6EHG.jpeg:66,89,110,138 00912fe666cd2b31102aebd3f8f08041.MT2V9.jpeg:53,77,103,121,151 00bebc089ae21814743963b8d0ec3422.Q9X4J.jpeg:56,80,104,130,150 01231260ea359c43877f654e8ab0e955.KH8P6.jpeg:58,80,104,126,145 01349ce3182ab1d608d4776ed4b79ac1.WQGBM.jpeg:50,76,110,132,152 013bb3f01ad530494837b6bf1b8af957.YK6H.jpeg:66,92,115,137 每一行是<filename>:<pos1>,<pos2>,...,<posN>,其中<filename>是文件名,<pos1>都<posN>是字符中心位置的x坐标值。一般而言,文件的行是按照文件名排序,x坐标值也是排好序的。 使用已标注好数据 解压captcha.tgz(.tar.gz格式)到当前目录就可以使用先前标注好的数据。 tar xzvf captcha.tgz 目前包含4000张已经标记了答案的验证码,其中2004张还标记了位置。 搜集并标记验证码答案 首先,标记验证码需要登录选课系统,所以你可以将config.example.json拷贝为config.json。然后编辑config.json,修改其中的用户名和密码。 而后运行: ./label-captcha.py 就可以开始标记验证码,它会提示你需要输入验证码。验证码会被下载到captcha.jpeg文件中。输入的时候是不区分大小写的。 合并两个人标记的验证码是很简单的。只需要把captcha/*.jpeg文件拷贝一下就行。注意位置信息,即segmentation.txt不能这样合并,如果需要合并的包含位置信息,见下一节。 标记验证码位置 运行: ./segment-captcha.py 会弹出一个窗口,如下: 鼠标点击图中可以标记一个位置,可以点住移动,再松开。单击已有的线会取消,拖拽已有的线会将它置于新的位置。<<表示寻找上一个没有完成标注的,>>同理,它们会循环,对应的快捷键是ctrl+方向键。<是上一张图,>是下一张图。所有的更改只有按了Ok(或者回车)才会保存,然后它会切到下一张图(相当于按了>>)。Prog表示现在共4000张图,2004张已标记,Idx是第几张图,Code是答案。正常的流程是打开软件,按>>切到第一未标记的张图,鼠标点击中心,然后回车,切到下张图,循环往复。 深度学习辅助的标记验证码答案 当训练好神经网络之后,标图就更加方便了。运行: ./label-captcha-gui.py # ./label-captcha-gui.py --generator_save_path ./save/generator-full 如果你使用的是训练好的模型,请执行第2行命令。它会弹出一个窗口,如下: 默认模式下会载入神经网络,然后只需要在输入框中修改错误的验证码即可。点击Ok(或者回车)切换下一张验证码。 生成用于训练数据集 当数据集准备好了。即有大量的数据被标注好答案,并且大部分的数据都被标注好位置后,就可以生成用于训练数据集。它会切割图片并且切分数据集,运行: ./prepare-dataset.py 生成的用于训练的数据集位于dataset,大致内容如下: dataset ├── segmented 将字符切割出来的数据集,用于训练分类器,需要被标注位置信息 │   ├── all 既包含训练又包含测试的数据集,用于训练最终给用户的模型 │   │   ├── + 包含空白或者两个字符之间的图片 │   │   │   ├── 000a5b757fe41963b3a3d503d3de4640.W2FP.158.jpeg 文件名.答案.中心位置.jpeg │   │   │   ... │   │   ├── 2 包含字符2的图片,下同 │   │   ├── 3 │   │   ... │   ├── test 测试集 │   │   ├── + │   │   ... │   └── train 训练集 │   │   ├── + │   │   ... └── whole 完整图片的数据集,只切去了预设定的图片留白边框,用于训练生成器 ├── all │   ├── 000a5b757fe41963b3a3d503d3de4640.W2FP.jpeg 文件名.答案.jpeg │   ... ├── test └── train 训练模型 模型的训练分为两步: 训练分类器:对单个字符的图片进行分类,用的是CNN; 训练生成器:讲分类器从原图中滑过提取出的特征,输入到RNN decoder中,生成答案。 实验表明,省去步骤一,直接用步骤二是不太能训练出好的模型的,我个人觉得那需要很强大的算力。实验也表明,提高分类器的准确度能比改进生成器提升得更快。此外,实验还表明,步骤二中似乎不把梯度回传给分类器特征提取部分,效果会更好,但差别不显著。 使用训练好的模型 预训练好的模型存放在这里,文件大小为752.3MB不再提供。下载后建议放置于save/generator-full/epoch.0050.pth这里,路径如果没有的话请手动创建。而后运行刷课脚本的时候,需要加入额外的参数--generator_save_path save/generator-full。 训练分类器 ./classifier.py # ./classifier.py --gpu 0,1 运行上面的命令即可。注意如果你有GPU,请务必加上--gpu <id1>,<id2>,...,以加速训练。运行完毕后,save/classifier目录下会多出epoch.<epoch>.pth文件,那是保存的模型,还会有running-log.json,里面是训练时长、参数之类的信息。 此外训练的数据,如loss、accuracy都保存进了TensorBoard。运行下面的命令即可查看: tensorboard --logdir runs 训练生成器 ./generator.py # ./classifier.py --gpu 0,1 运行上面的命令即可。需要注意的点同上。只是保存的目录默认是save/generator,此外目录里还会有predicates.<epoch>.txt文件,是模型在测试集上预测出来的验证码值,按文件名排序。 运行刷课脚本 首先,请确认你准备好了config.json文件,它包含了你的用户名和密码。你可以将config.example.json拷贝为config.json。然后编辑config.json,修改其中的用户名和密码。 然后,请你准备好courses.csv,它包含了你想要抢课的信息。你可以将courses.example.csv拷贝为courses.csv。然后编辑courses.csv。这里示例里给出的是2020年秋季学期学位课的两门英语课。一般情况下,你需要修改学年学期Semester、课程号Course Number和课序号Course ID。 ./graduate-crawler.py # ./graduate-crawler.py --generator_save_path ./save/generator-full 如果你使用的是预训练好的模型,请使用第2行。你也可以加入--verbose查看额外的信息,默认情况只有尝试申请课程的时候才会打印输出。 设计细节 神经网络结构 首先,我利用切割出来的图片训练一个分类器,这里切割出来的图片是从左到右滑动窗口取出来的。如果滑动窗口的中心距离某个字符位置中心很近,这个图片的标签就是这个字符;否则这个图片会被标记为不包含字符,我们用+这个标签表示。可以注意到,并不是所有的数字和大写英文字母都会出现在验证码中,最终分类器是25分类。 分类器由两部分组成: 由卷积组成的特征提取部分; 由全连层组成的分类部分。 对于我们而言,最重要的是特征提取部分。 接着我们构建一个生成器,生成器类似于一个Seq2Seq模型,只是差别在于它的Encoder不是一个处理变长序列的RNN(GRU),而是处理定长输入的全连层。全连层将一个分类器特征提取出的所有特征连接在一起,映射到RNN的初始隐藏状态。RNN的输入是上一次产生的Token,输入的第一个Token是一个特殊的SOS(Start-Of-Sentence)字符。最后训练生成器的时候,我不训练分类器特征提取部分,这样似乎更好。

2020/9/12
articleCard.readMore

更安全的编程语言(幻灯片)

这是个幻灯片,用以介绍编程语言如何被设计成更安全、更抽象、更易于进行程序分析的。请移步幻灯片。

2020/9/5
articleCard.readMore

更安全的编程语言(幻灯片)

更安全的程序设计语言 ——从常见漏洞的角度浅谈编程语言设计 By 周旻 副研究员 & 孙子平 本幻灯片在CC BY 4.0下提供,转载请注明来自https://szp.io。本幻灯片包含大量的个人观点,对错误的内容造成的损失,概不负责。 1 目录 目录 错误使用空值 未初始化数据 整数溢出 内存及其他资源错误 异常安全 数据竞争 类型安全 最后的建议 2 错误使用空值 This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years. – Tony Hoare, inventor of ALGOL W. 某些语言允许空值作为某些类型的一种特殊状态存在: C/C++:指针(NULL/nullptr) Java:对象类型(null) Python:None JavaScript:null和undefined 空值带来了便利:没有值的时候可以先赋值为空;同时也是危险的:它不强制程序员检查非空,导致运行时才发现。 2.1 空值的例子和可能解决方案 x = *p; // do something if (p == NULL) { // and then do something } 上面是一个简单的空值漏洞的例子:代码的顺序出现了错误,即先使用后检查。 如果能强制程序员检查空值,就能提醒它犯了这种错误。 (续) 如何解决空值的问题? 强大的编程技巧——不保险 静态分析工具的支持——不完备(可能有疏漏) 编程语言层面的支持✔️ 2.2 使用可空类型避免空值错误 借助完善的类型系统: 不允许普通类型出现空值 使用可空类型强制程序员处理可空或断言非空 例如: C++:std::optional(C++17)替代空指针表示可空类型 Java:java.util.Optional(由于允许对象类型可空,形同虚设) Python:typing.Optional(由于类型检查很弱,形同虚设) TypeScript、Haskell:采用和类型(Union类型或代数数据类型) 可空类型是如何解决这个问题的? 检查之后再断言非空(传统的过程式方式,C/C++、Java): Optional<X> x = ...; if (x.isPresent()) { y = x.get(); } 使用模式匹配(Haskell/Rust),避免了断言: Maybe a = Just a | Nothing f x = case x of Just a => ... Nothing => ... 函数式地进行映射、且、或之类的运算(Haskell/Rust/Java): > fmap (+ 1) (Just 1) -- Maybe, it's a functor! Just 2 3 未初始化数据 未初始化与空值类似,是一个正常类型不该有的状态,解决方案: 不允许未初始化数据:使用构造函数,C++(甚至初始化列表) 使用控制流分析:如Rust 一些语言采用一些特殊的值初始化了对象(如Java的null),这叫饮鸩止渴。 3.1 一个使用未初始化数据的例子 下面是C语言的一个使用未初始化数据的例子。这是由于忘记了赋予初值,这种错误通常很容易犯。 int i, counter; for(i = 0; i < 10; ++i) counter += i; printf("%d\n", counter) 4 整数溢出 整数溢出很可能是程序异常的行为。为了避免这种情况: 任意精度的整型:Python 运行时检查:C++(clang -fsanitize=undefined),Rust debug模式 $ cat test.c int main(int argc, char *argv[]) { return 0x7fffffff + argc; } $ clang -fsanitize=undefined test.c $ ./a.out test.c:1:54: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int' SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior test.c:1:54 in 5 内存及其他资源错误 不再需要的资源(如堆内存、文件、锁)未释放,就会造成泄露。为了避免这种情况: 自动调用析构函数:C++(RAII范式) 垃圾回收:Java及绝大多数脚本语言(仅能可靠释放堆内存资源) 所有权机制:Rust 对于使用垃圾回收的语言,诸如文件之类的资源是需要手动释放的。于是就有了: 可关闭对象:Java(AutoClosable)、Python(__enter__,__exit__),需要手动调用close()或使用try-with-resources语句 5.1 RAII范式 RAII是资源获得即初始化的简写,指对象的生命周期与资源的获取释放完全一致。这是异常安全的。下面的代码不需要显示释放锁和文件。 void WriteToFile(const std::string& message) { static std::mutex mutex; std::lock_guard<std::mutex> lock(mutex); std::ofstream file("example.txt"); if (!file.is_open()) throw std::runtime_error("unable to open file"); file << message << std::endl; } 来自Resource acquisition is initialization - Wikipedia 5.2 循环引用及垃圾回收 对于C++、Rust之类的语言,可以通过计数有多少引用,来自动释放不再被引用的内存。循环引用对此是致命的,示例如下,所以就引入了弱引用。 struct Person { std::shared_ptr<Person> partner; }; auto lucy = std::make_shared<Person>(); auto ricky = std::make_shared<Person>(); lucy->partner = ricky; ricky->partner = lucy; 主流的垃圾回收方式是使用遍历判断可达性,可以有效解决循环引用问题。 6 异常安全 异常安全,是指当异常发生时: 资源无泄漏 数据具有一致性 即使是在实践了RAII的语言中,这也是一件很困难的事情: int a = new int; foo(); delete a; 上面的代码就会在foo()跑出异常时造成内存泄露。所以在C++中全面使用智能指针是很有必要的。 6.1 Try-With-Resources语句 对于不能实践RAII的垃圾回收语言,通过try-finally来释放资源很容易有遗漏,所以就有了try-with-resources语句。如Python: with open("lol") as f: print(f.read()) 7 数据竞争 数据竞争竞争是并发中很容易出现的问题,加锁可以解决这个问题,但谁能确保不忘呢。为了避免这种情况: 纯函数&不可变数据:Haskell,消除了数据竞争的可能 借用检查:Rust,同样消除了数据竞争的可能 同步原语:锁(互斥锁、读写锁),channel(Go、Rust) 数据竞争的条件: 可能同时出现多次写($\geq 2$) 或,可能同时出现一次写和多次读($\geq 1$) 7.1 Rust的借用检查 Rust只允许一个时刻持有: 要么,一个可写引用 要么,多个可读引用 以上内容是编译时检查的,这对编译器的静态分析提出了更高的要求,也限制了一些可能正确的程序。 8 类型安全 编程语言有如下的分类: 强类型:不容忍隐式类型转换 弱类型:容忍隐式类型转换 静态类型:编译期确定变量类型 动态类型:编译期无法确定变量类型 总的来说,动态语言很容易写出运行时才会报错的代码,所以静态比强是更重要的。当然又强又静态是最好的。 来自弱类型、强类型、动态类型、静态类型语言的区别是什么? - 知乎 8.1 动态类型语言的坏处 下面是一个典型的动态语言Bug,这在静态语言是不会出现的。相信也有不少的人遇到过,Python写了个神经网络,跑了几小时后,崩在保存模型的代码上。 a = [1, 2, 3] print('.'.join(a)) 8.2 弱类型语言的坏处 JavaScript是一门神一般的语言,举几个栗子: [] == ![]; // -> true [6, -2, 2, -7].sort() // -> [-2, -7, 2, 6] null == 0; // -> false null > 0; // -> false null >= 0; // -> true 来自denysdovhan/wtfjs: 🤪 A list of funny and tricky JavaScript examples 8.3 Java数组协变带来的错误 早期Java尚不支持泛型,为了支持对数组的通用操作,引入了数组协变:如果A是B的子类型,那么A[]也是B[]的子类型。然而这又是一个引来麻烦的设计: String[] strings = new String[1]; Object[] objects = strings; objects[0] = 12; 这是运行时错误,所以现代语言大多禁止了数组及其他容器的协变。 8.4 泛型 通过泛型机制,在保证代码正确的基础上,复用代码。我个人将支持泛型的语言分为两类: 无约束泛型:类型变量没有约束,在泛型代码被实例化时检查操作的合法性,如C++ 有约束泛型:类型变量需要实现接口,不需要实例化泛型即可检查错误,编译更快,如Java、TypeScript、Rust C++在20版本中也引入了Concept,但历史包袱很重。现代语言大多采用有约束泛型。 9 最后的建议 如果你在使用C++,那么看看Rust对你会很有帮助,会很香的 如果你在用JavaScript,请转移阵营到TypeScript,也会很香的,你会发现它不断地从bug边缘拯救你 如果你在用Python,那么你可以尝试加上类型注解并用mypy,但体验可能很一般 谢谢大家! 10 被移除的章节 这里包含一些可能过于琐碎和深奥的章节。 10.1 关于控制流分析 (原3.1) 程序分析通常都是不是万能的。例如,loop和while true在rust中是基本等价的,只有下面的不同: 编译成功✔️ let x; loop { x = 1; break; } println!("{}", x) 编译失败❌ let x; while true { x = 1; break; } println!("{}", x) 这是因为编译器不认为while循环一定会被执行一次。 来自rust - What is the difference between loop and while true? - Stack Overflow 10.2 为什么make_unique (原6.2) 考虑下面的C++代码: foo(std::unique_ptr<X>(new X), std::unique_ptr<Y>(new Y)) 由于C++中表达式的求值顺序不定。假设求值顺序是:先执行new X,再执行new Y,最后执行两个unique_ptr的构造函数。此时,如果Y的构造函数抛出了异常,就会出现内存泄漏。所以上述代码应当改为: foo(std::make_unique<X>(), std::make_unique<Y>()) 10.3 Copy-And-Swap范式 (原6.3) 考虑一个C++某类的拷贝赋值函数设计方式: Person& operator=(const Person& that) { if (this != &that) { delete[] name; name = new char[std::strlen(that.name) + 1]; std::strcpy(name, that.name); age = that.age; } return *this; } 当new抛出异常,this所指对象被析构时,就会出现重复释放内存。在拷贝对象时,先销毁旧状态再复制新状态,通常很危险。 (续) 使用Copy-And-Swap模式就能解决这个问题,这需要类实现拷贝构造函数和移动构造函数,后者在C++中不应该抛出异常: Person& operator=(Person that) { std::swap(*this, that); return *this; } .reveal .slides { text-align: left; } .reveal { font-size: 28px; } .reveal blockquote { width: 90%; }

2020/9/5
articleCard.readMore

iptables教程

这篇文章深入地探讨了Linux的防火墙iptables的使用方法。主要内容参考自An In-Depth Guide to iptables, the Linux Firewall - Boolean World。 iptables如何工作 iptabels是netfilter包过滤系统的命令行界面。本文中用iptables统称iptables及netfilter。 iptables有3种结构组成表(table)、链(chain)和目标(target)。表可以被用于指定处理包的方法,默认的表是filter,也有其他的表。 链归属于某个表,链被用于检查匹配一些包,并将它传给一个目标。目标决定了包的命运,例如接受或拒绝。 iptables会在链中逐一匹配规则,如果规则匹配了,就会跳转到对应的目标。否则,改链的默认政策(default policy)会被使用。默认政策也是一个目标。所有链的默认政策默认是接受。 表 在现代Linux中,有四张表: filter:最常见的表,用于过滤包。 mangle:用于该表包的头,如TTL信息。 nat:允许通过修改包的目标和源构建NAT网络,如将内网的服务暴露给公网。 raw:iptables是一个有状态的防火墙。raw表使得你能够在内核追踪状态前,处理一些包。你也可以使某些包免于状态追踪。 除此之外,一些内核有别的表。如SELinux有security表。 链 某个表由几个默认的链组成。它们可以使: PREROUTING:应用于刚从网络上抵达的包,出现在nat、mangle和raw表中。 INPUT:应用于即将抵达本地进程的包,出现在mangle和filter表中。 OUTPUT:应用于本地进程产生的包,出现在raw、mangle、nat和filter表中。 FORWARD:应用于略过主机的包,出现在mangle和filter表中。 -POSTROUTING:应用于即将从网络上离开的包,出现在nat和mangle表中。 目标 一些目标是终结的,也就是说它们立刻决定了包的命运。常见的有: ACCEPT:iptables会接受该包。 DROP:iptables会丢弃包,就好像系统未曾接收到包。 REJECT:iptables会拒绝包,对于TCP会发送connection reset,对于UDP和ICMP会发送destination host unreachable。 另一些目标是非终结的。例如LOG目标,用于输出包的信息到内核日志。 此外你也可以创建自定义的链。 关于iptabels命令的一点说明 有两种网络协议,IPv4和IPv6。这两个协议是不同的,因而iptables为IPv4 提供了iptables命令,为IPv6提供了ip6tables命令。 这两个命令接受的参数差别不大。此外你需要以root身份运行这些命令。 屏蔽IP 屏蔽59.45.175.62发来的包可以采用下面的命令: iptables -t filter -A INPUT -s 59.45.175.62 -j REJECT -t参数指定规则在哪个表中,-A参数是说在哪个链的最后添加。-s是指定源IP,-j是说跳转到哪个目标。 -t filter可以省略,因为filter是默认表。 也可以采用CIDR记号指定源IP,类似-s 59.45.175.0/24。 类似地,屏蔽前往31.13.78.35的包: iptables -A OUTPUT -d 31.13.78.35 -j DROP 列出规则 使用-L开关可以列出某个表中的所有链和规则,使用--line-numbers可以标记行号。你也可以用-t指定显示哪个表。 iptables会对IP采用DNS查询。这通常是不必要,且使命令缓慢。使用-n开关可以组织这个行为。 删除规则 将-A chain命令替换成-D chain命令就可以删除规则。此外,-D chain rulenum还可以接受第二个可选的参数,就是行号,可以用于删除指定行的规则。当你删除规则时,其后的规则行号会减少,所以一般从后往前删除规则。 使用-F [chain]后面跟链名,可以删除该链的所有规则。 插入和替换规则 使用-I chain [rulenum]命令可以插入规则到最开始或者指定的行号,行号从1开始计数。也可以使用-R chain rulenum命令替换掉某个行的规则。 协议和模块 使用-p proto可以指定匹配的协议,如tcp、udp和icmp(对于IPv4)或ipv6-icmp(对于IPv6)。 通过-m match可以加载模块,使得之后的命令可以有额外的匹配选项。如tcp模块提供了--dport可以用于到目标端口的包。一个完整的例子如下: iptables -A INPUT -p tcp -m tcp --dport 22 -s 59.45.175.0/24 -j DROP multiport模块提供了--dports可以同时指定多个逗号分隔的端口。icmp提供了--icmp-type可以指定icmp包的类型。 连接追踪模块 先前屏蔽某些IP的做法会导致自己也无法访问改IP上的服务,因为那些服务返回给你的包也被屏蔽了。 所以我们需要知道包的状态,因而就有了conntrack模块。它有以下的状态: NEW:第一个创建连接的包。 ESTABLISHED:归属于已创建连接的包。 RELATED:与某个连接相关的包,如FTP的数据连接。 INVALID:状态不合法的包,如资源不足造成的。 UNTRACKED:所有在raw表中,跳转到NOTRACK目标的包。 DNAT:目标地址被nat表更改的包。 SNAT:源地址被nat表更改的包。 --ctstate接受逗号分隔的状态列表。如下面的两条规则很适合置于INPUT链的最开始: iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT iptables -A INPUT -m conntrack --ctstate INVALID -j DROP 修改默认政策 通过-P chain target开关可以设置链的默认目标。如: iptables -P INPUT DROP 选择接口 由于iptables要依照链的每个规则处理每个包,这会变得很慢。一些程序会使用lo接口通信。因而可以在INPUT链的最开始插入下面的规则提速: iptables -A INPUT -i lo -j ACCEPT 这里-i input指定输入的接口。类似地,也可以在OUTPUT链使用-o output指定匹配的输出接口。 负面匹配 某些命令支持前面添加!表示匹配相反的。 使用tcp模块屏蔽非法的TCP包 一些tcp包标志的组合是非法的,我们可以通过tcp模块阻止这些包。它提供了--tcp-flags MASK FLAGS命令,其中MASK是掩码,FLAGS是掩码下需要匹配的标志位。掩码可以为ALL表示所有的标志位。下面是一些例子: # 屏蔽同时有SYN和FIN标志位的包 iptables -A INPUT -p tcp -m tcp --tcp-flags SYN,FIN SYN,FIN -j DROP # 屏蔽状态为new,但没有SYN标志位的包 iptables -A INPUT -p tcp -m conntrack --ctstate NEW -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -j DROP 限制包:limit模块 使用limit模块可以限制包的速率。了解limit模块之前我们需要了解令牌桶算法。可以想象一个桶里面装了一些令牌。只有拿到了令牌,一个包才可以顺利通过。桶中的令牌数目有最大限制,当桶没有令牌,包就无法通过。桶每过一会儿会生成一个令牌,当生成的令牌达到最大限制的时候,多余的令牌就会被抛弃。这里,令牌数目的最大限制就是--limit-burst number,令牌生成的速率就是--limit rate[/second|/minute|/hour|/day]。下面是一个例子: iptables -A INPUT -p icmp -m limit --limit 1/sec --limit-burst 1 -j ACCEPT 每个IP的限制,recent模块 limit模块无法对每个IP做限制。这就出现了recent模块。像下面的例子: iptables -A INPUT -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --name SSHLIMIT --seconds 180 --hitcount 5 --rsource -j DROP iptables -A INPUT -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW -m recent --set --name SSHLIMIT --rsource 首先,第二条命令中,--set表示会向SSHLIMIT列表中向该IP更新最近访问时间并添加当前访问记录到列表中,而--resource是记录源IP,类似地,--rdest可以记录目标IP。第一条命令中--update是指从列表中读取IP对应的访问,并会更新最近访问时间并添加当前访问记录到列表中(然而后面的第二条命令并不能省略,我也不太清楚为什么),如果改用--rcheck则只是读取IP对应的访问,不会更新。这两者的差异就是对于持续性的访问,--update会始终拒绝除非有一段冷却时间,而--rcheck则是间歇性的可以通过不需要冷却时间。--seconds seconds和--hitcount hits是搭配--rcheck和--update使用的,前者表示从最近访问时间前的几秒内搜索对应的访问记录,--hitcount hits设置访问记录的匹配数目上限。 owner模块 owner模块可以根据用户筛选包。它提供了--uid-owner来指定用户。 iptables -A OUTPUT -d 31.13.78.35 -m owner --uid-owner bobby -j DROP 自定义链 通过下面的命令可以创建一个新链ssh-rules: iptables -N ssh-rules 这时有一个RETURN目标,表示回到父链上。同时ssh-rules也能作为新的链。此外还可以删除一个链,但这个链必须为空: iptables -X ssh-rules 对包进行日志:LOG目标 LOG目标可以输出日志到/var/log/syslog或/var/log/messages。你可以使用--log-prefix可以为日志添加前缀。 iptables-save和iptables-restore 使用下面的命令来持久化iptables配置: iptables-save -f iptables.rules # 保存配置,IPv6使用ip6tables-save iptables-restore iptables.rules # 加载配置,IPv6使用ip6tables-store

2020/7/10
articleCard.readMore

C++模板(未完待续)

本文是对《C++ Templates》第二版英文原版的学习笔记。部分内容会参考Walton1128翻译的《C++ Templates 第二版》中文翻译 函数模板 模板概览 定义模板 模板的形式为template<...>,中间省略号代表上逗号,分割的模板参数列表。以下是一个简单的例子: template<typename T> T max(T a, T b) { return a < b ? b : a; } typename关键字用以引入一个类型参数,也可以使用class关键字,模板参数除此之外还有非类型模板参数。T是一个标识符。这里的T必须支持<运算和拷贝赋值。 使用模板 通过使用::max(a, b)就可以使用改模板函数。使用全局作用域限定符是为了避免与std::max冲突。当使用不同类型的参数时,函数会实例化(模板参数替换成具体参数的过程)成多个实体。void也是一个合法的类型参数。 两阶段翻译 尝试对模板实例化那些不支持的模板需要的操作的类型,会是个编译错误。所以模板经历了两次编译 在定义阶段,忽略模板参数检查模板自身的正确性,可检查的错误包括语法错误、使用未知变量名、未通过且不依赖于模板参数的静态断言; 在实例化阶段,模板再次被检查。 这导致模板在实例化的过程中,编译器必须看到模板的定义。因而模板一般被置于头文件中。注意:某些编译器在定义阶段没有完整地检查。 模板实参推导 模板参数可以只是实际类型的一部分。就像下面那样。 template<typename T> T max(T const& a, T const& b) { return a < b ? b : a; } 传递int给模板函数参数则T会是int。 自动类型转换在模板类型推导中是受限的: 当以引用传参的时候,即使是最平凡的转换也不被允许。类型必须精确匹配; 当以值传参的时候,只支持decay的普通类型转换:忽略cv限定,去除引用,数组转指针,函数转函数指针。如果两个函数参数使用了同一个类型参数,则类型参数decayed的类型必须一致。 如果类型推导失败,可以手动指定类型,就像max<double>(4, 7.2)。 类型推导会忽略函数的默认参数。如果要支持设定函数默认参数,需要给模板参数也提供一个默认参数。 多个模板参数 考虑下面的函数: template<typename T1, typename T2> T1 max(T1 a, T2 b) { return a < b ? b : a; } 我们注意到函数的返回参数被强制转换为了第一个参数的类型,这不符合我们的预期。为此我们有3中解决方案。 返回类型为模板参数 模板参数推导不考虑返回值。由于模板参数的指定必须从左到右依次指定,所以一般将返回类型置于模板其他类型参数之前。 返回类型推导 在C++14中,你可以使用auto关键字。类似auto max(T1 a, T2 b)。实际上,对于不带尾随返回类型的auto返回类型,编译器会从函数体中推断类型。这要求函数定义必须存在,且多个返回语句的类型必须一致。 在C++11中,你可以使用尾随返回类型,类似auto max(T1 a, T2 b) -> decltype(a<b?b:a)。实际上decltype(true?b:a)也可行。为了避免T1或T2可能为引用类型,我们可以用typename std::decay<decltype(true?a:b)>::type。 需要注意的对于auto变量的赋值类型始终会被decay,auto返回值也是如此。 返回共同类型 在C++11中,使用std::common_type<Types...>::type可以获得多个类型共同的类型。在C++14中,可以使用std::common_type_t<Types...>。std::common_type同样decay。 默认模板参数 在C++11之前只有类模板支持默认模板参数。C++11之后,所有类型的模板都支持默认模板参数。默认模板参数可以不是在模板参数的最后。 重载函数模板 函数模板也支持重载。普通函数可以与同名同类型的模板函数共存。只有当模板版本能够产生一个更好的匹配的时候(如更少的类型转换),才会选用模板版本。你也可以通过指定一个空模板参数列表::max<>(7, 42),强制使用模板版本。模板参数推导是不会考虑到自动类型转换的,所以当普通函数能自动类型转换成功的话,而模板函数无法匹配的时候,普通函数就会被选用。 也可以重载多个模板的版本,但如果两个模板都能匹配上函数就会发生错误。因此通常重载函数模板时,需要确保对任意一个调用只有一个模板匹配。 在重载函数模板的时候要尽可能少做改动。比如不要混用按值传参和按引用传参。下面是个反面的例子: #include <cstring> template<typename T> T const& max (T const& a, T const& b) { return b < a ? a : b; } char const* max (char const* a, char const* b) { return std::strcmp(b,a) < 0 ? a : b; } // maximum of three values of any type (call-by-reference) template<typename T> T const& max(T const& a, T const& b, T const& c) { return max (max(a,b), c); // error if max(a,b) uses call-by-value // 我的注释:这里对于T: const char *来讲,两个max都是匹配到的非模板函数版本 } int main () { char const* s1 = "frederic"; char const* s2 = "anica"; char const* s3 = "lucas"; auto m = ::max(s1, s2, s3); //run-time ERROR } 此外,还要确保函数模板被调用时,其前方某处有定义。下面是一个反面的例子: // maximum of two values of any type: template<typename T> T max (T a, T b) { std::cout << "max<T>() \n"; return b < a ? a : b; } // maximum of three values of any type: template<typename T> T max (T a, T b, T c) { return max (max(a,b), c); // uses the template version even for ints } //because the following declaration comes // too late: // maximum of two int values: int max (int a, int b) { std::cout << "max(int,int) \n"; return b < a ? a : b; } int main() { ::max(47,11,33); // OOPS: uses max<T>() instead of max(int,int) } 难道,我们不应该 按值传递还是按引用传递 按引用传递对于非简单类型可能能省去拷贝成本,但出于以下原因,按值传递通常更好: 语法简单。 编译器能够更好地进行优化。 移动语义通常使拷贝成本比较低。 某些情况下可能没有拷贝或者移动。 模板有一些额外情况: 模板既可以用于简单类型,也可以用于复杂类型,因此如果默认选择适合于复杂类型可能方式,可能会对简单类型产生不利影响。 作为调用者,你通常可以使用std::ref()和std::cref()来按引用传递参数。(参见7.3) 虽然按值传递 string literal 和 raw array 经常会遇到问题,但是按照引用传递它们通常只会遇到更大的问题。(参见7) 为什么不inline 模板不需要inline。唯一的例外是模板全特化,这时代码不再是泛型。严格地从语言角度来看,inline只意味着在程序中函数的定义可以出现很多次。当然,编译器在做决定的时候依然会将关键字inline纳入考虑因素。 为什么不constexpr 8.2会讨论一些constexpr的例子,为了更专注于模板的原理,接下来的讨论会跳过模板。 总结 函数模板定义了一组适用于不同类型的函数。 当向模板函数传递变量时,函数模板会自行推断模板参数的类型,来决定去实例化出那种类型的函数。 你也可以显式的指出模板参数的类型。 你可以定义模板参数的默认值。这个默认值可以使用该模板参数前面的模板参数的类型,而且其后面的模板参数可以没有默认值。 函数模板可以被重载。 当定义新的函数模板来重载已有的函数模板时,必须要确保在任何调用情况下都只有一个模板是最匹配的。 当你重载函数模板的时候,最好只是显式地指出了模板参数得了类型。 确保在调用某个函数模板之前,编译器已经看到了相对应的模板定义。 类模板 容器类是类模板的典型应用。本章我们将用栈作为一个例子。 Stack类模板的实现 类模板的声明和定义都放在头文件里。 声明一个类模板 template<typename T> class Stack { ... }; 这里可以用class关键字替代typename关键字。T这个类型就和普通类型一样,可以被用于声明成员变量类型,和成员函数的返回值类型及形参类型。 这个类的类型是Stack<T>,其中T是模板参数。在使用类型的时候,你必须指明Stack<T>,除非可以推导出T的类型。不过在类模板内部可以使用Stack代替Stack<T>,推荐在类模板内使用Stack。但在类模板外,你就需要使用Stack<T>,如下: template<typename T> bool Stack<T>::operator== (Stack<T> const& lhs, Stack<T> const& rhs); 在只需要类的名字而不是类型的地方,只能用Stack,如构造函数和析构函数。 不同于非模板类,不可以在局部作用于声明和定义模板,可以在全局、命名空间或者其他类里定义。 成员函数的实现 定义类模板的成员函数时,必须指出它是一个模板,且模板的类型需要完整地给出。 template<typename T> void Stack<T>::push (T const& elem) { elems.push_back(elem); } 类模板的成员函数也可以在类的定义中给出。 template<typename T> class Stack { ... void push (T const& elem) { elems.push_back(elem); // append copy of passed elem } ... }; Stack类模板的使用 在C++17前,类模板的参数必须显式给出。模板函数和模板成员函数只有在被调用时才会实例化。如果一个类模板有static成员,对每一个用到这个类模板的类型,相应的静态成员也只会被实例化一次。 被实例化的类模板类型可以被cv限定,可以被创建它的数组及应用,可以被typedef或者using,也可以作为类型参数。 C++11之前两个相邻的模板闭合括号之间需要有空格,如Stack<Stack<int> >,否则会被解析成右移运算符。 部分地使用类模板 模板参数只需要提供那些会被用到的操作。也就是说模板的成员函数只有被调用才会实例化并检查非语法层面的错误。 Concept Concept指定了为了需要实例化模板,参数需要支持的操作。C++标准库是基于Concept的,比如可随机访问迭代器和可默认构造等等。 C++11开始,你可以使用static_assert和其他的预定的类型trait完成检查: template<typename T> class C { static_assert(std::is_default_constructible<T>::value, "Class C requires default-constructible elements"); ... }; 从C++20引入了Concept,使用方法大致如下: template<typename T> concept Hashable = requires(T a) { { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>; }; template<Hashable T> void f(T); 有机会我会出一个关于C++20的新文章,这里不再多做介绍。 友元 template<typename T> class Stack { friend std::ostream& operator<< (std::ostream &strm, const Stack<T>& s) { // ... return strm; } }; 这里定义的operator<<是一个普通函数,确切讲是templated entity。如果你尝试先声明后定义,那么有两种选择。 使之成为函数模板,但它必须使用新的模板参数。 template<typename T> class Stack { ... template<typename U> friend std::ostream& operator<< (std::ostream&, Stack<U> const&); }; 前向声明一个模板函数,并特例化它: template<typename T> class Stack; template<typename T> std::ostream& operator<< (std::ostream&, Stack<T> const&); template<typename T> class Stack { ... friend std::ostream& operator<< <T> (std::ostream&, Stack<T> const&); }; 注意这里operator<<之后的<T>,它代表我们声明了一个特例化的非成员模板函数作为友元,如果没有<T>,我们会声明一个新的非模板函数。 但无论是上面的三种情况的哪一种,你都可以将Stack用于没有定义operator<<的元素,前提是你不会用到这个操作。 类模板的特化 函数模板不支持特化,但支持重载。类似于函数的重载,我们可以特化类模板。如果对类模板进行了特化,你也需要特化所有的成员函数: template<> class Stack<std::string> { ... }; void Stack<std::string>::push (std::string const& elem) { elems.push_back(elem); // append copy of passed elem } 这里push如果采用转发引用会更好。 偏特化 类模板可以只是被部分特例化。 template<typename T> class Stack<T*> { ... }; 多参数模板特例化 template<typename T1, typename T2> class MyClass { ... }; 对于上面的类模板可以有以下的偏特化: template<typename T> class MyClass<T,T> { ... }; template<typename T> class MyClass<T,int> { ... }; template<typename T1, typename T2> class MyClass<T1*,T2*> { ... }; 这里如果出现匹配得同样好的偏特化时,就会报错: MyClass<int*,int*> m; // matches MyClass<T,T> and MyClass<T1*,T2*> 为了解决歧义,可以添加一个额外的偏特化: template<typename T> class MyClass<T*,T*> { ... }; 默认类模板参数 与函数模板类似,类模板也可以有默认模板参数。类模板成员函数定义之处不必加上默认参数。 类型别名 Typedef和Alias声明 可以使用关键字typedef给类型声明别名,其产生的别名我们称为typedef-name: typedef Stack<int> IntSack; 或者C++11开始可以使用using: using IntStack = Stack <int>; 以上两种给一个已存在类型新名字的方式,我们统一称为type alias declaration,新的名字称为type alias。 别名模板 (未完待续)

2020/7/3
articleCard.readMore

GraphQL学习笔记

这篇文章是在我写山楂记账的过程中,尝试使用新的API调用方式。因此重新考虑了曾经接触过的GraphQL,决定学习一下。内容主要来自Introduction to GraphQL | GraphQL 。 查询和更改 最简单的查询像下面这样: { <object> { # can contain comment <field1> # can be a sub-selection for field refering objects <field2> { <sub-field1> <sub-field2> ... } ... } } 查询中的每一个字段(包括标量字段)和嵌套对象都可以带有参数: { <object>(<argument1>: <value1>, ...) { ... <fieldN>(<argument2>: <value2>, ...) } } 参数拥有不同的类型,比如原文例子的height(unit: FOOT)就是枚举。用户可以自定义了类型。 默认情况下,查询返回的字段会与查询的字段一致,不包括参数。使用别名可以避免查询到结果同名造成的错误。 { <alias1>: <object1> { ... } <alias2>: <object2> { ... } } 可以使用片段来减少重复的查询。片段可以访问到查询或更改定义的变量。 query <operation>(<$var>: <type> = <value>) { <alias1>: <object1>(<argument1>: <value1>) { ...<fragment> } <alias2>: <object2>(<argument2>: <value2>) { ...<fragment> } } fragment <fragment> on <type> { <field1> ... } 之前我们一直才用简写的方式省略了query <operation>。这里query是操作类型,也可以为mutation、subscription等等。<operation>是个对操作有意义的名字,多文档操作时是必须的,它对于日志输出很有帮助。 如果不使用变量,随着一些查询的变更,我们可能需要重新序列化成GraphQL格式。对此我们可以使用变量: 讲查询使用的静态值改为$variableName; 声明$variableName作为查询接受的变量; 通过另外的某种格式如JSON,将变量传递进来。 query <operation>(<$var1>: <type1>, ...) { ... } 变量定义中的名字必须是$开头的。其类型只能为标量、枚举或者输入对象类型。默认情况下,变量定义默认是可选的,但如果变量类型后面有!或者变量被传递给了一个要求非空的参数时,变量就成为必须的了。通过<$var1>: <type1> = <value1>可以设定变量中的默认值。 GraphQL中还有指令,它可以附着于字段或者片段包含上。目前有两个字段: @include(if: Boolean):当条件成立时,包含字段; @skip(if: Boolean):当条件成立时,跳过字段。 通过下面的方式可以创建更改,并指定返回的字段: mutation <operation>(<$var1>: <type1>, ...) { field1 field2 ... } 查询是平行运行的,但是更改被担保是顺序执行的。 如果你需要的数据类型是个接口或者联合,你需要使用内联片段访问数据,就像下面这样。 query <operation> { ... on <Type> { field1 ... } ... } GraphQL可以通过指定__typename元字段,包含额外的类型信息。 模式和类型 一个模式大致像下面的样子: type Character { name: String! appearsIn: [Episode!]! } 其中Character是一个GraphQL对象类型。name和appearsIn是字段。String是内置标量类型。!表示不可为空。[Episode!]!表示Episode的数组,同时数组及其元素不为空。 每个字段都可以有0到多个参数,例如length(unit: LengthUnit = METER): Float。参数都是具名的。有默认值的参数就是可选的。 GraphQL有两种类型比较特殊: schema { query: Query mutation: Mutation } 一个服务可以有一个query类型和0个或多个mutation类型。它们与普通对象类型类似,不过它们定义了入口。 GraphQL有如下的内置标量类型: Int:32位有符号整型; Float:双精度浮点数; String:UTF-8字符串; Boolean:true或false; ID:唯一标识符,不需要给人读的。 你也可以像下面那样自定义标量类型: scalar Date 或者像下面那样定义枚举类型: enum Episode { NEWHOPE EMPIRE JEDI } 对象类型、标量、枚举是你在Graphql中唯一能定义的类型。你可以使用非空修饰符!,非空修饰符也可以用于参数。也可以定义列表,并且与非空修饰符配合使用。 GraphQL还提供了接口。接口是抽象的类型,它拥有一系列字段需要被实现。就像下面这样。 interface Character { id: ID! name: String! friends: [Character] appearsIn: [Episode]! } type Human implements Character { id: ID! name: String! friends: [Character] appearsIn: [Episode]! starships: [Starship] totalCredits: Int } type Droid implements Character { id: ID! name: String! friends: [Character] appearsIn: [Episode]! primaryFunction: String } 接口经常和内联片段配合使用。 联合类型则类似下方: union SearchResult = Human | Droid | Starship 通过下面的方法可以定义输入对象: input ReviewInput { stars: Int! commentary: String } 输入对象可以引用其他输入对象,但不能与输出对象混用。且输入对象的字段不能有参数。 验证 一个片段不能引用自己。如果我们要查询一个字段,它必须存在。如果我们查询的字段不是标量和枚举,那么我们需要指定想要得到的字段;而对于标量,你不应该指定想要得到的字段。 执行 GraphQL执行的时候,回由一种叫resolver的函数获取当前类型,然后如果返回的类型是标量或者枚举就完成了,否则会调用另一个resolver获取下一个类型。 一个resolver接受四个参数: obj:前一个对象; args:参数; context:上下文,表示一些额外的信息,如数据库; info:本次访问相关的信息; 内省 通过查询__schema可以获取一些内省信息。__type(name: "<name>")可以查询某个名字的类型。等等。

2020/6/28
articleCard.readMore

Python教程4 - 模块

这篇教程讲了Python的模块,主要内容来自6. 模块 — Python 3.7.7 文档。 介绍 当你使用交互的Python解释器时,你会发现一旦你退出了解释器,你的所有定义的函数和变量都丢失。如果你想保存下你的代码,你就需要用文本编辑器,将代码保存成.py文件,也就是脚本。 有时你希望自己的某个代码片段能在多个项目中使用,却不需要拷贝来拷贝去。Python可以允许你将代码保存成文件,被其他脚本或者交互式解释器使用。这样的文件我们称之为模块。这些模块可以被导入到其他模块或交互式解释器中。在所有的模块中,会有一个最顶层的模块包含执行的入口,我们称之为主模块。 在每个模块的内部,会有一个全局变量__name__存储着模块的名字,如果是主模块,__name__的值为'__main_'。 首先你可以创建一个fibonacci.py。你既可以用文本编辑器编辑后保存至某个项目文件夹或者桌面,也可以是PyCharm之类的IDE新建工程添加文件,内容如下: def fib(n): # 返回小于n的斐波那契数列 result = [] a, b = 0, 1 while a < n: result.append(a) a, b = b, a+b return result def main(): print('Fibonacci less than 100:') for i in fib(100): print(i) if __name__ == '__main__': main() 然后你可以执行它,就像我们先前做的那样,这时fib.py这个文件就成了主模块,__name__的值为'__main__',if语句成立。 当然你也可以导入这个模块。第一步是打开命令行,对于Windows上没有使用IDE的同学,可以打开cmd,输入cd "PATH_TO_FOLDER",这里PATH_TO_FOLDER需要改成fibonacci.py所在的目录,这一步是切换工作目录;对于Windows上使用PyCharm的同学按Alt+F12或者点击下方的Terminal。接着在命令行中输入python运行交互式解释器,输入以下的代码: >>> import fibonacci >>> fibonacci.fib(100) [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89] >>> fibonacci.__name__ 'fibonacci' 上面的代码中import fibonacci就是Python导入模块的方式,这里import fibonacci,会执行fibonacci.py中的代码,并将其中的全局对象(全局变量和全局函数等)收集起来,存放到fibonacci这个对象中,供导入fibonacci.py的模块或交互解释器使用。这样你就可以通过fibonacci这个对象去访问fibonacci.py中的全局对象了。由于__name__的值为'fibonacci',所以在导入的过程中没有像运行的过程中那样输出东西。 关于模块 模块只初始化一次 每个模块的语句仅在第一次被导入的时候被执行,这意味着多次导入的时候(甚至一个模块分别被多个模块导入的时候),模块只会被初始化一次。为了演示这种现象,我们可以修改fibonacci.py成下面的样子: print('initializing fibonacci') def fib(n): # 返回小于n的斐波那契数列 result = [] a, b = 0, 1 while a < n: result.append(a) a, b = b, a+b return result def main(): print('Fibonacci less than 100:') for i in fib(100): print(i) if __name__ == '__main__': main() 在同一目录添加一个foo.py,内容如下: import fibonacci print('initializing foo') 这时候运行交互式解释器,执行下面的内容: >>> import foo initializing fibonacci initializing foo >>> import fibonacci >>> foo.fibonacci is fibonacci True 在遇到import foo的时候,Python会去加载并执行foo.py。这时遇到了import fibonacci,又去加载并执行fibonacci.py,这就输出了第一条initializing fibonacci。执行完fibonacci.py,完成了foo.py中的import fibonacci就执行到输出initializing foo。最后foo.py执行完成,import foo也完成了。 sequenceDiagram Python->foo.py:"import foo" started foo.py->fibonacci.py:"import fibonacci" started Note right of fibonacci.py: "print('initializing fibonacci')" Note right of fibonacci.py: "def fib(n): ..." Note right of fibonacci.py: "def main(): ..." Note right of fibonacci.py: "if __name__ == '__main__': ..." fibonacci.py->foo.py:"import fibonacci" finished Note right of foo.py: "print('initializing foo')" foo.py->Python:"import foo" finished 然后我们在交互式命令行输入的import fibonacci并没有触发重新加载,没有initializing fibonacci被打印出来。实际上可以看出,在foo.py中导入的fibonacci(foo.fibonacci)和我们刚刚命令行导入的fibonacci是一个对象。 模块的独立全局作用域 我们可以看到每个模块都有自己的全局作用域,比如在上一个例子中,被导入的foo.py的所有全局对象都存放在foo这个对象下,而fibonacci.py的所有全局对象存放在foo.fibonacci和fibonacci共同指向的对象下。这样每个模块的全局作用域都是独立的,模块的开发者就不用担心自己需要使用的全局对象和别人的冲突。 import语句一般放在文件的开始,但是这不是必须的。 import语句的变体 首先你可以使用from module import name1, name2只导入模块中的某些全局对象: >>> from fibonacci import fib, main initializing fibonacci 此外你也可以使用from module import *导入模块的所有全局对象。它会导入那些不以_开头的名字。这在写代码的时候是不建议的,但如果你只是在交互式解释器里尝试,这是可以的。 >>> from fibonacci import * initializing fibonacci >>> fib(10) [0, 1, 1, 2, 3, 5, 8] 接着你可以通过import module as name来给导入的模块设置新的名字,这样做使得导入多个同名的模块成为了可能。 >>> import fibonacci as fib initializing fibonacci >>> fib.fib(10) [0, 1, 1, 2, 3, 5, 8] 最后你可以结合上面两种导入语句的方式,通过from module import name1 as name2来给导入的模块的全局对象设置新的名字,这使得导入多个同名的(不同模块的)全局对象成为了可能。 >>> from fibonacci import fib as func initializing fibonacci >>> func(10) [0, 1, 1, 2, 3, 5, 8] 模块搜索路径 当我们在命令行中输入import foo的时候,Python会在哪些地方搜索模块foo.py是否存在呢?从先到后它会查询这些路径: 包含主模块的目录×如果是在交互模式中,则是当前路径); 环境变量PYTHONPATH中指定的路径; 与安装环境相关的默认路径。 环境变量 这里我介绍一下环境变量的相关概念。每个进程(运行着的程序)都会有各自的环境变量,你可以理解环境变量是字符串到字符串的键值对。默认情况下,当新的进程被创建时,它会继承父进程的所有环境变量。通过环境变量,父进程可以向子进程传递一些信息。 在各种环境变量中,最著名的是PATH环境变量,它存放着启动进程时的搜索路径,Windows系统上是分号;分隔的路径列表,POSIX系统上是冒号:分隔的路径列表。这个环境变量决定了你在命令行(如Windows的cmd,Win + R启动的运行对话框)里输入的命令会在哪里查找并且被启动,比如Windows上,命令行里输入notepad,这时候Windows就会顺着环境变量PATH里的目录查找,直到找到C:\Windows\system32这个路径,发现下面有notepad.exe就启动该程序。 如果你想要查看命令行中的环境变量,在Windows上可以输入echo %PATH%,在POSIX系统上可以输入echo $PATH。你也可以在命令行里临时设置环境变量,如添加foo路径到PATH里,Windows上可以set PATH="foo;%PATH%",POSIX上可以export PATH="foo:$PATH"。如果你想要在Windows上永久地修改环境变量,你可以右击“计算机”,选择“属性”,点击左侧“高级系统设置”,选择“高级”Tab页,点击“环境变量”进行编辑。 PYTHONPATH和PATH环境变量是类似的,它们拥有相同的分隔符(Windows分号,POSIX冒号)。 sys.path sys是一个Python内置的模块,它里面包含了很多系统信息。其中它的path变量是一个字符串的列表,存放了Python的搜索路径。 >>> import sys >>> sys.path ['', '/usr/lib/python38.zip', '/usr/lib/python3.8', '/usr/lib/python3.8/lib-dynload', '/usr/lib/python3.8/site-packages'] 这里我的环境是ArchLinux,所以具体的输出会有所差别。这里我们注意到列表的第一个是空目录,这表示当前交互式环境所在的目录。如果你将下面的代码保存成一个文件。第一个路径会变为主模块所在的文件夹。 import sys print(sys.path) Python程序可以在运行时修改sys.path的值,从而改变搜索路径。 编译的Python文件 Python虽然是解释型语言,但它也有编译这个操作。这个编译和我们一般所指的C/C++之类的语言编译成机器指令是不同的,它会编译成一种非文本的字节码,这种字节码最后仍会被Python解释执行,是平台无关的,其执行速度与普通Python代码是一样的,但是其加载速度会快出很多。为了加速加载,Python会把每个模块编译后的版本存放到同目录下的__pycache__/module.version.pyc,如对spam模块,使用的是CPython 3.8,会编译成__pycache__/spam.cpython-38.pyc。 当运行的时候,Python会检查源文件和编译后文件的时间戳,如果发现源文件编译过了就会重新编译。 Python有两种情况不会检查编译后的缓存文件: 主模块一定会被重新编译; 没有对应源文件的缓存。

2020/3/21
articleCard.readMore

Python教程3 - 复习:内置对象

这篇文章是对我们之前所学知识的复习,也教授了一些新的知识,并且提供了一些习题来巩固大家所学的知识。由于我们已经大致掌握了Python命令式编程的设施,这里我就把很多细节补全,并且不再是按照人的学习顺序组织。这篇文章的内容适合不仅适合那些Python新手,也适合已经熟练运用Python的人巩固知识或作为参考查询。 内置对象 我们可能经常会用到对象这个词。按照Python的官网上的说法,所谓对象就是一个有唯一ID、值和类型的东西。对象的ID和类型是不会改变的,而根据对象的值能否改变,对象可以分为可变的(mutable) 和 不可变的(immutable),这是由它的类型决定的。有些容器如元组,虽然它的内容可能是个可变对象,但我们依旧认为它是个不可变对象。Python中的一切实体都是对象,或者说一切可被赋值给变量的都是对象。而变量是一种到对象的引用,它将一个对象绑定到了名字。当没有任何方法能够获取到某个对象时,那个对象就会被销毁。 Python的类型之间是有父子关系的。子类型会拥有父类型的所有方法。所有的类型都是object类型的子类型。比如int类型就是object类型的子类型,这种父子关系我们称为继承(inheritance),因此我们说int类型继承自object类型。这里我们还需要引入一个名词叫实例(instance),当我们说对象a是类型A的实例的时候,其实就在说对象a的类型是A或者A的子类型(包括直接或间接的)。比如1是int的实例,'1'是str的实例,而1和'1'都是object的实例,甚至Python中一切的对象都是object的实例。 面向对象编程启发自大自然。我们就以大自然中的动物为例,每个动物都有一些属性,比如性别,同时它们也有一些方法,比如繁殖。将这些属性和方法聚集在一起,这就是面向对象的第一个组成部分——封装(encapsulation)。而人类和鸟类都继承自动物,因此人类和鸟类都有繁殖这个方法,同时又有性别这个属性,这就是继承(inheritance),面向对象编程的第二个重要组成部分。另一方面,子类型除了能继承父类型的所有方法之外,还能重新定义父类型的方法。比如人类的繁殖是胎生,而鸟类的繁殖就是卵生。这样同样是动物,却对同一个方法有不同的具体行为,这就是多态(polymorphism)。多态是面向对象编程的第三个重要组成部分。 最后,我和正在阅读这些文字的你都是人类和动物的实例(instance),是一种对象(object)。 下图是一些内置类型的继承关系,其中方框是内置类型,椭圆来自collection.abc,每个椭圆的内容都提供一些方法。以后如果有机会,我会再来介绍每个节点是什么含义,你可以点击此处查看相关文档。 接下来介绍下面2个运算符和4个函数: id(a)函数:获取对象的ID,这个ID是个整数,而且在对象存在的期间是唯一的(不与其他对象一样); type(a)函数:获取对象的类型; a is b和a is not b:判断a和b是不是同一对象,如果id(a) == id(b),则就认为是同一对象; isinstance(a, A):判断对象a是否是类型A的实例,注意这个运算符与type(a) is A还是有区别的比如isinstance(1, object)是真,但是type(1)是int不是object。 issubclass(A, B):判断类型A是否是类型B的子类型。 下面是例子。 >>> id(1) # 这个数字是会改变的 139698536331168 >>> type(1) is int # 最好不要在此处使用 == True >>> a = [1, 2, 3] >>> b = [1, 2, 3] >>> a == b True >>> a is b # 对于相同值的可变对象,它们也可能不是同一对象,拥有不同的ID False >>> a is not b # 上面式子的否定 True >>> isinstance(1, int) True >>> isinstance('1', int) False >>> isinstance(1, object) True >>> isinstance('1', object) True >>> issubclass(int, object) True ::: warning 请不要把判断对象相同运算符is、is not和判断值相等运算符==和!=搞混。 ::: tip Python中经常有一些函数,其实也是类型,如str、int等,因为这个类型可以接受参数来实例化出对象,这种函数我们称为构造函数(constructor)。 我们将在以后学习自定义的对象和类型,但其实内置对象已经足够我们使用。我们先从复习内置对象开始。 整数 Python内置支持无限位数的整数。它是一种不可变对象。 整数字面量 整数的字面量可以是以下几种情况: 十进制:非0开头的十进制数,如127; 二进制:0b或0B开头的二进制数,如0b1111111; 八进制:0o或0O开头的八进制数,如0o177; 十六进制:0x或0X开头的十六进制数,使用a到f或A到F表示10到15,如0x7f。 其中十进制肯定是最常用的,而有时十六进制也很常用,因为2位16进制可以表示一个字节。 整数字面量的进制标识(类似0x)和数字以及数字和数字之间都可以加入下线符_,如1_1、0x_ff,而11_和1__1都是非法的。增加这个语法主要是为了方便很长的数字被阅读,不过这个语法也用得很少。 整数与字符串的转换 int是整数的构造函数,它可以从浮点数和字符串中构造出整数: 对于参数为浮点数的情况,它会截断(正数是向下取整); 对于参数为字符串的情况,它有第二个参数名叫base可以指定进制数,必须是2到36(因为数字+字母总共36个,只能表示36进制),默认为10进制,如果你指定0,则它会从开头的进制标识来推断是几进制。 >>> int(0.9) 0 >>> int(-0.9) 0 >>> int('177') 177 >>> int('177', 8) 127 >>> int('0o177', base=0) 127 除了将某进制的字符串转成整型,还可以将整型转成指定进制的字符串。这里Python提供了以下函数: str:这其实是个通用的构造函数,可以将任意对象转换成字符串,对于整数它会转成10进制。 bin:将整数转成2进制字符串,包含前缀0b; oct:将整数转成8进制字符串,包含前桌0o; hex:将整数转成16进制字符串,包含前缀0x。 下面给一些例子: >>> str(127) '127' >>> bin(127) '0b1111111' >>> oct(127) '0o177' >>> hex(127) '0x7f' >>> hex(127)[2:] # 如果你不想要进制标识,可以用切片语法去除掉 '7f' 浮点数 浮点数能存储精度为15位有效数字左右的实数。它是一种不可变对象。 浮点数字面量 浮点数的字面量格式大致如下: $$\text{整数} \underbrace{. \text{小数}}_{\text{小数部分}} \overbrace{\text{e(或E)} \pm \text{指数}}^{\text{指数部分}}$$ 它表示的数字其实就是:$\text{整数}.\text{小数}\times 10^{\text{指数}}$。这里整数、小数和指数可以是任意十进制数字,数字和数字之间可以插入_(但同样不常用)。浮点数的字面量可以省略一些部分,但必须满足如下规则: 整数和小数可以省略,但不可以同时省略:1.e-2和.5e6都是合法的浮点数,但是.e-2都不是合法的。 小数部分和指数部分可以省略,但不可以同时省略:1e2和1.0都是合法的浮点数,但1不是,实际上它是整数。 为了确保某些字面量是浮点类型,我们经常会加上.0,比如1.0,0.0。 浮点数与字符串的转换 float是浮点数的构造函数,它可以从整数和字符串中构造出浮点数。同样地,你可以通过str函数将浮点数转成字符串。 浮点数的精度问题 浮点数是有误差的,因此用等于去判断两个浮点数的值是否是相等的是有风险的: >>> 0.1 + 0.1 + 0.1 == 0.3 False 在上一讲中,我介绍了一种用两数只差小于某个很小的值来判断两个浮点数是否相等的方法。实际上,我后来查阅了一些文档,发现在Python 3.5中新增了用于判断两个实数是否相近的函数,使用方法如下: >>> import math >>> math.isclose(0.1 + 0.1 + 0.1, 0.3) True 特殊的浮点数值 这里我们使用import语句导入了一个包,我们会在之后的课程里讲解这些。 注意浮点数的零有正零和负零之分,但是正零和负零是相等的: >>> +0 0 >>> -0 # 整数只有一种0 0 >>> +0.0 0.0 >>> -0.0 # 浮点数有两种0 -0.0 >>> +0.0 == -0.0 # 但这两种0是相等的。 True 此外浮点数还有3个特殊的值:+inf、-inf和nan,分别表示为正无穷、负无穷和不是一个数(not a number)。对于大到超出浮点数表示范围的数,就成了正负无穷。在很多语言包括Python的一些数学库(如numpy)中,浮点数除以0也会得到无穷,这是CPU指令约定成俗的,但是在Python中,它会额外检查除数,遇到0就报除零错误。 >>> 1e1000 inf >>> float('inf') inf >>> import numpy >>> numpy.float64(1) / 0.0 <stdin>:1: RuntimeWarning: divide by zero encountered in double_scalars inf >>> 1.0 / 0.0 Traceback (most recent call last): File "<stdin>", line 1, in <module> ZeroDivisionError: float division by zero 如果出现了无穷相除、0相除(Python),或者无穷乘以0这种极限中的不定式的情况,就会产生nan值,nan很特殊,它是唯一一个不等于自身的浮点数。 >>> inf = float('inf') >>> inf / inf nan >>> nan = float('nan') >>> nan == nan False 字符串和Bytes 字符串类型用于存储文本数据,而Bytes用于存储二进制数据,它是种不可变对象,也是一种序列。 字符串和Bytes的字面量 字符串和Bytes字面量都可以用一对单引号或双引号括起来,区别在于单引号括住的字符串中的双引号可以不必转义,同样的双引号括住的字符串中的单引号可以不必转义。此外字符串和Bytes也可以用三重引号括起,区别在于单重引号不能包含不加转义的换行和对应的引号,而三重引号则可以。 >>> 'It\'s me.' "It's me." >>> "It's me." "It's me." >>> '''And then, ... that's you''' "And then,\nthat's you" 字符串和Bytes字面量形式上的差别在于Bytes字面量有前缀b或B。字符串和Bytes字面量还可以有前缀r或R表示原始字符串(raw string)。前缀的大小写是忽略的,其顺序也是无所谓的。原始字符串中的\代表它本身,不再处理转义序列。原始字符串经常出现在Windows路径或者正则表达式中。此外字符串还允许有f或F前缀表示格式化字符串,我们将在以后的章节介绍这种。 转义 下表是可以出现在字符串和bytes字面量中的转义字符列表: 转义序列 含义 \紧跟换行 \和换行被忽略 \\ 反斜杠符 (\) \' 单引号 (') \" 双引号 (") \a ASCII 响铃 (Bell,BEL) \b ASCII 退格 (Backspace,BS) \f ASCII 换页 (Formfeed,FF) \n ASCII 换行 (Linefeed,LF) \r ASCII 回车 (Carriage Return,CR) \t ASCII 水平制表符 (Horizontal Tab,TAB) \v ASCII 垂直制表符 (Vertical Tab,VT) \ooo 8进制值为ooo的字符,o代表8进制数位,最多可以有3个数位 \xhh 16进制值为hh的字符,h代表16进制数位,必须2个数位 除此之外,字符串还支持了额外的转义,列在了下表中,bytes不支持这些转义: 转义序列 含义 \N{name} 名字为name的 Unicode 字符 \uxxxx 16进制值为 xxxx 的16位字符,x表16进制数位,必须4个数位 \Uxxxxxxxx 16进制值为 xxxxxxxx 的32位字符,x表16进制数位,必须8个数位 有一些小细节这里提示一下,原始字符串也可以转义引号,但反斜杠符仍会被保留,如r"\""是"\\\"";原始字符串也不能以反斜杠符结尾。 上面的两张表不必死记硬背,记住个大概就好,其中\t(水平制表)、\r(回车)、\n(换行)和\xhh这4种转义序列用得比较多。其中\r,我们在涉及文件输入输出时再细细介绍,这里我们给出一些例子: >>> print('1\t2\n3\t4') 1 2 3 4 >>> '\x0a' == '\n' # 换行符ASCII码为10,十六进制为0a True 字符与整数的转换 每个字符都是拥有一个编码的,字符编码中Unicode是ASCII的拓展,前者包含了我们用到的所有字符,包括中日韩表意文字、emoji等等,而后者只有128个字符,这个字符对应的编码称为码位(code point)。我们有两个函数对字符和码位互相转换: 通过ord()函数我们能获得长度为1的字符串或bytes对应的码位; 通过chr()函数我们能获得码位对应的长度为1的字符串。 >>> ord('我') 25105 >>> chr(25105) '我' 编码与解码 文本在进行编码后就成了二进制数据,而二进制数据解码之后也就变回了文本。Python提供了负责这两种转化的方法(这里方法是指某个对象拥有的函数): str.encode(encoding='utf-8'):将文本编码成二进制数据; bytes.decode(encoding='utf-8'):将二进制数据解码为文本。 编码默认是utf-8。对于中国大陆而言,由于Windows大都采用国标码记录中文,也就是gbk,所以gbk也会比较常见。想看完整的编码列表可以点击此处。下面给一些例子: >>> text = '你好世界' >>> binary = text.encode() >>> binary b'\xe4\xbd\xa0\xe5\xa5\xbd\xe4\xb8\x96\xe7\x95\x8c' >>> binary.decode() '你好世界' 将任何对象转换成字符串 Python提供了两个函数将对象转换为字符串: str()函数:将对象转成一个适合打印的形式; repr()函数:将对象转成一个包含很多信息的形式,如果可能,得到的字符串将是可以产生这个对象的Python代码。 可能即使这么说还是不够形象,我们给一个字符串的例子,对于字符串而言str()函数就是返回它本身: >>> print(str('1\t2')) 1 2 >>> print(repr('1\t2')) '1\t2' 其实print()函数对于非字符串的对象会调用str()函数转换成字符串后再打印。另外我们可以看到,当Python的交互式模式输入的是表达式的时候,Python会调用repr函数打印这个表达式的值。 实际上str()会调用对象的__str__()方法,而repr()会调用对象的__repr__()方法。这些方法是所有对象都有的。 >>> print("1\t2".__str__()) 1 2 >>> print("1\t2".__repr__()) '1\t2' 作为可迭代对象和序列 关于可迭代对象和序列的介绍可以见可迭代对象的操作和序列的操作。作为不可变的序列,字符串和Bytes只支持获取长度,根据索引或切片获取元素等操作,不支持对索引或切片赋值,也不支持删除索引或切片等操作。 常见的字符串方法 完整的字符串方法列表可以点击此处查看。 判断是否包含子串: str.startswith(prefix):判断字符串是否以prefix开头; str.endswith(suffix):判断字符串是否以suffix结尾; str.find(sub):寻找字符串中包含子串sub的起始索引,如果找不到返回-1。 >>> '1234'.startswith('12') True >>> '1234'.endswith('12') False >>> '1234'.find('23') 1 >>> '1234'.find('32') -1 对字符串做一些变换(返回新的字符串): str.lower():将所有的字母转成小写; str.uppper():将所有的字母转成大写; str.strip():删除前导和后继空白符。 >>> 'abc'.upper() 'ABC' >>> ' a\tb\t '.strip() 'a\tb' 一些判断字符类型的函数: str.isalpha():是否都是英文字母且长度至少为1; str.isdigit():是否都是数字且长度至少为1; str.isspace():是否是空白字符且长度至少为1。 分割与合并: str.split(sep=None):以sep为分隔符分割字符串成列表,如果sep为None,则以空白符分割; str.join(iterable):将可迭代对象以str为分隔符连接成字符串。 >>> items = '1\t 2\n3'.split() >>> items ['1', '2', '3'] >>> ','.join(items) '1,2,3' 布尔 布尔对象只有两种值:真和假,分别用两个关键字True和False表示,它们是布尔类型仅有的两个实例。布尔类型实际上和整数类型比较类似,在绝大多是情况下,True和1有同样的表现,而False和0有同样的表现。bool是布尔类型的构造函数,可以将任意类型转为布尔类型。对于if语句和while语句中的条件表达式,其值会被转换为bool类型。 其他类型转换到布尔类型 通常而言,我们认为以下的值是假的: 被定义成假的常量:None和False; 值为0的数值类型:如0,0.0等; 空的容器:'',(),[],{}。 其他情况被认为是真值。 实际上,bool()会调用对象的__bool__()方法。如果这个方法没有定义,它会调用对象的__len__()方法看它是否非零,如果这个方法也没有,那这个对象就被认为是真的。 列表和元组 列表和元组都是序列。其中列表是可变对象,而元组是不可变对象。 列表和元组的字面量 ::: warning 官方文档里面并没有采用列表字面量和元组字面量的词汇,所以我的说法可能是不标准的。官方采用列表显示(list display)指我这里说的列表字面量。而元组字面量我没有找到专业的名词。 使用中括号[]括起,逗号分隔的表达式列表就能表示一个列表,最后一个表达式的尾部可以有一个可选的逗号。如果你采用多行来完成列表字面量,很推荐你在最后一个表达式的尾部加上逗号,这样以后你再添加元素会很方便。 >>> list1 = [1, 2, 3, 4] >>> list2 = [ ... 'one', ... 'two', ... 'three', ... 'four', ... ] >>> empty = [] >>> mono = ['a'] 类似地,使用圆括号()括起,逗号分隔的表达式就能表示一个元组,最后一个表达式尾部可以有一个可选的逗号。当只有一个元素的时候,这个逗号是必须的,如果不添加逗号,就成了一个加了括号改变优先级的表达式。 >>> (1, 2, 3) (1, 2, 3) >>> (1) # 错误的单元素元组表示方法 1 >>> (1,) (1,) >>> () # 空元组 () 值得一提的是,列表和元组的构造是支持解包语法的,这和我们之前说过的函数解包语法是类似的: >>> a = [3, 4] >>> b = [1, 2, *a, *(5, 6), 7] >>> b [1, 2, 3, 4, 5, 6, 7] 此外,元组的括号在不引起歧义(如赋值语句的右侧)的时候是可以省略的。下方代码中,b, c = 3, 4利用了赋值语句左侧如果是多个目标,则右侧的可迭代对象会被展开挨个赋值给目标;最后一行是很Pythonic的交换两个变量。 >>> a = 1, 2 >>> a (1, 2) >>> b, c = 3, 4 >>> b 3 >>> c 4 >>> b, c = c, b # 交换两个变量 Pythonic是对代码的一种要求,就是代码不仅是语法正确的,而且是遵循了Python的习俗的,容易被人理解的。 Python哪些地方元组可以省略括号?就我看到的而言有如下地方: 表达式语句(支持解包); 赋值语句右侧(支持解包); 复合赋值语句右侧(不支持解包); yield右侧(不支持解包); 下标运算符内(不支持解包); return右侧(不支持解包) 其中yield语句我们还没学过。 可迭代对象到列表和元组的转换 可迭代对象转成列表和元组可以直接使用列表和元组的构造函数: >>> list(range(10)) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> tuple(range(10)) (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) 当你有多个可迭代对象,而你希望把它们拼接在一起,那么解包语法也是一个不错的选择,但注意这么做会把可迭代对象的所有值取出,占耗大量的内存,如果你希望得到一个惰性的迭代器,可以使用itertools.chain(): >>> [*range(5), *range(3)] [0, 1, 2, 3, 4, 0, 1, 2] >>> import itertools >>> list(itertools.chain(range(5), range(3))) [0, 1, 2, 3, 4, 0, 1, 2] 可迭代对象的操作 可迭代对象是指能够一次返回一个元素的对象。可迭代对象包括序列如list、str和tuple、range,也包括映射dict,还有enumerate等等。 通过调用内置函数iter()可以获取可迭代对象的迭代器。所谓迭代器,是能够通过内置函数next()不断获取下一个元素的对象,直到抛出StopIteration异常终止(异常我们以后会介绍)。迭代器本身也是可迭代对象,调用iter()会获得自己,这使得迭代器能出现在可迭代对象出现的地方。一般而言一个迭代器只能使用一遍。如果你需要多遍遍历,你需要用iter()获取多个全新的迭代器。我们可以看看示例代码: >>> iterable = [1, 2] >>> iterator = iter(iterable) >>> next(iterator) 1 >>> next(iterator) 2 >>> next(iterator) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration >>> iterator = iter(iterable) >>> iterator is iter(iterator) True 这部分内容可能对于初学者过于深奥。请谨慎食用。 iter()函数会尝试调用可迭代对象的iterable.__iter__()方法获得迭代器;如果这个方法不存在,它会试图创建一个迭代器,这个迭代器会,从0开始用整数调用iterable.__getitem__()(基本等价于下标)获取元素直到出现IndexError异常。 next()函数会调用迭代器的iterator.__next__()方法。 以下我们给出可迭代对象支持的操作,除了下面列出的操作,可迭代对象还支持解包和用for语句遍历: 操作 结果 来源 iter(iterable) 返回iterable的迭代器 Iterable要求的方法 min(iterable) iterable最小的元素 Iterable提供的方法 max(iterable) iterable最大的元素 ^^ sorted(iterable) 返回一个迭代器,是排好序的iterable,排序是stable的 ^^ sum(iterable, start=0) 从start开始左往右对所有数据求和 ^^ all(iterable) iterable所有元素是否都是真 ^^ any(iterable) iterable是否存在某个元素是真 ^^ enumerate(iterable, start=0) 返回一个迭代器,这个迭代器返回的元素是一个由序号(从start开始编号)和原可迭代对象的值组成的二元组,经常被用于for循环 ^^ map(function, iterable) 返回一个迭代器,这个迭代器返回的元素是传给function得到True的元素 ^^ filter(function, iterable) 返回一个迭代器,这个迭代器返回的是传给iterable元素给function得到的结果 ^^ zip(*iterables) 返回一个迭代器,这个迭代器依次同时取出所有可迭代对象的元素,组成一个元组返回,经常用于for循环中 ^^ 下面给出一些例子: >>> a = list(range(5, 10)) # 产生一个5到9的列表 >>> a [5, 6, 7, 8, 9] >>> import random >>> random.shuffle(a) # 随机打乱这个列表 >>> a [5, 9, 7, 6, 8] >>> min(a) 5 >>> max(a) 9 >>> sum(a) 35 >>> list(enumerate(a)) # 由于enumerate返回的是迭代器,所以需要list来转换为列表,下同 [(0, 5), (1, 9), (2, 7), (3, 6), (4, 8)] >>> for i, v in enumerate(a): # 这个是enumerate经常被使用的方式 ... print(str(i) + ': ' + str(v)) ... 0: 5 1: 9 2: 7 3: 6 4: 8 >>> list(filter(lambda x: x > 6, a)) # 筛选出列表中大于6的元素 [9, 7, 8] >>> list(map(lambda x: x * 2, a)) # 将列表中所有元素乘以2 [10, 18, 14, 12, 16] >>> all(map(lambda x: x > 5, a)) # 测试是否所有元素都大于5 False >>> any(map(lambda x: x > 5, a)) # 测试是否存在元素大于5 True >>> list(zip(range(5, 10), range(10, 5, -1))) [(5, 10), (6, 9), (7, 8), (8, 7), (9, 6)] 序列的操作 所谓序列就是那些能够通过整数索引元素s[i]、并能通过len()函数获取长度的对象,所有的序列对象一定是可迭代对象(在先前的继承图中你可以看到Sequence继承自Iterable)。可变的序列是普通序列的子类型,除继承得到的方法之外,更进一步支持了对索引赋值s[i] = value、删除索引del s[i]和插入元素s.insert(index, value)这一些操作。这些继承关系可以用下图表示。 这里这些概念有些复杂,我们把这些操作制作成表格,方便大家理解,而后我们给出示例代码。首先是序列的操作列表,list、tuple、str、bytes、bytearray和range都支持下表中的操作: 操作 结果 注释 来源 x in s 如果s的一个元素为x,则为True否则为False 如str、bytes和bytearray的一些序列使用in进行子串匹配 Sequence提供的方法 x not in s 如果s的一个元素为x,则为False否则为True ^^ ^^ s + t 连接序列s和t,返回新的序列 这种连接如果需要执行多次会有较高的时间开销 某些序列拥有的额外方法,range之类的序列没有 s * n和n * s 重复序列s n次 如果n为负,则当做0处理返回空串,注意s中的对象发生的是浅拷贝 ^^ s[i] 第$i$个元素,从0开始计数 如果$i$或$j$是负数,则分别等价于len(s) + i或len(s) + j,但-0仍是0 Sequence要求的方法 s[i:j] 找出所有下标$k$满足$k\in[i, j)$ ^^ ^^ s[i:j:k] 找出所有下标$x$满足$\exists n\in [0,\frac{j-i}{k}), x=i+nk$ ^^ ^^ len(s) s的长度 ^^ iter(s) 返回s的迭代器 Sequence提供的方法 reversed(s) 返回s的反向迭代器 Sequence提供的方法 s.index(x[, i[, j]]) x出现在s中的第一次位置的下标,额外的参数基本等价于s[i:j].index(x),找不到会抛出ValueError异常 ^^ s.count(x) s中出现了x的总数 ^^ 下面是一些示例代码: >>> s, t = list(range(5)), list(range(5, 10)) >>> s [0, 1, 2, 3, 4] >>> t [5, 6, 7, 8, 9] >>> 5 in s False >>> 5 not in s True >>> s + t [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> s * 2 [0, 1, 2, 3, 4, 0, 1, 2, 3, 4] >>> s[-1] 4 >>> s[::-1] # 一种颠倒序列的方式 [4, 3, 2, 1, 0] >>> len(s) 5 >>> list(reversed(s)) # 另一种颠倒序列的方式 [4, 3, 2, 1, 0] >>> s.index(3) 3 >>> s.count(1) 1 对于索引和切片下标值的含义,我们还是祭出下方的这个图,这是字符串Python的索引对应的位置。索引从0开始计数,索引6是个非法的索引,它越界了。此外还要注意切片是左闭右开的。 接着是可变序列的操作列表,list和bytearray支持下表中的操作。 操作 结果 注释 来源 s[i] = x s中的第i个元素被替换为x MutableSequence要求的方法 s[i:j] = t s从i到j(不包括j)的切片被替换为序列t ^^ del s[i:j] 等价于s[i:j] = [] ^^ s[i:j:k] = t 将s[i:j:k]中的元素替换为序列t 切片和t的长度必须相等 ^^ del s[i:j:k] 移除s[i:j:k]中的元素 ^^ s.append(x) 将x添加到s的末尾,等价于s[len(s):len(s)] = [x] MutableSequence提供的方法 s.clear() 移除s中的所有元素,等价于del s[:] Python 3.3新加入的方法 某些可变序列拥有的额外方法 s.copy() 创建s的浅拷贝,等价于s[:] ^^ ^^ s.extend(t)和s += t 用序列t扩展s,等价于s[len(s):len(s)] = t MutableSequence提供的方法 s *= n 更新s的内容重复n词 内容会被浅拷贝 某些可变序列拥有的额外方法 s.insert(i, x) 将x插入到s下表为i的地方,等价于s[i:i] = [x] MutableSequence要求的方法 s.pop([i]) 返回下表为i的元素并且移除它,默认i为-1 MutableSequence提供的方法 s.remove(x) 移除第一个值等于x的元素,没找到时抛出ValueError异常 ^^ s.reverse() 将s中的元素倒过来 ^^ 下面是示例代码: >>> s = list(range(5)) >>> s [0, 1, 2, 3, 4] >>> s[0] = 5 >>> s [5, 1, 2, 3, 4] >>> s[1:3] = [2, 1] >>> s [5, 2, 1, 3, 4] >>> s[::-1] = s # 一种原处颠倒序列的方法,和 s[:] = s[::-1] 以及 s.reverse() 等价 >>> s [4, 3, 1, 2, 5] >>> del s[::2] # 删除下标是偶数的元素 >>> s [3, 2] >>> s.append(6) >>> s [3, 2, 6] >>> s += [7, 8] >>> s [3, 2, 6, 7, 8] >>> s *= 2 >>> s [3, 2, 6, 7, 8, 3, 2, 6, 7, 8] >>> s.pop() 8 >>> s [3, 2, 6, 7, 8, 3, 2, 6, 7] >>> s.remove(8) >>> s [3, 2, 6, 7, 3, 2, 6, 7] >>> s.reverse() >>> s [7, 6, 2, 3, 7, 6, 2, 3] 注意s * n以及s *= n都是进行浅拷贝,这在二维数组的时候会出现问题,看下面的例子: >>> s = [[0]] * 3 >>> s [[0], [0], [0]] >>> s[0][0] = 1 >>> s [[1], [1], [1]] 如果你想要避免这种情况,建议使用列表推导式。这里先给出代码: >>> s = [[0] for _ in range(3)] >>> s [[0], [0], [0]] >>> s[0][0] = 1 >>> s [[1], [0], [0]] 此外list还提供了以下方法: list.sort(key=None, reverse=False):将列表排序。key是一个函数,接受元素,返回排序的键,如果reverse为True,改为由大到小排序,排序一定是stable的。 示例代码如下: >>> fruits = ['orange', 'apple', 'banana'] >>> fruits_sorted = fruits[:] >>> fruits_sorted.sort() # 普通的排序方法 >>> fruits_sorted ['apple', 'banana', 'orange'] >>> fruits_with_index = list(enumerate(fruits)) # argsort的排序方法 >>> fruits_with_index [(0, 'orange'), (1, 'apple'), (2, 'banana')] >>> fruits_with_index.sort(key=lambda x: x[1]) >>> fruits_with_index [(1, 'apple'), (2, 'banana'), (0, 'orange')] >>> fruits_argsorted = list(map(lambda x: x[0], fruits_with_index)) >>> fruits_argsorted # argsort排序完成,得到的结果就是数字。数组是从小到大各个元素的下标 [1, 2, 0] 获取s[i](包括切片)都会调用s.__getitem__(i),s[i] = t会调用s.__setitem__(i, t),而del s[i]会调用s.__getitem__(i)。实际上如何处理i是负数、切片之类的完全有对象s掌管。如果i不是一个合适的类型,可以抛出TypeError异常;对于序列类型,如果i越界,可以抛出IndexError异常;对于映射类型,如果i不存在,可以抛出KeyError异常。 len(s)会调用s.__len__(),这个方法应当返回$geq 0$的整数。 reversed(s)会先尝试调用s.__reversed__()得到逆序迭代器,如果这个方法不存在,它会试图创建一个迭代器,这个迭代器会,从s.__len__() - 1一直递减到0,调用iterable.__getitem__()获取元素。 列表推导式 列表推导式是一种创建列表更简介的方式。让我们先以创建一系列的平方数为例: >>> squares = [] >>> for x in range(10): ... squares.append(x**2) ... >>> squares [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] 可以看到这里变量x在循环结束之后依旧存在。我们可以用下面不带任何副作用的函数式写法: squares = list(map(lambda x: x**2, range(10))) 但是这种写法可读性不是特别高,我们可以用下面的列表推导式的写法: squares = [x**2 for x in range(10)] 列表推导式语法上是由一个中括号括住的里面包含一个表达式紧跟for子句而后可跟任意数目的for和if子句。其结果就是一个新的列表,列表里面是在for和if语句上下文中对表达式求值得到值。 举个例子,下面的列表推导式会结合两个列表中的元素,如果它们是不相同的: >>> [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y] [(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)] 它和下面的代码是等价的: >>> combs = [] >>> for x in [1,2,3]: ... for y in [3,1,4]: ... if x != y: ... combs.append((x, y)) ... >>> combs [(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)] 此外列表推导式也可以嵌套: >>> matrix = [ ... [1, 2, 3, 4], ... [5, 6, 7, 8], ... [9, 10, 11, 12], ... ] >>> [[row[i] for row in matrix] for i in range(4)] [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]] 这等价于: >>> transposed = [] >>> for i in range(4): ... transposed.append([row[i] for row in matrix]) ... >>> transposed [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]] 又进一步等价于: >>> transposed = [] >>> for i in range(4): ... # the following 3 lines implement the nested list comprehension ... transposed_row = [] ... for row in matrix: ... transposed_row.append(row[i]) ... transposed.append(transposed_row) ... >>> transposed [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]] 字典 字典属于可变映射类型,它可以存储键值对。 字典的字面量 字典的字面量是由一对花括号{},逗号分隔的键值对key: value组成,最后一个键值对的末尾可以有逗号,,这里key和value可以是任意表达式,但需要注意key表达式的结果一定是可哈希对象(hashable),想知道那些对象是可哈希对象可以查看本章开头的那张图,继承自Hashable的对象都是可哈希对象,所有的数学类型、元组、字符串、bytes是可哈希的。在Python标准库中不可变对象都是可哈希的,可变对象都不是可哈希的,但实际上可变和可哈希没有必然的联系。 可能大家还是不清楚什么叫“哈希”,哈希的意思是将对象的内容映射到一个整数,这种映射就像一个指纹,这对于哈希表这种数据结构是必须的,而哈希表在存储键值对的数据中有很优秀的性能。 >>> {'a': 1, 1: 'a', ('1', 1): 'a'} {'a': 1, 1: 'a', ('1', 1): 'a'} >>> { ... 'a': 1, # 末尾可以跟逗号 ... } {'a': 1} >>> {['1', 1]: 'a'} # 列表不是可哈希对象 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'list' >>> {} # 空的字典 {} 所谓可哈希对象必须满足两个条件: hashable.__hash__()方法返回一个整数,这个整数在对象存在期间不会发生变化。该函数会被内置函数hash(object)调用; hashable.__eq__(other)方法可以判断对象是否相等,对于相等的对象,它们的哈希值必须相等。该函数会被二元运算符==调用。 一旦定义了__hash__()方法,从逻辑上而言必须定义__eq__(other)方法。 对于一个用户自定义的类,默认就是有上述两个方法的,这个时候x == y,等价于x和y就是同一个对象。且这时,x和y拥有相同的哈希值即hash(x) == hash(y)。 如果一个自定义的类覆盖了__eq__()方法,却没定义__hash__()方法,则其__hash__会隐式地成为None,这会使得它被hash()调用时返回TypeError。如果这个类是一个子类,它可以通过__hash__ = ParentClass.__hash__沿用父类的哈希方法。 如果一个自定义类想彻底禁用哈希,可以__hash__ = None。 此外,字典的字面量中还允许字典的解包语法,这和函数的解包是一样的。 当字典的字面量包含重复的键的时候,后面的那个会起效。 >>> a = {'a': 0, 'b': 1} >>> b = {**a, 'a': 1} # 解包 >>> b {'a': 1, 'b': 1} >>> {1: 'a', 1.0: 'b'} # 由于 1 == 1.0 ,后面的键值对会替代前面的 {1: 'b'} 从Python 3.7开始,映射的迭代顺序一定遵循插入顺序。 其他构造出字典的方式 dict是字典的构造函数。dict可以接受其他映射对象构造出一个新的字典,也可以从可迭代对象构造出新的字典,这个可迭代对象的元素是包含键和值的二元素可迭代对象。dict还接受关键字参数,可以给先前说的两种构造方式添加新的元素。 当出现重复的键时,后面的有效,由于关键字参数位于最后,所以关键字参数总归有效。我们来看例子: >>> a = {'a': 0, 'b': 1} >>> dict(a, a=1) {'a': 1, 'b': 1} >>> dict([('a', 0), ('b', 1), ('c', 2)]) {'a': 0, 'b': 1, 'c': 2} >>> dict(zip(['a', 'b', 'c'], range(3))) {'a': 0, 'b': 1, 'c': 2} 映射的操作 操作 结果 来源 d[k] 返回键k对应的值,如果该值不存在,抛出KeyError异常 Mapping要求的方法 len(d) 返回d中元素的个数 ^^ iter(d) 返回遍历d所有键的迭代器,等价于iter(d.keys()) ^^ d[k] = v 将d[k]设置为v,如果’k’存在会更新,不存在会插入 MutableMapping要求的方法 del d[k] 删除d[k],如果k存在会抛出KeyError异常 ^^ k in d和k not in d 判断键k是否存在 Mapping提供的方法 d1 == d2和d1 != d2 判断两个字典是否相等,只要有相同的键值对的字典就认为相同,这与插入顺序无关 ^^ d.keys() 返回字典键的视图,这是一个包含了字典所有键的集合,详见字典的试图 ^^ d.items() 返回字典键值对的试图,这是一个包含了(key, value)二元组的集合,详见字典的试图 ^^ d.values() 返回字典值的试图,这是一个包含了所有值的迭代器,注意d.values() == d.values()为False ^^ d.get(key, default=None) 类似d[key],只是如果该值不存在会返回default ^^ d.pop(key[, default]) 如果key在字典中,移除它并返回,否则返回default,如果default也没指定,抛出KeyError异常 MutableMapping提供的方法 d.popitem() 返回(key, value)二元组键值对,并移除它,如果字典空会抛出KeyError,对于字典,最后插入的会最先pop出来 ^^ d.clear() 移除字典中所有的元素 ^^ d.update([other, ]**kwargs) 和[其他构造出字典的方式]#_1-6-2-其他构造出字典的方式)类似,只是会保留或者更新字典中的键值对,other可以是映射或者包含键值对的可迭代对象,也可以携带关键字参数 ^^ d.setdefault(key, default=None) 等价于d.get(key, default),如果key不存在,会执行d[key] = default ^^ d.copy() 返回字典的一个浅拷贝 字典对象的额外方法 同样的,对于上述操作,我们会给出示例,置于字典的视图,更详细的示例代码会在字典的试图那一节给出。 >>> d = {'a': 1, 'b': 2} >>> len(d) 2 >>> list(d) # 等价于list(d.keys()) ['a', 'b'] >>> d['c'] = 3 >>> d {'a': 1, 'b': 2, 'c': 3} >>> del d['a'] >>> d {'b': 2, 'c': 3} >>> 'a' in d False >>> d == {'c': 3, 'b': 2} # 可以看出顺序并不影响是否相等 True >>> list(d.items()) [('b', 2), ('c', 3)] >>> list(d.values()) [2, 3] >>> d.get('d', 4) 4 >>> d.pop('c', 5) 3 >>> d {'b': 2} >>> d.update([('a', 1), ('c', 3)]) >>> d.popitem() # 最后插入的是('c', 3),所以它被pop了出来 ('c', 3) >>> d {'b': 2, 'a': 1} >>> d.clear() >>> d {} >>> d.setdefault('a', 0) 0 >>> d {'a': 0} >>> d.copy() {'a': 0} 对于字典d[k]的操作,如果字典的子类型实现了d.__missing__(key)的方法,当k找不到的时候就会调用并返回d.__missing__(k)。 字典的视图 dict.keys()、dict.values()和dict.items()返回的是视图对象。它们是动态的,也就是说如果字典发生了变化,这些视图也会发生变化。下面这张图显示了各个视图对象的关系。 所有的视图都支持下列操作: 操作 结果 来源 len(view) 得到字典元素的个数 MappingView提供的方法 iter(view) 按照插入顺序遍历,边遍历边插入删除元素会抛出RuntimeError ItemsView、KeysView和ValuesView提供的方法 x in view 判断x是否是键、值或键值对 ^^ dict.keys()返回的对象类似集合,而如果字典的值也是可哈希对象,dict.items()返回的对象也类似集合。这里类似集合是指可以用判断是否相等的运算(==和!=)、判断子集的运算(<、<=、>和>=)、交(&)、并(|)、差(-)、对称差(^)和isdisjoint()。具体集合的操作可以查看集合和frozenset。 下面是例子: >>> dishes = {'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500} >>> keys = dishes.keys() >>> values = dishes.values() >>> # 迭代,会调用iter(values) >>> n = 0 >>> for val in values: ... n += val >>> print(n) 504 >>> # keys和values按照同样的顺序(插入顺序) >>> list(keys) ['eggs', 'sausage', 'bacon', 'spam'] >>> list(values) [2, 1, 1, 500] >>> # 视图对象是动态的,会反映字典的变化 >>> del dishes['eggs'] >>> del dishes['sausage'] >>> list(keys) ['bacon', 'spam'] >>> # 集合操作 >>> keys & {'eggs', 'bacon', 'salad'} {'bacon'} >>> keys ^ {'sausage', 'juice'} {'juice', 'sausage', 'bacon', 'spam'} 最后我指出一下,其实视图对象用得最多的地方是for循环。如果你只需要遍历键,就可以for k in d:(d是个字典)或者for k in d.keys():;如果只需要遍历值,就可以for v in d.values():;如果需要遍历键和值,就可以for k, v in d.items():。 字典推导式 与列表类似,字典也有推导式。字典的推导式语法与列表类似,只是使用花括号{}括起,单个表达式也变成了:分隔的两个表达式: >>> {x: x**2 for x in (2, 4, 6)} {2: 4, 4: 16, 6: 36} 集合和frozenset 集合是一序列互不相同的可哈希对象组成的无序容器,它们可以被用于测试in关系(比一般的序列快),去重,以及进行交并补计算。set和frozenset的差别在于前者是可变的、不可哈希的,而frozenset是不可变的、可哈希的。 集合字面量 集合的字面量和字典的字面量比较类似,都是用一对花括号{}括起的,并用逗号分隔的。不同之处是字典是键值对key: value组成的一对表达式,而集合则只包含一个表达式: >>> {1, 3, 2} # 集合元素迭代的顺序不一定遵循插入顺序 {1, 2, 3} >>> {1, 1, 3.0, 3, 2} # 集合会去除掉重复的元素 {1, 2, 3.0} 集合字面量同样支持可迭代对象的解包: >>> a = [1, 2, 3] >>> b = [2, 3, 4] >>> {*a, *b} {1, 2, 3, 4} 可迭代对象构与集合和frozenset之间的转换 集合和frozenset本身就是可迭代对象,这意味着你获取字典和frozenset的迭代器、将他们转换成列表或元组、用for循环遍历,具体支持的操作可以看可迭代对象的操作,当然仍需要指出集合和frozenset迭代的顺序不一定遵循插入顺序。 >>> a = {1, 3, 2} >>> [2 * i for i in a] [2, 4, 6] >>> tuple(a) (1, 2, 3) 另一方面集合set和frozenset的构造函数可以接受一个可迭代对象,构建出新的集合和frozenset对象,其中重复的元素会被消除掉。 >>> a = [1, 2, 3, 2, 1] >>> set(a) {1, 2, 3} >>> frozenset(a) frozenset({1, 2, 3}) 所以这两个操作和在一起,先把可迭代对象转成集合,再从集合转换为可迭代对象可以成功地消除重复的元素: >>> a = [1, 2, 3, 2, 1] >>> list(set(a)) # 一个典型的去重操作 [1, 2, 3] 集合和frozenset的操作 从上图我们可以看到集合和frozenset的继承关系,首先我们可以看到所有集合都共有的一些操作: 操作 结果 来源 len(s) s的元素个数 Set要求的方法 x in s / x not in s 测试s是否包含/不包含x ^^ set.isdisjoint(other) 返回两个集合是否没有相同的元素 Set提供的方法 set <= other_set / set < other_set / set >= other_set / set > other_set 测试set是否是other_set的子集/真子集/超集/真超集 ^^ `set other_set/set & other_set/set - other_set/set ^ other_set` 返回set和other_set的并集/交集/差集/对称差 issubset(other_iterable) / issuperset(other_iterable) 几乎等价于set <= other/set >= other set和frozenset特有的方法 set.union(*other_iterables) / set.intersection(*other_iterables) / set.difference(*other_iterables) 几乎等价于`set other set.symmetric_difference(other_iterable) 几乎等价于set ^ other ^^ set.copy() 返回set的浅拷贝 ^^ 这里需要指出交、并、差、对称差这4个操作和子集、超集都有对应的两个版本:运算符和方法。这两个我们说是“几乎”等价的,差别在于运算符版要求操作数必须同样是集合类型的,而方法则可以接受可迭代对象,除此之外,某些方法还接受任意多参数。下面我们即将讲到的可变集合操作也类似。 集合和frozenset之间是可以混合运算的,所以我们有set('abc') == frozenset('abc')。对于交、并、差、对称差,如果存在混合操作,返回的类型是第一个操作数的类型。 这里我们需要指出,集合之间的<,>和==并不组成全序关系,也就是说可能存在两个集合a和b,a < b、a > b和a == b都为假,这和我们学习到的实数之类的序的关系是不一样的。 集合的交、并和对称差&、|、^可能对于新手不那么容易记,这3个符号来自于C系语言位运算,和集合运算也恰恰是对应上的。之后我们会在表达式中再次遇到这3个运算符作用在整数上的情况。 接下来我们来看看上面运算的例子: >>> a = {1, 2, 3} >>> b = {2, 3, 4} >>> c = {1, 2, 3, 4} >>> len(c) 4 >>> a.isdisjoint(b) False >>> a < c # 真子集 True >>> a & b # 交 {2, 3} >>> a ^ b # 对称差 {1, 4} >>> c.intersection(a, b) # a、b和c的交集 {2, 3} 上面所讲的是普通集合,也就是集合和frozenset都支持的操作。下面我们来讲可变集合,也就是集合set支持的操作。 操作 结果 来源 `set = other_set/set &= other_set/set -= other_set/set ^= other_set` 等价于`set = set set.update(*other_iterables) / set.intersection_update(*other_iterables) / set.difference_update(*other_iterables) 等价于set = set.union(*other_iterables)/set = set.intersection(*other_iterables)/set = set.difference(*other_iterables) set特有的方法 set.symmetric_difference_update(other_iterable) 等价于set =set.symmetric_difference(other_iterable) ^^ set.add(elem) 向set中添加元素elem MutableSet要求的方法 set.discard(elem) 向set中移除元素elem,如果elem不存在不报错 ^^ set.remove(eleme) 从set中移除元素elem,如果elem不存在则抛出KeyError错误 MutableSet提供的方法 set.pop() 移除一个元素并返回它 ^^ set.clear() 移除集合中的所有元素 ^^ 同样地,我们来看一下例子: >>> a = {1, 2, 3} >>> b = {4, 5} >>> a |= b >>> a {1, 2, 3, 4, 5} >>> a.add(7) >>> a.discard(6) >>> a.remove(6) Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 6 >>> a.pop() 1 >>> a {2, 3, 4, 5, 7} >>> a.clear() >>> a set() 集合推导式 集合的推导式和列表的很像,只是使用了花括号括起: >>> {x**2 for x in (2, 4, 6)} {16, 4, 36} 复数 Python有内置对复数的支持。 Python提供了构造纯虚数的方法,通过在一个整数或浮点数后面添加j或J就能产生实部为0,虚部为该数的纯虚数。这里要注意单位虚数$i$不是写作j,而是1j。同时复数也有构造函数,分别接受实部和虚部,构造新的复数。 >>> 1 + 1j # 产生复杂复数的方法 (1+1j) >>> complex(1, 2) (1+2j) 复数支持加减乘除和次方运算,此外还有一些额外的操作,见下面的例子: >>> a = 1 + 2j >>> abs(a) # 求模 2.23606797749979 >>> a.real # 取实部 1.0 >>> a.imag # 取虚部 2.0 >>> a.conjugate() # 求共轭复数 更多的复数操作在cmath中,比如获取复数的幅角,计算附属的指对、三角等函数。 习题 A+B问题 A+B问题:题目要求 输入两个浮点数,输出它们的和,你可以有适当的输出提示来让用户输入浮点数和显示结果。 A+B问题:样例输入输出 Please input the first real number: 1.2 Please input the second real number: 2.3 1.2 + 2.3 = 3.5 A+B问题:提示 input()函数用于输入; print()函数用于输出: 浮点数与字符串的转换。 A+B问题:答案 if __name__ == '__main__': a = float(input('Please input the first real number: ')) b = float(input('Please input the second real number: ')) print(str(a) + ' + ' + str(b) + ' = ' + str(a + b)) 判断正负 判断正负:题目要求 输入一个浮点数,输出它是正数、负数还是0。 判断正负:样例输入输出 Please input a real number: -1 -1.0 is a negative number 判断正负:提示 使用if语句来判断。 判断正负:答案 if __name__ == '__main__': number = float(input('Please input a real number: ')) if number > 0.0: print(str(number) + ' is a positive number') elif number < 0.0: print(str(number) + ' is a negative number') else: print(str(number) + ' is zero') 最大值与最小值 最大值与最小值:题目要求 输入一系列的浮点数,直到输入为0停止,输出他们的最大值最小值平均数和和。 最大值与最小值:样例输入输出 Please input a real number (input 0 to stop): 2 Please input a real number (input 0 to stop): 1 Please input a real number (input 0 to stop): 3 Please input a real number (input 0 to stop): 0 The maximum number is 3.0 The minimum number is 1.0 The average number is 2.0 The sum number is 6.0 最大值与最小值:提示 使用while语句来制造循环; 使用break语句来终止循环。 使用max()、min()和sum()函数,注意max()函数既可作用域可迭代对象max(iterable),也可直接比较两个数max(a, b),min()函数也同样。 最大值与最小值:答案 对于这道题,我们有两种解法。第一种是存下所有的数字,这种方法的代码较为简短,这里需要额外指出的是如果是空的序列min()和max()会报错,平均数也有可能除零异常 if __name__ == '__main__': numbers = [] while True: number = float(input('Please input a real number (input 0 to stop): ')) if number == 0: break numbers.append(number) if numbers: print('The maximum number is ' + str(max(numbers))) print('The minimum number is ' + str(min(numbers))) sum_of_numbers = sum(numbers) print('The average number is ' + str(sum_of_numbers / len(numbers))) print('The sum number is ' + str(sum_of_numbers)) else: print('You entered a empty sequence') 另一方式就是,只存放最大值、最小值和和。这样空间性能会比较好,如果有大量的甚至超出内存承受范围的浮点数需要输入,也不会出现问题。 import math if __name__ == '__main__': max1, min1, sum1, n = -math.inf, math.inf, 0.0, 0 while True: number = float(input('Please input a real number (input 0 to stop): ')) if number == 0: break max1 = max(max1, number) min1 = min(min1, number) sum1 += number n += 1 if n: print('The maximum number is ' + str(max1)) print('The minimum number is ' + str(min1)) print('The average number is ' + str(sum1 / n)) print('The sum number is ' + str(sum1)) else: print('You entered a empty sequence') 判断回文串 判断回文串:题目要求 输入一行字符串,判断是不是回文字符串(正过来和倒过来都一样的字符串),如abcdcba就是回文数。 判断回文串:样例输入输出 Please input a line: aba It is a palindrome 判断回文串:提示 字符串的切片方法。 判断回文串:答案 这道题我们也有几种解法,第一种解法非常简单,使用反向的切片就可以得到原来字符串反过来的字符串。 if __name__ == '__main__': line = input('Please input a line: ') if line == line[::-1]: print('It is a palindrome') else: print('It is not a palindrome') 但是我们会发现这个实现中创建了一个临时字符串,所以第一种改进方法是不再创建这个临时字符串: if __name__ == '__main__': line = input('Please input a line: ') for a, b in zip(line, reversed(line)): if a != b: print('It is not a palindrome') break else: print('It is a palindrome') 最后我们还会发现回文字符串多比较了几次,所以稍加改动性能就可以翻倍,这里只改动了第3行: if __name__ == '__main__': line = input('Please input a line: ') for a, b, _ in zip(line, reversed(line), range(len(line) // 2)): if a != b: print('It is not a palindrome') break else: print('It is a palindrome') 统计字符数目 统计字符数目:题目要求 输入一行字符串,统计英文字符(包括大小写)和数字字符的个数。 统计字符数目:样例输入输出 Please input a line: a1h45 Number of alphas is 2 Number of digits is 3 统计字符数目:提示 常见的字符串方法有一些判断字符类型的方法。 统计字符数目:答案 if __name__ == '__main__': line = input('Please input a line: ') n_alpha, n_digit = 0, 0 for char in line: if char.isalpha(): n_alpha += 1 elif char.isdigit(): n_digit += 1 print('Number of alphas is ' + str(n_alpha)) print('Number of digits is ' + str(n_digit)) 统计单词数目 统计单词数目:题目要求 输入一行字符串,字符串包含若干单词(单词中不包含空白符),单词之间有空白符,统计每个单词出现的个数,并将所有的单词按照字母表顺序排列出来: 统计单词数目:样例输入输出 Please input a line: what the fuck does what the fuck mean does: 1 fuck: 2 mean: 1 the: 2 what: 2 统计单词数目:提示 你应当使用字典之类的类型存储个数; 你可以使用sorted()函数或list.sort()方法。 统计单词数目:答案 if __name__ == '__main__': line = input('Please input a line: ') words = line.split() counter = {} for word in words: counter[word] = counter.get(word, 0) + 1 for key, value in sorted(counter.items()): print(str(key) + ': ' + str(value)) 考拉兹猜想 考拉兹猜想:题目要求 考拉兹猜想,又称为奇偶归一猜想、3n+1猜想、冰雹猜想、角谷猜想、哈塞猜想、乌拉姆猜想或叙拉古猜想,是指对于每一个正整数,如果它是奇数,则对它乘3再加1,如果它是偶数,则对它除以2,如此循环,最终都能够得到1。(来自维基百科)。 $$f(n) = \begin{cases} n/2 &\mbox{if } n \equiv 0 \ 3n+1 & \mbox{if } n\equiv 1 \end{cases} \pmod{2}$$ 我们的目标是输入一个整数,输出考拉兹猜想对应的序列,如输入为6,序列就是6,3,10,5,16,8,4,2,1。 考拉兹猜想:提示 你需要熟悉一下数学运算。 考拉兹猜想:样例输入输出 Please input a number: 6 6 3 10 5 16 8 4 2 1 考拉兹猜想:答案 if __name__ == '__main__': num = int(input('Please input a number: ')) while True: print(num) if num == 1: break elif num % 2 == 0: num //= 2 else: num = 3 * num + 1 快速幂 快速幂:题目要求 Python内置的**运算符其实就是快速幂的一种实现。快速幂是求一个数的整数次方时采用的算法,它的计算方法来源于这个式子: $$x^n = \begin{cases} 1, & \mbox{if } n = 0 \ x , ( x \times x)^{\frac{n - 1}{2}}, & \mbox{if } n \mbox{ is odd} \ (x \times x)^{\frac{n}{2}} , & \mbox{if } n \mbox{ is even}. \end{cases}$$ 你的目标是输入两个整数$a$和$b$,求$a^b$而不使用**运算符。我们强烈建议初学者使用递归来解决,这里给出递归的模板代码: def power(x, n): # Your need to call power inside this function pass if __name__ == '__main__': x = int(input('Please input the base: ')) n = int(input('Please input the exponent: ')) print(str(x) + '^' + str(n) + ' = ' + str(power(x, n))) 快速幂:样例输入输出 Please input the base: 2 Please input the exponent: 5 2^5 = 32 快速幂:答案 def power(x, n): if n == 0: return 1 elif n % 2: return x * power(x * x, (n - 1) // 2) else: return power(x * x, n // 2) if __name__ == '__main__': x = int(input('Please input the base: ')) n = int(input('Please input the exponent: ')) print(str(x) + '^' + str(n) + ' = ' + str(power(x, n))) 统计新出现的单词 统计新出现的单词:题目要求 输入两行字符串,这两行字符串包含若干单词(单词中不包含空白符),单词之间有空白符,输出那些出现在第二行但不出现在第一行单词。 统计新出现的单词:样例输入输出 Please input the first line: how are you today Please input the second line: how old are you old 绘制曼德博集合 绘制曼德博集合:题目要求 在做这个项目之前你需要执行下面的命令,来安装两个依赖: pip install numpy matplotlib 曼德博集合$M$是一个复平面上美丽的分形图形。我们的目标就是用Python绘制这样一个图形。它的定义是这样的:对于一个复数$c$,构建一个数列: $$\begin{cases} z_0 = 0 \ z_{n+1}=z_n^2+c, n=0,1,2,\dots \end{cases}$$ 如果数列${z_n}$的绝对值${|z_n|}$是发散的,那我们就说$c\in M$。 在我们的这次题目中,问题被简化。我们不太可能迭代无穷次来判断数列是否发散。此外我们发现只要存在$n$使$z_n\geq 2$,那么$c\notin M$。所以,总而言之,你需要做的就是完善下面代码中的mandelbrot函数。你需要最多迭代MAX_ITER次,计算出$z_1,z_2,\dots,z_{256}$,一旦遇到某个$z_n\geq2$,就返回$n - 1$,如果始终没有遇到,就返回MAX_ITER。 import numpy as np import matplotlib.pyplot as plt MAX_ITER = 256 def mandelbrot(c): # TODO pass def main(): resolution = 512 c = np.linspace(-2, 1, resolution)[np.newaxis, :] \ + 1j * np.linspace(-1.5, 1.5, resolution)[:, np.newaxis] result = np.vectorize(mandelbrot)(c) plt.imshow(np.log(result + 1)) plt.show() if __name__ == '__main__': main() 绘制曼德博集合:样例输出

2020/2/6
articleCard.readMore

ZeptoVM服务器配置

这篇文章是关于如何配置我的ZeptoVM服务器的。目前下来我尝试下来上网最快的服务器就是ZeptoVM。其上面运行的主要服务是某科学的超能上网。目前服务器被绑定到了域名szp.io上。其Web服务会重定向到szp15.com上。 服务器信息 以下是服务器的信息。 项目 数值 CPU 1 vCore RAM 512MiB Disk 10 GiB NVMe SSD Bandwidth 10 Gbps Traffic 1 TiB IPv4 1 * /32 IPv6 1 * /64 OS Arch Linux (Daily Build) Price $8.00 USD 基础配置 创建新用户 # root at szp.io pacman -Sy archlinux-keyring pacman -Syu vim sed -i "s/^# %wheel ALL=(ALL) ALL$/%wheel ALL=(ALL) ALL/" /etc/sudoers useradd -m -G wheel -s /bin/bash sun passwd sun passwd -l root # at local machine cat .ssh/id_rsa.pub | ssh sun@szp.io -T "mkdir -p .ssh && chmod 700 .ssh && cat >> .ssh/authorized_keys" 配置git和zsh等 基本同阿里云服务器配置。注意szp15.com应改为szp.io,apt改为对应的pacman。 开启BBR # at szp.io sudo sh -c 'echo "net.core.default_qdisc=fq" >> /etc/sysctl.d/bbr.conf' sudo sh -c 'echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.d/bbr.conf' sudo reboot 配置web服务器 首先: sudo pacman -S nginx-mainline 将Nginx#Configure example的内容拷贝到/etc/nginx/nginx.conf。 sudo mkdir /etc/nginx/sites-available sudo mkdir /etc/nginx/sites-enabled sudo mkdir /etc/nginx/snippets 而后基本同阿里云服务器配置。注意ArchLinux上letsencrypt的包名和CLI都叫certbot。以及,szp15.com www.szp15.com应改成szp.io www.szp.io ipv4.szp.io ipv6.szp.io。 最后Nginx的配置如下: server { listen 80 default_server; listen [::]:80 default_server; server_name szp.io www.szp.io ipv4.szp.io ipv6.szp.io; access_log /var/log/nginx/szp.io-access.log; error_log /var/log/nginx/szp.io-error.log; include snippets/letsencrypt-acme-challenge.conf; include snippets/ssl-redirect.conf; } server { listen 443 ssl http2 default_server; listen [::]:443 ssl http2 default_server; server_name szp.io www.szp.io ipv4.szp.io ipv6.szp.io; access_log /var/log/nginx/szp.io-access.log; error_log /var/log/nginx/szp.io-error.log; include snippets/ssl-szp.io.conf; include snippets/ssl-params.conf; location / { return 302 https://szp15.com$request_uri; } } 某科学的超能上网 这部分内容可能包含敏感词,等加密插件完成后,会被加密。 sudo -s # root at szp.io pacman -S v2ray pwgen PORT=$(((2 * RANDOM) % (0xffff - 1024) + 1024)) WS_PATH=/$(pwgen) UUID=$(uuidgen) cat << EOF > /etc/v2ray/config.json { "inbounds": [ { "port": $PORT, "listen":"127.0.0.1", "protocol": "vmess", "settings": { "clients": [ { "id": "$UUID", "alterId": 64 } ] }, "streamSettings": { "network": "ws", "wsSettings": { "path": "$WS_PATH" } } } ], "outbounds": [ { "protocol": "freedom", "settings": {} } ] } EOF cat << EOF > /etc/nginx/snippets/v2ray.conf location $WS_PATH { proxy_redirect off; proxy_pass http://127.0.0.1:$PORT; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header Host \$host; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; } EOF 修改/etc/nginx/sites-available/default.conf,加上: include snippets/v2ray.conf; 而后: nginx -t systemctl start v2ray systemctl reload nginx systemctl enable v2ray systemctl enable nginx

2020/2/1
articleCard.readMore

2020新年寄语

这是我的2020新年寄语,主要是总结总结过去的生活。给自己的生活打打气。 一转眼间,我已经24岁了,可能已经度过了1/3或者运气好点1/4的日子。我希望自己能开开心心地过度过自己剩下的小日子,希望自己能成为一个幸运儿,平平安安地,不再像以前那么累。 我的童年 舍长是我第一个完整地听我诉说童年经历的人,倒不是因为舍长有多特殊,而是那时候我已经释怀了。从进入宝山实验开始,我的所有的家庭经历对于同学而言都是保密的。其实到现在,我也并不会到处宣扬,只是会告诉那些愿意了解我的人。这个网站的这个页面,也应该不会有多少人看,所以我就写在这了。毕竟指不定我的记忆还会被这么清除几次。我想记录下这段已经非常模糊的记忆。 其实我最幸福的童年是在北翼商业街背后的违章建筑中度过的,虽然家小得不能再小,雨天还会漏水,但那时候的我无忧无虑。记得那是姐姐在上行知中学,有我姐姐和母亲陪着,我过得很开心。 等我上了小学,母亲找了个后爸。那时候我不觉得这个后爸有什么问题,而且终于有了一个完整的家了,我也很开心。可是直到长大后我才发现这个人是彻头彻尾的人渣。他想要的只是钱和性。 和他离婚后,母亲就开始有点不对了。她四处奔走,想要上访,为自己失去的工作还有薪酬找到补偿。在母亲怀上我的时候,她为了生我旷工了一些时间,而她的单位就因为她的无辜旷工。一开始她只是在宝钢、宝山区人民法院、二中院、高院这几个地方来回跑。然后没人理她的时候,她就会在别人门口坐上一天。我觉得这是遗传了我外婆的那种犟的性格。然而在我四年级下半学期开始,她逼我辍学跟她一起。我一开始是不同意的,虽然那个时候我也并不是好学,只是觉得辍学不好。然而她见我不同意,就用铅笔盒打我,最后我哭着同意了。从此我便跟随母亲到处上访。除了上访之外,还有在家写信,都是她先写好、我再誊写。她认为这样能够让别人更注意我们。在最开始我还是自学的,然后在第二学期期中考试回去了一趟。我至今记得很清楚,我站在操场上,我的班主任对我说:“你以为学校是你想去就去的么?”我当时委屈极了,毕竟又不是我不想来学校。我觉得没人能够理解我。之后我去参加了期中考试。本来我的成绩一般是在前三,那是个小学校所以竞争远没有宝山实验那么激烈。考完试后,成绩我记得好像是前50%左右。看到我的成绩下降了,我当时心里有点难受。不过数学老师安慰了我。她对全班的同学说,我不上学都考得比很多人高。 后来我经历了很多,但有些记不清了,期间有被母亲假装遗弃过,当时我不懂,先被送到幼儿园,而后送到了养老院里,最后送到了一对老夫妇手里。他们没有孩子,非常喜欢我,就领养了我。而后没过多久吧,我的母亲出现了,要把我接回去。两位老人有些不情愿,但最后还是送走我了。临别的时候,我看到他们在哭,我不知道他们为什么哭,母亲让我跟他们说,以后还会再来看他们的。我照着说了,然而其实以后我再也没有看到过她们了。 在离开学校一年后,我已经不抱任何希望了,我觉得我的人生被毁了。这时候母亲也知道这样子对我不好,而恰巧政府非常在意我这个辍学的人,因为他们无法完成100%的义务教学。只要我母亲让我上学,政府愿意把我送到任何学校里去。于是我的母亲就说要把我送到附近最好的宝山实验里。就这样我进了宝山实验,我记得是一位来自法院的阿姨把我送到了学校,班主任很惊讶于我是靠着什么背景转校的,就问我,那位法院阿姨是我什么人,法院阿姨看到我有点尴尬,就赶紧接上说是亲戚。我第一次觉得这些法院的工作者是多么好。 经历了这些,我就越来越恨我的母亲。她那时候开始怀疑外婆之类的亲戚们和别人串通一起搞她,而且姐姐也是怀疑的对象之一,所以她很早就把姐姐赶出家门了。在我五年级的时候,我在我的日记本上记满了对她的仇恨。而不幸的是,我的日记本被母亲发现了。她疯狂地试图抢夺我的日记本,她应该是觉得这日记本是我和别人串通的证据,而我害怕那些咒骂她的话被她看见。最终,她一把夺过我的日记,钻进厕所,把门反锁起来,看了起来。过了好久,她才出来,把日记本还给我。我和她之间什么话都没说。 从那以后我和她的关系就很差。而她也变得越来越偏执。原本还只是四处上访,赖在别人门口不走,或者绝食、吃安眠药。而后来在我上初中的时候,她就开始拒绝一切帮助,也不再尝试别的养家户口的方式,而是尽可能以我们的惨来博取他人的注意。她坚持只拿我父亲给的每月450元的生活费,和每半年给的48元副食品补贴。我向来是不在学校吃午饭的。大多数都是我回家吃饭,不过母亲也送过一阵子饭。从一开始,吃的面疙瘩里还会有些青菜或者油豆腐。到后来我吃的只有两种,白饭和面疙瘩。很快我就有了些营养不良的症状。我在晚上看不见东西了,我的双腿内侧包括生殖器的皮肤开始不停地流血、结痂、脱落,我的嘴角也会不停地裂开、流血,严重的时候连吃东西或者打哈欠都会导致伤口又裂开。到了后来,我又出现了神经性厌食症,我见到饭和面疙瘩就会呕吐,这根本不是我能控制的,甚至即使是吃其他的一些东西,我也会呕吐。我过得痛苦极了,而在这段日子里,只有3位亲人来看过我,或者给我些钱,那就是姐姐、外婆和大姨妈。其他的所有亲戚都根本不在乎我的死活。除此之外,老师们也渐渐了解到我家庭不是特别富裕,而我的母亲又不肯接受任何形式的帮助,所以就偷偷让同学们在中午的时候给我留写饭菜,这就成了我那时候几乎唯一的营养来源。 到了八年级的某一天,我忘记是为了什么了,我刚洗完澡或者还没开始洗,就和母亲吵了起来,我一气之下摔门走了,连鞋子都随便穿得是我妈的。我一开始打算往我姐家走。就沿着地铁3号线,不停地走。才走了没多久,我的双脚就磨出了水泡。于是我就脱下鞋子走。走着走着我的脚就变成了银色的,还能反光,连水泡破出的地方都是一样的银色。不就3号线一转,钻进了高楼大厦间,我无法跟着走了,就照着感觉瞎走。就这样我从晚上离家出走走到了第二天天亮,这时我的双腿真的走不动了,每一步都伴随着剧烈的疼痛。我试图在公共电话亭打电话给姐姐,但打不通。所以最后我只能打电话给外婆了。然后小娘舅让我报出大致的位置,就骑着摩托车来接我了。来到外婆家后,我洗了脚。然后二娘舅也过来了。我说把母亲送到精神病院里去。而小娘舅和二娘舅的反应确实反过来骂我恶毒、不孝。他们认为精神病院是打人的地方,而母亲没有病,它们甚至因此想打我,我太委屈了,只有外婆她们开脱说小孩子不懂事。这是我最恨我的舅舅们的地方了,他们因为自己的愚昧无知和要面子,害得我这么早失去了母亲。 我过得营养不良,而我的母亲就更惨了,毕竟如果有东西可以吃,她会留给我。大概是某个暑假,我好像去我姐姐家住了,具体什么情况我有点记不太清了。总之,姐姐接到一个电话说母亲摔倒了。我和姐姐立刻赶到宝山家中,发现母亲躺在家门口,站不起来。那时候的母亲已经非常消瘦了。母亲说她梦到了三个萝卜,就想出去买点吃的。可是她那时候根本没力气走路,才到家门口就摔了。没多久亲人也来了。二娘舅把我的母亲背起来去了最近的宝山医院。那时母亲只有30公斤重。宝山医院那边治不了,所以后来就叫了辆救护车送到了瑞金医院。一查是肠梗阻。一番治疗之后,母亲终于算是救过来了。一开始我们说好不再去上访什么的。然而没过了多久,她还是去上访了,走回了老路。 初中毕业时,填报志愿,我故意将寄宿制高中填在了前面,零志愿是交大附中。本来我拿了物理竞赛二等奖中的第一名,是很有机会参加自主招生的。然而我家没有电脑,而我托付的同学又“忘了”帮我报名,就这样我只能凭中考了。然而中考我和交大分数线同分,不同于高考,零志愿人数有限,比完四门成绩再比语数英三门。就这样,我零分只差进入了上大附中。 进入了高中,我只有周末会回家里,而回到家里,我也基本是一句话都不和母亲说。直到高一又一次回去,我隐约听到母亲似乎在哭泣。又过了一周,我回去,反常地,门锁了却敲不开。我等了好久,就撬开了窗,从窗口翻进去这是我发现母亲躺在角落边,近视的我看到母亲的鼻翼似乎还在翕动,就赶紧摇了摇她,见她没有反应,我凑近一看,是蛆从鼻孔中钻出来啃食她的鼻子。我吓呆了,整个人懵了。我赶紧叫来了姐姐还有其他亲戚。最后叫来的120见人死了就走了。警察带我和姐姐去做笔录。警察问我要尸检么,我说不用。他又问我母亲生前说过什么不。我突然想起母亲说过她死后,要把她葬在外公附近。想到这个我就大哭出来了,我告诉姐姐母亲想被葬在外公附近。这个愿望她算是实现了。 后来我的日子就渐渐正常了。只是没有父母的陪伴,而是由姐姐抚养着我。 关于母亲 或许我小时候还恨母亲,但从她去世的那刻开始,我就不这么恨她了,而且还觉得对不起她。她很爱我,只是得了病而已。我讨厌我的舅舅们,他们不把她送进医院治疗,是真的愚昧。可能我很久没去过母亲的坟头上了,我只是不相信有阴间,也不相信人有来世。我认为人只有一辈子,死了就什么都没了。坟墓只是留给那些活着的人的。而我不需要它来安慰我,我憎恶那些虚情假意却还要用礼法限制别人的人,生前该做的事没做,人死了又有什么用。我倒是所有人中一直陪着她的人。 不过过去的都过去了,我是除了母亲之外受伤最多的人,可我只能原谅,不是么? 双相 我原以为我从此就可以过上正常的生活。然而到了大三下接近寒假的时候,据别人说,我突然发疯,以为自己成了预言家之类的。没多久我就被送到安定医院,确诊是得了双相情感障碍。在安定医院治疗了一个多月后,我在春节前回到了家,然而据说没多久又复发了,被送到了上海市精神卫生中心。在那儿我一呆就是四个月,等我记事时,已经是出院前一两个月了。因为做MECT(改良无抽搐电刺激治疗)会极大地影响记忆,我从发病到进市精卫的所有记忆都被抹去了。在医院的日子真是无比的枯燥。每天都是吃了睡睡了吃。 大概在2018年4月末,我出院了。刚开始药物的副作用是很大的,会很嗜睡且会变胖。如今已经将近2年过去了,我很幸运地没再复发,嗜睡的症状也减轻了。只是人已经胖到了200多斤。 新年寄语 希望在新的一年我能顺利毕业。此外希望我的体重能稳定下来,不会再增加。希望今年的武汉疫情能让我出去逛逛。就这么多了吧。平平安安其实就是最大的福报了。

2020/1/27
articleCard.readMore

Python教程2 - 流程控制

本文是Python教程的第2篇。我们会在第1和第2节分别介绍语句和函数,第3节介绍代码风格,第4节介绍PyCharm IDE的使用,最后1节复习我们所学的知识。这篇教程还有Jupyter Notebook版本,点击此处下载。这篇教程的内容主要来自4. 其他流程控制工具 — Python 3.7.5 文档。 语句 接下来我会介绍一些基本的概念,如果觉得很抽象难以理解,可以先学习后面的部分再回来看。即使不能理解这些概念也不会妨碍编程。 语句是构成命令式程序设计的另一个重要组成部分组成,语句之间也可以像表达式一样互相嵌套形成复杂的结构。可以包含其他语句作为子语句的称为复合语句,如我们接下来会讲的if、while和for,反之称为简单语句。 与表达式不同,语句不会产生值,语句的所有意义在于产生副作用。副作用有两种情况: 修改状态,比如对变量赋值:如a = 1; 输入输出:如print('hello')。 在保存运行代码的情况(而非交互模式)下,一个没有副作用的语句,如1 + 1,是没有意义的,可以删除,而且这通常是一个潜在的错误。 接下来我来介绍2种我们接触过的语句: 表达式语句:单独一个表达式可以形成语句,称为表达式语句,如刚才提到的1 + 1和print('hello')都是表达式语句,后者有输出的副作用,而前者毫无作用; 赋值语句:在Python中赋值是一个语句,如a = 1,赋值语句的副作用就是修改状态。 传统的结构化编程包含以下3种复合语句结构: 顺序:将多个语句挨个写出来,就是顺序结构,程序会依次执行这些语句,下面将给出示例; 选择:根据条件只执行一部分语句,如1.1中的if语句; 循环:根据条件重复执行一段语句,如1.2和1.3中的while和for语句。 这里给出一个顺序执行求$\sqrt{2}$的例子,方法是牛顿迭代法($x_{n+1}=\frac{1}{2}(x_n + \frac{S}{x})$数列的极限为$\sqrt{S}$),迭代3次,并和直接计算的结果作比较: a = 2 a = 0.5 * (a + 2 / a) a = 0.5 * (a + 2 / a) a = 0.5 * (a + 2 / a) print('My sqrt is', a) print('Real sqrt is', 2 ** 0.5) 好啦说了这么多(没啥用的),让我们开始学习流程控制语句吧。 if语句 if示例 先看if语句的例子: x = int(input("Please enter an integer: ")) if x < 0: print('Negative changed to zero') elif x == 0: print('Zero') elif x == 1: print('Single') else: print('More') 上面的例子中有两个新函数,这边介绍以下: input():接受一个可选的字符串参数,打印这个字符串,然后读入一行新的字符串,返回读入的字符串(不包括换行); int():接受一个字符串,返回字符串代表的整数。 所以上面代码的第一行的含义是打印Please enter an integer:,读入一行输入,转成对应的整数,将整数存入变量x。 接下来的if和行末的:之间有一个表达式x < 0,这里涉及了布尔类型和比较运算符,我来介绍一下。 布尔类型(bool) 布尔类型是一种只有两个值,代表真假的类型。它有两个字面量:True和False,分别代表真和假,你可以在交互式命令行输入它们试试看: >>> True True >>> False False 比较运算符(comparison operator) 一共有6种比较运算符,它们都是二元中缀的运算符(接受2个操作数,位于2个操作数中间)。所有的比较运算符都具有相同的优先级且比算术运算符低,返回布尔类型。这6个比较运算符分别是: <:小于; >:大于; <=:小于等于; >=: 大于等于; ==:等于; !=:不等于。 这里关于键入这些符号,我有3点提醒大家。首先别在键盘或者输入法中找$\leq$和$\geq$,小于等于和大于等于就是两个字符拼接出来。然后是等于,一定记住是两个等号,一个等号的是赋值。最后也别找$\neq$,记住是!=就行。 接下来我来讲解一些比较运算符的操作数类型。显而易见,数值类型(整数和浮点数)都是可以参与比较的,来看看一些运算符计算的示例吧: >>> 2.0 <= 4 True >>> 3 > 5 False >>> 1.0 == 1 # 混合数学类型比较 True >>> 0.1 + 0.1 + 0.1 == 0.3 # 浮点数的精度丢失问题 False 最后两个例子我解释下。首先1.0 == 1并不会因为操作数的类型不同而是False,实际上,右边的操作数1会转换为浮点数,和左边比较得到相等的结果。最后一个例子中,3个0.1的和不等于0.3可能就会令很多人困惑,这是因为不同于整数,浮点数的精度是有限的,0.1实际上约是0.10000000000000001,而0.3实际上约是0.29999999999999999,所以3倍0.1就不等于0.3。那么我们该如何判断两个浮点数是不是相等呢?实际上我们会对两个数作差,取绝对值后如果小于某个很小的数(这个很小的数习惯叫epsilon),我们就认为相等: >>> epsilon = 1e-8 >>> x = 0.1 + 0.1 + 0.1 >>> y = 0.3 >>> abs(x - y) < epsilon True 这里我们用到了绝对值函数abs(),它接受一个数,返回它的绝对值。 除了数值可以作为比较运算符的操作数,字符串和列表也能参与比较运算,他们的比较涉及一个概念“字典序”。对于两串序列而言,我们先比较第1个元素看谁小,如果第1个元素相同就再比较第2个元素,如果最后前缀的元素都相同,但长度不一样,我们就认为短的小。对于字符串,比较的是每个字符的编码值,你只需要记住以下规则就行:0到9是依次递增不夹杂别的字符,A到Z也是依次递增不夹杂别的字符,a到z也同样是。我们来看例子: >>> 'a' < 'z' True >>> 'a' < 'a0' True >>> [1, 1] < [1, 0] False >>> [1, 1] < [1, 1, 0] True 然后我们讲解一下串连比较运算符。一般而言,我们说运算符会具有结合性,但Python的比较运算符不是这样的。我们先看例子: >>> -1 < 3 < 2 # -1 < 3 且 3 < 2 False >>> (-1 < 3) < 2 # True < 2 True >>> -1 < (3 < 2) # -1 < False True >>> 1 < 3 > 2 # 1 < 3 且 3 > 2 True Python的串连比较运算符既不是左结合也不是右结合的,而是很符合数学直觉的一种计算方式。-1 < 3 < 2等价于-1 < 3且3 < 2。而当你加上括号之后,这个魔法就消失了,如(-1 < 3) < 2会先计算(-1 < 3)得到True,True转换为数值类型成1,而后1 < 2得到True;或者-1 < (3 < 2)会先计算(3 < 2)得到False,False转换为数值类型成0,而后-1 < 0得到True。串联的比较运算符不必是同一种运算符,最后一个例子混用了<和>,但注意1和2之间没有任何比较关系。这里的讨论其实涉及了布尔类型到数值类型的类型转换,这种类型转换使得布尔类型也可以参与数值计算,我们会在1.1.5介绍其他类型到布尔类型的转换,这使得其他类型也能作为if等控制语句的判断条件。 if语句格式 再看例子: if x < 0: print('Negative changed to zero') elif x == 0: print('Zero') elif x == 1: print('Single') else: print('More') 我们先说语法,if后面和elif后面都是跟一个布尔类型的表达式,这是条件。else后面不需要跟条件。而后,后面跟冒号:和换行,换行后必须跟至少一个子语句,子语句必须使用缩进(也就是用空格或者Tab使代码往右侧便宜),且同一块的自语句缩进相同。一般我们使用4空格缩进。elif部分只能出现在中间,可以有零个或多个,else部分只能出现在最后,可以有零个或一个。 然后我们说说语义,程序首先检查if语句的条件,如果if后面的条件成立就执行if子句的内容,然后退出if语句(不参与之后的条件判断);否则再比较第1个elif(如果有)后面的条件,成立了就执行第1个elif子句的内容,然后退出if语句,然后再比较第2个elif(如果有)的条件,以此类推。如果所有的条件不满足,就会执行else子句。 我们来看一些例子。 示例1:若x是奇数,将x加1,省略elif和else部分。 x = 3 if x % 2 == 1: x = x + 1 print(x) # 4 示例2:猜猜下面的x是多少?记住一旦进入了某个子句内部执行完毕,就不会再去参与别的条件判断了。 x = 4 if x > 0: x = -1 elif x < 0: x = 1 print(x) # -1 示例3:if语句可以嵌套,上面的示例其实等价于下面的嵌套if语句。实际上elif就是else if的简称,但由于这种嵌套的写法会使代码缩进得越来越深,所以最终出现了elif。 x = 4 if x > 0: x = -1 else: if x < 0: x = 1 print(x) # -1 示例4:如果子句只有一条语句,也可以不换行,像下面那样,但可以看出这种写法的可读性不高,所以不建议这么写: x = 3 if x % 2 == 1: print('x is odd number') else: print('x is even number') 真假值 上一个例子中x % 2 == 1其实可以简写成x % 2,像下面那样: x = 3 if x % 2: print('x is odd number') else: print('x is even number') 这就是因为其他类型可以转换为布尔类型,从而作为if语句的判断条件。以下值会被认为是假的,转换成False: 0:整型0; 0.0:浮点型0; '':空字符串; []:空列表。 除此之外,我们目前学到的其他类型的值都是真的,会转换为True,举个例子: if []: print('[] is true') else: print('[] is false') if '': print('\'\' is true') else: print('\'\' is false') while语句 我们先写个例子打印1到10: i = 1 while i <= 10: print(i) i = i + 1 while语句中间缩进的部分我们称为循环体,循环体不能省略。while只要条件成立就不会不断执行循环体。如果循环无法终止,我们就称之为死循环。 for语句 我们经常需要遍历列表中所有的元素进行操作,如果用while,它会是这样的: words = ['cat', 'window', 'defenestrate'] # 打印每个单词及其长度 i = 0 end = len(words) while i < end: w = words[i] print(w, len(w)) i = i + 1 这种时候用for循环就方便很多,而且可读性更高: for w in words: print(w, len(w)) for语句紧跟在for后面的是标识符(以后我们可以看到是别的东西),然后是in,再然后是一个可迭代对象。它会将可迭代对象的每个值按顺序赋予for和in之间标识符,然后在执行循环体。与while相同,循环体不能省略。我们接触到的可迭代对象只有2个,字符串和列表,之后会介绍第3个,range()。 一般情况下,一边循环一边修改可迭代对象是个错误,这是你可以对可迭代对象做个拷贝(用切片语法),再迭代。就像下面的代码那样,如果你去掉拷贝,将words[:]改为words会造成死循环: words = ['cat', 'window', 'defenestrate'] # 将长度超过6的单词拷到尾部 for w in words[:]: if len(w) > 6: words.append(w) print(words) # ['cat', 'window', 'defenestrate', 'defenestrate'] range()函数 有时你只是想要迭代一个计数器,这时候range()函数就很有用,它会返回一个可迭代对象。 range()可以接受1到3个参数,我们来解释一下这3种情况: range(stop):生成一个从0到stop(包含0不包含stop)的递增序列; range(start, stop):生成一个从start到stop(包含start不包含stop)的递增序列; range(start, stop, step):生成一个从start开始的等差序列,其步进(公差)是step,如果step是正数(负数),直到数列大于(小于)stop就终止。 我们看示例: range表达式 可迭代对象包含的数值 range(5) 0, 1, 2, 3, 4 range(5, 10) 5, 6, 7, 8, 9 range(0, 10, 3) 0, 3, 6, 9 range(-10, -100, -30) -10, -40, -70 让我们看一下示例代码,先是打印1到10: for i in range(1, 11): print(i) 再是遍历单词列表,输出它是第几个单词和单词本身: a = ['Mary', 'had', 'a', 'little', 'lamb'] for i in range(len(a)): print(i, a[i]) 当你只是打印range,会出现奇怪的结果: >>> range(10) range(0, 10) 你并没有看到一串数。因为实际上只有你去把它作为for语句in后面的可迭代对象时,才会生成这一系列的数,这是为了性能考虑的。有时我们也需要将可迭代对象转换为列表,这时可用list()函数。 >>> list(range(5)) [0, 1, 2, 3, 4] break、continue和循环else子句 先看示例: for n in range(2, 10): for x in range(2, n): if n % x == 0: print(n, 'is not a prime number') break # 跳出最内层(第2个)for循环 else: # 当for正常结束而非break时执行 print(n, 'is a prime number') 上面的程序用最原始的试除法来判断数是不是素数。break语句是用于终止最近的循环的。和if一样,for和while语句也可以有else子句,它的含义书如果for和while不是通过break终止的,它就会被执行,否则不执行。此外还有continue语句,它用于跳过剩余的循环体,直接进入下一次循环的,如下: for num in range(2, 10): if num % 2 == 0: print("Found an even number", num) continue # 跳过后面的语句 print("Found a number", num) pass语句 我们可以注意到if、elif、else、while和for的子句部分都必须需要语句,如果你什么语句都不想指定的时候,就可以用pass语句,它什么事都不做。如下面的死循环的例子: while True: pass 关键字(keyword) 学了这么多语句,我们可以注意到一些单词在程序的语法和语义上扮演着特殊的角色,如if、elif、else、while、for、break、continue、pass、True、False和我们即将学到def及None,这些单词我们称之为关键字。而另一些词,如len、int、print、input、abs,它们只是一些实体的名字,我们称之为标识符。你可以通过赋值覆盖掉这些标识符原来的含义。当然,你可以给新的实体起名字,如变量名和下面会接触到的函数名,这些名字不能是关键字。我在这里列出合法标识符的命名要求(虽然可以,不建议包含中文): 不是关键字; 由数字、大小写英文字母和下线符_组成; 不能以数字开头。 函数 函数(function) 可以说是传统命令式编程的最后一个拼图。我们经常会需要将一些功能模块化。比如上面的判断一个数是否是素数,就可以写成一个函数,其输入是一个整数,输出是一个布尔值。除了模块化之外,函数直接或者间接地调用自己,也很多时候能简化问题,我们称之为递归,这会在2.1.4中介绍。模块化和递归是我认为函数最主要的作用。 定义函数 你可以像下面那样定义一个函数。 def add(a, b): """Add two number.""" return a + b 定义函数的时候,函数并不会被执行,只有在调用时,才会真正执行并根据参数返回一个值。调用(call) 这个函数就和调用别的函数那样: >>> add(1, 2) 3 在def后面跟上一个标识符作为函数名称,而后跟上括号括起,逗号分割的标识符列表,我们称这些标识符为,形式参数,如上面的a和b。之后的语句从下一行开始,必须缩进,称为函数体,如果函数体不需要任何操作,可以用pass语句。 最后一行的语句是return语句,return语句的后面可以跟个表达式,这个表达式的值就是函数的返回值,作为函数调用的结果。return语句不必存在在函数的最后一行。return语句也可以省略后面的表达式,这样等价于return None;函数末尾也可以没有return语句,也等价于在函数末尾写了return None。None这个值表示什么都没有。如果在交互界面下,一个表达式的值是None,那么什么东西都不会打印出来。看下面例子: def get(pred): if pred: return 1 # 没有return等价于“return None” >>> get() # 缺参数会报错 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: get() missing 1 required positional argument: 'pred' >>> get(True) 1 >>> get(False) # 什么都没输出,因为返回了None >>> None # 同样什么都没输出 >>> print(None) None 函数体的第一个语句可以是字符串文本,就像这里的"""Add two number.""",我们称之为文档字符串(docstring),通常是用3个双引号括住的跨行字符串。它给出函数的一些帮助信息,但不同于注释,它能被Python识别出来,并可以通过help()显示出来。help()携带一个参数,通常是要查询信息的函数等等,返回它的帮助信息,我们会在之后再讲文档字符串: >>> help(add) Help on function add in module __main__: add(a, b) Add two number. 接下来我们来细致讲解函数定义的细节、准则和技巧。 形式参数(parameter)、实际参数(argument)与传参方式 正如上文说过的,我们把函数定义中,例如def add(a, b):中的a和b称为形式参数,简称形参。而我们把函数调用中,例如add(1, 2)中的1和2称为实际参数,简称实参。但实际生活中我们可能会混用这些称呼,统称为参数。 在Python中,函数调用会发生实参向形参的赋值,你可以认为这是一种浅拷贝,因而在函数体内再对形参的赋值不会作用到函数外部,如下: def reassign(a, b): a = 2 b.append(3) >>> c = 1 >>> d = [] >>> reassign(c, d) >>> c # c没有发生更改,因为形参a只是被绑定到了新的值2上 1 >>> d [3] 像上面这种发生赋值的函数调用方式,我们称之为按值调用(call by value)。Python的所有函数调用均是按值调用。 当然这个世界上还有很多的调用方式,如按引用调用,这种方式下,对形参的修改会直接反应给外部。不过这不在我们的研究范围内。 作用域(scope) 每个名字都有它有效的范围,即能获取这个名字的范围,我们称之为作用域。如果没有函数,所有的名字的作用域都是很显而易见的,从它定义开始一直到最后。有了函数之后,事情稍微复杂了一些。 我们一般把定义在函数内的变量称为局部变量(local variable),而定义在函数外的变量称为全局变量(global variable),当然啦,如果你没有忘记,还有一类变量叫做形式参数。目前我们就学了这3类变量。看下面这个例子: >>> c = 2 >>> def f(a): ... b = 1 ... print(c) ... >>> f(1) 2 >>> a Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'a' is not defined >>> b Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'b' is not defined 这里a是形式参数,b是局部变量,c是全局变量。 全局变量的作用域从它定义的位置一直到最后,而局部变量的作用域只从它定义的位置到函数体的结尾,最后,形式参数从性质上来讲更像局部变量,它的作用域只在函数体内部。 按照上个例子,c是全局变量,所以我们甚至能在函数f中引用它,但是函数f中的a和b在外面是访问不到的。 上面我们只是访问了c,如果我们试图修改c,那情形就更加复杂了,我们来看下面的代码: >>> c = 2 >>> def g(): ... c = 1 ... print('inside:', c) ... >>> g() inside: 1 >>> print('outside:', c) outside: 2 很多人的第一反应是,我们明明对c修改了,为什么没有起效呢?这是因为在函数内部的c = 1只是创建了个局部变量,它与全局变量同名。要理解这一切的行为,我们需要讲两个规则: 在默认情况下,函数内部的赋值只会创建局部变量; 当需要使用名字(不包含赋值),且有多个同名变量存在的时候,位于最内层作用域的变量被使用。 对于第2点补充一下,你可以理解为当使用一个名字时,我们会从内到外依次去寻找这个变量。所以当我们print('inside:', c)的时候,它在局部作用域就找到了c,所以打印了1。实际上内部的c屏蔽(shadow) 了外部的c。 那有没有什么方法能阻止这个行为呢?答案是有的。只要在赋值发生之前用global语句声明变量在全局作用域,就可以使赋值创建或修改全局变量。如下: >>> c = 2 >>> def h(): ... global c, d ... c = 3 ... d = 4 ... >>> h() >>> c 3 >>> d 4 其实一开始有些人会觉得作用域的设计带来了很多不方便。但其实这个设计是很合理的。因为函数是对一些操作的封装,如果你随随便便修改几个全局变量,就会影响到其他函数的运行,那是有很大安全隐患的。毕竟这么多Python库的作者们才不会互相协调好,哪个名字的全局变量归谁使用。 函数设计准则 可以看出函数像个运算符(或者这句话更准确来说是运算符像个函数),它有参数和返回值。函数除了以这种方式(返回值)或修改参数所指的对象产生作用外,还可以产生副作用,即修改全局变量或者输入输出。但是,工程上来讲,我们希望函数拥有尽可能小的副作用。实际上全局变量很多时候是万恶的。一开始编程可能觉得没什么问题,但当你程序变得庞大的时候,减少全局变量、函数无副作用(尤其是不要访问修改全局变量)可以使程序逻辑更清晰。 递归(recursion) 在函数中,你不仅能够调用其他函数,你还能调用函数自己,这就被称为递归。我们以求阶乘为例,给出递归版本的和非递归版本的。递归版如下: def factorial(n): if n <= 0: return 1 return n * factorial(n - 1) 像这样使用: >>> factorial(5) 120 可以看出写递归版基本是无脑的:先考虑边界条件,如果n小于等于0,那阶乘就是1(0的阶乘是1哦);其他情况$n! =n\times(n-1)!$。 突然你看到了知乎提问如何编程求10000!(一万的阶乘)? - 知乎,你跃跃欲试,却发现结果是: >>> factorial(10000) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in factorial File "<stdin>", line 4, in factorial File "<stdin>", line 4, in factorial [Previous line repeated 995 more times] File "<stdin>", line 2, in factorial RecursionError: maximum recursion depth exceeded in comparison 没错,递归是有深度限制的,递归太多次就会出错,那我们老老实实写循环吧: def factorial2(n): prod = 1 for i in range(1, n + 1): prod = prod * i return prod 这个写法其实也没比递归复杂太多。我们有个累乘变量prod,然后生成范围为[1, n + 1)的整数,挨个乘上去。现在你可以去知乎,把下面的答案贴上去,说这么简单的问题,5行代码就能搞定。 >>> factorial2(10000) 2846...0000 于是乎,如果你真的打开了知乎,你发现高票的Python只有一行(红红火火恍恍惚惚)。其实它少了一行from functools import reduce。如果你想知道这是怎么回事。你可以看完下面的高阶函数,再看看2.6 Lambda表达式,最后查查reduce()的帮助。 当然我才不会告诉你,阶乘函数是自带的: >>> import math >>> math.factorial(10000) 高阶函数(hight-order function) Python中所谓的函数名其实是绑定了函数的变量名,你可以让函数绑定到新的变量名上: >>> f = print # 现在f就是print的别名 >>> f('hello, world!') hello, world! 那么函数能不能定义在函数里面,答案是可以的,像下面的函数会带来3层嵌套的作用域(全局作用域、a的局部作用域和b的局部作用域): def a(): def b(): pass pass 接下来的问题就很有趣啦,函数可不可以作为实际参数,或者作为返回值,答案也是可以的。让我们先试着写看一个叫map的高阶函数,它是Python自带的,它对一个列表中的所有元素进行某一操作,再将操作的结果收集起来。我们实现一个类似的map2函数,代码如下: def map2(operation, numbers): result = [] for number in numbers: result.append(operation(number)) return result 它是这么用的: >>> def double(a): return 2 * a ... >>> map2(double, [1, 2, 3]) [2, 4, 6] >>> # 当然Python自带map >>> list(map(double, [1, 2, 3])) 和range()一样,Python自带的map()是需要list()才能转换为列表的。再来看map2()函数,它的第一个参数operation就是一个函数,这个函数接受一个数,返回这个数2倍的值。然后在map2的函数体中,我们直接通过operation(number)调用这个函数。 上面讲的是函数作为参数的情况,接下来我们看看函数作为返回值的情况。让我们来看下面这个很烧脑的add2()函数: def add2(a): def f(b): return a + b return f 我们来看看这个怎么用: >>> add2(2)(3) 5 >>> add_to_3 = add2(3) >>> add_to_5 = add2(5) >>> add_to_3(5) 8 >>> add_to_5(4) 9 这里add2调用1次实际上就是返回了内部的f,其return语句中的加法左操作数被填上对应的值,而再调用f1次,就会填补上return语句中的加法右操作数,这样就可以得到加法的结果。这里非常有趣的是参数a,每次调用add2都会得到一个全新的a,而后a的值就被函数f捕获。所以add_to_3捕获的a是3,而add_to_5捕获的a是5,它们互不相关。这种捕获了外部函数局部变量的内部函数,我们称之为闭包。 参数默认值(default argument value) 可以给函数提供的参数提供默认值,这样当这些参数没有提供的时候,也就是给的实际参数个数比定义时的形式参数少的话,没有给出的参数就由默认参数提供。这些参数有时也会被称为可选参数(optional argument),类似地,那些没有默认值的参数就称为必选参数(required argument)。默认参数通过在参数列表中,给参数后面加上=value来设置,如下面的函数: def power(base, exponent=2): return base ** exponent 习惯上我们不在形参和它的默认参数之间添加空格。你可以省略exponent的实际参数,这样exponent值为2,你也可以为它提供一个你想要的值,如下: >>> power(2) 4 >>> power(2, 3) 8 那么我们能否给base制定默认值,而不给exponent默认值呢?答案是不行的: >>> def power(base=2, exponent): ... return base ** exponent ... File "<stdin>", line 1 SyntaxError: non-default argument follows default argument 也就是没有默认值的形参必须出现在有默认值的形参之前。这其实是很合理的,因为如果调用power(3),就不知道是base为3,还是exponent为3。 默认值实在函数定义处计算的,所以下面的代码会打印出5: i = 5 def f(arg=i): print(arg) i = 6 f() 最后一个关于默认值的考点,也是经常出现在各种面试题里的:默认值只会被执行一次。这条规则对于不可变的默认值是没有影响的,但如果默认值是可变对象(如列表),那就会有影响。比如,下面的函数会存储所有调用传递给它的参数: def f(a, L=[]): L.append(a) return L print(f(1)) print(f(2)) print(f(3)) 它会打印出: [1] [1, 2] [1, 2, 3] 如果你不想要这种行为(一般也非常不建议默认值是可变的),你可以这样写: def f(a, L=None): if L is None: L = [] L.append(a) return L 关键字参数(keyword argument) 有些时候,记住参数的位置可能会很困难,但记住参数的名字会容易很多,而且使用名字来指定参数可读性更高,因而就有了关键字参数。在函数调用的地方,可以使用parameter_name=value方式指定某一名字参数的值。此外还有2条规则: 所有的关键字参数必须出现在普通参数(positional argument)之后,关键字参数之间的顺序不重要; 不能对同一参数指定两次值。 考虑下面的函数: def print_values(x, y=2, z=3): print('x:', x, 'y:', y, 'z:', z) 你可以通过以下的方式调用这个函数: >>> print_values(1) # 普通的调用方式 x: 1 y: 2 z: 3 >>> print_values(x=2) # 通过关键字给非默认参数指定值 x: 2 y: 2 z: 3 >>> print_values(x=3, y=4) # 通过关键字也能给默认参数指定值 x: 3 y: 4 z: 3 >>> print_values(y=5, x=4) # 关键字参数的顺序不重要 x: 4 y: 5 z: 3 >>> print_values(5, 6, 7) # 给所有参数都指定值 x: 5 y: 6 z: 7 >>> print_values(6, y=7) # 可以同时使用普通参数和关键字参数 x: 6 y: 7 z: 3 但以下的调用都是无效的: >>> print_values() # 缺少必选参数 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: print_values() missing 1 required positional argument: 'x' >>> print_values(y=2, 1) # 普通参数在非关键字参数之后 File "<stdin>", line 1 SyntaxError: positional argument follows keyword argument >>> print_values(1, x=1) # 多次指定同一参数 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: print_values() got multiple values for argument 'x' >>> print_values(1, i=2) # 未知的关键字参数 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: print_values() got an unexpected keyword argument 'i' 任意多的参数 有时候,我们希望函数能带任意多的普通参数和/或关键字参数。这时候就要用到一类特殊的语法。在介绍语法之前,我们先简略地介绍两个新的类型元组(tuple)和字典(dict)。 元组(tuple) 元组和列表几乎完全相同,也是一种序列容器。元组和列表唯一的差别是元组是不可变的,这意味着你不能对元组的元素和切片赋值,也没有append等操作。 创建元组和列表类似,只是是使用圆括号括起。如果元组只有一个元素,必须在元素的后面添加个逗号,这样做了是为了区分改变优先级的圆括号和元组的圆括号。 >>> () # 空元组 () >>> (1,) # 单元素元组,注意末尾的逗号 (1,) >>> (1, 2, 3) # 更多元素的元组 (1, 2, 3) 在没有二义性的情况下,括号是可以省略的,就像下面那样: >>> 1, (1,) >>> 1, 2, 3 (1, 2, 3) >>> 1, 2 + 2, 3 # 逗号的优先级几乎是最低的 (1, 4, 3) 和列表一样,你可以通过索引和切片获取元组某个或某些元素的值,但由于元组是不可变的,你不能对索引或切片赋值: >>> a = 0, 1, 2, 3 >>> a[1] 1 >>> a[1:] (1, 2, 3) >>> a[0] = 4 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment 元组和列表一样是可迭代对象。先前我们说到list()函数可以将可迭代对象转换为列表,类似地,tuple()函数可以将可迭代对象转换为元组: >>> tuple([1, 2, 3]) (1, 2, 3) >>> tuple(range(1, 4)) (1, 2, 3) 可能你的心中会有疑问,为什么有了列表还要有元组,我想这有两方面考虑: 性能:维护一个定长的序列比不定长的序列更简单,因为定长序列不需要考虑序列的扩展(如append())操作,因而在内存占耗和性能上,定长序列都更优; 不变性:有些时候我们需要不变的值,比如下面提到的字典的键,我们必须确保字典的键不能发生变化。 我相信对于Python,第2个方面的考虑吧是更加重要的。 字典(dict) 先前我们学到列表和元组都是序列容器,接下来我们介绍一种关联容器,字典。字典是一种可变的数据结构,它存储的是一种映射关系,可以根据某一个键(key)获取或修改该键对应的值(value)。字典字面量的格式形如{ key1: value1, key2: value2, ... },其中键必须是不可变类型,比如数字类型、字符串和元素都是不可变类型的元组。下面是一些合法的字典字面量: >>> {} # 空字典 {} >>> {' ': 1} # 字符串到数字的映射 {' ': 1} >>> {'a': 1, 2: 'b'} # 键和值可以是不同的类型 {'a': 1, 2: 'b'} >>> {(1, 2): 3} # 元组也可以作为键 {(1, 2): 3} >>> {[1, 2]: 3} # 列表不能作为键 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'list' 你可以通过字典的索引,获取某个键对应的值。也可以对索引赋值,如果这个键不存在则会创建新的键值对,否则旧的值会被覆盖掉。让我们来看例子: >>> order = {'a': 3, 'b': 2, 'c': 3} >>> order['b'] # 获取字典中键对应的值 2 >>> order['a'] = 1 # 用新值覆盖字典中键对应的旧值 >>> order {'a': 1, 'b': 2, 'c': 3} >>> order['d'] = 4 # 创建新的键值对 >>> order {'a': 1, 'b': 2, 'c': 3, 'd': 4} 我们会在以后学到关于元组和列表的更多操作。接下来让我们学习任意多的参数。 任意多的参数(续) 我们会发现,所有提供的实参无外乎两种: 普通参数,又称位置参数(positional argument),非关键字参数(non-keyword argument); 关键字参数(keyword argument)。 而且注意,所有的位置参数出现在所有的关键字参数之前。 有时候,我们希望函数能接受任意多的位置参数和关键字参数。我们可以在函数形参列表中添加下面两个参数: *args用于将多余的位置参数捕获成一个名叫args的元组; **kwargs用于将多余的关键字参数捕获成一个名叫kwargs的字典。 这里args和kwargs是惯用的命名。此外*args必须出现在**kwargs之前,看例子: def func(a, *args, **kwargs): print('a:', a) print('args:', args) print('kwargs:', kwargs) 然后我们可以像下面那样调用这个函数: >>> func(1) # 只提供了一个位置参数,args和kwargs为空 a: 1 args: () kwargs: {} >>> func(2, 3, 4) # 提供了多余的位置参数存放在了args中 a: 2 args: (3, 4) kwargs: {} >>> func(a=1) # 只提供了一个关键字参数,args和kwargs为空 a: 1 args: () kwargs: {} >>> func(2, 3, b=4, c=5) # 提供了多余的位置参数和关键字参数,分别存放在args和kwargs中 a: 2 args: (3,) kwargs: {'b': 4, 'c': 5} **kwargs后面是不允许更其他形参的,但*args和**kwargs之间可以跟别的参数,就像下面这样: def func(a, *args, b, **kwargs): print('a:', a) print('args:', args) print('b:', b) print('kwargs:', kwargs) 这时参数b只能通过关键字参数的方式提供给参数,但其实这个语法用得并不多: >>> func(1, 2, b=3) a: 1 args: (2,) b: 3 kwargs: {} 最后总结一下,函数的形参列表由以下几个部分组成: 不带默认值的参数; 带默认值的参数; *args; 必须采用关键字参数赋值的参数; **kwargs。 解包参数列表 上面的函数接受任意参数实际上是将函数的实参打包(pack) 成元组和字典,但有些时候我们需要相反的操作,即将可迭代对象和字典解包(unpack) 成函数的实参。这两个操作结合就能实现参数的转发。 首先可迭代对象(如列表、元组)可以通过解包成一系列的位置参数,方法是在可迭代对象之前加上*。看示例: >>> list(range(3, 6)) # 普通的调用方式 [3, 4, 5] >>> args = [3, 6] >>> list(range(*args)) # 函数解包的方式 [3, 4, 5] 当然你可以串连多个可迭代对象: >>> def func(*args): ... print(args) ... >>> func(1, *[2, 3], 4, *(5, 6, 7)) (1, 2, 3, 4, 5, 6, 7) 然后字典也可以解包成关键字参数,方法是在字典前加上**。看示例: >>> def func(**kwargs): ... print(kwargs) ... >>> func(a=1, **{'b': 2, 'c': 3}, d=4, **{'e': 5}) {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5} 当然这里的解包仍需要遵循先前的约定,如不能对一个参数赋两次值,所有的位置参数必须在关键字参数之前。 最后,我们来看看函数参数的转发: def my_pow(*args, **kwargs): return pow(*args, **kwargs) 上面代码的第一行是函数任意多参数的语法,第二行是解包参数的语法。这两者配合使得my_pow函数的用法和pow函数的用法一样了,而pow函数可以接受两个参数base和exp,其结果为base ** exp。让我们来看看如何使用my_pow: >>> my_pow(2, 3) 8 >>> my_pow(3, exp=4) 81 Lambda表达式 使用lambda关键字可以创建一个简短的函数,形式是lambda param1, param2, ... : expression,如求把两个参数的和作为返回值,可以用lambda a, b: a + b这个lambda表达式表示。当然lambda表达式也可以没有参数,如lambda: 1。lambda函数与普通函数的唯一差别是,lambda函数的函数体只能是一个表达式,不能包含语句。 来看看下面的示例: >>> def make_incrementor(n): ... return lambda x: x + n ... >>> f = make_incrementor(42) >>> f(0) 42 >>> f(1) 43 这里我们使用了高阶函数,将lambda函数作为返回值。这里lambda函数还捕获了一个局部变量n。 这种高阶函数并非只是烧脑用的,它有实际的用处。如列表提供了sort()方法用于排序,它的key参数可以提供给一个函数。这个函数接受列表的元素,返回真正参与比较运算的值。 >>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')] >>> pairs.sort() >>> pairs [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')] >>> pairs.sort(key=lambda pair: pair[1]) >>> pairs [(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')] 上面的代码中,第1次sort()会采用元素本身作为比较的参数,进而对这4个元组采用字典序比较。而第2次sort(),我们给了个lambda函数,选用元组的第2个元素,也就是one、two、three和four,进行比较,这种比较是字符串上的字典序。 文档字符串 关于文档字符串,有一些约定: 文档字符串的第一行是简要概述。不应显式声明函数的名称或类型。这一行应以大写字母开头,以句点结尾; 如果文档字符串要有更多行,则第二行应为空白,后面更段略,从而与开头一行分开。 你可以通过func.__doc__来获取函数的文档字符串: def my_function(): """Do nothing, but document it. No, really, it doesn't do anything. """ pass >>> print(my_function.__doc__) Do nothing, but document it. No, really, it doesn't do anything. >>> help(my_function) Help on function my_function in module __main__: my_function() Do nothing, but document it. No, really, it doesn't do anything. 函数标注 Python本身是动态类型语言,也就是运行的时候才会确定类型。但是写代码的时候,有时能知道类型会让我们更好的知道错误,如range()函数的3个参数我们知道都是int类型,如果我们写出了range('a', 'z'),那就会是一个潜在的bug。而且不仅如此,很多集成开发环境(IDE)的自动补全都会依赖于类型的推断,如果IDE能够得知某个变量和函数的具体类型,它的自动补全也能做得更好,方便我们开发。 通过在函数的参数和默认值(如果有)之间加入: type,可以将某个参数标记为type类型,通过在def语句结束的冒号:之前加入-> type,可以将函数的返回值标记为type类型: def char_at(text: str, index: int) -> str: return text[index] 这里这个函数的text和返回值就是字符串类型,而index是整数类型。注意类型注解只是像注释一样提供了信息,它并不会检查类型是否符合。 通过函数的__annotations__属性可以获得所有的注解: >>> char_at.__annotations__ {'text': <class 'str'>, 'index': <class 'int'>, 'return': <class 'str'>} 下表给出常见的类型对应的注解,当然下表你不必记住,需要的时候搜索即可: 类型名 类型注解 None的类型(如无返回值) None 布尔类型 bool 整数类型 int 浮点数类型 float 字符串类型 str 元素都为type类型的列表类型 List[type] 元素类型依次为type1、…、typeN的元组类型 Tuple[type1, ..., typeN] 键为key类型,值为value类型的字典类型 Dict[key, value] 参数类为type1、…、typeN,返回类型为retType的函数类型 Callable[[type1, ..., typeN], retType] 可能是type1或…或typeN类型 Union[type1, ..., typeN] 对于最后的List、Tuple、Dict、Callable和Union类型需要在最开始加上from typing import *,以后我们会将如何导入包。 最后补一句,类型注解其实在平时开发中使用的并不多,一般只有包的作者才会考虑采用类型注解。所以读者如果对这部分内容很困惑,也不必纠结太多,因为这对日常编程的影响不大。 代码风格 PEP 8是大家都遵循的一种代码风格指南。以下是最重要的内容: 使用4空格缩进,不要使用制表符; 一行不超过79个字符,超过了可以换行; 可以用空行分割函数内的代码块; 使用文档字符串; 在除运算符前后使用空格,括号内部不加,如a = f(1, 2) + g(3, 4); 使用lowercase_with_underscores的命名风格命名变量和函数; 使用UTF-8编码,代码标识符不要采用中文。 你可以使用代码风格检查工具或者IDE(集成开发环境)来帮助你达成好的代码风格。下面我们会介绍PyCharm,它是我认为功能最强大的Python IDE之一。 PyCharm的使用 什么时候会用到IDE 先前我们介绍过JupyterLab。它主要是用于交互式的编程,也就是你输入若干行语句或表达式,它就立刻显示运行的结果。对于简短的编程(比如测试函数的用法、大多数数据分析、调用一些机器学习库等,通常来讲它们的代码量并不会特别大),交互式编程是很有帮助的。但有时候: 我们需要开发一些大项目,这些大项目的代码多到如果放在一个文件里会显得很没有组织,这也算一种模块化,关于涉及多个源文件的编程我们会在以后介绍; 我们觉得JupyterLab的代码补全、代码调试(debug)、代码静态分析(inspect)、风格检查(lint)、代码性能分析(benchmark)等等功能不够完善,我们需要一个强大的IDE; 我们需要将我们的代码提供给别人运行,而那些人可能没有安装JupyterLab。 当遇到上面这些情况时,将代码保存成一个个.py文件,用IDE编辑就非常有优势了。 接下来我们介绍如何安装PyCharm。注意我下面的文章是基于PyCharm 2019.3版本的,之后的版本可能步骤会有变化。 安装PyCharm 你可以在Download PyCharm: Python IDE for Professional Developers by JetBrains页面下载PyCharm,它有两个版本:付费的专业(Professional)版和免费的社区(Community)版。如果你是学生,可以在JetBrains Products for Learning获得免费的专业版账号,它的验证邮件可能发得比较慢,需要耐心等待。当然你也可以选择购买,JetBrains(制作PyCharm等IDE的公司)的产品的价格并不便宜。当然社区版的功能也足够大多数人使用了。 下载完毕之后运行会出现安装向导。在选项界面,我建议勾上“Add “Open Folder as Project””,对于需要的人也可以勾上“Create Desktop Shortcut”。然后会出现选择主题界面,有明亮和黑暗两款可以选,这个就看个人喜好了。最后是插件界面,建议不要勾选任何插件,毕竟在这之后是可以再下载插件的,尤其是Vim插件,给很多初学者带来了极大的困扰。 创建工程 IDE通常将一个Python项目组织成工程。让我们来创建一个工程。 运行PyCharm后,点击“Create New Project”就可以创建一个工程。然后你可以选择一个合适的“Location”,.py文件会被直接存放到那个目录中,如果那个目录不存在会创建。如果你不知道怎么取个名字,就把Untitled改成hello吧。 先别急着创建项目,点开“Project Intepreter”,你有以下两个主要选项: New environment using Virtualenv:这个选项是创建一个虚拟的环境,你安装的所有外部的包都会存放在这个虚拟环境中,这在绝大多数情况下是个很好的选择,但也意味着如果你的多个项目共同用一个很大的外部包,那么它们都会独自安装一份,占用很大的磁盘空间(由于pip会缓存安装包,所以并不会多次下载同一版本的同一个包,网络流量不需要担忧); Existing interpreter下点击省略号 -> System Intepreter -> 选择你安装的Python位置(默认是在AppData目录下):这个是使用全局的Python环境,所有的包都会安装到全局的同一个目录下,如果不同的项目依赖不同版本的同一个包,那就会出现问题,这种方案虽然节省存储空间,但可能会存在各种冲突,不过也算一个不错的选择。 如果你很纠结,就使用Virtualenv吧(第一个选项),然后下一步,就可以创建工程。 编辑和运行 进入主界面后,你应该可以看到一个叫Project的窗口位于左侧。如果你没看到。可以在侧栏找到1:Project这个竖着的按钮点开,或者按Alt+1又或者点击View->Tool Windows->Project。 打开完Project窗口后,它显示了项目的目录结构,应该长这样: {.text-center} 然后右键项目的根目录(这里是写着hello高亮出来的一行)-> New -> Python File,创建一个文件(不需要输入后缀名),比如下面图中所示的main.py: {.text-center} 然后我们添加下面的示例代码: def convert_string_to_uppercase(text2): return text2.upper() if __name__ == '__main__': text = input('Please input an English word:') result = convert_string_to_uppercase(text) print('It\'s uppercase is', result) 这个代码的功能就是输入一行字,输出它的大写。虽然我们把函数参数命名为text2以区别text,但这不是必要的,只是方便调试时大家看清楚是哪个变量。你可能很好奇,if __name__ == '__main__':是做啥用的,__name__是内置的全局变量,而当它的值为__main__表示这个文件是正在被执行的文件,而不是其他文件导入的依赖,如果你没听懂我说什么,那就记得加上这句话就行,以后我们还会解释为什么的。 输入完后就是下面的样子,我装的是另一个IDE,叫IDEA,所以一些细节可能不同: {.text-center} 注意到if语句的左侧有个绿色的箭头,这就是运行的按钮。点击一下会出现一个菜单,选择Run ‘main’就可以运行起来,这时底部会弹出一个窗口,效果就像下面这样: {.text-center} 点击左侧第一个绿色圆圈箭头的按钮可以重新运行,点击左侧第二个红色的方块可以终止运行,然后你可以输入一个英文单词,它会变长全部大写。 当你运行过一次之后,右上方的工具栏就会变样子: {.text-center} 这是因为点击if语句左侧的绿色箭头会创建一个可以运行的配置,方便以后再次运行或者调试。下拉框右侧第一个绿色箭头是运行按钮,第二个臭虫是调试按钮。 调试(Debug) 调试是一种特殊的运行模式,在调试模式下,你可以让程序暂停,查看变量的值、函数调用的经过。 在进入调试模式之前,我们需要添加断点(breakpoint)。断点是会在程序运行达到一定条件时暂停程序的运行。主要用到的断点,是行断点:当程序将要但还执行指定行时,暂停运行。我们来在函数的return语句那行添加一个断点,方法是点击行号右侧灰色的的空白区域,此时一个红色的圆点会显示出来,再次点击圆点可以取消断点。添加断点后是下图的样子: {.text-center} 接着我们进入调试模式,方法是点击工具栏上的臭虫图标,就是下图红框圈出来的部分。 {.text-center} 点击后程序会运行起来,同时下方会弹出调试窗口。调试窗口相比运行窗口,其左侧从上往下,多出了第2个继续(灰掉了)和第3个暂停按钮,同时窗口内部分为两个标签页,分别是“Debugger”(下图红框圈出)和“Console”。“Console”标签页会显示程序的输入输出,包括input()输出的提示。 {.text-center} 让我们随便输入一句话,比如hello,再按下回车。 {.text-center} 这时编辑窗口中有一行高亮了出来,表示的是即将执行的一行。同时调试窗口从“Console”标签页切换到了“Debug”标签页。并且有两个子窗口,左边的“Frames”显示的是函数调用栈,右边的“Variables”显示的是当前作用域下的变量。 我们经常称函数调用的这种关系称为函数调用栈。这就涉及到一个概念,什么是栈。栈(stack) 指一种后添加的元素现出来的数据结构,有点像堆碟子,对应的还有一种是队列(queue),也就是指一种先添加的元素先出来的数据结构,有点像人们排队。具体见下方示意图。 栈的示意图 队列的示意图 而函数调用时,会创建一个局部作用域,里面包含了局部变量,这被称为栈帧(stack frame),而函数执行完毕时,这个刚插入的栈帧被丢弃。最后调用的那个函数总是先退出。函数一层层栈帧累积出来的数据结构就是一种栈,因此被称为函数调用栈。 继续回到调试界面,这时函数调用栈上有两个栈帧: 上面就是convert_string_to_uppercase()函数被调用的栈帧,你可以看到参数text2的值; 下面是全局作用域形成的栈帧,点击后,你可以看到全局变量text的值。 在“Variables”标签页左侧有个“+”按钮,点击后你可以添加一个表达式,IDE会时刻显示这个表达式的值。选中添加的表达式,点击“-”按钮就可以删除。 当成功暂停程序时,你可以选择怎么继续执行,这时就要关注调试窗口的上方,那里有一排按钮,就像下图显示的那样。 {.text-center} 我们暂且讲解图中红框框出来的4个按钮,从左到右依次讲解它们的用途: 步过:运行到下一行; 步入:如果这一行有函数调用,则进入该函数调用; 步出:运行到函数结束; 运行到光标:运行到光标所在的行。 我们点击两次步过,这时候就可以发现result变量出现了,且它的值是HELLO。最后我们可以点击右侧的继续运行按钮,执行完程序。 其他 PyCharm会通过灰色波浪线、黄色背景、红色波浪线等方式提示你的潜在错误。这时你只要按Alt + Enter就可以得到错误的解决办法之类的。 光标移到函数或变量上,再按Ctrl + Q就能显示文档信息。按住Ctrl键鼠标悬停到变量或函数上就可以显示减压信息。按住Ctrl键,鼠标点击变量或函数会跳转到定义或显示使用情况。 同样的按住Ctrl + /可以注释或者解除注释。 按住Alt,鼠标左键拖拽可以矩形选定。 复习 概念重提 这次我们介绍了Python语句和函数。 Python有下面的这些语句: 简单语句: 表达式语句:如函数调用print('hello'); 赋值语句:a = 1; break语句:用于跳出循环体,必须在循环体中使用; continue语句:用于略过剩余的循环体,进入下一次循环,必须在循环体中使用; pass语句:复合语句或函数定义时都需要一个子语句,如果想要省略子语句,需要加上pass; return语句:结束函数的执行,并将值返回给函数的调用处,必须在函数体中使用; 复合语句: if语句:像下面这样。必须包含if分支,可包含任意个elif分支,else分支可选。依次测试条件,若条件满足则执行分支; if condition1: statements1 elif condition2: statements2 else: statements3 while语句:像下面这样。如果条件用真则不断执行循环体; while condition: statements for语句:像下面这样。令element依次被赋值为iterable中的元素,执行循环体。iterable为可迭代对象,如range()、列表、元组等等; for element in iterable: statements while和for语句还可跟else子句,在函数不被break时执行,不太常用。 接下来我们学习了函数。函数定义的格式如下: def function(param1, param2, ...): """This is docstring.""" statements 定义函数时,可以给函数参数指定默认值。此外可以通过*args和**kwargs可以接收任意多剩余的位置参数和关键字参数。总的而言,函数的形参列表包含以下东西: 不带默认值的参数; 带默认值的参数; *args; 必须采用关键字参数赋值的参数; **kwargs。 在调用函数时,可以通过位置参数或者关键字参数给形参提供实参。也可以解包可迭代对象(如列表、元组等)成为位置参数,或者解包字典成关键字参数。不论如何,函数调用必须满足下面两个条件: 不能对一个参数赋两次值; 所有的位置参数必须在关键字参数之前。 此外我们还学习了lambda表达式,这是定义了一个简短的函数方法,其格式如下: lambda x, y, ...: expression 这个函数没有名字,lambda表达式产生一个值,就是该函数本身。 最后我们还学习了两个新数据结构,元组和字典。 其中元组用(elements, ...)表示,单元素元组必须结尾添加逗号(element,)。没有歧义时,括号可以省略。其他操作上元组和列表几乎一样,只是元组是不可变的。 字典是关联容器用{key: value, ...}表示,通过dict[key] = value可以创建一个键值对,通过dict[key]可以根据键获取值。 术语表 术语 英文 含义 语句 statement 一个不产生值的最小执行单元 简单语句 simple statement 不包含其他语句的语句 复合语句 compound statement 包含其他语句作为子语句的语句 副作用 side effect 修改程序状态或进行输入输出 表达式语句 expression statement 有表达式组成的语句 赋值语句 assignment statement 注意赋值是一种语句而非表达式 条件语句 condition statement 指Python中的if语句 循环语句 loop statement 指Python中的while语句和for语句 布尔类型 boolean 拥有两种值:真和假的类型 比较运算符 comparison operator 比较两个值的大小或是否相等 字典序 dictionary order 类似英文字典那样的对序列排序的方法 缩进 indent 在行首插入空白符,Python使用缩进判别语法 循环体 loop body 循环语句被循环执行的那一部分 死循环 dead loop 不终止的循环 可迭代对象 iterable object 可以按顺序遍历的对象 关键字 keyword 程序中被保留做特殊用途的英文单词 标识符 identifier 实体的名字 递归 recursion 函数直接或间接调用自己 调用 call 执行另一个函数,将传输传递给它并获取返回值的过程 形式参数 parameter 函数定义时参数列表中的名字 实际参数 argument 函数调用时传给函数的参数 函数体 function body 函数定义中被执行的那一部分 返回值 return value 函数执行完所得到的值 文档字符串 docstring 位于函数、模块等实体开头部分用于生成文档的字符串 按值调用 call by value 函数调用时,实参拷贝给形参的调用方式 作用域 scope 名字有效的范围 局部变量 local variable 定义在函数中的变量,其作用域到函数的结束 全局变量 global variable 定义在模块中的变量,其作用域到模块的结束 屏蔽 shadow 内层作用域的标识符使外层标识符不可见的现象 高阶函数 high-order function 以函数作为参数或返回值的函数 位置参数 position argument 通过实参的位置确定对应的形参的那些参数 关键字参数 keyword argument 通过给出形参明确定对应的形参的那些参数 元组 tuple 一种不可变的序列容器 字典 dictionary 一种关联容器 打包 pack 将给定的实参合成为元组和字典的过程 解包 unpack 将可迭代对象和字典变成实参的过程 lambda表达式 lambda expression 匿名函数

2019/11/11
articleCard.readMore

Python教程1 - 简介

这个系列的教程适用于Python新手,讲得相比其他教程可能会更深入一些,内容将依照Python官方的中文教程。我将在我的博客和知乎专栏上同步发表这个系列的文章。 本文是这个系列的第1篇文章,其中第1节将介绍如何安装Python,第2节会介绍Python基本类型和函数的调用方法,最后一节我会复习下先前讲到的知识。 Python介绍 Python版本 Python是免费开源的编程语言。它有两个版本:2和3。其中Python 2已经计划在2020年放弃支持,不建议大家再使用。这篇及之后的教程主要教Python 3。 安装Python 你可以从Python官网的下载页面下载并安装Python。目前(2019年11月)最新版的Python是3.8,但不够稳定。如果你需要使用之后提到的JupyterLab的话,建议安装3.7版本,目前最新的是3.7.5。对于Windows建议选择“Windows x86-64 executable installer”。当然这篇文章可能会过时,大家可以酌情选择版本安装。这里给出Python 3.7.5的Windows版下载链接。 安装的时候建议把add to PATH的选项勾上。 如何运行Python代码 Python是脚本语言或者称之为解释型语言。所谓解释型语言是相对编译型语言的,解释型语言不需要生成可执行文件(Windows上是后缀.exe的文件)就可以运行起来,因此开发起来更快捷,但运行速度更慢。而编译型语言则相反,因而开发较为麻烦,但运行速度快。将解释型语言代码一行行运行起来的软件称为解释器。而将编译型语言代码生成可执行文件的软件称为编译器。你有以下2种方式运行你的代码: 将代码直接输入在解释器中:如果你安装了官方的Python,会自带一个叫Idle的软件,打开Idle,就可以输入代码;也有可能你没有Idle,这时候你可以打开命令行(Windows上Win键+R,然后输入cmd打开命令提示符)输入python回车,然后输入代码,退出可以输入exit();之后我们还会介绍JupyterLab。 将代码保存成.py后缀的代码,再运行(Windows上双击即可),如果程序没有输入,可能会一闪而过。 第1种方式适合编辑测试代码,而第2种方式适合完成代码后便于运行,此外第2种方式可以使用个人喜好的编辑器或者IDE(IDE是集成开发环境的缩写,它提供了编辑、运行、调试一体化的开发环境),并且可以使用版本控制系统(如git,版本控制系统可以维护代码的所有历史版本,并提供多人协同开发之类的功能),我们会在以后介绍PyCharm的使用。 你现在可以不必急着安装编辑器/IDE。当然,我会给出我推荐的编辑器/IDE,以下按顺序排列。这只是个人偏好,我不想引起圣战: PyCharm Visual Studio Code Visual Studio Sublime Text Idle(Python自带) Notepad++ Notepad(Windows自带,如果你其他编辑器都没的话) Python的包管理器 Python可以被用于写爬虫,做网站,进行数据处理或者机器学习。其强大的功能除了来自于其优雅的语法,关键还在于它有很多库,包括标准库和可以下载的扩展库。我们称这些扩展库为包。而自动下载管理这些包的工具称之为包管理器。Python的包管理器叫pip,你可以打开命令行,输入pip help查看pip支持的命令。以下是最常用的下载和安装命令: pip install xxx:安装xxx包; pip uninstall xxx:卸载xxx包; pip install -r requirements.txt:将requirements.txt列出的包全部安装,许多Python项目会将需要安装的包列出在requirements.txt中,方便大家下载。 由于pip需要从国外下载包,这样网速会非常慢,遇到这种情况的可以考虑改一个源,从国内下载。比如可以改为清华TUNA的源,执行下面的命令即可。 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pip -U pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple 尝试安装JupyterLab JupyterLab可以更方便交互式地编辑运行代码。它是Jupyter Notebook的改进版。它还能显示表格和图片,如果你要做数据处理和可视化,那它会是很好的工具。它的界面是通过浏览器查看的。它将代码、输出及其他信息保存成后缀为.ipynb的格式。安装它需要费一番力气,接下来介绍如何在Windows上安装它,你大概需要7G的存储(主要是Visual Studio占的)。跳过这一节也无妨。 首先安装Visual Studio,下载链接在此处,下载Community版本就行,安装完毕之后打开Visual Studio Install,选择“桌面C++开发”,再选择安装,才算安装完毕,这是最耗时的一步。 然后安装Node.js,下载链接在此处,下载Current版本即可,安装一路默认。 最后打开命令行,输入pip install jupyterlab即可安装。 安装完毕后,在命令行输入jupyter lab就会自动打开网页。点击新建Python3 Notebook即可。 接下来介绍JupyterLab的简单用法。首先它是有自上向下排列的单元格组成的,每个单元格有输入框和输出内容。然后,其编辑是有两种模式的,一种是编辑单元格内容,一种是编辑单元格本身。编辑单元格内容模式下,会有一个单元格的输入框为白色且光标闪动,编辑文本像平常一样,可以按Tab补全,按Esc切换到编辑单元格模式;编辑单元格模式下,会有一个单元格左侧有蓝条,你可以点击x删除单元格,点击a在前方添加单元格,点击b在下方添加单元格,按Enter切换到编辑单元格模式。此外,在编辑单元格内容模式下,按Ctrl + Enter会运行单元格,而按Shift + Enter会运行并切换到下一单元格。 之后的教程我会同步发布Jupyter Notebook版本。 Python的非正式介绍 注释 Python的注释是以#开头到行末结束的一段文字,它会被彻底忽略,注意字符串(引号扩住的东西)中的#不会被作为注释。 # 这是注释 spam = 1 # 这也是注释 # 这还是注释 text = "# 这不是注释" 数学运算 Python可以被像计算器那样使用,像下面的示例,它支持+、-、*、/和括号,注意所有的括号均应使用圆括号。这里>>>后面的输入的内容,其他的是输出的结果。 >>> 2 + 2 4 >>> 50 - 5*6 20 >>> (50 - 5*6) / 4 5.0 >>> 8 / 5 # 除法返回浮点数(之后会介绍) 1.6 每一个值都有类型,关于类型详见2.2.2节。诸如2、4、20的整数属于int类型,中文为整数类型,简称整型;诸如5.0、1.6有小数部分的值属于float类型,中文为浮点数类型,简称浮点型。/运算附始终得到浮点类型,而其他数学运算如果有一个操作数是浮点数则结果是浮点数,否则为整数,关于运算符和操作数详见2.2.3。 还有求商运算(floor division向下除法,又称为整除)//和求余运算%: >>> 17 / 3 # 除法运算返回浮点型 5.666666666666667 >>> >>> 17 // 3 # 求商运算会抹去小数部分 5 >>> 17 % 3 # 求余运算 2 >>> 5 * 3 + 2 # 商 * 除数 + 余 17 此外Python中还有**乘方运算符: >>> 5 ** 2 # 5 的平方 25 >>> 2 ** 7 # 2 的 7 次方 128 接下来我们会介绍一些较为抽象,但很有帮助的概念。如果你发现接下来的部分很困难,可以跳过(2.2.1-2.2.4)。 表达式(expression) 所谓表达式,就是产生一个值的一串式子,上个例子中的2 + 2到8 / 5都是表达式。表达式是构建复杂程序的一个重要组成部分。表达式的一大特点是可以嵌套,如上面例子中的(50 - 5*6) / 4,其中50 - 5*6是个子表达式。 值(value)和类型(type) 每一个值都有类型,类型规定了可能的取值范围和可能的操作。如我们称2的类型为int整数类型,它可以表示所有的整数,而2.0是float浮点数类型,可以存储约16位有效数字。此外我们马上还会学到字符串类型,它可以用于存储一串文本,但它不能参与乘除和减法的运算。 运算符(operator)和操作数(operand) 运算符代表了要执行的运算,而操作数是运算的输入,如8 / 5中,/被称为运算符,而8和5就被称为操作数。 运算符可以根据其携带的操作数个数分类,有一元、二元和三元、或者称之为单目、双目和三目。之前的示例中的+、-、*、/都是二元运算符。一元运算符的一个例子是负号和正号,它也可以嵌套成形如--+1(负的负的正的1,即+1,也就是1)。 运算符也可以根据出现的位置分类。一元运算符有前缀和后缀之分,前缀出现在操作数的前面,类似的后缀出现在操作数的后面。Python中,所有的一元运算符都是前缀的(所以并没有阶乘运算符!)。二元运算符也有前缀和后缀,除此还有中缀,即出现在两个操作数的中间,Python的所有的二元表达式都是中缀的。中缀表达式主要是方便人们的理解而出现的(实际上如果都采用前缀表达式就没有优先级和结合性什么事了,当然你可以不知道我在说什么)。 当然还有运算符可能有多个组成部分,如两个配对的中括号组成的下标运算符obj[k](我们会在以后介绍,obj和k是变量)和函数调用运算符。运算符也可能有两个以上的字符组成,如等会儿会讲到的整除//,注意不能在这种运算符的中间加上空格。运算符也可能由一个单词(我们称之为关键字)组成,如由not组成的一元逻辑非not a(我们会在以后介绍,a是变量)。 优先级(precedence)和结合性(associativity) 当我们计算50 - 5*6,我们会自然地先计算5*6,因为我们知道先乘除后加减。先算什么后算什么,这就是优先级决定的。每个运算符都有一个优先级,优先级约高的运算符就越先算,如乘除的优先级就比加减高。而当优先级一样的运算符连接在一起的时候,如3 - 2 + 1,我们知道先算3 - 2,这就是结合性决定的。每个运算符都有结合性,结合性有两种,自左至右和自右至左。自左至右就是先算左边的,而自右至左就是先算右边。你所见的+、-、*、/都是自左至右结合的运算符。实际上Python的二元自右向左结合的运算符只有1个,就是次方运算符**,因而2**2**3是2**(2**3)就是256。注意:Python的次方运算符优先级高于负号,因而-2**2的值是-4。 括号能够忽略优先级,使子表达式先算,比如5 * (3 - 2),没人会再去先算5 * 3,这就是括号的作用。 当你背熟运算符优先级和结合性列表的时候,你可能会跃跃欲试,能不用括号就不用括号,然而这可能并不是个好习惯。对于一些晦涩难懂的代码而言,加上括号有时能比用注释更好地告诉别人你在做什么。 变量(variable)与赋值(assignment) 变量和赋值这两个概念是非常重要的,可以说如果你无法理解变量和赋值,那么你就不可能学会命令式编程。 先看看如何赋值。使用单个等号=给一个变量赋值。赋值左侧是变量名,右侧是表达式。赋值之后,可以用变量名指代原先表达式的值,进而出现在新的表达式中: >>> width = 20 >>> height = 5 * 9 >>> width * height 900 变量可以存储值,当然这个值也有类型,我们会称之为变量的类型,同一个变量可以存储不同类型的值。给变量赋予一个值的过程称为赋值。其实私底下,我个人更愿称这个行为为变量被绑定上了一个值,我认为绑定比赋值这个词描述Python处理变量更准确。因为Python的变量本质上只是指向一个值的名字,如果一个值没有变量指向,它就会被销毁(这个过程称为垃圾回收)。当发生赋值时,并不是旧的值被改成了新的值,而是变量指向了一个新的值,这时候如果旧的值没有变量指向,就会被销毁。实际上,在Python中,所有的整数、浮点数和字符串都不可被修改,修改的只是变量所指的东西。之后我们会进一步介绍类型的可变性与不可变性的相关概念,我们即将学习的列表list类型就是可变的。 赋值是一个语句,不是一个表达式,因而不能出现1 + (a = 2)之类的式子,关于语句的更多概念我们将在下一个教程中介绍。 变量名是一种标识符(identifier),所谓标识符就是各种名字的统称。合法的标识符由数字、大小写英文字母和下线符_组成,但不能以数字开头(虽然可以但不推荐用中文命名)。 最后,如果一个变量未定义(未赋值),试图使用它时会向你提示错误: >>> n # 试图引用一个未定义的变量 Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'n' is not defined 在交互模式下,上一次打印出来的表达式被赋值给变量_: >>> tax = 12.5 / 100 >>> price = 100.50 >>> price * tax 12.5625 >>> price + _ 113.0625 >>> round(_, 2) 113.06 字符串(String) 字符串这种类型用于存储有限长度的文本。使用一对单引号或一对双引号括住,即可创建字符串值。单引号和双引号除了转义稍有不同,完全等价。当遇到一些不可打印或是有特殊含义的字符就需要转义,我们先看例子,会在2.4.1我来详细解释转义: >>> 'spam eggs' # 单引号 'spam eggs' >>> 'doesn\'t' # 使用 \' 表示单引号... "doesn't" >>> "doesn't" # ...或者使用双引号括住字符串 "doesn't" >>> '"Yes," they said.' '"Yes," they said.' >>> "\"Yes,\" they said." '"Yes," they said.' >>> '"Isn\'t," they said.' '"Isn\'t," they said.' 接下来我们也会介绍一些概念,2.4.1需要一定的理解,而2.4.2和2.4.3如果有阅读困难可以跳过。 转义(Escape) 转义字符是用一串文本来代表一个字符的方式,所有的转义序列都以反斜杠符\开头,可以大致分为以下几类: 不可打印或不方便书写的字符:诸如换行\n和Tab\t之类的字符不方便在代码中书写出来,因而我们就用一串文本来表示这一个字符,关于换行、回车、Tab的介绍在后面。 有特殊含义的字符:有三个这种情况的: 反斜杠符\\,由于它本身被作为转义字符的开始,打印反斜杠符就不得不转义了; 如果字符串使用单引号括住,这时候字符串中的单引号作为字符串的起始和终止符也需要转义成\'; 类似地,如果字符串使用双引号括住,这时候字符串中的双引号也需要转义成\"。 字符编码表示:计算机只能存储二进制,因而每个字符都被编码成了一个整数,Python每个字符对应4字节的整数。我们可以直接在字符串中输入包含编码的转义序列得到对应的字符,有4种这样的转义序列,我们会在2.4.3进一步讨论: 形如\ooo,\后面跟1到3位8进制数,得到对应的1字节字符,其他3字节为0; 形如\xhh,\x后面根2位16进制数,得到对应的1字节字符,其他3字节为0; 形如\uxxxx,\u后面跟4位16进制数,得到对应的2字节字符,其他2字节为0; 形如\Uxxxxxxxx,\U后面跟8位16进制数,得到对应的4字节字符。 对于最后一种用整数表示字符编码的方式,我们给一些例子: >>> '\101' # A 对应的编码是 65,而 1 * 64 + 1 = 101 'A' >>> '\x41' # 4 * 16 + 1 = 101 'A' >>> '\u6211' # “我”对应的编码是 25105,而 6 * 4096 + 2 * 256 + 1 * 16 + 1 = 25105 '我' >>> '\U0001f60b' # 和上面一样的原理,“😋”的编码是 128523,这是一个emoji '😋' 注意,转义序列对应的字符长度还为1,上面4个例子中的所有字符串长度都为1,即只包含一个字符。2.5会介绍3个函数,len获取字符串的长度、ord获取字符对应的整数编码,chr获取整数编码对应的字符。 最后我列出所有的转义字符列表,你不需要记住每一项: 转义序列 含义 \紧跟换行 \和换行被忽略 \\ 反斜杠符 (\) \' 单引号 (') \" 双引号 (") \a ASCII 响铃 (Bell,BEL) \b ASCII 退格 (Backspace,BS) \f ASCII 换页 (Formfeed,FF) \n ASCII 换行 (Linefeed,LF) \r ASCII 回车 (Carriage Return,CR) \t ASCII 水平制表符 (Horizontal Tab,TAB) \v ASCII 垂直制表符 (Vertical Tab,VT) \ooo 8进制值为 ooo 的字符 \xhh 16进制值为 hh 的字符 \N{name} 名字为name的 Unicode 字符 \uxxxx 16进制值为 xxxx 的16位字符 \Uxxxxxxxx 16进制值为 xxxxxxxx 的32位字符 空白符 换行\r、回车\n、水平制表\t等一些字符被称为空白符,实际生活中常用的空白符就这3个。 在Windows上打开记事本,按下Tab键后,光标会向右跳过约8个字符的位置,你就输入了水平制表符。它被用于将不同行的字对齐到同一列上,制作表格。然后你可以在Python中尝试下一个例子: >>> print('1\t2\n34\t5') 1 2 34 5 print函会将字符串打印出来。这里我们输出了两行,第一行是数字1跟水平制表符再跟2,第二行是数字34跟水平制表符再跟5。可以看出即使前面字符的长度不同,水平制表符还会把它之后的字符对齐到下一个8个字符的位置。 当你在记事本敲入回车,你的光标会到下一行,这个时候你同时输入了两个字符:回车\r之后换行\n。在Windows上,文本文件的换行其实包含了这两个字符,即CRLF。但在别的系统不是这样的,Linux的文本文件以\n换行,即LF。而Mac的文本文件又以\r换行,即CR。各个系统之间不同的换行方式其实带来了一些问题,我们会在以后关于文件IO的部分更细致的谈到这些问题。说了这么多,可能很多人还不知道回车换行这两个字符的含义,这要追溯到计算机出现之前,那时候有电传打字机。在那个时代,回车是让打字机回到行首,而换行则是让打字机移到下一行。虽然电传打字机消失了,但回车和换行还是遗留到了现在。 进制、字节、码位与编码 关于2进制、8进制、16进制的数学含义,我就留给数学老师来讲解了,如果这一段阅读有困难可以忽略。这里指出一下16进制使用额外的A到F(小写亦可)分别表示10到15。1个字节是8个比特,而16进制1位能表示2进制的4位,因而1个字节可以对应2位16进制。因此,大家更喜欢16进制,8进制并不常用。 接下来我们谈谈字符编码,ASCII是最早出现的编码规范,它使用了7个比特存储字符,里面的字符只有很少的标点符号、英文、数字等。 而Unicode是之后出现的编码规范,它包含了中日韩表意文字、甚至还有emoji。每个Unicode字符都与一个32位的整数对应(因而它可以表达2的32次方个字符,目前还留有很大的空间供未来使用),这个整数称为码位(codepoint)。Python的字符串内部就是存储了这些码位。然而如果直接每个字符32位存储在文本文件里,会浪费很多空间(因为实际上除了emoji和少数汉子,几乎所有的字符都最多只用了16位),因而我们采用变长字符编码来将一个码位映射成多个字节。其中最著名的编码方案就是UTF-8,它会把32位的码位映射成1到6个字节,而且它与ASCII完全兼容。 在Python中,字符串通过encode()函数编码成实际存在在文件中的bytes类型,而bytes类型通过decode()函数编码成字符串,我们在2.5节会给出例子。 然而在Unicode出现之前就有了中文Windows系统,那时候中文都采用国标码GBK编码。这里就不再赘述。当你的代码保存成文件后,如果你使用了中文做字符串,那么声明你代码所用的编码可能是必须的。你需要在代码的最开始加上一行如下的声明: # -*- coding: encoding -*- 其中encoding改成你的编码方式,比如gbk或者utf-8。 字面量(Literal) 字面量是指源代码中用于表示一个值的一串文本,它也有类型。比如1就是int类型字面量,而1.0就是float类型字面量,还有'abc'就是字符串类型字面量。 函数(Function) 函数是许多编程语言的又一个重要组成部分。它是一种特殊的类型,支持被调用。它将一系列的操作封装、复用。这里我们不会涉及如何编写函数,只会谈谈如何使用它。 首先我们来谈谈普通函数的调用,它的形式类似于func(arg1, arg2, ...),先写函数名fun,后面再用括号括住逗号分割的列表,我们称之为参数。函数调用的本质也是个运算符,他拥有很高的优先级,其中的func和arg1都可以为子表达式。不同的函数会有不同的参数个数和参数类型的要求。类似数学的函数,调用一些函数会得到值,我们称为返回值。我们来看例子,print函数接受多个参数,参数没有类型限制,不返回值(也可以认为返回空),它会将所有的参数输出出来,并在最后加上换行: >>> print(3, 'is odd') 3 is odd >>> print('123\n456') 123 456 最后一个例子我们调用print输出了123、换行和456。接下来我们再介绍3个函数: len:接受1个字符串类型的参数,返回字符串的长度; ord:接受1个长度为1的字符串,返回字符串中字符的码位; chr:接受1个整数,返回整数对应的字符组成的长度为1的字符串。 我们可以说ord和chr函数互为反函数。看例子: >>> len('') # 空字符串 0 >>> len('123') # 长度为3 3 >>> ord('😋') 128523 >>> chr(128523) '😋' 然后,还有一类特殊的函数叫方法,它是某一类型拥有的函数,它的调用形如obj.method(arg1, arg2, ...),与普通函数的差别是多了obj.,obj是某一类型的值(或者产生该类型的值的表达式,包括变量)。其实obj.method是个子表达式,它获得一个函数,然后和后面的函数调用运算符构成了方法的调用。这里介绍两个方法: 字符串类型的encode:可以接受一个代表编码的参数,将字符串转化为该编码的bytes类型返回。 bytes类型的decode:可以接受一个代表编码的参数,将bytes解码为该编码的字符串类型返回。 >>> '😋'.encode('utf-8') # 字符串用 utf-8 编码成 bytes b'\xf0\x9f\x98\x8b' >>> _.decode('utf-8') # bytes 再用 utf-8 解码成字符串 '😋' 这里我们可以看出这个这个emoji被编码成了4个字节,然而我们可以注意到这个编码f09f988b(16进制)和它的Unicode码位0001f60b(16进制)是不同的。 字符串(续) 我们继续讲解字符串。先前讲到字符串的转义,然而有时候我们不需要转义,希望让\直接代表它本身(这种情况多发生在Windows路径和正则表达式中),我们可以使用原始字符串(raw string),只需要在字符串的前面加上r即可: >>> print('C:\some\name') # 这里 \n 表示换行! C:\some ame >>> print(r'C:\some\name') # 引号前有 r C:\some\name 普通的字符串是不能跨行的(除了\紧跟换行这种转义),而Python也提供了三重引号用于跨行输入的多行字符串,如"""..."""和'''...'''(...表示内容)。字符串的中的回车会被包含到字符串中,如果你不想包含换行可以在行尾添加一个\,这其实就是上面转义表中的第一行转义。如下: print("""\ Usage: thingy [OPTIONS] -h Display this usage message -H hostname Hostname to connect to """) 上面的代码会产生如下输出: Usage: thingy [OPTIONS] -h Display this usage message -H hostname Hostname to connect to 注意代码中的第一个换行和反斜杠符一起被消去了。 接下来我们来介绍字符串支持的一些操作:字符串和字符串之间可以用+连接;字符串和整数做乘法可以使字符串重复整数次。如下: >>> # 'un' 重复 3 次,而后拼接上 'ium' >>> 3 * 'un' + 'ium' 'unununium' 相邻的字符串字面值可以不用+就串联起来,这拥有最高的优先级。注意只有字面量有这种行为,变量和字面量不能这样连接: >>> 'Py' 'thon' 'Python' >>> prefix = 'Py' >>> prefix 'thon' # 无法将变量和字符串字面常量串接在一起 File "<stdin>", line 1 prefix 'thon' ^ SyntaxError: invalid syntax 接下来我们来讲字符串的的索引和切片操作。有时,我们需要将字符串的某个位置或某一部分取出来,这就需要索引(index)和切片(slice)操作。 在字符串后面用中括号括住一个整数,就可以取出整数对应位置的字符,类似text[n]。索引是从0开始到字符串长度减1,索引也可以是负数,表示倒数第几个字符,是从-1到负的字符串长度。超出这个范围的索引是个错误。注意Python没有单独的字符类型,字符就是长度为1的字符串: >>> word = 'Python' # 长度为 6 >>> word[0] # 位置为 0 的字符 'P' >>> word[5] # 位置为 5 的字符(最后一个字符) 'n' >>> word[-1] # 倒数第 1 个字符 'n' >>> word[-2] # 倒数第 2 个字符 'o' >>> word[-6] # 倒数第 6 个字符(第一个字符) 'P' 可以用下面的图来表示Python这个字符串中各个字符的位置,这里位置6不能用于索引,但可以用于表示切片的结尾: +---+---+---+---+---+---+ | P | y | t | h | o | n | +---+---+---+---+---+---+ 0 1 2 3 4 5 6 -6 -5 -4 -3 -2 -1 接下来是切片,切片是中括号括住冒号分隔的两个整数,它返回一段字符串,类似text[a:b]。它会取出索引值为[a, b)区间(左闭右开)内的所有字符(b的值可以为字符串的长度)。此外a和b都可以省略,省略a就相当于a为0(切片从头开始),省略b就相当于b为字符串长度(切片一直到尾结束)。与索引不同,切片的起始和终止都可以超过范围,如下: >>> word[42] # the word only has 6 characters Traceback (most recent call last): File "<stdin>", line 1, in <module> IndexError: string index out of range >>> word[4:42] 'on' >>> word[42:] '' 如果你第一次学习编程,或者一直使用类似MATLAB的软件,你可能很好奇为什么要从0开始数数,又为什么使用左闭右开的区间。实际上从0会更加方便,以后会体现出来。 Python的字符串是不可变的,因而以下的操作是个错误: >>> word[0] = 'J' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'str' object does not support item assignment >>> word[2:] = 'py' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'str' object does not support item assignment 但你可以创建字符串,并将它赋值给新的变量: >>> word = 'J' + word[1:] >>> word 'Jython' >>> word = word[:2] + 'py' >>> word 'Jypy' 列表(List) 通过方括号括起,逗号分隔的一组值,就可以得到列表字面量,形如[a, b, ...., n]。一个列表可以包含不同类型的元素,但通常各个元素类型相同。 >>> squares = [1, 4, 9, 16, 25] >>> squares [1, 4, 9, 16, 25] 和字符串一样,列表支持索引和切片,不过索引返回的是元素: >>> squares[0] # 索引返回元素 1 >>> squares[-1] 25 >>> squares[-3:] # 切片返回新的列表 [9, 16, 25] 完整的切片会返回列表的浅拷贝,形如squares[:]。所谓浅拷贝就是返回的是个新列表,但两个列表中所指的元素是一样的,我们会在之后给出一个例子。 列表支持+的拼接操作和*的重复操作,如下: >>> [1] * 3 + [2] [1, 1, 1, 2] 列表是可变的,因而可以对列表的索引赋值,其结果是元素被替换: >>> cubes = [1, 8, 27, 65, 125] # 位置 3 的值是错的 >>> cubes[3] = 64 # 替换掉错误的值 >>> cubes [1, 8, 27, 64, 125] 通过append()方法可以为列表添加元素,这会改列表本身: >>> cubes.append(216) # 添加 6 的 3 次方 >>> cubes.append(7 ** 3) # 添加 7 的 3 次方 >>> cubes [1, 8, 27, 64, 125, 216, 343] 给切片赋值也是可以的,这可能会改变列表的大小,或者把列表清空: >>> letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g'] >>> letters ['a', 'b', 'c', 'd', 'e', 'f', 'g'] >>> # 替换掉一些值 >>> letters[2:5] = ['C', 'D', 'E'] >>> letters ['a', 'b', 'C', 'D', 'E', 'f', 'g'] >>> # 移除这些值 >>> letters[2:5] = [] >>> letters ['a', 'b', 'f', 'g'] >>> # 清空列表 >>> letters[:] = [] >>> letters [] 函数len()也可以用于获取列表的长度: >>> letters = ['a', 'b', 'c', 'd'] >>> len(letters) 4 列表也可以嵌套列表,甚至嵌套它自己,如下: >>> a = ['a', 'b', 'c'] >>> n = [1, 2, 3] >>> x = [a, n] >>> x [['a', 'b', 'c'], [1, 2, 3]] >>> x[0] ['a', 'b', 'c'] >>> x[0][1] 'b' 最后我们为浅拷贝给出一些例子: >>> b = a = ['a', 'b', 'c'] # 让 a 和 b 指向同一列表 >>> a[:] = [] # 清空 a 指向的列表 >>> b # b 也为空 [] >>> # 采用浅拷贝,这里给出个为啥叫“拷贝”的例子 >>> a = ['a', 'b', 'c'] >>> b = a[:] # b 是 a 的浅拷贝 >>> a[:] = [] # 清空 a 指向的列表 >>> b # b 不为空 ['a', 'b', 'c'] >>> # 再给出一个为啥叫“浅”的例子 >>> a = [1, [2]] >>> b = a[:] >>> a[0] = 3 # a 修改了但 b 没有修改 >>> b [1, [2]] >>> a[1][0] = 4 >>> b [1, [3]] 最后一个例子可能对初学者有点困惑,我们把最后的变量布局画出来,浅拷贝只是创建了新列表,但新列表所指向的元素和旧列表是公用的,如果元素都是不可变的可能感受不到这个差异,但如果元素是可变的,那么修改旧列表中的该元素,新列表也会改变: +---+---+ a -> | 3 | | +---+-+-+ | +---+ +---> | 4 | | +---+ +---+-+-+ b -> | 1 | | +---+---+ 复习 概念重提 有两种方法运行Python代码: 交互运行:好处是方便测试,有以下几种: 用Idle:只有Windows可用,有自动补全; 在命令行里用Python:跨平台,无自动补全; 在命令行里启动JupyteLab,并在浏览器中使用:有最好的交互体验,文件后缀为.ipynb。 保存执行:好处是方便使用,可以选择编辑器/IDE,并搭配版本控制系统,文件后缀为.py。 运行Python代码的软件称为Python解释器。Python的扩展库称为包,管理这些包的软件叫包管理器,Python自带的包管理器叫pip。你可以在命令行中输入以下命令安装/卸载包: pip install xxx:安装xxx包; pip uninstall xxx:卸载xxx包; pip install -r requirements.txt:将requirements.txt列出的包全部安装。 关于JupyterLab的安装和使用这里不再重复。 Python的注释以#开头,到行尾结束。 Python对于数学类型支持+、-、*、/、//(整除)、%(求余)和**(乘方)。数学类型有整型和浮点型。 使用赋值语句可以将值绑定到变量。交互模式下,_变量存储上一个表达式的值。 字符串字面量可以由双引号和单引号括住,特殊的字符需要转义。除了普通的字符串字面量,还有原始字符串和多行字符串。字符串支持数乘重复和加法拼接、索引和切片操作。 函数是一种特殊的类型,它可以被调用。函数可以分为普通函数和方法。有以下函数或方法: print()打印一系列的值; len():获得字符串或列表的长度; ord():返回字符的码位; chr():根据码位返回字符; 字符串的encode():将字符串转换成bytes; bytes的decode():将bytes转换为字符串; 列表的append():向列表追加元素。 列表是可变的类型,同样支持数乘重复和加法拼接、索引和切片操作。由于可变,它的索引和切片都可以被赋值。它存储的不必是同一类型,可以构成嵌套列表。 术语表 术语 英文 含义 表达式 expression 产生一个值的一串式子 类型 type 每个值有类型,类型决定了操作和取值范围 整型 int 存储整数的类型 浮点型 float 存储实数的类型,会有精度损失 运算符 operator 代表所要进行的运算 操作数 operand 代表运算的输入,是一种值 优先级 precedence 决定不同运算符运算先后顺序的主要因素 结合性 associativity 决定同优先级运算符运算的顺序 变量 variable 能代表值的一个名字 赋值 assignment 将值绑定到名字的操作 标识符 identifier 名字的统称 字符串 string 存储文本的类型 转义 escape 用一串文本代表一个字符 字面量 literal 代码中代表一个值的文本 函数 function 一种类型,可被调用,接受多个值,返回一个值 参数 parameter 函数接受的值 返回值 return value 函数返回的值 方法 method 一种特殊的函数,属于某一类型 浅拷贝 shallow copy 一种只拷贝最外层成员的拷贝方法

2019/11/1
articleCard.readMore

TypeScipt学习笔记(未完待续)

这篇文章是TypeScript的学习笔记,主要内容来自TypeScript官网的handbook。 基础类型 TypeScript提供了如下类型: boolean:Bool类型; number:数字类型(包括整数和浮点数); string:字符串类型; type[]或Array<type>:数组,其中type是某一类型,下同; [type1, type2, ...]:元组,对元组的下标索引会得到对应的类型,越界会报错; enum Type {value1, value2, ...}中的Type:枚举,通过Type.value来访问枚举值对应的整型,类似C语言,可以为枚举设置不同的整型值,也可通过Type[index]来访问整型对应的枚举值字符串; any:任意类型,与Object类型类似(注意大写),但Object类型只允许被赋予任意值,但不能访问任意属性和方法; void:用于函数返回值表示不返回值,对于void变量,则只能赋值undefined或者null(--strictNullChecks不开启); undefined和null:与同名的值对应,默认情况下它们是所有其他类型的子类型,但--strictNullChecks开启时,它们只能赋值给any或者对应的类型或者undefined赋值给void; never:用于函数返回值类型,表示函数始终不返回(如抛异常),它是任意类型的子类型,但不是其他类型的父类型,即使是any也不能赋值给never; object:(注意小些)代表所有非原始数据类型的类型。 类型断言是告知编译器相信程序员所指定的类型,有两种等价的形式:<type>value和value as type。在使用JSX的时候只有第二种形式可用。 变量声明 与JavaScript相同,var有函数级作用域,可被定义多次;而let有块作用域,不能屏蔽同作用域同名变量。const的变量不能修改,但可以修改其成员。TypeScript支持数组解构(包括let [first, ...rest] = [1, 2, 3, 4];或省略部分元素)、元组解构(类似于数组)和对象解构(支持...剩余语法、属性重命名let { a: newName } = o;、默认值let { a = 1001 } = o)。此外还支持解构相反的传播语法,如[0, ...first, ...second, 5]和{ ...defaults, food: "rich" }。 接口 使用如下语法定义接口: interface Name { property: Type1; optionalProperty?: Type2; readonly ReadonlyProperty?: Type3; // 如果允许包含其他属性 [propName: string]: any; } 也可以用这样的类型代替接口{ property: Type1, optionalProperty?: Type2 }。 TypeScript还有ReadonlyArray<T>这种数组类型,只是可变方法都移除了。 此外还可以创建函数接口,其中参数名不必匹配: interface Func { // 函数类型 (param1: Type1, param2: Type2): Type3; } 此外也可以创建数组接口: interface StringArray { [index: number]: string; } 类可以实现接口,如下: interface ClockInterface { currentTime: Date; setTime(d: Date): void; } class Clock implements ClockInterface { currentTime: Date = new Date(); setTime(d: Date) { this.currentTime = d; } constructor(h: number, m: number) { } } 但注意一个类实现一个接口的时候,只有实例是检查的,而静态方法不检查。而类的构造函数属于静态方法,只有当把类作为实例的时候才能检查其构造函数。 此外还可以用extends关键字扩展接口。对于一个接口可以扩展多个接口。 类 总的来说TypeScript里的类和JavaScript的类似,这里着重于不同之处。TypeScript的类的字段要显式声明。子类构造函数必须调用super()。成员可以有访问权限public(默认)、private和protected。可以有readonly字段,必须由构造函数或声明处初始化。 此外还有抽象类,如下: abstract class Animal { abstract makeSound(): void; move(): void { console.log("roaming the earth..."); } } 接口也可以继承类。 函数 函数类型为(param1: type1, param: type2, ...) => returnType。可选参数必须在必选参数的后面。可以对this参数加类型注解,确保它是某种类型。可以对一个函数的类型进行重载(不包括定义),如下: let suits = ["hearts", "spades", "clubs", "diamonds"]; function pickCard(x: {suit: string; card: number; }[]): number; function pickCard(x: number): {suit: string; card: number; }; function pickCard(x): any { // Check to see if we're working with an object/array // if so, they gave us the deck and we'll pick the card if (typeof x == "object") { let pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // Otherwise just let them pick the card else if (typeof x == "number") { let pickedSuit = Math.floor(x / 13); return { suit: suits[pickedSuit], card: x % 13 }; } } 注意function pickCard(x): any不是重载的一部分。 泛型 可以给函数添加类型变量,创建泛型函数: function identity<T>(arg: T): T { return arg; } 调用时,如果泛型是函数参数,可以省略模板参数。上述的函数,其类型写下来是<U>(arg: U) => U。此外在类名后面也可以添加类型变量,构成泛型类。 类似Java,类型变量可以有类型约束如<T extends someType>。 枚举 数字枚举如上所示,此外也可以有字符串枚举或者混合的。还有const枚举,形如const enum Enum { ... },它会彻底在编译时移除,只能含有编译期计算的值。 类型推导 (未完待续)

2019/9/12
articleCard.readMore

Rust学习笔记

这篇文章是Rust的学习笔记,主要内容来自Rust官方的《The Rust Programming Language》。 常见编程概念 变量和可变性 默认通过let x;定义的变量是不可变的,这样可以更安全且更利于并发。通过let mut x;定义可变的变量。可以在变量名后面后缀= init_value初始化。初始化不是必须的。 使用const NAME: type = value;定义常量,其中类型注解和初始化不能省略。常量可以被定义于任何作用域,如全局作用域,而变量不行。常量只能被常量表达式初始化。常量生命期是整个程序运行的,而其作用域是它被声明的作用域。 可以通过多次let x;来屏蔽先前定义的变量,常量不能这么做。新的变量与被屏蔽的变量不必是同一类型的。 数据类型 Rust是静态类型语言,所有变量的类型在编译期确定。 标量类型 Rust有四种标量类型,分别是整数、浮点数、Bool和字符。其中整数类型如下表所示: Length Signed Unsigned 8-bit i8 u8 16-bit i16 u16 32-bit i32 u32 64-bit i64 u64 128-bit i128 u128 arch isize usize 其中isize 和usize在x86系统上是32位的,在x64系统上是64位的。下表是整型字面常量,除了Byte字面常量,其他类型字面常量都允许有类型后缀如57u8,同时_可以作为视觉上的分隔符,如1_000。 Number literals Example Decimal 98_222 Hex 0xff Octal 0o77 Binary 0b1111_0000 Byte (u8 only) b’A' Rust默认使用i32,而isize和usize主要用于集合上的索引。在debug模式下,整数溢出会导致程序panic;在release模式下,溢出不会报错,而会发生wrap。如果你想显式地要wrap,可以使用std::num::Wrapping。 Rust有两种浮点数类型f32和f64。默认使用f64 。也可以采用后缀修改字面常量的类型。 Rust的bool类型有两种取值:true和false。其大小是1字节。 Rust的char类型占四字节,可以代表任意Unicode字符。字符字面常量由单引号括住(区别于字符串,是用双引号括住)。 复合类型 Rust有两种复合类型,元组和数组。 元组有固定长度,元素类型可以不同,但不能改变。用括号括住,逗号分隔的列表表示元组,如(500, 6.4, 1u8),其类型为(i32, f64, u8)。可以使用模式匹配获取元组内的东西,如let (x, y, z) = tup;。除此之外也可以用.0、.1来获取第0个元素、第1个元素等等。 数组的每个元素都必须是相同的类型,它也拥有固定的长度。用中括号括住,逗号分隔的列表表示数组,如[1, 2, 3, 4, 5],其类型为[i32; 5]。如果想创建多个同一值的数组,语法如[3; 5]。可以通过arr[index]访问,其中index是usize类型。越界访问可以通过编译,但程序运行时会有runtime错误。 元组和数组的末尾都可以有多余的逗号,单元组只能用(x,)表示。 函数 使用fn关键字声明函数,形如fn func() {}。建议变量和函数命名都采用蛇式命名法(如snake_case)。即使函数在后面定义,也能使用。 函数可以带有参数,形如fn func(x: i32, y: i32) { ... },每个参数都需要跟类型注解。函数调用形如func(x, y)。 语句是不返回值的,而表达式则会求值。用let创建并初始化变量和函数定义都是语句。块{}创建了新的作用域,其值是最后一个表达式的值,如果没有则返回空元组()。 通过fn func() -> type { ...; value } 创建一个有返回值的函数。注意这里不能写成{ ...; value; },这样相当于返回空元组()。可以通过return关键字提早返回。 注释 // ...一直到行末构成单行注释。 控制流 if表达式 if表达式形如if condition { ... } else { ... }其中else分支是不必须的,而condition的类型必须是bool。Rust不会将非bool类型自动转化为bool类型。使用下方的代码可以构建多条件分支: if condition1 { // ... } else if condition2 { // ... } else if condition3 { // ... } else { // ... } 使用太多的else if可能使代码看起来混乱,建议可以使用match。 if是一种表达式,可以嵌套在别的表达式中,但其各支产生的值必须是同一类型。注意省略的else分支具有()类型的值;if表达式如果是语句的开头,需要括号括起。 循环 Rust有3种循环:loop、while和for。 loop循环形如loop { ... }会不停地执行。可以在循环内,通过break value;返回一个值,value不是必须的(此时返回())。 while循环形如while condition { ... }。for循环形如for element in iterator { ... }。如果a是数组,通过a.iter()得到迭代器;通过begin..end可以得到一个范围迭代器,end可以省略,它有rev()方法可以倒过来迭代。注意while和for循环返回()。 理解所有权 什么是所有权 有些语言使用垃圾回收管理内存,而另一些语言需要手动分配和释放。而Rust采用所有权管理内存,所有权不会带来额外的性能开销。 记住以下几条: Rust中的每一个值都有一个变量是它的所有者 一个时候只能有一个所有者 离开所有者作用域时,值被丢弃 变量从它声明开始一直到当前作用域结束都始终有效。 String类型是一种可变的字符串,通过String::from("hello")可以从字符串常量中构造出String类型的对象。其内容就是放在堆上,栈上仅保留指针。为了能把内存返回操作系统,Rust引入所有权。当所有者变量离开作用域时,drop函数会被调用,进而内存会被释放。 对于简单的类型,下面的代码会复制值产生两个对象。但对于没有实现Copy trait的对象如String类型的,这会是一个移动操作,s1将无效。如果要复制,可以let s2 = s1.clone();。如果一个类型的某些部分实现了Drop trait,那么就无法将该类型注解为有Copy trait的。 let s1 = ...; let s2 = s1; 以下的类型实现了Copy trait: 所有的整数类型; 所有的Bool类型; 所有的浮点数类型; 所有的字符类型; 元素都实现了Copy trait的元组类型; 元素实现了Copy trait的数组类型。 函数的传参和返回,与赋值类似都会发生所有权转移。可以通过传递参数将所有权转移至函数,再通过返回值将所有权转移回来(Rust允许多返回值),然而这种做法过于琐碎,应当使用引用。 引用和借用 使用&variable可以创建对变量的引用,使用&mut variable创建对变量的可变引用。如果variable的类型是Type,则其引用的类型是&Type,可变引用的类型是&mut Type。对引用的赋值、传参和返回不会移交所有权,这被称为借用(borrowing)。 在某一作用域内对某一变量只能同时有要么一个可变引用,要么任意数目不可变引用。注意引用的作用域是从它引入一直到最后一次使用。这个机制是为了避免数据竞争(data race),当下面三条发生时就会有数据竞争: 多个指针同时读取一个数据; 至少有一个指针被用来写数据; 没有同步机制。 返回局部变量的引用是个错误。 切片类型 另一种不需要所有权的数据类型是切片,切片可以引用容器的一段连续的元素,而非整个容器。通过&s[starting_index..ending_index]获取切片。内部实现上,切片存储起始位置和长度。如果想要从开始切片,可以省略starting_index;如果想要切片到结尾,可以省略ending_index。 字符串的切片必须是在合法的UTF-8编码边界处。字符串切片和字符串字面常量的类型都是&str。对于元素是Type类型的数组切片,其类型是&[Type]。 对结构化数据使用结构体 结构体和枚举是Rust创建新的类型的基础。 定义并实例化结构体 如下定义结构体: struct Name { field1: Type1, field2: Type2, // ... fieldN: TypeN, } 实例化结构体如下,其中字段的顺序不必和定义的顺序一致: let foo = Name { field1: value1, field2, // shorthand for `field2: field2,' // ... } 访问字段采用foo.field的方式。只有当foo是可变的时候,才能修改foo.field,且Rust不支持某几个字段是可变的。此外还可以从现有的实例中更新得到一个新的实例: let bar = Name { field1: value1, // ... ..foo } 此外也可以创建没有具名字段的元组结构体。其定义和使用如下: struct Name(Type1, Type2, /* ... */, TypeN); let foo = Name(value1, value2, /* ... */, valueN); 即使字段类型完全一致,不同的元组结构体也是不同的类型。元组结构体可以解构(如let Name(var1, ...) = foo;),也可以通过.然后跟索引访问元素。 结构体也可以没有字段,这时结构体被称为类单位结构体(unit-like structs),它们和()很像。 在println!的格式化字符串中的{:?}代表使用Debug格式输出,用{:#?}代表有更好看格式的Debug输出。在结构体定义前添加#[derive(Debug)],即可使结构体实现Debug trait。 方法语法 方法的第一个参数必须是self,代表调用时的结构体实例。定义方法形式如下: impl StructName { fn func(&self, param1: Type1 /**, ... **/) -> RetType { // ... } } 注意func的第一个参数&self,实际上,方法可以获取所有权self、不可变借用&self或者可变借用&mut self。object.something()方法调用时,Rust会对object自动引用或解引用。 此外也可以在impl块里定义不以self为参数的函数(它们不是方法),可以通过StructName::func()调用。 同一个结构体有多个impl块是合法的。 枚举和模式匹配 Rust的枚举类型更像是函数式编程语言的代数数据类型。 定义枚举 如下定义枚举: enum Name { Value1, // Without data Value2(Type1, Type2 /*, ... */), Value3 { name1: Type1, name2: Type2 /*, ... */}, } 用Name::Value1、Name::Value2(type1, type2 /*, ... */)或Name::Value3 { name1: type1, name2: type2 /*, ... */ }创建枚举对象。 枚举也可以拥有impl块,即拥有方法。 标准库有个Option枚举,用于代表可以缺失的值,它的定义如下: enum Option<T> { Some(T), None, } 可以使用Some(value)来创建Option值,如果使用None,需要类型注解,注意使用它们不需要加前缀。 match控制流运算符 如下定义match表达式: match value { pattern1 => value1, pattern2 => value2, // ... } 其中每一支的值也可以是个块语句;每一支的模式可以是字面值、变量和枚举值构造器(想不出更好的名词了)。模式匹配必须匹配完所有的可能。可以在最后的模式中使用_占位符捕获所有可能。 用if let简化控制流 如下使用if let: if let pattern = value { // ... } else { // ... } 其中pattern和match表达式的一致,else可选。if let等价于单支match。 使用包、Crate和模块管理增长中的项目 Rust使用一系列的特色来管理代码,包括哪些细节需要暴露、哪些细节是私有的以及哪些名字属于程序。这些特性被称为模块系统,它包含: 包:Cargo的特色,用以构建、测试和分享crate; Crate:树状的模块,提供一个库或可执行; 模块和use:使你能控制组织、作用域和路径的权限; 路径:一种命名如结构体、函数或模块的组件的方法。 包和Crate Crate要么是binary(有可执行文件),要么是library(只是库)。Crate root是一个源文件,Rust从那里开始编译,并由此组成了crate的根模块。一个包是一个或多个crate,它必须包含Cargo.toml来指定如何构建crate。 一个包至多只能包含一个library crate,但它可以包含任意个binary crate。 如下创建一个包: $ cargo new my-project Created binary (application) `my-project` package $ ls my-project Cargo.toml src $ ls my-project/src main.rs Cargo.toml没有提到src/main.rs,因为Cargo遵循了这样一个约定:src/main.rs是与包同名的binary crate的根。同样的,如果包目录下src/lib.rs,那这是与包同名的library crate的根。Cargo会将crate root传递给rustc来编译。此外可以在src/bin目录下添加多个源文件,每个源文件都是独立的binary crate。 定义模块来控制作用域和隐私 使用mod关键字定义一个模块,然后指明模块名和花括号,模块中可以有其他模块、结构体、枚举、常量、traits和函数,如下: mod name { mod nested_mod { // ... } } 使用路径引用模块树中的东西 一个路径可以有两种形式: 绝对路径:使用crate名字或者crate字面量,这是从crate根开始寻找的; 相对路径:使用self、super或者当前模块中的标识符,这是从当前模块开始寻找的。 使用super可以引用父模块。对于可能会一起移动的的模块,应该使用相对路径;对于可能会分开移动的模块,应该使用绝对路径。路径采用::将多个名字连接在一起。 默认情况下所有的Rust的东西(函数、方法、结构体、枚举、模块和常量)都是私有的。父模块的东西无法访问子模块的私有东西,但子模块的东西可以访问祖先模块的东西。 通过在东西前面增加pub关键字即可使东西变为公有的,这样就能在外部访问到模块内部的东西。注意同一模块内部的兄弟部分是可以互相访问的。此外结构体成员也默认是私有的,需要pub关键字变为公有的,方法也是如此。如果有字段不是pub的,就无法在外部构造这个对象。而枚举则不一样,如果枚举是pub的,那么它的所有成员都是pub的。 使用use将路径带进作用域中 使用use后面跟路径和;可以将名字带入本作用于,类似文件系统中的符号链接。当使用相对路径,必须在前面加上self::。未来可能不需要加self::。 一般而言,我们将函数的父模块而非函数本身,用use带入作用域。而结构体、枚举等其他东西,我们会指定完整路径。除非有重名的情况。如果两个东西重名,这是个错误。 可以用as关键字重命名: use std::fmt::Result; use std::io::Result as IoResult; use导入的名字默认是私有的。使用pub use可以重新将名字导出。 如果use的根路径是外部的crate名字,即可导入这个crate。std是跟随Rust语言的标准库,无需在Cargo.toml中指定,其他库需要指定。 可以通过以下方式同时导入多个路径: use std::{cmp::Ordering, io}; use std::io::{self, Write}; use std::collections::*; // 通配符 将模块分到多个文件中 使用mod name;可以将名为name.rs文件的内容作为名为name的子模块。如下面的示例: // src/lib.rs mod front_of_house; pub use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); hosting::add_to_waitlist(); hosting::add_to_waitlist(); } // src/front_of_house.rs pub mod hosting; // src/front_of_house/hosting.rs pub fn add_to_waitlist() {} 常见容器 使用向量存储一系列值 向量的类型为Vec<T>。它只能存储同一类型的值。 可以使用Vec::new函数创建向量,使用push方法可以添加元素,也可以使用vec!宏创建,如下: let v1 = Vec::new(); v1.push(5); let v2 = vec![1, 2, 3]; 可以通过&v[index]获取一个引用,也可以通过v.get(index)获得一个Option<&T>值。前者如果访问越界会panic,而后者会返回None。 可以使用for循环遍历元素: let mut v = vec![100, 32, 57]; for i in &v { println!("{}", i); } for i in &mut v { *i += 50; } 使用字符串存储UTF-8编码的文本 Rust中的字符串通常是指两种类型:String和&str,而非单一一种类型,这两个都是用UTF-8编码的。 可以通过以下的方式创建字符串: let mut s = String::new(); let s = "initial contents".to_string(); let hello = String::from("你好"); 通过以下方式追加字符串: let mut s = String::from("foo"); s.push_str("bar"); s.push('l'); push_str和push都不会移交所有权。也可以使用+来连接字符串,如下: let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2; // 注意s1被移动了 +会使用如下的方法(非泛型版),而&String可被强转为&str,因而+的左操作数会被获取所有权,而右操作数不会: fn add(self, s: &str) -> String { /* ... */ } 可以使用format!宏更方便地格式化字符串,它和println!很像: let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = format!("{}-{}-{}", s1, s2, s3); 对字符串的下标索引是个错误,因为它可能会截断一个字符,而且不同的计数方法会有不同结果,此外从头开始计算字符个数会使得下标索引不是常数复杂度。 可以对字符串进行切片操作,但切片边界必须是合法的UTF-8边界,否则会panic。 使用str.chars()可以获得字符,而str.bytes()可以获得UTF-8内部编码。 使用哈希映射存储键值对 类型HashMap<K, V>可以以K类型为键存储V类型。使用new函数可以创建对象,使用insert可以插入,或者使用collect将元组的向量转成哈希映射,如下: use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); // or let teams = vec![String::from("Blue"), String::from("Yellow")]; let initial_scores = vec![10, 50]; let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect(); insert会移交所有权。使用get方法可以根据键获取值,参数为引用,返回的类型为Option<&v>。可以使用如下方式遍历: for (key, value) in &scores { println!("{}: {}", key, value); } insert会覆盖旧值。可以使用Entry搭配or_insert不覆盖旧值地插入新值,如下: let text = "hello world wonderful world"; let mut map = HashMap::new(); for word in text.split_whitespace() { let count = map.entry(word).or_insert(0); *count += 1; } 错误处理 Rust将错误分成两类,一种是可恢复的错误、另一种是不可恢复的错误。 使用panic!处理不可恢复错误 使用panic!宏可以终止程序运行。默认情况下,它会回退栈并释放资源。这会造成更大的可执行文件。将下面两行加入Cargo.toml,可以使panic!直接调用abort: [profile.release] panic = 'abort' panic!如下使用: fn main() { panic!("crash and burn"); } 默认panic!只打印最后的函数栈,使用环境变量RUST_BACKTRACE=1可以完整打印函数调用栈。 使用Result处理可恢复错误 Result枚举的定义如下: enum Result<T, E> { Ok(T), Err(E), } 我们可以使用match表达式匹配错误。一些error对象还有kind方法,可以进一步用match表达式匹配。 对于Result<T, E>类型,还有unwrap方法,如果出错会自动调用panic!。类似地,还有expect函数可以选择出错信息: let f = File::open("hello.txt").unwrap(); let f = File::open("hello.txt").expect("Failed to open hello.txt"); 除了直接处理错误,也可以将错误传递出去。基本方法也是使用match表达式。也可以使用?运算符。?运算符放在Result值的后面,会达到一样的效果。它还会调用from函数,对错误进行类型转换: fn read_username_from_file() -> Result<String, io::Error> { let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s) } 注意?运算符只能在返回Result的函数内调用。main函数也可以返回Result,如下,这里Box<dyn Error>是个trait对象,表示一切错误: use std::error::Error; use std::fs::File; fn main() -> Result<(), Box<dyn Error>> { let f = File::open("hello.txt")?; Ok(()) } 该不该使用panic! 在写示例、原型或者测试不需要良好的错误处理,可以直接调用unwrap。同样地,如果你确信代码不会有异常,也可以用unwrap。 泛型、Trait和生命周期 泛化数据类型 使用类型参数由尖括号括起来,跟在函数名后面就可以定义泛型函数。其中类型参数命名通常较短,由一个字母组成,使用驼峰命名。如下: fn largest<T: PartialOrd + Copy>(list: &[T]) -> T { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest } 类似地,在结构体名中加尖括号括住的类型参数就可以定义泛型结构体,同样地枚举也可以是泛型的: struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } impl Point<f32> { fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() } } 注意对于方法,需要在impl后面声明类型,Rust才能知道Point后面跟的是具体类型还是泛化类型。上面最后一个例子就是具体类型的例子。 Trait:定义共同行为 Trait更像是接口,但有稍微的不同。如下定义trait: pub trait TraitName { fn method(&self) -> ReturnType; fn method_with_default_impl(&self) -> ReturnType { // ... } } impl TraitName for StructOrEnumName { fn method(&self) -> ReturnType { // ... } } Trait实现的一个约束是trait本身和要实现的类型中至少有一个是属于该crate的。 Trait可以有默认实现,如果要采用默认实现,只要impl块不给出实现即可。默认实现可以调用同Trait的其他函数。 可以将trait作为参数或返回值,使用+连接多个trait,如下: pub fn func(item: impl TraitName1 + TraitName2) { // ... } 实际上,这是下面代码trait限制的语法糖: pub fn func<T: TraitName1 + TraitName2>(item: T) { // ... } 此外也可以使用where更清晰地显示: fn some_function<T, U>(t: T, u: U) -> i32 where T: Display + Clone, U: Clone + Debug { // ... } 也可以使用impl TraitName作为返回值类型,但你只能返回一种类型。 用生命周期验证引用 每个引用都有生命周期。一般情况下生命周期和类型一样都会被推断。生命周期注解必须以'开头而后跟很简单的名称如'a,它被放在&之后。下面是一些例子: &i32 &'a i32 &'a mut i32 就像声明泛型类型参数,我们可以在同样的位置声明泛型生命周期参数。像下面这样: fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } 上面的函数使得返回值的生命周期是参数中生命周期较短的那个。注意生命周期参数不会修改参数或返回值的声明周期,它们只是用于是借用检查器拒绝不满足的情况。 在函数内,Rust无需帮助就能检查生命周期,但当函数有到外部或来自外部的引用,这件事就不太可能了,所以我们需要手动注解。 并不是所有的参数都需要注解。当函数返回引用时,它一定会包含某个参数的生命周期(如果不这样,说明这个引用来自于局部变量)。 结构体也可以包含生命周期注解,这种情况下,每个引用都需要有一个类型注解。 Rust提供了3条生命周期省略规则,那些函数或方法的参数的生命周期称为输入生命周期,那些返回值的生命周期称为输出生命周期。如果规则应用完毕后,仍有引用的生命周期未解决就会报错: 所有的引用参数获得它们各自的生命周期参数; 如果只有一个输入生命周期参数,这个生命周期会赋值给所有输出生命周期参数; 如果有多个输入生命周期参数,但其中一个是&self或&mut self,那么self的生命周期会赋值给所有输出生命周期参数。 还有一种特殊的生命周期叫'static,它表示引用在整个程序运行时都有效。 写自动测试 如何写测试 Rust中的测试是一个被注解上test属性的函数。当用Cargo创建一个库项目的时候,一个包含着一个测试函数的测试模块会被生成,就像下方的代码。每个测试都是独立的一个线程。默认情况下一个测试函数panic则代表测试出错,通过加入#[should_panic]注解使测试函数不panic表示出错,它有一个expected参数表示期待的错误内容。 #[cfg(test)] mod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); } } 一般测试模块会使用use super::*;,方便测试,起可见性规则与一般模块一致。 你可以使用assert_eq!、assert_ne!、assert!之类的宏断言。前两者能给出错误更详细的信息,其左右参数位置在Rust中并不重要,它们要求参数实现了PartialEq和Debug traits。这三个函数之后的参数会被传递给format!宏,用于更好地显示错误。 测试函数可以返回Result<T, E>,当返回Err(...)时,测试失败。此时不应该使用#[should_panic]。 控制测试是如何运行的 cargo test --中,--前的参数是cargo的参数,之后的参数是测试程序的参数。测试程序的参数如下: 参数名 含义 --test-threads 并行线程数,设为1避免并行 --show-output 不捕获程序的输出 cargo test可以跟位置参数,就会运行测试名字包含该参数的测试。使用#[ignore]注解可以标注测试函数被忽略,然后使用cargo test -- --ignored可以运行被忽略的测试。 测试的组织 测试一般分为两类,单元测试和集成测试。前者小而精,后者则测试整个库。 使用#[cfg(test)]注解告诉Rust只在cargo test的时候编译代码。由于集成测试不和实际代码在同一目录,因而不需要该注解。 集成测试通常放在tests目录下。每个文件都会被编译成独立的crate。通过cargo test --test integration_test可以只跑特定的集成测试。如果有共用的函数可以放到子目录下,如tests/common/mod.rs 函数式语言特色:迭代器和闭包 闭包 通过|param1, ... | { statements; ... } 定义闭包。如果闭包只是一个表达式,花括号可以省略。闭包在可以推导类型的时候,并不需要像函数那样注解类型,不过也可以注解上类型|param1: type1, ... | -> retType { ... }。注意一个闭包只能有一个实际的类型。每个闭包都有它们独立的类型,即使签名一样类型也不一样。 每个函数至少实现了以下trait中的一个:Fn、FnMut和FnOnce。可以使用trait限制。如下面的代码: struct Caller<T: Fn(u32) -> u32> { func: T, } 使用闭包可以捕获变量。闭包可以通过三种方式捕获变量: FnOnce:消耗了捕获的对象,为了消耗,它获取了所有权,这种闭包只能被调用一次; FnMut:获取了可变引用,这种闭包可以修改变量; Fn:获取了不可变引用。 其中Fn继承自FnMut,FnMut继承自FnOnce。当你创建一个闭包时,Rust根据闭包如何使用环境中的值来确定其类型。 在参数列表前加入move能强制闭包获取所有权。 迭代器 迭代器是惰性遍历元素的。它们实现了这样一个trait。 pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // methods with default implementations elided } 这里Item是关联类型,以后会涉及。当next()返回一个元素,如果迭代完成,返回None。 如果你想获取被迭代对象的所有权,并返回其拥有的对象,可以使用into_iter(),如果想要遍历可变引用,可以使用iter_mut()。 调用next的函数被称为消耗适配器(consuming adaptors),因为它们用完了一个迭代器,如sum()函数。另一些方法定义在Iterator trait上,它们被称为迭代器适配器(iterator adaptor)。它们可以被串联使用,由于迭代器是惰性的,所以需要被消耗适配器使用,才会计算,例如map()函数。 只要实现了next()函数就可以定义自己的迭代器。 关于Cargo和Crates.io 自定义构建 通过cargo build可以运行dev profile,通过cargo build --release可以运行release profile。 通过在Cargo.toml中加入[profile.dev]或[profile.build],可以配置选项。其中一个是opt-level,即优化程度,默认如下: [profile.dev] opt-level = 0 [profile.release] opt-level = 3 发布Crate到Crates.io 使用///可以插入文档注释,文档注释支持Markdown格式。通过cargo doc构建文档,会输出到target/doc目录下。加上--open参数会打开文档。 一般而言文档注释中有以下大家都会用的小节: # Examples:示例; # Panics:哪些情况函数会panic; # Errors:如果一个函数返回Result,描述哪些错误会发生,以及在什么情况下发生; # Safety:如果函数是unsafe的,描述原因以及调用者应当遵循的约定。 文档中的代码块也会成为测试。 使用//!注释会对包含它的东西进行注释,而非对它之后紧随的进行注释。这通常用于根crate。 在crates.io注册并且获取API token,可以使用cargo login登录。发布前,Cargo.toml中的description和license是必须的。使用cargo publish就可以发布。通过cargo yank --veres 1.0.1可以阻止这个版本被使用,再加上--undo可以撤销。 Cargo工作区 Cargo使用工作区来控制多个相关的包,它们共享Cargo.lock和输出目录。以下方的Cargo.toml为根,再通过cargo new adder等做法就可以创建包含多个包的工作区。 [workspace] members = [ "adder", "add-one", "add-two", ] 通过添加下面的代码到adder/Cargo.toml,可以添加依赖: [dependencies] add-one = { path = "../add-one" } 从Crates.io上安装二进制包 使用cargo install即可。 用自定义命令扩展Cargo 如果PATH路径下有cargo-something,那么可以通过cargo something来运行它。通过cargo --list可以列出安装的命令。 智能指针 智能指针通常使用结构体完成,它们实现了Deref和Drop traits。 使用Box<T>指向堆上的数据 Box不提供性能以及额外的功能,你可能会在以下情况下使用它们: 使用编译期大小未知的类型; 确保大量数据不会因为改变所有权被复制; 但你想拥有一个值,只关心它的某个trait而不是具体类型。 使用Box::new()可以将对象放置在堆上。 此外递归类型也需要用到box。 Deref Trait 普通的引用也是一种指针,你有时需要解引用*p来获取内容。Box也是类似的。通过下面的代码可以在栈上创建一个盒子对象: struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } 这时候,*y会被替换成*(y.deref()),其中的*是普通解引用,不会递归替换。因而deref()函数需要返回引用。 解引用强迫(Deref coercion)会把那些实现了Deref trait的类型转换为对应的类型。如&String会被转换为&str类型。解引用强迫会在你传递一个类型不一致的引用给函数时发生,而且可以发生多次。就像下面演示的那样。 fn hello(name: &str) { println!("Hello, {}!", name); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&m); } 使用DerefMut trait可以定义可变对象解引用的效果。发生下面三种情况,会有解引用强迫: 从&T到&U,当T: Deref<Target=U>时; 从&mut T到&mut U,当T: DerefMut<Target=U>时; 从&mut T到&U,当T: Deref<Target=U>时。 Drop Trait Drop trait有fn drop(&mut self)方法,实现后就可以自定义清理行为。变量以创造时相反的顺序被丢弃。 使用std::mem::drop函数可以丢弃值,不能直接调用变量的drop()方法。 Rc<T>引用计数智能指针 Rc<T>不能够在多线程中使用。需要use std::rc::Rc;来引入Rc。通过Rc::new()可以创建引用计数的变量,通过Rc::clone()可以复制变量,使引用计数增加。通过Rc::strong_count()可以获得引用计数的个数。Rc<T>只能拥有不可变的引用。 RefCell<T>和内部可变模式 内部可变性使得你能通过不可变引用修改数据。这在底层使用了unsafe。使用RefCell<T>,引用规则会在运行时检查。打破引用规则会造成程序panic。RefCell<T>同样不能够在多线程中使用。 可以通过RefCell::new()创建。再通过.borrow_mut()可以获取可变引用和.borrow()获取不可变引用。前者返回RefMut<T>类型,后者返回Ref<T>类型,它们都实现了Deref trait。通过这两种智能指针,RefCell<T>实现了计数。 结合Rc<T>和RefCell<T>,也就是,Rc<RefCell<T>>,就可以有多个所有权的可变数据。 循环引用会造成内存泄露 使用Weak<T>可以避免出现循环引用,使用.upgrade()方法会返回Option<Rc<T>>,使用Rc::downgrade()函数可以降级引用。通过Rc::weak_count()可以获得弱引用计数的个数。 无惧并发 使用线程 使用thread::spawn函数并传入闭包就可以启动一个线程。主线程结束后新线程也会结束。它会返回一个类型为JoinHandle的句柄,可以调用其.join()方法等待线程执行完毕。使用thread::sleep可以睡眠一定时间。 如果传入spawn的闭包使用了外部的变量。由于无法确认闭包运行的时间和外部变量的生命周期谁更长,所以会报错。通过加入move关键字可以解决这个问题。 使用消息传递来在线程间传输数据 信道(channel)是Rust用于消息传递的重要设施。一个信道由两个部分组成:发送者和接受者。前者传递数据给后者。如果它们中的一个drop了,那么这个信道就认为是关闭了。 使用std::sync::mpsc::channel()创建一个信道,其中mpsc是multiple producer, single consumer的简称。它会返回一个元组,即发送者和接受者。就像下面代码展示的那样。 use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from("hi"); tx.send(val).unwrap(); }); let received = rx.recv().unwrap(); println!("Got: {}", received); } 如果tx.send()方法返回一个Result<(), E>,如果接收端关闭会报错。如果没有值发送,rx.recv()会阻塞住线程,其返回值实Result<T, E>类型,如果发送端关闭会报错。rx.try_recv()不会报错,它在没有值以及关闭的时候会返回错误。此外rx也是个迭代器,可以迭代输入的值,直到信道关闭。使用mpsc::Sender::clone()可以复制一个发送者。 共享状态的并发 为了访问mutex的数据,一个线程必须获得mutex的锁,使用完数据后,必须解锁,以使其他线程使用数据。 use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); } 再访问数据前,我们使用lock()函数获取锁,这可能造成阻塞。如果获得锁的线程panic了,那么其他线程使用lock()函数会返回错误。获得锁之后就可以以可变引用的方式使用它。 实际上,lock返回了一个MutexGuard的智能指针,它实现了Deref trait,它也实现了Drop trait来自动地解锁。 Arc<T>和Rc<T>是类似的,只是它具有原子性,但会有性能损失。从某种角度来说Mutex<T>和RefCell<T>很像,它提供了内在可变性。 使用Sync和Send扩展并发 Send trait能够移交所有权到另一个线程中,几乎所有的类型都实现了Send trait,除了Rc<T>。如果一个类型是由Send trait组成的,那么它也实现了Send trait。 Sync trait能让数据的引用在多个线程中使用。也就是说T是Sync当且仅当&T是Send。初等类型都是Sync的,哪些由Sync组成的类型也是Sync的。 Rc<T>不是Sync。一般而言,具有内部可变性且非线程安全的类型不是Sync,RefCell<T>以及其他的Cell<T>也因此不是Sync。 Send和Sync是自动的,它们作为标记trait也没有方法需要实现。手动实现这两个类型是不安全的。 Rust的面向对象特性 面向对象语言有以下特色: 对象包括数据和方法; 通过封装隐藏实现细节; 以继承作为类型系统和代码复用,这其中包括了多态。 可以使用trait对象完成多态。一个trait对象指向一个实现了该trait的实例和一个用于查找trait方法的表格。我们需要使用指针创建动态对象,如&引用和Box<T>智能指针,而后使用dyn关键字,最后跟我们要的trait。就像下面的代码那样: pub trait Draw { fn draw(&self); } pub struct Screen { pub components: Vec<Box<dyn Draw>>, } Trait对象使用动态分发,这区别于所有方法调用再编译器确定的静态分发。这会带来一些性能损失。 只有对象安全的trait才能成为trait对象,它的所有方法需要遵循两条原则: 返回类型不是Self; 没有泛型参数。 模式和匹配 一个模式包含以下东西: 字面量; 解构的数组、枚举、结构体和元组; 变量; 通配符; 占位符。 模式匹配可以使用到的地方 模式匹配可以出现在: match分支; if let表达式; while let循环; for循环; let语句; 函数参数。 模式匹配是否永真 一个永远能匹配的模式是永真的如let x = 5;,而有些可能不能匹配,称为不是永真的,如if let Some(x) = a_value。函数参数,let语句和for语句只能接受永真模式。 模式语法 模式中的命名变量是永真模式。可以使用|匹配多个模式。可以使用..=匹配一个两端包含的范围,如1..=5。此外支持各种解构,可以使用_忽略一些值,或者使用_开头的变量表示不使用的变量。可以使用..忽略剩余的部分(包括结构体、元组)。可以在Match守卫上增加if的条件,但其优先级较|低。可以使用@绑定正在测试的某个值,如id_variable @ 3..=7。 高级特性 不安全Rust 使用unsafe关键字开启一个块,就可以使用不安全超能力,包括: 解引用裸指针; 调用不安全的函数和方法; 访问或修改一个可变静态变量; 实现一个不安全的trait; 访问union。 但这不包括关闭借用检查和其他检查。 裸指针可以是不可变的或者可变的,分别用*const T和*mut T表示。不同于引用和智能指针,裸指针可以: 同时拥有不可变和可变指针指向同一个区域; 指向的内存可以不合法; 可以为空; 并不自动清理。 let mut num = 5; let r1 = &num as *const i32; let r2 = &mut num as *mut i32; let address = 0x012345usize; let r = address as *const i32; unsafe { println!("r1 is: {}", *r1); println!("r2 is: {}", *r2); } 向上面的方式可以创建裸指针,这不需要unsafe。 在函数前加上unsafe可以创建不安全函数。你必须在unsafe块中调用它。 unsafe fn dangerous() {} unsafe { dangerous(); } 使用extern可以指定外部的函数,如外部C函数: extern "C" { fn abs(input: i32) -> i32; } 也可以以某种方式导出Rust函数成别的语言的,这里就不是使用extern块: #[no_mangle] pub extern "C" fn call_from_c() { println!("Just called a Rust function from C!"); } Rust中用静态变量指代全局变量。静态变量和常量很像,但实际上常量允许内容不在同一个内存区域。静态变量还可以是可变的,但这时候访问或修改变量就是不安全的。 static mut COUNTER: u32 = 0; fn add_to_count(inc: u32) { unsafe { COUNTER += inc; } } fn main() { add_to_count(3); unsafe { println!("COUNTER: {}", COUNTER); } } 如果一个trait的某个方法不能被编译器验证其不变性,那么就是一个不安全的trait。实现这个trait也需要加上unsafe。比如Sync和Send就是。 unsafe trait Foo { // methods go here } unsafe impl Foo for i32 { // method implementations go here } 高级Trait 通过在trait中加入形如type Item;的部分就可以制定一个关联类型。trait中的方法可以使用这个类型。而trait的实现必须指定这个具体的类型。关联类型和泛型很像,但是前者只能对某个类型实现一次trait,而后者可以针对类型实现多个不同泛型参数的同种trait。 Rust支持默认的泛型参数,形如<PlaceholderType=ConcreteType>。这在运算符重载中用到。Rust不允许创建运算符。你可以通过实现std::ops下的trait完成运算符重载。如加法trait如下: trait Add<RHS=Self> { type Output; fn add(self, rhs: RHS) -> Self::Output; } Trait之间以及类型自带的方法都是可以重名的。默认情况下,编译器调用类型自带的方法。但有时可以显示调用某个方法,如Trait::method(&instance)。对于没有self参数,需要才用完全限定语法<Type as Trait>::function(receiver_if_method, next_arg, ...);。 可以使用trait ChildTrait: SuperTrait { ... },指定某个trait需要另一个trait。 使用newtype模式,可以绕过实现trait必须和trait或类型在同一crate的限制。如下: use std::fmt; struct Wrapper(Vec<String>); impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } } fn main() { let w = Wrapper(vec![String::from("hello"), String::from("world")]); println!("w = {}", w); } 此外Wrapper还可以实现Deref trait。 高级类型 通过type Type1 = Type2;可以创建类型别名。但这失去了类型检查。它也可以带有泛型参数。 此外Rust有个特殊的类型!表使用不返回。continue就有!类型。 str就是一种动态大小类型(DST)。Rust拥有Sized trait表示类型是否编译期知道大小。这个Trait是自动实现的。同时Rust默认对每个泛型函数的类型参数都加入了Sized。你可以通过fn generic<T: ?Sized>(t: &T) { ... }来取消这种行为,这样T可能是也可能不是Sized。 高级函数和闭包 通过fn(type1, ...) -> retType,可以定义函数指针类型。普通函数的名字就是函数指针值。fn是类型而非trait。函数指针实现了Fn、FnMut和FnOnce。实际上元组结构体和元组结构体枚举的构造器就是一个函数。 如果你要返回闭包,可以才用Box<dyn Fn(type1, ...) -> retType>的形式。 宏 Rust的宏指使用macro_rules!的声明性宏和3种过程宏: 自定义#[derive]宏; 类似属性的宏; 类似函数的宏。 宏会被展开成为更多的代码。宏能有不同数目和类型的参数。宏由于是用代码产生代码,因而更难读懂和维护。宏必须在作用域内才能使用。 声明性宏,如vec!宏: #[macro_export] macro_rules! vec { ( $( $x:expr ),* ) => { { let mut temp_vec = Vec::new(); $( temp_vec.push($x); )* temp_vec } }; } #[macro_export]表示当这个宏可以被模块外导入。vec!内部的结构类似match表达式。完整的匹配格式见Macros By Example - The Rust Reference 过程宏以代码作为输入,输出代码。当创建宏的时候,这个代码必须位于自己的crate,并且有一个特殊的crate类型。如下: use proc_macro; #[some_attribute] pub fn some_name(input: TokenStream) -> TokenStream { } 这里定义了一个some_attribute的过程宏,它们以TokenStream作为输入输出。 一个为了自定义derive的crate需要在创建时的Cargo.toml中加入: [lib] proc-macro = true 然后内容如下: extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; use syn; #[proc_macro_derive(HelloMacro)] pub fn hello_macro_derive(input: TokenStream) -> TokenStream { // Construct a representation of Rust code as a syntax tree // that we can manipulate let ast = syn::parse(input).unwrap(); // Build the trait implementation impl_hello_macro(&ast) } fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream { let name = &ast.ident; let gen = quote! { impl HelloMacro for #name { fn hello_macro() { println!("Hello, Macro! My name is {}!", stringify!(#name)); } } }; gen.into() } 属性过程宏的使用如下: #[route(GET, "/")] fn index() {} 定义方法如下: #[proc_macro_attribute] pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { ... } 函数的过程宏使用如下: let sql = sql!(SELECT * FROM posts WHERE id=1); 定义方式如下: #[proc_macro] pub fn sql(input: TokenStream) -> TokenStream { ... }

2019/9/8
articleCard.readMore

微软实习总结

原文由$\LaTeX$编写,点击此处下载报告原文,点击此处下载幻灯片。 实习任务 我们在微软STCA实习的目标是代码搜索,也就是用自然语言查询代码。具体来说,它包含以下几个步骤:1) 收集Python和JavaScript(下简称py和js)的代码库,2) 建立代码搜索的模型(又分为数据预处理、编写模型、训练调参这3个步骤),3) 提供Web服务端以查询py和js的代码,4)编写VS Code插件搜索代码。我们起初也根据搜索语言的不同分组,即py和js两组。但实际上这两个语言的差别仅仅是数据处理,模型是一样的,因而我们两个组是混在一起工作、互相协作的。上述的第4个任务由李超老师完成,而前3个任务就交给了我们。 我们选出冯昊作为我们两个组的共同组长,由他分配任务。我被派的任务是复现CODEnn模型,其他人则负责复现Baseline和Code2Vec模型。而第个任务,即收集代码库也交给了其他人,这是实习第一周完成的任务,而我第一周主要在阅读论文。而后的几乎所有时间几乎都花在了第2个任务,这也是我们这次实习的重点。第3个任务被派给了谭新宇,我和他商量将搜索的请求转发给我,我再返回结果。第3个任务的工作量不大,是在最后一周花费几小时完成的。 除了分配的任务之外,我对代码还有更高的要求,希望代码能更具扩展性,进而实现一个很方便地处理数据、训练评测模型的框架。 完成情况 数据预处理 每个数据集(即代码库)大致会经历提取代码片段、清洗、切分和转化为输入格式这4个阶段。不同的模型的数据预处理可能只有转化阶段不同,不同数据集的数据预处理又可能只有提取阶段不同,有些数据预处理还可能跳过阶段,这种数据预处理的交互错杂使得维护清晰的数据处理层次变得相对困难。 为了维护清晰的数据处理层次,我经历了1次大重构。最开始我将数据预处理的不同阶段放在不同的目录里,用Makefile自动处理数据。很快地,由于数据集日益复杂,Makefile就很冗长被弃用,进而导致数据处理混乱,不知道上阶段和下阶段数据的位置。此外各个服务器之间同步数据集也是个问题。之后,我重构了整个系统,改用自己写的脚本替代Makefile来批量处理数据,它还可根据参数自动下载处理过或未处理的数据集、根据需要运行依赖。数据集也只分配到了3个目录下面:原始数据集、预处理的、最终用于训练的数据集。数据集的命名也很规范,可以体现上下游阶段的数据集。 编写模型 CODEnn相较于BaseLine是一个简单模型,它只需要单步训练。其原理大致是将代码特征映射到一个向量,再将描述文字也映射到一个向量,将其cos距离作为loss训练。对于代码特征,原论文提取了函数名、调用API序列和token集;对于描述文字,通常选取docstring(Python)或函数上方或内部注释(JavaScript)。对于函数名、token集,会按照驼峰命名和下划线命名进一步划分成更小的词法单元,而API序列则保留不再分割。对于token集,不同于其他3种数据(方法名、API序列和描述),我们认为它是无序的(BOW,bag of words),因而进行了去重。所有的这些词素,对于有序的会使用RNN或其变种处理,再将RNN每一个词的输出进行池化;对于无序的,会用MLP(多层感知机,但是论文作者其实只用了单层)处理再进行池化。所有的代码特征池化得到的特征向量再经过一层全连层,使其维度与描述向量的维度一致,最后以cos距离作为loss。为了便于batch处理这些变长的数据,这些数据会被截断或者填充到某一个长度,截断截尾,填充填后。 我重新实现了CODEnn的代码,从而更好地支持了多卡训练、断点、可视化,并适应了我们的数据集。 训练调参 CODEnn有很多超参可以调,如RNN的选取LSTM还是GRU(原作选取LSTM),池化的选取Max还是Average(原作选取Max)、激活的选取RELU还是Tanh(原作选取Tanh)、双向还是单向RNN(原作选取双向)、各种变长数据截断或填充的最后长度、vocabulary大小、embedding大小、representing大小。 由于大数据集上的调参需要较大的时间和资源,我相信在小数据集上表现较好的模型在大数据集上也会表现得很好。所以我选用了GitHub上的Numpy代码作为测试数据集。下图是各个调参后训练的结果,这里有两条蓝线,起初在下方后来在上方的是无bidirectional,另一条是原版,improved是指使用了mean池化、GRU和RELU激活,横坐标是epoch。可以看出使用mean池化、GRU、RELU激活都能对模型有所帮助,而bidirectional很难说有没有帮助。 接着,我们想知道Vocabulary的大小会不会影响模型性能,但小数据集本来就没多少Vocabuary,所以我们就在大数据集上测试,测试结果如下图,这里10000vocabulary的是分成2段训练的,分别是蓝色和绿色,其他则是20000vocabulary的,分成4段,横坐标是epoch。可以看出Vocabulary的选取没有太大的影响,这说明出现频率很低的那些词确实不那么重要。后来为了尝试模型的性能是否会有最高点,我又继续训练20000vocabulary,确实发现模型的性能存在最高点,这提示我们需要加入Regularization方法或者提前终止训练。我们选择800epoch的模型作为最终模型。 下表是该模型的性能。就acc@top k的性能指标而言,CODEnn性能远远优于Baseline和Code2Vec以及后来周展平实现的CodeNet。 dataset acc@top 5 acc@top 10 validation set (pool=200) 0.806875 0.884375 validation set (pool=800) 0.621563 0.726250 test set (pool=200) 0.780667 0.860333 test set (pool=800) 0.596250 0.711250 收获 深度学习 最大的收获可以说是深度学习领域的技能得到很大的长进,主要是2个方面:1) Pytorch的编程能力,2) NLP领域的理论能力。 在参加微软实习的一年多以前,我在龙明盛手下做过一会儿的活,主要是研究迁移学习和哈希(也就是embedding),使用过PyTorch和Tensorflow。由于很长时间没有再在深度学习领域写代码,我已经将PyTorch和Tensorflow忘的一干二净了,而且它们的API也发生了一些改动。另外,由于自己一直在迁移学习和哈希的领域研究,很多其他的神经网络模型并没有接触过,比如循环神经网络,以及在这之上做的Seq2Seq模型等等。这一切就导致了我在实习前,上面提到的两个能力是极度匮乏的。 这次实习,我又重温了PyTorch,而且也发现了原来设计的一些API发生了改变,比如Variable这个类被deprecated了、出现了DataParallel这个新的类。当然这些改变也是喜闻乐见的,它们使得PyTorch的API看起来更加优美。 此外,我用PyTorch复现了CODEnn这个模型,它让我知道了RNN及其变体LSTM、GRU的实现方式和工作原理。为了复现Baseline,我还写了个Code Summarizer的模型,这个模型将输入的代码token流转化为总结的文本流,从中我学习了Seq2Seq模型以及带attention的变种的原理及实现方法。总而言之,我的NLP领域的知识和实践能力都得到了很大的长进。 团队合作 微软的团队管理还是很先进的,甚至让刚实习的我有些惊叹。在这之前,我们只有在软件工程的课堂上,而这是我们第一次实践这些。主要有3个方面:1) 冲刺管理,2) PAI分配(GPU服务器),3) Teams协同。 我们会有每日的站立会议(实际上是坐着的)来汇报完成的工作和将要完成的工作。我们经历了1次冲刺,在这1次冲刺里,有很多的任务,任务再分配到每个人。通过这种方式我们分配任务并且管理进度。此外,通过各种图表(如燃尽图),我们可以查看到当前的进度。 还有PAI的GPU管理也是很先进的,虽然我觉得有些不便利,因为之前的我都是在实验室里想用多少GPU就和其他人商谈好,而后占用。但考虑到要多人使用,一些任务需要等待,那就需要个很好的调度系统,PAI就是为此而生的。但我也觉得它有些不便利,比如很难和提交的任务交互、无法在运行时改变GPU数目和无法改变端口映射(这是docker限制的)。总的而言,PAI还是不错的。当然最后也要感谢微软给我们提供了充足的运算资源。 Teams也是一个很方便的软件。我之前在Pony.ai实习使用的是slack。个人觉得这两个软件在功能上相仿,而且也处于同样的生态地位。在Teams上交流相比微信便利很多,降低了沟通的瓶颈。 感谢体会 首先我要谢谢我的各位导师们,包括但不限于李超、林武桃,它们给予了我很大的帮助。然后我要感谢我的同学们,感谢他们与我协作完成了这个项目。接着我要感谢我在微软的同写字间的其他两个室友,他们为我解答了很多困惑。最后我要感谢微软和学校,微软为我们提供了很多很好的设备,而学校则为我们提供了这么好的一次机会。 除了上面说的这些收获,我也有遗憾,那就是时间太过仓促。我还想尝试很多其他模型,并将它们集成进我的仓库。但时间不允许。总的而言微软的实习是很有意义的它让我学到了很多。

2019/9/8
articleCard.readMore

数据库小抄

这是我的数据库期末小抄,是对老师课件的总结。原文由$\LaTeX$编写,可点击此处下载。 形式关系查询语言 关系代数:选择$\sigma$,投影$\Pi$,并$\cup$,差$-$,笛卡尔积$\times$,更名$\rho$。推广的运算:交$r\cap s=r-(r-s)$,自然连接$r\bowtie s=\Pi_{R\cup S}(\sigma_{r.A_1=s.A_1\land\cdots\land r.A_n=s.A_n}(r\times s))$,除法$r\div s=\Pi_{R-S}(r)-\Pi_{R-S}((\Pi_{R-S}(r)\times s)-\Pi_{R-S,S}(r))$,赋值$\leftarrow$。扩展的关系代数:泛化投影(投影的参数是函数或运算)、聚集函数(有avg、min、max、sum和count,$_{G_1,\cdots,G_n}g_{F_1(A_1),\cdots,F_m(A_m)}$)、外连接。谓词元组关系演算:${t|P(t)}$,由以下组成,1) 属性和常量,2) 比较运算符,3) 逻辑运算符,4) 量词。域关系演算:${\langle x_1,\cdots,x_n\rangle|P(x_1,\cdots,x_n)}$。 数据库设计和E-R图 E-R组成:实体集(实体是对象,矩形)、关系集(可以是多个实体之间,个数称为degree,菱形)、属性(可以是关系的,这时候是虚线)。属性分类:简单和组合(缩进表示),单值和多值(花括号括住),派生(末尾一对括号)。映射基数限制:一对一,一对多,多对一,多对多(箭头指向实体集代表一,无箭头代表多,可以用$a..b$来代表更复杂基数限制,其中$b$也可以是$*$代表没有限制)。超键:可以决定其他属性的一组属性。候选键:最小的超键。主键:挑选出一个候选键(下划线属性)。全参与与部分参与:全参与(双线)是所有实体有至少一个关系,部分参与(单线)是存在实体没有关系。冗余属性:对于一些出现在两个实体集像外键的属性,在ER图需要移除。弱实体集:(双矩形,区分属性下划虚线,关系双菱形)所有属性不足以形成主键的实体,依赖于(被own)强实体(identifying entity),强实体与弱实体的关系(identifying relationship)只能是一对一或一对多且弱实体是全参与。角色:对于实体集多次参与同一关系集,每次参与都有个角色(线上文字)。E-R图转数据库schema:强实体集转化为所有属性组成schema;弱实体集还带上强实体集的主键;多对多关系转化包含两者主键;一对多或多对一如果多的一边是全参与则在多的一边添加一的一边的主键,如果不是全参与则使用null值;一对一两边都可以当做多的一边处理;弱实体集关系是冗余的;组合属性扁平化;多值属性则用单独表示包含主键和多值属性。多元关系:为了避免困惑,只有一个出箭头是允许的,可以转化为二元关系。特化和泛化:重合(箭头直接指向基础实体)、分离(箭头汇合后指向基础实体);两种转换方法:1) 派生实体包含基础实体的主键,2) 派生实体直接包含基础实体;完整性约束:全/部分(基础实体是否必须是派生实体)。 关系数据库设计 第一范式:属性的域都是原子的(不可分割)。函数依赖:$\alpha\to\beta, \alpha\subseteq R\land\beta\subseteq R$,如果$\forall t_1, t_2 \in r, t_1[\alpha]=t_2[\alpha]\to t_1[\beta]=t_2[\beta]$。超键:$K$是超键$\Leftrightarrow K\to R$。候选键:$K$是候选键$\Leftrightarrow K\to R\land\neg\exists\alpha(\alpha\subseteq K\land\alpha\to R)$。平凡的函数依赖:$\beta\subseteq\alpha\Leftrightarrow\alpha\to\beta$是平凡的。函数依赖集合的闭包:令$F$是函数依赖的集合,所有$F$隐含的函数依赖集合是$F$的闭包,记作$F^+$,一定有$F\subseteq F^+$。BCNF范式:$\forall \alpha\to\beta\in F^+, \alpha\to\beta$是平凡的$(\beta\subseteq\alpha)$或$\alpha$是超键$(\alpha\to R)$。分解为BCNF范式:对于违反BCNF范式的函数依赖$\alpha\to\beta$,分解为$\alpha\cup\beta$和$R-(\beta-\alpha)$,分解有时候不保留依赖关系。第三范式:$\forall \alpha\to\beta\in F^+, \alpha\to\beta$是平凡的或$\alpha$是超键或$\forall A\in\beta-\alpha, A$被某个候选键包含。符合BCNF范式一定符合第三范式,第三范式保留依赖关系。求解函数依赖闭包:反复应用下面3条法则,即可求出闭包:1) (自反性)$\beta\subseteq\alpha\Rightarrow\alpha\to\beta$;2) (提升性)$\alpha\to\beta\Rightarrow\gamma\alpha\to\gamma\beta$;3) (传递性)$\alpha\to\beta\land\beta\to\gamma\Rightarrow\alpha\to\gamma$。闭包额外的性质:1) (联合)$\alpha\to\beta\land\alpha\to\gamma\Rightarrow\alpha\to\beta\gamma$;2) (分解)$\alpha\to\beta\gamma\Rightarrow\alpha\to\beta\land\alpha\to\gamma$;3) (伪传递)$\alpha\to\beta\land\gamma\beta\to\delta\Rightarrow\alpha\gamma\to\delta$。属性的闭包:在函数依赖$F$下,能够被属性集$\alpha$决定的属性集$\alpha^+$。属性闭包的应用:1) 测试超键$\alpha$,$\alpha^+=R$;2) 测试函数依赖$\alpha\to\beta$,$\beta\subseteq\alpha^+$;3) 计算函数依赖$F$的闭包,对于每个$\gamma\subseteq R$,计算属性闭包$\gamma^+$,然后对于每个$S\subseteq\gamma^+$,输出依赖$\gamma\to S$。Canonical覆盖:最小的函数依赖集合。无关属性:对于函数依赖集$F$中的$\alpha\to\beta$,1) $A$是$\alpha$中无关的属性,如果$F$逻辑上蕴含$(F-{\alpha\to\beta})\cup{(\alpha-A)\to\beta}$;2) $A$是$\beta$中无关的属性,如果$(F-{\alpha\to\beta})\cup{\alpha\to(\beta-A)}$逻辑上蕴含$F$。测试属性是否是无关的:对于函数依赖集$F$中的$\alpha\to\beta$,1) 测试$A\in\alpha$,用$F$计算$({\alpha}-A)^+$,如果它包含$\beta$,则$A$是多余的;2) 测试$A\in\beta$,用$F’=(F-{\alpha\to\beta})\cup{\alpha\to(\beta-A)}$计算$\alpha^+$,如果包含$A$,则$A$是多余的。计算Canonical覆盖:先使用联合规则合并函数依赖,在测试属性是否多余,循环往复。无损分解:将$R$分解为$R_1$和$R_2$,如果$R_1\cap R_2\to R_1$或$R_1\cap R_2\to R_2$则为无损分解。依赖保留:$F_i$为各个分解的函数依赖集,如果$(F_1\cup F_2\cup\cdots\cup F_n)^+=F^+$,则分解为依赖保留的。测试BCNF分解算法:需要用到分解前的$F^+$中相关的部分。第三范式分解:首先计算Canonical覆盖$F_c$,对于$F_C$中每个$\alpha\to\beta$的函数依赖,将$\alpha\beta$添加进分解中,如果没有一个分解包含候选键,则随便添加一个候选键到分解里,最后将那些被包含在其他分解里的分解移除。 存储和文件结构 架构层数:两层(直接操纵数据)、三层(通过服务端操纵数据)。存储分类:volatile(停电丢失数据)、non-volatile。存储层级:Cache缓存、内存、闪存(读快写慢擦除更慢,如SSD)、磁盘、光盘、磁带,分为一级(缓存、内存)、二级(闪存、磁盘),三级(光盘、磁带)。磁盘:柱面(多盘面的某一磁道组成)、盘面、磁道、扇区;网络连接方式有SAN(Storage Area Networks)和NAS(Network Attached Storage)。磁盘的性能测量:访问时间(寻道时间+旋转延时),数据传输速率,MTTF(Mean time to failure)。优化性能:块(多个连续的扇区),电梯算法,文件组织(去碎片),non-volatile缓存,Log disk,日志文件系统。存储访问:数据库文件被组织成固定长度的块,Buffer缓存用于存储磁盘块的副本。缓存置换策略:LRU策略(Least recently used)。文件组织:固定长度记录的删除,1) 将之后的记录往前移;2) 将最后的移到最前;3) 维护空闲列表,将它加入空闲列表。变长记录使用槽页结构,其头部包含了记录数目,空闲的末尾,每条记录的地址和大小。文件中记录的组织:堆,序列,哈希。序列文件组织:删除使用指针链;插入时如果有空闲则插入到空闲,否则插入到溢出块,最后更新指针链。 索引和哈希 搜索键:用于查找的键。索引文件:存放搜索键、指针二元组的文件,有顺序和哈希两种。有序索引:主索引搜索键的顺序决定了索引的顺序,又称聚集索引;反之称之为二级索引或非聚集索引;密集索引每一个搜索键都有索引;反之称之为稀疏索引。二级索引:索引指向桶,桶再指向记录。多级索引:如果主键索引不能放到内存,就将索引当做记录,对它创建稀疏索引。删除记录的索引更新:密集索引直接删除对应索引;稀疏索引将下一个搜索键作为索引,如果该搜索键已经有索引,则删除索引。插入记录的索引更新:密集索引直接插入索引;稀疏索引寻找对应位置插入。$B^+$树性质:根节点到叶子节点的路径等长;非根非叶的节点有$\lceil n/2\rceil$到$n$个子节点;叶节点有$\lceil(n-1)/2\rceil$到$n-1$个节点;如果根不是叶,则至少有$2$个子节点;如果根是叶,它有$0$到$n-1$个值;$K$个搜索键的高度不超过$\lceil\log_{\lceil n/2\rceil}(K)\rceil$。$B^+$树节点结构:每个节点有$n-1$个有序的搜索键$K_i$和$n$个指针$P_i$;对于非叶节点指针指向孩子;对于叶节点指针指向记录或桶,最后一个指针指向下一个叶节点,$P_i<K_i,P_{i+1}\geq K_i$。重复搜索键:$K_1\leq K_2\leq\cdots\leq K_{n-1}, P_i\leq K_i$。$B^+$树的插入:如果查找键出现在了叶节点里,则添加入桶,否则将查找键和指针插入叶节点中,如果此时空间不够,则分裂节点;分裂叶节点时左边留$\lceil n/2\rceil$个节点,剩余的留在右边,再对父节点插入元组,如果父节点满再将分裂传递下去;分裂非叶节点时,左边留$P_1,K_1\cdots K_{\lceil n/2\rceil-1},P_{\lceil n/2\rceil}$,右边留$P_{\lceil n/2\rceil+1},K_{\lceil n/2\rceil+1}\cdots K_n,P_{n+1}$,再将$(K_{\lceil n/2\rceil},\text{新节点})$插入到父节点。$B^+$树的删除:合并兄弟节点,并删除父节点到删除节点的搜索键和指针。静态哈希:哈希函数映射到桶,桶包含多条记录;如果溢出,存放在溢出桶里。哈希索引:采用哈希的索引,一定是二级索引。动态哈希:桶地址表规模为$2^i$,初始$i=0$,每个桶对应于一个$i$;插入时如果桶满则分裂,对于桶$j$,如果$i>i_j$则插入一个新桶,如果$i=i_j$则重新计算地址表;删除时,如果桶空,则合并。位图索引:应用于取值很少的属性,是位的数组。 查询处理 查询时间开销:A1 线性搜索$b_r\text{(块数目)}t_T\text{(传输时间)}+t_S\text{(寻道时间)}$;A2 $B^+$树主索引,判相等,搜索键$(h_i\text{(索引高度)}+1)\times(t_T+t_S)$;A3 $B^+$树主索引,判相等,非搜索键$h_i\times(t_T+t_S)+t_S+b\text{(搜索键对应的块数)}\times t_T$;A4 $B^+$树二级索引,判相等,搜索键,同A1;A4 $B^+$树二级索引,判相等,非搜索键$(h_i+n\text{(记录数)})\times(t_T+t_S)$;A5 $B^+$树主索引,比较$B^+$,同A3;A6 $B^+$树二级索引,比较,同A4非搜索键;A7 利用1个索引合取选择;A8 使用组合索引合取选择;A9 通过标识符的交实现合取选择;A10 通过标识符的并实现合取选择。排序操作:内存中可使用快排;否则使用外部排序(归并排序),令$M$是内存块的个数,$b_b$是每次归并读取的块数,其磁盘块传输总数$b_r(2\lceil\log_{M-1}(b_r/M)\rceil+1)$,寻道总数为$2\lceil b_r/M\rceil+\lceil b_r/b_b\rceil(2\lceil\log_{\lfloor M/b_b\rfloor-1}(b_r/M)\rceil-1)$。连接操作:嵌套循环,块传输总数$n_r\times b_s+b_r$,寻道总数$n_r+b_r$;嵌套块循环,块传输总数$b_r\times b_s+b_r$,寻道总数$2b_r$;索引嵌套循环,开销为$b_r(t_T+t_S)+n_r\times c\text{(找到r中对应元素的开销)}$;合并连接,先对两个关系排序,再连接,块传输总数$b_r+b_s$,寻道总数$\lceil b_r/b_b\rceil+\lceil b_s/b_b\rceil$;哈希连接,先哈希,再对每一个哈希的块连接,如果不能全部加载入内存会有递归划分,不考虑递归划分,块传输总数$3(b_r+b_s)+4n_h\text{(划分总数)}$,寻道总数$2(\lceil b_r/b_b\rceil+\lceil b_s/b_b\rceil)$,考虑递归划分,块传输总数$2(b_r+b_s)\lceil\log_{\lfloor M/b_b\rfloor-1}(b_s/M)\rceil+b_r+b_s$,寻道总数$2(\lceil b_r/b_b\rceil+\lceil b_s/b_b\rceil)\lceil\log_{\lfloor M/b_b\rfloor-1}(b_s/M)\rceil$。表达式求值:物化和流水线。 查询优化 等价规则:$\sigma$的级联及交换律,$\sigma_{\theta_1\land\theta_2}(E)=\sigma_{\theta_1}(\sigma_{\theta_2}(E))=\sigma_{\theta_2}(\sigma_{\theta_1}(E))$;$\Pi$的级联,$\Pi_{L_1}(\cdots(\Pi_{L_n}(E))\cdots)=\Pi_{L_1}(E)$;选择、笛卡尔积及$\theta$连接结合,$\sigma_{\theta}(E_1\times E_2)=E_1\bowtie_{\theta}E_2$,$\sigma_{\theta_1}(E_1\bowtie_{\theta_2}E_2)=E_1\bowtie_{\theta_1\land\theta_2}E_2$;$\theta$连接的交换性,$E_1\bowtie_{\theta}E_2=E_2\bowtie_{\theta}E_1$;自然连接的结合律,$(E_1\bowtie E_2)\bowtie E_3=E_1\bowtie(E_2\bowtie E_3)$;$\theta$连接的结合律,如果$\theta_2$只涉及$E_2$与$E_3$的属性,则$(E_1\bowtie_{\theta_1}E_2)\bowtie_{\theta_2\land\theta_3}E_3=E_1\bowtie_{\theta_1\land\theta_3}(E_2\bowtie_{\theta_2}E_3)$;选择连接对$\theta$连接的分配律,如果$\theta_0$只涉及$E_1$,则$\sigma_{\theta_0}(E_1\bowtie_{\theta}E_2)=(\sigma_{\theta_0}(E_1))\bowtie_{\theta}E_2$,如果$\theta_1$只涉及$E_1$且$\theta_2$只涉及$E_2$,则$\sigma_{\theta_1\land\theta_2}(E_1\bowtie_{\theta}E_2)=(\sigma_{\theta_1}(E_1))\bowtie_{\theta}(\sigma_{\theta_2}(E_2))$;投影运算对$\theta$连接的分配律,如果$L_1$、$L_2$是$E_1$、$E_2$的属性,假设$\theta$只涉及$L_1\cup L_2$中的属性,则$\Pi_{L_1\cup L_2}(E_1\bowtie_{\theta}E_2)=(\Pi_{L_1}(E_1))\bowtie_{\theta}(\Pi_{L_2}(E_2))$,假设$\theta$还涉及了$L_3$中的属性,则$\Pi_{L_1\cup L_2}(E_1\bowtie_{\theta}E_2)=\Pi_{L_1\cup L_2}((\Pi_{L_1\cup L_3}(E_1))\bowtie_{\theta}(\Pi_{L_2\cup L_3}(E_2)))$;集合的交和并有交换律;集合的交和并有结合律;选择对并、交、差的分配律;投影对并的分配律。转换的例子:先投影再连接,先连接小的。开销估计的统计信息:$l_r$元组的大下,$f_r$一个块中的元组个数,$V(A,r)=n_{\Pi_A,r}$,一定有$b_r=\lceil n_r/f_r\rceil$。选择大小估计:$\sigma_{A=v}(r)$的大小约为$n_r/V(A,r)$;$\sigma_{A\leq V}(r)$的大小约为$n_r\cdot\frac{v-\min(A,r)}{\max(A,r)-\min(A,r)}$;合取,$\sigma_{\theta_1\land\cdots\land\theta_n}(r)$的大小约为$n_r\cdot\frac{S_1\cdot S_2\cdot\cdots\cdot S_n}{n_r^n}$,其中$S_i$是对$\sigma_{\theta_i}(r)$大小的估计;析取,$\sigma_{\theta_1\lor\cdots\lor\theta_n}(r)$的大小约为$n_r\cdot\left(1-\left(1-\frac{S_i}{n_r}\right)\cdots\left(1-\frac{S_n}{n_r}\right)\right)$;取反,$\sigma_{\neg\theta}(r)$的大小约为$n_r-\mathrm{size}(\sigma_{\theta}(r))$。连接大小估计:$R\cap S=\emptyset$,用笛卡尔积估计;$R\cap S={A}$且$A$是$R$的键,不会超过$s$的个数;$R\cap S={A}$且$A$不是$R$的键,$\min\left\{\frac{n_rn_s}{V(A,s)},\frac{n_rn_s}{V(A,r)}\right\}$。其他操作大小估计:$\Pi_A(r)$大小约为$V(A,r)$;$_Ag_F$大小约为$V(A,r)$;$r\cup s$大小约为$n_r+n_s$;$r\cap s$大小约为$\min{n_r,n_s}$;$r-s$大小约为$n_r$;$r⟕s$大小约为$\mathrm{size}(r\bowtie s)+n_r$;$r⟗s$大小约为$\mathrm{size}(r\bowtie s)+n_r+n_s$。$V(A,\sigma_{\theta}(r))$的估计**:若$\theta$取特定值,估计为1;若$\theta$取给定值,估计为给定值个数;若$\theta=A~op~v$,估计为$V(A,r)\times s$,$s$是选中概率;其他情况,估计为$\min{V(A,r),n_{\sigma_{\theta}(r)}}$。$V(A,r\bowtie s)$的估计:若$A$属性全来自$r$,则估计为$\min{V(A,r),n_{r\bowtie s}}$;若$A$包括了来自$r$的属性$A1$和来自$s$的属性$A2$,估计为$\min{V(A1,r)\times V(A2-A1,s),V(A1-A2,r)\times V(A_2,s),n_{r\bowtie s}}$。 事务 事务的要求:原子性、隔离性、持久性、一致性(ACID)。稳定性存储器:永远不会丢失数据。事务的状态:活动的、部分提交的、失败的、终止的、提交的。可串行化:等价于串行调度的调度,有冲突可串行化和视图可串行化。冲突:如果两个指令访问了同一数据且有一个指令写了数据,则它们是冲突的。冲突等价:如果$S$通过交换非冲突指令得到$S’$,则$S$和$S’$冲突等价。冲突可串行化:与串行调度冲突等价的调度。优先图:画一条$T_i$到$T_j$的边,如果两个事务冲突且$T_i$先访问数据,优先图无环则可串行序列化。视图可串行化:满足以下3条称为$S’$与$S$视图等价,1) $S$中某事务读取初始值,$S’$中也是如此;2) $S$中某事务读取的值是另一事务的结果,$S’$中也是如此;3) $S$中某事务最后写,$S’$中也是如此;冲突可序列化一定视图可序列化;每个非冲突可序列化的视图可序列化存在盲写。可恢复调度:$T_j$读取了$T_i$写入的数据,$T_i$必须出现在$T_j$的提交之前。级联回滚:一个事务的失败会造成一系列未提交事务的失败。无级联调度:$T_j$读取了$T_i$写入的数据,$T_i$的提交出现在$T_j$的读之前。隔离性级别:可串行化、可重复读、已提交读、未提交读。 并发控制 锁的类型:排他锁(可读可写,lock-X获得)、共享锁(只读,lock-S获得);共享锁之间可以相容,别的情况都不可以;死锁可以通过回退事务解决。两阶段加锁协议:分为两阶段;先是增长阶段,事务只能获取锁,再是缩减阶段,事务只能释放锁;保证冲突串行化,但不保证不发生死锁,级联回滚可能发生;严格两阶段加锁,事务提交后方可释放排他锁,可避免级联回滚;强两阶段加锁,提交后方可释放资源。锁转换:第一阶段可将共享锁升级为排他锁,第二阶段可将排他锁降级为共享锁。锁表:哈希索引数据项的列表,元素为锁,保证了无饿死现象。树形协议:基于图的协议,数据项$D={d_1,d_2,\cdots,d_h}$,偏序关系$d_i\to d_j$,访问$d_j$之前必须访问$d_i$,树形协议是一种简单的图协议;只有排他锁,首次加锁可以是任何数据,接下来的加锁必须是已加锁的子节点,可以随时解锁,解锁完了不能再加锁;保证冲突可串行化和无死锁,不保证可恢复和无级联回滚。死锁预防:一次全部加锁;规定加锁次序,如树形协议;wait-die机制(非抢占式),老的事务可以等待新的事务,当新的事务等待老的事务的时候回滚;wound-wait机制(抢占式),新的事务可以等待老的事务,当老的事务等待新的事务的时候回滚;超时机制。死锁检测:使用等待图,顶点时事务,$T_i\to T_j$表示$T_i$在等待$T_j$释放所需要数据项,有环则有死锁。多粒度:细粒度、粗粒度;4层,数据库、区域、文件、记录。多粒度的意向锁:共享意向(IS,底层只能加共享锁)、排他意向(IX,底层可加共享或排他锁),共享排他意向锁(SIX,底层加了共享锁,更底层加了排他锁);IS-IS、IS-IX、IX-IX、IS-S、IS-SIX、S-S相容。基于时间戳的协议:为每个事务记录了时间戳$\mathrm{TS}(T_i)$,为每个数据记录了两个时间戳,$\mathrm{W\text{-}timestamp(Q)}$最大执行$\mathrm{write}(Q)$的时间、$\mathrm{R\text{-}timestamp}(Q)$最大执行$\mathrm{read}(Q)$的时间;$T_i$发出$\mathrm{read}(Q)$,若$\mathrm{TS}(T_i)<\mathrm{W\text{-}timestamp}(Q)$则拒绝回滚,否则成功并更新时间戳;$T_i$发出$\mathrm{write}(Q)$,若$\mathrm{TS}(T_i)<\mathrm{R\text{-}timestamp}(Q)\lor\mathrm{TS}(T_i)<\mathrm{W\text{-}timestamp}(Q)$则拒绝并回滚,否则成功并更新时间戳;无死锁,可能出现及联合回滚,可能不可恢复。Thomas写规则:当$\mathrm{TS}(T_i)<\mathrm{W\text{-}timestamp}(Q)$,忽略写操作。基于有效性检查的协议:事务分为3阶段,读和执行、验证、写,又称为乐观并发控制,3阶段对应3个时间戳$\mathrm{Start}(T_i)$、$\mathrm{Validation}(T_i)$、$\mathrm{Finish}(T_i)$,其中$\mathrm{TS}(T_i)=\mathrm{Validation}(T_i)$。$T_i$的有效性测试:对于所有的$\mathrm{TS}(T_k)<\mathrm{TS}(T_i)$,满足下面2条条件之一,1) $\mathrm{Finish}(T_k)<\mathrm{Start}(T_i)$,2) $T_k$写的数据与$T_i$读的数据不相交且$\mathrm{Start}(T_i)<\mathrm{Finish}(T_k)<\mathrm{Validation}(T_i)$,则$T_i$通过并提交,否则终止。多版本机制:多版本时间戳排序和多版本两阶段加锁。多版本时间戳排序:存储一系列版本,每个版本包含内容和读写时间戳;$T_i$写时读写时间戳初始化为$\mathrm{TS}(T_i)$,$T_i$读时如果$\mathrm{TS}(T_i)$大于读时间戳则更新;令$Q_k$是小于等于$\mathrm{TS}(T_i)$的最大写时间戳的版本;$\mathrm{read}(Q)$时返回$Q_k$的内容;$\mathrm{write}(Q)$时,若$\mathrm{TS}(T_i)<\mathrm{R\text{-}timestamp(Q_k)}$则回滚,若$\mathrm{TS}(T_i)=\mathrm{W\text{-}timestamp(Q_k)}$则覆盖$Q_k$的内容,否则创建新的版本;不保证可恢复性和无级联性。多版本两阶段加锁:区分只读事务和更新事务;数据项有时间戳$\mathrm{ts\text{-}counter}$;更新事务执行强两阶段加锁协议;只读事务开始时读取当前$\mathrm{ts\text{-}counter}$,读取小于$\mathrm{TS}(T_i)$的最大时间戳的版本的内容;更新数据项时,创建新版本,时间戳置为$\infty$,提交时时间戳置为$\mathrm{ts\text{-}counter}+1$,再对$\mathrm{ts\text{-}counter}$加1;可恢复的和无级联的。 恢复系统 错误分类:事务错误(包含逻辑错误、系统错误)、系统崩溃(Fail-stop假设非易失存储的内容不会改变)、磁盘错误。基于日志的恢复:当事务$T_i$启动的时候,插入日志$\langle T_i~\mathrm{start}\rangle$;当$T_i$执行$\mathrm{write}(X)$,插入日志$\langle T_i,X,V_1,V_2\rangle$,$V_1$是旧值,$V_2$是新值;当$T_i$执行完毕后,插入日志$\langle T_i~\mathrm{commit}\rangle$。数据库修改:延迟修改,知道提交都没修改数据库;立即修改,修改在事务活跃时发生。撤销和重做:撤销$\langle T_i,X,V_1,V_2\rangle$时将$V_1$写入$X$,重做$\langle T_i,X,V_1,V_2\rangle$时将$V_2$写入$X$;$\mathrm{undo}(T_i)$时,回退所有操作,并写入特殊日志$\langle T_i,X,V_1\rangle$,最后插入$\langle T_i~\mathrm{abort}\rangle$;$\mathrm{redo}(T_i)$时没有日志输出。检查点:隔段时间执行,写入所有日志到稳定存储,并插入日志$\langle\mathrm{checkpoint}~L\rangle$,$L$是所有活跃的事务。系统崩溃后的恢复:重做阶段,1) 将undo-list设为$\langle\mathrm{checkpoint}~L\rangle$中的$L$,2) 遇到$\langle T_i,X,V_1,V_2\rangle$或$\langle T_i,X,V_2\rangle$,将$V_2$赋给$X$,3) 发现$\langle T_i~\mathrm{start}\rangle$,将$T_i$加入到undo-list,4) 发现$\langle T_i~\mathrm{abort}\rangle$或$\langle T_i~\mathrm{commit}\rangle$,将$T_i$从undo-list中移除;撤销阶段,反向扫描日志,撤销undo-list中的操作。

2019/9/8
articleCard.readMore

系统分析与控制小抄

这是我的系统分析与控制期末小抄,是对老师课件的总结。原文由$\LaTeX$编写,可点击此处下载。 Laplace变换 傅里叶变换的定义:$F(\omega)=\int_{-\infty}^{+\infty}f(t)e^{-j\omega t}dt$。绝对可积是充分不必要条件。 拉普拉斯变换的定义:$F(s)=\int_0^{+\infty}f(t)e^{-st}dt$;记号:$F(s)=\mathrm{L}[f(t)],f(t)=\mathrm{L}^{-1}[F(s)]$。 常见函数的拉普拉斯变换:阶跃函数,$f(t)=A,F(s)=\frac{A}{s}$;斜坡函数,$f(t)=At,F(s)=\frac{A}{s^2}$;指数函数,$f(t)=e^{-at},F(s)=\frac{1}{s+a}$;正弦函数,$f(t)=\sin(\omega t),F(s)=\frac{\omega}{s^2+\omega^2}$。 拉普拉斯变换的性质:线性性质,$\mathrm{L}[af_1(t)+bf_2(t)]=aF_1(s)+bF_2(s)$;微分性质,若$f(0)=f’(0)=\cdots=0$,则$\mathrm{L}[f^{(n)}(t)]=s^nF(s)$,否则$\mathrm{L}[f’(t)]=sF(s)-f(0)$;积分性质,$\mathrm{L}[\int_0^tf(\tau)d\tau]=\frac{1}{s}F(s)$;延迟性质,$\mathrm{L}[f(t-\tau)]=e^{-\tau s}F(s)$;终值定理:$\lim\limits_{t\to\infty}f(t)=\lim\limits_{s\to 0}sF(s)$;初值定理:$\lim\limits_{t\to 0}f(t)=\lim\limits_{s\to\infty}sF(s)$。 有理函数的分解:对于$F(s)=\frac{\cdots}{(s-s_1)(s-s_2)\cdots(s-s_n)}$,1) 均为单实根,令$F(s)=\frac{c_1}{s-s_1}+\frac{c_2}{s-s_2}+\cdots+\frac{c_n}{s-s_n}$,则$c_i=\lim\limits_{s\to s_i}(s-s_i)F(s)$,2) 多重实根,令$F(s)=\frac{c_n}{(s-s_1)^n}+\frac{c_{n-1}}{(s-s_1)^{n-1}}+\cdots+\frac{c_1}{s-s_1}$,则$c_n=\lim\limits_{s\to s_1}(s-s_1)^nF(s),c_{n-j}=\frac{1}{j!}\lim\limits{s\to s_1}\frac{d^j}{ds^j}[(s-s_1)^nF(s)]$。 商的求导法则,$[\frac{u}{v}]’=\frac{u’v-uv’}{v^2}$,$[\frac{1}{x}]=-\frac{1}{x^2}$。 时域分析 稳定性 定义:系统偏离平衡状态后,在没有外力作用下,其状态能自动地回到平衡状态。令$y_t(t)$为暂态分量,稳定则$\lim\limits_{t\to\infty}y_t(t)=0$。为什么需要稳定性? 由系统内在特性造成的输出响应必须逐渐衰减并最终消失,从而才可能专心地跟踪输入信号或者抑制干扰影响。 稳定性分析:特征方程的根,1) 都在左半平面,则稳定,2) 虚轴上有单根,其他根都在左半平面,则临界稳定,3) 由半平面有根或者虚轴上有重根,则不稳定。 传递函数:零初始条件。$G(s)=\frac{Y(s)}{R(s)}$,其中$Y(s)$是输出,$R(s)$是输入。特征方程的根就是传递函数的极点。 结构图 闭环传递函数:令$G(s)$是前向传递函数,$H(s)$是负反馈传递函数,则闭环传递函数$\frac{Y(s)}{R(s)}-\frac{G(s)}{1+G(s)H(s)}$。 结构图的等效与化简:略。 劳斯判据(根稳定性判别方法):对于6次方程$F(s)=a_0s^6+a_1s^5+\cdots+a_5s+a_6$,如下列出前两行: |-|-|-|-|-| | $s^6$ | $a_0$ | $a_2$ | $a_4$ | $a_6$ | | $s^5$ | $a_1$ | $a_3$ | $a_5$ | 0 | 然后按照$a_{ij}=-\frac{1}{a_{i-1,1}}\begin{vmatrix} a_{i-2,1} & a_{i-2,j+1} \ a_{i-1,1} & a_{i-1,j+1} \end{vmatrix}$,填充其它行。第一列符号改变次数等于右半平面根数。若劳斯判据第一列无符号改变,则根稳定。 劳斯判据第一列为0:如果某一行第一个元素为0,其余元素不为0,将0代替为一个小的正数$\epsilon$;如果某一行第一个元素为0,其余元素也为0,则有关于原点对称的根,这时使用辅助多项式,求其微分作为新的一行,例子如下。 |-|-|-| | $s^3$ | 6 | 6 | | $s^2$ | 4 | 4 | | $s^1$ | 0 | 0 | 这时辅助多项式为$A(s)=4s^2+4$,则$\frac{dA(s)}{ds}=8s$,故最后的表格如下。 |-|-|-| | $s^3$ | 6 | 6 | | $s^2$ | 4 | 4 | | $s^1$ | 8 | 0 | | $s^0$ | 4 | | 稳态性能 产生原因:反馈控制系统需要误差信号来产生控制作用。如果稳态时仍然需要控制作用,就必须有非零的误差以维持控制作用($u=Ke$)。从而产生了稳态误差。 计算稳态误差的前提:系统是稳定的。 输入稳态误差和干扰稳态误差计算如下。 $$e_{ss}=\lim_{s\to 0}\frac{sR(s)}{1+G_1(s)H(s)}+\lim_{s\to 0}\frac{-sG_2(s)H(s)V(s)}{1+G_1(s)H(s)}$$ 阶跃输入下的稳态误差:$e_s=\frac{R}{1+\lim\limits_{s\to 0}G_1(s)H(s)}=\frac{R}{1+K_p}$,其中$K_p=\lim\limits_{s\to 0}G_1(s)H(s)$为位置品质系数。 斜坡输入下的稳态误差:$e_s=\frac{R}{\lim\limits_{s\to 0}sG_1(s)H(s)}=\frac{R}{K_v}$,其中$K_v=\lim\limits_{s\to 0}sG_1(s)H(s)$为速度品质系数。 斜坡输入下的稳态误差:$e_s=\frac{R}{\lim\limits_{s\to 0}s^2G_1(s)H(s)}=\frac{R}{K_a}$,其中$K_a=\lim\limits_{s\to 0}s^2G_1(s)H(s)$为加速度品质系数。 稳态误差总结:令 $$G_1(s)H(s)=\frac{K\prod\limits_{k=1}^p(T_ks+1)\prod\limits_{l=1}^q(T_l^2s^2+2\xi_lT_ls+1)}{s^r\prod\limits_{i=1}^m(T_is+1)\prod\limits_{j=1}^n(T_j^2s^2+2\xi_jT_js+1)}$$ 阶跃 斜坡 抛物线 $r=0$ $\frac{R}{1+K}$ $\infty$ $\infty$ $r=1$ $0$ $\frac{R}{K}$ $\infty$ $r=2$ $0$ $0$ $\frac{R}{K}$ $\frac{1}{s}$越多,稳态性能越好。 动态性能 超调量:$\sigma=\frac{y_m-y_s}{y_s}$,其中$y_s$是稳态值不是期望值。 过渡过程时间:$t_s$是进入稳态值5%范围的时间。 一阶系统定量分析:传递函数为$\frac{1}{Ts+1}$。单位阶跃响应为$y(t)=1-e^{-t/T}$。$\sigma=0,t_s\approx 3T$。 二阶系统定量分析:传递函数为$\frac{\omega^2}{s^2+2\xi\omega s+\omega^2}$。$\xi>0$则稳定。 $$\sigma=\begin{cases} 0 & \xi\geq 1 \ e^{-\frac{\pi\xi}{\sqrt{1-\xi^2}}} & 0<\xi<1 \end{cases}$$ $$t_s\begin{cases} \frac{3.2}{\xi\omega} & 0<\xi<0.69 \ \frac{2.8+6.5(\xi-0.7)}{\omega} & \xi\geq 0.69 \end{cases}$$ 高阶系统的近似简化:设传递函数为$M(s)=\frac{K(s-z_1)\cdots(s-z_m)}{(s-p_1)\cdots(s-p_n)}$,1) 零极点相消,$|p_k-z_r|$很小时对消,结果为$\bar{M}(s)=\frac{Kz_r}{p_k}\frac{\prod\limits_{j=1,k\neq r}^m(s-z_j)}{\prod\limits_{i=1,i\neq k}^n(s-p_i)}$,右半平面的零、极点不能对消;2) 远极点消除,对于$\mathrm{Re}(p_k)$很小的情况,可消除该极点,结果为$\bar{M}(s)=\frac{K}{p_k}\frac{\prod\limits_{j=1}^m(s-z_j)}{\prod\limits_{i=1,i\neq k}^n(s-p_i)}$。消除时稳态放大倍数应不变。 状态方程 状态方程的一般形式: $$\begin{cases} \dot{x}(t)=Ax(t)+Bu(t) \ y(t)=Cx(t)+Du(t) \end{cases}$$ 状态方程之间的转换:状态变量的选取不唯一,从而状态方程不唯一(传递函数是唯一的)。如果$\bar{x}(t)=Px(t)$,则新状态方程的各个参数变为$\bar{A}=PAP^{-1},\bar{B}=PB,\bar{C}=CP^{-1},\bar{D}=D$。 状态方程到传递函数的转换:初态必须是0,即$x(0)=0$,此时$G(s)=C(sI-A)^{-1}B+D$;否则$Y(s)=G(s)U(s)+Cx(0)$。 传递函数到状态方程的转换:设$G(s)=\frac{Y(s)}{U(s)}=\frac{b_0s^m+b_1s^{m-1+\cdots+b_{m-1}s+b_m}}{s^n+a_1s^{n-1}+\cdots+a_{n-1}s+a_n}$。若$n>m$,则 $$A=\begin{bmatrix} 0 & 1 & 0 & \cdots & 0 \ 0 & 0 & 1 & \cdots & 0 \ \vdots & \vdots & \vdots & \ddots & \vdots \ 0 & 0 & 0 & \cdots & 1 \ -a_n & -a_{n-1} & -a_{n-2} & \cdots & -a_1 \end{bmatrix} B=\begin{bmatrix} 0 \ 0 \ \vdots \ 0 \ 1 \end{bmatrix}$$ $$ C = \begin{bmatrix} b_m & b_{m-1} & \cdots & b_1 & b_0 & 0 & \cdots & 0 \end{bmatrix}, D = 0 $$ 若$n=m$,则$D=b_0$。 非线性系统的线性化: $$\begin{align*} \frac{dx(t)}{dt}=f(x,u)\approx f(x_0,u_0)&+\frac{\partial f(x,u)}{\partial x}\bigg\rvert_{(x_0,u_0)}(x-x_0) \ &+\frac{\partial f(x,u)}{\partial u}\bigg\rvert_{(x_0,u_0)}(u-u_0) \end{align*}$$ 选择工作点$f(x_0,u_0)=0$,令$\tilde{x}=x-x_0, \tilde{u}=u-u_0$。 步骤: 列写原始微分方程 建立状态方程 确定工作点 建立增量的线性化方程 频域分析 频域分析的特点:稳定的线性系统不改变输入正弦信号的频率,只改变输入正弦信号的幅值和相位。 与时域响应的关系:将传递函数中的$s$替换为$j\omega$即可得到频率特性。 一阶系统的频率特性:$G(j\omega)=\frac{1}{j\omega T+1}$,故幅频$A(\omega)=|G(j\omega)|=\frac{1}{\sqrt{(\omega T)^2+1}}$,相频$\angle G(j\omega)=-\arctan(\omega T)$。可以看出:1) 低频信号,幅值衰减少,相位偏移少,能够基本复现输入信号;高频信号,幅值衰减很多,相位偏移很大,信号变形很厉害。2) 定义$\omega_b=\frac{1}{T}$,它是输出下降到$0.707A$处的频率,$\omega_b$大则可通过的频率成分越多,惯性小,输出过渡过程也越快;$\omega_b$小则惯性大。 极坐标图:$G(j\omega)=A(\omega)e^{j\varphi(\omega)}$,作出极坐标的参数方程。 Bode图:横坐标采用10倍频程$\log(\omega)$。上方是幅频图,纵坐标$L(\omega)=20\log(A(\omega))$。下方是相频图。 一阶系统的Bode图:1) $\omega T\ll 1$,则$A(\omega)\approx 1,L(\omega)\approx 0,\varphi(\omega)\approx 0$;2) $\omega T\gg 1$,则$A(\omega)\approx\frac{1}{\omega T},L(\omega)\approx 20\log(\frac{1}{T})-20\log{\omega},\varphi(\omega)\approx -90^{\circ}$,3) $\omega T=1$,则$A(\omega)=\frac{1}{\sqrt{2}},L(\omega)\approx -3,\varphi(\omega)=-45^{\circ}$。 Bode图的性质: 采用频率的对数坐标,展宽了视野 作图容易,可利用折线段近似 频率特性乘除对应于幅频特性曲线加减 频率特性的纵向放大、缩小对应于幅频特性曲线的上移和下移 简化了频率特性的倒数关系 基本环节的Bode图:1) 比例环节$G(s)=K$,则$L(\omega)=20\log(k),\varphi(\omega)=0$;2) 积分环节$G(s)=\frac{1}{s}$,则$L(\omega)=-20\log\omega,\varphi(\omega)=-90^{\circ}$;3) 微分环节$G(s)=s$,则$L(\omega)=20\log\omega,\varphi(\omega)=90^{\circ}$。 二阶震荡环节的Bode:$G(s)=\frac{1}{T^2s^2+2\xi Ts+1}, 0\leq \xi<1$,$G(j\omega)=\frac{1}{1-\omega^2T^2+2\xi\omega Tj}$,则 $$A(\omega)=\frac{1}{\sqrt{(1-\omega^2T^2)^2+(2\xi\omega T)^2}}$$ $$\varphi(\omega)=-\arctan\frac{2\xi\omega T}{1-\omega^2T^2}$$ $$L(\omega)=-20\log\sqrt{(1-\omega^2T^2)^2+(2\xi\omega T)^2}$$ $\omega\ll\frac{1}{T}$,则$L(\omega)\approx 0,\varphi(\omega)\approx 0$;2) $\omega\gg\frac{1}{T}$,则$L(\omega)\approx 20\log\frac{1}{(\omega T)^2}=40\log\frac{1}{T}-40\log\omega,\varphi(\omega)\approx-180^{\circ}$;3) $\omega=\frac{1}{T}$,则$L(\omega)=-20\log(2\xi),\varphi(\omega)=-90^{\circ}$。 一般传递函数的Bode图:一般地,$G(s)=G_1(s)G_2(s)\cdots G_n(s)$,则$L(\omega)=L_1(\omega)+L_2(\omega)+\cdots+L_n(\omega)$,$\varphi(\omega)=\varphi_1(\omega)+\varphi_2(\omega)+\cdots+\varphi_n(\omega)$。对下方的传递函数: $$G(s)=\frac{K\prod\limits_{k=1}^p(T_ks+1)\prod\limits_{l=1}^q(T_l^2s^2+2\xi_lT_ls+1)}{s^r\prod\limits_{i=1}^m(T_is+1)\prod\limits_{j=1}^n(T_j^2s^2+2\xi_jT_js+1)}$$ 幅频特性作图步骤如下: 化标准形 低频部分:找$\omega=1,L(\omega)=20\log K$的点,由该点向左画斜率为$-20r$的斜线 求转折频率$\omega_i=1/T_i$,并由小到大排序,$\omega_1<\omega_2<\cdots$ 从低频渐近线开始自左向右画,碰到$\omega_i$就拐弯,分母环节向下弯,分子环节向上弯,一阶环节斜率变20,二阶环节变40 修正(圆滑过渡) 相频特性作图步骤如下: 画$-90^{\circ}\times r$水平线 算出转折点$\varphi(\omega)$ 粗画相频特性$\varphi(\omega)$ 稳定裕量:令$\omega_c$为剪切频率,即$L(\omega)=0$时$\omega$的值,则稳定裕量为$\gamma=\varphi(\omega_c)-(-180^{\circ})$。一般而言,$30^{\circ}\leq\gamma\leq 70^{\circ}$是可接受的范围。$\gamma$太小,稳定裕量小,超调大,振荡多;$\gamma$太大,稳定裕量大,动态响应慢,过渡过程时间长。 采样控制系统 概念 采样控制系统的特性:采样周期越小,采样信号越接近原始信号。香农定律:为了完美地重构信号,需要按照不小于2倍带宽采样率对信号进行采样。数学描述:$e^*(t)=\sum\limits_{k=0}^{\infty}e(kT)\delta(t-kT)$。 系统分类: 连续控制系统:连续信号 离散控制系统:离散信号 采样控制系统:连续、离散信号 数字控制系统:连续、离散信号,量化效应 采样系统的数学模型: $$\begin{cases} x(k+1)=Fx(k)+Gu(k) \ y(k)=Cx(k)+Du(k) \end{cases}$$ Z变换 Z变换:$R(z)=\sum\limits_{k=0}^{+\infty}r(k)z^{-k}$。 常见Z变换:1) 对于$r(k)=\begin{cases}1 | k=0 \ 0 | k\neq 0\end{cases}$,有$R(z)=1$;2) 对于$r(k)=\begin{cases}1 | k\geq 0 \ 0 | k<0\end{cases}$,有$R(z)=\frac{z}{z-1}$。 Z变换性质:1) 线性性质,$Z[ar_1(t)+br_2(t)]=aR_1(z)+bR_2(z)$;2) 延迟性质,$Z[r(k-1)]=z^{-1}R(z)$;3) 超前性质,$Z[r(k+1)]=z(R(z)-r(0))$;4) 初值定理,$\lim\limits_{k\to 0}r(k)=\lim\limits_{z\to\infty}R(z)$;5) 终值定理,$\lim\limits_{k\to\infty}r(k)=\lim\limits_{z\to 1}(1-z^{-1})R(z)$。 离散与连续之间的转换 连续系统对应的离散化模型: $$F=e^{AT},G=\left(\int_0^Te^{AT}dt\right)B$$ 连续传递函数到离散传递函数的转换: $$G(z)=Z\left[\frac{1-e^{-Ts}}{s}G(s)\right]=(1-z^{-1})Z\left[\frac{1}{s}G(s)\right]$$ 离散状态方程到传递函数的转换:与连续类似。 $$G(z)=C(zI-F)^{-1}G+D$$ 离散传递函数到状态方程的转换:与连续类似。 离散系统稳定性 离散系统稳定性条件:特征方程的根均在单位圆内。先作替换$z=\frac{\omega+1}{\omega-1}$,再用劳斯判据。 离散系统稳态性能 阶跃输入下的稳态误差:输入$R(z)=R\frac{z}{z-1}$,$e_s=\frac{R}{1+\lim\limits_{z\to 1}D(z)G(z)}=\frac{R}{1+K_p}$,其中$K_p=\lim\limits_{z\to 1}D(z)G(z)$为位置品质系数。 斜坡输入下的稳态误差:输入$R(z)=R\frac{Tz}{(z-1)^2}$,$e_s=\frac{R}{\lim\limits_{s\to 1}\frac{z-1}{T}D(z)G(z)}=\frac{R}{K_v}$,其中$K_v=\lim\limits_{z\to 1}\frac{z-1}{T}D(z)G(z)$为速度品质系数。 斜坡输入下的稳态误差:输入$R(z)=R\frac{T^2z(z+1)}{2(z-1)^3}$,$e_s=\frac{R}{\lim\limits_{z\to 1}(\frac{z-1}{T})^2D(z)G(z)}=\frac{R}{K_a}$,其中$K_a=\lim\limits_{z\to 1}(\frac{z-1}{T})^2D(z)G(z)$为加速度品质系数。 总结: $\frac{1}{(z-1)^r}$ 阶跃 斜坡 抛物线 $r=0$ $\frac{R}{1+K_p}$ $\infty$ $\infty$ $r=1$ $0$ $\frac{R}{K_v}$ $\infty$ $r=2$ $0$ $0$ $\frac{R}{K_a}$ 离散系统动态性能 近似等效法 $$z=e^{sT}\approx\frac{1+sT/2}{1-sT/2}$$ 现代控制理论 极点配置: 设$u(t)=-Lx(t)$,要适当选取$L$,通过改变$x(t)$的运动规律,间接改变了输出$y(t)$的运动规律。设期望的极点为$p_1,p_2,\cdots,p_n$,则可求解$|sI-(A-BL)|=(s-p_1)(s-p_2)\cdots(s-p_n)$得到$L$。 能控性:$S=\begin{bmatrix} B | AB | \cdots | A^{n-1}B \end{bmatrix}$满秩则能控。

2019/9/8
articleCard.readMore

JavaScript学习笔记

这篇文章是JavaScript的学习笔记。内容主要来自MDN的JavaScript Guide - JavaScript | MDN。 语法和类型 基础 JS采用Unicode字符集,并且大小写敏感。语句使用;结束。一行单条语句的;不是必需的。建议始终加上;。 注释 注释与C++类似,有单行注释//,和多行注释/* */。多行注释不能嵌套。 声明 有四种方式方式定义变量: 直接赋值:定义一个全局变量(不严格模式) var:定义一个变量(全局或局部作用域) let:定义一个局部变量(块作用域) const:定义一个常量 形式均类似var name1 = value1 [, name2 = value2 [, ... [, nameN = valueN]]];。合法的标识符由英文字母、_、$开头,后还可跟数字组成。 未初始化的变量值为undefined。使用未声明的变量会产生ReferenceError。 undefined转换成bool类型为false,转化成数字类型为NaN。null转换成bool类型为false,转化成数字类型为0。可以使用严格等号===判断一个变量是否为undefined或null。 (var)函数外声明的变量为全局变量,函数内部声明的变量为局部变量。ECMA2015中出现了块作用域(let),之前没有。JS函数可嵌套。 var声明的变量会被提前(相当与声明提前,但初始化并未提前)。let和const不会被提前。注意:函数声明也会被提前,也就是函数可以在声明前使用。 全局变量实际上是全局对象的属性,对于web而言,就是window。 const变量不能和同一作用域的其他函数和变量重名。const对象的属性不受保护,可重新赋值。 数据结构和类型 共有8种数据类型: 7种基础数据类型 Boolean:布尔 null undefined Number:数字 BigInt:高精度整数 String:字符串 Symbol:符号 (ECMA2015) 对象 JavaScript是动态类型语言。 字符串类型与数字类型进行+运算,数字类型转换为字符串类型。而其他的运算,字符串类型转换为数字类型。注意:可以通过一元+,将字符串转换为数字。 有以下两个函数可以进行字符串到数字的转换: parseInt(string, radix):转换为整数 parseFloat(value):转换为浮点数 字面常量 数组字面常量是由[]括住的,分割的列表。未指定的元素将为undefined。如果列表的最后有尾随的逗号,将被忽略。建议省略的元素显式地用undefined表示。 true和false是两种布尔类型的字面常量。不要混淆基础的布尔类型和布尔对象。 整数字面常量有以下几种: 10进制:非0开头的数字序列 8进制:0开头,或0o(0O)开头 16进制:0x(0X)开头 2进制:0b(0B)开头 浮点字面常量与其他语言类似,一个浮点常量必须有一个数字和小数点或指数部分。 对象字面常量是由{}括住的:和,分割的键值对列表。不应在语句的开始处使用对象字面常量。键可采用标识符、字符串(包括空字符串)和数字。访问属性可采用.(必须是合法的标识符)或[]运算符(必须是值,如字符串)。 ES2015支持构造对象时指定原型(__proto__: theProtoObj)、简写foo: foo语句成foo、直接定义成员函数name() {}、调用基类super.id和运行时计算属性名[expr]: value。 正则表达式字面常量是由//括住的正则表达式。 字符串字面常量是由''或""括住的字符串。可调用字符串对象拥有的属性,如length。在ES2015中,还支持了模板字符串,`...${var}...`,可跨多行。此外还可以使用标签自定义字符串。字符串转义规则同其他语言。 执行控制和错误处理 块语句 多条语句用花括号括起来构成块语句。经常被用于控制语句的语句体部分。 条件语句 if-else语句、switch语句与其他语言类似。 不建议直接在条件判断处使用赋值语句,如果确实需要,则加上括号,如if ((x = y)) {} 假值包括: false undefined null 0 NaN ""(空字符串) 注意:new Boolean(false)的值为真。 异常处理语句 throw语句与其他语言相似,可以throw各种值。catch语句形式为catch (catchID) { statements },其中的catchID作用域仅在catch块中。finally语句与Java类似。注意:catch中的return语句和异常抛出语句会被挂起,先执行finally中的语句,如果finally语句中存在return语句,则之前的返回值和抛出的异常不再起作用。 可以使用Error对象用于异常处理,它有name和message两个属性。 可以使用Promise用于异步或者延迟操作的控制处理。 循环 for、while、do…while、beak、continue语句与其他语言相似。break、continue语句可以带有label。 for…in语句遍历一个对象的所有属性。for…of语句遍历可迭代对象(Array、Map、Set、Arguments等等)。 函数 定义和调用函数 函数定义(又称函数声明、函数语句)形如function name(parameterlist) { statements },其中parameterlist是逗号分隔的参数列表。return语句可以返回值。数据类型传参采用传值,对对象(包括Array)属性的修改会对外部可见。 函数表达式与函数声明形式类似,但作为语句的一个部分,其中name可选。也可以通过Function构造函数从字符串里构造出函数。函数表达式不会提前。 方法是一种作为对象属性的函数。 ES6支持默认形参,以及...name的方式声明剩余形参,其中name是存放额外参数的数组。 注意:函数声明会被提前。函数表达式赋值的对象会被提前,但其值为undefined。 函数调用语法和其他语言一样,可以递归。函数本身也是对象,可以通过apply方法或call方法调用。 作用域和函数栈 在函数内,可以访问函数内定义的变量(及函数)以及父函数所能访问到的变量(及函数),但不能访问到子函数的变量(及函数)。 在函数内部,可以通过arguments.callee引用自己,也可以通过arguments[index]和arguments.length获取参数及其个数。注意:arguments不是数组。函数调用提供的参数和函数声明提供的参数数目不一致,提供少了的参数是默认参数或(没默认参数)undefined,提供多了的参数可以通过arguments访问。 嵌套函数和闭包 嵌套函数会形成闭包,闭包内引用的外层作用域的变量会被保留,直到该闭包销毁。闭包按引用捕获外层变量。 注意:内层作用域会屏蔽外层作用域的同名变量。闭包不捕获this,闭包中的this变量为其调用者而非创建者。对于普通函数this是(构造函数)一个全新的对象或(strict模式的函数调用)undefined或(以对象方法的形式调用)原始对象。 箭头函数 语法形式如(parameters, ...) => { statements }或者(parameters, ...) => expression创建一个箭头函数。当形参个数为1个时,可省略括号,支持默认形参和剩余形参。箭头函数没有自己的this、arguments、super和new.target。 注意:箭头函数捕获this,箭头函数中的this变量为其创建者而非调用者。 预定义函数 eval:执行字符串中的JS代码; isFinite、isNan:判断是否是有限数字和NaN; parseFloat、parseInt:解析字符串返回数字,parseInt还可以选择基数; encodeURI、decodeURI:将URI中的某些字符(不改变URI本身的地址)转换为转义字符,和转换回去; encodeURIComponent、decodeURIComponent:将整个URI的某些字符转换为转义字符,和转换回去。 运算符 与其他类C语言类似。 赋值支持解包的语法:var [var1, var2, ...] = array。 相等判断有三类: ==&!=:不严格相等判断,进行必要的类型转换后再判断; ===&!==:严格相等判断,不进行类型转换(类型不符则一定不等); Object.is(a, b):与严格相等的不同之处在于,Object.is(-0, +0)为false,而Object.is(NaN, NaN)为true。 对于数学运算,除以0会产生Infinity。ES7支持**作为乘方运算符。 对于位运算符,会将操作数转化为32为整型,高位丢弃。右移运算符分为符号右移>>和补0右移>>>。 对于逻辑运算符,expr1 && expr2在expr1能转换为false时返回expr1,否则返回expr2。expr1 || expr2在expr1能转换为true时返回expr1,否则返回expr2。 使用+可以连接字符串。 delete表达式可以有以下几种形式: delete objectName:只能针对隐式声明的变量(不采用var,而是直接赋值); delete objectName.property:删除属性; delete objectName[index]:删除元素,不影响数组长度; delete property:只能在with语句中用。 delete表达式会返回布尔值,操作可行返回true,不可行返回false。用var定义的变量和内置属性都无法删除。 typeof operand或者typeof (operand)返回operand类型对应的字符串。typeof null返回"object"。对于对象,一律返回"object";对于函数和方法。一律返回"function"。 void (expression)或者void expression会对expression进行求值,却不返回结果(也就是返回undefined)。 对于关系运算符,propNameOrNumber in objectName返回对象是否存在某属性,可用于数组和对象。objectName instanceof objectType返回对象是否为某类型的实例。 表达式 JavaScript包含以下5种表达式: 数学 字符串 逻辑 初等 左值 初等表达式 this关键字指当前的对象。()用于调整优先级。此外,非标准的JS还支持两种推导式语法:数组推导式[for (x of y) x]和生成器推导式(for (x of y) x)。 左值表达式 new可以创建对象的实例。super可以调用父对象的函数。通过展开运算符...array可以将数组展开至字面数组或者函数参数处。 数字和日期 数字 在JavaScript中,所有的数字都是64位浮点数。 Number对象有如下的属性和方法。 MAX_VALUE、MIN_VALUE:最大和最小的数字; NAN、NEGATIVE_INFINITY、POSITIVE_INFINITY:NaN,正无穷和负无穷; EPSILON:1和比1大的最小数之间的差; MIN_SAFE_INTEGER、MAX_SAFE_INTEGER:最小和最大的安全整数,分别是($-2^{53}+1$和$+2^{53}-1$); parseFloat()、parseInt():和全局的一样; isFinite()、isNaN():和全局的类似,但是不会将参数转换为数字; isInteger():返回是否是整数; isSafeInteger():返回是否是安全整数。 Number.prototype有如下方法: toExponential():返回指数形式的字符串; toFixed():返回浮点形式的字符串; toPrecision():返回指定精度的浮点形式的字符串。 Math对象 Math对象包含了如PI和E之类的常量,还有各种数学函数。 Date对象 Date对象的范围是-100,000,000天到100,000,000天相较于1970年1月1日UTC时间。 通过new Date()创建Date对象,参数可以是 无:现在的时间戳; 代表时间戳的字符串:形如Month day, year hours:minutes:seconds,可以省略小时、分钟或秒。 代表年(FullYear)、月、日的整数 代表年(FullYear)、月、日、小时、分钟、秒的整数 注意:月份从0计数。年份(非FullYear)是从1900开始的年数。 Date对象的方法大致有如下几类: set方法 get方法 to方法:返回字符串 解析UTC时间 文字处理 字符串 JavaScript的字符串采用UTF-16编码。 对于字符串字面常量,通过\x可以转义16进制字符;通过\u后跟4个16进制字符可以转义UTF-16字符;在ES6中可以通过\u{xxxx}转义Unicode字符。通过String.fromCodePoint()可以将Unicode编码的数字转成字符,通过String.prototype.codePointAt()可以返回指定位置的Unicode字符。 String对象是对String基础数据类型的一层封装。对于字符串字面常量的成员函数调用,会创建临时的String对象完成。注意:应尽量使用String基础数据类型而非String对象。 String对象只有一个属性length。String对象和基础数据类型都是不可变的,对元素的赋值是无效的。 String有如下方法: charAt、charCodeAt和codePointAt:返回指定位置的字符或编码; indexOf和lastIndexOf:返回子串的位置; startsWith、endsWith和includes:返回字符串是否以子串开头、结尾或包含子串; concat:连接字符串; fromCharCode和fromCodePoint:从编码中构建字符串; split:以某字符串为分隔符分割字符串; slice、substring和substr:取出子字符串; match、matchAll、replace和search:使用正则表达式匹配; toLowerCase和toUpperCase:大小写转换; normalize:返回Unicode正规化的字符串; repeat:重复字符串; trim:删除前导和后继字符。 模板字符串是``括起的字符串。模板字符串可以多行(保留回车符),也可以包含占位符,形如${expression}。 国际化 Collator、NumberFormat和DateTimeFormat的构造函数是Intl对象的属性,被用于国际化。 正则表达式的特殊字符如下: \:转义字符,注意:在字符串中还需要转义该字符; ^:匹配开始,多行模式下匹配行首; $:匹配末尾,多行模式下匹配行末; *:出现0或多次,等价于{0,}; +:出现1或多次,等价于{1,}; ?:出现0或1次,等价于{0,1}; .:匹配任意字符,默认情况下换行符除外; (x):捕获组,可以用\1、\2等等来引用捕获组,在replace的第2个参数中,可以用$&、$1、$2等等来引用; (?:x):不捕获的组; x(?=y):匹配后面跟着y的x; x(?!y):匹配后面不跟y的x; (?<=y)x:匹配前面有y的x; (?<!y)x:匹配前面没有y的x; x|y:匹配x或y; {n}:出现n次; {n,}:出现至少n次; {n,m}:出现n到m次; [xyz]:匹配x、y或z字符,特殊字符不用转义,只需要转义-、^、]和\; [^xyz]:匹配除x、y和z之外的字符; \b和\B:匹配单词边界和非单词边界,对于退格使用[\b]匹配; \d和\D:匹配数字和非数字; \s和\S:匹配空白符和非空白符; \w和\W:匹配英文数字和下线符或不匹配它们; \cX:匹配Ctrl—X; \f、\n、\r、\t、\v和\0:匹配换页、换行、回车、制表、垂直制表和空字符; \xhh、\uhhhh:8进制和16进制字符,必须是2位或4位十六进制字符; \u{hhhh}:只有设置了u才起效,匹配Unicode编码字符。 对于字符*、/、\等都需要用\转义。 有如下函数使用了正则表达式。 RegExp.prototype.exec:执行一次搜索,返回匹配信息(包括index和input属性); RegExp.prototype.test:测试是否匹配,返回Bool值; String.prototype.match:执行一次搜索,返回匹配信息,(对于设置了g的会搜索全部); String.prototype.matchAll:执行搜索,返回捕获组的迭代器(Node.js是12开始支持); String.prototype.search:执行搜索,返回出现的索引,找不到返回-1; String.prototype.replace:执行替换; String.prototype.split:切割字符串。 正则表达式对象包含lastIndex和source属性,lastIndex只在设置了g或y才启用,source是原始的正则表达式字符串。 有以下的设置选项: g:全局搜索; i:不区分大小写搜索; m:多行搜索; s:允许.匹配换行; u:见上,Unicode编码; y:sticky搜索(与g类似)。 正则表达式 可以使用/xxxx/或者new RegExp('xxxx')定义正则表达式。前者是在脚本加载时编译,后者是运行时编译。 容器 下标索引的容器 以下3种创建数组的方式等价,1) new Array(element0, element1, ..., elementN),2) Array(element0, element1, ..., elementN),3) [element0, element1, ..., elementN]。 创建空的定长数组也有3个方式,1) new Array(arrayLength);,2) Array(arrayLength);,3) var arr = []; arr.length = arrayLength;。 通过array[index] = value可以给数组赋值。如果index不是整数,则会创建一个属性。 注意:不建议使用for...in遍历数组,因为属性也会被迭代。 数组有如下方法: concat:合并多个数组并将结果返回; join:连接数组成字符串; push和pop:在数组末尾插入或删除元素; shift和unshift:在数组开始处删除和插入元素; slice:抽取一段数组并返回; splice:删除或替换一段数组并返回被删除的元素; reverse:翻转数组; sort:排序,可以接受一个比较函数; indexOf和lastIndexOf:查找元素所在位置; map、filter和forEach:遍历数组,映射、过滤或者访问元素; every和some:映射后,判断是否所有值都为真或者有值为真; reduce和reduceRight:汇总数据。 通过Array.prototype.someFunc.call(xxx)可以对类似数组的对象调用函数,如NodeList和arguments。 ArrayBuffer代表了一块没有类型的存储空间,即缓冲区。xxxArray是视图,对ArrayBuffer的包装,这里xxx可以是Int8、Uint8、Uint8Clamped、Int16、Uint16、Int32、Uint32、Float32、Float64、BigInt64和BigUint64。 键索引的容器 相等比较时,遵行下面的规则: 相等性比较类似===; -0与+0相等; NaN与自己相等(与===不同)。 Map Map对象存储键值对。可以用for...of遍历得到[key, value],顺序是插入的顺序。有如下属性和方法: set(key, value):设置键值对; get(key):查询键对应的值; has(key):测试键是否存在; delete(key):删除键值对; clear():清空Map; size:大小。 WeakMap的值只能是对象,对键的持有是weak的,也就是说如果没有其他引用,这些对象会被键值对回收。 Set Set存储值的集合,可以用for...of以插入顺序遍历元素。有如下属性和方法: add(value):添加值; has(value):测试值是否存在; delete(value):删除值; clear():清空集合; size:大小。 可以通过Array.from(set)或[...set]从集合中创建数组;通过new Set(array)从数组中创建集合。 WeakSet与WeakMap类似。 对象 属性 JavaScript对象是属性的集合,属性包含了键值对,值可能是个函数,这就成了方法。 可以对属性赋值,访问未赋值的属性会得到undefined。访问属性可以用object.id或object[value],键不是合法标识符的属性只能通过方括号访问(包括空串)。键只能是字符串或Symbol类型,其他类型的键会被转化为字符串。 ES5开始,有3种遍历属性的方式: for...in:遍历所有的可枚举属性,包括原型链; Object.keys(o):返回所有属于该对象(不包括原型链)的可枚举属性的键的数组; Object.getOwnPropertyNames(o):返回所有属于该对象的键(包括不可枚举属性)的数组。 构造 创建对象有如下的方式: 使用对象初始化器:{property1: value1, ... },这里property1等可以是标识符、数字或字符串。如果是语句开始,需要加括号以避免和复合语句混淆。同样的对象初始化器产生的对象是不等的。所有对象字面表达式产生的对象都是Object的实例; 使用构造函数:通过构造函数定义对象(构造函数首字母应当大写),再通过new创建实例。定义时,使用函数,其内部可以用this指代对象,通过对this的赋值即可创建属性; 使用Object.create:它接受一个对象参数,返回新的对象,新对象的原型是该对象参数。 继承 JavaScript是基于原型的面向对象语言。JavaScript的所有对象都至少继承自另一个对象。被继承的对象称为原型。继承的属性都是来自构造函数的prototype对象。this指代调用对象。通过如下面所示的代码定义继承: function Base() { this.a = ...; this.b = ...; } function Derived() { Base.call(this); this.c = ...; } Derived.prototype = Object.create(Base.prototype); Derived.prototype.constructor = Derived; 其中的Derived函数也可以如下写,下面的base只是一个普通的名字: function Derived() { this.base = Base; this.base(); this.c = ...; } 当JavaScript遇到new操作符,它会创建一个对象,并将它的内部属性[[Prototype]]设置成构造函数的prototype属性,再将该对象作为this传递给构造函数。 当访问属性的时候,JavaScript先查看该对象是否有这个属性,有的话就采用,如果没有就查看[[Prototype]]属性的对象,如此继续下去。 如果给constructor.prototype添加属性,那么所有该构造函数的对象都会拥有这个属性。 getter和setter 对于对象字面量,可以通过如下方式构造getter和setter: var o = { get b() { return ...; } set c(x) { ... } }; 也可以通过如下方式创建getter和setter: Object.defineProperty(o, 'b', { get: function() { return ...; }, set: function(y) { ... } }); 也可以通过Object.defineProperties定义,形式如下: Object.defineProperties(o, { 'b': { get: function() { return ,,,; } }, 'c': { set: function(x) { ... } } }); 使用delete可以删除非继承属性。 只有当两个对象是同一个对象,才相等。 使用Pormise Promise用于异步函数回调,其构造函数接受一个函数,形如(resolve, reject) => ...,如果成功则调用resolve可以传递一个值;如果失败则调用reject也可以传递一个值。 Promise拥有如下方法: then(onFulfilled, onRejected):调用指定的handler,返回一个新的promise用于形成链。如果onFulfiled不是函数,则以“Identity”替代;如果onRejected不是函数,则以“Thrower”替代。如果指定的handler 返回一个值,则then返回的promise以该值resolve; 不返回值,则then返回的promise以undefined resolve 抛出异常,则then返回的promise以该异常reject; 返回一个已经resolve的promise,则then返回的promise以该promise resolve的值resolve; 返回一个已经reject的promise,则then返回的promise以该promise reject的值reject; 返回一个pending的promise,则then返回的promise的resolve/reject将紧随handler返回的promise的resolve/reject。 catch(onRejected):等价于then(null, onRejected)。 ES2017加入了async/await语法糖,形如: async function foo() { try { const result = await doSomething(); ... } catch (error) { ... } } await后面根一个promise,如果该promise resolve了,则返回resolve的值;如果reject了,则以异常抛出的形式抛出reject的值。async函数返回一个promise。 当promise被reject时,会有以下两个消息中的一个发往全局对象(window)或者Worker(process): rejectionhandled:当一个promise reject,并被处理时发出; unhandledrejection:当一个promise reject,但没有handler时发出。 在这两种情况下,会有一个PromiseRejectionEvent对象作为参数,它有promise和reason两个属性。 注意:Node.js与上面描述的有些许不同。 此外还有如下4个函数: Promise.resolve和Promise.reject:创建一个已经resolve或已经reject的Promise; Promise.all和Promise.race:执行数组所有的promise或竞争。 即使是已经resolve的对象,传递给then()的函数也会异步调用(过一会调用)。 迭代器和生成器 迭代器 所谓迭代器,就是有next()方法,返回一个有两个属性的对象: value:序列中的下一个值,当done为true时可省略; done:最后一个值是否已经被获取(true时,迭代器位于past the end)。 可迭代对象是值实现了Symbol.iterator返回一个迭代器的对象。 String、Array、TypedArray、Map和Set都是可迭代对象。 生成器 生成器形如下面的代码,函数内可以yield多次: function* foo() { ... yield value; // or yield* iterable; ... } 它会返回一个生成器,它是可迭代对象,每个生成器只能遍历一次。实际上生成器的Symbol.iterator方法返回了它自己。 next()方法还接受一个值,这个值通过result = yield value得到。调用throw()方法还可以在生成器中抛出异常。 元编程 Proxy 在ES6中,Proxy对象可以可以拦截某些操作,实现自定义行为。 使用方法为var newObject = new Proxy(oldObject, handler);,这里handler为一个对象,其方法就是trap(陷阱)。trap的行为必须遵守invariants。关于trap、它所拦截的对象和invariants见Meta programming - JavaScript | MDN。 使用Proxy.revocable可以创建一个可撤销的Proxy。它返回一个对象,其proxy属性是代理的对象,其revoke()方法可以撤销代理。 Reflection Reflect主要是将对象的操作变成函数,并与Proxy对象的方法一一对应。包含如下的函数: Reflect.has():检查是否有属性; Reflect.apply():将this和参数列表应用到函数; Reflect.defineProperty():与Object.defineProperty()不同,如果失败不抛出异常,而是返回false。 模块 命名导出:可以在定义变量、函数、类的时候用export导出,如export const foo = 42;。也可以在文件最后用花括号扩住、逗号分隔的列表导出,如export { foo, bar }。 使用import { foo, bar } from 'path'即可导入,其中path是相对或绝对(即相对于站点根目录)路径。 使用了模块导入导出的主模块,需要按照如下方式导入HTML: <script type="module" src="path"></script> 注意,模块和标准脚本是不同的,不添加type="module"会使import和export语句报错。此外还有如下不同: 在本地加载模块会遇到CORS错误,而标准脚本不会; 模块默认是strict模式; 默认为defer的; 从主模块导入的模块对外部是不可见的。 默认导出:通过export default expression;可以默认导出。再通过import name from 'path';或者import {default as name} from 'path';导入。 export和import都可以带别名,如export { foo as bar };和import { bar as foo } from 'path';。 还可以创建模块对象,形如import * as Module from 'path';。 还有聚合模块,用以将多个模块聚合在一起。有这样的语法:export * from 'path';和export { name } from 'path';。 使用import()函数可以动态加载模块,它返回Promise。

2019/5/13
articleCard.readMore

Java学习笔记

该学习笔记着重于Java相较于C++等编程语言的独特之处。内容主要来自 The Java™ Tutorials 和 JAVA8 官网笔记教程 。 语言基础 变量 变量分为以下4种类型,其中前2种合称为字段: 实例变量(非静态字段) 类变量(静态字段) 局部变量 参数,包括构造器和异常处理器 一个类型的字段、方法和嵌套类型统称为成员。合法的标识符由大小写敏感的字母、数字、$和_组成,其中数字不能作为第一个字符,不建议使用$。不建议以_开头。标识符不能与关键字相同。 建议:对于变量一般采用驼峰命名法,即首字母小写后续单词首字母大写;对于常量一般全部大写,_分隔各个单词。 基本数据类型 共有8种基本类型byte(1字节)、short(2字节)、int(4字节)、long(8字节)、float(4字节)、double(8字节)、boolean和char(2字节)。int和long的包装器Integer和Long提供了诸如compareUnsigned和divideUnsigned之类的无符号操作。java.math.BigDecimal提供了精确的实数。基本数据类型不存在无符号类型,除了boolean之外的基础数据类型大小固定。此外,双引号括住的字符串默认是java.lang.String类,它是不可变的。 默认值 未被初始化的字段会被初始化为0(基本数据类型)或者null(对象)。局部变量不会自动初始化,访问未被初始化的局部变量将导致编译错误。不建议依赖自动初始化。 字面常量 字面常量代表一个固定值,可以直接向基本类型的变量赋值(无需new)。 整型字面常量 若后缀为L或者l,则其类型为long,否则为int。建议使用大写L,避免和数字1搞混。可用int类型的字面常量初始化byte、short、int和long。long类型的字面常量只可初始化long。默认为10进制。0X或者0x前缀表示16进制;0B或者0b表示2进制;0开头的表示8进制。 注意: 大小超过int类型的字面常量却未指明是long类型是一个编译错误。 浮点型常量 若后缀为F或者f,则其类型为float;若不存在后缀或后缀为D或者d,则其类型为double。 字符和字符串常量 char和String类型可以存储任何Unicode(UTF-16)的字符。可直接在源代码中使用这些字符,也可以\uXXXX(XXXX为4位16进制数字)转义。使用单引号代表字符;使用双引号代表字符串。 此外还有转义序列,如\b(退格符)、\t(水平制表符)、\n(换行符)、\f(换页符)、\r(回车符)、\"(双引号)、\'(单引号)、\\(反斜杠符)。 null字面常量可赋值给任何非基本数据类型变量。<typename>.class用于得到类型本身(其类型为Class),这被称作类字面常量(class literal)。 在数字字面常量中使用下线符 可以使用_分割数字(包括整型和浮点型常量),增加代码可读性,只允许在两个数字之间插入一个或多个_。 数组 Java的数组定长。里面的每一项目称为元素,通过下标索引(从0开始)。通过<type>[] <identifier>;定义一个可以引用数组的变量(数组大小不是类型的一部分)。通过new <type>[<length>]来创建数组(内容会被0初始化)。可以用{ <value1>, ... }初始化;可使用new <type>[] { <value1>, ... }赋值,其中最后一个,可选。支持多维数组及其初始化,其低维数组长度可不一样。<array>.length可用来得到数组长度。 注意:将[]至于标识符后面也是可行的,但并不推荐。需要注意两者在遇到多变量定义时的差别,前者对所有变量都将产生数组的修饰作用,后者只对其所属的那一个变量产生作用。 采用java.lang.System的以下函数复制数组: public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) 此外java.util.Arrays提供了很多数组的工具,诸如copyOfRange(返回新数组)、binarySearch、equals、fill、sort和parallelSort(并发排序)。 运算符 运算符优先级为后缀、单目、算术、移位、关系、按位、逻辑、条件、赋值;除赋值运算符外,结合性自左至右;赋值运算符结合性自右至左。这些和C/C++一致。 +和+=用于连接字符串。==运算符判断是否值相等(基本类型)或者是否为同一对象(包括String);equals()方法判断对象(非数组)内容是否一样(equals()的默认行为等价于==运算符)。instanceof运算符只能用于判断是否是类和数组(元素是类)的实例,它与关系运算符(偏序而非等价)具有同一优先级。null不是任一类型的实例。boolean类型只可参与逻辑和双目按位运算。数学类型(包括char类型)不可参与逻辑运算,不可强制转型为boolean类型,其中浮点数类型不可参与位运算。boolean类型和数字类型不可互相转化。选择和循环语句的条件判断必须是boolean类型。逻辑运算符遵循短路原则。>>运算符采用算术移位;>>>运算符采用逻辑移位。窄化转换必须采用显示的类型转换。混合类型运算会自动进行类型提升,算术等运算中的byte、short和char类型会被直接提升至int类型。Java没有sizeof运算符。逗号运算符只能用于for循环的初始化和步进表达式处,可在初始化表达式中定义多个同一类型的对象。默认整型常量为int类型;默认浮点型常量为double类型。对象的赋值运算符仅复制对象引用。 表达式、语句和块 语句分为两类声明语句和流程控制语句。 只有以下4种表达式能作为表达式语句: 赋值表达式; 自增和自减; 方法调用; 对象创建表达式。 流程控制语句 switch语句可以测试整型及其包装类,枚举类型或者String对象(SE 7)。对于String对象会调用String.equals判断是否相等。如果switch语句中的测试表达式为null,会抛出NullPointerException,应当避免这种情况的发生。 增加了foreach的语法,可用于任何可迭代对象。标签可置于迭代语句前,从而使带标签的break <label>;和continue <label>;语句跳转至制定的循环层次。 类和对象 类 声明类 类大致按照如下声明: <accessModifier> class <name> extends <superClass> implements <interface1>, ... { // body } 其中<accessModifier>为public,private等等,可以省略。extends <superClass>和implements <interface1>, ...也可以省略。 声明成员变量(字段) 字段声明形如<accessModifier> <type> <name>;,其中<accessModifier>可以是public,private等等,也可以省略。 声明方法 方法声明如下: <accessModifier> <returnType> <name>(<paramType> <paramName>, ... ) { // body } 一般而言,方法应当是个动词后面可跟名词或形容词,采用驼峰命名。方法可被重载,根据方法签名(形参列表)区分。注意: 返回值不属于方法签名。 为你的类提供构造函数 类通过与之重名的构造器进行初始化,与方法一样也可重载,它没有返回值,除此之外声明和方法一样。当类没有其他构造器的时候会有一个默认的无参构造器,如果它有父类,则会调用父类的无参构造函数。如果一个类没有父类,那它隐式地含有父类Object。如果父类没有无参构造函数,子类没有构造函数会造成编译错误。如果一个类没有访问某个类构造函数的权限,那它不能创造该类的对象。 传递信息给方法或构造函数 如果你想要将一个方法传递给另一个方法,请使用lambda表达式或方法引用。 通过函数的最后一个参数采用<type>... <identifier>的形式,可以实现可变参数列表,其中identifier是type类型的数组,对于基本类型会自动包装,也可传递与之对应的数组。 参数名字在其作用域内必须是唯一的,它会覆盖同名的字段,良好的风格应避免这种事发生。 基本类型的传参使用按值。引用数据类型参数也是传值调用,但它的内容可能发生变化。 对象 创建对象 通过new <className>(<arg>, ...)创建对象。使用<type> <name>;可以声明一个指向对象的变量。 使用对象 对于类内的方法,可直接使用名字访问字段;对于类外的方法,可采用<objectReference>.<fieldName>访问字段。调用方法也与之类似。不再使用的对象会被垃圾回收。` 更多关于对象 从方法内返回值 使用return <value>;返回值,对于返回值为void的方法,则return语句不包含<value>。return的类型必须与方法的返回值类型一致,或是返回值类型的子类,否则是编译错误。子类方法的返回值可以是父类被覆盖方法返回值的子类,这称之为协变返回类型。 使用this关键字 在方法或构造函数内,this指向当前对象。通过this()在构造器内的第一行处调用其他构造器。 类成员的访问权限控制 访问权限从大到小依次为public、protected、包访问权限和private。有两层访问权限控制: 顶层:public或包访问权限。 成员层:public、private、protected或包访问权限。 下面的表展示了访问权限: Modifier Class Package Subclass World public Y Y Y Y protected Y Y Y N no modifier Y Y N N private Y N N N 理解类成员 这里讨论的是静态字段和方法。使用static修饰字段或方法可令其称为静态字段或方法。可以通过类名(以及实例)访问。和final(表示不可变)结合,可以用于定义常量。 初始化字段 可通过包含前向引用的表达式初始化字段。初始化顺序按照定义顺序。可在构造器内进行赋值。可通过类内的static { <statements> }静态初始化块初始化静态成员。非静态字段可通过类内的{ <statements> }初始化块初始化。一个类可以拥有多个初始化块,按照其出现的先后次序依次执行。类的静态成员在类被第一次使用时初始化。使用final方法的返回值可以被用于初始化字段,调用非final方法进行初始化可能会造成问题。 Java中类的字段、static方法和final方法施行前期绑定,其余都施行后期绑定。初始化的顺序为: 将整个对象初始化为全0(对于对象引用则会拥有null); 递归初始化父类; 依次按照初始化语句(定义先后顺序),初始化块和构造器初始化对象。 注意: 如果在父类构造器内部调用子类方法,这时子类获得的是一个半初始化的对象。因此应当避免在类的构造器内调用可被后期绑定的方法。 嵌套类 Java可在类内定义另一个类,这样的类称为嵌套类。有两类嵌套类: 静态的:静态嵌套类 非静态的:内部类 嵌套类的权限可以是private、public、protected或包访问权限。 有两种特殊的内部类: 局部类 匿名类 内部作用域会屏蔽同名的外部作用域名字。 注意: 对内部类(包括局部类和匿名类)的序列化是强烈不建议的。 内部类 内部类可访问外部类的所有字段。外部类的非静态方法可以直接用内部类的名称访问内部类,除此之外,访问内部类需要使用<outer>.<inner>。除了编译期常量表达式初始化的static final字段,内部类不允许有static成员(包括接口)。 注意:内部类的实例化必须与外部类对象相关联。 这也就是说,内部类可以通过内部类的方法、外部类的非静态方法以及.new的语法实例化。 在内部类方法内使用<OuterClass>.this访问外部对象。在外部类静态方法或外部类外,使用<outerObject>.new <InnerClass>通过外部类对象引用创建内部对象。如果继承自内部类,必须在子类构造器内使用<enclosingClassReference>.super()。如果继承自外部类,内部类的名称是前期绑定,所以不存在内部类的多态。 静态嵌套类 静态嵌套类通过<OuterClass>.<StaticNestedClass>访问,不可直接访问外部类的非静态字段。静态嵌套类的行为同顶级类的行为一样。静态嵌套类可嵌套于接口中。 局部类 定义在方法或者块内的类称为局部类。局部类没有访问权限修饰,其作用域与局部变量一致。除了编译期常量表达式初始化的static final字段,局部类不允许有static成员。局部类可以访问方法内部的final或等价于final(不存在对变量的赋值)的局部变量和参数。定义在非静态方法内部的局部类可以访问外部类的所有成员,定义在静态方法内部的局部类只可以访问外部类的静态成员。 interface默认是static的。因而不存在局部接口。 匿名类 通过new <BaseClassOrInterfaceName>(<arguments>, ...) { <ClassDefinition> }。匿名类在访问权限、成员限制等方面与局部类基本一致,此外匿名类无法定义构造函数。 lambda 表达式 当一个接口只存在一个抽象方法的时候,可以用lambda表达式实现该接口。语法为(<parameters>, ...) -> <expression>或者为(<patameters>, ...) -> { <statements> },可以省略形参的类型,此外,若只有一个形参,可省略括号。和局部类及匿名类一样lambda表达式可以捕获同样的变量及参数。lambda表达式不产生新一层的作用域,因而在lambda表达式中覆盖外层变量是个编译错误。Java通过上下文判断lambda表达式的目标类型,包括: 变量声明 复制 返回语句 数组初始化 方法或构造器实参(重载判断和实参类型推断) lambda表达式的函数体 条件表达式 转型表达式 同样不建议对lambda表达式进行序列化。java.util.function提供了一些函数式编程的设施。可以使用聚合操作结合函数式编程处理流中的元素。 方法引用 当lambda表达式仅仅是调用函数时,可采用方法引用。共有4类方法引用,包括: 静态方法的引用:<ContainingClass>::<staticMethodName> 某一对象方法的引用:<containingObject>::<instanceMethodName> 某一类型任意对象同一方法的引用:<ContainingType>::<methodName> 构造器的引用:<ClassName>::new 枚举类型 通过enum <TypeName> { <CONSTANTS>, ... }创建枚举类型,其中最后一个,可选,通过<TypeName>.<CONSTANTS>访问常量。通过enum <TypeName> { <CONSTANT> (<constructArguments>, ...), ... ; private final <fields>; ... ; <ConstructorAndMethods> }为enum定制字段和方法。枚举可嵌套在接口中。枚举类型隐式地是从继承自java.lang.Enum的,拥有values()(static)、toString()和ordinal()等方法。enum类型的switch语句case字句必须为其该enum类型的常量。 注解 注解是一种元数据,对程序的执行不产生直接的影响。 基础语法 基本的形式为@Annotation(name = value, … ),其中内部的键值对称为元素,可分为有名的和无名的。对于只有一个键值对的注解可省略为@Annotation(value),对于没有键值对的注解,可省略括号。注解可应用注解可应用于字段、方法和类。Java SE 8中,注解还可应用与类型的使用处,这被称为类型注解,如: new表达式 类型转换 implements子句 异常声明 声明注解类型 通过@interface <AnnotationName> { <Type> <name>() default <value>; … }声明注解类型,其中default <value>可省略,支持数组。可通过@Documented使定义的注解类型出现在Javadoc生成的文档里。注解的访问权限可为public或包访问权限。 预定义注解 在java.lang预定义了以下注解:@Deprecated、@Override和@SuppressWarnings。@Deprecated 表明所标识的元素不被建议使用,当使用这个元素时,编译器会产生一个警告。建议在这类元素的注释中添加@deprecated标签,注明不被建议的原因。@Override 提示编译器某个元素需要覆盖其父类的元素。若没有覆盖,编译器将产生一个错误。@SuppressWarnings(value = <StrWarning>) 指示编译器抑制某个警告。在Java中,警告分成两类deprecation和unchecked。可通过@SuppressWarnings({“unchecked”, “deprecation”})同时指定多种错误。@SafeVarargs 应用于方法或者构造器,用于抑制潜在对varargs不安全操作的警告。@FunctionalInterface 提示编译器某个类型声明被期望是个函数式接口。 那些应用于其他注解的注解称为元注解。java.lang.annotation定义了一些元注解。@Retention(value = EnumValue) 指定被标注的注解如何存储: RetentionPolicy.SOURCE:只留存在源代码内,被编译器忽略; RetentionPolicy.CLASS(默认):只保留到中间代码内,被JVM忽略; RetentionPolicy.RUNTIME:保留到JVM中,可被运行环境使用。 @Documented 当元素使用这个标签时,会被Javadoc记录进文档。@Target(value = EnumValue) 指示该标签可被应用的元素: ElementType.ANNOTATION_TYPE:应用于其他注解类型; ElementType.CONSTRUCTOR:应用于其他构造器; ElementType.FIELD:应用于字段或者属性; ElementType.LOCAL_VARIABLE:应用于局部变量; ElementType.METHOD:应用于方法; ElementType.PACKAGE:应用于包声明; ElementType.PARAMETER:应用于方法的参数; ElementType.TYPE:应用于一个类的任一元素。 @Inherited 指示该注解会被子类继承,默认是不会继承的。只对应用于类声明的注解有效。@Repeatable(value = ContainerAnnotationClass) 指示该注解可重复多次。 可重复注解 可重复注解会被保存在一个容器注解内。注解容器内部必须有一个数组类型的value元素。 接口和继承 接口 接口与类类似都是引用类型。接口只能包含常量、抽象方法签名、默认方法、静态方法和嵌套类型。只有默认方法和静态方法有函数体。接口无法被实例化。 接口定义如下: <accessModifier> interface <name> extends <Interface> ... { // body } 接口体内部的抽象方法没有函数体,默认方法由default修饰,静态方法由static修饰。其方法隐式地为public的,其字段隐式地为public static final的。 对于类,使用implements关键字可以实现接口。 向接口添加默认方法和静态方法,接口的使用者无须重新编译。 可以在类中嵌套接口,此时该接口的访问权限可以为public、protected、包访问权限和private,接口为隐式地静态成员。也可以在接口中嵌套接口,此时其访问权限隐式地为public的。不可以在方法以及块中嵌套接口。 继承 通过extends继承。除了Object类外,所有的类都有父类,如未显式指明,其父类即为Object。 子类继承了父类的public和protected成员,如果位于同一个包,它还会继承包权限的成员。可以使用、替换、遮蔽它们。 子类向父类的转型可以是隐式转型,而父类向子类的转型必须是显式转型。可以使用instanceof运算符避免显示转型的运行时错误。 Java对所有转型都会进行检查,包括向下转型(RTTI)。若无法向下转型,抛出ClassCastException异常。 Java中,最多只能继承自一个类,但可实现多个接口。 覆盖和遮蔽方法 可以在子类中重载或覆盖父类方法,@Override注解确保是覆盖父类方法。被覆盖的方法返回值可是子类方法返回值的派生类型。其访问权限可扩大,不可缩小。 父类实例方法 父类静态方法 子类实例方法 覆盖 编译错误 子类静态方法 编译错误 遮蔽 类的extends与implements冲突: 实例方法比接口默认方法更为优先; 类的多个implements冲突: 被覆盖的接口默认方法被忽略(菱形继承);其余情况即多个default方法冲突,或default方法与abstract方法冲突时,编译器报错。 使用关键字super 可使用super访问父类的成员。可通过super()在构造器内的第一行处调用父类构造器,若无则自动调用父类的无参构造器。 将Object类作为父类 所有的类在无特殊指明时,都从Object类继承。以下是这个类的一些方法: protected Object clone() throws CloneNotSupportedException 创建并返回对象的拷贝。 public boolean equals(Object obj) 判断对象是否与另一个相等。 protected void finalize() throws Throwable 回收前被垃圾回收器调用。不建议在其中释放资源。 public final Class getClass() 返回对象运行时的类。 public int hashCode() 返回一个对象的散列值。 public String toString() 返回一个代表对象的字符串。 Object的clone()方法在对象未实现Cloneable接口时会抛出CloneNotSupportedException异常;如果实现了,默认的clone()方法会创建一个与原对象拥有相同值的新对象,包括相同的对象引用。因此,对于含有对象引用的对象应当覆盖默认的clone()方法。 getClass()方法返回一个Class对象,拥有getSimpleName()、getSuperclass()、getInterfaces()、isAnnotation()、isInterface()、isEnum()、getFields()、getMethods()等方法。 如果两个对象相等,那它们的散列值必须相同。 final类和方法 final变量初始化后不可改变其值,(对于对象的引用,引用不可改变,但对象可以改变)。final参数由函数传参初始化,类的final字段可在初始化快内或构造器内初始化。未初始化的final变量是个编译错误。 注意:对于编译时已知的常量(编译期常量表达式初始化的static final字段),编译器会在原处展开其值。若该常量定义处发生变更,需要对使用此常量的所有源文件重新进行编译。 final方法不可覆盖。所有的private方法都隐式地是final方法。对于父类和子类中签名相同的private方法,两者独立不构成覆盖。final类不可继承。其所有的方法都隐式地是final方法。 注意: 构造函数调用的方法应该都是final的,否则子类覆盖方法会产生意想不到的结果。 抽象方法和类 通过abstract声明一个抽象方法,含有抽象方法的类称为抽象类,抽象类必须被显示也声明为abstract的。通过abstract声明一个抽象类,可避免此类被实例化。 数字与字符串 数字 标准库提供了包装器类,当需要对象却只有基本类型的时候,会自动打包,同样,当需要基本类型却只有对象的时候,会自动解包。 所有的数字类型继承自Number,包括Byte、Integer、Double、Short、Float、Long以及用于高精度的BigDecimal和BigInteger和用于多线程操作的AtomicInteger和AtomicLong。以下是Number类具有的方法: byte byteValue() short shortValue() int intValue() long longValue() float floatValue() double doubleValue()将Number对象转型成基本数据类型。 int compareTo(Byte anotherByte) int compareTo(Double anotherDouble) int compareTo(Float anotherFloat) int compareTo(Integer anotherInteger) int compareTo(Long anotherLong) int compareTo(Short anotherShort)将Number对象与实参比较大小。 boolean equals(Object obj)判断两个对象是否相同。 以Integer类为例,每一个Number子类还包含以下的几个方法: static Integer decode(String s)解析整数,可接受10进制、8进制和16进制。 static int parseInt(String s) static int parseInt(String s, int radix)返回整数,radix可接受10进制、2进制、8进制和16进制。 String toString() static String toString(int i)返回对象对应的String对象。 static Integer valueOf(int i) static Integer valueOf(String s) static Integer valueOf(String s, int radix)返回对象对应的Integer对象。 可通过System.out.printf或String.format格式化输出字串,与C语言中的使用方法基本相同。以下是不同之处: %n:与平台相关的换行符。不建议使用\\n。 时间格式化符号:对于Calendar对象,有以下格式化符号: tB:区域特定化的月份全名; td:2位数的日期,必要时带前导0; te:2位数的日期,不带前导0; ty:2位数的年份; tY:4位数的年份; tl:12小时制的小时; tM:2位数的分钟,必要时带前导0; tp:区域特定化的上下午; tM:2位数的月份,必要时带前导0; tD:%tm/%td/%ty。 ,:区域特定化的数字分割符。 可通过java.text.DecimalFormat类格式化数字。java.lang.Math则提供了更多数学相关的常数和方法。 字符和字符串 Character类和String类是不可改变的。java.lang.Character提供了字符处理的一些方法。转义字符与C语言相同。java.lang.String提供了字符处理的一些方法,其中format()和输出有相同的使用方法。StringBuilder是一种可以修改的字符串。 泛型 泛型使得类型可以作为类、接口、方法的参数。使用泛型可以有以下好处:更强的类型检查、消除转型等等。 泛型类型 泛型类型是指泛型的类或接口。(这里不再用尖括号代表可变动项)通过“class name<T, … > {}”和“interface name<T, … > {}”定义泛型类和泛型接口。其中T称为类型变量,不可为基本类型变量。通过 “name<t, ...>”调用泛型类和接口。当可从上下文推断类型时,可以只留一对空尖括号,这被称为钻石。 原始类型是泛型类或接口不带类型参数。这和它的参数类型是Object等价。不建议使用原始类型。 泛型方法 在函数返回值类型前面添加<T, ...>可以创建泛型方法。可以使用类型推断,省略函数的尖括号。类型推断会根据函数的实参、返回值推断一个最具体的类型。 受限类型参数 可以限定类型变量必须是某些类型的子类,这被称为受限类型参数,格式为<T extends S1 & S2 ... >,这里extends也被用于接口。 通配符 使用通配符可以限定泛型的类型。 name<? extends T>:上限通配符(T也可以是接口),输入变量通常使用。 name<?>:无限定通配符。当输入变量只使用了Object类方法的时候使用。 name<? super T>:下限通配符,输出变量通常使用。 如果既输入又输出的变量,不使用通配符。 编译器推断通配符具体类型的过程称为通配符捕获。 类型擦除 为了实现泛型,Java编译器做了如下的事: 将所有类型参数替换成限定的的类型(Object如果没有),产生子界面 如果需要,插入类型转化 产生桥接方法以保证多态性 包 通过在源文件的第一行添加package <packagename>;组织成一个可被import包。其中的<packagename>必须和该文件的目录名保持一致(由.充当/分隔)。一个源文件内至多有一个public类,该类名必须与文件名相同(区分大小写)。其余类拥有包访问权限。 注意:包的本质是一个独立的模块,一个模块则是由一组类组成的。这个模块在Java中被组织成一个目录,这组类被组织为同一目录下的不同Java源文件,源文件与一个公开类一一对应。包既是模块化的手段,也是避免冲突的一种实现方法。 当一个源文件没有声明属于某一包,则该源文件属于这个目录下的默认包。通过import <packagename>.<classname>;导入某个类,也可通过import <packagename>.*;导入整个模块,这时访问这些类不带模块的名称和路径。通过静态导入import static <packagename>.<classname>.<membername>,可仅导入某个类的所有静态成员,也可通过import static <packagename>.<classname>.*;导入某个类的所有静态成员,这时访问这些成员不带类名。 异常 异常如下分类: Checked Exception:应当被处理的异常 Unchecked Exception:包含以下两类 Error及其子类:致命的错误 RuntimeException及其子类:程序逻辑上的错误 对于Checked Exception,方法要么捕获它,要么提供throws抛出异常列表的声明。对于Unchecked Exception则没这类限制。 异常处理格式如下: try { // try body } catch (ExceptionType name) { // catch body } finally { // finally body } try-with-resources的格式如下,括号内结尾无分号,可以跟catch块,AutoClosable是: try ( AutoClosable name = new ; ... ) { // try body } 在函数右括号后面throws Exception, ...可以指定方法抛出的异常。 抛出异常的格式为throw throwableObject,其中throwableObject必须是Throwable的子类。 并发 有两种方法创建线程,推荐前者 实现Runnable接口的run函数,并将该对象提供给Thread构造函数。 派生Thread类,实现其run函数。 Thread.start可以启动线程;Thread.sleep可以睡眠,睡眠可能有InterruptedException;Thread.interrupted可以判断线程是否中断;Thread.interrupt发起中断;Thread.join等待线程退出。 在方法的返回类型前添加synchronized即创建了同步方法,一个对象的所有同步方法同一时刻只能有一个在执行。构造函数不能是同步方法。 使用synchronized(object) { statements }可以创建同步块,object的所有同步块同一时候只能有一个在执行。

2019/4/11
articleCard.readMore

Haskell学习笔记(未完待续)

这篇文章是我Haskell的学习笔记,内容来自《Learn You a Haskell for Greate Good》中文版。 起步 使用ghci打开REPL交互环境。可以输入表达式常看其结果。通过:l module可以载入模块。 支持的运算符有一元负号-(唯一的一元运算符),双目运算符有算术+、-、*、/(参数为Num类),关系==、不等于/=(参数为Eq类)等等。 如果表达式存在负数常量,最好用括号括起来(5 * -3会报错,5 * (-3)则不会)。对于5之类的数,它既可以是整数也可以是浮点数。 函数有中缀函数(运算符)和前缀函数。调用前缀函数的格式为func arg1 arg2 ...。函数应用拥有最高优先级。二元函数可以使用中缀函数的形式调用arg1 `func` arg2。 函数定义的格式为func param1 param2 ... = expression。函数定义没有顺序的概念。函数名可以包含一些符号,但不能大写开头。函数一经定义,不能改变。 有如下函数: succ和pred:一元,获取后继和前趋,参数为Enum类; min和max:二元,获取最小和最大值,参数为Ord类; div和mod:二元,整数取整和求余,参数为Integral类。 if表达式形如if boolExpr then expr1 else expr2,else不可省略。 列表是单类型的数据结构。使用[elem1,elem2,..,elemN]的方式定义列表。字符串是列表的语法糖。使用list ++ list可以连接列表,其复杂度正比于左边列表的长度。使用elem:list可插入元素到列表头部,[1, 2, 3]实际上是1:2:3:[]的语法糖。使用list !! index可以索引元素(index为Int类型)。列表可以嵌套,但类型必须一致。列表可以比较,采用字典序。 列表有以下函数: head list:返回list的第一个元素; tail list:返回list中除第一个元素的剩余部分(是一个列表); init list:返回list中除最后一个元素的剩余部分(是一个列表); last list:返回list的最后一个元素; length list:返回list的长度(Int类型); null list:返回list是否为空; reverse list:返回翻转的list; take num list:返回list中的前num个元素(列表长度可小于num); drop num list:返回list删除前num个元素的结果(列表长度可小于num); maximum list和minimum list:返回最大最小值; sum list和product list:返回加和和乘积; elem value list:返回value是否在list中,通常以中缀函数调用。 可以使用[elem1..elemN]产生一个区间,其元素为Enum,如果需要更改步长,可以采用[elem1,elem2..elemN]。使用[elem1..]或[elem1,elem2..]可创建无穷列表,可以充分利用Haskell的惰性求值。注意浮点数可能有误差。 有以下的生成列表的函数: cycle list:创建一个循环list的无穷列表; repeat elem:创建一个循环elem的无穷列表; replicate count elem:创建一个将elem循环count次的列表。 使用[expr | bindOrPredict, ...]可以创建列表推导式,其中bindOrPredict为形如name <- list的绑定或Bool表达式的谓词,expr和谓词都可以含有绑定创建的名字,可以有多个谓词和绑定,可以嵌套。 元组将多个不同类型的值合成为一个值。使用(elem1,elem2,..,elemN)的方式定义元组。不同长度、不同元素类型的元组都是不同的类型。元组可以比较。元组是固定大小的。不存在单元素的元组。 元组有以下函数: fst tuple和snd tuple:tuple为二元组,分别返回第一个元素和第二个元素; zip list1 list2:将两个列表交叉配对,返回元素为二元组的列表,较长的列表会被截断。 类型和类型类 在GHCi使用:t expr查看表达式的类型。列表的类型为[type],元组的类型为(type1, ... , typeN)。可以给函数显示的类型声明,形如func :: argType1 -> ... -> argTypeN -> retType。函数类型中可以包含类型变量(小写开头),这样的函数称为多态函数。 有如下基本类型: Int:有界的与机器相关的整数; Integer:无界的高精度整数; Float:单精度浮点数; Double:双精度浮点数; Bool:布尔值; Char:Unicode字符。 类型类定义接口。一个类型可以是类型类的实例。带类型类的函数签名形如func :: (TypeClass1 typeVar1, ..., TypeClassN typeVarN) => argType -> ... -> retType,其中=>左侧的东西称为类型约束。 有如下类型类: Eq:定义了==和/=; Ord:是Eq的子类型类,还定义了<、>、<=和>=,compare比较两个Ord类型类的值返回Ordering类型的结果,它有3个值GT、LT和EQ; Show:定义了show(将参数转为字符串); Read:定义了read(将字符串参数转换为实例,一般需要配合类型注解,形如expr :: Type); Enum:定义了pred和succ,包含()、Bool、Char、Ordering、Int、Integer、Float和Double; Bounded:定义了多态常量minBound和maxBound,如果元组的每个元素类型都属于Bounded的实例,那么该元组的类型也是Bounded实例; Num:是Show和Eq的子类型类,定义了数值运算,包含Int、Integer、Float和Double; Floating:包含了Float和Double; Integral:包含了Int和Integer,可以通过fromIntegral将Integral类型类的实例转换为Num类型类的实例。 函数的语法 模式匹配可以检查数据是否匹配模式,并从模式中解析出数据。其形式类似额外的函数定义,只不过参数不再是单一变量。模式匹配自上而下,通常结尾包含万能模式。除了函数定义之外,列表推导式中的绑定也可以使用模式匹配。失败的模式匹配是个运行错误。有以下的几种匹配: 常量:参数为常量,匹配常量; 元组:参数为(var1, ..., varN),匹配一个元组; 列表:参数为x1:...:xN:xs,其中xs也可以为[],匹配一个列表的前几个(或全部)元素。 此外还有特殊的模式,叫as模式,形如var@pattern,可以保留整体引用。 模式是检查参数结构是否匹配,哨卫检查参数的性质是否为真。其形式如下: func params | predict1 = expr1 ... | predictN = exprN | otherwise = expr 如果所有哨卫都没通过,且没有otherwise作为万能条件,就转入下一个模式。其实otherwise就是True 使用where绑定可以将定义置于函数的末尾,也可以跟在哨卫的后面。形如: func params = expression where var1 = expr1 ... varN = exprN 其名字的作用域只对函数可见,名字必须对齐在同一行。where绑定中也可以使用模式匹配。 使用let表达式可以定义局部变量(或函数),不能与哨卫配合使用。形如let bindings in expr。也可以使用模式匹配。在列表推导式中,可省略in绑定局部变量。GHCi中let的in部分也可以省略,名字会在整个回话中存在。如果bindings有多个,可以采用分号分隔,末尾分号可选。此外列表推导式也可以使用不带in字句的let表达式。 使用case表达式可以对表达式进行模式匹配,形如: case expression of pattern1 -> result1 ... patternN -> resultN 函数参数的模式匹配是case表达式的语法糖。 递归 递归在Haskell中有广泛的应用。 zip函数接受两个列表,返回一个元素是二元元组的列表,较长的列表会从中间断开。 高阶函数 Haskell的所有函数都是柯里函数(只有一个参数)。函数签名中的->是右结合的。可以以部分参数调用某函数,得到一个部分应用。通过截断,可以对中缀函数进行部分应用。将一个参数放在中缀函数的一侧,即可截断中缀函数。用括号括住一个二元运算符即可得到二元函数。 有以下函数: zipWith func list1 list2:接受一个二元函数和两个列表,返回一个列表,其元素是两个列表的元素作为func参数的结果; takeWhile func list:func是谓词,返回满足func的list的最长前缀; flip func:接受一个二元函数,返回一个二元函数,交换它的两个参数; map func list:接受一个一元函数和一个列表,返回一个列表,其元素是list的元素作为func参数后的结果; filter func list:接受一个谓词和一个列表,返回一个列表,其元素为list元素作为func参数结果为真的那些参数。 lambda函数形如\param1 ... paramN -> expr,可以创建一个临时的函数。 Haskell有折叠函数,将列表归约为单个值。左折叠无法处理无限列表,右折叠可以。有如下函数: foldl func init list:左折叠,func为二元函数,第一个为累加值,第二个为list的元素; foldr func init list:右折叠,func为二元函数,第一个为list的元素,第二个为累加值; foldl1 func list和foldr1 func list:左折叠和右折叠,无初始值; scanl func init list和scanr func init list:左扫描和右扫描,与折叠类似,不过值会被累积地记录在一个列表中; 函数应用符$的定义为f $ x = f x,右结合,具有低优先级,可以用来减少括号。 函数组合.的定义为f . g = \x -> f (g x),右结合。 模块 导入模块的语法为import module。在GHCi中,可以通过:m + module1 ... moduleN。如果只导入一个模块的某几个函数,可以用import module (func1, ..., funcN)。如果导入一个模块除某些函数之外的函数,可以用import module hiding (func1, ..., funcN)。如果希望导入的东西都需要模块名前缀,即限定导入,可以用import qualified module,也可以设置别名import qualified module as alias。 Data.List模块里有以下函数: words str:以空白符为分隔,将str切割成字符串列表; unwords strList:以空格为分隔,合并字符串列表; group list:返回列表的列表,将相邻的相同元素组合成一个列表; sort list:给list排序 inits list和tails list:返回列表的列表,从左到右依次返回前缀的列表和后缀的列表; list1 `predict` list2,其中predict是isPrefixOf、isInfixOf和isSuffixOf:判断是否为前缀、中缀和后缀; foldl' func init list和foldl1' func list:严格左折叠,不延迟计算; find predict list:返回Maybe类型,表示是否在list中找到predict为真的元素。 Data.Char模块里有以下函数: ord char:返回char的Unicode码; chr num:返回num对应的Unicode字符; digitToInt char:返回16进制字符char对应的数字; isDigit:返回字符是否是十进制数字。 我们用Maybe a类型来表示一个可以为空或包含一个元素的类型。如果为空,就用Nothing,如果不为空就用Just value。 Data.Map模块提供了映射的数据结构Map k a,一般使用限定引入。它提供了以下函数: fromList list:list是关联列表(其元素是键值对元组),构建映射(重复键中的旧键会被忽略); lookup key map:在映射中查找键,返回Maybe包装的值; insert key value map:向映射插入键值对; size map:返回映射的大小; fromListWith func list:func接受两个值,返回一个值,list是关联列表,构建映射,如果有重复的键,调用func。 通过以下方式导出模块中的函数: module name ( func1 , func2 ... , funcN ) where 其中name也可以是path.name,name必须与文件名对应,path是.分隔的路径,必须与文件夹名称对应。 构造我们自己的类型和类型类 使用data Type = DataCtor1 Params1 | ... | DataCtorN ParamsN定义自定义类型。其中DataCtor1到DataCtorN是值构造器,可以有任意参数(包括无参数),它与普通函数没有差别。Params1到ParamsN是空格分隔的类型列表,表示值构造器的参数的类型。 可以使用DataCtor arg1 ... argN进行模式匹配。 在导出类型时,使用Type(DataCtor1, ..., DataCtorN)导出某几个值构造器,使用Type(..)导出所有值构造器,也可以不带括号,不导出值构造器。 使用记录语法可以方便地处理多字段的情况,格式如下: data Type = DataCtor { field1 :: Type1 , ... , fieldN :: TypeN } 这会自动生成提取字段的函数,如field1、fieldN。可以这样构建值,DataCtor {field1=value1, ..., fieldN=valueN},这也可以作为模式匹配。 自定义类型也可以带有类型参数,形如data Type a b ...,这里Type不是类型,而是类型构造器。填满类型构造器的所有参数就可以得到类型。可以在声明中添加类型约束,形如data (TypeClass1 typeArg1, ...) => Type a b ... = ...,但请永远不要那么做。 注意:如果一个类型没有参数,或参数填满,则称这个类是具体的。凡是值的类型都是具体的。 值构造器和类型构造器可以同名,但可以根据上下文区分,请勿搞混。 在自定义类型后面添加deriving (TypeClass1, ..., TypeClassN),可以将该类型作为类型类的实例。诸如Eq(先检查值构造器是否一致,再检查字段是否一致)、Show、Read、Ord(先比较值构造器,越靠前越小,再比较字段)、Enum、Bounded类型类都可以。 通过type NewType = SomeType可以创建类型别名,也可以带有类型参数,形如type NewType a b = ...。类型别名也可以$\eta$归约。 除了Maybe外,Either也经常用于错误处理,形式如下,错误使用Left,正确使用Right。 data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show) 还可以创建递归数据结构,这被用于构建列表和树。 通过下面所示的语法创建类型类,函数可以没有实现。许多类型类的函数互相调用,从而有最小的实现方式。 class TypeClassName typeParam where funcName :: funcType funcName params = ... ... 实现类型类的语法如下所示。 instance TypeClassName TypeName where funcName params = ... 也可以将一个类型类实现为另一个类型类的子类,这时语法形如class (TypeClass type) => SubTypeClass type where ...。类型类实例的类型也可以带参数,以Eq Maybe举例,形如instance (Eq m) => Eq (Maybe m) where。 通过GHCi中输入:info TypeClass或:info Type(或类型构造器)可以查看实现的类型类。、 Functor类型类定义如下: class Functor f where fmap :: (a -> b) -> f a -> f b fmap函数取一个函数和封装的数据,将函数应用到被封装的数据,返回结果。数组、Maybe、树、Either a都是函子。 类型有kind,类似于值有类型,可以再GHCi输入:k Type查看。 输入与输出 像诸如putStrLn返回的类型是IO (),其中()是空元组。可以通过do将多个I/O操作合并成为一个操作,以putStrLn和getLine为例,如下: main = do putStrLn "Hello, what's your name?" name <- getLine ... 第3行的操作叫绑定名字,它从容器中取出值,绑定到指定的名字。不能为最后一个操作绑定名字。与列表推导式类似,可以在do语句块中使用let。return取一个值作为参数,并将它包装到容器中。因此return ()的类型就是IO ()。return不会中断do代码块的执行。可以通过name <- return value来绑定名字,当然使用let更好。 有如下几个实用的I/O函数: putStrLn:打印字符串并换行; putStr:打印字符串不换行; putChar:打印字符不换行,可以借此递归实现出putStr; print:等价于putStrLn . show; when:定义于Control.Monad,取一个布尔值和一个I/O操作,如果布尔值为真则返回I/O操作,否则返回return (); sequence:接受一个I/O操作的列表,返回一个I/O操作,其内容是所有参数I/O操作执行后结果组成的列表; mapM:接受一个返回I/O操作的函数,和一个列表,将函数应用到列表的每一个元素上,再调用sequence,如果不关心返回值,可以调用mapM_; forever:接受一个I/O操作,返回一个永远重复执行该I/O操作的I/O操作; forM:与mapM相似,但参数顺序相反。 更多的输入输出操作 使用getContents读入所有字符直到遇到EOF,它是惰性的。interact接受一个字符串到字符串的函数,返回一个I/O操作,读入,应用函数并输出。 (未完待续)

2019/2/26
articleCard.readMore

我的项目

流量预警!!! 这篇文章是我参与的项目的汇总。 扫雷作弊器 依赖:Windows API 语言:C++ 代码量:586行 源码:sunziping2016/WinminePlugin 这个项目是我高一的时候用OllyDbg调试扫雷,偶然发现其全局变量与答案之间的关系,因而实现作弊的一个软件。这个软件会从外部读入一个配置文件,选项包括软件的延时、解决的模式(全部解出还是只标出雷)等等,然后借助Windows API读取/修改扫雷的内存。项目的代码是在我初步了解了C++的面向对象机制后写的,作为最初的尝试,其代码结构还有待改进。 Nokia复式记账软件 依赖:PyS60(平台为s60v3) 语言:Python2 代码量:2059行,46个类,298个函数 源码:sunziping2016/NokiaCash 这个项目是我高二的时候为了记账而写的第一个大型应用。这个应用支持复式记账、多文档视图、智能的交互、自动结账、多语言、撤销重做等等。这个项目经历了一次彻底的重构,而这应该是第2版。其代码风格很好地利用了面向对象的优势,简洁而又灵活。这款软件一直被我使用直至我Nokia E71退休。所有的代码都是在我的Nokia E71完成的。 在这前后,我还写过一个走迷宫的游戏,也算是个不小的项目,但代码已经遗失,便不在这里展示。 平板跑酷游戏 依赖:Windows API 语言:纯C 代码量:3552行 缘由:大一上《软件工程(1)》大作业 源码:sunziping2016/Escape 文档:设计文档 《Escape the Deadline》是一款采用物理模拟的跑酷游戏。所有的游戏场景均从文件读取。游戏中,若玩家与地面碰撞,将失去所有垂直于地面的动量分量。玩家的跳跃以垂直于地面方向的冲量实现。玩家不能在空中控制。目前实现了三个地图。第一个地图为无限随机的跑酷地图,强制向右卷屏。第二个地图的重力加速度较小(这使得玩家可沿着墙壁向上爬行),地图有限,当玩家高度低于-5000时才会死亡,此地图考验技术。第三个地图为空白地图,玩家可进入命令模式后手动绘制(进入命令模式务必确保输入法为英文模式。按住shift再按单引号)。由于使用纯C,整个代码都是面向过程的,使用了模块化设计。 联网井字棋 依赖:Java运行环境 语言:Java 代码量:713行 缘由:《Java语言程序设计》第九次作业 源码:sunziping2016/Java-TicTacToe 这个项目分服务端和客户端。服务端会将相邻的两个连接作为一组游戏,支持同时多组游戏。客户端依次落子下井字棋。 联网贪吃蛇 依赖:Java运行环境 语言:Java 代码量:2864行 缘由:《Java语言程序设计》大作业 源码:sunziping2016/Java-snake 文档:设计文档 这个项目分为服务端和客户端。其架构进行了精心设计,使用了MVC设计模式,但最后客户端因为时间问题草率手工。服务端还有一个简易的用户管理机制。 Qt植物大战僵尸 依赖:Qt 语言:C++ 代码量:4465行 缘由:《程序设计实训》大作业 源码:sunziping2016/Qt-PlantsVsZombies 文档:说明文档 这是一款使用Qt,模仿植物大战僵尸的游戏。虽然只有一关,可选的植物和僵尸也不多,但是代码非常具有扩展性。有火炬树桩、南瓜头、寒冰射手等植物。 知乎搜索引擎 依赖:Electron(前端) 语言:C++(内核)、JavaScript(前端) 代码量:3504行(内核)、463行(前端) 缘由:《数据结构与算法(1)》的《数据结构》课程实验2 源码:sunziping2016/zhihu-search-engine 文档:实验报告 这项作业要求手写各种数据结构、解析给定的HTML、使用前缀树分词、建立倒排索引从而完成对知乎日报文章的检索。我重新实现了C++中的vector、list、unordered_set等数据结构、写了一个具有良好错误检查的HTML解析器,并提供了类似BeautifulSoap的接口。为了加速我使用了多线程。相较于他人动辄1分钟以上的耗时,我仅用1.5秒即可完成这些步骤。前端部分使用Electron,以DLL的方式调用内核,界面模仿了Google。 图的可视化 依赖:Boost(内核)、Electron(前端) 语言:C++(内核)、JavaScript(前端) 代码量:1196行(内核)、1502行(前端) 缘由:《数据结构与算法(1)》的《图论》大作业 源码:sunziping2016/GraphVisualization 这个项目使用了Boost图的算法,将其以DLL的方式导出,从而使前端Electron可以调用Boost。前端部分使用了three.js来做到3D的效果。 汇编画图 依赖:Windows API 语言:汇编(RadASM) 代码量:2522行(三人组队,我是主力) 缘由:《计算机与网络体系结构(2)》的《汇编》小组作业 源码:sunziping2016/asmpaint 文档:说明文档 用汇编实现了一个类似mspaint的画图软件。功能非常齐全,包括缩放、撤销重做、矩形椭圆手绘作图、选择前景背景颜色、画笔粗细、填充效果等等。 FTP服务器 依赖:Linux,bcrypt,hash_table 语言:C 代码量:3011行 缘由:《计算机与网络体系结构(1)》的《计算机网络》第一次实验 源码:sunziping2016/ftp-server 文档:说明文档 超高性能非阻塞IO的FTP服务器。支持用于添/删服务、添/删用户、踢出客户的CLI界面,支持双栈,支持基于bcrypt用户验证,有良好的信号处理。采用了防御式编程。 微信抢票系统 依赖:Node.js, MongoDB,Redis 语言:JavaScript,Vue 代码量:3851行(后端)、6966行(前端) 缘由:《软件工程(3)》作业 源码: 后端:sunziping2016/YetAnotherWeChatTicketServer 前端:sunziping2016/YetAnotherWeChatTicketClient 文档:博客文章:微信抢票 这是一个借助微信发布活动、抢票、检票的系统,有邮箱验证的用户权限系统。前后端分离,API设计符合RESTful,有实时消息推送,安全且Scalable,前端为PWA。项目有单元测试,持续集成。 云众包平台 依赖:Node.js, MongoDB,Redis 语言:JavaScript,Vue 代码量:5127行(后端,4人协助开发,我的贡献约为1/3)、2946行(接收者前端,我负责)、6257行(发布者前端,车行负责) 缘由:《软件工程(3)》大作业 源码: 后端:sunziping2016/crowdsourcing-platform-server 接受者前端:sunziping2016/crowdsourcing-platform-client 发布者前端:chehThss/crowdsourcing-platform-publisher-client 文档:交付文档,开发文档 此项目的整体结构与微信抢票差异不大,不再赘述。 微信墙&微信弹幕 这个视频录制得有点卡。这是我后来录的,头像和昵称出了点问题。 依赖:Node.js,MongoDB,Electron 语言:JavaScript,React 代码量:1104行(后端),3292行(前端),25行(桌面弹幕) 缘由:学生节使用 源码: 后端:sunziping2016/wewall-server 前端:sunziping2016/wewall-client 桌面弹幕:sunziping2016/wewall-browser 这个项目包含微信墙和微信桌面弹幕两部分组成,支持emoji和微信表情。爬虫会模拟登陆微信号管理系统从而爬取用户头像。 体感游戏之碰撞 依赖:Leap Motion SDK,jBox2d,Java运行环境 语言:Java 代码量:2455行 缘由:科展使用 源码:sunziping2016/Collision 这是一个基于Leap Motion的体感游戏,玩家用手控制游戏中的球,避免被其他的球撞击。架构采用了MVC。 小型数据库 依赖:ANTLR4,Java运行环境,JUnit5(测试) 语言:Java 代码量:3774 缘由:《计算机系统软件(2)》的《数据库》大作业 源码:sunziping2016/mini-sql 文档:演示文档 这是一个简单的关系型数据库实现。支持同时连接多个客户端;客户端服务端都支持命令行参数;对于核心代码有单元测试,可以JOIN任意多张表,支持丰富的表达式及表达式的递归。 函数式语言解释器 依赖:megaparsec 语言:Haskell 代码量:1839 缘由:《软件理论基础(2):函数式语言程序设计》大作业 源码:sunziping2016/fpproject 这是一个简单的函数语言解释器,它支持递归函数、ADT、模式匹配、REPL等等特性,文法详见README.md。 简易操作系统 语言:C 缘由:《操作系统》大作业 源码:sunziping2016/xv6-improved 这是一个在Xv6基础上继续改进的操作系统,我领导着大组约40人。但最终管理较为混乱。 声音通信 语言:MATLAB(测试),Android Java(最终实现) 代码量:261行(MATLAB),1879行(Java) 缘由:《网络系统2》大作业2 源码: MATLAB:sunziping2016/matlab-sound-ofdm-qpsk Android:sunziping2016/SoundMessage 文档: 报告 幻灯片 声音通过OFDM和QPSK调制方式传输信息。使用循环前缀避免干扰;使用Preamble信号定位其实与终止;最后使用导频信号恢复相位与振幅。开发了既可以作为发送端也可以作为接送端的Android APP。 声音定位 语言:MATLAB(测试),Android Java(最终实现) 代码量:150行(MATLAB),1420行(Java) 缘由:《网络系统2》大作业3 源码: MATLAB:sunziping2016/matlab-sound-localization Android:sunziping2016/SoundLocalization 文档: 报告 幻灯片 这个项目采用FMCW的方式进行定位,我实现了1维和2维的实时定位,并且提供了不错的用于调试的UI。2维的定位效果不是特别好。 Graphviz可视化 语言:TypeScript 代码量:6682行 缘由:专业课程实践导师布置的任务 源码:暂时闭源 文档:幻灯片 这个软件是用于可视化Graphviz文件。它有两个模式,第一个模式采用KamadaKawai作初始布局,紧接着用物理模拟,多体问题采用BarnesHut算法,可以拖拽。第二个模式使用Xdot的输出格式,按照里面的作图信息作图,不可拖拽。 全自动刷课脚本 依赖:requests/beautifulsoup4(爬虫),PySide2(标注软件),PyTorch(深度学习) 语言:Python 代码量:1493行 缘由:为了抢到英语课 源码:sunziping2016/THUCourseSpider 文档:博客文章:全自动清华刷课脚本 通过爬虫爬取了4千验证码后,在朋友的帮助下标记了2千验证码。从头设计并训练神经网络,借助CNN和RNN使验证码识别准确率达到了95%。从而做到了全自动无人自动刷课。 RSA加密 语言:Rust 代码量:2000行 缘由:研一上《应用密码学》期中大作业 源码:sunziping2016/rsa-rs 文档:报告 借助汇编手写高精度整数,实现了高性能的RSA密钥的生成、加密和解密。借助Rust,代码具有很好的跨平台支持,可在各x86_64平台运行。兼容OpenSSH密钥格式。代码风格良好,搭配单元测试。 简易联盟链实现 依赖:SQLite 语言: Python3(使用Mypy和PyLint),Vue2 代码量:2267行(后端)、1264行(前端),我是主力 缘由:研一上《应用密码学》期末大作业 源码:sunziping2016/horde-blockchain 文档: Wiki 看板 使用数据库存储区块链,利用实用拜占庭容错协商,适配国密加密算法,可视化地展现了联盟链的操作。数据库和TCP连接均采用异步编程。值得一提的是这个项目在软件工程方面做得比较好,有Wiki帮助成员了解项目、有看板管理现在的任务、有issue追踪需求和bug、有CI对每个commit进行风格检查和单元测试。 光线追踪 依赖:PyQt5 语言:Rust、Python 代码量:4842行(Rust内核)、3343(Python前端) 缘由:研一下《真实感渲染技术》期末大作业 源码:sunziping2016/ray-tracing 文档:报告 本项目实现了包含多种材质、多种形状的场景的3D光线追踪。核心逻辑用Rust实现,部分API可被Python前端调用。项目最大的特色在于性能,使用了多线程和基于AoSoA的SIMD加速。代码使用了泛型,可在多种SIMD指令集中方便地切换。 article img { width: 50%; text-align: center; }

2018/12/4
articleCard.readMore

微积分复习笔记(上)

这篇文章是我数学复习计划的一部分,内容参考清华大学出版社的《高等微积分教程》上册和姚家燕老师在微积分课上的讲义。 实数系与实数列的极限 实数系 引入了上界、下界、有界和无界的概念。 还引入了最大值、最小值、上确界(最小上界)$\sup A$和下确界$\inf A$的概念。 确界定理:有上界的非空数集必有上确界,有下界的非空数集必有下确界。 上确界的刻画:$\xi=\sup A\Leftrightarrow\xi$是$A$的上界,且$\forall\epsilon>0,\exists x\in A, x>\xi-\epsilon$。同样也有下确界的刻画。 数列极限的基本概念 数列极限:$\forall\epsilon>0,\exists N\in\mathbb{N}, \forall n>N,|a_n-A|<\epsilon$。则称数列$\{a_n\}$有极限$A$,$\lim\limits_{n\to\infty}a_n=A$。 收敛数列的性质 收敛数列有以下性质: 唯一性; 有限韧性:添加、删除或改变有限项,不改变收敛性与极限值; 均匀性:数列$\{a_n\}$收敛于$A \Leftrightarrow$它的任意子列收敛于$A$; 有界性; 局部保序:$\lim\limits_{n\to\infty}a_n=A,\lim\limits_{n\to\infty}b_n=B$,则 $A>B\Rightarrow\exists N>0,\forall n>N, a_n>b_n$; $\exists N>0, \forall n>N, a_n\geq b_n\Rightarrow A\geq B$。 局部保号:上述数列$\{b_n\}$取$0$常数列可得到的推论。 极限遵守四则运算法则。此外还有夹逼原理:$\exists n_0>0,\forall n>n_0, a_n\leq x_n\leq b_n$且$\lim\limits_{n\to\infty}a_n=\lim\limits_{n\to\infty}b_n=A$,则$x_n$收敛且$\lim\limits_{n\to\infty}x_n=A$。 增长速度:$1 < \log n < n^\alpha (\alpha>0) < a^n (a>1) < n! < n^n$。 平均性:$\lim\limits_{n\to\infty}a_n=A\Rightarrow\lim\limits_{n\to\infty}\frac{a_1+a_2+\cdots+a_n}{n}=A$。 单调数列 引入了单调递增(单调递减)、严格单调递增(严格单调递减)的概念。 单调有界定理:单调递增有上界的数列必收敛,单调递减有下界的数列必收敛。 由此可定义自然对数$\lim\limits_{n\to\infty}(1+\frac{1}{n})^n=e$。且有$(1+\frac{1}{n})^n<e<(1+\frac{1}{n})^{n+1}$。 Stolz定理 引入了趋向于$+\infty$、趋向于$-\infty$和趋向于$\infty$的概念。 Stolz定理:设数列$\{b_n\}$严格递增趋于$+\infty$,$\lim\limits_{n\to\infty}\frac{a_n-a_{n-1}}{b_n-b_{n-1}}=A\in R\cup\{\pm\infty\}\Rightarrow\lim\limits_{n\to\infty}\frac{a_n}{b_n}=A$。 关于实数系的几个基本定理 下列结论等价: 确界定理; 单调有界定理; 区间套定理:$\{[a_n,b_n]\}$满足$[a_1,b_1]\supseteq[a_2,b_2]\supseteq\cdots\supseteq[a_n,b_n]\supseteq\cdots$且$\lim\limits_{n\to\infty}(b_n-a_n)=0$,则$\lim\limits_{n\to\infty}a_n=\lim\limits_{n\to\infty}b_n=c$且$\bigcap\limits_{n=1}^\infty[a_n,b_n]=\{c\}$; 列紧性:有界数列必有收敛子列; 称$\{x_n\}$为Cauchy数列,若$\forall\epsilon>0,\exists N>0,\forall m,n>N,|x_m-x_n|<\epsilon$。数列收敛当且仅当它为Cauchy数列。 函数,函数的极限与连续 函数 引入了映射、定义域和值域的概念。定义域和值域均为数集的映射称为函数。 定义了函数的四则运算及映射的复合,单射、满射及双射的概念。对于双射还可以定义其逆映射。 函数的基本性质有: 有界性; 周期性; 奇偶性; 单调性。 严格单调函数为单射。严格单调函数的反函数与原来的函数有同样的单调性。 基本初等函数有: 常数函数; 幂指对函数; 三角函数; 反三角函数。 基本初等函数经过有限次四则运算和复合后得到的函数称为初等函数。 函数极限的概念 对于$x_0\in\mathbb{R}\cup\{\infty,\pm\infty\}$,定义了$\epsilon$-领域$B_X(x_0,\epsilon)$和$\epsilon$-去心领域$\mathring{B}_X(x_0,\epsilon)$。 设$X$为非空数集,$x_0\in\mathbb{R}\cup\{\infty,\pm\infty\}$,如果$\forall\epsilon>0,\mathring{B}_X(x_0,\epsilon)\neq\emptyset$,则称$x_0$为$X$的极限点。 刻画:设$X$为非空数集,$a\in\mathbb{R}\cup\{\infty,\pm\infty\}$,则$a$为$X$的极限点$\Leftrightarrow X\setminus\{a\}$存在极限为$a$的数列。 函数极限的定义:设$X$为非空数集,$x_0\in\mathbb{R}\cup\{\infty,\pm\infty\}$为$X$的极限点,$a\in\mathbb{R}\cup\{\infty,\pm\infty\}$,$f:X\to\mathbb{R}$为函数。若$\forall\epsilon>0,\exists\delta>0,\forall x\in\mathring{B}_X(x_0,\delta),f(x)\in B(a,\epsilon)$,则称当$x$趋近于$x_0$时,$f(x)$趋近于$a$,记为$\lim\limits_{X\ni x\to x_0}f(x)=a$。有如下几种情况: $x_0,a\in\mathbb{R}$,有如下三种情况: $\exists\eta>0,X\supseteq(x_0-\eta,x_0+\eta)$,则我们有$\lim\limits_{x\to x_0}f(x)=a$; 右极限:$\exists\eta>0,X\supseteq(x_0,x_0+\eta)$,则我们有$\lim\limits_{x\to x_0^+}f(x)=a$; 左极限:$\exists\eta>0,X\supseteq(x_0-\eta,x_0)$,则我们有$\lim\limits_{x\to x_0^-}f(x)=a$; $x_0\in\mathbb{R},a\in\{\infty,\pm\infty\}$,极限及其左右极限,共9种情况; $x_0\in\{\infty,\pm\infty\},a\in\mathbb{R}\cup\{\infty,\pm\infty\}$,共12种情况。 函数在某点(包括$\infty$)极限存在等价于其左右极限存在且相等。 函数极限的性质 函数极限与数列极限的关系:设$X$为非空数集,$x_0$为$X$的极限点,$f:X\to\mathbb{R}$为函数,而$a\in\mathbb{R}\cup\{\infty,\pm\infty\}$,那么$\lim\limits_{X\ni x\to x_0}f(x)=a\Leftrightarrow$对$X\setminus\{x_0\}$中极限为$x_0$的任意数列$\{a_n\}$,有$\lim\limits_{n\to\infty}f(a_n)=a$。 函数极限有以下性质: 唯一性; 局部有界性:对于$\lim\limits_{X\ni x\to x_0}f(x)=a$,则$\exists\delta,M>0,\forall x\in\mathring{B}_X(x_0,\delta),|f(x)|<M$; 局部保序:对于$\lim\limits_{X\ni x\to x_0}f(x)=a,\lim\limits_{X\ni x\to x_0}g(x)=b$: $a>b\Rightarrow\exists\delta>0,\forall x\in\mathring{B}_X(x_0,\delta),f(x)>g(x)$; $\exists\delta>0,\forall x\in\mathring{B}_X(x_0,\delta),f(x)\geq g(x)\Rightarrow a\geq b$。 局部保序:上述$g(x)$恒取$0$可得到的推论。 有5条定理: 函数极限遵守四则运算和广义四则运算法则(包含了$\infty$); 此外也有夹逼原理:$\exists\delta>0,\forall x\in\mathring{B}_X(x_0,\delta),g(x)\leq f(x)\leq h(x)$且$\lim\limits_{X\ni x\to x_0}g(x)=\lim\limits_{X\ni x\to x_0}h(x)=a\in\mathbb{R}\cup\{\pm\infty\}$,则$\lim\limits_{X\ni x\to x_0}f(x)=a$; 复合函数极限:设$X,Y$为非空数集,$x_0$为$X$的极限点而$y_0\in\mathbb{R}\cup\{\infty,\pm\infty\}$,且函数$f:X\to\mathbb{R},g:Y\to\mathbb{R}$,满足$\forall x\in X\setminus\{x_0\},f(x)\in Y\setminus\{y_0\}$且$\lim\limits_{X\ni x\to x_0}f(x)=y_0,\lim\limits_{Y\ni y\to y_0}g(y)=a$,则$\lim\limits_{X\ni x\to x_0}g(f(x))=a$; 单调有界定理: 左极限:设$x_0\in\mathbb{R}\cup\{+\infty\},\eta\in\mathbb{R}\cup\{-\infty\},f:(\eta,x_0)\to\mathbb{R}$为单调函数,$\lim\limits_{x\to x_0^-}$存在。若$f$递增,$\operatorname{Im}(f)$是上确界。若$f$递增,$\operatorname{Im}(f)$是下确界。还有与之类似的右极限的情况; 左右极限:设$f:(a,b)\to\mathbb{R}$为单调函数而$x_0\in(a,b)$,$\lim\limits_{x\to x_0^-}f(x),\lim\limits_{x\to x_0^+}f(x)$存在且有限。若$f$递增,$\lim\limits_{x\to x_0^-}f(x)\leq f(x_0)\leq\lim\limits_{x\to x_0^+}f(x)$。若$f$递减,$\lim\limits_{x\to x_0^-}f(x)\geq f(x_0)\geq\lim\limits_{x\to x_0^+}f(x)$。 Cauchy准则:设$X$为非空数集,$x_0$为其极限点,$f:X\to\mathbb{R}$为函数,则极限$\lim\limits_{X\ni x\to x_0}f(x)$存在且有限$\Leftrightarrow\forall\epsilon>0,\exists\delta>0,\forall x,x’\in\mathring{B}_X(x_0,\delta),|f(x)-f(x’)|<\epsilon$。 无穷小量与无穷大量 极限为0的函数被称为无穷小量。若$\lim\limits_{X\ni x\to x_0}\alpha(x)=0$,则记$\alpha(x)=o(1)(X\ni x\to x_0)$。极限为无穷的函数称为无穷大量。 设$\lim\limits_{X\ni x\to x_0}\alpha(x)=\lim\limits_{X\ni x\to x_0}\beta(x)=0$且$\beta(x)\neq 0$: 如果$\lim\limits_{X\ni x\to x_0}\frac{\alpha(x)}{\beta(x)}=0$,则称$X\ni x\to x_0$时,$\alpha(x)$是$\beta(x)$的高阶无穷小量,记作$\alpha(x)=o(\beta(x))(X\ni x\to x_0)$; 如果$\lim\limits_{X\ni x\to x_0}\frac{\alpha(x)}{\beta(x)}=c\neq 0$,则称$X\ni x\to x_0$时,$\alpha(x)$与$\beta(x)$为同阶无穷小量; 如果$\lim\limits_{X\ni x\to x_0}\frac{\alpha(x)}{\beta(x)}=1$,则称$X\ni x\to x_0$时,$\alpha(x)$与$\beta(x)$为等价无穷小量,记作$\alpha(x)\sim\beta(x)(X\ni x\to x_0)$; 设$x_0\in\mathbb{R},r\in\mathbb{N}^*$如果$\lim\limits_{X\ni x\to x_0}\frac{\alpha(x)}{(x-x_0)^r}=c\neq 0$,则称$X\ni x\to x_0$时,$\alpha(x)$为 $r$阶无穷小量。 等价无穷小量的代换是求极限的方法之一。无穷大量也有类似的定义。 函数的连续与间断 设$X$为数集,$x_0\in X,f:X\to\mathbb{R}$为函数。若$\forall\epsilon>0,\exists\delta>0,\forall x\in B(x_0,\delta),|f(x)-f(x_0)|<\epsilon$,则称$f$在点$x_0$连续。若$f$在$X$的每点连续,则称$f$为$X$的连续函数。这样连续函数的集合记为$C(X)$。若$x_0$为$X$的极限点,则$f$在点$x_0$连续$\Leftrightarrow\lim\limits_{X\ni x\to x_0}f(x)=f(x_0)$。 若$\lim\limits_{x\to x_0^-}f(x)=f(x_0)$,称$f$在点$x_0$左连续;若$\lim\limits_{x\to x_0^+}f(x)=f(x_0)$,称$f$在点$x_0$右连续。 有如下的定理: 函数在某点(内点)连续$\Leftrightarrow$函数在该点左、右连续; 设$X$为数集,$x_0\in X$,则$f:X\to\mathbb{R}$在点$x_0$处连续$\Leftrightarrow$对$X$中收敛到$x_0$的任意数列$\{a_n\}$,均有$\lim\limits_{n\to\infty}f(a_n)=f(x_0)$; 设$X$为数集,$x_0\in X$而$f,g:X\to\mathbb{R}$: 局部有界:若$f$在点$x_0$处连续,则$\exists\delta>0,\forall x\in B_X(x_0,\delta),|f(x)|<1+|f(x_0)|$; 局部保序:设$f,g$在点$x_0$处连续 $f(x_0)>g(x_0)\Rightarrow\exists\delta>0\forall x\in B_X(x_0,\delta),f(x)>g(x)$; $\exists\delta>0\forall x\in B_X(x_0,\delta),f(x)\geq g(x)\Rightarrow f(x_0)\geq g(x_0)$。 局部保号:上述$g$恒为0的特殊情况; 四则运算法则:连续函数的四则运算也连续; 复合法则:连续函数函数的复合也连续。 初等函数在其定义域内连续。 间断点的分类: $\lim\limits_{X\ni x\to x_0}f(x)$存在且有限,但异于$f(x_0)$或$f$在$x_0$处无定义,称点$x_0$为$f$的可去间断点; 若左右极限存在且有限,但不相等,称改点为$f$的跳跃间断点 可去间断点及跳跃间断点合称为第一类间断点,其余的称为第二类间断点,它的至少一个单侧极限不存在或无限。 单调函数只能有第一类间断点(否则与单调有界定理矛盾)。 闭区间上连续函数的性质 有如下的定理: 连续函数介值定理:若$f\in C[a,b],\forall\mu$介于$f(a)$和$f(b)$之间,则$\exists\xi\in[a,b],f(\xi)=\mu$。推论:零点存在定理:若$f\in C[a,b],f(a)f(b)\leq 0$,则$\exists\xi\in[a,b],f(\xi)=0$; 若$X$为区间,$f\in C(X)$,则$\operatorname{Im}f$为区间; 若$X$为区间,$f\in C(X)$为单射,则$f$为严格单调函数; 若$X$为区间,$f$为单调函数,则$f\in C(X)\Leftrightarrow\operatorname{Im}f$是区间。 反函数定理:若$X$为区间,$f\in C(X)$为单射,则反函数$f^{-1}:\operatorname{Im}f\to X$存在且连续; 最值定理:若$f\in C[a,b]$,则$f$有最值。 函数的导数 导数与微分的概念 设$f:(a,b)\to\mathbb{R}$为函数,$x_0\in(a,b)$。若$\lim\limits_{x\to x_0}\frac{f(x)-f(x_0)}{x-x_0}=\lim\limits_{h\to 0}\frac{f(x_0+h)-f(x_0)}{h}$存在且有限,则称$f$在点$x_0$处可导,该极限称为$f$在点$x_0$处的导数,记作$f’(x_0),\frac{dy}{dx}\Big|_{x=x_0},\frac{df}{dx}(x_0)$。若$f$在$(a,b)$处处可导,则称$f$在$(a,b)$上可导,由此得到的函数$f’$称为$f$的导函数。此外还有单侧导数: 左导数:$f’_-(x_0):=\lim\limits_{x\to x_0^-}\frac{f(x)-f(x_0)}{x-x_0}$; 右导数:$f’_+(x_0):=\lim\limits_{x\to x_0^+}\frac{f(x)-f(x_0)}{x-x_0}$。 函数在某点可导$\Leftrightarrow$函数在该点左右导数存在有限且相等。若函数在某点可导,则函数在该点连续。 设$f:(a,b)\to\mathbb{R}$为函数,$x_0\in(a,b)$。若$\exists A\in\mathbb{R}$,使得$f(x_0+h)-f(x_0)=Ah+o(h)(h\to 0)$,则称$f$在点$x_0$处可微,此时还称线性函数$h\mapsto Ah$为$f$在点$x_0$的微分,记作$df(x_0),dy\Big|_{x=x_0}$。如果函数$f$在$(a,b)$处处可微,则称$f$在$(a,b)$上可微。 函数在某点可微$\Leftrightarrow$函数在该点可导。此时$df(x_0)=f’(x_0)dx$。 求导法则 有如下定理: 导数的四则运算:若$f,g:(a,b)\to\mathbb{R}$在点$x_0\in(a,b)$处可导,则: $\forall\lambda,\mu\in\mathbb{R},(\lambda f+\mu g)’(x_0)=\lambda f’(x_0)+\mu g’(x_0)$; $(fg)’(x_0)=f’(x_0)g(x_0)+f(x_0)g’(x_0)$; $\left(\frac{f}{g}\right)’(x_0)=\frac{f’(x_0)g(x_0)-f(x_0)g’(x_0)}{(g(x_0))^2}$,若$g(x_0)\neq 0$。推论:$\left(\frac{1}{g}\right)’(x_0)=-\frac{g’(x_0)}{(g(x_0))^2}$。 复合函数求导的链式法则:$(f\circ g)’(x_0)=f’(g(x_0))g’(x_0)$; 反函数求导法则:设函数$f:(a,b)\to(c,d)$为双射,在点$x_0$可导且$f’(x_0)\neq 0$,若$f^{-1}:(c,d)\to(a,b)$在点$y_0=f(x_0)$处连续,则$f^{-1}$在点$y_0$处可导,且$(f^{-1})’(y_0)=\frac{1}{f’(x_0)}$; 初等函数在其定义域内内部可导,且导函数也为初等函数。 隐函数求导:在两变量的方程$f(x,y)=0$中,将$y$看成$x$的函数,对$x$求导后解出$y’=\frac{dy}{dx}$。 参数方程求导:$\frac{dy}{dx}=\frac{\frac{dy}{dt}}{\frac{dx}{dt}}$。 高阶导数 $f$的 $n$阶导数记作$f^{(n)},\frac{d^nf}{dx^n}$。若$f$ $n$阶可导且$f^{(n)}$连续,那么称$f$为$n$阶连续可导。这样的函数的集合记为$C^{n}$。 有如下定理: 初等函数在其定义域内内部无穷可导; 高阶导数的四则运算:若$f,g:(a,b)\to\mathbb{R}$为$n$阶可导,则: $\forall\lambda,\mu\in\mathbb{R},(\lambda f+\mu g)^{(n)}=\lambda f^{(n)}+\mu g^{(n)}$; $(fg)^{(n)}=\sum\limits_{k=0}^n\binom{n}{k}f^{(k)}g^{(n-k)}$。 导数的应用 微分中值定理 极值的定义:设$X$为数集,$x_0\in X$,$f:X\to\mathbb{R}$,若$\exists\delta>0,B(x_0,\delta)\subseteq X\land\forall x\in B(x_0,\delta),f(x)\geq f(x_0)$,则称$x_0$为$f$的极小值点,称$f(x_0)$为极小值,类似可以定义极大值点和极大值。 有如下定理: Fermat(费马):设$x_0$为$f$的极值点,若$f$在$x_0$处可导,则$f’(x_0)=0$。导数为0的点称为驻点; Darboux(达布)导数介值定理:若$f$在$[a,b]$上可导,而$\mu$严格介于$f’_+(a),f’_-(b)$之间,则$\exists\xi\in(a,b),f’(\xi)=\mu$; 推论:若$f$在某个区间上可导,则其导函数的像集为区间。若$f’$恒不为0,则它恒正或恒负。 Rolle(罗尔):若$f\in C[a,b]$在$(a,b)$内可导且$f(a)=f(b)$,则$\exists\xi\in(a,b),f’(\xi)=0$; Lagrange拉格朗日中值定理:若$f\in C[a,b]$在$(a,b)$内可导,则$\exists\xi\in(a,b),f’(\xi)=\frac{f(b)-f(a)}{b-a}$; 设$f\in C[a,b]$在$(a,b)$内可导,则$f$为常值函数$\Leftrightarrow\forall x\in(a,b),f’(x)=0$; 设$f,g\in C[a,b]$在$(a,b)$内可导,若$\forall x\in(a,b),f’(x)=g’(x)$,则$\exists C\in\mathbb{R},\forall x\in[a,b],f(x)=g(x)+C$; 反函数定理:若$f\in C[a,b]$在$(a,b)$内可导且$f’$不为0,则$f$为单射且反函数可导。 Cauchy柯西中值定理:设$f\in C[a,b]$在$(a,b)$内可导,则$\exists\xi\in(a,b),(f(b)-f(a))g’(\xi)=(g(b)-g(a))f’(\xi)$。 L’Hospital(洛必达)法则 设$-\infty\leq a<b\leq+\infty$,函数$f,g:(a,b)\in\mathbb{R}$可导,而$g’$恒不为0,且$\lim\limits_{x\to a^+}\frac{f’(x)}{g’(x)}=\alpha\in\mathbb{R}\cup\{\pm\infty,\infty\}$,若$\lim\limits_{x\to a^+}f(x)=\lim\limits_{x\to a^+}g(x)=0$,或$\lim\limits_{x\to a^+}g(x)=\infty$,则我们有$\lim\limits_{x\to a^+}\frac{f(x)}{g(x)}=\alpha$。极限过程换成$x\to a^-$和$x\to a$仍成立。 Taylor(泰勒)公式 有如下定理: 带Peano(皮亚诺)余项的Taylor公式:设$n\geq 1$为整数,$x_0\in\mathbb{R}$,函数$f:B(x_0)\to\mathbb{R}$为$n-1$阶可导,且在点$x_0$为$n$阶可导。则当$x\to x_0$时,$f(x)=\sum\limits_{x=0}^n\frac{f^{(k)}(x_0)}{k!}(x-x_0)^k+o((x-x_0)^n)$。当$x_0=0$时,该公式也称为Maclaurin(麦克劳林)公式; 带Lagrange余项的Taylor公式:设$n\geq 1$为整数,$f\in C^{(n)}[a,b]$在(a,b)上$n+1$阶可导,那么$\forall x_0,x\in[a,b](x_0\neq x),\exists\xi$严格介于$x_0,x$之间,使得$f(x)=\sum\limits_{x=0}^n\frac{f^{(k)}(x_0)}{k!}(x-x_0)^k+\frac{f^{(n+1)}(\xi)}{(n+1)!}(x-x_0)^{(n+1)}$。 推论:如果$f\in C^{(n)}[a,b]$在$(a,b)$上的$n+1$阶导数恒为0,则$f$为次数不超过$n$的多项式。 函数的增减性与极值问题 对于函数的增减性,有如下定理: 若$f\in C[a,b]$在$(a,b)$内可导,则$f$递增$\Leftrightarrow\forall x\in(a,b),f’(x)\geq 0$;$f$递减$\Leftrightarrow\forall x\in(a,b),f’(x)\leq 0$; 若$f\in C[a,b]$在$(a,b)$内可导,则$f$严格递增$\Leftrightarrow\forall x\in(a,b),f’(x)\geq 0$且$f’$在$(a,b)$的任意子区间不恒为0;$f$严格递减$\Leftrightarrow\forall x\in(a,b),f’(x)\leq 0$且$f’$在$(a,b)$的任意子区间不恒为0。 对于函数的极值,有如下定理: 设$x_0\in(a,b),f\in C(a,b)$在$(a,b)\setminus\{x_0\}$上可导,若$\forall x\in(a,x_0),f’(x)\geq 0,\forall x\in(x_0,b),f’(x)\leq 0$,则$x_0$为$f$的最大值点,也为极大值点;若$\forall x\in(a,x_0),f’(x)\leq 0,\forall x\in(x_0,b),f’(x)\geq 0$,则$x_0$为$f$的最小值点,也为极小值点; 设$f:(a,b)\to\mathbb{R}$可导,$x_0\in(a,b), f’(x_0)=0$,且$f’’(x_0)$存在,若$f’’(x_0)<0$,则$x_0$为$f$的极大值点;若$f’’(x_0)>0$,则$x_0$为$f$的极小值点;若$f’’(x_0)=0$,无法判断。 凸函数 凸函数的定义:设$I$为区间,而$f:I\to\mathbb{R}$为函数: 若$\forall x,y\in I,\forall\lambda\in(0,1),f(\lambda x+(1-\lambda)y)\leq\lambda f(x)+(1-\lambda)f(y)$,则称$f$为$I$上的下凸函数,简称凸函数; 若$\forall x,y\in I(x\neq y),\forall\lambda\in(0,1),f(\lambda x+(1-\lambda)y)<\lambda f(x)+(1-\lambda)f(y)$,则称$f$为$I$上的严格下凸函数,也称严格凸; 若$\forall x,y\in I,\forall\lambda\in(0,1),f(\lambda x+(1-\lambda)y)\geq\lambda f(x)+(1-\lambda)f(y)$,则称$f$为$I$上的上凸函数,简称凹函数; 若$\forall x,y\in I(x\neq y),\forall\lambda\in(0,1),f(\lambda x+(1-\lambda)y)>\lambda f(x)+(1-\lambda)f(y)$,则称$f$为$I$上的严格上凸函数,也称严格凹。 有如下定理: 函数$f$为区间$I$上的凸函数$\Leftrightarrow\forall n\in\mathbb{N}^*,\forall x_1,x_2,\cdots,x_n \in I,\forall \lambda_1,\lambda_2,\cdots,\lambda_n\geq 0$,若$\lambda_1+\lambda_2+\cdots+\lambda_n=1$,则$f(\sum\limits_{k=1}^n\lambda_kx_k)\leq\sum\limits_{k=1}^n\lambda_kf(x_k)$; 函数$f$为区间$I$上的凸函数$\Leftrightarrow x_1,x_2,x_3\in I$,当$x_1<x_2<x_3$时,均有$\frac{f(x_2)-f(x_1)}{x_2-x_1}\leq\frac{f(x_3)-f(x_1)}{x_3-x_1}\leq\frac{f(x_3)-f(x_2)}{x_3-x_2}$; 若$f\in C[a,b]$在$(a,b)$内可导,则$f$为凸函数$\Leftrightarrow f’$在$(a,b)$上递增; 若$f\in C[a,b]$在$(a,b)$上二阶可导,则$f$为凸函数$\Leftrightarrow\forall x\in(a,b),f’’(x)\geq 0$; 若$f\in C[a,b]$在$(a,b)$上二阶可导,则$f$为严格凸函数$\Leftrightarrow\forall x\in(a,b),f’’(x)\geq 0$且$f’’$在$(a,b)$的任意子区间上不恒为0。 函数凹凸性发生改变的点为拐点。 函数作图 渐近线的定义:设曲线$\Gamma$由方程$y=f(x)$给出: 若$L\in\mathbb{R},\lim\limits_{x\to-\infty}f(x)=L$或$\lim\limits_{x\to+\infty}f(x)=L$,则称$y=L$为曲线$\Gamma$的水平渐近线; 若$x_0\in\mathbb{R},\lim\limits_{x\to x_0^-}f(x)=\infty$或$\lim\limits_{x\to x_0^+}f(x)=\infty$,则称$x=x_0$为曲线$\Gamma$的竖直渐近线; 若$k,b\in\mathbb{R},k\neq 0,\lim\limits_{x\to+\infty}(f(x)-kx-b)=0$或$\lim\limits_{x\to-\infty}(f(x)-kx-b)=0$,则称$y=kx+b$为曲线$\Gamma$的斜渐近线。 Riemann(黎曼)积分 Riemann积分的概念 设$f:[a,b]\to\mathbb{R}$为函数: 分割:称$P:a=x_0<x_1<\cdots<x_n=b$为$[a,b]$的分割,它将$[a,b]$分成内部不相交的小区间$\Delta_i=[x_{i-1},x_i] (1\leq i\leq n)$。令$\Delta x_i:=x_i-x_{i-1}(1\leq i\leq n)$,步长$\lambda(P):=\max\limits_{1\leq i\leq n}\Delta x_i$; 取点:称$\xi=\{\xi_1,\xi_2,\cdots,\xi_n\}$为分割$P$的取点,其中$\xi_i\in[x_{i-1},x_i] (1\leq i\leq n)$。此时称$(P,\xi)$为$[a,b]$的带点分割; Riemann和:对$[a,b]$的带点分割$(P,\xi)$,令$\sigma(f;P,\xi)=\sum\limits_{i=1}^nf(\xi_i)\Delta x_i$,称为$f$关于带点分割$(P,\xi)$的Riemann和; Riemann积分:若$\exists I\in\mathbb{R},\forall\epsilon>0,\exists\delta>0$,对于$[a,b]$的任意带点分割$(P,\xi)$,当$\lambda(P)<\delta,|\sigma(f;P,\xi)-I|<\epsilon$。此时记$I=\lim\limits_{\lambda(P)\to 0}\sigma(f;P,\xi)=\lim\limits_{\lambda(P)\to 0}\sum\limits_{i=1}^nf(\xi_i)\Delta x_i$,称为$f$在$[a,b]$上的定积分,简记为$I=\int_a^bf(x)dx$,并且称$f$在$[a,b]$上可积,否则称之为不可积。 记$R[a,b]$为$[a,b]$上所有可积函数的集合。若$f\in R[a,b]$,则$f$在$[a,b]$上有界。 判断函数可积的Darboux准则:设$f:[a,b]\to\mathbb{R}$为有界函数,而$P:a=x_0<x_1<\cdots<x_n=b$为$[a,b]$的分割。对于$1\leq i\leq n$,定义$m_i=\inf\limits_{x\in\Delta_i}f(x),M_i=\sup\limits_{x\in\Delta_i}f(x)$,Darboux下和$L(f;P)=\sum\limits_{i=1}^nm_i\Delta x_i$,Darboux上和$U(f;P)=\sum\limits_{i=1}^nM_i\Delta x_i$。有如下定理: 设$f:[a,b]\to\mathbb{R}$为有界函数,而$P_1,P_2$为$[a,b]$的两个分割,则$L(f;P_1)\leq U(f;P_2)$。由此定义下积分$\underline{\int_a^b}f(x)dx=\sup\limits_PL(f;P)$,上积分$\overline{\int_a^b}f(x)dx=\inf\limits_PU(f;P)$,我们有$L(f;P)\leq\underline{\int_a^b}f(x)dx\leq\overline{\int_a^b}f(x)dx\leq U(f;P)$; 设$f:[a,b]\to\mathbb{R}$为有界函数,而$P$为$[a,b]$的分割,则$L(f:P)=\inf\limits_\xi\sigma(f;P,\xi),U(f:P)=\sup\limits_\xi\sigma(f;P,\xi)$; Darboux:设$f:[a,b]\to\mathbb{R}$为有界函数,则下述结论等价: $f\in R[a,b]$; $\forall\epsilon>0$,存在$[a,b]$的分割$P$使得$U(f;P)-L(f;P)<\epsilon$; $\underline{\int_a^b}f(x)dx=\overline{\int_a^b}f(x)dx$; $\lim\limits_{\lambda(P)\to 0}(U(f;P)-L(f;P))=0$。 利用振幅刻画函数的可积性:设$X$为非空数集,而$f:X\to\mathbb{R}$为有界函数,对于任意非空子集$J\subseteq X$,定义$\omega(f;J):=\sup\limits_{x,y\in J}|f(x)-f(y)|$,并称之为$f$在$J$上的振幅。则有$\omega(f;J)=\sup\limits_{x\in J}f(x)-\inf\limits_{x\in J}f(x)$。有如下定理: $f\in R[a,b]\Leftrightarrow\lim\limits_{\lambda(P)\to 0}\sum\limits_{i=1}^n\omega(f;\Delta_i)\Delta x_i=0$。 一致连续函数:设$X$为非空数集,$f:X\to\mathbb{R}$为函数,若$\forall\epsilon>0,\exists\delta>0,\forall x,y\in X$,当$|x-y|<\delta$时,有$|f(x)-f(y)|<\epsilon$,则称$f$为一致连续。有如下定理: 函数$f$为一致连续$\Leftrightarrow$对于$X$中任意的数列$\{x\},\{y\}$,若$\lim\limits_{n\to\infty}(x_n-y_n)=0$,则$\lim\limits_{n\to\infty}(f(x_n)-f(y_n))=0$; 若$f\in C[a,b]$,则$f$为一致连续; 可积函数类有: $C[a,b]\subseteq R[a,b]$; 若$f:[a,b]\to\mathbb{R}$为有界函数并且存在有限多个点间断,则$f\in R[a,b]$; 若$f:[a,b]\to\mathbb{R}$单调,则$f\in R[a,b]$; Lebesgue判别准则:称数集$X$为零测度集,若$\forall\epsilon>0$,存在一列开区间$\{(a_n,b_n)\}$,使$X\subseteq\bigcup\limits_{n=1}^\infty(a_n,b_n), \lim\limits_{n\to\infty}\sum\limits_{k=1}^n(b_k-a_k)<\epsilon$。区间$[a,b]$上的有界函数$f$为Riemann可积$\Leftrightarrow f$的所有间断点构成的集合是零测度集。 Riemann积分的性质 有如下性质: 积分的线性性:设$f,g\in R[a,b],\alpha,\beta\in\mathbb{R}$,则$\alpha f+\beta g\in R[a,b]$且$\int_a^b(\alpha f(x)+\beta g(x))dx=\alpha\int_a^bf(x)dx+\beta\int_a^bg(x)dx$; 积分区间的可加性:设$f:[a,b]\to\mathbb{R},c\in(a,b)$,则$f$在$[a,b]$上可积$\Leftrightarrow f$分别在$[a,c],[c,b]$上可积,此时$\int_a^bf(x)dx=\int_a^cf(x)dx+\int_c^bf(x)dx$; 保序性:若$f,g\in R[a,b]$且$f\geq g$,则$\int_a^bf(x)dx\geq\int_a^bg(x)dx$。特别地,若$\exists m,M\in\mathbb{R}, m\leq f\leq M$,则$m(b-a)\leq \int_a^bf(x)dx\leq M(b-a)$; 保号性:若$f\in R[a,b]$非负,则$\int_a^bf(x)dx\geq 0$; 严格保号性:若$f\in C[a,b]$非负,则$\int_a^bf(x)dx=0\Leftrightarrow f\equiv 0$; 严格保序性:若$f,g\in C[a,b]$且$f\geq g$,则$\int_a^bf(x)dx\geq\int_a^bg(x)dx$,等号成立$\Leftrightarrow f\equiv g$。 若$f\in R[a,b]$,则$|f|\in R[a,b]$且$|\int_a^bf(x)dx|\leq\int_a^b|f(x)|dx$; 若$f,g\in R[a,b]$,则$fg\in R[a,b]$。 有如下定理: Cauchy不等式:若$f,g\in R(a,b)$,则$\left(\int_a^bf(x)g(x)dx\right)^2\leq\left(\int_a^b(f(x))^2\right)\left(\int_a^b(g(x))^2\right)$。 经典的Hölder不等式:若$x_k,y_k,p,q>0(1\leq k\leq n),\frac{1}{p}+\frac{1}{q}=1$,则$\sum\limits_{k=1}^nx_ky_k\leq\left(\sum\limits_{k=1}^nx_k^p\right)^{\frac{1}{p}}\left(\sum\limits_{k=1}^ny_k^q\right)^{\frac{1}{q}}$,且等号成立$\Leftrightarrow x_k^py_k^{-q}$为不依赖$k$的常数。积分Hölder不等式:若$f,g\in C[a,b],p,q>1$且$\frac{1}{p}+\frac{1}{q}=1$,则$\left|\int_a^bf(x)g(x)dx\right|=\left(\int_a^b|f(x)|^p\right)^{\frac{1}{p}}\left(\int_a^b|g(x)|^q\right)^{\frac{1}{q}}$。 积分第一中值定理:若$f\in C[a,b]$,则$\exists\xi\in[a,b],\int_a^bf(x)dx=f(\xi)(b-a)$。 广义积分第一中值定理:若$f,g\in C[a,b]$且$g$不变号,则$\exists\xi\in[a,b],\int_a^bf(x)g(x)dx=f(\xi)\int_a^bg(x)dx$。 微积分基本定理 原函数的定义:设$J$为区间,$F,f:J\to\mathbb{R}$为函数,若$F$在$J$上连续,在$J$内部可导且$F’=f$,则称$F$为$f$的一个原函数。 有如下定理: 设$f\in R[a,b]$,$\forall x\in[a,b]$,定义$F(x)=\int_a^xf(t)dt$,则$F\in C[a,b]$,若$f$在点$x_0\in[a,b]$连续,则$F$在点$x_0$处可导且$F’(x_0)=f(x_0)$,若$f$仅有单侧连续,则$F$有相应的单侧导数; 若$f\in C[a,b]$,则$F\in C^{(1)}[a,b]$且$F’=f$,即$F$为$f$在$[a,b]$上的一个原函数; 若$f\in C[a,b]$,$\phi,\psi:[\alpha,\beta]\to[a,b]$可导,$\forall u\in[\alpha,\beta]$,令$G(u)=\int_{\psi(u)}^{\phi(u)}f(t)dt$,则$G$可导且$\forall u\in[\alpha,\beta], G’(u)=f(\phi(u))\phi’(u)-f(\psi(u))\psi’(u)$。 Newton-Leibniz公式:设$f\in C[a,b], G\in C[a,b]$为$f$的一个原函数,则$\int_a^bf(x)dx=G\big|_a^b:=G(b)-G(a)$。 不定积分 将定义在区间上的函数$f$的原函数的一般表达式称为$f$的不定积分,记作$\int f(x)dx$。 有如下性质: 若$F,G$均为$f$的原函数,则$\exists C\in\mathbb{R},G-F=C$,故$\int f(x)dx=F(x)+C$($C$为常数); 若$f\in C[a,b]$,则$\int f(x)dx=\int_a^xf(t)dt+C$; 线性性:$\forall\alpha,\beta\in\mathbb{R},\int(\alpha f(x)+\beta g(x))dx=\alpha\int f(x)dx+\beta\int g(x)dx$。 有跳跃间断点的函数没有原函数。 有如下的积分法: 第一换元积分法(凑微分):$\int f(u(x))u’(x)dx=\int f(u(x))du(x)=F(u(x))+C$; 第二换元积分法:$\int f(x)dx\stackrel{x=x(t)}{=}\int f(x(t))x’(t)dt=F(t)+C\stackrel{t=t(x)}{=}F(t(x))+C$; 分部积分法:$\int udv=uv-\int vdu$。 有理函数的不定积分:有理真分式最终可以分解成如下4种最简单的分式之和:($m\geq 2$)$\frac{A}{x-\alpha},\frac{A}{(x-\alpha)^m},\frac{Ax+B}{x^2+px+q},\frac{Ax+B}{(x^2+px+q)^m}(p^2-4q<0)$,经过变量替换,可以归结成下述6种最简单的分式的不定积分($a>0$):$\frac{1}{x-\alpha},\frac{1}{(x-\alpha)^m},\frac{x}{x^2+a^2},\frac{1}{x^2+a^2},\frac{x}{(x^2+a^2)^m},\frac{1}{(x^2+a^2)^m}$,这些不定积分有公式可用。 三角有理函数的不定积分:设$R(u,v)$为关于$u,v$的有理函数,则$\int R(\sin x,\cos x)dx\stackrel{t=\tan\frac{x}{2}}{=}\int R\left(\frac{2t}{1+t^2},\frac{1-t^2}{1+t^2}\right)\frac{2}{1+t^2}dt$。 某些无理函数的不定积分:考虑不定积分$\int R(x,y(x))dx$: $y(x)=\sqrt[n]{\frac{ax+b}{cx+d}},(ad-bc\neq 0)$:$\int R\left(x,\sqrt[n]{\frac{ax+b}{cx+d}}\right)\stackrel{t=\sqrt[n]{\frac{ax+b}{cx+d}}}{=}\int R\left(\frac{dt^n-b}{a-ct^n},t\right)\frac{n(ad-bc)t^{n-1}}{(a-ct^n)^2}dt $; $y(x)=\sqrt{ax^2+bx+c},(a\neq 0)$:先配方,再用三角函数将原来的不定积分转化为三角有理函数的不定积分。 定积分的计算 有如下定理: 定积分的换元积分公式:若$f\in C[a,b],\phi:[\alpha,\beta]\to[a,b]\in C^{(1)}$,则$\int_{\varphi(\alpha)}^{\varphi(\beta)}f(x)dx=\int_{\alpha}^{\beta}f(\phi(t))\phi’(t)dt$; 定积分的分部积分公式:若$u,v\in C^{(1)}[a,b]$,则$\int_a^bu(x)dv(x)=uv\Big|_a^b-\int_a^bv(x)du(x)$; 积分的对称性:$f\in R[-a,a] (a>0)$,若$f$为奇函数,则$\int_{-a}^af(x)dx=0$;若$f$为偶函数,则$\int_{-a}^af(x)dx=2\int_0^af(x)dx$; 周期连续函数的定积分:如果$f\in C(\mathbb{R})$是周期为$T>0$的周期函数,则$\forall a\in\mathbb{R},\int_a^{a+T}f(x)dx=\int_0^Tf(x)dx$; 定积分与数列极限:设$f\in R[a,b]$,而$\{P_n\}$为$[a,b]$的一列分割,使得$\lim\limits_{n\to\infty}\lambda(P_n)=0$,记$P_n=(x_i^{(n)})_{0\leq i\leq k_n}$,则对任意点$\xi_i^{(n)}\in[x_{i-1}^{(n)},x_i^{(n)}](1\leq i\leq k_n)$,均有$\lim\limits_{n\to\infty}\sum\limits_{i=1}^{k_n}f(\xi_i^{(n)})(x_i^{(n)}-x_{i-1}^{(n)})=\int_a^bf(x)dx$; 带积分余项的Taylor:设$n\in\mathbb{N}$,若$f\in C^{(n+1)}[a,b],x_0\in[a,b]$,则$\forall x\in[a,b],f(x)=\sum\limits_{k=0}^n\frac{f^{(k)}(x_0)}{k!}(x-x_0)^k+\frac{1}{n!}\int_{x_0}^x(x-u)^nf^{(n+1)}(u)du$。 积分的应用 积分有如下的应用: 平面面积: 直角坐标系:设$f,g\in C[a,b],\forall x\in[a,b], f(x)\geq g(x)$,则由曲线$y=f(x),y=g(x)$与直线$x=a,x=b$,所围平面区域的面积$S=\int_a^b(f(x)-g(x))dx$; 直角坐标系下的参数方程:设曲线$\Gamma$的方程为$\begin{cases}x=x(t)\\y=y(t)\end{cases},(\alpha\leq t\leq\beta)$,其中$x,y$连续,$y\geq 0$,$x(t)$为严格递增,定义$a=x(\alpha),b=x(\beta)$,则由$\Gamma,x=a,x=b$和$x$轴所围区域的面积$S=\int_a^by(t(x))dx\stackrel{x=x(t)}{=}\int_{\alpha}^{\beta}y(t)x’(t)dt$; 极坐标系:设曲线弧$\overset{\frown}{AB}$的极坐标方程为$\rho=\rho(\theta)(\alpha\leq\theta\leq\beta)$,其中$\rho(\theta)$为连续函数,则曲线弧$\overset{\frown}{AB}$与射线$\theta=\alpha,\theta=\beta$所围成的区域的面积等于$S=\frac{1}{2}\int_{\alpha}^{\beta}(\rho(\theta))^2d\theta$。 曲线弧长: 直角坐标系下的参数方程:若曲线$\Gamma$的参数方程为$\begin{cases}x=x(t)\\y=y(t)\end{cases},(\alpha\leq t\leq\beta)$,其中$x(t),y(t)$为连续可导且导数不同时为0,这样的曲线称为光滑曲线,则弧长为$L=\int_{\alpha}^{\beta}\sqrt{(x’(t))^2+(y’(t))^2}dt$; 直角坐标系:若曲线$\Gamma$的方程为$y=f(x)(a\leq x\leq b)$,其中$f$连续可导,则弧长为$L=\int_a^b\sqrt{1+(f’(x))^2}dx$; 极坐标系:若曲线$\Gamma$的极坐标系方程为$\rho=\rho(\theta)(\alpha\leq\theta\leq\beta)$,其中$\rho(\theta)$连续可导,则弧长为$L=\int_{\alpha}^{\beta}\sqrt{(\rho(\theta))^2+(\rho’(\theta))^2}d\theta$; 曲线曲率:曲线曲率为$\kappa:=\left|\frac{d\alpha}{dl}\right|$,其中$\alpha$是切线与$x$轴正向的夹角: 直角坐标系下的参数方程:$\kappa=\frac{|x’y’’-x’‘y’|}{(x’)^2+(y’)^2)^{\frac{3}{2}}}$; 直角坐标系:$\kappa=\frac{|y’’|}{(1+(y’)^2)^{\frac{3}{2}}}$; 极坐标系:$\kappa=\frac{|\rho^2+2(\rho’)^2-\rho\rho’’|}{(\rho^2+(\rho’)^2)^{\frac{3}{2}}}$。 旋转体体积: 绕$x$轴旋转:$V=\pi\int_a^b(f(x))^2dx$; 绕$y$轴旋转:$V=2\pi\int_a^bxf(x)dx$。 旋转体的侧面积,绕$x$轴旋转: 直角坐标系下的参数方程:$S=2\pi\int_{\alpha}^{\beta}|y(t)|\sqrt{(x’(t))^2+(y’(t))^2}dt$; 直角坐标系:$S=2\pi\int_a^b|f(x)|\sqrt{1+(f’(x))^2}dx$; 极坐标系:$S=2\pi\int_{\alpha}^{\beta}|\rho(\theta)\sin\theta|\sqrt{(\rho(\theta))^2+(\rho’(\theta))^2}f\theta$。 广义Riemann积分 广义Riemann积分的概念 设$a\in\mathbb{R},\omega\in(a,+\infty],f:[a,\omega)\to\mathbb{R},\forall A\in(a,\omega),f\in R[a,A]$,定义$f$在$[a,\omega)$上的广义积分为$\int_a^{\omega}f(x)dx=\lim\limits_{A\to\omega^-}\int_a^Af(x)dx$。当$\omega\in\mathbb{R}$,而函数$f$在$\omega$的领域内无界,此时称$\omega$为$f$的奇点,相应的广义积分称为瑕积分。 广义积分继承了正常的定积分的性质,比如说线性性,保序性,Newton-Leibniz公式,分部积分,换元法。 广义积分收敛性的判定 有如下定理: Cauchy准则:设$a\in\mathbb{R},\omega\in(a,+\infty],f:[a,\omega)\to\mathbb{R},\forall A\in(a,\omega),f\in R[a,A]$,则$\int_a^\omega f(x)dx$收敛$\Leftrightarrow\forall\epsilon>0,\exists c\in(a,\omega),\forall A_1,A_2\in(c,\omega),\left|\int_{A_1}^{A_2}f(x)dx\right|<\epsilon$; 比较法则:设$f,g:[a,\omega)\to[0,+\infty)$在$[a,\omega)$的任意闭子区间上可积且$f(x)=O(g(x))(x\to\omega^-)$,若$\int_a^{\omega}g(x)dx$收敛,则$\int_a^{\omega}f(x)dx$收敛;若$\int_a^{\omega}f(x)dx$发散,则$\int_a^{\omega}g(x)dx$发散。有如下推论: 若$f:[a,\omega)\to[0,+\infty)$在$[a,\omega)$的任意闭子区间上可积,则$\int_a^{\omega}f(x)dx$发散$\Leftrightarrow\int_a^{\omega}f(x)dx=+\infty$; 设$f,g:[a,\omega)\to[0,+\infty)$在$[a,\omega)$的任意闭子区间上可积且$\lim\limits_{x\to\omega^-}\frac{f(x)}{g(x)}=\alpha\in[0,+\infty]$,若$\alpha\in(0,+\infty)$,则$\int_a^{\omega}g(x)dx$和$\int_a^{\omega}f(x)dx$同敛散;若$\alpha=0$且$\int_a^{\omega}g(x)dx$收敛,则$\int_a^{\omega}f(x)dx$收敛;若$\alpha=+\infty$且$\int_a^{\omega}g(x)dx$发散,则$\int_a^{\omega}f(x)dx$发散; 设$f:[1,+\infty)\to[0,+\infty)$在$[1,+\infty)$的任意闭子区间上可积且$\lim\limits_{x\to+\infty}\frac{f(x)}{\frac{1}{x^p}}=\lim\limits_{x\to+\infty}x^pf(x)=\alpha\in[0,+\infty]$,若$p>1\land 0\leq\alpha<+\infty$,则$\int_1^{+\infty}f(x)dx$收敛;若$p\leq 1\land 0<\alpha\leq+\infty$,则$\int_1^{+\infty}f(x)dx$发散; 设$f:(0,b]\to[0,+\infty)$在$(0,b]$的任意闭子区间上可积且$\lim\limits_{x\to 0^+}\frac{f(x)}{\frac{1}{x^p}}=\lim\limits_{x\to 0^+}x^pf(x)=\alpha$,若$p<1\land 0\leq\alpha<+\infty$,则$\int_0^bf(x)dx$收敛;若$p\geq 1\land 0<\alpha\leq+\infty$,则$\int_0^bf(x)dx$发散; 设$f:[a,\omega)\to\mathbb{R}$在$[a,\omega)$的任意闭子区间上可积,若$\int_a^{\omega}|f(x)|dx$收敛,则$\int_a^{\omega}f(x)dx$收敛;若$\int_a^{\omega}|f(x)|dx$收敛,则称$\int_a^{\omega}f(x)dx$绝对收敛;若$\int_a^{\omega}f(x)dx$收敛但不绝对收敛,则称$\int_a^{\omega}f(x)dx$条件收敛; 积分第二中值定理:若$f\in R[a,b]$,而$g$在$[a,b]$上单调,则$\exists\xi\in[a,b],\int_a^bf(x)g(x)dx=g(a)\int_a^{\xi}f(x)dx+g(b)\int_{\xi}^bf(x)dx$; 设$f,g:[a,\omega)\to\mathbb{R}$在$[a,\omega)$的任意闭子区间上可积: Abel判别准则:若$\int_a^{\omega}f(x)dx$收敛,$g$单调有界,则$\int_a^{\omega}f(x)g(x)dx$收敛; Dirichlet判别准则:若$F(A)=\int_a^Af(x)dx(A\in[a,\omega))$有界,而$g$单调且$\lim\limits_{x\to\omega^-}g(x)=0$,则$\int_a^{\omega}f(x)g(x)dx$收敛。 $\Gamma$函数:$\Gamma(s)=\int_0^{+\infty}x^{s-1}e^{-x}dx$。$\Gamma(s)$收敛当且仅当$s>0$。有如下性质: $\forall s>1,\Gamma(s)=(s-1)\Gamma(s-1)$。推论:$\forall n\in\mathbb{N},\Gamma(n+1)=n!$; $\forall s\in(0,1),\Gamma(s)\Gamma(1-s)=\frac{\pi}{\sin s\pi}$。特别地,$\Gamma\left(\frac{1}{2}\right)=\sqrt{\pi}$。 $B$函数:$B(p,q)=\int_0^1x^{p-1}(1-x)^{q-1}dx$。$B(p,q)$收敛当且仅当$p,q>0$。有如下性质: $B(p,q)=B(q,p)$; $B(p,q)=\frac{\Gamma(p)\Gamma(q)}{\Gamma(p+q)}$; $B(p+1,q)=\frac{p\Gamma(p)\Gamma(q)}{(p+q)\Gamma(p+1)}=\frac{p}{p+q}B(p,q)$。 常微分方程 常微分方程的基本概念 等式$F(x,y,y’,\cdots,y^{(n)})=0$被称为常微分方程。方程中导数的最高阶称为方程的阶,若$F$为线性函数,则称之为线性常微分方程。多个常微分方程组联立成常微分方程组。在区间$I$上满足$F(x,y,y’,\cdots,y^{(n)})=0$的函数$y=y(x)$称为该方程在$I$上的一个解,称$I$为解的存在区间。如果该解含$n$个独立常数,则称为方程的通解。若不含常数,则称之为特解。没有包含在通解中的特解称为奇解。$n$阶常微分方程一般需要$n$个条件确定通解中的常数,这类条件称为定解条件。 一阶常微分方程的初等解法 一阶常微分方程的一般形式为$F(x,y,\frac{dy}{dx})=0$。一阶线性常微分方程的典型形式为$\frac{dy}{dx}+P(x)y=Q(x)$。如果$Q(x)\equiv 0$则称之为一阶线性齐次常微分方程,否则称之为一阶线性非齐次常微分方程。 有如下定理: 一阶线性非齐次常微分方程的通解为方程的特解与相应的齐次方程的通解之和; $\frac{dy}{dx}+P(x)y=0$的通解为$y=Ce^{-\int P(x)dx}$; $\frac{dy}{dx}+P(x)y=Q(x)$的通解为$y=e^{-\int P(x)dx}\left(C+\int Q(x)e^{\int P(x)dx}dx\right)$(可通过常数变易法求)。 有如下求解方法: 可分离变量的一阶常微分方程,$\frac{dy}{dx}=f(x)g(y)$:当$g(y)\neq 0$时,$\int\frac{dy}{g(y)}=\int f(x)dx+C$。此外当$g(y_0)=0$时,$y\equiv y_0$也为方程的解; $\frac{dy}{dx}=f(ax+by+c)(b\neq 0)$:首先作变换$u=ax+by+c$,再利用分离变量法; 齐次型一阶常微分方程,$\frac{dy}{dx}=F(\frac{y}{x})$:首先作变换$u=\frac{y}{x}$,再利用分离变量法; $\frac{dy}{dx}=f(\frac{a_1x+b_1y+c_1}{a_2x+b_2y+c_2})(a_1b_2\neq a_2b_1)$:设直线$a_1x+b_1y+c_1=0,a_2x+b_2y+c_2=0$的交点为$(x_0,y_0)$,作变换$X=x-x_0,Y=y-y_0$,则方程化为齐次型一阶常微分方程; Bernoulli方程,$\frac{dy}{dx}+p(x)y=q(x)y^{\alpha}(\alpha\neq 0\land\alpha\neq 1)$:作变换$z=y^{1-\alpha}$,则方程化为一阶线性常微分方程。 可降阶的高阶常微分方程 有如下求解方法: $y^{(n)}=f(x)$:求$n$次原函数; $y^{(n)}=F(x,y^{(k)},\cdots,y^{(n-1)})(k\geq 1)$:令$p(x)=y^{(k)}$,由$p^{(n-k)}=F(x,p,p’,\cdots,p^{(n-k-1)})$解出$p=p(x)$,再对$y^{(k)}=p(x)$求$k$次原函数; $F(y,\frac{dy}{dx},\frac{d^2y}{dx^2})=0$:令$p=\frac{dy}{dx}$,原方程变为$F(y,p,p\frac{dp}{dy})=0$,解出$p=p(y)$,再对$\frac{dy}{dx}=p(y)$应用分离变量法。 高阶线性常微分方程解的结构 $n$阶线性常微分方程的标准形式为$y^{(n)}+a_{n-1}(x)y^{(n-1)}+\cdots+a_1(x)y’+a_0(x)y=f(x)$,其中$a_0,\cdots,a_{n-1},f$均为区间$I$上的连续函数,函数$f$被称为该方程的非齐次项。当$f\equiv 0$时,相应的方程为齐次方程。有如下基本结论: 存在与唯一:$\forall x_0\in I,\forall\xi_0,\cdots,\xi_{n-1}\in\mathbb{R}$,在区间$I$上均存在唯一的解$y=y(x)$使得$y^{(k)}(x_0)=\xi_k(0\leq k\leq n-1)$; 齐次方程的解集:齐次方程的所有解组成的集合是一个$n$维的线性空间; 非齐次方程的解集:非齐次方程的通解就是非齐次方程的特解与齐次方程的通解之和。 线性相关与线性无关:设函数$f_1,\cdots,f_n:I\to\mathbb{R}$,若存在不全为0的实数$c_1,\cdots,c_n$,使得$\forall x\in I,c_1f_1(x)+\cdots+c_nf_n(x)=0$,则称$f_1,\cdots,f_n$在$I$上线性相关,否则称$f_1,\cdots,f_n$在$I$上线性无关。 Wronsky(朗斯基)行列式:设$f_1,f_2,\cdots,f_n\in C^{(n-1)}(I)$。定义$W(x):=W(f_1,f_2,\cdots,f_n)(x):=\begin{vmatrix} f_1(x) & f_2(x) & \cdots & f_n(x) \\ f_1’(x) & f_2’(x) & \cdots & f_n’(x) \\ \vdots & \vdots & & \vdots \\ f_1^{(n-1)}(x) & f_2^{(n-1)}(x) & \cdots & f_n^{(n-1)}(x) \end{vmatrix}$,并称为$f_1,f_2,\cdots,f_n$的Wronsky行列式。 有如下定理: 若$f_1,f_2,\cdots,f_n\in C^{(n-1)}(I)$在$I$上线性相关,则$\forall x\in I,W(f_1,f_2,\cdots,f_n)(x)=0$; 设$y_1,\cdots,y_n\in C^{(n-1)}(I)$为$n$阶齐次线性常微分方程$I$上的解,则它们在$I$上线性相关$\Leftrightarrow W(y_1,\cdots,y_n)\equiv 0$(证明充分性仅需要$\exists x_0\in I, W(y_1,\cdots,y_n)(x_0)=0$)。 $n$阶齐次线性常微分方程的$n$个线性无关解被称为该方程的基本解组。 常系数高阶线性常微分方程 $n$阶线性常系数常微分方程的标准形式为$y^{(n)}+a_{n-1}y^{(n-1)}+\cdots+a_1y’+a_0y=f(x)$,其中$a_0,a_1,\cdots,a_{n-1}\in\mathbb{R},f\in C(I)$,函数$f$被称为该方程的非齐次项。当$f\equiv 0$时,相应的方程为齐次方程。 二阶线性常系数齐次方程:$y’’+py’+qy=0$,$p,q\in\mathbb{R}$,称$\lambda^2+p\lambda+q=0$为特征方程,称其解为特征根。令$\Delta=p^2-4q$: 若$\Delta>0$,则有两个不同的实特征根$\lambda_1,\lambda_2$,方程通解为$y=C_1e^{\lambda_1x}+C_2e^{\lambda_2x}$; 若$\Delta=0$,方程通解为$y=(C_1+C_2x)e^{-\frac{p}{2}x}$; 若$\Delta<0$,则有两共轭复特征根$\lambda=\alpha\pm i\beta$,方程通解为$y=e^{\alpha x}(C_1\cos\beta x+C_2\sin\beta x)$。 考虑$n$阶线性常系数齐次常微分方程$y^{(n)}+a_{n-1}y^{(n-1)}+\cdots+a_1y’+a_0y=0$,其中$a_0,a_1,\cdots,a_{n-1}\in\mathbb{R}$。其特征多项式被定义为$P(\lambda)=\lambda^n+a_{n-1}\lambda^{n-1}+\cdots+a_1\lambda+a_0$。假设该特征多项式不同的特征根为$\lambda_1,\cdots,\lambda_k$,重数为$n_1,\cdots,n_k$,则齐次方程的复值通解为$y(x)=\sum\limits_{j=1}^k\sum\limits_{l=0}^{n_j-1}C_{j,l}x^le^{\lambda_jx}$,其中$C_{j,l}\in\mathbb{C}$。为得到实值通解,只需要针对复数值特征根$\lambda_j$,在上式中将$e^{\lambda_jx}$及其共轭替换成$e^{\lambda_jx}$的实部和虚部,并让$C_{j,l}$为任意的实常数。 二阶线性常系数非齐次方程:由公式$z_0(x)=\int_{x_0}^x\frac{y_1(t)y_2(x)-y_1(x)y_2(t)}{W(y_1,y_2)(t)f(t)dt}$可得非齐次方程的一个通解。 特殊的二阶线性常系数方程的求解:$y’’+py’+qy=P_n(x)e^{\mu x},p,q\in\mathbb{R},\mu\in\mathbb{C},P_n$为$n$次多项式。有以下情况: 若$\mu$不是齐次方程的特征根,则会有特解$z_0(x)=Q_n(x)e^{\mu x},Q_n$为待定$n$次多项式; 若$\mu$是齐次方程的一重特征根,则会有特解$z_0(x)=Q_n(x)xe^{\mu x},Q_n$为待定$n$次多项式; 若$\mu$是齐次方程的二重特征根,则会有特解$z_0(x)=Q_n(x)x^2e^{\mu x},Q_n$为待定$n$次多项式。 两个有用的命题: 设$p(x),q(x)$为实值函数,$f(x)$为复值函数,而复值函数$y=y(x)$满足非齐次方程$y’’+p(x)y’+q(x)y=f(x)$,令$u(x)=\operatorname{Re}y(x),v(x)=\operatorname{Im}y(x)$,则$u’’+p(x)u’+q(x)u=\operatorname{Re}f(x),v’’+p(x)v’+q(x)v=\operatorname{Im}f(x)$; 假设$z_1’’+pz_1’+qz_1=f_1(x),z_2’’+pz_2’+qz_2=f_2(x)$,则$z_0=z_1+z_2$为非齐次方程$y’’+py’+qy=f_1(x)+f_2(x)$的特解。 Euler方程:一般的Euler方程为$x^ny^{(n)}+a_{n-1}x^{n-1}y^{(n-1)}+\cdots+a_1xy’+a_0y=0$,其中$a_0,a_1,a_{n-1}$为常数,作变量替换$t=\log|x|$。 一阶线性常微分方程组 一阶线性常微分方程组可以写成$\begin{cases} \frac{dy_1}{dx}&=a_{11}(x)y_1+a_{12}(x)y_2+\cdots+a_{1n}(x)y_n+f_1(x) \\ &\vdots \\ \frac{dy_n}{dx}&=a_{n1}(x)y_1+a_{n2}(x)y_2+\cdots+a_{nn}(x)y_n+f_n(x) \end{cases}$。该方程组满足初值条件$y_j(x_0)=\xi_j(1\leq j\leq n)$的解存在且唯一。用向量和矩阵可以重新表述为$\begin{cases}\frac{d\mathbf{Y}}{dx}=\mathbf{A}(x)\mathbf{Y}+\mathbf{F}(x)\\\mathbf{Y}(x_0)=\vec{\xi}\end{cases}$,其解为$\mathbf{Y}(x)=P_{x_0}^x(\mathbf{A})\vec{\xi}+\int_{x_0}^xP_t^x(\mathbf{A})\mathbf{F}(t)dt$,$P_{x_0}^x(A)$为Volterra积分。 一阶线性常微分方程组解的结构: $n$个方程组成的一阶线性齐次常微分方程组的解集是$n$维线性空间; 设$\mathbf{Y}_1,\mathbf{Y}_2,\cdots,\mathbf{Y}_n$为该齐次方程组的$n$个线性无关的解,定义$\mathbf{\Phi}=(\mathbf{Y}_1,\mathbf{Y}_2,\cdots,\mathbf{Y}_n)$称为齐次方程组的基解矩阵,则其通解为$\mathbf{Y}=C_1\mathbf{Y}_1+C_2\mathbf{Y}_2+\cdots+C_n\mathbf{Y}_n=\mathbf{\Phi}\mathbf{C}$,其中$\mathbf{C}=(C_1,C_2,\cdots,C_n)^T$为常数列向量; 设$\mathbf{Y}_1,\mathbf{Y}_2,\cdots,\mathbf{Y}_n$为方程组的$n$个解,令$W(x):=W(\mathbf{Y}_1,\mathbf{Y}_2,\cdots,\mathbf{Y}_n)(x):=\det(\mathbf{Y}_1(x),\mathbf{Y}_2(x),\cdots,\mathbf{Y}_n(x))$,称为$\mathbf{Y}_1,\mathbf{Y}_2,\cdots,\mathbf{Y}_n$的Wronsky行列式; $W’(x)=(a_{11}(x)+\cdots+a_{nn}(x))W(x)$,于是$W(x)=W(x_0)e^{\int_{x_0}^x(a_{11}(t)+\cdots+a_{nn}(t))dt}$; $W(x)$或者恒为0,或者恒不为0。 有如下定理: 基解矩阵满足$\frac{d\mathbf{\Phi}}{dx}=\mathbf{A}(x)\mathbf{\Phi}$; 齐次方程组的$n$个解$\mathbf{Y}_1,\mathbf{Y}_2,\cdots,\mathbf{Y}_n$线性相关当且仅当$W(x)\equiv 0$(证明充分性仅需要$W(x_0)=0$); 一阶线性非齐次常微分方程组的通解为该方程组的一个特解与相应的齐次方程组的通解之和。 一阶线性常系数常微分方程组的求解:$\begin{cases}\frac{d\mathbf{Y}}{dx}=\mathbf{A}\mathbf{Y}+\mathbf{F}(x)\\\mathbf{Y}(x_0)=\vec{\xi}\end{cases}$,其解为$\mathbf{Y}(x)=e^{(x-x_0)\mathbf{A}}\vec{\xi}+\int_{x_0}^xe^{(x-t)\mathbf{A}}\mathbf{F}(t)dt$。 设$\mathbf{\Phi}$为$\frac{d\mathbf{Y}}{dx}=\mathbf{A}\mathbf{Y}+\mathbf{F}(x)$的相应齐次方程组的基解矩阵,则非齐次方程组的通解为$\mathbf{Y}(x)=\mathbf{\Phi}(x)\mathbf{C}+\mathbf{\Phi}(x)\int_{x_0}^x(\mathbf{\Phi}(t))^{-1}\mathbf{F}(t)dt$。 一阶线性常系数齐次方程组的求解:$\frac{d\mathbf{Y}}{dx}=\mathbf{A}\mathbf{Y}$,特征方程为$\det(\lambda\mathbf{E}-\mathbf{A})=0$,考虑$n=2$的情形: 若$\mathbf{A}$有两个不相等的实特征根$\lambda_1,\lambda_2$,那么相应特征向量$\mathbf{r}_1,\mathbf{r}_2$为实向量且线性无关,通解为$\mathbf{Y}=C_1e^{\lambda_1x}\mathbf{r}_1+C_2e^{\lambda_2x}\mathbf{r}_2$,其中$C_1,C_2$为任意实常数; 若$\mathbf{A}$有两个相等的实特征根$\lambda$,相应特征向量$\mathbf{r}$为实向量,则$e^{\lambda x}\mathbf{r}$为方程组的解,与之线性无关的解可以取为$e^{\lambda x}\mathbf{P}(x)$,其中$\mathbf{P}(x)$是一个待定列向量,它的每个元素为次数$\leq 1$的多项式; 若$\mathbf{A}$有两个不相等的共轭复特征根$\lambda_1,\lambda_2$,相应的特征向量$\mathbf{r}_1,\mathbf{r}_2$也为共轭的复向量且线性无关。通解为$\mathbf{Y}=Ce^{\lambda_1x}\mathbf{r}_1+\bar{C}e^{\lambda_2x}\mathbf{r}_2$,其中$C$为任意复常数,也可表示成$\mathbf{Y}=C_1\operatorname{Re}(e^{\lambda_1x}\mathbf{r}_1)+C_2\operatorname{Im}(e^{\lambda_1x}\mathbf{r}_1)$,其中$C_1,C_2$为任意实常数。 一般情形($n\geq 1$):假设$\mathbf{A}$的不同特征根为$\lambda_1,\cdots,\lambda_k$,其重数分别为$n_1,\cdots,n_k$,对于$1\leq j\leq k$,存在$n_j$个形如$e^{\lambda_jx}\mathbf{P}_j(x)$的线性无关的解,其中$\mathbf{P}_j(x)$为$n$阶列向量,其元素为次数$\leq n_j-1$的多项式,待定系数即可求解。

2018/11/1
articleCard.readMore

线性代数复习笔记(上)

这篇文章是我数学复习计划的一部分,内容参考清华大学出版社的《线性代数与几何(第2版)》上册。 预备知识 设$\mathbb{F}$是$\mathbb{C}$的子集,且满足: 至少含有一个非零数; 对加减乘除运算封闭。 则$\mathbb{F}$是数域。 所有的数域都含有$\mathbb{Q}$。 行列式 首先引入 $n$阶排列、逆序数$\tau$、奇排列和偶排列的概念。 有如下的3个定理: 对换操作改变排列的奇偶性。 所有的$n$阶排列中($n\geq2$),奇偶各占一半。 $n$阶排列通可过对换操作转为自然排列,奇偶次数与排列的奇偶性一致。 然后引入n阶行列式的概念: $$D = \begin{vmatrix} a_{11} & a_{12} & \dots & a_{1n} \\ a_{21} & a_{22} & \dots & a_{2n} \\ \vdots & \vdots & & \vdots \\ a_{n1} & a_{n2} & \dots & a_{nn} \\ \end{vmatrix} = \sum_{j_1j_2 \dots j_n} (-1)^{\tau(j_1j_2 \dots j_n)} a_{1j_1}a_{2j_2} \dots a_{nj_n}$$ 行列式有如下的性质: 转置值不变; 数乘某行等于该数乘行列式; 推论:某一行全为0的行列式为0; 行列式对行的加法具有分配律; 对换两行,行列式反号; 两行成比例,行列式为0; 把一行的倍数加到另一行,行列式不变。 引入余子式$M_{ij}$和代数余子式$A_{ij}=(-1)^{i+j}M_{ij}$的概念。则有: $$\sum_{s=i}^n a_{is}A_{ks} = \begin{cases} D, i = k, \\ 0, i \neq k \end{cases} = \delta_{ik}D$$ 因而行列式可以按行或按列展开。 克莱姆法则。对于线性方程组: $$\begin{cases} a_{11}x_1 + a_{12}x_2 + &\dots + a_{1n}x_n = b_1, \\ a_{21}x_1 + a_{22}x_2 + &\dots + a_{2n}x_n = b_1, \\ &\vdots \\ a_{n1}x_1 + a_{n2}x_2 + &\dots + a_{nn}x_n = b_1, \\ \end{cases}$$ 若其系数行列式不为0,则方程组有唯一解: $$x_j = \frac{D_j}{D}$$ 其中$D$为系数行列式,$D_j$为常数项替换$D$第$j$列得到的行列式。 对于齐次线性方程组系数行列式非0,则只有0解。 矩阵 高斯消元法 线性方程组的初等变换有: 用非零数乘方程; 将一个方程的$k$倍加到另一个方程; 交换两方程。 与之对应的是对增广矩阵的变换,称为矩阵的行初等变换。 高斯消元法:通过初等变换使增广矩阵个变为如下的阶梯形矩阵: $$\begin{bmatrix} c_{11} & c_{12} & \dots & c_{1r} & \dots & c_{1n} & d_1 \\ & c_{22} & \dots & c_{2r} & \dots & c_{2n} & d_2 \\ & & \ddots & \vdots & & \vdots & \vdots \\ & & & c_{rr} & \dots & c_{rn} & d_r \\ & & & & & & d_{r+1} \\ & & & & & & 0 \\ & & & & & & \vdots \\ & & & & & & 0 \\ \end{bmatrix}$$ 有如下情况: $d_{r+1} \neq 0$,无解; $d_{r+1} = 0$且$r = n$,有唯一解; $d_{r+1} = 0$且$r < n$,有无穷多解,其中$x_{r+1},x_{r+2},\dots,x_n$称为自由变量。 对于齐次线性方程组: $$\begin{cases} a_{11}x_1 + a_{12}x_2 + &\dots + a_{1n}x_n = 0, \\ a_{21}x_1 + a_{22}x_2 + &\dots + a_{2n}x_n = 0, \\ &\vdots \\ a_{m1}x_1 + a_{m2}x_2 + &\dots + a_{mn}x_n = 0, \\ \end{cases}$$ 如果$m<n$,则一定有非零解。当$m=n$时,方程组有非0解$\Leftrightarrow$系数行列式$D=0$。 矩阵的运算 引入了矩阵、元素、零矩阵、单位矩阵、纯量矩阵和上(下)三角矩阵的概念。 定义了矩阵的加法、数乘、乘法。并满足: 有零元 有单位元 乘法结合律 左分配律和右分配律 定义了矩阵的转置$\mathbf{A}^\mathrm{T}$。引入了对称矩阵,反对称矩阵的概念。转置满足: $(\mathbf{A}^\mathrm{T})^\mathrm{T}=\mathbf{A}$ $(\mathbf{A}+\mathbf{B})^\mathrm{T}=\mathbf{A}^\mathrm{T}+\mathbf{B}^\mathrm{T}$ $(k\mathbf{A})^\mathrm{T}=k\mathbf{A}^\mathrm{T}$ $(\mathbf{A}\mathbf{B})^\mathrm{T}=\mathbf{B}^\mathrm{T}\mathbf{A}^\mathrm{T}$ 矩阵的多项式满足交换律:$f(\mathbf{A})g(\mathbf{A})=g(\mathbf{A})f(\mathbf{A})$。 逆矩阵 $\det(\mathbf{A}\mathbf{B})=\det\mathbf{A}\det\mathbf{B}$。 对于$n$阶方阵$\mathbf{A}$,若$\mathbf{A}\mathbf{B}=\mathbf{B}\mathbf{A}=\mathbf{I}$,则$\mathbf{A}$是可逆的,$\mathbf{B}$是$\mathbf{A}$的逆矩阵。若不存在这样的$\mathbf{B}$,则$\mathbf{A}$是不可逆的。有左逆就有右逆。逆矩阵唯一且满足: $(\mathbf{A}^{-1})^{-1}=\mathbf{A}$ $(\mathbf{A}\mathbf{B})^{-1}=\mathbf{B}^{-1}\mathbf{A}^{-1}$ $(\mathbf{A}^\mathrm{T})^{-1}=(\mathbf{A}^{-1})^\mathrm{T}$ 定义伴随矩阵: $$\mathbf{A}^*=\begin{bmatrix} A_{11} & A_{21} & \dots & A_{n1} \\ A_{12} & A_{22} & \dots & A_{n2} \\ \vdots & \vdots & & \vdots \\ A_{1n} & A_{2n} & \dots & A_{nn} \\ \end{bmatrix}$$ $\mathbf{A}$可逆$\Leftrightarrow \det\mathbf{A}\neq 0$。$\mathbf{A}$可逆时: $$\mathbf{A}^{-1}=\frac{1}{\det\mathbf{A}}\mathbf{A}^*$$ 设$\mathbf{A}\in M_n$: 若$\mathbf{A}$可逆,则$\mathbf{A}\vec{x}=\vec{b}$有唯一解$\mathbf{A}^{-1}\vec{b}$; $\mathbf{A}$不可逆$\Leftrightarrow \mathbf{A}\vec{x}=\vec{0}$有非0解。 分块矩阵 引入了分块矩阵的概念,并讨论了其加法、数乘、转置和乘法。对于乘法$\mathbf{A}\mathbf{B}$,$\mathbf{A}$的列划分需要与$\mathbf{B}$的行划分一致)。 矩阵的初等变换 矩阵的行初等变换和矩阵的列初等变换统称为矩阵的初等变换。 单位矩阵经过一次初等变换得到的矩阵称为初等矩阵。它们是: $\mathbf{E}_i(\lambda)$:$\lambda$乘以第$i$行; $\mathbf{E}_{i,j(\mu)}$:第$i$行的$\mu$倍加到第$j$行; $\mathbf{E}_{i,j}$:交换第$i$行和第$j$行。 初等矩阵都是可逆的。 用初等矩阵左乘矩阵相当于做初等行变换,用初等矩阵右乘矩阵相当于做初等列变换。 若矩阵$\mathbf{B}$可以由矩阵$\mathbf{A}$经过一系列初等变换得到,则$\mathbf{A}$与$\mathbf{B}$相抵,记作$\mathbf{A}\simeq\mathbf{B}$。这是一种等价关系。 任何一个矩阵都与形如$\begin{bmatrix} \mathbf{I}_r & \mathbf{0} \\ \mathbf{0} & \mathbf{0} \\ \end{bmatrix}$的矩阵相抵,称为相抵标准形。有如下几条推论: 对于$\mathbf{A}\in M_{m,n}$,存在一系列$m$阶初等矩阵$\mathbf{P}_1,\mathbf{P}_2,\dots,\mathbf{P}_s$和$n$阶初等矩阵$\mathbf{Q}_1,\mathbf{Q}_2,\dots,\mathbf{Q}_t$,使得: $$\mathbf{P}_s\dots\mathbf{P}_2\mathbf{P}_1\mathbf{A}\mathbf{Q}_1\mathbf{Q}_2\dots\mathbf{Q}_t=\begin{bmatrix} \mathbf{I}_r & \mathbf{0} \\ \mathbf{0} & \mathbf{0} \\ \end{bmatrix}$$ 对于$\mathbf{A}\in M_{m,n}$,存在可逆矩阵$\mathbf{P}\in M_m$和可逆矩阵$\mathbf{Q}\in M_n$,使得: $$\mathbf{P}\mathbf{A}\mathbf{Q}=\begin{bmatrix} \mathbf{I}_r & \mathbf{0} \\ \mathbf{0} & \mathbf{0} \\ \end{bmatrix}$$ 对于$\mathbf{A}\in M_{m}$,$\mathbf{A}$可逆$\Leftrightarrow \mathbf{A}\simeq\mathbf{I} \Leftrightarrow \mathbf{A}$可表示成有限个初等矩阵的乘积。 由于$\mathbf{A}^{-1}\begin{bmatrix}\mathbf{A}&\mathbf{I}\end{bmatrix}=\begin{bmatrix}\mathbf{I}&\mathbf{A}^{-1}\end{bmatrix}$,因此不断对$\begin{bmatrix}\mathbf{A}&\mathbf{I}\end{bmatrix}$进行初等行变换,使子块$\mathbf{A}$化作$\mathbf{I}$,则子块$\mathbf{I}$就化作了$\mathbf{A}^{-1}$,否则$\mathbf{A}$不可逆。 同样分块矩阵也有初等变换。 几何空间中的向量 向量及其运算 引入了向量(既有方向又有大小的几何解释)、向量的相等、反向量、零向量和单位向量的概念。 定义了向量的加法、减法和数乘。均满足线性空间的要求。 $\vec{\alpha},\vec{\beta}$共线$\Leftrightarrow$存在不全为0的数$\lambda,\mu$,使$\lambda\vec{\alpha}+\mu\vec{\beta}=0$。 $\vec{\alpha},\vec{\beta},\vec{\gamma}$共线$\Leftrightarrow$存在不全为0的数$k_1,k_2,k_3$,使$k_1\vec{\alpha}+k_2\vec{\beta}+k_3\vec{\gamma}=0$。 仿射坐标系 引入了仿射坐标系(三维)、坐标向量(又称基础向量,基)、坐标轴、坐标平面和坐标轴的概念。 根据坐标向量的位置关系,可分为右手仿射坐标系和左手仿射坐标系。 三个向量$\vec{\alpha_1}=(x_1,x_2,x_3),\vec{\alpha_2}=(y_1,y_2,y_3),\vec{\alpha_3}=(z_1,z_2,z_3)$共面$\Leftrightarrow \begin{vmatrix} x_1 & x_2 & x_3 \\ y_1 & y_2 & y_3 \\ z_1 & z_2 & z_3 \\ \end{vmatrix}=0$ 直角坐标系是坐标向量两两垂直且是单位向量的仿射坐标系。引入了方向角和方向余弦的概念。三个方向角不独立,满足: $$\cos^2\alpha+\cos^2\beta+\cos^2\gamma=1$$ 数量积、向量积与混合积 定义数量积$\vec{\alpha}\cdot\vec{\beta}=|\vec{\alpha}| |\vec{\beta}| \cos\langle\vec{\alpha},\vec{\beta}\rangle$。满足: 交换律 对加法的分配律 可提出因子 正定性 在仿射坐标系$\{O;\vec{e_1},\vec{e_2},\vec{e_3}\}$下,$\vec{\alpha}=x_1\vec{e_1}+x_2\vec{e_2}+x_3\vec{e_3},\vec{\beta}=y_1\vec{e_1}+y_2\vec{e_2}+y_3\vec{e_3}$,其数量积: $$\vec{\alpha}\cdot\vec{\beta}=(x_1,x_2,x_3)\begin{bmatrix}a&k&h\\k&b&g\\h&g&c\end{bmatrix}\begin{bmatrix}y_1\\y_2\\y_3\end{bmatrix}$$ 其中的矩阵如下表,被称为度量矩阵: $$\begin{array}{c:ccc} \cdot & \vec{e_1} & \vec{e_2} & \vec{e_3} \\ \hdashline \vec{e_1} & a & k & h \\ \vec{e_2} & k & b & g \\ \vec{e_3} & h & g & c \\ \end{array}$$ $\vec{\alpha}\bot\vec{\beta}\Leftrightarrow\vec{\alpha}\vec{\beta}=0$。 定义向量积$\vec{\alpha}\times\vec{\beta}$的方向为与它们垂直的右手系方向,模为$|\vec{\alpha}| |\vec{\beta}| \sin\langle\vec{\alpha},\vec{\beta}\rangle$。满足 反交换律:$\vec{\alpha}\times\vec{\beta}=-\vec{\beta}\times\vec{\alpha}$ 可提出因子 对加法的分配律 在右手直角坐标系$\{O;\vec{i},\vec{j},\vec{k}\}$中,向量积可表示为: $$\vec{\alpha}\times\vec{\beta}=\begin{vmatrix} \vec{i} & \vec{j} & \vec{k} \\ x_1 & x_2 & x_3 \\ y_1 & y_2 & y_3 \\ \end{vmatrix}$$ $\vec{\alpha}\parallel\vec{\beta}\Leftrightarrow\vec{\alpha}\times\vec{\beta}=0$。 定义混合积$(\vec{\alpha},\vec{\beta},\vec{\gamma})=(\vec{\alpha}\times\vec{\beta})\cdot\vec{\gamma}$。满足: $(\vec{\alpha},\vec{\beta},\vec{\gamma}) = (\vec{\beta},\vec{\gamma},\vec{\alpha}) = (\vec{\gamma},\vec{\alpha},\vec{\beta})$ $(\vec{\alpha},\vec{\beta},\vec{\gamma}) = -(\vec{\beta},\vec{\alpha},\vec{\gamma})$ 其几何意义是平行六面体体积。在右手直角坐标系中,混合积可表示为。 $$(\vec{\alpha},\vec{\beta},\vec{\gamma})=\begin{vmatrix} x_1 & x_2 & x_3 \\ y_1 & y_2 & y_3 \\ z_1 & z_2 & z_3 \\ \end{vmatrix}$$ 平面与直线 略 距离 略 向量空间 数域$\mathbb{F}$上的$n$维向量空间 引入了向量(有序数组的抽象解释)、分量、行向量、列向量和零向量的概念。 定义了向量的相等、加法和数乘,均满足线性空间的要求。 向量组的线性相关性 定义了线性组合、线性表出、线性相关和线性无关。 有如下的定理: $n$维向量$\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$线性相关$\Leftrightarrow$有向量可被其余向量线性表出; $n$维向量$\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$线性无关而$\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s},\vec{\beta}$线性相关$\Rightarrow \vec{\beta}$可由$\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$线性表出,且表示法唯一; $n$维向量$\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$线性相关$\Leftrightarrow \mathbf{A}\vec{x}=\vec{0}$有非零解,其中$\mathbf{A}=(\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s})$; $n$个$n$维向量$\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_n}$线性相关$\Leftrightarrow |\mathbf{A}|=0$; $\mathbb{F}^n$中任意$n+1$个向量必定线性相关; $\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}\in\mathbb{F}^m,\vec{\beta_1},\vec{\beta_2},\dots,\vec{\beta_s}\in\mathbb{F}^n$,构造$s$个$m+n$维向量$\vec{\gamma_1}=\begin{bmatrix}\vec{\alpha_1}\\ \vec{\beta_1}\end{bmatrix},\vec{\gamma_2}=\begin{bmatrix}\vec{\alpha_2}\\ \vec{\beta_2}\end{bmatrix},\dots,\vec{\gamma_s}=\begin{bmatrix}\vec{\alpha_s}\\ \vec{\beta_s}\end{bmatrix}$则: $\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$线性相关$\Rightarrow \vec{\gamma_1},\vec{\gamma_2},\dots,\vec{\gamma_s}$线性相关; $\vec{\gamma_1},\vec{\gamma_2},\dots,\vec{\gamma_s}$线性无关$\Rightarrow \vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$线性无关 向量组的秩 引入了向量组的线性表处、向量组的等价的概念。 有如下的定理: $\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$可由$\vec{\beta_1},\vec{\beta_2},\dots,\vec{\beta_t}$线性表出,且$s>t$,则$\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$线性相关; $\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$可由$\vec{\beta_1},\vec{\beta_2},\dots,\vec{\beta_t}$线性表出,且$\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$线性无关,则$s\leq t$。 向量组的等价是一种等价关系。引入了极大线性无关组(向量组的线性无关的部分,如再添加元素,都会使之线性相关)的概念。有如下的定理: 向量组的极大线性无关组的元素个数都相等。 因而引入了向量组的秩的概念。有如下的定理: $\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$可由$\vec{\beta_1},\vec{\beta_2},\dots,\vec{\beta_t}$线性表出$\Rightarrow \operatorname{r}(\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s})\leq\operatorname{r}(\vec{\beta_1},\vec{\beta_2},\dots,\vec{\beta_t})$; 等价的向量组的秩相等。 矩阵的秩 引入了矩阵的行秩和列秩的概念。有如下的定理: 初等变换不改变矩阵的行秩和列秩; 矩阵的行秩等于列秩。 进而引入了矩阵的秩的概念。 $\mathbf{A}\in M_{m,n}(\mathbb{F})$,而$\mathbf{P}\in M_m(\mathbb{F}),\mathbf{Q}\in M_n(\mathbb{F})$均为可逆矩阵,则$\operatorname{r}(\mathbf{P}\mathbf{A}\mathbf{Q})=\operatorname{r}(\mathbf{A})$; 相抵标准型唯一; $\mathbf{A},\mathbf{B}$相抵$\Leftrightarrow \operatorname{r}(\mathbf{A})=\operatorname{r}(\mathbf{B})$; 矩阵的秩等于该矩阵的非零子式的最高阶数。 秩有如下的性质: $\operatorname{r}(\mathbf{A}+\mathbf{B})\leq\operatorname{r}(\mathbf{A})+\operatorname{r}(\mathbf{B})$; $\operatorname{r}(\mathbf{A})+\operatorname{r}(\mathbf{B})-n\leq\operatorname{r}(\mathbf{A}\mathbf{B})\leq\min\{\operatorname{r}(\mathbf{A}),\operatorname{r}(\mathbf{B})\}$; $\operatorname{r}\left(\begin{bmatrix}\mathbf{A}&\mathbf{0}\\ \mathbf{0}&\mathbf{B}\end{bmatrix}\right)=\operatorname{r}(\mathbf{A})+\operatorname{r}(\mathbf{B})$; $\operatorname{r}\left(\begin{bmatrix}\mathbf{A}&\mathbf{0}\\ \mathbf{C}&\mathbf{B}\end{bmatrix}\right)\geq\operatorname{r}(\mathbf{A})+\operatorname{r}(\mathbf{B})$; $\max\{\operatorname{r}(\mathbf{A}),\operatorname{r}(\mathbf{B})\}\leq\operatorname{r}\left(\begin{bmatrix}\mathbf{A}&\mathbf{B}\end{bmatrix}\right)\leq\operatorname{r}(\mathbf{A})+\operatorname{r}(\mathbf{B})$; 如$\operatorname{r}(\mathbf{A})=r$,则存在列满秩矩阵$\mathbf{G}_{m\times r}$和行满秩矩阵$\mathbf{H}_{r\times n}$,使$\mathbf{A}=\mathbf{G}\mathbf{H}$(满秩分解)。 齐次线性方程组 对于$n$个未知量的齐次线性方程组: $\mathbf{A}\vec{x}=\vec{0}$有非零解$\Leftrightarrow\operatorname{r}(\mathbf{A})\leq n$; $\mathbf{A}\vec{x}=\vec{0}$只有零解$\Leftrightarrow\operatorname{r}(\mathbf{A})=n$。 对于齐次线性方程组,其解的线性组合也是解。其解集的极大线性无关组称为基础解系。 $\mathbf{A}\vec{x}=\vec{0}$的基础解系含有$n-\operatorname{r}(\mathbf{A})$个向量。 非齐次线性方程组 $\mathbf{A}\vec{x}=\vec{b}$有解$\Leftrightarrow \operatorname{r}(\mathbf{A})=\operatorname{r}(\begin{bmatrix}\mathbf{A}&\vec{b}\end{bmatrix})$。 非齐次线性方程组的通解为特解加上对应的齐次线性方程组基础解系的线性组合。 线性空间 数域$\mathbb{F}$上的线性空间 引入了线性空间、线性相关、线性无关、线性组合、线性表出和向量组的等价的概念。有如下定理(自4.2和4.3复制过来): $n$维向量$\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$线性相关$\Leftrightarrow$有向量可被其余向量线性表出; $n$维向量$\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$线性无关而$\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s},\vec{\beta}$线性相关$\Rightarrow \vec{\beta}$可由$\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$线性表出,且表示法唯一; $\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$可由$\vec{\beta_1},\vec{\beta_2},\dots,\vec{\beta_t}$线性表出,且$s>t$,则$\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$线性相关; $\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$可由$\vec{\beta_1},\vec{\beta_2},\dots,\vec{\beta_t}$线性表出,且$\vec{\alpha_1},\vec{\alpha_2},\dots,\vec{\alpha_s}$线性无关,则$s\leq t$。 还引入了维度$\operatorname{dim}$和基的概念。有如下定理: 如果线性空间$V$中有$n$个线性无关的向量,且$V$中任何向量可被其线性表出,那么$\operatorname{dim}V=n$且它们是$V$的一组基。 因而引入了坐标,自然基的概念。 设$\epsilon_1,\epsilon_2,\cdots,\epsilon_n$和$\eta_1,\eta_2,\cdots,\eta_n$是$V$的两组基,若: $$\begin{cases} \eta_1 = c_{11}\epsilon_1 + c_{21}\epsilon_2 + &\dots + c_{n1}\epsilon_n \\ \eta_2 = c_{12}\epsilon_1 + c_{22}\epsilon_2 + &\dots + c_{n2}\epsilon_n \\ &\vdots \\ \eta_n = c_{1n}\epsilon_1 + c_{2n}\epsilon_2 + &\dots + c_{nn}\epsilon_n \\ \end{cases}$$ 可写成: $$(\eta_1,\eta_2,\cdots,\eta_n)=(\epsilon_1,\epsilon_2,\cdots,\epsilon_n)\begin{bmatrix} c_{11} & c_{12} & \cdots & c_{1n} \\ c_{21} & c_{22} & \cdots & c_{2n} \\ \vdots & \vdots & & \vdots \\ c_{n1} & c_{n2} & \cdots & c_{nn} \\ \end{bmatrix}$$ 上述矩阵称为由基$\epsilon_1,\epsilon_2,\cdots,\epsilon_n$到基$\eta_1,\eta_2,\cdots,\eta_n$的过渡矩阵。有如下定理: 设由基$\epsilon_1,\epsilon_2,\cdots,\epsilon_n$到基$\eta_1,\eta_2,\cdots,\eta_n$的过渡矩阵为$\mathbf{C}$,则$\mathbf{C}$可逆。如果一向量在两组基的坐标分别是$\vec{x},\vec{y}$,则$\vec{x}=\mathbf{C}\vec{y}$。 线性子空间 引入了子空间、平凡子空间(零子空间和本身)、解空间(化零空间)$N$、列空间$R$、向量组生成的子空间的概念。有如下定理: 设$W$是线性空间$V$的非空子集,$W$是$V$的子空间$\Leftrightarrow$W对加法和数乘封闭; $\mathbf{A}\in M_{m,n},\operatorname{r}(\mathbf{A})=r$,则$\operatorname{dim}N(\mathbf{A})=n-r$,基础解系构成一组基; $\mathbf{A}\in M_{m,n},\operatorname{r}(\mathbf{A})=r$,则$\operatorname{dim}R(\mathbf{A})=r$,化为阶梯形矩阵主元对应的列构成一组基; $\mathbf{A}\in M_{m,n},\mathbf{B}\in M_{n,s}$: $R(\mathbf{A}\mathbf{B})\subseteq R(\mathbf{A})$; $N(\mathbf{A}\mathbf{B})\supseteq N(\mathbf{B})$。 引入了子空间的交与和的概念。有如下的定理: 子空间的交与和也是子空间; $W$是有限维线性空间$V$的子空间,则$W$的任何一组基可以扩充为$V$的一组基; $W_1,W_2$是有限维线性空间$V$的子空间,则$\operatorname{dim}W_1+\operatorname{dim}W_2=\operatorname{dim}(W_1+W_2)+\operatorname{dim}(W_1\cap W_2)$。 如果$W_1+W_2$任一向量在子空间的分解形式唯一,则$W_1+W_2$为$W_1$和$W_2$的直和$W_1\oplus W_2$。 $W_1+W_2$是直和$\Leftrightarrow$零向量表示方法唯一$\Leftrightarrow W_1\cap W_2=\{\theta\}\Leftrightarrow\operatorname{dim}W_1+\operatorname{dim}W_2=\operatorname{dim}(W_1+W_2)$(可推广到$n$个子空间的直和); $W_1$是$V$的子空间,则必存在$V$的子空间$W_2$,使$V=W_1\oplus W_2$。 线性空间的同构 保持线性关系的从一个线性空间到另一个线性空间的双射叫做同构映射,两个线性空间同构。同构映射有如下的性质: 把零元素映射为零元素,负元素映射为负元素; 保持线性组合不变; 保持线性相关性不变; 将基映射为基; 复合映射仍为同构映射(同构是一种等价关系)。 有如下定理: $n$维线性空间必同构于$\mathbb{F}$上的$n$维向量空间; 两个有限维线性空间同构$\Leftrightarrow$其维数相等。 欧几里得空间 满足对称性、线性性和正定性的从两个向量到实数的映射称为内积$(\vec{\alpha},\vec{\beta})$,定义了内积的实线性空间称为欧几里得空间。 由此可以定义长度$|\alpha|=\sqrt{(\vec{\alpha},\vec{\alpha})}$、单位向量和单位化。 柯西-施瓦茨不等式:$(\vec{\alpha},\vec{\beta})^2\leq|\vec{\alpha}|^2|\vec{\beta}|^2$。 由此可以定义夹角$\cos\theta=\frac{(\vec{\alpha},\vec{\beta})}{|\vec{\alpha}||\vec{\beta}|}$、正交和正交向量组。有如下定理: 正交向量组线性无关; 正交向量组个数不会超过维度。 由此引入了正交基和标准正交基的概念。有如下的定理: $\vec{\epsilon_1},\vec{\epsilon_2},\cdots,\vec{\epsilon_n}$是$n$维欧氏空间$V$中的一组标准正交基,设$\vec{\alpha}\in V$的坐标为$\vec{x}=(x_1,x_2,\cdots,x_n)^\mathrm{T}$,则$x_i=(\vec{\alpha},\vec{\epsilon_i})$; $\vec{\epsilon_1},\vec{\epsilon_2},\cdots,\vec{\epsilon_n}$是$n$维欧氏空间$V$中的一组标准正交基,对$\alpha=(\vec{\epsilon_1},\vec{\epsilon_2},\cdots,\vec{\epsilon_n})\vec{x},\beta=(\vec{\epsilon_1},\vec{\epsilon_2},\cdots,\vec{\epsilon_n})\vec{y}$,有$(\vec{\alpha},\vec{\beta})=\vec{x}^\mathrm{T}\vec{y}=\sum_{i=1}^n x_iy_i$。 施密特正交化:对任意$s$个线性无关的向量$\vec{\alpha_1},\vec{\alpha_2},\cdots,\vec{\alpha_s}$,先将其转换为一个正交向量组$\vec{\beta_1},\vec{\beta_2},\vec{\beta_s}$,其中, $$\begin{align} \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}, \\ &\vdots \\ \vec{\beta_s}&=\vec{\alpha_s}-\frac{(\vec{\alpha_s},\vec{\beta_1})}{(\vec{\beta_1},\vec{\beta_1})}\vec{\beta_1}-\frac{(\vec{\alpha_s},\vec{\beta_2})}{(\vec{\beta_2},\vec{\beta_2})}\vec{\beta_2}-\cdots-\frac{(\vec{\alpha_s},\vec{\beta_{s-1}})}{(\vec{\beta_{s-1}},\vec{\beta_{s-1}})}\vec{\beta_{s-1}} \end{align}$$ 然后将其单位化$\vec{\gamma_i}=\frac{\vec{\beta_i}}{|\vec{\beta_i}|}$,可得到标准正交向量组$\vec{\gamma_1},\vec{\gamma_2},\cdots,\vec{\gamma_s}$。 设$\mathbf{Q}\in M_n(\mathbb{R})$,如$\mathbf{Q}^\mathrm{T}\mathbf{Q}=\mathbf{I}$,则称$\mathbf{Q}$是正交矩阵。有如下定理: 由标准正交基到标准正交基的过渡矩阵是正交矩阵;若过渡矩阵是正交矩阵且一组基是标准正交基,则另一组基也是标准正交基; 正交矩阵的性质是: 其行(列)向量组成正交向量组,且每个向量都是单位向量; 其行列式值为$+1$或$-1$; 其逆矩阵还是正交矩阵; 其乘积还是正交矩阵。 对任意$n$阶可逆实矩阵$\mathbf{A}$,存在一个$n$阶正交矩阵$\mathbf{Q}$和一个$n$阶主对角元素为正数的上三角阵$\mathbf{R}$,使$\mathbf{A}=\mathbf{Q}\mathbf{R}$,称为QR分解,这个分解唯一。存在性由施密特正交化可得。 引入了向量与子空间的正交、子空间与子空间的正交和正交补$W^\perp$的概念。有如下定理: 设$W$是$n$维欧几里得空间$V$的子空间,则$V=W\oplus W^\perp$。 线性变换 线性变换的定义和基本性质 线性空间上保持线性关系的变换称为线性变换或线性算子,其集合记为$L(V)$。有如下的性质: 将零向量映射为零向量; 将负向量映射为负向量; 保持线性组合不变; 将线性相关的向量组映射为线性相关的向量组。 引入了线性变换的和和数乘的概念。满足线性空间的要求,即$L(V)$构成线性空间。 又引入了线性变换的乘积、可逆、逆变换和多项式的概念。 线性变换的矩阵 有如下定理: 设$\sigma$是$n$维线性空间$V$的线性变换,$\vec{\alpha_1},\vec{\alpha_2},\cdots,\vec{\alpha_n}$是$V$的一组基,则$V$中任意向量$\vec{\alpha}$的像$\sigma(\vec{\alpha})$由基的像$\sigma(\vec{\alpha_1}),\sigma(\vec{\alpha_2}),\cdots,\sigma(\vec{\alpha_n})$完全确定。 $$\sigma(\vec{\alpha_1},\vec{\alpha_2},\cdots,\vec{\alpha_n})=(\vec{\alpha_1},\vec{\alpha_2},\cdots,\vec{\alpha_n})\mathbf{A}$$ $n$阶矩阵$\mathbf{A}$叫做线性变换$\sigma$在基$\vec{\alpha_1},\vec{\alpha_2},\cdots,\vec{\alpha_n}$下的矩阵。有如下的定理: 设线性变换$\sigma$在基$\vec{\alpha_1},\vec{\alpha_2},\cdots,\vec{\alpha_n}$下的矩阵是$\mathbf{A}$,$\vec{\alpha}$和$\sigma(\vec{\alpha})$的坐标分别为$\vec{x},\vec{y}$,则$\vec{y}=\mathbf{A}\vec{x}$; 设$\vec{\alpha_1},\vec{\alpha_2},\cdots,\vec{\alpha_n}$是$n$维线性空间$V$的一组基,对给定的$n$个向量$\vec{\beta_1},\vec{\beta_2},\cdots,\vec{\beta_n}$都存在线性变换$\sigma$,使得$\sigma(\alpha_i)=\beta_i(i=1,2,\cdots,n)$; 设$\vec{\alpha_1},\vec{\alpha_2},\cdots,\vec{\alpha_n}$是$n$维线性空间$V$的一组基,$\mathbf{A}$是任一$n$阶方阵,则有唯一的线性变换$\sigma$满足:$$\sigma(\vec{\alpha_1},\vec{\alpha_2},\cdots,\vec{\alpha_n})=(\vec{\alpha_1},\vec{\alpha_2},\cdots,\vec{\alpha_n})\mathbf{A}$$ 设$V$是$\mathbb{F}$上$n$维线性空间,则$L(V)$与$M_n(\mathbb{F})$同构。 这种一一对应关系除了保持了线性运算,也保持了乘法运算。有如下定理: 设$\varphi:L(V)\rightarrow M_n(\mathbb{F})$为上述同构映射,则对$\sigma,\tau\in L(V)$,有$\varphi(\sigma\tau)=\varphi(\sigma)\varphi(\tau)$; 设$\sigma\in L(V),\varphi(\sigma)=\mathbf{A}$,$\sigma$可逆$\Leftrightarrow \mathbf{A}$可逆,且$\varphi(\sigma^{-1})=\mathbf{A}^{-1}$。 线性变换的核与值域 全体像的集合称为值域,记作$\operatorname{Im}\sigma$。被映射成零向量的向量的集合称为核,记作$\operatorname{ker}\sigma$。它们都是子空间。$\operatorname{dim}\operatorname{Im}\sigma$称为秩,$\operatorname{dim}\operatorname{ker}\sigma$称为零度。 有如下定理: 设$\sigma\in L(V),\vec{\epsilon_1},\vec{\epsilon_2},\cdots,\vec{\epsilon_n}$是$V$的一组基,$\mathbf{A}$是$\sigma$在这组基下的矩阵,则: $\operatorname{Im}\sigma=L(\sigma\vec{\epsilon_1},\sigma\vec{\epsilon_2},\cdots,\sigma\vec{\epsilon_n})$; $\sigma$的秩$= \mathbf{A}$的秩。 设$\sigma\in L(V)$,则$\operatorname{dim}V=\operatorname{dim}\operatorname{ker}\sigma+\operatorname{dim}\operatorname{Im}\sigma$; 对于有限维线性空间的线性变换$\sigma$,$\sigma$是单射$\Leftrightarrow \sigma$是满射。 引入了不变子空间的概念。将$\sigma$的作用限制到$W$上记作$\sigma\Bigg|_W$。不变子空间对于化简矩阵有着很重要的作用。 特征值与特征向量 设$\sigma\in L(V)$,对于$\mathbb{F}$中的数$\lambda$,存在非零向量$\vec{\xi}$,使得$\sigma\vec{\xi}=\lambda\vec{\xi}$,则$\lambda$是$\sigma$的特征值,$\vec{\xi}$是$\sigma$属于特征值$\lambda$的特征向量。 属于$\lambda$的特征向量的线性组合仍是属于$\lambda$的特征向量。它们构成特征子空间,其维数称为特征值$\lambda$的几何重数。多项式$f_A(\lambda)=\det(\lambda\mathbf{I}-\mathbf{A})$称为线性变换$\sigma$的特征多项式,它的根称为$\sigma$的特征根。特征根的重数称为代数重数。 类似地,还定义了矩阵的特征值、特征向量和特征多项式。求特征值和特征向量的步骤如下: 求出特征多项式$f_A(\lambda)=\det(\lambda\mathbf{I}-\mathbf{A})=0$的解,得到全部特征值; 分别把特征值代入到齐次线性方程组$(\lambda_i\mathbf{I}-\mathbf{A})\vec{x}=\vec{0}$,其基础解系就是属于$\lambda_i$的特征向量。 有如下定理: 设$\mathbf{A}\in M_n(\mathbb{C})$, $\sum_{i=1}^n\lambda_i=\sum_{i=1}^n a_{ii}=\operatorname{tr}\mathbf{A}$; $\prod_{i=1}^n\lambda_i=|\mathbf{A}|$。 $n$阶方阵可逆$\Leftrightarrow$其$n$个特征值全不为零; Hamilton-Cayley定理:设$\mathbf{A}\in M_n(\mathbb{F}),f_A(\lambda)=|\lambda\mathbf{I}-\mathbf{A}|$是$\mathbf{A}$的特征多项式,则$f_A(\mathbf{A})=0$,对于线性变换也有类似的结论。 相似矩阵 $\mathbf{A},\mathbf{B}$是两个$n$阶方阵,如果存在$n$阶可逆矩阵$\mathbf{P}$,使得$\mathbf{P}^{-1}\mathbf{A}\mathbf{P}=\mathbf{B}$,则称$\mathbf{B}$相似于$\mathbf{A}$,记作$\mathbf{B}\sim\mathbf{A}$。 相似关系是一种等价关系。线性变换在不同基下的矩阵是相似矩阵。有如下性质: 相似矩阵的多项式相似; 若$\mathbf{A}_i\sim\mathbf{B}_i,i=1,2,\cdots,s$,则$\operatorname{diag}(\mathbf{A}_1,\mathbf{A}_2,\cdots,\mathbf{A}_s)\sim\operatorname{diag}(\mathbf{B}_1,\mathbf{B}_2,\cdots,\mathbf{B}_s)$; 相似矩阵,一个可逆,则另一个也可逆,且逆相似; 相似矩阵有相同的特征值和特征多项式; 相似矩阵有相同的迹和行列式。 接下来研究矩阵的相似对角化,有如下定理: $n$阶方阵可对角化$\Leftrightarrow$有$n$个线性无关的特征向量; 不同特征值的特征向量线性无关; 若$n$阶方阵$\mathbf{A}$有$n$个互异的特征值$\lambda_1,\lambda_2,\cdots,\lambda_n$,则$\mathbf{A}$可对角化,$\mathbf{A}\sim\operatorname{diag}(\lambda_1,\lambda_2,\cdots,\lambda_n)$且可逆矩阵是由相应的特征向量作列向量构成的; 设$\lambda_1,\lambda_2,\cdots,\lambda_s$是$\mathbf{A}$的$s$个互异的特征值,$\vec{x_{i1}},\vec{x_{i2}},\cdots,\vec{x_{im_i}}$是$\mathbf{A}$的属于$\lambda_i$的$m_i$个线性无关的特征向量,$i=1,2,\cdots,s$,则$\vec{x_{11}},\vec{x_{12}},\cdots,\vec{x_{1m_1}},\vec{x_{21}},\vec{x_{22}},\cdots,\vec{x_{2m_2}},\cdots,\vec{x_{s1}},\vec{x_{s2}},\cdots,\vec{x_{sm_s}}$也线性无关; 设$\lambda_i$是$n$阶复方阵$\mathbf{A}$的特征值,则它的几何重数总不大于它的代数重数,即$m_i\leq n_i$; $\mathbf{A}$是$n$阶复方阵,$\mathbf{A}$可对角化$\Leftrightarrow m_i=n_i, i=1,2,\cdots,s\Leftrightarrow\sum_{i=1}^s m_i=n\Leftrightarrow\operatorname{r}(\lambda_i\mathbf{I}-\mathbf{A})=n-n_i,i=1,2,\cdots,s$。 接下来研究实对称矩阵的对角化。有如下定理: 实对称矩阵的特征值都是实数; $n$阶实对称阵$\mathbf{A}$,总存在正交阵$\mathbf{Q}$,使得$\mathbf{Q}^{-1}\mathbf{A}\mathbf{Q}$是对角阵; 实对称矩阵属于不同特征值的特征向量正交。 二次型 二次型概述 $$Q(x_1,x_2,\cdots,x_n)=\sum_{i=1}^n\sum_{j=1}^na_{ij}x_ix_j, a_{ij}=a_{ji}=\vec{x}^\mathrm{T}\mathbf{A}\vec{x}$$ 如上形式称为$n$元二次型。$\mathbf{A}$是二次型的矩阵,它是对称矩阵。还引入了实二次型和复二次型的概念。 $\mathbf{A},\mathbf{B}$是两个$n$阶方阵,如果存在$n$阶可逆矩阵$\mathbf{P}$,使得$\mathbf{B}=\mathbf{P}^\mathrm{T}\mathbf{A}\mathbf{P}$,则称$\mathbf{B}$与$\mathbf{A}$相合或合同。这是个等价关系。 二次型的标准形 有下面3个方法化二次型为标准形: 主轴定理:任一实二次型$Q(\vec{\alpha})=\vec{x}^\mathrm{T}\mathbf{A}\vec{x}$,其中$\mathbf{A}^\mathrm{T}=\mathbf{A}$,存在正交线性替换$\vec{x}=\mathbf{P}\vec{y}$,其中$\mathbf{P}$是正交矩阵,使得$Q(\vec{\alpha})$化为标准形,即$Q(\vec{\alpha})=\lambda_1y_1^2+\lambda_2y_2^2+\cdots+\lambda_ny_n^2$,其中$\lambda_1,\lambda_2,\cdots,\lambda_n$是$\mathbf{A}$的$n$个特征值; 任何一个二次型都可通过可逆线性替换(配方法)化成标准形; 对每个实对称矩阵$\mathbf{A}$,存在初等矩阵$\mathbf{P}_1\mathbf{P}_2\cdots\mathbf{P}_s$,使得$\mathbf{P}_s^\mathrm{T}\cdots\mathbf{P}_2^\mathrm{T}\mathbf{P}_1^\mathrm{T}\mathbf{A}\mathbf{P}_1\mathbf{P}_2\cdots\mathbf{P}_s=\operatorname{diag}(d_1,d_2,\cdots,d_n)$。 对于方法3,构造一个$2n\times n$的矩阵$\begin{bmatrix}\mathbf{A}\\\mathbf{I}\end{bmatrix}$,对$\begin{bmatrix}\mathbf{A}\\\mathbf{I}\end{bmatrix}$作一次列变换的同时,对$\mathbf{A}$作一次对应的行变换,当$\mathbf{A}$化作$\mathbf{P}^\mathrm{T}\mathbf{A}\mathbf{P}$时,$\mathbf{I}$就化作$\mathbf{P}$。 惯性定理和二次型的规范形 形如$z_1^2+z_2^2+\cdots+z_r^2$的二次型称为复系数的二次型的规范形;形如$z_1^2+z_2^2+\cdots+z_p^2-z_{p+1}^2-\cdots-z_r^2$的二次型称为实二次型的规范形。$p$称为正惯性指数,$r-p$称为负惯性指数,差$p-(r-p)$称为符号差。 有如下定理: 任意一个复系数的二次型,总可以经过一个适当的可逆线性替换,化为规范形,且规范形唯一; 任意一个复数的对称矩阵相合于$\begin{bmatrix}\mathbf{I}_r&\mathbf{0}\\\mathbf{0}&\mathbf{0}\end{bmatrix}$,其中$r$是对称阵的秩; 惯性定理:任意一个实二次型,总可以经过一个适当的可逆线性替换,化为规范形,且规范形唯一; 任意实对称矩阵相合于对角阵$\begin{bmatrix}\mathbf{I}_p&&\\&-\mathbf{I}_{r-p}&\\&&\mathbf{0}\end{bmatrix}$,其中$r$是对称阵的秩。 实二次型的正定性 设$Q(\vec{\alpha})$是实二次型,对非零向量$\vec{\alpha}$,恒有$Q(\vec{\alpha})\gt0$,则称实二次型正定,其矩阵称为正定矩阵。 有如下性质: 二次型经过可逆线性替换,其正定性不变; 实对称矩阵正定$\Leftrightarrow$所有特征值都是正的; 实二次型正定$\Leftrightarrow$正惯性指数$p=n$; 实对称矩阵$\mathbf{A}$正定$\Leftrightarrow\mathbf{A}$与$\mathbf{I}$相合; 实对称矩阵$\mathbf{A}$正定$\Leftrightarrow$存在可逆矩阵$\mathbf{C}$,使得$\mathbf{A}=\mathbf{C}^\mathrm{T}\mathbf{C}$; 实对称矩阵$\mathbf{A}$正定$\Leftrightarrow$各阶顺序主子式大于零$\Leftrightarrow$各阶主子式大于零。 类似地,可以引入半正定、负定、半负定、不定的概念。 设$Q(\vec{\alpha})=\vec{x}^\mathrm{T}\mathbf{A}\vec{x}$是半正定实二次型,等价于下列命题: 正惯性指数$p=r<n$,$r$是$\mathbf{A}$的秩; $\mathbf{A}$相合于$\operatorname{diag}(\mathbf{I}_r,\mathbf{0}), r<n$; 存在非满秩$n$阶方阵$\mathbf{C}$,使得$\mathbf{A}=\mathbf{C}^\mathrm{T}\mathbf{C}$; 所有特征值非负,且有零特征值; 所有主子式大于等于零,且有主子式等于零。

2018/10/23
articleCard.readMore

微信抢票

大作业报告 产品目标 相较于原来的项目,我会想到了以下可能的功能性需求 邮箱验证:部分用户很担忧密码被泄露(见知乎); 微信扫码检票:活动发布者能够更加方便的检票; 脱离于微信的完整Web前端:使得用户可以在没有手机的情况下使用产品。 此外还有一些更加面向程序员的非功能性需求: RBAC:基于Role的访问权限控制(因而也就有了独立的认证机制) 前后端分离:无HTML模板,后端只负责提供动态内容而非表现; RESTful:AJAX部分的API设计符合RESTful语义; 实时消息:借助 Socket.IO,任何数据库的更新可以及时反馈到终端; Scalable:状态管理及消息分发依托数据库(除缓存),支持多种分布式解决方案; 安全:使用jwt,微信认证借助OAuth,用一次性token防回放攻击; 日志:良好的日志信息,易于定位错误; PWA:前端为单页应用,同时拥有Service Worker,像本地应用那样的前端体验。 总体来说,我更侧重于独立于微信的部分的开发,而不是依附于微信的那部分。 设计实现 后端架构 后端主要使用了Koa。RestFul部分的API直接调用Mongoose手写。微信部分API调用的是co-wechat系列的支持Promise的。Socket.IO负责消息实时推送,由于需要跨多个节点进行消息分发,因而也就引入了Redis作为消息的一个载体。此外Redis还用于存放一些一次性的token和一些临时信息。 细致来说,目前Mongodb数据库里有四张表 用户:存储了用户的权限,密码加盐存储及其他信息。 微信用户:之所以将这个表独立于用户表,是为了更加灵活地绑定解除绑定等等的操作。 活动表: 电子票表: 权限的主线在于JWT。用户可以通过用户名密码登录,或是续尚未过期的JWT或是使用微信OAuth登录等方式获得JWT。JWT中间存放了很关键的权限信息,是每一次需要特殊权限的借口调用的凭证。 所有的和验证相关API都是返回一个一次性使用的token,这些token存于redis中,主要有以下几种: 用户注册信息:如邮箱验证(这样一份激活邮件只能使用一次) 用户回话信息:有些回话可能因为跳转的缘故不那么容易保持,比如微信的OAuth认证,我们不信任前端发来的openId(原紫荆之声的实现)所以有了OAuth,但把不怎么能撤回的无状态的JWT放进URL真的是个非常危险的行为。因而我们会给用户一个临时的token用以兑换一个JWT。 电子票临时检票信息:当用户打开电子票的时候,其上的二维码也是一个一次性的token(2分钟有效期),如果在这期间未能检票需要重新刷新。 其他所有的RESTful部分的API都是符合语义的,这里不再细说。每个API都是使用了符合JSON SCHEMA标准的ajv库验证输入的。此外查询部分的API还做了分页处理。涉及到文件上传的Multipart部分的文件检查是在上传过程中进行的,任何出错(如文件类型不对、文件过大)都会直接终止上传。 Socket.IO 的实时消息是做在Mongoose hook那一层的,因而基本上任何数据库更改都会实时反馈给相关的用户。 为了弥补NodeJS单线程的不足,我做了多进程的支持。 前端架构 前端贡献了代码的绝大部分。还是使用了Vue全家桶包括vuex、vue-router。UI使用的Vuetify。 前端部分并不比后端部分简单。实际上页面数目众多、交互复杂。 活动列表和电子票详情页面是支持类似知乎的infinity scroll。用户可以不停地往下滑,讲侦测用户滚动来实现按需加载。 抢票的时候需要与服务器同步时间,我这里采用的方法是用WebSocket向服务器发10条请求,取中位数分别作为ping和时间差。实际的抢票按钮再稍早于服务器时间一整个ping的时间开放允许点击(半个ping是数学期望50%刚好时间okay,改为一个ping为了避免偏差)。 扫码和分享的部分调用了微信JS SDK。用户分享能配个图啥的,感觉会好一点。 测试方案 使用了Mocha进行自动化测试,严格意义上可能这算是功能测试而非单元测试。此外还有手动的功能测试,主要是使用了Postman之类的RESTful API工具。 由于项目设计之初所有的API调用都嵌死在了RESTful API里,所以单元测试可能并不容易实现。这个我以后的项目可能会改进。 不过诚然,项目对于我而言是极其庞大的(不包括测试代码,只是运行的那部分前后端代码就已经达到了1万行),因此测试真的很重要。之后的大作业会尝试测试驱动。 性能方面测试的不多。只是先前随意测试,基本500到600每秒并发应该是okay的,带宽才是最重要的。 系统部署 考虑到助教提供的服务器出口带宽有限,我使用了自己的一台VPS szp15.cn。该域名尚在备案中。 Nginx方面主要负责静态文件(包括前端和上传的文件),开启了静态gzip查件,使得后缀为gz的静态文件能被直接压缩地发往用户。此外用Nginx作为反向代理还解决了SSL证书的问题。目前证书来源于Let’s Encrypt。 Mongodb开启了权限验证,不过由于本身不监听公网端口,倒也无所谓。 用户指南 删除旧用户 & 用户注册 用户是软删除,但其实除了数据库应该是对其他地方透明的。也可以禁掉用户,之后可以解禁。此外用户创建是可以借助邮箱的。 添加活动 & 修改微信菜单 上述视频出现了个小bug,在于修改活动的时候,后台因为前端请求多了个字段反回了400错误。该bug原因在于后端这里活动的“短名称”是创建之后不能修改的,旧版可修改。前端还没及时更新。 真的懒得fix这个bug了。反正大作业是写后端,不过变相说明我后端对于请求检查是很严的,anyway不是500而是400。 绑定微信 & 抢票 & 退票 微信绑定中的一闪而过的“登录中”其实是微信弹出的OAuth验证进度条。 演示了微信抢票和Web抢票,Web抢票其实有同步时间的过程,但已经完成。 整个系统都是实时通信的,唯独余票数目需要手动刷新,这是因为本来计划余票数目借由redis广播debounce之后再推送给所有客户端,属于单独的一条推送线路,然而比较复杂所以没写完。 微信扫码检票 这里借助手机的前置摄像头看了一下电脑的情况,检票成功后客户端是同步收到电子票检票成功的推送的。电子票的二维码是临时生成、每次都不同、只有2分钟有效期且一次性使用的。(可以等个2分钟看手动刷新界面) 用户管理 演示了RBAC和用户屏蔽。

2017/10/16
articleCard.readMore

操作系统复习笔记

主要是操作系统的笔记。 操作系统复习进度表 进度表:操作系统 1. 第1章 概述 2. 第2章 进程管理 3. 第2章 死锁问题 4. 第3章 存储管理 5. 第4章 IO设备管理 6. 第5章 文件系统 7. 第6章 操作系统与安全 进度表:Petri网 1. 第2章 Petri网 2. 第2章 着色Petri 3. 第3章 基于Petri网的缺失事件高效修复 4. 第5章 清洗结构化事件日志 5. 第6章 过程模型相似性法 进度表:中间件 1. 第01章 中间件概述 2. 第02章 GlassFish简介 3. 第02章 J2EE 4. 第03章 EJB 3.0入门 - Part I 5. 第03章 EJB 3.0入门 - Part II 6. 第03章 EJB 3.0入门 - Part III 7. 第03章 EJB 3.0入门 - Part IV 8. 第04章 Workflow Technology 9. 第05章 JBPM介绍 10. 第06章 数据库访问中间件-ADO.NET 11. 第06章 数据库访问中间件-JDBC 笔记 操作系统 概述 操作系统分类: 无操作系统:纸带 单道批处理系统:减少手工操作 vs 调试比较困难,CPU和I/O设备使用忙闲不均 多道批处理系统:并行 分时系统:交互(CTSS,MULTICS,UNIX) 实时操作系统:实时性、可靠性 实时过程控制 实时通信处理 其他分类: 嵌入式操作系统 个人计算机操作系统 分布式操作系统 处理器状态(存于PSW): 管态(系统调用引起访管中断进入):特权指令如硬件、IO、页表、系统状态 目态(修改PSW) 中断(中断向量进入异常处理程序): 同步中断:异常(错误、陷阱和中止) 异步中断: 可屏蔽中断:I/O中断 不可屏蔽中断:硬件故障 进程管理 进程(动态性、独立性、并发性,系统调用创建,PCB存储状态)包括: 代码、数据 寄存器(各线程独立) 堆(动态分配)、栈(存储局部变量和上下文信息,各线程独立) 系统资源(地址空间、打开的文件) 三种基本状态: 运行状态:小于CPU数目 就绪状态:可被运行 阻塞状态 被组织成状态队列:运行、就绪、阻塞。 线程(并发、共享,执行流,TCB存储状态,状态同进程): 寄存器 栈 进程间通信IPC(满足互斥、同步的需求): 低级通信:信号量(semaphore)、信号(signal) 高级通信:共享内存(shared memory)、消息传递(message passing)、管道(pipe) 竞争状态: 临界区:完成互斥共享资源访问的程序段 临界资源:互斥共享资源 互斥访问四条件: 两个进程不同时进入临界区 CPU个数和运行速度无关 临界区外进程不妨碍其他进程进入临界区 有限时间内被满足进入 互斥实现: 基于关闭中断(不能用户、多CPU) 基于繁忙等待 加锁标志位法:while (lock); lock = 1; 临界区; lock = 0;,对lock的竞争状态问题 强制轮流法:while (turn != 0); 临界区; turn = 1;,违反3 Peterson 方法:enter_region(0); 临界区; leave_region(0); void enter_region(int process) { int other = 1 - process; interested[process] = true; turn = process; while (interested[other] == true && turn == process); } void leave_region(int process) { interested[process] = false; } 信号量(P原语获取资源、V原语释放资源) 信号量实现的互斥机制: semaphore mutex(1); P(mutex); 临界区; V(mutex); 信号量实现的同步机制: semaphore Buffer; semaphore Data; // Compute while (计算未完成) { 得到一个计算结果; P(Buffer); 将数据送到缓冲区; V(Data); } // Print while (打印未完成) { P(Data); 从缓冲区中取一数据; V(Buffer); 打印该数据; } 生产者消费者问题: 使用互斥锁保护缓冲区。 哲学家就餐问题: void philosopher(int i) { while (true) { think(); take_forks(i); eat(); put_forks(i); } } void take_forks(int i) { P(mutex) state[i] = HUNGRY; test(i); V(mutex); P(s[i]); } void test(int i) { if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING) { state[i] = EATING; V(s[i]); } } void put_forks(int i) { P(mutex); state[i] = THINKING; test(LEFT); test(RIGHT); V(mutex); } 进程调度:CPU繁忙(大时间片) vs I/O繁忙(高优先级),抢占式(分时、实时) vs 非抢占式(多批道) 衡量标准: 用户视角:平均周转时间$T = \frac{1}{N} \sum\limits_{i = 1}^N T_i$,平均带权周转时间$W = \frac{1}{N} \sum\limits{i = 1}^N \frac{T_i}{r_i}$,等待时间,响应时间 系统视角:吞吐量、CPU利用率,设备均衡利用。 进程切换算法: 批处理系统: 先来先服务FCFS 短作业优先SJF 时间片轮转法RR(循环遍历队列),时间片一般20-50ms 优先级算法(抢占 vs 非抢占,静态 vs 动态) 按优先级分组,不同级别按优先级执行,同级别按RR(高优先级阻塞时发生优先级反转) 多级反馈队列,默认优先级最高。时间片内完成提升优先级,时间片内未完成降低优先级。高优先级时间片短。同级别按FCFS。 死锁问题 资源分两类: 可抢占 不可抢占(死锁的主要原因) 死锁发生的条件(用资源分配图进行描述): 互斥条件 请求和保持条件 不可抢占条件 环路条件 死锁的应对策略: “无为而治” 死锁的检测和解除(检测:借助资源图判断环路或可否简化,解除:剥夺资源、进程回退、撤销进程) 动态避免:安全状态保证进程能结束,不安全状态不保证。下列等式始终满足 银行家算法 死锁预防 抢占式分配资源(破坏1) 一次性分配所有资源(破坏2) 资源编号,按序分配(破坏4) 银行家算法:不断判断分配资源后是否会进入不安全状态,如果会进入则不批准分配资源。($n$:进程数,$m$:资源数,$\vec{E}$:总资源向量,$\vec{A}$:空闲资源向量,$C_{n \times m}$:当前分配矩阵,$R_{n \times m}$:当前请求矩阵) $$\sum{i = 1}^n C_{ij} + A_j = E_j$$ 其安全性算法如下: $\vec{W}=\vec{A}$(表示资源)。$\vec{F}={(false)}_{m \times 1}$(表示完成与否) 寻找 $F_i=false$且$\forall 1 \leq j \leq m, R_{ij} \leq W_j$。找不到跳转4 $\forall 1 \leq j \leq m, W_j = W_j + C_{ij}$。$F_i = true$。跳转2 如果$\vec{F}={(true)}_{m \times 1}$那么安全,否则不安全 存储管理 存储管理: 单道程序存储管理(分系统区、用户区,无保护) 分区存储管理,多道/分时(分系统区、用户区,用户区又被划分) 固定分区(划分大小不同,内碎片) 动态分区(链表维护,外碎片):分配(最先匹配法、下次匹配法、最佳匹配法、最坏匹配法),回收(合并) 地址映射(逻辑地址$\rightarrow$物理地址): 静态地址映射:对指令代码进行修改 动态地址映射:硬件,含保护,分段 vs 分页(基地址+限长寄存器) 程序内存布局: 动态地址映射包括: 分页(逻辑页面Page$\rightarrow$物理页面即页框Page Frame,大小确定,内碎片,需要PTBR和PTLR):页表(逻辑页面号Page$\rightarrow$物理页面号,TLB缓存映射),物理页面表(物理页面空闲与否+空闲页数,位示图) $$\text{逻辑地址} = \text{逻辑页面号} \times \text{页面大小} + \text{页内偏移地址}$$ 分段(分代码段、数据段、栈段等,段号$\rightarrow$基地址和长度,外碎片,需要STBR和STLR) 段页式存储管理。先把程序划分为段,然后在段内分页 分页可以进一步使用虚拟内存(局部性原理),缺页中断后载入页面(后备存储上的后备页面)。页表项包含(驻留位、保护位、修改位、访问位)。 页面置换算法: 最优页面置换算法OPT:等待时间最长 最近最久未使用算法LRU(可以使用页面链表、活动页面栈、时间戳或近似算法实现) 最不常用算法LFU 先进先出算法FIFO(性能较差) 时钟页面置换算法Clock(FIFO+跳过访问过的页面,定期清0) 工作集:当前使用的逻辑页面集合$W(t,\Delta)$(进程属性)。工作集大小在局部性区域的位置改变时,快速扩张和收缩过渡到一个稳定值。 驻留集:驻留在内存当中的逻辑页面集合(系统决定)。 各种访问时间计算和地址计算。 页面分配策略: 驻留集大小固定(进程内置换)。 可变分配策略(全局置换)。缺页率算法。 页表结构: 多级页表(二级页表,32位) 反置页表:物理页面号作为索引,页表项个数仅与物理内存大小相关,页表项内记录进程号和进程中的逻辑页面号。 Windows的存储管理: 低2G为用户空间,高2G为内核空间,虚拟页式存储(4K页大小,二级页表) 逻辑空间划分为区域,用VAD指明属性 努力维持空闲物理页面。 IO设备管理 设备文件: 块设备(数据块,有地址) 字符设备(字符,无寻址) I/O单元由两部分组成: 机械部分 设备控制器(芯片)或适配器(印刷电路卡插入扩充槽) 通信方法: I/O独立编址:I/O端口地址对应于控制器中的每一个寄存器(与内存无关) 内存映像编址:内存地址对应于控制器中的每一个寄存器(一般是高位) 混合编址:寄存器采用独立编制,数据缓存区采用内存映像编址 I/O控制方式: 程序循环检测方式(浪费CPU) 中断驱动方式 直接内存访问方式(借助DMA控制器,通过中断表示完成IO操作) 程序/OS的接口(设备独立性,统一命名,阻塞 vs 非阻塞)。 OS/设备驱动的接口(块设备、字符设备和网络设备,分层),由底层到高层: 中断处理程序 设备驱动程序:与中断协调方式 直接调用(适合互斥资源) 借助请求队列对I/O进行优化 设备独立的I/O软件(缓冲技术、数据块大小无关) 用户空间的I/O软件(库函数,Spooling技术) 磁盘结构(由大到小):柱面、磁道、扇区。 格式化: 低级格式化:标出磁道和扇区,扇区包括相位编码(魔数、柱面号、扇区号、扇区大小)、数据区和纠错码 分区:逻辑分区 高级格式化:每个逻辑分区生成一个文件系统 寻址时间: 柱面定位时间 旋转延迟时间(和转速有关) 数据传送时间 磁盘调度算法: 先来先服务FCFS(低效) 最短定位时间优先SSTF 电梯算法:从某个方式移动,直到没有访问请求反方向 文件系统 文件是一种抽象机制,单独的、连续的逻辑地址空间。文件的逻辑结构一般是字节流。用户在其之上构建数据结构。 文件名.扩展名,文件名不超过8个字符,扩展名不超过3个字符(FAT),但很多系统支持长文件名。 文件的分类: 普通文件 ASCII文件 二进制文件 目录文件 文件的属性:权限、创建者、只读/隐藏/系统、时间戳、文件大小。 磁盘空间划分为大小相同的物理块(一个或多个扇区组成),逻辑地址空间也分成大小相同的逻辑块。扇区0称为主引导记录MBR,包含启动盘引导器和分区表。分区表记录了每个分区的起始扇区和大小。 文件控制块FCB包含文件类型、大小、所有者、权限、时间戳和物理块信息。Unix中成为I结点。 文件的物理结构: 连续结构:高效,外碎片,文件不能动态的增长,广泛用于一次性写入介质 链表结构:低效,不适合随机访问。 带有文件分配表的链表结构(FAT):存在一个与物理块一一对应的FAT表,建立索引。 索引结构: FAT有三种版本(12、16、32)决定FAT表项宽度,FAT-32仅用了28位。 表项的个数是物理块的个数。块大小为512的倍数(扇区大小)。 $$\text{FAT表的大小}=\text{FAT表项的个数}\times\text{表项宽度}$$ $$\text{最大容量}=\text{FAT表项的最大个数}\times\text{块的大小}$$ 目录项的内容: 直接法:文件名和FCB,(FAT每一个目录项有32字节,包含起始物理块编号) 间接表:文件名和FCB的地址 内存中的数据结构: 系统内打开文件表:FCB、共享计数 进程内打开文件表:打开方式、读写指针、系统打开文件表中的索引 各种文件系统计算。 空闲空间列表: 位图法 链表法 索引法 操作系统与安全 计算机安全的3个目标: 数据的私密性 数据的完整性 系统的可用性 数据加密: 私钥加密:加密密钥和解密密钥都是秘密的,且可互推 公钥加密:加密密钥公开,和解密密钥不可互推 数字签名:MD5散列 用户认证: 基于密码的认证(已被穷举法攻破) 基于物理对象的认证(IC卡) 基于生物特征的认证 系统内部攻击: 特洛伊木马 登录欺骗 逻辑炸弹 后门 缓冲区溢出 系统外部攻击: 病毒(感染) 可执行程序病毒 常驻内存病毒 宏病毒 蠕虫(传播) Petri网 Petri网概述 $N = (S,T,F)$,$F: S \times T \rightarrow T \times S$。S表示资源集合(含Token的状态)用圆表示,T表示任务集合用方块表示。Marking Vector存储资源状态。 注意:形状及箭头方向和资源图的相反。 Trainsition发生条件: 任务的前缀集全部Mark,后缀集全无Mark 每次仅有一个发生 中间件 中间件概述 中间件网络环境中运行于操作系统与应用软件之间可以简化应用软件的复杂性克服网络环境多种挑战的一类系统软件。 分类: 终端仿真/屏幕转换中间件 数据访问中间件 远程过程调用中间件 面向消息中间件 事务(交易)中间件 对象中间件 作用: 支持软件实体的交互模式(过程、对象、构件与服务) = 支持软件实体的交互质量(可靠性、安全性、高效性) 中间件系列规范:DCE,OMA,J2EE/DNA,SOA 中间件发展趋势:开源化、发散化、易用化、挑战

2017/7/18
articleCard.readMore

服务器配置

这篇文章主要是关于自己VPS的配置的。 Vultr VPS VPS 配置 基础配置 初始操作系统为 Ubuntu 16.04 LTS amd64。初始密码在“产品消息”板块获取。 首先完成用户及 SSH 配置。 # 腾讯默认账号为 ubuntu ssh ubuntu@szp.io # 添加自定义用户 sun sudo adduser sun # 将 sun 加入到默认用户的所有组内 for i in $(groups); do if [ $i != $USER ]; then sudo gpasswd -a sun $i; fi; done # 切换用户 su sun cd ~ # 生成服务器的 ssh 密钥(此时就有了 600 的 .ssh 目录) ssh-keygen # 添加密钥 cd .ssh vim authorized_keys chmod 600 authorized_keys # 退出服务器 ^D ^D # 重新登录,删除默认账号 ssh szp.io sudo userdel -r ubuntu 注意查看/etc/sudoers文件是否需要更新 然后完成hostname和hosts的更改。 sudo sh -c "echo szp-io > /etc/hostname" # 在 127.0.0.1 和 ::1 两行末尾添加 szp-io sudo vim /etc/hosts # 重启生效 sudo reboot 最后更新系统。 sudo apt update sudo apt upgrade sudo apt autoremove # 重启让内核更新生效 sudo reboot 配置开发环境(zsh、vim 和 git) 安装基本软件。 sudo apt install htop git zsh vim 首先配置 git。 git config --global user.name "Sun Ziping" git config --global user.email sunziping2016@gmail.com 而后将.ssh/id_rsa.pub提交到 GitHub 的 SSH 密钥处。 然后依照 https://github.com/robbyrussell/oh-my-zsh 配置 zsh。 sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" # 加入自己的主题,修改`ZSH_THEME`的值为`my_theme` vim .zshrc cat << EOF > .oh-my-zsh/themes/my_theme.zsh-theme PROMPT='%B%F{red}%(?..%? )%F{blue}%n%f%b@%m %B%~%b $(git_prompt_info)%# ' ZSH_THEME_GIT_PROMPT_PREFIX="%B%F{blue}(%F{red}" ZSH_THEME_GIT_PROMPT_SUFFIX="%f%b" ZSH_THEME_GIT_PROMPT_DIRTY="%F{yellow}*%F{blue})" ZSH_THEME_GIT_PROMPT_CLEAN="%F{blue})" EOF # 安装语法高亮 sudo apt install zsh-syntax-highlighting echo "source /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" >> .oh-my-zsh/custom/example.zsh # 启用配置 source .zshrc 最后是 vim。由于服务端的 vim 使用频率较低,不再配置。 配置 Web 服务器与 SSL 证书 安装必要的包。 sudo apt install nginx letsencrypt 根据 https://community.letsencrypt.org/t/how-to-nginx-configuration-to-enable-acme-challenge-support-on-all-http-virtual-hosts/5622 创建/etc/nginx/snippets/letsencrypt-acme-challenge.conf。 location ^~ /.well-known/acme-challenge/ { default_type "text/plain"; root /var/www/letsencrypt; } 创建/etc/nginx/snippets/ssl-redirect.conf。 location / { return 301 https://$host$request_uri; } 以下配置均参照 https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04 。先配置 Diffie-Hellman 。 sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048 创建/etc/nginx/snippets/ssl-params.conf。 # from https://cipherli.st/ # and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; ssl_ecdh_curve secp384r1; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s; # Disable preloading HSTS for now. You can use the commented out header line that includes # the "preload" directive if you understand the implications. add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; add_header Strict-Transport-Security "max-age=63072000; includeSubdomains"; add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; ssl_dhparam /etc/ssl/certs/dhparam.pem; 创建/etc/nginx/snippets/generate-ssl-config.sh。 #! /bin/sh cat << EOF > /etc/nginx/snippets/ssl-$1.conf ssl_certificate /etc/letsencrypt/live/$1/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/$1/privkey.pem; EOF 增加其可执行权限。 sudo chmod +x /etc/nginx/snippets/generate-ssl-config.sh 修改默认配置文件。 sudo rm /etc/nginx/sites-enabled/* sudo rm /etc/nginx/sites-available/default 创建/etc/nginx/sites-available/default.conf。 server { listen 80 default_server; listen [::]:80 default_server; server_name szp.io www.szp.io; access_log /var/log/nginx/szp.io-access.log; error_log /var/log/nginx/szp.io-error.log; include snippets/letsencrypt-acme-challenge.conf; root /srv/http/szp.io; index index.html; } 开启服务器,获取证书。 sudo mkdir /var/www/letsencrypt cd /etc/nginx/sites-enabled sudo ln -s ../sites-available/default.conf . sudo nginx -t sudo systemctl reload nginx sudo letsencrypt certonly --webroot -w /var/www/letsencrypt -d szp.io -d www.szp.io sudo /etc/nginx/snippets/generate-ssl-config.sh szp.io 重新修改/etc/nginx/sites-available/default.conf。 server { listen 80 default_server; listen [::]:80 default_server; server_name szp.io www.szp.io; access_log /var/log/nginx/szp.io-access.log; error_log /var/log/nginx/szp.io-error.log; include snippets/letsencrypt-acme-challenge.conf; include snippets/ssl-redirect.conf; } server { listen 443 ssl http2 default_server; listen [::]:443 ssl http2 default_server; server_name szp.io www.szp.io; access_log /var/log/nginx/szp.io-access.log; error_log /var/log/nginx/szp.io-error.log; include snippets/ssl-szp.io.conf; include snippets/ssl-params.conf; root /srv/http/szp.io; index index.html; } 然后启用新配置。 sudo nginx -t sudo systemctl reload nginx 开启自动续,sudo crontab -e编辑。 30 2 * * 1 /usr/bin/letsencrypt renew >> /var/log/le-renew.log 35 2 * * 1 /bin/systemctl reload nginx 配置 shadowsocks 服务器 安装。 sudo apt-get install python-pip pwgen sudo pip install git+https://github.com/shadowsocks/shadowsocks.git@master 添加一些配置文件。 sudo mkdir -p /etc/shadowsocks sudo bash -c 'cat << EOF > /etc/shadowsocks/server.json { "server":"::", "server_port":$(((2 * RANDOM) % (0xffff - 1024) + 1024)), "password":"$(pwgen)", "timeout":300, "method":"aes-256-cfb", "fast_open": true, "workers": 1 } EOF' sudo bash -c 'cat << EOF > /etc/systemd/system/shadowsocks-server@.service [Unit] Description=Shadowsocks Server Service After=network.target [Service] Type=simple User=nobody ExecStart=/usr/local/bin/ssserver -c /etc/shadowsocks/%i.json [Install] WantedBy=multi-user.target EOF' sudo systemctl start shadowsocks-server@server.service sudo systemctl enable shadowsocks-server@server.service 配置 seafile 服务器 以下步骤依照 https://manual.seafile.com/deploy/using_sqlite.html 。 从 https://www.seafile.com/en/download/#server 上寻找最新二进制版。 sudo -s # 下载解压二进制包 mkdir -p /srv/seafile/ cd /srv/seafile wget https://bintray.com/artifact/download/seafile-org/seafile/seafile-server_6.0.9_x86-64.tar.gz tar -xzf seafile-server_* mkdir installed mv seafile-server_* installed # 下载依赖 apt install python2.7 libpython2.7 python-setuptools python-pil python-ldap python-urllib3 sqlite3 ./setup-seafile.sh auto -n Sunlab -i file.szp.io 接下来是 seafile 的配置。 SEAHUB_PORT=$(((2 * RANDOM) % (0xffff - 1024) + 1024)) SEAFILE_PORT=$(((2 * RANDOM) % (0xffff - 1024) + 1024)) SEAFDAV_PORT=$(((2 * RANDOM) % (0xffff - 1024) + 1024)) # 修改配置 cd /srv/seafile/conf sed -i "s/^SERVICE_URL.*/SERVICE_URL = https:\/\/file.szp.io/" ccnet.conf sed -i "s/enabled = .*/enabled = true/" seafdav.conf sed -i "s/port = .*/port = $SEAFDAV_PORT/" seafdav.conf sed -i "s/fastcgi = .*/fastcgi = true/" seafdav.conf sed -i "s/host = .*/host = 127.0.0.1/" seafdav.conf sed -i "s/share_name = .*/share_name = \/seafdav/" seafdav.conf sed -i "s/port=.*/port=$SEAFILE_PORT/" seafile.conf cat >> seahub_settings.py <<EOF # EMAIL_USE_TLS = False # EMAIL_HOST = 'localhost' # EMAIL_HOST_USER = '' # EMAIL_HOST_PASSWORD = '' # EMAIL_PORT = '25' # DEFAULT_FROM_EMAIL = EMAIL_HOST_USER # SERVER_EMAIL = EMAIL_HOST_USER TIME_ZONE = 'Asia/Shanghai' SITE_BASE = 'https://file.szp.io' SITE_NAME = 'Sun\'s Seafile Server' SITE_TITLE = 'Sun\'s Seafile Server' SITE_ROOT = '/' ENABLE_SIGNUP = False ACTIVATE_AFTER_REGISTRATION = False SEND_EMAIL_ON_ADDING_SYSTEM_MEMBER = True SEND_EMAIL_ON_RESETTING_USER_PASSWD = True CLOUD_MODE = False FILE_PREVIEW_MAX_SIZE = 30 * 1024 * 1024 SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2 SESSION_SAVE_EVERY_REQUEST = False SESSION_EXPIRE_AT_BROWSER_CLOSE = False FILE_SERVER_ROOT = 'https://file.szp.io/seafhttp' EOF 然后创建管理员用户。 cd /srv/seafile/seafile-server-latest ./seafile.sh start ./seahub.sh start-fastcgi ./seahub.sh stop ./seafile.sh stop 最后我们创建必要的脚本。首先是 nginx 与 SSL 配置。 cat << EOF > /etc/nginx/sites-available/seafile.conf server { listen 80; listen [::]:80; server_name file.szp.io; include snippets/letsencrypt-acme-challenge.conf; } EOF cd /etc/nginx/sites-enabled ln -s ../sites-available/seafile.conf . sudo nginx -t sudo systemctl reload nginx sudo letsencrypt certonly --webroot -w /var/www/letsencrypt -d file.szp.io sudo /etc/nginx/snippets/generate-ssl-config.sh file.szp.io 获得证书后,配置好 nginx。 cat << EOF > /etc/nginx/sites-available/seafile.conf server { listen 80; listen [::]:80; server_name file.szp.io; include snippets/letsencrypt-acme-challenge.conf; include snippets/ssl-redirect.conf; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name file.szp.io; include snippets/ssl-file.szp.io.conf; include snippets/ssl-params.conf; proxy_set_header X-Forwarded-For \$remote_addr; location / { fastcgi_pass 127.0.0.1:$SEAHUB_PORT; fastcgi_param SCRIPT_FILENAME \$document_root$fastcgi_script_name; fastcgi_param PATH_INFO \$fastcgi_script_name; fastcgi_param SERVER_PROTOCOL \$server_protocol; fastcgi_param QUERY_STRING \$query_string; fastcgi_param REQUEST_METHOD \$request_method; fastcgi_param CONTENT_TYPE \$content_type; fastcgi_param CONTENT_LENGTH \$content_length; fastcgi_param SERVER_ADDR \$server_addr; fastcgi_param SERVER_PORT \$server_port; fastcgi_param SERVER_NAME \$server_name; fastcgi_param REMOTE_ADDR \$remote_addr; fastcgi_param HTTPS on; fastcgi_param HTTP_SCHEME https; access_log /var/log/nginx/seahub.access.log; error_log /var/log/nginx/seahub.error.log; fastcgi_read_timeout 36000; client_max_body_size 0; } location /seafhttp { rewrite ^/seafhttp(.*)\$ \$1 break; proxy_pass http://127.0.0.1:$SEAFILE_PORT; client_max_body_size 0; proxy_connect_timeout 36000s; proxy_read_timeout 36000s; proxy_send_timeout 36000s; proxy_request_buffering off; send_timeout 36000s; } location /media { root /srv/seafile/seafile-server-latest/seahub; } location /seafdav { fastcgi_pass 127.0.0.1:$SEAFDAV_PORT; fastcgi_param SCRIPT_FILENAME \$document_root$fastcgi_script_name; fastcgi_param PATH_INFO \$fastcgi_script_name; fastcgi_param SERVER_PROTOCOL \$server_protocol; fastcgi_param QUERY_STRING \$query_string; fastcgi_param REQUEST_METHOD \$request_method; fastcgi_param CONTENT_TYPE \$content_type; fastcgi_param CONTENT_LENGTH \$content_length; fastcgi_param SERVER_ADDR \$server_addr; fastcgi_param SERVER_PORT \$server_port; fastcgi_param SERVER_NAME \$server_name; fastcgi_param REMOTE_ADDR \$remote_addr; fastcgi_param HTTPS on; client_max_body_size 0; proxy_connect_timeout 36000s; proxy_read_timeout 36000s; proxy_send_timeout 36000s; send_timeout 36000s; proxy_request_buffering off; access_log /var/log/nginx/seafdav.access.log; error_log /var/log/nginx/seafdav.error.log; } } EOF nginx -t systemctl reload nginx 再配置好启动脚本。 useradd --system --comment seafile seafile --home-dir /srv/seafile chown seafile:seafile -R /srv/seafile cat << EOF > /etc/init.d/seafile-server #!/bin/bash ### BEGIN INIT INFO # Provides: seafile-server # Required-Start: \$remote_fs \$syslog # Required-Stop: \$remote_fs \$syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Seafile server # Description: Start Seafile server ### END INIT INFO # Author: Zheng Xie <xie.zheng@seafile.com> user=seafile seafile_dir=/srv/seafile script_path=\${seafile_dir}/seafile-server-latest seafile_init_log=\${seafile_dir}/logs/seafile.init.log seahub_init_log=\${seafile_dir}/logs/seahub.init.log # Change the value of fastcgi to true if fastcgi is to be used fastcgi=true # Set the port of fastcgi, default is 8000. Change it if you need different. fastcgi_port=${SEAHUB_PORT} case "\$1" in start) sudo -u \${user} \${script_path}/seafile.sh start >> \${seafile_init_log} if [ \$fastcgi = true ]; then sudo -u \${user} \${script_path}/seahub.sh start-fastcgi \${fastcgi_port} >> \${seahub_init_log} else sudo -u \${user} \${script_path}/seahub.sh start >> \${seahub_init_log} fi exit 0 ;; restart) sudo -u \${user} \${script_path}/seafile.sh restart >> \${seafile_init_log} if [ \$fastcgi = true ]; then sudo -u \${user} \${script_path}/seahub.sh restart-fastcgi \${fastcgi_port} >> \${seahub_init_log} else sudo -u \${user} \${script_path}/seahub.sh restart >> \${seahub_init_log} fi exit 0 ;; stop) sudo -u \${user} \${script_path}/seafile.sh \$1 >> \${seafile_init_log} sudo -u \${user} \${script_path}/seahub.sh \$1 >> \${seahub_init_log} ;; *) echo "Usage: /etc/init.d/seafile-server {start|stop|restart}" exit 1 ;; esac EOF chmod +x /etc/init.d/seafile-server update-rc.d seafile-server defaults systemctl daemon-reload systemctl start seafile-server systemctl enable seafile-server 更新 nodejs sudo apt install nodejs npm sudo npm install -g n sudo n latest 安装 cnpm sudo npm install -g cnpm --registry=https://registry.npm.taobao.org cat > .npmrc << EOF registry=https://registry.npm.taobao.org cache=~/.npm/.cache/cnpm disturl=https://npm.taobao.org/dist userconfig=~/.cnpmrc EOF

2017/7/16
articleCard.readMore

中文字符编码

这篇文章,针对字符编码,以C/C++程序员的角度,谈谈我的一些理解。 前言 QAQ,我是被迫发的这个。。迟到的推送。前一阵子,大二的各位同学们都忙于数算大作业不可开交。这次大作业,各种各样的坑不一一列举啦。这里仅仅针对字符编码,以 C/C++ 程序员的角度,谈谈小编的一些理解。如有错误,请多多包含。 GBK GBK 是贵国制定的汉字编码标准。这里 GB 是“国标”的意思,K 就是“扩”展的意思,指的是 GBK 向对于之前的 GB2312 进行了向下兼容的扩展。其中 GB2312 只包含 6763 个常用汉字,而 GBK 包含了几乎所有的汉字字符。 GBK 编码包含了各种简繁体汉字、奇葩符号等等,编码占据 2 字节,范围为 0x8140-0xFEFE。 GBK 的 ASCII 兼容 这里我们可以发现,所有 GBK 编码的首字节最高位均为 1(大于 0x80)。这意味着不需要特殊处理,GBK 编码即可和 ASCII 码(0x00 ~ 0xFF)混合于同一文件中。混合后的字符编码成了变长编码,即首字节最高位为 0 时,字符占据 1 个字节,首字节最高位为 1 时,字符占据 2 个字节。 判断 ASCII 需要依赖变长编码的分割 GB2312 的第二个字节最高位一定为 1。但注意,GBK 编码的第二个字节最高位可能并不是 1。这也就意味着,如果不进行特殊处理,仅用 char 存储 GBK 编码的汉字,一些偏僻的只有 GBK 编码不存在 GB2312 编码的汉字的第二个字节可能会被当作 ASCII 编码的字符处理。如“镕”的 GBK 编码为 0xE946,其中第二个字节的编码对应于 ASCII 中的“F”。如果不分青红皂白对存储着这个字的 char 数组的所有元素调用tolower()之类的函数,“镕”字将变成“閒”(0xE966,其中 0x66 对应于 ASCII 中的“f”)。 字符串匹配需要依赖变长编码的分割 此外,在 GBK 编码中,可能存在一个字的编码,其首字节与另一个字编码的第二个字节一样,其第二个字节又与再一个字编码的首字节一样(有点绕,看例子)。这意味着如果不进行特殊处理,仅用 char 存储 GBK 编码的汉字,搜索算法可能会匹配到截断的汉字。如“你”、“好”这两个字的 GBK 编码分别为 0xC4E3 和 0xBAC3,而还有一个汉字“愫”的编码是 0xE3BA。假设我们的 char 数组里存着 GBK 编码的“你好”:char hello[] = "你好"; // encoded in GBK(C 语言小贴士:这时候hello为 5 个元素的数组:hello[0...1]为“你”,hello[2...3]为“好”,hello[4]为'\0')。若你对整个数组用伟大的 KMP 算法搜索是否存在“愫”这个汉字的时候,你将惊讶的发现,hello[1...2]匹配上了“愫”这个汉字。 Unicode 和 UTF-8 歪楼,好喜欢 Unicode 学生节以及这个谐音(You need code)的创意!(求编辑在这里添加一个很期待很可爱很萌的颜文字)可是。。。可是呜呜,我订的院衫还没来。 QAQ (严肃脸)Unicode 是全球的统一的字符标准。包含各种文字、emoji(求编辑在这里放个翻白眼的 emoji)等等。到目前为止,Unicode 中最长的编码占用 21 位。为了确保可持续发展,Unicode 目前被认为是需要占用 4 个字节。 Unicode 只是给每一个字符定义了唯一的编码,具体存储则可以根据具体需求在 Unicode 编码上再一次进行编码。直接以 32 位定长编码存储,则称之为 UTF-32 或 UCS4;若以 1 ~ 6 个(一般只需要考虑 1 ~ 4 个)8 位变长编码存储,则称之为 UTF-8;若以 1 ~ 6 个(目前只需要考虑 1 ~ 4 个)8 位变长编码存储,则称之为 UTF-8;若以 1 ~ 2 个 16 位变长编码存储,则称之为 UTF-16。限于篇幅,我们只谈谈 UTF-8。 UTF-8 是以 8 位为最小单元去存储,存储方法如下表所示(“U+hhhh”实际上就是 Unicode 编码为“0xhhhh”的字符): UTF-8 字节数 Unicode 编码位数 第一个 Unicode 编码 最后一个 Unicode 编码 字节 1 字节 2 字节 3 字节 4 1 7 U+0000 U+007F 0xxxxxxx 2 11 U+0080 U+07FF 110xxxxx 10xxxxxx 3 16 U+0800 U+FFFF 1110xxxx 10xxxxxx 10xxxxxx 4 12 U+10000 U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx UTF-8 的 ASCII 兼容 UTF-8 编码下首字节的最高位若不是 1,则编码占据 1 个字节,这与 ASCII 相兼容。若最高位是 1,则高位的 1 的个数对应于编码占据的字节个数。 这里举个例子,“你”的 Unicode 编码为 U+4F60,二进制为 01001111 01100000。由上表知,其 UTF-8 占用的字节长度为 3,对应的 UTF-8 二进制编码为 11100100 10111101 10100000,也就是 0xE4BDA0。 判断 ASCII 不需要依赖变长编码的分割 UTF-8 相对于上面所说的 GBK 编码有很多优点。比如,对于非 ASCII 字符,其编码的任一字节最高位都不是 0。这意味着,如果你只是用 char 存储 UTF-8 编码的字符串,不做任何处理,仅通过最高位是否为 1,即可判断字符是否为 ASCII 字符。这对于tolower()之类的函数,无疑是个福音。 字符串匹配不需要依赖变长编码的分割 对于 UTF-8 编码,可以通过判断其高位是否为 10,得知该字节是否是编码的首字节。这同样也就意味着,不可能存在一个字的编码,其前若干字节与其他字符编码的后若干字节相同。这意味着,对于 char 存储的 UTF-8 编码的字符串,若试图搜索子串,不可能出现截断匹配的现象。 Windows 的 GBK 与 UTF-8 兼容之战 GBK 和 Unicode 本身采用了完全不同的字符编码体系,但两者的字符集有很大的对应关系。许多时候对于程序员而言,这两者之间的转换只能靠暴力打表。接下来,我谈谈在 Windows 编程时,由于字符编码大家可能遇到的困惑。 BOM 对于我们可爱的记事本而言,知道一个文本是 GBK 编码还是 UTF-8 编码似乎并不容易。一个最著名的梗便是,用记事本的保存 GBK 编码的文本“联通”,再次用记事本打开将会是乱码。这是由于“联通”的 GBK 编码为 0xC1AACDA8,即 11000001 10101010 11001101 10101000。这与 UTF-8 中 2 字节编码的规则长得太像了,因而被用错误的编码方式打开了。 当然,为了方便记事本识别 UTF-8 文本,微软还是动了一些歪脑经(虽然至今并未修复上述的 Bug)。其中,最坑爹的莫过于 BOM。当你用记事本以 UTF-8 方式保存文本时,记事本会自动为你在整个文档的最前面插入一个字符“零宽度不可换行的空白符”,Unicode 编码为 U+FEFF,UTF-8 编码为 0xEFBBBF。该字符本来是用来防止在一些不该折行的地方折行,却被微软用作了 UTF-8(也包括其他 Unicode 编码方式)识别的工具。由于其本身是“零宽度”,并不可见,对于用户而言没什么差别。但对于程序而言,这无疑是个灾难。 以这次大作业为例。这次作业助教发布的“字典.dic”文件便是带着 BOM 字符的,标准 C++ 组件并不对 BOM 字符进行特殊处理,因而实际读入该文件第一行“一一列举”,转成 Unicode 存储时,其长度将会比正常的字符串多 1 个字符,为 5。 对于这个问题,建议大家可以在代码中对于 BOM 进行特判,或者用 notepad++ 另存为。 程序的字符集 对于中文版 Windows 而言,所有程序默认的显示字符编码都是 GBK。换言之,如果你想在源代码中包含有趣的中文字符串,若想在 Windows 上显示而不产生乱码,则必须采用 GBK 保存源代码。这样编译器便会把字符串按照 GBK 编码编译到程序中,并最后以 GBK 编码显示到各种地方。 悲伤的是,除了 Windows,其他操作系统都默认使用 UTF-8。这也就为什么我们不建议在代码中包含中文。如果你在源代码中包含了中文,而不愿意写任何平台相关的代码,势必你的代码只能在 Windows 或其他操作系统上的一个里面保持不乱码的输出。 当然控制台窗口也如上所说,默认输出编码为 GBK。因而,如果你将这次大作业读入的 UTF-8 或者转换得到的 UTF-32 编码的文字直接输出到标准输出上,势必会导致乱码。对此,没有任何简单的解决方案。 (PS 小编本人觉得,大家在代码里尽量保持纯 ASCII 编码不失为一个好习惯。注释也可以尝试用英文写。 C++ 其实这次大作业可以不必将 UTF-8 编码的文件进行转码。但是变长编码终究会带来很多不变。幸运的是,C++11 确实提供了一些用于 UTF-8 转码的库。这点我相信大家也已经有所体会,小编便不再多说啦。这里提一下平台不同导致的一些问题。一般而言,Windows 上 wchar_t 的大小为 16 位,并不一定能存储所有的 Unicode 字符,而其他平台上基本是 32 位。这也意味着,如果遇到文本中存在着类似 emoji 这类 Unicode 字符时,Windows 上的 C++ 标准库将无能为力。

2016/11/18
articleCard.readMore

Pug学习笔记

这篇博客是学习Pug的笔记。 文档类型(Doctype) 通过doctype html可以声明一个文档是html,此外html还可以被替换为xml、transitional、strict、frameset、1.1、basic、mobile和plist。也可以自定义doctype。 注意:doctype会进一步影响到文档的渲染。 标签(Tags) 以文字开头的一行表示一个标签,缩进表示嵌套。 也可以通过tag1: tag2,在一行内创建嵌套的标签。 对于诸如img、meta、link等的自闭合标签,Pug会自动处理。也可通过后缀(属性之后)/强制闭合。 属性(Attributes) 紧跟在标签后形如(name=value, ...),其中value可以是任意JavaScript。分隔的,可省略。value会被转义,如果需要避免转义,用!=替代=。 对于布尔属性,value也可以是布尔类型,属性真值同该布尔值。若完全省略=value则属性为真。对于html,Pug会采用简短形式(即不是value="value"而是value)。 对于单个较长的属性可以采用模板字符串。属性也可以跨越多行: tag ( name1=value1 name2=value2 ... ) 如果属性名包含了如[]和()的奇怪字符,需要用""或者''括注属性名。 对于style属性,value也可以是一个对象。 对于class属性,value也可以是一个包含类名的数组,或一个从类名映射到布尔值的对象。 对于class属性可以采用tag.classname的简便形式;对于id属性可以采用tag#id的简便形式。如果tag是div,则可以省略tag。其余的属性跟在上述简便形式之后。 在普通的属性之后添加&attributes(object),可以把object的属性名和值作为标签的属性名和值。注意:通过这种方式添加的属性不会被转义。 纯文本(Plain Text) |开头的行即为单行的纯文本。文本中可嵌入html。 在属性后添加空格而后紧跟文本,可以在行内嵌入该文本;在标签后紧跟.和换行,而后所有缩进层次内的都将被作为内嵌的文本。 注释(Comments) //开头的行即为单行的注释,该注释会被转换成对应的html的注释。 //-开头的行为单行的不会出现在html中的注释。 //紧跟换行,而后所有缩进层次内的都将被作为注释。 Pug对于html条件注释没有特殊的支持。但是由于所有以<开头的行都会被作为纯文本,因而可直接嵌入html条件注释。 代码(Code) -开头的行即为单行的JavaScript代码,代码执行的结果不会出现在html中。 -紧跟换行,而后所有缩进层次内的都将被作为代码。 =开头的行也为单行代码,但代码执行的结果会转义后出现在html中。其位置也可以紧跟在属性后。 !=开头的行也为单行代码,但代码执行的结果会直接出现在html中。其位置也可以紧跟在属性后。 插值(Interpolation) 纯文本中的#{expression}会被替换成expression转义后的值。如果纯文本中需要#{,可转义为\#{。 纯文本中的!{expression}会被替换成expression的值(不转义)。 标签插值的语法为#[tag text]。由于Pug会去除标签前后的空白字符,因而标签插值的语法对于需要控制空白字符的内联标签非常有用。 条件语句(Conditionals) if-else语句 if-else语句形如: if condition1 ... else if condition2 ... else ... 也可以采用unless语句: unless condition ... 条件部分的括号,及if、else和unless前的-可省略。 case语句 case语句形如: case variable when value ... ... default ... 只有当一个when语句块为空时,该语句块的执行才会移交到下一个when语句块。可以通过- break跳出case语句。 也可在when value和default后,紧跟:后跟上标签,在行内完成语句块。 循环语句(Iteration) each语句形如: each value, key in arrayOrObject: ... else ... 对于数组会遍历数组元素,对于对象会遍历其属性。, key可以省略。else语句块也可以省略,该语句块会在迭代空数组或空对象时被执行。也可以用for替代each。 while语句形如: while condition ... 过滤器(Filters) 过滤器,将纯文本作为输入,其输出的内容再被嵌入到Pug模板的输出中,所有的JSTransformer模块都可以作为过滤器,常见的过滤器有:babel、:uglify-js、:scss和:markdown-it。 过滤器语法如下: :filter(key=value ...) ... 也可以采用单行的形式: :filter(key=value ...) ... 其中(key=value ...)为选项可以省略,如果key为布尔类型,值为真,=value也可以省略。过滤器也可嵌套,语法类似:filter1:filter2。 注意:过滤器的渲染发生在编译阶段。 也可以自定义过滤器: options.filters = { 'my-own-filter': function (text, options) { ... } }; 包含文件(Includes) 包含文件的语法如下: include:filter file 其中:filter可选。如果file是Pug模板文件,会被Pug处理。如果file是其他文本文件,则文本被原处保留。file的路径如果是绝对路径,由options.basedir制定更目录,否则则为想对于当前编译文件的路径。 复用块(Mixins) 复用块的定义语法如下: mixin name(parameters, ...restArguments) ... 使用的语法如下: +name(arguments)(attributes) ... 其中,, ...restArguments可选,所有的实参列表和形参列表连同()也可选。使用语法下面的块可选。通过block,可以在定义处引用使用处的块。attributes为=连接,空格分割的键值对(值已经被转义),连同()也是可选的。通过attributes对象,可以引用使用处的attributes。 模板继承(Template Inheritance) 声明一个可被派生模板修改的块的语法如下: block name ... 其中,包含的子块为派生后默认的块,是可选的。通过在模板开始处声明extends file,可以从file派生出新的模板,然后通过block name可以重新定义块,通过block prepend name或prepend name可以在默认块前添加新的内容,通过block append name或append name可以在默认块后添加新的内容。

2016/9/24
articleCard.readMore

匿名函数

这篇文章是关于如何巧妙地运用模板在C++中复现boost::lambda库。 引言 在2011年,lambda表达式正式进入C++11标准,成为了语法级别的组件。而本文所涉及的C++函数编程,是指在C++11标准前,借助函数调用运算符重载实现的一种编程范式。 boost库里的函数式编程库颇为丰富,其中尤以lambda和Phoenix两个库著名。在此,我们就以boost::lambda作为典型案例,分析C++的函数式编程。在接下来的文章里,我将首先概述函数式编程(第2章),接着分析boost::lambda的应用(第3章)和实现(第4章),最后给出自己改进版的实现(第5章),分析其安全、性能和灵活性。在本文的最后,我将对C++的函数式编程进行一个总结(第6章)。 在这里,我假定读者拥有一定的模板编程技能。对于函数对象、匿名函数等相关概念已经有很多了解的读者可以跳过第2章。 声明:所有的代码都通过了gcc 6.1.1和clang 3.8.0的测试,visual studio 2015的编译器对于部分代码可能会出现错误。 C++的函数式编程 函数对象 借助运算符重载,我们可以构建出函数对象。该技术很早就出现在了C++的标准库里,标准库functional集中地提供了各种各样的函数对象。接下来我以LLVM头文件(这里我对LLVM的代码进行了简化,以增加可读性)作为示例。标准库在头文件functional里定义了std::less,其可能的实现如下: template <class T> struct less { bool operator()(const T &lhs, const T &rhs) const { return lhs < rhs; } }; 因而标准库algorithm里面只带2个参数的std::sort就可以采用如下的实现: template <class RandomAccessIterator> inline void sort(RandomAccessIterator first, RandomAccessIterator last) { sort(first, last, less<typename iterator_traits<RandomAccessIterator>::value_type>()); } 在这里我们通过标准库iterator中的std::iterator_traits进行类型推断得到其迭代器所指的值类型,进而得到可以用于小于比较的函数对象,将之传递给了拥有3个参数的std::sort函数(第3个参数是一个用于比较的函数对象)。 当然,我们惊喜的发现函数对象的语法是和函数指针相互兼容的,考虑带3个参数的std::sort函数的声明,大致是这个样子的: template <class RandomAccessIterator, class Compare> inline void sort(RandomAccessIterator first, RandomAccessIterator last, Compare comp); 假设RandomAccessIterator迭代器其所指的类型叫T,而我们定义了函数bool compareFunc(const T &lhs, const T &rhs)用于比较,当我们调用sort(first, last, compareFunc)时,模板形参Compare的值为bool (const T &lhs, const T &rhs)是一种函数类型,又由于当函数类型作为函数形参的类型时会自动转换为函数指针类型,所以函数形参comp的真实类型为bool (*)(const T &lhs, const T &rhs),而函数形参comp的值为&compareFun。这里,我们看到C++对于旧有的C语言的相关概念惊人的抽象和兼容能力,数组抽象为了容器(序列容器),指针抽象为了迭代器,而函数指针抽象为了函数对象。 接下来我们分析一下性能,先丢结论,函数对象的性能可能高于函数指针。虽然sort函数多传进去一个参数,但由于该参数实际不占任何空间,实际编译后不会存在一个空对象被压入栈的指令。又由于类模板成员函数的内联展开,原来定义对于std::less对象的函数调用运算符会在sort函数内展开成一个小于比较的表达式,相较于函数指针,减少了一次函数调用的开销。所以函数对象在空间开销上不差于函数指针,在性能开销上可能优于函数指针。 匿名函数 脱离C++,所谓匿名函数,就是一个不具有名字的函数。这种函数通常很小,其定义处就是其使用处。考虑C++98,就其语法层面,匿名函数是不直接支持的,但其实通过函数对象,我们可以构建出类似的语法。考虑上面第3行代码,我们希望最后的代码长成这个样子: sort(first, last, _1 < _2); 形式上,_1 < _2就像是定义了一个匿名函数,并将这个函数作为sort的第3个参数传了进去。实际上,_1 < _2就是所谓的lambda表达式,该表达式产生一个类似std::less的函数对象,_1代表该函数对象的第一个参数,同理_2代表该函数对象的第二个参数。 这里我们可以看出实现这样一个lambda表达式有两个疑问: _1和_2改如何实现?就上面的代码,我们可以看出来那必须是某一个全局对象。 <该如何实现?似乎就上面的代码,我们可以假象有这么一个全局作用域的operator < (),当其左右操作数分别是_1和_2时会返回一个类似std::less的对象。 不论如何,这至少证明了借助模板、函数对象、运算符重载和几个预定义的全局变量,C++的匿名函数是可以实现的。当然,boost::lambda实现了。 boost::lambda的应用 占位符 诸如_1、_2和_3等的被称之占位符(placeholder)。它们本身形成了一个原子lambda表达式,并分别对应于lambda表达式定义的函数的第1个、第2个和第3个参数。使用的占位符排位最高的那个决定了函数是几元的: _1 + 5 // 一元 _1 * _1 + _1 // 一元 _1 + _2 // 二元 bind(f, _1, _2, _3) // 三元 _3 + 10 // 三元 注意最后一个例子产生的函数忽略了前两个参数。不过,对于lambda表达式产生的函数,永远可以提供给超过其元数的实参,多余的实参会被忽略: int i, j, k; _1(i, j, k) // 返回 i,忽略 j and k (_2 + _2)(i, j, k) // 返回 j+j, 忽略 i and k 所有的lambda表达式产生的函数均为传引用调用,因而作用在占位符上的副作用将作用在实参上: int i = 1; (_1 += 2)(i); // i 现在是 3 (++_1, cout << _1)(i) // i 现在是 4,输出 4 运算符表达式 一条基本的规则是任何一个以lambda表达式作为操作数(子lambda表达式)的运算都是lambda表达式。 不被支持的运算符 一些运算符不被支持,包括::、.、.*、->、->.、new、new[]、delete、delete[]、?:、左操作数不是lambda表达式的=和左操作数不是lambda表达式的[]: int i; _1 = i; // 可以 i = _1; // 不可以。 i 不是lambda表达式 逻辑运算符符合短径求值 bool flag = true; int i = 0; (_1 || ++_2)(flag, i); // i 不会自增 成员指针运算符 成员指针运算符->*可被自由地重载。有两种使用情况: 右操作数是成员变量:返回该成员数据的引用 struct A { int d; }; A* a = new A(); ... (a ->* &A::d); // 返回 a->d 的引用 (_1 ->* &A::d)(a); // 类似 有操作数是成员函数:返回该成员函数的延迟调用 struct B { int foo(int); }; B* b = new B(); ... (b ->* &B::foo) // 返回 b->foo 的延迟调用 // 接下来必须跟随函数参数列表 (b ->* &B::foo)(1) // 可以,调用 b->foo(1) (_1 ->* &B::foo)(b); // 返回 b->foo 的延迟调用 // 并没有任何作用 (_1 ->* &B::foo)(b)(1); // 调用 b->foo(1) 绑定表达式 绑定表达式可以包含以下两种形式: bind(target-function, bind-argument-list) bind(target-member-function, object-argument, bind-argument-list) 绑定表达式延迟对于函数的调用。函数的参数个数与之后给的绑定列表必须一一对应。若返回值推导失败,可以显示指定返回值类型bind<RET>(target-function, bind-argument-list)。 函数指针或引用作为目标 X foo(A, B, C); A a; B b; C c; bind(foo, _1, _2, c)(a, b); bind(&foo, _1, _2, c)(a, b); bind(_1, a, b, c)(foo); 对于上述情况,返回值推到总能成功。对于有歧义的函数,应当通过强制类型转换确定函数。 成员函数作为目标 对象参数既可以是对象的引用也可以是对象的指针: bool A::foo(int) const; A a; vector<int> ints; ... find_if(ints.begin(), ints.end(), bind(&A::foo, a, _1)); find_if(ints.begin(), ints.end(), bind(&A::foo, &a, _1)); 成员变量作为目标 成员目标虽然不是函数,但也可以使用绑定表达式。 延迟常量和变量 通过constant和var可以将其参数转换为返回对应的引用类型的lambda表达式: for_each(a.begin(), a.end(), cout << constant(' ') << _1); int index = 0; for_each(a.begin(), a.end(), cout << ++var(index) << ':' << _1 << '\n'); 这可用于将非lambda表达式转换为lambda表达式,通常这是因为结合性和优先级不满足lambda表达式形成的规则的辅助手段。此外,对于=和[]这两个运算,其左操作数必须是lambda表达式。 boost::lambda的实现 在这里首先感谢shfzhzhr在iteye.com上发表的帖子“C++ Template Metaprogramming——一个小型lambda库的实作”,其中给的减缩版boost::lambda给了我很大的帮助。 整个减缩版约莫300多行,可在附件src/minimal-boostlambda/lambda.h里看到完整的源代码。注意,简化代码只包含占位符和运算符表达式的实现,不包括绑定表达式之类的实现。 概要 需要注意,整个lambda表达式的实现其实有两套参数,一套是构建lambda表达式时,各个操作数组成的函数参数,另一套是调用lambda表达式时的函数参数。具体实现上,针对前者为了惰性求值需要捕获其值(或引用),对于后者并不需要存储,但需要递归地传递给子lambda表达式,更具体一些,后者的传递包含了类型的传递和值(或引用)的传递,类型的传递是为了进行返回值类型推导,用的是成员类型sig::type,而值的传递属于简单的递归,用的是成员函数call()。类型推导使用的是元编程(metaprogramming)的技巧。 构建lambda表达式其实是借助运算符的优先级结合性自然地生成一个嵌套的对象。而调用lambda表达式实际上经历了以下4个步骤: 类型传递 类型推导 值(或引用)传递 求值 接下来,我简要介绍一下boost::lambda的大致实现。lambda_functor是所有lambda表达式得到的最后对象,它为lambda的调用实现一层简要的封装,主要可以用来简化代码,也为后期的类型推断做出一些帮助,其被封装的对象必须实现用于传递参数的类型并最终推导返回值类型的成员类型sig::type,以及用于传递参数的值(或引用)并最终求值的成员函数call()。 placeholder是占位符的类型,它的成员函数call()只会返回某一位置的参数。而lambda_functor_base是所有运算的共同类型,根据其模板参数的不同会有不同的行为(比如加减乘除),它会借助全局的运算符重载被构建出来,并用引用记录下构建它时的所有操作数,而它的成员函数call()会将其调用的所有参数传递给所有操作数,并将得到的所有值进行一次操作后(比如加减乘除)返回。传递是借助一个叫select()的全局函数,如果其第一个操作数是lambda_functor类型,则调用其被封装的对象的名为call()的成员函数,传递给它所有的参数,如果其第一个参数是其他类型,则直接返回该参数。 通俗一点,select()全局函数是递归调用子lambda表达式的中间层,placeholder和延迟常量变量它们本身就是一个原子lambda表达式,不存在子表达式所以无需调用select(),而对于其他的诸如运算符表达式、绑定表达式之类的语法,它们可能存在子lambda表达式,因而也就需要调用select()。 lambda_functor——lambda表达式的封装类型 lambda_functor是所有lambda表达式最后得到结构的最外层派生得到的函数对,简化版的ilambda_functor的定义大致长成这个样子: template<class T> class lambda_functor: public T { public: typedef T inherited; lambda_functor() {} lambda_functor(const lambda_functor& l) : inherited(l) {} lambda_functor(const T& t) : inherited(t) {} ... template<class A, class B, class C> typename inherited::template sig<tuple<A, B, C> >::type operator()(A& a, B& b, C& c) const { return inherited::template call< typename inherited::template sig<tuple<A, B, C> >::type >(a, b, c, cnull_type()); } template<class A, class B, class C> typename inherited::template sig<tuple<A, B, C> >::type operator()(A const& a, B const& b, C const& c) const { return inherited::template call< typename inherited::template sig<tuple<A , B , C> >::type >(a, b, c, cnull_type()); } }; ... class plus_action {}; LAMBDA_BINARY_ACTION(+,plus_action) 其中cnull_type()是一个空类的对象,长这个样子,没有实际的用途: struct null_type {}; static const null_type constant_null_type = null_type(); #define cnull_type() constant_null_type 总的而言lambda_functor会对其被继承的类做一个封装,做到可复制。总的而言,它具有以下3个功能: 将各个不同元的operator()()的参数类型,用boost::tuple封装后通过模板参数传递给基类的sig结构体。并将得到的结果sig::type作为返回值类型。注意:boost::tuple元素的个数就是函数调用参数的个数,它是变长的。 将各种不同元的operator()()函数调用不改变参数const属性传递地给基类指定元数的名为call()的成员函数调用(缩减版的代码里面转为了三元),不足的用一个空类null_type的实例对象填充。不改变参数const属性借助的是函数的const重载。上面的代码省略了其他零元、一元、二元的operator()()的定义。 唯一地标识了lambda表达式,能被更容易地识别出这是lambda表达式。这对于lambda表达式的构建和select()函数的实现是非常有必要的。 这里所涉及的函数参数都属于上文说的调用lambda表达式时的函数参数,上述功能的前两个分别就是类型的传递和值(或引用)的传递。 placeholder —— 占位符 placeholder的组件如下,这里只选取了_2作为例子: enum { NONE = 0x00, FIRST = 0x01, SECOND = 0x02, THIRD = 0x04, EXCEPTION = 0x08, RETHROW = 0x10 }; template<int N, class T> struct tuple_element_as_reference { typedef typename boost::tuples::access_traits< typename boost::tuples::element<N, T>::type >::non_const_type type; }; template<int N, class Tuple> struct get_element_or_null_type { typedef typename tuple_element_as_reference<N, Tuple>::type type; }; template<int N> struct get_element_or_null_type<N, null_type> { typedef null_type type; }; template<int I> struct placeholder; ... template<> struct placeholder<SECOND> { template<class SigArgs> struct sig { typedef typename get_element_or_null_type<1, SigArgs>::type type; }; template<class RET, class A, class B, class C, class Env> RET call(A& a, B& b, C& c, Env& env) const { return b;} }; ... const lambda_functor<placeholder<SECOND> >& _2 = lambda_functor<placeholder<SECOND> >(); 可以看出placeholder其call()成员函数的实现真的非常简单,即返回指定位置的参数,对于_1就是return a、对于_2就是return b,以此类推。 而其用于类型推到的sig则稍微复杂。这里再次提一下sig的模板参数SigArgs实际上就是一个boost::tuple<A, B, ...>,其中A、B等等对应的就是call的模板参数,boost::tuple的模板参数是变长的,具体依赖于调用参数的个数,而sig::type对应的就是call()的模板参数RET。 首先boost::tuples::element<N, SigArgs>::type会返回SigArgs第N个类型,接着tuple_element_as_reference<N>::type会在其上增加一个引用,最后get_element_or_null_type<N, SigArgs>::type实际上是防御性编程,针对SigArgs不为boost::tuple而是null_type做了特殊处理。总的而言,sig<SigArgs>::type的类型便是与之对应的call()的参数的引用类型。 让我们想一下下面这段代码会有什么结果: int i = 0; _2(i); 实际上单看成员函数call()似乎并不产生问题,因为lambda_functor会用cnull_type()填充这些确实的参数,最后_2返回的便是cnull_type()。然而实际上错误会发生在类型推导时,在传给placeholder<SECOND>::sig的模板形参SigArgs的值实际上是boost::tuple<int>,而boost::tuples::element<1, SigArgs>::type便会报错,无法找到对应的参数,实际通常编译器报的错是无法推导出改模板的类型参数。 lambda_functor_base —— 运算符表达式 相对于placeholder,lambda_functor_base不可不说是简明扼要: template<class Act, class Args> class lambda_functor_base; #define LAMBDA_BINARY_ACTION(SYMBOL, ACTION_CLASS) \ template<class Args> class lambda_functor_base<ACTION_CLASS, Args> { \ public: \ Args args; \ public: \ explicit lambda_functor_base(const Args& a) : args(a) {} \ template<class RET, class A, class B, class C, class Env> \ RET call(A& a, B& b, C& c, Env& env) const { \ return select(boost::tuples::get<0>(args), a, b, c, env) \ SYMBOL \ select(boost::tuples::get<1>(args), a, b, c, env); \ } \ template<class SigArgs> struct sig { \ typedef typename \ boost::tuples::element<0,SigArgs>::type type; \ }; \ }; 其中构造函数的参数args是一个boost::tuple的对象,不同于上面的boost::tuple这次它不仅是用来存储类型,还是用来真正存储值(或引用)。实际上它被用于存储运算的所有操作数,对于一个二元运算总共会有两个操作数,因此实际上boost::tuple在上面的例子中是个二元组。 先看call(),其中的select()我们将在之后具体讨论,实际上它是将所有的参数再一次传递给了子lambda表达式,最后将得到的结果进行一次运算返回。至于sig的实现不可不说是简单粗暴,直接返回其左操作数的类型。由于这是减缩版,并不具参考性。个人认为这会是一个bug。 通过下面的这段代码,可以看出如何通过全局作用域重载运算符实现代运算符的lambda表达式的合成。 template<bool If, class Then, class Else> struct IF { typedef Then RET; }; template<class Then, class Else> struct IF<false, Then, Else> { typedef Else RET;}; template<class T> struct const_copy_argument { typedef typename typename IF<boost::is_function<T>::value, T&, const T>::RET type; }; #define LAMBDA_BE(OPER_NAME, ACTION) \ template<class Arg, class B> \ inline const lambda_functor< \ lambda_functor_base< \ ACTION, tuple<lambda_functor<Arg>, typename const_copy_argument <const B>::type> \ > \ > \ OPER_NAME (const lambda_functor<Arg>& a, const B & b) { \ return lambda_functor_base< \ ACTION, tuple<lambda_functor<Arg>, typename const_copy_argument <const B>::type> \ > (tuple<lambda_functor<Arg>, typename const_copy_argument <const B>::type>(a, b)); \ } ... LAMBDA_BE(operator+, plus_action) 这里我们可以看出实际上对于函数类型作为操作数的情况,boost::lambda对此加了引用,这是元编程的手法。限于知识水平,我没有悟出这么做的意义何在。通过const引用,可以成功地捕获住操作数。但需要注意,这里由于采用了const引用捕获其值,因而对于可能改变其值得运算,会有编译不通过的问题。实际的实现应当采用const重载。再次说一下,由于这是boost::lambda的减缩版,并不完全具有参考性,作者只实现了加减乘除4个运算,对于作者的情况这是可以的。 select —— 传递子lambda表达式 对于包含子lambda表达式的情况,select()是一个举足轻重的函数,实际上它的实现很简单,即当其第一个参数为lambda_functor类型的时候便会调用它,递归计算子表达式。如果其第一个参数是普通类型,即为一个被捕获的值,则直接返回它。 template<class Any, class A, class B, class C, class Env> inline Any& select(Any& any, A& a, B& b, C& c, Env& env) { return any; } template<class Arg, class A, class B, class C, class Env> inline typename Arg::template sig<tuple<A, B, C, Env&> >::type select ( const lambda_functor<Arg>& op, A& a, B& b, C& c, Env& env ) { return op.template call< typename Arg::template sig<tuple<A, B, C, Env&> >::type > (a, b, c, env); } ... 局限 类型推导 由于没有采用C++11,boost::lambda借助了traits技术,并通过boost::tuple这一类型作为模板参数在各个函数调用中传递,从而解析出返回类型。其返回类型的推导是较为主观的。例如其加减乘除会认为在其左右操作数都是同一类型时,其返回类型也是该类型。而当遇到其左右操作数不是同一类型的时候,这将出现函数推导错误。当然boost::lambda提供了一套比较复杂的机制,用户可使用模板特化帮助这种情况下的类型推导。这是boost::lambda的一大局限。 参数个数 由于没有变长模板参数,实际上boost::lambda处理的参数个数是有最高限制的,虽然通常情况下这种限制并不容易达到,但无疑,通过重复的把一元、二元一直到九元的情形都写一遍,会造成代码膨胀。对于函数参数个数处理不够灵活又是boost::lambda的一大局限。 基于C++11的lambda实现 C++11对于lambda表达式的实现无疑是一大福音,我认为主要体现在下面3个地方: decltype带来的类型推导:无疑boost::lambda最令人头大的代码集中在类型推导上。借助decltype,我们可以删去所有的基于sig的类型推导,不仅简化代码,还可以更加合理的进行类型推导。 变长模板参数:通过变长模板参数,我们可以用少得多的代码涵盖更多的参数。 std::forward、右值引用和引用折叠:这些语法避免了const重载,也对右值有了更好的支持。代码更短,更安全且适用范围可以更广。 于是,基于C++11,我重新实现了一个精简版的lambda库。我实现了占位符、除了->*之外的所有可重载运算符(包括需要特殊处理的=和[])以及延迟变量的相关语法。其他语法也可以非常简单的扩展。限于篇幅,我就只展现一部分代码。完整的代码可参见附件src/mylambda/lambda.h。 placeholder 变长参数模板的递归实例化 运用变长模板参数可以递归实例化了placeholder,在对placeholder<0>写一个模板特例,即可实现占位符的功能,具体代码如下: template<int N> struct placeholder { template<class Arg, class... Args> auto call(Arg &&arg, Args &&... args) const -> decltype(placeholder<N - 1>().call(std::forward<Args>(args)...)) { do_nothing(arg); return placeholder<N - 1>().call(std::forward<Args>(args)...); } }; template<> struct placeholder<0> { template<class Arg, class... Args> auto call(Arg &&arg, Args &&... args) const -> decltype(arg) { do_nothing(args...); return std::forward<Arg>(arg); } }; #define LAMBDA_PLACEHOLDER(POSITION, NAME) const auto NAME = lambda_functor<placeholder<POSITION> >(); LAMBDA_PLACEHOLDER(0, _1) LAMBDA_PLACEHOLDER(1, _2) LAMBDA_PLACEHOLDER(2, _3) ... 举个例子placeholder<2>().call(a, b, c, d),实际会递归调用placeholder<1>().call(b, c, d),再调用placeholder<0>().call(c, d),这时,匹配上了特化的模板,最后返回的值为c。由于采用了一种叫做perfect forwarding的技巧,这种占位符能够完整地保留下参数类型。同时在这里,可以看到后缀返回值声明配上decltype是如何解决返回值推导的问题的。在C++14的帮助下,我们甚至可以完全省略后缀的返回值声明,代码将更为精简。 细化lambda_functor_base 在我写的库中,为了避免使用boost::tuple存储下捕获的值,我取消掉了原来的lambda_functor_base,转而以多个更加具体的类替代,包括: lambda_binary_functor:所有的二元运算符 lambda_prefix_unary_functor:所有的一元前缀运算符 lambda_postfix_unary_functor:所有的一元后缀运算符 lambda_ref_functor:延迟变量var() 这里就以最为简单的延迟变量为例: template<class Arg> struct lambda_ref_functor { Arg &arg; lambda_ref_functor(Arg &arg): arg(arg) {} template<class... Args> auto call(Args &&... args) const -> decltype(select(arg, std::forward<Args>(args)...)) { return select(arg, std::forward<Args>(args)...); } }; template<class A> inline lambda_functor<lambda_ref_functor<A> > var(A &a) { return lambda_ref_functor<A>(a); } template<class A> inline lambda_functor<lambda_ref_functor<const A> > var(const A &a) { return lambda_ref_functor<const A>(a); } 这是一个很典型的例子,我直接用模板参数Arg直接存储需要捕获的变量,同时借助const重载保留被捕获变量的const属性,这比用boost::tuple更为直接了当。 结束语 早期C++的面向对象采用了颇为烧脑的泛型编程和函数对象的技术,甚至不惜动用元编程进行一些编译期的逻辑。然而随着时代的进步,我们可以看到,C++11就像是一门全新的语言,它摧枯拉朽般地将旧有的拗口的技术全部推翻,代之以简洁明了的语义。然而另一方面,新的lambda表达式的语法,似乎也算是给上文所讨论的一切判了死刑。如今C++已然从语法的层面上支持lambda表达式,但这不代表我们应当忘记模板赋予这门语言的自由。

2016/9/21
articleCard.readMore