初识C++之list的使用

一、list的概念

list,简单来讲,我们可以将其看做“一个带头双向循环链表”。它的每个数据块都是通过指针互相连接起来的。这里“带头”的意思是指它带有一个哨兵位头节点,哨兵位指向下一个数据块和尾部数据块的位置,不保存数据“双向循环”是指这里面的数据是以双指针首尾相连的,其中尾结点与头节点相连

初识C++之list的使用_第1张图片

二、list的基本使用

在list中,就不再支持“[]”方式的随机访问了,需要访问时就必须要使用“迭代器”。并且其具体使用方式和vector基本一样,这里就不做过多演示,而是简单介绍一下list中的部分接口的作用

  1. 迭代器

初识C++之list的使用_第2张图片
初识C++之list的使用_第3张图片

在list中,有正向和反向两种迭代器。上图是正向迭代器中的beign()和end()。可以看到,这里进行了函数重载,提供了const类型和非const类型的迭代器。

  1. 获取头节点和尾结点数据

要获取头节点和尾结点的数据也很简单,直接调用front()和back()函数即可。同样的,为了支持const和非const类的传入,也是分别提供了两个函数接口。

  1. 头插头删

如果要在头部插入,使用push_front();如果要在头部删除,则使用pop_front()

  1. 尾插尾删

如果要在尾部插入,使用push_back();如果要在尾部删除,则使用pop_back()

  1. 在任意位置插入数据

初识C++之list的使用_第4张图片

要在任意位置插入数据,使用insert()函数即可。一般来讲,我们最常用的是第一个insert()函数,第二个在pos位置插入n个val和在pos位置插入另一个类的迭代器区间数据都并不常用。

在vector中,我们知道vector因为可以看做是以数据的形式存在的,所以如果在除了尾部的尾部插入数据需要进行数据挪动,效率比较低,不建议使用。但是在list中,因为它的数据可以看做是链表链接起来的,所以在任意位置插入数据都可以看做是O(1)的时间复杂度。当然,O(1)的前提是你已经知道了要插入的数据的位置。因此当需要频繁在中间位置插入或删除数据时,推荐使用list

  1. 在任意位置删除数据

初识C++之list的使用_第5张图片

要在任意位置删除数据,使用erase()即可。第一个接口为删除pos位置的数据,第二个接口为删除一个迭代器区间的数据

  1. 数据交换

数据交换的逻辑很简单,只需要交换两个list的头节点即可。

  1. 重新设置数据个数

resize()函数可以用来插入节点,当然,该函数是从尾部开始插入的,会插入n个val。同时,resize()还可以用来删除数据,按照尾删的方式,将你的节点个数删除到只剩下n个

  1. 删除指定数据

如果我们想删除保存了某个数据的节点,就可以使用remove()函数。该函数会在list中查找val值,如果找到了存有val的节点,就删除该节点。如果没有找到,就什么都不做

  1. 去除重复的数据

unique()函数可以供我们将list中的数据去重。但是要注意,该函数只能在list有序的情况下才能正确运行。也就是说,如果我们要用该函数,就必要要保证对应的list中的数据是有序的。

  1. 节点转移

初识C++之list的使用_第6张图片

splice()函数可以将一个节点上的数据转移到另一个list上。这里的转移并不是指拷贝,转移走的数据在原list中会被删除

三、模拟实现部分list

list的函数实现上和vector相比,其实并没有什么难度。但是其某些函数实现上,却和vector上有着很大的不同

因此,在这里就不再逐一讲解各个函数的逻辑实现,而是挑选其中比较有价值的几个实现具体讲解。

  1. 代码实现

以下是模拟实现部分list的代码:

#pragma once

#include 
#include
#include

using namespace std;

namespace MyList
{
    template
    struct ListNode//list中存储指向上下节点的指针和数据块中的数据
    {
        ListNode* _next;
        ListNode* _prev;
        T _data;

        ListNode(const T& x)//构造函数
            : _next(nullptr)
            , _prev(nullptr)
            , _data(x)
        {}
    };

    template
    struct list_iterator//迭代器,每个迭代器都是一个类ListNode的指针
    {
        typedef ListNode node;
        typedef list_iterator    Self;
        node* _pnode;

        list_iterator(node* _pn)//构造函数
            : _pnode(_pn)
        {}

        Ref operator*()//运算符*重载
        {
            return _pnode->_data;
        }

