AlgoMaster Logo

C# Crash Course for DSA

Last Updated: March 31, 2026

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

Using Directives You Will Use Everywhere

Before we get into the language itself, let us address something practical. 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 is the workhorse. It contains every collection you will need for DSA. System.Linq adds powerful 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. Do not waste time worrying about imports.

Variables, Types, and the Overflow Trap

C# is a statically typed language, which means every variable must have a declared type. For DSA, you will use a small set of value types over and over.

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 just like Java and C++. 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? Four common scenarios:

  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

Notice the placement of the cast. (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 that you will use often 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, just easier to read and verify.

Bitwise operators unlock an entire category of problems:

The key bitwise operators you will encounter:

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 cleanest 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, you will frequently extract logic into helper methods to keep your code clean. On LeetCode, your solution lives inside a class with instance or static methods.

C# follows a convention of PascalCase for method names (unlike Java's camelCase). In interviews, consistency matters more than convention, so pick one style and stick with it.

A critical concept to understand: 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 that Java lacks: 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 enormously 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:

Notice the new List<int>(path) when saving results. 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 one of the most common bugs 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 most fundamental 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:

Notice that unlike Java, 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, which is a key difference from Java:

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 one of the most common patterns you will write 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 essential 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 most candidates write their own binary search in interviews.

Array.Sort() uses IntroSort for primitive types (a hybrid of Quicksort, Heapsort, and Insertion Sort) which guarantees O(n log n) worst case. This is better than Java's dual-pivot Quicksort which has O(n^2) worst case for primitives.

Strings

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

Here is something that trips up developers coming from Java: C# string comparison with `==` compares content, not references. This is different from Java where == compares references and you must use .equals():

This is a genuine advantage of C# over Java. You never need to worry about the == vs .equals() trap. Both work for content comparison.

Here are the string methods you will use most often:

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. This is different from Java's substring(startIndex, endIndex). Getting this wrong is a very common bug when switching between the two languages:

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

StringBuilder is your tool for building strings efficiently:

Note that StringBuilder in C# supports direct indexing with sb[i], which is slightly cleaner than Java's sb.charAt(i) and sb.setCharAt(i, c).

Character utilities come up in problems involving letter manipulation:

The c - 'a' pattern deserves special attention. Since characters are internally represented 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

This is the most important section of this chapter. The System.Collections.Generic namespace provides the data structures you will reach for in virtually 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:

Unlike Java's ArrayList, there is no ambiguity between Remove(int) and RemoveAt(int). RemoveAt removes by index, Remove removes by value. The naming is clear and you will not mix them up.

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 is arguably the single most used collection in DSA. It provides O(1) average-case lookups, inserts, and deletes. It is C#'s equivalent of Java's HashMap.

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 cleaner than kvp.Key and kvp.Value. Use it when you need both.

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

The fact that Add() returns a boolean is useful. It tells you whether the element was actually 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. These are C#'s equivalents of Java's TreeMap and TreeSet.

However, SortedDictionary has a significant limitation compared to Java's TreeMap: it lacks FloorKey, CeilingKey, LowerKey, and HigherKey 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 instead of Java's offer and poll. 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

Unlike Java where you should avoid the Stack class (it extends the legacy Vector), C#'s Stack<T> is perfectly fine to use:

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 works differently from Java's PriorityQueue. The key difference: C#'s version separates the element from its priority, while Java stores a single element and uses a comparator.

Making a max-heap is trickier in C# than in Java. 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 is cleaner when you want readable code.

C#'s PriorityQueue does not support updating priorities or removing arbitrary elements efficiently. PriorityQueue has an EnqueueDequeue method that is useful for maintaining a fixed-size heap (like top-K problems), but for problems requiring priority updates (like a true Dijkstra), you typically re-enqueue with the new priority and skip stale entries when dequeuing.

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 (similar to Java's Comparator). 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:

C# does not have a Collections.reverseOrder() equivalent like Java. The pattern 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 powerful 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 cleaner, 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 elegant null-handling operators that other languages envy:

Rule 4: Combine null and empty checks.

These checks at the top of your solution handle edge cases cleanly. Many interviewers appreciate seeing these guard clauses. 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 the most idiomatic C# approach 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 is the backbone of tree traversal, graph DFS, backtracking, and divide-and-conquer. Understanding how C# handles recursion is essential.

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 when nodes are numbered 0 to n-1.

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

A cleaner 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

This is one of C#'s strongest advantages over Java for DSA. 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):

This is a huge improvement over Java, where you would need to encode the pair as a string ("row,col"), use a custom class with hashCode() and equals(), or use nested maps.

Tuple deconstruction:

That last pattern, swapping via tuple deconstruction, is particularly elegant. It 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

Notice how C# tuples shine here. Because tuples have value equality, you can put them directly in a HashSet for deduplication. In Java, you would need to convert each triplet to a sorted list and rely on list equality, which is slower and more verbose.

Common DSA Idioms in C#

These are the small patterns and utility calls you will reach for repeatedly.

Swapping elements:

The tuple swap is a C# exclusive that makes your code cleaner 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:

Notice that converting between int[] and List<int> is much cleaner in C# than in Java. 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. You will encounter these definitions throughout the course:

Notice the public keyword on fields. 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. This is cleaner than Java where you need separate constructors for each combination.

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. You will see it most often in constructors to distinguish the parameter from the field (this.val = val).