Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Stacks and Queues: Core Operations and Data Structures

Tech 2

A stack is a linear data structure adhering to the Last-In-First-Out (LIFO) principle. The top of the stack is where elements are added and removed, while the bottom remains fixed.

Common Stack Operations

Standard stack operations include push (add to top), pop (remove from top), and peek (inspect top element), all typically executed in constant O(1) time.

Implementing a Stack with a Linked List

When using a linked list, the head node serves as the stack top. Pushing involves inserting a new node at the head, and popping removes the head node.

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

struct Node {
    int data;
    struct Node* next;
};

typedef struct {
    struct Node* top;
    int count;
} LinkedStack;

LinkedStack* createStack() {
    LinkedStack* s = (LinkedStack*)malloc(sizeof(LinkedStack));
    s->top = NULL;
    s->count = 0;
    return s;
}

bool isStackEmpty(LinkedStack* s) {
    return s->count == 0;
}

void stackPush(LinkedStack* s, int value) {
    struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
    newNode->data = value;
    newNode->next = s->top;
    s->top = newNode;
    s->count++;
}

int stackPop(LinkedStack* s) {
    if (isStackEmpty(s)) {
        printf("Stack underflow.\n");
        exit(1);
    }
    struct Node* temp = s->top;
    int poppedValue = temp->data;
    s->top = s->top->next;
    free(temp);
    s->count--;
    return poppedValue;
}

int stackPeek(LinkedStack* s) {
    if (isStackEmpty(s)) {
        printf("Stack is empty.\n");
        exit(1);
    }
    return s->top->data;
}

void freeStack(LinkedStack* s) {
    while (!isStackEmpty(s)) {
        stackPop(s);
    }
    free(s);
}

int main() {
    LinkedStack* myStack = createStack();
    stackPush(myStack, 10);
    stackPush(myStack, 20);
    stackPush(myStack, 30);
    printf("Top element: %d\n", stackPeek(myStack));
    printf("Popped: %d\n", stackPop(myStack));
    printf("Popped: %d\n", stackPop(myStack));
    freeStack(myStack);
    return 0;
}

Implementing a Stack with an Array

Using an array, the end of the array is treated as the stack top. A dynamic array can handle resizing automatically.

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef struct {
    int* items;
    int capacity;
    int topIndex;
} ArrayStack;

ArrayStack* initArrayStack(int cap) {
    ArrayStack* stk = (ArrayStack*)malloc(sizeof(ArrayStack));
    stk->capacity = cap;
    stk->items = (int*)malloc(cap * sizeof(int));
    stk->topIndex = -1;
    return stk;
}

bool isArrayStackFull(ArrayStack* stk) {
    return stk->topIndex == stk->capacity - 1;
}

bool isArrayStackEmpty(ArrayStack* stk) {
    return stk->topIndex == -1;
}

void arrayStackPush(ArrayStack* stk, int val) {
    if (isArrayStackFull(stk)) {
        printf("Stack overflow.\n");
        return;
    }
    stk->items[++(stk->topIndex)] = val;
}

int arrayStackPop(ArrayStack* stk) {
    if (isArrayStackEmpty(stk)) {
        printf("Stack underflow.\n");
        exit(1);
    }
    return stk->items[(stk->topIndex)--];
}

int arrayStackPeek(ArrayStack* stk) {
    if (isArrayStackEmpty(stk)) {
        printf("Stack is empty.\n");
        exit(1);
    }
    return stk->items[stk->topIndex];
}

void destroyArrayStack(ArrayStack* stk) {
    free(stk->items);
    free(stk);
}

int main() {
    ArrayStack* stack = initArrayStack(5);
    arrayStackPush(stack, 5);
    arrayStackPush(stack, 15);
    arrayStackPush(stack, 25);
    printf("Top: %d\n", arrayStackPeek(stack));
    printf("Popped: %d\n", arrayStackPop(stack));
    destroyArrayStack(stack);
    return 0;
}

Comparison of Implementations

An array-based stack generally offers better cache locality and average performance, though occasional resizing can cause O(n) push operations. A linked-list stack provides more consistent O(1) performance for all operations but uses more memory per element due to pointer storage.

