
Category:.NET
C# Data Structures and Algorithms Tutorial: Production Examples with Code
Complete C# data structures and algorithms tutorial with production code examples. Learn arrays, dictionaries, queues, stacks, linked lists, trees, graphs, and sorting/searching algorithms used in real .NET applications. Includes time complexity cheat sheet and trading system examples.
If you're searching for a C# data structures and algorithms tutorial with real code examples you can actually use, you're in the right place. This guide covers arrays, dictionaries, queues, stacks, linked lists, trees, and graphs — with production C# examples drawn from trading systems, exchange APIs, and .NET applications.
Quick Reference: Time & Space Complexity
| Data Structure | Access | Search | Insert | Delete | Typical Use |
|---|---|---|---|---|---|
| Array | O(1) | O(n) | O(n) | O(n) | Fixed-size time-series data |
| Dictionary<TKey,TValue> | O(1) | O(1) | O(1) | O(1) | Lookup tables, caches |
| Stack<T> | O(n) | O(n) | O(1) | O(1) | Call stacks, undo operations |
| Queue<T> | O(n) | O(n) | O(1) | O(1) | Message buffers, task queues |
| LinkedList<T> | O(n) | O(n) | O(1) | O(1) | Order book chains |
| SortedDictionary | O(log n) | O(log n) | O(log n) | O(log n) | Order book depth |
| HashSet<T> | - | O(1) | O(1) | O(1) | Deduplication, tracking |
1. Arrays — Fixed-Size Collections for Time-Series Data
Arrays store elements in contiguous memory with O(1) index-based access. In C#, arrays are the foundation for time-series data like price candles (OHLCV).
// Trading: storing OHLCV price data
public record Ohlcv(decimal Open, decimal High, decimal Low, decimal Close, long Volume);
Ohlcv[] candles = new Ohlcv[100]; // Pre-allocate 100 candles
candles[0] = new Ohlcv(50000m, 51000m, 49800m, 50500m, 1250);
// Fast access by index for indicator calculation
int latest = candles.Length - 1;
decimal currentPrice = candles[latest].Close;When to use arrays: Fixed-size buffers, ring buffers for real-time data, pre-allocated lookup tables.
When not to use: Dynamic resizing (use List<T>), key-based lookups (use Dictionary<TKey, TValue>).
2. Dictionaries — O(1) Lookups for Order Tracking
Dictionary<TKey, TValue> maps unique keys to values using hash-based lookups. In trading systems, this is the go-to structure for tracking orders by client ID.
// Trading: track open orders by clientOrderId
ConcurrentDictionary<string, Order> openOrders = new();
// Insert: new order placed
openOrders.TryAdd(order.ClientOrderId, order);
// Lookup: order fill callback
if (openOrders.TryGetValue(clientOrderId, out var existing))
{
existing.FilledQuantity += fillQuantity;
}
// Remove: order fully filled
openOrders.TryRemove(clientOrderId, out _);When to use dictionaries: Lookup tables, order tracking, symbol-to-exchange mappings, configuration caches.
Production note: In multi-threaded trading bots, use ConcurrentDictionary<TKey, TValue> instead of the standard Dictionary to avoid race conditions during concurrent fill callbacks.
3. Queues — FIFO for Message Buffers and Execution
Queue<T> follows First-In-First-Out (FIFO). Perfect for buffering WebSocket messages or sequencing trade executions.
// Trading: buffer WebSocket trade updates for processing
Queue<TradeUpdate> tradeBuffer = new();
// Enqueue: incoming WebSocket message
void OnTradeUpdate(TradeUpdate update)
{
tradeBuffer.Enqueue(update);
}
// Process in order (background thread)
while (tradeBuffer.TryDequeue(out var update))
{
ProcessTrade(update);
}When to use queues: Message buffering, task scheduling, rate-limit bucketing, write-ahead logging.
Production note: In high-throughput scenarios, use Channel<T> (System.Threading.Channels) for bounded backpressure instead of an unbounded Queue.
4. Stacks — LIFO for Error Tracing and Undo
Stack<T> follows Last-In-First-Out (LIFO). Useful for tracing execution paths and undo operations.
// Error handling: track API call chain for debugging
Stack<string> callChain = new();
callChain.Push("PlaceOrder");
callChain.Push("CheckBalance");
callChain.Push("FetchPrice");
// On error, unwind to see full path
while (callChain.TryPop(out var step))
{
logger.LogError("Failed at step: {Step}", step);
}When to use stacks: Call stack tracing, undo/redo in trading UIs, expression tree evaluation.
5. Linked Lists — Sequential Data with Efficient Insertions
LinkedList<T> is a doubly-linked list where each node points to the next and previous. Insertions and deletions are O(1) at known positions — useful for order book chains.
// Trading: order book bid levels as linked list
LinkedList<PriceLevel> bidLevels = new();
// Insert new bid level
bidLevels.AddLast(new PriceLevel(50000m, 1.5m));
// Remove filled level
bidLevels.Remove(filledLevel);When to use linked lists: Frequent insertions/removals at arbitrary positions where arrays would require shifting elements.
When not to: Random access by index (use array), most real-world scenarios (use List<T>).
6. Trees — Hierarchical Data and Sorted Lookups
SortedDictionary<TKey, TValue> and SortedSet<T> are backed by balanced binary search trees. They maintain sorted order with O(log n) operations — critical for order book depth aggregation.
// Order book: aggregate volume by price level (sorted ascending)
SortedDictionary<decimal, decimal> orderBookDepth = new();
orderBookDepth[50000m] = 2.5m; // 2.5 BTC at $50,000
orderBookDepth[50100m] = 1.2m; // 1.2 BTC at $50,100
orderBookDepth[49900m] = 3.0m; // 3.0 BTC at $49,900
// Get best bid (highest price ≤ current)
decimal bestBid = orderBookDepth.Keys
.Where(k => k <= currentPrice)
.LastOrDefault();When to use tree-based structures: Maintaining sorted data with inserts/deletes, order books, leaderboards, range queries.
7. Graphs — Networks and Arbitrage Detection
Graphs model relationships between nodes. In trading, graphs detect arbitrage paths across currency pairs.
// Arbitrage: currency pairs as a graph
Dictionary<string, List<(string To, decimal Rate)>> forexGraph = new();
// Build graph from exchange pairs
forexGraph["BTC"] = new() { ("USDT", 50000m), ("ETH", 15.5m) };
forexGraph["USDT"] = new() { ("EUR", 0.92m) };
forexGraph["ETH"] = new() { ("BTC", 0.065m) };
// BFS to find conversion path
HashSet<string> visited = new();
Queue<(string Currency, decimal Rate)> queue = new();
queue.Enqueue(("BTC", 1m));
while (queue.TryDequeue(out var current))
{
if (current.Currency == target)
{
Console.WriteLine($"Rate: {current.Rate}");
break;
}
foreach (var edge in forexGraph.GetValueOrDefault(current.Currency, new()))
{
if (visited.Add(edge.To))
queue.Enqueue((edge.To, current.Rate * edge.Rate));
}
}When to use graphs: Arbitrage detection, dependency resolution, network routing, order flow analysis.
8. Searching Algorithms in C#
C# provides built-in search methods, but understanding the underlying algorithms helps you choose correctly.
Binary Search (Sorted Data)
Use Array.BinarySearch() or List<T>.BinarySearch() when the collection is already sorted:
decimal[] priceLevels = { 49000m, 49500m, 50000m, 50500m, 51000m };
int index = Array.BinarySearch(priceLevels, 50000m);
// Returns 2 (index of 50000)
// If not found, returns bitwise complement of insertion point
int insertAt = ~Array.BinarySearch(priceLevels, 50200m);
// insertAt = 3 (between 50000 and 50500)Linear Search (Unsorted Data)
Use linear search for small collections or when data isn't sorted:
// Find first order matching a condition
foreach (var order in openOrders.Values)
{
if (order.Symbol == "BTCUSDT" && order.Side == "BUY")
return order;
}Rule of thumb: If you're searching more than ~50 items and the data can be sorted, use binary search. For everything else, linear search with FirstOrDefault() or Any().
9. Sorting Algorithms in C#
C#'s built-in sorting is sufficient for 99% of use cases. Understand the defaults:
// Built-in QuickSort (List<T> / Array)
List<Trade> trades = GetTrades();
trades.Sort((a, b) => b.Timestamp.CompareTo(a.Timestamp)); // Sort by time descending
// LINQ OrderBy (stable sort)
var sorted = trades.OrderBy(t => t.Price).ThenBy(t => t.Timestamp);When .NET's built-in sort isn't enough: Custom comparers for complex keys, partial sorting with Top(N) patterns, or sorting external data that doesn't fit in memory.
Production Patterns: Data Structures in .NET Systems
Choosing the right data structure is one decision. Using it correctly in production is another:
- Thread safety: Trading systems process fills on background threads. Use
ConcurrentDictionary,ConcurrentQueue, andConcurrentBaginstead of their single-threaded counterparts. - Memory pooling: High-frequency order book updates generate allocation pressure. Use
ArrayPool<T>for temporary buffers. - Backpressure: Unbounded queues cause OOM during latency spikes. Use
Channel<T>with bounded capacity for WebSocket message streams.
These patterns are why structured logging, correlation IDs, and retry policies matter — they help you debug which data structure is causing the bottleneck when things go wrong in production.
Why Built-in Sorting Wins in Production
C# provides List<T>.Sort() (introsort, O(n log n)) and LINQ's OrderBy (stable quicksort). In production, these built-in implementations outperform hand-rolled bubble/insertion/selection sort in every realistic scenario. The only reason to implement custom sorting is for specialized cases like external sorting of data that doesn't fit in memory.
Code Example - Bubble Sort in C#:
void BubbleSort(int[] array)
{
int n = array.Length;
for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - i - 1; j++)
{ if (array\[j\] > array\[j + 1\])
{
int temp = array\[j\];
array\[j\] = array\[j + 1\];
array\[j + 1\] = temp;
}
}
}
}
Introduction to Divide and Conquer Algorithms
Divide and conquer is a powerful algorithmic paradigm that involves breaking a problem into smaller subproblems, solving them recursively, and then combining their solutions to solve the original problem. Classic examples include merge sort and quicksort, which divide the sorting process into smaller sublists, sort them, and then merge or combine the results.
Recursion: Understanding the Concept and Its Role in Algorithms
Recursion is a programming technique where a function calls itself to solve a smaller version of the same problem. It's often used in algorithms that have a self-replicating structure, like trees and graphs. Recursion can simplify complex problems, but it should be used judiciously to avoid infinite loops.
Code Example - Recursive Factorial Calculation in C#:
int Factorial(int n)
{
if (n == 0)
return 1;
else
return n \* Factorial(n - 1);
}
The complexity cheat sheet at the top of this guide gives you the O-notation for each data structure. In production .NET code, the most impactful complexity choice you'll make is between Dictionary (O(1) lookup), List (O(n) linear search), and SortedDictionary (O(log n) tree). The rest — bubble sort vs. quicksort, recursion depth, divide and conquer — is handled by the framework's built-in implementations and rarely needs manual optimization.
Conclusion
This C# data structures and algorithms tutorial covered the essential structures you'll use in production .NET applications: arrays for time-series data, dictionaries for O(1) lookups, queues for message buffering, stacks for execution tracing, linked lists for sequential insertions, trees for sorted data, and graphs for relationship modeling.
The key takeaway: the right data structure depends on your access patterns and production constraints. A ConcurrentDictionary in a multi-threaded trading engine needs different considerations than a List<T> in a single-threaded batch processor. Always consider thread safety, allocation pressure, and backpressure when choosing your data structures for real systems.
Related resources
- Retry Policy Kit — Production-grade Polly policies with exponential backoff, circuit breakers, and monitoring
- Correlation IDs Playbook — Trace requests across services and jobs in .NET
- HTTP Client Retry-After Playbook — Handle 429s and rate limiting in .NET
- C# 8 Default Interface Methods: Complete Guide — How default interface methods enable trait-like composition in .NET resilience libraries
Related posts

C# 8 Default Interface Methods & Static Fields: Which Version? Complete Guide with Examples
Which C# version introduced default interface methods and static fields? C# 8.0. Complete reference covering static methods, fields, access modifiers, sealed/virtual members, CLR support, and production use cases with code examples.

Performance triage in legacy .NET: find the top 3 bottlenecks fast
When the legacy system is slow and no one knows where to start, a structured triage finds the real bottlenecks in hours, not weeks. This playbook gives you a repeatable method to identify, rank, and fix the top 3 performance killers.

Outbox pattern: reliable writes + events without the enterprise baggage
When a database write succeeds but the event never arrives, your system is lying to downstream consumers. The outbox pattern fixes this without a distributed transaction or a message broker rewrite.
Next step
Minimal APIs, OpenTelemetry, idempotency, and legacy .NET rescue patterns.