list模拟实现
Posted DR5200
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了list模拟实现相关的知识,希望对你有一定的参考价值。
文章目录
一. list接口函数总览
namespace lyp
{
//模拟实现list当中的结点类
template<class T>
struct _list_node
{
//成员函数
_list_node(const T& val = T()); //构造函数
//成员变量
T _val; //数据域
_list_node<T>* _next; //后继指针
_list_node<T>* _prev; //前驱指针
};
//模拟实现list迭代器
template<class T, class Ref, class Ptr>
struct _list_iterator
{
typedef _list_node<T> node;
typedef _list_iterator<T, Ref, Ptr> self;
_list_iterator(node* pnode); //构造函数
//各种运算符重载函数
self operator++();
self operator++(int);
self operator--();
self operator--(int);
bool operator==(const self& s) const;
bool operator!=(const self& s) const;
Ref operator*();
Ptr operator->();
//成员变量
node* _pnode; //一个指向结点的指针
};
//模拟实现list
template<class T>
class list
{
public:
typedef _list_node<T> node;
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
//默认成员函数
list();
list(const list<T>& lt);
list<T>& operator=(const list<T>& lt);
~list();
//迭代器相关函数
iterator begin();
iterator end();
const_iterator begin() const;
const_iterator end() const;
//访问容器相关函数
T& front();
T& back();
const T& front() const;
const T& back() const;
//插入、删除函数
void insert(iterator pos, const T& x);
iterator erase(iterator pos);
void push_back(const T& x);
void pop_back();
void push_front(const T& x);
void pop_front();
//其他函数
size_t size() const;
void resize(size_t n, const T& val = T());
void clear();
bool empty() const;
void swap(list<T>& lt);
private:
node* _head; //指向链表头结点的指针
};
}
二.结点类的模拟实现
list 的底层结构为带头双向循环链表,所以结点类里的成员变量有 T _val(数据),_list_node< T >* _prev(前驱指针),_list_node< T >* _next(后继指针),成员函数只需要构造函数即可(初始化数据,初始化指针为nullptr)
template<class T>
struct _list_node
{
_list_node(const T& val = T())
:_val(val)
,_prev(nullptr)
,_next(nullptr)
{}
T _val; // 数据
_list_node<T>* _prev; // 前驱指针
_list_node<T>* _next; // 后继指针
}
三. 迭代器类模拟实现
前面的博客已经介绍过 vector 和 string 类的模拟实现,在 vector 和 string 中,迭代器均为原生指针,是因为vector和string底层实现均为数组,在使用迭代器进行遍历时可以支持 != 判断是否到末尾,++ 移动到下一数据,但list为带头双向循环链表,若迭代器采用原生指针,不支持 != 操作,且++后不能移动到下一数据(底层是链表,空间不连续),因此需要用一个类来封装指针,对上述操作的运算符进行重载,使list能够像vector/string一样使用同样的方式去进行遍历
迭代器的成员变量 : node* _pnode; //一个指向结点的指针
迭代器的成员函数 : 运算符的重载
可能有读者对模板参数比较疑惑,为什么会有三个模板参数呢?
template<class T,class Ref,class Ptr>
因为我们在list类中定义了两个迭代器类,普通迭代器类,const迭代器类(Ref为 T&/const T& 类型,Ptr为 T*/const T* 类型)
typedef _list_iterator<T,T&,T*> iterator;
typedef _list_iterator<T,const T&,const T*> const_iterator;
当我们使用普通迭代器对象时,实例化出普通迭代器类(iterator),使用const迭代器对象时,实例化出const迭代器类(const_iterator)
构造函数
对封装的指针进行初始化即可
_list_iterator(node* pnode)
:_pnode(pnode)
{}
前置++和后置++
前置++/后置++都是将指针指向下一个数据,即 _pnode = _pnode->_next,前置++返回++之后的值,后置++返回原先的值,为了区分前置++和后置++,后置++比前置++多一个int参数
self 为当前迭代器类型
typedef _list_iterator<T,Ref,Ptr> self
// 前置++
self& operator++()
{
_pnode = _pnode->_next;
return *this;
}
// 后置++
self operator++(int)
{
self tmp(*this);
_pnode = _pnode->_next;
return tmp;
}
前置- -和后置- -
前置- -/后置- -都是将指针指向前一个数据,即 _pnode = _pnode->_prev,前置- -返回- -之后的值,后置- -返回原先的值,为了区分前置- -和后置- -,后置- -比前置- -多一个int参数
self 为当前迭代器类型
// 前置- -
self& operator--()
{
_pnode = _pnode->_prev;
return *this;
}
// 后置- -
self& operator--(int)
{
self tmp(*this);
_pnode = _pnode->_prev;
return tmp;
}
==/!=运算符重载
==运算符重载
比较两个迭代器对象的_pnode指针指向是否相同
// ==运算符重载
bool operator==(const self& s)const
{
return _pnode == s._pnode;
}
!=运算符重载
比较两个迭代器对象的_pnode指针指向是否不同
// !=运算符重载
bool operator!=(const self& s)const
{
return _pnode != s._pnode;
}
* 运算符重载
重载 * 运算符的目的是为了得到迭代器对象的_pnode指针所指向的数据
// * 运算符重载
Ref operator*()
{
return _pnode->_val;
}
-> 运算符重载
重载 -> 运算符的目的是当list中的数据是自定义类型时,我们可以通过 -> 来访问自定义类型的成员变量
// -> 运算符重载
Ptr operator->()
{
return &_pnode->_val;
}
使用案例
class Date
{
public:
int _year = 0;
int _month = 1;
int _day = 1;
}
int main()
{
lt<Date> lt;
lt.push_back(Date());
list<Date>::iterator it = lt.begin();
cout<<it->_year<<" "<<it->_month<<" "<<it->_day<<endl;
}
在这里编译器做了一些优化
it->_year 本来应该是 it->->_year ,第一个->去调用operator->重载函数返回T*的指针,第二个->用来去访问自定义类型的成员变量,但是两个箭头程序的可读性较差,所以编译器做了优化,省略了一个箭头
list模拟实现
构造函数
list是一个带头双向循环链表,构造函数的目的是创建一个头结点,_next 和 _prev都指向自己
list()
{
_head = new node; // 申请一个头结点
_head->_next = _head; // 后继指针指向自己
_head->_prev = _head; // 前驱指针指向自己
}
拷贝构造函数
1). 申请一个头结点,_next 和 _prev都指向自己
2). 将 lt 中的数据拷贝到新构造的容器中
list(const list<T>& lt)
{
_head = new node; // 申请一个头结点
_head->_next = _head; // 后继指针指向自己
_head->_prev = _head; // 前驱指针指向自己
for(const auto& e : lt) // 拷贝到新构造的容器中
{
push_back(e);
}
}
赋值运算符重载
传统写法 :
1). 释放除了头结点以外的结点
2). 将 lt 中的数据拷贝到新构造的容器中
list<T>& operator=(const list<T>& lt)
{
// 防止自己给自己赋值
if(this != <)
{
clear(); // 清空数据
for(const auto& e : lt) // 拷贝到新构造的容器中
{
push_back(e);
}
}
return *this; // 支持连续赋值
}
现代写法 :
1). 拷贝构造出 lt 对象
2). 交换 this 和 lt 的 _head 指针,出了作用域,lt 调用析构函数,释放掉原this的结点
list<T>& operator=(list<T> lt) // 拷贝构造
{
std::swap(_head,lt._head); // 交换指针
return *this; // 支持连续赋值
}
析构函数
1). 使用 clear() 释放除了头结点以外的结点
2). 释放掉头结点
~list()
{
clear(); // 释放除了头结点以外的结点
delete _head; // 释放掉头结点
_head = nullptr; // 置空头指针
}
begin和end
begin() :
构造出指针指向第一个结点的迭代器对象
end() :
构造出指针指向头结点的迭代器对象
iterator begin()
{
return iterator(_head->_next);
}
const_iterator begin()const
{
return const_iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator end()const
{
return const_iterator(_head);
}
front/back
front() :
返回第一个结点数据的引用
end() :
返回最后一个结点数据的引用
T& front()
{
return *begin();
}
const T& front()const
{
return *begin();
}
T& end()
{
return *(--end());
}
const T& end()const
{
return *(--end());
}
insert
1). 新开辟一个结点newnode(值为val),得到当前结点的指针,前驱结点的指针
2). 前驱结点的_next 指向 newnode,newnode的_prev指向前驱结点
3). newnode的_next 指向当前结点,当前结点的_prev指向newnode
void insert(iterator pos,const T& val)
{
assert(pos._pnode); // 断言指针不为空
node* cur = pos._pnode; // 当前结点指针
node* prev = pos._pnode->_prev; // 前驱结点指针
node* newnode = new node(val); // 新开辟结点
prev->_next = newnode; // 前驱结点的_next 指向 newnode
newnode->_prev = prev; // newnode的_prev指向前驱结点
newnode->_next = cur; // newnode的_next 指向当前结点
cur->_prev = newnode; // 当前结点的_prev指向newnode
}
erase
1). 得到前驱结点和后继结点的指针
2). 前驱结点的_next 指向后继结点
3). 后继结点的_prev指向前驱结点
4). 删除当前结点,返回删除位置的下一个位置
iterator erase(iterator pos)
{
assert(!empty()); // 链表不为空
assert(pos._pnode != _head); // 不能删头结点
node* prev = pos._pnode->_prev; // 前驱结点指针
node* next = pos._pnode->_next; // 后继结点指针
prev->_next = next; // 前驱结点的_next 指向后继结点
next->_prev = prev; // 后继结点的_prev指向前驱结点
delete pos._pnode; // 删除当前结点
return iterator(next); // 返回删除位置的下一个位置
}
push_back/pop_back/push_front/pop_front
这四个函数都可以复用 insert 和 erase 函数
push_back :
尾插即在头结点前插入一个结点
pop_back :
尾删,删除最后一个结点
push_front :
头插即在第一个结点前插入一个结点
pop_front :
头删,删除第一个结点
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());
}
size
获取链表中有效结点的个数,遍历一次链表即可得到,但这是O(n)的时间复杂度,如果要频繁的使用size接口,可以在list类中加入_size成员变量
size_t size() const
{
size_t sz = 0; //统计有效数据个数
const_iterator it = begin(); //获取第一个有效数据的迭代器
while (it != end()) //通过遍历统计有效数据个数
{
sz++;
it++;
}
return sz; //返回有效数据个数
}
clear
只保留头结点,遍历链表调用erase接口进行删除,注意调用erase后 it 迭代器就已经失效了
void clear()
{
iterator it = begin();
while(it != end())
{
it = erase(it);
}
}
empty
判断容器是否为空
bool empty()const
{
return begin() == end();
}
swap
交换容器的头指针即可
void swap(list<T>& lt)
{
std::swap(_head,lt._head);
}
resize
1). 若n大于当前容器的size,则尾插结点,直到size等于n为止。
2). 若n小于当前容器的size,则只保留前n个结点。
void resize(size_t n, const T& val = T())
{
size_t len = size();
if (n < len)
{
size_t count = len - n;
while (count--)
{
pop_back();
}
}
// n > len
else
{
size_t count = n - len;
while (count--)
{
push_back(val);
}
}
}
以上是关于list模拟实现的主要内容,如果未能解决你的问题,请参考以下文章