Chapter 05 - Plate Tetris: Mastering Code Stacks with Graceful Simplicity

Simplicity Unleashed: How Stacks Turn Mundane Tasks into Elegant Solutions with Push, Pop, and Peek Magic

Chapter 05 - Plate Tetris: Mastering Code Stacks with Graceful Simplicity

Have you ever tried stacking up a bunch of plates in your kitchen, only to realize that the easiest one to pick up is the last you put down? Welcome to the world of stacks! In the programming universe, stacks are those simple yet powerful data structures that follow the Last In, First Out (LIFO) principle. Picture a literal stack of anything where the last thing you put on is the first one you can take away. That’s what stacks are all about.

Stacks have a few straightforward operations that define their behavior. Let’s kick things off with the push operation. Imagine you’re adding a coffee mug to your stack of plates. In programming, when you push an item onto the stack, you’re adding it to the top spot. Conversely, the pop operation is akin to removing that mug when you’re ready to use it. The peek function lets you sneak a peek at that top coffee mug without disturbing the stack – you’re just checking what’s up there, kind of like when you’re deciding which cup to put back if it’s not your favorite.

Implementing a stack in JavaScript can be as easy as pie. Let me show you how it can be done using arrays first. Although not the most memory-efficient way, implementing stacks with arrays is intuitive and straightforward. Here’s a simple go at it:

class Stack {
  constructor() {
    this.items = [];
  }

  push(element) {
    this.items.push(element);
  }

  pop() {
    if (this.isEmpty()) {
      throw new Error("Stack is empty.");
    }
    return this.items.pop();
  }

  peek() {
    if (this.isEmpty()) {
      throw new Error("Stack is empty.");
    }
    return this.items[this.items.length - 1];
  }

  isEmpty() {
    return this.items.length === 0;
  }

  size() {
    return this.items.length;
  }
}

const stack = new Stack();
stack.push(10);
stack.push(20);
console.log(stack.peek()); // 20
console.log(stack.pop()); // 20
console.log(stack.pop()); // 10
console.log(stack.isEmpty()); // true

For those who crave a bit more efficiency, particularly when it comes to memory and constant time complexity, we might want to use a linked list. This doesn’t rely on contiguous storage and can be more routinely used in environments with dynamic memory allocation.

Here’s how you might craft a stack using a singly linked list:

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class Stack {
  constructor() {
    this.top = null;
    this.count = 0;
  }

  push(element) {
    const node = new Node(element);
    if (!this.top) {
      this.top = node;
    } else {
      node.next = this.top;
      this.top = node;
    }
    this.count++;
  }

  pop() {
    if (this.isEmpty()) {
      throw new Error("Stack is empty.");
    }
    const value = this.top.value;
    this.top = this.top.next;
    this.count--;
    return value;
  }

  peek() {
    if (this.isEmpty()) {
      throw new Error("Stack is empty.");
    }
    return this.top.value;
  }

  isEmpty() {
    return this.count === 0;
  }

  size() {
    return this.count;
  }
}

const stack = new Stack();
stack.push(30);
stack.push(40);
console.log(stack.peek()); // 40
console.log(stack.pop()); // 40
console.log(stack.pop()); // 30
console.log(stack.isEmpty()); // true

Now let’s talk shop: real-world applications. You might be surprised how often this simple stack concept comes in handy. Picture you’re evaluating complex mathematical expressions. Those tend to have a problematic order of operations, right? With stacks, you can manage operators and operands, resolve them appropriately, and avoid going bonkers.

Here’s an example implementing expression evaluation for the infix notation:

function evaluatePostfix(expression) {
  const stack = new Stack();
  const tokens = expression.split(' ');

  tokens.forEach(token => {
    if (!isNaN(token)) {
      stack.push(parseInt(token, 10));
    } else {
      const operand2 = stack.pop();
      const operand1 = stack.pop();
      switch (token) {
        case '+': stack.push(operand1 + operand2); break;
        case '-': stack.push(operand1 - operand2); break;
        case '*': stack.push(operand1 * operand2); break;
        case '/': stack.push(operand1 / operand2); break;
      }
    }
  });

  return stack.pop();
}

console.log(evaluatePostfix("3 4 + 2 * 1 +")); // (3 + 4) * 2 + 1 = 15

I also need to nod at stacks’ instrumental role in recursion. Whenever you’re calling a function within a function (maybe even within another function), stacks help you keep track of all those active function calls. JavaScript’s call stack is busy managing how and when each function finishes and hands control back to its caller, keeping it all in orderly chaos.

So, while we may think of stacks as basic building blocks in the wild world of programming, never undervalue how they manage seemingly complex tasks with the grace and simplicity of an acrobat performing a straightforward act. Whether you’re stacking anything from your kitchen plates to building complex expressions or solving intricate recursive functions, remember that sometimes simplicity holds the key to mastering complexity—much like using a simple mug in precisely three lethal yet elegant operations: push, pop, and peek.