Arrays and Collections

Intermediate 35 min read Lesson 7 of 13

What are Collections?

Collections are containers that hold multiple items of data. Think of them like a shopping cart - you can put many items in it, add new ones, remove ones you don't want, and access any item you need.

Array
Fixed size

List
Dynamic size

Dictionary
Key-Value pairs

HashSet
Unique items

Queue
FIFO

Stack
LIFO

Think of it like this: Imagine you have a box of chocolates. An array is a box with a fixed number of slots. A list is a box that can grow. A dictionary is like a labeled box where each chocolate has a name tag.

1. Arrays

An array is a fixed-size collection of items of the same type. Once you create an array, its size cannot change.

When to use Arrays
  • You know the exact number of items
  • You need the fastest performance
  • You're working with a fixed dataset
  • You need to store items of the same type
When NOT to use Arrays
  • You don't know how many items
  • You need to add/remove items
  • You need to insert items in the middle
  • You need a dynamic collection
// ===== DECLARING ARRAYS =====

// Method 1: Create with size (all elements are default values)
int[] numbers = new int[5];  // 5 slots: [0, 0, 0, 0, 0]

// Method 2: Create and initialize with values
string[] names = { "Alice", "Bob", "Charlie" };  // Size is 3

// Method 3: Create with size and assign values
int[] scores = new int[3] { 95, 88, 76 };

// ===== ACCESSING ARRAY ELEMENTS =====
string[] fruits = { "Apple", "Banana", "Orange" };

string first = fruits[0];  // "Apple" (first element - index 0)
string second = fruits[1]; // "Banana" (second element - index 1)
string last = fruits[2];   // "Orange" (third element - index 2)

// Change a value
fruits[1] = "Grape";  // Now: ["Apple", "Grape", "Orange"]

// ===== ARRAY PROPERTIES =====
int length = fruits.Length;  // Gets the number of elements (3)

// ===== LOOPING THROUGH AN ARRAY =====
// Using for loop (with index)
for (int i = 0; i < fruits.Length; i++)
{
    Console.WriteLine($"Index {i}: {fruits[i]}");
}

// Using foreach (cleaner, no index)
foreach (string fruit in fruits)
{
    Console.WriteLine($"Fruit: {fruit}");
}

// ===== MULTIDIMENSIONAL ARRAYS =====
// 2D array (like a grid or table)
int[,] grid = new int[2, 3];  // 2 rows, 3 columns
grid[0, 0] = 1;
grid[0, 1] = 2;
grid[0, 2] = 3;
grid[1, 0] = 4;
grid[1, 1] = 5;
grid[1, 2] = 6;
// grid = [[1,2,3], [4,5,6]]

// Initialize 2D array with values
int[,] grid2 = { { 1, 2 }, { 3, 4 } };
Remember: Arrays are zero-indexed. The first element is at index 0, not 1. Trying to access an index that doesn't exist will cause an error!

2. Lists (List<T>) - The Most Used Collection

A List is like an array that can grow and shrink automatically. It's the most commonly used collection in C# because it's flexible, powerful, and easy to use.

When to use Lists
  • You don't know how many items you'll have
  • You need to add or remove items frequently
  • You need to insert items in the middle
  • You need to search, sort, or filter items
  • You're working with dynamic data from users or databases
Real-World Examples
  • Shopping cart items
  • Student list in a school
  • Employee records
  • Search results
  • Product catalog
// ===== CREATING A LIST =====
using System.Collections.Generic;  // Required for List

// Empty list (most common)
List<string> names = new List<string>();

// With initial items
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// Using var (shortcut)
var students = new List<Student>();  // List of Student objects

// ===== ADDING ITEMS =====
names.Add("Alice");     // Adds at the end
names.Add("Bob");
names.Add("Charlie");

// Add multiple items at once
var moreNames = new List<string> { "Dave", "Eve" };
names.AddRange(moreNames);  // Adds all items from another list

// ===== INSERTING ITEMS (IN THE MIDDLE) =====
names.Insert(1, "Zara");  // Inserts "Zara" at index 1
// Before: ["Alice", "Bob", "Charlie"]
// After:  ["Alice", "Zara", "Bob", "Charlie"]

