【list】list库介绍 + 简化模拟实现

简介: 【list】list库介绍 + 简化模拟实现

本节博客先对list进行用法介绍,再在库的基础上简化其内容和形式,简单进行模拟实现,有需要借鉴即可。

1.list介绍

1.1 list概述

1.概述:list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。

2. 底层实现:list的底层是双向链表结构双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。


3. list与forward_list区别:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。


4. list的优势:与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。

5. list的缺陷:与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)

我们用代码来体会一下list的缺点:

void test_op1()
{
  srand(time(0));
  const int N = 1000000;//一百万数据
  //两个链表
  list<int> lt1;
  list<int> lt2;
  
  //一个顺序表
  vector<int> v;
  
  //生成随机数据,尾插到链表1和顺序表v中去
  for (int i = 0; i < N; ++i)
  {
    auto e = rand()+i;//加上这个i主要是为了减少重复数字概率
    lt1.push_back(e);
    v.push_back(e);
  }
  //vector排序
  int begin1 = clock();
  sort(v.begin(), v.end());
  int end1 = clock();
  
  //list排序
  int begin2 = clock();
  lt1.sort();
  int end2 = clock();
  
  //打印比较两者用时
  printf("vector sort:%d\n", end1 - begin1);
  printf("list sort:%d\n", end2 - begin2);
}
void test_op2()
{
  srand(time(0));
  const int N = 1000000;
  list<int> lt1;
  list<int> lt2;
  for (int i = 0; i < N; ++i)
  {
    auto e = rand();
    lt1.push_back(e);
    lt2.push_back(e);
  }
  // 拷贝vector
  int begin1 = clock();
  vector<int> v(lt2.begin(), lt2.end());
  
  // 排序
  sort(v.begin(), v.end());
  // 拷贝回lt2
  lt2.assign(v.begin(), v.end());
  int end1 = clock();
  
  //lt1排序
  int begin2 = clock();
  lt1.sort();
  int end2 = clock();
  
  //打印
  printf("list copy vector sort copy list sort:%d\n", end1 - begin1);
  printf("list sort:%d\n", end2 - begin2);
}

1.2相关接口的介绍

2.简化模拟实现

通过去查看stl中list.h的源码我们可以知道,list是通过一个_head的Node*指针进行维护的,而其中广泛使用迭代器进行传值和访问数据。下面对其先直接摆代码,然后对其中细节进行详细介绍。

#pragma once
#include<assert.h>
#include<iostream>
namespace zzg
{
  template<typename T>
  struct ListNode
  {
    ListNode<T>* _next;//这个地方为什么类型不是T*???答:因为我们指针是需要指向一个ListNode<T>*类型的,而非T类型。
    ListNode<T>* _prev;
    T _data;
    //ListNode有参构造
    ListNode(const T& x = T())
      :_next(nullptr)
      ,_prev(nullptr)
      , _data(x)
      {} 
  };
  template<class T, class Ref, class Ptr>
  class list_iterator
  {
    typedef struct ListNode<T> Node;
    typedef list_iterator<T, Ref, Ptr> Self;
  public:
    Node* _node;
  public:
    //带参构造
    list_iterator(Node* node)//这个地方用值拷贝,用引用会有bug
      :_node(node)
    {}
    //++
    Self& operator++()
    {
      _node = _node->_next;
      return *this;
    }
    
