数据结构|【数据结构】优先级队列 - 堆


文章目录

  • 1.优先级队列
    • 1.1概念
  • 2.堆的模拟实现
    • 2.1 堆的概念
    • 2.2 堆的存储方式
    • 2.3 堆的创建
      • 2.3.1 堆的向下调整(shiftDown)
      • 2.3.2 堆的创建
      • 2.3.3 建堆的时间复杂度
    • 2.4堆的插入和删除
      • 2.4.1 堆的插入
      • 2.4.3 堆的删除
  • 3.Java中的优先级队列
    • 3.1 PriorityQueue
    • 3.2 PriorityQueue常用接口介绍
      • 1. 构造方法
      • 2.常用方法
      • 3. 扩容方式
  • 4. 堆的应用
    • 4.1 堆排序
    • 4.2 Top-k问题

1.优先级队列 1.1概念 之前我们学习过队列是一种先进先出的数据结构,但在某些场景下,操作的数据具有优先级,在出队列时,需要优先级高的数据先出队列。比如在打游戏时来电话了,那么操作系统应该先处理电话;还有在游戏英雄联盟中泰坦的Q的优先级比机器人高;
诸如这些情况下,我们需要数据结构提供两种最基本的操作,首先添加新的对象,然后返回最高优先级对象,这种数据结构就是优先级队列(Priority Queue).
2.堆的模拟实现 在JDK1.8中的PriorityQueue底层使用了堆的数据结构,而堆通常是一个可以被看做一棵完全二叉树的数组对象。
2.1 堆的概念 如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆
堆满足以下性质:
  • 堆中某个节点的值总是不大于或不小于其父节点的值。
  • 堆总是一棵完全二叉树。
    数据结构|【数据结构】优先级队列 - 堆
    文章图片
2.2 堆的存储方式 因为堆是一颗完全二叉树,所以我们可以利用层序遍历的方式来存储元素
数据结构|【数据结构】优先级队列 - 堆
文章图片

注:对应非完全二叉树则不适用于顺序方式来存储,因为为了通过顺序表来还原二叉树,则空间中就需要存储空结点,这样会导致空间利用率低。
当元素存储到数组中后,我们如何还原二叉树呢?假设i为节点在数组中的下标,
  • 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
  • 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
  • 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
用代码来表示就是:
parent = i / 2; leftChild = 2 * i; rightChild = 2 * i + 1; //或者已知父节点为root parent = root; leftChild = (2 * parent) + 1; rightChild = (2 * parent)+ 1;

2.3 堆的创建 2.3.1 堆的向下调整(shiftDown)
对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如何将其创建成堆呢?
数据结构|【数据结构】优先级队列 - 堆
文章图片

通过观察可知:根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可。
建小堆为例:
  1. 让parent标记需要调整的节点,child标记parent的左孩子(注意:parent如果有孩子一定先是有左孩子)
  2. 如果parent的左孩子存在,即:child < len, 进行以下操作,直到parent的左孩子不存在或者已经满足堆的性质。
    2.1 parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标记
    2.2 将parent与较小的孩子child比较,如果parent小于较小的孩子child,调整结束;否则:交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后重复2 。
    数据结构|【数据结构】优先级队列 - 堆
    文章图片
