【C++初阶】list使用及相关实现,思路顺滑流畅!

◆ 博主名称:Severus-Yuan
◆ 来自泥潭,充满野心,强大冷静,从不后悔
◆ 欢迎光临我的思考天地,期待我们在这刹那的同频共振


Severus-Yuan的专栏 链接🔗
C语言 C语言
初阶数据结构与算法 初阶数据结构与算法
数据结构算法题 数据结构算法题
C++ C++

文章目录


一.list的使用

(1)list

网站分享:https://legacy.cplusplus.com/reference/list/list/?kw=list

list就是双向带头循环列表。

图中有一句"allow constant time insert and erase operations anywhere",支持常数时间地任意位置的插入和删除,什么是常数时间?常数时间就是插入删除函数的时间复杂度为O(1),那为什么是常数时间呢?插入和删除函数不是会涉及数据的挪动吗?注意,这里的容器是list,底层是由前驱后继指针链接的不一定连续的节点空间,插入删除直接修改某节点的前驱后继指针就可以。

相比于string、vector,list不再支持\[\]下标访问。不过list想实现\[\]访问也是可行的,但成本更高了。string、vector底层是一块连续的存储空间,指针加减就能直接访问到,时间复杂度为O(1)。但是list底层是不一定连续的几块由前驱后继指针连起来的节点,如果要实现\[\]访问,就要遍历链表,时间复杂度为O(N)。

list的和string、vector大差不差的相关函数的使用,这里就不赘述了,讲讲list有但是string、vector没有的Operations类成员函数。

1.reverse

实现list元素的逆置,但其实库里面也有相关函数,都可以用。

2.remove/remove_if
3.unique
4.merge

通俗来说,就是将两个按同样的排序方式排好的list并到一个list里,并且还是按原先的排序方式,譬如两个升序排列的list,merge之后,就是一个升序的list。注意:merge并没有申请新空间,只是改变了原先list的指针的指向。

5.splice

理解成剪切,将x中的元素(单个迭代器就是剪切一个元素,迭代器区间就是剪切区间内的所有元素)剪切之后,再插入到指定的list的position位置之前。剪切之后,x中的元素就没有了。

6.sort

默认排为升序,想降序,需要通过仿函数(还没学到)实现。

算法库里也提供了sort,但是list并不能用算法库里的sort,库里的sort只支持随机迭代器,但是list的迭代器是双向迭代器。详情见后面的迭代器的分类里的讲解。

list的sort的效率:

list的sort的效率其实蛮低的,所以更建议先将list中的元素拷贝给vector,在vector里排好序之后,再拷贝回list里,这样效率反而更高。

(2)迭代器的分类:

根据功能(正向、反向、仅访问不能修改存储内容),可将迭代器分为四类;

根据容器底层的结构(存储结构是否连续、是否支持双向),可将迭代器分为三类------单向、双向和随机迭代器。这三类迭代器分别支持相应的++、--、+和-操作(在图中写出)。在这三类迭代器之外,还引申出InputIterator和OutputIterator,现可以理解为InputIterator、OutputIterator可以接收任意底层结构的迭代器。

由此可知:< algorithm >中包含的算法并不是对所有容器都适用,必须是能和所需迭代器类型对应上的底层结构的容器。这也就不难解释为啥list还要单独实现sort了。

二.list底层及相关实现

自我感觉学好string和vector之后,list的实现其实就比较简单了。除了节点、迭代器和链表的三个类模板有一点理解成本,其他都手拿把掐。

所以我们重点讲三个类模板,其他就直接贴代码了。

(1)三个类模板

cpp 复制代码
//list.h
#pragma once
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

namespace yuan
{
   //1.节点类模板
   template<class T>
   struct list_node
  {
	   T _data;
	   list_node<T>* _prev = nullptr;
	   list_node<T>* _next = nullptr;
	   //指向前后节点的指针类型肯定是节点指针啊
  };

   //2.迭代器类模板
   template<class T>
   //类中的成员会在类外被频繁访问,与其写class再加public,不如直接写struct
   struct list_iterator
  {
	   typedef list_node<T> Node;
	   //public修饰的类型,可以在类外被访问,不过记得格式 类型::Node
	   typedef list_iterator<T> Self;
	   
