c++复习:模拟实现list来更好的理解和应用list

一、介绍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不会知道下标的随机访问,只能够通过迭代器进行访问

只能用迭代器进行访问。

二、特定接口介绍

接口介绍:

本人觉得这里list的大多数接口与vector和string的差不多,重点介绍几个即可。

list有反向迭代器(待会讲解)

我们来讲讲splice,remove,remove_if,unique,merge,sort,reverse。

2.1 splice

复制代码
void splice (iterator position, list& x)
cpp 复制代码
int main()
{
	list<int> l1(5, 2);
	list<int> l2(5, 3);
	list<int>::iterator pos = l1.begin();
	//void splice(iterator pos,list&x):转移整个链表
	//第一个pos指的是我们要转移到的那个链表的具体位置,list&x代表转移的整个链表
	l1.splice(pos, l2);
	for (auto& e : l1)
	{
		cout << e << endl;
	}
	return 0;
}
复制代码
void splice (iterator position, list& x, iterator i);
cpp 复制代码
int main()
{
	list<int> l1(5, 2);
	list<int> l2(5, 3);
	list<int>::iterator pos1 = l1.begin();
	list<int>::iterator pos2 = l2.begin();
	//void splice (iterator position, list& x, iterator i):转移单个元素
	//第一个pos指的是我们要转移到的那个链表的具体位置,list&x代表转移的整个链表,i是x中迭代器指向的  
    //要转移的元素
	l1.splice(pos1, l2,pos2);
	for (auto& e : l1)
	{
		cout << e << endl;
	}
	return 0;
}
复制代码
void splice (iterator position, list& x, iterator first, iterator last);
cpp 复制代码
int main()
{
	list<int> l1(5, 2);
	list<int> l2(5, 3);
	list<int>::iterator pos1 = l1.begin();
	list<int>::iterator first = l2.begin(), end = l2.end();
	//void splice (iterator position, list& x, iterator first, iterator last):转移迭代器区间
	//第一个pos指的是我们要转移到的那个链表的具体位置,list&x代表转移的整个链表,first和last是要转移的迭代器区间。
	l1.splice(pos1, l2,first,end);
	for (auto& e : l1)
	{
		cout << e << endl;
	}
	return 0;
}

2.2 remove的介绍

复制代码
void remove (const value_type& val);
cpp 复制代码
int main()
{
	list<int> l1(5, 2);
	list<int> l2(5, 3);
	list<int>::iterator pos1 = l1.begin();
	list<int>::iterator first = l2.begin(), end = l2.end();
	//void splice (iterator position, list& x, iterator first, iterator last):转移迭代器区间
	//第一个pos指的是我们要转移到的那个链表的具体位置,list&x代表转移的整个链表,first和last是要转移的迭代器区间。
	l1.splice(pos1, l2,first,end);
	l1.remove(3);
	for (auto& e : l1)
	{
		cout << e << endl;
	}
	return 0;
}

2.3 remove_if的介绍

这里需要等到我们讲完lambda表达式等之后再讲,通俗来说就是删除所有满足自定义条件的元素

2.4 unique的介绍

就是去除重复元素:

cpp 复制代码
int main()
{
	list<int> l1(5, 2);
	list<int> l2(5, 3);
	list<int>::iterator pos1 = l1.begin();
	list<int>::iterator first = l2.begin(), end = l2.end();
	//void splice (iterator position, list& x, iterator first, iterator last):转移迭代器区间
	//第一个pos指的是我们要转移到的那个链表的具体位置,list&x代表转移的整个链表,first和last是要转移的迭代器区间。
	l1.splice(pos1, l2,first,end);
	l1.unique();
	for (auto& e : l1)
	{
		cout << e << endl;
	}
	return 0;
}

2.5 merge的介绍

复制代码
void merge (list& x);

这是默认使用<来进行比较的,将两个有序链表进行合并,变成一个新的有序链表。

cpp 复制代码
int main()
{
	list<int> l1(5, 2);
	list<int> l2(5, 3);
	list<int>::iterator pos1 = l1.begin();
	//void splice (iterator position, list& x, iterator first, iterator last):转移迭代器区间
	//第一个pos指的是我们要转移到的那个链表的具体位置,list&x代表转移的整个链表,first和last是要转移的迭代器区间。
	l1.merge(l2);
	for (auto& e : l1)
	{
		cout << e << endl;
	}
	return 0;
}

