对于堆大家都不陌生,无非就是最大堆和最小堆之分,堆的使用很广泛,优先队列、求大叔组的前k个数都可以用堆实现,且时间复杂度低。但是对于堆的具体实现存在几种不同的方式,它们各有优势。
根据堆底层的实现可分为顺序存储堆和链式存储堆,链式存储又分为左式堆、斜堆以及二项堆。
1、顺序存储堆就是我们经常接触的堆,即使用数组实现,逻辑上相当于完全二叉树。它的实现以及堆排序大家已经很熟悉了,这里不再赘述,顺序堆的缺点是合并两个堆的时间复杂度比较高,通常从一个堆中逐次取出元素插入到另一个堆中,时间复杂度为O(NlogM)注:假设两个堆元素分别为N,M。或者使用一个M+N的数组,将所有元素存储在数组中然后进行一次初始堆操作,这样虽然降低了时间复杂度,但是需要额外内存开销。
2、链式堆很好的解决了上面的缺陷,同时堆的所有操作可以转化为合并操作。
2.1、左式堆:在介绍左式堆之前先介绍一个概念——零路径长:节点到一个不具有两个儿子的节点 的最短路径长,NULL节点零路径为-1。左式堆要求左子树零路径长不小于右子树,合并两个左式堆时先比较堆顶元素,如果是最小堆(最大堆相反),则将堆顶值小的堆的右子树与另一个堆进行合并,递归进行,直到其中一个堆为NULL,返回,合并后判断左右子树的零路径长,若右子树比左子树长则交换左右子树,同时修改节点零路径值为右子树+1。核心代码下:
struct heapNode{ int data; int zeroPath; heapNode* left; heapNode* right; heapNode(){ } heapNode(int key,int zero = 0,heapNode* l = NULL,heapNode* r = NULL):data(key),zeroPath(zero),left(l),right(r){ } }; int getzeroPath(heapNode* root){//获取零路径长 return root == NULL ? -1 : root->zeroPath; } void setzeroPath(heapNode* root){//设置零路径长 //对于左式堆可以直接root->zeroPath = getzeroPath(root->right)+1,但斜堆求//零路径长时必须比较左右子树,这里为了代码复用,故不做区分 root->zeroPath = min(getzeroPath(root->left),getzeroPath(root->right)) + 1; } void swap(heapNode* &t1,heapNode* &t2){//交换左右子树 heapNode* t = t1;t1 = t2;t2 = t; } heapNode* mergeHeap(heapNode* r1,heapNode* r2){//堆合并算法 if(r1 == NULL) return r2; if(r2 == NULL) return r1; if(r1-> data > r2->data){ r2->right = mergeHeap(r1,r2->right); if(getzeroPath(r2->left) < getzeroPath(r2->right)){ swap(r2->left,r2->right); } setzeroPath(r2); return r2; }else{ r1->right = mergeHeap(r1->right,r2); if(getzeroPath(r1->left) < getzeroPath(r1->right)) swap(r1->left,r1->right); setzeroPath(r1); return r1; } }
2.2、斜堆:斜堆与左式堆类似,唯一不同在于合并后不管左右子树零路径大小都必须交换左右子树,不同代码如下:
heapNode* mergeHeap(heapNode* r1,heapNode* r2){ if(r1 == NULL) return r2; if(r2 == NULL) return r1; if(r1-> data > r2->data){ r2->right = mergeHeap(r1,r2->right); swap(r2->left,r2->right);//与左式堆不同之处 setzeroPath(r2); return r2; }else{ r1->right = mergeHeap(r1->right,r2); swap(r1->left,r1->right);//与左式堆不同之处 setzeroPath(r1); return r1; } }
2.3、二项堆:二项堆实际是一个森林,用一个数组或者链表保存所有二叉树的根节点,可以有序, 合并方式与左式堆有点不同,如果是最小堆(最大堆相反),合并时将相同度的二叉树合并,且堆顶值大的堆作为堆顶值小的堆的子树,二项堆不允许有相同度的树存在。
- 度数为0的二项树只包含一个结点
- 度数为k的二项树有一个根结点,根结点下有个子女,每个子女分别是度数分别为的二项树的根
- 度数为k的二项树共有个结点,高度为。在深度d处有(二项式系数)个结点。
- 度数为k的二项树可以很容易从两颗度数为k-1的二项树合并得到:把一颗度数为k-1的二项树作为另一颗原度数为k-1的二项树的最左子树。这一性质是二项堆用于堆合并的基础。