7.堆和优先队列

一. 什么是优先队列

  • 特点:

 普通队列: 先进先出;后进后出
 优先队列: 出队顺序和入队顺序无关; 和优先级相关
    - 最典型应用: 操作系统会动态的选择优先级最高的任务去执行 
  • 接口设计与之前的普通队列保持一致


  • 不同底层实现优先队列的时间复杂度比较



二. 堆的基础表示

  • 使用二叉树实现堆 称为 二叉堆
  • 二叉堆是一棵完全二叉树

image

二叉堆的性质:

  • 最大堆: 堆中某个节点的值总是不大于其父节点
  • 最小堆: 堆中某个节点的值总是不小于其父节点

image

用数组存储二叉堆

特性:(数组索引为0 的位置空出来)

假设一个节点对应在数组中的下标为 i, 
    父节点: i/2
    左孩子: 2*i
    右孩子: 2*i+1


如果数组索引为0 的位置不空出来,则

    父节点: (i-1)/2
    左孩子: 2*i+1
    右孩子: 2*i+2

下面都默认 都是 索引为0的位置 不空出来

代码架构实现

新建项目MaxHeap:

.
├── MaxHeap.iml
└── src
    ├── Array.java
    ├── Main.java
    └── MaxHeap.java

Array.java即为以前实现过的 动态数组
MaxHeap.java

public class MaxHeap<E extends Comparable<E>> {
    private Array<E>  data;

    public MaxHeap(int capacity){
        data = new Array<>(capacity);
    }

    public MaxHeap(){
        data = new Array<>();
    }

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

    // 返回一个布尔值   表示堆中是否为空
    public boolean isEmpty(){
        return data.isEmpty();
    }

    // 返回完全二叉树的数组表示中, 一个索引所表示的元素的父亲节点的索引
    private int parent(int index){
        if(index == 0){
            throw new IllegalArgumentException("index-0 does't have parent");
        }
        return (index-1) / 2;
    }

    // 返回完全二叉树的数组表示中, 一个索引所表示的元素的左孩子节点的索引
    private int leftChild(int index){
        return index * 2 + 1;
    }

    // 返回完全二叉树的数组表示中, 一个索引所表示的元素的右孩子节点的索引
    private int rightChild(int index){
        return index * 2 + 2;
    }
}


三. 向堆中添加元素和Sift Up

流程:

 步骤一: 在数组末尾添加入元素 a
 步骤二:Sift Up
    while(a > a的父亲节点):
        aa的父亲节点互换位置

代码实现

MaxHeap.java

public class MaxHeap<E extends Comparable<E>> {
    ...

    // 向堆中添加元素
    public void add(E e){
        data.addLast(e);
        siftUp(data.getSize() - 1);
    }

    private void siftUp(int k){
        while(k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0){
            data.swap(k, parent(k));
            k = parent(k);
        }
    }
}

为动态数组添加 数据交换方法 swap
Array.java

public class Array<E> {
    ...
    
    public void swap(int i, int j){
        if(i < 0 || i>= size || j < 0 || j >= size){
            throw new IllegalArgumentException("Index is illegal");
        }

         E t = data[i];
        data[i] = data[j];
        data[j] = data[i];
    }
    
    ...
}

四. 从堆中取出元素和Sift Down

流程:

假设取出的元素为a
步骤一: 取出a 将数组中最后一个元素b 放到原先a的位置
步骤二: Sift Down
    while(存在b的子元素 > b):
        b与较大的那个子元素互换位置

代码实现

MaxHeap.java

public class MaxHeap<E extends Comparable<E>> {

    ...
    
    // 看堆中最大的元素
    public E findMax(){
        if(data.getSize() == 0){
            throw new IllegalArgumentException("Can not finMax when heap is empty");
        }
        return data.get(0);
    }
    // 取出堆中最大元素
    public E extracMax(){
        E ret = findMax();

        data.swap(0, data.getSize()-1);
        data.removeLast();
        siftDown(0);

        return ret;
    }

    private void siftDown(int k){
        while(leftChild(k) < data.getSize()){
            int j = leftChild(k);
            if(j+1 < data.getSize() && data.get(j+1).compareTo(data.get(j)) > 0){
                j++;// j = rightChild(k);  此时data[j] 两孩子中的最大值
            }

            if(data.get(k).compareTo(data.get(j)) >= 0)
                break;
            data.swap(k, j);
            k = j;
        }
    }
}

测试:
Main.java

import java.util.Random;

public class Main {

    public static void main(String[] args) {
	// write your code here
        int n = 1000000;

        MaxHeap<Integer> maxHeap = new MaxHeap<>();
        Random random = new Random();
        for(int i = 0; i < n; i++){
            maxHeap.add(random.nextInt(Integer.MAX_VALUE));
        }

        int[] arr = new int[n];
        for(int i = 0; i < n; i++){
            arr[i] = maxHeap.extracMax();
        }

        for(int i = 1; i < n; i++ ){
            if(arr[i-1] < arr[i]){
                throw new IllegalArgumentException("Error");
            }
        }

        System.out.println("Test MaxHeap completed");
    }
}

