W5 - Shorts 📚🔍- Data Structures in C (Linked Lists, BST, Hash Tables, Tries, Stacks, Queues, and Dictionaries)
Read more here
CS50 IDE: https://cs50.dev/ (Online VSCode)
CS50 Library Documentation: https://manual.cs50.io/#cs50.h
#CS50x #C #Computer_Science #VisualStudioCode
Table of Contents:
A) Comparing Key Data Structures: Arrays, Linked Lists, Hash Tables, and Tries 🎦
In CS50, we’ve covered various data structures—arrays, linked lists, hash tables, and tries—that serve as the foundation for most implementations in C. Each has its strengths and weaknesses, depending on your data and use case. Let’s consolidate their properties to evaluate which structure might suit specific situations.
A.1) Arrays
Strengths:
- Lookup: Constant time (
) due to random access. - Example: Accessing the 7th element is as simple as
array[6]
.
- Example: Accessing the 7th element is as simple as
- Sorting: Arrays are relatively easy to sort.
- Algorithms like selection sort, insertion sort, bubble sort, and merge sort work directly on arrays.
- Compact Size: Only use as much memory as necessary to hold the data.
Weaknesses:
-
Insertion: Poor performance, especially in the middle of the array.
- Requires shifting elements (
). - Example:
// Insert 5 at index 2 for (int i = n; i > 2; i--) { array[i] = array[i - 1]; } array[2] = 5;
- Requires shifting elements (
-
Deletion: Also requires shifting elements to avoid gaps.
- Example:
// Remove element at index 3 for (int i = 3; i < n - 1; i++) { array[i] = array[i + 1]; }
- Example:
-
Fixed Size: Declaring a size upfront limits flexibility.
- Resizing involves creating a new array and copying elements:
int *new_array = malloc(new_size * sizeof(int)); for (int i = 0; i < old_size; i++) { new_array[i] = array[i]; } free(array); array = new_array;
A.2) Linked Lists
Strengths:
-
Insertion: Efficient (
) at the beginning or end. - Example:
node *new_node = malloc(sizeof(node)); new_node->data = value; new_node->next = head; head = new_node;
- Example:
-
Deletion: Simply involves updating pointers and freeing memory, with no need for shifting elements.
- Example:
node *temp = head; head = head->next; free(temp);
- Example:
Weaknesses:
-
Lookup: Inefficient (
) as you must traverse the list to find an element. -
Sorting: Difficult. Sorting requires constructing the list in order, which negates the benefit of quick insertions.
- Example: To insert in order:
node *prev = NULL, *curr = head; while (curr && value > curr->data) { prev = curr; curr = curr->next; } new_node->next = curr; prev->next = new_node;
- Example: To insert in order:
-
Space: Linked lists require extra memory for pointers.
A.3) Hash Tables
Strengths:
-
Insertion: Efficient due to hashing (
on average). - Example:
int index = hash_function(key); node *new_node = malloc(sizeof(node)); new_node->key = key; new_node->value = value; new_node->next = table[index]; table[index] = new_node;
- Example:
-
Deletion: Quick for separate chaining. Update pointers and free memory.
- Example:
node *prev = NULL, *curr = table[index]; while (curr && strcmp(curr->key, key) != 0) { prev = curr; curr = curr->next; } if (curr) { if (prev) prev->next = curr->next; else table[index] = curr->next; free(curr); }
- Example:
-
Lookup: Faster than linked lists, as each bucket contains a smaller linked list.
Weaknesses:
- Collisions: If hash function is poor, collisions increase, degrading performance.
- Sorting: Difficult, as hash tables are not designed for ordered data.
- Size: Can waste memory if the hash table is too large or too small.
A.4) Tries
Strengths:
-
Insertion: Efficient and constant time relative to the length of the key (
). - Example:
trie_node *curr = root; for (int i = 0; key[i]; i++) { int index = key[i] - 'a'; if (!curr->children[index]) { curr->children[index] = create_node(); } curr = curr->children[index]; } curr->is_end_of_word = true;
- Example:
-
Lookup: Also
, as it only depends on the key length, not the total number of entries. - Example:
trie_node *curr = root; for (int i = 0; key[i]; i++) { int index = key[i] - 'a'; if (!curr->children[index]) return false; curr = curr->children[index]; } return curr->is_end_of_word;
- Example:
-
Sorting: Inherently sorted as entries are stored in lexicographical order.
Weaknesses:
- Space: Tries require large amounts of memory, as each node contains multiple pointers (e.g., 26 for lowercase letters).
- Example: Each node in a trie with 26 children uses:
bytes on a 64-bit system.
A.5) Summary of Strengths and Weaknesses
Structure | Insertion | Deletion | Lookup | Sorting | Size |
---|---|---|---|---|---|
Array | O(1) (end) / O(n) (middle) | O(1) (end) / O(n) (middle) | O(1) (random access) | Easy (O(nlogn) | Compact |
Linked List | O(1) | O(1) | O(n) | Hard | Small |
Hash Table | O(1) (average) | O(1) | O(1) (average) | Hard | Flexible |
Trie | O(k) | O(k) | O(k) | Inherent | Large |
Final Thoughts: how to chose
Choosing the right data structure depends on your specific needs:
- If you prioritize random access, use an array.
- If you need dynamic insertion/deletion, go with a linked list.
- For fast lookups and flexible sizes, use a hash table.
- If you deal with prefixes or lexicographical data, a trie may be the best choice.
Ultimately, all data structures come with trade-offs. The best choice depends on balancing speed, memory usage, and functionality for your application.
B) Structures in C: Creating Custom Data Types 🎦
Video: https://youtu.be/E4lb2gkyXr8
C provides basic data types like int
, float
, char
, and double
, and CS50 introduces additional types like string
and bool
.
But what if we want to combine multiple variables, even of different types, into a single cohesive unit?
Enter structures, a powerful feature of C that allows us to define custom data types that unify multiple fields with logical connections.
Example:
B.1) Introduction to Structures
[[Structures]] allow us to group different variables (potentially of different types) into a "super variable."
Unlike arrays, which can only hold values of the same type, structures can mix and match data types.
B.1.0) Why Use Structures?
Logical Grouping
Structures allow us to group logically related data into a single unit, making code more readable and maintainable.
Custom Data Types
You can define types that represent real-world entities like cars, students, or books.
Example: Student Structure
struct student {
int id;
char name[50];
float gpa;
};
// Create a student variable
struct student s1;
s1.id = 1001;
strcpy(s1.name, "Alice");
s1.gpa = 3.9;
B.1.1) Example: Defining a Structure
To define a structure, we use the struct
keyword, followed by a name and a set of fields.
Example: Car Structure
struct car {
int year; // Year of manufacture
char model[20]; // Model name
char plate[8]; // License plate
int odometer; // Mileage in miles
float engine_size; // Engine capacity in liters
};
struct car
is the type name.- Inside the
{}
we define the fields, each with its own data type.
Important: Don’t forget the semicolon ;
at the end of the structure definition!
B.1.2) Example: Using Structures
Declaring a Variable:
To create a variable of the struct car
type:
struct car my_car;
Accessing Fields:
We use the [[dot operator]] (.
) to access individual fields.
my_car.year = 2011;
strcpy(my_car.model, "Civic"); // Copy string into `model`
my_car.odometer = 50505;
my_car.engine_size = 1.8;
B.2) Dynamic Allocation of Structures (malloc and linked lists)
When we don’t know how many structures we’ll need at runtime, we can dynamically allocate memory using malloc
.
Example: Allocating a struct car
Example: Allocating a struct car
struct car *my_car = malloc(sizeof(struct car));
my_car->year = 2015; // Use the arrow operator (->)
strcpy(my_car->model, "Camry");
my_car->odometer = 75000;
my_car->engine_size = 2.5;
Explanation:
malloc
allocates enough memory for astruct car
.my_car
is a pointer to that memory.- We use the arrow operator (
->
) to dereference the pointer and access fields.
B.2.1) Dot Operator vs. Arrow Operator
-
If you have a normal structure variable, use the dot operator:
my_car.year = 2011;
-
If you have a pointer to a structure, use the arrow operator:
my_car->year = 2011;
The arrow operator is a shorthand for:
(*my_car).year = 2011;
B.3) Note: Structures in Functions (passed by value or reference)
Structures can be passed to functions by value or reference.
By Value
Passing a copy of the structure:
void print_student(struct student s) {
printf("ID: %d, Name: %s, GPA: %.2f\n", s.id, s.name, s.gpa);
}
print_student(s1);
By Reference
Passing a pointer to avoid copying:
void update_gpa(struct student *s, float new_gpa) {
s->gpa = new_gpa; // Use arrow operator for pointers
}
update_gpa(&s1, 4.0);
B.4) Key Takeaways: struct
-
Combining Data: Structures unify variables of different types into one logical unit.
-
Dynamic Allocation: Use
malloc
and the arrow operator for dynamically allocated structures. -
Shortcuts: The arrow operator (
->
) simplifies dereferencing and accessing fields. -
Real-World Applications: Structures can model complex data like cars, students, or even more abstract concepts.
C) Singly-Linked Lists in C 🎦
Video: https://youtu.be/zQI3FyWm144
Introduction: Linked Lists in C
- C provides us with basic data types and structures, but by combining them, we can create more powerful and flexible data structures.
- Linked Lists allow us to grow and shrink collections of values dynamically, solving limitations of arrays like:
- Insertion Complexity: Arrays require shifting elements for insertion.
- Fixed Size: Arrays have a fixed size and require reallocation to resize.
What is a linked list?
- A linked list is made up of nodes, where:
- Each node contains data (e.g., an integer, float, etc.).
- Each node contains a pointer to the next node in the list.
- Singly linked list (SLL):
- Each node points to the next one, forming a chain.
- The last node points to
NULL
, signifying the end of the list.
Defining a linked list node
typedef struct sllist {
int value; // Data stored in the node
struct sllist *next; // Pointer to the next node
} sll_node;
- Self-referential structures:
- A structure that contains a pointer to the same type of structure.
- Temporary name needed: While defining the struct, we use
struct sllist
temporarily becausesll_node
isn't recognized until the definition is complete.
Operations on linked lists
Creating a linked list node
Function prototype:
sll_node *create(int value);
Steps:
- Dynamically allocate memory for a new node using
malloc
. - Initialize fields:
- Set
value
to the provided data. - Set
next
toNULL
(no next node yet).
- Set
- Return a pointer to the new node.
Example implementation:
sll_node *create(int value) {
sll_node *new_node = malloc(sizeof(sll_node));
if (new_node == NULL) {
return NULL; // Memory allocation failed
}
new_node->value = value;
new_node->next = NULL;
return new_node;
}
Searching a linked list
Function prototype:
bool find(sll_node *head, int value);
Steps:
- Start at the head of the list.
- Traverse the list using a traversal pointer.
- Check each node's value:
- If found, return
true
. - If
NULL
is reached, returnfalse
.
- If found, return
Example implementation:
bool find(sll_node *head, int value) {
sll_node *trav = head;
while (trav != NULL) {
if (trav->value == value) {
return true;
}
trav = trav->next;
}
return false;
}
Inserting at the beginning
Function prototype:
sll_node *insert(sll_node *head, int value);
Steps:
- Allocate a new node with the given value.
- Point the new node's
next
to the current head of the list. - Move the head pointer to the new node.
- Return the updated head pointer.
Example implementation:
sll_node *insert(sll_node *head, int value) {
sll_node *new_node = create(value);
if (new_node == NULL) {
return NULL; // Memory allocation failed
}
new_node->next = head;
return new_node;
}
Deleting the entire list
Function prototype:
void destroy(sll_node *head);
Steps:
- Use recursion to delete the rest of the list before freeing the current node.
- Base case: Stop when
head
isNULL
.
Example implementation:
void destroy(sll_node *head) {
if (head == NULL) {
return; // Base case: end of the list
}
destroy(head->next); // Recursively delete the rest of the list
free(head); // Free the current node
}
Deleting a single element
- Deleting a node in a singly linked list requires:
- Finding the node to delete.
- Maintaining links between nodes to avoid "orphaning" parts of the list.
Challenges:
- Singly linked lists don't allow backward traversal.
- Solutions:
- Use two pointers: one trailing the other.
- Traverse the list and adjust pointers carefully.
Advantages and disadvantages of linked lists
Advantages:
- Dynamic size: Can grow or shrink as needed.
- Efficient insertion/deletion: At the beginning of the list, operations are O(1).
Disadvantages:
- Sequential access: No random access like arrays.
- Memory overhead: Each node requires extra memory for the pointer.
- Complexity: Manipulating pointers can be error-prone.
Next steps: doubly linked lists
- Singly vs. doubly linked lists:
- Doubly linked lists allow backward traversal, simplifying certain operations like deletion.
- Applications:
- Linked lists are foundational for more complex data structures like stacks, queues, and graphs.
D) Doubly-Linked Lists in C 🎦
Video: https://youtu.be/FHMPswJDCvU
Introduction to Doubly-Linked Lists
- Doubly-linked lists address some limitations of singly-linked lists:
- They allow traversal both forwards and backwards through the list.
- This capability simplifies operations like deleting a single element.
- Trade-off: Nodes in doubly-linked lists are larger due to the extra field (the
previous
pointer), requiring more memory.
Structure of a Doubly-Linked List Node
typedef struct dllist {
int value; // Data stored in the node
struct dllist *next; // Pointer to the next node
struct dllist *prev; // Pointer to the previous node
} dll_node;
- New Field:
prev
: Points to the previous node, enabling backward traversal.
- Each node contains:
- Data (e.g., integers or other types).
- Pointer to the next node (
next
). - Pointer to the previous node (
prev
).
Operations on Doubly-Linked Lists
Inserting a Node at the Beginning
Function prototype:
dll_node *insert(dll_node *head, int value);
Steps:
- Dynamically allocate memory for a new node.
- Set the node's
value
. - Point the new node's
next
to the current head of the list. - Set the
prev
pointer of the old head to the new node. - Update the list's head pointer to point to the new node.
- Return the updated head pointer.
Example implementation:
dll_node *insert(dll_node *head, int value) {
dll_node *new_node = malloc(sizeof(dll_node));
if (new_node == NULL) {
return NULL; // Memory allocation failed
}
new_node->value = value;
new_node->next = head;
new_node->prev = NULL; // New node has no previous node
if (head != NULL) {
head->prev = new_node; // Update the old head's previous pointer
}
return new_node;
}
Deleting a Node
Function prototype:
void delete_node(dll_node **head_ref, dll_node *node_to_delete);
Steps:
- Update the
next
pointer of the previous node (if it exists) to skip the node being deleted. - Update the
prev
pointer of the next node (if it exists) to skip the node being deleted. - Free the memory occupied by the node.
- If the node being deleted is the head, update the head pointer.
Example implementation:
void delete_node(dll_node **head_ref, dll_node *node_to_delete) {
if (node_to_delete == NULL) return;
// Update the next pointer of the previous node
if (node_to_delete->prev != NULL) {
node_to_delete->prev->next = node_to_delete->next;
} else {
// Node to delete is the head
*head_ref = node_to_delete->next;
}
// Update the prev pointer of the next node
if (node_to_delete->next != NULL) {
node_to_delete->next->prev = node_to_delete->prev;
}
free(node_to_delete); // Free the memory occupied by the node
}
Advantages of Doubly-Linked Lists
- Bidirectional traversal:
- Nodes can be traversed both forwards and backwards.
- Efficient deletion:
- Deleting a node requires fewer steps since we can move backward.
- Insertion flexibility:
- New nodes can be inserted at the beginning, middle, or end with minimal pointer adjustments.
Trade-offs of Linked Lists
Advantages
- Efficient insertion and deletion:
- Constant time operations for adding or removing elements.
- Dynamic memory usage:
- No wasted space like arrays (no need to predefine size).
Disadvantages
- Linear search: Searching for an element takes O(n) time.
- Memory overhead: Each node requires extra memory for pointers (
next
andprev
in doubly-linked lists). - Complexity: Pointer manipulation can lead to bugs if not handled carefully.
Comparison: Singly vs. Doubly-Linked Lists
Feature | Singly-Linked List | Doubly-Linked List |
---|---|---|
Traversal | Forward only | Forward and backward |
Memory usage | Less (one pointer per node) | More (two pointers per node) |
Insertion | Simple at the beginning | Slightly more complex |
Deletion | Requires extra traversal | More efficient |
Applications of Doubly-Linked Lists
- Implementing deques, stacks, and queues.
- Undo/redo functionality in applications.
- Navigation in file systems or multilevel menus.
Summary: Doubly-Linked Lists
- Doubly-linked lists improve flexibility and efficiency in deletion and traversal compared to singly-linked lists.
- They require more memory and careful pointer management.
- Choosing between singly- and doubly-linked lists depends on the specific use case and memory constraints.
E) Hash Tables in C 🎦
Video: https://youtu.be/nvzVHwrrub0
Introduction to Hash Tables
- Hash tables combine arrays and linked lists to leverage the benefits of both:
- Advantages of arrays: Fast, random access (e.g., array[4], array[8]).
- Advantages of linked lists: Dynamic resizing, efficient insertion, and deletion.
- Hash tables provide near constant-time performance for:
- Insertion
- Deletion
- Lookup
- Limitation: Hash tables are not suitable for ordered or sorted data. Attempting to use them for sorting negates their performance benefits.
Core Components of a Hash Table
- Hash Function:
- Takes input data and generates a non-negative integer (hashcode).
- Determines where data is stored in the array.
- Array:
- Stores pointers to data (or linked lists in advanced implementations).
How a Hash Table Works
- Data is passed through a hash function.
- The hash function returns a hashcode.
- The hashcode is used as an index to store the data in the array.
- To retrieve the data:
- Pass the input through the hash function.
- Use the resulting hashcode to access the data directly.
Example:
- Insert
John
:- Run
John
through the hash function → returns4
. - Store
John
at array location4
.
- Run
- Lookup
John
:- Run
John
through the hash function → returns4
. - Access array location
4
.
- Run
Hash Functions: Characteristics of a Good Hash Function
- Uses only the input data:
- Should not rely on external factors.
- Uses all of the input data:
- Incorporates the entire dataset.
- Deterministic:
- For the same input, always produces the same output.
- Uniform distribution:
- Evenly distributes hashcodes across the table.
- Minimizes collisions:
- Avoids assigning the same hashcode to different inputs.
Example of a Simple Hash Function
int hash(char *str) {
int sum = 0;
for (int j = 0; str[j] != '\0'; j++) {
sum += str[j]; // Add ASCII values of characters
}
return sum % HASH_MAX; // Ensure the hashcode fits in the array
}
Explanation:
- Adds the ASCII values of all characters in the string.
- Modulo operation ensures the hashcode is within array bounds.
- Limitation: Simple and not robust against similar data (e.g., "John" and "Jonathan").
Collisions and Solutions
Collision: Two pieces of data generate the same hashcode.
- Example:
- Hash
Paul
→6
. - Hash
Ringo
→ also6
.
- Hash
Collision Resolution Methods
-
Linear Probing:
- If the desired index is occupied, check the next available index (e.g.,
hashcode + 1
). - Problem: Clustering—groups of occupied indices grow, increasing collisions.
- If the desired index is occupied, check the next available index (e.g.,
-
Separate Chaining:
- Store multiple elements at the same index using a linked list.
- Each array index points to the head of a linked list.
- Efficient for dynamic growth and avoids clustering.
Implementation of Separate Chaining:
- Each array index is a pointer to the head of a linked list.
- Insert new data:
- Add a node to the linked list at the desired index.
- Lookup:
- Traverse the linked list at the hashcode index.
Example: Separate Chaining
-
Initial State:
- Array of size 10, all elements set to
NULL
.
- Array of size 10, all elements set to
-
Insert
Joey
:- Hash
Joey
→6
. - Create a linked list at index
6
, starting withJoey
.
- Hash
-
Insert
Phoebe
:- Hash
Phoebe
→6
. - Add
Phoebe
to the linked list at index6
.
- Hash
-
Insert
Ross
:- Hash
Ross
→2
. - Create a linked list at index
2
, starting withRoss
.
- Hash
-
Lookup
Joey
:- Hash
Joey
→6
. - Traverse the linked list at index
6
to findJoey
.
- Hash
Advantages of Separate Chaining
- Avoids Clustering:
- Multiple elements can exist at the same hashcode without performance degradation.
- Dynamic Growth:
- Linked lists allow arbitrary growth at each index.
Advantages and Limitations of Hash Tables
Advantages
- Fast Operations:
- Average case for insertion, deletion, and lookup is O(1).
- Flexible Growth:
- Separate chaining avoids fixed size limitations of arrays.
Limitations
- Collisions:
- Although reduced, collisions still occur.
- Inefficient Sorting:
- Hash tables are unsuitable for sorting or ordering data.
- Memory Overhead:
- Requires additional space for pointers (linked lists).
Key Takeaways: Hash Tables
- Hash tables combine arrays and linked lists to achieve efficient data storage and retrieval.
- Separate chaining is an effective method to resolve collisions, leveraging linked lists for dynamic growth.
- Ideal for scenarios where ordering of data is unnecessary and fast access is prioritized.
- For more advanced data structures with faster guarantees, explore tries.
F) Tries in C 🎦
Video: https://youtu.be/MC-iQHFdEDI
Introduction to Tries
- A trie is a tree-like data structure used for efficient insertion, deletion, and lookup of data, especially when working with key-value pairs.
- Key Characteristics:
- Keys are guaranteed to be unique, unlike hash tables where collisions can occur.
- The data structure navigates using the keys, serving as a roadmap to find the data.
- Ideal for applications where quick insertion, deletion, and lookup are prioritized over memory efficiency.
Comparison with Other Data Structures
- Array:
- Key: Index (e.g.,
array[0]
,array[1]
). - Value: Data stored at that index.
- Limitation: Fixed size and lack of dynamic growth.
- Key: Index (e.g.,
- Hash Table:
- Key: Hashcode generated by a hash function.
- Value: Data or linked list stored at the hashcode index.
- Limitation: Collisions require resolution (e.g., chaining or probing).
- Trie:
- Key: Unique roadmap (e.g., a sequence of digits or characters).
- Value: Data associated with the key (e.g.,
true/false
or a string). - No collisions, as keys directly determine unique paths.
Structure of a Trie
- Each node in a trie contains:
- Data: The value associated with the key (e.g., a string like "Harvard").
- Pointers: An array of pointers, each representing a possible next step.
- For numeric keys (0-9), each node has 10 pointers.
- For alphabetical keys (A-Z), each node has 26 pointers.
Node Definition:
typedef struct trie_node {
char *data; // Value associated with the key (e.g., university name)
struct trie_node *next[10]; // Array of 10 pointers for digits 0-9
} trie_node;
Insertion in a Trie
- Start at the Root:
- Begin navigation from the root node.
- Navigate the Key:
- For each digit/character in the key, move to the corresponding branch.
- If the branch doesn’t exist, create a new node.
- Insert Data:
- Once the entire key is processed, store the associated value (e.g., the university name) in the final node.
Example: Insert "Harvard" with key 1636
.
- Start at the root.
- Navigate through
1 → 6 → 3 → 6
, creating nodes where necessary. - At the final node, store
"Harvard"
.
Searching in a Trie
- Start at the Root:
- Use a traversal pointer starting at the root.
- Navigate the Key:
- For each digit/character in the key, follow the corresponding branch.
- If a branch doesn’t exist, the data isn’t in the trie.
- Check Data:
- If the entire key is processed and a node exists, check its data.
Example: Search for "Harvard" with key 1636
.
- Start at the root.
- Navigate through
1 → 6 → 3 → 6
. - Check the final node. If it contains
"Harvard"
, the search is successful.
Efficiency of Tries
- Insertion: Follows the key’s roadmap, creating nodes if necessary. Time complexity depends on the key length, often considered O(1) for fixed-length keys.
- Lookup: Similar to insertion but only follows the roadmap. Also O(1) for fixed-length keys.
- Deletion: Involves clearing the roadmap for a key. Nodes without other branches can be freed.
Tradeoffs of Tries
-
Advantages:
- No collisions: Keys guarantee unique paths.
- Fast operations: Constant time for insertion, lookup, and deletion for fixed-length keys.
- Scalable with existing branches: As more data is added, fewer new nodes are needed.
-
Disadvantages:
- Memory-intensive: Each node contains multiple pointers (e.g., 10 for digits, 26 for letters), even if most are
NULL
. - Complexity: Implementation requires careful management of pointers and memory allocation.
- Memory-intensive: Each node contains multiple pointers (e.g., 10 for digits, 26 for letters), even if most are
Example Use Cases
- Storing Dictionaries:
- Keys: Words (e.g., "bat", "batch").
- Values: Definitions or metadata.
- Mapping Data:
- Keys: Years (e.g.,
1636
for Harvard,1701
for Yale). - Values: Names of universities.
- Keys: Years (e.g.,
Key Takeaways
- Tries are an effective data structure for dynamic and quick operations.
- Ideal when key uniqueness and fast access are critical.
- Memory usage can be a limitation, but the tradeoff is often worthwhile for the performance benefits.
- With tries, we may have found a "holy grail" for certain applications requiring constant time operations.
G) Stacks in C 🎦
Video: https://youtu.be/hVsNqhEthOk
Introduction to Stacks
- Stack: A data structure for maintaining data in a last-in, first-out (LIFO) manner.
- Analogy: Think of a stack of plates where you can only add or remove the top plate.
- Two primary operations:
- Push: Add an element to the top of the stack.
- Pop: Remove the topmost element from the stack.
- Can be implemented using:
- Arrays (fixed size).
- Linked Lists (dynamic size).
Array-Based Stacks
-
Uses a fixed-size array to store elements.
-
Structure definition:
typedef struct { int data[CAPACITY]; // Array to hold elements int top; // Tracks the top of the stack } Stack;
CAPACITY
: Maximum size of the stack, defined elsewhere.top
: Index of the top element.
Operations
Push
- Check if the stack is full (
top == CAPACITY
). - Add the element at
data[top]
. - Increment
top
.
Example:
void push(Stack *s, int value) {
if (s->top == CAPACITY) {
printf("Stack overflow\n");
return;
}
s->data[s->top++] = value;
}
Pop
- Check if the stack is empty (
top == 0
). - Decrement
top
. - Return the element.
Example:
int pop(Stack *s) {
if (s->top == 0) {
printf("Stack underflow\n");
return -1; // Sentinel value
}
return s->data[--s->top];
}
Linked-List-Based Stacks
-
Dynamically allocates memory for each element.
-
Structure definition:
typedef struct Node { int data; struct Node *next; } Node; typedef struct { Node *head; // Pointer to the top of the stack } Stack;
Operations
Push
- Allocate memory for a new node.
- Assign the value to the node.
- Update the
next
pointer to point to the current head. - Move the head to the new node.
Example:
void push(Stack *s, int value) {
Node *newNode = malloc(sizeof(Node));
if (!newNode) {
printf("Memory allocation failed\n");
return;
}
newNode->data = value;
newNode->next = s->head;
s->head = newNode;
}
Pop
- Check if the stack is empty (
head == NULL
). - Save a reference to the current head.
- Update the head to the next node.
- Free the old head.
- Return the data.
Example:
int pop(Stack *s) {
if (!s->head) {
printf("Stack underflow\n");
return -1; // Sentinel value
}
Node *temp = s->head;
int value = temp->data;
s->head = temp->next;
free(temp);
return value;
}
Comparing Implementations
Feature | Array-Based | Linked-List-Based |
---|---|---|
Memory | Fixed size, pre-allocated. | Dynamic size, memory allocated per node. |
Efficiency | Faster due to contiguous memory. | Slower due to pointer operations. |
Flexibility | Limited by initial capacity. | Grows/shrinks as needed. |
Ease of Use | Simpler to implement. | More complex due to memory management. |
Applications of Stacks
- Expression evaluation (e.g., infix to postfix conversion).
- Undo operations in text editors.
- Backtracking algorithms, like solving mazes.
- Function call management in programming.
H) Queues in C 🎦
Video: https://youtu.be/3TmUv1uS92s
Introduction to Queues
- A queue is a data structure for organizing data in a First In, First Out (FIFO) order.
- The first element added to the queue is the first one removed, unlike a stack, which follows Last In, First Out (LIFO).
- Analogy: Similar to a line at a bank or amusement park, where the first person in line is served first.
Key Operations:
- Enqueue: Add an element to the end of the queue.
- Dequeue: Remove an element from the front of the queue.
Implementations of Queues
Queues can be implemented using:
- Arrays
- Linked Lists
Array-Based Queue
Structure Definition
- The queue is represented by:
- An array to store elements.
- An integer to track the front of the queue (index of the oldest element).
- An integer to track the size of the queue (number of elements currently in the queue).
- Capacity is predefined as a constant (e.g.,
CAPACITY = 10
).
Example:
typedef struct {
int data[CAPACITY]; // Array to hold queue elements
int front; // Index of the oldest element
int size; // Number of elements in the queue
} queue;
Enqueue Operation
Purpose: Add an element to the end of the queue.
Steps:
- Check if the queue is full (
size == CAPACITY
). - Place the new element at the position
front + size
(modCAPACITY
for wrapping around). - Increment the
size
of the queue.
Visualization:
- Start with
front = 0
andsize = 0
. - Add elements:
- Enqueue
28
: Place atdata[0]
, incrementsize
to 1. - Enqueue
33
: Place atdata[1]
, incrementsize
to 2. - Enqueue
19
: Place atdata[2]
, incrementsize
to 3.
- Enqueue
Dequeue Operation
Purpose: Remove the oldest element (at the front of the queue).
Steps:
- Check if the queue is empty (
size == 0
). - Retrieve the element at
front
. - Increment
front
(modCAPACITY
for wrapping around). - Decrement the
size
.
Visualization:
- Start with
front = 0
,size = 3
, and elements[28, 33, 19]
. - Dequeue:
- Remove
28
: Updatefront = 1
, decrementsize
to 2. - Remove
33
: Updatefront = 2
, decrementsize
to 1.
- Remove
Linked-List-Based Queue
Structure Definition
- Each node contains:
- Data: The value stored in the node.
- Next pointer: Points to the next node.
- Previous pointer (for doubly linked lists).
Example:
typedef struct node {
int data;
struct node *next;
struct node *prev; // Optional for singly linked lists
} node;
typedef struct {
node *head; // Pointer to the first element
node *tail; // Pointer to the last element
} queue;
Enqueue Operation
Purpose: Add a new element to the end of the queue.
Steps:
- Dynamically allocate memory for the new node.
- Set the node's
data
to the new value. - Update pointers:
- Point the current
tail
’snext
to the new node. - Set the new node’s
prev
to the currenttail
. - Update
tail
to point to the new node.
- Point the current
Visualization:
- Start with
head
pointing to12
andtail
pointing to13
. - Enqueue
10
:- Allocate memory for the new node.
- Set
10.prev
to13
and13.next
to10
. - Update
tail
to point to10
.
Dequeue Operation
Purpose: Remove the oldest element (at the head of the queue).
Steps:
- Retrieve the element at the head.
- Update pointers:
- Move
head
to the next node. - Set the new
head
'sprev
toNULL
.
- Move
- Free the memory of the removed node.
Visualization:
- Start with
head
pointing to12
andtail
pointing to10
. - Dequeue:
- Free the node containing
12
. - Update
head
to point to15
.
- Free the node containing
Comparison of Array vs. Linked List Implementation
Aspect | Array | Linked List |
---|---|---|
Memory Usage | Fixed size (CAPACITY ). |
Dynamically allocated, flexible size. |
Speed | Fast access, potential wraparound. | Slightly slower due to pointer manipulation. |
Ease of Use | Simple for small, static queues. | More complex but scales dynamically. |
Applications of Queues
- Scheduling:
- CPU task scheduling.
- Printer job scheduling.
- Data Streams:
- Buffering in communication protocols.
- Breadth-First Search (BFS):
- Used to explore nodes level by level in graphs or trees.
Queues provide a fair, FIFO-based mechanism for managing data and are critical in a variety of computational tasks.
Z) 🗃️ Glossary
File | Definition |
---|