Embedded DSA Interview Questions: Heap and Sorting (Q189–Q218)

Embedded DSA Interview Questions: Heap and Sorting (Q189–Q218)

Part 6 of the seven-part Embedded Firmware DSA Interview Guide — 30 questions answered at full depth for embedded, firmware and senior technical roles.

Sorting algorithms interview questions for embedded roles are rarely about reproducing quick sort from memory. They are about which sort you would actually ship when RAM is fixed, writes are expensive, and a worst case has to go into the design document. Thirty questions on heaps and sorting, answered that way.


Heap and sorting algorithms interview questions for embedded roles


Questions 189 to 218, explained fully.


Section 11: Heap


Q189. What is a max heap?

Say this out loud A complete binary tree where every parent is greater than or equal to both of its children. The largest element is therefore always at the root and readable in O(1). Because the tree is complete, it is stored in a flat array with no pointers at all.

The full explanation

Two rules, and both must hold:

  1. Shape rule. The tree is complete: every level full except possibly the last, which fills from the left. This is what permits the array representation from Q155.
  2. Order rule. parent >= child for every node. Note this is a partial order, not a total one. A heap is not sorted. Siblings have no defined relationship to each other.
            50
          /    \
        30      40
       /  \    /
     10   20  35

array: [50, 30, 40, 10, 20, 35]
index:   0   1   2   3   4   5

Check the index arithmetic: node 30 is at index 1, so its children are at 3 and 4, which are 10 and 20. Correct. Node 40 is at index 2, children at 5 and 6, and 6 is past the end so it has only one child, 35. Correct.

#define HEAP_MAX 64

typedef struct {
    int data[HEAP_MAX];
    int size;
} heap_t;

static inline int parent(int i) { return (i - 1) / 2; }
static inline int lchild(int i) { return 2 * i + 1; }
static inline int rchild(int i) { return 2 * i + 2; }

The partial order point matters. In the heap above, 35 is greater than 30, yet 30 is closer to the root. That is perfectly legal, because they are in different subtrees. Candidates who expect a heap to be sorted get confused when they print the array and it looks wrong. Say explicitly that a heap only guarantees the root, nothing else.

Why the array representation is the whole appeal for firmware. No left and right pointers means 4 bytes per element instead of 12, no allocation, contiguous memory, and the entire structure can be a fixed array in .bss whose size is visible in the linker map.


Q190. What is a min heap?

Identical, with the comparison inverted: every parent is less than or equal to its children, so the smallest element is at the root.

            10
          /    \
        20      15
       /  \
     30   25

array: [10, 20, 15, 30, 25]

The only implementation difference is one comparison operator. Good code parameterises it:

typedef bool (*cmp_fn)(int a, int b);        /* returns true if a should be above b */

static bool max_cmp(int a, int b) { return a > b; }
static bool min_cmp(int a, int b) { return a < b; }

Or, if you only ever need one and want zero indirection cost, negate the keys on the way in and out.

Which one to use. A min heap is what you want for a scheduler or timer queue, because the interesting event is the one with the earliest deadline or the smallest virtual runtime. A max heap is what heapsort uses to produce ascending order, for the reason in Q194.

The two classic uses worth naming

  • Kth largest element: keep a min heap of size k. Each new element that exceeds the root replaces it and sifts down. At the end the root is the kth largest. O(n log k) time and O(k) space, which beats sorting when k is small.
  • Merging k sorted streams: a min heap holding one element from each stream. Pop the smallest, emit it, push the next from that stream. O(n log k).

Q191. What does heapify do and what does it cost?

Say this out loud Heapify, or sift down, restores the heap property at one node assuming both its subtrees are already valid heaps. It compares the node with its children, swaps with the larger child if needed, and repeats downward. O(log n). Build-heap applies it to every internal node from the bottom up and is O(n), not O(n log n).

Sift down

void sift_down(heap_t *h, int i) {
    for (;;) {
        int largest = i;
        int l = lchild(i), r = rchild(i);

        if (l < h->size && h->data[l] > h->data[largest]) largest = l;
        if (r < h->size && h->data[r] > h->data[largest]) largest = r;

        if (largest == i) return;                       /* already correct, stop */

        int t = h->data[i]; h->data[i] = h->data[largest]; h->data[largest] = t;
        i = largest;                                    /* follow the element down */
    }
}

Trace. Array [10, 50, 40, 30, 20], sift down from index 0:

        10                    50                    50
      /    \               /     \               /     \
    50      40    -->    10       40    -->    30       40
   /  \                 /  \                  /  \
  30   20             30    20              10    20

step 1: children of 10 are 50 and 40. Largest is 50, swap.
step 2: 10 is now at index 1, children are 30 and 20. Largest is 30, swap.
step 3: 10 is at index 3, no children. Done.

Sift up, used by insert:

void sift_up(heap_t *h, int i) {
    while (i > 0 && h->data[i] > h->data[parent(i)]) {
        int p = parent(i);
        int t = h->data[i]; h->data[i] = h->data[p]; h->data[p] = t;
        i = p;
    }
}

Build heap, and the O(n) proof that is the real question

void build_heap(heap_t *h) {
    for (int i = h->size / 2 - 1; i >= 0; i--) {   /* last internal node, downward */
        sift_down(h, i);
    }
}

Starting at size/2 - 1 skips every leaf, because leaves are already valid heaps of one element. That is half the array skipped for free.

Why it is O(n) and not O(n log n). The naive bound says n nodes times O(log n) each. But almost all nodes are near the bottom, where sift down travels almost no distance.

Level from the bottom Nodes at that level Max sift distance Work
0, the leaves n/2 0 0
1 n/4 1 n/4
2 n/8 2 2n/8
3 n/16 3 3n/16
h, the root 1 h h

Total work is n * sum(k / 2^(k+1)) for k from 0 upward. That sum converges to 1, so the total is O(n). The nodes that could travel far are the rare ones near the top, and the many nodes near the bottom barely move.

Being able to give that table and say “the sum converges” is a strong answer. It is one of the few places where the obvious bound is genuinely loose.

The consequence: building a heap from an array is O(n), which is why heapsort’s setup phase is cheaper than its sorting phase.


Q192. How do you insert into a heap?

Say this out loud Append the new element at the end of the array, which preserves completeness, then sift it up until its parent is larger. O(log n) worst case.

bool heap_insert(heap_t *h, int v) {
    if (h->size >= HEAP_MAX) return false;      /* fixed capacity: report, do not grow */
    h->data[h->size] = v;
    sift_up(h, h->size);
    h->size++;
    return true;
}

Trace. Insert 60 into [50, 30, 40, 10, 20]:

append at index 5:
        50                     50                    60
      /    \                 /    \                /    \
    30      40      -->    30      60    -->     30      50
   /  \    /              /  \    /             /  \    /
 10   20  60            10   20  40           10   20  40