    Self operator++(int)//后置++
    {
      Self temp = *this;//拷贝构造一份,这时候会调用编译器自动生成的拷贝构造,为浅拷贝,但是需求满足了。
      _node = _node->_next;
      return temp;//这个地方得返回值了,因为现在的Self已经变了
    }
    //--
    Self& operator--()
    {
      _node = _node->_prev;
      return *this;
    }
    Self operator--(int)//后置--
    {
      Self temp = *this;//拷贝构造一份,这时候会调用编译器自动生成的拷贝构造,为浅拷贝,但是需求满足了。
      _node = _node->_prev;
      return temp;//这个地方得返回值了,因为现在的Self已经变了
    }
    //*it
    Ref operator*()
    {
      return _node->_data;
    }
    //!=
    bool operator!=(const Self& it) 
    {
      return _node != it._node;
    }
    //==
    bool operator==(const Self& it)
    {
      return _node == it._node;
    }
    //->重载
    Ptr operator->()
    {
      return &_node->_data;
    }
  };
  //template<typename T>
  //class list_const_iterator
  //{
  //  typedef struct ListNode<T> Node;
  //  typedef list_const_iterator<T> Self;
  //public:
  //  Node* _node;
  //public:
  //  //带参构造
  //  list_const_iterator(Node* node)//这个地方用值拷贝,用引用会有bug
  //    :_node(node)
  //  {}
  //  //++
  //  Self& operator++()
  //  {
  //    _node = _node->_next;
  //    return *this;
  //  }
  //  Self operator++(int)//后置++
  //  {
  //    Self temp = *this;//拷贝构造一份,这时候会调用编译器自动生成的拷贝构造,为浅拷贝,但是需求满足了。
  //    _node = _node->_next;
  //    return temp;//这个地方得返回值了,因为现在的Self已经变了
  //  }
  //  //--
  //  Self& operator--()
  //  {
  //    _node = _node->_prev;
  //    return *this;
  //  }
  //  Self operator--(int)//后置--
  //  {
  //    Self temp = *this;//拷贝构造一份,这时候会调用编译器自动生成的拷贝构造,为浅拷贝,但是需求满足了。
  //    _node = _node->_prev;
  //    return temp;//这个地方得返回值了,因为现在的Self已经变了
  //  }
  //  //*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;
  //  }
  //};
  template<typename T>
  class list
  {
  public:
    typedef list_iterator<T, T&, T*> iterator;
    typedef list_iterator<T, const T&, const T*> const_iterator;
    typedef struct ListNode<T> Node;
  private:
    Node* _head;//头节点
  public:
    //无参构造函数
    list()
    {
      empty_init();
    }
    //拷贝构造
    list(const list<T>& lt)
    {
      empty_init();
      for (auto& e : lt)
      {
        push_back(e);
      }
    }
    // lt1 = lt3
    list<T>& operator=(list<T> lt)//先调用拷贝构造,构造出一个lt来
    {
      swap(lt);//然后交换这个局部变量与this,原this中是其他的东西
      return *this;//返回this本身
    }
    void empty_init()
    {
      _head = new Node;
      _head->_next = _head;
      _head->_prev = _head;
    }
    //清理函数
    void clear()
    {
      iterator it = begin();
      while (it != end())
      {
        it = erase(it);
      }
    }
    //析构函数
    ~list()
    {
      clear();
      delete _head;
      _head = nullptr;
    }
    iterator begin()
    {
      return _head->_next;//隐式类型转换
      //return iterator(_head->_next);
    }
    iterator end()
    {
      return _head;//隐式类型转换
      //return iterator(_head);
    }
    
    //const + 迭代器 --> 迭代器本身不可修改
    //我们需要的:迭代器指向的内容不可修改 const T*类型 而不是 T* const类型
    //如果我们直接在一般迭代器前面+const,即const iterator --> 该迭代器不可修改,因为这是一个自定义类
    //解决,直接再单独一个自定义const迭代器类出来
    const_iterator begin() const
    {
      return _head->_next;//return iterator(_head->_next);
    }
    const_iterator end() const
    {
      return _head;//return iterator(_head);
    }
    //尾插
    void push_back(const T& x)
    {
      insert(end(), x);
    }
    //头插
    void push_front(const T& x)
    {
      insert(begin(), x);
    }
    
    //void push_back(const T& x)
    //{
    //  Node* newnode = new Node(x);
    //  //找到尾巴
    //  Node* tail = _head->_prev;
    //  tail->_next = newnode;//链接临近两点
    //  newnode->_prev = tail;
    //  newnode->_next = _head;
    //  _head->_prev = newnode;
    //}
    //任意插入
    void insert(iterator pos, const T& val)
    {
      Node* cur = pos._node;
      Node* prev = cur->_prev;
      Node* newnode = new Node(val);
      prev->_next = newnode;
      newnode->_prev = prev;
      newnode->_next = cur;
      cur->_prev = newnode;
    }
    //任意删除
    iterator erase(iterator pos)
    {
      Node* cur = pos._node;
      Node* prev = cur->_prev;
      Node* next = cur->_next;
      prev->_next = next;
      next->_prev = prev;
      delete cur;
      return iterator(next);
    }
    //尾删
    void pop_back()
    {
      erase(--end());
    }
    //头删
    void pop_front()
    {
      erase(begin());
    }
 
  };
  struct A
  {
  public:
    int _a;
    int _b;
    A(int a = 0, int b = 0)
    {
      _a = a;
      _b = b;
    }
  };
  //测试函数
  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);
    lt.push_back(6);
    lt.pop_back();
    lt.pop_back();
    lt.pop_back();
    list<int>::iterator it = lt.begin();
    while (it != lt.end())
    {
      std::cout << *it << " ";
      it++;
    }
    std::cout << std::endl;*/
    list<A> Al;
    Al.push_back({ 1,1 });
    Al.push_back({ 2,2 });
    Al.push_back({ 3,3 });
    Al.push_back({ 4,4 });
    Al.push_back({ 5,5 });
    Al.push_back({ 6,6 });
    list<A>::iterator it = Al.begin();
    while (it != Al.end())
    {
      //std::cout << (*it)._a << '/' << (*it)._b << std::endl;
      std::cout << it->_a << '/' << it->_b << std::endl;//it->_a  --->  it->->_a == it.operator->()->_a;
      it++;
    }
    std::cout << std::endl;
  }
}

