STL源码笔记(14)—堆和优先级队列(一)

STL源码笔记(14)—堆和优先级队列

priority_queue是拥有权值观念的queue,跟queue类似,其只能在一端push,一端pop,不同的是,每次push元素之后再容器内部元素将按照一定次序排列,使得pop得到的元素始终是当前权值的极大值。

很显然,满足这个条件就需要某些机制了,缺省情况下使用max-heap大顶堆来实现,联想堆排序的实现,使用大顶完成序列从小到大的排序,过程大概是:

  1. 把堆的根元素(堆中极大值)交换到最后
  2. 堆的长度减1

这样每次取出堆中的极大值完成排序,刚好与优先级队列要求的权值最高类似,因此可以利用这个特性完成优先级队列。

对于一个优先级队列来说,他使用vector<T>作为其底层默认容器,与stack,queue类似,priority_queue也是一个adapter(配接器),被归类为container adapter

注:在SGI STL源码中priority_queue的源码位于文件stl_queue.h中,heap算法的源码位于stl_heap.h中。

heap算法

heap算法是实现优先级队列的核心。主要有:

1.push_heap()

2.pop_heap()

3.__adjust_heap()

4.sort_heap()

5.make_heap()

除了__adjust_heap()外,其余四个都是可以在外部使用的函数,如果要实现堆排序的话可以直接使用上述4,5函数。

push_heap函数

书中给了一个很形象的表述叫做percolate up(上溯)操作,这里假设:

在容器vector中,前n个元素(索引0~n-1)为已经排好的max-heap,现在待插入元素的索引为n,这里,程序会在这里挖个坑,称之为holeindex,可以理解为我现在要插入一个元素,但是我并不知道要插在哪里,所以我先挖一个坑,然后经过一系列的算法,这个holeindex,要开始慢慢往上爬,以满足大顶堆条件,其实这里的上爬做了两件事:

  1. 将比value小的parent节点往下拉
  2. 将原来的holeindex往上拉,占据原来parent所在的位置

持续操作,直到holeindex爬到顶端(根节点),以及满足大顶堆的条件了,此时holeindex已经上溯到正确的位置,只需要将value值填进去即可。

template <class _RandomAccessIterator, class _Distance, class _Tp,
          class _Compare>
void
__push_heap(_RandomAccessIterator __first, _Distance __holeIndex,
            _Distance __topIndex, _Tp __value, _Compare __comp)
{
  _Distance __parent = (__holeIndex - 1) / 2;//父节点位置
  while (__holeIndex > __topIndex && __comp(*(__first + __parent), __value)) {//__comp默认是小于:父节点小于子节点
  //调整hole的位置,如果满足条件hole将一直上溯
    *(__first + __holeIndex) = *(__first + __parent);
    __holeIndex = __parent;
    __parent = (__holeIndex - 1) / 2;
  }
  *(__first + __holeIndex) = __value;//把value放到新的位置
}

template <class _RandomAccessIterator, class _Compare,
          class _Distance, class _Tp>
inline void
__push_heap_aux(_RandomAccessIterator __first,
                _RandomAccessIterator __last, _Compare __comp,
                _Distance*, _Tp*)
{
    //直接调用__push_heap
  __push_heap(__first, _Distance((__last - __first) - 1), _Distance(0),
              _Tp(*(__last - 1)), __comp);
}

template <class _RandomAccessIterator, class _Compare>
inline void
push_heap(_RandomAccessIterator __first, _RandomAccessIterator __last,
          _Compare __comp)
{
  __STL_REQUIRES(_RandomAccessIterator, _Mutable_RandomAccessIterator);
  __push_heap_aux(__first, __last, __comp,
                  __DISTANCE_TYPE(__first), __VALUE_TYPE(__first));
}

pop_heap与__adjust_heap函数

与push_heap对应的就是“出堆”操作了,在堆排序中,我们对已经建好的堆进行“出堆”,操作是每次将堆顶根元素换到容器末尾,先对堆的长度减1,再对堆进行调堆操作,重复进行直到堆中所有元素出来,由于出堆操作每次出的是堆顶元素,也就是极大值,因此完成出堆操作后就完成排序工作。

pop_heap与push_heap对应的是percolate down(下溯)操作,这里很容易理解因此堆顶的根元素被取走以后,这里原来的根元素就是一个没有元素的空节点了,也就是所谓的holeindex,这里就是要将这个holeindex调整到合适的位置,holeindex之前的元素是一个堆,实际上做的工作是:

  1. 找到holeindex对应两个孩子中较大的将其换到holeindex的位置
  2. 将holeindex下溯到上述较大孩子的索引的位子
  3. 重复执行上述操作,最终holeindex将落在某个叶子节点处。

上述第2点需要考虑没有右子树的情况,例如:

当下溯到(索引2处)原65节点时,此时没有右节点,右节点索引second==len

由于此时并没有考虑从容器末尾换出来的value值,所以要对堆的索引为 0~holeindex(模拟一下过程可以很容易得到此时的holeindex位于叶子节点处) 的元素进行一次push_heap操作,使得value插入到合适的位置。

