C++ STL -- list

文章详细介绍了C++中的list数据结构,包括list的功能、构造方法、迭代器的使用、容量、成员访问、增删查改操作,特别强调了迭代器的失效问题和sort方法。此外,还探讨了list的模拟实现,特别是节点和迭代器的封装,以及如何通过模板参数减少代码冗余。
摘要由CSDN通过智能技术生成

目录

前言

一. list概述

1.1 介绍

 1.2 list功能介绍

1.2.1 list的构造

1.2.2 list的迭代器

1.2.3 capacity

1.2.4 list member access

1.2.5 增删查改

1.2.6 list迭代器失效问题

1.2.7 sort

二、list的模拟实现

         2.1 list的节点

2.2 list迭代器

2.2.1 关于 ->  的重载

2.3 list常用接口

2.3 const迭代器

2.4 反向迭代器


前言

C语言学习中有一个数据结构叫做链表 , 当时引入这个数据结构是因为对于数组我们可以随机访问 , 但是如果进行插入的话就要对插入位置往后进行挪动,效率较低,所以我们创造了一个能够在任意位置插入时间复杂度都是O(1)的链表 , 而链表我们学过基础的单向链表 , 双向链表 , 循环链表等等, stl中的list容器就是一个双向链表,它的接口基本上跟vector相似度很高 , 只不过不支持随机访问罢了 , 在学习list过程中我们会认识到实现迭代器的新的封装方式 ,我们要了解其底层逻辑,特性以及实现方式。


一. list概述


1.1 介绍

相较于vector的线性空间,list显得复杂得多,它的好处是每次插入或者删除一个元素,就分配或者释放一个空间。因此,list对于空间的运用有着绝对的精准,一点也不浪费。而且,对于任何位置的元素安插或移除,永远是O(1)的时间复杂度

 1.2 list功能介绍

通过下面示范代码,我们发现其用法其实与vector很类似

1.2.1 list的构造

 // constructors used in the same order as described above:
  std::list<int> first;                                // int的空链
  std::list<int> second (4,100);                       // 4个int类型的100
  std::list<int> third (second.begin(),second.end());  // 通过second的迭代器进行构造
  std::list<int> fourth (third);                       // third的拷贝构造

  // 迭代器构造也能通过数组进行
  int myints[] = {16,2,77,29};
  std::list<int> fifth (myints, myints + sizeof(myints) / sizeof(int) );

  std::cout << "The contents of fifth are: ";
  for (std::list<int>::iterator it = fifth.begin(); it != fifth.end(); it++)
    std::cout << *it << ' ';

  std::cout << '\n';

1.2.2 list的迭代器

正向迭代器
iterator begin();
const_iterator begin() const;
iterator end();
const_iterator end() const;

反向迭代器
reverse_iterator rbegin();
const_reverse_iterator rbegin() const;
reverse_iterator rend();
const_reverse_iterator rend() const;

1.2.3 capacity

size_type size() const;//节点个数
bool empty() const;//是否非空

1.2.4 list member access

reference front();
const_reference front() const;
reference back();
const_reference back() const;

这里我们注意,front和back都是引用返回

 1.2.5 增删查改

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中最后一个元素

在pos处插入val
iterator insert (iterator position, const value_type& val);

pos处插入n个val
void insert (iterator position, size_type n, const value_type& val);

pos处插入迭代器first到last的内容
template <class InputIterator>    void insert (iterator position, InputIterator first, InputIterator last);

iterator erase (iterator position);删除list position位置的元素
iterator erase (iterator first, iterator last);	
删除从first到last的元素

void swap (list& x);	交换两个list中的元素
void clear();	清空list中的有效元素

 对于insert的使用可以参考如下代码

	std::list<int> mylist;
	std::list<int>::iterator it;

	// set some initial values:
	for (int i = 1; i <= 5; ++i) mylist.push_back(i); // 1 2 3 4 5

	it = mylist.begin();
	++it;       // it points now to number 2           ^

	mylist.insert(it, 10);                        // 1 10 2 3 4 5

	// "it" still points to number 2                      ^
	mylist.insert(it, 2, 20);                      // 1 10 20 20 2 3 4 5

	--it;       // it points now to the second 20            ^

	std::vector<int> myvector(2, 30);
	mylist.insert(it, myvector.begin(), myvector.end());
	// 1 10 20 30 30 20 2 3 4 5
	//               ^
	std::cout << "mylist contains:";
	for (it = mylist.begin(); it != mylist.end(); ++it)
		std::cout << ' ' << *it;
	std::cout << '\n';

 1.2.6 list迭代器失效问题

在vector和string的使用中我们遇到了迭代器失效问题, 同样的假如我们想要删除list的中的奇数,

我们用如下逻辑就会出问题

 it所指向的节点已经删除,这时再进行操作的话显然是非法的,正确的代码如下

 如果我们进行insert的话 ,那便不会失效, 因为对于list它的空间是非连续的 , 不像vector那样进行插入就要把一连串数据进行更改 ,不会对原迭代器指向内容进行更改,这里就不赘述了

1.2.7 sort

list中的sort是单独实现的 , 因为它的排序涉及到了节点间的连接关系 

void sort();

template <class Compare>  void sort (Compare comp);

 我们可以指定compare

注意:
list的sort底层其实是一个mergesort

二、list的模拟实现

list的实现是要比前面学的string和vector复杂一些的 , 运用到了很多新的思想,比如节点和迭代器的封装


2.1 list的节点

与我们C语言双向链表的实现相似,都是用结构体对于节点进行封装,不过C++中我们使用了模板来达到我们泛型编程的目的,同时我们结构体中多了一个构造函数

	template<class T>
	struct __list_node
	{
		__list_node(const T& x = T()):data(x),prev(nullptr),next(nullptr)
		{
		}
		__list_node<T>* prev;
		__list_node<T>* next;
		T data;
	};

