8. 最大堆
一、最大堆的定义
最大堆是一棵完全二叉树,而且每个结点的值都不小于其孩子结点的值。
二、选择数组作为最大堆的内存表现形式
由于最大堆是一棵完全二叉树,所以我们使用数组的形式来存储最大堆的值,并从1号单元开始存储。
假设树的节点个数为n,以1为下标开始编号,直到n结束。对于下标为 i 的结点,其父结点的坐标为i / 2,左孩子结点的坐标为i * 2,右孩子结点的坐标为i*2 + 1。
例如,有数组heap[10] = {0, 100, 19, 36, 17, 3, 25, 1, 2, 7},其对应的完全二叉树如下图所示。
- heap[leftChild] = heap[father * 2]
- heap[rightChild] = heap[father * 2 + 1]
- heap[fathrt] = heap[leftChild / 2] = heap[rightChild / 2]
三、构造最大堆
构造堆的基本思想就是:首先将每个叶子结点视为一个堆,再将每个叶子结点与其父结点一起构造成一个包含更多节点的堆。
构造最大堆:首先需要找到最后一个结点的父结点,从这个结点开始构造最大堆,直到该结点前面的所有分支节点都处理完毕,这样最大堆就构造完毕了。
如下图所示,我们要将一个heap[11] = {0, 4, 1, 3, 2, 16, 9, 10, 14, 8, 7}的二叉完全树构造成最大堆。因为最后一个结点为7,其父结点为16,所以应从16这个结点开始构造最大堆;构造完毕之后,转移到下一个父结点2,直到所有父结点都构造完毕。
注:如上图(d)所示,当我们将结点1和结点16互换后,发现结点1又要和结点7互换。即结点1和结点16对调后,需要递归进行调整,请一定注意!
代码:
void create(MaxHeap &H) { // 从最后一个结点的父结点开始构造最大堆 for(int i = H.heapSize / 2; i > 0; --i) { int temp = H.heap[i]; // 将父结点存放到temp中 // 本次for循环的以下过程皆是为了寻找父结点最终应存放的位置 // 因为假设当前父结点与其子结点对调了,但它可能还要再与其下的新子结点对调 // son一直记录当前父结点的孩子结点的坐标,所以它的值才一直在变 int son = i * 2; // 当前父结点递归下调过程 while(son <= H.heapSize) { // 右孩子结点的值更大 if(son < H.heapSize && H.heap[son] < H.heap[son+1]) son++; // 当前父结点已是以其为根结点的最大堆的最大值 if(temp >= H.heap[son]) break; // 需要与孩子结点对调,但此时尚不能退出循环,因为是递归下调 else { H.heap[son / 2] = H.heap[son]; son = son * 2; } } H.heap[son/2] = temp; // 找到父结点最终的位置,此时的son为父结点的最新孩子 } }
四、插入
上浮:先在堆的最后添加一个结点,然后沿着堆上升,直到找到正确的插入位置。
过程:
-
在下一个空闲位置创建一个空穴。
-
如果元素X可以放在该空穴而并不破坏堆的序,那么插入完成;
- 否则,我们把空穴的父结点上的元素移入该空穴中,继续该过程直到X能被放入空穴。
代码:
/* 插入值为x的结点 */ /* * 在堆的最后产生一个空穴,然后不断上移,直到找到能插入结点x的位置 * 空穴的最终位置就是结点x应插入的位置 * 补:在空穴不断上移的过程中,要更换对应结点的值 */ bool insert(MaxHeap &H, int x) { if(H.maxSize == H.heapSize) return false; H.heapSize++; int xIndex = H.heapSize; // 空穴的下标 while(xIndex != 1 && x > H.heap[xIndex / 2]) { H.heap[xIndex] = H.heap[xIndex / 2]; // 将那个比x小的根结点下沉到空穴 xIndex = xIndex / 2; // 空穴上浮到新的位置(上浮一层) } H.heap[xIndex] = x; return true; }
五、删除最大元
下沉:将堆的最后的结点提到根结点,然后删除最大值,然后再把新的根节点放到合适的位置。
过程:
-
删除最大元后,会在根结点处产生一个空穴。
-
由于堆中少了一个元素,故堆中最后一个元素X必须移动到该堆的某个地方。
- 将空穴的两个儿子中较小者移入空穴,重复该过程直到X可以被放入空穴中。
代码:
/* 删除根结点 */ bool deleteMax(MaxHeap &H) { if(H.heapSize == 0) return false; // 将最后一个结点存到temp中 ,因为temp是要填入空穴的数,也即空穴的值就是temp int temp = H.heap[H.heapSize]; H.heapSize--; // 空穴出现在堆的根结点处,即下标为1处 int newIndex = 1, son = newIndex * 2; // son一直为空穴的孩子结点,随着空穴的下沉而改变 // 当空穴没有到最后一个位置时 while(newIndex < H.heapSize) { // 右孩子结点的值更大 if(newIndex < H.heapSize && H.heap[son] <= H.heap[son+1]) son++; // 孩子结点的值比空穴的值大,空穴下沉 if(H.heap[son] > temp) { H.heap[newIndex] = H.heap[son]; newIndex = son; son = son * 2; } // 得到空穴的最终位置 if(H.heap[son] <= temp) break; } H.heap[newIndex] = temp; return true; }