        Self& operator++()//运算符++重载
        {
            _pnode = _pnode->_next;
            return *this;
        }

        Self& operator--()//运算符--重载
        {
            _pnode = _pnode->_prev;
            return *this;
        }

        Ptr operator->()//运算符->重载
        {
            return &_pnode->_data;
        }

        bool operator!=(const Self& it) const//迭代器是否相等
        {
            return _pnode != it._pnode;
        }

        bool operator==(const Self& it) const
        {
            return _pnode == it._pnode;
        }
    };

    template
    class list
    {
        typedef ListNode node;//结构体重命名为node,方便使用
    public:
        typedef list_iterator iterator;//迭代器类重命名
        typedef list_iterator const_iterator;//const迭代器重命名

        void empty_initialize()//空初始化(创建哨兵位)
        {
            _head = new node(T());
            _head->_next = _head;
            _head->_prev = _head;

            _size = 0;
        }

        //默认成员函数
        list()//构造函数
        {
            empty_initialize();
        }

        template
        list(inputiterator first, inputiterator last)//迭代器区间构造
        {
            empty_initialize();

            size_t size = 0;
            while (first != last)
            {
                push_back(first._pnode->_data);
                ++first;
                ++size;
            }

            _size = size;
        }

        list( list& lt)//构造函数
        {
            empty_initialize();

            list tmp(lt.begin(), lt.end());
            swap(tmp);
            _size = tmp._size;
        }

        ~list()//析构函数
        {
            clear();

            delete[] _head;
            _head->_prev = _head->_next = nullptr;
            _head = nullptr;
        }

        list& operator=(list lt)//运算符=重载
        {
            swap(lt);
            
            return *this;
        }

        //迭代器
        iterator begin()//普通迭代器
        {
            return iterator(_head->_next);
        }
        iterator end()//普通迭代器
        {
            return iterator(_head);
        }

        const_iterator begin() const//const迭代器
        {
            return const_iterator(_head->_next);
        }
        const_iterator end() const//const迭代器
        {
            return const_iterator(_head);
        }

        //Capacity(容量相关):
        size_t size() const//链表中的数据个数
        {
            return _size;
        }

        bool empty() const//链表是否为空
        {
            return _size == 0;
        }

        //(数据操作相关)Modifiers:
        iterator insert(iterator pos, const T& val)//在任意pos位置插入val
        {
            node* newnode = new node(val);
            node* tail = pos._pnode->_prev;

            tail->_next = newnode;
            newnode->_prev = tail;
            newnode->_next = pos._pnode;
            pos._pnode->_prev = newnode;

            ++_size;

            return iterator(newnode);
        }

        iterator erase(iterator pos)//删除pos位置的值
        {
            assert(pos != end());

            node* tail = pos._pnode->_prev;
            node* cur = pos._pnode->_next;

            tail->_next = cur;
            cur->_prev = tail;

            --_size;

            delete pos._pnode;
            return iterator(cur);
        }

        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 clear()//清除list除哨兵位的所有节点
        {
            iterator it = begin();
            while (it != end())
            {
                it = erase(it);
            }
        }

        void swap(list& lt)//哨兵位交换
        {
            std::swap(_head, lt._head);
        }
    private:
        node* _head;//头节点(哨兵位)
        size_t _size;//节点个数
    };
}
  1. 迭代器实现注意事项

仔细观察上面的代码,我们可以发现,这里的迭代器iterator不再是像vector和string中那样,采用原生指针typedef的方式实现迭代器。而是用类模板重新封装。不知道大家听过这么一句话没,“迭代器只是一个类似指针,和指针有着类似功能的东西”。看了上面的代码大家应该就能明白,在stl的某些组件中,是可以用原生指针来当做迭代器的。但是在某些组件中却不行。原因很简单,可以使用原生指针当做迭代器,仅仅只是一种巧合。

以vector为例,vector我们可以将其看做 一个类似数组的东西,它所存储的数据都是在一段连续空间之上。因此,通过指针++,--等形式,我们就可以遍历整个空间。但是list却不同,list可以看成“带头双向循环链表”,它的每个数据块都是独立的,通过存储在其数据块内的指针找到上下数据块的位置。从存储结构上看,并不连续。

初识C++之list的使用_第7张图片

在list上是无法通过原生指针满足迭代器需求的。因此,我们就需要在重新写一个类域,用于实现迭代器。