3.各部分的细节详述

主要包含三个部分,一是整体的链表类,二是链表中的每个元素结点类,还有就是用来访问修改结点的迭代器类

下面分开进行细节介绍。

3.1结点

每个结点我们很熟悉,无非是两个指针和一个数据,一个指针指向它前面的结点,另一个指向其后面的结点,数据中放具体元素的值。

下面是基本结构框架:

template<typename T>
struct ListNode
{
  ListNode<T>* _next;//这个地方为什么类型不是T*???答:因为我们指针是需要指向一个ListNode<T>*类型的,而非T类型。
  ListNode<T>* _prev;
  T _data;
  //ListNode有参构造
  ListNode(const T& x = T())
    :_next(nullptr)
    ,_prev(nullptr)
    , _data(x)
    {} 
};

细节:

  • 使用struct,标注为公有属性,方便外部调用
  • list是带头双向循环链表,因而每个结点要有两个指针
  • 提供全缺省的默认构造函数

思考:每个结点的两个指针为什么是ListNode*类型而不是T*类型呢?

答:因为我们每个结点的指针指向的是一个结点,T仅仅是一个结点中的数据而已。

3.2迭代器

template<class T, class Ref, class Ptr>
class list_iterator
{
  typedef struct ListNode<T> Node;
  typedef list_iterator<T, Ref, Ptr> Self;
public:
  Node* _node;
public:
  //带参构造
  list_iterator(Node* node)//这个地方用值拷贝,用引用会有bug
    :_node(node)
  {}
}

细节1:迭代器用原生指针还是专门设计为类的问题

思考:list迭代器为什么要专门设置一个类???

答:

这是由于list的每个节点物理空间不连续,导致迭代器不能像之前string\vector那样简单的设计为原生指针,而是设计为一个类,以此来扩大我们对迭代器行为控制权限,重新设计*,->,++等操作。

vector,string原生指针充当迭代器:

像之前string,vector这种容器,其原生指针T*就是天然的迭代器,因为++就会自动指向到下一个数据,*引用也是拿到的我们想要的数据。

但是在list中,我们++T*,很明显由于地址不连续的缘故,压根不知道会指向的是什么(大概率会是随机值)。

但是需要重点注意的是,我们所设计的迭代器类是模拟的string/vector中T*的动作。

细节2:迭代器++、–行为重载的返回类型问题

//++
Self& operator++()
{
  _node = _node->_next;
  return *this;
}
Self operator++(int)//后置++
{
  Self temp = *this;//拷贝构造一份,这时候会调用编译器自动生成的拷贝构造,为浅拷贝,但是需求满足了。
  _node = _node->_next;
  return temp;//这个地方得返回值了,因为现在的Self已经变了
}
//--
Self& operator--()
{
  _node = _node->_prev;
  return *this;
}
Self operator--(int)//后置--
{
  Self temp = *this;//拷贝构造一份,这时候会调用编译器自动生成的拷贝构造,为浅拷贝,但是需求满足了。
  _node = _node->_prev;
  return temp;//这个地方得返回值了,因为现在的Self已经变了
}

思考:为什么前置++返回的是类对象引用,而后置++返回类型是一般类型?

答:这要结合函数设计来看,在前置++中,我们返回的是类本身;而后置++,我们返回的是一个局部的类对象,局部类对象在出函数后会自动销毁

细节3:迭代器解引用返回类型

