Welcome to an intensive workout for your C programming muscles! 💪 In this comprehensive guide, we'll dive deep into a series of challenging C exercises designed to sharpen your coding skills and deepen your understanding of this powerful language. Whether you're a beginner looking to solidify your foundation or an experienced developer aiming to refine your expertise, these exercises will push you to new heights in your C programming journey.

1. The FizzBuzz Challenge

Let's start with a classic programming exercise that's deceptively simple yet remarkably effective at testing your grasp of basic C concepts.

📝 Exercise: Write a program that prints the numbers from 1 to 100. But for multiples of three, print "Fizz" instead of the number, and for multiples of five, print "Buzz". For numbers which are multiples of both three and five, print "FizzBuzz".

Here's a solution to get you started:

#include <stdio.h>

int main() {
    for (int i = 1; i <= 100; i++) {
        if (i % 3 == 0 && i % 5 == 0) {
            printf("FizzBuzz\n");
        } else if (i % 3 == 0) {
            printf("Fizz\n");
        } else if (i % 5 == 0) {
            printf("Buzz\n");
        } else {
            printf("%d\n", i);
        }
    }
    return 0;
}

This program uses a for loop to iterate through numbers 1 to 100. The modulus operator % is used to check divisibility. The if-else statements determine what to print based on the divisibility rules.

🧠 Challenge: Can you modify this program to accept user input for the range of numbers and the divisors for "Fizz" and "Buzz"?

2. Reverse a String

String manipulation is a crucial skill in C programming. Let's practice with a string reversal exercise.

📝 Exercise: Write a function that reverses a string in-place (without using additional memory).

Here's a solution:

#include <stdio.h>
#include <string.h>

void reverse_string(char* str) {
    int length = strlen(str);
    int i, j;
    char temp;

    for (i = 0, j = length - 1; i < j; i++, j--) {
        temp = str[i];
        str[i] = str[j];
        str[j] = temp;
    }
}

int main() {
    char str[] = "Hello, World!";
    printf("Original string: %s\n", str);

    reverse_string(str);
    printf("Reversed string: %s\n", str);

    return 0;
}

This program defines a reverse_string function that swaps characters from the start and end of the string, moving towards the center. The main function demonstrates its usage.

Output:

Original string: Hello, World!
Reversed string: !dlroW ,olleH

🧠 Challenge: Can you modify this function to reverse individual words in a sentence while keeping their order intact? For example, "Hello World" should become "olleH dlroW".

3. Prime Number Generator

Understanding prime numbers is fundamental in many areas of computer science, especially cryptography. Let's write a prime number generator.

📝 Exercise: Write a function that generates all prime numbers up to a given limit.

Here's an implementation using the Sieve of Eratosthenes algorithm:

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

void generate_primes(int limit) {
    bool *is_prime = (bool*)calloc(limit + 1, sizeof(bool));
    for (int i = 2; i <= limit; i++) {
        is_prime[i] = true;
    }

    for (int i = 2; i * i <= limit; i++) {
        if (is_prime[i]) {
            for (int j = i * i; j <= limit; j += i) {
                is_prime[j] = false;
            }
        }
    }

    printf("Prime numbers up to %d are:\n", limit);
    for (int i = 2; i <= limit; i++) {
        if (is_prime[i]) {
            printf("%d ", i);
        }
    }
    printf("\n");

    free(is_prime);
}

int main() {
    int limit = 50;
    generate_primes(limit);
    return 0;
}

This program uses the Sieve of Eratosthenes algorithm to efficiently generate prime numbers. It creates a boolean array where each index represents a number, and true indicates that the number is prime. The algorithm then marks multiples of each prime number as non-prime.

Output:

Prime numbers up to 50 are:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47

🧠 Challenge: Can you modify this program to find the nth prime number? For example, if the user inputs 10, the program should output the 10th prime number.

4. Matrix Multiplication

Matrix operations are crucial in many scientific and engineering applications. Let's implement matrix multiplication.

📝 Exercise: Write a program that multiplies two matrices.

Here's a solution:

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

void multiply_matrices(int **a, int **b, int **result, int m, int n, int p) {
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < p; j++) {
            result[i][j] = 0;
            for (int k = 0; k < n; k++) {
                result[i][j] += a[i][k] * b[k][j];
            }
        }
    }
}

