Stacks & Queues

Data Structures & Algorithms

Overview

Two fundamental abstract data types that restrict how elements are added and removed, creating specific behavioral patterns useful for many algorithms and system designs.


Stacks (LIFO - Last In, First Out)

Why Stacks Exist

LIFO behavior mirrors natural problem patterns - function calls, undo operations, parsing nested structures, backtracking algorithms.

When to Use Stacks

Use when:

  • Expression evaluation (parentheses matching, postfix notation)
  • Function call management (call stack)
  • Undo functionality in applications
  • Backtracking algorithms (maze solving, N-queens)
  • Parsing nested structures (JSON, XML, code)
  • Depth-first search implementations

Don’t use when:

  • Need to access middle elements
  • FIFO behavior is required
  • Need to process elements in insertion order

Modern reality: Most languages provide built-in stacks. In interviews, often implement with arrays or linked lists to show understanding.

Time Complexity

  • Push: O(1)
  • Pop: O(1)
  • Peek/Top: O(1)
  • Search: O(n)

C# Implementation

// Node class for linked implementation
public class Node<T>
{
    public T Value { get; set; }
    public Node<T> LinkNode { get; set; }

    public Node(T value, Node<T> linkNode = null)
    {
        Value = value;
        LinkNode = linkNode;
    }

    public void SetLinkNode(Node<T> linkNode) => LinkNode = linkNode;
    public Node<T> GetLinkNode() => LinkNode;
    public T GetValue() => Value;
}

public class Stack<T>
{
    private Node<T> topItem;
    private int size;
    private int limit;
    
    public Stack(int limit = 1000)
    {
        this.limit = limit;
        size = 0;
        topItem = null;
    }
    
    public void Push(T value)
    {
        if (HasSpace())
        {
            var item = new Node<T>(value);
            item.SetLinkNode(topItem);
            topItem = item;
            size++;
            Console.WriteLine($"Pushed {value} onto stack");
        }
        else
        {
            Console.WriteLine($"Stack overflow! No room for {value}");
        }
    }
    
    public T Pop()
    {
        if (!IsEmpty())
        {
            var itemToRemove = topItem;
            topItem = itemToRemove.GetLinkNode();
            size--;
            var value = itemToRemove.GetValue();
            Console.WriteLine($"Popped {value} from stack");
            return value;
        }
        else
        {
            Console.WriteLine("Stack underflow! Stack is empty");
            return default(T);
        }
    }
    
    public T Peek()
    {
        if (!IsEmpty())
        {
            return topItem.GetValue();
        }
        else
        {
            Console.WriteLine("Nothing to peek at!");
            return default(T);
        }
    }
    
    public bool HasSpace() => limit > size;
    public bool IsEmpty() => size == 0;
    public int Size => size;
}

// Example usage
var stack = new Stack<string>(5);
stack.Push("First");
stack.Push("Second");
stack.Push("Third");
Console.WriteLine($"Top item: {stack.Peek()}");  // Third
Console.WriteLine($"Popped: {stack.Pop()}");     // Third
Console.WriteLine($"Popped: {stack.Pop()}");     // Second
Console.WriteLine($"Size: {stack.Size}");        // 1

Common Queue Applications

public static bool BFS(Dictionary<int, List<int>> graph, int startNode, int target)
{
    var queue = new Queue<int>();
    var visited = new HashSet<int>();

    queue.Enqueue(startNode);
    visited.Add(startNode);

    while (queue.Count > 0)
    {
        int currentNode = queue.Dequeue();

        if (currentNode == target)
            return true;

        foreach (int neighbor in graph[currentNode])
        {
            if (!visited.Contains(neighbor))
            {
                visited.Add(neighbor);
                queue.Enqueue(neighbor);
            }
        }
    }

    return false;
}

2. Level-Order Tree Traversal

public static List<T> LevelOrderTraversal<T>(TreeNode<T> root)
{
    if (root == null)
        return new List<T>();

    var queue = new Queue<TreeNode<T>>();
    var result = new List<T>();

    queue.Enqueue(root);

    while (queue.Count > 0)
    {
        var node = queue.Dequeue();
        result.Add(node.Value);

        if (node.Left != null)
            queue.Enqueue(node.Left);
        if (node.Right != null)
            queue.Enqueue(node.Right);
    }

    return result;
}

3. Producer-Consumer with Queue

using System.Collections.Concurrent;
using System.Threading.Tasks;

public static class ProducerConsumer
{
    public static async Task RunProducerConsumer()
    {
        var queue = new ConcurrentQueue<string>();
        var items = new[] { "task1", "task2", "task3", "task4" };

        var producerTask = Task.Run(() => Producer(queue, items));
        var consumerTask = Task.Run(() => Consumer(queue));

        await Task.WhenAll(producerTask, consumerTask);
    }

    private static void Producer(ConcurrentQueue<string> queue, string[] items)
    {
        foreach (var item in items)
        {
            Console.WriteLine($"Producing {item}");
            queue.Enqueue(item);
            Thread.Sleep(100);
        }
    }

