10 Essential Data Structures Every C Programmer Should Know
Table of Contents
1. Arrays
Fundamental Concepts
An array is a collection of elements of the same data type stored in contiguous memory locations. It has a fixed size, which is determined at the time of declaration.
Usage Methods
#include <stdio.h>
int main() {
// Declaration and initialization of an array
int arr[5] = {1, 2, 3, 4, 5};
// Accessing array elements
for(int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
return 0;
}
Common Practices
- Use arrays when you know the number of elements in advance.
- Access elements randomly using the index.
Best Practices
- Always check the index bounds to avoid buffer overflows.
2. Linked Lists
Fundamental Concepts
A linked list is a linear data structure where each element is a separate object called a node. Each node contains data and a reference (pointer) to the next node in the sequence.
Usage Methods
#include <stdio.h>
#include <stdlib.h>
// Define a node structure
struct Node {
int data;
struct Node* next;
};
// Function to create a new node
struct Node* newNode(int data) {
struct Node* node = (struct Node*)malloc(sizeof(struct Node));
node->data = data;
node->next = NULL;
return node;
}
int main() {
struct Node* head = newNode(1);
head->next = newNode(2);
head->next->next = newNode(3);
// Traverse the linked list
struct Node* temp = head;
while(temp != NULL) {
printf("%d ", temp->data);
temp = temp->next;
}
return 0;
}
Common Practices
- Use linked lists when you need to insert or delete elements frequently.
- Implement operations like insertion, deletion, and traversal.
Best Practices
- Always free the memory allocated for nodes to avoid memory leaks.
3. Stacks
Fundamental Concepts
A stack is a linear data structure that follows the Last - In - First - Out (LIFO) principle. Elements can be added (pushed) and removed (popped) only from the top of the stack.
Usage Methods
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
// Stack structure
struct Stack {
int top;
int items[MAX_SIZE];
};
// Function to initialize the stack
void initStack(struct Stack* s) {
s->top = -1;
}
// Function to check if the stack is empty
int isEmpty(struct Stack* s) {
return s->top == -1;
}
// Function to push an element onto the stack
void push(struct Stack* s, int item) {
if(s->top == MAX_SIZE - 1) {
printf("Stack overflow\n");
return;
}
s->items[++(s->top)] = item;
}
// Function to pop an element from the stack
int pop(struct Stack* s) {
if(isEmpty(s)) {
printf("Stack underflow\n");
return -1;
}
return s->items[(s->top)--];
}
int main() {
struct Stack s;
initStack(&s);
push(&s, 1);
push(&s, 2);
printf("%d\n", pop(&s));
return 0;
}
Common Practices
- Use stacks for expression evaluation, backtracking, and function call management.
Best Practices
- Check for stack overflow and underflow conditions.
4. Queues
Fundamental Concepts
A queue is a linear data structure that follows the First - In - First - Out (FIFO) principle. Elements are added at the rear (enqueue) and removed from the front (dequeue).
Usage Methods
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
// Queue structure
struct Queue {
int front, rear;
int items[MAX_SIZE];
};
// Function to initialize the queue
void initQueue(struct Queue* q) {
q->front = q->rear = -1;
}
// Function to check if the queue is empty
int isEmpty(struct Queue* q) {
return q->front == -1;
}
// Function to enqueue an element
void enqueue(struct Queue* q, int item) {
if(q->rear == MAX_SIZE - 1) {
printf("Queue overflow\n");
return;
}
if(q->front == -1) q->front = 0;
q->items[++(q->rear)] = item;
}
// Function to dequeue an element
int dequeue(struct Queue* q) {
if(isEmpty(q)) {
printf("Queue underflow\n");
return -1;
}
int item = q->items[(q->front)++];
if(q->front > q->rear) q->front = q->rear = -1;
return item;
}
int main() {
struct Queue q;
initQueue(&q);
enqueue(&q, 1);
enqueue(&q, 2);
printf("%d\n", dequeue(&q));
return 0;
}
Common Practices
- Use queues for job scheduling, breadth - first search, and resource management.
Best Practices
- Check for queue overflow and underflow conditions.
5. Trees
Fundamental Concepts
A tree is a hierarchical data structure consisting of nodes connected by edges. Each node can have zero or more child nodes, and there is a single root node.
Usage Methods
#include <stdio.h>
#include <stdlib.h>
// Tree node structure
struct TreeNode {
int data;
struct TreeNode* left;
struct TreeNode* right;
};
// Function to create a new tree node
struct TreeNode* newTreeNode(int data) {
struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode));
node->data = data;
node->left = node->right = NULL;
return node;
}
int main() {
struct TreeNode* root = newTreeNode(1);
root->left = newTreeNode(2);
root->right = newTreeNode(3);
return 0;
}
Common Practices
- Use trees for hierarchical data representation, searching, and sorting.
Best Practices
- Implement tree traversal algorithms like in - order, pre - order, and post - order traversal.
6. Binary Search Trees
Fundamental Concepts
A binary search tree (BST) is a binary tree where for each node, all nodes in its left subtree have values less than the node’s value, and all nodes in its right subtree have values greater than the node’s value.
Usage Methods
#include <stdio.h>
#include <stdlib.h>
// BST node structure
struct BSTNode {
int data;
struct BSTNode* left;
struct BSTNode* right;
};
// Function to create a new BST node
struct BSTNode* newBSTNode(int data) {
struct BSTNode* node = (struct BSTNode*)malloc(sizeof(struct BSTNode));
node->data = data;
node->left = node->right = NULL;
return node;
}
// Function to insert a node into the BST
struct BSTNode* insert(struct BSTNode* root, int data) {
if(root == NULL) return newBSTNode(data);
if(data < root->data)
root->left = insert(root->left, data);
else if(data > root->data)
root->right = insert(root->right, data);
return root;
}
// Function to search for a node in the BST
struct BSTNode* search(struct BSTNode* root, int data) {
if(root == NULL || root->data == data) return root;
if(data < root->data)
return search(root->left, data);
return search(root->right, data);
}
int main() {
struct BSTNode* root = NULL;
root = insert(root, 5);
insert(root, 3);
insert(root, 7);
struct BSTNode* result = search(root, 3);
if(result != NULL) printf("Found\n");
return 0;
}
Common Practices
- Use BSTs for efficient searching, insertion, and deletion operations.
Best Practices
- Balance the BST to ensure optimal performance.
7. Heaps
Fundamental Concepts
A heap is a complete binary tree that satisfies the heap property. A max - heap has the property that the value of each node is greater than or equal to the values of its children, while a min - heap has the opposite property.
Usage Methods
#include <stdio.h>
#include <stdlib.h>
// Function to swap two integers
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
// Function to heapify a subtree rooted at index i
void heapify(int arr[], int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if(left < n && arr[left] > arr[largest])
largest = left;
if(right < n && arr[right] > arr[largest])
largest = right;
if(largest != i) {
swap(&arr[i], &arr[largest]);
heapify(arr, n, largest);
}
}
// Function to build a max - heap
void buildMaxHeap(int arr[], int n) {
for(int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
}
int main() {
int arr[] = {12, 11, 13, 5, 6, 7};
int n = sizeof(arr) / sizeof(arr[0]);
buildMaxHeap(arr, n);
for(int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}
Common Practices
- Use heaps for priority queues, sorting (heap sort), and graph algorithms.
Best Practices
- Maintain the heap property during insertions and deletions.
8. Hash Tables
Fundamental Concepts
A hash table is a data structure that uses a hash function to compute an index into an array of buckets or slots, from which the desired value can be found.
Usage Methods
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TABLE_SIZE 10
// Hash table node structure
struct HashNode {
char* key;
int value;
struct HashNode* next;
};
// Hash function
unsigned int hash(const char* key) {
unsigned int hashval = 0;
for(int i = 0; key[i] != '\0'; i++)
hashval = key[i] + (hashval << 6) + (hashval << 16) - hashval;
return hashval % TABLE_SIZE;
}
// Function to insert a key - value pair into the hash table
void insertHash(struct HashNode** table, const char* key, int value) {
unsigned int index = hash(key);
struct HashNode* newNode = (struct HashNode*)malloc(sizeof(struct HashNode));
newNode->key = strdup(key);
newNode->value = value;
newNode->next = table[index];
table[index] = newNode;
}
// Function to search for a key in the hash table
int searchHash(struct HashNode** table, const char* key) {
unsigned int index = hash(key);
struct HashNode* current = table[index];
while(current != NULL) {
if(strcmp(current->key, key) == 0)
return current->value;
current = current->next;
}
return -1;
}
int main() {
struct HashNode* table[TABLE_SIZE] = {NULL};
insertHash(table, "apple", 1);
insertHash(table, "banana", 2);
printf("%d\n", searchHash(table, "apple"));
return 0;
}
Common Practices
- Use hash tables for fast data retrieval, dictionary implementation, and caching.
Best Practices
- Choose a good hash function to minimize collisions.
9. Graphs
Fundamental Concepts
A graph is a non - linear data structure consisting of a set of vertices (nodes) and a set of edges that connect pairs of vertices.
Usage Methods
#include <stdio.h>
#include <stdlib.h>
#define MAX_VERTICES 100
// Graph structure
struct Graph {
int V;
int adj[MAX_VERTICES][MAX_VERTICES];
};
// Function to create a graph
struct Graph* createGraph(int V) {
struct Graph* graph = (struct Graph*)malloc(sizeof(struct Graph));
graph->V = V;
for(int i = 0; i < V; i++)
for(int j = 0; j < V; j++)
graph->adj[i][j] = 0;
return graph;
}
// Function to add an edge to the graph
void addEdge(struct Graph* graph, int src, int dest) {
graph->adj[src][dest] = 1;
graph->adj[dest][src] = 1;
}
int main() {
struct Graph* graph = createGraph(3);
addEdge(graph, 0, 1);
addEdge(graph, 1, 2);
return 0;
}
Common Practices
- Use graphs for representing networks, social relationships, and path - finding problems.
Best Practices
- Choose the appropriate graph representation (adjacency matrix or adjacency list) based on the problem.
10. Tries
Fundamental Concepts
A trie (also known as a prefix tree) is a tree - like data structure used for efficient retrieval of a key in a dataset of strings.
Usage Methods
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ALPHABET_SIZE 26
// Trie node structure
struct TrieNode {
struct TrieNode* children[ALPHABET_SIZE];
int isEndOfWord;
};
// Function to create a new trie node
struct TrieNode* newTrieNode() {
struct TrieNode* node = (struct TrieNode*)malloc(sizeof(struct TrieNode));
for(int i = 0; i < ALPHABET_SIZE; i++)
node->children[i] = NULL;
node->isEndOfWord = 0;
return node;
}
// Function to insert a word into the trie
void insertTrie(struct TrieNode* root, const char* word) {
struct TrieNode* current = root;
for(int i = 0; word[i] != '\0'; i++) {
int index = word[i] - 'a';
if(current->children[index] == NULL)
current->children[index] = newTrieNode();
current = current->children[index];
}
current->isEndOfWord = 1;
}
// Function to search for a word in the trie
int searchTrie(struct TrieNode* root, const char* word) {
struct TrieNode* current = root;
for(int i = 0; word[i] != '\0'; i++) {
int index = word[i] - 'a';
if(current->children[index] == NULL)
return 0;
current = current->children[index];
}
return current->isEndOfWord;
}
int main() {
struct TrieNode* root = newTrieNode();
insertTrie(root, "apple");
printf("%d\n", searchTrie(root, "apple"));
return 0;
}
Common Practices
- Use tries for autocomplete, spell - checking, and IP routing.
Best Practices
- Free the memory allocated for trie nodes to avoid memory leaks.
Conclusion
In this blog, we have explored ten essential data structures that every C programmer should know. Each data structure has its own unique properties, advantages, and