← All Posts
DSA Series · Arrays · Sorting Techniques · Competition Patterns

Sorting Patterns for Competitive Programming

Sorting is rarely the entire solution. It is usually the first step that enables a faster algorithm. This post covers the most important patterns where sorting unlocks an efficient solution: custom comparators, sort combined with two pointers, sort combined with binary search, interval scheduling, and counting inversions with merge sort.

Each pattern includes the key idea, when to recognize it, and complete C++ code.

Pattern 1: Custom Comparators

The default sort orders elements in ascending order. Many problems require a different ordering: sort by second element, sort strings by length, sort events by end time. C++ makes this easy with custom comparators.

Lambda Comparator

// Sort pairs by second element (ascending)
vector<pair<int,int>> v = {{3,5},{1,2},{4,1},{2,8}};
sort(v.begin(), v.end(), [](const pair<int,int>& a, const pair<int,int>& b){
    return a.second < b.second;
});
// Result: {4,1}, {1,2}, {3,5}, {2,8}

Struct with Comparator

struct Event {
    int start, end, id;
};

// Sort by end time, break ties by start time
sort(events.begin(), events.end(), [](const Event& a, const Event& b){
    if (a.end != b.end) return a.end < b.end;
    return a.start < b.start;
});

Common Comparator Pitfalls

Strict Weak Ordering

The comparator must satisfy strict weak ordering. This means:

  • Irreflexivity: comp(a, a) must be false.
  • Asymmetry: if comp(a, b) is true, then comp(b, a) must be false.
  • Transitivity: if comp(a, b) and comp(b, c), then comp(a, c).

Using <= instead of < violates irreflexivity and causes undefined behavior with std::sort. This is a frequent contest bug.

Largest Number Problem

Given a list of non-negative integers, arrange them to form the largest number. The trick is a custom comparator that compares string concatenations.

// LeetCode 179: Largest Number
string largestNumber(vector<int>& nums) {
    vector<string> strs;
    for (int x : nums) strs.push_back(to_string(x));

    sort(strs.begin(), strs.end(), [](const string& a, const string& b){
        return a + b > b + a;  // "30" + "3" = "303" vs "3" + "30" = "330"
    });

    if (strs[0] == "0") return "0";
    string result;
    for (const string& s : strs) result += s;
    return result;
}

Sort by Frequency

// Sort elements by frequency (most frequent first), ties broken by value
vector<int> frequencySort(vector<int>& nums) {
    unordered_map<int,int> freq;
    for (int x : nums) freq[x]++;

    sort(nums.begin(), nums.end(), [&freq](int a, int b){
        if (freq[a] != freq[b]) return freq[a] > freq[b];
        return a < b;
    });
    return nums;
}

Pattern 2: Sort + Two Pointers

Many problems ask for pairs or triplets satisfying a condition. Brute force checks all combinations in $O(n^2)$ or $O(n^3)$. Sorting first enables a two-pointer scan that reduces the complexity by one order.

The Template

  1. Sort the array.
  2. Fix one element (or use two pointers from opposite ends).
  3. Use the sorted order to advance pointers intelligently.

3Sum

Find all unique triplets that sum to zero. After sorting, fix one element and use two pointers on the remaining subarray.

vector<vector<int>> threeSum(vector<int>& nums) {
    sort(nums.begin(), nums.end());
    vector<vector<int>> result;
    int n = nums.size();

    for (int i = 0; i < n - 2; i++) {
        if (i > 0 && nums[i] == nums[i-1]) continue;  // skip duplicates
        int lo = i + 1, hi = n - 1;
        int target = -nums[i];

        while (lo < hi) {
            int sum = nums[lo] + nums[hi];
            if (sum == target) {
                result.push_back({nums[i], nums[lo], nums[hi]});
                while (lo < hi && nums[lo] == nums[lo+1]) lo++;
                while (lo < hi && nums[hi] == nums[hi-1]) hi--;
                lo++; hi--;
            } else if (sum < target) {
                lo++;
            } else {
                hi--;
            }
        }
    }
    return result;
}

3Sum: Sort + Two Pointers Step-Through