    private static void Consumer(ConcurrentQueue<string> queue)
    {
        while (true)
        {
            if (queue.TryDequeue(out string item))
            {
                Console.WriteLine($"Consuming {item}");
            }
            else
            {
                Thread.Sleep(10); // Brief pause if queue is empty
                if (queue.IsEmpty)
                    break;
            }
        }
    }
}

Advanced Variations

Deque (Double-Ended Queue)

Supports insertion and deletion at both ends.

// Using C#'s LinkedList for deque-like behavior
var deque = new LinkedList<int>(new[] { 1, 2, 3 });
deque.AddFirst(0);    // Add to front: [0, 1, 2, 3]
deque.AddLast(4);     // Add to back: [0, 1, 2, 3, 4]
deque.RemoveFirst();  // Remove from front: [1, 2, 3, 4]
deque.RemoveLast();   // Remove from back: [1, 2, 3]

Priority Queue

Elements are served based on priority rather than insertion order.

// Using .NET 6+ PriorityQueue
public static class PriorityQueueExample
{
    public static void DemonstratePriorityQueue()
    {
        var pq = new PriorityQueue<string, int>();

        pq.Enqueue("Low priority task", 3);
        pq.Enqueue("High priority task", 1);
        pq.Enqueue("Medium priority task", 2);

        while (pq.Count > 0)
        {
            Console.WriteLine(pq.Dequeue());
        }
        // Output: High priority task, Medium priority task, Low priority task
    }
}

Circular Queue (Ring Buffer)

Fixed-size queue that wraps around when full.

public class CircularQueue<T>
{
    private T[] queue;
    private int head;
    private int tail;
    private int count;
    private int size;

    public CircularQueue(int size)
    {
        this.size = size;
        queue = new T[size];
        head = 0;
        tail = 0;
        count = 0;
    }

    public bool Enqueue(T item)
    {
        if (count < size)
        {
            queue[tail] = item;
            tail = (tail + 1) % size;
            count++;
            return true;
        }
        return false; // Queue is full
    }

    public T Dequeue()
    {
        if (count > 0)
        {
            T item = queue[head];
            queue[head] = default(T);
            head = (head + 1) % size;
            count--;
            return item;
        }
        return default(T); // Queue is empty
    }

    public bool IsFull() => count == size;
    public bool IsEmpty() => count == 0;
}

// Usage
var cq = new CircularQueue<string>(3);
cq.Enqueue("A");
cq.Enqueue("B");
cq.Enqueue("C");
Console.WriteLine(cq.Enqueue("D")); // False - queue is full
Console.WriteLine(cq.Dequeue());   // "A"
cq.Enqueue("D");                   // Now succeeds

Interview Problems

1. Implement Queue Using Stacks

public class QueueUsingStacks<T>
{
    private Stack<T> stack1; // For enqueue
    private Stack<T> stack2; // For dequeue

    public QueueUsingStacks()
    {
        stack1 = new Stack<T>();
        stack2 = new Stack<T>();
    }

    public void Enqueue(T item)
    {
        stack1.Push(item);
    }

    public T Dequeue()
    {
        if (stack2.Count == 0)
        {
            // Move all elements from stack1 to stack2
            while (stack1.Count > 0)
            {
                stack2.Push(stack1.Pop());
            }
        }

        if (stack2.Count > 0)
            return stack2.Pop();

        return default(T);
    }
}

2. Implement Stack Using Queues

public class StackUsingQueues<T>
{
    private Queue<T> queue1;
    private Queue<T> queue2;

    public StackUsingQueues()
    {
        queue1 = new Queue<T>();
        queue2 = new Queue<T>();
    }

    public void Push(T item)
    {
        // Add to queue2, move all from queue1 to queue2, then swap
        queue2.Enqueue(item);
        while (queue1.Count > 0)
        {
            queue2.Enqueue(queue1.Dequeue());
        }

        // Swap queues
        var temp = queue1;
        queue1 = queue2;
        queue2 = temp;
    }

    public T Pop()
    {
        if (queue1.Count > 0)
            return queue1.Dequeue();

        return default(T);
    }
}

3. Valid Parentheses (Stack Application)

public static bool IsValidParentheses(string s)
{
    var stack = new Stack<char>();
    var mapping = new Dictionary<char, char>
    {
        {')', '('},
        {'}', '{'},
        {']', '['}
    };

    foreach (char c in s)
    {
        if (mapping.ContainsKey(c))
        {
            if (stack.Count == 0 || stack.Pop() != mapping[c])
                return false;
        }
        else
        {
            stack.Push(c);
        }
    }

    return stack.Count == 0;
}

// Test cases
Console.WriteLine(IsValidParentheses("()"));       // True
Console.WriteLine(IsValidParentheses("()[]{}"));   // True
Console.WriteLine(IsValidParentheses("(]"));       // False

Modern Usage

C#:

  • Use Stack<T> for stack operations
  • Use Queue<T> for queue operations
  • Use PriorityQueue<TElement, TPriority> (.NET 6+)
  • Use ConcurrentQueue<T> for thread-safety

Key Takeaways

