STL源码剖析——序列式容器#2 List
list就是链表的实现,链表是什么,我就不再解释了。list的好处就是每次插入或删除一个元素,都是常数的时空复杂度。但遍历或访问就需要O(n)的时间。
List本身其实不难理解,难点在于某些功能函数的实现上,例如我们会在最后讨论的迁移函数splice()、反转函数reverse()、排序函数sort()等等。
list的结点
设计过链表的人都知道,链表本身和链表结点是不一样的结构,需要分开设计,这里的list也不例外,以下是STL list的结点结构:
template <class T>
struct __list_node {
typedef void* void_pointer;
void_pointer next;
void_pointer prev;
T data;
};
从结点结构可以看出,list是一个双向链表,有指向前一结点的prev指针,指向下一结点的next指针。
list的迭代器
显然,list的迭代器本身是什么类型早已由其本身的数据结构所决定,list的双向链表,不支持随机存取,只能是双向迭代器(Bidirectional Iterators)。另外,list的迭代器应该支持正确的递增、递减、取值、成员取用等操作。
以下是list迭代器的源码:
template<class T, class Ref, class Ptr>
struct __list_iterator { // 未继承 std::iterator
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
typedef __list_iterator<T, Ref, Ptr> self;
// 未继承 std::iterator,所以必须自行撰写五个必要的迭代器相应类型
typedef bidirectional_iterator_tag iterator_category; // (1)双向迭代器类型
typedef T value_type; // (2)
typedef Ptr pointer; // (3)
typedef Ref reference; // (4)
typedef __list_node<T>* link_type;
typedef size_t size_type;
typedef ptrdiff_t difference_type; // (5)
link_type node; // node就是一个指向链表节点的指针 __list_iterator(link_type x) : node(x) {}
__list_iterator() {}
__list_iterator(const iterator& x) : node(x.node) {}
// 迭代器必要的操作
bool operator==(const self& x) const { return node == x.node; }
bool operator!=(const self& x) const { return node != x.node; }
// * 取值符号,data是T类型,而reference是T&,返回reference,说明支持当作左值
reference operator*() const { return (*node).data; }
#ifndef __SGI_STL_NO_ARROW_OPERATOR
//不太能理解
pointer operator->() const { return &(operator*()); }
#endif /* __SGI_STL_NO_ARROW_OPERATOR */ //关键:对迭代器累加 1,就是前进一个节点
self& operator++() {
node = (link_type)((*node).next); //next为void*,強制转为指向节点的指针类型(__list_node<T>*)
return *this; //迭代器对象本身没有前进后退这一说法,前进后退的是隐藏在迭代器里的成员node
}
self operator++(int) { //后缀++
self tmp = *this;
++*this;
return tmp;
}
// 对迭代器累减 1,就是后退一个节点
self& operator--() {
node = (link_type)((*node).prev); // 同理
return *this;
}
self operator--(int) { //后缀--
self tmp = *this;
--*this;
return tmp;
}
};
于我而言,我觉得都比较容易理解,唯一不太懂的就是operator->()函数,这里讲一下我的理解,但不知道对不对,其返回的是&(operator*()),而operator*()我们知道返回的是data变量的引用,前面有个取址符号&,说明返回的是data变量的地址?然后返回的类型是pointer,即T*,总的来说返回的是一个T类型指针,该指针指向data变量所在的地址?好像合理,那该怎么使用?可能是it->data时我们能继续当它是指针然后it->data->?
list的数据结构
由上述可知,list是一个双向链表,但不仅如此,它还是个环状双向链表,为什么这么设计?因为这样用一个节点指针就能表示整个链表,何为表示整个链表?就是说我们能够从这一个节点出发,顺序遍历整个链表以及逆序遍历整个链表,而非环状不能做到如此,除非用两个节点(一个头、一个尾)才能表示整个链表。双向链表做成环状很简单,我们用一个节点指针指向链表尾部的一个空白结点(也可以说是头部之前的一个空白结点),该空白结点连接了链表的头部和尾部。
template <class T, class Alloc = alloc> // 預設使用 alloc 為配置器
class list {
protected:
typedef __list_node<T> list_node;
public:
typedef list_node* link_type; protected:
link_type node; // 永远指向最后结点的下一节点。该结点无元素值,代表空节点。
// 其 next 节点永远是头节点
};
这可能会有疑问,为什么不让首尾直接相连,而要隔一个空白结点呢?因为有一个空白结点,符合STL对于区间“前闭后开[, )”的要求,成为last迭代器所指。这么一来,一些功能函数的设计就变得很简单:
iterator begin() { return (link_type)((*node).next); }
const_iterator begin() const { return (link_type)((*node).next); } iterator end() { return node; }
const_iterator end() const { return node; } bool empty() const { return node->next == node; }
size_type size() const {
size_type result = ;
distance(begin(), end(), result);
return result;
} // 取头节点的內容(元素值)。
reference front() { return *begin(); }
const_reference front() const { return *begin(); }
// 取尾节点的內容(元素值)。
reference back() { return *(--end()); }
const_reference back() const { return *(--end()); }
list的构造与内存管理
与Vector类似,list也定义了一个simple_alloc<>对象来负责空间配置事宜,其第一个类型参数为结点类__list_node<T>,为的是每次都分配一个节点大小的空间,第二个类型参数为alloc空间配置器。
template <class T, class Alloc = alloc> // 预设使用alloc为配置器
class list {
protected:
typedef void* void_pointer;
typedef __list_node<T> list_node;
// 专属分配空间的对象,每次配置一个节点的大小
typedef simple_alloc<list_node, Alloc> list_node_allocator;
以下4个函数分别用来配置、释放、构造、销毁一个节点:
protected:
// 配置一个节点并将其返回
link_type get_node() { return list_node_allocator::allocate(); }
// 释放一个节点
void put_node(link_type p) { list_node_allocator::deallocate(p); }
// 构造一个节点(配置空间并赋予值)并返回
link_type create_node(const T& x) {
link_type p = get_node();
__STL_TRY{
construct(&p->data, x);
}
__STL_UNWIND(put_node(p));
return p;
}
// 摧毁一个节点(析构并释放)
void destroy_node(link_type p) {
destroy(&p->data);
put_node(p);
}
list提供了很多种构造函数,第一个就是默认构造函数,允许我们不指定任何参数而创建一个空的list:
public:
list() { empty_initialize(); } // 产生一个空链表
protected:
void empty_initialize() {
node = get_node(); // 配置一个节点空间,令 node 指向它。为空白节点
node->next = node; // 令 node 头尾都指向自己,不设元素值。
node->prev = node;
}
第二个就是比较常用的,构造n个值为value的节点,第三个就是构造n个结点,内容用T类型的默认构造函数填充,两个构造函数虽然用法不一,但实际上都转调用了同一个函数:
public:
list(size_type n, const T& value) { fill_initialize(n, value); }
list(int n, const T& value) { fill_initialize(n, value); }
list(long n, const T& value) { fill_initialize(n, value); }
explicit list(size_type n) { fill_initialize(n, T()); }
protected:
void fill_initialize(size_type n, const T& value) {
empty_initialize();
__STL_TRY{
insert(begin(), n, value);
}
__STL_UNWIND(clear(); put_node(node));
}
对就是fill_initialize(),而fill_initialize()自己也精明的很,我就创建一个空链表,然后就撒手不管了,全交给insert()函数,能看出这样能把构造变成原子操作,能就n个节点全部构造好,不然就一个都不构造。另外,在push_back()函数里也是把自己的工作外包了给insert()函数,可见insert()函数承担了太多,当然我们会在稍后重点学习Insert()函数。
// 安插一个节点,做为头节点
void push_front(const T& x) { insert(begin(), x); }
// 安插一个节点,做为尾节点
void push_back(const T& x) { insert(end(), x); }
insert()具有多个版本,其中最简单的一种,就是在指定位置插入一个节点:
// 在迭代器 position 所指位置安插一個节点,內容为 x。
iterator insert(iterator position, const T& x) {
link_type tmp = create_node(x); // 产生一个节点(值为x)
// 调整前后指针,使 tmp 安插进去。
tmp->next = position.node;
tmp->prev = position.node->prev;
(link_type(position.node->prev))->next = tmp;
position.node->prev = tmp;
return tmp;
}
而其余的版本都是在反复调用上面这个版本罢了:
template <class T, class Alloc>
void list<T, Alloc>::insert(iterator position, size_type n, const T& x) {
for (; n > ; --n)
insert(position, x);
}
template <class T, class Alloc>
void list<T, Alloc>::insert(iterator position,
const_iterator first, const_iterator last) {
for (; first != last; ++first)
insert(position, *first);
}
template <class T, class Alloc>
void list<T, Alloc>::insert(iterator position, const T* first, const T* last)
{
for (; first != last; ++first)
insert(position, *first);
}
void list<T, Alloc>::insert(iterator position,
InputIterator first, InputIterator last) {
for (; first != last; ++first)
insert(position, *first);
}
所谓的插入,就是指 指定位置的前方,这是STL的插入规范。另外,不像vector,list所有的插入操作都不会影响现有迭代器的有效性。
list的元素操作
- erase(iterator position) —— 移除迭代器position所指节点
iterator erase(iterator position) {
//断开当前节点连接,并将前后节点相连
link_type next_node = link_type(position.node->next);
link_type prev_node = link_type(position.node->prev);
prev_node->next = next_node;
next_node->prev = prev_node;
destroy_node(position.node);
return iterator(next_node);
}
- pop_front() —— 移除头节点
void pop_front() { erase(begin()); }
- clear() —— 清除所有节点(整个链表)
template <class T, class Alloc>
void list<T, Alloc>::clear()
{
link_type cur = (link_type)node->next; // 切记,list类内有一node节点指针指向链表末尾的空白结点,这里就是begin()
while (cur != node) { // 遍历链表
link_type tmp = cur;
cur = (link_type)cur->next;
destroy_node(tmp); // 逐一摧毁节点
}
// 恢復 node 原始狀態
node->next = node;
node->prev = node;
}
- remove(const T& value) —— 将值为value的所有元素移除
template <class T, class Alloc>
void list<T, Alloc>::remove(const T& value) {
iterator first = begin();
iterator last = end();
while (first != last) { // 遍历链表
iterator next = first;
++next; //next指针负责前进一格,并与之比较
if (*first == value) erase(first); // 找到就移除
first = next;
}
}
- unique() —— 移除值相同的连续元素,只有相同且连续的元素才会被移除,移除剩一个
template <class T, class Alloc>
void list<T, Alloc>::unique() {
iterator first = begin();
iterator last = end();
if (first == last) return;
iterator next = first;
//next前进一格探路,相同排掉,不同叫first跟上
while (++next != last) {
if (*first == *next)
erase(next);
else
first = next;
next = first;
}
}
比较难的几个函数来了,相比于上面这些小打小闹的弟弟,这些函数确实不易理解,尤其是排序函数sort(),其高超的算法逻辑使我瞠目结舌,作者造轮子的能力太强了。
- transfer(iterator position, iterator first, iterator last) —— 将[first, last)内的所有元素移动到position之前。
protected:
void transfer(iterator position, iterator first, iterator last) {
if (position != last) {
//处理各节点的next
(*(link_type((*last.node).prev))).next = position.node; // (1) last的上一结点(next)与position相连
(*(link_type((*first.node).prev))).next = last.node; // (2) first的上一节点(next)与last相连
(*(link_type((*position.node).prev))).next = first.node; // (3) position的上一节点(next)与first相连 link_type tmp = link_type((*position.node).prev); // (4) 用一节点指针记录position的上一节点tmp,因为下一步position的上一节点就应该是last的上一节点了 //处理各节点的prev
(*position.node).prev = (*last.node).prev; // (5) position(prev)与last的上一节点相连
(*last.node).prev = (*first.node).prev; // (6) last节点(prev)与first的上一节点相连
(*first.node).prev = tmp; // (7) first节点(prev)与原是position上一节点的节点tmp相连
}
}
要注意的是,transfer所接受的区间是没有限制的(从源码也可以看出),这意味着[first, last)与position可以来自同一个链表,甚至position就在[first, last)区间内都行,只是从结果来看前者还能得到想要的答案,后者就有点奇怪了。
从protected可知,我们并不能直接使用transfer,而是使用到transfer的公开接口为:
- splice(iterator position, list& x) —— 将x接合与position所指位置前,注意x必须不同于*this(调用者本身),将自身链表接合在自身链表的某一位置上毫无意义。
void splice(iterator position, list& x) {
if (!x.empty())
transfer(position, x.begin(), x.end());
}
- splice(iterator position, list&, iterator i) —— 将 i 所指元素接合于position所指位置之前,position与 i 可来自于同一链表
void splice(iterator position, list&, iterator i) {
iterator j = i;
++j;
if (position == i || position == j) return; //position == j 说明i已经在position前面了
transfer(position, i, j); //[i, j)
}
- merge(list<T, Alloc>& x) —— 将 x 合并到*this身上。两个lists的内容都必须经过递增排序,注意,这是链表自底向上归并排序用到的重要函数。
template <class T, class Alloc>
void list<T, Alloc>::merge(list<T, Alloc>& x) {
iterator first1 = begin();
iterator last1 = end();
iterator first2 = x.begin();
iterator last2 = x.end();
// 注意:前提是,兩个 lists 都已经过递增排序,
while (first1 != last1 && first2 != last2)
if (*first2 < *first1) {
iterator next = first2;
transfer(first1, first2, ++next); //[first2,++first2) 放在first1所指位置的前面
first2 = next;
}
else
++first1;
if (first2 != last2) transfer(last1, first2, last2); //如果first1已经到尾而first2还有,将剩下的全部放到first1后面
}
- reverse() —— 反转链表
template <class T, class Alloc>
void list<T, Alloc>::reverse() {
// 以下判断,如果是空白链表,或仅有一个元素,就不做任何动作。
// 使用 size() == 0 || size() == 1 來判断,虽然也可以,但是比较慢。
if (node->next == node || link_type(node->next)->next == node) return;
iterator first = begin();
++first;
while (first != end()) {
iterator old = first;
++first;
transfer(begin(), old, first); //逐个元素往链表头前面移动,最终反转
}
}
因为STL的算法sort()只接受随机迭代器(Random Access Iteartor),但list只有双向迭代器(Bidirectional Iterator),所以不能使用,为此STL为list设计了专有的sort(),侯捷老师书上说该函数使用了quick sort,但我觉得它更像是merge sort。因为丝毫没有快排的特征。存疑。
template <class T, class Alloc>
void list<T, Alloc>::sort() {
// 以下判断,如果是空白链表,或仅有一个元素,就不做任何动作。
if (node->next == node || link_type(node->next)->next == node) return;
// 我们应该知道,归并排序一般需要一个临时数组来存放合并后的数据
list<T, Alloc> carry;
list<T, Alloc> counter[]; //这个就是临时数组,第i个链表存放2的i次方个数据,设计者认为64次合并已经是足够大了,毕竟2的64次方是个很大的数据量了。
int fill = ;
while (!empty()) {
carry.splice(carry.begin(), *this, begin()); //只要调用者的链表非空,就将链表的头节点移动到carry里面,每次就移一个节点,就是头节点。注意由transfer源码知,是移动而非复制
int i = ;
//i每次从0开始,这说明每次都会按顺序遍历counter的各个连续非空链表,空就退出
//进入到循环,意味着counter[i]非空
//而carry绝不会为空,因为从循环内得知,先把carry数组合并到counter[i]里,然后又把它放回carry里,所以carry绝非空
//从循环内的carry.swap(counter[i++])得知carry存放的都是上一个counter数据(因为是先swap再自增i再进入下一循环),而counter[i]只要是非空,里面存放的数据量肯定就是2的i次方
//carry存放的也是2的i次方的数据量,而counter[i]本身也存放了2的i次方的数据量,两者一合并,就变成了2的i+1次方的数据量,已经大于counter[i]所能存放的数据量,所以把它转回到carry里,在下一循环中放到下一个counter链表中(也即是counter[i++],这也解释了为什么本次循环中carry存放的也是2的i次方的数据量,因为该数据来自上一次循环的counter[i-1])
while (i < fill && !counter[i].empty()) {
counter[i].merge(carry);
carry.swap(counter[i++]);
}
//
//这里有两个作用
//1.是因为i == fill而终止的循环,此时counter[i]肯定为空(在此之前从未使用过counter[fill]),而carry肯定有数据(来自counter[i-1]),把carry数据转给空的counter[i],此时counter[i]的数据量为2的i次方
// 为什么是2的i次方,是因为i == fill而终止的循环,代表count[i-1]放的数据已经超过2的i-1次方,所以转给carry,并把i自增,终止循环,才能给到counter[i]
//2.是因为counter[i]为空而终止的循环,将存放在carry的来自上一个counter的数据转给counter[i],
carry.swap(counter[i]);
//fill代表的是目前开放的最大counter,只有i==fill才自增,而其上一句是carry.swap(counter[i]);将carry的数据转到counter[fill]里,说明只有当counter[fill]有数据了,fill才自增(才开放下一个counter)
//如果i没到fill,说明目前从调用者链表里拿的数据量还不足2的fill次方。不足以开放下一个counter。
if (i == fill) ++fill;
}
for (int i = ; i < fill; ++i)
counter[i].merge(counter[i - ]);
swap(counter[fill - ]);
}
我真的很想把这段代码用文字的方式解释清楚,我尽力了,但还是太难了,上面的注释看得懂最好,看不懂最好自己用数据走一下这个函数的流程,大概就能懂了。我对这段代码真是佩服得五体投地。是真的只可意会不可言传。
STL源码剖析——序列式容器#2 List的更多相关文章
- STL源码剖析——序列式容器#1 Vector
在学完了Allocator.Iterator和Traits编程之后,我们终于可以进入STL的容器内部一探究竟了.STL的容器分为序列式容器和关联式容器,何为序列式容器呢?就是容器内的元素是可序的,但未 ...
- STL源码剖析——序列式容器#4 Stack & Queue
Stack stack是一种先进后出(First In Last Out,FILO)的数据结构,它只有一个出口,元素的新增.删除.最顶端访问都在该出口进行,没有其他位置和方法可以存取stack的元素. ...
- STL源码剖析——序列式容器#5 heap
准确来讲,heap并不属于STL容器,但它是其中一个容器priority queue必不可少的一部分.顾名思义,priority queue就是优先级队列,允许用户以任何次序将任何元素加入容器内,但取 ...
- STL源码剖析——序列式容器#3 Deque
Deque是一种双向开口的连续线性空间.所谓的双向开口,就是能在头尾两端分别做元素的插入和删除,而且是在常数的时间内完成.虽然Vector也可以在首端进行元素的插入和删除(利用insert和erase ...
- STL源码剖析:算法
启 算法,问题之解法也 算法好坏的衡量标准:时间和空间,单位是对数.一次.二次.三次等 算法中处理的数据,输入方式都是左闭又开,类型就迭代器, 如:[first, last) STL中提供了很多算法, ...
- STL源码剖析之序列式容器
最近由于找工作需要,准备深入学习一下STL源码,我看的是侯捷所著的<STL源码剖析>.之所以看这本书主要是由于我过去曾经接触过一些台湾人,我一直觉得台湾人非常不错(这里不涉及任何政治,仅限 ...
- STL"源码"剖析-重点知识总结
STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点略多 :) 1.STL概述 STL提供六大组件,彼此可以组合 ...
- 【转载】STL"源码"剖析-重点知识总结
原文:STL"源码"剖析-重点知识总结 STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点 ...
- STL源码剖析读书笔记之vector
STL源码剖析读书笔记之vector 1.vector概述 vector是一种序列式容器,我的理解是vector就像数组.但是数组有一个很大的问题就是当我们分配 一个一定大小的数组的时候,起初也许我们 ...
随机推荐
- SpringBoot第二节(SpringBoot整合Mybatis)
1.创建Spring Initiallizr项目 一直点击下一步 2.引入依赖 <dependencies> <dependency> <groupId>org.s ...
- Greenplum 资源队列(转载)
1.创建资源队列语法 Command: CREATE RESOURCE QUEUEDescription: create a new resource queue for workload m ...
- 2019.12.09 java循环(while)
class Demo04 { public static void main(String[] args) { int sum=0; int i=1; while(i<=100){ //sum ...
- 洛谷 P3884 [JLOI2009]二叉树问题
目录 题目 思路 \(Code\) 题目 P3884 [JLOI2009]二叉树问题 思路 深搜统计深度,倍增\(\text{LCA}\)求边数 \(Code\) #include<iostre ...
- 如何更改sdk版本
- es6学习4:async和await
async async函数返回一个 Promise 对象,可以使用then方法添加回调函数.当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句. funct ...
- Innodb的redo log block
- React中跨域问题的完美解决方案
针对react版本^16.6.0有多种解决方案 方案一:package.json中加上proxy代理配置 在packge.json加入 "proxy": "http:// ...
- Oracle系列二 基本的SQL SELECT语句
1.查询表中全部数据 示例: SELECT * FROM employees; 说明: SELECT 标识 选择哪些列. FROM 标识从哪个表中选择. * 选择全部 ...
- matlab学习笔记10_4MATLAB中的字符串表示
一起来学matlab-字符串操作 10_4 MATLAB中的字符串表示 觉得有用的话,欢迎一起讨论相互学习~Follow Me 参考书籍 <matlab 程序设计与综合应用>张德丰等著 感 ...