Stacks and Queues
Updated: 01 February 2024
Stacks and Queues
- Collections of objects
- Some simple operations: insert, remove, iterate, and test if empty
- Two ways we may modofy data:
- Stack - pop and push (LIFO)
- Queue - enqueue and dequeue (FIFO)
Modular Programming
For the purpose of these types of data structures we want to completely separate the details of implementation from the specific client that we want to use. We want to design our libraries so that clients can choose different implementations and allows us to focus on performance where we would like to
Bags, Queues, and Stacks
Stack
A stack stores some data, and should have the following interface:
Linked List
Considering the above, we can implement this using a few different methods. For the first implement this using a linked list:
In this case pushing and popping is a constant time operation
Array
And using an array:
Note that we have used the non-trivial implementation to demonstrate the general concept since in Javascript the builtin array type does everything we would need anyway. As a general note in array based implementations is that we need to consider that an array has a specific capacity that will need to be accounted for when pusing or popping elements. Even though this isn’t really something we need to consider in Javascript the implementation assumes a bit of a more general concept of how arrays work
Considerations
- Underflow: We probably want to throw if a client tries to pop from an empty stack
- Overflow: We can use a resizing array to handle this and not require the client to provide us with a capacity
- Loitering: Currently we still hold a reference to popped items in the stack, we can update our implementation to replace that reference in our array to better handle this
Resizing Arrays
To resize our array we will need to do some kind of copy every time we want to extend our array. Since copying an array is expensive we want to avoid having to do this too frequently. One strategy for choosing when to do this is called repeated doubling in which once we have filled the array we create a new array that is double the size of our old array and copy all the items into that
In this implementation the cost of inserting an item is N
for cases where we are inserting a single item but may be some power of 2 for cases where we are inseting an item that is in a position outside of the current array. Overall however this cost becomes something that we need to pay less frequently as N
grows and overall the cost of inserting the first N
items comes to be around 3N
. This idea of averaging out these costs is called the amortized cost
In the above implementation we do not resize shrink the array when the array is half full since we can end up in a situation where the consumer frequently pushes or pops value around this mark which will cause frequent array resizes and will be very expensive. A bit of a more general solution to this could be to wait until the array is a quarter full and then resize it to half the size which will help avoid some of this resizing
Tradeoffs
- Linked-list
- Every operation takes constant time in the worst case
- Extra time and space for dealing with links
- Rezing array
- Every operation takes a constant amortized time
- Less wasted space
Queue
The definition of a Queue looks very similar to the Stack but the enqueue and dequeue operations work slightly differently
Linked List
In a linked-list implementation we add items at the start of the list and remove them from the end
From an implementation standpoint we also need to keep track of the last
and first
item in the list
Iterators
An iterator is a data structure that implements the iterator and iterable interfaces in the respective language. This enables us to work with in loops and other language specific functionality
Javascript Definition
In Javascript, the relevant definitions look like so:
Range Iterator
For the purpose of our example, we can implement an iterator that will iterate over a range
Bag
In some cases we don’t particularly care about the sequencing but we just want t be able to add items and iterate throught the items that we added in any order - this is called a bag. The interface for how we can define a bag looks like such:
Applications
Lots of languages have the functionality that we discussed above but the reason you may want to implement your own is because these implementations try to address a lot of general cases but may not meet specific requirements that you require for your implementation, for example builtin library code may compromise between read and write operations for a list which may cause a read-heavy algorithm to be very slow unlike a more specialized implementation which can be designed to have more specific performance characteristics to the problem at hand
Dijkstra’s two stack algorithm
THe two stack algorithm for arithmetic operation can be used to evaluate infix expressions. The algorithm is as follows:
Given some input set of strings: ( 1 + ( 2 + 3 ) * ( 4 + 5 ) - 6 )
we can evaluate the result using the following process:
- Define two stacks
- Value stack
- Operator stack
- Iterate over all items
- If value - push onto value stack
- If Operator - push onto operator stack
- If left paren - ignore
- If right paren - pop two values, pop one operator, and apply the operator, put result onto value stack
The implementation can be seen below: