数据结构之优先级队列(堆)及top-k问题讲解(二)

简介: 数据结构之优先级队列(堆)及top-k问题讲解(二)

数据结构之优先级队列(堆)及top-k问题讲解(一)+https://developer.aliyun.com/article/1413566

2. PriorityQueue常用接口介绍

1.构造方法

1.1不含参的构造方法
// 不含参的构造方法
    public PriorityQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }
    // 默认容量是11
    private static final int DEFAULT_INITIAL_CAPACITY = 11;
1.2 指定容量的构造方法
// 指定容量的构造方法
    public PriorityQueue(int initialCapacity) {
        this(initialCapacity, null);
    }
1.3指定容量  并接受比较器的构造方法(最核心的一个构造方法)
// 指定容量  并接受比较器的构造方法
    public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator) {
        // Note: This restriction of at least one is not actually needed,
        // but continues for 1.5 compatibility
        // 如果初始容量<1  就抛出异常  但实际上源码中也说了  <1这个条件并不是必需的  只不过是为了和1.5保持一致设置的条件
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
    }
1.4利用其他集合创建一个优先级队列

只要实现了Collection的集合都能作为参数参与创建一个优先级队列

PriorityQueue(Collection<? extends E> c)  用一个集合来创建优先级队列

List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(12);
        list.add(13);
        PriorityQueue<Integer> priorityQueue =
                new PriorityQueue<>(list);
        System.out.println(priorityQueue.toString());// 输出1 12 13

2.PriorityQueue的扩容机制

PriorityQueue的存储结构是顺序表,在不断添加数据的时候涉及到扩容问题,下面来研究一下PriorityQueue中是如何进行扩容的

先来看offer对应的源码

public boolean offer(E e) {
        // 为空 直接抛出异常  不能插入空指针
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
        // 进行扩容
        if (i >= queue.length)
            // grow方法内部是扩容的机制
            grow(i + 1);
        size = i + 1;
        if (i == 0)
            queue[0] = e;
        else  // 向上调整
            siftUp(i, e);
        return true;
    }

接下来看grow方法的实现


3.如何实现大根堆

PriorityQueue默认是小根堆,想要实现大根堆则需要重新构建比较逻辑,使用Comparator接口,下面以整数的比较为例实现大根堆

1.方法一:直接构造一个比较器
// 构造比较器 实现大根堆
        class IntCmp implements Comparator<Integer> {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        }
        PriorityQueue<Integer> priorityQueue =
                new PriorityQueue<>(new IntCmp());
        priorityQueue.offer(1);
        priorityQueue.offer(2);
        priorityQueue.offer(33);
        priorityQueue.offer(44);
        priorityQueue.offer(55);
        System.out.println(priorityQueue.poll());// 输出55
2.方法二:使用匿名内部类
// 使用匿名内部类
        PriorityQueue<Integer> priorityQueue =
                new PriorityQueue<>(new Comparator<Integer>() {
                    @Override
                    public int compare(Integer o1, Integer o2) {
                        return o2.compareTo(o1);
                    }
                });
        priorityQueue.offer(1);
        priorityQueue.offer(2);
        priorityQueue.offer(33);
        priorityQueue.offer(44);
        priorityQueue.offer(55);
        System.out.println(priorityQueue.poll());// 输出55
3.方法三:使用lambda表达式(推荐)
// 使用lambda表达式
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(
                (o1,o2) ->{return o2.compareTo(o1);}
        );
        priorityQueue.offer(1);
        priorityQueue.offer(2);
        priorityQueue.offer(33);
        priorityQueue.offer(44);
        priorityQueue.offer(55);
        System.out.println(priorityQueue.poll());// 输出55

四.重点:top-k问题的三种解决方法

https://leetcode.cn/problems/smallest-k-lcci/description/

1.最简单的思路:对数组进行排序

// 解法1
    public int[] smallestK1(int[] arr, int k) {
        int[] ret = new int[k];
        if(arr == null || k <= 0) return ret;
        Arrays.sort(arr);
        // 排序之后  arr从小到大排列完毕
        for (int i = 0; i < k; i++) {
            ret[i] = arr[i];
        }
        return ret;
    }

2.直接使用堆(优先级队列)的特性  创建小根堆  poll k次即可

// 解法2
    public int[] smallestK2(int[] arr, int k) {
        int[] ret = new int[k];
        if(arr == null || k <= 0) return ret;
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
        for (int i = 0; i < arr.length; i++) {
            priorityQueue.offer(arr[i]);
        }
        for (int i = 0; i < k; i++) {
            ret[i] = priorityQueue.poll();
        }
        return ret;
    }

3.建立一个k个结点的大根堆  再与数组剩余元素进行比较(重点掌握)

步骤:

  1. 先将前k个元素创建为大根堆(k 是因为我最后要返回k个元素)
  2. 将数组中剩余的元素依次去和堆顶元素进行比较,如果小于堆顶元素,插入堆中
  3. 返回容量为k的大根堆

