C# Stacks Queues

Stacks and queues are collection types for data where order controls what you process next. A stack is last-in, first-out: the most recently added item is removed first. A queue is first-in, first-out: the oldest waiting item is removed first. In C#, Stack<T> and Queue<T> make these patterns explicit, efficient, and safer than manually managing indexes in a list.

Overview: How Stacks and Queues Work

A Stack<T> models a pile of items. You Push an item onto the top, Peek at the top without removing it, and Pop the top item off. This is called LIFO, or last-in, first-out. Function calls, undo history, depth-first search, and bracket matching all naturally use stack behavior because the most recent unfinished thing is the next thing to handle.

A Queue<T> models a waiting line. You Enqueue an item at the back, Peek at the front, and Dequeue the front item. This is FIFO, or first-in, first-out. Print jobs, support tickets, breadth-first search, background work, and event processing often use queue behavior because older work should be handled before newer work.

Both types are generic collections in System.Collections.Generic. The type parameter T says what kind of item the collection stores, such as Stack<string> or Queue<int>. Internally, the standard implementations use arrays that can grow as needed. Adding or removing at the logical end is usually O(1). When the internal array is full, the collection allocates a larger array and copies existing items, so that particular add operation costs more; spread over many operations, additions are still amortized O(1).

These collections are not sorted, not indexed for random access, and not thread-safe for simultaneous mutation from multiple threads. That is intentional. Their power is that they restrict access to the correct end of the data structure, which makes the code communicate the intended ordering rule.

Syntax

Stack<T> stack = new Stack<T>();
stack.Push(item);
T top = stack.Peek();
T removedTop = stack.Pop();

Queue<T> queue = new Queue<T>();
queue.Enqueue(item);
T front = queue.Peek();
T removedFront = queue.Dequeue();
Member Stack behavior Queue behavior
Count Number of items in the stack Number of items in the queue
Push / Enqueue Adds to the top Adds to the back
Pop / Dequeue Removes the newest item Removes the oldest item
Peek Reads the newest item Reads the oldest item
TryPop / TryDequeue Safely attempts removal Safely attempts removal
Clear Removes all items Removes all items
  • T is the element type. A Stack<string> stores only strings, while a Queue<Order> stores Order objects.
  • Peek, Pop, and Dequeue throw InvalidOperationException when the collection is empty.
  • TryPop, TryPeek, and TryDequeue avoid exceptions by returning false when no item is available.

Examples

A Stack for Browser Back History

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Stack<string> history = new Stack<string>();

        history.Push("/home");
        history.Push("/products");
        history.Push("/cart");

        Console.WriteLine($"Current: {history.Peek()}");
        history.Pop();

        Console.WriteLine($"Back to: {history.Peek()}");
        history.Pop();

        Console.WriteLine($"Back to: {history.Peek()}");
        history.Pop();

        Console.WriteLine(history.Count == 0 ? "No page left." : history.Peek());
    }
}

Output:

Current: /cart
Back to: /products
Back to: /home
No page left.

The last page pushed, /cart, is the first page seen by Peek and the first one removed by Pop. This is exactly what a Back button needs: when the user moves backward, the most recent page is discarded first.

A Queue for Support Tickets

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Queue<string> tickets = new Queue<string>();

        tickets.Enqueue("T-1001: password reset");
        tickets.Enqueue("T-1002: billing question");
        tickets.Enqueue("T-1003: cannot upload file");

        while (tickets.Count > 0)
        {
            string nextTicket = tickets.Dequeue();
            Console.WriteLine($"Handling {nextTicket}");
        }
    }
}

Output:

Handling T-1001: password reset
Handling T-1002: billing question
Handling T-1003: cannot upload file

The first ticket enqueued is handled first. This makes the rule fair and easy to read. A List<string> could do this, but repeatedly removing from the front of a list shifts later elements; a queue is designed for this workflow.

Checking Balanced Brackets with a Stack

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        string[] samples = { "()[]{}", "([{}])", "([)]", "((())" };

        foreach (string sample in samples)
        {
            Console.WriteLine($"{sample} -> {(IsBalanced(sample) ? "balanced" : "not balanced")}");
        }
    }

    static bool IsBalanced(string text)
    {
        Stack<char> openings = new Stack<char>();

        foreach (char ch in text)
        {
            if (ch == '(' || ch == '[' || ch == '{')
            {
                openings.Push(ch);
            }
            else if (ch == ')' || ch == ']' || ch == '}')
            {
                if (!openings.TryPop(out char open) || !Matches(open, ch))
                {
                    return false;
                }
            }
        }

        return openings.Count == 0;
    }

    static bool Matches(char open, char close)
    {
        return (open == '(' && close == ')')
            || (open == '[' && close == ']')
            || (open == '{' && close == '}');
    }
}

