STL源码剖析学习笔记——list

本文详细介绍了C++STL中的list容器的实现,包括构造、读取、插入、删除等操作,以及splice、merge、sort等高级功能。作者通过编写一个简单的tinylist,探讨了list的数据结构、结点操作和迭代器设计,并提供了测试代码和解析。
摘要由CSDN通过智能技术生成

list

一、前言

继前几天学完vector以后又过了这么多天,总算是把list学了个大概,也是依葫芦画瓢写了个tiny list。难度上来看我觉得和vector差不多,vector难在空间动态增长的思想上,list难在链表结点与指针的各种操作上。话不多说,还是按照之前的老套路,一边测试一边记笔记,gogogo!

注:list的仿写从GitHub大佬写的学到了很多,和源码的风格很像,而且可读性特别好,贴上链接,感谢大佬!!

Alinshans/MyTinySTL: Achieve a tiny STL in C++11 (github.com)

二、测试代码纵览

2.1 数据结构

这次不搞自定义数据结构了,浅试了一下貌似没什么问题,为了笔记简洁明了就用int吧

另外由于需要多次打印链表,单独拎出来打印函数,后面不再解释

template<typename T>
void print_list(list<T>& l) {
    for(const auto& n : l) {
        std::cout << n << ' ';
    }
    std::cout << std::endl;
}

2.2 测试内容

1、构造:constructor

2、读取:front、back

3、插入:push_back、push_front、insert

4、删除:erase、clear、unique、remove

5、骚操作:splice、merge、sort

三、故事从一个孤零零的结点开始

3.1 构造

一上来直接干两个表,两个表的构造方式不同

int iv[5] = {1, 2, 3, 4, 5};
list<int> l(iv, iv + 5);
list<int> l2(3, 99);

先说l2,目的是构造3个值为99的元素。早有耳闻list的内部是链表,这下可以亲自上手操作一番。

l2对应的构造函数如下:

list(size_type n, const T &val) { fill_initialize(n, val); }

又是咱的老朋友fill_initialize,但是上次vector是在申请的连续空间上构建一连串对象,这次操作的目标不再是连续空间,而是一个个孤立但通过指针连接的结点。首先需要了解的是list的具体数据结构:带有头结点的双向链表。那么初始化的第一步当然是新建一个头结点。那么结点的结构我们就必须先实现。

template<typename T>
struct list_node {
    list_node<T> *prev;
    list_node<T> *next;
    T data;
};

前后指针+实际数据存放,比较简单。

那么迭代器呢?之前的vector由于是随机访问迭代器所以没有特殊的设计,但是从list的结构可以看出它的迭代器类型应该是双向迭代器bidirection_iterator。迭代器内部应该封装指向结点node的指针,同时实现解引用、成员访问、++、==等一系列操作。以下是简略版的迭代器设计:

template<typename T>
struct list_iterator : public tinystl::iterator<tinystl::bidirection_iterator_tag, T> {
    using self = list_iterator<T>;
    using iterator = list_iterator<T>;

    using value_type = T;
    using pointer = T *;
    using reference = T &;
    using link_type = list_node<T> *; //这个就是结点指针的类型

    link_type _node; //指向结点的指针

    explicit list_iterator(link_type x) : _node(x) {}
    list_iterator() = default;
    list_iterator(const iterator &x) : _node(x._node) {}

    reference operator*() { return _node->data; }
    pointer operator->() { return &(operator*()); }
    //重载后itr->val其实是itr.operator->()->val
    //而operator->()做的事是取回结点内实际元素的指针

    //前加加
    self &operator++();
    //后加加
    self operator++(int);
    self &operator--();
    self operator--(int);

    bool operator==(const iterator &rhs) const { return this->_node == rhs._node; }
    bool operator!=(const iterator &rhs) const { return this->_node != rhs._node; }
};

