Build Data Structures in C#
Build a linked list, stack, and queue from scratch in C#.
Introduction
30 Second Summary
Every app you use relies on data structures to organize information. Your browser's back button, your music queue, your undo shortcut. These all depend on structures like stacks, queues, and linked lists working behind the scenes.
In this project, you will build a complete data structures library from scratch in C#. You will implement a linked list, a stack, and a queue using nothing but nodes and references, then wire them all into a working Task Manager console application.
What You'll Build
A generic data structures library in C# with a singly linked list, stack, and queue, all built from scratch without using any built-in collections.
By the end of this project, you'll have:
- A fully functional generic Singly Linked List built from scratch with node-based storage and head/tail management.
- A custom Stack (LIFO) and Queue (FIFO) implemented on top of your linked list with no built-in collections used.
- A working Task Manager console app that uses all three custom data structures together to add, process, and undo tasks.
- Secret Mission: Implement a Hash Table from scratch using separate chaining and give your Task Manager O(1) lookup by task name.
Are there any prerequisites?
You should be comfortable with basic C# syntax including variables, classes, methods, and loops. No prior data structures knowledge is required.
Before We Start
You are building a data structures library from scratch in C#. Instead of relying on .NET's built-in collections like List<T>, Stack<T>, and Queue<T>, you will implement each one yourself using nodes and references.
This teaches you what actually happens inside these structures. That knowledge is essential for technical interviews, performance-sensitive code, and becoming a stronger developer overall.
In this step, get ready to:
- Install the .NET 10 SDK and verify it works.
- Install Visual Studio Code with the C# Dev Kit extension.
- Create and run your first .NET console project.
Install the .NET 10 SDK
The .NET SDK is the toolkit that lets you build and run C# applications from your terminal. This project requires .NET 10, the current Long Term Support release.
- Check if you already have .NET 10 installed by running this command in your terminal:
dotnet --version
✔️ I see version 10.0 or higher
You're all set. Your .NET SDK is ready to go.
ⓧ I see an older version
.NET supports side-by-side installations, so you can install .NET 10 alongside your existing version without breaking anything.
- Download the .NET 10 SDK installer from the official page:
- https://dotnet.microsoft.com/en-us/download/dotnet/10.0
- Run the installer and follow the prompts.
- Close and reopen your terminal, then run dotnet --version again to confirm it shows 10.0.x.
ⓧ Command not found
You don't have .NET installed yet. Let's fix that.
- Download the .NET 10 SDK installer from the official page:
- https://dotnet.microsoft.com/en-us/download/dotnet/10.0
- Run the installer and follow the prompts.
- Close and reopen your terminal after installation completes.
- Run dotnet --version to confirm it shows 10.0.x.
Still not seeing a version number?
Make sure you fully closed and reopened your terminal after installing. The new dotnet command won't be available in terminals that were already open.
On macOS, if your terminal still can't find dotnet, try opening a new Terminal window from your Applications folder.
help me troubleshoot why dotnet --version is not working after installing the .NET 10 SDK
Install Visual Studio Code with C# Dev Kit
Visual Studio Code is a lightweight code editor that works great for C# development. The C# Dev Kit extension adds IntelliSense, debugging, and project management features.
- If you don't already have VS Code, download and install it from:
- https://code.visualstudio.com
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
- Type C# Dev Kit in the search bar at the top of the panel.
- Click Install on the extension published by Microsoft.
What does C# Dev Kit include?
Installing C# Dev Kit automatically installs the base C# language extension as a dependency. You get IntelliSense, code navigation, refactoring, and a Solution Explorer all in one install.
Can't find the extension?
Make sure you're searching for C# Dev Kit (with a space between C# and Dev). The publisher should show as Microsoft.
help me find and install the C# Dev Kit extension in Visual Studio Code
Create the project
Now you'll scaffold a new C# console application using the .NET CLI. This gives you a ready-to-run project with a single Program.cs file.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir DataStructures
cd DataStructures
- Confirm the folder exists by running:
ls
You should see the DataStructures folder listed if you run ls from the Desktop. Since you've already moved into it, the folder will appear empty for now.
- Scaffold a new .NET console project by running:
dotnet new console -n DataStructures
What does this command do?
dotnet new console creates a new C# console application from a template. The -n DataStructures flag names the project and creates a subfolder called DataStructures containing your .csproj file and Program.cs.
- Move into the new project folder by running:
cd DataStructures
- Verify your project works by running:
dotnet run
You should see Hello, World! printed in your terminal. This confirms the .NET SDK and your project are working correctly.
Don't see Hello, World! output?
Make sure you ran cd DataStructures after the dotnet new console command. You need to be inside the inner DataStructures folder (the one with Program.cs) for dotnet run to work.
If you see a build error, confirm your .NET version is 10.0.x by running dotnet --version again.
help me troubleshoot why dotnet run is not printing Hello World in my DataStructures project
- Open the project folder in VS Code by running:
code .
If the code command doesn't work
Open VS Code manually, then click File in the top menu bar and select Open Folder. Navigate to Desktop/DataStructures/DataStructures and click Open.
In VS Code's Explorer sidebar, you should see Program.cs and DataStructures.csproj. Click on Program.cs to open it.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file and that dotnet run still prints Hello, World! from the terminal.
ⓧ I'd like to double check the full code
Your Program.cs should look like this:
// Default Hello World entry point generated by dotnet new console
Console.WriteLine("Hello, World!");
Your development environment is ready. Next up, you'll build the foundational Node class and start constructing your first data structure from scratch.
Set Up Your .NET Project
Before you can build data structures from scratch, you need the tools that compile and run C# code.
In this step, you will install the .NET 10 SDK, set up Visual Studio Code with the C# Dev Kit extension, and scaffold a brand new console project to confirm everything works.
In this step, get ready to:
- Install and verify the .NET 10 SDK.
- Install Visual Studio Code with the C# Dev Kit extension.
- Create and run your first console project.
Install the .NET 10 SDK
The .NET SDK is the toolkit that compiles and runs your C# code. This project requires version 10.0 or higher.
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check your .NET version by running this command:
dotnet --version
✔️ I see version 10.0 or higher
Your .NET SDK is ready. Move on to the next substep.
ⓧ I see an older version
You have .NET installed but it is an older version. You need 10.0 or higher for this project.
- Open your browser and navigate to the official .NET 10 download page:
- https://dotnet.microsoft.com/en-us/download/dotnet/10.0
- Under the Build apps - SDK section, find the Installers column for your operating system and download the installer for your CPU architecture (x64 is most common).
- Run the installer and follow the prompts.
- Close and reopen your terminal, then run dotnet --version again to confirm it now shows 10.0.x.
Why close and reopen the terminal?
Your terminal caches the PATH variable when it opens. After installing new software, reopening the terminal picks up the updated PATH so it can find the new dotnet binary.
ⓧ Command not found
The .NET SDK is not installed on your machine. Follow these steps to install it.
- Open your browser and navigate to the official .NET 10 download page:
- https://dotnet.microsoft.com/en-us/download/dotnet/10.0
- Under the Build apps - SDK section, find the Installers column for your operating system and download the installer for your CPU architecture (x64 is most common).
- Run the downloaded installer and follow the prompts to complete installation.
- Close and reopen your terminal, then run dotnet --version to confirm it shows 10.0.x.
Still seeing command not found?
Make sure you downloaded the SDK (not the Runtime). The SDK includes the dotnet CLI. If you installed via a manual binary, you may need to add the install directory to your system PATH.
Help me troubleshoot why dotnet --version is not working after installing the .NET 10 SDK.
What is .NET?
.NET is an open-source platform by Microsoft for building applications. The SDK (Software Development Kit) includes the compiler, runtime, and CLI tools you need to write and execute C# programs.
Install Visual Studio Code with C# Dev Kit
Visual Studio Code is a lightweight code editor that works on macOS, Windows, and Linux. The C# Dev Kit extension adds IntelliSense, debugging, and project management for C# files.
- If you do not already have VS Code, download it from https://code.visualstudio.com and run the installer.
- Open VS Code. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
- Type C# Dev Kit in the search bar at the top of the panel.
- Click Install on the C# Dev Kit extension published by Microsoft.
What does C# Dev Kit give you?
C# Dev Kit adds IntelliSense (autocomplete suggestions), inline error highlighting, and a Solution Explorer panel to VS Code. Without it, VS Code would treat your .cs files as plain text.
Create the project
Now you will scaffold a new C# console application using the dotnet CLI. This generates a project file and a starter Program.cs you can run immediately.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and scaffold the console app by running these commands:
mkdir DataStructures
cd DataStructures
dotnet new console -n DataStructures
cd DataStructures
What does this do?
The mkdir DataStructures command creates a folder on your Desktop. Then dotnet new console -n DataStructures scaffolds a new C# console project inside a subfolder also named DataStructures. This generates a DataStructures.csproj file and a Program.cs with a single Hello World statement.
- Open this folder in VS Code by running:
code .
If the code command is not found
In VS Code, press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) to open the Command Palette. Type Shell Command: Install 'code' command in PATH and select it. Then retry code . in your terminal.
- Verify the project works by running:
dotnet run
You should see Hello, World! printed in your terminal.
Not seeing Hello, World! output?
Make sure you are inside the inner DataStructures folder (the one containing DataStructures.csproj). Run ls (macOS/Linux) or dir (Windows) to check. You should see Program.cs and DataStructures.csproj listed.
Help me figure out why dotnet run is not printing Hello World in my DataStructures project.
Your environment is ready. Next up, you will build the fundamental building block of every data structure: the Node.
Build the Node and Singly Linked List
Your project compiles and prints "Hello, World!" which means the foundation is solid. Now it's time to build the core data structure that powers everything else in this project.
Every linked list is made of nodes. Each node holds a piece of data and a reference (pointer) to the next node. You will create a generic Node<T> class first, then build a full SinglyLinkedList<T> on top of it.
In this step, get ready to:
- Create a generic Node class as the universal building block.
- Build a SinglyLinkedList class with insertion, removal, and search methods.
- Test your linked list by adding, finding, and removing data.
Create the Node class
A node is the smallest unit of a linked list. It stores one piece of data and one reference pointing to the next node in the chain.
- In the VS Code Explorer sidebar, hover over the project folder name and click the New File icon (a file with a plus sign).
- Type Node.cs and press Enter.
- Paste the following code into Node.cs:
namespace DataStructures;
public class Node<T>
{
public T Data { get; set; }
public Node<T>? Next { get; set; }
public Node(T data)
{
Data = data;
Next = null;
}
}
What does this code do?
- Node<T> is a generic class. The T means you can store any type of data (integers, strings, custom objects) without rewriting the class.
- Data holds the actual value this node carries.
- Next is a nullable reference (Node<T>?) that points to the next node. When Next is null, you have reached the end of the list.
- Save Node.cs.
- Verify the file compiles by running this command in your terminal:
dotnet build
You should see "Build succeeded" in the output.
Seeing a build error?
- Check that the namespace line says namespace DataStructures; with a semicolon at the end. A missing semicolon causes a syntax error.
- Make sure the file is saved inside the inner DataStructures folder (the one containing Program.cs), not the outer parent folder.
My Node.cs won't compile. Can you help me debug the error message?
✔️ Awesome, I've got everything!
Great. Make sure the file is saved before moving on.
ⓧ I'd like to double check the full code
namespace DataStructures;
public class Node<T>
{
public T Data { get; set; }
public Node<T>? Next { get; set; }
public Node(T data)
{
Data = data;
Next = null;
}
}
Create the SinglyLinkedList class
A singly linked list is a chain of nodes where each node points to the next one. Unlike an array, a linked list does not store elements in contiguous memory. This means insertions and removals at the head are O(1), with no shifting required.
- Create a new file called SinglyLinkedList.cs in the same folder as Node.cs.
- Paste the following code to set up the class structure with its insertion methods:
namespace DataStructures;
public class SinglyLinkedList<T>
{
private Node<T>? _head;
private Node<T>? _tail;
public int Count { get; private set; }
public SinglyLinkedList()
{
_head = null;
_tail = null;
Count = 0;
}
// Insert a new node at the beginning of the list
public void AddFirst(T data)
{
Node<T> newNode = new Node<T>(data);
newNode.Next = _head;
_head = newNode;
if (_tail is null)
{
_tail = newNode;
}
Count++;
}
// Insert a new node at the end of the list
public void AddLast(T data)
{
Node<T> newNode = new Node<T>(data);
if (_tail is null)
{
_head = newNode;
_tail = newNode;
}
else
{
_tail.Next = newNode;
_tail = newNode;
}
Count++;
}
}
What does this code do?
- _head tracks the first node in the list. _tail tracks the last node. Having both lets you insert at either end in O(1) time without traversing.
- AddFirst creates a new node, points it at the current head, then updates _head to the new node. If the list was empty, _tail also points to this new node.
- AddLast appends a new node after _tail, then moves _tail forward. Without the tail reference, this would require O(n) traversal every time.
- Save SinglyLinkedList.cs. You won't see output yet. The next chunk adds retrieval methods.
- Still in SinglyLinkedList.cs, add these methods inside the class, just above the closing } brace:
// Remove and return the first node's data
public T RemoveFirst()
{
if (_head is null)
{
throw new InvalidOperationException("The list is empty.");
}
T data = _head.Data;
_head = _head.Next;
Count--;
if (_head is null)
{
_tail = null;
}
return data;
}
// Look at the first node's data without removing it
public T PeekFirst()
{
if (_head is null)
{
throw new InvalidOperationException("The list is empty.");
}
return _head.Data;
}
What does this code do?
- RemoveFirst saves the head's data, moves _head forward to the next node, and returns the saved data. If the list becomes empty, it resets _tail to null.
- PeekFirst returns the head's data without modifying the list. Both methods throw an InvalidOperationException if the list is empty.
- Both operations are O(1) because they only touch the head node.
- Save the file. The next chunk adds search and traversal methods.
- Below the PeekFirst method you just added, paste these final methods (still inside the class):
// Check if a value exists anywhere in the list
public bool Contains(T data)
{
Node<T>? current = _head;
while (current is not null)
{
if (EqualityComparer<T>.Default.Equals(current.Data, data))
{
return true;
}
current = current.Next;
}
return false;
}
// Find and remove a specific value from the list
public bool Remove(T data)
{
if (_head is null)
{
return false;
}
if (EqualityComparer<T>.Default.Equals(_head.Data, data))
{
_head = _head.Next;
Count--;
if (_head is null)
{
_tail = null;
}
return true;
}
Node<T>? current = _head;
while (current.Next is not null)
{
if (EqualityComparer<T>.Default.Equals(current.Next.Data, data))
{
if (current.Next == _tail)
{
_tail = current;
}
current.Next = current.Next.Next;
Count--;
return true;
}
current = current.Next;
}
return false;
}
// Print all node values from head to tail
public void Print()
{
Node<T>? current = _head;
while (current is not null)
{
Console.Write($"{current.Data}");
if (current.Next is not null)
{
Console.Write(" -> ");
}
current = current.Next;
}
Console.WriteLine(" -> null");
}
What does this code do?
- Contains walks the chain from head to tail, comparing each node's data. It uses EqualityComparer<T>.Default because generic types cannot use == directly.
- Remove handles three cases: empty list (return false), removing the head (special case), and removing from the middle or tail (traverse with a one-behind pointer to "unlink" the target node).
- Print traverses the list and displays each value separated by -> arrows, ending with null to show where the chain ends.
Why EqualityComparer<T>.Default instead of ==?
In C# generics, the == operator is not available for unconstrained type parameters. EqualityComparer<T>.Default handles value types, reference types, and null correctly. It is the standard .NET approach for comparing generic values.
- Save SinglyLinkedList.cs.
- Confirm the full class compiles by running:
dotnet build
You should see "Build succeeded" in the output.
Build failed?
- Check that you have exactly one opening { after the class declaration and one closing } at the very end of the file. Each method chunk adds code inside the class, not after it.
- If you see an error about Node<T> not being found, make sure Node.cs exists in the same folder and uses the same namespace DataStructures; line.
- If you see an error mentioning EqualityComparer, make sure the full type name is EqualityComparer<T>.Default.Equals with the correct angle brackets.
Help me fix my SinglyLinkedList.cs build errors.
✔️ Awesome, I've got everything!
Great. Double check you have saved the file before continuing.
ⓧ I'd like to double check the full code
namespace DataStructures;
public class SinglyLinkedList<T>
{
private Node<T>? _head;
private Node<T>? _tail;
public int Count { get; private set; }
public SinglyLinkedList()
{
_head = null;
_tail = null;
Count = 0;
}
public void AddFirst(T data)
{
Node<T> newNode = new Node<T>(data);
newNode.Next = _head;
_head = newNode;
if (_tail is null)
{
_tail = newNode;
}
Count++;
}
public void AddLast(T data)
{
Node<T> newNode = new Node<T>(data);
if (_tail is null)
{
_head = newNode;
_tail = newNode;
}
else
{
_tail.Next = newNode;
_tail = newNode;
}
Count++;
}
public T RemoveFirst()
{
if (_head is null)
{
throw new InvalidOperationException("The list is empty.");
}
T data = _head.Data;
_head = _head.Next;
Count--;
if (_head is null)
{
_tail = null;
}
return data;
}
public T PeekFirst()
{
if (_head is null)
{
throw new InvalidOperationException("The list is empty.");
}
return _head.Data;
}
public bool Contains(T data)
{
Node<T>? current = _head;
while (current is not null)
{
if (EqualityComparer<T>.Default.Equals(current.Data, data))
{
return true;
}
current = current.Next;
}
return false;
}
public bool Remove(T data)
{
if (_head is null)
{
return false;
}
if (EqualityComparer<T>.Default.Equals(_head.Data, data))
{
_head = _head.Next;
Count--;
if (_head is null)
{
_tail = null;
}
return true;
}
Node<T>? current = _head;
while (current.Next is not null)
{
if (EqualityComparer<T>.Default.Equals(current.Next.Data, data))
{
if (current.Next == _tail)
{
_tail = current;
}
current.Next = current.Next.Next;
Count--;
return true;
}
current = current.Next;
}
return false;
}
public void Print()
{
Node<T>? current = _head;
while (current is not null)
{
Console.Write($"{current.Data}");
if (current.Next is not null)
{
Console.Write(" -> ");
}
current = current.Next;
}
Console.WriteLine(" -> null");
}
}
Test your linked list
Your linked list class is complete. Time to replace the default "Hello, World!" code with a test that exercises insertion, searching, and removal.
- Open Program.cs in VS Code.
- Select all the existing code (Cmd+A on macOS or Ctrl+A on Windows) and replace it with:
using DataStructures;
// Test SinglyLinkedList
SinglyLinkedList<int> list = new SinglyLinkedList<int>();
list.AddFirst(1);
list.AddFirst(2);
list.AddLast(3);
list.Print();
Console.WriteLine(list.Contains(3));
Console.WriteLine(list.RemoveFirst());
list.Print();
What does this test do?
- AddFirst(1) puts 1 at the head. AddFirst(2) then pushes 2 before it, making the order: 2 -> 1.
- AddLast(3) appends 3 at the end, making the order: 2 -> 1 -> 3.
- Contains(3) searches the list and returns True.
- RemoveFirst() removes and prints 2 (the head), leaving 1 -> 3.
- Save Program.cs.
- Run the project by executing:
dotnet run
You should see this output in your terminal:
2 -> 1 -> 3 -> null
True
2
1 -> 3 -> null
Output doesn't match?
- If you see "Hello, World!" instead, make sure you replaced ALL of the original Program.cs content (select all before pasting).
- If you see a build error about SinglyLinkedList not being found, check that the first line of Program.cs says using DataStructures; and that SinglyLinkedList.cs uses the same namespace.
- If the arrows or values are in the wrong order, double check your AddFirst and AddLast logic against the full file in the double-check tab above.
My linked list output doesn't match what's expected. Help me debug.
✔️ Awesome, I've got everything!
Great. Make sure the file is saved before moving on.
ⓧ I'd like to double check the full code
using DataStructures;
// Test SinglyLinkedList
SinglyLinkedList<int> list = new SinglyLinkedList<int>();
list.AddFirst(1);
list.AddFirst(2);
list.AddLast(3);
list.Print();
Console.WriteLine(list.Contains(3));
Console.WriteLine(list.RemoveFirst());
list.Print();
Your linked list is fully operational. You can insert at both ends, remove from the head, search for values, and display the chain. Next up, you will build a Stack that uses this linked list as its engine.
Build the Stack
Your linked list is working and storing data dynamically with nodes and references. Now you have a powerful foundation to build higher-level data structures on top of.
In this step, you will implement a Stack using your linked list as the underlying storage. A stack enforces a strict access pattern: the last item you add is always the first one you remove. By building it on top of your existing list, you will see how simple data structures compose into more powerful ones.
In this step, get ready to:
- Create the MyStack<T> class with Push, Pop, and Peek methods.
- Understand LIFO ordering and why it maps perfectly to linked list head operations.
- Test your stack and verify items come out in reverse order.
Create the MyStack class
A stack is a restricted data structure. You can only add to the top and remove from the top. This makes it ideal for things like undo history, backtracking algorithms, and expression parsing.
Instead of building storage from scratch, your stack will wrap the SinglyLinkedList<T> you already built. It delegates all the heavy lifting to the list while exposing only the three operations a stack should have.
- Create a new file called MyStack.cs in the same DataStructures project folder (where Node.cs and SinglyLinkedList.cs live).
- Add the following code to MyStack.cs:
namespace DataStructures;
public class MyStack<T>
{
// The linked list that stores all stack items
private readonly SinglyLinkedList<T> _list;
// Expose the count from the underlying list
public int Count => _list.Count;
public bool IsEmpty => _list.Count == 0;
public MyStack()
{
_list = new SinglyLinkedList<T>();
}
// Push adds to the head of the list (O(1))
public void Push(T item)
{
_list.AddFirst(item);
}
// Pop removes from the head of the list (O(1))
public T Pop()
{
if (IsEmpty)
{
throw new InvalidOperationException("Stack is empty.");
}
return _list.RemoveFirst();
}
// Peek looks at the head without removing it
public T Peek()
{
if (IsEmpty)
{
throw new InvalidOperationException("Stack is empty.");
}
return _list.PeekFirst();
}
}
What does this code do?
This class is a thin wrapper around your linked list. It does not manage nodes or references directly.
- _list is marked readonly because the stack should never swap out its storage after construction.
- Push(T item) calls AddFirst, which inserts at the head in O(1) time.
- Pop() calls RemoveFirst, which removes the head in O(1) time. It throws if the stack is empty.
- Peek() calls PeekFirst, which reads the head's data without removing it.
- Save MyStack.cs.
Seeing a red squiggly under SinglyLinkedList?
Make sure the namespace DataStructures; line at the top of MyStack.cs matches the namespace in SinglyLinkedList.cs. Both files must share the same namespace to find each other.
help me fix the namespace error in MyStack.cs
✔️ Awesome, I've got everything!
Great. Double check you have saved MyStack.cs.
ⓧ I'd like to double check the full code
namespace DataStructures;
public class MyStack<T>
{
private readonly SinglyLinkedList<T> _list;
public int Count => _list.Count;
public bool IsEmpty => _list.Count == 0;
public MyStack()
{
_list = new SinglyLinkedList<T>();
}
public void Push(T item)
{
_list.AddFirst(item);
}
public T Pop()
{
if (IsEmpty)
{
throw new InvalidOperationException("Stack is empty.");
}
return _list.RemoveFirst();
}
public T Peek()
{
if (IsEmpty)
{
throw new InvalidOperationException("Stack is empty.");
}
return _list.PeekFirst();
}
}
Understand LIFO ordering
Your stack enforces a pattern called LIFO: Last-In, First-Out. The most recently pushed item is always the first one popped off.
What is LIFO?
Think of a stack of plates in a cafeteria. You can only take the top plate (the one placed most recently). You cannot reach into the middle or bottom without removing everything above it first.
By using AddFirst for Push and RemoveFirst for Pop, both operations work at the head of the linked list. This means Push and Pop are both O(1) constant time. No traversal is needed regardless of how many items are in the stack.
Why composition instead of inheritance?
Your MyStack<T> class holds a private SinglyLinkedList<T> field instead of inheriting from it. This is intentional. A stack should only expose Push, Pop, and Peek. If you inherited from the list, callers could bypass the stack rules and call AddLast or Remove directly.
Test the stack
Time to prove that LIFO ordering actually works. You will push three strings onto the stack and then pop them off one by one.
- In Program.cs, select all the existing code and replace it with the following:
using DataStructures;
// Test MyStack
MyStack<string> stack = new MyStack<string>();
stack.Push("first");
stack.Push("second");
stack.Push("third");
Console.WriteLine($"Peek: {stack.Peek()}");
Console.WriteLine(stack.Pop());
Console.WriteLine(stack.Pop());
Console.WriteLine(stack.Pop());
What does this test do?
- Three strings are pushed: "first", "second", then "third".
- Peek() shows the top item without removing it.
- Each Pop() removes and prints the current top. If LIFO works correctly, items come out in reverse order: third, second, first.
- Save Program.cs.
- Run the app by executing this command in your terminal:
dotnet run
- You should see this output:
Peek: third
third
second
first
The items come out in the exact reverse order they went in. That is LIFO in action.
Seeing an error about Stack is empty?
If you see an InvalidOperationException with the message "Stack is empty", you may be calling Pop more times than you called Push. Make sure you have exactly three Push calls before three Pop calls.
Also check that your Program.cs does not still contain the old linked list test code. The file should only contain the stack test.
help me debug my stack test output
✔️ Awesome, I've got everything!
Great. Double check you have saved Program.cs.
ⓧ I'd like to double check the full code
using DataStructures;
// Test MyStack
MyStack<string> stack = new MyStack<string>();
stack.Push("first");
stack.Push("second");
stack.Push("third");
Console.WriteLine($"Peek: {stack.Peek()}");
Console.WriteLine(stack.Pop());
Console.WriteLine(stack.Pop());
Console.WriteLine(stack.Pop());
Your stack is complete and proven. You built it entirely on top of your linked list without touching a single built-in collection. Next up, you will build a Queue that enforces the opposite pattern: first in, first out.
Build the Queue
Your stack proves that a linked list can power a Last-In-First-Out structure. Now you will build the opposite: a Queue that processes items in First-In-First-Out (FIFO) order.
Think of a queue like a line at a coffee shop. The first person to arrive is the first person served. Your linked list already has a _tail reference that makes appending O(1), which is exactly what makes an efficient queue possible.
In this step, get ready to:
- Create a generic MyQueue class that wraps your linked list.
- Implement Enqueue, Dequeue, and Peek operations.
- Test FIFO ordering to confirm items come out in the order they went in.
Understand how FIFO maps to your linked list
A queue needs two efficient operations: adding to the back and removing from the front. Your SinglyLinkedList<T> already supports both.
Why is this already O(1)?
Your linked list has a _tail reference that points directly to the last node. This means AddLast appends in O(1) without traversing the entire list.
Combined with RemoveFirst (also O(1) since it just moves the head pointer), your queue will enqueue and dequeue in constant time. That is the same performance as .NET's built-in Queue<T>.
Create the MyQueue class
Just like your stack wraps the linked list and exposes only LIFO operations, your queue will wrap the same list and expose only FIFO operations.
- Create a new file called MyQueue.cs in the same DataStructures project folder (where MyStack.cs lives).
- Add the full queue implementation by pasting in this code:
namespace DataStructures;
public class MyQueue<T>
{
private readonly SinglyLinkedList<T> _list;
public int Count => _list.Count;
public bool IsEmpty => _list.Count == 0;
public MyQueue()
{
_list = new SinglyLinkedList<T>();
}
public void Enqueue(T item)
{
_list.AddLast(item);
}
public T Dequeue()
{
if (IsEmpty)
{
throw new InvalidOperationException("Queue is empty.");
}
return _list.RemoveFirst();
}
public T Peek()
{
if (IsEmpty)
{
throw new InvalidOperationException("Queue is empty.");
}
return _list.PeekFirst();
}
}
What does this code do?
- Enqueue(T item) adds to the back of the queue by calling AddLast. Thanks to the tail reference, this is O(1).
- Dequeue() removes from the front by calling RemoveFirst. The first item enqueued is the first item out.
- Peek() lets you see the front item without removing it, using PeekFirst.
- Both Dequeue and Peek throw an InvalidOperationException if the queue is empty, preventing null reference errors.
Why composition instead of inheritance?
Notice that MyQueue<T> keeps its _list field private. A queue should only expose Enqueue, Dequeue, and Peek. If you inherited from the linked list, callers could also call AddFirst or Remove, which breaks the FIFO guarantee.
- Save MyQueue.cs.
You won't see a change yet. The next substep wires this into a test so you can verify FIFO ordering.
Seeing a red squiggly under SinglyLinkedList?
- Make sure the namespace at the top of MyQueue.cs says namespace DataStructures; exactly matching your other files.
- Confirm the file is saved inside the same folder as SinglyLinkedList.cs and MyStack.cs.
Why can't my MyQueue class find SinglyLinkedList?
✔️ Awesome, I've got everything!
Great. Double check you have saved MyQueue.cs before moving on.
ⓧ I'd like to double check the full code
namespace DataStructures;
public class MyQueue<T>
{
private readonly SinglyLinkedList<T> _list;
public int Count => _list.Count;
public bool IsEmpty => _list.Count == 0;
public MyQueue()
{
_list = new SinglyLinkedList<T>();
}
public void Enqueue(T item)
{
_list.AddLast(item);
}
public T Dequeue()
{
if (IsEmpty)
{
throw new InvalidOperationException("Queue is empty.");
}
return _list.RemoveFirst();
}
public T Peek()
{
if (IsEmpty)
{
throw new InvalidOperationException("Queue is empty.");
}
return _list.PeekFirst();
}
}
Test the queue
Time to prove FIFO works. You will enqueue three integers and dequeue them to confirm they come out in the same order they went in.
- Open Program.cs in VS Code.
- Select all the existing code and replace it with this queue test:
using DataStructures;
// Test MyQueue
MyQueue<int> queue = new MyQueue<int>();
queue.Enqueue(10);
queue.Enqueue(20);
queue.Enqueue(30);
Console.WriteLine(queue.Dequeue());
Console.WriteLine(queue.Dequeue());
Console.WriteLine(queue.Dequeue());
What does this test prove?
You enqueue 10, then 20, then 30. If the queue is truly FIFO, dequeuing should return them in that same order: 10, 20, 30.
Compare this to your stack test from the previous step. Pushing 'first', 'second', 'third' and popping returned them in reverse. The queue returns them in the original order.
- Save Program.cs.
- Run the app by executing this command in your terminal:
dotnet run
- You should see this output, confirming FIFO order:
10
20
30
Seeing numbers in the wrong order?
- If you see 30, 20, 10 (reversed), your Enqueue method is calling AddFirst instead of AddLast. Check that Enqueue uses _list.AddLast(item).
- If you see a build error, make sure you removed all the old stack test code from Program.cs before pasting the queue test.
My queue output is wrong or I'm getting a build error. Help me debug.
✔️ Awesome, I've got everything!
Great. Double check you have saved Program.cs before moving on.
ⓧ I'd like to double check the full code
using DataStructures;
// Test MyQueue
MyQueue<int> queue = new MyQueue<int>();
queue.Enqueue(10);
queue.Enqueue(20);
queue.Enqueue(30);
Console.WriteLine(queue.Dequeue());
Console.WriteLine(queue.Dequeue());
Console.WriteLine(queue.Dequeue());
You now have three custom data structures: a linked list, a stack, and a queue. Next up, you will wire all three together into a working Task Manager application that uses LIFO for undo history, FIFO for task processing, and the linked list for dynamic storage.
Build the Task Manager App
You've built all three core data structures: a linked list for dynamic storage, a stack for LIFO operations, and a queue for FIFO processing. Now it's time to see them work together in a real application.
In this step, you'll wire all three structures into an interactive Task Manager console app. The app will let you add tasks, process them in FIFO order, undo your last action, and view everything at a glance.
In this step, get ready to:
- Design how each data structure fits into the Task Manager.
- Implement the full interactive console app in Program.cs.
- Test the app by adding, processing, and undoing tasks.
Design the Task Manager
Each of your three data structures solves a different problem inside the Task Manager. Choosing the right structure for each job is what makes the app efficient.
- SinglyLinkedList<string> stores all active tasks. It grows and shrinks dynamically as tasks are added and removed.
- MyStack<string> tracks undo history. The most recent action is always on top, ready to be reversed with a single O(1) pop.
- MyQueue<string> handles task processing order. Tasks are processed first-in-first-out, just like a real job queue.
Why use three structures together?
Each structure has a strength. The linked list gives you flexible storage with O(n) search. The stack gives you O(1) undo. The queue gives you O(1) fair scheduling.
Using them together means each part of your app operates at the optimal time complexity for its job. This is exactly how real systems are designed.
Implement the core logic
The Task Manager uses a while loop that displays a menu, reads the user's choice, and routes it through a switch statement. Each case calls the appropriate method on the right data structure.
- Replace everything in Program.cs with the following code:
using DataStructures;
// Declare our three custom data structures
SinglyLinkedList<string> allTasks = new SinglyLinkedList<string>();
MyStack<string> undoHistory = new MyStack<string>();
MyQueue<string> processingQueue = new MyQueue<string>();
bool running = true;
while (running)
{
// Display the menu options
Console.WriteLine("\n===== Task Manager =====");
Console.WriteLine("1. Add Task");
Console.WriteLine("2. Process Next Task");
Console.WriteLine("3. Undo Last Action");
Console.WriteLine("4. View All Tasks");
Console.WriteLine("5. Exit");
Console.Write("Choose an option: ");
string? input = Console.ReadLine();
switch (input)
{
case "1":
// Add task to all three structures
Console.Write("Enter task name: ");
string? taskName = Console.ReadLine();
if (!string.IsNullOrWhiteSpace(taskName))
{
allTasks.AddLast(taskName);
processingQueue.Enqueue(taskName);
undoHistory.Push(taskName);
Console.WriteLine($"Task '{taskName}' added.");
}
break;
case "2":
// Process the next task from the queue (FIFO)
if (processingQueue.IsEmpty)
{
Console.WriteLine("No tasks to process.");
}
else
{
string processed = processingQueue.Dequeue();
allTasks.Remove(processed);
Console.WriteLine($"Processed task: '{processed}'");
}
break;
case "3":
// Undo the last added task (LIFO)
if (undoHistory.IsEmpty)
{
Console.WriteLine("Nothing to undo.");
}
else
{
string undone = undoHistory.Pop();
allTasks.Remove(undone);
Console.WriteLine($"Undone: removed task '{undone}'");
}
break;
case "4":
// Display all active tasks using the linked list
Console.WriteLine("Active tasks:");
allTasks.Print();
Console.WriteLine($"Total: {allTasks.Count} task(s)");
break;
case "5":
running = false;
Console.WriteLine("Goodbye!");
break;
default:
Console.WriteLine("Invalid option. Try again.");
break;
}
}
What does this code do?
- Case 1 (Add Task): adds the task to the linked list for storage, enqueues it for FIFO processing, and pushes it onto the stack for undo history.
- Case 2 (Process Next Task): dequeues the oldest task from the queue and removes it from the linked list. This proves FIFO order.
- Case 3 (Undo Last Action): pops the most recently added task from the stack and removes it from the linked list. This proves LIFO order.
- Case 4 (View All Tasks): traverses the linked list using your Print() method and displays the current count.
- Save Program.cs.
Seeing a compile error?
- Make sure you deleted ALL the previous test code from Program.cs. The file should only contain the Task Manager code above.
- Check that your SinglyLinkedList.cs has a Remove(T data) method. The Task Manager calls it in cases 2 and 3.
- Verify the using DataStructures; line is at the top of the file.
Still stuck? Help me fix a compile error in my Task Manager Program.cs. I replaced the test code with the full Task Manager switch-case loop but something isn't compiling.
✔️ Awesome, I've got everything!
Great. Make sure you've saved Program.cs before moving on to test it.
ⓧ I'd like to double check the full code
using DataStructures;
SinglyLinkedList<string> allTasks = new SinglyLinkedList<string>();
MyStack<string> undoHistory = new MyStack<string>();
MyQueue<string> processingQueue = new MyQueue<string>();
bool running = true;
while (running)
{
Console.WriteLine("\n===== Task Manager =====");
Console.WriteLine("1. Add Task");
Console.WriteLine("2. Process Next Task");
Console.WriteLine("3. Undo Last Action");
Console.WriteLine("4. View All Tasks");
Console.WriteLine("5. Exit");
Console.Write("Choose an option: ");
string? input = Console.ReadLine();
switch (input)
{
case "1":
Console.Write("Enter task name: ");
string? taskName = Console.ReadLine();
if (!string.IsNullOrWhiteSpace(taskName))
{
allTasks.AddLast(taskName);
processingQueue.Enqueue(taskName);
undoHistory.Push(taskName);
Console.WriteLine($"Task '{taskName}' added.");
}
break;
case "2":
if (processingQueue.IsEmpty)
{
Console.WriteLine("No tasks to process.");
}
else
{
string processed = processingQueue.Dequeue();
allTasks.Remove(processed);
Console.WriteLine($"Processed task: '{processed}'");
}
break;
case "3":
if (undoHistory.IsEmpty)
{
Console.WriteLine("Nothing to undo.");
}
else
{
string undone = undoHistory.Pop();
allTasks.Remove(undone);
Console.WriteLine($"Undone: removed task '{undone}'");
}
break;
case "4":
Console.WriteLine("Active tasks:");
allTasks.Print();
Console.WriteLine($"Total: {allTasks.Count} task(s)");
break;
case "5":
running = false;
Console.WriteLine("Goodbye!");
break;
default:
Console.WriteLine("Invalid option. Try again.");
break;
}
}
Run and test the app
Your data structures are now powering a real interactive application. Time to prove they work.
- Run the Task Manager by executing this command in your terminal:
dotnet run
You should see the menu appear with five options.
- Type 1 and press Enter.
- Enter a task name like Write unit tests and press Enter.
- Add two more tasks: Review pull request and Deploy to staging.
- Type 4 to view all tasks. You should see all three listed in order.
- Type 2 to process the next task. The first task you added should be processed (FIFO).
- Type 3 to undo. The last task you added should be removed (LIFO).
- Type 4 again to confirm only one task remains.
- Type 5 to exit.
What should I see?
When you process a task (option 2), it always takes the oldest task from the queue. When you undo (option 3), it always removes the most recently added task from the stack.
This is the key difference between FIFO and LIFO in action. Your queue processes fairly (first come, first served). Your stack undoes the most recent change first.
App exits immediately or menu doesn't appear?
- Make sure bool running = true; is set before the while loop. If it starts as false, the loop never executes.
- If you see an unhandled exception when processing or undoing, check that your linked list's Remove(T data) method exists and handles the case where the item is not found.
Need help? My Task Manager app isn't working as expected when I run dotnet run. Help me debug the interactive behavior.
Secret mission
Implement a Hash Table with Separate Chaining
Your Task Manager currently needs to traverse the entire linked list to find a task by name. That is O(n) lookup. Can you build a hash table from scratch that gives instant O(1) access to any task using an array of linked-list chains?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources for future reference, or delete them to free up disk space.
Resources you used:
- DataStructures project folder on your Desktop.
- .NET 10 SDK installed on your machine.
- Visual Studio Code with the C# Dev Kit extension.
Keep everything running
No action needed. Choose this if you plan to keep building C# projects or want to revisit this code later.
- Your DataStructures folder stays on your Desktop ready for future experiments.
- The .NET 10 SDK and VS Code remain installed for your next project.
Pause - I'll come back to this later
There are no running processes or cloud services to stop. Your project files sit idle on disk until you return.
- No action required. Everything is already paused by default since this is a local console application.
Delete - I don't want to use this again
Remove all project files and optionally uninstall the tools you installed for this project.
- Delete the project folder by running this command in your terminal (macOS/Linux):
rm -rf ~/Desktop/DataStructures
- On Windows, open PowerShell and run:
Remove-Item -Recurse -Force ~\Desktop\DataStructures
- To uninstall the .NET 10 SDK on Windows: open Settings, select Apps, then Installed Apps. Search for .NET, select the SDK entry, and click Uninstall.
- To uninstall the .NET 10 SDK on macOS: remove the versioned SDK folder. Run dotnet --list-sdks to find your installed version, then delete it with rm -rf on the path shown (typically under /usr/local/share/dotnet/sdk/).
- To uninstall VS Code on macOS: open Finder, go to Applications, and drag Visual Studio Code to the Trash.
- To uninstall VS Code on Windows: open Settings, select Apps, then Installed Apps. Search for Visual Studio Code, select it, and click Uninstall.
Nice Work!
Nice Work!
You just built the core data structures that power almost every program. From scratch. In C#. No shortcuts.
You've learned how to:
- Build a generic Node<T> and a complete SinglyLinkedList<T> with head/tail management from scratch.
- Create a MyStack<T> and MyQueue<T> proving you understand LIFO and FIFO patterns with O(1) operations.
- Wire all three custom data structures into a working Task Manager console application that demonstrates real-world usage.
- Built a MyHashTable<TKey, TValue> with separate chaining and O(1) lookup to give the Task Manager instant search by name.
Ready to quiz yourself?