C++(STL源码):22---heap算法源码剖析(push_heap、pop_heap、sort_heap、make_heap)

一、heap概述

  • heap并不归属于 STL 容器组件
  • binary heap作为priority queue的底层实现机制。顾名思义,priority queue允许使用者以任何次序将任何元素推入容器内,但取出时一定是从优先权最高(也就是数值最高)之元素开始取。binary max heap正是具有这样的特性,适合做为priorityqueue的底层机制

heap作为priority queue的底层实现

  • 假设list作为priority queue的底层机制:
    • 元素插入动作可享常数时间
    • 但是要找到list中的极值,却需要对整个list进行线性扫描。我们也可以改个做法,让元素插入前先经过排序这一关,使得list的元素值总是由小到大(或由大到小),但这么一来:虽然取得极值以及元素删除动作达到最高效率,可元素的插入却只有线性表现
  • 如果以binary serach tree(二叉搜索树,如RB-tree)作为priority queue的底层机制:
    • 这么一来元素的插入和极值的取得就有O(logN)的表现
    • 但是这么一来binary search tree的输入需要足够的随机性,二来 binary search tree并不容易实现
  • 如果以binary heap作为priority queue的底层机制:
    • 为了减少复杂度,我们选择介于queue和binary search tree之间的binary heap作为priority queue的底层实现

heap的分类

  • 根据元素排列方式,heap可分为max-heap和 min-heap两种:
    • max-heap:每个节点的 键值(key)都大于或等于其子节点键值
    • min-heap:的每个节点键值(key)都小于或等于其子节点键值。
  • 因此:
    • max-heap的最大值在根节点,并总是位于底层array或vector的起头处
    • min-heap的最小值在根节点,亦总是位于底层 array或vector 的起头处
    • STL 供应的是 max-heap,因此以下我说 heap 时,指的是 max-heap

heap没有迭代器

  • heap的所有元素都必须遵循特别的(complete binary tree)排列规则,所以 heap 不提供遍历功能,也不提供迭代器

二、binary heap概述

  • 所谓binary heap就是一种complete binary tree(完全二叉树)
  • 完全二叉树的介绍可以参见:https://blog.csdn.net/qq_41453285/article/details/103560556
  • 下图是一个完全二叉树:

C++(STL源码):22---heap算法源码剖析(push_heap、pop_heap、sort_heap、make_heap)_第1张图片

三、heap的实现

  • complete binary tree整棵树内没有任何节点为空,这带来一个极大好处:我们可以利用array来储存所有节点
  • 假设array的0索引处元素保留(或设为无限大值或无限小值),那么当完全二叉树中的某个节点位于array的i索引处时,有如下规则:
    • 其左子节点必位于array的2i索引处
    • 其右子节点必位于array 的2i+1索引处
    • 其父节点必位于i/2索引处
  • 通过这么简单的位置规则,array可以轻易实作出完全二叉树。 这种以array表述tree的方式 , 我们称为隐式表述法( implicit representation)
  • 这么一来,我们需要的工具就很简单了:一个array和一组heap算法(用来插入元素、删除元素、取极值、将某一整组数据排列成一个 heap)。array的缺点是无法动态改变大小,而heap却需要这项功能,因此以vector代替array是更好的选择

四、heap的分类

  • 根据元素排列方式,heap可分为max-heap和 min-heap两种:
    • max-heap:每个节点的键值(key)都大于或等于其子节点键值
    • min-heap:的每个节点键值(key)都小于或等于其子节点键值
  • 因此:
    • max-heap的最大值在根节点,并总是位于底层array或vector的起头处
    • min-heap的最小值在根节点,亦总是位于底层 array或vector 的起头处
  • STL供应的max-heap,因此本文下面介绍的heap,指的是max-heap

五、push_heap算法

算法图解

  • push_heap算法思路如下:
    • 1.把新元素插入在底层vector的end()处
    • 2.如果其有父节点,将其与父节点比较,如果值比父节点小,就保持位置不变,结束算法;如果值比父节点大,进行下一步
    • 3.如果值比父节点大,将其与父节点进行位置交换;交换之后如果还有父节点,那么再次回到步骤2
  • 下面是一个演示:
    • 1.将插入的新元素50放在vector的末尾
    • 2.因为其父节点24比自己小,所以将父节点与自己交换位置
    • 3.交换之后,其父节点变为31,其值比31大,所以继续交换位置
    • 4.接着其父节点变为68,其值比68小,所以结束push_heap算法