       Node* _node;
  };
  
  //3.链表类模板
   template<class T>
   class list
   {
	  typedef list_node<T> Node;
	  //list_node是类名,list_node<T>是类类型名
 
  public:
      //类型也受修饰符限定
      typedef list_iterator<T> iterator;
      
  private:
		Node* _head;
		//头结点指针
		size_t _size;
	};
}

节点类模板好理解,里面装了存储的数据元素、前驱指针和后继指针,使用类模板就不用定死存储数据的类型,更灵活;链表类模板也好理解,这就是我们模拟实现的list容器的结构,里面装头结点指针就能访问到整个链表,加上_size就能高效获得链表的有效节点的个数。那为什么list容器的迭代器都要封装成一个类模板呢?

回想一下string和vector的迭代器,是不是按以下代码形式进行的访问?

cpp 复制代码
//string
  string s1;
  s1.push_back("111");
  string::iterator sit = s1.begin();
  while(sit != s1.end())
  {
     cout<<*sit<<" ";
     sit++;
  }

//vector
  vector<int> v1;
  v1.push_back(1);
  vector<int>::iterator vit = v1.begin();
  while(vit != v1.end())
  {
    cout<<*vit<<" ";
    vit++;
  }

在实现string和vector的begin()和end()时,都是返回的数组的原生指针,所以解引用得到数组元素,自增来到下一位元素。

但在list中,实现begin()和end(),我们可以直接返回节点的指针吗?显然是不行的。因为节点指针解引用得不到节点里存储的_data数据,只能得到节点本身;节点进行自增也不会到下一个节点,因为list的底层结构是不一定连续的内存空间,自增到下一个元素的前提是内存空间必须连续。

所以,我们单独声明一个迭代器类模板,里面装着节点指针,里面也重载了解引用、自增和不等于等一系列函数。

注意:list迭代器底层就是节点指针,但是由于链表不能++来到下一个节点,也不能解引用节点指针就得到_data,所以我们单独封装了一个迭代器类。在类里面我们声明了节点指针成员变量(迭代器可以是链表中的任一节点的指针,但是原先的节点指针和指针间的关系并不满足迭代器的需求,所以需要单独封装成迭代器类),并且里面重载了自增和解引用函数。

(2)迭代器类模板的相关实现:

cpp 复制代码
//2.迭代器类模板
   template<class T>
   //类中的成员会在类外被频繁访问,与其写class再加public,不如直接写struct
   struct list_iterator
  {
	   typedef list_node<T> Node;
	   //public修饰的类型,可以在类外被访问,不过记得格式 类型::Node
	   typedef list_iterator<T> Self;
	   
       Node* _node;
       
      //构造函数函数名和类同名,而不是和类型名同名
       list_iterator(Node* n)
	  //_node不是const,所以n也不能是const
	     :_node(n)
      {}
      
       //解引用运算符重载
       T& operator*()
      {
	     return _node->_data;
      }

       //迭代器++的结果必然也是迭代器
       //list_iterator<T> operator++()
        Self& operator++()
       //前置++
      {
	      _node = _node->_next;
	      return *this;
      }

        Self operator++(int)
	   //后置++
      {
	      Self tmp = *this;
	     //Self tmp(*this);
	     _node = _node->_next;
	     return tmp;
      }
     //这里并不需要显式定义的深拷贝的拷贝构造和赋值重载
     //因为在Iterator类中需要的只是指针指向和节点相同的空间,不是要申请额外的空间
     //同时,也就不需要显式的析构了

     //前置--
     Self& operator--()
    {
	    _node = _node->_prev;
	    return *this;
    }
    
     //后置--
     Self operator--(int)
    {
	    Self tmp = *this;
	    _node = _node->_prev;
	    return tmp;
    }

    bool operator!=(const Self& x) const
   {
	   //判断迭代器是否相等就是判断是否指向同一个节点
	   //那就判断迭代器指向节点的指针是否相同就可以了
	   return _node != x._node;
   }

     bool operator==(const Self& x) const
   {
	   return _node == x._node;
   }

    //->运算符重载
     T* operator->()
   {
	   return &(_node->_data);
   }
 };

