/*
- This declares a reference variable 'arr' of type int[].
- No array object is created.
- If local variable -> initially unassigned (not even 'null'). Must assign before use (compile-time error otherwise).
- If field (class member) -> defaults to null automatically.
*/
int[] arr;
 
/*
- Allocates a new int array of fixed size 5 on the heap.
- Memory for 5 contiguous int elements is created.
- All elements are automatically initialized to default(int) -> 0.
- Time complexity -> O(n) due to zero-initialization of all elements.
- Size is fixed after allocation (cannot resize).
*/
arr = new int[5];   // {0,0,0,0,0}
 
/*
- Declares and allocates a new int array with 4 elements.
- Elements are initialized with the specified values {5, 2, 8, 1}.
- Compiler infers the array size from the number of elements.
- Memory is contiguous.
- Time complexity -> O(n) due to allocation + element initialization.
*/
int[] nums = { 5, 2, 8, 1 };
 
/*
- Accesses element at index 0 from the array 'nums'.
- Arrays provide constant-time random access via index.
- Time complexity -> O(1).
- Performs bounds checking at runtime (throws IndexOutOfRangeException if invalid index).
*/
int first = nums[0];
 
/*
- Updates the element at index 1 in the array 'nums'.
- Direct index assignment (random access).
- Time complexity -> O(1).
- Performs bounds checking at runtime (throws IndexOutOfRangeException if index is invalid).
*/
nums[1] = 10;
 
/*
- Retrieves the total number of elements in the array 'nums'.
- 'Length' is a property of the array type (not a method).
- Stored internally, so no iteration is performed.
- Time complexity -> O(1).
- Value is fixed after allocation (arrays have immutable size).
*/
int length = nums.Length;
 
/*
- Iterates through the array using index-based access.
- Loop runs from index 0 to Length - 1.
- Each element access is O(1), total traversal cost -> O(n).
- Bounds checking occurs on every nums[i] access.
- Suitable when index is needed (e.g., comparisons, modifications).
*/
for (int i = 0; i < nums.Length; i++)
{
    Console.WriteLine(nums[i]);
}
 
/*
- Iterates through the array using foreach.
- Internally uses an enumerator to traverse elements sequentially.
- Does not expose the index directly.
- Each element is accessed once -> total time complexity O(n).
- Iteration variable 'x' is a read-only copy of each element.  
- For value types (e.g., int) -> cannot modify array elements via 'x'.  
- For reference types -> cannot reassign element, but can modify the object's internal state.
*/
foreach (int x in nums)
{
    Console.WriteLine(x);
}
 
/*
- Allocates a new array 'copy' with the same length as 'nums'.
- Calls Array.Copy(sourceArray, destinationArray, length):
    - sourceArray -> 'nums' (array to copy from)
    - destinationArray -> 'copy' (array to copy into)
    - length -> number of elements to copy ('nums.Length')
- Copies each element from 'nums' to 'copy'.
- Time complexity -> O(n) because each element is copied.
- Elements are copied by value:
    - For value types -> actual values are copied.
    - For reference types -> references are copied (shallow copy).
*/
int[] copy = new int[nums.Length];
Array.Copy(nums, copy, nums.Length);
 
/*
- Allocates a new array 'partial' with length 2.
- Copies a subrange of elements from 'nums' into 'partial'.
- Array.Copy parameters:
    - sourceArray -> 'nums' (array to copy from)
    - sourceIndex -> 1 (start copying from nums[1])
    - destinationArray -> 'partial' (array to copy into)
    - destinationIndex -> 0 (start placing at partial[0])
    - length -> 2 (number of elements to copy)
- Time complexity -> O(k), where k = number of elements copied.
- Elements copied by value:
    - Value types -> actual values copied
    - Reference types -> references copied (shallow copy)
*/
int[] partial = new int[2];
Array.Copy(nums, 1, partial, 0, 2);
 
/*
- Creates a shallow copy of the array 'nums'.
- Returns an object, so a cast to int[] is required.
- Allocates a new array of the same length and copies all elements.
- Time complexity -> O(n) (each element is copied once).
- Elements copied by value:
    - Value types -> actual values copied
    - Reference types -> references copied (shallow copy)
*/
int[] cloned = (int[])nums.Clone();
 
/*
- Resizes the array 'nums' to a new length (6 in this case).
- Internally allocates a new array of the specified size.
- Copies existing elements from the old array to the new array.
- Time complexity -> O(n) (n = number of elements in the original array).
- If new size > old size -> additional elements are initialized to default(int) -> 0.
- If new size < old size -> extra elements are truncated.
- Original array reference is updated via 'ref' parameter (The original variable 'nums' is updated to point to the new array).
- Note: Arrays in C# are fixed-size; Resize always creates a new array.
*/
Array.Resize(ref nums, 6);
 
/*
- Sorts the array 'nums' in-place using the default comparer (int in this case).
- Internally uses QuickSort/Introspective Sort (Introsort) for primitive types.
- Time complexity -> O(nlog n).
- No new array is allocated; sorting modifies the original array.
- Elements are compared using the default comparer (IComparable for value types).
*/
Array.Sort(nums);
 
