STL源码学习系列八: 序列式容器( List)

序列式容器( List)


前言

在SGI STL中,list容器是一个循环的双向链表,它的内存空间效率较前文介绍的vector容器高。相对于 vector 的连续线性空间,list 就显得复杂许多,他的好处是每次插入和删除一个元素,就配置和释放一个元素空间。因此,list 对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或元素移除,list 永远是常数时间。

与vector容器不同的是,list容器在进行插入操作或拼接操作时,迭代器并不会失效;且不能以普通指针作为迭代器,因为普通指针的+或-操作只能指向连续空间的后移地址或前移个地址,不能保证指向list的下一个节点,迭代器必须是双向迭代器,因为list容器具备有前移和后移的能力。

list 和 vector 是两个最常被使用的容器 , 什么时机最适合使用哪种容器 , 必须视元素的多寡,元素的构造 复杂度 ( 有无 non-trivial copy constructor, non-tirivial copy assigiunen operator) 、元素存取行为的特性而定。


list节点和list数据结构

在list容器中,list本身和list节点是分开设计的,list节点结构是存储数据和指向相邻节点的指针;如下源码所示:

template<class T>
struct _list_node
{
    typedef _list_node* void_pointer;
    void_pointer prev; //指向直接前驱节点  
    void_pointer next; //指向直接后继节点  
    T data; //节点存储的数据  
};

list本身的数据结构是只有一个指向链表节点的指针,因为list容器是循环双向链表,则足够遍历整个链表;如下源码所示:

//以下是双向链表list类的定义,分配器_Alloc默认为第二级配置器  
template <class T, class Alloc = alloc>
class list
{
    protected:
        typedef _list_node<T> list_node;

    public:
        typedef list_node* link_type;

    protected:
            link_type node; //list是一个环状双向链表,一个指针即可表示整个环状双向链表,指向尾端的空白节点
            ...
};

下面给出list节点和list本身的数据结构图:
这里写图片描述


list容器的迭代器

list容器的内存空间存储不一定是连续的,则不能用普通指针做为迭代器;list的迭代器要能够指向list的节点,并且可以进行正确的递增、递减、取值和成员存取等操作。所以迭代器要有前移、后退的能力,所以list的迭代器为双向迭代器。

这也是导致list容器的排序成员函数sort()不能使用STL算法中的排序函数,因为STL中的排序算法接受的迭代器是随机访问迭代器;

list容器在进行插入和拼接操作时迭代器不会失效;以下是源码对迭代器的定义:

#include "listNode.h"
#include "iterator.h"

namespace EasySTL
{
    //迭代器本身不是指针,因为List不是连续的区间
    template<class T,class Ref,class Ptr>
    struct _list_iterator
    {
        typedef _list_iterator<T,T&,T*> iterator; //指向内部元素值的迭代器
        typedef _list_iterator<T,Ref,Ptr> self;   //指向List节点的迭代器
        typedef bidirectional_iterator_tag iterator_category;
        typedef T value_type;
        typedef Ptr pointer;
        typedef Ref reference;
        typedef _list_node<T>* link_type;
        typedef size_t size_type;
        typedef ptrdiff_t difference_type;

        link_type node;  //普通指针指向节点

        //constructor
        _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;}

        //dereference 迭代器的取值,取的是节点的数据值
        reference operator*() const {return (*node).data;}

        //迭代器的成员存取运算的标准运算
        pointer operator->() const {return &(operator*());};

        //迭代器向前移动一个位置++
        self& operator++()
        {
            node=(link_type)((*node).next);  //node=node->next;
            return *this;
        }
        self operator++(int)
        {
            self tmp=*this;
            ++*this;
            return tmp;
        }

        //迭代器后退一个节点
        self& operator--()
        {
            node=node->prev;
            return *this;
        }
        self operator--(int)
        {
            self tmp=*this;
            --*this;
            return tmp;
        }

        self operator+(int dis)
        {
            self tmp=*this;
            while(dis-->0)
                tmp=tmp.node->next;
            return tmp;
        }

