您的位置:首页 > 教育 > 锐评 > STL-list

STL-list

2024/10/6 14:32:00 来源:https://blog.csdn.net/weixin_64423718/article/details/139236251  浏览:    关键词:STL-list

目录

【本节目标】

1.list的介绍及使用

1.1 list的介绍(双向链表)

1.2 list的使用

1.2.1 list的构造

1.2.2 list iterator的使用(迭代器)

1.2.3 list capacity(容量)

1.2.4 list element access

1.2.5 list modifiers

1.2.6 list的迭代器失效

2.list的模拟实现

2.1节点类

2.2 list迭代器类 

 2.3 list类

2.4 输出遇到的问题

2.5 遇到const迭代器传参时的问题

3.list与vector的对比


【本节目标】

1. list的介绍及使用
2. list的深度剖析及模拟实现
3. list与vector的对比

1.list的介绍及使用

1.1 list的介绍(双向链表)

1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。
4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)

 

1.2 list的使用

list中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展的能力。以下为list中一些常见的重要接口。

1.2.1 list的构造

构造函数( (constructor))接口说明
list (size_type n, const value_type& val = value_type())构造的list中包含n个值为val的元素
list()构造空的list
list (const list& x)拷贝构造函数
list (InputIterator first, InputIterator last)用[first, last)区间中的元素构造list

1.2.2 list iterator的使用(迭代器)

函数声明接口说明
begin +
end
返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器
rbegin +
rend
返回第一个元素的reverse_iterator,即end位置,返回最后一个元素下一个位置的
reverse_iterator,即begin位置

注意:

1.begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动

2.rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动

1.2.3 list capacity(容量)

函数声明接口说明
empty检测list是否为空,是返回true,否则返回false
size返回list中有效节点的个数

1.2.4 list element access

函数声明接口说明
front返回list的第一个节点中值的引用
back返回list的最后一个节点中值的引用

1.2.5 list modifiers

函数声明接口说明
push_front在list首元素前插入值为val的元素
pop_front删除list中第一个元素
push_back在list尾部插入值为val的元素
pop_back删除list中最后一个元素
insert在list position 位置中插入值为val的元素
erase删除list position位置的元素
swap交换两个list中的元素
clear清空list中的有效元素

1.2.6 list的迭代器失效

list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代
器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。

void TestListIterator1()
{int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };list<int> l(array, array + sizeof(array) / sizeof(array[0]));auto it = l.begin();while (it != l.end()){// erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值l.erase(it);++it;}
}

删除时会导致迭代器失效,由于删除之后,之前的节点已经删除,但是迭代器还是指在这个位置,没有发生改变,从而导致迭代器失效。改为如下

// 改正
void TestListIterator()
{int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };list<int> l(array, array + sizeof(array) / sizeof(array[0]));auto it = l.begin();while (it != l.end()){l.erase(it++); // it = l.erase(it);}
}

注意:

list只有删除时(erase)迭代器才会失效,插入时(insert)不会失效。

2.list的模拟实现

2.1节点类

首先将节点封装成一个类,节点类

//节点类
//初始化每个节点
template<class T>//用struct结构体时由于节点的数据全部公开,也可以时class类中的public中
struct ListNode
{ListNode<T>* _next;//指向下一个节点ListNode<T>* _prev;//指向前一个节点T data;//节点的构造函数//只用构造节点属性//默认缺省值为默认无参构造ListNode(const T& x = T()):_next(nullptr),_prev(nullptr),data(x){}};

运用结构体来封装,因为就结构体的默认时public类型,而节点的数据本来就要全部公开。

2.2 list迭代器类 

如果物理空间是连续的,迭代器就可以认为是原生指针。由于list迭代器的空间是不连续的,原生指针不满足需求。封装一个类来实现迭代器。由于STL的每个结构都有iterator迭代器,所以可以用内嵌类型解决,即用typedef重命名或者内部类。

list迭代器也就相当于一个节点的指针

ListIterator类

起初的ListIterator类

template<class T>
struct ListIterator
{typedef ListNode<T> Node;typedef ListIterator<T> Self;Node* _node; //一个迭代器节点//迭代器构造ListIterator(Node *node):_node(node){}++it前置++,返回++以后的值Self& operator++(){_node = _node->_next;return *this;}it++后置++Self operator++(int){Self tmp(*this);//浅拷贝,即两个迭代器指针指向同一个空间,直接应用默认拷贝构造_node = _node->_next;return tmp;//拷贝}--itSelf& operator--(){//向前走_node = _node->_prev;return *this;}it--Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}*it解引用,返回的是数据T& operator*(){return _node->data;}==比较两个迭代器相等,即比较迭代器的位置(引用/地址)相同bool operator==(const Self& it){return _node == it._node;}//!=bool operator!=(const Self& it){return _node != it._node;}//->//返回的是数据的地址T* operator->(){return &_node->data;}};

前置++、后置++、前置--、后置--都额可以简单应用

重点: 

解引用运算符重载

