摘要:介绍 list 容器,list 模拟实现,list与vector的对比
list(带头双向循环列表)
导入:list 的成员函数基本上与 vector 类似,具体内容可以查看相关文档(cplusplus.com/reference/list/list/),这里不多赘述。以下对 list 的 Operations 部分的函数进行简单讲解。
splice | Transfer elements from list to list (public member function) |
remove | Remove elements with specific value (public member function) |
remove_if | Remove elements fulfilling condition (public member function template) |
unique | Remove duplicate values (public member function) |
merge | Merge sorted lists (public member function) |
sort | Sort elements in container (public member function) |
reverse | Reverse the order of elements (public member function) |
注意:list 没有扩容的概念,而是一份一份相对独立的节点串连起来的。
1)sort
- #include<list> std::list::sort 与 #include<algorithm> std::sort
如上图,RandomAccessIterator 至少已经在名称上提示使用者,这个 sort 函数要求支持能够被随机访问的迭代器。
首先,list 的迭代器是双向迭代器;其次,从底层实现来看,std::sort 函数用到了迭代器相减,而 list 的地址是不连续的。所以 list 不支持使用 std::sort 函数。
-
std::list::sort 的使用:该函数默认升序排列(底层是归并排序)
如果要降序排序有如下代码以供参考:(std::greater<int>() 是一个 greater 类型的匿名对象,这种写法更常用)#include<functional> #include<list> int main() { std::list<int> lt; //在 lt 中插入一些数据之后 std::greater<int> gt; lt.sort(gt);//or:lt.sort(std::greater<int>()); return 0; }
- std::list::sort 的性能测试
测试结果:
①在 Rlease 模式下, std::vector::sort 效率大约是 list 的 2 倍,并且数据量越大效率差距越大。(tip.性能测试要在 Rlease 模式下进行,Debug 模式下优化没有全开)
②通过 vector 给 list 排序:把 list 对象 → 拷贝数据到 vector 对象中 →对 vector 对象 sort → 把排序好的数据拷贝到 list 。这样对 list 排序,在数据量较大的情况下效率甚至比 list 直接排序要高。
sum. list 的 sort 在性能上没有什么优势,list 中的 sort 函数在对于数据量小的情况下可以使用,但平时能不用尽量不要频繁使用。
2)merge
归并两个 list 到一个 list(要先 sort 才可以 merge,实践中很少用)。
3)unique
去重,但也有要求——只能去除连续相同的,所以要先 sort 再 unique 才可以真正“去重”。
4)splice
转移(移动指针),如下图。
以上就是对 list 一些函数的简单介绍。
list 的模拟实现
1)结构
如上图,list 中的每个节点是一个自定义类型 Node,对于双向链表,每个节点内包括自身储存的数据、前节点指针和后节点指针。
对于由一个一个节点组成的 list,通过头节点来管理整个 list。
代码示例
// List的节点类
template<class T>
struct ListNode
{
ListNode<T>* _pPre;
ListNode<T>* _pNext;
T _val;
};
//List类
template<class T>
class list
{
PNode _pHead;//注意:这里是一个内置类型(指针)
};
2)初始化_Constructor
对 list 的初始化首先是对头节点的初始化。
// List的节点类
template<class T>
struct ListNode
{
ListNode(const T& val = T())
: _val(val)
, _pPre(nullptr)
, _pNext(nullptr)
{}
ListNode<T>* _pPre;
ListNode<T>* _pNext;
T _val;
};
//List类
template<class T>
class list
{
typedef ListNode<T> Node;
typedef Node* PNode;
public:
///
// List的构造
list()
{
CreateHead();
}
private:
void CreateHead()//对头结点进行初始化
{
_pHead = new Node;//这里会去调用struct ListNode的构造函数
_pHead->_pNext = _pHead;
_pHead->_pPre = _pHead;
}
PNode _pHead;//注意:这里是一个内置类型(指针)
};
3)Iterator
class Iterator——Iterator类
- 成员变量:Node* _pNode
- 成员函数:operator* 、operator++ 、operator-- 、operator!= 、operator==(模拟指针的行为)——这里体现了“封装”。封装屏蔽底层差异和实现细节,提供统一的访问修改遍历方式。
代码示例
//List的迭代器类
template<class T>
class ListIterator
{
typedef ListNode<T>* PNode;
typedef ListIterator<T> Self;
public:
//constructor
ListIterator(PNode pNode = nullptr)
:_pNode(pNode)
{}
ListIterator(const Self& l)//copy constructor
{
_pNode = l._pNode;
}
//operations
T& operator*()
{
return _pNode->_val;
}
T* operator->()
{
return &_pNode->_val;
}
Self& operator++()
{
_pNode = _pNode->_pNext;
return *this;
}
Self operator++(int)
{
Self tmp = _pNode;
_pNode = _pNode->_pNext;
return tmp;
}
Self& operator--()
{
_pNode = _pNode->_pPre;
return *this;
}
Self operator--(int)
{
Self tmp = _pNode;
_pNode = _pNode->_pPre;
return tmp;
}
bool operator!=(const Self& l)
{
return _pNode != l._pNode;
}
bool operator==(const Self& l)
{
return _pNode == l._pNode;
}
PNode _pNode;
};
对 operator-> 的补充说明
我们知道,对于自定义类型,可以通过对其指针解引用 " *(pointer). " 和 " (pointer)-> " 来访问其成员。而 iterator 实际上是在模拟指针的行为,对于 operator-> 的使用编译器做出了优化。如下图。
3)Const_Iterator
注意!const_iterator 不是用 const 修饰 iterator,如上 iterator 中的模拟实现可以看出,iterator 底层是原生指针,用 const 修饰 iterator 是使得指针本身不可修改,const_iterator 本身是要能被进行 ++ 和 -- 操作的,否则无法实现遍历;而 const_iterator 针对的是被 const 修饰的 list 的对象,即 const 修饰的是 list 的实例化对象本身。(ps. list 对象是 const 的,那储存在节点中的数据肯定也是 const 的,即为 const T)
如上图,实际上我们需要实现两个不同的 iterator —— class ListIterator 和 class ListConst_Iterator ,而对于 const 对象,begin 和 end 函数将会返回 const_iterator。
优化:使用类模板实现 List 的 Iterator 类
代码示例
//List的迭代器类
template<class T, class Ref, class Ptr>
class ListIterator
{
typedef ListNode<T>* PNode;
typedef ListIterator<T, Ref, Ptr> Self;
public:
//constructor
ListIterator(PNode pNode = nullptr)
:_pNode(pNode)
{}
ListIterator(const Self& l)//copy constructor
{
_pNode = l._pNode;
}
//operations
Ref operator*()
{
return _pNode->_val;
}
Ptr operator->()
{
return &_pNode->_val;
}
Self& operator++()
{
_pNode = _pNode->_pNext;
return *this;
}
Self operator++(int)
{
Self tmp = _pNode;
_pNode = _pNode->_pNext;
return tmp;
}
Self& operator--()
{
_pNode = _pNode->_pPre;
return *this;
}
Self operator--(int)
{
Self tmp = _pNode;
_pNode = _pNode->_pPre;
return tmp;
}
bool operator!=(const Self& l)
{
return _pNode != l._pNode;
}
bool operator==(const Self& l)
{
return _pNode == l._pNode;
}
PNode _pNode;
};
//list类
template<class T>
class list
{
typedef ListNode<T> Node;
typedef Node* PNode;
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T&> const_iterator;
public:
///
// List的构造
list()
{
CreateHead();
}
///
// List Iterator
iterator begin()
{
return _pHead->_pNext;
}
iterator end()
{
return _pHead;
}
const_iterator begin() const
{
return _pHead->_pNext;
}
const_iterator end()const
{
return _pHead;
}
}
注意:同一个类模板,实例化参数不同,就是完全不同的类型,即对于 ListIterator<T, T&, T*> 和 ListIterator<T, const T&, const T&> 是两个不同的类型。(ps. iterator 和 const_iterator 都实现之后才可以支持使用范围 for)
4)其他成员函数
这些成员函数实现起来思路很简单,有问题建议去看数据结构的文章回顾一下。以下简略说明。
①insert
insert 之后 iterator 不失效,因为没有扩容的影响。
// 在pos位置前插入值为val的节点
iterator insert(iterator pos, const T& val)
{
PNode cur = pos._pNode;
PNode newnode = new Node(val);
newnode->_pNext = cur;
newnode->_pPre = cur->_pPre;
cur->_pPre = newnode;
newnode->_pPre->_pNext = newnode;
return pos;
}
②erase
erase 之后 iterator 失效,因为这个被 erase 的节点被释放了,那么指向它的 iterator 也就失效了。
// 删除pos位置的节点,返回该节点的下一个位置
iterator erase(iterator pos)
{
if (!empty())
{
PNode next = pos._pNode->_pNext;
pos._pNode->_pPre->_pNext = next;
next->_pPre = pos._pNode->_pPre;
delete pos._pNode;
--_size;
return next;
}
return _pHead;
}
③push_back and push_front
复用 insert。
// List Modify
void push_back(const T& val) { insert(end(), val); }
void push_front(const T& val) { insert(begin(), val); }
④pup_back and pop_front
复用 erase。
// List Modify
void pop_back() { erase(--end()); }
void pop_front() { erase(begin()); }
⑤clear
用 iterator 遍历,依次 erase 每个节点。
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
⑥Destructor
clear → delete → nullptr,即清理 list,释放头节点,头结点指针指针置空。
//destructor
~list()
{
clear();
delete _pHead;
_pHead = nullptr;
}
⑦Copy Constructor
范围 for 循环 push_back。(注意:使用范围 for 需要把 const_iterator 也实现了才能用)
list(const list<T>& l)//copy constructor
{
CreateHead();
for (auto e : l)
{
push_back(e);
}
}
⑧赋值重载
//assign
list<T>& operator=(list<T> l)
{
if (_pHead != l._pHead)
{
swap(l);
return *this;
}
}
void swap(list<T>& l)
{
std::swap(_pHead, l._pHead);
std::swap(_size, l._size);
}
⑨其他构造函数重载
list(int n, const T& value = T())
{
CreateHead();
while (n--)
{
push_back(value);
}
}
template <class Iterator>
list(Iterator first, Iterator last)
{
CreateHead();
Iterator it = first;
while (it != last)
{
push_back(*it);
++it;
}
}
补充:list 的成员变量中可以加一个 size_t 类型的变量来记录节点个数,因为如果没有这个成员变量就需要遍历来获取有效数据个数,效率比较低。(提醒:如果增加了 size_t 类型的成员变量记得在 insert 和 erase 的函数实现中相应地做出调整)
5)补充:Print
针对于 list<int> / list<char> 等类型的打印函数很好实现,以下我们尝试写出更通用的打印函数。
打印 list<T> 而不只是针对某个具体的 T 类型
因为语法编译之前要先对模板进行实例化,对于 Btl::list<T>::const_iterator 由于模板没有被实例化,所以编译器不知道 const_iterator 是 list<T> 中的一个内嵌类型还是静态成员变量,这样的行为对于编译器是未知的。
所以,Btl::list<T>::const_iterator 前加 typename 来声明这是一个内嵌类型。代码如下。
template<typename T>
void print_l(const Btl::list<T>& _list)
{
typename Btl::list<T>::const_iterator it = _list.begin();
while (it != _list.end())
{
std::cout << *it;
++it;
}
std::cout << std::endl;
}
打印任意容器
提醒:下列代码中要求 *it 支持流插入。
template<typename Container>
void print_l(const Container& _con)
{
typename Container::const_iterator it = _con.begin();
while (it != _con.end())
{
std::cout << *it;
++it;
}
std::cout << std::endl;
}
回顾:vector模拟实现中涉及的深浅拷贝的问题
对于类似 vector<string> 而出现的深浅拷贝问题,因为 list 不涉及扩容的概念,所以不会出现深浅拷贝的问题。
list与vector的对比
vector | list | |
底层结构 | 动态顺序表,一段连续空间 | 带头结点的双向循环链表 |
随机访问 | 支持随机访问,访问某个元素的效率为O(1) | 不支持随机访问,访问某个元素的效率为O(N) |
插入和删除 | 任意位置插入和删除效率低,需要搬移元素(挪动数据),时间复杂度为O(N),插入时有可能需要增容——开辟新空间,拷贝元素,释放就空间,导致效率更低 | 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1) |
空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层结点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低 |
迭代器 | 原生态指针 | 对原生态指针(节点指针)进行封装 |
迭代器失效 | 在插入元素时,要给所有迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效;删除时,当前迭代器需要重新给赋值否则会失效 | 插入元素不会导致迭代器失效;删除元素时,只会导致当前迭代器失效,其他迭代器不受影响 |
使用场景 | 需要高效存储,支持随机访问,不关系插入删除效率 | 大量插入和删除操作,不关心随机访问 |
附
完整代码链接My_List/My_List/My_List.h · fantansy-13-07/Cpp - 码云 - 开源中国 (gitee.com)
END