0
点赞
收藏
分享

微信扫一扫

Docker 安装 Redis 单机&集群总结

目录

1.观察库中的链表的实现          

2.list的实现

2.1 基本结构   

2.2 基本接口

3.迭代器(本篇重点)

3.1 迭代器的构建

3.2 联系list

3.3 ListIterator的完善

3.3.1 不需要写析构函数

3.3.2 拷贝构造和赋值运算符

3.3.3  operator-> (双箭头变单箭头)

4.const迭代器

5. list中的insert和erase


1.观察库中的链表的实现          

首先一上来就是一个结构体模版 , 此处库中将void_pointer(也就是指向自己的指针)设计成一个void* , 所以在之后的使用中必定涉及到强转来使用,我们更建议直接设计成T*

                

然后是一个超长的和iterator有关的结构体模版的定义:

                                        (此处没有截图完整,在压缩包中48~94行)

由此我们可以推测,双向迭代器是一个类封装的,不再是vector中简单的原生指针。

 最后来到链表,通过观察我们可发现,主要成员变量就是一个node:

                                                        

     而node的类型是一个节点的指针                       

初始化:无参构造就是实现一个哨兵位,哨兵位的next和prev都指向自己

                

get_node:通过内存池(为了效率)或得空间

其它插入方法:

push_back   以及   insert

都是将原来的元素往后顺移一个,将希望放入的元素放在position的位置

我们还能观察出,position被当作一个指针一样通过.去访问对应的元素。


2.list的实现

2.1 基本结构   

节点:

由头尾指针,一个数据构成。                     

链表:

             

2.2 基本接口

list() {
	_head = new Node();
	_head->_next = _head;//_head是Node类型的变量,我们此处访问他的成员变量
	_head->_prev = _head;//如果Node(也就是ListNode<T>)是一个class
}                        //成员变量默认就会变成私有,会被拒绝访问 

复习如何push_back:

先找到当前的tail , 记录此节点的prev , 然后改变tail的next的指针指向,指向新加入的元素。

