《STL 源码剖析》学习笔记之容器(二)list

[图]The orange 2019-08-06

这是 herongwei 的第 83 篇原创

阅读本文大概需要 6.66 分钟

1、list 概述

相较于 vector 的连续线性空间,list 就显得复杂许多,它的好处是每次安插或删除一个元素,就配置或释放一个元素空间。因此,list 对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素安插或元素移除,list 永远是常数时间。

list 和 vector 是两个最常被使用的容器。什么时机下最适合使用哪一种容器,必须视元素的多寡、元素的构造复杂度、元素存取行为的特性而定。

list  结构定义

SGI list 不仅是一个双向串行,而且还是一个环状双向串行。所以它只需要一个指标,便可以完整表现整个串行:

安插一个元素之后的 list 状态如图 4-6。注意,安插完成后,新节点将位于标兵迭代器(标示出安插点)所指之节点的前方—这是 STL 对于「安插动作」的标准规范。

由于 list 不像 vector那样有可能在空间不足时做重新配置、数据搬移的动作,所以安插前的所有迭代器在安插动作之后都仍然有效。

2、几点注解

list 内部提供一个所谓的迁移动作(transfer):将某连续范围的元素迁移到某个特定位置之前。技术上很简单,节点间的指标移动而已。这个动作为其它的复杂动作如 splice, sort, merge 等奠定良好的基础。

1、node 是一个指针,SGI list 是一种双向环状链表,除了绿色的数据域本身之外,还要附带两个指针,一个往前指,一个往后指,也就是前驱指针和后继指针。

2、前驱和后继指针分别是 void_pointer 类型,看源码知道,void_pointer 是 void* 类型。原因?可见在程序后面操作要去转化类型。

3、list 的迭代器如何使用?链表是一种非连续的结构,所以迭代器不能是一种指针,但是我们还是要让迭代器要模拟指针的功能,也就是说,迭代器要足够聪明的知道,当用户要访问下一个元素的时候,迭代器要能够通过当前指向的位置的指针的 next 域,定位到正确访问的下一个位置,进去看 next 指针。

4、除了 array,vector 之外,所有的容器的迭代器必须是一种 class ,是一种智能指针,它才能设计出聪明的动作。

5、所有的容器必须要有一个 typedef xxx<T,T&,T> iterator 代表一种 class。有三个模板参数。

6、指针可能会被使用者进行怎样的操作呢?++,–,&,等等。

7、重载运算符 ++ 

operator++() {node = (link_type)((node).next); return this;} 

调整迭代器的指向,使得指向当前结点的next结点

8、重载运算符 --也是一样

 self& operator–() node = (link_type)((node).prev);return this;}

9、这里的两个返回值,一个返回 reference 一个不是,是为了模拟整数的这两种操作,在 C++ 中,两次前置++可以运行,但不允许两次后置++。

10、node 结点的取值操作,为什么这里要用.data 而不是直接用 -> ,这里有个原因在于为了让编译器区分后面的重载-> 运算符

11、所有容器遵循的设计原则是 前闭后开原则,在 list 里面,要把最后一个元素的下一个元素设置为不属于容器本身。因此,list 的设计刻意在环状尾端加一个空白结点。

12、在 G2.9 版 中 sizeof(list) = 4而在 G4.9 版 sizeof(list) = 8;

3、源码剖析

还记得之前的手写 LRU 吗?

我们知道,STL 中的 list 就是一种双向链表,我们在定义一个 cachesMap ,key 保存键,value 保存前面的 list 中指向双向链表实现的 LRU 的 Node 节点,也就是说 cachesMap 里面 每一个 value 对应的是一个双向链表的结点,那么具体访问的时候,利用迭代器就可以了,非常的方便。

//用容器实现双向链表+哈希表 
//Author:herongwei
class LRUCache {
public:
    LRUCache(int capacity):cap(capacity){}
    /*
    1、判断插入的新元素是否出现过,
    2、如果之前出现过,在双向链表中删除
    3、如果之前没有出现,插入到双向链表最前面,更新哈希表
    4、最后判断容量是否够,如果不够,先删除双向链表最不常用的,更新哈希表
    */
    void put(int key, int value) {
        auto it = m.find(key);
        if(it != m.end()) cache.erase(it->second);
        cache.push_front(make_pair(key,value));
        m[key]=cache.begin();
        if(m.size()>cap) {
            int k = cache.rbegin()->first;
            cache.pop_back();
            m.erase(k);
        }
    }
    /*
    1、我们在哈希表中查找给定的 key,若不存在直接返回-1。
    2、如果存在则将此项移到顶部,这里我们使用C++ STL中的函数splice,专门移动链表中的一个或若干个结点到某个特定的位置,这里我们就只移动key对应的迭代器到列表的开头,然后返回value。
    */
    int get(int key) {
        auto it = m.find(key);
        if(it != m.end()) {
            cache.splice(cache.begin(), cache,it->second);
            return it->second->second;
        }
        return -1;
    }
private:
    int cap;
    list<pair<int,int>>cache;
    unordered_map<int,list<pair<int,int>>::iterator >m;
};

LRU 的 Node 节点,也就是说 cachesMap 里面 每一个 value 对应的是一个双向链表的结点,那么具体访问的时候,利用迭代器就可以了,非常的方便。

