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.
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.
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.
| 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 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:
n * (n - 1) where n is near 10^5)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:
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:
| 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 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:
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:
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 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 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:
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 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:
| 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. 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).
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.
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 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.
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:
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:
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.
C#'s Stack<T> is the standard LIFO collection:
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 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.
| 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. 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 (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:
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 shorter, 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 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).
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 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:
| 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 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:
| 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 |
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:
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.
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:
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).