【数据结构】堆和优先级队列
创始人
2024-03-13 07:26:03
0

目录

一、堆

1.1堆的特点

1.2如何构造一个最大堆

(1)最大堆的构造以及常用方法的实现

(2)添加操作

 (3)删除操作

(3)将任意数组调整为堆

 二、TopK问题

2.1使用优先级队列

(1)找最大元素

 (2)找最小元素


优先级队列:入队和队列是一样的默认都是队尾入队,出队的时候按照优先级出队,优先级相同的元素按照FIFO出队,优先级高的元素首先出队。

优先级队列的两大特点:

(1)按照优先级出队。

(2)优先级高的元素首先出队。

一、堆

堆是优先级队列背后的数据结构。

我们此处讲得堆其实是二叉堆,基于完全二叉树的二叉堆,广义的堆还有多叉堆(多叉树)。

还有索引堆,普通的对只知道堆顶元素的大小,索引堆可以取得堆中任意位置元素=》在图的最小生成树prim算法和最短路径Diijkstra算法的优化中使用索引堆。

1.1堆的特点

(1)堆首先是完全二叉树,一般使用动态数组(不用存储null结点,元素都靠左排序)存储具体元素。

(2)元素元素的值:

堆中任意一个节点的值<=父节点值(最大堆/大根堆)数据结点>=子树节点。

堆中任意一个节点的值>=父节点值(最小堆/小根堆)数据结点<=子树节点,JDK中的优先级队列默认是最小堆的实现。

在堆中,结点值的大小和节点所处的层次没有明确关系。

关于完全二叉树的节点编号:根据编号以及索引关系,在数据中寻找某一个结点的子节点与父节点编号和索引位置,都从0开始编号,若某个节点的编号(索引)为x。

性质1:判断父节点:

如何判定父结点存在,若存在,索引是多少?parent(x) = (x-1)/2。

当x>0,则一定存在父节点(在二叉树中,只有根节点(根节点索引为0)没有父节点),且父节点的编号为(x-1)/2。

性质2:判断左子树结点

如何判断当前节点x还存在左子树结点?左子树结点编号是多少》

当前完全二叉树的结点个数<=数组最大长度,左子树结点编号为2x+1

性质3:判断右子树结点:

右子树结点编号为2x+2

1.2如何构造一个最大堆

(1)最大堆的构造以及常用方法的实现

创建一个线性表的数组,然后创建一个int类型的size来判断存储了多少个数据。

默认状态数组长度为10,也可以通过有参构造自定义数组的长度。

创建三个判断父节点左右节点坐标的方法以及判空方法和输出方法。

public class MaxHeap {private List arr;private int size;public MaxHeap() {this(10);}public MaxHeap(int capacity) {this.arr = new ArrayList<>(capacity);}private int parent(int k){return (k-1)>>1;}private int leftChild(int k){return (k<<1)+1;}private int rightChild(int k) {return (k << 1) + 2;}public boolean isEmpty() {return size == 0;}public String toString() {return arr.toString();}
}

(2)添加操作

尾插方法很简单直接调用线性表的add方法即可,然后size++,在调用我们的siftUp方法。

任意数组都可以看做一颗完全二叉树,因此向堆中插入一个数据,就相当于在数组中插入一个数据(默认尾插)。

siftUp(上浮)不断比较插入后的元素和其父节点的值大小关系,若当前节点值>父节点值,交换他俩元素顺序,直到这个元素落在了正确位置。结束条件:要么此时k==0,已经走到了树根节点,要么此刻arr[k]<=arr[parent[k]],当前上浮操作结束。

在创建一个swap方法,用来调换两个元素。

    public void add(int val){this.arr.add(val);size++;siftUp(size - 1);}private void siftUp(int k) {while(k>0&&arr.get(k)>arr.get(parent(k))){swap(k,parent(k));k = parent(k);}}private void swap(int i, int j) {int temp = arr.get(i);arr.set(i,arr.get(j));arr.set(j,temp);}
    public static void main(String[] args) {MaxHeap heap = new MaxHeap();heap.add(17);heap.add(11);heap.add(3);heap.add(2);heap.add(5);System.out.println(heap);heap.add(20);System.out.println(heap);}

一开始创建好的堆如下。

​ 然后要向尾部加入一个元素20。

然后调用siftUp方法,先将20与它的父结点值进行比较发现20更大那么就调用swap进行交换。

​然后继续比较父节点的值,发现还是20更大继续交换。

​ 到这时20已经没有父节点了,所以插入操作就结束了。

 (3)删除操作

extractMax():移除当前堆的最大值并返回。

最大堆的最大值在堆顶,删除树根之后,为了对其他子树影响最小,将数组的最后一个元素顶到当前二叉树的堆顶=》这个二叉树仍然是一颗完全二叉树。

需要进行运算的下沉操作siftDown,不断比较当前元素和子节点的最大值的关系,不断交换当前元素和子树的最大值,直到落到正确的位置。也有两个终止条件:1、此时元素已经落到了叶子结点)没有子树了。2、arr[k] >= arr[child[k]],大于左右子树的最大值,落在最终位置。

