二叉堆:
每个父节点的值都大于等于(或者小于等于)其两个孩子的值,那么就称这种特殊的数据结构为二叉堆
二叉堆数据结构是一种数组对象,把它可以看作一棵完全二叉树结构或近似二叉树结构。在逻辑层面二叉堆是一种特殊的树形数据结构,一般用数组存储二叉堆的节点元素,所以在物理层面二叉堆是一个数组
二项式堆,斐波纳契堆等其它堆用的较少,一般将二叉堆简称为堆(以下表述中直接说堆了)
堆满足的两个特征:
- 父节点的键值大于或等于(小于或等于) 任何一个子节点的键值
- 每个节点的左子树和右子树都是一个堆(最大堆或最小堆)
当父节点的键值大于或等于左右孩子节点的键值是称为最大堆
当父节点的键值小于或等于左右孩子节点的键值时称为最小堆
注意:堆中两个孩子之前的大小没有关系,可能左孩子>=右孩子,也可能右>=左
堆的存储:
堆一般存储在下标为0开始的数组中,下图是一个小堆的存储
因此在堆中给定下标为i的结点时:
如果i=0,结点i是根节点,没有双亲节点;否则结点i的双亲结点为(i-1)/2
如果2 * i + 1 <= n - 1,则结点i的左孩子为结点2 * i + 1,否则结点i无左孩子
如果2 * i + 2 <= n - 1,则结点i的右孩子为结点2 * i + 2,否则结点i无右孩子
堆的创建:
下面我们建一个小堆
每一个数组都有其对应的树表示形式,一般情况下,树并不满足堆的条件,需要经过重新排列才能使数组在逻辑层面对应一个堆
将二叉树调整为小堆的原理:
从最后一个非叶子节点开始调整,直至根节点为止, 将每个节点及其子树调整成满足小堆的性质就可以
具体调整方法(针对一个子树来说的):
- 记某个父亲节点的下表为parent
- 找到该节点的左孩子Lchild = 2*parent+1
- 如果右孩子 Rchild = Lchild+1 存在,找到左孩子和右孩子中最小的孩子
- 判断praent与左右孩子中较小者的关系,如果praent小于等于左右孩子中的较小者,调整结束;否则,将parent与左右孩子中最小者交换,此时很可能导致其子树不满足小堆的性质,继续调整子树直到满足堆的性质(这里将用到向下调整算法)
这里先给出定义的堆的结构
//定义一个结构是为了对堆进行更多的操作
typedef int HeapDateType;
typedef struct Heap
{
HeapDateType* _a;
int _size; //数组中有效元素个数
int _capacity; //动态开辟的数组的容量
}Heap;
对该结构体的初始化如下
void HeapInit(Heap* hp, HeapDateType* a, size_t n)
{
assert(hp);
hp->_a = (HeapDateType*)malloc(sizeof(HeapDateType)*n);
assert(hp->_a);
hp->_size = n;
hp->_capacity = n;
for (int i = 0; i < hp->_size; ++i) //这一步初始化不能忽视奥
{
hp->_a[i] = a[i];
}
}
根据上面的步骤写出堆的向下调整算法
void HeapAdjustDown(Heap* hp, HeapDateType root)
{
int parent = root;
int child = 2 * parent + 1;
while (child < hp->_size)
{
//左右孩子的处理比较巧妙,注意理解
if (child+1 < hp->_size && hp->_a[child + 1] < hp->_a[child])
{
child++;
}
if (hp->_a[parent] > hp->_a[child]) //出现这种情况时有可能会破坏该节点以下节点满足的小堆性质
{
swap(&(hp->_a[parent]), &(hp->_a[child]));
}
else //该父子节点及以下的所有节点都满足小堆的性质,不需要继续调整
break;
parent = child;
child = 2 * parent + 1;
}
}
建堆函数,建堆其实就是从最后一个非叶子节点直至根节点不断向下调整的过程
void HeapMake(Heap* hp)
{
assert(hp);
// i 从最后非叶子节点开始
for (int i = (hp->_size - 2) / 2; i >= 0; --i)
{
HeapAdjustDown(hp, i);
}
}
插入操作:
堆的有序性:每个父节点的值小于等于(大于等于)其左右孩子的值被称为堆的有序性
堆的插入是将新数据放在数组最后,这时候很有可能破坏了堆的有序性。从这个新数据的父结点到根结点必然为一个有序的数列,现在的任务是将这个新数据插入到这个有序数据中——这就类似于直接插入排序中将一个数据插入到有序区间中
很显然此时我们要调整堆使之恢复堆的有序性,可以将1和它的父亲比较,如果小于其父,那就交换两个节点的值
此时1和父节点7交换了,我们称这个过程为“上浮”,这个时候1成为了2的右孩子了,此时它比2小,还得继续交换
此时,整个向上调整过程就完成了
根据上述写出向上调整代码
void HeapAdjustUp(Heap* hp, HeapDateType pushchild)
{
HeapDateType child = pushchild;
HeapDateType parent = (child - 1) / 2;
while (parent >= 0){
if (hp->_a[parent] > hp->_a[child])
{
swap(&(hp->_a[parent]), &(hp->_a[child]));
}
else
break;
child = parent;
parent = (child - 1) / 2;
}
}
小堆中插入一个元素
void HeapPush(Heap* hp, HeapDateType x)
{
assert(hp);
if (hp->_size == hp->_capacity) //扩容过程
{
hp->_a = (HeapDateType*)realloc(hp->_a, 2 * hp->_capacity*sizeof(HeapDateType));
assert(hp->_a);
hp->_capacity = 2 * hp->_capacity;
}
hp->_a[hp->_size++] = x;
HeapAdjustUp(hp, hp->_size - 1); //插入节点的下标值
}
删除操作:
堆中每次都只能删掉根节点的数据。为了便于重建堆,实际的操作是将最后一个数据与根节点交换,然后再从根结点开始进行一次向下的调整。调整时先在左右孩子结点中找最小的,如果父结点比这个最小的子结点还小说明不需要调整了,反之将父结点和它交换后再考虑后面的结点。
第一步处理,将最后一个数据与根节点交换,然后逻辑上删除最后一个元素(_size减一)
当堆顶元素大于两个孩子中的任意一个,就需要下沉堆顶元素,直到堆顶元素小于等于它的两个孩子,并且要将其下沉到两个孩子中较小的那一个,因为最小的那个要上浮到堆顶
这个时候堆顶元素9来到了它的左孩子处,但是此时它大于现在的左右孩子,所以要继续下沉
此时对顶元素下沉完毕,二叉树恢复成小堆了
删除操作的代码
void HeapPop(Heap* hp)
{
assert(hp && hp->_size > 0);
swap(&(hp->_a[0]), &(hp->_a[hp->_size-1]));
--hp->_size; //逻辑上删除交换后的堆顶元素
HeapAdjustDown( hp, 0); //然后从堆顶来一次向下调整就好了
}
总结:
堆数据结构最核心的东西就是它的创建插入和删除,是最基础的同事也是最重要的,它的经典应用有优先级队列、100亿个数中找出最大的前K个数(海量数据top K问题)、堆排序,将在后面陆续写一下