Paramclasses Academy - Blog

Tutorials Details

Practical DSA Projects in C with 6 Complete Projects with Source Code

Practical DSA Projects in C with 6 Complete Projects with Source Code

October 17, 2026

Introduction to Practical DSA Projects

Data Structures and Algorithms (DSA) help programmers organize information and solve problems efficiently. In previous modules, we learned arrays, linked lists, stacks, queues, trees, graphs, hashing, and algorithm design techniques.

Now we will apply those concepts to six practical projects using the C programming language.

Projects Covered in This Module

No. Project Main Data Structure
1 Student Record Management System Linked List
2 Stack-Based Expression Calculator Stack
3 Queue-Based Token Management System Queue
4 Binary Search Tree Management System Binary Search Tree
5 Contact Management System Linked List
6 Graph-Based Route Finder Graph and BFS

These are educational console applications. They demonstrate core DSA concepts and can be extended into larger applications with persistent storage, input validation, and more advanced features.


Project 1: Student Record Management Using Linked Lists

1.1 Problem Statement

Create a Student Record Management System that allows users to:

  • Add a student record

  • Display all student records

  • Search for a student by ID

  • Delete a student record

We will use a singly linked list because its nodes can be allocated dynamically as new student records are added.

Each node contains a student ID, name, marks, and a pointer to the next node.

1.2 Complete C Program

#include 
#include 
#include 

struct Student {
    int id;
    char name[50];
    float marks;
    struct Student *next;
};

struct Student *head = NULL;

void addStudent(int id, const char name[], float marks) {
    struct Student *newNode = malloc(sizeof(struct Student));

    if (newNode == NULL) {
        printf("Memory allocation failed.\n");
        return;
    }

    newNode->id = id;
    snprintf(newNode->name, sizeof(newNode->name), "%s", name);
    newNode->marks = marks;
    newNode->next = NULL;

    if (head == NULL) {
        head = newNode;
    } else {
        struct Student *current = head;

        while (current->next != NULL) {
            current = current->next;
        }

        current->next = newNode;
    }

    printf("Student added successfully.\n");
}

void displayStudents(void) {
    struct Student *current = head;

    if (current == NULL) {
        printf("No student records found.\n");
        return;
    }

    printf("\n--- Student Records ---\n");

    while (current != NULL) {
        printf("ID: %d | Name: %s | Marks: %.2f\n",
               current->id, current->name, current->marks);
        current = current->next;
    }
}

void searchStudent(int id) {
    struct Student *current = head;

    while (current != NULL) {
        if (current->id == id) {
            printf("Student found: ID=%d, Name=%s, Marks=%.2f\n",
                   current->id, current->name, current->marks);
            return;
        }

        current = current->next;
    }

    printf("Student not found.\n");
}

void deleteStudent(int id) {
    struct Student *current = head;
    struct Student *previous = NULL;

    while (current != NULL && current->id != id) {
        previous = current;
        current = current->next;
    }

    if (current == NULL) {
        printf("Student not found.\n");
        return;
    }

    if (previous == NULL) {
        head = current->next;
    } else {
        previous->next = current->next;
    }

    free(current);
    printf("Student deleted successfully.\n");
}

void freeStudents(void) {
    while (head != NULL) {
        struct Student *temp = head;
        head = head->next;
        free(temp);
    }
}

int main(void) {
    addStudent(101, "Aman", 85.5f);
    addStudent(102, "Priya", 91.0f);
    addStudent(103, "Rahul", 78.5f);

    displayStudents();

    printf("\nSearching for ID 102:\n");
    searchStudent(102);

    printf("\nDeleting ID 101:\n");
    deleteStudent(101);

    displayStudents();

    freeStudents();
    return 0;
}

1.3 Sample Output

Student added successfully.
Student added successfully.
Student added successfully.

--- Student Records ---
ID: 101 | Name: Aman | Marks: 85.50
ID: 102 | Name: Priya | Marks: 91.00
ID: 103 | Name: Rahul | Marks: 78.50

Searching for ID 102:
Student found: ID=102, Name=Priya, Marks=91.00

Deleting ID 101:
Student deleted successfully.

--- Student Records ---
ID: 102 | Name: Priya | Marks: 91.00
ID: 103 | Name: Rahul | Marks: 78.50

1.4 How It Works

  • struct Student defines the data stored in each node.

  • head points to the first node.

  • addStudent() creates a new node and appends it to the list.

  • displayStudents() traverses the list.

  • searchStudent() checks each student ID.

  • deleteStudent() removes the matching node and releases its memory.

  • freeStudents() releases all remaining nodes before the program exits.

