STL库list::sort()实现深度解析
原创,转载请注明出处: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()实现深度解析的更多相关文章
- [转] C++的STL库,vector sort排序时间复杂度 及常见容器比较
http://www.169it.com/article/3215620760.html http://www.cnblogs.com/sharpfeng/archive/2012/09/18/269 ...
- 使用STL库sort函数对vector进行排序
使用STL库sort函数对vector进行排序,vector的内容为对象的指针,而不是对象. 代码如下 #include <stdio.h> #include <vector> ...
- Flink 源码解析 —— 深度解析 Flink 是如何管理好内存的?
前言 如今,许多用于分析大型数据集的开源系统都是用 Java 或者是基于 JVM 的编程语言实现的.最着名的例子是 Apache Hadoop,还有较新的框架,如 Apache Spark.Apach ...
- (转载)(收藏)OceanBase深度解析
一.OceanBase不需要高可靠服务器和高端存储 OceanBase是关系型数据库,包含内核+OceanBase云平台(OCP).与传统关系型数据库相比,最大的不同点, 是OceanBase是分布式 ...
- Kafka深度解析
本文转发自Jason’s Blog,原文链接 http://www.jasongj.com/2015/01/02/Kafka深度解析 背景介绍 Kafka简介 Kafka是一种分布式的,基于发布/订阅 ...
- java8Stream原理深度解析
Java8 Stream原理深度解析 Author:Dorae Date:2017年11月2日19:10:39 转载请注明出处 上一篇文章中简要介绍了Java8的函数式编程,而在Java8中另外一个比 ...
- mybatis 3.x源码深度解析与最佳实践(最完整原创)
mybatis 3.x源码深度解析与最佳实践 1 环境准备 1.1 mybatis介绍以及框架源码的学习目标 1.2 本系列源码解析的方式 1.3 环境搭建 1.4 从Hello World开始 2 ...
- 蓝鲸DevOps深度解析系列(2):蓝盾流水线初体验
关注嘉为科技,获取运维新知 前面一篇文章<蓝鲸DevOps深度解析系列(1):蓝盾平台总览>,我们总览了蓝鲸DevOps平台的背景.应用场景.特点和能力: 接下来我们继续解析蓝盾平台的 ...
- 深度解析 Vue 响应式原理
深度解析 Vue 响应式原理 该文章内容节选自团队的开源项目 InterviewMap.项目目前内容包含了 JS.网络.浏览器相关.性能优化.安全.框架.Git.数据结构.算法等内容,无论是基础还是进 ...
随机推荐
- 2015第43周一solr相关概念
Solr是一种开放源码的.基于Lucene的搜索服务器.它易于安装和配置,而且附带了一个基于HTTP 的管理界面. 官网:http://lucene.apache.org/solr/ solr学习 ...
- 数据结构(启发式合并):HNOI 2009 梦幻布丁
Description N个布丁摆成一行,进行M次操作.每次将某个颜色的布丁全部变成另一种颜色的,然后再询问当前一共有多少段颜色.例如颜色分别为1,2,2,1的四个布丁一共有3段颜色. Input 第 ...
- java基础(十三)常用类总结(三)
这里有我之前上课总结的一些知识点以及代码大部分是老师讲的笔记 个人认为是非常好的,,也是比较经典的内容,真诚的希望这些对于那些想学习的人有所帮助! 由于代码是分模块的上传非常的不便.也比较多,讲的也是 ...
- HDOJ/HDU 1297 Children’s Queue(推导~大数)
Problem Description There are many students in PHT School. One day, the headmaster whose name is Pig ...
- bzoj 3611 [Heoi2014]大工程(虚树+DP)
3611: [Heoi2014]大工程 Time Limit: 60 Sec Memory Limit: 512 MBSubmit: 408 Solved: 190[Submit][Status] ...
- 《A First Course in Probability》-chape1-组合分析-二项式定理
二项式系数的概念给人最直观的概念就是,这里有n个物品,分成两组,其中一组的数量是i的所有组合情况. 它的证明过程既可以从组合分析的角度,也可以从数学归纳的角度,由于数学归纳涉及到计算比较困难,我们这里 ...
- MongoDB Java 连接配置
[前言] 由于处于线程安全等考虑,MongoDBJava从3.0开始已经打算废弃DB开头的类的使用,所以整体调用上有了较大的区别,特以此文志之 [正文] 环境配置 在Java程序中如果要使用Mongo ...
- CAS学习笔记(三)—— SERVER登录后用户信息的返回
一旦CAS SERVER验证成功后,我们就会跳转到客户端中去.跳转到客户端去后,大家想一想,客户端总要获取用户信息吧,不然客户端是怎么知道登录的是哪个用户.那么客户端要怎么获取用户信息呢? 其实验证成 ...
- C语言学习_一个简单程序的解释与C学习方法概括
简单计算器程序示例: # include <stdio.h> //1.头文件 //2.加法函数 int add(int a,int b)//3.函数定义方式 { //4.函数体 retur ...
- 1388 - Graveyard(数论)
题目链接:1388 - Graveyard 题目大意:在一个周长为10000的圆形水池旁有n个等距离的雕塑,现在要再添加m个雕塑,为了使得n + m个雕塑等距离,需要移动一些雕塑,问如何使得移动的总位 ...