Command Palette

Search for a command to run...

MTH 4300

Lecture 7

std::vector

std::vector is the standard library's resizable array. Unlike C-style arrays from Lecture 6, a vector grows automatically as you add elements — no manual new/delete required.

Including and declaring
#include <vector>

std::vector<int> nums;                   // empty vector
std::vector<int> primes = {2, 3, 5, 7}; // initialized with values
std::vector<int> zeros(5, 0);            // 5 elements, all 0

std::vector<T> is a template — you specify the element type inside <>. Any type works: std::vector<double>, std::vector<std::string>, even std::vector<std::vector<int>> for a 2D grid.

Adding and removing elements

push_back and pop_back
std::vector<int> v;
v.push_back(10); // v = [10]
v.push_back(20); // v = [10, 20]
v.push_back(30); // v = [10, 20, 30]
v.pop_back();    // v = [10, 20] — removes the last element

push_back appends to the end. pop_back removes the last element. Both run in amortized O(1).

Size and capacity

size and capacity
std::vector<int> v = {1, 2, 3, 4, 5};
std::cout << v.size() << "\n";     // 5 — number of elements
std::cout << v.capacity() << "\n"; // >= 5 — allocated storage (implementation-defined)

size() is how many elements you have. capacity() is how much space has been reserved. When size hits capacity, the vector reallocates to a larger block (typically doubling). You almost always only care about size().

Element access

Accessing elements
std::vector<int> v = {10, 20, 30};

std::cout << v[1] << "\n";      // 20 — no bounds check
std::cout << v.at(2) << "\n";   // 30 — bounds-checked (throws std::out_of_range)
std::cout << v.front() << "\n"; // 10 — first element
std::cout << v.back() << "\n";  // 30 — last element

Like C-style arrays, v[i] performs no bounds check. Out-of-range access is undefined behavior. Use v.at(i) when you want a guaranteed exception instead of silent corruption.

Iterators

An iterator is an object that points into a container and supports the same operations as a pointer: dereference (*), increment (++), and comparison (==, !=). Every standard container provides begin() and end():

begin and end
std::vector<int> v = {10, 20, 30};

auto it  = v.begin(); // points to the first element
auto end = v.end();   // points one past the last element

v.end() is a sentinel — it points past the last valid element, just like ptr + n sits past the end of a C-style array.

Iterating with an iterator
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {10, 20, 30};

    for (auto it = v.begin(); it != v.end(); ++it) {
        std::cout << *it << " "; // 10 20 30
    }
    std::cout << "\n";

    return 0;
}

The type of it is std::vector<int>::iterator, which is verbose. auto is idiomatic here.

Range-for under the hood

The range-for loop you've already seen is sugar for iterator-based traversal:

Range-for is iterator traversal
// these two loops are equivalent
for (int x : v) {
    std::cout << x << "\n";
}

for (auto it = v.begin(); it != v.end(); ++it) {
    int x = *it;
    std::cout << x << "\n";
}

Use range-for when you don't need the iterator position explicitly.

Lambdas

A lambda is an anonymous function you write inline. The syntax is:

Lambda syntax
[capture](parameters) { body }
Basic lambda
auto square = [](int x) { return x * x; };
std::cout << square(5) << "\n"; // 25

Lambdas are most useful as arguments to <algorithm> functions that accept a predicate or comparator. Instead of defining a named function somewhere else, you write the logic right where it's used.

Captures

The [] is the capture list — it controls which variables from the surrounding scope the lambda can see.

Capturing by value and by reference
int threshold = 10;

// capture by value: lambda gets its own copy of threshold
auto above_val = [threshold](int x) { return x > threshold; };

// capture by reference: lambda sees the original variable
auto above_ref = [&threshold](int x) { return x > threshold; };

threshold = 20;
std::cout << above_val(15) << "\n"; // 1 (true) — still compares against 10
std::cout << above_ref(15) << "\n"; // 0 (false) — sees threshold = 20

Common shorthand: [=] captures all locals by value, [&] captures all by reference.

Capture shorthand
int a = 2, b = 3;

auto add_both = [=](int x) { return x + a + b; }; // captures a and b by value
std::cout << add_both(10) << "\n"; // 15

Capturing by reference ([&]) is safe when the lambda doesn't outlive the enclosing scope. If you store a lambda and the captured variable goes out of scope, the reference dangles — same problem as any other dangling reference.

The <algorithm> Header

<algorithm> provides a large library of operations that work on iterator ranges. The pattern is always the same: pass begin and end and the function does the work over that range.

#include <algorithm>

Sorting

std::sort
std::vector<int> v = {5, 1, 4, 2, 3};
std::sort(v.begin(), v.end()); // ascending by default
// v = [1, 2, 3, 4, 5]

std::sort(v.begin(), v.end(), std::greater<int>()); // descending
// v = [5, 4, 3, 2, 1]

// custom comparator via lambda
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; }); // also descending

std::sort runs in O(n log n). The optional third argument is a comparator — any callable that returns true if the first argument should sort before the second.

Reversing

std::reverse
std::vector<int> v = {1, 2, 3, 4, 5};
std::reverse(v.begin(), v.end());
// v = [5, 4, 3, 2, 1]

Searching

std::find
std::vector<int> v = {10, 20, 30, 40};
auto it = std::find(v.begin(), v.end(), 30);

