Standard Lists
Steven J. Zeil
The list abstraction is closely related to the linked list data structure that we have already exampled.
1 The Standard list
From the point of view of the public interface, std::list
is very similar to std::vector
. The primary differences are:
-
In addition to
push_back
andpop_back
, we also gainpush_front
andpop_front
operations.All are $O(1)$.
-
We cannot access arbitrary positions by number (e.g.,
v[i]
,v.at(23)
). Access to internal elements is exclusively via iterators.-
Compensating for the difficulty of getting to an internal position, operations like
insert(position, data)
anderase(position)
are now $O(1)$ for lists. (They areO(size())
for vectors.)
-
-
A standard list’s iterators are bi-directional. (A vector’s are random access.)
The standard list is shown here.
namespace std {
template <class T>
class list {
public:
typedef T& reference;
typedef const T& const_reference;
typedef ... iterator;
typedef ... const_iterator;
typedef ... size_type;
typedef ... difference_type;
typedef T value_type;
typedef T* pointer;
typedef const T* const_pointer
typedef std::reverse_iterator<iterator> reverse_iterator;
typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
// construct/copy/destroy:
explicit list(const Allocator& = Allocator());
explicit list(size_type n, const T& value = T());
template <class InputIterator>
list(InputIterator first, InputIterator last);
list(const list<T>& x);
~list();
list<T>& operator=(const list<T>& x);
template <class InputIterator>
void assign(InputIterator first, InputIterator last);
void assign(size_type n, const T& t);
// iterators:
iterator begin();
const_iterator begin() const;
iterator end();
const_iterator end() const;
reverse_iterator rbegin();
const_reverse_iterator rbegin() const;
reverse_iterator rend();
const_reverse_iterator rend() const;
// _lib.list.capacity_ capacity:
bool empty() const;
size_type size() const;
size_type max_size() const;
void resize(size_type sz, T c = T());
// element access:
reference front();
const_reference front() const;
reference back();
const_reference back() const;
// _lib.list.modifiers_ modifiers:
void push_front(const T& x);
void pop_front();
void push_back(const T& x);
void pop_back();
iterator insert(iterator position, const T& x);
void insert(iterator position, size_type n, const T& x);
template <class InputIterator>
void insert(iterator position, InputIterator first,
InputIterator last);
iterator erase(iterator position);
iterator erase(iterator position, iterator last);
void swap(list<T>&);
void clear();
// _lib.list.ops_ list operations:
void splice(iterator position, list<T>& x);
void splice(iterator position, list<T>& x, iterator i);
void splice(iterator position, list<T>& x, iterator first,
iterator last);
void remove(const T& value);
template <class Predicate> void remove_if(Predicate pred);
void unique();
template <class BinaryPredicate>
void unique(BinaryPredicate binary_pred);
void merge(list<T>& x);
template <class Compare> void merge(list<T>& x, Compare comp);
void sort();
template <class Compare> void sort(Compare comp);
void reverse();
};
template <class T>
bool operator==(const list<T>& x, const list<T>& y);
template <class T>
bool operator< (const list<T>& x, const list<T>& y);
template <class T>
bool operator!=(const list<T>& x, const list<T>& y);
template <class T>
bool operator> (const list<T>& x, const list<T>& y);
template <class T>
bool operator>=(const list<T>& x, const list<T>& y);
template <class T>
bool operator<=(const list<T>& x, const list<T>& y);
// specialized algorithms:
template <class T, class Allocator>
void swap(list<T>& x, list<T>& y);
}
It is, for the most part, identical to the vector
interface, except that list
does not allow you to access elements by indexing (e.g., v[i]
). Instead, any access to the contents of a std::list
will almode always be provided via iterators.
list
provides a few specialized operations for shifting nodes from one list to another (splice
). These aren’t used all that often, but can sometimes significantly reduce the complexity of an algorithm by avoiding the need to copy long sequences of data.
list
also provides some utility functions for sorting, merging and other list operations. In many cases, these same functions exist as separate function templates in std
, but may not be usable with list
s. For example, std
has a sort
function, but std::sort
usually employs a variation of quicksort, and the quicksort algorithm requires random access iterators. list
provides bi-directional iterators, but not random-access, so the std::sort
function will not work with lists. Instead, therefore, list
provides its own sort function as a member function of the list<T>
class.
Let’s look at this declaration, piece by piece.
1.1 Constructors
list<int> l1;
l1
is a list of integers. Initially, it has 0 elements
list<string> l2(10, "abc");
l2
is a list of strings initialized to hold 10 copies of "abc"
template <class T>
// construct / copy / destroy
list();
explicit list(size_type n, const T& value = T());
template <class InputIterator>
list(InputIterator first, InputIterator last);
list(const list<T>& x);
~list();
The last constructor is rather curious. It’s actually a template function, and is designed to initialize the list with a sequence of values taken from some other container. The desired sequence is indicated via a pair of iterators, one for the beginning of the sequence, the other for the position just after the last desired element.
char[] greeting = "Hello";
list<char> l3(greeting+1, greeting+4);
l3
is a list of characters containing 'e'
, 'l'
and another 'l'
.
— In fact, a similar constructor was available for vectors as well, but I didn’t mention it then because we had not yet introduced iterators. All the standard containers support this type of constructor template. For vectors and lists, this is an $O(n)$ operation, where $n$ is the number of elements in the indicated range.
1.2 Copying
list<double> v, w;
⋮
v = w;
list<double> z(v);
template <class InputIterator>
list(InputIterator first, InputIterator last);
list(const list<T>& x);
~list();
list<T>& operator=(const list<T>& x);
template <class InputIterator>
void assign(InputIterator first, InputIterator last);
void assign(size_type n, const T& u);
1.3 Size
list<int>::size_type n;
n = l1.size();
How many elements currently in l1
?
l1.resize(n, x);
Add or remove elements, as necessary, at the end of the list to make l1
have exactly n
elements. If elements need to be added, the value of the new elements will be x
.
l1.resize(n);
Same as the last example, but if any elements need to be added to the list, the new elements are formed using their data type’s default constructor.
// capacity
size_type size() const;
size_type max_size() const;
void resize(size_type sz, T c = T());
bool empty() const;
1.4 Inserting Elements
l2.push_back("def");
Adds a new element, "def"
to the end of the list l2
. l2.size()
increases by 1.
l2.push_front("def");
Adds a new element, "def"
to the front of the list l2
. l2.size()
increases by 1.
list<std::string>::iterator pos;
⋮
l2.insert(pos, "def");
Inserts a new element, "def"
, at the indicated position within the list l2
. Any elements already in l2
at that position or higher are moved back one position to make room. l2.size()
increases by 1.
// modifiers:
void push_back(const T& x);
void push_front(const T& x);
void pop_back();
void pop_front();
iterator insert(iterator position, const T& x);
void insert(iterator position, size_type n, const T& x);
template <class InputIterator>
void insert(iterator position,
InputIterator first, InputIterator last);
iterator erase(iterator position);
iterator erase(iterator first, iterator last);
void swap(list<T>&);
void clear();
1.5 Removing Elements
l2.pop_back();
Removes the element at the end of the list, decreasing the size()
by 1.
l2.pop_front();
Removes the element at the front of the list, decreasing the size()
by 1.
list<std::string>::iterator pos;
⋮
l2.erase(pos);
Removes the element at the indicated position within the list l2
. Any elements already in l2
at higher positions are moved forward one position to “take up the slack”. l2.size()
decreases by 1.
list<std::string>::iterator start, stop;
⋮
l2.erase(start, stop);
Removes the elements at the indicated positions from start
up to, but not including stop
, within the list l2
. Any elements already in l2
at higher positions are moved forward to “take up the slack”. l2.size()
decreases by the number of elements removed.
You can remove all elements from a list like this:
list<std::string>::iterator pos;
⋮
l2.clear();
// modifiers:
void push_back(const T& x);
void push_front(const T& x);
void pop_back();
void pop_front();
iterator insert(iterator position, const T& x);
void insert(iterator position, size_type n, const T& x);
template <class InputIterator>
void insert(iterator position,
InputIterator first, InputIterator last);
iterator erase(iterator position);
iterator erase(iterator first, iterator last);
void swap(list<T>&);
void clear();
1.6 Access to Elements
x = l1.front();
l1.front() = l1.front()+1;
Provides access to the first element in the list.
x = l1.back();
l1.back() = l1.back()+1;
Provides access to the last element in the list.
// element access:
reference front();
const_reference front() const;
reference back();
const_reference back() const;
1.6.1 Access to Interior Elements
None of the operations described so far give access to elements other than the front and back.
- Interior elements are accessed via iterators, which follow the usual C++ conventions. For example, a typical loop through an entire list looks like:
list<string> names;
⋮
for (list<string>::iterator pos = names.begin();
pos != names.end(); ++pos)
{
cout << "One of the names is " << *pos << endl;
}
or, in C++11,
list<string> names;
⋮
for (auto pos = names.begin(); pos != names.end(); ++pos)
{
cout << "One of the names is " << *pos << endl;
}
or, using the range-based for loop:
list<string> names;
⋮
for (string aName: names)
{
cout << "One of the names is " << aName << endl;
}
1.7 Comparing Lists
Lists can be compared using ==
and <
operators.
-
Two lists are equal if they are the same length and if each corresponding pair of elements are equal.
-
The
<
operator compares lists according to the idea of lexicographic comparison.
These rules are essentially the same as described earlier for vectors.
2 Implementing the List ADT
The std::list
is usually implemented as pointers to the first and last nodes in a fairly conventional doubly-linked list of nodes like these:
template <class T>
struct listNode {
T data;
listNode<T>* prevLink;
listNode<T>* nextLink;
};
Once that’s been established, the individual functions are pretty much straightforward variations on the linked list algorithms that we have already looked at.
Your text adds the additional wrinkle of using sentinel nodes at each end of the list. You can read an explanation of the implementation there.
2.1 iterators
The iterators are declared as classes that present the typical iterator interface, implemented using a pointer to the list node representing the current position and a pointer to the list object itself.
class const_iterator
{
public:
// Public constructor for const_iterator.
const_iterator( ) : current{ nullptr }
{ }
// Return the object stored at the current position.
// For const_iterator, this is an accessor with a
// const reference return type.
const Object & operator* ( ) const
{ return retrieve( ); }
const_iterator & operator++ ( )
{
current = current->next;
return *this;
}
const_iterator operator++ ( int )
{
const_iterator old = *this;
++( *this );
return old;
}
const_iterator & operator-- ( )
{
current = current->prev;
return *this;
}
const_iterator operator-- ( int )
{
const_iterator old = *this;
--( *this );
return old;
}
bool operator== ( const const_iterator & rhs ) const
{ return current == rhs.current; }
bool operator!= ( const const_iterator & rhs ) const
{ return !( *this == rhs ); }
protected:
Node *current;
// Protected helper in const_iterator that returns the object
// stored at the current position. Can be called by all
// three versions of operator* without any type conversions.
Object & retrieve( ) const
{ return current->data; }
// Protected constructor for const_iterator.
// Expects a pointer that represents the current position.
const_iterator( Node *p ) : current{ p }
{ }
friend class List<Object>;
};
You can see, for example, that incrementing one of these iterators involves simply following a “next” pointer by one hop.
3 Required Performance
The C++ standard specifies that a legal (i.e., standard-conforming) implementation of list
must satisfy the following performance requirements:
Operation | Speed |
---|---|
list() |
O(1) |
list(n, x) |
O(n) |
size() |
O(size()) |
push_back(x) |
O(1) |
push_front(x) |
O(1) |
pop_back |
O(1) |
pop_front |
O(1) |
insert |
O(1) |
erase |
O(1) |
front, back |
O(1) |
splice |
O(1) |
Perhaps the only surprise here is that size()
is O(size()). The relatively seldom-used splice
operation allows one to transfer arbitrarily long ranges of elements from one list to another. It is supposed to take place in O(1)
time. But there’s no way for a list to know how many elements hjave been removed or added in a splice
in that amount of time. Consequently, lists cannot keep track of their sizes as things are added and removed, the way that were able to do with vector
. Hence every call to list::size()
must actually walk the entire list, counting the nodes.
Beware, therefore, of writing loops like this:
void process (const list<T>& aList)
{
int numProcessed = 0;
auto pos = aList.begin();
while (numProcessed < aList.size())
{
doSomethingWith(*pos);
++pos;
++numProcessed;
}
}
(I often see code like this from clumsy conversions of code from arrays to lists.)
Because size()
is O(size())
, and because it is being called on each iteration of the loop, this function winds up being $O(\mbox{aList.size()}^2)$.
This is better, being $O(\mbox{aList.size()})$.
void process (const list<T>& aList)
{
int numProcessed = 0;
int numToBeProcessed = alist.size();
auto pos = aList.begin();
while (numProcessed < numToBeProcessed)
{
doSomethingWith(*pos);
++pos;
++numProcessed;
}
}
Of course, this is both $O(\mbox{aList.size()})$ and simpler:
void process (const list<T>& aList)
{
auto pos = aList.begin();
while (pos != aList.end())
{
doSomethingWith(*pos);
++pos;
}
}
and this is the same complexity and still simpler:
void process (const list<T>& aList)
{
for (const T& t: aList)
{
doSomethingWith(t);
}
}