目录
堆
定义
如果有一个元素的集合,,将其所有的元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足
且
(
且
)i=0,1,2...,则称为小堆(大堆)。将根结点最大的堆叫做最大堆或者大根堆,反之,根结点最小的堆叫做最小堆或者小根堆。
性质
由上面的定义我们可以知道1、堆的某个结点的值总是不大于或者不小于其双亲结点。
2、堆总是一棵完全二叉树。
上图是大顶堆和小顶堆的逻辑结构和存储结构。
向下(上)调整算法
在给出堆的实现之前,我们先给出一个调整算法,该算法在堆的创建与堆的各接口实现中占着举足轻重的地位。
现在我们给出一个数组,其在逻辑上可以看作一个完全二叉树,我们可以通过向下调整算法从根结点开始将其调整为一个小(大)堆,但是该算法有一个前提为该树的根结点的左右子树必须是堆。
向下调整算法
当堆中删除元素时(通常都是堆顶元素),为了不改变堆的性质,我们需要向下调整算法。调整小(大)堆时,通过双亲结点与孩子结点的关系,从根结点开始找到其孩子节点并比较大小,遇到比根结点小(大)的就交换,直到找不到孩子为止。
void AdJustDown(HPDataType* a, int n, int root)//向下调整算法
{
//找到最小的孩子
int parent = root;
int child = 2 * parent + 1;//先把左孩子当作两个孩子中最小的
while (child < n)
{
if (child+1<n&&a[child + 1] < a[child])//防止越界
{
child++;
}
else
{
if (a[child] < a[parent])
{
//父子节点交换
Swap(&a[child], &a[parent]);
}
else
{
break;
}
parent = child;
child = 2 * parent + 1;
}
}
}
向上调整算法
当堆中插入新的元素时,为了不改变当前堆的性质,通常需要向上调整算法。
向上调整算法是从数组中最后一个元素(完全二叉树最后一个叶结点)开始向上与该结点的双亲结点比较,判断是否交换,直到找到根结点。
void AdJustUp(HPDataType* a, int n, int root)//向上排序算法
{
int child = root;//孩子节点
int parent = (child - 1) / 2;//父亲节点
while (child>0)
{
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
堆的创建与初始化
创建
在实际中,对于给出的一个数组,这个数组在逻辑上可以看成是一棵完全二叉树,但是其根结点的左右子树并不满足堆,这种情况下我们该怎么调整呢?
我们考虑可以从该树的倒数第一个非叶子结点的子树开始调整,一直 调整到根节点的树,这样就可以调整成堆。
要对堆进行操作时,需要有一个数组来保存数据,进行插入和删除操作时,我们还需要知道堆的大小和有效数字个数,所以我们用一个结构体来表示堆,里面的含三个成员变量。
typedef struct Heap
{
HPDataType* _a;
int _size;
int _capcity;
}Heap;
以下为建小堆
for (int i = (n - 1 - 1) / 2; i>=0; i--)
{
AdJustDown(a, n, i);
}
初始化
因为给出的是一个数组,所以我们首先考虑将该数组利用memcpy函数将原数组中的数据copy到堆的数组中。
void HeapInit(Heap* php, HPDataType* a, int n)//堆的初始化
{
assert(a);
php->_a = (HPDataType*)malloc(sizeof(HPDataType) * n);
memcpy(php->_a, a, sizeof(HPDataType) * n);
php->_capcity = n;
php->_size = n;
}
建堆的复杂度
因为堆是完全二叉树,满二叉树也是完全二叉树,所以为了简便,我们就使用满二叉树来得出完全二叉树建堆得时间复杂度。(因为时间复杂度并不是一个精确值,只是一个近似值,所以多有限个结点在极限情况下并不会影响到总体的水平)。
堆的插入
如图所示,插入是在数组最后面(树的最后的结点后)插入,插入数据之后,还要保持堆的特性,所以这里需要向上排序算法,如果堆的性质被破坏,将新插入的结点顺着其双亲向上调整到合适位置。
void HeapPush(Heap* php,HPDataType x)
{
assert(php);
if (php->_capcity == php->_size)
{
php->_capcity *= 2;
HPDataType* tmp = (HPDataType*)realloc(php->_a, sizeof(HPDataType) * php->_capcity);
if (tmp == NULL)
{
printf("增容失败\n");
exit(-1);
}
else
{
php->_a = tmp;
php->_a[php->_size++] = x;
AdJustUp(php->_a, php->_size, php->_size - 1);
}
}
}
堆的删除
删除数据是删除堆顶元素(完全二叉树的根结点),首先将要删除的堆顶元素与堆中的最后一个元素交换,然后将换下来的最后一个元素删除,此时已经破坏了堆的性质,但是删除了最后一个元素后,根结点的左右子树还都满足着堆的性质,所以此时只需要对堆的根结点进行一次向下调整算法即可。
void HeapPop(Heap* php)
assert(php);
assert(php->_size > 0);
Swap(&php->_a[0], &php->_a[php->_size - 1]);
php->_size--;
AdJustDown(php->_a, php->_size, 0);
}
堆顶元素
HPDataType HeapTop(Heap* php)//获取堆顶元素
{
assert(php);
return php->_a[0];
}
堆的销毁
void HeapDestroy(Heap* php)//销毁堆
{
assert(php);
free(php->_a);
php->_capcity=php->_size = 0;
}
堆的应用
堆排序
即用堆的思想来进行排序,在建成的堆的基础上,不断将堆顶元素和堆最后一个元素交换,每次交换完之后再对对堆的其他元素进行向下调整算法,这样就可以不断把剩下元素中最大(小)的元素通过向下调整算法放在堆顶,再通过与堆最后一个元素不断交换将其放在后面。这样最后我们就可以得到一个有序数组。
1、建堆
要进行堆排序,我们首先要创建一个堆,这里需要注意的是如果我们需要对数组进行升序排序则我们需要建大堆,如果对数组进行降序排列则需要建小堆。
2、利用堆删除思想进行排序
如下图所示,下图为升序排序,首先建大堆,此时堆顶元素一定是数组中最大的元素,将他与堆末尾元素交换,此时假设该最大元素已经被删除,将剩下的元素进行向下排序算法,此时又构成大堆,堆顶元素依然是剩下这些元素中最大的,再进行交换,假设第二大元素也被删除。重复上述过程,最后便能得到一个升序序列。
void HeapSort(HPDataType* a, int n)//堆排序
{
//建堆
for (int i = (n - 1 - 1) / 2; i>=0; i--)
{
AdJustDown(a, n, i);
}
int end = n - 1;
while (end>0)
{
Swap(&a[0], &a[end]);
AdJustDown(a, end, 0);
end--;
}
}
3、堆排序特性总结
1)堆排序使用堆来选数,效率提升不少
2)时间复杂度:
3)空间复杂度:
4)稳定性:不稳定