|
| 1 | +\chapter{数组}\label{fortran_array} |
| 2 | + |
| 3 | +迄今为止我们折腾的东东都是标量(scalar), 那都是小case, 大case是数组(array). 用非常严谨的方式来讨论数组, 以我这语文水平肯定是不行滴, 讲着讲着同志们就头疼, 所以我接下来讲的内容会不太严谨, 但对于实际应用来说肯定是不成问题的. |
| 4 | + |
| 5 | +数组是形如$a\!:\!\{j_1,\dots,k_1\}\!\times\!\dots\!\times\!\{j_n,\dots,k_n\}\rightarrow\mathbb{C},(s_1,\dots,s_n)\mapsto a_{s_1\dots s_n}$的东东. 正整数$n$称为数组$a$的维数/秩(rank). 任意$i\in\{1,\dots,n\}$, $j_i$和$k_i$称为数组$a$的第$i$个维度(dimension)的下界(lower-bound)和上界(upper-bound), $k_i-j_i+1$称为数组$a$的第$i$个维度的长度(extent). 矢量$(k_1-j_1+1,\dots,k_n-j_n+1)$称为数组$a$的形状(shape), 其本身是一维数组. $\prod_{i=1}^n(k_i-j_i+1)$称为数组$a$的大小(size). 标量可以当成维数为$0$的数组. |
| 6 | + |
| 7 | +形如$a_{s_1\dots s_n}$的东东称为数组$a$中的元素(element). 任意$i\in\{1,\dots,n\}$, 称$s_i$为$a_{s_1\dots s_n}$的第$i$个下标(subscript)或第$i$个索引(index). 同一个数组中的所有元素都有相同的类型和种别. |
| 8 | + |
| 9 | +数组中的元素有一个被规定死的排列顺序: 任意$a_{s_1\dots s_n}$和$a_{t_1\dots t_n}$, 当$s_{i+1}=t_{i+1},\dots,s_n=t_n$时, 若$s_i<t_i$, 则$a_{s_1\dots s_n}$在$a_{t_1\dots t_n}$前面, 若$s_i>t_i$, 则$a_{s_1\dots s_n}$在$a_{t_1\dots t_n}$后面\footnote{这和Matlab的默认情形一样, 但和C/Python的默认情形不一样!}. |
| 10 | + |
| 11 | +\section{数组声明}\label{fortran_array_specification} |
| 12 | + |
| 13 | +用数组之前当然要声明它. 数组的声明和标量的声明还是很像的, 也是需要给一个数据类型(整型/实型/复型), 还可以给一个种别. 数组的类型和种别就是数组中每一个元素的类型和种别. |
| 14 | + |
| 15 | +为表明声明的变量是个数组, 还需给出$j_1,\dots,j_n,k_1,\dots,k_n$. 有两种方法可行, 一种是在变量名后加\verb|(j1:k1,...,jn:kn)|, 另一种是在类型和种别后加\verb|,dimension(j1:k1,...,jn:kn)|. 所有\verb|ji|, \verb|ki|都必须是整型常量或整型常量表达式\footnote{ |
| 16 | + 就是只含整型常量的表达式. |
| 17 | +}. 可以省略任意\verb|ji:|, 如果省略\verb|ji:|, 则\verb|ji|为\verb|1|\footnote{这和Matlab的默认情形一样, 但和C/Python的默认情形不一样!}. |
| 18 | + |
| 19 | +请大家猛戳\href{https://fortran-lang.org/learn/quickstart/arrays_strings#array-declaration}{这个链接}获取数组声明示例. |
| 20 | + |
| 21 | +选择哪种声明方式呢? 一般大家喜欢用第一种方式, 毕竟少打几个字, 而且看着比较简洁. 但如果是要一口气声明一堆一样的数组\footnote{ |
| 22 | + 方法同标量声明. |
| 23 | +}, 这时第一种方式反而不好使了, 大家就喜欢用第二种方式. |
| 24 | + |
| 25 | +和字符串一样, 有些时候我们并不清楚到底要用一个什么样的数组, 这时我们就可以和字符串一样, 先在类型和种别后加\verb|,allocatable|, 然后再在变量名后加\verb|(:,...,:)|或在类型和种别后加\verb|,dimension(:,...,:)|, 其中\verb|:|的个数等于数组的维数, 这样我们就搞出一个延迟形状数组(deferred-shape array). 假如我们声明的数组叫\verb|a|, 接下来我们就可以像延迟长度字符串那样, 用\verb|allocate(a(j1:k1,...,jn:kn))|把$j_1,\dots,j_n,k_1,\dots,k_n$确定下来(仍可省略\verb|ji:|), 用\verb|deallocate(a)|将$j_1,\dots,j_n,k_1,\dots,k_n$重新变成未定义的, 然后再来个\verb|allocate(a(j1:k1,...,jn:kn))|\dots |
| 26 | + |
| 27 | +请大家猛戳\href{https://fortran-lang.org/learn/quickstart/arrays_strings#allocatable-dynamic-arrays}{这个链接}获取延迟形状数组声明示例. |
| 28 | + |
| 29 | +\section{数组构造} |
| 30 | + |
| 31 | +数组构造器(array constructor)是一维数组常量, 形如\verb|[e1,...,em]|, 其中\verb|ei|可以是标量也可以是数组. 若将\verb|[e1,...,em]|记为$(a_1,\dots,a_n)$, \verb|ei|的大小记为$S_i$\footnote{ |
| 32 | + 标量的大小当然是$1$. |
| 33 | +}, 则$a_{(S_1+\dots+S_{i-1}+l)}$为\verb|ei|的第$l$个元素. 如果\verb|ei|维数大于$1$, 则按本章开头``数组元素顺序''找到第$l$个元素. |
| 34 | + |
| 35 | +直接了当的说法是: 把所有\verb|ei|重新排成一维数组并保证元素顺序不变, 然后首尾相接拼起来. |
| 36 | + |
| 37 | +在数组构造器\underline{中}$\!$$\!$还可以使用一种神奇的隐式do循环(implied \verb|do| loop), 这玩意儿形如\verb|((...(e(i1,...,in),i1=p1,q1,r1)...),in=pn,qn,rn)|, 里头\verb|e(i1,...,in)|是个数据实体, 通常是个含有\verb|i1|,\dots ,\verb|in|的表达式, \verb|i1|,\dots ,\verb|in|是一堆事先声明过的整型变量, 整个隐式do循环相当于\underline{一维}$\!$$\!$数组. 下面这样的两个程序总是等价的\footnote{ |
| 38 | +看懂这两个程序, 同志们可能需要先读\ref{fortran_array_assignment}节. |
| 39 | +}. 自然任意\verb|,ri|都可以省略(默认为\verb|1|). |
| 40 | +\begin{verbatim} |
| 41 | +program normal_do_loop |
| 42 | + ... |
| 43 | + integer :: i1 |
| 44 | + ... |
| 45 | + integer :: in |
| 46 | + integer :: i |
| 47 | + i = 0 |
| 48 | + do in = pn, qn, rn |
| 49 | + ... |
| 50 | + do i1 = p1, q1, r1 |
| 51 | + i = i+1 |
| 52 | + a(i) = e(i1,...,in) ! a is [a(1),...,a(S)]. |
| 53 | + end do |
| 54 | + ... |
| 55 | + end do |
| 56 | + ! Now i == S. |
| 57 | +end program normal_do_loop |
| 58 | +\end{verbatim} |
| 59 | +\begin{verbatim} |
| 60 | +program implied_do_loop |
| 61 | + ... |
| 62 | + integer :: i1 |
| 63 | + ... |
| 64 | + integer :: in |
| 65 | + a = [((...(e(i1,...,in),i1=p1,q1,r1)...),in=pn,qn,rn)] |
| 66 | +end program implied_do_loop |
| 67 | +\end{verbatim} |
| 68 | + |
| 69 | +隐式do循环属于比较高级的语法, 还是稍稍让人不好理解滴. 幸好不用隐式do循环也总是可以完成任务, 所以可以干脆不用. Python中也有一个类似的叫列表推导式的东东, 不过当年老师好像根本没讲过, 我也只是在耍帅时会用这玩楞. |
| 70 | + |
| 71 | +用Ifort的话, 可以用\verb|p:q:r|代替\verb|(i1,i1=p,q,r)|, 当然\verb|:r|可以省略. 这是Ifort的器规, Gfortran是不认的\footnote{ |
| 72 | +所以造轮子时最好不用这东东. |
| 73 | +}. \verb|(i1,i1=p,q,r)|这样的一重简单隐式do循环比较常用, 是要掌握的. 我再把上一对示例程序的简单情形重新列一下, 其中\verb|a(i) = i1|的意思就是令$a_i$为那时的$i_1$. |
| 74 | +\begin{verbatim} |
| 75 | +program normal_do_loop |
| 76 | + ... |
| 77 | + integer :: i1 |
| 78 | + integer :: i |
| 79 | + i = 0 |
| 80 | + do i1 = p, q, r |
| 81 | + i = i+1 |
| 82 | + a(i) = i1 |
| 83 | + end do |
| 84 | +end program normal_do_loop |
| 85 | +\end{verbatim} |
| 86 | +\begin{verbatim} |
| 87 | +program implied_do_loop |
| 88 | + ... |
| 89 | + integer :: i1 |
| 90 | + a = [(i1,i1=p,q,r)] |
| 91 | +end program implied_do_loop |
| 92 | +\end{verbatim} |
| 93 | +举个更具体的例子: 输出$1$到$9$中奇数的平方. |
| 94 | +\begin{verbatim} |
| 95 | +program main |
| 96 | + integer :: i, odd_squares(5) |
| 97 | +! integer :: s |
| 98 | +! s = 1 |
| 99 | +! do i = 1, 9, 2 |
| 100 | +! odd_squares(s) = i**2 |
| 101 | +! s = s+1 |
| 102 | +! end do |
| 103 | + odd_squares = [(i**2, i=1,9,2)] |
| 104 | + print *, odd_squares |
| 105 | +end program main |
| 106 | +\end{verbatim} |
| 107 | + |
| 108 | +我们可以对先前构造出来的一维数组进行变形(reshape)操作来获取多维数组, 只需来个\verb|a_new = reshape(a_old,s)|就成. \verb|a_new|和\verb|a_old|是两个数组, \verb|s|是\verb|a_new|的形状(当然得是整型一维数组), \verb|a_new|的第$l$个元素和\verb|a_old|的第$l$个元素总是相同的\footnote{ |
| 109 | +不考虑赋值时的类型和种别转化的情况下. |
| 110 | +}. 整个变形操作说白了就是: 将数组\verb|a_old|中的元素复制到形状为\verb|s|的数组\verb|a_new|中, 并保证元素顺序不变, 比如下面这样. |
| 111 | +\begin{verbatim} |
| 112 | +program main |
| 113 | + implicit none |
| 114 | + integer :: one2four(2,2) ! a11=1 a12=3 |
| 115 | + one2four = reshape([1,2,3,4],[2,2]) ! a21=2 a22=4 |
| 116 | +end program main |
| 117 | +\end{verbatim} |
| 118 | + |
| 119 | +\section{数组切片} |
| 120 | + |
| 121 | +数组切片(slicing)是用一个数组得到另一个数组的操作. 现假设有一个维数为\verb|n|的数组\verb|a|, 则\verb|a(e1,...,en)|是另一个数组, 其中\verb|e1|,\dots ,\verb|en|乃整型一维数组或整型标量. 如何确定\verb|a(e1,...,en)|? |
| 122 | + |
| 123 | +引入\verb|v1|,\dots ,\verb|vn|, 保证若\verb|ei|为数组则\verb|vi|为\verb|ei|, 若\verb|ei|为标量则\verb|vi|为\verb|[ei]|. 这样\verb|a(v1,...,vn)|等于\verb|b|. 记\verb|vi|为$(v_{i;1},\dots,v_{i;{l_i}})$, 则$b_{{\iota_1},\dots,{\iota_n}}=a_{{v_{1;\iota_1}},\dots,{v_{n;\iota_n}}}$. |
| 124 | + |
| 125 | +假设\verb|e1|,\dots ,\verb|en|中\verb|ei1|,\dots ,\verb|eim|\ ($i_1\!<\!\dots\!<\!i_m$)是长度为$l_{i_1},\dots,l_{i_m}$的数组, 其他是标量, 则可将\verb|b|变形成形状为$(l_{i_1},\dots,l_{i_m})$的\verb|c|, \verb|c|就是\verb|a(e1,...,en)|. |
| 126 | + |
| 127 | +我本来处心积虑地想再来几段话来把这切片讲得更明白些, 然后我就放弃了, 只好先来个示例给同志们做练习. 我敢保证自己写的东东肯定是真实不虚的, 但看来是很难理解记忆了. 幸好非常复杂的数组切片一般是用不上的. 如果老师敢考那些难死人的切片, 我们就当即暴动$\!\text{\~{}}$ |
| 128 | +\begin{verbatim} |
| 129 | +program main ! a000=1 a001=5 a100=2 a101=6 |
| 130 | + implicit none ! a010=3 a011=7 a110=4 a111=8 |
| 131 | + integer :: i, one2eight(0:1,0:1,0:1) |
| 132 | + integer :: result(1,4) |
| 133 | + one2eight = reshape([(i,i=1,8)],[2,2,2]) |
| 134 | + result = one2eight(0,[0],[0,1,1,0]) ! The shape is [1,4]. |
| 135 | + ! result X one2eight(0,0,[0,1,1,0]) ! The shape is [4]! |
| 136 | + print *, result |
| 137 | +end program main |
| 138 | +\end{verbatim} |
| 139 | + |
| 140 | +先前用向量下标(vector subscript)来切片, 我们还可以用三元下标(triplet subscript)\footnote{ |
| 141 | +官方文档里用的是``subscript triplet''. |
| 142 | +}, 以\verb|p:q:r|代替\verb|(i,i=p,q,r)|, 当然\verb|:r|可以省略. 这不是器规, 是通用的. 而且三元下标中\verb|p|和\verb|q|也可以省略(但是注意\verb|p|和\verb|q|之间的\verb|:|不能省), \verb|p|省略就等于那一维的下界, \verb|q|省略就等于那一维的上界. 这样的切片简单且比较常用(尤其是省略\verb|:r|的时候), 是要掌握的. 比如我们可以方便地摘出$1$到$9$中的奇数. |
| 143 | +\begin{verbatim} |
| 144 | +program main |
| 145 | + implicit none |
| 146 | + integer :: i |
| 147 | + integer :: singles(9), odds(5) |
| 148 | + singles = [(i, i=1,9)] |
| 149 | + odds = singles(::2) ! singles(1:9:2) |
| 150 | +end program main |
| 151 | +\end{verbatim} |
| 152 | +还有, 如果\verb|i1,...,in|都是整型标量, 则\verb|a(i1,...,in)|就是$a_{i_1\dots i_n}$, 这更要掌握. 问: \verb|a(i1,...,in)|和\verb|a(i1:i1,...,in:in)|有什么区别? |
| 153 | + |
| 154 | +对数组切片, 可以得到一个新数组, 看起来可以对这个新数组再切片. 然而这是不成的, 原因在于对数组切片得到的新数组, 其每一维的上下界其实都是不确定的\footnote{ |
| 155 | +虽然在介绍切片规则时看起来有确定的上下界, 那只是为了说话方便. |
| 156 | +}, 所以新数组中每个元素的下标都是不确定的, 因此没法切片. 同样地, 由数组构造器得到的数组也是不能切片的. |
| 157 | + |
| 158 | +\section{数组运算} |
| 159 | + |
| 160 | +有了数组, 总是要用数组来算些什么东西. 现在我们可以在表达式中混用数组, 标量和\ref{fortran_opration}节中的所有运算符. 运算符的优先级, 和之前是一样的, \verb|+|和\verb|-|之前如果什么也没有, 也还是默认有个\verb|0|. 只需要知道, 当运算符两边出现数组时会有什么结果, 我们就能推理出任意表达式的结果了. 这又分两种情况. |
| 161 | +\begin{itemize} |
| 162 | +\item 运算符两边都是数组. |
| 163 | +\item 运算符一边是标量, 另一边是数组. |
| 164 | +\end{itemize} |
| 165 | + |
| 166 | +如果运算符两边都是数组, 我们首先必须保证这两个数组形状完全一致, 绝对一致, 这样这两个数组中的元素按所处的位置, 自然就能一一对应. 我们假设两个数组分别是$a$和$b$, 并用符号$\star$表示一个运算符, 现在我们要算$a\star b$. 假设$a$和$b$的下界分别为$j_{a;1},\dots,j_{a;n}$和$j_{b;1},\dots,j_{b;n}$, 则首先可以搞到另外两个数组$\alpha$和$\beta$, 使得$\alpha_{i_1\dots i_n}=a_{(i_1+j_{a;1})\dots(i_n+j_{a;n})}$, $\beta_{i_1\dots i_n}=b_{(i_1+j_{b;1})\dots(i_n+j_{b;n})}$, 然后可以弄出一个数组$c$, 使得$c_{i_1\dots i_n}=\alpha_{i_1\dots i_n}\star\beta_{i_1\dots i_n}$, 则$c$就是$a\star b$. 简单来说就是对$a$和$b$中两两对应的元素进行$\star$运算, 得到新数组. 示例如下. |
| 167 | +\begin{verbatim} |
| 168 | +program main ! 1+5=6 3+7=10 |
| 169 | + implicit none ! 2+6=8 4+8=12 |
| 170 | + integer :: one2four(2,2), five2eight(2,2,1) |
| 171 | + one2four = reshape([1,2,3,4],[2,2]) |
| 172 | + five2eight = reshape([5,6,7,8],[2,2,1]) |
| 173 | + print *, one2four+reshape(five2eight,[2,2]) |
| 174 | + ! print *, one2four+five2eight (X) |
| 175 | +end program main |
| 176 | +\end{verbatim} |
| 177 | + |
| 178 | +如果运算符一边是标量, 另一边是数组, 不妨设数组为$a$, 标量为$b$, 仍设运算符为$\star$. 此时若$a$的形状为$\vec{s}$, 则可以另搞一个形状为$\vec{s}$的数组$\tilde{b}$, 使得$\tilde{b}$中任意元素都是$b$, 然后就有$a\star b=a\star\tilde{b}$, $b\star a=\tilde{b}\star a$. 示例如下. |
| 179 | +\begin{verbatim} |
| 180 | +program main ! 1+9=10 3+9=12 |
| 181 | + implicit none ! 2+9=11 4+9=13 |
| 182 | + integer :: one2four(2,2) |
| 183 | + one2four = reshape([1,2,3,4],[2,2]) |
| 184 | + print *, one2four+9 |
| 185 | +end program main |
| 186 | +\end{verbatim} |
| 187 | + |
| 188 | +即使表达式的结果是一个数组, 也不能对其切片, 因为此时数组的上下界依然是不确定的. 比如, 有两个一维数组, 一个下界是$1$, 一个下界是$0$, 把这俩加起来, 得到的数组上下界应该是多少? 不好规定. 有些情况下, 比如让两个下界都是$1$的一维数组相加, 看起来数组上下界是好规定的, 然而若真来个规定, 造编译器的人就得处心积虑地要让编译器能够区分这两种情形, 他们会很不开心, 很不快乐, 所以统统规定上下界不确定是好的. |
| 189 | + |
| 190 | +\section{数组赋值}\label{fortran_array_assignment} |
| 191 | + |
| 192 | +数组赋值可分两种, 一种``\verb|=|''右侧是数组, 另一种``\verb|=|''右侧是标量. |
| 193 | + |
| 194 | +如果``\verb|=|''右侧是数组, 则俩玩楞形状必须一样滴. 假设我们要令\verb|a=b|, 并设$n$维数组$a$和$b$的下界分别为$j_{a;1},\dots,j_{a;n}$和$j_{b;1},\dots,j_{b;n}$, 则$a$和$b$的元素自然能一一对应. 首先我们要搞一个$\tilde{b}$, 使得$\tilde{b}_{(i_1-j_{b;1}+j_{a;1})\dots(i_n-j_{b;n}+j_{a;n})}=b_{i_1\dots i_n}$, 然后令$a$等于$\tilde{b}$即可. 简单来说就是令$a$和$b$中两两对应的元素相等. 这里$b$的上下界可能是不确定的, 但赋值给$a$后, $a$先前声明过, 所以其上下界一定是确定的. |
| 195 | + |
| 196 | +如果``\verb|=|''右侧是标量, 比如数组是$a$, 标量是$b$, 则先把$b$变成和$a$形状相同的数组$c$, 使得$c$中任意元素都是$b$, 然后令\verb|a=c|即可. |
| 197 | + |
| 198 | +对于数组, 我们还可以用一些特殊东东来赋值, 比如forall, where和do concurrent, 不过这些东东我自己貌似会用, 却研究不出它们的明确规则. 我计划在第\ref{fortran_parallel_conpute}章介绍它们. |
0 commit comments