// ===== ACCESSING ITEMS =====
string first = names[0];  // "Alice"
string last = names[names.Count - 1];  // "Charlie"

// ===== REMOVING ITEMS =====
names.Remove("Bob");       // Removes "Bob" by value
names.RemoveAt(0);      // Removes the first item (index 0)
names.RemoveAll(n => n.StartsWith("A"));  // Removes all items starting with "A"
names.Clear();           // Removes ALL items

// ===== LIST PROPERTIES =====
int count = names.Count;   // Number of items
bool isEmpty = names.Count == 0;
int capacity = names.Capacity;  // Internal size (usually bigger than Count)
// ===== CHECKING IF AN ITEM EXISTS =====
bool hasAlice = names.Contains("Alice");  // true or false

// ===== FINDING ITEMS =====
int index = names.IndexOf("Alice");  // Returns index or -1 if not found
string found = names.Find(n => n.StartsWith("A"));  // Returns first match
List<string> allFound = names.FindAll(n => n.Length > 3);

// ===== SORTING =====
names.Sort();  // Sorts alphabetically (A to Z)
names.Reverse();  // Reverses the order

// ===== LOOPING THROUGH A LIST =====
// Using foreach (cleanest - most common)
foreach (string name in names)
{
    Console.WriteLine($"Hello, {name}");
}

// Using for loop (when you need the index)
for (int i = 0; i < names.Count; i++)
{
    Console.WriteLine($"Index {i}: {names[i]}");
}

// ===== CONVERTING =====
string[] nameArray = names.ToArray();  // Convert to array
List<string> newList = nameArray.ToList();  // Convert array to list
Pro Tip: Use List<T> by default for 90% of your collection needs. It's fast, flexible, and has all the methods you need.

Finding Items in a List - The Find Methods

One of the most powerful features of Lists is the ability to search for items. You can find items that match a condition, check if an item exists, or get all items that match.

What is a Lambda Expression?

Before we dive into Find methods, you need to understand lambda expressions. A lambda is a short way to write a condition. Think of it like saying:

Example: n => n.StartsWith("A")
  • n = each item in the list
  • => = "goes to" or "such that"
  • n.StartsWith("A") = the condition
In plain English:

"For each item n in the list, check if n starts with 'A'"

Method 1: Find() - Returns the First Match

Find() searches the list and returns the first item that matches your condition. If nothing matches, it returns null (or the default value).

// ===== Find() - Get the first matching item =====
List<string> names = new List<string>
{
    "Alice", "Bob", "Charlie", "Anna", "Alex"
};

// Find first name that starts with 'A'
string firstA = names.Find(n => n.StartsWith("A"));
Console.WriteLine(firstA);  // Output: "Alice"

// Find first name with exactly 3 characters
string threeLetter = names.Find(n => n.Length == 3);
Console.WriteLine(threeLetter);  // Output: "Bob"

// Find first name that contains 'li'
string containsLi = names.Find(n => n.Contains("li"));
Console.WriteLine(containsLi);  // Output: "Alice"

// ===== SAFE WAY TO USE Find() - Check for null =====
string result = names.Find(n => n == "Unknown");
if (result != null)
{
    Console.WriteLine($"Found: {result}");
}
else
{
    Console.WriteLine("Name not found");
}

// ===== REAL-WORLD EXAMPLE: Find a customer by ID =====
var customers = new List<Customer>
{
    new Customer { Id = 1, Name = "John" },
    new Customer { Id = 2, Name = "Jane" },
    new Customer { Id = 3, Name = "Bob" }
};

// Find customer with Id = 2
Customer foundCustomer = customers.Find(c => c.Id == 2);
if (foundCustomer != null)
{
    Console.WriteLine($"Found customer: {foundCustomer.Name}");  // Output: Jane
}
When to use Find(): Use it when you need one specific item and you want the first match.
Method 2: FindAll() - Returns All Matches

FindAll() returns a new List containing all items that match your condition. If nothing matches, it returns an empty list (not null).

// ===== FindAll() - Get all matching items =====
List<string> names = new List<string>
{
    "Alice", "Bob", "Anna", "Charlie", "Alex"
};

