【c++】STL中list容器的模拟实现

目录

前言

一、list的框架

 需要注意的是:当结点只有一个的时候,头指针和尾指针均需指向自己。(而且尾结点是_head的_prev所指向的结点,并不是_next) 

二、push_back函数

三、迭代器

1.迭代器框架

2.迭代器++

3.*迭代器

4. it1 != it2

5.迭代器的begin()与end()

6.重载->

7.测试

四、再谈迭代器

 五、insert()函数

六、erase()函数

 需要注意的是:

七、反向迭代器

1.框架

2.++反向迭代器 & 反向迭代器++

3.重载*

4.重载->

5.重载!=

6.反向迭代器的rbegin()与rend()

7.测试

 八、其他的复用代码

1.push_front

2.push_back

3.pop_front

4.pop_back

九、默认成员函数

1.copy构造函数

3.析构函数

3.1clear()

3.2~list()


前言

        STL中list容器就是带头结点的双向带头循环链表,也是之前使用c语言实现过的。

对比顺序表的优点是:

        1.头插头删效率高。

        2.增容效率高,存一个增容一个。

        3.空间利用率高。

缺点是:

        1.不支持随机访问,访问元素的时间复杂度是O(N)。

        2.要占用额外的物理空间进行存储元素之间的关系(prev与next指针)


一、list的框架

        list是双线链表,链表就需要使用指针去完成这个结构。

         

        list只需要一个成员,那就是头结点。

        这个头节点需要引出其他的结点,每个结点是由头指针、尾指针以及数据组成的,据此我们也可以得到结点的框架。(应为struct类型,因为要能够被list类访问到)

代码如下:

#pragma once

#include <iostream>
using namespace std;

namespace qyy
{
	template<class T>
	struct ListNode
	{
		// 初始化结点
		ListNode(cosnt T& data = T())
			:_data(data)
			,_prev(nullptr)
			,_next(nullptr)
		{}

		// 构成结点的成员
		T _data;
		ListNode<T>* _prev;
		ListNode<T>* _next;
	};

	template<class T>
	class list
	{
	public:
		typedef ListNode<T> list_node;
		list()
			:_head(new list_node())
		{
			_head->_prev = _head;
			_head->_next = _head;
		}
	private:
		ListNode<T>* _head;
	};
}

 需要注意的是:当结点只有一个的时候,头指针和尾指针均需指向自己。(而且尾结点是_head的_prev所指向的结点,并不是_next) 

代码如下:

        list()
			:_head(new list_node())
		{
			_head->_prev = _head;
			_head->_next = _head;
		}

 图示:


二、push_back函数

        尾插就如同之前实现的双向带头循环链表一样:

        找到为尾结点,创建一个要插入的结点,将新节点与旧链链接起来即可。

代码如下:

void push_back(const T& val)
		{
			// 创造新结点
			list_node* newNode = new list_node(val);
			// 找到尾结点
			list_node* tail = _head->_prev;
			// 链接
			tail->_next = newNode;
			newNode->_prev = tail;
			newNode->_next = _head;
			_head->_prev = newNode;
		}

图示: 

三、迭代器

       使用迭代器遍历整个list进行验证这个迭代器是否生效。

        list的迭代器是最重要的一个部分,这决定了这个链表能否实现随机访问。

        这个迭代器是一个双向迭代器(Bidirection Iterator),需要支持++、--、但不能支持+、-。

        但是list的迭代器比较特殊:特殊在它不是原生指针,因为链表不是连续存储的物理空间,对于原生指针来说“++”到不了真正意义上的下一个位置。

        所以,list的迭代器需要对原生指针进行封装,使得“++”能够到达下一个结点(可得迭代器的成员变量是指针)。

        同样地,这个迭代器也需要被list类进行访问,所以实现为struct是最好的结构。

1.迭代器框架

        由以上的分析可知,迭代器需要由原生结点指针进行构造。

代码如下:

// 迭代器结构
	template<class T>
	struct Iterator
	{
		typedef ListNode<T> list_node;
		// 成员:结点指针
		list_node* _node;
		// 使用结点指针构造迭代器
		Iterator(list_node* node)
			:_node(node)
		{}
    }

2.迭代器++

        与c语言实现访问链表写一个结点相同,使当前的结点指向_next即可。

        需要注意的是:

        前置++不需要进行多余操作,但是后置++需要返回++之前的值,所以需要保存将++之前的值保存下来。

代码如下:

        // ++it
		Iterator<T> operator++()
		{
			_node = _node->_next;
			return *this;
		}
		// it++
		Iterator<T> operator++(int)
		{
			// 保存没++的迭代器用于return
			Iterator<T>* tmp = this;
			_node = _node->_next;
			return *tmp;
		}

3.*迭代器

        对迭代器进行解引用得到的是结点对应的data,返回类型应为T。

代码如下:

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

4. it1 != it2

        判断相等不相等主要是看结点是不是一个结点,为bool类型。

