DEV Community

张宇
张宇

Posted on

The Complete Guide to Data Structures in C: From Arrays to Hash Tables

The Complete Guide to Data Structures in C: From Arrays to Hash Tables

English Version | 中文版

Introduction

Data structures form the foundation of computer science, and understanding their implementation in C is crucial for system programming. As Niklaus Wirth stated in his book "Algorithms + Data Structures = Programs": "A program is a combination of algorithms and data structures." In C, this combination is even more intimate—you need to manage memory manually, understand data layout in memory, and make performance optimization decisions.

This comprehensive guide will take you through the core data structures in C, from basic arrays to complex hash tables. Each section includes complete code implementations, performance analysis, and real-world use cases. Whether you're a beginner or an experienced developer, you'll gain practical knowledge and techniques.

This article covers:

  • Fundamental data structures: arrays, strings, linked lists
  • Linear structures: stacks, queues, deques
  • Tree structures: binary trees, binary search trees, heaps
  • Graph fundamentals: representation and traversal algorithms
  • Efficient lookup: hash table design and implementation
  • Advanced topics: memory management, cache optimization, practical applications

1. Fundamental Data Structures: Arrays and Strings

1.1 Static Arrays

Arrays are the most fundamental data structure in C, allowing you to store elements of the same type in contiguous memory space. Static arrays have their size determined at compile time, making them highly efficient in terms of performance and memory usage.

#include <stdio.h>

// Static array example
void static_array_example() {
    int numbers[5] = {1, 2, 3, 4, 5};

    // Traverse array
    printf("Static array elements: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");

    // Calculate array size
    size_t size = sizeof(numbers) / sizeof(numbers[0]);
    printf("Array size: %zu\n", size);
}