// Get all names that start with 'A'
List<string> namesStartingWithA = names.FindAll(n => n.StartsWith("A"));
foreach (string name in namesStartingWithA)
{
    Console.WriteLine(name);  // Output: Alice, Anna, Alex
}

// Get all names with exactly 3 characters
List<string> threeLetterNames = names.FindAll(n => n.Length == 3);
// threeLetterNames = ["Bob"]

// Get all names longer than 4 characters
List<string> longNames = names.FindAll(n => n.Length > 4);
// longNames = ["Alice", "Charlie"]

// ===== REAL-WORLD EXAMPLE: Filter active customers =====
var customers = new List<Customer>
{
    new Customer { Name = "John", IsActive = true },
    new Customer { Name = "Jane", IsActive = false },
    new Customer { Name = "Bob", IsActive = true }
};

// Get all active customers
List<Customer> activeCustomers = customers.FindAll(c => c.IsActive);
Console.WriteLine($"Active customers: {activeCustomers.Count}");  // Output: 2
When to use FindAll(): Use it when you need all items that match a condition, like filtering a list.
Method 3: FindIndex() - Returns the Index

FindIndex() returns the index position of the first item that matches. If nothing matches, it returns -1.

// ===== FindIndex() - Get the position of an item =====
List<string> names = new List<string>
{
    "Alice", "Bob", "Charlie", "Anna"
};

// Find index of first name starting with 'A'
int index = names.FindIndex(n => n.StartsWith("A"));
Console.WriteLine($"Index: {index}");  // Output: 0

// Find index of name "Charlie"
int charlieIndex = names.FindIndex(n => n == "Charlie");
Console.WriteLine($"Charlie is at index: {charlieIndex}");  // Output: 2

// Find index of name not in the list
int notFound = names.FindIndex(n => n == "Unknown");
Console.WriteLine($"Index: {notFound}");  // Output: -1

// ===== SAFE WAY TO USE FindIndex() =====
if (notFound != -1)
{
    Console.WriteLine($"Found at index {notFound}");
}
else
{
    Console.WriteLine("Item not found in the list");
}
Remember: -1 means "not found". Always check for -1 before using the index!
Method 4: FindLast() and FindLastIndex() - Search from the End

These methods search the list from the end to the beginning. They are useful when you want the last match instead of the first.

// ===== FindLast() - Get the last matching item =====
List<string> names = new List<string>
{
    "Alice", "Bob", "Anna", "Charlie", "Alex"
};

// Find the LAST name starting with 'A'
string lastA = names.FindLast(n => n.StartsWith("A"));
Console.WriteLine($"Last name starting with A: {lastA}");  // Output: Alex

// Find the last item with exactly 3 characters
string lastThreeLetter = names.FindLast(n => n.Length == 3);
Console.WriteLine($"Last 3-letter name: {lastThreeLetter}");  // Output: Bob

// ===== FindLastIndex() - Get the index from the end =====
int lastIndex = names.FindLastIndex(n => n.StartsWith("A"));
Console.WriteLine($"Last A name is at index: {lastIndex}");  // Output: 4
Method 5: Where() - The LINQ Way

Where() is similar to FindAll() but it comes from LINQ (Language Integrated Query). It's more powerful and can be used with any collection.

// ===== Where() - LINQ filtering =====
using System.Linq;  // Required for LINQ

List<string> names = new List<string>
{
    "Alice", "Bob", "Anna", "Charlie", "Alex"
};

// Filter names starting with 'A'
IEnumerable<string> filtered = names.Where(n => n.StartsWith("A"));

// Convert to List
List<string> result = filtered.ToList();

// Or chain it together
List<string> namesA = names.Where(n => n.StartsWith("A")).ToList();

// ===== Difference between FindAll() and Where() =====
// FindAll() returns a List immediately
// Where() returns an IEnumerable (lazy evaluation) - more efficient

// ===== REAL-WORLD EXAMPLE: Find customers who spent more than $100 =====
var orders = new List<int> { 50, 150, 75, 200, 25 };
var largeOrders = orders.Where(o => o > 100).ToList();
// largeOrders = [150, 200]
Checking Methods: Any(), All(), and Contains()

These methods check if items exist without returning the actual items.