针对最后一个->运算符重载,我们来仔细讲讲:

cpp 复制代码
#include"list.h"
namespace yuan
{
  struct AA
 {
	int _val;
 };

   void mylist_test02()
 {
	list<AA> lta;
	lta.push_back(AA());
	lta.push_back(AA());
	lta.push_back(AA());
	lta.push_back(AA());
	list<AA>::iterator it = lta.begin();
	while (it != lta.end())
	{
		cout << it->_val << " ";
		//cout<<it.operator->()->_val
		//it.operator->()返回的是struct AA*类型的指针变量
		//struct AA*类型的指针变量再->就能访问到_val
		//两个->省略为1个了
		it++;
	}
 }
}

int main()
{
   yuan::mylist_test02();
   return 0;
}

(3)list链表类模板的相关实现:

cpp 复制代码
template<class T>
class list
{
	typedef list_node<T> Node;

public:

	typedef list_iterator<T> iterator;
	//public修饰,类外可访问,但访问也是要按照一定格式的

	iterator begin()
	{
	    //1.有名对象写法:
		//iterator it(_head->_next);
		////函数内的局部对象返回不能用引用接收
		//return it;

		//2.匿名对象写法:
		//return iterator(_head->_next);

		//3.隐式类型转换写法:
		//在Iterator类中有符合类型的单参数构造函数
		return _head->_next;
	}

	iterator end()
	{
		//iterator it(_head);
		//return it;

		//隐式类型转换
		return _head;
		//尾结点的后一个节点,不是_head->_prev
	}

	//构造
	list()
	{
		_head = new Node(T());
		_head->_prev = _head;
		_head->_next = _head;
	}

    //插入
	iterator insert(const iterator pos, const T& x)
	{
		Node* newnode = new Node(x);
		Node* p = pos._node;
		newnode->_prev = p->_prev;
		newnode->_next = p;
		newnode->_prev->_next = newnode;
		p->_prev = newnode;
		_size++;

		return newnode;
	}

	//写了insert之后,头插尾插就可以直接复用了
	void push_back(const T& x)
	{
		////传匿名对象,因为T的类型不定
		//Node* newnode = new Node(x);
		////newnode->_data = x;
		////Node* tail = _head->_prev;
		//_head->_prev->_next = newnode;
		//newnode->_prev = _head->_prev;
		//_head->_prev = newnode;
		//newnode->_next = _head;
		//_size++;

		//insert(_head, x);
		insert(end(), x);
	}

	void push_front(const T& x)
	{
		//insert(_head->_next,x);
		insert(begin(), x);
	}

	size_t size() const
  //加了const,非const和const迭代器都能使用
	{
		return _size;
	}

	bool empty()
	{
		return _size == 0;
	}

    //删除
	iterator erase(const iterator& pos)
	{
		assert(pos != _head);
		//头结点不能删除
		Node* del = pos._node;
		Node* next = del->_next;
		del->_prev->_next = del->_next;
		del->_next->_prev = del->_prev;
		delete del;
		_size--;
		return next;
	}

	//头尾删调用erase里会处理size 
	iterator pop_back()
	{
		//return erase(_head->_prev);
		return erase(end()--);
	}

	iterator pop_front()
	{
		return erase(begin());
	}

private:
	Node* _head;
	//头结点指针
	size_t _size;
};

(4)const迭代器类模板

const迭代器(const_iterator)的作用是不能改变容器存储的值(想一想,我们在vector中写的是typedef const T* const_iterator,const T*就是不能改变指针指向内容),那我们将怎样实现const迭代器类模板呢?其实能通过迭代器改变容器存储的值的途径无非只有解引用和->两种,所以我们只要让这两种函数返回const的存储元素就可以。不过也不要忘了在list的类模板中加上相应的begin()和end()函数。

1.初步实现:
cpp 复制代码
//常迭代器类模板:
template<class T>
struct list_const_iterator
{
	typedef list_node<T> Node;
	typedef list_const_iterator<T> Self;
	Node* _node;
	