Typical Stack Applications

  • Browser Navigation: The back button uses a stack; each new page is pushed onto a stack, and going back pops the top page.
  • Function Call Management: The call stack manages function contexts, pushing frames for new calls and popping them upon return.
  • Undo/Redo Operations: Many applications use stacks to record actions for undo functionality.

Queue

A queue is a linear data structure following the First-In-First-Out (FIFO) rule. Elements are added at the rear and removed from the front.

Common Queue Operations

Standard operations are enqueue (add to rear), dequeue (remove from front), and front (inspect first element), all typically O(1).

Implementing a Queue with a Linked List

The front of the list serves as the queue head, and the rear as the tail. Enqueue adds a node after the tail, dequeue removes the head node.

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

struct QNode {
    int key;
    struct QNode* link;
};

typedef struct {
    struct QNode *head, *tail;
    int length;
} ListQueue;

ListQueue* createQueue() {
    ListQueue* q = (ListQueue*)malloc(sizeof(ListQueue));
    q->head = q->tail = NULL;
    q->length = 0;
    return q;
}

void enqueue(ListQueue* q, int k) {
    struct QNode* temp = (struct QNode*)malloc(sizeof(struct QNode));
    temp->key = k;
    temp->link = NULL;
    if (q->tail == NULL) {
        q->head = q->tail = temp;
    } else {
        q->tail->link = temp;
        q->tail = temp;
    }
    q->length++;
}

int dequeue(ListQueue* q) {
    if (q->head == NULL) {
        printf("Queue is empty.\n");
        return -1;
    }
    struct QNode* oldHead = q->head;
    int item = oldHead->key;
    q->head = q->head->link;
    if (q->head == NULL) {
        q->tail = NULL;
    }
    free(oldHead);
    q->length--;
    return item;
}

int queueFront(ListQueue* q) {
    if (q->head == NULL) {
        printf("Queue is empty.\n");
        return -1;
    }
    return q->head->key;
}

bool isQueueEmpty(ListQueue* q) {
    return q->head == NULL;
}

void freeQueue(ListQueue* q) {
    while (!isQueueEmpty(q)) {
        dequeue(q);
    }
    free(q);
}

int main() {
    ListQueue* q = createQueue();
    enqueue(q, 100);
    enqueue(q, 200);
    printf("Front: %d\n", queueFront(q));
    printf("Dequeued: %d\n", dequeue(q));
    freeQueue(q);
    return 0;
}

Implementing a Circular Queue with an Array

A standard array queue suffers from O(n) dequeue operations due to shifting. A circular array implementation uses front and rear indices with modulo arithmetic to wrap around.

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef struct {
    int* arr;
    int frontIdx;
    int rearIdx;
    int maxSize;
    int currentSize;
} CircularQueue;

CircularQueue* initCircularQueue(int size) {
    CircularQueue* cq = (CircularQueue*)malloc(sizeof(CircularQueue));
    cq->maxSize = size;
    cq->arr = (int*)malloc(size * sizeof(int));
    cq->frontIdx = 0;
    cq->rearIdx = -1;
    cq->currentSize = 0;
    return cq;
}

bool isCQFull(CircularQueue* cq) {
    return cq->currentSize == cq->maxSize;
}

bool isCQEmpty(CircularQueue* cq) {
    return cq->currentSize == 0;
}

void cqEnqueue(CircularQueue* cq, int val) {
    if (isCQFull(cq)) {
        printf("Queue is full.\n");
        return;
    }
    cq->rearIdx = (cq->rearIdx + 1) % cq->maxSize;
    cq->arr[cq->rearIdx] = val;
    cq->currentSize++;
}

int cqDequeue(CircularQueue* cq) {
    if (isCQEmpty(cq)) {
        printf("Queue is empty.\n");
        return -1;
    }
    int item = cq->arr[cq->frontIdx];
    cq->frontIdx = (cq->frontIdx + 1) % cq->maxSize;
    cq->currentSize--;
    return item;
}

int cqPeek(CircularQueue* cq) {
    if (isCQEmpty(cq)) {
        printf("Queue is empty.\n");
        return -1;
    }
    return cq->arr[cq->frontIdx];
}

void destroyCQ(CircularQueue* cq) {
    free(cq->arr);
    free(cq);
}

