堆
堆的概念及结构
用数组存储表示的完全二叉树
小根堆:树中所有父亲都小于等于孩子,根是最小值
大根堆:树中所有父亲都大于等于孩子,根是最大值
堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
堆的实现
堆向下调整算法
我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
int array[] = {27,15,19,18,28,34,65,49,25,37};
上图为向下调整算法调整为小堆的图解
1、从根开始,不断往下调
2、选出左右孩子中较小的,跟父亲比较,如果比父亲小,跟父亲交换,以小的孩子位置继续往下调。如果比父亲大,则停止
代码如下:
void swap(int *p1,int *p2)
{
int temp=*p1;
*p1=*p2;
*p2=temp;
}
void AdjustDown(int *a,int n,int parent)
{
int child=parent*2+1; //左孩子,左孩子+1即为右孩子
while(child<n)
{
//选择出左右孩子中较小/大的那个
//小堆
if(child+1<n && a[child+1]<a[child])//右孩子存在(防止越界)且如果右孩子比左孩子小
{
child++;//那就下标来到右孩子
}
if(a[child]<a[parent])//小的孩子小于父亲就交换,继续调整
{
swap(&a[parent],&a[child]);
parent=child;
child=parent*2+1;
}
else//小的孩子比父亲大或相等,则不处理,调整结束
{
break;
}
}
}
堆向上调整算法
void swap(int *p1,int *p2)
{
int temp=*p1;
*p1=*p2;
*p2=temp;
}
void AdjustUp(HPDataType *a,int child)
{
int parent = (child-1)/2;
//while(parent>=0) 不对的,parent不会小于0
while(child > 0)
{
if(a[parent]<a[child])
{
swap(&a[parent],&a[child]);
child=parent;
parent=(child-1)/2;
}
else
{
break;
}
}
}
向上调整算法我们找当前结点的父亲,假如我们调小堆时,如果父亲结点值大于它,则交换,然后进行迭代继续调整,注意我们进行迭代的条件是child>0,如果父亲结点值小于它,则调整结束,此时它已经是小堆
堆的创建
那么如果不满足左右子树是堆,不能直接用向下调整算法,怎么办呢?下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。
int array[] = {2,5,8,6,4,7};
建堆代码:
- 向下调整算法建堆:
void swap(int *p1,int *p2)
{
int temp=*p1;
*p1=*p2;
*p2=temp;
}
void AdjustDown(int *a,int n,int parent)
{
int child=parent*2+1; //左孩子,左孩子+1即为右孩子
while(child<n)
{
//选择出左右孩子中较小/大的那个
//小堆
if(child+1<n && a[child+1]<a[child])//右孩子存在(防止越界)且如果右孩子比左孩子小
{
child++;//那就下标来到右孩子
}
if(a[child]<a[parent])//小的孩子小于父亲就交换,继续调整
{
swap(&a[parent],&a[child]);
parent=child;
child=parent*2+1;
}
else//小的孩子比父亲大或相等,则不处理,调整结束
{
break;
}
}
}
int main()
{
int a[]={2,5,8,6,4,7};
int n=sizeof(a)/sizeof(a[0]);
//1、建堆
int i=0;
//为了满足向下调整条件,从最后一个非叶子结点开始调整,从下往上调整
for(i=(n-1-1)/2;i>=0;--i)//最后一个结点的父亲是最后一个非叶子结点
{
AdjustDown(a,n,i);//O(N)
}
}
我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆
- 向上调整算法建堆
void swap(int *p1,int *p2)
{
int temp=*p1;
*p1=*p2;
*p2=temp;
}
void AdjustUp(HPDataType *a,int child)
{
int parent = (child-1)/2;
//while(parent>=0) 不对的,parent不会小于0
while(child > 0)
{
if(a[parent]<a[child])
{
swap(&a[parent],&a[child]);
child=parent;
parent=(child-1)/2;
}
else
{
break;
}
}
}
int main()
{
int a[]={2,5,8,6,4,7};
int n=sizeof(a)/sizeof(a[0]);
for(int i=0;i<n;i++)
{
AdjustUp(a,i);
}
return 0;
}
向上调整算法我们从第一个结点开始调整,直到最后一个结点
建堆的时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):
每层结点个数*最坏情况向下的调整次数
因此,建堆的时间复杂度为O(N)。
堆的结构
typedef int HPDataType;
typedef struct Heap
{
HPDataType* _a;
int _size;
int _capacity;
}Heap;
我们给出一个_a指针指向动态开辟的内存,这块内存用来存储数据,_size用来保存当前数据的个数,_capacity用来保存当前的容量
堆的构建
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
assert(hp);
hp->_a = (HPDataType*)malloc(sizeof(HPDataType) * n);
if (hp->_a == NULL)
{
perror("malloc");
return;
}
memcpy(hp->_a, a, (sizeof(HPDataType) * n));
hp->_size = hp->_capacity = n;
//建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(hp->_a, n, i);
}
}
我们构建堆时,可以将初始化的数据以参数形式传进来,利用memcpy函数进行拷贝,将size和capacity进行更新,并进行建堆
堆的销毁
// 堆的销毁
void HeapDestory(Heap* hp)
{
assert(hp);
free(hp->_a);
hp->_a = NULL;
hp->_capacity = hp->_size = 0;
}
堆的插入
//插入x,保持它继续是堆
void HeapPush(HP* php,HPDataType x)
{
assert(php);
//首先看数组空间够不够,不够就要扩容
if(php->size==php->capacity)
{
HPDataType* temp = (HPDataType*)realloc(php->a,php->capacity*2*sizeof(HPDataType));
if(temp==NULL)
{
perror("malloc");
return;
}
php->capacity*=2;
php->a=temp;
}
php->a[size]=x;
php->size++;
AdjustUp(php->a,php->size-1);
}
在进行插入时,我们首先要看数组空间够不够,不够就要扩容,我们每次扩容两倍,扩容完成后,将数组最后元素后面的位置添加新元素,因为我们还要保持继续是堆,故我们将它向上调整一次
堆的删除
//删除堆顶的数据,删除后保持它继续是堆
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
swap(&php->a[0],&php->a[php->size-1]);
php->size--;
AdjustDown(php->a,php->size,0);
}
1.将堆顶元素与堆中最后一个元素进行交换
2.删除堆中最后一个元素
3.将堆顶元素向下调整到满足堆特性为止
取堆顶的数据
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
assert(hp);
return hp->_a[0];
}
堆的数据个数
// 堆的数据个数
int HeapSize(Heap* hp)
{
assert(hp);
return hp->_size;
}
堆的判空
// 堆的判空
int HeapEmpty(Heap* hp)
{
assert(hp);
return hp->_size == 0;
}
堆的打印
void HeapPrint(Heap* hp)
{
assert(hp);
for (int i = 0; i < hp->_size; i++)
{
printf("%d ", hp->_a[i]);
}
printf("\n");
}
堆的应用
堆排序
堆排序分两个步骤:
1、建堆
2、排序
那么我们排升序建大堆还是建小堆呢?答案是建大堆。
升序为什么不能建小堆呢?
建堆选出最小的数,花了O(N)的时间复杂度,紧接着如何选次小的数呢?剩下的数父子关系全乱了,向下调整需要满足左右子树都是堆,但是关系都乱了,左右子树可能都不满足向下调整的条件了,故剩下的N-1个数只能重新建堆,效率太低了
升序建大堆
1、选出了最大的数,把最大的数与最后的数交换
2、紧接着选出次大的数,与倒数第二个位置的数交换…
因为堆结构没有破坏,最后一个数不看作堆里面,左右子树依旧是大堆,向下调整即可,选出第二大
建大堆完成后,排序的步骤如图:
排序代码如下:
#include<stdio.h>
void swap(int *p1,int *p2)
{
int temp=*p1;
*p1=*p2;
*p2=temp;
}
//左右子树都是小堆或者大堆
void AdjustDown(int *a,int n,int parent)
{
int child=parent*2+1; //左孩子,左孩子+1即为右孩子
while(child<n)
{
//选择出左右孩子中较小/大的那个
//小堆
if(child+1<n && a[child+1]>a[child])//右孩子存在(防止越界)且如果右孩子比左孩子小
{
child++;//那就下标来到右孩子
}
if(a[child]>a[parent])//小的孩子小于父亲就交换,继续调整
{
swap(&a[parent],&a[child]);
parent=child;
child=parent*2+1;
}
else//小的孩子比父亲大或相等,则不处理,调整结束
{
break;
}
}
}
//排序(升序),排升序要建大堆,排降序要建小堆
void HeapSort(int *a,int n)
{
//1、建堆
int i=0;
//为了满足向下调整条件,从最后一个非叶子结点开始调整,从下往上调整
for(i=(n-1-1)/2;i>=0;--i)//最后一个结点的父亲是最后一个非叶子结点
{
AdjustDown(a,n,i);//O(N)
}
//2、排序
int end=n-1;
while(end>0)
{
swap(&a[0],&a[end]);
AdjustDown(a,end,0);//O(nlogn),一次向下调整为层数次 即logn次,while循环一共调整n次,故是nlogn
end--;
}
}
int main()
{
int a[]={2,5,8,6,4,7};
int n=sizeof(a)/sizeof(a[0]);
HeapSort(a,n);
int i =0;
for(i=0;i<n;i++)
{
printf("%d ",a[i]);
}
return 0;
}
堆排序时间复杂度O(N*logN)
欢迎大家学习讨论!