Data Structures and Algorithms Crash Course

Data Structures

Computer Components

References vs Primitives

Memory Size of Types

Memory Alignment

Strings are special

Stack and Heap

Stack

Heap

How it helps in coding?

References vs Values (Mutation problem)

Why Data Structures?

Matching structure to problem

At scale, performance determines if software is usable

Algorithmic Complexity

Counting operations

Constant vs Linear Time

Quadratic Time

Avoiding Quadratic Time

Best, worst, and average case

Space Complexity:

Space-Time trade offs

When space matters:

Arrays:

Iteration

Adding/Removing elements at the end

Adding/Removing from Middle

Dynamic Arrays

Strings

Indexing

Iteration

Concactenation

Comparison

String Methods

Immutability

Character Counting

Linked List

Nodes and Pointers

struct Node {
    int data;
    Node* next;

    Node(int data) : data(data), next(nullptr) {}
};

// Create a node with data = 5
Node* node = new Node(5);
cout << node->data << endl;  // 5
cout << node->next << endl;  // 0 (nullptr)

// Create two nodes
Node* first = new Node(10);
Node* second = new Node(20);

// Link first to second
first->next = second;

// Now: first -> second -> nullptr
cout << first->data << endl;         // 10
cout << first->next->data << endl;   // 20
cout << second->next << endl;        // 0 (nullptr)

Traverse O(N)

void traverse(Node* head) {
    Node* current = head;
    while (current != nullptr) {
        cout << current->data << endl;
        current = current->next;
    }
}

Insert

Deletion

Linked List vs Array

Queues

FIFO Model

Queue Operations

std::queue<std::string> q; q.push(“Task 1”); // Enqueue first task q.push(“Task 2”); // Enqueue second task q.push(“Task 3”); // Enqueue third task // Queue: [Task 1, Task 2, Task 3]


**Dequeue:** remove (and return) from the front

