算法环境 algorithm2e

介绍

在LaTeX中使用伪代码来描述算法是常见的需求,LaTeX其实有很多类似名称的宏包,简单辨析一下:(参考latex 中 algorithm、algorithmic、algorithmicx、algorithm2e 的区别

  • algorithm 用来封装算法:给算法加上标题(caption)和标签(label),方便进行索引;
  • algorithmic 相当于算法的内容物;
  • algorithmicx 相当于 algorithmic 的升级版;
  • algpseudocodealgorithmicx 的一种呈现方式,完整包含了algorithmicx
  • algorithm2ealgorithmicxalgorithmic 类似,也是用来描述算法的;但是其语法不如 algorithmicx 直白。

总得来说,通常有两条路线可以选择:

  1. algorithm2e宏包搞定
  2. algpseudocodealgorithmic等撰写算法本身,再用algorithm包给算法加标题

本文主要考虑第一个方式——使用algorithm2e宏包。

在导入algorithm2e宏包时,通常会顺便加上一些选项,例如

1
2
3
4
\usepackage[ruled,linesnumbered,noline]{algorithm2e}
% ruled 上下添加横线,类似于三线表,此时标题在上面
% linesnumbered 显示行数
% noline 关闭默认体现层级的竖线

算法环境的主体框架如下,通常会添加标题(caption)和标签(label

1
2
3
4
5
6
\begin{algorithm}
\caption{XXX}\label{alg1}

...

\end{algorithm}

一个简单的例子:欧几里得算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
\documentclass{article}
\usepackage[ruled,linesnumbered,noline]{algorithm2e}

\begin{document}

\begin{algorithm}
\caption{Euclid's algorithm}
\KwData{Two nonnegative integers $a$ and $b$}
\KwResult{Their greatest common divisor $d = \gcd(a, b)$}
\While{$b \neq 0$}{
$r \leftarrow a \bmod b$\;
$a \leftarrow b$\;
$b \leftarrow r$\;
}
$d \leftarrow a$\;
\end{algorithm}

\end{document}

在导入时使用[ruled,linesnumbered,noline]选项,得到的效果如下图

与之相比,使用默认选项的效果就差很多,并不符合文献中的主流风格

基本使用

以欧几里得算法为例

1
2
3
4
5
6
7
8
9
10
11
\begin{algorithm}
\caption{Euclid's algorithm}
\KwData{Two nonnegative integers $a$ and $b$}
\KwResult{Their greatest common divisor $d = \gcd(a, b)$}
\While{$b \neq 0$}{
$r \leftarrow a \bmod b$\;
$a \leftarrow b$\;
$b \leftarrow r$\;
}
$d \leftarrow a$\;
\end{algorithm}

在算法环境中,每一行语句可以使用\;结尾,这会在文本中加上分号结尾并换行,还可以使用\\强行换行。 除此之外,下面的某些固定语法结构也会导致换行。 算法内部需要数学表达式时,仍然需要$$等来创建数学环境。

算法通常需要使用下面这些预定义宏:

  • IO:

    • 输入:\KwIn{<input>}
    • 数据:\KwData{<input>}
    • 输出:\KwOut{<output>}
    • 结果:\KwResult{<output>}
  • 基础:

    • to: \KwTo
    • 返回值:\KeRet{<value>}或等效的\Return{<value>}
  • 循环:包括下面几种形式,其中含l-前缀的版本是行内形式,即内部不产生换行(但结尾仍然有换行)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    \For{<condition>}{<text loop>}
    \While{<condition>}{<text loop>}
    \ForEach{<condition>}{<text loop>}
    \ForAll{<condition>}{<text loop>}

    \lFor{<condition>}{<text loop>}
    \lWhile{<condition>}{<text loop>}
    \lForEach{<condition>}{<text loop>}
    \lForAll{<condition>}{<text loop>}

  • 条件:包括下面几种形式,默认形式会产生换行并使用end结尾;含l-前缀的版本是行内形式,即内部不产生换行(但结尾仍然有换行);含e-前缀的eIf自带真假两个分支;含u-前缀的版本是未完成形式,即结尾不含end,需要搭配其他命令来收尾

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    \If{<condition>}{<then block>}
    \ElseIf{<elseif block>}{<then block>}
    \Else{<else block>}

    \lIf{<condition>}{<then block>}
    \lElseIf{<elseif block>}{<then block>}
    \lElse{<else block>}

    \uIf{<condition>}{<then block>}
    \uElseIf{<elseif block>}{<then block>}
    \uElse{<else block>}

    \eIf{<condition>}{<then block>}{<else block>}

    \leIf{<condition>}{<then block>}{<else block>}

  • 注释:默认支持两类注释,即tcc(C语言风格/**/)和tcp(C++风格//)注释,有几个选项会调整注释的细节,这里略去

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    \tcc{<comment>}
    \tcc*{<comment>}
    \tcc*[r]{<comment>}
    \tcc*[l]{<comment>}
    \tcc*[h]{<comment>}
    \tcc*[f]{<comment>}

    \tcp{<comment>}
    \tcp*{<comment>}
    \tcp*[r]{<comment>}
    \tcp*[l]{<comment>}
    \tcp*[h]{<comment>}
    \tcp*[f]{<comment>}

对于循环和条件结构的呈现,还涉及到如下几个选项:

  • lined:伪代码的 start-end 之间用竖线相连
  • vlined:伪代码的 start-end 之间用竖折线相连,其中折线代替了end的作用
  • noline:伪代码的 start-end 之间没有线相连

这几个选项既可以在宏包选项中设置,也可以使用下面的命令设置

1
2
3
\SetAlgoLined
\SetAlgoVlined
\SetAlgoNoline

在循环和条件结构中,都支持加上括号包裹的注释选项,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
\lIf(\tcc*[h]{lif comment}){test}{
text
}
\uIf(\tcc*[f]{uif comment}){test}{
then text\;
}
\uElseIf(\tcc*[f]{comment}){test}{
elseif text\;
}
\lElseIf(\tcc*[h]{comment}){test}{
text
}
\lElse(\tcc*[f]{comment}){text}

宏包还支持自定义一些宏,例如下面定义了与IO宏类似的一个命令\KwPara{<input>},代表算法需要的各种细节参数

1
2
3
\SetKwInput{KwPara}{Parameter}

\KwPara{m,n,$\varepsilon$}

除了这个宏包之外,还可以使用其他方式来实现,例如导入algorithmalgpseudocode这两个宏包,本文暂不讨论具体细节,不过可以明确的是,它的命令风格与algorithm2e是截然不同的。

抄录环境 verbatim

LaTeX直接提供verbatim抄录环境,在其中以等宽字体按照原样排版代码,回车和空格都是正常的换行和空位的作用,缩进也是原样保留的。使用示例如下

1
2
3
4
5
6
7
8
\begin{verbatim}
#include <iostream>
int main()
{
std::cout << "Hello, world!" << std::endl;
return 0;
}
\end{verbatim}

效果如下图

注意这不是代码环境,因为它只是忠实地保持原样输出,并没有提供常见的语法高亮功能。 还有与之类似的verbatim*环境,它更进一步将空格显示为

与之类似的,还有展示行内抄录片段或关键字的\verb命令

1
\verb<delim><code><delim>

与常见的命令不同,\verb需要提供自定义的分界符delim来标明代码的前后分界位置,要求前后分界符必须一致,除字母、空格或星号外,可任意选择不与代码自身冲突的符号(习惯上可以使用|符号),例如

1
2
3
4
5
\verb|a\ b|         % 使用|作为分界符 a\ b

\verb+(a || b)+ % 使用+作为分界符 (a || b)

\verb+\ldots+ % 使用+作为分界符 \ldots

代码环境 listings

介绍

这里指的是展示编程语言的源代码片段的环境,与前面的算法环境/抄录环境都是不一样的。

listings宏包是一个功能强大也非常复杂的宏包,用于提供特定编程语言的语法高亮。

1
\usepackage{listings}

下面是最简单的例子,在文档中展示了一段Python代码

1
2
3
4
5
6
7
8
9
10
11
12
13
\documentclass{article}
\usepackage{listings}

\begin{document}

\begin{lstlisting}[language=Python, caption=Demo]
def helloworld():
print("Hello, world!")

helloworld()
\end{lstlisting}

\end{document}

效果如下图(具体效果可能和不同的编译器有关,下图是使用xelatex编译的结果)

除了listingsminted宏包是另一种常见语法高亮方案,它基于Python提供的支持进行语法高亮,在编译时需要传入特殊的选项-shell-escape,暂略。

基本使用

listings宏包最主要的用法是使用lstlisting环境导入代码片段

1
2
3
\begin{lstlisting}
<codes>
\end{lstlisting}

可以加上一些选项,例如指定语言

1
2
3
\begin{lstlisting}[language=Python]
<codes>
\end{lstlisting}

提供的语言类型会被用于识别关键字、字符串、注释等,进而影响具体的显示效果。

listings宏包还支持从源代码文件中直接导入代码,需要使用专门的\lstinputlisting命令

1
\lstinputlisting[language=Python]{main.py}

\lstinputlisting命令默认会导入所有代码,可以设置起止行号来导入源文件中的部分片段

1
\lstinputlisting[language=Python, firstline=2, lastline=12]{main.py}

补充:\lstlistoflistings命令会将所有添加caption的代码片段生成代码目录。

除此之外,listings宏包还提供\lstinline命令用于行内代码片段展示:\lstinline|<codes>|。 与\verb命令类似,它的分界符可以是任意的,从而避免与代码中的字符冲突,例如

1
2
3
\lstinline!var i:integer;!

\lstinline|int x=1;|

配置

listings宏包默认的显示效果很丑,需要经过一番配置才能达到美观的语法高亮显示效果。 配置既可以在具体的环境开头指定,也可以在导言区进行全局默认设置。

对于具体环境的配置通过可选参数给出,例如

1
2
3
4
5
6
7
8
9
10
11
\begin{lstlisting}[
language=Python, % 语言
numbers=left, % 行号显示位置
firstnumber=1 % 行号开始
]
# Python
def hello():
print("Hello, world!")

hello()
\end{lstlisting}

在导言区进行全局默认配置,也就是修改默认样式,例如

1
2
3
4
5
6
7
8
\lstset{
language=Python, % 设置语言
columns = fixed, % 列距排版
basicstyle = \linespread{0.8} \ttfamily, % 设置行距,字体
numbers = left, % 行号显示设置
frame = single, % 背景边框
showstringspaces = false, % 字符串中的空格显示
}

此时就不需要在每个环境中进行配置了。

除此之外,还可以重新定义一套新的样式(而非修改默认样式),例如针对特定的语言进行配置

1
2
3
\lstdefinestyle{cppStyle}{
...
}

对自定义样式的使用通过style选项指定

1
2
3
4
5
6
7
8
\begin{lstlisting}[style=cppStyle]
#include <iostream>

int main() {
std::cout << "Hello, world!" << std::endl;
return 0;
}
\end{lstlisting}

还可以在导言区使用下面的命令将新定义的样式设置为默认样式

1
\lstset{style=cppStyle}

在更丰富的配置中通常会搭配使用xcolor宏包,定义新的颜色用于代码高亮,例如

1
2
3
4
5
6
7
\usepackage{xcolor}

\definecolor{dkgreen}{rgb}{0,0.6,0}
\definecolor{gray}{rgb}{0.5,0.5,0.5}
\definecolor{mauve}{rgb}{0.58,0,0.82}

\definecolor{epubblue}{RGB}{1,126,218}

颜色的使用例如\color{gray}。注意上面的小写rgb接受的参数为0-1的小数值,而大写RGB接受的是0-256的整数值。

下面是listings宏包支持的常见配置选项:

  • 语言设置:language=Python,C++,支持多个语言,注意C++的名称要用C++而非Cpp
  • 基本样式设置basicstyle = \ttfamliy,这是指定等宽字体(符合编程习惯)
  • 样式:主要是颜色和字体设置,针对关键词,字符串和注释
    • 关键字样式:keywordstyle=\bfseries \color[RGB]{40,40,255},通常指定一个颜色
    • 注释样式:commentstyle=\color[RGB]{0,96,96}, 通常指定一个颜色
    • 字符串样式:stringstyle=\color[RGB]{128,0,0}, 通常指定一个颜色
    • 行号样式:numberstyle=\footnotesize,这可以让行号的字体更小一点
    • 标识符样式:identifierstyle=\color{black},标识符的颜色字体等
  • 补充关键词:morekeywords={eg1,eg2},这可以加入自定义关键词,默认的不太全
  • 自动换行:breaklines=true,建议,否则过长的行会直接截断!
  • 行号显示:
    • 是否显示行号:numbers=left,支持left/right/none,默认为none不显示
    • 间隔多少行显示行号:stepnumber=5
    • 行号起始:firstnumber=10,设置为last则继承上一个代码环境的行号
  • 列间距设置:columns=flexible,让不同列之间的距离自适应,反之可以使用fixed设置固定距离
  • 字符串中的空格显示:showstringspaces=false,否则默认会对字符串内的空格显示下划线
  • tab与缩进:
    • 突出显示tab:showtabs=true,会将tab显示为长的下划线,默认不显示(只展示连续空格)
    • tab长度:tabsize=8,默认长度为8个空格
  • 边框:frame=single,single显示单边框,shadowbox为阴影框,默认是none无边框
  • 背景颜色:backgroundcolor=xxx
  • 代码块标记:name=xxx,同名的代码块行号会自动延续
  • 代码块显示名称:caption=Name,还可以指定名称的显示位置(默认在顶部),例如captionpos=b改在底部,caption会自动编号,如果不需要编号,可以使用title=xxx,如果同时出现则会显示后设置的项
  • 忽略部分字符以支持latex命令:escapeinside={\%*}{*)}

下面是一份参考的语法高亮配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
\usepackage{listings}
\usepackage{xcolor}

\lstdefinestyle{simpleStyle}{
basicstyle=\ttfamily\small, % 设置字体族
breaklines=true, % 自动换行
keywordstyle=\color{blue}, % 关键字样式
identifierstyle=\color{black}, % 标识符样式
stringstyle=\color{violet}, % 字符串样式
commentstyle=\color[RGB]{34,139,34}, % 注释样式
showstringspaces=false, % 不显示字符串中的空格
numbers=left, % 显示行号在左边
numbersep=2em, % 设置行号的具体位置
numberstyle=\footnotesize, % 缩小行号
frame=single, % 边框
framesep=1em, % 设置代码与边框的距离
}

\lstset{style=simpleStyle}

这里并没有指定语言,因此每一个代码块必须要指定语言才能有更好的显示效果。

下面依次是MATLAB,Python,C的使用示例以及对应的显示效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
\begin{lstlisting}[language=Matlab, caption=MATLAB Example]
% Call function to compute first 10 terms
fibonacci(10);

function fibonacci(n)
% Calculate the first n Fibonacci numbers
fib = zeros(1, n); % Initialize array
fib(1) = 0; % First term
if n > 1
fib(2) = 1; % Second term
end

for i = 3:n
fib(i) = fib(i-1) + fib(i-2); % Current term
end

% Display results
fprintf('First %d Fibonacci numbers:\n', n);
disp(fib);
end
\end{lstlisting}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
\begin{lstlisting}[language=Python, caption=Python Example]
def fibonacci(n):
"""Calculate the Fibonacci sequence up to n."""
fib = [0, 1] # Initialize the list
while len(fib) < n:
fib.append(fib[-1] + fib[-2]) # Append the next Fibonacci number
return fib

# Get user input
num = int(input("Enter the number of terms: "))
result = fibonacci(num)

# Display the result
print(f"The first {num} Fibonacci numbers are: {result}")

\end{lstlisting}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
\begin{lstlisting}[language=C, caption=C Example]
#include <stdio.h>

void fibonacci(int n) {
// Calculate Fibonacci numbers up to n
int fib[n];
fib[0] = 0;
fib[1] = 1;

for (int i = 2; i < n; i++) {
fib[i] = fib[i - 1] + fib[i - 2]; // Calculate the next Fibonacci number
}

// Print the results
printf("The first %d Fibonacci numbers are:\n", n);
for (int i = 0; i < n; i++) {
printf("%d ", fib[i]);
}
printf("\n");
}
\end{lstlisting}