目录
2.3.2 push_back pop_back push_front pop_front
前言
这篇文章讲述常用容器list的使用和一些重要部分的简单模拟实现,仅仅只是了解一些实现方法。内容丰富,干货多多。
1. list的简介
- list是序列容器,允许在序列内的任何位置进行常量时间的插入和删除操作,以及两个方向的迭代。
- 列表容器被实现为双链表;双链表中每个元素存储在互不相关的几点钟,在节点中通过指针指向其前一个元素和后一个元素。
- 它们与forward_list非常相似:主要区别在于forward_list对象是单链表,因此它们只能向前迭代,让其更简单高效。
- 与其他基本标准序列容器(array、vector和deque)相比,列表在容器内的任何位置插入、提取和移动元素方面通常表现更好。
- 与其他序列容器相比,列表和forward_lists的主要缺点是它们无法通过位置直接访问元素;例如,要访问列表中的第八个元素,必须从已知位置(如开始或结束)迭代到该位置,这需要在两者之间的距离上花费线性时间。它们还消耗一些额外的内存来保存与每个元素相关联的链接信息(对于包含小元素的大型列表来说,这可能是一个重要因素)。
- 下面是展示list类的内部成员变量结构,并且之后要简单模拟实现,所以加上一个命名空间,防止与库中list起冲突。
- list即是双向链表结构,我们使用模版定义一个ListNode类,内部成员变量有两个指针,指向前一个元素和后一个元素,还有存储T类型的元素。在list类中,我们可以使用typedef简化LIstNode<T>成Node。Node类型的指针变量_head,代表第一个哨兵位结点,不存放元素。
- 其中ListNode使用struct而不用class,因为struct默认权限是公有的,且ListNode类里面的所有内容list都会使用到,所以直接用struct定义这个类。
namespace User
{
template<class T>
struct ListNode
{
ListNode*<T> _next;
ListNode*<T> _prev;
T _data;
//...
};
template<class T>
class list
{
typedef ListNode<T> Node;
//...
private:
Node* _head;
};
}
2.list讲解和模拟实现
2.1 默认构造函数和push_back函数
默认构造函数构造一个只含有哨兵位结点的链表,并且哨兵位结点的的两个指针指向自身。我们暂时使用库里的list,先创建一个int类型的list容器,调用的是默认构造函数,之后尾插几个整数。遍历整个链表一般使用迭代器,还可以使用范围for。因为物理结构是不连续的,不支持使用下标方括号遍历链表。
list();//默认构造函数
void test_list1()
{
std::list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
list<int>::iterator it1 = lt1.begin();
while (it1 != lt1.end())
{
cout << *it1 << " ";
++it1;
}
cout << endl;
for (const auto& e : lt1)
{
cout << e << " ";
}
cout << endl;
}
运行结果如下:
- 实现一个list的默认构造函数,使用new动态开辟一个Node类型元素的内存空间,传入一个T类型的值调用LIstNode类的构造函数。下面写的是T()表示如果是自定义类型,就调用该自定义类型的构造函数,如果是内置类型,会初始化一个该类型的值,整型一般初始化为0。
- 既然list构造函数中调用了LIstNode类的构造函数,LIstNode的构造函数也需要实现。构造函数的参数是const修饰的T类型引用的data变量,顺便给上一个缺省值T(),跟上面的用法相同。
- push_back函数先动态开辟一个新结点,传入x初始化。然后再改变结点指向的位置即可。
template<class T>
struct ListNode
{
ListNode*<T> _next;
ListNode*<T> _prev;
T _data;
ListNode(const T& data = T())
:_next(nullptr)
,_prev(nullptr)
,_data(data)
{}
};
template<class T>
class list
{
typedef ListNode<T> Node;
public:
list()
{
_head = new Node(T());
_head->_next = _head;
_head->_prev = _head;
}
void push_back(const T& x)
{
Node* newnode = new Node(x);
Node* tail = _head->_prev;
// head tail newnode
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;
}
private:
Node* _head;
};
2.2 迭代器实现
2.2.1 非const正向迭代器
list因本身物理空间不连续,需要通过结点存储前后元素地址,进行访问。那么list的迭代器需要封装成一个类,重载前置++,后置--,!=,==,*等符号。
template<class T>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T> Self;
Node* _node;
//...
};
- typedef可以帮我们简化类名称,方便之后使用,重命名ListNode<T>类为Node,还有将ListIterator<T>类重命名为Self。
ListIterator(Node* node)
:_node(node)
{}
- 构造函数实现一个有参的构造函数,使用初始化列表初始化_node。
template<class T>
class list
{
typedef ListNode<T> Node;
public:
typedef ListIterator<T> iterator;
iterator begin()
{
//iterator it(_head->_next);
//return it
//下面是隐士类型转换调用
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
};
- 在list中提供begin和end函数,对迭代器进行初始化,或者遍历时做判断。
- begin函数中,可以先创建一个iterator对象,再返回该对象。也可以直接使用匿名对象,内部传结点参数,进行隐式类型转换。
- end函数提供最后一个元素的下一个位置,所以用哨兵位结点进行构造。
// ++it 前置
Self& operator++()
{
_node = _node->_next;
return *this;
}
// it++ 后置
Self operator++(int)
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
// --it 前置
Self& operator--()
{
_node = _node->_prev;
return *this;
}
- 前置++,表示访问后一个元素,让_node赋值为_next指针即可。返回类型使用Self的引用,不用再次拷贝,更高效,记得要返回*this。
- 后置++,返回当前的位置,但是迭代器自身指向下一个元素。创建一个Self类型的tmp变量对*this进行拷贝构造,然后自身赋值为下一个结点指针,返回tmp。
- 前置--,表示访问前一个元素。做法跟前置++类似,让_node赋值为它的_prev指针即可。也是使用Self引用类型返回。
T& operator*()
{
return _node->_data;
}
- *符号,表示要解引用到Node类型存储的元素。返回类型使用T类型的引用,直接返回data的别名,减少拷贝。
bool operator!=(const Self& it)
{
return _node != it._node;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
- !=和==可直接返回两个迭代器结点地址比较结果。
- ->符号比较特别,表示当前结点元素数据的地址,可以像结构体->一样使用。
T* operator->()
{
return &_node->_data;
}
虽然operator->看起来比较奇特,但是也有它的应用场景。
- 看下面的代码,我们定义一个Pos,Pos是表示坐标位置的类。我们创建一个数据类型为Pos的list容器,尾插三个Pos类对象数据。
- 使用迭代遍历。如果在while循环使用*it,进行打印,程序会报错,这是因为Pos类没有匹配的cout流插入函数,在全局作用域下也没有重载cout函数。
- 所以可以用(*it)._row这样的方式打印两个整型坐标,*符号返回的是结点的数据别名,在这里是Pos类变量的别名,我们可以用.符号进行打印,但是看起来太别扭了。
- 重载的->就派上用场了,我们使用->就像结构体指针访问内部对象。不过正常来说应该是两个->符号,代码中有展开示例。为了可读性,只用写一个->符号就可以使用。
struct Pos
{
int _row;
int _col;
Pos(int row = 0, int col = 0)
:_row(row)
, _col(col)
{}
};
void test_list2()
{
list<Pos> lt1;
lt1.push_back(Pos(100, 100));
lt1.push_back(Pos(200, 100));
lt1.push_back(Pos(300, 100));
//临时对象可以调用非静态成员函数,但是不能引用定义
list<Pos>::iterator it = lt1.begin();
while (it != lt1.end())
{
//cout<< *it <<endl;//error,这样写无法运行成功。
//可以这样写但是太别扭了
//cout << (*it)._row << ":" << (*it)._col << " ";
//为了可读性,省略了一个->
cout << it->_row << ":" << it->_col << " " << endl;
//cout << it->->_row << ":" << it->->_col << " "; 展开就成了下面的样子
cout << it.operator->()->_row << ":" << it.operator->()->_col << " "<< endl;
++it;
}
cout << endl;
}
运行结果如下:
2.2.2 const正向迭代器
- 非const对象的迭代器迭代器指向的元素可以修改,但是有时候容器内部元素加上const修饰,不可以修改。因此,还要提供const类型的迭代器。我们可以使用上面的代码,只要将operator*和operator->函数的返回类型加上const修饰就行。
template<class T>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T> Self;
Node* _node;
ListIterator(Node* node = Node())//默认构造
:_node(node)
{}
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self operator++(int)
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
const T& operator*()
{
return _node->_data;
}
const T* operator->()
{
return &_node->_data;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
};
template<class T>
class list
{
typedef ListNode<T> Node;
public:
typedef ListIterator<T> iterator;
typedef ListIterator<T> const_iterator;
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
//...
};
- 但是这两类迭代器就只有operator*和operator->函数的返回类型不同而已,返回类型上有区别,其实我们可以借助模版解决。
- 只需要在定义两个模版类型参数Ref和Ptr,分别表示引用和指针,在list中分别传不同的参数表示iterator和const_iterator。
template<class T, class Ref, class Ptr>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node* _node;
ListIterator(Node* node = Node())//默认构造
:_node(node)
{}
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self operator++(int)
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
};
template<class T>
class list
{
typedef ListNode<T> Node;
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
//...
};
2.2.3 反向迭代器
反向迭代器的模拟实现和正向迭代器类似,需要注意++和--跟正向迭代器的的方向完全反过来,还要注意迭代器begin和end函数给的结点位置。
template<class T, class Ref, class Ptr>
struct Reverse_ListIterator
{
typedef ListNode<T> Node;
typedef Reverse_ListIterator<T, Ref, Ptr> Self;
Node* _node;
Reverse_ListIterator(Node* node)
:_node(node)
{}
Self& operator++()//前置++,赋值为前面的结点的指针
{
_node = _node->_prev;
return *this;
}
Self operator++(int)//后置++
{
Self tmp(_node);
_node = _node->_prev;
return tmp;
}
Self& operator--()
{
_node = _node->_next;
return *this;
}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
};
template<class T>
class list
{
typedef ListNode<T> Node;
public:
typedef Reverse_ListIterator<T, T&, T*> reverse_iterator;
typedef Reverse_ListIterator<T, const T&, const T*> reverse_const_iterator;
reverse_const_iterator rbegin() const
{
return reverse_const_iterator(_head->_prev);
}
reverse_const_iterator rend() const
{
return reverse_const_iterator(_head);
}
reverse_iterator rbegin()
{
return reverse_iterator(_head->_prev);
}
reverse_iterator rend()
{
return reverse_iterator(_head);
}
//...
};
2.3 插入删除函数
2.3.1 insert和erase
- insert函数是在指定结点位置前插入一个新的结点。需要改变各个结点的指向问题,可以创建几个变量表示结点。
- 因为该函数传入的参数类型是迭代器,所以还要考虑迭代器失效的问题。解决方法是返回指向新结点位置的迭代器类型的变量。下面采用的是匿名对象。
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* newnode = new Node(x);
Node* prev = cur->_prev;
//prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
- erase函数是删除传入的迭代器变量指向的结点。也可以创建几个变量表示结点,改变各个结点的指向问题。
- 返回类型也是迭代器,返回的是被删除结点的下一个结点迭代器类型变量。
//erase后Pos失效,因为被释放了
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next);
}
2.3.2 push_back pop_back push_front pop_front
完成了对insert和erase函数的实现,头部或者尾部插入和删除的函数都可以复用这两个函数。
- push_back函数是在尾部插入一个结点,insert是在指向结点前插入新结点,因为end函数是哨兵位结点,在它前面插入新结点就是尾插,所以传end函数作为迭代器即可。
- pop_back函数是删除最后一个结点,最后一个结点在哨兵位结点之前,即end函数所代表的迭代器,所以需要--。
- push_front和pop_front同理。
void push_back(const T& x)
{
insert(end(), x);
}
void pop_back()
{
erase(--end());
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_front()
{
erase(begin());
}
写一个测试函数
void Func(const list<int>& lt)
{
list<int>::const_iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
void test_list3()
{
list<int> lt1;
//按需实例化
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
lt1.push_back(5);
Func(lt1);
lt1.push_front(10);
lt1.push_front(20);
lt1.push_front(30);
Func(lt1);
lt1.pop_back();
lt1.pop_back();
lt1.pop_back();
Func(lt1);
lt1.pop_front();
lt1.pop_front();
lt1.pop_front();
Func(lt1);
}
运行结果如下:
2.4 构造函数,赋值拷贝函数,析构函数
构造函数这块,有四种类型,默认构造,initializer初始化器构造,迭代器区间构造和拷贝构造。
- 首先,封装一个创建哨兵位头结点的函数,因为后面的构造函数都需要用到。再写一个遍历打印整个链表的函数。
- 默认构造函数直接调用empty_init函数,初始化一个头结点。
void Printlist(const list<int> lt)
{
for (const auto& e: lt1)
{
cout << e << " ";
}
cout << endl;
}
void empty_init()
{
_head = new Node(T());
_head->_next = _head;
_head->_prev = _head;
}
list()
{
empty_init();
}
- initializer_list函数是针对花括号内部有相同类型的构造函数
list(initializer_list<T> il)
{
empty_init();
for (const auto& e : il)
{
push_back(e);
}
}
void test1()
{
list<int> lt1 = { 1,2,3,4,5,6,7,8 };
Printlist(lt1);
}
- 需要注意判断条件只能是!=,不能是<之类的符号。
template <class InputIterator>
list(InputIterator first, InputIterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
void test2()
{
list<int> lt1 = { 1,2,3,4,5,6,7,8 };
Printlist(lt2);
list<int> lt2(lt1.begin(), lt2.end());
Printlist(lt2);
}
- 拷贝构造函数,可以使用范围for尾插元素实现。
//lt1(lt2)
list(const list<T>& lt)
{
empty_init();
for (const auto& e : lt)
{
push_back(e);
}
}
void test3()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
Printlist(lt2);
list<int> lt2(lt1);
Printlist(lt2);
}
- 赋值拷贝函数可以先删除原链表的结点,然后再一个个尾插结点。
- 但是更好的方法就是用正常模版类型参数list接受被拷贝对象,此时的lt就是待拷贝对象的一份临时拷贝,然后使用交换函数交换头结点,因为lt是一份临时拷贝,函数结束后会自动调用析构函数销毁原链表的内容。
//lt1 = lt3
list<T>& operator=(list<T> lt)
{
std::swap(_head, lt._head);
return *this;
}
void test3()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
Printlist(lt2);
list<int> lt2;
lt2 = lt1;
Printlist(lt2);
}
- 析构函数需要先一个个释放结点,再释放头结点。
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
++it;
}
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
总结
看完整篇文章,我相信你对list的使用和一些底层实现原理有进一步的了解,你也可以尝试自己手撕一个简单的list容器实现。树欲静而风不止,既然我们无法改变大势,但是我们可以改变自己!
创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连,你的支持的我最大的动力!!!