总结堆的几种操作:删除操作(调整堆),添加堆
调整堆:例如在进行删除操作的时候,我们将根结点与最后一个节点进行替换,替换以后,删除掉最后一个节点或在将lastSize(记录数组大小的指针)前移。
int lastSize=0;
void reshape(int[] heap,int root){ boolean done=false;//判断orphan是否大于largeChild,用来跳出循环 int orphan=heap[root];//删除操作均是对root的调整 int leftIndex=root*2;//左孩子的下标 while (!done&&leftIndex<=lastSize){ int largeChild = leftIndex; int rightIndex = leftIndex+1; if (rightIndex<=lastSize&&heap[leftIndex]<heap[rightIndex])largeChild=rightIndex;//判断一下左右孩子中最大的 if (orphan<heap[largeChild]){ heap[root]=heap[largeChild];//交换,并依次调整孩子 root=largeChild; leftIndex=root*2; }else { done=true; } heap[root]=orphan; } }
void del(int[] heap){ if(lastSize<1)return;//这里的0下标位置空出来了,最小下标为1 heap[1]=heap[lastSize];//根节点进行替换 lastSize--;//删除最后一个元素 reshape(heap,1);//调整根节点 }
添加元素也是包括两种操作,1是将最新的下标加入到末尾位置2调整,这里的调整是从下向上进行调整。
void add(int[] heap,int val){ int newIndex=lastSize+1;//插入的位置 int parentIndex=newIndex/2;//插入节点的父亲下标 while (parentIndex>0&&val>heap[parentIndex]){//父节点下标大于1,插入节点的值大于父亲节点 heap[newIndex]=heap[parentIndex];//插入到子节点中 newIndex=parentIndex;//依次进行相同的操作 parentIndex=newIndex/2; } heap[parentIndex]=val; lastSize++; }
时间复杂度分析
首先考虑建堆的过程,有两种方式建堆,1每个元素添加到数组中,添加一次,并进行依次的调整。添加一个元素的平均时间复杂度是O(logn),添加n个元素的平均时间复杂度是O(nlogn)
2将整个数组直接进行建堆的过程,时间复杂度是O(n),下面就分析一下为什么时间复杂度是这样的。
首先针对第一种情况,调整堆的时间复杂度是树的高度h=logn,第一次只有一个元素,log1,第二次有两个元素,log2.依次递推,时间复杂度为log1+log2+log3+...+logn< nlogn。这里考虑时间复杂度的上界。
还有一种比较严格的计算方法,使用斯特林不等式。
图1
针对第二种情况,对整个数组进行建堆。时间复杂度O(n)
图2
这里出现的几个字母,第一个是h,h是树的深度,为logN,l为某一层,树的深度是从1开始到h-1,因为最后一层是不参与调整的。每次调整的时间复杂度就是树的深度,每次调整需要调整的元素就是某一层的节点的个树,这个值为2^(l-1)
因此可以写出如上所示的公式。
另外考虑堆排序的时间复杂度,首先刚才已经确定数组建堆的时间复杂度最好为O(n),堆调整的平均复杂度为O(logn),堆排序需要每次将堆首元素删除,一共要出n次,时间复杂度是O(nlogn)。
所以堆排序的时间复杂度为O(nlogn)