【C++】STL之list学习

版权声明:本文为博主原创文章,欢迎转载,转载请声明出处! https://blog.csdn.net/hansionz/article/details/84435599

一. list是什么?

1.list概述

  • list是可以在常数时间范围内在任意位置进行插入和删除序列式容器,并且该容器可以前后双向迭代
  • list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向前一个元素和后一个元素。
  • listforward_list非常相似,最主要的不同在于forward_list单链表,只能单向迭代

2.list相对其他容器的优缺点

  • 与其他的序列式容器相比(array,vector,deque)list通常在任意位置进行插入、移除元素的执行效率更好,一般在常数时间内。
  • list和forward_list最大的缺陷不支持任意位置的随机访问。比如:要访问list第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(指向前一个结点或后一个结点的指针域)
  • list相对于vector还有一个好处就是空间的利用率比较高。每删除或者插入一个元素,就配置或释放一个空间。

3.list的数据结构

SGI版本的list不仅仅是一个双向链表,而且还是一个环状双向链表,也就是一条双向循环链表
在这里插入图片描述

注:本图截取自《STL源码剖析一书》

二.list的使用(常见使用接口)

1.list的常见构造函数

  • list():构造空的list
  • list (size_type n, const value_type& val = value_type()):构造一个含有nval值的list
  • list (const list& x):拷贝构造函数
  • list (InputIterator first, InputIterator last):迭代器区间构造