Complexity: Adding a student at the end takes (O(n)) because the list is traversed. Displaying, searching, and deleting by ID take (O(n)) in the worst case. Deleting a node is (O(1)) once its location and predecessor are known.

This example assumes IDs are unique but does not enforce uniqueness. A production system should reject duplicate IDs and validate marks.


Project 2: Stack-Based Expression Calculator

2.1 Problem Statement

Create a calculator that evaluates a postfix expression using a stack.

In postfix notation, the operator appears after its operands.

For example:

Infix:   5 + 3 * 2
Postfix: 5 3 2 * +
Result:  11

Postfix expressions can be evaluated using a stack because each operator consumes its operands from the top of the stack.

This implementation accepts single-digit non-negative integers and the operators +, -, *, and /, with tokens separated by spaces.

2.2 Complete C Program

#include 
#include 
#include 
#include 

#define MAX 100

int stack[MAX];
int top = -1;

int push(int value) {
    if (top == MAX - 1) {
        return 0;
    }

    stack[++top] = value;
    return 1;
}

int pop(int *value) {
    if (top == -1) {
        return 0;
    }

    *value = stack[top--];
    return 1;
}

int evaluatePostfix(const char expression[], int *result) {
    char copy[256];

    if (strlen(expression) >= sizeof(copy)) {
        return 0;
    }

    strcpy(copy, expression);
    top = -1;

    char *token = strtok(copy, " \t\n");

    while (token != NULL) {
        if (strlen(token) == 1 &&
            isdigit((unsigned char)token[0])) {
            if (!push(token[0] - '0')) {
                return 0;
            }
        } else if (strlen(token) == 1 &&
                   strchr("+-*/", token[0]) != NULL) {
            int right, left;

            if (!pop(&right) || !pop(&left)) {
                return 0;
            }

            int value;

            switch (token[0]) {
                case '+':
                    value = left + right;
                    break;

                case '-':
                    value = left - right;
                    break;

                case '*':
                    value = left * right;
                    break;

                case '/':
                    if (right == 0) {
                        return 0;
                    }
                    value = left / right;
                    break;

                default:
                    return 0;
            }

            if (!push(value)) {
                return 0;
            }
        } else {
            return 0;
        }

        token = strtok(NULL, " \t\n");
    }

    if (top != 0) {
        return 0;
    }

    return pop(result);
}

int main(void) {
    const char expression[] = "5 3 2 * +";
    int result;

    printf("Postfix expression: %s\n", expression);

    if (evaluatePostfix(expression, &result)) {
        printf("Result: %d\n", result);
    } else {
        printf("Invalid expression or arithmetic error.\n");
    }

    return 0;
}

2.3 Sample Output

Postfix expression: 5 3 2 * +
Result: 11

2.4 How It Works

The program processes the expression from left to right:

  1. Push 5.

  2. Push 3.

  3. Push 2.

  4. Read *, pop 2 and 3, then push 3 * 2 = 6.

  5. Read +, pop 6 and 5, then push 5 + 6 = 11.

  6. The final stack contains the result 11.

Important: Operand order matters for subtraction and division. For example, postfix 8 2 - means 8 - 2, not 2 - 8.

Complexity: For (n) tokens, time complexity is (O(n)). Auxiliary stack space is (O(n)).

This calculator does not support multi-digit numbers, negative-number tokens, parentheses, exponentiation, or infix-to-postfix conversion. Those can be added as extensions.


Project 3: Queue-Based Token Management System

3.1 Problem Statement

Create a simple token-management system for a service counter. Customers receive token numbers and are served in the order they arrive.

A queue follows FIFO (First In, First Out).

We will use a circular array queue so that freed positions at the beginning of the array can be reused.

3.2 Complete C Program

#include 

#define MAX 5

int queue[MAX];
int front = 0;
int rear = 0;
int count = 0;
int nextToken = 1;

int enqueue(int token) {
    if (count == MAX) {
        return 0;
    }

    queue[rear] = token;
    rear = (rear + 1) % MAX;
    count++;

    return 1;
}

int dequeue(int *token) {
    if (count == 0) {
        return 0;
    }

    *token = queue[front];
    front = (front + 1) % MAX;
    count--;

    return 1;
}