// ===== Any() - Check if ANY item matches =====
List<string> names = new List<string>
{
    "Alice", "Bob", "Charlie"
};

bool hasA = names.Any(n => n.StartsWith("A"));  // true
bool hasZ = names.Any(n => n.StartsWith("Z"));  // false

// ===== All() - Check if ALL items match =====
bool allStartWithA = names.All(n => n.StartsWith("A"));  // false (Bob and Charlie don't start with A)

List<int> numbers = new List<int> { 2, 4, 6, 8 };
bool allEven = numbers.All(n => n % 2 == 0);  // true

// ===== Contains() - Check if specific item exists =====
bool hasBob = names.Contains("Bob");  // true
bool hasDave = names.Contains("Dave");  // false
Quick Comparison: Which Method to Use?
Method Returns When to Use
Find() First match (or null) Need one specific item
FindAll() List of all matches Need all items that match
FindIndex() Index number (or -1) Need the position of an item
FindLast() Last match (or null) Need the last matching item
Where() IEnumerable (use ToList()) LINQ filtering (more powerful)
Any() true/false Check if ANY item matches
All() true/false Check if ALL items match
Contains() true/false Check if a specific item exists
Exercise: Practice Finding

Try these challenges:

  1. Create a list of 10 random numbers
  2. Use Find() to get the first number greater than 50
  3. Use FindAll() to get all numbers between 20 and 80
  4. Use FindIndex() to find the position of the number 42
  5. Use Any() to check if any number is greater than 100
  6. Use All() to check if all numbers are positive

3. Dictionary<TKey, TValue> - Key-Value Lookups

A Dictionary stores items as key-value pairs. Think of it like a real dictionary or phone book - you look up a word (the key) to find its definition (the value). Keys must be unique.

When to use Dictionary
  • You need to look up items by a unique key
  • You need fast lookups (O(1) time)
  • You have key-value data
  • Example: Student ID → Student, Product ID → Product
  • Example: Username → User object
When NOT to use Dictionary
  • You don't have unique keys
  • You need to access items by position (use List instead)
  • You have a small collection (List is simpler)
  • You need to preserve insertion order
  • You need to iterate in a specific order
// ===== CREATING A DICTIONARY =====
using System.Collections.Generic;  // Required

// Empty dictionary
Dictionary<string, int> ages = new Dictionary<string, int>();

// With initial values
Dictionary<string, string> countries = new Dictionary<string, string>
{
    { "USA", "Washington D.C." },
    { "Canada", "Ottawa" },
    { "UK", "London" }
};

// Using var
var productPrices = new Dictionary<int, decimal>();

// ===== ADDING ITEMS =====
// Method 1: Using indexer (adds or updates)
ages["Alice"] = 25;
ages["Bob"] = 30;

// Method 2: Using Add() (throws error if key exists)
ages.Add("Charlie", 35);

// Method 3: TryAdd (safe - returns true/false)
if (ages.TryAdd("Dave", 28))
{
    Console.WriteLine("Dave added successfully");
}
else
{
    Console.WriteLine("Dave already exists");
}

// ===== ACCESSING VALUES =====
int aliceAge = ages["Alice"];  // 25 (throws error if key doesn't exist)

// ===== SAFE ACCESS (Best Practices) =====

// Method 1: Check if key exists first
if (ages.ContainsKey("Bob"))
{
    Console.WriteLine($"Bob is {ages["Bob"]} years old");
}

// Method 2: TryGetValue (BEST - one lookup, no error)
if (ages.TryGetValue("Unknown", out int age))
{
    Console.WriteLine($"Age: {age}");
}
else
{
    Console.WriteLine("Key not found");
}

// Method 3: GetValueOrDefault (C# 8+)
int unknownAge = ages.GetValueOrDefault("Unknown", 0);  // Returns 0 if not found

// ===== UPDATING VALUES =====
ages["Alice"] = 26;  // Updates Alice's age to 26

// ===== REMOVING ITEMS =====
bool removed = ages.Remove("Bob");  // Removes Bob, returns true if found
ages.Clear();  // Removes ALL items

