堆的概念
按照优先级的大小动态出队(动态指的是元素个数的动态变化,而非固定),对比普通的队列是先入先出
现实中的堆
- 1操作系统中的任务调度,(一般内核任务的优先级是高于普通应用的)
- 2比如医院中,医生要按照病人的病情来动态安排手术,比如今天有三个病人,已经安排好明天手术的顺序,突然来了一个病重的病人,那么医生就得先给这个病人做手术
优先级队列和普通队列的时间复杂度比较
入队时间复杂度 出队(最大值)时间复杂度 普通队列 O(1) O(n) 优先级队列 O(logn( O(logn)
- 在计算机领域,遇到logn级别的时间复杂度,几乎都是跟树有关(不一定非要构造树结构,但是在逻辑上都运用了树的思想)
堆的实现
基于二叉树的堆叫做二叉堆,也有d叉堆和索引堆
二叉堆的特点
二叉树的存储
是一颗完全二叉树,基于数组存储(因为完全二叉树的元素都是靠左排列,数组存储的时候不会浪费空间,其他二叉树的存储需要用链式结构,因为用数组会导致大量浪费)
两种特别的堆
- 如果堆中根节点>=子树节点中的值——>最大堆,大根堆(所有的子树也满足这个要求)
- 如果堆中根节点<=子树节点中的值——>最小堆,小根堆(所有的子树也满足这个要求)
- 这两个堆可以完成TopK问题,如果求前k个最大值,就用最小堆,如果求前k个最小值,就用最大堆
节点的层次和节点的大小没有任何关系,只能保证在当前树中,树根是最大值
堆的编号问题
因为堆是基于数组存储的,所以节点之间的关系要通过数组下标来表示,且从0开始编号
假设子节点为i
- 则父节点为(i-1)/2
- 左子树的编号为2*i+1
- 右子树的编号为2*i+2
基于动态数组实现的最大堆
因为JDK中实现的是最小堆,所以我们来实现一下最大堆
数据结构
public class MyHeap { private List<Integer> element;//用于存储堆的内容 private int size=0;//用于记录堆中元素个数 }
两大核心操作
上浮操作
- 比如我们要给这个最大堆加入一个元素,因为是基于数组实现的,一个元素的添加应该在size-1的位置,但是这个新加的元素可能会破坏这个最大堆的结构,比如我们添加了49,那么49比它的父亲节点33大,这是不符合最大堆的规定,所以我们引入上浮操作,
- 一直将这个元素与父节点比较,如果小于父节点的值,就进行交换,直到它成为根节点,也就是这个堆中最大的数或者是当它的父节点大于这个值(符合最大堆的规定了)
下沉操作
- 如果我们要取出这个最大堆的最大值,那么就是取出其根节点,但是这是基于数组实现的堆,我们要删除数组的第一个元素,代价比较大,因为会移动后面所有的元素,我们可以将根节点的值跟size-1的值进行交换,然后删除size-1的值,这样消耗比较小
- 但是这样我们删除了一个元素,其堆的结构不满足最大堆的结构,所以我们引入了下沉操作,对根节点进行下沉操作
- 每次将父节点与其左右子树的值进行比较,如果比其左右子树中最大的值小,就与之进行交换,直到这个节点不存在左右子树或者其左右子树的值都比父节点大
public int pollMax(){ //将最大元素出队 就是将根节点出队 if (isEmpty()){ throw new NoSuchElementException("heap is empty"); } int max=element.get(0); element.set(0,element.get(size-1)); element.remove(size-1); size--; siftdowm(0); return max; } private void siftdowm(int k) { while (leftchild(k)<size){ //说明还是存储子树的 int j=leftchild(k); if (j+1<size&&element.get(j+1)>element.get(j)){ j=j+1; //现在j表示的就是左右子树中值最大那个值 } if (element.get(k)>=element.get(j)){ break; //当前元素的值大于左右子树的值,下沉结束 }else { swap(k,j); k=j; } } } private void swap(int k, int parent) { int child=element.get(k); int par=element.get(parent); element.set(parent,child); element.set(k,par); }
最大堆的两种建立
利用add方法一直添加元素
public void add(int val){ element.add(val);//在size的地方添加元素 size++; siftup(size-1);//对新加入的元素进行上浮操作 } private void siftup(int k) { while (k>0&&element.get(k)>element.get(parent(k))){ swap(k,parent(k)); k=parent(k); } } private void swap(int k, int parent) { int child=element.get(k); int par=element.get(parent); element.set(parent,child); element.set(k,par); }
原地堆化 heapify(把一个任意数组变成最大堆)
public MyHeap(int[] arr){ element=new ArrayList<>(arr.length); for (int i : arr) { element.add(i); size++; }//将所有元素放入存储数据的动态数组中 for (int i = parent(size-1); i >=0 ; i--) { siftdowm(i); }//从第一个非叶子节点进行siftdown操作 } private void siftdowm(int k) { while (leftchild(k)<size){ //说明还是存储子树的 int j=leftchild(k); if (j+1<size&&element.get(j+1)>element.get(j)){ j=j+1; //现在j表示的就是左右子树中值最大那个值 } if (element.get(k)>=element.get(j)){ break; //当前元素的值大于左右子树的值,下沉结束 }else { swap(k,j); k=j; } } }