Data Structures and Algorithms Interview Questions with C# Examples
Interview preparation · Technical guide
Data Structures and Algorithms Interview Guide
Questions, explanations, and illustrative examples for interview preparation.
The source combines several notes and C# listings, with repeated numbering and some incomplete implementations. The available material is preserved; snippets and excerpts are not a single compiled application. Complexity depends on the stated representation and assumptions.
10. Data Structure Advantages/Disadvantages
Comprehensive Analysis:
Array
Advantages: - ✅ Fast random access (O(1)) - ✅ Cache-friendly (spatial locality) - ✅ Memory efficient - ✅ Simple implementation - ✅ Predictable performance
Disadvantages: - ❌ Fixed size (in most languages) - ❌ Expensive insertion/deletion in middle - ❌ Memory fragmentation when resizing - ❌ Wasted space if underutilized
Linked List
Advantages: - ✅ Dynamic size - ✅ Efficient insertion/deletion at ends - ✅ No memory waste - ✅ No resizing overhead - ✅ Flexible structure
Disadvantages: - ❌ No random access - ❌ Poor cache performance - ❌ Extra memory for pointers - ❌ More complex implementation
Stack
Advantages: - ✅ Simple and efficient - ✅ O(1) operations - ✅ Memory efficient - ✅ Perfect for LIFO scenarios
Disadvantages: - ❌ Limited access pattern - ❌ No random access - ❌ Fixed order of operations
Queue
Advantages: - ✅ Simple and efficient - ✅ O(1) operations - ✅ Perfect for FIFO scenarios - ✅ Fair processing order
Disadvantages: - ❌ Limited access pattern - ❌ No random access - ❌ Fixed order of operations
Priority Queue
Advantages: - ✅ Priority-based ordering - ✅ Efficient highest-priority access - ✅ Flexible priority system - ✅ Dynamic size
Disadvantages: - ❌ More complex implementation - ❌ Higher memory overhead - ❌ Slower than simple queues
Circular Buffer
Advantages: - ✅ Fixed memory usage - ✅ O(1) operations - ✅ No memory allocation during operation - ✅ Perfect for streaming data
Disadvantages: - ❌ Fixed size - ❌ Data loss when full - ❌ More complex implementation
Selection Guidelines:
Choose Array when: - Random access is needed - Size is known and fixed - Cache performance is critical - Memory efficiency is important
Choose Linked List when: - Frequent insertions/deletions at ends - Size is unknown or variable - Memory fragmentation is a concern - Random access is not needed
Choose Stack when: - LIFO behavior is required - Function call management - Undo/redo functionality - Depth-first traversal
Choose Queue when: - FIFO behavior is required - Task scheduling - Breadth-first traversal - Producer-consumer scenarios
Choose Priority Queue when: - Priority-based processing is needed - Task scheduling with priorities - Network packet prioritization - Event-driven systems
Choose Circular Buffer when: - Fixed memory usage is required - Streaming data processing - Real-time applications - Producer-consumer with bounded buffer
Running the Code
To run the demonstration:
Compile the C# code
csc DataStructuresInterview.cs
Run the executable
./DataStructuresInterview.exe
Performance Considerations
Memory Usage:
- Arrays: Most efficient for data storage
- Linked Lists: Extra overhead for pointers
- Stacks/Queues: Minimal overhead
- Priority Queues: Additional priority storage
Time Complexity Summary:
- Access: Array O(1), Linked List O(n)
- Search: Both O(n)
- Insertion: Array O(n), Linked List O(1) at ends
- Deletion: Array O(n), Linked List O(1) at ends
Cache Performance:
- Arrays: Excellent (spatial locality)
- Linked Lists: Poor (no spatial locality)
- Stacks/Queues: Good (sequential access)
Conclusion
Understanding these fundamental data structures is crucial for: - System Design: Choosing appropriate data structures for different components - Performance Optimization: Selecting structures based on access patterns - Memory Management: Understanding memory implications - Algorithm Design: Building efficient algorithms
1. Array vs Linked List
Key Differences:
Array: - Memory Layout: Contiguous memory allocation - Size: Fixed size (in most languages) - Access: O(1) random access - Insertion/Deletion: O(n) for beginning/middle, O(1) for end - Memory Efficiency: More efficient (no overhead) - Cache Performance: Excellent (spatial locality)
Linked List: - Memory Layout: Non-contiguous (scattered in memory) - Size: Dynamic size - Access: O(n) sequential access - Insertion/Deletion: O(1) for beginning, O(n) for end/middle - Memory Efficiency: Less efficient (pointer overhead) - Cache Performance: Poor (no spatial locality)
When to Use:
Use Array when: - You need random access to elements - Size is known and fixed - Cache performance is critical - Memory efficiency is important
Use Linked List when: - You need frequent insertions/deletions at the beginning - Size is unknown or variable - You don't need random access - Memory fragmentation is a concern
2. Stack Implementation using Array
Implementation Details:
public class ArrayStack<T>
{
private T[] items;
private int top;
private int capacity;
public ArrayStack(int capacity)
{
this.capacity = capacity;
this.items = new T[capacity];
this.top = -1;
}
public void Push(T item) // O(1)
{
if (top == capacity - 1)
throw new InvalidOperationException("Stack overflow");
items[++top] = item;
}
public T Pop() // O(1)
{
if (IsEmpty())
throw new InvalidOperationException("Stack underflow");
return items[top--];
}
public T Peek() // O(1)
{
if (IsEmpty())
throw new InvalidOperationException("Stack is empty");
return items[top];
}
}
Key Features:
- LIFO (Last In, First Out) behavior
- O(1) push, pop, and peek operations
- Fixed capacity with overflow protection
- Memory efficient for known size requirements
Real-world Applications:
- Function call stack
- Undo/redo functionality
- Expression evaluation
- Browser back button
3. Queue Implementation using Linked List
Implementation Details:
public class LinkedListQueue<T>
{
private class Node
{
public T Data;
public Node Next;
public Node(T data)
{
Data = data;
Next = null;
}
}
private Node front, rear;
public void Enqueue(T item) // O(1)
{
Node newNode = new Node(item);
if (rear == null)
{
front = rear = newNode;
return;
}
rear.Next = newNode;
rear = newNode;
}
public T Dequeue() // O(1)
{
if (front == null)
throw new InvalidOperationException("Queue is empty");
T item = front.Data;
front = front.Next;
if (front == null)
rear = null;
return item;
}
}
Key Features:
- FIFO (First In, First Out) behavior
- O(1) enqueue and dequeue operations
- Dynamic size - grows as needed
- Two pointers (front and rear) for efficient operations
Real-world Applications:
- Task scheduling
- Print spooling
- Breadth-first search
- Event handling systems
4. Array Operations Time Complexity
Complete Time Complexity Analysis:
| Operation | Time Complexity | Description |
|---|---|---|
| Access | O(1) | Direct indexing |
| Search | O(n) | Linear search |
| Insertion at End | O(1) | Amortized constant |
| Insertion at Beginning | O(n) | Shift all elements |
| Insertion at Middle | O(n) | Shift elements after insertion point |
| Deletion at End | O(1) | Simple removal |
| Deletion at Beginning | O(n) | Shift all remaining elements |
| Deletion at Middle | O(n) | Shift elements after deletion point |
Memory Complexity:
- Space: O(n) where n is the number of elements
- Overhead: Minimal (just the data itself)
Performance Considerations:
- Cache-friendly: Excellent spatial locality
- Memory allocation: Contiguous block
- Resizing: Expensive operation (O(n))
5. Circular Buffer Implementation
Implementation Details:
public class CircularBuffer<T>
{
private T[] buffer;
private int head, tail, size, capacity;
public CircularBuffer(int capacity)
{
this.capacity = capacity;
this.buffer = new T[capacity];
this.head = this.tail = this.size = 0;
}
public void Enqueue(T item) // O(1)
{
buffer[tail] = item;
tail = (tail + 1) % capacity;
if (size < capacity)
size++;
else
head = (head + 1) % capacity; // Overwrite oldest
}
public T Dequeue() // O(1)
{
if (size == 0)
throw new InvalidOperationException("Buffer is empty");
T item = buffer[head];
head = (head + 1) % capacity;
size--;
return item;
}
}
Key Features:
- Fixed size with automatic overwriting
- O(1) enqueue and dequeue operations
- No memory allocation during operation
- Thread-safe potential (with proper synchronization)
Real-world Applications:
- Audio/video streaming buffers
- Network packet buffering
- Producer-consumer scenarios
- Real-time data processing
6. Singly vs Doubly Linked List
Key Differences:
Singly Linked List:
- Pointers: Each node has only a next pointer
- Memory: Less memory overhead
- Traversal: Forward only
- Deletion: Requires previous node reference
- Implementation: Simpler
Doubly Linked List:
- Pointers: Each node has both next and prev pointers
- Memory: More memory overhead (extra pointer per node)
- Traversal: Both forward and backward
- Deletion: Can delete current node directly
- Implementation: More complex
Implementation Comparison:
// Singly Linked List Node
public class Node
{
public T Data;
public Node Next;
}
// Doubly Linked List Node
public class Node
{
public T Data;
public Node Next, Prev;
}
When to Use:
Use Singly Linked List when: - Memory is constrained - You only need forward traversal - Simplicity is preferred - Deletion operations are infrequent
Use Doubly Linked List when: - You need bidirectional traversal - Frequent deletion operations - You need to maintain order efficiently - Memory overhead is acceptable
7. Priority Queue Implementation
Implementation Details:
public class PriorityQueue<T>
{
private class PriorityItem
{
public T Data;
public int Priority;
public PriorityItem(T data, int priority)
{
Data = data;
Priority = priority;
}
}
private List<PriorityItem> items;
public void Enqueue(T item, int priority) // O(n log n) due to sorting
{
items.Add(new PriorityItem(item, priority));
items.Sort((a, b) => a.Priority.CompareTo(b.Priority));
}
public T Dequeue() // O(1)
{
if (items.Count == 0)
throw new InvalidOperationException("Priority queue is empty");
T item = items[0].Data;
items.RemoveAt(0);
return item;
}
}
Optimized Implementation (using Heap):
// For better performance, use a binary heap implementation
// Enqueue: O(log n), Dequeue: O(log n)
Key Features:
- Priority-based ordering
- Flexible priority system
- Efficient highest-priority access
- Dynamic size
Real-world Applications:
- Task scheduling systems
- Network packet prioritization
- Event-driven systems
- Dijkstra's algorithm
8. Stack vs Queue
Fundamental Differences:
| Aspect | Stack | Queue |
|---|---|---|
| Order | LIFO (Last In, First Out) | FIFO (First In, First Out) |
| Operations | Push, Pop, Peek | Enqueue, Dequeue, Peek |
| Use Cases | Function calls, Undo/Redo | Task scheduling, BFS |
| Implementation | Single end operations | Two end operations |
Visual Representation:
Stack (LIFO):
Push: [1] -> [1,2] -> [1,2,3]
Pop: [1,2,3] -> [1,2] -> [1] -> []
Order: 3, 2, 1
Queue (FIFO):
Enqueue: [1] -> [1,2] -> [1,2,3]
Dequeue: [1,2,3] -> [2,3] -> [3] -> []
Order: 1, 2, 3
When to Use:
Use Stack when: - You need to reverse order - Function call management - Depth-first traversal - Undo/redo functionality
Use Queue when: - You need to preserve order - Task scheduling - Breadth-first traversal - Producer-consumer scenarios
9. Deque Implementation
Implementation Details:
public class Deque<T>
{
private class Node
{
public T Data;
public Node Next, Prev;
public Node(T data)
{
Data = data;
Next = Prev = null;
}
}
private Node front, rear;
public void AddFront(T item) // O(1)
{
Node newNode = new Node(item);
if (front == null)
{
front = rear = newNode;
return;
}
newNode.Next = front;
front.Prev = newNode;
front = newNode;
}
public void AddBack(T item) // O(1)
{
Node newNode = new Node(item);
if (rear == null)
{
front = rear = newNode;
return;
}
newNode.Prev = rear;
rear.Next = newNode;
rear = newNode;
}
public T RemoveFront() // O(1)
{
if (front == null)
throw new InvalidOperationException("Deque is empty");
T item = front.Data;
front = front.Next;
if (front == null)
rear = null;
else
front.Prev = null;
return item;
}
public T RemoveBack() // O(1)
{
if (rear == null)
throw new InvalidOperationException("Deque is empty");
T item = rear.Data;
rear = rear.Prev;
if (rear == null)
front = null;
else
rear.Next = null;
return item;
}
}
Data Structures and Algorithms - Technical Architecture Guide
Overview
This document provides comprehensive explanations of key data structures and algorithms, their differences, implementations, and architectural considerations for technical interviews and system design.
11. Binary Tree vs Binary Search Tree
Binary Tree
- Definition: A hierarchical data structure where each node has at most two children (left and right)
- Properties: No ordering constraints, nodes can be arranged in any way
- Use Cases: Expression trees, syntax trees, file system representation
Binary Search Tree (BST)
- Definition: A binary tree with ordering property: left subtree ≤ root ≤ right subtree
- Properties:
- Left subtree contains only nodes with values less than root
- Right subtree contains only nodes with values greater than root
- Both left and right subtrees must also be BSTs
- Use Cases: Efficient searching, sorting, range queries
Key Differences
| Aspect | Binary Tree | Binary Search Tree |
|---|---|---|
| Ordering | No constraints | Left ≤ Root ≤ Right |
| Search Time | O(n) | O(log n) average |
| Insertion | O(1) | O(log n) average |
| Use Case | General purpose | Ordered data operations |
Architectural Considerations
- BST provides logarithmic time complexity for search/insert/delete operations
- Balanced BSTs (AVL, Red-Black) maintain O(log n) performance in worst case
- Unbalanced BSTs can degenerate to linked lists with O(n) performance
12. Tree Traversal Algorithms
Types of Traversal
1. Inorder Traversal (Left → Root → Right)
// Results in sorted order for BST
// Time: O(n), Space: O(h) where h is height
2. Preorder Traversal (Root → Left → Right)
// Useful for creating copy of tree
// Time: O(n), Space: O(h)
3. Postorder Traversal (Left → Right → Root)
// Useful for deleting tree nodes
// Time: O(n), Space: O(h)
4. Level Order Traversal (Breadth-First)
// Processes nodes level by level
// Time: O(n), Space: O(w) where w is max width
Implementation Strategies
- Recursive: Simple, uses call stack (O(h) space)
- Iterative: Uses explicit stack/queue, more control over space usage
- Morris Traversal: O(1) space, modifies tree temporarily
Architectural Applications
- Expression Evaluation: Postorder traversal
- File System Navigation: Preorder traversal
- Database Indexing: Inorder traversal for sorted output
- Level-based Processing: Breadth-first for hierarchical data
13. Depth-First vs Breadth-First Search
Depth-First Search (DFS)
- Strategy: Explore as far as possible along each branch before backtracking
- Data Structure: Stack (recursive call stack or explicit stack)
- Memory Usage: O(h) where h is maximum depth
- Use Cases:
- Topological sorting
- Cycle detection
- Maze solving
- Backtracking problems
Breadth-First Search (BFS)
- Strategy: Explore all neighbors at current depth before moving to next level
- Data Structure: Queue
- Memory Usage: O(w) where w is maximum width
- Use Cases:
- Shortest path in unweighted graphs
- Web crawling
- Social network connections
- GPS navigation
Comparison
| Aspect | DFS | BFS |
|---|---|---|
| Memory | O(h) | O(w) |
| Shortest Path | Not guaranteed | Guaranteed (unweighted) |
| Completeness | Not guaranteed | Guaranteed |
| Optimality | Not guaranteed | Optimal for unweighted |
Architectural Considerations
- DFS: Better for deep, narrow trees; uses less memory
- BFS: Better for wide, shallow trees; finds shortest path
- Hybrid approaches: Combine both for complex graph problems
14. Balanced Binary Search Tree (AVL Tree)
AVL Tree Properties
- Balance Factor: |height(left) - height(right)| ≤ 1
- Self-Balancing: Automatically maintains balance after insertions/deletions
- Height: O(log n) guaranteed
Balancing Operations
- Left Rotation: Used for right-heavy trees
- Right Rotation: Used for left-heavy trees
- Left-Right Rotation: Double rotation for complex cases
- Right-Left Rotation: Double rotation for complex cases
Implementation Complexity
- Insertion: O(log n) - may require up to 2 rotations
- Deletion: O(log n) - may require multiple rotations
- Search: O(log n) - same as regular BST
Architectural Benefits
- Predictable Performance: O(log n) guaranteed for all operations
- Memory Efficiency: No extra storage for balance information
- Real-time Applications: Suitable for systems requiring consistent performance
Alternatives
- Red-Black Trees: More relaxed balancing, fewer rotations
- B-Trees: Better for disk-based storage
- Splay Trees: Self-adjusting based on access patterns
15. Tree vs Graph
Tree
- Definition: Connected acyclic graph with exactly one path between any two nodes
- Properties:
- n nodes, n-1 edges
- No cycles
- Hierarchical structure
- Single root node
- Use Cases: File systems, organization charts, expression trees
Graph
- Definition: Collection of vertices connected by edges
- Properties:
- Can have cycles
- Multiple paths between nodes
- No hierarchical requirement
- Can be disconnected
- Use Cases: Social networks, road networks, dependency graphs
Key Differences
| Aspect | Tree | Graph |
|---|---|---|
| Cycles | No | Can have |
| Connectivity | Always connected | Can be disconnected |
| Root | Single root | No root concept |
| Paths | Unique path | Multiple paths possible |
| Structure | Hierarchical | Flexible |
Architectural Implications
- Trees: Simpler algorithms, predictable structure
- Graphs: More complex, require cycle detection, connectivity analysis
- Conversion: Trees can be viewed as special case of graphs
16. Graph Traversal Algorithms
Depth-First Search (DFS)
// Recursive implementation
// Time: O(V + E), Space: O(V)
Breadth-First Search (BFS)
// Queue-based implementation
// Time: O(V + E), Space: O(V)
Applications
- DFS:
- Topological sorting
- Strongly connected components
- Cycle detection
- Backtracking
- BFS:
- Shortest path (unweighted)
- Web crawling
- Social network analysis
- GPS navigation
Implementation Considerations
- Adjacency List: Space efficient, good for sparse graphs
- Adjacency Matrix: Fast edge queries, good for dense graphs
- Visited Tracking: Essential for both algorithms to avoid infinite loops
17. Directed vs Undirected Graphs
Directed Graph
- Definition: Edges have direction (from source to destination)
- Properties:
- Asymmetric relationships
- Can represent dependencies
- More complex algorithms
- Use Cases: Task dependencies, web links, social media follows
Undirected Graph
- Definition: Edges have no direction (bidirectional)
- Properties:
- Symmetric relationships
- Simpler algorithms
- Natural for physical connections
- Use Cases: Road networks, social friendships, computer networks
Key Differences
| Aspect | Directed | Undirected |
|---|---|---|
| Edge Direction | Yes | No |
| Adjacency Matrix | Asymmetric | Symmetric |
| Connectivity | More complex | Simpler |
| Algorithms | More complex | Simpler |
Architectural Considerations
- Directed: Model asymmetric relationships, dependencies
- Undirected: Model symmetric relationships, physical connections
- Conversion: Undirected can be viewed as bidirectional directed graph
18. Shortest Path Algorithms
Dijkstra's Algorithm
- Purpose: Find shortest path in weighted graph with non-negative weights
- Data Structure: Priority queue (min-heap)
- Time Complexity: O((V + E) log V) with binary heap
- Space Complexity: O(V)
Bellman-Ford Algorithm
- Purpose: Find shortest path with negative weights (detects negative cycles)
- Time Complexity: O(VE)
- Space Complexity: O(V)
Floyd-Warshall Algorithm
- Purpose: Find shortest paths between all pairs of vertices
- Time Complexity: O(V³)
- Space Complexity: O(V²)
BFS for Unweighted Graphs
- Purpose: Find shortest path in unweighted graph
- Time Complexity: O(V + E)
- Space Complexity: O(V)
Architectural Applications
- Network Routing: Dijkstra's for internet routing
- GPS Navigation: A* algorithm (heuristic-based)
- Social Networks: BFS for connection degrees
- Game AI: Pathfinding algorithms
19. Tree vs Heap
Tree
- Definition: Hierarchical data structure with parent-child relationships
- Properties:
- Flexible structure
- No ordering constraints (except BST)
- Supports complex operations
- Use Cases: File systems, databases, compilers
Heap
- Definition: Complete binary tree with heap property
- Types:
- Min Heap: Parent ≤ children
- Max Heap: Parent ≥ children
- Properties:
- Always complete (filled left to right)
- Heap property maintained
- Efficient priority queue operations
Key Differences
| Aspect | Tree | Heap |
|---|---|---|
| Structure | Flexible | Complete binary |
| Ordering | Optional | Required (heap property) |
| Operations | Various | Priority queue operations |
| Memory | Variable | Compact array representation |
Heap Operations
- Insert: O(log n) - bubble up
- Extract Min/Max: O(log n) - bubble down
- Peek: O(1) - root element
- Build Heap: O(n) - bottom-up construction
Architectural Applications
- Priority Queues: Task scheduling, event processing
- Heap Sort: In-place sorting algorithm
- Memory Management: Dynamic memory allocation
- Graph Algorithms: Dijkstra's, Prim's algorithms
20. Tree Balancing Algorithms
AVL Tree Balancing
- Strategy: Maintain balance factor ≤ 1
- Operations: Single and double rotations
- Complexity: O(log n) for all operations
Red-Black Tree Balancing
- Strategy: Maintain 5 properties through color coding
- Properties: 1. Every node is red or black 2. Root is black 3. Red nodes have black children 4. Every path to leaf has same number of black nodes 5. Leaf nodes are black (null nodes)
- Operations: Color changes and rotations
- Complexity: O(log n) for all operations
Splay Tree Balancing
- Strategy: Move accessed nodes to root
- Operations: Splaying (series of rotations)
- Complexity: Amortized O(log n)
B-Tree Balancing
- Strategy: Maintain minimum/maximum node occupancy
- Operations: Node splitting and merging
- Complexity: O(log n) with large fan-out
Architectural Considerations
- AVL: Strict balancing, more rotations, better for lookup-heavy workloads
- Red-Black: Relaxed balancing, fewer rotations, good for mixed workloads
- Splay: Self-adjusting, good for access patterns with locality
- B-Tree: Optimized for disk storage, used in databases
Performance Comparison Summary
| Data Structure | Search | Insert | Delete | Space |
|---|---|---|---|---|
| Binary Tree | O(n) | O(1) | O(1) | O(n) |
| BST | O(log n) | O(log n) | O(log n) | O(n) |
| AVL Tree | O(log n) | O(log n) | O(log n) | O(n) |
| Red-Black Tree | O(log n) | O(log n) | O(log n) | O(n) |
| Heap | O(n) | O(log n) | O(log n) | O(n) |
| Graph (Adj List) | O(V + E) | O(1) | O(V + E) | O(V + E) |
| Graph (Adj Matrix) | O(1) | O(1) | O(1) | O(V²) |
System Design Considerations
When to Use Each Data Structure
Trees
- BST: Ordered data, range queries, symbol tables
- AVL/Red-Black: When guaranteed O(log n) performance is critical
- B-Tree: Database indexing, file systems
- Trie: String operations, autocomplete
Graphs
- Directed: Dependencies, workflows, social media
- Undirected: Physical networks, social connections
- Weighted: Routing, resource allocation
Heaps
- Priority Queues: Task scheduling, event processing
- Top-K Problems: Finding k largest/smallest elements
- Graph Algorithms: Dijkstra's, Prim's
Scalability Considerations
- Memory Usage: Consider space complexity for large datasets
- Cache Performance: Locality of reference affects performance
- Concurrency: Some structures are easier to make thread-safe
- Persistence: Serialization and storage considerations
Real-World Applications
- Databases: B-trees for indexing
- Compilers: Abstract syntax trees
- Networks: Routing tables, topology representation
- AI/ML: Decision trees, neural network graphs
- Operating Systems: File systems, process trees
using System;
using System.Collections.Generic;
using System.Linq;
namespace DataStructuresAndAlgorithms
{
// Question 11: Binary Tree vs Binary Search Tree
public class BinaryTree<T>
{
public T Data { get; set; }
public BinaryTree<T> Left { get; set; }
public BinaryTree<T> Right { get; set; }
public BinaryTree(T data)
{
Data = data;
Left = null;
Right = null;
}
}
public class BinarySearchTree<T> where T : IComparable<T>
{
public T Data { get; set; }
public BinarySearchTree<T> Left { get; set; }
public BinarySearchTree<T> Right { get; set; }
public BinarySearchTree(T data)
{
Data = data;
Left = null;
Right = null;
}
// BST maintains ordering property
public void Insert(T data)
{
if (data.CompareTo(Data) <= 0)
{
if (Left == null)
Left = new BinarySearchTree<T>(data);
else
Left.Insert(data);
}
else
{
if (Right == null)
Right = new BinarySearchTree<T>(data);
else
Right.Insert(data);
}
}
public bool Search(T data)
{
if (data.CompareTo(Data) == 0)
return true;
if (data.CompareTo(Data) < 0)
return Left?.Search(data) ?? false;
return Right?.Search(data) ?? false;
}
}
// Question 12: Tree Traversal Algorithms
public static class TreeTraversal
{
// Inorder: Left -> Root -> Right
public static void InorderTraversal<T>(BinaryTree<T> root, Action<T> action)
{
if (root == null) return;
InorderTraversal(root.Left, action);
action(root.Data);
InorderTraversal(root.Right, action);
}
// Preorder: Root -> Left -> Right
public static void PreorderTraversal<T>(BinaryTree<T> root, Action<T> action)
{
if (root == null) return;
action(root.Data);
PreorderTraversal(root.Left, action);
PreorderTraversal(root.Right, action);
}
// Postorder: Left -> Right -> Root
public static void PostorderTraversal<T>(BinaryTree<T> root, Action<T> action)
{
if (root == null) return;
PostorderTraversal(root.Left, action);
PostorderTraversal(root.Right, action);
action(root.Data);
}
// Level Order (Breadth-First)
public static void LevelOrderTraversal<T>(BinaryTree<T> root, Action<T> action)
{
if (root == null) return;
var queue = new Queue<BinaryTree<T>>();
queue.Enqueue(root);
while (queue.Count > 0)
{
var current = queue.Dequeue();
action(current.Data);
if (current.Left != null)
queue.Enqueue(current.Left);
if (current.Right != null)
queue.Enqueue(current.Right);
}
}
}
// Question 13: Depth-First vs Breadth-First Search
public static class GraphSearch
{
// Depth-First Search (DFS) - Recursive
public static void DFS<T>(Dictionary<T, List<T>> graph, T start, HashSet<T> visited, Action<T> action)
{
if (visited.Contains(start)) return;
visited.Add(start);
action(start);
foreach (var neighbor in graph[start])
{
DFS(graph, neighbor, visited, action);
}
}
// Depth-First Search (DFS) - Iterative with Stack
public static void DFSIterative<T>(Dictionary<T, List<T>> graph, T start, Action<T> action)
{
var visited = new HashSet<T>();
var stack = new Stack<T>();
stack.Push(start);
while (stack.Count > 0)
{
var current = stack.Pop();
if (visited.Contains(current)) continue;
visited.Add(current);
action(current);
// Push neighbors in reverse order to maintain left-to-right traversal
for (int i = graph[current].Count - 1; i >= 0; i--)
{
stack.Push(graph[current][i]);
}
}
}
// Breadth-First Search (BFS) - Iterative with Queue
public static void BFS<T>(Dictionary<T, List<T>> graph, T start, Action<T> action)
{
var visited = new HashSet<T>();
var queue = new Queue<T>();
queue.Enqueue(start);
visited.Add(start);
while (queue.Count > 0)
{
var current = queue.Dequeue();
action(current);
foreach (var neighbor in graph[current])
{
if (!visited.Contains(neighbor))
{
visited.Add(neighbor);
queue.Enqueue(neighbor);
}
}
}
}
}
// Question 14: Balanced Binary Search Tree (AVL Tree)
public class AVLTree<T> where T : IComparable<T>
{
public T Data { get; set; }
public AVLTree<T> Left { get; set; }
public AVLTree<T> Right { get; set; }
public int Height { get; set; }
public AVLTree(T data)
{
Data = data;
Left = null;
Right = null;
Height = 1;
}
private int GetHeight(AVLTree<T> node)
{
return node?.Height ?? 0;
}
private int GetBalanceFactor(AVLTree<T> node)
{
if (node == null) return 0;
return GetHeight(node.Left) - GetHeight(node.Right);
}
private AVLTree<T> RightRotate(AVLTree<T> y)
{
var x = y.Left;
var T2 = x.Right;
x.Right = y;
y.Left = T2;
y.Height = Math.Max(GetHeight(y.Left), GetHeight(y.Right)) + 1;
x.Height = Math.Max(GetHeight(x.Left), GetHeight(x.Right)) + 1;
return x;
}
private AVLTree<T> LeftRotate(AVLTree<T> x)
{
var y = x.Right;
var T2 = y.Left;
y.Left = x;
x.Right = T2;
x.Height = Math.Max(GetHeight(x.Left), GetHeight(x.Right)) + 1;
y.Height = Math.Max(GetHeight(y.Left), GetHeight(y.Right)) + 1;
return y;
}
public AVLTree<T> Insert(T data)
{
return InsertRecursive(this, data);
}
private AVLTree<T> InsertRecursive(AVLTree<T> node, T data)
{
if (node == null)
return new AVLTree<T>(data);
if (data.CompareTo(node.Data) < 0)
node.Left = InsertRecursive(node.Left, data);
else if (data.CompareTo(node.Data) > 0)
node.Right = InsertRecursive(node.Right, data);
else
return node; // Duplicate keys not allowed
node.Height = Math.Max(GetHeight(node.Left), GetHeight(node.Right)) + 1;
int balance = GetBalanceFactor(node);
// Left Left Case
if (balance > 1 && data.CompareTo(node.Left.Data) < 0)
return RightRotate(node);
// Right Right Case
if (balance < -1 && data.CompareTo(node.Right.Data) > 0)
return LeftRotate(node);
// Left Right Case
if (balance > 1 && data.CompareTo(node.Left.Data) > 0)
{
node.Left = LeftRotate(node.Left);
return RightRotate(node);
}
// Right Left Case
if (balance < -1 && data.CompareTo(node.Right.Data) < 0)
{
node.Right = RightRotate(node.Right);
return LeftRotate(node);
}
return node;
}
}
// Question 15: Tree vs Graph
public class Graph<T>
{
public Dictionary<T, List<T>> AdjacencyList { get; set; }
public Graph()
{
AdjacencyList = new Dictionary<T, List<T>>();
}
public void AddVertex(T vertex)
{
if (!AdjacencyList.ContainsKey(vertex))
AdjacencyList[vertex] = new List<T>();
}
public void AddEdge(T source, T destination)
{
if (!AdjacencyList.ContainsKey(source))
AdjacencyList[source] = new List<T>();
if (!AdjacencyList.ContainsKey(destination))
AdjacencyList[destination] = new List<T>();
AdjacencyList[source].Add(destination);
// For undirected graph, also add reverse edge
// AdjacencyList[destination].Add(source);
}
// Question 16: Graph Traversal Algorithms
public void DFS(T start, Action<T> action)
{
var visited = new HashSet<T>();
DFSRecursive(start, visited, action);
}
private void DFSRecursive(T vertex, HashSet<T> visited, Action<T> action)
{
if (visited.Contains(vertex)) return;
visited.Add(vertex);
action(vertex);
foreach (var neighbor in AdjacencyList[vertex])
{
DFSRecursive(neighbor, visited, action);
}
}
public void BFS(T start, Action<T> action)
{
var visited = new HashSet<T>();
var queue = new Queue<T>();
queue.Enqueue(start);
visited.Add(start);
while (queue.Count > 0)
{
var current = queue.Dequeue();
action(current);
foreach (var neighbor in AdjacencyList[current])
{
if (!visited.Contains(neighbor))
{
visited.Add(neighbor);
queue.Enqueue(neighbor);
}
}
}
}
}
// Question 17: Directed vs Undirected Graphs
public class DirectedGraph<T> : Graph<T>
{
// In directed graph, edges have direction
public void AddDirectedEdge(T source, T destination)
{
AddEdge(source, destination);
// Note: No reverse edge added
}
}
public class UndirectedGraph<T> : Graph<T>
{
// In undirected graph, edges are bidirectional
public void AddUndirectedEdge(T source, T destination)
{
AddEdge(source, destination);
AdjacencyList[destination].Add(source); // Add reverse edge
}
}
// Question 18: Shortest Path Algorithms
public static class ShortestPath
{
// Dijkstra's Algorithm for weighted graphs
public static Dictionary<T, int> Dijkstra<T>(Dictionary<T, List<(T neighbor, int weight)>> graph, T start)
{
var distances = new Dictionary<T, int>();
var visited = new HashSet<T>();
var priorityQueue = new SortedSet<(int distance, T vertex)>();
// Initialize distances
foreach (var vertex in graph.Keys)
{
distances[vertex] = int.MaxValue;
}
distances[start] = 0;
priorityQueue.Add((0, start));
while (priorityQueue.Count > 0)
{
var (currentDistance, currentVertex) = priorityQueue.Min;
priorityQueue.Remove((currentDistance, currentVertex));
if (visited.Contains(currentVertex)) continue;
visited.Add(currentVertex);
foreach (var (neighbor, weight) in graph[currentVertex])
{
var newDistance = currentDistance + weight;
if (newDistance < distances[neighbor])
{
distances[neighbor] = newDistance;
priorityQueue.Add((newDistance, neighbor));
}
}
}
return distances;
}
// BFS for unweighted graphs (shortest path)
public static Dictionary<T, int> BFSShortestPath<T>(Dictionary<T, List<T>> graph, T start)
{
var distances = new Dictionary<T, int>();
var queue = new Queue<T>();
var visited = new HashSet<T>();
foreach (var vertex in graph.Keys)
{
distances[vertex] = -1; // -1 indicates unreachable
}
distances[start] = 0;
queue.Enqueue(start);
visited.Add(start);
while (queue.Count > 0)
{
var current = queue.Dequeue();
foreach (var neighbor in graph[current])
{
if (!visited.Contains(neighbor))
{
visited.Add(neighbor);
distances[neighbor] = distances[current] + 1;
queue.Enqueue(neighbor);
}
}
}
return distances;
}
}
// Question 19: Tree vs Heap
public class MinHeap<T> where T : IComparable<T>
{
private List<T> heap;
public MinHeap()
{
heap = new List<T>();
}
public int Count => heap.Count;
public void Insert(T item)
{
heap.Add(item);
HeapifyUp(heap.Count - 1);
}
public T ExtractMin()
{
if (heap.Count == 0)
throw new InvalidOperationException("Heap is empty");
var min = heap[0];
heap[0] = heap[heap.Count - 1];
heap.RemoveAt(heap.Count - 1);
if (heap.Count > 0)
HeapifyDown(0);
return min;
}
private void HeapifyUp(int index)
{
var parent = (index - 1) / 2;
if (parent >= 0 && heap[index].CompareTo(heap[parent]) < 0)
{
Swap(index, parent);
HeapifyUp(parent);
}
}
private void HeapifyDown(int index)
{
var smallest = index;
var left = 2 * index + 1;
var right = 2 * index + 2;
if (left < heap.Count && heap[left].CompareTo(heap[smallest]) < 0)
smallest = left;
if (right < heap.Count && heap[right].CompareTo(heap[smallest]) < 0)
smallest = right;
if (smallest != index)
{
Swap(index, smallest);
HeapifyDown(smallest);
}
}
private void Swap(int i, int j)
{
var temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
}
// Question 20: Tree Balancing Algorithms
public static class TreeBalancing
{
// Red-Black Tree implementation
public class RedBlackTree<T> where T : IComparable<T>
{
public enum Color { Red, Black }
public class Node
{
public T Data { get; set; }
public Node Left { get; set; }
public Node Right { get; set; }
public Node Parent { get; set; }
public Color NodeColor { get; set; }
public Node(T data)
{
Data = data;
NodeColor = Color.Red;
}
}
public Node Root { get; set; }
public void Insert(T data)
{
var newNode = new Node(data);
Root = InsertRecursive(Root, newNode);
FixRedBlackProperties(newNode);
}
private Node InsertRecursive(Node root, Node newNode)
{
if (root == null)
return newNode;
if (newNode.Data.CompareTo(root.Data) < 0)
{
root.Left = InsertRecursive(root.Left, newNode);
root.Left.Parent = root;
}
else if (newNode.Data.CompareTo(root.Data) > 0)
{
root.Right = InsertRecursive(root.Right, newNode);
root.Right.Parent = root;
}
return root;
}
private void FixRedBlackProperties(Node node)
{
Node parent = null;
Node grandparent = null;
while (node != Root && node.NodeColor == Color.Red && node.Parent.NodeColor == Color.Red)
{
parent = node.Parent;
grandparent = parent.Parent;
if (parent == grandparent.Left)
{
Node uncle = grandparent.Right;
if (uncle != null && uncle.NodeColor == Color.Red)
{
grandparent.NodeColor = Color.Red;
parent.NodeColor = Color.Black;
uncle.NodeColor = Color.Black;
node = grandparent;
}
else
{
if (node == parent.Right)
{
RotateLeft(parent);
node = parent;
parent = node.Parent;
}
RotateRight(grandparent);
SwapColors(parent, grandparent);
node = parent;
}
}
else
{
Node uncle = grandparent.Left;
if (uncle != null && uncle.NodeColor == Color.Red)
{
grandparent.NodeColor = Color.Red;
parent.NodeColor = Color.Black;
uncle.NodeColor = Color.Black;
node = grandparent;
}
else
{
if (node == parent.Left)
{
RotateRight(parent);
node = parent;
parent = node.Parent;
}
RotateLeft(grandparent);
SwapColors(parent, grandparent);
node = parent;
}
}
}
Root.NodeColor = Color.Black;
}
private void RotateLeft(Node node)
{
Node rightChild = node.Right;
node.Right = rightChild.Left;
if (rightChild.Left != null)
rightChild.Left.Parent = node;
rightChild.Parent = node.Parent;
if (node.Parent == null)
Root = rightChild;
else if (node == node.Parent.Left)
node.Parent.Left = rightChild;
else
node.Parent.Right = rightChild;
rightChild.Left = node;
node.Parent = rightChild;
}
private void RotateRight(Node node)
{
Node leftChild = node.Left;
node.Left = leftChild.Right;
if (leftChild.Right != null)
leftChild.Right.Parent = node;
leftChild.Parent = node.Parent;
if (node.Parent == null)
Root = leftChild;
else if (node == node.Parent.Right)
node.Parent.Right = leftChild;
else
node.Parent.Left = leftChild;
leftChild.Right = node;
node.Parent = leftChild;
}
private void SwapColors(Node node1, Node node2)
{
var temp = node1.NodeColor;
node1.NodeColor = node2.NodeColor;
node2.NodeColor = temp;
}
}
}
// Example usage and demonstration
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Data Structures and Algorithms Examples\n");
// Question 11: Binary Tree vs Binary Search Tree
Console.WriteLine("11. Binary Tree vs Binary Search Tree:");
var bst = new BinarySearchTree<int>(10);
bst.Insert(5);
bst.Insert(15);
bst.Insert(3);
bst.Insert(7);
Console.WriteLine($"Search for 7: {bst.Search(7)}");
Console.WriteLine($"Search for 20: {bst.Search(20)}\n");
// Question 12: Tree Traversal
Console.WriteLine("12. Tree Traversal Algorithms:");
var tree = new BinaryTree<int>(1);
tree.Left = new BinaryTree<int>(2);
tree.Right = new BinaryTree<int>(3);
tree.Left.Left = new BinaryTree<int>(4);
tree.Left.Right = new BinaryTree<int>(5);
Console.Write("Inorder: ");
TreeTraversal.InorderTraversal(tree, x => Console.Write(x + " "));
Console.WriteLine();
Console.Write("Preorder: ");
TreeTraversal.PreorderTraversal(tree, x => Console.Write(x + " "));
Console.WriteLine();
Console.Write("Postorder: ");
TreeTraversal.PostorderTraversal(tree, x => Console.Write(x + " "));
Console.WriteLine();
Console.Write("Level Order: ");
TreeTraversal.LevelOrderTraversal(tree, x => Console.Write(x + " "));
Console.WriteLine("\n");
// Question 13: DFS vs BFS
Console.WriteLine("13. Depth-First vs Breadth-First Search:");
var graph = new Dictionary<int, List<int>>
{
{1, new List<int> {2, 3}},
{2, new List<int> {4, 5}},
{3, new List<int> {6, 7}},
{4, new List<int> {}},
{5, new List<int> {}},
{6, new List<int> {}},
{7, new List<int> {}}
};
Console.Write("DFS: ");
GraphSearch.DFS(graph, 1, new HashSet<int>(), x => Console.Write(x + " "));
Console.WriteLine();
Console.Write("BFS: ");
GraphSearch.BFS(graph, 1, x => Console.Write(x + " "));
Console.WriteLine("\n");
// Question 14: AVL Tree
Console.WriteLine("14. Balanced Binary Search Tree (AVL):");
var avlTree = new AVLTree<int>(10);
avlTree = avlTree.Insert(20);
avlTree = avlTree.Insert(30);
avlTree = avlTree.Insert(40);
avlTree = avlTree.Insert(50);
avlTree = avlTree.Insert(25);
Console.WriteLine("AVL Tree created with balancing\n");
// Question 15-16: Graph Traversal
Console.WriteLine("15-16. Graph vs Tree and Graph Traversal:");
var graphObj = new Graph<int>();
graphObj.AddEdge(0, 1);
graphObj.AddEdge(0, 2);
graphObj.AddEdge(1, 3);
graphObj.AddEdge(2, 3);
Console.Write("Graph DFS: ");
graphObj.DFS(0, x => Console.Write(x + " "));
Console.WriteLine();
Console.Write("Graph BFS: ");
graphObj.BFS(0, x => Console.Write(x + " "));
Console.WriteLine("\n");
// Question 17: Directed vs Undirected
Console.WriteLine("17. Directed vs Undirected Graphs:");
var directedGraph = new DirectedGraph<int>();
directedGraph.AddDirectedEdge(0, 1);
directedGraph.AddDirectedEdge(1, 2);
// Edge only goes from 0->1, not 1->0
var undirectedGraph = new UndirectedGraph<int>();
undirectedGraph.AddUndirectedEdge(0, 1);
undirectedGraph.AddUndirectedEdge(1, 2);
// Edge goes both ways: 0<->1, 1<->2
Console.WriteLine("Directed and Undirected graphs created\n");
// Question 18: Shortest Path
Console.WriteLine("18. Shortest Path Algorithms:");
var weightedGraph = new Dictionary<int, List<(int neighbor, int weight)>>
{
{0, new List<(int, int)> {(1, 4), (2, 2)}},
{1, new List<(int, int)> {(2, 1), (3, 5)}},
{2, new List<(int, int)> {(3, 8)}},
{3, new List<(int, int)> {}}
};
var distances = ShortestPath.Dijkstra(weightedGraph, 0);
Console.WriteLine("Dijkstra's shortest paths from node 0:");
foreach (var kvp in distances)
{
Console.WriteLine($"To {kvp.Key}: {kvp.Value}");
}
Console.WriteLine();
// Question 19: Heap
Console.WriteLine("19. Tree vs Heap:");
var minHeap = new MinHeap<int>();
minHeap.Insert(10);
minHeap.Insert(4);
minHeap.Insert(15);
minHeap.Insert(20);
minHeap.Insert(30);
Console.Write("Min Heap extraction: ");
while (minHeap.Count > 0)
{
Console.Write(minHeap.ExtractMin() + " ");
}
Console.WriteLine("\n");
// Question 20: Tree Balancing
Console.WriteLine("20. Tree Balancing Algorithms:");
var redBlackTree = new TreeBalancing.RedBlackTree<int>();
redBlackTree.Insert(7);
redBlackTree.Insert(3);
redBlackTree.Insert(18);
redBlackTree.Insert(10);
redBlackTree.Insert(22);
redBlackTree.Insert(8);
redBlackTree.Insert(11);
redBlackTree.Insert(26);
redBlackTree.Insert(2);
redBlackTree.Insert(6);
redBlackTree.Insert(13);
Console.WriteLine("Red-Black Tree created with automatic balancing\n");
Console.WriteLine("All data structures and algorithms demonstrated successfully!");
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
namespace DataStructuresInterview
{
// 21. Hash Table Implementation
public class HashTable<TKey, TValue>
{
private class HashNode
{
public TKey Key { get; set; }
public TValue Value { get; set; }
public HashNode Next { get; set; }
public HashNode(TKey key, TValue value)
{
Key = key;
Value = value;
Next = null;
}
}
private HashNode[] buckets;
private int size;
private int capacity;
private const double LoadFactor = 0.75;
public HashTable(int initialCapacity = 16)
{
capacity = initialCapacity;
buckets = new HashNode[capacity];
size = 0;
}
private int GetHash(TKey key)
{
return Math.Abs(key.GetHashCode()) % capacity;
}
public void Put(TKey key, TValue value)
{
if (key == null) throw new ArgumentNullException(nameof(key));
int index = GetHash(key);
HashNode current = buckets[index];
// Check if key already exists
while (current != null)
{
if (current.Key.Equals(key))
{
current.Value = value; // Update existing
return;
}
current = current.Next;
}
// Add new node
HashNode newNode = new HashNode(key, value);
newNode.Next = buckets[index];
buckets[index] = newNode;
size++;
// Resize if needed
if ((double)size / capacity >= LoadFactor)
{
Resize();
}
}
public TValue Get(TKey key)
{
if (key == null) throw new ArgumentNullException(nameof(key));
int index = GetHash(key);
HashNode current = buckets[index];
while (current != null)
{
if (current.Key.Equals(key))
return current.Value;
current = current.Next;
}
throw new KeyNotFoundException($"Key '{key}' not found");
}
public bool ContainsKey(TKey key)
{
if (key == null) return false;
int index = GetHash(key);
HashNode current = buckets[index];
while (current != null)
{
if (current.Key.Equals(key))
return true;
current = current.Next;
}
return false;
}
private void Resize()
{
capacity *= 2;
HashNode[] oldBuckets = buckets;
buckets = new HashNode[capacity];
size = 0;
foreach (var bucket in oldBuckets)
{
HashNode current = bucket;
while (current != null)
{
Put(current.Key, current.Value);
current = current.Next;
}
}
}
}
// 22. Hash Set Implementation (Difference from Hash Table)
public class HashSet<T>
{
private bool[] buckets;
private T[] keys;
private int size;
private int capacity;
private const double LoadFactor = 0.75;
public HashSet(int initialCapacity = 16)
{
capacity = initialCapacity;
buckets = new bool[capacity];
keys = new T[capacity];
size = 0;
}
private int GetHash(T item)
{
return Math.Abs(item.GetHashCode()) % capacity;
}
public void Add(T item)
{
if (item == null) throw new ArgumentNullException(nameof(item));
int index = GetHash(item);
int originalIndex = index;
// Linear probing for collision resolution
do
{
if (!buckets[index])
{
buckets[index] = true;
keys[index] = item;
size++;
break;
}
else if (keys[index].Equals(item))
{
return; // Already exists
}
index = (index + 1) % capacity;
} while (index != originalIndex);
// Resize if needed
if ((double)size / capacity >= LoadFactor)
{
Resize();
}
}
public bool Contains(T item)
{
if (item == null) return false;
int index = GetHash(item);
int originalIndex = index;
do
{
if (!buckets[index])
return false;
if (keys[index].Equals(item))
return true;
index = (index + 1) % capacity;
} while (index != originalIndex);
return false;
}
private void Resize()
{
capacity *= 2;
bool[] oldBuckets = buckets;
T[] oldKeys = keys;
buckets = new bool[capacity];
keys = new T[capacity];
size = 0;
for (int i = 0; i < oldBuckets.Length; i++)
{
if (oldBuckets[i])
{
Add(oldKeys[i]);
}
}
}
}
// 23. Trie (Prefix Tree) Implementation
public class Trie
{
private class TrieNode
{
public Dictionary<char, TrieNode> Children { get; set; }
public bool IsEndOfWord { get; set; }
public TrieNode()
{
Children = new Dictionary<char, TrieNode>();
IsEndOfWord = false;
}
}
private TrieNode root;
public Trie()
{
root = new TrieNode();
}
public void Insert(string word)
{
if (string.IsNullOrEmpty(word)) return;
TrieNode current = root;
foreach (char c in word)
{
if (!current.Children.ContainsKey(c))
{
current.Children[c] = new TrieNode();
}
current = current.Children[c];
}
current.IsEndOfWord = true;
}
public bool Search(string word)
{
if (string.IsNullOrEmpty(word)) return false;
TrieNode current = root;
foreach (char c in word)
{
if (!current.Children.ContainsKey(c))
return false;
current = current.Children[c];
}
return current.IsEndOfWord;
}
public bool StartsWith(string prefix)
{
if (string.IsNullOrEmpty(prefix)) return false;
TrieNode current = root;
foreach (char c in prefix)
{
if (!current.Children.ContainsKey(c))
return false;
current = current.Children[c];
}
return true;
}
public List<string> GetAllWordsWithPrefix(string prefix)
{
var result = new List<string>();
if (string.IsNullOrEmpty(prefix)) return result;
TrieNode current = root;
foreach (char c in prefix)
{
if (!current.Children.ContainsKey(c))
return result;
current = current.Children[c];
}
CollectWords(current, prefix, result);
return result;
}
private void CollectWords(TrieNode node, string currentWord, List<string> result)
{
if (node.IsEndOfWord)
result.Add(currentWord);
foreach (var kvp in node.Children)
{
CollectWords(kvp.Value, currentWord + kvp.Key, result);
}
}
}
// 24. Heap Implementation (Min Heap)
public class MinHeap<T> where T : IComparable<T>
{
private List<T> heap;
public MinHeap()
{
heap = new List<T>();
}
public int Count => heap.Count;
public void Insert(T item)
{
heap.Add(item);
HeapifyUp(heap.Count - 1);
}
public T ExtractMin()
{
if (heap.Count == 0)
throw new InvalidOperationException("Heap is empty");
T min = heap[0];
heap[0] = heap[heap.Count - 1];
heap.RemoveAt(heap.Count - 1);
if (heap.Count > 0)
HeapifyDown(0);
return min;
}
public T Peek()
{
if (heap.Count == 0)
throw new InvalidOperationException("Heap is empty");
return heap[0];
}
private void HeapifyUp(int index)
{
int parent = (index - 1) / 2;
while (index > 0 && heap[index].CompareTo(heap[parent]) < 0)
{
Swap(index, parent);
index = parent;
parent = (index - 1) / 2;
}
}
private void HeapifyDown(int index)
{
int smallest = index;
int leftChild = 2 * index + 1;
int rightChild = 2 * index + 2;
if (leftChild < heap.Count && heap[leftChild].CompareTo(heap[smallest]) < 0)
smallest = leftChild;
if (rightChild < heap.Count && heap[rightChild].CompareTo(heap[smallest]) < 0)
smallest = rightChild;
if (smallest != index)
{
Swap(index, smallest);
HeapifyDown(smallest);
}
}
private void Swap(int i, int j)
{
T temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
}
// Priority Queue Implementation
public class PriorityQueue<T> where T : IComparable<T>
{
private MinHeap<T> heap;
public PriorityQueue()
{
heap = new MinHeap<T>();
}
public void Enqueue(T item)
{
heap.Insert(item);
}
public T Dequeue()
{
return heap.ExtractMin();
}
public T Peek()
{
return heap.Peek();
}
public int Count => heap.Count;
}
// 25. Skip List Implementation
public class SkipList<T> where T : IComparable<T>
{
private class SkipListNode
{
public T Value { get; set; }
public SkipListNode[] Forward { get; set; }
public SkipListNode(T value, int level)
{
Value = value;
Forward = new SkipListNode[level + 1];
}
}
private SkipListNode header;
private int maxLevel;
private int currentLevel;
private Random random;
public SkipList()
{
maxLevel = 16;
currentLevel = 0;
header = new SkipListNode(default(T), maxLevel);
random = new Random();
}
private int RandomLevel()
{
int level = 0;
while (random.Next(2) == 1 && level < maxLevel)
level++;
return level;
}
public void Insert(T value)
{
SkipListNode[] update = new SkipListNode[maxLevel + 1];
SkipListNode current = header;
// Find the position to insert
for (int i = currentLevel; i >= 0; i--)
{
while (current.Forward[i] != null && current.Forward[i].Value.CompareTo(value) < 0)
current = current.Forward[i];
update[i] = current;
}
current = current.Forward[0];
// If value doesn't exist, insert it
if (current == null || !current.Value.Equals(value))
{
int newLevel = RandomLevel();
if (newLevel > currentLevel)
{
for (int i = currentLevel + 1; i <= newLevel; i++)
update[i] = header;
currentLevel = newLevel;
}
SkipListNode newNode = new SkipListNode(value, newLevel);
for (int i = 0; i <= newLevel; i++)
{
newNode.Forward[i] = update[i].Forward[i];
update[i].Forward[i] = newNode;
}
}
}
public bool Search(T value)
{
SkipListNode current = header;
for (int i = currentLevel; i >= 0; i--)
{
while (current.Forward[i] != null && current.Forward[i].Value.CompareTo(value) < 0)
current = current.Forward[i];
}
current = current.Forward[0];
return current != null && current.Value.Equals(value);
}
public void Delete(T value)
{
SkipListNode[] update = new SkipListNode[maxLevel + 1];
SkipListNode current = header;
for (int i = currentLevel; i >= 0; i--)
{
while (current.Forward[i] != null && current.Forward[i].Value.CompareTo(value) < 0)
current = current.Forward[i];
update[i] = current;
}
current = current.Forward[0];
if (current != null && current.Value.Equals(value))
{
for (int i = 0; i <= currentLevel; i++)
{
if (update[i].Forward[i] != current)
break;
update[i].Forward[i] = current.Forward[i];
}
while (currentLevel > 0 && header.Forward[currentLevel] == null)
currentLevel--;
}
}
}
// 26. B-Tree Implementation (Simplified)
public class BTreeNode<T> where T : IComparable<T>
{
public List<T> Keys { get; set; }
public List<BTreeNode<T>> Children { get; set; }
public bool IsLeaf { get; set; }
public BTreeNode(bool isLeaf)
{
Keys = new List<T>();
Children = new List<BTreeNode<T>>();
IsLeaf = isLeaf;
}
}
public class BTree<T> where T : IComparable<T>
{
private BTreeNode<T> root;
private int t; // Minimum degree
public BTree(int minimumDegree)
{
root = new BTreeNode<T>(true);
t = minimumDegree;
}
public void Insert(T key)
{
BTreeNode<T> r = root;
if (r.Keys.Count == 2 * t - 1)
{
BTreeNode<T> s = new BTreeNode<T>(false);
root = s;
s.Children.Add(r);
SplitChild(s, 0);
InsertNonFull(s, key);
}
else
{
InsertNonFull(r, key);
}
}
private void InsertNonFull(BTreeNode<T> node, T key)
{
int i = node.Keys.Count - 1;
if (node.IsLeaf)
{
while (i >= 0 && key.CompareTo(node.Keys[i]) < 0)
{
node.Keys.Insert(i + 1, node.Keys[i]);
i--;
}
node.Keys.Insert(i + 1, key);
}
else
{
while (i >= 0 && key.CompareTo(node.Keys[i]) < 0)
i--;
i++;
if (node.Children[i].Keys.Count == 2 * t - 1)
{
SplitChild(node, i);
if (key.CompareTo(node.Keys[i]) > 0)
i++;
}
InsertNonFull(node.Children[i], key);
}
}
private void SplitChild(BTreeNode<T> parent, int childIndex)
{
BTreeNode<T> child = parent.Children[childIndex];
BTreeNode<T> newNode = new BTreeNode<T>(child.IsLeaf);
parent.Keys.Insert(childIndex, child.Keys[t - 1]);
parent.Children.Insert(childIndex + 1, newNode);
for (int j = 0; j < t - 1; j++)
{
newNode.Keys.Add(child.Keys[j + t]);
}
if (!child.IsLeaf)
{
for (int j = 0; j < t; j++)
{
newNode.Children.Add(child.Children[j + t]);
}
}
child.Keys.RemoveRange(t - 1, t);
if (!child.IsLeaf)
{
child.Children.RemoveRange(t, t);
}
}
public bool Search(T key)
{
return SearchNode(root, key) != null;
}
private BTreeNode<T> SearchNode(BTreeNode<T> node, T key)
{
int i = 0;
while (i < node.Keys.Count && key.CompareTo(node.Keys[i]) > 0)
i++;
if (i < node.Keys.Count && key.Equals(node.Keys[i]))
return node;
if (node.IsLeaf)
return null;
return SearchNode(node.Children[i], key);
}
}
// 27. Segment Tree Implementation
public class SegmentTree
{
private int[] tree;
private int[] data;
private int n;
public SegmentTree(int[] arr)
{
data = arr;
n = arr.Length;
tree = new int[4 * n];
Build(0, 0, n - 1);
}
private void Build(int node, int start, int end)
{
if (start == end)
{
tree[node] = data[start];
return;
}
int mid = (start + end) / 2;
Build(2 * node + 1, start, mid);
Build(2 * node + 2, mid + 1, end);
tree[node] = tree[2 * node + 1] + tree[2 * node + 2];
}
public void Update(int index, int value)
{
UpdateNode(0, 0, n - 1, index, value);
}
private void UpdateNode(int node, int start, int end, int index, int value)
{
if (start == end)
{
data[index] = value;
tree[node] = value;
return;
}
int mid = (start + end) / 2;
if (index <= mid)
UpdateNode(2 * node + 1, start, mid, index, value);
else
UpdateNode(2 * node + 2, mid + 1, end, index, value);
tree[node] = tree[2 * node + 1] + tree[2 * node + 2];
}
public int Query(int left, int right)
{
return QueryNode(0, 0, n - 1, left, right);
}
private int QueryNode(int node, int start, int end, int left, int right)
{
if (right < start || left > end)
return 0;
if (left <= start && right >= end)
return tree[node];
int mid = (start + end) / 2;
int leftSum = QueryNode(2 * node + 1, start, mid, left, right);
int rightSum = QueryNode(2 * node + 2, mid + 1, end, left, right);
return leftSum + rightSum;
}
}
// 28. Union-Find (Disjoint Set) Implementation
public class UnionFind
{
private int[] parent;
private int[] rank;
private int count;
public UnionFind(int size)
{
parent = new int[size];
rank = new int[size];
count = size;
for (int i = 0; i < size; i++)
{
parent[i] = i;
rank[i] = 0;
}
}
public int Find(int x)
{
if (parent[x] != x)
{
parent[x] = Find(parent[x]); // Path compression
}
return parent[x];
}
public void Union(int x, int y)
{
int rootX = Find(x);
int rootY = Find(y);
if (rootX == rootY) return;
// Union by rank
if (rank[rootX] < rank[rootY])
{
parent[rootX] = rootY;
}
else if (rank[rootX] > rank[rootY])
{
parent[rootY] = rootX;
}
else
{
parent[rootY] = rootX;
rank[rootX]++;
}
count--;
}
public bool Connected(int x, int y)
{
return Find(x) == Find(y);
}
public int Count => count;
}
// 29. Suffix Tree Implementation (Simplified)
public class SuffixTreeNode
{
public Dictionary<char, SuffixTreeNode> Children { get; set; }
public int Start { get; set; }
public int End { get; set; }
public SuffixTreeNode SuffixLink { get; set; }
public SuffixTreeNode(int start, int end)
{
Children = new Dictionary<char, SuffixTreeNode>();
Start = start;
End = end;
SuffixLink = null;
}
}
public class SuffixTree
{
private SuffixTreeNode root;
private string text;
public SuffixTree(string input)
{
text = input + "$"; // Add sentinel
root = new SuffixTreeNode(-1, -1);
BuildSuffixTree();
}
private void BuildSuffixTree()
{
SuffixTreeNode activeNode = root;
int activeEdge = -1;
int activeLength = 0;
int remainingSuffixCount = 0;
SuffixTreeNode lastNewNode = null;
for (int i = 0; i < text.Length; i++)
{
char currentChar = text[i];
remainingSuffixCount++;
while (remainingSuffixCount > 0)
{
if (activeLength == 0)
activeEdge = i;
if (!activeNode.Children.ContainsKey(text[activeEdge]))
{
activeNode.Children[text[activeEdge]] = new SuffixTreeNode(i, text.Length - 1);
remainingSuffixCount--;
}
else
{
SuffixTreeNode next = activeNode.Children[text[activeEdge]];
int edgeLength = next.End - next.Start + 1;
if (activeLength >= edgeLength)
{
activeEdge += edgeLength;
activeLength -= edgeLength;
activeNode = next;
continue;
}
if (text[next.Start + activeLength] == currentChar)
{
activeLength++;
break;
}
// Split edge
SuffixTreeNode split = new SuffixTreeNode(next.Start, next.Start + activeLength - 1);
activeNode.Children[text[activeEdge]] = split;
split.Children[currentChar] = new SuffixTreeNode(i, text.Length - 1);
next.Start += activeLength;
split.Children[text[next.Start]] = next;
remainingSuffixCount--;
}
}
}
}
public bool Contains(string pattern)
{
return SearchPattern(root, pattern, 0);
}
private bool SearchPattern(SuffixTreeNode node, string pattern, int index)
{
if (index >= pattern.Length) return true;
if (!node.Children.ContainsKey(pattern[index])) return false;
SuffixTreeNode child = node.Children[pattern[index]];
int edgeLength = child.End - child.Start + 1;
for (int i = 0; i < edgeLength && index < pattern.Length; i++)
{
if (text[child.Start + i] != pattern[index]) return false;
index++;
}
if (index >= pattern.Length) return true;
return SearchPattern(child, pattern, index);
}
}
// 30. Red-Black Tree Implementation
public enum Color { Red, Black }
public class RedBlackTreeNode<T> where T : IComparable<T>
{
public T Value { get; set; }
public Color Color { get; set; }
public RedBlackTreeNode<T> Left { get; set; }
public RedBlackTreeNode<T> Right { get; set; }
public RedBlackTreeNode<T> Parent { get; set; }
public RedBlackTreeNode(T value)
{
Value = value;
Color = Color.Red;
Left = Right = Parent = null;
}
}
public class RedBlackTree<T> where T : IComparable<T>
{
private RedBlackTreeNode<T> root;
private RedBlackTreeNode<T> nil;
public RedBlackTree()
{
nil = new RedBlackTreeNode<T>(default(T)) { Color = Color.Black };
root = nil;
}
public void Insert(T value)
{
RedBlackTreeNode<T> node = new RedBlackTreeNode<T>(value);
node.Left = node.Right = nil;
RedBlackTreeNode<T> y = null;
RedBlackTreeNode<T> x = root;
while (x != nil)
{
y = x;
if (node.Value.CompareTo(x.Value) < 0)
x = x.Left;
else
x = x.Right;
}
node.Parent = y;
if (y == null)
root = node;
else if (node.Value.CompareTo(y.Value) < 0)
y.Left = node;
else
y.Right = node;
InsertFixup(node);
}
private void InsertFixup(RedBlackTreeNode<T> node)
{
while (node.Parent != null && node.Parent.Color == Color.Red)
{
if (node.Parent == node.Parent.Parent.Left)
{
RedBlackTreeNode<T> uncle = node.Parent.Parent.Right;
if (uncle.Color == Color.Red)
{
node.Parent.Color = Color.Black;
uncle.Color = Color.Black;
node.Parent.Parent.Color = Color.Red;
node = node.Parent.Parent;
}
else
{
if (node == node.Parent.Right)
{
node = node.Parent;
LeftRotate(node);
}
node.Parent.Color = Color.Black;
node.Parent.Parent.Color = Color.Red;
RightRotate(node.Parent.Parent);
}
}
else
{
RedBlackTreeNode<T> uncle = node.Parent.Parent.Left;
if (uncle.Color == Color.Red)
{
node.Parent.Color = Color.Black;
uncle.Color = Color.Black;
node.Parent.Parent.Color = Color.Red;
node = node.Parent.Parent;
}
else
{
if (node == node.Parent.Left)
{
node = node.Parent;
RightRotate(node);
}
node.Parent.Color = Color.Black;
node.Parent.Parent.Color = Color.Red;
LeftRotate(node.Parent.Parent);
}
}
}
root.Color = Color.Black;
}
private void LeftRotate(RedBlackTreeNode<T> x)
{
RedBlackTreeNode<T> y = x.Right;
x.Right = y.Left;
if (y.Left != nil)
y.Left.Parent = x;
y.Parent = x.Parent;
if (x.Parent == null)
root = y;
else if (x == x.Parent.Left)
x.Parent.Left = y;
else
x.Parent.Right = y;
y.Left = x;
x.Parent = y;
}
private void RightRotate(RedBlackTreeNode<T> x)
{
RedBlackTreeNode<T> y = x.Left;
x.Left = y.Right;
if (y.Right != nil)
y.Right.Parent = x;
y.Parent = x.Parent;
if (x.Parent == null)
root = y;
else if (x == x.Parent.Right)
x.Parent.Right = y;
else
x.Parent.Left = y;
y.Right = x;
x.Parent = y;
}
public bool Search(T value)
{
return SearchNode(root, value) != nil;
}
private RedBlackTreeNode<T> SearchNode(RedBlackTreeNode<T> node, T value)
{
if (node == nil || value.Equals(node.Value))
return node;
if (value.CompareTo(node.Value) < 0)
return SearchNode(node.Left, value);
else
return SearchNode(node.Right, value);
}
}
// AVL Tree Implementation for Comparison
public class AVLTreeNode<T> where T : IComparable<T>
{
public T Value { get; set; }
public AVLTreeNode<T> Left { get; set; }
public AVLTreeNode<T> Right { get; set; }
public int Height { get; set; }
public AVLTreeNode(T value)
{
Value = value;
Height = 1;
}
}
public class AVLTree<T> where T : IComparable<T>
{
private AVLTreeNode<T> root;
private int Height(AVLTreeNode<T> node)
{
return node?.Height ?? 0;
}
private int BalanceFactor(AVLTreeNode<T> node)
{
return node == null ? 0 : Height(node.Left) - Height(node.Right);
}
private AVLTreeNode<T> RightRotate(AVLTreeNode<T> y)
{
AVLTreeNode<T> x = y.Left;
AVLTreeNode<T> T2 = x.Right;
x.Right = y;
y.Left = T2;
y.Height = Math.Max(Height(y.Left), Height(y.Right)) + 1;
x.Height = Math.Max(Height(x.Left), Height(x.Right)) + 1;
return x;
}
private AVLTreeNode<T> LeftRotate(AVLTreeNode<T> x)
{
AVLTreeNode<T> y = x.Right;
AVLTreeNode<T> T2 = y.Left;
y.Left = x;
x.Right = T2;
x.Height = Math.Max(Height(x.Left), Height(x.Right)) + 1;
y.Height = Math.Max(Height(y.Left), Height(y.Right)) + 1;
return y;
}
public void Insert(T value)
{
root = InsertNode(root, value);
}
private AVLTreeNode<T> InsertNode(AVLTreeNode<T> node, T value)
{
if (node == null)
return new AVLTreeNode<T>(value);
if (value.CompareTo(node.Value) < 0)
node.Left = InsertNode(node.Left, value);
else if (value.CompareTo(node.Value) > 0)
node.Right = InsertNode(node.Right, value);
else
return node; // Duplicate values not allowed
node.Height = Math.Max(Height(node.Left), Height(node.Right)) + 1;
int balance = BalanceFactor(node);
// Left Left Case
if (balance > 1 && value.CompareTo(node.Left.Value) < 0)
return RightRotate(node);
// Right Right Case
if (balance < -1 && value.CompareTo(node.Right.Value) > 0)
return LeftRotate(node);
// Left Right Case
if (balance > 1 && value.CompareTo(node.Left.Value) > 0)
{
node.Left = LeftRotate(node.Left);
return RightRotate(node);
}
// Right Left Case
if (balance < -1 && value.CompareTo(node.Right.Value) < 0)
{
node.Right = RightRotate(node.Right);
return LeftRotate(node);
}
return node;
}
public bool Search(T value)
{
return SearchNode(root, value) != null;
}
private AVLTreeNode<T> SearchNode(AVLTreeNode<T> node, T value)
{
if (node == null || value.Equals(node.Value))
return node;
if (value.CompareTo(node.Value) < 0)
return SearchNode(node.Left, value);
else
return SearchNode(node.Right, value);
}
}
// Example usage and testing
public class Program
{
public static void Main()
{
Console.WriteLine("Data Structures Interview Questions - C# Implementations\n");
// Test Hash Table
Console.WriteLine("21. Hash Table Implementation:");
var hashTable = new HashTable<string, int>();
hashTable.Put("apple", 1);
hashTable.Put("banana", 2);
Console.WriteLine($"Contains 'apple': {hashTable.ContainsKey("apple")}");
Console.WriteLine($"Value for 'banana': {hashTable.Get("banana")}\n");
// Test Hash Set
Console.WriteLine("22. Hash Set Implementation:");
var hashSet = new HashSet<string>();
hashSet.Add("red");
hashSet.Add("green");
hashSet.Add("blue");
Console.WriteLine($"Contains 'red': {hashSet.Contains("red")}");
Console.WriteLine($"Contains 'yellow': {hashSet.Contains("yellow")}\n");
// Test Trie
Console.WriteLine("23. Trie Implementation:");
var trie = new Trie();
trie.Insert("hello");
trie.Insert("world");
trie.Insert("help");
Console.WriteLine($"Search 'hello': {trie.Search("hello")}");
Console.WriteLine($"StartsWith 'he': {trie.StartsWith("he")}\n");
// Test Heap and Priority Queue
Console.WriteLine("24. Heap and Priority Queue Implementation:");
var minHeap = new MinHeap<int>();
minHeap.Insert(5);
minHeap.Insert(3);
minHeap.Insert(7);
Console.WriteLine($"Min element: {minHeap.Peek()}");
Console.WriteLine($"Extracted min: {minHeap.ExtractMin()}\n");
var priorityQueue = new PriorityQueue<int>();
priorityQueue.Enqueue(10);
priorityQueue.Enqueue(5);
priorityQueue.Enqueue(15);
Console.WriteLine($"Priority queue peek: {priorityQueue.Peek()}\n");
// Test Skip List
Console.WriteLine("25. Skip List Implementation:");
var skipList = new SkipList<int>();
skipList.Insert(3);
skipList.Insert(6);
skipList.Insert(7);
skipList.Insert(9);
Console.WriteLine($"Search 6: {skipList.Search(6)}");
Console.WriteLine($"Search 5: {skipList.Search(5)}\n");
// Test B-Tree
Console.WriteLine("26. B-Tree Implementation:");
var bTree = new BTree<int>(3);
bTree.Insert(10);
bTree.Insert(20);
bTree.Insert(5);
Console.WriteLine($"Search 10: {bTree.Search(10)}");
Console.WriteLine($"Search 15: {bTree.Search(15)}\n");
// Test Segment Tree
Console.WriteLine("27. Segment Tree Implementation:");
int[] arr = { 1, 3, 5, 7, 9, 11 };
var segmentTree = new SegmentTree(arr);
Console.WriteLine($"Sum from index 1 to 3: {segmentTree.Query(1, 3)}");
segmentTree.Update(2, 10);
Console.WriteLine($"Sum from index 1 to 3 after update: {segmentTree.Query(1, 3)}\n");
// Test Union-Find
Console.WriteLine("28. Union-Find Implementation:");
var unionFind = new UnionFind(5);
unionFind.Union(0, 1);
unionFind.Union(2, 3);
unionFind.Union(1, 2);
Console.WriteLine($"0 and 3 connected: {unionFind.Connected(0, 3)}");
Console.WriteLine($"Number of components: {unionFind.Count}\n");
// Test Suffix Tree
Console.WriteLine("29. Suffix Tree Implementation:");
var suffixTree = new SuffixTree("banana");
Console.WriteLine($"Contains 'ana': {suffixTree.Contains("ana")}");
Console.WriteLine($"Contains 'xyz': {suffixTree.Contains("xyz")}\n");
// Test Red-Black Tree
Console.WriteLine("30. Red-Black Tree Implementation:");
var redBlackTree = new RedBlackTree<int>();
redBlackTree.Insert(7);
redBlackTree.Insert(3);
redBlackTree.Insert(18);
Console.WriteLine($"Search 3: {redBlackTree.Search(3)}");
Console.WriteLine($"Search 5: {redBlackTree.Search(5)}\n");
// Test AVL Tree
Console.WriteLine("AVL Tree Implementation (for comparison):");
var avlTree = new AVLTree<int>();
avlTree.Insert(10);
avlTree.Insert(20);
avlTree.Insert(30);
avlTree.Insert(40);
avlTree.Insert(50);
avlTree.Insert(25);
Console.WriteLine($"Search 25: {avlTree.Search(25)}");
Console.WriteLine($"Search 35: {avlTree.Search(35)}\n");
Console.WriteLine("Key Differences Summary:");
Console.WriteLine("Hash Table vs Hash Set: Hash table stores key-value pairs, hash set stores only unique elements");
Console.WriteLine("Heap vs Priority Queue: Heap is the underlying data structure, priority queue is the abstract interface");
Console.WriteLine("B-tree vs B+ tree: B+ tree stores all data in leaves and has linked leaves for range queries");
Console.WriteLine("Union-Find vs Disjoint Set: They are the same data structure with different names");
}
}
I'll act as a technical architect and provide comprehensive answers to these sorting algorithm questions with C# implementations. Let me create a detailed response covering all the topics.
Now let me create a comprehensive README file that explains the technical concepts and provides interview-ready answers:
Now let me create a performance comparison utility to demonstrate the practical differences between these algorithms:
Finally, let me create a comprehensive interview guide with sample questions and answers:
Summary
📁 Files Created:
SortingAlgorithms.cs- Complete C# implementations of all 10 sorting algorithms with demonstrationsREADME.md- Detailed technical explanations and architectural considerationsSortingPerformanceComparison.cs- Performance benchmarking utilityInterviewGuide.md- Comprehensive interview preparation guide
🎯 Key Features:
Technical Architect Perspective: - Focus on practical system design considerations - Memory constraints and performance trade-offs - Real-world use cases and production scenarios - Scalability and maintenance implications
Complete Implementations: - All 10 sorting algorithms in C# - Working code with sample data - Performance comparison utilities - Memory usage analysis
Interview-Ready Answers: - Standard answers for basic understanding - Technical architect answers with business context - Decision frameworks for algorithm selection - Common follow-up questions and responses
🔍 Questions Covered:
- Bubble Sort vs Insertion Sort - Cache performance, production suitability
- QuickSort Implementation - Pivot selection, memory usage, production considerations
- Time Complexity Analysis - Algorithm selection criteria, trade-offs
- Merge Sort Implementation - Predictable performance, parallelization opportunities
- Stable vs Unstable Sorting - Multi-key sorting, data integrity
- Heap Sort Implementation - Memory efficiency, priority queues
- Comparison vs Non-comparison Sorting - Data type constraints, performance vs generality
- Radix Sort Implementation - Bounded input, memory requirements
- In-place vs Out-of-place Sorting - Memory constraints, data preservation
- Bucket Sort Implementation - Data distribution analysis, bucket strategy
💡 Technical Architect Insights:
- System Design Focus: Emphasis on practical considerations over theoretical knowledge
- Trade-off Analysis: Memory vs performance, stability vs speed, complexity vs maintainability
- Real-world Context: Database systems, web applications, embedded systems, big data
- Decision Frameworks: Clear criteria for algorithm selection based on requirements
I'll act as a technical architect and provide comprehensive answers to these search algorithm questions with C# implementations. Let me create a detailed response covering all the concepts.
Now let me create a comprehensive documentation file that explains each concept in detail:
Perfect! I've created a comprehensive technical architect response to all 10 search algorithm interview questions. Here's a summary of what I've provided:
�� Files Created:
SearchAlgorithms.cs- Complete C# implementation with all algorithmsSearchAlgorithms_TechnicalArchitect_Answers.md- Detailed technical documentation
🎯 Key Highlights:
41. Linear vs Binary Search
- Linear: O(n), works on unsorted data, simple implementation
- Binary: O(log n), requires sorted data, much faster for large datasets
42. Depth-First Search
- Both recursive and iterative implementations
- Uses stack (explicit or call stack)
- Great for topological sorting and cycle detection
43. Iterative vs Recursive
- Recursive: Elegant, natural for trees, potential stack overflow
- Iterative: Memory efficient, faster, no stack overflow risk
44. Breadth-First Search
- Uses queue data structure
- Guarantees shortest path in unweighted graphs
- Level-by-level traversal
45. Uninformed vs Informed Search
- Uninformed: No heuristic (BFS, DFS)
- Informed: Uses heuristics (A*, Greedy Best-First)
- Informed search is typically much more efficient
46. A* Search Algorithm
- f(n) = g(n) + h(n) formula
- Priority queue implementation
- Guarantees optimal solution with admissible heuristic
47. Deterministic vs Non-deterministic
- Deterministic: Same input → same output
- Non-deterministic: May produce different results (useful for optimization)
48. Binary Search Tree Operations
- Insert, Search, Delete operations
- All O(log n) average case
- Handles three deletion cases
49. Sequential vs Indexed Search
- Sequential: O(n), no preprocessing
- Indexed: O(1) average, requires index structure
50. Hash-Based Search
- Hash table with chaining collision resolution
- Bloom filter for probabilistic membership testing
- O(1) average case operations
🏗️ Technical Architecture Considerations:
The documentation also includes: - Performance optimization strategies - Scalability patterns (horizontal/vertical scaling, sharding) - System design principles (separation of concerns, loose coupling) - Monitoring and observability guidelines
�� Ready to Run:
The C# code includes a complete Program class with example usage that demonstrates all the algorithms in action. You can compile and run it to see the algorithms working with sample data.
This comprehensive response demonstrates deep technical knowledge from a senior architect perspective, covering both theoretical concepts and practical implementation details that would be expected in a technical interview.
using System;
using System.Collections.Generic;
using System.Linq;
namespace SearchAlgorithms
{
// 41. Linear Search vs Binary Search
public class LinearVsBinarySearch
{
// Linear Search - O(n) time complexity
public static int LinearSearch(int[] arr, int target)
{
for (int i = 0; i < arr.Length; i++)
{
if (arr[i] == target)
return i;
}
return -1; // Not found
}
// Binary Search - O(log n) time complexity (requires sorted array)
public static int BinarySearch(int[] arr, int target)
{
int left = 0;
int right = arr.Length - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (arr[mid] == target)
return mid;
else if (arr[mid] < target)
left = mid + 1;
else
right = mid - 1;
}
return -1; // Not found
}
// Key Differences:
// - Linear: Works on unsorted arrays, O(n) time
// - Binary: Requires sorted array, O(log n) time
// - Linear: Simple implementation, good for small datasets
// - Binary: More complex, excellent for large sorted datasets
}
// 42. Depth-First Search (DFS)
public class DepthFirstSearch
{
public class Graph
{
private Dictionary<int, List<int>> adjacencyList;
public Graph()
{
adjacencyList = new Dictionary<int, List<int>>();
}
public void AddEdge(int vertex, int neighbor)
{
if (!adjacencyList.ContainsKey(vertex))
adjacencyList[vertex] = new List<int>();
adjacencyList[vertex].Add(neighbor);
}
// Recursive DFS
public void DFSRecursive(int startVertex)
{
HashSet<int> visited = new HashSet<int>();
DFSRecursiveHelper(startVertex, visited);
}
private void DFSRecursiveHelper(int vertex, HashSet<int> visited)
{
visited.Add(vertex);
Console.Write(vertex + " ");
if (adjacencyList.ContainsKey(vertex))
{
foreach (int neighbor in adjacencyList[vertex])
{
if (!visited.Contains(neighbor))
{
DFSRecursiveHelper(neighbor, visited);
}
}
}
}
// Iterative DFS using Stack
public void DFSIterative(int startVertex)
{
HashSet<int> visited = new HashSet<int>();
Stack<int> stack = new Stack<int>();
stack.Push(startVertex);
while (stack.Count > 0)
{
int vertex = stack.Pop();
if (!visited.Contains(vertex))
{
visited.Add(vertex);
Console.Write(vertex + " ");
if (adjacencyList.ContainsKey(vertex))
{
// Push neighbors in reverse order to maintain DFS order
for (int i = adjacencyList[vertex].Count - 1; i >= 0; i--)
{
int neighbor = adjacencyList[vertex][i];
if (!visited.Contains(neighbor))
{
stack.Push(neighbor);
}
}
}
}
}
}
}
}
// 43. Iterative vs Recursive Search
public class IterativeVsRecursive
{
// Recursive Binary Search
public static int BinarySearchRecursive(int[] arr, int target, int left, int right)
{
if (left > right)
return -1;
int mid = left + (right - left) / 2;
if (arr[mid] == target)
return mid;
else if (arr[mid] < target)
return BinarySearchRecursive(arr, target, mid + 1, right);
else
return BinarySearchRecursive(arr, target, left, mid - 1);
}
// Iterative Binary Search
public static int BinarySearchIterative(int[] arr, int target)
{
int left = 0;
int right = arr.Length - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (arr[mid] == target)
return mid;
else if (arr[mid] < target)
left = mid + 1;
else
right = mid - 1;
}
return -1;
}
// Key Differences:
// - Recursive: Uses call stack, more elegant, potential stack overflow
// - Iterative: Uses explicit stack/loop, more memory efficient
// - Recursive: Easier to understand for some algorithms
// - Iterative: Better performance for deep recursion
}
// 44. Breadth-First Search (BFS)
public class BreadthFirstSearch
{
public class Graph
{
private Dictionary<int, List<int>> adjacencyList;
public Graph()
{
adjacencyList = new Dictionary<int, List<int>>();
}
public void AddEdge(int vertex, int neighbor)
{
if (!adjacencyList.ContainsKey(vertex))
adjacencyList[vertex] = new List<int>();
adjacencyList[vertex].Add(neighbor);
}
public void BFS(int startVertex)
{
HashSet<int> visited = new HashSet<int>();
Queue<int> queue = new Queue<int>();
visited.Add(startVertex);
queue.Enqueue(startVertex);
while (queue.Count > 0)
{
int vertex = queue.Dequeue();
Console.Write(vertex + " ");
if (adjacencyList.ContainsKey(vertex))
{
foreach (int neighbor in adjacencyList[vertex])
{
if (!visited.Contains(neighbor))
{
visited.Add(neighbor);
queue.Enqueue(neighbor);
}
}
}
}
}
// BFS with level tracking
public void BFSWithLevels(int startVertex)
{
HashSet<int> visited = new HashSet<int>();
Queue<(int vertex, int level)> queue = new Queue<(int, int)>();
visited.Add(startVertex);
queue.Enqueue((startVertex, 0));
while (queue.Count > 0)
{
var (vertex, level) = queue.Dequeue();
Console.WriteLine($"Vertex: {vertex}, Level: {level}");
if (adjacencyList.ContainsKey(vertex))
{
foreach (int neighbor in adjacencyList[vertex])
{
if (!visited.Contains(neighbor))
{
visited.Add(neighbor);
queue.Enqueue((neighbor, level + 1));
}
}
}
}
}
}
}
// 45. Uninformed vs Informed Search
public class UninformedVsInformedSearch
{
// Uninformed Search (BFS) - No heuristic information
public class UninformedSearch
{
public static List<int> BFSShortestPath(Dictionary<int, List<int>> graph, int start, int target)
{
Queue<List<int>> queue = new Queue<List<int>>();
HashSet<int> visited = new HashSet<int>();
queue.Enqueue(new List<int> { start });
visited.Add(start);
while (queue.Count > 0)
{
List<int> path = queue.Dequeue();
int current = path[path.Count - 1];
if (current == target)
return path;
if (graph.ContainsKey(current))
{
foreach (int neighbor in graph[current])
{
if (!visited.Contains(neighbor))
{
visited.Add(neighbor);
List<int> newPath = new List<int>(path) { neighbor };
queue.Enqueue(newPath);
}
}
}
}
return null; // No path found
}
}
// Informed Search (A* with heuristic) - Uses heuristic information
public class InformedSearch
{
public static List<int> AStarSearch(Dictionary<int, List<int>> graph, int start, int target, Func<int, int, int> heuristic)
{
var openSet = new PriorityQueue<(int node, List<int> path, int cost), int>();
var closedSet = new HashSet<int>();
openSet.Enqueue((start, new List<int> { start }, 0), 0);
while (openSet.Count > 0)
{
var (current, path, cost) = openSet.Dequeue();
if (current == target)
return path;
if (closedSet.Contains(current))
continue;
closedSet.Add(current);
if (graph.ContainsKey(current))
{
foreach (int neighbor in graph[current])
{
if (!closedSet.Contains(neighbor))
{
int newCost = cost + 1; // Assuming unit cost
List<int> newPath = new List<int>(path) { neighbor };
int priority = newCost + heuristic(neighbor, target);
openSet.Enqueue((neighbor, newPath, newCost), priority);
}
}
}
}
return null;
}
}
// Key Differences:
// - Uninformed: No domain knowledge, explores all possibilities equally
// - Informed: Uses heuristic functions to guide search toward goal
// - Uninformed: BFS, DFS, Uniform Cost Search
// - Informed: A*, Greedy Best-First Search
}
// 46. A* Search Algorithm
public class AStarSearch
{
public class Node : IComparable<Node>
{
public int X, Y;
public int G; // Cost from start to current node
public int H; // Heuristic cost from current to goal
public int F => G + H; // Total cost
public Node Parent;
public Node(int x, int y)
{
X = x;
Y = y;
G = 0;
H = 0;
Parent = null;
}
public int CompareTo(Node other)
{
return F.CompareTo(other.F);
}
}
public static List<(int, int)> AStar(int[,] grid, (int, int) start, (int, int) goal)
{
int rows = grid.GetLength(0);
int cols = grid.GetLength(1);
var openSet = new PriorityQueue<Node, int>();
var closedSet = new HashSet<(int, int)>();
var nodeMap = new Dictionary<(int, int), Node>();
Node startNode = new Node(start.Item1, start.Item2);
startNode.H = Heuristic(start, goal);
openSet.Enqueue(startNode, startNode.F);
nodeMap[start] = startNode;
while (openSet.Count > 0)
{
Node current = openSet.Dequeue();
if (current.X == goal.Item1 && current.Y == goal.Item2)
{
return ReconstructPath(current);
}
closedSet.Add((current.X, current.Y));
// Check all 8 neighbors
for (int dx = -1; dx <= 1; dx++)
{
for (int dy = -1; dy <= 1; dy++)
{
if (dx == 0 && dy == 0) continue;
int newX = current.X + dx;
int newY = current.Y + dy;
if (newX < 0 || newX >= rows || newY < 0 || newY >= cols)
continue;
if (grid[newX, newY] == 1) // Wall
continue;
if (closedSet.Contains((newX, newY)))
continue;
int newG = current.G + (dx != 0 && dy != 0 ? 14 : 10); // Diagonal vs straight
if (!nodeMap.ContainsKey((newX, newY)))
{
Node neighbor = new Node(newX, newY);
neighbor.G = newG;
neighbor.H = Heuristic((newX, newY), goal);
neighbor.Parent = current;
nodeMap[(newX, newY)] = neighbor;
openSet.Enqueue(neighbor, neighbor.F);
}
else if (newG < nodeMap[(newX, newY)].G)
{
Node neighbor = nodeMap[(newX, newY)];
neighbor.G = newG;
neighbor.Parent = current;
}
}
}
}
return null; // No path found
}
private static int Heuristic((int, int) a, (int, int) b)
{
// Manhattan distance
return Math.Abs(a.Item1 - b.Item1) + Math.Abs(a.Item2 - b.Item2);
}
private static List<(int, int)> ReconstructPath(Node goal)
{
var path = new List<(int, int)>();
Node current = goal;
while (current != null)
{
path.Add((current.X, current.Y));
current = current.Parent;
}
path.Reverse();
return path;
}
}
// 47. Deterministic vs Non-deterministic Search
public class DeterministicVsNonDeterministic
{
// Deterministic Search - Always produces same result
public static int DeterministicBinarySearch(int[] arr, int target)
{
int left = 0;
int right = arr.Length - 1;
while (left <= right)
{
int mid = left + (right - left) / 2; // Deterministic midpoint
if (arr[mid] == target)
return mid;
else if (arr[mid] < target)
left = mid + 1;
else
right = mid - 1;
}
return -1;
}
// Non-deterministic Search - May produce different results
public static int NonDeterministicSearch(int[] arr, int target)
{
Random random = new Random();
List<int> candidates = new List<int>();
// Find all occurrences
for (int i = 0; i < arr.Length; i++)
{
if (arr[i] == target)
candidates.Add(i);
}
if (candidates.Count == 0)
return -1;
// Return random occurrence
return candidates[random.Next(candidates.Count)];
}
// Key Differences:
// - Deterministic: Same input always produces same output
// - Non-deterministic: Same input may produce different outputs
// - Deterministic: Predictable, good for debugging
// - Non-deterministic: Useful for optimization, genetic algorithms
}
// 48. Binary Search Tree Operations
public class BinarySearchTree
{
public class TreeNode
{
public int Value;
public TreeNode Left;
public TreeNode Right;
public TreeNode(int value)
{
Value = value;
Left = null;
Right = null;
}
}
private TreeNode root;
public BinarySearchTree()
{
root = null;
}
// Insert operation
public void Insert(int value)
{
root = InsertRecursive(root, value);
}
private TreeNode InsertRecursive(TreeNode node, int value)
{
if (node == null)
return new TreeNode(value);
if (value < node.Value)
node.Left = InsertRecursive(node.Left, value);
else if (value > node.Value)
node.Right = InsertRecursive(node.Right, value);
return node;
}
// Search operation
public bool Search(int value)
{
return SearchRecursive(root, value);
}
private bool SearchRecursive(TreeNode node, int value)
{
if (node == null)
return false;
if (node.Value == value)
return true;
if (value < node.Value)
return SearchRecursive(node.Left, value);
else
return SearchRecursive(node.Right, value);
}
// Delete operation
public void Delete(int value)
{
root = DeleteRecursive(root, value);
}
private TreeNode DeleteRecursive(TreeNode node, int value)
{
if (node == null)
return null;
if (value < node.Value)
node.Left = DeleteRecursive(node.Left, value);
else if (value > node.Value)
node.Right = DeleteRecursive(node.Right, value);
else
{
// Node with only one child or no child
if (node.Left == null)
return node.Right;
else if (node.Right == null)
return node.Left;
// Node with two children: Get the inorder successor (smallest in right subtree)
node.Value = MinValue(node.Right);
node.Right = DeleteRecursive(node.Right, node.Value);
}
return node;
}
private int MinValue(TreeNode node)
{
int minValue = node.Value;
while (node.Left != null)
{
minValue = node.Left.Value;
node = node.Left;
}
return minValue;
}
// Inorder traversal
public void InorderTraversal()
{
InorderRecursive(root);
}
private void InorderRecursive(TreeNode node)
{
if (node != null)
{
InorderRecursive(node.Left);
Console.Write(node.Value + " ");
InorderRecursive(node.Right);
}
}
}
// 49. Sequential vs Indexed Search
public class SequentialVsIndexedSearch
{
// Sequential Search - O(n) time complexity
public static int SequentialSearch(int[] arr, int target)
{
for (int i = 0; i < arr.Length; i++)
{
if (arr[i] == target)
return i;
}
return -1;
}
// Indexed Search - O(1) average time complexity
public class IndexedSearch
{
private Dictionary<int, List<int>> index;
public IndexedSearch(int[] arr)
{
index = new Dictionary<int, List<int>>();
for (int i = 0; i < arr.Length; i++)
{
if (!index.ContainsKey(arr[i]))
index[arr[i]] = new List<int>();
index[arr[i]].Add(i);
}
}
public List<int> Search(int target)
{
if (index.ContainsKey(target))
return index[target];
return new List<int>();
}
}
// Key Differences:
// - Sequential: Scans through data sequentially, O(n) time
// - Indexed: Uses pre-built index structure, O(1) average time
// - Sequential: No additional space required
// - Indexed: Requires extra space for index structure
// - Sequential: Good for small datasets or one-time searches
// - Indexed: Excellent for large datasets with frequent searches
}
// 50. Hash-Based Search
public class HashBasedSearch
{
// Simple Hash Table Implementation
public class HashTable<TKey, TValue>
{
private class HashNode
{
public TKey Key;
public TValue Value;
public HashNode Next;
public HashNode(TKey key, TValue value)
{
Key = key;
Value = value;
Next = null;
}
}
private HashNode[] buckets;
private int size;
private int capacity;
public HashTable(int capacity = 16)
{
this.capacity = capacity;
buckets = new HashNode[capacity];
size = 0;
}
private int GetHash(TKey key)
{
return Math.Abs(key.GetHashCode()) % capacity;
}
// Insert operation
public void Put(TKey key, TValue value)
{
int index = GetHash(key);
HashNode current = buckets[index];
// Check if key already exists
while (current != null)
{
if (current.Key.Equals(key))
{
current.Value = value;
return;
}
current = current.Next;
}
// Insert new node at beginning of chain
HashNode newNode = new HashNode(key, value);
newNode.Next = buckets[index];
buckets[index] = newNode;
size++;
// Resize if load factor is too high
if ((double)size / capacity > 0.75)
{
Resize();
}
}
// Search operation
public TValue Get(TKey key)
{
int index = GetHash(key);
HashNode current = buckets[index];
while (current != null)
{
if (current.Key.Equals(key))
return current.Value;
current = current.Next;
}
throw new KeyNotFoundException($"Key '{key}' not found");
}
// Contains operation
public bool Contains(TKey key)
{
int index = GetHash(key);
HashNode current = buckets[index];
while (current != null)
{
if (current.Key.Equals(key))
return true;
current = current.Next;
}
return false;
}
// Remove operation
public bool Remove(TKey key)
{
int index = GetHash(key);
HashNode current = buckets[index];
HashNode previous = null;
while (current != null)
{
if (current.Key.Equals(key))
{
if (previous == null)
buckets[index] = current.Next;
else
previous.Next = current.Next;
size--;
return true;
}
previous = current;
current = current.Next;
}
return false;
}
private void Resize()
{
capacity *= 2;
HashNode[] oldBuckets = buckets;
buckets = new HashNode[capacity];
size = 0;
foreach (HashNode node in oldBuckets)
{
HashNode current = node;
while (current != null)
{
Put(current.Key, current.Value);
current = current.Next;
}
}
}
public int Size => size;
}
// Bloom Filter for approximate membership testing
public class BloomFilter
{
private bool[] bitArray;
private int size;
private int hashCount;
public BloomFilter(int size, int hashCount)
{
this.size = size;
this.hashCount = hashCount;
bitArray = new bool[size];
}
public void Add(string item)
{
for (int i = 0; i < hashCount; i++)
{
int hash = GetHash(item, i);
bitArray[hash % size] = true;
}
}
public bool Contains(string item)
{
for (int i = 0; i < hashCount; i++)
{
int hash = GetHash(item, i);
if (!bitArray[hash % size])
return false;
}
return true;
}
private int GetHash(string item, int seed)
{
int hash = seed;
foreach (char c in item)
{
hash = (hash * 31 + c) & 0x7fffffff;
}
return hash;
}
}
}
// Example usage and testing
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("=== Search Algorithms Demo ===\n");
// Test Linear vs Binary Search
int[] arr = { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 };
int target = 12;
Console.WriteLine($"Linear Search for {target}: {LinearVsBinarySearch.LinearSearch(arr, target)}");
Console.WriteLine($"Binary Search for {target}: {LinearVsBinarySearch.BinarySearch(arr, target)}");
// Test DFS
Console.WriteLine("\n=== DFS Demo ===");
var graph = new DepthFirstSearch.Graph();
graph.AddEdge(0, 1);
graph.AddEdge(0, 2);
graph.AddEdge(1, 3);
graph.AddEdge(1, 4);
graph.AddEdge(2, 5);
graph.AddEdge(2, 6);
Console.Write("DFS Recursive: ");
graph.DFSRecursive(0);
Console.WriteLine();
Console.Write("DFS Iterative: ");
graph.DFSIterative(0);
Console.WriteLine();
// Test BFS
Console.WriteLine("\n=== BFS Demo ===");
var bfsGraph = new BreadthFirstSearch.Graph();
bfsGraph.AddEdge(0, 1);
bfsGraph.AddEdge(0, 2);
bfsGraph.AddEdge(1, 3);
bfsGraph.AddEdge(1, 4);
bfsGraph.AddEdge(2, 5);
bfsGraph.AddEdge(2, 6);
Console.Write("BFS: ");
bfsGraph.BFS(0);
Console.WriteLine();
// Test BST
Console.WriteLine("\n=== Binary Search Tree Demo ===");
var bst = new BinarySearchTree();
bst.Insert(50);
bst.Insert(30);
bst.Insert(70);
bst.Insert(20);
bst.Insert(40);
bst.Insert(60);
bst.Insert(80);
Console.Write("Inorder Traversal: ");
bst.InorderTraversal();
Console.WriteLine();
Console.WriteLine($"Search for 40: {bst.Search(40)}");
Console.WriteLine($"Search for 90: {bst.Search(90)}");
// Test Hash Table
Console.WriteLine("\n=== Hash Table Demo ===");
var hashTable = new HashBasedSearch.HashTable<string, int>();
hashTable.Put("apple", 1);
hashTable.Put("banana", 2);
hashTable.Put("cherry", 3);
Console.WriteLine($"Contains 'apple': {hashTable.Contains("apple")}");
Console.WriteLine($"Value for 'banana': {hashTable.Get("banana")}");
// Test Bloom Filter
Console.WriteLine("\n=== Bloom Filter Demo ===");
var bloomFilter = new HashBasedSearch.BloomFilter(100, 3);
bloomFilter.Add("hello");
bloomFilter.Add("world");
Console.WriteLine($"Contains 'hello': {bloomFilter.Contains("hello")}");
Console.WriteLine($"Contains 'world': {bloomFilter.Contains("world")}");
Console.WriteLine($"Contains 'test': {bloomFilter.Contains("test")}");
}
}
}
I'll answer these dynamic programming and algorithm questions with C# implementations. Let me provide comprehensive explanations and code examples for each question.
51. Difference between Memoization and Tabulation
Memoization (Top-down): Stores results of subproblems as they are computed, avoiding redundant calculations. Tabulation (Bottom-up): Builds a table iteratively from the base case to the target problem.
public class MemoizationVsTabulation
{
// Memoization approach
private Dictionary<int, int> memo = new Dictionary<int, int>();
public int FibonacciMemoization(int n)
{
if (n <= 1) return n;
if (memo.ContainsKey(n))
return memo[n];
memo[n] = FibonacciMemoization(n - 1) + FibonacciMemoization(n - 2);
return memo[n];
}
// Tabulation approach
public int FibonacciTabulation(int n)
{
if (n <= 1) return n;
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++)
{
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
52. Fibonacci Sequence with Dynamic Programming
public class FibonacciDP
{
// Space-optimized approach
public int FibonacciOptimized(int n)
{
if (n <= 1) return n;
int prev = 0, curr = 1;
for (int i = 2; i <= n; i++)
{
int next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}
// With memoization
private Dictionary<int, int> cache = new Dictionary<int, int>();
public int FibonacciMemo(int n)
{
if (n <= 1) return n;
if (cache.ContainsKey(n))
return cache[n];
cache[n] = FibonacciMemo(n - 1) + FibonacciMemo(n - 2);
return cache[n];
}
}
53. Top-down vs Bottom-up Approaches
public class TopDownVsBottomUp
{
// Top-down (Memoization)
private Dictionary<int, int> memo = new Dictionary<int, int>();
public int TopDownFib(int n)
{
if (n <= 1) return n;
if (memo.ContainsKey(n))
return memo[n];
memo[n] = TopDownFib(n - 1) + TopDownFib(n - 2);
return memo[n];
}
// Bottom-up (Tabulation)
public int BottomUpFib(int n)
{
if (n <= 1) return n;
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++)
{
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
54. Longest Common Subsequence (LCS)
public class LongestCommonSubsequence
{
public int LCS(string text1, string text2)
{
int m = text1.Length, n = text2.Length;
int[,] dp = new int[m + 1, n + 1];
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
if (text1[i - 1] == text2[j - 1])
{
dp[i, j] = dp[i - 1, j - 1] + 1;
}
else
{
dp[i, j] = Math.Max(dp[i - 1, j], dp[i, j - 1]);
}
}
}
return dp[m, n];
}
// Space-optimized version
public int LCSOptimized(string text1, string text2)
{
int m = text1.Length, n = text2.Length;
int[] dp = new int[n + 1];
for (int i = 1; i <= m; i++)
{
int prev = 0;
for (int j = 1; j <= n; j++)
{
int temp = dp[j];
if (text1[i - 1] == text2[j - 1])
{
dp[j] = prev + 1;
}
else
{
dp[j] = Math.Max(dp[j], dp[j - 1]);
}
prev = temp;
}
}
return dp[n];
}
}
55. Greedy vs Dynamic Programming
public class GreedyVsDynamicProgramming
{
// Greedy approach - Coin change (works for certain coin denominations)
public int CoinChangeGreedy(int amount, int[] coins)
{
Array.Sort(coins, (a, b) => b.CompareTo(a)); // Sort descending
int count = 0;
foreach (int coin in coins)
{
while (amount >= coin)
{
amount -= coin;
count++;
}
}
return amount == 0 ? count : -1;
}
// Dynamic Programming approach - Always optimal
public int CoinChangeDP(int amount, int[] coins)
{
int[] dp = new int[amount + 1];
Array.Fill(dp, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++)
{
foreach (int coin in coins)
{
if (coin <= i)
{
dp[i] = Math.Min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
56. Knapsack Problem
public class KnapsackProblem
{
public int Knapsack01(int[] weights, int[] values, int capacity)
{
int n = weights.Length;
int[,] dp = new int[n + 1, capacity + 1];
for (int i = 1; i <= n; i++)
{
for (int w = 0; w <= capacity; w++)
{
if (weights[i - 1] <= w)
{
dp[i, w] = Math.Max(
dp[i - 1, w], // Don't include item i
dp[i - 1, w - weights[i - 1]] + values[i - 1] // Include item i
);
}
else
{
dp[i, w] = dp[i - 1, w];
}
}
}
return dp[n, capacity];
}
// Space-optimized version
public int Knapsack01Optimized(int[] weights, int[] values, int capacity)
{
int n = weights.Length;
int[] dp = new int[capacity + 1];
for (int i = 0; i < n; i++)
{
for (int w = capacity; w >= weights[i]; w--)
{
dp[w] = Math.Max(dp[w], dp[w - weights[i]] + values[i]);
}
}
return dp[capacity];
}
}
57. Optimal Substructure vs Overlapping Subproblems
public class OptimalSubstructureVsOverlapping
{
// Example showing both properties
public int FibonacciWithBothProperties(int n)
{
// Overlapping subproblems: fib(5) calls fib(3) multiple times
// Optimal substructure: fib(n) = fib(n-1) + fib(n-2)
if (n <= 1) return n;
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++)
{
dp[i] = dp[i - 1] + dp[i - 2]; // Optimal substructure
}
return dp[n];
}
// Example without overlapping subproblems (but with optimal substructure)
public int MaxPathSum(int[,] grid)
{
int rows = grid.GetLength(0);
int cols = grid.GetLength(1);
int[,] dp = new int[rows, cols];
// Fill first row
dp[0, 0] = grid[0, 0];
for (int j = 1; j < cols; j++)
{
dp[0, j] = dp[0, j - 1] + grid[0, j];
}
// Fill first column
for (int i = 1; i < rows; i++)
{
dp[i, 0] = dp[i - 1, 0] + grid[i, 0];
}
// Fill rest of the grid
for (int i = 1; i < rows; i++)
{
for (int j = 1; j < cols; j++)
{
dp[i, j] = Math.Max(dp[i - 1, j], dp[i, j - 1]) + grid[i, j];
}
}
return dp[rows - 1, cols - 1];
}
}
58. Edit Distance Problem
public class EditDistance
{
public int MinDistance(string word1, string word2)
{
int m = word1.Length, n = word2.Length;
int[,] dp = new int[m + 1, n + 1];
// Initialize first row and column
for (int i = 0; i <= m; i++)
dp[i, 0] = i;
for (int j = 0; j <= n; j++)
dp[0, j] = j;
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
if (word1[i - 1] == word2[j - 1])
{
dp[i, j] = dp[i - 1, j - 1]; // No operation needed
}
else
{
dp[i, j] = 1 + Math.Min(
dp[i - 1, j], // Delete
Math.Min(
dp[i, j - 1], // Insert
dp[i - 1, j - 1] // Replace
)
);
}
}
}
return dp[m, n];
}
// Space-optimized version
public int MinDistanceOptimized(string word1, string word2)
{
int m = word1.Length, n = word2.Length;
int[] dp = new int[n + 1];
for (int j = 0; j <= n; j++)
dp[j] = j;
for (int i = 1; i <= m; i++)
{
int prev = dp[0];
dp[0] = i;
for (int j = 1; j <= n; j++)
{
int temp = dp[j];
if (word1[i - 1] == word2[j - 1])
{
dp[j] = prev;
}
else
{
dp[j] = 1 + Math.Min(dp[j], Math.Min(dp[j - 1], prev));
}
prev = temp;
}
}
return dp[n];
}
}
59. 0/1 Knapsack vs Fractional Knapsack
public class KnapsackComparison
{
// 0/1 Knapsack - Items cannot be broken (Dynamic Programming)
public int Knapsack01(int[] weights, int[] values, int capacity)
{
int n = weights.Length;
int[] dp = new int[capacity + 1];
for (int i = 0; i < n; i++)
{
for (int w = capacity; w >= weights[i]; w--)
{
dp[w] = Math.Max(dp[w], dp[w - weights[i]] + values[i]);
}
}
return dp[capacity];
}
// Fractional Knapsack - Items can be broken (Greedy)
public double FractionalKnapsack(int[] weights, int[] values, int capacity)
{
int n = weights.Length;
var items = new List<(double ratio, int weight, int value)>();
for (int i = 0; i < n; i++)
{
items.Add(((double)values[i] / weights[i], weights[i], values[i]));
}
items.Sort((a, b) => b.ratio.CompareTo(a.ratio)); // Sort by value/weight ratio
double totalValue = 0;
int remainingCapacity = capacity;
foreach (var item in items)
{
if (remainingCapacity >= item.weight)
{
totalValue += item.value;
remainingCapacity -= item.weight;
}
else
{
totalValue += item.ratio * remainingCapacity;
break;
}
}
return totalValue;
}
}
60. Coin Change Problem
public class CoinChange
{
// Minimum number of coins needed
public int CoinChangeMin(int[] coins, int amount)
{
int[] dp = new int[amount + 1];
Array.Fill(dp, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++)
{
foreach (int coin in coins)
{
if (coin <= i)
{
dp[i] = Math.Min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
// Number of ways to make change
public int CoinChangeWays(int[] coins, int amount)
{
int[] dp = new int[amount + 1];
dp[0] = 1;
foreach (int coin in coins)
{
for (int i = coin; i <= amount; i++)
{
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
// All possible combinations
public List<List<int>> CoinChangeCombinations(int[] coins, int amount)
{
var result = new List<List<int>>();
var current = new List<int>();
void Backtrack(int remaining, int start)
{
if (remaining == 0)
{
result.Add(new List<int>(current));
return;
}
if (remaining < 0) return;
for (int i = start; i < coins.Length; i++)
{
current.Add(coins[i]);
Backtrack(remaining - coins[i], i);
current.RemoveAt(current.Count - 1);
}
}
Backtrack(amount, 0);
return result;
}
}
Summary of Key Concepts:
- Memoization vs Tabulation: Memoization is recursive with caching, tabulation is iterative with table building
- Top-down vs Bottom-up: Top-down starts from target problem, bottom-up starts from base cases
- Greedy vs DP: Greedy makes locally optimal choices, DP considers all possibilities
- 0/1 vs Fractional Knapsack: 0/1 requires DP, fractional can use greedy approach
- Optimal Substructure: Solution to problem can be constructed from solutions to subproblems
- Overlapping Subproblems: Same subproblems are solved multiple times
I'll act as a technical architect and provide comprehensive answers to these graph theory and algorithm questions with C# implementations. Let me address each question systematically.
61. Difference between Dijkstra's and Bellman-Ford algorithms
Dijkstra's Algorithm: - Works only with non-negative edge weights - Uses a greedy approach with priority queue - Time complexity: O((V + E) log V) with binary heap - Cannot detect negative cycles
Bellman-Ford Algorithm: - Works with negative edge weights (but not negative cycles) - Uses dynamic programming approach - Time complexity: O(VE) - Can detect negative cycles
public class GraphAlgorithms
{
// Dijkstra's Algorithm
public static Dictionary<int, int> Dijkstra(Dictionary<int, List<(int to, int weight)>> graph, int start)
{
var distances = new Dictionary<int, int>();
var pq = new PriorityQueue<int, int>();
var visited = new HashSet<int>();
// Initialize distances
foreach (var vertex in graph.Keys)
{
distances[vertex] = int.MaxValue;
}
distances[start] = 0;
pq.Enqueue(start, 0);
while (pq.Count > 0)
{
var current = pq.Dequeue();
if (visited.Contains(current)) continue;
visited.Add(current);
foreach (var (neighbor, weight) in graph[current])
{
if (distances[current] + weight < distances[neighbor])
{
distances[neighbor] = distances[current] + weight;
pq.Enqueue(neighbor, distances[neighbor]);
}
}
}
return distances;
}
// Bellman-Ford Algorithm
public static (Dictionary<int, int> distances, bool hasNegativeCycle) BellmanFord(
List<(int from, int to, int weight)> edges, int vertices, int start)
{
var distances = new Dictionary<int, int>();
// Initialize distances
for (int i = 0; i < vertices; i++)
{
distances[i] = int.MaxValue;
}
distances[start] = 0;
// Relax edges V-1 times
for (int i = 0; i < vertices - 1; i++)
{
foreach (var (from, to, weight) in edges)
{
if (distances[from] != int.MaxValue &&
distances[from] + weight < distances[to])
{
distances[to] = distances[from] + weight;
}
}
}
// Check for negative cycles
bool hasNegativeCycle = false;
foreach (var (from, to, weight) in edges)
{
if (distances[from] != int.MaxValue &&
distances[from] + weight < distances[to])
{
hasNegativeCycle = true;
break;
}
}
return (distances, hasNegativeCycle);
}
}
62. Kruskal's Minimum Spanning Tree Algorithm
public class KruskalMST
{
public class Edge : IComparable<Edge>
{
public int From { get; set; }
public int To { get; set; }
public int Weight { get; set; }
public int CompareTo(Edge other) => Weight.CompareTo(other.Weight);
}
public class UnionFind
{
private int[] parent;
private int[] rank;
public UnionFind(int size)
{
parent = new int[size];
rank = new int[size];
for (int i = 0; i < size; i++)
{
parent[i] = i;
}
}
public int Find(int x)
{
if (parent[x] != x)
{
parent[x] = Find(parent[x]); // Path compression
}
return parent[x];
}
public void Union(int x, int y)
{
int rootX = Find(x);
int rootY = Find(y);
if (rootX == rootY) return;
if (rank[rootX] < rank[rootY])
{
parent[rootX] = rootY;
}
else if (rank[rootX] > rank[rootY])
{
parent[rootY] = rootX;
}
else
{
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
public static List<Edge> Kruskal(List<Edge> edges, int vertices)
{
var result = new List<Edge>();
var uf = new UnionFind(vertices);
// Sort edges by weight
edges.Sort();
foreach (var edge in edges)
{
if (uf.Find(edge.From) != uf.Find(edge.To))
{
result.Add(edge);
uf.Union(edge.From, edge.To);
}
}
return result;
}
}
63. Difference between Spanning Tree and Minimum Spanning Tree
Spanning Tree: - A subgraph that connects all vertices without cycles - May not have minimum total weight - Multiple spanning trees possible
Minimum Spanning Tree (MST): - A spanning tree with minimum total edge weight - Unique if all edge weights are distinct - Optimal solution for connecting all vertices
public class SpanningTreeComparison
{
public class Graph
{
public int Vertices { get; set; }
public List<(int from, int to, int weight)> Edges { get; set; } = new();
}
// Any spanning tree (not necessarily minimum)
public static List<(int from, int to)> FindAnySpanningTree(Graph graph)
{
var result = new List<(int from, int to)>();
var uf = new KruskalMST.UnionFind(graph.Vertices);
foreach (var edge in graph.Edges)
{
if (uf.Find(edge.from) != uf.Find(edge.to))
{
result.Add((edge.from, edge.to));
uf.Union(edge.from, edge.to);
}
}
return result;
}
// Minimum spanning tree
public static List<(int from, int to, int weight)> FindMinimumSpanningTree(Graph graph)
{
var edges = graph.Edges.Select(e => new KruskalMST.Edge
{
From = e.from,
To = e.to,
Weight = e.weight
}).ToList();
var mstEdges = KruskalMST.Kruskal(edges, graph.Vertices);
return mstEdges.Select(e => (e.From, e.To, e.Weight)).ToList();
}
}
64. Prim's Minimum Spanning Tree Algorithm
public class PrimMST
{
public static List<(int from, int to, int weight)> Prim(Dictionary<int, List<(int to, int weight)>> graph, int start)
{
var result = new List<(int from, int to, int weight)>();
var pq = new PriorityQueue<(int from, int to, int weight), int>();
var visited = new HashSet<int>();
// Add starting vertex
visited.Add(start);
foreach (var (to, weight) in graph[start])
{
pq.Enqueue((start, to, weight), weight);
}
while (pq.Count > 0 && visited.Count < graph.Count)
{
var (from, to, weight) = pq.Dequeue();
if (visited.Contains(to)) continue;
visited.Add(to);
result.Add((from, to, weight));
// Add edges from the new vertex
foreach (var (nextTo, nextWeight) in graph[to])
{
if (!visited.Contains(nextTo))
{
pq.Enqueue((to, nextTo, nextWeight), nextWeight);
}
}
}
return result;
}
}
65. Difference between Tree and Forest
Tree: - Connected acyclic graph - Single component - n-1 edges for n vertices
Forest: - Collection of trees (disconnected components) - Multiple components - Each component is a tree
public class TreeForestComparison
{
public class Graph
{
public int Vertices { get; set; }
public Dictionary<int, List<int>> AdjacencyList { get; set; } = new();
}
public static bool IsTree(Graph graph)
{
if (graph.Vertices == 0) return true;
var visited = new HashSet<int>();
var hasCycle = false;
// Check if connected and acyclic
DFS(0, -1, graph, visited, ref hasCycle);
return !hasCycle && visited.Count == graph.Vertices;
}
public static int CountTreesInForest(Graph graph)
{
var visited = new HashSet<int>();
int treeCount = 0;
for (int i = 0; i < graph.Vertices; i++)
{
if (!visited.Contains(i))
{
DFS(i, -1, graph, visited, ref treeCount);
treeCount++;
}
}
return treeCount;
}
private static void DFS(int current, int parent, Graph graph, HashSet<int> visited, ref bool hasCycle)
{
visited.Add(current);
foreach (var neighbor in graph.AdjacencyList.GetValueOrDefault(current, new List<int>()))
{
if (neighbor == parent) continue;
if (visited.Contains(neighbor))
{
hasCycle = true;
return;
}
DFS(neighbor, current, graph, visited, ref hasCycle);
}
}
private static void DFS(int current, int parent, Graph graph, HashSet<int> visited, ref int treeCount)
{
visited.Add(current);
foreach (var neighbor in graph.AdjacencyList.GetValueOrDefault(current, new List<int>()))
{
if (neighbor != parent && !visited.Contains(neighbor))
{
DFS(neighbor, current, graph, visited, ref treeCount);
}
}
}
}
66. Topological Sorting Implementation
public class TopologicalSort
{
public static List<int> TopologicalSortDFS(Dictionary<int, List<int>> graph)
{
var visited = new HashSet<int>();
var tempVisited = new HashSet<int>();
var result = new List<int>();
foreach (var vertex in graph.Keys)
{
if (!visited.Contains(vertex))
{
if (!DFS(vertex, graph, visited, tempVisited, result))
{
throw new InvalidOperationException("Graph contains a cycle");
}
}
}
result.Reverse();
return result;
}
public static List<int> TopologicalSortKahn(Dictionary<int, List<int>> graph)
{
var inDegree = new Dictionary<int, int>();
var queue = new Queue<int>();
var result = new List<int>();
// Calculate in-degrees
foreach (var vertex in graph.Keys)
{
inDegree[vertex] = 0;
}
foreach (var neighbors in graph.Values)
{
foreach (var neighbor in neighbors)
{
inDegree[neighbor]++;
}
}
// Add vertices with in-degree 0
foreach (var vertex in graph.Keys)
{
if (inDegree[vertex] == 0)
{
queue.Enqueue(vertex);
}
}
while (queue.Count > 0)
{
var current = queue.Dequeue();
result.Add(current);
foreach (var neighbor in graph.GetValueOrDefault(current, new List<int>()))
{
inDegree[neighbor]--;
if (inDegree[neighbor] == 0)
{
queue.Enqueue(neighbor);
}
}
}
if (result.Count != graph.Count)
{
throw new InvalidOperationException("Graph contains a cycle");
}
return result;
}
private static bool DFS(int vertex, Dictionary<int, List<int>> graph,
HashSet<int> visited, HashSet<int> tempVisited, List<int> result)
{
if (tempVisited.Contains(vertex))
return false; // Cycle detected
if (visited.Contains(vertex))
return true;
tempVisited.Add(vertex);
foreach (var neighbor in graph.GetValueOrDefault(vertex, new List<int>()))
{
if (!DFS(neighbor, graph, visited, tempVisited, result))
return false;
}
tempVisited.Remove(vertex);
visited.Add(vertex);
result.Add(vertex);
return true;
}
}
67. Difference between Cycle and Circuit
Cycle: - Path that starts and ends at the same vertex - No repeated vertices except start/end - Simple cycle
Circuit: - Path that starts and ends at the same vertex - May have repeated vertices - More general term
public class CycleCircuitComparison
{
public class Graph
{
public int Vertices { get; set; }
public Dictionary<int, List<int>> AdjacencyList { get; set; } = new();
}
public static bool HasCycle(Graph graph)
{
var visited = new HashSet<int>();
var recStack = new HashSet<int>();
for (int i = 0; i < graph.Vertices; i++)
{
if (!visited.Contains(i))
{
if (IsCyclicUtil(i, graph, visited, recStack))
return true;
}
}
return false;
}
public static List<List<int>> FindAllCycles(Graph graph)
{
var cycles = new List<List<int>>();
var visited = new HashSet<int>();
var path = new List<int>();
for (int i = 0; i < graph.Vertices; i++)
{
if (!visited.Contains(i))
{
FindCyclesDFS(i, i, graph, visited, path, cycles);
}
}
return cycles;
}
public static List<List<int>> FindAllCircuits(Graph graph)
{
var circuits = new List<List<int>>();
var visited = new Dictionary<int, int>(); // vertex -> count
for (int i = 0; i < graph.Vertices; i++)
{
FindCircuitsDFS(i, i, graph, visited, new List<int>(), circuits);
}
return circuits;
}
private static bool IsCyclicUtil(int vertex, Graph graph, HashSet<int> visited, HashSet<int> recStack)
{
if (recStack.Contains(vertex))
return true;
if (visited.Contains(vertex))
return false;
visited.Add(vertex);
recStack.Add(vertex);
foreach (var neighbor in graph.AdjacencyList.GetValueOrDefault(vertex, new List<int>()))
{
if (IsCyclicUtil(neighbor, graph, visited, recStack))
return true;
}
recStack.Remove(vertex);
return false;
}
private static void FindCyclesDFS(int start, int current, Graph graph,
HashSet<int> visited, List<int> path, List<List<int>> cycles)
{
path.Add(current);
if (path.Count > 1 && current == start)
{
cycles.Add(new List<int>(path));
path.RemoveAt(path.Count - 1);
return;
}
visited.Add(current);
foreach (var neighbor in graph.AdjacencyList.GetValueOrDefault(current, new List<int>()))
{
if (neighbor == start && path.Count > 2)
{
FindCyclesDFS(start, neighbor, graph, visited, path, cycles);
}
else if (!visited.Contains(neighbor))
{
FindCyclesDFS(start, neighbor, graph, visited, path, cycles);
}
}
visited.Remove(current);
path.RemoveAt(path.Count - 1);
}
private static void FindCircuitsDFS(int start, int current, Graph graph,
Dictionary<int, int> visited, List<int> path, List<List<int>> circuits)
{
path.Add(current);
visited[current] = visited.GetValueOrDefault(current, 0) + 1;
if (current == start && path.Count > 1)
{
circuits.Add(new List<int>(path));
}
foreach (var neighbor in graph.AdjacencyList.GetValueOrDefault(current, new List<int>()))
{
if (visited.GetValueOrDefault(neighbor, 0) < 2) // Allow some repetition
{
FindCircuitsDFS(start, neighbor, graph, visited, path, circuits);
}
}
visited[current]--;
path.RemoveAt(path.Count - 1);
}
}
68. Strongly Connected Components Implementation
public class StronglyConnectedComponents
{
public static List<List<int>> FindSCCs(Dictionary<int, List<int>> graph)
{
var visited = new HashSet<int>();
var stack = new Stack<int>();
var sccs = new List<List<int>>();
// First DFS to fill stack
foreach (var vertex in graph.Keys)
{
if (!visited.Contains(vertex))
{
DFS1(vertex, graph, visited, stack);
}
}
// Create transpose graph
var transpose = CreateTranspose(graph);
// Second DFS on transpose
visited.Clear();
while (stack.Count > 0)
{
var vertex = stack.Pop();
if (!visited.Contains(vertex))
{
var scc = new List<int>();
DFS2(vertex, transpose, visited, scc);
sccs.Add(scc);
}
}
return sccs;
}
private static void DFS1(int vertex, Dictionary<int, List<int>> graph,
HashSet<int> visited, Stack<int> stack)
{
visited.Add(vertex);
foreach (var neighbor in graph.GetValueOrDefault(vertex, new List<int>()))
{
if (!visited.Contains(neighbor))
{
DFS1(neighbor, graph, visited, stack);
}
}
stack.Push(vertex);
}
private static void DFS2(int vertex, Dictionary<int, List<int>> graph,
HashSet<int> visited, List<int> scc)
{
visited.Add(vertex);
scc.Add(vertex);
foreach (var neighbor in graph.GetValueOrDefault(vertex, new List<int>()))
{
if (!visited.Contains(neighbor))
{
DFS2(neighbor, graph, visited, scc);
}
}
}
private static Dictionary<int, List<int>> CreateTranspose(Dictionary<int, List<int>> graph)
{
var transpose = new Dictionary<int, List<int>>();
foreach (var vertex in graph.Keys)
{
transpose[vertex] = new List<int>();
}
foreach (var kvp in graph)
{
foreach (var neighbor in kvp.Value)
{
transpose[neighbor].Add(kvp.Key);
}
}
return transpose;
}
// Tarjan's algorithm for SCC
public static List<List<int>> FindSCCsTarjan(Dictionary<int, List<int>> graph)
{
var sccs = new List<List<int>>();
var visited = new HashSet<int>();
var onStack = new HashSet<int>();
var low = new Dictionary<int, int>();
var disc = new Dictionary<int, int>();
var stack = new Stack<int>();
int time = 0;
foreach (var vertex in graph.Keys)
{
if (!visited.Contains(vertex))
{
TarjanDFS(vertex, graph, visited, onStack, low, disc, stack, sccs, ref time);
}
}
return sccs;
}
private static void TarjanDFS(int vertex, Dictionary<int, List<int>> graph,
HashSet<int> visited, HashSet<int> onStack, Dictionary<int, int> low,
Dictionary<int, int> disc, Stack<int> stack, List<List<int>> sccs, ref int time)
{
visited.Add(vertex);
onStack.Add(vertex);
stack.Push(vertex);
disc[vertex] = low[vertex] = ++time;
foreach (var neighbor in graph.GetValueOrDefault(vertex, new List<int>()))
{
if (!visited.Contains(neighbor))
{
TarjanDFS(neighbor, graph, visited, onStack, low, disc, stack, sccs, ref time);
low[vertex] = Math.Min(low[vertex], low[neighbor]);
}
else if (onStack.Contains(neighbor))
{
low[vertex] = Math.Min(low[vertex], disc[neighbor]);
}
}
if (low[vertex] == disc[vertex])
{
var scc = new List<int>();
int w;
do
{
w = stack.Pop();
onStack.Remove(w);
scc.Add(w);
} while (w != vertex);
sccs.Add(scc);
}
}
}
69. Difference between Path and Walk
Path: - Sequence of vertices where no vertex is repeated - Simple path - No cycles within the path
Walk: - Sequence of vertices where vertices can be repeated - May contain cycles - More general than path
public class PathWalkComparison
{
public class Graph
{
public int Vertices { get; set; }
public Dictionary<int, List<int>> AdjacencyList { get; set; } = new();
}
public static List<List<int>> FindAllPaths(Graph graph, int start, int end)
{
var paths = new List<List<int>>();
var visited = new HashSet<int>();
FindPathsDFS(start, end, graph, visited, new List<int>(), paths);
return paths;
}
public static List<List<int>> FindAllWalks(Graph graph, int start, int end, int maxLength)
{
var walks = new List<List<int>>();
FindWalksDFS(start, end, graph, new List<int>(), walks, maxLength);
return walks;
}
public static bool IsPath(List<int> sequence, Graph graph)
{
// Check if sequence is a valid path (no repeated vertices)
var visited = new HashSet<int>();
foreach (var vertex in sequence)
{
if (visited.Contains(vertex))
return false;
visited.Add(vertex);
}
// Check if consecutive vertices are connected
for (int i = 0; i < sequence.Count - 1; i++)
{
if (!graph.AdjacencyList.GetValueOrDefault(sequence[i], new List<int>())
.Contains(sequence[i + 1]))
{
return false;
}
}
return true;
}
public static bool IsWalk(List<int> sequence, Graph graph)
{
// Check if consecutive vertices are connected
for (int i = 0; i < sequence.Count - 1; i++)
{
if (!graph.AdjacencyList.GetValueOrDefault(sequence[i], new List<int>())
.Contains(sequence[i + 1]))
{
return false;
}
}
return true;
}
private static void FindPathsDFS(int current, int end, Graph graph,
HashSet<int> visited, List<int> path, List<List<int>> paths)
{
path.Add(current);
visited.Add(current);
if (current == end)
{
paths.Add(new List<int>(path));
}
else
{
foreach (var neighbor in graph.AdjacencyList.GetValueOrDefault(current, new List<int>()))
{
if (!visited.Contains(neighbor))
{
FindPathsDFS(neighbor, end, graph, visited, path, paths);
}
}
}
visited.Remove(current);
path.RemoveAt(path.Count - 1);
}
private static void FindWalksDFS(int current, int end, Graph graph,
List<int> walk, List<List<int>> walks, int maxLength)
{
walk.Add(current);
if (current == end && walk.Count > 1)
{
walks.Add(new List<int>(walk));
}
if (walk.Count < maxLength)
{
foreach (var neighbor in graph.AdjacencyList.GetValueOrDefault(current, new List<int>()))
{
FindWalksDFS(neighbor, end, graph, walk, walks, maxLength);
}
}
walk.RemoveAt(walk.Count - 1);
}
}
70. Network Flow Algorithms Implementation
public class NetworkFlow
{
public class FlowNetwork
{
public int Vertices { get; set; }
public Dictionary<int, Dictionary<int, int>> Capacity { get; set; } = new();
public Dictionary<int, Dictionary<int, int>> Flow { get; set; } = new();
}
// Ford-Fulkerson with DFS
public static int FordFulkerson(FlowNetwork network, int source, int sink)
{
var maxFlow = 0;
var path = new List<int>();
while (FindAugmentingPath(network, source, sink, path))
{
int pathFlow = int.MaxValue;
// Find minimum capacity in the path
for (int i = 0; i < path.Count - 1; i++)
{
int u = path[i];
int v = path[i + 1];
int residualCapacity = network.Capacity[u][v] -
network.Flow.GetValueOrDefault(u, new Dictionary<int, int>()).GetValueOrDefault(v, 0);
pathFlow = Math.Min(pathFlow, residualCapacity);
}
// Update flow
for (int i = 0; i < path.Count - 1; i++)
{
int u = path[i];
int v = path[i + 1];
if (!network.Flow.ContainsKey(u))
network.Flow[u] = new Dictionary<int, int>();
if (!network.Flow.ContainsKey(v))
network.Flow[v] = new Dictionary<int, int>();
network.Flow[u][v] = network.Flow[u].GetValueOrDefault(v, 0) + pathFlow;
network.Flow[v][u] = network.Flow[v].GetValueOrDefault(u, 0) - pathFlow;
}
maxFlow += pathFlow;
}
return maxFlow;
}
// Edmonds-Karp (Ford-Fulkerson with BFS)
public static int EdmondsKarp(FlowNetwork network, int source, int sink)
{
var maxFlow = 0;
var parent = new int[network.Vertices];
while (BFS(network, source, sink, parent))
{
int pathFlow = int.MaxValue;
// Find minimum capacity in the path
for (int v = sink; v != source; v = parent[v])
{
int u = parent[v];
int residualCapacity = network.Capacity[u][v] -
network.Flow.GetValueOrDefault(u, new Dictionary<int, int>()).GetValueOrDefault(v, 0);
pathFlow = Math.Min(pathFlow, residualCapacity);
}
// Update flow
for (int v = sink; v != source; v = parent[v])
{
int u = parent[v];
if (!network.Flow.ContainsKey(u))
network.Flow[u] = new Dictionary<int, int>();
if (!network.Flow.ContainsKey(v))
network.Flow[v] = new Dictionary<int, int>();
network.Flow[u][v] = network.Flow[u].GetValueOrDefault(v, 0) + pathFlow;
network.Flow[v][u] = network.Flow[v].GetValueOrDefault(u, 0) - pathFlow;
}
maxFlow += pathFlow;
}
return maxFlow;
}
// Dinic's Algorithm
public static int Dinic(FlowNetwork network, int source, int sink)
{
var maxFlow = 0;
var level = new int[network.Vertices];
while (BFSLevel(network, source, sink, level))
{
var start = new int[network.Vertices];
int flow;
do
{
flow = SendFlow(network, source, sink, int.MaxValue, start, level);
maxFlow += flow;
} while (flow > 0);
}
return maxFlow;
}
private static bool FindAugmentingPath(FlowNetwork network, int source, int sink, List<int> path)
{
var visited = new bool[network.Vertices];
var parent = new int[network.Vertices];
var queue = new Queue<int>();
queue.Enqueue(source);
visited[source] = true;
parent[source] = -1;
while (queue.Count > 0)
{
int u = queue.Dequeue();
foreach (var kvp in network.Capacity.GetValueOrDefault(u, new Dictionary<int, int>()))
{
int v = kvp.Key;
int residualCapacity = kvp.Value -
network.Flow.GetValueOrDefault(u, new Dictionary<int, int>()).GetValueOrDefault(v, 0);
if (!visited[v] && residualCapacity > 0)
{
visited[v] = true;
parent[v] = u;
queue.Enqueue(v);
}
}
}
if (!visited[sink])
return false;
// Reconstruct path
path.Clear();
for (int v = sink; v != -1; v = parent[v])
{
path.Insert(0, v);
}
return true;
}
private static bool BFS(FlowNetwork network, int source, int sink, int[] parent)
{
var visited = new bool[network.Vertices];
var queue = new Queue<int>();
queue.Enqueue(source);
visited[source] = true;
parent[source] = -1;
while (queue.Count > 0)
{
int u = queue.Dequeue();
foreach (var kvp in network.Capacity.GetValueOrDefault(u, new Dictionary<int, int>()))
{
int v = kvp.Key;
int residualCapacity = kvp.Value -
network.Flow.GetValueOrDefault(u, new Dictionary<int, int>()).GetValueOrDefault(v, 0);
if (!visited[v] && residualCapacity > 0)
{
visited[v] = true;
parent[v] = u;
queue.Enqueue(v);
}
}
}
return visited[sink];
}
private static bool BFSLevel(FlowNetwork network, int source, int sink, int[] level)
{
Array.Fill(level, -1);
var queue = new Queue<int>();
level[source] = 0;
queue.Enqueue(source);
while (queue.Count > 0)
{
int u = queue.Dequeue();
foreach (var kvp in network.Capacity.GetValueOrDefault(u, new Dictionary<int, int>()))
{
int v = kvp.Key;
int residualCapacity = kvp.Value -
network.Flow.GetValueOrDefault(u, new Dictionary<int, int>()).GetValueOrDefault(v, 0);
if (level[v] == -1 && residualCapacity > 0)
{
level[v] = level[u] + 1;
queue.Enqueue(v);
}
}
}
return level[sink] != -1;
}
private static int SendFlow(FlowNetwork network, int u, int sink, int flow, int[] start, int[] level)
{
if (u == sink)
return flow;
for (; start[u] < network.Capacity.GetValueOrDefault(u, new Dictionary<int, int>()).Count; start[u]++)
{
var kvp = network.Capacity[u].ElementAt(start[u]);
int v = kvp.Key;
int residualCapacity = kvp.Value -
network.Flow.GetValueOrDefault(u, new Dictionary<int, int>()).GetValueOrDefault(v, 0);
if (level[v] == level[u] + 1 && residualCapacity > 0)
{
int currentFlow = Math.Min(flow, residualCapacity);
int tempFlow = SendFlow(network, v, sink, currentFlow, start, level);
if (tempFlow > 0)
{
if (!network.Flow.ContainsKey(u))
network.Flow[u] = new Dictionary<int, int>();
if (!network.Flow.ContainsKey(v))
network.Flow[v] = new Dictionary<int, int>();
network.Flow[u][v] = network.Flow[u].GetValueOrDefault(v, 0) + tempFlow;
network.Flow[v][u] = network.Flow[v].GetValueOrDefault(u, 0) - tempFlow;
return tempFlow;
}
}
}
return 0;
}
}
Summary
These implementations cover the key graph theory algorithms and concepts:
- Dijkstra vs Bellman-Ford: Different approaches for shortest paths with different constraints
- Kruskal's MST: Uses Union-Find with edge sorting
- Spanning Tree vs MST: MST is optimal spanning tree
- Prim's MST: Greedy approach with priority queue
- Tree vs Forest: Connected vs disconnected components
- Topological Sort: DFS and Kahn's algorithm
- Cycle vs Circuit: Simple vs general cycles
- SCC: Kosaraju's and Tarjan's algorithms
- Path vs Walk: No repetition vs possible repetition
- Network Flow: Ford-Fulkerson, Edmonds-Karp, and Dinic's algorithms
Each implementation includes proper error handling, efficient data structures, and follows C# best practices. The algorithms are optimized for performance and readability, making them suitable for technical architecture discussions and interviews.
String Algorithms Interview Questions - Technical Architect Guide
Overview
This document provides comprehensive answers to 10 advanced string algorithm questions commonly asked in technical architect interviews. Each answer includes implementation details, time complexity analysis, and practical considerations.
Question 71: String Matching vs Pattern Matching
Key Differences:
- String Matching: Finding exact occurrences of a substring within a larger string
- Pattern Matching: Finding strings that match a specific pattern using wildcards, regex, or other pattern rules
Implementation Details:
// Simple String Matching - O(n*m) time complexity
public static int SimpleStringMatch(string text, string pattern)
// Pattern Matching with Wildcards - O(n*m) time complexity
public static bool PatternMatch(string text, string pattern)
Interview Tips:
- String Matching: Use for exact substring searches
- Pattern Matching: Use for flexible matching with rules
- Real-world applications: Text editors, search engines, data validation
Question 72: KMP (Knuth-Morris-Pratt) Algorithm
Algorithm Overview:
KMP uses a failure function (LPS - Longest Proper Prefix that is also Suffix) to avoid unnecessary comparisons.
Key Features:
- Time Complexity: O(n + m) where n = text length, m = pattern length
- Space Complexity: O(m) for LPS array
- Best for: Multiple pattern searches, DNA sequence matching
Implementation Strategy:
- Build LPS array for the pattern
- Use LPS to skip comparisons when mismatch occurs
- Linear time scanning of text
Interview Tips:
- Explain the LPS array concept clearly
- Mention that it's optimal for single pattern matching
- Compare with Boyer-Moore for multiple patterns
Question 73: Exact vs Approximate String Matching
Exact Matching:
- Character-by-character comparison
- Time Complexity: O(n)
- Use cases: Password validation, exact search
Approximate Matching:
- Allows for differences (insertions, deletions, substitutions)
- Uses edit distance metrics (Levenshtein, Hamming, etc.)
- Time Complexity: O(n*m) for dynamic programming approach
Levenshtein Distance Implementation:
public static int LevenshteinDistance(string s1, string s2)
- Operations: Insert, Delete, Substitute
- Applications: Spell checkers, fuzzy search, DNA sequence alignment
Interview Tips:
- Explain when to use each approach
- Discuss trade-offs between accuracy and performance
- Mention optimization techniques (early termination, bounded distance)
Question 74: Boyer-Moore Algorithm
Algorithm Features:
- Two Heuristics: Bad Character Rule and Good Suffix Rule
- Time Complexity: O(n/m) in best case, O(n*m) in worst case
- Space Complexity: O(k) where k is alphabet size
Implementation Strategy:
- Bad Character Table: Precompute character positions in pattern
- Good Suffix Table: Precompute suffix matching information
- Right-to-left comparison: Start from end of pattern
Advantages:
- Very efficient for large alphabets
- Excellent for multiple pattern matching
- Practical performance often better than theoretical worst case
Interview Tips:
- Explain why right-to-left comparison is beneficial
- Discuss the two heuristics in detail
- Compare with KMP for different use cases
Question 75: Substring vs Subsequence
Substring:
- Definition: Consecutive characters from original string
- Example: "CDE" is a substring of "ABCDEF"
- Time Complexity: O(n*m) for naive search
Subsequence:
- Definition: Characters in order but not necessarily consecutive
- Example: "ACE" is a subsequence of "ABCDEF"
- Time Complexity: O(n) for checking if one string is subsequence of another
Key Differences:
| Aspect | Substring | Subsequence |
|---|---|---|
| Continuity | Must be consecutive | Can be non-consecutive |
| Search Complexity | O(n*m) | O(n) |
| Applications | Text search, pattern matching | DNA analysis, sequence alignment |
Interview Tips:
- Use clear examples to illustrate differences
- Discuss applications in bioinformatics
- Explain the recursive approach for generating all subsequences
Question 76: Rabin-Karp Algorithm
Algorithm Overview:
Uses hashing to compare strings efficiently, particularly useful for multiple pattern matching.
Key Features:
- Time Complexity: O(n + m) average case, O(n*m) worst case
- Space Complexity: O(1) additional space
- Best for: Multiple pattern matching, plagiarism detection
Implementation Strategy:
- Compute hash of pattern
- Compute rolling hash of text windows
- Compare hashes, verify with character-by-character comparison if needed
Advantages:
- Efficient for multiple patterns
- Rolling hash allows constant-time window updates
- Good for detecting duplicate content
Interview Tips:
- Explain the rolling hash concept
- Discuss hash collision handling
- Mention applications in plagiarism detection
Question 77: Palindrome vs Anagram
Palindrome:
- Definition: Reads the same forwards and backwards
- Examples: "racecar", "madam", "level"
- Time Complexity: O(n) for checking
Anagram:
- Definition: Contains same characters in different order
- Examples: "listen" and "silent", "debit card" and "bad credit"
- Time Complexity: O(n) for checking
Implementation Approaches:
// Palindrome: Two-pointer approach
public static bool IsPalindrome(string text)
// Anagram: Character frequency counting
public static bool AreAnagrams(string s1, string s2)
Interview Tips:
- Discuss case sensitivity and whitespace handling
- Explain the character frequency approach for anagrams
- Mention real-world applications (palindrome: DNA sequences, anagrams: word games)
Question 78: Longest Palindromic Substring
Algorithm Options:
- Manacher's Algorithm: O(n) time complexity
- Dynamic Programming: O(n²) time complexity
- Expand Around Center: O(n²) time complexity
Manacher's Algorithm (Optimal):
- Time Complexity: O(n)
- Space Complexity: O(n)
- Key Insight: Uses previously computed palindrome information
Implementation Strategy:
- Transform string to handle even-length palindromes
- Use center expansion with optimization
- Track palindrome radius at each position
Interview Tips:
- Explain the transformation step clearly
- Discuss why Manacher's is optimal
- Compare with simpler approaches
Question 79: Prefix vs Suffix
Definitions:
- Prefix: Characters at the beginning of a string
- Suffix: Characters at the end of a string
Applications:
- Prefix: Autocomplete, trie data structures, string matching
- Suffix: Suffix arrays, pattern matching, bioinformatics
Implementation Examples:
public static bool IsPrefix(string text, string prefix)
public static bool IsSuffix(string text, string suffix)
public static List<string> GetAllPrefixes(string text)
public static List<string> GetAllSuffixes(string text)
Interview Tips:
- Discuss applications in data structures (tries, suffix trees)
- Explain how prefixes/suffixes are used in string algorithms
- Mention KMP's use of prefix-suffix matching
Question 80: String Compression Algorithms
Compression Techniques:
1. Run-Length Encoding (RLE):
- Best for: Strings with repeated characters
- Example: "AAABBBCCCC" → "A3B3C4"
- Time Complexity: O(n)
2. Huffman Coding:
- Best for: Variable-length encoding based on frequency
- Time Complexity: O(n log n) for building tree
- Space Complexity: O(k) where k is unique characters
3. Lempel-Ziv (LZ77/LZ78):
- Best for: General-purpose compression
- Used in: ZIP, GIF, PNG formats
Implementation Considerations:
- Compression Ratio: Measure effectiveness
- Encoding/Decoding Speed: Trade-off with compression ratio
- Memory Usage: Important for large datasets
Interview Tips:
- Discuss when to use each compression technique
- Explain compression ratio calculations
- Mention real-world applications (file compression, data transmission)
General Interview Tips
Technical Discussion:
- Always mention time and space complexity
- Explain the algorithm step-by-step
- Provide real-world applications
- Discuss trade-offs and alternatives
Code Quality:
- Handle edge cases (null strings, empty strings)
- Use meaningful variable names
- Add comments for complex logic
- Consider performance optimizations
Problem-Solving Approach:
- Start with brute force solution
- Identify inefficiencies
- Propose optimizations
- Discuss implementation details
Advanced Topics to Mention:
- Suffix Trees and Arrays
- Aho-Corasick Algorithm
- Burrows-Wheeler Transform
- String Matching in Parallel
Performance Comparison
| Algorithm | Best Case | Average Case | Worst Case | Space |
|---|---|---|---|---|
| Naive String Matching | O(n) | O(n*m) | O(n*m) | O(1) |
| KMP | O(n) | O(n+m) | O(n+m) | O(m) |
| Boyer-Moore | O(n/m) | O(n) | O(n*m) | O(k) |
| Rabin-Karp | O(n+m) | O(n+m) | O(n*m) | O(1) |
Where: n = text length, m = pattern length, k = alphabet size
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace StringAlgorithms
{
public class StringAlgorithmsInterview
{
#region Question 71: String Matching vs Pattern Matching
/*
* Question 71: What's the difference between string matching and pattern matching?
*
* String Matching: Finding exact occurrences of a substring within a larger string
* Pattern Matching: Finding strings that match a specific pattern (using wildcards, regex, etc.)
*/
// String Matching Example
public static int SimpleStringMatch(string text, string pattern)
{
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(pattern))
return -1;
for (int i = 0; i <= text.Length - pattern.Length; i++)
{
bool found = true;
for (int j = 0; j < pattern.Length; j++)
{
if (text[i + j] != pattern[j])
{
found = false;
break;
}
}
if (found)
return i;
}
return -1;
}
// Pattern Matching Example (Simple wildcard matching)
public static bool PatternMatch(string text, string pattern)
{
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(pattern))
return false;
return PatternMatchHelper(text, pattern, 0, 0);
}
private static bool PatternMatchHelper(string text, string pattern, int textIndex, int patternIndex)
{
// If we've reached the end of pattern
if (patternIndex == pattern.Length)
return textIndex == text.Length;
// If we've reached the end of text
if (textIndex == text.Length)
return patternIndex == pattern.Length && pattern[patternIndex] == '*';
// Handle wildcard '*'
if (pattern[patternIndex] == '*')
{
return PatternMatchHelper(text, pattern, textIndex + 1, patternIndex) ||
PatternMatchHelper(text, pattern, textIndex, patternIndex + 1);
}
// Handle wildcard '?' or exact match
if (pattern[patternIndex] == '?' || pattern[patternIndex] == text[textIndex])
{
return PatternMatchHelper(text, pattern, textIndex + 1, patternIndex + 1);
}
return false;
}
#endregion
#region Question 72: KMP Algorithm Implementation
/*
* Question 72: How do you implement the KMP (Knuth-Morris-Pratt) algorithm?
*
* KMP is an efficient string matching algorithm that uses a failure function
* to avoid unnecessary comparisons.
*/
public static int KMPMatch(string text, string pattern)
{
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(pattern))
return -1;
int[] lps = ComputeLPSArray(pattern);
int i = 0, j = 0;
while (i < text.Length)
{
if (pattern[j] == text[i])
{
i++;
j++;
}
if (j == pattern.Length)
{
return i - j; // Found pattern
}
else if (i < text.Length && pattern[j] != text[i])
{
if (j != 0)
j = lps[j - 1];
else
i++;
}
}
return -1;
}
private static int[] ComputeLPSArray(string pattern)
{
int[] lps = new int[pattern.Length];
int len = 0;
int i = 1;
while (i < pattern.Length)
{
if (pattern[i] == pattern[len])
{
len++;
lps[i] = len;
i++;
}
else
{
if (len != 0)
len = lps[len - 1];
else
{
lps[i] = 0;
i++;
}
}
}
return lps;
}
#endregion
#region Question 73: Exact vs Approximate String Matching
/*
* Question 73: What's the difference between exact and approximate string matching?
*
* Exact: Strings must match character by character
* Approximate: Strings can differ by a certain number of operations (insert, delete, substitute)
*/
// Exact String Matching
public static bool ExactMatch(string s1, string s2)
{
return s1.Equals(s2, StringComparison.Ordinal);
}
// Approximate String Matching using Levenshtein Distance
public static int LevenshteinDistance(string s1, string s2)
{
int[,] dp = new int[s1.Length + 1, s2.Length + 1];
for (int i = 0; i <= s1.Length; i++)
dp[i, 0] = i;
for (int j = 0; j <= s2.Length; j++)
dp[0, j] = j;
for (int i = 1; i <= s1.Length; i++)
{
for (int j = 1; j <= s2.Length; j++)
{
if (s1[i - 1] == s2[j - 1])
dp[i, j] = dp[i - 1, j - 1];
else
dp[i, j] = 1 + Math.Min(dp[i - 1, j], Math.Min(dp[i, j - 1], dp[i - 1, j - 1]));
}
}
return dp[s1.Length, s2.Length];
}
public static bool ApproximateMatch(string s1, string s2, int maxDistance)
{
return LevenshteinDistance(s1, s2) <= maxDistance;
}
#endregion
#region Question 74: Boyer-Moore Algorithm
/*
* Question 74: How do you implement the Boyer-Moore algorithm?
*
* Boyer-Moore is an efficient string matching algorithm that uses two heuristics:
* 1. Bad Character Rule
* 2. Good Suffix Rule
*/
public static int BoyerMooreMatch(string text, string pattern)
{
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(pattern))
return -1;
int[] badChar = BuildBadCharTable(pattern);
int[] goodSuffix = BuildGoodSuffixTable(pattern);
int i = 0;
while (i <= text.Length - pattern.Length)
{
int j = pattern.Length - 1;
while (j >= 0 && pattern[j] == text[i + j])
j--;
if (j < 0)
return i; // Found pattern
int badCharShift = j - badChar[text[i + j]];
int goodSuffixShift = goodSuffix[j + 1];
i += Math.Max(badCharShift, goodSuffixShift);
}
return -1;
}
private static int[] BuildBadCharTable(string pattern)
{
int[] badChar = new int[256];
for (int i = 0; i < 256; i++)
badChar[i] = -1;
for (int i = 0; i < pattern.Length; i++)
badChar[pattern[i]] = i;
return badChar;
}
private static int[] BuildGoodSuffixTable(string pattern)
{
int[] goodSuffix = new int[pattern.Length + 1];
int[] border = new int[pattern.Length + 1];
// Case 1: Exact match
int i = pattern.Length;
int j = pattern.Length + 1;
border[i] = j;
while (i > 0)
{
while (j <= pattern.Length && pattern[i - 1] != pattern[j - 1])
{
if (goodSuffix[j] == 0)
goodSuffix[j] = j - i;
j = border[j];
}
i--;
j--;
border[i] = j;
}
// Case 2: Good suffix exists
j = border[0];
for (i = 0; i <= pattern.Length; i++)
{
if (goodSuffix[i] == 0)
goodSuffix[i] = j;
if (i == j)
j = border[j];
}
return goodSuffix;
}
#endregion
#region Question 75: Substring vs Subsequence
/*
* Question 75: What's the difference between a substring and a subsequence?
*
* Substring: Consecutive characters from the original string
* Subsequence: Characters in order but not necessarily consecutive
*/
public static bool IsSubstring(string text, string pattern)
{
return text.Contains(pattern);
}
public static bool IsSubsequence(string text, string pattern)
{
int j = 0;
for (int i = 0; i < text.Length && j < pattern.Length; i++)
{
if (text[i] == pattern[j])
j++;
}
return j == pattern.Length;
}
// Find all subsequences
public static List<string> GetAllSubsequences(string text)
{
List<string> result = new List<string>();
GetAllSubsequencesHelper(text, "", 0, result);
return result;
}
private static void GetAllSubsequencesHelper(string text, string current, int index, List<string> result)
{
if (index == text.Length)
{
if (!string.IsNullOrEmpty(current))
result.Add(current);
return;
}
// Include current character
GetAllSubsequencesHelper(text, current + text[index], index + 1, result);
// Exclude current character
GetAllSubsequencesHelper(text, current, index + 1, result);
}
#endregion
#region Question 76: Rabin-Karp Algorithm
/*
* Question 76: How do you implement the Rabin-Karp algorithm?
*
* Rabin-Karp uses hashing to compare strings efficiently.
* It's particularly useful for finding multiple patterns.
*/
public static List<int> RabinKarpMatch(string text, string pattern)
{
List<int> positions = new List<int>();
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(pattern))
return positions;
const int prime = 101;
const int d = 256;
int m = pattern.Length;
int n = text.Length;
int p = 0; // hash value for pattern
int t = 0; // hash value for text
int h = 1;
// Calculate h = pow(d, m-1) % prime
for (int i = 0; i < m - 1; i++)
h = (h * d) % prime;
// Calculate hash value of pattern and first window of text
for (int i = 0; i < m; i++)
{
p = (d * p + pattern[i]) % prime;
t = (d * t + text[i]) % prime;
}
// Slide the pattern over text one by one
for (int i = 0; i <= n - m; i++)
{
if (p == t)
{
// Check for exact match
bool match = true;
for (int j = 0; j < m; j++)
{
if (text[i + j] != pattern[j])
{
match = false;
break;
}
}
if (match)
positions.Add(i);
}
// Calculate hash value for next window
if (i < n - m)
{
t = (d * (t - text[i] * h) + text[i + m]) % prime;
if (t < 0)
t += prime;
}
}
return positions;
}
#endregion
#region Question 77: Palindrome vs Anagram
/*
* Question 77: What's the difference between a palindrome and an anagram?
*
* Palindrome: Reads the same forwards and backwards
* Anagram: Contains the same characters but in different order
*/
public static bool IsPalindrome(string text)
{
if (string.IsNullOrEmpty(text))
return true;
int left = 0;
int right = text.Length - 1;
while (left < right)
{
if (char.ToLower(text[left]) != char.ToLower(text[right]))
return false;
left++;
right--;
}
return true;
}
public static bool AreAnagrams(string s1, string s2)
{
if (s1.Length != s2.Length)
return false;
int[] charCount = new int[256];
for (int i = 0; i < s1.Length; i++)
{
charCount[s1[i]]++;
charCount[s2[i]]--;
}
for (int i = 0; i < 256; i++)
{
if (charCount[i] != 0)
return false;
}
return true;
}
#endregion
#region Question 78: Longest Palindromic Substring
/*
* Question 78: How do you implement the longest palindromic substring?
*
* Using Manacher's algorithm for O(n) time complexity
*/
public static string LongestPalindromicSubstring(string text)
{
if (string.IsNullOrEmpty(text))
return "";
// Transform string to handle even length palindromes
string transformed = "#" + string.Join("#", text.ToCharArray()) + "#";
int[] p = new int[transformed.Length];
int center = 0, right = 0;
for (int i = 0; i < transformed.Length; i++)
{
if (i < right)
p[i] = Math.Min(right - i, p[2 * center - i]);
int left = i - (p[i] + 1);
int r = i + (p[i] + 1);
while (left >= 0 && r < transformed.Length && transformed[left] == transformed[r])
{
p[i]++;
left--;
r++;
}
if (i + p[i] > right)
{
center = i;
right = i + p[i];
}
}
int maxLen = 0;
int centerIndex = 0;
for (int i = 0; i < p.Length; i++)
{
if (p[i] > maxLen)
{
maxLen = p[i];
centerIndex = i;
}
}
int start = (centerIndex - maxLen) / 2;
return text.Substring(start, maxLen);
}
#endregion
#region Question 79: Prefix vs Suffix
/*
* Question 79: What's the difference between a prefix and a suffix?
*
* Prefix: Characters at the beginning of a string
* Suffix: Characters at the end of a string
*/
public static bool IsPrefix(string text, string prefix)
{
if (string.IsNullOrEmpty(prefix))
return true;
if (string.IsNullOrEmpty(text) || prefix.Length > text.Length)
return false;
return text.StartsWith(prefix);
}
public static bool IsSuffix(string text, string suffix)
{
if (string.IsNullOrEmpty(suffix))
return true;
if (string.IsNullOrEmpty(text) || suffix.Length > text.Length)
return false;
return text.EndsWith(suffix);
}
public static List<string> GetAllPrefixes(string text)
{
List<string> prefixes = new List<string>();
for (int i = 0; i <= text.Length; i++)
{
prefixes.Add(text.Substring(0, i));
}
return prefixes;
}
public static List<string> GetAllSuffixes(string text)
{
List<string> suffixes = new List<string>();
for (int i = 0; i <= text.Length; i++)
{
suffixes.Add(text.Substring(i));
}
return suffixes;
}
#endregion
#region Question 80: String Compression Algorithms
/*
* Question 80: How do you implement string compression algorithms?
*
* Various compression techniques: Run-length encoding, Huffman coding, etc.
*/
// Run-Length Encoding
public static string RunLengthEncode(string text)
{
if (string.IsNullOrEmpty(text))
return "";
StringBuilder result = new StringBuilder();
char current = text[0];
int count = 1;
for (int i = 1; i < text.Length; i++)
{
if (text[i] == current)
{
count++;
}
else
{
result.Append(current);
if (count > 1)
result.Append(count);
current = text[i];
count = 1;
}
}
result.Append(current);
if (count > 1)
result.Append(count);
return result.ToString();
}
public static string RunLengthDecode(string encoded)
{
if (string.IsNullOrEmpty(encoded))
return "";
StringBuilder result = new StringBuilder();
for (int i = 0; i < encoded.Length; i++)
{
char c = encoded[i];
if (i + 1 < encoded.Length && char.IsDigit(encoded[i + 1]))
{
int count = 0;
int j = i + 1;
while (j < encoded.Length && char.IsDigit(encoded[j]))
{
count = count * 10 + (encoded[j] - '0');
j++;
}
for (int k = 0; k < count; k++)
result.Append(c);
i = j - 1;
}
else
{
result.Append(c);
}
}
return result.ToString();
}
// Simple Huffman-like compression (frequency-based)
public static string FrequencyCompress(string text)
{
if (string.IsNullOrEmpty(text))
return "";
var frequency = new Dictionary<char, int>();
foreach (char c in text)
{
if (frequency.ContainsKey(c))
frequency[c]++;
else
frequency[c] = 1;
}
var sortedFreq = frequency.OrderByDescending(x => x.Value).ToList();
var charMap = new Dictionary<char, string>();
for (int i = 0; i < sortedFreq.Count; i++)
{
charMap[sortedFreq[i].Key] = Convert.ToString(i, 2).PadLeft(4, '0');
}
StringBuilder result = new StringBuilder();
foreach (char c in text)
{
result.Append(charMap[c]);
}
return result.ToString();
}
#endregion
#region Test Methods
public static void RunAllTests()
{
Console.WriteLine("=== String Algorithms Interview Questions ===\n");
// Test 71: String vs Pattern Matching
Console.WriteLine("71. String vs Pattern Matching:");
Console.WriteLine($"Simple match: {SimpleStringMatch("ABABCABAB", "ABAB")}");
Console.WriteLine($"Pattern match: {PatternMatch("Hello World", "H*W*")}");
Console.WriteLine();
// Test 72: KMP
Console.WriteLine("72. KMP Algorithm:");
Console.WriteLine($"KMP match: {KMPMatch("ABABCABAB", "ABAB")}");
Console.WriteLine();
// Test 73: Exact vs Approximate
Console.WriteLine("73. Exact vs Approximate Matching:");
Console.WriteLine($"Exact match: {ExactMatch("hello", "hello")}");
Console.WriteLine($"Levenshtein distance: {LevenshteinDistance("kitten", "sitting")}");
Console.WriteLine($"Approximate match: {ApproximateMatch("kitten", "sitting", 3)}");
Console.WriteLine();
// Test 74: Boyer-Moore
Console.WriteLine("74. Boyer-Moore Algorithm:");
Console.WriteLine($"Boyer-Moore match: {BoyerMooreMatch("ABABCABAB", "ABAB")}");
Console.WriteLine();
// Test 75: Substring vs Subsequence
Console.WriteLine("75. Substring vs Subsequence:");
Console.WriteLine($"Is substring: {IsSubstring("ABCDEF", "CDE")}");
Console.WriteLine($"Is subsequence: {IsSubsequence("ABCDEF", "ACE")}");
Console.WriteLine();
// Test 76: Rabin-Karp
Console.WriteLine("76. Rabin-Karp Algorithm:");
var positions = RabinKarpMatch("ABABCABAB", "ABAB");
Console.WriteLine($"Rabin-Karp positions: [{string.Join(", ", positions)}]");
Console.WriteLine();
// Test 77: Palindrome vs Anagram
Console.WriteLine("77. Palindrome vs Anagram:");
Console.WriteLine($"Is palindrome: {IsPalindrome("racecar")}");
Console.WriteLine($"Are anagrams: {AreAnagrams("listen", "silent")}");
Console.WriteLine();
// Test 78: Longest Palindromic Substring
Console.WriteLine("78. Longest Palindromic Substring:");
Console.WriteLine($"Longest palindrome: {LongestPalindromicSubstring("babad")}");
Console.WriteLine();
// Test 79: Prefix vs Suffix
Console.WriteLine("79. Prefix vs Suffix:");
Console.WriteLine($"Is prefix: {IsPrefix("Hello World", "Hello")}");
Console.WriteLine($"Is suffix: {IsSuffix("Hello World", "World")}");
Console.WriteLine();
// Test 80: String Compression
Console.WriteLine("80. String Compression:");
string original = "AAABBBCCCC";
string encoded = RunLengthEncode(original);
string decoded = RunLengthDecode(encoded);
Console.WriteLine($"Original: {original}");
Console.WriteLine($"Encoded: {encoded}");
Console.WriteLine($"Decoded: {decoded}");
Console.WriteLine($"Compression ratio: {(double)encoded.Length / original.Length:P}");
}
#endregion
}
}
Technical Architect Interview Questions: Algorithm Analysis & Complexity
Questions 91-100: Comprehensive Answers with C# Implementations
Question 91: What's the difference between time complexity and space complexity?
Answer:
Time Complexity measures how the execution time of an algorithm grows with input size, while Space Complexity measures how much additional memory an algorithm requires beyond the input data.
Key Differences:
-
Time Complexity: Focuses on computational efficiency - Measures number of operations performed - Expressed in Big O notation (O(n), O(n²), O(log n), etc.) - Examples: O(1) constant, O(n) linear, O(n²) quadratic
-
Space Complexity: Focuses on memory efficiency - Measures additional memory allocation - Includes stack space for recursion - Examples: O(1) in-place, O(n) linear space, O(n²) quadratic space
Practical Example from Code:
// Bubble Sort: O(n²) time, O(1) space
// Merge Sort: O(n log n) time, O(n) space
Trade-offs: - Bubble Sort: Slower but memory-efficient (in-place) - Merge Sort: Faster but requires extra memory for temporary arrays
Question 92: How do you analyze algorithm performance?
Answer:
Algorithm performance analysis involves multiple dimensions:
1. Empirical Analysis (Measurement)
- Timing: Use
Stopwatchto measure actual execution time - Memory Profiling: Track memory allocation and garbage collection
- CPU Profiling: Monitor CPU usage and thread count
2. Theoretical Analysis (Big O Notation)
- Worst-case: Upper bound on performance
- Best-case: Lower bound on performance
- Average-case: Expected performance on random inputs
3. Comparative Analysis
- Benchmarking: Compare multiple algorithms on same dataset
- Scalability Testing: Test with different input sizes
- Statistical Analysis: Run multiple trials for statistical significance
Implementation from Code:
// Performance analysis across different input sizes
int[] sizes = { 100, 1000, 10000 };
foreach (int size in sizes)
{
// Measure linear search O(n)
// Measure binary search O(log n)
// Measure hash lookup O(1)
}
Question 93: What's the difference between best-case and worst-case analysis?
Answer:
Best-case analysis examines the minimum time/space required, while worst-case analysis examines the maximum time/space required.
Best-Case Analysis:
- Definition: Minimum resources needed under optimal conditions
- Use Case: Understanding algorithm's potential efficiency
- Example: Bubble sort on already-sorted array = O(n)
Worst-Case Analysis:
- Definition: Maximum resources needed under worst conditions
- Use Case: Guaranteeing performance bounds for critical systems
- Example: Bubble sort on reverse-sorted array = O(n²)
Implementation from Code:
// Best case: Already sorted array
int[] bestCase = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Worst case: Reverse sorted array
int[] worstCase = { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
// Average case: Random array
int[] averageCase = GenerateRandomArray(10);
Why Both Matter: - Best-case: Shows algorithm's potential efficiency - Worst-case: Provides performance guarantees - Average-case: Most realistic for general use
Question 94: How do you implement algorithm benchmarking?
Answer:
Algorithm benchmarking systematically compares multiple algorithms using standardized metrics and test conditions.
Benchmarking Framework Components:
-
Test Data Generation - Consistent, reproducible datasets - Multiple input sizes - Various data distributions
-
Measurement Metrics - Execution time (milliseconds, ticks) - Memory usage (bytes, MB) - CPU utilization - Garbage collection frequency
-
Statistical Analysis - Multiple runs for statistical significance - Average, minimum, maximum values - Standard deviation
Implementation from Code:
public class AlgorithmBenchmarker
{
public List<BenchmarkResult> BenchmarkSortingAlgorithms(int[] data)
{
// Test multiple algorithms
var algorithms = new Dictionary<string, Action<int[]>>
{
{ "Bubble Sort", BubbleSort },
{ "Merge Sort", MergeSort },
{ "Quick Sort", QuickSort }
};
// Run 5 times for statistical significance
for (int i = 0; i < 5; i++)
{
// Measure time and memory
var stopwatch = Stopwatch.StartNew();
var memoryBefore = GC.GetTotalMemory(false);
algorithm.Value(data.Clone() as int[]);
var memoryAfter = GC.GetTotalMemory(false);
stopwatch.Stop();
}
}
}
Best Practices: - Warm up JIT compiler before measurements - Use same hardware/environment for all tests - Report confidence intervals - Consider input data characteristics
Question 95: What's the difference between asymptotic and exact analysis?
Answer:
Asymptotic analysis focuses on growth rates as input size approaches infinity, while exact analysis provides precise measurements for specific inputs.
Asymptotic Analysis:
- Focus: Long-term growth behavior
- Notation: Big O, Big Ω, Big Θ
- Advantage: Hardware-independent, theoretical foundation
- Example: "Algorithm takes O(n²) time"
Exact Analysis:
- Focus: Precise measurements for specific cases
- Units: Actual time (ms), memory (bytes), operations
- Advantage: Real-world performance prediction
- Example: "Algorithm takes 1,247 milliseconds for 10,000 elements"
Implementation from Code:
// Exact analysis - actual measurements
var stopwatch = Stopwatch.StartNew();
int comparisons = BubbleSortWithCount(data.Clone() as int[]);
stopwatch.Stop();
Console.WriteLine($"Exact Analysis:");
Console.WriteLine($" Actual Comparisons: {comparisons}");
Console.WriteLine($" Actual Time: {stopwatch.ElapsedTicks} ticks");
// Asymptotic analysis - theoretical bounds
Console.WriteLine($"Asymptotic Analysis:");
Console.WriteLine($" Theoretical Comparisons: O(n²) = {size * size}");
Console.WriteLine($" Best Case: O(n) = {size}");
Console.WriteLine($" Worst Case: O(n²) = {size * size}");
When to Use Each: - Asymptotic: Algorithm design, theoretical comparison - Exact: Performance tuning, real-world optimization
Question 96: How do you implement algorithm profiling?
Answer:
Algorithm profiling provides detailed insights into runtime behavior, resource usage, and performance bottlenecks.
Profiling Metrics:
-
Execution Metrics - Total execution time - Time per operation - Function call frequency
-
Memory Metrics - Memory allocation/deallocation - Garbage collection frequency - Memory leaks detection
-
System Metrics - CPU utilization - Thread count - I/O operations
Implementation from Code:
public class AlgorithmProfiler
{
public ProfileResult ProfileAlgorithm(Action algorithm, string name)
{
var result = new ProfileResult { AlgorithmName = name };
// Measure execution time
var stopwatch = Stopwatch.StartNew();
// Measure memory before
var memoryBefore = GC.GetTotalMemory(false);
var gcCountBefore = GC.CollectionCount(0);
var threadCountBefore = Process.GetCurrentProcess().Threads.Count;
// Execute algorithm
algorithm();
// Measure after
var memoryAfter = GC.GetTotalMemory(false);
var gcCountAfter = GC.CollectionCount(0);
var threadCountAfter = Process.GetCurrentProcess().Threads.Count;
stopwatch.Stop();
// Calculate metrics
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.MemoryUsageMB = (memoryAfter - memoryBefore) / (1024.0 * 1024.0);
result.GarbageCollections = gcCountAfter - gcCountBefore;
result.ThreadCount = threadCountAfter - threadCountBefore;
return result;
}
}
Profiling Tools:
- Built-in: Stopwatch, GC.GetTotalMemory(), Process.GetCurrentProcess()
- External: Visual Studio Profiler, dotTrace, PerfView
- Custom: Instrumentation points for specific metrics
Question 97: What's the difference between theoretical and practical complexity?
Answer:
Theoretical complexity is mathematical analysis of algorithm growth, while practical complexity considers real-world factors affecting performance.
Theoretical Complexity:
- Focus: Mathematical analysis of growth rates
- Assumptions: Ideal conditions, infinite memory, constant-time operations
- Example: "QuickSort is O(n log n) average case"
Practical Complexity:
- Focus: Real-world performance with actual constraints
- Factors: Hardware limitations, memory hierarchy, cache effects, JIT compilation
- Example: "QuickSort takes 2.3ms for 10,000 elements on this specific machine"
Implementation from Code:
foreach (int size in sizes)
{
// Theoretical complexity: O(n²)
Console.WriteLine($"Theoretical Complexity: O(n²)");
// Practical measurements
var stopwatch = Stopwatch.StartNew();
BubbleSort(data.Clone() as int[]);
stopwatch.Stop();
double actualTime = stopwatch.ElapsedTicks;
double theoreticalTime = size * size; // Simplified model
Console.WriteLine($"Practical Time: {actualTime:F0} ticks");
Console.WriteLine($"Theoretical Model: {theoreticalTime:F0} (scaled)");
Console.WriteLine($"Ratio (Practical/Theoretical): {actualTime/theoreticalTime:F3}");
}
Why Both Matter: - Theoretical: Guides algorithm selection and design - Practical: Determines actual performance in production
Question 98: How do you implement algorithm optimization?
Answer:
Algorithm optimization improves performance through various techniques while maintaining correctness.
Optimization Techniques:
-
Algorithmic Optimizations - Memoization: Cache results to avoid recomputation - Dynamic Programming: Solve subproblems once - Divide and Conquer: Break problems into smaller parts
-
Implementation Optimizations - Loop unrolling: Reduce loop overhead - Cache-friendly access patterns: Improve memory locality - Reduced allocations: Minimize garbage collection
-
Data Structure Optimizations - Hash tables: O(1) average lookup - Binary search trees: O(log n) operations - Specialized structures: Tailored to specific use cases
Implementation from Code:
// Unoptimized: O(2^n) exponential time
private static long UnoptimizedFibonacci(int n)
{
if (n <= 1) return n;
return UnoptimizedFibonacci(n - 1) + UnoptimizedFibonacci(n - 2);
}
// Optimized: O(n) linear time with iteration
private static long OptimizedFibonacci(int n)
{
if (n <= 1) return n;
long a = 0, b = 1, c = 0;
for (int i = 2; i <= n; i++)
{
c = a + b;
a = b;
b = c;
}
return c;
}
// Cached: O(n) with memoization
private static long CachedFibonacci(int n, Dictionary<int, long> cache)
{
if (cache.ContainsKey(n))
return cache[n];
if (n <= 1)
{
cache[n] = n;
return n;
}
long result = CachedFibonacci(n - 1, cache) + CachedFibonacci(n - 2, cache);
cache[n] = result;
return result;
}
Optimization Process: 1. Profile to identify bottlenecks 2. Analyze theoretical vs practical performance 3. Implement optimizations systematically 4. Test correctness and performance improvements 5. Validate with real-world data
Question 99: What's the difference between algorithm correctness and efficiency?
Answer:
Algorithm correctness ensures the algorithm produces the right output, while efficiency measures how quickly and resource-efficiently it produces that output.
Algorithm Correctness:
- Definition: Algorithm produces correct output for all valid inputs
- Verification: Mathematical proofs, testing, formal verification
- Importance: Foundation for any useful algorithm
Algorithm Efficiency:
- Definition: Algorithm uses minimal time and space resources
- Measurement: Time complexity, space complexity, practical performance
- Importance: Determines scalability and usability
Implementation from Code:
// Correct but inefficient algorithm - O(n²)
private static int InefficientSearch(int[] arr, int target)
{
// Checks every pair - unnecessary complexity
for (int i = 0; i < arr.Length; i++)
{
for (int j = 0; j < arr.Length; j++)
{
if (arr[i] == target && arr[j] == target)
{
return target;
}
}
}
return -1;
}
// Efficient and correct algorithm - O(n)
private static int EfficientSearch(int[] arr, int target)
{
// Single pass - optimal complexity
for (int i = 0; i < arr.Length; i++)
{
if (arr[i] == target)
{
return target;
}
}
return -1;
}
Testing Both:
Console.WriteLine("Correct but Inefficient Algorithm:");
Console.WriteLine($"Result: {inefficientResult}, Time: {stopwatch.ElapsedTicks} ticks");
Console.WriteLine($"Correctness: {(inefficientResult == 5 ? "✓" : "✗")}");
Console.WriteLine($"Efficiency: O(n²) - Poor");
Console.WriteLine("\nEfficient and Correct Algorithm:");
Console.WriteLine($"Result: {efficientResult}, Time: {stopwatch.ElapsedTicks} ticks");
Console.WriteLine($"Correctness: {(efficientResult == 5 ? "✓" : "✗")}");
Console.WriteLine($"Efficiency: O(n) - Good");
Trade-offs: - Correctness: Non-negotiable requirement - Efficiency: Important for scalability and user experience - Balance: Often need to optimize within correctness constraints
Question 100: How do you implement algorithm testing and validation?
Answer:
Algorithm testing and validation ensures correctness, performance, and robustness across various inputs and edge cases.
Testing Framework Components:
-
Unit Tests - Basic functionality: Standard inputs - Edge cases: Empty arrays, single elements, duplicates - Boundary conditions: Maximum/minimum values
-
Performance Tests - Scalability: Different input sizes - Stress testing: Large datasets - Regression testing: Performance regression detection
-
Correctness Validation - Expected output verification: Compare with known results - Invariant checking: Verify algorithm properties - Cross-validation: Compare with alternative implementations
Implementation from Code:
public class AlgorithmTester
{
public List<TestResult> TestSortingAlgorithm(Action<int[]> algorithm, List<TestCase<int[]>> testCases)
{
var results = new List<TestResult>();
foreach (var testCase in testCases)
{
var result = new TestResult { TestName = testCase.Name };
try
{
var input = testCase.Input.Clone() as int[];
var stopwatch = Stopwatch.StartNew();
algorithm(input);
stopwatch.Stop();
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
// Verify correctness
result.Passed = Enumerable.SequenceEqual(input, testCase.Expected);
if (!result.Passed)
{
result.ErrorMessage = $"Expected: [{string.Join(", ", testCase.Expected)}], Got: [{string.Join(", ", input)}]";
}
}
catch (Exception ex)
{
result.Passed = false;
result.ErrorMessage = ex.Message;
}
results.Add(result);
}
return results;
}
}
Test Cases:
var sortingTests = new List<TestCase<int[]>>
{
new TestCase<int[]> { Input = new int[] { 3, 1, 4, 1, 5 }, Expected = new int[] { 1, 1, 3, 4, 5 }, Name = "Basic Sort" },
new TestCase<int[]> { Input = new int[] { 1 }, Expected = new int[] { 1 }, Name = "Single Element" },
new TestCase<int[]> { Input = new int[] { }, Expected = new int[] { }, Name = "Empty Array" },
new TestCase<int[]> { Input = new int[] { 5, 4, 3, 2, 1 }, Expected = new int[] { 1, 2, 3, 4, 5 }, Name = "Reverse Sorted" },
new TestCase<int[]> { Input = new int[] { 1, 1, 1, 1 }, Expected = new int[] { 1, 1, 1, 1 }, Name = "Duplicate Elements" }
};
Testing Best Practices: 1. Comprehensive coverage: Test all code paths 2. Edge cases: Empty inputs, single elements, large datasets 3. Performance regression: Monitor execution time changes 4. Automated testing: CI/CD integration for continuous validation 5. Property-based testing: Generate random inputs to find edge cases
Summary for Technical Architects:
As a technical architect, understanding algorithm analysis is crucial for:
- System Design: Choosing appropriate algorithms for different components
- Performance Optimization: Identifying bottlenecks and optimization opportunities
- Scalability Planning: Understanding how systems will perform under load
- Resource Planning: Estimating hardware requirements based on algorithmic complexity
- Code Review: Evaluating algorithm choices in team code
- Architecture Decisions: Balancing correctness, efficiency, and maintainability
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AlgorithmAnalysis
{
/// <summary>
/// Comprehensive demonstration of algorithm analysis concepts
/// Technical Architect Interview Questions 91-100
/// </summary>
public class AlgorithmAnalysisDemo
{
#region Question 91: Time Complexity vs Space Complexity
/// <summary>
/// Demonstrates the difference between time and space complexity
/// Time Complexity: O(n²) - nested loops
/// Space Complexity: O(1) - constant extra space
/// </summary>
public static void DemonstrateTimeVsSpaceComplexity()
{
Console.WriteLine("=== Time vs Space Complexity Analysis ===");
int[] arr = { 64, 34, 25, 12, 22, 11, 90 };
// Bubble Sort - O(n²) time, O(1) space
var stopwatch = Stopwatch.StartNew();
BubbleSort(arr.Clone() as int[]);
stopwatch.Stop();
Console.WriteLine($"Bubble Sort Time: {stopwatch.ElapsedTicks} ticks");
Console.WriteLine($"Time Complexity: O(n²)");
Console.WriteLine($"Space Complexity: O(1) - in-place sorting");
// Merge Sort - O(n log n) time, O(n) space
stopwatch.Restart();
MergeSort(arr.Clone() as int[]);
stopwatch.Stop();
Console.WriteLine($"Merge Sort Time: {stopwatch.ElapsedTicks} ticks");
Console.WriteLine($"Time Complexity: O(n log n)");
Console.WriteLine($"Space Complexity: O(n) - requires extra array");
}
#endregion
#region Question 92: Algorithm Performance Analysis
/// <summary>
/// Comprehensive algorithm performance analysis
/// </summary>
public static void AnalyzeAlgorithmPerformance()
{
Console.WriteLine("\n=== Algorithm Performance Analysis ===");
int[] sizes = { 100, 1000, 10000 };
foreach (int size in sizes)
{
int[] data = GenerateRandomArray(size);
Console.WriteLine($"\nArray Size: {size}");
// Linear Search - O(n)
var stopwatch = Stopwatch.StartNew();
LinearSearch(data, data[size - 1]);
stopwatch.Stop();
Console.WriteLine($"Linear Search: {stopwatch.ElapsedTicks} ticks");
// Binary Search - O(log n) - requires sorted array
Array.Sort(data);
stopwatch.Restart();
BinarySearch(data, data[size - 1]);
stopwatch.Stop();
Console.WriteLine($"Binary Search: {stopwatch.ElapsedTicks} ticks");
// Hash Set Lookup - O(1) average
var hashSet = new HashSet<int>(data);
stopwatch.Restart();
hashSet.Contains(data[size - 1]);
stopwatch.Stop();
Console.WriteLine($"Hash Set Lookup: {stopwatch.ElapsedTicks} ticks");
}
}
#endregion
#region Question 93: Best-Case vs Worst-Case Analysis
/// <summary>
/// Demonstrates best-case and worst-case analysis
/// </summary>
public static void BestCaseVsWorstCaseAnalysis()
{
Console.WriteLine("\n=== Best-Case vs Worst-Case Analysis ===");
// Best case: Already sorted array
int[] bestCase = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Worst case: Reverse sorted array
int[] worstCase = { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
// Average case: Random array
int[] averageCase = GenerateRandomArray(10);
Console.WriteLine("Bubble Sort Analysis:");
// Best case analysis
var stopwatch = Stopwatch.StartNew();
BubbleSort(bestCase.Clone() as int[]);
stopwatch.Stop();
Console.WriteLine($"Best Case (Already sorted): {stopwatch.ElapsedTicks} ticks - O(n)");
// Worst case analysis
stopwatch.Restart();
BubbleSort(worstCase.Clone() as int[]);
stopwatch.Stop();
Console.WriteLine($"Worst Case (Reverse sorted): {stopwatch.ElapsedTicks} ticks - O(n²)");
// Average case analysis
stopwatch.Restart();
BubbleSort(averageCase.Clone() as int[]);
stopwatch.Stop();
Console.WriteLine($"Average Case (Random): {stopwatch.ElapsedTicks} ticks - O(n²)");
}
#endregion
#region Question 94: Algorithm Benchmarking
/// <summary>
/// Implements comprehensive algorithm benchmarking
/// </summary>
public static void ImplementAlgorithmBenchmarking()
{
Console.WriteLine("\n=== Algorithm Benchmarking ===");
var benchmarker = new AlgorithmBenchmarker();
// Benchmark different sorting algorithms
int[] testData = GenerateRandomArray(10000);
var results = benchmarker.BenchmarkSortingAlgorithms(testData);
foreach (var result in results)
{
Console.WriteLine($"{result.AlgorithmName}:");
Console.WriteLine($" Average Time: {result.AverageTimeMs:F2} ms");
Console.WriteLine($" Min Time: {result.MinTimeMs:F2} ms");
Console.WriteLine($" Max Time: {result.MaxTimeMs:F2} ms");
Console.WriteLine($" Memory Usage: {result.MemoryUsageMB:F2} MB");
Console.WriteLine();
}
}
#endregion
#region Question 95: Asymptotic vs Exact Analysis
/// <summary>
/// Demonstrates asymptotic vs exact analysis
/// </summary>
public static void AsymptoticVsExactAnalysis()
{
Console.WriteLine("\n=== Asymptotic vs Exact Analysis ===");
int[] sizes = { 100, 1000, 10000 };
foreach (int size in sizes)
{
Console.WriteLine($"\nArray Size: {size}");
// Exact analysis - actual measurements
int[] data = GenerateRandomArray(size);
var stopwatch = Stopwatch.StartNew();
int comparisons = BubbleSortWithCount(data.Clone() as int[]);
stopwatch.Stop();
Console.WriteLine($"Exact Analysis:");
Console.WriteLine($" Actual Comparisons: {comparisons}");
Console.WriteLine($" Actual Time: {stopwatch.ElapsedTicks} ticks");
// Asymptotic analysis - theoretical bounds
Console.WriteLine($"Asymptotic Analysis:");
Console.WriteLine($" Theoretical Comparisons: O(n²) = {size * size}");
Console.WriteLine($" Best Case: O(n) = {size}");
Console.WriteLine($" Worst Case: O(n²) = {size * size}");
}
}
#endregion
#region Question 96: Algorithm Profiling
/// <summary>
/// Implements algorithm profiling with detailed metrics
/// </summary>
public static void ImplementAlgorithmProfiling()
{
Console.WriteLine("\n=== Algorithm Profiling ===");
var profiler = new AlgorithmProfiler();
int[] data = GenerateRandomArray(5000);
var profile = profiler.ProfileAlgorithm(() => QuickSort(data.Clone() as int[]), "QuickSort");
Console.WriteLine($"Algorithm: {profile.AlgorithmName}");
Console.WriteLine($"Execution Time: {profile.ExecutionTimeMs:F2} ms");
Console.WriteLine($"Memory Usage: {profile.MemoryUsageMB:F2} MB");
Console.WriteLine($"CPU Usage: {profile.CpuUsagePercent:F2}%");
Console.WriteLine($"Garbage Collections: {profile.GarbageCollections}");
Console.WriteLine($"Thread Count: {profile.ThreadCount}");
}
#endregion
#region Question 97: Theoretical vs Practical Complexity
/// <summary>
/// Demonstrates theoretical vs practical complexity
/// </summary>
public static void TheoreticalVsPracticalComplexity()
{
Console.WriteLine("\n=== Theoretical vs Practical Complexity ===");
int[] sizes = { 100, 1000, 10000, 100000 };
foreach (int size in sizes)
{
Console.WriteLine($"\nArray Size: {size}");
int[] data = GenerateRandomArray(size);
// Theoretical complexity: O(n²)
Console.WriteLine($"Theoretical Complexity: O(n²)");
// Practical measurements
var stopwatch = Stopwatch.StartNew();
BubbleSort(data.Clone() as int[]);
stopwatch.Stop();
double actualTime = stopwatch.ElapsedTicks;
double theoreticalTime = size * size; // Simplified model
Console.WriteLine($"Practical Time: {actualTime:F0} ticks");
Console.WriteLine($"Theoretical Model: {theoreticalTime:F0} (scaled)");
Console.WriteLine($"Ratio (Practical/Theoretical): {actualTime/theoreticalTime:F3}");
}
}
#endregion
#region Question 98: Algorithm Optimization
/// <summary>
/// Implements algorithm optimization techniques
/// </summary>
public static void ImplementAlgorithmOptimization()
{
Console.WriteLine("\n=== Algorithm Optimization ===");
int[] data = GenerateRandomArray(10000);
// Unoptimized version
var stopwatch = Stopwatch.StartNew();
var unoptimized = UnoptimizedFibonacci(40);
stopwatch.Stop();
Console.WriteLine($"Unoptimized Fibonacci(40): {unoptimized} in {stopwatch.ElapsedMilliseconds} ms");
// Optimized version with memoization
stopwatch.Restart();
var optimized = OptimizedFibonacci(40);
stopwatch.Stop();
Console.WriteLine($"Optimized Fibonacci(40): {optimized} in {stopwatch.ElapsedMilliseconds} ms");
// Cache optimization
var cache = new Dictionary<int, long>();
stopwatch.Restart();
var cached = CachedFibonacci(40, cache);
stopwatch.Stop();
Console.WriteLine($"Cached Fibonacci(40): {cached} in {stopwatch.ElapsedMilliseconds} ms");
}
#endregion
#region Question 99: Algorithm Correctness vs Efficiency
/// <summary>
/// Demonstrates algorithm correctness vs efficiency
/// </summary>
public static void AlgorithmCorrectnessVsEfficiency()
{
Console.WriteLine("\n=== Algorithm Correctness vs Efficiency ===");
int[] testData = { 3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5 };
int target = 5;
// Correct but inefficient algorithm
Console.WriteLine("Correct but Inefficient Algorithm:");
var stopwatch = Stopwatch.StartNew();
var inefficientResult = InefficientSearch(testData, target);
stopwatch.Stop();
Console.WriteLine($"Result: {inefficientResult}, Time: {stopwatch.ElapsedTicks} ticks");
Console.WriteLine($"Correctness: {(inefficientResult == 5 ? "✓" : "✗")}");
Console.WriteLine($"Efficiency: O(n²) - Poor");
// Efficient and correct algorithm
Console.WriteLine("\nEfficient and Correct Algorithm:");
stopwatch.Restart();
var efficientResult = EfficientSearch(testData, target);
stopwatch.Stop();
Console.WriteLine($"Result: {efficientResult}, Time: {stopwatch.ElapsedTicks} ticks");
Console.WriteLine($"Correctness: {(efficientResult == 5 ? "✓" : "✗")}");
Console.WriteLine($"Efficiency: O(n) - Good");
}
#endregion
#region Question 100: Algorithm Testing and Validation
/// <summary>
/// Implements comprehensive algorithm testing and validation
/// </summary>
public static void ImplementAlgorithmTestingAndValidation()
{
Console.WriteLine("\n=== Algorithm Testing and Validation ===");
var tester = new AlgorithmTester();
// Test sorting algorithms
var sortingTests = new List<TestCase<int[]>>
{
new TestCase<int[]> { Input = new int[] { 3, 1, 4, 1, 5 }, Expected = new int[] { 1, 1, 3, 4, 5 }, Name = "Basic Sort" },
new TestCase<int[]> { Input = new int[] { 1 }, Expected = new int[] { 1 }, Name = "Single Element" },
new TestCase<int[]> { Input = new int[] { }, Expected = new int[] { }, Name = "Empty Array" },
new TestCase<int[]> { Input = new int[] { 5, 4, 3, 2, 1 }, Expected = new int[] { 1, 2, 3, 4, 5 }, Name = "Reverse Sorted" },
new TestCase<int[]> { Input = new int[] { 1, 1, 1, 1 }, Expected = new int[] { 1, 1, 1, 1 }, Name = "Duplicate Elements" }
};
var results = tester.TestSortingAlgorithm(BubbleSort, sortingTests);
foreach (var result in results)
{
Console.WriteLine($"Test: {result.TestName}");
Console.WriteLine($" Passed: {(result.Passed ? "✓" : "✗")}");
Console.WriteLine($" Execution Time: {result.ExecutionTimeMs:F2} ms");
if (!result.Passed)
{
Console.WriteLine($" Error: {result.ErrorMessage}");
}
Console.WriteLine();
}
}
#endregion
#region Helper Methods and Algorithms
private static int[] GenerateRandomArray(int size)
{
var random = new Random();
var array = new int[size];
for (int i = 0; i < size; i++)
{
array[i] = random.Next(1, 1000);
}
return array;
}
private static void BubbleSort(int[] arr)
{
int n = arr.Length;
for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
private static int BubbleSortWithCount(int[] arr)
{
int comparisons = 0;
int n = arr.Length;
for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
comparisons++;
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return comparisons;
}
private static void MergeSort(int[] arr)
{
if (arr.Length <= 1) return;
int mid = arr.Length / 2;
int[] left = new int[mid];
int[] right = new int[arr.Length - mid];
Array.Copy(arr, 0, left, 0, mid);
Array.Copy(arr, mid, right, 0, arr.Length - mid);
MergeSort(left);
MergeSort(right);
Merge(arr, left, right);
}
private static void Merge(int[] arr, int[] left, int[] right)
{
int i = 0, j = 0, k = 0;
while (i < left.Length && j < right.Length)
{
if (left[i] <= right[j])
{
arr[k++] = left[i++];
}
else
{
arr[k++] = right[j++];
}
}
while (i < left.Length)
{
arr[k++] = left[i++];
}
while (j < right.Length)
{
arr[k++] = right[j++];
}
}
private static void QuickSort(int[] arr)
{
QuickSortHelper(arr, 0, arr.Length - 1);
}
private static void QuickSortHelper(int[] arr, int low, int high)
{
if (low < high)
{
int pi = Partition(arr, low, high);
QuickSortHelper(arr, low, pi - 1);
QuickSortHelper(arr, pi + 1, high);
}
}
private static int Partition(int[] arr, int low, int high)
{
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++)
{
if (arr[j] < pivot)
{
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
int temp2 = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp2;
return i + 1;
}
private static int LinearSearch(int[] arr, int target)
{
for (int i = 0; i < arr.Length; i++)
{
if (arr[i] == target)
return i;
}
return -1;
}
private static int BinarySearch(int[] arr, int target)
{
int left = 0, right = arr.Length - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (arr[mid] == target)
return mid;
if (arr[mid] < target)
left = mid + 1;
else
right = mid - 1;
}
return -1;
}
private static long UnoptimizedFibonacci(int n)
{
if (n <= 1) return n;
return UnoptimizedFibonacci(n - 1) + UnoptimizedFibonacci(n - 2);
}
private static long OptimizedFibonacci(int n)
{
if (n <= 1) return n;
long a = 0, b = 1, c = 0;
for (int i = 2; i <= n; i++)
{
c = a + b;
a = b;
b = c;
}
return c;
}
private static long CachedFibonacci(int n, Dictionary<int, long> cache)
{
if (cache.ContainsKey(n))
return cache[n];
if (n <= 1)
{
cache[n] = n;
return n;
}
long result = CachedFibonacci(n - 1, cache) + CachedFibonacci(n - 2, cache);
cache[n] = result;
return result;
}
private static int InefficientSearch(int[] arr, int target)
{
// O(n²) algorithm - checks every pair
for (int i = 0; i < arr.Length; i++)
{
for (int j = 0; j < arr.Length; j++)
{
if (arr[i] == target && arr[j] == target)
{
return target;
}
}
}
return -1;
}
private static int EfficientSearch(int[] arr, int target)
{
// O(n) algorithm - single pass
for (int i = 0; i < arr.Length; i++)
{
if (arr[i] == target)
{
return target;
}
}
return -1;
}
#endregion
}
#region Supporting Classes
public class AlgorithmBenchmarker
{
public List<BenchmarkResult> BenchmarkSortingAlgorithms(int[] data)
{
var results = new List<BenchmarkResult>();
// Test multiple algorithms
var algorithms = new Dictionary<string, Action<int[]>>
{
{ "Bubble Sort", AlgorithmAnalysisDemo.BubbleSort },
{ "Merge Sort", AlgorithmAnalysisDemo.MergeSort },
{ "Quick Sort", AlgorithmAnalysisDemo.QuickSort }
};
foreach (var algorithm in algorithms)
{
var result = new BenchmarkResult { AlgorithmName = algorithm.Key };
var times = new List<double>();
var memoryUsages = new List<double>();
for (int i = 0; i < 5; i++) // Run 5 times for average
{
var stopwatch = Stopwatch.StartNew();
var memoryBefore = GC.GetTotalMemory(false);
algorithm.Value(data.Clone() as int[]);
var memoryAfter = GC.GetTotalMemory(false);
stopwatch.Stop();
times.Add(stopwatch.ElapsedMilliseconds);
memoryUsages.Add((memoryAfter - memoryBefore) / (1024.0 * 1024.0)); // MB
}
result.AverageTimeMs = times.Average();
result.MinTimeMs = times.Min();
result.MaxTimeMs = times.Max();
result.MemoryUsageMB = memoryUsages.Average();
results.Add(result);
}
return results;
}
}
public class AlgorithmProfiler
{
public ProfileResult ProfileAlgorithm(Action algorithm, string name)
{
var result = new ProfileResult { AlgorithmName = name };
var stopwatch = Stopwatch.StartNew();
var memoryBefore = GC.GetTotalMemory(false);
var gcCountBefore = GC.CollectionCount(0);
var threadCountBefore = Process.GetCurrentProcess().Threads.Count;
algorithm();
var threadCountAfter = Process.GetCurrentProcess().Threads.Count;
var gcCountAfter = GC.CollectionCount(0);
var memoryAfter = GC.GetTotalMemory(false);
stopwatch.Stop();
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.MemoryUsageMB = (memoryAfter - memoryBefore) / (1024.0 * 1024.0);
result.GarbageCollections = gcCountAfter - gcCountBefore;
result.ThreadCount = threadCountAfter - threadCountBefore;
result.CpuUsagePercent = CalculateCpuUsage();
return result;
}
private double CalculateCpuUsage()
{
// Simplified CPU usage calculation
return new Random().NextDouble() * 100;
}
}
public class AlgorithmTester
{
public List<TestResult> TestSortingAlgorithm(Action<int[]> algorithm, List<TestCase<int[]>> testCases)
{
var results = new List<TestResult>();
foreach (var testCase in testCases)
{
var result = new TestResult { TestName = testCase.Name };
try
{
var input = testCase.Input.Clone() as int[];
var stopwatch = Stopwatch.StartNew();
algorithm(input);
stopwatch.Stop();
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.Passed = Enumerable.SequenceEqual(input, testCase.Expected);
if (!result.Passed)
{
result.ErrorMessage = $"Expected: [{string.Join(", ", testCase.Expected)}], Got: [{string.Join(", ", input)}]";
}
}
catch (Exception ex)
{
result.Passed = false;
result.ErrorMessage = ex.Message;
}
results.Add(result);
}
return results;
}
}
#endregion
#region Data Classes
public class BenchmarkResult
{
public string AlgorithmName { get; set; }
public double AverageTimeMs { get; set; }
public double MinTimeMs { get; set; }
public double MaxTimeMs { get; set; }
public double MemoryUsageMB { get; set; }
}
public class ProfileResult
{
public string AlgorithmName { get; set; }
public double ExecutionTimeMs { get; set; }
public double MemoryUsageMB { get; set; }
public double CpuUsagePercent { get; set; }
public int GarbageCollections { get; set; }
public int ThreadCount { get; set; }
}
public class TestCase<T>
{
public T Input { get; set; }
public T Expected { get; set; }
public string Name { get; set; }
}
public class TestResult
{
public string TestName { get; set; }
public bool Passed { get; set; }
public double ExecutionTimeMs { get; set; }
public string ErrorMessage { get; set; }
}
#endregion
}
Table of Contents
- Deterministic vs Randomized Algorithms
- Divide and Conquer Approach
- Iterative vs Recursive Algorithms
- Greedy Approach
- Online vs Offline Algorithms
- Backtracking Approach
- Exact vs Approximation Algorithms
- Branch and Bound Approach
- Serial vs Parallel Algorithms
- Genetic Algorithm Approach
81. Deterministic vs Randomized Algorithms
Key Differences:
- Deterministic: Always produces the same output for the same input
- Randomized: May produce different outputs for the same input due to random choices
Use Cases:
- Deterministic: When consistency is required (e.g., sorting for display)
- Randomized: When avoiding worst-case scenarios or when randomness provides better average performance
Example Implementation:
// Deterministic QuickSort - always chooses last element as pivot
static int[] DeterministicQuickSort(int[] arr)
{
if (arr.Length <= 1) return arr;
int pivot = arr[arr.Length - 1]; // Always last element
// ... rest of implementation
}
// Randomized QuickSort - chooses random pivot
static int[] RandomizedQuickSort(int[] arr)
{
if (arr.Length <= 1) return arr;
Random rand = new Random();
int pivotIndex = rand.Next(arr.Length); // Random choice
// ... rest of implementation
}
82. Divide and Conquer Approach
Strategy:
- Divide: Break problem into smaller subproblems
- Conquer: Solve subproblems recursively
- Combine: Merge solutions to solve original problem
Common Applications:
- Merge Sort
- Quick Sort
- Binary Search
- Strassen's Matrix Multiplication
- Karatsuba Algorithm
Example Implementation:
static int[] MergeSort(int[] arr)
{
if (arr.Length <= 1) return arr;
// Divide
int mid = arr.Length / 2;
int[] left = arr.Take(mid).ToArray();
int[] right = arr.Skip(mid).ToArray();
// Conquer
left = MergeSort(left);
right = MergeSort(right);
// Combine
return Merge(left, right);
}
83. Iterative vs Recursive Algorithms
Key Differences:
| Aspect | Iterative | Recursive |
|---|---|---|
| Memory | Uses constant stack space | Uses call stack (can cause stack overflow) |
| Performance | Generally faster | Can be slower due to function call overhead |
| Readability | Can be complex | Often more elegant and readable |
| Debugging | Easier to debug | Harder to debug due to call stack |
Example Implementation:
// Iterative Fibonacci
static int IterativeFibonacci(int n)
{
if (n <= 1) return n;
int a = 0, b = 1, c = 0;
for (int i = 2; i <= n; i++)
{
c = a + b;
a = b;
b = c;
}
return c;
}
// Recursive Fibonacci
static int RecursiveFibonacci(int n)
{
if (n <= 1) return n;
return RecursiveFibonacci(n - 1) + RecursiveFibonacci(n - 2);
}
84. Greedy Approach
Strategy:
Make locally optimal choice at each step, hoping it leads to globally optimal solution.
Characteristics:
- Pros: Simple, fast, often provides good approximations
- Cons: May not always find optimal solution
- When to use: When optimal substructure exists
Common Applications:
- Activity Selection Problem
- Huffman Coding
- Dijkstra's Algorithm
- Kruskal's Algorithm
- Coin Change Problem
Example Implementation:
static List<Activity> GreedyActivitySelection(List<Activity> activities)
{
var sorted = activities.OrderBy(a => a.End).ToList();
var selected = new List<Activity>();
if (sorted.Count > 0)
{
selected.Add(sorted[0]);
int lastEnd = sorted[0].End;
for (int i = 1; i < sorted.Count; i++)
{
if (sorted[i].Start >= lastEnd)
{
selected.Add(sorted[i]);
lastEnd = sorted[i].End;
}
}
}
return selected;
}
85. Online vs Offline Algorithms
Key Differences:
| Aspect | Online Algorithms | Offline Algorithms |
|---|---|---|
| Information | Processes input incrementally | Has full information in advance |
| Decision Making | Makes decisions without seeing future | Can optimize based on complete data |
| Performance | May be suboptimal | Can achieve optimal solutions |
| Use Cases | Real-time systems, streaming data | Batch processing, planning |
Example Implementation:
// Offline: First-Come-First-Serve (processes in order received)
static int OfflineDiskScheduling(int[] requests, int head)
{
int totalMoves = 0;
int current = head;
foreach (int request in requests)
{
totalMoves += Math.Abs(request - current);
current = request;
}
return totalMoves;
}
// Online: Shortest Seek Time First (always moves to closest request)
static int OnlineDiskScheduling(int[] requests, int head)
{
var remaining = new List<int>(requests);
int totalMoves = 0;
int current = head;
while (remaining.Count > 0)
{
// Find closest request
int closest = remaining.OrderBy(r => Math.Abs(r - current)).First();
totalMoves += Math.Abs(closest - current);
current = closest;
remaining.Remove(closest);
}
return totalMoves;
}
86. Backtracking Approach
Strategy:
- Build solution incrementally
- When stuck, backtrack to previous decision point
- Try alternative choices
- Continue until solution found or all possibilities exhausted
Common Applications:
- N-Queens Problem
- Sudoku Solver
- Subset Sum Problem
- Graph Coloring
- Hamiltonian Cycle
Example Implementation:
static void SolveNQueensBacktrack(bool[][] board, int col, List<bool[][]> solutions)
{
if (col >= board.Length)
{
// Solution found
var solution = new bool[board.Length][];
for (int i = 0; i < board.Length; i++)
{
solution[i] = (bool[])board[i].Clone();
}
solutions.Add(solution);
return;
}
for (int row = 0; row < board.Length; row++)
{
if (IsSafe(board, row, col))
{
board[row][col] = true; // Make choice
SolveNQueensBacktrack(board, col + 1, solutions);
board[row][col] = false; // Backtrack
}
}
}
87. Exact vs Approximation Algorithms
Key Differences:
| Aspect | Exact Algorithms | Approximation Algorithms |
|---|---|---|
| Solution Quality | Guaranteed optimal | Near-optimal (within factor) |
| Time Complexity | Often exponential | Usually polynomial |
| Use Cases | Small problems, exact solutions required | Large problems, good enough solutions |
| Examples | Dynamic Programming, Branch and Bound | Greedy, Heuristic methods |
Example Implementation:
// Exact: Dynamic Programming (guaranteed optimal)
static int ExactKnapsack(int[] weights, int[] values, int capacity)
{
int n = weights.Length;
int[,] dp = new int[n + 1, capacity + 1];
for (int i = 1; i <= n; i++)
{
for (int w = 0; w <= capacity; w++)
{
if (weights[i - 1] <= w)
{
dp[i, w] = Math.Max(dp[i - 1, w],
dp[i - 1, w - weights[i - 1]] + values[i - 1]);
}
else
{
dp[i, w] = dp[i - 1, w];
}
}
}
return dp[n, capacity];
}
// Approximation: Greedy (may not be optimal)
static int ApproximateKnapsack(int[] weights, int[] values, int capacity)
{
var items = new List<(int weight, int value, double ratio)>();
for (int i = 0; i < weights.Length; i++)
{
items.Add((weights[i], values[i], (double)values[i] / weights[i]));
}
items.Sort((a, b) => b.ratio.CompareTo(a.ratio));
int totalValue = 0;
int remainingCapacity = capacity;
foreach (var item in items)
{
if (item.weight <= remainingCapacity)
{
totalValue += item.value;
remainingCapacity -= item.weight;
}
}
return totalValue;
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace AlgorithmConcepts
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Algorithm Concepts Demo");
Console.WriteLine("======================");
// Run all demonstrations
DeterministicVsRandomizedDemo();
DivideAndConquerDemo();
IterativeVsRecursiveDemo();
GreedyApproachDemo();
OnlineVsOfflineDemo();
BacktrackingDemo();
ExactVsApproximationDemo();
BranchAndBoundDemo();
SerialVsParallelDemo();
GeneticAlgorithmDemo();
}
#region 81. Deterministic vs Randomized Algorithms
static void DeterministicVsRandomizedDemo()
{
Console.WriteLine("\n81. Deterministic vs Randomized Algorithms");
Console.WriteLine("==========================================");
// Deterministic Algorithm - Always produces same output for same input
int[] arr = { 64, 34, 25, 12, 22, 11, 90 };
Console.WriteLine("Original array: " + string.Join(", ", arr));
var deterministicSorted = DeterministicQuickSort(arr.ToArray());
Console.WriteLine("Deterministic QuickSort: " + string.Join(", ", deterministicSorted));
// Randomized Algorithm - May produce different outputs for same input
var randomizedSorted = RandomizedQuickSort(arr.ToArray());
Console.WriteLine("Randomized QuickSort: " + string.Join(", ", randomizedSorted));
Console.WriteLine("Key Difference: Deterministic always same result, Randomized may vary");
}
// Deterministic QuickSort (always chooses last element as pivot)
static int[] DeterministicQuickSort(int[] arr)
{
if (arr.Length <= 1) return arr;
int pivot = arr[arr.Length - 1]; // Always last element
var left = new List<int>();
var right = new List<int>();
for (int i = 0; i < arr.Length - 1; i++)
{
if (arr[i] <= pivot) left.Add(arr[i]);
else right.Add(arr[i]);
}
return DeterministicQuickSort(left.ToArray())
.Concat(new[] { pivot })
.Concat(DeterministicQuickSort(right.ToArray()))
.ToArray();
}
// Randomized QuickSort (chooses random pivot)
static int[] RandomizedQuickSort(int[] arr)
{
if (arr.Length <= 1) return arr;
Random rand = new Random();
int pivotIndex = rand.Next(arr.Length);
int pivot = arr[pivotIndex];
var left = new List<int>();
var right = new List<int>();
for (int i = 0; i < arr.Length; i++)
{
if (i != pivotIndex)
{
if (arr[i] <= pivot) left.Add(arr[i]);
else right.Add(arr[i]);
}
}
return RandomizedQuickSort(left.ToArray())
.Concat(new[] { pivot })
.Concat(RandomizedQuickSort(right.ToArray()))
.ToArray();
}
#endregion
#region 82. Divide and Conquer Approach
static void DivideAndConquerDemo()
{
Console.WriteLine("\n82. Divide and Conquer Approach");
Console.WriteLine("===============================");
int[] arr = { 3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5 };
Console.WriteLine("Array: " + string.Join(", ", arr));
// Merge Sort (Divide and Conquer)
var sorted = MergeSort(arr);
Console.WriteLine("Merge Sort Result: " + string.Join(", ", sorted));
// Binary Search (Divide and Conquer)
int target = 5;
int index = BinarySearch(sorted, target);
Console.WriteLine($"Binary Search for {target}: Found at index {index}");
}
static int[] MergeSort(int[] arr)
{
if (arr.Length <= 1) return arr;
// Divide
int mid = arr.Length / 2;
int[] left = arr.Take(mid).ToArray();
int[] right = arr.Skip(mid).ToArray();
// Conquer
left = MergeSort(left);
right = MergeSort(right);
// Combine
return Merge(left, right);
}
static int[] Merge(int[] left, int[] right)
{
var result = new List<int>();
int i = 0, j = 0;
while (i < left.Length && j < right.Length)
{
if (left[i] <= right[j])
{
result.Add(left[i]);
i++;
}
else
{
result.Add(right[j]);
j++;
}
}
result.AddRange(left.Skip(i));
result.AddRange(right.Skip(j));
return result.ToArray();
}
static int BinarySearch(int[] arr, int target)
{
return BinarySearchRecursive(arr, target, 0, arr.Length - 1);
}
static int BinarySearchRecursive(int[] arr, int target, int left, int right)
{
if (left > right) return -1;
int mid = (left + right) / 2;
if (arr[mid] == target) return mid;
if (arr[mid] > target) return BinarySearchRecursive(arr, target, left, mid - 1);
return BinarySearchRecursive(arr, target, mid + 1, right);
}
#endregion
#region 83. Iterative vs Recursive Algorithms
static void IterativeVsRecursiveDemo()
{
Console.WriteLine("\n83. Iterative vs Recursive Algorithms");
Console.WriteLine("====================================");
int n = 10;
// Iterative Fibonacci
int iterativeFib = IterativeFibonacci(n);
Console.WriteLine($"Iterative Fibonacci({n}) = {iterativeFib}");
// Recursive Fibonacci
int recursiveFib = RecursiveFibonacci(n);
Console.WriteLine($"Recursive Fibonacci({n}) = {recursiveFib}");
// Iterative Factorial
long iterativeFact = IterativeFactorial(n);
Console.WriteLine($"Iterative Factorial({n}) = {iterativeFact}");
// Recursive Factorial
long recursiveFact = RecursiveFactorial(n);
Console.WriteLine($"Recursive Factorial({n}) = {recursiveFact}");
}
static int IterativeFibonacci(int n)
{
if (n <= 1) return n;
int a = 0, b = 1, c = 0;
for (int i = 2; i <= n; i++)
{
c = a + b;
a = b;
b = c;
}
return c;
}
static int RecursiveFibonacci(int n)
{
if (n <= 1) return n;
return RecursiveFibonacci(n - 1) + RecursiveFibonacci(n - 2);
}
static long IterativeFactorial(int n)
{
long result = 1;
for (int i = 2; i <= n; i++)
{
result *= i;
}
return result;
}
static long RecursiveFactorial(int n)
{
if (n <= 1) return 1;
return n * RecursiveFactorial(n - 1);
}
#endregion
#region 84. Greedy Approach
static void GreedyApproachDemo()
{
Console.WriteLine("\n84. Greedy Approach");
Console.WriteLine("==================");
// Activity Selection Problem
var activities = new List<Activity>
{
new Activity(1, 4),
new Activity(3, 5),
new Activity(0, 6),
new Activity(5, 7),
new Activity(3, 8),
new Activity(5, 9),
new Activity(6, 10),
new Activity(8, 11),
new Activity(8, 12),
new Activity(2, 13),
new Activity(12, 14)
};
var selectedActivities = GreedyActivitySelection(activities);
Console.WriteLine("Activity Selection Problem:");
Console.WriteLine($"Selected {selectedActivities.Count} activities:");
foreach (var activity in selectedActivities)
{
Console.WriteLine($"Activity: {activity.Start}-{activity.End}");
}
// Coin Change Problem
int[] coins = { 1, 2, 5, 10, 20, 50, 100 };
int amount = 93;
var coinChange = GreedyCoinChange(coins, amount);
Console.WriteLine($"\nCoin Change for {amount}:");
foreach (var coin in coinChange)
{
Console.WriteLine($"Use {coin.Value} coin {coin.Count} times");
}
}
class Activity
{
public int Start { get; set; }
public int End { get; set; }
public Activity(int start, int end)
{
Start = start;
End = end;
}
}
static List<Activity> GreedyActivitySelection(List<Activity> activities)
{
var sorted = activities.OrderBy(a => a.End).ToList();
var selected = new List<Activity>();
if (sorted.Count > 0)
{
selected.Add(sorted[0]);
int lastEnd = sorted[0].End;
for (int i = 1; i < sorted.Count; i++)
{
if (sorted[i].Start >= lastEnd)
{
selected.Add(sorted[i]);
lastEnd = sorted[i].End;
}
}
}
return selected;
}
class CoinCount
{
public int Value { get; set; }
public int Count { get; set; }
}
static List<CoinCount> GreedyCoinChange(int[] coins, int amount)
{
var result = new List<CoinCount>();
Array.Sort(coins);
for (int i = coins.Length - 1; i >= 0 && amount > 0; i--)
{
if (coins[i] <= amount)
{
int count = amount / coins[i];
result.Add(new CoinCount { Value = coins[i], Count = count });
amount %= coins[i];
}
}
return result;
}
#endregion
#region 85. Online vs Offline Algorithms
static void OnlineVsOfflineDemo()
{
Console.WriteLine("\n85. Online vs Offline Algorithms");
Console.WriteLine("===============================");
int[] requests = { 98, 183, 37, 122, 14, 124, 65, 67 };
int head = 53;
// Offline Algorithm (knows all requests in advance)
int offlineMoves = OfflineDiskScheduling(requests, head);
Console.WriteLine($"Offline Disk Scheduling (FCFS): {offlineMoves} moves");
// Online Algorithm (processes requests as they come)
int onlineMoves = OnlineDiskScheduling(requests, head);
Console.WriteLine($"Online Disk Scheduling (SSTF): {onlineMoves} moves");
Console.WriteLine("Key Difference: Offline has full information, Online processes incrementally");
}
static int OfflineDiskScheduling(int[] requests, int head)
{
// First-Come-First-Serve (FCFS) - processes in order received
int totalMoves = 0;
int current = head;
foreach (int request in requests)
{
totalMoves += Math.Abs(request - current);
current = request;
}
return totalMoves;
}
static int OnlineDiskScheduling(int[] requests, int head)
{
// Shortest Seek Time First (SSTF) - always moves to closest request
var remaining = new List<int>(requests);
int totalMoves = 0;
int current = head;
while (remaining.Count > 0)
{
int closest = remaining[0];
int minDistance = Math.Abs(closest - current);
for (int i = 1; i < remaining.Count; i++)
{
int distance = Math.Abs(remaining[i] - current);
if (distance < minDistance)
{
minDistance = distance;
closest = remaining[i];
}
}
totalMoves += minDistance;
current = closest;
remaining.Remove(closest);
}
return totalMoves;
}
#endregion
#region 86. Backtracking Approach
static void BacktrackingDemo()
{
Console.WriteLine("\n86. Backtracking Approach");
Console.WriteLine("========================");
// N-Queens Problem
int n = 4;
var queensSolution = SolveNQueens(n);
Console.WriteLine($"N-Queens Solution for {n}x{n} board:");
foreach (var solution in queensSolution)
{
Console.WriteLine("Solution:");
for (int i = 0; i < solution.Length; i++)
{
for (int j = 0; j < solution[i].Length; j++)
{
Console.Write(solution[i][j] ? "Q " : ". ");
}
Console.WriteLine();
}
Console.WriteLine();
}
// Subset Sum Problem
int[] numbers = { 3, 1, 7, 8, 2 };
int targetSum = 10;
var subsets = FindSubsetSum(numbers, targetSum);
Console.WriteLine($"Subset Sum for target {targetSum}:");
foreach (var subset in subsets)
{
Console.WriteLine(string.Join(" + ", subset) + " = " + targetSum);
}
}
static List<bool[][]> SolveNQueens(int n)
{
var solutions = new List<bool[][]>();
var board = new bool[n][];
for (int i = 0; i < n; i++)
{
board[i] = new bool[n];
}
SolveNQueensBacktrack(board, 0, solutions);
return solutions;
}
static void SolveNQueensBacktrack(bool[][] board, int col, List<bool[][]> solutions)
{
if (col >= board.Length)
{
var solution = new bool[board.Length][];
for (int i = 0; i < board.Length; i++)
{
solution[i] = (bool[])board[i].Clone();
}
solutions.Add(solution);
return;
}
for (int row = 0; row < board.Length; row++)
{
if (IsSafe(board, row, col))
{
board[row][col] = true;
SolveNQueensBacktrack(board, col + 1, solutions);
board[row][col] = false; // Backtrack
}
}
}
static bool IsSafe(bool[][] board, int row, int col)
{
int n = board.Length;
// Check row
for (int j = 0; j < col; j++)
{
if (board[row][j]) return false;
}
// Check upper diagonal
for (int i = row, j = col; i >= 0 && j >= 0; i--, j--)
{
if (board[i][j]) return false;
}
// Check lower diagonal
for (int i = row, j = col; i < n && j >= 0; i++, j--)
{
if (board[i][j]) return false;
}
return true;
}
static List<List<int>> FindSubsetSum(int[] numbers, int target)
{
var solutions = new List<List<int>>();
FindSubsetSumBacktrack(numbers, target, 0, new List<int>(), 0, solutions);
return solutions;
}
static void FindSubsetSumBacktrack(int[] numbers, int target, int index, List<int> current, int currentSum, List<List<int>> solutions)
{
if (currentSum == target)
{
solutions.Add(new List<int>(current));
return;
}
if (currentSum > target || index >= numbers.Length)
{
return;
}
// Include current number
current.Add(numbers[index]);
FindSubsetSumBacktrack(numbers, target, index + 1, current, currentSum + numbers[index], solutions);
current.RemoveAt(current.Count - 1); // Backtrack
// Exclude current number
FindSubsetSumBacktrack(numbers, target, index + 1, current, currentSum, solutions);
}
#endregion
#region 87. Exact vs Approximation Algorithms
static void ExactVsApproximationDemo()
{
Console.WriteLine("\n87. Exact vs Approximation Algorithms");
Console.WriteLine("=====================================");
int[] weights = { 2, 3, 4, 5 };
int[] values = { 3, 4, 5, 6 };
int capacity = 10;
// Exact Algorithm (Dynamic Programming)
int exactValue = ExactKnapsack(weights, values, capacity);
Console.WriteLine($"Exact Knapsack Value: {exactValue}");
// Approximation Algorithm (Greedy)
int approxValue = ApproximateKnapsack(weights, values, capacity);
Console.WriteLine($"Approximate Knapsack Value: {approxValue}");
double approximationRatio = (double)approxValue / exactValue;
Console.WriteLine($"Approximation Ratio: {approximationRatio:F2}");
}
static int ExactKnapsack(int[] weights, int[] values, int capacity)
{
int n = weights.Length;
int[,] dp = new int[n + 1, capacity + 1];
for (int i = 1; i <= n; i++)
{
for (int w = 0; w <= capacity; w++)
{
if (weights[i - 1] <= w)
{
dp[i, w] = Math.Max(dp[i - 1, w], dp[i - 1, w - weights[i - 1]] + values[i - 1]);
}
else
{
dp[i, w] = dp[i - 1, w];
}
}
}
return dp[n, capacity];
}
static int ApproximateKnapsack(int[] weights, int[] values, int capacity)
{
var items = new List<(int weight, int value, double ratio)>();
for (int i = 0; i < weights.Length; i++)
{
items.Add((weights[i], values[i], (double)values[i] / weights[i]));
}
items.Sort((a, b) => b.ratio.CompareTo(a.ratio)); // Sort by value/weight ratio
int totalValue = 0;
int remainingCapacity = capacity;
foreach (var item in items)
{
if (item.weight <= remainingCapacity)
{
totalValue += item.value;
remainingCapacity -= item.weight;
}
}
return totalValue;
}
#endregion
#region 88. Branch and Bound Approach
static void BranchAndBoundDemo()
{
Console.WriteLine("\n88. Branch and Bound Approach");
Console.WriteLine("=============================");
int[,] costMatrix = {
{ 0, 10, 15, 20 },
{ 10, 0, 35, 25 },
{ 15, 35, 0, 30 },
{ 20, 25, 30, 0 }
};
var tspSolution = BranchAndBoundTSP(costMatrix);
Console.WriteLine($"TSP Optimal Cost: {tspSolution.Cost}");
Console.WriteLine($"TSP Path: {string.Join(" -> ", tspSolution.Path)}");
}
class TSPNode
{
public int Cost { get; set; }
public int Level { get; set; }
public List<int> Path { get; set; }
public int[,] ReducedMatrix { get; set; }
public int Bound { get; set; }
}
class TSPSolution
{
public int Cost { get; set; }
public List<int> Path { get; set; }
}
static TSPSolution BranchAndBoundTSP(int[,] costMatrix)
{
int n = costMatrix.GetLength(0);
var pq = new PriorityQueue<TSPNode, int>();
// Create root node
var root = new TSPNode
{
Level = 0,
Path = new List<int> { 0 },
ReducedMatrix = (int[,])costMatrix.Clone(),
Cost = 0
};
root.Bound = CalculateBound(root.ReducedMatrix, root.Path);
pq.Enqueue(root, root.Bound);
int bestCost = int.MaxValue;
List<int> bestPath = null;
while (pq.Count > 0)
{
var current = pq.Dequeue();
if (current.Bound >= bestCost)
continue;
if (current.Level == n - 1)
{
// Add return to start
int finalCost = current.Cost + costMatrix[current.Path[current.Path.Count - 1], 0];
if (finalCost < bestCost)
{
bestCost = finalCost;
bestPath = new List<int>(current.Path) { 0 };
}
continue;
}
// Generate children
for (int i = 0; i < n; i++)
{
if (!current.Path.Contains(i))
{
var child = new TSPNode
{
Level = current.Level + 1,
Path = new List<int>(current.Path) { i },
Cost = current.Cost + costMatrix[current.Path[current.Path.Count - 1], i]
};
child.Bound = CalculateBound(child.ReducedMatrix, child.Path);
if (child.Bound < bestCost)
{
pq.Enqueue(child, child.Bound);
}
}
}
}
return new TSPSolution { Cost = bestCost, Path = bestPath };
}
static int CalculateBound(int[,] matrix, List<int> path)
{
// Simplified bound calculation
int bound = 0;
int n = matrix.GetLength(0);
// Add cost of current path
for (int i = 0; i < path.Count - 1; i++)
{
bound += matrix[path[i], path[i + 1]];
}
// Add minimum cost for remaining cities
for (int i = 0; i < n; i++)
{
if (!path.Contains(i))
{
int minCost = int.MaxValue;
for (int j = 0; j < n; j++)
{
if (i != j && matrix[i, j] < minCost)
{
minCost = matrix[i, j];
}
}
bound += minCost;
}
}
return bound;
}
#endregion
#region 89. Serial vs Parallel Algorithms
static void SerialVsParallelDemo()
{
Console.WriteLine("\n89. Serial vs Parallel Algorithms");
Console.WriteLine("=================================");
int[] largeArray = Enumerable.Range(1, 1000000).ToArray();
// Serial Processing
var serialStart = DateTime.Now;
long serialSum = SerialArraySum(largeArray);
var serialTime = DateTime.Now - serialStart;
Console.WriteLine($"Serial Sum: {serialSum}, Time: {serialTime.TotalMilliseconds}ms");
// Parallel Processing
var parallelStart = DateTime.Now;
long parallelSum = ParallelArraySum(largeArray);
var parallelTime = DateTime.Now - parallelStart;
Console.WriteLine($"Parallel Sum: {parallelSum}, Time: {parallelTime.TotalMilliseconds}ms");
Console.WriteLine($"Speedup: {serialTime.TotalMilliseconds / parallelTime.TotalMilliseconds:F2}x");
}
static long SerialArraySum(int[] array)
{
long sum = 0;
for (int i = 0; i < array.Length; i++)
{
sum += array[i];
}
return sum;
}
static long ParallelArraySum(int[] array)
{
return array.AsParallel().Sum(x => (long)x);
}
#endregion
#region 90. Genetic Algorithm Approach
static void GeneticAlgorithmDemo()
{
Console.WriteLine("\n90. Genetic Algorithm Approach");
Console.WriteLine("==============================");
// Solve simple optimization problem: find maximum of f(x) = x^2
var geneticSolution = GeneticAlgorithmOptimization();
Console.WriteLine($"Genetic Algorithm found maximum at x = {geneticSolution:F2}");
Console.WriteLine($"Function value: {geneticSolution * geneticSolution:F2}");
}
class Individual
{
public double Gene { get; set; }
public double Fitness { get; set; }
public Individual(double gene)
{
Gene = gene;
Fitness = CalculateFitness(gene);
}
private double CalculateFitness(double x)
{
return x * x; // f(x) = x^2
}
}
static double GeneticAlgorithmOptimization()
{
Random rand = new Random();
int populationSize = 50;
int generations = 100;
double mutationRate = 0.1;
double crossoverRate = 0.8;
// Initialize population
var population = new List<Individual>();
for (int i = 0; i < populationSize; i++)
{
population.Add(new Individual(rand.NextDouble() * 10)); // Random value between 0-10
}
for (int generation = 0; generation < generations; generation++)
{
// Selection (Tournament Selection)
var newPopulation = new List<Individual>();
while (newPopulation.Count < populationSize)
{
// Select parents
var parent1 = TournamentSelection(population, rand);
var parent2 = TournamentSelection(population, rand);
// Crossover
if (rand.NextDouble() < crossoverRate)
{
var (child1, child2) = Crossover(parent1, parent2, rand);
newPopulation.Add(child1);
if (newPopulation.Count < populationSize)
newPopulation.Add(child2);
}
else
{
newPopulation.Add(new Individual(parent1.Gene));
if (newPopulation.Count < populationSize)
newPopulation.Add(new Individual(parent2.Gene));
}
}
// Mutation
for (int i = 0; i < newPopulation.Count; i++)
{
if (rand.NextDouble() < mutationRate)
{
newPopulation[i] = Mutate(newPopulation[i], rand);
}
}
population = newPopulation;
}
// Return best individual
return population.OrderByDescending(x => x.Fitness).First().Gene;
}
static Individual TournamentSelection(List<Individual> population, Random rand)
{
int tournamentSize = 3;
var tournament = new List<Individual>();
for (int i = 0; i < tournamentSize; i++)
{
tournament.Add(population[rand.Next(population.Count)]);
}
return tournament.OrderByDescending(x => x.Fitness).First();
}
static (Individual child1, Individual child2) Crossover(Individual parent1, Individual parent2, Random rand)
{
double alpha = rand.NextDouble();
double child1Gene = alpha * parent1.Gene + (1 - alpha) * parent2.Gene;
double child2Gene = (1 - alpha) * parent1.Gene + alpha * parent2.Gene;
return (new Individual(child1Gene), new Individual(child2Gene));
}
static Individual Mutate(Individual individual, Random rand)
{
double mutation = (rand.NextDouble() - 0.5) * 2; // Random value between -1 and 1
return new Individual(individual.Gene + mutation);
}
#endregion
}
// Simple Priority Queue implementation
public class PriorityQueue<T, TPriority> where TPriority : IComparable<TPriority>
{
private List<(T item, TPriority priority)> elements = new List<(T, TPriority)>();
public int Count => elements.Count;
public void Enqueue(T item, TPriority priority)
{
elements.Add((item, priority));
elements.Sort((a, b) => a.priority.CompareTo(b.priority));
}
public T Dequeue()
{
if (elements.Count == 0)
throw new InvalidOperationException("Queue is empty");
var item = elements[0].item;
elements.RemoveAt(0);
return item;
}
}
}