Sort K-Sorted Array in C++ (also known as Sort a Nearly Sorted Array) is a classic algorithmic problem frequently tested in technical interviews at top software engineering firms and quant trading shops including Google, Amazon, Microsoft, Citadel, Jane Street, Goldman Sachs, and Bloomberg. In a K-Sorted Array of length N, every single element is at most k positions away from its correctly sorted target index (where k < N). While standard comparison-based sorting algorithms require O(N · log N) time, we can exploit this local displacement bound to sort the array in O(N · log k) time complexity and O(k) auxiliary space using a Sliding Min-Heap of Size (k + 1). In this comprehensive masterclass, we will deconstruct the problem from first principles: covering the O(N · log N) Full Introsort, O(N · k) Bounded Insertion Sort, and the Optimal O(N · log k) Sliding Min-Heap algorithm. We will also analyze L1 cache behavior, explore real world network packet reordering (TCP jitter buffers), run a 10,000,000-element benchmark lab, and provide interactive โTry it yourselfโ challenges after every section.
๐น What is a K-Sorted Array? The K-Displacement Invariant
An array nums of size N is defined as k-sorted (or k-nearly sorted) if every element currently residing at index i belongs at a sorted target index j such that:
|i - j| <= k (for all 0 <= i < N)Let us analyze a concrete example with k = 2 and N = 7:
Input Array (k = 2):
Index: 0 1 2 3 4 5 6
Values: [ 6, 5, 3, 2, 8, 10, 9 ]
Sorted Array:
Index: 0 1 2 3 4 5 6
Values: [ 2, 3, 5, 6, 8, 9, 10 ]
Displacement Check (|Original Index - Sorted Index|):
- Value 6: index 0 → sorted index 3 → |0 - 3| = 3 (wait: in our valid k=2 example, let's trace subarray [0..2])
- Value 2: index 3 → sorted index 0 → |3 - 0| = 3 → (k=3 bound)
For k = 2:
Input: [ 3, 2, 1, 5, 4, 7, 6 ]
Sorted: [ 1, 2, 3, 4, 5, 6, 7 ]
- 1 is at index 2 → sorted index 0 (|2 - 0| = 2 <= k)
- 2 is at index 1 → sorted index 1 (|1 - 1| = 0 <= k)
- 3 is at index 0 → sorted index 2 (|0 - 2| = 2 <= k)From this definition, we derive the fundamental Sliding Window Window-of-(k+1) Theorem:
- The (k + 1) Locality Theorem: For any destination index i in the sorted array, the element that belongs at index i must currently reside within the index range
[i, min(N - 1, i + k)]. Therefore, the minimum among the nextk + 1elements is guaranteed to be the next smallest element in the global sorted order. - The Sliding Min-Heap Principle: By inserting the first
k + 1elements into a Min-Heap of capacityk + 1:- The top element of the heap (
min_heap.top()) is guaranteed to be the smallest overall element for the current write index. - We extract this minimum, write it to index i, and push the next element from the array (at index
i + k + 1) into the heap.
- The top element of the heap (
Try it yourself
- If k = 0, what does this imply about the array? (Answer: The array is already perfectly sorted!).
- If k = N – 1, what does this algorithm degrade to? (Answer: Standard Heap Sort in O(N log N)).
๐น Approach 1: Brute Force (Full Introsort via std::sort: O(N · log N))
Intuition & Reasoning: The most naive baseline is to ignore the k-sorted guarantee and sort the entire array directly using C++ standard introsort (std::sort). While it guarantees correctness, it wastes CPU cycles by sorting elements across arbitrary distances.
#include <iostream>
#include <vector>
#include <algorithm>
#include <cassert>
using namespace std;
// Approach 1: Full Introsort (Naive Baseline)
// Time Complexity: O(N * log N), Space Complexity: O(log N) stack
void sort_k_sorted_brute(vector<int>& nums) {
sort(nums.begin(), nums.end());
}
int main() {
vector<int> arr1 = {3, 2, 1, 5, 4, 7, 6};
vector<int> expected = {1, 2, 3, 4, 5, 6, 7};
cout << "--- Approach 1: Full Introsort ---\n";
sort_k_sorted_brute(arr1);
for (int x : arr1) cout << x << " ";
cout << "\n";
assert(arr1 == expected);
cout << "All assertions passed successfully!\n";
return 0;
}
Output
--- Approach 1: Full Introsort ---
1 2 3 4 5 6 7
All assertions passed successfully!
Complexity Analysis
- Time Complexity (TC): O(N · log N): Standard comparison sort lower bound without domain constraints.
- Space Complexity (SC): O(log N): Stack memory used by recursive introsort partitions.
- Bottleneck: For N = 10,000,000 and k = 10,
logโ(10,000,000) ≈ 24whereaslogโ(10) ≈ 3.3. Standard sort performs 7x more comparisons than necessary.
Try it yourself
- Why is
std::sortunable to automatically optimize for nearly sorted arrays? (Hint: Quicksort pivots are chosen randomly or via median-of-three, not by bounded window).
๐น Approach 2: Bounded Insertion Sort (O(N · k) Time, O(1) Space)
Intuition & Reasoning: Standard Insertion Sort has a worst-case time complexity of O(Nยฒ) on reverse-sorted data. However, in a k-sorted array, every element is at most k positions away from its sorted destination. Therefore, during the inner shifting loop of insertion sort, an element will be shifted at most k times! This bounds the total runtime to O(N · k) with strict O(1) auxiliary space.
#include <iostream>
#include <vector>
#include <cassert>
using namespace std;
// Approach 2: Bounded Insertion Sort
// Time Complexity: O(N * k), Space Complexity: O(1)
void sort_k_sorted_insertion(vector<int>& nums, int k) {
const int n = static_cast<int>(nums.size());
for (int i = 1; i < n; ++i) {
int key = nums[i];
int j = i - 1;
// Inner loop runs AT MOST k times due to the k-sorted invariant!
while (j >= 0 && nums[j] > key) {
nums[j + 1] = nums[j];
--j;
}
nums[j + 1] = key;
}
}
int main() {
vector<int> arr = {3, 2, 1, 5, 4, 7, 6};
vector<int> expected = {1, 2, 3, 4, 5, 6, 7};
cout << "--- Approach 2: Bounded Insertion Sort ---\n";
sort_k_sorted_insertion(arr, 2);
for (int x : arr) cout << x << " ";
cout << "\n";
assert(arr == expected);
cout << "All assertions passed successfully!\n";
return 0;
}
Output
--- Approach 2: Bounded Insertion Sort ---
1 2 3 4 5 6 7
All assertions passed successfully!
Complexity Analysis
- Time Complexity (TC): O(N · k): The outer loop runs N times; the inner loop executes at most k shifts per iteration.
- Space Complexity (SC): O(1): Sorts entirely in place without any auxiliary heap or stack allocations.
- Hardware Sweet Spot: For very small k (e.g.
k ≤ 16), Bounded Insertion Sort often beats heap-based sorting on real CPUs due to perfect L1 cache line sequential access and zero heap maintenance overhead.
Try it yourself
- What happens when k grows large (e.g. k = 10,000)? Why does Bounded Insertion Sort become too slow?
๐น Approach 3: Optimal Sliding Min-Heap of Size (k + 1) (O(N · log k) Time, O(k) Space)
Intuition & Structural Invariants: We maintain a std::priority_queue (Min-Heap) containing at most k + 1 elements:
Step 1: Push the first min(N, k + 1) elements into the Min-Heap.
Step 2: Initialize write_index = 0.
Step 3: Loop while next elements exist (from index k + 1 to N - 1):
- Extract the smallest element from the heap: nums[write_index++] = min_heap.top()
- Pop the extracted element: min_heap.pop()
- Push the new incoming element: min_heap.push(nums[i])
Step 4: Drain all remaining elements from the heap into nums[write_index++].#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#include <cassert>
using namespace std;
// Approach 3: Optimal Sliding Min-Heap of Size (k + 1)
// Time Complexity: O(N * log k), Space Complexity: O(k)
void sort_k_sorted_optimal(vector<int>& nums, int k) {
if (nums.empty() || k <= 0) return;
const int n = static_cast<int>(nums.size());
// Min-Heap using std::greater comparator
priority_queue<int, vector<int>, greater<int>> min_heap;
// Step 1: Push the first (k + 1) elements (or all elements if N < k + 1)
int initial_window = min(n, k + 1);
for (int i = 0; i < initial_window; ++i) {
min_heap.push(nums[i]);
}
int write_index = 0;
// Step 2: Slide the window across the remaining elements
for (int i = k + 1; i < n; ++i) {
nums[write_index++] = min_heap.top();
min_heap.pop();
min_heap.push(nums[i]);
}
// Step 3: Drain all remaining elements from the heap
while (!min_heap.empty()) {
nums[write_index++] = min_heap.top();
min_heap.pop();
}
}
int main() {
vector<int> arr1 = {3, 2, 1, 5, 4, 7, 6};
vector<int> arr2 = {6, 5, 3, 2, 8, 10, 9};
vector<int> expected1 = {1, 2, 3, 4, 5, 6, 7};
vector<int> expected2 = {2, 3, 5, 6, 8, 9, 10};
cout << "--- Approach 3: Optimal Sliding Min-Heap ---\n";
sort_k_sorted_optimal(arr1, 2);
sort_k_sorted_optimal(arr2, 3);
for (int x : arr1) cout << x << " ";
cout << "\n";
assert(arr1 == expected1);
assert(arr2 == expected2);
cout << "All assertions passed successfully!\n";
return 0;
}
Output
--- Approach 3: Optimal Sliding Min-Heap ---
1 2 3 4 5 6 7
All assertions passed successfully!
Step by Step Sliding Window Trace
Tracing sort_k_sorted_optimal on nums = [3, 2, 1, 5, 4, 7, 6] with k = 2 (Window size = 3):
Initial: Push nums[0..2] → [3, 2, 1] into heap.
Heap State: [1, 3, 2] (Min is 1)
Step 1 (i = 3, val = 5):
- Extract top: nums[0] = 1, pop().
- Push 5. Heap: [2, 3, 5]
Step 2 (i = 4, val = 4):
- Extract top: nums[1] = 2, pop().
- Push 4. Heap: [3, 4, 5]
Step 3 (i = 5, val = 7):
- Extract top: nums[2] = 3, pop().
- Push 7. Heap: [4, 7, 5]
Step 4 (i = 6, val = 6):
- Extract top: nums[3] = 4, pop().
- Push 6. Heap: [5, 7, 6]
Drain Phase:
- Pop top (5) → nums[4] = 5. Heap: [6, 7]
- Pop top (6) → nums[5] = 6. Heap: [7]
- Pop top (7) → nums[6] = 7. Heap: []
Final Array: [1, 2, 3, 4, 5, 6, 7]!Complexity Analysis & Optimality Proof
- Time Complexity (TC): O(N · log k): Inserting k + 1 elements takes O(k). Each of the remaining N – (k + 1) iterations performs one
pop()and onepush()in a heap of size k + 1, taking O(log k) time. Total operations = N × O(log k). - Space Complexity (SC): O(k): The priority queue holds at most k + 1 elements in memory simultaneously.
- Information-Theoretic Optimality: Since each element has at most 2k + 1 possible destination slots, the decision tree has (2k + 1)N leaves, establishing an information-theoretic lower bound of Ω(N log k) comparisons. The Sliding Min-Heap algorithm matches this lower bound exactly.
Try it yourself
- What happens if k ≥ N? How does
int initial_window = min(n, k + 1)prevent out-of-bounds access? - Can we use C++ STL
std::make_heap,std::push_heap, andstd::pop_heapon a raw vector of size k + 1 to avoid priority queue dynamic allocation?
๐น Advanced Systems Topic: TCP Packet Stream Reordering (Jitter Buffers)
In high throughput networking (e.g. video conferencing or real time trading feeds over UDP/RTP), packets arrive slightly out of order due to multi-path network routing. If the network guarantees that packets are delayed by at most k milliseconds or k packet sequence slots, the receiver maintains a sliding jitter min-heap of size (k + 1) to restore in-order packet processing with minimal latency overhead.
๐น Lab: High Performance Benchmark Across 10,000,000 Elements
The following benchmark executable measures sorting performance across 10,000,000 integers with k = 100 using std::chrono::high_resolution_clock:
#include <iostream>
#include <vector>
#include <chrono>
#include <algorithm>
#include <queue>
#include <random>
using namespace std;
using namespace std::chrono;
void bench_std_sort(vector<int> nums) {
sort(nums.begin(), nums.end());
}
void bench_min_heap(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> pq;
int n = nums.size(), win = min(n, k + 1);
for (int i = 0; i < win; ++i) pq.push(nums[i]);
int w = 0;
for (int i = k + 1; i < n; ++i) {
nums[w++] = pq.top(); pq.pop(); pq.push(nums[i]);
}
while (!pq.empty()) { nums[w++] = pq.top(); pq.pop(); }
}
int main() {
const size_t N = 10'000'000;
const int K = 100;
cout << "Generating " << N << "-element array with k = " << K << " displacement...\n";
vector<int> data(N);
for (size_t i = 0; i < N; ++i) data[i] = static_cast<int>(i);
// Add random perturbation within [-K, K]
mt19937 rng(42);
for (size_t i = 0; i < N; ++i) {
int offset = (rng() % (2 * K + 1)) - K;
int target = max(0, min(static_cast<int>(N - 1), static_cast<int>(i) + offset));
swap(data[i], data[target]);
}
auto d1 = data;
auto t0 = high_resolution_clock::now();
bench_std_sort(d1);
auto t1 = high_resolution_clock::now();
cout << "1. std::sort O(N log N): " << duration_cast<milliseconds>(t1 - t0).count() << " ms\n";
auto d2 = data;
auto t2 = high_resolution_clock::now();
bench_min_heap(d2, K);
auto t3 = high_resolution_clock::now();
cout << "2. Optimal Min-Heap O(N log k): " << duration_cast<milliseconds>(t3 - t2).count() << " ms ← ~3x FASTER!\n";
return 0;
}
Benchmark Results
Generating 10000000-element array with k = 100 displacement...
1. std::sort O(N log N): 680 ms
2. Optimal Min-Heap O(N log k): 225 ms ← ~3x FASTER!
๐น Comparison Matrix of All Approaches
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Approach โ Time Complexity โ Space Complexity โ Comparisons per Element โ Optimal Use Case โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 1. Full Introsort (std::sort) โ O(N * log N) โ O(log N) โ ~24 comparisons โ k ≈ N (General Sort) โ
โ 2. Bounded Insertion Sort โ O(N * k) โ O(1) โ ~k / 2 shifts โ k <= 16 (Tiny k) โ
โ 3. Optimal Sliding Min-Heap (k + 1) โ O(N * log k) โ O(k) โ ~logโ(k) comparisons โ 16 < k < N (Gold Standard)โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโ๐น Frequently Asked Questions (FAQs)
Q1: Why does the Min-Heap need size (k + 1) rather than size k?
An element at sorted index 0 could have started anywhere from index 0 up to index k. That constitutes exactly k + 1 candidate elements ([0, 1, 2, ..., k]). If the heap were only of size k, the true minimum could be left unexamined in the array, violating correctness.
Q2: When is Bounded Insertion Sort preferred over Min-Heap?
When k is tiny (e.g. k ≤ 8), Bounded Insertion Sort runs in O(1) auxiliary space with pure sequential L1 cache accesses and zero pointer/heap overhead, often executing faster than a priority queue.
Q3: Can we sort a K-Sorted Linked List using this approach?
Yes! Maintain a Min-Heap of size k + 1 containing ListNode* pointers. Pop the minimum node, attach it to the tail of the sorted list, and push the next node from the original list into the heap.
Q4: What if k is not known in advance?
If k is unknown, you can start with an initial guess k = 1 and double k dynamically (Galloping / Exponential search) whenever an element violates the current window invariant, or simply fall back to std::sort.
Q5: Why is the sliding heap space complexity O(k) instead of O(N)?
Because at any moment during the sliding traversal, the priority queue holds at most k + 1 elements. Once an element is popped, its slot is reused by the next incoming element, keeping the memory footprint strictly bounded by O(k).
Q6: How does this relate to external merge sort on disk?
External merge sort divides massive datasets into sorted runs and merges them using a K-way min-heap in RAM. The sliding min-heap technique applies identical streaming merge mechanics.
Q7: Can we use std::push_heap and std::pop_heap instead of std::priority_queue?
Yes. Using raw vector storage with std::push_heap and std::pop_heap eliminates the dynamic memory allocation overhead of std::priority_queue, resulting in even faster execution times.
Q8: What is the behavior if the array contains duplicate elements?
The Min-Heap algorithm is fully stable with respect to duplicate values. Duplicates with identical keys will simply be popped in sequence, correctly sorted without altering the algorithm’s time or space complexity.
๐น Wrapping Up
Sorting a K-Sorted Array in C++ demonstrates how exploiting localized structural constraints reduces algorithmic complexity from O(N · log N) down to an optimal O(N · log k) time and O(k) space. Continue expanding your data structures and algorithms mastery with our tutorials on Matrix Median, Replace Each Element by Its Rank, and Find Minimum in Rotated Sorted Array.