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.
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.
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.
| Type | Size | Range | DSA Use Case |
|---|---|---|---|
int | 4 bytes | -2.1B to 2.1B | Array indices, counters, most values |
long | 8 bytes | -9.2 x 10^18 to 9.2 x 10^18 | Prefix sums, large products, overflow-prone math |
char | 2 bytes | 0 to 65,535 | String characters, frequency arrays |
bool | 1 byte | true / false | Visited arrays, flags |
double | 8 bytes | ±1.7 x 10^308 | Rarely 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:
n * (n - 1) where n is near 10^5)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:
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:
| Operator | Symbol | DSA 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:
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:
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 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 Type | Default Value | DSA Significance |
|---|---|---|
int[] | 0 | Distance arrays, DP tables start at 0 |
long[] | 0L | Same, for large value computations |
bool[] | false | Visited arrays start as unvisited |
char[] | '\0' | Null character |
string[], object[] | null | Must 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 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:
| Method | Returns | Example | DSA Use |
|---|---|---|---|
s[i] | char | s[0] | Access individual characters (indexer, not a method) |
Length | int | s.Length | Loop bounds (property, no parentheses) |
Substring(start, len) | string | s.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) | int | s.IndexOf("ab") | Find substrings (-1 if not found) |
Equals(other) | bool | s.Equals(t) | Content comparison (same as ==) |
CompareTo(other) | int | s.CompareTo(t) | Lexicographic comparison |
Trim() | string | s.Trim() | Remove leading/trailing whitespace |
string.IsNullOrEmpty(s) | bool | Check for null or empty | |
StartsWith(prefix) | bool | s.StartsWith("ab") | Prefix check |
Contains(seq) | bool | s.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).
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.
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 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.
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:
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:
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.
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> 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 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.
| Collection | C# Type | Key Methods | Time Complexity | DSA Use Case |
|---|---|---|---|---|
| Dynamic Array | List<T> | Add, [], RemoveAt, Count | O(1) get, O(1) add | Result lists, dynamic arrays |
| Hash Map | Dictionary<K,V> | [], TryGetValue, ContainsKey | O(1) average | Frequency counting, lookups |
| Hash Set | HashSet<T> | Add, Contains, Remove | O(1) average | Visited tracking, duplicates |
| Sorted Map | SortedDictionary<K,V> | [], Keys, ContainsKey | O(log n) | Sorted access, range queries |
| Sorted Set | SortedSet<T> | Add, Min, Max, GetViewBetween | O(log n) | Sorted unique elements |
| Queue | Queue<T> | Enqueue, Dequeue, Peek | O(1) | BFS queues |
| Stack | Stack<T> | Push, Pop, Peek | O(1) | DFS, monotonic stack |
| Linked List | LinkedList<T> | AddFirst, AddLast, RemoveFirst | O(1) ends | Deque, sliding window |
| Heap | PriorityQueue<E,P> | Enqueue, Dequeue, Peek | O(log n) enq/deq | Top-K, Dijkstra |
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 (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:
GroupBy for grouping problems (anagrams, categorization)OrderBy when you need a sorted copy without modifying the originalDistinct for removing duplicatesToHashSet() for converting a list to a setWhen to avoid LINQ in DSA:
for loop with break is more efficient than Where().First())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 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).
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 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:
| Scenario | Typical Depth | Risk |
|---|---|---|
| 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.
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:
| Approach | Pros | Cons | Use When |
|---|---|---|---|
List<List<int>> | Fast index access, no hashing overhead | Wastes space if node IDs are sparse | Nodes are 0 to n-1 |
Dictionary<int, List<int>> | Handles any node IDs, no wasted space | Slightly slower due to hashing | Node IDs are large, sparse, or non-numeric |
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:
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.
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:
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).