void test1()
{
	list<int> l1;//空构造
	list<int> l2(4, 10);//构造4个值为10的结点
	list<int> l3(l2);//拷贝构造
	list<int> l4(l2.begin(), l2.end());//使用l2的迭代器区间[begin,end)构造
	
	//C++11语法糖遍历
	for (auto& e : l4)
	{
		cout << e << " ";
	}
	cout << endl;

	int arr[] = { 1, 2, 3, 4 };//使用数组为迭代器区间构造
	list<int> l5(arr, arr + (sizeof(arr) / sizeof(int)));
	//迭代器遍历
	list<int>::iterator it = l5.begin();
	while (it != l5.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

2.list常见的iterator

2.1 常见接口

  • begin():返回第一个元素的迭代器
  • end():返回最后一个元素下一个位置的迭代器
  • rbegin():返回第一个位置的反向迭代器,既end() - 1
  • rend():返回最后一个元素下一个位置的reverse_iterator,即end()位置
  • cbegin() (C++11):返回第一个元素的const_iterator
  • cend() (C++11):返回最后一个元素下一个位置的const_iterator

2.2 迭代器在list的位置

在这里插入图片描述

  • begin与end正向迭代器,对迭代器执行++操作,迭代器向后移动
  • rbegin()与rend()反向迭代器,对迭代器执行++操作,迭代器向前移动
  • cbegin与cendconst正向迭代器,与begin和end不同的是:该迭代器指向节点中的元素值不能修改
  • crbegin与crendconst的反向迭代器,与rbegin和rend不同的是:该迭代器指向节点中的元素值不能修改

2.3 iterator的使用

void test2()
{
	int arr[] = { 1, 2, 3, 4 };
	list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
	//正向迭代器 1 2 3 4
	list<int>::iterator it = l1.begin();
	while (it != l1.end())
	{
		//*it *= 2;可以修改
		cout << *it << " ";
		++it;
	}
	cout << endl;
	//正向const迭代器 1 2 3 4(但是*it不能修改)
	list<int>::const_iterator cit = l1.cbegin();
	while (cit != l1.cend())
	{
		//*cit *= 2; 不能修改
		cout << *cit << " ";
		++cit;
	}
	cout << endl;
	//反向迭代器 4 3 2 1
	list<int>::reverse_iterator rit = l1.rbegin();
	while (rit != l1.rend())
	{
		//*rit *= 2; 可以修改
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
	//反向const迭代器 4 3 2 1
	list<int>::const_reverse_iterator crit = l1.crbegin();
	while (crit != l1.crend())
	{
		//*crit *= 2; 不可以修改
		cout << *crit << " ";
		++crit;
	}
	cout << endl;
}

3.list的size和empty接口

  • bool empty() const:检测list是否为空,是返回true,否则返回false
  • size_t size() const:返回list中有效节点的个数
void test3()
{
	int arr[] = { 1, 2, 3, 4 };
	list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
	int size = l1.size();
	//输出l1的大小
	cout << size << endl;
	//判断l1是否为空,不为空遍历打印
	if (l1.empty())
	{
		cout << "list为空" << endl;
	}
	else
	{
		for (auto e : l1)
		{
			cout << e << " ";
		}
		cout << " ";
	}
}

4.list获取数据元素(front&&back)

  • reference front():返回list第一个结点值的引用
  • const_reference front() const:返回list的第一个节点中值的const引用
  • reference back():返回list的最后一个节点中值的引用
  • const_reference back() const:返回list的最后一个节点中值的const引用
void test4()
{
   int arr[] = { 1, 2, 3, 4 };
   list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
   l1.front() = 10;//将l1中的第一个数据改为10
   const int& x = l1.back();//获取l1中最后一个结点值的const引用
   //x = 10;const引用不可改变
   cout << x << endl;
}

5.list的增删改查接口

5.1 接口说明

  • void push_front (const value_type& val):在list第一个结点前插入值为 val的结点

  • void pop_front():删除list中第一个结点

  • void push_back (const value_type& val):在list最后一个结点后插入值为val的结点

  • void pop_back():删除list中最后一个结点

  • iterator insert (iterator position, const value_type& val):在list position 位置中插 入值为val的结点

  • void insert (iterator position, size_type n, const value_type& val):在position位置插入值为val的n个结点

  • void insert (iterator position, InputIterator first, InputIterator last):在list position位置插入 [first, last)区间中元素

  • iterator erase (iterator position):删除position位置上的结点

  • iterator erase (iterator first, iterator last): 删除list中[first, last)区间中的元素

  • void swap (list& x):交换两条list中的结点

  • void resize (size_type n, value_type val = value_type()):将list中有效元素个数改变 到n个,多出的元素用val 填充

  • void clear():清空list中的有效元素

下边3个接口为C++11新特性的接口:

  • template <class... Args> void emplace_front (Args&&... args):在list中的第一个结点前根据参数构造新的结点
  • template <class... Args> void emplace_back (Args&&... args):在list最后一个结点后根据参数直接构造新的结点
  • template <class... Args> iterator emplace( const_iterator position, Args&&... args):在list的任意位置根据参数直接构造新的结点
/*测试emplace_back、emplace_front、emplace */
class Date {
public:    
	Date(int year = 1900, int month = 1, int day = 1) 
		: _year(year)
		, _month(month)
		, _day(day)    
	{ 
		cout << "Date(int, int, int):" << this << endl; 
	}

	Date(const Date& d) 
		: _year(d._year)
		, _month(d._month)
		, _day(d._day)    
	{ 
		cout << "Date(const Date&):" << this << endl; 
	}

private:    
	int _year;    
	int _month;    
	int _day;
};
//push_back尾插:先构造好元素,然后将元素拷贝到节点中,插入时先调构造函数,再调拷贝构造函数 
//emplace_back尾插:先构造节点,然后调用构造函数在节点中直接构造对象 
//emplace_back比push_back更高效,少了一次拷贝构造函数的调用 
void test8()
{
	list<Date> l;    
	Date d(2018, 10, 20);    
	l.push_back(d); //构造--拷贝构造
	l.emplace_back(2018, 10, 21);  //插入时直接构造 
	l.emplace_front(2018, 10, 19); //插入时直接构造 
}

5.2 测试功能

/*测试push_front、push_back、pop_front、pop_back*/
void test5()
{
	int arr[] = { 1, 2, 3, 4 };
	list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
	l1.push_front(0);
	l1.push_back(5);
	PrintList(l1);//0 1 2 3 4 5

	l1.pop_front();
	l1.pop_back();
	PrintList(l1);//1 2 3 4 
}
/*测试insert、erase*/
void test6()
{
	int arr[] = { 1, 2, 3, 4 };
	list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
	
	//获得值为3的迭代器
	list<int>::iterator pos = find(l1.begin(), l1.end(), 3);
	
	//在pos前插入值为0的结点
	l1.insert(pos, 0);
	PrintList(l1);//1 2 0 3 4 
	
	//在pos前插入5个值为8的结点
	l1.insert(pos, 2, 8);
	PrintList(l1);//1 2 0 8 8 3 4

	//在pos前插入[v.begin(), v.end)区间中的结点
	vector<int> v(2, 7);
	l1.insert(pos, v.begin(), v.end());
	PrintList(l1);//1 2 0 8 8 7 7 3 4

	//删除pos位置的元素
	l1.erase(pos);
	PrintList(l1);//1 2 0 8 8 7 7 4

	//删除迭代器区间的值
	l1.erase(l1.begin() , l1.end());
	PrintList(l1);//list为空
}
/* 测试resize、swap、clear */
void test7()
{
	int arr[] = { 1, 2, 3, 4 };
	list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
	PrintList(l1);//1 2 3 4

	//将l1中元素个数增加到10个,多出的元素用默认值填充    
	//如果list中放置的是内置类型,默认值为0
	//如果list中放置自定义类型元素,调用缺省构造函数
	l1.resize(6);
	PrintList(l1);//1 2 3 4 0 0

	//将l1中的元素增加到8个,多出的元素用8来填充
	l1.resize(8, 8);
	PrintList(l1);//1 2 3 4 0 0 8 8

	//将l1中的元素减少到5个 
	l1.resize(5);
	PrintList(l1);//1 2 3 4 0

	// 用vector中的元素来构造list    
	vector<int> v{ 4, 5, 6 };    
	list<int> l2(v.begin(), v.end());    
	PrintList(l2);//4 5 6

	//交换l1和l2的元素
	l1.swap(l2);
	PrintList(l1);//4 5 6
	PrintList(l2);//1 2 3 4 0

	// 将l2中的元素清空    
	l2.clear();    
	cout << l2.size() << endl;//0
	PrintList(l2);//空
}

6.list的迭代器失效问题

vector的insert和erase迭代器都会失效,vector在insert时会出现扩容的问题,所以使用迭代器时可能会出现野指针问题,vector在erase时由于vs的检查机制,虽然将pos迭代器位置的数据删除后,会将后边的元素移动到前边,不会存在野指针,但是vs编译器会检查出来,这个迭代器已经失效。相对而说,listinsert不会失效,因为list不存在扩容的问题,但是erase也会存在失效,失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。例如:(1-2-3,删除2之后,1-3,迭代器依旧指向2,但是2已经被删除了,所以会出现类似野指针的问题)

void test9()
{
	int arr[] = { 1, 2, 3, 4 };
	list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));

	list<int>::iterator it = l1.begin();
	while (it != l1.end())
	{
		// erase()函数执行后,it所指向的节点已被删除
		//因此it无效,在下一次使用it时,必须先给其赋值
		//l1.erase(it);
		//++it;这样做编译器报错迭代器失效

		/*可以这么修改*/
		//it = l1.erase(it);//erase删除之后,返回下一个位置的迭代器
		l1.erase(it++);//也可以这么写,相当于it = l1.erase(it);
	}
}

三.list的模拟实现

1.list的结点类

list本身list的结点是两个不同的结构,要实现list,必须先要实现它的结点类。要实现的是一个双向带头循环链表,所以结点设计必须包括三个域,既prev指针域(指向前一个几点)、next指针域(指向后一个结点)、data(数据域)

template<class T>
  //为什么不用struct而用class?
  //如果不使用访问限定符使用struct,struct默认访问限定符为public
  //如果使用访问此限定符使用class,class默认访问限定符为class
  struct ListNode
  {
    //构造函数(T()相当于是一个匿名对象)
    ListNode(const T& data = T())
     :_data(data)
     ,_next(nullptr)
     ,_prev(nullptr)
    {}
    
	//不需要重载析构函数,直接使用默认生成的析构即可,因为成员变量只是3个指针
	
    T _data; //数据域
    ListNode<T>* _next;//指向前一个结点的指针
    ListNode<T>* _prev;//指向回一个结点的指针
  };

2.list的迭代器

list不能像vector一样以一个原生指针作为迭代器,因为它的结点不能保证在存储空间中连续存在list迭代器必须要能够指向list的结点,并且有能力进行正确的递增、递减、取值、成员取用等操作。递增操作时指向下一个结点,递减操作时指向下一个操作,取值时取的是结点的数据值,成员取用的是结点的成员。所以我们应该讲上述的成员和操作封装在一个类中。我们可以将原生态指针进行封装,因迭代器的使用形式与指针完全相同,因此,在自定义的类中必须实现以下方法:

  • 指针可以解引用,迭代器的类中必须重载operator*()
  • 指针可以++向后移动,迭代器类中必须重载operator++()operator++(int)operator--()和operator--(int)
  • 迭代器需要进行是否相等的比较,因此还需要重载operator==()与operator!=()
/**
   * 模拟实现list的迭代器类
   */ 
  template<class T>
  struct ListIterator
  {
    typedef ListNode<T> Node;
    typedef ListIterator<T> iterator;

    //构造
    ListIterator(Node* node)
      :_node(node)
    {}
    //拷贝构造
    ListIterator(const iterator& i)
      :_node(i._node)
    {}

    /**
     * 重载迭代器的解引用、++、!=、==、--等
     */ 
    // *it
    T& operator*()
    {
      return _node -> _data;
    }
    // ++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;
    }
    // it1 != it2
    bool operator!=(const iterator& it)
    {
      return _node != it._node;
    }
    // it1 == it2
    bool operator==(const iterator& it)
    {
      return _node == it._node;
    }
    Node* _node;
  };