        self operator-(int dis)
        {
            self tmp=*this;
            while(dis-->0)
                tmp=tmp.node->prev;
            return tmp;
        }

    };
}

list容器的构造与析构

list缺省使用alloc二级空间配置器,每次配置、释放、构造和销毁一个空间。初始化时,会生成一个节点其next和prev指针都指向自己。

   namespace EasySTL 
   {
    template <class T, class Alloc = alloc>
    class list 
    {
     public:
            //配置一个节点并传回
        link_type get_node() 
        {
                return list_node_allocator::allocate();
        }

        //释放一个节点
        void put_node(link_type x) 
        {
            list_node_allocator::deallocate(x);
        }

        //产生一个节点,并且带有元素值
        link_type create_node(const T& x) 
        {
            link_type newListNode = get_node();
            construct(&newListNode->data, x); //所指的数据,创建
            return newListNode;
        }

        //销毁一个元素点 析构并释放
        void destroy_node(link_type d)
        {
            destroy(&d->data);
            put_node(d);
        }
        //构造函数
        list() { empty_initialize();} //产生空的链表
        ~list() 
        {
            clear();
            erase(end());
        }

    protected:
        link_type node;//list是一个环状双向链表,一个指针即可表示整个环状双向链表,指向尾端的空白节点

        void empty_initialize() 
        {
            node = get_node();
            node->next = node;
            node->prev = node;
        }
            ...
        };
       }

这里写图片描述


list容器的简单成员函数

list容器的成员函数为我们使用该容器提供了很大的帮助,所以这里对其进行讲解,首先先给出源码的剖析,然后在对其中一些重点的成员函数进行图文讲解;先讲解链表操作,插入,删除等:

        //尾部插入节点
        void push_back(const T& x) 
        { //在list尾部空元素处添加数据元素
            insert(end(), x);
        }

        //头部插入节点
        void push_front(const T& x) 
        {
            insert(begin(), x);
        }

        //删除头部节点
        void pop_front() 
        {
            erase(begin());
        }

        //删除尾部节点
        void pop_back() 
        {
        erase(--end());
        }

        //清除所有list节点
        void clear() 
        {
            iterator s = begin();
            while(s != end()) 
        {
                s = erase(s);
            }
            //空list的状态
            node->next=node;
            node->prev=node;
        }

        //删除所有值为x的节点
        void remove(const T& x) 
        {
            iterator s = begin();
            while(s != end()) 
        {
                if(*s == x)
                    s = erase(s);
                else
                    s++;
            }
        }

        //移除连续并相同的元素
        void unique() 
        {
            iterator first = begin();
            iterator last  = end();
            if(first == end) return;  //空链表
            iterator next = first;
            while(++next != last) 
        {
                if (*first == *next) 
                    erase(next);
                else 
                    first = next;
                next = first;
            }
        }

        iterator erase(iterator position) 
        {
            link_type next_node = position.node->next;
            link_type prev_node = position.node->prev;
            prev_node->next = next_node;
            destroy_node(position.node);
            return iterator(next_node);
        }

        iterator insert(iterator position, const T& x) 
        {   
            link_type tmp = create_node(x);
            tmp->next = position.node;
            tmp->prev = position.node->prev;
            position.node->prev->next = tmp;
            position.node->prev = tmp;
            return tmp;
        }

        //reverse颠倒顺序
        void reverse()
        {
            iterator start = begin();
            iterator stop  = end();
            if(size() == 0 || size() == 1)
                return;
            start++;
            while(start != stop) 
            {
                iterator next = start + 1;
                splice(begin(), start);
                start = next;
            }
        }

        //与x交换成员 交换完成后原来两个list上的迭代器要注意
        void swap(list& x)
        {
            link_type tmp = x.node;
            x.node = this->node;
            this->node = tmp;
        }

下面举一个例子对插入函数insert()进行图文分析:假设在以下list链表中节点5之前插入节点9,具体实现见下图步骤:注:图中的箭头旁边的数字表示语句的执行步骤

第一步:首先初始化节点9,并为其分配节点空间;
第二步:调整节点5指针方向,使其与节点9连接;
第三步:调整节点5的前驱结点7指针的方向,使其与节点9连接    