/*
- Reverses the elements of the array 'nums' in-place.
- No new array is allocated; the original array is modified.
- Swaps elements from both ends moving toward the center.
- Time complexity -> O(n) (n = number of elements in the array).
- Space complexity -> O(1) (constant extra space for swaps).
*/
Array.Reverse(nums);
 
/*
- Array.Reverse with a subrange.
- Reverses elements starting from 'startIndex' for 'length' elements in-place.
- Original array is modified; other elements remain unchanged.
- Time complexity -> O(length)
*/
int[] arr2 = { 1, 2, 3, 4, 5 };
Array.Reverse(arr2, 1, 3); // arr2 -> {1, 4, 3, 2, 5}
 
/*
- Performs a binary search for the value 8 in the sorted array 'nums'.
- Array must be sorted in ascending order for correct results.
- Returns the index of the element if found.
- If not found, returns a negative number: ~insertionIndex (bitwise complement of where it would be inserted).
- Time complexity -> O(logn).
- Binary search uses a divide-and-conquer approach (halves the search space each step).
*/
int index = Array.BinarySearch(nums, 8);
 
/*
- Performs a linear search for an element in the array 'nums'.
- Returns the index of the first occurrence if found; -1 if not present.
- Syntax:
    1. Array.IndexOf(nums, value) -> searches the entire array.
    2. Array.IndexOf(nums, value, startIndex, count) -> searches a specific range:
       - startIndex -> index to start searching from
       - count -> number of elements to check
- Time complexity:
    - O(n) for full array search
    - O(count) for range search
- Useful for unsorted arrays or when searching within a subarray.
*/
int idx = Array.IndexOf(nums, 10);
int idx2 = Array.IndexOf(nums, 3, 1, 3); // searches for 3 starting at index 1, up to 3 elements
 
/*
- Checks if the value 5 exists in the array 'nums' using LINQ.
- Returns true if found, false otherwise.
- Time complexity -> O(n) (scans elements sequentially).
- Does not modify the array.
- Requires 'using System.Linq;' namespace.
*/
bool exists = nums.Contains(5);
 
/*
- Array.Exists checks if at least one element satisfies a condition (predicate).
- Returns true if any element matches the predicate; otherwise false.
- Time complexity -> O(n) (scans each element until match is found).
- Useful when LINQ is not preferred.
*/
bool hasEven = Array.Exists(nums, x => x % 2 == 0); // true
 
/*
- Array.Find returns the first element that satisfies a given predicate.
- If no element matches, returns default(T) (0 for int, null for reference types).
- Time complexity -> O(n) (scans sequentially until match).
*/
int firstEven = Array.Find(nums, x => x % 2 == 0); // 2
 
/*
- Sets a range of elements in the array 'nums' to their default value (0 for int).
- Parameters:
    - nums -> array to clear
    - 0 -> start index
    - nums.Length -> number of elements to clear
- Time complexity -> O(n) (each element in the range is set individually).
- In-place operation; no new array allocated.
- Useful for resetting or reusing an array.
*/
Array.Clear(nums, 0, nums.Length);
 
/*
- Creates a new array 'filled' with 5 elements, each initialized to 7.
- Uses LINQ's Enumerable.Repeat to generate the sequence.
- Converts the sequence to an array with ToArray().
*/
int[] filled = Enumerable.Repeat(7, 5).ToArray();
 
/*
- Converts the array 'nums' into a List<int>.
- Time complexity -> O(n) (copies each element into the new List).
- Space complexity -> O(n) (new List allocated).
- Original array 'nums' is not modified.
- Allows using List methods like Add, Remove, Insert, etc.
- Requires 'using System.Linq;' namespace.
*/
var list = nums.ToList();
 
/*
- Declares and allocates a rectangular 2D array 'grid' with 2 rows and 3 columns.
- All elements are initialized to default(int) -> 0.
- Memory is contiguous for each row, internally stored as a single block.
- Time complexity -> O(m*n) (initialization of all elements, m = rows, n = columns).
- Access elements using grid[row, col].
- Size is fixed after allocation; cannot resize rows or columns.
*/
int[,] grid = new int[2, 3];
 
/*
- Accesses or updates an element in a rectangular 2D array 'grid' by row and column indices.
- grid[0, 1] = 5; sets the element at row 0, column 1 to 5.
- int val2D = grid[0, 1]; reads the value at the same position.
- Time complexity -> O(1) (direct index access).
- Performs bounds checking at runtime (IndexOutOfRangeException if indices are invalid).
*/
grid[0, 1] = 5;
int val2D = grid[0, 1];
 
/*  
- Iterates through a rectangular 2D array 'grid' using index-based nested loops.
- We use grid.GetLength(dim) to get the number of rows and columns:  
	- grid.GetLength(0) -> number of rows (m)  
	- grid.GetLength(1) -> number of columns (n)  
- Each element is accessed once -> total time complexity O(m*n).
*/  
for (int i = 0; i < grid.GetLength(0); i++) // iterate rows  
{  
	for (int j = 0; j < grid.GetLength(1); j++) // iterate columns  
	{  
		Console.WriteLine(grid[i, j]);
	}  
}  
  
