C# Data Structures and Algorithms Tutorial: Production Examples with Code

Aug 23, 20237 min read

Share|

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 StructureAccessSearchInsertDeleteTypical Use
ArrayO(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
SortedDictionaryO(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).

csharp
// 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&lt;T&gt;), key-based lookups (use Dictionary&lt;TKey, TValue&gt;).


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.

csharp
// 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&lt;TKey, TValue&gt; 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.

csharp
// 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&lt;T&gt; (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.

csharp
// 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.

csharp
// 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&lt;T&gt;).


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.

csharp
// 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.

csharp
// 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&lt;T&gt;.BinarySearch() when the collection is already sorted:

csharp
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:

csharp
// 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:

csharp
// 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, and ConcurrentBag instead of their single-threaded counterparts.
  • Memory pooling: High-frequency order book updates generate allocation pressure. Use ArrayPool&lt;T&gt; for temporary buffers.
  • Backpressure: Unbounded queues cause OOM during latency spikes. Use Channel&lt;T&gt; 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&lt;T&gt;.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)

ts
{
    int n = array.Length;
    for (int i = 0; i < n - 1; i++)
    {
        for (int j = 0; j < n - i - 1; j++)
        {
code
code
            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)

code
{
    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&lt;T&gt; in a single-threaded batch processor. Always consider thread safety, allocation pressure, and backpressure when choosing your data structures for real systems.


Related posts

Next step

Minimal APIs, OpenTelemetry, idempotency, and legacy .NET rescue patterns.

Explore .NET Production Reliability →