nth_element in C++ STL

Do not miss this exclusive book on Binary Tree Problems. Get it now for free.

nth_element is a function in the C++ Standard Template Library (STL) that allows you to partially sort a range of elements in a container, such that the element at the nth position is the one that would be at that position if the entire range was sorted. The elements before and after the nth position are not sorted.

Table of Contents

  1. Introduction
  2. Syntax & Parameters
  3. Internal Implementation in C++ STL
  4. Time Complexity
  5. Space Complexity
  6. Variations
  7. Examples
  8. Use Cases
  9. Conclusion

1. Introduction

nth_element is part of the <algorithm> header in C++ STL. It efficiently rearranges elements in a specified range such that the element at the nth position is in its sorted position, while the elements before and after it are not necessarily sorted.

2. Syntax & Parameters

The syntax for nth_element is straightforward:

#include <algorithm>

// The nth_element function is a part of the C++ Standard Template Library (STL) and is used to partially sort a range of elements.
// It rearranges the elements in the range [first, last) such that the element at the 'nth' position is the one that would be in that position
// if the entire range was sorted.

template <class RandomAccessIterator>
void nth_element (RandomAccessIterator first, RandomAccessIterator nth, RandomAccessIterator last);

where the RandomAccessIterator is the iterator of any type the user will pass into the function like vector<int>::iterator etc. The following are the inputs :

  • first: Iterator pointing to the beginning of the range.
  • nth: Iterator pointing to the position of the element that will be at its sorted position after the function call.
  • last: Iterator pointing to the end of the range.

3. Internal Implementation in C++ STL

The nth_element algorithm is commonly implemented using a variant of the quickselect algorithm. Quickselect is a selection algorithm that is derived from quicksort. It works by partitioning a range of elements around a chosen pivot, similar to quicksort, but it only recurses into one partition, depending on the desired position (k) relative to the pivot.

Here's a step-by-step explanation of how nth_element can be implemented using quickselect:

  1. Choose a Pivot:

    • Pick a pivot element from the range [first, last). The choice of pivot can affect the algorithm's performance.
    • A common strategy is to select the pivot as the middle element.
  2. Partition the Range:

    • Rearrange the elements in the range [first, last) such that elements smaller than the pivot are on its left, and elements greater than the pivot are on its right.
    • Move the pivot to its final sorted position as shown in the code below :
template <class RandomAccessIterator>
RandomAccessIterator partition(RandomAccessIterator first, RandomAccessIterator last) {
    // Choose the last element as the pivot
    RandomAccessIterator pivot = last - 1;
    RandomAccessIterator i = first - 1;    // Index of smaller element

    // Iterate through the range [first, last - 1)
    for (RandomAccessIterator j = first; j < last - 1; ++j) {
        // Compare the current element (*j) with the pivot element (*pivot)
        if (*j <= *pivot) {
            // If the current element is smaller than or equal to the pivot,
            // increment the index of the smaller element (i)
            ++i;
            // Swap the current element with the element at index i
            std::iter_swap(i, j);
        }
    }

    // After the loop, swap the pivot element with the element at index (i + 1)
    std::iter_swap(i + 1, pivot);
    // Return the index of the pivot in its final sorted position
    return i + 1;
}

  1. Recursive Step:

    • Determine whether the 'nth' element is in the left or right partition based on the pivot's index.
    • Recursively apply the algorithm to the relevant partition.
    template <class RandomAccessIterator>
    void nth_element_helper(RandomAccessIterator first, RandomAccessIterator nth, RandomAccessIterator last) {
        while (first < last) {
            RandomAccessIterator pivotIndex = partition(first, last);
    
            if (pivotIndex == nth) {
                // 'nth' element is at its final sorted position
                return;
            } else if (pivotIndex < nth) {
                // 'nth' element is in the right partition
                first = pivotIndex + 1;
            } else {
                // 'nth' element is in the left partition
                last = pivotIndex;
            }
        }
    }
    
  2. Call the Helper Function:

    • Call the helper function with the appropriate iterators.
    template <class RandomAccessIterator>
    void nth_element(RandomAccessIterator first, RandomAccessIterator nth, RandomAccessIterator last) {
        nth_element_helper(first, nth, last);
    }
    

