Ring buffer implementation in Embedded C - Time & Space Complexity
We want to understand how the time it takes to run a ring buffer changes as we add more data.
Specifically, how does the number of steps grow when we insert or remove items?
Analyze the time complexity of the following code snippet.
#define BUFFER_SIZE 8
typedef struct {
int buffer[BUFFER_SIZE];
int head;
int tail;
} RingBuffer;
void ring_buffer_put(RingBuffer *rb, int data) {
rb->buffer[rb->head] = data;
rb->head = (rb->head + 1) % BUFFER_SIZE;
}
int ring_buffer_get(RingBuffer *rb) {
int data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % BUFFER_SIZE;
return data;
}
This code adds data to and removes data from a fixed-size circular buffer using simple index updates.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Single-step insert or remove at an index.
- How many times: Each call does exactly one insert or one remove operation.
Each insert or remove takes the same small number of steps, no matter how full the buffer is.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | 1 operation per insert or remove |
| 100 | 1 operation per insert or remove |
| 1000 | 1 operation per insert or remove |
Pattern observation: The time to insert or remove does not grow with the number of items; it stays constant.
Time Complexity: O(1)
This means each insert or remove operation takes the same small amount of time, no matter how many items are in the buffer.
[X] Wrong: "Adding more items makes the insert slower because the buffer is bigger."
[OK] Correct: The ring buffer uses fixed-size indexing and does not shift data, so each insert or remove is always quick and does not depend on buffer size.
Understanding how ring buffers work and their time complexity shows you know how to handle fixed-size data efficiently, a useful skill in many real-world embedded systems.
"What if the ring buffer needed to resize dynamically when full? How would the time complexity change?"
Practice
What is the main purpose of the head pointer in a ring buffer?
Solution
Step 1: Understand the role of head in ring buffer
The head pointer marks the position where the next data element will be stored (written) in the buffer.Step 2: Differentiate head from tail
The tail pointer is used for reading data, while the head is for writing new data.Final Answer:
To indicate where new data will be written next -> Option DQuick Check:
Head = write position [OK]
- Confusing head with tail pointer
- Thinking head stores buffer size
- Assuming head resets the buffer
Which of the following is the correct way to advance the head pointer in a ring buffer of size BUFFER_SIZE after writing one element?
head = ?;Solution
Step 1: Understand circular increment
To move head forward in a circular buffer, increment and wrap around using modulo with buffer size.Step 2: Apply modulo operation
The expression(head + 1) % BUFFER_SIZEensures head wraps to 0 after reaching the end.Final Answer:
head = (head + 1) % BUFFER_SIZE; -> Option CQuick Check:
Use modulo to wrap index [OK]
- Not using modulo causes overflow
- Subtracting instead of adding
- Incorrect wrap calculation
Given this code snippet for a ring buffer of size 4, what will be the value of head and tail after these operations?
int head = 0, tail = 0;
int BUFFER_SIZE = 4;
// Write 3 elements
for (int i = 0; i < 3; i++) {
head = (head + 1) % BUFFER_SIZE;
}
// Read 1 element
tail = (tail + 1) % BUFFER_SIZE;
Solution
Step 1: Calculate head after writing 3 elements
Starting at 0, head increments 3 times with modulo 4: head = (0+1)%4=1, then 2, then 3.Step 2: Calculate tail after reading 1 element
Tail starts at 0, increments once: tail = (0+1)%4=1.Final Answer:
head = 3, tail = 1 -> Option BQuick Check:
Head=3, Tail=1 after ops [OK]
- Forgetting modulo wrap
- Mixing head and tail values
- Counting increments incorrectly
Identify the bug in this ring buffer write function and select the correct fix:
void write(int data) {
if ((head + 1) == tail) {
// Buffer full
return;
}
buffer[head] = data;
head = (head + 1) % BUFFER_SIZE;
}
Solution
Step 1: Analyze buffer full condition
The buffer is full when the next head position wraps to tail, so modulo is needed to wrap index.Step 2: Correct condition with modulo
Using((head + 1) % BUFFER_SIZE) == tailcorrectly detects full buffer.Final Answer:
Change condition to ((head + 1) % BUFFER_SIZE) == tail -> Option AQuick Check:
Modulo needed for circular check [OK]
- Missing modulo causes wrong full detection
- Confusing head and tail roles
- Incorrectly incrementing tail on write
You want to implement a ring buffer that stores bytes and can hold exactly 8 bytes. Which of these buffer sizes and pointer update methods correctly avoids ambiguity between full and empty states?
Options:- Buffer size = 8, full when (head + 1) % 8 == tail
- Buffer size = 9, full when (head + 1) % 9 == tail
- Buffer size = 8, full when head == tail
- Buffer size = 9, full when head == tail
Solution
Step 1: Understand full vs empty ambiguity
When head == tail, buffer is empty. To distinguish full, buffer size is often set to capacity + 1.Step 2: Choose buffer size and condition
Using buffer size 9 for 8 bytes capacity and full condition (head + 1) % 9 == tail avoids ambiguity.Final Answer:
Use buffer size 9 with full condition (head + 1) % 9 == tail -> Option AQuick Check:
Extra slot avoids full/empty confusion [OK]
- Using buffer size equal to capacity causes ambiguity
- Checking full with head == tail
- Not reserving one slot for full detection
Ready to prove it?
Pass the test and this pattern moves unknown
