Compare commits

...

14 Commits

Author SHA1 Message Date
fc42369e54 Ref: #04 Simplify topic 2026-08-03 15:12:10 -04:00
7a329c639e Closes #4 2026-08-03 15:07:39 -04:00
360d2c3d21 Ref #11 fixed iteration file name 2026-07-27 23:33:00 -04:00
063f3277bb Ref #11 added some UML 2026-07-27 20:09:01 -04:00
4cb625ab9c Closes #11 2026-07-27 19:44:41 -04:00
eda3bf691a simplify template 2026-06-23 18:12:44 -04:00
233c53fb9d Closes #2 Added idea for nex one 2026-06-23 18:02:40 -04:00
cae7127193 clean up 2026-06-23 12:56:46 -04:00
848235fc8a gitignore 2026-06-23 12:54:16 -04:00
463614284f Ref: 2 Rename folder to follow previous articles 2026-06-17 18:23:06 -04:00
5f7e37b79b Ref: 2 memory walkthrough was added 2026-06-17 18:01:14 -04:00
fc364dd739 Ref: 2 step by step diagrams added. 2026-06-17 17:48:09 -04:00
939935c258 Ref: 2 - added example 2026-06-17 17:40:27 -04:00
e1ef02c4a6 Ref: #2 Main problem description 2026-06-17 17:21:58 -04:00
36 changed files with 3078 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.user

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,35 @@
@startuml
title Step 0 — Initial State
object "Node 1" as n1 {
value = 1
}
object "Node 2" as n2 {
value = 2
}
object "Node 3" as n3 {
value = 3
}
n1 --> n2 : next
n2 --> n3 : next
n3 --> "null" : next
object "previous" as prev
object "current" as cur
prev --> "null"
cur --> n1
note bottom
Before the loop starts:
previous = null
current = head
The original list is still intact.
end note
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,38 @@
@startuml
title Step 1 — Save next
object "Node 1" as n1 {
value = 1
}
object "Node 2" as n2 {
value = 2
}
object "Node 3" as n3 {
value = 3
}
n1 --> n2 : next
n2 --> n3 : next
n3 --> "null" : next
object "previous" as prev
object "current" as cur
object "next" as nxt
prev --> "null"
cur --> n1
nxt --> n2
note right of nxt
next = current->next
We save the next node before changing
current->next.
Without this temporary pointer, the rest
of the original list would become unreachable.
end note
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,38 @@
@startuml
title Step 2 — Reverse current->next
object "Node 1" as n1 {
value = 1
}
object "Node 2" as n2 {
value = 2
}
object "Node 3" as n3 {
value = 3
}
n1 --> "null" : next
n2 --> n3 : next
n3 --> "null" : next
object "previous" as prev
object "current" as cur
object "next" as nxt
prev --> "null"
cur --> n1
nxt --> n2
note right of n1
current->next = previous
Node 1 no longer points to Node 2.
It now points to the already reversed part.
At the first iteration, the reversed part is empty,
so Node 1 points to null.
end note
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,39 @@
@startuml
title Step 3 — Move previous and current
object "Node 1" as n1 {
value = 1
}
object "Node 2" as n2 {
value = 2
}
object "Node 3" as n3 {
value = 3
}
n1 --> "null" : next
n2 --> n3 : next
n3 --> "null" : next
object "previous" as prev
object "current" as cur
prev --> n1
cur --> n2
note right
previous = current
current = next
The reversed part is now:
1 -> null
The remaining original part is still:
2 -> 3 -> null
end note
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,37 @@
@startuml
title Step 4 — Second Iteration After Reversing Node 2
object "Node 1" as n1 {
value = 1
}
object "Node 2" as n2 {
value = 2
}
object "Node 3" as n3 {
value = 3
}
n2 --> n1 : next
n1 --> "null" : next
n3 --> "null" : next
object "previous" as prev
object "current" as cur
object "next" as nxt
prev --> n2
cur --> n3
nxt --> n3
note bottom
After processing Node 2:
2 -> 1 -> null
The reversed part grows from the front.
The remaining part starts at current.
end note
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,35 @@
@startuml
title Step 5 — Final State
object "Node 1" as n1 {
value = 1
}
object "Node 2" as n2 {
value = 2
}
object "Node 3" as n3 {
value = 3
}
n3 --> n2 : next
n2 --> n1 : next
n1 --> "null" : next
object "head" as head
object "previous" as prev
object "current" as cur
head --> n3
prev --> n3
cur --> "null"
note right of head
When current becomes null,
previous points to the new head.
head = previous
end note
@enduml

View File

@@ -0,0 +1,58 @@
# Reverse Linked List — PlantUML Diagrams
This directory contains PlantUML diagrams for the three-pointer linked list reversal algorithm.
The diagrams use a small list:
```text
1 -> 2 -> 3 -> null
```
and show how it becomes:
```text
3 -> 2 -> 1 -> null
```
## Files
- `00_initial_state.puml` — initial state before the loop
- `01_save_next.puml` — saving `next = current->next`
- `02_reverse_current_link.puml` — reversing `current->next`
- `03_move_pointers.puml` — moving `previous` and `current`
- `04_second_iteration.puml` — state after the second node is processed
- `05_final_state.puml` — final state after the loop
## Generate PNG Files
```sh
plantuml diagrams/*.puml
```
## Generate SVG Files
```sh
plantuml -tsvg diagrams/*.puml
```
## Core Idea
During the loop, the list is logically split into two parts:
- `previous` points to the already reversed part
- `current` points to the node currently being processed
- `next` temporarily preserves access to the remaining original list
The key operation is:
```cpp
current->next = previous;
```
But this is only safe after saving:
```cpp
Node* next = current->next;
```
Otherwise the remaining part of the original list would be lost.