void print_matrix(int **matrix, int rows, int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int m = 2, n = 3, p = 2;

    // Allocate memory for matrices
    int **a = (int**)malloc(m * sizeof(int*));
    int **b = (int**)malloc(n * sizeof(int*));
    int **result = (int**)malloc(m * sizeof(int*));

    for (int i = 0; i < m; i++) {
        a[i] = (int*)malloc(n * sizeof(int));
        result[i] = (int*)malloc(p * sizeof(int));
    }
    for (int i = 0; i < n; i++) {
        b[i] = (int*)malloc(p * sizeof(int));
    }

    // Initialize matrices
    int a_values[2][3] = {{1, 2, 3}, {4, 5, 6}};
    int b_values[3][2] = {{7, 8}, {9, 10}, {11, 12}};

    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            a[i][j] = a_values[i][j];
        }
    }

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < p; j++) {
            b[i][j] = b_values[i][j];
        }
    }

    // Multiply matrices
    multiply_matrices(a, b, result, m, n, p);

    // Print results
    printf("Matrix A:\n");
    print_matrix(a, m, n);
    printf("\nMatrix B:\n");
    print_matrix(b, n, p);
    printf("\nResult Matrix:\n");
    print_matrix(result, m, p);

    // Free allocated memory
    for (int i = 0; i < m; i++) {
        free(a[i]);
        free(result[i]);
    }
    for (int i = 0; i < n; i++) {
        free(b[i]);
    }
    free(a);
    free(b);
    free(result);

    return 0;
}

This program demonstrates matrix multiplication using dynamically allocated 2D arrays. It includes functions for matrix multiplication and printing.

Output:

Matrix A:
1 2 3 
4 5 6 

Matrix B:
7 8 
9 10 
11 12 

Result Matrix:
58 64 
139 154

🧠 Challenge: Can you modify this program to handle matrix addition and subtraction as well? Implement a menu system that allows the user to choose which operation to perform.

5. Linked List Operations

Linked lists are fundamental data structures in C. Let's implement some basic operations on a singly linked list.

📝 Exercise: Implement a singly linked list with functions to insert a node at the beginning, end, and a specific position, delete a node, and display the list.

Here's a comprehensive implementation:

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

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

struct Node* head = NULL;

void insert_at_beginning(int value) {
    struct Node* new_node = (struct Node*)malloc(sizeof(struct Node));
    new_node->data = value;
    new_node->next = head;
    head = new_node;
}

void insert_at_end(int value) {
    struct Node* new_node = (struct Node*)malloc(sizeof(struct Node));
    new_node->data = value;
    new_node->next = NULL;

    if (head == NULL) {
        head = new_node;
        return;
    }

    struct Node* temp = head;
    while (temp->next != NULL) {
        temp = temp->next;
    }
    temp->next = new_node;
}

void insert_at_position(int value, int position) {
    if (position < 1) {
        printf("Invalid position\n");
        return;
    }

    if (position == 1) {
        insert_at_beginning(value);
        return;
    }

    struct Node* new_node = (struct Node*)malloc(sizeof(struct Node));
    new_node->data = value;

    struct Node* temp = head;
    for (int i = 1; i < position - 1 && temp != NULL; i++) {
        temp = temp->next;
    }

    if (temp == NULL) {
        printf("Position out of range\n");
        free(new_node);
        return;
    }

    new_node->next = temp->next;
    temp->next = new_node;
}

void delete_node(int value) {
    struct Node* temp = head;
    struct Node* prev = NULL;

    if (temp != NULL && temp->data == value) {
        head = temp->next;
        free(temp);
        return;
    }

    while (temp != NULL && temp->data != value) {
        prev = temp;
        temp = temp->next;
    }

    if (temp == NULL) {
        printf("Value not found in the list\n");
        return;
    }

    prev->next = temp->next;
    free(temp);
}

void display_list() {
    struct Node* temp = head;
    while (temp != NULL) {
        printf("%d -> ", temp->data);
        temp = temp->next;
    }
    printf("NULL\n");
}

int main() {
    insert_at_end(10);
    insert_at_end(20);
    insert_at_beginning(5);
    insert_at_position(15, 3);

    printf("Initial list: ");
    display_list();

    delete_node(20);

    printf("List after deleting 20: ");
    display_list();

    return 0;
}

This program implements a singly linked list with the following operations:

  • Insert at the beginning
  • Insert at the end
  • Insert at a specific position
  • Delete a node with a given value
  • Display the list

Output:

Initial list: 5 -> 10 -> 15 -> 20 -> NULL
List after deleting 20: 5 -> 10 -> 15 -> NULL

🧠 Challenge: Can you extend this implementation to include a function that reverses the linked list in-place?

6. Binary Search Tree

Binary Search Trees (BST) are efficient for searching and organizing data. Let's implement a basic BST.

📝 Exercise: Implement a Binary Search Tree with functions to insert a node, search for a value, and perform in-order traversal.

Here's an implementation:

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

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

struct Node* create_node(int value) {
    struct Node* new_node = (struct Node*)malloc(sizeof(struct Node));
    new_node->data = value;
    new_node->left = NULL;
    new_node->right = NULL;
    return new_node;
}

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

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

    return root;
}

struct Node* search(struct Node* root, int value) {
    if (root == NULL || root->data == value) {
        return root;
    }

    if (value < root->data) {
        return search(root->left, value);
    }

    return search(root->right, value);
}

void inorder_traversal(struct Node* root) {
    if (root != NULL) {
        inorder_traversal(root->left);
        printf("%d ", root->data);
        inorder_traversal(root->right);
    }
}

int main() {
    struct Node* root = NULL;
    root = insert(root, 50);
    insert(root, 30);
    insert(root, 20);
    insert(root, 40);
    insert(root, 70);
    insert(root, 60);
    insert(root, 80);

    printf("In-order traversal: ");
    inorder_traversal(root);
    printf("\n");

    int search_value = 40;
    struct Node* result = search(root, search_value);
    if (result != NULL) {
        printf("%d found in the BST\n", search_value);
    } else {
        printf("%d not found in the BST\n", search_value);
    }

    return 0;
}

This program implements a Binary Search Tree with the following operations:

  • Insert a node
  • Search for a value
  • Perform in-order traversal

The BST property ensures that for each node, all elements in the left subtree are smaller, and all elements in the right subtree are larger.

Output:

In-order traversal: 20 30 40 50 60 70 80 
40 found in the BST

🧠 Challenge: Can you add functions to find the minimum and maximum values in the BST? Also, try implementing a function to delete a node from the BST.

7. File Handling: Word Counter

File handling is an essential skill in C programming. Let's create a program that counts words in a text file.

📝 Exercise: Write a program that reads a text file and counts the number of words in it. Assume words are separated by spaces, tabs, or newlines.

Here's an implementation:

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

#define MAX_FILENAME 100

int count_words(FILE* file) {
    int count = 0;
    int in_word = 0;
    int ch;

    while ((ch = fgetc(file)) != EOF) {
        if (isspace(ch)) {
            in_word = 0;
        } else if (!in_word) {
            in_word = 1;
            count++;
        }
    }

    return count;
}

int main() {
    char filename[MAX_FILENAME];
    FILE* file;

    printf("Enter the filename: ");
    scanf("%99s", filename);

    file = fopen(filename, "r");
    if (file == NULL) {
        printf("Error opening file.\n");
        return 1;
    }

    int word_count = count_words(file);
    printf("The file contains %d words.\n", word_count);

    fclose(file);
    return 0;
}

This program does the following:

  1. Prompts the user for a filename.
  2. Opens the file in read mode.
  3. Counts the words in the file using the count_words function.
  4. Prints the total word count.

The count_words function works by tracking whether we're currently in a word or not. It increments the count when it encounters the start of a new word.

To test this program, create a text file (e.g., "sample.txt") with some content:

This is a sample text file.
It contains multiple lines and words.
Let's count them all!

Then run the program:

Enter the filename: sample.txt
The file contains 15 words.

🧠 Challenge: Can you extend this program to also count the number of characters, lines, and paragraphs in the file? A paragraph can be defined as text separated by one or more blank lines.

8. Memory Management: Custom Memory Allocator