if (it != v.end()) {
    std::cout << "Found: " << *it << "\n"; // Found: 30
} else {
    std::cout << "Not found\n";
}

std::find returns an iterator to the first match, or v.end() if not found. Always check against v.end() before dereferencing.

std::find_if takes a predicate instead of a value — this is where lambdas shine:

std::find_if
std::vector<int> v = {10, 20, 30, 40};
auto it = std::find_if(v.begin(), v.end(), [](int x) { return x > 25; });
// it points to 30 — the first element greater than 25

Counting

std::count and std::count_if
std::vector<int> v = {1, 2, 2, 3, 2, 4};

int twos  = std::count(v.begin(), v.end(), 2);                                   // 3
int evens = std::count_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; }); // 4

Min and max

std::min_element and std::max_element
std::vector<int> v = {3, 1, 4, 1, 5, 9};

auto min_it = std::min_element(v.begin(), v.end());
auto max_it = std::max_element(v.begin(), v.end());

std::cout << *min_it << "\n"; // 1
std::cout << *max_it << "\n"; // 9

Both return iterators, not values — dereference to get the value.

Filling

std::fill
std::vector<int> v(5);
std::fill(v.begin(), v.end(), 7);
// v = [7, 7, 7, 7, 7]

Replacing

std::replace
std::vector<int> v = {1, 2, 3, 2, 4};
std::replace(v.begin(), v.end(), 2, 99);
// v = [1, 99, 3, 99, 4]

Predicates: any, all, none

std::any_of, std::all_of, std::none_of
std::vector<int> v = {2, 4, 6, 7, 8};

bool any_odd  = std::any_of(v.begin(), v.end(),  [](int x) { return x % 2 != 0; }); // true
bool all_even = std::all_of(v.begin(), v.end(),  [](int x) { return x % 2 == 0; }); // false
bool none_neg = std::none_of(v.begin(), v.end(), [](int x) { return x < 0; });       // true

Accumulate (from <numeric>)

std::accumulate
#include <numeric>

std::vector<int> v = {1, 2, 3, 4, 5};
int sum = std::accumulate(v.begin(), v.end(), 0); // 15 — initial value is 0

std::accumulate lives in <numeric>, not <algorithm>. It folds the range with + by default but accepts a custom binary operation — a lambda — as a fourth argument.

Exercises

Q1.

Without running the code, predict what this program prints. Then verify.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3};

    v.push_back(4);
    v.pop_back();
    v.push_back(10);

    std::cout << v.size() << "\n";
    std::cout << v.front() << "\n";
    std::cout << v.back() << "\n";
    std::cout << v[1] << "\n";

    return 0;
}

Q2.

Without running the code, predict what this program prints.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {5, 10, 15, 20};

    auto it = v.begin();
    std::cout << *it << "\n";
    ++it;
    std::cout << *it << "\n";
    it += 2;
    std::cout << *it << "\n";
    --it;
    std::cout << *it << "\n";

    return 0;
}

Q3.

Without running the code, predict what this program prints.

#include <iostream>

int main() {
    int limit = 5;

    auto under = [limit](int x) { return x < limit; };
    auto over  = [&limit](int x) { return x > limit; };

    limit = 10;

    std::cout << under(7) << "\n";  // line A
    std::cout << over(7) << "\n";   // line B
    std::cout << under(3) << "\n";  // line C
    std::cout << over(15) << "\n";  // line D

    return 0;
}

Q4.

Trace through this program step by step. At each checkpoint, write the contents of v.

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> v = {3, 1, 4, 1, 5, 9, 2};

    std::sort(v.begin(), v.end());
    // CHECKPOINT A

    std::reverse(v.begin(), v.end());
    // CHECKPOINT B

    std::replace(v.begin(), v.end(), 1, 99);
    // CHECKPOINT C

    return 0;
}

Q5.

Each snippet has a bug. Identify what's wrong and what happens at runtime.

// Snippet A
std::vector<int> v = {1, 2, 3};
std::cout << v[5] << "\n";

// Snippet B
std::vector<int> v2 = {10, 20, 30};
auto it = std::find(v2.begin(), v2.end(), 99);
std::cout << *it << "\n";

// Snippet C
std::vector<int> v3;
std::fill(v3.begin(), v3.end(), 5);
std::cout << v3.size() << "\n";

// Snippet D
std::vector<int> v4 = {4, 2, 5, 1};
std::sort(v4.end(), v4.begin());

Q6.

Write a function above_average(const std::vector<int>& v) that returns a new vector containing only the elements strictly greater than the average of all elements. Use std::accumulate to compute the sum.

#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>

std::vector<int> above_average(const std::vector<int>& v) {
    // TODO
}

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::vector<int> result = above_average(v);

    for (int x : result) {
        std::cout << x << " ";
    }
    std::cout << "\n"; // 6 7 8 9 10

    return 0;
}

Q7.

Write a function top_k(std::vector<int> v, int k) that returns a vector of the k largest elements in descending order. If k exceeds the vector size, return all elements sorted descending. Take v by value so the caller's copy is not modified.

#include <iostream>
#include <vector>
#include <algorithm>

std::vector<int> top_k(std::vector<int> v, int k) {
    // TODO
}

int main() {
    std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
    std::vector<int> result = top_k(v, 3);

    for (int x : result) {
        std::cout << x << " ";
    }
    std::cout << "\n"; // 9 6 5

    return 0;
}

Practice Problems