Skip to content

05 · Vectors & Basic Data Structures

Creating vectors

nums <- c(10, 20, 30, 40, 50)
nums
# [1] 10 20 30 40 50

seq_vec <- 1:10                  # sequence shorthand
seq_vec
# [1] 1 2 3 4 5 6 7 8 9 10

by_step <- seq(0, 100, by = 25)  # explicit step
by_step
# [1] 0 25 50 75 100

repeated <- rep("x", times = 4)
repeated
# [1] "x" "x" "x" "x"

Indexing — 1-based, not 0-based

R vectors are indexed starting at 1, not 0 — a frequent source of off-by-one bugs for people coming from Python, JavaScript, or C.

fruits <- c("apple", "banana", "cherry", "date")

fruits[1]        # "apple"  -- the FIRST element, not the second
# [1] "apple"
fruits[length(fruits)]   # "date" -- the last element
# [1] "date"

fruits[2:3]      # slice: elements 2 through 3
# [1] "banana" "cherry"

fruits[-1]       # negative index: everything EXCEPT element 1
# [1] "banana" "cherry" "date"

fruits[c(1, 3)]  # select specific, non-contiguous indices
# [1] "apple"  "cherry"

Logical (boolean) indexing

Indexing with a logical vector of the same length keeps only the elements where the condition is TRUE — this is the foundation of filtering data later on:

ages <- c(15, 22, 8, 35, 61, 19)

ages[ages >= 18]
# [1] 22 35 61 19

adults_only <- ages >= 18
adults_only
# [1] FALSE  TRUE FALSE  TRUE  TRUE  TRUE
ages[adults_only]
# [1] 22 35 61 19

Named vectors

prices <- c(apple = 0.5, banana = 0.3, cherry = 2.0)
prices
#  apple banana cherry
#    0.5    0.3    2.0

prices["banana"]
# banana
#    0.3

names(prices)
# [1] "apple"  "banana" "cherry"

Vectorized arithmetic

Operations on vectors apply element-wise, without an explicit loop:

a <- c(1, 2, 3)
b <- c(10, 20, 30)

a + b
# [1] 11 22 33
a * 2
# [1] 2 4 6

# Recycling: the shorter vector repeats to match the longer one's length
c(1, 2, 3, 4) + c(1, 0)
# [1] 2 2 4 4

Recycling can hide bugs

If the longer vector's length isn't an exact multiple of the shorter one's, R still recycles but issues a warning. Silent recycling on mismatched lengths is a common source of subtle data bugs — check length() on both sides if a vectorized result looks wrong.

Useful vector functions

nums <- c(5, 3, 8, 1, 9, 3)

length(nums)     # 6
sum(nums)        # 29
mean(nums)       # 4.833333
max(nums)        # 9
min(nums)        # 1
sort(nums)       # 1 3 3 5 8 9
rev(nums)        # 3 9 1 8 3 5  -- reversed order
unique(nums)     # 5 3 8 1 9    -- duplicates removed
which(nums > 4)  # 1 3 5 -- INDICES where the condition holds, not values

Lists — mixed-type containers

Unlike a vector (single type), a list can hold elements of different types, including other lists or vectors — R's general-purpose container:

person <- list(name = "Alice", age = 30, scores = c(90, 85, 88))

person$name
# [1] "Alice"
person$scores
# [1] 90 85 88
person[["age"]]     # equivalent to person$age
# [1] 30

$ and [["..."]] both extract a named element; $ is more concise but only works with literal names (not a variable holding a name), while [["..."]] works with either.

field <- "age"
person[[field]]     # works -- [[ ]] accepts a variable
# [1] 30
# person$field     # would NOT work -- this looks for a literal field called "field"

[ ] vs. [[ ]] on lists

A crucial distinction: [ ] on a list returns a sub-list (still a list), while [[ ]] returns the actual element inside:

person["age"]
# $age
# [1] 30
class(person["age"])
# [1] "list"

person[["age"]]
# [1] 30
class(person[["age"]])
# [1] "numeric"

Vector and list cheat sheet

Task Syntax
Create a vector c(1, 2, 3)
Sequence 1:10 or seq(0, 100, by = 25)
First element x[1] (1-based!)
Last element x[length(x)]
Filter by condition x[x > 10]
Named access x["name"]
Create a list list(a = 1, b = "two")
Get list element (unwrapped) lst[["a"]] or lst$a
Get list element (still a list) lst["a"]

How It Actually Works

An R vector is a contiguous block of memory holding elements of one type, plus a header carrying the length and, for named/typed data, an attributes list (that's how a plain "atomic vector" becomes a factor or gains names — attributes decorate the same underlying memory rather than wrapping it in a new structure). Because storage is contiguous, x[3] is O(1) pointer arithmetic, and arithmetic like x * 2 is a tight C loop that walks the array once — no per-element interpreter overhead, which is the real reason vectors outperform loops.

Recycling — R silently repeating the shorter vector when you combine two of different lengths (c(1,2,3,4) + c(1,2)c(2,4,4,6)) — isn't a convenience feature layered on top; it's baked into the C arithmetic routines themselves: they compute the output length as the max of the two input lengths and index into each input modulo its own length as they loop. A list(), by contrast, doesn't store elements contiguously by value — it's a vector of pointers to arbitrary SEXPs, which is exactly why a list can mix types and hold other vectors/lists without coercion.

🔀 See this in another language

Exercise

Create a named vector inventory mapping three item names to their integer counts (e.g. c(apples = 10, bananas = 0, cherries = 25)). Use logical indexing to print only the items with a count greater than 0. Then create a list product <- list(name = "Widget", price = 9.99, in_stock = TRUE) and print each field using both $ and [[ ]] syntax to confirm they give the same result.