void push_back(const T& x) {
	Node* tmp = new Node(x);
	Node* pretail = _head->_prev;

	pretail->_next = tmp;
	tmp->_next = _head;
	tmp->_prev = pretail;
	_head->_prev = tmp;

为什么struct而不是class:

                  

1. 模版不会被编译。只有在实例化以后才会编译模版类

2. ListNode没有构造函数

注意:new Node()和new Node是不一样的,后者不会调用构造函数,只是开空间 

3.访问权限问题

解决如下:             

template<typename T>
struct ListNode {
	ListNode<T>* _next;
	ListNode<T>* _prev;
    T data;

	ListNode(const T& x = T())
		:_next(nullptr),//一个新创建的节点的next和prev不需要指向任何地方,赋给nullptr即可
		_prev(nullptr),
		_data(x)
	{}
};

或者class加上修饰限定符public也可以:

                   


关于哨兵位的初始化:不能用0,因为不确定T到底是int还是string或者vector等。。。。

​​​​​​​​​​​​

建议使用匿名构造:

                       

或者全缺省( 反正我们在ListNode<T>里面是加了参数T()的 )

                         


3.迭代器(本篇重点)

3.1 迭代器的构建

之前都是使用的原生指针T*作为iterator

因为其物理空间都是连续的,所以可以直接用指针。

而我们刚才由源码中观察得到,链表的指针是封装过的自定义类型。

实现iterator:

先实现框架:

template<typename T>
class ListIterator {
    
	typedef ListNode<T> Node;
	typedef ListIterator<T> _self;//这里是模仿库中的写法,将自己的名字缩短一点
    Node* _pnode;
public:
	ListIterator(Node* pnode) //之后在list中就可以通过iterator it(li1.begin())等构造
		:_pnode(pnode)// iterator it = li1.begin()也可以,这是隐式类型转换
	{}

	operator++
	operator--	
};

再来实现++(要在参数处加int才是后置加加):

_self& operator++() {
	_pnode = _pnode->_next;
	return *this;
}

小练习:

_self operator++() {
	return _self(_pnode->_next);//测试一下能不能跑,并阐述理由
}

同理实现--和解引用:

_self& operator--() {
	_pnode = _pnode->_prev;
	return *this;
}
T& operator*() {
	return _pnode->_data;
}

 使用迭代器访问各种元素时,经常用到!=判断是否走到了末尾:

bool operator!=(const _self& it) {
	return this->_pnode != it._pnode;
}

除了前置++--,还有后置的:

                            


3.2 联系list

再回到list中:                           

iterator begin() {
	return iterator(_head->_next);
}
iterator end() {
	return iterator(_head);
}

 同时,在调用测试时:

因此还需要把 ListIterator中的几个函数变为公有。

(!=作为重载的运算符,因为需要访问it的_node所以也是不能访问的)


并且,由于for循环就是自动去找iteraotr和begin()与end()三个名字,我们也能使用循环for

我们不能直接改变内置类型的行为,但是可以通过封装的方式来将该内置类型封装成想要的样子。

理解清楚这张图:

this是一个指向self(也就是ListIterator<T>)的指针 ,return *this就相当于把这个iterator传回去了


3.3 ListIterator的完善

3.3.1 不需要写析构函数

3.3.2 拷贝构造和赋值运算符

3.3.3  operator-> (双箭头变单箭头)

先实现一个箭头访问: 

                             

                      (注意,_node的类型是Node*   ,  箭头优先级也是高于取地址的)

假设我们的T是一个控制坐标的自定义类型Pos

                     ​​​​​​​

我们再对自己的链表进行测试:

it解引用之后是一个Pos,但是Pos没法直接被留提取<<给输出,因此报错。

我们当然可以“投降”,一个一个的访问元素:

但是我们更应该直接通过iterator去访问pos的成员变量:

(日常的迭代器用operator->的会不多,之后在学习图(map)的时候会比较多)

特殊现象:

关于为什么begin()的返回值可以++,也就是

++li1.begin();

                   

        

        


4.const迭代器

              


不能直接在list<int> :: iterator 前面加const  , 

const list<int>::iterator it2 = li1.begin();//错误样例

因此我们可以实现两个类,一个是刚刚的ListIterator ,另一个是我们再实现一个ListConstIterator

改一改名字,修改一下两个核心模块的函数返回值即可。 

        

template<typename T>
class ListConstIterator {

	typedef ListNode<T> Node;//与class list中的命名保持一致性
	typedef ListConstIterator<T> _self;
	Node* _pnode;
public:
	ListConstIterator(Node* pnode)
		:_pnode(pnode)
	{}

	//_self operator++() {
	//	return _self(_pnode->_next);//测试一下能不能跑
 //   }
	_self& operator++() {
		_pnode = _pnode->_next;
		return *this;
	}
	_self operator++(int) {
		_pnode = _pnode->_next;
		return _self(_pnode->_prev);
	}
	_self& operator--() {
		_pnode = _pnode->_prev;
		return *this;
	}
	_self operator--(int) {
		_pnode = _pnode->_prev;
		return _self(_pnode->_next);
	}
	const T& operator*() {
		return _pnode->_data;
	}
	const T* operator->() {
		return &_pnode->_data;
	}
	bool operator!=(const _self& it) {
		return this->_pnode != it._pnode;
	}

};

这样我们就能达到目的了。

别忘了再去写const对应的begin和end

            

注意此处的lt也必须是const list<int> lt ;

     


读者朋友们是否会觉得这样的实现有点过于冗余呢?

因为ListIterator<T>和ListConstIterator<T>两者非常相像,只有少数接口不一样

能否不定义一个新的类呢:

     

class ListIterator是满足了要求,但是class list中依然实例化的是T,   若调用:

       ​​​​​​​

那么初始化const_iterator的就是一个T而非const T,会报错。

解决方案:

直接将引用和指针传入,一个不使用const , 一个使用const修饰

iterator:

对照着来看: 


5. list中的insert和erase

首先看insert :

 按照合理的顺序更改各个指针的指向即可

再看返回值:

指向用户插入的第一个元素:

iterator insert(iterator pos, const T& x) {
	Node* tmp = new Node(x);

	tmp->_next = pos._pnode;
	tmp->_prev = pos._pnode->_prev;
	pos._pnode->_prev->_next = tmp;
	pos._pnode->_prev = tmp;

	return iterator(tmp);
}

(返回临时变量时就不要传引用返回了,返回一个tmp的拷贝更加合适)

           


类似于vector ,  erase返回的也是被删除的下一个位置的迭代器

同时还要注意断言,不要把哨兵位给删掉了

iterator erase(iterator pos) {
	assert(pos != end());//别把哨兵位给删了
	Node* cur = pos._pnode;
	Node* prev = pos._pnode->_prev;
	Node* next = pos._pnode->_next;

	prev->_next = next;
	next->_prev = prev;
	delete cur;
	return iterator(next);
}

举报

相关推荐

0 条评论