/** * 向下调整 - * 时间复杂度 0(logN) * @param arr 二叉树中存放的元素 * @param root 根节点的下标 * @param len 数组长度 */ private static void shiftDown(int[] arr, int root, int len) { int parent = root; int child = (2 * parent) + 1; //左孩子 //孩子节点索引不会大于len while (child < len) { //使child索引位置保存子节点的最大值 if (child + 1< len && arr[child] < arr[child + 1]) { child++; } if (arr[child] > arr[parent]) { swap(arr, child, parent); parent = child; child = (2 * parent) + 1; } else { break; } } }private static void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }

2.3.2 堆的创建
对于一个普通序列,即根的左右子树都不满足堆的特性,那么该如何调整呢?
数据结构|【数据结构】优先级队列 - 堆
文章图片

/** * 建堆 * 时间复杂度 0(n) * @param arr */ private static void createHeap(int[] arr) { // 找倒数第一个非叶子节点,从该节点位置开始往前一直到根节点,遇到一个节点,应用向下调整 for (int p = (arr.length - 1 - 1) / 2; p >=0; p--) { shiftDown(arr, p, arr.length); }}

2.3.3 建堆的时间复杂度
堆是完全二叉树,这里我们简化用满二叉树来证明
数据结构|【数据结构】优先级队列 - 堆
文章图片

所以,建堆的时间复杂度为0(N)
2.4堆的插入和删除 2.4.1 堆的插入
插入步骤:
  1. 先把元素放到堆最后(即数组最后),如果空间不足,则需要扩容。
  2. 将插入的元素向上调整,直到满足堆的性质
    数据结构|【数据结构】优先级队列 - 堆
    文章图片
/** * 向堆中插入元素 * @param val */ public void push(int val) { //判满 if (ifFull()) { this.elem = Arrays.copyOf(elem, elem.length * 2); } //插入 this.elem[usedSize] = val; //调整 shiftUp(usedSize); //有效数据调整 usedSize++; }private boolean ifFull() { return elem.length == usedSize; }/** * 向上调整 - 向堆中添加元素 - 只能添加到末尾然后向上调整 */ privatevoid shiftUp(int child) { int parent = (child - 1) / 2; while (child > 0) { //这里本来就是小根堆,根节点的值一定比原来的左右子结点小,所以不需要比较左右节点的大小 if (elem[child] > elem[parent]) { swap(elem, child, parent); child = parent; parent = (child - 1) / 2; } else { break; } } }

2.4.3 堆的删除
删除步骤:
注:堆的删除一定是删除优先级高的元素(即堆顶元素)
  1. 将堆顶元素和堆中最后的元素交换
  2. 有效数据减一
  3. 从堆顶元素开始,向下调整
    数据结构|【数据结构】优先级队列 - 堆
    文章图片
/** * 出堆顶元素 */ public void pollHeap() { if (isEmpty()) { System.out.println("队列为空"); return; } //把堆顶元素和堆尾元素交换 swap(elem, 0, usedSize - 1); //调整大小 usedSize--; //堆的结构改变,所以要向下调整 shiftDown(elem, 0, usedSize); } /** * 向下调整 - * 时间复杂度 0(logN) * @param arr 二叉树中存放的元素 * @param root 根节点的下标 * @param len 数组长度 */ private static void shiftDown(int[] arr, int root, int len) { int parent = root; int child = (2 * parent) + 1; //左孩子 //孩子节点索引不会大于len while (child < len) { //使child索引位置保存子节点的最大值 if (child + 1< len && arr[child] < arr[child + 1]) { child++; } if (arr[child] > arr[parent]) { swap(arr, child, parent); parent = child; child = (2 * parent) + 1; } else { break; } } }

3.Java中的优先级队列 3.1 PriorityQueue Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,我们主要介绍PriorityQueue。
使用注意事项:
  1. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常
  2. 不能插入null对象,否则会抛出NullPointerException
  3. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
  4. 插入和删除元素的时间复杂度为0(logN)
  5. PriorityQueue底层使用了堆数据结构, 且是最小堆
3.2 PriorityQueue常用接口介绍 1. 构造方法
它的构造方法在JDK1.8中一共有7种,下面我们介绍几种常见的构造方法
构造器 功能介绍
PriorityQueue() 创建一个空的优先级队列,默认容量是11
PriorityQueue(int initialCapacity) 创建一个初始容量为initialCapacity的优先级队列,注意:initialCapacity不能小于1,否则会抛IllegalArgumentException异常
PriorityQueue(Collection c) 用一个集合来创建优先级队列
PriorityQueue(Comparator comparator) 设置自己的比较器
public static void main(String[] args) { // 创建一个空的优先级队列,底层默认容量是11 PriorityQueue q1 = new PriorityQueue<>(); // 创建一个空的优先级队列,底层的容量为initialCapacity PriorityQueue q2 = new PriorityQueue<>(100); ArrayList list = new ArrayList<>(); list.add(4); list.add(3); list.add(2); list.add(1); // 用ArrayList对象来构造一个优先级队列的对象// q3中已经包含了三个元素 PriorityQueue q3 = new PriorityQueue<>(list); System.out.println(q3.size()); System.out.println(q3.peek()); //自定义比较器 PriorityQueue p4 = new PriorityQueue<>(new Comparator() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); }

注:在默认情况下,PriorityQueue队列是小堆,如果需要大堆需要用户自定义比较器
  1. 自定义比较器
class IntCmp implements Comparator { @Override public int compare(Integer o1, Integer o2) { return o1 - o2; } }

  1. 使用匿名内部类
PriorityQueue priorityQueue = new PriorityQueue<>(new Comparator() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } });

  1. 自定义类中实现Comparable接口
class Student implements Comparable { public int age; public Student(int age) { this.age = age; }@Override public int compareTo(Student o) { return o.age - this.age; }@Override public String toString() { return "Student{" + "age=" + age + '}'; } }

2.常用方法
函数名 功能介绍
boolean offer(E e) 插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度0(logN) ,注意:空间不够时候会进行扩容
E peek() 获取优先级最高的元素,如果优先级队列为空,返回null
E poll() 移除优先级最高的元素并返回,如果优先级队列为空,返回null
int size() 获取有效元素的个数
void clear() 清空队列
boolean isEmpty() 检测优先级队列是否为空,空返回true
3. 扩容方式
JDK1.8 中PriorityQueue的扩容方式及其源码
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; private void grow(int minCapacity) { int oldCapacity = queue.length; // Double size if small; else grow by 50% int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1)); // overflow-conscious code if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); queue = Arrays.copyOf(queue, newCapacity); }private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }

【数据结构|【数据结构】优先级队列 - 堆】扩容介绍:
  • 如果容量小于64时,是按照oldCapacity的2倍方式扩容的
  • 如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
  • 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容
4. 堆的应用 4.1 堆排序 堆排序即利用堆的思想来进行排序,总共分为两个步骤
  1. 建堆
  • 升序:建大堆
  • 降序:建小堆
  1. 利用堆删除思想来进行排序
    数据结构|【数据结构】优先级队列 - 堆
    文章图片
/** * 时间复杂度:0(N*logN) * 空间复杂度:0(1) * 稳定性:不稳定 * @param arr */ public static void heapSort(int[] arr) { //建堆 createHeap(arr); int end = arr.length - 1; while (end >= 0) { swap(arr, 0, end); shiftDown(arr, 0, end); end--; } }/** * 向下调整 - * 时间复杂度 0(logN) * @param arr 二叉树中存放的元素 * @param root 根节点的下标 * @param len 数组长度 */ private static void shiftDown(int[] arr, int root, int len) { int parent = root; int child = (2 * parent) + 1; //左孩子 //孩子节点索引不会大于len while (child < len) { //使child索引位置保存子节点的最大值 if (child + 1< len && arr[child] < arr[child + 1]) { child++; } if (arr[child] > arr[parent]) { swap(arr, child, parent); parent = child; child = (2 * parent) + 1; } else { break; } } }/** * 建堆 * 时间复杂度 0(n) * @param arr */ private static void createHeap(int[] arr) { for (int p = (arr.length - 1 - 1) / 2; p >=0; p--) { shiftDown(arr, p, arr.length); }} private static void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }

4.2 Top-k问题 TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大,所以对于一般排序是不可取的,最佳的方式是堆排序,基本思想如下:
  1. 用数据集合中前K个元素来建堆
    1.1 前k个最大的元素,则建小堆
    1.2 前k个最小的元素,则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素

    推荐阅读