C++(STL源码):22---heap算法源码剖析(push_heap、pop_heap、sort_heap、make_heap)_第2张图片

  • 上面这种比较方式我们称之为:percolate up(上溯)程序

push_heap源码

  • 下面便是 push_heap 算法的实现细节。此函数接受两个迭代器,用来表现一个heap底部容器(vector)的头尾,新元素并且已经插入到底部容器的最尾端。 如果不符合这两个条件,push_heap的执行结果未可预期
template 
inline void push_heap(RandomAccessIterator first,RandomAccessIterator last) 
{
    // 注意,此函式被调用时,新元素已置于底部容器的最尾端
    __push_heap_aux(first, last, distance_type(first),value_type(first));
}
template 
inline void push_heap_aux(RandomAccessIterator first,RandomAccessIterator last, Distance*, T*) 
{
    __push_heap(first, Distance((last - first) - 1), Distance(0),T(*(last - 1)));
    // 以上系根据implicit representation heap的结构特性:新值必置于底部
    // 容器的最尾端,此即第一个洞号:(last-first)–1
}
// 以下这组 push_back()不允许指定“大小比较标准”
template 
void __push_heap(RandomAccessIterator first, Distance holeIndex,Distance topIndex, T value) 
{
    Distance parent = (holeIndex - 1) / 2; // 找出父节点
    
    while (holeIndex > topIndex && *(first + parent) < value) {
        // 当尚未到达顶端,且父节点小于新值(于是不符合 heap 的次序特性)
        // 由于以㆖使用 operator<,可知 STL heap 是一种 max-heap(大者为父)
        *(first + holeIndex) = *(first + parent); //令洞值为父值
        holeIndex = parent; // percolate up:调整洞号,向上提升至父节点
        parent = (holeIndex - 1) / 2; // 新洞的父节点
    } // 持续至顶端,或满足heap的次序特性为止
    
    *(first + holeIndex) = value; //令洞值为新值,完成插入动作
}

六、pop_heap算法

算法图解

  • 因为是max-heap,最大值必然在根节点,pop操作时取走根节点(取走vector的最后一个元素),取走根节点之后,为了满足完全二叉树的规则,我们需要重新调整二叉树的结构
  • pop_heap算法思路如下:
    • 1.把根节点元素取出,把最后一个节点的元素取出
    • 2.将原根节点元素放在vector的最后一个节点处
    • 3.将原先的最后一个节点的元素放置到原根节点处作为新根节点
    • 4.将新根节点逐个与子节点比较,直到其值比子节点都大时,结束算法
  • 下面是一个演示:
    • 1.取出根节点元素68,取出最后一个节点的元素24
    • 2.将原根节点元素68放在vector的最后一个位置
    • 3.将原先最后一个节点的元素24放置到原根节点68处作为新根节点
    • 4.接着其值24比左、右节点50、65都小,但是65比50大,于是就将其值24与值68的子节点互换
    • 5.接着其值24又比左、右节点32、26都小,但是32比26大,于是就将其值24与值32的子节点互换
    • 6.最后其没有子节点可以比较了,结束算法

C++(STL源码):22---heap算法源码剖析(push_heap、pop_heap、sort_heap、make_heap)_第3张图片

  • 上面这种比较方式我们称之为:percolate down(下溯)程序
  • 注意,pop_heap 之后,最大元素只是被置放于底部容器的最尾端,尚未被取走。 如果要取其值,可使用底部容器(vector)所提供的 back() 操作函数。如果要移除它,可使用底部容器(vector)所提供的 pop_back() 操作函数

pop_heap源码

  • 下面便是 pop_heap算法的实现细节。此函数接受两个迭代器,用来表现以个 heap 底部容器(vector)的头尾。如果不符合这个条件,pop_heap 的执行结果 未可预期。
