堆的存储结构
- 堆数据结构是一种数组对象,它可以被视为一颗完全二叉树(前n-1层是满的,最后一层从左向右的叶节点连续存在,只缺右侧若干节点)
- 二叉树的表示方法:
1)链式结构(二叉链、三叉链)
2)数组(只适合用来表示完全二叉树) - 数组如何构建完全二叉树
int a[]={10,16,18,12,11,13,15,17,14,19};
如何建大堆
定义堆的数据结构
- 堆的数据结构是数组对象,所以我们可以通过数组来定义堆的数据结构
template <class T>
class Heap
{
protected:
T* a;
size_t _size;
size_t _capacity;
};
- 上面这种定义数组对象是C语言的方法,在C++标准库中,我们可以利用vector类定义数组对象,vector内置计算数组大小以及增容的功能,所以我们可以直接调用,避免自己定义自己管理空间来的方便,下面是我们用vector定义的数组对象:
template <class T>
class Heap
{
protected:
vector<T> _a;
};
我们如何来建一个大堆呢?
- 首先,我们可以想象如果每一颗子树都是大堆,那么我们只用调整根节点与其孩子节点使得最终所得的二叉树为大堆,这就是所谓的向下调整算法
- 向下调整算法:比较左右孩子,将左右孩子中较大的那一个与其父亲相比,如果大于父亲,则与父亲进行交换;否则,若左右孩子中最大的那个节点是小于父亲节点的则不用调整。此时,已经是一个大堆了。
- 下面我们以图示上面所说的调整使得左右子树为大堆的过程:
- 通过上述调整我们已经使得左右子树都为大堆了,只需要完成最后一个过程:即对根节点及其左右孩子进行调整建大堆,下面是调整过程
- 通过上面的分析,我们已经知道如何通过向下调整算法用数组对象来建一个大堆,下面我们通过代码来实现一下向下调整算法:
void AdjustDown(size_t root)
{
int parent = root;
int child = parent * 2 + 1; //只定义左孩子,右孩子为child+1
while (child < _a.size())
{
//如果右孩子存在,且大于左孩子,则选择右孩子
if ((child + 1) < _a.size() && _a[child] < _a[child + 1])
{
++child;
}
if (_a[child]>_a[parent])
{
//交换父子节点的值
swap(_a[child + 1], _a[parent]);
//交换其下标
parent = child;
child = parent * 2 + 1;
}
else
{
//已经是大堆了,不用交换
break;
}
}
}
- 现在我们已经可以建大堆了,接下来我们实现一下构造函数。首先考虑一下,vector最大的缺陷是增容(因为需要开辟空间,然后将数据拷贝过去,然后再释放开辟的空间,代价比较大),我们如何避免这样的问题呢?最简单的方法是我们需要多大的内存,就直接把它开出来,避免后续增容问题。
vector提供给我们两个开辟空间的接口,reserve()和resize(),那么这两个的区别在哪里?
- reserve():直接将空间开出来,什么也不做
- resize():开辟空间并对其进行初始化
我们选择reserve()来开辟内存(因为我们并不关心初始化的问题),下面是构造函数的代码实现:
Heap()
{}
Heap(T* a, size_t n)
{
//首先将数组中每个元素都push_back进vector中
for (size_t i = 0; i < n; ++i)
{
_a.reserve(n);
_a.push_back(a[i]);
}
//建堆,通过向下调整算法来建大堆
//先要找到倒数第一个非叶节点,进行向下调整算法
for (int i = (_a.size() - 2)/2; i >= 0; --i)
{
AdjustDown(i);
}
}
- 至此,我们的大堆算法就写好了,我们用上面的数组a[]来验证一下,我们在分析阶段得出的大堆和我们代码写出来的大堆是否一致?
- 我们构建出来的大堆序列是:19,17,18,14,16,13,15,12,10,11;和我们分析出来的大队序列一致,说明我们的大堆算法是没有问题的
- 注意:堆不代表有序,只是接近有序
堆的其他函数接口的实现:
bool Empty()
{
return _a.empty();
}
size_t Size()
{
return _a.size();
}
const T& Top()
{
return _a[0];
}
Pop堆顶的数据,使之依然为大堆,我们应该如何调整?
- 可以想象一下,我们直接Pop掉堆顶的数据,然后按照我们上面所述的向下调整算法重新调整堆,但这不异于重新建一个堆,代价太大。在这里我们有一个更好的办法:将第一个数据与最后一个数据交换,然后删除最后一个位置的数据,这样我们发现可以用我们的向下调整算法了(因为左右子树都是大堆)
- 我们的实现如下:
void Pop()
{
swap(_a[0], _a[_a.size() - 1]);
_a.pop_back();
AdjustDown(0);
}
Push一个数据在堆底,使之依然为大堆,我们应该如何调整?
- 在堆底插入数据只会影响此节点到根节点所在路径上的子树,所以只需要对这些子树进行调整,使之成为大堆,
void AdjustUp(size_t child)
{
int parent = (child - 1) >> 1;
//循环结束条件
while (child >0)
{
if (_a[child]>_a[parent])
{
swap(_a[child], _a[parent]);
child = parent;
parent = (child - 1) >> 1;
}
else
break;
}
}
void Push(const T& x)
{
_a.push_back(x);
//逐层往上调整该节点到根节点路径上的子树
AdjustUp(_a.size()-1);
}
不考虑类型的泛型堆
- 上一小节我们实现了大堆,那么小堆该如何实现呢?只要孩子节点中较小的那个小于父亲节点,那么就交换父子节点,从最后一个非叶节点开始进行向下调整,最终会建成一个小堆,我们会发现,建大小堆的代码几乎完全一样,除了中间的几个比较不同。我们可以通过仿函数来实现同一份代码,同时实现大小堆。
- 我们先来看一个例子:
template<class T>
struct Less
{
bool operator()(const T& l, const T& r)
{
return l < r;
}
};
template<class T>
struct Greater
{
bool operator()(const T& l, const T& r)
{
return l > r;
}
};
void Test()
{
Greater<int> greater;
cout << greater(1, 2) << endl;
}
- 所以我们只需在Heap的模板参数中添加一个比较大小的类型,就可以将代码中比较大小的事交给仿函数来做,这样让我们的代码复用性更高,下面是我通过仿函数实现的Heap:
#pragma once
#include <iostream>
#include <vector>
using namespace std;
template <class T,class Compare>
class Heap
{
public:
Heap(T* a, size_t n)
{
_a.reserve(n);
for (size_t i = 0; i < n; ++i)
{
_a.push_back(a[i]);
}
for (int i = (_a.size() - 2) / 2; i >= 0; --i)
{
AdjustDown(i);
}
}
void Push(const T& x)
{
_a.push_back(x);
AdjustUp(_a.size() - 1);
}
void Pop()
{
swap(_a[0], _a[_a.size() - 1]);
_a.pop_back();
AdjustDown(0);
}
bool Empty()
{
return _a.empty();
}
const T& Top()
{
return _a[0];
}
size_t Size()
{
return _a.size();
}
protected:
void AdjustUp(size_t child)
{
Compare com;
parent = (child - 1) >> 1;
while (parent > 0)
{
if (com(_a[child], _a[parent]))
{
swap(_a[child], _a[parent]);
child = parent;
parent = (child - 1) >> 1;
}
else
break;
}
}
void AdjustDown(size_t root)
{
Compare com;
size_t parent = root;
size_t child = parent * 2 + 1;
while (child < _a.size())
{
if (child + 1 < _a.size() && com(_a[child + 1], _a[child]))
{
++child;
}
if (com(_a[child], _a[parent]))
{
swap(_a[child], _a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
vector<T> _a;
};
template <class T>
struct Less
{
bool operator()(const T& l, const T& r)
{
return l < r;
}
};
template <class T>
struct Greater
{
bool operator()(const T& l, const T& r)
{
return l > r;
}
};
- 测试用例:
void TestHeap()
{
int a[] = { 10, 11, 13, 12, 16, 18, 15, 17, 14, 19 };
Heap<int,Greater<int>> hp1(a, sizeof(a) / sizeof(int));
}