当我们要在一组数据中找到最小/大值或者前K大/小值的时候,我们可以使用传统的遍历方法。那么这个时候时间复杂度就是$O(N^2)$,但我们可以使用"堆"来进行优化,我们可以把找到最小/大值的复杂度降低到$O(logN)$。插入一个新值的复杂度也是$O(logN)$。
维护一个堆关键的就是向下维护和向上维护,基于这两种方法我们就可以实现插入,删除
向下调整,时间复杂度:$O(logn)$
建堆,时间复杂度:$O(n)$
//建立大根堆 const int maxn = 100; //heap为堆,n为元素个数 int heap[maxn],n=10; void downAdjust(int low,int high) { int i = low, j = i*2; while(j <= high) { if(j+1<=high && heap[j+1]>heap[j]) ++j; if(heap[j] > heap[i]) { swap(heap[j],heap[i]); i = j; j = i*2; } else break; } } void createHeap() { for(int i=n/2;i>=1;--i) { downAdjust(i,n); } }
完全二叉树的叶子结点个数为$iggllceilfrac{n}{2}iggr ceilqquad$,因此数组下标在$[1,iggllfloorfrac{n}{2}iggr floor]$范围内的结点都是非叶子结点。于是可以从$iggllfloorfrac{n}{2}iggr floor]$号位开始倒着枚举结点。倒着枚举保证了每个结点都是以其为根节点的子树中权值最大的结点。
删除堆顶元素 时间复杂度:$O(logn)$
void deleteTop() // 删除堆顶元素 时间复杂度:O(logn) { heap[1] = heap[n--]; downAdjust(1,n); } 向上调整,时间复杂度:O(logn) void upAdjust(int low,int high) { int i = high,j = i/2; while(j>=low) { if(heap[j] < heap[i]) { swap(heap[j],heap[i]); i = j; j = i/2; } else break; } } void insert(int x) // 添加元素 { heap[++n] = x; upAdjust(1,n); }
堆排序
具体实现时,为了节省空间,可以倒着遍历数组,假设当前访问到i号位,那么将堆顶元素与i号位的元素交换,接着在[1,i-1]范围内对堆顶元素进行一次向下调整
void heapSort() { createHeap(); // 建堆 for(int i=n;i>1;--i) { swap(heap[i],heap[1]); downAdjust(1,i-1); } }
以上的就是最大堆的实现,最小堆只要把判断大小换过来便是了
Dijkstra算法也可以使用堆来优化找离源点最近的顶点的过程,使时间复杂度下降到$O(M+N)logN$,堆还可以用来求一个数列中第K大的数:首先建立一个大小为K的最小堆,从第K+1个数开始,与堆顶进行比较,如果比堆顶大则代替堆顶并维护,比堆顶小则直接舍弃。这样最后堆顶便是第K大数。时间复杂度为$O(NlogK)$。