3.list构造函数

list的常见构造函数在上边的已经提及,可以根据其功能模拟实现它:

 //迭代器定义为和库一样的名字可以支持语法糖
    typedef ListIterator<T> iterator;
    /**
     * 构造函数的模拟实现
     */ 

    //无参构造函数,构造一条只有头结点的空链表
    List()
      :_head(new Node)
    {
      _head -> _next = _head;
      _head -> _prev = _head;
    }
    //构造一条含有n个值为data的结点的链表
    List(int n, const T& data = T())
      :_head(new Node)
    {
      _head -> _next = _head;
      _head -> _prev = _head;

      for(int i = 0;i < n;i++)
      {
        PushBack(data);
      }
    }
    //迭代器区间构造
    template<class InputIterator>
    List(InputIterator first,InputIterator last)
      :_head(new Node)
    {
      _head -> _next = _head;
      _head -> _prev = _head;

      while(first != last)
      {
        PushBack(*first);
        ++first;
      }
    }
    //拷贝构造(现代方法)
    List(List<T>& l)
      :_head(new Node)
    {
      _head -> _next = _head;
      _head -> _prev = _head;

      List<T> tmp(l.begin(),l.end());
      swap(_head, tmp._head);
    }
    //赋值运算符重载
    List<T>& operator=(List<T>& l)
    {
      if(this != &l)
      {
        List<T> tmp(l);
        swap(_head, l._head);
      }
      return *this;
    }
    //析构函数
    ~List()
    {
      Clear();
      delete _head;
      _head = nullptr;
    }

