手撕list 以及模拟实现list的相关函数

⸝⋆ ━━━┓

  • 个性标签 - :来于"云"的"羽球人"。 Talk is cheap. Show me the code

┗━━━━━━━ ➴ ⷯ

本人座右铭 : 欲达高峰,必忍其痛;欲戴王冠,必承其重。

👑💎💎👑💎💎👑

💎💎💎自💎💎💎

💎💎💎信💎💎💎

👑💎💎 💎💎👑 希望在看完我的此篇博客后可以对你有帮助哟

👑👑💎💎💎👑👑 此外,希望各位大佬们在看完后,可以互相支持,蟹蟹!

👑👑👑💎👑👑👑

目录:

一:简单使用

1.push_front()

push_front():此函数功能就是头插一个数据(在第一个有效数据的前面进行插入)

此函数的参数:任意类型的数据(注意这里是使用引用,避免对象过大)

使用示范:

cpp 复制代码
void test()
{
	list<int>l1;// 创建一个list 类型对象
	// 对当前对象l1 进行数据的头插
	l1.push_front(4);
	l1.push_front(3);
	l1.push_front(2);
	l1.push_front(1);
	list<int>::iterator it = l1.begin();
	// 遍历当前l1这个对象的所有数据
	while (it != l1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}
2.push_back()

**此函数功能:**尾插一个数据(就是在尾结点进行插入数据)

使用示范:

cpp 复制代码
void test()
{
	list<int>l1;// 创建一个list 类型对象
	// 对当前对象l1 进行数据的尾插
	l1.push_back(1);
	l1.push_back(2);
	l1.push_back(3);
	l1.push_back(4);
	list<int>::iterator it = l1.begin();
	// 遍历当前l1这个对象的所有数据
	while (it != l1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}
3.pop_front()

pop_front() 函数功能:把第一个有效数据对应的节点进行释放

使用示范:

cpp 复制代码
void test()
{
	list<int>l1;// 创建一个list 类型对象
	// 对当前对象l1 进行数据的头插
	l1.push_front(4);
	l1.push_front(3);
	l1.push_front(2);
	l1.push_front(1);
	list<int>::iterator it = l1.begin();
	// 遍历当前l1这个对象的所有数据
	while (it != l1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	l1.pop_front();//删除第一个数据
	cout << "pop_front:";
	it = l1.begin();
	while (it != l1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

运行结果:

4. pop_back()

pop_back()函数功能:把最后一个有效数据对应的节点进行释放

使用示范:

cpp 复制代码
void test()
{
	list<int>l1;// 创建一个list 类型对象
	// 对当前对象l1 进行数据的头插
	l1.push_front(4);
	l1.push_front(3);
	l1.push_front(2);
	l1.push_front(1);
	list<int>::iterator it = l1.begin();
	// 遍历当前l1这个对象的所有数据
	while (it != l1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	l1.pop_back();//删除最后一个数据
	cout << "pop_back:";
	it = l1.begin();
	while (it != l1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

运行结果:

5. insert()

insert()函数功能: 在指定位置之前进行数据插入

此函数支持:某个位置插入一个数据,也支持从当前位置插入n 个数据;还支持一段区间数据的插入

使用示范:

cpp 复制代码
void test()
{
	list<int>l1;// 创建一个list 类型对象
	// 对当前对象l1 进行数据的头插
	l1.push_front(4);
	l1.push_front(3);
	l1.push_front(2);
	l1.push_front(1);
	list<int>::iterator it = l1.begin();
	// 遍历当前l1这个对象的所有数据
	while (it != l1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	it = l1.begin();
	l1.insert(++it, 22);
	it = l1.begin();
	while (it != l1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

运行结果:

6. erase()

erase() 函数功能:对指定位置删除

此函数支持:对某个位置的删除;也支持一段区间的删除

使用示范:

cpp 复制代码
void test()
{
	list<int>l1;// 创建一个list 类型对象
	// 对当前对象l1 进行数据的头插
	l1.push_front(4);
	l1.push_front(3);
	l1.push_front(2);
	l1.push_front(1);
	list<int>::iterator it = l1.begin();
	// 遍历当前l1这个对象的所有数据
	while (it != l1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	it = l1.begin();
	l1.erase(++it);
	it = l1.begin();
	while (it != l1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

运行结果:

7. swap()

swap() 函数功能: 对多个list 类型对象进行数据交换

使用示范:

cpp 复制代码
void test()
{
	list<int>l1;// 创建一个list 类型对象
	// 对当前对象l1 进行数据的头插
	l1.push_front(4);
	l1.push_front(3);
	l1.push_front(2);
	l1.push_front(1);

	list<int>l2;
	l2.push_back(11);
	l2.push_back(22);
	l2.push_back(33);
	l2.push_back(44);
	l2.push_back(55);
	cout << " 交换之前对应的数据:" << endl;

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

	list<int>::iterator it1 = l2.begin();
	cout << "l2:";
	while (it1 != l2.end())
	{
		cout << *it1 << " ";
		++it1;
	}
	cout << endl;
	l1.swap(l2);
	cout << "交换之后对应的数据:" << endl;
	 it = l1.begin();
	cout << "l1:";
	while (it != l1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	 it1 = l2.begin();
	cout << "l2:";
	while (it1 != l2.end())
	{
		cout << *it1 << " ";
		++it1;
	}
	cout << endl;
}

运行结果:


不知道一些老铁是否和我一样有个问题:为什么算法库里面有一个swap() 而list 容器还要自己实现swap(),这是为啥???

这就涉及到迭代器的分类了

迭代器按功能分:正向迭代器和反向迭代器

迭代器按性质分为:

单向迭代器:只支持 ++ 比如单链表,哈希表

双向迭代器:支持++ 和-- 比如list ,红黑树(map,set)

随机迭代器:++ , -- ,+ ,- 比如vector ,string......

而我们的list 迭代器是双向的,但是算法库里面的swap () 要求迭代器的类型是随机的,所以list 只能自己实现一个swap () 函数

list 迭代器:

8.clear()

clear() 函数功能:清除有效数据

使用示范:

cpp 复制代码
void test()
{
	list<int>l1;// 创建一个list 类型对象
	// 对当前对象l1 进行数据的头插
	l1.push_front(4);
	l1.push_front(3);
	l1.push_front(2);
	l1.push_front(1);
	list<int>::iterator it = l1.begin();
	// 遍历当前l1这个对象的所有数据
	while (it != l1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	l1.clear();//清除有效数据
	it = l1.begin();
	while (it != l1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

运行结果:

二:模拟实现相关函数

在我们模拟list 相关函数之前对需要对list 这个容器有一定的了解。通过前面数据结构对链表结构的学习,再结合官方文档模拟实现应该是不成问题。

首先list 这一个容器是由3个大类进行封装实现的:一个是节点类,一个是迭代器类,一个是链表类。其中前节点类和迭代器类都是为链表类进行服务的

对list这一容器实现的灵魂在于迭代器的实现:通过对一些运算符进行重载从而实现对迭代器的封装。

准备工作:

注意我这里为了避免与库里面的list 发生冲突,就把自己写的相关代码放在自己定义的y这个命名空间了(各位在模拟实现的时候,随意,看自己)

定义一个节点类

对于双向循环链表的每一个节点而言无非就是由3部分构成的:数据域;前一个节点的地址;后一个节点的地址

cpp 复制代码
template<class T>
	struct list_node  // 之所以使用struct 而不用class ,没有特殊情况下,前者默认属性是pubblic
	{
		T _val;
		list_node<T>* _next;
		list_node<T>* _pre;
		list_node(const T& x = T())   //对于T 的类型并不明确,不能指定_val 就是int类型,就初始化为0,所以采用匿名对象的形式
			:_val(x)
			, _next(nullptr)
			, _pre(nullptr)
		{}
	};

定义一个迭代器类

cpp 复制代码
template <class T>
	struct list_iterator// 迭代器类
	{
		typedef list_iterator<T> self;//对当前迭代器进行重命名
		typedef list_node Node;
		Node* node;//迭代器其实就是指向每一个的节点的
		//迭代器涉及到的相关函数
		list_iterator(Node* node) // 不需要实现析构函数
			:_node(node)
		{}
		T& operator*()
		{
			return _node->_val;
		}
		T* operator->()
		{
			return &_node->_val;//返回当前数据对应的地址
		}
		self& operator++()
		{
			_node = _node->_next;
			return *this;//前置++ 不涉及到临时对象,可以使引用返回
		}
		self operator++(int)//后置++
		{
			Node* tmp(_node);//创建一个临时对象,因为后置++ 返回的是++之前的对象
			_node = _node->_next;
			return tmp;
		}
		self& operator--()
		{
			_node = _node->_pre;
			return *this;
		}
		self operator--(int) // 后置--
		{
			Node* tmp(_node);
			_node = _node->_pre;
			return tmp;
		}
	};

接下来思考一个问题是不是const 迭代器就是对普通迭代器加一个const 进行修饰即可???

各位老铁看看以下写法是否正确

普通迭代器: iterator

const 迭代器: const iterator

答案自然是错误的:相信有不少人在初学迭代器相关内容的时候,cosnt 迭代器无非就是对普通迭代器加一个cosnt 进行修饰嘛。OK 那思考以下问题

cosnt T*

T* const

是否有区别??? 若有区别,具体又是啥区别???

有区别的

cosnt T* : 是对 T* 进行修饰,换言之就是对当前指针指向的内容修饰(当前的数据不可以修改,只能读)

T* cosnt : 修饰的是当前指针

cosnt 迭代器和普通迭代器是两个不同的类型

对于list 迭代器的实现:在底层是没有区别的

所以说:对于一个程序员而言,相同的代码那就没有必要在撸一遍了,这种事情咱还是交给小弟来搞即可(编译器)

这就不得不提及模板参数了

编译器会根据当前参数的不同,自动生成对应的函数

普通迭代器对应的参数类型:T,T&,T*

const 迭代器对应参数类型:T ,const T&,const T*

cpp 复制代码
template<class T,class Ref,class Ptr> // 此时的Ref可以表示T& 或者 coonst T& ;Ptr可以表示T* 或者const T* 

struct _list_iterator //迭代器其实就是对当前节点进行一系列的封装
	{
		typedef _list_iterator<T,Ref,Ptr> self;// 进行重命名
		typedef list_node<T> Node;
		Node* _node;
		_list_iterator(Node* node)// 迭代器的构造函数  不需要写析构函数(析构函数是把当前这个节点释放,但是在使用迭代器的时候并不需要进行节点释放在某些场景下)
			:_node(node)
		{}
		//T& operator*()
		//{
		//	return _node->_val;
		//}
		//T* operator->()// 意义:对自定义类型数据访问更加方便,原始访问:(*it).某个具体成员
		//{
		//	return &(_node->_val);//返回当前数据的地址
		//}
		//self& operator++()
		//{
		// _node = _node->_next;//注意返回的是一个迭代器
		// return *this;
		//}
		//self& operator++(int)
		//{
		//	Node* tmp(_node);
		//	_node = _node->_next;
		//	return tmp;
		//}
		//self*& operator--()
		//{
		//	_node =  _node->_pre;
		//	return _node;
		//}
		//self*& operator--(int)
		//{
		//	Node* tmp(_node);
		//	_node = _node->_pre;
		//	return tmp;
		//}
		Ref operator*()
		{
			return _node->_val;
		}
		Ptr operator->()// 意义:对自定义类型数据访问更加方便,原始访问:(*it).某个具体成员
		{
			return &(_node->_val);//返回当前数据的地址
		}
		self& operator++()
		{
			_node = _node->_next;//注意返回的是一个迭代器
			return *this;
		}
		self& operator++(int)
		{
			Node* tmp(_node);
			_node = _node->_next;
			return tmp;
		}
		self& operator--()
		{
			_node = _node->_pre;
			return _node;
		}
		self& operator--(int)
		{
			Node* tmp(_node);
			_node = _node->_pre;
			return tmp;
		}
		bool operator==(const self& s)
		{
			return _node == s;
		}
		bool operator!=(const self& s)
		{
			//return _node != s;// 应该比较的是 _node
			return _node != s._node;
		}
	};

定义一个链表类

cpp 复制代码
	struct list//链表类
	{
		typedef list_node<T> Node;
	private:
		size_t _size;//统计节点的个数
		Node* _head;//哨兵位
	pubblic:
		void empty_init()
		{
			_head = new Node;
			//构成环
			_head->_next = _head->_pre = _head;
			_size = 0;
		}
		typedef list_iterator<T> self;//对当前迭代器进行重命名
		list()
		{
			empty_init();
		}
	};
1.push_back() 模拟实现

尾插也就是在在尾结点(_head-> _pre)后面进行数据插入,之后在进行指针连接

cpp 复制代码
	void push_back(const T& x) // 使用T:来表示任意类型数据
		{
			Node* tail = _head->_pre;
			Node* newnode = new Node(x);
			tail->_next = newnode;
			newnode->_next = _head;
			newnode->_pre = tail;
			//注意不要忘记对_head->pre进行链接
			_head->_pre = newnode;
			
		}
2.push_front() 模拟实现

头插:在第一个数据前面插入(_head-> _next)

cpp 复制代码
void pop_back()
		{
			Node* del = _head->_pre;
			del->_pre->_next = _head;
			_head->_pre = del->_pre;
			delete del;
			
		}
3. pop_back() 模拟实现

尾删:把最后一个数据对应的节点释放,同时让倒数第二个节点成为新的尾结点

cpp 复制代码
void pop_back()
		{
			Node* del = _head->_pre;
			del->_pre->_next = _head;
			_head->_pre = del->_pre;
			delete del;
	
		}
4. pop_front() 模拟实现
cpp 复制代码
		void pop_front()
		{
			Node* del = _head->_next;
			_head->_next = del->_next;
			del->_next->_pre = _head;
			delete del;
	
		}
5. insert( ) 模拟实现

insert( )一般默认是在指定位置之前进行数据的插入

注意节点连接的先后顺序问题

cpp 复制代码
iterator insert(iterator pos,const T& x)
		{
			//默认在pos 位置之前插入
			Node* newnode = new Node(x);
			Node* pre = pos._node->_pre;
			newnode->_next = pos._node;
			pos._node->_pre = newnode;
			newnode->_pre = pre;
			pre->_next = newnode;
			++_size;
			return newnode;//返回插数据对应的迭代器
		}
6. erase ()模拟实现

erase( ) 默认是把指定位置的数据删除

相信初次模拟此函数的时候,有不少老铁是这样写的吧:


注意这样写是有问题的:迭代器失效

当我们再访问pos 下一个位置的时候就会出现迭代器失效的问题

解决:返回下一个节点对应的迭代器

正确代码:
cpp 复制代码
iterator erase(iterator pos)
		{
			//进行判空
			//assert(!empty());
			//删除pos位置数据
			Node* pre = pos._node->_pre;
			Node* next = pos._node->_next;
			pre->_next = next;
			next->_pre = pre;
			delete pos._node;//此时迭代器失效
			--_size;
			return next;//返回下一个节点,解决迭代器失效问题
		}
7.clear ( ) 模拟实现

一般写法:就是一个节点一个节点进行释放

其实完全可以借助erase( ) 函数进行编写:因为erase () 函数返回的就是下一个节点的迭代器

cpp 复制代码
	void clear()//清除有效数据
		{
			
			list<T>::iterator it = begin();// 其实是this 对象调用当前函数
			//传统写法:就是保留下一个节点,删除当前节点
			//现代写法:
			while (it != end())
			{
				it = erase(it);//erase() 返回的就是下一个节点对应的迭代器
			}
			_size = 0;
		}
8. 析构函数

此时还是借用clear ( ) 函数进行

只不过只需要把头结点进行释放即可

cpp 复制代码
	~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}
9. swap()函数模拟实现

直接调用库里面的函数 swap(),交换链表对应的头结点以及_size的大小即可

cpp 复制代码
void swap(list<int>& l)
		{
			std::swap(_head, l._head);
			std::swap(_size, l._size);
		}
10.operator=() 函数重载
传统写法:
cpp 复制代码
// l2 = l1 :l2是对l1 的拷贝构造
		//传统写法
		iterator operator=(const list<int>& l)//l 指向对象 l1  this 指向当前对象l2
		{
			//第一次写拷贝构造函数的时候,编译错误:const 对象调用非const对象,权限扩大
			//解决:需要const 迭代器
			//const 与非const迭代器区别:无非就是对应函数返回值类型不一样==》 借用函数模板
			clear();//对l2进行数据清理
			list<int>::iterator it = l.begin();
			while (it != l.end())
			{
				push_back(*it);
				++it;
			}
		}

注意一些细节的实现:当我们没有实现对 const 迭代器的时候,此时编译是有问题的,因为此时对象的属性是cosnt ,编译器只能调用非const 对应的函数,自然造成权限放大的问题,导致编译不过

解决:写一个cosnt 迭代器

现代写法:
cpp 复制代码
list<int>& operator=( list<int>& l)
		{
			swap(l);
			return *this;
		}

结语:以上就是我今日share 的内容。对于list 这个容器模拟实现的关键在于对迭代器的掌握(进行封装,屏蔽底层实现的细节,让使用者以统一的方式进行使用,但是迭代器类型不同在底层实现也是不一样的可以这么说,迭代器其实就是模拟指针的行为

相关推荐
就爱学编程18 分钟前
重生之我在异世界学编程之C语言:数据在内存中的存储篇(上)
c语言·数据结构
狄加山67543 分钟前
数据结构(顺序表)
数据结构
Protinx44 分钟前
2009年408真题解析-数据结构篇(未完)
数据结构·经验分享·考研·408·计算机考研
析木不会编程44 分钟前
【数据结构】【线性表】栈在算术表达式中的应用
数据结构
R_.L1 小时前
数据结构:单链表
数据结构
GISer_Jing1 小时前
前端Javascript数据结构与算法常见题目(二 **简单level**)
javascript·数据结构·算法
SchrodingerSDOG1 小时前
(补)算法刷题Day24: BM61 矩阵最长递增路径
数据结构·python·算法·矩阵
用户0099383143012 小时前
代码随想录算法训练营第十三天 | 二叉树part01
数据结构·算法
shinelord明2 小时前
【再谈设计模式】享元模式~对象共享的优化妙手
开发语言·数据结构·算法·设计模式·软件工程
小手cool2 小时前
List反转的方法
list