int main() {
    static_array_example();
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Key characteristics:

  • Contiguous memory, fast access (O(1) time complexity)
  • Fixed size, cannot be dynamically expanded
  • Decay to pointers when passed as function arguments

1.2 Dynamic Array Implementation

Dynamic arrays (also known as resizable arrays) allow changing their size at runtime. This is achieved using malloc, realloc, and free functions.

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

// Dynamic array structure
typedef struct {
    int *data;      // Data pointer
    size_t size;    // Current number of elements
    size_t capacity; // Capacity
} DynamicArray;

// Create dynamic array
DynamicArray* create_dynamic_array(size_t initial_capacity) {
    DynamicArray *arr = (DynamicArray*)malloc(sizeof(DynamicArray));
    if (!arr) return NULL;

    arr->data = (int*)malloc(initial_capacity * sizeof(int));
    if (!arr->data) {
        free(arr);
        return NULL;
    }

    arr->size = 0;
    arr->capacity = initial_capacity;
    return arr;
}

// Append element to dynamic array
int dynamic_array_append(DynamicArray *arr, int value) {
    if (arr->size >= arr->capacity) {
        // Capacity expansion: typically expand to 1.5-2 times current capacity
        size_t new_capacity = arr->capacity * 2;
        int *new_data = (int*)realloc(arr->data, new_capacity * sizeof(int));
        if (!new_data) return 0; // Failure

        arr->data = new_data;
        arr->capacity = new_capacity;
    }

    arr->data[arr->size++] = value;
    return 1;
}

// Free dynamic array
void free_dynamic_array(DynamicArray *arr) {
    if (arr) {
        free(arr->data);
        free(arr);
    }
}

int main() {
    DynamicArray *arr = create_dynamic_array(5);
    if (!arr) {
        printf("Failed to create dynamic array\n");
        return 1;
    }

    // Add elements
    for (int i = 0; i < 10; i++) {
        dynamic_array_append(arr, i * 10);
    }

    printf("Dynamic array elements: ");
    for (size_t i = 0; i < arr->size; i++) {
        printf("%d ", arr->data[i]);
    }
    printf("\nSize: %zu, Capacity: %zu\n", arr->size, arr->capacity);

    free_dynamic_array(arr);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

1.3 String Manipulation and Common Algorithms

Strings in C are character arrays terminated with a null character ('\0'). Understanding string operations is essential for data processing.

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

// Custom string copy function
char* my_strcpy(char *dest, const char *src) {
    char *start = dest;
    while ((*dest++ = *src++) != '\0');
    return start;
}

// String reversal algorithm
void reverse_string(char *str) {
    if (!str) return;

    char *end = str;
    char tmp;

    // Find string end
    while (*end) end++;
    end--; // Point to last character (skip '\0')

    // Two-pointer reversal
    while (str < end) {
        tmp = *str;
        *str = *end;
        *end = tmp;
        str++;
        end--;
    }
}

// Check if string is palindrome
int is_palindrome(const char *str) {
    if (!str) return 0;

    const char *start = str;
    const char *end = str;

    while (*end) end++;
    end--;

    while (start < end) {
        if (tolower(*start) != tolower(*end)) {
            return 0;
        }
        start++;
        end--;
    }
    return 1;
}

int main() {
    char text[] = "Hello, World!";
    char buffer[50];

    my_strcpy(buffer, text);
    printf("Original string: %s\n", text);
    printf("Copied string: %s\n", buffer);

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

    char palindrome[] = "A man a plan a canal Panama";
    printf("Is '%s' a palindrome? %s\n", 
           palindrome, 
           is_palindrome(palindrome) ? "Yes" : "No");

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

2. Linked Lists: Flexible Data Organization

2.1 Singly Linked List Implementation

Linked lists organize data through links between nodes, where each node contains data and a pointer to the next node. Linked lists provide dynamic sizing and efficient insertion/deletion operations.

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

// Linked list node structure
typedef struct Node {
    int data;
    struct Node *next;
} Node;

// Create new node
Node* create_node(int data) {
    Node *new_node = (Node*)malloc(sizeof(Node));
    if (!new_node) return NULL;

    new_node->data = data;
    new_node->next = NULL;
    return new_node;
}

// Insert node at head of list
void insert_at_head(Node **head, int data) {
    Node *new_node = create_node(data);
    if (!new_node) return;

    new_node->next = *head;
    *head = new_node;
}

// Insert node at tail of list
void insert_at_tail(Node **head, int data) {
    Node *new_node = create_node(data);
    if (!new_node) return;

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

    Node *current = *head;
    while (current->next) {
        current = current->next;
    }
    current->next = new_node;
}

// Traverse linked list
void print_list(Node *head) {
    Node *current = head;
    printf("Linked list: ");
    while (current) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

// Reverse linked list
Node* reverse_list(Node *head) {
    Node *prev = NULL;
    Node *current = head;
    Node *next = NULL;

    while (current) {
        next = current->next;
        current->next = prev;
        prev = current;
        current = next;
    }

    return prev;
}

// Free linked list memory
void free_list(Node *head) {
    Node *current = head;
    Node *next;

    while (current) {
        next = current->next;
        free(current);
        current = next;
    }
}

int main() {
    Node *list = NULL;

    // Insert elements
    insert_at_tail(&list, 10);
    insert_at_tail(&list, 20);
    insert_at_head(&list, 5);
    insert_at_tail(&list, 30);

    print_list(list);

    // Reverse list
    list = reverse_list(list);
    printf("After reversal: ");
    print_list(list);

    free_list(list);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

2.2 Linked List Operation Algorithms

Linked lists support various efficient algorithms. Here are a few classic examples:

// Detect cycle in linked list (Floyd's Cycle Detection Algorithm)
int has_cycle(Node *head) {
    if (!head || !head->next) return 0;

    Node *slow = head;
    Node *fast = head->next;

    while (fast && fast->next) {
        if (slow == fast) return 1; // Cycle detected
        slow = slow->next;
        fast = fast->next->next;
    }

    return 0; // No cycle
}

// Find middle node of linked list
Node* find_middle(Node *head) {
    if (!head) return NULL;

    Node *slow = head;
    Node *fast = head;

    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
    }

    return slow;
}

// Merge two sorted linked lists
Node* merge_sorted_lists(Node *list1, Node *list2) {
    // Create a dummy node to simplify operations
    Node dummy;
    Node *tail = &dummy;
    dummy.next = NULL;

    while (list1 && list2) {
        if (list1->data <= list2->data) {
            tail->next = list1;
            list1 = list1->next;
        } else {
            tail->next = list2;
            list2 = list2->next;
        }
        tail = tail->next;
    }

    // Attach remaining portion
    tail->next = list1 ? list1 : list2;

    return dummy.next;
}
Enter fullscreen mode Exit fullscreen mode

(Due to length constraints, the following sections will be outlined in detail, with complete code provided in subsequent versions)


3. Stacks and Queues: LIFO and FIFO

3.1 Stack Implementation and Applications

A stack is a Last-In-First-Out (LIFO) data structure. The last element pushed is the first one popped—like a stack of plates.

Array Implementation

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

#define MAX_SIZE 100

typedef struct {
    int data[MAX_SIZE];
    int top;
} ArrayStack;

void stack_init(ArrayStack *stack) { stack->top = -1; }
bool stack_is_empty(ArrayStack *stack) { return stack->top == -1; }
bool stack_is_full(ArrayStack *stack) { return stack->top == MAX_SIZE - 1; }

bool stack_push(ArrayStack *stack, int value) {
    if (stack_is_full(stack)) return false;
    stack->data[++stack->top] = value;
    return true;
}

bool stack_pop(ArrayStack *stack, int *value) {
    if (stack_is_empty(stack)) return false;
    *value = stack->data[stack->top--];
    return true;
}

bool stack_peek(ArrayStack *stack, int *value) {
    if (stack_is_empty(stack)) return false;
    *value = stack->data[stack->top];
    return true;
}

int stack_size(ArrayStack *stack) { return stack->top + 1; }
Enter fullscreen mode Exit fullscreen mode

Linked List Implementation

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

typedef struct StackNode {
    int data;
    struct StackNode *next;
} StackNode;

typedef struct {
    StackNode *top;
    int size;
} LinkedStack;

void linked_stack_init(LinkedStack *stack) {
    stack->top = NULL;
    stack->size = 0;
}

bool linked_stack_push(LinkedStack *stack, int value) {
    StackNode *new_node = (StackNode*)malloc(sizeof(StackNode));
    if (!new_node) return false;

    new_node->data = value;
    new_node->next = stack->top;
    stack->top = new_node;
    stack->size++;
    return true;
}

bool linked_stack_pop(LinkedStack *stack, int *value) {
    if (stack->top == NULL) return false;

    StackNode *temp = stack->top;
    *value = temp->data;
    stack->top = temp->next;
    stack->size--;
    free(temp);
    return true;
}
Enter fullscreen mode Exit fullscreen mode

Application: Parentheses Matching

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

#define MAX_SIZE 1000

typedef struct {
    char data[MAX_SIZE];
    int top;
} CharStack;

void char_stack_init(CharStack *s) { s->top = -1; }
bool char_stack_is_empty(CharStack *s) { return s->top == -1; }
void char_stack_push(CharStack *s, char c) { s->data[++s->top] = c; }
char char_stack_pop(CharStack *s) { return s->data[s->top--]; }
char char_stack_peek(CharStack *s) { return char_stack_is_empty(s) ? '\0' : s->data[s->top]; }

bool is_valid_parentheses(const char *str) {
    CharStack stack;
    char_stack_init(&stack);

    for (int i = 0; str[i] != '\0'; i++) {
        char c = str[i];

        if (c == '(' || c == '[' || c == '{') {
            char_stack_push(&stack, c);
        }
        else if (c == ')' || c == ']' || c == '}') {
            if (char_stack_is_empty(&stack)) return false;

            char top = char_stack_pop(&stack);

            if ((c == ')' && top != '(') ||
                (c == ']' && top != '[') ||
                (c == '}' && top != '{')) {
                return false;
            }
        }
    }

    return char_stack_is_empty(&stack);
}

int main() {
    const char *tests[] = {"()", "([])", "{[()]}", "([)]", "(((", "())"};

    for (int i = 0; i < 6; i++) {
        printf("'%s' -> %s\n", tests[i], is_valid_parentheses(tests[i]) ? "Valid" : "Invalid");
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Application: Expression Evaluation (Infix to Postfix)

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

#define MAX_SIZE 100

typedef struct {
    char data[MAX_SIZE];
    int top;
} CharStack;

void init_stack(CharStack *s) { s->top = -1; }
bool is_empty(CharStack *s) { return s->top == -1; }
void push(CharStack *s, char c) { s->data[++s->top] = c; }
char pop(CharStack *s) { return s->data[s->top--]; }
char peek(CharStack *s) { return is_empty(s) ? '\0' : s->data[s->top]; }

int precedence(char op) {
    switch (op) {
        case '+': case '-': return 1;
        case '*': case '/': return 2;
        default: return 0;
    }
}

int is_operator(char c) {
    return c == '+' || c == '-' || c == '*' || c == '/';
}

void infix_to_postfix(const char *infix, char *postfix) {
    CharStack stack;
    init_stack(&stack);
    int j = 0;

    for (int i = 0; infix[i] != '\0'; i++) {
        char c = infix[i];

        if (isspace(c)) continue;

        if (isdigit(c)) {
            postfix[j++] = c;
            postfix[j++] = ' ';
        }
        else if (c == '(') {
            push(&stack, c);
        }
        else if (c == ')') {
            while (!is_empty(&stack) && peek(&stack) != '(') {
                postfix[j++] = pop(&stack);
                postfix[j++] = ' ';
            }
            pop(&stack);
        }
        else if (is_operator(c)) {
            while (!is_empty(&stack) && precedence(peek(&stack)) >= precedence(c)) {
                postfix[j++] = pop(&stack);
                postfix[j++] = ' ';
            }
            push(&stack, c);
        }
    }

    while (!is_empty(&stack)) {
        postfix[j++] = pop(&stack);
        postfix[j++] = ' ';
    }
    postfix[j] = '\0';
}

int main() {
    const char *infix = "3 + 4 * 2 - 1";
    char postfix[100];

    infix_to_postfix(infix, postfix);
    printf("Infix: %s\n", infix);
    printf("Postfix: %s\n", postfix);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

3.2 Queue Implementation and Variants

A queue is a First-In-First-Out (FIFO) data structure. Like a line at a store—first come, first served.

Circular Queue Implementation

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

#define MAX_SIZE 100

typedef struct {
    int data[MAX_SIZE];
    int front;
    int rear;
    int size;
} CircularQueue;

void queue_init(CircularQueue *q) {
    q->front = 0;
    q->rear = 0;
    q->size = 0;
}

bool queue_is_empty(CircularQueue *q) { return q->size == 0; }
bool queue_is_full(CircularQueue *q) { return q->size == MAX_SIZE; }

bool queue_enqueue(CircularQueue *q, int value) {
    if (queue_is_full(q)) return false;

    q->data[q->rear] = value;
    q->rear = (q->rear + 1) % MAX_SIZE;
    q->size++;
    return true;
}

bool queue_dequeue(CircularQueue *q, int *value) {
    if (queue_is_empty(q)) return false;

    *value = q->data[q->front];
    q->front = (q->front + 1) % MAX_SIZE;
    q->size--;
    return true;
}

bool queue_peek(CircularQueue *q, int *value) {
    if (queue_is_empty(q)) return false;
    *value = q->data[q->front];
    return true;
}
Enter fullscreen mode Exit fullscreen mode

Linked Queue Implementation

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

typedef struct QueueNode {
    int data;
    struct QueueNode *next;
} QueueNode;

typedef struct {
    QueueNode *front;
    QueueNode *rear;
    int size;
} LinkedQueue;

void linked_queue_init(LinkedQueue *q) {
    q->front = q->rear = NULL;
    q->size = 0;
}

bool linked_queue_enqueue(LinkedQueue *q, int value) {
    QueueNode *new_node = (QueueNode*)malloc(sizeof(QueueNode));
    if (!new_node) return false;

    new_node->data = value;
    new_node->next = NULL;

    if (q->rear == NULL) {
        q->front = q->rear = new_node;
    } else {
        q->rear->next = new_node;
        q->rear = new_node;
    }
    q->size++;
    return true;
}

bool linked_queue_dequeue(LinkedQueue *q, int *value) {
    if (q->front == NULL) return false;

    QueueNode *temp = q->front;
    *value = temp->data;
    q->front = temp->next;

    if (q->front == NULL) {
        q->rear = NULL;
    }

    q->size--;
    free(temp);
    return true;
}
Enter fullscreen mode Exit fullscreen mode

Deque (Double-Ended Queue)

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

typedef struct DequeNode {
    int data;
    struct DequeNode *prev;
    struct DequeNode *next;
} DequeNode;

typedef struct {
    DequeNode *front;
    DequeNode *rear;
    int size;
} Deque;

void deque_init(Deque *d) {
    d->front = d->rear = NULL;
    d->size = 0;
}

bool deque_push_front(Deque *d, int value) {
    DequeNode *new_node = (DequeNode*)malloc(sizeof(DequeNode));
    if (!new_node) return false;

    new_node->data = value;
    new_node->prev = NULL;
    new_node->next = d->front;

    if (d->front == NULL) {
        d->rear = new_node;
    } else {
        d->front->prev = new_node;
    }
    d->front = new_node;
    d->size++;
    return true;
}

bool deque_push_back(Deque *d, int value) {
    DequeNode *new_node = (DequeNode*)malloc(sizeof(DequeNode));
    if (!new_node) return false;

    new_node->data = value;
    new_node->next = NULL;
    new_node->prev = d->rear;

    if (d->rear == NULL) {
        d->front = new_node;
    } else {
        d->rear->next = new_node;
    }
    d->rear = new_node;
    d->size++;
    return true;
}

bool deque_pop_front(Deque *d, int *value) {
    if (d->front == NULL) return false;

    DequeNode *temp = d->front;
    *value = temp->data;
    d->front = temp->next;

    if (d->front == NULL) {
        d->rear = NULL;
    } else {
        d->front->prev = NULL;
    }

    d->size--;
    free(temp);
    return true;
}

bool deque_pop_back(Deque *d, int *value) {
    if (d->rear == NULL) return false;

    DequeNode *temp = d->rear;
    *value = temp->data;
    d->rear = temp->prev;

    if (d->rear == NULL) {
        d->front = NULL;
    } else {
        d->rear->next = NULL;
    }

    d->size--;
    free(temp);
    return true;
}
Enter fullscreen mode Exit fullscreen mode

3.3 Real-World Use Cases

Browser Forward/Back Navigation

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

#define MAX_URL 256
#define MAX_HISTORY 50

typedef struct { char url[MAX_URL]; } Page;
typedef struct { Page pages[MAX_HISTORY]; int top; } PageStack;

void page_stack_init(PageStack *s) { s->top = -1; }
void page_stack_push(PageStack *s, const char *url) {
    if (s->top < MAX_HISTORY - 1) strncpy(s->pages[++s->top].url, url, MAX_URL - 1);
}
int page_stack_pop(PageStack *s, char *url) {
    if (s->top < 0) return 0;
    strncpy(url, s->pages[s->top--].url, MAX_URL - 1);
    return 1;
}

typedef struct {
    PageStack back_stack;
    PageStack forward_stack;
    char current_url[MAX_URL];
} BrowserHistory;

void browser_init(BrowserHistory *bh, const char *homepage) {
    page_stack_init(&bh->back_stack);
    page_stack_init(&bh->forward_stack);
    strncpy(bh->current_url, homepage, MAX_URL - 1);
}

void browser_visit(BrowserHistory *bh, const char *url) {
    page_stack_push(&bh->back_stack, bh->current_url);
    page_stack_init(&bh->forward_stack);
    strncpy(bh->current_url, url, MAX_URL - 1);
    printf("Visit: %s\n", url);
}

void browser_back(BrowserHistory *bh) {
    char prev_url[MAX_URL];
    if (page_stack_pop(&bh->back_stack, prev_url)) {
        page_stack_push(&bh->forward_stack, bh->current_url);
        strncpy(bh->current_url, prev_url, MAX_URL - 1);
        printf("Back to: %s\n", bh->current_url);
    } else {
        printf("Cannot go back\n");
    }
}

void browser_forward(BrowserHistory *bh) {
    char next_url[MAX_URL];
    if (page_stack_pop(&bh->forward_stack, next_url)) {
        page_stack_push(&bh->back_stack, bh->current_url);
        strncpy(bh->current_url, next_url, MAX_URL - 1);
        printf("Forward to: %s\n", bh->current_url);
    } else {
        printf("Cannot go forward\n");
    }
}
Enter fullscreen mode Exit fullscreen mode

Stack vs Queue Comparison

Feature Stack Queue
Access Principle LIFO (Last-In-First-Out) FIFO (First-In-First-Out)
Insert Position Top Rear
Delete Position Top Front
Main Operations push, pop, peek enqueue, dequeue, peek
Typical Uses Function calls, undo, parentheses Task scheduling, BFS, buffers
Array Implementation Simple, top pointer Circular array, front/rear
Linked Implementation Head insert/delete Tail insert, head delete

4. Tree Structures: Hierarchical Data Organization

4.1 Binary Trees and Binary Search Trees

A binary tree has at most two children per node. A Binary Search Tree (BST) is a special binary tree where: left subtree values < root value < right subtree values.

Binary Tree Implementation

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

typedef struct TreeNode {
    int data;
    struct TreeNode *left;
    struct TreeNode *right;
} TreeNode;

TreeNode* create_tree_node(int data) {
    TreeNode *node = (TreeNode*)malloc(sizeof(TreeNode));
    node->data = data;
    node->left = node->right = NULL;
    return node;
}

// Preorder: Root -> Left -> Right
void preorder_traversal(TreeNode *root) {
    if (root == NULL) return;
    printf("%d ", root->data);
    preorder_traversal(root->left);
    preorder_traversal(root->right);
}

// Inorder: Left -> Root -> Right
void inorder_traversal(TreeNode *root) {
    if (root == NULL) return;
    inorder_traversal(root->left);
    printf("%d ", root->data);
    inorder_traversal(root->right);
}

// Postorder: Left -> Right -> Root
void postorder_traversal(TreeNode *root) {
    if (root == NULL) return;
    postorder_traversal(root->left);
    postorder_traversal(root->right);
    printf("%d ", root->data);
}

// Level Order (BFS)
#define MAX_QUEUE 100

typedef struct {
    TreeNode* data[MAX_QUEUE];
    int front, rear;
} TreeQueue;

void tree_queue_init(TreeQueue *q) { q->front = q->rear = 0; }
bool tree_queue_empty(TreeQueue *q) { return q->front == q->rear; }
void tree_queue_enqueue(TreeQueue *q, TreeNode *node) { q->data[q->rear++] = node; }
TreeNode* tree_queue_dequeue(TreeQueue *q) { return q->data[q->front++]; }

void level_order_traversal(TreeNode *root) {
    if (root == NULL) return;

    TreeQueue q;
    tree_queue_init(&q);
    tree_queue_enqueue(&q, root);

    while (!tree_queue_empty(&q)) {
        TreeNode *node = tree_queue_dequeue(&q);
        printf("%d ", node->data);

        if (node->left) tree_queue_enqueue(&q, node->left);
        if (node->right) tree_queue_enqueue(&q, node->right);
    }
}

int tree_height(TreeNode *root) {
    if (root == NULL) return 0;
    int left = tree_height(root->left);
    int right = tree_height(root->right);
    return (left > right ? left : right) + 1;
}

int tree_count_nodes(TreeNode *root) {
    if (root == NULL) return 0;
    return 1 + tree_count_nodes(root->left) + tree_count_nodes(root->right);
}

void free_tree(TreeNode *root) {
    if (root == NULL) return;
    free_tree(root->left);
    free_tree(root->right);
    free(root);
}
Enter fullscreen mode Exit fullscreen mode

Binary Search Tree (BST)

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

typedef struct BSTNode {
    int data;
    struct BSTNode *left;
    struct BSTNode *right;
} BSTNode;

BSTNode* bst_create_node(int data) {
    BSTNode *node = (BSTNode*)malloc(sizeof(BSTNode));
    node->data = data;
    node->left = node->right = NULL;
    return node;
}

BSTNode* bst_insert(BSTNode *root, int data) {
    if (root == NULL) return bst_create_node(data);

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

BSTNode* bst_search(BSTNode *root, int data) {
    if (root == NULL || root->data == data) return root;

    if (data < root->data) return bst_search(root->left, data);
    return bst_search(root->right, data);
}

BSTNode* bst_find_min(BSTNode *root) {
    if (root == NULL) return NULL;
    while (root->left) root = root->left;
    return root;
}

BSTNode* bst_find_max(BSTNode *root) {
    if (root == NULL) return NULL;
    while (root->right) root = root->right;
    return root;
}

BSTNode* bst_delete(BSTNode *root, int data) {
    if (root == NULL) return NULL;

    if (data < root->data) {
        root->left = bst_delete(root->left, data);
    } else if (data > root->data) {
        root->right = bst_delete(root->right, data);
    } else {
        // Found node to delete
        if (root->left == NULL) {
            BSTNode *temp = root->right;
            free(root);
            return temp;
        } else if (root->right == NULL) {
            BSTNode *temp = root->left;
            free(root);
            return temp;
        }

        // Two children: replace with inorder successor
        BSTNode *successor = bst_find_min(root->right);
        root->data = successor->data;
        root->right = bst_delete(root->right, successor->data);
    }
    return root;
}

void bst_inorder(BSTNode *root) {
    if (root == NULL) return;
    bst_inorder(root->left);
    printf("%d ", root->data);
    bst_inorder(root->right);
}
Enter fullscreen mode Exit fullscreen mode

4.2 Balanced Trees Introduction

Regular BSTs can degenerate into linked lists (O(n) worst case). Balanced trees maintain height through rotations.

AVL Tree Basics

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

typedef struct AVLNode {
    int data;
    struct AVLNode *left;
    struct AVLNode *right;
    int height;
} AVLNode;

int avl_height(AVLNode *node) { return node ? node->height : 0; }
int avl_balance_factor(AVLNode *node) {
    return node ? avl_height(node->left) - avl_height(node->right) : 0;
}

void avl_update_height(AVLNode *node) {
    int left = avl_height(node->left);
    int right = avl_height(node->right);
    node->height = (left > right ? left : right) + 1;
}

AVLNode* avl_create_node(int data) {
    AVLNode *node = (AVLNode*)malloc(sizeof(AVLNode));
    node->data = data;
    node->left = node->right = NULL;
    node->height = 1;
    return node;
}

// Right rotation (LL case)
AVLNode* avl_rotate_right(AVLNode *y) {
    AVLNode *x = y->left;
    AVLNode *T2 = x->right;

    x->right = y;
    y->left = T2;

    avl_update_height(y);
    avl_update_height(x);

    return x;
}

// Left rotation (RR case)
AVLNode* avl_rotate_left(AVLNode *x) {
    AVLNode *y = x->right;
    AVLNode *T2 = y->left;

    y->left = x;
    x->right = T2;

    avl_update_height(x);
    avl_update_height(y);

    return y;
}

AVLNode* avl_insert(AVLNode *root, int data) {
    if (root == NULL) return avl_create_node(data);

    if (data < root->data) {
        root->left = avl_insert(root->left, data);
    } else if (data > root->data) {
        root->right = avl_insert(root->right, data);
    } else {
        return root; // No duplicates
    }

    avl_update_height(root);
    int balance = avl_balance_factor(root);

    // LL case
    if (balance > 1 && data < root->left->data)
        return avl_rotate_right(root);

    // RR case
    if (balance < -1 && data > root->right->data)
        return avl_rotate_left(root);

    // LR case
    if (balance > 1 && data > root->left->data) {
        root->left = avl_rotate_left(root->left);
        return avl_rotate_right(root);
    }

    // RL case
    if (balance < -1 && data < root->right->data) {
        root->right = avl_rotate_right(root->right);
        return avl_rotate_left(root);
    }

    return root;
}
Enter fullscreen mode Exit fullscreen mode

4.3 Heaps and Priority Queues

A heap is a complete binary tree where parent >= children (max heap) or parent <= children (min heap).

Max Heap Implementation

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

#define MAX_HEAP_SIZE 100

typedef struct {
    int data[MAX_HEAP_SIZE];
    int size;
} MaxHeap;

void heap_init(MaxHeap *heap) { heap->size = 0; }
int heap_parent(int i) { return (i - 1) / 2; }
int heap_left(int i) { return 2 * i + 1; }
int heap_right(int i) { return 2 * i + 2; }

void heap_swap(int *a, int *b) { int t = *a; *a = *b; *b = t; }

void heap_sift_up(MaxHeap *heap, int i) {
    while (i > 0 && heap->data[heap_parent(i)] < heap->data[i]) {
        heap_swap(&heap->data[heap_parent(i)], &heap->data[i]);
        i = heap_parent(i);
    }
}

void heap_sift_down(MaxHeap *heap, int i) {
    int max_index = i;
    int left = heap_left(i);
    int right = heap_right(i);

    if (left < heap->size && heap->data[left] > heap->data[max_index])
        max_index = left;
    if (right < heap->size && heap->data[right] > heap->data[max_index])
        max_index = right;

    if (i != max_index) {
        heap_swap(&heap->data[i], &heap->data[max_index]);
        heap_sift_down(heap, max_index);
    }
}

bool heap_insert(MaxHeap *heap, int value) {
    if (heap->size >= MAX_HEAP_SIZE) return false;

    heap->data[heap->size] = value;
    heap_sift_up(heap, heap->size);
    heap->size++;
    return true;
}

int heap_extract_max(MaxHeap *heap) {
    if (heap->size == 0) return -1;

    int max = heap->data[0];
    heap->data[0] = heap->data[heap->size - 1];
    heap->size--;
    heap_sift_down(heap, 0);
    return max;
}
Enter fullscreen mode Exit fullscreen mode

5. Graph Fundamentals: Relationships and Connections

5.1 Graph Representation Methods

Adjacency Matrix

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

#define MAX_VERTICES 100

typedef struct {
    int matrix[MAX_VERTICES][MAX_VERTICES];
    int num_vertices;
    bool is_directed;
} GraphMatrix;

void graph_matrix_init(GraphMatrix *g, int vertices, bool directed) {
    g->num_vertices = vertices;
    g->is_directed = directed;
    for (int i = 0; i < vertices; i++)
        for (int j = 0; j < vertices; j++)
            g->matrix[i][j] = 0;
}

void graph_matrix_add_edge(GraphMatrix *g, int from, int to, int weight) {
    g->matrix[from][to] = weight;
    if (!g->is_directed) g->matrix[to][from] = weight;
}
Enter fullscreen mode Exit fullscreen mode

Adjacency List

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

#define MAX_VERTICES 100

typedef struct AdjNode {
    int dest;
    int weight;
    struct AdjNode *next;
} AdjNode;

typedef struct {
    AdjNode *head;
} AdjList;

typedef struct {
    AdjList array[MAX_VERTICES];
    int num_vertices;
    bool is_directed;
} GraphList;

void graph_list_init(GraphList *g, int vertices, bool directed) {
    g->num_vertices = vertices;
    g->is_directed = directed;
    for (int i = 0; i < vertices; i++)
        g->array[i].head = NULL;
}

void graph_list_add_edge(GraphList *g, int from, int to, int weight) {
    AdjNode *node = (AdjNode*)malloc(sizeof(AdjNode));
    node->dest = to;
    node->weight = weight;
    node->next = g->array[from].head;
    g->array[from].head = node;

    if (!g->is_directed) {
        node = (AdjNode*)malloc(sizeof(AdjNode));
        node->dest = from;
        node->weight = weight;
        node->next = g->array[to].head;
        g->array[to].head = node;
    }
}
Enter fullscreen mode Exit fullscreen mode

5.2 Traversal Algorithms

DFS and BFS

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

#define MAX_VERTICES 100

typedef struct AdjNode {
    int dest;
    struct AdjNode *next;
} AdjNode;

typedef struct {
    AdjNode *head;
} AdjList;

typedef struct {
    AdjList array[MAX_VERTICES];
    int num_vertices;
} Graph;

void graph_init(Graph *g, int vertices) {
    g->num_vertices = vertices;
    for (int i = 0; i < vertices; i++)
        g->array[i].head = NULL;
}

void graph_add_edge(Graph *g, int from, int to) {
    AdjNode *node = (AdjNode*)malloc(sizeof(AdjNode));
    node->dest = to;
    node->next = g->array[from].head;
    g->array[from].head = node;

    node = (AdjNode*)malloc(sizeof(AdjNode));
    node->dest = from;
    node->next = g->array[to].head;
    g->array[to].head = node;
}

// DFS Recursive
void dfs_recursive(Graph *g, int start, bool visited[]) {
    visited[start] = true;
    printf("%d ", start);

    AdjNode *current = g->array[start].head;
    while (current) {
        if (!visited[current->dest])
            dfs_recursive(g, current->dest, visited);
        current = current->next;
    }
}

// BFS Iterative
void bfs(Graph *g, int start) {
    bool visited[MAX_VERTICES] = {false};
    int queue[MAX_VERTICES];
    int front = 0, rear = 0;

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

    while (front < rear) {
        int v = queue[front++];
        printf("%d ", v);

        AdjNode *current = g->array[v].head;
        while (current) {
            if (!visited[current->dest]) {
                visited[current->dest] = true;
                queue[rear++] = current->dest;
            }
            current = current->next;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

5.3 Classic Graph Algorithms

Dijkstra's Shortest Path

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

#define MAX_VERTICES 100

int graph[MAX_VERTICES][MAX_VERTICES];
int num_vertices;

int min_distance(int dist[], bool visited[]) {
    int min = INT_MAX, min_index;
    for (int v = 0; v < num_vertices; v++)
        if (!visited[v] && dist[v] <= min) {
            min = dist[v];
            min_index = v;
        }
    return min_index;
}

void print_path(int parent[], int j) {
    if (parent[j] == -1) { printf("%d", j); return; }
    print_path(parent, parent[j]);
    printf(" -> %d", j);
}

void dijkstra(int start) {
    int dist[MAX_VERTICES];
    bool visited[MAX_VERTICES];
    int parent[MAX_VERTICES];

    for (int i = 0; i < num_vertices; i++) {
        dist[i] = INT_MAX;
        visited[i] = false;
        parent[i] = -1;
    }
    dist[start] = 0;

    for (int count = 0; count < num_vertices - 1; count++) {
        int u = min_distance(dist, visited);
        visited[u] = true;

        for (int v = 0; v < num_vertices; v++) {
            if (!visited[v] && graph[u][v] && dist[u] != INT_MAX
                && dist[u] + graph[u][v] < dist[v]) {
                dist[v] = dist[u] + graph[u][v];
                parent[v] = u;
            }
        }
    }

    printf("Vertex\tDistance\tPath\n");
    for (int i = 0; i < num_vertices; i++) {
        printf("%d\t%d\t\t", i, dist[i]);
        print_path(parent, i);
        printf("\n");
    }
}
Enter fullscreen mode Exit fullscreen mode

6. Hash Tables: The Power of Efficient Lookup

6.1 Hash Function Design

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

// Integer hash
unsigned int hash_int(int key, int table_size) {
    return key % table_size;
}

// String hash (djb2 algorithm)
unsigned int hash_string(const char *str, int table_size) {
    unsigned long hash = 5381;
    int c;
    while ((c = *str++))
        hash = ((hash << 5) + hash) + c;
    return hash % table_size;
}

// FNV-1a hash
unsigned int hash_fnv1a(const char *str, int table_size) {
    unsigned int hash = 2166136261u;
    while (*str) {
        hash ^= (unsigned char)*str++;
        hash *= 16777619;
    }
    return hash % table_size;
}
Enter fullscreen mode Exit fullscreen mode

6.2 Collision Resolution Methods

Separate Chaining

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

#define TABLE_SIZE 100

typedef struct HashNode {
    char *key;
    int value;
    struct HashNode *next;
} HashNode;

typedef struct {
    HashNode *buckets[TABLE_SIZE];
    int size;
} HashTable;

unsigned int hash(const char *key) {
    unsigned long hash = 5381;
    int c;
    while ((c = *key++))
        hash = ((hash << 5) + hash) + c;
    return hash % TABLE_SIZE;
}

void hash_table_init(HashTable *ht) {
    for (int i = 0; i < TABLE_SIZE; i++)
        ht->buckets[i] = NULL;
    ht->size = 0;
}

void hash_table_put(HashTable *ht, const char *key, int value) {
    unsigned int index = hash(key);
    HashNode *current = ht->buckets[index];

    while (current) {
        if (strcmp(current->key, key) == 0) {
            current->value = value;
            return;
        }
        current = current->next;
    }

    HashNode *new_node = (HashNode*)malloc(sizeof(HashNode));
    new_node->key = strdup(key);
    new_node->value = value;
    new_node->next = ht->buckets[index];
    ht->buckets[index] = new_node;
    ht->size++;
}

int* hash_table_get(HashTable *ht, const char *key) {
    unsigned int index = hash(key);
    HashNode *current = ht->buckets[index];

    while (current) {
        if (strcmp(current->key, key) == 0)
            return &current->value;
        current = current->next;
    }
    return NULL;
}
Enter fullscreen mode Exit fullscreen mode

Open Addressing (Linear Probing)

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

#define TABLE_SIZE 10
#define DELETED_NODE ((char*)-1)

typedef struct {
    char *key;
    int value;
} HashEntry;

typedef struct {
    HashEntry table[TABLE_SIZE];
    int size;
} OpenHashTable;

void open_hash_init(OpenHashTable *ht) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        ht->table[i].key = NULL;
        ht->table[i].value = 0;
    }
    ht->size = 0;
}

int open_hash_put(OpenHashTable *ht, const char *key, int value) {
    unsigned int index = hash(key);

    for (int i = 0; i < TABLE_SIZE; i++) {
        int pos = (index + i) % TABLE_SIZE;

        if (ht->table[pos].key == NULL || ht->table[pos].key == DELETED_NODE) {
            ht->table[pos].key = strdup(key);
            ht->table[pos].value = value;
            ht->size++;
            return 1;
        }

        if (strcmp(ht->table[pos].key, key) == 0) {
            ht->table[pos].value = value;
            return 1;
        }
    }
    return 0;
}

int* open_hash_get(OpenHashTable *ht, const char *key) {
    unsigned int index = hash(key);

    for (int i = 0; i < TABLE_SIZE; i++) {
        int pos = (index + i) % TABLE_SIZE;

        if (ht->table[pos].key == NULL) return NULL;
        if (ht->table[pos].key != DELETED_NODE && strcmp(ht->table[pos].key, key) == 0)
            return &ht->table[pos].value;
    }
    return NULL;
}
Enter fullscreen mode Exit fullscreen mode

Collision Resolution Comparison

Method Pros Cons Best For
Separate Chaining Simple, handles high load Extra pointer space General use, high collision
Linear Probing Cache-friendly, no pointers Clustering, complex delete Small tables, low collision
Quadratic Probing Less clustering Complex delete Medium tables
Double Hashing Best distribution Computation overhead High performance needs

7. Advanced Topics and Performance Analysis

7.1 Data Structure Selection Strategies

  • Frequent random access → Array
  • Frequent insert/delete → Linked List
  • LIFO access pattern → Stack
  • FIFO access pattern → Queue
  • Ordered data with fast lookup → BST or Hash Table
  • Priority-based access → Heap
  • Relationship modeling → Graph

7.2 Memory Management and Cache Optimization

Cache Optimization Tips:
1. Use contiguous memory (arrays vs linked lists)
2. Align data to cache lines (typically 64 bytes)
3. Access memory sequentially (spatial locality)
4. Reuse recently accessed data (temporal locality)
5. Consider cache-aware data structures (B-trees, cache-oblivious)
Enter fullscreen mode Exit fullscreen mode

7.3 Applications in Real Projects

  • Database Systems: B-trees for indexing, hash tables for caching
  • Network Protocol Stacks: Ring buffers, priority queues
  • Game Development: Spatial partitioning (quadtrees, octrees)
  • Compilers: Symbol tables (hash tables), AST (trees)

8. Common Interview Questions and Answers

8.1 Fundamental Concept Questions

Q1: Array vs Linked List - Differences and Use Cases?

Feature Array Linked List
Memory Layout Contiguous Discrete
Access Time O(1) random O(n) sequential
Insert/Delete O(n) shift elements O(1) modify pointers
Memory Overhead Data only Extra pointers
Cache Friendliness High Low
Size Adjustment Reallocate Dynamic

Selection Guide:

  • Frequent random access → Array
  • Frequent insert/delete → Linked List
  • Fixed data size → Array
  • Variable data size → Linked List or dynamic array

Q2: Stack vs Queue Applications?

Stack Applications:

  • Function call stack (recursion)
  • Expression evaluation
  • Parentheses matching
  • Browser back/forward
  • Undo/redo operations

Queue Applications:

  • Task scheduling
  • BFS traversal
  • Message queues
  • Buffer management
  • Print job queue

Q3: Tree Structure Advantages?

Characteristics:

  • Hierarchical data organization
  • Recursive definition, natural for recursive algorithms
  • One-to-many relationships

Advantages:

  • BST provides O(log n) search efficiency
  • Heaps enable efficient min/max retrieval
  • Foundation for sorting and searching algorithms
  • Core structure in file systems and database indices

8.2 Coding Implementation Challenges

Implement: LRU Cache

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

#define CACHE_SIZE 3

typedef struct CacheNode {
    int key;
    int value;
    struct CacheNode *prev;
    struct CacheNode *next;
} CacheNode;

typedef struct {
    CacheNode *head;
    CacheNode *tail;
    int size;
    int capacity;
} LRUCache;

LRUCache* lru_create(int capacity) {
    LRUCache *cache = (LRUCache*)malloc(sizeof(LRUCache));
    cache->head = cache->tail = NULL;
    cache->size = 0;
    cache->capacity = capacity;
    return cache;
}

void move_to_head(LRUCache *cache, CacheNode *node) {
    if (cache->head == node) return;

    if (node->prev) node->prev->next = node->next;
    if (node->next) node->next->prev = node->prev;
    if (cache->tail == node) cache->tail = node->prev;

    node->next = cache->head;
    node->prev = NULL;
    if (cache->head) cache->head->prev = node;
    cache->head = node;
}

int* lru_get(LRUCache *cache, int key) {
    CacheNode *current = cache->head;
    while (current) {
        if (current->key == key) {
            move_to_head(cache, current);
            return &current->value;
        }
        current = current->next;
    }
    return NULL;
}

void lru_put(LRUCache *cache, int key, int value) {
    CacheNode *current = cache->head;
    while (current) {
        if (current->key == key) {
            current->value = value;
            move_to_head(cache, current);
            return;
        }
        current = current->next;
    }

    CacheNode *new_node = (CacheNode*)malloc(sizeof(CacheNode));
    new_node->key = key;
    new_node->value = value;
    new_node->prev = NULL;
    new_node->next = cache->head;

    if (cache->head) cache->head->prev = new_node;
    cache->head = new_node;
    if (!cache->tail) cache->tail = new_node;

    cache->size++;

    if (cache->size > cache->capacity) {
        CacheNode *to_remove = cache->tail;
        cache->tail = to_remove->prev;
        if (cache->tail) cache->tail->next = NULL;
        printf("Evicted: key=%d, value=%d\n", to_remove->key, to_remove->value);
        free(to_remove);
        cache->size--;
    }
}
Enter fullscreen mode Exit fullscreen mode

Implement: Detect Cycle in Linked List

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

typedef struct ListNode {
    int val;
    struct ListNode *next;
} ListNode;

// Floyd's Cycle Detection
int has_cycle(ListNode *head) {
    if (!head || !head->next) return 0;

    ListNode *slow = head;
    ListNode *fast = head;

    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) return 1;
    }
    return 0;
}

// Find cycle entry
ListNode* find_cycle_entry(ListNode *head) {
    if (!head || !head->next) return NULL;

    ListNode *slow = head;
    ListNode *fast = head;

    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) {
            ListNode *entry = head;
            while (entry != slow) {
                entry = entry->next;
                slow = slow->next;
            }
            return entry;
        }
    }
    return NULL;
}

int cycle_length(ListNode *head) {
    if (!has_cycle(head)) return 0;
    ListNode *entry = find_cycle_entry(head);
    int length = 1;
    ListNode *current = entry->next;
    while (current != entry) {
        length++;
        current = current->next;
    }
    return length;
}
Enter fullscreen mode Exit fullscreen mode

Implement: Reverse Linked List

// Iterative
ListNode* reverse_list_iterative(ListNode *head) {
    ListNode *prev = NULL;
    ListNode *current = head;
    ListNode *next = NULL;

    while (current) {
        next = current->next;
        current->next = prev;
        prev = current;
        current = next;
    }
    return prev;
}

// Recursive
ListNode* reverse_list_recursive(ListNode *head) {
    if (!head || !head->next) return head;

    ListNode *new_head = reverse_list_recursive(head->next);
    head->next->next = head;
    head->next = NULL;
    return new_head;
}
Enter fullscreen mode Exit fullscreen mode

Implement: Find Kth from End

ListNode* find_kth_from_end(ListNode *head, int k) {
    if (!head || k <= 0) return NULL;

    ListNode *fast = head;
    ListNode *slow = head;

    for (int i = 0; i < k; i++) {
        if (!fast) return NULL;
        fast = fast->next;
    }

    while (fast) {
        fast = fast->next;
        slow = slow->next;
    }
    return slow;
}
Enter fullscreen mode Exit fullscreen mode

Implement: Merge Two Sorted Lists

// Iterative
ListNode* merge_two_lists_iterative(ListNode *l1, ListNode *l2) {
    ListNode dummy = {0, NULL};
    ListNode *tail = &dummy;

    while (l1 && l2) {
        if (l1->val <= l2->val) {
            tail->next = l1;
            l1 = l1->next;
        } else {
            tail->next = l2;
            l2 = l2->next;
        }
        tail = tail->next;
    }
    tail->next = l1 ? l1 : l2;
    return dummy.next;
}

// Recursive
ListNode* merge_two_lists_recursive(ListNode *l1, ListNode *l2) {
    if (!l1) return l2;
    if (!l2) return l1;

    if (l1->val <= l2->val) {
        l1->next = merge_two_lists_recursive(l1->next, l2);
        return l1;
    } else {
        l2->next = merge_two_lists_recursive(l1, l2->next);
        return l2;
    }
}
Enter fullscreen mode Exit fullscreen mode

Implement: Is Balanced Binary Tree

int check_height(TreeNode *root) {
    if (!root) return 0;

    int left = check_height(root->left);
    if (left == -1) return -1;

    int right = check_height(root->right);
    if (right == -1) return -1;

    if (abs(left - right) > 1) return -1;

    return (left > right ? left : right) + 1;
}

int is_balanced(TreeNode *root) {
    return check_height(root) != -1;
}
Enter fullscreen mode Exit fullscreen mode

Implement: Level Order Traversal by Level

void level_order_by_level(TreeNode *root) {
    if (!root) return;

    TreeNode *queue[MAX_QUEUE];
    int front = 0, rear = 0;
    queue[rear++] = root;

    while (front < rear) {
        int level_size = rear - front;
        printf("Level: ");

        for (int i = 0; i < level_size; i++) {
            TreeNode *node = queue[front++];
            printf("%d ", node->val);

            if (node->left) queue[rear++] = node->left;
            if (node->right) queue[rear++] = node->right;
        }
        printf("\n");
    }
}
Enter fullscreen mode Exit fullscreen mode

Implement: Lowest Common Ancestor (LCA)

TreeNode* lowest_common_ancestor(TreeNode *root, TreeNode *p, TreeNode *q) {
    if (!root || root == p || root == q) return root;

    TreeNode *left = lowest_common_ancestor(root->left, p, q);
    TreeNode *right = lowest_common_ancestor(root->right, p, q);

    if (left && right) return root;
    return left ? left : right;
}

// For BST
TreeNode* bst_lca(TreeNode *root, TreeNode *p, TreeNode *q) {
    if (!root) return NULL;

    if (root->val < p->val && root->val < q->val)
        return bst_lca(root->right, p, q);
    if (root->val > p->val && root->val > q->val)
        return bst_lca(root->left, p, q);
    return root;
}
Enter fullscreen mode Exit fullscreen mode

8.3 Performance Analysis Questions

Q1: Hash Table Performance Under Different Load Factors

Load Factor α = elements / buckets

Separate Chaining:
- Successful search: 1 + α/2 comparisons average
- Unsuccessful search: α comparisons average
- Recommended load factor: 0.7 - 0.8

Open Addressing (Linear Probing):
- Successful search: (1 + 1/(1-α)) / 2
- Unsuccessful search: (1 + 1/(1-α)²) / 2
- Recommended load factor: < 0.7

Examples:
α = 0.5: ~1.5 comparisons for successful search
α = 0.75: ~2.0 comparisons for successful search
α = 0.9: ~5.5 comparisons (performance drops sharply)
Enter fullscreen mode Exit fullscreen mode

Q2: Sorting Algorithm Comparison

Algorithm Best Average Worst Space Stable
Bubble Sort O(n) O(n²) O(n²) O(1) Yes
Selection Sort O(n²) O(n²) O(n²) O(1) No
Insertion Sort O(n) O(n²) O(n²) O(1) Yes
Quick Sort O(n log n) O(n log n) O(n²) O(log n) No
Merge Sort O(n log n) O(n log n) O(n log n) O(n) Yes
Heap Sort O(n log n) O(n log n) O(n log n) O(1) No

Selection Guide:

  • Small data → Insertion sort
  • Large data, stable → Merge sort
  • Large data, in-place → Quick sort or Heap sort

8.4 Advanced Interview Questions

Implement: Thread-Safe Queue

#include <pthread.h>

#define MAX_SIZE 100

typedef struct {
    int data[MAX_SIZE];
    int front, rear, size;
    pthread_mutex_t mutex;
    pthread_cond_t not_empty;
    pthread_cond_t not_full;
} ThreadSafeQueue;

void ts_queue_init(ThreadSafeQueue *q) {
    q->front = q->rear = q->size = 0;
    pthread_mutex_init(&q->mutex, NULL);
    pthread_cond_init(&q->not_empty, NULL);
    pthread_cond_init(&q->not_full, NULL);
}

void ts_queue_enqueue(ThreadSafeQueue *q, int value) {
    pthread_mutex_lock(&q->mutex);
    while (q->size >= MAX_SIZE)
        pthread_cond_wait(&q->not_full, &q->mutex);

    q->data[q->rear] = value;
    q->rear = (q->rear + 1) % MAX_SIZE;
    q->size++;

    pthread_cond_signal(&q->not_empty);
    pthread_mutex_unlock(&q->mutex);
}

int ts_queue_dequeue(ThreadSafeQueue *q) {
    pthread_mutex_lock(&q->mutex);
    while (q->size <= 0)
        pthread_cond_wait(&q->not_empty, &q->mutex);

    int value = q->data[q->front];
    q->front = (q->front + 1) % MAX_SIZE;
    q->size--;

    pthread_cond_signal(&q->not_full);
    pthread_mutex_unlock(&q->mutex);
    return value;
}
Enter fullscreen mode Exit fullscreen mode

Implement: Min Stack (O(1) Get Min)

#define MAX_SIZE 1000

typedef struct {
    int data[MAX_SIZE];
    int min_data[MAX_SIZE];
    int top;
} MinStack;

void min_stack_init(MinStack *s) { s->top = -1; }

void min_stack_push(MinStack *s, int val) {
    s->data[++s->top] = val;
    s->min_data[s->top] = (s->top == 0) ? val : 
        (val < s->min_data[s->top - 1] ? val : s->min_data[s->top - 1]);
}

int min_stack_pop(MinStack *s) { return s->data[s->top--]; }
int min_stack_get_min(MinStack *s) { return s->min_data[s->top]; }
Enter fullscreen mode Exit fullscreen mode

Implement: Maximum Subarray (Kadane's Algorithm)

#include <limits.h>

int max_subarray_sum(int nums[], int n) {
    int max_sum = INT_MIN;
    int current_sum = 0;

    for (int i = 0; i < n; i++) {
        current_sum += nums[i];
        if (current_sum > max_sum) max_sum = current_sum;
        if (current_sum < 0) current_sum = 0;
    }
    return max_sum;
}
Enter fullscreen mode Exit fullscreen mode

Implement: Top-K Elements (Min Heap)

typedef struct {
    int *data;
    int capacity;
    int size;
} MinHeap;

void heap_push_topk(MinHeap *h, int val) {
    if (h->size < h->capacity) {
        h->data[h->size++] = val;
        // Sift up
        int i = h->size - 1;
        while (i > 0 && h->data[(i-1)/2] > h->data[i]) {
            int t = h->data[i]; h->data[i] = h->data[(i-1)/2]; h->data[(i-1)/2] = t;
            i = (i-1)/2;
        }
    } else if (val > h->data[0]) {
        h->data[0] = val;
        // Sift down
        int i = 0;
        while (1) {
            int left = 2*i+1, right = 2*i+2, smallest = i;
            if (left < h->size && h->data[left] < h->data[smallest]) smallest = left;
            if (right < h->size && h->data[right] < h->data[smallest]) smallest = right;
            if (smallest == i) break;
            int t = h->data[i]; h->data[i] = h->data[smallest]; h->data[smallest] = t;
            i = smallest;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

8.5 System Design Questions

Q: How to Design an Efficient Cache System?

Key Considerations:

1. Cache Strategy:
   - LRU: Good for temporal locality
   - LFU: Good for stable access patterns
   - FIFO: Simple but less effective

2. Data Structure:
   - Hash Table + Doubly Linked List: O(1) get/put
   - Skip List: O(log n), supports range queries
   - Hash Table + Min Heap: For LFU

3. Concurrency Control:
   - Read-Write Lock: Read-heavy workloads
   - Segmented Locking: Reduce contention
   - Lock-free: CAS operations

4. Memory Management:
   - Pre-allocated memory pool
   - Object reuse
   - Memory compaction

5. Expiration:
   - Lazy deletion: Check on access
   - Periodic cleanup: Background thread
   - Active deletion: Timers

Architecture:
┌─────────────────────────────────┐
│           Client                │
└─────────────┬───────────────────┘
              │
┌─────────────▼───────────────────┐
│        Cache Interface          │
│  ┌─────────────────────────┐   │
│  │   Hash Table (O(1))     │   │
│  └───────────┬─────────────┘   │
│              │                  │
│  ┌───────────▼─────────────┐   │
│  │  Doubly Linked List     │   │
│  │  (LRU order)            │   │
│  └─────────────────────────┘   │
└─────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

9. Conclusion

Recommended Learning Path

  1. Beginner Level: Master basic implementations of arrays, linked lists, stacks, and queues
  2. Intermediate Level: Deepen understanding of tree structures, graph fundamentals, and hash tables
  3. Advanced Level: Learn about balanced trees, advanced graph algorithms, and performance optimization
  4. Practical Level: Apply data structures in projects to solve real problems

Recommended Resources

  • Books:
    • "Introduction to Algorithms" - Classic algorithm textbook
    • "The C Programming Language" - C language bible
    • "Data Structures and Algorithm Analysis in C"
  • Online Platforms:
    • LeetCode - Algorithm practice platform
    • GeeksforGeeks - Data structures tutorials
    • MIT OpenCourseWare - Free course resources

Community and Contributions

  • Contribute to open-source projects with data structure libraries
  • Share your implementation experiences in technical communities
  • Follow developments in the C language standard and new features

Appendix

Complete Code Repository

All code examples in this article can be found at:
[GitHub Repository Link]

Performance Test Results

Benchmark tests were conducted on operations of different data structures:
| Data Structure | Insert Operation | Search Operation | Delete Operation | Space Complexity |
|----------------|-----------------|------------------|------------------|------------------|
| Dynamic Array | O(1) amortized | O(1) | O(n) | O(n) |
| Singly Linked List | O(1) | O(n) | O(1) | O(n) |
| Binary Search Tree | O(log n) | O(log n) | O(log n) | O(n) |
| Hash Table | O(1) | O(1) | O(1) | O(n) |

References

  1. Cormen, T.H., et al. "Introduction to Algorithms." MIT Press, 2009.
  2. Knuth, D.E. "The Art of Computer Programming." Addison-Wesley, 1997.
  3. Kernighan, B.W., Ritchie, D.M. "The C Programming Language." Prentice Hall, 1988.

Author: [Your Name]
Last Updated: March 29, 2026
License: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International

Top comments (0)