还有一个类型的接口是关于仿函数的,后面讲了模版进阶再补充

2.6 sort

复制代码
void sort();

进行从小到大的排序

2.7 reverse

需要注意的是,他只是改变节点的顺序,1<->2<->3<->4<->5,变成了5<->4<->3<->2<->1,一开始迭代器it指向1,reverse之后仍然指向1!!!

三、迭代器失效问题

3.1 insert是否会导致迭代器失效呢?

insert:在迭代器 pos 之前插入新元素。

list的insert插入并不会产生移动,所以有以下结论:

  • 所有已存在的迭代器、指针、引用,全部保持有效!
  • 仅仅新增了一个节点,原来所有节点的内存没有动,只是修改相邻节点的指针指向。
  • 返回值:指向新插入元素的迭代器

所以insert并不会导致迭代器失效。

3.2 erase是否会导致迭代器失效呢?

而earse必然会失效,因为该迭代器对应的节点被删除了。如果我们想继续用的话,就得利用返回值去更新迭代器,返回值是被删除元素的下一个位置的迭代器。

四、模拟实现

4.1基础知识介绍:

先看看源代码

第一部分:链表节点

第二部分:迭代器

我们在自己封装时,先整出一个结点类

然后再定义出链表类,链表类的成员变量只需要头结点和大小就可以了。

list类里面的接口实现基本都依赖于迭代器,没有迭代器根本实现不了,所以我们开始我们的重头戏,迭代器类的封装:

4.2迭代器的实现

首先对比一下vector和list的迭代器

我们在学习vector的时候,vector的迭代器本质就是原生指针,这是因为vector的空间是连续的,我们能够直接通过指针的++或者--,以及解引用来获得下一个数据,上一个数据。

但是list可不是啊

list它压根不连续,我们怎么通过++找到next指向的结点,通过--找到prev指向的结点,通过解引用直接找到_data呢?答案是运算符重载,由于绝大多数运算符都要求重载,所以我们就直接封装一个迭代器,在这个迭代器里面实现各种运算符的重载即可,在定义形参的时候,直接用迭代器定义即可。

我们先把代码写出来,待会慢慢分析即可

cpp 复制代码
template<class T,class Ref,class Ptr>
struct iterator
{
private:
	typedef list_node<T> Node;
	typedef iterator<T, Ref, Ptr> Self;
	Node* _node;//定义一个结点
public:
	iterator(Node*node):_node(node)
	{}
	//解引用:
	Ref operator*()const
	{
		return _node->_data;
	}
	Ptr operator->()const
	{
		return &_node->_data;
	}
	//前置++
	Self& operator++()
	{
		return _node->_next;
	}
	//前置--
	Self& operator--()
	{
		return _node->_prev;
	}
	//后置++
	Self operator++(int)
	{
		Self temp(*this);
		_node = _node->_next;
		return temp;
	}
	Self operator--(int)
	{
		Self temp(*this);
		_node = _node->_prev;
		return temp;
	}
	//判断相等
	bool operator==(const Self& s)const
	{
		return _node == s._node;
	}
	bool operator!=(const Self& s)const
	{
		return _node != s._node;
	}
};
4.2.1 解引用

我们要的效果就是解引用之后返回_data。

第一个解引用运算符,返回Node的内置类型_data,直接得到数据,返回的是引用类型,避免了拷贝。

第二个是箭头运算符,返回的是数据成员的地址,这里有个小知识:

it->member 等价于 (it.operator->())->member,箭头运算符支持链式展开,这也是迭代器实现operator->的标准写法。

4.2.2 前置++与前置--

其实就是返回这个结点的_prev指向的结点和_next指向的结点

4.2.3 后置++与后置--

先保存现在的状态,然后再改变指向,最后返回初始状态:

问题1:我们在这里++和--的时候直接返回节点不好吗,为什么要返回迭代器呢?

答案:链表节点(list_node<T>*)是迭代器内部的实现细节,对外绝不暴露!迭代器才是给用户使用的统一接口。

如果说返回结点会怎么样呢?

  • 用户可以随便篡改 _prev_next 指针,直接破坏链表结构;
  • 一旦后续底层实现改动(修改节点结构),外部代码全部崩溃。

