Mastering SortedList in C#
When working with collections in C#, there are some situations where you need to keep your key/value pairs in order. Instead of sorting them manually every time, you can just use the SortedList<TKey, TValue> class. It automatically keeps everything sorted by the key and also gives you quick access to the values. I personally find it really handy for lookup tables, storing configuration data or anywhere you actually care about the order of the keys.
The SortedList<TKey, TValue> class implements multiple interfaces that make it a versatile and strongly typed collection, combining the features of both arrays and dictionaries.
public class SortedList<TKey,TValue> :
System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<TKey,TValue>>,
System.Collections.Generic.IDictionary<TKey,TValue>,
System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<TKey,TValue>>,
System.Collections.Generic.IReadOnlyCollection<System.Collections.Generic.KeyValuePair<TKey,TValue>>,
System.Collections.Generic.IReadOnlyDictionary<TKey,TValue>,
System.Collections.IDictionaryWhat is SortedList<TKey, TValue> in C#?
The SortedList<TKey, TValue> class in C# is a generic collection designed to store key/value pairs where the elements are automatically sorted by key. Unlike an ordinary dictionary, a SortedList keeps its data in sorted order at all times, giving you both fast lookups and ordered access to elements.
This makes it an excellent choice for cases where you need efficient retrieval while also maintaining a predictable, sorted sequence of keys, such as configuration data, lookup tables or caching scenarios where order matters.
Key Characteristics
- Unique Keys : Each key in the collection must be unique. If you try to insert a duplicate key, an exception will be thrown.
- Nullable Values : Values can be null when working with reference types, allowing flexibility in storing data.
- Strongly Typed Keys : All keys must be of the same data type, ensuring consistency and type safety throughout the list.
- Automatic Sorting : The elements are automatically arranged in ascending order of their keys by default. You can also provide a custom comparer for a different sorting logic.
- Array-Based Internal Structure : Internally, SortedList uses arrays for keys and values, which makes index-based access extremely fast compared to dictionary-style collections.
- Dual Access : You can access elements either by their key or by index position, making it behave like both a dictionary and a list.
- Efficient Lookups : Key lookups are optimized using binary search, giving an average time complexity of O(log n) for retrieval operations.
- Memory Efficient : Compared to SortedDictionary<TKey, TValue>, it uses less memory since it doesn’t rely on a tree-based structure.
- Maintains Order on Insertion : Even when new elements are added, the list automatically reorders itself to maintain the sorted order of keys.
- Supports Custom Comparers : You can define a custom sorting logic using the IComparer<TKey> interface during initialization.
- Indexed Operations : Since it supports indexing, you can retrieve keys and values by position using properties like Keys[i] and Values[i].
- Enumeration in Sorted Order : When you iterate through the list using a foreach loop, elements are always returned in sorted order by key.
- The capacity of SortedList<TKey,TValue> is a number of element that SortedList<TKey,TValue> can hold. Whenever we add a new element in the SortedList<TKey,TValue>the capacity is automatically increased as required by reallocating the internal array.
- Capacity of the SortedList<TKey,TValue> can be decreased by calling TrimExcess or by setting the Capacity property explicitly.whenever we decrease the capacity, This process will reallocates memory and copies all the elements in the SortedList<TKey,TValue>.
- Ideal for Small to Medium Data Sets : SortedList performs best when dealing with moderate amounts of data where insertions and deletions are infrequent but lookups are common.
Internal Working of SortedList<TKey, TValue>
A SortedList<TKey, TValue> internally maintains two parallel arrays
- keys[] : stores the keys in sorted order.
- values[] : stores the corresponding values aligned by index.
Whenever you add, remove or look up elements, operations are performed on these two arrays. The sorting is maintained at all times, so data is always in ascending order (or custom comparer order).
Whenever you add a new element, it uses binary search to find the correct position of the key to maintain sorted order.
Step-by-Step Visualization Example of SortedList<int, int> Internal Storage
| Step | Operation | Binary Search (Insert Position) | Keys (keys[]) | Values (values[]) | Internal Process (Explanation) |
|---|---|---|---|---|---|
| 1 | Start (Empty SortedList) | — | [] | [] | The SortedList starts empty , both keys[] and values[] arrays have 0 length. |
| 2 | Add(4, 10) | Insert at index 0 | [4] | [10] | First element added -> no sorting needed. Arrays are created and key 4 is stored at index 0. |
| 3 | Add(10, 20) | Insert at index 1 | [4, 10] | [10, 20] | Key 10 is larger than 4, so it's appended to the end. Internal binary search confirms sorted position. |
| 4 | Add(1, 100) | Insert at index 0 | [1, 4, 10] | [100, 10, 20] | Binary search finds 1 should come before 4. Elements are shifted right to make space at index 0. |
| 5 | Add(5, 201) | Insert at index 2 | [1, 4, 5, 10] | [100, 10, 201, 20] | Binary search locates position between 4 and 10. New key/value inserted at index 2, shifting items after it. |
| 6 | Add(6, 300) | Insert at index 3 | [1, 4, 5, 6, 10] | [100, 10, 201, 300, 20] | Binary search: 6 fits between 5 and 10. Existing elements from index 3 onward are moved one step right. |
| 7 | Add(2, 201) | Insert at index 1 | [1, 2, 4, 5, 6, 10] | [100, 201, 10, 201, 300, 20] | Key 2 belongs between 1 and 4. Internal array shifting occurs , all elements from index 1 onward move right. |
| 8 | Add(1, 203) | Key found (index 0) | [1, 2, 4, 5, 6, 10] | [100, 201, 10, 201, 300, 20] | SortedList finds an existing key (1) -> duplicate keys not allowed -> throws ArgumentException. No change occurs. |
| 9 | Remove(1) | Found at index 0 | [2, 4, 5, 6, 10] | [201, 10, 201, 300, 20] | Key 1 found and removed. Both arrays shift left from index 1 onward to fill the empty spot. Count decreases by 1. |
Time Complexity
| Operation | Average Time | Worst Case | Description |
|---|---|---|---|
Add |
O(n) | O(n) | Inserts key-value pairs in sorted order, may require shifting elements. |
Remove |
O(n) | O(n) | Removes element and shifts remaining elements to fill the gap. |
ContainsKey |
O(log n) | O(log n) | Binary search in the sorted keys array. |
ContainsValue |
O(n) | O(n) | Performs linear search in the values array. |
Item[TKey] (get) |
O(log n) | O(log n) | Retrieves value by performing binary search on keys. |
Item[TKey] (set) |
O(log n) if key exists, O(n) if insertion | O(n) | Updates existing key’s value or inserts a new key, possibly requiring array shifts. |
TrimExcess / Resize |
O(n) | O(n) | Reallocates and copies internal arrays to adjust capacity. |
SortedList<TKey, TValue> Constructors
- public SortedList ();
- public SortedList (System.Collections.Generic.IComparer<TKey>? comparer);
- public SortedList (System.Collections.Generic.IDictionary<TKey,TValue> dictionary);
- public SortedList (System.Collections.Generic.IDictionary<TKey,TValue> dictionary, System.Collections.Generic.IComparer<TKey>? comparer);
- public SortedList (int capacity);
- public SortedList (int capacity, System.Collections.Generic.IComparer<TKey>? comparer);
SortedList <TKey, TValue> Properties
- Capacity :- public int Capacity { get; set; }Gets the number of elements that SortedList<TKey,TValue> can store. getting the value of capacity is O(1) operation and setting the capacity property is O(n) operation.
- Comparer :- public System.Collections.Generic.IComparer<TKey> Comparer { get; }Gets the IComparer<TKey> that is used to sort/order element of the SortedList<TKey,TValue>.
- Count :- public int Count { get; }Gets the number of key/value pairs that is present in the SortedList<TKey, TValue>. It is an O(1) operation.
- Item[TKey] :- public TValue this[TKey key] { get; set; }Gets or sets the value associated with the specified key. Getting the value by this property is an o( log n) operation . Setting the value in the SortedList<TKey,TValue> O( log n) operation if key already exist in the SortedList<TKey,TValue>. if key not present in the SortedList<TKey,TValue> setting will be O( n ) operation for unsorted data OR if the new element added at the end of the SortedList<TKey,TValue> this will be an O( log n) operation. if adding a new element require array resizing then this will be an O(n) operation.
- Keys :- public System.Collections.Generic.IList<TKey> Keys { get; }Gets a IList<TKey> containing the keys in the SortedList <TKey, TValue> . it is also an O(1) operation.
- Values :- public System.Collections.Generic.IList<TValue> Values { get; }Gets a IList<TValue> containing the values in the SortedList<TKey, TValue>. it is an O(1) operation.
Creating and adding element in SortedList<TKey, TValue>
1. Initialize with Values with initial values (Collection Initializer)
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var sortedList = new SortedList<string,string> {
{"language","C#"},
{"topic","SortedList"},
{"label","Beginner"}
};
Console.WriteLine("We are learning "+sortedList["language"]+" "+sortedList["topic"] +" and level : "+sortedList["label"]);
Console.WriteLine("--------------loop------------");
foreach (KeyValuePair<string, string> kv in sortedList) {
Console.WriteLine("K: "+ kv.Key+" V: "+kv.Value);
}
}
}We are learning C# SortedList and level : Beginner --------------loop------------ K: label V: Beginner K: language V: C# K: topic V: SortedList
2. Direct Assign to add new element in SortedList<TKey,TValue>
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var sortedList = new SortedList<string,string>();
sortedList["language"]="C#";
sortedList["topic"]="SortedList";
sortedList["label"]="Beginner";
Console.WriteLine("We are learning "+sortedList["language"]+" "+sortedList["topic"] +" and level : "+sortedList["label"]);
Console.WriteLine("--------------loop------------");
foreach (KeyValuePair<string, string> kv in sortedList) {
Console.WriteLine("K: "+ kv.Key+" V: "+kv.Value);
}
}
}We are learning C# SortedList and level : Beginner --------------loop------------ K: label V: Beginner K: language V: C# K: topic V: SortedList
3. Using Add() Method to add new element in SortedList<TKey, TValue>
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var sortedList = new SortedList<string,string>();
sortedList.Add("language","C#");
sortedList.Add("topic","SortedList");
sortedList.Add("label","Beginner");
Console.WriteLine("We are learning "+sortedList["language"]+" "+sortedList["topic"] +" and level : "+sortedList["label"]);
Console.WriteLine("--------------loop------------");
foreach (KeyValuePair<string, string> kv in sortedList) {
Console.WriteLine("K: "+ kv.Key+" V: "+kv.Value);
}
}
}We are learning C# SortedList and level : Beginner --------------loop------------ K: label V: Beginner K: language V: C# K: topic V: SortedList
Actual Internal Implementation (Simplified View)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
namespace System.Collections.Generic
{
[DebuggerDisplay("Count = {Count}")]
[Serializable]
public class SortedList<TKey, TValue> :
IDictionary<TKey, TValue>,
ICollection,
IReadOnlyDictionary<TKey, TValue>
{
// Private Fields
private TKey[] _keys; // Stores all keys in sorted order
private TValue[] _values; // Stores values corresponding to each key
private int _size; // Number of elements in the list
private IComparer<TKey> _comparer;// Defines how keys are compared (sorting logic)
private KeyList? _keyList; // Lazy wrapper for key collection
private ValueList? _valueList; // Lazy wrapper for value collection
private int _version; // Tracks modifications for enumerator safety
private const int _defaultCapacity = 4; // Default capacity of arrays
// Constructors
public SortedList()
: this((IComparer<TKey>?)null)
{ }
public SortedList(IComparer<TKey>? comparer)
{
_comparer = comparer ?? Comparer<TKey>.Default;
_keys = Array.Empty<TKey>();
_values = Array.Empty<TValue>();
_size = 0;
}
public SortedList(int capacity)
: this(capacity, null)
{ }
public SortedList(int capacity, IComparer<TKey>? comparer)
{
if (capacity < 0)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
_comparer = comparer ?? Comparer<TKey>.Default;
_keys = new TKey[capacity];
_values = new TValue[capacity];
_size = 0;
}
public SortedList(IDictionary<TKey, TValue> dictionary)
: this(dictionary, null)
{ }
public SortedList(IDictionary<TKey, TValue> dictionary, IComparer<TKey>? comparer)
: this(dictionary?.Count ?? 0, comparer)
{
if (dictionary == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.dictionary);
foreach (KeyValuePair<TKey, TValue> pair in dictionary)
{
Add(pair.Key, pair.Value);
}
}
// Public Properties
public int Count => _size;
public IComparer<TKey> Comparer => _comparer;
public TKey[] Keys => _keys;
public TValue[] Values => _values;
public KeyList GetKeyList()
{
if (_keyList == null)
_keyList = new KeyList(this);
return _keyList;
}
public ValueList GetValueList()
{
if (_valueList == null)
_valueList = new ValueList(this);
return _valueList;
}
public TValue this[TKey key]
{
get
{
int i = IndexOfKey(key);
if (i >= 0)
return _values[i];
ThrowHelper.ThrowKeyNotFoundException(key);
return default!; // Unreachable
}
set
{
if (key == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
int i = Array.BinarySearch(_keys, 0, _size, key, _comparer);
if (i >= 0)
{
_values[i] = value;
_version++;
return;
}
Insert(~i, key, value);
}
}
// Explicit Interface Properties
ICollection<TKey> IDictionary<TKey, TValue>.Keys
{
get
{
if (_keyList == null)
_keyList = new KeyList(this);
return _keyList;
}
}
ICollection<TValue> IDictionary<TKey, TValue>.Values
{
get
{
if (_valueList == null)
_valueList = new ValueList(this);
return _valueList;
}
}
IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys
{
get
{
if (_keyList == null)
_keyList = new KeyList(this);
return _keyList;
}
}
IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values
{
get
{
if (_valueList == null)
_valueList = new ValueList(this);
return _valueList;
}
}
// Indexer (by position, internal use)
internal TKey GetKey(int index)
{
if (index < 0 || index >= _size)
ThrowHelper.ThrowArgumentOutOfRange_IndexException();
return _keys[index];
}
internal TValue GetValue(int index)
{
if (index < 0 || index >= _size)
ThrowHelper.ThrowArgumentOutOfRange_IndexException();
return _values[index];
}
}
}SortedList<TKey, TValue> vs. SortedDictionary<TKey, TValue>
| Feature | SortedList |
SortedDictionary |
|---|---|---|
| Internal Structure | Uses two parallel arrays , one for keys and one for values. | Uses a balanced binary search tree (Red-Black Tree) internally. |
| Sorting Behavior | Automatically keeps keys sorted in ascending order (or custom order with comparer). | Also keeps keys sorted, using the same comparer logic. |
| Key Uniqueness | Keys must be unique. Duplicate keys are not allowed. | Same , each key must be unique. |
| Lookup Performance | Average O(log n) using binary search. | Average O(log n) due to tree traversal. |
| Insertion Performance | Slower for large collections , inserting a new item may require shifting elements (O(n)). | Faster insertion , tree-based structure doesn’t need shifting (O(log n)). |
| Removal Performance | Removing elements may involve shifting data (O(n)). | More efficient deletions (O(log n)). |
| Index Access | Supports index-based access (Keys[i], Values[i]). | Does not support index-based access. |
| Memory Usage | More memory-efficient (compact array-based). | Consumes slightly more memory due to tree node overhead. |
| Best Use Case | When you need fast lookups and index-based access with relatively small data sets. | When dealing with frequent insertions/deletions and larger data sets. |
| Enumeration Order | Always in sorted order of keys. | Also enumerates in sorted order of keys. |
| Custom Comparer Support | Supported via IComparer |
Supported via IComparer |
| Performance Trade-Off | High performance for lookups, but slower for updates. | Balanced performance across insert, delete and lookup operations. |
Important Points
- SortedDictionary<TKey,TValue> and SortedList<TKey,TValue> both has o( log n) time complexity for getting/retrieval of data
- SortedList<TKey,TValue> uses less memory than SortedDictionary<TKey,TValue>.
- SortedList<TKey,TValue> is faster than SortedDictionary<TKey,TValue> if list is created all at once from sorted data.
- For unsorted data , SortedDictionary<TKey,TValue> performs faster insertion and removal operations in O(log n) compared to SortedList<TKey,TValue> which performs these operations in O(n).
