STL heap部分源码分析

本文假设你已对堆排序的算法有基本的了解。

要分析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点改变:

  1. 把普通代码的递归操作变成了循环操作。这点很好理解,因为递归需要系统使用资源来维护递归栈,开销比较大,所以stl中除了sort的快排之外(因为快排的递归深度有限制),一般都会把递归的算法转换成循环来做。
  2. 普通代码中,我们直接比较根节点和左右节点的值,然后如果需要的话跟节点直接和较大的节点交换,这样只需要从上到下一趟比较,就能完成树的调整。而stl的代码中,则要先下溯,然后再上溯,两趟才能完成调整,看起来反而效率更低了,为什么呢?我仔细分析了代码,感觉可能有一下两点的原因:1.代码的复用,因为下溯的主要作用其实在保证大顶堆性质的前提下,让要调整的那个节点从根节点开始下沉,造成了一个新插入节点的假象,而push_heap()正是为了应对新插入节点而写的一个函2.数,这就复用了这块的代码。2.效率上的一点提升,因为下溯的过程只需要两个子节点的一次比较和根节点的一次赋值,而上溯的过程也只需要与根节点的一次比较和子节点的一次赋,所以合起来其实是两次赋值和两次比较;而普通的代码中,如果不考虑边界检查,找出三个点里的最大值,并与之交换,则至少需要两次比较,和三次赋值,所以stl的算法中,赋值运算少了一次,效率有所提升。
  3. 普通代码中,每次都需要对左子节点和右子节点进行边界的检查,而stl代码中,只对右子节点进行边界检查,为了防止右子节点越界而左子节点没有越界的情况发生,在循环结束后增加了对左子节点的边界检查。这一修改大幅度减少了边界检查的次数,明显提升了效率。

通过上述的分析,其实我们也可以以小见大。其实stl中,存在着大量这样的优化,递归转循环,减少边界检查,用赋值代替交换等等,如果我们能仔细研究,并在平时的编码中也养成这样的习惯,就能极大得提升代码的效率。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-11-04 23:36:08

STL heap部分源码分析的相关文章

STL学习_hash_table源码分析

由于自己最近在看STL中的hash_table,被它精巧的设计所折服.无论是对桶子个数的确定,对链表的维护方式,以及判断元素在哪个桶子里等等方法都考虑到了方方面面.所以自己写了篇总结. hash_table存储数据的特性 二叉树,AVL树,RB_tree等数据结构各有各的用途,并且具有对数平均时间,但之所以有这样高的效率取决于输入的数据有足够的随机性,那么hash_table这种数据结构在插入,删除,搜寻等操作上也具有常数平均时间,并且不需要依赖于数据的随机性. hash冲突 hash_tabl

golang中container/heap包源码分析

学习golang难免需要分析源码包中一些实现,下面就来说说container/heap包的源码 heap的实现使用到了小根堆,下面先对堆做个简单说明 1. 堆概念 堆是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于(或不小于)其左孩子和右孩子节点的值. 最大堆和最小堆是二叉堆的两种形式. 最大堆:根结点的键值是所有堆结点键值中最大者. 最小堆:根结点的键值是所有堆结点键值中最小者. 2. heap 树的最小元素在根部,为index 0. heap包对任意实现了heap接口的类型提供

STL源码分析--空间配置器的底层实现 (二)

STL源码分析-空间配置器 空间配置器中门道 在STL中的容器里都是使用统一的空间配置器,空间配置器就是管理分配内存和销毁内存的.在STL将在heap空间创建一个对象分为两个步骤,第一是申请一块内存,第二是在这块内存中初始化一个对象.首先申请空间是由malloc提供,初始化一个对象时由constructor管理.销毁一个对象也是由两步骤完成,第一是销毁空间上的对象,第二是释放这块内存. 同时,STL的空间配置器分为两级内存,如果申请的内存空间大于128KB,那么就使用第一级空间配置,如果小于,那

C++STL内存配置的设计思想与关键源码分析

说明:我认为要读懂STL中allocator部分的源码,并汲取它的思想,至少以下几点知识你要了解:operator new和operator delete.handler函数以及一点模板知识.否则,下面你很可能看不大明白,补充点知识再学习STL源码比较好. 下面会结合关键源码分析C++STL(SGI版本)的内存配置器设计思想.关键词既然是“思想”,所以重点也就呼之欲出了. 1.allocator的简短介绍 我阅读的源码是SGI公司的版本,也是看起来最清楚的版本,各种命名最容易让人看懂.alloc

stl源码分析之hash table

本文主要分析g++ stl中哈希表的实现方法.stl中,除了以红黑树为底层存储结构的map和set,还有用哈希表实现的hash_map和hash_set.map和set的查询时间是对数级的,而hash_map和hash_set更快,可以达到常数级,不过哈希表需要更多内存空间,属于以空间换时间的用法,而且选择一个好的哈希函数也不那么容易. 一. 哈希表基本概念 哈希表,又名散列表,是根据关键字直接访问内存的数据结构.通过哈希函数,将键值映射转换成数组中的位置,就可以在O(1)的时间内访问到数据.举

stl源码分析之list

本文主要分析gcc4.8版本的stl list的源码实现,与vector的线性空间结构不同,list的节点是任意分散的,节点之间通过指针连接,好处是在任何位置插入删除元素都只需要常数时间,缺点是不能随机访问,查询复杂度是O(n),n为list中的元素个数.所以list非常适合应用与数据插入删除频繁的场景. 一. list节点 list节点定义如下, struct _List_node_base { _List_node_base* _M_next; _List_node_base* _M_pre

C++:浅谈c++资源管理以及对[STL]智能指针auto_ptr源码分析,左值与右值

C++:浅谈c++资源管理以及对[STL]智能指针auto_ptr源码分析 by 小威威 1. 知识引入 在C++编程中,动态分配的内存在使用完毕之后一般都要delete(释放),否则就会造成内存泄漏,导致不必要的后果.虽然大多数初学者都会有这样的意识,但是有些却不以为意.我曾问我的同学关于动态内存的分配与释放,他的回答是:"只要保证new和delete成对出现就行了.如果在构造函数中new(动态分配内存),那么在析构函数中delete(释放)就可以避免内存泄漏了!" 事实果真如此么?

STL源码分析--仿函数 &amp; 配接器

STL源码分析-仿函数 & 配接器 仿函数就是函数对象.就实现观点而言,仿函数其实就是一个"行为类似函数"的对象.为了能够"行为类似函数",其类别定义中必须自定义(或说改写.重载)function call 运算子(operator()),拥有这样的运算子后,我们就可以在仿函数的对象后面加上一对小括号,以此调用仿函数所定义的operator().仿函数作为可配接的关键因素. 配接器在STL组件的灵活组合运用功能上,扮演着轴承.转换器的角色,adapter的定

stl源码分析之priority queue

前面两篇介绍了gcc4.8的vector和list的源码实现,这是stl最常用了两种序列式容器.除了容器之外,stl还提供了一种借助容器实现特殊操作的组件,谓之适配器,比如stack,queue,priority queue等,本文就介绍gcc4.8的priority queue的源码实现. 顾名思义,priority queue是带有优先级的队列,所以元素必须提供<操作符,与vector和list不同,priority queue允许加入元素,但是取出时只能取出优先级最高的元素. 一. pri