迭代器把节点指针包起来,只开放安全允许的操作++ -- * -> == !=。 用户无法直接访问、修改 _node->_next_node->_prev

而迭代器就不会,迭代器将_node变为了私有成员,外部的代码只能调用我的成员函数,外部拿不到_node,无法直接读写_prev和_next。

还有以下困难

总的来说:迭代器内部封装了什么,外部的才能够用什么,这大大提高了list的安全性。

4.3迭代器的使用

cpp 复制代码
template<class T>
class list
{
public:
	typedef list_node<T> Node;
	typedef iterator<T, T&, T*> iterator;
	typedef iterator<T, const T&, const T*> const_iterator;
	//begin
	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);
	}
private:
	Node* _head;//这里只需要定义我们的头结点即可
	size_t _size;//然后这里记录了我们的链表大小
};

这是使用迭代器实现的一些接口

有个小问题:为什么不直接写成typedef const iterator const_iterator呢?

因为typedef碰到const的话,就不是简单的字符串替换 实际上你以为的const T* ,在这里变成了T*const ,因为迭代器我们是希望他可以进行++和--的,而我们只是不希望他指向的内容给改变,所以我们的const要修饰的是指针的内容,而不是修饰指针。

4.4 list成员函数的实现

4.4.1构造函数
1.默认构造函数
2.有参构造函数
3.迭代器区间构造
4.拷贝构造

直接范围for即可

4**.5修改相关的函数**

empty和size

cpp 复制代码
//返回大小
size_t size() const
{
	return _size;
}
//判断是否为空
bool empty()const
{
	return _size == 0;
}

insert

cpp 复制代码
//插入接口
iterator insert(iterator pos, const T& val)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* newnode = new Node(val);
	newnode->_next = cur;
	newnode->_prev = prev;
	prev->_next = newnode;
	cur->_prev = newnode;
	_size++;
	return iterator(newnode);
}

erase

cpp 复制代码
//删除接口
iterator erase(iterator pos)
{
	assert(pos != end());
	Node* prev = pos._node->_prev;
	Node* next = pos._node->_next;
	prev->_next = next;
	next->_prev = prev;
	delete pos._node;
	--_size;
	return iterator(next);
}

尾插尾删头插头删

cpp 复制代码
//尾插
void push_back(const T& val)
{
	insert(end(),val);

}
//头插
void push_front(const T& val)
{
	insert(begin(), val);
}
//头删
void pop_back()
{
	erase(begin());
}
//尾删
void pop_front()
{
	erase(--end());
}

resize

cpp 复制代码
////resize  如果n小于当前容量的大小,则内容将减少到前n个元素 当n大于容器大小时,则在末尾插入任意容量的内容。
void resize(size_t n, const T& val = T())
{
	size_t sz = size();
	if (n < sz)
	{
		while (n < sz)
		{
			pop_back();
			sz--;
		}
	}
	else
	{
		while (n > sz)
		{
			push_back(val);
			sz++;
		}
	}
}

4.6 clear和析构函数

cpp 复制代码
//清理函数:只剩下头结点
void clear()
{
	iterator it = begin();
	while (it!=end())
	{
		it=erase(it);
	}
}
~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

4.7 赋值重载和assign

cpp 复制代码
//assign直接替换
void assign(size_t n, const T& val)
{
	clear();//只剩头结点
	for (size_t i = 0; i < n; i++)
	{
		push_back(val);
	}
}
//assign迭代器区间替换
template<class InputIterator>
void assign(InputIterator first, InputIterator last)
{
	clear();
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}
//assign迭代器区间替换重载,防止间接寻址
void assign(int n, const T& val)
{
	clear();//只剩头结点
	for (size_t i = 0; i < n; i++)
	{
		push_back(val);
	}
}
//赋值重载
list<T>& operator=(const list<T>& lt)
{
	assign(lt.begin(), lt.end());
	return *this;
}

五、总代码

