Lesson 2

Hello learners, are you ready for another exciting session? Today, we are diving deeper into understanding how to operate **Stacks** in Python by solving problems that often appear in coding interviews or even real-world projects. Let's get ready to solve some Stack-based problems and enhance your problem-solving skills with stacks!

Let's start with a common coding challenge - checking if the brackets in a string are balanced. Imagine that the string is the source code of a software application, and these brackets are the open and close statements for loops, if-else conditions, or function blocks in the source code. For the code to be valid and runnable, every open statement (bracket) must have a corresponding close statement (bracket) in the proper order.

To make this problem more relatable, let's consider this real-world scenario. You are part of a team developing a text editor for programming languages. As a value-added feature, you want to provide real-time feedback to the users of your text editor about the number of unbalanced brackets in their code to assist them in avoiding syntax errors. This problem accurately mimics such a feature where we are given a string of code, and our task is to check if all the brackets in the code are balanced.

If we consider a simple way to approach this problem, we could initialize a counter variable for each type of bracket (parentheses, braces, and square brackets), increment the counters when we encounter an opening bracket, and decrement it when we get a closing bracket. Although this approach checks whether we have a closing bracket for every opening bracket, it completely misses one critical aspect - the order of brackets. For the brackets to be considered balanced, every closing bracket must correspond to the most recently opened bracket of the same type, which is not checked in this approach.

An efficient way to solve this problem is by using a *stack data structure*. The stack follows the *LIFO* (Last In, First Out) principle, which makes it highly suitable when we want to track the opening and closing brackets' order, as the most recently opened bracket needs to be closed first before we move on to the next opening bracket.

Let's break down the solution into simple steps:

We start by creating a dictionary that maps each opening bracket to its corresponding closing bracket and an empty stack. Then, we iterate over each character `character`

in the string `input_str`

:

- If
`character`

is an opening bracket, it gets appended to the stack. - If
`character`

is a closing bracket and the top element in the stack is the corresponding opening bracket, we remove the top element from the stack. - If neither of the above conditions is met, we return
`False`

.

Finally, if the stack is empty (all opening brackets had matching closing brackets), we return `True`

. If there are some unmatched opening brackets left, we return `False`

.

This way, the stack helps us keep track of all opening brackets and ensures that every one of them gets their closing mate.

Python`1def are_brackets_balanced(input_str): 2 brackets = set(["(", ")", "[", "]", "{", "}"]) 3 bracket_map = {"(": ")", "[": "]", "{": "}"} 4 open_par = set(["(", "[", "{"]) 5 stack = [] 6 7 for character in input_str: 8 if character not in brackets: 9 # Skipping non-bracket characters 10 continue 11 if character in open_par: 12 stack.append(character) 13 elif stack and character == bracket_map[stack[-1]]: 14 stack.pop() 15 else: 16 return False 17 return len(stack) == 0`

Continuing on to the next problem, we have the task of reversing the characters of a string using a *Stack*. This is quite a common task that you will often see in coding tests or interviews because it is a good demonstration of understanding the rules and principles of the Stack data structure.

Imagine you're tasked with building a function in which a user can input a string, and you need to display the reversed string as part of the application features. Or, as a more advanced example, in computer networks, stack buffers are often used to reverse the order of packets that arrive out of order. Understanding how to reverse the order of elements using a Stack is a crucial skill.

A direct approach for this problem is using built-in Python methods like string slicing on a list to revert it. But remember, the focus here is to integrate our understanding of the stack operations in solutions, so we will be oriented toward solving this problem using a Stack.

Thanks to the Last In First Out (LIFO) feature of the stack, it serves as an excellent tool to reverse elements' order. The strategy here is straightforward: push all the characters to a stack and then pop them out. As a result, we get the reversed string.

In Python, we can easily simulate a Stack using a list. Here is how we do it:

Python`1def reverse_strng(input_str): 2 stack = list(input_str) 3 result = '' 4 5 while len(stack): 6 result += stack.pop() 7 return result`

The `list(input_str)`

breaks the string into characters and simulates a stack where each letter is stacked on top of the previous one. Then `result += stack.pop()`

pops out the characters from the top of the stack (which is the reversed order as they were put in) and appends them to the result string. In the end, we get the string in reverse order.

Now, let's move on to another classic algorithmic problem - evaluating postfix expressions. In simple terms, a postfix expression is an arithmetic expression where operators are placed after their operands. For example, the expression `2 3 +`

is a simple postfix expression, which equals 5 when evaluated.

You've been given a task at work to build a small calculator application. This calculator should be capable of evaluating postfix expressions, as this form of notation eliminates the need for parentheses to indicate the execution order. This problem perfectly fits into such a scenario where you're given a postfix expression as a string; your task is to evaluate the expression and return the result.

One might think of directly parsing the expression from left to right and performing the operations. However, this won't work because it ignores one fundamental aspect of postfix expressions – an operator applies to the most recently seen numbers that haven't been used yet. This basic understanding of postfix expression pushes us to think about a certain data structure that we've encountered before.

The evaluation of postfix expressions can be efficiently done using a *stack data structure*. The stack follows the *LIFO* (Last In, First Out) principle, which is fitting in this scenario because we process the most recently encountered yet unused numbers first.

The solution process is as follows:

We create an empty stack. Then, we iterate over each character `operand`

in the expression. If `operand`

is a number, we push it onto the stack. If `operand`

is an operator, we pop two numbers from the stack, perform the operation, and push the result back onto the stack. After we have processed all characters of the expression, the stack should contain exactly one element, the result of the expression.

Python`1def evaluate_postfix(expression): 2 stack = [] 3 for element in expression.split(' '): 4 if element.isdigit(): 5 stack.append(int(element)) 6 7 else: 8 operand2 = stack.pop() 9 operand1 = stack.pop() 10 11 if element == '+': stack.append(operand1 + operand2) 12 elif element == '-': stack.append(operand1 - operand2) 13 elif element == '*': stack.append(operand1 * operand2) 14 elif element == '/': stack.append(operand1 / operand2) 15 16 return stack[0]`

As you can see, the stack again helps us keep track of the numbers in the order needed for the postfix expression evaluation. In this manner, we're able to solve yet another algorithmic problem efficiently using a stack data structure.

Thank you for joining us on this journey of exploring Stacks and their operations. We have learned how the LIFO characteristic of Stacks can be leveraged to solve common problems like checking if brackets in a string are balanced, reversing a string, and sorting a list of integers. Believe me, these concepts are just the tip of the iceberg. Understanding these basic concepts serves as a stepping stone toward more advanced data structures and algorithms, so be sure to practice these operations until you're confident. Apply these concepts to solve problems in your surroundings because, without practice, theory fades away. With that in mind, let's meet on our exercise platform. Until then, happy coding!