Output:

()[]{} -> balanced
([{}]) -> balanced
([)] -> not balanced
((()) -> not balanced

Nested brackets must close in reverse order. The stack remembers openings that have not been closed yet. When a closing bracket appears, the most recent opening bracket must match it. TryPop prevents an exception if a closing bracket appears before any opening bracket.

Breadth-First Search with a Queue

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Dictionary<string, string[]> graph = new Dictionary<string, string[]>
        {
            ["A"] = new[] { "B", "C" },
            ["B"] = new[] { "D" },
            ["C"] = new[] { "E" },
            ["D"] = Array.Empty<string>(),
            ["E"] = Array.Empty<string>()
        };

        Queue<string> frontier = new Queue<string>();
        HashSet<string> seen = new HashSet<string>();

        frontier.Enqueue("A");
        seen.Add("A");

        while (frontier.Count > 0)
        {
            string node = frontier.Dequeue();
            Console.WriteLine($"Visited {node}");

            foreach (string neighbor in graph[node])
            {
                if (seen.Add(neighbor))
                {
                    frontier.Enqueue(neighbor);
                }
            }
        }
    }
}

Output:

Visited A
Visited B
Visited C
Visited D
Visited E

Breadth-first search visits all nodes at the current distance before moving deeper. A queue preserves that order: neighbors discovered first are processed first. The HashSet<string> prevents revisiting the same node if the graph contains cycles.

How It Works Step by Step

When you create new Stack<int>() or new Queue<int>(), the CLR creates an object on the managed heap. The object keeps bookkeeping fields such as the count and a reference to an internal array. The generic type argument is part of the constructed type, so Stack<int> and Stack<string> are different runtime types with type-safe methods.

For a stack, Push stores the item at the next available array slot and moves the logical top forward. Pop moves the top backward, returns the old top value, and clears the slot when needed so references can be garbage collected. For a queue, the implementation tracks a head and tail position in a circular array. Enqueue writes at the tail, and Dequeue reads from the head. When either position reaches the end of the array, it wraps around instead of shifting every element.

Enumeration order is another detail to remember. A stack enumerates from top to bottom, while a queue enumerates from front to back. Enumerating does not remove items, but changing the collection during enumeration causes an exception because the enumerator detects that the collection changed.

Common Mistakes

Calling Pop or Dequeue on an Empty Collection

Stack<int> numbers = new Stack<int>();
int value = numbers.Pop();

This code is wrong because an empty stack has no top item. It compiles, but at runtime Pop throws InvalidOperationException. Use Count or a Try method when emptiness is possible.

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Stack<int> numbers = new Stack<int>();

        if (numbers.TryPop(out int value))
        {
            Console.WriteLine(value);
        }
        else
        {
            Console.WriteLine("The stack is empty.");
        }
    }
}

Output:

The stack is empty.

Using the Wrong Structure for the Required Order

Queue<string> undo = new Queue<string>();
undo.Enqueue("Type title");
undo.Enqueue("Delete paragraph");
Console.WriteLine(undo.Dequeue());

This code treats undo history as FIFO, so it would undo the oldest action first. Undo should usually reverse the most recent action first, so a stack is the better model.

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Stack<string> undo = new Stack<string>();
        undo.Push("Type title");
        undo.Push("Delete paragraph");

        Console.WriteLine($"Undo: {undo.Pop()}");
    }
}

Output:

Undo: Delete paragraph

Best Practices

  • Choose Stack<T> when the newest pending item should be handled first.
  • Choose Queue<T> when the oldest pending item should be handled first.
  • Use TryPop, TryPeek, and TryDequeue when the collection may be empty.
  • Prefer these types over List<T> when your code only needs stack or queue behavior; the type documents the algorithm.
  • Do not rely on random indexing. If you need frequent access by index, use an array or list instead.
  • Avoid modifying a stack or queue while iterating over it with foreach.
  • For producer-consumer work across threads, use thread-safe collections such as ConcurrentQueue<T> instead of manually locking unless you have a clear reason.

Practice Exercises

  1. Write a program that reads five names into a Stack<string> and prints them in reverse order.
  2. Create a Queue<string> for three print jobs. Print and remove each job in the order it arrived.
  3. Extend the bracket checker so it ignores letters and numbers inside an expression such as a * (b + ).

Summary

  • Stack<T> is LIFO: Push, Peek, and Pop work with the newest item.
  • Queue<T> is FIFO: Enqueue, Peek, and Dequeue work with the oldest item.
  • Both collections are generic, type-safe, and usually O(1) for their main operations.
  • Empty Pop, Peek, or Dequeue calls throw exceptions, so use Try methods when needed.
  • The right choice depends on the algorithm’s ordering rule, not just on storing multiple values.