This implementation is a simplified example, and the actual C++ Standard Library implementation might have additional optimizations and considerations for edge cases. The key idea is to use the partitioning step to iteratively narrow down the range until the 'nth' element is in its final sorted position.

An simple implementation for finding the nth smallest element in a vector is as follows :

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

template <typename RandomAccessIterator>
RandomAccessIterator partition(RandomAccessIterator first, RandomAccessIterator last) {
    // Choose the last element as the pivot
    RandomAccessIterator pivot = last - 1;
    RandomAccessIterator i = first - 1;

    for (RandomAccessIterator j = first; j < last - 1; ++j) {
        if (*j <= *pivot) {
            ++i;
            std::iter_swap(i, j);
        }
    }

    std::iter_swap(i + 1, pivot);
    return i + 1;
}

template <typename RandomAccessIterator>
RandomAccessIterator quick_select(RandomAccessIterator first, RandomAccessIterator nth, RandomAccessIterator last) {
    while (first < last) {
        RandomAccessIterator pivotIndex = partition(first, last);

        if (pivotIndex == nth) {
            // Found the nth smallest element
            return nth;
        } else if (pivotIndex < nth) {
            // Nth element is in the right partition
            first = pivotIndex + 1;
        } else {
            // Nth element is in the left partition
            last = pivotIndex;
        }
    }

    // If the range is exhausted, return the last iterator
    return last;
}

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

    // Find the 4th smallest element
    auto nth_element_iterator = quick_select(numbers.begin(), numbers.begin() + 4, numbers.end());

    if (nth_element_iterator != numbers.end()) {
        std::cout << "The 4th smallest element is: " << *nth_element_iterator << std::endl;
    } else {
        std::cout << "Invalid range or position" << std::endl;
    }

    return 0;
}

Output:

The 4th smallest element is: 3

4. Time Complexity

The time complexity of nth_element is O(N), where N is the size of the range [first, last). This linear time complexity makes it more efficient than a full sort operation, which typically has O(N log N) time complexity.

5. Space Complexity

nth_element has a constant space complexity of O(1), as it performs the rearrangement in-place without requiring additional memory proportional to the size of the input.

6. Variations using in C++ STL

There are many variation of using the nth element but we are discussing the major ones only.

a. nth_element with Custom Comparator

You can use nth_element with a custom comparator function to handle sorting based on specific criteria. The syntax is as follows:

#include <functional>
// assume numbers is a vector of size 10
std::nth_element(numbers.begin(), numbers.begin() + 3, numbers.end(), std::greater<int>());

In this example, std::greater<int>() is a custom comparator that sorts the range in descending order.

b. nth_element with User-Defined Type

nth_element can be used with a container of user-defined types. You just need to ensure that the comparison operator (operator<) is defined for your type.

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

// Define a struct representing a Person with a name and an age
struct Person {
    std::string name;
    int age;
};

// Define a custom comparison operator for Person objects based on age
bool operator<(const Person& a, const Person& b) {
    return a.age < b.age;
}

int main() {
    // Create a vector of Person objects with different ages
    std::vector<Person> people = {{"Alice", 25}, {"Bob", 20}, {"Charlie", 30}};

    // Use the nth_element algorithm to partially sort the vector
    std::nth_element(people.begin(), people.begin() + 1, people.end());

    // Now, people[1] is the person with the second smallest age

    // Print the information of the person with the second smallest age
    std::cout << "Person with the second smallest age: " << people[1].name << std::endl;

    return 0;
}

Here's the explanation of the above code :

  1. Person Structure: The code defines a Person struct with two members: name (a string) and age (an integer).

  2. Custom Comparison Operator: An operator < function is defined outside the struct, providing a custom way to compare two Person objects based on their age. This operator is necessary for the nth_element algorithm to determine the order of elements.