View File

@@ -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

View File

@@ -0,0 +1,30 @@
# Reverse Linked List Example
This directory contains a small standalone C++ example for the classic three-pointer linked list reversal algorithm.
## Build
```bash
g++ -std=c++17 -Wall -Wextra -pedantic main.cpp -o reverse_linked_list
```
## Run
```bash
./reverse_linked_list
```
## Expected Output
```text
Original list:
1 -> 2 -> 3 -> 4 -> 5 -> null
Reversed list:
5 -> 4 -> 3 -> 2 -> 1 -> null
```
## Memory walkthrough
see memory_walkthrough.md

View File

@@ -0,0 +1,216 @@
/**
* @file main.cpp
* @brief Demonstrates the classic three-pointer algorithm for reversing a singly linked list.
*
* This example is intentionally small and self-contained.
* It is not meant to show that reversing linked lists is a common production task.
* Instead, it documents the interview pattern clearly enough that a reader unfamiliar
* with it can compile the program, run it, and inspect the output.
*/
#include <iostream>
#include <initializer_list>
/**
* @brief A minimal singly linked list node.
*
* Each node stores an integer value and a pointer to the next node.
* The last node in the list has @c next equal to @c nullptr.
*/
struct Node {
int value; ///< Payload stored in the node.
Node *next; ///< Pointer to the next node, or nullptr for the last node.
};
/**
* @brief Appends a new value to the end of the list.
*
* @param head Reference to the head pointer of the list.
* @param value Value to store in the new node.
*
* This helper is used only to build the demonstration list.
* It keeps the example simple and avoids using STL containers for the list itself,
* because the goal is to demonstrate raw pointer manipulation.
*/
void appendNode (Node *&head, int value) {
Node *node = new Node{value, nullptr};
if (head == nullptr) {
head = node;
return;
}
Node *current = head;
while (current->next != nullptr)
current = current->next;
current->next = node;
}
/**
* @brief Creates a linked list from an initializer list.
*
* @param values Values to insert into the list in the given order.
* @return Pointer to the first node of the created list.
*
* The caller owns the returned list and must release it with freeList().
*/
Node *createList (std::initializer_list<int> values) {
Node *head = nullptr;
for (int value : values)
appendNode (head, value);
return head;
}
/**
* @brief Prints the list without modifying it.
*
* @param head Pointer to the first node of the list.
*
* Output example:
* @code
* 1 -> 2 -> 3 -> 4 -> null
* @endcode
*/
void printList (const Node *head) {
const Node *current = head;
while (current != nullptr) {
std::cout << current->value << " -> ";
current = current->next;
}
std::cout << "null" << std::endl;
}
/**
* @brief Reverses a singly linked list in place.
*
* @param head Pointer to the first node of the original list.
* @return Pointer to the first node of the reversed list.
*
* This is the classic three-pointer interview algorithm.
*
* The three pointers are:
*
* - @c previous — the already reversed part of the list
* - @c current — the node we are processing right now
* - @c next — the original next node saved before we overwrite @c current->next
*
* Why @c next is necessary:
*
* In a singly linked list, each node only knows where the next node is.
* When we execute:
*
* @code
* current->next = previous;
* @endcode
*
* we destroy the original forward link.
* Without saving it first, the rest of the list would be lost.
*
* The algorithm works by moving one node at a time from the original forward chain
* into the reversed chain.
*
* Initial state:
*
* @code
* previous = null
* current = 1 -> 2 -> 3 -> 4 -> null
* @endcode
*
* After processing node 1:
*
* @code
* previous = 1 -> null
* current = 2 -> 3 -> 4 -> null
* @endcode
*
* After processing node 2:
*
* @code
* previous = 2 -> 1 -> null
* current = 3 -> 4 -> null
* @endcode
*
* When @c current becomes @c nullptr, @c previous points to the new head.
*
* Complexity:
*
* - Time: O(n), because each node is visited once.
* - Extra memory: O(1), because only a fixed number of pointers is used.
*/
Node *reverseList (Node *head) {
Node *previous = nullptr;
Node *current = head;
while (current != nullptr) {
/*
* Save the original next node before changing current->next.
* Without this line, the rest of the list would become unreachable.
*/
Node *next = current->next;
/*
* Reverse the direction of the link.
* The current node now points to the already reversed part.
*/
current->next = previous;
/*
* Move previous forward.
* The current node becomes the new head of the reversed part.
*/
previous = current;
/*
* Continue with the node that originally followed current.
*/
current = next;
}
return previous;
}
/**
* @brief Releases all nodes in the list.
*
* @param head Pointer to the first node of the list.
*
* This function is separated from the reversal and printing logic.
* It exists only because this example uses raw @c new to keep the node structure explicit.
*/
void freeList (Node *head) {
Node *current = head;
while (current != nullptr) {
Node *next = current->next;
delete current;
current = next;
}
}
/**
* @brief Program entry point.
*
* Builds a small list, prints it, reverses it, prints it again,
* and finally releases all allocated nodes.
*/
int main() {
Node *list = createList ({1, 2, 3, 4, 5});
std::cout << "Original list:" << std::endl;
printList (list);
list = reverseList (list);
std::cout << "\nReversed list:" << std::endl;
printList (list);
freeList (list);
return 0;
}

