对于堆大家都不陌生,无非就是最大堆和最小堆之分,堆的使用很广泛,优先队列、求大叔组的前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个子女,每个子女分别是度数分别为k-1, k-2, ..., 2, 1, 0的二项树的根

  • 度数为k的二项树共有2^k个结点,高度为k。在深度d处有\tbinom n d二项式系数)个结点。

  • 度数为k的二项树可以很容易从两颗度数为k-1的二项树合并得到:把一颗度数为k-1的二项树作为另一颗原度数为k-1的二项树的最左子树。这一性质是二项堆用于堆合并的基础。