60 > its parent 40, swap.
60 > its new parent 50, swap.
60 is at the root, stop.

Two swaps for six elements. In a heap of a million elements the maximum is 20.

Why appending is correct. The next free array slot is exactly the next position in the complete tree’s fill order, so completeness is maintained automatically with no reasoning required. That is a second benefit of the array representation.

Average case is better than worst case. Half the nodes are leaves, so a randomly valued insert usually stops after one or two comparisons. The amortized cost of inserting n elements one at a time is O(n) in practice, though the worst case per insert remains O(log n).


Q193. How do you extract the root from a heap?

Say this out loud Take the root as the result, move the last element into the root position, shrink the size, and sift that element down. O(log n).

bool heap_extract_max(heap_t *h, int *out) {
    if (h->size == 0) return false;

    *out = h->data[0];                    /* the answer */
    h->data[0] = h->data[h->size - 1];    /* last element becomes the new root */
    h->size--;
    sift_down(h, 0);
    return true;
}

Why the last element and not one of the children. Promoting a child would leave a hole in the middle of the tree and break completeness, so the array indexing would stop working. Moving the last element keeps the shape valid immediately, and the order rule is then repaired by one sift down.

Trace. Extract from [60, 30, 50, 10, 20, 40]:

result is 60.
move 40 (the last) to the root, size becomes 5:

        40                     50
      /    \                 /    \
    30      50     -->     30      40
   /  \                   /  \
 10   20                10   20

children of 40 are 30 and 50. Largest is 50, swap.
40 at index 2 has no children now. Done.

Deleting an arbitrary element, which is the follow up:

void heap_delete_at(heap_t *h, int i) {
    h->data[i] = h->data[h->size - 1];
    h->size--;
    if (i < h->size) {
        sift_down(h, i);
        sift_up(h, i);          /* the replacement may be too LARGE for this position */
    }
}

Both directions are needed, because the element moved in from the end could be smaller than the children, requiring a sift down, or larger than the parent, requiring a sift up. Only one of the two will actually do work, but you cannot know which in advance.

The problem with arbitrary deletion. Finding element i costs O(n), because a heap gives no search structure. That is why priority queues supporting cancellation, such as a timer queue where a timer can be stopped, must store each element’s current index inside the element itself and update it on every swap. That index-back-reference is the standard solution and mentioning it shows practical experience.


Q194. How does heap sort work and when do you choose it?

Say this out loud Build a max heap from the array in O(n), then repeatedly swap the root with the last unsorted element and sift down over the shrinking heap. O(n log n) in all cases, in place with O(1) extra space, and not stable.

static void sift_down_n(int *a, int n, int i) {
    for (;;) {
        int largest = i, l = 2*i + 1, r = 2*i + 2;
        if (l < n && a[l] > a[largest]) largest = l;
        if (r < n && a[r] > a[largest]) largest = r;
        if (largest == i) return;
        int t = a[i]; a[i] = a[largest]; a[largest] = t;
        i = largest;
    }
}

void heap_sort(int *a, int n) {
    for (int i = n/2 - 1; i >= 0; i--) sift_down_n(a, n, i);   /* build, O(n) */

    for (int end = n - 1; end > 0; end--) {
        int t = a[0]; a[0] = a[end]; a[end] = t;               /* largest to the back */
        sift_down_n(a, end, 0);                                /* restore over the rest */
    }
}

Why a max heap gives ascending order. The root is the largest remaining element, and you swap it to the end of the unsorted region. The sorted portion grows from the right, filled with progressively smaller values, so the final array is ascending. Using a min heap here would produce descending order.

Trace on [4, 10, 3, 5, 1]

build heap:
  i=1: sift down 10, children 5 and 1, already largest -> no change
  i=0: sift down 4, children 10 and 3, swap with 10
       then 4 at index 1, children 5 and 1, swap with 5
  heap: [10, 5, 3, 4, 1]

extract phase:
  swap a[0] and a[4]:  [1, 5, 3, 4 | 10]   sift over first 4 -> [5, 4, 3, 1 | 10]
  swap a[0] and a[3]:  [1, 4, 3 | 5, 10]   sift -> [4, 1, 3 | 5, 10]
  swap a[0] and a[2]:  [3, 1 | 4, 5, 10]   sift -> [3, 1 | 4, 5, 10]
  swap a[0] and a[1]:  [1 | 3, 4, 5, 10]

result: [1, 3, 4, 5, 10]

The properties table, which is why it matters for firmware

Property Heap sort
Time, best, average, worst O(n log n) in all three
Space O(1), fully in place
Stable No
Recursive No, purely iterative
Adaptive No, sorted input is not faster
Cache behaviour Poor, sift down jumps by powers of two

Why heap sort is the right choice for hard real time. Quick sort can degrade to O(n squared). Merge sort needs O(n) extra memory. Heap sort has a guaranteed O(n log n) bound, uses no extra memory, and uses no recursion, so it has no stack depth risk. When you must state a worst case number in a design document, heap sort is the sort that lets you do it. In practice it is roughly two to three times slower than quick sort on typical data because of the cache behaviour, and that is the trade you are making.


Q195. How does a heap implement a priority queue?

Covered from the queue side in Q142. The heap-specific view:

Operation Heap implementation Cost
peek highest priority read data[0] O(1)
push append, sift up O(log n)
pop swap root and last, shrink, sift down O(log n)
build from an array bottom up heapify O(n)
Change a priority update in place, then sift up or down O(log n) once located
Find an arbitrary element linear scan O(n)

When a heap is the right choice is when the number of distinct priorities is large or continuous, such as timestamps or deadlines.

When the bucket approach from Q142 is better is when priorities come from a small fixed set, such as 8 or 32 RTOS priority levels. Then an array of FIFOs plus a CLZ on a ready mask makes everything O(1) and deterministic, which beats O(log n) for a scheduler.

Stating both, and the condition that selects between them, is the complete answer.

Stability. A plain heap is not stable, so two items with equal priority can come out in either order. If FIFO order within a priority matters, and for a scheduler it usually does, store a monotonically increasing sequence number alongside the priority and compare it as a tiebreaker.


Q196. What are the time complexities of heap operations?

Operation Time Notes
Find max, in a max heap O(1) it is data[0]
Find min, in a max heap O(n) it is a leaf, but you do not know which one
Insert O(log n) worst, O(1) average most inserts stop near the bottom
Extract root O(log n) always, the last element usually sifts a long way
Delete arbitrary O(log n) once located, O(n) to locate unless you track indices
Increase or decrease key O(log n) once located
Build from n elements O(n) bottom up. Inserting one at a time is O(n log n)
Heap sort O(n log n) all cases
Merge two heaps O(n + m) rebuild. A binomial or Fibonacci heap does better
Search for a value O(n) a heap provides no search structure
Space O(n) with zero per element overhead the array representation

