二叉树有两种实现结构,一种是链式结构(详解在上一篇),一种就是顺序结构,普通二叉树其实是不适合用数组来存储数据,因为会造成大量空间的浪费,但完全二叉树似乎更合适于顺序结构存储,我们通常把堆(完全二叉树)使用顺序数组来存储
1. 什么是堆?
堆就是将一个集合的数据按照完全二叉树的顺序结构存储在一个一维数组中,堆在逻辑上是一棵完全二叉树,在物理结构上是一个一维数组.
按照根结点的大小分为大堆(根结点的值最大)和小堆(根结点的值最小)
1.1 堆的性质
性质:
- 对于任意一个结点,都要求根的值 大于或等于 其所有子树结点的值
- 堆是一棵完全二叉树
1.2 堆的结构
1.3 怎么实现堆
(1)向下调整
现在给出一个数组,逻辑上可以看成是一棵完全二叉树,通过从根节点向下调整的方式把它调整成一个小堆,可以向下调整的前提是左右子树必须是个堆,除了一个位置以外,所有其他位置都已经满足堆的性质了才能进行向下调整
int[] array = { 27,15,19,18,28,34,65,49,25,37 };
向下调整三次,最后形成小堆
注意刚开始是只有15和27的位置不满足堆的性质
向下调整的步骤:
- 找到要调整结点的最小的孩子
- 比较根结点和最小孩子的值,如果根结点小于最小孩子的值,返回,不进行调整
- 如果根结点的值大于最小孩子的值,交换值,
- 继续向下调整,直到叶子结点[找最大的孩子就是大堆,找最小的孩子就是小堆,]
下面来看一段向下调整成大堆的代码:
package com.bittech;
/**
* Author:weiwei
* description:
* Creat:2019/5/2
**/
public class Heap {
/**
* 向下调整(堆化)大堆
* 必须满足可以向下调整的前提:只有一个位置不满足堆
* @param tree 完全二叉树的数组
* @param index 要调整位置的下标
*/
private static void heapify(int[] tree,int size,int index){
/**
* 判断index位置是不是叶子结点
* 完全二叉树可以只判断有没有左孩子
* 转化成数组下标越界的问题去处理
*/
int left = 2*index+1;
if(left >= size){
return; //数组越界就返回,只要小于数组长度就一直找
}
/**
*否则,不是叶子结点就一定有左孩子,但不一定有右孩子
* 找到最大的一个孩子
* 没有右孩子,就去左孩子找
* 有右孩子
* 1.左边大 左孩子
* 2.右边大 右孩子
*/
int right = 2*index+2;
int max = left;//假设大的数在左边
if(right >size && tree[right] > tree[left]){
max=right;
}
/**
* 和根的值进行比较
* 如果比根的值大,满足堆的性质,不需要调整
* 否则,交换数组中两个下标的值
* 并且继续以max作为下标,继续向下调整
*/
if(tree[index] >= tree[max]){
return;
} //不需要调整
/**
* 根的值比较小,先交换,
*/
int t = tree[index];
tree[index] = tree[max];
tree[max] = t;
//继续向下调整
heapify(tree,size,max);
}
}
反之小堆也是一样
package com.bittech;
/**
* Author:weiwei
* description:
* Creat:2019/5/2
**/
public class adjustDown {
/**
* 向下调整(小堆)
*向下调整考虑左右两个孩子结点
* @param array
* @param size
* @param index
*/
private static void adjustDown(int[] array, int size, int index) {
int min = 2 * index + 1;
while (min < size) {
if (min + 1 < size && array[min + 1] < array[min]) {
min += 1;
}
if(array[index] <= array[min]){
break; //如果下标小于最小值,就不需要调整了,break
}
//向下调整,交换
int t = array[min];
array[min] = array[index];
array[index] = t;
//继续向下调整
index = min;
min = 2* index+1;
}
}
}
(2)向上调整
向上调整的时候只需要考虑双亲结点值的大小,相对于向下调整简单一些,如果是大堆就把下面最大的元素往上调整,如果是小堆就把最小的元素往上调整
package com.bittech;
/**
* Author:weiwei
* description:
* Creat:2019/5/2
**/
public class adjustUp {
/**
* 向上调整(大堆)
* 向上调整只需要考虑双亲结点
* @param array
* @param size
* @param index
*/
private void adjustUp(int[] array,int size,int index){
while(index > 0){
int parent = (index - 1)/2;
if(array[parent] >= array[index]){
break; //如果双亲结点大于等于下标,就说明满足大堆的性质,不需要交换,break
}
//否则,交换
int t = array[parent];
array[parent] = array[index];
array[index] = t;
//继续向下调整
index = parent;
}
}
}
1.4 怎么创建堆
堆的创建
/**
* 时间复杂度:细算 O(n)
* 粗略 O(n * log(n))
* @param array
*/
private void createHeap(int[] array, int size) {
// 从最后一个非叶子结点的下标开始,一路向下调整至根位置
// [(array.length - 2) / 2, 0 ]
for (int i = (size - 2) / 2; i >= 0; i--) {
heapify(array, size, i);
}
}
public int getSize() {
return size;
}
}
1.5 堆的插入
先插入一个80到数组的尾上,再进行向上调整算法,直到满足堆。
1.6 堆的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
1.7 堆排序
堆排序的过程:
将数组分为有序区间和无序区间,刚开始有序区间元素为0,无序区间元素为array.length
每一次选择一个最大的元素放到无序区间的后面,无序区间慢慢变小,有序区间慢慢变大,直到全部元素都排好序
注意:堆排序只能选择最大的放到最后面,不能选择最小的放到最前面
代码展示:
private static void biggestHeapify(int[] array, int size, int index) {
int max = 2 * index + 1;
while (max < size) {
if (max + 1 < size && array[max+1] > array[max]) {
max += 1;
}
if (array[index] >= array[max]) {
break;
}
int t = array[max];
array[max] = array[index];
array[index] = t;
index = max;
max = 2 * index + 1;
}
}
// O(n * log(n))
private static void heapSort(int[] array) {
// 建大堆
for (int i = (array.length - 2) / 2; i >= 0; i--) {
biggestHeapify(array, array.length, i);
}
for (int j = 0; j < array.length; j++) {
// 无序区间 [0, array.length - j)
// 有序区间 [array.length - j, array.length)
// 交换最大的数到无序部分的最后位置
int t = array[0];
array[0] = array[array.length - j - 1];
array[array.length - j - 1] = t;
// 无序部分,除了 [0] ,都满足堆的性质
biggestHeapify(array, array.length - j - 1,0);
}
}