Start learning free

RUST · BIG O NOTATION

Time Complexity Practice 2 in Rust, explained

Two operations that look cheap

Taking a piece of a collection and joining two collections are everyday operations, and their cost depends on one question: does the operation copy the data, or does it only point at it? In Rust the answer is visible in the types, which makes the complexity easy to read once you know the rule.

Slicing

A slice expression such as &v[2..5] does not create a new vector. It creates a borrowed view, a pointer into the original data plus a length. Building that view takes constant time, whatever the size of the vector and the length of the range.

let v: Vec<i32> = (0..10).collect();
let part = &v[2..5];
println!("{:?}", part);
[2, 3, 4]

The pointer of part is exactly the address of the element at index 2 in v: no element was copied. The same holds for strings: with s = String::from("hello world"), the expression &s[6..] is a &str holding world, and it points inside s. So passing slices to functions is O(1), and recursive algorithms that work on &v[1..] do not pay a copying cost at each call.

Copying happens only when you ask for it:

  • part.to_vec() allocates a new Vec and copies the elements: O(k) for k elements.
  • w.to_string() or String::from(w) on a &str copies the bytes: O(k).
  • v.clone() copies the whole vector: O(n).

Anything you then do with the slice also costs time in proportion to its length. Summing &v[a..b] takes O(b - a), even though creating the slice was free.

Slicing can panic

An out-of-range slice such as &v[2..20] on a 10-element vector panics. For strings there is a second rule: the range is in bytes, and both ends must fall on character boundaries. In "é", which takes two bytes, position 1 is in the middle of the character, so is_char_boundary(1) is false and &s[..1] would panic. When the text may contain non-ASCII characters, work with chars() or collect them into a Vec<char>.

Concatenation

Joining two sequences has to produce a sequence holding both, so its cost is proportional to the amount of data written.

  • a + &b on strings consumes a and appends the bytes of b to it. The cost is O(len of b) amortized, because a's buffer is reused. After let c = a + &b; with a = "ab" and b = "cd", c is "abcd", b is still usable, and a has been moved and can no longer be used.
  • s.push_str(t) is the same append without a new variable, and s.push(c) appends one character in O(1) amortized.
  • format!("{}{}", a, b) builds a new string and copies both parts: O(len a + len b).
  • v.extend_from_slice(&w) appends a slice to a vector in O(len of w) amortized; vec_of_vecs.concat() allocates a new vector and copies every element once. For [[1, 2], [3]] it produces [1, 2, 3].

Building a result in a loop

The loop below appends the text of the numbers 0 to 4 and ends with "01234":

let mut out = String::new();
for i in 0..5 {
    out.push_str(&i.to_string());
}

Appending to one growing String is linear overall: the total work is proportional to the total number of bytes written. The pattern to avoid is creating a fresh string on every iteration that copies the whole result so far, for example out = format!("{}{}", out, i). The k-th iteration then copies about k characters, and the total is 1 + 2 + ... + n, which is O(n²). The same trap exists for vectors: result = [result, piece].concat() inside a loop is quadratic, while result.extend_from_slice(&piece) is linear overall.

Summary table

OperationTime
&v[a..b], &s[a..b]O(1)
v[a..b].to_vec(), s[a..b].to_string()O(b - a)
s.push(c)O(1) amortized
s.push_str(t), a + &bO(len t), O(len b) amortized
format!("{}{}", a, b)O(len a + len b)
v.extend_from_slice(&w)O(len w) amortized
[x, y].concat()O(total length)

Common mistakes

  • Believing that a slice is a copy, and therefore avoiding slices in recursion. The slice itself is free; the copy appears only with to_vec(), to_string() or clone().
  • Calling to_vec() or to_string() on a slice inside a loop "just to be safe", which turns a linear algorithm into a quadratic one.
  • Slicing strings with byte positions that split a multi-byte character.
  • Rebuilding a string or vector from scratch on every loop iteration instead of appending to it.

Key takeaways

  • A slice is a view: creating it is O(1), and copying it out costs time proportional to its length.
  • Concatenation costs time proportional to the data appended when you reuse the buffer, and to everything copied when you build a new value.
  • Append to one growing String or Vec rather than rebuilding it in a loop.
Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
Start learning free