Stack, queue and linked list
In short: A stack gives back the last added element first (LIFO), a queue gives back the first added first (FIFO), and a linked list keeps each element in a node that points to the next.
Problem
"Undo" undoes the wrong thing
A text editor keeps the actions in a list. Ctrl+Z should undo the last one, but it undoes the first:
List<string> actions = new List<string>();actions.Add("Typed text");actions.Add("Made the title bold");actions.Add("Inserted a photo");
// Ctrl+Z: undo the last action.string undone = actions[0];actions.RemoveAt(0);Console.WriteLine("Undone: " + undone);Console.WriteLine("Left: " + string.Join(", ", actions));Undone: Typed textLeft: Made the title bold, Inserted a photoThe fix is actions[^1] and actions.RemoveAt(actions.Count - 1): RemoveAt does not return the removed element, so you read it first. 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:
Stack<string> history = new Stack<string>();history.Push("Home");history.Push("Laptops");history.Push("14-inch laptop");Console.WriteLine("Now: " + history.Peek());
history.Pop();Console.WriteLine("Back: " + history.Peek());Console.WriteLine(string.Join(", ", history));Now: 14-inch laptopBack: LaptopsLaptops, HomePushputs on top,Popremoves and returns the top one, andPeekonly looks at the top one.Stack<T>is inSystem.Collections.Generic, likeList<T>.string.Joingoes through the stack from the top down.
More precisely, if you are curious
PopandPeekon an empty stack stop the program.TryPeekdoes not: it returnsfalseif the stack is empty, and gives you the top element throughout:
Stack<string> history = new Stack<string>();if (history.TryPeek(out string page)){ Console.WriteLine("Now: " + page);}else{ Console.WriteLine("There is no previous page.");}history.Pop();Unhandled exception. System.InvalidOperationException: Stack empty.The program first prints "There is no previous page.", and it stops at Pop. For Pop there is also TryPop.
- C# also has an older
Stackclass with no angle brackets, inSystem.Collections. It keeps everything asobject. It works, but today you useStack<T>:
Stack stack = new Stack();stack.Push("Google");stack.Push("Apple");stack.Push("Microsoft");Console.WriteLine("On top: " + stack.Peek());while (stack.Count > 0){ Console.WriteLine("Popped: " + stack.Pop());}On top: MicrosoftPopped: MicrosoftPopped: ApplePopped: GoogleTry it
What is left to undo?
What will the program print?
Stack<string> undo = new Stack<string>();undo.Push("font");undo.Push("color");undo.Push("size");undo.Pop();undo.Push("underline");while (undo.Count > 0){ Console.WriteLine(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:
Queue<string> printer = new Queue<string>();printer.Enqueue("invoice.pdf");printer.Enqueue("contract.docx");printer.Enqueue("photo.jpg");Console.WriteLine("Next: " + printer.Peek());
while (printer.Count > 0){ Console.WriteLine("Printing " + printer.Dequeue());}Console.WriteLine("Queue: " + printer.Count);Next: invoice.pdfPrinting invoice.pdfPrinting contract.docxPrinting photo.jpgQueue: 0Enqueueputs at the back,Dequeueremoves and returns the first one, andPeeklooks at the first one.Queue<T>is inSystem.Collections.Generic.Countsays how many are waiting, so the loop asksCount > 0.DequeueandPeekon an empty queue stop the program.TryDequeueandTryPeekreturnfalse.
More precisely, if you are curious
- The empty queue:
Queue<string> clients = new Queue<string>();clients.Enqueue("Customer 1");Console.WriteLine("Served: " + clients.Dequeue());if (!clients.TryDequeue(out string next)){ Console.WriteLine("Nobody is waiting.");}clients.Dequeue();Unhandled exception. System.InvalidOperationException: Queue empty.TryDequeue says "Nobody is waiting.", and the last Dequeue stops the program.
- C# has no separate class for a queue with two ends.
LinkedList<T>can add and remove at both ends, so older examples use it as a queue:
LinkedList<string> clients = new LinkedList<string>();clients.AddLast("Customer 1");clients.AddLast("Customer 2");clients.AddFirst("Customer with an appointment");Console.WriteLine("Served: " + clients.First.Value);clients.RemoveFirst();Console.WriteLine("Last: " + clients.Last.Value);Console.WriteLine("Waiting: " + string.Join(", ", clients));Served: Customer with an appointmentLast: Customer 2Waiting: Customer 1, Customer 2Queue<string>acceptsnullas an element too. You tell an empty queue byCountor byTryDequeue, not bynull:
Queue<string> printer = new Queue<string>();printer.Enqueue(null);Console.WriteLine(printer.Count);1Try it
Next at the checkout
The cashier calls the next customer. Which method takes them from the queue?
Queue<string> checkout = new Queue<string>();
checkout.Enqueue("Ivan");
checkout.Enqueue("Maria");
string next = checkout.();
Console.WriteLine("Your turn, " + next);Dequeue removes and returns the first one in the queue, here Ivan. Peek would only look, without removing him, and next time it would be Ivan again.
See it
A linked list inside
List<T> 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{ public string Value; public Node Next;
public Node(string value, Node next) { Value = value; 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.
Node head = null;head = new Node("Coffee", head);head = new Node("Milk", head);head = new Node("Bread", head);
for (Node n = head; n != null; n = n.Next){ Console.WriteLine(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
LinkedList<T>, and which one when
C# has a ready-made linked list: LinkedList<T>. It adds at both ends with AddFirst and AddLast, removes by value with Remove, and gives you the first and the last node through First and Last:
LinkedList<string> route = new LinkedList<string>();route.AddLast("Plovdiv");route.AddLast("Stara Zagora");route.AddFirst("Sofia");route.AddLast("Burgas");Console.WriteLine(string.Join(", ", route));
route.Remove("Stara Zagora");Console.WriteLine("After the change: " + string.Join(", ", route));Console.WriteLine("From " + route.First.Value + " to " + route.Last.Value);Sofia, Plovdiv, Stara Zagora, BurgasAfter the change: Sofia, Plovdiv, BurgasFrom Sofia to BurgasLinkedList<T> has no indexes at all. You reach an element by number only through the nodes, so the compiler does not allow [ ]:
Console.WriteLine(route[1]);error CS0021: Cannot apply indexing with [] to an expression of type 'LinkedList<string>'Which one when:
- A list by number:
List<T>. This is the usual choice. - A stack:
Stack<T>. A queue:Queue<T>. LinkedList<T>: rarely. When you need both ends, or you often insert and remove next to a node you already have.
More precisely, if you are curious
- Why is
LinkedList<T>usually slower, even at inserting? Each node is a separate object somewhere in memory, and the processor reads data fastest when it sits side by side. The array behindList<T>,Stack<T>andQueue<T>is exactly that. - You can see the nodes of a
LinkedList<T>:Firstis aLinkedListNode<string>withValue,NextandPrevious. The arrows go both ways, soLastandRemoveLastare fast too:
LinkedList<string> route = new LinkedList<string>();route.AddLast("Sofia");route.AddLast("Plovdiv");route.AddLast("Burgas");LinkedListNode<string> second = route.First.Next;Console.WriteLine(second.Value);Console.WriteLine(second.Previous.Value + " <- " + second.Value + " -> " + second.Next.Value);Console.WriteLine(route.Last.Next == null);PlovdivSofia <- Plovdiv -> BurgasTrue- The node from the previous card has only
Next. A list like that is called singly linked.
Try it
Find the bug
The method should count the nodes. For the list Bread → Milk → Coffee it returns 2 instead of 3. And for an empty list it stops with NullReferenceException. 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 NullReferenceException: Object reference not set to an instance of an object.: null has no Next.
Exercise
Stacks and queues at work
private field of the class.1. A toy train. A Train class with the methods AddWagon(string 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 a property 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(string 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(double 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: a Queue<T> sees only its first element, so keep the last price in a separate field. Print the prices with F2.
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(string 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(string value) (at the end), Remove(string value), Contains(string value), a property Count 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.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Polymorphism · Interfaces · Exceptions · Lists
- Continues in: Hash codes and hash functions · Nested classes · Command · Space complexity
- Tasks on this topic: Collections