优先队列和堆

 

目录

优先队列

定义和基本操作

利用数组定义一个最大堆结构

往堆中添加元素-ShiftUP操作

取出堆中最大元素-ShiftDown(下沉操作)

堆升序排序过程

堆的Replace操作

堆的Heapify操作-将任意数组整理成堆

如何理解向上调整和向下调整过程?

用堆实现优先队列

优先队列的应用


优先队列

  • 普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除
  • 在优先队列中,元素被赋予优先级,当访问元素时具有最高优先级的元素最先出队
  • 优先队列具有最高级先出(fistin,largestout)的行为特征

定义和基本操作

  • 二叉堆是满足一些特殊的二叉树
  • 二叉堆是一颗完全二叉树(叶子节点要么在最后一层且满足左连续,要么在倒数第二层且满足右连续)
  • 堆中任一节点的值总是大于等于其孩子节点值(最大堆:这种优先级可以自己来指定)
  • 实际可以不用使用创建一个节点的方式,可以使用数组的形式来表示二叉堆,下标从0开始时: left(i) = 2 * i + 1, right(i) = 2 * i + 2, parent(i) = Mth.floor((i - 1) / 2)
  • 可以利用堆来进行堆排序,升序利用大根堆,降序利用小根堆。

利用数组定义一个最大堆结构

public class MaxHeap<E extends Comparable<E>> {
    private ArrayList<E> data;
    public  MaxHeap(){
        data = new ArrayList<>();
    }
    public MaxHeap(int capacity){
        data = new ArrayList<>(capacity);
    }

    //返回堆中数组元素的个数
    public int size(){
        return data.size();
    }

    //判断堆中是否为空
    public boolean isEmpty(){
        return data.isEmpty();
    }

    //得到父节点的索引值
    public int parent(int index){
        //表示的已经为堆的根节点了,再没有任何根节点
        if(index <= 0){
            throw  new RuntimeException("index 值不合法");
        }
        return (index - 1) / 2;
    }

    //得到左孩子
    public int leftChild(int index){
        return index * 2 + 1;
    }

    //得到右孩子
    public int rightChild(int index){
        return index * 2 + 2;
    }
}

往堆中添加元素-ShiftUP操作

添加元素(刚开始就相当于添加在了数组的最后一个位置):Shift Up(上浮动,不断和父亲节点比较然后往上调整)往最小堆中添加一个元素无需向下调整(只有向上调整的过程)

//添加节点,刚开始添加在最后一个位置
    public void add(E e){
        data.add(e);
        //然后浮动元素:传递一个位置即可
        shiftUp(data.size() - 1);
    }
    private void shiftUp(int k){
        //注意:两个&&条件的先后次序
        while(k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0){
            //利用集合类给我们提供好的方法
            //编码技巧:对于静态方法,我们可以先导入这个类,然后直接使用swap()方法即可
            Collections.swap(data, k, parent(k));
            k = parent(k);//将返回的结果重新赋值为parent(k),一直下去
        }
    }

 

取出堆中最大元素-ShiftDown(下沉操作)

  • 把堆顶元素和最后一个元素进行交换,删除最后一个元素(也就是返回)
  • 向下调整堆(和两个孩子的最大值进行比较,如果小的话,就需要交换,下沉)
//取出堆中最大元素
    public E extractMax(){
        //找到最大元素(就是根节点的值)
        //交换根和最后一个叶子节点
        //删除交换后的根
        //不断的shiftDown
        E ret = findMax();
        Collections.swap(data, 0,  data.size() - 1);
        data.remove(data.size() - 1);
        shifDown(0);
        return ret;
    }

    public E findMax(){
        if(data.size() == 0){
            throw new RuntimeException("this is a empty heap");
        }
        return data.get(0);
    }
    private void shifDown(int k){
        //当leftChild越界了的话就直接停止下沉,因为rightChild的值会比leftChild的值大
        while(leftChild(k) < data.size()){
            int j = leftChild(k);
            //如果右孩子还没有越界的话,表明还有右孩子并且右孩子的值比左孩子的值要大
            //过滤出左右孩子谁是最大的
            if(j + 1 < data.size() && data.get(j+1).compareTo(data.get(j)) >0){
                j ++;
            }
            //下沉结束,什么都不用再操作了
            if(data.get(k).compareTo(data.get(j)) >= 0){
                break;
            }
            //交换值并且重新赋值k的值
            Collections.swap(data, k ,j);
            k = j;
        }
    }

堆升序排序过程

