priority_queue剖析

priority_queue又叫优先队列,其实应该算是一个容器适配器,存储结构默认设置为vector,特点是每次使用pop()都会弹出最大优先级的那个元素,原因就是这个容器的排序准则是维持一个大根堆(1.根节点要大于等于左右节点,但是左右节点没有要求谁大谁小。2.元素的逻辑模型应该为一颗完全二叉树),在源码中可以窥见一二

 

 数据元素类型为T,默认使用vector存储元素,排序准则默认为less

priority_queue最重要的两个函数pop和push定义如下:

  void push(const value_type& x) {
    __STL_TRY {
      /*压入一个新值放入最后*/
      c.push_back(x); 
      /*堆化处理*/
      push_heap(c.begin(), c.end(), comp);
    }
    __STL_UNWIND(c.clear());
  }
  void pop() {
    __STL_TRY {
      /*将根部元素和最后一片叶子交换(此时最后一个元素为根值)然后把除去最后一个元素的剩余元素做重新堆化处理*/
      /*注意这里最大的元素还在最后一个位置*/
      pop_heap(c.begin(), c.end(), comp);
      /*真正弹出元素*/
      c.pop_back();
    }
    __STL_UNWIND(c.clear());
  }

当插入元素的时候,会先调用底部容器的push_back方法往最后一个位置插入元素,然后调用push_heap重新堆化

堆的性质:当节点有两个子节点时,假设当前下边为i(i不等于0),则左孩子下标为2i,右边孩子下标为2i+1;当一个点不是顶点时,其父节点下标为n/2。

但是stl实作里顶点下标是0,使用情况稍微复杂了点,知道什么意思就行。

template <class RandomAccessIterator, class Distance, class T>
void __push_heap(RandomAccessIterator first, Distance holeIndex,
                 Distance topIndex, T value) {
  /*找出待检查点的父节点位置*/
  /*待检查点开始从下往上检查*/
  Distance parent = (holeIndex - 1) / 2;
  /*未到达顶端,且父节点小于插入点*/
  while (holeIndex > topIndex && *(first + parent) < value) {
    /*父节点移到子节点(值交换)*/
    *(first + holeIndex) = *(first + parent);
    /*与父节点交换位置*/   
    holeIndex = parent;
    /*重新计算父节点位置*/
    parent = (holeIndex - 1) / 2;
  }   
  /*设置目标值,完成插入操作*/ 
  *(first + holeIndex) = value;
}

template <class RandomAccessIterator, class Distance, class T>
inline void __push_heap_aux(RandomAccessIterator first,
                            RandomAccessIterator last, Distance*, T*) {
  __push_heap(first, Distance((last - first) - 1), Distance(0), 
              T(*(last - 1)));
}

template <class RandomAccessIterator>
inline void push_heap(RandomAccessIterator first, RandomAccessIterator last) {
  __push_heap_aux(first, last, distance_type(first), value_type(first));
}

 删除元素:优先队列每次删除的都是优先级最大(或最小)的那个节点,stl的priority_queue默认是大根堆规则形成的vector,下边不再多说;

pop的源码如下,这里选用的是compare为默认值的情况(less)

