C++初阶----list模拟实现 +(迭代器总结)

1)list介绍

结构,list的迭代器用类封装为了实现重载,<T,T&,T*>

template < class T, class Alloc = allocator<T> > class list;
  • ①:list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代
  • ②: list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素
  • ③: list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效
  • ④: 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好
  • ⑤:与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)
  • ⑥: list 有一个重要性质:插入操作(insert)与接合操作(splice)都不会造成原有的list迭代器失效。这在vector是不成立的,因为vactor的插入可能引起空间的重新配置,导致原来的迭代器全部失效。list的迭代器失效,只会出现在删除的时候,指向删除元素的那个迭代器在删除后失效

2)list使用

详细请参考:cplusplus -list

①list构造函数

参见下面模拟实现部分

②list的访问及遍历

③list容量操作

③list修改

注意容器尽量只用自己的swap,std库中的实现如下

template <class T> void swap ( T& a, T& b )
{
 T c(a); a=b; b=c;
}

对于list容器的swap,只用交换头指针就行了,
库中的T c(a);语句是进行的深拷贝,自然不推荐使用

④list操作

①:list的成员函数sort不建议使用,因为是归并排序,效率低无意义(链表的快排有缺陷有序时候是O(N^2))
②: 注意list不能使用库函数的sort,库函数是使用的快排

  1. list迭代器不是原生指针,++不是取到下一个数据,地址是随机的,要实现找到下一个位置的这个操作本身实现就需要O(N)的时间复杂度,所以无意义
  2. 库函数里的sort用了迭代器相减,变相只支持原生指针,list迭代器不能相减

3)list模拟实现(结构:三个类)

list底层是一个带头双向循环链表
在这里插入图片描述

Ⅰ.节点类

前面说过struct中的成员默认都是公有的(public)

//1. List的节点类
template<class T>
struct ListNode
{
   ListNode(const T& val = T())//匿名对象缺省值
       :_prev(nullptr)
       ,_next(nullptr)
       ,_val(val)
   {}
   ListNode<T>* _prev;
   ListNode<T>* _next;
   T _val;
};

Ⅱ. List的迭代器类

  1. 当我们要使用const_iterator时,只能重新定义一个const版的List的迭代器类
  2. 而const_iterator和iterator只有解引用的重载不一样我们使用这种方法
  3. 类模板参数改为三个分别对应T,T&,T*
template<class T, class Ref, class Ptr>
struct ListIterator
{
   typedef ListNode<T> Node;
   typedef ListIterator<T, Ref, Ptr> Self;
   Node* _pNode  
};

1.构造
①默认构造和拷贝构造

注意 在List的迭代器类里不需要自定义析构函数,默认析构函数已经够用了

ListIterator(Node* pNode = nullptr)
   :_pNode(pNode)
{}
ListIterator(const Self& l)
	: _pNode(l._pNode)
{}

2.运算符重载
①解引用*和->重载

解引用*

Ref operator*()
{
    return _pNode->_val;
}
//Self* operator->();

->重载

Self* operator->()
{
   return &_pNode->data;
}

编译器对‘->’的省略

假如我们有一个结构体类型

struct TreeNode
{	
	struct TreeNode* _left;
	struct TreeNode* _right;
	int _val;
	TreeNode(int val = -1)
		:_left(nullptr)
		, _right(nullptr)
		, _val(val)
	{}
};

构建一个树

list<TreeNode> lt;
lt.push_back(TreeNode(1));
lt.push_back(TreeNode(2));
lt.push_back(TreeNode(3));
lt.push_back(TreeNode(4));

list<TreeNode>::iterator it = lt.begin();
while (it != lt.end())
{
	//cout << (*it)._val << " ";
	printf("val:%d,left:%p,right:%p\n", (*it)._val, (*it)._left, (*it)._right);
	printf("val:%d,left:%p,right:%p\n", it->_val, it->_left, it->_right);

	++it;
}

想要拿到树节点的_val值有三种方法

  1. cout << *it,参考上面的重载*可以得知这里的*it是TreeNode结构体而不是结构体里的_val值,所以我们可以在本命名空间重载一个<<运算符 ostream& operator<<(ostream& out, TreeNode n);
  2. 直接cout << (*it)._val (*it==it.operator*())
  3. 利用->的重载:it->_valit->==it.operator->()
    但是->的重载返回的是TreeNode*指针,不是应该这样调用吗it->->_val?
    !!注意!!:所有编译器对->->做了优化,省略为一个->
②前置/后置++/ - -

注意 后置,临时变量出作用域销毁,不能引用返回

Self& operator++()//注意 这是前置++
{
   _pNode = _pNode->_next;
   return *this;
}
Self operator++(int)//注意 后置++要加int
//注意 临时变量出作用域销毁,不能引用返回
{
   Self tmp = _pNode;
   _pNode = _pNode->_next;
   return tmp;
}
Self& operator--()//注意 这是前置++
{
   _pNode = _pNode->_prev;
   return *this;
}
Self operator--(int)//注意 后置++要加int
{
   Self tmp = _pNode;
   _pNode = _pNode->_prev;
   return tmp;
}
③关系运算符!= ==重载
bool operator!=(const Self& it) const
{
    return _pNode != it._pNode;
}
bool operator==(const Self& it) const
{
    return (_pNode == it._pNode);
}
3.list迭代器失效问题

仅在节点被删除的时候会造成迭代器失效,迭代器失效问题在C++初阶—vector的使用及模拟实现中详细探讨了,这里不再赘述,参考下面的erase实现


