【C++】list类

一、list的介绍及使用:

1.list的介绍:

对于list类来说,其中的大多数函数功能都与string、vector相同,大部分实用的成员函数也是非常相似,因此我们在这里也是简单明了的查看文档:list文档 。通过文档我们可以更加直观的了解成员函数的功能。

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

 

通过这个结构我们已经知道,list的迭代器实现起来将会困难一些,原因是其节点的地址是不连续的,其次也会存在对节点的类型。

2. list的使用:

对于使用这里,与vector调用方式是一样的,无论是push_back,还是find等,查上面的文档就好了。而对于list的重要的部分,是模拟实现中的问题。

3.模拟list的节点结构:

对于一个双向带头循环链表的节点的结构,为了存入prev、next指针以及数据,我们需要一个类来封装这几个变量,这个类在C语言中也可以称为结构体,但实际上又有所区别,在这个节点类中,虽然没有成员函数,但是却有着公有和私有的区别,因此为了便于实现,我们采用struct公有的类封装;此外类也需要实现构造函数。由于我们不知道存入什么变量,因此会用到模板。

template<class T>
	struct list_node
	{
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;

		list_node(const T& x = T())  //构造函数
			:_data(x)
			,_next(nullptr)
			,_prev(nullptr)
		{}
	};

4.list类的封装:

对于list类来讲,私有的成员变量是指向头结点的指针。

template<class T>
class list
{
    typedef list_node<T> node; //将节点重命名一下
public:
    //成员函数:
    //……
private:
    node* _head;//指向头结点的指针变量
}

补充:list自带的排序函数:

sort:

之前的vector类,可以用到算法库的排序sort,但当查看list的文档,发现其自带一个排序函数:

由于list是链表结构,而算法库中的排序的底层是快速排序,不能实现链表的排序,因此设计了一个list自带的排序,通过前面的学习,list排序有纯粹的暴力插入排序,也有更好的归并排序,而这个list的sort的底层就是归并排序。

然而,对于实际来说,通过将list的值赋值给vector之后调用算法库sort的时间还是要比只接归并排序快的,因此在这里排序还是推荐拷贝到vector后调用算法库的排序。
 

list自带的去重函数:

unique:

sort实现了之后,我们也来了解一下另一个特有的成员函数:unique:去重函数,由于这has必须建立在有序的基础上才能使用,因此我们也可以想象得到底层是如何实现的,毕竟刚学C语言的时候经常会自己写出那种的代码,那么接下来就看一下怎么使用吧:

可见,调用unique的前提是数据必须有序。

二、list的迭代器:

1、迭代器失效的问题:

在上一节的vector中,我们讲述了迭代器失效的问题,vector的insert和erase都会导致迭代器失效,insert是 。因为挪动空间导致,erase是因为删掉之后不存在这个元素导致。而对于list来讲,list的insert是不会失效的,因为list的insert并没有移动空间,而是直接插入节点,而erase由于删除的原因也会造成list中的迭代器失效。

因此这里只有erase会造成list迭代器失效。而解决这一问题就可以根据返回值来改变失效后的迭代器。

这样,删除之后也不会造成失效了。

2、迭代器的分类:

1、单向迭代器:只能++,不能–。例如单链表,哈希表;

2、双向迭代器:既能++也能–。例如双向链表;

3、随机访问迭代器:能+±-,也能+和-。例如vector和string。

迭代器是内嵌类型(内部类或定义在类里)

3、迭代器的模拟实现:

对于list结构,已经提到过是双向带头循环链表,而对于迭代器的begin和end又是左闭右开区间,因此模拟实现时begin在_head->next的位置,end在_head的位置,因为最后一个节点的下一个就是_head。

对于list的迭代器,与vector有很大的不同,因为每一个节点都是通过指针的方式链接的,因此迭代器的++和–并不是单一的地址++与–,而是以解引用的方式进行,也就是说需要多一个运算符重载,那么既然又多了一个需求,对于迭代器我们也就有必要也封装成类供给list类使用。

迭代器的封装: 

template<class T, class Ref, class Ptr>
struct __list_iterator
{
    typedef list_node<T> node;
    typedef __list_iterator<T, Ref, Ptr> Self;
    node* _pnode;
    
    __list_iterator(node* p)//构造
        :_pnode(p)
    {}
}

这里我们采用了三个模板,为的就是处理下面的const。

3.1、普通迭代器:

实现普通迭代器,我们用不到上面的三个模板参数,只需要一个T就够了,因此在这里为了更好的理解,我们不看上面迭代器的类,在这里自己造一个。

	template<class T>
	struct list_iterator
	{
		typedef list_node<T> node;
		typedef list_iterator<T> self;
		node* _node;

		list_iterator(node* n) //拷贝构造
			:_node(n)
		{}



		self& operator++() //前置++
		{
			_node = _node->_next;
			return *this;
		}

		self& operator--() //前置--
		{
			_node = _node->_prev;
			return *this;
		}

		self operator++(int) //后置++
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

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

		bool operator==(const self& s)
		{
			return _node == s._node;
		}

		bool operator!=(const self& s)
		{
			return _node != s._node;
		}
	};

因此我们可以看到普通的迭代器除了封装之外没有什么难点。

3.2、const迭代器:

那么对于const迭代器来说,vector是在解引用的时候直接加上const就可以了,但list明显不能像vector那样直截了当,这也是这一节最难以实现的部分。

其与vector对比,对于const的list迭代器来说,因为本身是以类的方式进行,而const实际上就代表迭代器指向的内容不可改变,也就是说只需要改变普通迭代器的解引用运算符重载就可以了,因此我们实现const有两种思路可行,一是再写一个类,只将普通迭代器运算符重载的函数换成const类型,也就是这样:多加了一个const类型的迭代器的类。
 

	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(n)
		{}

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


		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		bool operator==(const self& s)
		{
			return _node == s._node;
		}

		bool operator!=(const self& s)
		{
			return _node != s._node;
		}
	};

但我们发现这种方式会产生很多的代码冗余,因为除了解引用运算符重载,别的都没有变化,因此大佬在设计这里的时候用到了多个模板参数,通过传入的类型不同,就将这个迭代器的类转化成const的和非const的:

	template<class T,class Ref>
	struct list_iterator
	{
		typedef list_node<T> node;
		typedef list_iterator<T, Ref> self;
		node* _node;

		list_iterator(node* n)  //拷贝构造
			:_node(n)
		{}

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


		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		bool operator==(const self& s)
		{
			return _node == s._node;
		}

		bool operator!=(const self& s)
		{
			return _node != s._node;
		}
	};

即这样的一个迭代器类通过在list类中传入对应的类型就可以实现const和非const。

总结一下实现const的迭代器的两种方法:

  1. 重新写一个类,不过里面只有一个函数是不一样的,会造成代码冗余
  2. 利用模板参数!将一个类通过传入的类型不同能够自动演化出不同的类。

如果对于list<T>,这个T是一个类,并且有两个成员变量,翻入list中是如何迭代的呢? 

struct Pos
{
    int _row;
    int _col;

    Pos(int row=0, int col=0)
        :_row(row)
        ,_col(col)
        {}
};

我们可以在通过迭代器进行这样的访问:

list<Pos> lt(1,2);
list<Pos>::iterator it = lt.begin();
while(it != lt.end())
{
    cout <<(*it)._row<<":"<<(*it)._col<<endl;
    ++it;
}

但事实上,我们在C/C++中有另一种方式:->即直接it->_row;下面的Ptr就是。

因此我们也需要考虑如何将这样的运算符也重载进去,只需要在类中再加一个模板参数,到现在三个模板参数,已经齐了。这也是我们最终的迭代器的类:

	template<class T,class Ref,class Ptr>
	struct list_iterator
	{
		typedef list_node<T> node;
		typedef list_iterator<T, Ref, Ptr> self;
		node* _node;

		list_iterator(node* n)  //拷贝构造
			:_node(n)
		{}

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

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

		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		bool operator==(const self& s)
		{
			return _node == s._node;
		}

		bool operator!=(const self& s)
		{
			return _node != s._node;
		}
	};

但意的是,->返回的值仍然是一个指针,那我们调用时确是一个函数:那返回时不应该是it->->_row吗?

这是因为编译器做了一定程度的优化,将两个->变成了一个,因此,我们直接写成两个->也是对的。

三、模拟实现list完整代码:

list.h:

namespace my_list
{
	template<class T>
	struct list_node
	{
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;
			
		list_node(const T& x = T())  //构造函数
			:_data(x)
			,_next(nullptr)
			,_prev(nullptr)
		{}
	};

	template<class T,class Ref,class Ptr>
	struct list_iterator
	{
		typedef list_node<T> node;
		typedef list_iterator<T, Ref, Ptr> self;
		node* _node;

		list_iterator(node* n)  //拷贝构造
			:_node(n)
		{}

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

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

		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		bool operator==(const self& s)
		{
			return _node == s._node;
		}

		bool operator!=(const self& s)
		{
			return _node != s._node;
		}
	};


