特别需要注意的是堆是用数组实现的,因为堆中的元素是顺序存储的,而数组中的元素也是顺序存储的。(一般的二叉树不用数组来实现,而是用链表来实现,因为普通的二叉树中可能存储元素的密度很低,用连续存储的结构会造成大量的空间浪费。
堆结构的初始化:和初始化一个数组类似,都是创建一个指针,和一些以后能用到的值。(针对结构体而言,不同于链表中结构体就代表链表中的某一个具体的节点,有关数组的结构体相当于对数组整体都可以施加影响的操作)
void HeapInit(HP* hp)
{
assert(hp);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
堆的销毁:要注意不能free(hp),hp->a才是特定指向整个数组的指针。Free掉这个指针才能释放整个数组的空间。(这里的指针a专门指向整个数组,不需要用它来找某个特定的节点,因为数组是随机存取的。
void HeapDestory(HP* hp)
{
assert(hp);
free(hp->a);
hp->capacity = hp->size = 0;
}
堆的向上调整:例如,对于一个小段存储的栈,再其数组末端插入了一个较小的数,显然如果想继续保持整个栈小端存储的结构,这个数字不应该是在它目前所在的位置上,这时就应该将这个节点按规律向上调整,直到满足要求为止。这里就需要思考,为什么在加入新节点之后栈不满足小端存储了,无非是因为双亲结点和孩子结点之间的相对大小关系被破坏了,因此想要将栈恢复为小端存储,就需要着手处理不满足条件的孩子节点与双亲结点。由于其它结点处都没有发生变化,因此,除了新插入的孩子节点和其双亲结点之外,其它的结点之间的关系仍然没有问题,当交换了出问题的那个双亲结点和孩子节点之后,唯一可能出问题的仍然是这个已经成为双亲结点的新插入的结点和它现在的双亲结点之间的关系。其余的不需要关心。依此类推不断处理、判断,直到满足条件(插入结点到根节点的路径上孩子结点不再大于双亲结点)或者已经处理到根结点为止。
补充:在二叉树中,双亲结点的下标等于其任何一个孩子节点的下标减1除以2。(0.5会直接忽略不计)
void AdjustUp(int* a, int n, int child)
{
assert(a);
int parent = (child - 1) / 2;
//while (parent >= 0)
while (child > 0)//当child>=0时,说明已经比较到头节点了
{
if (a[child] > a[parent])//如果孩子节点大于双亲节点,说明这时整个堆的存储结构不符合大端存储,需要进行改动,即将大于双亲节点的孩子节点与双亲节点互换。那调整过后需不需要再次将新的双亲节点和另一个孩子节点相比较呢?答案是不需要,因为对于原先就是大端存储的堆而言,原本双亲节点就一定大于其孩子节点,因为我插入了一个新的节点,所以才破坏了大端存储的结构,当我调整完新插入的孩子节点和原先的双亲节点的顺序之后(如果有必要的话),另一个孩子节点一定小于原先的双亲节点,也一定小于新的双亲节点,所以没有调整的必要。事实上,如果画图的话,新插入的孩子节点如果需要调整,所有需要调整的节点所连城的线就是新插入的孩子节点到根节点的路径。
{
HPDataType tmp = a[child];
a[child] = a[parent];
a[parent] = tmp;//交换两个节点的位置
child = parent;
parent = (child - 1) / 2;//将现在的双亲节点看成孩子节点,再找到这个孩子节点的双亲节点
}
else
{
break;
}
}
}
堆的向下调整:不同于向上调整,向下调整中多出了一个代表数组包含元素数量的变量n,因为对于向上调整而言,元素的下表只会越来越小,直到0为止,但是对于向下调整而言,如果不规定向下调整的边界,就可能会越界。
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] < a[child + 1])
{
child++;
}//向上调整只可能经过一个对象,即双亲结点,而向下调整却有两条路可走,即两个孩子结点。要想确定走那一条路,就需要对两个孩子结点的值的大小进行比较
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
打印出堆:
void HeapPrint(HP* hp)
{
int i = 0;
for (i = 0; i < hp->size; i++)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
堆的初始化:对于任意一个数组而言,当将其排列成二叉树的结构时(计算机中并不用数来表示数组,这只是针对人的思维而言的抽象),这个结构中的元素都是随机排列的,不满足堆的结构要求,因此要对其进行调整。调整要从从后往前的第一个非叶子结点开始,因为叶子结点就是单独的一个结点,根本没有比较或者调整的余地。如果一个数组中有n个元素,那其最后一个结点的下标即为n-1,最后一个结点的双亲结点即为最后一个非叶子结点,其下标为((n-1)-1)/2=(n-2)/2。每一个循环过后,起码就该结点极其孩子结点之间一定满足堆的要求,依此类推,每一个双亲结点都满足要求之后,整个二叉树就变成了一个堆。
void HeapInit(Heap* hp, HPDataType* a, int n)
{
int i;
assert(hp && a);
hp->_a = (HPDataType*)malloc(sizeof(HPDataType) * n);
hp->_size = n;
hp->_capacity = n;
for (i = 0; i < n; ++i)
{
hp->_a[i] = a[i];
}
for (i = (n - 2) / 2; i >= 0; --i)
{
AdjustDown(hp->_a, hp->_size, i);
}
}
向堆中尾插一个元素(假设这个堆采用的是小端存储,这时就需要用到向上调整):尾插元素还是有之前的套路,先判断数组的空间是否够用,如果不够的话,就用realloc新申请一块空间。将这块新的空间赋给数组,再进行插入操作。插入操作之后,可能会破坏原先小/大堆存储的结构,因此要进行向上/下调整。
void HeadPush(HP* hp, HPDataType x)
{
assert(hp);
if (hp->capacity = hp->size)
{
int mewcapacity = hp->capacity == 0 ? 4: hp->capacity * 2;
HPDataType* tmp = realloc(hp->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
hp->capacity = newcapacity;
hp->a = tmp;
}
hp->a[hp->size] = x;
hp->size++;
AdjustUp(hp->a, hp->size, hp->size - 1);
}
在堆中头删一个元素:如果直接将根节点从堆中删除,可能需要重新对堆进行排序,非常麻烦。常规的删除堆中的头节点的方法是将头节点和最后一个结点交换,通过size—就实现了删除该元素的目的。对于新的根节点,其一定不满足堆的要求,将其按照向下调整的算法进行调整即可。
void HeapPop(Heap* hp)
{
assert(hp);
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown(hp->a, hp->size, 0);
}
找出n个数中最大的k个元素:
注:为什么要先建一个含k个元素的小堆,再进行比较?因为将所有的暑假同时建堆的话,比如一个国家的人口,那会超过计算机的内存,如果构建一个堆的话,就可以将待处理的数字放入文件中,然后每次从文件中拿出一个数据和现成的堆进行比较,不需要将所有的元素都拿出来建堆。
void PrintTopK(int* a, int n, int k)
{
Heap hp;
//建立含有K个元素的堆
HeapInit(&hp, a, k);
for (size_t i = k; i < n; ++i) // N
{
//每次和堆顶元素比较,大于堆顶元素,则删除堆顶元素,插入新的元素
if (a[i] > HeapTop(&hp)) // LogK
{
HeapPop(&hp);//删除原先的堆顶元素
HeapPush(&hp, a[i]);//将新的符合条件的元素插入堆中
//这两步中包含了极其复杂的操作过程,比如删除堆顶元素需要将K个元素的堆中根节点和最后一个结点互换为止,然后删除原先的根节点后再对新的根节点向下调整。
//而将新的结点插入堆中需要先将该结点插入堆中的最后一个结点中,然后再向上调整,将结点放到它应该在的位置上以满足堆的要求。
//这两步操作非常复杂,单之前已经完成了对这些步骤的定义,所以现在看起来很简单,只需要两步
}
}
for (int i = 0; i < k; ++i)
{
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
}
找出n个数中最小的k个元素:和找最大的k个元素道理相同。只不过找最大的k个元素需要构建含k个元素的小堆,保证目前找到的k个最大的元素最小的那一个一定在堆的最顶端,方便与后面的元素比较,后面的元素只要大于这个堆中最小的元素,就有资格替换掉这个元素进入堆中。而找出最小的k个元素则需要构建大堆,每次替换都替换掉最大的那个元素。
void PrintTopK(int* a, int n, int k)
{
Heap hp;
//建立含有K个元素的堆
HeapInit(&hp, a, k);
for (size_t i = k; i < n; ++i) // N
{
//每次和堆顶元素比较,小于堆顶元素,则删除堆顶元素,插入新的元素
if (a[i] < HeapTop(&hp)) // LogK
{
HeapPop(&hp);
HeapPush(&hp, a[i]);
}
}
for (int i = 0; i < k; ++i) {
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
}
删除堆:
void HeapDestory(Heap* hp)
{
assert(hp);
free(hp->a);//释放掉堆的结构体中指针指向的数组,即堆本身被释放
hp->a = NULL;//释放掉一块空间后不要忘记释放指向这块空间的指针,否则会出现野指针
hp->capacity = hp->size = 0;
}