	list_const_iterator(Node* n)
	//_node不是const,所以n也不能是const
	//指针访问要考虑权限问题
		:_node(n)
	{}

	const T& operator*()
	{
		return _node->_data;
	}

  	const T* operator->()
	{
		return &(_node->_data);
	}

	//其余函数照旧......
};
cpp 复制代码
//list链表类模板
template<class T>
class list
{
	    typedef list_node<T> Node;

public:

		typedef list_iterator<T> iterator;
		typedef list_const_iterator<T> const_iterator;

		iterator begin()
		{
			return _head->_next;
		}

		iterator end()
		{
			return _head;
		}

		const_iterator begin() const
		{
			return _head->_next;
		}

		const_iterator end() const
		{
			return _head;
		}

       //其余函数照旧......

private:
	Node* _head;
	size_t _size;
};

单独写一个list_const_iterator类模板固然也算一种实现办法,但是list_iterato类模板r和list_const_iterator类模板重复的部分也太多了吧?写着也太冗余了。

能不能只实现一个类模板,却能完成两种迭代器的功能呢?可以的。

2.最终实现
cpp 复制代码
//链表类模板:
template<class T>
class list
{
	typedef list_node<T> Node;

public:

	typedef list_iterator<T, T&, T*> iterator;
	typedef list_iterator<T, const T&, const T*> const_iterator;

	iterator begin()
	{
		return _head->_next;
	}

	iterator end()
	{
		return _head;
	}

	const_iterator begin() const
	{
		return _head->_next;
	}

	const_iterator end() const
	{
		return _head;
	}

  //......

private:
	Node* _head;
	size_t _size;
};
cpp 复制代码
//迭代器模板:
//同时写两个重复率很高的迭代器类没有必要,我们直接写一个迭代器类模板
template<class T, class Ref, class Ptr>
struct list_iterator
{
	typedef list_node<T> Node;
	//Ref, Ptr分别对应list类模板中的T&/const T&,T*/const T*
	typedef list_iterator<T, Ref, Ptr> Self;
	Node* _node;

	list_iterator(Node* n)
		//_node不是const,所以n也不能是const
		:_node(n)
	{}

  //T&做返回值,现在写成了
	Ref operator*()
	{
		return _node->_data;
	}

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

  //......
};

你可能会疑问:

为啥在list_iterator类模板里写

cpp 复制代码
typedef list_iterator<T, Ref, Ptr> Self;

而在list类模板里写

cpp 复制代码
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;

就这么看似随意一写,就能在用到迭代器的时候,精准匹配到对应的迭代器类吗?

是的,但不随意。

首先我们要明白list容器的主要结构是list类模板,成员函数都是放在list类模板里的,我们在测试文件里定义list容器也是定义list类,访问list迭代器也是在list类模板里访问(定义迭代器都是写成list< int >::iterator it)。所以list类模板知道T是什么数据类型的时间绝对比节点类模板和迭代器类模板知道的早,我们将

cpp 复制代码
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;

写在list类模板里,这样实例化list类的同时,也就同时实例化了Iterator和const_iterator,那么list类模板中的begin和end也就能根据list类型返回正确类型的迭代器,list_iterator类模板中的typedef list_iterator<T, Ref, Ptr> Self;也就能确定下来,解引用和->运算符重载也能确定返回值类型,一切都圆满。

(5)迭代器失效

list中,只有erase会造成迭代器失效,插入不会。

新插入的节点只是完成了前驱后继指针的连接,又没有影响到原先迭代器指向的节点的位置,所以不会造成迭代器失效;但是erase之后,原先迭代器指向的节点被释放了,会变成野指针,就会造成迭代器失效。所以同样的,erase函数的返回值换成删除节点的下一个节点的指针就好。

(6)list类的显式拷贝构造、赋值重载和析构(深拷贝)

前面有提到说迭代器类不需要显式的深拷贝拷贝构造和赋值重载函数,也不需要显式的析构函数,默认的就已经够用(浅拷贝)。这是因为迭代器只需要指向原链表就好,不需要先申请空间,创建一个和原先链表数据一模一样的新链表,再指向。