/*  
- Iterates through a rectangular 2D array 'grid' using foreach.  
- Foreach visits each element in row-major order.  
- Does not expose row/column indices directly.  
- Total time complexity -> O(m*n).  
- Read-only iteration variable; cannot modify array elements via 'val' in value types.
*/  
foreach (int val in grid)  
{  
	Console.WriteLine(val);
}
 
/*
- Declares a jagged array (array of arrays) 'jagged' with 2 rows.
- Each row is an independent array, can have different lengths.
- jagged[0] = {1, 2} and jagged[1] = {3, 4, 5} demonstrates unequal lengths.
- Access elements via jagged[row][col].
- Time complexity for allocation -> O(sum of lengths of all inner arrays),  
because each inner array is allocated and initialized separately.
- Useful when rows have variable lengths or non-rectangular data.
- Allows dynamic resizing of inner arrays independently (each row can be assigned a different array).
*/
int[][] jagged = new int[2][];
jagged[0] = new int[] { 1, 2 };
jagged[1] = new int[] { 3, 4, 5 };
 
/*
- Accesses an element in a jagged array 'jagged' by row and column indices.
- jagged[1][2] reads the element at row 1, column 2.
- Time complexity -> O(1) (direct index access).
- Performs bounds checking at runtime (IndexOutOfRangeException if indices are invalid).
- Each inner array can have different lengths; indices must be valid for the specific inner array.
*/
int valJagged = jagged[1][2];
 
/*  
- Iterates through a jagged array 'jagged' using nested for loops.  
- Outer loop traverses rows, inner loop traverses elements in each row.  
- Each element is accessed once -> total time complexity O(total number of elements).  
- Performs bounds checking for each access.  
*/  
for (int i = 0; i < jagged.Length; i++)  
{  
	for (int j = 0; j < jagged[i].Length; j++)  
	{  
		Console.WriteLine(jagged[i][j]);
	}  
}
  
/*  
- Iterates through a jagged array 'jagged' using nested foreach loops.  
- Outer foreach traverses each row array, inner foreach traverses elements in that row.  
- Each element is accessed once -> total time complexity O(total number of elements).  
- Read-only iteration variable; for reference types, internal state can be modified.  
*/  
foreach (int[] row in jagged)  
{  
	foreach (int val in row)  
	{  
		Console.WriteLine(val);  
	}  
}
 
/*
- Creates subarrays (slices) from an existing array using the range operator (C# 8+).
- Syntax and examples:
    1. nums[start..end] -> elements from index 'start' up to, but not including 'end'.
    2. nums[..end] -> elements from index 0 up to, but not including 'end'.
    3. nums[start..] -> elements from index 'start' to the last element.
- Original array is not modified; new arrays are created.
- Time complexity -> O(k), where k = number of elements in the slice.
*/
int[] nums2 = { 1, 2, 3, 4, 5 };
int[] sub = nums2[1..4];    // {2, 3, 4}
int[] firstThree = nums2[..3]; // {1, 2, 3}
int[] lastThree = nums2[2..];  // {3, 4, 5}

/*
- Declares and allocates a new generic List of integers.
- 'list' is a reference variable pointing to a List<int> object on the heap.
- Internally, List<T> uses a resizable array to store elements.
- Initial capacity is small (0 or 4 depending on .NET version); capacity grows automatically as elements are added.
- Count -> number of actual elements in the list (starts at 0).
- Capacity -> size of the internal array (may be larger than Count; resizes dynamically when exceeded).
- Time complexity:
    - Access by index: O(1)
    - Add at end: Amortized O(1)
    - Insert/remove at arbitrary index: O(n)
- Suitable for dynamic collections where number of elements changes frequently.
*/
List<int> list = new List<int>();
 
/*
- Adds an element to the end of the list.
- Internal array may need to resize if Count == Capacity:
    - When resizing occurs, a new larger array is allocated and existing elements are copied over.
    - Resizing is usually by doubling the capacity.
- Time complexity:
    - Amortized O(1) for Add at end
    - Worst-case O(n) if resizing happens
*/
list.Add(10);
list.Add(20);
 
/*
- Inserts an element at a specified index in the list.
- All elements from the insertion index onward are shifted one position to the right.
- Time complexity -> O(n) in the worst case (shifting elements).
- Does not resize unless Count == Capacity; in that case, resizing occurs first (copying all elements to a new array).
- Example:
    - Inserts 15 at index 1 in the list.
*/
list.Insert(1, 15);
 
/*
- Accesses an element at the specified index in the list.
- Provides constant-time random access O(1).
- Performs bounds checking at runtime (throws ArgumentOutOfRangeException if index is invalid).
*/
int first = list[0];
 
/*
- Updates the element at the specified index in the list.
- Provides constant-time random access O(1).
- Performs bounds checking at runtime (throws ArgumentOutOfRangeException if index is invalid).
*/
list[0] = 5;
 
/*
- Removes the first occurrence of the specified value from the list.
- Time complexity -> O(n) (searches for the element, then shifts subsequent elements left).
- Returns true if an element was removed; false if not found.
*/
list.Remove(15);
 
