AlgoMaster Logo

C# Crash Course for DSA

57 min readUpdated May 29, 2026
Listen to this chapter
Unlock Audio

This chapter is a focused crash course on C# features that come up repeatedly in DSA and coding interviews. Instead of covering the entire language, we will concentrate only on the parts that matter for solving problems efficiently in interviews.

Common Using Directives

On LeetCode, most namespaces are imported automatically. But when writing C# locally or on some interview platforms, you need to know what to include. Here are the using directives that cover 95% of DSA problems:

The System.Collections.Generic namespace contains heavily used types. It contains every collection commonly used in DSA. System.Linq adds query methods that can make your code more concise, though you should use them judiciously in performance-sensitive code. We will cover LINQ in its own section later.

In a real interview, write these at the top and move on.

Variables, Types, and the Overflow Trap

C# is a statically typed language, which means every variable must have a declared type. For DSA, a small set of value types covers most problems.

TypeSizeRangeDSA Use Case
int4 bytes-2.1B to 2.1BArray indices, counters, most values
long8 bytes-9.2 x 10^18 to 9.2 x 10^18Prefix sums, large products, overflow-prone math
char2 bytes0 to 65,535String characters, frequency arrays
bool1 bytetrue / falseVisited arrays, flags
double8 bytes±1.7 x 10^308Rarely used, some geometry problems

C# also has the var keyword for implicit typing. The compiler infers the type from the right-hand side:

Use var when the type is obvious from the assignment. It reduces verbosity without sacrificing readability, especially with long generic types like Dictionary<string, List<int>>.

The most dangerous pitfall with types is integer overflow. Consider the classic binary search:

This is not a theoretical concern. When left and right are both above 1 billion, their sum exceeds int range and wraps to a negative number. The safe version avoids this by computing the difference first.

C# has a unique feature here: the checked keyword. In a checked block, arithmetic overflow throws an OverflowException instead of silently wrapping:

By default, C# arithmetic is unchecked, meaning it wraps silently on overflow. The checked keyword is useful for debugging but too slow for production DSA code. Know it exists, but rely on safe formulas instead.

When should you use long instead of int? Use long in these cases:

  1. Prefix sums of arrays with large values (sum of 10^5 elements each up to 10^9 gives 10^14)
  2. Multiplication of two large ints (e.g., n * (n - 1) where n is near 10^5)
  3. Factorial or combination calculations
  4. Modulo problems where intermediate sums can exceed int range before taking mod

The placement of the cast matters. (long)n * (n - 1) first promotes n to long, then performs long multiplication. But (long)(n * (n - 1)) performs int multiplication first (which overflows), then casts the already-corrupted result to long.

The ternary operator is a compact alternative to if-else for inline decisions:

Operators and Control Flow

Most operators in C# work as you would expect, but a few deserve special attention for DSA.

Modular arithmetic with % appears in hashing, cyclic array problems, and math-heavy questions. Remember that C#'s % can return negative values for negative operands: -7 % 3 gives -1, not 2. To get a positive result, use ((n % m) + m) % m.

Many problems ask you to return the result "modulo 10^9 + 7" to prevent overflow in the output. This is a standard pattern:

Note the underscore in 1_000_000_007. C# allows underscores in numeric literals for readability. This is the same as writing 1000000007, easier to read and verify.

Bitwise operators unlock an entire category of problems:

Key bitwise operators:

