R Programming Quiz: Apply Family Functions
10 questions · exam conditions
0:00
Apply Family FunctionsQuestion 1 of 10

A list x contains nn numeric vectors, each of length mm. A programmer computes lapply(x, sum). Assume summing one vector takes time proportional to its length.

Which statement best describes this use of lapply() compared with an equivalent loop that preallocates its output?

Its time is O(n)O(n) because each call to the compiled sum() function has constant cost.
Its time is O(n+m)O(n+m) because lapply() vectorizes the work across all list elements.
Its time is O(nm)O(nm) and its result uses O(n)O(n) additional space, matching the loop asymptotically.
Its time is O(nm)O(nm), but it necessarily uses asymptotically less space than the preallocated loop.
← Back to quizzes

R Programming Quiz

R Programming Quiz: Apply Family Functions

Practice Apply Family Functions in R Programming with focused quiz questions that help you check what you know, review explanations, and build confidence with test-style prompts.

What this quiz covers

This quiz focuses on Apply Family Functions, giving you a quick way to practice the rules, question types, and explanations that matter most for R Programming.

How to use this quiz

Try each quiz question before looking at the correct answer. Use the explanations to review missed ideas, then come back to similar questions until the pattern feels familiar.

All questions

Question 1

A list x contains nn numeric vectors, each of length mm. A programmer computes lapply(x, sum). Assume summing one vector takes time proportional to its length.

Which statement best describes this use of lapply() compared with an equivalent loop that preallocates its output?

  1. Its time is O(n)O(n) because each call to the compiled sum() function has constant cost.
  2. Its time is O(n+m)O(n+m) because lapply() vectorizes the work across all list elements.
  3. Its time is O(nm)O(nm) and its result uses O(n)O(n) additional space, matching the loop asymptotically. (correct answer)
  4. Its time is O(nm)O(nm), but it necessarily uses asymptotically less space than the preallocated loop.
Explanation: When analyzing lapply() performance, think in two dimensions: how many elements are processed and how much work each element requires. Neither dimension can be ignored. Here, lapply(x, sum) calls sum() on each of the nn vectors, and each sum() must traverse all mm elements to accumulate a total. That gives n×mn \times m total operations, so the time complexity is O(nm)O(nm). For space, lapply() returns a new list of nn scalar results — one number per vector — which is O(n)O(n) additional memory. A preallocated loop does exactly the same thing: it iterates over nn vectors, sums mm elements each, and stores nn results. The two approaches are asymptotically identical in both time and space, making C correct. A is wrong because sum() being compiled doesn't make it O(1)O(1) — it still scales with the length of its input. A compiled function has a smaller constant, not a different complexity class. B is wrong because O(n+m)O(n+m) would imply each element is visited only once total across all vectors, which isn't possible when you have nn vectors each requiring mm steps. lapply() also does not vectorize across list elements in the parallel-computation sense — it's still sequential iteration. D is wrong because lapply() does not use less space than a preallocated loop; both store nn output values, so they match at O(n)O(n). A useful rule of thumb: lapply() is syntactically cleaner than an explicit loop but is not a performance shortcut — match their complexities carefully whenever an exam question contrasts the two.

Question 2

Suppose x is a named list and f(value, index) returns exactly one transformed object for each element. The following loop must be replaced without changing the argument order or the names of the output:

out <- vector("list", length(x))

for (i in seq_along(x)) out[[i]] <- f(x[[i]], i)

names(out) <- names(x)

Which apply-family expression is equivalent to the loop?

  1. setNames(lapply(x, function(value) f(value, seq_along(x))), names(x))
  2. setNames(Map(f, x, seq_along(x)), names(x)) (correct answer)
  3. setNames(lapply(seq_along(x), function(i) f(x, i)), names(x))
  4. setNames(lapply(seq_along(x), function(i) f(i, x[[i]])), names(x))
Explanation: When replacing a loop with an apply-family function, ask yourself two things: how many varying inputs does the function take, and does argument order matter? Here, f takes two arguments — the list element and its index — in that specific order. That immediately signals you need something that iterates over two parallel sequences simultaneously, not just one. Map(f, x, seq_along(x)) does exactly this. Map is R's vectorized multi-input mapper: it calls f(x[[1]], 1), f(x[[2]], 2), and so on, passing each element and its index in the correct order. Wrapping it in setNames(..., names(x)) restores the names, making option B a perfect match for the loop. Option A is flawed because inside the lapply callback, seq_along(x) returns the entire index vector, not a single index i — so f receives the wrong second argument entirely. Option C passes the whole list x as the first argument instead of x[[i]], meaning f receives the full list rather than the individual element at position i. Option D looks close but reverses the argument order, calling f(i, x[[i]]) when the loop clearly calls f(x[[i]], i) — a subtle but critical swap. A useful study pattern: whenever a function depends on both an element and its position, Map (or mapply) is your natural tool because it zips multiple vectors together. Using lapply alone forces awkward workarounds and creates traps like those in A, C, and D.