/*
- Removes the element at the specified index from the list.
- Time complexity -> O(n) (shifts subsequent elements left after removal).
- Performs bounds checking at runtime (throws ArgumentOutOfRangeException if index is invalid).
*/
list.RemoveAt(0);
 
/*
- Checks if the specified value exists in the list.
- Performs a linear search from start to end.
- Time complexity -> O(n) (scans each element sequentially).
- Returns true if the element is found; otherwise false.
*/
bool exists = list.Contains(20);
 
/*
- Returns the index of the first occurrence of the specified value in the list.
- Performs a linear search from start to end.
- Time complexity -> O(n).
- Returns -1 if the element is not found.
*/
int index = list.IndexOf(20);
 
/*
- Retrieves the number of elements currently in the list.
- Time complexity -> O(1) (stored internally, no iteration required).
- Equivalent to array.Length for arrays.
*/
int count = list.Count;
 
/*
- Sorts the elements of the list in-place using the default comparer (IComparable<T> for value types).
- Time complexity -> O(nlog n)
- No new list is created; the original list is modified.
*/
list.Sort();
 
/*
- Reverses the elements of the list in-place.
- Time complexity -> O(n) (each element is swapped once).
- No new list is created; the original list is modified.
*/
list.Reverse();
 
/*
- Iterates through all elements of the list sequentially.
- Time complexity -> O(n) (each element accessed once).
- Iteration variable 'x' is read-only:
    - For value types (e.g., int) -> modifying 'x' does not change the element in the list.
    - For reference types -> you can modify the object's internal state, but cannot reassign 'x' to a new object.
*/
foreach (int x in list)
{
    Console.WriteLine(x);
}
 
/*
- Iterates through the list using index-based for loop.
- Each element is accessed sequentially via list[i].
- Time complexity -> O(n) (each element accessed once).
- Allows modification of elements directly by index:
    - list[i] = newValue; updates the element in the list.
- Useful when you need both the element and its index, or want to modify elements.
*/
for (int i = 0; i < list.Count; i++)
{
    Console.WriteLine(list[i]);
}
 
/*
- Executes an action for each element of the list.
- Time complexity -> O(n)
- Similar to foreach loop, but allows using a lambda directly.
*/
list.ForEach(x => Console.WriteLine(x));
 
/*
- Creates a new List<T> containing a subrange of elements.
- Parameters:
    - index -> starting index
    - count -> number of elements
- Time complexity -> O(count)
- Original list remains unmodified.
*/
List<int> subList = list.GetRange(1, 3);  // gets 3 elements starting at index 1
 
/*
- Converts each element to another type using a converter function.
- Returns a new List<TResult> without modifying the original.
- Time complexity -> O(n)
*/
List<string> strList = list.ConvertAll(x => x.ToString());
 
/*
- Checks if any element satisfies a condition (predicate).
- Returns true if at least one element matches.
- Time complexity -> O(n)
*/
bool anyMatch = list.Exists(x => x > 10);
 
/*
- Checks if all elements satisfy a condition.
- Returns true if all elements match.
- Time complexity -> O(n)
*/
bool allMatch = list.TrueForAll(x => x >= 0);
 
/*
- Converts the List<T> into a new array of type T[].
- Time complexity -> O(n) (each element is copied once).
- Allocates a new array; original list remains unmodified.
- Useful when you need fixed-size contiguous memory or array-specific operations.
*/
int[] arr = list.ToArray();
 
/*
- Removes all elements from the list.
- Time complexity -> O(n) (each element reference is cleared).
- List.Count becomes 0.
- Capacity remains unchanged (no memory is released; underlying array is retained for reuse).
*/
list.Clear();
 
/*
- Capacity property represents the size of the internal array used by the list.
- Always >= Count; automatically grows when Count exceeds Capacity.
- You can manually set Capacity using list.Capacity = newCapacity.
- list.TrimExcess() reduces the Capacity to match Count, freeing unused memory.
- Time complexity -> O(n) when resizing occurs.
*/
int capacity = list.Capacity;
list.TrimExcess();
 
/*
- Adds multiple elements at once from any IEnumerable<T> collection.
- Internal array may resize if Count + collection.Count exceeds current Capacity.
- Time complexity -> O(n + m), where n = current Count, m = number of elements being added.
*/
list.AddRange(new T[] { item1, item2, item3 });
 
/*
- Inserts multiple elements at a specified index from any IEnumerable<T> collection.
- Shifts existing elements from the insertion index onward to the right.
- Time complexity -> O(n + m), n = Count, m = number of elements inserted.
*/
list.InsertRange(1, new T[] { item4, item5 });
 
/*
- Removes all elements that satisfy a given predicate.
- Scans each element, removes matches, and shifts remaining elements.
- Time complexity -> O(n) (each element checked once).
- Returns the number of elements removed.
*/
int removedCount = list.RemoveAll(x => x.SomeCondition());
 
/*
- Searches for an element that matches a predicate.
- Returns the first matching element, or default(T) if none found.
- Time complexity -> O(n) (linear search).
- Useful for more complex conditions than simple equality.
*/
T found = list.Find(x => x.SomeCondition());
 
/*
- Returns the index of the first element that matches a predicate.
- Time complexity -> O(n) (linear scan).
- Returns -1 if no match is found.
*/
int foundIndex = list.FindIndex(x => x.SomeCondition());
 