Ⅲ. list类


swap

用于赋值重载和拷贝构造的现代写法
当然使用库函数里的可以这样达到一样的效果 std::swap(_head,l._head);(只交换头节点)

void swap(list<T>& l)
{
   PNode tmp = _head;
   _head = l._head;
   l._head = tmp;
}

1.构造
①默认构造
list()
{
   _head = new Node;
   _head->_next = _head;
   _head->_prev = _head;
}
②构造有n个节点值为val的list
list(int n, const T& value = T())
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;
	while (n--)
	{
		push_back(value);
	}
}
③拷贝构造

传统写法

// lt2(lt1) -- 传统写法
list(const list<T>& l)        
{
   _head = new Node;
   _head->_next = _head;
   _head->_prev = _head;
   for (const auto& e : l)
   {
       push_back(e);
   }
}

现代写法(与传统写法实际上没有太大区别)

//lt2(lt1) -- 现代写法
list(const list<T>& l)
{
    _head = new Node;
    _head->_next = _head;
    _head->_prev = _head;
    list<T> tmp(l.begin(), l.end());
    this->swap(tmp);
}
④迭代器区间

支持各种迭代器类型

template <class Iterator>
list(Iterator first, Iterator last)
{
    _head = new Node;
    _head->_next = _head;
    _head->_prev = _head;
    PNode cur = _head;
    while (first != last)
    {
        push_back(*first);
        first++;
    }
}

2.赋值运算符重载

传统写法 注意范围for加&防止T是自定义类型时深拷贝

//传统
list<T>& operator=(const list<T>& l)
{  
   if (this != &l)
   {
       this->clear();
       for (const auto& e : l)//引用防止深拷贝
       {
           push_back(e);
       }
   }
   return *this;
}

现代写法(对于深拷贝现代写法一定比传统写法更简单

//现代
list<T>& operator=(list<T> l)
{
   swap(l);
   return *this;
}

3.析构

注意clear不会清理头节点

void clear()
{
   list<T>::iterator cur = begin();
   while (cur != end())
   {
       cur = erase(cur);//注意迭代器失效问题
   }
}

析构直接复用clear+清理头节点即可

~list()
{
   clear();
   delete _head;
   _head = nullptr;
}
4. 容量
①size

不包括头节点,节点个数

size_t size()const
{
   size_t count = 0;
   PNode cur = _head->_next;
   while (cur!=_head)
   {
       cur = cur->_next;
       count++;
   }
   return count;
}
②empty

判空

bool empty()const
{
    return _head->_next == _head;
}

5.访问
①front

分为可写只读两种

T& front()
{
   assert(!empty());
   return _head->_next->_val;
}
const T& front()const//常
{
   assert(!empty());
   return _head->_next->_val;
}
②back

分为可写只读两种

T& back()
{
   assert(!empty());
   return _head->_prev->_val;
}
const T& back()const
{
   assert(!empty());
   return _head->_prev->_val;
}

6.修改操作
①insert

操作和之前c语言实现带头双向循环链表一样,不再赘述

// 在pos位置前插入值为val的节点
iterator insert(iterator pos, const T& val)
{
   Node* cur = pos._pNode;
   Node* prev = cur->_prev;
   Node* newnode = new Node(val);
   newnode->_next = cur;
   cur->_prev = newnode;
   prev->_next = newnode;
   newnode->_prev = prev;
   //1. 原始 
   //iterator ret(newnode);//
   //return ret;//
   
   //2. 隐式类型转换
   //return newnode;

   //3. 匿名对象
   return iterator(newnode);//== return ListIterator<T>(newnode); 

返回值有三种写法

  1. 原始
    iterator ret(newnode);
    return ret;
  2. 隐式类型转换
    return newnode;
  3. 匿名对象
    return iterator(newnode);
②erase

防止出现迭代器失效,需要返回一个iterator指向删除位置的下一个位置的

// 删除pos位置的节点,返回该节点的下一个位置
iterator erase(iterator pos)
{
   assert(pos!=end());
   Node* cur = pos._pNode;
   Node* prev = cur->_prev;
   prev->_next = cur->_next;
   cur->_next->_prev = prev;
   delete cur;
   return iterator(prev->_next);
}
③pop_back,push_back等复用

服用

void push_back(const T& val)
{ 
   insert(end(), val);
   //Node* tail = _head->_prev;
   //Node* newnode = new Node(val);//节点直接有构造函数,不需要自己给_val值了
   //tail->_next = newnode;
   //newnode->_prev = tail;
   //newnode->_next = _head;
   //_head->_prev = newnode;
}
void pop_back()
{ 
   erase(--end());
}
void push_front(const T& val) 
{ 
   insert(begin(), val);
}
void pop_front()
{
   erase(begin());
}

4)迭代器总结

①:迭代器从使用功能分类

  1. 正向/正向const
  2. 反向/反向const

②:迭代器从底层结构分类

  1. 单向
    单链表/哈希表(只支持++)
  2. 双向
    双向链表/二叉树/map(支持++/ - -)
  3. 随机
    dequeue/vector/string/map(支持++ /- -/ +/ -)

随机>双向>单向(包含关系)
在这里插入图片描述
迭代器在不暴露容器底层实现细节的情况下,提供统一的方式去修改容器中储存的数据,是算法和容器的胶合剂
思考 在模拟实现list第一感觉会直接进行遍历节点的操作,而不是利用实现好的迭代器进行操作,是对list底层体会不够深刻

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值