	template<class T>
	class list
	{
	public:
		typedef list_node<T> node;
		typedef list_iterator<T, T&, T*> iterator;
		typedef list_iterator<T, const T&, const T*> const_iterator;

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

		iterator end()
		{
			return iterator(_head);
		}

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

		const_iterator end() const
		{
			return const_iterator(_head);
		}

		void empty_init()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}

		list()
		{
			empty_init();
		}

		template<class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			empty_init();
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		void swap(list<T>& tmp)
		{
			std::swap(_head, tmp._head);
		}

		list(const list<T>& It)
		{
			empty_init();
			lsit<T> tmp(begin(), end());
			swap(tmp);
		}

		list<T>& operator=(list<T> It)
		{
			swap(It);
			return *this;
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				erase(it++);
			}
		}

		void push_back(const T& x)
		{
			/*node* tail = _head->_prev;
			node* new_node = new node(x);

			tail->_next = new_node;
			new_node->_prev = tail;
			_head->_prev = new_node;
			new_node->_next = _head;*/

			insert(end(), x);
		}

		void pop_back()
		{
			erase(--end());
		}

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

		void pop_front()
		{
			erase(begin();
		}

		void insert(iterator pos, const T& x)
		{
			node* cur = pos._node;
			node* prev = cur->_prev;

			node* new_node = new node(x);

			prev->_next = new_node;
			new_node->_prev = prev;
			new_node->_next = cur;
			cur->_prev = new_node;
		}

		iterator erase(iterator pos)
		{
			node* prev = pos._node->_prev;
			node* next = pos._node->_next;

			prev->_next = next;
			next->_prev = prev;
			delete pos._node;

			return iterator(next);
		}

	private:
		node* _head;
	};

	void test_list1()
	{
		list<int> l1;
		l1.push_back(1);
		l1.push_back(2);
		l1.push_back(3);
		l1.push_back(4);
		auto pos = l1.begin();
		++pos;
		l1.insert(pos, 20);

		list<int>::iterator It = l1.begin();
		while (It != l1.end())
		{
			cout << *It << " ";
			++It;
		}
		cout << endl;
	}
}

四、vector与list的优缺点:

1、vector的优点及缺点:

优点:

  • 支持下标随机访问。
  • 尾删尾插效率高(但不明显)。
  • CPU高速缓存命中率高:因为物理空间连续。

缺点:

  • 前面部分插入删除效率低。
  • 扩容有消耗,还存在一定的空间浪费,扩容开多了浪费,开少了浪费空间。

2、list的优点及缺点:

优点:

  • 按需申请释放,无需扩容。
  • 任意位置插入删除是O(1),前提是已经知道这个位置了,查找是O(n)。

缺点:

  • 不支持下标的随机访问
  • CPU高速缓存命中率低

两个容器各自都有属于自己的特性,缺一不可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++ 中的 `list` 是一个双向链表容器,它可以动态地存储数据,并且可以在任意位置进行快速插入和删除操作,具有较好的性能表现。`list` 容器的头文件是 `#include <list>`。使用 `list` 需要注意以下几点: 1. `list` 是一个模板,需要指定存储元素的型,例如 `list<int>` 表示存储整型数据的链表。 2. `list` 中的元素是通过指针进行连接的,因此不能使用下标运算符 `[]` 来访问元素,可以使用迭代器来访问元素。 3. `list` 支持以下操作: - `push_back` / `push_front`:在链表尾部或头部插入元素; - `pop_back` / `pop_front`:删除链表尾部或头部的元素; - `insert`:在指定位置插入元素; - `erase`:删除指定位置的元素; - `size`:返回链表长度; - `clear`:清空链表中的所有元素。 一个简单的示例代码如下: ```c++ #include <iostream> #include <list> using namespace std; int main() { list<int> mylist; for (int i = 0; i < 5; i++) { mylist.push_back(i); } for (auto it = mylist.begin(); it != mylist.end(); it++) { cout << *it << " "; } cout << endl; mylist.pop_front(); mylist.insert(mylist.begin(), -1); mylist.erase(++mylist.begin()); for (auto x : mylist) { cout << x << " "; } cout << endl; return 0; } ``` 输出: ``` 0 1 2 3 4 -1 1 2 3 4 ``` 在这个示例中,我们首先创建了一个空的 `list` 对象 `mylist`,然后使用 `push_back` 在尾部插入了五个整数元素;接着使用迭代器遍历并输出链表中的元素;然后使用 `pop_front` 删除了链表头部的元素 `0`,使用 `insert` 在链表头部插入了一个元素 `-1`,使用 `erase` 删除了链表中的第二个元素 `1`;最后再次遍历并输出链表中的元素。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值