Chapter Goals
- To become familiar with the list, queue, and stack data types
- To understand the implementation of linked lists
- To understand the efficiency of vector and list operations
Linked Lists
- A data structure for collecting a sequence of objects
- Addition and removal of elements in the middle is efficient
- Consider maintaining a vector of sorted objects (such
as employees)
- Many elements will need to be shifted back if a new
object is inserted in the middle
- Many elements will need to be shifted forward if a
object is deleted from the middle
- Moving a large number of records can involve a substantial
amount of computer time
Linked Lists
- A linked list stores each value in its own memory block, together
with the locations of the neighboring blocks in the sequence
Linked Lists
- Inserting an element requires no shifting, merely reassigning new
locations to adjacent objects
Figure 2 Adding a Node to a
Linked List
Linked Lists
- Removing an element involves only constant work:
Figure 3 Removing a Node from
a Linked List
Linked Lists
- However, elements are not indexed (efficiently)
- Element access is slow
- Must walk the list to get a particular element
- Iterating through the list is no more costly
Linked Lists
std::list
- C++ STL has a doubly-linked list
- Singly-linked lists lack the predecessor links
- Like vector, list is a template
#include <list>
using namespace std;
list<string> names;
- Adding elements to the end of a list is straight-forward:
names.push_back("Tom");
names.push_back("Dick");
names.push_back("Harry");
Linked Lists
Iterators
- Cannot directly access elements using subscript access
(e.g. names[2])
- Instead, start at the beginning, then visit each element in turn
- list-iterator – marks a position in a
list
list<string>::iterator pos;
pos = names.begin();
- The ++ operator moves to the next element:
pos++;
- The -- operator moves an iterator backward:
pos--;
Linked Lists
- Use the * operator to find the value that is stored at that
position
- Remember, if pos "points to" an element, then
*pos is that element
- To read or change an element:
string value = *pos;
*pos = "Romeo";
// The list value at the position is changed
- Changing pos merely changes the current position:
pos = names.begin();
// The list value at the position is changed
Linked Lists
Insertion
- To insert an element before the iterator position, use
the insert function:
names.insert(pos, "Romeo");
- To insert at the beginning of the list:
pos = names.begin();
names.insert(pos, "Romeo");
- To insert at the end:
pos = names.end(); // Points "one past" the end
names.insert(pos, "Juliet");
- The end() iterator does not point at a valid object
Linked Lists
Traversal
- end() is useful for stopping a traversal:
pos = names.begin();
while (pos != names.end())
{
cout << *pos << "\n";
pos++;
}
- More concisely with a for loop:
for (pos = names.begin(); pos != names.end(); pos++)
cout << *pos << "\n";
- Very similar to a vector:
for (i = 0; i < s.size(); i++)
cout << s[i] << "\n";
Linked Lists
Removal
- To remove an element, move an iterator to the element:
pos = names.begin();
pos++;
- Call the erase() method:
names.erase(pos);
- pos now points to what was the 3rd element
Linked Lists – list1.cpp
Implementing Linked Lists
- Useful member functions for implementing a linked list:
- push_back
- insert
- erase
- The iterator operations
- A linked list stores each value in a separate object called
a node
- A Node contains pointers to previous and next nodes
Implementing Linked Lists
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 Node
- A class should not grant friendship to another class lightly,
because it breaks the privacy protection
Implementing Linked Lists
- A List holds the locations of the first and last
nodes:
class List
{
public:
List();
void push_back(string data);
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
- Stores no data; just knows where to find the node objects that store
the list contents
Implementing Linked Lists
- 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;
List* container;
friend class List;
};
- We will enable the operators ++, --, *
and == in the next chapter
Implementing Linked Lists
- If the iterator points past the end of the list, then
position is NULL
- If position is NULL, previous uses
container to find the last element
- Optionally, List could create and maintain a dummy node at
the end of the list
Implementing Iterators
- Iterators are created by begin and end methods of
the List:
Iterator List::begin()
{
Iterator iter;
iter.position = first;
iter.container = this;
return iter;
}
Iterator List::end()
{
Iterator iter;
iter.position = NULL;
iter.container = this;
return iter;
}
Implementing Iterators
- next() advances the iterator to the next position
- Just like the ++ operator
Figure 4 Advancing an Iterator
Implementing Iterators
void Iterator::next()
{
assert(position != NULL)
position = position->next;
}
- Illegal to advance the iterator once it
is in the past-the-end position
- Semantically, has no meaning
- The NULL pointer can't be dereferenced
Implementing Iterators
- The previous function is similar
- Just like the -- operator
void Iterator::previous()
{
assert(position != container->first);
if (position == NULL)
position = container->last;
else
position = position->previous;
}
- When the iterator points to the first element in the list,
it is illegal to move it further backward
Implementing Iterators
- get() returns the data value of the node
- Just like the * operator:
string Iterator::get() const
{
assert(position != NULL);
return position->data;
}
- equals() compares two position pointers
- Just like the == operator
bool Iterator::equals(Iterator b) const
{
return position == b.position;
}
Implementing Insertion and Removal
push_back()
Adds the new value to the end of the list
- First, make a new node:
Node* new_node = new Node(s);
- Add to end of list:
- Point next of last node to new node
- Set previous of new node to last node
- Update last to point to new node
- Special case when list is empty:
- New node is only node
- Point both first and last to new node
Implementing Insertion and Removal
Figure 5 Appending a Node to
the End of a Linked List
void List::push_back(string data)
{
Node* new_node = new Node(data);
if (last == NULL) // List is empty
{
first = new_node;
last = new_node;
}
else
{
new_node->previous = last;
last->next = new_node;
last = new_node;
}
}
Implementing Insertion and Removal
Inserting into the middle of a list
- 2 neighbors (maybe)
- More pointers to fix up
- Name the neighbors:
Node* after = iter.position;
Node* before = after->previous;
- Insert new node:
new_node->previous = before;
new_node->next = after;
- Update before and after:
after->previous = new_node;
before->next = new_node; // If before != NULL
Implementing Insertion and Removal
Special cases:
- after is NULL – call push_back()
- before is NULL – check before updating:
if (before = NULL) // Insert at beginning
first = new_node;
else
before->next = new_node;
Implementing Insertion and Removal
Figure 6 Inserting a Node into
a Linked List
Implementing Insertion and Removal
- We will need to work with before and after
Node* remove = iter.position;
Node* before = remove->previous;
Node* after = remove->next;
- Of course we cannot remove a node that is not there
assert(iter.position != NULL)
- Update before and after
- Note the special case when either is NULL
if (remove == first)
first = after;
else
before->next = after;
if (remove == last)
last = before;
else
after->previous = before;
Implementing Insertion and Removal
Figure 7 Removing a Node from
a Linked List
- Fix up the iterator to point to the next element
- Delete the removed node!
Implementing Insertion and Removal – list2.cpp
The Efficiency of List and Vector Operations
We will analyze and compare the efficiency of fundamental operations on
linked lists and vectors
These operations are:
- Getting the kth element
- Adding and removing an element at a given position
- Adding and removing an element at the end
The Efficiency of List and Vector Operations
To find the kth element of a linked list:
- Start at the beginning
- Advance k times
- Advancing the iterator is a constant-time operation, so
- Finding the kth element is an O(k)
operation
The Efficiency of List and Vector Operations
vector has the following attributes:
- buffer – pointer to an array of elements
- capacity – max size of current buffer (buffer can be
resized)
- size – # of elements actually being stored
- size is also the next available location
Figure 8 Internal Data Fields
Maintained by Vector
The Efficiency of List and Vector Operations
- When the kth element is accessed, the vector
simply uses buffer[k]
- Arrays have constant-time access
- Lookup time is independent of k
- So, accessing a vector element takes O(1) time
The Efficiency of List and Vector Operations
Modifying the Middle
To add an element in the middle of a list (the cost of
finding the location is considered separately):
- An element is added by modifying the previous and
next pointers of the new and surrounding nodes (see Fig. 6)
- Constant work done, regardless of position
- Same holds for removing an element
- So, insertion and removal are O(1) operations
The Efficiency of List and Vector Operations
To insert an element at position k of a
vector (if relative order matters):
- The elements with higher index values need to move down one spot
- On average, we move n/2 elements with each insertion
- When an element is removed, the "hole" must be filled in
- Vector insertion and removal are O(n) operations
Figure 9 Inserting and
Removing Vector Elements
The Efficiency of List and Vector Operations
- If size < capacity, adding to end is O(1)
- Otherwise, the vector must be resized first:
- Allocate a new buffer (typically, twice the size of the current
one)
- Copy all size (n) elements
- Delete old buffer
- Insert new element at end
- Resize is an O(n) operation
- But, not every insertion requires a resize
The Efficiency of List and Vector Operations
The cost of 1280 push_back() operations:
- Initial buffer size is 10
- Each reallocation doubles the size
- A single insertion costs T1
- So, we'll pay 1280 xT1 for the insertions
- Reallocating a vector with k elements costs k
T2
- Cost for all the reallocations:
10T2 + 20T2 + 40T2 + ... + 1280T2 = (1+2+4+...+128) x 10 x T2
= 255 x 10 x T2
< 256 x 10 x T2
= 1280 x 2 x T2
The Efficiency of List and Vector Operations
- So, the total cost is a bit less than
1280 x (T1 + 2 T2)
- More generally, the cost of n push_backs is less
than
n x (T1 + 2 T2)
- T1 + 2 T2 is a constant,
so
- n push_backs is O(n)
- The average cost of a single push_back does not
increase
- A particular push_back might be noticed
- We write O(1)+ or O(1*), for amortized analysis
The Efficiency of List and Vector Operations
Table 1 Execution Times for Container
Operations
| Operation |
Vector |
Linked List |
| Add/remove element at end |
O(1)+ |
O(1)♦ |
| Add/remove element in the middle |
O(n) |
O(1) |
| Get kth element |
O(1) |
O(k) |
♦ If a pointer to the last element is maintained
Queues and Stacks
- Special data types, allow insertion and removal of items at the ends
only
Queue
- FIFO – First in, first out
- Items inserted at the back, removed from the front
- To visualize a queue, consider people lining up in a store
Figure 14 Stack and Queue
Behavior
Queues and Stacks
STL queue
- The STL has a queue template class
- To insert and remove, use push() and pop()
#include <queue>;
using namespace std;
queue<string> q;
q.push("Tom");
q.push("Dick");
q.push("Harry");
while (q.size() > 0)
{
cout << q.front() << "\n";
q.pop();
}
- front() doesn't modify the queue
Queues and Stacks
Stacks
- LIFO – Last in, first out
- Items inserted and removed at one end, the top
- Items removed in reverse order of insertion
- Many uses in computers:
- Call stack
- Undo/redo stacks in your word processor
- Operator and operand stacks in your (infix) calculator
Queues and Stacks
STL stack
- The STL has a stack template class
- To insert and remove, use push() and pop()
#include <stack>;
using namespace std;
stack<string> s;
s.push("Tom");
s.push("Dick");
s.push("Harry");
while (s.size() > 0)
{
cout << s.top() << "\n";
s.pop();
}
- top() doesn't modify the stack
Stacks and Queues – fifolifo.cpp
Compares and contrasts the 2 containers
Stacks and Queues
Reverse Polish Notation (RPN)
(postfix) calculator
- Operands are written before the operators
- 3 + 4 * 7 becomes 3 4 7 * +
- Postfix expressions are easy to evaluate
- Numbers are pushed onto a stack
- When a (binary) operator is encountered, 2 numbers are popped,
operation is performed, result is pushed back on the stack
- At end of expression, result is at the top of the stack
- "Commands": p (to print), and q (to quit)
Stacks and Queues – calc.cpp
Random Fact
Polish Notation
- Infix notation – the way you've been looking at
expressions since you learned to add
- Ambiguous
- Rules for precedence and associativity required
- Parenthesis required to force evaluation
- Prefix notation – operator appears first
- Eliminates need for parenthesis and precedence rules
- By Jan Łukasiewicz, in the 1920s
- Easily evaluated using a recursive algorithm
- Dubbed Polish Notation
Random Fact
(cont.)
Polish Notation
- Postfix notation (or Reverse Polish Notation,
RPN) – operator appears last
- Evaluation with a single stack is straightforward:
- Push operands onto the stack
- Each operator pops the appropriate # of values from the stack,
performs the operation, pushes the result back on the stack
- By Charles Hamblin, in the 1950s
Random Fact
(cont.)
Polish Notation
| Standard Notation |
Łukasiewicz Notation |
RPN |
| 3 + 4 |
+ 3 4 |
3 4 + |
| 3 + 4 × 5 |
+ 3 * 4 5 |
3 4 5 * + |
| 3 × (4 + 5) |
* 3 + 4 5 |
3 4 5 + * |
| (3 + 4) × 5 |
* + 3 4 5 |
3 4 + 5 * |
| 3 + 4 + 5 |
+ + 3 4 5 |
3 4 + 5 + |
Random Fact
(cont.)
Polish Notation
In 1972 Hewlett-Packard introduced the HP35
- World's first scientific calculator
- First calculator to use RPN
- Had trig and exponential functions, and used scientific notation
- Did not have parenthesis, nor an equal sign
- Pocket calculator finally replaced the slide rule
- Sold for US $395 (roughly equivalent to $1700 in 2005)
RPN users tend to be fanatical proponents of the system.