Question 3

Consider this grouped transformation:

x <- c(8, 2, 6, 4)

g <- factor(c("b", "a", "b", "a"), levels = c("a", "b"))

centered <- unsplit(lapply(split(x, g), function(v) v - mean(v)), g)

What is the value of centered?

  1. c(-1, 1, 1, -1), with centered groups concatenated in factor-level order.
  2. c(7, 3, 7, 3), with each observation replaced by its corresponding group mean.
  3. c(3, -3, 1, -1), with every value centered using the overall mean.
  4. c(1, -1, -1, 1), with centered values restored to original observation order. (correct answer)
Explanation: When you see split(), lapply(), and unsplit() chained together in R, the key question to ask yourself is: what order does each function work in, and does the final result preserve the original order? Here's what happens step by step. split(x, g) divides x by factor levels in level order — so group "a" gets values at positions 2 and 4 (i.e., c(2, 4)), and group "b" gets positions 1 and 3 (i.e., c(8, 6)). The lapply() then centers each group by subtracting its mean: group "a" has mean 3, yielding c(-1, 1); group "b" has mean 7, yielding c(1, -1). So far the centered values are computed correctly. The critical step is unsplit(), which restores values back to their original positions using the factor g. Position 1 belonged to "b" → gets 1; position 2 to "a" → gets -1; position 3 to "b" → gets -1; position 4 to "a" → gets 1. The result is c(1, -1, -1, 1), confirming D is correct. Choice A is tempting because it correctly computes the centered values but presents them in factor-level order (groups concatenated), ignoring what unsplit() actually does. Choice B confuses centering with replacing values by the group mean — v - mean(v) subtracts the mean, it doesn't return it. Choice C applies the overall mean (which would be 5) instead of group-specific means, a different operation entirely. Remember: unsplit() is the exact inverse of split() — it always maps values back to their original indices, not the split order.

Question 4

Consider the following expression:

mapply(function(x, y, k) x * y + k, x = 1:4, y = c(10, 20), MoreArgs = list(k = 1))

Which numeric vector is returned?

  1. c(11, 21, 31, 41), because only the first value of y is used.
  2. c(11, 41, 31, 81), because y is recycled and k remains fixed. (correct answer)
  3. c(11, 41), because iteration stops when the shorter varying argument ends.
  4. c(11, 31, 61, 81), because all pairings are formed before simplification.