Array: [-4, -1, -1, 0, 1, 2]. Fix i=0 (value -4), then scan with lo and hi.

Two Sum (Sorted Array)

// LeetCode 167: Two Sum II
vector<int> twoSum(vector<int>& nums, int target) {
    int lo = 0, hi = nums.size() - 1;
    while (lo < hi) {
        int sum = nums[lo] + nums[hi];
        if (sum == target) return {lo + 1, hi + 1};
        else if (sum < target) lo++;
        else hi--;
    }
    return {};  // guaranteed to have a solution
}

Closest Pair Sum

// Find two elements whose sum is closest to a target
pair<int,int> closestPairSum(vector<int>& arr, int target) {
    sort(arr.begin(), arr.end());
    int lo = 0, hi = arr.size() - 1;
    int bestDiff = INT_MAX;
    pair<int,int> result;

    while (lo < hi) {
        int sum = arr[lo] + arr[hi];
        if (abs(sum - target) < bestDiff) {
            bestDiff = abs(sum - target);
            result = {arr[lo], arr[hi]};
        }
        if (sum < target) lo++;
        else if (sum > target) hi--;
        else break;  // exact match
    }
    return result;
}

When you need to answer "does this value exist?" or "what is the first value >= X?" for many queries, sort once and binary search each query. This reduces per-query time from $O(n)$ to $O(\log n)$.

The Template

  1. Sort the array (or one of the arrays) in $O(n \log n)$.
  2. For each query, binary search in $O(\log n)$.
  3. Total: $O(n \log n + q \log n)$ where $q$ is the number of queries.

Counting Pairs with Difference

// Count pairs (i, j) where |arr[i] - arr[j]| == k
int countPairsWithDiff(vector<int>& arr, int k) {
    sort(arr.begin(), arr.end());
    int count = 0;
    for (int i = 0; i < (int)arr.size(); i++) {
        // Search for arr[i] + k in arr[i+1..]
        if (binary_search(arr.begin() + i + 1, arr.end(), arr[i] + k))
            count++;
    }
    return count;
}

Two Arrays: Count Pairs

Given two arrays a[] and b[], for each element in a, count how many elements in b are less than it.

// For each a[i], count elements in b[] that are strictly less
vector<int> countSmaller(vector<int>& a, vector<int>& b) {
    sort(b.begin(), b.end());
    vector<int> result;
    for (int x : a) {
        // lower_bound gives first element >= x
        int cnt = lower_bound(b.begin(), b.end(), x) - b.begin();
        result.push_back(cnt);
    }
    return result;
}

Longest Increasing Subsequence (Patience Sort)

LIS can be solved in $O(n \log n)$ using a sort-like approach with binary search. Maintain a "tails" array where tails[i] is the smallest tail element of all increasing subsequences of length i+1.

int lengthOfLIS(vector<int>& nums) {
    vector<int> tails;
    for (int x : nums) {
        auto it = lower_bound(tails.begin(), tails.end(), x);
        if (it == tails.end())
            tails.push_back(x);
        else
            *it = x;
    }
    return tails.size();
}

Pattern 4: Interval Scheduling After Sorting

Interval problems almost always start with sorting. The two most common orderings are: sort by start time (for merging overlapping intervals) and sort by end time (for selecting the most non-overlapping intervals).

Merge Overlapping Intervals

Sort intervals by start time. Then scan left to right, merging whenever the current interval overlaps with the previous one.

vector<vector<int>> merge(vector<vector<int>>& intervals) {
    sort(intervals.begin(), intervals.end());
    vector<vector<int>> merged;

    for (auto& iv : intervals) {
        if (merged.empty() || merged.back()[1] < iv[0]) {
            merged.push_back(iv);
        } else {
            merged.back()[1] = max(merged.back()[1], iv[1]);
        }
    }
    return merged;
}

Merge Intervals: Step-Through

Intervals sorted by start time. Green intervals are merged results.

Maximum Non-Overlapping Intervals (Activity Selection)

Sort by end time. Greedily select the interval that ends earliest and does not overlap with the last selected interval. This greedy choice is provably optimal.