cpp 复制代码
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace whx
{
	//首先实现一个结点
	template<class T>
	struct list_node
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _data;
		
		list_node(const T &data = T())
			:_next(nullptr)
			,_prev(nullptr)
			,_data(data)
		{}
	};
	template<class T>
	class list;
	template<class T,class Ref,class Ptr>
	struct list_iterator
	{
	private:
		typedef list_node<T> Node;
		typedef list_iterator<T, Ref, Ptr> Self;
		Node* _node;//定义一个结点
	public:
		friend class list<T>;//声明友元
		list_iterator(Node*node):_node(node)
		{}
		//解引用:
		Ref operator*()const
		{
			return _node->_data;
		}
		Ptr operator->()const
		{
			return &_node->_data;
		}
		//前置++
		Self& operator++()
		{
			return _node->_next;
		}
		//前置--
		Self& operator--()
		{
			return _node->_prev;
		}
		//后置++
		Self operator++(int)
		{
			Self temp(*this);
			_node = _node->_next;
			return temp;
		}
		Self operator--(int)
		{
			Self temp(*this);
			_node = _node->_prev;
			return temp;
		}
		//判断相等
		bool operator==(const Self& s)const
		{
			return _node == s._node;
		}
		bool operator!=(const Self& s)const
		{
			return _node != s._node;
		}
	};

	//开始封装链表
	//可以在迭代器类内部声明lis为友元函数,这样迭代器的_node仅仅对list开放
	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;
		//begin
		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;
			_size = 0;
		}
		//无参构造
		list()
		{
			empty_init();
		}
		//有参构造
		list(size_t n,const T& val=T())
		{
			empty_init();
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);
			}
		}
		//迭代器区间构造
	    template<class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			empty_init();
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}
		//拷贝构造:直接迭代器
		list(const list<T>& l)
		{
			empty_init();
			for (auto& e : l)
			{
				push_back(e);
			}
		}
		//插入接口
		iterator insert(iterator pos, const T& val)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(val);
			newnode->_next = cur;
			newnode->_prev = prev;
			prev->_next = newnode;
			cur->_prev = newnode;
			_size++;
			return iterator(newnode);
		}
		//删除接口
		iterator erase(iterator pos)
		{
			assert(pos != end());
			Node* prev = pos._node->_prev;
			Node* next = pos._node->_next;
			prev->_next = next;
			next->_prev = prev;
			delete pos._node;
			--_size;
			return iterator(next);
		}
		//尾插
		void push_back(const T& val)
		{
			insert(end(),val);

		}
		//头插
		void push_front(const T& val)
		{
			insert(begin(), val);
		}
		//头删
		void pop_back()
		{
			erase(begin());
		}
		//尾删
		void pop_front()
		{
			erase(--end());
		}
		////resize  如果n小于当前容量的大小,则内容将减少到前n个元素 当n大于容器大小时,则在末尾插入任意容量的内容。
		void resize(size_t n, const T& val = T())
		{
			size_t sz = size();
			if (n < sz)
			{
				while (n < sz)
				{
					pop_back();
					sz--;
				}
			}
			else
			{
				while (n > sz)
				{
					push_back(val);
					sz++;
				}
			}
		}
		//assign直接替换
		void assign(size_t n, const T& val)
		{
			clear();//只剩头结点
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);
			}
		}
		//assign迭代器区间替换
		template<class InputIterator>
		void assign(InputIterator first, InputIterator last)
		{
			clear();
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		//assign迭代器区间替换重载,防止间接寻址
		void assign(int n, const T& val)
		{
			clear();//只剩头结点
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);
			}
		}
		//赋值重载
		list<T>& operator=(const list<T>& lt)
		{
			assign(lt.begin(), lt.end());
			return *this;
		}
		//返回大小
		size_t size() const
		{
			return _size;
		}
		//判断是否为空
		bool empty()const
		{
			return _size == 0;
		}
		//清理函数:只剩下头结点
		void clear()
		{
			iterator it = begin();
			while (it!=end())
			{
				it=erase(it);
			}
		}
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}
	private:
		Node* _head;//这里只需要定义我们的头结点即可
		size_t _size;//然后这里记录了我们的链表大小
	};

}

测试用例:

cpp 复制代码
#include<list>
#include"list.h"

#include <iostream>
#include <string>
using namespace std;

// 这里放你已实现的 Node、list_iterator、list<T> 代码

void TestBasicInsert()
{
    cout << "===== 测试用例1:基础插入与正向遍历 =====\n";
    list<int> lt;
    auto it1 = lt.insert(lt.end(), 10);
    auto it2 = lt.insert(lt.end(), 20);
    lt.insert(it2, 15); // 在20前面插入15

    // 预期链表:10 <-->15 <-->20
    for (auto it = lt.begin(); it != lt.end(); ++it)
    {
        cout << *it << " ";
    }
    cout << "\n预期输出:10 15 20\n\n";
}