int main() {
    CircularQueue* myQueue = initCircularQueue(3);
    cqEnqueue(myQueue, 7);
    cqEnqueue(myQueue, 14);
    printf("Front: %d\n", cqPeek(myQueue));
    printf("Dequeued: %d\n", cqDequeue(myQueue));
    destroyCQ(myQueue);
    return 0;
}

Typical Queue Applications

  • Task Scheduling: Print job queues, restaurant order systems, and network packet buffers.
  • Order Processing: E-commerce platforms manage incoming orders in queues.

Double-Ended Queue (Deque)

A deque allows insertion and deletion at both ends, combining stack and queue capabilities.

Common Deque Operations

Operations include addFront, addRear, removeFront, removeRear, getFront, and getRear, all O(1).

Implementing a Deque with a Doubly Linked List

A doubly linked list, with pointers to both next and previous nodes, naturally supports operations at both ends.

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

struct DNode {
    int info;
    struct DNode* prev;
    struct DNode* next;
};

typedef struct {
    struct DNode* first;
    struct DNode* last;
    int numElements;
} DoublyLinkedDeque;

DoublyLinkedDeque* createDeque() {
    DoublyLinkedDeque* dq = (DoublyLinkedDeque*)malloc(sizeof(DoublyLinkedDeque));
    dq->first = dq->last = NULL;
    dq->numElements = 0;
    return dq;
}

bool isDequeEmpty(DoublyLinkedDeque* dq) {
    return dq->first == NULL;
}

void insertFront(DoublyLinkedDeque* dq, int data) {
    struct DNode* newNode = (struct DNode*)malloc(sizeof(struct DNode));
    newNode->info = data;
    newNode->prev = NULL;
    newNode->next = dq->first;
    if (isDequeEmpty(dq)) {
        dq->last = newNode;
    } else {
        dq->first->prev = newNode;
    }
    dq->first = newNode;
    dq->numElements++;
}

void insertRear(DoublyLinkedDeque* dq, int data) {
    struct DNode* newNode = (struct DNode*)malloc(sizeof(struct DNode));
    newNode->info = data;
    newNode->next = NULL;
    newNode->prev = dq->last;
    if (isDequeEmpty(dq)) {
        dq->first = newNode;
    } else {
        dq->last->next = newNode;
    }
    dq->last = newNode;
    dq->numElements++;
}

int deleteFront(DoublyLinkedDeque* dq) {
    if (isDequeEmpty(dq)) {
        printf("Deque empty.\n");
        return -1;
    }
    struct DNode* temp = dq->first;
    int value = temp->info;
    dq->first = dq->first->next;
    if (dq->first == NULL) {
        dq->last = NULL;
    } else {
        dq->first->prev = NULL;
    }
    free(temp);
    dq->numElements--;
    return value;
}

int deleteRear(DoublyLinkedDeque* dq) {
    if (isDequeEmpty(dq)) {
        printf("Deque empty.\n");
        return -1;
    }
    struct DNode* temp = dq->last;
    int value = temp->info;
    dq->last = dq->last->prev;
    if (dq->last == NULL) {
        dq->first = NULL;
    } else {
        dq->last->next = NULL;
    }
    free(temp);
    dq->numElements--;
    return value;
}

int getFront(DoublyLinkedDeque* dq) {
    if (isDequeEmpty(dq)) {
        printf("Deque empty.\n");
        return -1;
    }
    return dq->first->info;
}

int getRear(DoublyLinkedDeque* dq) {
    if (isDequeEmpty(dq)) {
        printf("Deque empty.\n");
        return -1;
    }
    return dq->last->info;
}

void freeDeque(DoublyLinkedDeque* dq) {
    while (!isDequeEmpty(dq)) {
        deleteFront(dq);
    }
    free(dq);
}

int main() {
    DoublyLinkedDeque* d = createDeque();
    insertRear(d, 8);
    insertFront(d, 4);
    insertRear(d, 12);
    printf("Front: %d, Rear: %d\n", getFront(d), getRear(d));
    printf("Removed from front: %d\n", deleteFront(d));
    freeDeque(d);
    return 0;
}

Deque Applications

Deques are versatile. A common use is an undo/redo system with history limits: actions are pushed onto a deque. When the capacity is exceeded, the oldest action (at the front) can be removed, which a simple stack cannot do efficiently.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.