Stack, queue and linked list
Problem
"Undo" undoes the wrong thing
A text editor keeps the actions in a array. Ctrl+Z should undo the last one, but it undoes the first:
const actions = [];actions.push("Typed text");actions.push("Made the title bold");actions.push("Inserted a photo");
// Ctrl+Z: undo the last action.const undone = actions.shift();console.log(`Undone: ${undone}`);console.log("Left:", actions);Undone: Typed textLeft: [ 'Made the title bold', 'Inserted a photo' ]The fix is actions.pop(). But everyone who touches this code has to remember which end to remove from. And at the store checkout it is the other way around: the first one who came is served first. Is there a collection that knows the end by itself?
The idea
Stack: the last one on top
A stack is like a pile of plates. You put on top and you take from the top. The last thing put in comes out first. This is called LIFO: last in, first out.
The "Back" button in the browser is a stack:
const history = [];history.push("Home");history.push("Laptops");history.push("14-inch laptop");console.log(`Now: ${history.at(-1)}`);
history.pop();console.log(`Back: ${history.at(-1)}`);console.log(history);Now: 14-inch laptopBack: Laptops[ 'Home', 'Laptops' ]JavaScript has no separate class for a stack: an array is a stack if you put and take only at the end.
pushputs on top,pop()removes and returns the top one, andat(-1)only looks at the top one.- The top is the end of the array. Both operations are fast, because nothing moves at the end.
console.logprints the stack from the bottom up: the top one is last.
More precisely, if you are curious
pop()andat(-1)on an empty array do not stop the program. They giveundefined:
const history = [];console.log(history.pop());console.log(history.at(-1));undefinedundefinedSo check history.length before you take.
Try it
What is left to undo?
What will the program print?
const undo = [];undo.push("font");undo.push("color");undo.push("size");undo.pop();undo.push("underline");while (undo.length > 0) { console.log(undo.pop());}pop removes "size", because it is on top. Then "underline" goes on top. The loop takes them out from the top down: underline, color, font.
The idea
Queue: first come, first served
A queue is like the line at the checkout. You join at the back, and the first one is served. This is called FIFO: first in, first out.
Print jobs wait in a queue:
const printer = [];printer.push("invoice.pdf");printer.push("contract.docx");printer.push("photo.jpg");console.log(`Next: ${printer[0]}`);
while (printer.length > 0) { console.log(`Printing ${printer.shift()}`);}console.log("Queue:", printer);Next: invoice.pdfPrinting invoice.pdfPrinting contract.docxPrinting photo.jpgQueue: []JavaScript has no separate class for a queue. An array becomes a queue if you put at the back and take from the front:
pushputs at the back,shiftremoves and returns the first one, andprinter[0]looks at the first one.shifton an empty array givesundefined. It does not stop the program.
But shift has a cost. It moves all the others one place forward, like inserting at the front in the lesson on arrays. With a few print jobs you do not notice it. With a queue of tens of thousands of elements, it is many times slower than just reading in order. That is when you use the linked list from the next card, which keeps both the first and the last node.
More precisely, if you are curious
shifton an empty queue:
const clients = ["Customer 1"];console.log(`Served: ${clients.shift()}`);console.log(clients.shift());Served: Customer 1undefined- The V8 engine sometimes makes
shiftfast by moving the start of the array instead of moving the elements. With big arrays this does not happen. Do not rely on it.
Try it
Next at the checkout
The cashier calls the next customer. Which method takes them from the queue?
const checkout = [];
checkout.push("Ivan");
checkout.push("Maria");
const client = checkout.();
console.log(`Your turn, ${client}`);shift removes and returns the first one in the queue, here Ivan. checkout[0] would only look, without removing him, and next time it would be Ivan again. And pop() would take the last one, Maria.
See it
A linked list inside
An array keeps the elements next to each other in an array. That is why inserting at the front moves all the others. There is another way: each element is in a separate object that holds the value and an arrow to the next one. An object like this is called a node:
class Node { constructor(value, next) { this.value = value; this.next = next; }}A list of nodes is called a linked list. It is enough to keep the first node, head. You find each next one by following the next arrow. The last one points to null.
let head = null;head = new Node("Coffee", head);head = new Node("Milk", head);head = new Node("Bread", head);
for (let n = head; n !== null; n = n.next) { console.log(n.value);}Step by step, in words
- The list is empty: head points to null.
- A new node: next is the old head (null), and head moves to the node.
- The second node points to the first. Nothing moves in memory: only the arrows change.
- The third node also goes in at the front: the last one added is first.
- n starts at head and prints each node.
- n reaches null: the loop stops.
- Inserting at the front: a new node whose
nextpoints to the old first one, andheadpoints to the new one. Two arrows, and you move nothing. - Looping: you start at
headand follownextuntil you reachnull. - The cost: nodes have no indexes. You reach the fifth element by going through the first four.
The idea
The array, and which one when
JavaScript has no ready-made linked list. An array can add and remove at both ends: push and pop at the back, unshift and shift at the front:
const route = [];route.push("Plovdiv");route.push("Stara Zagora");route.unshift("Sofia");route.push("Burgas");console.log(route);
route.splice(route.indexOf("Stara Zagora"), 1);console.log("After the change:", route);console.log(`From ${route[0]} to ${route.at(-1)}`);[ 'Sofia', 'Plovdiv', 'Stara Zagora', 'Burgas' ]After the change: [ 'Sofia', 'Plovdiv', 'Burgas' ]From Sofia to BurgasWhich one when:
- A list by number: an array. This is the usual choice.
- A stack: an array with
pushandpop. - A queue: an array with
pushandshift, while it is small. A big queue with thousands of elements: a linked list like the one from the previous card, which also keeps the last node.
More precisely, if you are curious
unshiftis likeshift: it moves all the elements by one place, so it is slow at the front of a big array.- Why is an array usually faster than a linked list, even at inserting? Each node is a separate object somewhere in memory, and the processor reads data fastest when it sits side by side. An array is exactly that.
- The node from this card has only
next. A list like that is called singly linked.
Try it
Find the bug
The function should count the nodes. For the list Bread → Milk → Coffee it returns 2 instead of 3. And for an empty list it stops with TypeError. Which line has the bug?
Pick the line with the bug.
n.next !== null stops when n is on the last node, before counting it. And if head is null, there is nowhere to read n.next from. The condition should be n !== null: go on while there is a node.
The message for an empty list is TypeError: Cannot read properties of null (reading 'next'): null has no next.
Exercise
Stacks and queues at work
# in front.1. A toy train. A Train class with the methods addWagon(wagon) and removeWagon(). The train is a stack: the last car coupled is the first one uncoupled. removeWagon() returns the uncoupled car, or null if there is none. Add count(). Couple three cars and uncouple two.
Uncoupled: Car 3Uncoupled: Car 2Cars: 12. A bus route. A BusRoute class with a queue of stops and the methods addStop(stop) and getNextStop(), which returns the next stop, or null if the route is over. Add "Central Station", "NDK" and "Studentski Grad", and go through the route.
Next stop: Central StationNext stop: NDKNext stop: Studentski GradEnd of the route.3. A stock ticker. A StockTicker class keeps the last N prices of one stock. The constructor gets the symbol and N. updatePrice(price) adds a price, and if there are more than N prices, it removes the oldest one. Add getLastPrice() and average() for the stored prices. For SAP with N = 3, pass the prices 150, 152, 149 and 155. Hint: at(-1) is the last price. Print the prices with two digits after the decimal point, with toFixed(2).
SAP: last price €155.00Average of the last 3: €152.004. Balanced brackets. A code editor checks whether the brackets (), [] and {} are closed in the right order. Write isBalanced(text) with a stack: at an opening bracket you put it on the stack, and at a closing one you remove the top one and check that it is the same kind. At the end the stack must be empty. Read three lines.
(1 + [2 * 3])(1 + 2]((5)(1 + [2 * 3]) -> yes(1 + 2] -> no((5) -> no5. Your own linked list. A MyLinkedList class with the nodes from the lesson and the methods add(value) (at the end), remove(value), contains(value), size() and toString(). In remove, find the node before the one you are looking for and redirect its next. Check the case where the first one is removed separately.
[Invoice, Report, Meeting]Contains Report: trueAfter removal: [Invoice, Meeting]Size: 2More tasks
- Undo and Redo. Keep two stacks:
undoandredo. A new action goes intoundoand clearsredo. "Undo" moves the top one fromundotoredo, and "Redo" moves it back. - Words in reverse. Read a sentence and print it with the words in reverse order, going through a stack: "Today is Friday" becomes "Friday is Today".
- Two checkouts. Customers come one by one and go to the checkout with the shorter line. Print who was served at which checkout.
- Reverse the linked list. Add a
reverse()method toMyLinkedListthat turns the arrows around without making new nodes.
- A fast queue. Add a field for the last node to
MyLinkedList, so thataddat the end is fast too. Now it is a queue withoutshift:addat the back, removing the first one at the front.
Test