2 复合数据结构

这一部分介绍Julia的向量、元组、集合、字典等复合数据结构, 以及函数的进一步介绍。

2.1 一维数组

Julia支持一维和多维的数组, 当一维数组的元素是数值时, 也可以理解成数学中的向量。

在程序中直接定义一个向量, 只要用方括号内写多个逗号分隔的数值,如

v1 = [2, 3, 5, 7, 11, 13, 17]
7-element Vector{Int64}:
  2
  3
  5
  7
 11
 13
 17
v2 = [1.5, 3, 4, 9.12]
4-element Vector{Float64}:
 1.5
 3.0
 4.0
 9.12

其中v1是整数型的向量, v2是浮点型Float64的向量。 也可以定义元素为字符串的数组, 元素为不同类型的数组, 等等:

v3 = ["苹果", "桔子", "香蕉"]
3-element Vector{String}:
 "苹果"
 "桔子"
 "香蕉"
v4 = [123, 3.14, "数学", [1, 2, 3]]
4-element Vector{Any}:
 123
   3.14
    "数学"
    [1, 2, 3]

length(x)求向量x的元素个数,如

length(v1)
## 7

可以用1:5定义一个范围, 在仅使用其中的元素值而不改写时作用与[1, 2, 3, 4, 5]类似。 1:2:9定义带有步长的范围,表示的值与[1, 3, 5, 7, 9]类似。 范围只需要存储必要的开始、结束、步长信息, 所以更节省空间, 但是不能对其元素进行修改。

1:5
## 1:5
1:2:7
## 1:2:7
5:-1:1
## 5:-1:1

范围不是向量, 而是一种“可遍历数据结构”。 用collect()函数可以将范围转换成向量,如:

collect(5:-1:1)
5-element Vector{Int64}:
 5
 4
 3
 2
 1

2.1.1 向量下标

x是向量,i是正整数, x[i]表示向量的第i个元素。 第一个元素的下标为1,这种规定与R、FORTRAN语言相同, 但不同于Python、C、C++、JAVA语言。 如

v1 = [2, 3, 5, 7, 11, 13, 17]
7-element Vector{Int64}:
  2
  3
  5
  7
 11
 13
 17
v1[2]
## 3

end表示最后一个元素位置,如:

v1[end]
## 17

对元素赋值将在原地修改元素的值,如

v1[2] = 0
@show v1;
## v1 = [2, 0, 5, 7, 11, 13, 17]

这说明数组是“可变类型”(mutable), 即其中的成分可以原地修改。 字符串和元组则属于不可变类型(immutable)。

@show expr可以用比较简洁的带有提示的方式显示表达式和表达式的值。

2.1.1.1 用范围作为下标

下标可以是一个范围,如

v1 = [2, 3, 5, 7, 11, 13, 17]
v1[2:4]
3-element Vector{Int64}:
 3
 5
 7

在这种范围中,用end表示最后一个下标,如

v1[4:end]
4-element Vector{Int64}:
  7
 11
 13
 17
v1[1:(end-3)]
4-element Vector{Int64}:
 2
 3
 5
 7
v1[1:2:7]
4-element Vector{Int64}:
  2
  5
 11
 17
v1[end:-1:1]
7-element Vector{Int64}:
 17
 13
 11
  7
  5
  3
  2

实际上,reverse(x)可以返回次序颠倒后的数组。

可以用仅有冒号作为下标, 这时表示包含所有元素的子集。 取出的多个元素可以修改, 可以用.=运算符赋值为同一个标量,如:

v1 = [2, 3, 5, 7, 11, 13, 17]
v1[:] .= 0; 
@show v1;
## v1 = [0, 0, 0, 0, 0, 0, 0]
v1 = [2, 3, 5, 7, 11, 13, 17]
v1[1:3] .= 0
@show v1;
## v1 = [0, 0, 0, 7, 11, 13, 17]

也可以分别赋值,如

v1 = [2, 3, 5, 7, 11, 13, 17]
v1[1:3] = [101, 303, 505]; 
@show v1;
## v1 = [101, 303, 505, 7, 11, 13, 17]

