# Data Structures

Every real program spends most of its time moving collections of things around. Julia gives you four you'll use constantly — arrays, tuples, dictionaries, and sets — plus one that's a quiet workhorse in data code, the named tuple. This chapter is about picking the right one and knowing what it costs.

## Arrays

The array is Julia's flagship data structure. It's the one the language was designed around, and it shows.

```{code-block} julia
balances = [1200, 800, 1500, 300]        # Vector{Int64}
readings = [20.5, 22.3, 19.8]            # Vector{Float64}
accounts = ["GL-4001", "GL-4002"]        # Vector{String}
```

`Vector{Int64}` is really an alias for `Array{Int64, 1}` — a one-dimensional array of `Int64`. The `1` is the number of dimensions, and it's part of the type. A matrix is `Array{Float64, 2}`, aliased as `Matrix{Float64}`.

### Building arrays

```{code-block} julia
zeros(5)                 # 5 Float64 zeros
zeros(Int, 5)            # 5 Int zeros
ones(3)                  # [1.0, 1.0, 1.0]
fill(-99.0, 4)           # [-99.0, -99.0, -99.0, -99.0]
rand(3)                  # 3 random Float64 in [0,1)
collect(1:5)             # [1, 2, 3, 4, 5] — materializes a range
similar(balances)        # uninitialized array of the same type and size
```

`similar` deserves a note: it's how generic code allocates an output array matching its input, without hardcoding the element type. You'll see it everywhere in library source.

### Indexing and slicing

```{code-block} julia
balances = [1200, 800, 1500, 300, 950]

balances[1]              # 1200   — remember, 1-indexed
balances[end]            # 950
balances[2:4]            # [800, 1500, 300]
balances[[1, 3, 5]]      # [1200, 1500, 950] — index with a vector
balances[balances .> 900]  # [1200, 1500, 950] — boolean mask
```

That last one is worth a pause. `balances .> 900` produces a vector of `Bool`, and indexing with a boolean vector selects the elements where it's `true`. That's the whole "filter rows matching a condition" operation, in one expression. The dot in `.>` is broadcasting, which gets its own chapter next.

:::{warning} Slicing copies
`balances[2:4]` allocates a **new array** and copies the data into it. For a five-element vector that's irrelevant; for a slice of a million-row dataset inside a loop, it's the difference between fast and unusable.

When you want to look at a slice without copying it, use a **view**:

```{code-block} julia
v = @view balances[2:4]     # no copy — a window onto the original
v[1] = 0                    # this modifies balances[2]!
```

`@views` (plural) applies the same transformation to every slice in a block of code. Reach for views when you're passing sub-arrays around in hot code. We'll return to this in the *Performance* chapter.
:::

### Growing and shrinking

```{code-block} julia
xs = [1, 2, 3]

push!(xs, 4)             # add to the end     → [1,2,3,4]
pushfirst!(xs, 0)        # add to the front   → [0,1,2,3,4]
pop!(xs)                 # remove & return last  → 4
popfirst!(xs)            # remove & return first → 0
append!(xs, [9, 10])     # extend with another collection
deleteat!(xs, 2)         # remove by index
empty!(xs)               # clear it
```

All mutating, all ending in `!`, exactly as promised.

### Matrices and higher dimensions

```{code-block} julia
A = [1 2 3
     4 5 6]              # 2×3 Matrix{Int64}

A[1, 2]                  # 2      — row 1, column 2
A[:, 2]                  # [2, 5] — all rows, column 2
A[1, :]                  # [1, 2, 3]
size(A)                  # (2, 3)
```

Inside `[ ]`, a **space** separates columns and a **newline** (or `;`) separates rows. This is MATLAB heritage, and it means you can type a matrix that looks like a matrix.

Julia arrays are **column-major**, meaning consecutive elements of a column sit next to each other in memory. That's the opposite of C and NumPy's default. The practical consequence: when you loop over a matrix, put the *column* index in the outer loop and the *row* index in the inner loop, so you walk memory in order.

```{code-block} julia
# Fast — walks memory contiguously
for j in axes(A, 2), i in axes(A, 1)
    A[i, j] *= 2
end
```

Getting this backwards can cost you several times the runtime on a large matrix, purely from cache misses. It's one of the few "which loop goes outside" rules genuinely worth memorizing.

### Linear algebra is right there

Because arrays are first-class, so is the math:

```{code-block} julia
using LinearAlgebra

A = [2.0 1.0; 1.0 3.0]
b = [5.0, 10.0]

A * b            # matrix-vector product
A'               # transpose (adjoint)
A \ b            # solve Ax = b  — this is the one to remember
inv(A)           # inverse (you almost never actually want this; use \)
det(A)           # determinant
```

`A \ b` solves the linear system. It doesn't compute an inverse and multiply — it picks an appropriate factorization based on the structure of `A`. That's a real algorithm-selection decision the language makes for you, and it's both faster and more numerically stable than `inv(A) * b`. If you take one thing from this section: use `\`.

## Tuples

A tuple is a **fixed-length, immutable** ordered collection whose elements can have different types:

```{code-block} julia
record = (1, "GL-4001", 1200.50)
record[2]                # "GL-4001"
typeof(record)           # Tuple{Int64, String, Float64}
```