Stacks are perfect for:

  • Reversing things
  • Keeping track of state/history
  • Parsing nested structures
  • Recursive algorithm implementations

Queues are perfect for:

  • Processing in order
  • Buffering between producers and consumers
  • Breadth-first algorithms
  • Scheduling and task management

Interview focus: Understand when to use each, how to implement with arrays or linked lists, and common applications like expression evaluation and tree traversal. Stack Applications

1. Parentheses Matching

public static bool IsBalanced(string expression)
{
    var stack = new Stack<char>();
    var pairs = new Dictionary<char, char>
    {
        {'(', ')'},
        {'[', ']'},
        {'{', '}'}
    };

    foreach (char c in expression)
    {
        if (pairs.ContainsKey(c)) // Opening bracket
        {
            stack.Push(c);
        }
        else if (pairs.ContainsValue(c)) // Closing bracket
        {
            if (stack.Count == 0)
                return false;
            if (pairs[stack.Pop()] != c)
                return false;
        }
    }

    return stack.Count == 0;
}

// Test cases
Console.WriteLine(IsBalanced("()[]{}"));   // True
Console.WriteLine(IsBalanced("([{}])"));   // True
Console.WriteLine(IsBalanced("([)]"));     // False

2. Postfix Expression Evaluation

public static double EvaluatePostfix(string expression)
{
    var stack = new Stack<double>();
    var operators = new HashSet<string> {"+", "-", "*", "/"};

    foreach (string token in expression.Split(' '))
    {
        if (operators.Contains(token))
        {
            double b = stack.Pop();
            double a = stack.Pop();

            double result = token switch
            {
                "+" => a + b,
                "-" => a - b,
                "*" => a * b,
                "/" => a / b,
                _ => throw new InvalidOperationException($"Unknown operator: {token}")
            };

            stack.Push(result);
        }
        else
        {
            stack.Push(double.Parse(token));
        }
    }

    return stack.Peek();
}

// Example: "3 4 + 2 *" = (3 + 4) * 2 = 14
Console.WriteLine(EvaluatePostfix("3 4 + 2 *")); // 14.0

Queues (FIFO - First In, First Out)

Why Queues Exist

FIFO behavior matches real-world scenarios - waiting lines, task scheduling, breadth-first processing, producer-consumer systems.

When to Use Queues

Use when:

  • Task scheduling and job processing
  • Breadth-first search algorithms
  • Handling requests in order (web servers, print queues)
  • Producer-consumer scenarios
  • Buffer for streaming data
  • Level-order tree traversal

Don’t use when:

  • Need random access to elements
  • LIFO behavior is required
  • Priority matters more than insertion order (use priority queue)

Modern reality: Built into most languages. Use Queue<T> in C#.

Time Complexity

  • Enqueue (add): O(1)
  • Dequeue (remove): O(1)
  • Front/Peek: O(1)
  • Search: O(n)

C# Implementation

public class Queue<T>
{
    private Node<T> head;
    private Node<T> tail;
    private int? maxSize;
    private int size;
    
    public Queue(int? maxSize = null)
    {
        this.maxSize = maxSize;
        size = 0;
        head = null;
        tail = null;
    }
    
    public void Enqueue(T value)
    {
        if (HasSpace())
        {
            var itemToAdd = new Node<T>(value);
            Console.WriteLine($"Adding {value} to the queue");
            
            if (IsEmpty())
            {
                head = itemToAdd;
                tail = itemToAdd;
            }
            else
            {
                tail.SetLinkNode(itemToAdd);
                tail = itemToAdd;
            }
            
            size++;
        }
        else
        {
            Console.WriteLine("Queue is full! Cannot add more items");
        }
    }
    
    public T Dequeue()
    {
        if (GetSize() > 0)
        {
            var itemToRemove = head;
            Console.WriteLine($"{itemToRemove.GetValue()} is being served!");
            
            if (GetSize() == 1)
            {
                head = null;
                tail = null;
            }
            else
            {
                head = head.GetLinkNode();
            }
            
            size--;
            return itemToRemove.GetValue();
        }
        else
        {
            Console.WriteLine("The queue is empty!");
            return default(T);
        }
    }
    
    public T Peek()
    {
        if (size > 0)
        {
            return head.GetValue();
        }
        else
        {
            Console.WriteLine("No items in queue!");
            return default(T);
        }
    }
    
    public int GetSize() => size;
    
    public bool HasSpace()
    {
        if (maxSize == null)
            return true;
        else
            return maxSize > GetSize();
    }
    
    public bool IsEmpty() => size == 0;
}

// Example usage
Console.WriteLine("Creating a deli queue with max 5 orders...");
var deliLine = new Queue<string>(5);

// Add orders
string[] orders = {"Sandwich", "Soup", "Salad", "Pizza", "Burger"};
foreach (var order in orders)
{
    deliLine.Enqueue(order);
}

Console.WriteLine($"\nNext order up: {deliLine.Peek()}");

// Serve orders
while (!deliLine.IsEmpty())
{
    deliLine.Dequeue();
}

Common

Found this guide helpful? Share it with your team:

Share on LinkedIn