An open API service indexing awesome lists of open source software.

https://github.com/arcsymer/algorithms

Idiomatic C#/.NET 10 algorithms & data-structures library built from scratch — 11 structures, 16 algorithms, 129 tests, 7 Codility-style katas
https://github.com/arcsymer/algorithms

algorithms csharp data-structures dotnet interview-prep portfolio

Last synced: about 4 hours ago
JSON representation

Idiomatic C#/.NET 10 algorithms & data-structures library built from scratch — 11 structures, 16 algorithms, 129 tests, 7 Codility-style katas

Awesome Lists containing this project

README

          

# AlgoLib — Algorithms & Data Structures in C#

> A C# (.NET 10) library of classic data structures and algorithms, built from scratch with no external algorithmic dependencies.

[![CI](https://img.shields.io/github/actions/workflow/status/arcsymer/algorithms/ci.yml?branch=main&label=CI&logo=github)](https://github.com/arcsymer/algorithms/actions/workflows/ci.yml)
[![Tests](https://img.shields.io/badge/tests-265%20passing-brightgreen)](https://github.com/arcsymer/algorithms/actions)
[![Coverage](https://img.shields.io/badge/line%20coverage-%7E94%25-brightgreen)](https://github.com/arcsymer/algorithms/actions)
[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4?logo=dotnet)](https://dotnet.microsoft.com/)

---

## What & Why

AlgoLib implements 14 data structures and 23 algorithms in C# with generics, null-safety, and xUnit tests. I wrote it to keep my CS fundamentals sharp at interview depth (Codility/LeetCode tier), without hiding the complexity behind BCL wrappers.

A few decisions worth noting:
- Separate chaining for HashMap: simple, cache-friendly chains; load factor 0.75 triggers doubling.
- Circular buffer for Queue/Deque: amortized O(1) at both ends without node allocation.
- Floyd's heapify in the MinHeap constructor: O(n) build instead of O(n log n) repeated inserts.
- Kahn's BFS for topological sort: cleaner cycle detection than DFS coloring.
- Median-of-three pivot in QuickSort: avoids O(n²) on already-sorted inputs.
- LRU Cache uses sentinel-head doubly-linked list + Dictionary: both Get and Put are strictly O(1).
- Fenwick Tree (BIT) stores 1-indexed internally, exposes 0-indexed API: O(log n) update and prefix query, vs O(1) query of static prefix sums when the array changes between queries.
- QuickSelect uses a random pivot (seeded for reproducibility) to defeat adversarial sorted-input worst cases.
- Bellman-Ford relaxes V-1 times then runs a V-th pass to detect negative cycles; early exit when no update occurs in a pass.
- Segment Tree (lazy propagation) defers range-add updates with a lazy array; pushes pending delta only when a node is actually visited, achieving O(log n) per range update and range query.
- Rabin-Karp uses a rolling polynomial hash (base 131, mod 10^9+7) to slide the window in O(1) per step; falls back to character comparison only on hash match to handle collisions.
- Z-function computes the Z-array in O(n) by maintaining a "Z-box" [l, r) — the rightmost interval where z[l] is known — reusing previous values without re-scanning.
- Floyd-Warshall runs the standard DP relaxation in-place; the O(V³) loop is safe because dist[i][k] and dist[k][j] are fixed for the current k iteration.

---

## Contents

### Data Structures (14)

| Name | File |
|---|---|
| DynamicArray | `src/AlgoLib/DataStructures/DynamicArray.cs` |
| SinglyLinkedList | `src/AlgoLib/DataStructures/SinglyLinkedList.cs` |
| DoublyLinkedList | `src/AlgoLib/DataStructures/DoublyLinkedList.cs` |
| Stack | `src/AlgoLib/DataStructures/Stack.cs` |
| Queue | `src/AlgoLib/DataStructures/Queue.cs` |
| Deque | `src/AlgoLib/DataStructures/Deque.cs` |
| BinarySearchTree | `src/AlgoLib/DataStructures/BinarySearchTree.cs` |
| MinHeap | `src/AlgoLib/DataStructures/MinHeap.cs` |
| HashMap | `src/AlgoLib/DataStructures/HashMap.cs` |
| UnionFind | `src/AlgoLib/DataStructures/UnionFind.cs` |
| Trie | `src/AlgoLib/DataStructures/Trie.cs` |
| **FenwickTree** (BIT) | `src/AlgoLib/DataStructures/FenwickTree.cs` |
| **LruCache** | `src/AlgoLib/DataStructures/LruCache.cs` |
| **SegmentTree** (lazy propagation) | `src/AlgoLib/DataStructures/SegmentTree.cs` |

### Algorithms (23)

| Name | File |
|---|---|
| QuickSort, MergeSort, HeapSort, CountingSort | `src/AlgoLib/Sorting/SortingAlgorithms.cs` |
| BinarySearch, LowerBound, UpperBound, CountOccurrences | `src/AlgoLib/Searching/BinarySearch.cs` |
| **QuickSelect** (KthSmallest / KthLargest) | `src/AlgoLib/Searching/QuickSelect.cs` |
| BFS, DFS, Dijkstra, Topological Sort, **Bellman-Ford** | `src/AlgoLib/Graphs/GraphAlgorithms.cs` |
| **Floyd-Warshall** (all-pairs shortest paths) | `src/AlgoLib/Graphs/FloydWarshall.cs` |
| KMP Search | `src/AlgoLib/Strings/KmpSearch.cs` |
| **Rabin-Karp** (rolling-hash search) | `src/AlgoLib/Strings/RabinKarp.cs` |
| **Z-Function** (Z-array + pattern search + period) | `src/AlgoLib/Strings/ZFunction.cs` |
| Two Pointers, Sliding Window, Prefix Sums, Kadane | `src/AlgoLib/Strings/ArrayTechniques.cs` |

---

## Complexity Reference Table

### Data Structures

| Structure | Operation | Time (avg) | Time (worst) | Space |
|---|---|---|---|---|
| **DynamicArray** | Get/Set by index | O(1) | O(1) | O(n) |
| | Add (append) | O(1) amortized | O(n) grow | |
| | Insert at index | O(n) | O(n) | |
| | RemoveAt | O(n) | O(n) | |
| | IndexOf | O(n) | O(n) | |
| **SinglyLinkedList** | AddFirst / AddLast | O(1) | O(1) | O(n) |
| | RemoveFirst | O(1) | O(1) | |
| | RemoveLast | O(n) | O(n) | |
| | Find | O(n) | O(n) | |
| **DoublyLinkedList** | AddFirst / AddLast | O(1) | O(1) | O(n) |
| | RemoveFirst / RemoveLast | O(1) | O(1) | |
| | Remove(node) | O(1) | O(1) | |
| | Find | O(n) | O(n) | |
| **Stack** | Push / Pop / Peek | O(1) amortized | O(n) grow | O(n) |
| **Queue** | Enqueue / Dequeue / Peek | O(1) amortized | O(n) grow | O(n) |
| **Deque** | AddFront/Back, RemoveFront/Back | O(1) amortized | O(n) grow | O(n) |
| **BinarySearchTree** | Insert / Contains / Remove | O(log n) | O(n) unbalanced | O(n) |
| | Min / Max | O(log n) | O(n) | |
| | InOrder traversal | O(n) | O(n) | |
| **MinHeap** | Push | O(log n) | O(log n) | O(n) |
| | Pop (extract min) | O(log n) | O(log n) | |
| | Peek | O(1) | O(1) | |
| | Build from n elements | O(n) | O(n) | |
| **HashMap** | Put / Get / Remove | O(1) avg | O(n) worst | O(n) |
| | ContainsKey | O(1) avg | O(n) worst | |
| **UnionFind** | Find / Union / Connected | O(α(n)) | O(α(n)) | O(n) |
| **Trie** | Insert / Search / StartsWith / Delete | O(L) | O(L) | O(α·N·L) |
| **FenwickTree** | Update (point add) | O(log n) | O(log n) | O(n) |
| | PrefixSum / RangeSum | O(log n) | O(log n) | |
| | Build from array | O(n log n) | O(n log n) | |
| **LruCache** | Get | O(1) | O(1) | O(capacity) |
| | Put (with eviction) | O(1) | O(1) | |
| **SegmentTree** | RangeAdd (lazy) | O(log n) | O(log n) | O(n) |
| | RangeSum | O(log n) | O(log n) | |
| | PointGet | O(log n) | O(log n) | |
| | Build from array | O(n) | O(n) | |

α(n) = inverse Ackermann — effectively O(1) in practice.
L = length of string key.

---

### Algorithms

| Algorithm | Time | Space | Notes |
|---|---|---|---|
| **QuickSort** | O(n log n) avg | O(log n) | Median-of-three pivot; O(n²) worst case |
| **MergeSort** | O(n log n) | O(n) | Stable; consistent in all cases |
| **HeapSort** | O(n log n) | O(1) | In-place; not stable |
| **CountingSort** | O(n + k) | O(n + k) | k = max value; integers only |
| **BinarySearch** | O(log n) | O(1) | Sorted input required |
| **LowerBound** | O(log n) | O(1) | First index ≥ target |
| **UpperBound** | O(log n) | O(1) | First index > target |
| **CountOccurrences** | O(log n) | O(1) | Uses LowerBound + UpperBound |
| **BFS** | O(V + E) | O(V) | Shortest path in unweighted graph |
| **DFS** | O(V + E) | O(V) | Cycle detection, connectivity |
| **Dijkstra** | O((V+E) log V) | O(V) | Non-negative weights; binary heap |
| **Bellman-Ford** | O(V × E) | O(V) | Negative weights OK; detects negative cycles |
| **Floyd-Warshall** | O(V³) | O(V²) | All-pairs shortest paths; detects negative cycles |
| **Topological Sort** | O(V + E) | O(V) | Kahn's BFS algorithm; detects cycles |
| **QuickSelect** | O(n) avg, O(n²) worst | O(n) copy + O(log n) stack | k-th smallest/largest; random pivot |
| **KMP Search** | O(n + m) | O(m) | n = text, m = pattern length |
| **Rabin-Karp** | O(n + m) avg, O(nm) worst | O(1) | Rolling polynomial hash; collision-safe |
| **Z-Function** | O(n) build, O(n+m) search | O(n) | Z-box technique; also computes string period |
| **Two Pointers** | O(n) | O(1) | Sorted array pair/range queries |
| **Sliding Window** | O(n) | O(1) | Fixed or variable window; sum/length |
| **Prefix Sums** | O(n) build, O(1) query | O(n) | Range sum in O(1) after preprocessing |
| **Kadane** | O(n) | O(1) | Maximum subarray sum with index tracking |

---

## Katas (Codility-style)

Seven worked problems, each with an annotated solution and complexity breakdown.

| # | Problem | Key Technique | Time | Link |
|---|---|---|---|---|
| 01 | Two Sum | Hash Map | O(n) | [katas/01-two-sum.md](katas/01-two-sum.md) |
| 02 | Maximum Subarray | Kadane's Algorithm | O(n) | [katas/02-max-subarray.md](katas/02-max-subarray.md) |
| 03 | Search in Rotated Sorted Array | Modified Binary Search | O(log n) | [katas/03-binary-search-rotated.md](katas/03-binary-search-rotated.md) |
| 04 | Number of Islands | BFS flood fill | O(R × C) | [katas/04-number-of-islands.md](katas/04-number-of-islands.md) |
| 05 | Longest Substring Without Repeating | Sliding Window + Hash Map | O(n) | [katas/05-longest-substring-no-repeat.md](katas/05-longest-substring-no-repeat.md) |
| 06 | Course Schedule | Kahn's Topological Sort | O(V + E) | [katas/06-course-schedule.md](katas/06-course-schedule.md) |
| 07 | Word Dictionary | Trie (Prefix Tree) | O(L) per op | [katas/07-word-search-trie.md](katas/07-word-search-trie.md) |

---

## Build & Test

Requires [.NET 10 SDK](https://dotnet.microsoft.com/download).

The SDK version is pinned via `global.json` (`10.0.300`, `rollForward: latestFeature`).

```bash
# Clone
git clone https://github.com/arcsymer/algorithms.git
cd algorithms

# Build the whole solution (Release, no warnings)
dotnet build AlgoLib.slnx -c Release

# Run all tests
dotnet test -c Release

# Run with line/branch coverage (coverlet.collector already in test project)
dotnet test -c Release --collect:"XPlat Code Coverage" --results-directory TestResults
```

Actual test output (Release):

```
Build succeeded.
0 Warning(s)
0 Error(s)

Passed! - Failed: 0, Passed: 265, Skipped: 0, Total: 265, Duration: 274 ms - AlgoLib.Tests.dll (net10.0)
```

**265 tests** pass across 22 test files (xUnit `[Fact]` + `[Theory]`/`[InlineData]`), mirroring the `src/` structure.

Coverage (Cobertura, via coverlet): **line ~94% / branch ~90%** (estimated).

---

## Demo

A runnable console showcase (`src/AlgoLib.Demo`) prints results for the key structures and algorithms — sortings, BFS/DFS/topological sort, Dijkstra/Bellman-Ford/Floyd-Warshall, KMP/Rabin-Karp/Z-function, LRU cache, Trie, heap, union-find, BST, Fenwick/segment trees.

```bash
dotnet run --project src/AlgoLib.Demo -c Release
```

Captured output: [`_Docs/screenshots/algorithms/demo.txt`](../_Docs/screenshots/algorithms/demo.txt).

## NuGet package

The library ships package metadata and can be packed into a `.nupkg`:

```bash
dotnet pack src/AlgoLib/AlgoLib.csproj -c Release
# -> src/AlgoLib/bin/Release/AlgoLib.1.0.0.nupkg (+ AlgoLib.1.0.0.snupkg symbols)
```

`PackageId=AlgoLib`, `Version=1.0.0`, MIT-licensed, zero runtime dependencies, ships XML docs.

---

## Performance

Measured with a Stopwatch harness (`bench/Program.cs`) in Release mode on .NET 10.
Each figure is the median of 5 runs; GC.Collect() called before each trial.
Hardware: Windows 11, Intel/AMD desktop (your mileage may vary by ±15%).

### Sorting — n = 1 000 000 random ints

| Algorithm | Median |
|---|---|
| QuickSort (median-of-three) | 98 ms |
| MergeSort | 118 ms |
| HeapSort | 130 ms |

QuickSort is fastest in practice due to cache-locality of the in-place partition.
HeapSort is slowest despite identical O(n log n) complexity — heap accesses jump around in memory.

### QuickSelect — n = 1 000 000, k = N/2 (median)

| Algorithm | Median |
|---|---|
| QuickSelect | 9 ms |

~11× faster than full QuickSort for a single order-statistic query.

### HashMap — n = 100 000

| Operation | Median |
|---|---|
| Put (including 2 rehashes) | 6 ms |
| Get (all hits, pre-filled) | 0 ms (< 1 ms) |

### LRU Cache — capacity = 1 000, ops = 100 000

| Operation | Median |
|---|---|
| Put (evict-heavy: 99k evictions) | 2 ms |
| Get (all hits, warm cache) | 1 ms |

### Fenwick Tree — n = 1 000 000

| Operation | Median |
|---|---|
| Build from array (n × Update) | 19 ms |
| RangeSum query × 100 000 | < 1 ms |
| Update (point add) × 100 000 | 2 ms |

For comparison, `BuildPrefixSums` + `RangeSum` (O(1) query, no updates): < 1 ms for 100 000 queries — identical throughput, but the static version cannot handle mid-stream updates.

### Dijkstra vs Bellman-Ford — V = 500, sparse directed graph

| Algorithm | Median |
|---|---|
| Dijkstra O((V+E) log V) | 10 µs |
| Bellman-Ford O(V×E) | 19 µs |

On small sparse graphs the difference is modest; on dense graphs or large V Bellman-Ford degrades faster.
Use Dijkstra when weights are non-negative; Bellman-Ford when negative weights or cycle detection are needed.

---

## Using AlgoLib as a Library

Reference the project directly (no NuGet package yet):

```xml

```

Example usage:

```csharp
using AlgoLib.DataStructures;
using AlgoLib.Sorting;
using AlgoLib.Searching;
using AlgoLib.Graphs;
using AlgoLib.Strings;

// Generic MinHeap
var heap = new MinHeap();
heap.Push(5); heap.Push(1); heap.Push(3);
Console.WriteLine(heap.Pop()); // 1

// In-place QuickSort (median-of-three pivot)
int[] arr = { 9, 3, 7, 1, 5 };
SortingAlgorithms.QuickSort(arr);
// arr is now [1, 3, 5, 7, 9]

// QuickSelect: 3rd smallest in O(n) average (original array untouched)
int kth = QuickSelect.KthSmallest(new[] { 9, 3, 7, 1, 5 }, 3); // 5

// LRU Cache (O(1) get/put)
var lru = new LruCache(capacity: 2);
lru.Put(1, "one"); lru.Put(2, "two");
lru.Get(1); // makes 1 MRU
lru.Put(3, "three"); // evicts key 2 (LRU)
Console.WriteLine(lru.TryGet(2, out _)); // False — evicted

// Fenwick Tree: point-update + range-sum in O(log n)
var bit = new FenwickTree(new long[] { 1, 3, 5, 7, 9 });
Console.WriteLine(bit.RangeSum(1, 3)); // 3+5+7 = 15
bit.Update(2, +10); // arr[2] += 10 → now 15
Console.WriteLine(bit.RangeSum(1, 3)); // 3+15+7 = 25

// Dijkstra shortest paths
var g = new WeightedGraph(5);
g.AddEdge(0, 1, 4); g.AddEdge(0, 2, 1);
g.AddEdge(2, 1, 2); g.AddEdge(1, 3, 1);
double[] dist = GraphAlgorithms.Dijkstra(g, source: 0);
// dist[3] == 4 (path: 0→2→1→3)

// Bellman-Ford: handles negative weights
var g2 = new WeightedGraph(3);
g2.AddEdge(0, 1, 4); g2.AddEdge(1, 2, -3);
double[] bfDist = GraphAlgorithms.BellmanFord(g2, source: 0);
// bfDist[2] == 1.0 (4 + (-3))

// Trie
var trie = new Trie();
trie.Insert("hello"); trie.Insert("world");
Console.WriteLine(trie.StartsWith("hel")); // True
Console.WriteLine(trie.Search("hell")); // False

// Segment Tree: range add + range sum in O(log n)
var seg = new SegmentTree(new long[] { 1, 2, 3, 4, 5 });
seg.RangeAdd(1, 3, 10); // arr[1..3] += 10 → [1,12,13,14,5]
Console.WriteLine(seg.RangeSum(1, 3)); // 39
Console.WriteLine(seg.PointGet(2)); // 13

// Rabin-Karp rolling-hash search
var rkMatches = RabinKarp.Search("abababab", "abab"); // [0, 2, 4]

// Z-Function pattern search
var zMatches = ZFunction.Search("abababab", "abab"); // [0, 2, 4]
int period = ZFunction.ShortestPeriod("ababab"); // 2

// Floyd-Warshall all-pairs shortest paths
var g3 = new WeightedGraph(3);
g3.AddEdge(0, 1, 1); g3.AddEdge(1, 2, 2); g3.AddEdge(0, 2, 4);
double[][] allDist = FloydWarshall.Compute(g3);
// allDist[0][2] == 3.0 (via 0→1→2, not direct 4)
var path = FloydWarshall.GetPath(g3, allDist, 0, 2); // [0, 1, 2]
```

---

## Project Structure

```
algorithms/
├── src/AlgoLib/
│ ├── DataStructures/ DynamicArray, SinglyLinkedList, DoublyLinkedList,
│ │ Stack, Queue, Deque, BinarySearchTree, MinHeap,
│ │ HashMap, UnionFind, Trie,
│ │ FenwickTree, LruCache, SegmentTree
│ ├── Sorting/ SortingAlgorithms (QuickSort, MergeSort, HeapSort, CountingSort)
│ ├── Searching/ BinarySearch (+ LowerBound, UpperBound, CountOccurrences),
│ │ QuickSelect (KthSmallest, KthLargest)
│ ├── Graphs/ Graph, WeightedGraph,
│ │ GraphAlgorithms (BFS, DFS, Dijkstra, TopoSort, BellmanFord),
│ │ FloydWarshall (all-pairs shortest paths + path reconstruction)
│ └── Strings/ KmpSearch, RabinKarp, ZFunction,
│ ArrayTechniques (TwoPointers, SlidingWindow, PrefixSums, Kadane)
├── src/AlgoLib.Demo/ Runnable console showcase of the whole library
├── tests/AlgoLib.Tests/ xUnit tests mirroring src structure (265 tests)
├── bench/ Stopwatch micro-benchmark harness (median of N runs)
├── katas/ 7 Codility-style problem walkthroughs
└── .github/workflows/ CI pipeline (dotnet build + test on push/PR)
```