1 堆
堆的重要性质:任意节点的值总是大于等于(或者小于等于)子节点的值
- 如果任意节点的值总是 ≥ 子节点的值 称为 最大堆 大根堆 大顶堆
- 如果任意节点的值总是 ≤ 子节点的值 称为 最小堆 小根堆 小顶堆
2 二叉堆
二叉堆的逻辑结构是一个完全二叉树 也叫完全二叉堆
鉴于完全二叉树的一些特性,二叉堆的底层(物理结构)一般用数组实现
索引 i 的规律,(n是元素的数量)
- 如果 i = 0,它是根节点
- 如果 i > 0,它的父节点索引为 floar( (i-1) / 2) 这里floar表示向下取整
- 对于索引为 i 的节点,如果 2i+1 ≤ n-1(不能超过最大索引),则其左叶子节点索引为 2i+1
- 对于索引为 i 的节点,如果 2i+1 > n-1(超过最大索引),则没有左子节点,因为是完全二叉树当然就不可有右字节点
- 对于索引为 i 的节点,如果 2i+2 ≤ n-1(不能超过最大索引),则其右叶子节点索引为 2i+2
- 对于索引为 i 的节点,如果 2i+2 > n-1(超过最大索引),则没有右子节点,但是可能有左字节点也可能没有
- 因为是完全二叉树,所以其第一个非叶子节点索引为 n/2 -1 注意 n 为 元素数量
3 最大堆的建立
两种方式: 1、自上而下的上虑 2、自下而上的下滤
上虑:把当前元素向上寻找其该在的位置,下滤:把当前元素向下寻找其该在的位置
对于大顶堆来说,上虑操作:当前元素与其父节点元素比较,如果小于父节点元素就进行交换位置,交换位置后再与其父节点元素进行比较,直到找到第一个小于其父节点元素的位置。而下滤操作是:当前元素其子节点中较大的元素进行比较,如果小于子节点,就进行交换位置,交换位置后再次与其子节点较大的元素进行比较,直到找到第一个大于其子节点元素的位置。因为下滤操作是当前点元素与子节点比较,所以对于叶子节点来说其没有子节点,因此没有下虑的必要,可以从第一个非叶子节点开始进行下虑,即索引为(n/2-1)的元素开始。需要注意的是,下滤过程是父节点与子节点中相对较大的节点进行交换位置,即先从子节点中比较得出较大的子节点,再将较大的元素与父节点进行比较。
显然:对于自上而下的上虑操作来说,每上滤一个元素,所有已经上虑完成的元素可以构成一个大顶堆,所以当上虑完成所有的元素的时候,整个堆会变成一个大顶堆。而对于自下而上的下滤操作来说,每完成一个下滤元素,该元素与左右子树构成一个大顶堆,以上图为例,第一次下虑完成后,[13, 17] 构成大顶堆,第二次下虑完成后,[7,19,25]构成大顶堆,第三次下滤时,父节点元素2的左右子树都已经时大顶堆了,元素2进行下滤时,其右子节点25要大于左子节点,所以元素2与右子节点25进行交换位置,当交换完成后,显然[25,17,13]并没有影响到左子树的大顶堆结构,如果进行交换的元素是17,显然右子树[13,2]其大顶堆结构会被破环,同时[2,17,25]也不能构成大顶堆结构,显然这样交换是不合理的。这里就解释了为什么自下而上的下滤操作可以构建堆结构,原因就是:每完成一个下滤操作,该节点与其所有以下滤的左右子树可以构成大顶堆,所以根节点下滤完成整个的堆结构完成构建过程。
4 堆的删除操作(删除堆顶元素)
以大顶堆为例,因为对于二叉堆结构来说,底层是数组结构,删除堆顶元素即数组的第一个元素,为减少数组的元素移动,是将数组的最后一个元素放到数组的第一个位置即堆顶位置,显然堆顶(根节点)的左右子树仍然保持了其大顶堆的结构,这是只需要对堆顶元素进行下滤操作(当前元素向下寻找其合适的位置)即可重新构建其堆结构。
自上而下的上虑、自下而上的下滤 两者效率比较(结论:自下而上的下滤操作效率要高)
5 堆排序
排序原理,将数组元素进行建堆,这里以大顶堆为例,整个数组完成大顶的建立后,将堆顶元素与数组的最后一个元素进行交换位置,在数组的前n-1一个元素再次进行建堆操作,完成建堆操作后再将堆顶元素与数组的倒数第二个元素进行交换,这样数组的最后两个元素就是整个数组的最大的两个元素,依次类推,直到完成整个数组的排序。整个排序过程其实就是利用了堆定元素的删除重新建堆的操作。
代码实现
public static void main(String[] args) {
int[] arr = {2,7,13,25,19,17,45,5,15};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void heapSort(int[] arr) {
// 原地建堆
int size = arr.length;
int index = (size >> 1) - 1;// 第一个非叶子节点开始下滤
System.out.println(Arrays.toString(arr));
while (index >= 0) {
siftDown(arr, index--, size);
System.out.println(Arrays.toString(arr));
}
System.out.println("----------------------");
while (size > 1) {
swap(0, size - 1 ,arr);
size--;
siftDown(arr, 0, size);
System.out.println(Arrays.toString(arr));
}
}
/**
* 下滤操作
* @param arr 下滤操作的数组
* @param index 当前下滤操作元素在数组中索引
* @param size 要要下了操作的数组的元素个数(并不一定是对所有的数组元素进行下滤,
* 可以是数组的前面的一部分进行下滤)
*/
private static void siftDown(int[] arr, int index, int size) {
while (2 * index + 1 <= size - 1) {// 只要存在子节点
int bigger = 2 * index + 1;
if (2 * index + 2 <= size - 1) {
// 找到左右子节点相对较大的元素
bigger = compare(2 * index + 1, 2 * index + 2, arr);
}
if (arr[bigger] > arr[index]) {
swap(bigger, index, arr); // 交换位置
index = bigger;
} else {
break;
}
}
}
/**
* 交换位置
*/
private static void swap(int i, int j, int[] arr) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
/**
* 返回较大的元素
*/
private static int compare(int left, int right, int[] arr) {
return arr[left] - arr[right] > 0 ? left : right;
}