二叉堆小结

一、简介

堆是一棵具有特定性质的二叉树。

  • 如果任意节点的值总是 >= 子节点的值,称为:最大堆,大根堆,大顶堆
  • 如果任意节点的值总是 <= 子节点的值,称为:最小堆,小根堆,小顶堆

除此以外,所有叶子结点都是处于第 h 或 h - 1层(h为树的高度),堆是一个完全二叉树。基本接口定义如下:

public interface Heap<E> {
	int size();	// 元素的数量
	boolean isEmpty();	// 是否为空
	void clear();	// 清空
	void add(E element);	 // 添加元素
	E get();	// 获得堆顶元素
	E remove(); // 删除堆顶元素
	E replace(E element); // 删除堆顶元素的同时插入一个新元素
}

二叉堆的顶层一般用数组实现即可,索引 i 的规律(n 是元素数量)

  • 如果 i=0,它是根节点
  • 如果 i>0,它的父节点的索引为 floor((i-1)/  2)
  • 如果 2i + 1 <=  n-1,它的左子节点的索引为 2i + 1
  • 如果 2i + 2 <=  n-1,它的右子节点的索引为 2i + 2
  • 如果 2i + 1 > n-1,它无左子节点
  • 如果 2i + 2 > n-1,它无右子节点

 

可以看成以下数组: 

7268504338472114403

二、添加

如下图所示,添加节点 80(node 节点),添加步骤如下:

  1. 如果 node > 父节点(38),与父节点交换位置
  2. 如果node <= 父节点,或者 node 没有没有父节点,退出循环
  3. 循环执行步骤 1,2

最终 80 成为根节点,72 成为左子树节点,50成为右子树节点。这个过程叫做上滤,时间复杂度O(logn) 

 

实现代码如下:不断与父节点比较,如果小于父节点退出循环,否则将父节点的值替换当前节点

    private void siftUp(int index) {
		E element = elements[index];
		while (index > 0) {
			int parentIndex = (index - 1) >> 1;
			E parent = elements[parentIndex];
			if (compare(element, parent) <= 0) break;
			
			// 将父元素存储在index位置
			elements[index] = parent;
			
			// 重新赋值index
			index = parentIndex;
		}
		elements[index] = element;
	}

三、删除

如下图所示删除根节点72,步骤如下:

  1. 用最后一个节点(叶子节点3)覆盖根节点
  2. 删除最后一个节点(叶子节点3),
  3. 循环执行以下操作(node 节点:根节点3):如果 node < 最大的子节点,最大的子节点交换位置,如果 node >= 最大的子节点,或者没有子节点,退出循环

这个操作叫做下滤(Sift Down) 。根节点 3 不断下滤,最终下滤到索引为 7 的节点 14处,时间复杂度 O(logn)

 

实现代码如下:

    private void siftDown(int index) {
		E element = elements[index];
		int half = size >> 1;
		// 第一个叶子节点的索引 == 非叶子节点的数量
		// index < 第一个叶子节点的索引
		// 必须保证index位置是非叶子节点
		while (index < half) { 
			// index的节点有2种情况
			// 1.只有左子节点
			// 2.同时有左右子节点
			
			// 默认为左子节点跟它进行比较
			int childIndex = (index << 1) + 1;
			E child = elements[childIndex];
			
			// 右子节点
			int rightIndex = childIndex + 1;
			
			// 选出左右子节点最大的那个
			if (rightIndex < size && compare(elements[rightIndex], child) > 0) {
				child = elements[childIndex = rightIndex];
			}
			
			if (compare(element, child) >= 0) break;

			// 将子节点存放到index位置
			elements[index] = child;
			// 重新设置index
			index = childIndex;
		}
		elements[index] = element;
	}

四、替换

替换是将传入的元素替换堆顶元素,实现步骤如下:

  1. 将堆顶元素替换成传入的元素
  2. 不断下滤

实现代码如下:

    public E replace(E element) {
		E root = null;
		if (size == 0) {
			elements[0] = element;
			size++;
		} else {
			root = elements[0];
			elements[0] = element;
			siftDown(0);
		}
		return root;
	}

五,批量建堆

批量建堆,有 2 种做法:

自上而下的上滤,从除根节点外其他节点开始做上滤操作。效率为所有节点的深度之和

仅仅是叶子节点,就有近 n/2 个,而且每一个叶子节点的深度都是O(logn)级别,所以时间复杂度达到了O(nlogn)

实现原理:上滤后所在索引之前的元素可以满足堆的性质(相当于添加节点后,新增节点上滤,满足堆的性质)

自下而上的下滤,从最后一个非叶子节点开始做下滤操作。效率为所有节点的高度之和

从最后一个非叶子节点做下滤,只有根节点操作的深度可能达到树高(O(logn))次,时间复杂度为O(n)效率比自上而下的上滤效率高,一般都是采用这种方式批量建堆。

实现原理:每一小部分下滤后满足堆的性质,然后多个小部分再合并,合并的那个节点下滤(相当于删除节点后的操作),合并的两个部分又满足了堆的性质,不断循环,最终建堆完成。

实现代码如下:

private void heapify() {
		// 自上而下的上滤
//		for (int i = 1; i < size; i++) {
//			siftUp(i);
//		}
		
		// 自下而上的下滤
		for (int i = (size >> 1) - 1; i >= 0; i--) {
			siftDown(i);
		}
	}

六、Top K 问题

描述:从 n 个整数中,找出最大的前 k 个数(k 远远小于 n)

  • 如果使用排序算法进行全排序,需要 O(nlogn)的时间复杂度
  • 如果使用二叉堆来解决,可以使用 O(nlogk)的时间复杂度来解决

解决步骤:

  1. 新建一个小顶堆
  2. 先将遍历的前 k 个数放入堆中
  3. 从 k + 1 个数开始,如果大于堆顶元素,就使用 replace 操作(删除堆顶元素,将 k + 1 个数添加到堆中)
  4. 扫描完毕后,堆中的元素就是最大的前 k 个数

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值