所以说,pop_heap的工作实际上很简单,主要任务是极大值出堆后要对剩余的元素进行重新调整,使得其成一个堆。

//4.
template <class _RandomAccessIterator, class _Distance, class _Tp>
void
__adjust_heap(_RandomAccessIterator __first, _Distance __holeIndex,
              _Distance __len, _Tp __value)
{
  _Distance __topIndex = __holeIndex;//holeindex在顶端
  _Distance __secondChild = 2 * __holeIndex + 2;//从右孩子开始
  while (__secondChild < __len) {//表示holeindex还没有到叶子节点,要不停下溯
  //取左右孩子的最大值
    if (*(__first + __secondChild) < *(__first + (__secondChild - 1)))
      __secondChild--;
    *(__first + __holeIndex) = *(__first + __secondChild);//较大的孩子上调
    __holeIndex = __secondChild;//holeindex下溯
    __secondChild = 2 * (__secondChild + 1);
  }
  if (__secondChild == __len) {//当前holeindex只有左孩子
    *(__first + __holeIndex) = *(__first + (__secondChild - 1));
    __holeIndex = __secondChild - 1;
  }
  __push_heap(__first, __holeIndex, __topIndex, __value);//重新插入
}

//3.
template <class _RandomAccessIterator, class _Tp, class _Distance>
inline void
__pop_heap(_RandomAccessIterator __first, _RandomAccessIterator __last,
           _RandomAccessIterator __result, _Tp __value, _Distance*)
{
  *__result = *__first;//首先将堆中极大值赋给堆末尾位置
  //这里的last已经完成减1操作
  //调堆操作
  __adjust_heap(__first, _Distance(0), _Distance(__last - __first), __value);
}

//2.
template <class _RandomAccessIterator, class _Tp>
inline void
__pop_heap_aux(_RandomAccessIterator __first, _RandomAccessIterator __last,
               _Tp*)
{
//直接调用,这里已经获取堆中最后一个元素的值,堆中最后一个元素的迭代器变量
  __pop_heap(__first, __last - 1, __last - 1,
             _Tp(*(__last - 1)), __DISTANCE_TYPE(__first));
}

//1.
template <class _RandomAccessIterator>
inline void pop_heap(_RandomAccessIterator __first,
                     _RandomAccessIterator __last)
{
  __STL_REQUIRES(_RandomAccessIterator, _Mutable_RandomAccessIterator);
  __STL_REQUIRES(typename iterator_traits<_RandomAccessIterator>::value_type,
                 _LessThanComparable);
  __pop_heap_aux(__first, __last, __VALUE_TYPE(__first));//直接调用
}

make_heap和heap_sort

这两个函数实现了堆排序。

make_heap是将一段现有的数据转化成一个heap,操作就是,从第一个非叶子节点向前进行调堆操作,为什么要选择第最后一个非叶子节点呢?因为,调堆操作是对holeindex进行下溯,现在你都是叶子节点了还怎么下?

至于最后一个非叶子节点的计算公式:

index=(len?2)/2(1)

其中len是数组的长度。

heap_sort函数就简单了,不断的执行pop_heap操作即可:

template <class _RandomAccessIterator, class _Tp, class _Distance>
void
__make_heap(_RandomAccessIterator __first,
            _RandomAccessIterator __last, _Tp*, _Distance*)
{
  if (__last - __first < 2) return;
  _Distance __len = __last - __first;
   //找出第一个需要重排的子树头部(因为需要执行perlocate down操作,而叶子节点不需要执行该操作)
  _Distance __parent = (__len - 2)/2;

  while (true) {
    __adjust_heap(__first, __parent, __len, _Tp(*(__first + __parent)));
    if (__parent == 0) return;
    __parent--;
  }
}

//将一段现有的数据转化成一个heap...在堆排序中就是build_heap
template <class _RandomAccessIterator>
inline void
make_heap(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
  __STL_REQUIRES(_RandomAccessIterator, _Mutable_RandomAccessIterator);
  __STL_REQUIRES(typename iterator_traits<_RandomAccessIterator>::value_type,
                 _LessThanComparable);
  __make_heap(__first, __last,
              __VALUE_TYPE(__first), __DISTANCE_TYPE(__first));
}
//每次将一个极大值放到尾端
template <class _RandomAccessIterator>
void sort_heap(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
  __STL_REQUIRES(_RandomAccessIterator, _Mutable_RandomAccessIterator);
  __STL_REQUIRES(typename iterator_traits<_RandomAccessIterator>::value_type,
                 _LessThanComparable);
  while (__last - __first > 1)
    pop_heap(__first, __last--);
}
时间: 2024-10-17 22:08:48

STL源码笔记(14)—堆和优先级队列(一)的相关文章

STL源码笔记(15)—堆和优先级队列(二)