这里,爱思考的同学们,看到上面的 splice 函数,不禁会思考,这个函数到底是如何实现的呢?本真刨根问底的精神,我们去探究一波 list 的源码吧。

先来看一张图片

没错了,上面的图示就是 splice 函数的实现过程,而在 list 里面,具体调用的则是 另外一个函数  transfer,下面是 transfer 的源码:

protected:
// 将 [first,last) 内的所有元素搬移到 position 之前。
void transfer(iterator position, iterator first, iterator last)
{
    if (position != last)
    {
        (*(link_type((*last.node).prev))).next = position.node; //1
        (*(link_type((*first.node).prev))).next = last.node;    //2
        (*(link_type((*position.node).prev))).next = first.node;//3
        link_type tmp = link_type((*position.node).prev);       //4
        (*position.node).prev = (*last.node).prev;              //5
        (*last.node).prev = (*first.node).prev;                 //6 
        (*first.node).prev = tmp;                               //7
    }
}

可以看到

第一步是调整 last 结点前一个结点的 next 指针;这样才能在调整 tmp 结点的 next 指针之后,将 first position 结点保存下来。

第二步是调整 first 结点的前一个结点的 next 指针;这样才能在调整 first 结点的 prev 指针之后,将 last 结点保存下来。

实际上,transfer 并非是一种公开的接口。list 公开提供的是所谓的接合动作(splice):将某连续范围的元素从一个 list 搬移到另一个(或同一个)list 的某个定点。

在来看一张比较形象的图片就大概知道了这个过程是怎么一回事了。

下面附上部分 list 源码。

//下面是一个测试程序,观察重点在建构的方式以及大小的变化:
// filename : 4list-test.cpp
#include <list>
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
    int i;
    list<int> ilist;
    cout << "size=" << ilist.size() << endl; // size=0
    ilist.push_back(0);
    ilist.push_back(1);
    ilist.push_back(2);
    ilist.push_back(3);
    ilist.push_back(4);
    cout << "size=" << ilist.size() << endl; // size=5
    list<int>::iterator ite;
    for(ite = ilist.begin(); ite != ilist.end(); ++ite)
        cout << *ite << ' '; // 0 1 2 3 4
    cout << endl;
    ite =find(ilist.begin(), ilist.end(), 3);
    if (ite!=0)
        ilist.insert(ite, 99);
    cout << "size=" << ilist.size() << endl; // size=6
    cout << *ite << endl; // 3
    for(ite = ilist.begin(); ite != ilist.end(); ++ite)
        cout << *ite << ' '; // 0 1 2 99 3 4
    cout << endl;
    ite =find(ilist.begin(), ilist.end(), 1);
    if (ite!=0)
        cout << *(ilist.erase(ite)) << endl; // 2
    for(ite = ilist.begin(); ite != ilist.end(); ++ite)
        cout << *ite << ' '; // 0 2 99 3 4
    cout << endl;
    return 0;
}
// 对迭代器累加 1,就是前进一个节点
self& operator++(){
    node = (link_type)((*node).next);
    return *this;
 } 
// 对迭代器递减 1,就是后退一个节点
self& operator--(){
    node = (link_type)((*node).prev);
    return *this;
 }
// 安插一个节点,做为头节点
void push_front(const T& x)
{
    insert(begin(), x);
}
// 安插一个节点,做为尾节点(上一小节才介绍过)
void push_back(const T& x)
{
    insert(end(), x);
}
// 移除迭代器 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);
}
// 移除头节点
void pop_front()
{
    erase(begin());
}
// 移除尾节点
void pop_back()
{
    iterator tmp = end();
    erase(--tmp);
}
//清除所有节点(整个串行)
template <class T, class Alloc>
void list<T, Alloc>::clear()
{
    link_type cur = (link_type) node->next; // begin()
    while (cur != node)  //巡访每一个节点
    {
        link_type tmp = cur;
        cur = (link_type) cur->next;
    }
    destroy_node(tmp); //摧毁(解构并释放)一个节点
// 恢复 node 原始状态
    node->next = node;
    node->prev = node;
}


//将数值为 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;
        if (*first == value)
            erase(first); //找到就移除
        first = next;
    }
}


//移除数值相同的连续元素。注意,只有「连续而相同的元素」,才会被移除剩一个。
template <class T, class Alloc>
void list<T, Alloc>::unique()
{
    iterator first = begin();
    iterator last = end();
    if (first == last)
        return;//空串行,什么都不必做。
    iterator next = first;
    while (++next != last)
    {
        if (*first == *next)
            erase(next);
        else
            first = next;//调整指标
        next = first;//修正区段范围
    }
}

参考资料:《STL源码剖析》(侯捷著)。

感兴趣的同学,欢迎有问题和我交流~

推荐阅读

《STL 源码剖析》学习笔记之容器(一)vector

秋招之路-链表面试题集合(二)

秋招之路-链表面试题集合(一)

认真的人,自带光芒!

原创不易

点个在看呗

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看REAdMe.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看REAdMe.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看READme.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 、 1资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看READmE.文件(md如有),本项目仅用作交流学习参考,请切勿用于商业用途。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值