一、堆
1.1 概念
堆大白话讲就是完全二叉树。如果有一个 关键码的集合 K = {k0 , k1 , k2 , … , kn-1} ,把它的所有元素 按完全二叉树的顺序存储方式存储 在一 个一维数组中 ,并满足: Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >=K2i+2) i = 0 , 1 , 2… ,则 称为小堆 ( 或大堆) 。(即双亲比孩子的数值小(大)——小(大)堆)将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
1.2 性质
1. 堆中某个节点的值总是不大于或不小于其父节点的值(保持大小根堆);
2. 堆总是一棵完全二叉树。
二、堆的实现
2.1 向下调整算法(建立大根堆)
1. 从最后一个节点的父亲节点开始向下调整。
2. 首先判断此父亲节点是否有两个孩子,然后从中选出一个最大的孩子与父亲节点进行比较(若只有一个孩子就此孩子最大),如果孩子大就与父亲交换位置。
3. 然后接着往下调整,此时孩子就是父亲,孩子是父亲*2+1,依次循环
所以,如果想建立大根堆,不能直接从根节点开始一次向下调整就能建立大根堆。需要从最后一个节点的父亲节点开始往上的节点都要进行向下调整即可以建成大根堆。
堆的建立一般用的是数组。
public void initElem(int[] array){ for (int i = 0; i < array.length; i++) { elem[i] = array[i]; usedSize++;//堆里的元素个数 } } public void createHeap() { for (int parent = (usedSize-1-1)/2; parent >=0 ; parent--) { shiftDown(parent,usedSize); } } private void shiftDown(int parent,int len) { int child = parent*2 + 1; while(child < len){ if(child+1 < len && elem[child] < elem[child+1]){ child++; } if(elem[parent] < elem[child]){ int temp = elem[child]; elem[child] = elem[parent]; elem[parent] = temp; parent = child; child = 2*parent+1; }else { break; } } }
2.2 向上调整(建立大根堆)
1. 给一个孩子节点,然后求出父亲节点就是(child-1)/ 2
2. 然后对比孩子和父亲的大小,交换即可
3. 然后孩子就是父亲,父亲就是(child-1) / 2,依次遍历
如果利用向上调整建立大根堆,需要从最后一个节点依次往上进行向上调整遍历到根节点(根节点不需要向上调整,也没有上面了,狗头)。
private void shiftUp(int child) { int parent = (child - 1)/2; while(child > 0){ if(elem[child] > elem[parent]){ int temp = elem[child]; elem[child] = elem[parent]; elem[parent] = temp; child = parent; parent = (child-1)/2; }else { break; } } }
利用向上调整建立大根堆代码小伙伴就自己实现咯
2.3 建堆的时间复杂度
向下调整建立大根堆(小根堆)的时间复杂度如下图推导
如果是向上调整建立大根堆的话那就比向下调整慢了很多,跟向下调整推导几乎一样
2.4 堆的插入与删除
堆的插入就是将数组最后一个元素后面插入一个新的元素,然后将这个元素进行向上调整就可插入堆中。
堆的删除就是将数组第一个元素删除,将数组第一个元素与数组最后一个元素交换位置,然后size减1,然后对新根进行向下调整。
当然进行删除和插入,要检查数组是否空或满。
/** * 出队【删除】:每次删除的都是优先级高的元素 * 仍然要保持是大根堆 */ public void pollHeap() { if (isEmpty()){ return; } int temp = elem[0]; elem[0] = elem[usedSize-1]; elem[usedSize-1] = temp; usedSize--; shiftDown(0,usedSize); }
//插入 public void push(int val) { if(isFull()){ elem = Arrays.copyOf(elem,elem.length*2); } elem[usedSize++] = val; shiftUp(usedSize-1); }
三、堆的运用
3.1 堆排序
升序需要建立大根堆
降序需要建立小根堆
具体怎么实现,就拿升序建立大根堆做例子,已知大根堆的根节点是最大的,根就与数组最后一个元素做交换,然后将堆的个数减1,再对新根进行向下调整,此时这个堆又是一个大根堆重复上述的过程就是堆排序。
public static void heapSort(int[] array){ createBigHeap(array); int end = array.length-1; while(end > 0){ swap(array,0,end); shiftDown(array,0,end); end--; } } private static void createBigHeap(int[] array){ for (int parent = (array.length-1-1)/2; parent >= 0; parent--) { shiftDown(array,parent,array.length); } } private static void shiftDown(int[] array,int parent,int len){ int child = 2*parent+1; while(child < len){ if(child+1 < len && array[child] < array[child+1]){ child++; } if(array[child] > array[parent]){ swap(array,child,parent); parent = child; child = parent*2+1; }else { break; } } }
3.2 TOPK问题
TOP-K 问题:即求数据结合中前 K 个最大的元素或者最小的元素
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆(和上面排序是一样的)
比如找前k个最大的元素,就要建立小根堆。先把K个元素入堆,建立小根堆,然后遍历数组,如果遍历中的元素比堆顶大就将堆顶元素弹出然后将元素插入进去。
public static int[] smallestK(int[] arr, int k) { int[] ret = new int[k]; if(arr == null || k == 0){ return ret; } PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2.compareTo(o1); } }); for (int i = 0; i < k; i++) { queue.offer(arr[i]); } for (int i = k; i < arr.length; i++) { if(arr[i] < queue.peek()){ queue.poll(); queue.offer(arr[i]); } } for (int i = 0; i < k; i++) { ret[i] = queue.poll(); } return ret; }