View File

@@ -0,0 +1,827 @@
# Reverse Linked List — Memory Walkthrough
This walkthrough explains the classic three-pointer linked list reversal using a memory-oriented view.
The goal is not only to show that the algorithm works, but also to show what happens to:
- stack variables
- heap nodes
- `next` fields inside each node
The example list contains three nodes:
```text
1 -> 2 -> 3 -> null
```
For clarity, fake addresses are used:
```text
Node 1: 0x1000
Node 2: 0x2000
Node 3: 0x3000
```
These addresses are illustrative only.
A real program will use different addresses.
---
## Algorithm
```cpp
Node* reverseList(Node* head) {
Node* previous = nullptr;
Node* current = head;
while(current != nullptr) {
Node* next = current->next;
current->next = previous;
previous = current;
current = next;
}
return previous;
}
```
The three important pointers are:
| Pointer | Meaning |
|---|---|
| `previous` | Head of the already reversed part |
| `current` | Node currently being processed |
| `next` | Saved pointer to the remaining original list |
The most important rule is:
> Save `next` before changing `current->next`.
Otherwise the rest of the original list may become unreachable.
---
## Step 0 — Initial State
Before the loop starts:
```cpp
Node* previous = nullptr;
Node* current = head;
```
### Stack
```text
+----------+----------+
| Variable | Value |
+----------+----------+
| head | 0x1000 |
| previous | nullptr |
| current | 0x1000 |
| next | not set |
+----------+----------+
```
### Heap
```text
0x1000
+-----------+-----------+
| value = 1 | next=2000 |
+-----------+-----------+
0x2000
+-----------+-----------+
| value = 2 | next=3000 |
+-----------+-----------+
0x3000
+-----------+-----------+
| value = 3 | next=null |
+-----------+-----------+
```
### Logical view
```text
head/current
|
v
1 -> 2 -> 3 -> null
previous -> null
```
At this point, nothing has been reversed yet.
---
## Step 1 — Save `next`
Inside the first loop iteration:
```cpp
Node* next = current->next;
```
`current` points to Node 1.
`current->next` points to Node 2.
So:
```text
next = 0x2000
```
### Stack
```text
+----------+----------+
| Variable | Value |
+----------+----------+
| head | 0x1000 |
| previous | nullptr |
| current | 0x1000 |
| next | 0x2000 |
+----------+----------+
```
### Heap
```text
0x1000
+-----------+-----------+
| value = 1 | next=2000 |
+-----------+-----------+
0x2000
+-----------+-----------+
| value = 2 | next=3000 |
+-----------+-----------+
0x3000
+-----------+-----------+
| value = 3 | next=null |
+-----------+-----------+
```
Nothing in the heap changed yet.
The `next` stack variable only saves access to the rest of the list.
Without this temporary pointer, Node 2 and Node 3 could be lost after the next operation.
---
## Step 2 — Reverse `current->next`
Now the algorithm changes the link:
```cpp
current->next = previous;
```
`current` is Node 1.
`previous` is `nullptr`.
So Node 1 no longer points to Node 2.
It now points to `nullptr`.
### Stack
```text
+----------+----------+
| Variable | Value |
+----------+----------+
| head | 0x1000 |
| previous | nullptr |
| current | 0x1000 |
| next | 0x2000 |
+----------+----------+
```
### Heap
```text
0x1000
+-----------+-----------+
| value = 1 | next=null |
+-----------+-----------+
0x2000
+-----------+-----------+
| value = 2 | next=3000 |
+-----------+-----------+
0x3000
+-----------+-----------+
| value = 3 | next=null |
+-----------+-----------+
```
### Logical view
```text
current
|
v
1 -> null
next
|
v
2 -> 3 -> null
```
This is the key mutation.
The original list is now split into two logical parts:
```text
Reversed part:
1 -> null
Remaining original part:
2 -> 3 -> null
```
---
## Step 3 — Move `previous`
The algorithm advances the reversed part:
```cpp
previous = current;
```
`previous` now points to Node 1.
### Stack
```text
+----------+----------+
| Variable | Value |
+----------+----------+
| head | 0x1000 |
| previous | 0x1000 |
| current | 0x1000 |
| next | 0x2000 |
+----------+----------+
```
### Heap
```text
0x1000
+-----------+-----------+
| value = 1 | next=null |
+-----------+-----------+
0x2000
+-----------+-----------+
| value = 2 | next=3000 |
+-----------+-----------+
0x3000
+-----------+-----------+
| value = 3 | next=null |
+-----------+-----------+
```
### Logical view
```text
previous/current
|
v
1 -> null
next
|
v
2 -> 3 -> null
```
The reversed part now officially starts at `previous`.
---
## Step 4 — Move `current`
The algorithm continues with the saved next node:
```cpp
current = next;
```
`current` now points to Node 2.
### Stack
```text
+----------+----------+
| Variable | Value |
+----------+----------+
| head | 0x1000 |
| previous | 0x1000 |
| current | 0x2000 |
| next | 0x2000 |
+----------+----------+
```
### Heap
```text
0x1000
+-----------+-----------+
| value = 1 | next=null |
+-----------+-----------+
0x2000
+-----------+-----------+
| value = 2 | next=3000 |
+-----------+-----------+
0x3000
+-----------+-----------+
| value = 3 | next=null |
+-----------+-----------+
```
### Logical view
```text
previous
|
v
1 -> null
current
|
v
2 -> 3 -> null
```
The first iteration is complete.
---
## Step 5 — Second Iteration: Save `next`
The loop repeats.
```cpp
Node* next = current->next;
```
`current` points to Node 2.
Node 2 points to Node 3.
So:
```text
next = 0x3000
```
### Stack
```text
+----------+----------+
| Variable | Value |
+----------+----------+
| head | 0x1000 |
| previous | 0x1000 |
| current | 0x2000 |
| next | 0x3000 |
+----------+----------+
```
### Heap
```text
0x1000
+-----------+-----------+
| value = 1 | next=null |
+-----------+-----------+
0x2000
+-----------+-----------+
| value = 2 | next=3000 |
+-----------+-----------+
0x3000
+-----------+-----------+
| value = 3 | next=null |
+-----------+-----------+
```
Again, the heap has not changed yet.
---
## Step 6 — Second Iteration: Reverse Link
```cpp
current->next = previous;
```
`current` is Node 2.
`previous` is Node 1.
So Node 2 now points back to Node 1.
### Stack
```text
+----------+----------+
| Variable | Value |
+----------+----------+
| head | 0x1000 |
| previous | 0x1000 |
| current | 0x2000 |
| next | 0x3000 |
+----------+----------+
```
### Heap
```text
0x1000
+-----------+-----------+
| value = 1 | next=null |
+-----------+-----------+
0x2000
+-----------+-----------+
| value = 2 | next=1000 |
+-----------+-----------+
0x3000
+-----------+-----------+
| value = 3 | next=null |
+-----------+-----------+
```
### Logical view
```text
current
|
v
2 -> 1 -> null
next
|
v
3 -> null
```
The reversed part will become:
```text
2 -> 1 -> null
```
after `previous` moves to Node 2.
---
## Step 7 — Second Iteration: Move Pointers
```cpp
previous = current;
current = next;
```
### Stack
```text
+----------+----------+
| Variable | Value |
+----------+----------+
| head | 0x1000 |
| previous | 0x2000 |
| current | 0x3000 |
| next | 0x3000 |
+----------+----------+
```
### Heap
```text
0x1000
+-----------+-----------+
| value = 1 | next=null |
+-----------+-----------+
0x2000
+-----------+-----------+
| value = 2 | next=1000 |
+-----------+-----------+
0x3000
+-----------+-----------+
| value = 3 | next=null |
+-----------+-----------+
```
### Logical view
```text
previous
|
v
2 -> 1 -> null
current
|
v
3 -> null
```
Now two nodes are reversed.
---
## Step 8 — Third Iteration: Save `next`
```cpp
Node* next = current->next;
```
`current` is Node 3.
Node 3 points to `nullptr`.
So:
```text
next = nullptr
```
### Stack
```text
+----------+----------+
| Variable | Value |
+----------+----------+
| head | 0x1000 |
| previous | 0x2000 |
| current | 0x3000 |
| next | nullptr |
+----------+----------+
```
### Heap
```text
0x1000
+-----------+-----------+
| value = 1 | next=null |
+-----------+-----------+
0x2000
+-----------+-----------+
| value = 2 | next=1000 |
+-----------+-----------+
0x3000
+-----------+-----------+
| value = 3 | next=null |
+-----------+-----------+
```
---
## Step 9 — Third Iteration: Reverse Link
```cpp
current->next = previous;
```
`current` is Node 3.
`previous` is Node 2.
So Node 3 now points to Node 2.
### Stack
```text
+----------+----------+
| Variable | Value |
+----------+----------+
| head | 0x1000 |
| previous | 0x2000 |
| current | 0x3000 |
| next | nullptr |
+----------+----------+
```
### Heap
```text
0x1000
+-----------+-----------+
| value = 1 | next=null |
+-----------+-----------+
0x2000
+-----------+-----------+
| value = 2 | next=1000 |
+-----------+-----------+
0x3000
+-----------+-----------+
| value = 3 | next=2000 |
+-----------+-----------+
```
### Logical view
```text
current
|
v
3 -> 2 -> 1 -> null
```
The whole list is now reversed, but the loop still needs to update the stack pointers.
---
## Step 10 — Third Iteration: Move Pointers
```cpp
previous = current;
current = next;
```
Since `next` is `nullptr`, `current` becomes `nullptr`.
### Stack
```text
+----------+----------+
| Variable | Value |
+----------+----------+
| head | 0x1000 |
| previous | 0x3000 |
| current | nullptr |
| next | nullptr |
+----------+----------+
```
### Heap
```text
0x1000
+-----------+-----------+
| value = 1 | next=null |
+-----------+-----------+
0x2000
+-----------+-----------+
| value = 2 | next=1000 |
+-----------+-----------+
0x3000
+-----------+-----------+
| value = 3 | next=2000 |
+-----------+-----------+
```
### Logical view
```text
previous
|
v
3 -> 2 -> 1 -> null
current -> null
```
The loop condition fails:
```cpp
while(current != nullptr)
```
because `current` is now `nullptr`.
---
## Step 11 — Return New Head
At the end:
```cpp
return previous;
```
`previous` points to Node 3.
Node 3 is the new head of the reversed list.
### Final stack view
```text
+----------+----------+
| Variable | Value |
+----------+----------+
| old head | 0x1000 |
| new head | 0x3000 |
+----------+----------+
```
### Final heap view
```text
0x3000
+-----------+-----------+
| value = 3 | next=2000 |
+-----------+-----------+
0x2000
+-----------+-----------+
| value = 2 | next=1000 |
+-----------+-----------+
0x1000
+-----------+-----------+
| value = 1 | next=null |
+-----------+-----------+
```
### Final logical view
```text
new head
|
v
3 -> 2 -> 1 -> null
```
---
## Why the Temporary `next` Pointer Matters
This line is not optional:
```cpp
Node* next = current->next;
```
Without it, this operation:
```cpp
current->next = previous;
```
would overwrite the only pointer to the remaining original list.
For example, at the beginning:
```text
1 -> 2 -> 3 -> null
```
If Node 1 is changed to:
```text
1 -> null
```
before saving Node 2, then Node 2 and Node 3 are no longer reachable from any local variable.
That is why the algorithm always follows this order:
```cpp
Node* next = current->next; // preserve the remaining list
current->next = previous; // reverse the link
previous = current; // grow the reversed part
current = next; // continue with the remaining part
```
The order is the algorithm.
---
## Summary
During the algorithm:
- `previous` points to the already reversed part.
- `current` points to the node being processed.
- `next` preserves access to the not-yet-processed part.
- Only one `next` field is modified per iteration.
- No nodes are copied.
- No new list is allocated.
- The original nodes are relinked in-place.
The algorithm is small, but it is easy to get wrong because it mutates the structure while traversing it.
That is why a memory-level walkthrough is often more useful than just showing the final code.

