Introduction to Arrays
Contents
2.1. Introduction to Arrays¶
An array is a sequence of values or objects. The values can be of any type, but it is very common with arrays of numbers (integers or floating point numbers), in which case the array can be used to present vectors, matrices, and tensors. The values in an array are often referred to as elements.
One way to create a socalled 1d array (or a vector) is to list the elements, separated by commas, inside square brackets ([ ]
):
x = [1, 2, 4, 3]
4element Vector{Int64}:
1
2
4
3
typeof(x)
Vector{Int64} (alias for Array{Int64, 1})
In the output we can see that the x
variable has the type Vector{Int64}
, and the typeof
command reveals that this is actually just an alternative name for the more general array type Array{Int64,1}
. Julia identified based on the elements that this appears to be an array of integers, and uses the default integer type Int64
. The 1
in the Array
form refers to the fact that this is a 1d array (that is, a vector).
y = [1.1, π, 4/3, 1e1]
4element Vector{Float64}:
1.1
3.141592653589793
1.3333333333333333
0.1
The y
variable is also a vector, but since it contains floating point numbers Julia uses the default type Float64
for all of the elements.
You can also create an array with elements of a specified type using the following syntax:
z = Float64[1, 2, 4, 3] # Without the `Float64`, Julia would make this an integer array
4element Vector{Float64}:
1.0
2.0
4.0
3.0
Arrays can also be created using builtin functions, such as the ones below where T
indicates the type of the elements and defaults to Float64
if omitted:
Function 
Description 


a vector of all zeros 

a vector of all ones 

a 

a 