// ===== DICTIONARY PROPERTIES =====
int count = ages.Count;  // Number of key-value pairs
List<string> allKeys = new List<string>(ages.Keys);  // Get all keys
List<int> allValues = new List<int>(ages.Values);  // Get all values
bool hasKeys = ages.Keys.Any();  // Check if there are any keys

// ===== LOOPING THROUGH A DICTIONARY =====
// Using foreach with KeyValuePair
foreach (KeyValuePair<string, int> item in ages)
{
    Console.WriteLine($"{item.Key} is {item.Value} years old");
}

// Loop through keys only
foreach (string key in ages.Keys)
{
    Console.WriteLine($"Key: {key}");
}

// Loop through values only
foreach (int value in ages.Values)
{
    Console.WriteLine($"Value: {value}");
}
Important: Keys must be unique. If you try to add a key that already exists using Add(), the program will throw an error. Use the indexer dictionary[key] = value to update existing keys.

4. HashSet<T> - Unique Items Only

A HashSet stores unique items. If you try to add a duplicate, it's automatically ignored. Think of it like a guest list - each person can only be on the list once.

When to use HashSet
  • You need to store unique items
  • You need fast lookups (check if an item exists)
  • You want to remove duplicates from a list
  • You need to perform set operations (union, intersection)
  • Example: Unique visitors, unique tags, unique words
When NOT to use HashSet
  • You need to access items by index
  • You need to preserve insertion order
  • You have duplicate items (use List instead)
  • You need to sort items
// ===== CREATING A HASHSET =====
HashSet<string> uniqueNames = new HashSet<string>();

// With initial items (duplicates automatically removed)
var tags = new HashSet<string> { "C#", "Java", "Python", "C#" };
// tags = ["C#", "Java", "Python"]  (C# only appears once)

// ===== ADDING ITEMS =====
uniqueNames.Add("Alice");  // true (added)
uniqueNames.Add("Bob");    // true (added)
uniqueNames.Add("Alice");  // false (ignored - already exists)

// ===== CHECKING IF AN ITEM EXISTS =====
bool hasAlice = uniqueNames.Contains("Alice");  // true
bool hasDave = uniqueNames.Contains("Dave");    // false

// ===== REMOVING ITEMS =====
bool removed = uniqueNames.Remove("Bob");  // true
uniqueNames.Clear();  // Removes all items

// ===== HASHSET PROPERTIES =====
int count = uniqueNames.Count;  // Number of unique items

// ===== SET OPERATIONS =====
var setA = new HashSet<int> { 1, 2, 3, 4 };
var setB = new HashSet<int> { 3, 4, 5, 6 };

// Union - combines both sets (no duplicates)
var union = new HashSet<int>(setA);
union.UnionWith(setB);  // {1,2,3,4,5,6}

// Intersection - items in both sets
var intersection = new HashSet<int>(setA);
intersection.IntersectWith(setB);  // {3,4}

// Difference - items in setA but not in setB
var difference = new HashSet<int>(setA);
difference.ExceptWith(setB);  // {1,2}

// ===== LOOPING THROUGH A HASHSET =====
foreach (string name in uniqueNames)
{
    Console.WriteLine(name);
}

// ===== REMOVING DUPLICATES FROM A LIST =====
var listWithDuplicates = new List<int> { 1, 2, 2, 3, 3, 4 };
var uniqueNumbers = new HashSet<int>(listWithDuplicates);
// uniqueNumbers = {1,2,3,4}
Remember: HashSet is perfect when you need to check if an item exists quickly and you don't care about the order.

5. Queue<T> - First In, First Out (FIFO)

A Queue works like a line at a store or a ticket counter. The first person in line is the first person served. Items are added to the back and removed from the front.

When to use Queue
  • Processing items in order
  • Task scheduling
  • Customer service systems
  • Print jobs
  • Message processing
  • Order processing
Real-World Examples
  • Line at a coffee shop
  • Print queue
  • Customer support tickets
  • Background tasks
  • Data processing pipeline
// ===== CREATING A QUEUE =====
Queue<string> queue = new Queue<string>();

// With initial items
var tasks = new Queue<string>(new string[] { "Task 1", "Task 2", "Task 3" });

// ===== ADDING ITEMS (Enqueue) =====
queue.Enqueue("First");   // Adds to the back
queue.Enqueue("Second");  // Adds to the back
queue.Enqueue("Third");   // Adds to the back
// queue = ["First", "Second", "Third"]