Understanding memory management is crucial for C programmers. Let's implement a simple memory allocator to deepen our understanding.

📝 Exercise: Implement a basic memory allocator that manages a fixed-size memory pool. Include functions for allocation and deallocation.

Here's a simple implementation:

#include <stdio.h>
#include <string.h>

#define MEMORY_SIZE 1024
#define BLOCK_SIZE 16

char memory[MEMORY_SIZE];
char allocation_map[MEMORY_SIZE / BLOCK_SIZE];

void init_allocator() {
    memset(allocation_map, 0, sizeof(allocation_map));
}

void* my_malloc(size_t size) {
    int blocks_needed = (size + BLOCK_SIZE - 1) / BLOCK_SIZE;
    int start_block = -1;
    int consecutive_blocks = 0;

    for (int i = 0; i < sizeof(allocation_map); i++) {
        if (allocation_map[i] == 0) {
            if (start_block == -1) {
                start_block = i;
            }
            consecutive_blocks++;
            if (consecutive_blocks == blocks_needed) {
                for (int j = start_block; j < start_block + blocks_needed; j++) {
                    allocation_map[j] = 1;
                }
                return &memory[start_block * BLOCK_SIZE];
            }
        } else {
            start_block = -1;
            consecutive_blocks = 0;
        }
    }

    return NULL;  // Out of memory
}

void my_free(void* ptr) {
    if (ptr == NULL) return;

    int start_block = ((char*)ptr - memory) / BLOCK_SIZE;
    for (int i = start_block; i < sizeof(allocation_map) && allocation_map[i] == 1; i++) {
        allocation_map[i] = 0;
    }
}

void print_memory_status() {
    printf("Memory status: ");
    for (int i = 0; i < sizeof(allocation_map); i++) {
        printf("%d", allocation_map[i]);
    }
    printf("\n");
}

int main() {
    init_allocator();

    printf("Initial memory status:\n");
    print_memory_status();

    int* p1 = (int*)my_malloc(sizeof(int) * 10);
    char* p2 = (char*)my_malloc(sizeof(char) * 20);
    double* p3 = (double*)my_malloc(sizeof(double) * 5);

    printf("After allocations:\n");
    print_memory_status();

    my_free(p2);

    printf("After freeing p2:\n");
    print_memory_status();

    my_free(p1);
    my_free(p3);

    printf("After freeing all:\n");
    print_memory_status();

    return 0;
}

This program implements a simple memory allocator with the following features:

  • A fixed-size memory pool of 1024 bytes
  • Memory blocks of 16 bytes each
  • An allocation map to track which blocks are in use
  • Functions for allocation (my_malloc) and deallocation (my_free)
  • A function to visualize the memory status

Output:

Initial memory status:
Memory status: 0000000000000000000000000000000000000000000000000000000000000000

After allocations:
Memory status: 1111111111111111111111111111000000000000000000000000000000000000

After freeing p2:
Memory status: 1111111100000000111111111111000000000000000000000000000000000000

After freeing all:
Memory status: 0000000000000000000000000000000000000000000000000000000000000000

🧠 Challenge: Can you extend this allocator to handle different block sizes more efficiently? Consider implementing a best-fit or first-fit allocation strategy.

9. Bit Manipulation: Bit Counter

Bit manipulation is a powerful technique in C programming, often used for optimization and low-level operations.

📝 Exercise: Write a function that counts the number of set bits (1s) in an integer.

Here's an implementation using bit manipulation:

#include <stdio.h>

int count_set_bits(unsigned int n) {
    int count = 0;
    while (n) {
        count += n & 1;
        n >>= 1;
    }
    return count;
}

int count_set_bits_optimized(unsigned int n) {
    int count = 0;
    while (n) {
        n &= (n - 1);
        count++;
    }
    return count;
}

void print_binary(unsigned int n) {
    if (n > 1) {
        print_binary(n >> 1);
    }
    printf("%d", n & 1);
}

int main() {
    unsigned int num = 0b10101010;  // Binary representation

    printf("Number: ");
    print_binary(num);
    printf("\n");

    printf("Number of set bits (method 1): %d\n", count_set_bits(num));
    printf("Number of set bits (method 2): %d\n", count_set_bits_optimized(num));

    return 0;
}