void TestInsertBegin()
{
    cout << "===== 测试用例2:在头部持续插入 =====\n";
    list<int> lt;
    lt.insert(lt.begin(), 3);
    lt.insert(lt.begin(), 2);
    lt.insert(lt.begin(), 1);
    // 预期:1 2 3
    for (auto it = lt.begin(); it != lt.end(); ++it)
    {
        cout << *it << " ";
    }
    cout << "\n预期输出:1 2 3\n\n";
}

void TestEmptyListInsert()
{
    cout << "===== 测试用例3:空链表调用insert(end()) =====\n";
    list<string> lt;
    // 空链表 end() == begin(),在end插入等价尾插
    auto new_it = lt.insert(lt.end(), "hello");
    cout << "插入元素:" << *new_it << "\n";
    cout << "size预期等于1\n\n";
}

void TestContinuousMultiInsert()
{
    cout << "===== 测试用例4:多次定点插入混合 =====\n";
    list<int> lt;
    auto pos = lt.insert(lt.end(), 100);
    lt.insert(pos, 90);
    lt.insert(lt.end(), 110);
    lt.insert(lt.begin(), 80);
    // 预期:80 90 100 110
    for (auto it = lt.begin(); it != lt.end(); ++it)
    {
        cout << *it << " ";
    }
    cout << "\n\n";
}

void TestReturnedIteratorValid()
{
    cout << "===== 测试用例5:验证insert返回迭代器有效 =====\n";
    list<int> lt;
    auto ret_it = lt.insert(lt.end(), 500);
    if (ret_it != lt.end() && *ret_it == 500)
    {
        cout << "✅ 返回迭代器有效测试通过\n\n";
    }
}

void TestCustomType()
{
    cout << "===== 测试用例6:自定义结构体类型测试 =====\n";
    struct Student
    {
        int id;
        string name;
        bool operator==(const Student& other) const
        {
            return id == other.id;
        }
    };
    list<Student> stulist;
    stulist.insert(stulist.end(), Student{ 1, "Zhang" });
    stulist.insert(stulist.end(), Student{ 2, "Li" });
    cout << "自定义结构体插入正常\n\n";
}

void TestLargeAmountInsert()
{
    cout << "===== 测试用例7:批量大量插入压力测试 =====\n";
    list<int> lt;
    const int N = 2000;
    for (int i = 0; i < N; i++)
    {
        lt.insert(lt.end(), i);
    }
    cout << "批量" << N << "个元素插入完成,无崩溃则正常\n\n";
}

int main()
{
    TestBasicInsert();
    TestInsertBegin();
    TestEmptyListInsert();
    TestContinuousMultiInsert();
    TestReturnedIteratorValid();
    TestCustomType();
    TestLargeAmountInsert();
    cout << "所有基础测试执行完毕!\n";
    return 0;
}

结果:

相关推荐
小白学大数据1 小时前
Python 爬虫实战:抓取汽车之家二手车成交价格与里程数据
开发语言·爬虫·python·汽车
AmyLin_20011 小时前
PDF 色彩保真工程实践【5】核心实现(下):拼装 Image XObject、页面资源与内容流
c++·pdf·sdk·颜色空间·印刷·icc·cmyk
深盾科技_Virbox1 小时前
软件加密工具怎么选:试用验证、采购成本与授权范围
开发语言·安全·软件需求
A_cainiao_A1 小时前
C++20:std::stop_source让线程说停就听
开发语言·c++20
乱七八糟的屋子2 小时前
MTL4 (Matrix Template Library) 超详细实战教程|C++高性能稠密/稀疏矩阵计算
c++·矩阵·稀疏矩阵·mtl4·matrixtemplate·c++线性代数·高性能数值计算
fīɡЙtīиɡ ℡2 小时前
深入学习JAVA并发编程(上)
java·开发语言·学习
zzzll11112 小时前
HashMap、HashTable、ConcurrentHashMap 详细区别与深度解析
开发语言·python
老师我太想进步了20262 小时前
C语言版讲解函数递归
c语言·开发语言
猿长大人3 小时前
C# | 函数式编程入门
开发语言·c#·.net