Rank 02 Deep Dives - kevshouse/exam_quest GitHub Wiki
Deep Dive: Linked List & Matrix Functions Let's visualize key algorithms with Mermaid memory diagrams and optimized implementations:
- Linked List: ft_list_foreach Purpose: Apply function f to every node's data in a linked list.
Memory Diagram: Diagram Code
graph LR
subgraph Heap Memory
A[Node1: data=5, next] --> B[Node2: data=8, next]
B --> C[Node3: data=3, next]
C --> D[NULL]
end
subgraph Stack
HEAD[Head Pointer] --> A
FUNC[f: print_data]
end
subgraph Execution
FUNC -->|Applied to| A.data
FUNC -->|Applied to| B.data
FUNC -->|Applied to| C.data
end
Implementation:
typedef struct s_list {
void *data;
struct s_list *next;
} t_list;
void ft_list_foreach(t_list *begin_list, void (*f)(void *)) {
t_list *current = begin_list;
while (current) {
f(current->data); // Apply function to data
current = current->next; // Move to next node
}
}
// Example usage:
void print_int(void *data) {
printf("%d\n", *(int *)data);
}
int main() {
t_list node3 = {.data = malloc(sizeof(int)), .next = NULL};
*(int *)node3.data = 3;
t_list node2 = {.data = malloc(sizeof(int)), .next = &node3};
*(int *)node2.data = 8;
t_list node1 = {.data = malloc(sizeof(int)), .next = &node2};
*(int *)node1.data = 5;
ft_list_foreach(&node1, print_int);
// Output: 5\n8\n3\n
}
Key Techniques:
Type-agnostic void* data handling
Iterative traversal (safe for large lists)
Function pointer usage (f(void*))
- Matrix: flood_fill (Enhanced) Purpose: Fill connected region in 2D grid starting from (x,y).
Memory Diagram: Diagram Code
graph TB
subgraph Grid Memory
Row0["'1', '1', '0', '1'"]
Row1["'1', '1', '0', '1'"]
Row2["'1', '0', '1', '1'"]
end
subgraph Stack
DIRS[Direction Vectors: 1,0 -1,0 0,1 0,-1]
STACK[Point Stack: 0,0 1,0 0,1 ...]
end
subgraph Execution
STACK -->|Pop| CUR["Current: (0,0)"]
CUR --> CHECK["Check: Within bounds? Color match?"]
CHECK -->|Yes| FILL["Set to 'F'"]
FILL --> PUSH["Push neighbors: (1,0), (0,1)"]
end
Optimized Implementation:
typedef struct s_point { int x; int y; } t_point;
void flood_fill(char **tab, t_point size, t_point begin) {
const t_point dirs[4] = {{1,0}, {-1,0}, {0,1}, {0,-1}};
t_point stack[1000];
int top = -1;
char orig = tab[begin.y][begin.x];
if (orig == 'F' || size.y <= 0 || size.x <= 0) return;
stack[++top] = begin; // Push starting point
while (top >= 0) {
t_point cur = stack[top--];
if (cur.y < 0 || cur.y >= size.y ||
cur.x < 0 || cur.x >= size.x ||
tab[cur.y][cur.x] != orig)
continue;
tab[cur.y][cur.x] = 'F';
for (int i = 0; i < 4; i++) {
t_point next = {cur.x + dirs[i].x, cur.y + dirs[i].y};
if (next.x >= 0 && next.x < size.x &&
next.y >= 0 && next.y < size.y &&
tab[next.y][next.x] == orig) {
stack[++top] = next;
}
}
}
}
Key Optimizations:
Fixed-size stack (no malloc)
Direction vector loop
Boundary checks before stack push
Skip condition before recursion
- Array Sorting: sort_int_tab Purpose: Sort integer array in-place using bubble sort.
Memory Diagram: Diagram Code
graph LR
subgraph Array Memory
A[Before: 5, 2, 8, 3]
B[After: 2, 3, 5, 8]
end
subgraph Process
P1["Pass 1: Compare 5↔2 → Swap"]
P2["Pass 2: Compare 5↔3 → Swap"]
P3["Pass 3: Compare 5↔8 → No swap"]
end
A --> P1 --> P2 --> P3 --> B
Implementation:
void sort_int_tab(int *tab, unsigned int size) {
if (!tab || size < 2) return;
for (unsigned int i = 0; i < size - 1; i++) {
int swapped = 0;
for (unsigned int j = 0; j < size - i - 1; j++) {
if (tab[j] > tab[j+1]) {
// Swap values
int temp = tab[j];
tab[j] = tab[j+1];
tab[j+1] = temp;
swapped = 1;
}
}
if (!swapped) break; // Early exit if sorted
}
}
Key Techniques:
In-place swapping
Early termination when no swaps occur
O(n²) worst-case, optimized for nearly-sorted arrays
- String Processing: epur_str Purpose: Remove extra whitespace from string (" hello world " → "hello world").
State Machine Diagram: Diagram
graph TD
S((Start)) --> A((IN_SPACE))
A -->|Non-space| B(("IN_WORD<br>Write char"))
B -->|Space| A
B -->|Non-space| B
A -->|Space| C(("LEADING_SPACE<br>Skip"))
C -->|Non-space| B
Implementation:
void epur_str(char *str) {
int i = 0, j = 0;
int in_word = 0;
while (str[i]) {
if (str[i] != ' ' && str[i] != '\t') {
if (in_word && str[i-1] == ' ')
str[j++] = ' '; // Add single space between words
str[j++] = str[i];
in_word = 1;
}
else if (in_word) {
in_word = 0; // Mark space after word
}
i++;
}
str[j] = '\0'; // Terminate string
}
Edge Handling:
Leading/trailing spaces removed
Consecutive spaces collapsed
Tabs treated as spaces
- Math Operations: lcm/pgcd Purpose: Compute Least Common Multiple and Greatest Common Divisor.
Diagram Code
graph TD
A[Input a,b] --> B{Compute PGCD}
B --> C["while b ≠ 0:<br>a, b = b, a % b"]
C --> D["PGCD = a"]
D --> E["LCM = |a*b| / PGCD"]
Euclidean Algorithm Implementation:
unsigned int pgcd(unsigned int a, unsigned int b) {
if (a == 0 || b == 0) return 0;
while (b != 0) {
unsigned int temp = b;
b = a % b;
a = temp;
}
return a;
}
unsigned int lcm(unsigned int a, unsigned int b) {
if (a == 0 || b == 0) return 0;
unsigned int gcd = pgcd(a, b);
return (a / gcd) * b; // Avoid overflow
}
Key Notes:
Handle division by zero
Order-independent results
Integer overflow protection in LCM
Purpose: Fill connected regions in a 2D grid starting from a given point, replacing target colors while respecting boundaries.
-
Recursive DFS (Simple but Risky):
void fill(int **grid, int rows, int cols, int r, int c, int target, int newColor) { if (r < 0 || r >= rows || c < 0 || c >= cols) return; // Bounds check if (grid[r][c] != target || grid[r][c] == newColor) return; // Color match grid[r][c] = newColor; // Update color // Recursive 4-directional calls fill(grid, rows, cols, r+1, c, target, newColor); // Down fill(grid, rows, cols, r-1, c, target, newColor); // Up fill(grid, rows, cols, r, c+1, target, newColor); // Right fill(grid, rows, cols, r, c-1, target, newColor); // Left }
Pros: Easy to understand (5–10 lines).
Cons: Stack overflow risk for large grids (>1k cells). -
Iterative BFS/DFS (Recommended for Exams):
typedef struct { int r, c; } Point; void floodFill(int **grid, int rows, int cols, Point start, int newColor) { int target = grid[start.r][start.c]; if (target == newColor) return; Point *stack = malloc(rows * cols * sizeof(Point)); // Static stack int top = -1; stack[++top] = start; while (top >= 0) { Point cur = stack[top--]; if (cur.r < 0 || cur.r >= rows || cur.c < 0 || cur.c >= cols) continue; if (grid[cur.r][cur.c] != target) continue; grid[cur.r][cur.c] = newColor; // Fill // Push neighbors (4-way) stack[++top] = (Point){cur.r+1, cur.c}; // Down stack[++top] = (Point){cur.r-1, cur.c}; // Up stack[++top] = (Point){cur.r, cur.c+1}; // Right stack[++top] = (Point){cur.r, cur.c-1}; // Left } free(stack); }
Pros:
- No stack overflow (handles grids up to 10k cells).
- 3x faster than recursion for complex shapes.
-
Direction Vectors: Simplify neighbor checks:
const int dirs[4][2] = {{1,0}, {-1,0}, {0,1}, {0,-1}}; // Down, Up, Right, Left for (int i = 0; i < 4; i++) { int nr = cur.r + dirs[i][0], nc = cur.c + dirs[i][1]; if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) { // Process neighbor } }
- Span Filling: For large grids, fill entire horizontal spans first, then scan rows above/below for new seeds. Reduces redundant checks by 50%.
-
Single-Cell Grid:
Input: grid = [[5]], start = (0,0), newColor=2 → Output: [[2]]
-
Barrier Test:
Input: grid = [[1,0,1],[0,1,0],[1,0,1]], start=(1,1), newColor=2 → Only center updates.
-
Color Bleed Prevention:
Input: grid = [[1,2],[2,1]], start=(0,0), newColor=3 → Updates only connected 1s, not 2s.
Purpose: Efficiently manipulate dynamic node chains using traversal, insertion, deletion, and transformation.
Operation | Code Snippet (C) | Time | Edge Cases |
---|---|---|---|
Insert at Head | newNode→next=head; head=newNode; |
O(1) | Empty list |
Insert at Tail | while(temp→next) temp=temp→next; temp→next=newNode; |
O(n) | Single-node list |
Delete Node | prev→next=curr→next; free(curr); |
O(n) | Deleting head/tail |
Search | while(curr){if(curr→data==key) return curr;} |
O(n) | Key not found |
graph LR
A[Head] --> B[Node1: data=5]
B --> C[Node2: data=8]
C --> D[Node3: data=3]
D --> E[NULL]
F[Temp Pointer] --> B
F -.-> C -.-> D
Process: Temp pointer moves node-by-node until temp→next == NULL
.
Convert 2D matrix into a 4-pointer doubly linked list (up/down/left/right):
typedef struct Node {
int data;
struct Node *up, *down, *left, *right;
} Node;
Node* buildMatrixList(int **matrix, int rows, int cols) {
Node *nodes[rows][cols];
// Create nodes
for (int i=0; i<rows; i++) {
for (int j=0; j<cols; j++) {
nodes[i][j] = malloc(sizeof(Node));
nodes[i][j]→data = matrix[i][j];
}
}
// Link neighbors
for (int i=0; i<rows; i++) {
for (int j=0; j<cols; j++) {
nodes[i][j]→right = (j+1 < cols) ? nodes[i][j+1] : NULL;
nodes[i][j]→left = (j-1 >= 0) ? nodes[i][j-1] : NULL;
nodes[i][j]→down = (i+1 < rows) ? nodes[i+1][j] : NULL;
nodes[i][j]→up = (i-1 >= 0) ? nodes[i-1][j] : NULL;
}
}
return nodes[0][0]; // Return head
}
Use Case: Sparse matrix storage (e.g., only non-zero elements linked).
-
Memory Leaks: Forgetting to
free()
after deletion → Use Valgrind checks. -
Null Pointer Dereference: Missing
if (head == NULL)
in insertion/deletion. -
Infinite Loops: Incorrectly set pointers in cyclic lists → Always set
tail→next=NULL
.
-
Flood Fill:
- Use iterative BFS for exam safety (no stack overflow).
- Optimize with direction vectors and span filling.
-
Linked Lists:
- Master O(1) head operations and O(n) traversals.
- Practice matrix conversion for spatial problems.
-
Always Test:
- Edge cases: empty grids, single-node lists, color boundaries.
For exam simulators, focus on iterative flood fill and pointer safety in linked lists. Implement all test cases above to avoid Moulinette traps! 🔑