STL源码笔记(15)-堆和优先级队列 优先级队列的源码实现基于heap的操作,底层容器默认是vector. 优先级队列简介 优先级队列跟队列类似,一端插入一端删除,不同的是,优先级队列的元素入队后会根据其优先级进行调整,默认情况下优先级高的将优先出队,在SGI STL中,优先级队列的功能保证由heap实现:stl_heap.h中,heap的分析见:STL堆源码分析 优先级队列构造函数 默认情况下,优先级队列使用vector作为底层容器,使用less作为比较函数,其在源码中的定义声明如下: te

STL源码笔记(12)—序列式容器之deque(二)

STL源码笔记(12)-序列式容器之deque(二) 再谈deque数据结构 我们知道deque是通过map管理很多个互相独立连续空间,由于对deque_iterator的特殊设计,使得在使用的时候就好像连续一样.有了deque_iterator的基础(例如重载的操作符等),对于我们实现容器的一些方法就十分方便了.与vector一样,deque也维护一个start,和finish两个迭代器,start指向容器中的一个元素,finish指向最后一个元素的后一个位置(前闭后开),从微观上讲,star

STL源码笔记(16)—单链表slist

STL单链表slist简介 概述 slist(Single linked list)顾名思义,是一个单向链表,这个容器并不在标准规格之内,在我几年的代码学习生涯中也是第一次听说,既然侯老师的书中提到了,那也还是学习一蛤. slist与list的主要差别是,前者的迭代器属于单向的Forward Iterator(可读写),后者的迭代器属于双向的Bidirectional Iterator(可以双向读写).看起来slist的功能应该会不如list,但由于其单向链表的实现,其消耗的空间更小,某些操作更

STL源码笔记(18)—平衡二叉树AVL(C++封装+模板)

AVLTree平衡二叉树 在几年前刚学数据结构时,AVL-Tree只是一个仅仅需要掌握其概念的东西,今非昔比,借看STL源码剖析的契机希望从代码层面将其拿下. 1.简介 二叉查找树给我们带来了很多方便,但是由于其在有序序列插入时就会退化成单链表(时间复杂度退化成 O(n)),AVL-tree就克服了上述困难.AVL-tree是一个"加上了平衡条件的"二叉搜索树,平衡条件确保整棵树的深度为O(log n). AVL树是最先发明的自平衡二叉查找树.在AVL树中任何节点的两个子树的高度最大差

STL源码笔记(17)—二叉排序树BST(C++封装)

二叉排序树BST STL中还有一类非常重要的容器,就是关联容器,比如map啊set啊等等,这些容器说实话,在应用层上还不能完全得心应手(比如几种容器效率的考虑等等),更别说源码了,因此这一部分打算稳扎稳打,好好做做笔记研究一番. 说到关联容器,我们想到了什么AVL树,红黑树等等,但大多时候我们仅仅局限于知道其名字,或者知道其概念,俗话说"talk is cheap,show me the code",因此,我打算从他们的祖爷爷二叉排序树开始下手.(其实,侯老师的书上也是这么安排的哈)

通读《STL源码剖析》之后的一点读书笔记

[QQ群: 189191838,对算法和C++感兴趣可以进来] 直接逼入正题. Standard Template Library简称STL.STL可分为容器(containers).迭代器(iterators).空间配置器(allocator).配接器(adaptors).算法(algorithms).仿函数(functors)六个部分. 迭代器和泛型编程的思想在这里几乎用到了极致.模板或者泛型编程其实就是算法实现时不指定具体类型,而由调用的时候指定类型,进行特化.在STL中,迭代器保证了ST

《STL源码剖析》---stl_alloc.h阅读笔记

这一节是讲空间的配置与释放,但不涉及对象的构造和析构,只是讲解对象构造前空前的申请以及对象析构后空间怎么释放. SGI版本的STL对空间的的申请和释放做了如下考虑: 1.向堆申请空间 2.考虑了多线程.但是这节目的只是讲解空间配置与释放,因此忽略了多线程,集中学习空间的申请和释放. 3.内存不足时的应变措施 4.考虑到了内存碎片的问题.多次申请释放小块内存可能会造成内存碎片. 在C++中,内存的申请和释放是通过operator new函数和operator delete函数,这两个函数相当于C语

《STL源码剖析》---stl_heap.h阅读笔记

Heap堆是常用的数据结构,Heap中也可以存放元素.但是STL中并没有提供Heap容器,只是提供了关于Heap操作的算法.只要支持RandomAccessIterator的容器都可以作为Heap容器. Heap分为max heap和min heap,max heap中每次取出的结点时heap结构中值最大的结点,min heap中每次取出的结点时heap结构中值最小的结点. 在实际应用中,经常用vector作为heap容器,heap经常作为priority queue.对于二叉堆,这里有描述ht

《STL源码剖析》---stl_pair.h阅读笔记

pair是STL中的模板类型,它可以存储两个元素,它也被称作"对组".在map中已经用到了它,pair其实就是一个struct结构,存有两个public的元素,重载了几个运算符,没有什么成员函数,源代码很简单. G++ 2.91.57,cygnus\cygwin-b20\include\g++\stl_pair.h 完整列表 /* * * Copyright (c) 1994 * Hewlett-Packard Company * * Permission to use, copy,