//*it
Ref operator*()
{
  return _node->_data;
}

注:

//iterator:
typedef list_iterator<T, Ref, Ptr> Self;
//list:
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;

返回Ref,用来区分const_iterator 和 iterator。

思考:为什么不重载const iterator?

答:const + 类 --> 表示该指针不可修改,并非我们所期望的指针指向内容不可修改。

思考:iterator 与 const_iterator 是同一个类吗?

答:不是。是利用同一份类模板生成的完全不同两份类。

细节4:迭代器operator->重载

//->重载
Ptr operator->()
{
  return &_node->_data;
}

注:

//iterator:
typedef list_iterator<T, Ref, Ptr> Self;
//list:
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
struct A
{
public:
  int _a;
  int _b;
  A(int a = 0, int b = 0)
  {
    _a = a;
    _b = b;
  }
};
main:
list<A> Al;
Al.push_back({ 1,1 });
Al.push_back({ 2,2 });
Al.push_back({ 3,3 });
Al.push_back({ 4,4 });
Al.push_back({ 5,5 });
Al.push_back({ 6,6 });
list<A>::iterator it = Al.begin();
while (it != Al.end())
{
  //std::cout << (*it)._a << '/' << (*it)._b << std::endl;
  std::cout << it->_a << '/' << it->_b << std::endl;//it->_a  --->  it->->_a == it.operator->()->_a;
  it++;
}
std::cout << std::endl;

为了可读性,编译器把it->->_a优化为了it->_a

思考:上述代码的it->->_a代表了什么?

答:it->_a —> it->->_a == it.operator->()->_a;,这里在编译器写法上理论上应该写两个箭头的,一个用于运算符重载函数的调用,另一个是为了进入到指针里面访问数据,这里编译器为了可读性将其优化为了一个箭头。

3.3链表

链表类主要结构如下:

template<typename T>
class list
{
public:
  typedef list_iterator<T, T&, T*> iterator;
  typedef list_iterator<T, const T&, const T*> const_iterator;
  typedef struct ListNode<T> Node;
  
  //无参构造函数
  list()
  {
    empty_init();
  }
  void empty_init()
  {
    _head = new Node;
    _head->_next = _head;
    _head->_prev = _head;
  }
  
private:
  Node* _head;//头节点
}

细节1:list中提供begin和end函数的理由和返回类型?

iterator begin()
{
  return _head->_next;//隐式类型转换
  //return iterator(_head->_next);
}
iterator end()
{
  return _head;//隐式类型转换
  //return iterator(_head);
}
//const + 迭代器 --> 迭代器本身不可修改
//我们需要的:迭代器指向的内容不可修改 const T*类型 而不是 T* const类型
//如果我们直接在一般迭代器前面+const,即const iterator --> 该迭代器不可修改,因为这是一个自定义类
//解决,直接再单独一个自定义const迭代器类出来
const_iterator begin() const
{
  return _head->_next;//return iterator(_head->_next);
}
const_iterator end() const
{
  return _head;//return iterator(_head);
}

思考:list提供begin和end的原因:

答:

1._head是私有的。

2.更加方便的使用迭代器,在上面代码我们可以发现,返回的都是迭代器类型。

返回迭代器类型的原因:

与后面使用迭代器相兼容。

细节2:插入元素代码

//尾插
void push_back(const T& x)
{
  insert(end(), x);
}
//头插
void push_front(const T& x)
{
  insert(begin(), x);
}
//void push_back(const T& x)
//{
//  Node* newnode = new Node(x);
//  //找到尾巴
//  Node* tail = _head->_prev;
//  tail->_next = newnode;//链接临近两点
//  newnode->_prev = tail;
//  newnode->_next = _head;
//  _head->_prev = newnode;
//}
//任意插入
void insert(iterator pos, const T& val)
{
  Node* cur = pos._node;
  Node* prev = cur->_prev;
  Node* newnode = new Node(val);
  prev->_next = newnode;
  newnode->_prev = prev;
  newnode->_next = cur;
  cur->_prev = newnode;
}

细节3:删除元素代码

//任意删除
iterator erase(iterator pos)
{
  Node* cur = pos._node;
  Node* prev = cur->_prev;
  Node* next = cur->_next;
  prev->_next = next;
  next->_prev = prev;
  delete cur;
  return iterator(next);
}
//尾删
void pop_back()
{
  erase(--end());
}
//头删
void pop_front()
{
  erase(begin());
}