range of 
Some other useful functions include the push!
function, which adds a new element to the end of an array:
push!(z, 10)
5element Vector{Float64}:
1.0
2.0
4.0
3.0
10.0
Note the exclamation mark !
at the end of the function name, it indicates that this function modifies some of its arguments (in this case the array z
).
Similarly, the append!
function adds the elements of the second array to the end of the first:
append!(z, y)
9element Vector{Float64}:
1.0
2.0
4.0
3.0
10.0
1.1
3.141592653589793
1.3333333333333333
0.1
2.1.1. Accessing the elements of an array¶
We can access an element in an array using an index inside square brackets. Julia arrays are 1based, meaning that the indices start at 1
. The keyword end
can be used to refer to the last element of the array, and the function length(x)
returns the number of elements in the array x
.
y[3] # Access the 3rd element in the array y
1.3333333333333333
z[1] = z[end] * z[end1] # Set the 1st element in z to the product of the last two elements
z # To print the entire array
9element Vector{Float64}:
0.13333333333333333
2.0
4.0
3.0
10.0
1.1
3.141592653589793
1.3333333333333333
0.1
2.1.2. Traversing an array¶
A common operation is to traverse the elements of an array. This can be done using a for
loop with an index variable:
for i = 1:length(x)
println("Element ", i, " of the array x has the value ", x[i])
end
Element 1 of the array x has the value 1
Element 2 of the array x has the value 2
Element 3 of the array x has the value 4
Element 4 of the array x has the value 3
An alternative syntax uses the in
keyword, and does not need an index variable:
for element in x
println("This element of x has the value ", element)
end
This element of x has the value 1
This element of x has the value 2
This element of x has the value 4
This element of x has the value 3
2.1.3. Example: Sieve of Eratosthenes¶
The sieve of Eratosthenes is a simple, ancient algorithm for finding all prime numbers up to any given limit \(n\). From Wikipedia:
Create a list of consecutive integers from \(2\) through \(n\): \((2, 3, 4, ..., n)\).
Initially, let \(p\) equal \(2\), the smallest prime number.
Enumerate the multiples of \(p\) by counting in increments of \(p\) from \(2p\) to \(n\), and mark them in the list (these will be \(2p\), \(3p\), \(4p\), …; the \(p\) itself should not be marked).
Find the first number greater than \(p\) in the list that is not marked. If there was no such number, stop. Otherwise, let \(p\) now equal this new number (which is the next prime), and repeat from step 3.
When the algorithm terminates, the numbers remaining not marked in the list are all the primes below \(n\).
As a refinement, it is sufficient to mark the numbers in step 3 starting from \(p^2\), as all the smaller multiples of \(p\) will have already been marked at that point. This means that the algorithm is allowed to terminate in step 4 when \(p^2\) is greater than \(n\).
Wikipedia also provides an animation to illustrate the method:
An implementation of the algorithm is shown below. The only remaining new function we need for this is the floor
function, to find the largest integer such that \(p^2\le n\). This can be found using isqrt(n)
, which returns the largest integer less than or equal to \(\sqrt{n}\).
function SieveOfEratosthenes(n)
# Create a boolean array of length n, initialize all entries to as true
# After the algorithm finishes, prime[i] will be true is i≥2 is a prime
prime = trues(n)
for p = 2:isqrt(n)
if prime[p]
for i = p^2:p:n
prime[i] = false
end
end
end
# Return an array with all prime numbers
primes = Int64[]
for i = 2:n
if prime[i]
push!(primes, i)
end
end
primes
end
SieveOfEratosthenes (generic function with 1 method)
println(SieveOfEratosthenes(100)) # All prime numbers up to 100
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
println(SieveOfEratosthenes(1000000)[end]) # The largest prime number less than 1,000,000
999983
2.1.4. The dotsyntax: Vectorized operators and functions¶
Julia provides a convenient dotsyntax to perform elementbyelement operations on an array.
For every binary operator, e.g.
*
, there is a corresponding dot operator.*
which is automatically defined to perform the*
operation elementbyelement.Any function
f
can be applied elementwise to an array by using the automatically defined functionf.
These dotoperators are illustrated in the examples below.
x = [1, 3, 5]
y = [0.2, 4.0 , 3.1]
z = x .* y # Elementbyelement multiplication
3element Vector{Float64}:
0.2
12.0
15.5
sqrt.(x) # Elementbyelement square roots
3element Vector{Float64}:
1.0
1.7320508075688772
2.23606797749979
w = x .+ sqrt.(y .^ x) # More complex mathematical expression, elementbyelement
3element Vector{Float64}:
1.4472135954999579
11.0
21.920151004054308
For elementwise complex expressions such as in the last example, Julia provides a @.
syntax that makes all operators in the expression be applied elementwise:
@. x + sqrt(y^x) # Same as before
3element Vector{Float64}:
1.4472135954999579
11.0
21.920151004054308
When using the dotoperator on binary operations, the two arrays much have the same number of elements:
push!(y, 7)
x .* y # Error: arrays must have same number of elements
DimensionMismatch("arrays could not be broadcast to a common size; got a dimension with lengths 3 and 4")
Stacktrace:
[1] _bcs1
@ ./broadcast.jl:501 [inlined]
[2] _bcs
@ ./broadcast.jl:495 [inlined]
[3] broadcast_shape
@ ./broadcast.jl:489 [inlined]
[4] combine_axes
@ ./broadcast.jl:484 [inlined]
[5] instantiate
@ ./broadcast.jl:266 [inlined]
[6] materialize(bc::Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1}, Nothing, typeof(*), Tuple{Vector{Int64}, Vector{Float64}}})
@ Base.Broadcast ./broadcast.jl:883
[7] toplevel scope
@ In[18]:2
[8] eval
@ ./boot.jl:360 [inlined]
[9] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
@ Base ./loading.jl:1116
However, if one of the arguments to the operator is a scalar, it is interpreted as a constant vector of the correct size:
x .* 3
3element Vector{Int64}:
3
9
15
The dotsyntax also works as expected for numerical literal coefficients:
3x
3element Vector{Int64}:
3
9
15
Note that the dotsyntax is automatically defined even for new functions:
function pick_largest(x, y)
if x > y
return x
else
return y
end
end
pick_largest (generic function with 1 method)
println("z = ", z)
println("w = ", w)
println("elementwise largest = ", pick_largest.(z,w))
z = [0.2, 12.0, 15.5]
w = [1.4472135954999579, 11.0, 21.920151004054308]
elementwise largest = [1.4472135954999579, 12.0, 21.920151004054308]
2.1.5. Array slices¶
An array can be sliced, which means extracting a subset of the original array. This subset can be expressed as a range of indices, similar to the forloop syntax, or more generally it can be any vector of integer indices.
println(y) # Original vector
println(y[1:3]) # First 3 elements
println(y[1:2:4]) # All oddnumbered elements
println(y[end:1:2]) # From end back to second element in reverse
println(y[4:3]) # Empty subset
println(y[:]) # All elements (same as original vector)
println(y[[4,2,4,3,3]]) # Index by vector  pick elements 4,2,4,3,3
[0.2, 4.0, 3.1, 7.0]
[0.2, 4.0, 3.1]
[0.2, 3.1]
[7.0, 3.1, 4.0]
Float64[]
[0.2, 4.0, 3.1, 7.0]
[7.0, 4.0, 7.0, 3.1, 3.1]
These socalled ranges can also be used to define the array itself. For example:
x = 1:8
will define an object that can be used as an array. If needed, the matrix can be explicitly created using the collect
function:
x1 = collect(1:3)
3element Vector{Int64}:
1
2
3
x2 = collect(10:2.5:3)
6element Vector{Float64}:
10.0
7.5
5.0
2.5
0.0
2.5
2.1.6. Arrays are passed by sharing¶
When assigning arrays to new variables or passing them to functions, they still refer to the same array. This behavior is natural for performance reasons (or Julia would have to make a copy), but can be confusing for people used to e.g. MATLAB.
x = [1, 2, 3]
y = x
y[2] = 123
println("y = ", y)
println("x = ", x)
y = [1, 123, 3]
x = [1, 123, 3]
Note how the original array x
also changed when an entry of y
was modified. This is because the statement y = x
only created a shared reference to the same array. If you really want a new copy which is independent of the original array, use the copy
function:
z = copy(x)
z[3] = 53
println("z = ", z)
println("x = ", x)
z = [1, 123, 53]
x = [1, 123, 3]
The situation is the same when passing arrays to functions. For example:
function modify_scalar(x)
x = 111
return nothing
end
function modify_vector!(x)
x[:] .= 111
return nothing
end
x = 0
modify_scalar(x)
println("x = ", x) # Still 0  function does not modify a scalar
x = zeros(1,5)
modify_vector!(x)
println("x = ", x) # Function modifies the original vector
x = 0
x = [111.0 111.0 111.0 111.0 111.0]
Because of this behavior, Julia recommends using an exclamation mark at the end of functions that might modify any of its arguments as an alert (but it is not enforced).