From 4cb625ab9ca18e39d840d1f40a904d7e2828d44b Mon Sep 17 00:00:00 2001 From: deeaitch Date: Mon, 27 Jul 2026 19:44:41 -0400 Subject: [PATCH] Closes #11 --- .../message_queue_partitioning/.gitignore | 74 +++++ .../message_queue_partitioning/main.cpp | 165 +++++++++++ .../message_queue_partitioning.pro | 7 + .../05-Message_Queue_Partitioning/readme.md | 276 ++++++++++++++++++ 4 files changed, 522 insertions(+) create mode 100644 analysis/05-Message_Queue_Partitioning/example/message_queue_partitioning/.gitignore create mode 100644 analysis/05-Message_Queue_Partitioning/example/message_queue_partitioning/main.cpp create mode 100644 analysis/05-Message_Queue_Partitioning/example/message_queue_partitioning/message_queue_partitioning.pro create mode 100644 analysis/05-Message_Queue_Partitioning/readme.md diff --git a/analysis/05-Message_Queue_Partitioning/example/message_queue_partitioning/.gitignore b/analysis/05-Message_Queue_Partitioning/example/message_queue_partitioning/.gitignore new file mode 100644 index 0000000..4a0b530 --- /dev/null +++ b/analysis/05-Message_Queue_Partitioning/example/message_queue_partitioning/.gitignore @@ -0,0 +1,74 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* +CMakeLists.txt.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/analysis/05-Message_Queue_Partitioning/example/message_queue_partitioning/main.cpp b/analysis/05-Message_Queue_Partitioning/example/message_queue_partitioning/main.cpp new file mode 100644 index 0000000..2982fab --- /dev/null +++ b/analysis/05-Message_Queue_Partitioning/example/message_queue_partitioning/main.cpp @@ -0,0 +1,165 @@ +#include +#include +#include + +struct Message { + std::uint32_t id; + bool retry; + Message *next; +}; + +struct MessageQueue { + Message *head; + Message *tail; +}; + +struct PartitionResult { + MessageQueue ready; + MessageQueue retry; +}; + +static void append (MessageQueue &queue, Message *message) { + assert (message != nullptr); + assert (message->next == nullptr); + + if (queue.tail == nullptr) { + queue.head = message; + queue.tail = message; + return; + } + + queue.tail->next = message; + queue.tail = message; +} + +PartitionResult partition_messages (MessageQueue &source) { + PartitionResult result{ + {nullptr, nullptr}, + {nullptr, nullptr} + }; + + Message *current = source.head; + + /* + * The source queue is consumed by this operation. + * + * Clearing it before traversal makes the ownership transfer explicit: + * every node taken from the original queue must be appended to exactly + * one of the two result queues. + */ + source.head = nullptr; + source.tail = nullptr; + + while (current != nullptr) { + /* + * Save the traversal link before modifying current->next. + * The same intrusive link is reused by the destination queue. + */ + Message *next = current->next; + current->next = nullptr; + + if (current->retry) + append (result.retry, current); + else + append (result.ready, current); + + current = next; + } + + return result; +} + +static void print_queue (const char *name, const MessageQueue &queue) { + std::cout << name << ": "; + + const Message *current = queue.head; + + if (current == nullptr) { + std::cout << "\n"; + return; + } + + while (current != nullptr) { + std::cout << current->id; + + if (current->next != nullptr) + std::cout << " -> "; + + current = current->next; + } + + std::cout << '\n'; +} + +static std::size_t queue_size (const MessageQueue &queue) { + std::size_t size = 0; + const Message *current = queue.head; + + while (current != nullptr) { + ++size; + current = current->next; + } + + return size; +} + +static void verify_queue (const MessageQueue &queue) { + if (queue.head == nullptr) { + assert (queue.tail == nullptr); + return; + } + + assert (queue.tail != nullptr); + assert (queue.tail->next == nullptr); + + const Message *current = queue.head; + + while (current->next != nullptr) + current = current->next; + + assert (current == queue.tail); +} + +int main() { + Message a{1U, false, nullptr}; + Message b{2U, true, nullptr}; + Message c{3U, false, nullptr}; + Message d{4U, true, nullptr}; + + a.next = &b; + b.next = &c; + c.next = &d; + + MessageQueue outgoing{&a, &d}; + + std::cout << "Before partition\n"; + print_queue ("Outgoing", outgoing); + + const PartitionResult result = partition_messages (outgoing); + + std::cout << "\nAfter partition\n"; + print_queue ("Outgoing", outgoing); + print_queue ("Ready", result.ready); + print_queue ("Retry", result.retry); + + verify_queue (outgoing); + verify_queue (result.ready); + verify_queue (result.retry); + + assert (outgoing.head == nullptr); + assert (outgoing.tail == nullptr); + + assert (result.ready.head == &a); + assert (result.ready.tail == &c); + assert (a.next == &c); + assert (c.next == nullptr); + + assert (result.retry.head == &b); + assert (result.retry.tail == &d); + assert (b.next == &d); + assert (d.next == nullptr); + + assert (queue_size (result.ready) + queue_size (result.retry) == 4U); + + return 0; +} diff --git a/analysis/05-Message_Queue_Partitioning/example/message_queue_partitioning/message_queue_partitioning.pro b/analysis/05-Message_Queue_Partitioning/example/message_queue_partitioning/message_queue_partitioning.pro new file mode 100644 index 0000000..595bdaa --- /dev/null +++ b/analysis/05-Message_Queue_Partitioning/example/message_queue_partitioning/message_queue_partitioning.pro @@ -0,0 +1,7 @@ +TEMPLATE = app +CONFIG += console c++17 +CONFIG -= app_bundle +CONFIG -= qt + +SOURCES += \ + main.cpp diff --git a/analysis/05-Message_Queue_Partitioning/readme.md b/analysis/05-Message_Queue_Partitioning/readme.md new file mode 100644 index 0000000..988fd45 --- /dev/null +++ b/analysis/05-Message_Queue_Partitioning/readme.md @@ -0,0 +1,276 @@ +# #05 — Message Queue Partitioning + +## Problem + +A communication subsystem maintains a singly linked intrusive queue of outgoing messages. + +Each message contains a transmission identifier, a retry flag, and a pointer to the next message: + +```cpp +struct Message { + uint32_t id; + bool retry; + Message* next; +}; +``` + +After a transmission attempt, some messages may need to be retried. + +Partition the original queue into two separate queues: + +- the ready queue, containing messages that do not require another transmission attempt; +- the retry queue, containing messages marked for retry. + +The relative order of messages must be preserved in both queues. + +### Requirements + +- no dynamic memory allocation; +- no copying of messages; +- reuse the existing list nodes; +- preserve the original order in both resulting queues; +- process the queue in `O(n)` time. + +## Example + +### Input + +```text +A -> B -> C -> D +``` + +```text +A: retry = false +B: retry = true +C: retry = false +D: retry = true +``` + +### Result + +Ready queue + +```text +A -> C +``` + +Retry queue + +```text +B -> D +``` + +The original queue is consumed during the operation, and every message must belong to exactly one of the two resulting queues. + +--- + +# Analysis + +At first glance, this looks like another linked list interview problem. + +Traverse the list. + +Check a flag. + +Split the nodes into two lists. + +Complexity: **O(n)**. + +Simple. + +Except this is one of those rare cases where the interview version is surprisingly close to a real engineering task. + +The interesting part is not the algorithm. + +The interesting part is what the algorithm is actually modifying. + +--- + +## This Is Not About Two Lists + +Each node already exists. + +```cpp +struct Message { + uint32_t id; + bool retry; + Message* next; +}; +``` + +No objects are created. + +No objects are destroyed. + +No messages are copied. + +Only ownership changes. + +The original outgoing queue disappears and every message becomes part of exactly one new queue. + +That small detail changes the entire nature of the problem. + +--- + +## The Real Challenge + +The boolean itself is trivial. + +```cpp +retry == true +``` + +is simply a classification. + +The difficult part is maintaining the integrity of two intrusive queues while consuming a third one. + +Every processed node must satisfy one invariant: + +- belong to exactly one queue; +- never be lost; +- never appear twice; +- never keep stale links into the original list. + +Most bugs are not caused by the condition. + +They are caused by pointer manipulation. + +--- + +## Why Saving `next` Matters + +The same pointer is used for two completely different purposes. + +During traversal: + +```text +current -> next +``` + +is how we reach the remaining nodes. + +After insertion into a new queue: + +```text +current -> next +``` + +becomes part of another list. + +If the original `next` pointer is overwritten before it is saved, the remainder of the queue is simply lost. + +This is one of the classic pitfalls of intrusive containers. + +--- + +## Stable Partition + +The requirements also say: + +> preserve order + +That sounds minor. + +It isn't. + +Appending to the head would produce: + +```text +D -> B +``` + +instead of + +```text +B -> D +``` + +The algorithm therefore performs a **stable partition**, preserving FIFO order in both resulting queues. + +In a communication subsystem this is often essential because later messages may depend on earlier ones. + +--- + +## Why No Allocation? + +The requirement + +```text +no allocation +``` + +isn't there to make the problem harder. + +It reflects reality. + +Communication stacks, embedded systems and real-time software often avoid dynamic allocation while processing packets or messages. + +The messages already exist. + +Only their position inside processing queues changes. + +--- + +## Hidden Engineering Questions + +The implementation itself is small. + +The engineering questions are not. + +For example: + +- Who owns the original queue after partitioning? +- Can another thread append messages during the operation? +- Can an interrupt modify the queue? +- What happens if the queue is already corrupted? +- Can a message belong to multiple intrusive containers? +- Should retry count also be updated? +- Is there exponential backoff before retrying? + +None of these appear in the problem statement. + +All of them appear in production systems. + +--- + +## What This Problem Actually Tests + +Unlike many linked list exercises, this one evaluates something genuinely useful. + +It tests whether a developer can safely manipulate ownership using pointers while preserving structural invariants. + +The algorithm itself is almost secondary. + +Correctness is everything. + +--- + +## Key Takeaway + +This is one of the few interview-style linked list problems that has a direct equivalent in production software. + +Not because splitting a list is inherently interesting. + +But because communication stacks, schedulers, networking software and embedded systems continuously reorganize intrusive queues exactly like this. + +The interview version removes most of the surrounding system. + +The engineering version adds ownership, invariants, concurrency and failure handling. + +The pointer operations remain almost identical. + +The responsibility does not. + +--- + +## Project Perspective + +> Exists in real engineering? + +**Yes. Frequently.** + +> Exists in interview form? + +**Yes.** + +One of the rare cases where the interview problem remains close to its real-world counterpart. \ No newline at end of file