Chapter 16: An Introduction to Data Structures
Chapter Goals
- To be able to write programs with standard lists
and iterators
- To understand the advantages and disadvantages of
the list data structure
- To be able to implement linked lists
- To learn about stacks and queues
Linked Lists
-
Suppose you are maintaining a vector of sorted
objects (such as employees)
- Many elements will need to be shifted back if
an new object is inserted in the middle.
- Many elements will need to be shifted forward
if an object is deleted from the middle.
- Moving a large number of records can involve a
substantial amount of computer time - O(n) time.
- Rather than store a sequence of values in one
long block of memory (like a vector or an array) a linked list stores
each value in its own memory block, together with the locations of the
neighboring blocks in the sequence.
- Inserting an element into a list now requires no
shifting, merely reassigning new locations to adjacent objects - O(1) time.
- Removing an element from the list doesn't require
shifts either - O(1) time.
-
The standard C++ library has an implementation of
the linked list structure.
-
First we will learn how to use the standard
list.
- Later we will find out how to implement lists.
- Doubly linked
lists (as shown in the
illustrations) have links going in both directions.
-
Singly linked
lists only link elements in one
direction.
- Just like the vector, the standard list is a
template.
- You can use push_back to add elements
to the list.
list<string> staff;
staff.push_back("Cracker, Carl");
staff.push_back("Hacker, Harry");
staff.push_back("Lam, Larry");
staff.push_back("Sandman, Susan");
- You cannot
directly access elements using
subscript access (e.g. staff[3]) - the values are not stored
in one contiguous block in memory and there is no fast way to access
them.
-
Instead you must start at the beginning of the
list, and visit each element in turn using a list iterator.
An iterator marks a position
in the list.
list<string>::iterator pos;
pos = staff.begin();
- To move an iterator forward in the list, use the ++
operator.
pos++;
- To move an iterator backward in the list, use the
-- operator.
pos--;
- You can find the value that is stored in the
position with the * operator.
string value = *pos;
- The value *pos represent the value that
is stored in the list.
*pos = "Van Dyck, Vicki"; // assign a value
pos = staff.begin(); // assign a position
- To insert another string before the iterator
position, use the insert function.
staff.insert(pos, "Reindeer, Rudolph");
- Each list has an end position that does not
correspond to any value in the list but that points past the list's
end.
pos = staff.end(); /* points past the end of the list */
staff.insert(pos, "Yaglov, Yvonne");
/* insert past the end of list */
- The end position does not point to any value, so
you cannot look up the value at that position.
string value = *(staff.end()); /* ERROR */
- Compare to accessing s[10] in a vector
with 10 elements.
- The end position is useful for stopping a
traversal of the list.
pos = staff.begin();
while (pos != staff.end())
{ cout << *pos << "\n";
pos++;
}
- The traversal can be described more concisely
with a for loop:
for (pos = staff.begin(); pos != staff.end(); pos++)
cout << *pos << "\n";
- Compare this to a traversal of a vector.
for (i = 0; i < s.size(); i++)
cout << s[i] << "\n";
- To remove an element from a list, you move an
iterator to the position you want to remove, then call the erase
function.
pos = staff.begin();
pos++;
staff.erase(pos);
Implementing Linked Lists (The Classes for Lists, Nodes, and
Iterators)
- We will start with a linked list of strings.
- A linked list stores each value in a separate
object called a node.
class Node {
public:
Node(string s);
private:
string data;
Node* previous;
Node* next;
friend class List;
friend class Iterator;
};
- The friend declarations indicate the List
and Iterator member functions are allowed to inspect and
modify the data members of the Node class.
- A class should not grant friendship to another
class lightly, because it breaks the privacy protection.
- The list object holds the locations of the first
and last nodes in the list.
class List {
public:
List();
void push_back(string s);
void insert(Iterator pos, string s);
Iterator erase(Iterator pos);
Iterator begin();
Iterator end();
private:
Node* first;
Node* last;
};
- When the list is empty the first and last
pointers are NULL.
- A list object stores no data; it just knows where
to find the node objects that store the list contents.
-
An iterator denotes a position in the list.
class Iterator {
public:
Iterator();
string get() const; /* use instead of * */
void next(); /* use instead of ++ */
void previous(); /* use instead of -- */
bool equals(Iterator b) const; /* use instead of == */
private:
Node* position;
Node* last;
friend class List;
};
- We will enable the operators ++, --,
* and == in the next chapter.
- If the iterator points past the end of the list,
then the position pointer is NULL.
-
The iterator stores a pointer to the last element
of the list, so that the previous
method can move the iterator back
from the past-the-end position to the last element of the list.
Implementing Linked Lists (Implementing Iterators)
-
Iterators are created by the begin and end
functions of the List
class.
Iterator List::begin() {
Iterator iter;
iter.position = first;
iter.last = last;
return iter;
}
Iterator List::end() {
Iterator iter;
iter.position = NULL;
iter.last = last;
return iter;
}
-
The next
function advances the iterator to the
next position.
- Note that it is illegal to advance the iterator
once it is in the past-the-end position.
void Iterator::next()
{ assert(position != NULL)
position = position->next;
}
-
The previous
function is similar.
- When the iterator points to the first element in
the list, it is illegal to move it further backward.
void Iterator::previous()
{ if (position == NULL) position = last;
else position = position->previous;
assert(position != NULL);
}
-
The get
function simply returns the data value of
the node to which position points.
string Iterator::get() const
{ assert(position != NULL);
return position->data;
}
-
The equals
function compares two position
pointers.
bool Iterator::equals(Iterator b) const
{ return position == b.position; }
Implementing Linked Lists (Implementing Insertion and Removal)
- For the push_back function, we must
first make a new node.
Node* newnode = new Node(s);
- The node must point back to the old end of the
list, while the old end must point to it.
newnode->previous = last; // (1)
last->next = newnode; // (2)
last = newnode; // (3) !
- There is a special case when last is NULL
(the list is empty).
if (last == NULL)
{ first = newnode;
last = newnode;
}
- The complete member-function:
void List::push_back(string s)
{ Node* newnode = new Node(s);
if (last == NULL) /* list is empty */
{ first = newnode;
last = newnode;
}
else
{ newnode->previous = last;
last->next = newnode;
last = newnode;
}
}
- To insert
an element in the middle of the list
requires manipulating both elements on either side of the new node.
- There is a special case when the list is empty.
if (iter.position == NULL)
{ push_back(s);
return;
}
- Otherwise we create pointers to track the
surrounding nodes.
Node* after = iter.position;
Node* before = after->previous;
- We tell the new node where it belongs in the
list.
newnode->previous = before; // (1)
newnode->next = after; // (2)
- Then update the surrounding nodes.
after->previous = newnode; // (3)
if (before == NULL) /* insert at beginning */
first = newnode;
else
before->next = newnode; // (4)
- When know that after is not NULL,
but before could be.
- As with insertion, we will need to work with both
the element before and after the one to be removed.
Of course we cannot remove a node that is not there.
assert(iter.position != NULL)
We create pointers to track all three nodes we need
to work with.
Node* remove = iter.position;
Node* before = remove->previous;
Node* after = remove->next;
We disconnect the node to be removed from the one
before it; note the special case when we delete from the front of the
list.
if (remove == first) first = after;
else before->next = after;
We repeat the process with the node after the one to
delete.
if (remove == last) last = before;
else after->previous = before;
Finally we delete the node.
delete remove;
Implementing Linked Lists (list2.cpp)
Stacks and Queues
- Stacks and queues are special data types that
allow insertion and remove of items at the ends only, not in the
middle.
- A stack lets you insert and remove
elements at one end only,
traditionally called the top of the
stack.
- To visualize a stack, think of a stack of books.
- Since items can only be added or removed from the
top of the stack, they are removed in the order that is opposite from
the order they were added.
- This is called last in, first out or
LIFO order.
- The addition and removal operations are called push
and pop.
- The is a stack template in the standard
C++ library.
stack<string> s;
s.push("Tom");
s.push("Dick");
s.push("Harry");
while (s.size() > 0)
{ cout << s.top() << "\n";
s.pop();
}
- A queue
is similar to a stack, except that you
add items to one end of the
queue (the back) and remove them
from the other end of the
queue (the front).
- To visualize a queue, think of people lining up.
- Queues store items in a first in,
first out or FIFO fashion.
- The standard queue template implements a queue in
C++.
queue<string> q;
q.push("Tom");
q.push("Dick");
q.push("Harry");
while (q.size() > 0)
{ cout << q.front() << "\n";
q.pop();
}