B Julia语言入门(*
)
B.1 Julia的安装和运行
B.1.1 Julia程序语言介绍
Julia程序语言是一种计算机编程语言, 就像C、C++、Fortran、Java、R、Python、Matlab等程序语言一样。 Julia语言历史比较短,发布于2012年,是MIT的几位作者和全世界的参与者共同制作的。 主网站在https://julialang.org/。
Julia与R、Python等一样是动态类型语言, 程序与R、Python一样简单, 但是它先进的设计使得Julia程序的效率基本达到和C、Fortran等强类型语言同样的高效率。 尤其适用于数值计算,在现今的大数据应用中也是特别合适的语言, 排在Python、R之后,已经获得广泛的关注, 现在用户较少只是因为历史还太短。
B.1.2 Julia软件安装和运行
从Julia网站下载安装Julia的命令行程序,称为REPL界面。
运行方式是在一个字符型窗口中, 在提示行julia>
后面键入命令,
回车后在下面显示结果。
在MS Windows操作系统中, 设存放Julia源程序和数据文件的目录为c:\work
,
将安装后显示在桌面上的Julia图标复制到目录中,
从右键选“属性”,将“起始位置”栏改为空白。
这样读写数据和程序文件的默认位置就是图标所在的目录。
Julia源程序用.jl
作为扩展名, 为了运行当前目录中的“myprog.jl”文件,
在命令行用命令
julia> include("myprog.jl")
(其中julia>
是自动显示的提示)。
B.1.3 Julia的其它运行方式
安装REPL界面后, 下载安装微软的软件集成开发界面Visual Studio Code, 然后安装Julia语言支持模块。
安装Anaconda软件,并安装Julia的IJulia模块, 将IJulia所需要的Python和Jupyter可执行程序的路径指定为安装的Anaconda软件的位置。 这可以同时获得Python语言和一些科学计算、数据整理、统计建模、机器学习等软件。
在REPL界面,安装Pluto笔记本软件,方法如:
安装好以后,只要在REPL中运行
就可以进入一个笔记本形式的软件开发环境。
B.2 Julia的基本数据和相应运算
B.2.1 整数与浮点数
Julia程序中的整数值可以直接写成如123
或者-123
这样。
虽然整数有多种类型, 一般程序中不必特别关心整数常量的具体类型。
Julia允许使用特别长的整数,这时其类型为BigInt。
Julia的浮点数可以写成带点的形式如123.0
, 1.23
,
也可以写成带有10的幂次如1.23e3
(表示\(1.23\times 10^3\)),
1.23e-3
(表示\(1.23\times 10^{-3}\))。
这些写法都属于Float64类型的浮点数。
Julia还有其他类型的浮点数,但是科学计算中主要使用Float64类型,
在别的语言中这称为双精度浮点数。
B.2.2 四则运算
表示加、减、乘、除、乘方的运算符分别为:
+ - * / ^
浮点数的四则运算遵循传统的算数运算规则和优先级规定。如
4.74
表示 \[1.3 + 2.5 \times 2.0 - 3.6 \div 1.2 + 1.2^2\]
B.2.3 整数的四则运算
整数加法、减法、乘法结果仍为整数, 这样因为整数的表示范围有限,有可能发生溢出。 如
4
整数用“/”作的除法总是返回浮点数,即使结果是整数也是一样:
5.0
整数用a % b
表示a整除b的余数,结果符号总是取a的符号。如
1
整数与浮点数的混合运算会将整数转换成浮点数再计算。
B.3 变量
B.4 向量
B.4.1 向量
在程序中直接定义一个向量,用方括号内写多个逗号分隔的数值,如
5-element Array{Int64,1}:
1
3
4
9
13
4-element Array{Float64,1}:
1.5
3.0
4.0
9.12
其中v1是整数型的向量, v2是浮点型Float64的向量。
向量实际上是一维数组。
可以用1:5
定义一个范围,
在仅使用其中的元素值而不改写时作用与[1, 2, 3, 4, 5]
类似。
1:2:9
定义带有跨度的范围, 与[1, 3, 5, 7, 9]
类似。
1:5
1:2:9
B.4.3 用范围作为下标
下标可以是一个范围,如
3-element Array{Int64,1}:
-999
4
9
熟悉Python语言的读者要注意,这里下标范围的终点是包含在内的, 而Python用这种方法取子集时不包括范围的终点。
用end
表示最后一个下标,如
2-element Array{Int64,1}:
9
13
4-element Array{Int64,1}:
1
-999
4
9
B.4.4 数组作为下标
向量的下标也可以是一个下标数组,如
3-element Array{Int64,1}:
1
4
13
取出的多个元素可以修改,可以赋值为同一个标量,如:
5-element Array{Int64,1}:
0
0
0
9
13
也可以分别赋值,如
5-element Array{Int64,1}:
101
0
303
9
505
B.4.5 向量与标量的运算
向量与一个标量作四则运算, 将运算符前面加句点“.”:
.+ .- .* ./ .^
表示向量的每个元素分别与该标量作四则运算, 结果仍是向量。如
5-element Array{Int64,1}:
101
103
104
109
113
5-element Array{Int64,1}:
99
97
96
91
87
5-element Array{Int64,1}:
2
6
8
18
26
5-element Array{Float64,1}:
0.1
0.3
0.4
0.9
1.3
5-element Array{Int64,1}:
1
9
16
81
169
B.4.6 向量与向量的四则运算
两个等长的向量之间作加点的四则运算,表示对应元素作相应的运算。如
5-element Array{Int64,1}:
3
8
10
16
23
5-element Array{Int64,1}:
-1
-2
-2
2
3
5-element Array{Int64,1}:
2
15
24
63
130
5-element Array{Float64,1}:
0.5
0.6
0.666667
1.28571
1.3
B.4.7 向量初始化
用zeros(n)
可以生成元素类型为Float64、元素值为0、长度为n的向量,如
5-element Array{Float64,1}:
0.0
0.0
0.0
0.0
0.0
用Vector{Float64}(undef, n)
可以生成元素类型为Float64的长度为n的向量,
元素值未初始化,如
3-element Array{Float64,1}:
1.114676523e-315
1.92399178e-315
1.5759804e-315
类似可以生成其它元素类型的向量,如
3-element Array{Int64,1}:
378739344
378739376
155525920
用这样的办法为向量分配存储空间后可以随后再填入元素值。
B.4.8 向量复制
定义一个向量后, 此向量的变量名就“绑定”在某个内存地址上。 修改变量元素实际是修改了变量名所绑定的地址中某个位置的值。如
5-element Array{Int64,1}:
2
3
0
7
11
将向量赋给另外一个向量,并不能实现复制; 两个向量实际上是绑定到了相同的内存地址。如
5-element Array{Int64,1}:
2
3
0
7
11
5-element Array{Int64,1}:
97
3
0
7
11
可以看出修改了v
的元素值,w
的元素值也被修改了,
因为这两个变量名指向的是相同的内存地址。
为了制作一个向量的副本,
不能使用直接赋值的方法,而是使用copy()
函数。如
5-element Array{Int64,1}:
2
3
5
7
11
v
被修改后,其副本w
未受影响。
另外,给w
另外赋值,相当于解除了w
原来的绑定,
并将w
绑定到了将v
的内容复制到另一内存地址的副本地址上。
B.4.9 向量的循环遍历
可以用for
循环对向量的每个元素遍历访问。格式如
v1[1] = 1
v1[2] = 3
v1[3] = 4
v1[4] = 9
v1[5] = 13
这里i
是循环产生的向量下标。
B.5 矩阵
B.5.1 矩阵
前面讲的向量是一维数组,不区分行向量还是列向量。
矩阵是二维数组,有两个下标:行下标和列下标。
在方括号内两个同行的元素之间用空格分隔, 两行之间用分号分隔,可以在程序中输入矩阵,如
2×3 Array{Int64,2}:
1 2 3
4 5 6
也可以直接用换行符分隔不同行,例如
B.5.3 矩阵列和行
设A
是矩阵,
则A[:, j]
表示A
的第j
列元素组成的向量(一维数组),如
2-element Array{Int64,1}:
2
5
A[i, :]
表示A
的第i
行元素组成的向量(一维数组),如
3-element Array{Int64,1}:
4
5
-6
取出后的列或者行不分行向量和列向量。
B.5.4 子矩阵
如果A
是矩阵,I
和J
是范围或者向量,
则A[I,J]
表示A
的行号在I
中的行与列号在J
中的列交叉所得的子矩阵,如
2×2 Array{Int64,2}:
2 3
5 -6
用冒号“:”作为行下标或列下标表示取该维的全部下标。
B.5.5 子矩阵赋值
可以给子矩阵赋值,赋值为一个标量使得对应元素都改为该标量值;如
2×3 Array{Int64,2}:
1 0 0
4 0 0
给子矩阵赋值为一个同样大小的子矩阵给对应元素赋值,如
2×3 Array{Int64,2}:
1 102 103
4 202 203
B.5.6 矩阵初始化
用zeros(m, n)
可以生成元素类型为Float64、元素值为0的\(m\times n\)矩阵,如
2×3 Array{Float64,2}:
0.0 0.0 0.0
0.0 0.0 0.0
用Array{Float64}(undef, m, n)
可以生成元素类型为Float64的\(m \times n\)矩阵,
元素值未初始化,如
2×3 Array{Float64,2}:
3.90752e-316 6.78756e-316 5.18128e-316
6.61297e-316 6.81373e-316 3.87102e-316
类似可以生成其它元素类型的矩阵,如
2×3 Array{Int64,2}:
386423696 78323216 78323216
79156592 78323216 78331944
用这样的办法先为矩阵分配存储空间,然后可以再填入元素值。
B.5.7 矩阵元素遍历
对行下标和列下标分别循环,行下标变化最快,如
2×3 Array{Int64,2}:
1 2 3
4 5 6
A1[1, 1] = 1
A1[2, 1] = 4
A1[1, 2] = 2
A1[2, 2] = 5
A1[1, 3] = 3
A1[2, 3] = 6
另一种办法是用类似于向量遍历的方法,如:
A1[1] = 1
A1[2] = 4
A1[3] = 2
A1[4] = 5
A1[5] = 3
A1[6] = 6
B.5.8 矩阵读入
设当前目录中文件“vecstore.txt”中包含如下内容:
1.2 -5 3.6
7.8 9.12 4.11
将文件中的内容每一行看作矩阵的一行, 文件中保存了一个\(2 \times 3\)矩阵。 读入方法如下:
B.5.9 保存矩阵到文件中
考虑上面的Ain矩阵,为了将其按文本文件格式保存到“tmp2.txt”中, 用如下程序:
using DelimitedFiles
writedlm("tmp2.txt", Ain, ' ')
B.5.10 矩阵与标量的四则运算
矩阵与一个标量之间用加点的四则运算符号进行运算, 与向量和标量之间的运算类似, 表示矩阵的每个元素和该变量的四则运算, 结果仍为矩阵。如
2×3 Array{Int64,2}:
1 2 3
4 5 6
2×3 Array{Int64,2}:
101 102 103
104 105 106
2×3 Array{Int64,2}:
99 98 97
96 95 94
2×3 Array{Int64,2}:
2 4 6
8 10 12
2×3 Array{Float64,2}:
0.1 0.2 0.3
0.4 0.5 0.6
2×3 Array{Int64,2}:
1 4 9
16 25 36
B.5.11 两个矩阵之间的四则运算
两个同样大小的矩阵之间用加点的四则运算符号进行运算, 表示两个矩阵的对应元素的运算。如
2×3 Array{Int64,2}:
100 200 300
400 500 600
2×3 Array{Int64,2}:
101 202 303
404 505 606
2×3 Array{Int64,2}:
99 198 297
396 495 594
2×3 Array{Int64,2}:
100 400 900
1600 2500 3600
2×3 Array{Float64,2}:
100.0 100.0 100.0
100.0 100.0 100.0
B.5.12 矩阵乘法
用A * B
表示矩阵乘法。 如
2×2 Array{Int64,2}:
11 12
21 22
2×3 Array{Int64,2}:
59 82 105
109 152 195
一个矩阵与一个向量(一维数组)作矩阵乘法, 向量自动变成列向量,如:
2-element Array{Int64,1}:
-1
-1
注意结果是向量(一维数组),而不是\(2 \times 1\)矩阵(二维数组)。
行向量可以直接表示成方括号内多个数值之间用空格分隔的格式,如
2×2 Array{Int64,2}:
1 -1
-1 1
又如
1×2 Array{Int64,2}:
-10 -10
注意结果是\(1 \times 2\)矩阵,即行向量,而不是向量。 向量是一维数组,行向量是二维数组。
从以上例子可以看出在矩阵运算中向量可以看成是列向量, 矩阵乘法结果如果是列向量,也会表示成向量(一维数组)。
B.6 自定义函数
B.6.1 介绍
Julia语言支持自定义函数。 在计算机程序语言中, 函数是代码模块化、代码复用的基础。 将常用的计算或者操作写成函数以后, 每次要进行这样的计算或者操作, 只要简单地调用函数即可。 将复杂的任务分解成一个个函数, 可以使得任务流程更加明晰, 任务的不同阶段之间减少互相干扰。 另外, 用自定义函数表示的算法才能充分利用Julia的即时编译优势。
B.6.2 自定义函数的单行格式
类似于\(f(x) = x^2 + 3 x + 1\)这样的简单函数, 可以用一行代码写成
f (generic function with 1 method)
调用如
11
5.510000000000001
B.6.3 自定义函数的多行格式
需要用多行才能完成的计算或者操作, 就需要写成多行的形式。格式如
function funcname(x, y, z)
...
end
其中funcname
是函数名称, x, y, z
等是自变量名,
...
是函数内的语句或表达式, 函数以最后一个表达式为返回值(结果)。
例如,写一个函数,以向量的形式输入一个变量的样本值, 计算样本标准差: \[ s = \sqrt{ \frac{1}{n-1} \sum_{i=1}^n (x_i - \bar x)^2 } \] 自定义函数如
function mysd(x)
n = length(x)
mx = 0.0
for z in x
mx += z
end
mx /= n
s = 0.0
for z in x
s += (z - mx)^2
end
sqrt(s / (n-1))
end
mysd (generic function with 1 method)
调用如
1.5811388300841898
事实上,上面的函数定义可以用已有的一些函数进行简化。
比如,sum(x)
对向量x的元素求和,于是以上的函数可以写成:
mysd_simple (generic function with 1 method)
第二个版本只是利用了向量化和已有函数, 在Julia中其运行效率并不比第一个版本更好, 甚至于不如第一个版本。 Julia语言与R、Matlab等语言不同, 显式的循环一般有更高的执行效率, 向量化写法仅仅是可以使得程序比较简洁。
均值、标准差这些基本统计函数已经在Julia的Statistics标准库中有定义。
B.7 程序控制结构
B.7.1 复合表达式
用begin ... end
可以将多行的多个表达式组合起来当作一个表达式,
复合表达式的值是其中最后一个表达式的值。
如
3
多个表达式也可以用分号分隔后写在圆括号中,作为一个复合表达式,如
3
B.7.2 比较运算
两个数值之间用如下的比较运算符进行比较:
== != < <= > >=
分别表示等于、不等于、小于、小于等于、大于、大于等于。 要特别注意“等于”比较用两个等号表示。
比较的结果是true(真值)或者false(假值)。 结果类型为Bool类型(布尔型)。
如
true
true
true
true
true
true
false
false
两个字符串之间也可以比较, 比较时按字典序比较, 两个字符的次序按照其Unicode编码值比较。如
false
true
true
B.7.3 逻辑运算
比较通常有变量参与。如
false
true
有时需要构造复合的条件, 如“年龄不足18岁且性别为女”, “年龄在18岁以上或者性别为男”等。
用&&
表示要求两个条件同时成立,
用||
表示只要两个条件之一成立则结果为真,
用!cond
表示cond
的反面。
如
false
true
B.7.4 短路与运算和分支
&&
是一种短路运算, 表达式cond && expr
仅当cond
为true时才计算(运行)expr
,
所以这种写法经常用作程序分支的简写: 条件cond
为真时执行expr
,
否则不执行。
比如,在计算x的平方根之前,先判断其非负:
平方根计算:自变量定义域错误,x=-1.44
B.7.5 短路或运算与分支
||
是一种短路或运算,表达式cond || expr
仅当cond为false时才计算(运行)expr
,
所以这种写法经常作为程序分支的缩写:
条件cond为假时才执行expr
,否则不执行。
比如,求平方根时当自变量不为负时才计算平方根:
true
B.7.7 if–else–end结构
if cond ... else ... end
结构当条件成立时执行第一个分支中的语句,
当条件不成立时执行第二个分支中的语句。
如
x = -1.44
if x >= 0
y = sqrt(x)
println("√", x, " = ", y)
else
y = sqrt(-x)
println("√", x, " = ", y, "i")
end
√-1.44 = 1.2i
B.7.8 if-elseif-else-end结构
if cond1 ... elseif cond2 ... else ... end
可以有多个分支,
有多个条件cond1
, cond2
, ……,
依次判断各个条件,那个条件成立就执行对应分支的语句,
所有条件都不成立则执行else
分支的语句。
条件cond2
隐含条件cond1
不成立,
cond3
隐含cond1
和cond2
都不成立, 依此类推。
例如
age = 35
if age < 18
println("未成年")
elseif age < 60
println("中青年")
elseif age < 100
println("老年")
else
println("老寿星!")
end
中青年
B.7.10 for循环
for循环一般是沿着某个范围进行计算或处理,格式如下:
for loopvar = a:b
expr1
expr2
...
end
其中loopvar
是自己命名的循环变量名,
for结构块内的语句(表达式)先对loopvar=a
运行,
再对loopvar=a+1
运行, 最后对loopvar=b
运行,然后结束。
如
1^3 = 1
2^3 = 8
3^3 = 27
范围也可以是1:2:9
, 0:0.1:1
这样的带有跨度(增量)的,
可以是倒数的如3:-1:1
表示3, 2, 1。
B.7.12 向量元素按下标循环
设x
是一个向量,上面的做法可以遍历x
每个元素。
有时还需要按照向量的下标遍历,这时使用eachindex(x)
,如
Prime No. 1 = 2
Prime No. 2 = 3
Prime No. 3 = 5
Prime No. 4 = 7
Prime No. 5 = 11
B.7.13 列表推导
对向量的循环,经常可以表达成一种称为comprehension的语法, 译为列表推导。 例如,为了生成1, 2, 3的立方的向量,可以写成
3-element Array{Int64,1}:
1
8
27
对前5个素数作立方,可以写成
5-element Array{Int64,1}:
8
27
125
343
1331
B.7.14 两重for循环
for循环可以嵌套,如
1×1 = 1
2×1 = 2 2×2 = 4
3×1 = 3 3×2 = 6 3×3 = 9
4×1 = 4 4×2 = 8 4×3 = 12 4×4 = 16
5×1 = 5 5×2 = 10 5×3 = 15 5×4 = 20 5×5 = 25
6×1 = 6 6×2 = 12 6×3 = 18 6×4 = 24 6×5 = 30 6×6 = 36
7×1 = 7 7×2 = 14 7×3 = 21 7×4 = 28 7×5 = 35 7×6 = 42 7×7 = 49
8×1 = 8 8×2 = 16 8×3 = 24 8×4 = 32 8×5 = 40 8×6 = 48 8×7 = 56 8×8 = 64
9×1 = 9 9×2 = 18 9×3 = 27 9×4 = 36 9×5 = 45 9×6 = 54 9×7 = 63 9×8 = 72 9×9 = 81
这种两重循环可以简写为一个for语句,外层循环先写, 内层循环后写,中间用逗号分隔。 如
1×1 = 1
2×1 = 2 2×2 = 4
3×1 = 3 3×2 = 6 3×3 = 9
4×1 = 4 4×2 = 8 4×3 = 12 4×4 = 16
5×1 = 5 5×2 = 10 5×3 = 15 5×4 = 20 5×5 = 25
6×1 = 6 6×2 = 12 6×3 = 18 6×4 = 24 6×5 = 30 6×6 = 36
7×1 = 7 7×2 = 14 7×3 = 21 7×4 = 28 7×5 = 35 7×6 = 42 7×7 = 49
8×1 = 8 8×2 = 16 8×3 = 24 8×4 = 32 8×5 = 40 8×6 = 48 8×7 = 56 8×8 = 64
9×1 = 9 9×2 = 18 9×3 = 27 9×4 = 36 9×5 = 45 9×6 = 54 9×7 = 63 9×8 = 72 9×9 = 81
B.7.15 矩阵元素遍历
矩阵元素按照行列下标遍历,可以写成两重循环的形式。 Julia的矩阵是按列存储的, 所以循环时先对第一列各个元素循环, 再对第二列各个元素循环, ……,按这样的次序遍历是比较有效的。 如
A1[1, 1] = 1
A1[2, 1] = 4
A1[1, 2] = 2
A1[2, 2] = 5
A1[1, 3] = 3
A1[2, 3] = 6
for结构的两重循环中写在前面的是外层循环, 循环变量变化较慢, 写在后面的是内层循环,循环变量变化较快。 上例中列下标j写在前面,行下标i写在后面, 所以关于列的循环j是外层循环,关于行的循环i是内层循环, 这样的矩阵元素遍历方式是按列次序遍历。
B.7.16 矩阵的列表推导
在方括号内用两重的循环变量遍历可以定义矩阵。如
2×3 Array{Int64,2}:
101 102 103
201 202 203
这里写在前面的循环变量i
对应于行下标,
写在后面的循环变量j
对应于列下标。
执行时行下标i
在内层循环,列下标j
在外层循环。
又如,如下程序返回矩阵对角化的结果:
2×3 Array{Int64,2}:
101 0 0
0 202 0
B.7.17 while循环
for循环适用于对固定的元素或者下标的遍历, 在预先未知具体循环次数时,需要使用当型循环循环或者直到型循环。
Julia中用while cond ... end
表示当型循环,
在条件cond成立时执行结构内的语句, 直到cond不成立时不再循环。
例如,要判断一个奇数x
是否素数,
从3开始逐个用奇数去除x
,直到除尽或者除数等于x
为止:
x = 1333333
i = 3
while i < x && x % i != 0
i += 2
end
if i == x
println(x, "is prime.")
else
println(x, "=", i, " X ", x ÷ i)
end
1333333=23 X 57971
B.7.18 直到型循环与break语句
当型循环每次进入循环之前判断循环条件是否成立, 成立才进入循环。
直到型循环每次先进入循环,在循环末尾判断循环退出条件是否满足, 满足退出条件时就不再循环。 Julia语言没有提供专门的直到型循环语法,可以用如下的方法制作直到型循环:
while true
expr1
expr2
...
cond && break
end
其中cond
是循环退出条件。break
语句表示退出一重循环。
例如,用泰勒展开近似计算自然对数\(\log(1 + x)\): \[ \log(1 + x) = x + \sum_{k=2}^\infty (-1)^{k-1} \frac{x^k}{k} \]
实际计算时不可能计算无穷次,所以指定一个精度如eps=0.0001
,
当计算的通项小于此精度时停止计算。
程序用直到型型循环写成:
eps = 0.0001
x = 1.0
y = x; xk = x; sgn = 1; k = 1
while true
k += 1; sgn *= -1; xk *= x
item = xk / k
y += sgn*item
item < eps && break
end
println("eps = ", eps, " log(1+", x, ") = ", y,
" Iterations: ", k)
eps = 0.0001 log(1+1.0) = 0.6931971730609582 Iterations: 10001