2.2 list迭代器

对于string和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* _node;
		__list_iterator(Node* node) :_node(node)
		{

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

		//++it
		Self& operator++()
		{
			_node = _node->next;
			return *this;
		}
		//it++
		Self operator++(int)
		{
			Self newit(*this);
			++* this;
			return newit;
		}

		//--it
		Self& operator--()
		{
			_node = _node->prev;
			return *this;
		}
		//it--
		Self operator--(int)
		{
			__list_iterator<T> newit(*this);
			--* this;
			return newit;
		}


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

		bool operator!=(const __list_iterator<T, Ref, Ptr>& it)const
		{
			return _node != it._node;
		}
		bool operator==(const __list_iterator<T, Ref, Ptr>& it)const
		{
			return _node == it._node;
		}
	};

观察上述代码我们发现,模板参数里面除了数据类型T之外,又多了Ref(引用)Ptr(指针)两个参数,那么为什么要引入这两个参数呢?

对于这个迭代器的实现,我们不难看懂

其实我们分析后发现似乎只需要T一个参数就可以实现list的迭代器

但是我们知道,我们还要实现const_iterator那么我们只要将代码复制一份,把指针换成常量指针,引用换成常量引用就可以了,这样就造成了代码冗余,不符合我们泛型编程的思想

如果我们换成三个参数呢?下面的代码将会让你赞不绝口

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

通过这样的处理,我们只需要实现一个迭代器就能实现普通迭代器和常迭代器两种迭代器,大大减少了代码量

2.2.1 关于 ->  的重载

我们发现list迭代器里面重载了一个->,如果我们list中元素类型为int这种内置类型,显然这个重载是没有作用的,但是对于下述代码就不一样了

	struct Person
	{
	
		Person():_name("张三"),_age(20),_ID(20230628)
		{

		}
	
		char _name[20];
		int _age;
		int _ID;
	};

	void Test()
	{
		Person p1;
		list<Person> l1;
		l1.push_back(p1);
		l1.push_back(p1);
		l1.push_back(p1);
		l1.push_back(p1);
		l1.push_back(p1);
		list<Person>::iterator it = l1.begin();

		while (it != l1.end())
		{
			cout << it->_name << endl;
			++it;
		}
	
	}

但是如果我们追究起来的话,->返回的应该是data的指针,我们如果想访问data的内容应该再增加一个->才对,这其实就是编译器对于这个操作的 优化

2.3 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 iterator(_head->next);
		}
		const_iterator begin()const
		{
			return const_iterator(_head->next);
		}
		iterator end()
		{
			return iterator(_head);
		}
		const_iterator end()const
		{
			return const_iterator(_head);
		}

		list():_head(new Node())
		{
			_head->next = _head;
			_head->prev = _head;
		}

		list(const list<T>& l1):_head(new Node())
		{
			_head->next = _head;
			_head->prev = _head;

			for (auto x : l1)
				push_back(x);
		}

		//list<T>& operator=(const list<T>& l1)
		//{
		//	if (this != &l1) 
		//	{
		//		clear();
		//		for (auto x : l1)
		//			push_back(x);
		//		return *this;
		//	}
		//}
		//modern operator=
		list<T>& operator=(list<T> l1)
		{
			swap(_head, l1._head);
			return *this;
		}
		void push_back(const T& x)
		{
			/*Node* newnode = new Node(x);
			_head->prev->next = newnode;
			newnode->prev = _head->prev;
			_head->prev = newnode;
			newnode->next = _head;*/
			insert(end(), x);
		}

		void push_front(const T& x)
		{
		/*	Node* newnode = new Node(x);
			newnode->next = _head->next;
			_head->next->prev = newnode;
			_head->next = newnode;
			newnode->prev = _head;*/
			insert(begin(), x);
		}

		void pop_back()
		{
			if (!empty())
			{
				/*Node* tar = _head->prev;
				_head->prev->prev->next = _head;
				_head->prev = _head->prev->prev;    
				delete tar; 
				tar = nullptr;*/
				erase(--(end()));
			}
		}
		bool empty()const
		{
			return _head->next == _head;
		}

		void insert(iterator it , const T& x)
		{
			Node* cur = it._node;
			Node* newnode = new Node(x);
			newnode->prev = cur;
			newnode->next = cur->next;
			cur->next->prev = newnode;
			cur->next = newnode;
		}

		iterator& erase(iterator it)
		{
			assert(it != end());

			Node* cur = it._node;
			cur->prev->next = cur->next;
			Node* newcur = cur->next;
			newcur->prev = cur->prev;
			delete cur;

			it = iterator(newcur);

			return it;
		}
		void clear()
		{
			//Node* pos = _head->next;
			//while (pos != _head)
			//{
			//	pos = pos->next;
			//	delete pos->prev;
			//}
			//_head->prev = _head->next = nullptr;
			iterator it = begin();
			while (it != end())
				erase(it++);
		}

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

		}

我们发现其实只要实现了insert,对于头插尾插都可以进行复用

 2.3 const迭代器

其实前面讲为什么迭代器有三个模板参数时已经介绍过了

 2.4 反向迭代器

list反向迭代器的实现其实借助了适配器reverse_iterator  , 通过将正向迭代器作为接口传给适配器,而适配器里面实现了反向++,--的操作从而我们就得到了反向迭代器,这其实也是复用的思想

typedef reverse_iterator<const_iterator> 		const_reverse_iterator;
	
typedef reverse_iterator<iterator> 				reverse_iterator;

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EQUINOX1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值