2.1.2 数组类型

当数组元素都是整数时, 显示其类型为“Array{Int64}”, 常用的还有“Array{Float64}”, “Array{String}”, “Array{Any}”等, “Any”是Julia语言类型系统的根类型, 相应的数组可以容纳任何Julia对象作为元素。

如果数组元素都是基本类型如Float64, 则不允许给元素赋值为其它类型,如:

vf = [1.2, 2.5, 3.6]; vf
3-element Vector{Float64}:
 1.2
 2.5
 3.6
vf[2] = "abc"
## MethodError: Cannot `convert` an object of type String to an object of type Float64
## .........

eltype()求元素类型,如:

eltype(vf)
## Float64

2.1.3 向量初始化

zeros(n)可以生成元素类型为Float64、元素值为0、长度为n的向量,如

zeros(3)
3-element Vector{Float64}:
 0.0
 0.0
 0.0

zeros(Int64, 3)可以生成指定类型的(这里是Int64)初始化向量。如

zeros(Int64, 3)
3-element Vector{Int64}:
 0
 0
 0

Vector{Float64}(undef, n)可以生成元素类型为Float64的长度为n的向量, 元素值未初始化,如

Vector{Float64}(undef, 3)
3-element Vector{Float64}:
 1.40319995e-315
 1.40320011e-315
 1.40320027e-315

类似可以生成其它元素类型的元素值未初始化向量,如

y1 = Vector{Int}(undef, 3);

用这样的办法为向量分配存储空间后可以随后再填入元素值。 可以用fill!()填入统一的值。 函数名以!结尾是一个习惯用法, 表示该函数会修改其第一自变量的值。

如:

fill!(y1, 100)
3-element Vector{Int64}:
 100
 100
 100

也可以用fill(value, n)生成一个元素值都等于value的长度为n的一维数组。

可以用collect()将一个范围转换成可修改的向量。如:

collect(1:5)
5-element Vector{Int64}:
 1
 2
 3
 4
 5

2.1.4 变量与值

由于Julia的变量仅仅是向实际存储空间的引用(reference), 或称绑定(binding), 所以两个变量可以引用(绑定)到同一个向量的存储空间, 修改了其中一个变量的元素值,则另一个变量的元素也被修改了。 如

x1 = [1,2,3]
x2 = x1
x2[2] = 100
@show x1;
## x1 = [1, 100, 3]

用“===”或“”(\equiv+TAB)可以比较两个变量是否同一对象, 如:

x2 === x1
## true

允许两个变量指向同一个对象是有用的, 尤其在函数自变量传递时, 但是在一般程序中这种作法容易引起混淆。 向量(或者数组)作为函数自变量时, 调用函数时传递的是引用, 在函数内可以修改传递进来的向量的元素值。

如果需要制作数组的副本, 用copy()函数。 如

x1 = [1,2,3]
x2 = copy(x1)
x2[2] = -100
@show x1;
## x1 = [1, 2, 3]
x2 === x1
## false

将仅有冒号的子集如x[:]放在等号左边可以修改所有元素, 如果将其放在等号右边并赋值给一个变量, 就可以制作副本,如:

x1 = [1,2,3]
x2 = x1[:]
x2[2] = -100
@show x1;
## x1 = [1, 2, 3]

Julia对象的这种引用或者绑定做法, 初学者比较容易用错。 例如, 下面的程序将一个数组嵌套在另一个数组中:

x0 = [3, 4]
x1 = [1,2, x0]
x1[3][1] = 333
@show x0;
## x0 = [333, 4]

因为x1中引用(绑定)了x0的值, 所以x1[3]x0共用同一存储, 修改了x1[3]就修改了x0。 那么, 制作x1的副本能否解决问题?

x0 = [3, 4]
x1 = [1, 2, x0]
x2 = copy(x1)
x2[1] = 111
x2[3][1] = 333
@show x2;
## x2 = Any[111, 2, [333, 4]]
@show x1;
## x1 = Any[1, 2, [333, 4]]
@show x0;
## x0 = [333, 4]