View File

@@ -0,0 +1,5 @@
Original list:
1 -> 2 -> 3 -> 4 -> 5 -> null
Reversed list:
5 -> 4 -> 3 -> 2 -> 1 -> null

View File

@@ -0,0 +1,7 @@
TEMPLATE = app
CONFIG += console c++17
CONFIG -= app_bundle
CONFIG -= qt
SOURCES += \
main.cpp

View File

@@ -0,0 +1,260 @@
# #04 — Reverse Linked List: Academic Exercise
## Problem
A classic interview question:
Given a singly linked list:
```cpp
struct Node {
int value;
Node* next;
};
```
Reverse the list:
```text
1 -> 2 -> 3 -> 4 -> null
```
into:
```text
4 -> 3 -> 2 -> 1 -> null
```
using:
* O(n) time
* O(1) additional memory
---
## Typical Interview Solution
The standard solution uses three pointers:
```cpp
Node* previous = nullptr;
Node* current = head;
while(current) {
Node* next = current->next;
current->next = previous;
previous = current;
current = next;
}
head = previous;
```
The candidate is expected to produce this solution quickly and correctly.
---
## What This Actually Tests
Despite its popularity, this problem tests a surprisingly narrow set of skills.
Primarily:
* Pointer manipulation
* Attention to detail
* Familiarity with linked lists
* Prior exposure to a common interview pattern
In many cases, prior exposure matters more than reasoning.
A candidate who has seen the problem twenty times may solve it in under a minute.
A senior engineer with years of production experience may need significantly longer if they have never encountered this specific exercise before.
---
## Why This Is Rare In Real Engineering
The interesting question is:
> When was the last time you actually reversed a linked list in production code?
For most engineers, the answer is:
> Almost never.
Modern systems rarely use linked lists as a primary data structure.
More commonly you will encounter:
* vectors
* deques
* ring buffers
* hash tables
* trees
* databases
* message queues
The embedded world is similar.
Typical structures include:
* circular buffers
* DMA buffers
* message queues
* routing tables
* state machines
Linked lists certainly exist.
However, fully reversing one is rarely a real business requirement.
---
## The Hidden Assumption
The interview question starts with an assumption:
> You already have a linked list.
Real engineering often starts with a different question:
> Why is this a linked list in the first place?
That decision is usually far more important than the reversal algorithm itself.
---
## Real-World Equivalent
Finding a true production equivalent is difficult.
Most real systems solve a different problem.
### Example 1: Event History Viewer
A user wants to see the newest events first.
A typical engineering solution is:
* iterate in reverse
* change presentation logic
* adjust query ordering
The underlying data structure often remains unchanged.
---
### Example 2: CAN Trace Analysis
Suppose a trace contains millions of CAN frames.
The user wants the newest messages displayed at the top.
Nobody reverses the entire dataset.
Instead:
* reverse iteration is used
* the UI changes presentation order
* indexing structures provide efficient access
The stored data remains exactly as it was.
---
## What Real Engineers Usually Ask
A more practical engineering question would be:
> The user wants to view data in reverse order.
>
> Do we actually need to modify the data structure?
This question frequently leads to better solutions.
---
## Better Interview Question
Instead of asking:
> Reverse a linked list.
Consider asking:
> A system stores ten million records.
>
> Users want to view them in reverse order.
>
> What solution options exist, and what are their trade-offs?
Now the discussion becomes much more interesting:
* memory usage
* cache locality
* ownership
* indexing
* performance
* maintainability
* user requirements
In other words:
Engineering begins.
---
## Common Mistakes
* ❌ Assuming data must be modified to change presentation order
* ❌ Ignoring alternative data structures
* ❌ Focusing on implementation before understanding requirements
* ❌ Treating algorithmic manipulation as the only valid solution
---
## Key Takeaway
Reverse Linked List is useful as an educational exercise.
It teaches pointer manipulation and careful reasoning about memory.
However, the problem itself rarely appears in production software in its original form.
The real engineering question is usually not:
> How do we reverse the list?
Instead it is:
> Do we need to reverse it at all?
---
## Project Perspective
> Exists in real engineering?
**Partially**
Linked lists exist.
Complete list reversal is a very uncommon business requirement.
> Exists in interview form?
**Yes**
It remains one of the most common classic coding interview questions.
The exercise is valuable for learning pointer manipulation.
Its usefulness as a predictor of engineering ability is far less obvious.
## A More Engineering-Oriented Alternatives
If the goal is to eveluate pointer manipulation, linked-list traversal and in-place node relocation, a message queue partitioning task may provide a more realistic engineering scenario. This is idea for #05

