原创,转载请注明出处:STL库list::sort()实现深度解析

list模板的定义以及一些基本成员函数的实现这里我就不赘述了,还不清楚的同学可以到网上查找相关资料或者直接查看侯捷翻译的《STL源码剖析》相应章节。我之所以写这篇笔记是因为当时看到list::sort()源码时一时没看懂,后来在VS项目里一步步跟踪数据变化发现了其中的奥秘,被其简洁高效的非递归归并排序的实现方法所震撼(侯捷在《STL源码剖析》上注释说此sort实现使用了快排,应该是弄错了),下面直接进入主题。

list::sort() 源码(摘自《STL源码剖析》)

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;
list<T, alloc> 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]);
}

以上源码咋一看并没有发现元素大小比较的地方,但仔细一看便发现其使用了list的merge成员函数,这个函数功能是合并两个非减有序链表为一个非减有序链表,结果为调用该函数的对象。显然,sort函数的实现使用了归并排序。为了能在vs下调试跟踪数据变化,我新建了一个list.sort()的等价外部实现函数。

list.sort()的等价外部实现

void sortList(list<int> &a) {
if(a.size() <= 1){
return;
} list<int> carry; // 辅助链表,用于从a中提取元素以及临时保存两个链表的合并结果
list<int> counter[64]; // 保存着当前每一个归并层次的结果, i号链表保存的元素个数为2的i次方或者0
int fill = 0; // 表示当前最大归并排序的层次,while循环之后fill变成log2(a.size()) while (!a.empty()) {
carry.splice(carry.begin(), a, a.begin()); // 将链表a中的第一个元素移动至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) { // i到达当前最大归并层次,说明得增加一层
++fill;
}
} for (int i = 1; i < fill; ++i) { // 将所有归并层次的结果合并得到最终结果counter[fill - 1]
counter[i].merge(counter[i - 1]);
}
a.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。

第i次循环之后 counter[0] counter[1] counter[2] counter[3]
0 8 null null null
1 null 6,8 null null
2 520 6.8 null null
3 null null 6,8,27,520 null
4 124 null 6,8,27,520 null
5 null 124,214 6,8,27,520 null
6 688 124,214 6,8,27,520 null
7 null null null 6,8,12,27,124,214,520,688
8 36 null null 6,8,12,27,124,214,520,688

前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数组上,具体实现以后有时间再写。

附调试完整代码

#include <iostream>
#include <list>
using namespace std; // list<T>.sort()等价外部实现,用到了归并排序的算法思想
void sortList(list<int> &a) {
if(a.size() <= 1){
return;
} list<int> carry; // 辅助链表,用于从a中提取元素以及临时保存两个链表的合并结果
list<int> counter[64]; // 保存着当前每一个归并层次的结果, i号链表保存的元素个数为2的i次方或者0
int fill = 0; // 表示当前最大归并排序的层次,while循环之后fill变成log2(a.size()) while (!a.empty()) {
carry.splice(carry.begin(), a, a.begin()); // 将链表a中的第一个元素移动至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) { // i到达当前最大归并层次,说明得增加一层
++fill;
}
} for (int i = 1; i < fill; ++i) { // 将所有归并层次的结果合并得到最终结果counter[fill - 1]
counter[i].merge(counter[i - 1]);
}
a.swap(counter[fill - 1]);
} int main() {
list<int> test;
test.push_back(8);
test.push_back(6);
test.push_back(520);
test.push_back(27);
test.push_back(124);
test.push_back(214);
test.push_back(688);
test.push_back(12);
test.push_back(36);
cout << "排序前" << endl;
for (auto i = test.begin(); i != test.end(); i++) {
cout << *i << " ";
}
cout << endl << "排序后:" << endl;
sortList(test);
for (auto i = test.begin(); i != test.end(); i++) {
cout << *i << " ";
}
cout << endl; return 0;
}

