Lesson 1

A hearty welcome to our learners! Building on the foundation of our previous lesson on the importance of data structures, today we're venturing into the depths of a fundamental data structure in software engineering: the **"Stack"**. Drawing parallels from the real world, a `Stack`

operates just like a stack of plates. The last plate you put (push) on the stack will be the first one you take (pop) off. It's this simple principle that makes Stacks so powerful and widely used.

In this lesson, our learning objectives are to conceptualize Stacks, understand how to implement them, analyze their time and space complexity, and learn how to manipulate Stacks to solve algorithmic problems.

As today's lesson concludes, you will have gained the ability to implement a Stack in Python, understand its basic operations, and possess a robust knowledge of its applications in real-world scenarios.

Take a moment to visualize a real-life stack of plates. The logic behind Stacks in the realm of computer science mirrors this real-world stack closely. A Stack follows the principle of "Last In, First Out" (LIFO), meaning the most recent item you place on the Stack will be the first one to be removed. All elements in a Stack are added and removed from the top.

As a real-world comparison, consider the Stack to be a stack of books. You can only add (push) a book to the top of the stack, and similarly, to remove (pop) a book, you must do so from the top of the stack. Suppose you need to access a specific book in the middle of the stack. In that case, you must remove (pop) all the books placed above it first.

In computer science, a Stack is a dynamic data structure. It has the ability to grow and shrink as we add and remove elements, respectively. Stacks find use in various areas of software engineering, such as tracking the execution of computer programs, memory management, parsing expressions, and much more.

We can utilize Python's built-in `list`

datatype to implement a Stack. To build a Stack, we need to define the following three basic operations:

`push`

- Adds an element to the top.`pop`

- Removes and returns the top element.`peek`

- Returns the top element without removing it.

Translating our understanding into Python code, these operations can be implemented as follows:

Python`1# Create an empty stack 2stack = [] 3 4# Push elements 5stack.append('A') 6stack.append('B') 7stack.append('C') 8 9print(stack) # Output: ['A', 'B', 'C'] 10 11# Pop an element 12topElement = stack.pop() 13print(f"Popped Element: {topElement}") # Output: 'C' 14 15# New top of the stack 16newTopElement = stack[-1] 17print(f"Top Element after Pop: {newTopElement}") # Output: 'B'`

To better understand the concept, consider a scenario where we have an empty stack. We push 'A', 'B', and 'C' onto it, making 'C' our top element. We then pop the top element, 'C', from the stack, and 'B' becomes the new top element.

In Python, we use `append()`

to push an element and `pop()`

without an index to extract an element from the stack.

A situation to consider is when there are no elements to pop. In this instance, the `pop()`

will throw an `IndexError`

. To prevent this, we should check if the Stack is empty before popping elements. This circumstance, where there's nothing left to pop, is referred to as Stack Underflow. Conversely, if the Stack has reached its maximum capacity and we try to add an item to it, it's referred to as Stack Overflow.

Let's unravel the complexities (pun intended!) behind these Stack operations. All three of our basic operations — `push`

, `pop`

, and `peek`

— have a time complexity of $O(1)$, meaning they take constant time to complete, regardless of the size of the Stack.

Contrarily, the space complexity is $O(n)$, where $n$ is the number of elements in the Stack. This is considering an average scenario where elements are continually added and then removed from the Stack.

Now that we've understood the fundamentals of Stack and its operations, let's focus on manipulating our Stack to gain a deeper understanding. We'll start by emptying our Stack and then confirm that it is indeed empty.

In Python, we can check if the Stack is empty by checking its length. Let's create a helper function named `isEmpty(stack)`

:

Python`1def is_empty(stack): 2 return len(stack) == 0 3 4# Create an empty stack 5stack = [] 6 7print(is_empty(stack)) 8# Output: True; as our stack is currently empty 9 10stack.append('D') 11 12print(is_empty(stack)) 13# Output: False; now our stack has an element 'D' 14 15# Fetch the size of the stack 16stack_size = len(stack) 17 18print(f"Size of Stack: {stack_size}") 19# Output: 1; length of stack is 1`

The `is_empty`

function gives us `True`

if the Stack is empty and `False`

otherwise. The `len()`

function provides the size of the stack.

Having understood our Stack's manipulations, let's explore some real-world applications of Stacks. They are used widely in numerous areas, including parsing expressions, navigating browser history, implementing the "undo" operation in text editors, and more.

Consider this scenario: We have a text editor that stores the history of text changes. Our task is to use a Stack to implement the "undo" feature — when the user performs an "undo" operation, the text reverts to its previous version - the last historic change stored in the stack. Here’s an example:

Python`1# Create a stack to store text changes 2# The stack stores all historical versions of editor states, excluding the current state 3text_stack = [] 4 5# The user inputs text 6text_stack.append("Hello, world!") 7text_stack.append("Hello, CodeSignal!") 8 9print(text_stack) 10# Output: ["Hello, world!", "Hello, CodeSignal!"] 11 12# Check if the stack is empty before performing "undo" 13if not is_empty(text_stack): 14 # The user performs an "undo" operation 15 previous_text = text_stack.pop() # Retrieve the last historic change 16 print(f'After "undo", the text is: {previous_text}') 17else: 18 print("Cannot perform undo operation. There are no historic changes.") 19 20# Output: After "undo", the text is: Hello, CodeSignal!`

This example demonstrates how the Stack allows us to track the history of changes and to revert to a previous state when necessary.

That's a wrap, folks! We've journeyed far and wide in our exploration of Stacks. We understand that Stacks are both fun and straightforward, following the simple LIFO principle, just like a stack of plates. We've examined their implementation in Python using the `list`

data type and delved into their inner workings by understanding their time and space complexities.

In the end, we manipulated Stacks in Python and understood their application in a real-life scenario: implementing the "Undo" feature in a text editor. Rest assured, there's much more to Stacks, and as we delve deeper into data structures, you'll encounter more complex applications and operations revolving around Stacks.

Kudos for reaching this far! You've now ventured into a whole new knowledge arena in Python by comprehending the concept of Stacks. Up next, you'll find an assortment of practice exercises that will solidify your understanding of Stacks. The age-old saying goes, "Practice makes perfect", and these exercises are designed to help you apply the concepts you've learned in this lesson effectively. Let's get to it!