终于!我们要创造出有史以来第一个结点了!!!

		using list_node = list_node<T>;
    using link_type = list_node *;
    using iterator = list_iterator<T>;
    
private:
		//list唯二的成员变量,如果size随用随算的话那就只剩一个指针
    link_type _node; //环形链表只需一个指针即可,这个指针指向末端结点(最后一个元素的下一个位置)
    size_type _size; 

		//申请一个空结点
		_node = new_node();
    _node->next = _node;
    _node->prev = _node;
    //申请一个结点的空间
    link_type new_node() { return list_node_allocator::allocate(); }

new_node非常easy,用allocator分配一个结点大小的空间。注意这里说的是一个结点大小的空间,而我们填入模板的T指的是结点内部data的类型,不要搞混了,为此也是声明了一个typedef:using list_node_allocator = tinystl::allocator<list_node>;刚开始的时候这个头结点的next和prev指针都是指向自己的。

有了头结点,下一步就是把n个val插进表里:

//申请并构造一个结点
link_type create_node(const T &val) {
    link_type blank_node = new_node();
    tinystl::construct(&blank_node->data, val);
    return blank_node;
}
for (; n > 0; --n) {
    link_type node = create_node(val);
    //这里是前插,不过反正都一样的结点就无所谓了
    node->next = _node->next;
    _node->next->prev = node;
    _node->next = node;
    node->prev = _node;
}
_size = n;

插入操作是头插还是尾插还是得想清楚,后面另一种构造函数插点就犯了个错误插反了。另外如果关注异常安全的话要有rollback的代码,这里就不赘述了。至此,l2的构造就完成了,基本过程就是创建头结点——依次创建元素结点并且插入链表中,更新前后指针。下面是用迭代器构造另一个表l的方案。

当我初始化l的时候传入的是两个int*指针,目的是按照这两个指针指向区间内的元素构造链表,这也就意味着对应的构造函数一定是一个模板函数,并且接受的是迭代器类型,当传入的两个参数不是input_iterator的时候应该拒绝这个重载。这里还涉及了模板元编程、SFINAE设计的知识,我还是太菜了,只能跑去参考大佬的实现,发现原来我当初写iterator_traits的实现必须修改,否则查int::iterator_category肯定是报错的,具体的报错是type ‘int’ cannot be used prior to ‘::’ because it has no members… … 折磨了我好久才勉强算是能看懂。总之,构造函数代码如下:

template<typename T>
template<typename Iter>
void list<T>::copy_initialize(Iter first, Iter last) {
    _node = new_node();
    _node->prev = _node;
    _node->next = _node;
    size_type n = tinystl::distance(first, last);
    try {
        for (; n > 0; --n, ++first) {
            auto node = create_node(*first);
            node->next = _node;
            node->prev = _node->prev;
            _node->prev = node;
            node->prev->next = node;
        }
        _size = n;
    }
    catch (...) {
        clear();
        list_node_allocator::deallocate(_node);
        _node = nullptr;
        _size = 0;
        throw;
    }
}
template<typename Iter, typename std::enable_if<
        tinystl::is_input_iterator<Iter>::value, int>::type = 0>
list(Iter first, Iter last) { copy_initialize(first, last); }

这个copy_initialize和fill_initialize异曲同工,思想就是逮着迭代器对应区间内的每一个元素挨个构造进链表里面。

终于,这两个表都构造出来了,打印一手让我好好看看它们。

3.2 存取:这下只能看头尾元素了

这一节主要测试front、back函数以及通过迭代器存取的方式。

cout << "l.front() = " << l.front() << endl;
cout << "l.back() = " << l.back() << endl;
cout << "l.size() = " << l.size() << endl;
*(l2.begin()) = 10;
print_list(l2);

回忆一下我们的头指针的处境,头结点的位置实际上是第一个结点front的后一个位置,同时是最后一个结点back的后一个位置(由于要满足前闭后开的规则所以头指针实际上正是指向end)。那么就可以依据指针的指向写一系列元素存取和头尾迭代器获取函数以及empty、size等功能函数:

iterator begin() { return iterator(_node->next); }
iterator end() { return iterator(_node); }
reference front() { return *begin(); }
reference back() { return *(--end()); }

bool empty() { return begin() == end(); }
[[nodiscard]] size_type size() const { return _size; }

size()的实现实际上要看有没有_size这个成员。如果有的话,那么进行插入、删除的时候要时刻记着更新,带来的好处就是size()的时间复杂度是O(1);如果没有,那要求list大小只能遍历整个链表,如果经常需要读取大小的话会是一笔不小的时间开销,所以我的选择是前者。

测试结果打印如下:

3.3 插入:有了指针就是硬气

vector的插入会涉及全体元素大搬家的操作,还要考虑申请空间大小、插入时后方元素的移动等烧脑的环节,到了list这边,反倒是感觉舒心多了,毕竟在链表中,无论多么复杂的环境下,插入或删除一个元素都只涉及三个结点,并且由于头结点的存在,咱们也不需要考虑什么边界条件,可以说是很良心了!!

不多说,测试:

l.push_back(100);
l.push_front(100);
auto itr = std::find(l.begin(), l.end(), 3);
l.insert(itr, 100);
print_list(l);

我们的目的是让l面目全非哈哈哈。这段测试是在头上插了个100,尾部插了个100,元素3(不是下标哦)的前面插个100,下面一个个看具体实现。

首先是insert,这个函数可以说是链表插入的根基。

template<typename T>
    typename list<T>::iterator list<T>::insert(list::iterator pos, const T &val) {
        //构造一个结点并且填入内容
        link_type tmp = create_node(val);
        //一顿骚操作把tmp结点接到pos的前一个位置(想象不出来过程就画图)
        tmp->next = pos._node;
        tmp->prev = pos._node->prev;
        pos._node->prev->next = tmp;
        pos._node->prev = tmp;

        ++_size;
        //返回插入元素,但是不存在空间移动问题,pos迭代器仍然有效
        return iterator(tmp);
    }
}

有了任意位置插入的函数,在头尾插还有难度吗?已经结束咧!

template<typename T>
void list<T>::push_back(const T &val) {
    insert(end(), val);
}

template<typename T>
void list<T>::push_front(const T &val) {
    insert(begin(), val);
}

舒服了,直接看测试结果吧:

3.4 删除

主要有erase、clear、unique、remove,两位pop纯粹调用erase就不说了。其中erase、clear老调重弹了,remove作用是删除所有指定的元素(指的是元素本身,而非迭代器位置的元素)。unique比较特殊,它的作用是把所有连续的重复元素“去重”,并不能做到任意链表的去重,所以常常搭配sort排序使用。接下来开始测试:

l.erase(l.begin());
print_list(l);
l.push_back(100);
l.push_back(100);
print_list(l);
l.unique();
print_list(l);
l.remove(100);
print_list(l);

首先我想删除头部的元素,没记错的话头部是一个100。然后在尾部插入两个100,这下尾部该有3个连续的100了。接下来调用unique删掉尾部多余的100而没有影响中间的一个孤零零的100。最后把表里的100全部删光,这下又只剩12345了,爷青回!!那么我们看一看实现,然后看看测试结果和我们想的一不一样。

template<typename T>
typename list<T>::iterator list<T>::erase(list::iterator pos) {
    link_type return_node = pos._node->next;
    pos._node->prev->next = pos._node->next;
    pos._node->next->prev = pos._node->prev;
    destroy_node(pos._node);
    --_size;
    return iterator(return_node);
}

erase确实没有什么很酷的细节,不过作为最低一级的删除函数,和insert一样不能忘了修改size,还有就是把结点从链表中删除了以后要记得析构对象+回收空间(destroy_node做的事),防范内存泄漏人人有责。