结果:Test MaxHeap completed


五. Heapify 和 Replace

replace

  • 取出最大元素后, 放入一个新的元素
  • 实现: 可以直接将堆顶的元素替换, 然后进行Sift Down, 一次O(logn)的操作

MaxHeap.java

...

    // 取出堆中最大值, 并且替换成元素e
    public E replace(E e){
        E ret = findMax();

        data.set(0, e);
        siftDown(0);
        return ret;
    }
}

heapify

  • 将任意的数组整理成堆的形状
  • 实现流程:
 数组最后一个元素的父节点索引为 k  (索引的计算 (最后一个节点的索引-1/2)
    while(k >= 0):
        siftdown k
        k--
  • 算法复杂度
- 方法一: n个元素逐个插入到一个空堆中, 算法复杂度是O(nlogn)
- 方法二: heapify的过程, 算法复杂度为O(n)

MaxHeap.java中加入一种新的构造方法

...

    public MaxHeap(E[] arr){
        data = new Array<>(arr);
        for(int i = parent(arr.length-1); i >=0; i--){
            siftDown(i);
        }
    }
...

Array.java

...
    public Array(E[] arr){
        data = (E[])new Object[arr.length];
        for(int i = 0; i < arr.length; i++){
            data[i] = arr[i];
        }
        size = arr.length;
    }
...

测试:
Main.java

import java.util.Random;

public class Main {



    private static double testHeap(Integer[] testData, boolean isHeapify){
        long startTime = System.nanoTime();

        MaxHeap<Integer> maxHeap;

        // 两种创建方法
        if(isHeapify){
            maxHeap = new MaxHeap<>(testData);
        }
        else{
            maxHeap = new MaxHeap<>();
            for(int num: testData){
                maxHeap.add(num);
            }
        }

        int[] arr = new int[testData.length];
        for(int i = 0; i < testData.length; i++){
            arr[i] = maxHeap.extracMax();
        }
        for(int i = 1; i < testData.length; i++){
            if(arr[i-1] < arr[i]){
                throw new IllegalArgumentException("Error");
            }
        }
        System.out.println("Test MaxHeap completed");
        long endTime = System.nanoTime();

        return (endTime-startTime)/1000000000.0;
    }

    public static void main(String[] args) {
        int n = 1000000;

        Random random = new Random();
        Integer[] testData = new Integer[n];
        for(int i = 0; i < n; i++){
            testData[i] = random.nextInt(Integer.MAX_VALUE);
        }

        double time1 = testHeap(testData, false);
        System.out.println("without heapify: "+time1+" s");

        double time2 = testHeap(testData, true);
        System.out.println("with heapify: "+time2+" s");

    }
}

结果:

Test MaxHeap completed
without heapify: 0.319265444 s
Test MaxHeap completed
with heapify: 0.20685298 s

由此可见Heapify能有效提升效率


六. 基于堆的优先队列

之前的代码,已经实现了最大堆, 我们在这些代码的基础上实现优先队列。
接口Queue.java

public interface Queue<E> {

    int getSize();

    boolean isEmpty();

    void enqueue(E e);

    E dequeue();

    E getFront();
}

优先队列的实现PriorityQueue.java

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

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

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

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

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

    @Override
    public void enqueue(E e) {
        maxHeap.add(e);
    }

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


七.Leetcode上优先队列相关问题

  • 优先队列的经典问题

1000000个元素中选出前100名?
即在N个元素中选出前M个元素

解题思路:

1.使用优先队列, 维护当前看到的前M个元素。遇到更小的就放入队列,并踢掉最大的。

  • Leetcode347题

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

例如,

给定数组 [1,1,1,2,2,3] ,  k = 2,返回 [1,2]

注意:

你可以假设给定的 k 总是合理的,1  k  数组中不相同的元素的个数。
你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。

用我们自己设计的PriorityQueue来解答

思路:
1. 使用映射map 存放给定数组nums中的元素及其出现频次
2. 一个长度为k的优先队列, 放入map中的元素(key,value),频次低的元素优先出队
3. 我们自定义的PriorityQueue,是越''优先级越高, 而我们可以自定义Comparable来定义''(频次越小越大)

Solution.java

import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;


class Solution {

    private class Freq implements Comparable<Freq>{
        int e, freq;

        public Freq(int e, int freq){
            this.e = e;
            this.freq = freq;
        }

        @Override
        public int compareTo(Freq another){   // 元素越小,优先级越高    这样在优先队列中,频率最低的先出, 最后留下的都是频率最高的几个
            if(this.freq < another.freq){
                return 1;
            }
            else if (this.freq > another.freq){
                return -1;
            }
            else
                return 0;
        }
    }

    public List<Integer> topKFrequent(int[] nums, int k) {
        TreeMap<Integer, Integer> map = new TreeMap<>();
        for(int num: nums){
            if(map.containsKey(num)){
                map.put(num, map.get(num)+1);
            }
            else{
                map.put(num, 1);
            }
        }

        PriorityQueue<Freq> pq = new PriorityQueue<Freq>();
        for(int key: map.keySet()){
            if(pq.getSize() < k){
                pq.enqueue(new Freq(key, map.get(key)));
            }
            else if(map.get(key) > pq.getFront().freq){ // 频率高于 优先队列中频率最低的那个, 入队
                pq.dequeue();
                pq.enqueue(new Freq, map.get(key));
            }
        }
        LinkedList<Integer> res = new LinkedList<>();
        while(!pq.isEmpty()){
            res.add(pq.dequeue().e);
        }
        return res;
    }
}

八. Java中的PriorityQueue

1.Java中的PriorityQueue是 最小优先队列

用Java自带的PriorityQueue, 改写347号问题:
Solution.java

import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;
import java.util.PriorityQueue;


class Solution {

    private class Freq implements Comparable<Freq>{
        int e, freq;

        public Freq(int e, int freq){
            this.e = e;
            this.freq = freq;
        }

        @Override
        public int compareTo(Freq another){   //PriorityQueue是最小优先队列, 所以反过来, 元素越大优先级越高。优先级小的会留在队中
            if(this.freq < another.freq){
                return -1;
            }
            else if (this.freq > another.freq){
                return 1;
            }
            else
                return 0;
        }
    }

    public List<Integer> topKFrequent(int[] nums, int k) {
        TreeMap<Integer, Integer> map = new TreeMap<>();
        for(int num: nums) {
            if (map.containsKey(num)) {
                map.put(num, map.get(num) + 1);
            } else {
                map.put(num, 1);
            }
        }
        PriorityQueue<Freq> pq = new PriorityQueue<Freq>();
        for(int key: map.keySet()){
            if(pq.size() < k){   // getsize 改为size
                pq.add(new Freq(key, map.get(key)));  // enqueue 改为 add
            }
            else if(map.get(key) > pq.peek().freq){  // getFront 改为peek
                pq.remove();                               // dequeue改为rempve
                pq.add(new Freq(key, map.get(key)));
            }
        }
        LinkedList<Integer> res = new LinkedList<>();
        while(!pq.isEmpty()){
            res.add(pq.remove().e);
        }
        return res;

    }
}

2. PriorityQueue传入比较器

之前我们通过改写compareTo来 自定义比较方法。 我们还有更好的方法:
Solution.java

import ..
import java.util.Comparator;


    // 之前的自定义比较的方法
//      private class Freq implements Comparable<Freq>{
//        int e, freq;
//
//        public Freq(int e, int freq){
//            this.e = e;
//            this.freq = freq;
//        }
//
//        @Override
//        public int compareTo(Freq another){   //PriorityQueue是最小优先队列, 所以反过来, 元素越大优先级越高。优先级小的会留在队中
//            if(this.freq < another.freq){
//                return -1;
//            }
//            else if (this.freq > another.freq){
//                return 1;
//            }
//            else
//                return 0;
//        }
//    }

    private class Freq {
        int e, freq;
        
        public Freq(int e, int freq) {
            this.e = e;
            this.freq = freq;
        }
    }
    

       
    private class FreqComparator implements Comparator<Freq>{
        @Override
        public int compare(Freq a, Freq b){
            return a.freq - b.freq;
        }
    }

    ...
    // 新的自定义比较器方法, 需要出入PriorityQueue
    // JavaPriorityQueue可以传入比较器
    PriorityQueue<Freq> pq = new PriorityQueue<Freq>(new FreqComparator());
    ...

3. 代码进一步优化

Solution.java

import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;
import java.util.PriorityQueue;
import java.util.Comparator;


class Solution {
    private class Freq {
        int e, freq;

        public Freq(int e, int freq) {
            this.e = e;
            this.freq = freq;
        }
    }


    public List<Integer> topKFrequent(int[] nums, int k) {
        TreeMap<Integer, Integer> map = new TreeMap<>();
        for (int num : nums) {
            if (map.containsKey(num)) {
                map.put(num, map.get(num) + 1);
            } else {
                map.put(num, 1);
            }
        }
        
//        PriorityQueue<Integer> pq = new PriorityQueue<Integer>(new Comparator<Integer>() { // 比较器在初始化优先队列的定义
//            @Override
//            public int compare(Integer a, Integer b) {
//                return map.get(a) - map.get(b);
//            }
//        });
        PriorityQueue<Integer> pa = new PriorityQueue<>( // 进一步简化
                (a, b) -> map.get(a) - map.get(b)       // 使用lamda表达式
        );

        for (int key : map.keySet()) {
            if (pq.size() < k) {
                pq.add(key);   //pq.add(new Freq(key, map.get(key)));
            } else if (map.get(key) > map.get(pq.peek())) {   //map.get(key) > pq.peek().freq
                pq.remove();
                pq.add(key);   // pq.add(new Freq(key, map.get(key)));
            }
        }
        LinkedList<Integer> res = new LinkedList<>();
        while (!pq.isEmpty()) {
            res.add(pq.remove());   //res.add(pq.remove().e);
        }
        return res;

    }
}

猜你喜欢

转载自blog.csdn.net/weixin_41207499/article/details/80955112