```cpp
// Assume queue has Task 1, Task 2, Task 3
std::string first = q.front();   // Get front
q.pop();                         // Remove it
std::cout << first << std::endl; // "Task 1"
// Queue now: [Task 2, Task 3]

std::string nextTask = q.front();
q.pop();
std::cout << nextTask << std::endl; // "Task 2"
// Queue now: [Task 3]

Peek: look at the front without removing it

// Assume queue has First, Second, Third
if (!q.empty()) {
    std::string frontItem = q.front();
    std::cout << frontItem << std::endl;   // "First"
}
// Queue unchanged

Size and empty check

// Assume queue has A, B, C
int size = q.size();
std::cout << "Queue has " << size << " items" << std::endl;

bool isEmpty = q.empty();
std::cout << isEmpty << std::endl;   // false

Queue w/ Array

Real World Apps

Hashmaps

Hash Functions

// hash func
std::hash<std::string> str_hash;
size_t hval = str_hash("Alice");

// unordered map uses hash func automatically
std::unordered_map<std::string, int> map;
map["Alice"] = 30;

Collision Handling

Chaining

Full table

Hash to Array Index

\(index = hashcode \% capacity\)

Complete lookup process

Common Ops:

#include <unordered_map>
#include <string>
#include <iostream>

// Create empty hash table
std::unordered_map<std::string, std::string> phoneBook;

// Insert key-value pairs
phoneBook["Alice"] = "555-1234";
phoneBook["Bob"] = "555-5678";
phoneBook["Carol"] = "555-9012";

// Lookup by key
std::cout << phoneBook["Alice"] << std::endl;  // "555-1234"

// Check if key exists
if (phoneBook.find("Bob") != phoneBook.end()) {
    std::cout << "Found Bob" << std::endl;
}

// Delete a key
phoneBook.erase("Carol");

Iteration: does not gurantee order (internal array order)

std::unordered_map<std::string, std::string> phoneBook = {
    {"Alice", "555-1234"},
    {"Bob", "555-5678"}
};

// Iterate over key-value pairs
for (const auto& [name, number] : phoneBook) {
    std::cout << name << ": " << number << std::endl;
}

Sets - Uniqueness

std::unordered_set<int> numbers;
numbers.insert(1);
numbers.erase(2);
numbers.find(2);

#include <unordered_set>
#include <algorithm>

std::unordered_set<int> a = {1, 2, 3};
std::unordered_set<int> b = {2, 3, 4};

// Union
std::unordered_set<int> u = a;
u.insert(b.begin(), b.end());  // {1, 2, 3, 4}

// Intersection (manual)
std::unordered_set<int> intersect;
for (int x : a) {
    if (b.find(x) != b.end()) {
        intersect.insert(x);  // {2, 3}
    }
}

// Difference (manual)
std::unordered_set<int> diff;
for (int x : a) {
    if (b.find(x) == b.end()) {
        diff.insert(x);  // {1}
    }
}

Stack (LIFO Model)

#include <stack>

// Using C++ stack
std::stack<int> stack;

// Push elements
stack.push(10);
stack.push(20);
stack.push(30);
// Stack: [10, 20, 30] (30 is on top)

// Pop element
int top = stack.top();  // Get top value (30)
stack.pop();            // Remove it
// Stack: [10, 20]

// Peek at top
if (!stack.empty()) {
    int topElement = stack.top();  // Returns 20
}
stack.size();

Stack Apps

Call sequence:

A starts: Stack = [A] A calls B: Stack = [A, B] B calls C: Stack = [A, B, C] C finishes: Stack = [A, B] B finishes: Stack = [A] A finishes: Stack = []

Imp Stack w/ Array

#include <vector>
#include <stdexcept>

template<typename T>
class Stack {
private:
    std::vector<T> items;

public:
    void push(const T& item) {
        items.push_back(item);  // O(1) amortized
    }

    T pop() {
        if (isEmpty()) {
            throw std::runtime_error("Pop from empty stack");
        }
        T top = items.back();
        items.pop_back();  // O(1)
        return top;
    }

    T peek() const {
        if (isEmpty()) {
            throw std::runtime_error("Peek from empty stack");
        }
        return items.back();
    }

    bool isEmpty() const {
        return items.empty();
    }

    size_t size() const {
        return items.size();
    }
};

Stack vs Queue (LIFO vs FIFO)

Sorting

When to Sort First

Problem Solve Patterns

When not to Sort

Selection Sort

void selectionSort(vector<int>& arr) {
    int n = arr.size();

    // Outer loop moves the boundary
    for (int i = 0; i < n - 1; i++) {
        // Track index of minimum in unsorted portion
        int minIndex = i;

        // Inner loop scans unsorted portion
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }

        // Swap minimum to boundary position
        swap(arr[i], arr[minIndex]);
    }
}

Insertion Sort

void insertionSort(std::vector<int>& arr) {
    int n = arr.size();

    // Start from second element
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;

        // Shift elements right until correct position found
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }

        // Insert key at correct position
        arr[j + 1] = key;
    }
}

Bubble Sort

void bubbleSort(std::vector<int>& arr) {
    int n = arr.size();

    // Outer loop for each pass
    for (int i = 0; i < n - 1; i++) {
        bool swapped = false;

        // Inner loop for comparisons
        for (int j = 0; j < n - 1 - i; j++) {
            // Compare adjacent elements
            if (arr[j] > arr[j + 1]) {
                // Swap if in wrong order
                std::swap(arr[j], arr[j + 1]);
                swapped = true;
            }
        }

        // Early termination if no swaps
        if (!swapped) break;
    }
}

Comparing These 3 Sorting Algo

Built-in Sort Functions

#include <algorithm>
#include <vector>

std::vector<int> numbers = {5, 2, 8, 1, 9};
std::sort(numbers.begin(), numbers.end());
// numbers is now [1, 2, 5, 8, 9]
#include <algorithm>
#include <vector>
#include <string>
#include <cctype>

std::vector<std::string> words = {
    "zebra", "apple", "Banana", "cherry"
};

// Default sort
std::sort(words.begin(), words.end());
// [Banana, apple, cherry, zebra]

// Sort by length
std::sort(words.begin(), words.end(),
    [](const std::string& a, const std::string& b) {
        return a.length() < b.length();
    }
);
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>

struct Product {
    std::string name;
    int price;
    double rating;
};

std::vector<Product> products = {
    {"Laptop", 999, 4.5},
    {"Mouse", 25, 4.8},
    {"Keyboard", 79, 4.2},
    {"Monitor", 299, 4.6}
};

// Sort by price (ascending)
std::sort(products.begin(), products.end(),
    [](const Product& a, const Product& b) {
        return a.price < b.price;
    }
);

// Sort by multiple criteria: price, then rating
std::sort(products.begin(), products.end(),
    [](const Product& a, const Product& b) {
        if (a.price != b.price) {
            return a.price < b.price;
        }
        return a.rating > b.rating;
    }
);

Beyond Basic Sorting

Merge Sort

Quicksort

Math for Algos

Binary Search

int binary_search(std::vector<int> arr, int target) {
    int left = 0;
    int right = arr.size() - 1;

    while (left <= right) { // <= here because left and right could point to the same element, < would miss it
        int mid = left + (right - left) / 2; // use `(right - left) / 2` to prevent `left + right` potential overflow
        // found target, return its index
        if (arr.at(mid) == target) return mid;
        if (arr.at(mid) < target) {
            // middle less than target, discard left half by making left search boundary `mid + 1`
            left = mid + 1;
        } else {
            // middle greater than target, discard right half by making right search boundary `mid - 1`
            right = mid - 1;
        }
    }
    return -1; // if we get here we didn't hit above return so we didn't find target
}

Two Pointers

Classifications

Non Array Apps

Why use two Pointers