public Heap(int count) { capacity = count; arr = newint[capacity+1]; n = 0; }
public void insert(int value) { if (n >= capacity) { // 超过堆大小了,不能再插入元素 return; } n++; // 先将元素插入到队尾中 arr[n] = value;
int i = n; // 由于我们构建的是一个大顶堆,所以需要不断调整以让其满足大顶堆的条件 while (i/2 > 0 && arr[i] > arr[i/2]) { swap(arr, i, i/2); i = i / 2; } } }时间复杂度就是树的高度,所以为 O(logn)。
删除堆顶元素
由于堆的特点(节点的值都大于等于(或小于等于)其子节点的值),所以其根节点(堆项)要么是所有节点中最大,要么是所有节点中最小的,当删除堆顶元素后,也需要调整子节点,以让其满足堆(大顶堆或小顶堆)的条件。假设我们要操作的堆是大顶堆,则删除堆顶元素后,要找到原堆中第二大的元素以填补堆顶元素,而第二大的元素无疑是在根节点的左右子节点上,假设是左节点,则用左节点填补堆顶元素之后,左节点空了,此时需要从左节点的左右节点中找到两者的较大值填补左节点...,不断迭代此过程,直到调整完毕,调整过程如下图示:但是这么调整后,问题来了,如上图所示,在最终调整后的堆中,出现了数组空洞,对应的数组如下:怎么解决?我们可以用最后一个元素覆盖堆顶元素,然后再自上而下地调整堆,让其满足大顶堆的要求,这样即可解决数组空洞的问题。看了以上示意图,代码实现应该比较简单,如下:/** * 移除堆顶元素 */ public void removeTopElement() { if (n == 0) { // 堆中如果没有元素,也就是不存在移除堆顶元素的情况了 return; } int count = n; arr[1] = arr[count]; --count; heapify(1, count); }
/** * 自上而下堆化以满足大顶堆的条件 */ public void heapify(int index, int n) {
while (true) { int maxValueIndex = index; if (2 * index <= n && arr[index] < arr[2 * index]) { // 左节点比其父节点大 maxValueIndex = 2 * index; }
if (2 * index + 1 <= n && arr[maxValueIndex] < arr[2 * index + 1]) { // 右节点比左节点或父节点大 maxValueIndex = 2 * index + 1; }
if (maxValueIndex == index) { // 说明当前节点值为最大值,无需再往下迭代了 break; } swap(arr, index, maxValueIndex); index = maxValueIndex; } }
/** * 交换数组第 i 和第 j 个元素 */ public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }时间复杂度和插入堆中元素一样,也是树的高度,所以为 O(logn)。
堆排序
用堆怎么实现排序?我们知道在大顶堆中,根节点是所有节点中最大的,于是我们有如下思路:假设待排序元素个数为 n(假设其存在数组中),对这组数据构建一个大顶堆,删除大顶堆的元素(将其与数组的最后一个元素进行交换),再对剩余的 n-1 个元素构建大顶堆,再将堆顶元素删除(将其与数组的倒数第二个元素交换),再对剩余的 n-2 个元素构建大顶堆...,不断重复此过程,这样最终得到的排序一定是从小到大排列的,堆排序过程如下图所示:从以上的步骤中可以看到,重要的步骤就两步,建堆(堆化,构建大顶堆)与排序。先看下怎么建堆,其实在上一节中我们已经埋下了伏笔,上一节我们简单介绍了堆的基本操作,插入和删除,所以我们可以新建一个数组,遍历待排序的元素,每遍历一个元素,就调用上一节我们定义的 insert(int value) 方法,这个方法在插入元素到堆的同时也会堆化调整堆为大顶堆,遍历完元素后,最终生成的堆一定是大顶堆。用这种方式生成的大顶堆空间复杂度是多少呢,由于我们新建了一个数组,所以空间复杂度是 O(n),但其实堆排序是原地排序的(不需要任何额外空间),所以我们重点看下如何在不需要额外空间的情况下生成大顶堆。其实思路很简单,对于所有非叶子节点,自上而下不断调整使其满足大顶堆的条件(每个节点值都大于等于其左右节点的值)即可,遍历到最后得到的堆一定是大顶堆!同时调整堆的过程中只是不断交换数组里的元素,没有用到额外的存储空间。那么非叶子节点的范围是多少呢,假设数组元素为 n,则数组下标为 1 到 n / 2 的元素是非叶子节点。下标 n / 2 + 1 到 n 的元素是叶子节点。画外音:假设下标 n/2+1 的节点不是叶子节点,则其左子节点下标为 (n/2 + 1) * 2 = n + 2,超过了数组元素 n,显然不符合逻辑,由此可以证明 n / 2 + 1 开始的元素一定是叶子节点。示意图如下:如图示:对每个非叶子节点自上而下调整后,最终得到大顶堆。有了以上思路,不难写出如下代码:/** * 对 1 妻 n/2 的非叶子节点自上而下进行堆化,以构建大顶堆 */ public void buildHeap() { for (int i = n/2; i > 0; i--) { heapify(i); } }这样建堆的时间复杂度是多少呢,我们知道对每个元素进行堆化时间复杂度是 O(log n),那么对 1 到 n/2 个元素进行堆化,则总的时间复杂度显然是 O(n log n)(实际上如果详细推导,时间复杂度是 O(n),这里不作展开,有兴趣的同学建议查一下资料看下 O(n) 是怎么来的)。知道怎么建堆,接下来排序就简单了,对 n 个元素来说,只要移除堆顶元素(将其与最后一个元素交换),再对之前的 n-1 个元素堆化,再移除堆顶元素(将其与倒数第二个元素交换)...,不断重复此过程即可,代码如下:/** * 堆排序 */ public void heapsort() { // 建堆 buildHeap(); int i = n; while (true) { if (i <= 1) { break; }