Lesson 2

Today, we embark on a journey through the functioning of **stacks** and their practical applications in solving algorithmic problems. Like a pile of plates in a cafeteria, stacks allow us to add and remove data in a last-in, first-out manner. This lesson revolves around leveraging stacks to tackle two common algorithmic challenges.

Our first challenge involves ensuring that parentheses, braces, and brackets within a string are correctly matched — crucial for verifying syntax in programming languages and mathematical formulas.

Imagine devising a tool within a code editor that flags syntax errors by checking for mismatched brackets — a much-needed feature for developers to catch common errors early in the coding process.

In a naive approach, one might consider scanning the string and manually checking for matching closing brackets for each opening one. While this might initially seem straightforward, it fails to handle nested structures efficiently, leading to repeat scanning and potential (O(n^2)) performance in the worst-case scenario.

A stack, however, is ideally suited for this task. It acts as a memory model, helping track the last opened bracket and ensuring its closure before moving on to the prior ones.

Our JavaScript implementation lays out the process in a structured, step-by-step manner:

JavaScript`1function areBracketsBalanced(s) { 2 // Initialized stack, ready to track opening brackets 3 const stack = []; 4 // Bracket pairs for simple access and checking 5 const brackets = { '(': ')', '[': ']', '{': '}' }; 6 7 for (let char of s) { 8 if (brackets[char]) { 9 // Insert the expected closing bracket onto the stack 10 stack.push(brackets[char]); 11 } else { 12 // The stack's top is compared to the actual closing bracket 13 // A mismatch or early termination indicates an invalid string 14 if (stack.length === 0 || stack.pop() !== char) { 15 return false; 16 } 17 } 18 } 19 20 // The stack must be empty; otherwise, missing closings remain 21 return stack.length === 0; 22} 23 24 25console.log(areBracketsBalanced('{{()}}[]')) // true 26console.log(areBracketsBalanced('[(())}')) // false`

It returns false in three cases:

- If at any point we find a closing bracket and the stack is empty
- If at any point we find a closing bracket and the latest opening bracket doesn't match
- If at the end of the process we have any brackets left in the stack

Here, we explore reversing a string by utilizing a stack's LIFO property, a classic operation underlying numerous applications, including data encoding and genetic research.

Using a stack provides an easy mechanism to reverse text. Each character is pushed onto the stack, and as we pop these characters off, the string is naturally reversed — as if we're unloading a stack of blocks, revealing the last-placed block first.

The code below demonstrates this string-reversal algorithm:

JavaScript`1function reverseString(str) { 2 const stack = []; 3 4 // Push each character of the original string onto the stack 5 for (let char of str) { 6 stack.push(char); 7 } 8 9 // Generate the reversed string based on the LIFO principle 10 let reversedStr = ''; 11 while (stack.length) { 12 reversedStr += stack.pop(); 13 } 14 15 // Return the reversed string 16 return reversedStr; 17} 18 19 20console.log(reverseString("abcde")) // edcba`

Applying this function to the string "algorithm" would return "mhtirogla," demonstrating the method's efficacy and practicality in reversing a sequence.

This lesson explored actionable applications of stacks, leveraging their LIFO behavior to solve practical problems such as validating nested structures and manipulating strings. The ability to recall the most recent addition aids in building efficient algorithms that mirror real-world actions, like tidying a stack of documents in the correct order or unstacking packages for delivery.

It's time for you to practice with stacks, applying the concepts to new problem sets that reinforce what we've learned.