Stack
I've been doing a ton of writing this week on a conference talk that I'm hoping to present in the next month or two. This is taking the majority of my focus, but I still wanted to put out my regular posts, so I'm tackling some shorter data structures. This week it'll be the Stack and next week it'll be the Queue. And that conference talk will likely make its way here as a nice, lengthy blog post as well. Anyway, onto the Stack!
A Stack is a data collection that abides by a LIFO (Last In, First Out) principle. The last element added to the Stack will be the first element removed from the Stack.
You can visualize this data structure like a stack of plates that you are cleaning. You build the stack of plates by placing one on top of another. When you take a plate to clean it, you will take that plate from the top of the stack. In this way the last plate placed on the stack is the first plate that is removed from the stack that gets cleaned.
Structure and Terminology
Like with Singly and Doubly Linked Lists, a single item of data within a Stack is called a node
. A node
in a Stack stores two things:
- A Value (I will use
val
), which is the thing you want stored - A Previous reference (I will use
prev
), which is the pointer that references the previous item in the Stack
As for the Stack itself, it will always keep track of a first
property, which is the very first item inserted into the Stack, a last
property, which is the last item inserted into the Stack, and the size
, which is the number of items in the Stack.
So what does a Stack look like? Here's a very basic example:
1000 -> 100 -> 10 -> 1
In this little example, each number represents a node
within the Stack. For each node
, the number represents that node
's value, and the arrow represents the prev
pointer reference.
1000
would be the last
item in the Stack, and the first item to be removed from the Stack. 1000
's prev
would reference 100
, 100
's prev
would reference 10
, and 10
's prev
would reference 1
. 1
would be the first
item in the Stack, making it the first item that was inserted into the Stack as well as the last item that would be removed from the Stack. The size
of the Stack is 4
, as there are 4 items within the Stack.
Practical Examples of a Stack
So where might you encounter a Stack in real world programming? The call stack is one instance, which keeps track of where a script has run within a program. Another example might be the Undo/Redo functionality within Photoshop.
Big O of a Stack
Operation | Complexity |
---|---|
Insertion | O(1) |
Removal | O(1) |
Searching | O(n) |
Access | O(n) |
Insertion and Removal: Insertion and Removal happen in constant time because we are only working with the start or end of the list, wherever insertion/removal happens.
Searching and Access: Searching and Access are not very efficient in a Stack. Remember that we are concerned with one data item at a time, the item on the top of the Stack (technically, the item that was most recently inserted into the Stack). If the thing we are looking for is at the bottom of Stack (or the first item inserted into the Stack), then we need to remove everything else within the Stack first, giving us a Big O of O(n)
.
Implementing a Stack with an Array
Some programming languages have their own Stack implementation. Unfortunately, JavaScript is not one of those languages. We can (and will) implement a Stack on our own, but we can also use a plain old Array with built-in array methods to mimic Stack-like behavior.
Array Implementation #1: .push()
and .pop()
One way that we can implement a Stack using an Array is with the .push()
and .pop()
methods, which add and remove items at the end of the array.
var stack = [];
// Add things instack.push("Joey");stack.push("Sebastian");stack.push("Bart");
// Items have been stacked one after another, newest item lastconsole.log(stack);// ["Joey", "Sebastian", "Bart"]
// "Peek" at the last item insertedconsole.log(stack.at(-1)); // "Bart"
// Check if the "Stack" is emptyconsole.log(!stack.length); // false
// Remove items - Last In, First Out (LIFO)stack.pop(); // "Bart";stack.pop(); // "Sebastian"stack.pop(); // "Joey"
I want to call out the stack.at(-1)
line of code. The .at()
method is new in JavaScript and not fully adopted by all modern browsers. It's an alternative way of accessing items in an Array (or a String), as opposed to bracket notation. I will likely write a blog post about this new method, but here I'm leveraging one of its useful features: the ability to use negative values to access items from the end of the Array. stack.at(-1)
is simply a way of accessing the item at the end of the array, and achieves the same thing as stack[stack.length - 1]
.
I'll also discuss the !stack.length
line. This simply coerces the value of stack.length
to its opposite Boolean. Remember that 0
is falsy, so if stack.size
is 0
, then stack.size
will coerce to false
, but the single !
operator will return its opposite value, true
. If stack.size
is any other value it will evaluate to true
, and the single !
operator will return its opposite value, false
, indicating that there are items within the array. This is the same as the following:
if (stack.length === 0) { return true;} else { return false;}
Array Implementation #2: .unshift()
and .shift()
The second way that we can implement Stack-like behavior using an Array is with the .unshift()
and .shift()
methods, which add and remove items at the beginning of the array.
var stack = [];
// Add things instack.unshift("Joey");stack.unshift("Sebastian");stack.unshift("Bart");
// Items have been stacked one after another, newest item firstconsole.log(stack);// ["Bart", "Sebastian", "Joey"]
// "Peek" at the last item insertedconsole.log(stack.at(0)); // "Bart"
// Check if the "Stack" is emptyconsole.log(!stack.length); // false
// Remove items - Last In, First Out (LIFO)stack.shift(); // "Bart"stack.shift(); // "Sebastian"stack.shift(); // "Joey"
This is a good place to offer a gentle reminder that adding and removing stuff at the beginning of an Array is inefficient due to re-indexing subsequent items. This makes the .push()
/.pop()
method far more efficient than .unshift()
/.shift()
.
A final note in this section is that this simply demonstrates the ability to use the built-in Array structure to achieve Stack-like behavior and treatment of data. You also could use one of our Linked List implementations as well. There are major performance considerations to think about if any of these structures are actually going to get used in a real-world application, and this gets into territory that is well beyond the scope of this blog.
Stack Implementation
I'd also like to implement my own version of the Stack data structure, just like I did with the Singly and Doubly Linked Lists. Here's the full implementation for future reference. Sections below will detail each method.
Note that there are a lot of different ways to implement a Stack, and this is just one. The thought with this approach is to continue with the same methods I have used to build Singly Linked List and Doubly Linked List implementations, and to demonstrate the foundational concepts behind a Stack. If you want to do an Object- or Array-based implementation, then I highly encourage you to do so! I believe that no matter your approach, understanding the mechanics of these data structures and getting them to work makes you a stronger programmer.
class Node { constructor(val) { this.val = val; this.prev = null; }}
class Stack { constructor() { this.first = null; this.last = null; this.size = 0; } isEmpty() { return this.first ? false : true; } peek() { return this.first ? this.last.val : undefined; } pop() { if (!this.first) { return undefined; } var currentLast = this.last; if (this.size === 1) { this.first = null; this.last = null; } else { this.last = currentLast.prev; currentLast.prev = null; } this.size--; return currentLast.val; } push(val) { var newLast = new Node(val); if (!this.first) { this.first = newLast; this.last = this.first; } else { newLast.prev = this.last; this.last = newLast; } return ++this.size; }}
Node Class
Each item within the Stack is called a node
. Any time we add a node
into the Stack, we'll do it using a Node Class. This class accepts a value, val
, and also has a prev
property. By default prev
will be assigned the value of null
, but as new node
s get added to the Stack, the prev
on these new node
s will point to earlier items in the Stack.
class Node { constructor(val) { this.val = val; this.prev = null; }}
Stack List Class
We'll also use a Stack Class to initialize the Stack iteslf. We're starting here with just its constructor, but we'll add in all of the needed methods as we go. On initialization, an empty Stack will have a first
set to null
, a last
also set to null
, and a size
set to 0
.
class Stack { constructor() { this.first = null; this.last = null; this.size = 0; }}
Methods and Operations
.push()
The first method we'll introduce is the .push()
method, which adds a new node
to the Stack.
Here are the steps we take to create the .push()
method:
- Create a new
node
with the value,val
. I'll store this in a variable callednewLast
- Check if the Stack is empty. We'll know we have an empty Stack if the Stack has no
first
- If the Stack is empty:
- Assign the new
node
aslast
- Assign the
first
to also equallast
- Assign the new
- If the Stack isn't empty:
- Set the new
node
'sprev
to equal the currentlast
item in the Stack - Assign the new
node
as thelast
item in the Stack
- Set the new
- Increment the
size
by 1 - Return the
size
.push()
Implementation
push(val) { var newLast = new Node(val); if (!this.first) { this.first = newLast; this.last = this.first; } else { newLast.prev = this.last; this.last = newLast; } return ++this.size;}
In the very last line, return ++this.size
, I'm using the ++
increment operator in a pre-increment fashion. This is done by pre-pending this.size
with ++
. This code simply increments this.size
before returning this.size
. It allows me to accomplish, in one line, the following:
this.size++;return this.size;
It's a small optimization. Read more about pre-incrementing in this Stack Overflow thread.
.pop()
Now let's look at the .pop()
method, which removes the last item in the Stack.
- Return
undefined
if the Stack is empty (again, if there's nofirst
then it's an empty Stack) - Store the current
last
item into a variable namedcurrentLast
- Check whether there is only 1 item in the Stack
- If there's only 1 item in the Stack:
- Assign
first
the value ofnull
- Assign
last
the value ofnull
- Assign
- If there's more than 1 item in the Stack:
- Assign
last
the value ofcurrentLast
'sprev
- Assign
currentLast
'sprev
the value ofnull
, breaking the link between thisnode
and the Stack
- Assign
- Decrement
size
by 1 - Return the value of the
node
we have removed from the list (currentLast.val
)
.pop()
Implementation
pop() { if (!this.first) { return undefined; } var currentLast = this.last; if (this.size === 1) { this.first = null; this.last = null; } else { this.last = currentLast.prev; currentLast.prev = null; } this.size--; return currentLast.val;}
.peek()
In a Stack we're only concerned with the value of the last item in the Stack. Because of this, there is no .get()
method. Instead, our Access method is called .peek()
.
- Check if the Stack is empty
- If the Stack is empty:
- Return
undefined
- Return
- If the Stack isn't empty:
- Return (but do not modify or remove) the value of the
last
item in the Stack
- Return (but do not modify or remove) the value of the
.peek()
Implementation
peek() { if (!this.first) { return undefined; } else { return this.last.val; }}
This code works just fine, but considering we're only returning one of two things based off the condition of one thing existing, this little bit of code can get cleaned up with the help of a Ternary Operator.
peek() { return this.first ? this.last.val : undefined;}
This code does the same thing as the if/else
statement above. It checks whether this.first
evaluates to true
. If so, it will return this.last.val
. Otherwise, if this.first
evaluates to false
, it will return undefined
.
.isEmpty()
The last method in our Stack implementation is the .isEmpty()
method, which returns true
if the Stack is empty and false
if the Stack is not empty.
- Check whether there are any items in the Stack
- If there aren't items in the Stack, return
true
- If there are items in the Stack, return
false
Similar to the .peek()
method, we could use an if/else
statement here, but I'll go for another Ternary Operator statement to keep things consistent.
.isEmpty()
Implementation
isEmpty() { return this.first ? false : true;}