/*
  first 首迭代器
  holeIndex 目标索引
  len 待检查长度
  value 原尾部元素
*/
template <class RandomAccessIterator, class Distance, class T>
void __adjust_heap(RandomAccessIterator first, Distance holeIndex,
                   Distance len, T value) {
  /*先记下开始检查的节点下标*/
  /*pop操作的话就是根部下标,因为要从顶点往下检查是否有子节点大于父节点*/
  Distance topIndex = holeIndex;

  /*
    1.更换顶端节点后重新堆化
  */

  /*先计算根节点的大儿子节点*/
  Distance secondChild = 2 * holeIndex + 2;       //(1)
  /*下标没超过堆长度*/
  while (secondChild < len) {
    /*左右子节点去比较,选出大的那一个,secondChild就是大的那一个的下标*/
    if (*(first + secondChild) < *(first + (secondChild - 1)))
      secondChild--;
    /*父节点值与大儿子值交换*/
    *(first + holeIndex) = *(first + secondChild);
    /*下一层检查,父下标变大儿子下标*/
    holeIndex = secondChild;
    /*计算新的大儿子下标
      为什么这里和(1)的计算方式不一样呢,上边是+2,这里是+1
      其实就是因为要兼容上边(1)的时候holeIndex是0的情况,使得每次secondChild默认值是右孩子
    */
    secondChild = 2 * (secondChild + 1);          //(2)
  }
  /*没有右子节点,只有左子节点*/
  if (secondChild == len) {
    *(first + holeIndex) = *(first + (secondChild - 1));
    holeIndex = secondChild - 1;
  }

  /*
    2.将原最后一个元素放入合适位置
  */
  __push_heap(first, holeIndex, topIndex, value);
}

template <class RandomAccessIterator, class T, class Distance>
inline void __pop_heap(RandomAccessIterator first, RandomAccessIterator last,
                       RandomAccessIterator result, T value, Distance*) {
  /*最后一个元素设置为根部的值*/
  *result = *first;
  /*这里的last已经是指向最后一个节点了,不是原来的最后一个节点的下一个节点*/
  /*所以Distance(last-first)是排除最后一个元素后的剩余系列长度*/
  __adjust_heap(first, Distance(0), Distance(last - first), value);
}

template <class RandomAccessIterator, class T>
inline void __pop_heap_aux(RandomAccessIterator first,
                           RandomAccessIterator last, T*) {
  __pop_heap(first, last - 1, last - 1, T(*(last - 1)), distance_type(first));
}

template <class RandomAccessIterator>
inline void pop_heap(RandomAccessIterator first, RandomAccessIterator last) {
  __pop_heap_aux(first, last, value_type(first));
}

要想深入理解还是得亲手调试几遍,迭代器改成指针,长度什么的改成int,用int数组调试就够了,复制到windows下用vs调(注意:进行pop操作前元素序列必须是有效大根堆)

 关于优先队列还有两个函数可以谈,分别是它的sort排序函数和将一个序列堆化的函数made_heap

sort_heap:前边讲过每次pop会把优先级最高的放置于尾部,调用while循环pop即可形成一个递增序列

template <class RandomAccessIterator>
void sort_heap(RandomAccessIterator first, RandomAccessIterator last) {
  /*从尾部调用pop_heap直到根部*/
  while (last - first > 1) pop_heap(first, last--);
}

made_heap:将一个序列变成大根堆

template <class RandomAccessIterator, class T, class Distance>
void __make_heap(RandomAccessIterator first, RandomAccessIterator last, T*,
                 Distance*) {
  /*一个或者0个元素不用排*/
  if (last - first < 2) return;
  /*长度*/
  Distance len = last - first;
  /*找第一个需要重排的节点下标*/
  /*parent这个命名有些歧义,还是较holeIndex较好*/
  Distance parent = (len - 2)/2;
    
  while (true) {
    /*以parent开始检查,直到顶点*/
    /*以parent这个点为每个小堆的顶点去做堆化处理*/
    __adjust_heap(first, parent, len, T(*(first + parent)));
    if (parent == 0) return;
    parent--;
  }
}

参考:

stl源码剖析4.8priority queue

数据结构于算法第二版 陈卫卫,王庆瑞第五章堆排序

大话数据结构第九章堆排序

关于算法还是一句话,调试是关键,硬看不推荐,实际上理解了它的运行模式也差不多了,调一下加深印象,脑袋要炸了,过两天可能就忘了,人之常情,作此记录以待需要可以快速拾起罢了。

posted @ 2020-11-05 15:59  乐swap火  阅读(122)  评论(0编辑  收藏  举报