(恋上数据结构笔记):二叉堆(Binary Heap)

目录

堆(Heap)

堆的基本接口设计

二叉堆(Binary Heap)

获取最大值

最大堆 - 添加

添加过程总结

最大堆 — 添加优化

最大堆 - 删除

replace(替换堆顶的元素)

最大堆 — 批量建堆(Heapify)

自上而下的上滤

自下而上的下滤

效率对比

二叉堆源码

堆的基本接口 Heap.java

抽象类 AbstractHeap.java

二叉堆 BinaryHeap.java

构建一个最小堆

TOP K 问题

【引出堆】

  • 堆   获取最大值:O(1)、删除最大值:O(logn)、添加元素:O(logn)
     

【Top K问题】

  • 什么是TopK问题
    • 从海量数据中找出前K个数据
  • 比如
    • 从100万个整数中找出最大的100个整数
  • TopK问题的解法之一:可以用数据结构“堆”来解决

堆(Heap)

(Heap)是一种 树状 的数据结构,常见的堆实现有

  • 二叉堆(Binary Heap,完全二叉堆
  • 多叉堆(D-heap、D-ary Heap)
  • 索引堆(Index Heap)
  • 二项堆(Binomial Heap)
  • 斐波那契堆(Fibonacci Heap)
  • 左倾堆(Leftist Heap,左式堆
  • 斜堆(Skew Heap)
  • 堆的重要性质:任意节点的值总是 ≥( ⩽) 子节点的值
    • 如果任意节点的值总是 ≥ 子节点的值,称为:最大堆、大根堆、大顶堆
    • 如果任意节点的值总是 ⩽ 子节点的值,称为:最小堆、小根堆、小顶堆
  • 堆中的元素必须具备可比较性(跟二叉搜索树一样)

堆的基本接口设计

二叉堆(Binary Heap)

  • 二叉堆的逻辑结构就是一棵完全二叉树,所以也叫完全二叉堆
  • 鉴于完全二叉树的一些特性,二叉堆的底层(物理结构)一般用数组实现即可

  • 使用数组存放

【索引 i 的规律(n 是元素数量)】

获取最大值

  • 注意检测数组不能为空
public E get() {
	emptyCheck();
	return elements[0];
}
private void emptyCheck() {
	if (size == 0) {
		throw new IndexOutOfBoundsException("Heap is empty");
	}
}

最大堆 - 添加

  • 添加一个新元素80

  • 和父节点进行交换

  • 进行循环比较,交换

添加过程总结

  • 代码实现

/**
 * 最大堆 — 添加
 */
public void add(E element) {
	// 检测传入的元素非空
	elementNotNullCheck(element);
	// 扩容操作,确保容量大于当前元素个数大小
	ensureCapacity(size + 1);
	// 将要添加的元素放到数组最后
	elements[size++] = element;
	// 上滤操作
	siftUp(size - 1);
}
/**
 * 让index位置的元素上滤
 */
private void siftUp(int index) {
	E e = elements[index]; // 要添加的节点
	while (index > 0) { // 直到比较到根结点
		int pindex = (index - 1) >> 1; // 父节点索引
		E p = elements[pindex]; // 添加节点的父节点
		if (compare(e, p) <= 0) return;
		
		// 交换index、pindex位置的内容
		E tmp = elements[index];
		elements[index] = elements[pindex];
		elements[pindex] = tmp;
		
		// 重新赋值index
		index = pindex;
	}
}

最大堆 — 添加优化

/**
 * 让index位置的元素上滤
 */
private void siftUp(int index) {
	E element = elements[index];
	while (index > 0) {
		// 父节点索引 = (子节点索引-1) / 2
		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;
}

最大堆 - 删除

/**
 * 最大堆—删除堆顶元素
 */
public E remove() {
	// 检测数组不能为空
	emptyCheck(); 
	// 获取数组最后元素的索引
	int lastIndex = --size;
	// 获取要删除元素的节点
	E root = elements[0];
	elements[0] = elements[lastIndex];
	elements[lastIndex] = null;
	
	siftDown(0); // 下滤操作
	return root;
}
/**
 * 让index位置的元素下滤
 */
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;
}

replace(替换堆顶的元素)

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

最大堆 — 批量建堆(Heapify)

  • 自上而下的上滤
  • 自下而上的下滤

自上而下的上滤

自下而上的下滤

效率对比

  • 自上而下的上滤时间复杂度:O ( n l o g n ) O(nlogn)O(nlogn)
  • 自下而上的下滤时间复杂度:O ( n l o g k ) O(nlogk)O(nlogk)

二叉堆源码

堆的基本接口 Heap.java

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

抽象类 AbstractHeap.java

@SuppressWarnings("unchecked")
public abstract class AbstractHeap<E> implements Heap<E> {
	protected int size;
	protected Comparator<E> comparator;
	
	public AbstractHeap(Comparator<E> comparator) {
		this.comparator = comparator;
	}
	
	public AbstractHeap() {
		this(null);
	}
	
	@Override
	public int size() {
		return size;
	}

	@Override
	public boolean isEmpty() {
		return size == 0;
	}
	
	protected int compare(E e1, E e2) {
		return comparator != null ? comparator.compare(e1, e2) 
				: ((Comparable<E>)e1).compareTo(e2);
	}
}

二叉堆 BinaryHeap.java

/**
 * 二叉堆(最大堆)
 */
@SuppressWarnings("unchecked")
public class BinaryHeap<E> extends AbstractHeap<E> {
	private E[] elements;
	private static final int DEFAULT_CAPACITY = 10;
	
	public BinaryHeap(E[] elements, Comparator<E> comparator)  {
		super(comparator);
		
		if (elements == null || elements.length == 0) {
			this.elements = (E[]) new Object[DEFAULT_CAPACITY];
		} else {
			size = elements.length;
			int capacity = Math.max(elements.length, DEFAULT_CAPACITY);
			this.elements = (E[]) new Object[capacity];
			for (int i = 0; i < elements.length; i++) {
				this.elements[i] = elements[i];
			}
			heapify();
		}
	}
	
	public BinaryHeap(E[] elements)  {
		this(elements, null);
	}
	
	public BinaryHeap(Comparator<E> comparator) {
		this(null, comparator);
	}
	
	public BinaryHeap() {
		this(null, null);
	}

	@Override
	public void clear() {
		for (int i = 0; i < size; i++) {
			elements[i] = null;
		}
		size = 0;
	}

	@Override
	public void add(E element) {
		elementNotNullCheck(element);
		ensureCapacity(size + 1);
		elements[size++] = element;
		siftUp(size - 1);
	}

	@Override
	public E get() {
		emptyCheck();
		return elements[0];
	}

	/**
	 * 删除堆顶元素
	 */
	public E remove() {
		emptyCheck();
		
		int lastIndex = --size;
		E root = elements[0];
		elements[0] = elements[lastIndex];
		elements[lastIndex] = null;
		
		siftDown(0);
		return root;
	}

	@Override
	public E replace(E element) {
		elementNotNullCheck(element);
		
		E root = null;
		if (size == 0) {
			elements[0] = element;
			size++;
		} else {
			root = elements[0];
			elements[0] = element;
			siftDown(0);
		}
		return root;
	}
	
	/**
	 * 批量建堆
	 */
	private void heapify() {
		// 自上而下的上滤
//		for (int i = 1; i < size; i++) {
//			siftUp(i);
//		}
		
		// 自下而上的下滤
		for (int i = (size >> 1) - 1; i >= 0; i--) {
			siftDown(i);
		}
	}
	
	/**
	 * 让index位置的元素下滤
	 * @param index
	 */
	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;
	}
	
	/**
	 * 让index位置的元素上滤
	 */
	private void siftUp(int index) {
//		E e = elements[index];
//		while (index > 0) {
//			int pindex = (index - 1) >> 1;
//			E p = elements[pindex];
//			if (compare(e, p) <= 0) return;
//			
//			// 交换index、pindex位置的内容
//			E tmp = elements[index];
//			elements[index] = elements[pindex];
//			elements[pindex] = tmp;
//			
//			// 重新赋值index
//			index = pindex;
//		}
		E element = elements[index];
		while (index > 0) {
			// 父节点索引 = (子节点索引-1) / 2
			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;
	}
	
	private void ensureCapacity(int capacity) {
		int oldCapacity = elements.length;
		if (oldCapacity >= capacity) return;
		
		// 新容量为旧容量的1.5倍
		int newCapacity = oldCapacity + (oldCapacity >> 1);
		E[] newElements = (E[]) new Object[newCapacity];
		for (int i = 0; i < size; i++) {
			newElements[i] = elements[i];
		}
		elements = newElements;
	}
	
	private void emptyCheck() {
		if (size == 0) {
			throw new IndexOutOfBoundsException("Heap is empty");
		}
	}
	
	private void elementNotNullCheck(E element) {
		if (element == null) {
			throw new IllegalArgumentException("element must not be null");
		}
	}
}

构建一个最小堆

  • 在写完最大堆以后,实现最小堆不需要修改源代码,只需要在创建堆时,传入与最大堆比较方式相反的比较器即可。
public static void main(String[] args) {
	Integer[] data = {88, 44, 53, 41, 16, 6, 70, 18, 85, 98, 81, 23, 36, 43, 37};
	BinaryHeap<Integer> heap = new BinaryHeap<>(data, new Comparator<Integer>() {
		public int compare(Integer o1, Integer o2) {
			return o2 - o1; // 与最大堆比较方式相反
		}
	});
}

TOP K 问题

public static void main(String[] args) {
	// 新建一个小顶堆
	BinaryHeap<Integer> heap = new BinaryHeap<>(new Comparator<Integer>() {
		public int compare(Integer o1, Integer o2) {
			return o2 - o1;
		}
	});
	
	// 找出最大的前k个数
	int k = 3;
	Integer[] data = {51, 30, 39, 92, 74, 25, 16, 93, 
			91, 19, 54, 47, 73, 62, 76, 63, 35, 18, 
			90, 6, 65, 49, 3, 26, 61, 21, 48};
	for (int i = 0; i < data.length; i++) {
		if (heap.size() < k) { // 前k个数添加到小顶堆
			heap.add(data[i]); // logk
		} else if (data[i] > heap.get()) { // 如果是第 k + 1 个数,并且大于堆顶元素
			heap.replace(data[i]); // logk
		}
	}
	// O(nlogk)
}
  • 如果是找出最小的前 k 个数呢?
    • 用大顶堆
    • 如果小于堆顶元素,就使用 replace 操作。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值