// ===== REMOVING ITEMS (Dequeue) =====
string firstItem = queue.Dequeue();  // "First" (removes from front)
string nextItem = queue.Dequeue();   // "Second"
// queue now has: ["Third"]

// ===== LOOKING AT ITEMS WITHOUT REMOVING =====
string front = queue.Peek();  // "Third" (doesn't remove)

// ===== QUEUE PROPERTIES =====
int count = queue.Count;  // Number of items
bool isEmpty = queue.Count == 0;

// ===== CHECKING IF AN ITEM EXISTS =====
bool hasFirst = queue.Contains("First");  // true or false

// ===== CLEARING =====
queue.Clear();  // Removes all items

// ===== LOOPING THROUGH A QUEUE =====
// Using foreach (doesn't remove items)
foreach (string item in queue)
{
    Console.WriteLine($"Processing: {item}");
}

// Process all items (removes them)
while (queue.Count > 0)
{
    string item = queue.Dequeue();
    Console.WriteLine($"Processing and removing: {item}");
}
Remember: FIFO = First In, First Out. Think of a queue like a line at a store.

6. Stack<T> - Last In, First Out (LIFO)

A Stack works like a stack of plates. The last plate you put on top is the first plate you take off. Items are added to the top and removed from the top.

When to use Stack
  • Undo/Redo functionality
  • Browser history
  • Backtracking algorithms
  • Expression evaluation
  • Function call stack
  • Recursive operations
Real-World Examples
  • Browser back button
  • Undo in text editors (Ctrl+Z)
  • Stack of books
  • Calculator history
  • Game inventory (LIFO)
// ===== CREATING A STACK =====
Stack<string> stack = new Stack<string>();

// With initial items
var history = new Stack<string>(new string[] { "Page 1", "Page 2" });

// ===== ADDING ITEMS (Push) =====
stack.Push("First");   // Adds to the top
stack.Push("Second");  // Adds to the top
stack.Push("Third");   // Adds to the top
// stack = ["First", "Second", "Third"] (Third is on top)

// ===== REMOVING ITEMS (Pop) =====
string lastItem = stack.Pop();  // "Third" (removes from top)
string previous = stack.Pop();  // "Second"
// stack now has: ["First"]

// ===== LOOKING AT ITEMS WITHOUT REMOVING =====
string top = stack.Peek();  // "First" (doesn't remove)

// ===== STACK PROPERTIES =====
int count = stack.Count;  // Number of items
bool isEmpty = stack.Count == 0;

// ===== CHECKING IF AN ITEM EXISTS =====
bool hasFirst = stack.Contains("First");  // true or false

// ===== CLEARING =====
stack.Clear();  // Removes all items

// ===== LOOPING THROUGH A STACK =====
// Using foreach (doesn't remove items)
foreach (string item in stack)
{
    Console.WriteLine($"Item: {item}");
}

// Process all items (removes them)
while (stack.Count > 0)
{
    string item = stack.Pop();
    Console.WriteLine($"Processing and removing: {item}");
}

// ===== REAL-WORLD EXAMPLE: Undo System =====
var undoStack = new Stack<string>();

// User performs actions
undoStack.Push("Typed 'Hello'");
undoStack.Push("Typed ' World'");
undoStack.Push("Added emoji");

// User presses Undo (Ctrl+Z)
string lastAction = undoStack.Pop();  // "Added emoji"
Console.WriteLine($"Undo: {lastAction}");

// Undo again
lastAction = undoStack.Pop();  // "Typed ' World'"
Console.WriteLine($"Undo: {lastAction}");
Remember: LIFO = Last In, First Out. Think of a stack like a stack of plates or a pile of books.

7. LinkedList<T> - Fast Insert/Delete

A LinkedList is a collection where each item points to the next and previous item. It's like a chain of items. It's very fast at inserting and deleting items in the middle.

When to use LinkedList
  • You frequently insert/delete in the middle
  • You need to process items in both directions (forward/backward)
  • You don't need random access by index
  • You're building a queue or stack
  • Example: Music playlist, document history
