- Published on
Implementing Stacks and Queues in C Intermediate C Concepts Part 3
- Authors
- Name
- Md Nasim Sheikh
- @nasimStg
Welcome back to our Intermediate C series! Having tackled dynamic memory allocation and explored the flexibility of linked lists, we're now ready to implement two crucial Abstract Data Types (ADTs): Stacks and Queues. These aren't specific data structures themselves, but rather concepts defining how data can be stored and accessed, often relying on arrays or linked lists for their underlying implementation. Let's dive into the worlds of LIFO and FIFO!
Table of Contents
- What are Stacks and Queues?
- I. Stacks (LIFO)
- Stack Implementation using Arrays
- Stack Implementation using Linked Lists
- II. Queues (FIFO)
- Queue Implementation using Linked Lists
- Queue Implementation using Arrays (Circular Queue - Conceptual)
- Choosing Between Array and Linked List Implementations
- Conclusion
- Further Reading & Related Posts
What are Stacks and Queues?
Stacks and Queues are linear data structures that restrict how elements can be added or removed.
- Stack: Follows the LIFO (Last-In, First-Out) principle. Imagine a stack of plates – the last plate you put on top is the first one you take off.
- Queue: Follows the FIFO (First-In, First-Out) principle. Think of a queue or line for tickets – the first person in line is the first person served.
They are called Abstract Data Types because we define what they do (their operations and behavior) separately from how they are implemented (using arrays, linked lists, etc.).
I. Stacks (LIFO)
A stack primarily supports two main operations: pushing (adding) an element onto the top and popping (removing) an element from the top.
Core Stack Operations:
push(item)
: Add an item to the top of the stack.pop()
: Remove and return the item from the top of the stack.peek()
ortop()
: Return the top item without removing it.isEmpty()
: Check if the stack is empty.isFull()
: (Relevant for array-based implementation) Check if the stack is full.
Applications: Function call management (the call stack), expression evaluation (infix to postfix conversion), backtracking algorithms (like solving mazes), undo/redo features in software.
Stack Implementation using Arrays
This is often the simplest implementation if the maximum size is known beforehand.
Structure:
#include <stdio.h>
#include <stdlib.h> // For malloc/free if creating dynamically
#include <limits.h> // For INT_MIN or other error indicators
#define MAX_SIZE 100 // Example fixed size
typedef struct {
int items[MAX_SIZE]; // Array to hold stack elements
int top; // Index of the top element (-1 means empty)
// Optionally add 'capacity' if using dynamic arrays
} StackArray;
// Function to create/initialize a stack
StackArray* createStackArray() {
// Could allocate dynamically: StackArray* stack = malloc(sizeof(StackArray));
// For simplicity here, we'll use a static/global or local instance
// If allocated dynamically, remember to check for NULL and free later.
static StackArray stack; // Example using static storage
stack.top = -1; // Initialize stack as empty
return &stack;
}
// Check if the stack is full
int isFull_Array(StackArray* stack) {
return stack->top == MAX_SIZE - 1;
}
// Check if the stack is empty
int isEmpty_Array(StackArray* stack) {
return stack->top == -1;
}
// Push operation
void push_Array(StackArray* stack, int item) {
if (isFull_Array(stack)) {
fprintf(stderr, "Stack Overflow! Cannot push %d.\n", item);
return;
}
// Increment top, then add item
stack->items[++(stack->top)] = item;
printf("Pushed %d onto the array stack.\n", item);
}
// Pop operation
int pop_Array(StackArray* stack) {
if (isEmpty_Array(stack)) {
fprintf(stderr, "Stack Underflow! Cannot pop.\n");
return INT_MIN; // Or some other error indicator
}
// Return item at top, then decrement top
printf("Popped %d from the array stack.\n", stack->items[stack->top]);
return stack->items[(stack->top)--];
}
// Peek operation
int peek_Array(StackArray* stack) {
if (isEmpty_Array(stack)) {
fprintf(stderr, "Stack is empty! Cannot peek.\n");
return INT_MIN; // Error indicator
}
return stack->items[stack->top];
}
// Example Usage (Array Stack)
void exampleArrayStack() {
printf("\n--- Array Stack Example ---\n");
StackArray* myStack = createStackArray();
push_Array(myStack, 10);
push_Array(myStack, 20);
push_Array(myStack, 30);
printf("Top element is: %d\n", peek_Array(myStack)); // Should be 30
pop_Array(myStack); // Popped 30
pop_Array(myStack); // Popped 20
printf("Is stack empty? %s\n", isEmpty_Array(myStack) ? "Yes" : "No"); // No
pop_Array(myStack); // Popped 10
printf("Is stack empty? %s\n", isEmpty_Array(myStack) ? "Yes" : "No"); // Yes
pop_Array(myStack); // Stack Underflow!
printf("--- End Array Stack Example ---\n");
}
Explanation:
- We use an array
items
to store data. - An integer
top
tracks the index of the last inserted element.top = -1
indicates an empty stack. push
: Incrementstop
, then places the item atitems[top]
. Checks for overflow first.pop
: Returns the item atitems[top]
, then decrementstop
. Checks for underflow first.peek
: Returns the item atitems[top]
without modifyingtop
.- Limitation: Fixed size (
MAX_SIZE
). You could usemalloc
/realloc
for a dynamic array, but that adds complexity.
Stack Implementation using Linked Lists
This leverages the efficiency of adding/removing from the beginning of a linked list.
Structure: We need the Node structure from the previous tutorial.
// Re-using or redefining Node structure
typedef struct Node {
int data;
struct Node* next;
} Node;
// Stack structure only needs a pointer to the top node (head of the list)
typedef struct {
Node* top; // Points to the top node (head of the linked list)
} StackLL;
// Function to create an empty stack
StackLL* createStackLL() {
StackLL* stack = (StackLL*)malloc(sizeof(StackLL));
if (!stack) {
perror("Failed to allocate stack");
return NULL;
}
stack->top = NULL; // Initialize stack as empty
return stack;
}
// Check if the stack is empty
int isEmpty_LL(StackLL* stack) {
return stack->top == NULL;
}
// Push operation (equivalent to inserting at the beginning of LL)
void push_LL(StackLL* stack, int data) {
// Create a new node
Node* newNode = (Node*)malloc(sizeof(Node));
if (!newNode) {
perror("Failed to allocate node for push");
return; // Or handle error more gracefully
}
newNode->data = data;
// Link the new node to the current top
newNode->next = stack->top;
// Update the top to point to the new node
stack->top = newNode;
printf("Pushed %d onto the linked list stack.\n", data);
}
// Pop operation (equivalent to deleting the first node of LL)
int pop_LL(StackLL* stack) {
if (isEmpty_LL(stack)) {
fprintf(stderr, "Stack Underflow! Cannot pop from linked list stack.\n");
return INT_MIN; // Error indicator
}
// Store the current top node
Node* temp = stack->top;
int poppedData = temp->data;
// Update the top to the next node
stack->top = stack->top->next;
// Free the old top node
free(temp);
temp = NULL;
printf("Popped %d from the linked list stack.\n", poppedData);
return poppedData;
}
// Peek operation
int peek_LL(StackLL* stack) {
if (isEmpty_LL(stack)) {
fprintf(stderr, "Stack is empty! Cannot peek linked list stack.\n");
return INT_MIN; // Error indicator
}
return stack->top->data;
}
// Function to free the entire stack (essential!)
void freeStackLL(StackLL* stack) {
Node* current = stack->top;
Node* nextNode;
printf("Freeing linked list stack...\n");
while (current != NULL) {
nextNode = current->next;
free(current);
current = nextNode;
}
free(stack); // Free the stack structure itself
printf("Linked list stack freed.\n");
}
// Example Usage (Linked List Stack)
void exampleLLStack() {
printf("\n--- Linked List Stack Example ---\n");
StackLL* myStack = createStackLL();
if (!myStack) return; // Allocation check
push_LL(myStack, 100);
push_LL(myStack, 200);
push_LL(myStack, 300);
printf("Top element is: %d\n", peek_LL(myStack)); // Should be 300
pop_LL(myStack); // Popped 300
pop_LL(myStack); // Popped 200
printf("Is stack empty? %s\n", isEmpty_LL(myStack) ? "Yes" : "No"); // No
pop_LL(myStack); // Popped 100
printf("Is stack empty? %s\n", isEmpty_LL(myStack) ? "Yes" : "No"); // Yes
pop_LL(myStack); // Stack Underflow!
// IMPORTANT: Free the stack memory
freeStackLL(myStack);
printf("--- End Linked List Stack Example ---\n");
}
Explanation:
- The stack only needs a
top
pointer, which functions exactly like thehead
pointer in our basic linked list. push
: Creates a new node and inserts it at the beginning of the list (updatingtop
). This is O(1).pop
: Removes the first node (the onetop
points to), updatestop
to the next node, andfree
s the removed node. This is O(1).peek
: Returns the data from thetop
node.- Advantage: Dynamic size, no artificial limit (only available memory).
II. Queues (FIFO)
A queue primarily supports adding elements to the rear (back) and removing elements from the front.
Core Queue Operations:
enqueue(item)
: Add an item to the rear of the queue.dequeue()
: Remove and return the item from the front of the queue.peek()
orfront()
: Return the front item without removing it.isEmpty()
: Check if the queue is empty.isFull()
: (Relevant for array-based implementation) Check if the queue is full.
Applications: Scheduling tasks (CPU scheduling, print queues), handling asynchronous requests (web server requests), Breadth-First Search (BFS) in graphs, buffers in data streaming.
Queue Implementation using Linked Lists
This is often the most intuitive way to implement a queue, directly modeling the "line" concept. We need pointers to both the front and rear of the list for efficiency.
Structure:
// Re-using or redefining Node structure
// typedef struct Node { ... } Node; (Assuming defined above)
// Queue structure needs pointers to both front and rear nodes
typedef struct {
Node* front; // Points to the front node (head of the list)
Node* rear; // Points to the rear node (tail of the list)
} QueueLL;
// Function to create an empty queue
QueueLL* createQueueLL() {
QueueLL* queue = (QueueLL*)malloc(sizeof(QueueLL));
if (!queue) {
perror("Failed to allocate queue");
return NULL;
}
queue->front = NULL;
queue->rear = NULL;
return queue;
}
// Check if the queue is empty
int isEmpty_QueueLL(QueueLL* queue) {
return queue->front == NULL; // If front is NULL, queue is empty
}
// Enqueue operation (add to the rear/tail)
void enqueue_LL(QueueLL* queue, int data) {
// Create a new node
Node* newNode = (Node*)malloc(sizeof(Node));
if (!newNode) {
perror("Failed to allocate node for enqueue");
return;
}
newNode->data = data;
newNode->next = NULL; // Node added at the end always points to NULL
// If queue is empty, new node is both front and rear
if (queue->rear == NULL) { // or check isEmpty_QueueLL(queue)
queue->front = newNode;
queue->rear = newNode;
printf("Enqueued %d (first element).\n", data);
return;
}
// Link the current rear node to the new node
queue->rear->next = newNode;
// Update the rear pointer to the new node
queue->rear = newNode;
printf("Enqueued %d to the rear.\n", data);
}
// Dequeue operation (remove from the front/head)
int dequeue_LL(QueueLL* queue) {
if (isEmpty_QueueLL(queue)) {
fprintf(stderr, "Queue Underflow! Cannot dequeue.\n");
return INT_MIN; // Error indicator
}
// Store the current front node
Node* temp = queue->front;
int dequeuedData = temp->data;
// Move front pointer to the next node
queue->front = queue->front->next;
// VERY IMPORTANT: If front becomes NULL, the queue is now empty,
// so rear must also become NULL!
if (queue->front == NULL) {
queue->rear = NULL;
}
// Free the old front node
free(temp);
temp = NULL;
printf("Dequeued %d from the front.\n", dequeuedData);
return dequeuedData;
}
// Peek/Front operation
int peek_QueueLL(QueueLL* queue) {
if (isEmpty_QueueLL(queue)) {
fprintf(stderr, "Queue is empty! Cannot peek.\n");
return INT_MIN; // Error indicator
}
return queue->front->data;
}
// Function to free the entire queue
void freeQueueLL(QueueLL* queue) {
Node* current = queue->front;
Node* nextNode;
printf("Freeing linked list queue...\n");
while (current != NULL) {
nextNode = current->next;
free(current);
current = nextNode;
}
free(queue); // Free the queue structure itself
printf("Linked list queue freed.\n");
}
// Example Usage (Linked List Queue)
void exampleLLQueue() {
printf("\n--- Linked List Queue Example ---\n");
QueueLL* myQueue = createQueueLL();
if (!myQueue) return;
enqueue_LL(myQueue, 10);
enqueue_LL(myQueue, 20);
enqueue_LL(myQueue, 30);
printf("Front element is: %d\n", peek_QueueLL(myQueue)); // Should be 10
dequeue_LL(myQueue); // Dequeued 10
printf("Front element is: %d\n", peek_QueueLL(myQueue)); // Should be 20
enqueue_LL(myQueue, 40);
dequeue_LL(myQueue); // Dequeued 20
dequeue_LL(myQueue); // Dequeued 30
printf("Is queue empty? %s\n", isEmpty_QueueLL(myQueue) ? "Yes" : "No"); // No
dequeue_LL(myQueue); // Dequeued 40
printf("Is queue empty? %s\n", isEmpty_QueueLL(myQueue) ? "Yes" : "No"); // Yes
dequeue_LL(myQueue); // Queue Underflow!
// IMPORTANT: Free the queue memory
freeQueueLL(myQueue);
printf("--- End Linked List Queue Example ---\n");
}
Explanation:
- The queue structure holds two pointers:
front
(likehead
) andrear
(points to the last node). enqueue
: Creates a new node. If the queue is empty, sets bothfront
andrear
to the new node. Otherwise, appends the node after the currentrear
and updatesrear
. This is O(1).dequeue
: Removes the node pointed to byfront
. Updatesfront
to the next node. Crucially, iffront
becomesNULL
after the update (meaning the last element was dequeued),rear
must also be set toNULL
. Frees the old front node. This is O(1).peek
: Returns data from thefront
node.- Advantage: Efficient O(1) time complexity for both enqueue and dequeue, fully dynamic size.
Queue Implementation using Arrays (Circular Queue - Conceptual)
Implementing a queue efficiently with a standard array is tricky because dequeuing from the front requires shifting all other elements (O(n)). A Circular Array (or Ring Buffer) solves this.
Concept:
- Use an array and two indices:
front
(index of the first element) andrear
(index after the last element). - When
front
orrear
reach the end of the array, they wrap around to the beginning using the modulo operator (% capacity
). - Special conditions are needed to distinguish between a full queue and an empty queue (e.g., keeping a
count
of elements, or leaving one slot unused).
While powerful, the index management and full/empty checks in circular queues can be complex to implement correctly compared to the linked list approach. For brevity and clarity, we focused on the linked list queue implementation above, which is often preferred for its simplicity and dynamic nature in C when fixed capacity isn't a strict requirement.
Choosing Between Array and Linked List Implementations
- Stack:
- Array: Simpler if max size is known, potentially slightly faster due to memory locality. Risk of overflow if max size is underestimated.
- Linked List: More flexible (dynamic size), no overflow risk (besides running out of memory), O(1) push/pop are naturally efficient. Generally preferred unless performance is absolutely critical and size is fixed.
- Queue:
- Circular Array: Can be very efficient if implemented correctly, good memory locality. Fixed size, complex index management.
- Linked List: Much simpler logic for enqueue/dequeue, dynamic size, O(1) operations are straightforward. Often the preferred implementation in C for general-purpose queues.
Conclusion
Stacks (LIFO) and Queues (FIFO) are fundamental abstract data types with numerous applications in computer science. We've seen how to implement them in C using both fixed-size arrays and, more flexibly, dynamic linked lists. Understanding the underlying principles and the trade-offs between implementation methods allows you to choose the right tool for the job. Mastering these is another key step in becoming a proficient C programmer.
Next up, we'll venture into non-linear structures as we get an Introduction to Trees and Graphs in C!