Arrays#
Learning Objectives#
Understand the concept of arrays in Julia
Create and initialise arrays
Utilise various array creation functions
Perform basic manipulations such as indexing and slicing on arrays
Implement common operations on arrays and matrices
Introduction#
We’ve already been using arrays informally earlier in the course, so let’s now consider them more thoroughly. Arrays are a fundamental data structure in Julia that are used to store collections of elements. If you’re coming from Python, you can think of Julia arrays as similar to NumPy arrays. One key difference: Julia arrays are 1-indexed (the first element is index 1, not 0).
In Julia, all elements of an array have the same type. To define an array in Julia, we need to specify the type of elements in the array and the number of dimensions (i.e. the number of ‘axes’):
Array{T, N} # T is the element type, N is the number of dimensions
For example:
Array{Int, 1}
: a 1-dimensional array ofInt
sArray{Float64, 2}
: a 2-dimensional array ofFloat64
sArray{Any, 99}
: a 99-dimensional array that can contain any objects.
This allows Julia to optimise the performance of operations on arrays, at least in cases where the type of elements is a concrete (non-abstract) type. While it is possible to have arrays of mixed types (e.g. any kind of Number
, or a mix of String
s and other types, we most often use uniform-type arrays.
Note: Julia uses the aliases Vector
for a 1-dimensional array and Matrix
for a 2-dimensional array. So, for example,
Vector{String}
is the same asArray{String, 1}
Matrix{Int32}
is the same asArray{Int32, 2}
We’ll start by looking at how to construct arrays of arbitrary shape, then look at the 1-dimensional (Vector
) and 2-dimensional (Matrix
) cases specifically, since these are the most common cases you’ll likely encounter.
Array Constructors#
Julia provides a general Array
constructor, though it is only useful for instantiating an array with ‘garbage’ values, ready for population later:
Array{T}(undef, n1, n2, ...) # element type T, shape (n1, n2, ...)
The undef
keyword tells Julia we want to allocate the array without initialising its contents. For numerical types, the contents of the array will be whatever was already in the memory locations, interpreted as elements of type T
(essentially garbage e.g. random-looking numbers). For example:
# Create an uninitialized 2D array of Int with 3 rows and 2 columns
Array{Int}(undef, 3, 2)
3×2 Matrix{Int64}:
140730758998000 140730758998256
140730758998000 140730758998256
140730758998256 140730759006912
# Create an uninitialised 3D array of String with 2 locations in each dim
Array{String}(undef, 2, 2, 2)
2×2×2 Array{String, 3}:
[:, :, 1] =
#undef #undef
#undef #undef
[:, :, 2] =
#undef #undef
#undef #undef
It’s up to us to assign values to all positions before using them, or else those initial values are meaningless. The benefit of undef
is if you plan to populate the array immediately, you don’t spend time setting arbitrary values (which is more performant).
To populate the array, we use square-bracket indexing and the assignment operator =
, like in many other languages. For an array A
with n
dimensions, use A[i1, i2, ..., in]
to assign to the element at location (i1, i2, ..., in)
. Note that array indices start at 1, rather than 0; if you try to access index 0, you’ll get a BoundsError
.
# 2D array of Float64
arr = Array{Float64}(undef, 2, 3)
# Assign element values
arr[1, 1] = 1
arr[2, 1] = 10
arr[1, 2] = 2
arr[2, 2] = 20
arr[1, 3] = 3
arr[2, 3] = 30
arr
2×3 Matrix{Float64}:
1.0 2.0 3.0
10.0 20.0 30.0
(As a side note: observe how the integer values 1
, 10
, … were coerced to the element type for the array, Float64
.)
Obviously this method will get tedious quickly if we just hard-code values: we’ll see alternative syntax for doing this later. For now, let’s look at how we could use iteration to fill in the values instead. We can use the function axes
to get the range of indices for an array along each dimension:
arr = Array{Float64}(undef, 2, 3)
# View the index range along dimension 1
println(axes(arr, 1))
# And along dimension 2
println(axes(arr, 2))
Base.OneTo(2)
Base.OneTo(3)
In the above, Base.OneTo(n)
signifies the range 1, ..., n
. So for our 2 x 3 array arr
, we the indices range from 1 to 2 in the first dimension (rows) and 1 to 3 in the second dimension (columns), as we’d expect.
We can use this to assign values to elements of arr
via a for
loop:
# Loop through each row and column
for i in axes(arr, 1)
for j in axes(arr, 2)
arr[i, j] = j * 10^(i - 1)
end
end
arr
2×3 Matrix{Float64}:
1.0 2.0 3.0
10.0 20.0 30.0
Exercise: a 3-dim array#
Create a 3-dimensional array with shape (2, 3, 4)
, where the element at index [i, j, k]
is equal to the integer \(2^i \times 3^j \times 5^k\).
# Answer here
Pre-made constructors#
Julia also offers convenient functions to create arrays filled with specific values, including:
zeros(T, dims...)
: array of zeros of typeT
(or leave outT
forFloat64
by default).ones(T, dims...)
: array of ones of typeT
(or leave outT
forFloat64
by default).fill(x, dims...)
: array with all entries equal tox
(element type inferred fromx
).rand(dims...)
: array of random numbers, each drawn from a uniform distribution in the half-open inveral \([0,1)\).randn(dims...)
: array of random numbers, each drawn from a normal (Gaussian) distribution.similar(A)
: an uninitialised array of the same type asA
, with the same element type and dimensions. Or specify a new element typeT
viasimilar(A, T)
and/or dimensions viasimilar(A, T, dims...)
.
The dims...
means we specify the length of each dimension as a separate argument.
For example:
# 1D array of Int zeros of length 5
zeros(Int, 5)
5-element Vector{Int64}:
0
0
0
0
0
# 2D array of Complex{Float64} ones of shape (3, 2)
ones(Complex{Float64}, 3, 2)
3×2 Matrix{ComplexF64}:
1.0+0.0im 1.0+0.0im
1.0+0.0im 1.0+0.0im
1.0+0.0im 1.0+0.0im
# 3D array filled with integer value 42 of shape (2, 2, 2)
fill(42, 2, 2, 2)
2×2×2 Array{Int64, 3}:
[:, :, 1] =
42 42
42 42
[:, :, 2] =
42 42
42 42
# 1D array of length 4 of random numbers (uniformly distributed)
rand(4)
4-element Vector{Float64}:
0.5541927388896394
0.6670051307870088
0.09739020540092713
0.7689171434698205
# 2D array of shape (2, 2) of random numbers (normally distributed)
randn(2, 2)
2×2 Matrix{Float64}:
-1.08486 -1.59205
-0.114509 0.294107
# Uninitialised array based on a random array
similar(rand(3, 4))
3×4 Matrix{Float64}:
1.44672e-311 1.44672e-311 1.44672e-311 1.44672e-311
1.44672e-311 1.44672e-311 1.4467e-311 1.44672e-311
1.44672e-311 1.4467e-311 1.44672e-311 1.44672e-311
Basic functions#
We have the following functions for getting basic information about an array:
eltype(arr)
: get the element typelength(arr)
: the total number of elements in the arrayndims(arr)
: the number of dimensions (number of axes)size(arr)
: tuple of dimensions / shape of array
arr = Array{Float64}(undef, 2, 3, 5)
# Get the element type
@show eltype(arr)
# Get the total number of elements
@show length(arr)
# The number of dimensions (number of axes)
@show ndims(arr)
# Tuple of dimensions
@show size(arr);
eltype(arr) = Float64
length(arr) = 30
ndims(arr) = 3
size(arr) = (2, 3, 5)
1-Dimensional Arrays (Vectors)#
A one-dimensional array (also called a Vector
) is essentially an ordered list of elements. The simplest way to create one is by using square brackets [...]
with commas separating elements:
# Declare and initialize a 1D array of integers
a = [1, 2, 3]
println(a)
# See type
println(typeof(a))
[1, 2, 3]
Vector{Int64}
Julia infers the type from the elements you provided. If you mix types, Julia will promote to a common type if possible or else use the generic type Any
.
a = [1, 2.2]
println("a is ", a, " of type ", typeof(a))
b = [Int16(1), UInt8(2)]
println("b is ", b, " of type ", typeof(b))
c = [1, "1"]
println("c is ", c, " of type ", typeof(c))
a is [1.0, 2.2] of type Vector{Float64}
b is Int16[1, 2] of type Vector{Int16}
c is Any[1, "1"] of type Vector{Any}
We can specify the type to convert elements to by prepending the square brackets with the type:
# Ensure we have an array of Float32
d = Float32[1 / 3, 1, 3 // 2]
println("d is ", d, " of type ", typeof(d))
d is Float32[0.33333334, 1.0, 1.5] of type Vector{Float32}
You can also create an empty array and push!
elements into it:
# Empty vector of Int
empty_vec = Int[]
# Append elements to it
push!(empty_vec, 10)
push!(empty_vec, 20)
empty_vec
2-element Vector{Int64}:
10
20
2-Dimensional Arrays (Matrices)#
You can create a 2D array (Matrix
) in Julia using spaces to separate columns and semicolons ;
(or just new lines) to separate rows, within the square brackets:
# 2x3 matrix (2 rows, 3 columns)
M = [1 2 3
4 5 6]
# Equivalently:
M = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
1 2 3
4 5 6
In the above, the first row is 1 2 3
, and the second row is 4 5 6
. Julia interprets that as a 2-row, 3-column matrix of Ints.
As for vectors, we can specify a particular element type by prepending the square brackets with the type:
# Matrix of Int16
Int16[1 1
0 0]
2×2 Matrix{Int16}:
1 1
0 0
Unlike 1-D array literals, where we would write something like [1, 2, 3]
, Julia’s matrix literals use spaces to separate columns and semicolons/newlines to separate rows, instead of commas.
In fact, when it comes to matrices
semicolons/newlines are used to perform vertical concatenation (to extend along the first dimension)
spaces are used to perform horizontal concatenation (to extend along the second dimension)
This can be rather useful for specifying matrices with certain block structures. For example:
# Create individual blocks
A = fill(1.0, 2, 2)
B = zeros(2, 3)
C = zeros(4, 2)
D = fill(2.0, 4, 3)
block_mat = [A B
C D]
6×5 Matrix{Float64}:
1.0 1.0 0.0 0.0 0.0
1.0 1.0 0.0 0.0 0.0
0.0 0.0 2.0 2.0 2.0
0.0 0.0 2.0 2.0 2.0
0.0 0.0 2.0 2.0 2.0
0.0 0.0 2.0 2.0 2.0
Exercise: Creating vectors and matrices#
Create a 3x3 identity matrix manually using the above notation; the identity matrix has 1s on its diagonal and 0s elsewhere). Verify its structure by printing it.
Create a few vectors using the above notation. Then create a matrix with where the columns are given by the vectors you create.
Use the functions
zeros
,ones
and/orfill
to create the following matrix (withInt
entries): $\( \begin{bmatrix} 0 & 0 & 1 & 1 \\ 0 & 0 & 1 & 1 \\ 0 & 0 & 2 & 2 \\ \end{bmatrix} \)$
# Answer here
Exercise: Vector / matrix notation workout#
For each of the following declarations, try to guess what kind of array will be created, by predicting whether it is a Vector
or a Matrix
, its shape and the element type. Then run the code to see if you were correct.
# x = [1 2 3; 1 2 3]
# x = [1, 2.0, 3, 4.0, 5, 6.0]
# x = [1 2 3 4 5 6]
# x = [1; 2; 3; 4; 5; 6]
# x = [[1, 1] [2, 2] [3, 3]]
# x = [[1, 1], [2, 2], [3, 3]]
# X = [[1 1], [2 2], [3 3]]
# x = [[1, 1]
# [2, 2]
# [3, 3]]
# x = [[1 1] [2 2] [3 3]]
Higher-dimensional array literals#
For higher-dimensional arrays, we can use a generalisation of concatenation with the semicolon ;
to build up arrays by slices. The number of semicolons we use depends on the dimension we’re concatenating along. For example, suppose we want to define a 3-dimensional array with shape (n1, n2, n3)
. The way we can think of this is as n3
lots of (n1, n2)
-dimensional matrices, stacked together along the 3rd (last) dimension. To perform the concatenation along the 3rd dimension, we use 3 semicolons, ;;;
.
For example, to create a 3D array with shape (2, 3, 2)
:
# Array with shape (2, 3, 2)
A = [
[1 3 5
2 4 6];;; # matrix at slice [:, :, 1]
[7 9 11
8 10 12] # matrix at slice [:, :, 2]
]
2×3×2 Array{Int64, 3}:
[:, :, 1] =
1 3 5
2 4 6
[:, :, 2] =
7 9 11
8 10 12
Notice how Julia prints out the array in a similar way to how we entered it i.e. as a series of matrix slices.
If we had higher dimensions we’d use the corresponding number of semicolons for each dimension e.g. ;;;;
for 4th dimension etc. Concatenations are performed along dimensions in increasing order i.e. first create the inner matrices, then concatenate along dimension 3, then along dimension 4 etc.
It’s rare to need to hard-code 3+ dimensional arrays in practice and the notation can quickly become tricky to read (and remember how to type!). So you may wish to instead build up higher dimensional arrays in steps, using the cat
function. For example, to create the same array as above using cat
we could do:
# Create A by concatenating matrices
A1 = [1 3 5
2 4 6]
A2 = [7 9 11
8 10 12]
A3 = [13 15 17
14 16 18]
A = cat(A1, A2, A3; dims=3) # concatenate A1 and A2 along a new, 3rd dimension
2×3×3 Array{Int64, 3}:
[:, :, 1] =
1 3 5
2 4 6
[:, :, 2] =
7 9 11
8 10 12
[:, :, 3] =
13 15 17
14 16 18
# Create another 3D array in similar way
B1 = [10 30 50
20 40 60]
B2 = [70 90 110
80 100 120]
B3 = [130 150 170
140 160 180]
B = cat(B1, B2, B3; dims=3) # concatenate B1 and B2 along a new, 3rd dimension
# Create a 4D array by concatenating the two 3D arrays
AB = cat(A, B; dims=4)
2×3×3×2 Array{Int64, 4}:
[:, :, 1, 1] =
1 3 5
2 4 6
[:, :, 2, 1] =
7 9 11
8 10 12
[:, :, 3, 1] =
13 15 17
14 16 18
[:, :, 1, 2] =
10 30 50
20 40 60
[:, :, 2, 2] =
70 90 110
80 100 120
[:, :, 3, 2] =
130 150 170
140 160 180
Indexing and Slicing#
Manipulating arrays involves accessing and modifying their elements. Recall from above that we use square brackets to index e.g. A[i, j, k]
(and indices start at 1). We saw this for assigning array values already, but we can also use it just to retrieve array elements:
v = [10, 20, 30, 40, 50]
# Get 3rd entry
println(v[3])
# Modify 3rd entry
v[3] = 35
println(v)
30
[10, 20, 35, 40, 50]
M = [10 20 30 40 50
60 70 80 90 100]
# Get/modify the (2, 3)-entry
println(M[2, 3])
M[1, 5] = 9000
println(M)
80
[10 20 30 40 9000; 60 70 80 90 100]
We can also use the keyword end
to indicate the last index along a dimension:
# Get the very last element of the matrix
M[end, end]
100
Julia also supports slicing to get subarrays. You can use the :
operator to indicate a range of indices.
Note that both bounds in the slice are included in the slice (unlike in Python where the last isn’t included).
A slice or single index needs to be provided for each dimension.
A a colon
:
on its own with no beginning/end means ‘run along all indices in that dimension’.
For example:
v = [10, 20, 30, 40, 50]
# 2nd through 4th elements
println(v[2:4])
# 2nd entry to the end
println(v[2:end])
M = [1 4 7
2 5 8
3 6 9];
# Submatrix of first 2 rows and cols 2-3
println(M[1:2, 2:3])
# The last column
println(M[:, end])
[20, 30, 40]
[20, 30, 40, 50]
[4 7; 5 8]
[7, 8, 9]
Preserving array shape#
When it comes to matrices (or higher-dimensional arrays) note that slicing along one axis but keeping the others fixed to a single index will return a Vector
:
# The first column as a Vector
M[:, 1]
3-element Vector{Int64}:
1
2
3
# The first row (also as a Vector)
M[1, :]
3-element Vector{Int64}:
1
4
7
Sometimes though we want to preserve the dimensionality of the array. In this case, we can use square brackets, like so:
# The first column as a 3 x 1 matrix
M[:, [1]]
3×1 Matrix{Int64}:
1
2
3
# The first row as a 1 x 3 matrix
M[[1], :]
1×3 Matrix{Int64}:
1 4 7
Note M[[1], :]
is using a length 1 Vector [1]
as part of the indexing. This applies more generally to allow us to specify particular indices along a dimension. For example:
v = [10, 20, 30, 40, 50, 60]
# Get even index entries
v[[2, 4, 6]]
3-element Vector{Int64}:
20
40
60
M = [10 20 30 40 50
60 70 80 90 100
110 120 130 140 150]
# Extract 2 x 2 submatrix
M[[1, 3], [2, 4]]
2×2 Matrix{Int64}:
20 40
120 140
Exercise: indexing and slicing arrays#
The following Julia code defines a 3D array where the \((i, j, k)\) entry is equal to \(2^i \times 3^j \times 5^k\) (for \(i,j,k\) each running from 1 to 10):
arr = [2^i * 3^j * 5^k for i in 1:10, j in 1:10, k in 1:10]
(If you’d like to learn how this works, check out the section on array comprehensions in the Julia manual!)
Use the array to find the value of \(2^6 \times 3 \times 5^4\) (i.e. the element at location
(6, 1, 4)
).Use array slicing to show all the values \(2^i \times 3^2 \times 5\) for \(i = 1, \dots, 6\).
Use array slicing to show all the values \(2^i \times 3^j \times 5^2\) where \(i,j\) are between \(1\) and \(10\) and are even numbers.
arr = [2^i * 3^j * 5^k for i in 1:10, j in 1:10, k in 1:10];
# Answer to 1
# Answer to 2
# Answer to 3
Assigning to a subarray#
You can also modify subarrays by slicing on the left-hand side of an assignment:
v = [1, 2, 3, 4, 5]
# Assign to elements 2 to 4
v[2:4] = [100, 200, 300]
v
5-element Vector{Int64}:
1
100
200
300
5
M = [1 2 3 4
5 6 7 8
9 10 11 12]
# Assign to submatrix
M[2:3, 2:4] = [9999 9999 9999
9999 9999 9999]
M
3×4 Matrix{Int64}:
1 2 3 4
5 9999 9999 9999
9 9999 9999 9999
Exercise: assigning to a submatrix#
Use assignment with appropriate index slicing to change the values of the matrix of ones as follows: $\( \begin{bmatrix} 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 \\ \end{bmatrix} \longrightarrow \begin{bmatrix} 0 & 1 & 1 & 0 \\ 0 & 1 & 1 & 0 \\ 0 & 1 & 1 & 0 \\ \end{bmatrix} \)$
M = ones(Int, 3, 4);
# Answer here
Common Operations on Arrays#
Julia provides lots of functions for working with arrays. Here are some you might find useful; check out the documentation on arrays for lots more!
Combining arrays: You can concatenate arrays along particular dimensions using
cat
, seen earlier. The functionsvcat
(vertical concatenation) andhcat
(horizontal concatenation) provide special cases.Built-in functions: Julia has many handy functions like
sum
to sum array elements,maximum
,minimum
, to find max/min elements,sort
for sorting, etc.Searching: Use
findall
to find the indices of all elements equal totrue
in an array ofBool
(or the indices where some functionf
evaluates to true on the array entries)Reshaping: Use
reshape
to convert an arrayA
to a new arrayA_reshaped
having the same elements asA
but a different shape (even potentially a different dimensionality).Linear algebra: The standard library module LinearAlgebra provides all sorts of functions for doing linear algebra with
Vector
,Matrix
, e.g. computing norms of vectors, determinants of matrices, eigenvectors/eigenvalues etc.
Some functions that apply just to Vector
:
Append elements: Use
push!(array, value)
to append a single value to the end of a 1D array (vector). Useappend!(array, collection)
to append all elements of another collection.Remove elements: Use
pop!(array
) to remove the last element (and return it). There’s alsodeleteat!(array, index)
to remove the element at a specific index.
Element-wise Operations#
In Julia, addition and subtraction between same-shaped arrays always work element-wise:
# Add two vectors
u = [1, 2, 3]
v = [10, 10, 10]
u + v
3-element Vector{Int64}:
11
12
13
# Subtract two matrices
M = [1 2
3 4]
N = [11 12
13 14]
M - N
2×2 Matrix{Int64}:
-10 -10
-10 -10
Multiplication, division and exponentiation invoke Julia’s linear algebra routines, so *
on two matrices (or on a matrix and a vector) does true matrix multiplication, \
solves linear systems, and so on. In particular, these operations are not defined for all arrays (e.g. 3D arrays, or matrices/vectors with the wrong shape, etc.)
However, if you prefix the operator with a dot (e.g., .*
, ./
, .^
), you instead get an element-wise operation:
M = [2 2
2 2]
N = [5 6
7 8]
# Matrix multiplication
println(M * N)
# Element-wise multiplication
println(M .* N)
[24 28; 24 28]
[10 12; 14 16]
In short, for element-wise operations on arrays, use +
and -
directly and use the dotted forms of *
, /
, ^
, etc.
A neat feature of Julia is that the same “dot” notation also works with any function: placing a dot before the parentheses in the function call tells Julia to broadcast that function over every element of an array (or a combination of arrays). For example:
angles = [0, π/4, π/2, π]
# Compute sin of each angle in x
sin.(angles)
4-element Vector{Float64}:
0.0
0.7071067811865475
1.0
1.2246467991473532e-16
This can then be further extended to multiple function calls composed together:
# Compute exp(sin(x) + 1) for each x in angles
exp.(sin.(angles) .+ 1)
4-element Vector{Float64}:
2.718281828459045
5.512988100637874
7.38905609893065
2.718281828459046
This dot-notation makes element-wise array-wide computation both concise and expressive without a need for explicit loops.
Exercise: Operations on Arrays#
Given an array data = [5, 3, 8, 1, 2]
, do the following:
Sort the array (ascending) and print the sorted result.
Add a new number (e.g. 10) to the end of the array.
Compute the sum of all elements in the array.
Create a new array
squares
that is the element-wise squares ofdata
# Answer here
Exercise: Useful functions#
Take one of the functions we mentioned above (e.g. reshape
, findall
, sum
, etc.) and learn a bit about how it works. You can read the Julia documentation entry by prepending the function with ?
(e.g. ?reshape
) or you can do an online search. Play with the functions on some small arrays.
# Answer here
Final comments#
Julia provide a lot of functionality around arrays. Furthemore, there are 3rd party packages out there that implement more exotic kinds of arrays which are taylored for greater performance or particular use-cases, e.g. StaticArray.jl
. What we’ve covered here can help get you started using arrays, but it won’t be long before you want to do more sophisticated things with them. For more details, do an online search, or check out