在搜索框里输入「latex 算法 宏包」,会返回四个几乎一样的名字——algorithm、algorithmic、algorithmicx、algorithm2e;而你此刻正在调试的导言区,很可能同时加载了其中互不相容的两个。这团乱麻正是本页的主题,因为一旦理清,用 LaTeX 排版伪代码其实很简单。要记住的关键是:伪代码不是代码的粘贴。与姊妹页讲的源代码清单不同,算法是按接近数学公式的方式排版的——变量用斜体,赋值用箭头,关键字用粗体——而且它被放进一个可以用 \ref 引用的带编号浮动体里。下面依次讲:到底该加载哪一对宏包、加载错了会看到什么真实报错、以及两种流派各自完整的写法。
伪代码与 listings 的区别:是粘贴,还是排版
一句话概括:listings 保存字符,伪代码排版含义。 交给 listings 或 minted 的内容按逐字环境处理,输入的每个空格和符号都会原样出现在纸面上——真实源代码正需要如此,这个取舍留给姊妹页。伪代码正好相反。写下 $y \gets 1$,y 会排成斜体变量,\gets 会变成箭头 ←:正文住在数学模式里,而不是逐字环境里。只有关键字以粗体罗马体立起来,与变量在视觉上分开。「变量用斜体、关键字用粗体」这一约定,正是把高德纳《计算机程序设计艺术》以来的教科书写法照搬到纸面上。顺带一提,\gets 这个名字本身就来自计算机科学,读作「x gets 1」,即赋值。
第二个区别是位置。lstlisting 待在正文流里不动,而伪代码按惯例放进 浮动体,与 figure、table 完全一样。于是 LaTeX 会避开难看的分页把它移到合适的位置,在上方加一个带连续编号的「Algorithm 1」标题,并让它出现在 \listofalgorithms 里。只要抓住一个结构性事实——容器(浮动体)和内容(伪代码)来自不同的宏包——那四个容易混淆的名字立刻各就各位。
到底该 \usepackage 哪一个:四个名字的真面目
如果是从头开始写,答案只有两行。要么把 \usepackage{algorithm} 与 \usepackage{algpseudocode} 并排放,要么单独加载 \usepackage{algorithm2e}。除这两种之外的组合基本都会出事。之所以令人困惑,是因为 CTAN 上的宏包名与样式文件名对不上。algorithm.sty 并非来自名叫 algorithm 的宏包,而是来自algorithms 套装(复数形式);algpseudocode.sty 则来自 algorithmicx。也就是说,你在导言区写了两个文件名,实际安装的却是两个名字完全不同的套装。
| 套装 | 包含的样式文件 | 作用 |
|---|---|---|
algorithms | algorithm.sty、algorithmic.sty | 容器(algorithm 浮动体)以及最早的内容 algorithmic;后者用全大写命令(\STATE),可追溯到 1994 年,现已被取代 |
algorithmicx | algorithmicx.sty、algpseudocode.sty、algcompatible.sty | 只有内容。Szász János 于 2005 年的重写,algpseudocode 是其标准布局。它完全不带容器 |
algorithm2e | algorithm2e.sty | 自成一体,容器与内容一并提供。由 Christophe Fiorio 自 1996 年起维护。切勿与 algorithm 同时加载 |
% Camp 1 -- two packages, two jobs. This is the usual choice.
\usepackage{algorithm} % the float: \begin{algorithm}, \caption, \listofalgorithms
\usepackage{algpseudocode} % the body: \begin{algorithmic}, \State, \If, \For
% Camp 2 -- one package does everything. Do NOT also load algorithm.
\usepackage[ruled,vlined,linesnumbered]{algorithm2e}这种分工有多彻底,打开 algorithm.sty 就一目了然:全文不到一百行,而且没有一行提到伪代码。它做的全部事情是加载 float 宏包、用 \newfloat{algorithm}{htbp}{loa} 声明一种新的浮动类型、把它命名为「Algorithm」、再定义 \listofalgorithms——整个宏包就这些(TeX Live 2024 附带的是 2009 年 8 月 24 日的 v0.1)。一个有趣的副作用是:凡是它不认识的选项,都会被当作浮动体的名字。写 \usepackage[Procedure]{algorithm},此后全文的标题就都变成「Procedure 1」「Procedure 2」。
出现 Command \algorithm already defined. 时
这条报错只有一个含义:你同时加载了 algorithm 和 algorithm2e。两者都想创建名为 algorithm 的环境,后到的那个就撞上了先来的。从导言区删掉其中一个即可。同理,加载顺序不同,措辞也不同:若先写 algorithm2e、后写 algorithm,得到的会是 ! LaTeX Error: Command \listofalgorithms already defined.,成因相同。另一条常见的是 ! LaTeX Error: Command \algorithmic already defined.,那是把旧的 algorithmic 与较新的 algpseudocode 一起加载的症状。algorithmicx 一系正是 algorithmic 的后继,二者不必同时使用。
% Each of these three preambles stops the compile.
\usepackage{algorithm}\usepackage{algorithm2e}
% ! LaTeX Error: Command \algorithm already defined.
\usepackage{algorithm2e}\usepackage{algorithm}
% ! LaTeX Error: Command \listofalgorithms already defined.
\usepackage{algorithmic}\usepackage{algpseudocode}
% ! LaTeX Error: Command \algorithmic already defined.
% If a class or a co-author really forces algorithm2e into a document that
% already owns the algorithm float, this option renames the environment to
% algorithm2e and \listofalgorithms to \listofalgorithmes, so nothing collides.
\usepackage[algo2e]{algorithm2e}不过,最麻烦的组合反而根本不报错。把 algorithm2e 和 algpseudocode 并排加载,文档会一声不吭地编译通过。原因是 algorithmicx 在定义 \For、\If 这类面向用户的名字时,会先检查该名字是否已经存在,若已存在就默默放弃定义。于是 algorithm2e 的含义存活下来,直到你写下 algorithmic 环境的正文,才会在 \EndWhile 那一行冒出看似毫不相干的 ! Missing number, treated as zero.。在离病因最远的地方崩溃,某种意义上比干脆报错更难对付。algorithm2e 之所以准备了 algo2e 选项,正是为了化解这类冲突;启用它之后,\listofalgorithms 会变成拼法不同的 \listofalgorithmes——这是该宏包出自法语环境留下的痕迹。
\begin{algorithm} 是浮动体:\caption、\label、[H]
algorithm 环境是真正的浮动体,与 figure、table 完全同义。其默认位置参数是 htbp(依次尝试此处、页顶、页底、独立成页),目录文件扩展名是 .loa。因此图表那套熟悉的做法可以照搬:\caption{…} 生成带编号的「Algorithm 1」标题,紧随其后放 \label{alg:…},正文便能用 \ref{alg:power} 取编号、用 \pageref{alg:power} 取页码。若把 \label 写在 \caption 之前,编号会出错——要么差一,要么取到节号——所以顺序必须遵守。在文档开头写 \listofalgorithms——它是 \listoffigures 与 \listoftables 的同胞——就会得到一份「List of Algorithms」,把编号与 \caption 的文字一一对应。和往常一样,编号、引用与目录需要编译两次才会稳定。如果恰恰不希望它浮动,写 \begin{algorithm}[H] 即可把盒子钉在原处——[H] 无需额外设置,因为 algorithm 已经加载了 float 宏包。
书写正文:\State、\If、\For、\While、\Function
内层环境名叫 algorithmic——这是命名上的最后一个陷阱:宏包叫 algpseudocode,环境却不叫这个名字。所有命令都采用首字母大写:\State 开启一行语句,代码块要显式闭合,如 \If … \EndIf、\For … \EndFor。开头的可选参数设定行号间隔:[1] 每行编号,[5] 每五行编号一次,省略则不编号。下面的例子是二分查找,涵盖了条件分支、循环、函数、前置条件和行末注释。
\documentclass{article}
\usepackage{algorithm}
\usepackage{algpseudocode}
\begin{document}
\listofalgorithms
\begin{algorithm}
\caption{Binary search}\label{alg:bsearch}
\begin{algorithmic}[1]
\Require sorted array $a[1..n]$, key $k$
\Ensure index of $k$, or $0$
\Function{Search}{$a,k$}
\State $lo \gets 1$;\ $hi \gets n$ \label{ln:init}
\While{$lo \le hi$}
\State $m \gets \lfloor (lo+hi)/2 \rfloor$ \Comment{midpoint}
\If{$a[m] = k$}
\State \Return $m$
\ElsIf{$a[m] < k$}
\State $lo \gets m+1$
\Else
\State $hi \gets m-1$
\EndIf
\EndWhile
\State \Return $0$
\EndFunction
\end{algorithmic}
\end{algorithm}
Algorithm~\ref{alg:bsearch} halves the interval; the bounds are set on
line~\ref{ln:init}, page~\pageref{alg:bsearch}.
\end{document}输出是这样的:一个上下带横线的框浮动到页面上,顶部写着「Algorithm 1 Binary search」。框内的 \Require 和 \Ensure 生成粗体标签 Require: 与 Ensure:,二者排在编号之外。编号从 \Function 那行开始,沿左边依次是 1、2、3 …;\Comment{midpoint} 会在行末排出一个小三角:▷ midpoint。\Function{Search}{$a,k$} 中的名字用小型大写字母排版,若要从别的行调用它,就写 \Call{Search}{$a,k$}。既然很多人想换掉这些标签:Require: 与 Ensure: 这两个字符串分别存放在 \algorithmicrequire 和 \algorithmicensure 中,写 \renewcommand{\algorithmicrequire}{\textbf{Input:}} 就能把前者改成 Input:。
| 命令 | 排出的样子 | 备注 |
|---|---|---|
\State | 一行带编号的语句 | 每条语句一个;漏写会接到上一行 |
\Require / \Ensure | Require: / Ensure: | 前置与后置条件,排在行号之外 |
\If ... \ElsIf ... \Else ... \EndIf | if … then / else if / else / end if | 命令拼作 \ElsIf,比 “else” 少一个 e,是常见的打字错误 |
\For ... \EndFor | for … do … end for | \ForAll{…} 排出 for all …;条件放在花括号里 |
\While ... \EndWhile | while … do … end while | 同样形式的 \Repeat … \Until{…} 也可使用 |
\Function ... \EndFunction | function Name(args) … end function | 名字用小型大写;调用写作 \Call{Name}{args}。对应的还有 \Procedure |
\Return | return | 要与 \State 搭配,如 \State \Return $y$ |
\Comment | ▷ 注释文字 | 放在行末;三角符号存放在 \algorithmiccomment 中 |
这一派最常见的事故是漏写 \State,而且它不报错。在 \State $x \gets 1$ 之后直接写 $y \gets 2$,输出会变成「1: x ← 1 y ← 2」——两条语句挤在同一行,编号还少了一个。若发现行数不对,先怀疑这里。另外,如果在旧的会议模板里看到 \STATE、\WHILE、\ENDWHILE 这类全大写命令,那是源自 1994 年的旧 algorithmic 写法。想原样沿用这种稿件,就用 \usepackage{algcompatible} 代替 algpseudocode:大写命令会在 algorithmicx 的底座上照常工作。
给行编号,并引用「第 3 行」
行号是可以引用的。用 [1] 打开 algorithmic 环境,在想引用的那一行末尾放 \label{ln:init},正文中的 \ref{ln:init} 就会展开为该行的编号。上例中 \Function 那行是 1,因此 \ref{ln:init} 得到 2。这一点比听上去更重要:如果手写「第 3 行的判断」,日后只要多加一条语句,这句话就变成了假话。algorithm2e 那边做法相同——启用 linesnumbered,并把 \label{…} 放在结束该行的 \; 之后。只有一条注意:给没有编号的行贴标签毫无意义,因为不指定 [1] 或 linesnumbered 时,根本不存在可指向的编号。
algorithm2e 的写法:用花括号代替 \End…,以及不能忘的 \;
另一派不用 \EndFor 之类的命令来闭合代码块,而是把正文作为花括号参数传入——\For{条件}{正文}、\While{条件}{正文};if–then–else 写作 \eIf{cond}{then}{else}(没有 else 时用 \uIf,想压在一行里用 \lIf)。输入输出有专用命令 \KwIn{…} 与 \KwOut{…},排成粗体 Input: 和 Output:(若更喜欢 Data: 与 Result: 的措辞,就改用 \KwData{…} 和 \KwResult{…})。最大的坑在于每条语句必须以 \; 结尾。忘了不会报错,下一条语句会直接接在同一行。若不想让「;」出现在成品里,用 \DontPrintSemicolon 可以隐藏它(\; 本身仍然必须写)。外观完全由加载选项决定:ruled(上下横线)、boxed(整体加框)、vlined 与 lined(画块竖线的两种方式)、plain(默认),再加 linesnumbered 显示行号。这三个线条选项其实就是替你执行文档内的 \SetAlgoVlined、\SetAlgoLined、\SetAlgoNoLine。TeX Live 2024 附带的是 2017 年 7 月 18 日的 v5.2。
\documentclass{article}
\usepackage[ruled,vlined,linesnumbered]{algorithm2e}
\begin{document}
\listofalgorithms
\begin{algorithm}
\DontPrintSemicolon
\KwIn{an array $a[1..n]$}
\KwOut{the sum $s$ of its positive entries}
$s \gets 0$\;\label{ln:zero}
\For{$i \gets 1$ \KwTo $n$}{
\eIf{$a[i] > 0$}{
$s \gets s + a[i]$\;
}{
\tcp{negative or zero: skip}
}
}
\Return $s$\;
\caption{Sum of positive entries}\label{alg:sum}
\end{algorithm}
Algorithm~\ref{alg:sum} accumulates into $s$, initialised on line~\ref{ln:zero}.
\end{document}输出的标题是「Algorithm 1: Sum of positive entries」——编号后面多一个冒号,这正是它与 algorithm 浮动体在外观上的区别。正文上方是 Input: 与 Output:;正文排成 for i ← 1 to n do 和 if … then … else,因为指定了 vlined,每个块的左侧会有一条竖线。\tcp{…} 是行内注释,输出为 // negative or zero: skip。请注意 \caption 写在环境的末尾——这是 algorithm2e 的惯例;指定了 ruled 时,即使写在末尾,标题依然出现在框的顶部。同样的文字也会进入 \listofalgorithms。另外,algorithm2e 的 algorithm 环境默认同样是浮动体,需要固定时可以写 \begin{algorithm}[H]。
让关键字不用英语:french、german 与 onelanguage
algorithm2e 可以翻译伪代码本身的关键字。只有这一派提供该功能,algpseudocode 没有对应物。可用的选项有 french、german、ngerman、spanish、italiano、portuguese、czech、slovak、croatian、english。不过行为上有个台阶。只写 \usepackage[french]{algorithm2e},浮动体的名字会变成「Algorithme 1 :」,但 \For 和 \eIf 仍然输出英语——要得到法语,得改用法语命令名 \Pour{…}{…}、\Si{…}{…}、\KwA。若不想改写既有稿件里的命令名,就加上 onelanguage:写 \usepackage[french,onelanguage]{algorithm2e} 后,原封不动的 \For 与 \eIf 会排成 pour i ← 1 à n faire 和 si … alors … sinon … fin。德语则用 [german,onelanguage],得到 für i ← 1 bis n tue、wenn … dann … sonst … Ende,标题为「Algorithmus 1:」。
% Keep the English command names, print French keywords.
\usepackage[french,onelanguage,ruled,linesnumbered]{algorithm2e}
% \For{...}{...} -> pour i <- 1 a n faire ... fin
% Without onelanguage, only the float name is translated; use the
% French command names for a French body.
\usepackage[french,ruled]{algorithm2e}
% \Pour{...}{...}, \Si{...}{...}, \KwA究竟该选哪个:algpseudocode 与 algorithm2e 的比较
如果投稿模板指定了其中之一,就照办——许多会议文类是围绕某一种选择来调校页边距和字体的。若没有指定:想把伪代码写成数学式的文字,选 algpseudocode;需要显式的输入输出声明、块竖线或关键字翻译,选 algorithm2e。剩下的用下表定夺。
| 方面 | algorithm + algpseudocode | algorithm2e |
|---|---|---|
packages | 两个:容器 + 内容 | 一个,自成一体 |
block syntax | 显式闭合:\If{…} … \EndIf | 正文作花括号参数:\eIf{…}{…}{…} |
input / output | \Require / \Ensure:前置与后置条件 | \KwIn / \KwOut:显式的输入与输出 |
end of line | 无需标记;由 \State 开启一行 | 必须写 \;;漏写会让下一条语句接在同一行 |
line numbers | 由 \begin{algorithmic}[1] 的可选参数设定间隔 | linesnumbered 选项(rightnl 可移到右侧) |
keyword language | 只有英语;需逐个重定义 \algorithmicwhile 之类 | 有 french、german 等选项;加 onelanguage 可保留英文命令名 |
无论选哪一个,作为出版物的处理方式完全相同:用 \caption 给出带编号的标题,用 \label 和 \ref 从正文指向它,用 \listofalgorithms 生成列表,并编译两次让编号稳定。最后再强调一次,因为这正是本页的全部要点:每份文档只用一个内容宏包。用 algorithm2e 就绝不加载 algorithm;用 algpseudocode 就绝不加载 algorithmic。这一行纪律,几乎可以杜绝开头那四个易混名字引发的所有事故。如果你真正想要的是带语法着色和行号的真实源代码而非伪代码,那么讲 listings 与 minted 的姊妹页才是目的地。