实际删除的也是数组的最后一个元素。

peekMax():查看当前最大堆的最大值元素。

    public int extractMax(){if (isEmpty()) {throw new NoSuchElementException("heap is empty!cannot extract!");}int k = arr.get(0);arr.set(0,arr.get(size-1));arr.remove(size-1);size--;siftDown(0);return k;}private void siftDown(int k) {while (leftChild(k) arr.get(j)){j = j+1;}if(arr.get(k)>=arr.get(j)){break;}else{swap(k,j);k = j;}}}
    public static void main(String[] args) {MaxHeap heap = new MaxHeap();heap.add(3);heap.add(2);heap.add(5);heap.add(17);heap.add(11);System.out.println(heap);heap.add(20);System.out.println(heap);System.out.println(heap.extractMax());System.out.println(heap);}

一开始插入操作得到的堆如下: 

 然后删除根节点,将最后一个元素补上。

 然后比较根节点3的左右子树获取最大值与根节点进行比较,右子树更大并且比根节点还打所以两个位置交换。

 这时3已经是叶子结点不能再继续比较,所以删除操作结束。

(3)将任意数组调整为堆

因为任意数组都可以看做是一棵完全二叉树,将完全二叉树进行适当的调整就可以得到一个堆。

从最后一个非叶子节点 开始进行元素的siftDown操作,直到根节点。(最后一个叶子节点的父节点就是最后一个非叶子节点)。

空间复杂度为O(n)。

    public MaxHeap(int[] num){this.arr = new ArrayList<>();for(int i:num){arr.add(i);size++;}for (int i = parent(size-1);i>=0;i--){siftDown(i);}}
    public static void main(String[] args) {int[] num = {3,2,17,11,20,5};System.out.println(new MaxHeap(num));}

 二、TopK问题

在一组非常大的数组中,找出前k个最大/最小的元素。

最能想到的方法就是先降序,然后依次取出前50个元素即可。时间复杂度是O(nlogn)。

2.1使用优先级队列

8字口诀:取大用小,取小用大。

这类问题的难点在于如何自定义“大小关系”,巧妙自定义类来定义大小关系,所有的TopK问题都可以使用相同的思路解决。

(1)找最大元素

如果是在100w个数据中心,找出前50个最大的元素。

就建立一个大小为50的最小堆,不断使用最小堆扫描整个数组,扫描完毕之后,最小堆中恰好能保存前50个最大元素。

1、当堆中元素

2、当堆中元素个数>=k时,不断比较当前扫描的元素和堆顶元素的情况。

若当前扫描的元素<=堆顶元素,则这个元素小于当前堆中所有元素,直接跳过,一定不是要找的值。若扫描元素>堆顶元素,将堆顶元素移除,将当前元素添加到堆中。

扫描过程中不断地把最小堆的堆顶元素替换为越来越大的值。

3、当整个集合扫描完毕,堆中一定保存了最大的k各元素。

首先用前k各元素创建一个最小堆。

 然后走到元素9,比对顶元素大,则出队,然后入队。

  然后走到元素7,比对顶元素大,则出队,然后入队。

   然后走到元素6,比对顶元素大,则出队,然后入队。

 再走到元素2,比堆顶元素小则直接不入队,下一个元素5同理。然后走到元素8,比对顶元素大,则出队,然后入队。

 然后最后一个元素10,比对顶元素大,则出队,然后入队。到此优先级队列中的三个元素就是这10个元素里的前3个最大元素。

    public static void main(String[] args) {int[] arr = {1,4,3,9,7,6,2,5,8,10};int k = 3;Queue queue = new PriorityQueue<>();for(int i : arr){if(queue.size()top){queue.poll();queue.offer(i);}}}System.out.println(queue);}

 第二种解法:直接让所有的元素逐渐入队,如果queue长度超过k,就直接poll,就是移除对顶元素,也是扔出最小的元素,仍然留下k个元素。

    public static void main(String[] args) {int[] arr = {1,4,3,9,7,6,2,5,8,10};int k = 3;Queue queue = new PriorityQueue<>();for(int i : arr){queue.offer(i);if(queue.size()>k){queue.poll();}}System.out.println(queue);}

 (2)找最小元素

就需要用到之前讲过的比较器,将JDK中默认的最大堆改为最小堆。

其他的都和最小堆的使用一样。

class IntDesc implements Comparator{@Overridepublic int compare(Integer o1, Integer o2) {return o2-o1;}
}
public class TopKTest {public static void main(String[] args) {int[] arr = {1,4,3,9,7,6,2,5,8,10};int k = 3;Queue queue = new PriorityQueue<>(new IntDesc());for(int i : arr){queue.offer(i);if(queue.size()>k){queue.poll();}}System.out.println(queue);}
}

 更快的方法就是使用快速排序,可以在O(n)的时间复杂度找到k个元素。

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...