Skip to content

🧠 Algorithms

Core Philosophy: Algorithms are the recipes for efficient computation. A Programmer doesn’t just memorize them but understands the underlying paradigms to choose the right tool for the specific constraints of the system.


Sorting is the foundation for many other optimizations (like binary search).

  • Quick Sort: A divide-and-conquer approach. Excellent average-case performance O(n log n).
  • Merge Sort: Stable and guarantees O(n log n) even in worst-case. Preferred for linked lists and when stability matters.
  • Heap Sort: O(n log n) and performs sorting in-place, making it memory efficient.
using System;
class Program
{
static void Main()
{
int[] numbers = { 10, 7, 8, 9, 1, 5 };
Console.WriteLine("Before sorting:");
PrintArray(numbers);
QuickSort(numbers, 0, numbers.Length - 1);
Console.WriteLine("\nAfter sorting:");
PrintArray(numbers);
}
static void QuickSort(int[] array, int low, int high)
{
if (low < high)
{
int pivotIndex = Partition(array, low, high);
QuickSort(array, low, pivotIndex - 1);
QuickSort(array, pivotIndex + 1, high);
}
}
static int Partition(int[] array, int low, int high)
{
int pivot = array[high];
int i = low - 1;
for (int j = low; j < high; j++)
{
if (array[j] <= pivot)
{
i++;
Swap(array, i, j);
}
}
Swap(array, i + 1, high);
return i + 1;
}
static void Swap(int[] array, int i, int j)
{
(array[i], array[j]) = (array[j], array[i]);
}
static void PrintArray(int[] array)
{
Console.WriteLine(string.Join(", ", array));
}
}

Finding data efficiently is the most common task in software engineering.

  • Binary Search: The gold standard for sorted data (O(log n)).
  • Breadth-First Search (BFS): Explores neighbors first. Ideal for finding the shortest path in unweighted graphs.
  • Depth-First Search (DFS): Explores branches deeply. Ideal for exhaustive searches and topological sorting.
using System;
public class BinarySearchExample
{
public static int BinarySearch(int[] array, int target)
{
int left = 0;
int right = array.Length - 1;
while (left <= right)
{
int middle = left + (right - left) / 2;
if (array[middle] == target)
{
return middle; // Target found
}
if (array[middle] < target)
{
left = middle + 1;
}
else
{
right = middle - 1;
}
}
return -1; // Target not found
}
public static void Main()
{
int[] numbers = { 2, 5, 8, 12, 16, 23, 38, 56, 72, 91 };
int target = 23;
int index = BinarySearch(numbers, target);
if (index != -1)
{
Console.WriteLine($"Found {target} at index {index}");
}
else
{
Console.WriteLine($"{target} was not found");
}
}
}

Greedy algorithms make the locally optimal choice at each step, hoping to find a global optimum.

  • Concept: They are fast and simple but don’t always yield the best solution.
  • Use Cases:
    • Dijkstra’s Algorithm: Finding the shortest path in a weighted graph.
    • Huffman Coding: Data compression.
    • Fractional Knapsack: When items can be broken into smaller pieces.
using System;
using System.Collections.Generic;
public class DijkstraExample
{
public static Dictionary<int, int> Dijkstra(
Dictionary<int, List<(int Neighbor, int Weight)>> graph,
int startNode)
{
var distances = new Dictionary<int, int>();
var priorityQueue = new PriorityQueue<int, int>();
// Initialize distances
foreach (var node in graph.Keys)
{
distances[node] = int.MaxValue;
}
distances[startNode] = 0;
priorityQueue.Enqueue(startNode, 0);
while (priorityQueue.Count > 0)
{
priorityQueue.TryDequeue(out int currentNode, out int currentDistance);
// Skip stale entries
if (currentDistance > distances[currentNode])
continue;
foreach (var (neighbor, weight) in graph[currentNode])
{
int newDistance = distances[currentNode] + weight;
if (newDistance < distances[neighbor])
{
distances[neighbor] = newDistance;
priorityQueue.Enqueue(neighbor, newDistance);
}
}
}
return distances;
}
public static void Main()
{
var graph = new Dictionary<int, List<(int Neighbor, int Weight)>>()
{
{ 0, new() { (1, 4), (2, 1) } },
{ 1, new() { (3, 1) } },
{ 2, new() { (1, 2), (3, 5) } },
{ 3, new() { } }
};
int startNode = 0;
var distances = Dijkstra(graph, startNode);
Console.WriteLine($"Shortest distances from node {startNode}:");
foreach (var kvp in distances)
{
Console.WriteLine($"Node {kvp.Key}: {kvp.Value}");
}
}
}

DP is used to solve complex problems by breaking them down into simpler subproblems. It is the way to handle recursion by avoiding redundant calculations.

  1. Memoization (Top-Down): Cache the results of expensive function calls and return the cached result when the same inputs occur again.
  2. Tabulation (Bottom-Up): Solve the smallest subproblems first and store their results in a table (usually an array or matrix).
  • 0/1 Knapsack: Making optimal choices with limited capacity.
  • Longest Common Subsequence: Used in diff tools and bioinformatics.
  • Fibonacci/Path Finding: The entry point for understanding state transitions.

StrategyWhen to UseTrade-off
GreedyWhen a β€œgood enough” or local optimum is sufficient.Might miss the global optimal solution.
Dynamic ProgrammingWhen subproblems overlap (e.g., calculating the same thing twice).Higher memory usage (Space Complexity).
Recursion vs. IterationWhen the problem is naturally recursive (Trees).Risk of StackOverflow in languages without Tail Call Optimization.

πŸ’‘ Note: Before implementing a complex DP solution, ask: β€œIs the dataset small enough that a simpler O(n^2) approach is more maintainable?” Often, code readability and maintenance cost are more important than shaving off a few milliseconds if n is consistently small.