原创,转载请注明出处: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. Google Map API 学习四

  2. ASP.Net 验证视图状态 MAC 失败

    错误信息: 验证视图状态 MAC 失败.如果此应用程序由网络场或群集承载,请确保 <machineKey> 配置指定了相同的 validationKey 和验证算法.不能在群集中使用 Au ...

  3. Lowest Common Ancestor of a Binary Tree——Leetcode

    Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree. According ...

  4. P1082丛林探险

    P1082丛林探险 描述 东非大裂谷中有一片神秘的丛林,是全世界探险家的乐园,著名黄皮肤探险家BB一直想去试试.正好我国科学家2005年4月将首次对东非大裂谷进行科考,BB决定随科考队去神秘丛林探险. ...

  5. 从Java视角理解CPU缓存(CPU Cache)

    从Java视角理解系统结构连载, 关注我的微博(链接)了解最新动态众所周知, CPU是计算机的大脑, 它负责执行程序的指令; 内存负责存数据, 包括程序自身数据. 同样大家都知道, 内存比CPU慢很多 ...

  6. 敏捷开发松结对编程系列:L型代码结构案例StatusFiltersDropdownList(中)

    这是松结对编程的第22篇(专栏目录). 接前文 业务代码 比较长,基本上就是看被注释隔开的三大段,先显示状态群筛选链接,然后是单个状态筛选,然后是显示下拉框的当前选中项,最后显示下拉框. public ...

  7. dom0的cpu hotplug【续】

    上一篇说到,手动xm vcpu-pin住,在hotplug就好了. 本质上,还是因为代码有bug,导致vcpu offline的时候,信息没有清理干净,有残留,当vcpu online的时候,如果调度 ...

  8. html5之canvas困惑 在canvas标签内需要设置了宽跟高,如果在css中设置同样的宽跟高,画出来的图像变形了?

    <canvas class="cvs"></canvas> 遇到的问题: 如css 中设.cvs{width:500px;height:400px;},也就 ...

  9. windows 下删除.svn文件

    Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\DeleteSVN] @= ...

  10. C程序中唯一序列号的生成

    在实际的软件开发项目中.常常会涉及唯一序列号的生成.本文以一个实际的程序为例,介绍了唯一序列号的生成过程. 本文生成的序列号的样式为:MMDDHHMINSS_XXXXXX. 程序例如以下: /**** ...