Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 17

Stack Data Structure and LIFO Problem Solving

Recommended resources

Hello! In our last lesson, we mastered the art of merging sorted linked lists, tackling the problem with both iterative and recursive approaches and analyzing their space-time trade-offs. That was a great way to solidify your pointer manipulation skills.

Today, we transition from linked lists to another fundamental linear data structure: the stack. You'll discover its core principle, Last-In, First-Out (LIFO), which underpins many algorithms and software features you use daily, from a compiler's syntax checker to your browser's back button. This lesson will equip you to implement a stack efficiently in Python and use it to solve a classic interview problem, "Valid Parentheses."

What is a Stack? The LIFO Principle

A stack is a linear data structure that follows a simple but powerful rule: Last-In, First-Out (LIFO). This means the last element added to the stack is always the first one to be removed.

A common and effective analogy is a stack of plates. You can only add a new plate to the top, and you can only take a plate from the top. You can't pull a plate from the middle or bottom without toppling the whole stack. This "top-only" access is the defining feature of a stack.

Python Stack: Implementing LIFO Data Structures - DataCamp

This DataCamp tutorial provides an excellent introduction to the core concepts of a stack.

Please read the first two sections, "What Is a Stack in Python?" and "Stack vs list". Focus on understanding the LIFO principle and how a stack's restricted access is a feature, not a limitation, when designing certain algorithms.


Core Stack Operations

All stacks, regardless of implementation, are defined by a few core operations. These operations are designed to be extremely fast.

  • Push: Adds a new element to the top of the stack.
  • Pop: Removes and returns the element currently at the top of the stack. Attempting to pop from an empty stack results in an error condition known as Stack Underflow.
  • Peek (or Top): Returns the top element without removing it. This is useful for inspecting the top element before deciding on the next action.
  • is_empty: A helper operation that returns true if the stack has no elements, and false otherwise. It's crucial for avoiding stack underflow errors.
  • size: Returns the number of elements currently in the stack.
This diagram illustrates the fundamental stack operations. Each 'push' adds an element to the top and increments the 'TOP' index. A 'pop' removes the top element and decrements the index, demonstrating the LIFO principle.

One of the main reasons stacks are so prevalent in algorithm design is their efficiency. Because all operations happen at one end, they can be performed in constant time.

Python Stack: Implementing LIFO Data Structures - DataCamp

The same DataCamp article explains these operations and their performance characteristics in detail.

Read the section "Core Stack Operations and Time Complexity". Pay close attention to the table summarizing the time complexity. The O(1) time for push, pop, and peek is the most important takeaway.


Implementing a Stack in Python

Python doesn't have a built-in stack data type like some other languages. Instead, it provides flexible data structures that can be used to implement a stack. Let's look at the most common options and their trade-offs, which is a key discussion point in technical interviews.

1. Using a list

The most straightforward method is to use Python's built-in list.

  • push is implemented with list.append().
  • pop is implemented with list.pop().
  • peek can be done by accessing the last element my_stack[-1].

While simple and often sufficient for quick scripts or whiteboard problems, there's a performance detail to be aware of. Python lists are dynamic arrays. While append() and pop() from the end are usually O(1), the list might need to be resized and reallocated in memory if it runs out of space, which is an O(n) operation. This makes the performance amortized O(1), not guaranteed O(1).

2. Using collections.deque

For performance-critical applications and robust algorithmic code, collections.deque is the recommended tool. A deque (pronounced "deck," short for double-ended queue) is implemented internally as a doubly-linked list.

This structure allows for highly efficient additions and removals from both ends. For our stack, we use it just like a list:

  • push is implemented with deque.append().
  • pop is implemented with deque.pop().
  • peek is done with my_stack[-1].

The key advantage is that these operations are guaranteed O(1) time, as there is never a need to shift elements or reallocate the entire structure.

Python's deque: Implement Efficient Queues and Stacks

This Real Python article dives into why deque is the superior choice for implementing stacks and queues.

Read the introductory section "Get Started With Python's deque" to understand what it is and how it differs from a list. Jump to the "Frequently Asked Questions" at the end and read the answer to "Why should you use deque instead of list for queues or stacks?". This nicely summarizes the key takeaway.

3. Using queue.LifoQueue

The standard library also offers queue.LifoQueue. This implementation is specifically designed for multi-threaded programming. It includes locking mechanisms to ensure that operations are thread-safe. However, this safety comes with performance overhead, making it unsuitable for single-threaded algorithmic problems where deque is much faster.

For interview purposes and general problem-solving, collections.deque is almost always the best answer when asked for a Python stack implementation.


Problem-Solving with Stacks: Valid Parentheses

Now, let's apply our knowledge to a classic LeetCode problem: Valid Parentheses. This problem is a perfect illustration of LIFO logic.

The Problem: Given a string s containing just the characters (, ), {, }, [ and ], determine if the input string is valid. An input string is valid if:

  1. Open brackets must be closed by the same type of brackets.
  2. Open brackets must be closed in the correct order.

For example, "{[()]}" is valid, but "([)]" is not. The "correct order" rule implies that the most recently opened bracket must be the first one to be closed. This is a direct signal to use a stack.

This image shows how a stack can track open parentheses. When a closing parenthesis is found, the top of the stack is popped and checked for a match, demonstrating the LIFO-based solution.

The algorithm is as follows:

  1. Create an empty stack.
  2. Create a hash map to store the matching pairs (e.g., ')': '(', ']': '[').
  3. Iterate through each character of the string.
  4. If the character is an opening bracket ((, [, {), push it onto the stack.
  5. If the character is a closing bracket (), ], }):
    • Check if the stack is empty. If it is, we have a closing bracket with no corresponding opener, so the string is invalid.
    • If the stack is not empty, pop the top element.
    • Check if this popped element is the correct opening bracket for the current closing bracket (using our hash map). If they don't match, the string is invalid.
  6. After the loop finishes, if the stack is empty, it means every opening bracket was correctly matched and closed. The string is valid. If the stack is not empty, it means there are unclosed opening brackets, so the string is invalid.

This video from Greg Hogg provides a concise walkthrough of this logic and the Python implementation.

Valid Parentheses - Leetcode 20 - Stacks (Python)

This video from Greg Hogg provides a clear, step-by-step solution to the Valid Parentheses problem.

Watch the initial explanation from the beginning to understand the rules and edge cases. Follow the core logic of using a stack from this segment. He explains how to handle open vs. closed brackets and the final check. Finally, watch the code implementation, where he uses a hash map and a list-as-a-stack to solve the problem, and discusses the complexity.

As a full-stack developer, you can see how this pattern is fundamental. Parsers that validate structured data formats like JSON or XML rely on similar stack-based mechanisms to ensure that every opening tag or bracket has a corresponding closing one in the correct nested order.


Conclusion

In this lesson, we demystified the stack, a cornerstone data structure in computer science. You've learned its defining LIFO principle and how to apply it to solve a common and important interview problem.

Key Takeaways:

  • A stack is a linear data structure that operates on a Last-In, First-Out (LIFO) basis.
  • The primary operations are Push, Pop, and Peek, all of which have an efficient O(1) time complexity.
  • In Python, a stack can be implemented with a list, but collections.deque is the preferred choice for its guaranteed O(1) performance.
  • Stacks are ideal for problems involving nested structures or "last-opened, first-closed" logic, such as validating parentheses.

Now that we have covered stacks and their LIFO behavior, our next lesson will introduce their counterpart: queues. We will explore the First-In, First-Out (FIFO) principle and see how it's used to model different kinds of problems, like processing tasks in order.