3.Vector of Persons: A vector named people is created, containing three Person objects with different names and ages.

  1. nth_element Algorithm: The std::nth_element algorithm is applied to the vector. It rearranges the elements in the range [people.begin(), people.end()) such that the element at the specified position (people.begin() + 1 in this case) is in its final sorted position, and elements before and after it are not sorted.

  2. Output: After applying nth_element, the person with the second smallest age is at index 1 in the people vector.

  3. Print Result: The code prints the name of the person with the second smallest age to the console using std::cout.

In this specific example, the output would be:

Output:

Person with the second smallest age: Alice

7. Examples

Consider the following examples:

Example 1:

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

int main() {
    std::vector<int> numbers = {9, 3, 7, 1, 5, 6, 8, 2, 4};
    
    std::cout << "Original vector: ";
    for (int num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    // Partially sort the vector so that the 4th element is at its sorted position
    std::nth_element(numbers.begin(), numbers.begin() + 3, numbers.end());

    std::cout << "After nth_element: ";
    for (int num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

Output:

Original vector: 9 3 7 1 5 6 8 2 4 
After nth_element: 4 2 3 1 5 6 8 7 9 

Example 2:

// ... (code from the previous section)

// Example of using nth_element with a custom comparator
std::nth_element(numbers.begin(), numbers.begin() + 3, numbers.end(), std::greater<int>());

std::cout << "After nth_element with custom comparator: ";
for (int num : numbers) {
    std::cout << num << " ";
}
std::cout << std::endl;

Output:

After nth_element with custom comparator: 8 6 5 4 3 1 2 7 9 

Example 3:

// ... (code from the previous section)

// Example of using nth_element with a user-defined type
std::vector<Person> people = {{"Alice", 25}, {"Bob", 20}, {"Charlie", 30}};

std::nth_element(people.begin(), people.begin() + 1, people.end());

std::cout << "Person with the second smallest age: " << people[1].name << std::endl;

Output:

Person with the second smallest age: Alice

8. Use Cases

The nth_element algorithm is particularly useful in situations where you need to find or work with specific elements in a collection but don't require the entire collection to be fully sorted. Here are some specific use cases for nth_element:

  1. Order Statistics: When you need to find the k-th smallest or largest element in a collection, nth_element is very efficient. This is useful in applications where you need to identify, for example, the median or other quantiles.

  2. Quick Approximation of Sorted Order: If you only need a rough approximation of sorted order for a subset of elements, nth_element can be faster than a full sort. This is particularly beneficial when dealing with large datasets.

  3. Partial Sorting: When you need to partially sort a collection, such as extracting the top k elements or sorting only a portion of the collection, nth_element is more efficient than sorting the entire collection.

Remember that the primary advantage of nth_element is its efficiency for finding the position of a specific element without fully sorting the entire collection. It's particularly well-suited for scenarios where only a subset of the sorted order is needed.

9. Conclusion

In conclusion, nth_element is a powerful algorithm in C++ STL for efficiently finding the nth element in a sorted sequence within a specified range. It provides a useful tool for situations where a full sort is unnecessary, contributing to improved performance in certain scenarios. Understanding and utilizing such algorithms is key to writing efficient and effective C++ code.

Key Takeaways (`nth_element` in C++ STL)

  • Efficient Partial Sorting: `nth_element` is designed for efficiently finding the nth element in a partially sorted sequence, making it suitable for scenarios where a full sort is unnecessary.
  • Linear Time Complexity: The time complexity of `nth_element` is O(N), where N is the size of the range being sorted. This linear time complexity distinguishes it from full sorting algorithms with O(N log N) complexity.
  • Constant Space Complexity: The space complexity of `nth_element` is O(1), indicating that it performs the rearrangement in-place without requiring additional memory proportional to the input size.
  • Variations with Custom Comparator: `nth_element` can be customized with a comparator function to handle sorting based on specific criteria, offering flexibility in its application.
  • Variations with User-Defined Type: `nth_element` is versatile and can be used with containers of user-defined types, provided the necessary comparison operators are defined for the type.

Sign up for FREE 3 months of Amazon Music. YOU MUST NOT MISS.