void displayQueue(void) {
    if (count == 0) {
        printf("No customers waiting.\n");
        return;
    }

    printf("Waiting tokens: ");

    for (int i = 0; i < count; i++) {
        printf("%d ", queue[(front + i) % MAX]);
    }

    printf("\n");
}

int main(void) {
    int token;

    for (int i = 0; i < 3; i++) {
        token = nextToken++;

        if (enqueue(token)) {
            printf("Token %d issued.\n", token);
        }
    }

    displayQueue();

    if (dequeue(&token)) {
        printf("Now serving token %d.\n", token);
    }

    token = nextToken++;

    if (enqueue(token)) {
        printf("Token %d issued.\n", token);
    }

    displayQueue();

    return 0;
}

3.3 Sample Output

Token 1 issued.
Token 2 issued.
Token 3 issued.
Waiting tokens: 1 2 3
Now serving token 1.
Token 4 issued.
Waiting tokens: 2 3 4

3.4 How It Works

  • enqueue() adds a token at the rear.

  • dequeue() removes the token at the front.

  • front identifies the next customer to be served.

  • rear identifies the next insertion position.

  • count distinguishes an empty queue from a full queue.

  • The modulo operation wraps indices back to zero when the array reaches its end.

Complexity: Enqueue and dequeue both take (O(1)) time. Displaying the queue takes (O(n)).

This program demonstrates the queue mechanics; it does not implement customer identity, persistent token numbers, or a full interactive menu.


Project 4: Binary Search Tree Management System

4.1 Problem Statement

Build a Binary Search Tree (BST) that supports:

  • Inserting values

  • Searching for a value

  • Displaying values in sorted order

In a BST, values smaller than a node are stored in its left subtree, while larger values are stored in its right subtree.

An inorder traversal visits the values in ascending order when duplicate values are not inserted.

4.2 Complete C Program

#include 
#include 

struct Node {
    int data;
    struct Node *left;
    struct Node *right;
};

struct Node *createNode(int value) {
    struct Node *newNode = malloc(sizeof(struct Node));

    if (newNode == NULL) {
        return NULL;
    }

    newNode->data = value;
    newNode->left = NULL;
    newNode->right = NULL;

    return newNode;
}

struct Node *insert(struct Node *root, int value) {
    if (root == NULL) {
        return createNode(value);
    }

    if (value < root->data) {
        root->left = insert(root->left, value);
    } else if (value > root->data) {
        root->right = insert(root->right, value);
    }

    return root;
}

int search(struct Node *root, int key) {
    while (root != NULL) {
        if (root->data == key) {
            return 1;
        }

        if (key < root->data) {
            root = root->left;
        } else {
            root = root->right;
        }
    }

    return 0;
}

void inorder(struct Node *root) {
    if (root == NULL) {
        return;
    }

    inorder(root->left);
    printf("%d ", root->data);
    inorder(root->right);
}

void freeTree(struct Node *root) {
    if (root == NULL) {
        return;
    }

    freeTree(root->left);
    freeTree(root->right);
    free(root);
}

int main(void) {
    struct Node *root = NULL;
    int values[] = {50, 30, 70, 20, 40, 60, 80};
    int n = sizeof(values) / sizeof(values[0]);

    for (int i = 0; i < n; i++) {
        struct Node *newRoot = insert(root, values[i]);

        if (newRoot == NULL && root == NULL) {
            printf("Memory allocation failed.\n");
            return 1;
        }

        root = newRoot;
    }

    printf("Inorder traversal: ");
    inorder(root);
    printf("\n");

    if (search(root, 60)) {
        printf("60 found in the BST.\n");
    } else {
        printf("60 not found.\n");
    }

    freeTree(root);
    return 0;
}

4.3 Sample Output

Inorder traversal: 20 30 40 50 60 70 80
60 found in the BST.

4.4 How It Works

  • createNode() dynamically allocates a tree node.

  • insert() places each value according to BST ordering.

  • search() follows the left or right branch depending on the key.

  • inorder() visits the left subtree, root, and right subtree.

  • freeTree() releases the allocated nodes.

Complexity: Insertion and search take (O(h)), where (h) is the tree height. For a balanced tree, this is (O(\log n)); for a highly skewed tree, it can be (O(n)). Inorder traversal takes (O(n)).

This is an educational BST implementation. A production implementation should propagate allocation failures carefully, and a balanced tree such as AVL or Red-Black Tree is preferable when predictable performance is required.


Project 5: Contact Management System

5.1 Problem Statement