思考:为什么erase()要返回迭代器类型???

答:因为要及时对外更新迭代器指针,防止迭代器失效

思考:为什么pop_back()没有返回迭代器类型?

答:因为pop_back()不会对外接收迭代器,不存在对外更新迭代器问题。但是erase是接收迭代器的,因而要及时更新。

思考:–end()是否会影响到_head,为什么?

答:会影响到,但是影响到的是_head的值拷贝,没有影响到“母体”。

细节4:clear()函数和析构函数

void empty_init()
{
  _head = new Node;
  _head->_next = _head;
  _head->_prev = _head;
}
//清理函数
void clear()
{
  iterator it = begin();
  while (it != end())
  {
    it = erase(it);
  }
}
//析构函数
~list()
{
  clear();
  delete _head;
  _head = nullptr;
}

细节5:拷贝构造函数与赋值运算符重载

//拷贝构造
list(const list<T>& lt)
{
  empty_init();
  for (auto& e : lt)
  {
    push_back(e);
  }
}
// lt1 = lt3
list<T>& operator=(list<T> lt)//先调用拷贝构造,构造出一个lt来
{
  swap(lt);//然后交换这个局部变量与this,原this中是其他的东西
  return *this;//返回this本身
}

思考:为什么赋值运算符重载函数参数用list lt而不是引用呢?

答:复用拷贝构造函数,是为现代写法。

细节6:insert返回为void?

//任意插入
void insert(iterator pos, const T& val)
{
  Node* cur = pos._node;
  Node* prev = cur->_prev;
  Node* newnode = new Node(val);
  prev->_next = newnode;
  newnode->_prev = prev;
  newnode->_next = cur;
  cur->_prev = newnode;
}

在string中insert我们返回的是迭代器,但是这里为什么返回值是void呢?

答:因为string是连续空间,插入数据会挪动数据,造成迭代器失效。但是链表是由结点链接而成,插入数据不会挪动数据,不会造成迭代器失效问题。

在vector中 insert/erase因为增删都会牵扯到数据挪动问题,两个函数肯定都要去返回迭代器来更新外部迭代器。

但是对于list,insert不会挪动数据因而不会失效,但是erase时候,原结点被删除,会造成迭代器失效。

4.总结

list模拟实现核心就是一个类迭代器的实现,相比之前string、vector,list迭代器更值得细细思考与总结。

list模拟实现还一个难点在于使用类模板,应注意类模板问题。


EOF


相关文章
|
3月前
|
存储 缓存 C语言
【C++】list介绍以及模拟实现(超级详细)
【C++】list介绍以及模拟实现(超级详细)
114 5
|
3月前
|
存储 算法 Java
【CPP】slt-list由认识到简化模拟实现深度理解~
【CPP】slt-list由认识到简化模拟实现深度理解~
|
3月前
|
存储 缓存 算法
【C++】list的模拟实现
【C++】list的模拟实现
|
4月前
|
Java C++ Python
【c++】list 模拟
【c++】list 模拟
29 1
|
4月前
|
存储 编译器 C语言
【C++】list模拟实现
本文档介绍了C++ STL中`list`容器的模拟实现,包括`ListNode`节点类、迭代器类和`list`类的详细设计。`ListNode`模板类存储数据并维护前后指针;`ListIterator`是一个复杂的模板类,提供解引用、自增/自减以及比较操作。`list`类包含了链表的各种操作,如插入、删除、访问元素等,并使用迭代器作为访问接口。实现中,迭代器不再是简单的指针,而是拥有完整功能的对象。此外,文档还提到了迭代器的实现对C++语法的特殊处理,使得`it-&gt;_val`的写法成为可能。文章通过分步骤展示`list`的各个组件的实现,帮助读者深入理解STL容器的内部工作原理。
|
5月前
|
C++
【c++】list模拟实现(2)
【c++】list模拟实现(2)
28 0
|
5月前
|
C++
【c++】list模拟实现(1)
【c++】list模拟实现(1)
29 0
|
6月前
|
存储 编译器 C++
list的介绍及其模拟实现
list的介绍及其模拟实现
48 2
|
6月前
|
存储 C++ 容器
C++:List的使用和模拟实现
C++:List的使用和模拟实现
|
6月前
|
存储 设计模式 C++
C++——list的使用及其模拟实现
C++——list的使用及其模拟实现
下一篇
无影云桌面