template<typename T>
void list<T>::remove(const T &val) {
    link_type cur = _node->next;
    while (cur != _node) {
        if (cur->data == val) { //找data项匹配的结点,找到就调erase删掉
            cur = erase(iterator(cur))._node;
        } else cur = cur->next;
    }
}

可以看到上面的remove是线性搜索+erase

template<typename T>
void list<T>::unique() {
    if (_size <= 1) return;
    link_type cur = _node->next;
    link_type next = cur->next;
    while (cur != _node) {
        if (cur->data == next->data) {
            next = erase(iterator(next))._node;
        } else {
            cur = next;
            next = next->next;
        }
    }
}

unique比上面二位稍微有意思一点,走的是一个双指针的法子,大哥在前面开路,遇到和小弟这个哨兵守着的一样的元素就给删了,直到不一样了小弟才走到大哥这,大哥继续往前探。

clear不再赘述,只需从头destroy到尾即可。

最后看看测试结果,很符合我对你们四位成员函数的想象捏。

3.5 下面是整活时间

上面的那一些在vector都见过,老面孔了,这次多少得整点新活,这不就来了。

auto itr2 = std::find(l2.begin(), l2.end(), 99);
l2.splice(itr2, l);
if(l.empty()) cout << "l is now empty!" << endl;
print_list(l2);
l2.reverse();
print_list(l2);
l2.sort();
print_list(l2);

splice做的事情是一个链表的拼接,有好几个重载,这里只演示一个:将链表l接到itr2的前面。注意:拼接不涉及任何拷贝的操作,所以拼接结束以后l虽然还活着,但是只剩个头了(doge)。reverse则是链表反转,sort是链表排序。下面一个个看吧。

template<typename T>
void list<T>::splice(list::iterator pos, list &x) {
    if(!x.empty()) {
        transfer(pos, x.begin(), x.end());
        _size += x._size;
        x._size = 0;
    }
}

里面有个没见过的transfer函数,实质上是对指针的一系列操作,把一段范围内的链表接到pos的前面,这一段链表从此就和老东家势不两立了,彻彻底底地加入了新的链表。那么splice就是对transfer的包装,加上了对size的修改。

template<typename T>
void list<T>::reverse() {
    if(_size == 0 || _size == 1) return;
    iterator cur = begin();
    ++cur;
    while(cur != end()) {
        iterator pre = cur;
        ++cur;
        transfer(begin(), pre, cur);
    }
}

reverse的实现则是遍历链表,并且把遍历到的一个个元素拆下来接到头上去,以此实现反转,反转链表可以说是链表操作的基础了,不说了该继续刷题了。。。

template<typename T>
void list<T>::sort() {
    if(_size == 0 || _size == 1) return;

    list<T> carry;
    list<T> counter[64];
    int fill = 0;
    while(!empty()) {
        carry.splice(carry.begin(), *this, begin());
        int i = 0;
        while(i < fill && !counter[i].empty()) {
            counter[i].merge(carry);
            carry.swap(counter[i++]);
        }
        carry.swap(counter[i]);
        if(i == fill) ++fill;
    }
    for(int i = 1; i < fill; ++i) {
        counter[i].merge(counter[i - 1]);
    }
    swap(counter[fill - 1]);
}

sort属于是难中难了,我单步调试了半天才勉强搞懂书上代码的思想,更不要说自己写了。。应该是个类似归并排序的算法,把链表元素逐步拆下并利用merge保持有序性(merge不细说了,就是有序的两个链表合成一个,当初考408的时候学的套路了),然后用counter数组保存一段一段的有序链表,最后用一个for循环收集成一个链表最终实现排序。只能说太妙了,然而我写不出来。。

芜湖,终于过了一遍,上测试结果,下班喽!

四、后记

不知不觉list也学完了,看了大佬的代码以后深感自身能力不够强,但是也不能急,还得是一点点学吧,咱以后有个班上就可以了,现在真是太卷了,,不管怎么说deque再见吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值