关于堆的知识
什么是堆
可能概念讲的不是很好理解,下面我们看两幅图。
那么问题来了?我们给的一组数据总不可能就那么巧吧,正好是有序的。这时就要介绍一个算法了:堆的向下排序算法。使一组不是那么有规律的数字也变成堆。
堆的向下排序算法
int array[] = {27,15,19,18,28,34,65,49,25,37};
从这组数据中你能发现什么特点吗?也不难发现,这组数据除了根节点,左右子树都是小堆。这也是使用向下调整算法的前提:
左右子树必须是一个堆,才能调整。
下面我们来看调整的过程:让根和它孩子中较小的孩子进行交换,然后孩子的位置换成父亲节点,父亲节点再找孩子节点,以此达到迭代的目的。
代码实现
void swap(int* children, int* gen)
{
int temp = *children;
*children = *gen;
*gen = temp;
}
//建的小堆
void Heapdown(int *a, int n,int gen)
{
//假设法:假设目标值是你想要的值
int children = gen * 2 + 1;
while (children<n)
{
if (children+1<n&&a[children + 1] < a[children])
{
children++;
}
if (a[children] <= a[gen])
{
//要传地址,不然影响不到外面
swap(&a[children],&a[gen]);
}
//为什么可以直接break出去了,这是由使用堆的向下调整算法的前提条件决定的,因为
//你只有满足左右子树都是小堆或大堆才能使用这个方法,所以这个时候你可以直击break
//出去,不然你继续向下迭代也没有意义,因为它根本不会发生交换,最后会因为children>n
//跳出循环。
else
{
break;
}
gen = children;
children = gen * 2 + 1;
}
}
注意点:
1.巧用假设法,假设目标值为小的孩子,这样可以大大简化代码。
2.要保证children+1<n以防数组越界。
3.swap传参的时候要传地址,这样才能影响到外面。
如何对无规律的数组进行堆的向下排序算法使其成为堆
到这儿, 相信聪明的你一定会问了?如果给的一组数据完全没有规律可循了,那怎么办了?
下面我们就来看看堆的向下排序的妙处:从后往前排,来解决这个问题。
其实这种思想我是这样理解的:如果你想让整棵树都成为小堆,那么你就要让其中各个部分变成小堆,从最后一个父亲节点开始逐渐调整。
代码实现
void swap(int* children, int* gen)
{
int temp = *children;
*children = *gen;
*gen = temp;
}
//建的小堆
void Heapdown(int *a, int n,int gen)
{
//假设法:假设目标值是你想要的值
int children = gen * 2 + 1;
while (children<n)
{
if (children+1<n&&a[children + 1] < a[children])
{
children++;
}
if (a[children] <= a[gen])
{
//要传地址,不然影响不到外面
swap(&a[children],&a[gen]);
}
else
{
break;
}
gen = children;
children = gen * 2 + 1;
}
}
int main()
{
int a[] = { 27, 15, 18, 13, 19, 21, 28, 29, 1, 2 };
int sz = sizeof(a) / sizeof(int);
int gen = (sz - 1 - 1) / 2;
int i = gen;
for (i = gen; i >= 0; i--)
{
//向下调整算法,用来建堆
Heapdown(a, sz, i);
}
for (i = 0; i < sz; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
注意点:这里要注意的就是,父亲节点与孩子节点之间的关系。
堆的向上调整算法
既然堆有向下调整算法,那么相对的也就有向上调整算法。
当我们在一个堆的末尾插入一个数据后,需要对堆进行调整,使其仍然是一个堆,这时需要用到堆的向上调整算法。
向上调整算法的基本思想(以建小堆为例):
1.将目标结点与其父结点比较。
2.若目标结点的值比其父结点的值小,则交换目标结点与其父结点的位置,并将原目标结点的父结点当作新的目标结点继续进行向上调整。若目标结点的值比其父结点的值大,则停止向上调整,此时该树已经是小堆了。
实现代码
void AdjustUp(int* a,int child)
{
int father = (child - 1) / 2;
while (child!=0)
{
if (a[child] > a[father])
{
swap(&a[child], &a[father]);
child = father;
father = (child - 1) / 2;
}
else
{
break;
}
}
}
注意点:
while里面的判断条件最好不要写上father>=0,这样写只不过是碰巧能过而已,因为到最后一次的时候(0-1)/2等于0,进循环0和0比,走的是break,因此这样跳出了循环。
建堆的时间复杂度
到这人你可能会问了?那么建堆的时间复杂度是多少了?
下面我们来看看推导过程(可能字写的有些丑,大家请见谅):
注意上图是以向下调整算法为例的,其实向上调整算法建堆的时间复杂度也为O(N)。
堆排序
前面介绍的东西其实都是为了堆排序做铺垫的。
下面我们来想一个问题:如果我们把一组数据排成降序,应该是用小堆还是大堆了? 相信很多人肯定会首先回答:大堆。因为大堆的0号位数字为最大的数,然后再找次大的数字。但这样的话会有一个很严重的问题:那就是堆的结构被破坏了。
下面我们来看一个例子:
当我们再次找最大的数据时,我们发现这时堆的结构已经被破坏了,如果你要继续找的话,还要将他进行建堆,效率低。
正确的做法应该是建小堆:
代码实现
void swap(int* children, int* gen)
{
int temp = *children;
*children = *gen;
*gen = temp;
}
//建的小堆
void Heapdown(int *a, int n,int gen)
{
//假设法:假设目标值是你想要的值
int children = gen * 2 + 1;
while (children<n)
{
if (children+1<n&&a[children + 1] < a[children])
{
children++;
}
if (a[children] <= a[gen])
{
//要传地址,不然影响不到外面
swap(&a[children],&a[gen]);
}
else
{
break;
}
gen = children;
children = gen * 2 + 1;
}
}
int main()
{
int a[] = { 27, 15, 18, 13, 19, 21, 28, 29, 1, 2 };
int sz = sizeof(a) / sizeof(int);
int gen = (sz - 1 - 1) / 2;
int i = gen;
for (i = gen; i >= 0; i--)
{
//向下调整算法,用来建堆
Heapdown(a, sz, i);
}
//堆排序,用小堆的话排的就是降序
//用大堆的话排的就是升序
int end = sz - 1;
for (end = sz - 1; end > 0;)
{
swap(&a[0],&a[end]);
end--;
sz--;
for (i = (sz - 1 - 1) / 2; i >= 0; i--)
{
Heapdown(a, sz, i);
}
}
sz = sizeof(a) / sizeof(int);
for (i = 0; i < sz; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
注意点:
1.这里要注意sz和end大小的调整。
2.这里要注意这种思想的运用,在之后的学习中,这种思想很重要。
堆的实现
初始化堆
首先,必须创建一个堆类型,该类型中需包含堆的基本信息:存储数据的数组、堆中元素的个数以及当前堆的最大容量。
typedef int HpDataType;
typedef struct Heap
{
HpDataType* a;
int size;
int cap;
}Hp;
然后我们需要一个初始化函数,对刚创建的堆进行初始化,注意在初始化期间要将传入数据建堆。
void HpInit(Hp* php,int *b,int sz)
{
assert(php);
HpDataType* new = (HpDataType*)malloc(sizeof(HpDataType)*sz);
if (new == NULL)
{
printf("malloc failed\n");
exit(-1);
}
php->a = new;
int i = 0;
//for (i = 0; i < sz; i++)
//{
// php->a[i] = b[i];
//}
//另外一种快速的传递方法
//拷贝数据到堆中
memcpy(php->a, b, sizeof(HpDataType)*sz);
php->size = sz;
php->cap = sz;
int gen = (sz - 2) / 2;
for (i = gen; i >= 0; i--)
{
Adjustdown(php->a, sz,i);
}
}
销毁堆
为了避免内存泄漏,使用完动态开辟的内存空间后都要及时释放该空间,所以,一个用于释放内存空间的函数是必不可少的。
void HpDestory(Hp* php)
{
assert(php);
//因为是连续的空间,所以可以这样释放,跟顺序表一样
free(php->a);
php->a = NULL;
php->size = php->cap = 0;
}
打印堆
把堆里面的数据打印出来,可以方便我们观察。
void HpPrint(Hp php)
{
int i = 0;
for (i = 0; i <php.size; i++)
{
printf("%d ", php.a[i]);
}
printf("\n");
}
注意:这里我们这按堆在内存中的物理结构打印的,并没有按逻辑结构(也就是数的形状来打印)。
堆的插入
数据插入时是插入到数组的末尾,即树形结构的最后一层的最后一个结点,所以插入数据后我们需要运用堆的向上调整算法对堆进行调整,使其在插入数据后仍然保持堆的结构。
void HpPush(Hp* php, HpDataType x)
{
assert(php);
//扩容
if (php->size == php->cap)
{
HpDataType* new = (HpDataType*)realloc(php->a,sizeof(HpDataType)*(php->cap)*2);
if (new == NULL)
{
printf("realloc failed\n");
exit(-1);
}
php->a = new;
php->cap *= 2;
}
//注意这儿写的时候的顺序,要先调整一下,再让size++,如果反过来的话,你传参进去的size就会多加了一下
php->a[php->size] = x;
AdjustUp(php->a, php->size);
php->size++;
}
注意:写的时候要先写向上调整算法,再写size++,不然参进去的size就会多加了一下
堆的删除
堆的删除,删除的是堆顶的元素,但是这个删除过程可并不是直接删除堆顶的数据,而是先将堆顶的数据与最后一个结点的位置交换,然后再删除最后一个结点,再对堆进行一次向下调整。
原因:我们若是直接删除堆顶的数据,那么原堆后面数据的父子关系就全部打乱了,需要全体重新建堆,时间复杂度为O ( N ) O(N)O(N)。若是用上述方法,那么只需要对堆进行一次向下调整即可,因为此时根结点的左右子树都是小堆,我们只需要在根结点处进行一次向下调整即可,时间复杂度为O ( log ( N ) )
void HpPop(Hp* php)
{
assert(php);
assert(!HpEmpty(*php));
swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
Adjustdown(php->a, php->size, 0);
}
获取堆顶的元素
HpDataType HpTop(Hp php)
{
return php.a[0];
}
堆的判空
bool HpEmpty(Hp php)
{
return php.size == 0;
}
堆中元素的个数
int HpSize(Hp php)
{
return php.size;
}