View File

@@ -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

View File

@@ -0,0 +1,165 @@
#include <cassert>
#include <cstdint>
#include <iostream>
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 << "<empty>\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;
}

View File

@@ -0,0 +1,7 @@
TEMPLATE = app
CONFIG += console c++17
CONFIG -= app_bundle
CONFIG -= qt
SOURCES += \
main.cpp

View File

@@ -0,0 +1,25 @@
@startuml
rectangle "Traversal"
() previous
() current
() next
previous --> current
current --> next
note right
save next
detach current
append to
destination queue
continue with next
end note
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,13 @@
@startuml
[*] --> Outgoing
Outgoing --> Sent : success
Outgoing --> Retry : retry == true
Retry --> Outgoing : rescheduled
Sent --> [*]
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,17 @@
@startuml
rectangle "Outgoing Queue" as Q1
rectangle "Ready Queue" as Q2
rectangle "Retry Queue" as Q3
Q1 --> Q2 : move node
Q1 --> Q3 : move node
note bottom
Every message belongs
to exactly one queue.
end note
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,39 @@
@startuml
left to right direction
skinparam linetype ortho
skinparam shadowing false
package "Before Partition" {
rectangle "A\nretry = false" as A
rectangle "B\nretry = true" as B
rectangle "C\nretry = false" as C
rectangle "D\nretry = true" as D
A --> B : next
B --> C : next
C --> D : next
}
package "After Partition" {
package "Ready Queue" {
rectangle "A" as ReadyA
rectangle "C" as ReadyC
ReadyA --> ReadyC : next
}
package "Retry Queue" {
rectangle "B" as RetryB
rectangle "D" as RetryD
RetryB --> RetryD : next
}
}
A ..> ReadyA : move
B ..> RetryB : move
C ..> ReadyC : move
D ..> RetryD : move
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,20 @@
@startuml
participant Queue
participant Algorithm
participant Ready
participant Retry
Queue -> Algorithm : get next node
alt retry == false
Algorithm -> Ready : append(node)
else retry == true
Algorithm -> Retry : append(node)
end
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -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.

