C++详解之list

目录

Ⅰ、list的介绍

Ⅱ、list和vector的区别

Ⅲ、list的构造函数

Ⅲ、list的迭代器

1、库中迭代器的接口

2、如何去实现list的迭代器

Ⅳ、list的其他接口

1、insert

2、erase

3、push_front , pop_front 和 push_back , pop_back

 4、front,back和size,empty


Ⅰ、list的介绍

list是序列容器,list的数据结构是一个双向带头链表,可以支持在任意节点O(1)的插入节点,list的成员变量是指向头节点的一个ListNode*类型的指针,这里大家可能有疑问为什么list的成员变量只要一个指向头节点的指针呢?这是以为list对于ListNode和ListIterator进行了一层封装,为什么要这样呢?我们要先从list与vector的区别开始说起。

Ⅱ、list和vector的区别

list我们在上面说了是一个双向带头链表,vector是一个顺序表,对于顺序表来说它的本质就是一个数组这也就说明了vector有一个天生的优势就是它的存储空间天生连续的可以通过对指针的++/--来实现遍历,但对于list来说就不一样,我们知道链表的存储是不连续的是通过listnode里的prev和next指针来链接的。这也就带来了一个问题,它无法直接通过指针的++/--去访问下一个和进行遍历但为了去实现iterator怎么办呢?,我们换一句话来说list不能用原生指针来实现迭代器。这时候我们就想到了一个大杀器就是运算符重载,我们可以进行一层封装如何去实现iterator的++/--从而可以实现范围for的。这也就解释了为什么list的成员变量只有一个指针因为prev和next我们封装在了listnode这个类里面了然后我们再去把list_iterator去封装一下,我们就可以去实现迭代器。

Ⅲ、list的构造函数

在说list的构造函数之前我们先看一下list的成员类型

下面这幅图是list的构造函数种类

list的构造函数支持无参构造(记得不传参的时候不要加(),因为加了()无法分辨是函数声明还是想调用无参构造),可以传n,val意思是构造一个有n个节点值为val的list,还可以支持迭代器构造和用initializer_list去构造。

Ⅲ、list的迭代器

在上面的第二点我们也去解释了list的迭代器需要去封装的原因,在这里我们先解释list在库中提供的接口在去解释一下我们是如何实现list的迭代器的。

1、库中迭代器的接口

库中支持正向,反向,const正向和const反向迭代器这几个的用法和作用和vector中的基本相同,这里的end依旧是指向最后一个的下一个的位置,线面我们来演示一下begin(),end()和rbegin(),rend()。cbegin(),cend()和 crbegin(),crend()的区别就是不能去对迭代器进行修改。

2、如何去实现list的迭代器

我们现在知道要进行一层封装并且要去重载operator++,operator--,operator!= 等等,还有一点我们要去把迭代器的名字统一改成iterator和前面的容器的迭代器保持一致,这里我们可以采用using和typedef的方式去实现。

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->_val;
	}
	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& it)
	{
		return _node != it._node;
	}
	bool operator==(const self& it)
	{
		return _node == it._node;
	}
	Ptr operator->()
	{
		return &(_node->_val);
	}
};