这里写图片描述

下面举一个例子对擦除函数erase()进行图文分析:假设在以下list链表中删除节点5,具体实现见下图步骤:图中的箭头旁边的数字表示语句的执行步骤

第一步:首先调整待删除节点直接前驱指针,使其指向待删除节点的直接后继节点;
第二步:调整待删除节点直接后继指针方向,使其指向待删除节点的直接前驱节点;
第三步:释放待删除节点对象,回收待删除节点内存空;

这里写图片描述


list容器的重要成员函数

transfer()
以下对迁移操作transfer()进行分析,该函数不是公共接口,属于list容器的保护成员函数,作用就是将特定范围的元素移动到position之前,说白了也是链表操作,只是看起来比较吃力。这个transfer作为内部的函数,为splice、sort、merge等奠定了良好的基础。它为拼接函数服务,拼接函数的核心就是迁移函数;

  //将[first,last)之间的元素移动到position之前
    void transfer(iterator position, iterator first, iterator last) 
    { 
        if (last == position)
            return;

           link_type last_node = last.node->prev;
           //将first last取下来
           first.node->prev->next = last.node;
           last.node->prev = first.node->prev;


           link_type prev_node = position.node->prev;
           prev_node->next = first.node;
           first.node->prev = prev_node;

           last_node->next = position.node;
           position.node->prev = last_node;
       }

transfer操作如下图:
这里写图片描述

splice()
transfer并非工开接口。splice提供所谓的接合操作:将某连续范围的元素从一个list移动到另一个list的某一定点。

    //x接合与position之前
      void splice(iterator position, list& x) 
      {
        if(x.empty())
            return;
          transfer(position, x.begin(), x.end());
      }

      //将某一个位置上的元素接合到position之前
      void splice(iterator position, iterator i)
      {
          iterator j = i;
          j++;
          transfer(position, i, j);
      }

      //
      void splice(iterator position, iterator first, iterator last) 
      {
          if(position == last)
            return;
        transfer(position, first, last);
      }

这里写图片描述

merge()
merge 将x 与*this合并,两个list必须经过递增排序

    void merge(list& x) 
    {
         iterator p = begin();
         iterator x_start = x.begin();

         while(p != end() && x_start != x.end()) 
         {
            std::cout<<*p<<std::endl;
              if(*x_start > *p)
                  p++;
              else {
                iterator next = x_start + 1; //注意:迭代器跟着节点跑,会脱离原来的list
                  splice(p, x_start);
                x_start = next; 
            }           
         }

         //如果x没完就添加到尾巴上
         if(!x.empty())
               splice(end(), x_start, x.end());
      }

sort()
list容器不能使用泛型算法sort(),因为list作为链表不支持随机存取,因此,list只能使用自己的sort,暂时写了插入排序,待完善:

//sort list不能使用STL的sort,应为他的迭代器是不是ramdon的
    //这里使用的是插入排序
    void sort() 
    {
        if(size() == 0 || size() == 1)
            return;
        list<T, Alloc> tmp;
        iterator q = begin();
        while(!empty()) 
        {
            iterator p = tmp.begin();
            while(p!= tmp.end() && *p < *q)
                p++;
            tmp.splice(p, q);
            q = begin();
        }
        //将tmp赋给本list
        swap(tmp);
    }

总结

与vector容器不同的是,list容器在进行插入操作或拼接操作时,迭代器并不会失效;且不能以普通指针作为迭代器,因为普通指针的+或-操作只能指向连续空间的后移地址或前移个地址,不能保证指向list的下一个节点,迭代器必须是双向迭代器,因为list容器具备有前移和后移的能力。

list 和 vector 是两个最常被使用的容器 , 什么时机最适合使用哪种容器 , 必须视元素的多寡,元素的构造 复杂度 ( 有无 non-trivial copy constructor, non-tirivial copy assigiunen operator) 、元素存取行为的特性而定。


End

猜你喜欢

转载自blog.csdn.net/qq_34777600/article/details/80685873