This program implements two methods to count set bits:

  1. count_set_bits: Checks each bit by ANDing with 1 and right-shifting.
  2. count_set_bits_optimized: Uses the trick that n & (n-1) always flips the rightmost set bit to 0.

The print_binary function is included to visualize the binary representation of the number.

Output:

Number: 10101010
Number of set bits (method 1): 4
Number of set bits (method 2): 4

🧠 Challenge: Can you implement a function that reverses the bits of an unsigned integer? For example, 0b10101010 should become 0b01010101.

10. Advanced Data Structures: Trie Implementation

A Trie (prefix tree) is an efficient data structure for storing and searching strings. Let's implement a basic Trie for storing and searching words.

📝 Exercise: Implement a Trie data structure with functions to insert words and search for words.

Here's an implementation:

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

#define ALPHABET_SIZE 26

struct TrieNode {
    struct TrieNode* children[ALPHABET_SIZE];
    bool is_end_of_word;
};

struct TrieNode* create_node() {
    struct TrieNode* node = (struct TrieNode*)malloc(sizeof(struct TrieNode));
    node->is_end_of_word = false;
    for (int i = 0; i < ALPHABET_SIZE; i++) {
        node->children[i] = NULL;
    }
    return node;
}

void insert(struct TrieNode* root, const char* word) {
    struct TrieNode* current = root;
    while (*word) {
        int index = *word - 'a';
        if (current->children[index] == NULL) {
            current->children[index] = create_node();
        }
        current = current->children[index];
        word++;
    }
    current->is_end_of_word = true;
}

bool search(struct TrieNode* root, const char* word) {
    struct TrieNode* current = root;
    while (*word) {
        int index = *word - 'a';
        if (current->children[index] == NULL) {
            return false;
        }
        current = current->children[index];
        word++;
    }
    return current->is_end_of_word;
}

void free_trie(struct TrieNode* node) {
    if (node == NULL) return;

    for (int i = 0; i < ALPHABET_SIZE; i++) {
        free_trie(node->children[i]);
    }
    free(node);
}

int main() {
    struct TrieNode* root = create_node();

    // Insert words
    insert(root, "hello");
    insert(root, "world");
    insert(root, "hi");
    insert(root, "hey");

    // Search words
    printf("Search results:\n");
    printf("hello: %s\n", search(root, "hello") ? "Found" : "Not found");
    printf("world: %s\n", search(root, "world") ? "Found" : "Not found");
    printf("hi: %s\n", search(root, "hi") ? "Found" : "Not found");
    printf("hey: %s\n", search(root, "hey") ? "Found" : "Not found");
    printf("hell: %s\n", search(root, "hell") ? "Found" : "Not found");
    printf("helloworld: %s\n", search(root, "helloworld") ? "Found" : "Not found");

    // Free memory
    free_trie(root);

    return 0;
}

This program implements a Trie with the following features:

  • A TrieNode structure representing each node in the Trie
  • Functions to create a new node, insert a word, and search for a word
  • A function to free the memory used by the Trie

The Trie efficiently stores and searches for words by sharing common prefixes.

Output:

Search results:
hello: Found
world: Found
hi: Found
hey: Found
hell: Not found
helloworld: Not found

🧠 Challenge: Can you extend this Trie implementation to include a function that finds all words with a given prefix? For example, given the prefix "he", it should return "hello" and "hey".

Conclusion

Congratulations on completing these challenging C programming exercises! 🎉 You've covered a wide range of topics, from basic algorithms to advanced data structures. Remember, the key to mastering C programming is consistent practice and application of these concepts in real-world scenarios.

Here are some key takeaways:

  1. Problem-solving skills: Each exercise required you to break down complex problems into manageable steps.
  2. Memory management: You've practiced dynamic memory allocation and deallocation, crucial for efficient C programs.
  3. Data structures: From linked lists to trees and tries, you've implemented various data structures essential for organizing and manipulating data.
  4. Algorithm efficiency: Many exercises encouraged you to think about optimizing your solutions for better performance.
  5. Bit manipulation: You've explored low-level operations that are often used for optimization in C.

Keep challenging yourself with more complex problems and projects. The world of C programming is vast, and there's always more to learn and explore. Happy coding! 💻🚀