【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】

【本节目标】

  • 1. list的介绍及使用

  • 2. list的深度剖析及模拟实现

  • 3. list与vector的对比

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第1张图片

1. list的介绍及使用

1.1 list的介绍

1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。

2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向 其前一个元素和后一个元素。

3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高 效。

4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率 更好。

5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list 的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间 开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这 可能是一个重要的因素)

1.2 list的使用

list中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展 的能力。以下为list中一些常见的重要接口。

1.2.1 list的构造

构造函数( (constructor)) 接口说明
list (size_type n, const value_type& val = value_type()) 构造的list中包含n个值为val的元素
list() 构造空的list
list (const list& x) 拷贝构造函数
list (InputIterator first, InputIterator last) 用[first, last)区间中的元素构造list
// list的构造
void TestList1()
{
	list l1;                         // 构造空的l1
	list l2(4, 100);                 // l2中放4个值为100的元素
	list l3(l2.begin(), l2.end());  // 用l2的[begin(), end())左闭右开的区间构造l3
	list l4(l3);                    // 用l3拷贝构造l4

	// 以数组为迭代器区间构造l5
	int array[] = { 16,2,77,29 };
	list l5(array, array + sizeof(array) / sizeof(int));

	// 列表格式初始化C++11
	list l6{ 1,2,3,4,5 };

	// 用迭代器方式打印l5中的元素
	list::iterator it = l5.begin();
	while (it != l5.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	// C++11范围for的方式遍历
	for (auto& e : l5)
		cout << e << " ";

	cout << endl;
}

1.2.2 list iterator的使用

此处,可暂时将迭代器理解成一个指针,该指针指向list中的某个节点。

函数声明 接口说明
begin + end 返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器
rbegin + rend 返回第一个元素的reverse_iterator,即end位置,返回最后一个元素下一个位置的 reverse_iterator,即begin位置

【注意】

  • 1. begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动
  • 2. rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动
// list迭代器的使用
// 注意:遍历链表只能用迭代器和范围for
void PrintList(const list& l)
{
    // 注意这里调用的是list的 begin() const,返回list的const_iterator对象
    for (list::const_iterator it = l.begin(); it != l.end(); ++it)
    {
        cout << *it << " ";
        // *it = 10; 编译不通过
    }

    cout << endl;
}

void TestList2()
{
	int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	list l(array, array + sizeof(array) / sizeof(array[0]));
	// 使用正向迭代器正向list中的元素
	// list::iterator it = l.begin();   // C++98中语法
	auto it = l.begin();                     // C++11之后推荐写法
	while (it != l.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	// 使用反向迭代器逆向打印list中的元素
	// list::reverse_iterator rit = l.rbegin();
	auto rit = l.rbegin();
	while (rit != l.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
}

1.2.3 list capacity

函数声明 接口说明
empty 检测list是否为空,是返回true,否则返回false
size 返回list中有效节点的个数

1.2.4 list element access

函数声明 接口说明
front 返回list的第一个节点中值的引用
back 返回list的最后一个节点中值的引用

1.2.5 list modifiers

函数声明 接口说明
push_front 在list首元素前插入值为val的元素
pop_front 删除list中第一个元素
push_back 在list尾部插入值为val的元素
pop_back 删除list中最后一个元素
insert 在list position 位置中插入值为val的元素
erase 删除list position位置的元素
swap 交换两个list中的元素
clear 清空list中的有效元素
// list插入和删除
// push_back/pop_back/push_front/pop_front
void TestList3()
{
	int array[] = { 1, 2, 3 };
	list L(array, array + sizeof(array) / sizeof(array[0]));

	// 在list的尾部插入4,头部插入0
	L.push_back(4);
	L.push_front(0);
	PrintList(L);

	// 删除list尾部节点和头部节点
	L.pop_back();
	L.pop_front();
	PrintList(L);
}

// insert /erase 
void TestList4()
{
	int array1[] = { 1, 2, 3 };
	list L(array1, array1 + sizeof(array1) / sizeof(array1[0]));

	// 获取链表中第二个节点
	auto pos = ++L.begin();
	cout << *pos << endl;

	// 在pos前插入值为4的元素
	L.insert(pos, 4);
	PrintList(L);

	// 在pos前插入5个值为5的元素
	L.insert(pos, 5, 5);
	PrintList(L);

	// 在pos前插入[v.begin(), v.end)区间中的元素
	vector v{ 7, 8, 9 };
	L.insert(pos, v.begin(), v.end());
	PrintList(L);

	// 删除pos位置上的元素
	L.erase(pos);
	PrintList(L);

	// 删除list中[begin, end)区间中的元素,即删除list中的所有元素
	L.erase(L.begin(), L.end());
	PrintList(L);
}

// resize/swap/clear
void TestList5()
{
	// 用数组来构造list
	int array1[] = { 1, 2, 3 };
	list l1(array1, array1 + sizeof(array1) / sizeof(array1[0]));
	PrintList(l1);

	// 交换l1和l2中的元素
	list l2;
	l1.swap(l2);
	PrintList(l1);
	PrintList(l2);

	// 将l2中的元素清空
	l2.clear();
	cout << l2.size() << endl;
}

1.2.6 list operations

函数声明 接口说明

splice

从一个列表转移元素到另一个列表

remove

移除具有特定值的元素

unique

移除重复的数值

merge

合并排序过的列表

sort

对容器中的元素进行排序

reverse

颠倒元素的顺序
int main()
{
	list lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);


	list::iterator it = lt.begin();
	//while (it < lt.begin())
	//这里不能使用 < ,因为不能保证前一个节点比后一个大节点
	//链表间没有大小关系
	// < 只适用于底层物理空间连续:string,vector
	while (it != lt.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	//逆置链表
	lt.reverse();
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	//归并链表
	//归并的前提:要求两个链表都有序
	list lt1;
	lt1.push_back(1);
	lt1.push_back(2);
	lt1.push_front(1);
	// lt和lt1链表排序
	lt.sort();
	lt1.sort();
	lt.merge(lt1);
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	//链表去重 - 要求 - 链表有序/重复元素相邻
	lt.unique();
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	//链表排序
	lt.sort();
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
	//这里提一个问题,能不能使用算法库的sort
	//这里为什么要单独提供一个链表的sort
	/*sort(lt.begin(), lt.end());
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;*/
	//error:_Sort_unchecked(_UFirst, _ULast, _ULast - _UFirst, _Pass_fn(_Pred));
	//算法库实现的sort是两个迭代器相减,list物理空间不连续,不支持相减

	//erase:删除pos位置的值
	//remove:根据值找pos位置+删除pos位置的值
	lt.remove(4);
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	//链表转移 - 节点转移
	list mylist1, mylist2;
	list::iterator it;

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

	for (int i = 1; i <= 3; ++i)
		mylist2.push_back(i * 10);// mylist2: 10 20 30

	it = mylist1.begin();
	++it;                         // points to 2

	mylist1.splice(it, mylist2); // mylist1: 1 10 20 30 2 3 4
							     // mylist2 (empty)

	//LRU,2最近被使用到 - 最少使用次数
	lt.splice(lt.end(), lt, find(lt.begin(), lt.end(), 2));
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

2. list的模拟实现

2.1 特点再次介绍

  • 1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代
  • 2. list的底层是带头双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
  • 3. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好
  • 4. 与其他序列式容器相比,list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第2张图片

强调一下head是哨兵结点,本身不存储数据,作用只是指向头和尾!

2.2 list接口实现

(1)结点类框架

template 
struct ListNode
{
public:
	//类模板
	//ListNode是一个基本类型或者自定义类型
	//ListNode就是int类型
	//ListNode就是string类型
	//实例化什么就是什么类型
	ListNode* _next;
	ListNode* _prev;
	T _data;

public:
    //记住不能这里写,构造函数的命名与类名相同
    //ListNode (const T& x)
	ListNode(const T& x)
	{
		_next = nullptr;
		_prev = nullptr;
        _data = x;
	}
};

这里我们可以解释一下,由于struct是类名,所以我们这里可以直接定义_next和_prev,不需要再加上struct,然后typedef了,同时这里也是一个类模板参数,当用户实例化类模板的时候,实例化什么类型该结点就是什么类型的结点。我们来看一下上面的结点有没有什么问题,还记得我们上面的提示吗?

强调一下head是哨兵结点,本身不存储数据,作用只是指向头和尾!

如果我们申请的是一个头节点,此时这个头结点就是一个哨兵位,本身不需要传入参数,但是我们上面的构造函数是传入了参数,所以我们需要修改我们的代码,这里可以写一个缺省值。

template 
struct ListNode
{
public:
	//类模板
	//ListNode是一个基本类型或者自定义类型
	//ListNode就是int类型
	//ListNode就是string类型
	//实例化什么就是什么类型
	ListNode* _next;
	ListNode* _prev;
	T _data;

public:
	ListNode(const T& x = 0)
	{
		_next = nullptr;
		_prev = nullptr;
		_data = x;
	}
};

我们这样写对吗?这就犯了一个错误,如果我们实例化类模板是LIstNode就没有问题,其他类型就会出错,所以我们这里还要修改。我们可以参考之前vector种resize参数的形式。

template 
struct ListNode
{
public:
	//类模板
	//ListNode是一个内置类型或者自定义类型
	//ListNode就是int类型
	//ListNode就是string类型
	//实例化什么就是什么类型
	ListNode* _next;
	ListNode* _prev;
	T _data;

public:
	ListNode(const T& x = T())
	{
		_next = nullptr;
		_prev = nullptr;
		_data = x;
	}
};

这里我们不传参的时候,匿名类它就会去调用默认的构造函数去初始化x,内置类型和自定义类型都会调用自己的默认构造去初始化x。

(2)链表类框架

(2.1)构造函数 + 尾插
template 
class list
{
	typedef ListNode Node;
public:
	list()
	{
		//带哨兵位的链表,第一个节点不存数据
		_head = new Node;//申请一个Node空间
		// new -> 开空间 + 调用构造函数
		_head->_next = _head;
		_head->_prev = _head;
			
	}

	void push_back(const T& x)
	{
		Node* newnode = new Node(x);//开空间 + 调用构造函数
		//先找到tail
		Node* tail = _head->_prev;

		tail->_next = newnode;
		newnode->_prev = tail;
		newnode->_next = _head;
		_head->_prev = newnode;
			
	}
private:
	Node* _head;
};

这里我们也稍微解释一下,使用new申请空间它首先会开辟我们想要的空间,然后再调用构造函数去初始化我们申请的空间,这也就是为什么上面的结点类我们需要写一个构造函数,然后我们来看一下我们的尾插函数,首先需要申请结点,然后再将申请的结点链接到我们的双向带头循环链表中。

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第3张图片

然后我们来测试一下,由于我们这里还没有实现迭代器,我们就写一个打印链表的接口。

void print()
{
	Node* cur = _head->_next;
	while (cur != _head)
	{
		printf("%d ", cur->_data);
		cur = cur->_next;
	}
}

然后来测试一下push_back的功能。

void test_list1()
{
	// 类模板实例化
	list lt;

	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);

	lt.print();
}

#include "list.h"

int main()
{
	yu::test_list1();
	return 0;
}

运行结果:

但是我们这里print存在限制,首先它的输出格式是写死的,其次它对数据只能读,不能写,所以我们这里就需要使用迭代器,我们在上面list使用的时候就可以++迭代器,但是链表底层的物理空间不连续,如何做到++呢?在这里我们可以重载++

强调一下Node*是指针类型,内置类型不能重载,所以我们要单独对Node*封装成一个类。

(3)迭代器类框架

template
struct __list_iterator
{
	typedef ListNode Node;
	typedef __list_iterator self;
		
public:
	Node* _node;

public:
	__list_iterator(Node* node)
	{
		_node = node;//一般传入的是begin()
	}

	self& operator++()//使用++it
	{
		_node = _node->_next;
		return *this;//这里的*this类型是__list_iterator
	}

	self operator++(int)//后置++
	{
		self tmp(*this);
		_node = _node->_next;
		return tmp;//返回局部对象,出了作用域就被释放,不能销毁
	}


	self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	self operator--(int)//后置--
	{
		self tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

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

	bool operator!=(const self& s)//使用it != end()
	{
		return _node != s._node;
	}

	bool operator==(const self& s)
	{
		return _node == s._node;
	}
};

首先我们需要将封装的迭代器需要实现的功能函数写好!

❓❓首先我们要问自己一个问题,链表的迭代器能不能像vector string 那样直接单纯地用解引用和++?

我们不妨回顾一下vector string 的迭代器,其底层是指针数组的原生指针!而它们容器的存储空间是连续的,解引用可以直接拿到数组数据,++可以直接加上类型的步长!

而list的呢?list是不连续的!它们通过前后指针链接!直接++不能找到后一个结点!解引用不是直接拿到存储数据,而是一个结点结构体!我们需要的是里面的data!

怎么办? 系统的【*】和【++】不能满足我们需要的结果!

必须重载运算符!所以我们得将迭代器封装成一个类!

现在再问自己一个问题! 他的类成员变量是谁? 联系一下我们的目的和链表容器结构!

我们想到一个指向node 的指针就可以解决了,++不就是next , --不就是prev , *不就是data!

这里单独将->符号拿出来说明一下! 想用->访问数据,->左边是什么? 是结构体地址! 

也就是说如果我们list 存储的数据是结构体或者类!里面有多个变量,那么我们访问数据就不能单纯的解引用!必须使用->来访问数据!

我们这里再介绍一个重点。

void func()
{
	//内置类型
	//yu::ListNode* it1 = _head->_next;
	Node* it1 = _head->_next;

	//对Node*进行封装成类 - 自定义类型
	//yu::__list_iterator it2 = _head->_next;
	iterator it2 = _head->_next;

	*it1;
	++it1;

	*it2;
	++it2;

	cout << sizeof(it1) << endl;
	cout << sizeof(it2) << endl;
}

我们封装Node*为一个单独的类,那么它和Node*一样吗?*it1;++it1;*it2;++it2;它们之间有区别吗?各自求sizeof大小是多少呢?

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第4张图片

虽然我们是用Node*为一个单独的类,但是就不再是基本类型了,而是自定义类型,对于Node*d的*it1;++it1;它此时就是一个原生指针,其解引用就是通过地址去找元素,++通过原生指针向后跳过一个Node*字节。而封装成的类Node*的*it2;++it2;此时就不再是原生指针,其解引用是调用操作符重载*去返回当前值的引用,++也是调用操作符重载++去指向当前指针的下一个结点。那它们底层物理上一样吗?

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第5张图片

但是它们的底层物理是一样的,都是Node*,都是4个字节,只不过一个是基本类型,一个是自定义类型。然后我么来看一下反向迭代器的特点。

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第6张图片

反向迭代器就是将list数据倒序遍历。

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第7张图片

根据上面的图,我们如果要实现反向迭代器的话,迭代的时候++rit就是让指针指向前面的位置。那我们就可以把之前写的普通迭代器改吧改吧,名字改成reverse_iterator,然后只需要将里面的操作符重载++让其指向的前一个位置。但是这种实现方式存在缺陷,这样会存在大量的代码重复,类似于普通迭代器和const迭代器单独实现的代码冗余,所以我们这里换一种写法,使用一下适配器的思路,传入正向迭代器我给你适配出反向迭代器,我们先来看一下库里面的实现方式。

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第8张图片

按照库里面的实现方法,指针的位置确实是向前走的,但是在解引用哪里,我们发现解引用是解引用的前一个位置,所以按照我们上面的逻辑是和库不一样,但是我们的也能实现,但是为了跟库保持一致,根据上面的reverse迭代器传参应该是我们下面的逻辑,end()位置是rbegin(),而begin()的位置是rend()位置

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第9张图片

直接看代码

//适配器
//给我list的iterator,适配出list的reverse_iterator
//给我vector的iterator,适配出vector的reverse_iterator

template
struct ReverseIterator
{
	typedef ReverseIterator Self;
public:
	Iterator cur;
public:
	ReverseIterator(Iterator it)
		:cur(it)
	{}

	Self& operator++()
	{
		--cur;
		return *this;
	}

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

    Ptr operator->()
    {
	    return &(this->operator*());
    }

	bool operator!=(const Self& s)
	{
		return cur != s.cur;
	}
};

typedef ReverseIterator reverse_iterator;
typedef ReverseIterator const_reverse_iterator;


reverse_iterator rbegin()
{
	return reverse_iterator(end());
}
reverse_iterator rend()
{
	return reverse_iterator(begin());
}

我们来测试一下:

int main()
{
	yu::list lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	yu::list::reverse_iterator rit = lt.rbegin();
	while (rit != lt.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
	return 0;
}

运行结果:

我们再来看一下vector容器的反向迭代器。

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第10张图片

typedef ReverseIterator reverse_iterator;
typedef ReverseIterator const_reverse_iterator;

reverse_iterator rbegin()
{
	return reverse_iterator(end());
}
reverse_iterator rend()
{
	return reverse_iterator(begin());
}

我们来测试一下:

int main()
{
	yu::vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	yu::vector::reverse_iterator rit = v.rbegin();
	while (rit != v.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
	return 0;
}

运行结果:

图解:

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第11张图片

(4)链表类框架后续

(4.1)迭代器+const迭代器
//再强调一下,迭代器被封装成类!!!
typedef __list_iterator iterator;//标准化
 
iterator begin()
{
	//匿名对象返回!这里调用了迭代器的构造函数
	return iterator(_head->_next);
}
 
iterator end()
{
    //这里千万记住,返回的是哨兵结点!不是尾结点!
	return _head;//单参数隐式类型转化 -> 构造
}


//测试:
void test_list2()
{
	// 类模板实例化
	list lt;

	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);

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

}

#include "list.h"

int main()
{
	yu::test_list2();
	return 0;
}

运行结果:

迭代器封装的价值

迭代器是一种可以遍历的抽象接口!

  • 1.封装底层实现,不暴露底层实现原理
  • 2.有了迭代器,可以不关心底层实现原理,而是注重使用!

我们上面用的*符号,看似简单几个字母,可底层却是返回data!我们不需要知道它返回的具体是什么,我们只要知道*符号代表着就是获取容器数据的方式!

类封装价值是什么?

  • 1.因为不同容器实现原理不同,vector,list同样是*符号,对于vector,*号是原生指针!解引用可以直接获取数据,但是对于list,解引用实现不了我们直接获取数据的方法!(前面说过)有了类封装,我们就可以重载*号,让它的意义改变!有了封装,让我们使得同样的符号,对于不同实现原理的容器,可以有同样的效果!
  • 2.我们可以注意到,即使是封装了迭代器,它的物理消耗内存仍只有一个指针大小!也就是说即使是类,他没有过多消耗内存!

根据之前的理论知识,普通的迭代器使用*it是可以修改的,但是有时候我们不期望进行修改,所以我们也要实现一下const版本的迭代器,那什么时候会使用const版本的迭代器,print_list的时候,我们希望传入的链表不要拷贝,所以带上引用,同时呢我们又期望打印的时候不要对元素进行修改,所以此时我们就要传入const版本的迭代器。

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第12张图片

这里我们能不能在普通的迭代器前面加一个const就变成const版本的迭代器呢?

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第13张图片

这里肯定是不能的,因为此时const修饰的是it,意味着此时的it不能改变,而我们的const迭代器是*it不能被修改,这两个指向的内容是不同的,我们的const迭代器是自己可以修改,但是指向的内容不能被修改。

template
struct __list_cosnt_iterator
{
	typedef ListNode Node;
	typedef __list_cosnt_iterator self;

public:
	Node* _node;

public:
	__list_cosnt_iterator(Node* node)
	{
		_node = node;//一般传入的是begin()
	}

	self& operator++()//使用++it
	{
		_node = _node->_next;
		return *this;//这里的*this类型是__list_cosnt_iterator
	}

	self operator++(int)//后置++
	{
		self tmp(*this);
		_node = _node->_next;
		return tmp;//返回局部对象,出了作用域就被释放,不能销毁
	}


	self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	self operator--(int)//后置--
	{
		self tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

	const T& operator*()//不能使用*it修改元素
	{
		return _node->_data;
	}

	bool operator!=(const self& s)//使用it != end()
	{
		return _node != s._node;
	}

	bool operator==(const self& s)
	{
		return _node == s._node;
	}
};



//再强调一下,迭代器被封装成类!!!
typedef __list_cosnt_iterator const_iterator;//标准化
 
const_iterator begin() const { return iterator(_head->_next); }//调用构造函数
const_iterator end() const { return _head; } //单参数隐式类型转化 -> 构造

运行结果:

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第14张图片

我们发现我们的代码只修改重载*的地方,代码其余的地方都没有变化,这样代码就有点冗余,所以我们这里可以使用模板解决,模板参数可以传任意类型。

template
struct __list_iterator
{
	typedef ListNode Node;
	typedef __list_iterator self;
		
public:
	Node* _node;

public:
	__list_iterator(Node* node)
	{
		_node = node;//一般传入的是begin()
	}

	self& operator++()//使用++it
	{
		_node = _node->_next;
		return *this;//这里的*this类型是__list_iterator
	}

	self operator++(int)//后置++
	{
		self tmp(*this);
		_node = _node->_next;
		return tmp;//返回局部对象,出了作用域就被释放,不能销毁
	}


	self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	self operator--(int)//后置--
	{
		self tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

	Ref operator*()//使用*it
	{
		return _node->_data;
	}

	bool operator!=(const self& s)//使用it != end()
	{
		return _node != s._node;
	}

	bool operator==(const self& s)
	{
		return _node == s._node;
	}
};


typedef __list_iterator iterator;//标准化
typedef __list_iterator const_iterator;//标准化

迭代器除了重载上面的operator*,同时还要重载operator->,我们先来看一下箭头会在结构体指针访问内部元素的场景下会用到。

void test_list3()
{
	struct AA 
	{
		int _a;
		int _b;
			
		AA(int a = 0, int b = 0)
		{
			_a = a;
			_b = b;
		}
	};
	list lt;
	//匿名对象
	lt.push_back(AA());
	lt.push_back(AA(1,1));
	//有名对象
	AA aa1;
	lt.push_back(aa1);
	AA aa2(1, 1);
	lt.push_back(aa2);

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

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第15张图片

此时我们想遍历就会出现问题,因为我们*it访问的就是AA,但是呢我们有没有实现AA的流插入的重载,所以我们这里就会出错,如果我们不想使用流插入的重载,我们这里就需要实现迭代器的->重载。

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第16张图片

由于->也可以修改数据,所以我们也要实现const版本的,所以上面的模板参数咱接着使用呗!

template
struct __list_iterator
{
	typedef ListNode Node;
	typedef __list_iterator self;
		
public:
	Node* _node;

public:
	__list_iterator(Node* node)
	{
		_node = node;//一般传入的是begin()
	}

	self& operator++()//使用++it
	{
		_node = _node->_next;
		return *this;//这里的*this类型是__list_iterator
	}

	self operator++(int)//后置++
	{
		self tmp(*this);
		_node = _node->_next;
		return tmp;//返回局部对象,出了作用域就被释放,不能销毁
	}


	self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	self operator--(int)//后置--
	{
		self tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

	Ref operator*()//使用*it
	{
		return _node->_data;
	}

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

	bool operator!=(const self& s)//使用it != end()
	{
		return _node != s._node;
	}

	bool operator==(const self& s)
	{
		return _node == s._node;
	}
};


typedef __list_iterator iterator;//标准化
typedef __list_iterator const_iterator;//标准化
(4.2)插入函数+删除函数
iterator insert(iterator pos, const T& x)
{
	Node* cur = pos._node;//访问__list_iterator类成员 - Node*
	Node* prev = cur->_prev;
	Node* newnode = new Node(x);

	//链接上面三个结点prev newnode cur
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;

	return newnode;
}

iterator erase(iterator pos)
{
	assert(pos != end());
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;

	//prev cur next //删除cur
	prev->_next = next;
	next->_prev = prev;
	delete cur;

	return next;
}

前面说过,此处大家可将迭代器暂时理解成类似于指针,迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。

void TestListIterator1()
{
	int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	list l(array, array + sizeof(array) / sizeof(array[0]));
	auto it = l.begin();
	while (it != l.end())
	{
		// erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值
		l.erase(it);
		++it;
	}
}
// 改正
void TestListIterator()
{
	int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	list l(array, array + sizeof(array) / sizeof(array[0]));
	auto it = l.begin();
	while (it != l.end())
	{
		l.erase(it++); // it = l.erase(it);
	}
}
(4.3)头插头删+尾插尾删
void push_back(const T& x)//尾插
{
	insert(end(),x);		
}

void push_front(const T& x)//头插
{
	insert(begin(),x);
}

void pop_back()//尾插
{
	erase(--end());
}

void pop_front()//头删
{
	erase(begin());
}

这里提一嘴,注意前后两者区别,前面不--end() 因为插入是插在结点前一个!end()返回哨兵结点,插在它的前一个就是尾结点!这里尾删,删的是尾结点,所以--迭代器重载返回前一个结点,也就是尾结点!

(4.4)析构函数+clear函数

※:析构函数是整个链表释放了,但是clear() 释放list非哨兵结点其余结点,链表依然存在!

void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);//!!!注意考虑迭代器失效
	}
}

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

这里再引入一个问题!为什么之前的迭代器类,结点类我们没有写析构函数?现在需要写呢?因为迭代器是获得结点的指针,如果写了析构函数,那么该节点被我们访问后就被释放了,下次就不能访问了,同时内置类型编译器会自动生成析构函数,只不过它什么都不做。

同时这里我们也可以不要写拷贝构造函数,这里不需要深拷贝,只需要浅拷贝即可,因为我们迭代器返回那个位置的指针,返回赋值给it,需要的就是浅拷贝!!!

如果list我们这里不写,默认析构函数只会将我们的头结点释放,那些剩余结点并没有被释放,也就是说list析构任务不仅仅是单个结点,而是所有结点!所以这里我们必须手动写! 

(4.5)  拷贝构造-现代写法

1.先构造临时链表tmp(深拷贝!)->> 2. 临时链表与拷贝构造链表交换!

那么如何交换两个链表? 很简单就是交换两个链表的哨兵结点

//list lt2(lt1)
void swap(list& lt)
{
	std::swap(_head, lt._head);//交换地址
}
//lt是lt1的别名
list(const list& lt)
{
	_head = new Node;//申请一个Node空间
	// new -> 开空间 + 调用构造函数
	_head->_next = _head;
	_head->_prev = _head;
	//注意我们必须初始化lt2的哨兵结点!这样tmp析构的时候析构的不是野指针!
	list tmp(lt.begin(), lt.end());
	swap(tmp);
}

或者我们也可以这样写

//list lt2(lt1)
list(const list& lt)
{
	_head = new Node;//申请一个Node空间
	// new -> 开空间 + 调用构造函数
	_head->_next = _head;
	_head->_prev = _head;
	//注意我们必须初始化lt2的哨兵结点!这样tmp析构的时候析构的不是野指针!
			
	for (const auto& e : lt)
	{
		push_back(e);
	}
}
(4.6)赋值拷贝-现代写法
//lt1 = lt2
//*this = lt
list& operator=(const list& lt) //传统写法
{
	if (this != <)
	{
		clear();//清除lt1数据
		for (const auto& e : lt)
		{
			push_back(e);
		}
	}
    return *this;
}

1.构造lt--> 2.交换--> 3.出作用域,临时变量调用析构函数,释放this原空间

//连等可能,所以返回链表!
//出作用域,被替换的lt会调用它的析构函数,析构this的原空间

void swap(list& lt)
{
	std::swap(_head, lt._head);//交换地址
}

//lt1 = lt2
//*this = lt
list& operator=(const list lt) //现代写法
{
	swap(lt);
	return *this;
}

3. list与vector的对比

vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:

vector list
底 层 结 构 动态顺序表,一段连续空间 带头结点的双向循环链表
随 机 访 问 支持随机访问,访问某个元素效率O(1) 不支持随机访问,访问某个元素 效率O(N)
插 入 和 删 除 任意位置插入和删除效率低,需要搬移元素,时间复杂 度为O(N),插入时有可能需要增容,增容:开辟新空 间,拷贝元素,释放旧空间,导致效率更低 任意位置插入和删除效率高,不 需要搬移元素,时间复杂度为 O(1)
空 间 利 用 率 底层为连续空间,不容易造成内存碎片,空间利用率 高,缓存利用率高 底层节点动态开辟,小节点容易 造成内存碎片,空间利用率低, 缓存利用率低
迭 代 器 原生态指针 对原生态指针(节点指针)进行封装
迭 代 器 失 效 在插入元素时,要给所有的迭代器重新赋值,因为插入 元素有可能会导致重新扩容,致使原来迭代器失效,删 除时,当前迭代器需要重新赋值否则会失效 插入元素不会导致迭代器失效, 删除元素时,只会导致当前迭代 器失效,其他迭代器不受影响
使 用 场 景 需要高效存储,支持随机访问,不关心插入删除效率 大量插入和删除操作,不关心随机访问

我们再来看一下list和vector在排序方面的效率,首先我们要知道算法库中的sort(快排)是不能给链表排序使用的,链表之间的地址是不确定的,而算法库中的排序要求两个迭代器相减,此时就会出现问题,所以我们这里使用链表单独的排序方法(归并)。

#include 
#include 
#include 
#include 
using namespace std;

int main()
{
	srand(time(0));
	const int N = 1000000;

	list lt;

	vector v;

	for (int i = 0; i < N; ++i)
	{
		auto e = rand();//产生一百万个随机数
		lt.push_back(e);//保存到list中
		v.push_back(e);//保存到vector中
	}

	int begin1 = clock();
	sort(v.begin(), v.end());
	int end1 = clock();

	int begin2 = clock();
	lt.sort();
	int end2 = clock();

	printf("vector sort:%d\n", end1 - begin1);
	printf("list sort:%d\n", end2 - begin2);

	return 0;
}

运行结果:

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第17张图片

现在我们再来看一下release版本下的运行结果

此时相差就非常大了,一般我们都知道一款软件发布的形式是release版本,所以此时谁效率更高就不言而说了。此时我们再来改一下代码,将list的数据拷贝到vector,利用vector的排序之后再拷贝回list,看看和list单独排序的效率谁高谁低。

void test_op2()
{
	srand(time(0));
	const int N = 1000000;

	list lt1;
	list lt2;

	for (int i = 0; i < N; ++i)
	{
		auto e = rand();
		lt1.push_back(e);
		lt2.push_back(e);
	}

	int begin1 = clock();
	vector v(lt2.begin(), lt2.end());
	sort(v.begin(), v.end());
	lt2.assign(v.begin(), v.end());
	int end1 = clock();

	int begin2 = clock();
	lt1.sort();
	int end2 = clock();

	printf("list copy vector sort copy list sort:%d\n", end1 - begin1);
	printf("list sort:%d\n", end2 - begin2);
}

运行结果:

我们发现此时的效率居然比list排序的效率还高,所以list排序的意义不大,如果要使用容器的时候排序,建议使用vector。

【C++ STL链表:节点串联,数据无限,解锁高效插入、删除与迭代的奇妙能力】_第18张图片

你可能感兴趣的:(C++初阶,c++,list)