STL heap部分源码分析
2015-08-19 12:15
459 查看
本文假设你已对堆排序的算法有基本的了解。
要分析stl中heap的源码的独到之处,最好的办法就是拿普通的代码进行比较。话不多说,先看一段普通的堆排序的代码:
对应第一个函数max_heap(),stl中有一个功能类似的函数adjust_heap(),也是调整整个heap,使之符合大顶堆的要求,代码如下:
stl里算法的堆的调整的主要流程如注释里所说,主要是先进行下溯,直到叶节点,然后用push_heap进行上溯才调增完毕。对比之前的普通代码,主要有3点改变:
把普通代码的递归操作变成了循环操作。这点很好理解,因为递归需要系统使用资源来维护递归栈,开销比较大,所以stl中除了sort的快排之外(因为快排的递归深度有限制),一般都会把递归的算法转换成循环来做。
普通代码中,我们直接比较根节点和左右节点的值,然后如果需要的话跟节点直接和较大的节点交换,这样只需要从上到下一趟比较,就能完成树的调整。而stl的代码中,则要先下溯,然后再上溯,两趟才能完成调整,看起来反而效率更低了,为什么呢?我仔细分析了代码,感觉可能有一下两点的原因:1.代码的复用,因为下溯的主要作用其实在保证大顶堆性质的前提下,让要调整的那个节点从根节点开始下沉,造成了一个新插入节点的假象,而push_heap()正是为了应对新插入节点而写的一个函2.数,这就复用了这块的代码。2.效率上的一点提升,因为下溯的过程只需要两个子节点的一次比较和根节点的一次赋值,而上溯的过程也只需要与根节点的一次比较和子节点的一次赋,所以合起来其实是两次赋值和两次比较;而普通的代码中,如果不考虑边界检查,找出三个点里的最大值,并与之交换,则至少需要两次比较,和三次赋值,所以stl的算法中,赋值运算少了一次,效率有所提升。
普通代码中,每次都需要对左子节点和右子节点进行边界的检查,而stl代码中,只对右子节点进行边界检查,为了防止右子节点越界而左子节点没有越界的情况发生,在循环结束后增加了对左子节点的边界检查。这一修改大幅度减少了边界检查的次数,明显提升了效率。
通过上述的分析,其实我们也可以以小见大。其实stl中,存在着大量这样的优化,递归转循环,减少边界检查,用赋值代替交换等等,如果我们能仔细研究,并在平时的编码中也养成这样的习惯,就能极大得提升代码的效率。
要分析stl中heap的源码的独到之处,最好的办法就是拿普通的代码进行比较。话不多说,先看一段普通的堆排序的代码:
//调整大顶堆,使得结构合理 void max_heap(int a[],int node,int size) { int lg=node; int l=node*2; int r=node*2+1; if(l<=size&&a[lg]<a[l]) { lg=l; } if(r<=size&&a[lg]<a[r]) { lg=r; } if(lg!=node) { //a[lg]=a[lg]^a[node];//交换 //a[node]=a[lg]^a[node]; //a[lg]=a[lg]^a[node]; int tt=a[lg]; a[lg]=a[node]; a[node]=tt; max_heap(a,lg,size); } } //生成一个大顶堆 void make_heap(int a[],int size) { for(int i=size/2;i>0;i--) { max_heap(a,i,size); } } //堆排序,使数据在数组中按从小到大的顺序排列 void heap_sort(int a[],int size) { make_heap(a,size); for(int i=1;i<size;i++) { int tt=a[1]; a[1]=a[size-i+1]; a[size-i+1]=tt; max_heap(a,1,size-i); } }
对应第一个函数max_heap(),stl中有一个功能类似的函数adjust_heap(),也是调整整个heap,使之符合大顶堆的要求,代码如下:
// ============================================================================ // 保持堆的性质 //整个的过程是从根节点开始,将根节点和其子节点中的最大值对调,一直到叶节点为止(称之为下溯) //然后,再从这个根节点开始,把它与其父节点进行比较,如果比父节点大,则父子节点对调,直到根节点为止(称之为上溯) //============================================================================ // first 起始位置 // holeIndex 要进行调整操作的位置 // len 长度 // value holeIndex新设置的值 template <class _RandomAccessIterator, class _Distance, class _Tp> void __adjust_heap(_RandomAccessIterator __first, _Distance __holeIndex, _Distance __len, _Tp __value) { // 当前根节点的索引值 _Distance __topIndex = __holeIndex; // 右孩子节点的索引值 _Distance __secondChild = 2 * __holeIndex + 2; // 如果没有到末尾 while (__secondChild < __len) { // 如果右孩子节点的值比左孩子节点的值要小,那么secondChild指向左孩子 if (*(__first + __secondChild) < *(__first + (__secondChild - 1))) __secondChild--; // 子节点的往上升 *(__first + __holeIndex) = *(__first + __secondChild); // 继续处理 __holeIndex = __secondChild; __secondChild = 2 * (__secondChild + 1); } // 如果没有右子节点 if (__secondChild == __len) { *(__first + __holeIndex) = *(__first + (__secondChild - 1)); __holeIndex = __secondChild - 1; } // 针对节点topIndex调用push_heap操作 __push_heap(__first, __holeIndex, __topIndex, __value); } //上溯 __push_heap(_RandomAccessIterator __first, _Distance __holeIndex, _Distance __topIndex, _Tp __value) { // 获取父节点的索引值 _Distance __parent = (__holeIndex - 1) / 2; // 如果还没有上升到根节点,且父节点的值小于待插入节点的值 while (__holeIndex > __topIndex && *(__first + __parent) < __value) { // 父节点下降到holeIndex *(__first + __holeIndex) = *(__first + __parent); // 继续往上检查 __holeIndex = __parent; __parent = (__holeIndex - 1) / 2; } // 插入节点 *(__first + __holeIndex) = __value; }
stl里算法的堆的调整的主要流程如注释里所说,主要是先进行下溯,直到叶节点,然后用push_heap进行上溯才调增完毕。对比之前的普通代码,主要有3点改变:
把普通代码的递归操作变成了循环操作。这点很好理解,因为递归需要系统使用资源来维护递归栈,开销比较大,所以stl中除了sort的快排之外(因为快排的递归深度有限制),一般都会把递归的算法转换成循环来做。
普通代码中,我们直接比较根节点和左右节点的值,然后如果需要的话跟节点直接和较大的节点交换,这样只需要从上到下一趟比较,就能完成树的调整。而stl的代码中,则要先下溯,然后再上溯,两趟才能完成调整,看起来反而效率更低了,为什么呢?我仔细分析了代码,感觉可能有一下两点的原因:1.代码的复用,因为下溯的主要作用其实在保证大顶堆性质的前提下,让要调整的那个节点从根节点开始下沉,造成了一个新插入节点的假象,而push_heap()正是为了应对新插入节点而写的一个函2.数,这就复用了这块的代码。2.效率上的一点提升,因为下溯的过程只需要两个子节点的一次比较和根节点的一次赋值,而上溯的过程也只需要与根节点的一次比较和子节点的一次赋,所以合起来其实是两次赋值和两次比较;而普通的代码中,如果不考虑边界检查,找出三个点里的最大值,并与之交换,则至少需要两次比较,和三次赋值,所以stl的算法中,赋值运算少了一次,效率有所提升。
普通代码中,每次都需要对左子节点和右子节点进行边界的检查,而stl代码中,只对右子节点进行边界检查,为了防止右子节点越界而左子节点没有越界的情况发生,在循环结束后增加了对左子节点的边界检查。这一修改大幅度减少了边界检查的次数,明显提升了效率。
通过上述的分析,其实我们也可以以小见大。其实stl中,存在着大量这样的优化,递归转循环,减少边界检查,用赋值代替交换等等,如果我们能仔细研究,并在平时的编码中也养成这样的习惯,就能极大得提升代码的效率。
相关文章推荐
- 139. Word Break
- [leetcode] 240 Search a 2D Matrix II
- 下载安装并修改wamp配置文件
- 使用Python将csv文件批量化导入SQL Server
- hibernate持久化对象的三个状态
- bzoj-2594 水管局长数据加强版
- LeetCode OJ 之 Longest Substring Without Repeating Characters 解题报告
- Android自定义View的注意点
- java TreeMap 排序 与 TreeSet 排序
- oracle的引用型变量和记录型变量程序举例
- OpenSSL使用方法
- LeetCode之Binary Tree Maximum Path Sum
- [MetaHook] BaseUI hook
- python 文件操作
- 旁观者效应
- ios开发之视图控制器(UIViewController)-- 详解
- string基本字符序列容器
- JDK运行.Jar文件的控制台命令是什么
- aauto学习系列之<7>控制语句2
- 虚拟机安装redhat--环境搭建