二叉堆和优先队列

优先队列

普通队列:先进先出;后进后出

优化队列:出队顺序和入队顺序无关,只跟优先级相关。

优化队列案例:

  1. 医生做手术,只按伤势来判断,动态选择伤势最严重的病人,而不管伤者来的先后顺序 。

  2. 系统的任务调度 (动态选择优先级最高的任务执行) 。

  3. AI攻击敌人,也是动态去攻击优先级最高的敌人,优先级的定义由开发者定义(距离最近、体积最大、攻击力最高等等)。

底层实现分析

普通线性结构 : 入队 O(1) 级别,出队 O(n) 级别 。

顺序线性结构 : 入队 O(n) 级别,出队 O(1) 级别 。

堆 : 入队 O(logn) 级别,出队 O(logn) 级别 。

二叉堆

  1. 二叉堆是一棵完全二叉树。节点都是从左到右插入进来的,最后一层一定是叶子节点,不满的部分一定是在右边部分。简单来说,就是把元素排列成树的形状,元素从左到右一层层的插入进来,那么元素不够的时候,缺失的部分一定是在最后一层的右边。

  2. 堆中某个节点的值总是不大于其父节点的值。(最大堆)

用数组来存储二叉堆

最大堆的代码实现

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

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

    public MaxHeap(E[] arr) {
        data = new Array<>(arr);
        // 从最后一个节点的父子节点开始处理,少了一半的处理节点数量
        for (int i = parent(data.getSize() - 1); i >= 0; i--) {
            siftDown(i);
        }
    }

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

    /**
     * 向堆中添加元素
     */
    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);
        }
    }

    /**
     * 看堆中的最大元素
     */
    public E findMax() {
        if (isEmpty()) {
            throw new IllegalArgumentException("Can not findMax when heap is empty.");
        }
        return data.get(0);
    }

    /**
     * 取出堆中最大的元素
     */
    public E extractMax() {
        // 先拿到最大的元素值
        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()) {
            // 在此轮循环中,data[k]和data[j]交换位置
            int j = leftChild(k);  // 此时 j 代表左孩子节点
            if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0) {
                // 如果有右孩子且右孩子比左孩子大,则将索引加1,此时 j 代表右孩子节点
                j++;
            }
            // data[j] 是 leftChild 和 rightChild 中的最大值
            if (data.get(k).compareTo(data.get(j)) > 0) {
                break;
            }
            data.swap(k, j);
            k = j;
        }
    }

    /**
     * 取出堆中的最大元素,并且替换成元素e
     */
    public E replace(E e) {
        E ret = findMax();
        // 将堆中最大的元素替换为 e
        data.set(0, e);
        // 下沉操作
        siftDown(0);
        return ret;
    }

    /**
     * 返回堆中的元素个数
     */
    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 not have parent.");
        }
        return (index - 1) / 2;
    }

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

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

底层为最大堆的优先队列

public class PriorityQueue<E extends Comparable<E>> implements Queue<E> {

    private MaxHeap<E> heap;

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

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

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

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

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

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

LeetCode 的题目解答

347.前K个高频元素

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

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

示例 2:

输入: nums = [1], k = 1
输出: [1]

基于自己实现的优先队列来解答

public class Soluction347 {

    public static void main(String[] args) {
        int[] arr = {1, 1, 1, 2, 2, 3};
        System.out.println(topKFrequent(arr, 2));
    }

    /**
     * 基于自己实现的优先队列来处理,复杂度为 O(nlongK)
     */
    public static List<Integer> topKFrequent(int[] nums, int k) {
        TreeMap<Integer, Integer> treeMap = new TreeMap<>();
        for (int num : nums) {
            if (treeMap.containsKey(num)) {
                treeMap.put(num, treeMap.get(num) + 1);
            } else {
                treeMap.put(num, 1);
            }
        }

        PriorityQueue<Freq> pq = new PriorityQueue<Freq>();

        for (Integer key : treeMap.keySet()) {
            if (pq.getSize() < k) {
                // 先入队 K 个元素
                pq.enqueue(new Freq(key, treeMap.get(key)));
            } else {
                // 第 K+1 个元素开始做处理
                if (treeMap.get(key) > pq.getFront().freq) {
                    // 出队最小频次节点(处于树的最顶的节点)
                    pq.dequeue();
                    // 将新节点放进来
                    pq.enqueue(new Freq(key, treeMap.get(key)));
                }
            }
        }

        LinkedList<Integer> linkedList = new LinkedList<>();
        while (!pq.isEmpty()) {
            linkedList.add(pq.dequeue().e);
        }

        return linkedList;
    }

    private static 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 class Soluction347 {

    public static void main(String[] args) {
        int[] arr = {1, 1, 1, 2, 2, 3};
        System.out.println(topKFrequent(arr, 2));
    }

    /**
     * 基于标准库的优先队列来处理,复杂度为 O(nlongK)
     */
    public static List<Integer> topKFrequent(int[] nums, int k) {
        TreeMap<Integer, Integer> treeMap = new TreeMap<>();
        for (int num : nums) {
            if (treeMap.containsKey(num)) {
                treeMap.put(num, treeMap.get(num) + 1);
            } else {
                treeMap.put(num, 1);
            }
        }
        // 在构造函数中传入一个比较器(定义优先级)
        PriorityQueue<Integer> pq = new PriorityQueue<>(Comparator.comparingInt(treeMap::get));
        for (Integer key : treeMap.keySet()) {
            if (pq.size() < k) {
                // 先入队 K 个元素
                pq.add(key);
            } else {
                // 第 K+1 个元素开始做处理
                if (treeMap.get(key) > treeMap.get(pq.peek())) {
                    // 出队最小频次节点(处于树的最顶的节点)
                    pq.remove();
                    // 将新节点放进来
                    pq.add(key);
                }
            }
        }
        LinkedList<Integer> linkedList = new LinkedList<>();
        while (!pq.isEmpty()) {
            linkedList.add(pq.remove());
        }
        return linkedList;
    }

}