list的访问与模拟实现

 

目录

<list>................................................................................................................................. 1

底层.......................................................................................................................... 1

使用.......................................................................................................................... 2

1.构造和访问...................................................................................................... 2

2.库函数使用...................................................................................................... 3

3.成员函数使用.................................................................................................. 3

综合应用代码..................................................................................................... 5

模拟实现................................................................................................................... 6

前言:................................................................................................................ 7

坑点一:迭代器只读的设计................................................................................ 7

坑点二:编译器带来的自定义类型->的特殊规则................................................. 8

坑点三:显示定义迭代器(这个是使用上的).................................................... 9

 

<list>

底层

如下方C++官方规定,list容器的底层是双向循环带头链表

image001

也正因为底层的特性,在实现成员函数的操作时,也和vector/string有所区别。

image002

多了时间复杂度低的pop_frontpush_front,删去了一次性开辟多空间的reserve(底层设计决定无法记录未存值的结点),同时resize函数功能不变,但是分类移动到了数据操作的modifier那一栏。新增了sort/merge/unique/remove/splice一系列针对链表的操作

 

使用

1.构造和访问

由于底层变为了链表,无法随机访问,历遍访问方法仅有iterator和范围for两种了。

同时element access支持了frontback,返回结点内容的引用

eg:

//库内链表的使用
void test1()
{
	//构造函数
	//默认
	list<int> a;
	for (auto e : a)
	{
		cout << e << " ";
	}
	cout << endl;

	//n个值构造
	list<int> b(3,20);
	list<int>::iterator i = b.begin();
	while (i!=b.end())
	{
		cout << *i << ' ';
		i++;
	}
	cout << endl;

	//迭代器区间构造
	vector<char> s = { 'a','b','c' };
	list<int> c(s.begin(),s.end());
	for (auto e : c)
	{
		cout << e << " ";
	}
	cout << endl;

	//initializer_List构造
	list<int> d{1,2,3,4};
	for (auto e : d)
	{
		cout << e << " ";
	}
	cout << endl;

2.库函数使用

库函数(algorithm)使用主要就是注意迭代器种类要求,下面我们就举一个例子

 

//	库函数使用
int n[4] = {2,1,3,4};
vector<int>v1{ 2,1,3,4 };
sort(n, n+4);
sort(v1.begin(), v1.end());
// list不能用,要求是random迭代器
//sort(c.begin(), c.end());
reverse(c.begin(), c.end());
reverse(v1.begin(), v1.end());

 

3.成员函数使用

这里我们主要介绍list独有的(相对于vector/string)的成员函数

 

sort

从上面的使用可以知道,list的迭代器不符合库函数的要求,因此库重新实现了一个成员函数sort来实现排序,不过这个排序效率极低,谨慎使用

 

在介绍缘由前,我们先看一段代码与运行结果

这个是百万数据的排序结果,debug版本,可以看到list库自带的排序算法时间大概是接近vector排序的两倍,甚至我用On)进行拷贝到vector来排序再拷贝回list的时间都比list库自带排序时间减少了1/4

image003

再看release下(函数栈帧优化全开时)

image004

差距更为明显了,差距到了2倍以上

为什么会有这么大的差异?

回到底层算法,list库实现sort使用的是归并排序,algorithm库实现sort使用的是快排,同为O(logn)的算法,二者在理论上的差异应该较少

 

跟本缘由是缓存命中率不同,即物理层存储带来的读取速度不同

CPU缓存读取数据时会读一段长度的连续存放的数据存入缓存,这时候,如果数组,那么后续要访问的数据也进缓存了,CPU查找数据时先看缓存是否有,有(命中)就用,没有(未命中)就让缓存去再次读取(耗时长)。(详见数据结构与算法)

image005

而链表的物理存储结构确定了无法命中,应为后续连续空间储存的不是他的结点,每次都要去内存读取到缓存再返回,使得时间消耗极大。

 

那为什么用release后速度差距还会变大?

release版本不需要在函数栈帧中插入各种调试信息,而快排本质依赖函数递归,这样函数栈帧的建立速度变快了,那么时间消耗就低了。

void merge(list& x)

该函数会合并两个有序数组(必须有序,可以先sort一下),后一个list(也就是x)将变为空list

 

splice(list& x)

image006

这个函数是选定链表(x)(一个区间),然后将这个链表(区间)截出放置到pos位置,然后再链接链表尾端与原链表的pos位置的结点。

 

综合应用代码

 

