堆:堆的表现形式其实是一棵完全二叉树,只不过,该完全二叉树中的元素有一些其他的考究。
1、首先堆分为大堆和小堆
2、大堆(小堆):任一节点的关键码大于(小于)等于他的左右孩子的关键码,位于堆顶节点的关键码永远是这棵二叉树中最大(最小)的一个值,从根节点到每个节点的路径上数组元素组成的序列都是递减(递增)的
1、堆存储在下标为0开始的数组中,因此在堆中给定下标为i的点时:
2、如果 i = 0 那么节点 i 就是当前堆的根节点(即堆顶元素),没有双亲节点,否则节点 i 的双亲节点为节点(i -1)/ 2。
3、如果2 * i+1 <= n-1,则节点 i 的左孩子为节点2 * i +1,否则无左孩子。
4、如果2 * i+2 <= n-1,则节点 i 的右孩子为节点2 * i +2,否则无右孩子。
下面我们一C语言实现堆的相关基本操作。
heap.h文件内容
#pragma once
#define max_size 1000
typedef char DataType;
typedef int (*Compare)(DataType a,DataType b);
typedef struct Heap
{
DataType data[max_size];
int size;//有效元素个数
Compare cmp;//判断大小堆的函数的函数指针
}Heap;
//初始化
void HeapInit(Heap *heap,Compare cmp);
//销毁
void HeapDestroy(Heap *heap);
//往堆中插入元素
void HeapInsert(Heap *heap,DataType to_insert);
//取堆顶元素
int HeapRoot(Heap *heap,DataType *root);
//删除堆顶元素
void HeapErase(Heap *heap);
//堆的创建
void HeapCreate(Heap *heap,DataType arr[],int len);
//堆排序
void HeapSort(DataType arr[],int len);
heap.c文件内容
#include<stdio.h>
#include<string.h>
#include"heap.h"
#define Test_Header printf("\n==========%s==========\n",__FUNCTION__);
//大堆判断函数
int Greater(DataType a,DataType b)
{
return a > b ? 1 : 0;
}
//小堆判断函数
int Less(DataType a,DataType b)
{
return a < b ? 1 : 0;
}
//初始化
void HeapInit(Heap *heap,Compare cmp)
{
if(heap == NULL)
{
//非法输入
return;
}
heap->size = 0;
heap->cmp = cmp;
}
//销毁
void HeapDestroy(Heap *heap)
{
if(heap == NULL)
{
//非法输入
return;
}
heap->size = 0;
heap->cmp = NULL;
}
//初始化函数测试
void TestInit()
{
Test_Header;
Heap heap;
HeapInit(&heap,Greater);
printf("expected size = 0,actual size = %d\n",heap.size);
printf("expected &cmp = %p,actual &cmp = %p\n",Greater,heap.cmp);
}
测试结果:
往堆中插入元素操作思路:
首先我们将待插入元素直接插入对重的最后面(因为堆是存储在数组中的,所以该操作利用数组的下标可直接一步完成),在调整堆中的元素位置,使之符合一开始的堆的规则(大堆或小堆),不能插入元素之后破坏了堆。
以上也就是我们用到的上浮式调整堆的方法。
//元素交换函数
void swap(DataType *a,DataType *b)
{
DataType tmp = *a;
*a = *b;
*b = tmp;
return;
}
//元素调整函数(上浮式调整)
void AdjustUp(Heap *heap,int index)
{
if(heap == NULL)
{
//非法输入
return;
}
int child = index;
int parent = (child-1)/2;
while(child > 0)
{
if(heap->cmp(heap->data[child],heap->data[parent]))
{
swap(&heap->data[parent],&heap->data[child]);
}
else
{
break;
}
child = parent;
parent = (child-1)/2;
}
}
//往堆中插入元素
void HeapInsert(Heap *heap,DataType to_insert)
{
if(heap == NULL || heap->size >= max_size)
{
//非法输入或者堆满了
return;
}
//先直接将元素插入堆的最后一个元素
heap->data[heap->size] = to_insert;
heap->size++;
//再调整元素的位置,使之符合堆的规则
//开始调整的位置就是从刚插入的元素开始往上
//刚插入的元素的下标就是size-1
AdjustUp(heap,heap->size-1);
return;
}
//测试一下
void TestInsert()
{
Test_Header;
Heap heap;
HeapInit(&heap,Greater);
HeapInsert(&heap,'d');
HeapInsert(&heap,'a');
HeapInsert(&heap,'b');
HeapInsert(&heap,'h');
HeapInsert(&heap,'e');
int i = 0;
for(;i < heap.size;i++)
{
printf("[%c|%d] ",heap.data[i],i);
}
printf("\n");
}
测试结果:
取堆顶元素操作思路:直接去数组的第一个元素就是堆顶。
删除堆顶元素操作思路:因为堆是存储在数组中的,而我们删除数组的最后一个元素比较简单可以一步完成,因而我们先将待删除的堆顶元素与堆的最后一个元素交换,此时,堆顶元素就到了堆的最后面,利用数组下标可直接将其删除。但是由于我们交换了元素,从而破坏了堆的结构,所以我们需要再对其调整,使之符合堆的规则。
这就是堆操作中的下沉式调整堆元素。
//取堆顶元素,取堆顶元素失败返回0,成功返回1
int HeapRoot(Heap *heap,DataType *root)
{
if(heap == NULL)
{
//非法输入
return 0;
}
if(heap->size == 0)
{
//空堆
return 0;
}
*root = heap->data[0];
return 1;
}
//元素调整函数(下沉式调整)
void AdjustDown(Heap *heap,int index)
{
if(heap == NULL)
{
//非法输入
return;
}
int parent = index;
int child = 2 * parent + 1;
while(child < heap->size)
{
//先找出当前根节点的左右子树较小的一个节点
if(child+1 < heap->size && heap->cmp(heap->data[child+1],heap->data[child]))
{
child = child+1;
}
//用交换后的堆顶元素与他的左右孩子中较大(小)的一个相比较
if(!heap->cmp(heap->data[parent],heap->data[child]))
{
//如果不满足当前所建堆的规则则交换二者的值
swap(&heap->data[parent],&heap->data[child]);
}
else
{
break;
}
//更新parent与child,用于下一次的循环比较
parent = child;
child = 2 * parent + 1;
}
}
//删除堆顶元素
void HeapErase(Heap *heap)
{
if(heap == NULL)
{
//非法输入
return;
}
if(heap->size == 0)
{
//空堆
return;
}
//交换堆顶元素和最后一个元素
swap(&heap->data[0],&heap->data[heap->size-1]);
//在将最后一个元素删除
heap->size--;
//以根节点开始调整元素使之符合堆的规则,下沉式调整
AdjustDown(heap,0);
return;
}
//测试一下
void TestHeapRootAndErase()
{
Test_Header;
Heap heap;
HeapInit(&heap,Greater);
HeapInsert(&heap,'d');
HeapInsert(&heap,'a');
HeapInsert(&heap,'b');
HeapInsert(&heap,'h');
HeapInsert(&heap,'e');
printf("【取堆顶元素函数测试】\n");
DataType get_root;
int ret = HeapRoot(&heap,&get_root);
if(ret == 1)
{
printf("expected get_root = h,actual get_root = %c\n",get_root);
}
printf("【删除堆顶元素函数测试】\n");
HeapErase(&heap);
int j = 0;
for(;j < heap.size;j++)
{
printf("[%c|%d] ",heap.data[j],j);
}
printf("\n");
}
测试结果:
//堆的创建
void HeapCreate(Heap *heap,DataType arr[],int len)
{
if(heap == NULL)
{
//非法输入
return;
}
//直接调用之前的插入元素函数即可
int i = 0;
for(;i < len;i++)
{
HeapInsert(heap,arr[i]);
}
return;
}
//测试一下
void TestCreate()
{
Test_Header;
Heap heap;
HeapInit(&heap,Greater);
DataType arr[] = "dabhe";
HeapCreate(&heap,arr,5);
int j = 0;
for(;j < heap.size;j++)
{
printf("[%c|%d] ",heap.data[j],j);
}
printf("\n");
}
测试结果:
//堆排序
void HeapSort(DataType arr[],int len)
{
//先将数组中的元素创建成一个堆
Heap heap;
HeapInit(&heap,Greater);
HeapCreate(&heap,arr,len);
//先取到堆顶元素
DataType get_root;
int i = len-1;
//如果跳出循环说明堆空了。也就说明排序完成
while(HeapRoot(&heap,&get_root))
{
//取到堆顶元素之后将其赋给数组的最后一个元素(升序排序)
arr[i] = get_root;
//更新i的值用于下一次赋值
i = i-1;
//再将该堆顶元素删除
HeapErase(&heap);
}
return;
}
//测试一下
void TestSort()
{
Test_Header;
DataType arr[] = "dabhe";
HeapSort(arr,5);
printf("期待升序排序结果:a b d e h\n实际升序排序结果:");
int i = 0;
for(;i < 5;i++)
{
printf("%c ",arr[i]);
}
}
测试结果:
总结一下:在操作堆 的过程中,其中用到的上浮式调整和下沉式调整这两种方法对于堆的操作很重要。理解了这两种方法,不仅有利于我们实现堆的相关操作,也可以帮助我们加深对于堆的特点的理解。