堆的相关概念
1.堆结构就是用数组实现的完全二叉树结构,逻辑上是一棵完全二叉树,但物理上是保存在数组中。
2.完全二叉树中如果每棵子树的最大值都在顶部就是大根堆
3.完全二叉树中如果每棵子树的最小值都在顶部就是小根堆
顺序存储中父子节点的关系
数组模拟,从0位置开始,i为下标
父节点:(i-1)/2
左子节点:2*i+1
右子节点:2*i+2
通过i下标,和公式,就可以得出当前下标节点的父节点,与左右子节点的位置。
数组模拟,从1位置开始,i为下标
左:2*i (i << 1)
右:2*i+1 (i << 1 |1)
父:i/2 (i >> 1)
数组模拟堆,第一个是从0位置开始,第二个是从1位置开始。
因为开始位置不同,所以计算的公式也不同。但第从1位置开始,可以优化成位运算。
Java中的堆实现
public static class MyMaxHeap {
private int[] heap;
private final int limit;
private int heapSize;
public MyMaxHeap(int limit) {
heap = new int[limit];
this.limit = limit;
heapSize = 0;
}
public boolean isEmpty() {
return heapSize == 0;
}
public boolean isFull() {
return heapSize == limit;
}
public void push(int value) {
if (heapSize == limit) {
throw new RuntimeException("heap is full");
}
heap[heapSize] = value;
heapInsert(heap, heapSize++);
}
public int pop() {
int ans = heap[0];
swap(heap, 0, --heapSize);
heapify(heap, 0, heapSize);
return ans;
}
private void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
private void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1;
while (left < heapSize) {
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
private void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
堆结构两个重要方法,heapInsert与heapify
heapInsert用于构建堆
push添加操作就主要调用的heapInsert方法
public void push(int value) {
if (heapSize == limit) {
throw new RuntimeException("heap is full");
}
//既是size,也可以用作新来元素存放的位置
heap[heapSize] = value;
heapInsert(heap, heapSize++);
}
因为用的数组实现堆,当数组添加了一个元素时,就会执行heapInsert操作,调整元素位置,以构建成逻辑上的堆。
heapInsert方法
private void heapInsert(int[] arr, int index) {
// (i-1)/2 为父节点位置
// 循环交换,直到不比父大
// index=0
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
新加进来的数,现停在了index位置。然后进行通过公式,与当前父节点循环比较,如果当前index大于此时它的父节点,就与父节点进行交换。不仅值交换了,当然index也要进行更新,变成它之前的父节点的index值。然后继续进行比较交换。
添加一个数,数在堆的逻辑中,位于完全二叉树的最底层。然后通过比较值的大小,不断往上移动,移动到正确的位置,以构成最大堆。
heapify也用于维护堆结构
pop取出数据主要调用heapify方法
public int pop() {
int ans = heap[0];
swap(heap, 0, --heapSize);
heapify(heap, 0, heapSize);
return ans;
}
pop取出大根堆中最大的值,即数组中的第一个数,相当于完全二叉树中的根节点。
pop数据,我们需要维护堆的结构。
所以上面代码大致思路为
记录heap[0]的值,然后将根节点与最后的节点进行交换,堆的heapSize减1。
这样根节点的值不是删除,而是使用最后的根节点进行补充,而原来的根节点因为交换到了最后,而heapSize也减1,所以相当于,将其排除出了堆中。
但因为交换后根节点的值,不满足大根堆的结构,所以我们需要进行heapify,维护堆结构。
heapify方法
因为根节点的值为之前最小节点的值,所以我们可以将其,与子节点进行循环比较。
往下看,不断的下沉,循环完成,则为一个正常的堆结构。
private void heapify(int[] arr, int index, int heapSize) {
//通过index根节点,计算出左子节点位置
int left = index * 2 + 1;
//有左子节点,就进入循环,进行比较。
// 如果有左孩子,可能有或没有右孩子。所以具体的在循环内部操作
while (left < heapSize) {
// 把较大孩子的下标,给largest
// 左右子节点的比较,如果存在右子节点left + 1 < heapSize,
// 并且右子节点大于左子节点,largest就为右子节点,否则是左
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
//最大值节点与父节点比较,最大为largest
largest = arr[largest] > arr[index] ? largest : index;
//largest等于index,即在上一行比较中,子节点没干过父节点index,index为最大值,堆结构正常,直接break
if (largest == index) {
break;
}
//子节点大于父节点index,index和较大子节点,要互换
swap(arr, largest, index);
//更新index与left,继续比较。
index = largest;
left = index * 2 + 1;
}
}
注:优先级队列结构,就是堆结构
堆排序
堆排序时间复杂度
一个数组,进行堆排序。
数组所有元素,循环heapinsert构建堆。时间复杂度O(N*logN)
因为一共N个数,想象中的树高度为logN ,所以一次操作O(logN),循环了N次。即O(N * logN)
然后不断swap,heapSize减一,heapify。
相当于将最大数放到数组末尾,heapSize减一将其排除出堆结构,heapify维护堆结构
int heapSize = arr.length;
swap(arr, 0, --heapSize);
// O(N*logN)
while (heapSize > 0) { // O(N)
heapify(arr, 0, heapSize); // O(logN)
swap(arr, 0, --heapSize); // O(1)
}
此操作也是,O(N * logN)
所有整个sort操作有2次O(N * logN),所以堆排序的时间复杂度为O(N * logN)
堆排序的优化
一个需求,只需要将数组形成堆结构,不需要排序,可以有O(N)的时间复杂度。
上面构建堆,是循环heapInsert,时间复杂度O(N*logN)
而我们可以从数组尾部倒序开始,每个节点heapify。
此操作只能用于一次得到所有数据,然后构建堆。如果是一个一个得到数据,则不适用
for (int i = arr.length - 1; i >= 0; i--) {
heapify(arr, i, arr.length);
}
==为什么是O(N)复杂度?==
可能有人会疑惑,为什么堆排序中,我的一次heapify是 O(logN)。循环N次O(N * logN)
但构建堆时,同样的heapify,进行N次,怎么变成O(N)了。
因为堆排序中,每次的heapify都是完整都走完树的高度,logN。循环N次,即O(N * logN)
但我构建堆时
从最后一层(N/2个节点)开始heapify,但无法往下沉,进行一次操作,复杂度N/2*1
倒数第二层(N/4个节点),heapify,最多往下沉1次,所以2次操作,复杂度N/4*2
倒数第三层(N/8个节点),heapify,最大往下沉2次,所以3次操作,复杂度N/8*3
依次类推
可以参考知乎的回答
堆排序中建堆过程时间复杂度O(n)怎么来的? - TwoFrogs的回答 - 知乎 https://www.zhihu.com/question/20729324/answer/16025846
堆排序代码与对数器
public class Code03_HeapSort {
// 堆排序额外空间复杂度O(1)
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// O(N*logN)
for (int i = 0; i < arr.length; i++) { // O(N)
heapInsert(arr, i); // O(logN)
}
// O(N)
// for (int i = arr.length - 1; i >= 0; i--) {
// heapify(arr, i, arr.length);
// }
int heapSize = arr.length;
swap(arr, 0, --heapSize);
// O(N*logN)
while (heapSize > 0) { // O(N)
heapify(arr, 0, heapSize); // O(logN)
swap(arr, 0, --heapSize); // O(1)
}
}
// arr[index]刚来的数,往上
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
// arr[index]位置的数,能否往下移动
public static void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1; // 左孩子的下标
while (left < heapSize) { // 下方还有孩子的时候
// 两个孩子中,谁的值大,把下标给largest
// 1)只有左孩子,left -> largest
// 2) 同时有左孩子和右孩子,右孩子的值<= 左孩子的值,left -> largest
// 3) 同时有左孩子和右孩子并且右孩子的值> 左孩子的值, right -> largest
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
// 父和较大的孩子之间,谁的值大,把下标给largest
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
// for test
public static void comparator(int[] arr) {
Arrays.sort(arr);
}
// for test
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
// for test
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
// for test
public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
// for test
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
// for test
public static void main(String[] args) {
// 默认小根堆
PriorityQueue<Integer> heap = new PriorityQueue<>();
heap.add(6);
heap.add(8);
heap.add(0);
heap.add(2);
heap.add(9);
heap.add(1);
while (!heap.isEmpty()) {
System.out.println(heap.poll());
}
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
heapSort(arr1);
comparator(arr2);
if (!isEqual(arr1, arr2)) {
succeed = false;
break;
}
}
System.out.println(succeed ? "Nice!" : "Fucking fucked!");
int[] arr = generateRandomArray(maxSize, maxValue);
printArray(arr);
heapSort(arr);
printArray(arr);
}
}
本文由博客一文多发平台 OpenWrite 发布!