数据结构与算法之二叉堆

(一) 定义

堆(Heap): 是计算机科学中一类特殊的非线性数据结构的统称。堆通常是一个可以被看做一棵完全二叉树的数组对象。堆总是满足下列性质:

  • 堆一定是一棵完全树(按照元素循序排列成树的形状)
  • 堆中任意一个结点的值总是不大于或不小于其父节点的值

最大堆: 堆中任意一个结点的值总是不大于其父节点的值; 最小堆: 堆中任意一个结点的值总是不小于其父节点的值
二叉堆: 是一棵完全二叉树
在这里插入图片描述
基于完全二叉树的特性: 将节点按顺序一层一层的码放出来(层序输出), 因此我们可以使用使用数组的方式表示一颗完全二叉树(两种方式).
在这里插入图片描述

(二) 自定义基于动态数组的最大堆

1.自定义最大堆的基础结构
public class MaxHeap<E extends Comparable<E>> {

	/**
	 * 存储数据的动态数组
	 */
	private Array<E> data;

	public MaxHeap(int capacity) {
		data = new Array<>(capacity);
	}

	public MaxHeap() {
		data = new Array<>();
	}

	/**
	 * 获取堆中的元素个数
	 * 
	 * @return
	 */
	public int getSize() {
		return data.getSize();
	}

	/**
	 * 返回堆中的元素是否为空
	 * 
	 * @return
	 */
	public boolean isEmpty() {
		return data.isEmpty();
	}

	/**
	 * 返回完全二叉树的数组表示中, 一个索引所表示的元素的父亲节点的索引
	 * 
	 * @param index
	 * @return
	 */
	private int parent(int index) {
		return (index - 1) / 2;
	}

	/**
	 * 返回完全二叉树的数组表示中, 一个索引所表示的元素的左孩子节点的索引
	 * 
	 * @param index
	 * @return
	 */
	private int leftChild(int index) {
		return index * 2 + 1;
	}

	/**
	 * 返回完全二叉树的数组表示中, 一个索引所表示的元素的右孩子节点的索引
	 * 
	 * @param index
	 * @return
	 */
	private int rightChild(int index) {
		return index * 2 + 2;
	}

}
2.查看堆中最大的元素
public E findMax() {
	if (data.getSize() == 0) {
		throw new IllegalArgumentException("MaxHeap is empty.");
	}
	return data.get(0);
}
3.堆的添加元素和Sift Up(上浮操作)

按层序添加元素65(即数组末尾添加元素), 为了满足最大堆的特性(堆中任意一个结点的值总是不大于其父节点的值), 进行上浮操作
在这里插入图片描述

/**
 * 向堆中添加元素e
 * 
 * @param e
 */
public void add(E e) {
	// 层序添加元素 相当于 动态数组末尾追加元素 
	data.addLast(e);
	// 为满足最大堆的性质: 堆中任意一个结点的值总是不大于其父节点的值. 对添加的元素进行上浮操作
	siftUp(data.getSize() - 1);
}

/**
 * 上浮操作: 为满足最大堆的性质, 堆中任意一个结点的值总是不大于其父节点的值.
 * 
 * @param index
 */
private void siftUp(int index) {
	// while循环的条件: 索引大于0 且 当前索引父节点的值 小于 当前索引节点的值
	while(index > 0 && data.get(parent(index)).compareTo(data.get(index)) < 0) {
		// 交换当前索引 与 当前索引父节点索引 的值
		data.swap(index, parent(index));
		index = parent(index);
	}
}
4.堆的取出最大元素和Sift Down(下沉操作)

取出数组中0索引处的元素为最大元素, 将数组中最后一个元素的值作为新0索引处的值同时删除最后一个元素.为了满足最大堆的特性(堆中任意一个结点的值总是不大于其父节点的值), 进行下沉操作.
在这里插入图片描述

/**
 * 取出堆中最大的元素
 * 
 * @return
 */
public E extractMax() {
	E res = findMax();
	// 交换 0索引处节点 与 最后索引处节点 的值
	data.swap(0, data.getSize() - 1);
	// 删除数组最后一个元素
	data.removeLast();
	// 下沉操作: 为满足最大堆的性质, 堆中任意一个结点的值总是不大于其父节点的值.
	siftDown(0);
	return res;
}

/**
 * 下沉操作: 为满足最大堆的性质, 堆中任意一个结点的值总是不大于其父节点的值.
 * 
 * @param index
 */
private void siftDown(int index) {
	// while循环的条件: index索引的左孩子结点的索引 小于 数组的最大索引
	while (leftChild(index) < data.getSize()) {
		// index索引的左孩子结点的索引
		int j = leftChild(index);
		// 存在 index索引的右孩子结点的索引 且  index索引的右孩子结点的索引的值 大于 index索引的左孩子结点的索引的值
		if (j + 1 < data.getSize() && data.get(j+1).compareTo(data.get(j)) > 0) {
			// 右孩子节点索引
			j++;
		}
		// data[j] 是 leftChild 和 rightChild 中的最大值
		if (data.get(index).compareTo(data.get(j)) >= 0) {
			break;
		}
		data.swap(index, j);
		index = j;
	}
}

测试

public static void main(String[] args) {
	int n = 100000;
	Random random = new Random();
	MaxHeap<Integer> maxHeap = new MaxHeap<>();
	for (int i = 0; i < n; i++) {
		// 向最大堆中添加一百万个Integer类型数据
		maxHeap.add(random.nextInt(Integer.MAX_VALUE));
	}
	int[] arr = new int[n];
	for (int i = 0; i < n; i++) {
		// 从堆中依次提取最大元素装入数组中
		arr[i] = maxHeap.extractMax();
	}
	for (int i = 1; i < n; i++) {
		// 遍历数组, 比较相邻两个元素的大小, 假设索引较小的元素大于索引较大的元素, 则抛出异常证明自定义二叉堆实现错误
		if (arr[i - 1] < arr[i]) {
			throw new IllegalArgumentException("Error");
		}
	}
	System.out.println("Test MaxHeap completed.");
}

在这里插入图片描述

5.堆的Heapify操作

将任意数组整理形成堆数据结构(两种方式).

  • 遍历数组: 依次将元素插入到一个空堆中, 时间复杂度为 O(nlogn)
  • Heapify(堆化):
    1. 首先将数组看作是一个二叉堆, 当然此时二叉堆并不满足堆的性质, 但此二叉堆中的每一个叶子节点都可以看作是一棵二叉堆
      在这里插入图片描述
    2. 然后在二叉堆中, 找到所有非叶子节点, 依次从最后一个非叶子节点到0索引所在的叶子节点 执行siftDown()下沉操作, 以此保证二叉堆的性质. 时间复杂度为 O(n)
      最后一个非叶子结点所在的索引 = 数组最后一个元素的父节点所在的索引
      在这里插入图片描述
public MaxHeap(E[] arr) {
	data = new Array<>(arr);
	for (int i = parent(arr.length - 1); i >= 0; i--) {
		siftDown(i);
	}
}

(三) 时间复杂度分析

二叉堆是一颗完全二叉树

函数时间复杂度分析
add(e)O(h) => O(logn)二叉堆是一颗完全二叉树, 因此SiftUp上浮操作的时间复杂度为 O(h) => O(logn)
extractMax()O(h) => O(logn)完全二叉树永远不会退化成链表, 因此SiftDown下沉操作的时间复杂度为 O(h) => O(logn)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值