Skip to main content

Command Palette

Search for a command to run...

Mastering C# Part 5.1 Collections (Part-1)

Generic namespace

Updated
10 min read
Mastering C# Part 5.1 Collections (Part-1)

In software development, data management is a crucial aspect of building efficient and scalable applications. Collections in C# provide a way to group, store, and manipulate data in a structured manner. They are essential for handling dynamic datasets, offering powerful tools for developers to work with.

C# collections can be broadly categorized into generic collections (from the System.Collections.Generic namespace) and non-generic collections (from the System.Collections namespace). Generic collections offer type safety and better performance, while non-generic collections are more flexible but less type-safe.

In this article, we'll dive into both types of collections, exploring their features, advantages, and examples. Whether you're dealing with lists, dictionaries, queues, or stacks, you'll find a suitable collection type for your needs in C#. So, let's get started!

Here are the collections that uses generic namespace

  • HashSet<T>

  • LinkedList<T>

  • List<T>

  • SortedSet<T>

  • Dictionary<TKey, TValue>

  • SortedDictionary<TKey, TValue>

  • Stack<T>

  • Queue<T>

HashSet<T>

HashSet<T> is a generic collection in C# that ensures all elements are unique and is designed for fast lookups. It doesn’t maintain any order of elements and provides efficient operations like add, remove, and search.


Key Features

  1. No duplicates: Prevents adding the same element multiple times.

  2. Unordered: Elements are stored without a specific sequence.

  3. Fast operations: Adding, removing, and searching are very fast (average O(1) time complexity).


Examples

Here’s a short example to understand its usage:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Create a HashSet
        HashSet<string> fruits = new HashSet<string>();

        // Add elements
        fruits.Add("Apple");
        fruits.Add("Banana");
        fruits.Add("Apple"); // Duplicate, won't be added

        // Display elements
        Console.WriteLine("Fruits in HashSet:");
        foreach (var fruit in fruits)
        {
            Console.WriteLine(fruit); // Output: Apple, Banana (unordered)
        }
    }
}

Common operations

  1. Check for an item:

     Console.WriteLine(fruits.Contains("Banana")); // Output: True
    
  2. Remove an item:

     fruits.Remove("Apple");
    
  3. Perform set operations (like union and intersection):

     HashSet<string> moreFruits = new HashSet<string> { "Banana", "Cherry" };
     fruits.UnionWith(moreFruits); // Adds Cherry to `fruits`
    

When to use?

  • To maintain a collection of unique items (e.g., IDs, names).

  • When quick lookups are required.


LinkedList<T>

LinkedList<T> is a generic collection in the System.Collections.Generic namespace that implements a doubly linked list. Each element (or node) contains a reference to both its predecessor and successor, enabling efficient insertion and removal operations.


Key Features

  1. Fast Insertion and Removal: Inserting or removing elements at any position (beginning, middle, or end) is fast, as it doesn’t require shifting other elements.

  2. Doubly Linked: Each node points to both the previous and next nodes, making traversal bidirectional.

  3. Individually Allocated Nodes: Each element is stored as a separate object, which can increase memory overhead.


Performance Considerations

  1. Strengths:

    • Best for frequent insertions or deletions at the middle of the list.

    • No need to copy the entire collection during modifications.

  2. Weaknesses:

    • Higher memory usage compared to arrays or lists (due to references).

    • Sequential access: Slower when accessing elements by index (no direct indexing like List<T>).


Examples

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Create a LinkedList
        LinkedList<string> animals = new LinkedList<string>();

        // Add elements
        animals.AddLast("Dog");
        animals.AddLast("Cat");
        animals.AddFirst("Bird"); // Add at the start

        // Display elements
        Console.WriteLine("Animals in LinkedList:");
        foreach (var animal in animals)
        {
            Console.WriteLine(animal); // Output: Bird, Dog, Cat
        }

        // Insert after a specific node
        LinkedListNode<string> dogNode = animals.Find("Dog");
        if (dogNode != null)
        {
            animals.AddAfter(dogNode, "Rabbit");
        }

        // Remove a specific node
        animals.Remove("Cat");

        // Display updated list
        Console.WriteLine("\\nUpdated LinkedList:");
        foreach (var animal in animals)
        {
            Console.WriteLine(animal); // Output: Bird, Dog, Rabbit
        }
    }
}

When to use LinkedList?

  • Frequent insertions/deletions: Use when you frequently add/remove elements, especially in the middle of the list.

  • Dynamic size: Preferable when the size of the collection changes frequently.

  • Avoid when random access by index is needed; use List<T> instead.


List<T>

List<T> is a generic collection in the System.Collections.Generic namespace. It represents a resizable array that allows:

  1. Indexed Access: Elements can be accessed using an index.

  2. Dynamic Resizing: Automatically resizes when adding elements.

  3. Versatility: Can hold any type of data, including custom objects.


