Mastering SortedSet in C#
In real-world applications, you need a collection of unique items that are kept in sorted order and you also want efficient operations like add, remove, lookup and set operations (union, intersection, difference). SortedSet (in System.Collections.Generic) is the data structure built exactly for that need.
SortedSet provides an efficient way to manage data when you need fast operations like add, remove, search and even powerful set operations such as union, intersection and difference.
Unlike a List<T>, it doesn’t allow duplicates and unlike a HashSet<T>, it maintains elements in sorted order. This combination of uniqueness, sorting and performance makes SortedSet<T> a perfect fit for scenarios like maintaining leaderboards, ranking systems, or ordered collections without extra sorting steps.
In short : SortedSet<T> gives you the speed of a set and the organization of a sorted list, all in one efficient data structure.
What is SortedSet<T> in C#?
SortedSet<T> is a generic collection class in C# that stores elements in sorted order and ensures uniqueness , no duplicate elements are allowed. It’s part of the System.Collections.Generic namespace.
Key Features:
- Unique Elements: SortedSet<T> automatically ignores duplicate elements. Each item in the set is unique.
- Sorted Order: Elements are always sorted (ascending by default or custom via an IComparer<T>).
- Implements ISet<T>: Supports standard set operations like Union, Intersection and Difference.
- Efficient Performance : Operations such as Add, Remove and Contains typically run in O(log n) time.
- Balanced Tree Structure : Internally implemented using a self-balancing binary search tree (like a red-black tree) for efficient lookups and ordering.
- Best Use Case: Use SortedSet<T> when you need to store unique elements while keeping them sorted automatically.
- Supports Custom Objects: You can store user-defined types, provided you implement proper comparison logic (e.g., by implementing IComparable<T> or providing a custom comparer).
- Not Thread-Safe: SortedSet is not synchronized for multi-threaded access. Use locking or a thread-safe wrapper when working in concurrent environments.
In simple terms : SortedSet<T> gives you the speed of a set and the order of a sorted list, making it perfect for use cases where you need fast lookups, no duplicates and consistent ordering such as maintaining ranked data, leaderboards or ordered unique collections.
public class SortedSet<T> :
System.Collections.Generic.ICollection<T>, System.Collections.Generic.IEnumerable<T>, System.Collections.Generic.IReadOnlyCollection<T>, System.Collections.Generic.IReadOnlySet<T>, System.Collections.Generic.ISet<T>, System.Collections.ICollection, System.Runtime.Serialization.IDeserializationCallback, System.Runtime.Serialization.ISerializable
Internal Working & Performance of SortedSet<T>
Internally, SortedSet<T> uses a self-balancing binary search tree, typically a red-black tree, to store its elements. This structure ensures that operations like Add, Remove and Contains remain efficient , all running in O(log n) time.
Because the set must maintain both sorted order and uniqueness, insertions are generally slower than HashSet<T>, which offers average O(1) performance. However, this trade-off provides automatic sorting and predictable ordering.
Note : Duplicate insertions don’t throw exceptions the Add method simply returns false if the element already exists.
Step-by-Step Visualization Example of SortedSet Internal Storage
Here’s a detailed, step-by-step walkthrough showing how SortedSet<T> in C#, internally manages the insertion sequence (40, 30, 60, 50, 10, 70, 20) and then performs Remove(20).
| Step | Operation | Tree Structure (Simplified View) | Explanation |
|---|---|---|---|
| 1 | Start (Empty Set) | (empty) | The SortedSet starts empty. Internally, it maintains a root node reference (currently null). |
| 2 | Insert 40 | 40 (root, black) | The first element becomes the root of the red-black tree. Every root node is always black by definition. |
| 3 | Insert 30 | 40 (B) / 30 (R) |
30 is less than 40, so it becomes the left child. It’s colored red initially. Since the parent is black, no rebalancing is needed. |
| 4 | Insert 60 |
40 (B) / \ 30 (R) 60 (R) |
60 is greater than 40, so it becomes the right child. Both children are red, but that’s fine because their parent (root) is black. Tree remains valid. |
| 5 | Insert 50 |
40 (B) / \ 30 (R) 60 (R) / 50 (R) |
50 goes to the left of 60. Both parent (60) and uncle (30) are red, triggering a recolor: 60 → black, 30 → black, 40 → red. Then root (40) recolored back to black. Tree balanced. |
| 6 | Insert 10 |
40 (B) / \ 30 (B) 60 (B) / / 10 (R) 50 (R) |
10 is less than 30, so it becomes the left child of 30. Parent is black, so the tree stays balanced. |
| 7 | Insert 70 |
40 (B) / \ 30 (B) 60 (B) / / \ 10 (R) 50 (R) 70 (R) |
70 is greater than 60, so it becomes the right child. Parent 60 is black, so the red-black properties remain valid. |
| 8 | Insert 20 |
40 (B) / \ 30 (B) 60 (B) / / \ 10 (R) 50 (R) 70 (R) \ 20 (R) |
20 is added as the right child of 10. Now 10 (R) → 20 (R) causes a red-red conflict. Tree performs Left Rotation (10) and Right Rotation (30) followed by recoloring. |
| 9 | Balanced after rotations |
40 (B) / \ 20 (B) 60 (B) / \ / \ 10 (R) 30 (R) 50 (R) 70 (R) |
After the rotations and recoloring, the tree is balanced again. Each path has the same number of black nodes, and no consecutive red nodes exist. |
| 10 | Remove 20 |
40 (B) / \ 30 (B) 60 (B) / / \ 10 (R) 50 (R) 70 (R) |
To remove 20 (black), its in-order successor (30) replaces it. Since the successor (30) was red, deletion doesn’t break the tree’s black-height property. Final tree remains balanced. |
When You Should Use SortedSet<T>
The SortedSet<T> class in C# is ideal when you need a collection that keeps elements unique, automatically sorted and efficiently searchable. Internally, it uses a self-balancing red-black tree, ensuring operations like Add(), Remove() and Contains() run in O(log n) time.
1. When You Need Unique and Sorted Elements
If your use case requires both uniqueness and sorted order, SortedSet<T> is the perfect fit. Unlike List<T>, it prevents duplicates and unlike HashSet<T>, it automatically maintains ascending order.
Example use cases- Maintaining a sorted list of usernames or IDs without duplicates
- Ranking systems where order must always be maintained.
- Keeping sorted logs or timestamps for efficient range queries.
2. When You Perform Frequent Set Operations
SortedSet<T> implements the ISet<T> interface, offering efficient set-based operations like : UnionWith(), IntersectWith(), ExceptWith(), SymmetricExceptWith(). These operations are optimized for sorted data and are faster than manually handling lists or arrays.
3. When You Need Predictable and Ordered Iteration
Elements in a SortedSet<T> are always in sorted order, so iterating over them yields results in sequence. You can also use custom comparers to define your own sort logic such as sorting by name length, descending order or complex object properties.
4. When Consistency and Integrity Matter
Since duplicates are automatically ignored, SortedSet<T> helps ensure data integrity. It’s a great choice for scenarios where duplicates could cause logical errors such as maintaining a list of active sessions, connected users or registered product IDs.
When You Should Not Use SortedSet<T>
While SortedSet<T> is powerful, it’s not always the most efficient or convenient choice. There are situations where other collections perform better.
1. When Ordering Is not Needed
If you only need unique elements and don’t care about sorting, HashSet<T> is more efficient. It provides O(1) average-time insertions and lookups, compared to O(log n) in SortedSet<T>.
2. When You Need Index-Based Access
SortedSet<T> does not support random access or indexing (like mySet[3]). If you need to access elements by position or index frequently, use List<T>, Array or SortedList<TKey, TValue>.
3. When Dealing with Heavy Duplicate or Bulk Inserts
If your data involves massive insertions or many duplicates, SortedSet<T> may become slower. Each insertion involves tree balancing and duplicate checks, which can impact performance compared to HashSet<T> or List<T>.
Better approach : Insert all elements into a List<T> or HashSet<T> first and only later convert to a SortedSet<T> if sorting is needed.
4. When Multi-Threading Without Synchronization
SortedSet<T> is not thread-safe. If multiple threads modify it simultaneously, you must use locking or wrap it with ConcurrentBag<T>, ConcurrentDictionary<TKey,TValue> or custom synchronization.
In short : Use SortedSet<T> when you want unique, automatically sorted elements with efficient set operations. Avoid it when you need unordered data, random access or extremely high insert performance in those cases, collections like HashSet<T> or List<T> will serve you better.
SortedSet<T> Constructors
The SortedSet<T> class provides multiple constructors to give you flexibility in how you initialize and organize your data. Whether you’re creating an empty set, building one from a collection or customizing the sort order, there’s a constructor that fits.
1. Default Constructor
public SortedSet<T>();
Creates an empty SortedSet<T> that uses the default comparer (Comparer<T>.Default). By default, the elements are sorted in ascending order based on the natural ordering of the type T (e.g., numbers -> smallest to largest, strings -> alphabetical order).
ExampleSortedSet<int> numbers = new SortedSet<int>();
2. Constructor with Custom Comparer
public SortedSet<T>(IComparer<T>? comparer);
Creates an empty SortedSet<T> but lets you define your own sorting logic through an IComparer<T> implementation. This is helpful when you need custom sorting for example, descending order, case-insensitive sorting or domain-specific comparison.
ExampleSortedSet<int> descendingNumbers = new SortedSet<int>(Comparer<int>.Create((a, b) => b.CompareTo(a)));
descendingNumbers.Add(10);
descendingNumbers.Add(40);
descendingNumbers.Add(20);
foreach (int num in descendingNumbers)
Console.WriteLine(num);
// -------- Output ----------
40
20
103. Constructor with Collection
public SortedSet<T>(IEnumerable<T> collection);
Creates a SortedSet<T> and initializes it with elements from an existing collection (like a list or array). All duplicates are automatically removed and the elements are sorted as per the default comparer.
ExampleList<int> list = new List<int> { 50, 10, 30, 10, 40 };
SortedSet<int> sorted = new SortedSet<int>(list);
foreach (int num in sorted)
Console.WriteLine(num);
// ------------ Output --------------
10
30
40
504. Constructor with Collection and Comparer
public SortedSet<T>(IEnumerable<T> collection, IComparer<T>? comparer);
Combines both - initializes the SortedSet<T> from an existing collection and lets you specify a custom comparer for sorting. Perfect when you have existing data and want to enforce a custom ordering rule.
ExampleList<int> list = new List<int> { 40, 10, 30, 60, 20 };
SortedSet<int> descendingSet = new SortedSet<int>(list, Comparer<int>.Create((a, b) => b.CompareTo(a)));
foreach (int num in descendingSet)
Console.WriteLine(num);
// ------------ Output -------------
60
40
30
20
10SortedSet Properties
- Count :- public int Count { get; }
Gets the number of elements present in the sorted set.
- Comparer :- public System.Collections.Generic.IComparer<T> Comparer { get; }
Provide the IComparer<T> object which is used to order the values in the SortedSet.
- Max :- public T? Max { get; }
Get the maximum value form the Sorted set as defined by the comparer.
- Min :- public T? Min { get; }
Get the minimum value form the Sorted set as defined by the comparer.
Creating and adding element in a SortedSet
1. Using collection initializer
Creates a SortedSet<string> and populates it with initial values all at once. Ideal when you already know the elements upfront and want concise code.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var set = new SortedSet<string>() { "C#", "Java", "C++", "Python", "React" };
foreach (var lang in set) {
Console.WriteLine(lang);
}
}
}C# C++ Java Python React
2. Using Add() method
Starts with an empty SortedSet<string> and adds elements one by one using Add(). Useful when elements arrive dynamically or you need to check the add-result (duplicate detection).
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var set = new SortedSet<string>();
set.Add("Python");
set.Add("Java");
set.Add("C#");
set.Add("React");
set.Add("C++");
foreach (var lang in set) {
Console.WriteLine(lang);
}
}
}C# C++ Java Python React