大家可能会好奇为什么会有三个模板参数呢?因为我们要去实现const类型的迭代器我们如果再写一份const类型的与不是const版本的迭代器内容大部分都是一样的这就造成了代码的冗余,那我们既要实现普通版本又要实现const版本的还想让代码看起来不冗余,我们采用的方式就是增加模板参数去控制。为什么可以这样呢?我们再来看一下在list中我们是如何去区分const_iterator和iterator。

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;
	void empty_list()
	{
		_head = new Node;
		_head->_val = T();
		_head->_next = _head;
		_head->_prev = _head;
	}

	list()
	{
		empty_list();
	}
	list(const list<T>& ls)
	{
		empty_list();
		for (auto& e : ls)
		{
			push_back(e);
		}
	}
	list& operator=(list<T> ls)
	{
		swap(ls);
		return *this;
	}
	~list()
	{
		iterator it = begin();
		while (it != end())
		{
			it = erase(it);
		}
		delete _head;
		_size = 0;
	}
	iterator erase(iterator pos)
	{
		Node* next = pos._node->_next;
		Node* prev = pos._node->_prev;
		prev->_next = next;
		next->_prev = prev;
		delete pos._node;
		return next;
	}
	void swap(list<T>& ls)
	{
		std::swap(_head, ls._head);
		std::swap(_size, ls._size);
	}
	iterator begin()
	{
		return _head->_next;
	}
	iterator end()
	{
		return _head;
	}
	const_iterator begin()const
	{
		return _head->_next;
	}
	const_iterator end()const
	{
		return _head;
	}
	T& front()
	{
		return _head->_next->_val;
	}
	T& back()
	{
		return _head->_prev->_val;
	}
	bool empty()
	{
		return (_head->next == _head);
	}
	size_t size()
	{
		return _size;
	}
	void push_front(const T& val)
	{
		Node* newnode = new Node;
		newnode->_val = val;
		Node* first = _head->_next;
		newnode->_next = first;
		newnode->_prev = _head;
		first->_prev = newnode;
		_head->_next = newnode;
		_size++;
	}
	void push_back(const T& val)
	{
		Node* newnode= new Node;
		newnode->_val = val;
		Node* last = _head->_prev;
		last->_next = newnode;
		_head->_prev = newnode;
		newnode->_next = _head;
		newnode->_prev = last;
		_size++;
	}
	iterator insert(iterator pos, const T& val)
	{
		Node* newnode = new Node(val);
		Node* first = pos._node->_prev;
		pos._node->_prev = newnode;
		newnode->_next = pos._node;
		first->_next = newnode;
		newnode->_prev = first;
		_size++;
		return first;
	}
	void pop_back()
	{
		Node* last = _head->_prev;
		Node* newlast = _head->_prev->_prev;
		_head->_prev->~list_node();
		newlast->_next = _head;
		_head->_prev = newlast;
		_size--;
		delete last;
	}
	
private:
	Node* _head;
	size_t _size;
};

这是我们实现的list的,在开头部分我们可以看见typedef list_iterator<T,T& ,T*> iterator;和
    typedef list_iterator<T, const T&, const T*> const_iterator;
这两句话我们在使用const list的时候我们会给const_iterator 的迭代器,在list_iterator中 Ref 和Ptr就会被推到成const T&和const T*,这样在我们调用operator*和operator->的时候返回的都是const类型的,但给iterator 的迭代器,在list_iterator中 Ref 和Ptr就会被推到成T&和 T*两个普通类型的,这样在我们调用operator*和operator->的时候返回的既可以是const类型的也可以是普通类型的,具体返回什么样的完全取决于我们需要用哪一个传参传的不一样返回的就不一样。这样这个问题就被解决了。

Ⅳ、list的其他接口

1、insert

list可以支持如何位置的插入,时间复杂度是O(1),list插入的效率挺高不需要像vector的在中间和头插入需要把数据全部向后移动,也不会因为插入以后造成迭代器失效的问题,因为insert了以后迭代器依旧指向那个节点既没有指向其他节点也没有像vector一样造成迭代器类似于野指针的问题。insert的返回值是一个迭代器指向的是刚刚插入的新节点。

insert是在指定位置之前插入数据所以顺序是倒着的。

2、erase

erase也支持在任意位置的删除,同样时间复杂度是O(1),同样的比vector的erase的效率高也是因为vector当要删除头和中间位置的数据时需要移动数据所以效率较低。list的erase也会造成迭代器失效因为如果删掉这个节点而迭代器依旧是指向这一块空间的会造成类似于野指针的问题所以在erase以后要去更新一下防止这个问题的发生。

这里依旧是不更新迭代器就会报错。

3、push_front , pop_front 和 push_back , pop_back

push_front 是在头节点的后面去插入一个节点, pop_front是删除第一个节点 , push_back是在最后插入一个节点 , pop_back是删除最后一个节点。

 4、front,back和size,empty

front返回第一个节点的引用,back是返回最后一个节点的引用,size返回list中有效节点的个数,恶魔empty是判断list是否为空。

文章到此结束,如果有疑问的小伙伴可以在评论区提问记得一键三连哦~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值