Key Features

  1. Index-Based Access: Quickly access elements using an index.

  2. Dynamic: No need to specify a fixed size; it grows/shrinks as needed.

  3. Methods for Manipulation: Includes methods like Add, Remove, Sort, and Find.


Example

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Create a List of integers
        List<int> numbers = new List<int>();

        // Add elements
        numbers.Add(10);
        numbers.Add(20);
        numbers.Add(30);

        // Access by index
        Console.WriteLine($"First element: {numbers[0]}"); // Output: 10

        // Insert element at a specific position
        numbers.Insert(1, 15); // Adds 15 at index 1

        // Remove element
        numbers.Remove(20); // Removes the first occurrence of 20

        // Display elements
        Console.WriteLine("Numbers in List:");
        foreach (var number in numbers)
        {
            Console.WriteLine(number); // Output: 10, 15, 30
        }

        // Sort the List
        numbers.Sort();
        Console.WriteLine("\\nSorted List:");
        foreach (var number in numbers)
        {
            Console.WriteLine(number); // Output: 10, 15, 30
        }
    }
}

Common Operations

  1. Add an item:

     numbers.Add(40); // Adds 40 to the end of the list.
    
  2. Remove an item:

     numbers.Remove(10); // Removes the first occurrence of 10.
    
  3. Find an item:

     int found = numbers.Find(x => x > 20); // Finds the first element > 20.
    
  4. Access by index:

     int first = numbers[0]; // Accesses the first element.
    
  5. Sort the list:

     numbers.Sort(); // Sorts the list in ascending order.
    

When to use List<T>?

  • Dynamic size: Use when the size of the collection changes frequently.

  • Indexed access: Ideal when you need fast access to elements by index.

  • Avoid when you need frequent insertions/deletions in the middle; prefer LinkedList<T> for such cases.

SortedSet<T>

SortedSet<T> is a generic collection in the System.Collections.Generic namespace that maintains elements in sorted order automatically. It ensures:

  1. No duplicates: Each element is unique.

  2. Sorted order: Elements are sorted based on the default comparer or a custom comparer provided during initialization.


Key Features

  1. Automatically Sorted: Elements are stored in ascending order by default.

  2. No Duplicates: Prevents duplicate elements, like HashSet<T>.

  3. Custom Comparer Support: You can define your sorting logic using an IComparer<T>.


Examples

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Create a SortedSet of integers
        SortedSet<int> numbers = new SortedSet<int>();

        // Add elements
        numbers.Add(30);
        numbers.Add(10);
        numbers.Add(20);
        numbers.Add(10); // Duplicate, won't be added

        // Display elements (sorted automatically)
        Console.WriteLine("Numbers in SortedSet:");
        foreach (var number in numbers)
        {
            Console.WriteLine(number); // Output: 10, 20, 30
        }

        // Check if an element exists
        Console.WriteLine($"Contains 20: {numbers.Contains(20)}"); // Output: True

        // Remove an element
        numbers.Remove(10);

        // Display updated set
        Console.WriteLine("\\nUpdated SortedSet:");
        foreach (var number in numbers)
        {
            Console.WriteLine(number); // Output: 20, 30
        }
    }
}

Common Operations

  1. Add an item:

     numbers.Add(40); // Adds 40, maintaining sorted order.
    
  2. Remove an item:

     numbers.Remove(30); // Removes 30 from the set.
    
  3. Check for an item:

     Console.WriteLine(numbers.Contains(20)); // Returns true if 20 exists.
    
  4. Find elements with conditions:

     // Find the smallest value greater than 15
     int minValue = numbers.GetViewBetween(15, int.MaxValue).Min;
    
  5. Union, Intersection, and Difference:

     SortedSet<int> otherSet = new SortedSet<int> { 20, 40, 50 };
     numbers.UnionWith(otherSet);       // Combines both sets
     numbers.IntersectWith(otherSet);   // Keeps common elements
     numbers.ExceptWith(otherSet);      // Removes elements present in `otherSet`
    

When to use SortedSet<T>?

  • Automatic sorting: Use when you need elements to remain sorted automatically.

  • No duplicates: Ideal for collections where duplicate elements are not allowed.

  • Sorted operations: Suitable when you often need sorted views or ranges.

Dictionary<TKey, TValue>

A Dictionary<TKey, TValue> is a generic collection that stores key-value pairs. It allows for fast lookups, additions, and deletions based on unique keys.


Key Features

  1. Fast Access: Uses a hash table for fast retrieval by key.

  2. Unique Keys: Each key must be unique.

  3. Unordered: The order of elements is not guaranteed.


Examples

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Create a Dictionary
        Dictionary<int, string> students = new Dictionary<int, string>();

        // Add key-value pairs
        students.Add(1, "Alice");
        students.Add(2, "Bob");
        students.Add(3, "Charlie");

        // Access a value by key
        Console.WriteLine($"Student with ID 2: {students[2]}"); // Output: Bob

        // Update a value
        students[2] = "Robert";

        // Remove a key-value pair
        students.Remove(1);

        // Iterate through the Dictionary
        Console.WriteLine("\\nRemaining Students:");
        foreach (var kvp in students)
        {
            Console.WriteLine($"ID: {kvp.Key}, Name: {kvp.Value}");
        }
    }
}
Student with ID 2: Bob