代码如下:

        // it1 != it2
		bool operator!=(Iterator<T> it)
		{
			return _node != it._node;
		}

5.迭代器的begin()与end()

        需要注意的是迭代器的起始位置与结束位置是对于list来说的,所以应该是list的成员函数。

        返回类型为迭代器,但是_head与_head->next只是一个结点,需要用这两个结点去构造两个迭代器。

        结构见图:

代码如下:

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

6.重载->

        “->”主要是针对于T是自定义类型的模板参数,需要使用箭头进行解引用。

        返回结点数据的地址即可。

代码如下:

// 当T是自定义类型时,需要使用“->”进行解引用
		// it->
		Ptr operator->()
		{
			return &(_node->_data);
		}

7.测试


四、再谈迭代器

        在(三)中实现了一个迭代器,但是它是不全面的,const对象无法调用,即使重载为const成员函数也不对。

         因为这个迭代器仍不是const的迭代器,在成员函数后面加const只是不能修改this中的成员变量,但是iterator仍然不是const iterator。

         解决方法:

        增加模板参数列表,使得迭代器在返回的时候直接返回成const的迭代器,从根本上解决了这个问题。

升级如下:

// 迭代器结构
	// 传进来的是const,那么Ref与Ptr就是const的,反之亦然
	template<class T, class Ref, class Ptr>
	struct Iterator
	{
		typedef ListNode<T> list_node;
		typedef Iterator<T, Ref, Ptr> self;
		// 成员:结点指针
		list_node* _node;
		// 使用结点指针构造迭代器
		Iterator(list_node* node)
			:_node(node)
		{}
		// ++it
		self operator++()
		{
			_node = _node->_next;
			return *this;
		}
		// --it
		self operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		// it--
		self operator--(int)
		{
			self* tmp = this;
			_node = _node->_prev;
			return *tmp;
		}
		// it++
		self operator++(int)
		{
			// 保存没++的迭代器用于return
			self* tmp = this;
			_node = _node->_next;
			return *tmp;
		}
		// *it
		Ref operator*()
		{
			return _node->_data;
		}
		// 当T是自定义类型时,需要使用“->”进行解引用
		// it->
		Ptr operator->()
		{
			return &(_node->_data);
		}
		// it1 != it2
		bool operator!=(const self& it) const
		{
			return _node != it._node;
		}
	};

实现的对于常对象与非常对象均适用的print_list函数:

        void print_list() const
		{
			const_iterator it = begin();
			while (it != end())
			{
				cout << *it << " ";
				it++;
			}
			cout << endl;
		}

测试:

 五、insert()函数

        与双链表实现的插入相同:

        创建要插入的结点,找到要插入地方的前一个,进行链接即可。

图解:

 代码如下:

void insert(iterator pos, const T& val)
		{
			// 创建新结点
			list_node* newNode = new list_node(val);
			// 找出前一个和当前结点
			list_node* prev = pos._node->_prev;
			list_node* cur = pos._node;
			// 链接
			prev->_next = newNode;
			newNode->_next = cur;
			cur->_prev = newNode;
			newNode->_prev = prev;
		}

测试:

六、erase()函数

         与双链表实现的删除相同:

        找到要删除结点的前一个结点与后一个结点,跨过欲删除结点进行链接,完成链接后释放欲删除结点即可。

        需要注意的是不能够删除头结点。

图解: 

代码如下:

void erase(iterator pos)
		{
			// 不能删除头结点
			assert(pos != end());
			// 找到前一个与后一个结点
			list_node* prev = pos._node->_prev;
			list_node* next = pos._node->_next;
			// 链接
			prev->_next = next;
			next->_prev = prev;
			// 释放
			delete pos._node;
		}

图解:

 需要注意的是:

        insert()与erase()均是由返回值的,可以更新迭代器进行持续地插入删除。

官方库:

insert():

 erase():

更新后代码如下:

        iterator insert(iterator pos, const T& val)
		{
			// 创建新结点
			list_node* newNode = new list_node(val);
			// 找出前一个和当前结点
			list_node* prev = pos._node->_prev;
			list_node* cur = pos._node;
			// 链接
			prev->_next = newNode;
			newNode->_next = cur;
			cur->_prev = newNode;
			newNode->_prev = prev;
			return iterator(newNode);
		}
		iterator erase(iterator pos)
		{
			// 不能删除头结点
			assert(pos != end());
			// 找到前一个与后一个结点
			list_node* prev = pos._node->_prev;
			list_node* next = pos._node->_next;
			// 链接
			prev->_next = next;
			next->_prev = prev;
			// 释放
			delete pos._node;

			return iterator(next);
		}

 测试:

insert():

 erase():

七、反向迭代器

1.框架

        反向迭代器与正向迭代器的本质都一样,都是为了访问list的结点且“++”或者“--”能变换指向的结点。不同之处在于反向迭代器的“++”方向与正向的“--”等效,反之亦然。

        反向迭代器是基于正向迭代器之上进行的操作,可以代码复用。(所以得出反向迭代器是需要传一个正向迭代器进来进行构造的)
        和正向迭代器一样,可以使用多个模板参数对于const对象进行设计。