Create a simple contact manager that supports:

  • Adding a contact

  • Displaying all contacts

  • Searching by name

  • Deleting a contact by name

We will use a singly linked list to store a dynamically changing number of contacts.

The following example uses a fixed set of sample contacts and demonstrates the core functions. It is not a persistent phonebook application.

5.2 Complete C Program

#include 
#include 
#include 

struct Contact {
    char name[50];
    char phone[20];
    struct Contact *next;
};

struct Contact *head = NULL;

void addContact(const char name[], const char phone[]) {
    struct Contact *newNode = malloc(sizeof(struct Contact));

    if (newNode == NULL) {
        printf("Memory allocation failed.\n");
        return;
    }

    snprintf(newNode->name, sizeof(newNode->name), "%s", name);
    snprintf(newNode->phone, sizeof(newNode->phone), "%s", phone);

    newNode->next = head;
    head = newNode;

    printf("Contact added.\n");
}

void displayContacts(void) {
    struct Contact *current = head;

    if (current == NULL) {
        printf("No contacts available.\n");
        return;
    }

    printf("\n--- Contacts ---\n");

    while (current != NULL) {
        printf("Name: %s | Phone: %s\n",
               current->name, current->phone);
        current = current->next;
    }
}

void searchContact(const char name[]) {
    struct Contact *current = head;

    while (current != NULL) {
        if (strcmp(current->name, name) == 0) {
            printf("Contact found: %s, Phone: %s\n",
                   current->name, current->phone);
            return;
        }

        current = current->next;
    }

    printf("Contact not found.\n");
}

void deleteContact(const char name[]) {
    struct Contact *current = head;
    struct Contact *previous = NULL;

    while (current != NULL &&
           strcmp(current->name, name) != 0) {
        previous = current;
        current = current->next;
    }

    if (current == NULL) {
        printf("Contact not found.\n");
        return;
    }

    if (previous == NULL) {
        head = current->next;
    } else {
        previous->next = current->next;
    }

    free(current);
    printf("Contact deleted.\n");
}

void freeContacts(void) {
    while (head != NULL) {
        struct Contact *temp = head;
        head = head->next;
        free(temp);
    }
}

int main(void) {
    addContact("Aman", "9000000001");
    addContact("Priya", "9000000002");
    addContact("Rahul", "9000000003");

    displayContacts();

    printf("\nSearching for Priya:\n");
    searchContact("Priya");

    printf("\nDeleting Aman:\n");
    deleteContact("Aman");

    displayContacts();

    freeContacts();
    return 0;
}

5.3 Sample Output

Contact added.
Contact added.
Contact added.

--- Contacts ---
Name: Rahul | Phone: 9000000003
Name: Priya | Phone: 9000000002
Name: Aman | Phone: 9000000001

Searching for Priya:
Contact found: Priya, Phone: 9000000002

Deleting Aman:
Contact deleted.

--- Contacts ---
Name: Rahul | Phone: 9000000003
Name: Priya | Phone: 9000000002

5.4 How It Works

  • Each contact is represented by a node containing a name and phone string.

  • addContact() inserts a new node at the beginning of the list.

  • displayContacts() traverses the list and prints each contact.

  • searchContact() compares names using strcmp().

  • deleteContact() removes the first matching contact.

  • freeContacts() releases remaining allocated memory.

Complexity: Insertion at the head takes (O(1)). Displaying, searching, and deleting by name take (O(n)) in the worst case, excluding the bounded cost of comparing names.

A production contact manager should support user input, duplicate-contact rules, robust validation, and persistent storage in a file or database. Phone numbers are stored as strings because they may contain leading zeroes or symbols.


Project 6: Graph-Based Route Finder

6.1 Problem Statement

Build a basic route finder that represents locations as vertices and connections as edges.

For this example, we will use an unweighted, undirected graph and Breadth-First Search (BFS) to find a path with the fewest edges between two locations.

BFS explores vertices level by level. In an unweighted graph, the first time it reaches a vertex, it has found a shortest path measured by the number of edges.

This is a basic route finder, not a road-navigation system: it does not account for physical distance, travel time, road restrictions, or traffic.

6.2 Complete C Program

#include 

#define V 6

const char *locations[V] = {
    "Home", "Market", "School",
    "Hospital", "Park", "Station"
};

int graph[V][V] = {0};

void addEdge(int u, int v) {
    graph[u][v] = 1;
    graph[v][u] = 1;
}

