Data Structures in C#
C# provides a rich collection of built-in data structures through the
System.Collections.Generic namespace. These collections make
it easier to store, access, search, update, and organize data without
implementing every data structure from scratch.
Understanding these collections is important for writing efficient C# applications and for solving programming and data structure problems. The right collection depends on how the application needs to access and modify its data.
Common Data Structures Available in C#
| C# Collection | Data Structure Concept | Typical Use |
|---|---|---|
Array |
Array | Fixed-size indexed data |
List<T> |
Dynamic array | Resizable sequential collections |
LinkedList<T> |
Doubly linked list | Node-based sequences |
Stack<T> |
Stack | LIFO processing |
Queue<T> |
Queue | FIFO processing |
Dictionary<TKey, TValue> |
Hash table | Key-value lookup |
HashSet<T> |
Hash set | Unique values and membership checks |
Arrays in C#
A C# array stores a fixed number of elements of the same type. Elements can be accessed directly using their zero-based index.
int[] numbers = { 10, 20, 30, 40, 50 };
Console.WriteLine(numbers[2]);
// 30
numbers[2] = 35;
Console.WriteLine(numbers[2]);
// 35
Array access by index is typically O(1). However, inserting
or deleting elements requires shifting values or creating another array
because the array has a fixed length.
List<T> as a Dynamic Array
List<T> is one of the most frequently used generic
collections in C#. It provides a resizable array-like structure while
retaining fast indexed access.
using System;
using System.Collections.Generic;
var numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
numbers.Add(30);
Console.WriteLine(numbers[1]);
// 20
numbers.Remove(20);
foreach (int number in numbers)
{
Console.WriteLine(number);
}
A List<T> automatically grows when its internal
capacity is reached. This makes it convenient when the number of elements
is not known in advance.
List Capacity and Count
A useful detail when working with List<T> is the
difference between Count and Capacity.
Count represents the number of elements currently stored,
while Capacity represents the allocated storage available
internally.
var values = new List<int>();
values.Add(10);
values.Add(20);
values.Add(30);
Console.WriteLine(values.Count);
Console.WriteLine(values.Capacity);
Stack<T> in C#
Stack<T> implements a Last-In, First-Out
(LIFO) structure. The most recently added element is the first
element removed.
var stack = new Stack<string>();
stack.Push("Page 1");
stack.Push("Page 2");
stack.Push("Page 3");
Console.WriteLine(stack.Pop());
// Page 3
Console.WriteLine(stack.Peek());
// Page 2
Common stack operations include Push() to add an element,
Pop() to remove the top element, and Peek() to
inspect the top element without removing it.
Queue<T> in C#
Queue<T> implements a
First-In, First-Out (FIFO) structure. The element that
enters the queue first is normally the first element removed.
var queue = new Queue<string>();
queue.Enqueue("Customer A");
queue.Enqueue("Customer B");
queue.Enqueue("Customer C");
Console.WriteLine(queue.Dequeue());
// Customer A
Console.WriteLine(queue.Peek());
// Customer B
Enqueue() adds an element, Dequeue() removes the
front element, and Peek() reads the front element without
removing it.
Dictionary<TKey, TValue> in C#
Dictionary<TKey, TValue> stores data as key-value
pairs. It is backed by a hash table and is useful when values need to be
retrieved using unique keys.
var users = new Dictionary<int, string>
{
{ 101, "Asha" },
{ 102, "Rahul" },
{ 103, "Priya" }
};
Console.WriteLine(users[102]);
// Rahul
users[102] = "Rahul Kumar";
Console.WriteLine(users[102]);
// Rahul Kumar
Dictionary lookup, insertion, and removal are typically
O(1) on average, although the actual performance depends on
hashing and collision behavior.
Safe Dictionary Lookup
Accessing a missing key directly through the indexer throws a
KeyNotFoundException. When the key may not exist,
TryGetValue() provides a safer lookup pattern.
var scores = new Dictionary<string, int>
{
{ "Asha", 95 },
{ "Rahul", 88 }
};
if (scores.TryGetValue("Priya", out int score))
{
Console.WriteLine(score);
}
else
{
Console.WriteLine("User not found");
}
HashSet<T> in C#
HashSet<T> stores unique values and is optimized for
membership checks. Adding a duplicate value does not create another
element.
var tags = new HashSet<string>();
tags.Add("C#");
tags.Add(".NET");
tags.Add("Data Structures");
tags.Add("C#");
Console.WriteLine(tags.Count);
// 3
Console.WriteLine(tags.Contains(".NET"));
// True
A hash set is useful when uniqueness matters more than maintaining elements at numeric indexes.
LinkedList<T> in C#
LinkedList<T> provides a doubly linked list. Each node
contains a value and references that connect it to neighboring nodes.
var list = new LinkedList<string>();
list.AddLast("HTML");
list.AddLast("CSS");
list.AddLast("JavaScript");
var cssNode = list.Find("CSS");
if (cssNode != null)
{
list.AddAfter(cssNode, "TypeScript");
}
foreach (string item in list)
{
Console.WriteLine(item);
}
A linked list is useful when nodes need to be inserted or removed without shifting all subsequent elements. However, accessing an arbitrary position requires traversal and is therefore not constant time.
Choosing the Right C# Data Structure
The best collection depends on the operations your application performs most frequently.
| Requirement | Suitable C# Collection |
|---|---|
| Fixed-size indexed data | Array |
| Resizable list with index access | List<T> |
| LIFO processing | Stack<T> |
| FIFO processing | Queue<T> |
| Key-value lookup | Dictionary<TKey, TValue> |
| Unique values | HashSet<T> |
| Node-based sequence | LinkedList<T> |
C# Data Structure Complexity
Understanding the typical complexity of collection operations helps when selecting a data structure for an application.
| Collection | Access | Search / Lookup | Typical Add |
|---|---|---|---|
Array |
O(1) | O(n) | Fixed size |
List<T> |
O(1) | O(n) | O(1) amortized at end |
LinkedList<T> |
O(n) | O(n) | O(1) with a known node |
Stack<T> |
Top only | O(n) | O(1) amortized |
Queue<T> |
Front only | O(n) | O(1) amortized |
Dictionary<TKey, TValue> |
By key | O(1) average | O(1) average |
HashSet<T> |
Not index-based | O(1) average | O(1) average |
Generic Collections in C#
Generic collections such as List<T>,
Stack<T>, and Dictionary<TKey, TValue>
allow the compiler to enforce the types of values stored in the
collection.
var numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
// numbers.Add("30"); // Compile-time type error
Generic collections are generally preferred over older non-generic collection types because they provide stronger type safety and avoid many unnecessary conversions.
Common Mistakes When Choosing C# Collections
- Using an array when the collection needs to grow dynamically.
- Using a list when the primary requirement is key-based lookup.
- Using a dictionary when duplicate keys are expected.
- Using a hash set when the application needs indexed access.
- Choosing a linked list without considering the cost of traversal.
- Ignoring the difference between average and worst-case complexity.
- Using non-generic collections when a generic collection is appropriate.
Practical Example: Managing Product Data
Different data structures can work together in the same application.
For example, a product list can use List<T>, product
lookup can use a Dictionary<TKey, TValue>, and unique
categories can use a HashSet<T>.
record Product(int Id, string Name, string Category);
var products = new List<Product>
{
new Product(1, "Laptop", "Electronics"),
new Product(2, "Keyboard", "Electronics"),
new Product(3, "Notebook", "Stationery")
};
var productById = products.ToDictionary(product => product.Id);
var categories = products
.Select(product => product.Category)
.ToHashSet();
Console.WriteLine(productById[2].Name);
// Keyboard
Console.WriteLine(categories.Count);
// 2
This example demonstrates an important principle: an application does not have to use one data structure for everything. Different collections can be selected for different access patterns.
Key Takeaways
- C# provides several generic collections for common data structure requirements.
Arrayis useful for fixed-size indexed data.List<T>provides a resizable array-like collection.Stack<T>follows LIFO ordering.Queue<T>follows FIFO ordering.Dictionary<TKey, TValue>provides key-value storage and fast average lookup.HashSet<T>is designed for unique values and membership checks.LinkedList<T>provides a doubly linked node-based structure.- The correct collection depends on the operations your application performs most often.