The two rows that get asked about most

Build is O(n), inserting n times is O(n log n). The difference is the argument in Q191. If you have all the data up front, always build rather than insert repeatedly.

Finding the minimum in a max heap is O(n). Candidates often assume it is at the end of the array. It is somewhere in the leaves, which is the last n/2 entries, but which one is unknown, so you scan them. Answering “O(n), but only over the last half of the array” is the precise version.


Q197. How do you build a timer scheduler on a min heap?

Say this out loud A min heap keyed by deadline or next-run time. The root is always the next event to fire, so the tick handler only examines data[0], which is O(1). Adding a timer is O(log n) and firing one is O(log n).

typedef struct {
    uint32_t expiry_tick;
    void   (*callback)(void *);
    void    *arg;
    uint32_t period;              /* 0 means one shot */
    int      heap_index;          /* self reference, so cancel is O(log n) not O(n) */
} timer_t;

typedef struct {
    timer_t *heap[MAX_TIMERS];    /* min heap ordered by expiry_tick */
    int      count;
} timer_queue_t;

/* called from the tick ISR or a timer task */
void timer_tick(timer_queue_t *q, uint32_t now) {
    while (q->count > 0 && q->heap[0]->expiry_tick <= now) {
        timer_t *t = q->heap[0];
        heap_pop(q);
        t->callback(t->arg);
        if (t->period) {
            t->expiry_tick = now + t->period;
            heap_push(q, t);          /* reschedule a periodic timer */
        }
    }
}

The heap_index field is the design point. Every swap inside sift up and sift down must update it:

static void heap_swap(timer_queue_t *q, int i, int j) {
    timer_t *t = q->heap[i]; q->heap[i] = q->heap[j]; q->heap[j] = t;
    q->heap[i]->heap_index = i;
    q->heap[j]->heap_index = j;
}

Without it, cancelling a timer requires an O(n) search to find where it lives. With it, cancellation is O(log n) and, more importantly, bounded. In a system with hundreds of timers that is the difference between a usable and an unusable design.

Heap versus sorted linked list for timers

Sorted list Min heap
Insert O(n) O(log n)
Peek next expiry O(1) O(1)
Remove next O(1) O(log n)
Cancel a known timer O(1) with a doubly linked list O(log n)
Memory 2 pointers per timer one array slot, no pointers
Determinism insert varies with n all operations bounded by log n

FreeRTOS uses a sorted list, not a heap, because with a modest timer count the O(n) insert is small and the O(1) removal on every tick is what runs most often. Linux uses a timer wheel for most timers and a red-black tree for high resolution ones. Knowing that different systems make different choices, and being able to explain why, is worth more than defending one structure.

The timer wheel, worth naming. Buckets indexed by expiry % wheel_size, giving O(1) insert and O(1) tick processing at the cost of a cascade step when timers wrap around. It is the right answer when you have thousands of timers and most are cancelled before they fire, which is exactly the network stack case.


Q198. Where are heaps used in embedded systems?

Use Why a heap
Software timer management Next expiry is always at the root
Deadline based scheduling, EDF The earliest deadline is O(1) to find
Event queues with priority Ordered dispatch without a full sort
Top-k sensor readings A size-k heap uses O(k) memory instead of O(n)
Merging k sorted log streams One element per stream in the heap
Bandwidth or rate limiting Next token refill time at the root
Dijkstra and A* path planning The frontier is a priority queue

The firmware advantages worth stating together:

  • Zero pointer overhead, since the array representation stores only the data.
  • Fixed capacity known at build time, so the memory appears in the linker map.
  • No allocation and no fragmentation.
  • No recursion, so no stack depth risk, provided you write sift down iteratively.
  • All operations bounded by O(log n), which for 1000 timers is 10 steps.

The disadvantages to acknowledge:

  • No search. Locating an arbitrary element is O(n) without an index back reference.
  • Poor cache behaviour, because sift down jumps by powers of two through the array. On a Cortex M with no cache this costs nothing, and on an M7 or an A series core it is measurable.
  • Not stable, so equal priorities need an explicit tiebreaker.

Section 12: Sorting


Q199. How does bubble sort work and is it ever the right choice?

Say this out loud Repeatedly walk the array swapping adjacent out-of-order pairs. Each pass bubbles the largest remaining element to the end. O(n squared) in general, but with an early exit flag it is O(n) on already sorted input, which is its one redeeming property.

void bubble_sort(int *a, int n) {
    for (int i = 0; i < n - 1; i++) {
        bool swapped = false;
        for (int j = 0; j < n - 1 - i; j++) {     /* -i : the tail is already sorted */
            if (a[j] > a[j + 1]) {
                int t = a[j]; a[j] = a[j+1]; a[j+1] = t;
                swapped = true;
            }
        }
        if (!swapped) return;                     /* nothing moved: fully sorted */
    }
}

Trace on [5, 1, 4, 2]

pass 1: [1,5,4,2] -> [1,4,5,2] -> [1,4,2,5]     5 is now in place
pass 2: [1,4,2,5] -> [1,2,4,5]                  4 is now in place
pass 3: no swaps, early exit

The two details that matter. The - i in the inner bound, because the last i elements are already final and rechecking them is wasted work. And the swapped flag, which turns the best case from O(n squared) into O(n).

Case Complexity
Best, already sorted O(n) with the flag
Average O(n squared)
Worst, reverse sorted O(n squared)
Space O(1)
Stable Yes

Honest assessment. Bubble sort is never the right answer in production. It is asked because it is the simplest thing to write correctly under pressure, and because the early exit flag and the shrinking inner bound reveal whether you actually thought about it or reproduced it from memory. If asked for the best small-array sort, say insertion sort and explain why, since it does strictly less work on the same inputs.


Q200. How does selection sort work and when does it win?

Say this out loud Find the minimum of the unsorted region and swap it into position. Always O(n squared) comparisons regardless of input, but it performs at most n swaps, which is the fewest of any comparison sort.

void selection_sort(int *a, int n) {
    for (int i = 0; i < n - 1; i++) {
        int min = i;
        for (int j = i + 1; j < n; j++) {
            if (a[j] < a[min]) min = j;
        }
        if (min != i) {
            int t = a[i]; a[i] = a[min]; a[min] = t;
        }
    }
}

Trace on [64, 25, 12, 22]

i=0: min is 12 at index 2, swap -> [12, 25, 64, 22]
i=1: min is 22 at index 3, swap -> [12, 22, 64, 25]
i=2: min is 25 at index 3, swap -> [12, 22, 25, 64]
Case Complexity
Best, average, worst O(n squared) always, no early exit possible
Swaps at most n minus 1
Space O(1)
Stable No, in the swap-based version