OperatorSymbolDSA Use
AND&Masking, checking if bit is set: (n & (1 << i)) != 0
OR|Setting bits: n | (1 << i)
XOR^Finding unique elements, toggling bits
NOT~Bit inversion
Left shift<<Multiply by powers of 2: 1 << n equals 2^n
Right shift>>Divide by powers of 2 (arithmetic shift, preserves sign)
Unsigned right shift>>>Divide by powers of 2 (fills with 0, C# 11+)

A common bit manipulation pattern is checking and setting individual bits, which shows up in problems using bitmasks to represent subsets:

For bit counting, BitOperations.PopCount from System.Numerics is the most concise approach. It maps to a single CPU instruction on modern hardware.

Control flow in C# offers three loop forms. Use each where it fits:

One limitation to remember: the foreach loop does not give you the index, and you cannot modify the underlying collection while iterating. If you need the index or need to modify elements, use the standard for loop.

Short-circuit evaluation with && and || is important for safety. C# evaluates left to right and stops as soon as the result is determined:

Methods and Parameter Passing

In DSA problems, extracting logic into helper methods keeps the code clean. On LeetCode, your solution lives inside a class with instance or static methods.

C# follows a convention of PascalCase for method names. Pick one style and stick with it.

C# passes value types (like int, bool, char) by value and reference types (like arrays, lists, classes) by reference value. This means:

  • Value type parameters are copies. Changing them inside a method does not affect the caller.
  • Reference type parameters pass a copy of the reference. You can modify the contents (like arr[i] = 5), but reassigning the reference itself (arr = new int[10]) does not affect the caller.

This is why the Swap method above works: it modifies the array contents through the reference. But you cannot write a method that swaps two int variables without special keywords.

C# gives you two tools for this: ref and out parameters.

The out pattern is used extensively in C#'s standard library, most notably in Dictionary.TryGetValue() which we will see in the collections section.

For DSA specifically, the pass-by-reference behavior of lists matters in recursive and backtracking problems. When you pass a List to a recursive call and add elements to it, those additions are visible to the caller. That is why the backtracking pattern works, adding and removing from a shared list as you explore different branches:

The new List<int>(path) when saving results creates a copy. If you wrote results.Add(path) instead, every entry in results would be a reference to the same list, and they would all end up empty after backtracking unwinds. This is a common bug in backtracking solutions.

Variable scope works the same as in most C-family languages. Variables declared inside a loop body exist only within that iteration:

If you need a value to persist across iterations (like a running sum or a previous element), declare it before the loop.

Arrays

Arrays are the foundational data structure in C# and the starting point for almost every DSA problem.

Default values matter and they differ by type:

Array TypeDefault ValueDSA Significance
int[]0Distance arrays, DP tables start at 0
long[]0LSame, for large value computations
bool[]falseVisited arrays start as unvisited
char[]'\0'Null character
string[], object[]nullMust initialize before use

The .Length property (no parentheses) gives the array size. This is different from List<T>.Count (also no parentheses) and string.Length. Getting these mixed up is a common mistake:

All three are properties (no parentheses). But the naming differs: arrays and strings use Length, while collections use Count.

2D arrays in C# come in two flavors:

For DSA problems, jagged arrays (int[][]) are almost always the right choice. LeetCode uses them for all matrix inputs, Array.Sort() works on them directly, and the syntax matches what you see in problem descriptions. Rectangular arrays (int[,]) are mostly useful when you need a single contiguous memory block for performance.

For DP problems, you often need a table with one extra row and column for the base case:

The 4-directional neighbor pattern is common in matrix problems:

For 8-directional movement (including diagonals), add the four diagonal pairs: {-1,-1}, {-1,1}, {1,-1}, {1,1}.

The Array class provides static operations:

Array.Sort() with a range lets you sort a portion of an array:

Array.BinarySearch() performs binary search on a sorted array:

The return value for a missing element is the bitwise complement (~) of the index where it would be inserted. So ~missing gives you the insertion point. This is useful but confusing, so writing your own binary search is often clearer.

Array.Sort() uses IntroSort for primitive types (a hybrid of Quicksort, Heapsort, and Insertion Sort) which guarantees O(n log n) worst case.

Strings

Strings in C# are immutable. Every time you modify a string, C# creates a new object. This has a performance implication for DSA:

C# string comparison with == compares content, not references:

Both == and .Equals() work for content comparison.

Common string methods:

MethodReturnsExampleDSA Use
s[i]chars[0]Access individual characters (indexer, not a method)
Lengthints.LengthLoop bounds (property, no parentheses)
Substring(start, len)strings.Substring(0, 3)Extract portions (start index, length, not end index!)
ToCharArray()char[]s.ToCharArray()When you need in-place modification
Split(sep)string[]s.Split(' ')Tokenize strings
IndexOf(str)ints.IndexOf("ab")Find substrings (-1 if not found)
Equals(other)bools.Equals(t)Content comparison (same as ==)
CompareTo(other)ints.CompareTo(t)Lexicographic comparison
Trim()strings.Trim()Remove leading/trailing whitespace
string.IsNullOrEmpty(s)boolCheck for null or empty
StartsWith(prefix)bools.StartsWith("ab")Prefix check
Contains(seq)bools.Contains("ab")Substring check

Watch out: C#'s Substring(startIndex, length) takes a length, not an end index. Getting this wrong is a common bug:

C# also supports range syntax with .. (C# 8+) for slicing:

StringBuilder is your tool for building strings efficiently:

StringBuilder supports direct indexing with sb[i] for both reading and writing characters.

Character utilities come up in problems involving letter manipulation:

The c - 'a' pattern works because characters are stored as numbers. Subtracting 'a' from a lowercase letter gives its zero-based position. This is how you build frequency arrays without a Dictionary:

This is faster and more memory-efficient than Dictionary<char, int> when you know the character set is limited to lowercase (or uppercase, or digits).

Collections Framework

The System.Collections.Generic namespace provides the data structures used in nearly every DSA problem. Choosing the right collection is often the difference between an O(n) and an O(n^2) solution.

List\<T\>: Dynamic Arrays

When you do not know the size upfront, or need to build a result list, List<T> is your go-to:

RemoveAt removes by index, Remove removes by value. The naming is clear: there is no ambiguity between the two.

A common pattern is building a list of lists for results like permutations or combinations:

Useful List methods:

Dictionary\<TKey, TValue\>: O(1) Lookups

Dictionary appears in most DSA problems. It provides O(1) average-case lookups, inserts, and deletes.

The TryGetValue pattern is a C# idiom you should know well. It attempts to get the value and returns false if the key does not exist, without throwing an exception. When the key is missing, the out variable gets the default value for the type (0 for int, null for reference types):

Iterating over a dictionary:

The deconstruction syntax var (key, value) is more concise than kvp.Key and kvp.Value. Use it when you need both.

HashSet\<T\>: O(1) Membership Testing

Add() returns a boolean telling you whether the element was newly added (i.e., was not already present). This lets you detect duplicates in a single operation:

HashSet<T> also supports set operations:

SortedDictionary and SortedSet: Sorted Collections

When you need keys in sorted order, SortedDictionary and SortedSet provide O(log n) operations backed by a red-black tree.

However, SortedDictionary lacks direct floor/ceiling/lower/higher key navigation methods. For these operations, use SortedSet<T> which has GetViewBetween(), or consider a different approach.

SortedSet<T> provides richer navigation:

For true floor/ceiling operations, you can use LINQ or write helper methods:

Queue\<T\>: BFS Foundation

Every BFS implementation starts with a queue:

C# uses Enqueue and Dequeue for queue operations. The methods throw InvalidOperationException if you try to dequeue from an empty queue, so always check queue.Count > 0 first. Use queue.Peek() to view the front element without removing it.

The queue.Count capture before the inner loop is a common pattern for level-order BFS, where you need to process all nodes at the current level before moving to the next.

Stack\<T\>: LIFO Operations

C#'s Stack<T> is the standard LIFO collection:

Always check stack.Count > 0 before calling Pop() or Peek() to avoid exceptions.

LinkedList\<T\>: Double-Ended Operations

LinkedList<T> in C# is a doubly-linked list that supports efficient operations at both ends, making it useful as a deque:

This is useful for sliding window maximum and monotonic deque problems where you need to add/remove from both ends.

PriorityQueue\<TElement, TPriority\>: Heaps

PriorityQueue was added in .NET 6 and separates the element from its priority. You enqueue both, and the queue dequeues the element with the smallest priority first.

Making a max-heap requires extra work because there is no built-in reverse comparator. You have three options:

For most DSA problems, Option 1 (negating the priority) is the simplest and fastest approach. Option 2 reads more clearly when you want explicit ordering.

C#'s PriorityQueue does not support decreasing an element's priority (there is no decrease-key operation) or removing arbitrary elements efficiently. It does have an EnqueueDequeue method that is useful for maintaining a fixed-size heap (like top-K problems). For problems that need priority updates (like Dijkstra), re-enqueue the node with its new priority and skip stale entries when dequeuing by checking whether the node was already finalized.

Collections Quick Reference

CollectionC# TypeKey MethodsTime ComplexityDSA Use Case
Dynamic ArrayList<T>Add, [], RemoveAt, CountO(1) get, O(1) addResult lists, dynamic arrays
Hash MapDictionary<K,V>[], TryGetValue, ContainsKeyO(1) averageFrequency counting, lookups
Hash SetHashSet<T>Add, Contains, RemoveO(1) averageVisited tracking, duplicates
Sorted MapSortedDictionary<K,V>[], Keys, ContainsKeyO(log n)Sorted access, range queries
Sorted SetSortedSet<T>Add, Min, Max, GetViewBetweenO(log n)Sorted unique elements
QueueQueue<T>Enqueue, Dequeue, PeekO(1)BFS queues
StackStack<T>Push, Pop, PeekO(1)DFS, monotonic stack
Linked ListLinkedList<T>AddFirst, AddLast, RemoveFirstO(1) endsDeque, sliding window
HeapPriorityQueue<E,P>Enqueue, Dequeue, PeekO(log n) enq/deqTop-K, Dijkstra

Sorting and Comparators

Sorting is a prerequisite for many algorithms: binary search, two pointers on sorted arrays, merge intervals, and greedy approaches.

For custom sorting, C# uses Comparison<T> delegates. The delegate receives two elements and returns a negative number if the first should come before the second, zero if they are equal, and a positive number if the first should come after:

For descending order:

The pattern for descending order is either sort-then-reverse for arrays, or flip the comparison in the lambda.

LINQ Essentials for DSA

LINQ (Language Integrated Query) is a C#-specific feature that adds query operations to collections. It can make certain DSA patterns significantly more concise, but you need to know when it helps and when it hurts.

Useful LINQ methods for DSA:

When to use LINQ in DSA:

  • Quick one-liners for min/max/sum when you do not need the loop for anything else
  • GroupBy for grouping problems (anagrams, categorization)
  • OrderBy when you need a sorted copy without modifying the original
  • Distinct for removing duplicates
  • ToHashSet() for converting a list to a set

When to avoid LINQ in DSA:

  • Inside nested loops or hot paths (LINQ has overhead from delegate invocations and iterator objects)
  • When you need early termination (a for loop with break is more efficient than Where().First())
  • When the manual approach is just as readable

For most interview problems, the performance difference is negligible. Use LINQ when it makes your code shorter, but be prepared to explain the trade-off if asked.

Null Handling

null is a source of many runtime errors in C#. In DSA, you encounter it primarily in three situations: tree/linked list problems, dictionary lookups, and uninitialized reference type arrays.

Rule 1: Always check for null before accessing fields or calling methods on an object.

Rule 2: Use TryGetValue instead of the indexer for dictionaries.

Rule 3: Use C#'s null-safety operators.

C# provides null-handling operators that simplify common patterns:

Rule 4: Combine null and empty checks.

These checks at the top of your solution handle edge cases cleanly. They short-circuit on empty or null input so the rest of the method can assume valid data. string.IsNullOrEmpty() is a C# convenience that checks both conditions in one call.

Nullable value types use ? syntax:

This is useful when you need to distinguish between "no value" and "zero" (e.g., a function that returns the index of a found element, or null if not found).

Iterating and Modifying Collections Safely

One of C#'s most common runtime errors is InvalidOperationException with the message "Collection was modified; enumeration operation may not execute." This is thrown when you modify a collection while iterating over it with a foreach loop:

There are three safe alternatives:

Option 3 is idiomatic for List<T>. It is a single line, efficient (O(n)), and reads clearly.

For HashSet and Dictionary, the same rules apply. If you need to remove entries while iterating, collect the keys first, then remove:

In practice though, most DSA problems do not require removing during iteration. You are far more likely to build a new collection with the desired elements.

Recursion and the Call Stack

Recursion underlies tree traversal, graph DFS, backtracking, and divide-and-conquer. It helps to know how C# handles it.

Every method call in C# goes on the call stack, which has limited space (typically 1 MB by default on Windows, which translates to a few thousand frames depending on frame size). If your recursion goes too deep, you get a StackOverflowException:

When to worry about stack depth:

ScenarioTypical DepthRisk
Balanced binary tree (n nodes)O(log n)Safe for n up to 10^6
Linked list / skewed tree (n nodes)O(n)Dangerous if n > 5,000-10,000
Backtracking (k choices, depth d)O(d)Usually safe (d is small)
DFS on graph (n nodes)O(n)Dangerous for large n

The fix: Convert deep recursion to an iterative approach using an explicit stack:

Note that C# does not optimize tail recursion in the standard runtime (though the JIT compiler can sometimes do it in specific cases). If your recursion depth is proportional to input size and the input can be large, convert to iteration.

Graph Representation Patterns

Graphs appear in a large portion of DSA problems. C# does not have a built-in graph class, so you need to build representations yourself. There are two common approaches:

Approach 1: Adjacency list with List<List<int>>. Use this when nodes are numbered 0 to n-1.

Approach 2: Adjacency list with Dictionary<int, List<int>>. Use this when node IDs are not contiguous or are very large.

A shorter version using TryGetValue for building:

Weighted graphs use tuples or int[] to store neighbor and weight:

ApproachProsConsUse When
List<List<int>>Fast index access, no hashing overheadWastes space if node IDs are sparseNodes are 0 to n-1
Dictionary<int, List<int>>Handles any node IDs, no wasted spaceSlightly slower due to hashingNode IDs are large, sparse, or non-numeric

Tuples and ValueTuple

C# has built-in tuple support with named fields, value equality, and the ability to use them as dictionary keys, all out of the box.

Basic tuple syntax:

Tuples as dictionary keys (a common DSA pattern):

Tuples give you value-based hashing and equality out of the box, so they work directly as dictionary keys without writing a custom equality implementation.

Tuple deconstruction:

That last pattern, swapping via tuple deconstruction, replaces the three-line temp variable swap with a single line.

Tuple comparison uses value equality (compares each field):

Using tuples in priority queues:

Deduplication Patterns

Many DSA problems require avoiding duplicate results (e.g., 3Sum, 4Sum, permutations with duplicates). C# offers two approaches:

Approach 1: Sort and skip duplicates (preferred for sorted array problems)

This is the standard approach for problems like 3Sum and 4Sum. It runs in O(1) extra space (beyond the sort) and produces results in sorted order.

Approach 2: Use a HashSet to collect unique results

C# tuples work well here. Because tuples have value equality, you can put them directly in a HashSet for deduplication.

Common DSA Idioms in C#

This section collects the small patterns and utility calls that come up repeatedly across problems.

Swapping elements:

The tuple swap is a C# exclusive that makes your code shorter and less error-prone.

Sentinel values for tracking min/max:

Be careful with int.MinValue. Negating it (-int.MinValue) overflows because the positive range is one less than the negative range. If you need to negate values that might be int.MinValue, use long.

Math utilities:

Ceiling division without floating point:

Type conversions:

Converting between int[] and List<int> is straightforward in C#. The List<int> constructor directly accepts arrays, and .ToList() / .ToArray() handle the reverse. No manual loops needed.

Deep copy vs shallow copy:

When you add a list to another list, you are adding a reference. If the original list changes later, the "copy" changes too:

For arrays, use Clone() or Array.Copy():

For 2D jagged arrays, Clone() only copies the outer array. The inner arrays are still shared references. You need a manual deep copy:

OOP Basics for DSA

DSA problems often define custom node classes. These definitions appear throughout the course:

The public keyword on fields makes them accessible from anywhere. In production C#, you would use properties with getters and setters, but for DSA, public fields are standard practice. You are not building production software here. You are solving problems under time pressure, and the overhead of encapsulation adds no value.

C# supports default parameter values in constructors (like val = 0, next = null), which reduces the need for constructor overloading. One constructor can handle multiple call patterns.

LeetCode often provides these node definitions. The default parameter constructor is handy for building data structures concisely:

The this keyword refers to the current instance. It appears most often in constructors to distinguish the parameter from the field (this.val = val).