虽然x1[1]没有被修改,但是x1[3][1]还是被修改了, x0也被修改了。 这是因为copy()执行的是所谓“浅层复制”, 对于内嵌的对象仍为引用。 可以用deepcopy(), 能解决大部分问题:

x0 = [3, 4]
x1 = [1,2, x0]
x2 = deepcopy(x1)
x2[1] = 111
x2[3][1] = 333
@show x2;
## x2 = Any[111, 2, [333, 4]]
@show x1;
## x1 = Any[1, 2, [3, 4]]
@show x0;
## x0 = [3, 4]

仅修改了x2,没有修改x1x0

2.1.5 向量的有关函数

为了判断元素x是否属于数组v,可以用表达式x in vx ∈ v判断, 结果为布尔值。

函数indexin(a, b)返回向量a的每个元素首次出现在b中的位置, 没有时返回nothing,如:

indexin([1,3,5,3], [1,2,3])
4-element Array{Union{Nothing, Int64},1}:
 1
 3
  nothing
 3

v是向量,x是一个元素, push!(v, x)修改向量v, 将x添加到向量v的末尾。 pushfirst!(v, x)修改向量v, 将x添加到向量v的开头,原有的元素后移。 注意, 函数名以叹号结尾是一个习惯约定, 表示此函数会修改其第一个自变量。

insert!(v, k, xi)函数可以在向量v的指定下标k位置插入指定的一个元素, 原有的元素后移。

v3 = [2,3,5]
push!(v3, 7)
@show v3;
## v3 = [2, 3, 5, 7]
pushfirst!(v3, 1)
@show v3;
## v3 = [1, 2, 3, 5, 7]

v是向量,u也是一个向量, append!(v, u)修改向量v, 将u的所有元素添加到向量v的末尾。 要注意append!push!的区别, 一个是添加一个向量的所有元素到末尾, 一个是添加一个元素到末尾。 如

v3 = [2,3,5]
append!(v3, [7,11])
@show v3;
## v3 = [2, 3, 5, 7, 11]

pop!(v)可以返回v的最后一个元素并从v中删除此元素。 popfirst!(v)类似。 splice!(v, k)函数可以返回指定下标位置的元素并从v中删除此元素, deleteat!(v, k)函数可以v中删除指定下标位置的元素但不返回值。 empty!(x)可以情况数组的所有元素, 实际上,这个函数可以情况集合、字典等复合类型的元素。

注意,push!()等函数修改输入的向量的大小, 根据使用的环境, 这可能是很高效的做法, 但是数值计算程序中通常不修改数组大小, 而是预先分配好数组的大小。

如果确实无法预先确定数组大小, 又有运行效率的困扰, 可以用如sizehint!(x, 10000)这样的做法为数组预先提示一个大小, 这可以提高程序的效率。

replace!()函数可以用来在数组中替换元素,如:

x = [1, 2, 1, 4, 1]
replace!(x, 1 => 0)
@show x;
## x = [0, 2, 0, 4, 0]
x = [1, 2, 1, 4, 1]
replace!(x, 1 => 0, 4 => 3)
@show x;
## x = [0, 2, 0, 3, 0]

可以指定一个总替换次数的上限,如:

x = [1, 2, 1, 4, 1]
replace!(x, 1 => 0, 4 => 3, count = 2)
@show x;
## x = [0, 2, 0, 4, 1]

如果要合并两个一维数组并将结果生成一个新数组, 不修改原来的两个数组, 可以用vcat()函数,如:

v1 = [1,2]; v2 = [-2, -1]
v3 = vcat(v1, v2)
@show v3;
## v3 = [1, 2, -2, -1]
v3[1] = 111
@show v1;
## v1 = [1, 2]

filter!(f, x)指定一个示性函数f,将x中不满足条件的元素删除, 如:

x = [2, 3, 5, 7, 11, 13]
filter!(a -> a % 3 == 1, x)
show(x)
## [7, 13]