但是在list类中,就需要显式定义式拷贝构造、赋值重载和析构函数了。因为list的拷贝和赋值是初始化新链表的过程,不能说新链表的初始化就直接让头指针的后继指针指向原链表就完事了吧?肯定要申请新空间。

1.拷贝构造

①相当原始粗暴的实现:不推荐,麻烦
cpp 复制代码
//拷贝构造
list(const list<T>& x)
{
	const_iterator it = x.begin();
	_head = new Node(T());
	_head->_prev = _head;
	_head->_next = _head;
	Node* prev = nullptr;

	while (it != x.end())
	{
		Node* newnode = new Node(*it);

		if (_head->_next == _head)
		{
			newnode->_next = _head;
			newnode->_prev = _head;
			_head->_next = newnode;
			_head->_prev = newnode;
		}
		else
		{
			newnode->_next = _head;
			newnode->_prev = prev;
			prev->_next = newnode;
		}
		
		prev = newnode;
		it++;
	}
}
②现代写法:
cpp 复制代码
//空构造:
void emptylist()
{
	_head = new Node(T());
	_head->_prev = _head;
	_head->_next = _head;
}

list()
{
	emptylist();
}

//拷贝构造
list(const list<T>& x)
{
    //没有头结点的链表不能插入
	emptylist();
	for (auto& lt : x)
	{
		push_back(lt);
	}
}

2.赋值重载

cpp 复制代码
//赋值重载

//1.
list<T>& operator=(const list<T>& x)
{
	list<T> tmp(x);
	swap(tmp._head, _head);
	swap(tmp._size, _size);
	return *this;
}

//2.
list<T>& operator=(list<T> lt)
{
	//要改变类的值,就不能传const参数了
	swap(lt._head, _head);
	swap(lt._size, _size);
	return *this;
}

//3.
list<T>& operator=(list<T> lt)
{
	//要改变类的值,就不能传const参数了
	swap(lt);
	return *this;
}

void swap(list<T>& lt)
{
	//指定库里的swap函数,不然会报参数不匹配
	std::swap(lt._head, _head);
	std::swap(lt._size, _size);
}

3.析构

cpp 复制代码
//析构
~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

void clear()
{
	//clear不处理头节点
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}
PS:按需实例化:
cpp 复制代码
//类外
template<class T>
void print(const T& container)
{
	typename T::const_iterator it = container.begin();
	//出现没有实例化的类模板的类型前记得加上typename
	//但凡写成 list<int>::const_iterator it = container.begin(); 都不用加typename了
	//auto it = container.begin(); auto可以不加typename
	while (it != container.begin())
	{
		cout << *it << " ";
		it++;
		(*it) += 10; 
		//const T& container下不允许改变容器的值
		//所以理论上来说,(*it) += 10; 会报错
		//但是在不使用print函数时,该行不会编译报错->按需实例化
		//按需实例化即当没有定义模板时,不会非常细致地检查内部,只能查出明显的标点符号类语法错误
	}
	cout << endl;

	for (auto e : container)
	{
		cout << e << " ";
	}
	cout << endl;
}

------end------

相关推荐
敲上瘾2 小时前
Qt Widgets入门:项目模板解析与Hello World
开发语言·数据库·c++·qt·ui·qt6.3
二本咕咕-机械转码2 小时前
FreeRTOS深入解析(八)IPC机制之FromISR详解
数据结构·单片机·链表
旖旎夜光3 小时前
【C++ 基础入门】C++ 多态(中):虚函数表、多态原理与动静绑定
开发语言·c++·笔记·学习
1-1=04 小时前
C# 数据结构和特殊类型
数据结构·c#·.net·.netcore
希和20274 小时前
C语言程序设计题|有序数组插入数字冒泡排序
c语言·数据结构·算法
旖旎夜光4 小时前
【C++ 基础入门】C++多态(下):单/多继承虚表、多态补充与 21 道高频题
开发语言·c++·笔记·学习
apcipot_rain15 小时前
C++转Java日记(03)
java·开发语言·c++
布莱克60515 小时前
C++ 中 map 的 insert 与 emplace:key 已存在时会覆盖吗?
开发语言·数据结构·c++·算法·map
xys_67815 小时前
深挖C++:类和对象(中)
开发语言·c++