首先把数组的n个数建成(Heapify操作:见下面内容)一棵大小为的大根堆,n堆顶是整个所有元素中的最大值,把堆顶元素和堆的最后一个位置的元素进行交换,然后把最大值脱离出整个堆结构,放在数组的最后位置,作为数组的有序部分保存下来,接下来把大小为n-1的堆从上往下进行大根堆的调整(向下调整),调整出n-1个数中的最大值放在堆顶的位置,然后再把堆顶的值和整个堆中最后一个位置的值交换,同样作为整个数组的有序部分脱离出整个堆, 堆的大小从n-1变成了n-2,重复上面的过程,不断从堆顶往下调整,每次堆的大小都会减1, 数组的有序部分也会依次增加,当堆的大小变为1的时候, 整个数组就变得有序了。

public class Main {
    //测试流程
    //1. 往堆中不断添加元素
    //2. 从堆中取出元素,放入数组
    //3. 遍历数组中的值,看是否为从大到小排列的值
    public static void main(String[] args) {
        int n = 1000000;
        MaxHeap<Integer> max = new MaxHeap<>();
        Random random = new Random();
        for(int i = 0; i < n; i++){
            max.add(random.nextInt(Integer.MAX_VALUE));//测试上浮操作
        }
        int[] arr = new int[n];
        for(int i = 0; i < n;i++){
            arr[i] = max.extractMax();//测试下沉操作
        }

        //业务判断
        for(int i = 1; i < n; i++){
            if(arr[i] - arr[i-1] > 0){
                throw  new RuntimeException("Error");
            }
        }
        System.out.println("yes");
    }
}

堆的Replace操作

取出堆中的最大元素并且替换成新的元素

  • 实现一∶先取出最大元素再添加元素,即shiftDown和shiftUp,就经历过了两次O(logn)的操作
  • 实现二∶Replace操作:先直接把堆顶元素直接替换了,然后再不断shiftDown,只经过一层O(logn)即可实现
  • //取出堆中地最大元素,并且替换成元素e
        public E repalce(E e){
            E max = findMax();
            data.add(0, e); //替换元素,直接覆盖
            shifDown(0);
            return max;
        }

     

堆的Heapify操作-将任意数组整理成堆

(1) 实现一∶扫描一遍数组,将扫描出来的数组元素添加到堆中O(nlogn):因为扫描一遍数组,需要O(n)的时间,add的操作需要O(logn)的时间,总共时间为O(nlogn)

(2) 实现二∶Heapity操作O(n):将数组看成一棵完全二叉树,然后从最后一个非叶子节点(在数组中表示就是最后一个叶子节点的父节点)开始倒着shiftDown;也就是把一个无序的完全二叉堆调整为二叉堆,本质上就是让所有非叶子节点下沉。 具体过程如下所示:

原始数组

0123456789
15171913221628304162

调整过程

调整之后的结果 

 

实现代码

public MaxHeap(ArrayList<E> arrayList){
        //找到最后一个非叶子节点(最后一个节点的父节点)
        int index = parent((arrayList.size() - 1));
        for(int i = index; i >= 0; i--){
            shifDown(i);
        }
        data = arrayList;//重现将arrayList指向给data
    }

如何理解向上调整和向下调整过程?

往堆中插入元素后,利用向上调整

将无序数据构建成堆时(heapify), 利用向下调整操作。

排序时,将根节点和最后一个节点交换后,也是利用向下调整操作。

用堆实现优先队列

(1) Queue.java

public interface Queue<E>{
    void enqueue(E e);
    E dequeue();
    E getFront();
    int getSize();
    boolean isEmpty();
}

(2) MaxHeap.java

package heap;

import java.util.ArrayList;
import java.util.Collections;

public class MaxHeap<E extends Comparable<E>> {
    private ArrayList<E> data;
    public  MaxHeap(){
        data = new ArrayList<>();
    }
    public MaxHeap(int capacity){
        data = new ArrayList<>(capacity);
    }

    //返回堆中数组元素的个数
    public int size(){
        return data.size();
    }

    //判断堆中是否为空
    public boolean isEmpty(){
        return data.isEmpty();
    }

    //得到父节点的索引值
    public int parent(int index){
        //表示的已经为堆的根节点了,再没有任何根节点
        if(index <= 0){
            throw  new RuntimeException("index 值不合法");
        }
        return (index - 1) / 2;
    }

    //得到左孩子
    public int leftChild(int index){
        return index * 2 + 1;
    }

    //得到右孩子
    public int rightChild(int index){
        return index * 2 + 2;
    }