int bfsRoute(int start, int destination) {
    int queue[V];
    int front = 0, rear = 0;
    int visited[V] = {0};
    int parent[V];

    for (int i = 0; i < V; i++) {
        parent[i] = -1;
    }

    visited[start] = 1;
    queue[rear++] = start;

    while (front < rear) {
        int current = queue[front++];

        if (current == destination) {
            break;
        }

        for (int next = 0; next < V; next++) {
            if (graph[current][next] && !visited[next]) {
                visited[next] = 1;
                parent[next] = current;
                queue[rear++] = next;
            }
        }
    }

    if (!visited[destination]) {
        printf("No route found.\n");
        return 0;
    }

    int path[V];
    int length = 0;

    for (int current = destination;
         current != -1;
         current = parent[current]) {
        path[length++] = current;
    }

    printf("Shortest route: ");

    for (int i = length - 1; i >= 0; i--) {
        printf("%s", locations[path[i]]);

        if (i > 0) {
            printf(" -> ");
        }
    }

    printf("\n");
    printf("Number of edges: %d\n", length - 1);

    return 1;
}

int main(void) {
    addEdge(0, 1);  // Home - Market
    addEdge(0, 2);  // Home - School
    addEdge(1, 3);  // Market - Hospital
    addEdge(2, 4);  // School - Park
    addEdge(4, 3);  // Park - Hospital
    addEdge(3, 5);  // Hospital - Station

    printf("Route from Home to Station:\n");
    bfsRoute(0, 5);

    return 0;
}

6.3 Sample Output

Route from Home to Station:
Shortest route: Home -> Market -> Hospital -> Station
Number of edges: 3

6.4 How It Works

  • Each location is represented by a vertex.

  • addEdge() connects two locations in both directions.

  • bfsRoute() uses a queue to explore the graph level by level.

  • visited[] prevents repeated visits.

  • parent[] stores the previous vertex on the discovered path.

  • After reaching the destination, the program reconstructs the route by following parent links backward.

There are two routes from Home to Station in the example, but the route through Market uses three edges. BFS finds a path with the minimum number of edges; when multiple shortest routes exist, the particular route returned depends on the order in which neighbors are examined.

Complexity: This implementation uses an adjacency matrix, so BFS takes (O(V^2)) time and (O(V)) additional space, excluding the graph storage. An adjacency-list implementation typically takes (O(V+E)) time.

For weighted roads, such as distances or travel times, use an appropriate weighted shortest-path algorithm such as Dijkstra's algorithm when all edge weights are non-negative.


Project Summary and Complexity Comparison

Project Main Data Structure Key Operations Typical Complexity
Student Record Management Singly linked list Add, search, delete (O(n)) for traversal-based operations
Expression Calculator Stack Push, pop, evaluate (O(n)) for (n) tokens
Token Management Circular queue Enqueue, dequeue (O(1)) per operation
BST Management Binary Search Tree Insert, search, inorder (O(h)) insert/search; (O(n)) traversal
Contact Management Singly linked list Add, search, delete (O(1)) head insertion; (O(n)) search/delete
Route Finder Graph with adjacency matrix BFS path finding (O(V^2))

Complexity depends on the chosen representation, implementation, and input shape. In particular, an ordinary BST can become skewed, and graph traversal complexity differs between adjacency matrices and adjacency lists.

How to Compile and Run the C Projects

Save each project's source code in a separate .c file. If you are using GCC, compile a project like this:

gcc student_management.c -o student_management

Run it on Linux or macOS:

./student_management

On Windows, run:

student_management.exe

Repeat the process using the appropriate source filename for each project.

For debugging, compile with warnings enabled:

gcc -Wall -Wextra -std=c11 student_management.c -o student_management

Warnings help identify common mistakes. For larger projects, test empty inputs, invalid values, memory-allocation failures, and boundary cases.

Final Conclusion — Complete DSA Course in C

Congratulations! You have reached the final module of the Param Classes DSA course in C.

Across these 12 modules, you studied the foundations of data structures, arrays, searching, sorting, linked lists, stacks, queues, trees, graphs, hashing, and algorithm design techniques. In this final module, you applied those concepts to six practical projects.

The most important lesson is to understand not only how a data structure works, but also why it is suitable for a particular problem. A linked list is useful for dynamically allocated records, a queue supports FIFO processing, a stack supports expression evaluation, a BST organizes ordered keys, and a graph models connections between locations.

These projects are starting points. Extend them with menus, persistent storage, input validation, unit tests, and stronger error handling to develop more complete applications.