整理和分享一些数据结构作业用到的 LaTeX 排版技巧

文章目录

Background

上个学期修了数据结构课程,作业要求是手写拍照或者电子版都行,最后上传到教学在线,然后呢我看了看感觉好像可以顺便用来锻炼 LaTeX 的样子,于是就一直用它来完成书面作业了。过程中碰到蛮多有意思的东西,特别是到后期对一些数据结构的描述和图形绘制,现在一起整理分享一下。所有的代码和对应生成的 PDF 文件都放在了 GitHub 上。

格式约定

除了最开始的 封面 部分,文中的区块 LaTeX 代码都是截取的片段,对于这些代码片段,会在开头以注释代码(%)的方式给出关联到的 package,之后的内容位置默认在文档的 \begin{document}\end{document} 之间。

对于排版环境、选项和包名等,直接使用行内代码格式,如 titlepage,而对于 LaTeX 命令,会使用其命令代码本身,也就是带 \ 的单词,如 \maketitle

额外提一点……博客上用的代码高亮插件不支持 LaTeX,所以文章中代码区块的高亮逻辑可能看起来有点吔……它检测到什么关键词就会当做对应的语言然后处理,我也没办法╮(╯▽╰)╭

基础内容

封面
\documentclass[titlepage]{article}

\title{
Homework 1 (Chapter 3) - Due 17th Sep, 2017\\  
\begin{large}
Data Structures and Algorithm Analysis in C++  
\end{large}
}
\author{Kingsley}
\date{\today}

\begin{document}
\maketitle
blabla...  
\end{document}

titlepage 选项 和 \maketitle 命令一起,作用是把标题部分垂直居中作为单独的一页,当然内容比较少的话也可以不让它作为封面。实际交上去的版本,因为是作业,建议把 \author{} 的值换成名字的拼音然后加个空格写上学号。

目录

如果文档的内容比较多的话,最好添加一个目录方便预览内容的结构:

\clearpage
\tableofcontents
\clearpage

前后的两个 \clearpage 指令作用是把目录设置为单独的页面,可以根据自己的需求取舍。

这个命令生成的是自动顺序编号的目录,那有时候可能我们并不想让一个链接被自动编号,就可以用:

\subsection*{Not Numbered Subsection}
\addcontentsline{toc}{subsection}{Not Numbered Subsection}

在分块的命令后加个星号 * 就可以让它不被自动编号,并且不会对后续区块的编号有影响。第二行的 \addcontentsline 作用是把当前位置加入索引中,它的使用格式如下:

\addcontentsline{file}{sec_unit}{entry}

第一个参数可以取值 tocloflot ,分别代表添加到目录(Table Of Contents)、插图列表(List Of Figures)和表格列表(List Of Tables)。第二个参数就是层级了,比如区块单元的名称或者 figure/table(用于插图/表格列表)。然后第三个是当前项的描述,也就是显示在目录列表中的名称。

缩进

LaTeX 默认对 \section{}\subsection{} 下的首段不加缩进,而对于非首段的段落默认都是有缩进的。那如果因为一些原因需要让首段有缩进的话,可以使用 indentfirst 包,只要在文件头部引入这个包就可以了,不需要其他的设置命令:

\usepackage{indentfirst}

此外,也可以在段落开头直接使用 \indent\noindent 命令来覆盖本段默认的缩进设置。

字母编号列表

LaTeX 默认的编号列表是数字的,要使用字母编号就需要通过 enumitem 包来指定 label 的格式:

% \usepackage{enumitem}

\begin{enumerate}[label=(\alph*)]
\item $\Theta(n)$
\end{enumerate}

这样的编号列表就是带括号的小写字母 (a)(b)(c) 等,相对应地,\Alph*\Roman*(\roman*) 分别代表大写字母编号和大(小)写罗马数字编号。

图片

直接用 \includegraphics{figure-path} 来插入需要的图片就可以了,大部分时候可能对于图片位置以及占用尺寸等有要求,那么可以自行添加设置,比如我是用 center 环境直接居中图片,然后添加 width 命令把图片宽度设置为文档宽度:

\begin{center}
\includegraphics[width=1\textwidth]{figures/all-probing.jpg}
\end{center}

代码

对于(伪)程序员来说,代码相关的东西总是会被特别关注的,排版文档的时候对代码的显示效果自然也会额外看重,下面按照分类谈谈在 LaTeX 中如何排版代码。

行内代码

行内代码有比较多的方法可以做到,这里简单列举一下:

  • \verb|inline code|
    这个命令的作用是直接把两条竖线中间的内容作为行内代码显示出来,如果代码里面包含了竖线的话,为了避免不必要的麻烦可以把起止符号换成感叹号 !

  • \texttt{inline code}
    这个命令实际上是将内部的文字设置为打印机字体,效果上也可以作为行内代码。此外,它还可以嵌套其他 LaTeX 命令,比如对内部某个词加下划线等:

\texttt{\underline{while} (true) i++;}
  • \lstinline{inline code}
    这个是 listings 包里的 \lstinline 命令,本身就是用于排版代码的。它还可以和别的包(比如 xcolor)配合,高亮部分保留字:
% \usepackage{listings}
% \usepackage{xcolor}

\lstset{language=C,keywordstyle={\bfseries \color{orange}}}
\lstinline{while (true) i++;}
区块代码

区块代码一般使用 lstlisting 环境,它也来自 listings 包。不过这个包排版出来的代码个人感觉看起来并不是很舒服,可以用 \lstset 改一下:

% \usepackage[T1]{fontenc}

\lstset{
    basicstyle=
        \def\fvm@Scale{0.8}
        \fontfamily{fvm}\selectfont,
    tabsize=4
}

这样就选择了一个等宽而且看起来蛮舒服的字体,然后代码就直接放在这个环境内部就可以了:

% \usepackage{listings}

\begin{lstlisting}
#include <iostream>

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

如果对 Tab 的缩进长度有要求的话可以自己改 tabsize 的值。

顺便介绍一下 verbatim 环境,这个词的翻译是“逐字地”,在 LaTeX 里面是用于排版 TeX 代码的,也就是用于显示这个排版系统本身的一些命令。虽然用起来好像和 lstlisting 差不多,不过还是不要混淆比较好,而且个人感觉它的字体、样式设置之类的都比较麻烦。

文件代码

有时候可能想直接从文件中把代码排版进文档,这个时候可以用 \lstinputlisting 命令,不用说应该也知道它同样是来自之前提到的那个包了,使用方法也很简单,直接在内容部分输入文件路径即可:

% \usepackage{listings}

\lstinputlisting{sample.cpp}

如果只需要文件中的一部分代码,可以通过设置指定起止行数:

\lstinputlisting[firstline=3, lastline=7]{sample.cpp}
样式设置

实际上 \lstset 能做的当然远不止 区块代码 部分提到的那些,但是如果不是整个文档都要用到的样式,原则上并不建议直接在这里面进行配置,我们可以用 \lstdefinestyle 来创建一个自己的样式配置,然后在需要使用到的时候只要在内容之前加一个中括号引入它即可:

% \usepackage{listings}

\lstdefinestyle{sample-style}{
    xleftmargin=2\parindent
}

\lstinline[style=sample-style]{while (true) i++;}
\lstinputlisting[style=sample-style]{sample.cpp}
\begin{lstlisting}[style=sample-style]
// code block
\end{lstlisting}

更多有关 listings 包设置的内容可以参考 Wikibook

效果图

设置好样式的行内代码的效果大致如下:

Inline Code

区块代码(包括文件代码)则是这样的: Code Block

具体的 LaTeX 源码和生成的 PDF 文件可以到 Repo 里看。

二叉树

直接用 tikz 包然后设置一下每个节点的样式以及层级之间的距离就可以了:

% \usepackage{tikz}

\begin{tikzpicture}[
    every node/.style={
        circle, draw,
        inner sep=0pt,
        text width=6mm,
        align=center
    },
    level distance=10mm,
    level 1/.style={sibling distance=30mm},
    level 2/.style={sibling distance=15mm}
]
\node{$15$}
child { node{$5$}  
    child[missing]
    child { node{$7$} }
}
child { node{$20$}  
    child { node{$18$}
        child { node{$16$} }
        child[missing]
    }
    child { node{$25$} }
};
\end{tikzpicture}

看一下代码大概就不用多解释了……还是很直观的,every node 设置每个节点的样式,level distance 设置层级之间的默认距离,然后后面额外针对一些层使用自定义的的样式覆盖默认值。内容部分就是很显然的根据树的形状和当前节点位置选择包含或并列关系,注意空的子树要用 [missing] 占位。

普通树
使用 tikz 包

我们可以使用刚刚提到的那个方式排版普通树:去掉占位的子树,然后根据实际子树的内容重新输入就可以了,比如:

% \usepackage{tikz}

\node{X}
child { node{P}  
    child { node{C} }
    child { node{Q} }
    child { node{R}
        child { node{V} }
        child { node{M} }
    }
};
使用 forest 包

实际上普通树有更简单的排版方式,用 forest 包就可以做到:

% \usepackage{forest}

\begin{forest}
[, phantom, s sep = 1cm
    [1
        [2[3]]
        [4[5]]
        [6]
    ]
]
\end{forest}

无论是编写还是阅读都好像清晰很多。

森林