int maxNonOverlapping(vector<vector<int>>& intervals) {
    sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b){
        return a[1] < b[1];  // sort by end time
    });

    int count = 1, lastEnd = intervals[0][1];
    for (int i = 1; i < (int)intervals.size(); i++) {
        if (intervals[i][0] >= lastEnd) {
            count++;
            lastEnd = intervals[i][1];
        }
    }
    return count;
}

Minimum Meeting Rooms

Sort start times and end times separately. Use two pointers to track how many meetings are active at any point.

int minMeetingRooms(vector<vector<int>>& intervals) {
    vector<int> starts, ends;
    for (auto& iv : intervals) {
        starts.push_back(iv[0]);
        ends.push_back(iv[1]);
    }
    sort(starts.begin(), starts.end());
    sort(ends.begin(), ends.end());

    int rooms = 0, maxRooms = 0, e = 0;
    for (int s = 0; s < (int)starts.size(); s++) {
        if (starts[s] < ends[e]) {
            rooms++;
        } else {
            e++;
        }
        maxRooms = max(maxRooms, rooms);
    }
    return maxRooms;
}
Why sort by end time for selection, but start time for merging? When selecting non-overlapping intervals, the greedy choice that maximizes count is "finish earliest." When merging overlapping intervals, you need to process them in order of when they begin so you can extend the current merged interval.

Pattern 5: Inversion Count via Merge Sort

An inversion is a pair $(i, j)$ where $i < j$ but $\text{arr}[i] > \text{arr}[j]$. The number of inversions measures how far the array is from being sorted. Counting inversions naively takes $O(n^2)$. Using a modified merge sort, we can count them in $O(n \log n)$.

The Key Insight

During the merge step, when we pick an element from the right half because it is smaller than the current element from the left half, that element forms inversions with all remaining elements in the left half.

If the left half has elements at positions i through mid and we pick right[j] because right[j] < left[i], then right[j] is inverted with left[i], left[i+1], ..., left[mid]. That is mid - i + 1 inversions.

Inversion Count via Merge Sort: Step-Through

Array: [5, 3, 8, 1, 2]. Red highlights show inversions detected during merge.

C++ Implementation

long long mergeCount(vector<int>& arr, int left, int mid, int right) {
    vector<int> tmp;
    int i = left, j = mid + 1;
    long long inversions = 0;

    while (i <= mid && j <= right) {
        if (arr[i] <= arr[j]) {
            tmp.push_back(arr[i++]);
        } else {
            // arr[j] is smaller than arr[i..mid], so (mid - i + 1) inversions
            inversions += (mid - i + 1);
            tmp.push_back(arr[j++]);
        }
    }
    while (i <= mid)  tmp.push_back(arr[i++]);
    while (j <= right) tmp.push_back(arr[j++]);
    for (int k = left; k <= right; k++)
        arr[k] = tmp[k - left];
    return inversions;
}

long long mergeSortCount(vector<int>& arr, int left, int right) {
    if (left >= right) return 0;
    int mid = left + (right - left) / 2;
    long long inv = 0;
    inv += mergeSortCount(arr, left, mid);
    inv += mergeSortCount(arr, mid + 1, right);
    inv += mergeCount(arr, left, mid, right);
    return inv;
}

long long countInversions(vector<int>& arr) {
    return mergeSortCount(arr, 0, arr.size() - 1);
}

The time complexity is $O(n \log n)$, same as merge sort. The space complexity is $O(n)$ for the temporary array.

Applications of Inversion Count

Pattern Summary

PatternTimeKey IdeaExample Problems
Custom Comparator $O(n \log n)$ Sort by a non-default criterion Largest Number, Sort by Frequency
Sort + Two Pointers $O(n \log n)$ Sort enables pointer movement logic 3Sum, Two Sum II, Closest Pair
Sort + Binary Search $O(n \log n)$ Sort once, binary search per query Count Pairs, LIS, Two Arrays
Interval Scheduling $O(n \log n)$ Sort by start or end time, then greedy/sweep Merge Intervals, Activity Selection, Meeting Rooms
Inversion Count $O(n \log n)$ Modified merge sort counts cross-inversions Inversion Count, Reverse Pairs

How to Recognize These Patterns

Practice Problems

Summary