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;
}
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;
}
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;
}
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;
}
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;
}
(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; }
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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");
}
}
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);
}
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);
}
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;
}
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;
}
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;
}
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;
}
}
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;
}
}
}
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");
}
}
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;
}
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 ¤t->value;
current = current->next;
}
return NULL;
}
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;
}
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)
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 ¤t->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--;
}
}
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;
}
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;
}
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;
}
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;
}
}
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;
}
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");
}
}
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;
}
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)
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;
}
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]; }
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;
}
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;
}
}
}
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) │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
9. Conclusion
Recommended Learning Path
- Beginner Level: Master basic implementations of arrays, linked lists, stacks, and queues
- Intermediate Level: Deepen understanding of tree structures, graph fundamentals, and hash tables
- Advanced Level: Learn about balanced trees, advanced graph algorithms, and performance optimization
- 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
- Cormen, T.H., et al. "Introduction to Algorithms." MIT Press, 2009.
- Knuth, D.E. "The Art of Computer Programming." Addison-Wesley, 1997.
- 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)