View File

@@ -0,0 +1,742 @@
# #06 — The Myth of Clean Input
## Problem
Most algorithmic problems begin in roughly the same way:
> Given an array.
> Given a linked list.
> Given a vector of temperatures.
For example:
```cpp
std::vector<float> temperatures;
```
The task may then ask us to find the maximum value, calculate an average, remove duplicates, or perform some other operation.
The input is assumed to:
- already exist;
- have the expected type;
- use the expected representation;
- be free from corruption;
- contain values within valid ranges;
- be ready for use.
This assumption feels so natural that it is rarely noticed.
In real engineering, however, a clean object is rarely the starting point.
More often, it is the final result of a long processing chain.
---
## Typical Interview Thinking
Consider a simple problem:
> Find the maximum temperature.
The candidate receives a ready-to-use container:
```cpp
std::vector<float> temperatures;
```
The solution may be almost trivial:
```cpp
const auto max_temperature =
std::max_element(temperatures.begin(), temperatures.end());
```
From there, the discussion may cover:
- computational complexity;
- memory usage;
- empty input handling;
- use of the standard library;
- possible optimizations.
All attention is focused on the algorithm.
But one question is almost never asked:
> Where did this `std::vector<float>` come from?
Who received the original data?
Who verified the frame?
Who determined the format?
Who converted the raw sensor value into degrees?
Who decided that the resulting number could be trusted?
The interview starts with an already prepared object.
A real system must first create that object.
---
## Clean Input Is Not a Starting Condition
Consider a temperature received from a remote sensor.
At the business-logic level, it may look like this:
```text
23.7 °C
```
But the system may have originally received nothing more than a sequence of bytes:
```text
02 03 00 1A FF 7C 91 4D
```
Before those bytes can become a temperature, the data must pass through several stages:
```text
UART / CAN / TCP
Receive raw bytes
Extract a complete frame
Validate frame length
Verify checksum
Check protocol version
Deserialize the payload
Identify the data source
Convert byte order
Apply scale and offset
Check the physical range
Normalized temperature
Append to std::vector<float>
Find the maximum value
```
The maximum-value algorithm is the final step and may be the simplest step in the entire chain.
---
## The Cost of Clean Input
This declaration looks simple:
```cpp
std::vector<float> temperatures;
```
But that simplicity was not free.
Before business logic can receive such a container, the system may already have had to:
- receive data from an external source;
- identify message boundaries;
- detect corruption;
- check protocol-version compatibility;
- parse a binary representation;
- handle byte order;
- apply scaling;
- recognize reserved or unavailable values;
- verify physical plausibility;
- convert the result into an internal representation.
The algorithm may require one line of code.
The infrastructure that makes that line meaningful may require thousands.
This is why business logic is often simpler than the code surrounding it.
The formula may already be known.
The algorithm may already exist in the standard library.
The real work is ensuring that the transition from the external world to the object expected by that algorithm is correct.
---
## Normalization: Valid Data Can Still Be Incomparable
Not every input problem is caused by corruption.
Sometimes each value is individually valid, but several values represent the same entity in different forms.
Consider a task that searches for duplicate vehicle identifiers.
An interview problem may provide this input:
```text
ABC123
ABC123
ABC123
```
The result is obvious.
A real system may receive:
```text
ABC123
abc123
ABC-123
ABC123
ABC123
```
At the string level, these values are different.
A duplicate-search algorithm will correctly report that they do not match.
At the domain level, however, they may represent the same object.
Before searching for duplicates, the system must define a canonical representation:
- Is character case significant?
- Are separators meaningful?
- Should surrounding whitespace be removed?
- Which characters are permitted?
- Is there a canonical format?
- What should happen when the input is ambiguous?
After normalization, the values may become:
```text
ABC123
ABC123
ABC123
ABC123
ABC123
```
Only now is the duplicate-search algorithm solving the correct problem.
Before normalization, it was comparing representations rather than entities.
---
## Normalization Is Part of the System Model
Normalization can look like little more than string cleanup.
In reality, it expresses domain rules.
For example:
- letter case may be irrelevant for one identifier and essential for another;
- two file paths may refer to the same object while remaining different strings;
- phone numbers may contain different country prefixes and formatting;
- MAC addresses may use different separators;
- timestamps may use different time zones;
- measurements may use different units;
- sensor values may require calibration.
Normalization does not merely answer:
> How should this string be modified?
It answers:
> What does this system consider to be the same value?
There is no universal normalization procedure.
It depends on the protocol, the contract, and the meaning of the data.
---
## Not All Well-Formed Data Is Usable
Successfully parsing a message does not mean that its contents are safe to use.
Consider these temperatures:
```text
23.7
-40.0
65535
NaN
-273.15
```
Every one of these values may be successfully represented as a number.
Their meanings, however, are very different.
`23.7` may be a normal measurement.
`-40.0` may be valid, or it may be the lower limit of the sensor.
`65535` may represent unavailable data.
`NaN` may have appeared after an invalid calculation.
`-273.15` is numerically valid, but for a particular device it almost certainly indicates a problem.
This reveals several different levels of correctness.
### Structural Correctness
Can the message be parsed?
### Protocol Correctness
Does it conform to the expected protocol format and version?
### Numeric Correctness
Can the value be represented using the required type?
### Semantic Correctness
Does the value make sense within the domain?
Syntactic validity does not guarantee meaningful data.
---
## From Raw Data to a Trusted Object
It is useful to view input handling not as one large validation step, but as a sequence of state transitions.
```text
Raw bytes
Framed data
Integrity-checked frame
Parsed message
Normalized values
Semantically valid object
Trusted domain object
Business algorithm
```
At every stage, the system gains stronger guarantees.
Raw bytes promise almost nothing.
After framing, the message boundaries are known.
After integrity checks, there is evidence that the data was not accidentally corrupted.
After parsing, typed fields exist.
After normalization, values use a consistent representation.
After semantic checks, the object is known to be acceptable within the domain.
Only then can the data be treated as trusted by a particular layer of the system.
---
## Trust Must Be Local
This leads to an important architectural principle:
> Data is not simply trusted or untrusted.
It is trusted only relative to a particular contract.
A transport layer may guarantee that:
- the complete frame was received;
- the checksum matches;
- the length is valid.
It cannot guarantee that a temperature is physically meaningful.
A parser may guarantee that:
- message fields were extracted successfully;
- their sizes and types match the protocol.
It cannot determine whether the value is acceptable for a specific device model.
That responsibility belongs to another layer.
Each layer checks its own invariants and passes a stronger representation to the next one.
---
## Every Layer Earns Trust for the Next One
A clean object at an algorithm boundary is not a magical property of the data.
It is the result of fulfilled contracts.
One layer says:
> I verified the integrity of the frame.
The next says:
> I parsed the message according to a supported protocol version.
The next says:
> I converted the values into the system's internal units.
The next says:
> I confirmed that the object is valid within this domain.
Only then may the business logic assume:
> This is a valid temperature.
That assumption is not justified because the external world is reliable.
It is justified because the previous layers did their work.
---
## Why Not Validate Everything Everywhere?
Distrusting input can lead to another bad conclusion:
> Every function should repeat every validation step.
That creates different problems:
- duplicated logic;
- contradictory checks;
- unclear ownership of responsibilities;
- more complex code;
- uncertainty about which guarantees already exist.
A function that accepts raw bytes must not assume that they are safe.
A function that accepts an object which can only be created after successful verification does not need to repeat the entire process.
Good architecture does not eliminate trust.
It makes the origin of trust explicit.
---
## Types as Evidence of the Path Already Taken
One practical way to express this is to use different types for different processing stages.
Instead of passing the same generic object through the entire system, the stages can be represented explicitly:
```cpp
struct RawFrame;
struct VerifiedFrame;
struct ParsedTemperatureMessage;
struct NormalizedTemperature;
```
The interfaces can then reflect the available guarantees:
```cpp
std::optional<VerifiedFrame>
verify_frame(const RawFrame& frame);
std::optional<ParsedTemperatureMessage>
parse_message(const VerifiedFrame& frame);
std::optional<NormalizedTemperature>
normalize_temperature(const ParsedTemperatureMessage& message);
```
Business logic can accept only the normalized value:
```cpp
void process_temperature(const NormalizedTemperature& temperature);
```
This does not make the data absolutely true.
It makes the stages already completed explicit.
It also prevents raw input from being passed accidentally into code that expects a verified object.
---
## What Happens When Processing Fails?
Data evolution does not always end with a valid business object.
Every stage may reject the input:
```text
Raw bytes
├── incomplete frame
├── unsupported version
├── invalid checksum
├── malformed payload
├── unknown sensor
├── invalid scaling
├── out-of-range value
└── valid temperature
```
This introduces another major part of real engineering that is usually absent from algorithmic problems:
- the message may need to be discarded;
- the failure may need to be logged;
- a diagnostic counter may need to be incremented;
- the source may need to be reconnected;
- the system may need to use the last known valid value;
- a component may enter a degraded mode;
- the failure may affect safety-related behavior.
In an interview problem, an invalid value is often just an edge case.
In a real system, it may trigger an entirely different operating scenario.
---
## The Algorithm Still Matters
None of this means that algorithms are unimportant.
Once data has been converted into a correct internal model, the algorithm still needs to be:
- correct;
- efficient;
- understandable;
- appropriate for the system constraints.
The problem begins when solving a task over a clean array is treated as a complete model of engineering ability.
An algorithm solves a problem under a set of assumptions.
An engineer must also:
- discover those assumptions;
- determine whether they are valid;
- assign responsibility for enforcing them;
- express the resulting guarantees in interfaces and architecture.
---
## What This Actually Tests
A problem over a ready-made container can test:
- knowledge of data structures;
- algorithmic reasoning;
- complexity analysis;
- recognition of known patterns;
- implementation accuracy.
It says much less about a candidate's ability to:
- work with external data sources;
- design trust boundaries;
- parse protocols;
- normalize representations;
- define semantic validity;
- design diagnostics;
- handle partial failures;
- create reliable contracts between layers.
This does not make the algorithmic task useless.
It only limits what can reasonably be concluded from it.
---
## Where the Interview Ends and Engineering Begins
An interview problem often presents this model:
```text
Clean input
Algorithm
Result
```
A real system often looks more like this:
```text
Physical world
Electrical signal
Raw bytes
Transport framing
Integrity checks
Protocol parsing
Version handling
Normalization
Semantic validation
Domain object
Algorithm
System decision
```
The interview begins near the end of this chain.
Engineering is responsible for the entire chain.
---
## The Evolution of Data
We can now return to the temperature example.
Initially, the system does not have a temperature.
It has a signal.
Then it has bytes.
Then a frame.
Then a message.
Then a raw sensor value.
Then a value expressed in physical units.
Then a normalized and semantically valid measurement.
Only after all of that does a number appear that can safely be stored in a container and passed to an algorithm.
```text
Signal
Bytes
Frame
Verified frame
Parsed message
Raw sensor value
Calibrated value
Normalized temperature
Trusted domain object
std::vector<float>
std::max_element
```
The maximum-search algorithm does not create the meaning of the data.
It consumes meaning that was established by the previous layers.
---
## Key Takeaway
Clean input is not a starting point.
It is an engineering result.
It exists only after the system has:
- identified the structure of the data;
- verified its integrity;
- understood its format;
- converted it into a canonical representation;
- checked its meaning;
- established a contract of trust.
The engineer's first question is therefore not:
> How do I process this array?
It is:
> Why can this array be trusted?
And then:
> Which layer guarantees that?
---
## Project Perspective
> Exists in real engineering?
> Yes. Almost constantly.
> Exists in interview form?
> Usually not. Most of the data journey is hidden by the problem statement.
Algorithmic tasks are useful for evaluating work on already prepared structures.
But they usually begin with a result that a real system still has to produce.
That is the myth of clean input:
> Data does not arrive ready for the algorithm.
> Engineering makes it ready.