/*
- Returns the index of the last element that matches a predicate.
- Time complexity -> O(n) (linear scan from end to start).
- Returns -1 if no match is found.
*/
int lastIndex = list.FindLastIndex(x => x.SomeCondition());
 
/*
- Performs a binary search on a sorted list for a specified element.
- Returns the index of the element if found; otherwise returns a negative number indicating insertion point (~index).
- Time complexity -> O(logn)
- List must be sorted before using BinarySearch; otherwise result is undefined.
*/
int idx = list.BinarySearch(42);
 
/*
- Sorts the list using a custom comparer or Comparison<T> delegate.
- Time complexity -> O(n log n)
- Allows defining custom sorting logic (e.g., descending order or complex object comparison).
*/
list.Sort((a, b) => b.CompareTo(a));  // descending sort example
 
/*
- Reverses a portion of the list in-place.
- Parameters:
    - index -> the starting index of the subrange to reverse.
    - count -> the number of elements to reverse starting from 'index'.
- Time complexity -> O(count) (each element in the subrange is swapped once).
- Does not allocate a new list; the original list is modified.
- Example:
    - list = {1, 2, 3, 4, 5}
    - list.Reverse(1, 3) -> list becomes {1, 4, 3, 2, 5}
*/
list.Reverse(1, 3);

/*  
- Declares and allocates a new generic dictionary mapping strings to integers.  
- 'dict' is a reference variable pointing to a Dictionary<string, int> object on the heap.  
- Internally, Dictionary uses a hash table with buckets for fast key-based lookup.  
- Count -> number of key-value pairs currently stored.  
- Time complexity for most operations (Add, Remove, Lookup) -> O(1) on average, O(n) worst case due to hash collisions.  
- Suitable for fast key-based lookups, insertions, and deletions.  
*/  
Dictionary<string, int> dict = new Dictionary<string, int>();
 
/*  
- Adds a key-value pair to the dictionary.  
- Throws ArgumentException if the key already exists.  
- Time complexity -> O(1) average, O(n) worst-case if hash collisions occur.  
*/  
dict.Add("apple", 10);  
dict.Add("banana", 20);
 
/*
- Adds a key-value pair only if the key does not already exist.
- Returns true if added, false if key exists.
- Time complexity -> O(1) average.
*/
bool added = dict.TryAdd("pear", 25);
 
/*  
- Updates the value for an existing key, or adds the key if it does not exist.  
- Uses indexer syntax.  
- Time complexity -> O(1) average, O(n) worst-case.  
*/  
dict["apple"] = 15; // updates existing key  
dict["cherry"] = 30; // adds new key
 
/*
- Accesses the value associated with a given key using the indexer.
- Time complexity -> O(1) average (hash lookup), O(n) worst-case if many hash collisions occur.
- Throws KeyNotFoundException if the key does not exist.
*/
int val = dict["a"];
 
/*  
- Checks if a key exists in the dictionary.  
- Time complexity -> O(1) average, O(n) worst-case.  
- Returns true if key is found, false otherwise.  
*/  
bool hasKey = dict.ContainsKey("banana");  
  
/*  
- Checks if a value exists in the dictionary.  
- Time complexity -> O(n) (must scan all values, as hash table is keyed by key).  
- Returns true if value is found, false otherwise.  
*/  
bool hasValue = dict.ContainsValue(20);
 
/*  
- Retrieves the value for a key safely using TryGetValue.  
- Returns true if key exists and outputs the value; false otherwise.  
- Time complexity -> O(1) average.  
*/  
if (dict.TryGetValue("apple", out int value))  
{  
	Console.WriteLine(value);  
}
 
/*  
- Removes a key-value pair by key.  
- Returns true if the key was found and removed; false otherwise.  
- Time complexity -> O(1) average, O(n) worst-case.  
*/  
bool removed = dict.Remove("banana");
 
/*  
- Gets the number of key-value pairs currently in the dictionary.  
- Time complexity -> O(1)  
*/  
int count = dict.Count;  
  
/*  
- Clears all key-value pairs from the dictionary.  
- Time complexity -> O(n) (each element reference is cleared).  
- Dictionary.Count becomes 0.  
- Internal capacity remains unchanged; no memory is released.  
*/  
dict.Clear();
 
/*  
- Iterates through all key-value pairs in the dictionary.  
- Time complexity -> O(n) (each pair visited once).  
- KeyValuePair<TKey, TValue> is read-only: cannot modify the key, but value can be modified via reference type if applicable.  
*/  
foreach (var kvp in dict)  
{  
	Console.WriteLine($"{kvp.Key} -> {kvp.Value}");  
}
  
/*  
- Iterates through all keys.  
- Time complexity -> O(n)  
- Keys collection is a snapshot view; modifying dictionary while iterating throws InvalidOperationException.  
*/  
foreach (string key in dict.Keys)  
{  
	Console.WriteLine(key);  
}  
  
/*  
- Iterates through all values.  
- Time complexity -> O(n)  
- Values collection is a snapshot view; modifying dictionary while iterating throws InvalidOperationException.  
*/  
foreach (int val in dict.Values)  
{  
	Console.WriteLine(val);  
}