Remaining Students:
ID: 2, Name: Robert
ID: 3, Name: Charlie

SortedDictionary<TKey, TValue>

A SortedDictionary<TKey, TValue> is similar to a Dictionary<TKey, TValue> but keeps the elements sorted by their keys.


Key Features

  1. Automatically Sorted: Keys are stored in ascending order.

  2. Binary Search Tree: Internally uses a red-black tree for operations.

  3. Unique Keys: Each key must be unique, just like a regular dictionary.


Examples

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Create a SortedDictionary
        SortedDictionary<int, string> students = new SortedDictionary<int, string>();

        // Add key-value pairs
        students.Add(3, "Charlie");
        students.Add(1, "Alice");
        students.Add(2, "Bob");

        // Iterate through the SortedDictionary
        Console.WriteLine("Students in SortedDictionary:");
        foreach (var kvp in students)
        {
            Console.WriteLine($"ID: {kvp.Key}, Name: {kvp.Value}");
        }
    }
}

Output:

Students in SortedDictionary:
ID: 1, Name: Alice
ID: 2, Name: Bob
ID: 3, Name: Charlie

Comparison of Dictionary vs SortedDictionary

FeatureDictionarySortedDictionary
Order of KeysUnorderedSorted in ascending order
PerformanceFaster for most operations (O(1))Slower (O(log n)) due to sorting
Best Use CaseFast lookups and updatesWhen sorted keys are needed

When to use which?

  1. Use Dictionary<TKey, TValue>:

    • When key order doesn't matter.

    • For maximum performance in lookups, additions, and removals.

  2. Use SortedDictionary<TKey, TValue>:

    • When you need the keys sorted.

    • For scenarios requiring range-based queries or ordered traversal.

Stack<T>

A Stack<T> is a generic collection in the System.Collections.Generic namespace that follows the LIFO (Last In, First Out) principle. This means the last element added to the stack is the first one to be removed.


Key Features

  1. LIFO Behavior: Operates like a stack of plates—remove the top one first.

  2. Fast Operations: Adding and removing items (via Push and Pop) are O(1).

  3. Use Case: Suitable for scenarios like backtracking, undo functionality, or expression evaluation.


Examples

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Create a Stack
        Stack<string> books = new Stack<string>();

        // Add items to the stack (Push)
        books.Push("The Catcher in the Rye");
        books.Push("To Kill a Mockingbird");
        books.Push("1984");

        // Display the top item
        Console.WriteLine($"Top book: {books.Peek()}"); // Output: 1984

        // Remove the top item (Pop)
        Console.WriteLine($"Removed book: {books.Pop()}"); // Output: 1984

        // Display remaining items
        Console.WriteLine("\\nRemaining Books:");
        foreach (var book in books)
        {
            Console.WriteLine(book);
        }
    }
}

Output:

Top book: 1984
Removed book: 1984

Remaining Books:
To Kill a Mockingbird
The Catcher in the Rye

Queue<T>

A Queue<T> is a generic collection in the System.Collections.Generic namespace that follows the FIFO (First In, First Out) principle. This means the first element added to the queue is the first one to be removed.


Key Features

  1. FIFO Behavior: Works like a queue in real life—serve the first person in line first.

  2. Fast Operations: Adding (Enqueue) and removing (Dequeue) items are O(1).

  3. Use Case: Suitable for scenarios like task scheduling, printer spooling, or breadth-first search.


Example

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Create a Queue
        Queue<string> customers = new Queue<string>();

        // Add items to the queue (Enqueue)
        customers.Enqueue("Alice");
        customers.Enqueue("Bob");
        customers.Enqueue("Charlie");

        // Display the front item
        Console.WriteLine($"First customer: {customers.Peek()}"); // Output: Alice

        // Remove the front item (Dequeue)
        Console.WriteLine($"Served customer: {customers.Dequeue()}"); // Output: Alice

        // Display remaining items
        Console.WriteLine("\\nRemaining Customers:");
        foreach (var customer in customers)
        {
            Console.WriteLine(customer);
        }
    }
}

Output:

First customer: Alice
Served customer: Alice

Remaining Customers:
Bob
Charlie

Comparison of Stack<T> vs Queue<T>

FeatureStack (LIFO)Queue (FIFO)
Order of OperationsLast In, First OutFirst In, First Out
Main MethodsPush, Pop, PeekEnqueue, Dequeue, Peek
Use Case ExamplesUndo functionality, backtrackingTask scheduling, breadth-first search

When to Use Which?

  1. Use Stack<T>:

    • When the most recently added item is needed first.

    • For operations like undo, parsing expressions, or function call tracking.

  2. Use Queue<T>:

    • When the first item added should be processed first.

    • For scenarios like processing requests, managing tasks, or event handling.