堆与堆排

Oildum

2020-12-20 08:35:52

Personal

rt,今天我们就来讲这些东西。 --- ### 1.堆是个什么玩意 堆是一种特殊的**二叉树**,可以用**数组**模拟 ~~所以堆具有随机访问性(雾)~~ 那么堆特殊在哪呢?其实堆分为两种:**大根堆(又称最大堆、大顶堆)**、**小根堆(又称最小堆、小顶堆)**。 其中大根堆的定义如下: $$\boxed{\text{设}x\text{为大根堆中的某个节点,则}x\text{的左右孩子的值均小于等于}x\text{的值}}$$ 小根堆则是把其中的小于等于换成大于等于。 那么,如何构建一个堆呢? --- ### 2.维护堆的性质 这里我们只讲大根堆,因为小根堆应该可以自己推得( 并且堆排序用到的也是大根堆。 首先来说一下思路吧: 1.对于某个节点 $x$,检查它的左孩子和右孩子的值是否比 $x$ 的值大; 2.如果 $x$ 是三者之中最大的,那么就结束; 3.否则将最大的节点的值与 $x$ 的值交换,再在最大的节点上重复这个过程。 这很明显就是一个递归函数。 那么用上我们模拟二叉树时候的几个函数: ```java int parent(int n) { return (n-1) / 2; } int left(int n) { return 2 * n + 1; } int right(n) { return 2 * n + 2; } ``` 就可以向下一节进发了! --- ### 3.Talking is cheap. Show me the code. 上一节中我们讲到了维护大根堆性质的思路,这次我们就来写代码。 首先我们把堆这个类先写个样子出来: ```java class Heap { private int heap_size; private int[] heap; public Heap() {this(0);} public Heap(int size) { heap_size = size; heap = new int[heap_size]; } public int getSize() {return heap_size;} private int parent(int n) { return (n - 1) / 2; } private int left(int n) { return 2 * n + 1; } private int right(int n) { return 2 * n + 2; } }//先写这么多 ``` 把前面的思路写成代码就可以得到: ```java public void max_heapify(int i) {//在i处大根堆化 int l = left(i); int r = right(i);//获取左右孩子的下标 int largest = 0;//储存最大节点的下标 if (l < heap_size && heap[l] > heap[i]) { largest = l;//最大的是左孩子 } else { largest = i;//最大的是自己 } if (r < heap_size && heap[r] > heap[largest]) { largest = r;//最大的是右孩子 } if (largest != i) {//如果最大的不是自己 int t = heap[i]; heap[i] = heap[largest]; heap[largest] = t;//交换 max_heapify(largest);//继续在最大处大根堆化 } }//结束 ``` 个人认为还挺容易理解的( --- ### 4.建堆 这个就没什么说的了,直接每个节点都最大堆化即可( 代码: ```java public void buildMaxHeap(int heap[]) { this.heap = heap; this.heap_size = heap.length;//先把堆拷贝到自己这里 for (int i = heap_size / 2 - 1; i >= 0; i--) { max_heapify(i); }//在每个节点大根堆化 } ``` --- ### 5.堆排序 终于进入了正题:堆排序。 堆排序其实算是堆的一个操作之一( 思路是这样的:首先把数组变成一个大根堆,然后将最大元素与当前最末一个元素交换。这是再大根堆化即可。 代码: ```java public int[] heapsort(int arr[]) { buildMaxHeap(arr);//构造大根堆 for (int i = arr.length - 1; i > 0; i--) { int t = heap[0]; heap[0] = heap[i]; heap[i] = t;//交换第一个与最末一个 heap_size -= 1; //堆大小减少1 max_heapify(0); //在0处大根堆化 } return heap;//把排序过的数组返回 }//由此可见调用heapsort会伤及自身,珍爱数据,远离堆排( ``` --- ### 6.当前代码 现在我们的`Heap`类也是有模有样了( 先把代码放出来: ```java class Heap { private int heap_size; private int[] heap; public Heap() {this(0);} public Heap(int size) { heap_size = size; heap = new int[heap_size]; } public int getSize() {return heap_size;} private int parent(int n) { return (n - 1) / 2; } private int left(int n) { return 2 * n + 1; } private int right(int n) { return 2 * n + 2; } public void max_heapify(int i) { int l = left(i); int r = right(i); int largest = 0; if (l < heap_size && heap[l] > heap[i]) { largest = l; } else { largest = i; } if (r < heap_size && heap[r] > heap[largest]) { largest = r; } if (largest != i) { int t = heap[i]; heap[i] = heap[largest]; heap[largest] = t; max_heapify(largest); } } public void buildMaxHeap(int heap[]) { this.heap = heap; this.heap_size = heap.length; for (int i = heap_size - 1; i >= 0; i--) { max_heapify(i); } } public int[] heapsort(int arr[]) { buildMaxHeap(arr); for (int i = arr.length - 1; i > 0; i--) { int t = heap[0]; heap[0] = heap[i]; heap[i] = t; heap_size -= 1; max_heapify(0); } return heap; } } ``` 然而,堆的操作可不止这些。 --- ### 7.改静为动 目前看来,堆好像是个静态数据结构,对吧? **不!** 其实,它是**动态**的! 我们怎么实现这几种操作呢? --- ### 8.先来一个小热身 首先,我们需要一个前置:数组的插入。 这个比较简单: ```java private int[] arrAppend(int arr[], int n) { arr = java.util.Arrays.copyOf(arr, arr.length + 1);//复制并扩容 arr[arr.length - 1] = n;//加到新的空间里 return arr;//返回 } ``` --- ### 9.堆的操作 首先就是最大值。 ```java public int maximum() { if (heap_size == 0) { throw new RuntimeException("堆为空");//检查 } return heap[0];//但愿调用时满足大根堆性质 } ``` 如果调用时满足大根堆性质,那就是这么简单。 然后是删除最大值,这一点和堆排序的主循环很像: ```java public int extract_max() { int max = maximum();//取最大值,这样顺便做个检查,如果为空就有异常,也不会执行后面的语句 heap[0] = heap[heap_size - 1];//最大值与最末一个值交换 heap_size -= 1;//堆大小减1 max_heapify(0);//维护大根堆性质 return max;//返回最大值 } ``` 接下来是增值: ```java public void increase_key(int i, int key) {//在节点i处增值 if (key < heap[i]) { throw new RuntimeException("新值小于旧值");//检查 } heap[i] = key;//暴力设置 while (i > 0 && heap[parent(i)] < heap[i]) {//从此处往上,检查有没有违反最大堆性质,如果有,进入循环 int t = heap[i]; heap[i] = heap[parent(i)]; heap[parent(i)] = t;//交换当前值与父节点值 i = parent(i);//继续往上 } } ``` 最后是插入和删除: ```java public void insert(int key) { heap = arrAppend(heap, Integer.MIN_VALUE); heap_size += 1; increase_key(heap_size-1, key); } public void delete(int i) {//删除i处的值 increase_key(i, Integer.MAX_VALUE); extract_max(); } ``` 怎么样,是不是不难( --- ### 10.结语 呼,终于讲完了 这是我整个系列里最长的一篇文章(((