堆(Heap)是一种常见的数据结构,常用于存储数据,其本质上是一棵完全二叉树。下面我们来看看如何用数组实现堆结构及其相关功能。
堆的定义
首先来看一下堆的存储结构:堆可以看成是一颗完全二叉树。首先什么是二叉树?借助百度中的解释:
二叉树(binary tree)是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树。
总结起来,二叉树可以看成是下面这种数据结构,每一个节点只有左右两个儿子。
而完全二叉树是在二叉树的基础上满足:所有节点都是非空的,最后一层节点的排布是从左到右排布的。而堆是差不多满足这些条件的完全二叉树,其中堆中又以小根堆最出名。小根堆在之前的基础上满足:根节点的数值是所有节点数值里面最小的、每一个节点的数值都是小于它的左右儿子的数值。
堆结构及操作原理
在代码中,我们计划用一个一维数组来实现这个堆,我们首先给堆每一个节点标上号:
那么我们的一维数组,下标为1的元素存的就是节点1,节点1的左儿子和右儿子分别占据下标为2和下标为3的位置,也就是说,对于某个节点x,它的左儿子位于数组的2x位置,它的右儿子位于数组的2x+1的位置。所以上面的堆在一个一维数组中存储方式如下:
堆有很多操作,但是堆的操作总结起来都可以通过按一定顺序反复调用两个函数来实现,这两个函数是down和up函数,它们的参数都是节点序号,其中down函数对应于将一个节点往树的下方调整,up函数对应于将一个节点往上调整。
down函数是将当前节点于其左儿子和右儿子节点的数值进行对比,哪个儿子的数值小就与当前节点换位置:
up函数是将当前节点与其父节点进行对比,一旦当前节点比父节点要小,那么就将当前节点与其父节点进行调换:
利用这两个函数,我们可以实现堆的几乎所有常见的操作,不过在此之前,我们还需要引入一个变量idx,并且创建一个一维数组heap,它表示我们的数组当前使用到的下标的位置,也表示最后一个节点的序号。
1、往堆中插入一个数:我们可以令heap数组的当前下标idx加一,将新数据赋予给新位置。但是这还不够,因为插入到新数不一定是最大的(刚插入的数都是在树的最下层),所以还需要将新的节点往上调,即调用up函数。
heap[++idx]=x;
up(x);
2、在堆中删除一个数:在堆中删除一个数我们的思路是先令这个数和heap数组的最末尾的数进行交换,然后再将末尾的数据删除。为什么要这么做?因为我们要删除的数一般位于数组的中间,删除数组中间的数一般比较麻烦(需要整体移动数组后边的元素)。所以我们采用迂回的方式,先将当前要删掉的数据调整到数组末尾(也就是当前的idx下标的地方)然后再将idx--就可以实现删除了!但是这样相当于将一个不确定的数移到了中间,我们也不确定是需要将其向上调?还是向上调?所以索性就两个操作都写上,需要哪个用哪个。
heap[n]=heap[idx];
idx--;
up[n];
down[n];
Down和Up函数代码
既然Down函数和Up函数如此重要,下面我们来看看如何实现这两个函数。首先是Down函数:
void Down(int x)
{
int t = x;
if (2 * x <= idx && heap[2*x]<heap[t])t = 2 * x;
if (2 * x + 1 <= idx && heap[2 * x + 1] < heap[t])t = 2 * x + 1;
if (x != t)
{
swap(&heap[x], &heap[t]);
my__Down(t);
}
}
Down函数主要是由两个判断,它们分别对应父节点对左儿子和右儿子的比较,首先是确定是否有左儿子和右儿子,即对应的下标x,2x和2x+1都比当前值idx小于或等于。其次才对比左右儿子和父节点的大小,变量t记录的是三个节点中数值最小的那个节点的下标。如果对比出来最小的下标不是父节点(x!=t),那么需要将父节点和最小的节点进行数值交换,最后继续递归这个过程,直至父节点就是最小的节点或者是最后一层。
然后就是Up函数的代码,相较于Down函数,Up函数就是子节点与其父节点进行数值对比:
void Up(int x)
{
while (x / 2 && heap[x / 2] > heap[x])
{
my_swap(&heap[x / 2], &heap[x]);
x = x / 2;
}
}
同样也需要多次进行比较(while循环往上走),想要获取一个点的父节点下标,只需要将其下标除以二即可(由于是int类型,余数自动省略)。其它就是对比以及判断x/2是否为0,即是否为根节点。
初始化
说了这么多,最后还需要看如何对堆进行初始化。首先我们需要准备一个一维数组:
int main()
{
int n = 0;
cin >> n;
idx = n;
for (int i = 0; i < n; i++)
cin >> heap[i];
return 0;
}
然后就是初始化这个一维数组成为一个堆,这里采用一个巧妙地方法,先上代码:
for (int i = idx / 2; i != 0; i--)
Down(i);
这个代码地意思是从倒数第二层开始,将本层的节点从左到右都Down一遍,将大的元素往下调,小的元素往上调,这样就可以逐层向上获取一个堆。
所以总的代码如下:
#include<iostream>
using namespace std;
const int N = 100;
int heap[N];
int idx;
void my_swap(int* x, int* y)
{
int p = *x;
*x = *y;
*y = p;
}
void Down(int x)
{
int t = x;
if (2 * x <= idx && heap[2*x]<heap[t])t = 2 * x;
if (2 * x + 1 <= idx && heap[2 * x + 1] < heap[t])t = 2 * x + 1;
if (x != t)
{
my_swap(&heap[x], &heap[t]);
Down(t);
}
}
void Up(int x)
{
while (x / 2 && heap[x / 2] > heap[x])
{
my_swap(&heap[x / 2], &heap[x]);
x = x / 2;
}
}
int main()
{
int n = 0;
cin >> n;
idx = n;
for (int i = 0; i < n; i++)
cin >> heap[i];
for (int i = n / 2; i != 0; i--)
Down(i);
return 0;
}