Look at that type. The *length* and the *type of every position* are part of the type itself. That means the compiler knows everything about a tuple statically, so tuples are essentially free — often they live entirely in CPU registers with no heap allocation at all.

Tuples are how Julia functions return multiple values:

```{code-block} julia
function minmax_balance(xs)
    return minimum(xs), maximum(xs)     # this is a tuple
end

lo, hi = minmax_balance([1200, 800, 1500])    # destructuring
```

That destructuring assignment works on any iterable, which is why `for (i, x) in enumerate(xs)` reads so naturally.

**Use a tuple when** the collection is small, fixed-size, and heterogeneous. **Use an array when** it's homogeneous and might grow.

## Named tuples

A named tuple is a tuple whose slots have names. In practice, it's the lightest possible "record" type:

```{code-block} julia
entry = (account = "GL-4001", amount = 1200.50, posted = true)

entry.account            # "GL-4001"
entry.amount             # 1200.5
```

Named tuples are immutable and, like tuples, fully known to the compiler — so they're fast. I reach for them constantly for function returns that would otherwise be an unlabelled tuple you have to remember the order of. Compare:

```{code-block} julia
return n, total, mean          # caller has to know the order
return (count = n, total = total, mean = mean)   # self-documenting
```

They're also the row type of most Julia table packages, so if you go on to do dataframe work, you'll be seeing a lot of them.

## Dictionaries

A `Dict` maps keys to values, with fast lookup:

```{code-block} julia
ages = Dict("Amit" => 42, "Sam" => 28)

ages["Amit"]                     # 42
ages["Priya"] = 35               # insert
haskey(ages, "Sam")              # true
get(ages, "Nobody", 0)           # 0 — default instead of an error
delete!(ages, "Sam")

for (name, age) in ages
    println("$name is $age")
end
```

Two practical notes.

**Order is not guaranteed.** A `Dict` iterates in whatever order its internal hash table happens to use, and that order can change when the dictionary grows. If you need insertion order preserved, use `OrderedDict` from the `OrderedCollections` package.

**Give it concrete types when you can.** `Dict{String, Int}()` tells the compiler exactly what's inside. `Dict()` with no types produces a `Dict{Any, Any}`, which is slow for the same reason `Vector{Any}` is slow — every value is a pointer to somewhere else.

```{code-block} julia
counts = Dict{String, Int}()      # good
counts = Dict()                   # works, but Dict{Any,Any}
```

A pattern I use constantly, counting occurrences:

```{code-block} julia
function tally(items)
    counts = Dict{eltype(items), Int}()
    for item in items
        counts[item] = get(counts, item, 0) + 1
    end
    return counts
end

tally(["a", "b", "a", "c", "a"])   # Dict("a" => 3, "b" => 1, "c" => 1)
```

## Sets

A `Set` holds unique values and answers "is this in here?" fast:

```{code-block} julia
seen = Set([1, 2, 2, 3])         # Set([1, 2, 3]) — duplicate dropped
push!(seen, 4)
2 in seen                        # true
length(seen)                     # 4

union(Set([1,2]), Set([2,3]))       # Set([1,2,3])
intersect(Set([1,2]), Set([2,3]))   # Set([2])
setdiff(Set([1,2]), Set([2,3]))     # Set([1])
```

The reason to reach for a `Set` over an array isn't just uniqueness — it's the membership test. `x in some_array` scans the whole array (O(n)); `x in some_set` is a hash lookup (O(1)). If you find yourself repeatedly checking membership inside a loop, converting to a `Set` first can turn a quadratic algorithm into a linear one. That's a bigger win than any micro-optimization.

## Choosing the right one

| You need | Reach for |
|---|---|
| Many items, same type, might grow | `Vector{T}` |
| Numerical data, math operations | `Vector` / `Matrix` |
| A few items, different types, fixed | `Tuple` |
| A record with named fields, lightweight | `NamedTuple` |
| A record with named fields, reusable across your code | a `struct` (next chapter but one) |
| Lookup by key | `Dict{K,V}` |
| Uniqueness, or fast membership tests | `Set{T}` |

## Try it yourself

1. Given `amounts = [1200, 800, 1500, 300, 950]`, use a boolean mask to select only the values above 900, then sum them.
2. Build a `Dict` mapping account codes to balances, then print every account whose balance is negative.
3. Given two vectors of customer IDs, find the IDs present in both, and the IDs present in only the first.
4. Write a function that returns the count, min, and max of a vector as a *named* tuple.

### Solutions

```{code-block} julia
# 1.
amounts = [1200, 800, 1500, 300, 950]
sum(amounts[amounts .> 900])          # 3650

# 2.
ledger = Dict("GL-4001" => 1200.0, "GL-4002" => -350.0, "GL-4003" => 90.0)
for (acct, bal) in ledger
    bal < 0 && println("$acct is overdrawn: $bal")
end

# 3.
a = Set([101, 102, 103, 104])
b = Set([103, 104, 105])
intersect(a, b)     # Set([103, 104])
setdiff(a, b)       # Set([101, 102])

# 4.
stats(xs) = (count = length(xs), min = minimum(xs), max = maximum(xs))
stats([1200, 800, 1500])   # (count = 3, min = 800, max = 1500)
```

You now have the containers. Next we'll look at the syntax that makes working with them feel less like programming and more like writing equations: comprehensions and broadcasting.