STL库list::sort()实现深度解析的更多相关文章

  1. [转] C++的STL库,vector sort排序时间复杂度 及常见容器比较

    http://www.169it.com/article/3215620760.html http://www.cnblogs.com/sharpfeng/archive/2012/09/18/269 ...

  2. 使用STL库sort函数对vector进行排序

    使用STL库sort函数对vector进行排序,vector的内容为对象的指针,而不是对象. 代码如下 #include <stdio.h> #include <vector> ...

  3. Flink 源码解析 —— 深度解析 Flink 是如何管理好内存的?

    前言 如今,许多用于分析大型数据集的开源系统都是用 Java 或者是基于 JVM 的编程语言实现的.最着名的例子是 Apache Hadoop,还有较新的框架,如 Apache Spark.Apach ...

  4. (转载)(收藏)OceanBase深度解析

    一.OceanBase不需要高可靠服务器和高端存储 OceanBase是关系型数据库,包含内核+OceanBase云平台(OCP).与传统关系型数据库相比,最大的不同点, 是OceanBase是分布式 ...

  5. Kafka深度解析

    本文转发自Jason’s Blog,原文链接 http://www.jasongj.com/2015/01/02/Kafka深度解析 背景介绍 Kafka简介 Kafka是一种分布式的,基于发布/订阅 ...

  6. java8Stream原理深度解析

    Java8 Stream原理深度解析 Author:Dorae Date:2017年11月2日19:10:39 转载请注明出处 上一篇文章中简要介绍了Java8的函数式编程,而在Java8中另外一个比 ...

  7. mybatis 3.x源码深度解析与最佳实践(最完整原创)

    mybatis 3.x源码深度解析与最佳实践 1 环境准备 1.1 mybatis介绍以及框架源码的学习目标 1.2 本系列源码解析的方式 1.3 环境搭建 1.4 从Hello World开始 2 ...

  8. 蓝鲸DevOps深度解析系列(2):蓝盾流水线初体验

    关注嘉为科技,获取运维新知 前面一篇文章<蓝鲸DevOps深度解析系列(1):蓝盾平台总览>,我们总览了蓝鲸DevOps平台的背景.应用场景.特点和能力: ​ 接下来我们继续解析蓝盾平台的 ...

  9. 深度解析 Vue 响应式原理

    深度解析 Vue 响应式原理 该文章内容节选自团队的开源项目 InterviewMap.项目目前内容包含了 JS.网络.浏览器相关.性能优化.安全.框架.Git.数据结构.算法等内容,无论是基础还是进 ...

随机推荐

  1. Android adb opendir failed ,permission denied

    最近在使用adb命令ls的时候会提示:adb opendir failed ,permission denied , 解决方法: adb shell cd data/data/pakageName l ...

  2. js 的 提交

    <script type="text/javascript"> function sub(){ if(document.form1.xingming.value==&q ...

  3. 利用NSIS软件制作C#安装包

    最近在做C#程序安装包,结果网上看到这个软件还是不错的,可以尝试以下. NSIS 是“Nullsoft 脚本安装系统”(Nullsoft Scriptable Installation System) ...

  4. 关于将客户端移植到Lua的解决方案设想。

    现在发行商都需要cp们做热更新,而对于unity制作的游戏来讲,这个恐怕是个噩梦,而项目已经进行到中后期,确实很麻烦,有UniLua,但是如果全部手动解决恐怕上不了线了工作量太大,初步设想如果做一个基 ...

  5. php表单提交方法汇总

    问题:网页上提交表单之后,PHP为什么不能获取提交的内容?然而在老版本的PHP上运行却正常. 新版的PHP已经废弃了原来的表单内容处理方式,即不再把提交的表单的内容直接复制到一个同名变量中.解决办法有 ...

  6. flexpaper 背景色变化

    1.mxml文件头部:添加 backgroundAlpha="0" <s:Application xmlns:fx="http://ns.adobe.com/mxm ...

  7. 1242Rescue (优先队列BFS)

    Problem Description Angel was caught by the MOLIGPY! He was put in prison by Moligpy. The prison is ...

  8. 406. Queue Reconstruction by Height

    一开始backtrack,设计了很多剪枝情况,还是TLE了 ..后来用PQ做的. 其实上面DFS做到一半的时候意识到应该用PQ做,但是不确定会不会TLE,就继续了,然后果然TLE了.. PQ的做法和剪 ...

  9. AFNetworking2.0 NSHipster翻译

    AFNetworking 是当前 iOS 和 Mac OS X 开发中最广泛使用的开源项目之一.它帮助了成千上万叫好又叫座的应用,也为其它出色的开源库提供了基础.这个项目是社区里最活跃.最有影响力的项 ...

  10. 【Android - 框架】之Fresco的使用

    当下有很多图片加载框架,常见的有Glide.Fresco.Picasso等.Glide因为其体积小.缓存机制强大等优点,受到了广大程序员的青睐:Fresco虽然体积比较大,缓存机制也没有Glide强大 ...