    //添加节点,刚开始添加在最后一个位置
    public void add(E e){
        data.add(e);
        //然后浮动元素:传递一个位置即可
        shiftUp(data.size() - 1);
    }
    private void shiftUp(int k){
        //注意:两个&&条件的先后次序
        while(k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0){
            //利用集合类给我们提供好的方法
            //编码技巧:对于静态方法,我们可以先导入这个类,然后直接使用swap()方法即可
            Collections.swap(data, k, parent(k));
            k = parent(k);//将返回的结果重新赋值为parent(k),一直下去
        }
    }
    //取出堆中最大元素
    public E extractMax(){
        //找到最大元素(就是根节点的值)
        //交换根和最后一个叶子节点
        //删除交换后的根
        //不断的shiftDown
        E ret = findMax();
        Collections.swap(data, 0,  data.size() - 1);
        data.remove(data.size() - 1);
        shifDown(0);
        return ret;
    }

    public E findMax(){
        if(data.size() == 0){
            throw new RuntimeException("this is a empty heap");
        }
        return data.get(0);
    }
    private void shifDown(int k){
        //当leftChild越界了的话就直接停止下沉,因为rightChild的值会比leftChild的值大
        while(leftChild(k) < data.size()){
            int j = leftChild(k);
            //如果右孩子还没有越界的话,表明还有右孩子并且右孩子的值比左孩子的值要大
            //过滤出左右孩子谁是最大的
            if(j + 1 < data.size() && data.get(j+1).compareTo(data.get(j)) >0){
                j ++;
            }
            //下沉结束,什么都不用再操作了
            if(data.get(k).compareTo(data.get(j)) >= 0){
                break;
            }
            //交换值并且重新赋值k的值
            Collections.swap(data, k ,j);
            k = j;
        }
    }
}

 (3) PriorityQueue.java

package heap;

import java.util.Collections;

public class PriorityQueue <E extends Comparable<E>> implements  Queue<E>{
    private MaxHeap<E> maxHeap;

    public PriorityQueue(){
        maxHeap = new MaxHeap<>();
    }

    public PriorityQueue(MaxHeap maxHeap){
        this.maxHeap = maxHeap;
    }
    @Override
    public void enqueue(E e) {
        maxHeap.add(e);
    }

    @Override
    public E dequeue() {
        return maxHeap.extractMax();
    }

    @Override
    public E getFront() {
        return maxHeap.findMax();
    }

    @Override
    public int getSize() {
        return maxHeap.size();
    }

    @Override
    public boolean isEmpty() {
        return maxHeap.isEmpty();
    }

}

优先队列的应用

(1)在n个元素中选出前m名

  • 如果m=1,那么直接遍历一遍即可,时间复杂度就是O(n)
  • n个元素排序,再选出前m个元素也可以完成,时间复杂度是O(nlogn)
  • 如果使用优先队里,在O(nlogm)的时间复杂度中即可以完成该操作:利用优先队列维护队列中看到的前n个元素,如果扫到的元素比维护的队列中的m个元素的最小值还要大的话,就进行替换操作。(因此我们需要使用最小堆

(2) 给定一个非空的整数数组,返回其中出现频率前k高的元素

  • 输入:nums = [1,1,1,2,2,3], k = 2
  • 输出: [1,2]
import java.util.*;

public class Solution {
    //这里自己自定义一个对象:数字和出现的频率
    private static class Freq  implements  Comparable<Freq>{
        int e;
        int freq;

        public Freq(int e, int fre) {
            this.e = e;
            this.freq = fre;
        }
        //因为是最小堆
        @Override
        public int compareTo(Freq o) {
            return o.freq - this.freq;
        }
    }
    public static void main(String[] args) {
        int[] nums = {1,1,1,2,2,3};
        int k = 2;
        topKEle(nums, k);
    }
    public static void topKEle(int[] nums, int k){
        //1,统计数组中每个元素出现的频率
        //2.将元素和出现的频率加工成一个对象放入到优先队列中
        Map<Integer, Integer> map = new HashMap<>();
        for(int num : nums){
            if(map.containsKey(num)){
                map.put(num, map.get(num) + 1);
            }
            else{
                map.put(num, 1);
            }
        }
        Queue<Freq> pq = new PriorityQueue<Freq>();
        for(int key: map.keySet()){
            if(pq.size() < k){
                pq.offer(new Freq(key, map.get(key)));
            }
            else if(map.get(key) > pq.peek().freq){
                pq.poll();
                pq.offer(new Freq(key, map.get(key)));
            }
        }

        //输出操作
        List<Integer> list = new ArrayList<>();
        while(!pq.isEmpty()){
            list.add(pq.poll().e);
        }
        System.out.println(list);
    }
}

Java默认提供的就是最小堆

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值