STL list

与vector的连续线性空间比,list 的好处是每次插入或删除一个元素,就配置或释放一个元素空间,对空间的利用很精准,且插入或删除都是常数时间。
list 和vector是最常用的容器。STL list中 list是一个双向链表,其有一个重要的性质是插入操作(insert)和接合操作(splice)都不会造成原有的list迭代器失效,这在vector是不成立的,list的元素删除操作只有指向被删除元素的那个迭代器失效,其他迭代器不会受影响。

list的数据结构

SGI中的list不仅是一个双向链表,而且还是一个环状的双向链表,只需要一个指针就能完整表现整个链表。 做法是让node指向刻意置于尾端的一个空白节点,node便能符合STL对于前闭后开区间的要求,成为list迭代器。此时 end()就是node, begin()就是(*node).next; 如果node->next==node,意味这里链表为空, 按照这个逻辑就能设计list
在这里插入图片描述

list提供了许多构造函数,其中一个是允许不指定任何参数作出一个空的list,也就是配置node,再指定node的头尾都为自己。
list的push_front() 、push_back()都是调用的insert()函数,insert函数的具体做法就是和普通的双向链表的插入一样,要注意前后节点关系的改变。pop_front() pop_back()函数则是调用erase()函数,删除的做法也和双向链表一致 都不做原理解析。
clear()函数清空所有节点 remove(const T&value)函数则把value的元素都删除。
unique()移除数值相同的连续元素,只有连续的相同才会被移除剩一个。

list内部提供一个迁移操作(transfer),将连续范围的元素迁移到某个特定的位置之前,就是节点间的指针移动而已。代码如下,可供学习

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
	}
}

在这里插入图片描述
按着以上图解来看代码就不难了 其中的指针移动可以好好去学习。

这个transfer并非公开接口,但是是实现以下函数的基础。list 提供的是接合操作 splice(),将某连续范围的元素从一个list移动到另一个或同一个list的某一个定点。splice(iterator positon,list&,iterator first,iterator last) 将[first,last)的内容结合于position位置之前,可以是同一个list或不同的list
list提供了merge,用于将x合并到this身上,采用了归并排序的方法。reverse()同样也是用到了transfer(),就是从第二个元素开始依次插入到begin()前面。

在list中,最不好理解的函数源码是sort函数。同时书本写的采用快排方法这是错误的,根据查找学习以及自己思考,参照了http://www.cnblogs.com/avota/p/5388865.html的解析,终于弄懂了,以下来自查找的博客。

template <class T, class Alloc>
void list<T, Alloc> :: sort(){
    // 判断链表是否为空或者只有一个元素
    if(node->next == node || link_type(node->next)->next == node){
        return;
    }
    
    list<T, Alloc> carry;// 辅助链表,用于从*this中提取元素以及临时保存两个链表的合并结果
    list<T, alloc> counter[64];// 保存着当前每一个归并层次的结果, i号链表保存的元素个数为2的i次方或者0
    int fill = 0;
    while(!empty()){
        carry.splice(carry.begin(), *this, begin());//将链表的第一个元素移动到carry
        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]);
        // 将所有归并层次的结果合并得到最终结果counter[fill - 1]
    }
    swap(counter[fill-1]); 
}

算法的巧妙之处在于外层while循环下counter链表数组的维护,下面我们就用例子a(8, 6, 520, 27, 124, 214, 688, 12, 36 )来跟踪counter的变化。事先约定,null表示list不含元素,下面所说的第i次循环之后均指外层while的。a的元素个数为9,归并层次最多到达第4层,故counter[3]之后的就不显示了, 它们的值均为null。
在这里插入图片描述
前3次循环的具体运行过程如下:

第0次外层循环,carry取得a列表头元素8,i == fill == 0 无法进入内层循环,之后carry与counter[0]交换,counter[0] 变为8, fill变为1;
第1次外层循环, carry取得a列表头元素6,counter[0]不为空故进入内层循环,合并carry和counter[0],内层一次循环之后counter[0]变为null, carry变为(6,8), i == fill == 1退出内层循环。然后carry与counter[1]交换,最后counter[1]变为(6, 8), fill 变为2;
第2次外层循环, carry取得a列表头元素520,counter[0]为空无法进入内层循环,之后carry与counter[0]交换,counter[0] 变为520, fill的值不变;
第3次外层循环,carry取得a列表头元素27,进入内层while循环,先是发现counter[0]不为空,故与其合并,合并之后carry变为(27, 520), counter[0]变为null,然后进入下一次内层循环发现counter[1]不为空,故与其合并,合并之后carry变为(6,8,27,520), counter[1]变为null,i == fill == 2退出内层循环。最后carry与counter[2]互换,counter[2]变为(6,8,27,520),fill变为3.
之后的循环过程类似,最后将counter[0]至counter[8]的结果合并即为结果。此算法的时间复杂度为O(N*logN),空间复杂度为O(N).

总结
传统归并排序使用先二分后调用递归函数的步骤,应用对象主要是普通数组和vector数组,这两者的共同点在于可以在O(1)的时间内找到中点。但分析list数据结构可知,寻找其中点需要O(N)复杂度,故不大适合使用传统归并排序的思想。后来不知哪位牛人想到了利用二进制的进位思想,结合一个list数组保存各个归并层次的结果,最终实现了非递归版的归并排序,此想法也可以用在普通数组和vector数组上,具体实现以后有时间再写。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值