在这里,我们的写了一个是struct ListNode类,用于存储节点和数据。

初识C++之list的使用_第8张图片

因此,在实现迭代器的struct list_iterator类中,我们需要一个指向ListNode类的指针。

初识C++之list的使用_第9张图片

为了简化书写,这里进行了typedef。有了这个类后,要实现对应的功能,如解引用、++、--等功能,就很简单了。我相信这对绝大部分人来说都不是难事。这里就不过多讲解了。

但是,看了上面的代码,我相信很多人都会有一个疑惑,为什么这里的类模板中参数有三个?这其实就和iter要实现的功能有关了。我们知道,在使用iterator时,我们不仅会使用非const修饰的迭代器,也可能会使用const修饰的迭代器。由const修饰的迭代器的功能主要是用于防止修改对应节点中的数据,而不是阻止迭代器访问。

我们查看库中的list提供的迭代器接口,也可以发现它是提供了const和非const两种形式的:

初识C++之list的使用_第10张图片

但是我们知道,const迭代器和非const迭代器其实只有一个差异,就是const迭代器可以修改数据,但是非const迭代器不能修改数据。为了实现这一功能,如果我们不写上第二个类模板参数,就需要将这个类模板重新拷贝一份,再将里面的*重载的返回值加上const修饰。这个方法虽然可以解决问题,但是却存在大量的重复代码,导致代码冗余。是一种非常不推荐的方式。因此,我们采用第二种解决方案,就是在原有的类模板中再加上一个参数。

我们知道,类模板会根据传入参数的不同初始化不同的类。利用这一特性,我们在类模板中加入一个“Ref”参数,并将Ref参数设置为运算符*重载的返回值。

初识C++之list的使用_第11张图片

然后再在我们的list类中,重命名一个“const_iterator”迭代器,该迭代器的类型为“list_iterator。并将原有的iterator修改为“iterator

通过这两个重命名,我们就确定了两种参数传入的模式。第一个传入的参数相同,但第二个就会有是否有const修饰的差异。由此,我们构造出const迭代器和非const迭代器:

初识C++之list的使用_第12张图片

编译器会根据传入的参数的不同,自动匹配对应的迭代器。

迭代器中的第三个参数,则与运算符->重载有关。“->”的重载比较奇怪,与运算符“*”重载返回解引用不同,运算符“->”重载返回的是要访问的数据的地址

初识C++之list的使用_第13张图片

原因也很简单,我们知道,要访问类的数据,我们有两种方式,其中“.”是用类名访问“->”则是用指针访问。因此在有指针的情况下,我们是可以用“指针->变量名”来访问变量的。但是,如果我们此时没有指针但又想用“->”来访问呢?要知道,例如在list中,都是用迭代器访问的,而迭代器实际上是一个类,而非指针。

初识C++之list的使用_第14张图片

在上图中,我们给list中传入了一个Pos结构体,为了便于访问,我们写了一个iterator it来进行迭代器访问。在访问的过程中,我们需要使用“(*it)._row”来进行访问。虽然这种访问方式可行,但是实际使用起来却是比较麻烦的。

通过重载“->”运算符的方式,我们就可以用以下方式访问:

初识C++之list的使用_第15张图片

在这里,“it->”实际上就是一个函数。但是就算是一个函数,大家也可能很奇怪,因为当其为函数时,“it->”实际看起来应该是“it.operator->() ”,它在上图的代码中返回的是一个“Pos*”指针,此时“it->_row”就可以看成“Pos*_row”。很明显,这样是无法访问数据的。这就说明“it->_row”的使用方式是错误的,正确的写法应该是“it->->_row”,这样才会被视为“Pos*->_row”。但是,如果我们用这种写法,就会导致写出的代码可读性较差,且会比较麻烦。因此,系统为了提高代码的可读性,就做了特殊处理,使我们重载了“->”后,在使用该运算符时可以省略一个“->”。因此,虽然写出来是“it->_row”,但是在系统中其实是被视为“it->->_row”的。

如果仅仅只是上述原因,其实并不需要在类模板中多加一个模板参数,其根本原因是,这里的“->”也可能传入const对象,以禁止对对应地址上的数据进行修改。由此,基于满足const和非const对象的需求,提供第三个类模板参数,以根据传入的第三个参数的类型进行不同的实例化:

你可能感兴趣的:(C++,#,stl库,c++,数据结构,开发语言,list)