Author: June Hong

  • C++ Standard Template Library (STL)

    The C++ Standard Template Library (STL) is a powerful collection of generic classes and functions that provide commonly used data structures and algorithms. It allows developers to work with collections of data (containers) and perform common tasks such as searching, sorting, and manipulation efficiently.

    Key Components of the STL

    The STL is primarily divided into four main components:

    1. Containers

    Containers are data structures used to store collections of elements. The STL provides several container types, each optimized for specific use cases:

    Sequence Containers:

    • Store data in a linear fashion.
    • Examples:
      • std::vector: Dynamic array.
      • std::deque: Double-ended queue.
      • std::list: Doubly linked list.
      • std::array: Fixed-size array.

    Associative Containers:

    • Store data in sorted order, typically as key-value pairs.
    • Examples:
      • std::set: Collection of unique keys.
      • std::map: Key-value pairs with unique keys.
      • std::multiset: Allows duplicate keys.
      • std::multimap: Allows duplicate key-value pairs.

    Unordered Containers:

    • Use hashing for faster access but do not guarantee order.
    • Examples:
      • std::unordered_set
      • std::unordered_map
      • std::unordered_multiset
      • std::unordered_multimap

    Container Adapters:

    • Provide a modified interface for existing containers.
    • Examples:
      • std::stack: LIFO (Last In First Out).
      • std::queue: FIFO (First In First Out).
      • std::priority_queue: Queue with elements sorted by priority.

    2. Iterators

    Iterators provide a way to traverse containers. They act like pointers and can access elements of a container sequentially.

    Types of Iterators:

    • Input Iterator: For reading data (e.g., istream_iterator).
    • Output Iterator: For writing data (e.g., ostream_iterator).
    • Forward Iterator: Single-pass traversal (e.g., std::forward_list).
    • Bidirectional Iterator: Forward and backward traversal (e.g., std::list).
    • Random Access Iterator: Access any element in constant time (e.g., std::vector).

    3. Algorithms

    Algorithms in the STL are generic functions that operate on containers through iterators. They implement common tasks like searching, sorting, modifying, and more.

    Examples:

    • Searching:
      • std::find: Find an element.
      • std::binary_search: Perform binary search (requires sorted range).
    • Sorting:
      • std::sort: Sort elements in ascending order.
      • std::stable_sort: Maintains relative order of equivalent elements.
    • Modifying:
      • std::copy: Copy elements.
      • std::transform: Apply a function to elements.
      • std::replace: Replace specific elements.
    • Others:
      • std::for_each: Apply a function to each element.
      • std::accumulate: Accumulate the result of a function over a range.

    4. Functors and Lambdas

    Functors (Function Objects) and lambdas provide callable objects that can be passed to STL algorithms for custom operations.

    Functors:

    • Objects that behave like functions by overloading the operator().
    struct Square {
        int operator()(int x) const { return x * x; }
    };

    Lambdas:

    • Anonymous functions that can capture variables and provide custom logic.
    std::for_each(vec.begin(), vec.end(), [](int x) {
        std::cout << x << " "; 
    });

    Advantages of STL

    • Reusability: Reduces the need to reinvent common data structures and algorithms.
    • Efficiency: Optimized implementations with consistent time complexities.
    • Portability: Part of the C++ standard, making it portable across platforms.
    • Generality: Provides generic, type-safe solutions via templates.

    Example: Using STL for Sorting and Searching

    #include <iostream>
    #include <vector>
    #include <algorithm> // For sort and binary_search
    
    int main() {
        std::vector<int> numbers = {10, 2, 33, 45, 23};
    
        // Sort the vector
        std::sort(numbers.begin(), numbers.end());
    
        // Display sorted elements
        for (const auto& num : numbers)
            std::cout << num << " ";
        std::cout << std::endl;
    
        // Perform a binary search
        int target = 33;
        if (std::binary_search(numbers.begin(), numbers.end(), target)) {
            std::cout << target << " found." << std::endl;
        } else {
            std::cout << target << " not found." << std::endl;
        }
    
        return 0;
    }

    Best Practices

    1. Choose the Right Container: Use containers suited to your data and access patterns.
    2. Leverage Algorithms: Avoid writing custom loops when STL algorithms can suffice.
    3. Use Range-Based Loops: For cleaner and safer iteration over containers.
    4. Optimize with Move Semantics: Use std::move for efficient data transfers.
    5. Avoid Manual Memory Management: Use containers like std::vector and std::shared_ptr to manage resources automatically.
  • Big O Notation

    What is Big O notation ?

    Big O notation is a mathematical concept used in computer science to describe the time complexity or space complexity of algorithms. It provides a standardized way to compare the efficiency of different algorithms in terms of their worst-case performance.

    Purpose of Big O Notation

    1. Measure Efficiency: Helps analyze how an algorithm scales with input size.
    2. Compare Algorithms: Provides a standard to evaluate and compare different solutions.
    3. Worst-Case Analysis: Describes the upper bound of an algorithm’s performance to ensure reliability under all conditions.

    Common Big O Notations

    1. O(1) – Constant Time
      • The runtime does not depend on the size of the input.
      • Example: Accessing an element in an array by index.
    2. O(log n) – Logarithmic Time
      • The runtime grows logarithmically with the input size.
      • Example: Binary search.
    3. O(n) – Linear Time
      • The runtime grows directly proportional to the input size.
      • Example: Traversing an array.
    4. O(n log n) – Quasilinear Time
      • Often associated with divide-and-conquer algorithms.
      • Example: Merge sort, quicksort (average case).
    5. O(n²) – Quadratic Time/O(n³) – Cubic Time 
      • The runtime grows quadratically/cubically with input size.
      • Example: Nested loops, such as in bubble sort.
    6. O(2ⁿ) – Exponential Time
      • The runtime doubles with each additional input element.
      • Example: Recursive algorithms for the Fibonacci sequence.
    7. O(n!) – Factorial Time
      • Extremely inefficient; used in algorithms that generate permutations.
      • Example: Solving the traveling salesman problem using brute force.

    Why Is Big O Important?

    1. Scalability: Determines how well an algorithm performs with large inputs.
    2. Optimization: Helps identify bottlenecks and improve performance.
    3. Decision Making: Guides the choice of the right algorithm for a given problem.

  • Designing an API rate limiter

    Designing an API rate limiter is crucial for maintaining system performance and preventing abuse. Here’s a high-level overview of how you can design a scalable API rate limiter:

    Components:

    1. Rate Limiter Middleware: This sits between the client and the server, handling rate limiting logic.
    2. Counter Storage: Stores the count of requests made by each user or IP address. In-memory data stores like Redis are commonly used for fast access.
    3. Rate Limiting Algorithm: Determines how to count and limit requests (e.g., token bucket, leaky bucket).

    High-Level Design:

    1. Client Request: The client sends a request to the API endpoint.
    2. Middleware Check: The rate limiter middleware checks the counter for the client’s IP/user ID.
    3. Counter Update: If the counter is below the limit, the middleware allows the request and increments the counter. If the limit is reached, the middleware blocks or throttles the request.
    4. Counter Expiry: The counter expires after a set time period, allowing the client to make requests again.

    Example Implementation:

    Here’s a simplified example using Redis for counter storage:

    #include <iostream>
    #include <string>
    #include <unordered_map>
    #include <chrono>
    
    class RateLimiter {
    public:
        RateLimiter(int limit, int timeWindow) : limit(limit), timeWindow(timeWindow) {}
    
        bool allowRequest(const std::string& clientId) {
            auto now = std::chrono::steady_clock::now();
            auto& counter = counters[clientId];
    
            if (now - counter.lastRequestTime > timeWindow) {
                counter.count = 0;
            }
    
            if (counter.count < limit) {
                counter.count++;
                counter.lastRequestTime = now;
                return true;
            } else {
                return false;
            }
        }
    
    private:
        struct Counter {
            int count = 0;
            std::chrono::steady_clock::time_point lastRequestTime;
        };
    
        int limit;
        int timeWindow;
        std::unordered_map<std::string, Counter> counters;
    };
    
    int main() {
        RateLimiter rateLimiter(10, std::chrono::seconds(60));
        std::string clientId = "client123";
    
        for (int i = 0; i < 15; ++i) {
            if (rateLimiter.allowRequest(clientId)) {
                std::cout << "Request allowed" << std::endl;
            } else {
                std::cout << "Request denied" << std::endl;
            }
        }
    
        return 0;
    }

    Explanation:

    • RateLimiter Class: Manages counters for each client.
    • allowRequest Method: Checks if the client has exceeded the rate limit.
    • Counter Expiry: Resets the counter after the time window expires.