代码如下:

#pragma once

#include <iostream>

namespace qyy
{
	template<class Iterator, class Ref, class Ptr>
	class reverse_iterator
	{
	public:
		// 定义反向迭代器的类型,方便改,也能够简化
		typedef reverse_iterator<Iterator, Ref, Ptr> self;
		// 使用正向迭代器进行构造
		reverse_iterator(const Iterator& it)
			:_rit(it)
		{}
	private:
		Iterator _rit;
	};
}

        为了能够使每个容器都能使用,反向迭代器的rebgin()与rend()是与正向的begin()和end()对称存在的。

图解:

  

        结合这幅图我们可以知道,如果还像正向迭代器那样去访问结点的data,那么会有一个 _head->next位置的结点是访问不到的,所以我们需要使用一个临时正向迭代器tmp进行提前访问。

        当rit到达rend时结束,此时tmp如果要走,已经到了_head。

        _head->_next是什么时候访问的呢?

        当rit到  _head->_next->_next就已经访问过了。

图示:

QQ录屏20230302125602

2.++反向迭代器 & 反向迭代器++

        对于反向迭代器来书,++相当于对正向迭代器的--,所以可以代码复用。

代码如下:

        // ++rit <==> --it
		self operator++()
		{
			--_rit;
			return *this;
		}

        后置++只需要保存++之前的值然后返回即可。

代码如下:

        // rit++ <==> it--
		self operator++(int)
		{
			self* tmp = this;
			_rit--;
			return *tmp;
		}

3.重载*

        需要解引用前一个结点的值。

代码如下:

        Ref operator*()
		{
			Iterator tmp = _rit;
			return *--tmp;
		}

4.重载->

        复用正向迭代器的解引用操作,对其进行取地址可达成目标。

代码如下:

// 复用正向迭代器的取地址操作
		Ptr operator->()
		{
			return &operator*();
		}

5.重载!=

        复用正向迭代器的!=重载即可。

代码如下:

// rit1 != rit2
		bool operator!=(const self& rit)
		{
			return _rit != rit._rit;
		}

6.反向迭代器的rbegin()与rend()

    为了能够使每个容器都能使用,反向迭代器的rebgin()与rend()是与正向的begin()和end()对称存在的。

        同正向的begin()与end()一样,这个也是list的成员函数。

图解:

代码如下:

 typedef reverse_iterator<const_iterator, const T&, const T*> const_reverse_iterator;
 typedef reverse_iterator<iterator, T&, T*> reverse_iterator;
       
        reverse_iterator rbegin()
		{
			return iterator(_head);
		}
		reverse_iterator rend()
		{
			return iterator(_head->_next);
		}
		const_reverse_iterator rbegin() const
		{
			return const_iterator(_head);
		}
		const_reverse_iterator rend() const
		{
			return const_iterator(_head->_next);
		}

7.测试

 八、其他的复用代码

1.push_front

2.push_back

3.pop_front

4.pop_back

        //头插
		iterator push_front(const T& val)
		{
			insert(begin(), val);
			return begin();
		}
		//头删
		iterator pop_front()
		{
			erase(begin());
			return begin();
		}
		//尾插
		iterator pop_front(const T& val)
		{
			insert(end(), val);
			return begin();
		}
		//尾删
		iterator pop_back()
		{
			// end()是头结点
			erase(--end());
			return begin();
		}

九、默认成员函数

        6个默认成员函数有:初始化(default构造)和清理(析构),拷贝(cop构造)和赋值(=重载),取地址重载(&重载(常对象与非常对象))。

        在list中,我们使用结点对list进行了default构造(也就是初始化)。

1.copy构造函数

        需要完成深拷贝,如果让两个指针均指向同一个结点,那么在析构时候会使得一块空间释放两回,造成内存错误。(可以复用尾插)

代码如下:

// copy构造
		// 必须完成深拷贝,否则内存错误 
		list(const list<T>& lt)
			:_head(new list_node())
		{
			_head->_next = _head;
			_head->_prev = _head;

			list<T>::const_iterator it = lt.begin();
			while (it != lt.end())
			{
				push_back(*it);
				++it;
			}
		}

3.析构函数

        可以先实现一个clear()函数对于结点进行数据的清理与结点的回收。

3.1clear()

        可以复用erase()函数进行数据清理。

代码如下:

/ 清理结点,复用erase
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				iterator tmp = it++;
				erase(tmp);
			}
		}

3.2~list()

        可以复用clear()函数进行空间回收,需要注意的是clear()函数不会对头结点进行清理,所以剩下的只用做一个销毁头结点。

代码如下:

// 析构
		~list()
		{
			clear();
			// 最后会剩下头结点
			delete _head;
			_head = nullptr;
		}

测试: 

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值