When NOT to use LinkedList
  • You need to access items by index (use List instead)
  • You need to search for items (LinkedList is slow for searching)
  • You have a small collection (List is simpler)
  • You need to sort items
// ===== CREATING A LINKEDLIST =====
LinkedList<string> playlist = new LinkedList<string>();

// With initial items
var songs = new LinkedList<string>(new string[] { "Song 1", "Song 2", "Song 3" });

// ===== ADDING ITEMS =====
playlist.AddLast("First");   // Adds at the end
playlist.AddLast("Second");  // Adds at the end
playlist.AddFirst("Zero");   // Adds at the beginning
// playlist = ["Zero", "First", "Second"]

// Insert before/after a specific node
LinkedListNode<string> firstNode = playlist.First;
playlist.AddAfter(firstNode, "After First");
// playlist = ["Zero", "First", "After First", "Second"]

// ===== ACCESSING ITEMS =====
string first = playlist.First.Value;     // "Zero"
string last = playlist.Last.Value;       // "Second"
LinkedListNode<string> node = playlist.First.Next;  // Gets the next node
LinkedListNode<string> prevNode = node.Previous;  // Gets the previous node

// ===== REMOVING ITEMS =====
bool removed = playlist.Remove("First");  // Removes by value
playlist.RemoveFirst();  // Removes the first item
playlist.RemoveLast();   // Removes the last item
playlist.Clear();  // Removes all items

// Remove a specific node
LinkedListNode<string> nodeToRemove = playlist.First;
playlist.Remove(nodeToRemove);

// ===== LINKEDLIST PROPERTIES =====
int count = playlist.Count;  // Number of items
bool isEmpty = playlist.Count == 0;

// ===== CHECKING IF AN ITEM EXISTS =====
bool hasFirst = playlist.Contains("First");  // true or false

// ===== LOOPING THROUGH A LINKEDLIST =====
foreach (string item in playlist)
{
    Console.WriteLine($"Song: {item}");
}

// Looping with nodes
LinkedListNode<string> current = playlist.First;
while (current != null)
{
    Console.WriteLine($"Current: {current.Value}");
    current = current.Next;  // Move to the next node
}
Remember: LinkedList is great when you need to insert or delete items in the middle frequently. But if you need to access items by index, use List instead.

Quick Comparison

Collection Order Unique Access Best For
List<T> Insertion order No By index Most common, dynamic data
Dictionary Not guaranteed Keys only By key Fast lookups
HashSet Not guaranteed Yes By value Unique items
Queue FIFO No First only Ordered processing
Stack LIFO No Last only Undo/redo, backtracking
LinkedList Insertion order No Node traversal Fast insert/delete in middle

Exercise: Student Management System

Task: Create a student management system using collections.

Instructions:
  1. Create a console application called "StudentManager"
  2. Use a List<Student> to store students
  3. Each student should have: ID (int), Name (string), Grade (double)
  4. Use a Dictionary to map student IDs to student objects
  5. Implement these features:
    • Add a new student
    • Find a student by ID
    • Display all students
    • Calculate the average grade
    • Find the highest and lowest grade
  6. Use foreach to display all students
  7. Use ContainsKey to check if a student exists
Hints:
  • Create a Student class with properties
  • Use List<Student> students = new List<Student>();
  • Use Dictionary<int, Student> studentDict = new Dictionary<int, Student>();
  • When adding, add to both the List and Dictionary
  • Use students.Average(s => s.Grade) for average
Expected Output:
--- Student Management System ---
1. Add Student
2. Find Student
3. Display All Students
4. Show Statistics
5. Exit
Choose an option: 1
Enter student ID: 101
Enter student name: Alice
Enter student grade: 85.5
Student added successfully!

--- Student Management System ---
1. Add Student
2. Find Student
3. Display All Students
4. Show Statistics
5. Exit
Choose an option: 3
ID: 101, Name: Alice, Grade: 85.5
ID: 102, Name: Bob, Grade: 92.0
Key Takeaway

Collections are essential for storing and managing groups of data:

Array → Fixed size, fast
List<T> → Dynamic size, most common
Dictionary → Key-value lookups
HashSet → Unique items
Queue → First In, First Out
Stack → Last In, First Out