堆的性质:
- 堆一定是一棵完全二叉树
- 堆序性:分为 大根堆——每个父节点元素都大于它的子节点元素,小根堆——每个父节点元素都小于它的子节点元素
完全二叉树的性质:
- 只允许最后一行不为满
- 最后一行的元素必须从左往右排序
- 最后一行元素之间不能有间隔
<-- 一棵完全二叉树
堆如何存储:
从上到下、从左到右给堆的每个元素编号,这些编号对应数组的下标,把所有元素存储在一个数组中(通常从索引1处开始存储数据)
节点与数组下标之间的对应关系:节点下标为i,则左子节点下标为2i+1,右子节点下标为2i+2
堆的常用操作:
- 将无序数组转换为大根堆(大元素“下坠”)
在顺序存储的完全二叉树中,非终端节点的编号i≤2n
步骤:从[n/2]元素开始向前遍历数组中元素,将它们分别与自己的左右子节点比较,把更大的项移到根部
代码实现:
void BuildMaxHeap(int *arr,int size) //size是堆中的元素数目,数组的实际长度是(size+1)
{
for(int i=size/2;i>=1;i--) //从后往前对每个根节点处理
{
HeadAdjust(arr,i,size);
}
}
void HeadAdjust(int *arr,int i,int size) //将以索引k元素作为根节点的堆转换成大根堆
{
arr[0]=arr[i]; //arr[i]是当前待排序的根元素,在arr[0]暂存
//索引为i的位置看做空位
for(int j=i*2;j<=size;j*=2) //从i的左子节点开始处理
{
if(j<size && arr[j]<arr[j+1]) //左子节点与右子节点比较找到最大的子节点元素,这里要判断是否存在右子节点
j++;
if(arr[0]<arr[j])
{
arr[i]=arr[j]; //将最大的子节点移到根处
i=j; //继续处理该子节点的子节点
}
else //后面已经是大根堆了,跳出
break;
}
arr[i]=arr[0];
}
时间复杂度分析:
BuildMaxHeap(建堆函数)中:
(1) 一个节点每下坠一层,最多进行两次关键字的对比(先对比左右子节点(如果有右子节点),再对比自己和最大的子节点)
因此若树高为h,某节点在第 i 层,则调整该节点最多需要“下坠” (h−i) 层,关键字对比不超过 2(h−i) 次
(2) n个节点的完全二叉树树高h=⌊log2n⌋+1
(3) 第i层最多有2i−1个节点,而只有1∼(h−1) 层的节点才有可能需要下坠调整(最后一层不需要下坠)
综上得出下坠操作的最大总次数:
(其中用到了换元,令j=h-i,将i=h-1与i=1代入得到新的上下界;2^⌊log n⌋≤n;最后一步是对差比数列进行错位相减,得出≤4n)
得出结论:建堆的过程,关键字对比次数不超过4n,建堆时间复杂度=O(n)
- 向堆中插入新元素
步骤:
对于小根堆,将新元素放到堆尾,将它与父节点对比,若新元素<父节点,则将二者互换,重复该过程直至无法继续上升;
对于大根堆,将新元素放到堆尾,将它与父节点对比,若新元素>父节点,则将二者互换,重复该过程直至无法继续上升
代码实现:
int* MaxHeapInsert(int *arr,int size,int element) //大根堆插入,前提是arr已经是一个大根堆
{
int newSize=size+1,index=newSize; //index始终指向新元素的位置
int *newArr=malloc(sizeof(int)*(newSize+1)); //申请更大的堆
if(newArr==NULL)
{
printf("内存申请失败\n");
return NULL;
}
for(int i=1;i<=size;i++) //将原堆数据复制过来
newArr[i]=arr[i];
newArr[index]=element; //将新元素插入到堆尾
for(int i=index/2;i>0;i/=2)
{
if(newArr[index]>newArr[i])
{
int tmp=newArr[index];
newArr[index]=newArr[i];
newArr[i]=tmp;
index=i;
}
else
return newArr;
}
return newArr;
}
- 从堆中删除元素
步骤:
对于大/小根堆,用堆尾元素覆盖要删除的元素,再将该元素不断下坠,直至无法下坠为止
代码实现:
int* MaxHeapDelete(int *arr,int size,int pos) //大根堆删除,删除第pos个元素,前提是arr已经是一个大根堆
{
int newSize=size-1;
int *newArr=malloc(sizeof(int)*(newSize+1));
if(newArr==NULL)
{
printf("内存申请失败\n");
return NULL;
}
for(int i=1;i<=newSize;i++)
newArr[i]=arr[i];
newArr[pos]=arr[size];
HeadAdjust(newArr,pos,newSize);
return newArr;
}
注意:
某根节点有两个子节点时,每下坠一层需要对比两次根节点;某根节点只有一个子节点时,每下坠一层需要对比一次关键字
“下坠”和“上升”是两种逻辑顺序,他们都可以分别用来构建大/小根堆