 2.3 list类

封装一个list类,成员变量是哨兵位头节点(_head)和计数节点的个数(_size),这是一个起初的list类之后会根据迭代器去变化

typedef ListIterator<T> iterator;是将迭代器名重定义到域内,就相当于只在list类内,即和内部类的功能一样

template<class T>
class list
{
public://重定义节点类名typedef ListNode<T> Node;//重定义迭代器名,作用域在list域内//没有用const迭代器时typedef ListIterator<T> iterator;private:Node *_head;//哨兵位size_t _size;//链表中节点的个数};

迭代器的begin和end

//迭代器的引用
iterator begin()
{//iterator it(_head->_next);//有名对象//return it;return iterator(_head->_next);//这是应用的是一个匿名对象
}iterator end()
{return iterator(_head);
}

由于哨兵位不算节点,哨兵位的下一个节点是第一个节点(begin)。

由于双向链表,即end就为_head。

构造函数

起初的构造函数,构造一个哨兵位头节点,_next和_prev都指向_head自己

//构造函数
//默认情况下,只有一个头节点的哨兵位
//并且_head->_next=_head
//_head->_prev=_head
list()
{_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;
}

为了更好些拷贝构造函数,则把构造函数改为以下方式

void empty_init()
{_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;
}
list()
{empty_init();
}

根据上述部分,写出拷贝构造

//拷贝构造
//lt2(lt1)
//逐节点拷贝
list(const list<T>& lt)
{empty_init();//插入for (auto& e : lt){push_back(e);}}

先构造出哨兵位头节点,之后再逐节点拷贝,即为得到

赋值重载

//交换
void swap(list<T>& lt)
{std::swap(_head, lt._head);std::swap(_size, lt._size);
}//赋值重载
//lt=lt1;
list<T>& operator=(const list<T> lt)
{//交换swap(lt);reurn* this;
}

插入

尾插

//尾插
//引用插入数据本身
//权限可以缩小,输入的实参可以是const类型数据或者普通类型数据都可以
void push_back(const T& x)
{开辟节点,存入数据//Node* newnode = new Node(x);//Node* tail = _head->_prev;// tail指向原来的最尾部的节点//tail->_next = newnode;//newnode->_prev = tail;//newnode->_next = _head;//_head->_prev = newnode;//已知insert函数时insert(end(), x);
}

 

insert函数

//c++中要隐藏底层,应用迭代器
//在pos位置插入
void insert(iterator pos, const T& val)
{Node *cur = pos._node;//当前位置Node* Prev = cur->_prev;Node *newnode = new Node(val);//Prev newnode curPrev->_next = newnode;newnode->_prev = Prev;newnode->_next = cur;cur->_prev = newnode;_size++;
}

头插

//头插
void push_front(const T& x)
{insert(begin(), x);
}

 删除

erase

//删除
//erase会导致迭代器失效,失效的原因是这个指针已经被释放
//需要更新节点指针,返回下一个节点的迭代器
iterator erase(iterator pos)
{Node *cur = pos._node;//当前位置Node *Prev = cur->_prev;//前一个位置Node *Next = cur->_next;//后一个位置Prev->_next = Next;Next->_prev = Prev;delete cur;//删除当前节点_size--;return iterator(Next);//匿名对象}

头删和尾删

//头删
void pop_front()
{erase(begin());
}//尾删
void pop_back()
{//erase(end() - 1);不能使用,由于没有重载减号的运算符erase(--end());
}

2.4 输出遇到的问题

struct A
{int _a1;int _a2;A(int a1 = 0, int a2 = 0):_a1(a1), _a2(a2){}
};void test_list2()
{list<A> lt;A aa1(1, 1);A aa2 = { 1, 1 };lt.push_back(aa1);lt.push_back(aa2);lt.push_back(A(2, 2));lt.push_back({ 3, 3 });//多参数可以这样构造lt.push_back({ 4, 4 });A* ptr = &aa1;(*ptr)._a1;ptr->_a1;list<A>::iterator it = lt.begin();while (it != lt.end()){//*it += 10;//cout<<*it<<" ";    //问题出处cout << (*it)._a1 << ":" << (*it)._a2 << endl;//解决问题2//解决问题3//it.operator->()->_a1;//第一个是->是运算符重载,第二个是->原生指针cout << it->_a1 << ":" << it->_a2 << endl;cout << it.operator->()->_a1 << ":" << it.operator->()->_a2 << endl;++it;}cout << endl;
}

由于之前输出时,用的int数据的链表,流插入可以输出结果,但是对应自定义类型时,是无法用流插入输出的。

解决方法有两种

1.写出流插入的运算符重载方法

2.用上面的方法解决:cout<<(*it)._a1<<":"<<(*it)._a2<<endl;即解决问题2

3.如果实在不想写流插入的运算符重载方法,可以运用解决问题3,在ListIterator类中重载->方法

解决问题3

直接在最初的ListIterator类的public部分中添加以下方法

//->
//返回的是数据的地址
T* operator->()
{return &_node->data;
}

之后可以应用问题解决3,他直接会去省略一个->,即为it->_a;原本是it.operator->()->_a;由于返回值为A*,所以解引用后可以访问到_a;

2.5 遇到const迭代器传参时的问题

由于上述的代码都是最初的代码,最初的ListIterator类,最初的list类

遇到下面代码时会报错

void PrintList(const list<int>& clt)
{list<int>::const_iterator it = clt.begin();while (it != clt.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);PrintList(lt);list<int> lt1(lt);PrintList(lt1);
}

由于传参数是const类型的迭代器,和我们上面写的不同,上面最初的迭代器ListIterator类只有普通方法,没有const方法。

解决问题方法有两种:

1.可以用ctrl+c/v,再写一个ListConstIterator类

如下

//第一种解决const类型的迭代器
//const迭代器类
template<class T>
struct ListConstIterator
{typedef ListNode<T> Node;typedef ListConstIterator<T> Self;Node* _node; //一个迭代器节点//迭代器构造ListConstIterator(Node* node) :_node(node){}++it前置++,返回++以后的值Self& operator++(){_node = _node->_next;return *this;}it++后置++Self operator++(int){Self tmp(*this);//浅拷贝,即两个迭代器指针指向同一个空间,直接应用默认拷贝构造_node = _node->_next;return tmp;//拷贝}--itSelf& operator--(){//向前走_node = _node->_prev;return *this;}it--Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}*it解引用,返回的是数据const T& operator*(){return _node->data;}==比较两个迭代器相等,即比较迭代器的位置(引用/地址)相同bool operator==(const Self& it){return _node == it._node;}//!=bool operator!=(const Self& it){return _node != it._node;}//->//返回的是数据的地址const T* operator->(){return &_node->data;}};

 在list类中也要添加

typedef ListConstIterator<T> const_iterator;//const迭代器,需要迭代器不能修改,还是迭代器指向的内容?
// 迭代器指向的内容不嫩被修改! const iterator不是我们需要的const迭代器
//以下是迭代器本身不能修改
//const iterator begin()错误
const_iterator begin() const
{//iterator it(_head->_next);//有名对象//return it;return const_iterator(_head->_next);//这是应用的是一个匿名对象
}const_iterator end() const
{return const_iterator(_head);
}

2.由于上述代码过于冗余,两个类的内容非常相似,可以用模板来解决问题

最终的ListIterator类

//迭代器类
// 一个链表指针用迭代器封装,实质上还是一个指针
//迭代器也就相当于指向一个节点的指针
//第二种解决const类型的迭代器问题
//利用模板来解决
template<class T,class Ref,class Ptr>
struct ListIterator
{typedef ListNode<T> Node;typedef ListIterator<T,Ref,Ptr> Self;Node* _node; //一个迭代器节点//迭代器构造ListIterator(Node *node):_node(node){}++it前置++,返回++以后的值Self& operator++(){_node = _node->_next;return *this;}it++后置++Self operator++(int){Self tmp(*this);//浅拷贝,即两个迭代器指针指向同一个空间,直接应用默认拷贝构造_node = _node->_next;return tmp;//拷贝}--itSelf& operator--(){//向前走_node = _node->_prev;return *this;}it--Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}*it解引用,返回的是数据//T& operator*()Ref operator*(){return _node->data;}==比较两个迭代器相等,即比较迭代器的位置(引用/地址)相同bool operator==(const Self& it){return _node == it._node;}//!=bool operator!=(const Self& it){return _node != it._node;}//->//返回的是数据的地址//T* operator->()Ptr operator->(){return &_node->data;}};

最终的list类

//list类
template<class T>
class list
{
public://重定义节点类名typedef ListNode<T> Node;//重定义迭代器名,作用域在list域内//没有用const迭代器时/*typedef ListIterator<T> iterator;typedef ListConstIterator<T> const_iterator;*///第二种方法解决const迭代器类typedef ListIterator<T,T&,T*> iterator;typedef ListIterator<T,const T&,const T*> const_iterator;private:Node *_head;//哨兵位size_t _size;//链表中节点的个数...
};

用模板实质上也是相当于创建了两个类,只是将创建类的工作都交给了编译器。

3.list与vector的对比

vectorlist
底层结构动态顺序表,一段连续空间带头结点的双向循环链表
随机访问支持随机访问,访问某个元素效率O(1)不支持随机访问,访问某个元素效率O(N)
插入和删除任意位置插入和删除效率低,需要搬移元素,时间复杂度O(N),插入时有可能需要增容。增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低任意位置插入和删除效率高,不需要搬移元素,时间复杂度O(1)
空间利用率底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高底层节点动态开辟,小姐点容易造成内存碎片,空间利用率低,缓存利用率低
迭代器原生态指针对原生态指针(节点指针)进行封装
迭代器失效在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来的迭代器失效,删除时,当前迭代器需要重新赋值否则会失效插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器,其他迭代器不受影响
使用场景需要高速存储,支持随机访问,不关心插入删除效率大量插入和删除操作,不关心随机访问

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com