STL源码剖析——序列式容器

来源:互联网 发布:mac队全灭动态图解 编辑:程序博客网 时间:2024/05/21 08:48

序列式容器

       序列式容器,其中的元素都可序,但未必有序。C++语言本身提供一个序列式容器array,STL另外提供vector,list,deque,stack,queue,priority-queue等。

 Vector

       vector实现的底层实现对象是数组,它的数据安排以及操作方式与array非常相似,两者的唯一差别在于空间的运用的灵活性。Array是静态空间,一旦配置就不能改变,若要换大空间则需要客户端自己来操作:分配新空间,将元素一一搬往新址,再把原来的空间释还给系统。Vector是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。

       vector的实现技术,关键在于对其大小的控制以及重新配置时的数据移动效率。Vector维护的是一个连续线性空间,所以普通指针可以作为vector的迭代器而满足所有必要条件,支持随机存取。

       Vector所采用的数据结构是线性连续空间,vector内部有3个iterator:start,finish,end_of_storage,分别表示容器的可使用空间头、目前已使用空间尾和目前和目前可使用空间尾。Vector的容量永远大于或等于其大小。其最关键的技术在于内存的管理。

       下面这个例子是vector在尾部插入一个元素的时候的实际操作:

template <class T, class Alloc>
void vector<T, Alloc>::insert_aux(iterator position, const T &x)
{
    if(finish!=end_of_storge)    //还有备用空间
    {
        construct(finish, *(finish-1));    //在备用空间处构造一个元素,并以最后一个元素为它的初值
        ++finish;
        T x_copy = x;
        copy_backward(position, finish-2, finish-1);//后移
        *position = x_copy;
    }
    else                        //已经没有备用空间
    {
        const size_type old_size = size();
        const size_type len = old_size!=0? 2*old_size:1;
        //长度扩充到2
        iterator new_start = data_allocator::allocate(len);
        iterator new_finish = new_start;
        try
        {
            //拷贝
            new_finish = uninitialized_copy(start, position, new_start);
            //为新元素设值
            construct(new_finish, x);
            ++new_finish;
            //将原vector的备用空间内容也拷贝过来(不知用途???)
            new_finish = uninitialized_copy(position, finish, new_finish);
        }
        catch()
        {
            destroy(new_start, new_finish);
            data_allocator::deallocate(new_start, len);
            throw;
        }

        //析构释放原vector
        destroy(begin(), end());
        deallocate();

        //调整迭代器
        start = new_start;
        finish = new_finish;
        end_of_storage = new_start + len;
    }
}

         所谓的动态增加大小,并不是在元空间之后接续新空间(因为无法保证原空间之后尚有可供配置的空间),而是申请一个两倍的空间、用以前空间的元素将新空间的前半部分copy初始化、在原内容之后构造新元素、析构释放原空间、调整迭代器。

List

       List每次插入或删除一个元素,就配置或释放一个元素空间,而且永远是常数时间。List不再能够想vector一样以普通指针作为迭代器,因为其节点不保证在存储空间中连续存在。List的插入操作和接合操作都不会造成原油的list迭代器失效,这在vector是不成立的,因为vector的插入操作可能造成记忆体重新配置,导致原油的迭代器全部失效。

       list是一个双向链表,而且在空间上不是连续储存的,所以需要自定义迭代器,迭代器应该是Bidirectional iterators。除此之外,list 还被设计成一个循环链表,表尾加上一个空白的节点,使其符合“前闭后开”的规则,这样就只需要一个标记,就可以完整的表示整个链表。

       list内部提供了一个很重要的迁移操作(transfer):将某连续范围的元素迁移到某个特定的位置之前,技术只是用到了节点间的指针移动,这个操作为后面的splice、merge、sort等奠定了良好的基础。list的sort方法:list不能使用stl提供的通用sort算法,因为sort算法只接受Random Access Iterator,所以提供了自己的sort方法。