//成员函数使用
	std::list<double> first, second;

	first.push_back(3.1);
	first.push_back(2.2);
	first.push_back(2.9);

	second.push_back(3.7);
	second.push_back(7.1);
	second.push_back(1.4);

	first.sort();
	second.sort();

	first.merge(second);

	std::list<double> lt1 = { 1,2,3,4,5,6,2,3,2 };
	lt1.remove(2);
	for (auto e : lt1)
	{
		cout << e << " ";
	}
	cout << endl;

	// 
	std::list<double> lt2 = { 1,2,3,4,5,6 };
	for (auto e : lt2)
	{
		cout << e << " ";
	}
	cout << endl;
	// 把5转移到头部位置
	auto it = find(lt2.begin(), lt2.end(), 5);
	lt2.splice(lt2.begin(), lt2, it);
	for (auto e : lt2)
	{
		cout << e << " ";
	}
	cout << endl;

 

 

模拟实现

代码链接:等待更新

前言:

在下面的叙述中,我们挑相对vectorstring不同的地方,有突破的地方进行讲解,重点在于增删查改,对于专门为list设计的功能不进行模拟实现,算作算法部分。(想要了解模拟实现stringvector基础的可以看作者前面的文章)

 

同样为stl部分的设计,增删查改其实思想都一样,设计思想是一样的,但是由于底层结构的不同(为双向循环链表),想要达到这种设计还是有所区别(重点)。

 

在模拟实现时,作者定义了Node为结点类型,iterator/const_iterator为迭代器类型,list为主类型

 

坑点一:迭代器只读的设计

 

我们知道作为list的迭代器,其应该是bidirection类型的迭代器,需要支持*/->/++/--,由于底层不是数组,仅仅靠原生指针以及其原生操作很难实现了,因此我们需要定义一个类来实现封装。

 

显然我们需要保留储存类型的模板(T),同时基于指针实现,我们使用struct类,因为iterator的设计上无法让我们得到指向结点类的指针,我们如果把iterator里面的指针设为私有,在主类里面用iterator实现增删时,会使得无法进行节点间链接关系的改变。

template<typename T>

struct _list_iterator {

Node* _ptr;

}

 

这就够了吗?我们回到iterator的设计思想上,其是支持const的,这里的const也是一个设计思想,其是支持iterator进行移动,但是不支持iterator指向的内容进行修改。

如果我们直接用const _list_iterator,我们无法对内嵌的Node* _ptr;进行修改了,那怎么实现++/--,显然是走不通的。

 

我们显然不能直接限制指针,而是需要在*的重载返回值上进行限定,要实现const与非const,显然第一考虑就是重载,但是参数相同(设计规定*为单操作数的操作符,都只要this指针),无法重载。显然还是不可以。

 

那怎么办?很直接粗暴的想法就是再定义一个const_iterator类,这样,在不同的类中返回值分别设定为Tconst T

这个路,能走,但是代码重复度太高,不够优雅,省事,维护困难。

eg:

template<typename T>
struct _list_iterator {
    Node* _ptr;
    T& operator*();
}

template<typename T>
struct _list_const_iterator {
    Node* _ptr;
    const T& operator*();
}

 

仅仅是类型不同,代码一样,那不就是该模板救场了嘛,我们不妨多定义一个模板参数,抽象为const与非const的返回值类型,这样,更具传入的参数自动推演实现,维护方便。出现类似问题的还有->操作符返回的指针,一样可以用此方法解决。

eg:

template<typename T,typename Ref, typename Ptr>

struct _list_iterator {
    Node* _ptr;
    Ref operator*();
    Ptr operator->();

}

 

坑点二:编译器带来的自定义类型->的特殊规则

 

->运算符设计的思想是通过一个指针拿到其中的值。而编译器会进行的优化是:

C++规定:当编译器看到对象->成员时,会按以下逻辑处理:

 

如果对象是原生指针(如 T*),直接访问成员。

如果对象是类类型,则调用其operator->()

如果返回值是原生指针,则通过该指针访问成员。

如果返回值仍然是类类型,则 继续对该返回值调用 operator->(),直到最终得到原生指针为止。

这个过程由编译器自动、递归地完成,直到达到终止条件(原生指针)。。

 

eg:

//假设结点存储的内容是一个结构体(两个成员变量_row_col

//为了可读性,这里省略了一个->

cout << it->_row << ":" << it->_col << endl;

//也可以显式写为cout << it.operator->()->_row << ":" << it.operator->()->_col << endl;

 

坑点三:显示定义迭代器(这个是使用上的)

 

//编译器看到这里时,会产生二义性,dr::list<int>::iterator

//这里list<int>并没有实例化,跟了一个::,其作用就变成了声明,不实例化,不知道里面有什么,我们知道类中全局变量也可以这么访问,自定义类型名也可以这么指定

//这时候typenameclass就多了用武之处,在前面写上typenameclass 不行),声明我确定这里这个是类型不是变量,告诉编译器就这么编译。

typename dr::list<int>::iterator m = ls1.begin();

 

还需要注意的是,typedef还受访问限定符的限制,如果你的iterator在类中是private的,即使能够找到,也无法使用。

 

模拟实现代码链接:待更新

持续优化ing…………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………

 

posted @ 2026-08-07 17:47  a_启航  阅读(6)  评论(0)    收藏  举报