4.list的元素操作

list所提供的元素操作有很多,我们只需要模拟实现其核心的接口即可。例如,push_front、push_back、earse、insert等等。

    /**
     * List Modify
     */ 
    void PushBack(const T& data)
    {
      Node* tail = _head -> _prev;
      Node* newnode = new Node(data);

      //_head tail newnode 
      tail -> _next = newnode;
      newnode -> _prev = tail;
      newnode -> _next = _head;
      _head -> _prev = newnode;
    }
    void PopBack()
    {
      Node* del = _head -> _prev;
      if(del != _head)
      {
        _head -> _prev = del -> _prev;
        del -> _prev -> _next = _head;
        delete del;
        del = nullptr;
      }
    }
    void PushFront(const T& data)
    {
      Node* first = _head -> _next;
      Node* newnode = new Node(data);
      
      // _head newnode first 
      _head -> _next = newnode;
      newnode -> _prev = _head;
      newnode -> _next = first;
      first -> _prev = newnode;
    }
    void PopFront()
    {
      Node* second = _head -> _next -> _next;
      _head -> _next = second;
      delete second -> _prev;
      second -> _prev = _head;
    }
    void Insert(iterator pos, const T& data)
    {
      Node* cur = pos._node;
      Node* pre = cur -> _prev;
      Node* newnode = new Node(data);
      
      // pre newnode cur 
      cur -> _prev = newnode;
      newnode -> _next = cur;
      newnode -> _prev = pre;
      pre -> _next = newnode;
      
    }
    void Erase(iterator pos)
    {
      Node* del = pos._node;
      Node* next = del -> _next;
      Node* pre = del -> _prev;

      pre -> _next = next;
      next -> _prev = pre;
      delete del;
      del = nullptr;
    }
    /**
     * List Access
     */ 
    T& Front()
    {
      return _head -> _next -> _data;
    }
    const T& Front() const 
    {
      return _head -> _next -> _data;
    }
    T& Back()
    {
      return _head -> _prev -> _data;
    }
    const T& Back() const 
    {
      return _head -> _prev -> _data;
    }
    /**
     * List capacity
     */ 
    //链表的长度
    size_t Size() const 
    {
      size_t count = 0;
      Node* cur = _head -> _next;
      while(cur != _head)
      {
        ++count;
        cur = cur -> _next;
      }
      return count;
    }
    // 判断链表是否为空
    bool Empty()
    {
      return _head -> _next == _head;
    }
    //调整链表的有效长度
    void Resize(int newsize, const T& data = T())
    {
      int oldsize = Size();
      if(newsize < oldsize)
      {
        for(int i = 0;i < (oldsize - newsize);i++)
        {
          PopBack();
        }
      }
      else 
      {
        for(int i = 0;i < (newsize - oldsize);i++)
        {
          PushBack(data);
        }
      }
    }
    //清空链表
    void Clear()
    {
      Node* cur = _head -> _next;
      while(cur != _head)
      {
        _head = cur -> _next;
        delete cur;
        cur = _head;
      }
      _head -> _next = _head;
      _head -> _prev = _head;
    }
    /**
     * 迭代器
     */ 
    iterator begin()
    {
      return iterator(_head -> _next);
    }
    iterator end()
    {
      return iterator(_head);
    }

5.源代码

注:自己实现的源代码位于我的github:https://github.com/hansionz/Cpp_Code/tree/master/List

猜你喜欢

转载自blog.csdn.net/hansionz/article/details/84435599