unique(v)返回去掉重复元素的结果, unique!(v)则直接去掉v中的重复元素。

sort(v)返回向量v按升序排序的结果; sort!(v)直接修改v,将其元素按升序排序。 如果要用降序排序,可以加选项rev=truesortperm(v)返回将v的元素从小到大排序所需要的下标序列, 在多个等长向量按照其中一个的次序同时排序时此函数有用。

maximum(v)求最大值, minimum(v)求最小值, argmax(v)求最大值首次出现的下标, argmin(v)求最大值首次出现的下标。 findmax(v)findmin(v)则返回最值和相应的首次出现的下标。

sum(v)求和, prod(v)求乘积。

对于布尔型数组, all(v)判断所有元素为真, any(v)判断存在真值元素。

x == y可以比较两个等长数组的对应元素是否完全相同。

对于字符串x, 可以用collect(x)将其转换为每个字符为一个元素的数组。 对于元组, 也可以用collect转换成数组。

2.1.6 广播

许多现代的数据分析语言, 如Python, Matlab, R等都存在循环的效率比编译代码低一两个数量级的问题, 在这些语言中, 如果将对向量和矩阵元素的操作向量化, 即以向量和矩阵整体来执行计算, 就可以利用语言内建的向量化计算获得与编译代码相近的执行效率。

Julia语言依靠其LLVM动态编译功能, 对向量和矩阵元素循环时不损失效率, 用显式循环处理向量、矩阵与向量化做法效率相近, 有时显式循环效率更高。 但是,向量化计算的程序代码更简洁。

Julia中的函数, 包括自定义函数, 如果可以对单个标量执行, 将函数名加后缀句点后, 就可以变成向量化版本, 对向量和矩阵执行。 这称为广播。 运算也是如此,运算符前面加点后就可以将标量运算应用到元素之间的运算。 如

sqrt.([1,2,3])
3-element Array{Float64,1}:
 1.0
 1.4142135623730951
 1.7320508075688772

这种向量化对于多个自变量的函数也成立。 通过编译优化, 可以达到专用的以向量作为输入的函数的效率, 如果同一个语句中有多个加点运算, 编译时能够将其尽可能地合并到同一循环中。

2.2 元组(Tuple)

2.2.1 概念和生成

与向量类似的一种数据类型称为元组(tuple)。 如

(1, 2, 3)
## (1, 2, 3)
(1, "John", 5.1)
## (1, "John", 5.1)

元组的元素不要求属于同一类型。

单个元素的元组要有逗号分隔符,如(1,)是单个元素的元组, 而(1)不是元组。

元组表面上类似于一维数组, 但是元组属于不可修改(immutable)类型, 不能修改其中的元素。 其存储也与数组不同。

可以用tuple()函数生成元组。 可以用类似一维数组的方法对元组取子集, 如x[1], x[2:3]等。 如:

x = ('a', 'b', 'c', 'd')
typeof(x)
## NTuple{4,Char}

这个类型的意思是由4个字符组成的元组。

2.2.2 访问片段

x[1]
## 'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
x[2:3]
## ('b', 'c')

不允许修改元组中的元素:

x[2] = 'x'

结果出错:

MethodError: no method matching setindex!(::NTuple{4,Char}, ::Char, ::Int64)
.........

2.2.3 比较

元组可以看作一个整体参加比较, 比较方法类似于字典序,如:

(1, 3, 5) < (1, 3, 6)
## true

2.2.4 赋值等号左边的元组

可以利用元组写法对变量同时赋值, 如

a, b = 13, 17
println("a=", a, " b=", b)
## a=13 b=17

这种赋值可以用来交换两个变量的值,如:

a, b = b, a
println("a=", a, " b=", b)
## a=17 b=13

元组赋值的右侧也可以是数组等其它序列类型,如

a, b = [19, 23]
println("a=", a, " b=", b)
## a=19 b=23

2.2.5 用元组在函数中返回多个值

自定义函数可以返回元组, 从而返回多个值,见下面的自定义函数章节。 内置的有些函数就利用了这种特性, 比如,divrem(x, y)返回除法的商和余数:

divrem(11, 4)
## (2, 3)

2.2.6 元组转换为一维数组

collect()将元组转换为一维数组,如:

collect((1,3,5))
3-element Array{Int64,1}:
 1
 3
 5

2.2.7 两个向量成对使用

xy是两个等长的一维数组, zip(x, y)是一个迭代器(可以用在for循环中), 每次迭代返回两个数组的一对对应元素。 可以用collect()将迭代器转换成二元组的数组:

x = ["a", "b", "c"]
y = [3, 1, 2]
collect(zip(x, y))
3-element Vector{Tuple{String, Int64}}:
 ("a", 3)
 ("b", 1)
 ("c", 2)

2.3 有名元组

元组可以为元素命名, 这使得其在一定程度上类似于字典。 但是,字典是可变类型(可以修改其中的元素), 元组是不可变类型。 如

tn1 = (; name="John", age=32)
tn1[:name]
## "John"
tn1[1]
## "John"

定义时用了左括号后面加一个分号的格式。 这是推荐的写法, 当有多个元素时可以省略分号, 但有分号使得定义有名元组的意图更明显。

有名元组经常用来给函数的关键字参数赋值, 而这样的关键字参数的值又是类似关键字参数的, 即内容可有可无,可多可少。

要注意有名元组用变量名访问时用的是符号(Symbol), 即不写成字符串的变量名前面有冒号。 也可以用加点格式访问:

tn1.name
## "John"

2.4 字典

2.4.1 生成字典

Julia提供了一种Dict数据类型, 是映射的集合, 每个元素是从一个“键”(key)到另一个“值”(value)的映射, 元素之间没有固定次序。如

d = Dict("name" => "Li Ming", "age" => 18)
Dict{String,Any} with 2 entries:
  "name" => "Li Ming"
  "age"  => 18

Dict()生成空的字典。

也可以用二元组的数组作为初值定义字典,如

d2orig = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
d2 = Dict(d2orig)
Dict{Char,Int64} with 4 entries:
  'a' => 1
  'c' => 3
  'd' => 4
  'b' => 2

当键和值分别保存在两个等长的数组中的时候, 可以用zip()函数将这两个数组合并为二元组的数组, 从而产生字典,如:

x = ['a', 'b', 'c', 'd']
y = [1,2,3,4]
d2 = Dict(zip(x, y))
Dict{Char, Int64} with 4 entries:
  'a' => 1
  'c' => 3
  'd' => 4
  'b' => 2

length()求长度,如:

length(d2)
## 4

字典的键可以用字符串、整数值、浮点数值、元组这样的不可变类型(immutable), 不能取数组这样的可变类型(mutable)。 最常用的是字符串。

上面生成字典的方法是自动判断键和值的数据类型, 为保险起见, 最好在生成字典时指定键和值的数据类型, 格式为Dict{S, T}(...)S为键的类型,T为值的类型。如:

Dict{String, Int64}("apple" => 1, "pear" => 2, "orange" => 3)
Dict{String,Int64} with 3 entries:
  "pear"   => 2
  "orange" => 3
  "apple"  => 1

2.4.2 对(Pair)

事实上, "apple" => 1这样的写法也是Julia的一种数据类型, 称为“对”(Pair)。 如:

x = "apple" => 1
typeof(x)
## Pair{String, Int64}

first(x)取出对的第一项, 用last(x)取出对的第二项。如:

(first(x), last(x))
## ("apple", 1)

可以用collect(dict)将字典转换成键、值二元组的一维数组,如:

collect(d2)
4-element Vector{Pair{Char, Int64}}:
 'a' => 1
 'c' => 3
 'd' => 4
 'b' => 2

2.4.3 访问元素

访问单个元素如

d = Dict("name" => "Li Ming", "age" => 18)
d["age"]
## 18

这种功能类似于R语言中用元素名作为下标, 但R中还可以用序号访问元素, 而字典中的元素没有次序,不能用序号访问。

读取字典中单个键的对应值也可以用get(d, key, default)的格式, 其中default是元素不存在时的返回值。如:

get(d, "age", 0)
## 18

可以用haskey(d, key)检查某个键值是否存在,如:

haskey(d, "gender")
## false

给不存在的键值赋值就可以增加一对映射,如

d["gender"] = "Male";
@show d;
## d = Dict{String, Any}("name" => "Li Ming", 
##     "gender" => "Male", "age" => 18)

delete!(d, key)可以删除指定的键值对。

get!(d, key, default)可以在指定键值不存在时用default值填入该键值, 已存在时就不做修改, 两种情况下都返回新填入或原有的键对应的值。

pop!(d, key)返回key对应的值并从字典中删除该键值对。

merge(dict1, dict2)合并两个字典, 有共同键时取后一个字典的值。

2.4.4 遍历字典

可以用keys()函数遍历各个键值,次序不确定:

d2 = Dict('a' => 1, 'b' => 2, 'c' => 3, 'd' => 4)
for k in keys(d2)
    println(k, " => ", d2[k])
end
a => 1
c => 3
d => 4
b => 2

除了可以用haskey(dict, key)判断某个键是否存在, 也可以用key in keys(dict)判断,如:

'a' in keys(d2)
## true
'g'  keys(d2)
## false

在字典中查找某个键是使用散列表(hash table)技术, 所以查找时间不会随元素个数增长而线性增长, 可以比较方便地存储需要快速查找的键值对。

字典存储并没有固定的存储次序。 为了在遍历时按键值的次序, 需要使用如下的效率较低的方法:

for k in sort(collect(keys(d2)))
    println(k, " => ", d2[k])
end
a => 1
b => 2
c => 3
d => 4

对字典排序遍历的另一方法是将字典转换成键值对的数组, 然后用sort排序, 再遍历,如:

d2p = collect(d2)
sort!(d2p, by=first)
for (k, v) in d2p
    println(k, " ==> ", v)
end
a ==> 1
b ==> 2
c ==> 3
d ==> 4

可以用values()遍历各个值,但也没有固定次序。比如

collect(values(d2))
4-element Array{Int64,1}:
 1
 3
 4
 2

可以直接用二元组对字典遍历,如

for (k,v) in d2
    println(k, " => ", v)
end
a => 1
c => 3
d => 4
b => 2

可以将字典转换成键值对的列表, 如:

[(k, v) for (k, v) in d2]
4-element Vector{Tuple{Char, Int64}}:
 ('a', 1)
 ('c', 3)
 ('d', 4)
 ('b', 2)

2.4.5 用生成器生成字典

可以用Dict(x => f(x) for x in collection)的方法生成字典, 如:

Dict(x => x*x for x in [2,3,5,7])
Dict{Int64,Int64} with 4 entries:
  7 => 49
  2 => 4
  3 => 9
  5 => 25

2.4.6 字典应用:频数表

在基本的描述统计中, 经常需要对某个离散取值的变量计算其频数表, 即每个不同值出现的次数。 如果不利用字典类型, 可以先找到所有的不同值, 将每个值与一个序号对应, 然后建立一个一维数组计数, 每个数组元素与一个变量值对应。

利用字典, 我们不需要预先找到所有不同值,而是直接用字典计数, 每个键值是一个不同的变量值, 每个值是一个计数值。 对字典可以用get()函数提取某个键值对应的值, 并在键值不存在时返回指定的缺省值。 如:

sex = ["F", "M", "M", "F", "M"]
freqs = Dict()
for xi in sex
    freqs[xi] = get(freqs, xi, 0) + 1
end
freqs
Dict{Any, Any} with 2 entries:
  "M" => 3
  "F" => 2

将上述的频数计算功能编写成一个函数如下:

function freqd(x)
    y = Dict()
    for xi in x
        y[xi] = get(y, xi, 0) + 1
    end
    return y
end
freqd(sex)
Dict{Any, Any} with 2 entries:
  "M" => 3
  "F" => 2

StatsBase包的countmap函数实现了上述的freqd的功能。 如:

using StatsBase
StatsBase.countmap(sex)
Dict{String, Int64} with 2 entries:
  "M" => 3
  "F" => 2

可以看出StatsBase的版本更为合理, 其返回的字典的数据类型更加精确。

有时返回字典类型不方便使用, 可以返回取值和频数分别的列表:

function freq(x)
    y = StatsBase.countmap(x)
    return keys(y), values(y)
end
freq(sex)
## (["M", "F"], [3, 2])
d3 = freq("disillusionment")
d3
## (['n', 'd', 'i', 's', 'l', 'u', 'o', 'm', 'e', 't'], 
##  [2, 1, 3, 2, 2, 1, 1, 1, 1, 1])

因为字典的键必须是不可变类型, 所以freq()中数组x的元素必须是不可变类型。

2.5 集合类型

Julia中Set是集合类型。 集合是可变类型, 没有重复元素, 元素没有次序。

Set()生成一个集合,如:

Set(1:3)
Set{Int64} with 3 elements:
  2
  3
  1
Set([1,2,3])
Set{Int64} with 3 elements:
  2
  3
  1
Set(['a', 'b', 'c', 'b'])
Set{Char} with 3 elements:
  'a'
  'c'
  'b'
Set("keep")
Set{Char} with 3 elements:
  'k'
  'e'
  'p'

Set()输入一个序列(字符串也是序列), 将序列的元素变成集合元素。 注意Set([1,2,3])正确而Set(1,2,3)错误。

因为字符串也是序列, 所以, 要生成只有一个字符串的集合, 也需要将其作为字符串的数组输入,如:

Set(["keep"])
Set{String} with 1 element:
  "keep"

支持集合的常见运算:

  • union(A, B)A ∪ B:并集。输入为\cup<TAB>
  • intersect(A, B)A ∩ B: 交集。输入为\cap<TAB>
  • setdiff(A, B)A \ B:差集。
  • symdiff(A, B):对称差集,即\((A \backslash B) \cup (B \backslash A)\)
  • issetequal(A, B): 集合相等。
  • issubset(A, B)A ⊆ B:子集,输入为\subseteq<TAB>(\nsubseteq+TAB)表示非子集。
  • (\supseteq<TAB>):超集。(\nsupseteq<TAB>)表示非超集。
  • 属于关系用in, (\in+TAB),(\ni+TAB),(\notin+TAB), (\nni+TAB)表示。

例如, 判断某个单词的字母都在另一个单词的字母中:

Set("cat")  Set("atomic")
## true

判断某个单词中有没有重复字母:

length(Set("keep")) < length("keep")
## true

push!(x, a)将元素a加入到集合x中。

对数组x, unique(x)返回由x的不同元素组成的数组。

2.6 自定义复合数据类型

类似于其它编程语言中的struct, class, Julia可以用mutable struct或者struct定义自己的复合数据类型。 例如, 为了表示平面上的一个矩形, 我们需要一个左下角坐标和长度、高度, 就可以定义如下的数据类型:

mutable struct Rectangle
    xll::Real 
    yll::Real 
    width::Real
    height::Real 
end

这定义了一个新的数据类型Rectangle。 命名的惯例是使用大写字母开头。 其中的xll, yll, width, height称为这个复合数据结构的“属性”。

生成这个类型的变量:

rect1 = Rectangle(0, 0, 2, 1)

这表示左下角坐标为\((0,0)\),宽度为2,高度为1的一个矩形。 用变量名.属性名的格式访问其中的属性,如:

rect1 = Rectangle(0, 0, 2, 1)
rect1.width
## 2

可以针对这样的自定义类型定义相应的运算和函数, 比如, 平移运算的函数:

function move(rect::Rectangle, offset) 
    Rectangle(rect.xll + offset[1], 
        rect.yll + offset[2],
        rect.width, rect.height)
end

测试:

rect2 = move(rect1, (20, 10))
## Rectangle(20, 10, 2, 1)

struct定义的复合数据, 其中的属性不允许修改。 用mutable struct定义的复合数据, 其中的属性是可以修改的。