
个人主页:小则又沐风
个人专栏:<数据结构>
<竞赛专栏>
<Linux>
座右铭
路虽远,行则将至;事虽难,做则必成
目录
前言
在之前的文章中,我们学习了C++库中的list接口的使用,今天我们来了解一下库中的list是怎么实现的,并且我们来模拟实现一下简单的list的接口
节点类
我们在数据结构中就知道,list的结构就像是一个一个的火车一样,所以我们在实现我们自己list的时候我们先把这个节点来封装成一个类
cs
template<class T>
struct listnode
{
listnode(const T& x=T())
{
_val = x;
_pre = nullptr;
_next = nullptr;
}
listnode<T>* _pre;
listnode<T>* _next;
T _val;
};
在这里我们使用的是类的模板,我们的这个节点的实现是依靠一个节点存储的数据,指向下一个节点和指向前一个结点的指针组成的
需要注意的是我们这个类是自定义的类型,所以编译器自带的默认构造函数是不可以满足我们的需求的,所以我们需要自己显示的实现构造函数,这个构造函数是非常的简单的,因为我们只需要造出一个节点不需要连接,所以我们指针都是一个个空指针
以上我们就把这个节点的类实现好了
现在我们来实现一下这个迭代器
迭代器类
我们在上一篇链表的文章中我们知道这个迭代器也是一个封装好的类,这是因为如果不把他封装好的话,我们想要像和其他的迭代器使用他的话,我们解引用节点得到的并不是节点的数据,而是这个节点,所以为了符合迭代器的使用的习惯,所以我们需要把迭代器进行封装,但是我们知道的是我们在之前的迭代器的实现的时候我们不仅仅实现了普通的迭代器,我们还实现了const的迭代器
所以在我们链表的迭代器的封装的过程难道我们需要把这个迭代器的实现写两份吗?并且我们知道的是这两串代码可是高度相似的
这时候我们就需要来进一步高级的运用我们的模板参数了
cpp
template<class T,class ref,class ptr >
在这里我的迭代器的模板参数并不是简简单单的一个,而是三个模板参数,现在我们详细介绍一下
这些模板参数有什么作用
我们知道我们在类实例化出对象的时候我们的模板类是需要传入模板的参数的,这时候我们就只需要const的传入的是const的参数,普通的传入普通的就行了,编译器会为我们自己实现出两份代码的
所以我们只需要把这个模板来封装好就好了
在我们的模板中这个ref指的是T&或者是const T&
ptr指的是T*或者是const T*
是不是恍然大悟
所以我们并不需要两段代码来实现迭代器了,只需要来用通用的模板参数就可以了
现在我们解决了代码重复率的问题,现在我们就正式来实现一下这个迭代器的类
我们知道的是这个所谓的迭代器就是一个节点的指针
cpp
typedef listnode<T> node;
node* _node;
listiterator(const listiterator& l)
:_node(l._node)
{ }
迭代器的构造函数我只写了最常用的一个
cpp
ref operator*()
{
return _node->_val;
}
ptr operator->()
{
return &_node->_val;
}
在这里我们就使用上了模板参数
cpp
self& operator++()
{
_node = _node->_next;
return *this;
}
self operator++(int)
{
self temp(*this);
_node = _node->_next;
return temp;
}
self& operator--()
{
_node = _node->_pre;
return *this;
}
self operator--(int)
{
self temp(*this);
_node = _node->_pre;
return temp;
}
bool operator!=(const self& l)
{
return !(_node == l._node);
}
bool operator==(const self& l)
{
return _node == l._node;
}
在这里有人问了这个self是何方神圣?
这是我们的迭代器本身,如果我们直接写的话我们的函数的返回值就太长了
cpp
typedef listiterator<T, ref, ptr> self;
以上就是我们的准备工作了下面就是进入我们真正的实现链表的环节了
链表类
cpp
typedef listnode <T> node;
typedef listiterator<T, T&, T*> iterator;
typedef listiterator<T, const T&, const T*> const_iterator;
实现的是双向循环的链表
先来展示一下链表含的成员变量
cs
private:
node* head;
size_t _size;
就是个这个第一个相当于我们的哨兵位节点,然后这个size是记录我们总共有多少个节点
构造函数
因为我们在之前我就已经封装好了我们的节点类,所以我们的构造函数的任务就是把节点连接并且构造出一个哨兵位节点
cpp
void empty_init()
{
head = new node;
head->_next = head;
head->_pre = head;
_size = 0;
}
list()
{
empty_init();
}
list(int n, const T& value = T())
{
empty_init();
while (n--)
{
push_back(value);
}
}
template <class Iterator>
list(Iterator first, Iterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
first++;
}
}
list(const list<T>& l)
{
empty_init();
for (auto e : l)
{
push_back(e);
}
}
list<T>& operator=(const list<T> l)
{
empty_init();
swap(l);
return *this;
}
需要解释的是如果我们的链表是一个空链表的话(只有一个哨兵位)设计的是head的next和preve都是指向自己的
在这里我们都是通过复用push_back实现的所以我们的构造函数实现的是轻而易举的
我们来仔细的剖析一下这个赋值构造
在这里我们函数的参数并不是一个引用的传参数,所以我们的实参会进行拷贝构造,所以进入函数体内的是一个全新的链表,这时候我们把它夺舍了就行了,直接swap一下
在链表的swap只需要交换一下头节点就好了,注意需要交换一下数据的个数
cpp
void swap(list<T>& l)
{
std::swap(head,l.head);
std::swap(_size,l._size);
}
析构函数
cpp
~list()
{
node* cur = head->_next;
while (cur != head)
{
node* next = cur->_next;
delete cur;
cur = next;
}
delete head;
}
我们析构函数的存在的意义就是防止我们的内存泄漏的,所以我们需要把我们所有开辟的空间进行释放,因为我们的每一个的节点都是我们自己new出来的所以我们需要遍历链表一个个delete
简单的容量的接口
cpp
size_t size()const
{
return _size;
}
bool empty()const
{
return _size == 0;
}
T& front()
{
return head->_next->_val;
}
const T& front()const
{
return head->_next->_val;
}
T& back()
{
return head->_pre->_val;
}
const T& back()const
{
return head->_pre->_val;
}
迭代器
cpp
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);
}
下面就是有难度的插入的接口了
数据的插入和删除
cpp
void push_back(const T& val)
{
insert(end(), val);
}
void pop_back()
{
erase(--end());
}
void push_front(const T& val)
{
insert(begin(), val);
}
void pop_front()
{
erase(begin());
}
在这几个接口的实现我们可以看到是高度的复用了erase和insert的函数
所以我们的重头戏就是来实现着两个函数
insert
cpp
// 在pos位置前插入值为val的节点
iterator insert(iterator pos, const T& val)
{
node* cur = pos._node;
node* pcur = cur->_pre;
node* newnode = new node(val);
pcur->_next = newnode;
cur->_pre = newnode;
newnode->_next=cur;
newnode->_pre = pcur;
_size++;
return iterator(newnode);
}
注意我们的函数的参数是一个迭代器加上一个数据
我们insert实现的在pos的之前来插入数据的

我们的目的就是在pos的前面插入一个节点

这样就搞定了
上面的代码就是这样的思路
注意需要返回插入节点的迭代器别忘了size++
erase
cpp
// 删除pos位置的节点,返回该节点的下一个位置
iterator erase(iterator pos)
{
node* cur = pos._node;
node* pcur = cur->_pre;
node* ncur = cur->_next;
pcur->_next = ncur;
ncur->_pre = pcur;
delete cur;
_size--;
return iterator(ncur);
}


修改完指针后
deletepos节点
然后调整一下数据的个数
并且返回下一个节点
clear
cpp
void clear()
{
auto cur = begin();
while (cur != end())
{
cur=erase(cur);
}
}
把链表的数据全删除并且释放节点
总结
本次内容围绕 C++ 手写链表展开,从节点类、迭代器类的基础实现,到链表类的封装(含构造 / 析构、容量接口),再到核心操作 insert、erase、clear 的实现逻辑,完整复刻了 STL 链表的核心框架。
在之后我们会来了解怎么来封装堆,栈,队列.
谢谢大家的观看!!!