The one place it wins, and it is a firmware place. When a write is far more expensive than a read. Writing to EEPROM or flash costs milliseconds and consumes a limited erase-write cycle budget, while reading is nearly free. Selection sort performs at most n writes where insertion sort may perform O(n squared). If you must sort records in place in EEPROM, selection sort is the correct choice specifically because of the write count. That answer distinguishes you immediately, because it is a real engineering reason rather than a textbook property.

Why it is not stable. Swapping a distant minimum into position can jump one equal element over another. [2a, 2b, 1] becomes [1, 2b, 2a], reversing the two 2s. A linked-list version that moves nodes instead of swapping values can be stable.


Q201. Why is insertion sort fast on nearly sorted data?

Say this out loud Take each element and slide it left into its correct place among the already sorted prefix. O(n squared) worst case but O(n) on nearly sorted data, stable, in place, and the fastest option for small arrays, which is why every production sort falls back to it.

void insertion_sort(int *a, int n) {
    for (int i = 1; i < n; i++) {
        int key = a[i];
        int j = i - 1;
        while (j >= 0 && a[j] > key) {
            a[j + 1] = a[j];          /* shift right, do not swap */
            j--;
        }
        a[j + 1] = key;
    }
}

Shifting rather than swapping is the optimisation. A swap is three assignments. Shifting is one per element moved, plus one final placement. That is roughly a threefold reduction in memory traffic over the naive swap version.

Trace on [12, 11, 13, 5]

i=1, key=11: 12 > 11, shift -> [12,12,13,5], place -> [11,12,13,5]
i=2, key=13: 12 < 13, no shift                        [11,12,13,5]
i=3, key=5 : 13,12,11 all > 5, shift three times   -> [5,11,12,13]
Case Complexity
Best, already sorted O(n), the inner loop never runs
Average O(n squared)
Worst, reverse sorted O(n squared)
Space O(1)
Stable Yes, because > stops at an equal element
Adaptive Yes, cost is proportional to the number of inversions

Why it beats everything else for small n. No recursion, no function call overhead, sequential memory access, and a very tight inner loop. The crossover against quick sort is typically somewhere between 10 and 32 elements depending on the core, which is why std::sort switches to insertion sort below 16 and glibc’s qsort does the same.

Using > and not >= in the loop condition is what makes it stable. With >= the loop keeps shifting past equal elements, so the new element ends up before its equals, reversing their order. One character decides stability.

Firmware relevance. This is the sort to write for anything under about 50 elements: a list of active connections, calibration points, a menu ordering. It compiles to a handful of instructions, has no worst case surprise at that size, and needs no stack.


Q202. How does merge sort work and what does it cost in memory?

Say this out loud Divide the array in half, sort each half recursively, then merge the two sorted halves. O(n log n) guaranteed in all cases, stable, but it needs O(n) auxiliary memory for the merge. It is the natural sort for linked lists, where the extra memory cost disappears.

static void merge(int *a, int lo, int mid, int hi, int *tmp) {
    int i = lo, j = mid + 1, k = lo;

    while (i <= mid && j <= hi)
        tmp[k++] = (a[i] <= a[j]) ? a[i++] : a[j++];    /* <= keeps it stable */

    while (i <= mid) tmp[k++] = a[i++];
    while (j <= hi)  tmp[k++] = a[j++];

    for (int x = lo; x <= hi; x++) a[x] = tmp[x];
}

void merge_sort(int *a, int lo, int hi, int *tmp) {
    if (lo >= hi) return;
    int mid = lo + (hi - lo) / 2;
    merge_sort(a, lo, mid, tmp);
    merge_sort(a, mid + 1, hi, tmp);
    if (a[mid] <= a[mid + 1]) return;     /* already ordered across the boundary, skip */
    merge(a, lo, mid, hi, tmp);
}

That a[mid] <= a[mid+1] check is a free optimisation: if the largest of the left half is already no bigger than the smallest of the right half, the merge would just copy, so skip it. On nearly sorted input this turns the whole sort into O(n).

The recursion tree

                [38,27,43,3,9,82,10]
              /                      \
      [38,27,43,3]                [9,82,10]
        /       \                  /      \
   [38,27]     [43,3]          [9,82]    [10]
    /   \       /   \           /   \
  [38] [27]  [43]  [3]        [9]  [82]

merge upward:
  [27,38]  [3,43]  [9,82]  [10]
  [3,27,38,43]     [9,10,82]
  [3,9,10,27,38,43,82]

Each level does O(n) total merging work, and there are log n levels, so O(n log n). That picture is the recursion-tree argument from Q87.

Case Complexity
Best, average, worst O(n log n) in all three
Space O(n) auxiliary, plus O(log n) stack
Stable Yes
Adaptive Only with the boundary check above

Allocating tmp once. Allocating inside merge would call malloc at every level of the recursion. Allocate one buffer at the top and pass it down, or use a static buffer in firmware. That detail is worth pointing out because the naive implementation is a performance and fragmentation disaster.


Q203. How does quick sort work and what is its worst case?

Say this out loud Choose a pivot, partition the array so everything smaller is on the left and everything larger on the right, then recurse on both sides. O(n log n) average with excellent constants and in-place operation, but O(n squared) worst case if the pivot choice is poor.

Lomuto partition, easier to write correctly

static int partition_lomuto(int *a, int lo, int hi) {
    int pivot = a[hi];                   /* last element as the pivot */
    int i = lo - 1;                      /* boundary of the "smaller" region */

    for (int j = lo; j < hi; j++) {
        if (a[j] <= pivot) {
            i++;
            int t = a[i]; a[i] = a[j]; a[j] = t;
        }
    }
    int t = a[i+1]; a[i+1] = a[hi]; a[hi] = t;   /* pivot into its final place */
    return i + 1;
}

void quick_sort(int *a, int lo, int hi) {
    if (lo >= hi) return;
    int p = partition_lomuto(a, lo, hi);
    quick_sort(a, lo, p - 1);
    quick_sort(a, p + 1, hi);
}

Trace of one partition on [7, 2, 1, 6, 8, 5, 3, 4], pivot 4

j a[j] <= 4 Action Array
0 7 no [7,2,1,6,8,5,3,4]
1 2 yes i=0, swap a[0] and a[1] [2,7,1,6,8,5,3,4]
2 1 yes i=1, swap a[1] and a[2] [2,1,7,6,8,5,3,4]
3 6 no
4 8 no
5 5 no
6 3 yes i=2, swap a[2] and a[6] [2,1,3,6,8,5,7,4]
end swap a[3] and a[7] [2,1,3,4,8,5,7,6]

Pivot 4 is at index 3, with everything smaller to its left. Correct.

Hoare partition, which is what real implementations use because it does about three times fewer swaps:

static int partition_hoare(int *a, int lo, int hi) {
    int pivot = a[lo + (hi - lo) / 2];
    int i = lo - 1, j = hi + 1;
    for (;;) {
        do { i++; } while (a[i] < pivot);
        do { j--; } while (a[j] > pivot);
        if (i >= j) return j;                 /* returns a SPLIT point, not a final index */
        int t = a[i]; a[i] = a[j]; a[j] = t;
    }
}
/* note the different recursion: quick_sort(a, lo, p); quick_sort(a, p + 1, hi); */

Hoare returns a division point rather than the pivot’s final position, so the recursive calls differ from Lomuto’s. Mixing the two conventions produces an infinite loop, and that is a common bug.

Case Complexity
Best O(n log n), pivot always splits evenly
Average O(n log n), with the smallest constant of any comparison sort
Worst O(n squared), pivot always the min or max
Space O(log n) stack average, O(n) worst
Stable No

Tail call elimination, which bounds the stack

void quick_sort_bounded(int *a, int lo, int hi) {
    while (lo < hi) {
        int p = partition_lomuto(a, lo, hi);
        if (p - lo < hi - p) {                 /* recurse on the SMALLER side */
            quick_sort_bounded(a, lo, p - 1);
            lo = p + 1;                        /* loop on the larger side */
        } else {
            quick_sort_bounded(a, p + 1, hi);
            hi = p - 1;
        }
    }
}

Always recursing on the smaller partition and looping on the larger bounds the stack depth at O(log n) even in the worst case. That single change makes quick sort acceptable in firmware where the naive version is not, and volunteering it is a strong senior signal.


Q204. When do you choose heap sort over quick sort?

Covered fully in Q194. The one-line role: the sort you choose when you must guarantee O(n log n) with O(1) memory and no recursion.


Q205. How does counting sort beat O(n log n)?

Say this out loud Not a comparison sort. Count how many times each value occurs, then use a running total to place each element directly at its final index. O(n + k) where k is the range of values, which beats O(n log n) when the range is small. Requires integer keys in a known bounded range.

void counting_sort(int *a, int n, int max_val) {
    int count[max_val + 1];
    memset(count, 0, sizeof count);

    for (int i = 0; i < n; i++) count[a[i]]++;              /* tally */

    for (int i = 1; i <= max_val; i++) count[i] += count[i-1];  /* running total */

    int out[n];
    for (int i = n - 1; i >= 0; i--) {                      /* BACKWARDS, for stability */
        out[--count[a[i]]] = a[i];
    }

    memcpy(a, out, n * sizeof(int));
}

Trace on [4, 2, 2, 8, 3] with max 8

counts:     index: 0 1 2 3 4 5 6 7 8
                   0 0 2 1 1 0 0 0 1

running:           0 0 2 3 4 4 4 4 5

place backwards:
  a[4]=3: count[3] is 3, decrement to 2, out[2]=3
  a[3]=8: count[8] is 5, decrement to 4, out[4]=8
  a[2]=2: count[2] is 2, decrement to 1, out[1]=2
  a[1]=2: count[2] is 1, decrement to 0, out[0]=2
  a[0]=4: count[4] is 4, decrement to 3, out[3]=4

result: [2, 2, 3, 4, 8]

Why the final loop runs backwards. Iterating the input from the end and decrementing the count places the last occurrence of a value at the highest of its output slots, which preserves the original relative order of equal elements. Running forwards would reverse them and break stability. That is the detail interviewers check.

Case Complexity
Time O(n + k)
Space O(n + k)
Stable Yes, with the backward loop
Comparison based No, so it is not bound by the O(n log n) lower limit

When it is the correct choice in firmware. Sorting ADC readings that are 12 bit, so k is 4096. Sorting bytes, where k is 256 and the count array is 1 KB. Sorting by a small enumerated priority. If k is comparable to n or smaller, this is dramatically faster than any comparison sort, and if k is huge, such as full 32 bit values, the count array is impossible and this is unusable. Stating that condition is the answer.


Q206. How does radix sort work?

Say this out loud Sort by one digit at a time, from least significant to most, using a stable sort such as counting sort for each pass. O(d times (n + b)) where d is the number of digits and b is the base. It works because each pass preserves the ordering established by all previous passes, which requires the inner sort to be stable.

void radix_sort(uint32_t *a, int n) {
    uint32_t *out = malloc(n * sizeof *out);

    for (int shift = 0; shift < 32; shift += 8) {     /* 4 passes of 8 bits */
        int count[256] = {0};

        for (int i = 0; i < n; i++) count[(a[i] >> shift) & 0xFF]++;
        for (int i = 1; i < 256; i++) count[i] += count[i-1];
        for (int i = n - 1; i >= 0; i--)
            out[--count[(a[i] >> shift) & 0xFF]] = a[i];

        memcpy(a, out, n * sizeof *a);
    }
    free(out);
}

Trace on [170, 45, 75, 90, 802, 24, 2, 66], base 10

by 1s digit:   170, 90, 802, 2, 24, 45, 75, 66
by 10s digit:  802, 2, 24, 45, 66, 170, 75, 90
by 100s digit: 2, 24, 45, 66, 75, 90, 170, 802

Watch the 10s pass: 802 and 2 both have a 10s digit of 0, and 802 came first from the previous pass, so it stays first. That is stability doing the work. If the inner sort were unstable, the 1s ordering would be destroyed and the result would be wrong.

Choosing the base. Base 256, meaning 8 bits per pass, gives 4 passes for a 32 bit key with a 1 KB count array. Base 65536 gives 2 passes but a 256 KB count array. On a microcontroller, base 16 with 8 passes and a 64 byte count array may be the right point. That is a genuine memory-versus-passes trade you can discuss.

Signed integers and floats need care. Two’s complement negatives have the top bit set, so a straight radix sort places them after positives. Flip the sign bit before sorting and flip it back afterwards. For IEEE floats, flip the sign bit for positives and all bits for negatives, which maps them to a monotonic unsigned ordering. Knowing this exists is a good bonus.

Firmware relevance. Radix sort is the fastest way to sort large sets of fixed-width integer keys, such as timestamps or sensor IDs, and it is used in packet classification. The cost is O(n) extra memory, which often rules it out on small parts.


Q207. How does bucket sort work and when does it degrade?

Say this out loud Distribute elements into buckets covering equal sub-ranges of the input, sort each bucket individually, then concatenate. O(n) average when the input is uniformly distributed, O(n squared) when everything lands in one bucket.

void bucket_sort(float *a, int n) {          /* values in [0, 1) */
    bucket_t buckets[n];
    for (int i = 0; i < n; i++) bucket_init(&buckets[i]);

    for (int i = 0; i < n; i++)
        bucket_add(&buckets[(int)(n * a[i])], a[i]);   /* index by value */

    int k = 0;
    for (int i = 0; i < n; i++) {
        insertion_sort_bucket(&buckets[i]);            /* each bucket is small */
        for (int j = 0; j < buckets[i].count; j++) a[k++] = buckets[i].items[j];
    }
}