Explanation: Whenever you see mapply() in R, focus on two distinct categories of arguments: varying arguments (passed by name, recycled like mapply's vector inputs) and fixed arguments (passed through MoreArgs, which stay constant across every call). Here, x = 1:4 gives four values, y = c(10, 20) gives two, and k = 1 is fixed via MoreArgs. Because x and y are varying arguments, R recycles the shorter one — y repeats as c(10, 20, 10, 20) — and applies the function element-wise across all four positions. The four calls become:
  • 1 * 10 + 1 = 11
  • 2 * 20 + 1 = 41
  • 3 * 10 + 1 = 31
  • 4 * 20 + 1 = 81
This produces c(11, 41, 31, 81), confirming B is correct. A is wrong because it assumes only the first element of y is ever used — mapply() cycles through all elements of shorter vectors, it doesn't freeze on one. C reflects a misunderstanding of recycling: iteration does not stop when the shorter argument runs out; the shorter vector wraps around to fill the longer one's length. D presents a plausible-sounding but invented result — no "pre-pairing" step exists in mapply(), and the arithmetic doesn't match any valid interpretation of the inputs. Your study tip: always distinguish MoreArgs from regular arguments in mapply(). Regular arguments recycle; MoreArgs values are scalar constants passed identically to every function call.

Question 5

What does the following expression return?

values <- c(5, 2, -1, 7)

group <- factor(c("b", "a", "b", NA), levels = c("a", "b", "c"))

tapply(values, group, sum)

  1. A grouped result with only a = 2 and b = 4, omitting c.
  2. A grouped result with a = 2, b = 4, and c = 7.
  3. A grouped result with a = 2, b = 11, and c = 0.
  4. A grouped result with a = 2, b = 4, and c = 0. (correct answer)
Explanation: When working with tapply(), the key things to track are: which values map to which group, what happens to NA indices, and how factor levels behave even when they have no observations. Here, values and group are paired by position. Position 1: 5 → "b", Position 2: 2 → "a", Position 3: -1 → "b", Position 4: 7 → NA. The factor was defined with three levels: "a", "b", and "c". tapply() applies sum across each level, so a = 2, b = 5 + (-1) = 4. Crucially, "c" has no values mapped to it, so sum() receives an empty vector — and sum(integer(0)) in R returns 0, not NA. The NA-indexed value (7) is silently dropped. This makes D correct: a = 2, b = 4, c = 0. A is wrong because tapply() does not omit unused factor levels — it includes all levels defined in the factor, which is exactly why factors are useful for representing groups with zero observations. B incorrectly assigns 7 to "c", but that value belongs to the NA index and is excluded entirely. C makes the same mistake of including the NA value, assigning it to "b" and getting 5 + (-1) + 7 = 11. As a study tip, remember this pattern: tapply() always produces a result for every level of the factor, empty groups return whatever the function returns on an empty input (for sum that's 0), and NA indices are always silently excluded.

Question 6

What is the structure of y after the following code runs?

x <- list(a = 1:2, b = 3:5, c = integer(0))

y <- sapply(x, function(v) v[v %% 2 == 1])

  1. A named list with elements a = 1, b = c(3, 5), and c = integer(0). (correct answer)
  2. A named numeric vector with values c(a = 1, b = 3, c = 5).
  3. A matrix with columns named a, b, and c, using missing values for shorter results.
  4. An unnamed list with elements 1, c(3, 5), and integer(0).
Explanation: When sapply() processes a list, it tries to simplify the results — but the key word is tries. The simplification only succeeds when all returned elements have the same length. Here, the filtering operation v[v %% 2 == 1] extracts odd numbers from each element: a = 1:2 yields 1 (length 1), b = 3:5 yields c(3, 5) (length 2), and c = integer(0) yields integer(0) (length 0). Because the lengths differ across elements, sapply() cannot collapse the results into a vector or matrix, so it falls back to returning a named list — making A the correct answer. B is wrong because a named numeric vector requires every element to have the same length (1). Since b returns two values and c returns zero, vector simplification is impossible. C describes what happens when all elements return the same length greater than one — sapply() would then bind results into a matrix with one column per list element. That doesn't apply here. D is wrong because sapply() always preserves the names from the input list (a, b, c), unlike lapply(), which also returns a named list but is being confused here with unnamed behavior. A useful rule of thumb: when you see sapply(), ask yourself "are all outputs the same length?" If yes, expect a vector or matrix. If no, expect a named list — essentially the same output as lapply(). This mental check will reliably guide you through sapply() simplification questions.

Question 7

Consider the following code:

m <- matrix(1:6, nrow = 2, byrow = TRUE)

apply(m, 1, function(v) c(first = v[1], total = sum(v)))

Which result is produced?

  1. A two-row matrix whose columns are c(first = 1, total = 6) and c(first = 4, total = 15). (correct answer)
  2. A two-row matrix whose columns are c(first = 1, total = 5) and c(first = 2, total = 7).
  3. A two-element list containing c(first = 1, total = 6) and c(first = 4, total = 15).
  4. A two-row matrix whose rows are c(first = 1, total = 6) and c(first = 4, total = 15).
Explanation: When you see apply() questions in R, focus on two things: what the function returns and how apply() assembles those returns into a final object. Here, m <- matrix(1:6, nrow = 2, byrow = TRUE) creates a 2×3 matrix filled row-by-row:
     [,1] [,2] [,3]
[1,]    1    2    3
[2,]    4    5    6
The call apply(m, 1, ...) applies the function across rows (margin = 1). Row 1 is c(1,2,3), so the function returns c(first = 1, total = 6). Row 2 is c(4,5,6), returning c(first = 4, total = 15). So far, so good — but now comes the subtle part: when your function returns a named vector, apply() binds the results column-by-column and uses those names as row names. The result is a 2-row matrix where each column corresponds to one row of the original matrix. That makes A correct. D is the most tempting trap — it describes the right values but claims they appear as rows, not columns. Remember, apply() transposes the output relative to what you might expect. B gets the structure right (2-row matrix) but uses wrong values — it seems to confuse column-wise indexing (v[1] returning the first column rather than the first row element) with row-wise. C is wrong because apply() simplifies equal-length vector returns into a matrix, not a list — you'd need lapply() or sapply(simplify = FALSE) to get a list. Your go-to reminder: apply(m, 1, f) produces a matrix where each original row becomes a column in the output — the result is always "transposed" from what feels natural.

Question 8

Consider this call to vapply():

x <- list(a = 1:4, b = numeric(0), c = 5:6)

vapply(x, function(z) if (length(z)) mean(z) else NA, numeric(1))

What happens?

  1. It returns the named numeric vector c(a = 2.5, b = NA, c = 5.5). (correct answer)
  2. It raises an error because the result for b is logical rather than numeric.
  3. It returns a named list because one element of x has length zero.
  4. It raises an error because mean() cannot accept the integer vectors in x.
Explanation: When you encounter a vapply() question, your first focus should be on the FUN.VALUE argument — it defines both the type and length that every function call must return. Here, numeric(1) means each result must be a length-1 numeric value. Walking through the list: for a = 1:4, length(z) is 4 (truthy), so mean(1:4) returns 2.5. For c = 5:6, mean(5:6) returns 5.5. The interesting case is b = numeric(0)length(z) is 0 (falsy), so the expression returns NA. Here's the key insight: bare NA in R is logical by type, but vapply() coerces it to numeric because NA is a universal missing-value constant that can inhabit any atomic type. Since the declared FUN.VALUE is numeric(1), R produces NA_real_ seamlessly. The final result is the named numeric vector c(a = 2.5, b = NA, c = 5.5), making A correct. B is the most tempting wrong answer — it assumes NA stays logical and triggers a type mismatch error. But vapply() is smart enough to coerce NA to match the declared numeric type without complaint. C is wrong because vapply() always returns a simplified array or vector, never a list — that's precisely what distinguishes it from sapply() in its flexible mode. D is wrong because mean() handles integer vectors just fine; integers are a subtype of numeric in R. A useful rule of thumb: NA is type-flexible in vapply() — it will conform to whatever atomic type you declared in FUN.VALUE.

Question 9

Given the following data frame, what does the expression return?

df <- data.frame(i = 1:3, d = c(1.5, 2.5, 3.5))

apply(df, 2, function(z) typeof(z))

  1. A named character vector with i = "integer" and d = "double".
  2. A named character vector with i = "double" and d = "double". (correct answer)
  3. A named character vector with i = "character" and d = "character".
  4. A named list with i = "integer" and d = "double".
Explanation: When you pass a data frame to apply(), R must first coerce it into a matrix — and this is the key insight the question is testing. A matrix in R can only hold one data type, so when df (which contains both integers and doubles) gets converted, R promotes everything to the most flexible type present. Since doubles are more general than integers, the entire matrix becomes double. This means that even though column i was originally created with 1:3 (which gives integers), by the time function(z) typeof(z) runs, z is a double vector for both columns. So apply(df, 2, function(z) typeof(z)) returns a named character vector c(i = "double", d = "double") — making B the correct answer. A is the trap most students fall into. It assumes apply() preserves the original column types, but it doesn't — the matrix coercion happens first, silently converting integers to doubles. C would be true if the data frame contained mixed types that forced coercion all the way to character (e.g., mixing numbers and strings), but that's not the case here. D is wrong on two counts: apply() returns a simplified vector when each call returns a length-1 result, not a list, and the types would be wrong anyway. A useful rule of thumb: treat apply() as matrix-first, always. If you need to preserve column types in a data frame, reach for lapply() or sapply() with df directly — those iterate over columns without the coercion step.

Question 10

What is the value of result after the following code runs?

rows <- lapply(1:2, function(i) { sapply(1:3, function(j) 10 * i + j) })

result <- do.call(rbind, rows)

  1. A 3 × 2 matrix with first row c(11, 21), second row c(12, 22), and third row c(13, 23).
  2. A 2 × 3 matrix with first row c(11, 13, 22) and second row c(12, 21, 23).
  3. A 2 × 3 matrix with first row c(11, 12, 13) and second row c(21, 22, 23). (correct answer)
  4. A 2 × 3 matrix with first row c(11, 21, 12) and second row c(22, 13, 23).
Explanation: When you see nested lapply/sapply calls combined with do.call(rbind, ...), trace the output of the inner function first, then understand how the pieces get assembled. The inner sapply(1:3, function(j) 10 * i + j) runs for each value of i and produces a numeric vector of length 3. When i = 1, you get c(11, 12, 13); when i = 2, you get c(21, 22, 23). The outer lapply collects these into a list of two vectors: list(c(11, 12, 13), c(21, 22, 23)). Finally, do.call(rbind, rows) binds those vectors as rows, stacking them vertically into a 2 × 3 matrix. The first row is c(11, 12, 13) and the second row is c(21, 22, 23) — making C correct. Choice A describes a 3 × 2 matrix, which would result from using cbind instead of rbind, or from transposing the result. Choice B has the correct dimensions but scrambles the values with no logical basis — this would never arise from this code. Choice D similarly has the right dimensions but presents values in a shuffled order that doesn't correspond to any systematic misreading of the code. A useful mental model: sapply over 1:3 always produces a length-3 vector, and rbind-ing two length-3 vectors always yields a 2 × 3 matrix (rows = number of vectors, columns = length of each vector). Keep this row-vs-column orientation locked in, as rbind/cbind confusion is one of the most common traps on R programming questions.