template 
inline void pop_heap(RandomAccessIterator first,RandomAccessIterator last) 
{
    __pop_heap_aux(first, last, value_type(first));
}
template 
inline void __pop_heap_aux(RandomAccessIterator first,RandomAccessIterator last, T*) 
{
    __pop_heap(first, last-1, last-1, T(*(last-1)),
    distance_type(first));
    // 以上,根据 implicit representation heap 的次序特性,pop操作的结果
    // 应为底部容器的第一个元素。因此,首先设定欲调整值为尾值,然后将首值调至
    // 尾节点(所以以上将迭代器 result设为 last-1)。然后重整 [first, last-1),
    // 使之重新成一个合格的 heap
}
// 以下这组 pop_heap() 不允许指定“大小比较标准”
template 
inline void __pop_heap(RandomAccessIterator first,
        RandomAccessIterator last,
        RandomAccessIterator result,
        T value, Distance*) 
{
    *result = *first; // 设定尾值为首值,于是尾值即为欲求结果,
                      // 可由客端稍后再以底层容器之 pop_back() 取出尾值

    __adjust_heap(first, Distance(0), Distance(last - first), value);
    // 以上欲重新调整 heap,洞号为 0(亦即树根处),欲调整值为 value(原尾值)
}
// 以下这个 __adjust_heap() 不允许指定“大小比较标准”
template 
void __adjust_heap(RandomAccessIterator first, Distance holeIndex,Distance len, T value) 
{
    Distance topIndex = holeIndex;
    Distance secondChild = 2 * holeIndex + 2; // 洞节点之右子节点

    while (secondChild < len) {
        // 比较洞节点之左右两个子值,然后以 secondChild 代表较大子节点
        if (*(first + secondChild) < *(first + (secondChild - 1)))
            secondChild--;
        // Percolate down:令较大子值为洞值,再令洞号下移至较大子节点处
        *(first + holeIndex) = *(first + secondChild);
        holeIndex = secondChild;
        // 找出新洞节点的右子节点
        secondChild = 2 * (secondChild + 1);
    }

    if (secondChild == len) { // 没有右子节点,只有左子节点
        // Percolate down:令左子值为洞值,再令洞号下移至左子节点处
        *(first + holeIndex) = *(first + (secondChild - 1));
        holeIndex = secondChild - 1;
    }

    // 将欲调整值填入目前的洞号内。注意,此时肯定满足次序特性
    push_heap(first, holeIndex, topIndex, value);
}

七、sort_heap算法

算法图解

  • 既然每次pop_heap可获得heap之中键值最大的元素,如果持续对整个heap做pop_heap动作,每次将操作范围从后向前缩减一个元素(因为pop_heap会把键值最大的元素放在底部容器的最尾端),当整个程序执行完毕,我们便有了一个递增序列
  • 下图展示sort_heap算法的实际演示操作

C++(STL源码):22---heap算法源码剖析(push_heap、pop_heap、sort_heap、make_heap)_第4张图片

sort_heap源码

  • 下面是sort_heap算法的实作细节。此函数接受两个迭代器,用来表现一个 heap 底部容器(vector)的头尾。如果不符合这个条件,sort_heap 的执行结 果未可预期。注意,排序过后,原来的heap就不再是个合法的 heap 了
// 以下这个sort_heap() 不允许指定“大小比较标准”
template 
void sort_heap(RandomAccessIterator first,RandomAccessIterator last) 
{
    // 以下,每执行一次 pop_heap(),极值(在STL heap中为极大值)即被放在尾端
    // 扣除尾端再执行一次 pop_heap(),次极值又被放在新尾端。一直下去,最后即得排序结果
    while (last - first > 1)
        pop_heap(first, last--); // 每执行 pop_heap() 一次,操作范围即退缩一格
}

八、make_heap算法

  • 这个算法用来将一段现有的数据转化为一个heap。其主要依据就是文章一开始提到的complete binary tree的隐式表述(implicit representation)
  • 这个算法是将一个迭代器区间数据转换为一个大根堆,思路可以参考这篇文章中大根堆的初始化:https://blog.csdn.net/qq_41453285/article/details/103639243
// 将 [first,last) 排列为一个heap
template 
inline void make_heap(RandomAccessIterator first,RandomAccessIterator last) 
{
    __make_heap(first, last, value_type(first), distance_type(first));
}
// 以下这组 make_heap() 不允许指定“大小比较标准”
template 
void __make_heap(RandomAccessIterator first,RandomAccessIterator last, T*,Distance*) 
{
    if (last - first < 2) 
        return; //如果长度为0或1,不必重新排列

    Distance len = last - first;
    // 找出第一个需要重排的子树头部,以parent标示出。由于任何叶节点都不需执行
    // perlocate down,所以有以下计算。parent命名不佳,名为holeIndex 更好
    Distance parent = (len - 2)/2;
    
    while (true) {
        // 重排以 parent 为首的子树。len是为了让__adjust_heap() 判断操作范围
        __adjust_heap(first, parent, len, T(*(first + parent)));
        if (parent == 0) 
            return; // 走完根节点,就结束。
        parent--; // (即将重排之子树的)头部向前一个节点
    }
}

九、heap算法演示案例

演示案例①

#include 
#include 

using namespace std;