/*
- Declares and allocates a new generic HashSet of integers.
- 'set' is a reference variable pointing to a HashSet<int> object on the heap.
- Internally, HashSet<T> uses a hash table for fast insertion, lookup, and deletion.
- Maintains unique elements; duplicates are ignored.
- Count -> number of elements currently in the set.
- Time complexity for Add, Remove, Contains -> O(1) average, O(n) worst-case due to hash collisions.
- Suitable for fast membership checks, uniqueness enforcement, and set operations.
*/
HashSet<int> set = new HashSet<int>();
 
/*
- Adds an element to the set.
- Returns true if the element was added; false if it already existed.
- Time complexity -> O(1) average, O(n) worst-case.
*/
set.Add(10);
set.Add(20);
 
// Add duplicate (ignored)
set.Add(10); // returns false
 
/*
- Checks if an element exists in the set.
- Time complexity -> O(1) average.
- Returns true if the element exists; false otherwise.
*/
bool exists = set.Contains(20);
 
/*
- Removes an element from the set.
- Time complexity -> O(1) average.
- Returns true if the element was found and removed; false otherwise.
*/
bool removed = set.Remove(20);
 
/*
- Gets the number of elements currently in the set.
- Time complexity -> O(1)
*/
int count = set.Count;
 
/*
- Clears all elements from the set.
- Time complexity -> O(n)
- Set.Count becomes 0.
*/
set.Clear();
 
/*
- Iterates through all elements in the set.
- Time complexity -> O(n) (each element accessed once).
- Iteration variable 'x' is read-only for value types:
    - Modifying 'x' does not change the element in the set.
*/
foreach (int x in set)
{
    Console.WriteLine(x);
}
 
/*
- Set operations with another HashSet<int> (other):
    - UnionWith -> set = set ∪ other
    - IntersectWith -> set = set ∩ other
    - ExceptWith -> set = set \ other
    - SymmetricExceptWith -> set = (set ∪ other) \ (set ∩ other)
- Time complexity -> O(n + m), where n = set.Count, m = other.Count
*/
HashSet<int> other = new HashSet<int> { 10, 30 };
set.UnionWith(other); // Adds all elements from 'other' to 'set' (union)
set.IntersectWith(other); // Keeps only elements present in both 'set' and 'other' (intersection)
set.ExceptWith(other); // Removes all elements from 'set' that are in 'other' (difference)
set.SymmetricExceptWith(other); // Keeps elements in either 'set' or 'other' but not in both (symmetric difference)
 
// Checks if set has any elements in common with another set, O(n)
bool hasOverlap = set.Overlaps(other);
 
/*
- Checks subset/superset relationships.
- Time complexity -> O(n)
*/
bool isSubset = set.IsSubsetOf(other);
bool isSuperset = set.IsSupersetOf(other);
 
/*  
- Proper subset/superset means strict relationship:  
- Proper subset -> set is a subset of 'other' but not equal to 'other'  
- Proper superset -> set contains all elements of 'other' but is not equal to 'other'  
*/
bool isProperSubset = set.IsProperSubsetOf(other);
bool isProperSuperset = set.IsProperSupersetOf(other);
 
/*
- Returns true if 'set' and 'other' contain exactly the same elements
*/
bool isEqual = set.SetEquals(other);
 
/*
- Removes all elements that satisfy a predicate.
- Time complexity -> O(n)
- Returns the number of elements removed.
*/
int removedCount = set.RemoveWhere(x => x % 2 == 0);
 
/*
- Copies the elements of the set into an array.
- Time complexity -> O(n)
- Useful when fixed-size contiguous memory is needed.
*/
int[] arr = new int[set.Count];
set.CopyTo(arr);

/*
- Declares and allocates a new generic Queue of integers.
- 'queue' is a reference variable pointing to a Queue<int> object on the heap.
- Internally, Queue<T> uses a circular buffer array for fast enqueue/dequeue.
- Count -> number of elements currently in the queue.
- Time complexity:
    - Enqueue: O(1) amortized
    - Dequeue: O(1)
    - Peek: O(1)
- Suitable for FIFO (first-in, first-out) operations.
*/
Queue<int> queue = new Queue<int>();
 
/*
- Adds an element to the back of the queue.
- Internal array may resize if Count == Capacity:
    - Resizing allocates a new array and copies existing elements.
- Time complexity -> O(1) amortized
*/
queue.Enqueue(1);
queue.Enqueue(2);
 
/*
- Returns the element at the front of the queue without removing it.
- Time complexity -> O(1)
- Throws InvalidOperationException if the queue is empty.
*/
int front = queue.Peek();
 
/*
- Removes and returns the element at the front of the queue.
- Time complexity -> O(1)
- Throws InvalidOperationException if the queue is empty.
*/
int removed = queue.Dequeue();
 
/*
- Checks if a specific element exists in the queue.
- Performs linear search from front to back.
- Time complexity -> O(n)
*/
bool exists = queue.Contains(2);
 
/*
- Gets the number of elements currently in the queue.
- Time complexity -> O(1)
*/
int count = queue.Count;
 
