🌈前言
本篇文章进行STL中list(双向链表)序列容器的学习!!!
🌸 list
🌷1、list的介绍及使用
🌸1.1、list的介绍
【list的文档介绍】
- list是可以在常数范围内在任意位置进行插入和删除的序列容器,并且该容器可以前后双向迭代
- list的底层是双向链表结构,双向链表中每个元素存储在互不关联的独立节点中,在节点中听过指针指向其前一个元素和后一个元素来进行链接
- list与forward_list非常相似:最主要的不同在于fowward_list是单链表,只能往前进行迭代,已让其简单高效,而list可以往前和往后进行迭代
- list与其他容器相比(array、vector、deque),list通常在任意位置进行插入、移除元素的执行效率更好,尾插尾删效率慢,需要迭代一遍
- list与其他序列容器相比,list和forward_list最大的缺陷是不支持任意位置的访问
比如:
- 要访问list的第n个元素,必须从已知的位置(头结点或尾节点)迭代到该位置,在这段位置上迭代需要线性时间的开销
- list还需要一些额外的空间,以保持每个节点的相关信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)
list底层原理图(双向链表):
🌹1.2、list的使用
list中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展的能力。以下为list中一些常见的重要接口
🌹1.2.1、list容器常见的构造函数
(constructor)构造函数 | 接口说明 |
---|
list() (重点) | 构造空的list | list(size_type n, const value_type& val = value_type()) | 构造list中包含n个值尾val的元素 | list (const listr& l) (重点) | 拷贝构造函数 | list (InputIterator first, InputIterator last) | 使用区间[first, last)中的元素来构造list |
举个栗子🌰:
void TestList()
{
list<int> l1;
list<int> l2(5, 100);
list<int> l3(l2);
list<int> l4(l3.begin(), l3.end());
int Array[] = { 1, 2, 3, 4 };
list<int> l5(Array, Array + sizeof(Array) / sizeof(int));
list<int>::iterator It = l5.begin();
while (It != l5.end())
{
cout << *It << ' ';
++It;
}
cout << endl;
for (auto e : l5)
cout << e << ' ';
cout << endl;
}
🌺1.2.2、list iterator(迭代器的使用)
函数声明 | 接口说明 |
---|
begin + end | 返回第一个元素的迭代器 + 返回最后一个元素的下一个位置的迭代器 | rbegin + rend | 返回第一个元素的reverse_iterator,即end位置,返回最后一个元素下一个位置的reserve_iterator,及begin位置 |
【注意】
- begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动
- rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动
举个栗子🌰:
void print_list(const list<int>& l)
{
for (list<int>::const_iterator it = l.begin(); it != l.end(); ++it)
{
cout << *it << " ";
}
cout << endl;
}
int main()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list<int> l(array, array + sizeof(array) / sizeof(array[0]));
for (list<int>::iterator it = l.begin(); it != l.end(); ++it)
cout << *it << " ";
cout << endl;
for (list<int>::reverse_iterator it = l.rbegin(); it != l.rend(); ++it)
cout << *it << " ";
cout << endl;
return 0;
}
🌻1.2.3、list capacity
函数声明 | 接口说明 |
---|
empty | 检测list是否为空,是返回true,否返回false | size | 返回list节点的有效个数 |
举个栗子🌰:
void TestListCapacity()
{
list<int> v1;
list<int> v2{ 1, 2, 3, 4 };
cout << "v1的有效节点个数为: " << v1.size() << '\t'
<< "v1是否为空: " << v1.empty() << endl;
cout << "v2的有效节点个数为: " << v2.size() << '\t'
<< "v是否为空: " << v2.empty() << endl;
}
int main()
{
TestListCapacity();
return 0;
}
🌼1.2.4、list element access
函数声明 | 接口说明 |
---|
front | 返回list第一个节点中值(val)的引用 | back | 返回list最后一个节点中值(val)的引用 |
举个栗子🌰:
void TestList()
{
list<int> v{ 1, 2, 3, 4, 5 };
cout << v.front() << endl;
cout << v.back() << endl;
v.front() = 100;
for (auto It = v.begin(); It != v.end(); ++It)
cout << *It << ' ';
cout << endl;
v.back() = 200;
for (auto It = v.begin(); It != v.end(); ++It)
cout << *It << ' ';
cout << endl;
}
int main()
{
TestList();
return 0;
}
注意:二个接口返回的是这个数据的引用,引用可以充当左值…
🌿1.2.5、list modifiers
举个栗子🌰1??:
void PrintList(list<int>& l)
{
for (auto& e : l)
cout << e << " ";
cout << endl;
}
void TestList1()
{
int array[] = { 1, 2, 3 };
list<int> L(array, array + sizeof(array) / sizeof(array[0]));
L.push_back(4);
L.push_front(0);
PrintList(L);
L.pop_back();
L.pop_front();
PrintList(L);
}
举个栗子🌰2??:
void TestList2()
{
int Array1[] = { 1, 2, 3 };
list<int> L(Array1, Array1 + sizeof(Array1) / sizeof(Array1[0]));
auto pos = ++L.begin();
cout << *pos << endl;
L.insert(pos, 4);
PrintList(L);
L.insert(pos, 5, 5);
PrintList(L);
vector<int> v{ 7, 8, 9 };
L.insert(pos, v.begin(), v.end());
PrintList(L);
L.erase(pos);
PrintList(L);
L.erase(L.begin(), L.end());
PrintList(L);
}
举个栗子🌰3??:
void PrintList(list<int>& l)
{
for (auto& e : l)
cout << e << " ";
cout << endl;
}
void TestList3()
{
int array1[] = { 1, 2, 3 };
list<int> l1(array1, array1 + sizeof(array1) / sizeof(array1[0]));
PrintList(l1);
list<int> l2{ 3, 2, 1 };
PrintList(l2);
l1.swap(l2);
PrintList(l1);
PrintList(l2);
l2.clear();
cout << l2.size() << endl;
}
🍀1.2.5、list迭代器失效问题
迭代器失效:
- 前面说过,大家可以将迭代器暂时理解成原生态指针,迭代器失效即所指向的节点已被销毁或释放,即该节点被删除了。
- list的底层结构为双向链表,因此在list中进行插入时,是不会导致list迭代器失效的(list底层存储空间不连续
- 只有在删除节点时才会失效,并且失效的知识指向被删除节点的迭代器,其他迭代器不受影响
举个栗子🌰:
void TestListIterator1()
{
int Array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
list<int> l(Array, Array + sizeof(Array) / sizeof(int));
auto It = l.begin();
while (It != l.end())
{
l.erase(It);
++It;
}
}
void TestListIterator2()
{
int Array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
list<int> l(Array, Array + sizeof(Array) / sizeof(int));
auto It = l.begin();
while (It != l.end())
{
l.erase(It++);
}
}
🍁2、list模拟实现
🍂2.1、模拟实现list(vs2022)
list.h
#ifndef LIST_H_
#define LIST_H_
#include <iostream>
#include <new>
#include <cassert>
namespace mylist
{
template <typename T>
struct ListNode
{
ListNode<T>* _Pre;
ListNode<T>* _Next;
T _val;
ListNode(const T& val = T())
: _Pre(nullptr)
, _Next(nullptr)
, _val(val)
{}
};
template <typename T, typename Ref, typename Ptr>
class ListIterator
{
typedef ListIterator<T, Ref, Ptr> Self;
typedef ListNode<T>* PNode;
public:
PNode _PNode;
ListIterator(PNode x = nullptr)
: _PNode(x)
{}
ListIterator(const Self& s)
: _PNode(s._PNode)
{}
bool operator ==(const Self& s) { return _PNode == s._PNode; }
bool operator !=(const Self& s) { return _PNode != s._PNode; }
T& operator*() { return _PNode->_val; }
T* operator->() { return &(operator*()); }
Self& operator++()
{
_PNode = _PNode->_Next;
return *this;
}
Self operator++(int)
{
Self tmp(*this);
_PNode = _PNode->_Next;
return tmp;
}
Self& operator--()
{
_PNode = _PNode->_Pre;
return *this;
}
Self operator--(int)
{
Self tmp(*this);
_PNode = _PNode->_Pre;
return tmp;
}
};
template <typename T>
class list
{
typedef ListNode<T>* PNode;
typedef ListIterator<T, T*, T&> iterator;
typedef ListIterator<T, const T&, const T&> const_iterator;
public:
iterator begin() { return iterator(_pHead->_Next); }
iterator end() { return iterator(_pHead); }
const_iterator cbegin() { return const_iterator(_pHead->_Next); }
const_iterator cend() { return const_iterator(_pHead); }
list() { CreatNode(); }
list(size_t n, const T& val = T())
{
CreatNode();
for (int i = 0; i < n; ++i)
push_back(val);
}
list(const list<T>& l)
{
CreatNode();
PNode cur = l._pHead->_Next;
while (cur != l._pHead)
{
push_back(cur->_val);
cur = cur->_Next;
}
}
template<typename iterator>
list(iterator* first, iterator* last)
{
CreatNode();
while (first != last)
{
push_back(*first);
++first;
}
}
list<T>& operator=(list<T> l)
{
this->swap(l);
return *this;
}
~list()
{
clear();
delete _pHead;
_pHead = nullptr;
}
size_t size()
{
size_t size = 0;
PNode cur = _pHead->_Next;
while (cur != _pHead)
{
++size;
cur = cur->_Next;
}
return size;
}
bool empty() { return _pHead->_Next == nullptr; }
void push_back(const T& val)
{
PNode newNode = new ListNode<T>(val);
PNode tailPrev = _pHead->_Pre;
tailPrev->_Next = newNode;
newNode->_Pre = tailPrev;
newNode->_Next = _pHead;
_pHead->_Pre = newNode;
}
void pop_back()
{
PNode tailprev = _pHead->_Pre->_Pre;
if (_pHead->_Next->_Next == nullptr)
{
delete _pHead->_Next;
_pHead->_Next = _pHead;
_pHead->_Pre = _pHead;
}
else
{
delete _pHead->_Pre;
tailprev->_Next = _pHead;
_pHead->_Pre = tailprev;
}
}
void push_front(const T& val)
{
PNode newNode = new ListNode<T>(val);
PNode next = _pHead->_Next;
_pHead->_Next = newNode;
newNode->_Pre = _pHead;
newNode->_Next = next;
next->_Pre = newNode;
}
void pop_front()
{
PNode next = _pHead->_Next->_Next;
if (next == nullptr)
{
delete _pHead->_Next;
_pHead->_Next = _pHead;
_pHead->_Pre = _pHead;
}
else
{
PNode nextnode = _pHead->_Next;
_pHead->_Next = next;
next->_Pre = _pHead;
delete nextnode;
}
}
PNode find(const T& val)
{
PNode cur = _pHead->_Next;
while (cur != _pHead->_Pre)
{
if (cur->_val == val)
return cur;
cur = cur->_Next;
}
return nullptr;
}
iterator insert(iterator pos, const T& val)
{
PNode newNode = new ListNode<T>(val);
PNode Posprev = pos._PNode->_Pre;
Posprev->_Next = newNode;
newNode->_Pre = Posprev;
newNode->_Next = pos._PNode;
pos._PNode->_Pre = newNode;
return iterator(newNode);
}
iterator erase(iterator pos)
{
PNode Posnext = pos._PNode->_Next;
PNode Posprev = pos._PNode->_Pre;
Posprev->_Next = Posnext;
Posnext->_Pre = Posprev;
delete pos._PNode;
return iterator(Posnext);
}
T& front() { return _pHead->_Next->_val; }
const T& front()const { return _pHead->_Next->_val; }
T& back() { return _pHead->_Pre->_val; }
const T& back()const { return _pHead->_Pre->_val; }
void clear()
{
PNode cur = _pHead->_Next;
while (cur != _pHead)
{
PNode curNext = cur->_Next;
delete cur;
cur = curNext;
}
_pHead->_Next = _pHead;
_pHead->_Pre = _pHead;
}
void swap(list<T>& l)
{
std::swap(_pHead, l._pHead);
}
private:
void CreatNode()
{
_pHead = new ListNode<T>;
_pHead->_Next = _pHead;
_pHead->_Pre = _pHead;
}
private:
PNode _pHead;
};
}
#endif
🍃2.2、对mylist接口进行测试
list.cpp
#include "list.h"
using namespace std;
template <typename T>
void Printlist(mylist::list<T>& l)
{
auto It = l.cbegin();
while (It != l.cend())
{
cout << *It << ' ';
++It;
}
cout << endl;
}
void Test_list_constructor()
{
mylist::list<int> l1;
mylist::list<int> l2(10, 5);
Printlist<int>(l2);
int Array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
mylist::list<int> l3(Array, Array + sizeof(Array) / sizeof(int));
Printlist(l3);
mylist::list<int> l4(l3);
Printlist(l4);
l1 = l4;
Printlist(l1);
}
void Testlist2()
{
mylist::list<int> l;
for (int i = 0; i < 5; ++i)
l.push_back(i);
Printlist(l);
for (int i = 0; i < 5; ++i)
l.pop_back();
cout << l.size() << endl;
for (int i = 0; i < 5; ++i)
l.push_front(i);
Printlist(l);
for (int i = 0; i < 5; ++i)
l.pop_front();
cout << l.size() << endl;
}
void Testlist3()
{
int array[] = { 1, 2, 3, 4, 5 };
mylist::list<int> l(array, array + sizeof(array) / sizeof(array[0]));
auto pos = l.begin();
l.insert(l.begin(), 0);
Printlist(l);
++pos;
l.insert(pos, 2);
Printlist(l);
l.erase(l.begin());
l.erase(pos);
Printlist(l);
cout << *pos << endl;
auto it = l.begin();
while (it != l.end())
it = l.erase(it);
cout << l.size() << endl;
}
int main()
{
Test_list_constructor();
Testlist2();
Testlist3();
return 0;
}
🍄 3、list与vector的对比
vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:
| vector | list |
---|
底层结构 | 动态顺序表,一段连续空间 | 带头节点的双向循环链表 | 随机访问 | 支持随机访问,访问某个元素效率O(1) | 不支持随机访问,访问某个元素效率O(N) | 插入和删除 | 任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时可能需要增容。增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低 | 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1),不存在增容问题 | 空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层节点随机动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低 | 迭代器 | 原生态指针 | 对原生态指针(节点指针)进行封装 | 迭代器失效 | 在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效 | 插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响 | 使用场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入删除操作,不关心随机访问 |
|