【STL】list的模拟实现

 放在专栏【C++知识总结】,会持续更新,期待支持


1、list数据结构

list是一个带有头节点的双向链表,list主要是由以下部分组成:list节点类、迭代器类、list本身

1.1、list节点类

【STL】list的模拟实现_第1张图片

关于list节点类,由于list本身是一个双向的链表,所以节点内必须包含指向前一个节点的指针、指向后一个节点的指针、用来存储数据的data。同时我们只需给该类一个构造即可,因为对于节点的析构,我们交给list本身这个类来实现即可。如下所示,为list的节点设计:

【STL】list的模拟实现_第2张图片

1.2、迭代器类

list不能像vector那样以一个原生指针作为迭代器,这是因为list中各个节点并不是连续的,但是list中的迭代器必须要能够像“原生指针”那样,能够指向list的节点,同时能够正确的进行递增、递减、解引用等操作,因此,我们在迭代器类的设计中,必须要重载诸如++、*、->等运算符,使其++能够指向当前节点的下一个节点。同时由于list为双向的链表结构,因此我们还要对--进行重载,使其--指向当前节点的前一个节点。

如下所示,为迭代器类设计的基本结构:

【STL】list的模拟实现_第3张图片

(补充:这里==与!=重载忘记填参数了,在后面实现时才发现,这里前面就懒得改了) 

1.3、list本身

对于list本身来说,成员变量只需要一个头节点的指针,即可表示出整个双向链表,同时list还要提供插入、删除等相关函数接口,以及多个形式构造函数的实现、同时链表节点的释放也是在list析构函数中实现的,并且list还要提供迭代器相关的一些函数,如下所示,为其list基本结构:

【STL】list的模拟实现_第4张图片

接下来,我们对这些接口一一进行模拟实现。

2、模拟实现

上面的节点类我们只需要提供上述的一个构造函数即可,这里我们从迭代器开始,一步步模拟实现。

2.1、迭代器类的模拟实现

2.1.1、迭代器类的模板参数

template

首先,我们来解释为什么要存在三个模板参数,以及这三个模板参数所表示的意义。

这里我们来观察我们在list中typedef出普通迭代器iterator,以及const迭代器const_iterator,如下所示:

typedef _iterator _iterator;
typedef _iterator _const_iterator;

在这里,我们与迭代器中的模板参数一一对应,就可以发现,Ref表示的是引用,Ptr表示的是指针类型。

对于普通迭代器,我们在实例化模板参数时,传入T&和T*,我们的_iterator类就会实例化成普通迭代器,Ref这里表示的就是T&,Ptr表示的就是T*,而对于const迭代器来说,在实例化模板参数时,我们传入了const T& 以及const T*,这时_iterator就会实例化成const版本的迭代器,Ref这里表示的就是const T&,Ptr表示的就是const T*

【STL】list的模拟实现_第5张图片

这里我们就实现了:用一份代码,让编译器根据传参的不同,来实例化出普通迭代器或者const迭代器。如果不这样的话,我们就得自己手动再写一份const版本迭代器的相关代码。而const与普通迭代器的代码实现存在大量重复的代码,完全没必要手动再写一份,让编译器来实现即可。使代码更加“优雅”。这也是泛型编程思想的一种体现。

2.1.2、构造函数

迭代器类的构造函数很简单,我们只需要根据传来的参数进行构造即可,如下:

【STL】list的模拟实现_第6张图片

2.1.2、前置++与--

这里我们来实现前置++和--,我们知道,list的节点是不连续的,所以我们要保证++操作可以实现迭代器从当前节点指向下一个节点,--操作可以实现从当前节点指向前一个节点。那该如何实现呢?很简单,利用节点中的next与prev指针来实现:

【STL】list的模拟实现_第7张图片

 2.1.3 后置++与--

前置操作是本身先自增/自减,然后返回自增/自减后的结果,而后置操作则是,本身实现了自增/自减,但是返回的却是自增/自减之前的结果。如下所示:

【STL】list的模拟实现_第8张图片

2.1.4、*与->

我们对迭代器进行解引用,想要实现的是访问到该节点的数据,所以在对*进行重载时,返回节点指向的数据即可,而->返回其节点指向数据的地址即可。 

【STL】list的模拟实现_第9张图片

 2.1.5、==与!=

迭代器在进行比较时,实际上比较的是两个迭代器指向的位置是否为同一个位置,而不是比较迭代器的大小,这是错误的。而判断是否指向同一个位置,只需要判断其指向的pnode是否相同即可。

【STL】list的模拟实现_第10张图片

 至此,我们的迭代器算是正是完成了,接下来我们对list中的接口进行完善。

 2.2、list的模拟实现

在构造相关方面,由于这几个构造都会先创建出一个带有头节点的空链表,所以这里我们对其进行封装成一个函数,然后由不同的构造函数进行调用。如下:

【STL】list的模拟实现_第11张图片【STL】list的模拟实现_第12张图片

2.2.1、空构造

空构造的实现很简单,就直接调用上面的empty_init函数即可表示是个空链表。