很显然 forest 包本身是用来排版 forest 的,效果上就是同一行的多颗普通树组成的森林:

% \usepackage{forest}

\begin{forest}
[, phantom, s sep = 1cm
[A
    [B [C] [D[E]] [F]]
    [G]]
[H
    [I]
    [J [K [L]]
    [M [N] [O]]]]
[P
    [Q] [R [S] [T]] [U] [V [W [X]] [Y]] [Z]]
]
\end{forest}

而同样地我们可以进行样式设置,可以直接在 \begin{forest} 后写,也可以在 \forestset 里定义一个样式然后引用,比如这样:

% \usepackage{forest}

\forestset{
    forest-style/.style={
        for tree={
            circle, draw,
            every node/.style={
                circle, draw,
                inner sep=0pt,
                text width=6mm,
                align=center
            }
        }
    }
}

\begin{forest} forest-style
[, phantom, s sep = 1cm
// Forest Content
\end{forest}

效果大致如下:

Forest

链表

这个的思路其实是先用 matrix 把内容排版成一个表,其中的内容都定义好样式(比如方框之类),然后用 \draw 命令绘制箭头,代码贴上来好像也没太大意义所以就只给个文件链接吧:linked-list.tex

好像也没有特别要注意的地方……不过调整间距确实有点麻烦,建议把字体设置为正常大小然后按照最长的位数调整单元格宽度和高度。有一点就是箭头是后续绘制上去的,要注意这个长度会不会覆盖了格子或者距离边界太远。

B+ 树

这个好像应该放在树那一节的hhh,不过无所谓了。

还是用 tikz 包,先通过 \tikzstyle 定义好节点的样式,然后应用到每一个 node,再根据需求调整一下行间距,最后用类似 二叉树 一节提到的语法编写排版就可以了:

% \usepackage{tikz}

\usetikzlibrary{shapes}

\begin{tikzpicture}
\tikzstyle{bplus-node}=
[
    draw, rectangle split,
    rectangle split horizontal,
    rectangle split ignore empty parts
]
\tikzstyle{every node}=[bplus-node]

\tikzstyle{level 1}=[sibling distance=35mm]
\tikzstyle{level 2}=[sibling distance=15mm]

\node {I \nodepart{two} M \nodepart{three} S} [-]
    child {node {D \nodepart{two} G}
        child {node {A \nodepart{two} B \nodepart{three} C}}
        child {node {D \nodepart{two} E}}
        child {node {G \nodepart{two} H}}
    }
    child {node {K}
        child {node {I \nodepart{two} J}}
        child {node {K \nodepart{two} L}}
    }
    child {node {P}
        child {node {M \nodepart{two} N \nodepart{three} O}}
        child {node {P \nodepart{two} R}}
    }
    child {node {U}
        child {node {S \nodepart{two} T}}
        child {node {U \nodepart{two} W}}
    }
;
\end{tikzpicture}

对应的排版输出结果是这样的:

Bplus Tree

严格来说最后一层应该用箭头从左至右连接起来的,不过有点麻烦当时就没做,现在也不想做了

微小的总结

其实有一些内容是在需要用到的时候去搜索然后在 Tex Stack Exchange 里找到的,不过因为自己也改了蛮多而且再去找链接地址也挺麻烦的,就没有给出这些内容的参考来源。

我在整理这些排版方式的时候也踩了好多坑= = 看看这篇文章本来是二月份开始写的[捂脸],前段时间开始除草然后还发现了 standalone 这个文档类型,可以很方便地用来写预览文件。

整理过程中也学到一些新的东西和规范,其实每次作业里的一些配置基本都是用全局命令直接设置的,所以在整理到一个文件里的时候简直是爆炸……各种排版样式的冲突,生成的 PDF 文件非常迷23333。所以在非必需的情况下还是要尽量少用全局设置,样式之类的东西可以在对应的环境里直接指定,也可以放在一个配置里然后在用到的时候引用,大概就是……松耦合?(逃

延伸参考

最后列举几个经常用到的文档之类的网站吧:

  1. CTAN
    这里收录了大部分的 TeX 包以及他们的 README、官方文档等内容,非常详尽。在我感叹 tikz 这个包怎么功能这么强大的时候,我找到了这个包的 文档,然后就……emmm

  2. Share LaTeX
    这个网站本身是用于在线合作编写 LaTeX 文档的,他们提供的这个 learn 部分感觉对于学习 LaTeX 很有帮助。我记得我高中的时候也有了解过这个网站,因为当时它给的 Demo 里有一张青蛙的图片,让我印象很深刻(

  3. WikiBook
    搜索结果里经常也看到来自这里的内容,不过我没太仔细了解,应该和上面的那个差不多。相对官方文档而言,这种整合并且带索引的结果可能有时候更符合我们的需求。