/*
- Removes all elements from the queue.
- Time complexity -> O(n)
- Queue.Count becomes 0.
- Underlying array is retained for reuse.
*/
queue.Clear();
 
/*
- Converts the queue into a new array.
- Time complexity -> O(n)
- Original queue remains unmodified.
*/
int[] arr = queue.ToArray();
 
/*
- Copies elements of the queue into an existing array starting at a specified index.
- Time complexity -> O(n)
- Useful for array-based algorithms or interop.
*/
int[] arr2 = new int[queue.Count];
queue.CopyTo(arr2, 0);
 
/*
- Iterates through all elements in the queue from front to back.
- Time complexity -> O(n) (each element accessed once)
- Iteration variable 'x' is read-only:
    - Modifying 'x' does not change the elements in the queue.
*/
foreach (int x in queue)
{
    Console.WriteLine(x);
}

/*
- Declares and allocates a new generic Stack of integers.
- 'stack' is a reference variable pointing to a Stack<int> object on the heap.
- Internally, Stack<T> uses a resizable array for fast push/pop operations.
- Count -> number of elements currently in the stack.
- Time complexity:
    - Push: O(1) amortized
    - Pop: O(1)
    - Peek: O(1)
- Suitable for LIFO (last-in, first-out) operations.
*/
Stack<int> stack = new Stack<int>();
 
/*
- Adds an element to the top of the stack.
- Internal array may resize if Count == Capacity:
    - Resizing allocates a new array and copies existing elements.
- Time complexity -> O(1) amortized
*/
stack.Push(10);
stack.Push(20);
 
/*
- Returns the element at the top of the stack without removing it.
- Time complexity -> O(1)
- Throws InvalidOperationException if the stack is empty.
*/
int top = stack.Peek();
 
/*
- Removes and returns the element at the top of the stack.
- Time complexity -> O(1)
- Throws InvalidOperationException if the stack is empty.
*/
int removed = stack.Pop();
 
/*
- Checks if a specific element exists in the stack.
- Performs linear search from top to bottom.
- Time complexity -> O(n)
*/
bool exists = stack.Contains(10);
 
/*
- Gets the number of elements currently in the stack.
- Time complexity -> O(1)
*/
int count = stack.Count;
 
/*
- Removes all elements from the stack.
- Time complexity -> O(n)
- Stack.Count becomes 0.
- Underlying array is retained for reuse.
*/
stack.Clear();
 
/*
- Converts the stack into a new array.
- Time complexity -> O(n)
- The array is in LIFO order (top element becomes first in array).
- Original stack remains unmodified.
*/
int[] arr = stack.ToArray();
 
/*
- Iterates through all elements in the stack from top to bottom.
- Time complexity -> O(n) (each element accessed once)
- Iteration variable 'x' is read-only:
    - Modifying 'x' does not change the elements in the stack.
*/
foreach (int x in stack)
{
    Console.WriteLine(x);
}

/*
- Declares and allocates a new generic PriorityQueue of strings with integer priorities.
- 'pq' is a reference variable pointing to a PriorityQueue<string, int> object on the heap.
- Internally, PriorityQueue<TElement, TPriority> uses a binary heap for fast priority-based operations.
- Lower values of TPriority are dequeued first (min-priority queue by default).
- Count -> number of elements currently in the queue.
- Time complexity:
    - Enqueue: O(log n)
    - Dequeue: O(log n)
    - Peek: O(1)
- Suitable for scenarios requiring retrieval of the highest/lowest priority element quickly.
*/
PriorityQueue<string, int> pq = new PriorityQueue<string, int>();
 
/*
- Adds an element with an associated priority.
- Time complexity -> O(log n) due to heap insertion.
*/
pq.Enqueue("taskA", 3);
pq.Enqueue("taskB", 1);
 
/*
- Returns the element with the minimum priority without removing it.
- Time complexity -> O(1)
- Throws InvalidOperationException if the queue is empty.
*/
string next = pq.Peek();
 
/*
- Removes and returns the element with the minimum priority.
- Time complexity -> O(log n) due to heap reordering.
- Throws InvalidOperationException if the queue is empty.
*/
string removed = pq.Dequeue();
 
/*
- Safely attempts to remove the element with the minimum priority.
- Returns true if an element was dequeued; false if the queue is empty.
- Outputs the dequeued element and its priority.
- Time complexity -> O(log n)
*/
bool success = pq.TryDequeue(out string element, out int priority);
 
/*
- Gets the number of elements currently in the priority queue.
- Time complexity -> O(1)
*/
int count = pq.Count;
 
/*
- Iterates through elements in the queue without any guaranteed order (heap order).
- Time complexity -> O(n)
- Useful for inspection/debugging only; does not dequeue elements.
*/
foreach (var item in pq.UnorderedItems)
{
    Console.WriteLine($"{item.Element}:{item.Priority}");
}
 
/*
- Removes all elements from the priority queue.
- Time complexity -> O(n)
- PriorityQueue.Count becomes 0.
- Underlying heap memory is retained for reuse.
*/
pq.Clear();