// 解法3  效率最高的一种方法  创建一个具有k个结点的大根堆
    // 注意  优先级队列默认是小根堆  要实现大根堆  需要先创建一个实现了Comparator接口的对象
    class IntCmp implements Comparator<Integer> {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2.compareTo(o1);
        }
    }
    public int[] smallestK(int[] arr, int k) {
        int[] ret = new int[k];
        if(arr == null || k <= 0) return ret;
        // 创建一个具有k个结点的大根堆
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new IntCmp());
        for (int i = 0; i < k; i++) {
            // 将前k个元素做成大根堆
            priorityQueue.offer(arr[i]);
        }
        for (int i = k; i < arr.length; i++) {
            // 去和栈顶元素比较
            if (arr[i] < priorityQueue.peek()) {
                // 证明栈顶元素不是前k个最小的元素  要删除
                priorityQueue.poll();
                priorityQueue.offer(arr[i]);
            }
        }
        for (int i = 0; i < k; i++) {
            ret[i] = priorityQueue.poll();
        }
        return ret;
    }

时间复杂度分析:

前两种方法对于数据量特别大的情况十分不友好!占用的内存太大

变式:

求数组中第k大/小元素

// 求数组中第k大/小元素
    //  前k个元素存储到小根堆中  堆里存放最大的k个元素 以小根堆的形式存储  则堆头一定是第k大的元素
    public int maxKestK(int[] arr, int k) {
        if(arr == null || k <= 0) {
            throw new ArrayEmptyException("不含有元素或k不合法");
        }
        // 创建一个具有k个结点的大根堆
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
        for (int i = 0; i < k; i++) {
            // 将前k个元素做成小根堆
            priorityQueue.offer(arr[i]);
        }
        for (int i = k; i < arr.length; i++) {
            if (arr[i] > priorityQueue.peek()) {
                priorityQueue.poll();
                priorityQueue.offer(arr[i]);
            }
        }
        return priorityQueue.poll();
    }

五.堆排

 堆排序即利用堆的思想进行排序,堆排序的过程可以分为两步

  1. 确定排序方式:升序--建立大根堆  降序--建立小根堆
  2. 利用堆删除的思想(向下调整进行堆排)

图解:

代码实现

// 堆排序
    // 升序  从小到大  创建大根堆
    // 降序  从大到小  创建小根堆
    /**
     * 升序  调整为大根堆  堆首元素一定是最大的
     * 交换堆首和堆尾元素  向下调整(不包含被调下去的最大元素)  使第二大的元素位于堆顶
     * 重复上述操作  每次都是堆首元素和堆尾元素进行交换
     */
    public void headSort() {
        int end = usedSize-1;
        while(end > 0) {
            swap(0,end);
            shiftDown(0,end);
            end--;
        }
    }
    private void shiftDown(int parent, int usedSize) {
        int child = 2 * parent+1;// 得到左孩子的下标
        while (child < usedSize) {
            // 首先要保证child是左右孩子最大元素的下标
            if(child + 1 < usedSize && elem[child] < elem[child+1]) {
                // 有右孩子  且右孩子的值比左孩子大
                child++;
            }
            // 此时child就是值最大孩子的下标
            if(elem[child] > elem[parent]) {
                swap(child,parent);
                parent = child;
                child = 2 * parent+1;
            }else {
                break;
            }
        }
    }

总结:

堆排时要选择好排序的顺序,如果是升序排序就创建大根堆,如果是降序排序就是用小根堆

以升序排序为例,创建一个大根堆存储数据,再不断交换堆顶和堆尾元素(此时一定是最大元素放到堆尾,最小元素放到堆顶),再进行向下调整,向下调整的数据范围并不报错刚刚被挪动到堆尾的元素,使整个堆仍然保持大根堆的性质,这样最大的元素就被移动到最后,依次操作,每次都可以把当前数据范围内的最大元素移动到最后,最后创建出的堆中存储的数据就是升序排序的

这种"逆向思维"在堆中很常见,想让堆顶是最小的元素,就先创建大根堆,再不断地挪动数据,向下调整


目录
相关文章
|
21小时前
|
存储 JavaScript 前端开发
什么是堆?什么是栈?他们之间从区别和联系
什么是堆?什么是栈?他们之间从区别和联系
33 0
|
21小时前
|
存储 程序员
什么是堆,什么是栈
什么是堆,什么是栈
7 0
|
21小时前
【数据结构】二叉树-堆(top-k问题,堆排序,时间复杂度)
【数据结构】二叉树-堆(top-k问题,堆排序,时间复杂度)
17 4
|
22小时前
【数据结构】二叉树-堆(函数实现)
【数据结构】二叉树-堆(函数实现)
13 2
|
21小时前
|
存储 C语言
数据结构:8、堆
数据结构:8、堆
21 0
|
21小时前
|
算法 PHP
堆——“数据结构与算法”
堆——“数据结构与算法”
|
22小时前
|
存储 机器学习/深度学习 算法
初阶数据结构之---堆的应用(堆排序和topk问题)
初阶数据结构之---堆的应用(堆排序和topk问题)
|
21小时前
|
存储 算法
初阶数据结构之---二叉树的顺序结构-堆
初阶数据结构之---二叉树的顺序结构-堆
|
22小时前
|
算法 搜索推荐 数据挖掘
【算法与数据结构】堆排序&&TOP-K问题
【算法与数据结构】堆排序&&TOP-K问题
|
22小时前
|
存储 算法
【算法与数据结构】深入解析二叉树(二)之堆结构实现
【算法与数据结构】深入解析二叉树(二)之堆结构实现