【STL】list的模拟实现_第13张图片

 2.2.2、构造n个val

实际上就是尾插n次,每次插入val即可。如下,一个for循环搞定:

【STL】list的模拟实现_第14张图片

 2.2.3、拷贝构造

对于拷贝构造,也有两种写法,传统写法以及现代写法,传统写法实际也很简单,创建一个头节点,然后将待拷贝的list遍历一遍,将数据进行push_back即可:

【STL】list的模拟实现_第15张图片

 现代写法也很简单,构造一个临时变量,然后进行swap即可。如下:

【STL】list的模拟实现_第16张图片

 2.2.4、迭代器区间构造

【STL】list的模拟实现_第17张图片

2.3、赋值运算符重载

对于赋值运算符重载,也存在两种写法,现代写法以及传统写法,对于传统写法,我们的思路同样是遍历链表,然后实现尾插。如下:

【STL】list的模拟实现_第18张图片

 现代写法就很巧妙,与vector相同,我们采用传值传参(相比传引用,会多一次拷贝构造),然后进行swap即可。

【STL】list的模拟实现_第19张图片

 2.4、迭代器相关

2.4.1、begin与end

begin指向第一个有效节点,而end指向最后一个有效节点的下一个节点。

【STL】list的模拟实现_第20张图片

 当然,我们也要提供const版本,如下:

【STL】list的模拟实现_第21张图片

2.5、元素访问相关

2.5.1、front与back

front返回头部有效节点的数据,也就是head的下一个节点的数据,back返回尾部节点的数据,即head的前一个节点的数据。同样我们也要支持const版本:

【STL】list的模拟实现_第22张图片

2.6、插入删除操作

 2.6.1、insert与erase

insert实现在pos位置插入,list的插入效率非常高,达到了常数级别,我们只需要new一个节点来存储待插入的数据,然后修改pos前一个位置的next以及pos位置的prev指针,以及待插入节点的两个指针即可。如下;

【STL】list的模拟实现_第23张图片

【STL】list的模拟实现_第24张图片

 erase则实现删除pos位置的节点,在删除时要注意一点,就是不能删除头节点,因此我们在erase之前,要先对pos进行判断。如下:

【STL】list的模拟实现_第25张图片

【STL】list的模拟实现_第26张图片 另外,erase我们还提供了迭代器区间删除版本,实际上就是对erase(pos)的复用:【STL】list的模拟实现_第27张图片

 2.6.2、push_back与pop_back

push_back实现尾插,不过由于上面我们实现了insert,对任意位置可以进行插入,因此在这里我们只需要进行复用insert即可。如下所示:

【STL】list的模拟实现_第28张图片

 同理,对于pop_back尾删操作,我们也可以进行复用erase:

【STL】list的模拟实现_第29张图片

 2.6.3、push_front与pop_front

同理,我们对于头插头删继续复用insert与erase:

【STL】list的模拟实现_第30张图片

2.7、其它函数

2.7.1、swap

swap函数实现两个链表进行交换,我们直接使用std中提供的swap函数即可:

【STL】list的模拟实现_第31张图片2.7.2、size

size返回有效节点的个数,这里我们用指针相减法是实现不了的,因为list中的节点不是连续的,所以这里我们只能定义一个变量,然后遍历链表进行++。:

【STL】list的模拟实现_第32张图片

 2.7.3、resize

resize与vector中的模拟实现相同,有两种情况,“扩容”以及缩容。当resize(n,val)中的n>当前size时,进行扩容,扩容多出来的节点,用val填充其data。否则缩容。如下:

【STL】list的模拟实现_第33张图片

2.7.4、clear

clear用来清空容器,如下所示,我们借助begin与end遍历链表,复用erase即可:

【STL】list的模拟实现_第34张图片

 2.7.5、empty

empty用来判断容器是否为空,这里只有当begin与end相等时,容器才为空:

2.8、析构函数

我们上方已经实现了一个clear,用来清空容器有效元素,我们在析构中可以进行复用,最后我们再把head节点释放即可。

【STL】list的模拟实现_第35张图片

2.9、补充

这里我们后面在测试时发现,我们的迭代器区间构造,以及构造n个val,这两者之间发生了冲突:

【STL】list的模拟实现_第36张图片

 解决方法也很简单,有两种,第一种就是我们在传参时把10再转成size_t 类型,让它与其构造的模板参数类型保持一致,都是size_t,此时就不会调错函数了。第二种就是再提供一个int版本的构造。【STL】list的模拟实现_第37张图片

 此时便可以解决该问题。

至此,我们的list基本上算是模拟实现完毕,还剩一个反向迭代器,将放在后面章节进行讲解。这里我在记录博客时都是以图片的形式进行上传,主要是个人觉得如果放代码的话,显得很冗余,也不便观察,不如图片清晰明了。代码都放在个人gitee:点击跳转


end.

生活原本沉闷,但跑起来就会有风!

你可能感兴趣的:(C++知识总结,链表,数据结构,c++,stl)