/*
- Declares and allocates a new generic SortedSet of integers.
- 'set' is a reference variable pointing to a SortedSet<int> object on the heap.
- Internally, SortedSet<T> uses a self-balancing binary search tree (Red-Black Tree).
- Elements are always stored in sorted order (ascending by default).
- Count -> number of elements currently in the set.
- Time complexity:
    - Add, Remove, Contains -> O(log n)
    - Min, Max -> O(log n)
- Suitable for maintaining a collection of unique elements in sorted order.
*/
SortedSet<int> set = new SortedSet<int>();
 
/*
- Adds an element to the set while maintaining sorted order.
- Returns true if the element was added; false if it already existed.
- Time complexity -> O(log n)
*/
set.Add(30);
set.Add(10);
set.Add(20);
 
/*
- Checks if an element exists in the set.
- Time complexity -> O(log n)
- Returns true if the element exists; false otherwise.
*/
bool exists = set.Contains(20);
 
/*
- Removes an element from the set.
- Time complexity -> O(log n)
- Returns true if the element was found and removed; false otherwise.
*/
bool removed = set.Remove(10);
 
/*
- Gets the minimum element in the set.
- Time complexity -> O(log n)
- Throws InvalidOperationException if the set is empty.
*/
int min = set.Min;
 
/*
- Gets the maximum element in the set.
- Time complexity -> O(log n)
- Throws InvalidOperationException if the set is empty.
*/
int max = set.Max;
 
/*
- Returns a view of the set containing elements in the specified range [lower, upper].
- The view is live; changes in the view affect the original set.
- Time complexity -> O(log n) for view creation; operations on the view are O(log n)
*/
var range = set.GetViewBetween(15, 35);
 
/*
- Iterates through all elements in sorted order.
- Time complexity -> O(n)
- Iteration variable 'x' is read-only for value types; modifying 'x' does not affect the set.
*/
foreach (int x in set)
{
    Console.WriteLine(x);
}
 
/*
- Removes all elements from the set.
- Time complexity -> O(n)
- SortedSet.Count becomes 0.
*/
set.Clear();
 
/*
- Retrieves the number of elements currently in the set.
- Time complexity -> O(1)
*/
int count = set.Count;

 
/*
- Declares and allocates a new generic SortedDictionary mapping integers to strings.
- 'dict' is a reference variable pointing to a SortedDictionary<int, string> object on the heap.
- Internally, SortedDictionary<TKey, TValue> uses a self-balancing binary search tree (Red-Black Tree).
- Keys are always stored in sorted order (ascending by default).
- Count -> number of key-value pairs currently stored.
- Time complexity:
    - Add, Remove, ContainsKey, indexer access -> O(log n)
- Suitable for maintaining key-value pairs with sorted keys and fast lookup by key.
*/
SortedDictionary<int, string> dict = new SortedDictionary<int, string>();
 
/*
- Adds a key-value pair to the dictionary.
- Throws ArgumentException if the key already exists.
- Time complexity -> O(log n)
*/
dict.Add(2, "b");
dict.Add(1, "a");
 
/*
- Adds or updates a key-value pair using the indexer.
- Updates the value if key exists, or adds a new key if it does not.
- Time complexity -> O(log n)
*/
dict[3] = "c";
 
/*
- Accesses the value associated with a given key using the indexer.
- Time complexity -> O(log n)
- Throws KeyNotFoundException if the key does not exist.
*/
string value = dict[1];
 
/*
- Retrieves the value for a key safely using TryGetValue.
- Returns true if key exists and outputs the value; false otherwise.
- Time complexity -> O(log n)
*/
bool found = dict.TryGetValue(2, out string val);
 
/*
- Checks if a key exists in the dictionary.
- Time complexity -> O(log n)
*/
bool exists = dict.ContainsKey(3);
 
/*
- Removes a key-value pair by key.
- Returns true if the key was found and removed; false otherwise.
- Time complexity -> O(log n)
*/
bool removed = dict.Remove(2);
 
/*
- Gets the smallest key in the dictionary.
- Time complexity -> O(log n)
- Throws InvalidOperationException if dictionary is empty.
*/
int minKey = dict.Keys.Min;
 
/*
- Gets the largest key in the dictionary.
- Time complexity -> O(log n)
- Throws InvalidOperationException if dictionary is empty.
*/
int maxKey = dict.Keys.Max;
 
/*
- Retrieves the number of key-value pairs currently in the dictionary.
- Time complexity -> O(1)
*/
int count = dict.Count;
 
/*
- Iterates through all key-value pairs in key-sorted order.
- Time complexity -> O(n)
- KeyValuePair<TKey, TValue> is read-only: cannot modify the key, but value can be modified via reference type if applicable.
*/
foreach (KeyValuePair<int, string> kv in dict)
{
    Console.WriteLine($"{kv.Key}:{kv.Value}");
}
 
/*
- Iterates through all keys in ascending order.
- Time complexity -> O(n)
*/
foreach (int key in dict.Keys)
{
    Console.WriteLine(key);
}
 
/*
- Iterates through all values in key order.
- Time complexity -> O(n)
*/
foreach (string val in dict.Values)
{
    Console.WriteLine(val);
}
 
/*
- Removes all key-value pairs from the dictionary.
- Time complexity -> O(n)
- SortedDictionary.Count becomes 0.
*/
dict.Clear();