【数据结构】堆
堆
这种数据结构。一般堆用来实现优先级队列。优先级队列:和通常的栈和队列一样,只不过里面的每个元素都有一个“优先级”,在处理的时候,首先处理优先级最高的。通常包含三个操作getMax/delMax/insert
栈和队列算是优先级队列的特例。
使用其他数据结构均不能同时在O(lgn)的复杂度下完成。至少有一种操作要耗时O(n).比如链表的插入操作O(1),但是获取最大值必须遍历链表O(N)。
可以使用BBST达到最好的时间复杂度O(lgn)。事实上没必要用那么高端的数据结构来实现如此简单的功能,因为这里毕竟只需要这三个简单的功能而已。
于是堆这种数据结构应运而生。
堆在逻辑上是一颗完全二叉树(平衡因子处处非负的AVL树),在物理上直接借助向量(可变数组),因此具有向量的形,树的神。
堆就是在向量的元素间定义了父子关系。
最大堆:所有节点的值均小于等于父节点,最小堆则反之。如下图为一个最大堆,我这里以最大堆为例。
构造这样的一个数据结构,上述插入删除接口的时间复杂度均为O(h),由于完全二叉树的树高为O(lgn),于是有delMax,insert的时间复杂度均为O(lgn),getMax只需要O(1)的时间。
在下面的代码中我没有使用向量,而是自己维护了一个数组,可以达到相同的效果。
堆的实现
public class Heap { private static int PARENT(int i){ return (i-1)>>1; } private static int LEFT(int i){ return 1+(i<<1); } private static int RIGHT(int i){ return (i+1)<<1; } private int[] heapArray; private int n; public Heap(int MAX_SIZE) { heapArray = new int[MAX_SIZE]; } /** * 获得堆中最大值 * @return */ public int getMax(){ if (n == 0) { throw new IndexOutOfBoundsException(); } return heapArray[0]; } /** * 删除堆中最大值 */ public void deleteMax() { if (n == 0) { throw new IndexOutOfBoundsException(); } heapArray[0] = heapArray[n-1]; n--; percolateDown(0); } /** * 插入操作 * @param e */ public void insert(int e) { heapArray[n++] = e; // percolateUP(n-1); percolateUPRecu(n-1); } /** * 上溢 * @param target 表示要进行上溢操作的元素的下标 */ private void percolateUP(int target) {// 完全二叉树树高控制在O(lgn) 上溢的时间复杂度O(lgn) 3lgn----> lgn+2 int parent = PARENT(target); while (target > 0) {// target最多上溢到根 if(heapArray[target] <= heapArray[parent]) break; swap(heapArray, parent, target); target = parent; parent = PARENT(target); } } /** * 递归上溢 * @param target 表示要进行上溢操作的元素的下标 */ private void percolateUPRecu(int target) { if (target > 0) { if (heapArray[target] > heapArray[PARENT(target)]) { swap(heapArray, PARENT(target), target); percolateUPRecu(PARENT(target)); } } } /** * 下滤 * @param target 表示要进行下滤操作的元素的下标 */ private void percolateDown(int target) { int maxIndex = maxOfThree(heapArray,target); while (target != maxIndex) {// 当target不是和两个孩子比起来最大的就一直下滤 swap(heapArray, maxIndex, target); target = maxIndex; maxIndex = maxOfThree(heapArray, target); } } /** * Floyd创建堆 自下而上的下滤 复杂度为O(n) 求和height(i) * @param heapArr */ public void createHeapFloyd(int [] heapArr){ System.arraycopy(heapArr, 0, heapArray, 0, heapArr.length); n = heapArr.length; for (int i = (n - 1) >> 1; i >= 0; i--) { percolateDown(i); } } /** * 创建堆 自上而下的上溢 复杂度为O(nlgn) 求和depth(i) * @param heapArr */ public void createHeap(int[] heapArr) { // for (int i : heapArr) { // insert(i); // } // 更紧凑的写法 System.arraycopy(heapArr, 0, heapArray, 0, heapArr.length); n = heapArr.length; for (int i = 0; i < n; i++) { percolateUP(i); } } /** * 获取target以及它的两个孩子中的对应值最大的下标 * @param heapArray * @param target * @return 返回target以及它的两个孩子中的对应值最大的下标 */ private int maxOfThree(int[] heapArray, int target) { // 比较target和他的两个孩子的大小,返回最大的下标 // 有效下标不能超过n-1 int left = LEFT(target);// > n-1 ? Integer.MIN_VALUE : LEFT(target); int right = RIGHT(target); // > n-1 ? Integer.MIN_VALUE : RIGHT(target); int max = target; if (left<=n-1) { max = heapArray[left] > heapArray[max] ? left : max; } if (right<=n-1) { max = heapArray[right] > heapArray[max] ? right : max; } return max; } private void swap(int[] heapArray, int i, int j) { int temp; temp = heapArray[i]; heapArray[i] = heapArray[j]; heapArray[j] = temp; } }
其中每个方法的具体意思已经做了详细的注释。
相关算法
下面关于上溢和下滤的算法和创建堆的算法做以解释。
上溢
当插入一个新的元素时,我们首先将这个新元素放在数组的最后,然后将其与逻辑上的父亲节点比较,如果大于父亲节点,则一直重复交换,直到满足最大堆的条件或者已交换到树根位置,具体的可参考下图
下滤
当删除一个最大的元素也就是堆中的第一个元素,我们首先将堆中的最后一个元素放在放到第一个位置,然后将其与逻辑上的孩子节点比较,如果小于孩子节点,则一直重复与最大的孩子节点交换,直到满足最大堆的条件或者没有孩子,
下滤操作其实也可以理解成是通过插入一个元素将两个小的最大堆合并成一个大的最大堆的过程。具体的可参考下图
创建堆
上述代码中共列举了两种创建最大堆的方法
1.自上而下的上溢
public void createHeap(int[] heapArr) { System.arraycopy(heapArr, 0, heapArray, 0, heapArr.length); n = heapArr.length; for (int i = 0; i < n; i++) { percolateUP(i); } }
遍历堆中的所有元素,对其进行上溢操作。于是共有n个元素,每个元素上溢的时间复杂度为O(lgn)。因此时间复杂度为T(n)=O(nlgn)
实际上时间复杂度详细的为T(n)= n∑depth(i),我们知道完全二叉树中超过一半的都是叶子结点深度为O(lgn),因此T(n)也是nlgn数量级的。
2.自下而上的下滤
public void createHeapFloyd(int [] heapArr){ System.arraycopy(heapArr, 0, heapArray, 0, heapArr.length); n = heapArr.length; for (int i = (n - 1) >> 1; i >= 0; i--) { percolateDown(i); } }
下滤操作我们是在删除堆中的最大元素时使用到,其实也可以理解成是将两个小的最大堆合并成一个大的最大堆的过程。
实际上也可以用在建堆操作中。
由于一个完全二叉树的从下标(n-1)/2+1开始就是叶子结点。于是每个叶子结点都可以堪称是一个只有一个元素的最大堆。所以从下标(n-1)/2开始倒序的进行下滤操作。
这种算法的时间复杂度为T(n)=O(n)。实际上,时间复杂度详细的为T(n)= (n-1)/2∑height(i)=O(n)
完全二叉树越往下节点越多,于是第一个算法与深度成线性关系其复杂度更高而第二个算法与高度成线性关系,显然,复杂度比第一个小很多。