Why it can beat O(n log n). With uniform input, each of the n buckets holds about one element, so sorting each is O(1) and the total is O(n). The comparison-sort lower bound does not apply because the bucket assignment uses the value directly rather than comparing.

Why it degrades. Skewed input puts everything in one bucket, and you are left running insertion sort on the whole array, which is O(n squared). The distribution assumption is doing all the work, and it is the assumption to state explicitly.

The relationship to the others, which is a good thing to say:

  • Counting sort is bucket sort with one bucket per distinct value and no inner sort needed.
  • Radix sort is repeated bucket sort, one digit at a time.
  • Bucket sort with two buckets and a pivot boundary is essentially one level of quick sort.

Presenting them as a family rather than three unrelated algorithms shows structural understanding.


Q208. How does shell sort improve on insertion sort?

Say this out loud Insertion sort applied at decreasing gaps. Sorting elements that are far apart first moves items a long distance cheaply, so by the time the gap reaches 1 the array is nearly sorted and the final insertion pass is close to O(n). Complexity depends on the gap sequence, roughly O(n^1.3) for good sequences.

void shell_sort(int *a, int n) {
    for (int gap = n / 2; gap > 0; gap /= 2) {          /* the original Shell sequence */
        for (int i = gap; i < n; i++) {
            int key = a[i];
            int j = i;
            while (j >= gap && a[j - gap] > key) {
                a[j] = a[j - gap];
                j -= gap;
            }
            a[j] = key;
        }
    }
}

Compare with Q201: it is literally insertion sort with 1 replaced by gap, wrapped in a loop that shrinks the gap. Saying that is the fastest way to explain it.

Why the gaps help. Plain insertion sort moves an element one position per operation, so an element that belongs n places away costs n operations. With a gap of n/2, it can travel half the array in a single step. Each pass reduces the number of inversions dramatically, so the final gap-1 pass has very little left to do.

Gap sequences and their bounds

Sequence Worst case
n/2, n/4, … 1, the original O(n squared)
Knuth: 1, 4, 13, 40, … using 3k+1 O(n^1.5)
Sedgewick O(n^1.33)
Ciura, found empirically: 1, 4, 10, 23, 57, 132, 301, 701 best measured in practice
Property Shell sort
Space O(1)
Stable No, gapped moves jump over equal elements
Recursive No
Adaptive Somewhat

Where it wins, and this is a firmware answer. Shell sort is the sweet spot for medium sized arrays, roughly 50 to 1000 elements, on a memory constrained system. It needs no extra memory, no recursion, and no stack, it is about 40 lines shorter than quick sort with its pivot selection and partition, and it substantially beats insertion sort. Several embedded C libraries use it as their general purpose sort for exactly these reasons. uClibc’s qsort has historically been a shell sort.


Q209. Which sorts are stable?

Say this out loud Stable means two elements with equal keys keep their original relative order. It matters whenever you sort by one field after already having sorted by another, because stability is what preserves the earlier ordering.

Sort Stable Why or why not
Bubble Yes only adjacent swaps, and only on strict >
Insertion Yes the shift loop stops at an equal element
Merge Yes <= in the merge takes from the left half on ties
Counting Yes with the backward placement loop
Radix Yes it requires a stable inner sort to work at all
Bucket Yes if the inner sort is stable
Selection No a long-distance swap jumps equal elements
Quick No partitioning swaps distant elements
Heap No sift down moves elements arbitrarily
Shell No gapped moves jump over equal elements

Why it matters, concretely. Sort log entries by timestamp, then stably sort by severity. The result is grouped by severity with each group still in timestamp order. With an unstable sort the timestamp ordering inside each group is destroyed and you must sort by a composite key instead.

Making an unstable sort stable. Append the original index as a tiebreaker in the comparison. This always works and costs O(n) extra memory for the indices, which is why it is not the default.

The gotcha to know: std::sort is not stable. std::stable_sort is, and it uses merge sort with a temporary buffer, falling back to a slower in-place merge if allocation fails. C’s qsort is not guaranteed stable either. Knowing which library functions guarantee stability is a standard checkpoint.


Q210. Which sorts are in place?

Say this out loud In place means O(1) or O(log n) auxiliary space, not counting the input array. The distinction that matters for firmware is whether the algorithm needs a second array of size n, because on a part with 32 KB of RAM that decides whether you can sort 4000 integers at all.

Sort Auxiliary space In place
Bubble, Selection, Insertion, Shell O(1) Yes
Heap O(1) Yes
Quick O(log n) stack Yes, by the usual convention
Merge, array version O(n) No
Merge, linked list version O(log n) stack Yes
Counting O(n + k) No
Radix O(n + b) No
Bucket O(n + b) No

The convention argument. Quick sort uses O(log n) stack for recursion, so strictly it is not O(1). It is conventionally called in place because it needs no auxiliary array, and O(log n) for a million elements is 20 frames. Stating the convention rather than arguing about it is the right move.

In-place merge sort exists but the in-place merge is O(n log n) by itself, making the whole sort O(n log squared n), and the constants are bad. It is a curiosity rather than a tool.


Q211. What is the worst case for each sorting algorithm?

Sort Worst Triggered by
Bubble O(n squared) reverse sorted
Selection O(n squared) any input, it never varies
Insertion O(n squared) reverse sorted
Shell O(n squared) to O(n^1.5) depends entirely on the gap sequence
Merge O(n log n) none, it is input independent
Quick O(n squared) pivot always the min or max, so sorted input with a first or last element pivot
Heap O(n log n) none
Counting O(n + k) a huge value range makes k dominate
Radix O(d(n + b)) long keys
Bucket O(n squared) all elements in one bucket

The row that matters is quick sort. Choosing the first or last element as the pivot makes already sorted input the worst case, which is the input you are most likely to encounter in practice. Every partition splits off one element, giving n levels of recursion at O(n) work each.

Worse, the recursion depth becomes O(n), so on a target you get a stack overflow rather than merely a slow sort. Mitigations are in Q216 and the tail-elimination trick in Q203.

The security angle worth mentioning. An attacker who knows your pivot rule can construct input that forces O(n squared), which is a denial of service against any service that sorts untrusted input. This was a real vulnerability class in language runtimes, and randomised pivots or introsort are the fix.


Q212. What is the best case for each sorting algorithm?

Sort Best Condition
Bubble O(n) already sorted, and only with the early exit flag
Insertion O(n) already sorted
Selection O(n squared) never better, it always scans the full remainder
Merge O(n log n), or O(n) with the boundary check
Quick O(n log n) pivot splits evenly every time
Heap O(n log n) never better
Shell O(n log n) nearly sorted
Counting, Radix same as average input independent

Adaptive sorts are the ones that exploit existing order: insertion, bubble with the flag, shell, and Timsort. Selection and heap sort are not adaptive at all, and doing nothing useful with sorted input is a real weakness for data that is usually nearly sorted, which describes most real data.