int main()
{

	int arr[6] = { 4,1,7,6,2,5 };

	std::make_heap(arr, arr + 6);
	for (int i = 0; i < 6; ++i)
		std::cout << arr[i] << " "; //7 6 5 1 2 4
	std::cout << std::endl;
	
	return 0;
}

  • 初始化堆(make_heap)的步骤如下(下面的演示案例相同):
    • 1.从最后一个具有孩子的节点开始检查(i=6/2=3),于是先检查i=3这个节点,其子节点都比其小,所以保持不变(图A所示)
    • 2.接着检查i=3-1=2这个节点,其值1比两个子节点都小,但是左子节点6比右子节点2大,因此与左子节点6进行互换,得到结果如图B所示
    • 3.接着检查i=2-1=1这个节点,其值比两个子节点都小,但是右子节点7比左子节点6大,因此与右子节点7进行互换,得到结果如图C所示。比较滞后其还有子节点,于是继续与子节点进行比较,发现比左子节点5值小,于是就继续进行互换,结果如下图D所示。(备注:这两步是同一步中完成的,我们分开来讲解)
    • 4.由于已经遍历到根节点了,所以结束make_heap

C++(STL源码):22---heap算法源码剖析(push_heap、pop_heap、sort_heap、make_heap)_第5张图片

演示案例②

#include 
#include 
#include 

using namespace std;

int main()
{

	int arr[9] = { 0,1,2,3,4,8,9,3,5 };
	std::vector ivec(arr, arr + 9);

    //构造一个最大堆
	std::make_heap(ivec.begin(), ivec.end());
	for (int i = 0; i < ivec.size(); ++i)
		std::cout << ivec[i] << " ";     //9 5 8 3 4 0 2 3 1
	std::cout << std::endl;

	ivec.push_back(7); //将7添加到尾部
	for (int i = 0; i < ivec.size(); ++i)
		std::cout << ivec[i] << " ";     //9 5 8 3 4 0 2 3 1 7
	std::cout << std::endl;

	/*
	上面将7添加到尾部,破坏了完全二叉树的结构,因此需要调用此函数
	显式说明尾部有个元素需要插入到完全二叉树中,因此下面打印的为重新调整后的元素顺序
	*/
	std::push_heap(ivec.begin(), ivec.end()); 
	for (int i = 0; i < ivec.size(); ++i)
		std::cout << ivec[i] << " ";     //9 7 8 3 5 0 2 3 1 4
	std::cout << std::endl;

	std::pop_heap(ivec.begin(), ivec.end());//只是将根节点移到容器尾部,但是还没有删除
	std::cout << ivec.back() << std::endl;  //打印9
	ivec.pop_back();                        //将最后的尾节点9移除

	for (int i = 0; i < ivec.size(); ++i)
		std::cout << ivec[i] << " ";     //8 7 4 3 5 0 2 3 1
	std::cout << std::endl;


	std::sort_heap(ivec.begin(), ivec.end()); //对vector进行堆排序
	for (int i = 0; i < ivec.size(); ++i)
		std::cout << ivec[i] << " ";     //0 1 2 3 3 4 5 7 8
	std::cout << std::endl;

	return 0;
}

C++(STL源码):22---heap算法源码剖析(push_heap、pop_heap、sort_heap、make_heap)_第6张图片

演示案例③

  • 下面的arr为数组(array),数组是不能动态改变大小的,因此当数组满载时不能对数组进行push_heap()操作
#include 
#include 
#include 

using namespace std;

int main()
{

	int arr[9] = { 0,1,2,3,4,8,9,3,5 };
	std::vector ivec(arr, arr + 9);

	std::make_heap(arr, arr + 9);
	for (int i = 0; i < 9; ++i)
		std::cout << arr[i] << " "; //9 5 8 3 4 0 2 3 1
	std::cout << std::endl;

	std::sort_heap(arr, arr + 9);
	for (int i = 0; i < 9; ++i)
		std::cout << arr[i] << " "; //0 1 2 3 3 4 5 8 9
	std::cout << std::endl;

	//以上面的arr顺序再次构造一个最大堆
	std::make_heap(arr, arr + 9);
	for (int i = 0; i < 9; ++i)
		std::cout << arr[i] << " "; //9 8 5 3 3 4 2 1 0
	std::cout << std::endl;

	//将最大堆的根节点弹出(移到尾部,但是还没有删除)
	std::pop_heap(arr, arr + 9);
	std::cout << arr[8] << std::endl; //9
	
	return 0;
}

C++(STL源码):22---heap算法源码剖析(push_heap、pop_heap、sort_heap、make_heap)_第7张图片

你可能感兴趣的:(C++(STL源码))