You are on page 1of 15

James S.

Directory: /home/plank/cs140/Notes/Stack-Queue
Lecture notes:
Last Modification: Mon Mar 12 17:00:19 EDT 2012
Time to look beneath the hood. While the standard template library is a great tool, it's important to understand how dynamic
data structures like lists and trees are actually implemented. We'll start with two simple data structures -- Stacks and Queues
-- and how we implement them without any help from the standard template library. We then demonstrate how to implement
doubly linked lists, which is how the STL list data structure is implemented.
One of the nice parts of the standard template library is that it works with all data types. We're going to pare it down, and
have our data structures only hold strings.

A stack is a "LIFO" -- "Last in, first out" data structure. It supports two main operations:
void Push(string s): The adds a string to the stack.
string Pop(): This returns the most recently added string.
The full stack API is in stack.h, which contains class definitions:
#include <string>
class Stacknode {
string s;
Stacknode *next;
class Stack {
int Empty();
void Push(string s);
string Pop();
Stacknode *top;

Ignore the "Stacknode" stuff for now. Just concentrate on the public methods for the stack. Besides the Push() and Pop()
methods, there is an Empty() method, plus a constructor and a destructor.
A simple application that uses a stack is a program that reverses standard input -- you read each line and push it onto a stack,
and then pop each string from the stack, printing as you go. The code is in stackrev.cpp:
#include "stack.h"
#include <iostream>
using namespace std;
string s;
Stack st;

while (!st. newnode->s = s.while (getline(cin. The very first Stacknode will have next equal NULL.Empty()) cout << st.Push(s). We'll go through each step: . Implementation Now let's look at the implementation. st. newnode->next = top. } What this does is create a new node with the given string. The top of the stack will be a pointer to last Stacknode that was pushed. and then each Stacknode will point the previously pushed node with its next pointer. s)) st. Each value that gets stored on the stack is going to be stored in a single instance of the Stacknode class. st. } It's nice when a data structure so naturally fits an application. newnode = new Stacknode. Our stack is going to look as follows: The reason that we have this structure is that it facilitates adding new Stacknode's via Push(). Here's the code (in stack. top = newnode.Push("Him"). and hook that node on to the top of the stack.cpp): void Stack::Push(string s) { Stacknode *newnode. st. That's some clean code.Pop() << endl. and removing them via Pop(). For example. Consider calling Push("Now") on the stack above.Push("Give"). suppose we do: Stack st. Let's consider Push().Push("Six").

rv = oldtop->s. remove that node from the stack. and we are left with a stack that has "Now" on top: Pop() on the other hand. } oldtop = top. return rv. if (top == NULL) { cerr << "Error: pop() called on an empty stack\n".When Push() returns. and return the stored string. delete oldtop. Stacknode *oldtop. Since Push() called new. top = oldtop->next. Here's the code: string Stack::Pop() { string rv. exit(1). Otherwise. Pop() must call delete. } And below we step through Pop() called on the original three-node stack: . our stack implementation will have a memory leak. needs to store the string that is on the first node. the variable newnode goes away.

With the exception of the destructor. It is called whenever a Stack is deallocated. we return "Six" to the caller. and this is the easiest example.cpp g++ -c stack. delete top. It also happens when you return from a procedure in which you declared a Stack as a variable. You need to define a destructor which deletes all the nodes on the stack: Stack::~Stack() { Stacknode *next. and our stack only has two elements: Be sure you understand how these two methods work. This can happen if you allocate a Stack with new and then call delete.cpp g++ -o stackrev stackrev. } } I could have made this code simpler: Stack::~Stack() { while (!Empty()) Pop(). You need to understand it before you can move on. while (top != NULL) { next = top->next. and it is not performing the error checking that Pop() performs. } } The destructor is a little subtle. } However.o stack. then the nodes on the stack would constitute a memory leak. Destructors are necessary because you called new during your Push() calls. If the Stack goes away and no destructor were called. top = next. Understanding linked data structures is a fundamental concept.o UNIX> head -n 5 input.When we're done. return (top == NULL). because it is not making a bunch of method calls.txt . the remaining two methods are straightforward: Stack::Stack() int Stack::Empty() { { top = NULL. my code is more efficient. Go ahead and double-check that stackrev works: UNIX> make stackrev g++ -c stackrev.