template<class T, class Alloc>
void list<T, Alloc>::sort()
{
    //以下判断如果是空链表或者仅有一个元素,就不进行操作
    if(node->next==node||link_type(node->next)->next==node)
        return ;

    //一些新的lists,用于中介数据的存放
    //counter[0]~counter[63]
分别存储124 ~ (2^64 -1 个元素。都是已序状态(整体不一定已序)。
    //
每次从list取出来一个元素,就将其与counter[0]进行merge操作,如果counter[0]的元素>1,则将
    //counter[0]
counter[1]进行merge操作,直到每个counter[]内的元素不超过前面说的数量。carry
    //
进行中转的一个临时listfill则表示counter现已用了多少个。
    list<T, Alloc> carry;
    list<T, Alloc> counter[64];
    int fill=0;
    while(!empty())
    {
        //将原来的list的元素一个个的取出来
        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;
    }
    //最后将counter内的所有元素合并到一起。
    for(int i=1; i<fill; ++i)
        counter[i].merge(counter[i-1]);
    swap(counter[fill-1]);
}

       个人理解是:由于list不支持随机读取数据,因此如果按照传统的merge sort,获取中间的元素将会耗时很多,因此才有了此改编的merge sort。看别人文章,此算法应该也是Nlog(N)的时间复杂度。空间上由于是直接对节点进行的操作,好像不需要多空间存储临时元素。

Deque

       deque相对于vector而言,不同之处是一个双向开口的连续线性空间,可以在头尾两端分别作元素的插入和删除操作。deque的数据结构:


       deque的连续线性空间其实只是一个假象,其实deque是由一段段的定量连续空间组成,由一个中控器map(非STLmap)联系起来,map中的每个元素指向一段定量的连续线性空间,称为缓冲区,缓冲区才是deque的存储空间主体。当需要在头或者尾扩充的时候,它动态地分段连续空间组合而成,随时可以增加一段新的空间并链接起来。所以没必要提供所谓的空间保留功能。

       它的迭代器并不是普通指针,复杂度相当高,所以deque在效率上来说是不够vector好的,因此有时候在对deque进行sort的时候,需要先将元素移到vector再进行sort,然后移回来。

       一旦有必要在deque的前端或尾端增加新空间,便配置一段定量连续空间,串接在整个deque的头端或尾端。它的最大任务是在这些分段的定量连续空间上维护其整体连续的假象,并提供随机存取的接口。避开“重新配置、赋值、释放”的轮回,代价则是复杂的迭代器架构。

       Deque迭代器应该具有的结构。首先,它必须能够指出分段连续空间在哪里,其次它必须能够判断自己是否已经处于其所在缓冲区的边远,如果是,一旦进行或后退时就必须跳跃至下一个或上一个缓冲区,为了能够正确跳跃,deque必须随时掌握管控中心(map)。

       deque的构造与内存管理:由于deque的设计思想就是由一块块的缓存区连接起来的,因此它的内存管理会比较复杂。插入的时候要考虑是否要跳转缓存区、是否要新建map节点(和vector一样,其实是重新分配一块空间给map,删除原来空间)、插入后元素是前面元素向前移动还是后面元素向后面移动(谁小移动谁)。而在删除元素的时候,考虑是将前面元素后移覆盖需要移除元素的地方还是后面元素前移覆盖(谁小移动谁)。移动完以后要析构冗余的元素,释放冗余的缓存区。

Stack

       stack是一种先进后出的数据结构,只可以在顶端进行元素操作。它以某种既有容器作为底部结构,将其接口改变,使之符合“先进后出”特性,被归类为container adapter(配接器)。SGI STL缺省的以deque做stack的底部结构,若要改变stack的内部结构只要给它的类模板指定第二个参数就可以了.例如:stack<int,list<int> > x;  stack还有一个特性就是没有迭代器:因为只能在栈顶进行操作,所以不必要定义迭代器,也不能遍历。

Queue

         queue是一宗先进先出的数据结构,它的实现方案和stack基本一样。也是属于container adapter一类。可以基于dequelist。它同样没有迭代器。

Heap

       heap并不是stl中的一种容器,它实际是以vector作为完全二叉树的存储结构,再加上一些heap算法。它是priority_queue的底层实现。binary heap:一种完全二叉树。可以用数组表示。如果数组的第一个元素保留(设为无穷大或者无穷西欧啊),从第二个元素开始储存,则有:对于i处的节点,其子节点必定位于2i和2i+1处,其父节点位于i/2处。Heap操作包括建堆,push_heap,pop_heapsort_heap等。

       push_heap算法:

       1.新插入的元素放入完全二叉树的最下层.并填补从左到右的第一个空位.(如果是用数组表示完全2叉树.则放入vector的end()处。
         2.新插入的元素是否适合现有位置?如果是最大堆.则要求它小于其父节点的值.否则和父节点交换.如此一直上朔.直到它小于父节点的值。

       pop_heap算法:

         1. 交换根节点和vector最后一个元素.(即将最后元素放入根.将根放入vector最后)。
         2.如果是最大堆. 则根节点的值要大于子节点的值. 所以将此元素与其子节点中值较大的节点交换. 如此一直下朔. 直到它大于两个子节点。

       sort_heap算法:

         因为每当pop_heap时.会将根(最大元素)放入vector的最后位置.所以可以持续对整个heap做pop_heap操作,每次将操作范围缩小一个元素。这样当sort_heap执行完毕后, vector中的元素就成了递增的了。

       make_heap算法:

       这个算法用来将一段现有的数据转化为一个heap。因为所有的叶子节点都不需要调整,所以从N/2处往上进行调整。

PriorityQueue

       priority queue是一种拥有权值概念的queue,因此取出元素的时候只能取出权值最大的元素。priority_queue也是container adapter,它以vector为存储结构,再以heap算法进行处理:priority_queue的元素存放在一个vector当中,按元素权值排列成一个heap。priority_queue也没有迭代器,不能遍历。

Slist

       slist 不是STL容器。只是甴SGISTL提供的单向链表,而list是属于双向链表。它可以快速在链头处插入/删除。单向链表只有next的指针,所以能节省些空间,但是这也导致它的insert等算法效率很差。因为insert根据习惯是在指定迭代器之前插入。而该迭代器之前的位置只有从开始遍历slist来计算了。为此slist提供了insert_after函数和erase_after函数。

       slist特点:节点和迭代器设计运用了继承关系,比list要复杂。(为什么要用继承?)

原创粉丝点击