Article directory
1 Introduction
We have learned string and vector before, today we will learn another container in C++ - list, the bottom layer of list is the leading two-way circular linked list.
2. Introduction to list
1.list is a sequential container that can be inserted and deleted at any position within a constant range, and the container can be iterated in both directions.
2. The bottom layer of the list is a doubly linked list structure. Each element in the doubly linked list is stored in an independent node that is not related to each other. In the node, pointers point to the previous element and the next element.
3. list is very similar to forward_list: the main difference is that forward_list is a single linked list, which can only be iterated forward, which makes it simpler and more efficient.
4. Compared with other serial containers (array, vector, deque), list usually has better execution efficiency for inserting and removing elements at any position.
5. Compared with other sequential containers, the biggest defect of list and forward_list is that it does not support random access at any position, for example: to access the sixth element of the list, it must be iterated from a known position (such as the head or tail) to this position, iterating over this position requires linear time overhead; the list also requires some additional space to hold the associated information for each node (this may be an important issue for large lists that store elements of smaller types the elements of)
3. Common interface of list
3.1 The constructor of list
function name | Function Description |
---|---|
list (size_type n, const value_type& val = value_type()) | The constructed list contains n elements whose value is val |
list() | Construct an empty list |
list (const list& x) | copy constructor |
list (InputIterator first, InputIterator last) | Constructs a list with elements in the range [first, last) |
在这里插入代码片void Test1()
{
list<int> lt1;
list<int> lt2(10, 1);
list<int> lt3(lt2);
}
3.2 The use of iterators
Here, the iterator can be temporarily understood as a pointer, which points to a certain node in the list.
function name | Function Description |
---|---|
begin+end | Returns an iterator to the first element + returns an iterator to the next position of the last element |
rbegin+rend | Return the reverse_iterator of the first element, which is the end position, and return the reverse_iterator of the next position of the last element, which is the begin position |
void Test2()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
lt1.push_back(5);
list<int>::iterator it = lt1.begin();
while (it != lt1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
it = lt1.begin();
while (it != lt1.end())
{
(*it)++;
cout << *it << " ";
it++;
}
cout << endl;
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
}
3.3 List space management
function name | Function Description |
---|---|
empty | Check whether the list is empty, return true, otherwise return false |
size | Returns the number of valid nodes in the list |
void Test3()
{
list<int> lt1;
list<int> lt2;
lt2.push_back(1);
lt2.push_back(2);
lt2.push_back(3);
lt2.push_back(4);
lt2.push_back(5);
cout << lt1.empty() << endl;
cout << lt2.size() << endl;
}
3.4 List node access
function name | Function Description |
---|---|
front | Returns a reference to the value in the first node of the list |
back | Returns a reference to the value in the last node of the list |
void Test4()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
lt1.push_back(5);
cout << lt1.front() << endl;
cout << lt1.back() << endl;
int& a = lt1.front();
int& b = lt1.back();
a++;
b++;
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
}
3.5 Add, delete, check and modify list
function name | Function Description |
---|---|
push_front | Insert an element with value val before the first element of the list |
pop_front | delete the first element in the list |
push_back | Insert an element with value val at the end of the list |
pop_back | delete the last element in the list |
insert | Insert an element with value val in list position |
erase | Delete the element at list position |
swap | Swap the elements of two lists |
clear | Clear the valid elements in the list |
void Test5()
{
list<int> lt1;
lt1.push_front(1);
lt1.push_front(2);
lt1.push_front(3);
lt1.push_front(4);
lt1.push_front(5);
for (auto e : lt1)
{
cout << e << ' ';
}
cout << endl;
lt1.pop_front();
lt1.pop_front();
lt1.pop_front();
for (auto e : lt1)
{
cout << e << ' ';
}
cout << endl;
list<int> lt2;
lt2.push_back(1);
lt2.push_back(2);
lt2.push_back(3);
lt2.push_back(4);
lt2.push_back(5);
for (auto e : lt2)
{
cout << e << ' ';
}
cout << endl;
lt2.pop_back();
lt2.pop_back();
lt2.pop_back();
for (auto e : lt2)
{
cout << e << ' ';
}
cout << endl;
list<int> lt3;
lt3.push_back(1);
lt3.push_back(2);
lt3.push_back(3);
lt3.push_back(4);
lt3.push_back(5);
list<int>::iterator pos = find(lt3.begin(), lt3.end(), 3);
lt3.insert(pos, 10);//迭代器不会失效
lt3.insert(pos, 20);
for (auto e : lt3)
{
cout << e << ' ';
}
cout << endl;
//lt3.erase(pos);//迭代器会失效
pos = lt3.erase(pos);
pos = lt3.erase(pos);
for (auto e : lt3)
{
cout << e << ' ';
}
cout << endl;
lt1.swap(lt3);
for (auto e : lt1)
{
cout << e << ' ';
}
cout << endl;
for (auto e : lt3)
{
cout << e << ' ';
}
cout << endl;
lt2.clear();
for (auto e : lt2)
{
cout << e << ' ';
}
cout << endl;
}
4. The problem of list iterator invalidation
As mentioned earlier, you can temporarily understand the iterator as similar to a pointer. The invalidation of the iterator means that the node pointed to by the iterator is invalid, that is, the node is deleted. Because the underlying structure of the list is a two-way circular linked list with the leading node, the iterator of the list will not be invalidated when it is inserted into the list. It will only be invalidated when it is deleted, and only the iteration pointing to the deleted node will be invalidated. , other iterators are not affected.
Iterator invalidation:
void Test6()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
lt1.push_back(5);
list<int>::iterator it = lt1.begin();
while (it != lt1.end())
{
//错误——迭代器失效
//erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值
lt1.erase(it);
++it;
}
}
After modification:
void Test7()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
lt1.push_back(5);
list<int>::iterator it = lt1.begin();
while (it != lt1.end())
{
it = lt1.erase(it);//正确
}
}
5. List simulation implementation
#pragma once
#include <assert.h>
namespace fiora
{
template<class T>
struct list_node
{
T _data;
list_node<T>* _next;
list_node<T>* _prev;
list_node(const T& x = T())
:_data(x)
, _next(nullptr)
, _prev(nullptr)
{
}
};
// typedef __list_iterator<T, T&, T*> iterator;
// typedef __list_iterator<T, const T&, const T*> const_iterator;
// 像指针一样的对象
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef list_node<T> Node;
typedef __list_iterator<T, Ref, Ptr> iterator;
typedef bidirectional_iterator_tag iterator_category;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef ptrdiff_t difference_type;
Node* _node;
__list_iterator(Node* node)
:_node(node)
{
}
bool operator!=(const iterator& it) const
{
return _node != it._node;
}
bool operator==(const iterator& it) const
{
return _node == it._node;
}
Ref operator*()
{
return _node->_data;
}
//T* operator->()
Ptr operator->()
{
return &(operator*());
}
// ++it
iterator& operator++()
{
_node = _node->_next;
return *this;
}
// it++
iterator operator++(int)
{
iterator tmp(*this);
_node = _node->_next;
return tmp;
}
// --it
iterator& operator--()
{
_node = _node->_prev;
return *this;
}
// it--
iterator operator--(int)
{
iterator tmp(*this);
_node = _node->_prev;
return tmp;
}
};
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
list()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
void push_back(const T& x)
{
//Node* tail = _head->_prev;
//Node* newnode = new Node(x);
_head tail newnode
//tail->_next = newnode;
//newnode->_prev = tail;
//newnode->_next = _head;
//_head->_prev = newnode;
insert(end(), x);
}
void push_front(const T& x)
{
insert(begin(), x);
}
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next);
}
private:
Node* _head;
};
void test_list1()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
it = lt.begin();
while (it != lt.end())
{
*it *= 2;
++it;
}
cout << endl;
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
struct Pos
{
int _a1;
int _a2;
Pos(int a1 = 0, int a2 = 0)
:_a1(a1)
, _a2(a2)
{
}
};
void test_list2()
{
int x = 10;
int* p1 = &x;
cout << *p1 << endl;
Pos aa;
Pos* p2 = &aa;
p2->_a1;
p2->_a2;
list<Pos> lt;
lt.push_back(Pos(10, 20));
lt.push_back(Pos(10, 21));
list<Pos>::iterator it = lt.begin();
while (it != lt.end())
{
//cout << (*it)._a1 << ":" << (*it)._a2 << endl;
cout << it->_a1 << ":" << it->_a2 << endl;
++it;
}
cout << endl;
}
void Func(const list<int>& l)
{
list<int>::const_iterator it = l.begin();
while (it != l.end())
{
//*it = 10;
cout << *it << " ";
++it;
}
cout << endl;
}
void test_list3()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
Func(lt);
}
void test_list4()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
it = lt.begin();
while (it != lt.end())
{
*it *= 2;
++it;
}
cout << endl;
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
lt.push_front(10);
lt.push_front(20);
lt.push_front(30);
lt.push_front(40);
lt.pop_back();
lt.pop_back();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
auto pos = find(lt.begin(), lt.end(), 4);
if (pos != lt.end())
{
lt.insert(pos, 40);
//lt.insert(pos, 30);
*pos *= 100;
}
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
}
6. The comparison between list and vector
Both vector and list are very important sequential containers in STL. Due to the different underlying structures of the two containers, their characteristics and application scenarios are different. The main differences are as follows:
vector | list | |
---|---|---|
underlying structure | Dynamic sequence table, a continuous space | Doubly linked circular list with head node |
random access | Support random access, the efficiency of accessing an element is O(1) | Random access is not supported, and the efficiency of accessing an element is O(N) |
insert and delete | Insertion and deletion at any position is inefficient, and elements need to be moved. The time complexity is O(N). When inserting, it may need to increase capacity. Increase: open up new space, copy elements, and release old space, resulting in lower efficiency | Insertion and deletion at any position are highly efficient, do not need to move elements, and the time complexity is O(1) |
Space utilization | The bottom layer is a continuous space, which is not easy to cause memory fragmentation, high space utilization rate, and high cache utilization rate | The underlying nodes are dynamically opened, and small nodes are likely to cause memory fragmentation, low space utilization, and low cache utilization |
iterator | Original ecological pointer | Encapsulate the original ecological pointer (node pointer) |
iterator invalidation | When inserting elements, all iterators must be reassigned, because inserting elements may cause re-expansion, causing the original iterator to become invalid. When deleting, the current iterator needs to be reassigned or it will become invalid | Inserting an element will not invalidate the iterator. When deleting an element, only the current iterator will be invalidated, and other iterators will not be affected |
scenes to be used | Requires efficient storage, supports random access, and does not care about insertion and deletion efficiency | Lots of insert and delete operations, don't care about random access |
7. Ending
We have learned about the list container of C++ here. Several containers of C++ are very important knowledge points. They are very simple and convenient to use, but the most important thing is that we need to understand the underlying principles and master their respective advantages, disadvantages and differences. point, so that it can be used flexibly in future study and work.
Finally, I would like to thank you for your patient reading and support. Friends who think this article is well written can follow and support it three times. If you have any questions or there are mistakes in this article, you can private message me or leave a message in the comment area Discussion, thanks again everyone.