The practical consequence. Sensor readings, timestamps, and log entries usually arrive nearly sorted. Insertion sort on that data is close to O(n) and beats quick sort’s O(n log n) for surprisingly large n. If you know your data is nearly sorted, say so and pick accordingly.


Q213. What is the average case for each sorting algorithm?

Sort Average Constant factor
Bubble O(n squared) poor
Selection O(n squared) poor comparisons, excellent swap count
Insertion O(n squared) very good for small n
Shell around O(n^1.3) good
Merge O(n log n) moderate, hurt by the copying
Quick O(n log n) the best of any comparison sort
Heap O(n log n) worse than quick sort due to cache behaviour
Counting O(n + k) excellent when k is small
Radix O(d(n + b)) excellent for fixed width keys

The point interviewers want. All three of merge, quick, and heap are O(n log n) on average, yet quick sort is typically two to three times faster in practice. Big O hides the constant, and here the constant is dominated by cache behaviour: quick sort’s partition is a sequential scan from both ends, while heap sort’s sift down jumps by powers of two and merge sort copies the whole array at every level.

Saying “they are all O(n log n) but quick sort has the best constant because its memory access is sequential” is a much stronger answer than reciting the complexities.

The comparison sort lower bound. Any sort that only compares elements needs at least log2(n!), which is about n log n, comparisons in the worst case. The argument is that there are n! possible orderings and each comparison distinguishes at most two branches, so the decision tree has depth at least log2(n!). This is why counting and radix sort, which do not compare, can be O(n): they use the key’s structure rather than comparisons.


Q214. Which sort for embedded?

Say this out loud It depends on three things: the array size, whether extra memory is available, and whether a worst case bound must be provable. My defaults are insertion sort below about 50 elements, shell sort or heap sort for medium arrays with no spare memory, counting or radix sort if the keys are small integers, and heap sort whenever a hard real time guarantee is required.

The decision table

Situation Choice Reason
Fewer than roughly 50 elements Insertion sort Smallest code, fastest at that size, no stack
50 to 1000, no spare RAM Shell sort O(1) space, no recursion, simple
Hard real time, worst case must be provable Heap sort Guaranteed O(n log n), O(1) space, no recursion
Integer keys in a small range Counting sort O(n + k), often several times faster
Fixed width integer keys, RAM available Radix sort O(n) in practice for the key widths that matter
Writes are expensive, EEPROM or flash Selection sort At most n writes
Linked list Merge sort No random access needed, no extra array
Nearly sorted data Insertion sort O(n) on that input
General purpose, RAM available, soft real time Quick sort with median-of-three plus insertion fallback Best average constant

What to avoid on a target, and why

  • Plain quick sort with a first-element pivot. Sorted input gives O(n squared) time and O(n) stack depth, which is a crash, not a slowdown.
  • Recursive merge sort with malloc inside the merge. An allocation at every level, and it can fail partway through.
  • Library qsort. It is a function-pointer comparison per element, so it cannot inline the comparison, and its implementation varies between C libraries with no guarantee on stack usage. A hand written insertion or shell sort for your specific type is smaller and faster.

The sentence that lands well: on a target I care more about the worst case and the memory than about the average case, so I will accept heap sort being twice as slow as quick sort in exchange for being able to write a bound in the design document.


Q215. Why does merge sort need extra memory?

Say this out loud Because merging two sorted runs in place is fundamentally hard. Placing an element from the right run into its correct position among the left run requires shifting everything between, which costs O(n) per element. The temporary array lets you write each merged element exactly once, keeping the merge at O(n).

The concrete demonstration

left:  [1, 3, 5]      right: [2, 4, 6]      merging in place

To place 2 after 1, you must shift 3 and 5 right by one. Then to place 4 you shift 5. Each insertion is O(n), and n insertions make the merge O(n squared), which destroys the whole point of the algorithm.

With a temporary buffer, you simply walk both runs and append the smaller each time. One write per element, O(n) total.

How much extra. A single buffer of n elements, allocated once at the top and reused at every level. The naive implementation allocating inside merge is what makes people think merge sort needs O(n log n) memory, which it does not.

Why the linked list version needs none. Merging lists rewires pointers instead of copying values, so there is no shifting and no buffer. The only extra space is O(log n) for the recursion.

node_t *merge_sort_list(node_t *head) {
    if (!head || !head->next) return head;
    node_t *mid = split_in_half(head);            /* slow and fast pointer, Q104 */
    return merge_sorted(merge_sort_list(head), merge_sort_list(mid));   /* Q109 */
}

That is why merge sort is the standard sort for linked lists, and it is exactly what std::list::sort does.

The firmware consequence. Sorting 4000 uint32_t values with merge sort needs 16 KB for the array and another 16 KB for the buffer. On a part with 32 KB of RAM that does not fit alongside everything else. Heap sort sorts the same array with zero extra bytes. That arithmetic is the answer to why heap sort exists.


Q216. How do you choose a quick sort pivot?

Say this out loud The pivot decides everything. First or last element gives O(n squared) on sorted input, which is the most common real input. Median of three is the standard fix and is cheap. Randomised pivots defeat adversarial input. Median of medians gives a true O(n) guarantee but its constant is too large to be worth it.

Strategy Worst case Cost Notes
First or last element O(n squared) on sorted input free Never use it
Middle element O(n squared) on crafted input free Fine for sorted input, still breakable
Median of three, first, middle, last O(n squared) on crafted input 3 comparisons The practical standard
Random O(n squared) with vanishing probability one RNG call Defeats adversarial input
Ninther, median of three medians of three very unlikely 9 comparisons Used for large arrays in libstdc++
Median of medians O(n) guaranteed high constant Theoretical, not used in practice
static int median_of_three(int *a, int lo, int hi) {
    int mid = lo + (hi - lo) / 2;
    if (a[mid] < a[lo])  swap(&a[mid], &a[lo]);
    if (a[hi]  < a[lo])  swap(&a[hi],  &a[lo]);
    if (a[hi]  < a[mid]) swap(&a[hi],  &a[mid]);
    return mid;                                   /* a[mid] is now the median */
}

This sorts the three sampled positions as a side effect, which is a small bonus, and it makes already-sorted input the best case rather than the worst, since the middle element of a sorted array is the perfect pivot.

The three-way partition, for arrays with many duplicates

Standard quick sort degrades badly when many elements equal the pivot, because they all pile into one partition. Dutch national flag partitioning splits into three regions, less than, equal to, and greater than, and recurses only on the outer two.

[ < pivot | == pivot | > pivot ]

An array of all identical values becomes O(n) instead of O(n squared). If the interviewer mentions duplicates, this is the expected answer.


Q217. What is hybrid sorting and why does introsort exist?