h) is pretty much identical to the Stack API. ~Queue(). ask me about it in class. you point to the person behind you. int size. int Empty(). and you need to understand it before proceeding. protected: Qnode *first. Each Qnode has ptr. and you can delete the first node.Push("Six"). or cars waiting at a traffic light. q. rather than the newest.John Eighth Tristan Passenger Joshua Classroom Oliver Andrew Propagandist Tristan Tristan Hieratic UNIX> head -n 5 input. void Push(string s). or ask your TA about it during their office hours. Now you are going to keep a pointer to the first and last nodes on the queue. then both will be NULL. class Queue { public: Queue(). string Pop(). If it's still unclear. }. The anology here is that if you are in line.txt | stackrev Tristan Tristan Hieratic Oliver Andrew Propagandist Joshua Classroom Tristan Passenger John Eighth UNIX> If any of that Stack description was unclear to you. too. It can be used to mimic lines at the grocery store. The reason is that this way you can insert new nodes behind the last one.Push("Him").this is fundamental material. Qnode *ptr. q. . #include <string> using namespace std.Push("Give"). The protected data is different. Let's take an example like the stack example: Queue q. The Queue API (in queue. Qnode *last. If the queue is empty. A queue is a FIFO data structure: first-in. class Qnode { public: string s. I know I'm repeating myself -. go over it again. which points to the previous node on the queue. It works differently. not the person in front of you. }. though. because Pop() now removes the oldest element of the queue. first-out. q. int Size().

only we are removing the first node rather than the top node. if (size == 0) { cerr << "Error: pop() called on an empty queue\n". The code is nearly exactly like the stack code. and we have to put some extra code in to handle popping the last node on a queue. if (first == NULL) last = NULL. exit(1). we'll go over an example step by step: . } As with the stack code.This is going to result in the following layout of data: Let's first consider calling Pop(). return rv. first = oldfirst->ptr.cpp: string Queue::Pop() { Qnode *oldfirst. string rv. oldfirst = first. delete oldfirst. size--. } rv = first->s. The code is in queue.

newnode = new Qnode. Otherwise. However.Pop() then returns "Give". we differ by setting the new node's ptr to NULL. Then we have to do two different things depending on whether the queue was previously empty. we set the last element's ptr to the new node. If it was empty. and then set last to be the new node. the new node goes after the last element. } last = newnode. } Below is an example of calling Push("Now") on the two element list above: . } else { last->ptr = newnode. Here is the code: void Queue::Push(string s) { Qnode *newnode. As before. we create a new node with new and set its string. its local variables go away. In that way. and then resulting Queue is: The code for Push() is quite different from the stack code. if (last == NULL) { first = newnode. newnode->s = s. then we set first and last to the new node. size++. newnode->ptr = NULL.

} while (!q. and the resulting queue is: The rest of the code is straightforward -.h" #include <iostream> using namespace std. main() { string s.Pop(). we implement tail with a queue in can just click on queue. s)) { q. Queue q. Make special note of the destructor.Pop() << endl.cpp to see it all. } Verify to yourself that it works: UNIX> make queuetail .cpp: #include "queue.Empty()) cout << q.When it returns. while (getline(cin. newnode goes away. For our example. which is very much like the stack code.Push(s). if (q.Size() > 10) q.

}.cpp g++ -c queue. int Size(). Dnode *End(). int Empty(). Dnode *flink. string Pop_Back().h: #include <string> using namespace std. class Dlist { public: Dlist().cpp g++ -o queuetail queuetail. void Push_Front(string s). Our API for the doubly linked list is in dlist. ~Dlist().txt | queuetail 11 Brayden Clarinet 12 Nicholas Casteth 13 Hunter Hart 14 Ellie Bangle 15 Addison Fracture 16 Ava Hereafter 17 Evelyn Kigali 18 Tristan Housewives 19 Jack Bodybuilder 20 Gabriel Millionth UNIX> The most general-purpose list data structure is the doubly linked list. void Push_Back(string s). Dnode *blink. The nice features of doubly-linked lists are that they may be traversed in the forward and reverse direction.o queue. It is how the list part of the STL is implemented. and they allow for arbitrary deletion of any nodes. they allow for arbitrary insertion of nodes anywhere in the list.g++ -c queuetail.o UNIX> cat -n input.txt 1 John Eighth 2 Tristan Passenger 3 Joshua Classroom 4 Oliver Andrew Propagandist 5 Tristan Tristan Hieratic 6 Isabelle Bailey Prey 7 Ellie Alkaloid 8 Elijah Epithelium 9 Daniel Topgallant 10 Kate Surcease 11 Brayden Clarinet 12 Nicholas Casteth 13 Hunter Hart 14 Ellie Bangle 15 Addison Fracture 16 Ava Hereafter 17 Evelyn Kigali 18 Tristan Housewives 19 Jack Bodybuilder 20 Gabriel Millionth UNIX> cat -n input. . string Pop_Front(). class Dnode { public: string s. Dnode *Begin().

A pointer to the previous node on the list: blink (backward link).h" #include <iostream> using namespace std. Dlist l. and then traverses it using Begin() and End(): #include "dlist. in dlistrev-4. } Alternatively. The Begin() method returns a pointer to the first node on the list. s)) l. void Erase(Dnode *n). } That for loop looks very STL-like. A pointer to the next node on the list: flink (forward link). though. main() { string s. s)) l.Dnode *Rbegin(). In a similar manner. That's because I find operator overloading odious. The first works by calling Push_Back() and Pop_Back(). void Insert_Before(string s. except Begin() and End() don't return iterators. while (getline(cin.cpp and dlistrev-2.Push_Front(s). protected: Dnode *sentinel. All of these methods should be familiar to you. which creates the list using Push_Front(). d != l. d = d->blink) cout << d->s << endl. and the second works by calling Push_Front() and Pop_Front().Rend(). Dnode *d.cpp. for (d = l.cpp. int size.Rbegin(). Dlist l.Push_Back(s). while (getline(cin.Begin(). main() { string s. }. and we traverse it from back to front using Rbegin(). Dnode *d. as they are analogous to STL methods.cpp. d = d->flink) cout << d->s << endl. Dnode *Rend(). d != l. That's just like the STL. Dnode *n).cpp. To move from one node to the next. for (d = l.End(). in dlistrev-5. we use d->flink. They return a pointer to the list node. and I don't want to overload the ++ operator. which contains three fields: The string s. Dnode *n).h" #include <iostream> using namespace std. Rend() and d->blink: #include "dlist. the list is created with Push_Back(). and End() returns a pointer to one element past the last node on the list. A little more subtle is dlistrev-3. we create the list with Push_Front() and print it by repeatedly printing the first element and . void Insert_After(string s. There are two simple implementations of reversing standard input in dlistrev-1.

main() { string s.Insert_Before(s.Push_Front(s).h" #include <iostream> using namespace std. Secondly. Then our list looks as follows: . l. while (getline(cin. while (getline(cin. d = d->blink) { cout << d->s << endl. This may seem confusing. l. which may seem convoluted at first. We do this by inserting before End(). Dlist l.cpp. l.Empty()) { cout << l.h" #include <iostream> using namespace std. We then traverse the list from back to front. s)) l.then erasing it: #include "dlist.Rend(). #include "dlist. This is an extra node which begins each list. } } I'm not going to implement dlist.Push_Back("Give"). l. and the blink field of the first node also points to the sentinel. for (d = l.Begin()). s)) l. then it will contain size+1 nodes. We are going to have our lists contain a sentinel node. we use the Insert_Before() method. Dnode *d. so let's look at an example. Suppose we do: Dlist l. if a list contains size elements. which inserts a new node before the given node. d != l.Push_Back("Him"). This means that the flink field of the last node in the list points to the sentinel. main() { string s.End()). That's your job in lab. } } And finally. I will outline the structure. However.cpp for you. our list is going to be circular.Push_Back("Six").Rbegin(). in dlistrev-6. but will seem elegant and beautiful by the time you're done with it. to create the list in a manner equivalent to Push_Back(). So.Erase(l. l. while (!l.Begin()->s << endl. Dlist l.

Begin() is going to return sentinel->flink. a student's first response to this structure is. Study that code and the picture above until you understand it. Since sentinel->flink is equal to sentinel.Think about traversing such a list. and the loop will end.Insert_Before("On". For example: Dlist l. it is going to allocate a sentinel node and have its flink and blink pointers point to itself: That means you are going to have to call new in the constructor. l. and then "Six". That's just what you want! Suppose I want to insert a new node before a given node in the list.End() is going to return sentinel. the pointer d will be pointing to the sentinel. Thus. Consider the loop from dlistrev-3. Consider the previous for loop executing on the empty list.End(). d = d->flink. When the constructor for a list is called. then "Him".Push_Back("Come"). l. l. and l. "How do I keep from getting into an infinite loop when I'm traversing?" The answer is that you end when you reach the sentinel node. d = d->flink) cout << d->s << endl. the body of the loop is never executed. d = l. executing this loop on the list above will indeed print out "Give". d != l. Typically.cpp: for (d = l.Begin(). here's the state of the program: . d). l.Push_Back("Eileen"). At the point of the Insert_Before() statement.Begin(). Dnode *d. At the next iteration of the loop.

Isn't that convenient. you can implement each of Push_Back(). What will that do? That will effect Push_Back()! In fact. you can insert new nodes between the sentinel and the last node on the list. Push_Front(). and the list is as we want it: Since the list is circular. and Insert_After with Insert_Before(). the new node is hooked into the list: Insert_Before() returns to the caller. we'll have two (Dnode *)'s called prev and newnode. When we're done. We'll set prev to d->blink and set newnode equal to a newly allocated Dnode with the proper string: Now we can hook newnode into the list by appropriately setting its flink and blink pointers.In Insert_Before(). which means that newnode and prev go away. and by appropriately setting prev->flink and d->blink. .

Consider the following code: Dlist l.First. At the point of the Erase() call.Push_Back("Eileen"). Deleting other nodes is a straightforward affair.Push_Back("Come"). Dnode *d.Begin(). this time called prev and next. d = l. the state of the program is: As before. you'll be in a world of hurt.Push_Back("On"). If you do.Erase(d). l. We set them to the two nodes surrounding d: We can then remove d from the list by setting next's blink pointer and prev's flink pointer: . we'll have two (Dnode *)'s. in Erase(). l. much like insertion... l. you should never allow deletion of the sentinel. l. d = d->flink.

As with Insert_Before(). As with the Stack and Queue implementations. . That includes the sentinel node. you can use Erase() to implement other methods like Pop_Back() and Pop_Front(). as are prev and next. you should should take care not to use it. The variable d is now pointing to deleted memory.And calling delete on d (and decrementing size): When Erase() returns. Be careful when you implement it. the node is gone. the Dlist destructor has to deallocate all memory that has been allocated for the list.