目录
堆是一种特殊的二叉树,它在结构上要求是一个满二叉树数,而在结点的内容上,也有特殊的要求——父亲结点的数组一定是大于(小于)或等于它的子结点的,我们把这种堆叫做大根堆(小根堆),因此堆有一个特别重要的性质——根结点的数据一定是当前堆中的极值。
一、堆结点的构造
堆的结构和我们之前写过的顺序表非常的一直,但是前文说了,堆是一个特殊的满二叉树,那不是应该用链表的结构来存储就更方便吗?其实并不会,因为堆的性质,在根的位置一定是堆里的极值,所以在插入和删除数据的时候,就会进行很多次的改变数据位置的操作,这种操作在数组中的操作却是很方便的;一个满二叉树父亲节点在数组中的位置为(它的子节点-1)/2(左右结点运算后的结果相同),同样(父亲节点*2+1)为左孩子结点的位置,(父亲节点*2+2)为右孩子结点的位置;基于这个结论,我们就可以很方便在数组中找到要改变的数组的相应位置,这样我们就顺利的把一个在物理结构上是数组,逻辑结构上是满二叉树的结构构造好了。
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}Hp;
二、堆的初始化
前面既然已经说了堆的结构在物理上和顺序表是一直,那很多的操作都和顺序表是一样的,因为堆内没有数据,所以堆的容量和数据个数都为0,此时还没有为堆中存储数据的数组分配空间,所以把数组的指针指向空。
void HeapInit(Hp* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
三、堆的销毁
因为堆内的数组是我们动态申请的空间,所以在销毁的时候要先释放申请的空间,再把指向这块空间的指针指向空,防止它成为野指针,同时将容量和数据个数置为0。
void HeapDestory(Hp* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
四、插入数据
(一)插入操作
由于堆的每个结点都要满足父亲节点的值大于(小于)或等于它的子节点的值的性质,所以,当我们在最后一个位置插入数据的时候,可以发现,如果只把最后插入的结点和它的亲兄弟节点以及父亲节点看成一个独立的结构,只有这个几个结点是不满足堆的条件的,排除这几个结点以后,剩下的结点依然可以构成一个堆,这个时候我们就要用向上调整算法,依次的把目前的几个结点调整为一个小堆,然后再依次往上调整。
void HeapPush(Hp* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail!");
return;
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a,php->size-1);
}
(二)向上调整算法
传入的结点为我们要调整的初始化位置,先求出它的父亲结点的位置,然后比较这个父亲结点的值和当前这个结点的值的大小,如果父亲结点的值要大于这个子结点的值,那说明此时不满足堆的条件,但是只要交换这两个数据的位置,此时这几个结点就能构成一个小堆了,但是交换结点以后,被交换上去的结点又可能破坏它和它原先的父亲结点之间的关系,所以还要继续判断是否需要调整,如果需要调整就重复以上的步骤,值到子结点的位置到达堆的根为止,此时子结点为根,那么它就没有父亲结点了,此时也就不需要再进行调整了,这样插入数据以后的堆又满足条件;如果遇到了不需要调整的情况,说明调整到当前位置的时候,已经满足堆的条件了,不需要再进行调整了。
void Swap(HPDataType* a, HPDataType* b)
{
HPDataType tmp = *a;
*a = *b;
*b = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
assert(a);
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;
}
}
}
五、取出数据
(一)取出操作
因为堆的特性,所以在堆中,我们只能确定一个堆的根是极值,所以取出其他位置的数据都没有意义,只需要取堆的根的位置的数据即可。
因为取出第一个数据以后,如果我们采用向前挪动数据的方式填补空位的话,整个堆的结构就被破坏了,这样再构建一个新的堆的时间成本太大了,因此我们在取出堆顶数据的时候,就会把堆的最后一个叶子结点的数据移动到根的位置,这样除了根以外的其他结点都还满足堆的条件,我们就只需要对根结点进行调整就行,这时候就需要用到向下调整算法了。
void HeapPop(Hp* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->a[0],&php->a[php->size-1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
(二)向下调整算法
这里用到的是假设法,因为一个父亲结点可能只有一个子结点,或者可能有两个子结点,并且这两个子接都有可能比现在的这个父亲结点的值要小,所以我们假设左孩子是更小的那个结点,当右孩子存在,并且右孩子的值要小于左孩子时,说明需要和父亲结点交换位置的是右孩子,所以只要把假设的child++,就能得到右孩子的位置了;与向上调整算法类似的,当前结点调制以后,可能破坏原来孩子结点位置与它的孩子结点之间的大小关系,还需要再次进行判断,如果需要调整就继续进行调整,直到孩子结点的位置超出我们数组的范围时,则这个结点不存在孩子结点,他就是叶子结点,调整就完成了;当遇到不需要调整的情况时,说明不满足条件的部分已经被调整完了,此时堆的根又是极值。
void AdjustDown(HPDataType* a, int last, int parent)
{
assert(a);
int child = parent * 2 + 1;
while(child<last)
{
if (child + 1 < last && a[child] > a[child + 1])
{
child++;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
六、取堆顶数据
数组的0下标位置对应的就是堆的根的位置。
HPDataType HeapTop(Hp* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
七、获取堆内数据个数
size成员变量就是记录的就是堆内的数据个数。
int HeapSize(Hp* php)
{
assert(php);
return php->size;
}
八、判空
bool HeapEmpty(Hp* php)
{
assert(php);
return php->size == 0;
}
九、堆排序
堆排序是要堆一个已有的数组进行排序,考虑到时间复杂度和空间复杂度,所以需要在不另外开辟空间的情况下完成堆数组的排序,借助前面构建的堆的功能,我们可以知道堆的根的位置是数组中的极值,那如果要对一个数组进行降序排序,那么我们只需要取出用这个数组构成的堆的根位置的数据放在最后,对剩下的数据进行向下调整的算法,直到所有这个堆中没有数据为止,此时这个数据就排序完成了。
在用这个数组构建堆时,采用向下调整算法的方式是效率最高的,因为向下调整算法要求原始的结构是一个堆,所以可以采用由下完上的方式进行构造,并且所有的叶子结点都可以看成是一个堆的根,那么我们就可以跳过叶子结点,从最后一个叶子结点的父亲结点的位置开始向上构造堆。
void HeapSort(HPDataType* a,int n)
{
assert(a);
//
//С
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--;
}
}