Say this out loud Real implementations combine algorithms to get each one’s best property. Introsort starts with quick sort, switches to heap sort when the recursion gets too deep, and finishes with insertion sort on small ranges. Timsort combines merge sort with insertion sort and detects existing sorted runs.

Introsort, which is what std::sort is

void introsort(int *a, int lo, int hi, int depth_limit) {
    while (hi - lo > 16) {                        /* 1. small ranges: leave for insertion */
        if (depth_limit == 0) {
            heap_sort_range(a, lo, hi);           /* 2. too deep: switch to heap sort */
            return;
        }
        depth_limit--;
        int p = partition(a, lo, hi);
        introsort(a, p + 1, hi, depth_limit);     /* recurse on one side */
        hi = p;                                    /* loop on the other, bounding the stack */
    }
    /* fall through: the array is now "nearly sorted" in 16-element blocks */
}

void sort(int *a, int n) {
    introsort(a, 0, n - 1, 2 * ilog2(n));
    insertion_sort(a, n);                          /* 3. one final near-linear pass */
}

Why each piece is there

Piece Solves
Quick sort as the main engine Best average constant
Heap sort at a depth limit of 2 log n Removes the O(n squared) worst case entirely
Insertion sort below 16 Avoids recursion overhead where it dominates
One final insertion pass over the whole array Cheaper than sorting each small block separately, because the array is already nearly sorted

That last point is subtle and worth stating: leaving the small blocks unsorted and doing a single insertion pass at the end is faster than sorting each block individually, because insertion sort on nearly-sorted data is O(n) and you pay the loop overhead once.

Timsort, used by Python and Java for objects, finds naturally occurring sorted runs, extends short ones with insertion sort, and merges runs using a stack with balance invariants. It is O(n) on already sorted input and stable. It is complex, but knowing why it exists, real data usually contains sorted runs, is the useful part.

The takeaway for the interview: no single sort is best. Production sorts are hybrids because worst case protection, small-array efficiency, and average speed are three different requirements.


Q218. What algorithms do std::sort and std::stable_sort use?

Function Algorithm Complexity Stable
std::sort Introsort: quick, heap, insertion O(n log n) guaranteed No
std::stable_sort Merge sort with a temporary buffer, in-place merge if allocation fails O(n log n) with a buffer, O(n log squared n) without Yes
std::partial_sort Heap based O(n log k) No
std::nth_element Introselect, quickselect with a heap fallback O(n) average No
std::sort_heap Heap sort O(n log n) No
C qsort Implementation defined, usually quick sort or merge sort not guaranteed Not guaranteed

std::sort versus C qsort, which is the question behind the question

std::sort is typically two to three times faster, for one reason: the comparator is a template parameter, so the compiler inlines it. qsort takes a function pointer, so every single comparison is an indirect call that cannot be inlined, cannot be optimised across, and costs a branch predictor slot.

For n elements there are about n log n comparisons, so at a million elements that is 20 million indirect calls versus 20 million inlined instructions. That is the entire difference, and it is a good concrete example of why C++ templates are described as zero cost abstraction.

The std::sort guarantee changed. Before C++11 the standard only required O(n log n) average. C++11 tightened it to O(n log n) worst case, which is precisely why implementations adopted introsort.

Embedded relevance. On a target, neither is usually the right answer. Both drag in code you may not want, std::sort instantiates a template per type which grows flash, and qsort has unspecified stack usage. For a known type and a known size range, a hand written insertion or shell sort is smaller, faster, and has a stack usage you can measure. Saying that, while demonstrating that you know exactly what the library does, is the strongest possible answer.


Master comparison table

Sort Best Average Worst Space Stable Adaptive Embedded verdict
Bubble O(n) O(n²) O(n²) O(1) Yes Yes Teaching only
Selection O(n²) O(n²) O(n²) O(1) No No Use when writes are expensive
Insertion O(n) O(n²) O(n²) O(1) Yes Yes Default under 50 elements
Shell O(n log n) ~O(n^1.3) O(n^1.5) O(1) No Some Good medium size choice
Merge O(n log n) O(n log n) O(n log n) O(n) Yes With a check Lists yes, arrays rarely
Quick O(n log n) O(n log n) O(n²) O(log n) No No Only with median of three plus depth bound
Heap O(n log n) O(n log n) O(n log n) O(1) No No Default for hard real time
Counting O(n+k) O(n+k) O(n+k) O(n+k) Yes No Excellent for small key ranges
Radix O(d(n+b)) O(d(n+b)) O(d(n+b)) O(n+b) Yes No Excellent for fixed width keys
Bucket O(n+k) O(n+k) O(n²) O(n+k) Yes No Needs a uniform distribution

Quick revision sheet, questions 189 to 218

Concept The one sentence to remember
Heap shape Complete tree, so it lives in an array with no pointers
Index arithmetic Children 2i+1 and 2i+2, parent (i-1)/2
Heap order Partial only: the root is guaranteed, siblings are not related
Sift down Swap with the larger child and follow the element down, O(log n)
Build heap is O(n) Most nodes are near the bottom and barely move
Heap insert Append, then sift up, so completeness is automatic
Heap extract Last element to the root, shrink, sift down
Delete arbitrary Needs both sift up and sift down, and an index back reference
Min in a max heap O(n), it is somewhere in the leaves
Heap sort Guaranteed O(n log n), O(1) space, no recursion, not stable
Timer queue Min heap keyed by expiry, with heap_index for O(log n) cancel
Bubble sort Only worth writing with the early exit flag and the shrinking bound
Selection sort At most n writes, so it wins on EEPROM and flash
Insertion sort Shift, do not swap; > not >= for stability
Merge sort O(n) buffer allocated once at the top, never inside the merge
Quick sort worst case Sorted input with a first or last pivot, and it overflows the stack
Tail elimination Recurse on the smaller side, loop on the larger, bounding depth at log n
Lomuto vs Hoare They return different things, so the recursive calls differ
Counting sort Place backwards through the input, or you lose stability
Radix sort Only works because the inner sort is stable
Shell sort Insertion sort with 1 replaced by gap
Stability Matters when sorting by a second key after a first
std::sort Introsort, and it is not stable
Comparison lower bound log2(n!), about n log n, which is why counting sort can beat it
Why quick sort is fastest Sequential memory access, so the best constant factor
std::sort vs qsort The comparator inlines instead of being an indirect call
Embedded default Insertion under 50, shell for medium, heap when the bound must be provable


All seven parts of the guide

Start here: The Embedded Firmware DSA Interview Guide, all 248 questions indexed in one place. Architecture round: Firmware Architecture Interview Questions: 100 Q&A.


Work with us

Kalapi Infotech builds firmware for connected embedded products — architecture and BSP work, RTOS and bare-metal development, secure boot and OTA infrastructure. If you are hiring for these roles, or building a product that needs them, we would be glad to talk.

Get in touch with our firmware team →

Leave a Reply

Your email address will not be published. Required fields are marked *